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

|
||||

|
||||
|
||||
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 9× playback. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.8.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
|
||||
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 6× playback with the idle pauses cut. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
|
||||
|
||||
## Download
|
||||
|
||||
- **Windows:** the [installer][windows-msi], or `winget install bherbruck.SolidCraft`.
|
||||
- **Windows:** the [installer][windows-msi].
|
||||
- **macOS:** the [disk image][macos-dmg] (Apple silicon and Intel).
|
||||
- **Linux:** the [AppImage][linux-appimage], or `sudo snap install solidcraft` ([Snap Store][snap-store]).
|
||||
- **Web:** [try it in your browser][web-app], no install.
|
||||
@@ -96,8 +96,10 @@ More commands, the Windows cross-build and the browser build are in [docs/buildi
|
||||
|
||||
`solidcraft-cli mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server. Headless it
|
||||
runs its own design session, or with `--connect` it drives the running app so you can watch.
|
||||
Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Tools, access rules and an
|
||||
example session: [docs/mcp.md](docs/mcp.md).
|
||||
Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. If a terminal doesn't find
|
||||
`solidcraft-cli` (older Windows installers don't put it on `PATH`), give its full path (by default
|
||||
`"C:\Program Files\SolidCraft\solidcraft-cli.exe"`). Tools, access rules and an example session:
|
||||
[docs/mcp.md](docs/mcp.md).
|
||||
|
||||
## Scripts
|
||||
|
||||
|
||||
@@ -1,672 +1,82 @@
|
||||
# SolidCraft roadmap
|
||||
|
||||
Status as of **2026-10-14**. SolidCraft supports parametric modelling (sketch →
|
||||
features → parametric timeline) as an open, pure-Rust, agent-drivable application.
|
||||
The short version, for people who use SolidCraft. The detailed developer status (milestone
|
||||
tables, limitations, measurements) is in [docs/roadmap-detail.md](docs/roadmap-detail.md).
|
||||
|
||||
## At a glance
|
||||
## What SolidCraft is
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| Where are we? | **M0 and M13 done; M1, M2, M5–M7, M9, M10, M14 in progress.** Sketch with solver and the full SKETCH create/modify set; extrude, revolve (two sides, symmetric, to object), fillet/chamfer (variable radius, chord length, a radius per edge, corner patches), holes, threads (cosmetic and modelled), patterns, mirror, shell (inside/outside/both), draft, loft, sweep, coil, split body and split face, delete face, construction planes, axes and points; persistent naming of faces and edges; parameters and configurations; components, joints, motion studies, exploded views; sheet metal and a plastic enclosure subset; STEP/IGES/3MF/STL/OBJ/DXF; FreeCAD/Rhino import; autosave and crash recovery; desktop and browser (wasm) app, CLI, MCP server. |
|
||||
| Features | Every registered command by area — [docs/features.md](docs/features.md) (`cargo xtask features`); registry breadth does not establish workflow or geometric depth |
|
||||
| Reference tests | [Current case count and results](docs/oracle.md): public cases in `tests/oracle/` with analytic or FreeCAD-computed expected values, checked by the reference-test job in [CI](.github/workflows/ci.yml). |
|
||||
| Tests | Solver, profiles, kernel booleans/blends/measures, expressions, timeline, persistent naming, file formats and migrations, assemblies, sheet metal, engine end-to-end, hostile-input fuzz over every command and headless UI scenarios. `cargo xtask ci` reports the current passing counts. |
|
||||
| Gates | `cargo xtask ci`: fmt, clippy -D warnings, tests, asset attribution, layering, wasm32 build — green |
|
||||
| Time to a useful alpha (M0–M6) | ≈ **82 agent hours** remain (sum of the M1–M6 estimates below) |
|
||||
SolidCraft is open-source parametric 3D CAD written in Rust. You sketch, constrain, extrude, and
|
||||
change a parameter to watch the timeline rebuild. It runs as a desktop app and in the browser
|
||||
([try it](https://solidcraft.io/app)), and it has a command-line tool and an MCP server so that
|
||||
scripts and AI agents can drive the same commands you use.
|
||||
|
||||
Optional GPU viewport ambient occlusion is available in View settings and Graphics preferences,
|
||||
with persistent settings, Performance/Quality graphics presets (Quality combines AO, Fine tessellation and adaptive 2× supersampling; manual combinations show Custom), and the
|
||||
`view.ambient_occlusion` and `view.graphics_preset` commands. See [display settings](docs/ambient-occlusion.md).
|
||||
## Works today
|
||||
|
||||
## Scope (owner decision, 2026-10-07)
|
||||
- Sketching with a constraint solver, and the usual create and modify tools.
|
||||
- Extrude, revolve, loft, sweep, coil, fillet, chamfer, shell, draft, holes, threads,
|
||||
patterns, mirror and split.
|
||||
- Parameters with units and expressions, and configurations.
|
||||
- Faces and edges keep their names through edits, so references survive changes.
|
||||
- Components, assemblies, joints, motion studies and exploded views.
|
||||
- Sheet metal with flat patterns and DXF export, and a plastic-enclosure subset.
|
||||
- A Render workspace with GPU rendering, studio lighting, materials and image export
|
||||
(first release in 0.22.0).
|
||||
- Import and export of STEP, IGES, 3MF, STL, OBJ and DXF, and FreeCAD and Rhino files.
|
||||
- Autosave and crash recovery, and the browser build of the same app.
|
||||
|
||||
The issue batch adds preservation of blind pockets in through cuts, associative projected
|
||||
offsets and plastic face placements, XZ (+Y normal) with a v6 migration preserving older
|
||||
designs, interactive angle placement, file-picker sketch imports, Home/recent controls,
|
||||
toolbar overflow, and the remaining component-frame and assembly WIP work. The enclosure
|
||||
acceptance scenarios now include board resizes; STEP reopening is automated. See [WIP reconciliation](docs/wip-reconciliation.md).
|
||||
Face-based Extrude Join (including outward Press Pull) now defaults to the picked body,
|
||||
so edge-touching assembly neighbours stay separate regardless of body order (#226).
|
||||
The full list of commands is in [docs/features.md](docs/features.md). Release notes for each
|
||||
version are on the [releases page](https://github.com/tangency-labs/solidcraft/releases).
|
||||
|
||||
In scope, in priority order:
|
||||
1. SOLID and SKETCH (now).
|
||||
2. Parameters / functional constraints (done: see `parameters.*`, `expr.evaluate`; unit-aware,
|
||||
cycles reported with their path, rename follows references, deletes list their users):
|
||||
user parameters with units and expressions (+-*/, ^,
|
||||
parentheses, sin/cos/sqrt/min/max/floor/ceil/round, unit conversion), model parameters
|
||||
d1, d2… for every dimension and feature input, references between parameters with a
|
||||
dependency graph and cycle detection, re-evaluation on change, comments, favourites,
|
||||
CSV/JSON import and export.
|
||||
3. Components and assemblies with joints (M9).
|
||||
4. SHEET METAL: flange, contour flange, bend, unfold/refold, flat pattern with DXF export, sheet
|
||||
metal rules (thickness, K-factor, bend radius) (M10).
|
||||
5. A small PLASTIC subset for enclosures, after sheet metal: Boss, Rib, Web, Lip/Groove, Snap Fit.
|
||||
## In progress now
|
||||
|
||||
Deferred until further notice: MESH, FORM (T-splines), SURFACE beyond what solids need (scope proposed in #772), PCB / 3D
|
||||
PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
Each feature below is a [milestone](https://github.com/tangency-labs/solidcraft/milestones) holding
|
||||
the issues for it, so the count shows how far along it is. Counts are as of 2026-10-25, one per
|
||||
feature or request. Work merged on a feature branch (`drawings`, `render`) counts as built but
|
||||
reaches the app only when that branch merges. They are rough, not a promise.
|
||||
|
||||
## Milestones
|
||||
- **[Rendering & materials](https://github.com/tangency-labs/solidcraft/milestone/17)**: in progress, 9 of 34 pieces done, 3 of them built only on the `render` branch and in the app when it merges. The basic Render workspace is already in releases (0.22.0); the interface rework (#6281) and the material and light libraries are what is left.
|
||||
- **[Drawings](https://github.com/tangency-labs/solidcraft/milestone/8)**: in progress, 3 of 21 pieces built (2 in releases, 1 on the `drawings` branch), and 3 more partly built there (sheets and borders, bill-of-materials types, the paper scene). Not in the app yet: it lands when the `drawings` branch merges. Today you can export the current view as a DXF.
|
||||
- **[Sheet metal](https://github.com/tangency-labs/solidcraft/milestone/15)**: in progress, 1 of 30 requests done. Flanges, hems, unfold and flat patterns already work; the 30 are extensions, mostly the per-edge flange options (segments, miters, partial flanges, reliefs) and bend allowance.
|
||||
- **[Sketching](https://github.com/tangency-labs/solidcraft/milestone/14)**: in progress, 12 of 50 pieces done. Dimension types, selection and snapping options.
|
||||
- **[Solid modeling](https://github.com/tangency-labs/solidcraft/milestone/2)**: in progress, 9 of 50 pieces done. Blends, holes, threads and patterns.
|
||||
- **[Assemblies & joints](https://github.com/tangency-labs/solidcraft/milestone/3)**: in progress, 4 of 19 pieces done. Joints, motion studies and exploded views already work; placement rules, linked sub-assemblies, fastener stacks and weldment corners are the pieces left.
|
||||
- **[Import & export](https://github.com/tangency-labs/solidcraft/milestone/5)**: in progress, 7 of 39 pieces done. FreeCAD, SolidWorks and point-cloud import, and mesh export options.
|
||||
- **[AI agents & MCP](https://github.com/tangency-labs/solidcraft/milestone/18)**: in progress, 2 of 8 pieces done. Agent chat and the MCP server.
|
||||
- **[Interface, web app & plugins](https://github.com/tangency-labs/solidcraft/milestone/4)**: in progress, 31 of 96 pieces done. Ribbon redesign, navigation, themes, the web app and plugins.
|
||||
- **[SMLib kernel migration](https://github.com/tangency-labs/solidcraft/milestone/1)**: in progress, 1 tracking issue open. The kernel is moving from truck to NVIDIA's SMLib (usd-brep, Apache-2.0) on the `kernel-smlib` branch; it is not in releases yet. Tracked in [#4012](https://github.com/tangency-labs/solidcraft/issues/4012).
|
||||
|
||||
| # | Milestone | Status | Hours left |
|
||||
|---|---|---|---|
|
||||
| M0 | Vertical slice | done | — |
|
||||
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, Shift-picked line midpoints in Horizontal/Vertical using persisted points and ordinary constraints (#2087), driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
|
||||
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
|
||||
| M3 | General fillets and chamfers (curved edges, chains, vertex blends); saddle rolling-ball surfaces now use continuous seam sections and arc-length fitting with shared contact boundaries and bounded accuracy checks; tiny rim edges remain selected instead of nearby wall seams when filleting (#2498) | in progress | 30 |
|
||||
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), FreeCAD/Rhino solids through cadmpeg (done, desktop and CLI), 3MF export and 3MF/STL import as mesh bodies (done), 3MF with B-rep face tags ([docs/formats/3mf-face-tags.md](docs/formats/3mf-face-tags.md), done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
|
||||
| M5 | Persistent naming of faces and edges (docs/book/src/dev/naming.md): faces named from their exact kernel-reported history (sketch curve sides, caps, revolve sections, primitive sides, blends, shell inner walls, pattern/mirror copies; split pieces told apart by the faces around them, lumps of a cut body named after their faces), edges by their faces; fillets/chamfers, sketches on faces, shell/draft/offset/replace faces, hole placements, joint origins and face appearances resolve by name first and warn when the entity split or vanished: done. sheet metal edges by base sketch line and flange side, shell inner faces, sweep and loft roles: done. kernel-reported face history: done (merged faces answer to every name; operations without history yet fall back to geometric names, flagged unverified; format 14 keeps older designs' geometric names). Left: history through sheet metal, plastic, surface and emboss features; FaceSource certificates through history | in progress | 20 |
|
||||
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; responsive feature history with width-driven icons/names/title, snapping width resize, bottom/left/browser-stack docking, shared-column collapse, rollback bar, multi-selection and persistent folders), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator, extrude taper), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
|
||||
| M7 | Thin extrude (walls along open or closed curves: centre, inside, outside or two sides; mitred corners, crossing walls joined; #721), sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
|
||||
| M8 | Surface workspace (#772). Done: Patch (sketch regions; edge loops filled G0, plus `crown`), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane, a face or sketch curves swept onto the surface, keeping a side or the picked regions; mutual trim of two surfaces, extended to meet and stitched, #767), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis, Surface Extrude/Revolve/Sweep/Loft from open and closed curves, Offset, Unstitch and Reverse Normal; the continuity-driven core (`kernel/src/bridge.rs`, G0–G3 Hermite rows) with **Blend** (`surface.blend`, #764: edge chains along one or several faces, closed rims paired seam to seam or from picked seams, a dialog), **Match** (`surface.match`), **Boundary** (`surface.boundary`, #763 — two sketch curves exact with G0–G3, three or four as the flat patch of their loop, and curve networks: Coons and Gordon surfaces through sketch, 3D-sketch or edge curves, a point for a three-sided side, G1/G2 against the faces along one direction's edges, joined into their body on request; G1/G2 on two sides that meet at a corner waits for the Gregory-type corner), **Fair** (`surface.fair`, CV fairing), **continuity analysis** (`inspect.continuity`, #770; a toolbar tool with edge markers and a legend, and curvature combs along face isocurves) and a multi-section **loft** with per-end continuity and crown, and guide rails on solid and surface lofts (one or two, scaling or stretching the sections between; #201). Also in the Class-A pass: G1…G4 blend curves in the sketch, chord and variable-radius fillets on curved edges (with honest refusals where the fit cannot hold), and a G2 corner patch where filleted edges meet (kernel; the command refuses until the patch's sections can be sewn to the blends' contacts). Left: bias/form-factor fillet sections (design recorded), a Gregory-type corner, surface Fillet between separate surface bodies (#769), Ruled (#762), Untrim (#761), curved-face Extend (#760), and every remaining #757–#772 item | deferred | — |
|
||||
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, closed kinematic loops (four-bar, slider-crank: the loop's other joints follow the driven one on its branch, and driving stops where a loop can no longer close), cams (`joint.cam`: a revolute cam drives a slider or revolute follower along a closed sketch profile or the cam's edges, knife, roller or flat followers, worked out again when the profile changes; drives, studies, closed loops, contact sets), As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), motion analysis (joint values and a point's path charted over a study as value, velocity and acceleration; the parts that collide along a study, with the steps where they do), Exploded views (automatic or by hand, shown without changing placements); interference, per-component physical properties; STEP with assembly structure; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link, Cam and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
|
||||
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
|
||||
| M11 | 2D drawings | deferred | — |
|
||||
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. Trusted distribution signing and broader performance work remain | in progress | 16 |
|
||||
| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (config.configure, config.table, config.column/row/activate). Per-row body materials and appearances (library looks, custom colour/opacity and clearing overrides), undoable activation and save/reopen: done. Ordered typed configuration rules, atomic validation and structured rule editing: done. Component appearance and certified semantic face style columns, atomic preview/apply and independent-copy remapping: done. Exact constructed sketch-side and immutable imported-face lineage now preserve legacy names and reject retargeting. Left: ambiguous shifted/two-sided caps | in progress | 4 |
|
||||
| M13 | Never lose work: atomic saves (temporary file, flush, rename; a crash at any moment leaves the old file or the new one) with the replaced version kept as `<file>.bak`; autosave of unsaved changes to the recovery folder every 5 minutes (preference) and after big operations, only when something changed; "Recover unsaved design?" on the next launch (`doc.recovery_list`, `doc.recover`, `doc.recovery_discard`); a kill-mid-save test (process killed while saving a 0.5 MB design 25 times). File format: `"format": "solidcraft/12"`, older formats upgraded step by step (`doc::format`), newer ones refused with a message, out-of-range numbers and broken placements refused; a corpus of designs saved by M0, components-era, joints-era and current builds (apps/solidcraft-cli/tests/corpus) that must open and evaluate as saved; damaged and hostile variants (truncation, bit flips, hostile values, removed keys, repeated features, deep nesting) never panic | done | 6 |
|
||||
## Next
|
||||
|
||||
M6 navigation preferences include **Touchpad**: Shift + pointer movement pans, Alt/Option +
|
||||
pointer movement orbits without a mouse button, and vertical scrolling zooms. Other presets
|
||||
retain their saved two-finger scroll preference; camera motion is available through
|
||||
`view.navigate` and modifier-aware `ui.move` automation.
|
||||
- **[Surfacing](https://github.com/tangency-labs/solidcraft/milestone/16)**: next, 6 of 14 pieces done. Patch, trim, blend and match exist; the rest of the surface toolset is planned.
|
||||
- **[Manufacturing / CAM](https://github.com/tangency-labs/solidcraft/milestone/9)**: next, 1 pieces tracked, none done. There is no CAM in SolidCraft yet. Send to Slicer and a first toolpath slice are in review; proposals are [#1153](https://github.com/tangency-labs/solidcraft/issues/1153) and [#1621](https://github.com/tangency-labs/solidcraft/issues/1621).
|
||||
|
||||
UI redesign (#2016): the File/start view (#2027) uses shared light/dark navigation,
|
||||
action tiles, recent rows and sample cards, with recovery review and Save As/Export
|
||||
entry points. Document loading, sample caches and recovery safety retain their
|
||||
existing command paths.
|
||||
Features are not tied to a release or a date. Work lands in the daily releases as it is finished.
|
||||
Anything without a milestone, such as bug fixes, is not a feature and is not listed here.
|
||||
|
||||
M11 precursor: File → Export Current View as DXF exports shown placed bodies and sketches from the current
|
||||
camera as a 2D millimetre drawing, with hidden edges omitted or dashed. Vector occlusion splits
|
||||
partly hidden edges and includes curved silhouettes. Repeated part meshes and triangle
|
||||
hierarchies are shared; bounded desktop preparation and occlusion use up to eight workers.
|
||||
Curves are approximated; drawing sheets,
|
||||
annotations and exact projected curves remain deferred. See [view DXF](docs/view-dxf.md).
|
||||
## How releases work
|
||||
|
||||
Drawing foundations: [RFC #860](https://github.com/tangency-labs/solidcraft/issues/860)
|
||||
and the [foundation ledger](docs/drawing-foundations.md) track the bounded implementation
|
||||
packages. The first geometry increment defines validated section records in a plane-local
|
||||
frame; the ledger separates that contract from section producers and the Drawing workspace.
|
||||
- Releases ship daily, as numbered versions (v0.22.0, v0.22.1) with nightly pre-releases in
|
||||
between. They are delivered through the in-app updater.
|
||||
- SolidCraft tells you when a newer stable release exists and never updates without asking.
|
||||
Where it can replace itself safely (AppImage, Windows installer and portable builds) there is
|
||||
an **Update now** button; downloads are checked against the release's SHA-256 list. See
|
||||
[updates and privacy](docs/book/src/guide/updates.md).
|
||||
- Design files carry a format number. A new version opens designs from older versions, but an
|
||||
older version refuses a design saved by a newer one (0.22.0 writes format 35, and 0.21.0 and
|
||||
earlier will not open it). Each release's notes say when the format changed. Keep a copy of
|
||||
an important design before you move to a new version.
|
||||
|
||||
Desktop view DXF export runs in the file-operation worker with five-stage progress and
|
||||
cooperative cancellation. Curve tolerance (0.0001–1 mm, default 0.01 mm) is remembered alongside
|
||||
the hidden-edge setting. Cancelled exports preserve the model and the previous destination;
|
||||
completed drawings publish atomically. The browser still exports synchronously.
|
||||
## Licence
|
||||
|
||||
## Interactive modelling
|
||||
SolidCraft is licensed MIT OR Apache-2.0, at your option; see [LICENSE-MIT](LICENSE-MIT) and
|
||||
[LICENSE-APACHE](LICENSE-APACHE). Versions already released stay available under those licences
|
||||
permanently.
|
||||
|
||||
- Modelled threads preview their cut before OK using the background worker on desktop.
|
||||
Browser users apply the cut with OK to avoid blocking on every edit.
|
||||
## How to follow along
|
||||
|
||||
- Thread resolves a suggested metric size when a cylindrical face is picked. Diameter and
|
||||
pitch selectors expose the existing metric designation parameter; custom designations stay
|
||||
editable, and Automatic size returns to the face's suggestion. Cosmetic/modelled and
|
||||
full/partial-length options remain available. This does not add tolerance classes or
|
||||
left-hand threads.
|
||||
- New Body extrusion and face-offset previews show completed intermediate geometry while a
|
||||
drag continues, then catch up to the exact final value. Changes to the selected geometry,
|
||||
operation or feature still discard obsolete results. Distance arrows and their value boxes
|
||||
follow the current frame's input.
|
||||
- Large fine modelled threads use pitch-based helix resolution and tighter faceted
|
||||
reconstruction fallbacks, preserving thin crests and closed shells on large shafts.
|
||||
|
||||
## Physical material data
|
||||
|
||||
- Physical material card validation (#923/#922) provides typed SI mechanical and
|
||||
thermal data, per-property sources/conditions and isotropic/orthotropic stability
|
||||
checks through read-only `material.properties.validate`. It does not assign or
|
||||
persist properties, provide a solver or enable rendering; see
|
||||
[the contract and staged follow-ups](docs/physical-material-cards.md).
|
||||
|
||||
- Directional elasticity (#994) queries the validated card in material-local axes
|
||||
through read-only `material.properties.directional`, with source/condition review.
|
||||
It does not assign body frames, predict structural behavior or enable a solver.
|
||||
|
||||
## Library foundations (#785, #786, #787)
|
||||
|
||||
- Local scalar libraries (#846) inspect/export bounded portable catalogs and copy exact
|
||||
selected appearance/material revisions into existing design definitions in one undo step.
|
||||
Appearance offers local loading/search/export; physical materials use existing commands.
|
||||
This slice is independent of the render stack and does not change the document format.
|
||||
See [the contract and limits](docs/book/src/dev/local-libraries.md).
|
||||
|
||||
- Proposed phases cover offline bundled/project/user packs, physical materials and
|
||||
appearances, editable wood mapping, and parameterized component/assembly families.
|
||||
Stable identity/revision, copied design definitions, provenance and resource limits
|
||||
apply from the first implementation. Existing material, appearance and standard-part
|
||||
commands remain the application paths. See [the proposal](docs/book/src/dev/libraries.md)
|
||||
for source/license review and acceptance gates. This is planning only: pack loading,
|
||||
texture rendering and expanded component families are not implemented by this entry.
|
||||
|
||||
## Render foundation
|
||||
|
||||
- Explicit texture normalization (#1218) produces completed float RGBA with
|
||||
role-aware linear color/data semantics, bounded matrix/TRC ICC conversion and
|
||||
opt-in EXIF orientation. It retains high-precision raw samples and records lossy
|
||||
gamut clipping only when explicitly requested. It is not a renderer/resource
|
||||
consumer; see [the supported policy and limits](docs/render-texture-normalization.md).
|
||||
|
||||
- Opt-in opaque PBR (#1046) connects persisted surface parameters to the integrated
|
||||
product viewport and CPU PNG, using a fixed direct-light studio. Geometry and mass
|
||||
stay unchanged. HDRI, transmission and textures remain separate gates.
|
||||
See [the consumer and limitations](docs/render-opaque-pbr.md).
|
||||
|
||||
- Combined integration is tracked in #842, with independent scalar libraries in
|
||||
#863 and richer PBR/material-card transport in #1086. See [phase boundaries and validation gates](docs/render-integration.md).
|
||||
Render/library consumers use the merged asynchronous picker (#1127). Material-card
|
||||
UI (#1094), texture mapping and HDRI lighting remain explicit follow-ups.
|
||||
|
||||
- R4a adds bounded CPU product-image jobs, cancellation and atomic PNG export through
|
||||
engine commands, the Render panel, CLI and persistent MCP. Immutable captures and
|
||||
results stay outside persistence/undo; native workers and cooperative browser tasks
|
||||
share the same work protocol. See [jobs, limits and export](docs/render-jobs.md).
|
||||
|
||||
- R3 adds an overlay-free product scene and the integrated Render workspace, with
|
||||
runtime GPU host capabilities, per-document drafts and named camera/background
|
||||
controls. Design keeps its camera and drawing policy. Raster transparency remains
|
||||
approximate; PBR and environments are later slices. See
|
||||
[workspace and scene contracts](docs/render-workspace.md).
|
||||
|
||||
- R2 adds bounded named render setups in document format 21 and the read/save
|
||||
`render.settings.get/set` commands. Proven render-only changes preserve geometry
|
||||
and prepared world data through undo/redo and checkpoint restore. This slice does
|
||||
not add a Render workspace, PBR, environments or image jobs; see
|
||||
[settings and refresh policy](docs/render-settings.md).
|
||||
- Bounded PNG/JPEG texture admission (#900) validates in-memory sources and returns
|
||||
completed RGBA8 pixels with explicit color/data roles. It is an input utility only:
|
||||
ICC/16-bit normalization, local wood mapping, resource persistence and the renderer
|
||||
consumer remain separate work. See [limits and licensed-file probes](docs/render-texture-input.md).
|
||||
|
||||
- The initial opaque surface numerical contract for #1004 provides bounded linear
|
||||
color/metallic/roughness/IOR admission and an original tested direct-light BRDF.
|
||||
The numerical module alone advertises no capabilities; the installed consumer above
|
||||
provides rendering. See
|
||||
[the model, evidence and remaining consumer gates](docs/render-surface.md).
|
||||
|
||||
- A bounded cooperative Radiance RGBE input utility is available in L1 render;
|
||||
it does not yet enable HDRI/PBR rendering or a document import command. See
|
||||
[the input contract](docs/render-hdr-input.md) and #813 for integration work.
|
||||
- R1 adds renderer-independent backend/profile metadata, strict request resolution and
|
||||
bounded image/scratch admission checks, exposed by the read-only `render.backends`
|
||||
command. It describes the current CPU raster snapshot path without evaluating the model.
|
||||
The legacy Design snapshot route remains unchanged. Later slices above provide the
|
||||
workspace/jobs and opaque PBR; HDRI and optional engines remain future work. See [the contract](docs/render-backends.md)
|
||||
and [the phased proposal (#620)](https://github.com/bherbruck/solidcraft/issues/620).
|
||||
|
||||
## Dimensioning and error reporting
|
||||
|
||||
- MCP `execute` accepts `verbose: false`, sharing the compact reply helper used by
|
||||
`batch`: body changes and created sketch IDs stay, while repeated lists are omitted.
|
||||
Both tools retain full replies by default.
|
||||
|
||||
- Sketch Create groups drawing variants under Line, Rectangle, Circle, Arc, Slot,
|
||||
Polygon and Spline. Each open part remembers its last interactive choice per family
|
||||
across sketch sessions and document-tab switches; closing the part resets these UI choices.
|
||||
- Inscribed, circumscribed and edge polygon tools preview their actual outline before
|
||||
placement, including orientation and the selected side count.
|
||||
- Construction (X) preserves the active sketch drawing tool, picked anchors and inline
|
||||
dimension values, so line type can change while a shape is in progress.
|
||||
- Planar sketch construction previews and hover feedback keep the same dashed pattern as
|
||||
committed geometry; centerlines retain their dash-dot pattern.
|
||||
|
||||
- Point has a default Sketch CREATE toolbar button alongside the existing drawing tools.
|
||||
Saved custom layouts are preserved; Reset Toolbar restores the default Point button.
|
||||
- Construction planes cannot take an origin-plane name (XY, XZ, YZ, Top, Front, Right). Plane
|
||||
arguments reject such a name when an older design's construction plane already has it. Use
|
||||
the other alias, the construction feature's numeric ID or an explicit plane; existing
|
||||
stored references keep their saved meaning. Section Analysis uses the same check.
|
||||
- Sketch text can be edited from a letter interior or outline's context menu or the Edit Text command.
|
||||
Interior selection requires every profile boundary to belong to the same text source;
|
||||
the editor highlights the whole source without replacing profile selections.
|
||||
Content, baseline X/Y, height and rotation update the original text source in one undo step;
|
||||
cancelling or rejecting an invalid value preserves the sketch. Position and height fields
|
||||
use the document units captured when editing starts; generated outlines remain associated
|
||||
with their text source, and untouched inputs preserve their saved internal values.
|
||||
Dragging a text outline moves the entire source in one undo step; Escape restores the
|
||||
gesture. Move/Copy on a selected text outline provides unit-aware X/Y distances and rotation
|
||||
about its baseline; copies keep an independent editable text source.
|
||||
- Repeating the initial point of a planar multi-point sketch drawing tool is neutral: the tool
|
||||
waits for a distinct point without a degenerate-shape error or history entry. Typed input
|
||||
and programmatic commands retain their validation.
|
||||
- Circular-pattern and angled-plane axes reject explicitly supplied malformed or expression
|
||||
origins instead of silently rotating about zero. Numeric origins remain in millimetres;
|
||||
associative axis expressions and construction-axis references remain unsupported.
|
||||
- Single-line and rectangle-side dimensioning supports selecting a line or its two endpoints.
|
||||
Type a value and press Enter to commit one undo step; rejected values remain editable and
|
||||
cancellation leaves the sketch unchanged. Fit keeps geometry clear of the Sketch Palette
|
||||
and navigation controls, including enlarged displays and nested pending placements.
|
||||
- Sketch Dimension takes a line and a circle or arc (#1803): it measures from the line to the
|
||||
circle's edge (the gap, 0 when tangent, or how far the edge reaches past a line that cuts
|
||||
it), drives it like any other dimension and keeps the circle on its side of the line.
|
||||
- Sketch annotations, live drawing lengths and Measure use document units. Inches show
|
||||
three decimals and retain a meaningful fourth digit. Bare length dimension entry captures
|
||||
the current design unit; changing units leaves existing geometry and expressions intact.
|
||||
Component/canvas distance controls, sketch cursor coordinates, flat-pattern dimensions
|
||||
and pocket depths, interference volumes and parts-library lengths also use design units.
|
||||
- Curvature and Polygon constraints accept complete preselected operands. Fix/Unfix
|
||||
consumes complete point/curve selections consistently; mixed or ambiguous selections
|
||||
retain the picking workflow. Rejected constraints preserve selection and undo history.
|
||||
- Contextual error reporting: a **Create Issue** action beside feature errors opens a
|
||||
reviewable GitHub issue draft with the error, operation settings, build diagnostics and
|
||||
useful reproduction context. The draft is editable, with optional capture of the actual
|
||||
viewport and copy/save controls. Opening GitHub does not submit the issue. Help also exposes
|
||||
reporting for unexpected results that did not produce an error.
|
||||
- The header exposes **Report a bug**, including an accessible compact control on
|
||||
narrow windows. It opens the same editable report and screenshot review without
|
||||
changing the active model or a pending dimension.
|
||||
- Crash reports: every panic (in a command, in the kernel, anywhere else) and every unclean
|
||||
exit (abort, stack overflow, kill: found at the next start through a locked per-run command
|
||||
journal) leaves a local report with the backtrace, version and commit, the command and the
|
||||
design that reproduce it (`solidcraft-cli crashes replay <id>`). Repeats are counted, not
|
||||
rewritten; at most 20 reports, 50 MB and 90 days. The app shows them after a crash (Help >
|
||||
Crash Reports…) with a reviewed issue draft; agents read them through
|
||||
`diagnostics.crash_reports`, the MCP `crash_reports` tool or the CLI. Nothing is sent
|
||||
anywhere. See [crash reports](docs/crash-reports.md).
|
||||
- Agent chat (off by default, desktop): a side panel that drives a coding agent the user
|
||||
already has (Claude Code through its ACP adapter, Codex, Gemini CLI, OpenCode, Grok Build, …) over the
|
||||
Agent Client Protocol, under the user's own login; SolidCraft stores no keys. The agent works
|
||||
through SolidCraft's MCP tools on a gated endpoint of its own: file actions wait for Allow or
|
||||
Deny, crash reports are off limits, 24 changes per message, one undo step per message, and
|
||||
the agent's whole process group ends with the panel or the app. Docks left or right,
|
||||
minimises to a strip. Grok Build discovery includes `~/.grok/bin` and reuses the user's
|
||||
Grok login. See [agent chat](docs/agent-chat.md).
|
||||
- Command scripts from designs (#886): `timeline.export_script` (CLI `solidcraft-cli script`)
|
||||
writes a design's history as a script that `eval` and `run` replay into the same design —
|
||||
user parameters, units, sheet metal and plastic rules and materials as their own commands,
|
||||
then each feature through `timeline.add` with its ids, names and dimension parameters kept,
|
||||
so every feature kind replays exactly and a failing feature fails the same way. Trimming the
|
||||
script after a step gives the design at that step. Appearances, folders, views and
|
||||
configurations are listed as left out; components and joints are not supported yet.
|
||||
|
||||
## 0.2.0 release batch
|
||||
|
||||
- Includes the new axis navigation, adaptive sketch drawing/picking, origin-corner repairs,
|
||||
durable supported profile/face references, endpoint dimensioning and contextual reporting.
|
||||
- Smooth ruled-wall display joins are hidden only within the supported geometric certificate;
|
||||
authored splits and actual creases remain protected. Wireframe retains physical edges.
|
||||
- Paste New uses a bounded current-geometry comparison for supported convex line/circle
|
||||
prisms with separated circular holes; unsupported shapes retain the existing strict check.
|
||||
- Common-bore Union retries check additional boundary curves before recutting the bore,
|
||||
preventing the reproduced thin-web loss. Unknown additional trims refuse; inherited
|
||||
common-cylinder trims retain their existing policy, so this is not a general Boolean proof.
|
||||
- Exact full-path assembly joint editing remains separate work and is not part of this batch.
|
||||
- Stable 0.2.0 is published: all release payload checksums, native cold reopening, deployed
|
||||
web app and both Flatpak architectures were verified. The replacement README demo uses
|
||||
the original electronics enclosure story and animated pointer in the actual English
|
||||
0.2.0 UI, at 4× playback. Publication follows [the release workflow](docs/releasing.md).
|
||||
|
||||
## 0.2.1 critical fixes
|
||||
|
||||
- Separated multi-profile Join/Cut extrusions into one solid target batch their tools using
|
||||
conservative construction bounds, avoiding a Boolean rebuild for every profile. Live
|
||||
extrusion previews build only their tools; confirmation validates the final Boolean.
|
||||
- Coplanar selected body faces share one linked sketch and one extrusion. Automatic Join/Cut
|
||||
keeps the evaluator's participant scope, including parts of an empty imported component. Desktop OK/Enter
|
||||
computes on a scratch session in the background, with atomic acceptance and cancellation.
|
||||
Planar section stitching projects lines/arcs analytically and removes duplicate endpoints.
|
||||
- Imported rotated face groups can check all full start-face seams in one guarded
|
||||
construction. Separated tools may batch a Join when each meets the same single body;
|
||||
nearby uninvolved bodies retain the original contact decisions. Regression comparisons
|
||||
cover all four extrusion operations.
|
||||
- Inward multi-face cuts retain every eligible target and subtract separated tools together.
|
||||
Safe interior classification handles roundoff at shared imported line/arc endpoints.
|
||||
Straight extrusion feedback caches existing face/profile triangles and updates the display
|
||||
directly, including a newly opened STEP's single identity component placement; other
|
||||
extents, moved or repeated components and curved sketch profiles keep the Engine preview.
|
||||
- The extrusion fixes retain tracked Boolean history and the current `solidcraft` packages.
|
||||
Confirmation and tool previews use main's shared background calculation. Main's collapsed
|
||||
blend-wall refusal and its tests are retained, including smaller valid radii and mirrored parts.
|
||||
- Axis view and navigation controls stay anchored to the viewport when feature dialogs or
|
||||
the command palette open, at normal and enlarged display scales. Fit still accounts for
|
||||
dialog coverage; covered controls do not receive clicks through dialogs.
|
||||
- Eligible planar faces survive fillet, chamfer and shell operations with exact source
|
||||
certificates. Sketches placed directly on the enclosure wall and inner floor follow
|
||||
width and wall changes, including after the USB cut and cold reopening.
|
||||
- Remove artificial straight-wall and rounded-corner subdivisions left by shell and
|
||||
extrusion cuts. Identical generated wall segments span adjacent slabs as one physical
|
||||
face, with exact full-height supports and boundary stitching. Preserve authored face
|
||||
splits and face appearances. Permanent regressions cover material, STEP, reopening and
|
||||
unaffected bodies across the extrusion end plane.
|
||||
- Feature dialogs use the available screen height for their fields, with OK and Cancel
|
||||
outside the scrolling region, including enlarged display scales.
|
||||
- Design format 13 records shell-face attachments. Older designs remain readable;
|
||||
newly saved designs require 0.2.1 or later. Ambiguous or unsupported faces still refuse
|
||||
attachment rather than guessing a plane.
|
||||
- Saved projections from a removed strip retain their geometry and report the lost
|
||||
reference. Explicitly reselect the wall to repair that projection.
|
||||
|
||||
## Workspace layouts (#2029)
|
||||
|
||||
- General preferences save Floating or Compact using the universal UI containers.
|
||||
Compact joins the workspace selector and ribbon, docks the content-hugging Browser
|
||||
flush beneath them with a square join, and opens tool sheets below the compass.
|
||||
Sheets remain draggable, rounding all corners away from their right dock; footer
|
||||
backgrounds follow the same corners. A separate preference hides ribbon icon labels
|
||||
for a shorter ribbon in either layout.
|
||||
Assistant keeps the outermost edge and History keeps its chosen dock. Switching
|
||||
retains the active tool, camera, selection and design; system windows and menus
|
||||
keep their existing behavior.
|
||||
|
||||
## View navigation
|
||||
|
||||
- Navigation has an opt-in Free orbit preference for mouse and 3D-mouse rotation through
|
||||
both poles with roll. It defaults off, preserving existing preset snapping and pole limits.
|
||||
The SolidWorks face menu offers Normal to Selection (`view.normal_to`), shared with Look At
|
||||
and Ctrl+8, including repeat-to-flip, preserving scale and projection.
|
||||
|
||||
- Desktop Ctrl/Cmd +/- and 0 change or reset the saved UI scale. The scale slider keeps
|
||||
its layout stable during a drag and applies the selected scale on release.
|
||||
|
||||
- OpenSCAD navigation preset: left drag orbits, right/middle drag pans, Shift+right/middle
|
||||
drag zooms, and the wheel zooms. Available in the shared desktop/web Navigation preferences.
|
||||
- Compact clickable signed X/Y/Z axis tips, smooth drag-to-orbit, and Shift-orbit snapping
|
||||
to 26 standard and diagonal views. Pointer motion accumulates independently of a lightly
|
||||
damped spring, with a boundary margin to prevent flicker. Z-up/Y-up and the nearest square
|
||||
top/bottom heading are preserved. Diagonals include 45-degree edge and isometric corner views.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Scheduled and manual CI scan locked dependency advisories, bans and sources with
|
||||
cargo-deny. Vulnerabilities fail the scan; unmaintained crates warn. Local vendored
|
||||
patches require separate upstream review because advisory databases cannot version-match
|
||||
their path dependencies. Pull requests retain the existing licence checks.
|
||||
|
||||
- Associative shell-face sketches require a unique certified planar source. Nested shell
|
||||
attachments and ambiguous coincident sources remain unsupported.
|
||||
- G2 fillets: exact quintic blends of straight edges between planar faces, with one planar
|
||||
end face at each end, including sloped ends (#2708). The UI and `solid.fillet` expose
|
||||
`curvature` (alias `g2`); the size is an equivalent circular setback. Curved faces/edges,
|
||||
multi-face ends and corner junctions remain unsupported.
|
||||
- Fillets and chamfers: constant, variable-radius and chord-length fillets; a radius per edge,
|
||||
rounds next to earlier rounds of another radius, and smooth corner patches where three rounds
|
||||
meet (a sphere for equal radii); perpendicular corner fillets after a closed ring of earlier
|
||||
rounds preserve the ring's radii and the enclosed planar face (#1557);
|
||||
mitres between separate rounds of the same radius keep their endpoint tangent, so a later
|
||||
fillet can select an upright and its adjoining bottom mitre as one chain (#2422);
|
||||
straight edges (convex or concave) between planar faces with perpendicular
|
||||
planar end faces (equal-distance chamfers also meet sloped planar end faces, including inner
|
||||
basin rims within the rim width); convex fillets also meet sloped planar ends and reflex
|
||||
corners around pocket rims, with unequal wall slopes and extensions confined to the face;
|
||||
connected equal-distance chamfers also trim each other across unequal sloped rim walls;
|
||||
curved chamfer chains with straight planar ends can terminate across two existing planar
|
||||
bevel faces, splitting their shared seam; picks on the same tangent chain are applied once (#1303);
|
||||
whole smooth loops of a planar face, picked whole or by one edge (tangent
|
||||
chain), including circles and walls at any angle (boss bases and tops, bores, pocket floors,
|
||||
plate outlines), loops with sharp corners between straight or mixed arc/line edges (straight
|
||||
blends meet in mitres; perpendicular curved walls use offset intersections, with D-rim removal
|
||||
checked within 0.1% of analytic offset slices), runs of
|
||||
neighbouring edges along a planar face's loop (two top edges of a box: mitred, ending square;
|
||||
with the edge between them too, the corner becomes a sphere octant),
|
||||
rounded corners rounded again (sphere octants, tori at concave corners); every edge of a
|
||||
convex planar body or of an extruded part; fillets inside patterns; closed chains of curved
|
||||
edges between any two smooth sides (a branch pipe on a main pipe), by a rolling ball (the
|
||||
blend a rational B-spline through exact arcs), closed or ending at planar faces square to the
|
||||
edges, where straight or curved cap edges are trimmed to the contacts (the straight edge
|
||||
between two overlapping extruded arcs); knife edges, where the two faces have opposite
|
||||
normals (a wall that runs out tangent into a face: the ball that fits there); an edge that
|
||||
runs out along an end face tangent to one side (the arc top edge of that wall), cut where the
|
||||
blend meets it; rounds that end in an earlier round on curved faces, in any order (the rounds
|
||||
of two edges at a vertex meet in a crease, as on a box), picked together or one after the
|
||||
other, except a round after a round of the edge between the wall and a flat face that it
|
||||
would run over; the three edges at a vertex of any smooth faces, of one radius, end in a
|
||||
piece of sphere. A limiting blend that collapses a side wall into coincident boundary
|
||||
curves is refused as too large rather than returning an unmeshable face (#1113).
|
||||
- Booleans: prisms along one direction (plates with holes, slots, patterns of them, equal
|
||||
sections stacked end to end) are combined exactly in 2D. Qualified axial cuts preserve
|
||||
annular holes with overrun cutters, including inward Press Pull after a joined extrusion;
|
||||
curved wall qualification rejects false transverse axes instead of filling holes.
|
||||
Bodies touching along one whole face
|
||||
(a part and its mirror image) are stitched; coincident planar faces are pushed apart when
|
||||
their neighbours are planes or cylinders. Other coincident curved configurations can still
|
||||
fail. Bounded containment classification preserves fully internal cavities and nested
|
||||
material islands independently of shell order, with STEP round-trip and material-membership
|
||||
regressions. Near-contact curved shells use the general boolean path rather than a
|
||||
containment shortcut. Spheres are built from six pole-free patches.
|
||||
- Mass properties come from fine tessellation (curved faces within ~1e-4 relative); triangles
|
||||
that cut through curved faces (trimmed faces after booleans) are split onto the surface first.
|
||||
- STEP import (our own reader, crates/kernel/src/step_in) reads solids (B-rep with analytic and B-spline geometry), units,
|
||||
product names, colours and assemblies; analytic offsets on planes, cylinders, cones, spheres
|
||||
and regular tori are read exactly (signed/nested distances, units and face sense). Freeform
|
||||
offset surfaces and pcurve-only edges are not read yet,
|
||||
and assembly components are flattened into bodies. Closed periodic faces (torus, B-spline
|
||||
bands) are split in two for meshing; measure() uses a finer tolerance for small radii.
|
||||
- Persistent naming follows the kernel's exact face history (docs/book/src/dev/naming.md):
|
||||
references resolve by name first, follow splits and merges, and warn when the entity split,
|
||||
merged or vanished, or was found by geometry only (operations that report no history yet).
|
||||
New planar sketch attachments use exact box, extrusion cap/side and immutable imported-face
|
||||
source certificates with checked target and author occurrence paths. A missing or changed
|
||||
source fails explicitly; Redefine Sketch Plane previews reattachment or a fixed plane while
|
||||
retaining current geometry and dimension identities. Unsupported modified faces need an
|
||||
explicit fixed plane. Legacy attachments retain their original resolver behavior.
|
||||
- Modelled threads are cut as a faceted body (curved faces within 1/200 of the pitch); Extrude
|
||||
and Revolve To Object on a curved face cut the tool back to the target's surface.
|
||||
- Explicit sketch breaks preserve exact ellipses, fit/control splines (including high degrees)
|
||||
and conics; analytic trim/break also sees free-form cutters. Free-form curves cut at a tangent
|
||||
touch (no clean crossing) enter that profile as polylines.
|
||||
Sweeps along 3D sketch curves follow their polyline (smooth walls when the path is smooth).
|
||||
A sweep with a twist, an end scale or a parallel or direction orientation (#771) follows its
|
||||
path that way too, even along sketch lines and arcs: B-spline walls through stations, ruled
|
||||
walls through denser stations where the path has corners or the profile has holes.
|
||||
Guide rails (one or two, #771) do the same: each station meets every rail once, and the rail
|
||||
turns the profile and scales it (or stretches it across to the rail) against the start; two
|
||||
rails carry it between them. Rails along a path with sharp corners are refused for now.
|
||||
- Extend intersects exact ellipses and rational/high-degree splines without moving the line or
|
||||
circular support. Associative free-form offsets have a whole-span 0.002 mm normal-distance
|
||||
certificate and refine within bounded node limits while retaining existing IDs.
|
||||
At the node limit, geometric certificate subdivisions accommodate unequal spline parameter
|
||||
speeds without changing stored nodes or relaxing the distance tolerance (#971).
|
||||
Associated offset fit nodes stay hidden from display, picking and snapping; dragging the
|
||||
editable source updates the derived boundary live. Break Association restores point editing.
|
||||
Singular, self-intersecting and ambiguous curves refuse atomically; mixed junctions and nonsmooth
|
||||
closed natural-spline seams remain limitations. New explicitly closed fit splines and closed
|
||||
offset outputs use periodic C2 interpolation, supporting repeated certified offsets; older
|
||||
natural splines are preserved. Clicking the starting fit point deliberately closes a new loop.
|
||||
Newly captured adaptive profile references with complete source coverage retain their outer
|
||||
and hole identities through span-count changes, including parameter edits after extrusion.
|
||||
Partial, intersected or ambiguous boundaries do not qualify for these whole-source references.
|
||||
Legacy curve-span multisets remain exact and unchanged; already-lost selections may need
|
||||
reselection. Format 12 stores the new bounded whole-boundary references.
|
||||
Projected analytic boundaries follow nested loop identities through resizing, including
|
||||
stationary holes and rounded corners. Supported convex, tangent line-and-arc boundaries
|
||||
can lose or regain corner arcs during an inset while preserving reserved curve identities
|
||||
and linked profile selections. Format 12 saves those associations; compatible older closed
|
||||
offsets upgrade structurally without changing their curve IDs. Breaking an association
|
||||
freezes its current boundary and materializes downstream profile references. Child offsets
|
||||
of a boundary with retired arcs still require breaking its association first; mixed or
|
||||
nonconvex junctions and retained references to disappearing individual arcs remain limited.
|
||||
- Extrude and Revolve editors retain original direction, extent expressions, explicit targets,
|
||||
axes, From/To inputs and All-profile intent. No-op edits preserve downstream dimension names
|
||||
and selected occurrence frames; cancel and successful edits preserve one-step undo history.
|
||||
Placed feature edits preview through the edited timeline entry, and cylindrical Emboss guides
|
||||
use the sketch's actual target, author or uniquely placed third-component frame.
|
||||
Revolve rotators follow the actual signed axis and support symmetric and two-sided extents;
|
||||
dragging a To Object extent converts its derived value to an explicit angle. Legacy reversed-axis
|
||||
symmetric geometry is preserved through format 11's explicit legacy convention. New revolves
|
||||
use the sketch-normal direction for each selected profile, including To Object and curved
|
||||
target trimming, independently of the selected axis sign.
|
||||
- Cylindrical emboss/deboss wraps bounded orthogonal line profiles, including holes, concave
|
||||
outlines and nested islands, onto complete exterior cylinders or coaxial sleeves with exact
|
||||
radial walls. New features infer a unique eligible cylinder when the face field is empty;
|
||||
ambiguous targets require an explicit face. Saved target
|
||||
and author occurrence paths retain the chosen placements; missing/split faces refuse rather
|
||||
than retarget. General free-form profiles, seams, stepped cylinders and other curved surfaces
|
||||
remain unsupported. STEP round-trip and independent radial/volume checks cover this subset.
|
||||
- Shell and offset faces work on bodies of planes, cylinders, cones, spheres and tori (convex or not:
|
||||
filleted boxes, bosses, domes, countersinks; corners where four or more planes meet, such as a
|
||||
floor falling to a drain, as long as the corner's edges are all convex or all concave); draft on convex planar bodies and on walls (planes, cylinders → cones) between caps; lofts through three or more sections are smooth (B-spline sides through every section), two
|
||||
sections ruled. Circular-meridian barrels retain curved inner walls through either axial
|
||||
opening (#2421). Solid lofts preserve holes through sections with matching hole counts
|
||||
(#2435), including inner walls and caps with openings; a hole cannot appear or disappear
|
||||
between sections, and sections with holes cannot collapse to a point.
|
||||
|
||||
- Sketch corner tools: fillet and all three chamfer modes pick a connected corner even when
|
||||
its endpoint overlaps the fixed origin or a loose point. Ordinary drawing still snaps to
|
||||
the origin; clicked UI regressions verify geometry, closed profiles and undo.
|
||||
- Sketch display: drawing, picking, highlighting, tool previews and profile shading share
|
||||
adaptive projected curve boundaries. Fit, zoom, display scale and placed sketch frames
|
||||
invalidate the display cache without changing CAD geometry or timeline history. Work
|
||||
budgets and clipped paths prevent unbounded sampling or invented edges across the eye plane.
|
||||
The Sketch Palette scrolls its options on short windows and enlarged displays while
|
||||
keeping constraint status and Finish Sketch visible.
|
||||
- Face projection: closed boundaries have a deterministic outer-first order. Refresh matches
|
||||
existing geometry before assigning positions, retaining saved curve IDs and constraints
|
||||
across enumeration changes and resize, including concentric circles. Ambiguous identities
|
||||
retain the last geometry and report a lost reference; permutation matching is bounded.
|
||||
- Project accepts complete supported preselection in one undo step. Visible external sketch
|
||||
curves and points retain their source sketch identity; model geometry, axes and planes use
|
||||
the existing linked projection commands. Rejection preserves the document and selection;
|
||||
mixed selections retain ordinary tool-first picking.
|
||||
- Inline sketch dimensions: Tab and Shift+Tab cycle within the active shape's value boxes,
|
||||
selecting visited values for replacement. Expression completion retains forward-Tab
|
||||
priority; real keyboard regressions cover rectangles, lines and single-value circles.
|
||||
Typed lengths, radii, line angles, signed arc sweeps and slot lengths persist as constraints
|
||||
and expressions through save/reopen. Shape creation and its dimensions form one undo step;
|
||||
rejected values keep the editable field focused.
|
||||
- Repeated components: Browser selection, visibility, properties, copy and Move/Copy use
|
||||
checked occurrence paths. Active nested occurrences author planar sketches and solids in
|
||||
their selected frame. Pending moves compose through parents; dialog cancellation restores
|
||||
its selected move. Source materials and body/face appearances follow transformed instances.
|
||||
Shared nested definitions remain shared. Deep Paste New copies reachable definition graphs,
|
||||
independent feature/input histories, styles and internal motion records, with bounded geometry
|
||||
checks and explicit external-link notices. Paste New selects the copy while preserving the active
|
||||
authoring context. Imported periodic STEP seams survive placement and reexport with shared
|
||||
topology and appearances intact. Recorded body transfers preserve historical Join/Cut membership,
|
||||
material, placements and machining data through edits, copying, undo and reopen. New 3D wires
|
||||
record their coordinate frame; legacy wires migrate from known provenance without changing
|
||||
coordinates. Broken legacy links with missing provenance can require explicit refresh or relink.
|
||||
See [occurrence workflows](docs/occurrence-workflows.md).
|
||||
- Toolbar drops on panel labels, gaps and margins append commands with insertion feedback;
|
||||
button drops keep insert-before ordering.
|
||||
- Parameters and Configurations have distinct actions in the SOLID toolbar's CONFIGURE panel.
|
||||
Parameters opens the user/model variable editor; Configurations opens the configuration table.
|
||||
Configure remains available in the panel dropdown.
|
||||
- Parameter creation supports Enter with validation and continued keyboard entry. Shared
|
||||
expression autocomplete supports Up/Down selection, Enter/Tab acceptance and Escape dismissal;
|
||||
accepting a suggestion does not submit a parameter or trigger viewport shortcuts.
|
||||
- Movable window title bars follow the app's dark/light header palette. Radial right-click
|
||||
menus are opt-in in Preferences > Navigation; ordinary context menus remain the default.
|
||||
- Desktop distribution: running Windows windows and executable resources use the app icon;
|
||||
packaging validates icon resources. Nix installs launchers, MIME data and generated icons,
|
||||
with explicit launch apps and Vulkan-loader/driver discovery. Flatpak feed instructions
|
||||
register the required runtime source; release checks exercise desktop modelling on X11/Wayland.
|
||||
- OpenGL rendering clamps effective anti-aliasing to 1x across UI pipelines, textures and CAD
|
||||
callbacks to prevent black frames. Other backends retain the requested sample count, and
|
||||
stored preferences remain unchanged.
|
||||
- Configuration rules: ordered typed conditions can drive parameters and suppressions or
|
||||
restrict values. Preview explains effective values without modifying the document; applying
|
||||
rules validates rows atomically. Length expressions retain their authoring units across
|
||||
document-unit changes. The UI supports structured All/Any/Not conditions and rule ordering.
|
||||
Component styles and certified semantic face styles are available; unsupported lineage refuses
|
||||
atomically, and stale certified face paint cannot jump to another surface. See [configurations](docs/configurations.md).
|
||||
- Sheet-metal material: inherited holes, crossing edge notches, overlapping cuts and retained
|
||||
islands share the same boundaries in flat preview, DXF and unfolded solids. Detached material
|
||||
pieces survive unfold/refold as one logical sheet group, including split flanged sheets.
|
||||
Cut-outs made in the flat pattern cross bends at any angle or curved: their edges fold to
|
||||
helices and curves on the bend, their walls square to the sheet. Free-form bases use bounded
|
||||
sampling.
|
||||
Parallel prismatic partial-depth cuts retain pocket side and depth through transfer and
|
||||
unfold/refold, with separate pocket contours in preview and DXF. Planar Intersect preserves
|
||||
exact holes and retained islands, including the union of multiple selected profiles, and
|
||||
records complementary depth layers as pockets while retaining the stock gauge. Unsupported
|
||||
tapered or bend-crossing machining refuses atomically instead of restoring material later.
|
||||
Sketches attach to a sheet's flat faces (base and flanges, either side) by panel, in the
|
||||
flat pattern's coordinates, and follow it through rebuilds and unfold/refold.
|
||||
A flange on another flange's far edge (a return) follows that flange's closed end and keeps
|
||||
clear of the flange beside it, and the returns of one feature are mitred toward each other
|
||||
with the rule's gap where their flanges meet at a corner; returns of separate features, and
|
||||
flanges on returns, are not closed toward each other.
|
||||
- Rolling-ball fillet accuracy: independent offset-cylinder tee references check surface
|
||||
distance, actual trimmed tessellation, supporting-cylinder contacts and tangency at two
|
||||
scales. The measured trimmed-part error is below 0.0001 mm; this is an analytic geometry
|
||||
check. Under-resolved fits return an error.
|
||||
- Contact driving checks the trajectory between clear endpoints, including linked joints,
|
||||
playback and sparse motion exports. Checks use bounded tessellation samples, not exact
|
||||
continuous collision detection; local thin walls and small details remain limitations.
|
||||
See [contact-set limits and recovery](docs/contact-sets.md).
|
||||
|
||||
## 3D sketch bridges
|
||||
|
||||
3D sketch drawing snaps to points on other visible sketch planes without flattening them onto
|
||||
the active plane. Bridge endpoints capture their source positions; associative endpoint links,
|
||||
construction-plane selection of 3D wires and loft guide rails remain separate work.
|
||||
Feature dialog header chevrons collapse and restore the form without discarding selections
|
||||
or edited values, including the Appearance panel.
|
||||
## Construction on 3D paths
|
||||
|
||||
Plane Along Path has a viewport path selector, fractional position, live preview and timeline
|
||||
editing. Construction planes and points resolve drawn 3D wires without applying their sketch
|
||||
plane a second time. Curved paths use sampled segments; multi-curve chains, absolute distance
|
||||
and reference-object placement remain separate work, as do loft rails and centerlines.
|
||||
|
||||
## Design properties (#1015)
|
||||
|
||||
- Designs carry project, part number, revision, author, organization, description and licence
|
||||
in an optional `metadata` object (File → Design Properties…, `document.metadata.*`,
|
||||
`inspect_design`). STEP and IGES headers and 3MF model metadata take the fields their format
|
||||
has a place for. Other tools keep their own data under a reverse-DNS key, kept as it is
|
||||
through open, edit and save and never exported. See [design properties](docs/book/src/guide/files.md#design-properties).
|
||||
Per-component properties (a part number per component) can follow with the same type.
|
||||
|
||||
## Document-opening performance
|
||||
|
||||
- Desktop STEP import prepares consecutive independent solids in bounded batches with up
|
||||
to eight workers, retaining serial traversal, result order and persistent source identities.
|
||||
The web path remains serial. Original multi-solid fixtures show 64–81% lower complete
|
||||
import times; single-solid assembly parts are not scheduled across nodes. See
|
||||
[measurements and limits](docs/perf.md#parallel-step-body-preparation-2026-10-12).
|
||||
- Design containers (#303, phase one) separate and deduplicate STEP, raw images and
|
||||
lossless binary mesh arrays while keeping plain JSON as a supported Save As/export
|
||||
format. Generated-mesh storage measurements reduce serialization and decoding work;
|
||||
a seekable reader defers original blobs until after provisional display.
|
||||
See [container format](docs/design-container.md) and [performance measurements](docs/perf.md).
|
||||
- Warm saves with explicit `cache: true` can include a versioned native B-rep cache through SolidCraft-owned topology
|
||||
and geometry records, with a separate truck adapter and bounded binary reader. Exact
|
||||
face-source certificates and operation history survive codec round trips. A provisional
|
||||
picture can be generated before original blobs are read; editing and measurement wait
|
||||
for authoritative reconstruction. Cached metadata binds preview and reconstruction to
|
||||
the same document despite concurrent file writes. Default saves omit this cache and never force cold
|
||||
evaluation or meshing; explicit `preview: true` remains a separate warm-mesh option.
|
||||
Keys, resource budgets and document/revision guards reject incompatible, hostile or
|
||||
stale results. A second modelling kernel still requires its own tested adapter.
|
||||
- Warm saves with explicit `cache: true` can also carry a bounded PNG thumbnail from already cached meshes.
|
||||
Home reads it without rebuilding the design when its local picture is missing;
|
||||
mismatched or malformed thumbnails fall back to normal generation. The image is
|
||||
kernel-independent and saving never forces cold meshing to produce it.
|
||||
|
||||
- Cold rebuild profiling separates parsing, geometry, placement, display meshing and
|
||||
thumbnails. Straight-line closest-point projection and exact curved-surface evaluation
|
||||
reuse reduce repeated work without relaxing geometry checks. Three alternating optimized
|
||||
benchmark runs reduce median rebuild time by 26% on the public sensor enclosure and 22%
|
||||
on a local case assembly; shape measurements and topology match the original build.
|
||||
See [performance measurements](docs/perf.md). Further cold Boolean and import costs remain.
|
||||
- Prepared assembly geometry survives selection and view changes while real geometry and
|
||||
placement changes invalidate it. A Pi assembly edge-selection probe drops from 9.24 s to
|
||||
0.57 s; initial opening is unchanged. Actual Windows desktop profiling measures 15.87 s
|
||||
median opening, with 82% spent rebuilding geometry; halving full opening remains open.
|
||||
|
||||
## Kernel
|
||||
|
||||
- Successful Boolean retries report their strategy and numerical adjustments as
|
||||
feature warnings and structured inspection data. Diagnostics survive cached
|
||||
evaluation, undo/redo and cold reopening; the robustness runner counts retry
|
||||
events by strategy and reports any bounded-log truncation. Existing geometry
|
||||
acceptance checks remain in force.
|
||||
|
||||
- Cylinders of one radius whose axes cross (a second bore across a bore of the same size, a pipe
|
||||
tee of equal pipes) touch at two points, where the mesh intersection cannot trace the curves
|
||||
and a boolean cannot tell the four arms of the crossing apart. When the ordinary boolean fails
|
||||
on such a pair, both faces are split along the line of their cylinder through the touching
|
||||
points (the solids are unchanged), the boolean is taken of those, and its history is composed
|
||||
back over the operands as given (#788). The result keeps the split lines as edges of the bore
|
||||
walls. A crossing the ordinary route fails on and the split faces do not help (a radius of 17.5
|
||||
in a 45 mm block with a tool of 80 mm) is still refused.
|
||||
- Boolean intersection-curve construction refuses point-only, collapsed and non-finite
|
||||
polylines without panicking. Direct constructor regressions retain valid open endpoints
|
||||
and closed spherical intersections; the original interactive trigger remains unidentified.
|
||||
- Shell of a stepped, joined planar body no longer creates horizontal subdivisions across
|
||||
continuous walls. Fresh axis-aligned Cut walls are stitched using their exact construction
|
||||
support and shared topology, preserving real steps and intentional boundaries. The local
|
||||
reported case returns from 86 to 49 faces with unchanged material and bounds; regressions
|
||||
cover stepped stock, STEP round trips and saved projections from removed pieces.
|
||||
|
||||
truck (Apache-2.0) behind our own `kernel` crate, with our own blend operation and boolean
|
||||
retry/validation; see `plan/adr/0001-geometry-kernel.md` (local) — summary in README.
|
||||
- Feature milestones: <https://github.com/tangency-labs/solidcraft/milestones>
|
||||
- Issues: <https://github.com/tangency-labs/solidcraft/issues> (area labels such as
|
||||
`area:kernel`, `area:sketch` and `area:ui` group them)
|
||||
- Discord: <https://discord.gg/FtU4NQNVDH>
|
||||
- Releases: <https://github.com/tangency-labs/solidcraft/releases>
|
||||
- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Third-party software in SolidCraft
|
||||
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 560 crates below.
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 559 crates below.
|
||||
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
|
||||
SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
|
||||
|
||||
@@ -60,13 +60,13 @@ bytemuck_derive 1.12.1 (Zlib OR Apache-2.0 OR MIT)
|
||||
byteorder 1.5.0 (Unlicense OR MIT)
|
||||
byteorder-lite 0.1.0 (Unlicense OR MIT)
|
||||
bytes 1.12.1 (MIT)
|
||||
cadmpeg-codec-freecad 0.6.1 (Apache-2.0)
|
||||
cadmpeg-codec-rhino 0.6.1 (Apache-2.0)
|
||||
cadmpeg-codec-step 0.6.1 (Apache-2.0)
|
||||
cadmpeg-container 0.6.1 (Apache-2.0)
|
||||
cadmpeg-core 0.6.1 (Apache-2.0)
|
||||
cadmpeg-ir 0.6.1 (Apache-2.0)
|
||||
cadmpeg-registry 0.6.1 (Apache-2.0)
|
||||
cadmpeg-codec-freecad 0.6.3 (Apache-2.0)
|
||||
cadmpeg-codec-rhino 0.6.3 (Apache-2.0)
|
||||
cadmpeg-codec-step 0.6.3 (Apache-2.0)
|
||||
cadmpeg-container 0.6.3 (Apache-2.0)
|
||||
cadmpeg-core 0.6.3 (Apache-2.0)
|
||||
cadmpeg-ir 0.6.3 (Apache-2.0)
|
||||
cadmpeg-registry 0.6.3 (Apache-2.0)
|
||||
calloop 0.13.0 (MIT)
|
||||
calloop 0.14.5 (MIT)
|
||||
calloop-wayland-source 0.3.0 (MIT)
|
||||
@@ -95,6 +95,7 @@ core_detect 1.0.0 (MIT/Apache-2.0)
|
||||
cpufeatures 0.2.17 (MIT OR Apache-2.0)
|
||||
cpufeatures 0.3.1 (MIT OR Apache-2.0)
|
||||
crc32fast 1.5.2 (MIT OR Apache-2.0)
|
||||
crossbeam-channel 0.5.17 (MIT OR Apache-2.0)
|
||||
crossbeam-deque 0.8.8 (MIT OR Apache-2.0)
|
||||
crossbeam-epoch 0.9.21 (MIT OR Apache-2.0)
|
||||
crossbeam-utils 0.8.23 (MIT OR Apache-2.0)
|
||||
@@ -210,6 +211,7 @@ jni-sys 0.4.1 (MIT OR Apache-2.0)
|
||||
jni-sys-macros 0.4.1 (MIT OR Apache-2.0)
|
||||
js-sys 0.3.106 (MIT OR Apache-2.0)
|
||||
katexit 0.1.5 (MIT OR Apache-2.0)
|
||||
keyboard-types 0.8.3 (MIT OR Apache-2.0)
|
||||
keyring 3.6.3 (MIT OR Apache-2.0)
|
||||
khronos-egl 6.0.0 (MIT/Apache-2.0)
|
||||
kurbo 0.13.1 (Apache-2.0 OR MIT)
|
||||
@@ -234,6 +236,7 @@ minisign-verify 0.3.0 (MIT)
|
||||
miniz_oxide 0.8.9 (MIT OR Zlib OR Apache-2.0)
|
||||
miniz_oxide 0.9.1 (MIT OR Zlib OR Apache-2.0)
|
||||
moxcms 0.8.1 (BSD-3-Clause OR Apache-2.0)
|
||||
muda 0.21.2 (Apache-2.0 OR MIT)
|
||||
multiversion_no_op 1.0.0 (Apache-2.0 OR MIT)
|
||||
naga 30.0.1 (MIT OR Apache-2.0)
|
||||
naga-types 30.0.1 (MIT OR Apache-2.0)
|
||||
@@ -352,6 +355,7 @@ rustls-platform-verifier 0.7.1 (MIT OR Apache-2.0)
|
||||
rustls-platform-verifier-android 0.2.0 (MIT OR Apache-2.0)
|
||||
rustls-webpki 0.103.15 (ISC)
|
||||
rustversion 1.0.23 (MIT OR Apache-2.0)
|
||||
ruzstd 0.9.1 (MIT)
|
||||
schannel 0.1.29 (MIT)
|
||||
schemars 1.2.3 (MIT)
|
||||
schemars_derive 1.2.3 (MIT)
|
||||
@@ -429,6 +433,7 @@ truck-shapeops 0.4.0 (Apache-2.0)
|
||||
truck-stepio 0.3.0 (Apache-2.0)
|
||||
truck-topology 0.6.0 (Apache-2.0)
|
||||
ttf-parser 0.25.1 (MIT OR Apache-2.0)
|
||||
twox-hash 2.1.5 (MIT)
|
||||
type-map 0.5.1 (MIT/Apache-2.0)
|
||||
typed-path 0.12.3 (MIT OR Apache-2.0)
|
||||
typeid 1.0.3 (MIT OR Apache-2.0)
|
||||
@@ -546,16 +551,10 @@ zeroize_derive 1.5.0 (Apache-2.0 OR MIT)
|
||||
zerotrie 0.2.5 (Unicode-3.0)
|
||||
zerovec 0.11.8 (Unicode-3.0)
|
||||
zerovec-derive 0.11.6 (Unicode-3.0)
|
||||
zip 8.6.0 (MIT)
|
||||
zip 9.0.4 (MIT)
|
||||
zlib-rs 0.6.8 (Zlib)
|
||||
zmij 1.0.23 (MIT)
|
||||
zopfli 0.8.3 (Apache-2.0)
|
||||
zstd 0.13.3 (MIT)
|
||||
zstd 0.14.2 (BSD-3-Clause)
|
||||
zstd-safe 7.3.0 (BSD-3-Clause)
|
||||
zstd-safe 8.1.0 (BSD-3-Clause)
|
||||
zstd-sys 2.1.1+zstd.1.5.7 (BSD-3-Clause)
|
||||
zune-core 0.5.3 (MIT OR Apache-2.0 OR Zlib)
|
||||
zune-jpeg 0.5.15 (MIT OR Apache-2.0 OR Zlib)
|
||||
zvariant 4.2.0 (MIT)
|
||||
@@ -1108,26 +1107,27 @@ ahash 0.8.12, arrayvec 0.7.8, async-channel 2.5.0, async-executor 1.14.0, async-
|
||||
4.7.1, atomic-waker 1.1.2, base64 0.23.1, bitflags 1.3.2, bitflags 2.13.2, blocking 1.7.0, bumpalo
|
||||
3.20.3, cfg-if 1.0.5, cgl 0.3.2, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation
|
||||
0.9.4, core-foundation-sys 0.8.7, core-graphics 0.23.2, core-graphics-types 0.1.3, core_detect
|
||||
1.0.0, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23, dbus-secret-service
|
||||
4.1.0, displaydoc 0.2.7, either 1.19.0, equivalent 1.0.2, errno 0.3.14, euclid 0.22.14,
|
||||
event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, flate2 1.1.10, form_urlencoded
|
||||
1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow 0.17.0, gltf 1.4.1, gltf-derive
|
||||
1.4.1, gltf-json 1.4.1, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, httparse 1.10.1, idna
|
||||
1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.13.0, itertools 0.15.0, js-sys 0.3.106,
|
||||
khronos-egl 6.0.0, lazy_static 1.5.1, linux-raw-sys 0.12.1, linux-raw-sys 0.4.15, lock_api 0.4.14,
|
||||
log 0.4.34, num 0.4.3, num-bigint 0.4.8, num-complex 0.4.6, num-integer 0.1.47, num-iter 0.1.46,
|
||||
num-rational 0.4.2, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, ordered-stream 0.2.0,
|
||||
parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5,
|
||||
plain 0.2.3, png 0.18.1, polling 3.11.0, pollster 0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core
|
||||
1.13.0, renderdoc-sys 1.1.0, robust 1.2.0, ron 0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix
|
||||
0.38.44, rustix 1.1.5, rustls 0.23.46, rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard
|
||||
1.2.0, secret-service 4.0.0, security-framework 2.11.1, security-framework 3.7.0,
|
||||
security-framework-sys 2.17.0, signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2,
|
||||
smol_str 0.2.2, stable_deref_trait 1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1,
|
||||
unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.27.0, wasi
|
||||
0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.129, wasm-bindgen-futures
|
||||
0.4.79, wasm-bindgen-macro 0.2.129, wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129,
|
||||
web-sys 0.3.106, weezl 0.1.12, wit-bindgen 0.57.1
|
||||
1.0.0, crossbeam-channel 0.5.17, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils
|
||||
0.8.23, dbus-secret-service 4.1.0, displaydoc 0.2.7, either 1.19.0, equivalent 1.0.2, errno 0.3.14,
|
||||
euclid 0.22.14, event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, flate2 1.1.10,
|
||||
form_urlencoded 1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow 0.17.0, gltf 1.4.1,
|
||||
gltf-derive 1.4.1, gltf-json 1.4.1, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, httparse
|
||||
1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.13.0, itertools 0.15.0, js-sys
|
||||
0.3.106, keyboard-types 0.8.3, khronos-egl 6.0.0, lazy_static 1.5.1, linux-raw-sys 0.12.1,
|
||||
linux-raw-sys 0.4.15, lock_api 0.4.14, log 0.4.34, muda 0.21.2, num 0.4.3, num-bigint 0.4.8,
|
||||
num-complex 0.4.6, num-integer 0.1.47, num-iter 0.1.46, num-rational 0.4.2, num-traits 0.2.19,
|
||||
once_cell 1.21.4, openssl-probe 0.2.1, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5,
|
||||
parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, plain 0.2.3, png 0.18.1, polling
|
||||
3.11.0, pollster 0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core 1.13.0, renderdoc-sys 1.1.0, robust
|
||||
1.2.0, ron 0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix 0.38.44, rustix 1.1.5, rustls 0.23.46,
|
||||
rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard 1.2.0, secret-service 4.0.0,
|
||||
security-framework 2.11.1, security-framework 3.7.0, security-framework-sys 2.17.0,
|
||||
signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2, smol_str 0.2.2, stable_deref_trait
|
||||
1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1, unicode-segmentation 1.13.3, unicode-width
|
||||
0.2.2, url 2.5.8, uuid 1.27.0, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12,
|
||||
wasm-bindgen 0.2.129, wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129,
|
||||
wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129, web-sys 0.3.106, weezl 0.1.12,
|
||||
wit-bindgen 0.57.1
|
||||
================================================================================
|
||||
|
||||
Apache License
|
||||
@@ -4030,7 +4030,605 @@ Apache License
|
||||
limitations under the License.
|
||||
|
||||
================================================================================
|
||||
crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23
|
||||
crossbeam-channel 0.5.17
|
||||
================================================================================
|
||||
|
||||
===============================================================================
|
||||
|
||||
matching.go
|
||||
https://creativecommons.org/licenses/by/3.0/legalcode
|
||||
|
||||
Creative Commons Legal Code
|
||||
|
||||
Attribution 3.0 Unported
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
|
||||
DAMAGES RESULTING FROM ITS USE.
|
||||
|
||||
License
|
||||
|
||||
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
|
||||
COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
|
||||
COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
|
||||
AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
|
||||
|
||||
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
|
||||
TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
|
||||
BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
|
||||
CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
|
||||
CONDITIONS.
|
||||
|
||||
1. Definitions
|
||||
|
||||
a. "Adaptation" means a work based upon the Work, or upon the Work and
|
||||
other pre-existing works, such as a translation, adaptation,
|
||||
derivative work, arrangement of music or other alterations of a
|
||||
literary or artistic work, or phonogram or performance and includes
|
||||
cinematographic adaptations or any other form in which the Work may be
|
||||
recast, transformed, or adapted including in any form recognizably
|
||||
derived from the original, except that a work that constitutes a
|
||||
Collection will not be considered an Adaptation for the purpose of
|
||||
this License. For the avoidance of doubt, where the Work is a musical
|
||||
work, performance or phonogram, the synchronization of the Work in
|
||||
timed-relation with a moving image ("synching") will be considered an
|
||||
Adaptation for the purpose of this License.
|
||||
b. "Collection" means a collection of literary or artistic works, such as
|
||||
encyclopedias and anthologies, or performances, phonograms or
|
||||
broadcasts, or other works or subject matter other than works listed
|
||||
in Section 1(f) below, which, by reason of the selection and
|
||||
arrangement of their contents, constitute intellectual creations, in
|
||||
which the Work is included in its entirety in unmodified form along
|
||||
with one or more other contributions, each constituting separate and
|
||||
independent works in themselves, which together are assembled into a
|
||||
collective whole. A work that constitutes a Collection will not be
|
||||
considered an Adaptation (as defined above) for the purposes of this
|
||||
License.
|
||||
c. "Distribute" means to make available to the public the original and
|
||||
copies of the Work or Adaptation, as appropriate, through sale or
|
||||
other transfer of ownership.
|
||||
d. "Licensor" means the individual, individuals, entity or entities that
|
||||
offer(s) the Work under the terms of this License.
|
||||
e. "Original Author" means, in the case of a literary or artistic work,
|
||||
the individual, individuals, entity or entities who created the Work
|
||||
or if no individual or entity can be identified, the publisher; and in
|
||||
addition (i) in the case of a performance the actors, singers,
|
||||
musicians, dancers, and other persons who act, sing, deliver, declaim,
|
||||
play in, interpret or otherwise perform literary or artistic works or
|
||||
expressions of folklore; (ii) in the case of a phonogram the producer
|
||||
being the person or legal entity who first fixes the sounds of a
|
||||
performance or other sounds; and, (iii) in the case of broadcasts, the
|
||||
organization that transmits the broadcast.
|
||||
f. "Work" means the literary and/or artistic work offered under the terms
|
||||
of this License including without limitation any production in the
|
||||
literary, scientific and artistic domain, whatever may be the mode or
|
||||
form of its expression including digital form, such as a book,
|
||||
pamphlet and other writing; a lecture, address, sermon or other work
|
||||
of the same nature; a dramatic or dramatico-musical work; a
|
||||
choreographic work or entertainment in dumb show; a musical
|
||||
composition with or without words; a cinematographic work to which are
|
||||
assimilated works expressed by a process analogous to cinematography;
|
||||
a work of drawing, painting, architecture, sculpture, engraving or
|
||||
lithography; a photographic work to which are assimilated works
|
||||
expressed by a process analogous to photography; a work of applied
|
||||
art; an illustration, map, plan, sketch or three-dimensional work
|
||||
relative to geography, topography, architecture or science; a
|
||||
performance; a broadcast; a phonogram; a compilation of data to the
|
||||
extent it is protected as a copyrightable work; or a work performed by
|
||||
a variety or circus performer to the extent it is not otherwise
|
||||
considered a literary or artistic work.
|
||||
g. "You" means an individual or entity exercising rights under this
|
||||
License who has not previously violated the terms of this License with
|
||||
respect to the Work, or who has received express permission from the
|
||||
Licensor to exercise rights under this License despite a previous
|
||||
violation.
|
||||
h. "Publicly Perform" means to perform public recitations of the Work and
|
||||
to communicate to the public those public recitations, by any means or
|
||||
process, including by wire or wireless means or public digital
|
||||
performances; to make available to the public Works in such a way that
|
||||
members of the public may access these Works from a place and at a
|
||||
place individually chosen by them; to perform the Work to the public
|
||||
by any means or process and the communication to the public of the
|
||||
performances of the Work, including by public digital performance; to
|
||||
broadcast and rebroadcast the Work by any means including signs,
|
||||
sounds or images.
|
||||
i. "Reproduce" means to make copies of the Work by any means including
|
||||
without limitation by sound or visual recordings and the right of
|
||||
fixation and reproducing fixations of the Work, including storage of a
|
||||
protected performance or phonogram in digital form or other electronic
|
||||
medium.
|
||||
|
||||
2. Fair Dealing Rights. Nothing in this License is intended to reduce,
|
||||
limit, or restrict any uses free from copyright or rights arising from
|
||||
limitations or exceptions that are provided for in connection with the
|
||||
copyright protection under copyright law or other applicable laws.
|
||||
|
||||
3. License Grant. Subject to the terms and conditions of this License,
|
||||
Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||
perpetual (for the duration of the applicable copyright) license to
|
||||
exercise the rights in the Work as stated below:
|
||||
|
||||
a. to Reproduce the Work, to incorporate the Work into one or more
|
||||
Collections, and to Reproduce the Work as incorporated in the
|
||||
Collections;
|
||||
b. to create and Reproduce Adaptations provided that any such Adaptation,
|
||||
including any translation in any medium, takes reasonable steps to
|
||||
clearly label, demarcate or otherwise identify that changes were made
|
||||
to the original Work. For example, a translation could be marked "The
|
||||
original work was translated from English to Spanish," or a
|
||||
modification could indicate "The original work has been modified.";
|
||||
c. to Distribute and Publicly Perform the Work including as incorporated
|
||||
in Collections; and,
|
||||
d. to Distribute and Publicly Perform Adaptations.
|
||||
e. For the avoidance of doubt:
|
||||
|
||||
i. Non-waivable Compulsory License Schemes. In those jurisdictions in
|
||||
which the right to collect royalties through any statutory or
|
||||
compulsory licensing scheme cannot be waived, the Licensor
|
||||
reserves the exclusive right to collect such royalties for any
|
||||
exercise by You of the rights granted under this License;
|
||||
ii. Waivable Compulsory License Schemes. In those jurisdictions in
|
||||
which the right to collect royalties through any statutory or
|
||||
compulsory licensing scheme can be waived, the Licensor waives the
|
||||
exclusive right to collect such royalties for any exercise by You
|
||||
of the rights granted under this License; and,
|
||||
iii. Voluntary License Schemes. The Licensor waives the right to
|
||||
collect royalties, whether individually or, in the event that the
|
||||
Licensor is a member of a collecting society that administers
|
||||
voluntary licensing schemes, via that society, from any exercise
|
||||
by You of the rights granted under this License.
|
||||
|
||||
The above rights may be exercised in all media and formats whether now
|
||||
known or hereafter devised. The above rights include the right to make
|
||||
such modifications as are technically necessary to exercise the rights in
|
||||
other media and formats. Subject to Section 8(f), all rights not expressly
|
||||
granted by Licensor are hereby reserved.
|
||||
|
||||
4. Restrictions. The license granted in Section 3 above is expressly made
|
||||
subject to and limited by the following restrictions:
|
||||
|
||||
a. You may Distribute or Publicly Perform the Work only under the terms
|
||||
of this License. You must include a copy of, or the Uniform Resource
|
||||
Identifier (URI) for, this License with every copy of the Work You
|
||||
Distribute or Publicly Perform. You may not offer or impose any terms
|
||||
on the Work that restrict the terms of this License or the ability of
|
||||
the recipient of the Work to exercise the rights granted to that
|
||||
recipient under the terms of the License. You may not sublicense the
|
||||
Work. You must keep intact all notices that refer to this License and
|
||||
to the disclaimer of warranties with every copy of the Work You
|
||||
Distribute or Publicly Perform. When You Distribute or Publicly
|
||||
Perform the Work, You may not impose any effective technological
|
||||
measures on the Work that restrict the ability of a recipient of the
|
||||
Work from You to exercise the rights granted to that recipient under
|
||||
the terms of the License. This Section 4(a) applies to the Work as
|
||||
incorporated in a Collection, but this does not require the Collection
|
||||
apart from the Work itself to be made subject to the terms of this
|
||||
License. If You create a Collection, upon notice from any Licensor You
|
||||
must, to the extent practicable, remove from the Collection any credit
|
||||
as required by Section 4(b), as requested. If You create an
|
||||
Adaptation, upon notice from any Licensor You must, to the extent
|
||||
practicable, remove from the Adaptation any credit as required by
|
||||
Section 4(b), as requested.
|
||||
b. If You Distribute, or Publicly Perform the Work or any Adaptations or
|
||||
Collections, You must, unless a request has been made pursuant to
|
||||
Section 4(a), keep intact all copyright notices for the Work and
|
||||
provide, reasonable to the medium or means You are utilizing: (i) the
|
||||
name of the Original Author (or pseudonym, if applicable) if supplied,
|
||||
and/or if the Original Author and/or Licensor designate another party
|
||||
or parties (e.g., a sponsor institute, publishing entity, journal) for
|
||||
attribution ("Attribution Parties") in Licensor's copyright notice,
|
||||
terms of service or by other reasonable means, the name of such party
|
||||
or parties; (ii) the title of the Work if supplied; (iii) to the
|
||||
extent reasonably practicable, the URI, if any, that Licensor
|
||||
specifies to be associated with the Work, unless such URI does not
|
||||
refer to the copyright notice or licensing information for the Work;
|
||||
and (iv) , consistent with Section 3(b), in the case of an Adaptation,
|
||||
a credit identifying the use of the Work in the Adaptation (e.g.,
|
||||
"French translation of the Work by Original Author," or "Screenplay
|
||||
based on original Work by Original Author"). The credit required by
|
||||
this Section 4 (b) may be implemented in any reasonable manner;
|
||||
provided, however, that in the case of a Adaptation or Collection, at
|
||||
a minimum such credit will appear, if a credit for all contributing
|
||||
authors of the Adaptation or Collection appears, then as part of these
|
||||
credits and in a manner at least as prominent as the credits for the
|
||||
other contributing authors. For the avoidance of doubt, You may only
|
||||
use the credit required by this Section for the purpose of attribution
|
||||
in the manner set out above and, by exercising Your rights under this
|
||||
License, You may not implicitly or explicitly assert or imply any
|
||||
connection with, sponsorship or endorsement by the Original Author,
|
||||
Licensor and/or Attribution Parties, as appropriate, of You or Your
|
||||
use of the Work, without the separate, express prior written
|
||||
permission of the Original Author, Licensor and/or Attribution
|
||||
Parties.
|
||||
c. Except as otherwise agreed in writing by the Licensor or as may be
|
||||
otherwise permitted by applicable law, if You Reproduce, Distribute or
|
||||
Publicly Perform the Work either by itself or as part of any
|
||||
Adaptations or Collections, You must not distort, mutilate, modify or
|
||||
take other derogatory action in relation to the Work which would be
|
||||
prejudicial to the Original Author's honor or reputation. Licensor
|
||||
agrees that in those jurisdictions (e.g. Japan), in which any exercise
|
||||
of the right granted in Section 3(b) of this License (the right to
|
||||
make Adaptations) would be deemed to be a distortion, mutilation,
|
||||
modification or other derogatory action prejudicial to the Original
|
||||
Author's honor and reputation, the Licensor will waive or not assert,
|
||||
as appropriate, this Section, to the fullest extent permitted by the
|
||||
applicable national law, to enable You to reasonably exercise Your
|
||||
right under Section 3(b) of this License (right to make Adaptations)
|
||||
but not otherwise.
|
||||
|
||||
5. Representations, Warranties and Disclaimer
|
||||
|
||||
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
|
||||
OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
|
||||
KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
|
||||
INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
|
||||
LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
|
||||
WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
|
||||
OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
|
||||
|
||||
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
|
||||
LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
|
||||
ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
|
||||
ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
|
||||
BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
7. Termination
|
||||
|
||||
a. This License and the rights granted hereunder will terminate
|
||||
automatically upon any breach by You of the terms of this License.
|
||||
Individuals or entities who have received Adaptations or Collections
|
||||
from You under this License, however, will not have their licenses
|
||||
terminated provided such individuals or entities remain in full
|
||||
compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
|
||||
survive any termination of this License.
|
||||
b. Subject to the above terms and conditions, the license granted here is
|
||||
perpetual (for the duration of the applicable copyright in the Work).
|
||||
Notwithstanding the above, Licensor reserves the right to release the
|
||||
Work under different license terms or to stop distributing the Work at
|
||||
any time; provided, however that any such election will not serve to
|
||||
withdraw this License (or any other license that has been, or is
|
||||
required to be, granted under the terms of this License), and this
|
||||
License will continue in full force and effect unless terminated as
|
||||
stated above.
|
||||
|
||||
8. Miscellaneous
|
||||
|
||||
a. Each time You Distribute or Publicly Perform the Work or a Collection,
|
||||
the Licensor offers to the recipient a license to the Work on the same
|
||||
terms and conditions as the license granted to You under this License.
|
||||
b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
|
||||
offers to the recipient a license to the original Work on the same
|
||||
terms and conditions as the license granted to You under this License.
|
||||
c. If any provision of this License is invalid or unenforceable under
|
||||
applicable law, it shall not affect the validity or enforceability of
|
||||
the remainder of the terms of this License, and without further action
|
||||
by the parties to this agreement, such provision shall be reformed to
|
||||
the minimum extent necessary to make such provision valid and
|
||||
enforceable.
|
||||
d. No term or provision of this License shall be deemed waived and no
|
||||
breach consented to unless such waiver or consent shall be in writing
|
||||
and signed by the party to be charged with such waiver or consent.
|
||||
e. This License constitutes the entire agreement between the parties with
|
||||
respect to the Work licensed here. There are no understandings,
|
||||
agreements or representations with respect to the Work not specified
|
||||
here. Licensor shall not be bound by any additional provisions that
|
||||
may appear in any communication from You. This License may not be
|
||||
modified without the mutual written agreement of the Licensor and You.
|
||||
f. The rights granted under, and the subject matter referenced, in this
|
||||
License were drafted utilizing the terminology of the Berne Convention
|
||||
for the Protection of Literary and Artistic Works (as amended on
|
||||
September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
|
||||
Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
|
||||
and the Universal Copyright Convention (as revised on July 24, 1971).
|
||||
These rights and subject matter take effect in the relevant
|
||||
jurisdiction in which the License terms are sought to be enforced
|
||||
according to the corresponding provisions of the implementation of
|
||||
those treaty provisions in the applicable national law. If the
|
||||
standard suite of rights granted under applicable copyright law
|
||||
includes additional rights not granted under this License, such
|
||||
additional rights are deemed to be included in the License; this
|
||||
License is not intended to restrict the license of any rights under
|
||||
applicable law.
|
||||
|
||||
|
||||
Creative Commons Notice
|
||||
|
||||
Creative Commons is not a party to this License, and makes no warranty
|
||||
whatsoever in connection with the Work. Creative Commons will not be
|
||||
liable to You or any party on any legal theory for any damages
|
||||
whatsoever, including without limitation any general, special,
|
||||
incidental or consequential damages arising in connection to this
|
||||
license. Notwithstanding the foregoing two (2) sentences, if Creative
|
||||
Commons has expressly identified itself as the Licensor hereunder, it
|
||||
shall have all rights and obligations of Licensor.
|
||||
|
||||
Except for the limited purpose of indicating to the public that the
|
||||
Work is licensed under the CCPL, Creative Commons does not authorize
|
||||
the use by either party of the trademark "Creative Commons" or any
|
||||
related trademark or logo of Creative Commons without the prior
|
||||
written consent of Creative Commons. Any permitted use will be in
|
||||
compliance with Creative Commons' then-current trademark usage
|
||||
guidelines, as may be published on its website or otherwise made
|
||||
available upon request from time to time. For the avoidance of doubt,
|
||||
this trademark restriction does not form part of this License.
|
||||
|
||||
Creative Commons may be contacted at https://creativecommons.org/.
|
||||
|
||||
===============================================================================
|
||||
|
||||
The Go Programming Language
|
||||
https://golang.org/LICENSE
|
||||
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
===============================================================================
|
||||
|
||||
The Rust Programming Language
|
||||
https://github.com/rust-lang/rust/blob/master/LICENSE-MIT
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
===============================================================================
|
||||
|
||||
The Rust Programming Language
|
||||
https://github.com/rust-lang/rust/blob/master/LICENSE-APACHE
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================================================================================
|
||||
crossbeam-channel 0.5.17, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23
|
||||
================================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
@@ -7716,6 +8314,30 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
keyboard-types 0.8.3
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2017 Pyfisch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
keyring 3.6.3
|
||||
================================================================================
|
||||
@@ -9452,6 +10074,56 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================================
|
||||
muda 0.21.2
|
||||
================================================================================
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2022 Tauri Programme within The Commons Conservancy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
muda 0.21.2
|
||||
================================================================================
|
||||
|
||||
SPDXVersion: SPDX-2.1
|
||||
DataLicense: CC0-1.0
|
||||
PackageName: muda
|
||||
DataFormat: SPDXRef-1
|
||||
PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
|
||||
PackageHomePage: https://tauri.app
|
||||
PackageLicenseDeclared: Apache-2.0
|
||||
PackageLicenseDeclared: MIT
|
||||
PackageCopyrightText: 2020-2022, The Tauri Programme in the Commons Conservancy
|
||||
PackageSummary: <text>Menu Utilities for Desktop Applications.
|
||||
</text>
|
||||
PackageComment: <text>The package includes the following libraries; see
|
||||
Relationship information.
|
||||
</text>
|
||||
Created: 2022-12-05T09:00:00Z
|
||||
PackageDownloadLocation: git://github.com/tauri-apps/muda
|
||||
PackageDownloadLocation: git+https://github.com/tauri-apps/muda.git
|
||||
PackageDownloadLocation: git+ssh://github.com/tauri-apps/muda.git
|
||||
Creator: Person: Daniel Thompson-Yvetot
|
||||
|
||||
================================================================================
|
||||
multiversion_no_op 1.0.0
|
||||
================================================================================
|
||||
@@ -11781,6 +12453,32 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
The files under third-party/chromium are licensed as described in
|
||||
third-party/chromium/LICENSE.
|
||||
|
||||
================================================================================
|
||||
ruzstd 0.9.1
|
||||
================================================================================
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Moritz Borcherding
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
schannel 0.1.29
|
||||
================================================================================
|
||||
@@ -13042,6 +13740,32 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
twox-hash 2.1.5
|
||||
================================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Jake Goulding
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
type-map 0.5.1
|
||||
================================================================================
|
||||
@@ -15663,7 +16387,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
zip 8.6.0, zip 9.0.4
|
||||
zip 9.0.4
|
||||
================================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
@@ -15918,89 +16642,6 @@ Apache License
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================================================================================
|
||||
zstd 0.13.3
|
||||
================================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2016 Alexandre Bury
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
zstd 0.14.2, zstd-safe 7.3.0, zstd-safe 8.1.0, zstd-sys 2.1.1+zstd.1.5.7
|
||||
================================================================================
|
||||
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2026, Alexandre Bury
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================================
|
||||
zstd-sys 2.1.1+zstd.1.5.7
|
||||
================================================================================
|
||||
|
||||
The auto-generated bindings are under the 3-clause BSD license:
|
||||
|
||||
BSD License
|
||||
|
||||
For Zstandard software
|
||||
|
||||
Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name Facebook, nor Meta, nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================================
|
||||
zune-core 0.5.3, zune-jpeg 0.5.15
|
||||
================================================================================
|
||||
@@ -16068,13 +16709,13 @@ accesskit_winit 0.32.2: Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
array-macro 2.1.8: MIT OR Apache-2.0 (https://codeberg.org/xfix/array-macro)
|
||||
block2 0.5.1: MIT (https://github.com/madsmtm/objc2)
|
||||
block2 0.6.2: MIT (https://github.com/madsmtm/objc2)
|
||||
cadmpeg-codec-freecad 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-rhino 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-step 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-container 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-core 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-ir 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-registry 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-freecad 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-rhino 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-step 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-container 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-core 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-ir 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-registry 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
clipboard-win 5.4.1: BSL-1.0 (https://github.com/DoumanAsh/clipboard-win)
|
||||
convert_case 0.4.0: MIT (https://github.com/rutrum/convert-case)
|
||||
dispatch 0.2.0: MIT (http://github.com/SSheldon/rust-dispatch)
|
||||
|
||||
@@ -19,6 +19,11 @@ path = "src/main.rs"
|
||||
name = "solvecraft-cli"
|
||||
path = "src/alias.rs"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
|
||||
|
||||
[dependencies]
|
||||
solidcraft-engine = { workspace = true }
|
||||
solidcraft-gpu = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
//! Reproducible evidence through the production command/job interfaces.
|
||||
//! Usage: studio_evidence RECIPE.json RENDER-PARAMS.json OUTPUT.png
|
||||
//! Writes the editable design, command timings and actual adapter/job metadata beside PNG.
|
||||
//! Accepts runtime requests as well as inline or named saved render jobs.
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::{
|
||||
Session,
|
||||
doc::render_settings::{RenderJob, RenderSetup},
|
||||
render::job::{CpuHost, ErrorCode, JobId, Phase},
|
||||
};
|
||||
use std::{
|
||||
io::Read,
|
||||
path::Path,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
fn read_json(path: &str) -> Result<Value, String> {
|
||||
let mut bytes = Vec::new();
|
||||
std::fs::File::open(path).map_err(|e| format!("{path}: {e}"))?.take(16_777_217).read_to_end(&mut bytes).map_err(|e| e.to_string())?;
|
||||
if bytes.len() > 16_777_216 {
|
||||
return Err("evidence input exceeds 16 MiB".into());
|
||||
}
|
||||
serde_json::from_slice(&bytes).map_err(|e| format!("{path}: {e}"))
|
||||
}
|
||||
|
||||
// Only decide which optional host must be installed. The production command
|
||||
// still validates and resolves the actual request without a second wire format.
|
||||
fn needs_gpu(session: &Session, params: &Value) -> Result<bool, String> {
|
||||
let inline = params.get("settings").filter(|value| !value.is_null());
|
||||
let named = params.get("setup").filter(|value| !value.is_null());
|
||||
if inline.is_some() && named.is_some() {
|
||||
return Err("choose either setup or settings".into());
|
||||
}
|
||||
let inline_setup = inline
|
||||
.filter(|value| value.get("job").is_some_and(|job| !job.is_null()))
|
||||
.map(|value| serde_json::from_value::<RenderSetup>(value.clone()).map_err(|e| e.to_string()))
|
||||
.transpose()?;
|
||||
let setup = if let Some(name) = named.and_then(Value::as_str) {
|
||||
Some(session.doc.render.setups.get(name).ok_or("no saved render setup with that name")?)
|
||||
} else {
|
||||
inline_setup.as_ref()
|
||||
};
|
||||
if let Some(setup) = setup
|
||||
&& let Some(job) = &setup.job
|
||||
{
|
||||
if params.as_object().is_some_and(|object| object.keys().any(|key| !matches!(key.as_str(), "setup" | "settings" | "replace"))) {
|
||||
return Err("saved render jobs cannot be mixed with runtime controls".into());
|
||||
}
|
||||
setup.validate().map_err(|e| e.to_string())?;
|
||||
return Ok(matches!(job, RenderJob::GpuPath { .. }));
|
||||
}
|
||||
Ok(params.get("progressive").and_then(Value::as_bool) == Some(true))
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let [recipe_path, params_path, output_path] = args.as_slice() else {
|
||||
return Err("usage: studio_evidence RECIPE.json RENDER-PARAMS.json OUTPUT.png".into());
|
||||
};
|
||||
let output = Path::new(output_path);
|
||||
if !output.extension().is_some_and(|e| e.eq_ignore_ascii_case("png")) {
|
||||
return Err("output must have a .png extension".into());
|
||||
}
|
||||
let recipe = read_json(recipe_path)?;
|
||||
let params = read_json(params_path)?;
|
||||
let mut session = Session::default();
|
||||
eprintln!("Generating original CAD scene from {recipe_path}");
|
||||
let generation_start = Instant::now();
|
||||
let (commands, command_timings) = session.run_script_timed(&recipe, false).map_err(|e| e.to_string())?;
|
||||
let generation_seconds = generation_start.elapsed().as_secs_f64();
|
||||
let design_path = output.with_extension("solidcraft");
|
||||
session.execute("file.save_as", &json!({"path":design_path})).map_err(|e| e.to_string())?;
|
||||
let scene = solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?;
|
||||
let bodies: Vec<Value> = scene
|
||||
.meshes
|
||||
.iter()
|
||||
.map(|mesh| json!({"name":mesh.name,"vertices":mesh.mesh.positions.len(),"triangles":mesh.mesh.triangles.len()}))
|
||||
.collect();
|
||||
drop(scene);
|
||||
|
||||
let cpu_host_start = Instant::now();
|
||||
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
|
||||
let cpu_host_seconds = cpu_host_start.elapsed().as_secs_f64();
|
||||
// Match runtime and saved-job dispatch. CPU evidence must not require a
|
||||
// Vulkan device, parse GPU policy or initialize a GPU host.
|
||||
let (adapter_report, gpu_host_seconds) = if needs_gpu(&session, ¶ms)? {
|
||||
let gpu_host_start = Instant::now();
|
||||
let policy = solidcraft_gpu::select::Policy::from_env().map_err(|e| e.to_string())?;
|
||||
let (adapter, report) =
|
||||
solidcraft_gpu::select::pick_vulkan(policy, |_| true).ok_or("no Vulkan adapter satisfies the requested GPU policy")?;
|
||||
session.register_render_job_host(solidcraft_gpu::path::host_on_adapter(&adapter).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
(Some(report), Some(gpu_host_start.elapsed().as_secs_f64()))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let preflight = session.execute("render.preflight", ¶ms).map_err(|e| e.to_string())?;
|
||||
let start = Instant::now();
|
||||
let started = session.execute("render.start", ¶ms).map_err(|e| e.to_string())?;
|
||||
let executor = started
|
||||
.get("resolved")
|
||||
.and_then(|resolved| resolved.get("executor"))
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("started job did not report the resolved image executor")?
|
||||
.to_owned();
|
||||
let id: JobId = serde_json::from_value(started.get("job").cloned().ok_or("missing job id")?).map_err(|e| e.to_string())?;
|
||||
let handle = session.render_job_handle(id).map_err(|e| e.to_string())?;
|
||||
eprintln!("Rendering through production {executor} executor");
|
||||
if let Some(adapter) = &adapter_report {
|
||||
eprintln!("GPU adapter: {adapter:?}");
|
||||
}
|
||||
let status = loop {
|
||||
match handle.wait_timeout(Duration::from_secs(10)) {
|
||||
Ok(status) => break status,
|
||||
Err(error) if error.code == ErrorCode::TimedOut && start.elapsed() < Duration::from_secs(3600) => {
|
||||
let status = handle.status().map_err(|e| e.to_string())?;
|
||||
eprintln!(
|
||||
"Render progress: {:?} {}/{} after {:.1}s",
|
||||
status.progress.phase,
|
||||
status.progress.done,
|
||||
status.progress.total,
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = handle.cancel();
|
||||
return Err(error.to_string());
|
||||
}
|
||||
}
|
||||
};
|
||||
let render_and_encode_seconds = start.elapsed().as_secs_f64();
|
||||
if status.progress.phase != Phase::Completed {
|
||||
return Err(status.error.map_or_else(|| "render did not complete".into(), |e| e.to_string()));
|
||||
}
|
||||
let exported = session.execute("render.export", &json!({"job":id,"path":output_path,"overwrite":true})).map_err(|e| e.to_string())?;
|
||||
let report = json!({
|
||||
"recipe":recipe_path,"render_params":params,"executor":executor,"adapter":adapter_report,
|
||||
"environment":{"SOLIDCRAFT_GPU":std::env::var("SOLIDCRAFT_GPU").ok(),"WGPU_BACKEND":std::env::var("WGPU_BACKEND").ok()},
|
||||
"cad_generation_seconds":generation_seconds,"cpu_host_setup_seconds":cpu_host_seconds,"gpu_host_setup_seconds":gpu_host_seconds,
|
||||
"render_and_encode_seconds":render_and_encode_seconds,
|
||||
"timing_scope":"wall time from render.start through completed PNG encoding; includes job preparation and readback, excludes CAD generation, preflight and initial CPU/GPU host setup; GPU setup timing includes adapter selection and pipelines when requested",
|
||||
"bodies":bodies,"commands":commands,"command_timings":command_timings,
|
||||
"preflight":preflight,"export":exported,"editable_design":design_path
|
||||
});
|
||||
std::fs::write(output.with_extension("json"), serde_json::to_vec_pretty(&report).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
println!("{}", serde_json::to_string_pretty(&report).map_err(|e| e.to_string())?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
run()
|
||||
}
|
||||
@@ -36,8 +36,9 @@ impl Script {
|
||||
);
|
||||
}
|
||||
|
||||
/// A part placed after it is built: free, even as the design's first part (#2804).
|
||||
fn component(&mut self, name: &str) {
|
||||
self.c("component.create", json!({"name": name}));
|
||||
self.c("component.create", json!({"name": name, "grounded": false}));
|
||||
}
|
||||
|
||||
fn root(&mut self) {
|
||||
|
||||
@@ -0,0 +1,701 @@
|
||||
//! `bench-assistant`: the standard Assistant model test (v1, #4851), run against a desktop app.
|
||||
//!
|
||||
//! The app is already running with `--control PORT`; everything goes through its control channel
|
||||
//! (`docs/control-protocol.md`), so the command needs no window code of its own. For each task it
|
||||
//! starts a new design, runs the task's prescribed setup (a box, for some), adds the endpoint as an
|
||||
//! API agent with All permissions, starts a new chat, sends the prompt once and waits for the turn
|
||||
//! to end. The result is checked on the design (`document.inspect` with measurements), never on
|
||||
//! what the model says, except for the one question whose answer is the reply. A failed task is
|
||||
//! put in one of the issue's failure categories from the conversation's items.
|
||||
//!
|
||||
//! ```text
|
||||
//! solidcraft-cli bench-assistant --connect 127.0.0.1:PORT --base-url URL --model NAME
|
||||
//! [--token T] [--tasks 1,2,5] [--repeat N] [--timeout-s 300]
|
||||
//! [--json out.json] [--markdown out.md]
|
||||
//! ```
|
||||
//! prints a table of the results; `--markdown` writes the issue's comment template filled in,
|
||||
//! `--json` every task's rules, conversation and design before and after.
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// Standard test v1, as #4851 states it. Changing a prompt or a rule makes it another test.
|
||||
const TASKS: &str = r#"[
|
||||
{"id": "01-cube", "prompt": "Make a 20 mm cube.", "check": {"bodies": 1, "volume": 8000, "bbox_size": [20, 20, 20]}},
|
||||
{"id": "02-cylinder", "prompt": "Make a cylinder 30 mm in diameter and 50 mm tall.", "check": {"bodies": 1, "volume": 35342.92, "bbox_size": [30, 30, 50]}},
|
||||
{"id": "03-block", "prompt": "Create a block 60 mm long, 40 mm wide and 10 mm thick.", "check": {"bodies": 1, "volume": 24000, "bbox_size": [10, 40, 60]}},
|
||||
{"id": "04-sketch-extrude", "prompt": "Sketch a 50 by 30 mm rectangle on the XY plane and extrude it 15 mm.", "check": {"bodies": 1, "volume": 22500, "bbox_size": [15, 30, 50], "feature_kinds": ["Extrude"]}},
|
||||
{"id": "05-plate-hole", "prompt": "Make a plate 80 x 60 x 10 mm with a 20 mm diameter hole straight through the middle.", "check": {"bodies": 1, "volume": 44858.41, "bbox_size": [10, 60, 80], "note": "80*60*10 - pi*10^2*10"}},
|
||||
{"id": "06-fillet-box", "prompt": "Round all the edges of the box with a 3 mm radius.", "setup": [{"command": "solid.box", "params": {"length": 40, "width": 40, "height": 40}}], "check": {"bodies": 1, "volume": 63108.53, "tol": 0.003, "feature_kinds": ["Fillet"], "note": "all 12 edges of a 40 mm cube rounded r=3"}},
|
||||
{"id": "07-move-body", "prompt": "Move the box 50 mm along the X axis.", "setup": [{"command": "solid.box", "params": {"length": 20, "width": 20, "height": 20}}], "check": {"bodies": 1, "volume": 8000, "bbox_min": [50, 0, 0]}},
|
||||
{"id": "08-rename-body", "prompt": "Rename the body to Bracket.", "setup": [{"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}], "check": {"bodies": 1, "volume": 6000, "body_name": "Bracket"}},
|
||||
{"id": "09-volume-question", "prompt": "What is the volume of the part?", "setup": [{"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}], "check": {"bodies": 1, "volume": 6000, "unchanged": true, "reply_number": "6[ ,. ]?000(?![0-9])|6(\\.0+)? ?cm"}},
|
||||
{"id": "10-two-holes", "prompt": "Make a 100 x 50 x 5 mm plate and drill two 10 mm holes through it, each 15 mm in from the short ends and centred across the width.", "check": {"bodies": 1, "volume": 24214.6, "bbox_size": [5, 50, 100], "note": "100*50*5 - 2*pi*5^2*5"}},
|
||||
{"id": "11-any-change", "prompt": "can you use the software to make a change to the current shape, any change at all", "setup": [{"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}], "check": {"changed": true, "note": "verbatim user prompt from #4846; pass = the design changed"}}
|
||||
]"#;
|
||||
|
||||
/// The agent the test adds. A fixed id replaces it on every run instead of adding another.
|
||||
const AGENT_ID: &str = "api-bench-assistant";
|
||||
const AGENT_NAME: &str = "bench";
|
||||
/// Longest control reply read (the channel's own request limit).
|
||||
const MAX_REPLY: usize = 16 << 20;
|
||||
/// Most result data kept for `--json`.
|
||||
const MAX_RESULTS: usize = 64 << 20;
|
||||
/// Deadline of the requests that only set up or read (each is answered at once by the app).
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
|
||||
/// How often a running turn is polled.
|
||||
const POLL: Duration = Duration::from_millis(250);
|
||||
/// After this long, an idle chat with no reply yet counts as a turn that ended without one.
|
||||
const START_GRACE: Duration = Duration::from_secs(30);
|
||||
/// Absolute tolerance of the lowest corner, in millimetres.
|
||||
const MIN_CORNER_TOL: f64 = 0.05;
|
||||
|
||||
/// A JSON-lines connection to the app. Requests are sent once; a lost reply is never retried,
|
||||
/// so nothing runs twice.
|
||||
struct Control {
|
||||
addr: SocketAddr,
|
||||
token: String,
|
||||
reader: BufReader<TcpStream>,
|
||||
id: u64,
|
||||
}
|
||||
|
||||
impl Control {
|
||||
fn connect(addr: &str, token: Option<&str>) -> Result<Self, String> {
|
||||
let addr: SocketAddr = addr.parse().map_err(|_| format!("--connect: `{addr}` is not an IP address and port"))?;
|
||||
if !addr.ip().is_loopback() || addr.port() == 0 {
|
||||
return Err("--connect requires a loopback address and a nonzero port (the app listens on 127.0.0.1)".into());
|
||||
}
|
||||
let token = solidcraft_engine::control_auth::client_token(addr.port(), token)
|
||||
.map(|t| t.token)
|
||||
.ok_or("no control token: pass --token, set SOLIDCRAFT_CONTROL_TOKEN, or run on the app's computer (its token file)")?;
|
||||
if token.len() > 4096 {
|
||||
return Err("the control token is longer than 4096 bytes".into());
|
||||
}
|
||||
Self::open(addr, token)
|
||||
}
|
||||
|
||||
fn open(addr: SocketAddr, token: String) -> Result<Self, String> {
|
||||
let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5)).map_err(|e| format!("connecting to {addr}: {e}"))?;
|
||||
Ok(Self { addr, token, reader: BufReader::new(stream), id: 0 })
|
||||
}
|
||||
|
||||
/// A new connection to the same app: a request that outlived its deadline keeps the old one
|
||||
/// busy until it is answered.
|
||||
fn reopen(&self) -> Result<Self, String> {
|
||||
Self::open(self.addr, self.token.clone())
|
||||
}
|
||||
|
||||
fn call(&mut self, method: &str, params: Value, deadline: Instant) -> Result<Value, String> {
|
||||
let left = || deadline.checked_duration_since(Instant::now()).filter(|d| !d.is_zero()).ok_or_else(|| "timeout".to_string());
|
||||
let io = |e: std::io::Error| match e.kind() {
|
||||
std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock => "timeout".to_string(),
|
||||
_ => format!("control channel: {e}"),
|
||||
};
|
||||
self.reader.get_ref().set_write_timeout(Some(left()?)).map_err(io)?;
|
||||
self.id += 1;
|
||||
// The first request on a connection carries the token; sending it on every one is allowed.
|
||||
let request = json!({"id": self.id, "method": method, "params": params, "token": self.token});
|
||||
writeln!(self.reader.get_mut(), "{request}").map_err(io)?;
|
||||
let mut line = Vec::new();
|
||||
loop {
|
||||
self.reader.get_ref().set_read_timeout(Some(left()?)).map_err(io)?;
|
||||
let bytes = self.reader.fill_buf().map_err(io)?;
|
||||
if bytes.is_empty() {
|
||||
return Err("the app closed the control connection".into());
|
||||
}
|
||||
let newline = bytes.iter().position(|b| *b == b'\n');
|
||||
let used = newline.map_or(bytes.len(), |n| n + 1);
|
||||
if line.len() + used > MAX_REPLY {
|
||||
return Err("a control reply is larger than 16 MiB".into());
|
||||
}
|
||||
line.extend_from_slice(bytes.get(..used).unwrap_or_default());
|
||||
self.reader.consume(used);
|
||||
if newline.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let reply: Value = serde_json::from_slice(&line).map_err(|e| format!("control reply: {e}"))?;
|
||||
if reply["id"].as_u64() != Some(self.id) {
|
||||
return Err("control reply for another request".into());
|
||||
}
|
||||
if reply["ok"] != true {
|
||||
return Err(format!("{method}: {}", reply["error"].as_str().unwrap_or("failed")));
|
||||
}
|
||||
reply.get("result").cloned().ok_or_else(|| format!("{method}: reply without a result"))
|
||||
}
|
||||
|
||||
fn execute(&mut self, command: &str, params: Value) -> Result<Value, String> {
|
||||
self.call("engine.execute", json!({"command": command, "params": params, "animate": false}), Instant::now() + REQUEST_TIMEOUT)
|
||||
}
|
||||
|
||||
fn chat(&mut self, params: Value) -> Result<Value, String> {
|
||||
self.call("ui.agent_chat", params, Instant::now() + REQUEST_TIMEOUT)
|
||||
}
|
||||
|
||||
fn inspect(&mut self) -> Result<Value, String> {
|
||||
let design = self.call("document.inspect", json!({"measure": true}), Instant::now() + REQUEST_TIMEOUT)?;
|
||||
if design.is_object() { Ok(design) } else { Err("document.inspect returned no design".into()) }
|
||||
}
|
||||
}
|
||||
|
||||
fn tasks() -> Result<Vec<Value>, String> {
|
||||
serde_json::from_str(TASKS).map_err(|e| format!("embedded tasks: {e}"))
|
||||
}
|
||||
|
||||
fn triple(value: &Value) -> Option<[f64; 3]> {
|
||||
match value.as_array()?.as_slice() {
|
||||
[x, y, z] => {
|
||||
let n = |v: &Value| v.as_f64().filter(|n| n.is_finite());
|
||||
Some([n(x)?, n(y)?, n(z)?])
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// What the model said (thoughts left out).
|
||||
fn reply(items: &[Value]) -> String {
|
||||
items.iter().filter(|i| i["thought"] != true).filter_map(|i| i["agent"].as_str()).collect::<Vec<_>>().join("\n")
|
||||
}
|
||||
|
||||
/// v1's reply pattern `6[ ,. ]?000(?![0-9])|6(\.0+)? ?cm`: "6000", "6 000", "6,000", "6.000" not
|
||||
/// followed by a digit, or "6 cm", "6.0cm", … (a narrow no-break space also separates thousands).
|
||||
fn volume_reply(text: &str) -> bool {
|
||||
text.match_indices('6').any(|(at, _)| {
|
||||
let tail = text.get(at + 1..).unwrap_or_default();
|
||||
let thousands = |t: &str| t.strip_prefix("000").is_some_and(|rest| !rest.starts_with(|c: char| c.is_ascii_digit()));
|
||||
let separated = tail.strip_prefix([' ', ',', '.', '\u{202f}']).is_some_and(thousands);
|
||||
let cm = {
|
||||
let t = tail.strip_prefix(".0").map_or(tail, |t| t.trim_start_matches('0'));
|
||||
t.strip_prefix(' ').unwrap_or(t).starts_with("cm")
|
||||
};
|
||||
thousands(tail) || separated || cm
|
||||
})
|
||||
}
|
||||
|
||||
/// The design as the user sees it, without what reading or saving changes (path, saved state,
|
||||
/// revision, selection, undo depth, evaluation timings).
|
||||
fn content(design: &Value) -> Value {
|
||||
let mut v = design.clone();
|
||||
if let Some(o) = v.as_object_mut() {
|
||||
for key in ["path", "dirty", "revision", "selection", "undo", "redo"] {
|
||||
o.remove(key);
|
||||
}
|
||||
}
|
||||
for feature in v.get_mut("timeline").and_then(Value::as_array_mut).into_iter().flatten() {
|
||||
if let Some(o) = feature.as_object_mut() {
|
||||
o.remove("ms");
|
||||
o.remove("boolean_diagnostics");
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Whether the design changed: its content, or a step on the undo stack (a colour, say, that the
|
||||
/// inspection does not list).
|
||||
fn changed(before: &Value, after: &Value) -> bool {
|
||||
content(before) != content(after) || after["undo"].as_u64() > before["undo"].as_u64()
|
||||
}
|
||||
|
||||
/// The rules a task's result breaks (none: it passed). `before` and `after` are
|
||||
/// `document.inspect {measure: true}` before the prompt and after the turn.
|
||||
pub(super) fn check(rule: &Value, before: &Value, after: &Value, items: &[Value]) -> Vec<String> {
|
||||
let Some(all) = after["bodies"].as_array() else { return vec!["the design could not be read".into()] };
|
||||
// Bodies the user sees; a hidden one (a tool body, a hidden original) is not the result.
|
||||
let bodies: Vec<&Value> = all.iter().filter(|b| b["visible"] != false).collect();
|
||||
let mut errors = Vec::new();
|
||||
if let Some(n) = rule["bodies"].as_u64()
|
||||
&& bodies.len() as u64 != n
|
||||
{
|
||||
errors.push(format!("{} bodies, expected {n}", bodies.len()));
|
||||
}
|
||||
if let Some(expected) = rule["volume"].as_f64() {
|
||||
let tol = rule["tol"].as_f64().unwrap_or(0.01);
|
||||
let volumes: Vec<f64> = bodies.iter().filter_map(|b| b["volume_mm3"].as_f64()).filter(|v| v.is_finite()).collect();
|
||||
let total: f64 = volumes.iter().sum();
|
||||
if volumes.len() != bodies.len() || (total - expected).abs() > expected * tol {
|
||||
errors.push(format!("volume {total:.1} mm³, expected {expected} mm³ ±{}%", tol * 100.0));
|
||||
}
|
||||
}
|
||||
let first = bodies.first().copied().unwrap_or(&Value::Null);
|
||||
let min = triple(&first["bbox"]["min"]);
|
||||
if let Some(mut expected) = triple(&rule["bbox_size"]) {
|
||||
expected.sort_by(f64::total_cmp);
|
||||
let size = min.zip(triple(&first["bbox"]["max"])).map(|(lo, hi)| {
|
||||
let mut size = [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]];
|
||||
size.sort_by(f64::total_cmp);
|
||||
size
|
||||
});
|
||||
if !size.is_some_and(|s| s.iter().zip(expected).all(|(a, e)| (a - e).abs() <= 0.01 * e.max(1.0))) {
|
||||
errors.push(format!("size {size:?}, expected {expected:?} in any orientation"));
|
||||
}
|
||||
}
|
||||
if let Some(expected) = triple(&rule["bbox_min"])
|
||||
&& !min.is_some_and(|m| m.iter().zip(expected).all(|(a, e)| (a - e).abs() <= MIN_CORNER_TOL))
|
||||
{
|
||||
errors.push(format!("lowest corner {min:?}, expected {expected:?}"));
|
||||
}
|
||||
if let Some(name) = rule["body_name"].as_str()
|
||||
&& !bodies.iter().any(|b| b["name"] == name)
|
||||
{
|
||||
errors.push(format!("no body named {name}"));
|
||||
}
|
||||
for kind in rule["feature_kinds"].as_array().into_iter().flatten().filter_map(Value::as_str) {
|
||||
let live = |f: &&Value| f["suppressed"] != true && f["rolled_back"] != true && f["error"].is_null();
|
||||
if !after["timeline"].as_array().into_iter().flatten().filter(live).any(|f| f["type"].as_str().is_some_and(|t| t.contains(kind))) {
|
||||
errors.push(format!("no {kind} in the timeline"));
|
||||
}
|
||||
}
|
||||
if rule["changed"] == true || rule["unchanged"] == true {
|
||||
let changed = before.is_object() && changed(before, after);
|
||||
if rule["changed"] == true && !changed {
|
||||
errors.push("the design did not change".into());
|
||||
}
|
||||
if rule["unchanged"] == true && (changed || !before.is_object()) {
|
||||
errors.push("the design changed".into());
|
||||
}
|
||||
}
|
||||
if rule["reply_number"].is_string() && !volume_reply(&reply(items)) {
|
||||
errors.push("the reply does not give the volume".into());
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
||||
/// A tool call written into the reply as JSON: `{"name": …`, `{"command": …`, `{"function": …`,
|
||||
/// `{"tool": …`.
|
||||
fn call_as_text(text: &str) -> bool {
|
||||
text.match_indices('{').any(|(at, _)| {
|
||||
let rest = text.get(at + 1..).unwrap_or_default().trim_start();
|
||||
["\"name\"", "\"command\"", "\"function\"", "\"tool\""]
|
||||
.iter()
|
||||
.any(|key| rest.strip_prefix(key).is_some_and(|r| r.trim_start().starts_with(':')))
|
||||
})
|
||||
}
|
||||
|
||||
/// How a failed task failed, in the issue's words, from the conversation's items.
|
||||
pub(super) fn classify(items: &[Value], timed_out: bool) -> &'static str {
|
||||
if timed_out {
|
||||
return "timeout";
|
||||
}
|
||||
let tools: Vec<&Value> = items.iter().filter(|i| i.get("tool").is_some()).collect();
|
||||
// A failed call, or a batch whose answer reports a failed step (`"ok": false`).
|
||||
let failed: Vec<String> = tools
|
||||
.iter()
|
||||
.map(|t| t["output"].as_str().unwrap_or_default())
|
||||
.zip(&tools)
|
||||
.filter(|(output, t)| t["status"] == "Failed" || serde_json::from_str::<Value>(output).is_ok_and(|v| v["ok"] == false))
|
||||
.map(|(output, _)| output.to_lowercase())
|
||||
.collect();
|
||||
let notices: Vec<String> = items.iter().filter_map(|i| i["notice"].as_str()).map(str::to_lowercase).collect();
|
||||
let answer = reply(items);
|
||||
if notices.iter().any(|n| n.contains("http 400")) {
|
||||
return "the endpoint refused tools (HTTP 400)";
|
||||
}
|
||||
if notices.iter().any(|n| n.contains("max_turn_requests")) || answer.contains("requests for one message") {
|
||||
return "hit the request limit";
|
||||
}
|
||||
if call_as_text(&answer) {
|
||||
return "wrote the call as text";
|
||||
}
|
||||
if failed.iter().any(|o| o.contains("unknown command") || o.starts_with("unknown tool")) {
|
||||
return "wrong or unknown command";
|
||||
}
|
||||
if failed.iter().any(|o| o.contains("invalid parameters") || o.starts_with("not run:") || o.contains("not valid json")) {
|
||||
return "wrong arguments";
|
||||
}
|
||||
if !failed.is_empty() || notices.iter().any(|n| n.starts_with("http ")) || items.iter().any(|i| i["error"] == true) {
|
||||
return "gave up after errors";
|
||||
}
|
||||
if tools.is_empty() {
|
||||
return if answer.trim().is_empty() { "an empty reply" } else { "only talked" };
|
||||
}
|
||||
"wrong result"
|
||||
}
|
||||
|
||||
/// One run's results in the issue's comment template (`run`: which repetition).
|
||||
pub(super) fn markdown(model: &str, version: &str, results: &[Value], run: u64) -> String {
|
||||
let clean = |s: &str| s.replace(['\n', '\r', '|'], " ");
|
||||
let rows: Vec<&Value> = results.iter().filter(|r| r["repeat"] == run).collect();
|
||||
let passed = rows.iter().filter(|r| r["passed"] == true).count();
|
||||
let mut out = format!(
|
||||
"### {} (<quant>, <download size>)\nRuntime: <Ollama/LM Studio/... version>, context <num_ctx if changed>\nSolidCraft: {}\nResult: {passed}/11\n| # | pass/fail | how it failed |\n|---|---|---|\n",
|
||||
clean(model),
|
||||
clean(version)
|
||||
);
|
||||
for n in 1..=11 {
|
||||
match rows.iter().find(|r| r["task"] == n) {
|
||||
Some(r) if r["passed"] == true => out.push_str(&format!("| {n} | pass | |\n")),
|
||||
Some(r) => out.push_str(&format!("| {n} | fail | {} |\n", clean(r["failure"].as_str().unwrap_or_default()))),
|
||||
None => out.push_str(&format!("| {n} | | not run |\n")),
|
||||
}
|
||||
}
|
||||
out.push_str(if rows.len() == 11 {
|
||||
"Notes: <anything odd, e.g. very slow, needed a retry>\n"
|
||||
} else {
|
||||
"Notes: partial run; the tasks marked \"not run\" were left out.\n"
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
fn write(path: &str, text: &str) -> Result<(), String> {
|
||||
solidcraft_engine::io::vfs::write(path, text.as_bytes()).map_err(|e| format!("{path}: {e}"))
|
||||
}
|
||||
|
||||
struct Options<'a> {
|
||||
connect: &'a str,
|
||||
token: Option<&'a str>,
|
||||
base_url: &'a str,
|
||||
model: &'a str,
|
||||
tasks: Vec<usize>,
|
||||
repeat: u64,
|
||||
timeout: Duration,
|
||||
json: Option<&'a str>,
|
||||
markdown: Option<&'a str>,
|
||||
}
|
||||
|
||||
fn options(args: &[String]) -> Result<Options<'_>, String> {
|
||||
const FLAGS: [&str; 9] = ["--connect", "--token", "--base-url", "--model", "--tasks", "--repeat", "--timeout-s", "--json", "--markdown"];
|
||||
let mut given = std::collections::BTreeMap::new();
|
||||
let mut args = args.iter();
|
||||
while let Some(flag) = args.next() {
|
||||
let Some(flag) = FLAGS.iter().find(|f| **f == flag.as_str()) else { return Err(format!("unknown bench-assistant option: {flag}")) };
|
||||
let value = args
|
||||
.next()
|
||||
.filter(|v| !v.starts_with("--") && !v.is_empty() && v.len() <= 4096)
|
||||
.ok_or_else(|| format!("{flag} needs a value (at most 4096 bytes)"))?;
|
||||
if given.insert(*flag, value.as_str()).is_some() {
|
||||
return Err(format!("{flag} given twice"));
|
||||
}
|
||||
}
|
||||
let required = |key: &str| given.get(key).copied().ok_or_else(|| format!("bench-assistant needs {key}"));
|
||||
let number = |key: &str, default: u64, max: u64| -> Result<u64, String> {
|
||||
given
|
||||
.get(key)
|
||||
.map_or(Ok(default), |s| s.parse::<u64>().ok().filter(|n| (1..=max).contains(n)).ok_or_else(|| format!("{key} must be 1 to {max}")))
|
||||
};
|
||||
let tasks = match given.get("--tasks") {
|
||||
None => (1..=11).collect(),
|
||||
Some(list) => {
|
||||
let mut tasks = Vec::new();
|
||||
for n in list.split(',') {
|
||||
let n = n
|
||||
.trim()
|
||||
.parse::<usize>()
|
||||
.ok()
|
||||
.filter(|n| (1..=11).contains(n))
|
||||
.ok_or("--tasks takes task numbers 1 to 11, separated by commas")?;
|
||||
if tasks.contains(&n) {
|
||||
return Err(format!("--tasks lists task {n} twice"));
|
||||
}
|
||||
tasks.push(n);
|
||||
}
|
||||
tasks
|
||||
}
|
||||
};
|
||||
Ok(Options {
|
||||
connect: required("--connect")?,
|
||||
token: given.get("--token").copied(),
|
||||
base_url: required("--base-url")?,
|
||||
model: required("--model")?,
|
||||
tasks,
|
||||
repeat: number("--repeat", 1, 100)?,
|
||||
timeout: Duration::from_secs(number("--timeout-s", 300, 3600)?),
|
||||
json: given.get("--json").copied(),
|
||||
markdown: given.get("--markdown").copied(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Waits for the turn the prompt started to end. Returns the chat's last state and whether the
|
||||
/// deadline passed first.
|
||||
fn wait_for_turn(control: &mut Control, mut chat: Value, began: Instant, end: Instant) -> Result<(Value, bool), String> {
|
||||
let mut seen_busy = false;
|
||||
loop {
|
||||
let state = chat["state"].as_str().unwrap_or_default();
|
||||
let turn = !chat["turn"].is_null();
|
||||
let queued = chat["pending"].as_array().is_some_and(|p| !p.is_empty());
|
||||
seen_busy |= state == "busy" || turn;
|
||||
let idle = !matches!(state, "busy" | "starting") && !turn && !queued;
|
||||
// A turn can start and end between two polls: then the conversation has its answer.
|
||||
let answered = chat["items"].as_array().into_iter().flatten().any(|i| i.get("user").is_none());
|
||||
if idle && (seen_busy || answered || matches!(state, "exited" | "auth" | "off") || began.elapsed() >= START_GRACE) {
|
||||
return Ok((chat, false));
|
||||
}
|
||||
let Some(left) = end.checked_duration_since(Instant::now()).filter(|d| !d.is_zero()) else { return Ok((chat, true)) };
|
||||
std::thread::sleep(POLL.min(left));
|
||||
match control.call("ui.agent_chat", json!({}), end) {
|
||||
Ok(next) => chat = next,
|
||||
Err(e) if e == "timeout" => return Ok((chat, true)),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
let o = options(args)?;
|
||||
let tasks = tasks()?;
|
||||
let mut control = Control::connect(o.connect, o.token)?;
|
||||
let help = control.call("ui.help", json!({}), Instant::now() + REQUEST_TIMEOUT)?;
|
||||
let version = help["version"].as_str().filter(|s| !s.is_empty() && s.len() <= 256).ok_or("the app reported no version")?.to_string();
|
||||
let config = control.chat(json!({
|
||||
"features": true,
|
||||
"add_agent": {"id": AGENT_ID, "name": AGENT_NAME, "kind": "api", "base_url": o.base_url, "model": o.model},
|
||||
}))?;
|
||||
if !config["agents"].as_array().into_iter().flatten().any(|a| a["id"] == AGENT_ID) {
|
||||
return Err("the app did not add the benchmark's agent".into());
|
||||
}
|
||||
// Put the user's permission setting back afterwards.
|
||||
let permissions = config["permissions"].as_str().map(str::to_string);
|
||||
let result = run_tasks(&mut control, &o, &tasks, &version);
|
||||
let _ = control.chat(json!({"stop": true}));
|
||||
if let Some(p) = permissions {
|
||||
let _ = control.chat(json!({"permissions": p}));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn run_tasks(control: &mut Control, o: &Options, tasks: &[Value], version: &str) -> Result<(), String> {
|
||||
let mut results: Vec<Value> = Vec::new();
|
||||
let mut kept = 0;
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
let mut say = |line: String| writeln!(stdout, "{line}").map_err(|e| format!("stdout: {e}"));
|
||||
say(format!("bench-assistant v1: {} on SolidCraft {version}", o.model))?;
|
||||
say("| run | # | task | pass/fail | how it failed | seconds |\n|---|---|---|---|---|---|".into())?;
|
||||
for run in 1..=o.repeat {
|
||||
for &n in &o.tasks {
|
||||
let task = tasks.get(n - 1).ok_or("missing embedded task")?;
|
||||
// A fresh design and a fresh chat, with nothing of the last task still running.
|
||||
control.chat(json!({"stop": true}))?;
|
||||
control.execute("file.new", json!({"name": "Benchmark"}))?;
|
||||
for step in task["setup"].as_array().into_iter().flatten() {
|
||||
control.execute(step["command"].as_str().ok_or("embedded setup without a command")?, step["params"].clone())?;
|
||||
}
|
||||
control.chat(json!({"agent": AGENT_ID, "permissions": "all", "new_chat": true, "attach_view": false}))?;
|
||||
let before = control.inspect()?;
|
||||
let began = Instant::now();
|
||||
let end = began + o.timeout;
|
||||
let (chat, timed_out) = match control.call("ui.agent_chat", json!({"send": task["prompt"]}), end) {
|
||||
Ok(chat) => wait_for_turn(control, chat, began, end)?,
|
||||
Err(e) if e == "timeout" => (json!({}), true),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
if timed_out {
|
||||
// The late reply would arrive on the old connection; stop the model on a new one
|
||||
// before reading the design.
|
||||
*control = control.reopen()?;
|
||||
}
|
||||
// Stop the agent before reading the result, so nothing changes the design meanwhile.
|
||||
let stopped = control.chat(json!({"stop": true}))?;
|
||||
let items = stopped["items"].as_array().or(chat["items"].as_array()).cloned().unwrap_or_default();
|
||||
let after = control.inspect()?;
|
||||
let failed_rules = check(&task["check"], &before, &after, &items);
|
||||
let passed = failed_rules.is_empty();
|
||||
let failure = if passed { "" } else { classify(&items, timed_out) };
|
||||
let seconds = began.elapsed().as_secs_f64();
|
||||
say(format!(
|
||||
"| {run} | {n} | {} | {} | {failure} | {seconds:.0} |",
|
||||
task["id"].as_str().unwrap_or_default(),
|
||||
if passed { "pass" } else { "fail" }
|
||||
))?;
|
||||
let result = json!({
|
||||
"task": n, "id": task["id"], "prompt": task["prompt"], "repeat": run, "passed": passed, "failure": failure,
|
||||
"failed_rules": failed_rules, "timed_out": timed_out, "seconds": seconds, "check": task["check"],
|
||||
"items": items, "before": before, "after": after,
|
||||
});
|
||||
kept += result.to_string().len();
|
||||
if kept > MAX_RESULTS {
|
||||
return Err("the results passed 64 MiB; run fewer tasks or repetitions".into());
|
||||
}
|
||||
results.push(result);
|
||||
// Written after every task, so an interrupted run keeps what it measured.
|
||||
if let Some(path) = o.json {
|
||||
let all = json!({"test": "v1", "model": o.model, "base_url": o.base_url, "solidcraft": version, "results": results});
|
||||
write(path, &serde_json::to_string_pretty(&all).map_err(|e| e.to_string())?)?;
|
||||
}
|
||||
if let Some(path) = o.markdown {
|
||||
write(path, &(1..=run).map(|r| markdown(o.model, version, &results, r)).collect::<Vec<_>>().join("\n"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for run in 1..=o.repeat {
|
||||
let rows: Vec<&Value> = results.iter().filter(|r| r["repeat"] == run).collect();
|
||||
say(format!("run {run}: {}/{} passed", rows.iter().filter(|r| r["passed"] == true).count(), rows.len()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn design(bodies: Value, timeline: Value) -> Value {
|
||||
json!({"name": "Benchmark", "params": [], "timeline": timeline, "bodies": bodies, "sketches": [], "undo": 1, "revision": 3, "dirty": true})
|
||||
}
|
||||
|
||||
fn body(name: &str, volume: f64, min: [f64; 3], max: [f64; 3]) -> Value {
|
||||
json!({"name": name, "volume_mm3": volume, "bbox": {"min": min, "max": max}, "visible": true})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_embedded_tasks_are_v1() {
|
||||
let tasks = tasks().unwrap();
|
||||
assert_eq!(tasks.len(), 11);
|
||||
assert_eq!(tasks[0]["prompt"], "Make a 20 mm cube.");
|
||||
assert_eq!(tasks[10]["prompt"], "can you use the software to make a change to the current shape, any change at all");
|
||||
assert_eq!(tasks[5]["check"]["tol"], 0.003);
|
||||
assert_eq!(tasks[6]["check"]["bbox_min"], json!([50, 0, 0]));
|
||||
let boxes: Vec<_> = tasks.iter().filter(|t| t["setup"].is_array()).map(|t| t["id"].as_str().unwrap()).collect();
|
||||
assert_eq!(boxes, ["06-fillet-box", "07-move-body", "08-rename-body", "09-volume-question", "11-any-change"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pass_rules() {
|
||||
let tasks = tasks().unwrap();
|
||||
let rule = |n: usize| tasks[n - 1]["check"].clone();
|
||||
let empty = design(json!([]), json!([]));
|
||||
let pass = |n: usize, before: &Value, after: &Value, items: &[Value]| check(&rule(n), before, after, items);
|
||||
// 3: any orientation, within 1%.
|
||||
let block = design(json!([body("Body1", 24_100.0, [0.0; 3], [40.0, 10.0, 60.2])]), json!([{"type": "Box"}]));
|
||||
assert!(pass(3, &empty, &block, &[]).is_empty());
|
||||
let thick = design(json!([body("Body1", 24_000.0, [0.0; 3], [40.0, 12.0, 60.0])]), json!([]));
|
||||
assert_eq!(pass(3, &empty, &thick, &[]).len(), 1);
|
||||
let two = design(json!([body("A", 12_000.0, [0.0; 3], [60.0, 40.0, 5.0]), body("B", 12_000.0, [0.0; 3], [60.0, 40.0, 5.0])]), json!([]));
|
||||
assert!(!pass(3, &empty, &two, &[]).is_empty());
|
||||
// A hidden body is not part of the result.
|
||||
let mut hidden = two.clone();
|
||||
hidden["bodies"][1]["visible"] = json!(false);
|
||||
hidden["bodies"][0]["volume_mm3"] = json!(24_000.0);
|
||||
hidden["bodies"][0]["bbox"]["max"] = json!([60.0, 40.0, 10.0]);
|
||||
assert!(pass(3, &empty, &hidden, &[]).is_empty());
|
||||
// 4: the timeline needs a working Extrude.
|
||||
let plate = |kind: Value| design(json!([body("Body1", 22_500.0, [0.0; 3], [50.0, 30.0, 15.0])]), kind);
|
||||
assert!(pass(4, &empty, &plate(json!([{"type": "Sketch"}, {"type": "Extrude"}])), &[]).is_empty());
|
||||
assert!(!pass(4, &empty, &plate(json!([{"type": "Box"}])), &[]).is_empty());
|
||||
for flag in [json!({"suppressed": true}), json!({"rolled_back": true}), json!({"error": "failed"})] {
|
||||
let mut feature = json!({"type": "Extrude"});
|
||||
feature.as_object_mut().unwrap().extend(flag.as_object().unwrap().clone());
|
||||
assert!(!pass(4, &empty, &plate(json!([feature])), &[]).is_empty(), "{flag}");
|
||||
}
|
||||
// 6: the fillet's tolerance is 0.3%.
|
||||
let start = design(json!([body("Body1", 64_000.0, [0.0; 3], [40.0; 3])]), json!([{"type": "Box"}]));
|
||||
let filleted = |v: f64| design(json!([body("Body1", v, [0.0; 3], [40.0; 3])]), json!([{"type": "Box"}, {"type": "Fillet"}]));
|
||||
assert!(pass(6, &start, &filleted(63_108.53 * 1.002), &[]).is_empty());
|
||||
assert!(!pass(6, &start, &filleted(63_108.53 * 1.004), &[]).is_empty());
|
||||
// 7: the lowest corner.
|
||||
let moved = |x: f64| design(json!([body("Body1", 8000.0, [x, 0.0, 0.0], [x + 20.0, 20.0, 20.0])]), json!([]));
|
||||
assert!(pass(7, &empty, &moved(50.0), &[]).is_empty());
|
||||
assert!(!pass(7, &empty, &moved(0.0), &[]).is_empty());
|
||||
// 8: the name.
|
||||
let named = |name: &str| design(json!([body(name, 6000.0, [0.0; 3], [30.0, 20.0, 10.0])]), json!([]));
|
||||
assert!(pass(8, &empty, &named("Bracket"), &[]).is_empty());
|
||||
assert!(!pass(8, &empty, &named("Body1"), &[]).is_empty());
|
||||
// 9: unchanged, and the reply gives the volume.
|
||||
let start = named("Body1");
|
||||
let said = |text: &str| [json!({"user": "What is the volume of the part?"}), json!({"agent": text, "thought": false})];
|
||||
let mut read = start.clone();
|
||||
read["revision"] = json!(7);
|
||||
read["dirty"] = json!(false);
|
||||
read["selection"] = json!([{"body": "Body1"}]);
|
||||
assert!(pass(9, &start, &read, &said("The volume is 6000 mm³.")).is_empty());
|
||||
assert!(!pass(9, &start, &read, &said("About 60 cm³.")).is_empty());
|
||||
assert!(!pass(9, &start, &named("Bracket"), &said("6 000 mm³")).is_empty());
|
||||
let mut undone = start.clone();
|
||||
undone["undo"] = json!(2);
|
||||
assert!(!pass(9, &start, &undone, &said("6000 mm³")).is_empty());
|
||||
// 11: any change counts; reading does not.
|
||||
assert!(pass(11, &start, &named("Bracket"), &[]).is_empty());
|
||||
assert!(pass(11, &start, &undone, &[]).is_empty());
|
||||
assert!(!pass(11, &start, &read, &[]).is_empty());
|
||||
// An unreadable design passes nothing.
|
||||
assert!(!pass(11, &start, &json!({}), &[]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_volume_reply() {
|
||||
for yes in ["6000", "6 000 mm³", "6,000", "6.000", "6\u{202f}000", "6 cm³", "6.00cm3", "is 6000."] {
|
||||
assert!(volume_reply(yes), "{yes}");
|
||||
}
|
||||
for no in ["60000", "6,0001", "60 cm³", "600", "6.01 cm³", ""] {
|
||||
assert!(!volume_reply(no), "{no}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failure_categories() {
|
||||
for (items, expected) in [
|
||||
(
|
||||
json!([{"user": "Make a 20 mm cube."}, {"agent": "{\"name\": \"execute\", \"arguments\": {}}", "thought": false}]),
|
||||
"wrote the call as text",
|
||||
),
|
||||
(
|
||||
json!([{"notice": "HTTP 400: registry.ollama.ai/library/gemma3:4b does not support tools", "error": true}]),
|
||||
"the endpoint refused tools (HTTP 400)",
|
||||
),
|
||||
(json!([{"user": "Make a 20 mm cube."}, {"agent": "", "thought": false}]), "an empty reply"),
|
||||
(json!([{"agent": "Let me think", "thought": true}]), "an empty reply"),
|
||||
(json!([{"agent": "Sure, here is how to make a cube.", "thought": false}]), "only talked"),
|
||||
(
|
||||
json!([{"tool": "1", "status": "Failed", "output": "unknown command `solid.cube` (did you mean `solid.box`?)"}]),
|
||||
"wrong or unknown command",
|
||||
),
|
||||
(json!([{"tool": "1", "status": "Failed", "output": "unknown tool `make_cube`"}]), "wrong or unknown command"),
|
||||
(json!([{"tool": "1", "status": "Failed", "output": "invalid parameters for `solid.box`: `length` is required"}]), "wrong arguments"),
|
||||
(json!([{"tool": "1", "status": "Failed", "output": "Not run: the tool arguments were not valid JSON"}]), "wrong arguments"),
|
||||
(json!([{"tool": "1", "status": "Failed", "output": "no body `Box`"}]), "gave up after errors"),
|
||||
(
|
||||
json!([{"tool": "1", "status": "Completed", "output": "{\"ok\": false, \"failed\": {\"error\": \"unknown command `solid.delete`\"}}"}]),
|
||||
"wrong or unknown command",
|
||||
),
|
||||
(
|
||||
json!([{"tool": "1", "status": "Completed"}, {"agent": "\n(stopped after 24 requests for one message; send another message to go on)"}]),
|
||||
"hit the request limit",
|
||||
),
|
||||
(json!([{"tool": "1", "status": "Completed", "output": "{}"}]), "wrong result"),
|
||||
] {
|
||||
let items = items.as_array().unwrap();
|
||||
assert_eq!(classify(items, false), expected, "{items:?}");
|
||||
assert_eq!(classify(items, true), "timeout");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_is_the_comment_template() {
|
||||
let rows: Vec<Value> =
|
||||
(1..=11).map(|n| json!({"task": n, "repeat": 1, "passed": n != 2, "failure": if n == 2 { "an empty reply" } else { "" }})).collect();
|
||||
let report = markdown("qwen2.5:7b", "0.19.0", &rows, 1);
|
||||
assert!(report.starts_with(
|
||||
"### qwen2.5:7b (<quant>, <download size>)\nRuntime: <Ollama/LM Studio/... version>, context <num_ctx if changed>\nSolidCraft: 0.19.0\nResult: 10/11\n| # | pass/fail | how it failed |\n|---|---|---|\n| 1 | pass | |\n| 2 | fail | an empty reply |\n"
|
||||
));
|
||||
assert!(report.ends_with("| 11 | pass | |\nNotes: <anything odd, e.g. very slow, needed a retry>\n"));
|
||||
let partial = markdown("m|x", "v", &rows[..1], 1);
|
||||
assert!(partial.starts_with("### m x ("));
|
||||
assert!(partial.contains("Result: 1/11") && partial.contains("| 2 | | not run |") && partial.contains("partial run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_are_checked_before_connecting() {
|
||||
for extra in [
|
||||
&["--repeat", "0"][..],
|
||||
&["--repeat", "101"],
|
||||
&["--timeout-s", "3601"],
|
||||
&["--tasks", "0"],
|
||||
&["--tasks", "12"],
|
||||
&["--tasks", "1,1"],
|
||||
&["--model", "again"],
|
||||
&["--unexpected", "1"],
|
||||
&["--token"],
|
||||
] {
|
||||
let mut args: Vec<String> =
|
||||
["--connect", "127.0.0.1:9", "--model", "m", "--base-url", "http://127.0.0.1:11434/v1"].iter().map(|s| s.to_string()).collect();
|
||||
args.extend(extra.iter().map(|s| s.to_string()));
|
||||
assert!(options(&args).is_err(), "{extra:?}");
|
||||
}
|
||||
let args: Vec<String> =
|
||||
["--connect", "127.0.0.1:9", "--model", "m", "--base-url", "u", "--tasks", "2, 5"].iter().map(|s| s.to_string()).collect();
|
||||
let o = options(&args).unwrap();
|
||||
assert_eq!((o.tasks, o.repeat, o.timeout), (vec![2, 5], 1, Duration::from_secs(300)));
|
||||
assert!(Control::connect("10.0.0.1:9271", Some("t")).is_err());
|
||||
assert!(Control::connect("localhost", Some("t")).is_err());
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,14 @@ use std::sync::Mutex;
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::crash;
|
||||
|
||||
const USAGE: &str = "usage: solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]";
|
||||
/// The command's usage, from the same lines the top-level usage lists.
|
||||
fn usage() -> String {
|
||||
crate::command_usage("crashes")
|
||||
}
|
||||
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
let dir = crash::default_dir().ok_or("no crash folder on this system")?;
|
||||
let id = || args.get(1).map(String::as_str).ok_or_else(|| USAGE.to_string());
|
||||
let id = || args.get(1).map(String::as_str).ok_or_else(usage);
|
||||
match args.first().map(String::as_str) {
|
||||
None | Some("list") => {
|
||||
let reports = crash::list(&dir);
|
||||
@@ -43,7 +46,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
println!("deleted {} report(s)", crash::clear(&dir, args.get(1).map(String::as_str)));
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(USAGE.to_string()),
|
||||
_ => Err(usage()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,16 @@ fn flag<T: std::str::FromStr>(args: &[String], name: &str, default: T) -> T {
|
||||
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// The options `fuzz` takes, and the ones that stand alone.
|
||||
const OPTIONS: &[&str] = &["--from", "--count", "--jobs", "--timeout", "--steps", "--out", "--report"];
|
||||
const SWITCHES: &[&str] = &["--minimise"];
|
||||
|
||||
/// One seed: a design and a boolean case, as JSON on stdout (progress on stderr).
|
||||
pub fn one(args: &[String]) -> Result<(), String> {
|
||||
let seed: u64 = args.first().and_then(|s| s.parse().ok()).ok_or("fuzz-one <seed> [--steps N] [--minimise]")?;
|
||||
// The seed is the first argument; the rest are options, and an option `fuzz-one` doesn't
|
||||
// take is refused rather than ignored.
|
||||
crate::known_args("fuzz-one", &args[1..], &["--steps"], SWITCHES, false)?;
|
||||
let steps = flag(args, "--steps", 8usize);
|
||||
let minimise = args.iter().any(|a| a == "--minimise");
|
||||
// Panics caught inside the kernel: harmless natively, fatal in the web build.
|
||||
@@ -64,6 +71,9 @@ struct Job {
|
||||
/// Run seeds `--from`…`--from + --count` in `--jobs` processes, `--timeout` seconds each;
|
||||
/// write failing scripts to `--out` and a summary to `--report`.
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
// An option `fuzz` doesn't take is refused before a seed runs: it would otherwise be
|
||||
// ignored, and the run would go on for minutes with the wrong settings.
|
||||
crate::known_args("fuzz", args, OPTIONS, SWITCHES, false)?;
|
||||
let from = flag(args, "--from", 0u64);
|
||||
let count = flag(args, "--count", 100u64);
|
||||
let last = from.checked_add(count.saturating_sub(1)).ok_or("seed range exceeds u64::MAX: reduce --from or --count")?;
|
||||
|
||||
@@ -6,15 +6,19 @@
|
||||
//! solidcraft-cli script <design.solidcraft> [--out script.json] the design's history as a command script
|
||||
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] loading timings as JSON lines
|
||||
//! solidcraft-cli bench-agent [--scale F] [--save F] [--script-out F] a ~1,300-body design built command by command, timed
|
||||
//! 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]
|
||||
//! [--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 mcp [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
//! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
//! [--param-check off|warn|strict]
|
||||
//! MCP server on stdio (docs/mcp.md)
|
||||
//! solidcraft-cli mcp setup [--dry-run] [--client NAME] [--live] (alias: mcp install) add it to OpenCode, Claude Desktop, Claude Code, Cursor
|
||||
//! solidcraft-cli licences SolidCraft's licence and the third-party licences
|
||||
//! solidcraft-cli icon --out F.png|F.ico [--size N] the app icon (packaging; drawn, not a file)
|
||||
//!
|
||||
@@ -24,6 +28,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod bench_agent;
|
||||
mod bench_assistant;
|
||||
mod crashes;
|
||||
mod fuzz;
|
||||
mod logger;
|
||||
@@ -42,57 +47,98 @@ mod app_icon;
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::view::Guides;
|
||||
|
||||
const USAGE: &str = "usage:
|
||||
solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]
|
||||
solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...
|
||||
(a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)
|
||||
(--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;
|
||||
--profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)
|
||||
solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect; or --in FILE)
|
||||
(--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,
|
||||
as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)
|
||||
solidcraft-cli script <design.solidcraft|script.json> [--out script.json]
|
||||
(the design's history as a command script that eval and run replay)
|
||||
solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]
|
||||
(--ops: after opening, run each command of the script and undo it, timing the command, the
|
||||
evaluations it caused and the view it leaves: placing and meshing what changed)
|
||||
solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]
|
||||
solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]
|
||||
[--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
|
||||
solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...
|
||||
(as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;
|
||||
--view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;
|
||||
--camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);
|
||||
--no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;
|
||||
--body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside
|
||||
it shows) and color #rrggbb, either or both, once per body;
|
||||
--tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body
|
||||
is visible inside the material it meets; a later --body overrides it;
|
||||
--section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world
|
||||
coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)
|
||||
solidcraft-cli render <script|design> --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS]
|
||||
solidcraft-cli exec <command> [json-params]
|
||||
solidcraft-cli commands
|
||||
solidcraft-cli recipe <recipe.json>
|
||||
solidcraft-cli oracle <case-dir>... [--json]
|
||||
solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]
|
||||
solidcraft-cli mcp [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
[--param-check off|warn|strict]
|
||||
solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]
|
||||
solidcraft-cli licences
|
||||
solidcraft-cli gpu [--policy auto|hardware|software|cpu]
|
||||
(the GPU adapters a headless render can open, best first, the one the policy picks and the tier it
|
||||
lands on: gpu, gpu-software, or the CPU rasterizer (auto with no adapter, or cpu); the policy defaults
|
||||
to SOLIDCRAFT_GPU, else auto; exits with an error when hardware or software finds no such adapter)
|
||||
solidcraft-cli icon --out icon.png [--size 256] | --out icon.ico
|
||||
--debug, with any command: what SolidCraft logs (why a body has no display plan, why a save left its
|
||||
model cache out), on stderr; RUST_LOG=error|warn|info|debug|trace sets one level for every crate
|
||||
";
|
||||
/// One command the CLI takes: the name to type, the usage it prints for `--help`,
|
||||
/// and whether the top-level usage lists it (an alias answers to another command's name).
|
||||
struct Command {
|
||||
name: &'static str,
|
||||
usage: &'static str,
|
||||
listed: bool,
|
||||
}
|
||||
|
||||
/// `run`'s lines in the usage.
|
||||
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
|
||||
/// `eval`'s lines in the usage.
|
||||
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect; or --in FILE)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
|
||||
/// `script`'s lines in the usage.
|
||||
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.solidcraft|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
|
||||
/// `bench-open`'s lines in the usage.
|
||||
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
|
||||
/// `bench-agent`'s lines in the usage.
|
||||
const USAGE_BENCH_AGENT: &str = " solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]\n";
|
||||
/// `bench-assistant`'s lines in the usage.
|
||||
const USAGE_BENCH_ASSISTANT: &str = " solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N] [--timeout-s 300] [--json out.json] [--markdown out.md]\n";
|
||||
/// `snapshot`'s lines in the usage.
|
||||
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]\n solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)\n";
|
||||
/// `render`'s lines in the usage.
|
||||
const USAGE_RENDER: &str = " solidcraft-cli render <script|design> --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS]\n [--progressive] [--samples N] [--max-depth N] [--seed N] [--exposure EV] [--lighting FILE.json] [--environment FILE.json] [--tone-map reinhard|aces_fitted]\n";
|
||||
/// `exec`'s lines in the usage.
|
||||
const USAGE_EXEC: &str = " solidcraft-cli exec <command> [json-params]\n";
|
||||
/// `commands`'s lines in the usage.
|
||||
const USAGE_COMMANDS: &str = " solidcraft-cli commands\n";
|
||||
/// `recipe`'s lines in the usage.
|
||||
const USAGE_RECIPE: &str = " solidcraft-cli recipe <recipe.json>\n";
|
||||
/// `oracle`'s lines in the usage.
|
||||
const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
|
||||
/// `fuzz`'s lines in the usage.
|
||||
const USAGE_FUZZ: &str =
|
||||
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
|
||||
/// `mcp`'s lines in the usage.
|
||||
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
|
||||
/// `crashes`'s lines in the usage.
|
||||
const USAGE_CRASHES: &str = " solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]\n";
|
||||
/// `licences`'s lines in the usage.
|
||||
const USAGE_LICENCES: &str = " solidcraft-cli licences\n";
|
||||
/// `gpu`'s lines in the usage.
|
||||
const USAGE_GPU: &str = " solidcraft-cli gpu [--policy auto|hardware|software|cpu]\n (the GPU adapters a headless render can open, best first, the one the policy picks and the tier it\n lands on: gpu, gpu-software, or the CPU rasterizer (auto with no adapter, or cpu); the policy defaults\n to SOLIDCRAFT_GPU, else auto; exits with an error when hardware or software finds no such adapter)\n";
|
||||
/// `icon`'s lines in the usage.
|
||||
const USAGE_ICON: &str = " solidcraft-cli icon --out icon.png [--size 256] | --out icon.ico\n";
|
||||
/// `fuzz-one`'s lines in the usage: a seed and the flags of one fuzz run.
|
||||
const USAGE_FUZZ_ONE: &str = " solidcraft-cli fuzz-one <seed> [--steps N] [--minimise]\n";
|
||||
/// `bench-edit`'s lines in the usage: the parameter a script changes.
|
||||
const USAGE_BENCH_EDIT: &str = " solidcraft-cli bench-edit <script.json|recipe.json> <param> <expr>\n";
|
||||
/// `save-stress`'s lines in the usage: the file saved over and over.
|
||||
const USAGE_SAVE_STRESS: &str = " solidcraft-cli save-stress <path> <rounds>\n";
|
||||
/// The flags every command takes, at the end of the top-level usage.
|
||||
const USAGE_DEBUG: &str = " --debug, with any command: what SolidCraft logs (why a body has no display plan, why a save left its\n model cache out), on stderr; RUST_LOG=error|warn|info|debug|trace sets one level for every crate\n";
|
||||
|
||||
/// The commands the CLI takes, in the order the top-level usage lists them.
|
||||
const COMMANDS: &[Command] = &[
|
||||
Command { name: "run", usage: USAGE_RUN, listed: true },
|
||||
Command { name: "eval", usage: USAGE_EVAL, listed: true },
|
||||
Command { name: "inspect", usage: USAGE_EVAL, listed: false },
|
||||
Command { name: "script", usage: USAGE_SCRIPT, listed: true },
|
||||
Command { name: "bench-open", usage: USAGE_BENCH_OPEN, listed: true },
|
||||
Command { name: "bench-agent", usage: USAGE_BENCH_AGENT, listed: true },
|
||||
Command { name: "bench-assistant", usage: USAGE_BENCH_ASSISTANT, listed: true },
|
||||
Command { name: "snapshot", usage: USAGE_SNAPSHOT, listed: true },
|
||||
Command { name: "render", usage: USAGE_RENDER, listed: true },
|
||||
Command { name: "exec", usage: USAGE_EXEC, listed: true },
|
||||
Command { name: "commands", usage: USAGE_COMMANDS, listed: true },
|
||||
Command { name: "recipe", usage: USAGE_RECIPE, listed: true },
|
||||
Command { name: "oracle", usage: USAGE_ORACLE, listed: true },
|
||||
Command { name: "fuzz", usage: USAGE_FUZZ, listed: true },
|
||||
Command { name: "fuzz-one", usage: USAGE_FUZZ_ONE, listed: false },
|
||||
Command { name: "mcp", usage: USAGE_MCP, listed: true },
|
||||
Command { name: "crashes", usage: USAGE_CRASHES, listed: true },
|
||||
Command { name: "licences", usage: USAGE_LICENCES, listed: true },
|
||||
Command { name: "licenses", usage: USAGE_LICENCES, listed: false },
|
||||
Command { name: "gpu", usage: USAGE_GPU, listed: true },
|
||||
Command { name: "icon", usage: USAGE_ICON, listed: true },
|
||||
Command { name: "bench-edit", usage: USAGE_BENCH_EDIT, listed: false },
|
||||
Command { name: "save-stress", usage: USAGE_SAVE_STRESS, listed: false },
|
||||
];
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let mut args: Vec<String> = std::env::args().skip(1).collect();
|
||||
logger::init(&mut args);
|
||||
let r = match args.first().map(String::as_str) {
|
||||
// Asked-for help is not an error: the command's usage on stdout, exit 0. It is read
|
||||
// before the command runs, so `--help` never starts a run (a fuzz run, a server).
|
||||
Some(cmd) if wants_help(&args[1..]) && is_command(cmd) => {
|
||||
use std::io::Write as _;
|
||||
let _ = std::io::stdout().lock().write_all(command_usage(cmd).as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
Some("run") => cmd_run(&args[1..]),
|
||||
Some("eval" | "inspect") => cmd_eval(&args[1..]),
|
||||
Some("script") => cmd_script(&args[1..]),
|
||||
@@ -113,7 +159,7 @@ fn main() -> ExitCode {
|
||||
}
|
||||
})
|
||||
}
|
||||
Some("recipe") => args.get(1).ok_or_else(|| USAGE.to_string()).and_then(|p| {
|
||||
Some("recipe") => args.get(1).ok_or_else(usage).and_then(|p| {
|
||||
let v = read_json(p)?;
|
||||
recipe::to_script(&v).map(|s| println!("{}", pretty(&s)))
|
||||
}),
|
||||
@@ -136,6 +182,8 @@ fn main() -> ExitCode {
|
||||
Some("bench-open") => cmd_bench_open(&args[1..]),
|
||||
// A ~1,300-body design built command by command, as an agent builds one over MCP.
|
||||
Some("bench-agent") => bench_agent::run(&args[1..]),
|
||||
// The standard Assistant model test (#4851), run against a desktop app over its control channel.
|
||||
Some("bench-assistant") => bench_assistant::run(&args[1..]),
|
||||
Some("--version" | "-V") => {
|
||||
println!("solidcraft-cli {}", env!("CARGO_PKG_VERSION"));
|
||||
Ok(())
|
||||
@@ -143,10 +191,10 @@ fn main() -> ExitCode {
|
||||
// Asked-for help is not an error: usage on stdout, exit 0.
|
||||
Some("--help" | "-h" | "help") => {
|
||||
use std::io::Write as _;
|
||||
let _ = std::io::stdout().lock().write_all(USAGE.as_bytes());
|
||||
let _ = std::io::stdout().lock().write_all(usage().as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(USAGE.to_string()),
|
||||
_ => Err(usage()),
|
||||
};
|
||||
match r {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
@@ -161,6 +209,35 @@ fn pretty(v: &Value) -> String {
|
||||
serde_json::to_string_pretty(v).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The whole usage: every command's lines under one `usage:` line. The lines come from the
|
||||
/// same place a command's own `--help` prints, so the two cannot drift apart.
|
||||
fn usage() -> String {
|
||||
let mut s = String::from("usage:\n");
|
||||
for c in COMMANDS.iter().filter(|c| c.listed) {
|
||||
s.push_str(c.usage);
|
||||
}
|
||||
s.push_str(USAGE_DEBUG);
|
||||
s
|
||||
}
|
||||
|
||||
/// A command's own usage: its lines under a `usage:` line.
|
||||
fn command_usage(cmd: &str) -> String {
|
||||
match COMMANDS.iter().find(|c| c.name == cmd) {
|
||||
Some(c) => format!("usage:\n{}", c.usage),
|
||||
None => usage(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the name is a command the CLI takes.
|
||||
fn is_command(cmd: &str) -> bool {
|
||||
COMMANDS.iter().any(|c| c.name == cmd)
|
||||
}
|
||||
|
||||
/// Whether the arguments ask for help.
|
||||
fn wants_help(args: &[String]) -> bool {
|
||||
args.iter().any(|a| a == "--help" || a == "-h")
|
||||
}
|
||||
|
||||
pub fn read_json(path: &str) -> Result<Value, String> {
|
||||
// Through the engine's file access, which refuses anything that is not a regular file and
|
||||
// reads no more than the cap: a character device or a FIFO would otherwise never end.
|
||||
@@ -185,8 +262,7 @@ fn load_checked(path: &str, check: solidcraft_engine::schema::ParamCheck) -> Res
|
||||
let bytes = solidcraft_engine::io::vfs::read(path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| format!("{path}: {e}"))?;
|
||||
if solidcraft_engine::io::is_design_container(&bytes) {
|
||||
let doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
let mut session = Session::new(doc);
|
||||
session.path = Some(path.to_owned());
|
||||
let mut session = Session::opened_at(doc, None, Some(path));
|
||||
session.param_check = check;
|
||||
return Ok(session);
|
||||
}
|
||||
@@ -263,10 +339,11 @@ fn print_json(v: &Value, file: Option<&str>) -> Result<(), String> {
|
||||
fn cmd_run(args: &[String]) -> Result<(), String> {
|
||||
install_renderer();
|
||||
let json_out = json_out(args)?;
|
||||
known_args("run", args, &["--in", "--out", "--save", "--json-out"], &["--quiet", "--results", "--timings", "--profile", "--allow-errors"], true)?;
|
||||
let base = flag(args, "--in");
|
||||
let path = positional(args);
|
||||
if path.is_none() && base.is_none() {
|
||||
return Err(USAGE.to_string());
|
||||
return Err(usage());
|
||||
}
|
||||
let want_results = args.iter().any(|a| a == "--results");
|
||||
// `--profile` times each command and splits out its kernel time; it implies `--timings`.
|
||||
@@ -326,13 +403,14 @@ fn open(base: Option<&str>, path: Option<&String>) -> Result<Session, String> {
|
||||
}
|
||||
(Some(b), None) => load(b),
|
||||
(None, Some(p)) => load(p),
|
||||
(None, None) => Err(USAGE.to_string()),
|
||||
(None, None) => Err(usage()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Refuses an option `cmd` doesn't take, and one missing its value: a typo, or another
|
||||
/// command's option, would otherwise be ignored without a word.
|
||||
fn known_flags(cmd: &str, args: &[String], valued: &[&str], switches: &[&str]) -> Result<(), String> {
|
||||
/// command's option, would otherwise be ignored without a word. `positionals` says whether the
|
||||
/// command takes a word of its own (a file, a seed): `fuzz` takes none, and would start a run.
|
||||
fn known_args(cmd: &str, args: &[String], valued: &[&str], switches: &[&str], positionals: bool) -> Result<(), String> {
|
||||
let mut i = 0;
|
||||
while let Some(a) = args.get(i) {
|
||||
if valued.contains(&a.as_str()) {
|
||||
@@ -340,7 +418,9 @@ fn known_flags(cmd: &str, args: &[String], valued: &[&str], switches: &[&str]) -
|
||||
Some(v) if !v.starts_with("--") => i += 2,
|
||||
_ => return Err(format!("{cmd}: {a} needs a value")),
|
||||
}
|
||||
} else if a.starts_with("--") && !switches.contains(&a.as_str()) {
|
||||
} else if switches.contains(&a.as_str()) {
|
||||
i += 1;
|
||||
} else if a.starts_with("--") || !positionals {
|
||||
return Err(format!("{cmd} doesn't take {a} (it takes {})", [valued, switches].concat().join(", ")));
|
||||
} else {
|
||||
i += 1;
|
||||
@@ -364,7 +444,8 @@ fn timeline_ok(s: &Session) -> Result<(), String> {
|
||||
|
||||
fn cmd_eval(args: &[String]) -> Result<(), String> {
|
||||
let json_out = json_out(args)?;
|
||||
let path = flag(args, "--in").or_else(|| positional(args).map(String::as_str)).ok_or_else(|| USAGE.to_string())?;
|
||||
known_args("eval", args, &["--in", "--json-out"], &[], true)?;
|
||||
let path = flag(args, "--in").or_else(|| positional(args).map(String::as_str)).ok_or_else(usage)?;
|
||||
let mut s = load(path)?;
|
||||
let m = s.execute("inspect.measure", &json!({})).map_err(|e| e.to_string())?;
|
||||
let d = s.execute("document.inspect", &json!({})).map_err(|e| e.to_string())?;
|
||||
@@ -382,7 +463,7 @@ fn cmd_eval(args: &[String]) -> Result<(), String> {
|
||||
/// A design's history as a command script (`timeline.export_script`): printed, or written to
|
||||
/// `--out`; what the script leaves out goes to stderr.
|
||||
fn cmd_script(args: &[String]) -> Result<(), String> {
|
||||
let path = positional(args).ok_or_else(|| USAGE.to_string())?;
|
||||
let path = positional(args).ok_or_else(usage)?;
|
||||
let s = &mut load(path)?;
|
||||
let out = flag(args, "--out");
|
||||
let r = s.execute("timeline.export_script", &json!({"path": out})).map_err(|e| e.to_string())?;
|
||||
@@ -443,14 +524,15 @@ fn install_renderer() {
|
||||
|
||||
fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
||||
install_renderer();
|
||||
known_flags(
|
||||
known_args(
|
||||
"snapshot",
|
||||
args,
|
||||
&["--in", "--out", "--width", "--height", "--view", "--camera", "--body", "--tool", "--section"],
|
||||
&["--no-grid", "--no-axes"],
|
||||
true,
|
||||
)?;
|
||||
if positional(args).is_none() && flag(args, "--in").is_none() {
|
||||
return Err(USAGE.to_string());
|
||||
return Err(usage());
|
||||
}
|
||||
let out = flag(args, "--out").ok_or("snapshot needs --out FILE.png")?;
|
||||
let size = |name: &str, default: u32| match flag(args, name) {
|
||||
@@ -534,7 +616,7 @@ fn ico(images: &[(usize, Vec<u8>)]) -> Result<Vec<u8>, String> {
|
||||
|
||||
fn cmd_exec(args: &[String]) -> Result<(), String> {
|
||||
install_renderer();
|
||||
let id = args.first().ok_or_else(|| USAGE.to_string())?;
|
||||
let id = args.first().ok_or_else(usage)?;
|
||||
let p: Value = match args.get(1) {
|
||||
Some(t) => serde_json::from_str(t).map_err(|e| format!("params: {e}"))?,
|
||||
None => json!({}),
|
||||
@@ -545,10 +627,27 @@ fn cmd_exec(args: &[String]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `mcp setup`: add the server to each installed AI client, with this program's own path.
|
||||
fn cmd_mcp_setup(args: &[String]) -> Result<(), String> {
|
||||
use solidcraft_engine::agent_setup as setup;
|
||||
let (lines, ok) = setup::setup(args, &setup::Env::from_process(), &setup::current_exe()?)?;
|
||||
for l in lines {
|
||||
println!("{l}");
|
||||
}
|
||||
if ok { Ok(()) } else { Err("mcp setup: some clients could not be configured".into()) }
|
||||
}
|
||||
|
||||
/// MCP server on stdio: headless (optionally starting from a design or script) or bridged to
|
||||
/// a running app's control port. stdout carries only protocol messages. Files the agent's tool
|
||||
/// calls reach are confined to `solidcraft_mcp::agent_scope` unless `--allow-any-path` is given.
|
||||
fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
if matches!(args.first().map(String::as_str), Some("setup" | "install")) {
|
||||
return cmd_mcp_setup(&args[1..]);
|
||||
}
|
||||
let render_gpu = args.iter().any(|arg| arg == "--render-gpu");
|
||||
if render_gpu && flag(args, "--connect").is_some() {
|
||||
return Err("mcp: --render-gpu installs a headless image host; a connected application owns its own device".into());
|
||||
}
|
||||
install_renderer();
|
||||
let scope = (!args.iter().any(|a| a == "--allow-any-path"))
|
||||
.then(|| solidcraft_mcp::agent_scope(flags(args, "--allow-path").into_iter().map(std::path::PathBuf::from)));
|
||||
@@ -592,18 +691,49 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
Box::new(remote)
|
||||
}
|
||||
(None, Some(path)) => Box::new(solidcraft_mcp::Headless::new(load_checked(path, param_check)?)),
|
||||
(None, None) => {
|
||||
let mut headless = solidcraft_mcp::Headless::default();
|
||||
headless.session.param_check = param_check;
|
||||
(None, Some(path)) => {
|
||||
let mut headless = solidcraft_mcp::Headless::new(load_checked(path, param_check)?);
|
||||
if render_gpu {
|
||||
headless.session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Box::new(headless)
|
||||
}
|
||||
(None, None) => {
|
||||
let auto = !args.iter().any(|a| a == "--headless");
|
||||
if auto && let Some(port) = discover_app() {
|
||||
eprintln!("solidcraft mcp: bridged to the running app at 127.0.0.1:{port} (--headless starts a session of its own)");
|
||||
return serve_mcp(Box::new(solidcraft_mcp::Remote::new(&format!("127.0.0.1:{port}"), flag(args, "--token"))), scope);
|
||||
}
|
||||
let mut headless = solidcraft_mcp::Headless::default();
|
||||
headless.session.param_check = param_check;
|
||||
if render_gpu {
|
||||
headless.session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
}
|
||||
if auto {
|
||||
// The app may start after the agent: look for it again until there is work here.
|
||||
Box::new(solidcraft_mcp::Auto::new(headless, Box::new(discover_app), flag(args, "--token")))
|
||||
} else {
|
||||
Box::new(headless)
|
||||
}
|
||||
}
|
||||
};
|
||||
serve_mcp(backend, scope)
|
||||
}
|
||||
|
||||
fn serve_mcp(backend: Box<dyn solidcraft_mcp::Backend>, scope: Option<solidcraft_engine::io::vfs::PathScope>) -> Result<(), String> {
|
||||
let stdin = std::io::stdin();
|
||||
let stdout = std::io::stdout();
|
||||
solidcraft_mcp::Server::new(backend).with_path_scope(scope).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// The port of the running desktop app: the one its discovery file names, if something listens
|
||||
/// there (a file left by an app that crashed names a closed port).
|
||||
fn discover_app() -> Option<u16> {
|
||||
let port = solidcraft_engine::control_auth::read_current_port()?;
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
|
||||
std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(500)).ok().map(|_| port)
|
||||
}
|
||||
|
||||
/// `save-stress <path> <rounds>`: save a design of a few hundred kilobytes `rounds` times, its
|
||||
/// parameter `save_round` set to the round number each time.
|
||||
fn cmd_save_stress(args: &[String]) -> Result<(), String> {
|
||||
@@ -714,11 +844,12 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
return Ok(());
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
let s = Session::opened(doc, saved.as_ref());
|
||||
let s = Session::opened_at(doc, saved.as_ref(), (!step).then_some(path.as_str()));
|
||||
drop(read);
|
||||
// Taken from the file: nothing evaluated (SOLIDCRAFT_NO_MODEL_CACHE evaluates anyway).
|
||||
// Taken from the file: nothing evaluated (SOLIDCRAFT_NO_MODEL_CACHE evaluates anyway), or
|
||||
// only the features whose state a thinned file left out.
|
||||
let adopted = saved.is_some() && s.model.last_recomputed == 0;
|
||||
println!("{}", json!({"step": "evaluate", "ms": t.elapsed().as_secs_f64() * 1000.0, "adopted": adopted}));
|
||||
println!("{}", json!({"step": "evaluate", "ms": t.elapsed().as_secs_f64() * 1000.0, "adopted": adopted, "recomputed": s.model.last_recomputed}));
|
||||
for r in &s.model.results {
|
||||
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
|
||||
}
|
||||
|
||||
@@ -1,25 +1,89 @@
|
||||
//! Complete headless image-job workflow through the same engine commands as the UI/MCP.
|
||||
use serde_json::json;
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::{
|
||||
doc::render_settings::RenderSetup,
|
||||
render::job::{CpuHost, JobId, Phase},
|
||||
Session,
|
||||
doc::render_settings::{RenderJob, RenderSetup},
|
||||
render::job::{CpuHost, JobId, Phase, ToneMap},
|
||||
};
|
||||
|
||||
/// Only explicit runtime CLI flags are stored here. A typed setup owns these
|
||||
/// values even when a supplied flag happens to match the saved value.
|
||||
#[derive(Default)]
|
||||
struct RuntimeOptions {
|
||||
progressive: bool,
|
||||
transparent: bool,
|
||||
samples: Option<u32>,
|
||||
max_depth: Option<u32>,
|
||||
seed: Option<u32>,
|
||||
lighting: Option<String>,
|
||||
tone_map: Option<ToneMap>,
|
||||
environment: Option<String>,
|
||||
}
|
||||
|
||||
impl RuntimeOptions {
|
||||
fn explicit_flag(&self) -> Option<&'static str> {
|
||||
[
|
||||
(self.progressive, "--progressive"),
|
||||
(self.transparent, "--transparent"),
|
||||
(self.samples.is_some(), "--samples"),
|
||||
(self.max_depth.is_some(), "--max-depth"),
|
||||
(self.seed.is_some(), "--seed"),
|
||||
(self.lighting.is_some(), "--lighting"),
|
||||
(self.tone_map.is_some(), "--tone-map"),
|
||||
(self.environment.is_some(), "--environment"),
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|(present, flag)| present.then_some(flag))
|
||||
}
|
||||
|
||||
fn request(&self, session: &Session, settings: RenderSetup) -> Result<(Value, bool), String> {
|
||||
if let Some(job) = &settings.job {
|
||||
if let Some(flag) = self.explicit_flag() {
|
||||
return Err(format!("{flag} cannot accompany a typed saved render setup; change the saved job options instead"));
|
||||
}
|
||||
settings.validate().map_err(|e| e.to_string())?;
|
||||
session.doc.render.validate_resources(&settings).map_err(|e| e.to_string())?;
|
||||
let needs_gpu = matches!(job, RenderJob::GpuPath { .. });
|
||||
return Ok((json!({"settings":settings}), needs_gpu));
|
||||
}
|
||||
// Legacy setups and unnamed requests retain their runtime wire contract.
|
||||
settings.validate().map_err(|e| e.to_string())?;
|
||||
let mut params = json!({"settings":settings,"transparent_background":self.transparent,
|
||||
"progressive":self.progressive,"samples":self.samples,"max_depth":self.max_depth,"seed":self.seed});
|
||||
if let Some(path) = &self.lighting {
|
||||
let text = solidcraft_engine::io::vfs::read_to_string(path, 65536).map_err(|e| format!("could not read lighting: {e}"))?;
|
||||
let lighting: solidcraft_engine::render::job::StudioLighting = serde_json::from_str(&text).map_err(|e| e.to_string())?;
|
||||
params["lighting"] = json!(lighting);
|
||||
}
|
||||
if let Some(tone_map) = self.tone_map {
|
||||
params["tone_map"] = json!(tone_map);
|
||||
}
|
||||
if let Some(path) = &self.environment {
|
||||
let text = solidcraft_engine::io::vfs::read_to_string(path, 65536).map_err(|e| format!("could not read environment: {e}"))?;
|
||||
params["environment"] = serde_json::from_str(&text).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok((params, self.progressive))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
let input = args.first().filter(|p| !p.starts_with('-')).ok_or("render requires an input design or script")?;
|
||||
let mut out = None;
|
||||
let mut setup = None;
|
||||
let mut width = None;
|
||||
let mut height = None;
|
||||
let mut transparent = false;
|
||||
let mut exposure = None;
|
||||
let mut runtime = RuntimeOptions::default();
|
||||
let mut overwrite = false;
|
||||
let mut timeout = 120u64;
|
||||
let mut i = 1;
|
||||
while let Some(arg) = args.get(i) {
|
||||
match arg.as_str() {
|
||||
"--transparent" => transparent = true,
|
||||
"--progressive" => runtime.progressive = true,
|
||||
"--transparent" => runtime.transparent = true,
|
||||
"--overwrite" => overwrite = true,
|
||||
"--out" | "--setup" | "--width" | "--height" | "--timeout" => {
|
||||
"--out" | "--setup" | "--width" | "--height" | "--timeout" | "--samples" | "--max-depth" | "--seed" | "--exposure" | "--lighting"
|
||||
| "--tone-map" | "--environment" => {
|
||||
i += 1;
|
||||
let value = args.get(i).ok_or_else(|| format!("missing value for {arg}"))?;
|
||||
match arg.as_str() {
|
||||
@@ -27,6 +91,20 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
"--setup" => setup = Some(value.clone()),
|
||||
"--width" => width = Some(value.parse::<u32>().map_err(|_| "invalid width")?),
|
||||
"--height" => height = Some(value.parse::<u32>().map_err(|_| "invalid height")?),
|
||||
"--samples" => runtime.samples = Some(value.parse::<u32>().map_err(|_| "invalid samples")?),
|
||||
"--max-depth" => runtime.max_depth = Some(value.parse::<u32>().map_err(|_| "invalid depth")?),
|
||||
"--seed" => runtime.seed = Some(value.parse::<u32>().map_err(|_| "invalid seed")?),
|
||||
"--exposure" => exposure = Some(value.parse::<f64>().map_err(|_| "invalid exposure")?),
|
||||
// Defer file reads until a saved job has rejected conflicting controls.
|
||||
"--environment" => runtime.environment = Some(value.clone()),
|
||||
"--lighting" => runtime.lighting = Some(value.clone()),
|
||||
"--tone-map" => {
|
||||
runtime.tone_map = Some(match value.as_str() {
|
||||
"reinhard" => ToneMap::Reinhard,
|
||||
"aces_fitted" => ToneMap::AcesFitted,
|
||||
_ => return Err("tone-map must be reinhard or aces_fitted".into()),
|
||||
})
|
||||
}
|
||||
_ => timeout = value.parse::<u64>().map_err(|_| "invalid timeout")?,
|
||||
}
|
||||
}
|
||||
@@ -39,18 +117,10 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
let out = out.ok_or("render requires --out FILE.png")?;
|
||||
let mut session = crate::load(input)?;
|
||||
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
|
||||
session.execute("render.scene", &json!({})).map_err(|e| e.to_string())?;
|
||||
let mut settings = if let Some(name) = setup {
|
||||
session.doc.render.setups.get(&name).cloned().ok_or("no saved render setup with that name")?
|
||||
let mut settings = if let Some(name) = setup.as_deref() {
|
||||
session.doc.render.setups.get(name).cloned().ok_or("no saved render setup with that name")?
|
||||
} else {
|
||||
let mut settings = RenderSetup::default();
|
||||
if let Some(camera) = session.camera {
|
||||
settings.camera = camera;
|
||||
} else {
|
||||
settings.camera.fit(&solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?.bounds);
|
||||
}
|
||||
settings
|
||||
RenderSetup::default()
|
||||
};
|
||||
if let Some(width) = width {
|
||||
settings.width = width;
|
||||
@@ -58,7 +128,27 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
if let Some(height) = height {
|
||||
settings.height = height;
|
||||
}
|
||||
let params = json!({"settings":settings,"transparent_background":transparent});
|
||||
if let Some(exposure) = exposure {
|
||||
settings.exposure_ev = exposure;
|
||||
}
|
||||
// Resolve saved intent before opening a GPU or explicitly settling geometry.
|
||||
// Width/height/exposure only change this validated inline settings clone.
|
||||
let (mut params, needs_gpu) = runtime.request(&session, settings)?;
|
||||
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
|
||||
if needs_gpu {
|
||||
session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
}
|
||||
session.execute("render.scene", &json!({})).map_err(|e| e.to_string())?;
|
||||
if setup.is_none() {
|
||||
let camera = if let Some(camera) = session.camera {
|
||||
camera
|
||||
} else {
|
||||
let mut camera = RenderSetup::default().camera;
|
||||
camera.fit(&solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?.bounds);
|
||||
camera
|
||||
};
|
||||
params["settings"]["camera"] = json!(camera);
|
||||
}
|
||||
let preflight = session.execute("render.preflight", ¶ms).map_err(|e| e.to_string())?;
|
||||
let started = session.execute("render.start", ¶ms).map_err(|e| e.to_string())?;
|
||||
let id: JobId = serde_json::from_value(started["job"].clone()).map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -395,8 +395,10 @@ fn actions(s: &Session) -> Vec<Action> {
|
||||
let r = round3((shortest * 0.1).max(0.01));
|
||||
let mut add = |action, act| out.push(Action { entity: entity.clone(), action, act });
|
||||
if !smooth(e, mid) {
|
||||
add("edge.fillet", Act::Run("solid.fillet", json!({"edges": [pt(mid)], "radius": r})));
|
||||
add("edge.chamfer", Act::Run("solid.chamfer", json!({"edges": [pt(mid)], "distance": r})));
|
||||
// The midpoint is interior to this edge, but other bodies (including component
|
||||
// copies in their local frames) can have coincident edges. Retain the picked body.
|
||||
add("edge.fillet", Act::Run("solid.fillet", json!({"body": world, "edges": [pt(mid)], "radius": r})));
|
||||
add("edge.chamfer", Act::Run("solid.chamfer", json!({"body": world, "edges": [pt(mid)], "distance": r})));
|
||||
}
|
||||
let dir = straight(pts);
|
||||
if dir.is_some() {
|
||||
@@ -1278,6 +1280,41 @@ fn rectangle(plane: Value, p0: [f64; 2], p1: [f64; 2]) -> [Value; 3] {
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_round_actions_keep_the_picked_body() {
|
||||
for (corner, length) in [([10, 0, 20], 4), ([0, 0, 0], 20)] {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 20, "width": 20, "height": 20, "body_name": "Plate"})).unwrap();
|
||||
s.execute(
|
||||
"solid.box",
|
||||
&json!({"corner": corner, "length": length, "width": length, "height": length, "body_name": "Block", "operation": "new"}),
|
||||
)
|
||||
.unwrap();
|
||||
let mut tried = 0;
|
||||
for a in actions(&s) {
|
||||
if !a.entity.starts_with("Plate/") || !matches!(a.action, "edge.chamfer" | "edge.fillet") {
|
||||
continue;
|
||||
}
|
||||
let Act::Run(id, ref p) = a.act else { continue };
|
||||
let q = &p["edges"][0];
|
||||
let x = q[0].as_f64().unwrap();
|
||||
if q[1] != 0.0 || q[2] != 20.0 || !(0.0..20.0).contains(&x) {
|
||||
continue;
|
||||
}
|
||||
// Block either meets this midpoint or coincides with the whole edge.
|
||||
assert_eq!(p["body"], "Plate", "{}: {p}", a.action);
|
||||
assert_eq!(q, &json!([10.0, 0.0, 20.0]));
|
||||
let mut unscoped = p.clone();
|
||||
unscoped.as_object_mut().unwrap().remove("body");
|
||||
let err = s.scratch().execute(id, &unscoped).unwrap_err().to_string();
|
||||
assert!(err.contains("edges meet"), "{err}");
|
||||
perform(&mut s.scratch(), &a.act, &errors(&s)).unwrap();
|
||||
tried += 1;
|
||||
}
|
||||
assert_eq!(tried, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fillet_or_chamfer_on_one_edge_ending_at_a_cylinder() {
|
||||
for (cmd, size) in [("solid.fillet", "radius"), ("solid.chamfer", "distance")] {
|
||||
@@ -1412,7 +1449,7 @@ fn a_sketch_on_a_patterned_boss_top() {
|
||||
fn construction_geometry_and_holes_on_a_moved_component() {
|
||||
let moved = || {
|
||||
vec![
|
||||
json!({"command": "component.create", "params": {"name": "Arm"}}),
|
||||
json!({"command": "component.create", "params": {"name": "Arm", "grounded": false}}),
|
||||
json!({"command": "solid.box", "params": {"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"}}),
|
||||
json!({"command": "component.activate", "params": {"component": 0}}),
|
||||
json!({"command": "occurrence.move", "params": {"occurrence": "Arm:1", "translate": [0, 50, 0]}}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{"commands": [
|
||||
{"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10, "body_name": "Base"}},
|
||||
{"command": "component.create", "params": {"name": "Arm"}},
|
||||
{"command": "component.create", "params": {"name": "Arm", "grounded": false}},
|
||||
{"command": "solid.box", "params": {"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"}},
|
||||
{"command": "solid.fillet", "params": {"edges": [[25, 4, 4]], "radius": 1}},
|
||||
{"command": "component.activate", "params": {"component": 0}},
|
||||
|
||||
@@ -1,6 +1,84 @@
|
||||
//! An explicit help request prints usage to stdout and succeeds; an unknown command still fails.
|
||||
|
||||
use std::process::Command;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Every command the CLI takes, with the name its own usage names: `inspect` is `eval`, and
|
||||
/// `licenses` is `licences`.
|
||||
const COMMANDS: &[(&str, &str)] = &[
|
||||
("run", "run"),
|
||||
("eval", "eval"),
|
||||
("inspect", "eval"),
|
||||
("script", "script"),
|
||||
("bench-open", "bench-open"),
|
||||
("bench-agent", "bench-agent"),
|
||||
("snapshot", "snapshot"),
|
||||
("render", "render"),
|
||||
("exec", "exec"),
|
||||
("commands", "commands"),
|
||||
("recipe", "recipe"),
|
||||
("oracle", "oracle"),
|
||||
("fuzz", "fuzz"),
|
||||
("fuzz-one", "fuzz-one"),
|
||||
("mcp", "mcp"),
|
||||
("crashes", "crashes"),
|
||||
("licences", "licences"),
|
||||
("licenses", "licences"),
|
||||
("gpu", "gpu"),
|
||||
("icon", "icon"),
|
||||
("bench-edit", "bench-edit"),
|
||||
("save-stress", "save-stress"),
|
||||
];
|
||||
|
||||
/// The commands the usage lists: the aliases answer to another command's name.
|
||||
const LISTED: &[&str] = &[
|
||||
"run",
|
||||
"eval",
|
||||
"script",
|
||||
"bench-open",
|
||||
"bench-agent",
|
||||
"snapshot",
|
||||
"render",
|
||||
"exec",
|
||||
"commands",
|
||||
"recipe",
|
||||
"oracle",
|
||||
"fuzz",
|
||||
"mcp",
|
||||
"crashes",
|
||||
"licences",
|
||||
"gpu",
|
||||
"icon",
|
||||
];
|
||||
|
||||
/// The command names a text mentions after `solidcraft-cli `, each one only once.
|
||||
fn mentioned(text: &str) -> Vec<&str> {
|
||||
let mut names: Vec<&str> = text.split("solidcraft-cli ").skip(1).filter_map(|rest| rest.split_whitespace().next()).collect();
|
||||
names.sort_unstable();
|
||||
names.dedup();
|
||||
names
|
||||
}
|
||||
|
||||
/// Runs the binary and waits at most `limit` for it to finish. `None` means it was still
|
||||
/// running and was killed: the fuzz run `fuzz --help` used to start goes on for minutes.
|
||||
fn run_within(limit: Duration, args: &[&str]) -> Option<(Option<i32>, String, String)> {
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args(args).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap();
|
||||
let started = Instant::now();
|
||||
loop {
|
||||
match child.try_wait().unwrap() {
|
||||
Some(status) => {
|
||||
let out = child.wait_with_output().unwrap();
|
||||
return Some((status.code(), String::from_utf8_lossy(&out.stdout).into_owned(), String::from_utf8_lossy(&out.stderr).into_owned()));
|
||||
}
|
||||
None if started.elapsed() >= limit => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return None;
|
||||
}
|
||||
None => std::thread::sleep(Duration::from_millis(25)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_prints_usage_and_succeeds() {
|
||||
@@ -21,3 +99,61 @@ fn unknown_command_still_fails_with_usage() {
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(String::from_utf8_lossy(&output.stderr).starts_with("error: usage:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_command_prints_its_own_usage() {
|
||||
for (cmd, named) in COMMANDS {
|
||||
for flag in ["--help", "-h"] {
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args([*cmd, flag]).output().unwrap();
|
||||
assert!(output.status.success(), "{cmd} {flag}: {}", String::from_utf8_lossy(&output.stderr));
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.starts_with("usage:"), "{cmd} {flag}: {stdout}");
|
||||
assert_eq!(mentioned(&stdout), vec![*named], "{cmd} {flag} printed another command's usage: {stdout}");
|
||||
assert!(output.stderr.is_empty(), "{cmd} {flag}: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_top_level_usage_still_lists_every_command() {
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).arg("--help").output().unwrap();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for cmd in LISTED {
|
||||
assert!(stdout.contains(&format!("solidcraft-cli {cmd}")), "{cmd}: {stdout}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzz_help_does_not_start_a_fuzz_run() {
|
||||
let Some((code, stdout, stderr)) = run_within(Duration::from_secs(20), &["fuzz", "--help"]) else {
|
||||
panic!("`fuzz --help` kept running: it started a fuzz run instead of printing the usage");
|
||||
};
|
||||
assert_eq!(code, Some(0), "{stderr}");
|
||||
assert!(stdout.starts_with("usage:"), "{stdout}");
|
||||
assert_eq!(mentioned(&stdout), vec!["fuzz"], "{stdout}");
|
||||
assert!(stderr.is_empty(), "{stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzz_refuses_an_argument_it_does_not_take() {
|
||||
for arg in ["--not-a-flag", "stray"] {
|
||||
let Some((code, _stdout, stderr)) = run_within(Duration::from_secs(20), &["fuzz", arg]) else {
|
||||
panic!("`fuzz {arg}` kept running: it started a fuzz run instead of refusing the argument");
|
||||
};
|
||||
assert_ne!(code, Some(0), "`fuzz {arg}` succeeded");
|
||||
assert!(stderr.contains(arg), "`fuzz {arg}`: {stderr}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_help_keeps_the_registered_gpu_request_controls() {
|
||||
for args in [vec!["--help"], vec!["render", "--help"]] {
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args(&args).output().unwrap();
|
||||
assert!(output.status.success(), "{args:?}: {}", String::from_utf8_lossy(&output.stderr));
|
||||
assert!(output.stderr.is_empty());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for flag in ["--progressive", "--samples", "--max-depth", "--seed", "--exposure", "--lighting", "--environment", "--tone-map"] {
|
||||
assert!(stdout.contains(flag), "{args:?} lost {flag}: {stdout}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,3 +89,177 @@ fn mcp_connect_without_a_port_is_refused_at_start() {
|
||||
assert!(stderr.contains("HOST:PORT"), "{addr}: {stderr}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `mcp` with no arguments finds the running app through the discovery file and uses that
|
||||
/// launch's token; with no app behind the file it stays a headless session.
|
||||
#[test]
|
||||
fn mcp_without_arguments_finds_the_running_app() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let control_dir = std::env::temp_dir().join(format!("solidcraft-mcp-discover-{}-{port}", std::process::id()));
|
||||
std::fs::create_dir_all(&control_dir).unwrap();
|
||||
let token = "fedcba9876543210".repeat(4);
|
||||
std::fs::write(control_dir.join(format!("{port}.token")), &token).unwrap();
|
||||
std::fs::write(control_dir.join("current.port"), format!("{port}\n")).unwrap();
|
||||
let app = std::thread::spawn(move || {
|
||||
// The discovery probe connects and leaves without a request; the tool call follows.
|
||||
let (stream, line) = loop {
|
||||
let (stream, _) = listener.accept().unwrap();
|
||||
let mut line = String::new();
|
||||
BufReader::new(stream.try_clone().unwrap()).read_line(&mut line).unwrap();
|
||||
if !line.is_empty() {
|
||||
break (stream, line);
|
||||
}
|
||||
};
|
||||
let req: Value = serde_json::from_str(&line).unwrap();
|
||||
let ok = req["token"] == json!(token);
|
||||
let reply = json!({"id": req["id"], "ok": ok, "result": {"bodies": [], "method": req["method"]}, "error": "unauthorized"});
|
||||
writeln!(&stream, "{reply}").unwrap();
|
||||
});
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
|
||||
.args(["mcp", "--allow-any-path"])
|
||||
.env("SOLIDCRAFT_CONTROL_DIR", &control_dir)
|
||||
.env_remove("SOLIDCRAFT_CONTROL_TOKEN")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
let mut stdout = BufReader::new(child.stdout.take().unwrap());
|
||||
writeln!(stdin, "{}", json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "measure", "arguments": {}}})).unwrap();
|
||||
let mut line = String::new();
|
||||
stdout.read_line(&mut line).unwrap();
|
||||
let r: Value = serde_json::from_str(&line).unwrap_or_else(|e| panic!("{e}: {line:?}"));
|
||||
assert_ne!(r["result"]["isError"], true, "{r}");
|
||||
assert!(r["result"]["content"][0]["text"].as_str().unwrap_or_default().contains("engine.execute"), "{r}");
|
||||
app.join().unwrap();
|
||||
drop(stdin);
|
||||
assert!(child.wait().unwrap().success());
|
||||
|
||||
// The file names a port nothing listens on (the app crashed): a headless session instead.
|
||||
let closed = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let dead = closed.local_addr().unwrap().port();
|
||||
drop(closed);
|
||||
std::fs::write(control_dir.join("current.port"), format!("{dead}\n")).unwrap();
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
|
||||
.args(["mcp", "--allow-any-path"])
|
||||
.env("SOLIDCRAFT_CONTROL_DIR", &control_dir)
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!String::from_utf8_lossy(&out.stderr).contains("bridged to the running app"), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
std::fs::remove_dir_all(&control_dir).ok();
|
||||
}
|
||||
|
||||
/// `mcp setup` against a temporary home: the client folders it finds are the only ones it writes.
|
||||
mod setup {
|
||||
use serde_json::{Value, json};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn home(name: &str) -> PathBuf {
|
||||
let h = std::env::temp_dir().join(format!("solidcraft-mcp-setup-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&h);
|
||||
std::fs::create_dir_all(&h).unwrap();
|
||||
h
|
||||
}
|
||||
|
||||
fn run(home: &Path, args: &[&str]) -> std::process::Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
|
||||
.args(["mcp", "setup"])
|
||||
.args(args)
|
||||
.env_clear()
|
||||
.env("HOME", home)
|
||||
.env("USERPROFILE", home)
|
||||
.env("APPDATA", home.join("AppData/Roaming"))
|
||||
.env("LOCALAPPDATA", home.join("AppData/Local"))
|
||||
.env("XDG_CONFIG_HOME", home.join(".config"))
|
||||
.env("PATH", "")
|
||||
.output()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn json_of(p: &Path) -> Value {
|
||||
serde_json::from_str(&std::fs::read_to_string(p).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn exe() -> String {
|
||||
let p = Path::new(env!("CARGO_BIN_EXE_solidcraft-cli")).canonicalize().unwrap();
|
||||
p.to_string_lossy().trim_start_matches(r"\\?\").to_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merges_into_opencode_and_cursor_keeping_other_servers() {
|
||||
let h = home("merge");
|
||||
let oc = h.join(".config/opencode");
|
||||
std::fs::create_dir_all(&oc).unwrap();
|
||||
let old = json!({"$schema": "https://opencode.ai/config.json", "theme": "x", "mcp": {"other": {"type": "local", "command": ["o"]}}});
|
||||
std::fs::write(oc.join("opencode.json"), old.to_string()).unwrap();
|
||||
std::fs::create_dir_all(h.join(".cursor")).unwrap();
|
||||
std::fs::write(h.join(".cursor/mcp.json"), r#"{"mcpServers": {"other": {"command": "o"}}}"#).unwrap();
|
||||
|
||||
let out = run(&h, &["--live"]);
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
let v = json_of(&oc.join("opencode.json"));
|
||||
assert_eq!(v["theme"], "x");
|
||||
assert_eq!(v["mcp"]["other"], old["mcp"]["other"]);
|
||||
assert_eq!(v["mcp"]["solidcraft"], json!({"type": "local", "command": [exe(), "mcp"], "enabled": true}));
|
||||
assert_eq!(v["mcp"]["solidcraft-live"]["command"], json!([exe(), "mcp", "--connect", "127.0.0.1:7878"]));
|
||||
assert_eq!(json_of(&oc.join("opencode.json.bak")), old);
|
||||
let c = json_of(&h.join(".cursor/mcp.json"));
|
||||
assert_eq!(c["mcpServers"]["other"], json!({"command": "o"}));
|
||||
assert_eq!(c["mcpServers"]["solidcraft"], json!({"command": exe(), "args": ["mcp"]}));
|
||||
|
||||
// Codex (TOML) and Claude Code (a big file the CLI owns) are edited in place too.
|
||||
std::fs::create_dir_all(h.join(".codex")).unwrap();
|
||||
std::fs::write(h.join(".codex/config.toml"), "# mine\nmodel = \"x\"\n").unwrap();
|
||||
let cc = "{\n \"numStartups\": 3,\n \"projects\": {}\n}\n";
|
||||
std::fs::write(h.join(".claude.json"), cc).unwrap();
|
||||
assert!(run(&h, &[]).status.success());
|
||||
let toml = std::fs::read_to_string(h.join(".codex/config.toml")).unwrap();
|
||||
assert!(toml.starts_with("# mine\nmodel = \"x\"\n") && toml.contains("[mcp_servers.solidcraft]"), "{toml}");
|
||||
let c = json_of(&h.join(".claude.json"));
|
||||
assert_eq!(c["numStartups"], 3);
|
||||
assert_eq!(c["mcpServers"]["solidcraft"], json!({"type": "stdio", "command": exe(), "args": ["mcp"]}));
|
||||
assert_eq!(std::fs::read_to_string(h.join(".claude.json.bak")).unwrap(), cc);
|
||||
|
||||
// Running again updates the same entries and leaves one of each.
|
||||
assert!(run(&h, &[]).status.success());
|
||||
assert_eq!(json_of(&h.join(".cursor/mcp.json"))["mcpServers"].as_object().unwrap().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dry_run_writes_nothing_and_missing_clients_are_skipped() {
|
||||
let h = home("dry");
|
||||
std::fs::create_dir_all(h.join(".cursor")).unwrap();
|
||||
let out = run(&h, &["--dry-run"]);
|
||||
assert!(out.status.success());
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(text.contains("would write") && text.contains("solidcraft"), "{text}");
|
||||
assert!(!h.join(".cursor/mcp.json").exists());
|
||||
assert!(!h.join(".config/opencode").exists(), "OpenCode was not installed, so nothing is created for it");
|
||||
let out = run(&h, &["--client", "claude-desktop"]);
|
||||
assert!(String::from_utf8_lossy(&out.stdout).contains("not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_that_is_not_json_is_left_alone() {
|
||||
let h = home("bad");
|
||||
std::fs::create_dir_all(h.join(".cursor")).unwrap();
|
||||
std::fs::write(h.join(".cursor/mcp.json"), "{ // comment").unwrap();
|
||||
let out = run(&h, &[]);
|
||||
assert!(!out.status.success());
|
||||
assert_eq!(std::fs::read_to_string(h.join(".cursor/mcp.json")).unwrap(), "{ // comment");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_desktop_msix_folder_is_found() {
|
||||
let h = home("msix");
|
||||
let pkg = h.join("AppData/Local/Packages/Claude_abc/LocalCache/Roaming/Claude");
|
||||
std::fs::create_dir_all(&pkg).unwrap();
|
||||
// The MSIX folder is only read through LOCALAPPDATA, set for every platform here.
|
||||
assert!(run(&h, &["--client", "claude-desktop"]).status.success());
|
||||
assert!(json_of(&pkg.join("claude_desktop_config.json"))["mcpServers"]["solidcraft"].is_object());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Explicit native GPU registration must not change bridged MCP ownership.
|
||||
use std::process::Command;
|
||||
#[test]
|
||||
fn connected_application_rejects_headless_gpu_installation_before_connecting() {
|
||||
let result = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args(["mcp", "--render-gpu", "--connect", "127.0.0.1:1"]).output().unwrap();
|
||||
assert!(!result.status.success());
|
||||
let message = String::from_utf8_lossy(&result.stderr);
|
||||
assert!(message.contains("connected application owns its own device"), "{message}");
|
||||
}
|
||||
@@ -7,7 +7,7 @@ use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::doc::{ModelBody, ModelState};
|
||||
use solidcraft_engine::doc::{ModelBody, ModelState, SavedModel};
|
||||
|
||||
fn corpus() -> Vec<PathBuf> {
|
||||
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus");
|
||||
@@ -149,6 +149,87 @@ fn imported_assemblies_open_from_their_model_as_evaluated() {
|
||||
check("imported hinge", s);
|
||||
}
|
||||
|
||||
/// `built` saved with `saved`, a model of it that keeps only some states (as a file without room
|
||||
/// for all of them does, #3220), opens as the design evaluated again: evaluating just the
|
||||
/// features whose state the file left out, then after suppressing a feature at, before or after
|
||||
/// each state it kept, after undoing that, and with the marker moved there and back.
|
||||
fn check_thinned(name: &str, built: &Session, saved: &SavedModel) {
|
||||
let design = built.design();
|
||||
let bytes =
|
||||
solidcraft_engine::io::write_design_container_with_cache(&design, solidcraft_engine::io::DesignCompression::Fast, None, Some(saved)).unwrap();
|
||||
assert!(solidcraft_engine::io::read_design_geometry(&bytes).is_some(), "{name}: the file keeps the final state");
|
||||
let path = temp(&name.replace(' ', "-"));
|
||||
std::fs::write(&path, &bytes).unwrap();
|
||||
let mut cached = open(&path);
|
||||
let mut rebuilt = Session::new(solidcraft_engine::io::read_design(&bytes).unwrap());
|
||||
let features = saved.records["features"].as_array().unwrap();
|
||||
let unkept = features.iter().filter(|f| f["skipped"] == false && f.get("state").is_none()).count();
|
||||
assert!(unkept > 0, "{name}: some states left out");
|
||||
assert_eq!(cached.model.recomputed, unkept, "{name}: evaluated just the features whose state the file left out");
|
||||
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: opened from the file's model");
|
||||
let n = cached.doc.features.len();
|
||||
let mut at: Vec<usize> = features
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, f)| f["skipped"] == false && f.get("state").is_some())
|
||||
.flat_map(|(k, _)| [k.saturating_sub(1), k, k + 1])
|
||||
.filter(|i| *i < n)
|
||||
.collect();
|
||||
at.dedup();
|
||||
for i in at {
|
||||
let edit = json!({"feature": cached.doc.features[i].id, "suppressed": true});
|
||||
let (a, b) = (cached.execute("timeline.suppress", &edit), rebuilt.execute("timeline.suppress", &edit));
|
||||
assert_eq!(a.is_ok(), b.is_ok(), "{name}: suppressing feature {i}");
|
||||
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: feature {i} suppressed");
|
||||
if a.is_ok() {
|
||||
cached.execute("edit.undo", &json!({})).unwrap();
|
||||
rebuilt.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: suppressing feature {i} undone");
|
||||
}
|
||||
for marker in [json!({"position": i}), json!({})] {
|
||||
cached.execute("timeline.rollback", &marker).unwrap();
|
||||
rebuilt.execute("timeline.rollback", &marker).unwrap();
|
||||
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: the marker at {marker}, from feature {i}");
|
||||
}
|
||||
}
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
/// A model too big for its file keeps the final state, then the slowest features' states as fit
|
||||
/// (#3220). Both the smallest such model (the final state, and states that add no body to it)
|
||||
/// and one that keeps more open as the design evaluated again. The placed components' design has
|
||||
/// a cut whose fingerprint reads where the bodies before it lie, which a state left out does not
|
||||
/// say until it is evaluated.
|
||||
#[test]
|
||||
fn thinned_models_open_as_evaluated() {
|
||||
let corpus = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/components-arm.solidcraft");
|
||||
for sample in ["Sample Plate", "Plastic Enclosure", "components-arm"] {
|
||||
let built = match solidcraft_engine::sample::samples().into_iter().find(|s| s.name == sample) {
|
||||
Some(s) => {
|
||||
let mut built = Session::default();
|
||||
for c in (s.script)()["commands"].as_array().unwrap() {
|
||||
built.execute(c["command"].as_str().unwrap(), &c["params"]).unwrap();
|
||||
}
|
||||
built
|
||||
}
|
||||
None => open(&corpus),
|
||||
};
|
||||
let saved = built.model.saved(&built.design()).unwrap();
|
||||
// Every body a megabyte (the records fit in the room left over): each budget keeps one more
|
||||
// body's worth of states.
|
||||
let body = 1 << 20;
|
||||
let pieces: Vec<Vec<(usize, usize)>> = (0..saved.bodies.len()).map(|i| vec![(i, body)]).collect();
|
||||
let thinned: Vec<SavedModel> =
|
||||
(0..=saved.bodies.len()).filter_map(|n| saved.thinned(&pieces, n * body + body - 1).map(|(m, _, _)| m)).collect();
|
||||
let kept = |m: &SavedModel| m.records["features"].as_array().unwrap().iter().filter(|f| f.get("state").is_some()).count();
|
||||
let smallest = thinned.first().unwrap();
|
||||
assert!(smallest.records["states"].as_array().unwrap().len() < saved.records["states"].as_array().unwrap().len(), "{sample}");
|
||||
let more = thinned.iter().find(|m| kept(m) > kept(smallest) && kept(m) < built.doc.features.len()).unwrap();
|
||||
check_thinned(&format!("{sample} smallest"), &built, smallest);
|
||||
check_thinned(&format!("{sample} thinned"), &built, more);
|
||||
}
|
||||
}
|
||||
|
||||
/// Opened from its model, a design evaluates only from the first feature an edit changes: the
|
||||
/// saved states before it are kept as they were read (the same states, not evaluated again).
|
||||
#[test]
|
||||
|
||||
@@ -22,6 +22,19 @@ impl Scratch {
|
||||
std::fs::write(&path, json!({"commands": [{"command": "solid.box", "params": {"length": 2, "width": 3, "height": 4}}]}).to_string()).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
fn saving_script(&self) -> (PathBuf, PathBuf) {
|
||||
let path = self.0.join("saving.json");
|
||||
let saved = self.0.join("script-ran.solidcraft");
|
||||
std::fs::write(
|
||||
&path,
|
||||
json!({"commands": [{"command": "solid.box", "params": {"length": 2, "width": 3, "height": 4}},
|
||||
{"command": "file.save_as", "params": {"path": saved}}]})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
(path, saved)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Scratch {
|
||||
@@ -43,6 +56,92 @@ fn body_count(path: &Path) -> Value {
|
||||
json_output(cli().arg("eval").arg(path).output().unwrap())["body_count"].clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_rejects_invalid_options_before_script_execution_or_output_writes() {
|
||||
let dir = Scratch::new();
|
||||
let (script, script_saved) = dir.saving_script();
|
||||
let cases = [
|
||||
(&["--out"][..], "--out needs a value"),
|
||||
(&["--save"][..], "--save needs a value"),
|
||||
(&["--in"][..], "--in needs a value"),
|
||||
(&["--out", "--quiet"][..], "--out needs a value"),
|
||||
(&["--ouut", "part.step"][..], "doesn't take --ouut"),
|
||||
(&["--unknown"][..], "doesn't take --unknown"),
|
||||
];
|
||||
for existing in [false, true] {
|
||||
let saved = dir.0.join(format!("{existing}.solidcraft"));
|
||||
let exported = dir.0.join(format!("{existing}.step"));
|
||||
let report = dir.0.join(format!("{existing}.json"));
|
||||
let outputs = [&saved, &exported, &report];
|
||||
if existing {
|
||||
for path in outputs {
|
||||
std::fs::write(path, b"existing output").unwrap();
|
||||
}
|
||||
}
|
||||
for (extra, message) in cases {
|
||||
let output = cli()
|
||||
.arg("run")
|
||||
.arg(&script)
|
||||
.arg("--save")
|
||||
.arg(&saved)
|
||||
.arg("--out")
|
||||
.arg(&exported)
|
||||
.arg("--json-out")
|
||||
.arg(&report)
|
||||
.args(extra)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(1), "{extra:?}");
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains(message), "{extra:?}: {}", String::from_utf8_lossy(&output.stderr));
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(!script_saved.exists(), "invalid arguments must be rejected before the script runs");
|
||||
for path in outputs {
|
||||
if existing {
|
||||
assert_eq!(std::fs::read(path).unwrap(), b"existing output");
|
||||
} else {
|
||||
assert!(!path.exists());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_and_inspect_reject_invalid_options_before_script_execution_or_reporting() {
|
||||
let dir = Scratch::new();
|
||||
let (script, script_saved) = dir.saving_script();
|
||||
let cases = [
|
||||
(&["--in"][..], "--in needs a value"),
|
||||
(&["--in", "--quiet"][..], "--in needs a value"),
|
||||
(&["--out", "part.step"][..], "doesn't take --out"),
|
||||
(&["--unknown"][..], "doesn't take --unknown"),
|
||||
];
|
||||
for command in ["eval", "inspect"] {
|
||||
for existing in [false, true] {
|
||||
let report = dir.0.join(format!("{command}-{existing}.json"));
|
||||
if existing {
|
||||
std::fs::write(&report, b"existing report").unwrap();
|
||||
}
|
||||
for (extra, message) in cases {
|
||||
let output = cli().arg(command).arg(&script).arg("--json-out").arg(&report).args(extra).output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1), "{command} {extra:?}");
|
||||
assert!(
|
||||
String::from_utf8_lossy(&output.stderr).contains(message),
|
||||
"{command} {extra:?}: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(!script_saved.exists(), "invalid arguments must be rejected before the script runs");
|
||||
if existing {
|
||||
assert_eq!(std::fs::read(&report).unwrap(), b"existing report");
|
||||
} else {
|
||||
assert!(!report.exists());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_loads_both_design_encodings_by_contents() {
|
||||
let dir = Scratch::new();
|
||||
@@ -59,6 +158,84 @@ fn cli_loads_both_design_encodings_by_contents() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #1369: a pending concept that did not land in the numbered release must not be silently
|
||||
/// dropped on open/resave, in either encoding or before replacing an existing destination.
|
||||
#[test]
|
||||
fn unknown_pending_concepts_preserve_the_open_design_and_output_files() {
|
||||
use solidcraft_engine::{Session, doc, io};
|
||||
|
||||
let dir = Scratch::new();
|
||||
for version in [33, doc::format::FORMAT] {
|
||||
let mut experimental = doc::Document::new("Experimental");
|
||||
experimental.format = doc::format::format_string(version);
|
||||
experimental.format_pending.push("experimental-card".into());
|
||||
for (extension, bytes) in [("json", io::write_design(&experimental)), ("solidcraft", io::write_design_container(&experimental).unwrap())] {
|
||||
let source = dir.0.join(format!("experimental-{version}.{extension}"));
|
||||
std::fs::write(&source, &bytes).unwrap();
|
||||
let mut session = Session::default();
|
||||
session.execute("solid.box", &json!({"length": 2, "width": 3, "height": 4})).unwrap();
|
||||
let original = session.doc.clone();
|
||||
let original_path = session.path.clone();
|
||||
let undo = session.undo.len();
|
||||
let dirty = session.is_dirty();
|
||||
let err = session.execute("doc.open", &json!({"path": source})).unwrap_err().to_string();
|
||||
assert!(err.contains("experimental-card") && err.contains("update SolidCraft"), "{err}");
|
||||
assert_eq!(session.doc, original);
|
||||
assert_eq!(session.path, original_path);
|
||||
assert_eq!(session.undo.len(), undo);
|
||||
assert_eq!(session.is_dirty(), dirty);
|
||||
|
||||
for existing in [false, true] {
|
||||
let destination = dir.0.join(format!("output-{version}-{extension}-{existing}.{extension}"));
|
||||
let backup = io::backup_path(&destination);
|
||||
if existing {
|
||||
std::fs::write(&destination, b"existing design").unwrap();
|
||||
std::fs::write(&backup, b"existing backup").unwrap();
|
||||
}
|
||||
let output = cli().args(["run", "--in"]).arg(&source).arg("--save").arg(&destination).output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains("experimental-card") && error.contains("update SolidCraft"), "{error}");
|
||||
if existing {
|
||||
assert_eq!(std::fs::read(&destination).unwrap(), b"existing design");
|
||||
assert_eq!(std::fs::read(&backup).unwrap(), b"existing backup");
|
||||
} else {
|
||||
assert!(!destination.exists() && !backup.exists());
|
||||
}
|
||||
assert_eq!(std::fs::read(&source).unwrap(), bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_pending_concepts_and_released_files_round_trip_with_inert_metadata() {
|
||||
use solidcraft_engine::{doc, io};
|
||||
|
||||
let dir = Scratch::new();
|
||||
let current = doc::Document::new("Supported");
|
||||
let mut released = current.clone();
|
||||
released.format = doc::format::format_string(33);
|
||||
released.format_pending.clear();
|
||||
let mut experimental = released.clone();
|
||||
experimental.format_pending.push("reserved-body-names".into());
|
||||
for (index, mut document) in [current, released, experimental].into_iter().enumerate() {
|
||||
document.metadata.0.insert("com.example.tool".into(), json!({"format_pending": ["experimental-card"], "physical_card": {"density": 700}}));
|
||||
for (extension, bytes) in [("json", io::write_design(&document)), ("solidcraft", io::write_design_container(&document).unwrap())] {
|
||||
let source = dir.0.join(format!("supported-{index}.{extension}"));
|
||||
let destination = dir.0.join(format!("resaved-{index}.{extension}"));
|
||||
std::fs::write(&source, &bytes).unwrap();
|
||||
let output = cli().args(["run", "--in"]).arg(&source).arg("--save").arg(&destination).output().unwrap();
|
||||
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
let reopened = io::read_design(&std::fs::read(&destination).unwrap()).unwrap();
|
||||
assert_eq!(reopened.metadata, document.metadata);
|
||||
assert_eq!(reopened.name, document.name);
|
||||
assert_eq!(reopened.features, document.features);
|
||||
assert_eq!(std::fs::read(&source).unwrap(), bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn results_still_save_and_export_the_script_once() {
|
||||
let dir = Scratch::new();
|
||||
@@ -209,3 +386,175 @@ fn timings_and_profile_report_each_command() {
|
||||
assert!(!output.status.success());
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("--timings needs a script"));
|
||||
}
|
||||
|
||||
fn render_setup_script(dir: &Scratch, job: Value) -> PathBuf {
|
||||
let path = dir.0.join("render.json");
|
||||
std::fs::write(
|
||||
&path,
|
||||
json!({"commands":[
|
||||
{"command":"solid.box","params":{"length":10,"width":10,"height":10}},
|
||||
{"command":"render.settings.set","params":{"name":"Portable setup","set":{"width":16,"height":16,"job":job}}}
|
||||
]})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[cfg(feature = "raytrace-cpu")]
|
||||
#[test]
|
||||
fn render_uses_typed_cpu_setups_from_scripts_and_saved_designs() {
|
||||
for options in [json!({}), json!({"progressive":{"samples_per_pixel":3,"seed":81},"lights":[]})] {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, json!({"method":"cpu_raytrace","options":options}));
|
||||
let image = dir.0.join("script.png");
|
||||
let result = json_output(
|
||||
cli()
|
||||
.arg("render")
|
||||
.arg(&script)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&image)
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-cpu")
|
||||
.output()
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(result["preflight"]["resolved"]["executor"], "cpu-raytrace");
|
||||
assert_eq!(result["export"]["frame"]["executor"], "cpu-raytrace");
|
||||
assert_eq!(result["preflight"]["request"]["integrator"], "raytrace");
|
||||
assert_eq!(result["preflight"]["request"]["raytrace"].get("progressive"), options.get("progressive"));
|
||||
let png = image::load_from_memory(&std::fs::read(&image).unwrap()).unwrap();
|
||||
assert_eq!((png.width(), png.height()), (16, 16));
|
||||
|
||||
let saved = dir.0.join("portable.solidcraft");
|
||||
json_output(cli().arg("run").arg(&script).arg("--save").arg(&saved).output().unwrap());
|
||||
let original = std::fs::read(&saved).unwrap();
|
||||
let overridden = dir.0.join("overridden.png");
|
||||
let result = json_output(
|
||||
cli()
|
||||
.arg("render")
|
||||
.arg(&saved)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&overridden)
|
||||
.args(["--width", "12", "--height", "8", "--exposure", "1.25"])
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-cpu")
|
||||
.output()
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(result["preflight"]["request"]["width"], 12);
|
||||
assert_eq!(result["preflight"]["request"]["height"], 8);
|
||||
assert_eq!(result["preflight"]["request"]["raytrace"]["exposure_ev"], 1.25);
|
||||
assert_eq!(result["export"]["frame"]["executor"], "cpu-raytrace");
|
||||
assert_eq!(result["preflight"]["request"]["raytrace"].get("progressive"), options.get("progressive"));
|
||||
let png = image::load_from_memory(&std::fs::read(overridden).unwrap()).unwrap();
|
||||
assert_eq!((png.width(), png.height()), (12, 8));
|
||||
assert_eq!(std::fs::read(saved).unwrap(), original, "CLI overrides must not rewrite saved intent");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_preserves_typed_raster_and_legacy_runtime_controls() {
|
||||
for job in [Value::Null, json!({"method":"raster","transparent_background":true})] {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, job.clone());
|
||||
let out = dir.0.join("raster.png");
|
||||
let mut command = cli();
|
||||
command
|
||||
.arg("render")
|
||||
.arg(&script)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&out)
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-raster");
|
||||
if job.is_null() {
|
||||
command.arg("--transparent");
|
||||
}
|
||||
let result = json_output(command.output().unwrap());
|
||||
assert_eq!(result["preflight"]["resolved"]["executor"], "cpu-product");
|
||||
assert_eq!(result["export"]["frame"]["executor"], "cpu-product");
|
||||
assert_eq!(result["preflight"]["request"]["transparent_background"], true);
|
||||
assert!(std::fs::read(out).unwrap().starts_with(b"\x89PNG\r\n\x1a\n"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_render_rejects_explicit_runtime_flags_before_gpu_or_option_file_reads() {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, json!({"method":"gpu_path","options":{"samples":64,"seed":0}}));
|
||||
let out = dir.0.join("existing.png");
|
||||
std::fs::write(&out, b"previous completed image").unwrap();
|
||||
for flags in [
|
||||
&["--progressive"][..],
|
||||
&["--transparent"][..],
|
||||
&["--samples", "64"][..],
|
||||
&["--max-depth", "8"][..],
|
||||
&["--seed", "0"][..],
|
||||
&["--lighting", "deliberately-missing-lighting.json"][..],
|
||||
&["--tone-map", "reinhard"][..],
|
||||
&["--environment", "deliberately-missing-environment.json"][..],
|
||||
] {
|
||||
let output = cli()
|
||||
.arg("render")
|
||||
.arg(&script)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&out)
|
||||
.arg("--overwrite")
|
||||
.args(flags)
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-conflicts")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains(&format!("{} cannot accompany a typed saved render setup", flags[0])), "{error}");
|
||||
assert!(output.stdout.is_empty());
|
||||
assert_eq!(std::fs::read(&out).unwrap(), b"previous completed image");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_gpu_render_selects_its_host_without_a_progressive_runtime_flag() {
|
||||
// A deliberately invalid policy proves that dispatch reached the GPU host
|
||||
// without depending on an adapter or accepting a CPU fallback. The legacy
|
||||
// --progressive path must still reach that same host-selection boundary.
|
||||
for job in [json!({"method":"gpu_path","options":{"samples":2}}), Value::Null] {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, job.clone());
|
||||
let out = dir.0.join("gpu.png");
|
||||
let mut command = cli();
|
||||
command.arg("render").arg(&script).args(["--setup", "Portable setup", "--out"]).arg(&out).env("SOLIDCRAFT_GPU", "not-a-gpu-policy");
|
||||
if job.is_null() {
|
||||
command.arg("--progressive");
|
||||
}
|
||||
let output = command.output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains("SOLIDCRAFT_GPU must be auto, hardware, software or cpu"), "{error}");
|
||||
assert!(!out.exists());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_setup_overrides_obey_shared_validation_before_host_initialization() {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, json!({"method":"cpu_raytrace","options":{"progressive":{"samples_per_pixel":64}}}));
|
||||
let out = dir.0.join("invalid.png");
|
||||
// 1024 * 1024 * 64 exceeds the unchanged default Compact camera-sample
|
||||
// budget. Admission must name that envelope before initializing any host.
|
||||
for (flags, message) in [
|
||||
(&["--width", "1024", "--height", "1024"][..], "CPU Compact exceeds its 16777216 camera-sample admission"),
|
||||
(&["--width", "0"][..], "render output must be at least"),
|
||||
(&["--exposure", "NaN"][..], "render exposure must be finite"),
|
||||
] {
|
||||
let output = cli()
|
||||
.arg("render")
|
||||
.arg(&script)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&out)
|
||||
.args(flags)
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-invalid-settings")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains(message), "{error}");
|
||||
assert!(!out.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,17 @@ workspace = true
|
||||
name = "solidcraft-web"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
|
||||
[dependencies]
|
||||
solidcraft-engine = { workspace = true }
|
||||
solidcraft-ui-egui = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
log = { workspace = true }
|
||||
url = "2"
|
||||
percent-encoding = "2"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
|
||||
@@ -30,5 +36,6 @@ web-sys = { version = "0.3", features = [
|
||||
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
|
||||
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage",
|
||||
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
|
||||
"BeforeUnloadEvent", "Event", "EventTarget", "Response", "MediaQueryList", "Gamepad", "Performance",
|
||||
"Headers", "ReadableStream", "ReadableStreamDefaultReader",
|
||||
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "MediaQueryList", "Gamepad", "Performance",
|
||||
] }
|
||||
|
||||
@@ -114,6 +114,46 @@ pub fn opens_design(extensions: &[&str]) -> bool {
|
||||
extensions.is_empty() || extensions.iter().any(|e| solidcraft_engine::io::DESIGN_EXTS.contains(e))
|
||||
}
|
||||
|
||||
/// Where an Open for `extensions` (what the command reads; `&[]`: anything) is answered.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum OpenRoute {
|
||||
/// The browser's open dialog for a design: the file keeps its handle, so Save writes back.
|
||||
Disk,
|
||||
/// The "Files in this browser" window, where the browser can't open a design from the disk.
|
||||
Panel,
|
||||
/// A plain file input that offers these types: the file is read into the engine's files and
|
||||
/// the command runs on it (Insert DXF or SVG, images, meshes, imported settings).
|
||||
Upload,
|
||||
}
|
||||
|
||||
/// Choose how an Open for `extensions` asks for its file; `api`: the browser can open a file
|
||||
/// from the disk and keep its handle.
|
||||
pub fn open_route(api: bool, extensions: &[&str]) -> OpenRoute {
|
||||
match (opens_design(extensions), api) {
|
||||
(true, true) => OpenRoute::Disk,
|
||||
(true, false) => OpenRoute::Panel,
|
||||
(false, _) => OpenRoute::Upload,
|
||||
}
|
||||
}
|
||||
|
||||
/// The `accept` list of a file input for `extensions` (with or without their dot): ".dxf,.svg".
|
||||
pub fn accept_list(extensions: &[&str]) -> String {
|
||||
let list: Vec<String> = extensions
|
||||
.iter()
|
||||
.map(|e| e.trim().trim_start_matches('.'))
|
||||
.filter(|e| !e.is_empty())
|
||||
.map(|e| format!(".{}", e.to_ascii_lowercase()))
|
||||
.collect();
|
||||
list.join(",")
|
||||
}
|
||||
|
||||
/// The largest file an upload for `extensions` reads: images go into a canvas, which has a size
|
||||
/// limit, so a bigger one is refused before it is read. `None`: the importer checks the file.
|
||||
pub fn upload_limit(extensions: &[&str]) -> Option<usize> {
|
||||
let image = |e: &&str| ["png", "jpg", "jpeg"].contains(&e.trim_start_matches('.').to_ascii_lowercase().as_str());
|
||||
(!extensions.is_empty() && extensions.iter().all(image)).then_some(solidcraft_engine::doc::canvas::MAX_CANVAS_BYTES)
|
||||
}
|
||||
|
||||
/// The file type a Save As dialog offers for `name`: (description, MIME type, extension).
|
||||
pub fn save_type(name: &str) -> (&'static str, &'static str, &'static str) {
|
||||
let json = std::path::Path::new(name).extension().is_some_and(|e| e.eq_ignore_ascii_case("json"));
|
||||
@@ -140,7 +180,7 @@ mod web {
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
use web_sys::{FileSystemFileHandle, IdbDatabase, IdbObjectStore, IdbOpenDbRequest, IdbRequest, IdbTransactionMode};
|
||||
|
||||
use super::{HANDLE_MAX_AGE_MS, is_abort, make_id, opens_design, parse, path, plan_prune, save_type};
|
||||
use super::{HANDLE_MAX_AGE_MS, is_abort, make_id, parse, path, plan_prune, save_type};
|
||||
use crate::files::{Files, err, read_handle};
|
||||
|
||||
const DB: &str = "solidcraft-disk-handles";
|
||||
@@ -294,7 +334,7 @@ mod web {
|
||||
let options = Object::new();
|
||||
let _ = Reflect::set(&options, &"multiple".into(), &JsValue::FALSE);
|
||||
let extensions = Array::new();
|
||||
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf"] {
|
||||
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm", ".dxf", ".svg"] {
|
||||
extensions.push(&e.into());
|
||||
}
|
||||
let accept = Object::new();
|
||||
@@ -315,11 +355,6 @@ mod web {
|
||||
p
|
||||
}
|
||||
|
||||
/// Does an Open for `extensions` use the browser's open dialog?
|
||||
pub fn handles_open(extensions: &[&str]) -> bool {
|
||||
supported() && opens_design(extensions)
|
||||
}
|
||||
|
||||
// ---- writing ------------------------------------------------------------------------
|
||||
|
||||
/// Queue what a command wrote to a disk path. Writes go one at a time; a newer write of the
|
||||
@@ -621,6 +656,21 @@ mod tests {
|
||||
assert!(!opens_design(&["json"]));
|
||||
}
|
||||
|
||||
/// Insert DXF or SVG, images and other non-design files get a file input that offers their
|
||||
/// types in every browser (#2052); designs keep the disk dialog or the browser's files.
|
||||
#[test]
|
||||
fn opens_of_other_files_use_a_file_input_for_their_types() {
|
||||
assert_eq!(open_route(true, &["dxf"]), OpenRoute::Upload);
|
||||
assert_eq!(open_route(false, &["png", "jpg", "jpeg"]), OpenRoute::Upload);
|
||||
assert_eq!(open_route(true, &[]), OpenRoute::Disk);
|
||||
assert_eq!(open_route(false, &["solidcraft"]), OpenRoute::Panel);
|
||||
assert_eq!(accept_list(&["dxf"]), ".dxf");
|
||||
assert_eq!(accept_list(&["png", ".JPG", "", "jpeg"]), ".png,.jpg,.jpeg");
|
||||
assert_eq!(accept_list(&[]), "");
|
||||
assert!(upload_limit(&["png", "jpg", "jpeg"]).is_some());
|
||||
assert_eq!(upload_limit(&["dxf"]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_save_dialog_offers_the_type_of_the_name() {
|
||||
assert_eq!(save_type("Plate.solidcraft").2, ".solidcraft");
|
||||
|
||||
@@ -448,6 +448,18 @@ pub fn recovery_path() -> String {
|
||||
format!("{OPFS}{RECOVERY}")
|
||||
}
|
||||
|
||||
/// A recovery file lives in OPFS, but its relative links belong to the original design's
|
||||
/// folder (which can be on disk). Resolve them before Open sees the temporary file location.
|
||||
pub fn prepare_recovery(original_path: Option<&str>) -> Result<String, String> {
|
||||
let path = recovery_path();
|
||||
let bytes = vfs::read(&path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| e.to_string())?;
|
||||
let mut doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| e.to_string())?;
|
||||
solidcraft_engine::resolve_design_links(&mut doc, original_path);
|
||||
let bytes = solidcraft_engine::io::write_design_container(&doc).map_err(|e| e.to_string())?;
|
||||
vfs::insert(&path, bytes);
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Pick files on the computer: the File System Access picker where there is one, else an
|
||||
/// `<input type=file>`. Read files arrive in `files.incoming`.
|
||||
pub fn pick_from_disk(files: &Rc<Files>, intent: Intent) {
|
||||
@@ -499,7 +511,11 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
|
||||
let Some(doc) = web_sys::window().and_then(|w| w.document()) else { return };
|
||||
let Ok(input) = doc.create_element("input").map(|e| e.unchecked_into::<web_sys::HtmlInputElement>()) else { return };
|
||||
input.set_type("file");
|
||||
input.set_accept(".solidcraft,.solvecraft,.json,.step,.stp,.3mf,.stl,.obj,.glb,.gltf");
|
||||
// Everything the browser build imports, from the format registry: cadmpeg is pure Rust, so
|
||||
// FreeCAD and Rhino files are part of it; plus the DXF and SVG drawings Open and Insert take.
|
||||
let accept =
|
||||
solidcraft_engine::io::formats::open_extensions(true).iter().chain(&["dxf", "svg"]).map(|e| format!(".{e}")).collect::<Vec<_>>().join(",");
|
||||
input.set_accept(&accept);
|
||||
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
|
||||
let _ = input.set_attribute("style", "display:none");
|
||||
if let Some(body) = doc.body() {
|
||||
@@ -524,6 +540,67 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
|
||||
input.click();
|
||||
}
|
||||
|
||||
/// Ask for one file of these types (`extensions`, e.g. `["dxf"]`) with a plain file input, the
|
||||
/// dialog every browser has. The answer is the file's path in the engine's files (under
|
||||
/// [`UPLOAD`], as for a file picked in the disk dialog), so the command that asked runs on it;
|
||||
/// `None` when the dialog was closed or the file could not be read.
|
||||
pub fn pick_upload(files: &Rc<Files>, extensions: &[&str]) -> impl std::future::Future<Output = Option<String>> + use<> {
|
||||
// Shown now, in the click: a file dialog needs the user's gesture.
|
||||
let chosen = choose_file(&disk::accept_list(extensions));
|
||||
let limit = disk::upload_limit(extensions);
|
||||
let files = files.clone();
|
||||
async move {
|
||||
let read = async {
|
||||
let value = JsFuture::from(chosen?).await.map_err(err)?;
|
||||
// Closing the dialog answers no file: nothing to say.
|
||||
let Ok(file) = value.dyn_into::<web_sys::File>() else { return Ok(None) };
|
||||
let name = file.name();
|
||||
if limit.is_some_and(|max| !file.size().is_finite() || file.size() > max as f64) {
|
||||
return Err(format!("{name}: the file is too large"));
|
||||
}
|
||||
let bytes = blob_bytes(&file).await.map_err(|e| format!("{name}: {e}"))?;
|
||||
let path = format!("{UPLOAD}{name}");
|
||||
vfs::insert(&path, bytes);
|
||||
Ok(Some(path))
|
||||
};
|
||||
match read.await {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
files.say(format!("open: {e}"), true);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a hidden `<input type=file>` offering `accept` (call in the click). The promise answers
|
||||
/// the picked `File`, or `null` when the dialog is closed (the input's `cancel` event).
|
||||
fn choose_file(accept: &str) -> Result<js_sys::Promise, String> {
|
||||
let doc = web_sys::window().and_then(|w| w.document()).ok_or("this page can't show a file dialog")?;
|
||||
let input = doc.create_element("input").map_err(err)?.dyn_into::<web_sys::HtmlInputElement>().map_err(|_| "no file input".to_string())?;
|
||||
input.set_type("file");
|
||||
input.set_accept(accept);
|
||||
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
|
||||
input.set_attribute("style", "display:none").map_err(err)?;
|
||||
doc.body().ok_or("this page has no body")?.append_child(&input).map_err(err)?;
|
||||
let promise = js_sys::Promise::new(&mut |resolve: Function, _reject: Function| {
|
||||
// One answer for a pick or a cancel; read from the event, so it does not keep the input.
|
||||
let done = Closure::once_into_js(move |event: web_sys::Event| {
|
||||
let file = event.target().and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()).and_then(|input| {
|
||||
input.set_onchange(None);
|
||||
input.set_oncancel(None);
|
||||
input.remove();
|
||||
input.files().and_then(|list| list.get(0))
|
||||
});
|
||||
let _ = resolve.call1(&JsValue::UNDEFINED, &file.map_or(JsValue::NULL, JsValue::from));
|
||||
});
|
||||
input.set_onchange(Some(done.unchecked_ref()));
|
||||
input.set_oncancel(Some(done.unchecked_ref()));
|
||||
});
|
||||
input.click();
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
/// Read files dropped on the page.
|
||||
pub fn take_drops(files: &Rc<Files>, raw: &mut egui::RawInput) {
|
||||
for f in std::mem::take(&mut raw.dropped_files) {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//! Bounded HTTPS or same-origin downloads, handed to the normal design-open command as unsaved copies.
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use js_sys::{Reflect, Uint8Array};
|
||||
use solidcraft_engine::io::vfs;
|
||||
use solidcraft_ui_egui::{SolveApp, ui_kit};
|
||||
use wasm_bindgen::{JsCast as _, JsValue};
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
|
||||
use crate::{check_download_size, design_url};
|
||||
|
||||
type Reply = Result<(String, Vec<u8>), String>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Download {
|
||||
reply: Rc<RefCell<Option<Reply>>>,
|
||||
pending: Option<(String, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl Download {
|
||||
pub fn start(url: Option<&str>, ctx: &egui::Context) -> Self {
|
||||
let download = Self::default();
|
||||
if let Some(url) = url {
|
||||
let url = url.to_owned();
|
||||
let slot = download.reply.clone();
|
||||
let ctx = ctx.clone();
|
||||
spawn_local(async move {
|
||||
*slot.borrow_mut() = Some(fetch(&url).await);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
download
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
|
||||
if let Some(reply) = self.reply.borrow_mut().take() {
|
||||
match reply {
|
||||
Ok(file) => self.pending = Some(file),
|
||||
Err(error) => app.set_status(format!("Could not open design link: {error}"), true),
|
||||
}
|
||||
}
|
||||
if self.pending.is_none() || app.file_picker_busy() {
|
||||
return;
|
||||
}
|
||||
if solidcraft_ui_egui::documents::any_dirty(app) {
|
||||
let mut answer = None;
|
||||
let closed = ui_kit::Window::new(egui::Id::new("open_link"), "status_warning", solidcraft_ui_egui::i18n::tr("web.open_link"))
|
||||
.required()
|
||||
.show_body(ctx, |ui| {
|
||||
ui.label(solidcraft_ui_egui::i18n::tr("web.open_link_confirm"));
|
||||
ui.horizontal(|ui| {
|
||||
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.open"), ui_kit::Button::Normal).clicked() {
|
||||
answer = Some(true);
|
||||
}
|
||||
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.cancel"), ui_kit::Button::Normal).clicked() {
|
||||
answer = Some(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
match answer {
|
||||
Some(true) => {}
|
||||
Some(false) => {
|
||||
self.pending = None;
|
||||
return;
|
||||
}
|
||||
None if closed => {
|
||||
self.pending = None;
|
||||
return;
|
||||
}
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
let Some((name, bytes)) = self.pending.take() else { return };
|
||||
// The extension selects the design loader even when the URL has no extension.
|
||||
let path = "/linked/design.solidcraft";
|
||||
vfs::insert(path, bytes);
|
||||
app.open_path(path);
|
||||
if app.session.path.as_deref() == Some(path) {
|
||||
app.session.path = None;
|
||||
app.session.doc_mut().name = name;
|
||||
app.session.mark_unsaved();
|
||||
} else {
|
||||
app.set_status("Could not open design link: the downloaded file is not a valid SolidCraft design", true);
|
||||
}
|
||||
let _ = vfs::remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch(value: &str) -> Reply {
|
||||
let window = web_sys::window().ok_or("No browser window")?;
|
||||
let page_origin = window.location().origin().map_err(|_| "Cannot read page origin")?;
|
||||
let url = design_url(value, &page_origin)?;
|
||||
let name = url.path_segments().and_then(|mut segments| segments.next_back()).filter(|name| !name.is_empty()).unwrap_or("Design.solidcraft");
|
||||
let decoded = js_sys::decode_uri_component(name).ok().and_then(|name| name.as_string()).unwrap_or_else(|| name.to_owned());
|
||||
let name: String = decoded.strip_suffix(".solidcraft").unwrap_or(&decoded).chars().take(255).collect();
|
||||
let response = JsFuture::from(window.fetch_with_str(url.as_str())).await.map_err(|_| "Network request failed or the host refused CORS access")?;
|
||||
let response: web_sys::Response = response.dyn_into().map_err(|_| "Invalid download response")?;
|
||||
design_url(&response.url(), &page_origin).map_err(|_| "Design redirected to a URL that does not use HTTPS or the page's own origin")?;
|
||||
if !response.ok() {
|
||||
return Err(format!("HTTP {} {}", response.status(), response.status_text()));
|
||||
}
|
||||
let length = response
|
||||
.headers()
|
||||
.get("Content-Length")
|
||||
.map_err(|_| "Cannot read download size")?
|
||||
.map(|value| value.parse::<u64>().map_err(|_| "Invalid Content-Length"))
|
||||
.transpose()?;
|
||||
if let Some(length) = length {
|
||||
check_download_size(0, length)?;
|
||||
}
|
||||
let stream = response.body().ok_or("Download has no body")?;
|
||||
let reader: web_sys::ReadableStreamDefaultReader = stream.get_reader().dyn_into().map_err(|_| "Cannot read download stream")?;
|
||||
let result = read(&reader).await;
|
||||
if result.is_err() {
|
||||
let _ = JsFuture::from(reader.cancel()).await;
|
||||
}
|
||||
// Fetch rejects interrupted HTTP bodies and decodes Content-Encoding. Content-Length
|
||||
// can describe compressed bytes, so the streamed limit also checks the decoded size.
|
||||
let bytes = result?;
|
||||
Ok((name, bytes))
|
||||
}
|
||||
|
||||
async fn read(reader: &web_sys::ReadableStreamDefaultReader) -> Result<Vec<u8>, String> {
|
||||
let mut bytes = Vec::new();
|
||||
// Bound even a stream which keeps returning empty chunks.
|
||||
for _ in 0..1_000_000 {
|
||||
let chunk = JsFuture::from(reader.read()).await.map_err(|_| "Download interrupted")?;
|
||||
if Reflect::get(&chunk, &JsValue::from_str("done")).map_err(|_| "Invalid download stream")?.as_bool() == Some(true) {
|
||||
return Ok(bytes);
|
||||
}
|
||||
let value = Reflect::get(&chunk, &JsValue::from_str("value")).map_err(|_| "Invalid download chunk")?;
|
||||
let chunk: Uint8Array = value.dyn_into().map_err(|_| "Invalid download bytes")?;
|
||||
check_download_size(bytes.len(), u64::from(chunk.length()))?;
|
||||
bytes.extend_from_slice(&chunk.to_vec());
|
||||
}
|
||||
Err("Download returned too many chunks".into())
|
||||
}
|
||||
@@ -14,9 +14,102 @@ mod disk;
|
||||
mod files;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod install;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod linked;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod space_mouse;
|
||||
|
||||
/// The browser switches tabs with Ctrl+Tab and Ctrl+Shift+Tab, so the page leaves those keys to
|
||||
/// it (#5227); plain Tab and Shift+Tab still move between the app's fields. (Ctrl+PageUp and
|
||||
/// Ctrl+PageDown already reach the browser.)
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn browser_switches_tabs(key: &str, ctrl: bool) -> bool {
|
||||
ctrl && key == "Tab"
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn query_flag(query: &str, name: &str) -> bool {
|
||||
url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()).any(|(key, _)| key == name)
|
||||
}
|
||||
|
||||
/// Launch options use exact query keys; URL values are decoded only once.
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
#[derive(Default, Debug)]
|
||||
struct Launch {
|
||||
open: Option<String>,
|
||||
embed: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
impl Launch {
|
||||
fn parse(query: &str) -> Self {
|
||||
let mut launch = Self::default();
|
||||
let mut embed = false;
|
||||
for (key, value) in url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"open" if launch.open.is_none() => launch.open = Some(value.into_owned()),
|
||||
"embed" => embed = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
launch.embed = embed && launch.open.is_some();
|
||||
launch
|
||||
}
|
||||
|
||||
fn persists(&self) -> bool {
|
||||
!self.embed
|
||||
}
|
||||
|
||||
fn full_app_url(&self, mut url: url::Url) -> String {
|
||||
url.set_query(None);
|
||||
url.set_fragment(None);
|
||||
if let Some(open) = &self.open {
|
||||
url.query_pairs_mut().append_pair("open", open);
|
||||
}
|
||||
url.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn design_url(value: &str, page_origin: &str) -> Result<url::Url, String> {
|
||||
if value.len() > 16_384 {
|
||||
return Err("Design URL is too long".into());
|
||||
}
|
||||
let decoded = percent_encoding::percent_decode_str(value).decode_utf8().map_err(|_| "Invalid design URL encoding")?;
|
||||
if decoded.chars().any(|ch| ch.is_control() || ch == '\\') {
|
||||
return Err("Design links must not contain backslashes or control characters".into());
|
||||
}
|
||||
// Check before URL parsing normalizes away dot segments. Decode escaped slashes too.
|
||||
let path = value.split(['?', '#']).next().unwrap_or_default();
|
||||
let path = percent_encoding::percent_decode_str(path).collect::<Vec<_>>();
|
||||
if path.split(|byte| *byte == b'/').any(|segment| segment == b"..") {
|
||||
return Err("Design links must not contain parent path segments".into());
|
||||
}
|
||||
let page = url::Url::parse(page_origin).map_err(|_| "Invalid page origin")?;
|
||||
let url = if value.starts_with('/') && !value.starts_with("//") {
|
||||
page.join(value).map_err(|_| "Invalid design path")?
|
||||
} else {
|
||||
url::Url::parse(value).map_err(|_| "Invalid design URL")?
|
||||
};
|
||||
if (url.scheme() != "https" && url.origin() != page.origin())
|
||||
|| url.host_str().is_none()
|
||||
|| !url.username().is_empty()
|
||||
|| url.password().is_some()
|
||||
{
|
||||
return Err("Design links must use HTTPS or the page's own origin without credentials".into());
|
||||
}
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn check_download_size(current: usize, incoming: u64) -> Result<(), String> {
|
||||
let cap = solidcraft_engine::io::MAX_DESIGN_BYTES as u64;
|
||||
if current as u64 > cap || incoming > cap.saturating_sub(current as u64) {
|
||||
return Err(format!("Design exceeds the {} MB size limit", cap / (1024 * 1024)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web {
|
||||
use std::rc::Rc;
|
||||
@@ -84,12 +177,29 @@ mod web {
|
||||
on_unload.forget();
|
||||
}
|
||||
|
||||
/// eframe cancels the browser's action for every Tab it gets, so the browser couldn't switch
|
||||
/// tabs while the app has the keyboard. Its tab-switching keys stop here, on the way down to
|
||||
/// the canvas, before eframe sees them; the browser then acts on them as usual.
|
||||
fn leave_tab_switching_to_browser(window: &web_sys::Window) {
|
||||
let on_key = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::KeyboardEvent)>::new(|e: web_sys::KeyboardEvent| {
|
||||
if crate::browser_switches_tabs(&e.key(), e.ctrl_key()) {
|
||||
e.stop_propagation();
|
||||
}
|
||||
});
|
||||
let _ = window.add_event_listener_with_callback_and_bool("keydown", on_key.as_ref().unchecked_ref(), true);
|
||||
// The listener lives as long as the page.
|
||||
on_key.forget();
|
||||
}
|
||||
|
||||
const JAPANESE_FONT_PATH: &str = "./fonts/BIZUDPGothic-Regular.ttf";
|
||||
|
||||
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<Vec<u8>, String>>>>;
|
||||
|
||||
struct Shell {
|
||||
app: SolveApp,
|
||||
launch: crate::Launch,
|
||||
linked: crate::linked::Download,
|
||||
full_url: Option<String>,
|
||||
api: Api,
|
||||
files: Rc<Files>,
|
||||
/// When the last autosave was (seconds), and the revision it holds (none stored: `None`).
|
||||
@@ -109,11 +219,15 @@ mod web {
|
||||
/// The language the fonts were last set up for (a CJK UI leads with the CJK face).
|
||||
font_language: String,
|
||||
space_mouse: SpaceMouse,
|
||||
wheel_gestures: solidcraft_ui_egui::WebWheelGestures,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
/// Files from the computer, writes to store, messages and the autosave.
|
||||
fn files_tick(&mut self, ctx: &egui::Context) {
|
||||
if !self.launch.persists() {
|
||||
return;
|
||||
}
|
||||
if self.files.ctx.borrow().is_none() {
|
||||
*self.files.ctx.borrow_mut() = Some(ctx.clone());
|
||||
}
|
||||
@@ -207,13 +321,15 @@ mod web {
|
||||
fn sync_recents(&mut self) {
|
||||
let mut entries = self.files.entries.borrow().clone();
|
||||
entries.sort_by(|a, b| b.modified.total_cmp(&a.modified));
|
||||
let forgotten = self.app.home.forgotten.clone();
|
||||
let recent = &mut self.app.home.recent;
|
||||
let known = self.files.disk_known.borrow().clone();
|
||||
// A design on the computer's disk stays listed while its file is remembered.
|
||||
recent.retain(|p| vfs::exists(p) || disk::parse(p).is_some_and(|(id, _)| known.contains(id)));
|
||||
for e in entries.iter().filter(|e| solidcraft_engine::io::is_design_path(&e.name)) {
|
||||
let p = format!("{}{}", files::OPFS, e.name);
|
||||
if !recent.contains(&p) {
|
||||
// A design the user removed from the list is not added back while it is stored.
|
||||
if !recent.contains(&p) && !forgotten.contains(&p) {
|
||||
recent.push(p);
|
||||
}
|
||||
}
|
||||
@@ -222,6 +338,10 @@ mod web {
|
||||
let recovered = self.files.recovery.borrow().as_ref().and_then(|(_, p)| p.clone());
|
||||
let keep = recent.iter().chain(recovered.as_ref()).filter_map(|p| disk::parse(p)).map(|(id, _)| id.to_string()).collect();
|
||||
disk::prune(keep);
|
||||
// Drop forgotten entries whose stored design is gone, so the set does not grow without bound.
|
||||
let stored: std::collections::BTreeSet<String> =
|
||||
entries.iter().filter(|e| solidcraft_engine::io::is_design_path(&e.name)).map(|e| format!("{}{}", files::OPFS, e.name)).collect();
|
||||
self.app.home.forgotten.retain(|p| stored.contains(p));
|
||||
}
|
||||
|
||||
/// The stored designs: open, insert, rename, delete, download; open from the computer.
|
||||
@@ -238,7 +358,7 @@ mod web {
|
||||
files::pick_from_disk(&self.files, Intent::Open);
|
||||
}
|
||||
if ui_kit::button(ui, "Insert from computer…", ui_kit::Button::Normal)
|
||||
.on_hover_text("STEP, 3MF or STL into the open design")
|
||||
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_drawings_hint"))
|
||||
.clicked()
|
||||
{
|
||||
files::pick_from_disk(&self.files, Intent::Insert);
|
||||
@@ -291,7 +411,7 @@ mod web {
|
||||
let design = solidcraft_engine::io::is_design_path(&e.name);
|
||||
if !design
|
||||
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
|
||||
.on_hover_text("Into the open design (STEP, 3MF, STL)")
|
||||
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
|
||||
.clicked()
|
||||
{
|
||||
act = Some(("insert", e.name.clone()));
|
||||
@@ -360,6 +480,8 @@ mod web {
|
||||
"delete" => {
|
||||
files::delete(&self.files, &name);
|
||||
self.app.home.recent.retain(|r| *r != path);
|
||||
// The file is gone, so there is nothing left to keep forgotten.
|
||||
self.app.home.forgotten.remove(&path);
|
||||
}
|
||||
"rename_cancel" => self.renaming = None,
|
||||
"rename" => {
|
||||
@@ -373,6 +495,10 @@ mod web {
|
||||
*r = new.clone();
|
||||
}
|
||||
}
|
||||
// A forgotten design follows its new name.
|
||||
if self.app.home.forgotten.remove(&old) {
|
||||
self.app.home.forgotten.insert(new);
|
||||
}
|
||||
}
|
||||
Err(e) => self.app.set_status(e, true),
|
||||
}
|
||||
@@ -400,15 +526,25 @@ mod web {
|
||||
});
|
||||
});
|
||||
let Some(recover) = answer else { return };
|
||||
*self.files.recovery.borrow_mut() = None;
|
||||
if recover {
|
||||
self.app.open_path(&files::recovery_path());
|
||||
let recovery = match files::prepare_recovery(path.as_deref()) {
|
||||
Ok(recovery) => recovery,
|
||||
Err(error) => {
|
||||
self.app.set_status(error, true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.app.open_path(&recovery);
|
||||
if self.app.session.path.as_deref() != Some(recovery.as_str()) {
|
||||
return;
|
||||
}
|
||||
// It belongs to its own file again (saving goes there); it is unsaved.
|
||||
self.app.session.path = path;
|
||||
self.app.session.doc_mut().name = name;
|
||||
self.app.session.mark_unsaved();
|
||||
self.app.home.open = false;
|
||||
}
|
||||
*self.files.recovery.borrow_mut() = None;
|
||||
files::autosave(&self.files, None);
|
||||
}
|
||||
|
||||
@@ -487,19 +623,46 @@ mod web {
|
||||
UNSAVED.with(|u| u.set(solidcraft_ui_egui::documents::any_dirty(&self.app)));
|
||||
}
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
files::take_drops(&self.files, raw);
|
||||
let now = raw.time.unwrap_or_else(|| ctx.input(|i| i.time));
|
||||
if let Some(delay) = self.wheel_gestures.update(raw, now) {
|
||||
ctx.request_repaint_after(delay);
|
||||
}
|
||||
if self.launch.persists() {
|
||||
files::take_drops(&self.files, raw);
|
||||
} else {
|
||||
raw.dropped_files.clear();
|
||||
}
|
||||
// Before the app's own hook, which adds the control channel's events.
|
||||
self.space_mouse.raw_input_hook(ctx, self.app.preferences.space_mouse_enabled, raw);
|
||||
self.app.raw_input_hook(raw);
|
||||
}
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.linked.tick(&mut self.app, ui.ctx());
|
||||
UNSAVED.with(|u| u.set(self.launch.persists() && solidcraft_ui_egui::documents::any_dirty(&self.app)));
|
||||
if self.launch.embed {
|
||||
ui_kit::Container::new(ui.ctx(), ui_kit::Surface::Chrome).edge("embed_header", ui_kit::Edge::Top).show(ui, |ui| {
|
||||
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("web.open_full"), ui_kit::Button::Normal).clicked()
|
||||
&& let (Some(window), Some(url)) = (web_sys::window(), &self.full_url)
|
||||
{
|
||||
let _ = window.open_with_url_and_target_and_features(url, "_blank", "noopener");
|
||||
}
|
||||
});
|
||||
}
|
||||
self.app.ui(ui);
|
||||
let ctx = ui.ctx().clone();
|
||||
self.file_panel(&ctx);
|
||||
self.recovery_prompt(&ctx);
|
||||
if self.launch.persists() {
|
||||
self.file_panel(&ctx);
|
||||
self.recovery_prompt(&ctx);
|
||||
}
|
||||
self.watch_text_for_font(&ctx);
|
||||
}
|
||||
fn persist_egui_memory(&self) -> bool {
|
||||
self.launch.persists()
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
if !self.launch.persists() {
|
||||
return;
|
||||
}
|
||||
// Persist the user's own language, not a session-only English fallback.
|
||||
let saved = self.font.persisted_language(&self.app.preferences.language).to_owned();
|
||||
let live = std::mem::replace(&mut self.app.preferences.language, saved);
|
||||
@@ -518,15 +681,15 @@ mod web {
|
||||
|
||||
/// Render with WebGL2: asked for now (`?webgl`, remembered) or before (until `?webgpu`).
|
||||
fn webgl() -> bool {
|
||||
let storage = web_sys::window().and_then(|w| w.local_storage().ok().flatten());
|
||||
let storage = crate::Launch::parse(&query()).persists().then(|| web_sys::window().and_then(|w| w.local_storage().ok().flatten())).flatten();
|
||||
let q = query();
|
||||
if q.contains("webgl") {
|
||||
if crate::query_flag(&q, "webgl") {
|
||||
if let Some(s) = &storage {
|
||||
let _ = s.set_item(WEBGL_KEY, "1");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if q.contains("webgpu") {
|
||||
if crate::query_flag(&q, "webgpu") {
|
||||
if let Some(s) = &storage {
|
||||
let _ = s.remove_item(WEBGL_KEY);
|
||||
}
|
||||
@@ -552,7 +715,11 @@ mod web {
|
||||
eframe::WebLogger::init(log::LevelFilter::Info).ok();
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
let Some(window) = web_sys::window() else { return };
|
||||
ask_before_unload(&window);
|
||||
let launch = crate::Launch::parse(&query());
|
||||
if launch.persists() {
|
||||
ask_before_unload(&window);
|
||||
}
|
||||
leave_tab_switching_to_browser(&window);
|
||||
let Some(document) = window.document() else { return };
|
||||
let Some(canvas) = document.get_element_by_id(CANVAS_ID).and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok()) else {
|
||||
log::error!("missing <canvas id=\"{CANVAS_ID}\">");
|
||||
@@ -571,19 +738,24 @@ mod web {
|
||||
Box::new(move |cc| {
|
||||
solidcraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx);
|
||||
let files = Rc::new(Files::default());
|
||||
files::start(&files);
|
||||
if launch.persists() {
|
||||
files::start(&files);
|
||||
}
|
||||
let (panel, opener, saver, shown) = (files.clone(), files.clone(), files.clone(), files.clone());
|
||||
let (restorable, restorer) = (files.clone(), files.clone());
|
||||
let services = Services {
|
||||
let services = if launch.embed { Services::default() } else { Services {
|
||||
// Open shows the browser's open dialog for a design where it has one
|
||||
// (the file keeps its handle, so Save writes back to it); otherwise,
|
||||
// and for inserting STEP or meshes, the browser's files window.
|
||||
pick_open: Some(Box::new(move |exts: &[&str]| {
|
||||
if disk::handles_open(exts) {
|
||||
return Box::pin(disk::pick_open(&opener));
|
||||
// (the file keeps its handle, so Save writes back to it), else the
|
||||
// browser's files window. A command that reads another kind of file
|
||||
// (Insert DXF or SVG, an image, a mesh, settings) gets a file dialog
|
||||
// offering that kind, and runs on the picked file.
|
||||
pick_open: Some(Box::new(move |exts: &[&str]| match disk::open_route(disk::supported(), exts) {
|
||||
disk::OpenRoute::Disk => Box::pin(disk::pick_open(&opener)),
|
||||
disk::OpenRoute::Upload => Box::pin(files::pick_upload(&opener, exts)),
|
||||
disk::OpenRoute::Panel => {
|
||||
panel.panel.set(true);
|
||||
Box::pin(std::future::ready(None))
|
||||
}
|
||||
panel.panel.set(true);
|
||||
Box::pin(std::future::ready(None))
|
||||
})),
|
||||
// A design is saved to a file the user picks, or kept in the browser
|
||||
// where the browser can't pick one; everything else is downloaded.
|
||||
@@ -608,26 +780,27 @@ mod web {
|
||||
browser_files: Some(Box::new(move || shown.panel.set(true))),
|
||||
install_app: Some(crate::install::service(&cc.egui_ctx)),
|
||||
..Default::default()
|
||||
};
|
||||
}};
|
||||
// Thumbnails of saves and start-page cards.
|
||||
solidcraft_ui_egui::gpu::install_cpu_picture_renderer();
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
let mut app = SolveApp::new(Session::default(), services).with_control(receiver);
|
||||
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
|
||||
app.embedded = launch.embed;
|
||||
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
// eframe's web painter has no stencil buffer.
|
||||
app.set_wgpu(rs, DEPTH_BITS, 0, 1);
|
||||
}
|
||||
if query().contains("sample") {
|
||||
if launch.open.is_none() && crate::query_flag(&query(), "sample") {
|
||||
if let Err(e) = app.session.run_script(&solidcraft_engine::sample::script()) {
|
||||
app.set_status(format!("sample: {e}"), true);
|
||||
}
|
||||
app.session.doc_mut().name = "Sample Plate".into();
|
||||
app.session.mark_saved();
|
||||
}
|
||||
if !query().contains("sample") {
|
||||
if launch.open.is_none() && !crate::query_flag(&query(), "sample") {
|
||||
// The empty design takes Units of new designs (#1205). The sample
|
||||
// part is built in millimetres and is left that way.
|
||||
solidcraft_ui_egui::prefs::apply_new_design(&mut app);
|
||||
@@ -636,8 +809,13 @@ mod web {
|
||||
}
|
||||
let api = Api::install(sender, cc.egui_ctx.clone()).map_err(|e| format!("SolidCraft browser API: {e:?}"))?;
|
||||
let font_language = app.preferences.language.clone();
|
||||
let linked = crate::linked::Download::start(launch.open.as_deref(), &cc.egui_ctx);
|
||||
let full_url = window.location().href().ok().and_then(|value| url::Url::parse(&value).ok()).map(|url| launch.full_app_url(url));
|
||||
Ok(Box::new(Shell {
|
||||
app,
|
||||
launch,
|
||||
linked,
|
||||
full_url,
|
||||
api,
|
||||
files,
|
||||
autosaved_at: f64::NEG_INFINITY,
|
||||
@@ -650,6 +828,7 @@ mod web {
|
||||
cjk_bytes: None,
|
||||
font_language,
|
||||
space_mouse: SpaceMouse::default(),
|
||||
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
|
||||
}))
|
||||
}),
|
||||
)
|
||||
@@ -675,3 +854,101 @@ fn main() {
|
||||
fn main() {
|
||||
eprintln!("solidcraft-web only runs in the browser: build it with `trunk build --release` in apps/solidcraft-web");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn link_query_decodes_once_and_embed_disables_persistence() {
|
||||
assert!(!query_flag("?open=https://example.org/webgl?sample", "webgl"));
|
||||
assert!(!query_flag("?open=https://example.org/webgl?sample", "sample"));
|
||||
assert!(query_flag("?embed&webgl", "webgl"));
|
||||
let launch = Launch::parse("?embed&open=https%3A%2F%2Fexample.org%2Farm.solidcraft%3Fx%3Da%2520b%26y%3D1");
|
||||
assert_eq!(launch.open.as_deref(), Some("https://example.org/arm.solidcraft?x=a%20b&y=1"));
|
||||
assert!(launch.embed);
|
||||
assert!(!launch.persists());
|
||||
assert!(!Launch::parse("?embed").embed);
|
||||
assert!(!Launch::parse("?other=embed&reopen=https://example.org").embed);
|
||||
assert!(Launch::parse("?open=https://example.org/a").persists());
|
||||
assert_eq!(Launch::parse("?open=first&open=second").open.as_deref(), Some("first"));
|
||||
let launch = Launch::parse("?embed&open=/dutchman/jobs/abc/part.solidcraft%3Fx%3Da%2520b");
|
||||
assert!(launch.embed);
|
||||
assert!(!launch.persists());
|
||||
let page = url::Url::parse("http://solidcraft.io/?embed#view").expect("synthetic page");
|
||||
let full = url::Url::parse(&launch.full_app_url(page)).expect("full app URL");
|
||||
let reopened = Launch::parse(full.query().unwrap_or_default());
|
||||
assert_eq!(reopened.open, launch.open);
|
||||
assert_eq!(reopened.open.as_deref(), Some("/dutchman/jobs/abc/part.solidcraft?x=a%20b"));
|
||||
assert!(!reopened.embed);
|
||||
assert!(reopened.persists());
|
||||
assert!(full.fragment().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_links_refuse_hostile_schemes_credentials_and_sizes() {
|
||||
assert!(design_url("https://example.org/arm.solidcraft?download=1", "http://solidcraft.io").is_ok());
|
||||
for value in [
|
||||
"http://example.org/a",
|
||||
"data:application/json,{}",
|
||||
"blob:https://example.org/a",
|
||||
"javascript:alert(1)",
|
||||
"https://user:secret@example.org/a",
|
||||
"//evil.example/x",
|
||||
"/../x",
|
||||
"/%2e%2e/x",
|
||||
"/a/%2E./x",
|
||||
"/a%2f..%2fx",
|
||||
"/a\\x",
|
||||
"/a%5cx",
|
||||
"/a\nx",
|
||||
"/a%00x",
|
||||
"/a%7fx",
|
||||
"/a%C2%85x",
|
||||
"/a%3f/../x",
|
||||
"/a/..#view",
|
||||
"/a/..?download=1",
|
||||
"https://solidcraft.io/a/../x",
|
||||
"http://user:secret@solidcraft.io/x",
|
||||
"http://solidcraft.io.evil.example/x",
|
||||
"http://solidcraft.io:8080/x",
|
||||
"relative.solidcraft",
|
||||
"",
|
||||
] {
|
||||
assert!(design_url(value, "http://solidcraft.io").is_err(), "{value}");
|
||||
}
|
||||
assert!(design_url(&format!("https://example.org/{}", "x".repeat(16_384)), "http://solidcraft.io").is_err());
|
||||
assert!(design_url("http://redirect.example.org/a", "http://solidcraft.io").is_err());
|
||||
|
||||
let cap = solidcraft_engine::io::MAX_DESIGN_BYTES;
|
||||
assert!(check_download_size(0, cap as u64).is_ok());
|
||||
assert!(check_download_size(0, cap as u64 + 1).is_err());
|
||||
assert!(check_download_size(cap - 2, 2).is_ok());
|
||||
assert!(check_download_size(cap - 2, 3).is_err());
|
||||
assert!(check_download_size(0, u64::MAX).is_err());
|
||||
assert!(check_download_size(cap, 1).is_err());
|
||||
assert!(check_download_size(usize::MAX, 0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_links_allow_same_origin_paths_and_redirects() {
|
||||
for origin in ["http://solidcraft.io", "https://solidcraft.io", "http://solidcraft.io:8080"] {
|
||||
assert_eq!(design_url("/a/b.solidcraft", origin).map(|url| url.to_string()), Ok(format!("{origin}/a/b.solidcraft")));
|
||||
assert!(design_url(&format!("{origin}/x"), origin).is_ok());
|
||||
// Final response URLs follow the same origin and scheme rules as the request.
|
||||
assert!(design_url("https://docs.solidcraft.io/x", origin).is_ok());
|
||||
assert!(design_url("http://other/x", origin).is_err());
|
||||
}
|
||||
assert!(design_url("http://solidcraft.io/x", "https://solidcraft.io").is_err());
|
||||
assert!(design_url("http://SOLIDCRAFT.io:80/x", "http://solidcraft.io").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_ctrl_tab_is_left_to_the_browser() {
|
||||
// Ctrl+Tab and Ctrl+Shift+Tab (the event's key is "Tab" either way) switch browser tabs.
|
||||
assert!(browser_switches_tabs("Tab", true));
|
||||
// Tab and Shift+Tab move between the app's fields; Ctrl with other keys stays the app's.
|
||||
assert!(!browser_switches_tabs("Tab", false));
|
||||
assert!(!browser_switches_tabs("s", true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,11 +246,11 @@ mod tests {
|
||||
let axis =
|
||||
|source, rest, full| solidcraft_ui_egui::space_mouse::AxisCalibration { source, reversed: false, rest, dead_zone: 0, full: [full; 2] };
|
||||
// A short-range device: 15 of its 50 is under the raw dead zone, but 105 of 350 calibrated.
|
||||
let short = Calibration { dead_zone_share: 0.0, axes: std::array::from_fn(|r| axis(r, 0, 50)) };
|
||||
let short = Calibration { dead_zone_share: 0.0, kind: Default::default(), axes: std::array::from_fn(|r| axis(r, 0, 50)) };
|
||||
let push = Axes { translation: [15, 0, 0], rotation: [0; 3] };
|
||||
assert!(!push.moving() && moving(push, Some(short)));
|
||||
// A device resting at 100 on every axis: past the raw dead zone, but at rest calibrated.
|
||||
let offset = Calibration { dead_zone_share: 0.0, axes: std::array::from_fn(|r| axis(r, 100, 350)) };
|
||||
let offset = Calibration { dead_zone_share: 0.0, kind: Default::default(), axes: std::array::from_fn(|r| axis(r, 100, 350)) };
|
||||
let resting = Axes { translation: [100; 3], rotation: [100; 3] };
|
||||
assert!(resting.moving() && !moving(resting, Some(offset)));
|
||||
// Without a calibration, as read.
|
||||
|
||||
@@ -7,9 +7,14 @@ license.workspace = true
|
||||
publish = false
|
||||
default-run = "solidcraft"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu", "bundled-render-library"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
|
||||
|
||||
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/open_documents.rs
|
||||
# (macOS file-open events), src/native_crash.rs (Windows exception filter) and src/native_frame.rs
|
||||
# (Windows resize border) can allow it: none has a safe API. Keep the rest in sync with [workspace.lints].
|
||||
# (Windows resize border, taskbar icon) can allow it: none has a safe API. Keep the rest in sync with [workspace.lints].
|
||||
[lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
@@ -49,7 +54,8 @@ hidapi = { version = "2.6.7", default-features = false, features = ["windows-nat
|
||||
# The main window's handle for src/native_frame.rs (already in the tree through eframe and winit).
|
||||
raw-window-handle = "0.6"
|
||||
# Native crash capture (src/native_crash.rs): the exception filter, the faulting module, a minidump.
|
||||
# The window's resize border and DPI size (src/native_frame.rs): the window procedure, hit test, DPI.
|
||||
# The window's resize border, DPI size and class icon (src/native_frame.rs): the window procedure, hit
|
||||
# test, DPI, the exe's icon.
|
||||
# Finding 3D mice through Plug and Play without asking other devices anything (src/space_mouse/windows_hid.rs).
|
||||
windows-sys = { version = "0.61", features = [
|
||||
"Win32_Devices_DeviceAndDriverInstallation",
|
||||
@@ -70,15 +76,17 @@ windows-sys = { version = "0.61", features = [
|
||||
] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
muda = { version = "0.21.2", default-features = false }
|
||||
# The 3D mouse through the 3Dconnexion driver, which keeps the device to itself (src/space_mouse.rs).
|
||||
solidcraft-3dx-client = { workspace = true }
|
||||
# The 3D mouse (src/space_mouse.rs), in the shared-device open mode so that a running 3DxWare
|
||||
# driver does not shut the app out.
|
||||
hidapi = { version = "2.6.7", default-features = false, features = ["macos-shared-device"] }
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs); all already come with eframe and rfd.
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs), and the fullscreen title-bar
|
||||
# inset (src/main.rs); all already come with eframe and rfd.
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder"] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSString", "NSURL"] }
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSWindow"] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSProcessInfo", "NSString", "NSURL"] }
|
||||
block2 = "0.6"
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
||||
|
||||
@@ -11,15 +11,20 @@ use std::time::{Duration, Instant};
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::MAX_LINE;
|
||||
use solidcraft_engine::control_auth;
|
||||
use solidcraft_engine::control_pending;
|
||||
use solidcraft_ui_egui::ControlRequest;
|
||||
use solidcraft_ui_egui::control::{DESIGN_WATCH, DesignWatch, Subscriber};
|
||||
|
||||
/// Connections served at once, so a flood must not be able to spawn threads without end.
|
||||
const MAX_CONNECTIONS: usize = 16;
|
||||
/// Lines that are not JSON before the connection is closed, so garbage cannot loop forever.
|
||||
const MAX_BAD_LINES: usize = 8;
|
||||
/// How long a subscribed connection waits after a `design.changed` line before the next: what
|
||||
/// changes meanwhile (a parameter dragged) goes out as one line, the latest (#6985).
|
||||
const EVENT_GAP: Duration = Duration::from_millis(100);
|
||||
|
||||
static OPEN: AtomicUsize = AtomicUsize::new(0);
|
||||
/// The token files this launch wrote (its `--control` port, the agent chat's endpoint), removed
|
||||
/// The token files this launch wrote (its control port, the agent chat's endpoint), removed
|
||||
/// when the app ends.
|
||||
static TOKEN_FILES: std::sync::Mutex<Vec<std::path::PathBuf>> = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
@@ -34,17 +39,45 @@ fn new_token() -> Option<String> {
|
||||
pub fn remove_token_file() {
|
||||
if let Ok(files) = TOKEN_FILES.lock() {
|
||||
for p in files.iter() {
|
||||
let _ = std::fs::remove_file(p);
|
||||
control_auth::remove_token_file(p);
|
||||
}
|
||||
}
|
||||
if let Some(port) = APP_PORT.lock().ok().and_then(|p| *p) {
|
||||
control_auth::remove_current_port(port);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
/// The port the app's own channel listens on, for removing the discovery file at exit.
|
||||
static APP_PORT: std::sync::Mutex<Option<u16>> = std::sync::Mutex::new(None);
|
||||
|
||||
pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
// Every connection must show the token before anything runs (docs/control-protocol.md).
|
||||
match control_auth::env_token().or_else(new_token) {
|
||||
Some(token) => {
|
||||
listen(port, token, tx, ctx, false);
|
||||
let listener = match listen_on {
|
||||
control_auth::Listen::Exact(port) => TcpListener::bind(("127.0.0.1", port)),
|
||||
control_auth::Listen::Default => control_auth::bind_with_fallback(control_auth::DEFAULT_PORT, control_auth::PORT_TRIES),
|
||||
};
|
||||
match listener {
|
||||
Ok(l) => {
|
||||
if let Some(port) = listen_on_listener(l, token, tx, ctx, false) {
|
||||
if matches!(listen_on, control_auth::Listen::Default) {
|
||||
eprintln!("solidcraft: control channel on the default port 127.0.0.1:{port} (Preferences > Assistant turns it off)");
|
||||
}
|
||||
// The discovery file: where `solidcraft-cli mcp` finds this app with no arguments.
|
||||
match control_auth::write_current_port(port) {
|
||||
Ok(_) => {
|
||||
if let Ok(mut p) = APP_PORT.lock() {
|
||||
*p = Some(port);
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("solidcraft: could not write the control discovery file ({e})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("solidcraft: control server failed to bind 127.0.0.1: {e}"),
|
||||
}
|
||||
}
|
||||
None => eprintln!("solidcraft: control server not started: no random token available"),
|
||||
}
|
||||
@@ -60,20 +93,20 @@ pub fn listen_agent(tx: Sender<ControlRequest>, ctx: egui::Context) -> Option<u1
|
||||
eprintln!("solidcraft: agent chat endpoint not started: no random token available");
|
||||
return None;
|
||||
};
|
||||
listen(0, token, tx, ctx, true)
|
||||
}
|
||||
|
||||
/// Listen on `port` (0: a free port the system picks) with `token`; the port it listens on.
|
||||
/// `patient`: every request may wait as long as a batch (a person decides on it).
|
||||
fn listen(port: u16, token: String, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) -> Option<u16> {
|
||||
let listener = match TcpListener::bind(("127.0.0.1", port)) {
|
||||
let listener = match TcpListener::bind(("127.0.0.1", 0)) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("solidcraft: control server failed to bind 127.0.0.1:{port}: {e}");
|
||||
eprintln!("solidcraft: control server failed to bind 127.0.0.1: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let port = listener.local_addr().map(|a| a.port()).unwrap_or(port);
|
||||
listen_on_listener(listener, token, tx, ctx, true)
|
||||
}
|
||||
|
||||
/// Serve `listener` (loopback) with `token`; the port it listens on.
|
||||
/// `patient`: every request may wait as long as a batch (a person decides on it).
|
||||
fn listen_on_listener(listener: TcpListener, token: String, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) -> Option<u16> {
|
||||
let port = listener.local_addr().ok()?.port();
|
||||
match control_auth::token_path(port) {
|
||||
Some(path) => match control_auth::write_token_file(&path, &token) {
|
||||
Ok(()) => {
|
||||
@@ -98,7 +131,7 @@ fn listen(port: u16, token: String, tx: Sender<ControlRequest>, ctx: egui::Conte
|
||||
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
|
||||
OPEN.fetch_sub(1, Ordering::Relaxed);
|
||||
let mut refused = stream;
|
||||
let _ = writeln!(refused, "{}", json!({"ok": false, "error": "too many connections"}));
|
||||
let _ = send(&mut refused, &json!({"ok": false, "error": "too many connections"}));
|
||||
continue;
|
||||
}
|
||||
let tx = tx.clone();
|
||||
@@ -145,6 +178,15 @@ fn read_line(r: &mut impl BufRead, max: usize) -> std::io::Result<Option<Vec<u8>
|
||||
}
|
||||
}
|
||||
|
||||
/// A reply line, written to the socket at once. `writeln!` with a JSON value hands the socket each
|
||||
/// of its pieces apart, a system call for every string, number and comma: half a second for a
|
||||
/// design's component tree when it has thousands of components.
|
||||
fn send(out: &mut impl Write, reply: &Value) -> std::io::Result<()> {
|
||||
let mut line = serde_json::to_vec(reply).map_err(std::io::Error::other)?;
|
||||
line.push(b'\n');
|
||||
out.write_all(&line)
|
||||
}
|
||||
|
||||
/// The reply to a connection that didn't show the token; the connection is then closed.
|
||||
fn unauthorized() -> Value {
|
||||
json!({"ok": false, "error": "unauthorized: send the control token as \"token\" (see docs/control-protocol.md)"})
|
||||
@@ -155,7 +197,7 @@ fn unauthorized() -> Value {
|
||||
/// the connection, and a reset can reach the client before it has read the refusal.
|
||||
fn refuse(out: &mut TcpStream, reader: &mut BufReader<TcpStream>) {
|
||||
use std::io::Read;
|
||||
let _ = writeln!(out, "{}", unauthorized());
|
||||
let _ = send(out, &unauthorized());
|
||||
let _ = out.shutdown(std::net::Shutdown::Write);
|
||||
let _ = reader.get_ref().set_read_timeout(Some(Duration::from_millis(500)));
|
||||
let _ = std::io::copy(&mut reader.by_ref().take(64 * 1024), &mut std::io::sink());
|
||||
@@ -176,6 +218,8 @@ const WAITS: Waits = Waits { short: Duration::from_secs(60), long: Duration::fro
|
||||
struct Pending {
|
||||
id: Value,
|
||||
method: String,
|
||||
/// The request in messages ("`engine.execute` (solid.fillet)").
|
||||
what: String,
|
||||
reply: Receiver<Value>,
|
||||
}
|
||||
|
||||
@@ -296,10 +340,120 @@ fn watch(reply: Receiver<Value>, ticket: Ticket) -> Receiver<Value> {
|
||||
late_rx
|
||||
}
|
||||
|
||||
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) {
|
||||
serve_with(stream, token, tx, ctx, patient, WAITS, &RUNNING);
|
||||
/// The reply text for a request that outlasts its wait. A request that only reads changes
|
||||
/// nothing when it finishes; an export or a save still writes its file, and any other request
|
||||
/// still applies, as it was not cancelled.
|
||||
fn timeout_message(what: &str, effect: control_pending::Effect) -> String {
|
||||
match effect {
|
||||
control_pending::Effect::ReadsOnly => {
|
||||
format!("timeout: {what} is still running in the app ({}); ask `control.pending` for its answer", effect.when_done())
|
||||
}
|
||||
control_pending::Effect::WritesFile => format!(
|
||||
"timeout: {what} is still running in the app and will still write the file when it finishes (it was not cancelled: \
|
||||
don't send it again; the design does not change); ask `control.pending` for its result"
|
||||
),
|
||||
control_pending::Effect::Changes => format!(
|
||||
"timeout: {what} is still running in the app and will still apply when it finishes (it was not cancelled: don't send it \
|
||||
again); ask `control.pending` for its result"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// The reply text for a request refused because an earlier one on the connection still runs.
|
||||
fn busy_message(p: &Pending, method: &str) -> String {
|
||||
let method: String = method.chars().take(128).collect();
|
||||
format!(
|
||||
"busy: request {} ({}) is still running, so `{method}` was not run; ask `control.pending` for that request's result, then \
|
||||
send this one again",
|
||||
p.id, p.what
|
||||
)
|
||||
}
|
||||
|
||||
/// A subscribed connection's socket, for its replies and its events: each line is written whole
|
||||
/// and alone, so an event never lands inside a reply (#6985).
|
||||
struct Lines {
|
||||
socket: TcpStream,
|
||||
turn: Mutex<()>,
|
||||
}
|
||||
|
||||
impl Lines {
|
||||
fn send(&self, line: &Value) -> std::io::Result<()> {
|
||||
let _turn = self.turn.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
send(&mut &self.socket, line)
|
||||
}
|
||||
}
|
||||
|
||||
/// A connection's `control.subscribe` (#6985): its `design.changed` lines, sent from a thread of
|
||||
/// their own until the connection ends.
|
||||
struct Subscription {
|
||||
lines: Arc<Lines>,
|
||||
stop: Arc<AtomicBool>,
|
||||
_counted: Subscriber,
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
/// Write `reply`, the window's answer to `control.subscribe` naming the design `seen`, on
|
||||
/// `out`, then send `design`'s changes after it: the reply goes out before any event. `None`,
|
||||
/// with nothing written, when no thread or socket was left for the events.
|
||||
fn start(
|
||||
design: &'static DesignWatch,
|
||||
counted: Subscriber,
|
||||
out: &TcpStream,
|
||||
seen: (String, u64),
|
||||
reply: &Value,
|
||||
) -> Option<(Self, std::io::Result<()>)> {
|
||||
let lines = Arc::new(Lines { socket: out.try_clone().ok()?, turn: Mutex::new(()) });
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let first = lines.turn.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
let (l, s) = (lines.clone(), stop.clone());
|
||||
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen)).ok()?;
|
||||
let sent = send(&mut &lines.socket, reply);
|
||||
drop(first);
|
||||
Some((Subscription { lines, stop, _counted: counted }, sent))
|
||||
}
|
||||
}
|
||||
|
||||
/// The design a `control.subscribe` reply names.
|
||||
fn design_of(reply: &Value) -> Option<(String, u64)> {
|
||||
let result = reply.get("result")?;
|
||||
Some((result.get("document_id")?.as_str()?.to_string(), result.get("revision")?.as_u64()?))
|
||||
}
|
||||
|
||||
impl Drop for Subscription {
|
||||
fn drop(&mut self) {
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
// The socket closes now, not when the thread next looks; a line blocked on a client that
|
||||
// stopped reading ends too.
|
||||
let _ = self.lines.socket.shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
}
|
||||
|
||||
/// The `design.changed` lines of a subscribed connection, each time the design differs from the
|
||||
/// last one it was told of (`seen`), until `stop`.
|
||||
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64)) {
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
let Some(now) = design.wait(&seen, stop, Duration::from_secs(1)).filter(|now| *now != seen) else { continue };
|
||||
if lines.send(&json!({"event": "design.changed", "params": {"document_id": now.0, "revision": now.1}})).is_err() {
|
||||
break;
|
||||
}
|
||||
seen = now;
|
||||
std::thread::sleep(EVENT_GAP);
|
||||
}
|
||||
}
|
||||
|
||||
/// Write `reply` on the connection: through its subscription once it has one.
|
||||
fn answer(out: &mut TcpStream, events: Option<&Subscription>, reply: &Value) -> std::io::Result<()> {
|
||||
match events {
|
||||
Some(e) => e.lines.send(reply),
|
||||
None => send(out, reply),
|
||||
}
|
||||
}
|
||||
|
||||
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) {
|
||||
serve_with(stream, token, tx, ctx, patient, WAITS, &RUNNING, &DESIGN_WATCH);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn serve_with(
|
||||
stream: TcpStream,
|
||||
token: &str,
|
||||
@@ -308,6 +462,7 @@ fn serve_with(
|
||||
patient: bool,
|
||||
waits: Waits,
|
||||
running: &'static Running,
|
||||
design: &'static DesignWatch,
|
||||
) {
|
||||
let connection = running.connections.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
let Ok(read) = stream.try_clone() else { return };
|
||||
@@ -316,12 +471,15 @@ fn serve_with(
|
||||
let mut bad = 0usize;
|
||||
let mut authenticated = false;
|
||||
let mut pending: Option<Pending> = None;
|
||||
let mut events: Option<Subscription> = None;
|
||||
loop {
|
||||
// A first `control.subscribe` on its way to the window (#6985).
|
||||
let mut joining = None;
|
||||
let line = match read_line(&mut reader, MAX_LINE) {
|
||||
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
let _ = writeln!(out, "{}", json!({"ok": false, "error": e.to_string()}));
|
||||
let _ = answer(&mut out, events.as_ref(), &json!({"ok": false, "error": e.to_string()}));
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -364,7 +522,15 @@ fn serve_with(
|
||||
}
|
||||
} else if let Some(p) = pending.as_ref().filter(|_| late.is_none()) {
|
||||
// Nothing more is queued behind it, to run unseen when it ends.
|
||||
json!({"ok": false, "busy": true, "error": format!("busy: request {} (`{}`) is still running; ask `control.pending` for its result", p.id, p.method)})
|
||||
json!({"ok": false, "busy": true, "error": busy_message(p, &method)})
|
||||
} else if method == "control.subscribe" && events.is_some() {
|
||||
if late.is_some() {
|
||||
// As below: finished, and the client moved on without asking.
|
||||
pending = None;
|
||||
}
|
||||
// Its events already say how the design moves; a second answer could only
|
||||
// arrive behind a newer one.
|
||||
json!({"ok": false, "error": "already subscribed: this connection is sent design.changed lines; ask document.revision for the design now"})
|
||||
} else {
|
||||
if late.is_some() {
|
||||
// Finished, and the client moved on without asking: it is dropped.
|
||||
@@ -372,6 +538,11 @@ fn serve_with(
|
||||
}
|
||||
// A batch replaces many short requests: the whole of it may take long (#743).
|
||||
let wait = if patient || method == "engine.batch" { waits.long } else { waits.short };
|
||||
let what = control_pending::describe(&method, ¶ms);
|
||||
let effect = control_pending::effect(&method, ¶ms);
|
||||
// Counted before the window answers, so it publishes every change from its
|
||||
// answer on.
|
||||
joining = (method == "control.subscribe").then(|| design.join());
|
||||
let (req, rrx) = ControlRequest::new(method.clone(), params);
|
||||
let ticket = running.add(connection, &id, &req);
|
||||
if tx.send(req).is_err() {
|
||||
@@ -381,8 +552,8 @@ fn serve_with(
|
||||
match rrx.recv_timeout(wait) {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
let r = json!({"ok": false, "running": true, "error": format!("timeout: `{method}` is still running in the app and will still apply; ask `control.pending` for its result")});
|
||||
pending = Some(Pending { id: id.clone(), method, reply: watch(rrx, ticket) });
|
||||
let r = json!({"ok": false, "running": true, "error": timeout_message(&what, effect)});
|
||||
pending = Some(Pending { id: id.clone(), method, what, reply: watch(rrx, ticket) });
|
||||
r
|
||||
}
|
||||
}
|
||||
@@ -401,7 +572,20 @@ fn serve_with(
|
||||
json!({"ok": false, "error": format!("bad JSON: {e}")})
|
||||
}
|
||||
};
|
||||
if writeln!(out, "{reply}").is_err() {
|
||||
// Subscribed once the window named the design (not when it refused or timed out).
|
||||
let sent = match joining.and_then(|counted| design_of(&reply).map(|seen| (counted, seen))) {
|
||||
Some((counted, seen)) => match Subscription::start(design, counted, &out, seen, &reply) {
|
||||
Some((subscription, sent)) => {
|
||||
events = Some(subscription);
|
||||
sent
|
||||
}
|
||||
None => {
|
||||
send(&mut out, &json!({"id": reply.get("id"), "ok": false, "error": "not subscribed: no thread or socket left for the events"}))
|
||||
}
|
||||
},
|
||||
None => answer(&mut out, events.as_ref(), &reply),
|
||||
};
|
||||
if sent.is_err() {
|
||||
break;
|
||||
}
|
||||
if bad >= MAX_BAD_LINES {
|
||||
@@ -441,7 +625,7 @@ mod tests {
|
||||
static RUNNING: Running = Running::new();
|
||||
std::thread::spawn(move || {
|
||||
if let Some(stream) = listener.incoming().flatten().next() {
|
||||
serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING);
|
||||
serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING, &DESIGN_WATCH);
|
||||
}
|
||||
});
|
||||
let s = TcpStream::connect(addr).unwrap();
|
||||
@@ -507,7 +691,7 @@ mod tests {
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let tx = tx.clone();
|
||||
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING));
|
||||
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING, &DESIGN_WATCH));
|
||||
}
|
||||
});
|
||||
let open = || {
|
||||
@@ -577,6 +761,114 @@ mod tests {
|
||||
assert_eq!((&r["result"]["pending"], &r["result"]["done"]), (&json!(7), &json!(true)), "{r}");
|
||||
}
|
||||
|
||||
/// #6985: a connection that sends `control.subscribe` gets the design in the reply, then a
|
||||
/// `design.changed` line each time the window publishes another: never before the reply,
|
||||
/// none for the same design, the latest after a burst, each line whole between its replies.
|
||||
/// A second subscribe is refused without reaching the window. A connection that did not
|
||||
/// subscribe gets no events, and one that closed is no longer counted.
|
||||
#[test]
|
||||
fn subscribed_connections_are_told_of_design_changes() {
|
||||
static RUNNING: Running = Running::new();
|
||||
static WATCH: DesignWatch = DesignWatch::new();
|
||||
let token = "0123456789abcdef0123456789abcdef";
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
// The window: it answers `control.subscribe` as the app does, publishing the design first;
|
||||
// then the design changes at once, before the reply may have been written.
|
||||
std::thread::spawn(move || {
|
||||
for req in rx {
|
||||
if req.method == "control.subscribe" {
|
||||
WATCH.publish("d-1", 3);
|
||||
let _ = req.reply.send(json!({"ok": true, "result": {"document_id": "d-1", "revision": 3}}));
|
||||
WATCH.publish("d-1", 4);
|
||||
} else {
|
||||
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
|
||||
}
|
||||
}
|
||||
});
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let tx = tx.clone();
|
||||
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, WAITS, &RUNNING, &WATCH));
|
||||
}
|
||||
});
|
||||
let open = || {
|
||||
let s = TcpStream::connect(addr).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
|
||||
(s.try_clone().unwrap(), BufReader::new(s))
|
||||
};
|
||||
let next = |lines: &mut BufReader<TcpStream>| -> Value {
|
||||
let mut l = String::new();
|
||||
lines.read_line(&mut l).unwrap();
|
||||
serde_json::from_str(&l).unwrap()
|
||||
};
|
||||
let changed = |id: &str, revision: u64| json!({"event": "design.changed", "params": {"document_id": id, "revision": revision}});
|
||||
|
||||
let (mut quiet_out, mut quiet) = open();
|
||||
writeln!(quiet_out, "{}", json!({"id": 1, "method": "ui.view", "token": token})).unwrap();
|
||||
assert_eq!(next(&mut quiet)["id"], 1);
|
||||
|
||||
let (mut out, mut lines) = open();
|
||||
writeln!(out, "{}", json!({"id": 1, "method": "control.subscribe", "token": token})).unwrap();
|
||||
assert_eq!(next(&mut lines), json!({"id": 1, "ok": true, "result": {"document_id": "d-1", "revision": 3}}));
|
||||
assert_eq!(next(&mut lines), changed("d-1", 4), "the change after the window's answer comes after the reply");
|
||||
assert!(WATCH.subscribed());
|
||||
// The design it was told of is no news; another revision is.
|
||||
WATCH.publish("d-1", 4);
|
||||
WATCH.publish("d-1", 5);
|
||||
assert_eq!(next(&mut lines), changed("d-1", 5));
|
||||
// A burst ends with its latest.
|
||||
for revision in 6..=10 {
|
||||
WATCH.publish("d-1", revision);
|
||||
}
|
||||
let mut last = 5;
|
||||
while last < 10 {
|
||||
let e = next(&mut lines);
|
||||
let revision = e["params"]["revision"].as_u64().unwrap();
|
||||
assert!(e["event"] == "design.changed" && revision > last, "{e} after {last}");
|
||||
last = revision;
|
||||
}
|
||||
// Another tab: another id, whatever its revision.
|
||||
WATCH.publish("d-2", 2);
|
||||
assert_eq!(next(&mut lines), changed("d-2", 2));
|
||||
// Subscribed already: refused here (the window would have published d-1 again).
|
||||
writeln!(out, "{}", json!({"id": 2, "method": "control.subscribe"})).unwrap();
|
||||
let r = next(&mut lines);
|
||||
assert!(r["id"] == 2 && r["ok"] == false && r["error"].as_str().is_some_and(|e| e.starts_with("already subscribed")), "{r}");
|
||||
// Replies still come, whole, and nothing else was sent meanwhile.
|
||||
writeln!(out, "{}", json!({"id": 3, "method": "ui.view"})).unwrap();
|
||||
assert_eq!(next(&mut lines), json!({"id": 3, "ok": true, "result": "ui.view"}));
|
||||
writeln!(quiet_out, "{}", json!({"id": 2, "method": "ui.view"})).unwrap();
|
||||
assert_eq!(next(&mut quiet), json!({"id": 2, "ok": true, "result": "ui.view"}), "no events without subscribing");
|
||||
// Closed, it is no longer counted.
|
||||
drop((out, lines));
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while WATCH.subscribed() && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert!(!WATCH.subscribed());
|
||||
}
|
||||
|
||||
/// A timed-out request that only reads is not said to "still apply", nor is one that only
|
||||
/// writes a file (an export, #1989); a change is, by name.
|
||||
#[test]
|
||||
fn timeout_text_tells_reads_from_changes() {
|
||||
let says = |method: &str, p: Value| timeout_message(&control_pending::describe(method, &p), control_pending::effect(method, &p));
|
||||
let read = says("ui.inspect", json!({}));
|
||||
assert!(read.starts_with("timeout") && read.contains("only reads") && !read.contains("will still apply"), "{read}");
|
||||
let read = says("engine.execute", json!({"command": "sketch.inspect"}));
|
||||
assert!(read.contains("only reads") && read.contains("sketch.inspect"), "{read}");
|
||||
let change = says("engine.execute", json!({"command": "solid.fillet"}));
|
||||
assert!(change.starts_with("timeout") && change.contains("will still apply") && change.contains("solid.fillet"), "{change}");
|
||||
let export = says("engine.execute", json!({"command": "file.export", "params": {"path": "/tmp/wall.step"}}));
|
||||
assert!(export.contains("will still write the file") && !export.contains("apply") && export.contains("file.export"), "{export}");
|
||||
let save = says("engine.execute", json!({"command": "file.save"}));
|
||||
assert!(save.contains("will still write the file") && !save.contains("apply"), "{save}");
|
||||
let render = says("ui.render", json!({"path": "/tmp/a.png"}));
|
||||
assert!(render.contains("will still write the file"), "{render}");
|
||||
}
|
||||
|
||||
/// #205: a connection runs nothing until a request carries the launch's token; a wrong or
|
||||
/// missing token, or garbage, is refused and the connection closed. Once shown, the token
|
||||
/// need not be repeated on that connection.
|
||||
@@ -669,4 +961,31 @@ mod tests {
|
||||
let mut r = BufReader::with_capacity(64, text.as_slice());
|
||||
assert!(read_line(&mut r, 1000).is_err());
|
||||
}
|
||||
|
||||
/// A large reply reaches the socket in one write, not one per JSON piece (a component tree of
|
||||
/// thousands of components took half a second to send), and reads back as it was.
|
||||
#[test]
|
||||
fn a_large_reply_is_written_at_once() {
|
||||
struct Counted {
|
||||
writes: usize,
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
impl Write for Counted {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.writes += 1;
|
||||
self.bytes.extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
let components: Vec<Value> = (0..2000).map(|i| json!({"id": i, "name": format!("Part {i}"), "bodies": ["a", "b"], "parent": 0})).collect();
|
||||
let reply = json!({"ok": true, "id": 7, "result": {"components": components}});
|
||||
let mut out = Counted { writes: 0, bytes: Vec::new() };
|
||||
send(&mut out, &reply).unwrap();
|
||||
assert_eq!(out.writes, 1);
|
||||
assert_eq!(out.bytes.last(), Some(&b'\n'));
|
||||
assert_eq!(serde_json::from_slice::<Value>(&out.bytes).unwrap(), reply);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//! The graphics device was lost after the window opened: the driver was reset (on Windows a TDR,
|
||||
//! "Display driver stopped responding and has recovered") or the GPU went away (#4172). A lost
|
||||
//! device can't draw again, so the window can't show anything more, not even a message. The
|
||||
//! painter stops drawing (`egui_wgpu::DeviceLost`); the app then closes, keeping unsaved changes
|
||||
//! 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.
|
||||
|
||||
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";
|
||||
|
||||
/// 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.
|
||||
const RECENT_SECS: u64 = 600;
|
||||
|
||||
/// Seconds since 1970.
|
||||
pub fn now_secs() -> u64 {
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs())
|
||||
}
|
||||
|
||||
/// Whether `stamp` ([`LOST_ENV`]) names a loss at most [`RECENT_SECS`] before `now`.
|
||||
pub fn recent(stamp: Option<&str>, now: u64) -> bool {
|
||||
stamp.and_then(|s| s.trim().parse::<u64>().ok()).is_some_and(|at| at <= now && now - at <= RECENT_SECS)
|
||||
}
|
||||
|
||||
/// Whether the start after a lost device uses the software renderer: when this launch already
|
||||
/// followed a recent loss (the driver keeps losing it), or ran on it anyway.
|
||||
pub fn restart_on_software(after_recent_loss: bool, cpu_renderer: bool) -> bool {
|
||||
after_recent_loss || cpu_renderer
|
||||
}
|
||||
|
||||
/// What the start after a lost device tells the user, `software` when it runs on the software
|
||||
/// renderer.
|
||||
pub fn note(software: bool) -> 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.");
|
||||
}
|
||||
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 };
|
||||
let mut command = std::process::Command::new(exe);
|
||||
command.args(std::env::args_os().skip(1)).env(LOST_ENV, lost_at.to_string());
|
||||
if software {
|
||||
command.env(startup::RENDERER_ENV, "cpu");
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
// Not tied to this process: its own process group, no console.
|
||||
command.creation_flags(0x0000_0200 | 0x0000_0008);
|
||||
}
|
||||
if let Err(e) = command.spawn() {
|
||||
log::error!("graphics: starting again after the device was lost: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// Only a recent, well-formed stamp counts: a later start that inherited it says nothing.
|
||||
#[test]
|
||||
fn only_a_recent_loss_counts() {
|
||||
let now = 1_000_000;
|
||||
assert!(super::recent(Some("999990"), now));
|
||||
assert!(super::recent(Some(" 999400 "), now));
|
||||
assert!(!super::recent(Some("999399"), now), "older than ten minutes");
|
||||
assert!(!super::recent(Some("1000001"), now), "from the future");
|
||||
assert!(!super::recent(Some("soon"), now));
|
||||
assert!(!super::recent(None, now));
|
||||
}
|
||||
|
||||
/// The first loss starts again on the GPU; a second one soon after, on the software renderer.
|
||||
#[test]
|
||||
fn a_second_loss_in_a_row_starts_on_the_software_renderer() {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,10 @@
|
||||
//! A `.step`/`.stp` file opens as a new design holding the file's bodies (an Import feature); a
|
||||
//! `.3mf`/`.stl`/`.obj`/`.glb`/`.gltf` file as a new design holding mesh bodies.
|
||||
//!
|
||||
//! `SOLIDCRAFT_SETTINGS_DIR` selects an explicit absolute settings directory without legacy adoption.
|
||||
//! `--control <port>` (or `SOLIDCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control
|
||||
//! server: `{"id":1,"method":"engine.execute","params":{"command":"solid.box","params":{…}}}`.
|
||||
//! Without them the channel still opens, on the default port, unless Preferences turns it off.
|
||||
//! See `solidcraft_ui_egui::control` and docs/control-protocol.md.
|
||||
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
@@ -19,11 +21,15 @@
|
||||
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
|
||||
mod app_icon;
|
||||
mod control_server;
|
||||
mod device_lost;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod native_crash;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod native_frame;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod native_menu;
|
||||
mod nav_bridge;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod open_documents;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
mod space_mouse;
|
||||
@@ -76,8 +82,14 @@ struct App {
|
||||
frames: u32,
|
||||
/// Started over the control channel: the window doesn't ask for the foreground.
|
||||
take_focus_unwanted: bool,
|
||||
/// The graphics device: lost, it can't draw again (`App::watch_device`, #4172).
|
||||
gpu: Option<LostWatch>,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
space_mouse: space_mouse::SpaceMouse,
|
||||
#[cfg(target_os = "macos")]
|
||||
native_menu: Option<native_menu::NativeMenu>,
|
||||
/// A device's own app moving the view (Preferences › Navigation, off by default).
|
||||
nav_bridge: nav_bridge::NavBridge,
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
@@ -102,11 +114,19 @@ impl eframe::App for App {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
|
||||
}
|
||||
solidcraft_engine::crash::phase(solidcraft_engine::crash::RUNNING);
|
||||
// A DMG install has no installer step: put solidcraft-cli on PATH, quietly.
|
||||
#[cfg(target_os = "macos")]
|
||||
std::thread::spawn(solidcraft_ui_egui::cli_link::install_on_launch);
|
||||
// Automation runs (a control port was asked for) leave the user's AI apps alone.
|
||||
if !self.take_focus_unwanted {
|
||||
solidcraft_ui_egui::ai_connect::begin(&mut self.solve);
|
||||
}
|
||||
if let Some(dir) = self.starting.take() {
|
||||
startup::started(&dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.watch_device();
|
||||
// The design on screen is the one whose commands a crash report replays.
|
||||
self.solve.session.crash_journal = solidcraft_engine::crash::enabled();
|
||||
// The OS closing the window (Alt+F4, Cmd+Q, the window manager): ask about unsaved
|
||||
@@ -119,23 +139,38 @@ impl eframe::App for App {
|
||||
for f in if self.solve.file_picker_busy() { Vec::new() } else { open_documents::take() } {
|
||||
self.solve.open_path_async(&f);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(menu) = &mut self.native_menu {
|
||||
menu.tick(&mut self.solve, ctx);
|
||||
}
|
||||
self.solve.logic(ctx);
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
if !self.solve.file_picker_busy() {
|
||||
self.space_mouse.tick(&mut self.solve, ctx);
|
||||
}
|
||||
// Every frame, so switching the bridge off reaches the reader while a picker is open too.
|
||||
self.nav_bridge.tick(&mut self.solve, ctx);
|
||||
if self.solve.quit_requested {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
}
|
||||
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(menu) = &mut self.native_menu {
|
||||
menu.raw_input_hook(&mut self.solve, ctx, raw);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_fullscreen_content(ctx, raw);
|
||||
// Before the app's own hook, which adds the control channel's events.
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
self.space_mouse.raw_input_hook(self.solve.preferences.space_mouse_enabled, raw);
|
||||
// The 3D mouse's buttons, before the app reads the frame: Esc and held modifiers join its input.
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
solidcraft_ui_egui::space_mouse::buttons_hook(&mut self.solve, ctx, self.space_mouse.buttons(), raw);
|
||||
self.solve.raw_input_hook(raw);
|
||||
}
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.solve.ui(ui);
|
||||
content_ui(&mut self.solve, ui);
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, self.solve.prefs());
|
||||
@@ -145,8 +180,21 @@ impl eframe::App for App {
|
||||
solidcraft_ui_egui::agent_chat::shutdown(&mut self.solve);
|
||||
// A save still being written ends before the app does.
|
||||
solidcraft_ui_egui::save_job::tick(&mut self.solve, true);
|
||||
// Pictures still being drawn off the window thread (Home cards, file previews) end
|
||||
// before the GPU is torn down: one drawn while the process exits can crash the driver.
|
||||
if !solidcraft_engine::capture::close(std::time::Duration::from_secs(5)) {
|
||||
log::warn!("a picture was still being drawn as the app closed");
|
||||
}
|
||||
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
|
||||
solidcraft_ui_egui::open_elsewhere::close(&mut self.solve);
|
||||
// Before a restart below starts the next instance, which would otherwise get nothing from
|
||||
// the 3D mouse driver (`solidcraft_3dx_client::stop`).
|
||||
#[cfg(target_os = "macos")]
|
||||
solidcraft_3dx_client::stop();
|
||||
solidcraft_engine::crash::end_session();
|
||||
if let Some(lost_at) = self.gpu.as_ref().and_then(|g| g.lost_at) {
|
||||
device_lost::restart(self.gpu.as_ref().is_some_and(|g| g.restart_on_software), lost_at);
|
||||
}
|
||||
// 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) {
|
||||
let mut command = std::process::Command::new(&program);
|
||||
@@ -166,6 +214,79 @@ impl eframe::App for App {
|
||||
}
|
||||
}
|
||||
|
||||
/// The window's graphics device, watched for its loss.
|
||||
struct LostWatch {
|
||||
lost: eframe::egui_wgpu::DeviceLost,
|
||||
/// Where the next start runs once this device is lost ([`device_lost::restart_on_software`]).
|
||||
restart_on_software: bool,
|
||||
/// When the loss was seen: the app is closing to start again.
|
||||
lost_at: Option<u64>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// The graphics device was lost (#4172): the window can't draw again, so close, keeping
|
||||
/// unsaved changes for recovery as a forced quit does (the window can't ask about them), and
|
||||
/// start again once closed ([`device_lost::restart`]).
|
||||
fn watch_device(&mut self) {
|
||||
let Some(gpu) = self.gpu.as_mut().filter(|g| g.lost_at.is_none() && g.lost.is_lost()) else { return };
|
||||
let reason = gpu.lost.reason().unwrap_or_default();
|
||||
log::error!("graphics: the device was lost ({reason}); closing to start again");
|
||||
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());
|
||||
self.solve.quit_requested = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Linux file dialogs match patterns by case, so each extension goes in as lower and upper
|
||||
/// case, plus FreeCAD's own mixed casing.
|
||||
fn case_variants(exts: &[&str]) -> Vec<String> {
|
||||
let mut all: Vec<String> = Vec::new();
|
||||
for e in exts {
|
||||
let mut forms = vec![e.to_ascii_lowercase(), e.to_ascii_uppercase()];
|
||||
if e.eq_ignore_ascii_case("fcstd") {
|
||||
forms.push("FCStd".into());
|
||||
}
|
||||
for v in forms {
|
||||
if !all.contains(&v) {
|
||||
all.push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
all
|
||||
}
|
||||
|
||||
/// egui's root Ui spans the whole viewport, including native safe areas. Lay out the app
|
||||
/// inside the unobscured rectangle so its application bar can receive native pointer input.
|
||||
fn content_ui(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let content = ui.ctx().content_rect();
|
||||
let above = egui::Rect::from_min_max(ui.max_rect().min, egui::pos2(ui.max_rect().right(), content.top()));
|
||||
ui.painter().rect_filled(above, 0.0, solidcraft_ui_egui::theme::Tokens::get().app_bar);
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(content), |ui| app.ui(ui));
|
||||
}
|
||||
|
||||
/// A transparent macOS title bar still intercepts clicks at the top in fullscreen (#3643).
|
||||
/// `contentLayoutRect` reports the whole content view once that bar auto-hides, so reserve
|
||||
/// the native titled-window height even then. AppKit supplies the height in logical points;
|
||||
/// egui's UI zoom is separate from the display's Retina scale.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn macos_fullscreen_content(ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
let Some(mtm) = objc2::MainThreadMarker::new() else { return };
|
||||
let frame = objc2_app_kit::NSWindow::frameRectForContentRect_styleMask(Default::default(), objc2_app_kit::NSWindowStyleMask::Titled, mtm);
|
||||
fullscreen_content_insets(raw, frame.size.height as f32, ctx.zoom_factor());
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
fn fullscreen_content_insets(raw: &mut egui::RawInput, title_height: f32, zoom: f32) {
|
||||
let fullscreen = raw.viewports.get(&raw.viewport_id).and_then(|v| v.fullscreen).unwrap_or(false);
|
||||
let mut insets = raw.safe_area_insets.unwrap_or_default();
|
||||
if fullscreen && title_height.is_finite() && title_height > 0.0 && zoom.is_finite() && zoom > 0.0 {
|
||||
insets.0.top = insets.0.top.max(title_height / zoom);
|
||||
}
|
||||
// Explicitly reset on leaving fullscreen: None would retain the preceding frame's inset.
|
||||
raw.safe_area_insets = Some(insets);
|
||||
}
|
||||
|
||||
fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
|
||||
let open_parent = parent.clone();
|
||||
@@ -177,48 +298,16 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
let mut dialog = open_parent.clone();
|
||||
if !first.is_empty() {
|
||||
let label = first.iter().map(|e| e.to_uppercase()).collect::<Vec<_>>().join(", ");
|
||||
let both: Vec<String> = first.iter().flat_map(|e| [e.to_lowercase(), e.to_uppercase()]).collect();
|
||||
dialog = dialog.add_filter(label, &both);
|
||||
dialog = dialog.add_filter(label, &case_variants(first));
|
||||
}
|
||||
let future = dialog
|
||||
.add_filter(
|
||||
"Designs, CAD files and meshes",
|
||||
&[
|
||||
io::DESIGN_EXT,
|
||||
io::LEGACY_DESIGN_EXT,
|
||||
"json",
|
||||
"step",
|
||||
"stp",
|
||||
"STEP",
|
||||
"STP",
|
||||
"igs",
|
||||
"iges",
|
||||
"IGS",
|
||||
"IGES",
|
||||
"FCStd",
|
||||
"fcstd",
|
||||
"3dm",
|
||||
"3DM",
|
||||
"3mf",
|
||||
"3MF",
|
||||
"stl",
|
||||
"STL",
|
||||
"obj",
|
||||
"OBJ",
|
||||
"glb",
|
||||
"GLB",
|
||||
"gltf",
|
||||
"GLTF",
|
||||
],
|
||||
)
|
||||
.add_filter("SolidCraft design (container or JSON)", &[io::DESIGN_EXT, io::LEGACY_DESIGN_EXT, "json"])
|
||||
.add_filter("STEP", &["step", "stp", "STEP", "STP"])
|
||||
.add_filter("IGES", &["igs", "iges", "IGS", "IGES"])
|
||||
.add_filter("FreeCAD and Rhino", &["FCStd", "fcstd", "3dm", "3DM"])
|
||||
.add_filter("Mesh (3MF, STL, OBJ, glTF)", &["3mf", "3MF", "stl", "STL", "obj", "OBJ", "glb", "GLB", "gltf", "GLTF"])
|
||||
.add_filter("Sketch drawings (DXF, SVG)", &["dxf", "DXF", "svg", "SVG"])
|
||||
.add_filter("All files", &["*"])
|
||||
.pick_file();
|
||||
// The groups come from the format registry, so a new format joins this dialog by itself.
|
||||
let mut everything = io::formats::open_extensions(true);
|
||||
everything.extend(["dxf", "svg"]);
|
||||
dialog = dialog.add_filter(solidcraft_ui_egui::i18n::tr("file.open.drawing_filter"), &case_variants(&everything));
|
||||
for (label, exts) in io::formats::import_filters(true) {
|
||||
dialog = dialog.add_filter(label, &case_variants(&exts));
|
||||
}
|
||||
let future = dialog.add_filter("All files", &["*"]).pick_file();
|
||||
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
|
||||
})),
|
||||
// One kind of file (the agent chat's attachments): only the types it names, as given and in
|
||||
@@ -257,7 +346,9 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
let future = dialog.save_file();
|
||||
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
|
||||
})),
|
||||
graphics_adapters: Vec::new(),
|
||||
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
|
||||
window_system: window_system_now(),
|
||||
http_get: Some(std::sync::Arc::new(http_get)),
|
||||
http_download: Some(std::sync::Arc::new(http_download)),
|
||||
// Already an installed app: only the browser build offers to install.
|
||||
@@ -336,7 +427,7 @@ fn agent_http(r: &solidcraft_ui_egui::agent_chat::backend::HttpRequest) -> Resul
|
||||
let agent: ureq::Agent = ureq::Agent::config_builder()
|
||||
.tls_config(tls)
|
||||
.http_status_as_error(false)
|
||||
.timeout_global(Some(Duration::from_secs(r.timeout_s.clamp(1, 900))))
|
||||
.timeout_global(Some(Duration::from_secs(r.timeout_s.clamp(1, 3600))))
|
||||
.timeout_connect(Some(Duration::from_secs(10)))
|
||||
.user_agent("SolidCraft")
|
||||
.max_redirects(0)
|
||||
@@ -427,14 +518,15 @@ fn http_call(url: &str, timeout: std::time::Duration) -> Result<ureq::http::Resp
|
||||
}
|
||||
|
||||
/// The per-user settings folder: eframe's storage (`app.ron`) and the anti-aliasing preference.
|
||||
fn settings_dir() -> Option<std::path::PathBuf> {
|
||||
eframe::storage_dir("SolidCraft")
|
||||
fn settings_dir() -> Result<Option<std::path::PathBuf>, String> {
|
||||
let selected = std::env::var_os("SOLIDCRAFT_SETTINGS_DIR");
|
||||
startup::settings_directory(selected.as_deref(), eframe::storage_dir("SolidCraft"))
|
||||
}
|
||||
|
||||
/// The anti-aliasing preference, kept next to eframe's storage (it is needed before the
|
||||
/// window opens).
|
||||
fn graphics_path() -> Option<std::path::PathBuf> {
|
||||
settings_dir().map(|d| d.join("graphics.json"))
|
||||
settings_dir().ok().flatten().map(|d| d.join("graphics.json"))
|
||||
}
|
||||
|
||||
/// Compatibility shim (docs/compatibility.md): on the first launch after the rename, copy the
|
||||
@@ -444,7 +536,7 @@ fn graphics_path() -> Option<std::path::PathBuf> {
|
||||
/// old app id's `~/.var/app` folder; that copy comes first, as it is the newest. The old folders
|
||||
/// are never changed.
|
||||
fn adopt_legacy_settings() {
|
||||
let (Some(current), Some(legacy)) = (settings_dir(), eframe::storage_dir("SolveCraft")) else { return };
|
||||
let (Some(current), Some(legacy)) = (settings_dir().ok().flatten(), eframe::storage_dir("SolveCraft")) else { return };
|
||||
let legacy_eframe = eframe::storage_dir(solidcraft_engine::io::LEGACY_APP_ID);
|
||||
let old_flatpak = solidcraft_engine::io::legacy_flatpak_path(¤t);
|
||||
if let Err(e) = adopt_settings(¤t, old_flatpak.as_deref(), &legacy, legacy_eframe.as_deref()) {
|
||||
@@ -483,13 +575,34 @@ fn backend_preference() -> Option<String> {
|
||||
graphics().and_then(|v| v.get("backend").and_then(serde_json::Value::as_str).map(str::to_string))
|
||||
}
|
||||
|
||||
/// Anti-aliasing samples from Preferences (4 when unset).
|
||||
fn msaa() -> u16 {
|
||||
/// The viewport scene's sample count: the app's anti-aliasing (the window's pass is 1x), clamped
|
||||
/// by the backend's policy (OpenGL renders 1x, see `effective_msaa_samples`). A count the device
|
||||
/// cannot render with falls back to the supported default.
|
||||
fn scene_samples(backend: eframe::wgpu::Backend, samples: u32, supported: &[u32]) -> u32 {
|
||||
let requested = eframe::egui_wgpu::effective_msaa_samples(backend, samples.max(1));
|
||||
if requested <= 1 || supported.is_empty() || supported.contains(&requested) {
|
||||
requested
|
||||
} else {
|
||||
supported.iter().copied().filter(|&n| n <= solidcraft_ui_egui::prefs::Prefs::default().msaa).max().unwrap_or(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep the default until the installed renderer's startup check proves a fallback is needed.
|
||||
fn scene_msaa_request(saved: Option<u16>) -> u16 {
|
||||
saved.unwrap_or(MSAA)
|
||||
}
|
||||
|
||||
/// Query the scene's colour/depth intersection, rather than the depthless window's counts.
|
||||
fn scene_sample_choices(depth_bits: u8, stencil_bits: u8, query: impl FnOnce(Option<eframe::wgpu::TextureFormat>) -> Vec<u32>) -> Vec<u32> {
|
||||
query(eframe::egui_wgpu::depth_format_from_bits(depth_bits, stencil_bits))
|
||||
}
|
||||
|
||||
/// An explicitly saved anti-aliasing choice, absent until Graphics changes it.
|
||||
fn msaa() -> Option<u16> {
|
||||
graphics()
|
||||
.and_then(|v| v.get("msaa").and_then(serde_json::Value::as_u64))
|
||||
.and_then(|m| u16::try_from(m).ok())
|
||||
.filter(|m| [1, 2, 4, 8].contains(m))
|
||||
.unwrap_or(MSAA)
|
||||
}
|
||||
|
||||
/// Windows and Linux: no OS title bar; the application bar is the title bar
|
||||
@@ -505,7 +618,8 @@ const FLOAT_DEPTH_BITS: u8 = 32;
|
||||
const STENCIL_BITS: u8 = 8;
|
||||
const MSAA: u16 = 4;
|
||||
|
||||
fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
|
||||
/// `custom_titlebar`: no OS decorations, the application bar is the title bar.
|
||||
fn app_viewport(custom_titlebar: bool) -> Result<egui::ViewportBuilder, eframe::Error> {
|
||||
// Let macOS use the bundle's layered icon; eframe's default bitmap would replace it.
|
||||
#[cfg(target_os = "macos")]
|
||||
let icon = egui::IconData::default();
|
||||
@@ -518,7 +632,7 @@ fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
|
||||
.with_inner_size([1600.0, 1000.0])
|
||||
.with_min_inner_size([960.0, 600.0])
|
||||
.with_drag_and_drop(true)
|
||||
.with_decorations(!CUSTOM_TITLEBAR)
|
||||
.with_decorations(!custom_titlebar)
|
||||
.with_fullsize_content_view(true)
|
||||
.with_titlebar_shown(false)
|
||||
.with_title_shown(false)
|
||||
@@ -528,9 +642,27 @@ fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
|
||||
/// The window's app id: it names the desktop entry, so it is the packages' reverse-DNS id.
|
||||
const APP_ID: &str = solidcraft_engine::io::APP_ID;
|
||||
|
||||
/// Disconnects from the 3D mouse driver when dropped, as `main` returns or a panic unwinds out
|
||||
/// of it (`solidcraft_3dx_client::stop`).
|
||||
#[cfg(target_os = "macos")]
|
||||
struct DriverClient;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl Drop for DriverClient {
|
||||
fn drop(&mut self) {
|
||||
solidcraft_3dx_client::stop();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
#[cfg(target_os = "macos")]
|
||||
objc2_foundation::NSProcessInfo::processInfo().setProcessName(objc2_foundation::ns_string!("SolidCraft"));
|
||||
mark("main");
|
||||
adopt_legacy_settings();
|
||||
// Validate before legacy adoption, crash markers or any user preference read.
|
||||
let settings = settings_dir().map_err(|error| eframe::Error::AppCreation(error.into()))?;
|
||||
if std::env::var_os("SOLIDCRAFT_SETTINGS_DIR").is_none() {
|
||||
adopt_legacy_settings();
|
||||
}
|
||||
let mut safe_mode = startup::env_on(std::env::var(startup::SAFE_MODE_ENV).ok().as_deref());
|
||||
let mut control_port: Option<u16> = solidcraft_engine::geom::env::var("CONTROL_PORT").and_then(|p| p.parse().ok());
|
||||
let mut files = Vec::new();
|
||||
@@ -567,7 +699,6 @@ fn main() -> eframe::Result {
|
||||
}
|
||||
// Settings that don't read are moved aside; launches that never got a window are counted,
|
||||
// and after a few in a row this one starts in safe mode (docs/book/src/guide/troubleshooting.md).
|
||||
let settings = settings_dir();
|
||||
let mut notes: Vec<String> = Vec::new();
|
||||
let mut failed_starts = 0;
|
||||
if let Some(dir) = &settings {
|
||||
@@ -579,10 +710,21 @@ fn main() -> eframe::Result {
|
||||
notes.push(format!("The last {failed_starts} launches stopped before the window opened, so this one is in safe mode."));
|
||||
}
|
||||
let cpu_renderer = safe_mode || startup::cpu_renderer_env(std::env::var(startup::RENDERER_ENV).ok().as_deref());
|
||||
// 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));
|
||||
}
|
||||
if safe_mode {
|
||||
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
|
||||
}
|
||||
let samples = if safe_mode { MSAA } else { msaa() };
|
||||
let fallback_note = std::env::var(startup::FALLBACK_NOTE_ENV).ok().map(|s| s.chars().take(4096).collect::<String>());
|
||||
if let Some(note) = &fallback_note {
|
||||
log::warn!("{note}");
|
||||
eprintln!("SolidCraft: {note}");
|
||||
notes.push(note.clone());
|
||||
}
|
||||
let saved_samples = if safe_mode { None } else { msaa() };
|
||||
let wgpu_options = wgpu_options(cpu_renderer, if safe_mode { None } else { backend_preference() }.as_deref());
|
||||
let depth_bits = if !safe_mode && startup::float_depth_everywhere(&wgpu_options) { FLOAT_DEPTH_BITS } else { DEPTH_BITS };
|
||||
// Crashes leave a report in the crash folder (solidcraft_engine::crash, docs/crash-reports.md).
|
||||
@@ -600,13 +742,21 @@ fn main() -> eframe::Result {
|
||||
// module on Windows, and on every system the startup step the app was in.
|
||||
#[cfg(target_os = "windows")]
|
||||
native_crash::install();
|
||||
// Windows with only OpenGL to start: the system title bar instead of ours (#2063).
|
||||
let started = match &wgpu_options.wgpu_setup {
|
||||
eframe::egui_wgpu::WgpuSetup::CreateNew(setup) => setup.instance_descriptor.backends,
|
||||
_ => eframe::wgpu::Backends::all(),
|
||||
};
|
||||
let custom_titlebar = CUSTOM_TITLEBAR && !startup::system_title_bar(cfg!(target_os = "windows"), started);
|
||||
eframe::egui_wgpu::set_startup_hook(startup_step);
|
||||
solidcraft_engine::crash::phase("window");
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: app_viewport()?,
|
||||
depth_buffer: depth_bits,
|
||||
stencil_buffer: STENCIL_BITS,
|
||||
multisampling: samples,
|
||||
viewport: app_viewport(custom_titlebar)?,
|
||||
// The window's pass (egui's) has no depth and one sample: the viewport draws its scene
|
||||
// offscreen with its own depth and MSAA, then blits it, so the UI pass pays for neither.
|
||||
depth_buffer: 0,
|
||||
stencil_buffer: 0,
|
||||
multisampling: 1,
|
||||
// Kept with the other settings, not in a folder named after the app id. Safe mode reads
|
||||
// and writes a scratch copy, leaving the user's settings as they were.
|
||||
persistence_path: if safe_mode {
|
||||
@@ -623,12 +773,24 @@ fn main() -> eframe::Result {
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
let mut options = options;
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
if prefer_x11(
|
||||
std::env::var_os("DISPLAY").is_some(),
|
||||
std::env::var_os("WAYLAND_DISPLAY").is_some(),
|
||||
std::env::var("XDG_CURRENT_DESKTOP").ok().as_deref(),
|
||||
solidcraft_engine::geom::env::var("WAYLAND").as_deref(),
|
||||
) {
|
||||
let x11 = {
|
||||
// SOLIDCRAFT_WAYLAND wins over Preferences › Graphics › Window system (#3083).
|
||||
let environment = solidcraft_engine::geom::env::var("WAYLAND");
|
||||
let preference = window_system_preference();
|
||||
let wayland_override = environment.as_deref().or(window_system_override(preference.as_deref()));
|
||||
let wayland_display = std::env::var_os("WAYLAND_DISPLAY").is_some();
|
||||
let x11 = prefer_x11(
|
||||
std::env::var_os("DISPLAY").is_some(),
|
||||
wayland_display,
|
||||
std::env::var("XDG_CURRENT_DESKTOP").ok().as_deref(),
|
||||
wayland_override,
|
||||
);
|
||||
// Without a Wayland socket winit opens on X11 anyway.
|
||||
let _ = WINDOW_SYSTEM.set(if x11 || !wayland_display { "x11" } else { "wayland" });
|
||||
x11
|
||||
};
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
if x11 {
|
||||
options.event_loop_builder = Some(Box::new(|b| {
|
||||
use winit::platform::x11::EventLoopBuilderExtX11;
|
||||
b.with_x11();
|
||||
@@ -640,6 +802,10 @@ fn main() -> eframe::Result {
|
||||
let created = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let created_in_app = created.clone();
|
||||
let starting = settings.clone();
|
||||
// A panic on the window's thread ends the app without `on_exit`; this disconnects from the
|
||||
// 3D mouse driver then too.
|
||||
#[cfg(target_os = "macos")]
|
||||
let _driver_client = DriverClient;
|
||||
mark("run_native");
|
||||
let result = eframe::run_native(
|
||||
"SolidCraft",
|
||||
@@ -653,42 +819,74 @@ fn main() -> eframe::Result {
|
||||
mark("fonts");
|
||||
let mut app = SolveApp::new(Session::default(), services(cc));
|
||||
app.apply_off_thread = Some(solidcraft_ui_egui::dialogs::APPLY_AT_ONCE);
|
||||
app.custom_titlebar = CUSTOM_TITLEBAR;
|
||||
// Windows: the OS hit test offers the resize border, at the window's DPI, from the
|
||||
// first message (#1381).
|
||||
app.custom_titlebar = custom_titlebar;
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
app.native_resize_border = CUSTOM_TITLEBAR && native_frame::install(cc);
|
||||
if CUSTOM_TITLEBAR && !app.native_resize_border {
|
||||
// Windows: the taskbar button shows the mark even when slow first frames keep the
|
||||
// taskbar waiting for the window's icon.
|
||||
if !native_frame::set_class_icon(cc) {
|
||||
eprintln!("SolidCraft window: no class icon; the taskbar may show a generic one");
|
||||
}
|
||||
// The OS hit test offers the resize border, at the window's DPI, from the first
|
||||
// message (#1381).
|
||||
app.native_resize_border = custom_titlebar && native_frame::install(cc);
|
||||
if custom_titlebar && !app.native_resize_border {
|
||||
eprintln!("SolidCraft window: no native resize border; egui edge zones instead");
|
||||
}
|
||||
}
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
app.cpu_meshing = safe_mode;
|
||||
app.graphics_fallback = fallback_note.clone();
|
||||
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
mark("app state and prefs");
|
||||
let gpu = cc.wgpu_render_state.as_ref().map(|rs| LostWatch {
|
||||
lost: rs.device_lost.clone(),
|
||||
restart_on_software: device_lost::restart_on_software(after_loss, cpu_renderer),
|
||||
lost_at: None,
|
||||
});
|
||||
if !safe_mode {
|
||||
app.preferences.graphics_adapter =
|
||||
graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string)).unwrap_or_default();
|
||||
}
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
eprintln!("SolidCraft graphics: {:?}, MSAA {}x (requested {}x)", rs.adapter.get_info().backend, rs.msaa_samples, samples);
|
||||
app.services.graphics_adapters = rs
|
||||
.available_adapters
|
||||
.iter()
|
||||
.map(|adapter| {
|
||||
let info = adapter.get_info();
|
||||
(info.name.clone(), format!("{} ({:?}, {:?})", info.name, info.device_type, info.backend))
|
||||
})
|
||||
.collect();
|
||||
let info = rs.adapter.get_info();
|
||||
let samples = scene_msaa_request(saved_samples);
|
||||
let supported = scene_sample_choices(depth_bits, STENCIL_BITS, |depth| {
|
||||
eframe::egui_wgpu::supported_msaa_samples(&rs.adapter, &rs.device, rs.target_format, depth)
|
||||
});
|
||||
let scene_samples = scene_samples(info.backend, u32::from(samples), &supported);
|
||||
eprintln!("SolidCraft graphics: {:?}, MSAA {scene_samples}x (requested {}x)", rs.adapter.get_info().backend, samples);
|
||||
solidcraft_engine::crash::phase("viewport pipelines (shader compile)");
|
||||
app.set_wgpu(rs, depth_bits, STENCIL_BITS, rs.msaa_samples);
|
||||
let scene_samples = app.set_wgpu_split(rs, depth_bits, STENCIL_BITS, scene_samples);
|
||||
app.msaa_supported = supported;
|
||||
app.preferences.msaa = u32::from(samples);
|
||||
solidcraft_ui_egui::gpu::install_picture_renderer(rs);
|
||||
// The graphics driver is loaded now; it may have put its own exception filter
|
||||
// in front of ours.
|
||||
#[cfg(target_os = "windows")]
|
||||
native_crash::reinstall();
|
||||
// A saved anti-aliasing the device can't do: back to the default, with a notice.
|
||||
// Reflect a confirmed probe fallback or an unsupported saved count in Graphics.
|
||||
solidcraft_ui_egui::prefs::check_msaa(&mut app, u32::from(samples));
|
||||
let info = rs.adapter.get_info();
|
||||
solidcraft_engine::crash::set_environment(&format!(
|
||||
"Renderer: {:?} {} ({}), MSAA {}x",
|
||||
info.backend, info.name, info.driver, rs.msaa_samples
|
||||
"Renderer: {:?} {} ({}), MSAA {scene_samples}x",
|
||||
info.backend, info.name, info.driver
|
||||
));
|
||||
}
|
||||
mark("viewport pipelines");
|
||||
if let Some(port) = control_port {
|
||||
let rx = control_server::start(port, cc.egui_ctx.clone());
|
||||
// `--control`/the environment name a port; otherwise the preference (on by default)
|
||||
// opens the default one (docs/control-protocol.md).
|
||||
if let Some(listen_on) = solidcraft_engine::control_auth::plan(control_port, app.preferences.agent_control) {
|
||||
let rx = control_server::start(listen_on, cc.egui_ctx.clone());
|
||||
app = app.with_control(rx);
|
||||
}
|
||||
// The agent chat's MCP bridge reaches the app through an endpoint of its own, started
|
||||
@@ -703,6 +901,9 @@ fn main() -> eframe::Result {
|
||||
app.session.mark_saved();
|
||||
app.session.undo.clear();
|
||||
}
|
||||
// Lock files next to open designs: a design open on another computer says so (#5467),
|
||||
// the files given at launch included.
|
||||
solidcraft_ui_egui::open_elsewhere::start(&mut app);
|
||||
// A launch from Finder: its files arrived before the window.
|
||||
#[cfg(target_os = "macos")]
|
||||
let files = {
|
||||
@@ -737,8 +938,21 @@ fn main() -> eframe::Result {
|
||||
if !notes.is_empty() {
|
||||
app.set_status(notes.join(" "), true);
|
||||
}
|
||||
let nav_bridge = nav_bridge::NavBridge::start(&cc.egui_ctx);
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx);
|
||||
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx, nav_bridge.claim());
|
||||
#[cfg(target_os = "macos")]
|
||||
let native_menu = match native_menu::NativeMenu::new(&app, &cc.egui_ctx) {
|
||||
Ok(menu) => Some(menu),
|
||||
Err(error) => {
|
||||
app.set_status(format!("System menu: {error}"), true);
|
||||
None
|
||||
}
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
app.native_file_menu = native_menu.is_some();
|
||||
}
|
||||
mark("app created");
|
||||
solidcraft_engine::crash::phase("first frame");
|
||||
Ok(Box::new(App {
|
||||
@@ -746,14 +960,18 @@ fn main() -> eframe::Result {
|
||||
starting,
|
||||
frames: 0,
|
||||
take_focus_unwanted: control_port.is_some(),
|
||||
gpu,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
space_mouse,
|
||||
#[cfg(target_os = "macos")]
|
||||
native_menu,
|
||||
nav_bridge,
|
||||
}))
|
||||
}),
|
||||
);
|
||||
control_server::remove_token_file();
|
||||
match result {
|
||||
Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, settings.as_deref()),
|
||||
Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, started, settings.as_deref()),
|
||||
r => r,
|
||||
}
|
||||
}
|
||||
@@ -774,13 +992,19 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
||||
let mut options = eframe::egui_wgpu::WgpuConfiguration::default();
|
||||
if let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_setup {
|
||||
let env_set = std::env::var(startup::WGPU_BACKEND_ENV).is_ok_and(|v| !v.trim().is_empty());
|
||||
// A retry applies only to this launch. WGPU_BACKEND always has priority.
|
||||
let retry = std::env::var(startup::RETRY_BACKEND_ENV).ok();
|
||||
let preference = if cpu { preference } else { retry.as_deref().or(preference) };
|
||||
if let Some(backends) = startup::choose_backends(cfg!(target_os = "windows"), env_set, cpu, preference, startup::has_hardware_adapter) {
|
||||
eprintln!("SolidCraft graphics: starting {backends:?}");
|
||||
setup.instance_descriptor.backends = backends;
|
||||
}
|
||||
if cpu {
|
||||
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
|
||||
}
|
||||
let intel_mac = cfg!(all(target_os = "macos", target_arch = "x86_64"));
|
||||
let indirect = std::env::var(startup::INDIRECT_VALIDATION_ENV).ok();
|
||||
setup.instance_descriptor.flags = startup::window_instance_flags(setup.instance_descriptor.flags, intel_mac, indirect.as_deref());
|
||||
let environment = std::env::var("WGPU_ADAPTER_NAME").ok();
|
||||
let adapter = graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string));
|
||||
startup::configure_adapter(setup, cpu, environment, adapter);
|
||||
// A float depth buffer with a stencil, wherever the adapter has it (see `FLOAT_DEPTH_BITS`).
|
||||
let base = setup.device_descriptor.clone();
|
||||
setup.device_descriptor = std::sync::Arc::new(move |adapter| {
|
||||
@@ -792,29 +1016,82 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
||||
options
|
||||
}
|
||||
|
||||
/// The window never opened (no graphics adapter, a device that wouldn't start): once more on
|
||||
/// the software renderer, else a message box saying what failed instead of a silent exit.
|
||||
fn no_window(error: eframe::Error, tried_cpu: bool, settings: Option<&std::path::Path>) -> eframe::Result {
|
||||
/// A failed renderer returns here before app creation. A fresh process avoids reusing winit's
|
||||
/// event loop or a failed device; each retry advances through the APIs, then software once.
|
||||
fn no_window(error: eframe::Error, tried_cpu: bool, failed: eframe::wgpu::Backends, settings: Option<&std::path::Path>) -> eframe::Result {
|
||||
log::error!("the window could not open: {error}");
|
||||
eprintln!("SolidCraft: the window could not open: {error}");
|
||||
if !tried_cpu && let Ok(exe) = std::env::current_exe() {
|
||||
eprintln!("SolidCraft: trying again with the software renderer ({}=cpu)", startup::RENDERER_ENV);
|
||||
match std::process::Command::new(exe).args(std::env::args_os().skip(1)).env(startup::RENDERER_ENV, "cpu").status() {
|
||||
// This failure was handled, so neither the marker nor the journal should call it a crash.
|
||||
if let Some(dir) = settings {
|
||||
startup::started(dir);
|
||||
}
|
||||
solidcraft_engine::crash::end_session();
|
||||
let explicit = std::env::var(startup::WGPU_BACKEND_ENV).is_ok_and(|v| !v.trim().is_empty());
|
||||
if let Some(next) = startup::fallback_backend(cfg!(target_os = "windows"), explicit, tried_cpu, failed)
|
||||
&& let Ok(exe) = std::env::current_exe()
|
||||
{
|
||||
let previous = std::env::var(startup::FALLBACK_NOTE_ENV).unwrap_or_default();
|
||||
let note = startup::fallback_note(&previous, failed, next, &error.to_string());
|
||||
log::warn!("{note}");
|
||||
eprintln!("SolidCraft: {note}");
|
||||
let mut command = std::process::Command::new(exe);
|
||||
command.args(std::env::args_os().skip(1)).env(startup::FALLBACK_NOTE_ENV, note);
|
||||
if next == "cpu" {
|
||||
command.env(startup::RENDERER_ENV, "cpu").env_remove(startup::RETRY_BACKEND_ENV);
|
||||
} else {
|
||||
command.env(startup::RETRY_BACKEND_ENV, next);
|
||||
}
|
||||
match command.status() {
|
||||
Ok(status) if status.success() => return Ok(()),
|
||||
// The second try showed its own message.
|
||||
// The final try showed its own message.
|
||||
Ok(_) => return Err(error),
|
||||
Err(e) => log::error!("could not start again with the software renderer: {e}"),
|
||||
Err(e) => log::error!("could not start again on {next}: {e}"),
|
||||
}
|
||||
}
|
||||
let message = if explicit {
|
||||
format!("SolidCraft could not open its window with WGPU_BACKEND.\n\n{error}\n\nChange or remove WGPU_BACKEND to try another graphics API.")
|
||||
} else {
|
||||
startup::no_window_message(&error.to_string(), settings)
|
||||
};
|
||||
let _ = rfd::MessageDialog::new()
|
||||
.set_level(rfd::MessageLevel::Error)
|
||||
.set_title("SolidCraft could not start")
|
||||
.set_description(startup::no_window_message(&error.to_string(), settings))
|
||||
.set_description(message)
|
||||
.set_buttons(rfd::MessageButtons::Ok)
|
||||
.show();
|
||||
Err(error)
|
||||
}
|
||||
|
||||
/// Linux: the window system this run opened with (`wayland` or `x11`), for Preferences (#3083).
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
static WINDOW_SYSTEM: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
|
||||
|
||||
/// The window system this run uses, where there is a choice (Linux).
|
||||
fn window_system_now() -> Option<&'static str> {
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
return WINDOW_SYSTEM.get().copied();
|
||||
#[cfg(not(all(unix, not(target_os = "macos"))))]
|
||||
None
|
||||
}
|
||||
|
||||
/// Preferences › Graphics › Window system (`graphics.json`'s `window_system`), read before the
|
||||
/// window opens.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn window_system_preference() -> Option<String> {
|
||||
graphics().and_then(|v| v.get("window_system").and_then(serde_json::Value::as_str).map(str::to_string))
|
||||
}
|
||||
|
||||
/// The window system preference as the `SOLIDCRAFT_WAYLAND` value it stands for: `wayland` is
|
||||
/// `1`, `x11` is `0`, and `auto` (or anything else) leaves the choice to [`prefer_x11`].
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn window_system_override(preference: Option<&str>) -> Option<&'static str> {
|
||||
match preference.map(str::trim) {
|
||||
Some("wayland") => Some("1"),
|
||||
Some("x11") => Some("0"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to run through XWayland rather than native Wayland. winit has no file drag-and-drop
|
||||
/// on Wayland (only on X11), so dropping files from the file manager showed a "no" cursor: use
|
||||
/// XWayland when it's there. Except on niri, whose XWayland (xwayland-satellite) breaks window
|
||||
@@ -852,14 +1129,27 @@ mod backend_tests {
|
||||
assert!(prefer_x11(true, true, Some("niri"), Some("0")));
|
||||
assert!(!prefer_x11(false, true, Some("niri"), Some("0")));
|
||||
}
|
||||
|
||||
/// Preferences › Graphics › Window system stands for the same values (#3083).
|
||||
#[test]
|
||||
fn the_window_system_preference_maps_to_the_override() {
|
||||
use super::window_system_override as over;
|
||||
assert_eq!(
|
||||
(over(Some("wayland")), over(Some("x11")), over(Some("auto")), over(None), over(Some("junk"))),
|
||||
(Some("1"), Some("0"), None, None, None)
|
||||
);
|
||||
// Wayland chosen on GNOME keeps native Wayland; X11 chosen on niri goes through XWayland.
|
||||
assert!(!prefer_x11(true, true, Some("GNOME"), over(Some("wayland"))));
|
||||
assert!(prefer_x11(true, true, Some("niri"), over(Some("x11"))));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod icon_tests {
|
||||
mod viewport_tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn macos_preserves_the_bundle_icon_instead_of_setting_a_runtime_bitmap() {
|
||||
let viewport = super::app_viewport().expect("viewport");
|
||||
let viewport = super::app_viewport(super::CUSTOM_TITLEBAR).expect("viewport");
|
||||
assert_eq!(viewport.icon.as_deref(), Some(&eframe::egui::IconData::default()));
|
||||
assert_eq!(viewport.app_id.as_deref(), Some(super::APP_ID));
|
||||
}
|
||||
@@ -867,12 +1157,89 @@ mod icon_tests {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn desktop_viewport_supplies_the_original_mark_to_the_window() {
|
||||
let viewport = super::app_viewport().expect("viewport");
|
||||
let viewport = super::app_viewport(super::CUSTOM_TITLEBAR).expect("viewport");
|
||||
let icon = viewport.icon.expect("running window must have an icon");
|
||||
assert_eq!((icon.width, icon.height), (64, 64));
|
||||
assert_eq!(icon.rgba, super::app_icon::rgba(64).expect("original mark"));
|
||||
assert_eq!(viewport.app_id.as_deref(), Some(super::APP_ID));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_bar_clears_native_chrome_and_undo_redo_keep_working() {
|
||||
use egui::{Event, PointerButton, Pos2, RawInput, Rect, ViewportId, vec2};
|
||||
use serde_json::json;
|
||||
use solidcraft_ui_egui::scenario::handle_at;
|
||||
|
||||
// AppKit points are independent of Retina scale and of the user's UI zoom.
|
||||
for (zoom, retina) in [(1.0, 1.0), (1.0, 2.0), (1.5, 2.0), (2.0, 2.0)] {
|
||||
let ctx = egui::Context::default();
|
||||
solidcraft_ui_egui::fonts::setup_bundled_fonts(&ctx);
|
||||
ctx.set_zoom_factor(zoom);
|
||||
let mut app = super::SolveApp::new(super::Session::default(), Default::default());
|
||||
app.integrated_titlebar = true;
|
||||
for (command, params) in [
|
||||
("sketch.create", json!({"plane":"XY"})),
|
||||
("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[40,30]})),
|
||||
("sketch.finish", json!({})),
|
||||
("solid.extrude", json!({"distance":15})),
|
||||
] {
|
||||
app.run(command, params).unwrap();
|
||||
}
|
||||
let mut time = 0.0;
|
||||
let mut frame = |app: &mut super::SolveApp, fullscreen, events| {
|
||||
time += 0.05;
|
||||
let mut raw = RawInput {
|
||||
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 1000.0))),
|
||||
time: Some(time),
|
||||
events,
|
||||
..Default::default()
|
||||
};
|
||||
let viewport = raw.viewports.get_mut(&ViewportId::ROOT).unwrap();
|
||||
viewport.fullscreen = Some(fullscreen);
|
||||
viewport.native_pixels_per_point = Some(retina);
|
||||
super::fullscreen_content_insets(&mut raw, 32.0, zoom);
|
||||
let _ = ctx.run_ui(raw, |ui| super::content_ui(app, ui));
|
||||
};
|
||||
// Enter, leave, and enter again: no stale inset when returning to a normal window.
|
||||
for fullscreen in [false, true, false, true] {
|
||||
frame(&mut app, fullscreen, vec![]);
|
||||
frame(&mut app, fullscreen, vec![]);
|
||||
let top = if fullscreen { 32.0 / zoom } else { 0.0 };
|
||||
for name in ["appbar:file", "appbar:undo", "appbar:redo", "appbar:help"] {
|
||||
let at = handle_at(name).unwrap();
|
||||
assert!((at.y - top - 17.0).abs() < 0.1, "{name}: {at:?}, fullscreen={fullscreen}, zoom={zoom}");
|
||||
}
|
||||
for (handle, bodies) in [("appbar:undo", 0), ("appbar:redo", 1)] {
|
||||
let at = handle_at(handle).unwrap();
|
||||
frame(&mut app, fullscreen, vec![Event::PointerMoved(at)]);
|
||||
for pressed in [true, false] {
|
||||
frame(
|
||||
&mut app,
|
||||
fullscreen,
|
||||
vec![Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }],
|
||||
);
|
||||
}
|
||||
assert_eq!(app.session.model.state().bodies.len(), bodies, "{handle}, fullscreen={fullscreen}, zoom={zoom}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_inset_preserves_existing_safe_areas_and_resets_on_exit() {
|
||||
use egui::epaint::MarginF32;
|
||||
use egui::{RawInput, SafeAreaInsets, ViewportId};
|
||||
let mut raw = RawInput::default();
|
||||
raw.viewports.get_mut(&ViewportId::ROOT).unwrap().fullscreen = Some(true);
|
||||
raw.safe_area_insets = Some(SafeAreaInsets(MarginF32 { top: 40.0, left: 5.0, right: 6.0, bottom: 7.0 }));
|
||||
super::fullscreen_content_insets(&mut raw, 32.0, 2.0);
|
||||
assert_eq!(raw.safe_area_insets.unwrap().0, MarginF32 { top: 40.0, left: 5.0, right: 6.0, bottom: 7.0 });
|
||||
|
||||
raw.viewports.get_mut(&ViewportId::ROOT).unwrap().fullscreen = Some(false);
|
||||
raw.safe_area_insets = None;
|
||||
super::fullscreen_content_insets(&mut raw, 32.0, 1.0);
|
||||
assert_eq!(raw.safe_area_insets, Some(SafeAreaInsets::default()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -909,4 +1276,41 @@ mod msaa_tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_sample_choices_include_the_scene_depth_format() {
|
||||
use eframe::wgpu::TextureFormat;
|
||||
for (bits, expected) in [(24, TextureFormat::Depth24PlusStencil8), (32, TextureFormat::Depth32FloatStencil8)] {
|
||||
// A synthetic colour target supports 8x, but the scene depth supports only 1x/4x.
|
||||
let supported = super::scene_sample_choices(bits, 8, |depth| match depth {
|
||||
Some(format) if format == expected => vec![1, 4],
|
||||
None => vec![1, 4, 8],
|
||||
other => panic!("unexpected scene depth: {other:?}"),
|
||||
});
|
||||
assert_eq!(supported, [1, 4]);
|
||||
assert_eq!(super::scene_samples(eframe::wgpu::Backend::Dx12, 8, &supported), 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_msaa_keeps_four_samples_by_default_and_explicit_choices() {
|
||||
use eframe::wgpu::Backend;
|
||||
let default = super::scene_msaa_request(None);
|
||||
assert_eq!(default, 4);
|
||||
for backend in [Backend::Dx12, Backend::Vulkan, Backend::Metal] {
|
||||
assert_eq!(super::scene_samples(backend, u32::from(default), &[1, 4]), 4);
|
||||
}
|
||||
for saved in [1, 2, 4, 8] {
|
||||
assert_eq!(super::scene_msaa_request(Some(saved)), saved);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_scene_follows_the_backend_policy_too() {
|
||||
use eframe::wgpu::Backend;
|
||||
assert_eq!(super::scene_samples(Backend::Gl, 4, &[1, 4]), 1);
|
||||
assert_eq!(super::scene_samples(Backend::Gl, 4, &[]), 1);
|
||||
assert_eq!(super::scene_samples(Backend::Vulkan, 4, &[1, 4]), 4);
|
||||
assert_eq!(super::scene_samples(Backend::Dx12, 8, &[1, 2, 4]), 4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,14 @@
|
||||
//! window rescales once at the crossing and stays under the pointer, instead of the OS's move
|
||||
//! loop and winit disagreeing about a frame the window doesn't have.
|
||||
//!
|
||||
//! Replacing a window procedure has no safe API, hence the `unsafe_code` allowance on this
|
||||
//! module (the app crate denies it; see `native_crash.rs` and `open_documents.rs` for the others).
|
||||
//! Replacing a window procedure and setting a window class's icon have no safe API, hence the
|
||||
//! `unsafe_code` allowance on this module (the app crate denies it; see `native_crash.rs` and
|
||||
//! `open_documents.rs` for the others).
|
||||
//! The procedure is installed once, on the thread that owns the window, for the main window only,
|
||||
//! and calls the previous one (winit's) for everything it doesn't answer.
|
||||
//!
|
||||
//! [`set_class_icon`] gives the window's class the exe's icon, so the taskbar button shows the
|
||||
//! mark even when the window is too busy to answer the taskbar's request for its icon.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::sync::atomic::{AtomicIsize, Ordering};
|
||||
@@ -27,12 +31,18 @@ use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
use solidcraft_ui_egui::titlebar;
|
||||
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, RECT, SIZE, WPARAM};
|
||||
use windows_sys::Win32::Graphics::Gdi::{GetMonitorInfoW, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromWindow};
|
||||
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows_sys::Win32::UI::HiDpi::{GetDpiForWindow, GetSystemMetricsForDpi};
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
CallWindowProcW, GWL_STYLE, GWLP_WNDPROC, GetClientRect, GetWindowLongPtrW, GetWindowRect, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTLEFT,
|
||||
HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IsIconic, IsZoomed, SM_CXPADDEDBORDER, SM_CXSIZEFRAME, SetWindowLongPtrW, WM_GETDPISCALEDSIZE,
|
||||
WM_NCDESTROY, WM_NCHITTEST, WNDPROC, WS_THICKFRAME,
|
||||
CallWindowProcW, GCLP_HICON, GCLP_HICONSM, GWL_STYLE, GWLP_WNDPROC, GetClientRect, GetWindowLongPtrW, GetWindowRect, HTBOTTOM, HTBOTTOMLEFT,
|
||||
HTBOTTOMRIGHT, HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IMAGE_ICON, IsIconic, IsZoomed, LR_DEFAULTCOLOR, LoadImageW, SM_CXICON,
|
||||
SM_CXPADDEDBORDER, SM_CXSIZEFRAME, SM_CXSMICON, SetWindowLongPtrW, WM_GETDPISCALEDSIZE, WM_NCDESTROY, WM_NCHITTEST, WNDPROC, WS_THICKFRAME,
|
||||
};
|
||||
// windows-sys has SetClassLongPtrW only on 64-bit targets; on 32-bit Windows it is SetClassLongW.
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::SetClassLongPtrW;
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::SetClassLongW as SetClassLongPtrW;
|
||||
|
||||
/// The work area (the monitor without the taskbar) of the monitor nearest the window, in physical
|
||||
/// pixels: left, top, width, height.
|
||||
@@ -54,6 +64,48 @@ pub fn work_area(window: &impl HasWindowHandle) -> Option<(i32, i32, i32, i32)>
|
||||
Some((rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top))
|
||||
}
|
||||
|
||||
/// The exe's icon resource (`build.rs` embeds it as `1 ICON`).
|
||||
const ICON_RESOURCE: usize = 1;
|
||||
|
||||
/// Give the main window's class the exe's icon, big and small. Call it before the window is
|
||||
/// shown (eframe's app creator).
|
||||
///
|
||||
/// The taskbar asks a window for its icon when the window appears. eframe sets the window's
|
||||
/// icon before that, while the window is still hidden, and then shows the window at the end of
|
||||
/// its first frame; if that frame is slow, or startup kept the thread busy long enough for
|
||||
/// Windows to mark the window as not responding, the request isn't answered in time. The taskbar then
|
||||
/// takes the window class's icon and doesn't ask again. winit registers its class without one,
|
||||
/// so the button showed Windows' generic icon for as long as the window was open.
|
||||
/// `false` if it isn't a Win32 window or the exe's icon doesn't load; SetClassLongPtrW's own
|
||||
/// failure can't be told from a class that had no icon (both return 0), so it isn't reported.
|
||||
pub fn set_class_icon(window: &impl HasWindowHandle) -> bool {
|
||||
let Ok(handle) = window.window_handle() else { return false };
|
||||
let RawWindowHandle::Win32(w) = handle.as_raw() else { return false };
|
||||
let hwnd = w.hwnd.get() as HWND;
|
||||
// SAFETY: a plain query on the live main window.
|
||||
let dpi = unsafe { GetDpiForWindow(hwnd) };
|
||||
let (Some(big), Some(small)) = (exe_icon(SM_CXICON, dpi), exe_icon(SM_CXSMICON, dpi)) else { return false };
|
||||
// SAFETY: `hwnd` is the live main window and both icons are valid handles. The class keeps
|
||||
// them for the rest of the process, so they are never destroyed. `as _`: on 32-bit Windows
|
||||
// this is SetClassLongW (see the imports), which takes an i32.
|
||||
unsafe {
|
||||
SetClassLongPtrW(hwnd, GCLP_HICON, big as _);
|
||||
SetClassLongPtrW(hwnd, GCLP_HICONSM, small as _);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The exe's icon at the size of system metric `metric` (`SM_CXICON`, `SM_CXSMICON`) at `dpi`.
|
||||
fn exe_icon(metric: i32, dpi: u32) -> Option<windows_sys::Win32::Foundation::HANDLE> {
|
||||
// SAFETY: GetModuleHandleW(null) is this exe, loaded for the life of the process; the name
|
||||
// is an integer resource id (MAKEINTRESOURCE), which LoadImageW accepts in place of a string.
|
||||
let icon = unsafe {
|
||||
let size = GetSystemMetricsForDpi(metric, dpi);
|
||||
LoadImageW(GetModuleHandleW(std::ptr::null()), std::ptr::without_provenance(ICON_RESOURCE), IMAGE_ICON, size, size, LR_DEFAULTCOLOR)
|
||||
};
|
||||
(!icon.is_null()).then_some(icon)
|
||||
}
|
||||
|
||||
/// The window procedure ours replaced (winit's), as an address; 0 before [`install`].
|
||||
static PREVIOUS: AtomicIsize = AtomicIsize::new(0);
|
||||
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
//! macOS system menus, owned and updated on eframe's main thread.
|
||||
|
||||
use std::sync::mpsc::{Receiver, sync_channel};
|
||||
|
||||
use muda::{
|
||||
AboutMetadata, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu,
|
||||
accelerator::{Accelerator, Code, Modifiers},
|
||||
};
|
||||
use solidcraft_ui_egui::{SolveApp, toolbar::FileAction};
|
||||
|
||||
pub struct NativeMenu {
|
||||
menu: Menu,
|
||||
actions: Vec<(MenuItem, FileAction)>,
|
||||
help_actions: Vec<(MenuItem, &'static str)>,
|
||||
events: Receiver<MenuEvent>,
|
||||
recent: Submenu,
|
||||
recent_paths: Vec<String>,
|
||||
}
|
||||
|
||||
impl NativeMenu {
|
||||
pub fn new(app: &SolveApp, ctx: &egui::Context) -> muda::Result<Self> {
|
||||
let menu = Menu::new();
|
||||
let application = Submenu::new("SolidCraft", true);
|
||||
let file = Submenu::new(app.tr("ui.file"), true);
|
||||
let edit = Submenu::new(app.tr_label("Edit"), true);
|
||||
let recent = Submenu::new(app.tr("ui.open_recent"), true);
|
||||
let window = Submenu::new("Window", true);
|
||||
let help = Submenu::new(app.tr("ui.help"), true);
|
||||
let mut help_actions = Vec::new();
|
||||
let mut actions = Vec::new();
|
||||
let mut add = |parent: &Submenu, label: &str, action: FileAction, shortcut: Option<Accelerator>| -> muda::Result<()> {
|
||||
let item = MenuItem::new(label, Self::action_enabled(&action, app, ctx), shortcut);
|
||||
parent.append(&item)?;
|
||||
actions.push((item, action));
|
||||
Ok(())
|
||||
};
|
||||
application.append(&PredefinedMenuItem::about(
|
||||
Some("About SolidCraft"),
|
||||
Some(AboutMetadata { name: Some("SolidCraft".into()), version: Some(env!("CARGO_PKG_VERSION").into()), ..Default::default() }),
|
||||
))?;
|
||||
application.append(&PredefinedMenuItem::separator())?;
|
||||
add(
|
||||
&application,
|
||||
&format!("{}\u{2026}", app.tr("ui.preferences")),
|
||||
FileAction::Settings,
|
||||
Some(Accelerator::new(Modifiers::META, Code::Comma)),
|
||||
)?;
|
||||
add(&application, &format!("{}\u{2026}", app.tr("ui.keyboard_shortcuts")), FileAction::Shortcuts, None)?;
|
||||
application.append(&PredefinedMenuItem::separator())?;
|
||||
application.append(&PredefinedMenuItem::services(None))?;
|
||||
application.append(&PredefinedMenuItem::separator())?;
|
||||
application.append_items(&[
|
||||
&PredefinedMenuItem::hide(Some("Hide SolidCraft")),
|
||||
&PredefinedMenuItem::hide_others(None),
|
||||
&PredefinedMenuItem::show_all(None),
|
||||
&PredefinedMenuItem::separator(),
|
||||
])?;
|
||||
add(&application, &format!("{} SolidCraft", app.tr("ui.quit")), FileAction::Quit, Some(Accelerator::new(Modifiers::META, Code::KeyQ)))?;
|
||||
add(&file, app.tr("ui.new_design"), FileAction::New, Some(Accelerator::new(Modifiers::META, Code::KeyN)))?;
|
||||
add(&file, &format!("{}\u{2026}", app.tr("ui.open")), FileAction::Open, Some(Accelerator::new(Modifiers::META, Code::KeyO)))?;
|
||||
file.append(&recent)?;
|
||||
file.append(&PredefinedMenuItem::separator())?;
|
||||
for (label, command) in
|
||||
[("Insert STEP\u{2026}", "file.insert_step"), ("Insert Mesh\u{2026}", "file.insert_mesh"), ("Insert Design", "component.insert_design")]
|
||||
{
|
||||
add(&file, app.tr_label(label), FileAction::Command(command), None)?;
|
||||
}
|
||||
file.append(&PredefinedMenuItem::separator())?;
|
||||
add(&file, app.tr("ui.save"), FileAction::Save, Some(Accelerator::new(Modifiers::META, Code::KeyS)))?;
|
||||
add(
|
||||
&file,
|
||||
&format!("{}\u{2026}", app.tr("ui.save_as")),
|
||||
FileAction::SaveAs,
|
||||
Some(Accelerator::new(Modifiers::META | Modifiers::SHIFT, Code::KeyS)),
|
||||
)?;
|
||||
add(&file, app.tr_label("Save As JSON\u{2026}"), FileAction::SaveJson, None)?;
|
||||
file.append(&PredefinedMenuItem::separator())?;
|
||||
for (label, action) in [
|
||||
("Export STL\u{2026}", FileAction::ExportMesh("stl")),
|
||||
("Export 3MF\u{2026}", FileAction::ExportMesh("3mf")),
|
||||
("Export STEP\u{2026}", FileAction::Export("step")),
|
||||
("Export Current View as DXF\u{2026}", FileAction::Command("file.export_view_dxf")),
|
||||
("Export OBJ\u{2026}", FileAction::Export("obj")),
|
||||
("Export URDF\u{2026}", FileAction::Export("urdf")),
|
||||
("Export Design as JSON\u{2026}", FileAction::Export("json")),
|
||||
] {
|
||||
add(&file, app.tr_label(label), action, None)?;
|
||||
}
|
||||
file.append(&PredefinedMenuItem::separator())?;
|
||||
add(&file, app.tr_label("Design Properties"), FileAction::Command("document.metadata"), None)?;
|
||||
add(&edit, app.tr("ui.undo"), FileAction::Undo, Some(Accelerator::new(Modifiers::META, Code::KeyZ)))?;
|
||||
add(&edit, app.tr("ui.redo"), FileAction::Redo, Some(Accelerator::new(Modifiers::META | Modifiers::SHIFT, Code::KeyZ)))?;
|
||||
add(&window, app.tr_label("Agent Chat"), FileAction::AgentChat, None)?;
|
||||
window.append(&PredefinedMenuItem::separator())?;
|
||||
window.append_items(&[
|
||||
&PredefinedMenuItem::minimize(None),
|
||||
&PredefinedMenuItem::maximize(None),
|
||||
&PredefinedMenuItem::fullscreen(None),
|
||||
&PredefinedMenuItem::separator(),
|
||||
&PredefinedMenuItem::bring_all_to_front(None),
|
||||
])?;
|
||||
for (action, label) in [
|
||||
("docs", "Documentation"),
|
||||
("shortcuts", "Keyboard Shortcuts"),
|
||||
("messages", "Messages\u{2026}"),
|
||||
("report", "Report an Issue\u{2026}"),
|
||||
("crashes", "Crash Reports\u{2026}"),
|
||||
("install_cli", "Install solidcraft-cli command"),
|
||||
] {
|
||||
let item = MenuItem::new(app.tr_label(label), true, None);
|
||||
help.append(&item)?;
|
||||
help_actions.push((item, action));
|
||||
}
|
||||
menu.append_items(&[&application, &file, &edit, &window, &help])?;
|
||||
let (sender, events) = sync_channel(64);
|
||||
let ctx = ctx.clone();
|
||||
MenuEvent::set_event_handler(Some(move |event| {
|
||||
let _ = sender.try_send(event);
|
||||
ctx.request_repaint();
|
||||
}));
|
||||
menu.init_for_nsapp();
|
||||
let mut native = Self { menu, actions, help_actions, events, recent, recent_paths: Vec::new() };
|
||||
native.update_recent(app)?;
|
||||
Ok(native)
|
||||
}
|
||||
|
||||
pub fn raw_input_hook(&mut self, app: &mut SolveApp, ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
for event in self.events.try_iter().take(64) {
|
||||
if let Some((_, action)) = self.help_actions.iter().find(|(item, _)| item.id() == &event.id) {
|
||||
solidcraft_ui_egui::help::run(app, ctx, action);
|
||||
}
|
||||
if let Some((_, action)) = self.actions.iter().find(|(item, _)| item.id() == &event.id) {
|
||||
if matches!(action, FileAction::Undo | FileAction::Redo) && ctx.egui_wants_keyboard_input() {
|
||||
raw.events.push(egui::Event::Key {
|
||||
key: egui::Key::Z,
|
||||
physical_key: None,
|
||||
pressed: true,
|
||||
repeat: false,
|
||||
modifiers: egui::Modifiers { command: true, mac_cmd: true, shift: matches!(action, FileAction::Redo), ..Default::default() },
|
||||
});
|
||||
} else {
|
||||
action.run(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn action_enabled(action: &FileAction, app: &SolveApp, ctx: &egui::Context) -> bool {
|
||||
action.enabled(app)
|
||||
|| (matches!(action, FileAction::Undo | FileAction::Redo) && ctx.egui_wants_keyboard_input() && FileAction::Settings.enabled(app))
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
|
||||
for (item, action) in &self.actions {
|
||||
let enabled = Self::action_enabled(action, app, ctx);
|
||||
if item.is_enabled() != enabled {
|
||||
item.set_enabled(enabled);
|
||||
}
|
||||
}
|
||||
if !self.recent_paths.iter().eq(app.home.recent.iter().take(32))
|
||||
&& let Err(error) = self.update_recent(app)
|
||||
{
|
||||
app.set_status(format!("Recent designs menu: {error}"), true);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_recent(&mut self, app: &SolveApp) -> muda::Result<()> {
|
||||
while self.recent.remove_at(0).is_some() {}
|
||||
self.actions.retain(|(_, action)| !matches!(action, FileAction::OpenRecent(_) | FileAction::ClearRecent));
|
||||
self.recent_paths = app.home.recent.iter().take(32).cloned().collect();
|
||||
for path in &self.recent_paths {
|
||||
let action = FileAction::OpenRecent(path.clone());
|
||||
let item = MenuItem::new(path, action.enabled(app), None);
|
||||
self.recent.append(&item)?;
|
||||
self.actions.push((item, action));
|
||||
}
|
||||
self.recent.append(&PredefinedMenuItem::separator())?;
|
||||
let clear = MenuItem::new(app.tr("ui.clear_recent"), !self.recent_paths.is_empty(), None);
|
||||
self.recent.append(&clear)?;
|
||||
self.actions.push((clear, FileAction::ClearRecent));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NativeMenu {
|
||||
fn drop(&mut self) {
|
||||
MenuEvent::set_event_handler(None::<fn(MenuEvent)>);
|
||||
self.menu.remove_for_nsapp();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
//! The navigation bridge: a 3D input device's own program (a homemade device's driver app) moves
|
||||
//! the view over a loopback connection, the way a 3D mouse driver hands its axes to the programs
|
||||
//! it supports, so the device emulates no mouse and the pointer stays the user's.
|
||||
//!
|
||||
//! The device app listens on `127.0.0.1:<port>`; with Preferences › Navigation › "Navigate with a
|
||||
//! device app" on (off by default), SolidCraft connects to it, says hello and reads one JSON
|
||||
//! object per line (docs/3d-mouse.md). The reader thread only sums the motion; the egui thread
|
||||
//! hands it to the view once per frame (`space_mouse::bridge_frame`). A device the app says it
|
||||
//! reads is left to it by the 3D mouse reader ([`Claim`]), so the view doesn't move twice.
|
||||
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::net::{Ipv4Addr, SocketAddr, TcpStream};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::time::Duration;
|
||||
|
||||
use egui::Context;
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_ui_egui::SolveApp;
|
||||
use solidcraft_ui_egui::space_mouse::{BRIDGE_VIEWS, BridgeMotion, bridge_frame, bridge_step};
|
||||
|
||||
/// The longest line read; a longer one is skipped to its end.
|
||||
const MAX_LINE: usize = 4096;
|
||||
/// Standard views waiting for the next frame; past this many, more are a flood and dropped.
|
||||
const MAX_VIEWS: usize = 8;
|
||||
/// The longest name a device app may give for itself or its device, in characters.
|
||||
const MAX_NAME: usize = 64;
|
||||
/// How long a connection attempt waits.
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
/// How long a read waits for data before the reader looks at the preferences again.
|
||||
const READ_WAKE: Duration = Duration::from_millis(250);
|
||||
/// The wait before connecting again when no device app answered or one went away.
|
||||
const RETRY_AFTER: Duration = Duration::from_secs(1);
|
||||
/// In zoom mode, a motion of this many points along Y is one zoom step: with 8 % a step, the 1 %
|
||||
/// a point that a zoom drag moves.
|
||||
const POINTS_PER_ZOOM_STEP: f64 = 8.0;
|
||||
|
||||
/// What the device's motion drives, as the device app last said (`mode`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Orbit,
|
||||
Pan,
|
||||
Zoom,
|
||||
}
|
||||
|
||||
/// What the reader thread and the egui thread share.
|
||||
#[derive(Debug, Default)]
|
||||
struct Shared {
|
||||
/// The port the preferences ask to connect to; none while the bridge is off.
|
||||
wanted: Option<u16>,
|
||||
/// The device app connected, by the name it gave ("?" until it gives one).
|
||||
connected: Option<String>,
|
||||
/// The device the connected app reads, as it named it: the 3D mouse reader leaves that one alone.
|
||||
device: Option<String>,
|
||||
/// Motion since the egui thread last took it.
|
||||
motion: BridgeMotion,
|
||||
/// Standard views asked for since then.
|
||||
views: Vec<String>,
|
||||
}
|
||||
|
||||
fn lock(shared: &Mutex<Shared>) -> Option<MutexGuard<'_, Shared>> {
|
||||
shared.lock().ok()
|
||||
}
|
||||
|
||||
/// A name a device app gave, without control characters and cut to [`MAX_NAME`]; none when empty.
|
||||
fn name(value: Option<&Value>) -> Option<String> {
|
||||
let name: String = value?.as_str()?.chars().filter(|c| !c.is_control()).take(MAX_NAME).collect();
|
||||
let name = name.trim();
|
||||
(!name.is_empty()).then(|| name.to_string())
|
||||
}
|
||||
|
||||
/// A number of a message, finite and in range; none when it isn't one.
|
||||
fn number(msg: &Value, key: &str) -> f64 {
|
||||
bridge_step(msg.get(key).and_then(Value::as_f64).unwrap_or(0.0))
|
||||
}
|
||||
|
||||
/// Takes one line from the device app into `shared`. Lines that aren't a message this version
|
||||
/// knows are ignored. Returns whether the view has something new to show.
|
||||
fn consume(line: &[u8], mode: &mut Mode, shared: &mut Shared) -> bool {
|
||||
let Ok(msg) = serde_json::from_slice::<Value>(line) else { return false };
|
||||
let add = |shared: &mut Shared, motion: BridgeMotion| {
|
||||
shared.motion = shared.motion.plus(motion);
|
||||
!motion.is_still()
|
||||
};
|
||||
match msg.get("t").and_then(Value::as_str) {
|
||||
Some("axis") => {
|
||||
let (x, y) = (number(&msg, "x"), number(&msg, "y"));
|
||||
let motion = match mode {
|
||||
Mode::Orbit => BridgeMotion { orbit: [x, y], ..Default::default() },
|
||||
Mode::Pan => BridgeMotion { pan: [x, y], ..Default::default() },
|
||||
// Up zooms in, as a zoom drag does.
|
||||
Mode::Zoom => BridgeMotion { zoom: -y / POINTS_PER_ZOOM_STEP, ..Default::default() },
|
||||
};
|
||||
add(shared, motion)
|
||||
}
|
||||
Some("zoom") => add(shared, BridgeMotion { zoom: number(&msg, "d"), ..Default::default() }),
|
||||
Some("mode") => {
|
||||
let on = msg.get("on").and_then(Value::as_bool).unwrap_or(false);
|
||||
*mode = match msg.get("mode").and_then(Value::as_str) {
|
||||
Some("pan") if on => Mode::Pan,
|
||||
Some("zoom") if on => Mode::Zoom,
|
||||
_ => Mode::Orbit,
|
||||
};
|
||||
false
|
||||
}
|
||||
Some("view") => {
|
||||
let view = msg.get("name").and_then(Value::as_str).and_then(|n| BRIDGE_VIEWS.iter().find(|v| **v == n));
|
||||
match view {
|
||||
Some(view) if shared.views.len() < MAX_VIEWS => {
|
||||
shared.views.push((*view).to_string());
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
Some("hello") => {
|
||||
shared.connected = name(msg.get("server")).or_else(|| shared.connected.clone());
|
||||
shared.device = name(msg.get("device"));
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits what arrives into lines, across reads. A line longer than [`MAX_LINE`] is dropped up to
|
||||
/// its newline, so a peer that never sends one can't make the reader hold more than that.
|
||||
#[derive(Default)]
|
||||
struct Lines {
|
||||
line: Vec<u8>,
|
||||
skipping: bool,
|
||||
}
|
||||
|
||||
impl Lines {
|
||||
fn feed(&mut self, mut data: &[u8], mut each: impl FnMut(&[u8])) {
|
||||
while !data.is_empty() {
|
||||
let end = data.iter().position(|b| *b == b'\n');
|
||||
let (part, rest) = match end {
|
||||
Some(i) => (data.get(..i).unwrap_or_default(), data.get(i + 1..).unwrap_or_default()),
|
||||
None => (data, &[][..]),
|
||||
};
|
||||
if !self.skipping {
|
||||
let room = (MAX_LINE + 1).saturating_sub(self.line.len());
|
||||
self.line.extend_from_slice(part.get(..room.min(part.len())).unwrap_or_default());
|
||||
if self.line.len() > MAX_LINE {
|
||||
self.line.clear();
|
||||
self.skipping = true;
|
||||
}
|
||||
}
|
||||
if end.is_some() {
|
||||
if !self.skipping {
|
||||
each(self.line.strip_suffix(b"\r").unwrap_or(&self.line));
|
||||
}
|
||||
self.line.clear();
|
||||
self.skipping = false;
|
||||
}
|
||||
data = rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What SolidCraft says first, so the device app knows which program it is driving.
|
||||
fn hello() -> Value {
|
||||
json!({"t": "hello", "app": "solidcraft", "version": env!("CARGO_PKG_VERSION")})
|
||||
}
|
||||
|
||||
/// Reads one connection until the device app closes it, it fails, or the preferences no longer
|
||||
/// ask for this port; then forgets what it had.
|
||||
fn serve(mut stream: TcpStream, port: u16, ctx: &Context, shared: &Mutex<Shared>) {
|
||||
// Without a read timeout the reader couldn't notice the bridge being switched off.
|
||||
if stream.set_read_timeout(Some(READ_WAKE)).is_err() || writeln!(stream, "{}", hello()).is_err() {
|
||||
return;
|
||||
}
|
||||
let _ = stream.set_nodelay(true);
|
||||
log::info!("navigation bridge: connected to 127.0.0.1:{port}");
|
||||
if let Some(mut shared) = lock(shared) {
|
||||
shared.connected = Some("?".into());
|
||||
}
|
||||
ctx.request_repaint();
|
||||
let (mut lines, mut mode) = (Lines::default(), Mode::default());
|
||||
let mut chunk = [0_u8; 4096];
|
||||
while lock(shared).is_some_and(|shared| shared.wanted == Some(port)) {
|
||||
match stream.read(&mut chunk) {
|
||||
Ok(0) => break,
|
||||
Ok(size) => {
|
||||
let mut news = false;
|
||||
if let Some(mut shared) = lock(shared) {
|
||||
lines.feed(chunk.get(..size).unwrap_or_default(), |line| news |= consume(line, &mut mode, &mut shared));
|
||||
}
|
||||
if news {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut | ErrorKind::Interrupted) => {}
|
||||
Err(error) => {
|
||||
log::info!("navigation bridge: connection lost: {error}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(mut shared) = lock(shared) {
|
||||
*shared = Shared { wanted: shared.wanted, ..Shared::default() };
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
/// Connects to the device app while the preferences ask for it, again whenever it goes away.
|
||||
fn reader(ctx: Context, shared: Arc<Mutex<Shared>>) {
|
||||
loop {
|
||||
let Some(port) = lock(&shared).and_then(|shared| shared.wanted) else {
|
||||
std::thread::sleep(READ_WAKE);
|
||||
continue;
|
||||
};
|
||||
if let Ok(stream) = TcpStream::connect_timeout(&SocketAddr::from((Ipv4Addr::LOCALHOST, port)), CONNECT_TIMEOUT) {
|
||||
serve(stream, port, &ctx, &shared);
|
||||
}
|
||||
std::thread::sleep(RETRY_AFTER);
|
||||
}
|
||||
}
|
||||
|
||||
/// Which 3D mouse the connected device app reads, for the 3D mouse reader to leave alone. Only
|
||||
/// the Windows and macOS readers know a device's strings to compare.
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
|
||||
pub struct Claim(Arc<Mutex<Shared>>);
|
||||
|
||||
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
|
||||
impl Claim {
|
||||
/// Whether a device with these manufacturer and product strings is the one the connected
|
||||
/// device app reads (its name appears in one, ignoring case).
|
||||
pub fn claims(&self, strings: &[String]) -> bool {
|
||||
let Some(device) = lock(&self.0).and_then(|shared| shared.device.clone()) else { return false };
|
||||
let device = device.to_lowercase();
|
||||
strings.iter().any(|s| s.to_lowercase().contains(&device))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NavBridge {
|
||||
shared: Arc<Mutex<Shared>>,
|
||||
}
|
||||
|
||||
impl NavBridge {
|
||||
pub fn start(ctx: &Context) -> Self {
|
||||
let shared = Arc::new(Mutex::new(Shared::default()));
|
||||
let (worker_ctx, worker_state) = (ctx.clone(), Arc::clone(&shared));
|
||||
if let Err(error) = std::thread::Builder::new().name("solidcraft-nav-bridge".into()).spawn(move || reader(worker_ctx, worker_state)) {
|
||||
log::warn!("navigation bridge: reader could not start: {error}");
|
||||
}
|
||||
Self { shared }
|
||||
}
|
||||
|
||||
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
|
||||
pub fn claim(&self) -> Claim {
|
||||
Claim(Arc::clone(&self.shared))
|
||||
}
|
||||
|
||||
/// Each frame: passes the preferences to the reader, and the motion and views since the last
|
||||
/// frame to the view. Motion that arrived while the window had no focus is dropped with it.
|
||||
pub fn tick(&mut self, app: &mut SolveApp, ctx: &Context) {
|
||||
let wanted = Some(app.preferences.nav_bridge_port).filter(|port| app.preferences.nav_bridge_enabled && *port != 0);
|
||||
let Some((motion, views, connected)) = lock(&self.shared).map(|mut shared| {
|
||||
shared.wanted = wanted;
|
||||
(std::mem::take(&mut shared.motion), std::mem::take(&mut shared.views), shared.connected.clone())
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
app.space_mouse.bridge = connected.filter(|_| wanted.is_some());
|
||||
if !motion.is_still() || !views.is_empty() {
|
||||
bridge_frame(app, ctx, motion, &views);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::net::TcpListener;
|
||||
use std::time::Instant;
|
||||
|
||||
fn feed(shared: &mut Shared, mode: &mut Mode, text: &str) -> bool {
|
||||
let mut news = false;
|
||||
Lines::default().feed(text.as_bytes(), |line| news |= consume(line, mode, shared));
|
||||
news
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_motion_orbits_pans_or_zooms_as_the_mode_says() {
|
||||
let (mut shared, mut mode) = (Shared::default(), Mode::default());
|
||||
assert!(feed(&mut shared, &mut mode, "{\"t\":\"axis\",\"x\":3,\"y\":-2}\n{\"t\":\"axis\",\"x\":1.5,\"y\":0}\n"));
|
||||
assert_eq!(shared.motion, BridgeMotion { orbit: [4.5, -2.0], ..Default::default() });
|
||||
feed(&mut shared, &mut mode, "{\"t\":\"mode\",\"mode\":\"pan\",\"on\":true}\n{\"t\":\"axis\",\"x\":5,\"y\":6}\n");
|
||||
assert_eq!(mode, Mode::Pan);
|
||||
assert_eq!(shared.motion.pan, [5.0, 6.0]);
|
||||
feed(&mut shared, &mut mode, "{\"t\":\"mode\",\"mode\":\"zoom\",\"on\":true}\n{\"t\":\"axis\",\"x\":0,\"y\":-16}\n");
|
||||
assert_eq!(shared.motion.zoom, 2.0, "up zooms in, 8 points a step");
|
||||
// Off, whichever mode it names, is back to orbit.
|
||||
feed(&mut shared, &mut mode, "{\"t\":\"mode\",\"mode\":\"pan\",\"on\":false}\n");
|
||||
assert_eq!(mode, Mode::Orbit);
|
||||
// The ring's steps add up with the zoom mode's.
|
||||
feed(&mut shared, &mut mode, "{\"t\":\"zoom\",\"d\":-0.5}\n");
|
||||
assert_eq!(shared.motion.zoom, 1.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn views_are_queued_by_their_known_names_only() {
|
||||
let (mut shared, mut mode) = (Shared::default(), Mode::default());
|
||||
assert!(feed(&mut shared, &mut mode, "{\"t\":\"view\",\"name\":\"iso\"}\n{\"t\":\"view\",\"name\":\"fit\"}\n"));
|
||||
assert!(!feed(&mut shared, &mut mode, "{\"t\":\"view\",\"name\":\"ISO\"}\n{\"t\":\"view\",\"name\":\"../etc\"}\n{\"t\":\"view\"}\n"));
|
||||
assert_eq!(shared.views, ["iso", "fit"]);
|
||||
let flood = "{\"t\":\"view\",\"name\":\"top\"}\n".repeat(100);
|
||||
feed(&mut shared, &mut mode, &flood);
|
||||
assert_eq!(shared.views.len(), MAX_VIEWS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_lines_never_move_the_view_out_of_range() {
|
||||
let (mut shared, mut mode) = (Shared::default(), Mode::default());
|
||||
let lines = [
|
||||
"not json",
|
||||
"{\"t\":\"axis\",\"x\":\"5\",\"y\":null}",
|
||||
"{\"t\":\"axis\",\"x\":1e308,\"y\":-1e308}",
|
||||
"{\"t\":\"axis\",\"x\":1e999}",
|
||||
"{\"t\":\"zoom\",\"d\":[1]}",
|
||||
"{\"t\":42}",
|
||||
"[1,2,3]",
|
||||
"{\"t\":\"mode\",\"mode\":{\"x\":1},\"on\":\"yes\"}",
|
||||
"\u{0}\u{1}\u{2}",
|
||||
];
|
||||
for line in lines {
|
||||
Lines::default().feed(format!("{line}\n").as_bytes(), |l| {
|
||||
consume(l, &mut mode, &mut shared);
|
||||
});
|
||||
}
|
||||
let limit = solidcraft_ui_egui::space_mouse::BRIDGE_MAX_STEP;
|
||||
let values = shared.motion.orbit.into_iter().chain(shared.motion.pan).chain([shared.motion.zoom]);
|
||||
for value in values {
|
||||
assert!(value.is_finite() && value.abs() <= limit, "{value}");
|
||||
}
|
||||
assert_eq!(mode, Mode::Orbit);
|
||||
// Invalid UTF-8 is no message either.
|
||||
assert!(!consume(&[0xff, 0xfe, b'{', b'}'], &mut mode, &mut shared));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lines_split_across_reads_and_long_ones_are_skipped_to_their_end() {
|
||||
let mut got = Vec::new();
|
||||
let mut lines = Lines::default();
|
||||
for part in ["{\"t\":", "\"zoom\"}\r\n{\"a\"", ":1}\n"] {
|
||||
lines.feed(part.as_bytes(), |line| got.push(String::from_utf8_lossy(line).to_string()));
|
||||
}
|
||||
assert_eq!(got, ["{\"t\":\"zoom\"}", "{\"a\":1}"]);
|
||||
got.clear();
|
||||
let long = vec![b'x'; MAX_LINE * 3];
|
||||
lines.feed(&long, |line| got.push(String::from_utf8_lossy(line).to_string()));
|
||||
lines.feed(&long, |line| got.push(String::from_utf8_lossy(line).to_string()));
|
||||
assert!(lines.line.len() <= MAX_LINE + 1, "never holds more than one line");
|
||||
lines.feed(b"tail\nnext\n", |line| got.push(String::from_utf8_lossy(line).to_string()));
|
||||
assert_eq!(got, ["next"], "the long line is dropped to its newline, the next one read");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_device_app_names_itself_and_the_device_it_leaves_alone() {
|
||||
let shared = Arc::new(Mutex::new(Shared::default()));
|
||||
let claim = Claim(Arc::clone(&shared));
|
||||
let strings = ["STMicroelectronics".to_string(), "MAKMOUSE 3D".to_string()];
|
||||
assert!(!claim.claims(&strings), "nothing is claimed before an app says so");
|
||||
let mut mode = Mode::default();
|
||||
if let Ok(mut shared) = shared.lock() {
|
||||
let hello = format!("{{\"t\":\"hello\",\"server\":\"Dev\\u0007App {}\",\"device\":\"makmouse\"}}\n", "x".repeat(200));
|
||||
feed(&mut shared, &mut mode, &hello);
|
||||
assert_eq!(shared.connected.as_deref().map(|n| n.chars().count()), Some(MAX_NAME));
|
||||
assert!(shared.connected.as_deref().is_some_and(|n| n.starts_with("DevApp")));
|
||||
}
|
||||
assert!(claim.claims(&strings), "the name matches ignoring case");
|
||||
assert!(!claim.claims(&["3Dconnexion".to_string(), "SpaceMouse Compact".to_string()]), "another 3D mouse stays the reader's");
|
||||
}
|
||||
|
||||
/// Waits until `check` holds, or fails after a few seconds.
|
||||
fn wait_for(what: &str, mut check: impl FnMut() -> bool) {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while !check() {
|
||||
assert!(Instant::now() < deadline, "timed out waiting for {what}");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_connection_says_hello_reads_the_stream_and_ends_when_the_bridge_is_switched_off() {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let shared = Arc::new(Mutex::new(Shared { wanted: Some(port), ..Default::default() }));
|
||||
let client = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
|
||||
let (mut server, _) = listener.accept().unwrap();
|
||||
let worker = {
|
||||
let shared = Arc::clone(&shared);
|
||||
std::thread::spawn(move || serve(client, port, &Context::default(), &shared))
|
||||
};
|
||||
let mut hello_line = String::new();
|
||||
BufReader::new(server.try_clone().unwrap()).read_line(&mut hello_line).unwrap();
|
||||
let hello: Value = serde_json::from_str(&hello_line).unwrap();
|
||||
assert_eq!((hello["t"].as_str(), hello["app"].as_str()), (Some("hello"), Some("solidcraft")));
|
||||
server
|
||||
.write_all(b"{\"t\":\"hello\",\"server\":\"MAKMOUSE\",\"device\":\"MAKMOUSE\"}\n{\"t\":\"axis\",\"x\":2,\"y\":1}\n{\"t\":\"view\",\"name\":\"top\"}\n")
|
||||
.unwrap();
|
||||
wait_for("the stream", || shared.lock().is_ok_and(|s| s.motion.orbit == [2.0, 1.0] && s.views == ["top"]));
|
||||
assert_eq!(shared.lock().unwrap().connected.as_deref(), Some("MAKMOUSE"));
|
||||
// Switched off: the connection ends and what it had is forgotten.
|
||||
shared.lock().unwrap().wanted = None;
|
||||
worker.join().unwrap();
|
||||
let after = shared.lock().unwrap();
|
||||
assert!(after.connected.is_none() && after.device.is_none() && after.motion.is_still() && after.views.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_connection_forgets_the_app() {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let shared = Arc::new(Mutex::new(Shared { wanted: Some(port), ..Default::default() }));
|
||||
let client = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
|
||||
let (server, _) = listener.accept().unwrap();
|
||||
let worker = {
|
||||
let shared = Arc::clone(&shared);
|
||||
std::thread::spawn(move || serve(client, port, &Context::default(), &shared))
|
||||
};
|
||||
wait_for("the connection", || shared.lock().is_ok_and(|s| s.connected.is_some()));
|
||||
drop(server);
|
||||
worker.join().unwrap();
|
||||
let after = shared.lock().unwrap();
|
||||
assert_eq!((after.connected.as_deref(), after.wanted), (None, Some(port)), "still wanted, so the reader connects again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_frame_hands_the_view_the_motion_and_passes_the_preferences_on() {
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), solidcraft_ui_egui::Services::default());
|
||||
app.home.open = false;
|
||||
let ctx = Context::default();
|
||||
let _ = ctx.run_ui(egui::RawInput { focused: true, ..Default::default() }, |_| {});
|
||||
let mut bridge = NavBridge { shared: Arc::new(Mutex::new(Shared::default())) };
|
||||
let queue = |bridge: &NavBridge| {
|
||||
let mut shared = bridge.shared.lock().unwrap();
|
||||
shared.connected = Some("MAKMOUSE".into());
|
||||
shared.motion = BridgeMotion { orbit: [10.0, 0.0], ..Default::default() };
|
||||
};
|
||||
// Off (the default): nothing is wanted, nothing reaches the view.
|
||||
queue(&bridge);
|
||||
bridge.tick(&mut app, &ctx);
|
||||
assert_eq!(bridge.shared.lock().unwrap().wanted, None);
|
||||
assert!(app.viewport.bridge_motion.is_none() && app.space_mouse.bridge.is_none());
|
||||
// On: the port is passed to the reader and the motion to the view, once.
|
||||
app.preferences.nav_bridge_enabled = true;
|
||||
app.preferences.nav_bridge_port = 9123;
|
||||
queue(&bridge);
|
||||
bridge.tick(&mut app, &ctx);
|
||||
assert_eq!(bridge.shared.lock().unwrap().wanted, Some(9123));
|
||||
assert_eq!(app.space_mouse.bridge.as_deref(), Some("MAKMOUSE"));
|
||||
assert_eq!(app.viewport.bridge_motion.map(|m| m.orbit), Some([10.0, 0.0]));
|
||||
assert!(bridge.shared.lock().unwrap().motion.is_still(), "taken, not handed over twice");
|
||||
// Port 0 is no port.
|
||||
app.preferences.nav_bridge_port = 0;
|
||||
bridge.tick(&mut app, &ctx);
|
||||
assert_eq!(bridge.shared.lock().unwrap().wanted, None);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,12 @@ const DEV: &str = "/dev";
|
||||
/// a read with nothing to read returns at once and the reader can see whether it should stop.
|
||||
pub const O_NONBLOCK: i32 = 0o4000;
|
||||
|
||||
/// Opens a hidraw node the way the Direct reader reads it: read-only and non-blocking.
|
||||
pub fn open(path: &Path) -> std::io::Result<std::fs::File> {
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
std::fs::OpenOptions::new().read(true).custom_flags(O_NONBLOCK).open(path)
|
||||
}
|
||||
|
||||
/// How long the reader waits when the device has nothing to say, which is also the most a report
|
||||
/// can be delayed.
|
||||
const IDLE_POLL: Duration = Duration::from_millis(5);
|
||||
@@ -76,6 +82,44 @@ fn ids(uevent: &str) -> Option<(u16, u16)> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A real EACCES, as a user without the udev rule gets: a file of the user's own with no permission
|
||||
/// bits. Skipped for root, which can open anything.
|
||||
#[test]
|
||||
fn a_device_without_permission_is_denied_and_the_daemon_takes_over_within_the_backoff() {
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
if std::fs::metadata("/proc/self").is_ok_and(|meta| meta.uid() == 0) {
|
||||
return;
|
||||
}
|
||||
let dir = std::env::temp_dir().join(format!("sc-hidraw-denied-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let node = dir.join("hidraw0");
|
||||
std::fs::write(&node, [0_u8; 4]).unwrap();
|
||||
std::fs::set_permissions(&node, std::fs::Permissions::from_mode(0o000)).unwrap();
|
||||
let denied = open(&node);
|
||||
std::fs::set_permissions(&node, std::fs::Permissions::from_mode(0o600)).unwrap();
|
||||
assert!(open(&node).is_ok(), "the same node opens once it is readable");
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
assert_eq!(denied.err().map(|error| error.kind()), Some(std::io::ErrorKind::PermissionDenied));
|
||||
// The failure is the choice's `since_failed`, with an injected clock: inside the backoff the
|
||||
// daemon is read, whichever driver was asked for; after it the device is tried again.
|
||||
use super::super::{Pick, SpaceMouseDriver, UNREADABLE_RETRY, choose, direct_usable};
|
||||
for wanted in [SpaceMouseDriver::Automatic, SpaceMouseDriver::Spacenavd, SpaceMouseDriver::Direct] {
|
||||
for ago in [Duration::ZERO, Duration::from_secs(29)] {
|
||||
assert_eq!(choose(wanted, true, direct_usable(true, Some(ago), UNREADABLE_RETRY)), Some(Pick::Spacenavd), "{wanted:?} {ago:?}");
|
||||
}
|
||||
}
|
||||
assert_eq!(choose(SpaceMouseDriver::Direct, true, direct_usable(true, Some(UNREADABLE_RETRY), UNREADABLE_RETRY)), Some(Pick::Direct));
|
||||
// And the daemon's own reader delivers over a fake connection.
|
||||
let (mut daemon, client) = std::os::unix::net::UnixStream::pair().unwrap();
|
||||
let mut record = [0_u8; 32];
|
||||
record[4..8].copy_from_slice(&100_i32.to_ne_bytes());
|
||||
std::io::Write::write_all(&mut daemon, &record).unwrap();
|
||||
drop(daemon);
|
||||
let mut seen = 0;
|
||||
super::super::spacenavd::read(client, |_| seen += 1, |_, _| {}, || false, Duration::from_secs(3600));
|
||||
assert_eq!(seen, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_come_through_idle_gaps_until_the_device_ends_or_a_stop() {
|
||||
use crate::space_mouse::fake::Script;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//! to its clients, so a program that reads the HID device itself bypasses that setup. Over its
|
||||
//! socket the daemon sends fixed 32-byte records: eight native-endian `i32`, the first the kind of
|
||||
//! record (0 for motion, then x y z rx ry rz and the milliseconds since the last record).
|
||||
//! Other records (button presses, from newer daemons also configuration changes) are ignored.
|
||||
//! Button records (1 pressed, 2 released, then the button's number from 0) are read too; other
|
||||
//! records (from newer daemons also configuration changes) are ignored.
|
||||
//!
|
||||
//! The socket is plain file I/O, so no library is needed, and it needs no udev rule: the daemon
|
||||
//! is what has access to the device.
|
||||
@@ -11,6 +12,7 @@
|
||||
use std::io::Read;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use solidcraft_ui_egui::space_mouse::{Axes, FULL_SCALE};
|
||||
|
||||
@@ -54,6 +56,22 @@ fn decode(record: &[u8; RECORD_BYTES]) -> Option<Axes> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The button a button record is about, from 0, and whether it went down; `None` for any other
|
||||
/// record, or a button past the 32 a bitmask holds.
|
||||
fn button(record: &[u8; RECORD_BYTES]) -> Option<(u32, bool)> {
|
||||
let mut data = [0_i32; 2];
|
||||
for (slot, bytes) in data.iter_mut().zip(record.as_chunks::<4>().0) {
|
||||
*slot = i32::from_ne_bytes(*bytes);
|
||||
}
|
||||
let [kind, number] = data;
|
||||
let number = u32::try_from(number).ok().filter(|&n| n < 32)?;
|
||||
match kind {
|
||||
1 => Some((number, true)),
|
||||
2 => Some((number, false)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A daemon value in our raw units. Out-of-range values stay in `i16`: they are input from another
|
||||
/// process.
|
||||
fn unit(value: impl Into<i64>) -> i16 {
|
||||
@@ -61,14 +79,30 @@ fn unit(value: impl Into<i64>) -> i16 {
|
||||
i16::try_from(scaled.clamp(i64::from(i16::MIN), i64::from(i16::MAX))).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Hands every motion record to `publish` until the daemon closes the connection or fails, or
|
||||
/// `stop` says so. A stream with a read timeout (`set_read_timeout`) times out when the daemon is
|
||||
/// quiet, which is no record and no disconnect: `stop` is asked, and the read goes on, with what
|
||||
/// of a record had arrived kept. A partial record at the end is dropped.
|
||||
pub fn read(mut stream: impl Read, mut publish: impl FnMut(Axes), mut stop: impl FnMut() -> bool) {
|
||||
/// Hands every motion record to `publish` and every button record to `pressed` until the daemon
|
||||
/// closes the connection or fails, or `stop` says so. A stream with a read timeout
|
||||
/// (`set_read_timeout`) times out when the daemon is quiet, which is no record and no disconnect:
|
||||
/// `stop` is asked, and the read goes on, with what of a record had arrived kept. `stop` is also asked
|
||||
/// at least every `check_every` while records keep arriving, so a daemon that never pauses for that
|
||||
/// long (a moving cap sends every few milliseconds) does not keep the reader from noticing that the
|
||||
/// device it prefers is back. A partial record at the end is dropped.
|
||||
pub fn read(
|
||||
mut stream: impl Read,
|
||||
mut publish: impl FnMut(Axes),
|
||||
mut pressed: impl FnMut(u32, bool),
|
||||
mut stop: impl FnMut() -> bool,
|
||||
check_every: Duration,
|
||||
) {
|
||||
let mut record = [0_u8; RECORD_BYTES];
|
||||
let mut filled = 0;
|
||||
let mut asked = Instant::now();
|
||||
loop {
|
||||
if asked.elapsed() >= check_every {
|
||||
asked = Instant::now();
|
||||
if stop() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
match stream.read(record.get_mut(filled..).unwrap_or_default()) {
|
||||
Ok(0) => {
|
||||
log::info!("3D mouse: spacenavd connection ended");
|
||||
@@ -80,11 +114,14 @@ pub fn read(mut stream: impl Read, mut publish: impl FnMut(Axes), mut stop: impl
|
||||
filled = 0;
|
||||
if let Some(axes) = decode(&record) {
|
||||
publish(axes);
|
||||
} else if let Some((number, down)) = button(&record) {
|
||||
pressed(number, down);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
|
||||
Err(error) if matches!(error.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => {
|
||||
asked = Instant::now();
|
||||
if stop() {
|
||||
return;
|
||||
}
|
||||
@@ -134,6 +171,10 @@ mod tests {
|
||||
fn other_records_and_hostile_values_are_harmless() {
|
||||
// A button record (kind 1, press) and one of an unknown kind are not motion.
|
||||
assert_eq!(decode(&record([1, 0, 1, 0, 0, 0, 0, 0])), None);
|
||||
// Nor are hostile button numbers a button.
|
||||
assert_eq!(button(&record([1, -1, 0, 0, 0, 0, 0, 0])), None);
|
||||
assert_eq!(button(&record([1, i32::MAX, 0, 0, 0, 0, 0, 0])), None);
|
||||
assert_eq!(button(&record([7, 3, 0, 0, 0, 0, 0, 0])), None);
|
||||
assert_eq!(decode(&record([7, 500, 500, 500, 500, 500, 500, 0])), None);
|
||||
// Nothing overflows on the extremes of `i32`.
|
||||
let extreme = decode(&record([0, i32::MIN, i32::MIN, i32::MAX, i32::MAX, i32::MIN, i32::MIN, -1]));
|
||||
@@ -147,14 +188,17 @@ mod tests {
|
||||
let (mut daemon, client) = UnixStream::pair().expect("a socket pair");
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend(record([0, 100, 0, 0, 0, 0, 0, 8]));
|
||||
bytes.extend(record([2, 1, 0, 0, 0, 0, 0, 0])); // a button release: skipped
|
||||
bytes.extend(record([1, 4, 0, 0, 0, 0, 0, 0])); // button 4 down
|
||||
bytes.extend(record([2, 4, 0, 0, 0, 0, 0, 0])); // and up
|
||||
bytes.extend(record([1, 40, 0, 0, 0, 0, 0, 0])); // past the 32 a bitmask holds: skipped
|
||||
bytes.extend(record([0, 0, 0, 0, 0, 0, 0, 900]));
|
||||
bytes.extend([1, 2, 3]); // the daemon went away mid-record
|
||||
daemon.write_all(&bytes).expect("written");
|
||||
drop(daemon);
|
||||
let mut seen = Vec::new();
|
||||
read(client, |axes| seen.push(axes.translation), || false);
|
||||
let (mut seen, mut buttons) = (Vec::new(), Vec::new());
|
||||
read(client, |axes| seen.push(axes.translation), |number, down| buttons.push((number, down)), || false, Duration::from_secs(3600));
|
||||
assert_eq!(seen, [[70, 0, 0], [0, 0, 0]]);
|
||||
assert_eq!(buttons, [(4, true), (4, false)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -168,10 +212,12 @@ mod tests {
|
||||
read(
|
||||
script,
|
||||
|axes| seen.push(axes.translation),
|
||||
|_, _| {},
|
||||
|| {
|
||||
asked += 1;
|
||||
false
|
||||
},
|
||||
Duration::from_secs(3600),
|
||||
);
|
||||
assert_eq!(seen, [[70, 0, 0]]);
|
||||
assert_eq!(asked, 4, "each timeout asks whether to stop");
|
||||
@@ -184,10 +230,44 @@ mod tests {
|
||||
let mut script = Script::new();
|
||||
script.data(&bytes).timeout(std::io::ErrorKind::WouldBlock).data(&bytes);
|
||||
let mut seen = 0;
|
||||
read(script, |_| seen += 1, || true);
|
||||
read(script, |_| seen += 1, |_, _| {}, || true, Duration::from_secs(3600));
|
||||
assert_eq!(seen, 1, "the record before the timeout arrived; the one after never was read");
|
||||
}
|
||||
|
||||
/// A daemon that never goes quiet (a moving cap sends every few milliseconds) is still asked whether to
|
||||
/// stop, at the interval given, so the reader can go back to a device that returned.
|
||||
#[test]
|
||||
fn a_busy_daemon_is_asked_whether_to_stop_on_a_timer() {
|
||||
// Ends by itself after half a second, so a read that never asks fails the assertion below
|
||||
// instead of hanging the test.
|
||||
struct Busy(Instant);
|
||||
impl Read for Busy {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
if self.0.elapsed() > Duration::from_millis(500) {
|
||||
return Ok(0);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(2));
|
||||
let bytes = record([0, 100, 0, 0, 0, 0, 0, 8]);
|
||||
let size = bytes.len().min(buf.len());
|
||||
buf[..size].copy_from_slice(&bytes[..size]);
|
||||
Ok(size)
|
||||
}
|
||||
}
|
||||
let (mut seen, mut asked) = (0, 0);
|
||||
read(
|
||||
Busy(Instant::now()),
|
||||
|_| seen += 1,
|
||||
|_, _| {},
|
||||
|| {
|
||||
asked += 1;
|
||||
asked == 3
|
||||
},
|
||||
Duration::from_millis(10),
|
||||
);
|
||||
assert_eq!(asked, 3, "asked on the timer although no read ever timed out");
|
||||
assert!(seen > 3, "records kept coming meanwhile: {seen}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_daemon_means_no_connection() {
|
||||
let missing = std::env::temp_dir().join(format!("sc-no-spnav-{}.sock", std::process::id()));
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
//! row the next launch starts in safe mode by itself.
|
||||
//! - **Safe mode** (`--safe-mode` or `SOLIDCRAFT_SAFE_MODE=1`): default settings (the saved ones
|
||||
//! are left untouched), no recovery offer, the software renderer and CPU meshing.
|
||||
//! - **No usable graphics device**: one retry on the software renderer
|
||||
//! (`SOLIDCRAFT_RENDERER=cpu`), then a message box saying what failed and what to try.
|
||||
//! - **A renderer that cannot start**: on Windows, try the remaining graphics APIs in order,
|
||||
//! then the software renderer (`SOLIDCRAFT_RENDERER=cpu`). An explicit `WGPU_BACKEND` fails
|
||||
//! cleanly without switching. Exhausted retries show a message box with the error.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -21,6 +22,25 @@ pub const RENDERER_ENV: &str = "SOLIDCRAFT_RENDERER";
|
||||
pub const FAILED_STARTS_FOR_SAFE_MODE: u32 = 2;
|
||||
const MARKER: &str = "starting";
|
||||
|
||||
/// Explicit application-settings isolation for side-by-side builds and native UI
|
||||
/// acceptance. A supplied invalid path refuses startup rather than falling back
|
||||
/// to the user's ordinary settings. No directory is read or created here.
|
||||
pub fn settings_directory(override_path: Option<&std::ffi::OsStr>, default: Option<PathBuf>) -> Result<Option<PathBuf>, String> {
|
||||
match override_path {
|
||||
None => Ok(default),
|
||||
Some(raw) => {
|
||||
let path = PathBuf::from(raw);
|
||||
if raw.is_empty() || !path.is_absolute() {
|
||||
return Err("SOLIDCRAFT_SETTINGS_DIR must be a nonempty absolute directory path".into());
|
||||
}
|
||||
if path.exists() && !path.is_dir() {
|
||||
return Err("SOLIDCRAFT_SETTINGS_DIR names a file, not a directory".into());
|
||||
}
|
||||
Ok(Some(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an environment value switches something on.
|
||||
pub fn env_on(v: Option<&str>) -> bool {
|
||||
v.map(str::trim).is_some_and(|v| !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false"))
|
||||
@@ -110,6 +130,48 @@ pub fn pick_cpu_adapter(adapters: &[eframe::wgpu::Adapter], surface: Option<&efr
|
||||
})
|
||||
}
|
||||
|
||||
/// Adapter properties used to choose a card without opening a device.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AdapterChoice {
|
||||
pub name: String,
|
||||
pub compatible: bool,
|
||||
}
|
||||
|
||||
/// Choose only surface-compatible adapters. Names saved in preferences are exact; the
|
||||
/// environment override follows wgpu's case-insensitive substring convention.
|
||||
/// No match leaves selection to the normal wgpu request.
|
||||
pub fn choose_adapter(adapters: &[AdapterChoice], environment: Option<&str>, preference: Option<&str>) -> Option<usize> {
|
||||
// wgpu's environment helper panics on a missing name; an override must remain recoverable.
|
||||
let compatible = || adapters.iter().enumerate().filter(|(_, a)| a.compatible);
|
||||
if let Some(name) = environment.filter(|s| !s.is_empty()) {
|
||||
let name = name.to_lowercase();
|
||||
if let Some((index, _)) = compatible().find(|(_, a)| a.name.to_lowercase().contains(&name)) {
|
||||
return Some(index);
|
||||
}
|
||||
}
|
||||
if let Some(name) = preference.filter(|s| !s.is_empty())
|
||||
&& let Some((index, _)) = compatible().find(|(_, a)| a.name == name)
|
||||
{
|
||||
return Some(index);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Leave Automatic on wgpu's normal request path; named choices may defer to it too.
|
||||
pub fn configure_adapter(setup: &mut eframe::egui_wgpu::WgpuSetupCreateNew, cpu: bool, environment: Option<String>, preference: Option<String>) {
|
||||
if cpu {
|
||||
setup.native_adapter_selector = Some(std::sync::Arc::new(|adapters, surface| pick_cpu_adapter(adapters, surface).map(Some)));
|
||||
} else if environment.as_deref().is_some_and(|s| !s.is_empty()) || preference.as_deref().is_some_and(|s| !s.is_empty()) {
|
||||
setup.native_adapter_selector = Some(std::sync::Arc::new(move |adapters, surface| {
|
||||
let choices: Vec<_> = adapters
|
||||
.iter()
|
||||
.map(|a| AdapterChoice { name: a.get_info().name, compatible: surface.is_none_or(|s| a.is_surface_supported(s)) })
|
||||
.collect();
|
||||
Ok(choose_adapter(&choices, environment.as_deref(), preference.as_deref()).and_then(|i| adapters.get(i)).cloned())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// `WGPU_BACKEND`: when set, wgpu picks the backends from it and the order below is not used.
|
||||
pub const WGPU_BACKEND_ENV: &str = "WGPU_BACKEND";
|
||||
|
||||
@@ -157,6 +219,34 @@ pub fn choose_backends(
|
||||
Some(WINDOWS_ORDER.into_iter().find(|b| has_hardware(*b)).unwrap_or(Backends::DX12))
|
||||
}
|
||||
|
||||
/// Internal, process-local retry choice; never written to the graphics preference.
|
||||
pub const RETRY_BACKEND_ENV: &str = "SOLIDCRAFT_STARTUP_BACKEND";
|
||||
/// Why this launch is using a fallback, carried to the new process for status and Preferences.
|
||||
pub const FALLBACK_NOTE_ENV: &str = "SOLIDCRAFT_GRAPHICS_FALLBACK";
|
||||
|
||||
/// A handled startup failure tries each remaining Windows API once, then software rendering.
|
||||
/// An explicit environment choice is final, including when software rendering was requested.
|
||||
pub fn fallback_backend(windows: bool, explicit: bool, cpu: bool, failed: eframe::wgpu::Backends) -> Option<&'static str> {
|
||||
if explicit || cpu {
|
||||
return None;
|
||||
}
|
||||
if windows {
|
||||
let last = WINDOWS_ORDER.iter().rposition(|backend| failed.intersects(*backend));
|
||||
if let Some(next) = last.and_then(|i| WINDOWS_ORDER.get(i + 1)) {
|
||||
return Some(if *next == eframe::wgpu::Backends::VULKAN { "vulkan" } else { "gl" });
|
||||
}
|
||||
}
|
||||
Some("cpu")
|
||||
}
|
||||
|
||||
/// Keep driver diagnostics on one bounded line, including across the finite retry chain.
|
||||
pub fn fallback_note(previous: &str, failed: eframe::wgpu::Backends, next: &str, error: &str) -> String {
|
||||
let previous: String = previous.chars().take(2048).collect();
|
||||
let error: String = error.chars().take(1024).collect();
|
||||
let line = format!("{previous} Graphics fallback: {failed:?} failed ({error}); trying {next}.");
|
||||
line.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(4096).collect()
|
||||
}
|
||||
|
||||
/// The device feature for a 32-bit float depth buffer with a stencil (reversed depth, #1434).
|
||||
pub const FLOAT_DEPTH_STENCIL: eframe::wgpu::Features = eframe::wgpu::Features::DEPTH32FLOAT_STENCIL8;
|
||||
|
||||
@@ -189,6 +279,29 @@ pub fn has_hardware_adapter(backends: eframe::wgpu::Backends) -> bool {
|
||||
adapters.iter().any(|a| a.get_info().device_type != eframe::wgpu::DeviceType::Cpu)
|
||||
}
|
||||
|
||||
/// The environment variable that switches wgpu's indirect-call validation on or off.
|
||||
pub const INDIRECT_VALIDATION_ENV: &str = "WGPU_VALIDATION_INDIRECT_CALL";
|
||||
|
||||
/// The instance flags for the window. wgpu's indirect-call validation compiles a compute shader
|
||||
/// when the device is created, and on Intel Macs (macOS 12 with Intel Iris graphics, #3024,
|
||||
/// #3080, #4605) the Metal compiler refuses it: the device is lost and the window never opens.
|
||||
/// So on an Intel Mac the validation is left off, unless the user turned it on or off with
|
||||
/// `WGPU_VALIDATION_INDIRECT_CALL` themselves.
|
||||
pub fn window_instance_flags(flags: eframe::wgpu::InstanceFlags, intel_mac: bool, env: Option<&str>) -> eframe::wgpu::InstanceFlags {
|
||||
if intel_mac && env.map(str::trim).is_none_or(str::is_empty) { flags - eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL } else { flags }
|
||||
}
|
||||
|
||||
/// Windows with OpenGL: the window keeps the system title bar (#2063). The window without OS
|
||||
/// decorations keeps its caption style while having no frame, and some OpenGL drivers (older
|
||||
/// Intel HD/UHD) then show the picture a caption's height lower than the window, so it no longer
|
||||
/// lines up with the pointer. Direct3D 12 and Vulkan draw it in place and keep the application bar
|
||||
/// as the title bar. `backends` are the ones wgpu starts: OpenGL alone is decided before the
|
||||
/// window opens.
|
||||
pub fn system_title_bar(windows: bool, backends: eframe::wgpu::Backends) -> bool {
|
||||
use eframe::wgpu::Backends;
|
||||
windows && backends.contains(Backends::GL) && !backends.intersects(Backends::DX12 | Backends::VULKAN)
|
||||
}
|
||||
|
||||
/// The message shown when no window could be opened at all.
|
||||
pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
|
||||
let folder = settings.map(|d| d.display().to_string()).unwrap_or_else(|| "the SolidCraft settings folder".into());
|
||||
@@ -207,6 +320,48 @@ pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cards() -> Vec<AdapterChoice> {
|
||||
["Integrated", "Discrete small", "Discrete large"].into_iter().map(|name| AdapterChoice { name: name.into(), compatible: true }).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapter_names_environment_override_and_missing_card_fallback() {
|
||||
let cards = cards();
|
||||
assert_eq!(choose_adapter(&cards, None, Some("Discrete small")), Some(1));
|
||||
assert_eq!(choose_adapter(&cards, Some("LARGE"), Some("Discrete small")), Some(2));
|
||||
assert_eq!(choose_adapter(&cards, None, Some("removed")), None);
|
||||
assert_eq!(choose_adapter(&cards, Some("missing"), Some("Discrete small")), Some(1));
|
||||
assert_eq!(choose_adapter(&cards, Some(""), Some("Discrete small")), Some(1));
|
||||
assert_eq!(choose_adapter(&cards, None, Some("Discrete")), None, "saved names are exact");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_adapter_leaves_selection_to_wgpu() {
|
||||
let mut cards = cards();
|
||||
assert_eq!(choose_adapter(&cards, None, None), None);
|
||||
assert_eq!(choose_adapter(&cards, Some(""), Some("")), None);
|
||||
cards[2].compatible = false;
|
||||
assert_eq!(choose_adapter(&cards, Some("large"), Some("Discrete large")), None);
|
||||
assert_eq!(choose_adapter(&cards, Some("small"), None), Some(1));
|
||||
assert_eq!(choose_adapter(&[], None, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_adapter_preserves_the_previous_request_configuration() {
|
||||
use eframe::egui_wgpu::WgpuSetupCreateNew;
|
||||
let previous = WgpuSetupCreateNew::without_display_handle();
|
||||
for preference in [None, Some(String::new())] {
|
||||
let mut setup = WgpuSetupCreateNew::without_display_handle();
|
||||
configure_adapter(&mut setup, false, None, preference);
|
||||
assert!(setup.native_adapter_selector.is_none(), "Automatic must use the normal wgpu request");
|
||||
assert_eq!(setup.power_preference, previous.power_preference);
|
||||
assert_eq!(setup.instance_descriptor.backends, previous.instance_descriptor.backends);
|
||||
}
|
||||
let mut setup = WgpuSetupCreateNew::without_display_handle();
|
||||
configure_adapter(&mut setup, false, Some("missing".into()), Some("removed".into()));
|
||||
assert!(setup.native_adapter_selector.unwrap()(&[], None).unwrap().is_none(), "missing names must use the normal request too");
|
||||
}
|
||||
|
||||
fn scratch(name: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("solidcraft-startup-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
@@ -214,6 +369,32 @@ mod tests {
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolated_settings_are_explicit_absolute_and_never_fall_back() {
|
||||
let default = std::env::temp_dir().join("ordinary-settings");
|
||||
assert_eq!(settings_directory(None, Some(default.clone())).unwrap(), Some(default.clone()));
|
||||
let selected = std::env::temp_dir().join("solidcraft-isolated-settings-fixture");
|
||||
assert_eq!(settings_directory(Some(selected.as_os_str()), Some(default.clone())).unwrap(), Some(selected));
|
||||
for raw in ["", "relative/settings", "../ordinary"] {
|
||||
assert!(settings_directory(Some(std::ffi::OsStr::new(raw)), Some(default.clone())).is_err());
|
||||
}
|
||||
let file = std::env::current_exe().unwrap();
|
||||
assert!(settings_directory(Some(file.as_os_str()), Some(default)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intel_macs_start_without_indirect_call_validation() {
|
||||
use eframe::wgpu::InstanceFlags as F;
|
||||
let on = F::VALIDATION | F::VALIDATION_INDIRECT_CALL;
|
||||
assert_eq!(window_instance_flags(on, true, None), F::VALIDATION);
|
||||
assert_eq!(window_instance_flags(on, true, Some(" ")), F::VALIDATION);
|
||||
// The user's own choice stands, on or off.
|
||||
assert_eq!(window_instance_flags(on, true, Some("1")), on);
|
||||
assert_eq!(window_instance_flags(F::VALIDATION, true, Some("0")), F::VALIDATION);
|
||||
// Every other machine keeps what it had.
|
||||
assert_eq!(window_instance_flags(on, false, None), on);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switches_read_from_the_environment() {
|
||||
assert!(env_on(Some("1")) && env_on(Some("yes")) && !env_on(Some("0")) && !env_on(Some(" ")) && !env_on(None) && !env_on(Some("false")));
|
||||
@@ -290,6 +471,54 @@ mod tests {
|
||||
assert_eq!(choose_backends(true, false, false, Some(""), probe(&[Backends::GL])), Some(Backends::GL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_windows_backends_fall_back_in_order_and_stop_after_software() {
|
||||
use eframe::wgpu::Backends as B;
|
||||
assert_eq!(fallback_backend(true, false, false, B::DX12), Some("vulkan"));
|
||||
assert_eq!(fallback_backend(true, false, false, B::VULKAN), Some("gl"));
|
||||
assert_eq!(fallback_backend(true, false, false, B::GL), Some("cpu"));
|
||||
assert_eq!(fallback_backend(true, false, true, B::DX12), None);
|
||||
assert_eq!(fallback_backend(true, false, false, B::DX12 | B::VULKAN), Some("gl"));
|
||||
assert_eq!(fallback_backend(false, false, false, B::VULKAN), Some("cpu"));
|
||||
let note = fallback_note("", B::DX12, "vulkan", "egui_pipeline\n0x80070057");
|
||||
assert!(note.contains("egui_pipeline 0x80070057") && note.contains("vulkan") && !note.contains('\n'));
|
||||
let long = fallback_note(&"x".repeat(5000), B::VULKAN, "gl", &"y".repeat(5000));
|
||||
assert!(long.chars().count() <= 4096 && long.ends_with("trying gl."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_backend_failure_never_switches_api_or_retries_software() {
|
||||
use eframe::wgpu::Backends as B;
|
||||
for windows in [false, true] {
|
||||
for cpu in [false, true] {
|
||||
for backend in [B::DX12, B::VULKAN, B::GL, B::all()] {
|
||||
assert_eq!(fallback_backend(windows, true, cpu, backend), None);
|
||||
let mut probed = false;
|
||||
assert_eq!(
|
||||
choose_backends(windows, true, cpu, Some("vulkan"), |_| {
|
||||
probed = true;
|
||||
false
|
||||
}),
|
||||
None
|
||||
);
|
||||
assert!(!probed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_opengl_on_windows_gets_the_system_title_bar() {
|
||||
use eframe::wgpu::Backends;
|
||||
assert!(system_title_bar(true, Backends::GL));
|
||||
for b in [Backends::DX12, Backends::VULKAN, Backends::DX12 | Backends::GL, Backends::VULKAN | Backends::GL, Backends::all()] {
|
||||
assert!(!system_title_bar(true, b), "{b:?}");
|
||||
}
|
||||
for b in [Backends::GL, Backends::VULKAN, Backends::METAL] {
|
||||
assert!(!system_title_bar(false, b), "{b:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wgpu_backend_the_preference_and_the_software_renderer_override_the_order() {
|
||||
use eframe::wgpu::Backends;
|
||||
|
||||
@@ -25,6 +25,7 @@ assets/
|
||||
fonts/{liberation-sans,biz-udp-gothic}/ # unmodified fonts and OFL licences
|
||||
locales/{en,de,ja,ko}.json # shared offline catalogs
|
||||
platforms/{macos,linux,windows}/ # bundle, freedesktop and WiX templates
|
||||
platforms/macos/dmg/ # disk image window: layout, dmgbuild settings, background art
|
||||
archive/icons/macos-v1/
|
||||
source/icon.vectorcraft # earlier editable work, retained
|
||||
# redundant historical exports are retired
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Original illustrative opaque finishes; not measured optical or engineering data.
|
||||
//! (name, sRGB color, metal fraction, perceptual roughness). IOR is an illustrative 1.5.
|
||||
//! Separate names preserve the scalar library's appearance and saved assignments.
|
||||
pub const OPAQUE_FINISHES: [(&str, [u8; 3], f64, f64); 6] = [
|
||||
("Steel - Satin PBR", [158, 164, 172], 1.0, 0.4),
|
||||
("Aluminum - Satin PBR", [204, 208, 214], 1.0, 0.3),
|
||||
("Copper - Clean PBR", [204, 122, 80], 1.0, 0.25),
|
||||
("ABS - Molded PBR", [222, 222, 216], 0.0, 0.4),
|
||||
("PLA - Smooth PBR", [110, 160, 222], 0.0, 0.35),
|
||||
("Wood - Oak Uniform PBR Preview", [190, 146, 92], 0.0, 0.65),
|
||||
];
|
||||
@@ -0,0 +1,292 @@
|
||||
//! Original starter visual data, MIT OR Apache-2.0. No external material library.
|
||||
use crate::appearance_pack::{Category, GlassBoundary, GlassRecipe, OpaqueRecipe, TextureAxis, TextureRecipe, VisualPreset};
|
||||
|
||||
const WOOD: &str = "Uniform opaque preview only; grain resources/mapping are not applied";
|
||||
const SOLAR: &str = "Uniform opaque preview only; no cell grid, busbars, electricity or efficiency claim";
|
||||
const REGOLITH: &str = "Uniform opaque preview only; no granular displacement or measured lunar optical data";
|
||||
const OPAQUE: &str = "Illustrative opaque isotropic PBR; no measured alloy, polymer or engineering properties";
|
||||
|
||||
const fn look(color: [u8; 3], metallic: f64, roughness: f64, dielectric_ior: f64) -> OpaqueRecipe {
|
||||
OpaqueRecipe { color, metallic, roughness, dielectric_ior }
|
||||
}
|
||||
const fn maps(id: &'static str, repeat_mm: [f64; 2], grain: bool) -> Option<TextureRecipe> {
|
||||
Some(TextureRecipe { id, repeat_mm, grain_axis: if grain { Some(TextureAxis::V) } else { None } })
|
||||
}
|
||||
const fn glass(transmission_linear_rgb: [f64; 3], ior: f64, reference_path_mm: f64, boundary: GlassBoundary) -> GlassRecipe {
|
||||
GlassRecipe { transmission_linear_rgb, dielectric_ior: ior, reference_path_mm, boundary }
|
||||
}
|
||||
|
||||
pub const STARTER_PACK: [VisualPreset; 40] = [
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.oak-honey",
|
||||
"Wood - Honey Oak Uniform Preview",
|
||||
Category::Wood,
|
||||
look([190, 145, 88], 0.0, 0.64, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.oak-honey.maps-v1", [120.0, 240.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.oak-smoked",
|
||||
"Wood - Smoked Oak Uniform Preview",
|
||||
Category::Wood,
|
||||
look([91, 72, 55], 0.0, 0.68, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.oak-smoked.maps-v1", [120.0, 240.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.walnut-dark",
|
||||
"Wood - Dark Walnut Uniform Preview",
|
||||
Category::Wood,
|
||||
look([99, 66, 43], 0.0, 0.60, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.walnut-dark.maps-v1", [100.0, 220.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.maple-light",
|
||||
"Wood - Light Maple Uniform Preview",
|
||||
Category::Wood,
|
||||
look([221, 192, 141], 0.0, 0.57, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.maple-light.maps-v1", [100.0, 200.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.cherry-warm",
|
||||
"Wood - Warm Cherry Uniform Preview",
|
||||
Category::Wood,
|
||||
look([164, 99, 65], 0.0, 0.58, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.cherry-warm.maps-v1", [110.0, 220.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.ash-pale",
|
||||
"Wood - Pale Ash Uniform Preview",
|
||||
Category::Wood,
|
||||
look([209, 186, 146], 0.0, 0.70, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.ash-pale.maps-v1", [120.0, 260.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.birch-cream",
|
||||
"Wood - Cream Birch Uniform Preview",
|
||||
Category::Wood,
|
||||
look([228, 206, 163], 0.0, 0.63, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.birch-cream.maps-v1", [100.0, 200.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.teak-golden",
|
||||
"Wood - Golden Teak Uniform Preview",
|
||||
Category::Wood,
|
||||
look([167, 122, 66], 0.0, 0.61, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.teak-golden.maps-v1", [110.0, 250.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.carbon-steel-polished",
|
||||
"Metal - Polished Carbon Steel",
|
||||
Category::Metal,
|
||||
look([161, 166, 173], 1.0, 0.16, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.stainless-soft-satin",
|
||||
"Metal - Soft Satin Stainless",
|
||||
Category::Metal,
|
||||
look([187, 193, 201], 1.0, 0.36, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.aluminum-bead-blast",
|
||||
"Metal - Bead Blast Aluminum",
|
||||
Category::Metal,
|
||||
look([195, 201, 210], 1.0, 0.58, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.aluminum-silver-polish",
|
||||
"Metal - Silver Polish Aluminum",
|
||||
Category::Metal,
|
||||
look([218, 224, 232], 1.0, 0.13, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.copper-aged-warm",
|
||||
"Metal - Warm Aged Copper",
|
||||
Category::Metal,
|
||||
look([171, 95, 57], 1.0, 0.48, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.brass-champagne",
|
||||
"Metal - Champagne Satin Brass",
|
||||
Category::Metal,
|
||||
look([205, 173, 102], 1.0, 0.32, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.bronze-dark-cast",
|
||||
"Metal - Dark Cast Bronze",
|
||||
Category::Metal,
|
||||
look([133, 95, 58], 1.0, 0.66, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.titanium-matte",
|
||||
"Metal - Matte Gray Titanium",
|
||||
Category::Metal,
|
||||
look([147, 149, 157], 1.0, 0.53, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.gold-warm",
|
||||
"Metal - Warm Reflective Gold",
|
||||
Category::Metal,
|
||||
look([235, 190, 82], 1.0, 0.18, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.silver-cool",
|
||||
"Metal - Cool Reflective Silver",
|
||||
Category::Metal,
|
||||
look([225, 230, 237], 1.0, 0.12, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.abs-charcoal",
|
||||
"Plastic - Charcoal ABS",
|
||||
Category::Plastic,
|
||||
look([43, 45, 48], 0.0, 0.44, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.abs-warm-white",
|
||||
"Plastic - Warm White ABS",
|
||||
Category::Plastic,
|
||||
look([232, 229, 220], 0.0, 0.40, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque("starter.plastic.pla-coral", "Plastic - Coral PLA", Category::Plastic, look([218, 99, 83], 0.0, 0.43, 1.5), OPAQUE, None),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.petg-cyan-opaque",
|
||||
"Plastic - Opaque Cyan PETG",
|
||||
Category::Plastic,
|
||||
look([36, 148, 167], 0.0, 0.29, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.nylon-ivory",
|
||||
"Plastic - Ivory Nylon",
|
||||
Category::Plastic,
|
||||
look([220, 213, 190], 0.0, 0.62, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.pc-graphite-opaque",
|
||||
"Plastic - Opaque Graphite PC",
|
||||
Category::Plastic,
|
||||
look([70, 76, 84], 0.0, 0.27, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.hdpe-natural-opaque",
|
||||
"Plastic - Opaque Natural HDPE",
|
||||
Category::Plastic,
|
||||
look([221, 222, 207], 0.0, 0.48, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque("starter.plastic.pom-black", "Plastic - Black POM", Category::Plastic, look([29, 31, 35], 0.0, 0.32, 1.5), OPAQUE, None),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.tpu-matte-black",
|
||||
"Plastic - Matte Black TPU",
|
||||
Category::Plastic,
|
||||
look([31, 30, 32], 0.0, 0.83, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.peek-amber-opaque",
|
||||
"Plastic - Opaque Amber PEEK",
|
||||
Category::Plastic,
|
||||
look([157, 119, 62], 0.0, 0.51, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::glass(
|
||||
"starter.glass.clear-soda-lime",
|
||||
"Glass Recipe - Clear Soda Lime",
|
||||
glass([0.92, 0.98, 0.96], 1.52, 3.0, GlassBoundary::Closed),
|
||||
),
|
||||
VisualPreset::glass("starter.glass.low-iron-clear", "Glass Recipe - Low Iron Clear", glass([0.98, 0.99, 1.0], 1.50, 4.0, GlassBoundary::Closed)),
|
||||
VisualPreset::glass("starter.glass.smoky-neutral", "Glass Recipe - Smoky Neutral", glass([0.35, 0.38, 0.42], 1.52, 3.0, GlassBoundary::Closed)),
|
||||
VisualPreset::glass("starter.glass.amber-tint", "Glass Recipe - Amber Tint", glass([0.84, 0.38, 0.08], 1.51, 3.0, GlassBoundary::Closed)),
|
||||
VisualPreset::glass("starter.glass.cobalt-blue", "Glass Recipe - Cobalt Blue", glass([0.06, 0.14, 0.76], 1.53, 3.0, GlassBoundary::Closed)),
|
||||
VisualPreset::glass(
|
||||
"starter.glass.thin-clear",
|
||||
"Glass Recipe - Thin Clear Sheet",
|
||||
glass([0.95, 0.98, 0.99], 1.50, 2.0, GlassBoundary::Thin { thickness_mm: 2.0 }),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.solar.mono-blue",
|
||||
"Solar Panel - Mono Blue Uniform Preview",
|
||||
Category::Solar,
|
||||
look([20, 41, 85], 0.0, 0.20, 1.5),
|
||||
SOLAR,
|
||||
maps("starter.solar.mono-blue.maps-v1", [160.0, 160.0], false),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.solar.thin-film-dark",
|
||||
"Solar Panel - Dark Thin Film Uniform Preview",
|
||||
Category::Solar,
|
||||
look([28, 33, 42], 0.0, 0.28, 1.5),
|
||||
SOLAR,
|
||||
maps("starter.solar.thin-film-dark.maps-v1", [100.0, 200.0], false),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.regolith.mare-charcoal",
|
||||
"Regolith - Mare Charcoal Uniform Preview",
|
||||
Category::Regolith,
|
||||
look([56, 56, 55], 0.0, 0.97, 1.5),
|
||||
REGOLITH,
|
||||
maps("starter.regolith.mare-charcoal.maps-v1", [200.0, 200.0], false),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.regolith.highland-pale",
|
||||
"Regolith - Highland Pale Uniform Preview",
|
||||
Category::Regolith,
|
||||
look([116, 113, 106], 0.0, 0.93, 1.5),
|
||||
REGOLITH,
|
||||
maps("starter.regolith.highland-pale.maps-v1", [200.0, 200.0], false),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.regolith.dust-neutral",
|
||||
"Regolith - Neutral Dust Uniform Preview",
|
||||
Category::Regolith,
|
||||
look([86, 85, 81], 0.0, 1.0, 1.5),
|
||||
REGOLITH,
|
||||
maps("starter.regolith.dust-neutral.maps-v1", [150.0, 150.0], false),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.regolith.agglutinate-dark",
|
||||
"Regolith - Dark Agglutinate Uniform Preview",
|
||||
Category::Regolith,
|
||||
look([66, 63, 60], 0.0, 0.89, 1.5),
|
||||
REGOLITH,
|
||||
maps("starter.regolith.agglutinate-dark.maps-v1", [200.0, 200.0], false),
|
||||
),
|
||||
];
|
||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 660 B |
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -1,88 +1,93 @@
|
||||
//! Original SolidCraft mark, shared by desktop, packaging and CLI.
|
||||
//! Original SolidCraft mark, shared by desktop, packaging and CLI: the borderless mark, five
|
||||
//! faces with transparent gaps between them, drawn from the macOS icon's
|
||||
//! `macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg`.
|
||||
type P = (f32, f32);
|
||||
|
||||
/// One face in that SVG's units: its outline, and its linear gradient from one point and
|
||||
/// colour to another.
|
||||
struct Face {
|
||||
outline: &'static [P],
|
||||
from: (P, [u8; 3]),
|
||||
to: (P, [u8; 3]),
|
||||
}
|
||||
|
||||
const FACES: [Face; 5] = [
|
||||
// Inner side of the upper step.
|
||||
Face {
|
||||
outline: &[(547.883, 282.66), (709.797, 188.211), (709.797, 375.985)],
|
||||
from: ((619.14, 179.734), [29, 99, 198]),
|
||||
to: ((694.687, 396.761), [21, 73, 158]),
|
||||
},
|
||||
// Top of the lower step.
|
||||
Face {
|
||||
outline: &[(727.797, 593.626), (727.797, 407.136), (889.571, 500.381)],
|
||||
from: ((619.14, 413.244), [69, 159, 255]),
|
||||
to: ((790.839, 605.547), [35, 121, 231]),
|
||||
},
|
||||
// Right face, with the recess.
|
||||
Face {
|
||||
outline: &[(530.0, 521.17), (691.797, 427.912), (691.797, 655.928), (907.594, 531.545), (907.594, 740.159), (530.0, 957.8)],
|
||||
from: ((632.876, 399.508), [27, 102, 207]),
|
||||
to: ((783.971, 968.175), [18, 69, 150]),
|
||||
},
|
||||
// Left face, stepped.
|
||||
Face {
|
||||
outline: &[(116.406, 510.769), (116.406, 303.529), (494.0, 521.17), (494.0, 957.8), (314.203, 854.167), (314.203, 624.777)],
|
||||
from: ((207.063, 303.357), [37, 141, 245]),
|
||||
to: ((426.837, 968.175), [20, 97, 218]),
|
||||
},
|
||||
// Top of the extruded profile.
|
||||
Face {
|
||||
outline: &[(134.374, 272.334), (512.0, 53.364), (691.828, 157.015), (476.117, 282.846), (673.752, 396.761), (512.0, 489.993)],
|
||||
from: ((385.63, 53.364), [81, 173, 255]),
|
||||
to: ((522.989, 510.769), [43, 131, 241]),
|
||||
},
|
||||
];
|
||||
|
||||
/// The mark's extent in SVG units: left, top, width, height.
|
||||
const BOUNDS: (f32, f32, f32, f32) = (116.406, 53.364, 791.188, 904.436);
|
||||
|
||||
/// This block is the icon image data
|
||||
pub fn rgba(size: usize) -> Result<Vec<u8>, String> {
|
||||
if !(1..=2048).contains(&size) {
|
||||
return Err("icon size must be 1..=2048".into());
|
||||
}
|
||||
type P = (f32, f32);
|
||||
// Coverage buffer at 4x4 samples per pixel (2x2 for big sizes), averaged down at the end.
|
||||
let ss = if size > 512 { 2 } else { 4 };
|
||||
let n = size * ss;
|
||||
let mut buf = vec![[0u8; 4]; n * n];
|
||||
let s = n as f32 / 412.217; // SVG units -> samples: the logo fills the height
|
||||
let ox = (n as f32 - 357.0 * s) / 2.0; // and is centred sideways
|
||||
// Fill a convex polygon given in SVG units.
|
||||
let mut fill = |pts: &[P], col: [u8; 3]| {
|
||||
let v: Vec<P> = pts.iter().map(|p| (ox + p.0 * s, p.1 * s)).collect();
|
||||
let (bx, by, bw, bh) = BOUNDS;
|
||||
let s = n as f32 / bh; // SVG units -> samples: the mark fills the height
|
||||
let (ox, oy) = ((n as f32 - bw * s) / 2.0 - bx * s, -by * s); // and is centred sideways
|
||||
// Fill each face's outline (even-odd spans per row, so concave outlines work) with its gradient.
|
||||
for face in &FACES {
|
||||
let v: Vec<P> = face.outline.iter().map(|p| (ox + p.0 * s, oy + p.1 * s)).collect();
|
||||
let (((fx, fy), c0), ((tx, ty), c1)) = (face.from, face.to);
|
||||
let (gx, gy) = (tx - fx, ty - fy);
|
||||
let g2 = gx * gx + gy * gy;
|
||||
let (top, bottom) = v.iter().fold((f32::MAX, f32::MIN), |(a, b), p| (a.min(p.1), b.max(p.1)));
|
||||
let mut xs = Vec::with_capacity(v.len());
|
||||
for y in ((top - 0.5).ceil() as i32).max(0)..=((bottom - 0.5).floor() as i32).min(n as i32 - 1) {
|
||||
let cy = y as f32 + 0.5;
|
||||
let (mut lo, mut hi) = (f32::MAX, f32::MIN);
|
||||
xs.clear();
|
||||
for i in 0..v.len() {
|
||||
let (a, b) = (v[i], v[(i + 1) % v.len()]);
|
||||
if (a.1 <= cy) != (b.1 <= cy) {
|
||||
let x = a.0 + (cy - a.1) / (b.1 - a.1) * (b.0 - a.0);
|
||||
lo = lo.min(x);
|
||||
hi = hi.max(x);
|
||||
xs.push(a.0 + (cy - a.1) / (b.1 - a.1) * (b.0 - a.0));
|
||||
}
|
||||
}
|
||||
if lo > hi {
|
||||
continue;
|
||||
}
|
||||
for x in ((lo - 0.5).ceil() as i32).max(0)..=((hi - 0.5).floor() as i32).min(n as i32 - 1) {
|
||||
buf[y as usize * n + x as usize] = [col[0], col[1], col[2], 255];
|
||||
xs.sort_by(f32::total_cmp);
|
||||
for &[from, to] in xs.as_chunks::<2>().0 {
|
||||
for x in ((from - 0.5).ceil() as i32).max(0)..=((to - 0.5).floor() as i32).min(n as i32 - 1) {
|
||||
// The sample's place along the gradient, in SVG units.
|
||||
let (px, py) = ((x as f32 + 0.5 - ox) / s, (cy - oy) / s);
|
||||
let t = (((px - fx) * gx + (py - fy) * gy) / g2).clamp(0.0, 1.0);
|
||||
let mix = |k: usize| (f32::from(c0[k]) + (f32::from(c1[k]) - f32::from(c0[k])) * t).round() as u8;
|
||||
buf[y as usize * n + x as usize] = [mix(0), mix(1), mix(2), 255];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// Faces: each is a few overlapping convex pieces (the filler only does convex shapes).
|
||||
let (light, mid, dark) = ([96, 158, 236], [58, 136, 238], [55, 108, 197]);
|
||||
fill(&[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)], light);
|
||||
fill(&[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], light);
|
||||
fill(&[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)], mid);
|
||||
fill(&[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)], mid);
|
||||
fill(&[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)], mid);
|
||||
fill(&[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)], dark);
|
||||
fill(&[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], dark);
|
||||
fill(&[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)], dark);
|
||||
fill(&[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)], dark);
|
||||
// White edge lines, 10 units wide: a rectangle per segment and a disc at each inner corner.
|
||||
let white = [255, 255, 255];
|
||||
let mut stroke = |pts: &[P], closed: bool| {
|
||||
let m = pts.len();
|
||||
for i in 0..(if closed { m } else { m - 1 }) {
|
||||
let (a, b) = (pts[i], pts[(i + 1) % m]);
|
||||
let l = ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt();
|
||||
let (nx, ny) = (-(b.1 - a.1) / l * 5.0, (b.0 - a.0) / l * 5.0);
|
||||
fill(&[(a.0 + nx, a.1 + ny), (b.0 + nx, b.1 + ny), (b.0 - nx, b.1 - ny), (a.0 - nx, a.1 - ny)], white);
|
||||
}
|
||||
for c in &pts[if closed { 0 } else { 1 }..if closed { m } else { m - 1 }] {
|
||||
let disc: Vec<P> = (0..16)
|
||||
.map(|k| {
|
||||
let t = k as f32 * std::f32::consts::TAU / 16.0;
|
||||
(c.0 + 5.0 * t.cos(), c.1 + 5.0 * t.sin())
|
||||
})
|
||||
.collect();
|
||||
fill(&disc, white);
|
||||
}
|
||||
};
|
||||
stroke(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)], false);
|
||||
stroke(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)], false);
|
||||
stroke(&[(177.0, 206.1), (263.8, 156.0)], false);
|
||||
stroke(
|
||||
&[
|
||||
(178.5, 5.8),
|
||||
(5.0, 105.9),
|
||||
(5.0, 206.1),
|
||||
(91.7, 256.2),
|
||||
(91.7, 356.4),
|
||||
(178.5, 406.4),
|
||||
(352.0, 306.3),
|
||||
(352.0, 206.1),
|
||||
(265.2, 156.0),
|
||||
(265.2, 55.9),
|
||||
],
|
||||
true,
|
||||
);
|
||||
}
|
||||
// Average each ss x ss block down to one pixel (straight alpha = covered share).
|
||||
let mut rgba = Vec::with_capacity(size * size * 4);
|
||||
for y in 0..size {
|
||||
@@ -123,4 +128,20 @@ mod tests {
|
||||
assert!(super::rgba(0).is_err());
|
||||
assert!(super::rgba(2049).is_err());
|
||||
}
|
||||
|
||||
/// Borderless: no white edge lines, and the gaps between faces are see-through. At 256 px
|
||||
/// the gap between the left and right faces (SVG x 494..530) is transparent on the row
|
||||
/// through the middle of the mark, and no opaque pixel is white.
|
||||
#[test]
|
||||
fn shared_mark_is_borderless() {
|
||||
let size = 256;
|
||||
let pixels = super::rgba(size).expect("valid size");
|
||||
let px = |x: usize, y: usize| pixels.as_chunks::<4>().0[y * size + x];
|
||||
let s = size as f32 / 904.436;
|
||||
let gap_x = ((size as f32 - 791.188 * s) / 2.0 + (512.0 - 116.406) * s) as usize;
|
||||
let row = ((750.0 - 53.364) * s) as usize;
|
||||
assert_eq!(px(gap_x, row)[3], 0, "the seam between the left and right faces is a gap");
|
||||
assert!(px(gap_x - 8, row)[3] == 255 && px(gap_x + 8, row)[3] == 255, "faces on both sides of the gap");
|
||||
assert!(pixels.as_chunks::<4>().0.iter().all(|p| p[3] == 0 || p[0] < 200), "no white edges");
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 44 KiB |
@@ -78,7 +78,7 @@ Its projected circle uses the same axes; do not arbitrarily flatten the ellipse.
|
||||
|
||||
Only use the stable `source` identifiers below in model SVGs. The renderer maps roles to
|
||||
light/dark theme values. `currentColor` is the background line-art role, not a way
|
||||
to add arbitrary colours. All values and accents come from `design/palette.toml`.
|
||||
to add arbitrary colours. Default display values and semantic accents come from `design/palette.toml`. Theme files may override those display roles and ramps through `icon_palette`; source identifiers and accent assignments stay unchanged.
|
||||
|
||||
| Role | Author helper | Light value | Use |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -96,9 +96,9 @@ The source hexes identify paint roles, not the displayed colour. **Never recolou
|
||||
SVG to apply a workspace accent.** Keep these identifiers and every finished shape
|
||||
unchanged; the runtime and review page resolve them through `design/palette.toml`.
|
||||
|
||||
That file is the single colour authority: edit `light`/`dark` in the relevant
|
||||
That file is the default colour authority: edit `light`/`dark` in the relevant
|
||||
`accents` ramp; `active-3` is its primary stroke and `active-1/2` are shaded faces.
|
||||
Do not edit a role's `source` identifier. `variants` holds colour-vision adjustments.
|
||||
Do not edit a role's `source` identifier. Theme `icon_palette` overrides affect display colours only; colour-vision variants still take precedence. `variants` holds colour-vision adjustments.
|
||||
A role with `use = "active-3"` follows that stroke automatically.
|
||||
|
||||
| Meaning | Accent | Assignment |
|
||||
|
||||
@@ -29,6 +29,7 @@ ground = "c_fix"
|
||||
|
||||
# Same finished drawing, different semantic operation. Colours live only in palette.toml.
|
||||
sketch_break = { master = "break", accent = "sketch" }
|
||||
sketch_cancel = { master = "close", accent = "sketch" }
|
||||
sketch_dxf = { master = "dxf", accent = "sketch" }
|
||||
sketch_move = { master = "move", accent = "sketch" }
|
||||
sketch_scale = { master = "scale", accent = "sketch" }
|
||||
|
||||
@@ -48,6 +48,38 @@ source = "#236ead"
|
||||
light = "#236ead"
|
||||
dark = "#2976b8"
|
||||
|
||||
# Simultaneous component colours for inspection icons; existing workspace ramps stay unchanged.
|
||||
|
||||
[roles.component-orange-1]
|
||||
source = "#f1d2af"
|
||||
light = "#f1d2af"
|
||||
dark = "#ba8f6a"
|
||||
|
||||
[roles.component-orange-2]
|
||||
source = "#c06f00"
|
||||
light = "#c06f00"
|
||||
dark = "#b86f04"
|
||||
|
||||
[roles.component-orange-3]
|
||||
source = "#a45000"
|
||||
light = "#a45000"
|
||||
dark = "#95682d"
|
||||
|
||||
[roles.component-yellow-1]
|
||||
source = "#e5d99b"
|
||||
light = "#e5d99b"
|
||||
dark = "#a9985f"
|
||||
|
||||
[roles.component-yellow-2]
|
||||
source = "#a28200"
|
||||
light = "#a28200"
|
||||
dark = "#9a8100"
|
||||
|
||||
[roles.component-yellow-3]
|
||||
source = "#826700"
|
||||
light = "#826700"
|
||||
dark = "#857100"
|
||||
|
||||
[strokes] # on the 64 grid
|
||||
geometry = 2.55
|
||||
action = 4.25
|
||||
|
||||
@@ -57,6 +57,8 @@ circle_tt false
|
||||
circle_ttt false
|
||||
close false
|
||||
coil false
|
||||
colored_components false
|
||||
colored_components_off false
|
||||
combine false
|
||||
compare false
|
||||
component false
|
||||
@@ -104,6 +106,7 @@ flange false
|
||||
flat false
|
||||
fold false
|
||||
folder false
|
||||
frame_member false
|
||||
gear false
|
||||
graph false
|
||||
ground false
|
||||
@@ -147,6 +150,7 @@ origin false
|
||||
pan false
|
||||
params false
|
||||
paste false
|
||||
path_flange false
|
||||
pattern_circ false
|
||||
pattern_path false
|
||||
pattern_rect false
|
||||
@@ -211,6 +215,7 @@ shield false
|
||||
silhouette false
|
||||
sketch false
|
||||
sketch_break false
|
||||
sketch_cancel false
|
||||
sketch_chamfer false
|
||||
sketch_chamfer_angle false
|
||||
sketch_chamfer_equal false
|
||||
@@ -242,6 +247,7 @@ status_error false
|
||||
status_recovery false
|
||||
style false
|
||||
suppress false
|
||||
surface false
|
||||
surface_blend false
|
||||
surface_boundary false
|
||||
surface_extend false
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Colored components</title>
|
||||
<path d="M9.235 17.706L15.059 20.882L15.059 48.941L9.235 44.926Z" fill="#c06f00"/>
|
||||
<path d="M35.176 9.235L15.059 20.882L15.059 48.941L36.765 35.139Z" fill="#a45000"/>
|
||||
<path d="M29.046 5L35.176 8.176L15.196 19.718L9.066 16.541Z" fill="#f1d2af"/>
|
||||
<path d="M29.046 5.529L35.176 8.706L35.176 36.235L15.059 49.471L9.235 45.765L9.066 17.071Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.066 17.6L15.059 20.882L34.647 9.235" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.059 20.882L15.059 47.067" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L44.949 51.122L32.222 43.774Z" fill="#328bd3"/>
|
||||
<path d="M54.934 20.173L44.949 25.944L44.949 51.122L54.934 45.352Z" fill="#236ead"/>
|
||||
<path d="M42.059 12.412L54.934 20.173L44.949 25.944L32 17.706Z" fill="#b9ddf8"/>
|
||||
<path d="M41.159 11.766L54.934 20.173L54.934 45.352L44.949 51.122L31.164 42.715L31.164 17.536Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L54.934 20.173" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44.949 25.944L44.949 51.122" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L31.015 59L18.299 51.652Z" fill="#a28200"/>
|
||||
<path d="M41.921 40.121L31.015 46.411L31.015 59L41.921 52.711Z" fill="#826700"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L31.015 46.411L18.299 39.062Z" fill="#e5d99b"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L41.921 52.711L31.015 59L18.299 51.652L18.299 39.062Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L41.921 40.121" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.015 46.411L31.015 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Disable component colors</title>
|
||||
<path d="M9.235 17.706L15.059 20.882L15.059 48.941L9.235 44.926Z" fill="#328bd3"/>
|
||||
<path d="M35.176 9.235L15.059 20.882L15.059 48.941L36.765 35.139Z" fill="#236ead"/>
|
||||
<path d="M29.046 5L35.176 8.176L15.196 19.718L9.066 16.541Z" fill="#b9ddf8"/>
|
||||
<path d="M29.046 5.529L35.176 8.706L35.176 36.235L15.059 49.471L9.235 45.765L9.066 17.071Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.066 17.6L15.059 20.882L34.647 9.235" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.059 20.882L15.059 47.067" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L44.949 51.122L32.222 43.774Z" fill="#328bd3"/>
|
||||
<path d="M54.934 20.173L44.949 25.944L44.949 51.122L54.934 45.352Z" fill="#236ead"/>
|
||||
<path d="M42.059 12.412L54.934 20.173L44.949 25.944L32 17.706Z" fill="#b9ddf8"/>
|
||||
<path d="M41.159 11.766L54.934 20.173L54.934 45.352L44.949 51.122L31.164 42.715L31.164 17.536Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L54.934 20.173" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44.949 25.944L44.949 51.122" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L31.015 59L18.299 51.652Z" fill="#328bd3"/>
|
||||
<path d="M41.921 40.121L31.015 46.411L31.015 59L41.921 52.711Z" fill="#236ead"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L31.015 46.411L18.299 39.062Z" fill="#b9ddf8"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L41.921 52.711L31.015 59L18.299 51.652L18.299 39.062Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L41.921 40.121" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.015 46.411L31.015 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Frame Member</title>
|
||||
<path d="M9.25 18.86L17.63 23.7" fill="none" stroke="#ffffff" stroke-width="4.4" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M9.25 18.86L17.63 23.7" fill="none" stroke="#236ead" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="9.25" cy="18.86" r="3" fill="#236ead"/>
|
||||
<path d="M11.88 20.39L40.62 36.98L40.62 50.25L11.88 33.66Z" fill="#328bd3"/>
|
||||
<path d="M23.38 13.75L52.12 30.34L40.62 36.98L11.88 20.39Z" fill="#b9ddf8"/>
|
||||
<path d="M52.12 30.34L40.62 36.98L40.62 50.25L52.12 43.61ZM49.8 34.35L42.94 38.31L42.94 46.24L49.8 42.28Z" fill="#8e9faf" fill-rule="evenodd"/>
|
||||
<defs><clipPath id="fm_hole"><path d="M49.8 34.35L42.94 38.31L42.94 46.24L49.8 42.28Z" fill="none"/></clipPath></defs>
|
||||
<g clip-path="url(#fm_hole)">
|
||||
<path d="M49.8 34.35L42.94 38.31L42.94 46.24L49.8 42.28Z" fill="#236ead"/>
|
||||
<path d="M49.8 34.35L49.8 42.28L42.86 38.27L42.86 30.34Z" fill="#328bd3"/>
|
||||
<path d="M49.8 42.28L42.94 46.24L35.99 42.23L42.86 38.27Z" fill="#b9ddf8"/>
|
||||
</g>
|
||||
<path d="M49.8 34.35L42.94 38.31L42.94 46.24L49.8 42.28Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M23.38 13.75L52.12 30.34L52.12 43.61L40.62 50.25L11.88 33.66L11.88 20.39Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M11.88 20.39L40.62 36.98L52.12 30.34" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M40.62 36.98L40.62 50.25" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M41.74 37.62L54.75 45.14" fill="none" stroke="#ffffff" stroke-width="4.4" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M41.74 37.62L54.75 45.14" fill="none" stroke="#236ead" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="54.75" cy="45.14" r="3" fill="#236ead"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Path Flange</title>
|
||||
<path d="M9.5 44L23.5 44A6 6 0 0 0 29.5 38L29.5 26A6 6 0 0 1 35.5 20L43.5 20A6 6 0 0 1 47.74 21.76L54.81 28.83" fill="none" stroke="#34465b" stroke-width="11.4" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M9.5 44L23.5 44" fill="none" stroke="#b8c3cf" stroke-width="6.3" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M23.5 44A6 6 0 0 0 29.5 38L29.5 26A6 6 0 0 1 35.5 20L43.5 20A6 6 0 0 1 47.74 21.76L54.81 28.83" fill="none" stroke="#328bd3" stroke-width="6.3" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 638 B |
@@ -104,10 +104,10 @@ pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
|
||||
|
||||
/// The same original mark as a single quiet silhouette, without face seams or outlines.
|
||||
pub(crate) fn brand_mark_silhouette(p: &egui::Painter, c: Pos2, height: f32, ink: Color32) {
|
||||
brand_mark_styled(p, c, height, [ink; 4], false);
|
||||
brand_mark_styled(p, c, height, [ink; 3], false);
|
||||
}
|
||||
|
||||
fn brand_mark_styled(p: &egui::Painter, c: Pos2, height: f32, colours: [Color32; 4], edges: bool) {
|
||||
fn brand_mark_styled(p: &egui::Painter, c: Pos2, height: f32, colours: [Color32; 3], edges: bool) {
|
||||
let s = height / 412.217;
|
||||
let m = |pts: &[(f32, f32)]| -> Vec<egui::Pos2> { pts.iter().map(|&(x, y)| pos2(c.x + (x - 178.5) * s, c.y + (y - 206.1) * s)).collect() };
|
||||
// egui only fills convex shapes, so each face is a few overlapping convex pieces.
|
||||
@@ -115,7 +115,7 @@ fn brand_mark_styled(p: &egui::Painter, c: Pos2, height: f32, colours: [Color32;
|
||||
p.add(egui::Shape::convex_polygon(m(pts), col, Stroke::NONE));
|
||||
};
|
||||
// The faces are separated by gaps that show the application bar, not by white lines.
|
||||
let [light, mid, dark, _] = colours;
|
||||
let [light, mid, dark] = colours;
|
||||
let edge_col = crate::theme::Tokens::get().app_bar;
|
||||
face(light, &[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)]);
|
||||
face(light, &[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
|
||||
@@ -129,7 +129,7 @@ fn brand_mark_styled(p: &egui::Painter, c: Pos2, height: f32, colours: [Color32;
|
||||
if !edges {
|
||||
return;
|
||||
}
|
||||
// White edge lines.
|
||||
// The seams between the faces and around the mark, in the bar's colour, so they read as gaps.
|
||||
let edge = Stroke::new(10.0 * s, edge_col);
|
||||
let line = |pts: &[(f32, f32)]| {
|
||||
p.add(egui::Shape::line(m(pts), edge));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Constraint, assembly snap and dimension marker artwork.
|
||||
//! Constraint, assembly snap, hole placement and dimension marker artwork.
|
||||
use crate::dialogs_assembly::SnapKind;
|
||||
use crate::theme::Tokens;
|
||||
use egui::{Color32, Pos2, Stroke, vec2};
|
||||
@@ -57,6 +57,39 @@ pub(crate) fn snap(painter: &egui::Painter, c: Pos2, tip: Option<Pos2>, kind: Sn
|
||||
painter.galley(at, g, t.text);
|
||||
}
|
||||
}
|
||||
/// A place a hole can snap to, before the cursor reaches it: a small hollow mark of its kind
|
||||
/// (a square for a face's centre, a ring for a round edge's centre).
|
||||
pub(crate) fn snap_hint(painter: &egui::Painter, c: Pos2, kind: SnapKind, col: Color32) {
|
||||
let ic = Tokens::get().icons;
|
||||
for stroke in [Stroke::new(3.5, ic.marker_halo), Stroke::new(1.4, col)] {
|
||||
match kind {
|
||||
SnapKind::FaceCenter => {
|
||||
painter.rect_stroke(egui::Rect::from_center_size(c, vec2(8.0, 8.0)), 1.0, stroke, egui::StrokeKind::Middle);
|
||||
}
|
||||
SnapKind::CircleCenter => {
|
||||
painter.circle_stroke(c, 4.5, stroke);
|
||||
}
|
||||
SnapKind::EdgeMid | SnapKind::Vertex => {
|
||||
let r = 5.0;
|
||||
let diamond = vec![c + vec2(0.0, -r), c + vec2(r, 0.0), c + vec2(0.0, r), c + vec2(-r, 0.0)];
|
||||
painter.add(egui::Shape::convex_polygon(diamond, Color32::TRANSPARENT, stroke));
|
||||
}
|
||||
}
|
||||
}
|
||||
painter.circle_filled(c, 1.3, col);
|
||||
}
|
||||
|
||||
/// A placed hole's drag handle: a ring round the hole's centre with a cross through it, filled
|
||||
/// while it is hovered or dragged.
|
||||
pub(crate) fn hole_handle(painter: &egui::Painter, c: Pos2, col: Color32, hot: bool) {
|
||||
let ic = Tokens::get().icons;
|
||||
let r = 6.0;
|
||||
painter.circle_filled(c, r + 3.5, ic.marker_halo);
|
||||
painter.circle(c, r, if hot { col } else { col.gamma_multiply(0.35) }, Stroke::new(1.6, col));
|
||||
let ink = Stroke::new(1.3, if hot { ic.highlight } else { col });
|
||||
painter.line_segment([c - vec2(r + 3.0, 0.0), c + vec2(r + 3.0, 0.0)], ink);
|
||||
painter.line_segment([c - vec2(0.0, r + 3.0), c + vec2(0.0, r + 3.0)], ink);
|
||||
}
|
||||
pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
|
||||
Some(match name {
|
||||
"Coincident" => "c_coincident",
|
||||
@@ -75,8 +108,8 @@ pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
|
||||
}
|
||||
pub(crate) fn glyph_letter(name: &str) -> &'static str {
|
||||
match name {
|
||||
"Horizontal" => "H",
|
||||
"Vertical" => "V",
|
||||
"Horizontal" => "-",
|
||||
"Vertical" => "|",
|
||||
_ => "?",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,11 +152,11 @@ pub(crate) fn curved_arrow(painter: &egui::Painter, at: Pos2, clockwise: bool, h
|
||||
pub(crate) const AXIS_RADIUS: f32 = 15.0;
|
||||
pub(crate) fn axis_tip(painter: &egui::Painter, c: Pos2, name: &str, d: &Vec3, at: Pos2, depth: f64, hovered: bool, t: Tokens) {
|
||||
let col = if d.x != 0.0 {
|
||||
t.triad[0]
|
||||
t.axis_x
|
||||
} else if d.y != 0.0 {
|
||||
t.triad[1]
|
||||
t.axis_y
|
||||
} else {
|
||||
t.triad[2]
|
||||
t.axis_z
|
||||
};
|
||||
let positive = !name.starts_with('-');
|
||||
let light = t.panel.r() > 128;
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Vybrat…",
|
||||
"Circular": "Kruhově",
|
||||
"Circumference": "Obvod",
|
||||
"Clockwise": "Ve směru hodinových ručiček",
|
||||
"Close draft": "Zavřít koncept",
|
||||
"Collisions": "Kolize",
|
||||
"Colour": "Barva",
|
||||
@@ -269,7 +270,7 @@
|
||||
"Preview Row": "Náhled řádku",
|
||||
"Preview leaves the current design unchanged.": "Náhled aktuální návrh nemění.",
|
||||
"Profiles/Curves": "Profily/křivky",
|
||||
"Quantity": "Množství",
|
||||
"Quantity": "Počet",
|
||||
"RECENT": "NEDÁVNÉ",
|
||||
"REFERENCE": "REFERENCE",
|
||||
"RULED SURFACE": "PŘÍMKOVÁ PLOCHA",
|
||||
@@ -322,6 +323,7 @@
|
||||
"Search commands": "Hledat příkazy",
|
||||
"Search this library": "Prohledat tuto knihovnu",
|
||||
"Section": "Řez",
|
||||
"Section position": "Poloha průřezu",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Vybrat jiné: podržte levé tlačítko na modelu (nebo pravé tlačítko › Vybrat jiné…) a zobrazí se vše pod kurzorem; dalším kliknutím na stejné místo vyberete další prvek. Dvojklikem prostředním tlačítkem přejdete na výchozí pohled (Předvolby › Navigace).",
|
||||
"Select bodies (a face picks its body)": "Vyberte tělesa (plocha vybere své těleso)",
|
||||
"Select bodies or faces instead": "Vyberte místo toho tělesa nebo plochy",
|
||||
@@ -333,6 +335,7 @@
|
||||
"Show Agent Cursor": "Zobrazit kurzor agenta",
|
||||
"Show in folder": "Zobrazit ve složce",
|
||||
"Show the pointer an AI agent moves (#35)": "Zobrazit ukazatel, kterým pohybuje AI agent (#35)",
|
||||
"Show tool at cursor": "Zobrazit nástroj u kurzoru",
|
||||
"Signed axes": "Osy se znaménkem",
|
||||
"Size": "Velikost",
|
||||
"Sketch constraints": "Vazby náčrtu",
|
||||
@@ -345,9 +348,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft se zavřel dříve, než se tyto návrhy uložily. Lze otevřít jejich poslední automatické uložení.",
|
||||
"Specialise": "Specializovat",
|
||||
"Speed": "Rychlost",
|
||||
"Square": "Čtverec",
|
||||
"Stand it up": "Postavit",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Postavit: otočení o čtvrt otáčky kolem osy X jako jeden prvek Posunutí, který lze vrátit.",
|
||||
"Stand this part up?": "Postavit tento díl?",
|
||||
"Start angle": "Počáteční úhel",
|
||||
"Start bulge": "Vypouklost začátku",
|
||||
"Start continuity": "Návaznost začátku",
|
||||
"Step": "Krok",
|
||||
@@ -412,6 +417,7 @@
|
||||
"angle, height": "úhel, výška",
|
||||
"appearance.dielectric_ior": "Index lomu dielektrika",
|
||||
"appearance.metallic": "Kovovost",
|
||||
"appearance.only_available": "Pouze dostupné vzhledy",
|
||||
"appearance.pbr_help": "Tento povrch zobrazíte v renderu nebo v produktovém PNG. Bez lomu a průchodu světla.",
|
||||
"appearance.pbr_opaque": "Neprůhledné PBR",
|
||||
"appearance.roughness": "Drsnost",
|
||||
@@ -530,6 +536,10 @@
|
||||
"cmd.file_insert_step": "Vložit STEP",
|
||||
"cmd.file_save_mesh": "Uložit jako síť",
|
||||
"cmd.file_slicers": "Nainstalované slicery",
|
||||
"cmd.frame_member": "Prvek rámu",
|
||||
"cmd.frame_profile_insert": "Vložit profil",
|
||||
"cmd.frame_profile_list": "Knihovna profilů",
|
||||
"cmd.frame_profile_properties": "Vlastnosti profilu",
|
||||
"cmd.inspect_accessibility": "Analýza přístupnosti",
|
||||
"cmd.inspect_center_of_mass": "Těžiště",
|
||||
"cmd.inspect_clearance": "Vůle",
|
||||
@@ -569,6 +579,7 @@
|
||||
"cmd.material_create": "Nový fyzický materiál",
|
||||
"cmd.material_delete": "Smazat fyzický materiál",
|
||||
"cmd.material_edit": "Upravit fyzický materiál",
|
||||
"cmd.material_inspect": "Zkontrolovat fyzický materiál",
|
||||
"cmd.material_list": "Seznam materiálů",
|
||||
"cmd.material_properties_directional": "Směrový modul pružnosti",
|
||||
"cmd.material_properties_validate": "Ověřit data fyzického materiálu",
|
||||
@@ -615,6 +626,8 @@
|
||||
"cmd.plastic_rules": "Seznam pravidel pro plasty",
|
||||
"cmd.plastic_snap_fit": "Zacvakávací spoj",
|
||||
"cmd.render_backends": "Renderovací jádra",
|
||||
"cmd.render_camera_fit": "Zarámovat objekt renderu",
|
||||
"cmd.render_camera_get": "Prohlédnout kameru renderu",
|
||||
"cmd.render_cancel": "Zrušit render",
|
||||
"cmd.render_export": "Exportovat obrázek renderu",
|
||||
"cmd.render_preflight": "Zkontrolovat požadavek na render",
|
||||
@@ -622,6 +635,7 @@
|
||||
"cmd.render_settings_apply": "Použít kameru renderu",
|
||||
"cmd.render_settings_get": "Nastavení renderu",
|
||||
"cmd.render_settings_set": "Uložit nastavení renderu",
|
||||
"cmd.render_stage_backdrop": "Uložit fyzické pozadí renderu",
|
||||
"cmd.render_start": "Renderovat obrázek",
|
||||
"cmd.render_status": "Průběh renderu",
|
||||
"cmd.select_clear": "Zrušit výběr",
|
||||
@@ -960,6 +974,7 @@
|
||||
"motion.key_here": "Vložit klíčový snímek sem",
|
||||
"motion.key_here_hint": "Přidat v tomto kroku klíčový snímek s aktuální hodnotou kloubu",
|
||||
"motion.no_collisions": "Během studie nedochází ke kolizím",
|
||||
"motion.plot_quantity": "Množství",
|
||||
"motion.step_back": "Krok zpět",
|
||||
"motion.step_forward": "Krok vpřed",
|
||||
"motion.to_end": "Na konec",
|
||||
@@ -985,6 +1000,16 @@
|
||||
"pattern.direction_1": "Směr 1",
|
||||
"pattern.direction_2": "Směr 2",
|
||||
"pattern.flip_direction": "Převrátit směr",
|
||||
"perf.click_for_details": "Klikněte pro podrobnosti: graf časů snímků, histogram a co vykreslila GPU",
|
||||
"perf.draw_calls": "Volání vykreslení",
|
||||
"perf.line_segments": "Úsečky",
|
||||
"perf.low_1": "1% minimum",
|
||||
"perf.renderer": "Vykreslování",
|
||||
"perf.scene_build": "Sestavení scény",
|
||||
"perf.triangles": "Trojúhelníky",
|
||||
"perf.ui_time": "Čas UI",
|
||||
"perf.viewport": "Pohled",
|
||||
"perf.worst": "nejhorší",
|
||||
"pick a hole to seat it, or OK places it at the origin": "vyberte otvor pro usazení, nebo OK umístí do počátku",
|
||||
"pick one or two vertices, edges or faces": "vyberte jeden nebo dva vrcholy, hrany či plochy",
|
||||
"prefs.cpu": "CPU (softwarově)",
|
||||
@@ -997,20 +1022,51 @@
|
||||
"recovery.min_ago": "před {count} min",
|
||||
"recovery.never_saved": "nikdy neuloženo",
|
||||
"render.background": "Pozadí scény",
|
||||
"render.bloom_enable": "Záře ve výstupu",
|
||||
"render.bloom_help": "Kreativní záře z lineárních jasů se třemi normalizovanými průchody. Poloměr v pixelech výstupu, přičtení před sRGB. Nejde o simulaci čočky, objemu ani zlepšení konvergence. Surové EXR se nemění.",
|
||||
"render.bloom_opaque": "Záře vyžaduje neprůhledný výstup. Průhledné PNG se odmítne, neprovede se skryté sloučení.",
|
||||
"render.bloom_radius": "Poloměr záře",
|
||||
"render.bloom_strength": "Síla záře",
|
||||
"render.bloom_threshold": "Lineární práh jasů",
|
||||
"render.cpu_aa_help": "Prostorové vzorky: více vzorků zlepší hrany a prodlouží vykreslení.",
|
||||
"render.cpu_key_rim": "Hlavní a obrysové",
|
||||
"render.cpu_lighting": "Předvolba osvětlení",
|
||||
"render.cpu_neutral": "Neutrální",
|
||||
"render.cpu_opaque_required": "CPU trasování vyžaduje neprůhledné pozadí. Vypněte průhledné pozadí.",
|
||||
"render.cpu_options_pending": "Panel podporuje vzorky hran, předvolby světel, stíny a expozici. Další volby jsou v API.",
|
||||
"render.cpu_primary_samples": "Vzorky hran na pixel",
|
||||
"render.cpu_shadows": "Vrhat stíny",
|
||||
"render.cpu_summary": "Neprůhledný CPU PNG · světla · stíny · bez skla a textur",
|
||||
"render.cpu_warm_cool": "Teplé a studené",
|
||||
"render.exr_pending": "GPU může uchovat surové lineární EXR. Komprese, vícevrstvé obrazy a pomocné průchody zatím nejsou dostupné.",
|
||||
"render.fit": "Přizpůsobit produktu",
|
||||
"render.gpu_depth": "Maximální hloubka cesty",
|
||||
"render.gpu_options_help": "Tyto volby platí pro tuto úlohu. Expozice vychází z uloženého nastavení.",
|
||||
"render.gpu_path_failed": "Trasování cest GPU není dostupné",
|
||||
"render.gpu_path_first_setup": "První příprava na této GPU může trvat několik minut; SolidCraft lze mezitím používat. Při dalších spuštěních se použijí zkompilované shadery, pokud je ovladač uchová.",
|
||||
"render.gpu_path_preparing": "Trasování cest GPU: kompilace shaderů…",
|
||||
"render.gpu_seed": "Náhodné semeno",
|
||||
"render.gpu_unavailable": "Trasování cest GPU není na tomto zařízení připraveno.",
|
||||
"render.image_advanced": "Pokročilé nastavení GPU",
|
||||
"render.image_cancelled": "Render zrušen",
|
||||
"render.image_cancelling": "Rušení",
|
||||
"render.image_completed": "Obrázek připraven k exportu",
|
||||
"render.image_encoding": "Kódování PNG",
|
||||
"render.image_export": "Exportovat PNG",
|
||||
"render.image_export": "Exportovat hotový obrázek",
|
||||
"render.image_exposure": "Expozice",
|
||||
"render.image_failed": "Render selhal",
|
||||
"render.image_height": "Výška",
|
||||
"render.image_help": "Náhledový obrázek z CPU · PNG · přibližná průhlednost",
|
||||
"render.image_host_missing": "Není dostupný vykreslovací hostitel.",
|
||||
"render.image_method": "Metoda obrazu",
|
||||
"render.image_overwrite": "Nahradit existující soubor",
|
||||
"render.image_preparing": "Příprava obrázku",
|
||||
"render.image_progressive": "Progresivní render",
|
||||
"render.image_queued": "Čeká na render",
|
||||
"render.image_reduce": "Zkuste menší obrázek nebo jednodušší scénu.",
|
||||
"render.image_running": "Renderování obrázku",
|
||||
"render.image_samples": "Vzorků na pixel",
|
||||
"render.image_scene_stale": "Před kontrolou nebo spuštěním dokončete modelování nebo obnovte scénu.",
|
||||
"render.image_stale": "Tento obrázek používá scénu zachycenou před pozdějšími změnami.",
|
||||
"render.image_start": "Spustit render",
|
||||
"render.image_supported": "Požadavek na render je podporován; fronta a paměť se znovu ověří při spuštění.",
|
||||
@@ -1020,17 +1076,135 @@
|
||||
"render.image_unknown": "Stav renderu není k dispozici",
|
||||
"render.image_use_largest": "Použít největší velikost v tomto poměru stran:",
|
||||
"render.image_width": "Šířka",
|
||||
"render.material_absorption": "Absorpce R · G · B (na mm)",
|
||||
"render.material_add": "Přidat nebo upravit těleso",
|
||||
"render.material_assignments": "Přiřazení těles",
|
||||
"render.material_binding_enable": "Zahrnout úpravy tohoto tělesa",
|
||||
"render.material_box": "Osově zarovnaný kvádr",
|
||||
"render.material_browse": "Vybrat obrázek…",
|
||||
"render.material_certification": "Uzavřené sklo používá geometrickou vzdálenost. GPU příprava ověřuje topologii, orientaci, překryvy a optická rozhraní; kontrola jen přijímá požadavek.",
|
||||
"render.material_choose_body": "Vybrat těleso scény",
|
||||
"render.material_closed": "Uzavřená objemová síť",
|
||||
"render.material_coat": "Čirý povlak na základním materiálu",
|
||||
"render.material_coat_enable": "Zapnout čirý povlak",
|
||||
"render.material_coat_help": "Ilustrativní dielektrický povlak na stávajícím neprůhledném vzhledu. Nulová váha ponechá základ beze změny.",
|
||||
"render.material_color_map": "Obraz základní barvy (sRGB)",
|
||||
"render.material_custom": "Vlastní ilustrativní optika",
|
||||
"render.material_enable": "Použít tyto úpravy materiálů GPU",
|
||||
"render.material_filter": "Filtrovat tělesa scény…",
|
||||
"render.material_filter_help": "Zobrazuje se nejvýše 128 shod. U velkých scén zpřesněte filtr.",
|
||||
"render.material_glass": "Sklo a optická absorpce",
|
||||
"render.material_glass_enable": "Použít fyzikální sklo na těleso",
|
||||
"render.material_glass_exclusive": "Tento renderer nepodporuje sklo s povlakem nebo mapami na témže tělese.",
|
||||
"render.material_glass_help": "Původní ilustrativní recepty. Vzorky ukazují propustnost, ne render. Absorpce je −ln(lineární propustnosti) / referenční vzdálenost; alfa se nemění.",
|
||||
"render.material_gpu_required": "Aktivní úpravy vyžadují GPU tracing. Před kontrolou nebo spuštěním vyberte GPU nebo úpravy vypněte.",
|
||||
"render.material_ior": "Index lomu (IOR)",
|
||||
"render.material_limits": "Nejvýše 128 nastavení těles, 32 oddělených skel, 64 povlaků a 128 vazeb textur. Shodné mapy sdílejí paměť; limity zařízení a celkové paměti zůstávají aktivní.",
|
||||
"render.material_map_invalid": "Cesty map musí být omezené místní cesty; síla normál musí být konečná mezi 0 a 1.",
|
||||
"render.material_map_required": "Před použitím map vyberte obraz barvy nebo normál.",
|
||||
"render.material_maps": "Rovinné mapy barvy a normál",
|
||||
"render.material_maps_enable": "Zapnout mapy neprůhledného tělesa",
|
||||
"render.material_maps_help": "PNG/JPEG do 1024×1024; celkem 16 MiB kódovaných dat. U a V jsou kolmé vektory opakování ve světových mm (nejvýše 128 opakování). Barva je sRGB, normály OpenGL +Y. Soubory se zachytí při kontrole/spuštění.",
|
||||
"render.material_normal_map": "Obraz normál (OpenGL, +Y)",
|
||||
"render.material_normal_strength": "Síla normál (0–1)",
|
||||
"render.material_origin": "Počátek mapování X · Y · Z (mm)",
|
||||
"render.material_refresh": "Obnovit tělesa scény",
|
||||
"render.material_refresh_needed": "Scéna se změnila. Před přidáním cíle obnovte seznam.",
|
||||
"render.material_remove": "Odstranit přiřazení",
|
||||
"render.material_repeat_u": "Jedno opakování U X · Y · Z (mm)",
|
||||
"render.material_repeat_v": "Jedno opakování V X · Y · Z (mm)",
|
||||
"render.material_revalidate": "Znovu ověřit těleso",
|
||||
"render.material_revalidate_needed": "Přiřazení patří ke starší revizi scény. Před renderem znovu ověřte přesnou identitu tělesa.",
|
||||
"render.material_roughness": "Drsnost povlaku (0,045–1)",
|
||||
"render.material_shadow": "Propouštět přímé stínové paprsky",
|
||||
"render.material_target_missing": "Pořadí a název tělesa již nesouhlasí. Odstraňte přiřazení a znovu výslovně vyberte těleso.",
|
||||
"render.material_thickness": "Tloušťka vrstvy (mm)",
|
||||
"render.material_thin": "Tenká vrstva",
|
||||
"render.material_title": "Materiály GPU obrazu",
|
||||
"render.material_transient": "Dočasné optické úpravy pro tuto kartu. Neukládají se do návrhu, nastavení renderu ani knihovny materiálů.",
|
||||
"render.material_weight": "Váha povlaku (0–1)",
|
||||
"render.method_cpu": "Trasování paprsků CPU",
|
||||
"render.method_gpu": "Trasování cest GPU",
|
||||
"render.method_raster": "CPU rastr",
|
||||
"render.navigation_help": "Kamera se ovládá stejně jako v návrhu (Předvolby › Navigace). Uložit nastavení zaznamená aktuální kameru.",
|
||||
"render.opaque_image_summary": "Náhledové PNG · neprůhledné PBR · smíšená průhlednost",
|
||||
"render.opaque_preview_help": "Neprůhledné PBR a jednoduché barvy; smíšená průhlednost. Lom světla, HDRI a finální render zatím nejsou k dispozici.",
|
||||
"render.output_format_status": "PNG je přesný dokončený obraz. Volitelné GPU EXR uchovává lineární FLOAT32 radianci před filtry. Export nikdy nenahrazuje výsledek náhledem nebo jinou úlohou.",
|
||||
"render.perspective": "Perspektivní kamera",
|
||||
"render.preview": "Integrovaný náhled",
|
||||
"render.preview_help": "Rastrové stínování a smíšená průhlednost. Lom světla, HDRI a finální render zatím nejsou k dispozici.",
|
||||
"render.progressive_image_summary": "Náhledové PNG · neprůhledné PBR · postupné vzorkování",
|
||||
"render.reload_settings": "Znovu načíst uložené nastavení",
|
||||
"render.restore_camera": "Obnovit uloženou kameru",
|
||||
"render.result_actual_help": "Jeden pixel obrázku na jeden pixel obrazovky.",
|
||||
"render.result_capture": "Zachycená revize",
|
||||
"render.result_current": "Zachycená revize modelu je aktuální.",
|
||||
"render.result_empty": "Zatím není hotový obrázek. Zkontrolujte nastavení a spusťte vykreslování.",
|
||||
"render.result_fit": "Přizpůsobit obrázek",
|
||||
"render.result_job": "Úloha",
|
||||
"render.result_navigation": "Posun a přiblížení používají nastavení navigace. Mezerník + tažení posouvá.",
|
||||
"render.result_open": "Otevřít hotový obrázek na hlavním plátně.",
|
||||
"render.result_preview": "Živý náhled",
|
||||
"render.result_retained": "Zobrazuje se poslední hotový obrázek; novější vykreslení jej zatím nenahradilo.",
|
||||
"render.result_title": "Výsledek vykreslení",
|
||||
"render.return_design": "Zpět k návrhu",
|
||||
"render.ribbon_backgrounds": "Pozadí",
|
||||
"render.ribbon_check": "Kontrola",
|
||||
"render.ribbon_check_help": "Ověřit přesný aktuální návrh renderu. Bez skrytého přepnutí vykreslovače, zmenšení nebo nahrazení zdrojů.",
|
||||
"render.ribbon_edit_lights": "Upravit světla",
|
||||
"render.ribbon_environment_help": "HDR osvětlení, pozadí kamery a fyzické plochy jsou samostatné ovládací prvky s potvrzením nahrazení.",
|
||||
"render.ribbon_finish_dialog": "Nejprve dokončete dialog souboru nebo potvrzení.",
|
||||
"render.ribbon_frame": "Zarámovat",
|
||||
"render.ribbon_hdr_library": "Knihovna HDR",
|
||||
"render.ribbon_job_active": "Render již běží; před spuštěním dalšího jej výslovně zrušte.",
|
||||
"render.ribbon_light_rigs": "Sestavy světel",
|
||||
"render.ribbon_lights_help": "Zkontrolovat světelné sestavy nebo upravit podporovaná světla; aktuální nastavení nahradí až výslovné použití.",
|
||||
"render.ribbon_materials": "Materiály",
|
||||
"render.ribbon_materials_help": "Otevřít inspektor vzhledu a původní povrchy. Optické úpravy nemění skrytě technické vlastnosti.",
|
||||
"render.ribbon_no_active_job": "Není aktivní render ke zrušení.",
|
||||
"render.ribbon_no_result": "Před exportem dokončete render.",
|
||||
"render.ribbon_no_saved_camera": "Před obnovením kamery uložte záběr.",
|
||||
"render.ribbon_saved_camera": "Uložená kamera",
|
||||
"render.ribbon_shots": "Záběry",
|
||||
"render.ribbon_studios": "Studia",
|
||||
"render.ribbon_studios_help": "Procházejte dostupná studia a před použitím zkontrolujte nahrazované prvky. Koncepty zůstávají označené.",
|
||||
"render.ribbon_surface_maps": "Mapy povrchu",
|
||||
"render.save_setup": "Uložit nastavení",
|
||||
"render.setup_name": "Název nastavení",
|
||||
"render.studio_add_area": "Přidat plošné světlo",
|
||||
"render.studio_ambient": "Konstantní zář prostředí",
|
||||
"render.studio_aperture": "Poloměr clony",
|
||||
"render.studio_area": "Plošné světlo",
|
||||
"render.studio_area_help": "Nejvýše čtyři jednostranné obdélníky. Kolmé poloviční hrany U a V určují velikost; U × V míří na osvětlenou stranu. Souřadnice jsou světové milimetry.",
|
||||
"render.studio_center": "Střed X, Y, Z (mm)",
|
||||
"render.studio_denoise": "Potlačit šum v hotovém obrazu",
|
||||
"render.studio_denoise_help": "Závěrečný filtr respektuje hrany; může změkčit detaily. Sklo a ostré zrcadlové povrchy se nefiltrují.",
|
||||
"render.studio_direction": "Směr ke slunci (X, Y, Z)",
|
||||
"render.studio_environment": "Prostředí",
|
||||
"render.studio_environment_bounds": "Intenzita prostředí musí být 0–100 a otočení −360° až 360°.",
|
||||
"render.studio_environment_none": "Bez mapy prostředí",
|
||||
"render.studio_focus": "Osová zaostřovací vzdálenost",
|
||||
"render.studio_half_u": "Poloviční hrana U (mm)",
|
||||
"render.studio_half_v": "Poloviční hrana V (mm)",
|
||||
"render.studio_hdr": "Soubor Radiance HDR",
|
||||
"render.studio_hdr_browse": "Vybrat HDR…",
|
||||
"render.studio_hdr_help": "Radiance .hdr, panorama 2:1, nejvýše 2048 × 1024. EXR není dostupné. Předpoklad barev platí jen při neurčených primárních barvách.",
|
||||
"render.studio_hdr_required": "Vyberte místní soubor .hdr.",
|
||||
"render.studio_hdr_srgb": "Neurčené primární barvy chápat jako lineární sRGB",
|
||||
"render.studio_intensity": "Intenzita prostředí",
|
||||
"render.studio_lens": "Hloubka ostrosti",
|
||||
"render.studio_lens_perspective": "Hloubka ostrosti vyžaduje perspektivní kameru.",
|
||||
"render.studio_lighting": "Studiové osvětlení",
|
||||
"render.studio_lighting_enable": "Použít vlastní studiové osvětlení",
|
||||
"render.studio_radiance": "Lineární RGB zář",
|
||||
"render.studio_remove_area": "Odebrat plošné světlo",
|
||||
"render.studio_rotation": "Otočení prostředí",
|
||||
"render.studio_runtime_help": "Optické úpravy těles jsou v Materiálech GPU obrazu. Ovládání studia a materiálů je dočasné a nemění uložené vzhledy.",
|
||||
"render.studio_sky": "Umělecká obloha",
|
||||
"render.studio_sky_help": "Původní přechod oblohy pro osvětlení; nejde o kalibrovaný model atmosféry.",
|
||||
"render.studio_sun": "Slunce",
|
||||
"render.studio_sun_radius": "Úhlový poloměr",
|
||||
"render.studio_tone_map": "Mapování tónů",
|
||||
"render.unavailable_preview": "Toto nastavení vyžaduje nedostupný profil náhledu.",
|
||||
"render.unsaved": "Neuložené změny náhledu",
|
||||
"render.use_preview": "Použít integrovaný náhled",
|
||||
@@ -1060,7 +1234,8 @@
|
||||
"search.try": "Zadejte název příkazu, prvek v tomto návrhu nebo parametr. Chcete-li spustit příkaz s hodnotami, zadejte jeho id a objekt JSON: {example}",
|
||||
"search.valid": "Platné",
|
||||
"settings.display_quality_note": "Jemnější úrovně kreslí zakřivené plochy a hrany hladčeji, ale síťování trvá déle. Mění se jen zobrazení, nikdy návrh ani exporty.",
|
||||
"settings.frame_rate_note": "Zobrazuje snímky za sekundu v pravém dolním rohu pohledu. Dokud je zapnuto, pohled se překresluje nepřetržitě.",
|
||||
"settings.frame_rate_details_note": "Přidá graf časů posledních snímků s vykreslenými trojúhelníky, histogram časů snímků a to, co vykreslila GPU. Kliknutím na snímkovou frekvenci v pohledu je zobrazíte nebo skryjete.",
|
||||
"settings.frame_rate_note": "Zobrazuje snímky za sekundu v pravém dolním rohu pohledu.",
|
||||
"settings.measure_axis_distances": "Zobrazit vzdálenosti X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Při otevřeném měření zobrazit kromě celkové vzdálenosti i složky ve směru os.",
|
||||
"settings.msaa_applies_on_restart": "Vyhlazování se projeví při příštím spuštění SolidCraftu",
|
||||
@@ -1116,12 +1291,115 @@
|
||||
"toolbar.customize_note": "Karta {tab}. Zaškrtnutím příkazu přidáte jeho tlačítko na panel nástrojů. Můžete také přetáhnout řádek z rozbalovací nabídky panelu na panel nástrojů, odstranit tlačítko přetažením mimo panel nástrojů nebo použít připínáček na řádku rozbalovací nabídky.",
|
||||
"toolbar.image": "OBRÁZEK",
|
||||
"toolbar.render": "RENDER",
|
||||
"toolbar.render_appearance": "VZHLED",
|
||||
"toolbar.render_camera": "KAMERA",
|
||||
"toolbar.render_environment": "PROSTŘEDÍ",
|
||||
"toolbar.render_lighting": "OSVĚTLENÍ",
|
||||
"toolbar.render_scene": "SCÉNA",
|
||||
"trace.points": "Bodů podél cesty: {count}",
|
||||
"trace.stopped_by_contact": "Končí ve vzorku {sample}: {a} by prošlo skrz {b}",
|
||||
"trace.stopped_by_loop": "Končí ve vzorku {sample}: smyčku přes {joint} už nelze dále uzavřít",
|
||||
"ui.1_degree_of_freedom": "1 stupeň volnosti",
|
||||
"ui.3d_mouse_axes_reassigned": "Osy jsou v Předvolbách přeřazené a to platí i po této kalibraci.",
|
||||
"ui.3d_mouse_button": "Tlačítko {number}",
|
||||
"ui.3d_mouse_button_nothing": "Nic",
|
||||
"ui.3d_mouse_button_settings": "Nastavení 3D myši",
|
||||
"ui.3d_mouse_buttons": "Tlačítka",
|
||||
"ui.3d_mouse_buttons_hint": "Stiskněte tlačítko na 3D myši, ať ho tu najdete, a vyberte, co má dělat.",
|
||||
"ui.3d_mouse_buttons_no_device": "Připojte 3D myš a nastavte, co dělají její tlačítka.",
|
||||
"ui.3d_mouse_buttons_reset": "Obnovit výchozí tlačítka",
|
||||
"ui.3d_mouse_calibrate": "Kalibrovat…",
|
||||
"ui.3d_mouse_calibrate_tip": "Pohybujte hlavicí podle pokynů v okně, aby se SolidCraft naučil, kterým směrem a jak daleko se pohybuje každá osa této 3D myši. Uloží se pro tuto 3D myš.",
|
||||
"ui.3d_mouse_calibrate_title": "Kalibrace 3D myši",
|
||||
"ui.3d_mouse_calibrate_wait": "Jednou pohněte 3D myší, aby ji SolidCraft uviděl.",
|
||||
"ui.3d_mouse_calibrated": "Zkalibrováno",
|
||||
"ui.3d_mouse_calibration": "Kalibrace 3D myši",
|
||||
"ui.3d_mouse_calibration_done": "Hotovo. Uložte ji a použije se vždy, když bude tato 3D myš připojená.",
|
||||
"ui.3d_mouse_calibration_no_response": "Jedna osa se skoro nepohnula, takže ji nelze zkalibrovat. Začněte znovu a pokaždé hlavicí pohněte dál.",
|
||||
"ui.3d_mouse_circles_help": "Kruhy ukazují, co zařízení posílá po kalibraci. Zkušební model ukazuje, co s tím udělá pohled po obrácení, rychlosti a přiřazení os.",
|
||||
"ui.3d_mouse_dead_zone": "Mrtvá zóna",
|
||||
"ui.3d_mouse_dead_zone_tip": "Jak daleko se musí hlavice pohnout, než se pohne pohled, jako podíl jejího plného rozsahu. Zvyšte ji, pokud pohled ujíždí, i když je hlavice v klidu.",
|
||||
"ui.3d_mouse_denied": "SolidCraft vidí 3D myš, ale nesmí ji číst. Pravidlo udev najdete v poznámkách k nastavení 3D myši.",
|
||||
"ui.3d_mouse_denied_short": "Nalezeno, ale nelze číst",
|
||||
"ui.3d_mouse_driven_by": "Který kalibrovaný pohyb zařízení ovládá tuto osu. Když vyberete pohyb, který už používá jiná osa, obě se prohodí.",
|
||||
"ui.3d_mouse_driven_by_label": "ovládá",
|
||||
"ui.3d_mouse_driver": "Ovladač 3D myši",
|
||||
"ui.3d_mouse_driver_auto": "Automaticky",
|
||||
"ui.3d_mouse_driver_direct": "Přímo (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Zatím se čte přes spacenavd: zařízení samotné se nepodařilo otevřít.",
|
||||
"ui.3d_mouse_driver_only_direct": "Zařízení se čte přímo; spacenavd neběží",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Čte se přes spacenavd, jiná možnost není",
|
||||
"ui.3d_mouse_forget": "Zapomenout kalibraci",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Vlevo ve výřezu",
|
||||
"ui.3d_mouse_ghost_left_window": "Vlevo v okně",
|
||||
"ui.3d_mouse_ghost_position": "Poloha",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Vpravo ve výřezu",
|
||||
"ui.3d_mouse_hint": "Posun a otáčení pohledu současně; náčrty používají jen posun a přiblížení.",
|
||||
"ui.3d_mouse_kind_cap": "Návrat do středu",
|
||||
"ui.3d_mouse_kind_question": "Jaký typ 3D myši to je?",
|
||||
"ui.3d_mouse_kind_trackball": "Trackball",
|
||||
"ui.3d_mouse_looks_like_cap": "Hodnota zůstává, dokud ji držíte, jako u zařízení typu „Návrat do středu“.",
|
||||
"ui.3d_mouse_looks_like_trackball": "Hodnota se vrací zpět, jakmile přestanete hýbat, jako u trackballu.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Rotace X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Rotace Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Rotace Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Posun X",
|
||||
"ui.3d_mouse_motion_translation_y": "Posun Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Posun Z",
|
||||
"ui.3d_mouse_navigate": "Navigace pomocí 3D myši",
|
||||
"ui.3d_mouse_none": "3D myš nenalezena",
|
||||
"ui.3d_mouse_not_calibrated": "Nezkalibrováno",
|
||||
"ui.3d_mouse_offer": "Kalibrace 3D myši opraví prohozené nebo obrácené osy a nastaví její rychlost a pocit z ovládání.",
|
||||
"ui.3d_mouse_offer_never": "Už nezobrazovat",
|
||||
"ui.3d_mouse_offer_swap": "Přepnout na Hidraw (přímý ovladač)",
|
||||
"ui.3d_mouse_reading": "Hodnoty",
|
||||
"ui.3d_mouse_reset_axes": "Obnovit přiřazení os",
|
||||
"ui.3d_mouse_reset_view": "Obnovit pohled",
|
||||
"ui.3d_mouse_restore": "Obnovit výchozí nastavení 3D myši",
|
||||
"ui.3d_mouse_restore_ask": "Vrátit nastavení 3D myši na výchozí hodnoty? Kalibrace a přiřazení tlačítek zůstanou.",
|
||||
"ui.3d_mouse_section": "3D myš",
|
||||
"ui.3d_mouse_show_in_view": "Zobrazit 3D myš ve 3D pohledu",
|
||||
"ui.3d_mouse_show_in_view_tip": "Obrázek vaší 3D myši v levém dolním rohu, který se hýbe s ní.",
|
||||
"ui.3d_mouse_skip": "Moje zařízení to neumí",
|
||||
"ui.3d_mouse_skip_tip": "Přeskočí tento pohyb v obou směrech a kalibrace ho vynechá.",
|
||||
"ui.3d_mouse_speed": "Rychlost 3D myši",
|
||||
"ui.3d_mouse_step_lift": "Zvedněte hlavici nahoru, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_press": "Zatlačte hlavici dolů, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_pull": "Přitáhněte hlavici k sobě, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_push": "Odtlačte hlavici od sebe, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_rest": "Dejte ruku z hlavice a nechte ji v klidu.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Dejte ruku z kuličky a nechte ji v klidu.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Otočte rolovacím kroužkem o pár cvaknutí po směru hodinových ručiček.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Otočte rolovacím kroužkem o pár cvaknutí proti směru hodinových ručiček.",
|
||||
"ui.3d_mouse_step_roll_away": "Několikrát kutálejte kuličkou od sebe, pak ji nechte zastavit.",
|
||||
"ui.3d_mouse_step_roll_left": "Několikrát kutálejte kuličkou doleva, pak ji nechte zastavit.",
|
||||
"ui.3d_mouse_step_roll_right": "Několikrát kutálejte kuličkou doprava, pak ji nechte zastavit.",
|
||||
"ui.3d_mouse_step_roll_toward": "Několikrát kutálejte kuličkou k sobě, pak ji nechte zastavit.",
|
||||
"ui.3d_mouse_step_slide_left": "Posuňte hlavici doleva, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_slide_right": "Posuňte hlavici doprava, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Několikrát roztočte kuličku po směru hodinových ručiček, pak ji nechte zastavit.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Několikrát roztočte kuličku proti směru hodinových ručiček, pak ji nechte zastavit.",
|
||||
"ui.3d_mouse_step_tilt_away": "Nakloňte vršek hlavice od sebe (okraj nejblíž vám jde nahoru), podržte a pusťte.",
|
||||
"ui.3d_mouse_step_tilt_left": "Nakloňte hlavici doleva (levá hrana dolů), podržte a pusťte.",
|
||||
"ui.3d_mouse_step_tilt_right": "Nakloňte hlavici doprava (pravá hrana dolů), podržte a pusťte.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Nakloňte vršek hlavice k sobě (okraj nejblíž vám jde dolů), podržte a pusťte.",
|
||||
"ui.3d_mouse_step_touch": "Položte ruku na hlavici, ale netlačte na ni.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Otočte hlavici po směru hodinových ručiček, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Otočte hlavici proti směru hodinových ručiček, podržte a pusťte.",
|
||||
"ui.3d_mouse_switch_cap": "Kalibrovat jako návrat do středu",
|
||||
"ui.3d_mouse_switch_trackball": "Kalibrovat jako trackball",
|
||||
"ui.3d_mouse_view_axis": "Osa pohledu",
|
||||
"ui.3d_mouse_view_off": "Vypnuto",
|
||||
"ui.3d_mouse_view_off_tip": "Profil ovladače 3D myši tuto osu vypíná, takže její pohyb s pohledem nehýbe. Když tu vyberete pohyb jiné osy, prohodí se s ní a s pohledem přestane hýbat.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbit vlevo / vpravo",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbit nahoru / dolů",
|
||||
"ui.3d_mouse_view_pan_sideways": "Posun vlevo / vpravo",
|
||||
"ui.3d_mouse_view_pan_up_down": "Posun nahoru / dolů",
|
||||
"ui.3d_mouse_view_roll": "Natočení",
|
||||
"ui.3d_mouse_view_unmapped": "Osa {n}",
|
||||
"ui.3d_mouse_view_zoom": "Zoom",
|
||||
"ui.3d_mouse_waiting": "Čeká na pohyb",
|
||||
"ui.3d_mouse_wiggle": "Pokud máte nějakou připojenou, zahýbejte s ní.",
|
||||
"ui.3d_sketch": "3D náčrt",
|
||||
"ui.activate": "Aktivovat",
|
||||
"ui.active_component": "Aktivní komponenta",
|
||||
@@ -1183,6 +1461,7 @@
|
||||
"ui.agent_chat_files_left_out": "K jedné zprávě lze připojit nejvýše {max} souborů; vynecháno: {files}",
|
||||
"ui.agent_chat_files_loading": "Čekání na přečtení souborů",
|
||||
"ui.agent_chat_files_max": "K jedné zprávě nejvýše {max} souborů",
|
||||
"ui.agent_chat_flatpak_none_found": "Nebyl nalezen žádný kódovací agent. Sandbox Flatpaku ve výchozím stavu nevidí agenty nainstalované ve vaší domovské složce a nemá přístup k síti. Chcete-li povolit Claude Code a jeho adaptér ACP nainstalované přes nvm nebo do ~/.local/bin, spusťte v terminálu tento příkaz a poté SolidCraft restartujte. Udělí přístup k uvedeným programům a přihlašovacím souborům a také přístup k síti:",
|
||||
"ui.agent_chat_import_failed": "{name}: import selhal: {error}",
|
||||
"ui.agent_chat_intro": "Chatujte s kódovacím agentem, kterého již používáte, nebo s modelem na zvoleném endpointu. Pracuje na otevřeném návrhu pomocí nástrojů SolidCraftu: jeho změny vidíte průběžně a každou zprávu lze vrátit jedním krokem. Co smí dělat bez ptaní, nastavíte v Předvolby > AI.",
|
||||
"ui.agent_chat_key_in_env": "klíč nastaven",
|
||||
@@ -1278,6 +1557,7 @@
|
||||
"ui.ai_http_busy": "endpoint je zaneprázdněný nebo překročil limit; zkuste to později",
|
||||
"ui.ai_http_key": "endpoint odmítl klíč: zkontrolujte jej v Předvolby > AI",
|
||||
"ui.ai_http_not_found": "takový endpoint nebo model neexistuje: zkontrolujte základní URL a název modelu",
|
||||
"ui.ai_http_too_large": "konverzace je pro tento endpoint příliš velká; začněte nový chat",
|
||||
"ui.ai_key": "API klíč",
|
||||
"ui.ai_key_hint": "volitelné; uloženo v systémové klíčence",
|
||||
"ui.ai_key_needs_https": "Klíč se odesílá pouze přes HTTPS nebo na tento počítač.",
|
||||
@@ -1305,7 +1585,6 @@
|
||||
"ui.ai_permissions": "Oprávnění",
|
||||
"ui.ai_preset": "Předvolba",
|
||||
"ui.ai_remove": "Odebrat",
|
||||
"ui.ai_request_limit": "zastaveno po 24 požadavcích na jednu zprávu; pro pokračování pošlete další zprávu",
|
||||
"ui.ai_reset": "Obnovit",
|
||||
"ui.ai_settings": "Nastavení AI…",
|
||||
"ui.ai_show_features": "Zobrazit funkce AI",
|
||||
@@ -1350,6 +1629,7 @@
|
||||
"ui.blocked": "Blokováno",
|
||||
"ui.bodies": "Tělesa",
|
||||
"ui.bodies_all_if_none": "Tělesa (všechna, pokud žádné)",
|
||||
"ui.bottom": "Zespodu",
|
||||
"ui.box": "Kvádr",
|
||||
"ui.break": "Přerušit",
|
||||
"ui.browser_bodies_one": "{count} těleso",
|
||||
@@ -1481,6 +1761,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtrovat příkazy nebo klávesy",
|
||||
"ui.findInTimeline": "Najít na časové ose",
|
||||
"ui.findInWindow": "Najít v okně",
|
||||
"ui.fine": "Jemná",
|
||||
"ui.finest": "Nejjemnější",
|
||||
"ui.finish_sketch": "Dokončit náčrt",
|
||||
"ui.fit": "Přizpůsobit",
|
||||
"ui.fix_unfix": "Zafixovat/uvolnit",
|
||||
@@ -1488,6 +1770,7 @@
|
||||
"ui.flip": "Převrátit",
|
||||
"ui.format": "Formát",
|
||||
"ui.frame_rate": "Snímková frekvence",
|
||||
"ui.frame_rate_details": "Podrobnosti snímkové frekvence",
|
||||
"ui.front": "Zepředu",
|
||||
"ui.fully_constrained": "Plně zavazbeno",
|
||||
"ui.function": "Funkce",
|
||||
@@ -1502,6 +1785,7 @@
|
||||
"ui.grid_follows_view_hint": "Boční pohledy zobrazí mřížku na svislé rovině, která je k nim přivrácená, místo na zemi",
|
||||
"ui.ground_shadow": "Stín na podložce",
|
||||
"ui.groupSelected": "Seskupit vybrané",
|
||||
"ui.hatch": "Šrafování",
|
||||
"ui.heading_add_ins": "DOPLŇKY",
|
||||
"ui.heading_assemble": "SESTAVA",
|
||||
"ui.heading_box": "KVÁDR",
|
||||
@@ -1568,6 +1852,7 @@
|
||||
"ui.heading_torus": "ANULOID",
|
||||
"ui.heading_utilities": "NÁSTROJE",
|
||||
"ui.heading_utility": "NÁSTROJE",
|
||||
"ui.heading_weldments": "SVAŘENCE",
|
||||
"ui.help": "Nápověda",
|
||||
"ui.hide": "Skrýt",
|
||||
"ui.hideAxes": "Skrýt osy",
|
||||
@@ -1575,6 +1860,8 @@
|
||||
"ui.hideOrigin": "Skrýt počátek",
|
||||
"ui.hidePlanes": "Skrýt roviny",
|
||||
"ui.hideProfile": "Skrýt profil",
|
||||
"ui.hide_origin_by_default": "Ve výchozím stavu skrýt počátek",
|
||||
"ui.hide_origin_note": "Nové návrhy začínají se skrytou složkou Počátek. Návrh si ponechá vlastní nastavení počátku, jakmile ho zobrazíte nebo skryjete.",
|
||||
"ui.history_back": "O krok zpět",
|
||||
"ui.history_below_browser": "Pod prohlížečem",
|
||||
"ui.history_first": "Přejít na začátek",
|
||||
@@ -1704,6 +1991,7 @@
|
||||
"ui.project_face_edges_sketch": "Promítat hrany plochy do nových náčrtů",
|
||||
"ui.properties": "Vlastnosti",
|
||||
"ui.pull_direction": "Směr vyjímání",
|
||||
"ui.quality": "Kvalita",
|
||||
"ui.quantity": "Počet",
|
||||
"ui.quantity_2": "Počet 2",
|
||||
"ui.quit": "Ukončit",
|
||||
@@ -1792,65 +2080,13 @@
|
||||
"ui.smooth_wheel_zoom": "Plynulé přiblížení kolečkem",
|
||||
"ui.smooth_wheel_zoom_tip": "Vypnutím se každý pohyb kolečka projeví okamžitě, bez dobíhání",
|
||||
"ui.snap": "Přichytávání",
|
||||
"ui.space_mouse_axes_reassigned": "Osy jsou v Předvolbách přeřazené a to platí i po této kalibraci.",
|
||||
"ui.space_mouse_calibrate": "Kalibrovat…",
|
||||
"ui.space_mouse_calibrate_tip": "Pohybujte hlavicí podle pokynů v okně, aby se SolidCraft naučil, kterým směrem a jak daleko se pohybuje každá osa této 3D myši. Uloží se pro tuto 3D myš.",
|
||||
"ui.space_mouse_calibrate_title": "Kalibrace 3D myši",
|
||||
"ui.space_mouse_calibrated": "Zkalibrováno",
|
||||
"ui.space_mouse_calibration": "Kalibrace 3D myši",
|
||||
"ui.space_mouse_calibration_done": "Hotovo. Uložte ji a použije se vždy, když bude tato 3D myš připojená.",
|
||||
"ui.space_mouse_calibration_no_response": "Jedna osa se skoro nepohnula, takže ji nelze zkalibrovat. Začněte znovu a pokaždé hlavicí pohněte dál.",
|
||||
"ui.space_mouse_dead_zone": "Mrtvá zóna",
|
||||
"ui.space_mouse_dead_zone_tip": "Jak daleko se musí hlavice pohnout, než se pohne pohled, jako podíl jejího plného rozsahu. Zvyšte ji, pokud pohled ujíždí, i když je hlavice v klidu.",
|
||||
"ui.space_mouse_driven_by": "Který kalibrovaný pohyb zařízení ovládá tuto osu. Když vyberete pohyb, který už používá jiná osa, obě se prohodí.",
|
||||
"ui.space_mouse_driven_by_label": "ovládá",
|
||||
"ui.space_mouse_driver": "Ovladač 3D myši",
|
||||
"ui.space_mouse_driver_auto": "Automaticky",
|
||||
"ui.space_mouse_driver_direct": "Přímo (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Zařízení se čte přímo; spacenavd neběží",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Čte se přes spacenavd, jiná možnost není",
|
||||
"ui.space_mouse_forget": "Zapomenout kalibraci",
|
||||
"ui.space_mouse_hint": "Posun a otáčení pohledu současně; náčrty používají jen posun a přiblížení.",
|
||||
"ui.space_mouse_motion_rotation_x": "Rotace X",
|
||||
"ui.space_mouse_motion_rotation_y": "Rotace Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Rotace Z",
|
||||
"ui.space_mouse_motion_translation_x": "Posun X",
|
||||
"ui.space_mouse_motion_translation_y": "Posun Y",
|
||||
"ui.space_mouse_motion_translation_z": "Posun Z",
|
||||
"ui.space_mouse_navigate": "Navigace pomocí SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "3D myš nenalezena",
|
||||
"ui.space_mouse_not_calibrated": "Nezkalibrováno",
|
||||
"ui.space_mouse_reading": "Hodnoty",
|
||||
"ui.space_mouse_reset_axes": "Obnovit přiřazení os",
|
||||
"ui.space_mouse_step_lift": "Zvedněte hlavici nahoru, podržte a pusťte.",
|
||||
"ui.space_mouse_step_press": "Zatlačte hlavici dolů, podržte a pusťte.",
|
||||
"ui.space_mouse_step_pull": "Přitáhněte hlavici k sobě, podržte a pusťte.",
|
||||
"ui.space_mouse_step_push": "Odtlačte hlavici od sebe, podržte a pusťte.",
|
||||
"ui.space_mouse_step_rest": "Dejte ruku z hlavice a nechte ji v klidu.",
|
||||
"ui.space_mouse_step_slide_left": "Posuňte hlavici doleva, podržte a pusťte.",
|
||||
"ui.space_mouse_step_slide_right": "Posuňte hlavici doprava, podržte a pusťte.",
|
||||
"ui.space_mouse_step_tilt_back": "Nakloňte hlavici dozadu (přední hrana nahoru), podržte a pusťte.",
|
||||
"ui.space_mouse_step_tilt_forward": "Nakloňte hlavici dopředu (přední hrana dolů), podržte a pusťte.",
|
||||
"ui.space_mouse_step_tilt_left": "Nakloňte hlavici doleva (levá hrana dolů), podržte a pusťte.",
|
||||
"ui.space_mouse_step_tilt_right": "Nakloňte hlavici doprava (pravá hrana dolů), podržte a pusťte.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Otočte hlavici po směru hodinových ručiček, podržte a pusťte.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Otočte hlavici proti směru hodinových ručiček, podržte a pusťte.",
|
||||
"ui.space_mouse_view_axis": "Osa pohledu",
|
||||
"ui.space_mouse_view_off": "Vypnuto",
|
||||
"ui.space_mouse_view_off_tip": "Profil ovladače 3D myši tuto osu vypíná, takže její pohyb s pohledem nehýbe. Když tu vyberete pohyb jiné osy, prohodí se s ní a s pohledem přestane hýbat.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Orbit vlevo / vpravo",
|
||||
"ui.space_mouse_view_orbit_up_down": "Orbit nahoru / dolů",
|
||||
"ui.space_mouse_view_pan_sideways": "Posun vlevo / vpravo",
|
||||
"ui.space_mouse_view_pan_up_down": "Posun nahoru / dolů",
|
||||
"ui.space_mouse_view_roll": "Natočení",
|
||||
"ui.space_mouse_view_unmapped": "Osa {n}",
|
||||
"ui.space_mouse_view_zoom": "Zoom",
|
||||
"ui.spacing": "Rozteč",
|
||||
"ui.spacing_2": "Rozteč 2",
|
||||
"ui.sphere": "Koule",
|
||||
"ui.spline": "Splajn",
|
||||
"ui.split_body": "Rozdělit těleso",
|
||||
"ui.stand_up_tip": "Když se soubor STEP nebo IGES otevře s osou Z nahoru, nabídnout otočení o čtvrt otáčky kolem X",
|
||||
"ui.standard": "Standardní",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Začít znovu",
|
||||
"ui.start_page": "Úvodní stránka",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"Choose…": "Auswählen…",
|
||||
"Circular": "Kreisförmig",
|
||||
"Circumference": "Umfang",
|
||||
"Clockwise": "Im Uhrzeigersinn",
|
||||
"Close draft": "Entwurf schließen",
|
||||
"Collapse": "Einklappen",
|
||||
"Collisions": "Kollisionen",
|
||||
@@ -337,7 +338,7 @@
|
||||
"Profile / Edges": "Profil / Kanten",
|
||||
"Profiles or curves": "Profile oder Kurven",
|
||||
"Profiles/Curves": "Profile/Kurven",
|
||||
"Quantity": "Größe",
|
||||
"Quantity": "Anzahl",
|
||||
"RECENT": "ZULETZT",
|
||||
"REFERENCE": "REFERENZ",
|
||||
"RULED SURFACE": "REGELFLÄCHE",
|
||||
@@ -404,6 +405,7 @@
|
||||
"Search this library": "Diese Bibliothek durchsuchen",
|
||||
"Second plane": "Zweite Ebene",
|
||||
"Section": "Schnitt",
|
||||
"Section position": "Lage des Querschnitts",
|
||||
"Sections (one sketch each)": "Querschnitte (je eine Skizze)",
|
||||
"Select Other": "Andere auswählen",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Anderes auswählen: Halten Sie die linke Taste ruhig auf dem Modell gedrückt (oder Rechtsklick › Anderes auswählen…), um alles unter dem Cursor aufzulisten; klicken Sie erneut an dieselbe Stelle, um das nächste Element dort auszuwählen. Doppelklick mit der mittleren Taste wechselt zur Startansicht (Einstellungen › Navigation).",
|
||||
@@ -419,6 +421,7 @@
|
||||
"Show Agent Cursor": "Agenten-Cursor anzeigen",
|
||||
"Show in folder": "Im Ordner anzeigen",
|
||||
"Show the pointer an AI agent moves (#35)": "Den Zeiger anzeigen, den ein KI-Agent bewegt (#35)",
|
||||
"Show tool at cursor": "Werkzeug am Cursor anzeigen",
|
||||
"Signed axes": "Vorzeichenbehaftete Achsen",
|
||||
"Size": "Größe",
|
||||
"Sketch constraints": "Skizzenbedingungen",
|
||||
@@ -432,9 +435,11 @@
|
||||
"Specialise": "Spezialisieren",
|
||||
"Speed": "Geschwindigkeit",
|
||||
"Splitting tool": "Trennwerkzeug",
|
||||
"Square": "Quadrat",
|
||||
"Stand it up": "Aufrichten",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Aufrichten: eine Vierteldrehung um X, als ein Verschieben-Merkmal, das sich rückgängig machen lässt.",
|
||||
"Stand this part up?": "Dieses Teil aufrichten?",
|
||||
"Start angle": "Startwinkel",
|
||||
"Start bulge": "Startausbauchung",
|
||||
"Start continuity": "Startstetigkeit",
|
||||
"Step": "Schritt",
|
||||
@@ -520,6 +525,7 @@
|
||||
"angle, height": "Winkel, Höhe",
|
||||
"appearance.dielectric_ior": "Brechungsindex des Dielektrikums",
|
||||
"appearance.metallic": "Metallanteil",
|
||||
"appearance.only_available": "Nur verfügbare Erscheinungsbilder",
|
||||
"appearance.pbr_help": "Diese Oberfläche in Render oder einem Produkt-PNG ansehen. Keine Brechung oder Transmission.",
|
||||
"appearance.pbr_opaque": "Opakes PBR",
|
||||
"appearance.roughness": "Rauheit",
|
||||
@@ -562,8 +568,10 @@
|
||||
"cmd.component_activate": "Komponente aktivieren",
|
||||
"cmd.component_break_link": "Verknüpfung trennen",
|
||||
"cmd.component_capture_position": "Position erfassen",
|
||||
"cmd.component_copies": "Identische Kopien finden",
|
||||
"cmd.component_create": "Neue Komponente",
|
||||
"cmd.component_delete": "Komponente löschen",
|
||||
"cmd.component_extract": "In Datei extrahieren",
|
||||
"cmd.component_from_bodies": "Komponenten aus Körpern erstellen",
|
||||
"cmd.component_insert_design": "Konstruktion einfügen",
|
||||
"cmd.component_links": "Verknüpfungen auflisten",
|
||||
@@ -576,6 +584,7 @@
|
||||
"cmd.component_properties": "Physikalische Eigenschaften der Komponente",
|
||||
"cmd.component_relink": "Quelle neu verknüpfen",
|
||||
"cmd.component_rename": "Komponente umbenennen",
|
||||
"cmd.component_replace_with_link": "Durch Verknüpfung ersetzen",
|
||||
"cmd.component_revert_position": "Position zurücksetzen",
|
||||
"cmd.component_unlock": "Inhalt entsperren",
|
||||
"cmd.component_update_links": "Verknüpfungen aktualisieren",
|
||||
@@ -622,6 +631,7 @@
|
||||
"cmd.document_metadata": "Konstruktionseigenschaften",
|
||||
"cmd.document_metadata_remove": "Konstruktionseigenschaft entfernen",
|
||||
"cmd.document_metadata_set": "Konstruktionseigenschaften ändern",
|
||||
"cmd.document_revision": "Konstruktionsrevision",
|
||||
"cmd.document_units": "Einheiten der Konstruktion ändern",
|
||||
"cmd.edit_checkpoint": "Prüfpunkt",
|
||||
"cmd.edit_restore_checkpoint": "Prüfpunkt wiederherstellen",
|
||||
@@ -638,6 +648,10 @@
|
||||
"cmd.file_insert_step": "STEP einfügen",
|
||||
"cmd.file_save_mesh": "Als Netz speichern",
|
||||
"cmd.file_slicers": "Installierte Slicer",
|
||||
"cmd.frame_member": "Rahmenelement",
|
||||
"cmd.frame_profile_insert": "Profil einfügen",
|
||||
"cmd.frame_profile_list": "Profilbibliothek",
|
||||
"cmd.frame_profile_properties": "Profileigenschaften",
|
||||
"cmd.inspect_accessibility": "Zugänglichkeitsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Massenschwerpunkt",
|
||||
"cmd.inspect_clearance": "Freiraum",
|
||||
@@ -677,6 +691,7 @@
|
||||
"cmd.material_create": "Neues physikalisches Material",
|
||||
"cmd.material_delete": "Physisches Material löschen",
|
||||
"cmd.material_edit": "Physisches Material bearbeiten",
|
||||
"cmd.material_inspect": "Physikalisches Material prüfen",
|
||||
"cmd.material_list": "Materialien auflisten",
|
||||
"cmd.material_properties_directional": "Richtungsabhängiger Elastizitätsmodul",
|
||||
"cmd.material_properties_validate": "Physikalische Materialdaten prüfen",
|
||||
@@ -723,13 +738,32 @@
|
||||
"cmd.plastic_rules": "Kunststoffregeln auflisten",
|
||||
"cmd.plastic_snap_fit": "Rastverbindung",
|
||||
"cmd.render_backends": "Render-Backends",
|
||||
"cmd.render_camera_fit": "Renderobjekt einpassen",
|
||||
"cmd.render_camera_get": "Renderkamera prüfen",
|
||||
"cmd.render_cancel": "Rendering abbrechen",
|
||||
"cmd.render_export": "PNG exportieren",
|
||||
"cmd.render_hdr_inspect": "HDR-Quelle untersuchen",
|
||||
"cmd.render_library_appearance_apply": "Opakes Bibliotheksaussehen anwenden",
|
||||
"cmd.render_library_describe": "Render-Ressource untersuchen",
|
||||
"cmd.render_library_environment_apply": "Integrierte HDR-Umgebung anwenden",
|
||||
"cmd.render_library_list": "Render-Bibliothek durchsuchen",
|
||||
"cmd.render_library_physical_prepare": "Prepare Physical Reference",
|
||||
"cmd.render_library_studio_apply": "Erstellte Studiolichter anwenden",
|
||||
"cmd.render_library_studio_prepare": "Erstellte Studiolichter vorbereiten",
|
||||
"cmd.render_library_surface_bind": "Save Surface Binding",
|
||||
"cmd.render_library_surface_prepare": "Erstellte Oberflächenmaps vorbereiten",
|
||||
"cmd.render_library_surface_unbind": "Remove Surface Binding",
|
||||
"cmd.render_library_targets": "Ziele für Render-Ressourcen suchen",
|
||||
"cmd.render_preflight": "Renderanfrage prüfen",
|
||||
"cmd.render_result_pin": "Angezeigtes Render-Ergebnis behalten",
|
||||
"cmd.render_scene": "Renderszene prüfen",
|
||||
"cmd.render_settings_apply": "Renderkamera anwenden",
|
||||
"cmd.render_settings_capture_hdr": "Render-Einstellungen mit HDR speichern",
|
||||
"cmd.render_settings_get": "Rendereinstellungen",
|
||||
"cmd.render_settings_set": "Rendereinstellungen speichern",
|
||||
"cmd.render_stage_backdrop": "Physischen Renderhintergrund speichern",
|
||||
"cmd.render_stage_clear": "Remove Render Stage",
|
||||
"cmd.render_stage_ground": "Save Render Ground",
|
||||
"cmd.render_start": "Bild rendern",
|
||||
"cmd.render_status": "Renderfortschritt",
|
||||
"cmd.select_clear": "Auswahl aufheben",
|
||||
@@ -1051,6 +1085,8 @@
|
||||
"manage.material_hint": "Wählen Sie Körper und deren physikalisches Material aus.",
|
||||
"manage.parameters_hint": "Bearbeiten Sie Ausdrücke und Einheiten. Änderungen werden mit der Eingabetaste oder beim Verlassen eines Feldes übernommen.",
|
||||
"manage.rules_hint": "Wählen Sie eine Regel zum Bearbeiten oder erstellen Sie eine neue. Bestätigen Sie, um die Regel zu speichern.",
|
||||
"material.editor.axes_numeric": "1 / 2 / 3",
|
||||
"material.editor.axes_wood": "L / R / T",
|
||||
"measure.at": "Bei",
|
||||
"measure.item": "Element {number}",
|
||||
"messages.ago_h": "vor {n} Std.",
|
||||
@@ -1069,6 +1105,7 @@
|
||||
"motion.key_here": "Schlüsselbild hier setzen",
|
||||
"motion.key_here_hint": "Ein Schlüsselbild bei diesem Schritt mit dem aktuellen Gelenkwert hinzufügen",
|
||||
"motion.no_collisions": "Keine Kollisionen in der Untersuchung",
|
||||
"motion.plot_quantity": "Größe",
|
||||
"motion.step_back": "Schritt zurück",
|
||||
"motion.step_forward": "Schritt vor",
|
||||
"motion.to_end": "Zum Ende",
|
||||
@@ -1081,6 +1118,7 @@
|
||||
"nav.fusion.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, rechte Maustaste dreht",
|
||||
"nav.inventor.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, Strg+Mitte zoomt, rechte Maustaste dreht",
|
||||
"nav.openscad.help": "Linksziehen auf leerem Hintergrund dreht, Umschalt+Linksziehen zieht ein Auswahlrechteck auf, Skizzengeometrie wird durch Ziehen bearbeitet; rechtes oder mittleres Ziehen verschiebt, Umschalt+rechtes oder Umschalt+mittleres Ziehen zoomt",
|
||||
"nav.perspective.sketch": "Beim Bearbeiten einer Skizze wird die orthografische Ansicht verwendet. Beenden Sie die Skizze, um die Perspektive umzuschalten.",
|
||||
"nav.solidworks.help": "Mittlere Maustaste dreht, Strg+Mitte verschiebt, Umschalt+Mitte zoomt, rechte Maustaste dreht",
|
||||
"nav.tinkercad.help": "Rechte Maustaste dreht, mittlere oder Umschalt+rechte Maustaste verschiebt",
|
||||
"nav.touchpad.help": "Umschalt gedrückt halten und den Zeiger zum Verschieben bewegen; mit Alt/Option drehen. Keine Maustaste nötig. Vertikales Scrollen zoomt.",
|
||||
@@ -1114,13 +1152,35 @@
|
||||
"recovery.just_now": "gerade eben",
|
||||
"recovery.min_ago": "vor {count} Min.",
|
||||
"recovery.never_saved": "nie gespeichert",
|
||||
"render.add_ground": "Add fitted ground to setup",
|
||||
"render.background": "Szenenhintergrund",
|
||||
"render.bloom_enable": "Darstellungs-Bloom",
|
||||
"render.bloom_help": "Kreativer Ausgabeglanz aus linearen Glanzlichtern mit drei normierten Durchläufen. Radius in Ausgabepixeln; Addition vor sRGB. Keine Linsen-, Volumen- oder Konvergenzsimulation. Das rohe EXR bleibt unverändert.",
|
||||
"render.bloom_opaque": "Bloom benötigt eine deckende Ausgabe. Transparentes PNG wird abgelehnt, nicht still abgeflacht.",
|
||||
"render.bloom_radius": "Bloom-Radius",
|
||||
"render.bloom_strength": "Bloom-Stärke",
|
||||
"render.bloom_threshold": "Linearer Glanzlichtschwellwert",
|
||||
"render.camera_pitch": "Kameraneigung",
|
||||
"render.camera_set": "Render-Kamera einstellen",
|
||||
"render.clear_stage": "Remove saved stage",
|
||||
"render.cpu_budget": "CPU-Ressourcenprofil",
|
||||
"render.cpu_budget_bodies": "Körperobergrenze",
|
||||
"render.cpu_budget_compact": "Kompakt (bisherige Grenzen)",
|
||||
"render.cpu_budget_details": "Ressourcengrenzen",
|
||||
"render.cpu_budget_help": "Größere Grenzen ausdrücklich wählen. Auflösung und Samples werden nie automatisch reduziert.",
|
||||
"render.cpu_budget_memory": "Speicherobergrenze pro Auftrag",
|
||||
"render.cpu_budget_not_guarantee": "Dies sind Zulassungsgrenzen, keine RAM-Reservierung oder Geschwindigkeitsgarantie. Geometrie, Film und Vorschauen teilen das Speicherbudget. Das Rendern bleibt abbrechbar.",
|
||||
"render.cpu_budget_resize": "Der Entwurf überschreitet das CPU-Profil oder sein Kamera-Sample-Budget. Kleinere Abmessungen ausdrücklich bestätigen oder ein größeres Profil wählen.",
|
||||
"render.cpu_budget_studio": "Studio (große Bilder)",
|
||||
"render.cpu_budget_triangles": "Dreiecksobergrenze",
|
||||
"render.cpu_budget_workstation": "Workstation (komplexe Szenen)",
|
||||
"render.exr_pending": "Die GPU kann rohe lineare EXR-Dateien behalten. Kompression, mehrschichtige Bilder und zusätzliche Renderpässe bleiben nicht verfügbar.",
|
||||
"render.fit": "Produkt einpassen",
|
||||
"render.image_cancelled": "Rendering abgebrochen",
|
||||
"render.image_cancelling": "Wird abgebrochen",
|
||||
"render.image_completed": "Bild bereit zum Export",
|
||||
"render.image_encoding": "PNG wird kodiert",
|
||||
"render.image_export": "PNG exportieren",
|
||||
"render.image_export": "Fertiges Bild exportieren",
|
||||
"render.image_failed": "Rendering fehlgeschlagen",
|
||||
"render.image_height": "Höhe",
|
||||
"render.image_help": "CPU-Vorschaubild · PNG · angenäherte Transparenz",
|
||||
@@ -1138,17 +1198,98 @@
|
||||
"render.image_unknown": "Renderstatus nicht verfügbar",
|
||||
"render.image_use_largest": "Größte Größe mit diesem Seitenverhältnis verwenden:",
|
||||
"render.image_width": "Breite",
|
||||
"render.include_rig_cards": "Include passive cards and flags",
|
||||
"render.library_applied": "Ressourcenvariante angewendet. Vollständigen Renderauftrag vor dem Start prüfen.",
|
||||
"render.library_apply_hdr": "Mit dieser HDR-Umgebung speichern",
|
||||
"render.library_apply_maps": "Erstellte GPU-Oberflächenmaps verwenden",
|
||||
"render.library_apply_studio": "Erstellte Studiovariante speichern",
|
||||
"render.library_apply_uniform": "Einheitliches opakes Aussehen anwenden",
|
||||
"render.library_authored_help": "Original erstellte Ressourcen dienen der Darstellung, nicht als gemessene technische Daten. Jede Aktion wendet nur ihre angegebene Variante an.",
|
||||
"render.library_blocked": "Diese Ressource benötigt noch Kalibrierung oder eine externe Sonne und kann noch nicht angewendet werden.",
|
||||
"render.library_center": "Mittelpunkt des Lichtaufbaus",
|
||||
"render.library_emitters_only_ack": "Nur Strahler verwenden; dokumentierte passive Karten auslassen",
|
||||
"render.library_environment_concepts": "Umgebungskonzepte",
|
||||
"render.library_extent": "Objektausdehnung",
|
||||
"render.library_hdr": "Integrierte HDR-Umgebungen",
|
||||
"render.library_hdr_help": "Bettet das originale HDR in die gespeicherte Einstellung ein. Der Längengradursprung wird ausdrücklich umgerechnet; nach dem Speichern ist keine externe Datei nötig.",
|
||||
"render.library_light_concepts": "Beleuchtungskonzepte",
|
||||
"render.library_maps_help": "Built-in maps can be saved with pinned sources and local occurrence mapping. Temporary optical overrides remain a separate mode.",
|
||||
"render.library_material_concepts": "Materialkonzepte",
|
||||
"render.library_next": "Nächste Seite",
|
||||
"render.library_physical_profiles": "Physical reference profiles",
|
||||
"render.library_previous": "Vorherige Seite",
|
||||
"render.library_quality_concepts": "Qualitätsspezifikationen",
|
||||
"render.library_replace_environment": "Vorhandene Umgebung ausdrücklich ersetzen",
|
||||
"render.library_replace_studio": "Replace saved lighting, environment and stage",
|
||||
"render.library_results": "Passende Ressourcen",
|
||||
"render.library_rig_help": "Fit once at an explicit center and physical extent. Complete rigs include passive cards; calibration blockers still refuse. Conservative emitter/card clearance is checked before saving.",
|
||||
"render.library_rigs": "Erstellte Lichtaufbauten",
|
||||
"render.library_save_maps": "Save maps to this setup",
|
||||
"render.library_scene_bounds": "Erfasste Szenengrenzen verwenden",
|
||||
"render.library_scenes": "Szenenspezifikationen",
|
||||
"render.library_search": "Bibliothek durchsuchen",
|
||||
"render.library_selene": "SELENE: opake Oberflächen",
|
||||
"render.library_specification_only": "Nur Spezifikation: keine vollständig ausführbare Voreinstellung. Erforderliche Effekte, Ressourcen und Objektrollen müssen noch implementiert und gebunden werden.",
|
||||
"render.library_surfaces": "Erstellte PBR-Oberflächen",
|
||||
"render.library_target": "Zielkörper",
|
||||
"render.library_title": "Render-Ressourcenbibliothek",
|
||||
"render.library_uniform_ack": "Einheitliche Näherung ohne räumliche oder erweiterte Effekte verwenden",
|
||||
"render.material_color_replace": "Basismap ersetzt Körperfarbe",
|
||||
"render.material_limits": "Höchstens 128 Körperüberschreibungen, 32 getrennte Glaskörper, 64 Beschichtungen und 128 Texturbindungen. Identische Maps teilen Speicher; Geräte- und Gesamtspeichergrenzen bleiben aktiv.",
|
||||
"render.material_metallic_map": "Metallanteil-Map (linear)",
|
||||
"render.material_roughness_map": "Rauheitsmap (linear)",
|
||||
"render.material_v_up": "Textur-V zeigt im Bild nach oben",
|
||||
"render.navigation_help": "Die Kamera bewegt sich wie im Konstruieren (Einstellungen › Navigation). Setup speichern übernimmt die aktuelle Kamera.",
|
||||
"render.opaque_image_summary": "Vorschau-PNG · opakes PBR · Transparenz durch Blending",
|
||||
"render.opaque_preview_help": "Opakes PBR und einfache Farben; Transparenz durch Blending. Brechung, HDRI und finales Rendering sind noch nicht verfügbar.",
|
||||
"render.output_format_status": "PNG enthält das exakte fertige Anzeigebild. Optionales GPU-EXR bewahrt rohe lineare FLOAT32-Strahldichte vor Filtern. Export ersetzt nie durch Vorschaucache oder anderen Auftrag.",
|
||||
"render.passive_stage": "Render-only stage",
|
||||
"render.passive_stage_help": "Finite passive panels belong only to rendered images. They do not add CAD bodies or mass. Placement is fixed after applying; intersecting product bounds refuse. CPU does not provide diffuse bounce lighting.",
|
||||
"render.perspective": "Perspektivische Kamera",
|
||||
"render.physical_acknowledge": "I understand this partial reference needs grade, condition and source review",
|
||||
"render.physical_associations": "Physical candidates and roles",
|
||||
"render.physical_copy_plan": "Copy plan with sources and omissions",
|
||||
"render.physical_material_name": "Proposed reference material name",
|
||||
"render.physical_omissions": "Properties not representable in the native card",
|
||||
"render.physical_plan_only": "Read-only plan prepared. No material was created or assigned. Keep its extended profile and omissions when reviewing the native card.",
|
||||
"render.physical_prepare_plan": "Prepare creation plan (does not apply)",
|
||||
"render.physical_separate": "Appearance is not material identification. These are sourced references, not certified effective properties. Existing mass, engineering materials and optics are unchanged.",
|
||||
"render.physical_unknown": "No numerical candidate is established. Grade, formulation or construction must be specified; unknown is not zero.",
|
||||
"render.preview": "Integrierte Vorschau",
|
||||
"render.preview_help": "Rasterdarstellung mit gemischter Transparenz. Brechung, HDRI und finales Rendering sind noch nicht verfügbar.",
|
||||
"render.reload_settings": "Gespeicherte Einstellungen laden",
|
||||
"render.replace_saved_surface": "Replace an existing saved binding for this target",
|
||||
"render.restore_camera": "Gespeicherte Kamera laden",
|
||||
"render.result_reduced_display": "Anzeigevorschau auf die Geräte-/Cachegrenze reduziert. PNG und Export behalten die volle Auflösung; native 100%-Ansicht ist hier nicht verfügbar.",
|
||||
"render.return_design": "Zurück zu Design",
|
||||
"render.ribbon_backgrounds": "Hintergründe",
|
||||
"render.ribbon_check": "Prüfen",
|
||||
"render.ribbon_check_help": "Genau den aktuellen Renderentwurf prüfen. Kein stiller Rendererwechsel, keine Verkleinerung oder Ersetzung.",
|
||||
"render.ribbon_edit_lights": "Licht ändern",
|
||||
"render.ribbon_environment_help": "HDR-Beleuchtung, Kamerahintergründe und physische Bühnen sind getrennte Einstellungen mit ausdrücklicher Ersetzungsbestätigung.",
|
||||
"render.ribbon_finish_dialog": "Zuerst den Datei- oder Bestätigungsdialog abschließen.",
|
||||
"render.ribbon_frame": "Einpassen",
|
||||
"render.ribbon_hdr_library": "HDR-Bibliothek",
|
||||
"render.ribbon_job_active": "Ein Render läuft bereits; vor einem neuen Auftrag ausdrücklich abbrechen.",
|
||||
"render.ribbon_light_rigs": "Lichtaufbauten",
|
||||
"render.ribbon_lights_help": "Lichtaufbauten prüfen oder unterstützte Lichter bearbeiten; erst ausdrücklich Anwenden ersetzt die Einrichtung.",
|
||||
"render.ribbon_materials": "Materialien",
|
||||
"render.ribbon_materials_help": "Gemeinsamen Erscheinungsinspektor und Originaloberflächen öffnen. Optik ändert keine technischen Eigenschaften stillschweigend.",
|
||||
"render.ribbon_no_active_job": "Kein aktiver Render zum Abbrechen.",
|
||||
"render.ribbon_no_result": "Vor dem Export muss ein Render abgeschlossen sein.",
|
||||
"render.ribbon_no_saved_camera": "Vor dem Wiederherstellen eine Aufnahme speichern.",
|
||||
"render.ribbon_saved_camera": "Gespeicherte Kamera",
|
||||
"render.ribbon_shots": "Aufnahmen",
|
||||
"render.ribbon_studios": "Studios",
|
||||
"render.ribbon_studios_help": "Implementierte Studios durchsuchen und Ersetzungen vor dem Anwenden prüfen. Konzepte bleiben gekennzeichnet.",
|
||||
"render.ribbon_surface_maps": "Oberflächen",
|
||||
"render.save_setup": "Setup speichern",
|
||||
"render.saved_maps_runtime_conflict": "Saved surface bindings cannot be mixed with temporary optical overrides or an unsaved HDR. Save the HDR or remove the temporary inputs explicitly.",
|
||||
"render.saved_surface_remove": "Remove saved binding",
|
||||
"render.saved_surfaces": "Saved surface bindings",
|
||||
"render.saved_surfaces_help": "Pinned built-in maps follow the selected occurrence in local millimeters. A replaced or missing target is an error, never an automatic reassignment.",
|
||||
"render.setup_name": "Name des Setups",
|
||||
"render.stage_panel_count": "Saved panels",
|
||||
"render.unavailable_preview": "Dieses Setup verlangt ein nicht verfügbares Vorschauprofil.",
|
||||
"render.unsaved": "Ungespeicherte Vorschauänderungen",
|
||||
"render.use_preview": "Integrierte Vorschau verwenden",
|
||||
@@ -1178,12 +1319,14 @@
|
||||
"search.try": "Geben Sie einen Befehlsnamen, ein Merkmal in diesem Design oder einen Parameter ein. Um einen Befehl mit Werten auszuführen, geben Sie seine ID und ein JSON-Objekt ein: {example}",
|
||||
"search.valid": "Gültig",
|
||||
"settings.display_quality_note": "Feinere Stufen zeigen gekrümmte Flächen und Kanten glatter und brauchen länger zum Vernetzen. Nur die Ansicht ändert sich, nie das Design oder Exporte.",
|
||||
"settings.frame_rate_note": "Zeigt die Bilder pro Sekunde unten rechts in der Ansicht. Solange sie an ist, wird die Ansicht fortlaufend neu gezeichnet.",
|
||||
"settings.frame_rate_note": "Zeigt die Bilder pro Sekunde unten rechts in der Ansicht.",
|
||||
"settings.measure_axis_distances": "X/Y/Z-Maße anzeigen",
|
||||
"settings.measure_axis_distances_help": "Bei geöffneter Messung die Abstände entlang der Weltachsen zusätzlich zum Gesamtabstand anzeigen.",
|
||||
"settings.msaa_applies_on_restart": "Die Kantenglättung wird beim nächsten Start von SolidCraft übernommen",
|
||||
"settings.msaa_reset": "{setting}: {requested}× wird von diesem Grafikgerät nicht unterstützt; jetzt {reset}× (unterstützt: {supported}).",
|
||||
"settings.msaa_unsupported": "Von diesem Grafikgerät nicht unterstützt",
|
||||
"settings.reference_geometry_through_bodies": "Referenzgeometrie durch Körper hindurch anzeigen",
|
||||
"settings.reference_geometry_through_bodies_help": "Konturen inaktiver Skizzen und Bezugsebenen über schattierten Körpern zeichnen",
|
||||
"settings.reset_intro": "Diese Einstellungen funktionieren auf diesem Computer nicht und würden den Start von SolidCraft verhindern. Sie wurden daher geändert:",
|
||||
"settings.reset_title": "Einstellungen wurden geändert",
|
||||
"settings.supersampling_note": "Zeichnet die Ansicht in höherer Auflösung und rechnet sie herunter: glattere Kanten, Linien und Raster auf Bildschirmen unter 4K. Kostet GPU-Zeit: etwa 2,25-mal so viele Pixel bei 1,5× und 4-mal bei 2×.",
|
||||
@@ -1223,6 +1366,7 @@
|
||||
"thread.designation": "Bezeichnung",
|
||||
"thread.iso_metric": "ISO metrisch",
|
||||
"thread.select_size": "Größe wählen",
|
||||
"timeline.feature_hint": "Doppelklicken zum Bearbeiten · Ziehen zum Neuordnen · Rechtsklicken für weitere Optionen",
|
||||
"timeline.group": "Neuen Merkmalsordner anlegen",
|
||||
"timeline.move_many": "Zeitleistenelemente verschieben",
|
||||
"timeline.move_to_group": "Merkmale in Ordner verschieben",
|
||||
@@ -1234,12 +1378,115 @@
|
||||
"toolbar.customize_note": "Registerkarte {tab}. Markieren Sie einen Befehl, um ihm eine Schaltfläche in der Werkzeugleiste zu geben. Sie können auch eine Zeile aus dem Aufklappmenü eines Panels auf die Werkzeugleiste ziehen, eine Schaltfläche zum Entfernen aus der Werkzeugleiste ziehen oder die Stecknadel einer Menüzeile verwenden.",
|
||||
"toolbar.image": "BILD",
|
||||
"toolbar.render": "RENDERN",
|
||||
"toolbar.render_appearance": "ERSCHEINUNG",
|
||||
"toolbar.render_camera": "KAMERA",
|
||||
"toolbar.render_environment": "UMGEBUNG",
|
||||
"toolbar.render_lighting": "BELEUCHTUNG",
|
||||
"toolbar.render_scene": "SZENE",
|
||||
"trace.points": "{count} Punkte entlang der Bahn",
|
||||
"trace.stopped_by_contact": "Endet bei Probe {sample}: {a} würde {b} durchdringen",
|
||||
"trace.stopped_by_loop": "Endet bei Probe {sample}: die Schleife über {joint} lässt sich nicht weiter schließen",
|
||||
"ui.1_degree_of_freedom": "1 Freiheitsgrad",
|
||||
"ui.3d_mouse_axes_reassigned": "Die Achsen sind in den Einstellungen neu zugeordnet, und das gilt auch nach dieser Kalibrierung.",
|
||||
"ui.3d_mouse_button": "Taste {number}",
|
||||
"ui.3d_mouse_button_nothing": "Nichts",
|
||||
"ui.3d_mouse_button_settings": "3D-Maus-Einstellungen",
|
||||
"ui.3d_mouse_buttons": "Tasten",
|
||||
"ui.3d_mouse_buttons_hint": "Drücken Sie eine Taste an Ihrer 3D-Maus, um sie hier zu finden, und wählen Sie dann, was sie tut.",
|
||||
"ui.3d_mouse_buttons_no_device": "Schließen Sie eine 3D-Maus an, um ihre Tasten zu belegen.",
|
||||
"ui.3d_mouse_buttons_reset": "Standardbelegung wiederherstellen",
|
||||
"ui.3d_mouse_calibrate": "Kalibrieren…",
|
||||
"ui.3d_mouse_calibrate_tip": "Bewegen Sie die Kappe so, wie das Fenster es verlangt. So lernt SolidCraft, in welche Richtung und wie weit jede Achse dieser 3D-Maus geht. Wird für diese 3D-Maus gespeichert.",
|
||||
"ui.3d_mouse_calibrate_title": "3D-Maus kalibrieren",
|
||||
"ui.3d_mouse_calibrate_wait": "Bewegen Sie die 3D-Maus einmal, damit SolidCraft sie erkennt.",
|
||||
"ui.3d_mouse_calibrated": "Kalibriert",
|
||||
"ui.3d_mouse_calibration": "3D-Maus-Kalibrierung",
|
||||
"ui.3d_mouse_calibration_done": "Fertig. Speichern Sie die Kalibrierung, damit sie immer gilt, wenn diese 3D-Maus angeschlossen ist.",
|
||||
"ui.3d_mouse_calibration_no_response": "Eine Achse hat sich kaum bewegt und lässt sich nicht kalibrieren. Beginnen Sie von vorn und bewegen Sie die Kappe jedes Mal weiter.",
|
||||
"ui.3d_mouse_circles_help": "Die Kreise zeigen, was das Gerät nach der Kalibrierung sendet. Das Testmodell zeigt, was die Ansicht daraus macht, nach Umkehren, Geschwindigkeit und Achsenzuordnung.",
|
||||
"ui.3d_mouse_dead_zone": "Totzone",
|
||||
"ui.3d_mouse_dead_zone_tip": "Wie weit sich die Kappe bewegen muss, bevor sich die Ansicht bewegt, als Anteil ihres vollen Wegs. Erhöhen Sie sie, wenn die Ansicht wandert, obwohl die Kappe ruht.",
|
||||
"ui.3d_mouse_denied": "SolidCraft sieht eine 3D-Maus, darf sie aber nicht lesen. Die udev-Regel steht in den Einrichtungshinweisen zur 3D-Maus.",
|
||||
"ui.3d_mouse_denied_short": "Gefunden, aber nicht lesbar",
|
||||
"ui.3d_mouse_driven_by": "Welche kalibrierte Bewegung des Geräts diese Achse steuert. Wählen Sie eine, die schon eine andere Achse nutzt, werden die beiden getauscht.",
|
||||
"ui.3d_mouse_driven_by_label": "gesteuert von",
|
||||
"ui.3d_mouse_driver": "3D-Maus-Treiber",
|
||||
"ui.3d_mouse_driver_auto": "Automatisch",
|
||||
"ui.3d_mouse_driver_direct": "Direkt (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Wird vorerst über spacenavd gelesen: das Gerät selbst ließ sich nicht öffnen.",
|
||||
"ui.3d_mouse_driver_only_direct": "Das Gerät wird direkt gelesen; spacenavd läuft nicht",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Lesen über spacenavd, die einzige verfügbare Möglichkeit",
|
||||
"ui.3d_mouse_forget": "Kalibrierung verwerfen",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Ansicht links",
|
||||
"ui.3d_mouse_ghost_left_window": "Fenster links",
|
||||
"ui.3d_mouse_ghost_position": "Position",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Ansicht rechts",
|
||||
"ui.3d_mouse_hint": "Ansicht gleichzeitig verschieben und drehen; Skizzen verwenden nur Schwenken und Zoomen.",
|
||||
"ui.3d_mouse_kind_cap": "Zurück zur Mitte",
|
||||
"ui.3d_mouse_kind_question": "Was für eine 3D-Maus ist es?",
|
||||
"ui.3d_mouse_kind_trackball": "Trackball",
|
||||
"ui.3d_mouse_looks_like_cap": "Der Wert bleibt stehen, solange Sie halten, wie bei einem Gerät vom Typ „Zurück zur Mitte“.",
|
||||
"ui.3d_mouse_looks_like_trackball": "Der Wert fällt zurück, sobald Sie aufhören, wie bei einem Trackball.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Rotation X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Rotation Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Rotation Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Translation X",
|
||||
"ui.3d_mouse_motion_translation_y": "Translation Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Translation Z",
|
||||
"ui.3d_mouse_navigate": "Mit der 3D-Maus navigieren",
|
||||
"ui.3d_mouse_none": "Keine 3D-Maus gefunden",
|
||||
"ui.3d_mouse_not_calibrated": "Nicht kalibriert",
|
||||
"ui.3d_mouse_offer": "Die Kalibrierung Ihrer 3D-Maus behebt vertauschte oder umgekehrte Achsen und stellt Geschwindigkeit und Gefühl ein.",
|
||||
"ui.3d_mouse_offer_never": "Nicht mehr anzeigen",
|
||||
"ui.3d_mouse_offer_swap": "Zu Hidraw wechseln (direkter Treiber)",
|
||||
"ui.3d_mouse_reading": "Werte",
|
||||
"ui.3d_mouse_reset_axes": "Achsenzuordnung zurücksetzen",
|
||||
"ui.3d_mouse_reset_view": "Ansicht zurücksetzen",
|
||||
"ui.3d_mouse_restore": "3D-Maus-Standardwerte wiederherstellen",
|
||||
"ui.3d_mouse_restore_ask": "3D-Maus-Einstellungen auf die Standardwerte zurücksetzen? Kalibrierung und Tastenbelegung bleiben.",
|
||||
"ui.3d_mouse_section": "3D-Maus",
|
||||
"ui.3d_mouse_show_in_view": "3D-Maus in der 3D-Ansicht anzeigen",
|
||||
"ui.3d_mouse_show_in_view_tip": "Ein Bild Ihrer 3D-Maus unten links, das sich mit ihr bewegt.",
|
||||
"ui.3d_mouse_skip": "Mein Gerät kann das nicht",
|
||||
"ui.3d_mouse_skip_tip": "Überspringt diese Bewegung in beide Richtungen; die Kalibrierung lässt sie aus.",
|
||||
"ui.3d_mouse_speed": "Geschwindigkeit der 3D-Maus",
|
||||
"ui.3d_mouse_step_lift": "Kappe nach oben ziehen, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_press": "Kappe nach unten drücken, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_pull": "Kappe zu sich heranziehen, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_push": "Kappe von sich wegschieben, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_rest": "Nehmen Sie die Hand von der Kappe und lassen Sie sie ruhen.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Nehmen Sie die Hand von der Kugel und lassen Sie sie ruhen.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Drehen Sie den Scrollring ein paar Rasten im Uhrzeigersinn.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Drehen Sie den Scrollring ein paar Rasten gegen den Uhrzeigersinn.",
|
||||
"ui.3d_mouse_step_roll_away": "Rollen Sie die Kugel ein paarmal von sich weg und lassen Sie sie dann auslaufen.",
|
||||
"ui.3d_mouse_step_roll_left": "Rollen Sie die Kugel ein paarmal nach links und lassen Sie sie dann auslaufen.",
|
||||
"ui.3d_mouse_step_roll_right": "Rollen Sie die Kugel ein paarmal nach rechts und lassen Sie sie dann auslaufen.",
|
||||
"ui.3d_mouse_step_roll_toward": "Rollen Sie die Kugel ein paarmal zu sich hin und lassen Sie sie dann auslaufen.",
|
||||
"ui.3d_mouse_step_slide_left": "Kappe nach links schieben, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_slide_right": "Kappe nach rechts schieben, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Drehen Sie die Kugel ein paarmal im Uhrzeigersinn und lassen Sie sie dann auslaufen.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Drehen Sie die Kugel ein paarmal gegen den Uhrzeigersinn und lassen Sie sie dann auslaufen.",
|
||||
"ui.3d_mouse_step_tilt_away": "Oberseite der Kappe von sich weg kippen (die Ihnen nächste Kante geht nach oben), halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_tilt_left": "Kappe nach links kippen (linke Kante runter), halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_tilt_right": "Kappe nach rechts kippen (rechte Kante runter), halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Oberseite der Kappe zu sich kippen (die Ihnen nächste Kante geht nach unten), halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_touch": "Legen Sie die Hand auf die Kappe, ohne zu drücken.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Kappe im Uhrzeigersinn drehen, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Kappe gegen den Uhrzeigersinn drehen, halten, dann loslassen.",
|
||||
"ui.3d_mouse_switch_cap": "Als Zurück zur Mitte kalibrieren",
|
||||
"ui.3d_mouse_switch_trackball": "Als Trackball kalibrieren",
|
||||
"ui.3d_mouse_view_axis": "Ansichtsachse",
|
||||
"ui.3d_mouse_view_off": "Aus",
|
||||
"ui.3d_mouse_view_off_tip": "Das Profil des 3D-Maus-Treibers schaltet diese Achse ab, daher bewegt ihre Bewegung die Ansicht nicht. Wählen Sie hier die Bewegung einer anderen Achse, werden die beiden getauscht und sie bewegt die Ansicht nicht mehr.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbit links / rechts",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbit hoch / runter",
|
||||
"ui.3d_mouse_view_pan_sideways": "Schwenken links / rechts",
|
||||
"ui.3d_mouse_view_pan_up_down": "Schwenken hoch / runter",
|
||||
"ui.3d_mouse_view_roll": "Rollen",
|
||||
"ui.3d_mouse_view_unmapped": "Achse {n}",
|
||||
"ui.3d_mouse_view_zoom": "Zoom",
|
||||
"ui.3d_mouse_waiting": "Warte auf Bewegung",
|
||||
"ui.3d_mouse_wiggle": "Falls eine angeschlossen ist, bewegen Sie sie kurz.",
|
||||
"ui.3d_sketch": "3D-Skizze",
|
||||
"ui.activate": "Aktivieren",
|
||||
"ui.active_component": "Aktive Komponente",
|
||||
@@ -1301,6 +1548,7 @@
|
||||
"ui.agent_chat_files_left_out": "Höchstens {max} Dateien pro Nachricht; ausgelassen: {files}",
|
||||
"ui.agent_chat_files_loading": "Warten, bis die Dateien gelesen sind",
|
||||
"ui.agent_chat_files_max": "Höchstens {max} Dateien pro Nachricht",
|
||||
"ui.agent_chat_flatpak_none_found": "Kein Coding-Agent gefunden. Die Flatpak-Sandbox sieht standardmäßig keine Agenten in Ihrem Benutzerordner und hat keinen Netzwerkzugriff. Um Claude Code und seinen ACP-Adapter zu erlauben, die über nvm oder in ~/.local/bin installiert sind, führen Sie diesen Befehl in einem Terminal aus und starten Sie SolidCraft dann neu. Er gewährt Zugriff auf die genannten Programme und Anmeldedateien sowie Netzwerkzugriff:",
|
||||
"ui.agent_chat_import_failed": "{name}: Der Import ist fehlgeschlagen: {error}",
|
||||
"ui.agent_chat_intro": "Chatten Sie mit einem Coding-Agenten, den Sie schon nutzen, oder einem Modell-Endpunkt Ihrer Wahl. Er arbeitet über die Werkzeuge von SolidCraft am geöffneten Design: Seine Änderungen erscheinen sofort und lassen sich pro Nachricht in einem Schritt rückgängig machen. Was er ohne Nachfrage darf, legen Sie unter Einstellungen > KI fest.",
|
||||
"ui.agent_chat_key_in_env": "Schlüssel gesetzt",
|
||||
@@ -1396,6 +1644,7 @@
|
||||
"ui.ai_http_busy": "der Endpunkt ist ausgelastet oder am Limit; versuchen Sie es später",
|
||||
"ui.ai_http_key": "der Endpunkt hat den Schlüssel abgelehnt: prüfen Sie ihn unter Einstellungen > KI",
|
||||
"ui.ai_http_not_found": "Endpunkt oder Modell nicht gefunden: prüfen Sie Basis-URL und Modellnamen",
|
||||
"ui.ai_http_too_large": "die Unterhaltung ist für diesen Endpunkt zu groß; beginnen Sie einen neuen Chat",
|
||||
"ui.ai_key": "API-Schlüssel",
|
||||
"ui.ai_key_hint": "optional; im Schlüsselbund des Systems gespeichert",
|
||||
"ui.ai_key_needs_https": "Ein Schlüssel wird nur über HTTPS oder an diesen Computer gesendet.",
|
||||
@@ -1423,7 +1672,6 @@
|
||||
"ui.ai_permissions": "Berechtigungen",
|
||||
"ui.ai_preset": "Vorlage",
|
||||
"ui.ai_remove": "Entfernen",
|
||||
"ui.ai_request_limit": "nach 24 Anfragen für eine Nachricht angehalten; senden Sie eine weitere Nachricht, um fortzufahren",
|
||||
"ui.ai_reset": "Zurücksetzen",
|
||||
"ui.ai_settings": "KI-Einstellungen…",
|
||||
"ui.ai_show_features": "KI-Funktionen anzeigen",
|
||||
@@ -1468,6 +1716,7 @@
|
||||
"ui.blocked": "Verdeckt",
|
||||
"ui.bodies": "Körper",
|
||||
"ui.bodies_all_if_none": "Körper (alle, wenn keine)",
|
||||
"ui.bottom": "Unten",
|
||||
"ui.box": "Block",
|
||||
"ui.break": "Unterbrechen",
|
||||
"ui.browser_bodies_one": "{count} Körper",
|
||||
@@ -1589,6 +1838,9 @@
|
||||
"ui.export_keymap_tip": "Tasten, S-Box-Favoriten und die Symbolleiste als Datei",
|
||||
"ui.extend": "Verlängern",
|
||||
"ui.extent_type": "Ausdehnungstyp",
|
||||
"ui.extract_component": "In Datei extrahieren…",
|
||||
"ui.extract_design_changed": "Das Design hat sich beim Wählen einer Datei geändert. Starten Sie In Datei extrahieren erneut.",
|
||||
"ui.extract_done": "Die Komponente ist jetzt ein eigenes Design und hier verknüpft.",
|
||||
"ui.extrude": "Extrudieren",
|
||||
"ui.factor": "Faktor",
|
||||
"ui.feature_history": "Feature-Verlauf",
|
||||
@@ -1599,6 +1851,8 @@
|
||||
"ui.filter_commands_or_keys": "Befehle oder Tasten filtern",
|
||||
"ui.findInTimeline": "In der Zeitleiste suchen",
|
||||
"ui.findInWindow": "Im Fenster suchen",
|
||||
"ui.fine": "Fein",
|
||||
"ui.finest": "Sehr fein",
|
||||
"ui.finish_sketch": "Skizze beenden",
|
||||
"ui.fit": "Einpassen",
|
||||
"ui.fix_unfix": "Fixieren/Lösen",
|
||||
@@ -1620,6 +1874,7 @@
|
||||
"ui.grid_follows_view_hint": "Seitenansichten zeigen das Raster auf der aufrechten Ebene, die ihnen zugewandt ist, statt auf dem Boden",
|
||||
"ui.ground_shadow": "Bodenschatten",
|
||||
"ui.groupSelected": "Auswahl gruppieren",
|
||||
"ui.hatch": "Schraffur",
|
||||
"ui.heading_add_ins": "ADD-INS",
|
||||
"ui.heading_assemble": "ZUSAMMENBAU",
|
||||
"ui.heading_box": "BLOCK",
|
||||
@@ -1686,6 +1941,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "WERKZEUGE",
|
||||
"ui.heading_utility": "HILFSMITTEL",
|
||||
"ui.heading_weldments": "SCHWEISSTEILE",
|
||||
"ui.help": "Hilfe",
|
||||
"ui.hide": "Ausblenden",
|
||||
"ui.hideAxes": "Achsen ausblenden",
|
||||
@@ -1720,6 +1976,10 @@
|
||||
"ui.length_decimals": "Längen-Dezimalstellen",
|
||||
"ui.light": "Hell",
|
||||
"ui.line": "Linie",
|
||||
"ui.link_copies": "Identische Kopien verknüpfen",
|
||||
"ui.link_copies_keep": "Behalten",
|
||||
"ui.link_copies_link": "Verknüpfen",
|
||||
"ui.link_copies_text": "Diese Komponenten erzeugen dieselben Körper wie {name}. Sollen sie zu Platzierungen seiner Verknüpfung werden?",
|
||||
"ui.lock": "Sperren",
|
||||
"ui.loft": "Loft",
|
||||
"ui.look_at": "Ansehen",
|
||||
@@ -1776,6 +2036,7 @@
|
||||
"ui.one_side": "Einseitig",
|
||||
"ui.open": "Öffnen",
|
||||
"ui.open_ends_tip": "Berührt nichts",
|
||||
"ui.open_linked_design": "Verknüpftes Design öffnen",
|
||||
"ui.open_preferences": "Einstellungen öffnen",
|
||||
"ui.open_recent": "Zuletzt verwendet",
|
||||
"ui.operation": "Operation",
|
||||
@@ -1822,6 +2083,7 @@
|
||||
"ui.project_face_edges_sketch": "Flächenkanten in neue Skizzen projizieren",
|
||||
"ui.properties": "Eigenschaften",
|
||||
"ui.pull_direction": "Entformungsrichtung",
|
||||
"ui.quality": "Qualität",
|
||||
"ui.quantity": "Anzahl",
|
||||
"ui.quantity_2": "Anzahl 2",
|
||||
"ui.quit": "Beenden",
|
||||
@@ -1843,6 +2105,7 @@
|
||||
"ui.render": "Rendern",
|
||||
"ui.renderer": "Renderer",
|
||||
"ui.repeat": "Wiederholen",
|
||||
"ui.replace_with_link_done": "Die Kopien sind jetzt Platzierungen des verknüpften Designs.",
|
||||
"ui.report_a_bug": "Fehler melden",
|
||||
"ui.reset": "Zurücksetzen",
|
||||
"ui.reset_all": "Alle zurücksetzen",
|
||||
@@ -1881,6 +2144,12 @@
|
||||
"ui.sent_to_slicer": "An {slicer} gesendet",
|
||||
"ui.sheet_gap": "Spalt",
|
||||
"ui.shell": "Hülle",
|
||||
"ui.shortcut_capture_add_key": "Eine normale Taste hinzufügen. Escape bricht ab.",
|
||||
"ui.shortcut_capture_hint": "Eine Belegung anklicken, dann Modifikatortasten und eine Taste drücken. Weitere Modifikatoren innerhalb einer Sekunde hinzufügen; zum Bestätigen alle Tasten loslassen. Escape bricht ab.",
|
||||
"ui.shortcut_capture_press": "Modifikatortasten und eine Taste drücken. Escape bricht ab.",
|
||||
"ui.shortcut_capture_release": "Zum Bestätigen alle Tasten loslassen. Escape bricht ab.",
|
||||
"ui.shortcut_capture_settling": "Tastenkürzel wird bestätigt… Escape bricht ab.",
|
||||
"ui.shortcut_multiple_keys": "Modifikatortasten und nur eine weitere Taste verwenden. Zum Wiederholen die Belegung anklicken.",
|
||||
"ui.show": "Einblenden",
|
||||
"ui.showAll": "Alle einblenden",
|
||||
"ui.showAllBodies": "Alle Volumenkörper einblenden",
|
||||
@@ -1910,65 +2179,13 @@
|
||||
"ui.smooth_wheel_zoom": "Sanftes Zoomen mit dem Rad",
|
||||
"ui.smooth_wheel_zoom_tip": "Ausschalten, um jedes Radereignis sofort anzuwenden, ohne Nachlauf",
|
||||
"ui.snap": "Einrasten",
|
||||
"ui.space_mouse_axes_reassigned": "Die Achsen sind in den Einstellungen neu zugeordnet, und das gilt auch nach dieser Kalibrierung.",
|
||||
"ui.space_mouse_calibrate": "Kalibrieren…",
|
||||
"ui.space_mouse_calibrate_tip": "Bewegen Sie die Kappe so, wie das Fenster es verlangt. So lernt SolidCraft, in welche Richtung und wie weit jede Achse dieser 3D-Maus geht. Wird für diese 3D-Maus gespeichert.",
|
||||
"ui.space_mouse_calibrate_title": "3D-Maus kalibrieren",
|
||||
"ui.space_mouse_calibrated": "Kalibriert",
|
||||
"ui.space_mouse_calibration": "3D-Maus-Kalibrierung",
|
||||
"ui.space_mouse_calibration_done": "Fertig. Speichern Sie die Kalibrierung, damit sie immer gilt, wenn diese 3D-Maus angeschlossen ist.",
|
||||
"ui.space_mouse_calibration_no_response": "Eine Achse hat sich kaum bewegt und lässt sich nicht kalibrieren. Beginnen Sie von vorn und bewegen Sie die Kappe jedes Mal weiter.",
|
||||
"ui.space_mouse_dead_zone": "Totzone",
|
||||
"ui.space_mouse_dead_zone_tip": "Wie weit sich die Kappe bewegen muss, bevor sich die Ansicht bewegt, als Anteil ihres vollen Wegs. Erhöhen Sie sie, wenn die Ansicht wandert, obwohl die Kappe ruht.",
|
||||
"ui.space_mouse_driven_by": "Welche kalibrierte Bewegung des Geräts diese Achse steuert. Wählen Sie eine, die schon eine andere Achse nutzt, werden die beiden getauscht.",
|
||||
"ui.space_mouse_driven_by_label": "gesteuert von",
|
||||
"ui.space_mouse_driver": "3D-Maus-Treiber",
|
||||
"ui.space_mouse_driver_auto": "Automatisch",
|
||||
"ui.space_mouse_driver_direct": "Direkt (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Das Gerät wird direkt gelesen; spacenavd läuft nicht",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Lesen über spacenavd, die einzige verfügbare Möglichkeit",
|
||||
"ui.space_mouse_forget": "Kalibrierung vergessen",
|
||||
"ui.space_mouse_hint": "Ansicht gleichzeitig verschieben und drehen; Skizzen verwenden nur Schwenken und Zoomen.",
|
||||
"ui.space_mouse_motion_rotation_x": "Rotation X",
|
||||
"ui.space_mouse_motion_rotation_y": "Rotation Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Rotation Z",
|
||||
"ui.space_mouse_motion_translation_x": "Translation X",
|
||||
"ui.space_mouse_motion_translation_y": "Translation Y",
|
||||
"ui.space_mouse_motion_translation_z": "Translation Z",
|
||||
"ui.space_mouse_navigate": "Mit SpaceNavigator / SpaceMouse navigieren",
|
||||
"ui.space_mouse_none": "Keine 3D-Maus gefunden",
|
||||
"ui.space_mouse_not_calibrated": "Nicht kalibriert",
|
||||
"ui.space_mouse_reading": "Werte",
|
||||
"ui.space_mouse_reset_axes": "Achsenzuordnung zurücksetzen",
|
||||
"ui.space_mouse_step_lift": "Kappe nach oben ziehen, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_press": "Kappe nach unten drücken, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_pull": "Kappe zu sich heranziehen, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_push": "Kappe von sich wegschieben, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_rest": "Nehmen Sie die Hand von der Kappe und lassen Sie sie ruhen.",
|
||||
"ui.space_mouse_step_slide_left": "Kappe nach links schieben, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_slide_right": "Kappe nach rechts schieben, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_tilt_back": "Kappe nach hinten kippen (Vorderkante hoch), halten, dann loslassen.",
|
||||
"ui.space_mouse_step_tilt_forward": "Kappe nach vorn kippen (Vorderkante runter), halten, dann loslassen.",
|
||||
"ui.space_mouse_step_tilt_left": "Kappe nach links kippen (linke Kante runter), halten, dann loslassen.",
|
||||
"ui.space_mouse_step_tilt_right": "Kappe nach rechts kippen (rechte Kante runter), halten, dann loslassen.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Kappe im Uhrzeigersinn drehen, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Kappe gegen den Uhrzeigersinn drehen, halten, dann loslassen.",
|
||||
"ui.space_mouse_view_axis": "Ansichtsachse",
|
||||
"ui.space_mouse_view_off": "Aus",
|
||||
"ui.space_mouse_view_off_tip": "Das Profil des 3D-Maus-Treibers schaltet diese Achse ab, daher bewegt ihre Bewegung die Ansicht nicht. Wählen Sie hier die Bewegung einer anderen Achse, werden die beiden getauscht und sie bewegt die Ansicht nicht mehr.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Orbit links / rechts",
|
||||
"ui.space_mouse_view_orbit_up_down": "Orbit hoch / runter",
|
||||
"ui.space_mouse_view_pan_sideways": "Schwenken links / rechts",
|
||||
"ui.space_mouse_view_pan_up_down": "Schwenken hoch / runter",
|
||||
"ui.space_mouse_view_roll": "Rollen",
|
||||
"ui.space_mouse_view_unmapped": "Achse {n}",
|
||||
"ui.space_mouse_view_zoom": "Zoom",
|
||||
"ui.spacing": "Abstand",
|
||||
"ui.spacing_2": "Abstand 2",
|
||||
"ui.sphere": "Sphäre",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Körper trennen",
|
||||
"ui.stand_up_tip": "Nach dem Öffnen einer STEP- oder IGES-Datei mit Z nach oben eine Vierteldrehung um X anbieten",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Von vorn beginnen",
|
||||
"ui.start_page": "Startseite",
|
||||
@@ -2004,7 +2221,7 @@
|
||||
"ui.top": "Oben",
|
||||
"ui.torus": "Torus",
|
||||
"ui.total_angle": "Gesamtwinkel",
|
||||
"ui.trackpad_pan_tip": "Trackpad-Gesten schwenken (aus: sie zoomen wie das Rad); Mausrad zoomt, Daumenrad schwenkt seitlich, Pinch zoomt immer",
|
||||
"ui.trackpad_pan_tip": "Scrollen mit zwei Fingern verschiebt die Ansicht; bei Fusion dreht Umschalt + zwei Finger die Ansicht. Aus: Scrollen zoomt. Mausrad und Pinch-Geste zoomen; das Daumenrad verschiebt seitlich.",
|
||||
"ui.trim": "Kürzen",
|
||||
"ui.twist": "Verdrehung",
|
||||
"ui.two_finger_scroll_pans": "Zwei-Finger-Scrollen schwenkt",
|
||||
@@ -2022,6 +2239,7 @@
|
||||
"ui.up_axis_note": "Welche Achse in den Ansichten nach oben zeigt (Achsenansicht, Home, Ansehen). Designs bleiben unverändert.",
|
||||
"ui.updateToCurrentView": "Auf aktuelle Ansicht aktualisieren",
|
||||
"ui.view_centre": "Ansichtszentrum",
|
||||
"ui.viewport_theme_follow": "Dem Design folgen",
|
||||
"ui.visual_shaded_hidden_edges": "Schattiert mit verdeckten Kanten",
|
||||
"ui.visual_shaded_with_edges": "Schattiert mit Kanten",
|
||||
"ui.visual_style": "Ansichtsstil",
|
||||
@@ -2071,6 +2289,7 @@
|
||||
"update.prefs_flatpak": "Flatpak aktualisiert diese Installation ({version}): {command} ausführen oder die Softwareverwaltung nutzen. SolidCraft sucht nicht selbst.",
|
||||
"update.prefs_label": "Updates",
|
||||
"update.prefs_note": "Dies ist SolidCraft {version} ({kind}). Die Suche fragt GitHub nach dem neuesten Release und sendet sonst nichts.",
|
||||
"update.prefs_web": "Laden Sie die Seite neu, um die neueste Web-App zu erhalten. Ist das Netzwerk nicht verfügbar oder kann das Update nicht rechtzeitig vollständig heruntergeladen werden, wird die gespeicherte Version geöffnet. Ein laufender Download wird für das nächste Neuladen fortgesetzt.",
|
||||
"update.release_page": "Release-Seite öffnen",
|
||||
"update.restart": "Jetzt neu starten",
|
||||
"update.restart_later": "Später",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"Modeled": "Modelled",
|
||||
"cmd.construct_point_center": "Point At Centre Of Circle/Sphere/Torus",
|
||||
"cmd.inspect_center_of_mass": "Centre of Mass",
|
||||
"cmd.render_aging_describe": "Layered Surface Aging",
|
||||
"cmd.render_effects_describe": "Lens and Creative Effects",
|
||||
"cmd.sketch_arc_center_point": "Centre Point Arc",
|
||||
"cmd.sketch_circle_center": "Centre Diameter Circle",
|
||||
"cmd.sketch_rectangle_center": "Centre Rectangle",
|
||||
@@ -14,8 +16,83 @@
|
||||
"cmd.sketch_slot_center_point": "Centre Point Slot",
|
||||
"cmd.sketch_slot_center_to_center": "Centre to Centre Slot",
|
||||
"pattern.alignment_hint": "Counts include the original. Centre balances copies; Flip chooses the extra side for even totals.",
|
||||
"render.aging_add": "Add aging layer",
|
||||
"render.aging_airborne": "Airborne dust / participating medium",
|
||||
"render.aging_airborne_help": "Airborne dust is a separate finite homogeneous scattering volume. It changes transport and raw EXR, not the surface-aging stack. Existing volume/glass/denoising restrictions remain explicit; no atmosphere is added automatically.",
|
||||
"render.aging_all_bodies": "All eligible opaque bodies",
|
||||
"render.aging_chips": "Chipped paint",
|
||||
"render.aging_class_any": "All opaque material classes",
|
||||
"render.aging_class_dielectric": "Nonmetallic surfaces only",
|
||||
"render.aging_class_metal": "Metallic surfaces only",
|
||||
"render.aging_color": "Layer colour · sRGB",
|
||||
"render.aging_coverage": "Coverage",
|
||||
"render.aging_dust": "Deposited dust",
|
||||
"render.aging_empty": "No aging layers: the original surfaces remain unchanged.",
|
||||
"render.aging_fading": "Faded coating",
|
||||
"render.aging_grime": "Grime & staining",
|
||||
"render.aging_help": "Combine up to eight optical layers, in order. Rust and patina are authored appearances, not age/corrosion simulations. Scale is in world millimetres. No geometry, mass or engineering property is changed; glass and passive stages are excluded.",
|
||||
"render.aging_metallic": "Layer metallic fraction",
|
||||
"render.aging_origin": "Field origin · world mm",
|
||||
"render.aging_patina": "Copper-green patina",
|
||||
"render.aging_refresh_targets": "Inspect current bodies",
|
||||
"render.aging_roughness": "Layer roughness",
|
||||
"render.aging_rust": "Oxidized iron / rust",
|
||||
"render.aging_scale": "Pattern scale",
|
||||
"render.aging_scratches": "Fine scratches",
|
||||
"render.aging_target": "Target & coordinates",
|
||||
"render.aging_target_help": "A named target must still match its captured index and name. Global masks are world-anchored; they do not follow moving parts. This is neither cavity detection nor true edge wear. Filter the list to reach additional bodies.",
|
||||
"render.aging_target_search": "Filter body names",
|
||||
"render.aging_upward": "Upward-facing deposition",
|
||||
"render.backdrop_x": "Centre X",
|
||||
"render.backdrop_y": "Centre Y",
|
||||
"render.background_camera_section": "Camera background · PNG presentation",
|
||||
"render.background_completed_frozen": "The displayed result is a frozen completed image. Editing a background does not repaint that result automatically. Use Regrade for camera-only changes, or Render for a new physical stage.",
|
||||
"render.background_hdr_separate": "The current HDR resources illuminate and reflect on surfaces; they are not automatically displayed as a panorama behind the subject. Changing a gradient does not rotate or recolour the lighting.",
|
||||
"render.background_lighting_section": "HDR illumination · separate from camera background",
|
||||
"render.background_no_alpha_override": "A camera background is not added to transparent output silently. Raw-master regrading uses captured coverage and does not reapply denoising; the source EXR remains unchanged.",
|
||||
"render.background_regrade": "Apply to retained raw master",
|
||||
"render.background_stage_occlusion": "This shot includes physical stage geometry. A large floor or wall can fill the frame and hide a camera-only background. Clear or resize the stage deliberately, or change its physical reflectance. Regrading cannot remove captured geometry.",
|
||||
"render.background_workflow": "Choose the background you intend: a camera-only colour/gradient, a physical stage that receives light, or HDR illumination. They are independent. Live Preview is a modelling view; render a new image or regrade a retained master to see camera finishing.",
|
||||
"render.chromatic_offset": "Chromatic edge offset",
|
||||
"render.effects_gpu_only": "These effects require GPU path tracing. Changing the page never switches the renderer automatically.",
|
||||
"render.effects_help": "Combine ordered colour looks and image-space lens effects. Rendering and raw-master regrading use the same saved settings. Raw EXR stays unchanged. These are original artistic controls, not calibrated lens or branded-film emulations.",
|
||||
"render.effects_reset_lens": "Reset lens effects",
|
||||
"render.effects_seed": "Pattern seed",
|
||||
"render.film_grain": "Photographic grain",
|
||||
"render.flare_ghosts": "Ghost rings per source",
|
||||
"render.flare_halo": "Flare halo",
|
||||
"render.flare_streak": "Anamorphic-style streak",
|
||||
"render.flare_strength": "Highlight flare strength",
|
||||
"render.flare_threshold": "Scene-linear flare threshold",
|
||||
"render.layer_down": "Move down",
|
||||
"render.layer_enabled": "Layer enabled",
|
||||
"render.layer_remove": "Remove layer",
|
||||
"render.layer_strength": "Layer strength",
|
||||
"render.layer_up": "Move up",
|
||||
"render.lens_effects_help": "Flare uses up to four separated bright sources in the actual linear image. Ghosts, halo, streaks, chromatic offsets and radial distortion are artistic image-space approximations. Spatial effects need opaque output; outside samples extend the image edge. Grain is deterministic. No physics, EXR or geometry is altered.",
|
||||
"render.lens_effects_title": "Lens-inspired & photographic",
|
||||
"render.look_add": "Add colour look",
|
||||
"render.look_bleach": "Bleach-bypass-inspired",
|
||||
"render.look_cool": "Cool",
|
||||
"render.look_cyanotype": "Cyanotype-inspired",
|
||||
"render.look_faded": "Faded print",
|
||||
"render.look_mono": "Monochrome",
|
||||
"render.look_sepia": "Sepia",
|
||||
"render.look_strength": "Look strength",
|
||||
"render.look_teal_orange": "Teal / orange",
|
||||
"render.look_title": "Creative look stack",
|
||||
"render.look_warm": "Warm",
|
||||
"render.opaque_preview_help": "Opaque PBR and simple colours; blended transparency. Refraction, HDRI and final rendering are not available yet.",
|
||||
"render.page_aging": "Aging & contamination",
|
||||
"render.page_background": "Backgrounds",
|
||||
"render.page_effects": "Lens & finishing effects",
|
||||
"render.radial_distortion": "Radial distortion",
|
||||
"render.ribbon_aging": "Aging",
|
||||
"render.ribbon_effects": "Effects",
|
||||
"render.sharpening": "Detail sharpening",
|
||||
"render.soft_focus": "Soft focus",
|
||||
"sketch_text.align_center": "Centre",
|
||||
"ui.3d_mouse_step_pull": "Pull the cap towards you, hold it, then let go.",
|
||||
"ui.center": "Centre",
|
||||
"ui.centerline": "Centreline",
|
||||
"ui.customizeToolbar": "Customise Toolbar…",
|
||||
@@ -24,7 +101,6 @@
|
||||
"ui.layout_compact": "Compact",
|
||||
"ui.layout_floating": "Floating",
|
||||
"ui.ribbon_labels": "Show text under ribbon icons",
|
||||
"ui.space_mouse_step_pull": "Pull the cap towards you, hold it, then let go.",
|
||||
"ui.workspace_layout": "Workspace layout",
|
||||
"ui.zoom_to_cursor_tip": "The wheel zooms towards the point under the cursor (off: towards the view's centre)",
|
||||
"update.prefs_flatpak": "Flatpak updates this copy ({version}): run {command} or use your software centre. SolidCraft doesn't check."
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Choisir…",
|
||||
"Circular": "Circulaire",
|
||||
"Circumference": "Circonférence",
|
||||
"Clockwise": "Horaire",
|
||||
"Close draft": "Fermer le brouillon",
|
||||
"Collisions": "Collisions",
|
||||
"Colour": "Couleur",
|
||||
@@ -269,7 +270,7 @@
|
||||
"Preview Row": "Aperçu de la ligne",
|
||||
"Preview leaves the current design unchanged.": "L'aperçu ne modifie pas la conception actuelle.",
|
||||
"Profiles/Curves": "Profils/Courbes",
|
||||
"Quantity": "Grandeur",
|
||||
"Quantity": "Quantité",
|
||||
"RECENT": "RÉCENTS",
|
||||
"REFERENCE": "RÉFÉRENCE",
|
||||
"RULED SURFACE": "SURFACE RÉGLÉE",
|
||||
@@ -322,6 +323,7 @@
|
||||
"Search commands": "Rechercher des commandes",
|
||||
"Search this library": "Rechercher dans cette bibliothèque",
|
||||
"Section": "Coupe",
|
||||
"Section position": "Position de la section",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Sélectionner un autre élément : maintenir le bouton gauche immobile sur le modèle (ou clic droit › Sélectionner un autre…) pour lister tout ce qui se trouve sous le curseur ; cliquer de nouveau au même endroit pour sélectionner l'élément suivant. Double-cliquer avec le bouton du milieu pour revenir à l'accueil (Préférences › Navigation).",
|
||||
"Select bodies (a face picks its body)": "Sélectionner des corps (une face sélectionne son corps)",
|
||||
"Select bodies or faces instead": "Sélectionner plutôt des corps ou des faces",
|
||||
@@ -333,6 +335,7 @@
|
||||
"Show Agent Cursor": "Afficher le curseur de l'agent",
|
||||
"Show in folder": "Afficher dans le dossier",
|
||||
"Show the pointer an AI agent moves (#35)": "Afficher le pointeur déplacé par un agent IA (#35)",
|
||||
"Show tool at cursor": "Afficher l'outil près du curseur",
|
||||
"Signed axes": "Axes signés",
|
||||
"Size": "Taille",
|
||||
"Sketch constraints": "Contraintes d'esquisse",
|
||||
@@ -345,9 +348,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft s'est fermé avant l'enregistrement de ces conceptions. Leur dernier enregistrement automatique peut être ouvert.",
|
||||
"Specialise": "Spécialiser",
|
||||
"Speed": "Vitesse",
|
||||
"Square": "Carré",
|
||||
"Stand it up": "Redresser",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Redresser : un quart de tour autour de X, sous forme d'une fonction Déplacer que vous pouvez annuler.",
|
||||
"Stand this part up?": "Redresser cette pièce ?",
|
||||
"Start angle": "Angle de départ",
|
||||
"Start bulge": "Renflement de début",
|
||||
"Start continuity": "Continuité de début",
|
||||
"Step": "Étape",
|
||||
@@ -412,6 +417,7 @@
|
||||
"angle, height": "angle, hauteur",
|
||||
"appearance.dielectric_ior": "Indice de réfraction (diélectrique)",
|
||||
"appearance.metallic": "Métallique",
|
||||
"appearance.only_available": "Apparences disponibles uniquement",
|
||||
"appearance.pbr_help": "Voir cette surface dans l'espace Rendu ou dans un PNG du produit. Sans réfraction ni transmission.",
|
||||
"appearance.pbr_opaque": "PBR opaque",
|
||||
"appearance.roughness": "Rugosité",
|
||||
@@ -530,6 +536,10 @@
|
||||
"cmd.file_insert_step": "Insérer un STEP",
|
||||
"cmd.file_save_mesh": "Enregistrer comme maillage",
|
||||
"cmd.file_slicers": "Slicers installés",
|
||||
"cmd.frame_member": "Élément de structure",
|
||||
"cmd.frame_profile_insert": "Insérer un profilé",
|
||||
"cmd.frame_profile_list": "Bibliothèque de profilés",
|
||||
"cmd.frame_profile_properties": "Propriétés du profilé",
|
||||
"cmd.inspect_accessibility": "Analyse d'accessibilité",
|
||||
"cmd.inspect_center_of_mass": "Centre de gravité",
|
||||
"cmd.inspect_clearance": "Jeu",
|
||||
@@ -569,6 +579,7 @@
|
||||
"cmd.material_create": "Nouveau matériau physique",
|
||||
"cmd.material_delete": "Supprimer le matériau physique",
|
||||
"cmd.material_edit": "Modifier le matériau physique",
|
||||
"cmd.material_inspect": "Inspecter le matériau physique",
|
||||
"cmd.material_list": "Lister les matériaux",
|
||||
"cmd.material_properties_directional": "Module d'élasticité directionnel",
|
||||
"cmd.material_properties_validate": "Valider les données du matériau physique",
|
||||
@@ -857,11 +868,17 @@
|
||||
"help.mit": "Licence MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.solid.chamfer": "Coupe les arêtes sélectionnées par un biseau plat de la distance indiquée. À utiliser pour casser des arêtes vives ou faciliter l'entrée d'une goupille ou d'une vis.",
|
||||
"help.solid.combine": "Joint des corps, les retire d'un corps cible ou ne garde que le volume qu'ils ont en commun, et peut conserver les corps outils. À utiliser pour fusionner des pièces, tailler une forme avec une autre ou repérer où deux corps se recouvrent.",
|
||||
"help.solid.extrude": "Pousse le profil d'une esquisse en 3D sur une distance, en ajoutant ou en retirant de la matière. À utiliser pour transformer un contour plat en bloc, en bossage ou en poche.",
|
||||
"help.solid.fillet": "Arrondit les arêtes sélectionnées par un raccord circulaire lisse du rayon indiqué. À utiliser pour adoucir des arêtes vives par souci de solidité, de confort ou d'aspect.",
|
||||
"help.solid.hole": "Perce un trou rond du diamètre indiqué en un point choisi d'une face. À utiliser pour les passages de vis, les goupilles et autres ouvertures rondes simples.",
|
||||
"help.solid.loft": "Raccorde en douceur deux profils d'esquisse ou plus, ou un profil et un point, par un solide lissé. À utiliser pour les transitions, entonnoirs, poignées et toute forme dont la section change sur sa longueur.",
|
||||
"help.solid.mirror": "Crée une copie symétrique des corps ou fonctions sélectionnés par rapport à un plan, et peut la fusionner avec l'original. À utiliser pour les pièces symétriques : on modélise une moitié et on obtient les deux.",
|
||||
"help.solid.pattern.circular": "Répète les objets sélectionnés autour d'un axe, selon le nombre et l'angle total indiqués. À utiliser pour les cercles de perçages, rayons, ailettes et dents.",
|
||||
"help.solid.pattern.rectangular": "Répète les objets sélectionnés dans une ou deux directions, selon le nombre et l'espacement indiqués. À utiliser pour les rangées et grilles de trous, nervures, goupilles ou pieds.",
|
||||
"help.solid.revolve": "Fait tourner le profil d'une esquisse autour d'un axe pour obtenir un solide de révolution. À utiliser pour les arbres, boutons, bols et toute pièce ronde autour d'une ligne centrale.",
|
||||
"help.solid.shell": "Évide un solide en laissant des parois de l'épaisseur indiquée, et peut ouvrir les faces choisies. À utiliser pour les boîtes, couvercles et autres pièces à parois minces.",
|
||||
"help.solid.sweep": "Entraîne le profil d'une esquisse le long d'une trajectoire pour obtenir un solide. À utiliser pour les tuyaux, rails, poignées et toute pièce de section constante qui suit une courbe.",
|
||||
"help.third_party": "Licences tierces",
|
||||
"home.back": "Revenir à {name}",
|
||||
"home.crashes": "Rapports de plantage",
|
||||
@@ -966,6 +983,7 @@
|
||||
"motion.key_here": "Placer une image clé ici",
|
||||
"motion.key_here_hint": "Ajouter une image clé à cette étape avec la valeur actuelle de la liaison",
|
||||
"motion.no_collisions": "Aucune collision au cours de l'étude",
|
||||
"motion.plot_quantity": "Grandeur",
|
||||
"motion.step_back": "Étape précédente",
|
||||
"motion.step_forward": "Étape suivante",
|
||||
"motion.to_end": "À la fin",
|
||||
@@ -1044,13 +1062,13 @@
|
||||
"sample.bracket.name": "Équerre en tôle",
|
||||
"sample.enclosure.about": "Boîte en coque avec lèvre, bossages de vis, appui et clip",
|
||||
"sample.enclosure.name": "Boîtier plastique",
|
||||
"sample.flange.about": "Moyeu et bride avec un réseau circulaire piloté par un paramètre",
|
||||
"sample.flange.about": "Moyeu et bride avec une répétition circulaire pilotée par un paramètre",
|
||||
"sample.flange.name": "Bride boulonnée",
|
||||
"sample.hinge.about": "Deux composants et une liaison pivot, entraînée à 60°",
|
||||
"sample.hinge.name": "Assemblage de charnière",
|
||||
"sample.plate.about": "Plaque paramétrique avec congés, perçages et un bossage alésé",
|
||||
"sample.plate.name": "Plaque d'exemple",
|
||||
"sample.vents.about": "Rainures en réseau rectangulaire : lignes, colonnes et pas sont des paramètres",
|
||||
"sample.vents.about": "Rainures en répétition rectangulaire : lignes, colonnes et pas sont des paramètres",
|
||||
"sample.vents.name": "Plaque d'aération",
|
||||
"search.command_keys": "Entrée exécuter Maj+Entrée exécuter et garder ouvert Échap fermer",
|
||||
"search.commands": "Commandes",
|
||||
@@ -1066,12 +1084,14 @@
|
||||
"search.try": "Saisissez un nom de commande, une entité de cette conception ou un paramètre. Pour exécuter une commande avec des valeurs, saisissez son id et un objet JSON : {example}",
|
||||
"search.valid": "Valide",
|
||||
"settings.display_quality_note": "Les niveaux plus fins affichent plus lisses les faces et arêtes courbes, mais leur maillage prend plus de temps. Seule la vue change, jamais la conception ni les exports.",
|
||||
"settings.frame_rate_note": "Affiche les images par seconde dans le coin inférieur droit de la vue. Tant qu'elle est activée, la vue est redessinée en continu.",
|
||||
"settings.frame_rate_note": "Affiche les images par seconde dans le coin inférieur droit de la vue.",
|
||||
"settings.measure_axis_distances": "Afficher les cotes X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Afficher les cotes suivant les axes du repère global en plus de la distance totale pendant la mesure.",
|
||||
"settings.msaa_applies_on_restart": "L'anticrénelage s'appliquera au prochain démarrage de SolidCraft",
|
||||
"settings.msaa_reset": "{setting} : {requested}× n'est pas pris en charge par ce périphérique graphique ; la valeur est désormais {reset}× (pris en charge : {supported}).",
|
||||
"settings.msaa_unsupported": "Non pris en charge par ce périphérique graphique",
|
||||
"settings.reference_geometry_through_bodies": "Afficher la géométrie de référence à travers les corps",
|
||||
"settings.reference_geometry_through_bodies_help": "Afficher les contours des esquisses inactives et les plans de référence par-dessus les corps ombrés",
|
||||
"settings.reset_intro": "Ces réglages ne fonctionnent pas sur cet ordinateur et empêcheraient SolidCraft de démarrer ; ils ont donc été modifiés :",
|
||||
"settings.reset_title": "Des réglages ont été modifiés",
|
||||
"settings.supersampling_note": "Dessine la vue à une résolution plus élevée puis la réduit : arêtes, lignes et grille plus lisses sur les écrans de définition inférieure à la 4K. Cela coûte du temps GPU : environ 2,25× plus de pixels à 1,5× et 4× plus à 2×.",
|
||||
@@ -1127,7 +1147,106 @@
|
||||
"trace.stopped_by_contact": "Se termine à l'échantillon {sample} : {a} traverserait {b}",
|
||||
"trace.stopped_by_loop": "Se termine à l'échantillon {sample} : la boucle passant par {joint} ne peut plus se fermer",
|
||||
"ui.1_degree_of_freedom": "1 degré de liberté",
|
||||
"ui.3d_mouse_axes_reassigned": "Les axes sont réattribués dans les Préférences, et cela s'applique toujours en plus de ce calibrage.",
|
||||
"ui.3d_mouse_button": "Bouton {number}",
|
||||
"ui.3d_mouse_button_nothing": "Rien",
|
||||
"ui.3d_mouse_button_settings": "Réglages de la souris 3D",
|
||||
"ui.3d_mouse_buttons": "Boutons",
|
||||
"ui.3d_mouse_buttons_hint": "Appuyez sur un bouton de votre souris 3D pour le trouver ici, puis choisissez ce qu'il fait.",
|
||||
"ui.3d_mouse_buttons_no_device": "Branchez une souris 3D pour choisir ce que font ses boutons.",
|
||||
"ui.3d_mouse_buttons_reset": "Rétablir les boutons par défaut",
|
||||
"ui.3d_mouse_calibrate": "Calibrer…",
|
||||
"ui.3d_mouse_calibrate_tip": "Déplacez le capuchon comme la fenêtre le demande, pour que SolidCraft apprenne dans quel sens et jusqu'où va chaque axe de cette souris 3D. Enregistré pour cette souris 3D.",
|
||||
"ui.3d_mouse_calibrate_title": "Calibrer la souris 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Bougez la souris 3D une fois pour que SolidCraft la voie.",
|
||||
"ui.3d_mouse_calibrated": "Calibrée",
|
||||
"ui.3d_mouse_calibration": "Calibrage de la souris 3D",
|
||||
"ui.3d_mouse_calibration_done": "Terminé. Enregistrez-le pour l'utiliser chaque fois que cette souris 3D est connectée.",
|
||||
"ui.3d_mouse_calibration_no_response": "Un axe a à peine bougé, il ne peut donc pas être calibré. Recommencez en déplaçant le capuchon plus loin à chaque fois.",
|
||||
"ui.3d_mouse_circles_help": "Les cercles montrent ce que l'appareil envoie après calibrage. Le modèle d'essai montre ce que la vue en fait, après inversion, vitesse et affectation des axes.",
|
||||
"ui.3d_mouse_dead_zone": "Zone morte",
|
||||
"ui.3d_mouse_dead_zone_tip": "De combien le capuchon doit bouger avant que la vue ne bouge, en part de sa course complète. Augmentez-la si la vue dérive alors que le capuchon est au repos.",
|
||||
"ui.3d_mouse_denied": "SolidCraft voit une souris 3D mais n'a pas le droit de la lire. La règle udev est dans les notes d'installation de la souris 3D.",
|
||||
"ui.3d_mouse_denied_short": "Trouvée, mais illisible",
|
||||
"ui.3d_mouse_driven_by": "Quel mouvement calibré de l'appareil pilote cet axe. Choisir un mouvement déjà utilisé par un autre axe les échange.",
|
||||
"ui.3d_mouse_driven_by_label": "piloté par",
|
||||
"ui.3d_mouse_driver": "Pilote de la souris 3D",
|
||||
"ui.3d_mouse_driver_auto": "Automatique",
|
||||
"ui.3d_mouse_driver_direct": "Direct (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Lecture via spacenavd pour l'instant : l'appareil lui-même n'a pas pu être ouvert.",
|
||||
"ui.3d_mouse_driver_only_direct": "Lecture directe du périphérique ; spacenavd n'est pas lancé",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Lecture via spacenavd, seule possibilité disponible",
|
||||
"ui.3d_mouse_forget": "Oublier le calibrage",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Vue à gauche",
|
||||
"ui.3d_mouse_ghost_left_window": "Fenêtre à gauche",
|
||||
"ui.3d_mouse_ghost_position": "Position",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Vue à droite",
|
||||
"ui.3d_mouse_hint": "Déplace et fait pivoter la vue en même temps ; dans les esquisses, seuls le panoramique et le zoom sont utilisés.",
|
||||
"ui.3d_mouse_kind_cap": "Retour au centre",
|
||||
"ui.3d_mouse_kind_question": "Quel type de souris 3D est-ce ?",
|
||||
"ui.3d_mouse_kind_trackball": "Trackball",
|
||||
"ui.3d_mouse_looks_like_cap": "La valeur reste tant que vous maintenez, comme sur un appareil de type « Retour au centre ».",
|
||||
"ui.3d_mouse_looks_like_trackball": "La valeur retombe dès que vous arrêtez de bouger, comme sur un trackball.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Rotation X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Rotation Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Rotation Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Translation X",
|
||||
"ui.3d_mouse_motion_translation_y": "Translation Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Translation Z",
|
||||
"ui.3d_mouse_navigate": "Naviguer avec la souris 3D",
|
||||
"ui.3d_mouse_none": "Aucune souris 3D trouvée",
|
||||
"ui.3d_mouse_not_calibrated": "Non calibrée",
|
||||
"ui.3d_mouse_offer": "Calibrer votre souris 3D corrige les axes échangés ou inversés et règle sa vitesse et sa sensation.",
|
||||
"ui.3d_mouse_offer_never": "Ne plus afficher",
|
||||
"ui.3d_mouse_offer_swap": "Passer à Hidraw (pilote direct)",
|
||||
"ui.3d_mouse_reading": "Lecture",
|
||||
"ui.3d_mouse_reset_axes": "Réinitialiser l'attribution des axes",
|
||||
"ui.3d_mouse_reset_view": "Réinitialiser la vue",
|
||||
"ui.3d_mouse_restore": "Rétablir les réglages de la souris 3D",
|
||||
"ui.3d_mouse_restore_ask": "Remettre les réglages de la souris 3D par défaut ? Votre calibrage et l'assignation des boutons sont conservés.",
|
||||
"ui.3d_mouse_section": "Souris 3D",
|
||||
"ui.3d_mouse_show_in_view": "Afficher la souris 3D dans la vue 3D",
|
||||
"ui.3d_mouse_show_in_view_tip": "Une image de votre souris 3D dans le coin inférieur gauche, qui bouge quand vous la bougez.",
|
||||
"ui.3d_mouse_skip": "Mon appareil ne peut pas le faire",
|
||||
"ui.3d_mouse_skip_tip": "Ignore ce mouvement dans les deux sens ; la calibration le laisse de côté.",
|
||||
"ui.3d_mouse_speed": "Vitesse de la souris 3D",
|
||||
"ui.3d_mouse_step_lift": "Soulevez le capuchon, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_press": "Appuyez sur le capuchon, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_pull": "Tirez le capuchon vers vous, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_push": "Poussez le capuchon loin de vous, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_rest": "Lâchez le capuchon et laissez-le immobile.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Lâchez la boule et laissez-la immobile.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Tournez l'anneau de défilement de quelques crans dans le sens horaire.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Tournez l'anneau de défilement de quelques crans dans le sens antihoraire.",
|
||||
"ui.3d_mouse_step_roll_away": "Faites rouler la boule loin de vous plusieurs fois, puis laissez-la s'arrêter.",
|
||||
"ui.3d_mouse_step_roll_left": "Faites rouler la boule vers la gauche plusieurs fois, puis laissez-la s'arrêter.",
|
||||
"ui.3d_mouse_step_roll_right": "Faites rouler la boule vers la droite plusieurs fois, puis laissez-la s'arrêter.",
|
||||
"ui.3d_mouse_step_roll_toward": "Faites rouler la boule vers vous plusieurs fois, puis laissez-la s'arrêter.",
|
||||
"ui.3d_mouse_step_slide_left": "Glissez le capuchon vers la gauche, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_slide_right": "Glissez le capuchon vers la droite, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Faites tourner la boule dans le sens horaire plusieurs fois, puis laissez-la s'arrêter.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Faites tourner la boule dans le sens antihoraire plusieurs fois, puis laissez-la s'arrêter.",
|
||||
"ui.3d_mouse_step_tilt_away": "Inclinez le haut du capuchon loin de vous (le bord le plus proche monte), maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_tilt_left": "Inclinez le capuchon vers la gauche (bord gauche vers le bas), maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_tilt_right": "Inclinez le capuchon vers la droite (bord droit vers le bas), maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Inclinez le haut du capuchon vers vous (le bord le plus proche descend), maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_touch": "Posez la main sur le capuchon sans le pousser.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Tournez le capuchon dans le sens horaire, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Tournez le capuchon dans le sens antihoraire, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_switch_cap": "Calibrer comme retour au centre",
|
||||
"ui.3d_mouse_switch_trackball": "Calibrer comme un trackball",
|
||||
"ui.3d_mouse_view_axis": "Axe de la vue",
|
||||
"ui.3d_mouse_view_off": "Désactivé",
|
||||
"ui.3d_mouse_view_off_tip": "Le profil du pilote de la souris 3D désactive cet axe : son mouvement ne déplace pas la vue. Choisir ici le mouvement d'un autre axe les échange, et ce mouvement cesse de déplacer la vue.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbite gauche / droite",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbite haut / bas",
|
||||
"ui.3d_mouse_view_pan_sideways": "Panoramique gauche / droite",
|
||||
"ui.3d_mouse_view_pan_up_down": "Panoramique haut / bas",
|
||||
"ui.3d_mouse_view_roll": "Roulis",
|
||||
"ui.3d_mouse_view_unmapped": "Axe {n}",
|
||||
"ui.3d_mouse_view_zoom": "Zoom",
|
||||
"ui.3d_mouse_waiting": "En attente de mouvement",
|
||||
"ui.3d_mouse_wiggle": "Si vous en avez une de branchée, remuez-la.",
|
||||
"ui.3d_sketch": "Esquisse 3D",
|
||||
"ui.activate": "Activer",
|
||||
"ui.active_component": "Composant actif",
|
||||
@@ -1189,6 +1308,7 @@
|
||||
"ui.agent_chat_files_left_out": "Un message ne peut contenir que {max} fichiers au plus ; non joints : {files}",
|
||||
"ui.agent_chat_files_loading": "Lecture des fichiers en cours",
|
||||
"ui.agent_chat_files_max": "{max} fichiers au plus par message",
|
||||
"ui.agent_chat_flatpak_none_found": "Aucun agent de programmation trouvé. Par défaut, le bac à sable Flatpak ne voit pas les agents installés dans votre dossier personnel et n’a pas accès au réseau. Pour autoriser Claude Code et son adaptateur ACP installés avec nvm ou dans ~/.local/bin, exécuter cette commande dans un terminal, puis redémarrer SolidCraft. Elle donne accès aux programmes et fichiers de connexion indiqués, ainsi qu’au réseau :",
|
||||
"ui.agent_chat_import_failed": "{name} : l'importation a échoué : {error}",
|
||||
"ui.agent_chat_intro": "Discuter avec un agent de programmation que vous utilisez déjà, ou avec un point d'accès de modèle de votre choix. Il travaille sur la conception ouverte avec les outils de SolidCraft : ses modifications apparaissent au fur et à mesure et s'annulent en une seule étape par message. Ce qu'il peut faire sans demander se règle dans Préférences › IA.",
|
||||
"ui.agent_chat_key_in_env": "clé définie",
|
||||
@@ -1284,6 +1404,7 @@
|
||||
"ui.ai_http_busy": "le point d'accès est occupé ou a dépassé sa limite ; réessayez plus tard",
|
||||
"ui.ai_http_key": "le point d'accès a refusé la clé : vérifiez-la dans Préférences › IA",
|
||||
"ui.ai_http_not_found": "point d'accès ou modèle introuvable : vérifiez l'URL de base et le nom du modèle",
|
||||
"ui.ai_http_too_large": "la conversation est trop volumineuse pour ce point d'accès ; commencez une nouvelle discussion",
|
||||
"ui.ai_key": "Clé d'API",
|
||||
"ui.ai_key_hint": "facultative ; conservée dans le trousseau de clés du système",
|
||||
"ui.ai_key_needs_https": "Une clé n'est envoyée qu'en HTTPS, ou vers cet ordinateur.",
|
||||
@@ -1311,7 +1432,6 @@
|
||||
"ui.ai_permissions": "Autorisations",
|
||||
"ui.ai_preset": "Préréglage",
|
||||
"ui.ai_remove": "Retirer",
|
||||
"ui.ai_request_limit": "arrêté après 24 requêtes pour un message ; envoyez un autre message pour poursuivre",
|
||||
"ui.ai_reset": "Réinitialiser",
|
||||
"ui.ai_settings": "Réglages IA…",
|
||||
"ui.ai_show_features": "Afficher les fonctionnalités IA",
|
||||
@@ -1356,6 +1476,7 @@
|
||||
"ui.blocked": "Inaccessible",
|
||||
"ui.bodies": "Corps",
|
||||
"ui.bodies_all_if_none": "Corps (tous si aucun)",
|
||||
"ui.bottom": "Dessous",
|
||||
"ui.box": "Boîte",
|
||||
"ui.break": "Fractionner",
|
||||
"ui.browser_bodies_one": "{count} corps",
|
||||
@@ -1373,7 +1494,7 @@
|
||||
"ui.chamfer_type": "Type de chanfrein",
|
||||
"ui.change_workspace": "Changer d'espace de travail",
|
||||
"ui.circle": "Cercle",
|
||||
"ui.circular_pattern": "Réseau circulaire",
|
||||
"ui.circular_pattern": "Répétition circulaire",
|
||||
"ui.clear": "Effacer",
|
||||
"ui.clear_recent": "Effacer les récents",
|
||||
"ui.clear_the_selection": "Effacer la sélection",
|
||||
@@ -1487,6 +1608,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtrer les commandes ou les touches",
|
||||
"ui.findInTimeline": "Trouver dans la chronologie",
|
||||
"ui.findInWindow": "Trouver dans la fenêtre",
|
||||
"ui.fine": "Fine",
|
||||
"ui.finest": "Très fine",
|
||||
"ui.finish_sketch": "Terminer l'esquisse",
|
||||
"ui.fit": "Cadrer",
|
||||
"ui.fix_unfix": "Fixer/Libérer",
|
||||
@@ -1508,11 +1631,12 @@
|
||||
"ui.grid_follows_view_hint": "Dans les vues de côté, la grille s'affiche sur le plan vertical qui leur fait face plutôt que sur le sol",
|
||||
"ui.ground_shadow": "Ombre au sol",
|
||||
"ui.groupSelected": "Grouper la sélection",
|
||||
"ui.hatch": "Hachures",
|
||||
"ui.heading_add_ins": "COMPLÉMENTS",
|
||||
"ui.heading_assemble": "ASSEMBLER",
|
||||
"ui.heading_box": "BOÎTE",
|
||||
"ui.heading_chamfer": "CHANFREIN",
|
||||
"ui.heading_circular_pattern": "RÉSEAU CIRCULAIRE",
|
||||
"ui.heading_circular_pattern": "RÉPÉTITION CIRCULAIRE",
|
||||
"ui.heading_coil": "HÉLICE",
|
||||
"ui.heading_combine": "COMBINER",
|
||||
"ui.heading_configure": "CONFIGURER",
|
||||
@@ -1545,14 +1669,14 @@
|
||||
"ui.heading_offset_plane": "PLAN DÉCALÉ",
|
||||
"ui.heading_ok": "OK",
|
||||
"ui.heading_parameters": "PARAMÈTRES",
|
||||
"ui.heading_pattern": "RÉSEAU",
|
||||
"ui.heading_pattern_on_path": "RÉSEAU SUR TRAJECTOIRE",
|
||||
"ui.heading_pattern": "RÉPÉTITION",
|
||||
"ui.heading_pattern_on_path": "RÉPÉTITION SUR TRAJECTOIRE",
|
||||
"ui.heading_pipe": "TUYAU",
|
||||
"ui.heading_plane_at_angle": "PLAN INCLINÉ",
|
||||
"ui.heading_plastic": "PLASTIQUE",
|
||||
"ui.heading_position": "POSITION",
|
||||
"ui.heading_prepare": "PRÉPARER",
|
||||
"ui.heading_rectangular_pattern": "RÉSEAU RECTANGULAIRE",
|
||||
"ui.heading_rectangular_pattern": "RÉPÉTITION RECTANGULAIRE",
|
||||
"ui.heading_revolve": "RÉVOLUTION",
|
||||
"ui.heading_right": "DROITE",
|
||||
"ui.heading_save_mesh": "ENREGISTRER COMME MAILLAGE",
|
||||
@@ -1574,6 +1698,7 @@
|
||||
"ui.heading_torus": "TORE",
|
||||
"ui.heading_utilities": "UTILITAIRES",
|
||||
"ui.heading_utility": "UTILITAIRE",
|
||||
"ui.heading_weldments": "MÉCANO-SOUDURE",
|
||||
"ui.help": "Aide",
|
||||
"ui.hide": "Masquer",
|
||||
"ui.hideAxes": "Masquer les axes",
|
||||
@@ -1682,7 +1807,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_on_path": "Réseau sur trajectoire",
|
||||
"ui.pattern_on_path": "Répétition sur trajectoire",
|
||||
"ui.perpendicular": "Perpendicularité",
|
||||
"ui.perspective_view": "Vue en perspective",
|
||||
"ui.pinToShortcuts": "Épingler aux raccourcis",
|
||||
@@ -1710,6 +1835,7 @@
|
||||
"ui.project_face_edges_sketch": "Projeter les arêtes de la face dans les nouvelles esquisses",
|
||||
"ui.properties": "Propriétés",
|
||||
"ui.pull_direction": "Direction de démoulage",
|
||||
"ui.quality": "Qualité",
|
||||
"ui.quantity": "Quantité",
|
||||
"ui.quantity_2": "Quantité 2",
|
||||
"ui.quit": "Quitter",
|
||||
@@ -1719,7 +1845,7 @@
|
||||
"ui.rail_fit": "Ajustement au rail",
|
||||
"ui.reachable": "Accessible",
|
||||
"ui.rectangle": "Rectangle",
|
||||
"ui.rectangular_pattern": "Réseau rectangulaire",
|
||||
"ui.rectangular_pattern": "Répétition rectangulaire",
|
||||
"ui.redo": "Rétablir",
|
||||
"ui.relink_design_changed": "L’assemblage a changé pendant le choix d’une source. Relancez Relier à nouveau la source.",
|
||||
"ui.relink_done": "Source reliée à nouveau pour toutes les instances.",
|
||||
@@ -1798,65 +1924,13 @@
|
||||
"ui.smooth_wheel_zoom": "Zoom fluide à la molette",
|
||||
"ui.smooth_wheel_zoom_tip": "Désactiver pour appliquer chaque cran de molette immédiatement, sans lissage",
|
||||
"ui.snap": "Accrochage",
|
||||
"ui.space_mouse_axes_reassigned": "Les axes sont réattribués dans les Préférences, et cela s'applique toujours en plus de ce calibrage.",
|
||||
"ui.space_mouse_calibrate": "Calibrer…",
|
||||
"ui.space_mouse_calibrate_tip": "Déplacez le capuchon comme la fenêtre le demande, pour que SolidCraft apprenne dans quel sens et jusqu'où va chaque axe de cette souris 3D. Enregistré pour cette souris 3D.",
|
||||
"ui.space_mouse_calibrate_title": "Calibrer la souris 3D",
|
||||
"ui.space_mouse_calibrated": "Calibrée",
|
||||
"ui.space_mouse_calibration": "Calibrage de la souris 3D",
|
||||
"ui.space_mouse_calibration_done": "Terminé. Enregistrez-le pour l'utiliser chaque fois que cette souris 3D est connectée.",
|
||||
"ui.space_mouse_calibration_no_response": "Un axe a à peine bougé, il ne peut donc pas être calibré. Recommencez en déplaçant le capuchon plus loin à chaque fois.",
|
||||
"ui.space_mouse_dead_zone": "Zone morte",
|
||||
"ui.space_mouse_dead_zone_tip": "De combien le capuchon doit bouger avant que la vue ne bouge, en part de sa course complète. Augmentez-la si la vue dérive alors que le capuchon est au repos.",
|
||||
"ui.space_mouse_driven_by": "Quel mouvement calibré de l'appareil pilote cet axe. Choisir un mouvement déjà utilisé par un autre axe les échange.",
|
||||
"ui.space_mouse_driven_by_label": "piloté par",
|
||||
"ui.space_mouse_driver": "Pilote de la souris 3D",
|
||||
"ui.space_mouse_driver_auto": "Automatique",
|
||||
"ui.space_mouse_driver_direct": "Direct (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Lecture directe du périphérique ; spacenavd n'est pas lancé",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Lecture via spacenavd, seule possibilité disponible",
|
||||
"ui.space_mouse_forget": "Oublier le calibrage",
|
||||
"ui.space_mouse_hint": "Déplace et fait pivoter la vue en même temps ; dans les esquisses, seuls le panoramique et le zoom sont utilisés.",
|
||||
"ui.space_mouse_motion_rotation_x": "Rotation X",
|
||||
"ui.space_mouse_motion_rotation_y": "Rotation Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Rotation Z",
|
||||
"ui.space_mouse_motion_translation_x": "Translation X",
|
||||
"ui.space_mouse_motion_translation_y": "Translation Y",
|
||||
"ui.space_mouse_motion_translation_z": "Translation Z",
|
||||
"ui.space_mouse_navigate": "Naviguer avec SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Aucune souris 3D trouvée",
|
||||
"ui.space_mouse_not_calibrated": "Non calibrée",
|
||||
"ui.space_mouse_reading": "Lecture",
|
||||
"ui.space_mouse_reset_axes": "Réinitialiser l'attribution des axes",
|
||||
"ui.space_mouse_step_lift": "Soulevez le capuchon, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_press": "Appuyez sur le capuchon, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_pull": "Tirez le capuchon vers vous, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_push": "Poussez le capuchon loin de vous, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_rest": "Lâchez le capuchon et laissez-le immobile.",
|
||||
"ui.space_mouse_step_slide_left": "Glissez le capuchon vers la gauche, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_slide_right": "Glissez le capuchon vers la droite, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_tilt_back": "Inclinez le capuchon vers l'arrière (bord avant vers le haut), maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_tilt_forward": "Inclinez le capuchon vers l'avant (bord avant vers le bas), maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_tilt_left": "Inclinez le capuchon vers la gauche (bord gauche vers le bas), maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_tilt_right": "Inclinez le capuchon vers la droite (bord droit vers le bas), maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Tournez le capuchon dans le sens horaire, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Tournez le capuchon dans le sens antihoraire, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_view_axis": "Axe de la vue",
|
||||
"ui.space_mouse_view_off": "Désactivé",
|
||||
"ui.space_mouse_view_off_tip": "Le profil du pilote de la souris 3D désactive cet axe : son mouvement ne déplace pas la vue. Choisir ici le mouvement d'un autre axe les échange, et ce mouvement cesse de déplacer la vue.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Orbite gauche / droite",
|
||||
"ui.space_mouse_view_orbit_up_down": "Orbite haut / bas",
|
||||
"ui.space_mouse_view_pan_sideways": "Panoramique gauche / droite",
|
||||
"ui.space_mouse_view_pan_up_down": "Panoramique haut / bas",
|
||||
"ui.space_mouse_view_roll": "Roulis",
|
||||
"ui.space_mouse_view_unmapped": "Axe {n}",
|
||||
"ui.space_mouse_view_zoom": "Zoom",
|
||||
"ui.spacing": "Espacement",
|
||||
"ui.spacing_2": "Espacement 2",
|
||||
"ui.sphere": "Sphère",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Scinder le corps",
|
||||
"ui.stand_up_tip": "Après l'ouverture d'un fichier STEP ou IGES avec Z vers le haut, proposer un quart de tour autour de X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Début",
|
||||
"ui.start_over": "Recommencer",
|
||||
"ui.start_page": "Page d’accueil",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Escoller…",
|
||||
"Circular": "Circular",
|
||||
"Circumference": "Circunferencia",
|
||||
"Clockwise": "Horario",
|
||||
"Close draft": "Pechar o borrador",
|
||||
"Collisions": "Colisións",
|
||||
"Colour": "Cor",
|
||||
@@ -322,6 +323,7 @@
|
||||
"Search commands": "Buscar ordes",
|
||||
"Search this library": "Buscar nesta biblioteca",
|
||||
"Section": "Sección",
|
||||
"Section position": "Posición da sección",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Seleccionar outro: mantén o botón esquerdo quieto sobre o modelo (ou clic dereito › Seleccionar outro…) para listar todo o que hai baixo o cursor; fai clic de novo no mesmo punto para seleccionar o seguinte. Fai dobre clic co botón central para ir ao Inicio (Preferencias › Navegación).",
|
||||
"Select bodies (a face picks its body)": "Selecciona corpos (unha cara escolle o seu corpo)",
|
||||
"Select bodies or faces instead": "Seleccionar corpos ou caras no seu lugar",
|
||||
@@ -333,6 +335,7 @@
|
||||
"Show Agent Cursor": "Mostrar o cursor do axente",
|
||||
"Show in folder": "Mostrar no cartafol",
|
||||
"Show the pointer an AI agent moves (#35)": "Mostrar o punteiro que move un axente de IA (#35)",
|
||||
"Show tool at cursor": "Mostrar a ferramenta xunto ao cursor",
|
||||
"Signed axes": "Eixes con signo",
|
||||
"Size": "Tamaño",
|
||||
"Sketch constraints": "Restricións do bosquexo",
|
||||
@@ -345,9 +348,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft pechouse antes de gardar estes deseños. Pódese abrir a súa última gravación automática.",
|
||||
"Specialise": "Especializar",
|
||||
"Speed": "Velocidade",
|
||||
"Square": "Cadrado",
|
||||
"Stand it up": "Poñela de pé",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Poñela de pé: un cuarto de volta arredor de X, como unha operación Mover que podes desfacer.",
|
||||
"Stand this part up?": "Poñer esta peza de pé?",
|
||||
"Start angle": "Ángulo inicial",
|
||||
"Start bulge": "Abaulamento inicial",
|
||||
"Start continuity": "Continuidade inicial",
|
||||
"Step": "Paso",
|
||||
@@ -412,6 +417,7 @@
|
||||
"angle, height": "ángulo, altura",
|
||||
"appearance.dielectric_ior": "IOR dieléctrico",
|
||||
"appearance.metallic": "Metálico",
|
||||
"appearance.only_available": "Só aparencias dispoñibles",
|
||||
"appearance.pbr_help": "Ve esta superficie en Render ou nun PNG de produto. Sen refracción nin transmisión.",
|
||||
"appearance.pbr_opaque": "PBR opaco",
|
||||
"appearance.roughness": "Rugosidade",
|
||||
@@ -530,6 +536,10 @@
|
||||
"cmd.file_insert_step": "Inserir STEP",
|
||||
"cmd.file_save_mesh": "Gardar como malla",
|
||||
"cmd.file_slicers": "Laminadores instalados",
|
||||
"cmd.frame_member": "Membro estrutural",
|
||||
"cmd.frame_profile_insert": "Inserir perfil",
|
||||
"cmd.frame_profile_list": "Biblioteca de perfís",
|
||||
"cmd.frame_profile_properties": "Propiedades do perfil",
|
||||
"cmd.inspect_accessibility": "Análise de accesibilidade",
|
||||
"cmd.inspect_center_of_mass": "Centro de masas",
|
||||
"cmd.inspect_clearance": "Folga",
|
||||
@@ -569,6 +579,7 @@
|
||||
"cmd.material_create": "Material físico novo",
|
||||
"cmd.material_delete": "Eliminar o material físico",
|
||||
"cmd.material_edit": "Editar o material físico",
|
||||
"cmd.material_inspect": "Inspeccionar o material físico",
|
||||
"cmd.material_list": "Listar os materiais",
|
||||
"cmd.material_properties_directional": "Módulo elástico direccional",
|
||||
"cmd.material_properties_validate": "Validar os datos do material físico",
|
||||
@@ -960,6 +971,7 @@
|
||||
"motion.key_here": "Inserir fotograma clave aquí",
|
||||
"motion.key_here_hint": "Engadir un fotograma clave neste paso co valor actual da unión",
|
||||
"motion.no_collisions": "Sen colisións durante o estudo",
|
||||
"motion.plot_quantity": "Cantidade",
|
||||
"motion.step_back": "Paso atrás",
|
||||
"motion.step_forward": "Paso adiante",
|
||||
"motion.to_end": "Ao final",
|
||||
@@ -1060,7 +1072,7 @@
|
||||
"search.try": "Escribe o nome dunha orde, unha operación deste deseño ou un parámetro. Para executar unha orde con valores, escribe o seu id e un obxecto JSON: {example}",
|
||||
"search.valid": "Válido",
|
||||
"settings.display_quality_note": "Os niveis máis finos debuxan as caras e arestas curvas máis suaves e tardan máis en xerar a malla. Só cambia a vista, nunca o deseño nin as exportacións.",
|
||||
"settings.frame_rate_note": "Mostra as imaxes por segundo na esquina inferior dereita da vista. Mentres está activado, a vista redebúxase continuamente.",
|
||||
"settings.frame_rate_note": "Mostra as imaxes por segundo na esquina inferior dereita da vista.",
|
||||
"settings.measure_axis_distances": "Mostrar as distancias en X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Mostra as compoñentes da distancia segundo os eixes do mundo xunto á distancia total mentres Medir está aberto.",
|
||||
"settings.msaa_applies_on_restart": "O antialiasing aplicarase a próxima vez que se inicie SolidCraft",
|
||||
@@ -1121,7 +1133,106 @@
|
||||
"trace.stopped_by_contact": "Remata na mostra {sample}: {a} atravesaría {b}",
|
||||
"trace.stopped_by_loop": "Remata na mostra {sample}: o bucle a través de {joint} non pode pecharse máis",
|
||||
"ui.1_degree_of_freedom": "1 grao de liberdade",
|
||||
"ui.3d_mouse_axes_reassigned": "Os eixes están reasignados en Preferencias, e iso segue aplicándose sobre esta calibración.",
|
||||
"ui.3d_mouse_button": "Botón {number}",
|
||||
"ui.3d_mouse_button_nothing": "Nada",
|
||||
"ui.3d_mouse_button_settings": "Axustes do rato 3D",
|
||||
"ui.3d_mouse_buttons": "Botóns",
|
||||
"ui.3d_mouse_buttons_hint": "Preme un botón do teu rato 3D para atopalo aquí e escolle que fai.",
|
||||
"ui.3d_mouse_buttons_no_device": "Conecta un rato 3D para escoller que fan os seus botóns.",
|
||||
"ui.3d_mouse_buttons_reset": "Restaurar os botóns predeterminados",
|
||||
"ui.3d_mouse_calibrate": "Calibrar…",
|
||||
"ui.3d_mouse_calibrate_tip": "Move o capuchón como pide a xanela para que SolidCraft aprenda cara a onde e ata onde vai cada eixe deste rato 3D. Gárdase para este rato 3D.",
|
||||
"ui.3d_mouse_calibrate_title": "Calibrar o rato 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Move o rato 3D unha vez para que SolidCraft o vexa.",
|
||||
"ui.3d_mouse_calibrated": "Calibrado",
|
||||
"ui.3d_mouse_calibration": "Calibración do rato 3D",
|
||||
"ui.3d_mouse_calibration_done": "Feito. Gárdaa para usala sempre que este rato 3D estea conectado.",
|
||||
"ui.3d_mouse_calibration_no_response": "Un eixe case non se moveu, así que non se pode calibrar. Volve comezar e move o capuchón máis lonxe cada vez.",
|
||||
"ui.3d_mouse_circles_help": "Os círculos mostran o que envía o dispositivo tras a calibración. O modelo de proba mostra o que fai a vista con iso, tras a inversión, a velocidade e a asignación de eixes.",
|
||||
"ui.3d_mouse_dead_zone": "Zona morta",
|
||||
"ui.3d_mouse_dead_zone_tip": "Canto ten que moverse o capuchón antes de que se mova a vista, como parte do seu percorrido completo. Auméntaa se a vista se despraza co capuchón en repouso.",
|
||||
"ui.3d_mouse_denied": "SolidCraft ve un rato 3D, pero non ten permiso para lelo. A regra udev está nas notas de configuración do rato 3D.",
|
||||
"ui.3d_mouse_denied_short": "Atopado, pero non se pode ler",
|
||||
"ui.3d_mouse_driven_by": "Que movemento calibrado do dispositivo controla este eixe. Se escolles un que xa usa outro eixe, intercámbianse.",
|
||||
"ui.3d_mouse_driven_by_label": "controlado por",
|
||||
"ui.3d_mouse_driver": "Controlador do rato 3D",
|
||||
"ui.3d_mouse_driver_auto": "Automático",
|
||||
"ui.3d_mouse_driver_direct": "Directo (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Lendo polo de agora a través de spacenavd: non se puido abrir o dispositivo.",
|
||||
"ui.3d_mouse_driver_only_direct": "Lectura directa do dispositivo; spacenavd non está en execución",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Lectura mediante spacenavd, a única opción dispoñible",
|
||||
"ui.3d_mouse_forget": "Esquecer a calibración",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Vista esquerda",
|
||||
"ui.3d_mouse_ghost_left_window": "Xanela esquerda",
|
||||
"ui.3d_mouse_ghost_position": "Posición",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Vista dereita",
|
||||
"ui.3d_mouse_hint": "Move e rota a vista á vez; os bosquexos só usan desprazamento e zoom.",
|
||||
"ui.3d_mouse_kind_cap": "Retorno ao centro",
|
||||
"ui.3d_mouse_kind_question": "Que tipo de rato 3D é?",
|
||||
"ui.3d_mouse_kind_trackball": "Trackball",
|
||||
"ui.3d_mouse_looks_like_cap": "A lectura mantense mentres a suxeitas, como nun dispositivo de tipo «Retorno ao centro».",
|
||||
"ui.3d_mouse_looks_like_trackball": "A lectura volve atrás en canto deixas de moverte, como nun trackball.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Rotación X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Rotación Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Rotación Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Translación X",
|
||||
"ui.3d_mouse_motion_translation_y": "Translación Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Translación Z",
|
||||
"ui.3d_mouse_navigate": "Navegar co rato 3D",
|
||||
"ui.3d_mouse_none": "Non se atopou ningún rato 3D",
|
||||
"ui.3d_mouse_not_calibrated": "Sen calibrar",
|
||||
"ui.3d_mouse_offer": "Calibrar o teu rato 3D corrixe os eixes trocados ou invertidos e axusta a súa velocidade e sensación.",
|
||||
"ui.3d_mouse_offer_never": "Non mostrar de novo",
|
||||
"ui.3d_mouse_offer_swap": "Cambiar a Hidraw (controlador directo)",
|
||||
"ui.3d_mouse_reading": "Lectura",
|
||||
"ui.3d_mouse_reset_axes": "Restablecer asignación de eixes",
|
||||
"ui.3d_mouse_reset_view": "Restablecer vista",
|
||||
"ui.3d_mouse_restore": "Restaurar valores do rato 3D",
|
||||
"ui.3d_mouse_restore_ask": "Volver aos valores predeterminados da configuración do rato 3D? A calibración e a asignación de botóns consérvanse.",
|
||||
"ui.3d_mouse_section": "Rato 3D",
|
||||
"ui.3d_mouse_show_in_view": "Mostrar o rato 3D na vista 3D",
|
||||
"ui.3d_mouse_show_in_view_tip": "Unha imaxe do teu rato 3D na esquina inferior esquerda que se move cando o moves.",
|
||||
"ui.3d_mouse_skip": "O meu dispositivo non fai isto",
|
||||
"ui.3d_mouse_skip_tip": "Omite este movemento nos dous sentidos; a calibración déixao fóra.",
|
||||
"ui.3d_mouse_speed": "Velocidade do rato 3D",
|
||||
"ui.3d_mouse_step_lift": "Levanta o capuchón, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_press": "Preme o capuchón cara abaixo, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_pull": "Tira do capuchón cara a ti, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_push": "Empurra o capuchón lonxe de ti, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_rest": "Solta o capuchón e déixao quieto.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Solta a bóla e déixaa quieta.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Xira o anel de desprazamento uns clics no sentido horario.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Xira o anel de desprazamento uns clics no sentido antihorario.",
|
||||
"ui.3d_mouse_step_roll_away": "Fai rodar a bóla lonxe de ti unhas cantas veces e déixaa parar.",
|
||||
"ui.3d_mouse_step_roll_left": "Fai rodar a bóla á esquerda unhas cantas veces e déixaa parar.",
|
||||
"ui.3d_mouse_step_roll_right": "Fai rodar a bóla á dereita unhas cantas veces e déixaa parar.",
|
||||
"ui.3d_mouse_step_roll_toward": "Fai rodar a bóla cara a ti unhas cantas veces e déixaa parar.",
|
||||
"ui.3d_mouse_step_slide_left": "Despraza o capuchón á esquerda, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_slide_right": "Despraza o capuchón á dereita, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Xira a bóla no sentido horario unhas cantas veces e déixaa parar.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Xira a bóla no sentido antihorario unhas cantas veces e déixaa parar.",
|
||||
"ui.3d_mouse_step_tilt_away": "Inclina a parte de arriba do capuchón lonxe de ti (o bordo máis próximo sobe), mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_tilt_left": "Inclina o capuchón á esquerda (bordo esquerdo abaixo), mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_tilt_right": "Inclina o capuchón á dereita (bordo dereito abaixo), mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Inclina a parte de arriba do capuchón cara a ti (o bordo máis próximo baixa), mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_touch": "Apoia a man no capuchón sen empurralo.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Xira o capuchón no sentido horario, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Xira o capuchón no sentido antihorario, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_switch_cap": "Calibrar como retorno ao centro",
|
||||
"ui.3d_mouse_switch_trackball": "Calibrar como trackball",
|
||||
"ui.3d_mouse_view_axis": "Eixe da vista",
|
||||
"ui.3d_mouse_view_off": "Desactivado",
|
||||
"ui.3d_mouse_view_off_tip": "O perfil do controlador do rato 3D desactiva este eixe, así que o seu movemento non move a vista. Se escolles aquí o movemento doutro eixe, intercámbianse e ese movemento deixa de mover a vista.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbitar á esquerda / dereita",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbitar arriba / abaixo",
|
||||
"ui.3d_mouse_view_pan_sideways": "Desprazar á esquerda / dereita",
|
||||
"ui.3d_mouse_view_pan_up_down": "Desprazar arriba / abaixo",
|
||||
"ui.3d_mouse_view_roll": "Xirar sobre a vista",
|
||||
"ui.3d_mouse_view_unmapped": "Eixe {n}",
|
||||
"ui.3d_mouse_view_zoom": "Zoom",
|
||||
"ui.3d_mouse_waiting": "Agardando movemento",
|
||||
"ui.3d_mouse_wiggle": "Se tes un conectado, móveo un pouco.",
|
||||
"ui.3d_sketch": "Bosquexo 3D",
|
||||
"ui.activate": "Activar",
|
||||
"ui.active_component": "Compoñente activo",
|
||||
@@ -1183,6 +1294,7 @@
|
||||
"ui.agent_chat_files_left_out": "Cunha mensaxe van como máximo {max} ficheiros; deixados fóra: {files}",
|
||||
"ui.agent_chat_files_loading": "Agardando a que se lean os ficheiros",
|
||||
"ui.agent_chat_files_max": "Como máximo {max} ficheiros por mensaxe",
|
||||
"ui.agent_chat_flatpak_none_found": "Non se atopou ningún axente de programación. De forma predeterminada, o illamento de Flatpak non ve os axentes instalados no teu cartafol persoal e non ten acceso á rede. Para permitir Claude Code e o seu adaptador ACP instalados con nvm ou en ~/.local/bin, executa esta orde nun terminal e despois reinicia SolidCraft. Concede acceso aos programas e ficheiros de inicio de sesión indicados, ademais de acceso á rede:",
|
||||
"ui.agent_chat_import_failed": "{name}: fallou a importación: {error}",
|
||||
"ui.agent_chat_intro": "Conversa cun axente de programación que xa uses, ou cun punto de acceso de modelo que escollas. Traballa no deseño aberto a través das ferramentas de SolidCraft: os seus cambios vense mentres os fai e desfanse nun paso por mensaxe. O que pode facer sen preguntar defínese en Preferencias > IA.",
|
||||
"ui.agent_chat_key_in_env": "clave configurada",
|
||||
@@ -1278,6 +1390,7 @@
|
||||
"ui.ai_http_busy": "o punto de acceso está ocupado ou superou o seu límite; téntao de novo máis tarde",
|
||||
"ui.ai_http_key": "o punto de acceso rexeitou a clave: compróbaa en Preferencias > IA",
|
||||
"ui.ai_http_not_found": "non existe ese punto de acceso ou modelo: comproba o URL base e o nome do modelo",
|
||||
"ui.ai_http_too_large": "a conversa é demasiado grande para este punto de acceso; comeza un chat novo",
|
||||
"ui.ai_key": "Clave da API",
|
||||
"ui.ai_key_hint": "opcional; gárdase no chaveiro do sistema",
|
||||
"ui.ai_key_needs_https": "A clave só se envía por HTTPS ou a este ordenador.",
|
||||
@@ -1305,7 +1418,6 @@
|
||||
"ui.ai_permissions": "Permisos",
|
||||
"ui.ai_preset": "Axuste predefinido",
|
||||
"ui.ai_remove": "Quitar",
|
||||
"ui.ai_request_limit": "detívose tras 24 solicitudes para unha mensaxe; envía outra mensaxe para continuar",
|
||||
"ui.ai_reset": "Restablecer",
|
||||
"ui.ai_settings": "Axustes de IA…",
|
||||
"ui.ai_show_features": "Mostrar as funcións de IA",
|
||||
@@ -1350,6 +1462,7 @@
|
||||
"ui.blocked": "Bloqueado",
|
||||
"ui.bodies": "Corpos",
|
||||
"ui.bodies_all_if_none": "Corpos (todos se non hai ningún)",
|
||||
"ui.bottom": "Inferior",
|
||||
"ui.box": "Caixa",
|
||||
"ui.break": "Romper",
|
||||
"ui.browser_bodies_one": "{count} corpo",
|
||||
@@ -1481,6 +1594,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtrar ordes ou teclas",
|
||||
"ui.findInTimeline": "Buscar na liña de tempo",
|
||||
"ui.findInWindow": "Buscar na xanela",
|
||||
"ui.fine": "Fina",
|
||||
"ui.finest": "Moi fina",
|
||||
"ui.finish_sketch": "Rematar o bosquexo",
|
||||
"ui.fit": "Axustar",
|
||||
"ui.fix_unfix": "Fixar/Liberar",
|
||||
@@ -1502,6 +1617,7 @@
|
||||
"ui.grid_follows_view_hint": "As vistas laterais mostran a grella no plano vertical que ten diante en vez do chan",
|
||||
"ui.ground_shadow": "Sombra no chan",
|
||||
"ui.groupSelected": "Agrupar a selección",
|
||||
"ui.hatch": "Raiado",
|
||||
"ui.heading_add_ins": "COMPLEMENTOS",
|
||||
"ui.heading_assemble": "ENSAMBLAR",
|
||||
"ui.heading_box": "CAIXA",
|
||||
@@ -1568,6 +1684,7 @@
|
||||
"ui.heading_torus": "TORO",
|
||||
"ui.heading_utilities": "UTILIDADES",
|
||||
"ui.heading_utility": "UTILIDADE",
|
||||
"ui.heading_weldments": "SOLDADURA",
|
||||
"ui.help": "Axuda",
|
||||
"ui.hide": "Ocultar",
|
||||
"ui.hideAxes": "Ocultar os eixes",
|
||||
@@ -1704,6 +1821,7 @@
|
||||
"ui.project_face_edges_sketch": "Proxectar as arestas da cara en bosquexos novos",
|
||||
"ui.properties": "Propiedades",
|
||||
"ui.pull_direction": "Dirección de extracción",
|
||||
"ui.quality": "Calidade",
|
||||
"ui.quantity": "Cantidade",
|
||||
"ui.quantity_2": "Cantidade 2",
|
||||
"ui.quit": "Saír",
|
||||
@@ -1792,65 +1910,13 @@
|
||||
"ui.smooth_wheel_zoom": "Zoom suave coa roda",
|
||||
"ui.smooth_wheel_zoom_tip": "Desactívao para aplicar cada movemento da roda de inmediato, sen suavizado",
|
||||
"ui.snap": "Axuste",
|
||||
"ui.space_mouse_axes_reassigned": "Os eixes están reasignados en Preferencias, e iso segue aplicándose sobre esta calibración.",
|
||||
"ui.space_mouse_calibrate": "Calibrar…",
|
||||
"ui.space_mouse_calibrate_tip": "Move o capuchón como pide a xanela para que SolidCraft aprenda cara a onde e ata onde vai cada eixe deste rato 3D. Gárdase para este rato 3D.",
|
||||
"ui.space_mouse_calibrate_title": "Calibrar o rato 3D",
|
||||
"ui.space_mouse_calibrated": "Calibrado",
|
||||
"ui.space_mouse_calibration": "Calibración do rato 3D",
|
||||
"ui.space_mouse_calibration_done": "Feito. Gárdaa para usala sempre que este rato 3D estea conectado.",
|
||||
"ui.space_mouse_calibration_no_response": "Un eixe case non se moveu, así que non se pode calibrar. Volve comezar e move o capuchón máis lonxe cada vez.",
|
||||
"ui.space_mouse_dead_zone": "Zona morta",
|
||||
"ui.space_mouse_dead_zone_tip": "Canto ten que moverse o capuchón antes de que se mova a vista, como parte do seu percorrido completo. Auméntaa se a vista se despraza co capuchón en repouso.",
|
||||
"ui.space_mouse_driven_by": "Que movemento calibrado do dispositivo controla este eixe. Se escolles un que xa usa outro eixe, intercámbianse.",
|
||||
"ui.space_mouse_driven_by_label": "controlado por",
|
||||
"ui.space_mouse_driver": "Controlador do rato 3D",
|
||||
"ui.space_mouse_driver_auto": "Automático",
|
||||
"ui.space_mouse_driver_direct": "Directo (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Lectura directa do dispositivo; spacenavd non está en execución",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Lectura mediante spacenavd, a única opción dispoñible",
|
||||
"ui.space_mouse_forget": "Esquecer a calibración",
|
||||
"ui.space_mouse_hint": "Move e rota a vista á vez; os bosquexos só usan desprazamento e zoom.",
|
||||
"ui.space_mouse_motion_rotation_x": "Rotación X",
|
||||
"ui.space_mouse_motion_rotation_y": "Rotación Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Rotación Z",
|
||||
"ui.space_mouse_motion_translation_x": "Translación X",
|
||||
"ui.space_mouse_motion_translation_y": "Translación Y",
|
||||
"ui.space_mouse_motion_translation_z": "Translación Z",
|
||||
"ui.space_mouse_navigate": "Navegar con SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Non se atopou ningún rato 3D",
|
||||
"ui.space_mouse_not_calibrated": "Sen calibrar",
|
||||
"ui.space_mouse_reading": "Lectura",
|
||||
"ui.space_mouse_reset_axes": "Restablecer asignación de eixes",
|
||||
"ui.space_mouse_step_lift": "Levanta o capuchón, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_press": "Preme o capuchón cara abaixo, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_pull": "Tira do capuchón cara a ti, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_push": "Empurra o capuchón lonxe de ti, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_rest": "Solta o capuchón e déixao quieto.",
|
||||
"ui.space_mouse_step_slide_left": "Despraza o capuchón á esquerda, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_slide_right": "Despraza o capuchón á dereita, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_tilt_back": "Inclina o capuchón cara atrás (bordo dianteiro arriba), mantéño e sóltao.",
|
||||
"ui.space_mouse_step_tilt_forward": "Inclina o capuchón cara adiante (bordo dianteiro abaixo), mantéño e sóltao.",
|
||||
"ui.space_mouse_step_tilt_left": "Inclina o capuchón á esquerda (bordo esquerdo abaixo), mantéño e sóltao.",
|
||||
"ui.space_mouse_step_tilt_right": "Inclina o capuchón á dereita (bordo dereito abaixo), mantéño e sóltao.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Xira o capuchón no sentido horario, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Xira o capuchón no sentido antihorario, mantéño e sóltao.",
|
||||
"ui.space_mouse_view_axis": "Eixe da vista",
|
||||
"ui.space_mouse_view_off": "Desactivado",
|
||||
"ui.space_mouse_view_off_tip": "O perfil do controlador do rato 3D desactiva este eixe, así que o seu movemento non move a vista. Se escolles aquí o movemento doutro eixe, intercámbianse e ese movemento deixa de mover a vista.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Orbitar á esquerda / dereita",
|
||||
"ui.space_mouse_view_orbit_up_down": "Orbitar arriba / abaixo",
|
||||
"ui.space_mouse_view_pan_sideways": "Desprazar á esquerda / dereita",
|
||||
"ui.space_mouse_view_pan_up_down": "Desprazar arriba / abaixo",
|
||||
"ui.space_mouse_view_roll": "Xirar sobre a vista",
|
||||
"ui.space_mouse_view_unmapped": "Eixe {n}",
|
||||
"ui.space_mouse_view_zoom": "Zoom",
|
||||
"ui.spacing": "Espazado",
|
||||
"ui.spacing_2": "Espazado 2",
|
||||
"ui.sphere": "Esfera",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Dividir o corpo",
|
||||
"ui.stand_up_tip": "Despois de abrir un ficheiro STEP ou IGES con Z cara arriba, ofrecer un cuarto de volta arredor de X",
|
||||
"ui.standard": "Estándar",
|
||||
"ui.start": "Inicio",
|
||||
"ui.start_over": "Volver comezar",
|
||||
"ui.start_page": "Páxina de inicio",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Válasszon…",
|
||||
"Circular": "Kör alakú",
|
||||
"Circumference": "Kerület",
|
||||
"Clockwise": "Óramutató járásával megegyező",
|
||||
"Close draft": "Vázlat bezárása",
|
||||
"Collisions": "Ütközések",
|
||||
"Colour": "Szín",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Parancsok keresése",
|
||||
"Search this library": "Keresés ebben a könyvtárban",
|
||||
"Section": "Metszet",
|
||||
"Section position": "Szelvény helyzete",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Más kijelölése: tartsa nyomva a bal gombot egy helyen a modellen (vagy jobb kattintás › Más kijelölése…) a kurzor alatti elemek listázásához; kattintson újra ugyanarra a helyre a következő elem kijelöléséhez. A középső gomb dupla kattintása a kezdőnézetre vált (Beállítások › Navigáció).",
|
||||
"Select bodies (a face picks its body)": "Testek kijelölése (egy lap a testét jelöli ki)",
|
||||
"Select bodies or faces instead": "Inkább testek vagy lapok kijelölése",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Ügynök kurzorának megjelenítése",
|
||||
"Show in folder": "Megjelenítés a mappában",
|
||||
"Show the pointer an AI agent moves (#35)": "Az AI-ügynök által mozgatott mutató megjelenítése (#35)",
|
||||
"Show tool at cursor": "Eszköz megjelenítése a kurzornál",
|
||||
"Signed axes": "Előjeles tengelyek",
|
||||
"Size": "Méret",
|
||||
"Sketch constraints": "Vázlatkényszerek",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "A SolidCraft bezárult, mielőtt ezek a tervek mentve lettek volna. Az utolsó automatikus mentésük megnyitható.",
|
||||
"Specialise": "Specializálás",
|
||||
"Speed": "Sebesség",
|
||||
"Square": "Négyzet",
|
||||
"Stand it up": "Felállítás",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Felállítás: negyedfordulat az X körül, egyetlen visszavonható Mozgatás elemként.",
|
||||
"Stand this part up?": "Felállítja ezt az alkatrészt?",
|
||||
"Start angle": "Kezdőszög",
|
||||
"Start bulge": "Kezdő ívelés",
|
||||
"Start continuity": "Kezdő folytonosság",
|
||||
"Step": "Lépés",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "szög, magasság",
|
||||
"appearance.dielectric_ior": "Dielektromos törésmutató",
|
||||
"appearance.metallic": "Fémes",
|
||||
"appearance.only_available": "Csak elérhető megjelenések",
|
||||
"appearance.pbr_help": "Tekintse meg ezt a felületet Renderelésben vagy termék-PNG-ben. Nincs fénytörés vagy áteresztés.",
|
||||
"appearance.pbr_opaque": "Átlátszatlan PBR",
|
||||
"appearance.roughness": "Érdesség",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "STEP beszúrása",
|
||||
"cmd.file_save_mesh": "Mentés hálóként",
|
||||
"cmd.file_slicers": "Telepített szeletelők",
|
||||
"cmd.frame_member": "Szerkezeti elem",
|
||||
"cmd.frame_profile_insert": "Profil beszúrása",
|
||||
"cmd.frame_profile_list": "Profilkönyvtár",
|
||||
"cmd.frame_profile_properties": "Profil tulajdonságai",
|
||||
"cmd.inspect_accessibility": "Hozzáférhetőség-elemzés",
|
||||
"cmd.inspect_center_of_mass": "Tömegközéppont",
|
||||
"cmd.inspect_clearance": "Hézag",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Új fizikai anyag",
|
||||
"cmd.material_delete": "Fizikai anyag törlése",
|
||||
"cmd.material_edit": "Fizikai anyag szerkesztése",
|
||||
"cmd.material_inspect": "Fizikai anyag vizsgálata",
|
||||
"cmd.material_list": "Anyagok listázása",
|
||||
"cmd.material_properties_directional": "Irányfüggő rugalmassági modulus",
|
||||
"cmd.material_properties_validate": "Fizikai anyagadatok ellenőrzése",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Kulcs ide",
|
||||
"motion.key_here_hint": "Kulcs hozzáadása ennél a lépésnél a csukló jelenlegi értékével",
|
||||
"motion.no_collisions": "Nincs ütközés a vizsgálat során",
|
||||
"motion.plot_quantity": "Mennyiség",
|
||||
"motion.step_back": "Lépés vissza",
|
||||
"motion.step_forward": "Lépés tovább",
|
||||
"motion.to_end": "A végére",
|
||||
@@ -1059,7 +1071,7 @@
|
||||
"search.try": "Írja be egy parancs nevét, a terv egy elemét vagy egy paramétert. Parancs értékekkel való futtatásához írja be az azonosítóját és egy JSON-objektumot: {example}",
|
||||
"search.valid": "Érvényes",
|
||||
"settings.display_quality_note": "A finomabb szintek simábban rajzolják az ívelt lapokat és éleket, de tovább tart a hálózás. Csak a nézetablak változik, a terv és az exportok soha.",
|
||||
"settings.frame_rate_note": "Megjeleníti a másodpercenkénti képkockaszámot a nézet jobb alsó sarkában. Bekapcsolt állapotban a nézet folyamatosan újrarajzolódik.",
|
||||
"settings.frame_rate_note": "Megjeleníti a másodpercenkénti képkockaszámot a nézet jobb alsó sarkában.",
|
||||
"settings.measure_axis_distances": "X/Y/Z távolságok megjelenítése",
|
||||
"settings.measure_axis_distances_help": "A világtengely-összetevők méreteit is megjeleníti a teljes távolság mellett, amíg a Mérés nyitva van.",
|
||||
"settings.msaa_applies_on_restart": "Az élsimítás a SolidCraft következő indításakor lép érvénybe",
|
||||
@@ -1120,7 +1132,50 @@
|
||||
"trace.stopped_by_contact": "Vége a(z) {sample}. mintánál: {a} áthatolna ezen: {b}",
|
||||
"trace.stopped_by_loop": "Vége a(z) {sample}. mintánál: a(z) {joint} csuklón átmenő hurok nem zárható tovább",
|
||||
"ui.1_degree_of_freedom": "1 szabadságfok",
|
||||
"ui.3d_mouse_calibrate": "Kalibrálás…",
|
||||
"ui.3d_mouse_calibrate_tip": "Mozgassa a kupakot az ablak kérése szerint, hogy a SolidCraft megtanulja, merre és mennyire mozdul a 3D-egér mindegyik tengelye. Ehhez a 3D-egérhez mentve.",
|
||||
"ui.3d_mouse_calibrate_title": "3D-egér kalibrálása",
|
||||
"ui.3d_mouse_calibrate_wait": "Mozdítsa meg egyszer a 3D-egeret, hogy a SolidCraft lássa.",
|
||||
"ui.3d_mouse_calibrated": "Kalibrálva",
|
||||
"ui.3d_mouse_calibration": "3D-egér kalibrálása",
|
||||
"ui.3d_mouse_calibration_done": "Kész. Mentse, hogy a 3D-egér csatlakoztatásakor mindig használatos legyen.",
|
||||
"ui.3d_mouse_calibration_no_response": "Az egyik tengely alig mozdult, így nem kalibrálható. Kezdje elölről, és minden alkalommal mozgassa a kupakot messzebb.",
|
||||
"ui.3d_mouse_circles_help": "A körök azt mutatják, mit küld az eszköz kalibrálás után. A tesztmodell azt mutatja, mit tesz ezzel a nézet a megfordítás, sebesség és tengelykiosztás után.",
|
||||
"ui.3d_mouse_dead_zone": "Holtsáv",
|
||||
"ui.3d_mouse_dead_zone_tip": "Mennyit kell mozdítani a kupakot, mielőtt a nézet mozdulna, a teljes elmozdulás arányában. Növelje, ha a nézet sodródik, miközben a kupak nyugalomban van.",
|
||||
"ui.3d_mouse_denied": "A SolidCraft látja a 3D-egeret, de nem olvashatja. Az udev-szabályt a 3D-egér beállítási jegyzetei tartalmazzák.",
|
||||
"ui.3d_mouse_denied_short": "Megvan, de nem olvasható",
|
||||
"ui.3d_mouse_driver_fallback": "Egyelőre a spacenavd-n át olvassa: magát az eszközt nem sikerült megnyitni.",
|
||||
"ui.3d_mouse_forget": "Kalibrálás elfelejtése",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Bal nézet",
|
||||
"ui.3d_mouse_ghost_left_window": "Bal ablak",
|
||||
"ui.3d_mouse_ghost_position": "Helyzet",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Jobb nézet",
|
||||
"ui.3d_mouse_hint": "A nézetet együtt mozgatja és forgatja; a vázlatok csak pásztázást és nagyítást használnak.",
|
||||
"ui.3d_mouse_navigate": "Navigálás a 3D-egérrel",
|
||||
"ui.3d_mouse_none": "Nem található 3D-egér",
|
||||
"ui.3d_mouse_not_calibrated": "Nincs kalibrálva",
|
||||
"ui.3d_mouse_offer": "A 3D-egér kalibrálása kijavítja a felcserélt vagy megfordított tengelyeket, és beállítja a sebességét és az érzetét.",
|
||||
"ui.3d_mouse_offer_never": "Ne jelenjen meg többé",
|
||||
"ui.3d_mouse_offer_swap": "Váltás Hidrawra (közvetlen illesztőprogram)",
|
||||
"ui.3d_mouse_reading": "Beolvasás",
|
||||
"ui.3d_mouse_reset_view": "Nézet alaphelyzetbe állítása",
|
||||
"ui.3d_mouse_restore": "3D-egér alapértékeinek visszaállítása",
|
||||
"ui.3d_mouse_restore_ask": "Visszaállítja a 3D-egér beállításait az alapértékekre? A kalibrálás és a gombkiosztás megmarad.",
|
||||
"ui.3d_mouse_speed": "3D-egér sebessége",
|
||||
"ui.3d_mouse_step_lift": "Emelje fel a kupakot, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_press": "Nyomja le a kupakot, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_pull": "Húzza a kupakot maga felé, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_push": "Tolja a kupakot magától el, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_rest": "Vegye le a kezét a kupakról, és hagyja nyugodtan.",
|
||||
"ui.3d_mouse_step_slide_left": "Csúsztassa a kupakot balra, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_slide_right": "Csúsztassa a kupakot jobbra, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_tilt_left": "Billentse balra a kupakot (a bal él lefelé), tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_tilt_right": "Billentse jobbra a kupakot (a jobb él lefelé), tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Csavarja el a kupakot az óramutató járásával megegyezően, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Csavarja el a kupakot az óramutató járásával ellentétesen, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_waiting": "Mozgásra vár",
|
||||
"ui.3d_mouse_wiggle": "Ha van csatlakoztatva egy, mozgassa meg egy kicsit.",
|
||||
"ui.3d_sketch": "3D vázlat",
|
||||
"ui.activate": "Aktiválás",
|
||||
"ui.active_component": "Aktív komponens",
|
||||
@@ -1182,6 +1237,7 @@
|
||||
"ui.agent_chat_files_left_out": "Egy üzenethez legfeljebb {max} fájl tartozhat; kihagyva: {files}",
|
||||
"ui.agent_chat_files_loading": "Várakozás a fájlok beolvasására",
|
||||
"ui.agent_chat_files_max": "Egy üzenetben legfeljebb {max} fájl lehet",
|
||||
"ui.agent_chat_flatpak_none_found": "Nem található kódolóügynök. A Flatpak homokozó alapértelmezés szerint nem látja a saját mappájába telepített ügynököket, és nincs hálózati hozzáférése. Az nvm-mel vagy a ~/.local/bin mappába telepített Claude Code és ACP-adaptere engedélyezéséhez futtassa ezt a parancsot egy terminálban, majd indítsa újra a SolidCraftot. Hozzáférést ad a felsorolt programokhoz és bejelentkezési fájlokhoz, valamint a hálózathoz:",
|
||||
"ui.agent_chat_import_failed": "{name}: az importálás sikertelen: {error}",
|
||||
"ui.agent_chat_intro": "Csevegjen egy már használt kódolóügynökkel vagy egy választott modellvégponttal. A megnyitott terven a SolidCraft eszközein keresztül dolgozik: a módosításai láthatók közben, és üzenetenként egy lépésben vonhatók vissza. Hogy mit tehet kérdés nélkül, azt a Beállítások > AI menüpontban adhatja meg.",
|
||||
"ui.agent_chat_key_in_env": "kulcs beállítva",
|
||||
@@ -1277,6 +1333,7 @@
|
||||
"ui.ai_http_busy": "a végpont foglalt vagy túllépte a korlátját; próbálja később",
|
||||
"ui.ai_http_key": "a végpont elutasította a kulcsot: ellenőrizze a Beállítások > AI menüben",
|
||||
"ui.ai_http_not_found": "nincs ilyen végpont vagy modell: ellenőrizze az alap-URL-t és a modell nevét",
|
||||
"ui.ai_http_too_large": "a beszélgetés túl nagy ehhez a végponthoz; kezdjen új csevegést",
|
||||
"ui.ai_key": "API-kulcs",
|
||||
"ui.ai_key_hint": "opcionális; a rendszer kulcstartójában tárolódik",
|
||||
"ui.ai_key_needs_https": "A kulcs csak HTTPS-en vagy erre a számítógépre kerül elküldésre.",
|
||||
@@ -1304,7 +1361,6 @@
|
||||
"ui.ai_permissions": "Jogosultságok",
|
||||
"ui.ai_preset": "Előbeállítás",
|
||||
"ui.ai_remove": "Eltávolítás",
|
||||
"ui.ai_request_limit": "egy üzenetre eső 24 kérés után leállt; küldjön újabb üzenetet a folytatáshoz",
|
||||
"ui.ai_reset": "Visszaállítás",
|
||||
"ui.ai_settings": "AI-beállítások…",
|
||||
"ui.ai_show_features": "AI-funkciók megjelenítése",
|
||||
@@ -1480,6 +1536,8 @@
|
||||
"ui.filter_commands_or_keys": "Parancsok vagy billentyűk szűrése",
|
||||
"ui.findInTimeline": "Keresés az idővonalon",
|
||||
"ui.findInWindow": "Keresés az ablakban",
|
||||
"ui.fine": "Finom",
|
||||
"ui.finest": "Legfinomabb",
|
||||
"ui.finish_sketch": "Vázlat befejezése",
|
||||
"ui.fit": "Illesztés",
|
||||
"ui.fix_unfix": "Rögzítés/feloldás",
|
||||
@@ -1501,6 +1559,7 @@
|
||||
"ui.grid_follows_view_hint": "Az oldalnézetek a talaj helyett a feléjük néző függőleges síkon mutatják a rácsot",
|
||||
"ui.ground_shadow": "Talajárnyék",
|
||||
"ui.groupSelected": "Kijelöltek csoportosítása",
|
||||
"ui.hatch": "Sraffozás",
|
||||
"ui.heading_add_ins": "BŐVÍTMÉNYEK",
|
||||
"ui.heading_assemble": "ÖSSZESZERELÉS",
|
||||
"ui.heading_box": "DOBOZ",
|
||||
@@ -1567,6 +1626,7 @@
|
||||
"ui.heading_torus": "TÓRUSZ",
|
||||
"ui.heading_utilities": "SEGÉDESZKÖZÖK",
|
||||
"ui.heading_utility": "SEGÉDESZKÖZ",
|
||||
"ui.heading_weldments": "HEGESZTÉS",
|
||||
"ui.help": "Súgó",
|
||||
"ui.hide": "Elrejtés",
|
||||
"ui.hideAxes": "Tengelyek elrejtése",
|
||||
@@ -1703,6 +1763,7 @@
|
||||
"ui.project_face_edges_sketch": "Lapélek vetítése új vázlatokba",
|
||||
"ui.properties": "Tulajdonságok",
|
||||
"ui.pull_direction": "Húzási irány",
|
||||
"ui.quality": "Minőség",
|
||||
"ui.quantity": "Mennyiség",
|
||||
"ui.quantity_2": "2. mennyiség",
|
||||
"ui.quit": "Kilépés",
|
||||
@@ -1791,40 +1852,13 @@
|
||||
"ui.smooth_wheel_zoom": "Sima görgőnagyítás",
|
||||
"ui.smooth_wheel_zoom_tip": "Kapcsolja ki az egyes görgőesemények azonnali alkalmazásához, kifutás nélkül",
|
||||
"ui.snap": "Illesztés",
|
||||
"ui.space_mouse_calibrate": "Kalibrálás…",
|
||||
"ui.space_mouse_calibrate_tip": "Mozgassa a kupakot az ablak kérése szerint, hogy a SolidCraft megtanulja, merre és mennyire mozdul a 3D-egér mindegyik tengelye. Ehhez a 3D-egérhez mentve.",
|
||||
"ui.space_mouse_calibrate_title": "3D-egér kalibrálása",
|
||||
"ui.space_mouse_calibrated": "Kalibrálva",
|
||||
"ui.space_mouse_calibration": "3D-egér kalibrálása",
|
||||
"ui.space_mouse_calibration_done": "Kész. Mentse, hogy a 3D-egér csatlakoztatásakor mindig használatos legyen.",
|
||||
"ui.space_mouse_calibration_no_response": "Az egyik tengely alig mozdult, így nem kalibrálható. Kezdje elölről, és minden alkalommal mozgassa a kupakot messzebb.",
|
||||
"ui.space_mouse_dead_zone": "Holtsáv",
|
||||
"ui.space_mouse_dead_zone_tip": "Mennyit kell mozdítani a kupakot, mielőtt a nézet mozdulna, a teljes elmozdulás arányában. Növelje, ha a nézet sodródik, miközben a kupak nyugalomban van.",
|
||||
"ui.space_mouse_forget": "Kalibrálás elfelejtése",
|
||||
"ui.space_mouse_hint": "A nézetet együtt mozgatja és forgatja; a vázlatok csak pásztázást és nagyítást használnak.",
|
||||
"ui.space_mouse_navigate": "Navigálás SpaceNavigator / SpaceMouse eszközzel",
|
||||
"ui.space_mouse_none": "Nem található 3D-egér",
|
||||
"ui.space_mouse_not_calibrated": "Nincs kalibrálva",
|
||||
"ui.space_mouse_reading": "Beolvasás",
|
||||
"ui.space_mouse_step_lift": "Emelje fel a kupakot, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_press": "Nyomja le a kupakot, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_pull": "Húzza a kupakot maga felé, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_push": "Tolja a kupakot magától el, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_rest": "Vegye le a kezét a kupakról, és hagyja nyugodtan.",
|
||||
"ui.space_mouse_step_slide_left": "Csúsztassa a kupakot balra, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_slide_right": "Csúsztassa a kupakot jobbra, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_tilt_back": "Billentse hátra a kupakot (az elülső él felfelé), tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_tilt_forward": "Billentse előre a kupakot (az elülső él lefelé), tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_tilt_left": "Billentse balra a kupakot (a bal él lefelé), tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_tilt_right": "Billentse jobbra a kupakot (a jobb él lefelé), tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Csavarja el a kupakot az óramutató járásával megegyezően, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Csavarja el a kupakot az óramutató járásával ellentétesen, tartsa, majd engedje el.",
|
||||
"ui.spacing": "Térköz",
|
||||
"ui.spacing_2": "2. térköz",
|
||||
"ui.sphere": "Gömb",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Test felosztása",
|
||||
"ui.stand_up_tip": "Miután egy STEP vagy IGES fájl Z felfelé tájolással megnyílik, negyedfordulat felajánlása az X körül",
|
||||
"ui.standard": "Normál",
|
||||
"ui.start": "Kezdés",
|
||||
"ui.start_over": "Újrakezdés",
|
||||
"ui.start_page": "Kezdőoldal",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Pilih…",
|
||||
"Circular": "Melingkar",
|
||||
"Circumference": "Keliling",
|
||||
"Clockwise": "Searah jarum jam",
|
||||
"Close draft": "Tutup draf",
|
||||
"Collisions": "Tabrakan",
|
||||
"Colour": "Warna",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Cari perintah",
|
||||
"Search this library": "Cari di pustaka ini",
|
||||
"Section": "Penampang",
|
||||
"Section position": "Posisi penampang",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Select Other: tahan tombol kiri diam di model (atau klik kanan › Select Other…) untuk mendaftar semua yang ada di bawah kursor; klik lagi di titik yang sama untuk memilih benda berikutnya. Klik dua kali tombol tengah untuk ke Home (Preferensi › Navigasi).",
|
||||
"Select bodies (a face picks its body)": "Pilih bodi (memilih muka berarti memilih bodinya)",
|
||||
"Select bodies or faces instead": "Pilih bodi atau muka saja",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Tampilkan Kursor Agen",
|
||||
"Show in folder": "Tampilkan di folder",
|
||||
"Show the pointer an AI agent moves (#35)": "Tampilkan penunjuk yang digerakkan agen AI (#35)",
|
||||
"Show tool at cursor": "Tampilkan alat di dekat kursor",
|
||||
"Signed axes": "Sumbu bertanda",
|
||||
"Size": "Ukuran",
|
||||
"Sketch constraints": "Batasan sketsa",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft tertutup sebelum desain ini disimpan. Simpan otomatis terakhirnya dapat dibuka.",
|
||||
"Specialise": "Spesialisasikan",
|
||||
"Speed": "Kecepatan",
|
||||
"Square": "Persegi",
|
||||
"Stand it up": "Tegakkan",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Tegakkan: putaran seperempat terhadap X, sebagai satu fitur Move yang dapat dibatalkan.",
|
||||
"Stand this part up?": "Tegakkan part ini?",
|
||||
"Start angle": "Sudut awal",
|
||||
"Start bulge": "Bulge awal",
|
||||
"Start continuity": "Kontinuitas awal",
|
||||
"Step": "Langkah",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "sudut, tinggi",
|
||||
"appearance.dielectric_ior": "IOR Dielektrik",
|
||||
"appearance.metallic": "Metalik",
|
||||
"appearance.only_available": "Hanya tampilan yang tersedia",
|
||||
"appearance.pbr_help": "Lihat permukaan ini di Render atau PNG produk. Tanpa refraksi atau transmisi.",
|
||||
"appearance.pbr_opaque": "PBR Opak",
|
||||
"appearance.roughness": "Kekasaran",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "Sisipkan STEP",
|
||||
"cmd.file_save_mesh": "Simpan Sebagai Mesh",
|
||||
"cmd.file_slicers": "Slicer Terpasang",
|
||||
"cmd.frame_member": "Batang Rangka",
|
||||
"cmd.frame_profile_insert": "Sisipkan Profil",
|
||||
"cmd.frame_profile_list": "Pustaka Profil",
|
||||
"cmd.frame_profile_properties": "Properti Profil",
|
||||
"cmd.inspect_accessibility": "Analisis Aksesibilitas",
|
||||
"cmd.inspect_center_of_mass": "Pusat Massa",
|
||||
"cmd.inspect_clearance": "Kelonggaran",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Material Fisik Baru",
|
||||
"cmd.material_delete": "Hapus Material Fisik",
|
||||
"cmd.material_edit": "Edit Material Fisik",
|
||||
"cmd.material_inspect": "Periksa Material Fisik",
|
||||
"cmd.material_list": "Daftar Material",
|
||||
"cmd.material_properties_directional": "Modulus Elastis Arah",
|
||||
"cmd.material_properties_validate": "Validasi Data Material Fisik",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Key di sini",
|
||||
"motion.key_here_hint": "Tambah key pada langkah ini dengan nilai sambungan saat ini",
|
||||
"motion.no_collisions": "Tidak ada tabrakan sepanjang studi",
|
||||
"motion.plot_quantity": "Jumlah",
|
||||
"motion.step_back": "Mundur satu langkah",
|
||||
"motion.step_forward": "Maju satu langkah",
|
||||
"motion.to_end": "Ke akhir",
|
||||
@@ -1059,7 +1071,7 @@
|
||||
"search.try": "Ketik nama perintah, fitur di desain ini, atau parameter. Untuk menjalankan perintah dengan nilai, ketik id-nya dan objek JSON: {example}",
|
||||
"search.valid": "Valid",
|
||||
"settings.display_quality_note": "Level lebih halus menggambar muka dan tepi lengkung lebih mulus dan butuh waktu meshing lebih lama. Hanya viewport yang berubah, tidak pernah desain atau ekspor.",
|
||||
"settings.frame_rate_note": "Menampilkan frame per detik di sudut kanan bawah tampilan. Saat aktif, tampilan digambar ulang terus-menerus.",
|
||||
"settings.frame_rate_note": "Menampilkan frame per detik di sudut kanan bawah tampilan.",
|
||||
"settings.measure_axis_distances": "Tampilkan jarak X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Tampilkan dimensi komponen sumbu dunia di samping jarak total saat Ukur terbuka.",
|
||||
"settings.msaa_applies_on_restart": "Anti-aliasing berlaku saat SolidCraft dimulai berikutnya",
|
||||
@@ -1120,7 +1132,50 @@
|
||||
"trace.stopped_by_contact": "Berakhir di sampel {sample}: {a} akan menembus {b}",
|
||||
"trace.stopped_by_loop": "Berakhir di sampel {sample}: loop melalui {joint} tidak dapat menutup lebih jauh",
|
||||
"ui.1_degree_of_freedom": "1 derajat kebebasan",
|
||||
"ui.3d_mouse_calibrate": "Kalibrasi…",
|
||||
"ui.3d_mouse_calibrate_tip": "Gerakkan cap sesuai permintaan jendela, agar SolidCraft mempelajari arah dan jarak setiap sumbu mouse 3D ini. Disimpan untuk mouse 3D ini.",
|
||||
"ui.3d_mouse_calibrate_title": "Kalibrasi mouse 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Gerakkan mouse 3D sekali agar SolidCraft dapat melihatnya.",
|
||||
"ui.3d_mouse_calibrated": "Terkalibrasi",
|
||||
"ui.3d_mouse_calibration": "Kalibrasi mouse 3D",
|
||||
"ui.3d_mouse_calibration_done": "Selesai. Simpan untuk dipakai setiap kali mouse 3D ini terhubung.",
|
||||
"ui.3d_mouse_calibration_no_response": "Satu sumbu hampir tidak bergerak, sehingga tidak dapat dikalibrasi. Mulai ulang dan gerakkan cap lebih jauh setiap kali.",
|
||||
"ui.3d_mouse_circles_help": "Lingkaran menunjukkan apa yang dikirim perangkat setelah kalibrasi. Model uji menunjukkan apa yang dilakukan tampilan dengannya, setelah pembalikan, kecepatan, dan penetapan sumbu.",
|
||||
"ui.3d_mouse_dead_zone": "Zona mati",
|
||||
"ui.3d_mouse_dead_zone_tip": "Seberapa jauh cap harus bergerak sebelum tampilan bergerak, sebagai bagian dari total jangkauannya. Naikkan jika tampilan bergeser saat cap diam.",
|
||||
"ui.3d_mouse_denied": "SolidCraft melihat mouse 3D tetapi tidak diizinkan membacanya. Lihat catatan penyiapan mouse 3D untuk aturan udev.",
|
||||
"ui.3d_mouse_denied_short": "Ditemukan, tetapi tidak bisa dibaca",
|
||||
"ui.3d_mouse_driver_fallback": "Sementara dibaca lewat spacenavd: perangkatnya sendiri tidak dapat dibuka.",
|
||||
"ui.3d_mouse_forget": "Lupakan kalibrasi",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Viewport kiri",
|
||||
"ui.3d_mouse_ghost_left_window": "Jendela kiri",
|
||||
"ui.3d_mouse_ghost_position": "Posisi",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Viewport kanan",
|
||||
"ui.3d_mouse_hint": "Gerakkan dan putar tampilan bersama; sketsa hanya memakai pan dan zoom.",
|
||||
"ui.3d_mouse_navigate": "Navigasi dengan mouse 3D",
|
||||
"ui.3d_mouse_none": "Mouse 3D tidak ditemukan",
|
||||
"ui.3d_mouse_not_calibrated": "Belum dikalibrasi",
|
||||
"ui.3d_mouse_offer": "Mengkalibrasi mouse 3D memperbaiki sumbu yang tertukar atau terbalik dan mengatur kecepatan serta rasanya.",
|
||||
"ui.3d_mouse_offer_never": "Jangan tampilkan lagi",
|
||||
"ui.3d_mouse_offer_swap": "Beralih ke Hidraw (driver langsung)",
|
||||
"ui.3d_mouse_reading": "Pembacaan",
|
||||
"ui.3d_mouse_reset_view": "Atur ulang tampilan",
|
||||
"ui.3d_mouse_restore": "Pulihkan bawaan mouse 3D",
|
||||
"ui.3d_mouse_restore_ask": "Kembalikan pengaturan mouse 3D ke bawaan? Kalibrasi dan pemetaan tombol tetap ada.",
|
||||
"ui.3d_mouse_speed": "Kecepatan mouse 3D",
|
||||
"ui.3d_mouse_step_lift": "Angkat cap ke atas, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_press": "Tekan cap ke bawah, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_pull": "Tarik cap ke arah Anda, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_push": "Dorong cap menjauh dari Anda, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_rest": "Lepaskan tangan dari cap dan biarkan diam.",
|
||||
"ui.3d_mouse_step_slide_left": "Geser cap ke kiri, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_slide_right": "Geser cap ke kanan, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_tilt_left": "Miringkan cap ke kiri (tepi kiri turun), tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_tilt_right": "Miringkan cap ke kanan (tepi kanan turun), tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Putar cap searah jarum jam, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Putar cap berlawanan arah jarum jam, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_waiting": "Menunggu gerakan",
|
||||
"ui.3d_mouse_wiggle": "Jika Anda memasang satu, goyangkan sedikit.",
|
||||
"ui.3d_sketch": "Sketsa 3D",
|
||||
"ui.activate": "Aktifkan",
|
||||
"ui.active_component": "Komponen aktif",
|
||||
@@ -1182,6 +1237,7 @@
|
||||
"ui.agent_chat_files_left_out": "Maksimal {max} file per pesan; tidak disertakan: {files}",
|
||||
"ui.agent_chat_files_loading": "Menunggu file selesai dibaca",
|
||||
"ui.agent_chat_files_max": "Maksimal {max} file per pesan",
|
||||
"ui.agent_chat_flatpak_none_found": "Agen coding tidak ditemukan. Secara bawaan, sandbox Flatpak tidak dapat melihat agen yang terpasang di folder rumah Anda dan tidak memiliki akses jaringan. Untuk mengizinkan Claude Code dan adaptor ACP-nya yang dipasang melalui nvm atau di ~/.local/bin, jalankan perintah ini di terminal, lalu mulai ulang SolidCraft. Perintah ini memberi akses ke program dan berkas masuk yang disebutkan, serta akses jaringan:",
|
||||
"ui.agent_chat_import_failed": "{name}: impor gagal: {error}",
|
||||
"ui.agent_chat_intro": "Mengobrol dengan agen coding yang sudah Anda pakai, atau endpoint model pilihan Anda. Agen bekerja pada desain yang terbuka lewat alat SolidCraft: perubahannya tampil saat dibuat dan dapat diurungkan satu langkah per pesan. Yang boleh dilakukannya tanpa bertanya diatur di Preferensi > AI.",
|
||||
"ui.agent_chat_key_in_env": "key diatur",
|
||||
@@ -1277,6 +1333,7 @@
|
||||
"ui.ai_http_busy": "endpoint sibuk atau melebihi batas; coba lagi nanti",
|
||||
"ui.ai_http_key": "endpoint menolak key: periksa di Preferensi > AI",
|
||||
"ui.ai_http_not_found": "endpoint atau model tidak ada: periksa URL dasar dan nama model",
|
||||
"ui.ai_http_too_large": "percakapan terlalu besar untuk endpoint ini; mulai obrolan baru",
|
||||
"ui.ai_key": "Key API",
|
||||
"ui.ai_key_hint": "opsional; disimpan di keychain sistem",
|
||||
"ui.ai_key_needs_https": "Key hanya dikirim lewat HTTPS, atau ke komputer ini.",
|
||||
@@ -1304,7 +1361,6 @@
|
||||
"ui.ai_permissions": "Izin",
|
||||
"ui.ai_preset": "Preset",
|
||||
"ui.ai_remove": "Hapus",
|
||||
"ui.ai_request_limit": "dihentikan setelah 24 permintaan untuk satu pesan; kirim pesan lain untuk melanjutkan",
|
||||
"ui.ai_reset": "Reset",
|
||||
"ui.ai_settings": "Pengaturan AI…",
|
||||
"ui.ai_show_features": "Tampilkan fitur AI",
|
||||
@@ -1480,6 +1536,8 @@
|
||||
"ui.filter_commands_or_keys": "Filter perintah atau tombol",
|
||||
"ui.findInTimeline": "Cari di Linimasa",
|
||||
"ui.findInWindow": "Cari di Jendela",
|
||||
"ui.fine": "Halus",
|
||||
"ui.finest": "Sangat halus",
|
||||
"ui.finish_sketch": "Selesai Sketsa",
|
||||
"ui.fit": "Pas",
|
||||
"ui.fix_unfix": "Kunci/Lepas Kunci",
|
||||
@@ -1501,6 +1559,7 @@
|
||||
"ui.grid_follows_view_hint": "Tampilan samping menampilkan grid pada bidang tegak yang menghadapnya, bukan pada dasar",
|
||||
"ui.ground_shadow": "Bayangan dasar",
|
||||
"ui.groupSelected": "Kelompokkan yang Dipilih",
|
||||
"ui.hatch": "Arsir",
|
||||
"ui.heading_add_ins": "ADD-IN",
|
||||
"ui.heading_assemble": "RAKIT",
|
||||
"ui.heading_box": "KOTAK",
|
||||
@@ -1567,6 +1626,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "UTILITAS",
|
||||
"ui.heading_utility": "UTILITAS",
|
||||
"ui.heading_weldments": "KONSTRUKSI LAS",
|
||||
"ui.help": "Bantuan",
|
||||
"ui.hide": "Sembunyikan",
|
||||
"ui.hideAxes": "Sembunyikan Sumbu",
|
||||
@@ -1703,6 +1763,7 @@
|
||||
"ui.project_face_edges_sketch": "Proyeksikan tepi muka ke sketsa baru",
|
||||
"ui.properties": "Properti",
|
||||
"ui.pull_direction": "Arah tarik",
|
||||
"ui.quality": "Kualitas",
|
||||
"ui.quantity": "Jumlah",
|
||||
"ui.quantity_2": "Jumlah 2",
|
||||
"ui.quit": "Keluar",
|
||||
@@ -1791,40 +1852,13 @@
|
||||
"ui.smooth_wheel_zoom": "Zoom roda halus",
|
||||
"ui.smooth_wheel_zoom_tip": "Matikan untuk menerapkan setiap peristiwa roda segera, tanpa ekor penghalusan",
|
||||
"ui.snap": "Snap",
|
||||
"ui.space_mouse_calibrate": "Kalibrasi…",
|
||||
"ui.space_mouse_calibrate_tip": "Gerakkan cap sesuai permintaan jendela, agar SolidCraft mempelajari arah dan jarak setiap sumbu mouse 3D ini. Disimpan untuk mouse 3D ini.",
|
||||
"ui.space_mouse_calibrate_title": "Kalibrasi mouse 3D",
|
||||
"ui.space_mouse_calibrated": "Terkalibrasi",
|
||||
"ui.space_mouse_calibration": "Kalibrasi mouse 3D",
|
||||
"ui.space_mouse_calibration_done": "Selesai. Simpan untuk dipakai setiap kali mouse 3D ini terhubung.",
|
||||
"ui.space_mouse_calibration_no_response": "Satu sumbu hampir tidak bergerak, sehingga tidak dapat dikalibrasi. Mulai ulang dan gerakkan cap lebih jauh setiap kali.",
|
||||
"ui.space_mouse_dead_zone": "Zona mati",
|
||||
"ui.space_mouse_dead_zone_tip": "Seberapa jauh cap harus bergerak sebelum tampilan bergerak, sebagai bagian dari total jangkauannya. Naikkan jika tampilan bergeser saat cap diam.",
|
||||
"ui.space_mouse_forget": "Lupakan kalibrasi",
|
||||
"ui.space_mouse_hint": "Gerakkan dan putar tampilan bersama; sketsa hanya memakai pan dan zoom.",
|
||||
"ui.space_mouse_navigate": "Navigasi dengan SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Mouse 3D tidak ditemukan",
|
||||
"ui.space_mouse_not_calibrated": "Belum dikalibrasi",
|
||||
"ui.space_mouse_reading": "Pembacaan",
|
||||
"ui.space_mouse_step_lift": "Angkat cap ke atas, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_press": "Tekan cap ke bawah, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_pull": "Tarik cap ke arah Anda, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_push": "Dorong cap menjauh dari Anda, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_rest": "Lepaskan tangan dari cap dan biarkan diam.",
|
||||
"ui.space_mouse_step_slide_left": "Geser cap ke kiri, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_slide_right": "Geser cap ke kanan, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_tilt_back": "Miringkan cap ke belakang (tepi depan naik), tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_tilt_forward": "Miringkan cap ke depan (tepi depan turun), tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_tilt_left": "Miringkan cap ke kiri (tepi kiri turun), tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_tilt_right": "Miringkan cap ke kanan (tepi kanan turun), tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Putar cap searah jarum jam, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Putar cap berlawanan arah jarum jam, tahan, lalu lepaskan.",
|
||||
"ui.spacing": "Jarak antar",
|
||||
"ui.spacing_2": "Jarak antar 2",
|
||||
"ui.sphere": "Bola",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Belah Bodi",
|
||||
"ui.stand_up_tip": "Setelah file STEP atau IGES terbuka dengan Z ke atas, tawarkan putaran seperempat terhadap X",
|
||||
"ui.standard": "Standar",
|
||||
"ui.start": "Mulai",
|
||||
"ui.start_over": "Mulai ulang",
|
||||
"ui.start_page": "Halaman awal",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Scegli…",
|
||||
"Circular": "Circolare",
|
||||
"Circumference": "Circonferenza",
|
||||
"Clockwise": "Orario",
|
||||
"Close draft": "Chiudi bozza",
|
||||
"Collisions": "Collisioni",
|
||||
"Colour": "Colore",
|
||||
@@ -163,6 +164,7 @@
|
||||
"From Bodies": "Dai corpi",
|
||||
"From edges": "Spigoli di partenza",
|
||||
"Full Length": "Lunghezza totale",
|
||||
"Full thread depth": "Profondità filetto completa",
|
||||
"Gap": "Gioco",
|
||||
"Grid": "Griglia",
|
||||
"HISTORY": "CRONOLOGIA",
|
||||
@@ -325,6 +327,7 @@
|
||||
"Search commands": "Cerca comandi",
|
||||
"Search this library": "Cerca in questa libreria",
|
||||
"Section": "Sezione",
|
||||
"Section position": "Posizione della sezione",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Seleziona altro: tieni premuto il pulsante sinistro senza muovere il puntatore sul modello (o fai clic destro › Seleziona altro…) per elencare gli oggetti sotto il cursore; fai di nuovo clic nello stesso punto per selezionare l'oggetto successivo. Fai doppio clic con il pulsante centrale per tornare alla vista iniziale (Preferenze › Navigazione).",
|
||||
"Select bodies (a face picks its body)": "Seleziona corpi (una faccia seleziona il corpo a cui appartiene)",
|
||||
"Select bodies or faces instead": "Seleziona invece corpi o facce",
|
||||
@@ -336,6 +339,7 @@
|
||||
"Show Agent Cursor": "Mostra cursore dell'agente",
|
||||
"Show in folder": "Mostra nella cartella",
|
||||
"Show the pointer an AI agent moves (#35)": "Mostra il puntatore mosso da un agente IA (#35)",
|
||||
"Show tool at cursor": "Mostra lo strumento accanto al cursore",
|
||||
"Signed axes": "Assi con segno",
|
||||
"Size": "Dimensioni",
|
||||
"Sketch constraints": "Vincoli dello schizzo",
|
||||
@@ -348,9 +352,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft si è chiuso prima del salvataggio di questi progetti. È possibile aprire il loro ultimo salvataggio automatico.",
|
||||
"Specialise": "Specializza",
|
||||
"Speed": "Velocità",
|
||||
"Square": "Quadrato",
|
||||
"Stand it up": "Raddrizza",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Raddrizza: un quarto di giro attorno a X, come singola lavorazione Sposta annullabile.",
|
||||
"Stand this part up?": "Raddrizzare questa parte?",
|
||||
"Start angle": "Angolo iniziale",
|
||||
"Start bulge": "Rigonfiamento iniziale",
|
||||
"Start continuity": "Continuità iniziale",
|
||||
"Step": "Passo",
|
||||
@@ -377,6 +383,7 @@
|
||||
"This child is shared. Deleting it removes it from every copy of its parent.": "Questo elemento figlio è condiviso. Eliminandolo verrà rimosso da ogni copia del padre.",
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "Questo elemento figlio è condiviso. Le modifiche alla posizione si applicano a ogni copia del padre.",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Questa faccia non consente un collegamento persistente. Scegli un'altra faccia o usa esplicitamente un piano fisso.",
|
||||
"Thread depth": "Profondità filetto",
|
||||
"To Offset": "Fino a una distanza",
|
||||
"To edges": "Spigoli di arrivo",
|
||||
"Tolerance": "Tolleranza",
|
||||
@@ -415,6 +422,7 @@
|
||||
"angle, height": "angolo, altezza",
|
||||
"appearance.dielectric_ior": "Indice di rifrazione dielettrico",
|
||||
"appearance.metallic": "Metallicità",
|
||||
"appearance.only_available": "Solo aspetti disponibili",
|
||||
"appearance.pbr_help": "Visualizza questa superficie in Rendering o in un PNG del prodotto. Senza rifrazione né trasmissione.",
|
||||
"appearance.pbr_opaque": "PBR opaco",
|
||||
"appearance.roughness": "Rugosità",
|
||||
@@ -533,6 +541,10 @@
|
||||
"cmd.file_insert_step": "Inserisci STEP",
|
||||
"cmd.file_save_mesh": "Salva come mesh",
|
||||
"cmd.file_slicers": "Slicer installati",
|
||||
"cmd.frame_member": "Membro strutturale",
|
||||
"cmd.frame_profile_insert": "Inserisci profilo",
|
||||
"cmd.frame_profile_list": "Libreria profili",
|
||||
"cmd.frame_profile_properties": "Proprietà del profilo",
|
||||
"cmd.inspect_accessibility": "Analisi di accessibilità",
|
||||
"cmd.inspect_center_of_mass": "Centro di massa",
|
||||
"cmd.inspect_clearance": "Distanza libera",
|
||||
@@ -572,6 +584,7 @@
|
||||
"cmd.material_create": "Nuovo materiale fisico",
|
||||
"cmd.material_delete": "Elimina materiale fisico",
|
||||
"cmd.material_edit": "Modifica materiale fisico",
|
||||
"cmd.material_inspect": "Ispeziona materiale fisico",
|
||||
"cmd.material_list": "Elenca materiali",
|
||||
"cmd.material_properties_directional": "Modulo elastico direzionale",
|
||||
"cmd.material_properties_validate": "Convalida dati del materiale fisico",
|
||||
@@ -860,6 +873,26 @@
|
||||
"help.mit": "Licenza MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenze di terze parti",
|
||||
"hole_presets.confirm_delete": "Conferma eliminazione",
|
||||
"hole_presets.delete": "Elimina",
|
||||
"hole_presets.delete_prompt": "Eliminare questo preset personale? I fori esistenti restano invariati.",
|
||||
"hole_presets.full": "La raccolta personale contiene già 64 preset.",
|
||||
"hole_presets.invalid_name": "Inserisci un nome breve, senza a capo o caratteri di controllo.",
|
||||
"hole_presets.invalid_parameters": "Completa i parametri del foro con espressioni valide e di lunghezza ragionevole.",
|
||||
"hole_presets.missing": "Il preset selezionato non è più disponibile.",
|
||||
"hole_presets.name": "Nome preset",
|
||||
"hole_presets.name_exists": "Esiste già un preset con questo nome. Usa un nome diverso.",
|
||||
"hole_presets.name_hint": "Un nome univoco",
|
||||
"hole_presets.overwrite": "Salva le modifiche nel preset selezionato",
|
||||
"hole_presets.preset": "Preset",
|
||||
"hole_presets.recall": "Richiama",
|
||||
"hole_presets.rename": "Rinomina",
|
||||
"hole_presets.save": "Salva nuovo",
|
||||
"hole_presets.scope": "Salvati nel tuo profilo per tutti i progetti.",
|
||||
"hole_presets.select": "Seleziona un preset",
|
||||
"hole_presets.title": "Preset personali",
|
||||
"hole_presets.unsupported_options": "Questo preset usa opzioni del filetto che questa versione non può richiamare.",
|
||||
"hole_presets.unsupported_overwrite": "Questo preset contiene opzioni che non possono essere sovrascritte in sicurezza.",
|
||||
"home.back": "Torna a {name}",
|
||||
"home.crashes": "Rapporti di arresto anomalo",
|
||||
"home.design_workspace": "Progettazione",
|
||||
@@ -963,6 +996,7 @@
|
||||
"motion.key_here": "Inserisci fotogramma chiave qui",
|
||||
"motion.key_here_hint": "Aggiungi un fotogramma chiave in questo passo con il valore attuale del giunto",
|
||||
"motion.no_collisions": "Nessuna collisione durante lo studio",
|
||||
"motion.plot_quantity": "Quantità",
|
||||
"motion.step_back": "Passo indietro",
|
||||
"motion.step_forward": "Passo avanti",
|
||||
"motion.to_end": "Alla fine",
|
||||
@@ -988,6 +1022,16 @@
|
||||
"pattern.direction_1": "Direzione 1",
|
||||
"pattern.direction_2": "Direzione 2",
|
||||
"pattern.flip_direction": "Inverti direzione",
|
||||
"perf.click_for_details": "Fai clic per i dettagli: grafico dei tempi, istogramma e ciò che la GPU ha disegnato",
|
||||
"perf.draw_calls": "Chiamate di disegno",
|
||||
"perf.line_segments": "Segmenti",
|
||||
"perf.low_1": "1% più lento",
|
||||
"perf.renderer": "Renderer",
|
||||
"perf.scene_build": "Costruzione scena",
|
||||
"perf.triangles": "Triangoli",
|
||||
"perf.ui_time": "Tempo UI",
|
||||
"perf.viewport": "Vista",
|
||||
"perf.worst": "peggiore",
|
||||
"pick a hole to seat it, or OK places it at the origin": "seleziona un foro in cui inserirlo, oppure premi OK per posizionarlo nell'origine",
|
||||
"pick one or two vertices, edges or faces": "seleziona uno o due vertici, spigoli o facce",
|
||||
"prefs.cpu": "CPU (software)",
|
||||
@@ -1063,7 +1107,8 @@
|
||||
"search.try": "Digita il nome di un comando, una feature di questo progetto o un parametro. Per eseguire un comando con dei valori, digita il suo id e un oggetto JSON: {example}",
|
||||
"search.valid": "Valido",
|
||||
"settings.display_quality_note": "I livelli più fini rendono più uniformi le facce curve e gli spigoli e richiedono più tempo per la mesh. Cambia solo la vista, mai il progetto o le esportazioni.",
|
||||
"settings.frame_rate_note": "Mostra i fotogrammi al secondo nell'angolo inferiore destro della vista. Quando è attivo, la vista viene ridisegnata continuamente.",
|
||||
"settings.frame_rate_details_note": "Aggiunge un grafico dei tempi degli ultimi fotogrammi con i triangoli disegnati sopra, un istogramma dei tempi dei fotogrammi e ciò che la GPU ha disegnato. Fai clic sulla frequenza nella vista per mostrarli o nasconderli.",
|
||||
"settings.frame_rate_note": "Mostra i fotogrammi al secondo nell'angolo inferiore destro della vista.",
|
||||
"settings.measure_axis_distances": "Mostra distanze X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Mostra le componenti lungo gli assi globali accanto alla distanza totale quando Misura è aperto.",
|
||||
"settings.msaa_applies_on_restart": "L'antialiasing verrà applicato al prossimo avvio di SolidCraft",
|
||||
@@ -1106,7 +1151,10 @@
|
||||
"the parts library is not available": "la libreria delle parti non è disponibile",
|
||||
"thread.automatic_size": "Dimensione automatica",
|
||||
"thread.designation": "Designazione",
|
||||
"thread.direction": "Verso del filetto",
|
||||
"thread.iso_metric": "Metrica ISO",
|
||||
"thread.left_hand": "Sinistro",
|
||||
"thread.right_hand": "Destro",
|
||||
"thread.select_size": "Seleziona dimensione",
|
||||
"timeline.group": "Nuova cartella di lavorazioni",
|
||||
"timeline.move_many": "Sposta elementi della sequenza temporale",
|
||||
@@ -1124,7 +1172,106 @@
|
||||
"trace.stopped_by_contact": "Termina al campione {sample}: {a} attraverserebbe {b}",
|
||||
"trace.stopped_by_loop": "Termina al campione {sample}: la catena chiusa che passa per {joint} non può chiudersi ulteriormente",
|
||||
"ui.1_degree_of_freedom": "1 grado di libertà",
|
||||
"ui.3d_mouse_axes_reassigned": "Gli assi sono riassegnati nelle Preferenze, e questo vale anche dopo questa calibrazione.",
|
||||
"ui.3d_mouse_button": "Pulsante {number}",
|
||||
"ui.3d_mouse_button_nothing": "Niente",
|
||||
"ui.3d_mouse_button_settings": "Impostazioni del mouse 3D",
|
||||
"ui.3d_mouse_buttons": "Pulsanti",
|
||||
"ui.3d_mouse_buttons_hint": "Premi un pulsante del tuo mouse 3D per trovarlo qui, poi scegli cosa fa.",
|
||||
"ui.3d_mouse_buttons_no_device": "Collega un mouse 3D per scegliere cosa fanno i suoi pulsanti.",
|
||||
"ui.3d_mouse_buttons_reset": "Ripristina i pulsanti predefiniti",
|
||||
"ui.3d_mouse_calibrate": "Calibra…",
|
||||
"ui.3d_mouse_calibrate_tip": "Muovi la manopola come chiede la finestra, così SolidCraft impara in che direzione e quanto va ogni asse di questo mouse 3D. Viene salvata per questo mouse 3D.",
|
||||
"ui.3d_mouse_calibrate_title": "Calibra mouse 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Muovi il mouse 3D una volta così SolidCraft lo vede.",
|
||||
"ui.3d_mouse_calibrated": "Calibrato",
|
||||
"ui.3d_mouse_calibration": "Calibrazione del mouse 3D",
|
||||
"ui.3d_mouse_calibration_done": "Fatto. Salvala per usarla ogni volta che questo mouse 3D è collegato.",
|
||||
"ui.3d_mouse_calibration_no_response": "Un asse si è mosso appena, quindi non si può calibrare. Ricomincia e muovi la manopola più a fondo ogni volta.",
|
||||
"ui.3d_mouse_circles_help": "I cerchi mostrano ciò che il dispositivo invia dopo la calibrazione. Il modello di prova mostra cosa ne fa la vista, dopo inversione, velocità e assegnazione degli assi.",
|
||||
"ui.3d_mouse_dead_zone": "Zona morta",
|
||||
"ui.3d_mouse_dead_zone_tip": "Quanto deve muoversi la manopola prima che si muova la vista, come quota della sua corsa completa. Aumentala se la vista si sposta con la manopola a riposo.",
|
||||
"ui.3d_mouse_denied": "SolidCraft vede un mouse 3D ma non ha il permesso di leggerlo. La regola udev è nelle note di configurazione del mouse 3D.",
|
||||
"ui.3d_mouse_denied_short": "Trovato, ma non leggibile",
|
||||
"ui.3d_mouse_driven_by": "Quale movimento calibrato del dispositivo comanda questo asse. Se ne scegli uno già usato da un altro asse, i due si scambiano.",
|
||||
"ui.3d_mouse_driven_by_label": "comandato da",
|
||||
"ui.3d_mouse_driver": "Driver del mouse 3D",
|
||||
"ui.3d_mouse_driver_auto": "Automatico",
|
||||
"ui.3d_mouse_driver_direct": "Diretto (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Per ora legge tramite spacenavd: il dispositivo stesso non si è potuto aprire.",
|
||||
"ui.3d_mouse_driver_only_direct": "Lettura diretta del dispositivo; spacenavd non è in esecuzione",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Lettura tramite spacenavd, l'unica possibilità disponibile",
|
||||
"ui.3d_mouse_forget": "Dimentica calibrazione",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Vista a sinistra",
|
||||
"ui.3d_mouse_ghost_left_window": "Finestra a sinistra",
|
||||
"ui.3d_mouse_ghost_position": "Posizione",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Vista a destra",
|
||||
"ui.3d_mouse_hint": "Sposta e ruota la vista insieme; gli schizzi usano solo panoramica e zoom.",
|
||||
"ui.3d_mouse_kind_cap": "Ritorno al centro",
|
||||
"ui.3d_mouse_kind_question": "Che tipo di mouse 3D è?",
|
||||
"ui.3d_mouse_kind_trackball": "Trackball",
|
||||
"ui.3d_mouse_looks_like_cap": "Il valore resta finché tieni, come in un dispositivo di tipo «Ritorno al centro».",
|
||||
"ui.3d_mouse_looks_like_trackball": "Il valore torna indietro appena smetti di muovere, come in una trackball.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Rotazione X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Rotazione Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Rotazione Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Traslazione X",
|
||||
"ui.3d_mouse_motion_translation_y": "Traslazione Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Traslazione Z",
|
||||
"ui.3d_mouse_navigate": "Naviga con il mouse 3D",
|
||||
"ui.3d_mouse_none": "Nessun mouse 3D trovato",
|
||||
"ui.3d_mouse_not_calibrated": "Non calibrato",
|
||||
"ui.3d_mouse_offer": "Calibrare il mouse 3D corregge gli assi scambiati o invertiti e imposta velocità e sensibilità.",
|
||||
"ui.3d_mouse_offer_never": "Non mostrare più",
|
||||
"ui.3d_mouse_offer_swap": "Passa a Hidraw (driver diretto)",
|
||||
"ui.3d_mouse_reading": "Lettura",
|
||||
"ui.3d_mouse_reset_axes": "Ripristina assegnazione assi",
|
||||
"ui.3d_mouse_reset_view": "Ripristina vista",
|
||||
"ui.3d_mouse_restore": "Ripristina impostazioni mouse 3D",
|
||||
"ui.3d_mouse_restore_ask": "Riportare le impostazioni del mouse 3D ai valori predefiniti? Calibrazione e assegnazione dei pulsanti restano.",
|
||||
"ui.3d_mouse_section": "Mouse 3D",
|
||||
"ui.3d_mouse_show_in_view": "Mostra il mouse 3D nella vista 3D",
|
||||
"ui.3d_mouse_show_in_view_tip": "Un'immagine del tuo mouse 3D nell'angolo in basso a sinistra, che si muove quando lo muovi.",
|
||||
"ui.3d_mouse_skip": "Il mio dispositivo non può farlo",
|
||||
"ui.3d_mouse_skip_tip": "Salta questo movimento in entrambi i versi; la calibrazione lo esclude.",
|
||||
"ui.3d_mouse_speed": "Velocità del mouse 3D",
|
||||
"ui.3d_mouse_step_lift": "Solleva la manopola, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_press": "Premi la manopola verso il basso, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_pull": "Tira la manopola verso di te, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_push": "Spingi la manopola lontano da te, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_rest": "Togli la mano dalla manopola e lasciala ferma.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Togli la mano dalla sfera e lasciala ferma.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Gira la ghiera di scorrimento di qualche scatto in senso orario.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Gira la ghiera di scorrimento di qualche scatto in senso antiorario.",
|
||||
"ui.3d_mouse_step_roll_away": "Fai rotolare la sfera lontano da te qualche volta, poi lasciala fermare.",
|
||||
"ui.3d_mouse_step_roll_left": "Fai rotolare la sfera verso sinistra qualche volta, poi lasciala fermare.",
|
||||
"ui.3d_mouse_step_roll_right": "Fai rotolare la sfera verso destra qualche volta, poi lasciala fermare.",
|
||||
"ui.3d_mouse_step_roll_toward": "Fai rotolare la sfera verso di te qualche volta, poi lasciala fermare.",
|
||||
"ui.3d_mouse_step_slide_left": "Fai scorrere la manopola a sinistra, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_slide_right": "Fai scorrere la manopola a destra, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Ruota la sfera in senso orario qualche volta, poi lasciala fermare.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Ruota la sfera in senso antiorario qualche volta, poi lasciala fermare.",
|
||||
"ui.3d_mouse_step_tilt_away": "Inclina la parte superiore della manopola lontano da te (il bordo più vicino sale), tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_tilt_left": "Inclina la manopola a sinistra (bordo sinistro in basso), tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_tilt_right": "Inclina la manopola a destra (bordo destro in basso), tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Inclina la parte superiore della manopola verso di te (il bordo più vicino scende), tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_touch": "Appoggia la mano sulla manopola senza spingerla.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Ruota la manopola in senso orario, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Ruota la manopola in senso antiorario, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_switch_cap": "Calibra come ritorno al centro",
|
||||
"ui.3d_mouse_switch_trackball": "Calibra come trackball",
|
||||
"ui.3d_mouse_view_axis": "Asse della vista",
|
||||
"ui.3d_mouse_view_off": "Disattivato",
|
||||
"ui.3d_mouse_view_off_tip": "Il profilo del driver del mouse 3D disattiva questo asse, quindi il suo movimento non muove la vista. Se qui scegli il movimento di un altro asse, i due si scambiano e quel movimento smette di muovere la vista.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbita sinistra / destra",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbita su / giù",
|
||||
"ui.3d_mouse_view_pan_sideways": "Panoramica sinistra / destra",
|
||||
"ui.3d_mouse_view_pan_up_down": "Panoramica su / giù",
|
||||
"ui.3d_mouse_view_roll": "Rollio",
|
||||
"ui.3d_mouse_view_unmapped": "Asse {n}",
|
||||
"ui.3d_mouse_view_zoom": "Zoom",
|
||||
"ui.3d_mouse_waiting": "In attesa di movimento",
|
||||
"ui.3d_mouse_wiggle": "Se ne hai uno collegato, muovilo un po'.",
|
||||
"ui.3d_sketch": "Schizzo 3D",
|
||||
"ui.activate": "Attiva",
|
||||
"ui.active_component": "Componente attivo",
|
||||
@@ -1186,6 +1333,7 @@
|
||||
"ui.agent_chat_files_left_out": "Un messaggio può contenere al massimo {max} file; esclusi: {files}",
|
||||
"ui.agent_chat_files_loading": "In attesa della lettura dei file",
|
||||
"ui.agent_chat_files_max": "Al massimo {max} file per messaggio",
|
||||
"ui.agent_chat_flatpak_none_found": "Nessun agente di programmazione trovato. Per impostazione predefinita, la sandbox Flatpak non vede gli agenti installati nella tua cartella home e non ha accesso alla rete. Per consentire Claude Code e il suo adattatore ACP installati con nvm o in ~/.local/bin, esegui questo comando in un terminale, poi riavvia SolidCraft. Concede l’accesso ai programmi e ai file di accesso elencati, oltre all’accesso alla rete:",
|
||||
"ui.agent_chat_import_failed": "{name}: importazione non riuscita: {error}",
|
||||
"ui.agent_chat_intro": "Conversa con un agente di programmazione che già usi o con un endpoint di modello a tua scelta. Opera sul progetto aperto tramite gli strumenti di SolidCraft: le modifiche sono visibili mentre vengono eseguite e si annullano in un solo passo per messaggio. Le azioni consentite senza conferma si impostano in Preferenze > IA.",
|
||||
"ui.agent_chat_key_in_env": "chiave impostata",
|
||||
@@ -1281,6 +1429,7 @@
|
||||
"ui.ai_http_busy": "l'endpoint è occupato o ha superato il limite; riprova più tardi",
|
||||
"ui.ai_http_key": "l'endpoint ha rifiutato la chiave: controllala in Preferenze > IA",
|
||||
"ui.ai_http_not_found": "endpoint o modello inesistente: controlla l'URL di base e il nome del modello",
|
||||
"ui.ai_http_too_large": "la conversazione è troppo grande per questo endpoint; inizia una nuova chat",
|
||||
"ui.ai_key": "Chiave API",
|
||||
"ui.ai_key_hint": "facoltativa; conservata nel portachiavi di sistema",
|
||||
"ui.ai_key_needs_https": "La chiave viene inviata solo tramite HTTPS o a questo computer.",
|
||||
@@ -1308,7 +1457,6 @@
|
||||
"ui.ai_permissions": "Autorizzazioni",
|
||||
"ui.ai_preset": "Preimpostazione",
|
||||
"ui.ai_remove": "Rimuovi",
|
||||
"ui.ai_request_limit": "arrestato dopo 24 richieste per un messaggio; invia un altro messaggio per proseguire",
|
||||
"ui.ai_reset": "Reimposta",
|
||||
"ui.ai_settings": "Impostazioni IA…",
|
||||
"ui.ai_show_features": "Mostra funzioni IA",
|
||||
@@ -1353,6 +1501,7 @@
|
||||
"ui.blocked": "Bloccato",
|
||||
"ui.bodies": "Corpi",
|
||||
"ui.bodies_all_if_none": "Corpi (tutti se nessuno)",
|
||||
"ui.bottom": "Inferiore",
|
||||
"ui.box": "Parallelepipedo",
|
||||
"ui.break": "Spezza",
|
||||
"ui.browser_bodies_one": "{count} corpo",
|
||||
@@ -1484,6 +1633,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtra comandi o tasti",
|
||||
"ui.findInTimeline": "Trova nella sequenza temporale",
|
||||
"ui.findInWindow": "Trova nella finestra",
|
||||
"ui.fine": "Fine",
|
||||
"ui.finest": "Finissima",
|
||||
"ui.finish_sketch": "Termina schizzo",
|
||||
"ui.fit": "Adatta",
|
||||
"ui.fix_unfix": "Fissa/Libera",
|
||||
@@ -1491,6 +1642,7 @@
|
||||
"ui.flip": "Inverti",
|
||||
"ui.format": "Formato",
|
||||
"ui.frame_rate": "Frequenza dei fotogrammi",
|
||||
"ui.frame_rate_details": "Dettagli della frequenza dei fotogrammi",
|
||||
"ui.front": "Frontale",
|
||||
"ui.fully_constrained": "Completamente vincolato",
|
||||
"ui.function": "Funzione",
|
||||
@@ -1505,6 +1657,7 @@
|
||||
"ui.grid_follows_view_hint": "Le viste laterali mostrano la griglia sul piano verticale rivolto verso di esse anziché sul piano di appoggio",
|
||||
"ui.ground_shadow": "Ombra sul piano di appoggio",
|
||||
"ui.groupSelected": "Raggruppa selezionati",
|
||||
"ui.hatch": "Tratteggio",
|
||||
"ui.heading_add_ins": "COMPONENTI AGGIUNTIVI",
|
||||
"ui.heading_assemble": "ASSEMBLA",
|
||||
"ui.heading_box": "PARALLELEPIPEDO",
|
||||
@@ -1571,6 +1724,7 @@
|
||||
"ui.heading_torus": "TORO",
|
||||
"ui.heading_utilities": "UTILITÀ",
|
||||
"ui.heading_utility": "UTILITÀ",
|
||||
"ui.heading_weldments": "SALDATURE",
|
||||
"ui.help": "Aiuto",
|
||||
"ui.hide": "Nascondi",
|
||||
"ui.hideAxes": "Nascondi assi",
|
||||
@@ -1707,6 +1861,7 @@
|
||||
"ui.project_face_edges_sketch": "Proietta gli spigoli delle facce nei nuovi schizzi",
|
||||
"ui.properties": "Proprietà",
|
||||
"ui.pull_direction": "Direzione di estrazione",
|
||||
"ui.quality": "Qualità",
|
||||
"ui.quantity": "Quantità",
|
||||
"ui.quantity_2": "Quantità 2",
|
||||
"ui.quit": "Esci",
|
||||
@@ -1795,65 +1950,13 @@
|
||||
"ui.smooth_wheel_zoom": "Zoom fluido con rotellina",
|
||||
"ui.smooth_wheel_zoom_tip": "Disattiva per applicare immediatamente ogni evento della rotellina, senza coda di interpolazione",
|
||||
"ui.snap": "Aggancio",
|
||||
"ui.space_mouse_axes_reassigned": "Gli assi sono riassegnati nelle Preferenze, e questo vale anche dopo questa calibrazione.",
|
||||
"ui.space_mouse_calibrate": "Calibra…",
|
||||
"ui.space_mouse_calibrate_tip": "Muovi la manopola come chiede la finestra, così SolidCraft impara in che direzione e quanto va ogni asse di questo mouse 3D. Viene salvata per questo mouse 3D.",
|
||||
"ui.space_mouse_calibrate_title": "Calibra mouse 3D",
|
||||
"ui.space_mouse_calibrated": "Calibrato",
|
||||
"ui.space_mouse_calibration": "Calibrazione del mouse 3D",
|
||||
"ui.space_mouse_calibration_done": "Fatto. Salvala per usarla ogni volta che questo mouse 3D è collegato.",
|
||||
"ui.space_mouse_calibration_no_response": "Un asse si è mosso appena, quindi non si può calibrare. Ricomincia e muovi la manopola più a fondo ogni volta.",
|
||||
"ui.space_mouse_dead_zone": "Zona morta",
|
||||
"ui.space_mouse_dead_zone_tip": "Quanto deve muoversi la manopola prima che si muova la vista, come quota della sua corsa completa. Aumentala se la vista si sposta con la manopola a riposo.",
|
||||
"ui.space_mouse_driven_by": "Quale movimento calibrato del dispositivo comanda questo asse. Se ne scegli uno già usato da un altro asse, i due si scambiano.",
|
||||
"ui.space_mouse_driven_by_label": "comandato da",
|
||||
"ui.space_mouse_driver": "Driver del mouse 3D",
|
||||
"ui.space_mouse_driver_auto": "Automatico",
|
||||
"ui.space_mouse_driver_direct": "Diretto (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Lettura diretta del dispositivo; spacenavd non è in esecuzione",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Lettura tramite spacenavd, l'unica possibilità disponibile",
|
||||
"ui.space_mouse_forget": "Dimentica calibrazione",
|
||||
"ui.space_mouse_hint": "Sposta e ruota la vista insieme; gli schizzi usano solo panoramica e zoom.",
|
||||
"ui.space_mouse_motion_rotation_x": "Rotazione X",
|
||||
"ui.space_mouse_motion_rotation_y": "Rotazione Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Rotazione Z",
|
||||
"ui.space_mouse_motion_translation_x": "Traslazione X",
|
||||
"ui.space_mouse_motion_translation_y": "Traslazione Y",
|
||||
"ui.space_mouse_motion_translation_z": "Traslazione Z",
|
||||
"ui.space_mouse_navigate": "Naviga con SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Nessun mouse 3D trovato",
|
||||
"ui.space_mouse_not_calibrated": "Non calibrato",
|
||||
"ui.space_mouse_reading": "Lettura",
|
||||
"ui.space_mouse_reset_axes": "Ripristina assegnazione assi",
|
||||
"ui.space_mouse_step_lift": "Solleva la manopola, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_press": "Premi la manopola verso il basso, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_pull": "Tira la manopola verso di te, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_push": "Spingi la manopola lontano da te, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_rest": "Togli la mano dalla manopola e lasciala ferma.",
|
||||
"ui.space_mouse_step_slide_left": "Fai scorrere la manopola a sinistra, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_slide_right": "Fai scorrere la manopola a destra, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_tilt_back": "Inclina la manopola all'indietro (bordo anteriore in alto), tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_tilt_forward": "Inclina la manopola in avanti (bordo anteriore in basso), tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_tilt_left": "Inclina la manopola a sinistra (bordo sinistro in basso), tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_tilt_right": "Inclina la manopola a destra (bordo destro in basso), tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Ruota la manopola in senso orario, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Ruota la manopola in senso antiorario, tienila, poi lasciala.",
|
||||
"ui.space_mouse_view_axis": "Asse della vista",
|
||||
"ui.space_mouse_view_off": "Disattivato",
|
||||
"ui.space_mouse_view_off_tip": "Il profilo del driver del mouse 3D disattiva questo asse, quindi il suo movimento non muove la vista. Se qui scegli il movimento di un altro asse, i due si scambiano e quel movimento smette di muovere la vista.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Orbita sinistra / destra",
|
||||
"ui.space_mouse_view_orbit_up_down": "Orbita su / giù",
|
||||
"ui.space_mouse_view_pan_sideways": "Panoramica sinistra / destra",
|
||||
"ui.space_mouse_view_pan_up_down": "Panoramica su / giù",
|
||||
"ui.space_mouse_view_roll": "Rollio",
|
||||
"ui.space_mouse_view_unmapped": "Asse {n}",
|
||||
"ui.space_mouse_view_zoom": "Zoom",
|
||||
"ui.spacing": "Spaziatura",
|
||||
"ui.spacing_2": "Spaziatura 2",
|
||||
"ui.sphere": "Sfera",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Dividi corpo",
|
||||
"ui.stand_up_tip": "Dopo l'apertura di un file STEP o IGES con asse Z verticale, proponi un quarto di giro attorno a X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Inizio",
|
||||
"ui.start_over": "Ricomincia",
|
||||
"ui.start_page": "Pagina iniziale",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Pilih…",
|
||||
"Circular": "Bulat",
|
||||
"Circumference": "Lilitan",
|
||||
"Clockwise": "Ikut arah jam",
|
||||
"Close draft": "Tutup draf",
|
||||
"Collisions": "Perlanggaran",
|
||||
"Colour": "Warna",
|
||||
@@ -322,6 +323,7 @@
|
||||
"Search commands": "Cari arahan",
|
||||
"Search this library": "Cari dalam koleksi ini",
|
||||
"Section": "Keratan",
|
||||
"Section position": "Kedudukan keratan",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Pilih Lain: tahan butang kiri pada model tanpa bergerak (atau klik kanan › Pilih Lain…) untuk senarai objek di bawah kursor. Klik tempat sama untuk objek seterusnya. Klik dua kali butang tengah untuk ke Utama (Tetapan › Navigasi).",
|
||||
"Select bodies (a face picks its body)": "Pilih jasad (klik muka untuk pilih jasadnya)",
|
||||
"Select bodies or faces instead": "Pilih jasad atau muka",
|
||||
@@ -333,6 +335,7 @@
|
||||
"Show Agent Cursor": "Papar Kursor Ejen",
|
||||
"Show in folder": "Papar dalam folder",
|
||||
"Show the pointer an AI agent moves (#35)": "Papar penuding yang digerakkan oleh ejen AI (#35)",
|
||||
"Show tool at cursor": "Tunjukkan alat di sebelah kursor",
|
||||
"Signed axes": "Paksi bertanda",
|
||||
"Size": "Saiz",
|
||||
"Sketch constraints": "Kekangan lakaran",
|
||||
@@ -345,9 +348,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft ditutup sebelum reka bentuk disimpan. Buka simpanan automatik terakhirnya.",
|
||||
"Specialise": "Khususkan",
|
||||
"Speed": "Kelajuan",
|
||||
"Square": "Segi empat sama",
|
||||
"Stand it up": "Tegakkan",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Tegakkan: suku putaran pada X, satu ciri Alih yang boleh dibatalkan.",
|
||||
"Stand this part up?": "Tegakkan bahagian ini?",
|
||||
"Start angle": "Sudut mula",
|
||||
"Start bulge": "Bonjolan permulaan",
|
||||
"Start continuity": "Keselanjaran permulaan",
|
||||
"Step": "Langkah",
|
||||
@@ -412,6 +417,7 @@
|
||||
"angle, height": "sudut, ketinggian",
|
||||
"appearance.dielectric_ior": "IOR Dielektrik",
|
||||
"appearance.metallic": "Sifat Logam",
|
||||
"appearance.only_available": "Rupa yang tersedia sahaja",
|
||||
"appearance.pbr_help": "Lihat dalam Render atau PNG produk. Tiada pembiasan atau penembusan cahaya.",
|
||||
"appearance.pbr_opaque": "PBR Legap",
|
||||
"appearance.roughness": "Kekasaran",
|
||||
@@ -530,6 +536,10 @@
|
||||
"cmd.file_insert_step": "Sisip STEP",
|
||||
"cmd.file_save_mesh": "Simpan sebagai Mesh",
|
||||
"cmd.file_slicers": "Penghiris Terpasang",
|
||||
"cmd.frame_member": "Anggota Rangka",
|
||||
"cmd.frame_profile_insert": "Sisipkan Profil",
|
||||
"cmd.frame_profile_list": "Pustaka Profil",
|
||||
"cmd.frame_profile_properties": "Sifat Profil",
|
||||
"cmd.inspect_accessibility": "Analisis Kebolehcapaian",
|
||||
"cmd.inspect_center_of_mass": "Pusat Jisim",
|
||||
"cmd.inspect_clearance": "Kelegaan",
|
||||
@@ -569,6 +579,7 @@
|
||||
"cmd.material_create": "Bahan Fizikal Baharu",
|
||||
"cmd.material_delete": "Padam Bahan Fizikal",
|
||||
"cmd.material_edit": "Sunting Bahan Fizikal",
|
||||
"cmd.material_inspect": "Periksa Bahan Fizikal",
|
||||
"cmd.material_list": "Senarai Bahan",
|
||||
"cmd.material_properties_directional": "Modulus Keanjalan Berarah",
|
||||
"cmd.material_properties_validate": "Sahkan Data Bahan Fizikal",
|
||||
@@ -960,6 +971,7 @@
|
||||
"motion.key_here": "Kunci di sini",
|
||||
"motion.key_here_hint": "Tambah kekunci pada langkah ini dengan nilai sendi sekarang",
|
||||
"motion.no_collisions": "Tiada perlanggaran sepanjang kajian",
|
||||
"motion.plot_quantity": "Kuantiti",
|
||||
"motion.step_back": "Langkah ke belakang",
|
||||
"motion.step_forward": "Langkah ke hadapan",
|
||||
"motion.to_end": "Ke penghujung",
|
||||
@@ -1060,7 +1072,7 @@
|
||||
"search.try": "Taip nama arahan, ciri dalam reka bentuk ini, atau parameter. Untuk menjalankan arahan dengan nilai, taip ID-nya dan objek JSON: {example}",
|
||||
"search.valid": "Sah",
|
||||
"settings.display_quality_note": "Tahap halus melicinkan muka dan tepi melengkung, tetapi mesh lebih lambat disiapkan. Hanya paparan berubah, bukan reka bentuk atau eksport.",
|
||||
"settings.frame_rate_note": "Papar bingkai sesaat di kanan bawah. Jika diaktifkan, pandangan dilukis semula tanpa henti.",
|
||||
"settings.frame_rate_note": "Papar bingkai sesaat di kanan bawah.",
|
||||
"settings.measure_axis_distances": "Papar jarak X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Papar jarak paksi dunia X/Y/Z bersama jarak keseluruhan semasa Ukur dibuka.",
|
||||
"settings.msaa_applies_on_restart": "Antialias digunakan apabila SolidCraft dimulakan semula",
|
||||
@@ -1121,7 +1133,106 @@
|
||||
"trace.stopped_by_contact": "Berakhir pada sampel {sample}: {a} akan menembusi {b}",
|
||||
"trace.stopped_by_loop": "Berakhir pada sampel {sample}: gelung melalui {joint} tidak lagi dapat ditutup",
|
||||
"ui.1_degree_of_freedom": "1 darjah kebebasan",
|
||||
"ui.3d_mouse_axes_reassigned": "Paksi ditetapkan semula dalam Keutamaan, dan itu masih terpakai di atas penentukuran ini.",
|
||||
"ui.3d_mouse_button": "Butang {number}",
|
||||
"ui.3d_mouse_button_nothing": "Tiada",
|
||||
"ui.3d_mouse_button_settings": "Tetapan tetikus 3D",
|
||||
"ui.3d_mouse_buttons": "Butang",
|
||||
"ui.3d_mouse_buttons_hint": "Tekan butang pada tetikus 3D anda untuk mencarinya di sini, kemudian pilih fungsinya.",
|
||||
"ui.3d_mouse_buttons_no_device": "Sambungkan tetikus 3D untuk menetapkan fungsi butangnya.",
|
||||
"ui.3d_mouse_buttons_reset": "Pulihkan butang lalai",
|
||||
"ui.3d_mouse_calibrate": "Tentukur…",
|
||||
"ui.3d_mouse_calibrate_tip": "Gerakkan penutup seperti yang diminta tetingkap, supaya SolidCraft mempelajari arah dan jarak setiap paksi tetikus 3D ini. Disimpan untuk tetikus 3D ini.",
|
||||
"ui.3d_mouse_calibrate_title": "Tentukur tetikus 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Gerakkan tetikus 3D sekali supaya SolidCraft dapat melihatnya.",
|
||||
"ui.3d_mouse_calibrated": "Telah ditentukur",
|
||||
"ui.3d_mouse_calibration": "Penentukuran tetikus 3D",
|
||||
"ui.3d_mouse_calibration_done": "Selesai. Simpan untuk digunakan setiap kali tetikus 3D ini disambungkan.",
|
||||
"ui.3d_mouse_calibration_no_response": "Satu paksi hampir tidak bergerak, jadi tidak dapat ditentukur. Mulakan semula dan gerakkan penutup lebih jauh setiap kali.",
|
||||
"ui.3d_mouse_circles_help": "Bulatan menunjukkan apa yang dihantar peranti selepas penentukuran. Model ujian menunjukkan apa yang dilakukan paparan dengannya, selepas songsangan, kelajuan dan penetapan paksi.",
|
||||
"ui.3d_mouse_dead_zone": "Zon mati",
|
||||
"ui.3d_mouse_dead_zone_tip": "Sejauh mana penutup perlu bergerak sebelum pandangan bergerak, sebagai bahagian daripada julat penuhnya. Naikkan jika pandangan hanyut semasa penutup dalam keadaan rehat.",
|
||||
"ui.3d_mouse_denied": "SolidCraft nampak tetikus 3D tetapi tidak dibenarkan membacanya. Lihat nota persediaan tetikus 3D untuk peraturan udev.",
|
||||
"ui.3d_mouse_denied_short": "Dijumpai, tetapi tidak boleh dibaca",
|
||||
"ui.3d_mouse_driven_by": "Gerakan peranti yang telah ditentukur yang memacu paksi ini. Memilih gerakan yang sudah digunakan paksi lain akan menukar kedua-duanya.",
|
||||
"ui.3d_mouse_driven_by_label": "dipacu oleh",
|
||||
"ui.3d_mouse_driver": "Pemacu tetikus 3D",
|
||||
"ui.3d_mouse_driver_auto": "Automatik",
|
||||
"ui.3d_mouse_driver_direct": "Terus (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Buat masa ini dibaca melalui spacenavd: peranti itu sendiri tidak dapat dibuka.",
|
||||
"ui.3d_mouse_driver_only_direct": "Membaca peranti secara terus; spacenavd tidak berjalan",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Membaca melalui spacenavd, satu-satunya cara yang ada",
|
||||
"ui.3d_mouse_forget": "Lupakan penentukuran",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Viewport kiri",
|
||||
"ui.3d_mouse_ghost_left_window": "Tetingkap kiri",
|
||||
"ui.3d_mouse_ghost_position": "Kedudukan",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Viewport kanan",
|
||||
"ui.3d_mouse_hint": "Alih dan putar pandangan serentak; dalam lakaran, hanya sorot dan zum digunakan.",
|
||||
"ui.3d_mouse_kind_cap": "Kembali ke tengah",
|
||||
"ui.3d_mouse_kind_question": "Apakah jenis tetikus 3D ini?",
|
||||
"ui.3d_mouse_kind_trackball": "Bola jejak",
|
||||
"ui.3d_mouse_looks_like_cap": "Bacaan kekal selagi anda menahannya, seperti peranti jenis “Kembali ke tengah”.",
|
||||
"ui.3d_mouse_looks_like_trackball": "Bacaan kembali sebaik sahaja anda berhenti bergerak, seperti bola jejak.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Putaran X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Putaran Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Putaran Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Anjakan X",
|
||||
"ui.3d_mouse_motion_translation_y": "Anjakan Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Anjakan Z",
|
||||
"ui.3d_mouse_navigate": "Navigasi dengan tetikus 3D",
|
||||
"ui.3d_mouse_none": "Tiada tetikus 3D ditemui",
|
||||
"ui.3d_mouse_not_calibrated": "Belum ditentukur",
|
||||
"ui.3d_mouse_offer": "Menentukur tetikus 3D membetulkan paksi yang bertukar atau terbalik dan menetapkan kelajuan serta rasanya.",
|
||||
"ui.3d_mouse_offer_never": "Jangan tunjuk lagi",
|
||||
"ui.3d_mouse_offer_swap": "Tukar ke Hidraw (pemacu terus)",
|
||||
"ui.3d_mouse_reading": "Bacaan",
|
||||
"ui.3d_mouse_reset_axes": "Tetapkan semula penetapan paksi",
|
||||
"ui.3d_mouse_reset_view": "Set semula paparan",
|
||||
"ui.3d_mouse_restore": "Pulihkan lalai tetikus 3D",
|
||||
"ui.3d_mouse_restore_ask": "Tetapkan semula tetapan tetikus 3D kepada lalai? Penentukuran dan pemetaan butang kekal.",
|
||||
"ui.3d_mouse_section": "Tetikus 3D",
|
||||
"ui.3d_mouse_show_in_view": "Tunjukkan tetikus 3D dalam paparan 3D",
|
||||
"ui.3d_mouse_show_in_view_tip": "Gambar tetikus 3D anda di sudut kiri bawah, yang bergerak apabila anda menggerakkannya.",
|
||||
"ui.3d_mouse_skip": "Peranti saya tidak boleh buat ini",
|
||||
"ui.3d_mouse_skip_tip": "Langkau gerakan ini kedua-dua arah; penentukuran tidak menggunakannya.",
|
||||
"ui.3d_mouse_speed": "Kelajuan tetikus 3D",
|
||||
"ui.3d_mouse_step_lift": "Angkat penutup ke atas, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_press": "Tekan penutup ke bawah, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_pull": "Tarik penutup ke arah anda, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_push": "Tolak penutup menjauhi anda, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_rest": "Alihkan tangan anda daripada penutup dan biarkan ia diam.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Alihkan tangan anda daripada bola dan biarkan ia diam.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Putar gelang tatal ikut arah jam beberapa klik.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Putar gelang tatal lawan arah jam beberapa klik.",
|
||||
"ui.3d_mouse_step_roll_away": "Gulingkan bola menjauhi anda beberapa kali, kemudian biarkan ia berhenti.",
|
||||
"ui.3d_mouse_step_roll_left": "Gulingkan bola ke kiri beberapa kali, kemudian biarkan ia berhenti.",
|
||||
"ui.3d_mouse_step_roll_right": "Gulingkan bola ke kanan beberapa kali, kemudian biarkan ia berhenti.",
|
||||
"ui.3d_mouse_step_roll_toward": "Gulingkan bola ke arah anda beberapa kali, kemudian biarkan ia berhenti.",
|
||||
"ui.3d_mouse_step_slide_left": "Luncurkan penutup ke kiri, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_slide_right": "Luncurkan penutup ke kanan, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Pusing bola ikut arah jam beberapa kali, kemudian biarkan ia berhenti.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Pusing bola lawan arah jam beberapa kali, kemudian biarkan ia berhenti.",
|
||||
"ui.3d_mouse_step_tilt_away": "Condongkan bahagian atas penutup menjauhi anda (tepi terdekat naik), tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_tilt_left": "Condongkan penutup ke kiri (tepi kiri turun), tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_tilt_right": "Condongkan penutup ke kanan (tepi kanan turun), tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Condongkan bahagian atas penutup ke arah anda (tepi terdekat turun), tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_touch": "Letakkan tangan anda pada penutup tanpa menolaknya.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Pusing penutup ikut arah jam, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Pusing penutup lawan arah jam, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_switch_cap": "Tentukur sebagai kembali ke tengah",
|
||||
"ui.3d_mouse_switch_trackball": "Tentukur sebagai bola jejak",
|
||||
"ui.3d_mouse_view_axis": "Paksi paparan",
|
||||
"ui.3d_mouse_view_off": "Mati",
|
||||
"ui.3d_mouse_view_off_tip": "Profil pemacu tetikus 3D mematikan paksi ini, jadi gerakannya tidak menggerakkan paparan. Memilih gerakan paksi lain di sini menukar kedua-duanya, dan gerakan itu tidak lagi menggerakkan paparan.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbit kiri / kanan",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbit atas / bawah",
|
||||
"ui.3d_mouse_view_pan_sideways": "Pan kiri / kanan",
|
||||
"ui.3d_mouse_view_pan_up_down": "Pan atas / bawah",
|
||||
"ui.3d_mouse_view_roll": "Gelek",
|
||||
"ui.3d_mouse_view_unmapped": "Paksi {n}",
|
||||
"ui.3d_mouse_view_zoom": "Zum",
|
||||
"ui.3d_mouse_waiting": "Menunggu pergerakan",
|
||||
"ui.3d_mouse_wiggle": "Jika anda ada yang disambungkan, gerakkannya sedikit.",
|
||||
"ui.3d_sketch": "Lakaran 3D",
|
||||
"ui.activate": "Aktifkan",
|
||||
"ui.active_component": "Komponen aktif",
|
||||
@@ -1183,6 +1294,7 @@
|
||||
"ui.agent_chat_files_left_out": "Paling banyak {max} fail bersama satu mesej; ditinggalkan: {files}",
|
||||
"ui.agent_chat_files_loading": "Menunggu fail dibaca",
|
||||
"ui.agent_chat_files_max": "Paling banyak {max} fail pada satu mesej",
|
||||
"ui.agent_chat_flatpak_none_found": "Tiada ejen pengekodan. Secara lalai, kotak pasir Flatpak tidak dapat melihat ejen yang dipasang dalam folder rumah anda dan tiada akses rangkaian. Untuk membenarkan Claude Code dan penyesuai ACP-nya yang dipasang melalui nvm atau dalam ~/.local/bin, jalankan arahan ini dalam terminal, kemudian mulakan semula SolidCraft. Ia memberi akses kepada program dan fail log masuk yang disenaraikan, serta akses rangkaian:",
|
||||
"ui.agent_chat_import_failed": "{name}: import gagal: {error}",
|
||||
"ui.agent_chat_intro": "Sembang dengan ejen pengekodan biasa anda atau titik akhir model pilihan. Ejen mengubah reka bentuk melalui alat SolidCraft; perubahan dipaparkan terus dan boleh dibatalkan sekali bagi setiap mesej. Tetapkan kebenaran di Tetapan > AI.",
|
||||
"ui.agent_chat_key_in_env": "kunci tersedia",
|
||||
@@ -1278,6 +1390,7 @@
|
||||
"ui.ai_http_busy": "titik akhir sibuk atau telah melebihi had; cuba lagi kemudian",
|
||||
"ui.ai_http_key": "kunci ditolak oleh titik akhir: semak dalam Tetapan > AI",
|
||||
"ui.ai_http_not_found": "titik akhir atau model tidak ditemui: semak URL asas dan nama model",
|
||||
"ui.ai_http_too_large": "perbualan terlalu besar untuk titik akhir ini; mulakan sembang baharu",
|
||||
"ui.ai_key": "Kunci API",
|
||||
"ui.ai_key_hint": "tidak wajib; disimpan dalam rantai kunci sistem",
|
||||
"ui.ai_key_needs_https": "Kunci hanya dihantar melalui HTTPS, atau ke komputer ini.",
|
||||
@@ -1305,7 +1418,6 @@
|
||||
"ui.ai_permissions": "Kebenaran",
|
||||
"ui.ai_preset": "Pratetap",
|
||||
"ui.ai_remove": "Buang",
|
||||
"ui.ai_request_limit": "berhenti selepas 24 permintaan untuk satu mesej; hantar mesej lain untuk meneruskan",
|
||||
"ui.ai_reset": "Set Semula",
|
||||
"ui.ai_settings": "Tetapan AI…",
|
||||
"ui.ai_show_features": "Papar ciri AI",
|
||||
@@ -1350,6 +1462,7 @@
|
||||
"ui.blocked": "Disekat",
|
||||
"ui.bodies": "Jasad",
|
||||
"ui.bodies_all_if_none": "Jasad (semua jika tiada)",
|
||||
"ui.bottom": "Bawah",
|
||||
"ui.box": "Kotak",
|
||||
"ui.break": "Putuskan",
|
||||
"ui.browser_bodies_one": "{count} jasad",
|
||||
@@ -1481,6 +1594,8 @@
|
||||
"ui.filter_commands_or_keys": "Tapis arahan atau kekunci",
|
||||
"ui.findInTimeline": "Cari dalam Garis Masa",
|
||||
"ui.findInWindow": "Cari dalam Tetingkap",
|
||||
"ui.fine": "Halus",
|
||||
"ui.finest": "Paling halus",
|
||||
"ui.finish_sketch": "Tamatkan Lakaran",
|
||||
"ui.fit": "Muatkan",
|
||||
"ui.fix_unfix": "Tetap/Lepas",
|
||||
@@ -1502,6 +1617,7 @@
|
||||
"ui.grid_follows_view_hint": "Pandangan sisi guna grid satah tegak yang menghadapnya, bukan satah tanah",
|
||||
"ui.ground_shadow": "Bayang tanah",
|
||||
"ui.groupSelected": "Kumpulkan Pilihan",
|
||||
"ui.hatch": "Lorek",
|
||||
"ui.heading_add_ins": "TAMBAHAN",
|
||||
"ui.heading_assemble": "PASANG",
|
||||
"ui.heading_box": "KOTAK",
|
||||
@@ -1568,6 +1684,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "UTILITI",
|
||||
"ui.heading_utility": "UTILITI",
|
||||
"ui.heading_weldments": "KIMPALAN",
|
||||
"ui.help": "Bantuan",
|
||||
"ui.hide": "Sembunyi",
|
||||
"ui.hideAxes": "Sembunyi Paksi",
|
||||
@@ -1704,6 +1821,7 @@
|
||||
"ui.project_face_edges_sketch": "Unjur tepi muka ke dalam lakaran baharu",
|
||||
"ui.properties": "Sifat",
|
||||
"ui.pull_direction": "Arah tarikan",
|
||||
"ui.quality": "Kualiti",
|
||||
"ui.quantity": "Kuantiti",
|
||||
"ui.quantity_2": "Kuantiti 2",
|
||||
"ui.quit": "Keluar",
|
||||
@@ -1792,40 +1910,13 @@
|
||||
"ui.smooth_wheel_zoom": "Zum roda lancar",
|
||||
"ui.smooth_wheel_zoom_tip": "Matikan untuk mengezum serta-merta bagi setiap putaran roda tetikus, tanpa gerakan susulan",
|
||||
"ui.snap": "Lekap",
|
||||
"ui.space_mouse_calibrate": "Tentukur…",
|
||||
"ui.space_mouse_calibrate_tip": "Gerakkan penutup seperti yang diminta tetingkap, supaya SolidCraft mempelajari arah dan jarak setiap paksi tetikus 3D ini. Disimpan untuk tetikus 3D ini.",
|
||||
"ui.space_mouse_calibrate_title": "Tentukur tetikus 3D",
|
||||
"ui.space_mouse_calibrated": "Telah ditentukur",
|
||||
"ui.space_mouse_calibration": "Penentukuran tetikus 3D",
|
||||
"ui.space_mouse_calibration_done": "Selesai. Simpan untuk digunakan setiap kali tetikus 3D ini disambungkan.",
|
||||
"ui.space_mouse_calibration_no_response": "Satu paksi hampir tidak bergerak, jadi tidak dapat ditentukur. Mulakan semula dan gerakkan penutup lebih jauh setiap kali.",
|
||||
"ui.space_mouse_dead_zone": "Zon mati",
|
||||
"ui.space_mouse_dead_zone_tip": "Sejauh mana penutup perlu bergerak sebelum pandangan bergerak, sebagai bahagian daripada julat penuhnya. Naikkan jika pandangan hanyut semasa penutup dalam keadaan rehat.",
|
||||
"ui.space_mouse_forget": "Lupakan penentukuran",
|
||||
"ui.space_mouse_hint": "Alih dan putar pandangan serentak; dalam lakaran, hanya sorot dan zum digunakan.",
|
||||
"ui.space_mouse_navigate": "Navigasi dengan SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Tiada tetikus 3D ditemui",
|
||||
"ui.space_mouse_not_calibrated": "Belum ditentukur",
|
||||
"ui.space_mouse_reading": "Bacaan",
|
||||
"ui.space_mouse_step_lift": "Angkat penutup ke atas, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_press": "Tekan penutup ke bawah, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_pull": "Tarik penutup ke arah anda, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_push": "Tolak penutup menjauhi anda, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_rest": "Alihkan tangan anda daripada penutup dan biarkan ia diam.",
|
||||
"ui.space_mouse_step_slide_left": "Luncurkan penutup ke kiri, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_slide_right": "Luncurkan penutup ke kanan, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_tilt_back": "Condongkan penutup ke belakang (tepi depan naik), tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_tilt_forward": "Condongkan penutup ke hadapan (tepi depan turun), tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_tilt_left": "Condongkan penutup ke kiri (tepi kiri turun), tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_tilt_right": "Condongkan penutup ke kanan (tepi kanan turun), tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Pusing penutup ikut arah jam, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Pusing penutup lawan arah jam, tahan, kemudian lepaskan.",
|
||||
"ui.spacing": "Selang",
|
||||
"ui.spacing_2": "Selang 2",
|
||||
"ui.sphere": "Sfera",
|
||||
"ui.spline": "Splin",
|
||||
"ui.split_body": "Belah Jasad",
|
||||
"ui.stand_up_tip": "Selepas buka STEP atau IGES dengan Z ke atas, cadangkan suku putaran pada X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Mula",
|
||||
"ui.start_over": "Mula semula",
|
||||
"ui.start_page": "Halaman mula",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Kiezen…",
|
||||
"Circular": "Cirkelvormig",
|
||||
"Circumference": "Omtrek",
|
||||
"Clockwise": "Met de klok mee",
|
||||
"Close draft": "Concept sluiten",
|
||||
"Collisions": "Botsingen",
|
||||
"Colour": "Kleur",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Opdrachten zoeken",
|
||||
"Search this library": "Deze bibliotheek doorzoeken",
|
||||
"Section": "Doorsnede",
|
||||
"Section position": "Positie van de doorsnede",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Overige selecteren: houd de linkermuisknop stil op het model (of klik met rechts › Overige selecteren…) om alles onder de cursor weer te geven; klik opnieuw op dezelfde plek om het volgende item daar te selecteren. Dubbelklik met de middelste knop om naar Home te gaan (Voorkeuren › Navigatie).",
|
||||
"Select bodies (a face picks its body)": "Lichamen selecteren (een vlak kiest zijn lichaam)",
|
||||
"Select bodies or faces instead": "In plaats daarvan lichamen of vlakken selecteren",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Agentcursor tonen",
|
||||
"Show in folder": "Tonen in map",
|
||||
"Show the pointer an AI agent moves (#35)": "De aanwijzer tonen die een AI-agent verplaatst (#35)",
|
||||
"Show tool at cursor": "Gereedschap bij de cursor tonen",
|
||||
"Signed axes": "Assen met teken",
|
||||
"Size": "Grootte",
|
||||
"Sketch constraints": "Schetsbeperkingen",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft is gesloten voordat deze ontwerpen zijn opgeslagen. Hun laatste automatische opslag kan worden geopend.",
|
||||
"Specialise": "Specialiseren",
|
||||
"Speed": "Snelheid",
|
||||
"Square": "Vierkant",
|
||||
"Stand it up": "Rechtop zetten",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Rechtop zetten: een kwartslag om X, als één Verplaatsen-kenmerk dat u ongedaan kunt maken.",
|
||||
"Stand this part up?": "Dit onderdeel rechtop zetten?",
|
||||
"Start angle": "Beginhoek",
|
||||
"Start bulge": "Beginbolling",
|
||||
"Start continuity": "Begincontinuïteit",
|
||||
"Step": "Stap",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "hoek, hoogte",
|
||||
"appearance.dielectric_ior": "Diëlektrische IOR",
|
||||
"appearance.metallic": "Metaalachtig",
|
||||
"appearance.only_available": "Alleen beschikbare verschijningen",
|
||||
"appearance.pbr_help": "Bekijk dit oppervlak in Render of een product-PNG. Geen breking of transmissie.",
|
||||
"appearance.pbr_opaque": "Ondoorzichtige PBR",
|
||||
"appearance.roughness": "Ruwheid",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "STEP invoegen",
|
||||
"cmd.file_save_mesh": "Opslaan als mesh",
|
||||
"cmd.file_slicers": "Geïnstalleerde slicers",
|
||||
"cmd.frame_member": "Constructiedeel",
|
||||
"cmd.frame_profile_insert": "Profiel invoegen",
|
||||
"cmd.frame_profile_list": "Profielbibliotheek",
|
||||
"cmd.frame_profile_properties": "Profieleigenschappen",
|
||||
"cmd.inspect_accessibility": "Toegankelijkheidsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Zwaartepunt",
|
||||
"cmd.inspect_clearance": "Speling",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Nieuw fysiek materiaal",
|
||||
"cmd.material_delete": "Fysiek materiaal verwijderen",
|
||||
"cmd.material_edit": "Fysiek materiaal bewerken",
|
||||
"cmd.material_inspect": "Fysiek materiaal inspecteren",
|
||||
"cmd.material_list": "Materialen weergeven",
|
||||
"cmd.material_properties_directional": "Richtingsafhankelijke elasticiteitsmodulus",
|
||||
"cmd.material_properties_validate": "Gegevens fysiek materiaal valideren",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Sleutel hier",
|
||||
"motion.key_here_hint": "Voeg een sleutel toe op deze stap met de huidige waarde van de verbinding",
|
||||
"motion.no_collisions": "Geen botsingen in de studie",
|
||||
"motion.plot_quantity": "Aantal",
|
||||
"motion.step_back": "Stap terug",
|
||||
"motion.step_forward": "Stap verder",
|
||||
"motion.to_end": "Naar het einde",
|
||||
@@ -1059,7 +1071,7 @@
|
||||
"search.try": "Typ een opdrachtnaam, een kenmerk in dit ontwerp of een parameter. Om een opdracht met waarden uit te voeren, typt u de id en een JSON-object: {example}",
|
||||
"search.valid": "Geldig",
|
||||
"settings.display_quality_note": "Fijnere niveaus tekenen gebogen vlakken en randen vloeiender en kosten meer tijd om te meshen. Alleen de viewport verandert, nooit het ontwerp of de exports.",
|
||||
"settings.frame_rate_note": "Toont frames per seconde in de rechteronderhoek van de weergave. Zolang dit aan staat, wordt de weergave continu opnieuw getekend.",
|
||||
"settings.frame_rate_note": "Toont frames per seconde in de rechteronderhoek van de weergave.",
|
||||
"settings.measure_axis_distances": "X/Y/Z-afstanden tonen",
|
||||
"settings.measure_axis_distances_help": "Toon de componentmaten langs de wereldassen naast de totale afstand terwijl Meten open is.",
|
||||
"settings.msaa_applies_on_restart": "Anti-aliasing wordt toegepast bij de volgende start van SolidCraft",
|
||||
@@ -1120,7 +1132,50 @@
|
||||
"trace.stopped_by_contact": "Eindigt bij monster {sample}: {a} zou door {b} heen gaan",
|
||||
"trace.stopped_by_loop": "Eindigt bij monster {sample}: de lus via {joint} kan niet verder sluiten",
|
||||
"ui.1_degree_of_freedom": "1 vrijheidsgraad",
|
||||
"ui.3d_mouse_calibrate": "Kalibreren…",
|
||||
"ui.3d_mouse_calibrate_tip": "Beweeg de dop zoals het venster vraagt, zodat SolidCraft leert welke kant elke as van deze 3D-muis op gaat en hoe ver. Opgeslagen voor deze 3D-muis.",
|
||||
"ui.3d_mouse_calibrate_title": "3D-muis kalibreren",
|
||||
"ui.3d_mouse_calibrate_wait": "Beweeg de 3D-muis één keer, zodat SolidCraft hem ziet.",
|
||||
"ui.3d_mouse_calibrated": "Gekalibreerd",
|
||||
"ui.3d_mouse_calibration": "Kalibratie van 3D-muis",
|
||||
"ui.3d_mouse_calibration_done": "Gereed. Sla op om het te gebruiken wanneer deze 3D-muis is aangesloten.",
|
||||
"ui.3d_mouse_calibration_no_response": "Een as bewoog nauwelijks en kan dus niet worden gekalibreerd. Begin opnieuw en beweeg de dop telkens verder.",
|
||||
"ui.3d_mouse_circles_help": "De cirkels tonen wat het apparaat na kalibratie stuurt. Het testmodel toont wat de weergave ermee doet, na omkeren, snelheid en asindeling.",
|
||||
"ui.3d_mouse_dead_zone": "Dode zone",
|
||||
"ui.3d_mouse_dead_zone_tip": "Hoe ver de dop moet bewegen voordat de weergave dat doet, als aandeel van de volledige slag. Verhoog dit als de weergave afdrijft terwijl de dop in rust is.",
|
||||
"ui.3d_mouse_denied": "SolidCraft ziet een 3D-muis maar mag hem niet lezen. Zie de installatienotities voor de 3D-muis voor de udev-regel.",
|
||||
"ui.3d_mouse_denied_short": "Gevonden, maar niet leesbaar",
|
||||
"ui.3d_mouse_driver_fallback": "Voorlopig via spacenavd gelezen: het apparaat zelf kon niet worden geopend.",
|
||||
"ui.3d_mouse_forget": "Kalibratie vergeten",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Viewport links",
|
||||
"ui.3d_mouse_ghost_left_window": "Venster links",
|
||||
"ui.3d_mouse_ghost_position": "Positie",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Viewport rechts",
|
||||
"ui.3d_mouse_hint": "Verplaats en roteer de weergave samen; schetsen gebruiken alleen pannen en zoomen.",
|
||||
"ui.3d_mouse_navigate": "Navigeren met de 3D-muis",
|
||||
"ui.3d_mouse_none": "Geen 3D-muis gevonden",
|
||||
"ui.3d_mouse_not_calibrated": "Niet gekalibreerd",
|
||||
"ui.3d_mouse_offer": "Je 3D-muis kalibreren verhelpt verwisselde of omgekeerde assen en stelt snelheid en gevoel in.",
|
||||
"ui.3d_mouse_offer_never": "Niet meer tonen",
|
||||
"ui.3d_mouse_offer_swap": "Naar Hidraw wisselen (direct)",
|
||||
"ui.3d_mouse_reading": "Uitlezen",
|
||||
"ui.3d_mouse_reset_view": "Weergave resetten",
|
||||
"ui.3d_mouse_restore": "3D-muis standaardwaarden herstellen",
|
||||
"ui.3d_mouse_restore_ask": "De 3D-muisinstellingen terugzetten naar de standaardwaarden? Je kalibratie en knoppentoewijzing blijven staan.",
|
||||
"ui.3d_mouse_speed": "Snelheid van 3D-muis",
|
||||
"ui.3d_mouse_step_lift": "Til de dop omhoog, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_press": "Druk de dop omlaag, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_pull": "Trek de dop naar u toe, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_push": "Duw de dop van u af, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_rest": "Haal uw hand van de dop en laat hem stilstaan.",
|
||||
"ui.3d_mouse_step_slide_left": "Schuif de dop naar links, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_slide_right": "Schuif de dop naar rechts, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_tilt_left": "Kantel de dop naar links (linkerrand omlaag), houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_tilt_right": "Kantel de dop naar rechts (rechterrand omlaag), houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Draai de dop met de klok mee, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Draai de dop tegen de klok in, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_waiting": "Wacht op beweging",
|
||||
"ui.3d_mouse_wiggle": "Als je er een hebt aangesloten, beweeg hem even.",
|
||||
"ui.3d_sketch": "3D-schets",
|
||||
"ui.activate": "Activeren",
|
||||
"ui.active_component": "Actief component",
|
||||
@@ -1182,6 +1237,7 @@
|
||||
"ui.agent_chat_files_left_out": "Maximaal {max} bestanden per bericht; weggelaten: {files}",
|
||||
"ui.agent_chat_files_loading": "Wachten tot de bestanden zijn gelezen",
|
||||
"ui.agent_chat_files_max": "Maximaal {max} bestanden per bericht",
|
||||
"ui.agent_chat_flatpak_none_found": "Geen codeeragent gevonden. De Flatpak-sandbox ziet standaard geen agents die in je persoonlijke map zijn geïnstalleerd en heeft geen netwerktoegang. Voer dit commando uit in een terminal en start SolidCraft daarna opnieuw om Claude Code en de ACP-adapter toe te staan die via nvm of in ~/.local/bin zijn geïnstalleerd. Het geeft toegang tot de genoemde programma’s en aanmeldbestanden, plus netwerktoegang:",
|
||||
"ui.agent_chat_import_failed": "{name}: importeren mislukt: {error}",
|
||||
"ui.agent_chat_intro": "Chat met een codeeragent die u al gebruikt, of met een modeleindpunt naar keuze. De agent werkt aan het geopende ontwerp via de tools van SolidCraft: de wijzigingen verschijnen terwijl ze worden gemaakt en worden per bericht als één stap ongedaan gemaakt. Wat hij zonder te vragen mag doen, stelt u in via Voorkeuren > AI.",
|
||||
"ui.agent_chat_key_in_env": "sleutel ingesteld",
|
||||
@@ -1277,6 +1333,7 @@
|
||||
"ui.ai_http_busy": "het eindpunt is bezet of boven zijn limiet; probeer het later opnieuw",
|
||||
"ui.ai_http_key": "het eindpunt weigerde de sleutel: controleer deze in Voorkeuren > AI",
|
||||
"ui.ai_http_not_found": "geen eindpunt of model gevonden: controleer de basis-URL en de modelnaam",
|
||||
"ui.ai_http_too_large": "het gesprek is te groot voor dit endpoint; begin een nieuwe chat",
|
||||
"ui.ai_key": "API-sleutel",
|
||||
"ui.ai_key_hint": "optioneel; bewaard in de sleutelhanger van het systeem",
|
||||
"ui.ai_key_needs_https": "Een sleutel wordt alleen via HTTPS of naar deze computer verzonden.",
|
||||
@@ -1304,7 +1361,6 @@
|
||||
"ui.ai_permissions": "Machtigingen",
|
||||
"ui.ai_preset": "Voorinstelling",
|
||||
"ui.ai_remove": "Verwijderen",
|
||||
"ui.ai_request_limit": "gestopt na 24 verzoeken voor één bericht; stuur nog een bericht om verder te gaan",
|
||||
"ui.ai_reset": "Herstellen",
|
||||
"ui.ai_settings": "AI-instellingen…",
|
||||
"ui.ai_show_features": "AI-functies tonen",
|
||||
@@ -1480,6 +1536,8 @@
|
||||
"ui.filter_commands_or_keys": "Opdrachten of toetsen filteren",
|
||||
"ui.findInTimeline": "Zoeken in tijdlijn",
|
||||
"ui.findInWindow": "Zoeken in venster",
|
||||
"ui.fine": "Fijn",
|
||||
"ui.finest": "Zeer fijn",
|
||||
"ui.finish_sketch": "Schets voltooien",
|
||||
"ui.fit": "Passend",
|
||||
"ui.fix_unfix": "Vastzetten/losmaken",
|
||||
@@ -1501,6 +1559,7 @@
|
||||
"ui.grid_follows_view_hint": "Zijaanzichten tonen het raster op het rechtopstaande vlak dat naar hen toe gericht is in plaats van op de grond",
|
||||
"ui.ground_shadow": "Grondschaduw",
|
||||
"ui.groupSelected": "Selectie groeperen",
|
||||
"ui.hatch": "Arcering",
|
||||
"ui.heading_add_ins": "INVOEGMODULES",
|
||||
"ui.heading_assemble": "SAMENSTELLEN",
|
||||
"ui.heading_box": "KUB",
|
||||
@@ -1567,6 +1626,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "HULPPROGRAMMA'S",
|
||||
"ui.heading_utility": "HULPPROGRAMMA",
|
||||
"ui.heading_weldments": "LASWERK",
|
||||
"ui.help": "Help",
|
||||
"ui.hide": "Verbergen",
|
||||
"ui.hideAxes": "Assen verbergen",
|
||||
@@ -1703,6 +1763,7 @@
|
||||
"ui.project_face_edges_sketch": "Vlakranden projecteren in nieuwe schetsen",
|
||||
"ui.properties": "Eigenschappen",
|
||||
"ui.pull_direction": "Trekrichting",
|
||||
"ui.quality": "Kwaliteit",
|
||||
"ui.quantity": "Aantal",
|
||||
"ui.quantity_2": "Aantal 2",
|
||||
"ui.quit": "Afsluiten",
|
||||
@@ -1791,40 +1852,13 @@
|
||||
"ui.smooth_wheel_zoom": "Vloeiend zoomen met wiel",
|
||||
"ui.smooth_wheel_zoom_tip": "Uitschakelen om elke wielgebeurtenis direct toe te passen, zonder uitloop",
|
||||
"ui.snap": "Snap",
|
||||
"ui.space_mouse_calibrate": "Kalibreren…",
|
||||
"ui.space_mouse_calibrate_tip": "Beweeg de dop zoals het venster vraagt, zodat SolidCraft leert welke kant elke as van deze 3D-muis op gaat en hoe ver. Opgeslagen voor deze 3D-muis.",
|
||||
"ui.space_mouse_calibrate_title": "3D-muis kalibreren",
|
||||
"ui.space_mouse_calibrated": "Gekalibreerd",
|
||||
"ui.space_mouse_calibration": "Kalibratie van 3D-muis",
|
||||
"ui.space_mouse_calibration_done": "Gereed. Sla op om het te gebruiken wanneer deze 3D-muis is aangesloten.",
|
||||
"ui.space_mouse_calibration_no_response": "Een as bewoog nauwelijks en kan dus niet worden gekalibreerd. Begin opnieuw en beweeg de dop telkens verder.",
|
||||
"ui.space_mouse_dead_zone": "Dode zone",
|
||||
"ui.space_mouse_dead_zone_tip": "Hoe ver de dop moet bewegen voordat de weergave dat doet, als aandeel van de volledige slag. Verhoog dit als de weergave afdrijft terwijl de dop in rust is.",
|
||||
"ui.space_mouse_forget": "Kalibratie vergeten",
|
||||
"ui.space_mouse_hint": "Verplaats en roteer de weergave samen; schetsen gebruiken alleen pannen en zoomen.",
|
||||
"ui.space_mouse_navigate": "Navigeren met SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Geen 3D-muis gevonden",
|
||||
"ui.space_mouse_not_calibrated": "Niet gekalibreerd",
|
||||
"ui.space_mouse_reading": "Uitlezen",
|
||||
"ui.space_mouse_step_lift": "Til de dop omhoog, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_press": "Druk de dop omlaag, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_pull": "Trek de dop naar u toe, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_push": "Duw de dop van u af, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_rest": "Haal uw hand van de dop en laat hem stilstaan.",
|
||||
"ui.space_mouse_step_slide_left": "Schuif de dop naar links, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_slide_right": "Schuif de dop naar rechts, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_tilt_back": "Kantel de dop naar achteren (voorrand omhoog), houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_tilt_forward": "Kantel de dop naar voren (voorrand omlaag), houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_tilt_left": "Kantel de dop naar links (linkerrand omlaag), houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_tilt_right": "Kantel de dop naar rechts (rechterrand omlaag), houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Draai de dop met de klok mee, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Draai de dop tegen de klok in, houd vast en laat dan los.",
|
||||
"ui.spacing": "Tussenruimte",
|
||||
"ui.spacing_2": "Tussenruimte 2",
|
||||
"ui.sphere": "Bol",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Lichaam splitsen",
|
||||
"ui.stand_up_tip": "Zet na het openen van een STEP- of IGES-bestand met Z omhoog een kwartslag om X aan",
|
||||
"ui.standard": "Standaard",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Opnieuw beginnen",
|
||||
"ui.start_page": "Startpagina",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Pending UI strings
|
||||
|
||||
A pull request that adds UI strings adds **one new file here** instead of editing
|
||||
`assets/locales/<locale>.json`. Two pull requests then never touch the same file, so they never
|
||||
conflict.
|
||||
|
||||
- Name it after the change, lowercase with dashes: `sketch-trim-hints.json`.
|
||||
- Contents: a map from locale code to `{"message id": "text"}`. Give English; add other locales
|
||||
you can translate (a locale you leave out falls back to English, as before):
|
||||
|
||||
```json
|
||||
{
|
||||
"en": { "sketch.trim.hint": "Click a segment to trim it" },
|
||||
"de": { "sketch.trim.hint": "Segment zum Kürzen anklicken" }
|
||||
}
|
||||
```
|
||||
|
||||
- The id must not already be in that locale's catalog, nor in another pending file; the build and
|
||||
`cargo xtask lists` fail otherwise. To change an existing message, edit the catalog itself.
|
||||
- Don't sort or fold anything. The app, `cargo xtask i18n` and the tests read these files as part of
|
||||
the catalogs, so your strings work in your own pull request.
|
||||
- After a merge, the Generated files workflow runs `cargo xtask fold-pending` on `main`: it adds the
|
||||
messages to the catalogs (sorted, one per line) and deletes the files. Never run it in a pull
|
||||
request.
|
||||
@@ -28,6 +28,7 @@
|
||||
"Also deletes": "Usuwa również",
|
||||
"Angle 2": "Kąt 2",
|
||||
"Animate": "Animuj",
|
||||
"App/system information and command settings are included. Review private details before sharing.": "Dołączone są informacje o aplikacji i systemie oraz ustawienia poleceń. Przed udostępnieniem sprawdź, czy nie ma tam prywatnych danych.",
|
||||
"Appearance name": "Nazwa wyglądu",
|
||||
"Applied Radius": "Zastosowany promień",
|
||||
"Apply To": "Zastosuj do",
|
||||
@@ -72,6 +73,7 @@
|
||||
"Choose…": "Wybierz…",
|
||||
"Circular": "Kołowy",
|
||||
"Circumference": "Obwód",
|
||||
"Clockwise": "Zgodnie z ruchem wskazówek zegara",
|
||||
"Close draft": "Zamknij szkic zgłoszenia",
|
||||
"Collisions": "Kolizje",
|
||||
"Colour": "Kolor",
|
||||
@@ -83,6 +85,7 @@
|
||||
"Contact": "Kontakt",
|
||||
"Contact Sets": "Zestawy kontaktów",
|
||||
"Contact sets": "Zestawy kontaktów",
|
||||
"Continue on GitHub": "Kontynuuj na GitHubie",
|
||||
"Continuity": "Ciągłość",
|
||||
"Copy Error Message": "Kopiuj komunikat błędu",
|
||||
"Copy Warning Message": "Kopiuj komunikat ostrzeżenia",
|
||||
@@ -102,6 +105,7 @@
|
||||
"Curv. Comb": "Grzebień krzyw.",
|
||||
"Curvature (G2)": "Krzywizna (G2)",
|
||||
"Curvature change (G3)": "Zmiana krzywizny (G3)",
|
||||
"Custom": "Niestandardowy",
|
||||
"Customize Toolbar": "Dostosuj pasek narzędzi",
|
||||
"Delete": "Usuń",
|
||||
"Delete all": "Usuń wszystko",
|
||||
@@ -110,6 +114,8 @@
|
||||
"Delete the set": "Usuń zestaw",
|
||||
"Density": "Gęstość",
|
||||
"Dependents": "Elementy zależne",
|
||||
"Describe the problem you saw": "Opisz zauważony problem",
|
||||
"Describe what happened before continuing.": "Zanim przejdziesz dalej, opisz, co się stało.",
|
||||
"Designs you open or save appear here.": "Tutaj pojawiają się otwierane lub zapisywane projekty.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Wynika z opcji Do obiektu. Wybierz Kąt, aby edytować zakres kątowy.",
|
||||
"Direction": "Kierunek",
|
||||
@@ -118,6 +124,11 @@
|
||||
"Don't ask again": "Nie pytaj ponownie",
|
||||
"Double-click to rename": "Kliknij dwukrotnie, aby zmienić nazwę",
|
||||
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Dwukrotne kliknięcie środkowym przyciskiem w widoku: Widok główny (kadruje to, co widoczne), Dopasuj (kadruje to, co widoczne, zachowując orientację) lub nic",
|
||||
"Drag to move the boss across the model. It snaps to sketch points on the face.": "Przeciągnij, aby przesunąć występ po modelu. Przyciąga się do punktów szkicu na ścianie.",
|
||||
"Drag to move the rest across the model. It snaps to sketch points on the face.": "Przeciągnij, aby przesunąć podparcie po modelu. Przyciąga się do punktów szkicu na ścianie.",
|
||||
"Drag to move the snap fit across the model. It snaps to sketch points on the face.": "Przeciągnij, aby przesunąć zatrzask po modelu. Przyciąga się do punktów szkicu na ścianie.",
|
||||
"Drag to point the catch in another direction.": "Przeciągnij, aby skierować zaczep w inną stronę.",
|
||||
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.": "Przeciągnij, aby obrócić podparcie co 5°. Przytrzymaj Alt lub Ctrl, aby obracać swobodnie.",
|
||||
"Drive Joint": "Steruj złączem",
|
||||
"Duration": "Czas trwania",
|
||||
"Edges": "Krawędzie",
|
||||
@@ -127,6 +138,7 @@
|
||||
"Enabled": "Włączone",
|
||||
"End bulge": "Wybrzuszenie końca",
|
||||
"End continuity": "Ciągłość końca",
|
||||
"Expected result (optional)": "Oczekiwany wynik (opcjonalnie)",
|
||||
"Explode": "Rozstrzel",
|
||||
"Explode Text": "Rozbij tekst",
|
||||
"Export Animation…": "Eksportuj animację…",
|
||||
@@ -168,6 +180,7 @@
|
||||
"Height": "Wysokość",
|
||||
"Hidden edges": "Krawędzie niewidoczne",
|
||||
"History layout": "Układ historii",
|
||||
"History names": "Nazwy w historii",
|
||||
"Holes": "Otwory",
|
||||
"Holes (edges on them)": "Otwory (krawędzie na nich)",
|
||||
"Home view": "Widok główny",
|
||||
@@ -234,6 +247,7 @@
|
||||
"No matching appearance presets.": "Brak pasujących ustawień wyglądu.",
|
||||
"No matching appearances": "Brak pasujących wyglądów",
|
||||
"No visible bodies or sketches to export.": "Brak widocznych brył lub szkiców do wyeksportowania.",
|
||||
"Nominal diameter": "Średnica nominalna",
|
||||
"None": "Brak",
|
||||
"Not available yet": "Jeszcze niedostępne",
|
||||
"Notch": "Wycięcie",
|
||||
@@ -242,7 +256,10 @@
|
||||
"Open GitHub draft": "Otwórz szkic zgłoszenia na GitHubie",
|
||||
"Open local libraries in the desktop app. You can export this design's library here.": "Otwórz biblioteki lokalne w aplikacji komputerowej. Tutaj możesz wyeksportować bibliotekę tego projektu.",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Otwartoźródłowy parametryczny CAD 3D w języku Rust. Licencja MIT OR Apache-2.0.",
|
||||
"Opens your browser and copies the report text for you to paste into GitHub.": "Otwiera przeglądarkę internetową i kopiuje treść raportu, którą wkleisz na GitHubie.",
|
||||
"Opens your browser. Review and submit your report on GitHub.": "Otwiera przeglądarkę internetową. Sprawdź raport i wyślij go na GitHubie.",
|
||||
"Operator": "Operator",
|
||||
"Orbit (right drag)": "Obracanie widoku (przeciąganie prawym przyciskiem)",
|
||||
"Other side": "Druga strona",
|
||||
"Other surface (mutual trim)": "Druga powierzchnia (wzajemne przycięcie)",
|
||||
"Outside": "Na zewnątrz",
|
||||
@@ -257,6 +274,7 @@
|
||||
"Pick the tool bodies": "Wybierz bryły narzędziowe",
|
||||
"Picked": "Wybrane",
|
||||
"Picked point": "Wybrany punkt",
|
||||
"Pitch": "Skok",
|
||||
"Place at the origin instead": "Zamiast tego umieść w początku układu",
|
||||
"Plastic Rules": "Reguły tworzyw",
|
||||
"Playback": "Odtwarzanie",
|
||||
@@ -290,6 +308,8 @@
|
||||
"Remove the track": "Usuń ścieżkę",
|
||||
"Remove this point": "Usuń ten punkt",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "Zgłoś błąd — sprawdź zrzut ekranu i otwórz szkic zgłoszenia na GitHubie",
|
||||
"Report text copied. Paste it into GitHub and submit your report there.": "Skopiowano treść raportu. Wklej ją na GitHubie i tam wyślij raport.",
|
||||
"Reports are public. A GitHub account is required.": "Raporty są publiczne. Wymagane jest konto GitHub.",
|
||||
"Reset Toolbar": "Przywróć pasek narzędzi",
|
||||
"Resize Part": "Zmień rozmiar części",
|
||||
"Restore View": "Przywróć widok",
|
||||
@@ -304,6 +324,7 @@
|
||||
"Revert Pos.": "Przywróć poł.",
|
||||
"Revert Position": "Przywróć położenie",
|
||||
"Review the report and screenshot before opening GitHub.": "Przed otwarciem GitHuba sprawdź raport i zrzut ekranu.",
|
||||
"Review your report on GitHub and click Submit new issue to publish it.": "Sprawdź raport na GitHubie i kliknij Submit new issue, aby go opublikować.",
|
||||
"Rim Edge": "Krawędź obrzeża",
|
||||
"Roll History Marker Here": "Przesuń tutaj znacznik historii",
|
||||
"Round": "Okrągłe",
|
||||
@@ -314,14 +335,17 @@
|
||||
"SURFACE QUALITY": "JAKOŚĆ POWIERZCHNI",
|
||||
"Save Rule": "Zapisz regułę",
|
||||
"Save View": "Zapisz widok",
|
||||
"Save screenshot…": "Zapisz zrzut ekranu…",
|
||||
"Save the study as an animated GIF, seen as the viewport shows it": "Zapisz badanie jako animowany GIF, tak jak widać je w widoku",
|
||||
"Saved preview. Geometry is being checked before editing.": "Zapisany podgląd. Geometria jest sprawdzana przed edycją.",
|
||||
"Scale factor": "Współczynnik skali",
|
||||
"Scene": "Scena",
|
||||
"Screenshot (optional)": "Zrzut ekranu (opcjonalnie)",
|
||||
"Search": "Szukaj",
|
||||
"Search commands": "Szukaj poleceń",
|
||||
"Search this library": "Przeszukaj tę bibliotekę",
|
||||
"Section": "Przekrój",
|
||||
"Section position": "Położenie przekroju",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Wybierz inny: przytrzymaj nieruchomo lewy przycisk na modelu (lub prawy przycisk › Wybierz inny…), aby wyświetlić wszystko pod kursorem; kliknij ponownie w tym samym miejscu, aby zaznaczyć kolejny obiekt. Dwukrotne kliknięcie środkowym przyciskiem przechodzi do widoku głównego (Preferencje › Nawigacja).",
|
||||
"Select bodies (a face picks its body)": "Zaznacz bryły (ściana wybiera swoją bryłę)",
|
||||
"Select bodies or faces instead": "Zamiast tego zaznacz bryły lub ściany",
|
||||
@@ -333,6 +357,7 @@
|
||||
"Show Agent Cursor": "Pokaż kursor agenta",
|
||||
"Show in folder": "Pokaż w folderze",
|
||||
"Show the pointer an AI agent moves (#35)": "Pokaż wskaźnik poruszany przez agenta AI (#35)",
|
||||
"Show tool at cursor": "Pokaż narzędzie przy kursorze",
|
||||
"Signed axes": "Osie ze znakiem",
|
||||
"Size": "Rozmiar",
|
||||
"Sketch constraints": "Wiązania szkicu",
|
||||
@@ -345,24 +370,30 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft zamknął się, zanim te projekty zostały zapisane. Można otworzyć ich ostatni autozapis.",
|
||||
"Specialise": "Uszczegółów",
|
||||
"Speed": "Prędkość",
|
||||
"Square": "Kwadrat",
|
||||
"Stand it up": "Postaw",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Postaw: obrót o ćwierć obrotu wokół osi X, jako jedna funkcja Przesuń, którą można cofnąć.",
|
||||
"Stand this part up?": "Postawić tę część?",
|
||||
"Start angle": "Kąt początkowy",
|
||||
"Start bulge": "Wybrzuszenie początku",
|
||||
"Start continuity": "Ciągłość początku",
|
||||
"Step": "Krok",
|
||||
"Steps": "Kroki",
|
||||
"Steps to reproduce (optional)": "Kroki do odtworzenia (opcjonalnie)",
|
||||
"Stitch the kept pieces": "Zszyj zachowane części",
|
||||
"Stitch to both bodies": "Zszyj z obiema bryłami",
|
||||
"Study": "Badanie",
|
||||
"Style": "Styl",
|
||||
"Summary": "Podsumowanie",
|
||||
"Suppressed": "Wygaszone",
|
||||
"Surface": "Powierzchnia",
|
||||
"Swap Ends": "Zamień końce",
|
||||
"TRIM": "PRZYCINANIE",
|
||||
"Tangent (G1)": "Styczność (G1)",
|
||||
"Tangent Chain": "Łańcuch styczny",
|
||||
"Tap drill diameter": "Średnica wiertła pod gwint",
|
||||
"Tear": "Rozdarcie",
|
||||
"Technical details": "Szczegóły techniczne",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Przerywany szkic to zapisana płaszczyzna pomocnicza. Wybierz nową ścianę, aby naprawić zaczepienie.",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Brak zapisanego wystąpienia autora. Przed ponownym powiązaniem wybierz prawidłowe położenie autora.",
|
||||
"Then": "Wtedy",
|
||||
@@ -374,6 +405,7 @@
|
||||
"This child is shared. Deleting it removes it from every copy of its parent.": "Ten element podrzędny jest współdzielony. Usunięcie go usuwa go z każdej kopii elementu nadrzędnego.",
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "Ten element podrzędny jest współdzielony. Zmiany położenia dotyczą każdej kopii elementu nadrzędnego.",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Ta ściana nie zapewnia trwałego zaczepienia. Wybierz inną ścianę lub jawnie użyj stałej płaszczyzny.",
|
||||
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Ten obraz nie jest jeszcze dołączony na GitHubie. Skopiuj go lub zapisz, a potem dołącz na GitHubie.",
|
||||
"To Offset": "Do odsunięcia",
|
||||
"To edges": "Do krawędzi",
|
||||
"Tolerance": "Tolerancja",
|
||||
@@ -399,6 +431,9 @@
|
||||
"Wall": "Ścianka",
|
||||
"Wall Location": "Położenie ścianki",
|
||||
"Wall Thickness": "Grubość ścianki",
|
||||
"What did you do just before the problem? A short list is enough": "Co robiłeś tuż przed wystąpieniem problemu? Wystarczy krótka lista",
|
||||
"What did you expect to see?": "Czego się spodziewałeś?",
|
||||
"What happened?": "Co się stało?",
|
||||
"When": "Gdy",
|
||||
"Width": "Szerokość",
|
||||
"Will fail": "Nie powiedzie się",
|
||||
@@ -409,9 +444,33 @@
|
||||
"Z angle": "Kąt Z",
|
||||
"Z distance": "Odległość Z",
|
||||
"Zebra": "Zebra",
|
||||
"ai.prefs.availability": "Sterowanie asystentem",
|
||||
"ai.prefs.background_commands": "Uruchamiaj polecenia agenta w tle",
|
||||
"ai.prefs.follow_camera": "Obracaj kamerę do szkiców",
|
||||
"ai.prefs.follow_camera_off": "Automatyczne podążanie kamery jest wyłączone.",
|
||||
"ai.prefs.follow_camera_on": "Kamera obraca się przodem do szkiców tworzonych przez agenta.",
|
||||
"ai.prefs.intro": "Agenty, uprawnienia i sposób pokazywania ich pracy.",
|
||||
"ai.prefs.make_default": "Używaj domyślnie",
|
||||
"ai.prefs.no_agents": "Brak agentów. Dodaj agenta lub odśwież, aby wyszukać zainstalowane agenty.",
|
||||
"ai.prefs.no_saved_approvals": "Brak zapisanych zgód.",
|
||||
"ai.prefs.override_all_warning": "Ten agent może zapisywać, nadpisywać, eksportować i zastępować projekt bez pytania.",
|
||||
"ai.prefs.permission_all": "Zezwalaj na wszystko",
|
||||
"ai.prefs.permission_all_summary": "Dostępne narzędzia SolidCraft działają bez pytania.",
|
||||
"ai.prefs.permission_ask": "Pytaj przed zmianami",
|
||||
"ai.prefs.permission_ask_summary": "Zatwierdzaj każdą zmianę projektu i każdą operację na plikach.",
|
||||
"ai.prefs.permission_edits": "Zezwalaj na edycję modelu",
|
||||
"ai.prefs.permission_edits_summary": "Edycja modelu działa bez pytania; operacje na plikach i nieodwracalne nadal wymagają pytania.",
|
||||
"ai.prefs.permission_scope": "Dotyczy tylko narzędzi SolidCraft. Powłoka agenta i edycja plików podlegają jego własnym uprawnieniom.",
|
||||
"ai.prefs.pointer_preview_hidden": "Wskaźnik ukryty",
|
||||
"ai.prefs.pointer_preview_speed": "Wskaźnik agenta · {speed}",
|
||||
"ai.prefs.pointer_speed": "Szybkość ruchu",
|
||||
"ai.prefs.privacy_note": "Rozmowę otrzymuje tylko wybrany przez Ciebie agent lub punkt końcowy. Klucze punktów końcowych są przechowywane w systemowym pęku kluczy.",
|
||||
"ai.prefs.show_pointer": "Pokaż wskaźnik agenta",
|
||||
"ai.prefs.turn_limit_hint": "Maksymalna liczba wywołań narzędzi zmieniających projekt, które asystent może wykonać dla jednej wiadomości.",
|
||||
"angle, height": "kąt, wysokość",
|
||||
"appearance.dielectric_ior": "Współczynnik załamania dielektryka (IOR)",
|
||||
"appearance.metallic": "Metaliczność",
|
||||
"appearance.only_available": "Tylko dostępne wyglądy",
|
||||
"appearance.pbr_help": "Zobacz tę powierzchnię w renderowaniu lub w obrazie PNG produktu. Bez załamania i przepuszczania światła.",
|
||||
"appearance.pbr_opaque": "Nieprzezroczysty PBR",
|
||||
"appearance.roughness": "Chropowatość",
|
||||
@@ -514,6 +573,7 @@
|
||||
"cmd.document_metadata": "Właściwości projektu",
|
||||
"cmd.document_metadata_remove": "Usuń właściwość projektu",
|
||||
"cmd.document_metadata_set": "Zmień właściwości projektu",
|
||||
"cmd.document_revision": "Wersja projektu",
|
||||
"cmd.document_units": "Zmień jednostki projektu",
|
||||
"cmd.edit_checkpoint": "Punkt kontrolny",
|
||||
"cmd.edit_restore_checkpoint": "Przywróć punkt kontrolny",
|
||||
@@ -530,6 +590,10 @@
|
||||
"cmd.file_insert_step": "Wstaw STEP",
|
||||
"cmd.file_save_mesh": "Zapisz jako siatkę",
|
||||
"cmd.file_slicers": "Zainstalowane slicery",
|
||||
"cmd.frame_member": "Element ramy",
|
||||
"cmd.frame_profile_insert": "Wstaw profil",
|
||||
"cmd.frame_profile_list": "Biblioteka profili",
|
||||
"cmd.frame_profile_properties": "Właściwości profilu",
|
||||
"cmd.inspect_accessibility": "Analiza dostępności",
|
||||
"cmd.inspect_center_of_mass": "Środek masy",
|
||||
"cmd.inspect_clearance": "Prześwit",
|
||||
@@ -569,6 +633,7 @@
|
||||
"cmd.material_create": "Nowy materiał fizyczny",
|
||||
"cmd.material_delete": "Usuń materiał fizyczny",
|
||||
"cmd.material_edit": "Edytuj materiał fizyczny",
|
||||
"cmd.material_inspect": "Sprawdź materiał fizyczny",
|
||||
"cmd.material_list": "Lista materiałów",
|
||||
"cmd.material_properties_directional": "Kierunkowy moduł sprężystości",
|
||||
"cmd.material_properties_validate": "Sprawdź dane materiału fizycznego",
|
||||
@@ -759,6 +824,7 @@
|
||||
"cmd.view_graphics_preset": "Ustawienie graficzne",
|
||||
"cmd.view_list": "Lista widoków nazwanych",
|
||||
"cmd.view_navigate": "Nawiguj widokiem",
|
||||
"cmd.view_normal_to": "Prostopadle do zaznaczenia",
|
||||
"cmd.view_rename": "Zmień nazwę widoku nazwanego",
|
||||
"cmd.view_save": "Nowy widok nazwany",
|
||||
"color.copy": "Kliknij, aby skopiować wartości koloru",
|
||||
@@ -856,6 +922,12 @@
|
||||
"help.licences": "Licencje",
|
||||
"help.mit": "Licencja MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.solid.chamfer": "Ścina zaznaczone krawędzie płaską fazą o zadanej odległości. Służy do łamania ostrych krawędzi lub ułatwienia wprowadzenia kołka czy śruby.",
|
||||
"help.solid.extrude": "Wyciąga profil szkicu w 3D na zadaną odległość, dodając lub usuwając materiał. Służy do zamiany płaskiego obrysu w blok, występ lub kieszeń.",
|
||||
"help.solid.fillet": "Zaokrągla zaznaczone krawędzie gładkim łukiem o zadanym promieniu. Służy do złagodzenia ostrych krawędzi dla wytrzymałości, wygody lub wyglądu.",
|
||||
"help.solid.hole": "Wierci okrągły otwór o zadanej średnicy w wybranym punkcie ściany. Służy do otworów przelotowych pod śruby, kołki i innych prostych okrągłych otworów.",
|
||||
"help.solid.revolve": "Obraca profil szkicu wokół osi, tworząc okrągłą bryłę. Służy do wałków, gałek, misek i wszystkich części okrągłych względem osi.",
|
||||
"help.solid.shell": "Wydrąża bryłę, pozostawiając ścianki o zadanej grubości, i może otworzyć wybrane ściany. Służy do pudełek, pokryw i innych części cienkościennych.",
|
||||
"help.third_party": "Licencje stron trzecich",
|
||||
"home.back": "Wróć do {name}",
|
||||
"home.crashes": "Raporty awarii",
|
||||
@@ -960,11 +1032,14 @@
|
||||
"motion.key_here": "Wstaw klatkę kluczową tutaj",
|
||||
"motion.key_here_hint": "Dodaj klatkę kluczową w tym kroku z bieżącą wartością złącza",
|
||||
"motion.no_collisions": "Brak kolizji w trakcie badania",
|
||||
"motion.plot_quantity": "Liczba",
|
||||
"motion.step_back": "Krok wstecz",
|
||||
"motion.step_forward": "Krok naprzód",
|
||||
"motion.to_end": "Na koniec",
|
||||
"motion.to_start": "Na początek",
|
||||
"motion.track_name": "Ścieżka {number}",
|
||||
"nav.free_orbit.help": "Mysz i mysz 3D obracają widok przez oba bieguny i zachowują przechylenie. Przyciąganie do osi z Shift jest wyłączone.",
|
||||
"nav.free_orbit.label": "Swobodne obracanie",
|
||||
"nav.touchpad.help": "Przytrzymaj Shift i poruszaj wskaźnikiem, aby przesuwać; przytrzymaj Alt/Option i poruszaj nim, aby obracać widok. Przycisk myszy nie jest potrzebny. Przewijanie w pionie powiększa.",
|
||||
"nav.touchpad.label": "Touchpad",
|
||||
"nav.touchpad.orbit": "Obracanie widoku (przytrzymaj Alt/Option i poruszaj; bez przycisku myszy)",
|
||||
@@ -987,8 +1062,131 @@
|
||||
"pattern.flip_direction": "Odwróć kierunek",
|
||||
"pick a hole to seat it, or OK places it at the origin": "wybierz otwór, aby go osadzić, lub OK umieszcza go w początku układu",
|
||||
"pick one or two vertices, edges or faces": "wybierz jeden lub dwa wierzchołki, krawędzie lub ściany",
|
||||
"prefs.add_item": "Dodaj element",
|
||||
"prefs.alt_frees_manipulators": "Alt / Option zwalnia również strzałki manipulatora",
|
||||
"prefs.anti_aliasing_note": "Wygładza poszarpane krawędzie.",
|
||||
"prefs.applies_on_restart": "Działa po ponownym uruchomieniu",
|
||||
"prefs.assistant": "Asystent",
|
||||
"prefs.axes": "Osie",
|
||||
"prefs.axis": "Oś",
|
||||
"prefs.bottom": "Dół",
|
||||
"prefs.browser": "Przeglądarka",
|
||||
"prefs.built_in": "Wbudowana",
|
||||
"prefs.button_left": "Przeciąganie lewym",
|
||||
"prefs.button_middle": "Przeciąganie środkowym",
|
||||
"prefs.button_right": "Przeciąganie prawym",
|
||||
"prefs.choose_font": "Wybierz plik czcionki…",
|
||||
"prefs.click": "Kliknięcie",
|
||||
"prefs.contact_shading": "Miękkie cieniowanie kontaktowe",
|
||||
"prefs.contact_shading_note": "Miękkie cieniowanie tam, gdzie części są blisko siebie.",
|
||||
"prefs.context_menu": "Menu kontekstowe",
|
||||
"prefs.context_menu_ring": "Menu kontekstowe · przytrzymaj, aby otworzyć menu promieniste",
|
||||
"prefs.cpu": "CPU (programowo)",
|
||||
"prefs.crash_reports": "Otwórz raporty o awariach…",
|
||||
"prefs.crash_reports_note": "Raporty są przechowywane na tym komputerze, razem z projektem i poleceniami, które odtwarzają problem.",
|
||||
"prefs.ctrl_cmd": "Ctrl / Cmd",
|
||||
"prefs.curved_surfaces": "Powierzchnie zakrzywione",
|
||||
"prefs.curves": "Powierzchnie zakrzywione",
|
||||
"prefs.dead_zone_note": "Zakreskowany pas to strefa martwa: ruch wewnątrz niej jest pomijany.",
|
||||
"prefs.default": "domyślnie",
|
||||
"prefs.design": "Ustawienia domyślne projektu",
|
||||
"prefs.design_note": "Ustawienia, z którymi startują nowe projekty. Otwarte projekty zachowują własne.",
|
||||
"prefs.devices": "Mysz 3D",
|
||||
"prefs.display": "Wyświetlanie",
|
||||
"prefs.edges": "Krawędzie i cieniowanie",
|
||||
"prefs.extend": "Rozszerz",
|
||||
"prefs.extra_navigation": "Ctrl+przeciąganie lewym na pustym miejscu obraca widok; przytrzymaj Spację, aby przesuwać.",
|
||||
"prefs.follows_os": "Zgodnie z systemem",
|
||||
"prefs.font": "Czcionka",
|
||||
"prefs.font_fallback": "Znaki, których brakuje w wybranej czcionce, są brane z czcionki wbudowanej, więc żadna etykieta nie pokaże pustego prostokąta.",
|
||||
"prefs.font_sample": "Zażółć gęślą jaźń · Ø12.50 mm · 45.0° · 0123456789",
|
||||
"prefs.font_unreadable": "Nie można odczytać wybranej czcionki.",
|
||||
"prefs.general": "Ogólne",
|
||||
"prefs.general_note": "Język, zapisywanie i aktualizacje.",
|
||||
"prefs.gesture_orbit": "Obracanie widoku",
|
||||
"prefs.gesture_pan": "Przesuwanie",
|
||||
"prefs.gesture_zoom": "Powiększanie",
|
||||
"prefs.graphics": "Grafika",
|
||||
"prefs.graphics_note": "Wybierz między szybkością a gładkością. Każda opcja pokazuje swój efekt na przykładzie.",
|
||||
"prefs.graphics_presets_note": "Ustawienia wstępne zmieniają razem poniższe opcje.",
|
||||
"prefs.history": "Historia",
|
||||
"prefs.hold_stop_snapping": "Przytrzymaj, aby wyłączyć przyciąganie",
|
||||
"prefs.icons_only": "Tylko ikony",
|
||||
"prefs.imported_file": "Zaimportowany plik",
|
||||
"prefs.input": "Wprowadzanie",
|
||||
"prefs.interface": "Interfejs",
|
||||
"prefs.interface_language": "Język interfejsu",
|
||||
"prefs.interface_note": "Wygląd SolidCraft i rozmieszczenie jego elementów.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.labels": "Etykiety",
|
||||
"prefs.layout": "Układ",
|
||||
"prefs.layout_note": "Kompaktowy dokuje panele do krawędzi okna. Przezroczystość przeglądarki jest dostępna w układzie Pływający. Stronę asystenta zmienisz na stronie Asystent.",
|
||||
"prefs.left": "Lewo",
|
||||
"prefs.left_click": "Kliknięcie lewym",
|
||||
"prefs.live_input": "Bieżące wejście",
|
||||
"prefs.live_preview": "Podgląd na żywo",
|
||||
"prefs.minutes": "minut",
|
||||
"prefs.minutes_suffix": " min",
|
||||
"prefs.mouse": "Mysz i gładzik",
|
||||
"prefs.mouse_heading": "Mysz",
|
||||
"prefs.mouse_mapping": "Przypisanie przycisków myszy",
|
||||
"prefs.navigation": "Nawigacja",
|
||||
"prefs.navigation_note": "Jak mysz, gładzik i mysz 3D poruszają widokiem.",
|
||||
"prefs.not_on_gpu": "niedostępne na tym GPU",
|
||||
"prefs.on": "Włączone",
|
||||
"prefs.opaque": "Nieprzezroczysta",
|
||||
"prefs.orbit_snap": "Przytrzymaj Shift podczas obracania widoku, aby przyciągnąć do osi.",
|
||||
"prefs.orientation": "Orientacja",
|
||||
"prefs.overall_speed": "Szybkość ogólna",
|
||||
"prefs.placement": "Umieszczanie",
|
||||
"prefs.preview_choices": "Podgląd wybranych opcji",
|
||||
"prefs.quality_default": "domyślnie",
|
||||
"prefs.quality_fastest": "najszybciej",
|
||||
"prefs.quality_slowest": "najwolniej",
|
||||
"prefs.quality_smoother": "gładziej",
|
||||
"prefs.reports_saved": "Zapisane: {n}",
|
||||
"prefs.right_click": "Kliknięcie prawym",
|
||||
"prefs.saving": "Autozapis i odzyskiwanie",
|
||||
"prefs.scale_note": "Także Ctrl + / Ctrl − w oknie. Przyciski mają 28 px przy 100%.",
|
||||
"prefs.scale_text": "Skala i tekst",
|
||||
"prefs.select": "Zaznacz",
|
||||
"prefs.select_one_item": "Zaznacz jeden element",
|
||||
"prefs.selecting": "Zaznaczanie",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Shift tylko dodaje; Ctrl / Cmd przełącza",
|
||||
"prefs.shift_add_note": "Włączone: Shift dodaje, Ctrl / Cmd przełącza. Wyłączone (domyślnie): oba przełączają.",
|
||||
"prefs.shortcuts": "Skróty",
|
||||
"prefs.sketch_selection": "Szkic i zaznaczanie",
|
||||
"prefs.sketch_selection_heading": "Szkic i zaznaczanie",
|
||||
"prefs.sketch_selection_note": "Jak działa przyciąganie w szkicach i jak kliknięcia zaznaczają.",
|
||||
"prefs.sketching": "Szkicowanie",
|
||||
"prefs.snap_drawing_note": "Podczas rysowania lub przeciągania punktu szkicu.",
|
||||
"prefs.source": "Źródło",
|
||||
"prefs.speed": "Szybkość",
|
||||
"prefs.stand_up_note": "Gdy plik STEP lub IGES otworzy się z osią Z w górę, proponuj obrót o ćwierć obrotu wokół osi X.",
|
||||
"prefs.status_done": "Gotowe",
|
||||
"prefs.status_error": "Błąd",
|
||||
"prefs.status_selected": "Zaznaczone",
|
||||
"prefs.status_warning": "Ostrzeżenie",
|
||||
"prefs.supersampling_brief": "Rysuje widok w większym rozmiarze i go zmniejsza: bardzo ostro, ale bardziej obciąża.",
|
||||
"prefs.tab_accent_note": "Kolor akcentu podąża za aktywną kartą",
|
||||
"prefs.theme_file": "plik motywu",
|
||||
"prefs.timeline_half_hour": "−30 min",
|
||||
"prefs.timeline_now": "teraz",
|
||||
"prefs.timeline_quarter_hour": "−15",
|
||||
"prefs.toggle_item": "Dodaj element lub usuń go ponownie",
|
||||
"prefs.toolbar": "Pasek narzędzi",
|
||||
"prefs.translucent": "Półprzezroczysta",
|
||||
"prefs.under_browser": "Pod przeglądarką",
|
||||
"prefs.under_cursor": "Pod kursorem",
|
||||
"prefs.units_precision": "Jednostki i dokładność",
|
||||
"prefs.updates": "Aktualizacje",
|
||||
"prefs.view": "Zachowanie widoku",
|
||||
"prefs.viewport": "Widok",
|
||||
"prefs.wheel": "Kółko",
|
||||
"prefs.wheel_trackpad": "Kółko i gładzik",
|
||||
"prefs.workspace": "Środowisko pracy",
|
||||
"prefs.zoom_toward_cursor": "Powiększanie w stronę kursora",
|
||||
"properties.title": "Właściwości: {name}",
|
||||
"recovery.days_ago": "{count} dni temu",
|
||||
"recovery.detail": "{file} · elementy: {features} · autozapis {when}",
|
||||
@@ -1060,12 +1258,14 @@
|
||||
"search.try": "Wpisz nazwę polecenia, element tego projektu lub parametr. Aby uruchomić polecenie z wartościami, wpisz jego identyfikator i obiekt JSON: {example}",
|
||||
"search.valid": "Prawidłowe",
|
||||
"settings.display_quality_note": "Wyższe poziomy rysują zakrzywione ściany i krawędzie gładziej, ale siatka powstaje dłużej. Zmienia się tylko widok, nigdy projekt ani eksport.",
|
||||
"settings.frame_rate_note": "Pokazuje liczbę klatek na sekundę w prawym dolnym rogu widoku. Gdy jest włączone, widok odświeża się bez przerwy.",
|
||||
"settings.frame_rate_note": "Pokazuje liczbę klatek na sekundę w prawym dolnym rogu widoku.",
|
||||
"settings.measure_axis_distances": "Pokaż odległości X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Gdy pomiar jest otwarty, pokazuj składowe wzdłuż osi globalnych obok odległości całkowitej.",
|
||||
"settings.msaa_applies_on_restart": "Wygładzanie krawędzi zadziała po ponownym uruchomieniu SolidCraft",
|
||||
"settings.msaa_reset": "{setting}: {requested}× nie jest obsługiwane przez to urządzenie graficzne; ustawiono {reset}× (obsługiwane: {supported}).",
|
||||
"settings.msaa_unsupported": "Nieobsługiwane przez to urządzenie graficzne",
|
||||
"settings.reference_geometry_through_bodies": "Pokaż geometrię odniesienia przez bryły",
|
||||
"settings.reference_geometry_through_bodies_help": "Rysuj kontury nieaktywnych szkiców i płaszczyzny odniesienia na cieniowanych bryłach",
|
||||
"settings.reset_intro": "Te ustawienia nie działają na tym komputerze i uniemożliwiłyby uruchomienie SolidCraft, więc zostały zmienione:",
|
||||
"settings.reset_title": "Zmieniono ustawienia",
|
||||
"settings.supersampling_note": "Rysuje widok w wyższej rozdzielczości i go zmniejsza: gładsze krawędzie, linie i siatka na ekranach poniżej 4K. Kosztuje czas GPU: ok. 2,25× więcej pikseli przy 1,5× i 4× przy 2×.",
|
||||
@@ -1105,6 +1305,9 @@
|
||||
"thread.designation": "Oznaczenie",
|
||||
"thread.iso_metric": "Metryczny ISO",
|
||||
"thread.select_size": "Wybierz rozmiar",
|
||||
"timeline.delete_after_marker_one": "Usuń 1 funkcję za znacznikiem historii",
|
||||
"timeline.delete_after_marker_other": "Usuń funkcje za znacznikiem historii: {count}",
|
||||
"timeline.delete_many": "Usuń zaznaczone funkcje: {count}",
|
||||
"timeline.group": "Nowy folder funkcji",
|
||||
"timeline.move_many": "Przenieś elementy osi czasu",
|
||||
"timeline.move_to_group": "Przenieś funkcje do folderu",
|
||||
@@ -1121,7 +1324,106 @@
|
||||
"trace.stopped_by_contact": "Kończy się na próbce {sample}: {a} przeszedłby przez {b}",
|
||||
"trace.stopped_by_loop": "Kończy się na próbce {sample}: pętla przez {joint} nie może się bardziej zamknąć",
|
||||
"ui.1_degree_of_freedom": "1 stopień swobody",
|
||||
"ui.3d_mouse_axes_reassigned": "Osie są przypisane na nowo w Ustawieniach i to nadal obowiązuje razem z tą kalibracją.",
|
||||
"ui.3d_mouse_button": "Przycisk {number}",
|
||||
"ui.3d_mouse_button_nothing": "Nic",
|
||||
"ui.3d_mouse_button_settings": "Ustawienia myszy 3D",
|
||||
"ui.3d_mouse_buttons": "Przyciski",
|
||||
"ui.3d_mouse_buttons_hint": "Naciśnij przycisk na myszy 3D, aby go tu znaleźć, a potem wybierz, co ma robić.",
|
||||
"ui.3d_mouse_buttons_no_device": "Podłącz mysz 3D, aby ustawić, co robią jej przyciski.",
|
||||
"ui.3d_mouse_buttons_reset": "Przywróć domyślne przyciski",
|
||||
"ui.3d_mouse_calibrate": "Kalibruj…",
|
||||
"ui.3d_mouse_calibrate_tip": "Przesuwaj nakładkę zgodnie z poleceniami okna, aby SolidCraft nauczył się kierunku i zakresu każdej osi tej myszy 3D. Zapisywane dla tej myszy 3D.",
|
||||
"ui.3d_mouse_calibrate_title": "Kalibracja myszy 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Rusz raz myszą 3D, aby SolidCraft ją zobaczył.",
|
||||
"ui.3d_mouse_calibrated": "Skalibrowano",
|
||||
"ui.3d_mouse_calibration": "Kalibracja myszy 3D",
|
||||
"ui.3d_mouse_calibration_done": "Gotowe. Zapisz, aby używać kalibracji po każdym podłączeniu tej myszy 3D.",
|
||||
"ui.3d_mouse_calibration_no_response": "Jedna oś prawie się nie poruszyła, więc nie można jej skalibrować. Zacznij od nowa i za każdym razem wychylaj nakładkę mocniej.",
|
||||
"ui.3d_mouse_circles_help": "Koła pokazują, co urządzenie wysyła po kalibracji. Model testowy pokazuje, co widok z tym robi po odwróceniu, szybkości i przypisaniu osi.",
|
||||
"ui.3d_mouse_dead_zone": "Strefa martwa",
|
||||
"ui.3d_mouse_dead_zone_tip": "O ile trzeba wychylić nakładkę, zanim widok się poruszy, jako część pełnego wychylenia. Zwiększ, jeśli widok dryfuje przy nieruszanej nakładce.",
|
||||
"ui.3d_mouse_denied": "SolidCraft widzi mysz 3D, ale nie ma pozwolenia, by ją odczytać. Regułę udev opisują uwagi o konfiguracji myszy 3D.",
|
||||
"ui.3d_mouse_denied_short": "Znaleziono, ale nie można odczytać",
|
||||
"ui.3d_mouse_driven_by": "Który skalibrowany ruch urządzenia steruje tą osią. Wybranie ruchu używanego już przez inną oś zamienia je miejscami.",
|
||||
"ui.3d_mouse_driven_by_label": "sterowana przez",
|
||||
"ui.3d_mouse_driver": "Sterownik myszy 3D",
|
||||
"ui.3d_mouse_driver_auto": "Automatyczny",
|
||||
"ui.3d_mouse_driver_direct": "Bezpośredni (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Na razie odczyt przez spacenavd: samego urządzenia nie udało się otworzyć.",
|
||||
"ui.3d_mouse_driver_only_direct": "Urządzenie jest czytane bezpośrednio; spacenavd nie działa",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Odczyt przez spacenavd, jedyna dostępna droga",
|
||||
"ui.3d_mouse_forget": "Zapomnij kalibrację",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Lewy widok",
|
||||
"ui.3d_mouse_ghost_left_window": "Lewe okno",
|
||||
"ui.3d_mouse_ghost_position": "Położenie",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Prawy widok",
|
||||
"ui.3d_mouse_hint": "Przesuwaj i obracaj widok jednocześnie; w szkicach działa tylko przesuwanie i powiększanie.",
|
||||
"ui.3d_mouse_kind_cap": "Powrót do środka",
|
||||
"ui.3d_mouse_kind_question": "Jaki to rodzaj myszy 3D?",
|
||||
"ui.3d_mouse_kind_trackball": "Trackball",
|
||||
"ui.3d_mouse_looks_like_cap": "Odczyt zostaje, dopóki trzymasz, jak w urządzeniu typu „Powrót do środka”.",
|
||||
"ui.3d_mouse_looks_like_trackball": "Odczyt wraca, gdy tylko przestajesz ruszać, jak w trackballu.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Obrót X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Obrót Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Obrót Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Przesunięcie X",
|
||||
"ui.3d_mouse_motion_translation_y": "Przesunięcie Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Przesunięcie Z",
|
||||
"ui.3d_mouse_navigate": "Nawiguj za pomocą myszy 3D",
|
||||
"ui.3d_mouse_none": "Nie znaleziono myszy 3D",
|
||||
"ui.3d_mouse_not_calibrated": "Nieskalibrowano",
|
||||
"ui.3d_mouse_offer": "Kalibracja myszy 3D naprawia zamienione lub odwrócone osie oraz ustawia jej szybkość i odczucie.",
|
||||
"ui.3d_mouse_offer_never": "Nie pokazuj więcej",
|
||||
"ui.3d_mouse_offer_swap": "Przełącz na Hidraw (sterownik bezpośredni)",
|
||||
"ui.3d_mouse_reading": "Odczyt",
|
||||
"ui.3d_mouse_reset_axes": "Przywróć przypisanie osi",
|
||||
"ui.3d_mouse_reset_view": "Resetuj widok",
|
||||
"ui.3d_mouse_restore": "Przywróć ustawienia myszy 3D",
|
||||
"ui.3d_mouse_restore_ask": "Przywrócić domyślne ustawienia myszy 3D? Kalibracja i przypisanie przycisków zostają.",
|
||||
"ui.3d_mouse_section": "Mysz 3D",
|
||||
"ui.3d_mouse_show_in_view": "Pokazuj mysz 3D w widoku 3D",
|
||||
"ui.3d_mouse_show_in_view_tip": "Obraz twojej myszy 3D w lewym dolnym rogu, który porusza się razem z nią.",
|
||||
"ui.3d_mouse_skip": "Moje urządzenie tego nie potrafi",
|
||||
"ui.3d_mouse_skip_tip": "Pomija ten ruch w obu kierunkach; kalibracja go nie uwzględnia.",
|
||||
"ui.3d_mouse_speed": "Czułość myszy 3D",
|
||||
"ui.3d_mouse_step_lift": "Unieś nakładkę, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_press": "Wciśnij nakładkę w dół, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_pull": "Pociągnij nakładkę do siebie, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_push": "Popchnij nakładkę od siebie, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_rest": "Zdejmij dłoń z nakładki i nie ruszaj jej.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Zdejmij dłoń z kulki i nie ruszaj jej.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Obróć pierścień przewijania o kilka kliknięć zgodnie z ruchem wskazówek zegara.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Obróć pierścień przewijania o kilka kliknięć przeciwnie do ruchu wskazówek zegara.",
|
||||
"ui.3d_mouse_step_roll_away": "Potocz kulkę kilka razy od siebie, a potem pozwól jej się zatrzymać.",
|
||||
"ui.3d_mouse_step_roll_left": "Potocz kulkę kilka razy w lewo, a potem pozwól jej się zatrzymać.",
|
||||
"ui.3d_mouse_step_roll_right": "Potocz kulkę kilka razy w prawo, a potem pozwól jej się zatrzymać.",
|
||||
"ui.3d_mouse_step_roll_toward": "Potocz kulkę kilka razy do siebie, a potem pozwól jej się zatrzymać.",
|
||||
"ui.3d_mouse_step_slide_left": "Przesuń nakładkę w lewo, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_slide_right": "Przesuń nakładkę w prawo, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Zakręć kulką kilka razy zgodnie z ruchem wskazówek zegara, a potem pozwól jej się zatrzymać.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Zakręć kulką kilka razy przeciwnie do ruchu wskazówek zegara, a potem pozwól jej się zatrzymać.",
|
||||
"ui.3d_mouse_step_tilt_away": "Przechyl górę nakładki od siebie (krawędź najbliżej ciebie idzie w górę), przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_tilt_left": "Przechyl nakładkę w lewo (lewa krawędź w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_tilt_right": "Przechyl nakładkę w prawo (prawa krawędź w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Przechyl górę nakładki do siebie (krawędź najbliżej ciebie idzie w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_touch": "Połóż dłoń na nakładce, nie naciskając jej.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Obróć nakładkę zgodnie z ruchem wskazówek zegara, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Obróć nakładkę przeciwnie do ruchu wskazówek zegara, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_switch_cap": "Kalibruj jako powrót do środka",
|
||||
"ui.3d_mouse_switch_trackball": "Kalibruj jako trackball",
|
||||
"ui.3d_mouse_view_axis": "Oś widoku",
|
||||
"ui.3d_mouse_view_off": "Wyłączona",
|
||||
"ui.3d_mouse_view_off_tip": "Profil sterownika myszy 3D wyłącza tę oś, więc jej ruch nie porusza widokiem. Wybranie tu ruchu innej osi zamienia je miejscami, a ten ruch przestaje poruszać widokiem.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbita w lewo / prawo",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbita w górę / dół",
|
||||
"ui.3d_mouse_view_pan_sideways": "Przesuwanie w lewo / prawo",
|
||||
"ui.3d_mouse_view_pan_up_down": "Przesuwanie w górę / dół",
|
||||
"ui.3d_mouse_view_roll": "Przechył",
|
||||
"ui.3d_mouse_view_unmapped": "Oś {n}",
|
||||
"ui.3d_mouse_view_zoom": "Powiększenie",
|
||||
"ui.3d_mouse_waiting": "Czekam na ruch",
|
||||
"ui.3d_mouse_wiggle": "Jeśli masz podłączoną, poruszaj nią chwilę.",
|
||||
"ui.3d_sketch": "Szkic 3D",
|
||||
"ui.activate": "Aktywuj",
|
||||
"ui.active_component": "Aktywny komponent",
|
||||
@@ -1183,6 +1485,7 @@
|
||||
"ui.agent_chat_files_left_out": "Do jednej wiadomości można dołączyć najwyżej {max} plików; pominięto: {files}",
|
||||
"ui.agent_chat_files_loading": "Oczekiwanie na odczytanie plików",
|
||||
"ui.agent_chat_files_max": "Najwyżej {max} plików w jednej wiadomości",
|
||||
"ui.agent_chat_flatpak_none_found": "Nie znaleziono agenta programistycznego. Piaskownica Flatpak domyślnie nie widzi agentów zainstalowanych w folderze domowym i nie ma dostępu do sieci. Aby zezwolić na Claude Code i jego adapter ACP zainstalowane przez nvm lub w ~/.local/bin, uruchom to polecenie w terminalu, a potem uruchom ponownie SolidCraft. Daje ono dostęp do wymienionych programów i plików logowania oraz dostęp do sieci:",
|
||||
"ui.agent_chat_import_failed": "{name}: import się nie powiódł: {error}",
|
||||
"ui.agent_chat_intro": "Rozmawiaj z agentem programistycznym, którego już używasz, lub z wybranym punktem końcowym modelu. Pracuje on na otwartym projekcie za pomocą narzędzi SolidCraft: jego zmiany są widoczne na bieżąco, a cofa się je jednym krokiem na wiadomość. To, co może robić bez pytania, ustawisz w Preferencje > AI.",
|
||||
"ui.agent_chat_key_in_env": "klucz ustawiony",
|
||||
@@ -1250,6 +1553,8 @@
|
||||
"ui.agent_chat_waiting": "Czeka na Ciebie",
|
||||
"ui.agent_cursor": "Kursor agenta",
|
||||
"ui.agent_cursor_speed": "Szybkość kursora agenta",
|
||||
"ui.agent_off_thread": "Zachowaj responsywność okna, gdy agenty wykonują polecenia",
|
||||
"ui.agent_off_thread_note": "Wolne polecenia agenta (z panelu AI lub klienta MCP) są liczone obok okna: możesz dalej obracać i oglądać widok oraz zatrzymać polecenie przyciskiem w widoku. Wyłączone: każde działa w wątku okna, a okno na nie czeka.",
|
||||
"ui.ai_add": "Dodaj",
|
||||
"ui.ai_add_agent": "Dodaj agenta…",
|
||||
"ui.ai_add_agent_title": "Dodaj agenta",
|
||||
@@ -1278,6 +1583,7 @@
|
||||
"ui.ai_http_busy": "punkt końcowy jest zajęty lub przekroczył limit; spróbuj ponownie później",
|
||||
"ui.ai_http_key": "punkt końcowy odrzucił klucz: sprawdź go w Preferencje > AI",
|
||||
"ui.ai_http_not_found": "nie ma takiego punktu końcowego lub modelu: sprawdź bazowy URL i nazwę modelu",
|
||||
"ui.ai_http_too_large": "rozmowa jest za duża dla tego punktu końcowego; rozpocznij nowy czat",
|
||||
"ui.ai_key": "Klucz API",
|
||||
"ui.ai_key_hint": "opcjonalny; przechowywany w systemowym pęku kluczy",
|
||||
"ui.ai_key_needs_https": "Klucz jest wysyłany tylko przez HTTPS albo do tego komputera.",
|
||||
@@ -1305,7 +1611,6 @@
|
||||
"ui.ai_permissions": "Uprawnienia",
|
||||
"ui.ai_preset": "Ustawienie wstępne",
|
||||
"ui.ai_remove": "Usuń",
|
||||
"ui.ai_request_limit": "zatrzymano po 24 żądaniach dla jednej wiadomości; wyślij kolejną wiadomość, aby kontynuować",
|
||||
"ui.ai_reset": "Resetuj",
|
||||
"ui.ai_settings": "Ustawienia AI…",
|
||||
"ui.ai_show_features": "Pokaż funkcje AI",
|
||||
@@ -1350,6 +1655,7 @@
|
||||
"ui.blocked": "Zablokowane",
|
||||
"ui.bodies": "Bryły",
|
||||
"ui.bodies_all_if_none": "Bryły (wszystkie, jeśli brak)",
|
||||
"ui.bottom": "Dół",
|
||||
"ui.box": "Prostopadłościan",
|
||||
"ui.break": "Podziel",
|
||||
"ui.browser_bodies_one": "{count} bryła",
|
||||
@@ -1481,6 +1787,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtruj polecenia lub klawisze",
|
||||
"ui.findInTimeline": "Znajdź na osi czasu",
|
||||
"ui.findInWindow": "Znajdź w oknie",
|
||||
"ui.fine": "Dokładna",
|
||||
"ui.finest": "Najdokładniejsza",
|
||||
"ui.finish_sketch": "Zakończ szkic",
|
||||
"ui.fit": "Dopasuj",
|
||||
"ui.fix_unfix": "Ustal/zwolnij",
|
||||
@@ -1502,6 +1810,7 @@
|
||||
"ui.grid_follows_view_hint": "Widoki boczne pokazują siatkę na pionowej płaszczyźnie zwróconej do nich zamiast na podłożu",
|
||||
"ui.ground_shadow": "Cień na podłożu",
|
||||
"ui.groupSelected": "Grupuj zaznaczone",
|
||||
"ui.hatch": "Kreskowanie",
|
||||
"ui.heading_add_ins": "DODATKI",
|
||||
"ui.heading_assemble": "ZŁOŻENIE",
|
||||
"ui.heading_box": "PROSTOPADŁOŚCIAN",
|
||||
@@ -1568,6 +1877,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "NARZĘDZIA",
|
||||
"ui.heading_utility": "NARZĘDZIE",
|
||||
"ui.heading_weldments": "SPAWANIE",
|
||||
"ui.help": "Pomoc",
|
||||
"ui.hide": "Ukryj",
|
||||
"ui.hideAxes": "Ukryj osie",
|
||||
@@ -1577,10 +1887,17 @@
|
||||
"ui.hideProfile": "Ukryj profil",
|
||||
"ui.history_back": "Cofnij o jeden krok",
|
||||
"ui.history_below_browser": "Pod przeglądarką",
|
||||
"ui.history_edge_hint": "kliknij dwukrotnie krawędź",
|
||||
"ui.history_first": "Przejdź do początku",
|
||||
"ui.history_forward": "Przejdź o jeden krok do przodu",
|
||||
"ui.history_hide_names": "Ukryj nazwy",
|
||||
"ui.history_last": "Przejdź do końca",
|
||||
"ui.history_marker_tip": "Znacznik historii: przeciągnij, aby cofnąć model; nowe funkcje trafiają tutaj",
|
||||
"ui.history_names": "Nazwy",
|
||||
"ui.history_play": "Odtwórz do końca",
|
||||
"ui.history_show_names": "Pokaż nazwy",
|
||||
"ui.history_title": "Historia",
|
||||
"ui.history_toggle_names": "Pokaż lub ukryj nazwy funkcji",
|
||||
"ui.hole": "Otwór",
|
||||
"ui.hole_tap_type": "Typ gwintu otworu",
|
||||
"ui.hole_type": "Typ otworu",
|
||||
@@ -1640,6 +1957,11 @@
|
||||
"ui.move": "Przesuń",
|
||||
"ui.multiple": "Wiele",
|
||||
"ui.named_views": "Widoki nazwane",
|
||||
"ui.nav_bridge": "Nawiguj programem urządzenia na tym komputerze",
|
||||
"ui.nav_bridge_connected": "Połączono",
|
||||
"ui.nav_bridge_port": "Port",
|
||||
"ui.nav_bridge_tip": "Własny program urządzenia wejściowego 3D (np. sterownik domowej myszy 3D) porusza widokiem przez połączenie lokalne, więc kursor myszy zostaje Twój. SolidCraft łączy się z nim na tym porcie, tylko na tym komputerze.",
|
||||
"ui.nav_bridge_waiting": "Czekam na program urządzenia",
|
||||
"ui.navigation": "Nawigacja",
|
||||
"ui.needs_design": "{command}: najpierw otwórz projekt",
|
||||
"ui.newSketchGroup": "Nowa grupa szkiców",
|
||||
@@ -1683,6 +2005,8 @@
|
||||
"ui.pinToToolbar": "Przypnij do paska narzędzi",
|
||||
"ui.pipe": "Rura",
|
||||
"ui.placement": "Umiejscowienie",
|
||||
"ui.placement_handles": "Uchwyty umieszczania",
|
||||
"ui.placement_handles_tip": "Uchwyty na podglądzie podparcia, występu lub zatrzasku: przeciągnij znacznik, aby przesunąć element po modelu, obróć go pierścieniem",
|
||||
"ui.plane": "Płaszczyzna",
|
||||
"ui.plane_at_angle": "Płaszczyzna pod kątem",
|
||||
"ui.plugins": "Wtyczki",
|
||||
@@ -1704,6 +2028,7 @@
|
||||
"ui.project_face_edges_sketch": "Rzutuj krawędzie ściany do nowych szkiców",
|
||||
"ui.properties": "Właściwości",
|
||||
"ui.pull_direction": "Kierunek wyjmowania",
|
||||
"ui.quality": "Jakość",
|
||||
"ui.quantity": "Liczba",
|
||||
"ui.quantity_2": "Liczba 2",
|
||||
"ui.quit": "Zakończ",
|
||||
@@ -1792,40 +2117,13 @@
|
||||
"ui.smooth_wheel_zoom": "Płynne powiększanie kółkiem",
|
||||
"ui.smooth_wheel_zoom_tip": "Wyłącz, aby każdy ruch kółka działał od razu, bez wygładzania",
|
||||
"ui.snap": "Przyciąganie",
|
||||
"ui.space_mouse_calibrate": "Kalibruj…",
|
||||
"ui.space_mouse_calibrate_tip": "Przesuwaj nakładkę zgodnie z poleceniami okna, aby SolidCraft nauczył się kierunku i zakresu każdej osi tej myszy 3D. Zapisywane dla tej myszy 3D.",
|
||||
"ui.space_mouse_calibrate_title": "Kalibracja myszy 3D",
|
||||
"ui.space_mouse_calibrated": "Skalibrowano",
|
||||
"ui.space_mouse_calibration": "Kalibracja myszy 3D",
|
||||
"ui.space_mouse_calibration_done": "Gotowe. Zapisz, aby używać kalibracji po każdym podłączeniu tej myszy 3D.",
|
||||
"ui.space_mouse_calibration_no_response": "Jedna oś prawie się nie poruszyła, więc nie można jej skalibrować. Zacznij od nowa i za każdym razem wychylaj nakładkę mocniej.",
|
||||
"ui.space_mouse_dead_zone": "Strefa martwa",
|
||||
"ui.space_mouse_dead_zone_tip": "O ile trzeba wychylić nakładkę, zanim widok się poruszy, jako część pełnego wychylenia. Zwiększ, jeśli widok dryfuje przy nieruszanej nakładce.",
|
||||
"ui.space_mouse_forget": "Zapomnij kalibrację",
|
||||
"ui.space_mouse_hint": "Przesuwaj i obracaj widok jednocześnie; w szkicach działa tylko przesuwanie i powiększanie.",
|
||||
"ui.space_mouse_navigate": "Nawiguj za pomocą SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Nie znaleziono myszy 3D",
|
||||
"ui.space_mouse_not_calibrated": "Nieskalibrowano",
|
||||
"ui.space_mouse_reading": "Odczyt",
|
||||
"ui.space_mouse_step_lift": "Unieś nakładkę, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_press": "Wciśnij nakładkę w dół, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_pull": "Pociągnij nakładkę do siebie, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_push": "Popchnij nakładkę od siebie, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_rest": "Zdejmij dłoń z nakładki i nie ruszaj jej.",
|
||||
"ui.space_mouse_step_slide_left": "Przesuń nakładkę w lewo, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_slide_right": "Przesuń nakładkę w prawo, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_tilt_back": "Przechyl nakładkę do tyłu (przednia krawędź w górę), przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_tilt_forward": "Przechyl nakładkę do przodu (przednia krawędź w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_tilt_left": "Przechyl nakładkę w lewo (lewa krawędź w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_tilt_right": "Przechyl nakładkę w prawo (prawa krawędź w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Obróć nakładkę zgodnie z ruchem wskazówek zegara, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Obróć nakładkę przeciwnie do ruchu wskazówek zegara, przytrzymaj, a następnie puść.",
|
||||
"ui.spacing": "Odstęp",
|
||||
"ui.spacing_2": "Odstęp 2",
|
||||
"ui.sphere": "Sfera",
|
||||
"ui.spline": "Splajn",
|
||||
"ui.split_body": "Podziel bryłę",
|
||||
"ui.stand_up_tip": "Gdy plik STEP lub IGES otworzy się z osią Z w górę, proponuj obrót o ćwierć obrotu wokół osi X",
|
||||
"ui.standard": "Standardowa",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Zacznij od nowa",
|
||||
"ui.start_page": "Strona startowa",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Alegeți…",
|
||||
"Circular": "Circular",
|
||||
"Circumference": "Circumferință",
|
||||
"Clockwise": "În sens orar",
|
||||
"Close draft": "Închide ciorna",
|
||||
"Collisions": "Coliziuni",
|
||||
"Colour": "Culoare",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Caută comenzi",
|
||||
"Search this library": "Caută în această bibliotecă",
|
||||
"Section": "Secțiune",
|
||||
"Section position": "Poziția secțiunii",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Selectează altul: țineți apăsat butonul stâng nemișcat pe model (sau clic dreapta › Selectează altul…) pentru a lista tot ce se află sub cursor; faceți clic din nou în același loc pentru a selecta următorul element. Dublu clic pe butonul din mijloc pentru a merge Acasă (Preferințe › Navigare).",
|
||||
"Select bodies (a face picks its body)": "Selectați corpuri (o față alege corpul ei)",
|
||||
"Select bodies or faces instead": "Selectați în schimb corpuri sau fețe",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Afișează cursorul agentului",
|
||||
"Show in folder": "Afișează în dosar",
|
||||
"Show the pointer an AI agent moves (#35)": "Afișează indicatorul pe care îl mută un agent AI (#35)",
|
||||
"Show tool at cursor": "Afișează instrumentul lângă cursor",
|
||||
"Signed axes": "Axe cu semn",
|
||||
"Size": "Dimensiune",
|
||||
"Sketch constraints": "Constrângerile schiței",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft s-a închis înainte ca aceste proiecte să fie salvate. Ultima lor salvare automată poate fi deschisă.",
|
||||
"Specialise": "Specializează",
|
||||
"Speed": "Viteză",
|
||||
"Square": "Pătrat",
|
||||
"Stand it up": "Ridică-l în picioare",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Ridică-l în picioare: un sfert de rotație în jurul lui X, ca o singură operație de mutare ce poate fi anulată.",
|
||||
"Stand this part up?": "Ridicați această piesă în picioare?",
|
||||
"Start angle": "Unghi de început",
|
||||
"Start bulge": "Bombare inițială",
|
||||
"Start continuity": "Continuitate inițială",
|
||||
"Step": "Pas",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "unghi, înălțime",
|
||||
"appearance.dielectric_ior": "IOR dielectric",
|
||||
"appearance.metallic": "Metalic",
|
||||
"appearance.only_available": "Doar aspecte disponibile",
|
||||
"appearance.pbr_help": "Vizualizați această suprafață în Randare sau într-un PNG de produs. Fără refracție sau transmisie.",
|
||||
"appearance.pbr_opaque": "PBR opac",
|
||||
"appearance.roughness": "Rugozitate",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "Inserează STEP",
|
||||
"cmd.file_save_mesh": "Salvează ca plasă",
|
||||
"cmd.file_slicers": "Slicere instalate",
|
||||
"cmd.frame_member": "Element de cadru",
|
||||
"cmd.frame_profile_insert": "Inserare profil",
|
||||
"cmd.frame_profile_list": "Bibliotecă de profile",
|
||||
"cmd.frame_profile_properties": "Proprietăți profil",
|
||||
"cmd.inspect_accessibility": "Analiza accesibilității",
|
||||
"cmd.inspect_center_of_mass": "Centru de masă",
|
||||
"cmd.inspect_clearance": "Joc",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Material fizic nou",
|
||||
"cmd.material_delete": "Șterge materialul fizic",
|
||||
"cmd.material_edit": "Editează materialul fizic",
|
||||
"cmd.material_inspect": "Inspectează materialul fizic",
|
||||
"cmd.material_list": "Listează materialele",
|
||||
"cmd.material_properties_directional": "Modul de elasticitate direcțional",
|
||||
"cmd.material_properties_validate": "Validează datele materialului fizic",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Cheie aici",
|
||||
"motion.key_here_hint": "Adaugă o cheie la acest pas cu valoarea curentă a îmbinării",
|
||||
"motion.no_collisions": "Nicio coliziune în studiu",
|
||||
"motion.plot_quantity": "Cantitate",
|
||||
"motion.step_back": "Pas înapoi",
|
||||
"motion.step_forward": "Pas înainte",
|
||||
"motion.to_end": "La sfârșit",
|
||||
@@ -1059,7 +1071,7 @@
|
||||
"search.try": "Tastați numele unei comenzi, o operație din acest proiect sau un parametru. Pentru a executa o comandă cu valori, tastați id-ul ei și un obiect JSON: {example}",
|
||||
"search.valid": "Valid",
|
||||
"settings.display_quality_note": "Nivelurile mai fine desenează fețele și muchiile curbe mai neted și durează mai mult la generarea plasei. Se schimbă doar fereastra de vizualizare, niciodată proiectul sau exporturile.",
|
||||
"settings.frame_rate_note": "Afișează cadrele pe secundă în colțul din dreapta jos al vizualizării. Cât timp este activat, vizualizarea se redesenează continuu.",
|
||||
"settings.frame_rate_note": "Afișează cadrele pe secundă în colțul din dreapta jos al vizualizării.",
|
||||
"settings.measure_axis_distances": "Afișează distanțele X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Afișează cotele pe axele globale alături de distanța totală cât timp Măsurare este deschis.",
|
||||
"settings.msaa_applies_on_restart": "Anti-aliasing se aplică la următoarea pornire a SolidCraft",
|
||||
@@ -1120,7 +1132,50 @@
|
||||
"trace.stopped_by_contact": "Se termină la eșantionul {sample}: {a} ar trece prin {b}",
|
||||
"trace.stopped_by_loop": "Se termină la eșantionul {sample}: bucla prin {joint} nu se mai poate închide",
|
||||
"ui.1_degree_of_freedom": "1 grad de libertate",
|
||||
"ui.3d_mouse_calibrate": "Calibrează…",
|
||||
"ui.3d_mouse_calibrate_tip": "Mișcați capacul așa cum cere fereastra, ca SolidCraft să învețe în ce direcție merge fiecare axă a acestui mouse 3D și cât de departe. Salvat pentru acest mouse 3D.",
|
||||
"ui.3d_mouse_calibrate_title": "Calibrează mouse-ul 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Mișcă o dată mouse-ul 3D ca SolidCraft să îl vadă.",
|
||||
"ui.3d_mouse_calibrated": "Calibrat",
|
||||
"ui.3d_mouse_calibration": "Calibrarea mouse-ului 3D",
|
||||
"ui.3d_mouse_calibration_done": "Gata. Salvați-o pentru a o folosi ori de câte ori acest mouse 3D este conectat.",
|
||||
"ui.3d_mouse_calibration_no_response": "O axă abia s-a mișcat, deci nu poate fi calibrată. Reîncepeți și mișcați capacul mai mult de fiecare dată.",
|
||||
"ui.3d_mouse_circles_help": "Cercurile arată ce trimite dispozitivul după calibrare. Modelul de test arată ce face vizualizarea cu acestea, după inversare, viteză și atribuirea axelor.",
|
||||
"ui.3d_mouse_dead_zone": "Zonă moartă",
|
||||
"ui.3d_mouse_dead_zone_tip": "Cât trebuie să se miște capacul înainte ca vizualizarea să se miște, ca fracțiune din cursa completă. Măriți-o dacă vizualizarea derivă cât capacul este în repaus.",
|
||||
"ui.3d_mouse_denied": "SolidCraft vede un mouse 3D, dar nu are voie să îl citească. Regula udev este în notele de configurare a mouse-ului 3D.",
|
||||
"ui.3d_mouse_denied_short": "Găsit, dar nu poate fi citit",
|
||||
"ui.3d_mouse_driver_fallback": "Deocamdată se citește prin spacenavd: dispozitivul în sine nu a putut fi deschis.",
|
||||
"ui.3d_mouse_forget": "Uită calibrarea",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Viewport stânga",
|
||||
"ui.3d_mouse_ghost_left_window": "Fereastră stânga",
|
||||
"ui.3d_mouse_ghost_position": "Poziție",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Viewport dreapta",
|
||||
"ui.3d_mouse_hint": "Mută și rotește vizualizarea împreună; schițele folosesc doar panoramare și zoom.",
|
||||
"ui.3d_mouse_navigate": "Navighează cu mouse-ul 3D",
|
||||
"ui.3d_mouse_none": "Niciun mouse 3D găsit",
|
||||
"ui.3d_mouse_not_calibrated": "Necalibrat",
|
||||
"ui.3d_mouse_offer": "Calibrarea mouse-ului 3D corectează axele inversate sau schimbate între ele și îi reglează viteza și senzația.",
|
||||
"ui.3d_mouse_offer_never": "Nu mai afișa",
|
||||
"ui.3d_mouse_offer_swap": "Treci la Hidraw (driver direct)",
|
||||
"ui.3d_mouse_reading": "Valoare citită",
|
||||
"ui.3d_mouse_reset_view": "Resetează vizualizarea",
|
||||
"ui.3d_mouse_restore": "Restaurează implicitele mouse-ului 3D",
|
||||
"ui.3d_mouse_restore_ask": "Revii la valorile implicite ale setărilor mouse-ului 3D? Calibrarea și atribuirea butoanelor rămân.",
|
||||
"ui.3d_mouse_speed": "Viteza mouse-ului 3D",
|
||||
"ui.3d_mouse_step_lift": "Ridicați capacul în sus, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_press": "Apăsați capacul în jos, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_pull": "Trageți capacul spre dumneavoastră, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_push": "Împingeți capacul de la dumneavoastră, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_rest": "Luați mâna de pe capac și lăsați-l nemișcat.",
|
||||
"ui.3d_mouse_step_slide_left": "Glisați capacul spre stânga, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_slide_right": "Glisați capacul spre dreapta, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_tilt_left": "Înclinați capacul spre stânga (marginea stângă în jos), țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_tilt_right": "Înclinați capacul spre dreapta (marginea dreaptă în jos), țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Răsuciți capacul în sensul acelor de ceasornic, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Răsuciți capacul în sens invers acelor de ceasornic, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_waiting": "Se așteaptă mișcare",
|
||||
"ui.3d_mouse_wiggle": "Dacă ai unul conectat, mișcă-l puțin.",
|
||||
"ui.3d_sketch": "Schiță 3D",
|
||||
"ui.activate": "Activează",
|
||||
"ui.active_component": "Componentă activă",
|
||||
@@ -1182,6 +1237,7 @@
|
||||
"ui.agent_chat_files_left_out": "Cel mult {max} fișiere merg cu un mesaj; omise: {files}",
|
||||
"ui.agent_chat_files_loading": "Se așteaptă citirea fișierelor",
|
||||
"ui.agent_chat_files_max": "Cel mult {max} fișiere pe un mesaj",
|
||||
"ui.agent_chat_flatpak_none_found": "Nu s-a găsit niciun agent de programare. Implicit, sandboxul Flatpak nu vede agenții instalați în dosarul personal și nu are acces la rețea. Pentru a permite Claude Code și adaptorul său ACP instalate prin nvm sau în ~/.local/bin, rulați această comandă într-un terminal, apoi reporniți SolidCraft. Ea acordă acces la programele și fișierele de autentificare enumerate, precum și acces la rețea:",
|
||||
"ui.agent_chat_import_failed": "{name}: importul a eșuat: {error}",
|
||||
"ui.agent_chat_intro": "Conversați cu un agent de programare pe care îl folosiți deja sau cu un endpoint de model ales de dumneavoastră. Lucrează la proiectul deschis prin instrumentele SolidCraft: modificările lui apar pe măsură ce le face și se anulează ca un singur pas per mesaj. Ce poate face fără să întrebe se stabilește în Preferințe > AI.",
|
||||
"ui.agent_chat_key_in_env": "cheie setată",
|
||||
@@ -1277,6 +1333,7 @@
|
||||
"ui.ai_http_busy": "endpointul este ocupat sau și-a depășit limita; încercați mai târziu",
|
||||
"ui.ai_http_key": "endpointul a refuzat cheia: verificați-o în Preferințe > AI",
|
||||
"ui.ai_http_not_found": "nu există un astfel de endpoint sau model: verificați URL-ul de bază și numele modelului",
|
||||
"ui.ai_http_too_large": "conversația este prea mare pentru acest endpoint; începeți un chat nou",
|
||||
"ui.ai_key": "Cheie API",
|
||||
"ui.ai_key_hint": "opțional; păstrată în depozitul de chei al sistemului",
|
||||
"ui.ai_key_needs_https": "O cheie se trimite doar prin HTTPS sau către acest calculator.",
|
||||
@@ -1304,7 +1361,6 @@
|
||||
"ui.ai_permissions": "Permisiuni",
|
||||
"ui.ai_preset": "Presetare",
|
||||
"ui.ai_remove": "Elimină",
|
||||
"ui.ai_request_limit": "oprit după 24 de cereri pentru un mesaj; trimiteți alt mesaj pentru a continua",
|
||||
"ui.ai_reset": "Resetează",
|
||||
"ui.ai_settings": "Setări AI…",
|
||||
"ui.ai_show_features": "Afișează funcțiile AI",
|
||||
@@ -1480,6 +1536,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtrează comenzi sau taste",
|
||||
"ui.findInTimeline": "Găsește în cronologie",
|
||||
"ui.findInWindow": "Găsește în fereastră",
|
||||
"ui.fine": "Fină",
|
||||
"ui.finest": "Foarte fină",
|
||||
"ui.finish_sketch": "Finalizează schița",
|
||||
"ui.fit": "Potrivire",
|
||||
"ui.fix_unfix": "Fixează/Defixează",
|
||||
@@ -1501,6 +1559,7 @@
|
||||
"ui.grid_follows_view_hint": "Vederile laterale afișează grila pe planul vertical orientat spre ele, în loc de sol",
|
||||
"ui.ground_shadow": "Umbra la sol",
|
||||
"ui.groupSelected": "Grupează selecția",
|
||||
"ui.hatch": "Hașură",
|
||||
"ui.heading_add_ins": "EXTENSII",
|
||||
"ui.heading_assemble": "ASAMBLARE",
|
||||
"ui.heading_box": "CUTIE",
|
||||
@@ -1567,6 +1626,7 @@
|
||||
"ui.heading_torus": "TOR",
|
||||
"ui.heading_utilities": "UTILITARE",
|
||||
"ui.heading_utility": "UTILITAR",
|
||||
"ui.heading_weldments": "SUDURĂ",
|
||||
"ui.help": "Ajutor",
|
||||
"ui.hide": "Ascunde",
|
||||
"ui.hideAxes": "Ascunde axele",
|
||||
@@ -1703,6 +1763,7 @@
|
||||
"ui.project_face_edges_sketch": "Proiectează muchiile feței în schițe noi",
|
||||
"ui.properties": "Proprietăți",
|
||||
"ui.pull_direction": "Direcția de extragere",
|
||||
"ui.quality": "Calitate",
|
||||
"ui.quantity": "Cantitate",
|
||||
"ui.quantity_2": "Cantitatea 2",
|
||||
"ui.quit": "Ieșire",
|
||||
@@ -1791,40 +1852,13 @@
|
||||
"ui.smooth_wheel_zoom": "Zoom lin cu rotița",
|
||||
"ui.smooth_wheel_zoom_tip": "Dezactivați pentru a aplica imediat fiecare eveniment al rotiței, fără coadă de netezire",
|
||||
"ui.snap": "Agățare",
|
||||
"ui.space_mouse_calibrate": "Calibrează…",
|
||||
"ui.space_mouse_calibrate_tip": "Mișcați capacul așa cum cere fereastra, ca SolidCraft să învețe în ce direcție merge fiecare axă a acestui mouse 3D și cât de departe. Salvat pentru acest mouse 3D.",
|
||||
"ui.space_mouse_calibrate_title": "Calibrează mouse-ul 3D",
|
||||
"ui.space_mouse_calibrated": "Calibrat",
|
||||
"ui.space_mouse_calibration": "Calibrarea mouse-ului 3D",
|
||||
"ui.space_mouse_calibration_done": "Gata. Salvați-o pentru a o folosi ori de câte ori acest mouse 3D este conectat.",
|
||||
"ui.space_mouse_calibration_no_response": "O axă abia s-a mișcat, deci nu poate fi calibrată. Reîncepeți și mișcați capacul mai mult de fiecare dată.",
|
||||
"ui.space_mouse_dead_zone": "Zonă moartă",
|
||||
"ui.space_mouse_dead_zone_tip": "Cât trebuie să se miște capacul înainte ca vizualizarea să se miște, ca fracțiune din cursa completă. Măriți-o dacă vizualizarea derivă cât capacul este în repaus.",
|
||||
"ui.space_mouse_forget": "Uită calibrarea",
|
||||
"ui.space_mouse_hint": "Mută și rotește vizualizarea împreună; schițele folosesc doar panoramare și zoom.",
|
||||
"ui.space_mouse_navigate": "Navighează cu SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Niciun mouse 3D găsit",
|
||||
"ui.space_mouse_not_calibrated": "Necalibrat",
|
||||
"ui.space_mouse_reading": "Valoare citită",
|
||||
"ui.space_mouse_step_lift": "Ridicați capacul în sus, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_press": "Apăsați capacul în jos, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_pull": "Trageți capacul spre dumneavoastră, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_push": "Împingeți capacul de la dumneavoastră, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_rest": "Luați mâna de pe capac și lăsați-l nemișcat.",
|
||||
"ui.space_mouse_step_slide_left": "Glisați capacul spre stânga, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_slide_right": "Glisați capacul spre dreapta, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_tilt_back": "Înclinați capacul spre spate (marginea din față în sus), țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_tilt_forward": "Înclinați capacul spre față (marginea din față în jos), țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_tilt_left": "Înclinați capacul spre stânga (marginea stângă în jos), țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_tilt_right": "Înclinați capacul spre dreapta (marginea dreaptă în jos), țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Răsuciți capacul în sensul acelor de ceasornic, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Răsuciți capacul în sens invers acelor de ceasornic, țineți-l, apoi eliberați-l.",
|
||||
"ui.spacing": "Spațiere",
|
||||
"ui.spacing_2": "Spațierea 2",
|
||||
"ui.sphere": "Sferă",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Împarte corpul",
|
||||
"ui.stand_up_tip": "După ce un fișier STEP sau IGES se deschide cu Z în sus, oferă un sfert de rotație în jurul lui X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Început",
|
||||
"ui.start_over": "Reîncepe",
|
||||
"ui.start_page": "Pagina de start",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Выбрать…",
|
||||
"Circular": "Круговой",
|
||||
"Circumference": "Окружность",
|
||||
"Clockwise": "По часовой стрелке",
|
||||
"Close draft": "Закрыть анализ уклонов",
|
||||
"Collisions": "Столкновения",
|
||||
"Colour": "Цвет",
|
||||
@@ -268,7 +269,7 @@
|
||||
"Preview Row": "Строка предпросмотра",
|
||||
"Preview leaves the current design unchanged.": "Предпросмотр не меняет текущий дизайн.",
|
||||
"Profiles/Curves": "Профили/кривые",
|
||||
"Quantity": "Величина",
|
||||
"Quantity": "Количество",
|
||||
"RECENT": "НЕДАВНИЕ",
|
||||
"REFERENCE": "ССЫЛОЧНАЯ ГЕОМЕТРИЯ",
|
||||
"RULED SURFACE": "ЛИНЕЙЧАТАЯ ПОВЕРХНОСТЬ",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Поиск команд",
|
||||
"Search this library": "Поиск в этой библиотеке",
|
||||
"Section": "Сечение",
|
||||
"Section position": "Положение сечения",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Выбрать другое: удерживайте левую кнопку на модели (или правый щелчок › Выбрать другое…), чтобы увидеть всё под курсором; повторный щелчок в том же месте выбирает следующий объект. Двойной щелчок средней кнопкой — «Главная» (Настройки › Навигация).",
|
||||
"Select bodies (a face picks its body)": "Выберите тела (выбор грани выбирает её тело)",
|
||||
"Select bodies or faces instead": "Вместо этого выберите тела или грани",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Показывать курсор агента",
|
||||
"Show in folder": "Показать в папке",
|
||||
"Show the pointer an AI agent moves (#35)": "Показывать указатель, которым двигается ИИ-агент (#35)",
|
||||
"Show tool at cursor": "Показывать инструмент у курсора",
|
||||
"Signed axes": "Знаковые оси",
|
||||
"Size": "Размер",
|
||||
"Sketch constraints": "Ограничения эскиза",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft закрылся до сохранения этих дизайнов. Можно открыть последнюю автосохранённую версию.",
|
||||
"Specialise": "Специализировать",
|
||||
"Speed": "Скорость",
|
||||
"Square": "Квадрат",
|
||||
"Stand it up": "Поставить вертикально",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Поставить вертикально: четверть оборота вокруг X одной операцией «Переместить», которую можно отменить.",
|
||||
"Stand this part up?": "Поставить эту деталь вертикально?",
|
||||
"Start angle": "Начальный угол",
|
||||
"Start bulge": "Выпуклость в начале",
|
||||
"Start continuity": "Непрерывность в начале",
|
||||
"Step": "Шаг",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "угол, высота",
|
||||
"appearance.dielectric_ior": "Показатель преломления диэлектрика",
|
||||
"appearance.metallic": "Металличность",
|
||||
"appearance.only_available": "Только доступные внешние виды",
|
||||
"appearance.pbr_help": "Просмотр поверхности во вкладке Рендер или на PNG изделия. Без преломления и пропускания света.",
|
||||
"appearance.pbr_opaque": "Непрозрачный PBR",
|
||||
"appearance.roughness": "Шероховатость",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "Вставить STEP",
|
||||
"cmd.file_save_mesh": "Сохранить как меш",
|
||||
"cmd.file_slicers": "Установленные слайсеры",
|
||||
"cmd.frame_member": "Элемент рамы",
|
||||
"cmd.frame_profile_insert": "Вставить профиль",
|
||||
"cmd.frame_profile_list": "Библиотека профилей",
|
||||
"cmd.frame_profile_properties": "Свойства профиля",
|
||||
"cmd.inspect_accessibility": "Анализ доступности",
|
||||
"cmd.inspect_center_of_mass": "Центр масс",
|
||||
"cmd.inspect_clearance": "Зазор",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Новый физический материал",
|
||||
"cmd.material_delete": "Удалить физический материал",
|
||||
"cmd.material_edit": "Изменить физический материал",
|
||||
"cmd.material_inspect": "Проверить физический материал",
|
||||
"cmd.material_list": "Список материалов",
|
||||
"cmd.material_properties_directional": "Направленный модуль упругости",
|
||||
"cmd.material_properties_validate": "Проверить данные физического материала",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Вставить ключевой кадр здесь",
|
||||
"motion.key_here_hint": "Добавить ключевой кадр на этом шаге с текущим значением сопряжения",
|
||||
"motion.no_collisions": "За всё исследование столкновений нет",
|
||||
"motion.plot_quantity": "Величина",
|
||||
"motion.step_back": "Шаг назад",
|
||||
"motion.step_forward": "Шаг вперёд",
|
||||
"motion.to_end": "В конец",
|
||||
@@ -1059,7 +1071,7 @@
|
||||
"search.try": "Введите название команды, операцию этого дизайна или параметр. Чтобы выполнить команду со значениями, введите её id и объект JSON: {example}",
|
||||
"search.valid": "Допустимо",
|
||||
"settings.display_quality_note": "Более высокие уровни рисуют криволинейные грани и рёбра глаже, но сетка строится дольше. Меняется только видовой экран, но не дизайн и не экспорт.",
|
||||
"settings.frame_rate_note": "Показывает кадры в секунду в правом нижнем углу вида. Пока включено, вид перерисовывается непрерывно.",
|
||||
"settings.frame_rate_note": "Показывает кадры в секунду в правом нижнем углу вида.",
|
||||
"settings.measure_axis_distances": "Расстояния X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Показывать размеры вдоль мировых осей вместе с общим расстоянием, пока открыто окно измерения.",
|
||||
"settings.msaa_applies_on_restart": "Сглаживание вступит в силу при следующем запуске SolidCraft",
|
||||
@@ -1120,7 +1132,106 @@
|
||||
"trace.stopped_by_contact": "Заканчивается на отсчёте {sample}: {a} прошёл бы сквозь {b}",
|
||||
"trace.stopped_by_loop": "Заканчивается на отсчёте {sample}: контур через {joint} дальше не замыкается",
|
||||
"ui.1_degree_of_freedom": "1 степень свободы",
|
||||
"ui.3d_mouse_axes_reassigned": "Оси переназначены в настройках, и это действует и поверх этой калибровки.",
|
||||
"ui.3d_mouse_button": "Кнопка {number}",
|
||||
"ui.3d_mouse_button_nothing": "Ничего",
|
||||
"ui.3d_mouse_button_settings": "Настройки 3D-мыши",
|
||||
"ui.3d_mouse_buttons": "Кнопки",
|
||||
"ui.3d_mouse_buttons_hint": "Нажмите кнопку на 3D-мыши, чтобы найти её здесь, и выберите, что она делает.",
|
||||
"ui.3d_mouse_buttons_no_device": "Подключите 3D-мышь, чтобы назначить её кнопки.",
|
||||
"ui.3d_mouse_buttons_reset": "Вернуть кнопки по умолчанию",
|
||||
"ui.3d_mouse_calibrate": "Калибровать…",
|
||||
"ui.3d_mouse_calibrate_tip": "Двигайте колпачок так, как просит окно, чтобы SolidCraft узнал, куда и насколько далеко движется каждая ось этой 3D-мыши. Сохраняется для этой 3D-мыши.",
|
||||
"ui.3d_mouse_calibrate_title": "Калибровка 3D-мыши",
|
||||
"ui.3d_mouse_calibrate_wait": "Один раз подвигайте 3D-мышь, чтобы SolidCraft её увидел.",
|
||||
"ui.3d_mouse_calibrated": "Откалибрована",
|
||||
"ui.3d_mouse_calibration": "Калибровка 3D-мыши",
|
||||
"ui.3d_mouse_calibration_done": "Готово. Сохраните её, чтобы она применялась всякий раз, когда подключена эта 3D-мышь.",
|
||||
"ui.3d_mouse_calibration_no_response": "Одна ось почти не сдвинулась, поэтому её нельзя откалибровать. Начните заново и каждый раз двигайте колпачок дальше.",
|
||||
"ui.3d_mouse_circles_help": "Круги показывают, что устройство отправляет после калибровки. Тестовая модель показывает, что вид делает с этим после инверсии, скорости и назначения осей.",
|
||||
"ui.3d_mouse_dead_zone": "Мёртвая зона",
|
||||
"ui.3d_mouse_dead_zone_tip": "Насколько нужно сдвинуть колпачок, прежде чем сдвинется вид, в доле от полного хода. Увеличьте, если вид смещается, когда колпачок в покое.",
|
||||
"ui.3d_mouse_denied": "SolidCraft видит 3D-мышь, но не имеет права её читать. Правило udev описано в заметках по настройке 3D-мыши.",
|
||||
"ui.3d_mouse_denied_short": "Найдена, но недоступна для чтения",
|
||||
"ui.3d_mouse_driven_by": "Какое откалиброванное движение устройства управляет этой осью. Если выбрать движение, которое уже использует другая ось, они поменяются местами.",
|
||||
"ui.3d_mouse_driven_by_label": "управляется",
|
||||
"ui.3d_mouse_driver": "Драйвер 3D-мыши",
|
||||
"ui.3d_mouse_driver_auto": "Автоматически",
|
||||
"ui.3d_mouse_driver_direct": "Напрямую (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Пока чтение идёт через spacenavd: само устройство открыть не удалось.",
|
||||
"ui.3d_mouse_driver_only_direct": "Чтение напрямую с устройства; spacenavd не запущен",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Чтение через spacenavd, других вариантов нет",
|
||||
"ui.3d_mouse_forget": "Забыть калибровку",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Область просмотра слева",
|
||||
"ui.3d_mouse_ghost_left_window": "Окно слева",
|
||||
"ui.3d_mouse_ghost_position": "Положение",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Область просмотра справа",
|
||||
"ui.3d_mouse_hint": "Совместное перемещение и вращение вида; в эскизе — только панорама и масштаб.",
|
||||
"ui.3d_mouse_kind_cap": "Возврат в центр",
|
||||
"ui.3d_mouse_kind_question": "Какая у вас 3D-мышь?",
|
||||
"ui.3d_mouse_kind_trackball": "Трекбол",
|
||||
"ui.3d_mouse_looks_like_cap": "Значение держится, пока вы удерживаете, — как у устройства типа «Возврат в центр».",
|
||||
"ui.3d_mouse_looks_like_trackball": "Значение возвращается, как только вы останавливаетесь, — как у трекбола.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Вращение X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Вращение Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Вращение Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Смещение X",
|
||||
"ui.3d_mouse_motion_translation_y": "Смещение Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Смещение Z",
|
||||
"ui.3d_mouse_navigate": "Навигация с помощью 3D-мыши",
|
||||
"ui.3d_mouse_none": "3D-мышь не найдена",
|
||||
"ui.3d_mouse_not_calibrated": "Не откалибрована",
|
||||
"ui.3d_mouse_offer": "Калибровка 3D-мыши исправляет перепутанные или перевёрнутые оси и настраивает скорость и ощущения.",
|
||||
"ui.3d_mouse_offer_never": "Больше не показывать",
|
||||
"ui.3d_mouse_offer_swap": "Переключить на Hidraw (прямой драйвер)",
|
||||
"ui.3d_mouse_reading": "Показания",
|
||||
"ui.3d_mouse_reset_axes": "Сбросить назначение осей",
|
||||
"ui.3d_mouse_reset_view": "Сбросить вид",
|
||||
"ui.3d_mouse_restore": "Восстановить настройки 3D-мыши",
|
||||
"ui.3d_mouse_restore_ask": "Вернуть настройки 3D-мыши к значениям по умолчанию? Калибровка и назначение кнопок останутся.",
|
||||
"ui.3d_mouse_section": "3D-мышь",
|
||||
"ui.3d_mouse_show_in_view": "Показывать 3D-мышь в 3D-виде",
|
||||
"ui.3d_mouse_show_in_view_tip": "Изображение вашей 3D-мыши в левом нижнем углу, которое двигается вместе с ней.",
|
||||
"ui.3d_mouse_skip": "Моё устройство так не умеет",
|
||||
"ui.3d_mouse_skip_tip": "Пропускает это движение в обе стороны; калибровка его не учитывает.",
|
||||
"ui.3d_mouse_speed": "Скорость 3D-мыши",
|
||||
"ui.3d_mouse_step_lift": "Поднимите колпачок вверх, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_press": "Нажмите на колпачок вниз, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_pull": "Потяните колпачок на себя, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_push": "Толкните колпачок от себя, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_rest": "Уберите руку с колпачка и не трогайте его.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Уберите руку с шара и не трогайте его.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Поверните кольцо прокрутки на несколько щелчков по часовой стрелке.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Поверните кольцо прокрутки на несколько щелчков против часовой стрелки.",
|
||||
"ui.3d_mouse_step_roll_away": "Прокатите шар от себя несколько раз и дайте ему остановиться.",
|
||||
"ui.3d_mouse_step_roll_left": "Прокатите шар влево несколько раз и дайте ему остановиться.",
|
||||
"ui.3d_mouse_step_roll_right": "Прокатите шар вправо несколько раз и дайте ему остановиться.",
|
||||
"ui.3d_mouse_step_roll_toward": "Прокатите шар к себе несколько раз и дайте ему остановиться.",
|
||||
"ui.3d_mouse_step_slide_left": "Сдвиньте колпачок влево, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_slide_right": "Сдвиньте колпачок вправо, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Покрутите шар по часовой стрелке несколько раз и дайте ему остановиться.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Покрутите шар против часовой стрелки несколько раз и дайте ему остановиться.",
|
||||
"ui.3d_mouse_step_tilt_away": "Наклоните верх колпачка от себя (ближний край поднимается), удержите и отпустите.",
|
||||
"ui.3d_mouse_step_tilt_left": "Наклоните колпачок влево (левый край вниз), удержите и отпустите.",
|
||||
"ui.3d_mouse_step_tilt_right": "Наклоните колпачок вправо (правый край вниз), удержите и отпустите.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Наклоните верх колпачка к себе (ближний край опускается), удержите и отпустите.",
|
||||
"ui.3d_mouse_step_touch": "Положите руку на колпачок, не нажимая на него.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Поверните колпачок по часовой стрелке, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Поверните колпачок против часовой стрелки, удержите и отпустите.",
|
||||
"ui.3d_mouse_switch_cap": "Калибровать как возврат в центр",
|
||||
"ui.3d_mouse_switch_trackball": "Калибровать как трекбол",
|
||||
"ui.3d_mouse_view_axis": "Ось вида",
|
||||
"ui.3d_mouse_view_off": "Выкл.",
|
||||
"ui.3d_mouse_view_off_tip": "Профиль драйвера 3D-мыши отключает эту ось, поэтому её движение не двигает вид. Если выбрать здесь движение другой оси, они поменяются местами, и это движение перестанет двигать вид.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Орбита влево / вправо",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Орбита вверх / вниз",
|
||||
"ui.3d_mouse_view_pan_sideways": "Сдвиг влево / вправо",
|
||||
"ui.3d_mouse_view_pan_up_down": "Сдвиг вверх / вниз",
|
||||
"ui.3d_mouse_view_roll": "Крен",
|
||||
"ui.3d_mouse_view_unmapped": "Ось {n}",
|
||||
"ui.3d_mouse_view_zoom": "Масштаб",
|
||||
"ui.3d_mouse_waiting": "Ожидание движения",
|
||||
"ui.3d_mouse_wiggle": "Если 3D-мышь подключена, слегка подвигайте её.",
|
||||
"ui.3d_sketch": "3D-эскиз",
|
||||
"ui.activate": "Активировать",
|
||||
"ui.active_component": "Активный компонент",
|
||||
@@ -1182,6 +1293,7 @@
|
||||
"ui.agent_chat_files_left_out": "К одному сообщению можно приложить не более {max} файлов; не добавлены: {files}",
|
||||
"ui.agent_chat_files_loading": "Ожидание чтения файлов",
|
||||
"ui.agent_chat_files_max": "Не более {max} файлов в одном сообщении",
|
||||
"ui.agent_chat_flatpak_none_found": "Кодирующий агент не найден. По умолчанию песочница Flatpak не видит агентов, установленных в домашней папке, и не имеет доступа к сети. Чтобы разрешить Claude Code и его адаптер ACP, установленные через nvm или в ~/.local/bin, выполните эту команду в терминале и перезапустите SolidCraft. Она даёт доступ к перечисленным программам и файлам входа, а также доступ к сети:",
|
||||
"ui.agent_chat_import_failed": "{name}: не удалось импортировать: {error}",
|
||||
"ui.agent_chat_intro": "Общайтесь с уже используемым кодирующим агентом или выбранной конечной точкой модели. Он работает с открытым дизайном через инструменты SolidCraft: изменения видны по мере выполнения и отменяются одним шагом на сообщение. Что ему разрешено без запроса, задаётся в Настройки > ИИ.",
|
||||
"ui.agent_chat_key_in_env": "ключ задан",
|
||||
@@ -1277,6 +1389,7 @@
|
||||
"ui.ai_http_busy": "конечная точка занята или превысила лимит; повторите позже",
|
||||
"ui.ai_http_key": "конечная точка отклонила ключ: проверьте его в Настройки > ИИ",
|
||||
"ui.ai_http_not_found": "нет такой конечной точки или модели: проверьте базовый URL и имя модели",
|
||||
"ui.ai_http_too_large": "разговор слишком велик для этой конечной точки; начните новый чат",
|
||||
"ui.ai_key": "Ключ API",
|
||||
"ui.ai_key_hint": "необязательно; хранится в системной связке ключей",
|
||||
"ui.ai_key_needs_https": "Ключ отправляется только по HTTPS или на этот компьютер.",
|
||||
@@ -1304,7 +1417,6 @@
|
||||
"ui.ai_permissions": "Разрешения",
|
||||
"ui.ai_preset": "Пресет",
|
||||
"ui.ai_remove": "Удалить",
|
||||
"ui.ai_request_limit": "остановлено после 24 запросов на одно сообщение; отправьте ещё одно сообщение, чтобы продолжить",
|
||||
"ui.ai_reset": "Сбросить",
|
||||
"ui.ai_settings": "Настройки ИИ…",
|
||||
"ui.ai_show_features": "Показывать функции ИИ",
|
||||
@@ -1349,6 +1461,7 @@
|
||||
"ui.blocked": "Заблокировано",
|
||||
"ui.bodies": "Тела",
|
||||
"ui.bodies_all_if_none": "Тела (все, если не выбраны)",
|
||||
"ui.bottom": "Снизу",
|
||||
"ui.box": "Бокс",
|
||||
"ui.break": "Разорвать",
|
||||
"ui.browser_bodies_one": "{count} тело",
|
||||
@@ -1480,6 +1593,8 @@
|
||||
"ui.filter_commands_or_keys": "Фильтр команд или клавиш",
|
||||
"ui.findInTimeline": "Найти на шкале",
|
||||
"ui.findInWindow": "Найти в окне",
|
||||
"ui.fine": "Высокое",
|
||||
"ui.finest": "Максимальное",
|
||||
"ui.finish_sketch": "Завершить эскиз",
|
||||
"ui.fit": "Вписать",
|
||||
"ui.fix_unfix": "Зафиксировать/Освободить",
|
||||
@@ -1501,6 +1616,7 @@
|
||||
"ui.grid_follows_view_hint": "На боковых видах сетка показывается на вертикальной плоскости, обращённой к вам, а не на земле",
|
||||
"ui.ground_shadow": "Тень на земле",
|
||||
"ui.groupSelected": "Группировать выбранное",
|
||||
"ui.hatch": "Штриховка",
|
||||
"ui.heading_add_ins": "НАДСТРОЙКИ",
|
||||
"ui.heading_assemble": "СБОРКА",
|
||||
"ui.heading_box": "БОКС",
|
||||
@@ -1567,6 +1683,7 @@
|
||||
"ui.heading_torus": "ТОР",
|
||||
"ui.heading_utilities": "УТИЛИТЫ",
|
||||
"ui.heading_utility": "УТИЛИТА",
|
||||
"ui.heading_weldments": "СВАРКА",
|
||||
"ui.help": "Справка",
|
||||
"ui.hide": "Скрыть",
|
||||
"ui.hideAxes": "Скрыть оси",
|
||||
@@ -1703,6 +1820,7 @@
|
||||
"ui.project_face_edges_sketch": "Проецировать рёбра граней в новые эскизы",
|
||||
"ui.properties": "Свойства",
|
||||
"ui.pull_direction": "Направление вытягивания",
|
||||
"ui.quality": "Качество",
|
||||
"ui.quantity": "Количество",
|
||||
"ui.quantity_2": "Количество 2",
|
||||
"ui.quit": "Выход",
|
||||
@@ -1791,65 +1909,13 @@
|
||||
"ui.smooth_wheel_zoom": "Плавное масштабирование колесом",
|
||||
"ui.smooth_wheel_zoom_tip": "Отключите, чтобы каждое движение колеса применялось сразу, без сглаживания",
|
||||
"ui.snap": "Привязка",
|
||||
"ui.space_mouse_axes_reassigned": "Оси переназначены в настройках, и это действует и поверх этой калибровки.",
|
||||
"ui.space_mouse_calibrate": "Калибровать…",
|
||||
"ui.space_mouse_calibrate_tip": "Двигайте колпачок так, как просит окно, чтобы SolidCraft узнал, куда и насколько далеко движется каждая ось этой 3D-мыши. Сохраняется для этой 3D-мыши.",
|
||||
"ui.space_mouse_calibrate_title": "Калибровка 3D-мыши",
|
||||
"ui.space_mouse_calibrated": "Откалибрована",
|
||||
"ui.space_mouse_calibration": "Калибровка 3D-мыши",
|
||||
"ui.space_mouse_calibration_done": "Готово. Сохраните её, чтобы она применялась всякий раз, когда подключена эта 3D-мышь.",
|
||||
"ui.space_mouse_calibration_no_response": "Одна ось почти не сдвинулась, поэтому её нельзя откалибровать. Начните заново и каждый раз двигайте колпачок дальше.",
|
||||
"ui.space_mouse_dead_zone": "Мёртвая зона",
|
||||
"ui.space_mouse_dead_zone_tip": "Насколько нужно сдвинуть колпачок, прежде чем сдвинется вид, в доле от полного хода. Увеличьте, если вид смещается, когда колпачок в покое.",
|
||||
"ui.space_mouse_driven_by": "Какое откалиброванное движение устройства управляет этой осью. Если выбрать движение, которое уже использует другая ось, они поменяются местами.",
|
||||
"ui.space_mouse_driven_by_label": "управляется",
|
||||
"ui.space_mouse_driver": "Драйвер 3D-мыши",
|
||||
"ui.space_mouse_driver_auto": "Автоматически",
|
||||
"ui.space_mouse_driver_direct": "Напрямую (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Чтение напрямую с устройства; spacenavd не запущен",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Чтение через spacenavd, других вариантов нет",
|
||||
"ui.space_mouse_forget": "Забыть калибровку",
|
||||
"ui.space_mouse_hint": "Совместное перемещение и вращение вида; в эскизе — только панорама и масштаб.",
|
||||
"ui.space_mouse_motion_rotation_x": "Вращение X",
|
||||
"ui.space_mouse_motion_rotation_y": "Вращение Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Вращение Z",
|
||||
"ui.space_mouse_motion_translation_x": "Смещение X",
|
||||
"ui.space_mouse_motion_translation_y": "Смещение Y",
|
||||
"ui.space_mouse_motion_translation_z": "Смещение Z",
|
||||
"ui.space_mouse_navigate": "Навигация через SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "3D-мышь не найдена",
|
||||
"ui.space_mouse_not_calibrated": "Не откалибрована",
|
||||
"ui.space_mouse_reading": "Показания",
|
||||
"ui.space_mouse_reset_axes": "Сбросить назначение осей",
|
||||
"ui.space_mouse_step_lift": "Поднимите колпачок вверх, удержите и отпустите.",
|
||||
"ui.space_mouse_step_press": "Нажмите на колпачок вниз, удержите и отпустите.",
|
||||
"ui.space_mouse_step_pull": "Потяните колпачок на себя, удержите и отпустите.",
|
||||
"ui.space_mouse_step_push": "Толкните колпачок от себя, удержите и отпустите.",
|
||||
"ui.space_mouse_step_rest": "Уберите руку с колпачка и не трогайте его.",
|
||||
"ui.space_mouse_step_slide_left": "Сдвиньте колпачок влево, удержите и отпустите.",
|
||||
"ui.space_mouse_step_slide_right": "Сдвиньте колпачок вправо, удержите и отпустите.",
|
||||
"ui.space_mouse_step_tilt_back": "Наклоните колпачок назад (передний край вверх), удержите и отпустите.",
|
||||
"ui.space_mouse_step_tilt_forward": "Наклоните колпачок вперёд (передний край вниз), удержите и отпустите.",
|
||||
"ui.space_mouse_step_tilt_left": "Наклоните колпачок влево (левый край вниз), удержите и отпустите.",
|
||||
"ui.space_mouse_step_tilt_right": "Наклоните колпачок вправо (правый край вниз), удержите и отпустите.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Поверните колпачок по часовой стрелке, удержите и отпустите.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Поверните колпачок против часовой стрелки, удержите и отпустите.",
|
||||
"ui.space_mouse_view_axis": "Ось вида",
|
||||
"ui.space_mouse_view_off": "Выкл.",
|
||||
"ui.space_mouse_view_off_tip": "Профиль драйвера 3D-мыши отключает эту ось, поэтому её движение не двигает вид. Если выбрать здесь движение другой оси, они поменяются местами, и это движение перестанет двигать вид.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Орбита влево / вправо",
|
||||
"ui.space_mouse_view_orbit_up_down": "Орбита вверх / вниз",
|
||||
"ui.space_mouse_view_pan_sideways": "Сдвиг влево / вправо",
|
||||
"ui.space_mouse_view_pan_up_down": "Сдвиг вверх / вниз",
|
||||
"ui.space_mouse_view_roll": "Крен",
|
||||
"ui.space_mouse_view_unmapped": "Ось {n}",
|
||||
"ui.space_mouse_view_zoom": "Масштаб",
|
||||
"ui.spacing": "Интервал",
|
||||
"ui.spacing_2": "Интервал 2",
|
||||
"ui.sphere": "Сфера",
|
||||
"ui.spline": "Сплайн",
|
||||
"ui.split_body": "Разделить тело",
|
||||
"ui.stand_up_tip": "Если файл STEP или IGES открывается с Z вверх, предложить поворот на четверть оборота вокруг X",
|
||||
"ui.standard": "Стандартное",
|
||||
"ui.start": "Старт",
|
||||
"ui.start_over": "Начать заново",
|
||||
"ui.start_page": "Начальная страница",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Välj…",
|
||||
"Circular": "Cirkulär",
|
||||
"Circumference": "Omkrets",
|
||||
"Clockwise": "Medurs",
|
||||
"Close draft": "Stäng utkast",
|
||||
"Collisions": "Kollisioner",
|
||||
"Colour": "Färg",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Sök kommandon",
|
||||
"Search this library": "Sök i detta bibliotek",
|
||||
"Section": "Sektion",
|
||||
"Section position": "Profilens läge",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Markera annan: håll vänsterknappen stilla på modellen (eller högerklicka › Markera annan…) för att lista allt under markören; klicka igen på samma plats för att markera nästa objekt där. Dubbelklicka med mittenknappen för att gå till Hem (Inställningar › Navigering).",
|
||||
"Select bodies (a face picks its body)": "Markera kroppar (en yta väljer sin kropp)",
|
||||
"Select bodies or faces instead": "Markera kroppar eller ytor istället",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Visa agentmarkör",
|
||||
"Show in folder": "Visa i mapp",
|
||||
"Show the pointer an AI agent moves (#35)": "Visa pekaren som en AI-agent flyttar (#35)",
|
||||
"Show tool at cursor": "Visa verktyget vid markören",
|
||||
"Signed axes": "Axlar med tecken",
|
||||
"Size": "Storlek",
|
||||
"Sketch constraints": "Skissvillkor",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft stängdes innan dessa konstruktioner sparades. Deras senaste autospar kan öppnas.",
|
||||
"Specialise": "Specialisera",
|
||||
"Speed": "Hastighet",
|
||||
"Square": "Kvadrat",
|
||||
"Stand it up": "Res upp den",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Res upp den: ett kvartsvarv runt X, som en enda Flytta-funktion som du kan ångra.",
|
||||
"Stand this part up?": "Resa upp denna del?",
|
||||
"Start angle": "Startvinkel",
|
||||
"Start bulge": "Startbukt",
|
||||
"Start continuity": "Startkontinuitet",
|
||||
"Step": "Steg",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "vinkel, höjd",
|
||||
"appearance.dielectric_ior": "Dielektriskt IOR",
|
||||
"appearance.metallic": "Metallisk",
|
||||
"appearance.only_available": "Endast tillgängliga utseenden",
|
||||
"appearance.pbr_help": "Visa den här ytan i Rendering eller en produkt-PNG. Ingen refraktion eller transmission.",
|
||||
"appearance.pbr_opaque": "Ogenomskinlig PBR",
|
||||
"appearance.roughness": "Skrovlighet",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "Infoga STEP",
|
||||
"cmd.file_save_mesh": "Spara som mesh",
|
||||
"cmd.file_slicers": "Installerade slicers",
|
||||
"cmd.frame_member": "Ramdel",
|
||||
"cmd.frame_profile_insert": "Infoga profil",
|
||||
"cmd.frame_profile_list": "Profilbibliotek",
|
||||
"cmd.frame_profile_properties": "Profilegenskaper",
|
||||
"cmd.inspect_accessibility": "Tillgänglighetsanalys",
|
||||
"cmd.inspect_center_of_mass": "Masscentrum",
|
||||
"cmd.inspect_clearance": "Frigång",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Nytt fysiskt material",
|
||||
"cmd.material_delete": "Ta bort fysiskt material",
|
||||
"cmd.material_edit": "Redigera fysiskt material",
|
||||
"cmd.material_inspect": "Granska fysiskt material",
|
||||
"cmd.material_list": "Lista material",
|
||||
"cmd.material_properties_directional": "Riktningsberoende elasticitetsmodul",
|
||||
"cmd.material_properties_validate": "Validera fysiska materialdata",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Nyckel här",
|
||||
"motion.key_here_hint": "Lägg till en nyckel vid detta steg med ledens nuvarande värde",
|
||||
"motion.no_collisions": "Inga kollisioner under studien",
|
||||
"motion.plot_quantity": "Antal",
|
||||
"motion.step_back": "Steg bakåt",
|
||||
"motion.step_forward": "Steg framåt",
|
||||
"motion.to_end": "Till slutet",
|
||||
@@ -1059,7 +1071,7 @@
|
||||
"search.try": "Skriv ett kommandonamn, en funktion i denna konstruktion eller en parameter. För att köra ett kommando med värden, skriv dess id och ett JSON-objekt: {example}",
|
||||
"search.valid": "Giltig",
|
||||
"settings.display_quality_note": "Finare nivåer ritar krökta ytor och kanter jämnare och tar längre tid att mesha. Endast vyporten ändras, aldrig konstruktionen eller exporter.",
|
||||
"settings.frame_rate_note": "Visar bildrutor per sekund i vyns nedre högra hörn. Medan det är på ritas vyn om kontinuerligt.",
|
||||
"settings.frame_rate_note": "Visar bildrutor per sekund i vyns nedre högra hörn.",
|
||||
"settings.measure_axis_distances": "Visa X/Y/Z-avstånd",
|
||||
"settings.measure_axis_distances_help": "Visa världsaxelns komponentmått bredvid det totala avståndet medan Mät är öppet.",
|
||||
"settings.msaa_applies_on_restart": "Kantutjämning tillämpas nästa gång SolidCraft startar",
|
||||
@@ -1120,7 +1132,50 @@
|
||||
"trace.stopped_by_contact": "Slutar vid prov {sample}: {a} skulle gå igenom {b}",
|
||||
"trace.stopped_by_loop": "Slutar vid prov {sample}: slingan genom {joint} kan inte stängas längre",
|
||||
"ui.1_degree_of_freedom": "1 frihetsgrad",
|
||||
"ui.3d_mouse_calibrate": "Kalibrera…",
|
||||
"ui.3d_mouse_calibrate_tip": "Flytta knappen som fönstret ber om, så att SolidCraft lär sig åt vilket håll och hur långt varje axel på denna 3D-mus går. Sparas för denna 3D-mus.",
|
||||
"ui.3d_mouse_calibrate_title": "Kalibrera 3D-mus",
|
||||
"ui.3d_mouse_calibrate_wait": "Rör 3D-musen en gång så att SolidCraft ser den.",
|
||||
"ui.3d_mouse_calibrated": "Kalibrerad",
|
||||
"ui.3d_mouse_calibration": "Kalibrering av 3D-mus",
|
||||
"ui.3d_mouse_calibration_done": "Klart. Spara den för att använda den när denna 3D-mus är ansluten.",
|
||||
"ui.3d_mouse_calibration_no_response": "En axel rörde sig knappt, så den kan inte kalibreras. Börja om och flytta knappen längre varje gång.",
|
||||
"ui.3d_mouse_circles_help": "Cirklarna visar vad enheten skickar efter kalibrering. Testmodellen visar vad vyn gör med det, efter inversion, hastighet och axeltilldelning.",
|
||||
"ui.3d_mouse_dead_zone": "Dödzon",
|
||||
"ui.3d_mouse_dead_zone_tip": "Hur långt knappen måste röra sig innan vyn gör det, som andel av dess fulla rörelse. Höj den om vyn driver medan knappen vilar.",
|
||||
"ui.3d_mouse_denied": "SolidCraft ser en 3D-mus men får inte läsa den. Se installationsanteckningarna för 3D-musen om udev-regeln.",
|
||||
"ui.3d_mouse_denied_short": "Hittad, men går inte att läsa",
|
||||
"ui.3d_mouse_driver_fallback": "Läser tills vidare via spacenavd: själva enheten kunde inte öppnas.",
|
||||
"ui.3d_mouse_forget": "Glöm kalibrering",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Vyn vänster",
|
||||
"ui.3d_mouse_ghost_left_window": "Fönster vänster",
|
||||
"ui.3d_mouse_ghost_position": "Placering",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Vyn höger",
|
||||
"ui.3d_mouse_hint": "Flytta och rotera vyn tillsammans; skisser använder endast panorering och zoom.",
|
||||
"ui.3d_mouse_navigate": "Navigera med 3D-musen",
|
||||
"ui.3d_mouse_none": "Ingen 3D-mus hittades",
|
||||
"ui.3d_mouse_not_calibrated": "Inte kalibrerad",
|
||||
"ui.3d_mouse_offer": "Att kalibrera din 3D-mus rättar förväxlade eller omvända axlar och ställer in hastighet och känsla.",
|
||||
"ui.3d_mouse_offer_never": "Visa inte igen",
|
||||
"ui.3d_mouse_offer_swap": "Byt till Hidraw (direkt drivrutin)",
|
||||
"ui.3d_mouse_reading": "Avläsning",
|
||||
"ui.3d_mouse_reset_view": "Återställ vyn",
|
||||
"ui.3d_mouse_restore": "Återställ 3D-musens standardvärden",
|
||||
"ui.3d_mouse_restore_ask": "Vill du återställa 3D-musens inställningar till standard? Din kalibrering och knapptilldelning behålls.",
|
||||
"ui.3d_mouse_speed": "3D-musens hastighet",
|
||||
"ui.3d_mouse_step_lift": "Lyft knappen uppåt, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_press": "Tryck ned knappen, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_pull": "Dra knappen mot dig, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_push": "Skjut knappen bort från dig, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_rest": "Ta bort handen från knappen och låt den vara stilla.",
|
||||
"ui.3d_mouse_step_slide_left": "Skjut knappen åt vänster, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_slide_right": "Skjut knappen åt höger, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_tilt_left": "Luta knappen åt vänster (vänsterkanten nedåt), håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_tilt_right": "Luta knappen åt höger (högerkanten nedåt), håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Vrid knappen medurs, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Vrid knappen moturs, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_waiting": "Väntar på rörelse",
|
||||
"ui.3d_mouse_wiggle": "Om du har en ansluten, rör lite på den.",
|
||||
"ui.3d_sketch": "3D-skiss",
|
||||
"ui.activate": "Aktivera",
|
||||
"ui.active_component": "Aktiv komponent",
|
||||
@@ -1182,6 +1237,7 @@
|
||||
"ui.agent_chat_files_left_out": "Högst {max} filer följer med ett meddelande; utelämnade: {files}",
|
||||
"ui.agent_chat_files_loading": "Väntar på att filerna ska läsas",
|
||||
"ui.agent_chat_files_max": "Högst {max} filer i ett meddelande",
|
||||
"ui.agent_chat_flatpak_none_found": "Ingen kodningsagent hittades. Flatpak-sandlådan ser som standard inte agenter som är installerade i din hemmapp och har ingen nätverksåtkomst. För att tillåta Claude Code och dess ACP-adapter installerade via nvm eller i ~/.local/bin kör du det här kommandot i en terminal och startar sedan om SolidCraft. Det ger åtkomst till de angivna programmen och inloggningsfilerna samt nätverksåtkomst:",
|
||||
"ui.agent_chat_import_failed": "{name}: importen misslyckades: {error}",
|
||||
"ui.agent_chat_intro": "Chatta med en kodningsagent du redan använder, eller en modellslutpunkt du väljer. Den arbetar med den öppna konstruktionen via SolidCrafts verktyg: dess ändringar syns medan den gör dem och ångras som ett steg per meddelande. Vad den får göra utan att fråga ställs in under Inställningar > AI.",
|
||||
"ui.agent_chat_key_in_env": "nyckel angiven",
|
||||
@@ -1277,6 +1333,7 @@
|
||||
"ui.ai_http_busy": "slutpunkten är upptagen eller över sin gräns; försök igen senare",
|
||||
"ui.ai_http_key": "slutpunkten nekade nyckeln: kontrollera den under Inställningar > AI",
|
||||
"ui.ai_http_not_found": "ingen sådan slutpunkt eller modell: kontrollera bas-URL:en och modellnamnet",
|
||||
"ui.ai_http_too_large": "konversationen är för stor för den här slutpunkten; starta en ny chatt",
|
||||
"ui.ai_key": "API-nyckel",
|
||||
"ui.ai_key_hint": "valfritt; lagras i systemets nyckelring",
|
||||
"ui.ai_key_needs_https": "En nyckel skickas endast via HTTPS, eller till den här datorn.",
|
||||
@@ -1304,7 +1361,6 @@
|
||||
"ui.ai_permissions": "Behörigheter",
|
||||
"ui.ai_preset": "Förinställning",
|
||||
"ui.ai_remove": "Ta bort",
|
||||
"ui.ai_request_limit": "stoppades efter 24 förfrågningar för ett meddelande; skicka ett nytt meddelande för att fortsätta",
|
||||
"ui.ai_reset": "Återställ",
|
||||
"ui.ai_settings": "AI-inställningar…",
|
||||
"ui.ai_show_features": "Visa AI-funktioner",
|
||||
@@ -1480,6 +1536,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtrera kommandon eller tangenter",
|
||||
"ui.findInTimeline": "Sök i tidslinjen",
|
||||
"ui.findInWindow": "Sök i fönstret",
|
||||
"ui.fine": "Fin",
|
||||
"ui.finest": "Finast",
|
||||
"ui.finish_sketch": "Avsluta skiss",
|
||||
"ui.fit": "Anpassa",
|
||||
"ui.fix_unfix": "Fäst/Lös",
|
||||
@@ -1501,6 +1559,7 @@
|
||||
"ui.grid_follows_view_hint": "Sidovyer visar rutnätet på det upprätta planet som vetter mot dem istället för marken",
|
||||
"ui.ground_shadow": "Markskugga",
|
||||
"ui.groupSelected": "Gruppera markerade",
|
||||
"ui.hatch": "Skraffering",
|
||||
"ui.heading_add_ins": "TILLÄGG",
|
||||
"ui.heading_assemble": "MONTERA",
|
||||
"ui.heading_box": "LÅDA",
|
||||
@@ -1567,6 +1626,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "VERKTYG",
|
||||
"ui.heading_utility": "VERKTYG",
|
||||
"ui.heading_weldments": "SVETSNING",
|
||||
"ui.help": "Hjälp",
|
||||
"ui.hide": "Dölj",
|
||||
"ui.hideAxes": "Dölj axlar",
|
||||
@@ -1703,6 +1763,7 @@
|
||||
"ui.project_face_edges_sketch": "Projicera ytkanter i nya skisser",
|
||||
"ui.properties": "Egenskaper",
|
||||
"ui.pull_direction": "Dragriktning",
|
||||
"ui.quality": "Kvalitet",
|
||||
"ui.quantity": "Antal",
|
||||
"ui.quantity_2": "Antal 2",
|
||||
"ui.quit": "Avsluta",
|
||||
@@ -1791,40 +1852,13 @@
|
||||
"ui.smooth_wheel_zoom": "Jämn hjulzoom",
|
||||
"ui.smooth_wheel_zoom_tip": "Stäng av för att tillämpa varje hjulhändelse direkt, utan utjämningsfördröjning",
|
||||
"ui.snap": "Snappa",
|
||||
"ui.space_mouse_calibrate": "Kalibrera…",
|
||||
"ui.space_mouse_calibrate_tip": "Flytta knappen som fönstret ber om, så att SolidCraft lär sig åt vilket håll och hur långt varje axel på denna 3D-mus går. Sparas för denna 3D-mus.",
|
||||
"ui.space_mouse_calibrate_title": "Kalibrera 3D-mus",
|
||||
"ui.space_mouse_calibrated": "Kalibrerad",
|
||||
"ui.space_mouse_calibration": "Kalibrering av 3D-mus",
|
||||
"ui.space_mouse_calibration_done": "Klart. Spara den för att använda den när denna 3D-mus är ansluten.",
|
||||
"ui.space_mouse_calibration_no_response": "En axel rörde sig knappt, så den kan inte kalibreras. Börja om och flytta knappen längre varje gång.",
|
||||
"ui.space_mouse_dead_zone": "Dödzon",
|
||||
"ui.space_mouse_dead_zone_tip": "Hur långt knappen måste röra sig innan vyn gör det, som andel av dess fulla rörelse. Höj den om vyn driver medan knappen vilar.",
|
||||
"ui.space_mouse_forget": "Glöm kalibrering",
|
||||
"ui.space_mouse_hint": "Flytta och rotera vyn tillsammans; skisser använder endast panorering och zoom.",
|
||||
"ui.space_mouse_navigate": "Navigera med SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Ingen 3D-mus hittades",
|
||||
"ui.space_mouse_not_calibrated": "Inte kalibrerad",
|
||||
"ui.space_mouse_reading": "Avläsning",
|
||||
"ui.space_mouse_step_lift": "Lyft knappen uppåt, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_press": "Tryck ned knappen, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_pull": "Dra knappen mot dig, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_push": "Skjut knappen bort från dig, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_rest": "Ta bort handen från knappen och låt den vara stilla.",
|
||||
"ui.space_mouse_step_slide_left": "Skjut knappen åt vänster, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_slide_right": "Skjut knappen åt höger, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_tilt_back": "Luta knappen bakåt (framkanten uppåt), håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_tilt_forward": "Luta knappen framåt (framkanten nedåt), håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_tilt_left": "Luta knappen åt vänster (vänsterkanten nedåt), håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_tilt_right": "Luta knappen åt höger (högerkanten nedåt), håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Vrid knappen medurs, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Vrid knappen moturs, håll den och släpp sedan.",
|
||||
"ui.spacing": "Avstånd",
|
||||
"ui.spacing_2": "Avstånd 2",
|
||||
"ui.sphere": "Sfär",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Dela kropp",
|
||||
"ui.stand_up_tip": "När en STEP- eller IGES-fil öppnas med Z uppåt, erbjud ett kvartsvarv runt X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Börja om",
|
||||
"ui.start_page": "Startsida",
|
||||
|
||||