mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-12 03:31:57 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 132106bb59 | |||
| 7d570dec06 | |||
| 05c4cabc33 | |||
| ed462d273e | |||
| c6817d49bc | |||
| 34ff474493 | |||
| ed8c42b117 | |||
| 4a46afb867 | |||
| d98d06fd01 | |||
| ac0b0b56c3 | |||
| 10882b74af | |||
| 81221ef97e | |||
| 9c79364983 | |||
| bd69229bfd | |||
| abf17984e6 | |||
| cacdd84430 | |||
| bd3b3c0897 | |||
| c56fa868e2 | |||
| c6bd384a2d | |||
| 0a2171337b | |||
| 58ed908743 | |||
| 9ed839901c | |||
| fff55b44a8 | |||
| f43ab6a42c | |||
| f756298665 | |||
| a65ecb004a | |||
| 7b3a08563f | |||
| 70339124b1 |
@@ -707,6 +707,8 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
|
||||
| docs/design/preferences/figures/mouse-mapping.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/navigator-axes.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/navigator-axes.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/navigator-cube.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/navigator-cube.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-centre.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-centre.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-cursor.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
|
||||
@@ -288,6 +288,16 @@ CI job runs `cargo xtask oracle` during full validation, and on pull requests th
|
||||
fails because of a known bug names its issue in `expected.json` (`"known_failure": "#123"`);
|
||||
never change an expected value to match SolidCraft's output.
|
||||
|
||||
### Quarantining a flaky test
|
||||
|
||||
A test that fails at random can fail a merge-queue batch, and every batch behind it starts over.
|
||||
Open an issue for it, then add one line to [`tests/QUARANTINED_TESTS`](tests/QUARANTINED_TESTS):
|
||||
`<test> #<issue> <YYYY-MM-DD> [why]`, with the test named as in `tests/KNOWN_FAILING_TESTS`.
|
||||
A quarantined test still runs. If it fails, the job summary says `quarantined, failed: <test>; see #<issue>`
|
||||
and the job stays green; if it passes, the summary says that too, so you can tell when it is safe to
|
||||
release. A quarantined line may be at most 14 days old: `cargo xtask lists` fails after that, so fix the
|
||||
test and delete the line. A test that fails every time belongs on `KNOWN_FAILING_TESTS` instead.
|
||||
|
||||
## Changing the design file format
|
||||
|
||||
A design file says which format it is in (`"format": "solidcraft/N"`). If your change saves
|
||||
|
||||
@@ -1770,7 +1770,7 @@ mod prefs_writer_tests {
|
||||
let now = Instant::now();
|
||||
assert_eq!(writer.store(&mut disk, app.prefs(), now), None);
|
||||
assert_eq!(disk.flushes, 0, "the settings read at start are not written again");
|
||||
app.preferences.navigator_style = NavigatorStyle::Custom;
|
||||
app.preferences.navigator_style = NavigatorStyle::Cube;
|
||||
app.preferences.display_quality = "fine".into();
|
||||
assert!(writer.due(true, now), "a frame with input is checked");
|
||||
assert_eq!(writer.store(&mut disk, app.prefs(), now), None);
|
||||
@@ -1782,7 +1782,7 @@ mod prefs_writer_tests {
|
||||
let mut next = new_app();
|
||||
let stored = solidcraft_ui_egui::prefs::stored(|k| disk.flushed.get(k).cloned()).expect("settings on disk");
|
||||
next.load_prefs(&stored);
|
||||
assert_eq!(next.preferences.navigator_style, NavigatorStyle::Custom);
|
||||
assert_eq!(next.preferences.navigator_style, NavigatorStyle::Cube);
|
||||
assert_eq!(next.preferences.display_quality, "fine");
|
||||
assert!(disk.flushed.contains_key(STORAGE_KEY));
|
||||
}
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "إنشاء بلاغ…",
|
||||
"Cross curves": "المنحنيات العرضية",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter يتابع في GitHub.",
|
||||
"Cube": "مكعب",
|
||||
"Current orthographic view (1:1 mm)": "العرض المتعامد الحالي (1:1 mm)",
|
||||
"Current perspective view": "العرض المنظوري الحالي",
|
||||
"Current plane": "المستوى الحالي",
|
||||
@@ -2682,9 +2683,9 @@
|
||||
"prefs.interface_note": "شكل SolidCraft وأماكن أجزائه.",
|
||||
"prefs.interval_minutes": "{n} دقيقة",
|
||||
"prefs.invert_curved_roll_arrows": "عكس سهمي التدوير المنحنيين",
|
||||
"prefs.invert_curved_roll_arrows_note": "سهما التدوير المنحنيان عند زاوية أداة التنقل يدوران العرض داخل مستوى الشاشة. عند التشغيل: سهم التدوير مع عقارب الساعة يدور عكسها، وسهم العكس يدور معها. عند الإيقاف: يدور كل سهم بالعرض حسب انحنائه.",
|
||||
"prefs.invert_navigator_arrows": "عكس أسهم تدوير أداة التنقل",
|
||||
"prefs.invert_navigator_arrows_note": "تدوّر الأسهم المحيطة بأداة التنقل الكاميرا في الاتجاه الذي تشير إليه، كما في Fusion وInventor: السهم الموجود على اليسار يشير إلى اليمين ويدوّر الكاميرا إلى اليمين. عند الإيقاف: يدوّر السهم العرض نحو الجهة التي يقع فيها، فيدوّر السهم الأيسر الكاميرا إلى اليسار. لا يتأثر سهما الدوران المنحنيان.",
|
||||
"prefs.invert_curved_roll_arrows_note": "سهما التدوير المنحنيان عند زاوية المكعب يدوران العرض داخل مستوى الشاشة. عند التشغيل: سهم التدوير مع عقارب الساعة يدور عكسها، وسهم العكس يدور معها. عند الإيقاف: يدور كل سهم بالعرض حسب انحنائه.",
|
||||
"prefs.invert_navigator_arrows": "عكس أسهم تدوير مكعب العرض",
|
||||
"prefs.invert_navigator_arrows_note": "تدوّر الأسهم المحيطة بالمكعب الكاميرا في الاتجاه الذي تشير إليه، كما في Fusion وInventor: السهم الموجود على اليسار يشير إلى اليمين ويدوّر الكاميرا إلى اليمين. عند الإيقاف: يدوّر السهم المكعب نحو الجهة التي يقع فيها، فيدوّر السهم الأيسر الكاميرا إلى اليسار. لا يتأثر سهما الدوران المنحنيان.",
|
||||
"prefs.keep_unsaved_also_asks": "السؤال أيضًا عند إغلاق علامة التبويب",
|
||||
"prefs.keep_unsaved_also_asks_tip": "عند إيقاف هذا الخيار، تُغلق علامة التبويب دون مربع حوار المتصفح بعد تحديث نسخة كل تصميم غير محفوظ.",
|
||||
"prefs.keep_unsaved_in_browser": "الاحتفاظ بالعمل غير المحفوظ في هذا المتصفح",
|
||||
@@ -2744,6 +2745,7 @@
|
||||
"prefs.move_drag_highlights_tip": "عند الإيقاف: لا يُميَّز أي شيء تحت المؤشر أثناء سحب مكوّن بأسهم أداة التحريك، فتبقى التجميعات الكبيرة سلسة. مع عرض مقطع أو تحليل يبقى التمييز كما هو، ولا يُميَّز شيء أبدًا عند سحب جزء على امتداد وصلاته.",
|
||||
"prefs.navigation": "التنقل",
|
||||
"prefs.navigation.axes_signed": "المحاور",
|
||||
"prefs.navigation.cube_compact": "المكعب",
|
||||
"prefs.navigation.device": "الجهاز",
|
||||
"prefs.navigation.free": "حر",
|
||||
"prefs.navigation.orbit_style": "نمط التدوير",
|
||||
@@ -2751,7 +2753,7 @@
|
||||
"prefs.navigation.turntable": "طاولة دوارة",
|
||||
"prefs.navigation_note": "كيف تحرك الفأرة ولوحة اللمس والفأرة ثلاثية الأبعاد العرض.",
|
||||
"prefs.navigator_fit": "ملاءمة النموذج بعد تغيير العرض من متصفح العرض",
|
||||
"prefs.navigator_fit_note": "النقر على وجه أو ركن أو سهم في أداة التنقل، أو على محور في أداة المحاور، يوجّه العرض أيضًا نحو منتصف النموذج من المسافة الواحدة التي تُظهره كاملًا من كل جهة (كرته المحيطة). يستخدم العرض الرئيسي التكبير والمركز ذاتهما ما دام هذا مفعّلًا، فلا تغيّر العروض المختارة بعده سوى الاتجاه. عند الإيقاف: يدور العرض في مكانه محتفظًا بالهدف والتكبير.",
|
||||
"prefs.navigator_fit_note": "النقر على وجه أو ركن أو سهم في المكعب، أو على محور في أداة المحاور، يوجّه العرض أيضًا نحو منتصف النموذج من المسافة الواحدة التي تُظهره كاملًا من كل جهة (كرته المحيطة). يستخدم العرض الرئيسي التكبير والمركز ذاتهما ما دام هذا مفعّلًا، فلا تغيّر العروض المختارة بعده سوى الاتجاه. عند الإيقاف: يدور العرض في مكانه محتفظًا بالهدف والتكبير.",
|
||||
"prefs.network_host_remove": "إزالة",
|
||||
"prefs.network_locations": "السماح بمواقع الشبكة في التصاميم",
|
||||
"prefs.network_locations_note": "للمؤسسات التي تحفظ التصاميم على خوادم الملفات. عند الإيقاف: الخوادم التي تسمح بها فقط.",
|
||||
|
||||
@@ -144,6 +144,7 @@
|
||||
"Create Issue…": "Create Issue…",
|
||||
"Cross curves": "Cross curves",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter continues on GitHub.",
|
||||
"Cube": "Cube",
|
||||
"Current orthographic view (1:1 mm)": "Current orthographic view (1:1 mm)",
|
||||
"Current perspective view": "Current perspective view",
|
||||
"Current plane": "Current plane",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Prijavi problem…",
|
||||
"Cross curves": "Presječne krive",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter nastavlja na GitHubu.",
|
||||
"Cube": "Kocka",
|
||||
"Current orthographic view (1:1 mm)": "Trenutni ortografski prikaz (1:1 mm)",
|
||||
"Current perspective view": "Trenutni perspektivni prikaz",
|
||||
"Current plane": "Trenutna ravan",
|
||||
@@ -2239,6 +2240,9 @@
|
||||
"prefs.interface_note": "Kako SolidCraft izgleda i gdje su njegovi dijelovi.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Obrni zakrivljene strelice za valjanje",
|
||||
"prefs.invert_curved_roll_arrows_note": "Dvije zakrivljene strelice u uglu kocke valjaju prikaz unutar ravni ekrana. Uključeno: strelica u smjeru kazaljke na satu valja prikaz suprotno od smjera kazaljke na satu, a strelica suprotno od smjera kazaljke na satu valja ga u smjeru kazaljke na satu. Isključeno: svaka strelica valja prikaz onako kako je zakrivljena.",
|
||||
"prefs.invert_navigator_arrows": "Obrni strelice za rotaciju kocke prikaza",
|
||||
"prefs.invert_navigator_arrows_note": "Strelice oko kocke orbitiraju kameru u smjeru u kojem pokazuju, kao u programima Fusion i Inventor: strelica s lijeve strane pokazuje udesno i zakreće kameru udesno. Isključeno: strelica okreće kocku prema strani na kojoj se nalazi, pa lijeva strelica zakreće kameru ulijevo. Postavka ne utiče na zakrivljene strelice za valjanje.",
|
||||
"prefs.keep_work_on_tab_switch": "Zadrži dijaloge i alate otvorenima pri prebacivanju između dizajna",
|
||||
"prefs.keep_work_on_tab_switch_note": "Svaka kartica dizajna zadržava svoj otvoreni dijalog ili alat, s onim što ste u njemu izabrali i upisali, te svoju karticu trake s alatima. Kada se vratite na dizajn, ponovo su tu. Isključeno: prebacivanje između dizajna otkazuje ih i prikazuje karticu Model.",
|
||||
"prefs.labels": "Oznake",
|
||||
@@ -2275,6 +2279,7 @@
|
||||
"prefs.navigation": "Navigacija",
|
||||
"prefs.navigation_note": "Kako miš, trackpad i 3D miš pomjeraju prikaz.",
|
||||
"prefs.navigator_fit": "Uklopi model nakon promjene prikaza u navigatoru",
|
||||
"prefs.navigator_fit_note": "Lice, ugao ili strelica kocke prikaza, odnosno osa alata za orijentaciju, također usmjerava pogled na sredinu modela s jedne udaljenosti na kojoj se uklapa iz svakog smjera (njegova granična sfera). Početni prikaz koristi isti zum i centar dok je ova opcija uključena, pa se prikazi koje izaberete poslije njega samo okreću. Isključeno: prikaz se okreće na mjestu i zadržava svoj cilj i zum.",
|
||||
"prefs.network_host_remove": "Ukloni",
|
||||
"prefs.network_locations": "Dozvoli mrežne lokacije u dizajnima",
|
||||
"prefs.network_locations_note": "Isključeno: povezane datoteke dizajna na drugom računaru (\\\\server\\share) otvaraju se samo sa servera navedenih ovdje ili nakon što ga dozvolite kada se to zatraži.",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "Crear una incidència…",
|
||||
"Cross curves": "Corbes transversals",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Retorn continua a GitHub.",
|
||||
"Cube": "Cub",
|
||||
"Current orthographic view (1:1 mm)": "Vista ortogràfica actual (1:1 mm)",
|
||||
"Current perspective view": "Vista en perspectiva actual",
|
||||
"Current plane": "Pla actual",
|
||||
@@ -2682,6 +2683,9 @@
|
||||
"prefs.interface_note": "Com es veu SolidCraft i on se situen les seves parts.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Invertir les fletxes de gir corbes",
|
||||
"prefs.invert_curved_roll_arrows_note": "Les dues fletxes corbes a la cantonada del cub giren la vista dins del pla de la pantalla. Activades: la fletxa horària gira en sentit antihorari i l'antihorària gira en sentit horari. Desactivades: cada fletxa gira la vista cap on es corba.",
|
||||
"prefs.invert_navigator_arrows": "Invertir les fletxes de rotació del cub de vista",
|
||||
"prefs.invert_navigator_arrows_note": "Les fletxes al voltant del cub orbiten la càmera cap on apunten, com a Fusion i Inventor: la fletxa de l'esquerra apunta a la dreta i gira la càmera a la dreta. Desactivades: una fletxa gira el cub cap al costat on és, així la fletxa de l'esquerra gira la càmera a l'esquerra. Les fletxes de gir corbes no en queden afectades.",
|
||||
"prefs.keep_unsaved_also_asks": "Continuar preguntant quan es tanca la pestanya",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Amb això desactivat, la pestanya es tanca sense la finestra del navegador un cop la còpia de cada disseny sense desar està al dia.",
|
||||
"prefs.keep_unsaved_in_browser": "Conservar la feina sense desar en aquest navegador",
|
||||
@@ -2741,6 +2745,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Desactivat: mentre les fletxes de Moure arrosseguen un component, no es ressalta res del que hi ha sota el punter, cosa que manté àgils els ensamblatges grans. Amb una secció o una anàlisi mostrades, es ressalta igualment, i una peça arrossegada al llarg de les seves unions no ho ressalta mai.",
|
||||
"prefs.navigation": "Navegació",
|
||||
"prefs.navigation.axes_signed": "Eixos",
|
||||
"prefs.navigation.cube_compact": "Cub",
|
||||
"prefs.navigation.device": "Dispositiu",
|
||||
"prefs.navigation.free": "Lliure",
|
||||
"prefs.navigation.orbit_style": "Estil d'òrbita",
|
||||
@@ -2748,6 +2753,7 @@
|
||||
"prefs.navigation.turntable": "Plat giratori",
|
||||
"prefs.navigation_note": "Com mouen la vista el ratolí, el panel tàctil i el ratolí 3D.",
|
||||
"prefs.navigator_fit": "Ajustar el model després d'un canvi de vista del navegador",
|
||||
"prefs.navigator_fit_note": "Una cara, cantonada o fletxa del cub de vista, o un eix del gizmo, també miren el centre del model des de la distància que l'ajusta des de tots els costats (la seva esfera envolupant). Inici fa servir el mateix zoom i centre mentre això està activat, així les vistes triades després només giren. Desactivat: la vista gira al seu lloc, mantenint el seu objectiu i zoom.",
|
||||
"prefs.network_host_remove": "Eliminar",
|
||||
"prefs.network_locations": "Permetre ubicacions de xarxa als dissenys",
|
||||
"prefs.network_locations_note": "Per a organitzacions que desen dissenys en servidors de fitxers. Desactivat: només els servidors que permeteu.",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "Vytvořit hlášení…",
|
||||
"Cross curves": "Příčné křivky",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter pokračuje na GitHubu.",
|
||||
"Cube": "Krychle",
|
||||
"Current orthographic view (1:1 mm)": "Aktuální pravoúhlý pohled (1:1 mm)",
|
||||
"Current perspective view": "Aktuální perspektivní pohled",
|
||||
"Current plane": "Aktuální rovina",
|
||||
@@ -2658,6 +2659,9 @@
|
||||
"prefs.interface_note": "Vzhled aplikace SolidCraft a rozmístění jejích částí.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Obrátit zakřivené šipky natočení",
|
||||
"prefs.invert_curved_roll_arrows_note": "Dvě zakřivené šipky v rohu krychle natáčejí pohled v rovině obrazovky. Zapnuto: šipka po směru hodinových ručiček natáčí pohled proti směru hodinových ručiček a šipka proti směru hodinových ručiček ho natáčí po směru. Vypnuto: každá šipka natáčí pohled tak, jak je zakřivená.",
|
||||
"prefs.invert_navigator_arrows": "Obrátit šipky otáčení krychle pohledu",
|
||||
"prefs.invert_navigator_arrows_note": "Šipky kolem krychle otáčejí kameru tím směrem, kam míří, jako ve Fusion a Inventor: šipka vlevo míří doprava a stáčí kameru doprava. Vypnuto: šipka otáčí krychli na stranu, na které leží, takže šipka vlevo stáčí kameru doleva. Zakřivené šipky natočení tím nejsou dotčeny.",
|
||||
"prefs.keep_unsaved_also_asks": "Přesto se ptát při zavření karty",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Když je vypnuto, karta se zavře bez dotazu prohlížeče, jakmile je kopie každého neuloženého návrhu aktuální.",
|
||||
"prefs.keep_unsaved_in_browser": "Uchovávat neuloženou práci v tomto prohlížeči",
|
||||
@@ -2717,6 +2721,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Vypnuto: když šipky Přesunout táhnou komponentu, nic pod ukazatelem se nezvýrazní, takže velké sestavy zůstanou plynulé. Při zobrazeném řezu nebo analýze se zvýrazňuje i tak a díl tažený po svých spojích nezvýrazňuje nikdy.",
|
||||
"prefs.navigation": "Navigace",
|
||||
"prefs.navigation.axes_signed": "Osy",
|
||||
"prefs.navigation.cube_compact": "Krychle",
|
||||
"prefs.navigation.device": "Zařízení",
|
||||
"prefs.navigation.free": "Volné",
|
||||
"prefs.navigation.orbit_style": "Styl otáčení",
|
||||
@@ -2724,6 +2729,7 @@
|
||||
"prefs.navigation.turntable": "Otočný stůl",
|
||||
"prefs.navigation_note": "Jak myš, trackpad a 3D myš pohybují pohledem.",
|
||||
"prefs.navigator_fit": "Přizpůsobit model po změně pohledu v navigátoru",
|
||||
"prefs.navigator_fit_note": "Plocha, roh nebo šipka krychle pohledu, případně osa orientačního ovladače, také zaměří pohled na střed modelu z jediné vzdálenosti, ze které je celý vidět ze všech stran (jeho ohraničující koule). Domů při zapnutí používá stejné přiblížení a střed, takže pohledy zvolené po něm se jen otáčejí. Vypnuto: pohled se otáčí na místě a zachovává svůj cíl a přiblížení.",
|
||||
"prefs.network_host_remove": "Odebrat",
|
||||
"prefs.network_locations": "Povolit síťová umístění v návrzích",
|
||||
"prefs.network_locations_note": "Vypnuto: propojené soubory návrhu na jiném počítači (\\\\server\\share) se otevírají jen ze zde uvedených serverů nebo poté, co jeden povolíte na výzvu.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Opret fejlrapport…",
|
||||
"Cross curves": "Tværkurver",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter fortsætter på GitHub.",
|
||||
"Cube": "Terning",
|
||||
"Current orthographic view (1:1 mm)": "Aktuel visning uden perspektiv (1:1 mm)",
|
||||
"Current perspective view": "Aktuel perspektivvisning",
|
||||
"Current plane": "Aktuelt plan",
|
||||
@@ -2239,6 +2240,9 @@
|
||||
"prefs.interface_note": "SolidCrafts udseende og placeringen af dets dele.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Vend de buede rulningspile om",
|
||||
"prefs.invert_curved_roll_arrows_note": "De to buede pile i kubens hjørne ruller visningen i skærmplanet. Slået til: Pilen med uret ruller mod uret, og pilen mod uret ruller med uret. Slået fra: Hver pil ruller visningen den vej, den krummer.",
|
||||
"prefs.invert_navigator_arrows": "Vend visningskubens rotationspile om",
|
||||
"prefs.invert_navigator_arrows_note": "Pilene rundt om kuben roterer kameraet i den retning, de peger, som i Fusion og Inventor: Pilen til venstre peger mod højre og svinger kameraet til højre. Slået fra: En pil drejer kuben mod den side, den sidder på, så venstre pil svinger kameraet til venstre. De buede rulningspile påvirkes ikke.",
|
||||
"prefs.keep_work_on_tab_switch": "Behold dialoger og værktøjer åbne, når du skifter design",
|
||||
"prefs.keep_work_on_tab_switch_note": "Hver designfane beholder sin åbne dialog eller sit åbne værktøj med det, du har valgt og skrevet i den, og sin fane på båndet. De er der igen, når du vender tilbage til designet. Slået fra: Skift af design annullerer dem og viser fanen Model.",
|
||||
"prefs.labels": "Etiketter",
|
||||
@@ -2275,6 +2279,7 @@
|
||||
"prefs.navigation": "Navigation",
|
||||
"prefs.navigation_note": "Hvordan musen, pegefeltet og 3D-musen styrer visningen.",
|
||||
"prefs.navigator_fit": "Tilpas modellen efter et skift af visning i navigatoren",
|
||||
"prefs.navigator_fit_note": "En flade, et hjørne eller en pil på visningskuben eller en akse på orienteringsgizmoen ser også mod modellens midte fra den ene afstand, der får den til at passe fra alle sider (dens omsluttende kugle). Hjem bruger samme zoom og centrum, mens dette er slået til, så visninger, der vælges bagefter, kun drejer. Slået fra: Visningen drejer på stedet og beholder sit mål og sin zoom.",
|
||||
"prefs.network_host_remove": "Fjern",
|
||||
"prefs.network_locations": "Tillad netværksplaceringer i designs",
|
||||
"prefs.network_locations_note": "Slået fra: Et designs linkede filer på en anden computer (\\\\server\\share) åbnes kun fra de servere, der er angivet her, eller efter at du har tilladt en, når du bliver spurgt.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Issue erstellen…",
|
||||
"Cross curves": "Querkurven",
|
||||
"Ctrl+Enter continues on GitHub.": "Strg+Enter geht weiter zu GitHub.",
|
||||
"Cube": "Würfel",
|
||||
"Current orthographic view (1:1 mm)": "Aktuelle orthogonale Ansicht (1:1 mm)",
|
||||
"Current perspective view": "Aktuelle perspektivische Ansicht",
|
||||
"Current plane": "Aktuelle Ebene",
|
||||
@@ -2468,6 +2469,8 @@
|
||||
"prefs.interface_language": "Oberflächensprache",
|
||||
"prefs.interface_note": "Legt das Erscheinungsbild von SolidCraft und die Anordnung seiner Bereiche fest.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_navigator_arrows": "Drehpfeile des Ansichtswürfels umkehren",
|
||||
"prefs.invert_navigator_arrows_note": "Die Pfeile um den Würfel drehen die Kamera in die Richtung, in die sie zeigen, wie in Fusion und Inventor: Der linke Pfeil zeigt nach rechts und schwenkt die Kamera nach rechts. Aus: Ein Pfeil dreht den Würfel zu der Seite hin, an der er sitzt, sodass der linke Pfeil die Kamera nach links schwenkt. Die gebogenen Rollpfeile sind davon nicht betroffen.",
|
||||
"prefs.keep_work_on_tab_switch": "Dialoge und Werkzeuge beim Wechsel zwischen Konstruktionen offen lassen",
|
||||
"prefs.keep_work_on_tab_switch_note": "Jeder Konstruktions-Tab behält seinen offenen Dialog oder sein Werkzeug mit allem, was darin ausgewählt und eingegeben wurde, sowie seinen Werkzeugleisten-Tab. Beim Zurückkehren zur Konstruktion ist alles wieder da. Aus: Ein Wechsel bricht sie ab und zeigt den Tab Modell.",
|
||||
"prefs.labels": "Beschriftungen",
|
||||
@@ -2523,6 +2526,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Aus: Solange die Pfeile von Verschieben eine Komponente ziehen, wird nichts unter dem Zeiger hervorgehoben; so bleiben große Baugruppen flüssig. Mit einem Schnitt oder einer Analyse wird es trotzdem hervorgehoben, und ein entlang seiner Gelenke gezogenes Teil hebt nie etwas hervor.",
|
||||
"prefs.navigation": "Navigation",
|
||||
"prefs.navigation.axes_signed": "Achsen",
|
||||
"prefs.navigation.cube_compact": "Würfel",
|
||||
"prefs.navigation.device": "Gerät",
|
||||
"prefs.navigation.free": "Frei",
|
||||
"prefs.navigation.orbit_style": "Drehstil",
|
||||
@@ -2530,6 +2534,7 @@
|
||||
"prefs.navigation.turntable": "Drehteller",
|
||||
"prefs.navigation_note": "Legt fest, wie Maus, Trackpad und 3D-Maus die Ansicht steuern.",
|
||||
"prefs.navigator_fit": "Modell nach einem Ansichtswechsel am Navigator einpassen",
|
||||
"prefs.navigator_fit_note": "Eine Fläche, Ecke oder ein Pfeil des Würfels oder eine Achse des Gizmos blickt auch auf die Mitte des Modells, aus dem einen Abstand, der es von jeder Seite einpasst (seine Hüllkugel). Home nutzt denselben Zoom und dieselbe Mitte, solange dies an ist, sodass danach gewählte Ansichten sich nur drehen. Aus: Die Ansicht dreht sich an Ort und Stelle und behält Ziel und Zoom.",
|
||||
"prefs.network_host_remove": "Entfernen",
|
||||
"prefs.network_locations": "Netzwerkspeicherorte in Designs zulassen",
|
||||
"prefs.network_locations_note": "Für Organisationen, die Entwürfe auf Dateiservern speichern. Aus: nur erlaubte Server.",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "Create Issue…",
|
||||
"Cross curves": "Cross curves",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter continues on GitHub.",
|
||||
"Cube": "Cube",
|
||||
"Current orthographic view (1:1 mm)": "Current orthographic view (1:1 mm)",
|
||||
"Current perspective view": "Current perspective view",
|
||||
"Current plane": "Current plane",
|
||||
@@ -2682,9 +2683,9 @@
|
||||
"prefs.interface_note": "How SolidCraft looks and where its parts sit.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Invert the curved roll arrows",
|
||||
"prefs.invert_curved_roll_arrows_note": "The two curved arrows at the navigator's corner roll the view within the screen plane. On: the clockwise arrow rolls counterclockwise and the counterclockwise arrow rolls clockwise. Off: each arrow rolls the view the way it curls.",
|
||||
"prefs.invert_navigator_arrows": "Invert the navigator rotation arrows",
|
||||
"prefs.invert_navigator_arrows_note": "The arrows around the navigator orbit the camera the way they point, as in Fusion and Inventor: the arrow on the left points right and swings the camera right. Off: an arrow rotates the view toward the side it sits on, so the left arrow swings the camera left. The curved roll arrows are not affected.",
|
||||
"prefs.invert_curved_roll_arrows_note": "The two curved arrows at the cube's corner roll the view within the screen plane. On: the clockwise arrow rolls counterclockwise and the counterclockwise arrow rolls clockwise. Off: each arrow rolls the view the way it curls.",
|
||||
"prefs.invert_navigator_arrows": "Invert the view cube rotation arrows",
|
||||
"prefs.invert_navigator_arrows_note": "The arrows around the cube orbit the camera the way they point, as in Fusion and Inventor: the arrow on the left points right and swings the camera right. Off: an arrow rotates the cube toward the side it sits on, so the left arrow swings the camera left. The curved roll arrows are not affected.",
|
||||
"prefs.keep_unsaved_also_asks": "Still ask when the tab closes",
|
||||
"prefs.keep_unsaved_also_asks_tip": "With this off, the tab closes without the browser's box once every unsaved design's copy is current.",
|
||||
"prefs.keep_unsaved_in_browser": "Keep unsaved work in this browser",
|
||||
@@ -2744,6 +2745,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Off: while Move's arrows drag a component, nothing under the pointer is highlighted, which keeps large assemblies smooth. With a section or an analysis shown it is highlighted all the same, and a part dragged along its joints never highlights it.",
|
||||
"prefs.navigation": "Navigation",
|
||||
"prefs.navigation.axes_signed": "Axes",
|
||||
"prefs.navigation.cube_compact": "Cube",
|
||||
"prefs.navigation.device": "Device",
|
||||
"prefs.navigation.free": "Free",
|
||||
"prefs.navigation.orbit_style": "Orbit style",
|
||||
@@ -2751,7 +2753,7 @@
|
||||
"prefs.navigation.turntable": "Turntable",
|
||||
"prefs.navigation_note": "How the mouse, trackpad and 3D mouse move the view.",
|
||||
"prefs.navigator_fit": "Fit the model after a navigator view change",
|
||||
"prefs.navigator_fit_note": "A face, corner or arrow of a navigator, or an axis of the gizmo, also looks at the middle of the model from the one distance that fits it from every side (its bounding sphere). Home uses the same zoom and centre while this is on, so views chosen after it only turn. Off: the view turns in place, keeping its target and zoom.",
|
||||
"prefs.navigator_fit_note": "A face, corner or arrow of the view cube, or an axis of the gizmo, also looks at the middle of the model from the one distance that fits it from every side (its bounding sphere). Home uses the same zoom and centre while this is on, so views chosen after it only turn. Off: the view turns in place, keeping its target and zoom.",
|
||||
"prefs.network_host_remove": "Remove",
|
||||
"prefs.network_locations": "Allow network locations in designs",
|
||||
"prefs.network_locations_note": "For organizations that keep designs on file servers. Off: only servers you allow.",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "Crear incidencia…",
|
||||
"Cross curves": "Curvas transversales",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Intro continúa en GitHub.",
|
||||
"Cube": "Cubo",
|
||||
"Current orthographic view (1:1 mm)": "Vista ortográfica actual (1:1 mm)",
|
||||
"Current perspective view": "Vista en perspectiva actual",
|
||||
"Current plane": "Plano actual",
|
||||
@@ -2682,6 +2683,9 @@
|
||||
"prefs.interface_note": "Cómo se ve SolidCraft y dónde se sitúan sus partes.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Invertir las flechas de giro curvo",
|
||||
"prefs.invert_curved_roll_arrows_note": "Las dos flechas curvas en la esquina del cubo giran la vista en el plano de la pantalla. Activado: la flecha horaria gira antihoraria y la antihoraria gira horaria. Desactivado: cada flecha gira la vista hacia donde se curva.",
|
||||
"prefs.invert_navigator_arrows": "Invertir las flechas de rotación del cubo de vista",
|
||||
"prefs.invert_navigator_arrows_note": "Las flechas alrededor del cubo orbitan la cámara hacia donde apuntan, como en Fusion e Inventor: la flecha de la izquierda apunta a la derecha y gira la cámara a la derecha. Desactivado: una flecha rota el cubo hacia el lado en que está, así la flecha izquierda gira la cámara a la izquierda. Las flechas de giro curvo no se ven afectadas.",
|
||||
"prefs.keep_unsaved_also_asks": "Preguntar igualmente al cerrar la pestaña",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Con esto desactivado, la pestaña se cierra sin el cuadro del navegador en cuanto la copia de cada diseño sin guardar está al día.",
|
||||
"prefs.keep_unsaved_in_browser": "Conservar el trabajo sin guardar en este navegador",
|
||||
@@ -2741,6 +2745,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Desactivado: mientras las flechas de Mover arrastran un componente no se resalta nada bajo el puntero, y los ensamblajes grandes se mueven con fluidez. Con un corte o un análisis visible se resalta igualmente, y una pieza arrastrada a lo largo de sus uniones nunca lo resalta.",
|
||||
"prefs.navigation": "Navegación",
|
||||
"prefs.navigation.axes_signed": "Ejes",
|
||||
"prefs.navigation.cube_compact": "Cubo",
|
||||
"prefs.navigation.device": "Dispositivo",
|
||||
"prefs.navigation.free": "Libre",
|
||||
"prefs.navigation.orbit_style": "Estilo de órbita",
|
||||
@@ -2748,6 +2753,7 @@
|
||||
"prefs.navigation.turntable": "Plato giratorio",
|
||||
"prefs.navigation_note": "Cómo mueven la vista el ratón, el panel táctil y el ratón 3D.",
|
||||
"prefs.navigator_fit": "Ajustar el modelo tras un cambio de vista del navegador",
|
||||
"prefs.navigator_fit_note": "Una cara, esquina o flecha del cubo de vista, o un eje del gizmo, también mira el centro del modelo desde la única distancia que lo encaja desde todos los lados (su esfera envolvente). Inicio usa el mismo zoom y centro mientras esto está activado, así que las vistas elegidas después solo giran. Desactivado: la vista gira en su lugar, manteniendo su objetivo y zoom.",
|
||||
"prefs.network_host_remove": "Eliminar",
|
||||
"prefs.network_locations": "Permitir ubicaciones de red en diseños",
|
||||
"prefs.network_locations_note": "Desactivado: los archivos vinculados de un diseño en otro equipo (\\\\servidor\\\\carpeta) solo se abren desde los servidores listados aquí, o tras permitir uno cuando se pregunte.",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "Luo ilmoitus…",
|
||||
"Cross curves": "Poikkikäyrät",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter jatkaa GitHubissa.",
|
||||
"Cube": "Kuutio",
|
||||
"Current orthographic view (1:1 mm)": "Nykyinen ortografinen näkymä (1:1 mm)",
|
||||
"Current perspective view": "Nykyinen perspektiivinäkymä",
|
||||
"Current plane": "Nykyinen taso",
|
||||
@@ -2680,6 +2681,9 @@
|
||||
"prefs.interface_note": "Miltä SolidCraft näyttää ja missä sen osat sijaitsevat.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Käännä kaarevat kallistusnuolet",
|
||||
"prefs.invert_curved_roll_arrows_note": "Kuution kulman kaksi kaarevaa nuolta kallistavat näkymää näytön tasossa. Päällä: myötäpäivään osoittava nuoli kallistaa vastapäivään ja vastapäivään osoittava myötäpäivään. Pois: kumpikin nuoli kallistaa näkymää kaartumissuuntaansa.",
|
||||
"prefs.invert_navigator_arrows": "Käännä näkymäkuution kiertonuolet",
|
||||
"prefs.invert_navigator_arrows_note": "Kuution ympärillä olevat nuolet kiertävät kameraa osoittamaansa suuntaan kuten Fusionissa ja Inventorissa: vasemmalla oleva nuoli osoittaa oikealle ja kääntää kameraa oikealle. Pois: nuoli kiertää kuutiota omaa puoltaan kohti, joten vasen nuoli kääntää kameraa vasemmalle. Kaarevat kallistusnuolet eivät muutu.",
|
||||
"prefs.keep_unsaved_also_asks": "Kysy silti, kun välilehti suljetaan",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Kun tämä on pois päältä, välilehti sulkeutuu ilman selaimen kyselyikkunaa, kun jokaisen tallentamattoman mallin kopio on ajan tasalla.",
|
||||
"prefs.keep_unsaved_in_browser": "Säilytä tallentamaton työ tässä selaimessa",
|
||||
@@ -2739,6 +2743,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Pois: kun Siirrä-toiminnon nuolilla vedetään komponenttia, osoittimen alla olevaa kohdetta ei korosteta, mikä pitää suuret kokoonpanot sulavina. Kun leikkaus tai analyysi on näkyvissä, kohde korostetaan silti, eikä niveliään pitkin vedetty osa koskaan korosta sitä.",
|
||||
"prefs.navigation": "Navigointi",
|
||||
"prefs.navigation.axes_signed": "Akselit",
|
||||
"prefs.navigation.cube_compact": "Kuutio",
|
||||
"prefs.navigation.device": "Laite",
|
||||
"prefs.navigation.free": "Vapaa",
|
||||
"prefs.navigation.orbit_style": "Kiertotapa",
|
||||
@@ -2746,6 +2751,7 @@
|
||||
"prefs.navigation.turntable": "Pyörivä alusta",
|
||||
"prefs.navigation_note": "Miten hiiri, kosketuslevy ja 3D-hiiri liikuttavat näkymää.",
|
||||
"prefs.navigator_fit": "Sovita malli navigaattorin näkymänvaihdon jälkeen",
|
||||
"prefs.navigator_fit_note": "Näkymäkuution pinta, kulma tai nuoli tai gizmon akseli katsoo myös mallin keskikohtaa siltä yhdeltä etäisyydeltä, joka sovittaa sen joka suunnasta (sen rajaava pallo). Aloitusnäkymä käyttää samaa zoomausta ja keskipistettä, kun tämä on päällä, joten sen jälkeen valitut näkymät vain kääntyvät. Pois: näkymä kääntyy paikallaan säilyttäen kohteensa ja zoomauksensa.",
|
||||
"prefs.network_host_remove": "Poista",
|
||||
"prefs.network_locations": "Salli verkkosijainnit malleissa",
|
||||
"prefs.network_locations_note": "Pois: mallin linkitetyt tiedostot toisella tietokoneella (\\\\server\\share) avataan vain tässä luetelluilta palvelimilta tai sen jälkeen, kun sallit palvelimen kysyttäessä.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Créer un ticket…",
|
||||
"Cross curves": "Courbes transversales",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Entrée continue sur GitHub.",
|
||||
"Cube": "Cube",
|
||||
"Current orthographic view (1:1 mm)": "Vue orthographique actuelle (1:1 mm)",
|
||||
"Current perspective view": "Vue en perspective actuelle",
|
||||
"Current plane": "Plan actuel",
|
||||
@@ -2417,9 +2418,9 @@
|
||||
"prefs.interface_note": "L'apparence de SolidCraft et la place de ses éléments.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Inverser les flèches de rotation courbe",
|
||||
"prefs.invert_curved_roll_arrows_note": "Les deux flèches courbes au coin du navigateur font rouler la vue dans le plan de l'écran. Activé : la flèche horaire fait rouler dans le sens antihoraire et la flèche antihoraire dans le sens horaire. Désactivé : chaque flèche fait rouler la vue dans le sens de sa courbure.",
|
||||
"prefs.invert_navigator_arrows": "Inverser les flèches de rotation du navigateur de vue",
|
||||
"prefs.invert_navigator_arrows_note": "Les flèches autour du navigateur font orbiter la caméra dans le sens où elles pointent, comme dans Fusion et Inventor : la flèche de gauche pointe vers la droite et fait pivoter la caméra vers la droite. Désactivé : une flèche fait tourner la vue vers le côté où elle se trouve, donc la flèche de gauche fait pivoter la caméra vers la gauche. Les flèches courbes de roulis ne sont pas concernées.",
|
||||
"prefs.invert_curved_roll_arrows_note": "Les deux flèches courbes au coin du cube font pivoter la vue dans le plan de l'écran. Activé : la flèche horaire fait tourner la vue dans le sens antihoraire, et inversement. Désactivé : chaque flèche fait tourner la vue dans le sens indiqué par sa courbure.",
|
||||
"prefs.invert_navigator_arrows": "Inverser les flèches de rotation du cube de vue",
|
||||
"prefs.invert_navigator_arrows_note": "Les flèches autour du cube font orbiter la caméra dans la direction qu'elles indiquent, comme dans Fusion et Inventor : la flèche de gauche pointe vers la droite et fait pivoter la caméra vers la droite. Désactivé : une flèche fait tourner le cube vers le côté où elle se trouve ; la flèche de gauche fait donc pivoter la caméra vers la gauche. Les flèches courbes de rotation ne sont pas affectées.",
|
||||
"prefs.keep_work_on_tab_switch": "Garder dialogues et outils ouverts en changeant de conception",
|
||||
"prefs.keep_work_on_tab_switch_note": "Chaque onglet de conception garde son dialogue ou son outil ouvert, avec ce que vous y avez sélectionné et saisi, ainsi que son onglet de barre d'outils. Vous les retrouvez en revenant à la conception. Désactivé : changer de conception les annule et affiche l'onglet Modèle.",
|
||||
"prefs.labels": "Libellés",
|
||||
@@ -2473,6 +2474,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Désactivé : pendant que les flèches de Déplacer font glisser un composant, rien n’est mis en évidence sous le pointeur, ce qui garde les grands assemblages fluides. Avec une coupe ou une analyse affichée, la mise en évidence reste, et une pièce glissée le long de ses liaisons n’en montre jamais.",
|
||||
"prefs.navigation": "Navigation",
|
||||
"prefs.navigation.axes_signed": "Axes",
|
||||
"prefs.navigation.cube_compact": "Cube",
|
||||
"prefs.navigation.device": "Appareil",
|
||||
"prefs.navigation.free": "Libre",
|
||||
"prefs.navigation.orbit_style": "Style d'orbite",
|
||||
@@ -2480,7 +2482,7 @@
|
||||
"prefs.navigation.turntable": "Plateau tournant",
|
||||
"prefs.navigation_note": "Comment la souris, le pavé tactile et la souris 3D déplacent la vue.",
|
||||
"prefs.navigator_fit": "Ajuster le modèle après un changement de vue du navigateur",
|
||||
"prefs.navigator_fit_note": "Une face, un coin ou une flèche du navigateur de vue, ou un axe du gizmo, regarde aussi le milieu du modèle depuis la distance unique qui le cadre sous tous les côtés (sa sphère englobante). Accueil utilise le même zoom et le même centre tant que cette option est activée, de sorte que les vues choisies ensuite ne font que tourner. Désactivé : la vue tourne sur place, en gardant sa cible et son zoom.",
|
||||
"prefs.navigator_fit_note": "Une face, un coin ou une flèche du cube de vue, ainsi qu'un axe du gizmo, centre aussi la vue sur le modèle à la distance permettant de le voir en entier depuis toutes les directions (sa sphère englobante). Tant que cette option est activée, la touche Accueil utilise le même zoom et le même centrage : les vues choisies ensuite ne font que tourner. Désactivée, la vue pivote sur place en conservant sa cible et son zoom.",
|
||||
"prefs.network_host_remove": "Retirer",
|
||||
"prefs.network_locations": "Autoriser les emplacements réseau dans les conceptions",
|
||||
"prefs.network_locations_note": "Désactivé : les fichiers liés d'une conception situés sur un autre ordinateur (\\\\server\\share) ne sont ouverts que depuis les serveurs listés ici, ou après en avoir autorisé un sur demande.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Crear incidencia…",
|
||||
"Cross curves": "Curvas transversais",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter continúa en GitHub.",
|
||||
"Cube": "Cubo",
|
||||
"Current orthographic view (1:1 mm)": "Vista ortográfica actual (1:1 mm)",
|
||||
"Current perspective view": "Vista en perspectiva actual",
|
||||
"Current plane": "Plano actual",
|
||||
@@ -2239,6 +2240,9 @@
|
||||
"prefs.interface_note": "Como se ve SolidCraft e onde están as súas partes.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Inverter as frechas curvas de balanceo",
|
||||
"prefs.invert_curved_roll_arrows_note": "As dúas frechas curvas da esquina do cubo fan xirar a vista no plano da pantalla. Activado: a frecha en sentido horario xira en sentido antihorario e a frecha en sentido antihorario xira en sentido horario. Desactivado: cada frecha xira a vista no sentido en que se curva.",
|
||||
"prefs.invert_navigator_arrows": "Invertir as frechas de rotación do cubo de vistas",
|
||||
"prefs.invert_navigator_arrows_note": "As frechas arredor do cubo fan orbitar a cámara na dirección na que apuntan, como en Fusion e Inventor: a frecha da esquerda apunta á dereita e move a cámara cara á dereita. Desactivado: unha frecha xira o cubo cara ao lado no que está, así que a frecha da esquerda move a cámara cara á esquerda. As frechas curvas de balanceo non se ven afectadas.",
|
||||
"prefs.keep_work_on_tab_switch": "Manter abertos os diálogos e as ferramentas ao cambiar de deseño",
|
||||
"prefs.keep_work_on_tab_switch_note": "Cada lapela de deseño conserva o seu diálogo ou ferramenta aberto, co que escolliches e escribiches nel, e a súa lapela da barra de ferramentas. Volven estar aí cando regresas ao deseño. Desactivado: cambiar de deseño cancélaos e mostra a lapela Modelo.",
|
||||
"prefs.labels": "Etiquetas",
|
||||
@@ -2275,6 +2279,7 @@
|
||||
"prefs.navigation": "Navegación",
|
||||
"prefs.navigation_note": "Como moven a vista o rato, o panel táctil e o rato 3D.",
|
||||
"prefs.navigator_fit": "Axustar o modelo despois dun cambio de vista do navegador",
|
||||
"prefs.navigator_fit_note": "Unha cara, esquina ou frecha do cubo de vistas, ou un eixe do gizmo, tamén mira ao centro do modelo desde a única distancia que o abarca por todos os lados (a súa esfera delimitadora). Inicio usa o mesmo zoom e centro mentres isto está activado, así que as vistas escollidas despois só xiran. Desactivado: a vista xira no sitio, conservando o seu punto de mira e o seu zoom.",
|
||||
"prefs.network_host_remove": "Quitar",
|
||||
"prefs.network_locations": "Permitir ubicacións de rede nos deseños",
|
||||
"prefs.network_locations_note": "Desactivado: os ficheiros ligados dun deseño que están noutro ordenador (\\\\server\\share) só se abren desde os servidores listados aquí, ou despois de que permitas un cando se che pregunte.",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "יצירת דיווח…",
|
||||
"Cross curves": "עקומות חוצות",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter ממשיך ב־GitHub.",
|
||||
"Cube": "קובייה",
|
||||
"Current orthographic view (1:1 mm)": "התצוגה האורתוגרפית הנוכחית (1:1 mm)",
|
||||
"Current perspective view": "תצוגת הפרספקטיבה הנוכחית",
|
||||
"Current plane": "המישור הנוכחי",
|
||||
@@ -2682,9 +2683,9 @@
|
||||
"prefs.interface_note": "איך SolidCraft נראה והיכן חלקיו ממוקמים.",
|
||||
"prefs.interval_minutes": "{n} דק׳",
|
||||
"prefs.invert_curved_roll_arrows": "היפוך חצי הגלילה המעוגלים",
|
||||
"prefs.invert_curved_roll_arrows_note": "שני החצים המעוגלים בפינת הנווט מסובבים את התצוגה במישור המסך. פועל: החץ עם כיוון השעון מסובב נגד כיוון השעון, והחץ נגד כיוון השעון מסובב עם כיוון השעון. כבוי: כל חץ מסובב את התצוגה לפי העקום שלו.",
|
||||
"prefs.invert_navigator_arrows": "היפוך חצי סיבוב הנווט",
|
||||
"prefs.invert_navigator_arrows_note": "החצים סביב הנווט מחוויים את המצלמה לכיוון שהחץ מצביע, כמו ב-Fusion ו-Inventor: החץ שמשמאל מצביע ימימה ומניע את המצלמה ימימה. כבוי: החץ מסובב את התצוגה אל הצד שבו הוא יושב, כך שחץ שמאל מניע את המצלמה שמאלה. חצי הגלילה המעוגלים אינם מושפעים.",
|
||||
"prefs.invert_curved_roll_arrows_note": "שני החצים המעוגלים בפינת הקובייה מסובבים את התצוגה במישור המסך. פועל: החץ עם כיוון השעון מסובב נגד כיוון השעון, והחץ נגד כיוון השעון מסובב עם כיוון השעון. כבוי: כל חץ מסובב את התצוגה לפי העקום שלו.",
|
||||
"prefs.invert_navigator_arrows": "היפוך חצי סיבוב קוביית התצוגה",
|
||||
"prefs.invert_navigator_arrows_note": "החצים סביב הקובייה מחוויים את המצלמה לכיוון שהחץ מצביע, כמו ב-Fusion ו-Inventor: החץ שמשמאל מצביע ימימה ומניע את המצלמה ימימה. כבוי: החץ מסובב את הקובייה אל הצד שבו הוא יושב, כך שחץ שמאל מניע את המצלמה שמאלה. חצי הגלילה המעוגלים אינם מושפעים.",
|
||||
"prefs.keep_unsaved_also_asks": "לשאול גם כשהכרטיסייה נסגרת",
|
||||
"prefs.keep_unsaved_also_asks_tip": "כשאפשרות זו כבויה, הכרטיסייה נסגרת ללא חלון הדפדפן לאחר שהעותק של כל תכן שלא נשמר עודכן.",
|
||||
"prefs.keep_unsaved_in_browser": "לשמור עבודה שלא נשמרה בדפדפן זה",
|
||||
@@ -2744,6 +2745,7 @@
|
||||
"prefs.move_drag_highlights_tip": "כבוי: בזמן שחצי ההזזה גוררים רכיב, דבר אינו מודגש מתחת לסמן, וכך מכלולים גדולים נשארים חלקים. כשמוצג חתך או ניתוח ההדגשה נשארת, וחלק שנגרר לאורך המפרקים שלו לעולם אינו מדגיש.",
|
||||
"prefs.navigation": "ניווט",
|
||||
"prefs.navigation.axes_signed": "צירים",
|
||||
"prefs.navigation.cube_compact": "קובייה",
|
||||
"prefs.navigation.device": "התקן",
|
||||
"prefs.navigation.free": "חופשי",
|
||||
"prefs.navigation.orbit_style": "סגנון סיבוב",
|
||||
@@ -2751,7 +2753,7 @@
|
||||
"prefs.navigation.turntable": "שולחן מסתובב",
|
||||
"prefs.navigation_note": "איך העכבר, משטח המגע והעכבר התלת־ממדי מזיזים את המבט.",
|
||||
"prefs.navigator_fit": "התאמת המודל לאחר שינוי תצוגה בניווט התצוגה",
|
||||
"prefs.navigator_fit_note": "לחיצה על פאה, פינה או חץ בנווט, או על ציר בגיזמו, גם מביטה אל מרכז המודל מהמרחק האחד שמכיל אותו מכל צד (הכדור החוסם שלו). תצוגת בית משתמשת באותו זום ובאותו מרכז כל עוד זה פעיל, כך שתצוגות שנבחרות אחריה רק מסתובבות. כבוי: התצוגה מסתובבת במקומה ושומרת על היעד ועל הזום.",
|
||||
"prefs.navigator_fit_note": "לחיצה על פאה, פינה או חץ בקובייה, או על ציר בגיזמו, גם מביטה אל מרכז המודל מהמרחק האחד שמכיל אותו מכל צד (הכדור החוסם שלו). תצוגת בית משתמשת באותו זום ובאותו מרכז כל עוד זה פעיל, כך שתצוגות שנבחרות אחריה רק מסתובבות. כבוי: התצוגה מסתובבת במקומה ושומרת על היעד ועל הזום.",
|
||||
"prefs.network_host_remove": "הסרה",
|
||||
"prefs.network_locations": "התרת מיקומי רשת בתכנים",
|
||||
"prefs.network_locations_note": "לארגונים השומרים תכנונים בשרתי קבצים. כשהאפשרות כבויה: רק שרתים שאישרתם.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Prijavi problem…",
|
||||
"Cross curves": "Krivulje presjeka",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter nastavlja na GitHubu.",
|
||||
"Cube": "Kocka",
|
||||
"Current orthographic view (1:1 mm)": "Trenutni ortografski prikaz (1:1 mm)",
|
||||
"Current perspective view": "Trenutni perspektivni prikaz",
|
||||
"Current plane": "Trenutna ravnina",
|
||||
@@ -2239,6 +2240,9 @@
|
||||
"prefs.interface_note": "Kako SolidCraft izgleda i gdje su njegovi dijelovi.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Obrni zakrivljene strelice za valjanje",
|
||||
"prefs.invert_curved_roll_arrows_note": "Dvije zakrivljene strelice u kutu kocke valjaju prikaz unutar ravnine zaslona. Uključeno: strelica u smjeru kazaljke na satu valja prikaz suprotno od smjera kazaljke na satu, a strelica suprotno od smjera kazaljke na satu valja ga u smjeru kazaljke na satu. Isključeno: svaka strelica valja prikaz onako kako je zakrivljena.",
|
||||
"prefs.invert_navigator_arrows": "Obrni strelice za rotaciju kocke prikaza",
|
||||
"prefs.invert_navigator_arrows_note": "Strelice oko kocke orbitiraju kameru u smjeru u kojem pokazuju, kao u programima Fusion i Inventor: strelica s lijeve strane pokazuje udesno i zakreće kameru udesno. Isključeno: strelica okreće kocku prema strani na kojoj se nalazi, pa lijeva strelica zakreće kameru ulijevo. Postavka ne utječe na zakrivljene strelice za valjanje.",
|
||||
"prefs.keep_work_on_tab_switch": "Zadrži dijaloge i alate otvorenima pri prebacivanju između dizajna",
|
||||
"prefs.keep_work_on_tab_switch_note": "Svaka kartica dizajna zadržava svoj otvoreni dijalog ili alat, s onim što ste u njemu odabrali i upisali, te svoju karticu alatne trake. Kad se vratite na dizajn, ponovno su tu. Isključeno: prebacivanje između dizajna poništava ih i prikazuje karticu Model.",
|
||||
"prefs.labels": "Oznake",
|
||||
@@ -2275,6 +2279,7 @@
|
||||
"prefs.navigation": "Navigacija",
|
||||
"prefs.navigation_note": "Kako miš, dodirna pločica i 3D miš pomiču prikaz.",
|
||||
"prefs.navigator_fit": "Uklopi model nakon promjene prikaza u navigatoru",
|
||||
"prefs.navigator_fit_note": "Lice, kut ili strelica kocke prikaza, odnosno os orijentacijskog gizma, također usmjerava pogled na sredinu modela s jedne udaljenosti na kojoj se uklapa iz svakog smjera (njegova granična sfera). Početni prikaz koristi isti zum i središte dok je ova opcija uključena, pa se prikazi koje odaberete poslije njega samo okreću. Isključeno: prikaz se okreće na mjestu i zadržava svoj cilj i zum.",
|
||||
"prefs.network_host_remove": "Ukloni",
|
||||
"prefs.network_locations": "Dopusti mrežne lokacije u dizajnima",
|
||||
"prefs.network_locations_note": "Isključeno: povezane datoteke dizajna na drugom računalu (\\\\server\\share) otvaraju se samo s poslužitelja navedenih ovdje ili nakon što ga dopustite kad se to zatraži.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Hibajegy létrehozása…",
|
||||
"Cross curves": "Keresztgörbék",
|
||||
"Ctrl+Enter continues on GitHub.": "A Ctrl+Enter a GitHubon folytatja.",
|
||||
"Cube": "Kocka",
|
||||
"Current orthographic view (1:1 mm)": "Aktuális ortografikus nézet (1:1 mm)",
|
||||
"Current perspective view": "Aktuális perspektivikus nézet",
|
||||
"Current plane": "Aktuális sík",
|
||||
@@ -2239,6 +2240,9 @@
|
||||
"prefs.interface_note": "A SolidCraft megjelenése és elemeinek elhelyezése.",
|
||||
"prefs.interval_minutes": "{n} perc",
|
||||
"prefs.invert_curved_roll_arrows": "Az íves gördülőnyilak megfordítása",
|
||||
"prefs.invert_curved_roll_arrows_note": "A nézetkocka sarkában lévő két íves nyíl a nézetet a képernyő síkjában gördíti. Be: az óramutató járásával megegyező nyíl ellentétes irányba gördít, az ellentétes nyíl pedig az óramutató járásával megegyező irányba. Ki: minden nyíl abba az irányba gördíti a nézetet, amerre ível.",
|
||||
"prefs.invert_navigator_arrows": "A nézetkocka forgatónyilainak megfordítása",
|
||||
"prefs.invert_navigator_arrows_note": "A kocka körüli nyilak úgy keringetik a kamerát, ahogy mutatnak, mint a Fusion és az Inventor esetében: a bal oldali nyíl jobbra mutat, és a kamerát jobbra lendíti. Ki: a nyíl a kockát afelé az oldal felé forgatja, amelyiken van, így a bal nyíl a kamerát balra lendíti. Az íves gördülőnyilakat ez nem érinti.",
|
||||
"prefs.keep_work_on_tab_switch": "A párbeszédablakok és eszközök nyitva tartása tervek közötti váltáskor",
|
||||
"prefs.keep_work_on_tab_switch_note": "Minden tervfül megtartja a nyitott párbeszédablakát vagy eszközét azzal együtt, amit kijelölt és beírt benne, valamint a menüszalag-fülét. A tervhez visszatérve újra ott vannak. Ki: a tervek közötti váltás megszakítja őket, és a Modell fület mutatja.",
|
||||
"prefs.labels": "Feliratok",
|
||||
@@ -2275,6 +2279,7 @@
|
||||
"prefs.navigation": "Navigáció",
|
||||
"prefs.navigation_note": "Hogyan mozgatja a nézetet az egér, az érintőpad és a 3D-egér.",
|
||||
"prefs.navigator_fit": "A modell illesztése navigátornézet-váltás után",
|
||||
"prefs.navigator_fit_note": "A nézetkocka egy lapja, sarka vagy nyila, illetve a gizmo egy tengelye a modell közepére is ránéz abból az egyetlen távolságból, amelyből minden oldalról belefér (a határoló gömbje). Amíg ez be van kapcsolva, a Kezdőnézet ugyanazt a nagyítást és középpontot használja, így az utána választott nézetek csak elfordulnak. Ki: a nézet a helyén fordul el, megtartva a célpontját és a nagyítását.",
|
||||
"prefs.network_host_remove": "Eltávolítás",
|
||||
"prefs.network_locations": "Hálózati helyek engedélyezése a tervekben",
|
||||
"prefs.network_locations_note": "Ki: egy terv másik számítógépen lévő hivatkozott fájljai (\\\\server\\share) csak az itt felsorolt kiszolgálókról nyílnak meg, vagy miután kérdésre engedélyez egyet.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Buat Issue…",
|
||||
"Cross curves": "Kurva silang",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter melanjutkan di GitHub.",
|
||||
"Cube": "Kubus",
|
||||
"Current orthographic view (1:1 mm)": "Tampilan ortografis saat ini (1:1 mm)",
|
||||
"Current perspective view": "Tampilan perspektif saat ini",
|
||||
"Current plane": "Bidang saat ini",
|
||||
@@ -2239,6 +2240,9 @@
|
||||
"prefs.interface_note": "Tampilan SolidCraft dan letak bagian-bagiannya.",
|
||||
"prefs.interval_minutes": "{n} mnt",
|
||||
"prefs.invert_curved_roll_arrows": "Balikkan panah roll melengkung",
|
||||
"prefs.invert_curved_roll_arrows_note": "Dua panah melengkung di sudut kubus memutar (roll) tampilan di dalam bidang layar. Aktif: panah searah jarum jam memutar berlawanan arah jarum jam dan panah berlawanan arah jarum jam memutar searah jarum jam. Mati: tiap panah memutar tampilan sesuai arah lengkungannya.",
|
||||
"prefs.invert_navigator_arrows": "Balikkan panah rotasi kubus tampilan",
|
||||
"prefs.invert_navigator_arrows_note": "Panah di sekeliling kubus memutar orbit kamera ke arah yang ditunjuknya, seperti di Fusion dan Inventor: panah di kiri menunjuk ke kanan dan mengayunkan kamera ke kanan. Mati: panah memutar kubus ke sisi tempatnya berada, sehingga panah kiri mengayunkan kamera ke kiri. Panah roll yang melengkung tidak terpengaruh.",
|
||||
"prefs.keep_work_on_tab_switch": "Biarkan dialog dan alat tetap terbuka saat berpindah desain",
|
||||
"prefs.keep_work_on_tab_switch_note": "Setiap tab desain mempertahankan dialog atau alat yang terbuka, beserta apa yang Anda pilih dan ketik di dalamnya, dan tab toolbar-nya. Semuanya kembali saat Anda kembali ke desain itu. Mati: berpindah desain membatalkannya dan menampilkan tab Model.",
|
||||
"prefs.labels": "Label",
|
||||
@@ -2275,6 +2279,7 @@
|
||||
"prefs.navigation": "Navigasi",
|
||||
"prefs.navigation_note": "Cara mouse, trackpad, dan mouse 3D menggerakkan tampilan.",
|
||||
"prefs.navigator_fit": "Buat model pas di tampilan setelah perubahan tampilan lewat navigator",
|
||||
"prefs.navigator_fit_note": "Sebuah muka, sudut, atau panah pada kubus tampilan, atau sumbu pada gizmo, juga menghadap ke tengah model dari satu jarak yang membuatnya pas dari segala sisi (bola pembatasnya). Awal memakai zoom dan pusat yang sama selama opsi ini aktif, sehingga tampilan yang dipilih setelahnya hanya berputar. Mati: tampilan berputar di tempat, dengan target dan zoom tetap.",
|
||||
"prefs.network_host_remove": "Hapus",
|
||||
"prefs.network_locations": "Izinkan lokasi jaringan di desain",
|
||||
"prefs.network_locations_note": "Nonaktif: file tertaut sebuah desain di komputer lain (\\\\server\\share) hanya dibuka dari server yang tercantum di sini, atau setelah Anda mengizinkan salah satunya saat ditanya.",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "Crea segnalazione…",
|
||||
"Cross curves": "Curve trasversali",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter continua su GitHub.",
|
||||
"Cube": "Cubo",
|
||||
"Current orthographic view (1:1 mm)": "Vista ortografica corrente (1:1 mm)",
|
||||
"Current perspective view": "Vista prospettica corrente",
|
||||
"Current plane": "Piano corrente",
|
||||
@@ -2682,6 +2683,9 @@
|
||||
"prefs.interface_note": "Aspetto di SolidCraft e disposizione dei suoi elementi.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Inverti le frecce curve di rollio",
|
||||
"prefs.invert_curved_roll_arrows_note": "Le due frecce curve all'angolo del cubo fanno ruotare la vista nel piano dello schermo. Attivato: la freccia oraria ruota in senso antiorario e la freccia antioraria ruota in senso orario. Disattivato: ogni freccia ruota la vista nel verso in cui si curva.",
|
||||
"prefs.invert_navigator_arrows": "Inverti le frecce di rotazione del cubo della vista",
|
||||
"prefs.invert_navigator_arrows_note": "Le frecce attorno al cubo fanno orbitare la camera nella direzione in cui puntano, come in Fusion e Inventor: la freccia a sinistra punta a destra e sposta la camera a destra. Disattivato: una freccia ruota il cubo verso il lato in cui si trova, quindi la freccia sinistra sposta la camera a sinistra. Le frecce curve di rollio non sono interessate.",
|
||||
"prefs.keep_unsaved_also_asks": "Chiedi comunque alla chiusura della scheda",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Se disattivato, la scheda si chiude senza la finestra del browser quando la copia di ogni progetto non salvato è aggiornata.",
|
||||
"prefs.keep_unsaved_in_browser": "Conserva il lavoro non salvato in questo browser",
|
||||
@@ -2741,6 +2745,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Disattivato: mentre le frecce di Sposta trascinano un componente, niente sotto il puntatore viene evidenziato, così i grandi assiemi restano fluidi. Con una sezione o un'analisi visibile viene evidenziato comunque, e un pezzo trascinato lungo i suoi giunti non lo evidenzia mai.",
|
||||
"prefs.navigation": "Navigazione",
|
||||
"prefs.navigation.axes_signed": "Assi",
|
||||
"prefs.navigation.cube_compact": "Cubo",
|
||||
"prefs.navigation.device": "Dispositivo",
|
||||
"prefs.navigation.free": "Libera",
|
||||
"prefs.navigation.orbit_style": "Stile di orbita",
|
||||
@@ -2748,6 +2753,7 @@
|
||||
"prefs.navigation.turntable": "Piatto girevole",
|
||||
"prefs.navigation_note": "Come mouse, trackpad e mouse 3D spostano la vista.",
|
||||
"prefs.navigator_fit": "Adatta il modello dopo un cambio di vista dal navigatore",
|
||||
"prefs.navigator_fit_note": "Una faccia, un angolo o una freccia del cubo della vista, o un asse del gizmo, guarda anche il centro del modello dall'unica distanza che lo inquadra da ogni lato (la sua sfera di ingombro). Inizio usa lo stesso zoom e lo stesso centro finché questa opzione è attiva, quindi le viste scelte dopo si limitano a ruotare. Disattivato: la vista ruota sul posto, mantenendo punto di mira e zoom.",
|
||||
"prefs.network_host_remove": "Rimuovi",
|
||||
"prefs.network_locations": "Consenti posizioni di rete nei progetti",
|
||||
"prefs.network_locations_note": "Disattivato: i file collegati di un progetto su un altro computer (\\\\server\\share) vengono aperti solo dai server elencati qui, o dopo che ne consenti uno quando richiesto.",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "Issueを作成…",
|
||||
"Cross curves": "交差する曲線",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+EnterでGitHubに進みます。",
|
||||
"Cube": "キューブ",
|
||||
"Current orthographic view (1:1 mm)": "現在の平行投影(実寸・mm)",
|
||||
"Current perspective view": "現在の透視投影",
|
||||
"Current plane": "現在の平面",
|
||||
@@ -2682,9 +2683,9 @@
|
||||
"prefs.interface_note": "SolidCraft の外観と各パネルの配置。",
|
||||
"prefs.interval_minutes": "{n} 分",
|
||||
"prefs.invert_curved_roll_arrows": "カーブしたロール矢印を反転",
|
||||
"prefs.invert_curved_roll_arrows_note": "ナビゲーターの隅にある2つのカーブした矢印は、画面内でビューをロールします。オン: 時計回りの矢印が反時計回りに、反時計回りの矢印が時計回りにロールします。オフ: 各矢印は、そのカーブの向きにビューをロールします。",
|
||||
"prefs.invert_navigator_arrows": "ナビゲーターの回転矢印を反転",
|
||||
"prefs.invert_navigator_arrows_note": "ナビゲーターの周りの矢印が、矢印の指す方向にカメラを回します(Fusion や Inventor と同じ動き):左の矢印は右を指し、カメラを右に振ります。オフ: 矢印は、その側へビューを回すため、左の矢印はカメラを左に振ります。カーブしたロール矢印は影響を受けません。",
|
||||
"prefs.invert_curved_roll_arrows_note": "キューブの隅にある2つのカーブした矢印は、画面内でビューをロールします。オン: 時計回りの矢印が反時計回りに、反時計回りの矢印が時計回りにロールします。オフ: 各矢印は、そのカーブの向きにビューをロールします。",
|
||||
"prefs.invert_navigator_arrows": "ビューキューブの回転矢印を反転",
|
||||
"prefs.invert_navigator_arrows_note": "キューブの周りの矢印が、矢印の指す方向にカメラを回します(Fusion や Inventor と同じ動き):左の矢印は右を指し、カメラを右に振ります。オフ: 矢印は、そのある面の方へキューブを回すため、左の矢印はカメラを左に振ります。カーブしたロール矢印は影響を受けません。",
|
||||
"prefs.keep_unsaved_also_asks": "タブを閉じるときにも確認する",
|
||||
"prefs.keep_unsaved_also_asks_tip": "オフにすると、未保存の各デザインのコピーが最新であれば、ブラウザーの確認なしでタブが閉じます。",
|
||||
"prefs.keep_unsaved_in_browser": "未保存の作業をこのブラウザーに保持する",
|
||||
@@ -2744,6 +2745,7 @@
|
||||
"prefs.move_drag_highlights_tip": "オフ: 移動の矢印でコンポーネントをドラッグしている間はポインターの下をハイライトしないため、大きなアセンブリでも滑らかに動きます。断面や解析を表示中は通常どおりハイライトし、ジョイントに沿ってドラッグする部品ではハイライトしません。",
|
||||
"prefs.navigation": "ナビゲーション",
|
||||
"prefs.navigation.axes_signed": "軸",
|
||||
"prefs.navigation.cube_compact": "キューブ",
|
||||
"prefs.navigation.device": "デバイス",
|
||||
"prefs.navigation.free": "自由",
|
||||
"prefs.navigation.orbit_style": "回転方式",
|
||||
@@ -2751,7 +2753,7 @@
|
||||
"prefs.navigation.turntable": "ターンテーブル",
|
||||
"prefs.navigation_note": "マウス、トラックパッド、3Dマウスによるビューの操作。",
|
||||
"prefs.navigator_fit": "ナビゲーターでの視点変更後にモデルを全体表示",
|
||||
"prefs.navigator_fit_note": "ナビゲーターの面・角・矢印やギズモの軸をクリックすると、どの方向からでも全体が収まる一定の距離(外接球)からモデルの中心を見ます。オンの間はホームビューも同じズームと中心を使うため、その後に選んだ視点は向きだけが変わります。オフ: 視点はその場で回転し、注視点とズームを保ちます。",
|
||||
"prefs.navigator_fit_note": "キューブの面・角・矢印やギズモの軸をクリックすると、どの方向からでも全体が収まる一定の距離(外接球)からモデルの中心を見ます。オンの間はホームビューも同じズームと中心を使うため、その後に選んだ視点は向きだけが変わります。オフ: 視点はその場で回転し、注視点とズームを保ちます。",
|
||||
"prefs.network_host_remove": "削除",
|
||||
"prefs.network_locations": "デザイン内のネットワーク上の場所を許可",
|
||||
"prefs.network_locations_note": "ファイルサーバーでデザインを管理する組織向け。オフの場合は許可したサーバーのみ。",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "შეტყობინების შექმნა…",
|
||||
"Cross curves": "განივი მრუდები",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter-ით GitHub-ზე გააგრძელებთ.",
|
||||
"Cube": "კუბი",
|
||||
"Current orthographic view (1:1 mm)": "მიმდინარე ორთოგრაფიული ხედი (1:1 mm)",
|
||||
"Current perspective view": "მიმდინარე პერსპექტიული ხედი",
|
||||
"Current plane": "მიმდინარე სიბრტყე",
|
||||
@@ -2315,6 +2316,9 @@
|
||||
"prefs.interface_note": "როგორ გამოიყურება SolidCraft და სად არის მისი ნაწილები.",
|
||||
"prefs.interval_minutes": "{n} წთ",
|
||||
"prefs.invert_curved_roll_arrows": "გორების მრუდე ისრების შებრუნება",
|
||||
"prefs.invert_curved_roll_arrows_note": "კუბის კუთხესთან მდებარე ორი მრუდე ისარი ხედს ეკრანის სიბრტყეში აგორებს. ჩართული: საათის ისრის მიმართულების ისარი საათის ისრის საწინააღმდეგოდ აგორებს, საწინააღმდეგო კი — საათის ისრის მიმართულებით. გამორთული: თითოეული ისარი ხედს თავისი მოხრის მიმართულებით აგორებს.",
|
||||
"prefs.invert_navigator_arrows": "ხედის კუბის ბრუნვის ისრების შებრუნება",
|
||||
"prefs.invert_navigator_arrows_note": "კუბის გარშემო ისრები კამერას ორბიტაზე იმ მიმართულებით ატრიალებს, საითაც მიუთითებს, როგორც Fusion-სა და Inventor-ში: მარცხენა ისარი მარჯვნივ მიუთითებს და კამერას მარჯვნივ აბრუნებს. გამორთული: ისარი კუბს იმ მხრისკენ აბრუნებს, რომელზეც მდებარეობს, ამიტომ მარცხენა ისარი კამერას მარცხნივ აბრუნებს. გორების მრუდე ისრებზე ეს არ მოქმედებს.",
|
||||
"prefs.keep_work_on_tab_switch": "პროექტებს შორის გადართვისას დიალოგებისა და ხელსაწყოების ღიად დატოვება",
|
||||
"prefs.keep_work_on_tab_switch_note": "პროექტის თითოეული ჩანართი ინარჩუნებს თავის ღია დიალოგს ან ხელსაწყოს, მასში არჩეულსა და აკრეფილს, და ხელსაწყოთა ზოლის ჩანართს. პროექტში დაბრუნებისას ისინი ისევ იქ დაგხვდებათ. გამორთული: პროექტების გადართვა მათ აუქმებს და „მოდელის“ ჩანართს აჩვენებს.",
|
||||
"prefs.labels": "წარწერები",
|
||||
@@ -2351,6 +2355,7 @@
|
||||
"prefs.navigation": "ნავიგაცია",
|
||||
"prefs.navigation_note": "როგორ ამოძრავებს ხედს მაუსი, სენსორული პანელი და 3D მაუსი.",
|
||||
"prefs.navigator_fit": "ნავიგატორით ხედის შეცვლის შემდეგ მოდელის ჩატევა",
|
||||
"prefs.navigator_fit_note": "ხედის კუბის წახნაგი, კუთხე ან ისარი, ან მანიპულატორის ღერძი ასევე მოდელის შუაგულს უყურებს ერთი მანძილიდან, საიდანაც ის ყველა მხრიდან ეტევა (მისი შემომსაზღვრელი სფერო). სანამ ეს ჩართულია, საწყისი ხედი იმავე მასშტაბსა და ცენტრს იყენებს, ამიტომ მის შემდეგ არჩეული ხედები მხოლოდ ბრუნავს. გამორთული: ხედი ადგილზე ბრუნავს, სამიზნისა და მასშტაბის შენარჩუნებით.",
|
||||
"prefs.network_host_remove": "წაშლა",
|
||||
"prefs.network_locations": "პროექტებში ქსელური მდებარეობების დაშვება",
|
||||
"prefs.network_locations_note": "ორგანიზაციებისთვის, რომლებიც პროექტებს ფაილურ სერვერებზე ინახავენ. გამორთული: მხოლოდ თქვენ მიერ დაშვებული სერვერები.",
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
"Create Issue — review draft": "이슈 만들기 — 초안 검토",
|
||||
"Create Issue…": "이슈 만들기…",
|
||||
"Cross curves": "교차 곡선",
|
||||
"Cube": "큐브",
|
||||
"Current orthographic view (1:1 mm)": "현재 직교 투영 뷰 (1:1 mm)",
|
||||
"Current perspective view": "현재 원근 투영 뷰",
|
||||
"Current plane": "현재 평면",
|
||||
@@ -2082,6 +2083,7 @@
|
||||
"prefs.move_drag_highlights_tip": "끄면 이동 화살표로 구성 요소를 끄는 동안 포인터 아래 항목을 강조 표시하지 않아 큰 어셈블리도 부드럽게 움직입니다. 단면이나 해석이 표시된 동안에는 그대로 강조 표시하며, 조인트를 따라 끄는 부품은 강조 표시하지 않습니다.",
|
||||
"prefs.navigation": "탐색",
|
||||
"prefs.navigation.axes_signed": "축",
|
||||
"prefs.navigation.cube_compact": "큐브",
|
||||
"prefs.navigation.device": "장치",
|
||||
"prefs.navigation.free": "자유",
|
||||
"prefs.navigation.orbit_style": "회전 방식",
|
||||
@@ -2089,6 +2091,7 @@
|
||||
"prefs.navigation.turntable": "턴테이블",
|
||||
"prefs.navigation_note": "마우스, 트랙패드, 3D 마우스로 뷰를 움직이는 방식입니다.",
|
||||
"prefs.navigator_fit": "뷰 탐색기로 뷰를 바꾼 뒤 모델 맞춤",
|
||||
"prefs.navigator_fit_note": "큐브의 면, 모서리, 화살표나 기즈모의 축을 클릭하면 어느 방향에서든 전체가 들어가는 한 거리(경계 구)에서 모델의 중심을 바라봅니다. 켜져 있는 동안 홈 뷰도 같은 확대/축소와 중심을 쓰므로 그 뒤에 고른 뷰는 방향만 바뀝니다. 끄면 뷰가 제자리에서 회전하며 대상과 확대/축소를 유지합니다.",
|
||||
"prefs.network_locations_note": "파일 서버에 디자인을 보관하는 조직용입니다. 끄면 허용한 서버만 사용합니다.",
|
||||
"prefs.not_on_gpu": "이 GPU에서는 사용할 수 없음",
|
||||
"prefs.on": "켜짐",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "Izveidot ziņojumu…",
|
||||
"Cross curves": "Šķērslīknes",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter turpina GitHub vietnē.",
|
||||
"Cube": "Kubs",
|
||||
"Current orthographic view (1:1 mm)": "Pašreizējais ortogrāfiskais skats (1:1 mm)",
|
||||
"Current perspective view": "Pašreizējais perspektīvas skats",
|
||||
"Current plane": "Pašreizējā plakne",
|
||||
@@ -2682,6 +2683,9 @@
|
||||
"prefs.interface_note": "Kā izskatās SolidCraft un kur atrodas tā daļas.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Apgriezt izliektās griešanas bultas",
|
||||
"prefs.invert_curved_roll_arrows_note": "Divas izliektās bultas kuba stūrī griež skatu ekrāna plaknē. Ieslēgts: pulksteņrādītāja virziena bulta griež pretēji pulksteņrādītājam, bet pretējā virziena bulta griež pulksteņrādītāja virzienā. Izslēgts: katra bulta griež skatu tā, kā tā ir izliekta.",
|
||||
"prefs.invert_navigator_arrows": "Apgriezt skata kuba rotācijas bultas",
|
||||
"prefs.invert_navigator_arrows_note": "Bultas ap kubu orbitē kameru tajā virzienā, kurp tās norāda, kā Fusion un Inventor: kreisā bulta norāda pa labi un pagriež kameru pa labi. Izslēgts: bulta pagriež kubu uz to pusi, kurā tā atrodas, tāpēc kreisā bulta pagriež kameru pa kreisi. Izliektās griešanas bultas netiek ietekmētas.",
|
||||
"prefs.keep_unsaved_also_asks": "Tomēr jautāt, kad cilne tiek aizvērta",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Ja šis ir izslēgts, cilne aizveras bez pārlūka dialoga, tiklīdz katra nesaglabātā modeļa kopija ir aktuāla.",
|
||||
"prefs.keep_unsaved_in_browser": "Glabāt nesaglabāto darbu šajā pārlūkā",
|
||||
@@ -2741,6 +2745,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Izslēgts: kamēr Pārvietot bultiņas velk komponenti, nekas zem kursora netiek izcelts, tāpēc lielas montāžas darbojas raiti. Ja tiek rādīts šķērsgriezums vai analīze, izcelšana notiek tik un tā, bet, velkot detaļu pa tās savienojumiem, tā nenotiek nekad.",
|
||||
"prefs.navigation": "Navigācija",
|
||||
"prefs.navigation.axes_signed": "Asis",
|
||||
"prefs.navigation.cube_compact": "Kubs",
|
||||
"prefs.navigation.device": "Ierīce",
|
||||
"prefs.navigation.free": "Brīvs",
|
||||
"prefs.navigation.orbit_style": "Orbitēšanas stils",
|
||||
@@ -2748,6 +2753,7 @@
|
||||
"prefs.navigation.turntable": "Grozāmgalds",
|
||||
"prefs.navigation_note": "Kā pele, skārienpaliktnis un 3D pele pārvieto skatu.",
|
||||
"prefs.navigator_fit": "Ietilpināt modeli pēc navigatora skata maiņas",
|
||||
"prefs.navigator_fit_note": "Skata kuba skaldne, stūris vai bulta vai gizmo ass arī vērš skatu uz modeļa vidu no tāda attāluma, kas to ietilpina no jebkuras puses (tā aptverošā sfēra). Kamēr šis ir ieslēgts, Sākums izmanto to pašu tālummaiņu un centru, tāpēc pēc tam izvēlētie skati tikai pagriežas. Izslēgts: skats pagriežas uz vietas, saglabājot savu mērķi un tālummaiņu.",
|
||||
"prefs.network_host_remove": "Noņemt",
|
||||
"prefs.network_locations": "Atļaut tīkla atrašanās vietas modeļos",
|
||||
"prefs.network_locations_note": "Organizācijām, kas glabā modeļus failu serveros. Izslēgts: tikai jūsu atļautie serveri.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Cipta Isu…",
|
||||
"Cross curves": "Lengkung silang",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter meneruskan di GitHub.",
|
||||
"Cube": "Kubus",
|
||||
"Current orthographic view (1:1 mm)": "Pandangan ortografik semasa (1:1 mm)",
|
||||
"Current perspective view": "Pandangan perspektif semasa",
|
||||
"Current plane": "Satah semasa",
|
||||
@@ -2239,6 +2240,9 @@
|
||||
"prefs.interface_note": "Rupa SolidCraft dan kedudukan bahagiannya.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Songsangkan anak panah gelek melengkung",
|
||||
"prefs.invert_curved_roll_arrows_note": "Dua anak panah melengkung di sudut kubus menggelek pandangan dalam satah skrin. Hidup: anak panah ikut arah jam menggelek lawan arah jam dan anak panah lawan arah jam menggelek ikut arah jam. Mati: setiap anak panah menggelek pandangan mengikut arah lengkungannya.",
|
||||
"prefs.invert_navigator_arrows": "Songsangkan anak panah putaran kubus pandangan",
|
||||
"prefs.invert_navigator_arrows_note": "Anak panah di sekeliling kubus mengorbit kamera mengikut arah yang ditunjuknya, seperti dalam Fusion dan Inventor: anak panah di kiri menunjuk ke kanan dan mengayunkan kamera ke kanan. Mati: anak panah memutarkan kubus ke arah sisi tempat ia berada, jadi anak panah kiri mengayunkan kamera ke kiri. Anak panah gelek yang melengkung tidak terjejas.",
|
||||
"prefs.keep_work_on_tab_switch": "Kekalkan dialog dan alat terbuka apabila menukar reka bentuk",
|
||||
"prefs.keep_work_on_tab_switch_note": "Setiap tab reka bentuk mengekalkan dialog atau alat yang dibukanya, bersama apa yang anda pilih dan taip di dalamnya, serta tab bar alatnya. Semuanya ada semula apabila anda kembali ke reka bentuk itu. Mati: menukar reka bentuk membatalkannya dan menunjukkan tab Model.",
|
||||
"prefs.labels": "Label",
|
||||
@@ -2275,6 +2279,7 @@
|
||||
"prefs.navigation": "Navigasi",
|
||||
"prefs.navigation_note": "Cara tetikus, pad jejak dan tetikus 3D menggerakkan pandangan.",
|
||||
"prefs.navigator_fit": "Muatkan model selepas perubahan pandangan navigator",
|
||||
"prefs.navigator_fit_note": "Muka, sudut atau anak panah pada kubus pandangan, atau paksi pada gizmo, turut menghadap ke tengah model dari satu jarak yang memuatkannya dari semua sisi (sfera pembatasnya). Utama menggunakan zum dan pusat yang sama selagi ini dihidupkan, jadi pandangan yang dipilih selepasnya hanya berputar. Mati: pandangan berputar di tempat, dengan sasaran dan zumnya kekal.",
|
||||
"prefs.network_host_remove": "Buang",
|
||||
"prefs.network_locations": "Benarkan lokasi rangkaian dalam reka bentuk",
|
||||
"prefs.network_locations_note": "Dimatikan: fail terpaut bagi reka bentuk pada komputer lain (\\\\server\\share) hanya dibuka daripada pelayan yang disenaraikan di sini, atau selepas anda membenarkannya apabila ditanya.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Opprett sak…",
|
||||
"Cross curves": "Tverrkurver",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter fortsetter på GitHub.",
|
||||
"Cube": "Kube",
|
||||
"Current orthographic view (1:1 mm)": "Gjeldende ortografisk visning (1:1 mm)",
|
||||
"Current perspective view": "Gjeldende perspektivvisning",
|
||||
"Current plane": "Gjeldende plan",
|
||||
@@ -2239,6 +2240,9 @@
|
||||
"prefs.interface_note": "Hvordan SolidCraft ser ut, og hvor delene er plassert.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Inverter de krumme rullepilene",
|
||||
"prefs.invert_curved_roll_arrows_note": "De to krumme pilene i hjørnet av kuben ruller visningen i skjermplanet. På: pilen med klokken ruller mot klokken, og pilen mot klokken ruller med klokken. Av: hver pil ruller visningen i den retningen den krummer seg.",
|
||||
"prefs.invert_navigator_arrows": "Inverter rotasjonspilene på visningskuben",
|
||||
"prefs.invert_navigator_arrows_note": "Pilene rundt kuben roterer kameraet i den retningen de peker, som i Fusion og Inventor: pilen til venstre peker mot høyre og svinger kameraet mot høyre. Av: en pil roterer kuben mot siden den står på, så venstre pil svinger kameraet mot venstre. De krumme rullepilene påvirkes ikke.",
|
||||
"prefs.keep_work_on_tab_switch": "Behold dialoger og verktøy åpne når du bytter design",
|
||||
"prefs.keep_work_on_tab_switch_note": "Hver designfane beholder sin åpne dialog eller sitt åpne verktøy, med det du har valgt og skrevet i den, og sin fane på båndet. De er der igjen når du kommer tilbake til designet. Av: å bytte design avbryter dem og viser fanen Modell.",
|
||||
"prefs.labels": "Etiketter",
|
||||
@@ -2275,6 +2279,7 @@
|
||||
"prefs.navigation": "Navigasjon",
|
||||
"prefs.navigation_note": "Hvordan mus, styreflate og 3D-mus beveger visningen.",
|
||||
"prefs.navigator_fit": "Tilpass modellen etter en visningsendring fra navigatoren",
|
||||
"prefs.navigator_fit_note": "En flate, et hjørne eller en pil på visningskuben, eller en akse på orienteringsindikatoren, ser også mot midten av modellen fra den ene avstanden som får den til å passe fra alle sider (den omsluttende kulen). Hjem bruker samme zoom og senter mens dette er på, så visninger som velges etterpå bare dreier. Av: visningen dreier på stedet og beholder målpunktet og zoomen sin.",
|
||||
"prefs.network_host_remove": "Fjern",
|
||||
"prefs.network_locations": "Tillat nettverksplasseringer i design",
|
||||
"prefs.network_locations_note": "Av: lenkede filer i et design på en annen datamaskin (\\\\server\\share) åpnes bare fra serverne som er oppført her, eller etter at du har tillatt en når du blir spurt.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Issue aanmaken…",
|
||||
"Cross curves": "Kruiscurven",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter gaat verder op GitHub.",
|
||||
"Cube": "Kubus",
|
||||
"Current orthographic view (1:1 mm)": "Huidige orthografische weergave (1:1 mm)",
|
||||
"Current perspective view": "Huidige perspectiefweergave",
|
||||
"Current plane": "Huidig vlak",
|
||||
@@ -2250,6 +2251,9 @@
|
||||
"prefs.interface_note": "Hoe SolidCraft eruitziet en waar de onderdelen staan.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "De gebogen rolpijlen omkeren",
|
||||
"prefs.invert_curved_roll_arrows_note": "De twee gebogen pijlen op de hoek van de kubus laten de weergave in het schermvlak rollen. Aan: de pijl met de klok mee rolt tegen de klok in en de pijl tegen de klok in rolt met de klok mee. Uit: elke pijl rolt de weergave in de richting waarin hij krult.",
|
||||
"prefs.invert_navigator_arrows": "De rotatiepijlen van de weergavekubus omkeren",
|
||||
"prefs.invert_navigator_arrows_note": "De pijlen rond de kubus laten de camera roteren in de richting waarin ze wijzen, zoals in Fusion en Inventor: de pijl links wijst naar rechts en zwaait de camera naar rechts. Uit: een pijl draait de kubus naar de kant waar hij staat, dus de linkerpijl zwaait de camera naar links. De gebogen rolpijlen worden hierdoor niet beïnvloed.",
|
||||
"prefs.keep_work_on_tab_switch": "Dialoogvensters en gereedschappen open houden bij het wisselen van ontwerp",
|
||||
"prefs.keep_work_on_tab_switch_note": "Elk ontwerptabblad behoudt zijn geopende dialoogvenster of gereedschap, met wat je erin hebt gekozen en getypt, en zijn lint-tabblad. Ze zijn er weer als je terugkeert naar het ontwerp. Uit: wisselen van ontwerp breekt ze af en toont het tabblad Model.",
|
||||
"prefs.labels": "Labels",
|
||||
@@ -2286,6 +2290,7 @@
|
||||
"prefs.navigation": "Navigatie",
|
||||
"prefs.navigation_note": "Hoe de muis, het trackpad en de 3D-muis de weergave verplaatsen.",
|
||||
"prefs.navigator_fit": "Het model passend in beeld brengen na een wijziging van de navigatorweergave",
|
||||
"prefs.navigator_fit_note": "Een vlak, hoek of pijl van de weergavekubus, of een as van de gizmo, kijkt ook naar het midden van het model, vanaf de ene afstand waarop het van alle kanten in beeld past (de omsluitende bol). Beginweergave gebruikt dezelfde zoom en hetzelfde middelpunt zolang dit aan staat, zodat weergaven die je daarna kiest alleen draaien. Uit: de weergave draait ter plaatse en behoudt het richtpunt en de zoom.",
|
||||
"prefs.network_host_remove": "Verwijderen",
|
||||
"prefs.network_locations": "Netwerklocaties in ontwerpen toestaan",
|
||||
"prefs.network_locations_note": "Uit: gekoppelde bestanden van een ontwerp op een andere computer (\\\\server\\share) worden alleen geopend vanaf de servers in deze lijst, of nadat je er een toestaat wanneer erom wordt gevraagd.",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"uk": {
|
||||
"ai.prefs.control_failed": "Не вдалося запустити канал керування: {error}",
|
||||
"ai.prefs.control_from_launch": "Запущено з --control на порту {port} для цього запуску. Зміни тут набудуть чинності наступного разу, коли SolidCraft запуститься без --control.",
|
||||
"ai.prefs.control_listening": "Прослуховування на 127.0.0.1:{port}",
|
||||
"ai.prefs.control_not_listening": "Не прослуховується",
|
||||
"ai.prefs.control_port": "Порт",
|
||||
"ai.prefs.control_port_note": "Порт, до якого ШІ-агенти підключаються на цьому комп'ютері (127.0.0.1). 27878 — типовий: якщо його зайнято, береться наступний вільний. Будь-який інший порт використовується точно. Спрямуйте свій клієнт MCP на той самий порт, наприклад solidcraft-cli mcp --connect 127.0.0.1:7878.",
|
||||
"ai.prefs.control_port_taken": "Порт {port} зайнятий іншою програмою (або іншим вікном SolidCraft). Виберіть інший порт.",
|
||||
"cmd.assembly_constraint_create": "Обмежити",
|
||||
"cmd.assembly_constraint_edit": "Редагувати обмеження",
|
||||
"cmd.construct.axis": "Вісь",
|
||||
"cmd.construct.plane": "Площина",
|
||||
"cmd.construct.point": "Точка",
|
||||
"cmd.note_add": "Додати примітку",
|
||||
"cmd.note_delete": "Видалити примітку",
|
||||
"cmd.note_list": "Список приміток",
|
||||
"cmd.note_update": "Редагувати примітку",
|
||||
"cmd.selection_get": "Отримати виділення",
|
||||
"dialog.component_origin": "{placement} · {item}",
|
||||
"file_load.opened_behind": "Відкрито {name} у новій вкладці",
|
||||
"help.construct.axis": "Створює допоміжну вісь методом, який підказують ваші вибори: уздовж ребра чи прямої, крізь отвір або круглу грань, там, де перетинаються дві площини, через дві точки чи перпендикулярно до грані через точку. Використовуйте, коли ви знаєте опорні елементи, але не метод.",
|
||||
"help.construct.plane": "Створює допоміжну площину методом, який підказують ваші вибори: зі зміщенням від площини, через точку, між двома площинами, через прямі чи точки, дотично до криволінійної грані чи перпендикулярно до траєкторії. Використовуйте, коли ви знаєте опорні елементи, але не метод.",
|
||||
"help.construct.point": "Створює допоміжну точку методом, який підказують ваші вибори: у вершині, у центрі кола чи кулі, уздовж ребра, там, де перетинаються дві прямі чи три площини, або там, де пряма перетинає площину. Використовуйте, коли ви знаєте опорні елементи, але не метод.",
|
||||
"nav.maya_horizontal_zoom.help": "Alt+праве перетягування: праворуч — наближення, ліворуч — віддалення. Вимкніть, щоб повернути вертикальне масштабування перетягуванням.",
|
||||
"nav.maya_horizontal_zoom.label": "Maya: горизонтальне масштабування перетягуванням",
|
||||
"operations.cancel": "Скасувати {name}",
|
||||
"operations.cancelled": "{name} скасовано",
|
||||
"operations.cancelling": "Скасування…",
|
||||
"operations.cannot_cancel": "{name} не можна скасувати",
|
||||
"operations.feature": "Операція",
|
||||
"operations.feature_edit": "Редагувати операцію",
|
||||
"operations.meshing": "Завантаження деталей",
|
||||
"operations.queued": "У черзі",
|
||||
"operations.stopped": "Скасовано",
|
||||
"operations.waiting": "Готово",
|
||||
"prefs.assemblies": "Збірки",
|
||||
"prefs.constraint_glyph_size": "Розмір позначок обмежень",
|
||||
"prefs.constraint_glyph_size_note": "Наскільки великими малюються позначки обмежень та їхні літери чи піктограми в ескізі. 100% — звичайний розмір; позначки зберігають цей розмір на екрані за будь-якого масштабу.",
|
||||
"prefs.construct_menu_groups": "Групувати інструменти побудови за типом об'єкта",
|
||||
"prefs.construct_menu_groups_help": "Показувати заголовки «Площини», «Осі» та «Точки». Вимкнено: зберігається початковий порядок меню.",
|
||||
"prefs.dialog_first_letter_buttons": "Кнопки діалогів реагують на першу літеру",
|
||||
"prefs.dialog_first_letter_buttons_help": "Коли діалог відкрито, натискання першої літери напису кнопки натискає її: O — OK, С — «Скасувати». Літера, що підходить до двох кнопок, нічого не робить, а сфокусоване текстове поле зберігає натискання. Вимкнено: натискання літер не діє на кнопки, як раніше.",
|
||||
"prefs.export": "Експорт",
|
||||
"prefs.export_name": "Ім'я файлу експорту",
|
||||
"prefs.export_name.body": "Назва експортованого тіла (першого, якщо їх кілька)",
|
||||
"prefs.export_name.design": "Назва проєкту",
|
||||
"prefs.export_name_hint": "напр. {design}-{body}",
|
||||
"prefs.export_name_sample": "Під час експорту цього проєкту пропонується: {name}",
|
||||
"prefs.export_name_tip": "Ім'я файлу, яке пропонують «Файл» › «Експорт» і «Зберегти як сітку». Ці заповнювачі замінюються:\n{placeholders}\nНевстановлену властивість проєкту пропущено. Залиште поле порожнім, щоб пропонувати назву проєкту або, для сітки, назву тіла.",
|
||||
"prefs.file_operations_wait": "Чекати на відкриття файлів",
|
||||
"prefs.file_operations_wait_note": "Поки файл відкривається чи вставляється або зв'язки читаються знову, проєкт чекає за карткою з поступом файлу й кнопкою «Скасувати». Вимкнено: файл показується в панелі операцій, і ви можете працювати далі; вставлення відбувається, коли ви завершите поточну дію, а відкритий тим часом файл потрапляє в нову вкладку.",
|
||||
"prefs.joint_drag_trackball": "Обертати деталі на кульовому з'єднанні під час перетягування, як трекболом",
|
||||
"prefs.joint_drag_trackball_help": "Перетягування деталі, яку утримує кульове з'єднання, обертає її найменшим поворотом навколо з'єднання, наче котиться куля розміром із деталь. Вимкнено: схоплена точка точно йде за вказівником, і деталь може також обертатися навколо неї.",
|
||||
"prefs.operations_panel": "Показувати операції у виді",
|
||||
"prefs.operations_panel_note": "Тривалі операції (відкриття файлу, обчислення операції, команда агента) показуються в правому нижньому куті виду з поступом і кнопкою, що їх скасовує. Вимкнено: повідомлення «Обчислення», як раніше.",
|
||||
"render.error_product_placement": "недійсне розміщення виробу",
|
||||
"sketch.move_released": "Переміщено: звільнено обмежень, що прив'язували його до нерухомого: {count} ({names}).",
|
||||
"sketch.move_released_one": "Переміщено: звільнено обмеження, що прив'язувало його до нерухомого: {count} ({names}).",
|
||||
"ui.agent_chat_answered_in": "Відповідь за {time}",
|
||||
"ui.agent_chat_ended_after": "Завершено за {time}",
|
||||
"ui.agent_chat_model_search_or_id": "Пошук або введіть будь-який ID моделі…",
|
||||
"ui.agent_chat_span_h": "{h} год {min} хв",
|
||||
"ui.agent_chat_span_min": "{n} хв",
|
||||
"ui.agent_chat_span_s": "{n} с",
|
||||
"ui.agent_chat_thinking_for": "Міркування: {time}",
|
||||
"ui.agent_chat_tool_running_for": "триває {time}",
|
||||
"ui.agent_chat_transcript_assistant": "Асистент:",
|
||||
"ui.agent_chat_transcript_user": "Користувач:",
|
||||
"ui.construct_auto.chosen": "Вибрано за вашими опорними елементами. Виберіть інший опорний елемент або приберіть один, щоб змінити метод.",
|
||||
"ui.construct_auto.no_fit": "Виберіть інші опорні елементи: жоден метод не будує з цих.",
|
||||
"ui.construct_auto.pick": "Виберіть, з чого будувати: метод випливає з ваших виборів.",
|
||||
"ui.construct_auto.refs": "Опорні елементи",
|
||||
"ui.construct_auto.select": "Спершу виберіть опорні елементи для побудови",
|
||||
"ui.construct_axes": "Осі",
|
||||
"ui.construct_planes": "Площини",
|
||||
"ui.construct_points": "Точки"
|
||||
}
|
||||
}
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "Utwórz zgłoszenie…",
|
||||
"Cross curves": "Krzywe poprzeczne",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter przechodzi dalej na GitHubie.",
|
||||
"Cube": "Sześcian",
|
||||
"Current orthographic view (1:1 mm)": "Bieżący widok ortogonalny (1:1 mm)",
|
||||
"Current perspective view": "Bieżący widok perspektywiczny",
|
||||
"Current plane": "Bieżąca płaszczyzna",
|
||||
@@ -2658,6 +2659,9 @@
|
||||
"prefs.interface_note": "Wygląd SolidCraft i rozmieszczenie jego elementów.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Odwróć zakrzywione strzałki przechylenia",
|
||||
"prefs.invert_curved_roll_arrows_note": "Dwie zakrzywione strzałki w rogu kostki przechylają widok w płaszczyźnie ekranu. Włączone: strzałka zgodna z ruchem wskazówek zegara przechyla przeciwnie, a przeciwna – zgodnie. Wyłączone: każda strzałka przechyla widok w stronę, w którą jest zakrzywiona.",
|
||||
"prefs.invert_navigator_arrows": "Odwróć strzałki obrotu kostki widoku",
|
||||
"prefs.invert_navigator_arrows_note": "Strzałki wokół kostki obracają kamerę w kierunku, w który wskazują, jak w Fusion i Inventor: strzałka po lewej wskazuje w prawo i obraca kamerę w prawo. Wyłączone: strzałka obraca kostkę w stronę, po której się znajduje, więc lewa strzałka obraca kamerę w lewo. Zakrzywione strzałki przechylenia nie są zmieniane.",
|
||||
"prefs.keep_unsaved_also_asks": "Mimo to pytaj przy zamykaniu karty",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Gdy wyłączone, karta zamyka się bez okna przeglądarki, jeśli kopia każdego niezapisanego projektu jest aktualna.",
|
||||
"prefs.keep_unsaved_in_browser": "Przechowuj niezapisaną pracę w tej przeglądarce",
|
||||
@@ -2717,6 +2721,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Wyłączone: gdy strzałki Przesuń przeciągają komponent, nic pod wskaźnikiem nie jest podświetlane, co zachowuje płynność dużych złożeń. Przy pokazanym przekroju lub analizie podświetlenie i tak działa, a część przeciągana wzdłuż złączy nigdy go nie podświetla.",
|
||||
"prefs.navigation": "Nawigacja",
|
||||
"prefs.navigation.axes_signed": "Osie",
|
||||
"prefs.navigation.cube_compact": "Kostka",
|
||||
"prefs.navigation.device": "Urządzenie",
|
||||
"prefs.navigation.free": "Swobodne",
|
||||
"prefs.navigation.orbit_style": "Styl obracania",
|
||||
@@ -2724,6 +2729,7 @@
|
||||
"prefs.navigation.turntable": "Obrotnica",
|
||||
"prefs.navigation_note": "Jak mysz, gładzik i mysz 3D poruszają widokiem.",
|
||||
"prefs.navigator_fit": "Dopasuj model po zmianie widoku w nawigatorze",
|
||||
"prefs.navigator_fit_note": "Ściana, narożnik lub strzałka kostki widoku albo oś wskaźnika orientacji kieruje też widok na środek modelu z odległości, z której mieści się on z każdej strony (jego sfera otaczająca). Gdy to jest włączone, Widok główny używa tego samego powiększenia i środka, więc widoki wybrane później tylko się obracają. Wyłączone: widok obraca się w miejscu, zachowując cel i powiększenie.",
|
||||
"prefs.network_host_remove": "Usuń",
|
||||
"prefs.network_locations": "Zezwalaj na lokalizacje sieciowe w projektach",
|
||||
"prefs.network_locations_note": "Wyłączone: połączone pliki projektu na innym komputerze (\\\\serwer\\udział) są otwierane tylko z serwerów wymienionych tutaj albo po zezwoleniu, gdy pojawi się pytanie.",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "Criar issue…",
|
||||
"Cross curves": "Curvas transversais",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter continua no GitHub.",
|
||||
"Cube": "Cubo",
|
||||
"Current orthographic view (1:1 mm)": "Vista ortográfica atual (1:1 mm)",
|
||||
"Current perspective view": "Vista em perspectiva atual",
|
||||
"Current plane": "Plano atual",
|
||||
@@ -2682,6 +2683,9 @@
|
||||
"prefs.interface_note": "A aparência do SolidCraft e onde ficam suas partes.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Inverter as setas curvas de rolagem",
|
||||
"prefs.invert_curved_roll_arrows_note": "As duas setas curvas no canto do cubo rolam a vista no plano da tela. Ativado: a seta horária rola no sentido anti-horário e a seta anti-horária rola no sentido horário. Desativado: cada seta rola a vista no sentido em que se curva.",
|
||||
"prefs.invert_navigator_arrows": "Inverter as setas de rotação do cubo de vista",
|
||||
"prefs.invert_navigator_arrows_note": "As setas em volta do cubo orbitam a câmera para onde apontam, como no Fusion e no Inventor: a seta da esquerda aponta para a direita e gira a câmera para a direita. Desativado: uma seta gira o cubo em direção ao lado em que está, então a seta da esquerda gira a câmera para a esquerda. As setas curvas de rolagem não são afetadas.",
|
||||
"prefs.keep_unsaved_also_asks": "Perguntar mesmo assim ao fechar a aba",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Com isto desativado, a aba fecha sem a caixa do navegador quando a cópia de cada projeto não salvo estiver atualizada.",
|
||||
"prefs.keep_unsaved_in_browser": "Guardar trabalho não salvo neste navegador",
|
||||
@@ -2741,6 +2745,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Desativado: enquanto as setas de Mover arrastam um componente, nada sob o ponteiro é destacado, o que mantém montagens grandes fluidas. Com um corte ou uma análise exibidos, o destaque acontece mesmo assim, e uma peça arrastada ao longo de suas juntas nunca o destaca.",
|
||||
"prefs.navigation": "Navegação",
|
||||
"prefs.navigation.axes_signed": "Eixos",
|
||||
"prefs.navigation.cube_compact": "Cubo",
|
||||
"prefs.navigation.device": "Dispositivo",
|
||||
"prefs.navigation.free": "Livre",
|
||||
"prefs.navigation.orbit_style": "Estilo de órbita",
|
||||
@@ -2748,6 +2753,7 @@
|
||||
"prefs.navigation.turntable": "Mesa giratória",
|
||||
"prefs.navigation_note": "Como o mouse, o trackpad e o mouse 3D movem a vista.",
|
||||
"prefs.navigator_fit": "Ajustar o modelo após uma mudança de vista do navegador",
|
||||
"prefs.navigator_fit_note": "Uma face, um canto ou uma seta do cubo de vista, ou um eixo do gizmo, também olha para o meio do modelo a partir da distância única que o enquadra de todos os lados (sua esfera envolvente). Início usa o mesmo zoom e centro enquanto isto estiver ativado, então as vistas escolhidas depois só giram. Desativado: a vista gira no lugar, mantendo seu alvo e zoom.",
|
||||
"prefs.network_host_remove": "Remover",
|
||||
"prefs.network_locations": "Permitir locais de rede nos projetos",
|
||||
"prefs.network_locations_note": "Para organizações que mantêm projetos em servidores de arquivos. Desativado: apenas servidores que você permitir.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Creează raport…",
|
||||
"Cross curves": "Curbe transversale",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter continuă pe GitHub.",
|
||||
"Cube": "Cub",
|
||||
"Current orthographic view (1:1 mm)": "Vizualizare ortografică curentă (1:1 mm)",
|
||||
"Current perspective view": "Vizualizare în perspectivă curentă",
|
||||
"Current plane": "Planul curent",
|
||||
@@ -2239,6 +2240,9 @@
|
||||
"prefs.interface_note": "Cum arată SolidCraft și unde sunt așezate elementele sale.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Inversează săgețile curbe de ruliu",
|
||||
"prefs.invert_curved_roll_arrows_note": "Cele două săgeți curbe din colțul cubului rotesc vizualizarea în planul ecranului. Activat: săgeata în sens orar rotește în sens antiorar, iar săgeata în sens antiorar rotește în sens orar. Dezactivat: fiecare săgeată rotește vizualizarea în direcția în care se curbează.",
|
||||
"prefs.invert_navigator_arrows": "Inversează săgețile de rotație ale cubului de vizualizare",
|
||||
"prefs.invert_navigator_arrows_note": "Săgețile din jurul cubului rotesc camera în jurul modelului în direcția în care indică, ca în Fusion și Inventor: săgeata din stânga indică spre dreapta și rotește camera spre dreapta. Dezactivat: o săgeată rotește cubul spre partea pe care se află, deci săgeata din stânga rotește camera spre stânga. Săgețile curbe de ruliu nu sunt afectate.",
|
||||
"prefs.keep_work_on_tab_switch": "Păstrează deschise dialogurile și instrumentele la schimbarea proiectului",
|
||||
"prefs.keep_work_on_tab_switch_note": "Fiecare filă de proiect își păstrează dialogul sau instrumentul deschis, cu ce ați ales și ați tastat în el, și fila ei din bara de instrumente. Le regăsiți când reveniți la proiect. Dezactivat: schimbarea proiectului le anulează și afișează fila Model.",
|
||||
"prefs.labels": "Etichete",
|
||||
@@ -2275,6 +2279,7 @@
|
||||
"prefs.navigation": "Navigare",
|
||||
"prefs.navigation_note": "Cum mută vizualizarea mouse-ul, trackpadul și mouse-ul 3D.",
|
||||
"prefs.navigator_fit": "Potrivește modelul după o schimbare de vizualizare din navigator",
|
||||
"prefs.navigator_fit_note": "O față, un colț sau o săgeată a cubului de vizualizare ori o axă a reperului de orientare privește și spre mijlocul modelului, de la singura distanță care îl cuprinde din toate părțile (sfera lui de încadrare). Acasă folosește același zoom și același centru cât timp opțiunea este activată, așa că vizualizările alese după aceea doar se rotesc. Dezactivat: vizualizarea se rotește pe loc, păstrându-și ținta și zoomul.",
|
||||
"prefs.network_host_remove": "Elimină",
|
||||
"prefs.network_locations": "Permite locațiile de rețea în proiecte",
|
||||
"prefs.network_locations_note": "Dezactivat: fișierele legate ale unui proiect aflate pe alt computer (\\\\server\\share) se deschid doar de pe serverele listate aici sau după ce permiteți unul, când vi se cere.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Создать Issue…",
|
||||
"Cross curves": "Поперечные кривые",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter — продолжить на GitHub.",
|
||||
"Cube": "Куб",
|
||||
"Current orthographic view (1:1 mm)": "Текущий ортогональный вид (1:1 мм)",
|
||||
"Current perspective view": "Текущий перспективный вид",
|
||||
"Current plane": "Текущая плоскость",
|
||||
@@ -2231,6 +2232,9 @@
|
||||
"prefs.interface_note": "Как выглядит SolidCraft и где расположены его части.",
|
||||
"prefs.interval_minutes": "{n} мин",
|
||||
"prefs.invert_curved_roll_arrows": "Инвертировать изогнутые стрелки крена",
|
||||
"prefs.invert_curved_roll_arrows_note": "Две изогнутые стрелки в углу куба поворачивают вид в плоскости экрана. Вкл.: стрелка по часовой стрелке поворачивает вид против часовой, а стрелка против часовой — по часовой. Выкл.: каждая стрелка поворачивает вид в ту сторону, куда она изогнута.",
|
||||
"prefs.invert_navigator_arrows": "Инвертировать стрелки вращения куба видов",
|
||||
"prefs.invert_navigator_arrows_note": "Стрелки вокруг куба вращают камеру в ту сторону, куда указывают, как в Fusion и Inventor: стрелка слева указывает вправо и поворачивает камеру вправо. Выкл.: стрелка поворачивает куб в сторону, где она расположена, поэтому левая стрелка поворачивает камеру влево. Изогнутые стрелки крена не затрагиваются.",
|
||||
"prefs.keep_work_on_tab_switch": "Сохранять открытые диалоги и инструменты при переключении между дизайнами",
|
||||
"prefs.keep_work_on_tab_switch_note": "Каждая вкладка дизайна сохраняет свой открытый диалог или инструмент вместе с тем, что вы в нём выбрали и ввели, а также свою вкладку панели инструментов. Они снова на месте, когда вы возвращаетесь к дизайну. Выкл.: при переключении дизайнов они отменяются, и показывается вкладка «Модель».",
|
||||
"prefs.labels": "Подписи",
|
||||
@@ -2267,6 +2271,7 @@
|
||||
"prefs.navigation": "Навигация",
|
||||
"prefs.navigation_note": "Как мышь, тачпад и 3D-мышь двигают вид.",
|
||||
"prefs.navigator_fit": "Вписывать модель после смены вида в навигаторе",
|
||||
"prefs.navigator_fit_note": "Грань, угол или стрелка куба видов либо ось гизмо также направляет взгляд на середину модели с единственного расстояния, с которого она целиком видна со всех сторон (её габаритная сфера). «Главная» при включённом параметре использует тот же масштаб и центр, поэтому выбранные после неё виды только поворачиваются. Выкл.: вид поворачивается на месте, сохраняя свою цель и масштаб.",
|
||||
"prefs.network_host_remove": "Убрать",
|
||||
"prefs.network_locations": "Разрешить сетевые расположения в дизайнах",
|
||||
"prefs.network_locations_note": "Выкл.: связанные файлы дизайна на другом компьютере (\\\\server\\share) открываются только с перечисленных здесь серверов или после того, как вы разрешите сервер по запросу.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Пријави проблем…",
|
||||
"Cross curves": "Пресечне криве",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter наставља на GitHub-у.",
|
||||
"Cube": "Коцка",
|
||||
"Current orthographic view (1:1 mm)": "Тренутни ортографски приказ (1:1 mm)",
|
||||
"Current perspective view": "Тренутни перспективни приказ",
|
||||
"Current plane": "Тренутна раван",
|
||||
@@ -2239,6 +2240,9 @@
|
||||
"prefs.interface_note": "Како SolidCraft изгледа и где су његови делови.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Обрни закривљене стрелице за ваљање",
|
||||
"prefs.invert_curved_roll_arrows_note": "Две закривљене стрелице у углу коцке ваљају приказ унутар равни екрана. Укључено: стрелица у смеру казаљке на сату ваља приказ супротно од смера казаљке на сату, а стрелица супротно од смера казаљке на сату ваља га у смеру казаљке на сату. Искључено: свака стрелица ваља приказ онако како је закривљена.",
|
||||
"prefs.invert_navigator_arrows": "Обрни стрелице за ротацију коцке приказа",
|
||||
"prefs.invert_navigator_arrows_note": "Стрелице око коцке орбитирају камеру у смеру у којем показују, као у програмима Fusion и Inventor: стрелица са леве стране показује удесно и закреће камеру удесно. Искључено: стрелица окреће коцку ка страни на којој се налази, па лева стрелица закреће камеру улево. Подешавање не утиче на закривљене стрелице за ваљање.",
|
||||
"prefs.keep_work_on_tab_switch": "Задржи дијалоге и алате отвореним при пребацивању између дизајна",
|
||||
"prefs.keep_work_on_tab_switch_note": "Свака картица дизајна задржава свој отворени дијалог или алат, са оним што сте у њему изабрали и уписали, те своју картицу траке са алатима. Када се вратите на дизајн, поново су ту. Искључено: пребацивање између дизајна отказује их и приказује картицу Модел.",
|
||||
"prefs.labels": "Ознаке",
|
||||
@@ -2275,6 +2279,7 @@
|
||||
"prefs.navigation": "Навигација",
|
||||
"prefs.navigation_note": "Како миш, додирна табла и 3D миш померају приказ.",
|
||||
"prefs.navigator_fit": "Уклопи модел након промене приказа у навигатору",
|
||||
"prefs.navigator_fit_note": "Лице, угао или стрелица коцке приказа, односно оса гизма оријентације, такође усмерава поглед на средину модела са једног растојања на којем се уклапа из сваког смера (његова гранична сфера). Почетни приказ користи исти зум и центар док је ова опција укључена, па се прикази које изаберете после њега само окрећу. Искључено: приказ се окреће на месту и задржава свој циљ и зум.",
|
||||
"prefs.network_host_remove": "Уклони",
|
||||
"prefs.network_locations": "Дозволи мрежне локације у дизајнима",
|
||||
"prefs.network_locations_note": "Искључено: повезане датотеке дизајна на другом рачунару (\\\\server\\share) отварају се само са сервера наведених овде или након што га дозволите када се то затражи.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Skapa ärende…",
|
||||
"Cross curves": "Korskurvor",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter fortsätter på GitHub.",
|
||||
"Cube": "Kub",
|
||||
"Current orthographic view (1:1 mm)": "Aktuell ortografisk vy (1:1 mm)",
|
||||
"Current perspective view": "Aktuell perspektivvy",
|
||||
"Current plane": "Aktuellt plan",
|
||||
@@ -2240,6 +2241,9 @@
|
||||
"prefs.interface_note": "Hur SolidCraft ser ut och var dess delar placeras.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_curved_roll_arrows": "Invertera de böjda rullningspilarna",
|
||||
"prefs.invert_curved_roll_arrows_note": "De två böjda pilarna vid kubens hörn rullar vyn i skärmplanet. På: medurspilen rullar moturs och moturspilen rullar medurs. Av: varje pil rullar vyn åt det håll den kröker sig.",
|
||||
"prefs.invert_navigator_arrows": "Invertera rotationspilarna på vykuben",
|
||||
"prefs.invert_navigator_arrows_note": "Pilarna runt kuben orbiterar kameran åt det håll de pekar, som i Fusion och Inventor: pilen till vänster pekar åt höger och svänger kameran åt höger. Av: en pil roterar kuben mot den sida den sitter på, så den vänstra pilen svänger kameran åt vänster. De böjda rullningspilarna påverkas inte.",
|
||||
"prefs.keep_work_on_tab_switch": "Behåll dialoger och verktyg öppna när du byter konstruktion",
|
||||
"prefs.keep_work_on_tab_switch_note": "Varje konstruktionsflik behåller sin öppna dialog eller sitt öppna verktyg, med det du har valt och skrivit i den, och sin flik i menyfliksområdet. De finns kvar när du kommer tillbaka till konstruktionen. Av: att byta konstruktion avbryter dem och visar fliken Modell.",
|
||||
"prefs.labels": "Etiketter",
|
||||
@@ -2276,6 +2280,7 @@
|
||||
"prefs.navigation": "Navigering",
|
||||
"prefs.navigation_note": "Hur musen, styrplattan och 3D-musen flyttar vyn.",
|
||||
"prefs.navigator_fit": "Anpassa modellen efter en vyändring i navigatorn",
|
||||
"prefs.navigator_fit_note": "En yta, ett hörn eller en pil på vykuben, eller en axel på orienteringsgizmot, tittar också mot modellens mitt från det enda avstånd som får den att rymmas från alla sidor (dess begränsningssfär). Hem använder samma zoom och centrum medan detta är på, så vyer som väljs efteråt bara vrids. Av: vyn vrids på stället och behåller sitt mål och sin zoom.",
|
||||
"prefs.network_host_remove": "Ta bort",
|
||||
"prefs.network_locations": "Tillåt nätverksplatser i konstruktioner",
|
||||
"prefs.network_locations_note": "Av: en konstruktions länkade filer på en annan dator (\\\\server\\share) öppnas bara från de servrar som listas här, eller efter att du tillåtit en när du tillfrågas.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Create Issue…": "Sorun Oluştur…",
|
||||
"Cross curves": "Çapraz eğriler",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter ile GitHub'da devam edilir.",
|
||||
"Cube": "Küp",
|
||||
"Current orthographic view (1:1 mm)": "Geçerli ortografik görünüm (1:1 mm)",
|
||||
"Current perspective view": "Geçerli perspektif görünüm",
|
||||
"Current plane": "Geçerli düzlem",
|
||||
@@ -2239,6 +2240,9 @@
|
||||
"prefs.interface_note": "SolidCraft'ın nasıl göründüğü ve parçalarının nerede durduğu.",
|
||||
"prefs.interval_minutes": "{n} dk",
|
||||
"prefs.invert_curved_roll_arrows": "Eğri yuvarlanma oklarını ters çevir",
|
||||
"prefs.invert_curved_roll_arrows_note": "Küpün köşesindeki iki eğri ok, görünümü ekran düzlemi içinde yuvarlar. Açık: saat yönündeki ok saat yönünün tersine, saat yönünün tersindeki ok saat yönünde yuvarlar. Kapalı: her ok görünümü kıvrıldığı yöne yuvarlar.",
|
||||
"prefs.invert_navigator_arrows": "Görünüm küpünün döndürme oklarını ters çevir",
|
||||
"prefs.invert_navigator_arrows_note": "Küpün çevresindeki oklar kamerayı gösterdikleri yönde döndürür, tıpkı Fusion ve Inventor'daki gibi: soldaki ok sağı gösterir ve kamerayı sağa savurur. Kapalı: bir ok küpü bulunduğu tarafa doğru döndürür, yani sol ok kamerayı sola savurur. Kıvrık yuvarlanma okları bundan etkilenmez.",
|
||||
"prefs.keep_work_on_tab_switch": "Tasarımlar arasında geçerken diyalogları ve araçları açık tut",
|
||||
"prefs.keep_work_on_tab_switch_note": "Her tasarım sekmesi, açık diyaloğunu veya aracını, içinde seçtiklerinizi ve yazdıklarınızı ve şerit sekmesini korur. Tasarıma döndüğünüzde yeniden oradadırlar. Kapalı: tasarımlar arasında geçmek bunları iptal eder ve Model sekmesini gösterir.",
|
||||
"prefs.labels": "Etiketler",
|
||||
@@ -2275,6 +2279,7 @@
|
||||
"prefs.navigation": "Gezinme",
|
||||
"prefs.navigation_note": "Farenin, trackpad'in ve 3B farenin görünümü nasıl hareket ettirdiği.",
|
||||
"prefs.navigator_fit": "Gezgin görünümü değiştikten sonra modeli sığdır",
|
||||
"prefs.navigator_fit_note": "Görünüm küpünün bir yüzü, köşesi veya oku ya da gizmonun bir ekseni, modelin ortasına da modeli her yönden sığdıran tek bir uzaklıktan (sınırlayıcı küresi) bakar. Bu açıkken Ana görünüm aynı yakınlaştırmayı ve merkezi kullanır; böylece sonrasında seçilen görünümler yalnızca döner. Kapalı: görünüm hedefini ve yakınlaştırmasını koruyarak yerinde döner.",
|
||||
"prefs.network_host_remove": "Kaldır",
|
||||
"prefs.network_locations": "Tasarımlarda ağ konumlarına izin ver",
|
||||
"prefs.network_locations_note": "Kapalı: bir tasarımın başka bir bilgisayardaki bağlı dosyaları (\\\\server\\paylaşım) yalnızca burada listelenen sunuculardan veya sorulduğunda izin verdiklerinizden açılır.",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "Створити повідомлення про проблему…",
|
||||
"Cross curves": "Поперечні криві",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter продовжує на GitHub.",
|
||||
"Cube": "Куб",
|
||||
"Current orthographic view (1:1 mm)": "Поточний ортографічний вид (1:1 mm)",
|
||||
"Current perspective view": "Поточний перспективний вид",
|
||||
"Current plane": "Поточна площина",
|
||||
@@ -2651,6 +2652,9 @@
|
||||
"prefs.interface_note": "Як виглядає SolidCraft і де розташовані його частини.",
|
||||
"prefs.interval_minutes": "{n} хв",
|
||||
"prefs.invert_curved_roll_arrows": "Інвертувати вигнуті стрілки обертання",
|
||||
"prefs.invert_curved_roll_arrows_note": "Дві вигнуті стрілки в куті куба обертають вид у площині екрана. Увімкнено: стрілка за годинниковою стрілкою обертає проти годинникової, а стрілка проти годинникової — за годинниковою. Вимкнено: кожна стрілка обертає вид у напрямку свого завитку.",
|
||||
"prefs.invert_navigator_arrows": "Інвертувати стрілки обертання куба виду",
|
||||
"prefs.invert_navigator_arrows_note": "Стрілки навколо куба обертають камеру в той бік, куди вказують, як у Fusion та Inventor: стрілка ліворуч вказує праворуч і повертає камеру праворуч. Вимк.: стрілка обертає куб у той бік, де вона розташована, тож ліва стрілка повертає камеру ліворуч. Вигнуті стрілки крену не змінюються.",
|
||||
"prefs.keep_unsaved_also_asks": "Усе одно запитувати під час закриття вкладки",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Коли це вимкнено, вкладка закривається без вікна браузера, щойно копія кожного незбереженого проєкту актуальна.",
|
||||
"prefs.keep_unsaved_in_browser": "Зберігати незбережену роботу в цьому браузері",
|
||||
@@ -2710,6 +2714,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Вимкнено: коли стрілки «Перемістити» тягнуть компонент, нічого під вказівником не підсвічується, що зберігає плавність великих збірок. Якщо показано переріз чи аналіз, підсвічування все одно працює, а деталь, яку тягнуть уздовж її з’єднань, ніколи його не вмикає.",
|
||||
"prefs.navigation": "Навігація",
|
||||
"prefs.navigation.axes_signed": "Осі",
|
||||
"prefs.navigation.cube_compact": "Куб",
|
||||
"prefs.navigation.device": "Пристрій",
|
||||
"prefs.navigation.free": "Вільне",
|
||||
"prefs.navigation.orbit_style": "Стиль обертання",
|
||||
@@ -2717,6 +2722,7 @@
|
||||
"prefs.navigation.turntable": "Поворотний стіл",
|
||||
"prefs.navigation_note": "Як миша, трекпад і 3D-миша рухають вид.",
|
||||
"prefs.navigator_fit": "Вміщувати модель після зміни виду в навігаторі",
|
||||
"prefs.navigator_fit_note": "Грань, кут або стрілка куба виду чи вісь гізмо також дивляться на середину моделі з єдиної відстані, з якої її видно цілком з усіх боків (її обмежувальна сфера). «Головний вид» за ввімкненого параметра використовує той самий масштаб і центр, тож види, вибрані після нього, лише повертаються. Вимк.: вид повертається на місці, зберігаючи свою ціль і масштаб.",
|
||||
"prefs.network_host_remove": "Вилучити",
|
||||
"prefs.network_locations": "Дозволяти мережеві розташування в проєктах",
|
||||
"prefs.network_locations_note": "Для організацій, які зберігають проєкти на файлових серверах. Вимкнено: лише дозволені вами сервери.",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "Tạo Issue…",
|
||||
"Cross curves": "Đường cong giao cắt",
|
||||
"Ctrl+Enter continues on GitHub.": "Nhấn Ctrl+Enter để tiếp tục trên GitHub.",
|
||||
"Cube": "Khối lập phương",
|
||||
"Current orthographic view (1:1 mm)": "Góc nhìn trực giao hiện tại (1:1 mm)",
|
||||
"Current perspective view": "Góc nhìn phối cảnh hiện tại",
|
||||
"Current plane": "Mặt phẳng hiện tại",
|
||||
@@ -2682,9 +2683,9 @@
|
||||
"prefs.interface_note": "Diện mạo của SolidCraft và cách bố trí các bảng điều khiển.",
|
||||
"prefs.interval_minutes": "{n} phút",
|
||||
"prefs.invert_curved_roll_arrows": "Đảo ngược mũi tên cuộn cong",
|
||||
"prefs.invert_curved_roll_arrows_note": "Hai mũi tên cong ở góc bộ điều hướng cuộn khung nhìn trong mặt phẳng màn hình. Khi bật: mũi tên theo chiều kim đồng hồ cuộn ngược chiều kim đồng hồ và ngược lại. Khi tắt: mỗi mũi tên cuộn khung nhìn theo chiều cong của nó.",
|
||||
"prefs.invert_curved_roll_arrows_note": "Hai mũi tên cong ở góc khối cuộn khung nhìn trong mặt phẳng màn hình. Khi bật: mũi tên theo chiều kim đồng hồ cuộn ngược chiều kim đồng hồ và ngược lại. Khi tắt: mỗi mũi tên cuộn khung nhìn theo chiều cong của nó.",
|
||||
"prefs.invert_navigator_arrows": "Đảo ngược mũi tên xoay của khối điều hướng",
|
||||
"prefs.invert_navigator_arrows_note": "Các mũi tên quanh bộ điều hướng xoay camera theo hướng mũi tên chỉ, như trong Fusion và Inventor: mũi tên bên trái chỉ sang phải và đưa camera sang phải. Khi tắt: mũi tên xoay khung nhìn về phía chứa nó, nên mũi tên bên trái đưa camera sang trái. Hai mũi tên cuộn cong không bị ảnh hưởng.",
|
||||
"prefs.invert_navigator_arrows_note": "Các mũi tên quanh khối xoay camera theo hướng mũi tên chỉ, như trong Fusion và Inventor: mũi tên bên trái chỉ sang phải và đưa camera sang phải. Khi tắt: mũi tên xoay khối về phía cạnh chứa nó, nên mũi tên bên trái đưa camera sang trái. Hai mũi tên cuộn cong không bị ảnh hưởng.",
|
||||
"prefs.keep_unsaved_also_asks": "Vẫn hỏi khi đóng thẻ",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Khi tắt, thẻ sẽ đóng mà không có hộp thoại của trình duyệt khi bản sao của mọi thiết kế chưa lưu đã được cập nhật.",
|
||||
"prefs.keep_unsaved_in_browser": "Giữ công việc chưa lưu trong trình duyệt này",
|
||||
@@ -2744,6 +2745,7 @@
|
||||
"prefs.move_drag_highlights_tip": "Tắt: khi các mũi tên của Di chuyển kéo một thành phần, không có gì dưới con trỏ được tô sáng, nhờ đó các cụm lắp ráp lớn vẫn mượt. Khi đang hiện mặt cắt hoặc phân tích thì vẫn tô sáng như thường, và chi tiết được kéo dọc theo khớp nối không bao giờ tô sáng.",
|
||||
"prefs.navigation": "Điều hướng góc nhìn",
|
||||
"prefs.navigation.axes_signed": "Trục",
|
||||
"prefs.navigation.cube_compact": "Khối lập phương",
|
||||
"prefs.navigation.device": "Thiết bị",
|
||||
"prefs.navigation.free": "Tự do",
|
||||
"prefs.navigation.orbit_style": "Kiểu xoay",
|
||||
@@ -2751,7 +2753,7 @@
|
||||
"prefs.navigation.turntable": "Bàn xoay",
|
||||
"prefs.navigation_note": "Cách chuột, bàn di cảm ứng và chuột 3D di chuyển góc nhìn.",
|
||||
"prefs.navigator_fit": "Khớp mô hình sau khi đổi góc nhìn bằng khối điều hướng",
|
||||
"prefs.navigator_fit_note": "Nhấp vào một mặt, góc hoặc mũi tên của bộ điều hướng, hoặc một trục của gizmo, cũng nhìn vào tâm mô hình từ một khoảng cách duy nhất chứa trọn nó từ mọi phía (khối cầu bao). Khi bật, góc nhìn chuẩn (Home) dùng cùng mức thu phóng và tâm đó, nên các góc nhìn chọn sau chỉ xoay. Tắt: góc nhìn xoay tại chỗ, giữ nguyên điểm nhìn và mức thu phóng.",
|
||||
"prefs.navigator_fit_note": "Nhấp vào một mặt, góc hoặc mũi tên của khối lập phương, hoặc một trục của gizmo, cũng nhìn vào tâm mô hình từ một khoảng cách duy nhất chứa trọn nó từ mọi phía (khối cầu bao). Khi bật, góc nhìn chuẩn (Home) dùng cùng mức thu phóng và tâm đó, nên các góc nhìn chọn sau chỉ xoay. Tắt: góc nhìn xoay tại chỗ, giữ nguyên điểm nhìn và mức thu phóng.",
|
||||
"prefs.network_host_remove": "Xóa",
|
||||
"prefs.network_locations": "Cho phép các vị trí mạng trong thiết kế",
|
||||
"prefs.network_locations_note": "Dành cho tổ chức lưu thiết kế trên máy chủ tệp. Khi tắt: chỉ các máy chủ bạn cho phép.",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "創建 Issue…",
|
||||
"Cross curves": "交叉曲線",
|
||||
"Ctrl+Enter continues on GitHub.": "按Ctrl+Enter在GitHub中繼續。",
|
||||
"Cube": "立方體",
|
||||
"Current orthographic view (1:1 mm)": "目前正交視圖(1:1毫米)",
|
||||
"Current perspective view": "目前透視視圖",
|
||||
"Current plane": "目前平面",
|
||||
@@ -2744,6 +2745,7 @@
|
||||
"prefs.move_drag_highlights_tip": "關閉:用移動的箭頭拖動組件時,不高亮指針下的對象,大型裝配體也能保持流暢。顯示剖切或分析時仍照常高亮;沿關節拖動零件時從不高亮。",
|
||||
"prefs.navigation": "導航",
|
||||
"prefs.navigation.axes_signed": "座標軸",
|
||||
"prefs.navigation.cube_compact": "立方體",
|
||||
"prefs.navigation.device": "設備",
|
||||
"prefs.navigation.free": "自由",
|
||||
"prefs.navigation.orbit_style": "旋轉方式",
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Create Issue…": "创建 Issue…",
|
||||
"Cross curves": "交叉曲线",
|
||||
"Ctrl+Enter continues on GitHub.": "按Ctrl+Enter在GitHub中继续。",
|
||||
"Cube": "立方体",
|
||||
"Current orthographic view (1:1 mm)": "当前正交视图(1:1毫米)",
|
||||
"Current perspective view": "当前透视视图",
|
||||
"Current plane": "当前平面",
|
||||
@@ -2682,9 +2683,9 @@
|
||||
"prefs.interface_note": "SolidCraft 的外观和面板布局。",
|
||||
"prefs.interval_minutes": "{n} 分钟",
|
||||
"prefs.invert_curved_roll_arrows": "反转弯曲的翻滚箭头",
|
||||
"prefs.invert_curved_roll_arrows_note": "导航器角落的两个弯曲箭头在屏幕平面内翻滚视图。开启:顺时针箭头逆时针翻滚,逆时针箭头顺时针翻滚。关闭:每个箭头按其弯曲方向翻滚视图。",
|
||||
"prefs.invert_navigator_arrows": "反转导航器的旋转箭头",
|
||||
"prefs.invert_navigator_arrows_note": "导航器周围的箭头按箭头所指的方向转动相机,与 Fusion 和 Inventor 相同:左侧箭头指向右,相机向右转动。关闭:箭头把视图转向它所在的那一侧,因此左侧箭头使相机向左转动。弯曲的翻滚箭头不受影响。",
|
||||
"prefs.invert_curved_roll_arrows_note": "立方体角落的两个弯曲箭头在屏幕平面内翻滚视图。开启:顺时针箭头逆时针翻滚,逆时针箭头顺时针翻滚。关闭:每个箭头按其弯曲方向翻滚视图。",
|
||||
"prefs.invert_navigator_arrows": "反转视图立方体的旋转箭头",
|
||||
"prefs.invert_navigator_arrows_note": "立方体周围的箭头按箭头所指的方向转动相机,与 Fusion 和 Inventor 相同:左侧箭头指向右,相机向右转动。关闭:箭头把立方体转向它所在的那一面,因此左侧箭头使相机向左转动。弯曲的翻滚箭头不受影响。",
|
||||
"prefs.keep_unsaved_also_asks": "关闭标签页时仍然询问",
|
||||
"prefs.keep_unsaved_also_asks_tip": "关闭此项后,只要每个未保存设计的副本都是最新的,标签页就会直接关闭,不显示浏览器的提示框。",
|
||||
"prefs.keep_unsaved_in_browser": "在此浏览器中保留未保存的工作",
|
||||
@@ -2744,6 +2745,7 @@
|
||||
"prefs.move_drag_highlights_tip": "关闭:用移动的箭头拖动组件时,不高亮指针下的对象,大型装配体也能保持流畅。显示剖切或分析时仍照常高亮;沿关节拖动零件时从不高亮。",
|
||||
"prefs.navigation": "导航",
|
||||
"prefs.navigation.axes_signed": "坐标轴",
|
||||
"prefs.navigation.cube_compact": "立方体",
|
||||
"prefs.navigation.device": "设备",
|
||||
"prefs.navigation.free": "自由",
|
||||
"prefs.navigation.orbit_style": "旋转方式",
|
||||
@@ -2751,7 +2753,7 @@
|
||||
"prefs.navigation.turntable": "转台",
|
||||
"prefs.navigation_note": "鼠标、触控板和3D鼠标如何移动视图。",
|
||||
"prefs.navigator_fit": "视图导航器切换视图后适配模型",
|
||||
"prefs.navigator_fit_note": "点击导航器的面、角或箭头,或坐标轴指示器的轴时,也会从各个方向都能容纳模型的同一距离(其包围球)注视模型中心。开启期间主视图使用相同的缩放和中心,因此之后选择的视图只改变方向。关闭:视图原地旋转,保持目标点和缩放。",
|
||||
"prefs.navigator_fit_note": "点击立方体的面、角或箭头,或坐标轴指示器的轴时,也会从各个方向都能容纳模型的同一距离(其包围球)注视模型中心。开启期间主视图使用相同的缩放和中心,因此之后选择的视图只改变方向。关闭:视图原地旋转,保持目标点和缩放。",
|
||||
"prefs.network_host_remove": "删除",
|
||||
"prefs.network_locations": "允许设计中的网络位置",
|
||||
"prefs.network_locations_note": "适用于将设计存放在文件服务器上的组织。关闭时:仅允许您授权的服务器。",
|
||||
|
||||
@@ -491,7 +491,7 @@ pub(crate) fn evaluate(doc: &Document, vals: &BTreeMap<String, Value>, f: &Featu
|
||||
.ok_or_else(|| invalid("cutting the member back for an inward end cap left nothing"))?;
|
||||
let cut = crate::lineage::Traced::step(cut, &[&mb.traced(), &tool]);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, cut, mb.feature);
|
||||
*slot = mb.remade(cut);
|
||||
}
|
||||
}
|
||||
let body = one(kernel::extrude(&p.plane, std::slice::from_ref(&p.region), p.cap.0, p.cap.1)?)?;
|
||||
|
||||
+17
-14
@@ -4710,7 +4710,7 @@ fn apply_traced_with(
|
||||
match acc {
|
||||
Some(b) => {
|
||||
if let Some(mb) = state.bodies.get_mut(ti) {
|
||||
*mb = ModelBody::traced_new(mb.name.clone(), b, mb.feature);
|
||||
*mb = mb.remade(b);
|
||||
}
|
||||
}
|
||||
None => remove.push(ti),
|
||||
@@ -4783,7 +4783,7 @@ fn join(state: &mut ModelState, tools: Vec<Traced>, target_idx: &[usize]) -> Res
|
||||
let operands: Vec<&Traced> = std::iter::once(&base).chain(&tools).collect();
|
||||
let acc = union_all(&operands)?;
|
||||
if let Some(mb) = state.bodies.iter_mut().find(|b| b.name == *keep) {
|
||||
*mb = ModelBody::traced_new(mb.name.clone(), acc, mb.feature);
|
||||
*mb = mb.remade(acc);
|
||||
}
|
||||
return Ok(vec![keep.clone()]);
|
||||
}
|
||||
@@ -4818,7 +4818,7 @@ fn join(state: &mut ModelState, tools: Vec<Traced>, target_idx: &[usize]) -> Res
|
||||
}
|
||||
let absorbed: std::collections::BTreeSet<String> = met.into_iter().skip(1).collect();
|
||||
if let Some(mb) = state.bodies.iter_mut().find(|b| b.name == keep) {
|
||||
*mb = ModelBody::traced_new(mb.name.clone(), acc, mb.feature);
|
||||
*mb = mb.remade(acc);
|
||||
}
|
||||
state.bodies.retain(|b| !absorbed.contains(&b.name));
|
||||
for t in state.threads.iter_mut().filter(|t| absorbed.contains(&t.body)) {
|
||||
@@ -5007,7 +5007,10 @@ fn named_pieces_of(
|
||||
(name, feature)
|
||||
};
|
||||
taken.insert(name.clone());
|
||||
out.push(ModelBody::new(name, p, made_by).with_lineage(lineage));
|
||||
// Each piece stays in the whole's component until the feature ends (#14305).
|
||||
let mut piece = ModelBody::new(name, p, made_by).with_lineage(lineage);
|
||||
piece.component = mb.component;
|
||||
out.push(piece);
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -8735,7 +8738,7 @@ fn replay(
|
||||
// A copy blends edges the pattern's own copies made: named after the body so far.
|
||||
let nb = mb.traced().then(nb);
|
||||
if let Some(slot) = st.bodies.get_mut(ti) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
*slot = mb.remade(nb);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8751,7 +8754,7 @@ fn replay(
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let nb = mb.traced().then(kernel::offset_faces(&mb.body, &moved, d)?);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
*slot = mb.remade(nb);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
@@ -9599,7 +9602,7 @@ fn eval_feature(
|
||||
// Certifying may join faces; their history stays over the same operands.
|
||||
let nb = Traced::step(nb.with_shell_certificate(f.id)?, &operands.iter().collect::<Vec<_>>());
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
*slot = mb.remade(nb);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -9610,7 +9613,7 @@ fn eval_feature(
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let nb = Traced::step(kernel::draft(&mb.body, &crate::plain(faces), &pl, pull.0, a)?, &[&mb.traced()]);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
*slot = mb.remade(nb);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -9744,7 +9747,7 @@ fn eval_feature(
|
||||
};
|
||||
let nb = Traced::step(nb, &[&mb.traced()]);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
*slot = mb.remade(nb);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -9781,7 +9784,7 @@ fn eval_feature(
|
||||
}
|
||||
let mut it = parts.into_iter();
|
||||
if let (Some(first), Some(slot)) = (it.next(), st.bodies.get_mut(i)) {
|
||||
*slot = ModelBody::traced_new(mb.name.clone(), first, mb.feature);
|
||||
*slot = mb.remade(first);
|
||||
}
|
||||
for (k, b) in it.enumerate() {
|
||||
// A piece renamed with `body.rename` keeps its own name (#3510).
|
||||
@@ -10109,7 +10112,7 @@ fn eval_feature(
|
||||
let i = st.bodies.iter().position(|b| &b.name == n).ok_or_else(|| DocError::Unknown(format!("body `{n}`")))?;
|
||||
if let Some(mb) = st.bodies.get_mut(i) {
|
||||
let nb = Traced::step(kernel::transform_matrix(&mb.body, m)?, &[&mb.traced()]);
|
||||
*mb = ModelBody::traced_new(mb.name.clone(), nb, mb.feature);
|
||||
*mb = mb.remade(nb);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -10120,7 +10123,7 @@ fn eval_feature(
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let nb = Traced::step(kernel::offset_faces(&mb.body, &crate::plain(faces), d)?, &[&mb.traced()]);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
*slot = mb.remade(nb);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -10139,7 +10142,7 @@ fn eval_feature(
|
||||
let left = if *heal { kernel::delete_faces(&mb.body, &all)? } else { kernel::remove_faces(&mb.body, &all)? };
|
||||
let nb = Traced::step(left, &[&mb.traced()]);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
*slot = mb.remade(nb);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -10182,7 +10185,7 @@ fn eval_feature(
|
||||
st.bodies.push(ModelBody::traced_new(name, traced, f.id));
|
||||
output += 1;
|
||||
} else if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name.clone(), Traced::step(nb, &[&mb.traced()]), mb.feature);
|
||||
*slot = mb.remade(Traced::step(nb, &[&mb.traced()]));
|
||||
}
|
||||
}
|
||||
if !*copy {
|
||||
|
||||
@@ -98,7 +98,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
}
|
||||
}
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, b, mb.feature);
|
||||
*slot = mb.remade(b);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2419,7 +2419,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
let edges = resolve_edges(&mb.body, &[position.0 + x * r0], &[], &mut warn)?;
|
||||
let nb = mb.traced().then(kernel::fillet(&mb.body, &edges, rf)?);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
*slot = mb.remade(nb);
|
||||
}
|
||||
}
|
||||
if let Some(hd) = hole_diameter {
|
||||
|
||||
@@ -122,7 +122,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
};
|
||||
let made = Traced::step(trimmed, &[&mb.traced()]);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, made, mb.feature);
|
||||
*slot = mb.remade(made);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -357,7 +357,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
match rest {
|
||||
Some(rest) => {
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name.clone(), source.then(rest), mb.feature);
|
||||
*slot = mb.remade(source.then(rest));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
@@ -375,7 +375,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let turned = mb.traced().then(kernel::reverse_faces(&mb.body, &crate::plain(faces))?);
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, turned, mb.feature);
|
||||
*slot = mb.remade(turned);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -551,7 +551,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
let made = mb.traced().then(kernel::untrim(&mb.body, fi, which, *outer, replace)?);
|
||||
if replace {
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, made, mb.feature);
|
||||
*slot = mb.remade(made);
|
||||
}
|
||||
} else {
|
||||
let name = new_name(st, f, 0);
|
||||
|
||||
@@ -960,7 +960,7 @@ fn cut_with_flanks(
|
||||
let nb = Traced { body: nb, lineage, recipe: None };
|
||||
let nb = if continuous_flanks { nb } else { chamfer(nb)? };
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
*slot = mb.remade(nb);
|
||||
}
|
||||
Ok(note)
|
||||
}
|
||||
|
||||
@@ -2081,6 +2081,27 @@ fn close_group(
|
||||
let bounds = limit_bounds(tree, &vars);
|
||||
// Closer than the conflict check needs: 1e-7 mm, and turns of 1e-10 rad across the loop.
|
||||
let tol = (1e-10 * scale).min(1e-7);
|
||||
let stored: Vec<f64> = vars.iter().map(|(id, i)| vals.get(id).and_then(|q| q.get(*i)).copied().unwrap_or(0.0)).collect();
|
||||
// A ball joint moves by a turn from where it starts, as in a reach (`BallTurn`), not by its
|
||||
// angles: at pitch 0, where a ball made as built starts, they can't tilt it about its x, and a
|
||||
// loop that needs that tilt can't move (#14852).
|
||||
let balls = reach::ball_turns(tree, &vars, &stored, &bounds);
|
||||
let (mut x0, mut free) = (stored, bounds.clone());
|
||||
for k in balls.iter().flat_map(|b| b.at..b.at + 3) {
|
||||
if let (Some(x), Some(b)) = (x0.get_mut(k), free.get_mut(k)) {
|
||||
(*x, *b) = (0.0, (f64::NEG_INFINITY, f64::INFINITY));
|
||||
}
|
||||
}
|
||||
// The values for the solve's `x`.
|
||||
let values = |x: &[f64]| -> Vec<f64> {
|
||||
let mut v = x.to_vec();
|
||||
for b in &balls {
|
||||
if let (Some(q), Some(slot)) = (b.values(x), v.get_mut(b.at..b.at + 3)) {
|
||||
slot.copy_from_slice(&q);
|
||||
}
|
||||
}
|
||||
v
|
||||
};
|
||||
// The values with the held ones `t` of the way along (and the unknowns at `x`).
|
||||
let place = |work: &mut Values, x: &[f64], t: f64| {
|
||||
for (id, i, from, to) in moves {
|
||||
@@ -2088,20 +2109,27 @@ fn close_group(
|
||||
*slot = from + (to - from) * t;
|
||||
}
|
||||
}
|
||||
for ((id, i), (v, (lo, hi))) in vars.iter().zip(x.iter().zip(&bounds)) {
|
||||
for ((id, i), (v, (lo, hi))) in vars.iter().zip(values(x).iter().zip(&bounds)) {
|
||||
if let Some(slot) = work.get_mut(id).and_then(|q| q.get_mut(*i)) {
|
||||
*slot = v.max(*lo).min(*hi);
|
||||
}
|
||||
}
|
||||
apply_links(asm, work);
|
||||
};
|
||||
let x0: Vec<f64> = vars.iter().map(|(id, i)| vals.get(id).and_then(|q| q.get(*i)).copied().unwrap_or(0.0)).collect();
|
||||
// Steps that move no value around the loops (links included) more than 5 degrees or a
|
||||
// twentieth of the loops' size.
|
||||
// How many such steps apart two sets of values are, over the loops' values.
|
||||
let stride = |a: &Values, b: &Values| -> f64 {
|
||||
let mut most = 0.0f64;
|
||||
for j in around.iter().filter_map(|id| tree.joints.get(id)) {
|
||||
if j.kind == JointKind::Ball {
|
||||
// A ball joint by how far it turns.
|
||||
let three = |v: &Values| v.get(&j.id).and_then(|q| Some([*q.first()?, *q.get(1)?, *q.get(2)?]));
|
||||
if let (Some(p), Some(q)) = (three(a), three(b)) {
|
||||
most = most.max(reach::ball_turn_between(p, q) / 5f64.to_radians());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for i in 0..j.kind.dofs().len() {
|
||||
let (Some(p), Some(q)) = (a.get(&j.id).and_then(|v| v.get(i)), b.get(&j.id).and_then(|v| v.get(i))) else { continue };
|
||||
let step = if j.kind.is_angle(i) { 5f64.to_radians() } else { 0.05 * scale };
|
||||
@@ -2144,7 +2172,7 @@ fn close_group(
|
||||
let mut x = x0;
|
||||
let start = largest(&gaps(&x, 0.0)?);
|
||||
let loose = loose_gap(scale);
|
||||
let tol = match close_within(&mut |x| gaps(x, 0.0), &mut x, &bounds, tol, loose)? {
|
||||
let tol = match close_within(&mut |x| gaps(x, 0.0), &mut x, &free, tol, loose)? {
|
||||
t if t > tol && moves.is_empty() && start <= t => return Some(BTreeMap::new()),
|
||||
t => t,
|
||||
};
|
||||
@@ -2158,7 +2186,7 @@ fn close_group(
|
||||
let mut trial = x.clone();
|
||||
// Each step as closed as the start, or within the conflict check where the loop's gap
|
||||
// grows as it moves (axes a hair out of parallel, #6211).
|
||||
if close_within(&mut |x| gaps(x, t1), &mut trial, &bounds, tol, loose).is_some() {
|
||||
if close_within(&mut |x| gaps(x, t1), &mut trial, &free, tol, loose).is_some() {
|
||||
// A step that moved some loop value too far (through a link and a limit, or close to
|
||||
// a dead point) is taken again in halves.
|
||||
let next = effective(&trial, t1);
|
||||
@@ -2179,7 +2207,7 @@ fn close_group(
|
||||
// The values that changed from what is stored.
|
||||
let moved = vars
|
||||
.iter()
|
||||
.zip(&x)
|
||||
.zip(&values(&x))
|
||||
.filter(|((id, i), v)| tree.joints.get(id).is_some_and(|j| j.values.get(*i).is_none_or(|s| (s - *v).abs() > 1e-12)))
|
||||
.map(|(var, v)| (*var, *v))
|
||||
.collect();
|
||||
|
||||
@@ -298,6 +298,12 @@ impl Quat {
|
||||
])
|
||||
}
|
||||
|
||||
/// The opposite turn.
|
||||
fn conj(self) -> Quat {
|
||||
let [w, x, y, z] = self.0;
|
||||
Quat([w, -x, -y, -z])
|
||||
}
|
||||
|
||||
/// The turn by the rotation vector `w`: |w| radians about its direction.
|
||||
fn exp([x, y, z]: [f64; 3]) -> Quat {
|
||||
let t = (x * x + y * y + z * z).sqrt();
|
||||
@@ -344,19 +350,26 @@ impl Quat {
|
||||
}
|
||||
}
|
||||
|
||||
/// How far (radians) a ball joint turns from the (yaw, pitch, roll) `a` to `b`: near pitch 0 a
|
||||
/// small turn changes its yaw and roll a lot.
|
||||
pub(super) fn ball_turn_between(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||
let [w, x, y, z] = Quat::zyz(a).conj().mul(Quat::zyz(b)).0;
|
||||
2.0 * (x * x + y * y + z * z).sqrt().atan2(w.abs())
|
||||
}
|
||||
|
||||
/// `t` moved by whole turns to lie nearest `near`.
|
||||
fn wrap(t: f64, near: f64) -> f64 {
|
||||
t + TAU * ((near - t) / TAU).round()
|
||||
}
|
||||
|
||||
/// A ball joint whose three values all move in a reach. It is solved over a small turn from where
|
||||
/// it starts (a rotation vector in its own frame, radians) instead of its Euler angles, which lock
|
||||
/// at pitch 0: there yaw and roll turn about the same axis, and nothing tilts it the other way
|
||||
/// (#12873).
|
||||
struct BallTurn {
|
||||
/// A ball joint whose three values all move in a reach or a loop solve. It is solved over a small
|
||||
/// turn from where it starts (a rotation vector in its own frame, radians) instead of its Euler
|
||||
/// angles, which lock at pitch 0: there yaw and roll turn about the same axis, and nothing tilts it
|
||||
/// the other way (#12873, #14852).
|
||||
pub(super) struct BallTurn {
|
||||
id: u64,
|
||||
/// Where its yaw is in the solve's values; pitch and roll follow.
|
||||
at: usize,
|
||||
pub(super) at: usize,
|
||||
from: Quat,
|
||||
start: [f64; 3],
|
||||
bounds: [(f64, f64); 3],
|
||||
@@ -365,7 +378,7 @@ struct BallTurn {
|
||||
impl BallTurn {
|
||||
/// Its (yaw, pitch, roll) for the turn in `x`, each held within its limits (pitch is the
|
||||
/// angle between the joint's z axes, so its limits hold it in a cone, its yaw still free).
|
||||
fn values(&self, x: &[f64]) -> Option<[f64; 3]> {
|
||||
pub(super) fn values(&self, x: &[f64]) -> Option<[f64; 3]> {
|
||||
let w = x.get(self.at..self.at + 3)?;
|
||||
let v = self.from.mul(Quat::exp([*w.first()?, *w.get(1)?, *w.get(2)?])).to_zyz(self.start);
|
||||
let mut out = [0.0; 3];
|
||||
@@ -377,7 +390,7 @@ impl BallTurn {
|
||||
}
|
||||
|
||||
/// The ball joints among `vars` with all three values there, in order.
|
||||
fn ball_turns(tree: &Tree, vars: &[(u64, usize)], start: &[f64], bounds: &[(f64, f64)]) -> Vec<BallTurn> {
|
||||
pub(super) fn ball_turns(tree: &Tree, vars: &[(u64, usize)], start: &[f64], bounds: &[(f64, f64)]) -> Vec<BallTurn> {
|
||||
vars.windows(3)
|
||||
.enumerate()
|
||||
.filter_map(|(at, w)| {
|
||||
@@ -633,6 +646,10 @@ mod tests {
|
||||
// A small turn off pitch 0 tilts it whichever way it points.
|
||||
let tilted = Quat::zyz([0.0; 3]).mul(Quat::exp([0.05, 0.0, 0.0])).to_zyz([0.0; 3]);
|
||||
assert!((tilted[1].abs() - 0.05).abs() < 1e-9, "{tilted:?}");
|
||||
// That tilt is a turn of 0.05, though its yaw and roll moved a quarter turn each; and a
|
||||
// turn is as far either way.
|
||||
assert!((ball_turn_between([0.0; 3], tilted) - 0.05).abs() < 1e-9, "{tilted:?}");
|
||||
assert!((ball_turn_between([0.3, 0.4, -1.2], [-2.0, -0.5, 0.7]) - ball_turn_between([-2.0, -0.5, 0.7], [0.3, 0.4, -1.2])).abs() < 1e-12);
|
||||
}
|
||||
|
||||
/// Exercise the IK callback itself: removing sharing at this call site must fail even if
|
||||
|
||||
@@ -562,6 +562,15 @@ impl crate::ModelBody {
|
||||
crate::ModelBody::new(name, made.body, feature).with_lineage(made.lineage)
|
||||
}
|
||||
|
||||
/// This body as the feature changed it: under its name, from the feature that made it and with
|
||||
/// its `component`, so a later step of the same feature still counts it among that
|
||||
/// component's bodies (the copies of a pattern's second cut, #14305).
|
||||
pub(crate) fn remade(&self, made: Traced) -> Self {
|
||||
let mut remade = Self::traced_new(self.name.clone(), made, self.feature);
|
||||
remade.component = self.component;
|
||||
remade
|
||||
}
|
||||
|
||||
/// This body with what it came from during the feature (`None`: not known).
|
||||
pub(crate) fn with_lineage(mut self, lineage: Option<Lineage>) -> Self {
|
||||
self.made = match lineage {
|
||||
|
||||
@@ -1667,3 +1667,62 @@ fn cut_that_leaves_a_closed_cavity_says_so() {
|
||||
.unwrap();
|
||||
assert!(!cup["warning"].as_str().unwrap_or_default().contains("closed cavity"), "{cup}");
|
||||
}
|
||||
|
||||
/// Issue #10: fuzzer seed 73. Extrude an arc-sided profile, fillet two top edges, then join a box.
|
||||
/// The join's boolean union used to be refused by solidcore's boolean check as "Bad Gap:
|
||||
/// Edge/Vertex gap across Edgeuse start larger than tol" (offset=7.7878382e-06,
|
||||
/// tolerance=7.5978919e-06). Every failing seed of the boolean fuzzer is a reproducible bug,
|
||||
/// fixed with a regression test that fails without the fix.
|
||||
/// Root cause not yet found — grows tolerances but gap may exceed platform-specific limits.
|
||||
#[ignore = "fails until the tolerance root cause for seed 73 is found (#10)"]
|
||||
#[test]
|
||||
fn fuzz_seed_73_boolean_join_builds() {
|
||||
let s = replay(include_str!("../../../examples/regressions/fuzz_seed_73_boolean_join.json"));
|
||||
let state = s.model.state();
|
||||
// Every step ran: every body is a sound solid, and the join produced one body.
|
||||
assert_eq!(state.bodies.len(), 1, "the join must leave one body: {:?}", state.bodies.iter().map(|b| &b.name).collect::<Vec<_>>());
|
||||
let b = &state.bodies[0];
|
||||
assert!(b.body.validity().is_empty(), "the joined body must be sound: {:?}", b.body.validity());
|
||||
let m = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-5)).unwrap().measure();
|
||||
assert!(m.volume > 0.0, "the union must be a real solid: {}", m.volume);
|
||||
println!("#10 seed 73: joined body volume {volume:.2} mm³", volume = m.volume);
|
||||
}
|
||||
|
||||
/// Issue #10: fuzzer seed 185. Box, join cylinder, cut cylinder, then rectangular pattern on the
|
||||
/// cut cylinder. The pattern's boolean result is refused as "SMLib's result is not a closed
|
||||
/// solid". Every failing seed of the boolean fuzzer is a reproducible bug, fixed with a
|
||||
/// regression test that fails without the fix.
|
||||
/// Root cause not yet found — pattern boolean result tolerance, topologically different from
|
||||
/// seed 73's "Bad Gap" boolean tolerance issue.
|
||||
#[ignore = "fails until the pattern boolean root cause for seed 185 is found (#10)"]
|
||||
#[test]
|
||||
fn fuzz_seed_185_pattern_builds() {
|
||||
let s = replay(include_str!("../../../examples/regressions/fuzz_seed_185_pattern.json"));
|
||||
let state = s.model.state();
|
||||
assert_eq!(state.bodies.len(), 1, "the pattern must leave one body: {:?}", state.bodies.iter().map(|b| &b.name).collect::<Vec<_>>());
|
||||
let b = &state.bodies[0];
|
||||
assert!(b.body.validity().is_empty(), "the pattern result must be sound: {:?}", b.body.validity());
|
||||
let m = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-5)).unwrap().measure();
|
||||
assert!(m.volume > 0.0, "the pattern result must be a real solid: {}", m.volume);
|
||||
println!("#10 seed 185: pattern result volume {volume:.2} mm³", volume = m.volume);
|
||||
}
|
||||
|
||||
/// Issue #10: fuzzer seed 178. Box, fillet, cut cylinder, cut box, shell, rectangular pattern,
|
||||
/// then fillet. The final fillet is refused as "SMLib fillet: SMLib's check refuses the result:
|
||||
/// Tolerance: Edge tolerance relative to brep tolerance (offset=0.083186985, tolerance=0.01)".
|
||||
/// Every failing seed of the boolean fuzzer is a reproducible bug, fixed with a regression test
|
||||
/// that fails without the fix.
|
||||
/// Root cause not yet found — fillet result tolerance, topologically different from seed 73's
|
||||
/// "Bad Gap" boolean tolerance issue.
|
||||
#[ignore = "fails until the fillet tolerance root cause for seed 178 is found (#10)"]
|
||||
#[test]
|
||||
fn fuzz_seed_178_fillet_builds() {
|
||||
let s = replay(include_str!("../../../examples/regressions/fuzz_seed_178_fillet.json"));
|
||||
let state = s.model.state();
|
||||
assert_eq!(state.bodies.len(), 1, "the fillet must leave one body: {:?}", state.bodies.iter().map(|b| &b.name).collect::<Vec<_>>());
|
||||
let b = &state.bodies[0];
|
||||
assert!(b.body.validity().is_empty(), "the filleted body must be sound: {:?}", b.body.validity());
|
||||
let m = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-5)).unwrap().measure();
|
||||
assert!(m.volume > 0.0, "the fillet result must be a real solid: {}", m.volume);
|
||||
println!("#10 seed 178: filleted body volume {volume:.2} mm³", volume = m.volume);
|
||||
}
|
||||
|
||||
@@ -4297,3 +4297,7 @@ mod corner_tests {
|
||||
#[cfg(test)]
|
||||
#[path = "fillet_connected_rounds_tests.rs"]
|
||||
mod fillet_connected_rounds_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "offset_chain_tangent_tests.rs"]
|
||||
mod offset_chain_tangent_tests;
|
||||
|
||||
@@ -1199,6 +1199,62 @@ fn a_locked_loop_does_not_move() {
|
||||
assert!(r["values"][0]["value"].as_f64().unwrap_or(f64::NAN).to_radians().abs() < 1e-3, "{r}");
|
||||
}
|
||||
|
||||
/// A spatial loop through a ball joint made as built, its values all 0 (#14852): a crank turning
|
||||
/// about Y at the hip, a rod on a ball at the crank's tip, a universal joint (two revolutes
|
||||
/// through a cross) at the rod's foot end, and the foot turning about X at the heel. As the crank
|
||||
/// starts to turn, the rod moves without turning, so the ball turns about the crank's y, which is
|
||||
/// the ball's x: its yaw, pitch and roll can't make that turn at pitch 0.
|
||||
fn ankle() -> Session {
|
||||
let mut s = Session::default();
|
||||
for (name, c) in [
|
||||
("Shank", [-1.0, -1.0, -5.0]),
|
||||
("Crank", [10.0, 9.0, 39.0]),
|
||||
("Rod", [19.0, 9.0, 19.0]),
|
||||
("Cross", [19.0, 9.0, -1.0]),
|
||||
("Foot", [5.0, -1.0, -1.0]),
|
||||
] {
|
||||
part(&mut s, name, c);
|
||||
}
|
||||
let g = occ(&s, "Shank");
|
||||
run(&mut s, "occurrence.ground", json!({"occurrence": g, "grounded": true}));
|
||||
pin(&mut s, "revolute", "Hip", "Shank", "Crank", Vec3::new(0.0, 10.0, 40.0), Vec3::Y);
|
||||
let tip = |o: &str| framed(o, [20.0, 10.0, 40.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]);
|
||||
let r = run(&mut s, "joint.create", json!({"type": "ball", "a": tip("Crank"), "b": tip("Rod"), "flip": true, "name": "Ball"}));
|
||||
assert_eq!(r["conflicts"].as_array().map(Vec::len), Some(0), "{r}");
|
||||
pin(&mut s, "revolute", "Ankle pitch", "Rod", "Cross", Vec3::new(20.0, 10.0, 0.0), Vec3::Y);
|
||||
pin(&mut s, "revolute", "Ankle roll", "Cross", "Foot", Vec3::new(20.0, 10.0, 0.0), Vec3::X);
|
||||
pin(&mut s, "revolute", "Heel", "Shank", "Foot", Vec3::ZERO, Vec3::X);
|
||||
s
|
||||
}
|
||||
|
||||
/// The ankle's crank drives both ways, and every joint of the loop stays met.
|
||||
#[test]
|
||||
fn a_loop_through_a_ball_joint_at_rest_drives() {
|
||||
let mut s = ankle();
|
||||
let (hip, tip, ankle) = (Vec3::new(0.0, 10.0, 40.0), Vec3::new(20.0, 10.0, 40.0), Vec3::new(20.0, 10.0, 0.0));
|
||||
for v in [5.0f64, 15.0, -10.0] {
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Hip", "value": v}));
|
||||
assert!(r["stopped_by_loop"].is_null() && r["conflicts"].as_array().map(Vec::len) == Some(0), "{v}°: {r}");
|
||||
assert!((r["values"][0]["value"].as_f64().unwrap_or(f64::NAN) - v).abs() < 1e-9, "{v}°: {r}");
|
||||
// The crank's tip turned v° about the hip's y, and the rod's ends are on the crank's tip
|
||||
// and on the foot's ankle, which turned about the heel's x.
|
||||
let at = place(&s, "Crank", tip);
|
||||
assert!(
|
||||
(at.dist(tip) - 40.0 * (v.to_radians() / 2.0).sin().abs()).abs() < 1e-6
|
||||
&& (at.dist(hip) - 20.0).abs() < 1e-6
|
||||
&& (at.y - 10.0).abs() < 1e-9,
|
||||
"{v}°: tip at {at:?}"
|
||||
);
|
||||
assert!(near(place(&s, "Rod", tip), at), "{v}°: {:?} vs {at:?}", place(&s, "Rod", tip));
|
||||
let foot = place(&s, "Foot", ankle);
|
||||
assert!(
|
||||
(foot.x - 20.0).abs() < 1e-9 && (foot.y.hypot(foot.z) - ankle.y.hypot(ankle.z)).abs() < 1e-6 && foot.dist(ankle) > 1e-3,
|
||||
"{v}°: ankle at {foot:?}"
|
||||
);
|
||||
assert!(near(place(&s, "Rod", ankle), foot) && near(place(&s, "Cross", ankle), foot), "{v}°: {:?} vs {foot:?}", place(&s, "Rod", ankle));
|
||||
}
|
||||
}
|
||||
|
||||
/// A link that turns the rocker 100 times as far as its dial: a small turn of the dial is a full
|
||||
/// turn of the rocker, which it can't make.
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#![allow(clippy::unwrap_used, clippy::panic)]
|
||||
//! #11597: offsetting a chain of an arc, a line and a spline.
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn run(s: &mut Session, command: &str, params: Value) -> Value {
|
||||
s.execute(command, ¶ms).unwrap_or_else(|error| panic!("{command} {params}: {error}"))
|
||||
}
|
||||
|
||||
/// An arc, a line and a control-point spline meeting end to end (the line leaves the arc's end
|
||||
/// and the spline leaves the line's end going the same way).
|
||||
fn chain() -> (Session, Vec<String>) {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
let mut ids = Vec::new();
|
||||
let mut id_of = |v: Value| ids.push(v["curves"][0].as_str().unwrap().to_string());
|
||||
id_of(run(
|
||||
&mut s,
|
||||
"sketch.arc.three_point",
|
||||
json!({"start": [27.057152168740334, 4.6392170815647855], "through": [29.1011788575, 6.683243770299466], "end": [27.057152168731445, 8.7272704590365]}),
|
||||
));
|
||||
id_of(run(
|
||||
&mut s,
|
||||
"sketch.line",
|
||||
json!({"points": [[27.057152168728745, 8.72727045903728], [3.3203869526962912, 8.727270459038918]], "connect": false}),
|
||||
));
|
||||
id_of(run(
|
||||
&mut s,
|
||||
"sketch.spline.control_point",
|
||||
json!({"connect": false, "points": [[3.320386952696254, 8.727270459038918], [1.8337408439045355, 8.7272704590], [1.2130077051, 10.6284037520], [1.6440729637, 13.3010122908], [3.3203869527, 16.7840253217], [6.0690326225, 19.6015032445], [9.8223501930, 21.9267287781], [14.9488315292, 22.8085245156], [16.7566174523, 22.8085245156]]}),
|
||||
));
|
||||
(s, ids)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offsetting_an_arc_line_and_spline_chain_works() {
|
||||
for (all, chain_on) in [(true, true), (true, false), (false, true)] {
|
||||
for distance in [1.0, 0.5, -1.0] {
|
||||
let (mut s, ids) = chain();
|
||||
let curves = if all { json!(ids) } else { json!([ids[0]]) };
|
||||
let r = s.execute("sketch.offset", &json!({"curves": curves, "distance": distance, "chain": chain_on}));
|
||||
assert!(r.is_ok(), "{curves} chain {chain_on} distance {distance}: {:?}", r.err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The 1 mm chain offset is the right curve, not just some curve: the chain's two ends are offset
|
||||
/// by exactly 1 mm. On main this fails with "offset source has a stationary tangent" (#11597).
|
||||
#[test]
|
||||
fn the_chain_offset_ends_lie_one_millimetre_from_the_source_ends() {
|
||||
let (mut s, ids) = chain();
|
||||
let r = run(&mut s, "sketch.offset", json!({"curves": ids, "distance": 1.0, "chain": true}));
|
||||
let curves = r["curves"].as_array().unwrap();
|
||||
assert_eq!(curves.len(), 1, "the chain offsets to one curve: {curves:?}");
|
||||
let sk = s.model.state().sketch(s.active_sketch.unwrap()).unwrap().sketch.clone();
|
||||
let ends: Vec<_> = curves
|
||||
.iter()
|
||||
.flat_map(|c| {
|
||||
let k = &sk.curves[sk.curve_index(c.as_str().unwrap()).unwrap()].kind;
|
||||
let (a, b) = k.ends().unwrap();
|
||||
[sk.points[a].pos, sk.points[b].pos]
|
||||
})
|
||||
.collect();
|
||||
let near = |x: f64, y: f64| ends.iter().any(|p| (((p.x - x).powi(2) + (p.y - y).powi(2)).sqrt() - 1.0).abs() < 1e-6);
|
||||
assert!(near(27.057152168740334, 4.6392170815647855), "the arc's free end: {ends:?}");
|
||||
assert!(near(16.7566174523, 22.8085245156), "the spline's free end: {ends:?}");
|
||||
}
|
||||
@@ -1048,6 +1048,80 @@ fn mirror_of_a_boss_and_a_cut_from_its_face_makes_both_copies() {
|
||||
assert!(rel(both, want) < 1e-6, "ring {ring} mm³, lid {lid} mm³, mirrored {both} mm³ (expected {want})");
|
||||
}
|
||||
|
||||
/// #14305: in a component, a pattern or mirror of two cuts makes the copies of both. The first
|
||||
/// cut's copies used to hand the plate back outside its component, so the second cut's copies
|
||||
/// found no body there ("there is no body to cut or intersect").
|
||||
#[test]
|
||||
fn copies_of_two_cuts_in_a_component_cut_its_body() {
|
||||
for (command, params, copies) in [
|
||||
("solid.pattern.circular", json!({"features": ["CutA", "CutB"], "axis": "Z", "count": 4, "angle": 360}), 3.0),
|
||||
("solid.mirror", json!({"features": ["CutA", "CutB"], "plane": "YZ"}), 1.0),
|
||||
] {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "component.create", json!({"name": "Plate"}));
|
||||
run(&mut s, "solid.box", json!({"length": 100, "width": 100, "height": 5, "corner": [-50, -50, 0]}));
|
||||
for (name, x, r) in [("A", 30, 3), ("B", 40, 2)] {
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": name}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [x, 0], "radius": r}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": name, "through_all": true, "operation": "cut", "name": format!("Cut{name}")}));
|
||||
}
|
||||
run(&mut s, command, params);
|
||||
// The plate and each instance's two bores, radius 3 and 2, through its 5 mm.
|
||||
let want = 100.0 * 100.0 * 5.0 - (1.0 + copies) * PI * (9.0 + 4.0) * 5.0;
|
||||
let v = volume(&mut s);
|
||||
assert!(rel(v, want) < 1e-6, "{command}: {v} mm³, expected {want}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #14305: a copied cut that splits the plate leaves its pieces in the component, so the copies of
|
||||
/// holes after it still find the piece each falls on. The slot's copy splits the left part of the
|
||||
/// plate in two, and one hole's copy lands on each piece.
|
||||
#[test]
|
||||
fn copies_of_a_cut_that_splits_the_body_in_a_component_leave_its_pieces_there() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "component.create", json!({"name": "Plate"}));
|
||||
run(&mut s, "solid.box", json!({"length": 100, "width": 100, "height": 5, "corner": [-50, -50, 0]}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Slot"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [20, -60], "p1": [24, 60]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Slot", "through_all": true, "operation": "cut", "name": "CutSlot"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "B"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [40, 0], "radius": 2}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [10, 0], "radius": 2}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "B", "through_all": true, "operation": "cut", "name": "CutB"}));
|
||||
run(&mut s, "solid.mirror", json!({"features": ["CutSlot", "CutB"], "plane": "YZ"}));
|
||||
// The plate less two 4 × 100 slots across it and four bores of radius 2, through its 5 mm.
|
||||
let want = 100.0 * 100.0 * 5.0 - 2.0 * 4.0 * 100.0 * 5.0 - 4.0 * PI * 4.0 * 5.0;
|
||||
let v = volume(&mut s);
|
||||
assert!(rel(v, want) < 1e-6, "{v} mm³, expected {want}");
|
||||
}
|
||||
|
||||
/// #14305: a chamfer among the copied features hands the disc back in its component, so a hole
|
||||
/// copied after it still finds the disc there, as it does at the design's root.
|
||||
#[test]
|
||||
fn copies_of_a_chamfer_and_a_later_hole_in_a_component_cut_its_body() {
|
||||
let pi = std::f64::consts::PI;
|
||||
// Per instance: the bore, π·3²·10, one ring of the chamfer's triangle (Pappus, as below) and
|
||||
// the pin hole, π·2²·10. The six instances do not meet.
|
||||
let instance = pi * 9.0 * 10.0 + 2.0 * pi * (3.0 + 0.5 / 3.0) * 0.125 + pi * 4.0 * 10.0;
|
||||
let want = pi * 900.0 * 10.0 - 6.0 * instance;
|
||||
for in_component in [false, true] {
|
||||
let mut s = Session::default();
|
||||
if in_component {
|
||||
run(&mut s, "component.create", json!({"name": "Part"}));
|
||||
}
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 30, "height": 10, "name": "Disc"}));
|
||||
run(&mut s, "solid.hole", json!({"position": [20, 0, 10], "diameter": 6, "name": "Bore"}));
|
||||
run(&mut s, "solid.chamfer", json!({"edges": [[23, 0, 10]], "distance": 0.5, "name": "TopChamfer"}));
|
||||
run(&mut s, "solid.hole", json!({"position": [0, 10, 10], "diameter": 4, "name": "Pin"}));
|
||||
run(&mut s, "solid.pattern.circular", json!({"features": ["Bore", "TopChamfer", "Pin"], "axis": "Z", "count": 6, "angle": 360}));
|
||||
let v = volume(&mut s);
|
||||
assert!((v - want).abs() < 4e-7 * want, "component {in_component}: {v} mm³, expected {want}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A disc with an off-centre hole whose top and bottom edges are chamfered.
|
||||
fn disc_with_chamfered_hole() -> Session {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -511,6 +511,28 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_saved_scan_with_properties_edges_and_faces_opens_again() {
|
||||
// The file's normals and other numbers, an edge and a face (#14572): a design file keeps
|
||||
// the points apart from them, so they were checked against no points and it didn't open.
|
||||
let d = dir("properties");
|
||||
let path = d.join("normals.ply");
|
||||
let text = "ply\nformat ascii 1.0\nelement vertex 3\nproperty float x\nproperty float y\nproperty float z\n\
|
||||
property float nx\nproperty float ny\nproperty float nz\nproperty int layer\nelement edge 1\nproperty int vertex1\n\
|
||||
property int vertex2\nelement face 1\nproperty list uchar int vertex_indices\nend_header\n\
|
||||
0 0 0 0 0 1 0\n1 0 0 0 0 1 1\n0 1 0 0 0 1 1\n0 1\n3 0 1 2\n";
|
||||
std::fs::write(&path, text).unwrap();
|
||||
let mut s = Session::default();
|
||||
s.execute("pointcloud.insert", &json!({"path": path})).unwrap();
|
||||
assert_eq!(s.doc.point_clouds[0].complex.columns.len(), 4, "nx, ny, nz and layer");
|
||||
let compact = solidcraft_io::write_design_container_with_compression(&s.doc, solidcraft_io::DesignCompression::Compact).unwrap();
|
||||
for file in [solidcraft_io::write_design_container(&s.doc).unwrap(), compact] {
|
||||
let opened = solidcraft_io::read_design(&file).unwrap();
|
||||
assert_eq!(opened.point_clouds, s.doc.point_clouds);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_large_scan_is_thinned_and_centred() {
|
||||
let d = dir("thin");
|
||||
|
||||
@@ -132,7 +132,15 @@ pub(super) fn add_geometry(sk: &mut Sketch, geom: &[Geom2], f: &dyn Fn(Vec2) ->
|
||||
Geom2::Ellipse { c, major, minor } => Some(CurveKind::Ellipse { c: pt(sk, *c)?, m: pt(sk, *major)?, r: minor * k }),
|
||||
Geom2::Spline { pts, control, degree } => {
|
||||
let mut idx = Vec::new();
|
||||
for p in pts.iter().take(solidcraft_sketch::MAX_SPLINE_POINTS) {
|
||||
// A spline past the cap keeps every n-th point and always its last, so a loop stays
|
||||
// closed (taking the first ones only cut its tail off, #11119).
|
||||
let cap = solidcraft_sketch::MAX_SPLINE_POINTS;
|
||||
let keep: Vec<&Vec2> = if pts.len() > cap {
|
||||
(0..cap).map(|k| &pts[(k * (pts.len() - 1) + (cap - 1) / 2) / (cap - 1)]).collect()
|
||||
} else {
|
||||
pts.iter().collect()
|
||||
};
|
||||
for p in keep {
|
||||
let i = pt(sk, *p)?;
|
||||
if idx.last() != Some(&i) {
|
||||
idx.push(i);
|
||||
@@ -653,6 +661,50 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// #11119: a closed SPLINE with more control points than a sketch spline holds keeps its
|
||||
/// whole loop, ends and all, so it still makes a profile; the tail was cut off before, which
|
||||
/// left an outline of open splines that cannot be extruded.
|
||||
#[test]
|
||||
fn a_closed_spline_with_too_many_control_points_still_closes() {
|
||||
let n = solidcraft_sketch::MAX_SPLINE_POINTS * 3;
|
||||
let mut points = String::new();
|
||||
for i in 0..=n {
|
||||
let a = std::f64::consts::TAU * (i % n) as f64 / n as f64;
|
||||
points.push_str(&format!(
|
||||
"10
|
||||
{}
|
||||
20
|
||||
{}
|
||||
",
|
||||
30.0 * a.cos(),
|
||||
30.0 * a.sin()
|
||||
));
|
||||
}
|
||||
let dxf = format!(
|
||||
"0
|
||||
SECTION
|
||||
2
|
||||
ENTITIES
|
||||
0
|
||||
SPLINE
|
||||
70
|
||||
11
|
||||
71
|
||||
3
|
||||
{points}0
|
||||
ENDSEC
|
||||
0
|
||||
EOF
|
||||
"
|
||||
);
|
||||
let mut s = Session::default();
|
||||
let r = s.execute("sketch.insert_dxf", &json!({"text": dxf})).unwrap();
|
||||
let sk = s.model.state().sketch(r["sketch"].as_u64().unwrap()).unwrap().clone();
|
||||
assert_eq!(sk.profiles.len(), 1, "the loop closes into one profile: {r}");
|
||||
let want = std::f64::consts::PI * 900.0;
|
||||
assert!((sk.profiles[0].area - want).abs() < want * 0.01, "{} vs {want}", sk.profiles[0].area);
|
||||
}
|
||||
|
||||
/// The sketch limit is process-wide: the tests that depend on it take turns.
|
||||
static SKETCH_LIMIT: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
|
||||
@@ -144,17 +144,22 @@ fn bad_thin_walls_are_refused_and_change_nothing() {
|
||||
assert!(e.contains(says), "{p}: {e}");
|
||||
assert_eq!(s.doc.to_json(), before);
|
||||
}
|
||||
// A wall thicker than an arc it follows bends.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Small"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 1}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let e = s.execute("solid.extrude", &json!({"sketch": "Small", "thin": true, "thickness": 3, "side": "inside", "distance": 5}));
|
||||
assert!(e.is_err_and(|e| e.to_string().contains("thicker")));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Small", "thin": true, "thickness": 3, "side": "outside", "distance": 5}));
|
||||
// Measured from the tessellation, within its 1e-4.
|
||||
let (v, exact) = (volume(&mut s), std::f64::consts::PI * (16.0 - 1.0) * 5.0);
|
||||
assert!((v - exact).abs() < 1e-4 * exact, "{v} vs {exact}");
|
||||
// A wall inside a circle thicker than its radius covers the whole disk (#7333); outside, the
|
||||
// wall is a ring. Each in its own session, so one solid does not count in the other's volume.
|
||||
let small = || {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Small"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 1}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
s
|
||||
};
|
||||
for (side, exact) in [("inside", std::f64::consts::PI * 5.0), ("outside", std::f64::consts::PI * (16.0 - 1.0) * 5.0)] {
|
||||
let mut s = small();
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Small", "thin": true, "thickness": 3, "side": side, "distance": 5}));
|
||||
// Measured from the tessellation, within its 1e-4.
|
||||
let v = volume(&mut s);
|
||||
assert!((v - exact).abs() < 1e-4 * exact, "{side}: {v} vs {exact}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Stateless camera commands shared by the view navigators and programmatic callers.
|
||||
//! Stateless camera commands shared by the navigation cube and programmatic callers.
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_geom::prelude::*;
|
||||
use solidcraft_viewport::{Camera, StandardView};
|
||||
@@ -83,7 +83,7 @@ CommandSpec::new("view.navigation_bar", "Navigation Bar", navigation_bar)
|
||||
CommandSpec::new("view.navigator", "Custom View Navigator", crate::checked!("view.navigator", navigator))
|
||||
.icon("nav_settings").noundo().doc_only()
|
||||
.contract(&NAVIGATOR_CONTRACT).output(output_schema_for::<NavigatorParams>)
|
||||
.params("style?: axes|custom; body?: body name|null; texture?: themed|modeled; arrows?, home?: bool → the navigator as set; the app's view navigator, never saved in the design; never opens a dialog (a toolbar start opens the Custom View Navigator panel)"),
|
||||
.params("style?: axes|cube|custom; body?: body name|null; texture?: themed|modeled; arrows?, home?: bool → the navigator as set; the app's view navigator, never saved in the design; never opens a dialog (a toolbar start opens the Custom View Navigator panel)"),
|
||||
CommandSpec::new("view.ribbon_tabs_only", "Ribbon Tabs Only", crate::checked!("view.ribbon_tabs_only", ribbon_tabs_only))
|
||||
.icon("chevron").noundo().doc_only().contract(&RIBBON_TABS_ONLY_CONTRACT).output(output_schema_for::<RibbonTabsOnly>)
|
||||
.params("on?: bool (omitted: switch) → on?; fold the ribbon to its tab row, a tab click shows its panels over the view (window setting)"),
|
||||
@@ -162,7 +162,7 @@ CommandSpec::new("view.previous_tab", "Previous Tab", crate::checked!("view.prev
|
||||
.icon("chevron_right").noundo().doc_only().key("Ctrl+Shift+Tab").contract(&TAB_STEP_CONTRACT).output(output_schema_for::<TabStep>)
|
||||
.params("no parameters → step: -1; the window shows the design tab to the left of the shown one (before the first, the last); never edits the design or opens a dialog")];
|
||||
|
||||
const STANDARD_PARAMS: &str = "camera?: Camera (default: current session camera or home view) → camera; turns to that standard view as the axes navigator does, keeping target, distance and projection";
|
||||
const STANDARD_PARAMS: &str = "camera?: Camera (default: current session camera or home view) → camera; turns to that standard view as the view cube does, keeping target, distance and projection";
|
||||
|
||||
/// The standard view a `view.front` … `view.isometric` command turns to.
|
||||
pub fn standard_view(id: &str) -> Option<StandardView> {
|
||||
@@ -562,7 +562,7 @@ fn navigate(_s: &mut Session, p: &Value) -> Result<Value> {
|
||||
} else {
|
||||
use std::f64::consts::FRAC_PI_2;
|
||||
let (r, u, b) = cam.basis();
|
||||
// Left, right, up and down turn the view a quarter turn (the navigator's arrows),
|
||||
// Left, right, up and down turn the view a quarter turn (the navigation cube's arrows),
|
||||
// or `degrees` (the arrow keys' 15° steps, #1541).
|
||||
let (sin, cos) = match p.get("degrees") {
|
||||
None => (1.0, 0.0),
|
||||
@@ -1038,7 +1038,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_direction_preserves_framing_without_editing_the_design() {
|
||||
fn every_cube_direction_preserves_framing_without_editing_the_design() {
|
||||
for up in [UpAxis::Z, UpAxis::Y] {
|
||||
let mut s = Session::default();
|
||||
let before = s.doc.to_json();
|
||||
|
||||
@@ -98,7 +98,7 @@ pub struct AngleReadoutResult {
|
||||
}
|
||||
|
||||
/// Turn the view to the command's standard orientation (front, back, left, right, top, bottom
|
||||
/// or isometric), as the axes navigator does, keeping the camera's target, distance, projection and
|
||||
/// or isometric), as the view cube does, keeping the camera's target, distance, projection and
|
||||
/// up axis. Display only: the design isn't changed and nothing is added to undo.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
@@ -275,25 +275,25 @@ pub struct RenderFrame {
|
||||
#[derive(Debug, Default, Deserialize, Serialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NavigatorParams {
|
||||
/// The navigator: signed axes, or a custom body (the signed axes until a body is
|
||||
/// The navigator: signed axes, the cube, or a custom body (the cube until a body is
|
||||
/// chosen). Default: unchanged.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub style: Option<NavigatorStyle>,
|
||||
/// A body of the open design for the custom navigator to draw; the app keeps a snapshot of
|
||||
/// it, so it stays when the design closes. `null`: no body (the signed axes again). A body over
|
||||
/// it, so it stays when the design closes. `null`: no body (the cube again). A body over
|
||||
/// 10,000 triangles, or whose thinnest width is under a fifth of its widest, is refused.
|
||||
/// Default: unchanged.
|
||||
#[serde(default, deserialize_with = "nullable", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
pub body: Option<Option<String>>,
|
||||
/// The custom navigator's colours: the theme's (`themed`) or the body's own appearance and
|
||||
/// The custom navigator's colours: the cube's (`themed`) or the body's own appearance and
|
||||
/// edge colour (`modeled`). Default: unchanged.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub texture: Option<NavigatorTexture>,
|
||||
/// The orbit and roll arrows around the custom navigator. Default: unchanged.
|
||||
/// The cube's orbit and roll arrows around the custom navigator. Default: unchanged.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub arrows: Option<bool>,
|
||||
/// The Home button beside the custom navigator. Default: unchanged.
|
||||
/// The cube's Home button beside the custom navigator. Default: unchanged.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub home: Option<bool>,
|
||||
}
|
||||
@@ -303,6 +303,7 @@ pub struct NavigatorParams {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NavigatorStyle {
|
||||
Axes,
|
||||
Cube,
|
||||
Custom,
|
||||
}
|
||||
|
||||
|
||||
+81
-1
@@ -460,6 +460,15 @@ fn encode_cloud(cloud: &solidcraft_doc::point_cloud::PointCloud) -> Result<Vec<u
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Each point cloud's `complex` (segments, triangles and properties), taken out of the metadata
|
||||
/// with the cloud's index.
|
||||
fn take_cloud_complexes(metadata: &mut Value) -> Vec<(usize, Value)> {
|
||||
let Some(clouds) = metadata.get_mut("point_clouds").and_then(Value::as_array_mut) else {
|
||||
return Vec::new();
|
||||
};
|
||||
clouds.iter_mut().enumerate().filter_map(|(index, cloud)| Some((index, cloud.as_object_mut()?.remove("complex")?))).collect()
|
||||
}
|
||||
|
||||
fn decode_cloud(bytes: &[u8], cloud: &mut solidcraft_doc::point_cloud::PointCloud) -> Result<()> {
|
||||
use solidcraft_doc::point_cloud::{Colors, MAX_POINTS, Points};
|
||||
if bytes.get(..8) != Some(POINTS_MAGIC.as_slice()) {
|
||||
@@ -943,8 +952,13 @@ fn read_document<R: Read + Seek>(reader: &mut PackageReader<R>) -> Result<Docume
|
||||
let hdr = hydrate_hdr_resources(&mut metadata, reader)?;
|
||||
hydrate_texture_resources(&mut metadata, reader)? || hdr
|
||||
};
|
||||
// A cloud's points are in its blob, so its segments, triangles and properties are read and
|
||||
// checked with them below (`decode_cloud`), not against the empty points the metadata holds
|
||||
// (#14572). The metadata keeps them for what a newer build's file holds. A newer build's file
|
||||
// reads them as before: what doesn't read here is kept whole (`newer_file`), unknown fields too.
|
||||
let complexes = if newer { Vec::new() } else { take_cloud_complexes(&mut metadata) };
|
||||
let hydrated_text;
|
||||
let text = if hydrated {
|
||||
let text = if hydrated || !complexes.is_empty() {
|
||||
hydrated_text = serde_json::to_string(&metadata).map_err(invalid)?;
|
||||
&hydrated_text
|
||||
} else {
|
||||
@@ -953,6 +967,14 @@ fn read_document<R: Read + Seek>(reader: &mut PackageReader<R>) -> Result<Docume
|
||||
// Migrations and all document validation run on the small parametric metadata.
|
||||
// Binary arrays have their own equivalent numeric/index validation below.
|
||||
let mut doc = Document::from_json(text)?;
|
||||
for (index, complex) in complexes {
|
||||
if let Some(cloud) = doc.point_clouds.get_mut(index) {
|
||||
cloud.complex = serde::Deserialize::deserialize(&complex).map_err(invalid)?;
|
||||
}
|
||||
if let Some(cloud) = metadata.get_mut("point_clouds").and_then(|v| v.get_mut(index)).and_then(Value::as_object_mut) {
|
||||
cloud.insert("complex".into(), complex);
|
||||
}
|
||||
}
|
||||
if newer_container && doc.newer_file.is_none() {
|
||||
let current = solidcraft_doc::format::FORMAT;
|
||||
let says = container.get("format").and_then(Value::as_str).unwrap_or("a later container").to_string();
|
||||
@@ -2044,6 +2066,64 @@ mod tests {
|
||||
assert_eq!(parts(&write_design_container(&opened).unwrap())[&renamed], encoded);
|
||||
}
|
||||
|
||||
/// A newer build's point cloud with numbers, an edge and a face (#14572): a field this build
|
||||
/// doesn't know inside them, or numbers it can't read at all, still open and save back.
|
||||
#[test]
|
||||
fn a_newer_builds_point_cloud_numbers_open_and_save_back_whole() {
|
||||
use solidcraft_doc::point_cloud::{ColorBy, Colors, Column, Complex, Floats, Indices, On, PointCloud, Points};
|
||||
let mut doc = Document::new("Scan");
|
||||
doc.point_clouds.push(PointCloud {
|
||||
id: 7,
|
||||
name: "Scan".into(),
|
||||
file: "scan.ply".into(),
|
||||
origin: solidcore_record::Vec3::new(0.0, 0.0, 0.0),
|
||||
points: Points::new(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0].into()),
|
||||
colors: Colors::default(),
|
||||
source_points: 3,
|
||||
units: "mm".into(),
|
||||
transform: solidcraft_doc::IDENTITY,
|
||||
point_size: 2.0,
|
||||
color_by: ColorBy::File,
|
||||
color: None,
|
||||
height_range: None,
|
||||
visible: true,
|
||||
complex: Complex {
|
||||
segments: Indices(vec![0, 1].into()),
|
||||
triangles: Indices(vec![0, 1, 2].into()),
|
||||
columns: vec![Column { name: "nz".into(), on: On::Point, integer: false, values: Floats(vec![1.0, 1.0, 1.0].into()) }],
|
||||
names: Vec::new(),
|
||||
},
|
||||
color_property: None,
|
||||
filters: Vec::new(),
|
||||
source: String::new(),
|
||||
max_points: None,
|
||||
});
|
||||
// This build reads its own file whole.
|
||||
let file = write_design_container(&doc).unwrap();
|
||||
assert_eq!(crate::read_design(&file).unwrap().point_clouds, doc.point_clouds);
|
||||
|
||||
let newer = solidcraft_doc::format::format_string(solidcraft_doc::format::FORMAT + 1);
|
||||
for complex in [json!("future curvature"), json!("columns this build can't read")] {
|
||||
let mut entries = parts(&file);
|
||||
let mut metadata: Value = serde_json::from_slice(&entries["document.json"]).unwrap();
|
||||
metadata["format"] = json!(newer);
|
||||
if complex == json!("future curvature") {
|
||||
metadata["point_clouds"][0]["complex"]["curvature"] = json!([0.5, 0.25, 0.0]);
|
||||
} else {
|
||||
metadata["point_clouds"][0]["complex"]["columns"] = json!({"nz": "packed"});
|
||||
}
|
||||
let original = metadata["point_clouds"][0]["complex"].clone();
|
||||
entries.insert("document.json".into(), serde_json::to_vec(&metadata).unwrap());
|
||||
|
||||
let opened = crate::read_design(&package(&entries)).unwrap_or_else(|e| panic!("{complex}: {e}"));
|
||||
let resaved = parts(&write_design_container(&opened).unwrap());
|
||||
let again: Value = serde_json::from_slice(&resaved["document.json"]).unwrap();
|
||||
assert_eq!(again["point_clouds"][0]["complex"], original, "{complex}");
|
||||
let blob = metadata["point_clouds"][0]["points_blob"].as_str().unwrap();
|
||||
assert_eq!(resaved[blob], entries[blob], "{complex}");
|
||||
}
|
||||
}
|
||||
|
||||
fn parts(bytes: &[u8]) -> BTreeMap<String, Vec<u8>> {
|
||||
let mut zip = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
|
||||
(0..zip.len())
|
||||
|
||||
@@ -100,11 +100,19 @@ fn parabola_piece_volume() -> f64 {
|
||||
+ (0.4 * q.powf(2.5) - 2.0 / 3.0 * c * c * q.powf(1.5)) / (4.0 * c * c)
|
||||
}
|
||||
|
||||
/// The conic edge's pcurve on the cone is fitted: between its knots its image lies up to 4.9e-7
|
||||
/// mm off the edge, nearly the 5e-7 mm zone of the file's 1e-6, so the edge's tolerance covers
|
||||
/// it. The gap is the reader's, not the file's, so no note (#12757); SMLib's check passes the body.
|
||||
fn covers_the_conics_pcurve(imp: &StepImport) {
|
||||
assert!(imp.warnings.is_empty(), "{:?}", imp.warnings);
|
||||
assert_eq!(imp.bodies.len(), 1);
|
||||
crate::validation::check_kept(&imp.bodies[0].body).unwrap_or_else(|e| panic!("{e}"));
|
||||
}
|
||||
|
||||
fn import_piece(text: &str, want: f64) -> StepImport {
|
||||
assert_eq!(step_orientation_errors(text).unwrap(), Vec::<String>::new());
|
||||
let imp = step_import(text).unwrap();
|
||||
assert!(imp.warnings.is_empty(), "{:?}", imp.warnings);
|
||||
assert_eq!(imp.bodies.len(), 1);
|
||||
covers_the_conics_pcurve(&imp);
|
||||
assert!(imp.bodies[0].closed);
|
||||
let m = measure(&imp.bodies[0].body).unwrap();
|
||||
assert_eq!(m.faces, 3);
|
||||
@@ -118,8 +126,7 @@ fn round_trip(imp: &StepImport, want: f64) {
|
||||
let text = step_export(&[&imp.bodies[0].body], "conic").unwrap();
|
||||
step_validate(&text).unwrap();
|
||||
let back = step_import(&text).unwrap();
|
||||
assert!(back.warnings.is_empty(), "{:?}", back.warnings);
|
||||
assert_eq!(back.bodies.len(), 1);
|
||||
covers_the_conics_pcurve(&back);
|
||||
assert!(back.bodies[0].closed);
|
||||
let v = measure(&back.bodies[0].body).unwrap().volume;
|
||||
assert!(rel(v, want) < 1e-4, "round trip volume {v} vs {want}");
|
||||
|
||||
@@ -1493,7 +1493,10 @@ fn nearly_round_spindle_torus_pin_end_in_halves_imports_whole() {
|
||||
t += &context(70, "(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))").replace("LENGTH_MEASURE(1.E-6)", "LENGTH_MEASURE(1.E-5)");
|
||||
t += TAIL;
|
||||
let (volume, area) = pin_with_cylinder(major, minor);
|
||||
assert_closed_measures(&t, volume, area, &[]);
|
||||
// The apex lies 3.6e-5 mm off the pole, past the file's 1e-5: the body's tolerance covers it,
|
||||
// as SMLib checks a vertex's gap from its faces against the body's.
|
||||
let note = "solid 'Pin': the file states its geometry within 0.00001 mm, but its edges and vertices need tolerances up to 2.174e-4 mm and its vertices lie up to 3.623e-5 mm off their faces: the body's tolerance is 7.246e-5 mm";
|
||||
assert_closed_measures(&t, volume, area, &[note]);
|
||||
}
|
||||
|
||||
/// The lemon of a spindle torus (the tube's part across the axis: a closed surface with a pole
|
||||
@@ -2041,7 +2044,10 @@ fn extruded_ellipse_patch_crossing_profile_seam_stays_trimmed() {
|
||||
);
|
||||
assert!(text.contains(&old));
|
||||
let text = text.replacen(&old, &surface, 1);
|
||||
let imported = step_import(&text).unwrap();
|
||||
let mut imported = step_import(&text).unwrap();
|
||||
// The ellipse's edges lie up to 6e-4 mm off the patch, past the zone the file's 1e-3
|
||||
// gives them in SMLib, so their tolerances are raised; that note is not this test's.
|
||||
imported.warnings.retain(|w| !w.contains("edges lie off their faces' surfaces"));
|
||||
// Against its loop (same_sense false) the face is turned over, and the import says so.
|
||||
let open = "solid 'Strip': imported as an open body (the shell is not closed)";
|
||||
let turned = "solid 'Strip': 1 faces turned over: their loops disagree with their same_sense";
|
||||
@@ -2180,9 +2186,22 @@ fn dome_with_edges_raised(dz: f64) -> String {
|
||||
patch_with_edges_raised(dz, false)
|
||||
}
|
||||
|
||||
/// [`dome_with_edges_raised`] with only the edges' inner control points raised: the vertices lie
|
||||
/// on the dome's corners and each edge bows off it between them, up to `0.75 dz` at its middle,
|
||||
/// as an intersection curve written looser than its file states does.
|
||||
fn dome_with_edges_bowed(dz: f64) -> String {
|
||||
patch_with_edges(dz, false, false)
|
||||
}
|
||||
|
||||
/// [`dome_with_edges_raised`], or with `flat` the plane z = 0 in its place: its edges' pcurves
|
||||
/// are then their control points' projections onto it, in closed form.
|
||||
fn patch_with_edges_raised(dz: f64, flat: bool) -> String {
|
||||
patch_with_edges(dz, flat, true)
|
||||
}
|
||||
|
||||
/// [`patch_with_edges_raised`]; with `ends` false the vertices and the edges' end control points
|
||||
/// stay on the patch.
|
||||
fn patch_with_edges(dz: f64, flat: bool, ends: bool) -> String {
|
||||
let z = |i: usize, j: usize| if !flat && (1..=2).contains(&i) && (1..=2).contains(&j) { 6.0 } else { 0.0 };
|
||||
let mut t = String::from(HEAD);
|
||||
let mut rows = Vec::new();
|
||||
@@ -2205,12 +2224,13 @@ fn patch_with_edges_raised(dz: f64, flat: bool) -> String {
|
||||
// i = 3, then back; each side's control points are the patch's, raised.
|
||||
let sides: [[(usize, usize); 4]; 4] =
|
||||
[[(0, 0), (1, 0), (2, 0), (3, 0)], [(3, 0), (3, 1), (3, 2), (3, 3)], [(3, 3), (2, 3), (1, 3), (0, 3)], [(0, 3), (0, 2), (0, 1), (0, 0)]];
|
||||
let p = |(i, j): (usize, usize)| format!("({}.,{}.,{})", i * 10, j * 10, z(i, j) + dz);
|
||||
let p = |(i, j): (usize, usize), dz: f64| format!("({}.,{}.,{})", i * 10, j * 10, z(i, j) + dz);
|
||||
let end_dz = if ends { dz } else { 0.0 };
|
||||
for (k, side) in sides.iter().enumerate() {
|
||||
t += &format!("#{}=CARTESIAN_POINT('',{});\n#{}=VERTEX_POINT('',#{});\n", 200 + k, p(side[0]), 210 + k, 200 + k);
|
||||
t += &format!("#{}=CARTESIAN_POINT('',{});\n#{}=VERTEX_POINT('',#{});\n", 200 + k, p(side[0], end_dz), 210 + k, 200 + k);
|
||||
let pts: Vec<String> = (0..4).map(|m| format!("#{}", 300 + k * 4 + m)).collect();
|
||||
for (m, &q) in side.iter().enumerate() {
|
||||
t += &format!("#{}=CARTESIAN_POINT('',{});\n", 300 + k * 4 + m, p(q));
|
||||
t += &format!("#{}=CARTESIAN_POINT('',{});\n", 300 + k * 4 + m, p(q, if m == 0 || m == 3 { end_dz } else { dz }));
|
||||
}
|
||||
t += &format!("#{}=B_SPLINE_CURVE_WITH_KNOTS('',3,({}),.UNSPECIFIED.,.F.,.F.,(4,4),(0.,1.),.UNSPECIFIED.);\n", 400 + k, pts.join(","));
|
||||
}
|
||||
@@ -2241,6 +2261,16 @@ fn edges_written_off_their_spline_face_carry_their_gap_as_their_tolerance() {
|
||||
assert!(imp.warnings.iter().any(|w| w.contains("4 edges lie off their faces' surfaces")), "{:?}", imp.warnings);
|
||||
let r = imp.bodies[0].body.record();
|
||||
assert!(r.edges.iter().all(|e| e.tolerance >= 2e-4 && e.tolerance < 1e-3), "{:?}", r.edges.iter().map(|e| e.tolerance).collect::<Vec<_>>());
|
||||
// A record tolerance is SMLib's intersection tolerance, twice the zone a gap must lie within,
|
||||
// and SMLib checks a vertex's gaps against half its zone (#11997): with its vertices on the
|
||||
// dome's corners and its edges bowed off between them, the body checks there (at 1.5 times
|
||||
// the gap SMLib refused it, "Bad Gap"). Edges up to 1e-4 off keep within SMLib's 1000:1 to
|
||||
// the body's 1e-6; vertices off their faces, or edges farther off, need the body's tolerance
|
||||
// raised.
|
||||
for dz in [2e-5, 1e-4] {
|
||||
let imp = step_import(&dome_with_edges_bowed(dz)).unwrap();
|
||||
crate::validation::check_kept(&imp.bodies[0].body).unwrap_or_else(|e| panic!("edges {dz} mm off: {e}"));
|
||||
}
|
||||
// Far off (1.5 mm) is refused, naming the gap.
|
||||
let far = step_import(&dome_with_edges_raised(1.5));
|
||||
let refused = match &far {
|
||||
@@ -2250,6 +2280,110 @@ fn edges_written_off_their_spline_face_carry_their_gap_as_their_tolerance() {
|
||||
assert!(refused, "{far:?}");
|
||||
}
|
||||
|
||||
/// A file whose edges or vertices lie farther off their faces than its stated uncertainty allows
|
||||
/// SMLib (#11997: a body written with 1e-6 whose intersection edges lie 5.6e-4 off a spline face).
|
||||
/// SMLib checks each vertex's gap from its faces against the body's tolerance, and takes no edge
|
||||
/// or vertex zone of 1000 times the body's: such a body was refused before any operation. The
|
||||
/// body's tolerance now covers both, and the body checks.
|
||||
#[test]
|
||||
fn edges_farther_off_than_the_stated_uncertainty_allows_raise_the_bodys_tolerance() {
|
||||
let tolerances = |imp: &crate::step_in::StepImport| {
|
||||
let r = imp.bodies[0].body.record();
|
||||
let widest = r.edges.iter().map(|e| e.tolerance).chain(r.vertices.iter().map(|v| v.tolerance)).fold(0.0, f64::max);
|
||||
(r.tolerance, widest)
|
||||
};
|
||||
// Edges bowed up to 3.75e-4 mm off between vertices on the dome, 375 times the file's 1e-6:
|
||||
// the body's tolerance keeps the widest edge or vertex within SMLib's ratio, no more.
|
||||
let imp = step_import(&dome_with_edges_bowed(5e-4)).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
|
||||
crate::validation::check_kept(&imp.bodies[0].body).unwrap();
|
||||
let (body, widest) = tolerances(&imp);
|
||||
assert!(body >= widest / 1000.0 && body <= widest / 100.0, "body {body}, widest {widest}");
|
||||
assert!(imp.warnings.iter().any(|w| w.contains("the body's tolerance is")), "{:?}", imp.warnings);
|
||||
// Edges and vertices 5e-4 mm off: the body's tolerance covers the vertices' gaps.
|
||||
let imp = step_import(&dome_with_edges_raised(5e-4)).unwrap();
|
||||
crate::validation::check_kept(&imp.bodies[0].body).unwrap();
|
||||
let (body, _) = tolerances(&imp);
|
||||
assert!((5e-4..=1.1e-3).contains(&body), "body {body}");
|
||||
// Within what the stated uncertainty allows, the body keeps it.
|
||||
let imp = step_import(&dome_with_edges_bowed(5e-5)).unwrap();
|
||||
assert_eq!(tolerances(&imp).0, 1e-6, "{:?}", imp.warnings);
|
||||
}
|
||||
|
||||
/// The dome of [`dome_with_edges_bowed`] whose four edges are polylines along its flat
|
||||
/// boundary, 64 spans each, with one inner point `dz` mm above it: each edge bumps off the dome
|
||||
/// over 1/32 of its length, peaking halfway between two of the points the reader
|
||||
/// samples its gaps at (#12757).
|
||||
fn dome_with_narrow_bumps(dz: f64) -> String {
|
||||
let base = patch_with_edges(0.0, false, false);
|
||||
let sides: [[(usize, usize); 2]; 4] = [[(0, 0), (3, 0)], [(3, 0), (3, 3)], [(3, 3), (0, 3)], [(0, 3), (0, 0)]];
|
||||
let mut t = String::new();
|
||||
for line in base.lines() {
|
||||
// Each side's cubic, #400..#403, is replaced by its polyline (points from #1000).
|
||||
let side = (0..4).find(|k| line.starts_with(&format!("#{}=B_SPLINE_CURVE_WITH_KNOTS", 400 + k)));
|
||||
let Some(k) = side else {
|
||||
t += line;
|
||||
t += "\n";
|
||||
continue;
|
||||
};
|
||||
let [(i0, j0), (i1, j1)] = sides[k];
|
||||
let n = 65;
|
||||
let mut ids = Vec::new();
|
||||
for m in 0..n {
|
||||
let f = m as f64 / (n - 1) as f64;
|
||||
let (x, y) = ((i0 as f64 + (i1 as f64 - i0 as f64) * f) * 10.0, (j0 as f64 + (j1 as f64 - j0 as f64) * f) * 10.0);
|
||||
let id = 1000 + k * 100 + m;
|
||||
t += &format!("#{id}=CARTESIAN_POINT('',({x:.17E},{y:.17E},{:.17E}));\n", if m == 2 { dz } else { 0.0 });
|
||||
ids.push(format!("#{id}"));
|
||||
}
|
||||
let knots: Vec<String> = (0..n).map(|m| format!("{:.17E}", m as f64 / (n - 1) as f64)).collect();
|
||||
let mults: Vec<String> = (0..n).map(|m| if m == 0 || m == n - 1 { "2" } else { "1" }.to_string()).collect();
|
||||
t += &format!(
|
||||
"#{}=B_SPLINE_CURVE_WITH_KNOTS('',1,({}),.UNSPECIFIED.,.F.,.F.,({}),({}),.UNSPECIFIED.);\n",
|
||||
400 + k,
|
||||
ids.join(","),
|
||||
mults.join(","),
|
||||
knots.join(",")
|
||||
);
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_edges_gap_between_the_points_sampled_is_covered() {
|
||||
// Each edge bumps 2e-5 mm off the dome, narrowly, between two of the reader's samples
|
||||
// (#12757: 15 edges of an imported body lay up to 1.7 times farther off than sampled, and
|
||||
// SMLib refused it, "Bad Gap"). Its tolerance covers the peak, and the body checks.
|
||||
let imp = step_import(&dome_with_narrow_bumps(2e-5)).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
|
||||
let r = imp.bodies[0].body.record();
|
||||
assert!(r.edges.iter().all(|e| e.tolerance >= 2e-5), "{:?}", r.edges.iter().map(|e| e.tolerance).collect::<Vec<_>>());
|
||||
crate::validation::check_kept(&imp.bodies[0].body).unwrap_or_else(|e| panic!("{e}"));
|
||||
}
|
||||
|
||||
/// A vertex's tolerance is six times its gap (#11997) but never past SMLib's 1 mm: the ends of
|
||||
/// edges 0.2 mm off their face take 1 mm and are kept, as they were at 1.5 times the gap. (The
|
||||
/// vertex 0.19 mm off its LINE of #3352 needs no raise now: the edge is read as the segment
|
||||
/// between its vertices, and the part still reads.)
|
||||
#[test]
|
||||
fn raised_vertex_tolerances_stay_within_smlib_limit() {
|
||||
let slipped = step_import(include_str!("../../../../tests/kernel-cases/import/3352-vertex-off-its-edge-curve/input.step")).unwrap();
|
||||
assert_eq!(slipped.bodies.len(), 1, "{:?}", slipped.warnings);
|
||||
let imp = step_import(&dome_with_edges_bowed(0.3)).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
|
||||
let r = imp.bodies[0].body.record();
|
||||
let most = r.vertices.iter().map(|v| v.tolerance).chain(r.edges.iter().map(|e| e.tolerance)).fold(0.0, f64::max);
|
||||
assert!(most > 0.2 && most <= 1.0, "bowed edges: {most}");
|
||||
// Edges bowed about 0.6 mm off: three times the gap passes SMLib's 1 mm, so they take 1 mm and
|
||||
// are kept, as they were at 1.5 times it (a NIST FTC 07 edge 0.64 mm off its face).
|
||||
let far = step_import(&dome_with_edges_bowed(0.8)).unwrap();
|
||||
assert_eq!(far.bodies.len(), 1, "{:?}", far.warnings);
|
||||
assert!(!far.warnings.iter().any(|w| w.contains("left out") || w.contains("imported as surfaces")), "{:?}", far.warnings);
|
||||
let r = far.bodies[0].body.record();
|
||||
let most = r.edges.iter().map(|e| e.tolerance).fold(0.0, f64::max);
|
||||
assert!(most > 0.5 && most <= 1.0, "edges bowed farther: {most}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spline_edges_off_their_plane_by_less_than_the_file_states_get_their_closed_form() {
|
||||
// 0.8e-6 mm off, inside the file's 1e-6 mm but past the half of it a closed-form pcurve is
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
],
|
||||
"control_border": "#ffffff",
|
||||
"control_fill": "#000000",
|
||||
"navigator_border": "#ffffff",
|
||||
"navigator_face": "#000000",
|
||||
"navigator_hover": "#000000",
|
||||
"navigator_text": "#ffffff",
|
||||
"cube_border": "#ffffff",
|
||||
"cube_face": "#000000",
|
||||
"cube_hover": "#000000",
|
||||
"cube_text": "#ffffff",
|
||||
"dialog_bg": "#000000",
|
||||
"dialog_body": "#000000",
|
||||
"dialog_border": "#ffffff",
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
],
|
||||
"control_border": "#000000",
|
||||
"control_fill": "#ffffff",
|
||||
"navigator_border": "#000000",
|
||||
"navigator_face": "#ffffff",
|
||||
"navigator_hover": "#ffffff",
|
||||
"navigator_text": "#000000",
|
||||
"cube_border": "#000000",
|
||||
"cube_face": "#ffffff",
|
||||
"cube_hover": "#ffffff",
|
||||
"cube_text": "#000000",
|
||||
"dialog_bg": "#ffffff",
|
||||
"dialog_body": "#ffffff",
|
||||
"dialog_border": "#000000",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#d4d0ca",
|
||||
"control_fill": "#fffdfa",
|
||||
"navigator_border": "#b5b0a9",
|
||||
"navigator_face": "#fffdfa",
|
||||
"navigator_hover": "#d4d0ca",
|
||||
"navigator_text": "#13110e",
|
||||
"cube_border": "#b5b0a9",
|
||||
"cube_face": "#fffdfa",
|
||||
"cube_hover": "#d4d0ca",
|
||||
"cube_text": "#13110e",
|
||||
"dialog_bg": "#f9f6f2",
|
||||
"dialog_body": "#f9f6f2",
|
||||
"dialog_border": "#d4d0ca",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#533812",
|
||||
"control_fill": "#1d1406",
|
||||
"navigator_border": "#533812",
|
||||
"navigator_face": "#36250c",
|
||||
"navigator_hover": "#563a13",
|
||||
"navigator_text": "#efa335",
|
||||
"cube_border": "#533812",
|
||||
"cube_face": "#36250c",
|
||||
"cube_hover": "#563a13",
|
||||
"cube_text": "#efa335",
|
||||
"dialog_bg": "#100b04",
|
||||
"dialog_body": "#1d1406",
|
||||
"dialog_border": "#533812",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#315777",
|
||||
"control_fill": "#112d4b",
|
||||
"navigator_border": "#315777",
|
||||
"navigator_face": "#163b60",
|
||||
"navigator_hover": "#285f89",
|
||||
"navigator_text": "#e3f0fa",
|
||||
"cube_border": "#315777",
|
||||
"cube_face": "#163b60",
|
||||
"cube_hover": "#285f89",
|
||||
"cube_text": "#e3f0fa",
|
||||
"dialog_bg": "#0c223c",
|
||||
"dialog_body": "#112d4b",
|
||||
"dialog_border": "#315777",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#45475a",
|
||||
"control_fill": "#313244",
|
||||
"navigator_border": "#45475a",
|
||||
"navigator_face": "#313244",
|
||||
"navigator_hover": "#302b46",
|
||||
"navigator_text": "#cdd6f4",
|
||||
"cube_border": "#45475a",
|
||||
"cube_face": "#313244",
|
||||
"cube_hover": "#302b46",
|
||||
"cube_text": "#cdd6f4",
|
||||
"dialog_bg": "#1e1e2e",
|
||||
"dialog_body": "#1e1e2e",
|
||||
"dialog_border": "#45475a",
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
],
|
||||
"control_border": "#55ffff",
|
||||
"control_fill": "#000000",
|
||||
"navigator_border": "#55ffff",
|
||||
"navigator_face": "#000000",
|
||||
"navigator_hover": "#aa00aa",
|
||||
"navigator_text": "#ffffff",
|
||||
"cube_border": "#55ffff",
|
||||
"cube_face": "#000000",
|
||||
"cube_hover": "#aa00aa",
|
||||
"cube_text": "#ffffff",
|
||||
"dialog_bg": "#000000",
|
||||
"dialog_body": "#000000",
|
||||
"dialog_border": "#55ffff",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#5d5246",
|
||||
"control_fill": "#3c352d",
|
||||
"navigator_border": "#706354",
|
||||
"navigator_face": "#3c352d",
|
||||
"navigator_hover": "#5d5246",
|
||||
"navigator_text": "#e4e1dd",
|
||||
"cube_border": "#706354",
|
||||
"cube_face": "#3c352d",
|
||||
"cube_hover": "#5d5246",
|
||||
"cube_text": "#e4e1dd",
|
||||
"dialog_bg": "#312b25",
|
||||
"dialog_body": "#312b25",
|
||||
"dialog_border": "#4b4238",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#dcd3c2",
|
||||
"control_fill": "#fdfaf3",
|
||||
"navigator_border": "#b7b1a7",
|
||||
"navigator_face": "#fdfaf3",
|
||||
"navigator_hover": "#dcd3c2",
|
||||
"navigator_text": "#14110d",
|
||||
"cube_border": "#b7b1a7",
|
||||
"cube_face": "#fdfaf3",
|
||||
"cube_hover": "#dcd3c2",
|
||||
"cube_text": "#14110d",
|
||||
"dialog_bg": "#faf7f1",
|
||||
"dialog_body": "#faf7f1",
|
||||
"dialog_border": "#dcd3c2",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#b0b0b4",
|
||||
"control_fill": "#ececec",
|
||||
"navigator_border": "#9a9aa0",
|
||||
"navigator_face": "#f2f2f4",
|
||||
"navigator_hover": "#cce5ff",
|
||||
"navigator_text": "#262628",
|
||||
"cube_border": "#9a9aa0",
|
||||
"cube_face": "#f2f2f4",
|
||||
"cube_hover": "#cce5ff",
|
||||
"cube_text": "#262628",
|
||||
"dialog_bg": "#ececec",
|
||||
"dialog_body": "#f3f3f3",
|
||||
"dialog_border": "#c6c6c8",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#484d55",
|
||||
"control_fill": "#303338",
|
||||
"navigator_border": "#484d55",
|
||||
"navigator_face": "#303338",
|
||||
"navigator_hover": "#293c49",
|
||||
"navigator_text": "#e0e2e5",
|
||||
"cube_border": "#484d55",
|
||||
"cube_face": "#303338",
|
||||
"cube_hover": "#293c49",
|
||||
"cube_text": "#e0e2e5",
|
||||
"dialog_bg": "#25272b",
|
||||
"dialog_body": "#25272b",
|
||||
"dialog_border": "#484d55",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#285333",
|
||||
"control_fill": "#0b1d10",
|
||||
"navigator_border": "#285333",
|
||||
"navigator_face": "#0f3617",
|
||||
"navigator_hover": "#175625",
|
||||
"navigator_text": "#40ee66",
|
||||
"cube_border": "#285333",
|
||||
"cube_face": "#0f3617",
|
||||
"cube_hover": "#175625",
|
||||
"cube_text": "#40ee66",
|
||||
"dialog_bg": "#061009",
|
||||
"dialog_body": "#0b1d10",
|
||||
"dialog_border": "#285333",
|
||||
|
||||
@@ -45,10 +45,10 @@
|
||||
"preview_new": "#fabd2f",
|
||||
"preview_sketch": "#b8bb26",
|
||||
"preview_cut": "#fb49346e",
|
||||
"navigator_face": "#ebdbb2",
|
||||
"navigator_border": "#665c54",
|
||||
"navigator_text": "#282828",
|
||||
"navigator_hover": "#83a598",
|
||||
"cube_face": "#ebdbb2",
|
||||
"cube_border": "#665c54",
|
||||
"cube_text": "#282828",
|
||||
"cube_hover": "#83a598",
|
||||
"viewport_top": "#32302f",
|
||||
"viewport_bottom": "#32302f",
|
||||
"grid": "#443f3c",
|
||||
|
||||
@@ -44,10 +44,10 @@
|
||||
"preview_new": "#d79921",
|
||||
"preview_sketch": "#98971a",
|
||||
"preview_cut": "#cc241d6e",
|
||||
"navigator_face": "#fbf1c7",
|
||||
"navigator_border": "#7c6f64",
|
||||
"navigator_text": "#3c3836",
|
||||
"navigator_hover": "#83a598",
|
||||
"cube_face": "#fbf1c7",
|
||||
"cube_border": "#7c6f64",
|
||||
"cube_text": "#3c3836",
|
||||
"cube_hover": "#83a598",
|
||||
"viewport_top": "#fcf5d8",
|
||||
"viewport_bottom": "#ebdbb2",
|
||||
"grid": "#d5c4a1",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#a0a0a0",
|
||||
"control_fill": "#151515",
|
||||
"navigator_border": "#a0a0a0",
|
||||
"navigator_face": "#151515",
|
||||
"navigator_hover": "#002c45",
|
||||
"navigator_text": "#ffffff",
|
||||
"cube_border": "#a0a0a0",
|
||||
"cube_face": "#151515",
|
||||
"cube_hover": "#002c45",
|
||||
"cube_text": "#ffffff",
|
||||
"dialog_bg": "#0c0c0c",
|
||||
"dialog_body": "#000000",
|
||||
"dialog_border": "#a0a0a0",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#4c566a",
|
||||
"control_fill": "#3b4252",
|
||||
"navigator_border": "#4c566a",
|
||||
"navigator_face": "#3b4252",
|
||||
"navigator_hover": "#35495b",
|
||||
"navigator_text": "#eceff4",
|
||||
"cube_border": "#4c566a",
|
||||
"cube_face": "#3b4252",
|
||||
"cube_hover": "#35495b",
|
||||
"cube_text": "#eceff4",
|
||||
"dialog_bg": "#2e3440",
|
||||
"dialog_body": "#2e3440",
|
||||
"dialog_border": "#4c566a",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#50546d",
|
||||
"control_fill": "#343746",
|
||||
"navigator_border": "#50546d",
|
||||
"navigator_face": "#343746",
|
||||
"navigator_hover": "#393149",
|
||||
"navigator_text": "#f8f8f2",
|
||||
"cube_border": "#50546d",
|
||||
"cube_face": "#343746",
|
||||
"cube_hover": "#393149",
|
||||
"cube_text": "#f8f8f2",
|
||||
"dialog_bg": "#282a36",
|
||||
"dialog_body": "#282a36",
|
||||
"dialog_border": "#50546d",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#474b4f",
|
||||
"control_fill": "#2c2e31",
|
||||
"navigator_border": "#585b5f",
|
||||
"navigator_face": "#2c2e31",
|
||||
"navigator_hover": "#474b4f",
|
||||
"navigator_text": "#e1e3e5",
|
||||
"cube_border": "#585b5f",
|
||||
"cube_face": "#2c2e31",
|
||||
"cube_hover": "#474b4f",
|
||||
"cube_text": "#e1e3e5",
|
||||
"dialog_bg": "#222527",
|
||||
"dialog_body": "#222527",
|
||||
"dialog_border": "#383b3e",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#4a5262",
|
||||
"control_fill": "#272b34",
|
||||
"navigator_border": "#5c6370",
|
||||
"navigator_face": "#abb2bf",
|
||||
"navigator_hover": "#61afef",
|
||||
"navigator_text": "#21252b",
|
||||
"cube_border": "#5c6370",
|
||||
"cube_face": "#abb2bf",
|
||||
"cube_hover": "#61afef",
|
||||
"cube_text": "#21252b",
|
||||
"dialog_bg": "#282c34",
|
||||
"dialog_body": "#3b4453",
|
||||
"dialog_border": "#4b5263",
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
],
|
||||
"control_border": "#4a4f54",
|
||||
"control_fill": "#303235",
|
||||
"navigator_border": "#50565b",
|
||||
"navigator_face": "#34373a",
|
||||
"navigator_hover": "#53616c",
|
||||
"navigator_text": "#dededc",
|
||||
"cube_border": "#50565b",
|
||||
"cube_face": "#34373a",
|
||||
"cube_hover": "#53616c",
|
||||
"cube_text": "#dededc",
|
||||
"dialog_bg": "#2b2d2f",
|
||||
"dialog_body": "#2b2d2f",
|
||||
"dialog_border": "#3d4145",
|
||||
@@ -451,7 +451,7 @@
|
||||
"hover_profile": "#b7bdc230",
|
||||
"origin_plane_hover": "#b7bdc260",
|
||||
"profile_fill": "#b7bdc228",
|
||||
"navigator_hover": "#454b50",
|
||||
"cube_hover": "#454b50",
|
||||
"chip": "#b7bdc2",
|
||||
"logo": [
|
||||
"#dde0e3",
|
||||
@@ -498,7 +498,7 @@
|
||||
"hover_profile": "#aabcae30",
|
||||
"origin_plane_hover": "#aabcae60",
|
||||
"profile_fill": "#aabcae28",
|
||||
"navigator_hover": "#414d45",
|
||||
"cube_hover": "#414d45",
|
||||
"chip": "#aabcae",
|
||||
"logo": [
|
||||
"#d1ddd4",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#888888",
|
||||
"control_fill": "#dddddd",
|
||||
"navigator_border": "#888888",
|
||||
"navigator_face": "#eeeeee",
|
||||
"navigator_hover": "#ccccff",
|
||||
"navigator_text": "#222222",
|
||||
"cube_border": "#888888",
|
||||
"cube_face": "#eeeeee",
|
||||
"cube_hover": "#ccccff",
|
||||
"cube_text": "#222222",
|
||||
"dialog_bg": "#dddddd",
|
||||
"dialog_body": "#eeeeee",
|
||||
"dialog_border": "#888888",
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
],
|
||||
"control_border": "#586e75",
|
||||
"control_fill": "#073642",
|
||||
"navigator_border": "#586e75",
|
||||
"navigator_face": "#073642",
|
||||
"navigator_hover": "#586e75",
|
||||
"navigator_text": "#93a1a1",
|
||||
"cube_border": "#586e75",
|
||||
"cube_face": "#073642",
|
||||
"cube_hover": "#586e75",
|
||||
"cube_text": "#93a1a1",
|
||||
"dialog_bg": "#073642",
|
||||
"dialog_body": "#073642",
|
||||
"dialog_border": "#586e75",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#b6b3a1",
|
||||
"control_fill": "#e3ddc9",
|
||||
"navigator_border": "#b6b3a1",
|
||||
"navigator_face": "#e3ddc9",
|
||||
"navigator_hover": "#e1e8d9",
|
||||
"navigator_text": "#586e75",
|
||||
"cube_border": "#b6b3a1",
|
||||
"cube_face": "#e3ddc9",
|
||||
"cube_hover": "#e1e8d9",
|
||||
"cube_text": "#586e75",
|
||||
"dialog_bg": "#eee8d5",
|
||||
"dialog_body": "#eee8d5",
|
||||
"dialog_border": "#b6b3a1",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#808080",
|
||||
"control_fill": "#c0c0c0",
|
||||
"navigator_border": "#808080",
|
||||
"navigator_face": "#c0c0c0",
|
||||
"navigator_hover": "#8080ff",
|
||||
"navigator_text": "#000000",
|
||||
"cube_border": "#808080",
|
||||
"cube_face": "#c0c0c0",
|
||||
"cube_hover": "#8080ff",
|
||||
"cube_text": "#000000",
|
||||
"dialog_bg": "#c0c0c0",
|
||||
"dialog_body": "#c0c0c0",
|
||||
"dialog_border": "#808080",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
],
|
||||
"control_border": "#414868",
|
||||
"control_fill": "#24283b",
|
||||
"navigator_border": "#414868",
|
||||
"navigator_face": "#24283b",
|
||||
"navigator_hover": "#253452",
|
||||
"navigator_text": "#c0caf5",
|
||||
"cube_border": "#414868",
|
||||
"cube_face": "#24283b",
|
||||
"cube_hover": "#253452",
|
||||
"cube_text": "#c0caf5",
|
||||
"dialog_bg": "#1a1b26",
|
||||
"dialog_body": "#1a1b26",
|
||||
"dialog_border": "#414868",
|
||||
|
||||
@@ -179,7 +179,9 @@ impl Raw {
|
||||
// as given, so the chain stays exactly continuous.
|
||||
let mut path = vec![seg.point_at(1.0)];
|
||||
for p in [joint, next_start, next.point_at(0.0)] {
|
||||
if path.last().is_some_and(|q: &Vec2| q.dist(p) > 1e-12) {
|
||||
// A gap shorter than sketch coincidence (a few 1e-12 between two curves of a saved
|
||||
// design) is no piece: a line that short has no tangent (#11597).
|
||||
if path.last().is_some_and(|q: &Vec2| q.dist(p) > 1e-9) {
|
||||
path.push(p);
|
||||
}
|
||||
}
|
||||
@@ -219,7 +221,9 @@ fn exact_pieces(seg: Seg2) -> Result<Vec<Seg2>> {
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
Seg2::Line { a, b } if a.dist(b) < 1e-12 => Ok(Vec::new()),
|
||||
// A line shorter than sketch coincidence is the gap between two pieces that meet (a few 1e-12
|
||||
// apart in a saved design), not a piece: it has no tangent (#11597).
|
||||
Seg2::Line { a, b } if a.dist(b) < 1e-9 => Ok(Vec::new()),
|
||||
other => Ok(vec![other]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +294,9 @@ fn offset_seg(s: &Seg2, d: f64) -> Result<Vec<Seg2>> {
|
||||
// The centre is on the left of a counter-clockwise arc.
|
||||
let r = if sweep > 0.0 { radius - d } else { radius + d };
|
||||
if r <= 1e-6 {
|
||||
return Err(bad(format!("the wall is thicker than the radius of an arc it follows ({radius:.3} mm)")));
|
||||
// The wall reaches the arc's centre or past it: that side has no outline of its
|
||||
// own, the band is the whole sector (its square ends already close it).
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
Ok(vec![Seg2::Arc { center, radius: r, start, sweep }])
|
||||
}
|
||||
|
||||
@@ -115,10 +115,31 @@ fn a_spline_wall_is_its_length_times_its_thickness() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #7333: a wall as thick as the radius of an arc it follows, or thicker, on its inner side.
|
||||
#[test]
|
||||
fn walls_thicker_than_a_curve_bends_or_without_thickness_are_refused() {
|
||||
fn a_wall_thicker_than_an_arcs_radius_covers_the_arcs_whole_sector() {
|
||||
let r: f64 = 2.0;
|
||||
// A line, a quarter arc of radius 2 and a line: the wall follows them on both sides.
|
||||
let path = vec![
|
||||
line([-10.0, 0.0], [0.0, 0.0]),
|
||||
vec![Seg2::Arc { center: Vec2::new(0.0, 2.0), radius: r, start: -PI / 2.0, sweep: PI / 2.0 }],
|
||||
line([2.0, 2.0], [2.0, 12.0]),
|
||||
];
|
||||
for half in [1.9, 2.0, 2.1, 3.0] {
|
||||
let regions = thin_wall_regions(&path, half, half).unwrap_or_else(|e| panic!("wall of {} mm: {e}", 2.0 * half));
|
||||
assert_eq!(regions.len(), 1, "{} mm", 2.0 * half);
|
||||
assert!(regions[0].holes.is_empty(), "{} mm", 2.0 * half);
|
||||
}
|
||||
// The bands of a half-circle: the sector out to radius + the outer wall, however thick the inner one.
|
||||
let arc = vec![Seg2::Arc { center: Vec2::ZERO, radius: r, start: 0.0, sweep: PI }];
|
||||
close(area(std::slice::from_ref(&arc), 3.0, 0.0), 0.5 * PI * r * r);
|
||||
close(area(std::slice::from_ref(&arc), 2.5, 2.5), 0.5 * PI * 4.5f64.powi(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walls_thicker_than_a_free_form_curve_bends_or_without_thickness_are_refused() {
|
||||
let arc = vec![Seg2::Arc { center: Vec2::ZERO, radius: 2.0, start: 0.0, sweep: PI }];
|
||||
assert!(thin_wall_regions(std::slice::from_ref(&arc), 3.0, 0.0).is_err());
|
||||
// (a wall thicker than an arc's radius is no longer refused, see below)
|
||||
assert!(thin_wall_regions(&[arc], 0.0, 3.0).is_ok());
|
||||
let l = [line([0.0, 0.0], [10.0, 0.0])];
|
||||
for (a, b) in [(0.0, 0.0), (-1.0, 2.0), (f64::NAN, 1.0), (1.0, f64::INFINITY)] {
|
||||
|
||||
@@ -49,6 +49,7 @@ use solidcore::topology::brep::p2 as brep_p2;
|
||||
use solidcore::topology::brep::p2::ArenaNotifyState;
|
||||
use solidcore::topology::brep::p3::{DeleteFaceOptions, delete_face};
|
||||
use solidcore::topology::brep::p5::{StitchAndOrientOptions, stitch_and_orient};
|
||||
use solidcore::topology::brep::validation::{CheckLevel, validate_and_update_tolerances};
|
||||
use solidcore::topology::edgeuse::p1 as eu_p1;
|
||||
use solidcore::topology::edgeuse::p3::{BinormalOptions, evaluate_binormal};
|
||||
use solidcore::topology::face::p2 as face_p2;
|
||||
@@ -87,6 +88,7 @@ pub(super) fn status(e: SolidError) -> Error {
|
||||
e if is_curve_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
|
||||
e if is_surface_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
|
||||
e if is_offset_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
|
||||
e if is_blend_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
|
||||
SolidError::InvalidInput { .. } => Error::Status(INVALID_INPUT),
|
||||
SolidError::Unsupported { reason, .. } if reason == MIXED_CORNER_RAILS_CROSS => Error::Unported(MIXED_CORNER_UNSUPPORTED),
|
||||
e if is_boolean_failure(&e) => boolean_status(e),
|
||||
@@ -180,6 +182,13 @@ fn is_offset_failure(e: &SolidError) -> bool {
|
||||
e.offset_failure().is_some_and(|o| o.replaces_upstream_status())
|
||||
}
|
||||
|
||||
/// A blend routine's failure (solidcore's `BlendFailure`) that replaced a status the routine
|
||||
/// returned (`Upstream` before solidcore described it): it passes on as that status, whether or
|
||||
/// not it names an entity.
|
||||
fn is_blend_failure(e: &SolidError) -> bool {
|
||||
e.blend_failure().is_some_and(|b| b.replaces_upstream_status())
|
||||
}
|
||||
|
||||
/// `size_ok`.
|
||||
fn size_ok(x: f64) -> bool {
|
||||
x.is_finite() && x > 0.0 && x <= 1e6
|
||||
@@ -734,10 +743,16 @@ fn corners(
|
||||
}
|
||||
|
||||
/// `finish`: the result must have faces, and a solid input must give a closed manifold solid.
|
||||
pub(super) fn finish(b: Brep, solid_in: bool) -> solidcore::Result<Out<Brep>> {
|
||||
/// Before refusing a non-manifold result, grows tolerances to cover marginal gaps (SolidCraft
|
||||
/// issue #10: fuzzer seeds 178 and others). Tolerances only grow.
|
||||
pub(super) fn finish(k: &mut Kernel, mut b: Brep, solid_in: bool) -> solidcore::Result<Out<Brep>> {
|
||||
if b.face_list().is_empty() {
|
||||
return Ok(Err(Error::Empty));
|
||||
}
|
||||
// Always try growing tolerances first: covers marginal edge/vertex gaps where the gap
|
||||
// exceeds the recorded tolerance. Tolerances only grow: a body that already passes is
|
||||
// unchanged; a marginal failure becomes sound.
|
||||
let _ = validate_and_update_tolerances(k, &mut b, false, None, CheckLevel::Level2 as u8);
|
||||
if solid_in && !brep_p2::is_manifold_solid(&b)? {
|
||||
return Ok(Err(Error::NotSolid));
|
||||
}
|
||||
@@ -868,10 +883,15 @@ fn overflow_first(
|
||||
}
|
||||
}
|
||||
if lead.is_empty() || rest.is_empty() {
|
||||
return Ok(match blend_edges(k, &mut fresh, &again, each, section, None, &[], None)? {
|
||||
Ok(()) => untagged(fresh),
|
||||
Err(e) => Err(failed(e)),
|
||||
});
|
||||
match blend_edges(k, &mut fresh, &again, each, section, None, &[], None)? {
|
||||
Ok(()) => {}
|
||||
Err(e) => return Ok(Err(failed(e))),
|
||||
}
|
||||
match validate_and_update_tolerances(k, &mut fresh, false, None, CheckLevel::Level2 as u8) {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Ok(Err(Error::Internal(e.to_string()))),
|
||||
}
|
||||
return Ok(untagged(fresh));
|
||||
}
|
||||
if let Err(e) = blend_edges(k, &mut fresh, &lead, &lead_sizes, section, None, &[], None)? {
|
||||
return Ok(Err(failed(e)));
|
||||
@@ -903,6 +923,10 @@ fn overflow_first(
|
||||
{
|
||||
return Ok(Err(failed(e)));
|
||||
}
|
||||
match validate_and_update_tolerances(k, &mut fresh, false, None, CheckLevel::Level2 as u8) {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Ok(Err(Error::Internal(e.to_string()))),
|
||||
}
|
||||
Ok(untagged(fresh))
|
||||
}
|
||||
|
||||
@@ -1053,7 +1077,7 @@ fn blend_call(
|
||||
let chamfers: Vec<ChamferSize> = each.iter().map(|&d| ChamferSize::Distances([d, d])).collect();
|
||||
let section = Section::Chamfer { sizes: &chamfers, along: &[] };
|
||||
return Ok(match blend_edges(k, &mut w, &picked, &[], section, None, &[], None)? {
|
||||
Ok(()) => finish(w, solid_in)?,
|
||||
Ok(()) => finish(k, w, solid_in)?,
|
||||
Err(Ok(_)) => Err(Error::Unsupported),
|
||||
Err(Err(e)) => Err(e),
|
||||
});
|
||||
@@ -1072,7 +1096,7 @@ fn blend_call(
|
||||
let done = blend_edges_stopping(k, &mut w, &picked, &each, section, None, &back, (open == Corners::Open).then_some(&mut patches), first)?;
|
||||
if stopped.is_some() {
|
||||
return Ok(match overflow_first(k, body, points, tolerance, section, &picked, &mitred, &each, &mut w, dropped)? {
|
||||
Ok(fresh) => finish(fresh, solid_in)?,
|
||||
Ok(fresh) => finish(k, fresh, solid_in)?,
|
||||
Err(e) => Err(e),
|
||||
});
|
||||
}
|
||||
@@ -1083,7 +1107,11 @@ fn blend_call(
|
||||
Err(Err(e)) => return Ok(Err(e)),
|
||||
}
|
||||
if open == Corners::Filled {
|
||||
return finish(w, solid_in);
|
||||
match validate_and_update_tolerances(k, &mut w, false, None, CheckLevel::Level2 as u8) {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Ok(Err(Error::Internal(e.to_string()))),
|
||||
}
|
||||
return finish(k, w, solid_in);
|
||||
}
|
||||
// Each corner opened: its patch removed, the edges round its hole in the result's
|
||||
// orders, once per corner point (a vertex picked twice gives its hole twice).
|
||||
@@ -1115,7 +1143,11 @@ fn blend_call(
|
||||
}
|
||||
holes.push(Hole { edges });
|
||||
}
|
||||
finish(w, false)
|
||||
match validate_and_update_tolerances(k, &mut w, false, None, CheckLevel::Level2 as u8) {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Ok(Err(Error::Internal(e.to_string()))),
|
||||
}
|
||||
finish(k, w, false)
|
||||
})?;
|
||||
Ok((made, holes))
|
||||
}
|
||||
@@ -1307,7 +1339,7 @@ pub(crate) fn chamfer_sides(body: &Body, points: &[[f64; 3]], tolerance: f64, di
|
||||
};
|
||||
let section = Section::Chamfer { sizes: std::slice::from_ref(&size), along: &along };
|
||||
Ok(match blend_edges(k, &mut w, &picked, &[], section, None, &[], None)? {
|
||||
Ok(()) => finish(w, solid_in)?,
|
||||
Ok(()) => finish(k, w, solid_in)?,
|
||||
Err(Ok(_)) => Err(Error::Unsupported),
|
||||
Err(Err(e)) => Err(e),
|
||||
})
|
||||
@@ -1489,7 +1521,7 @@ pub(crate) fn fillet_variable(
|
||||
let solid_in = brep_p2::is_manifold_solid(&w)?;
|
||||
let (r0, r1) = (radii.first().copied().unwrap_or(0.0), radii.last().copied().unwrap_or(0.0));
|
||||
Ok(match blend_edges(k, &mut w, &[e], &[r0.max(r1)], Section::Round(Turn::Mitre), Some(law), &[], None)? {
|
||||
Ok(()) => finish(w, solid_in)?,
|
||||
Ok(()) => finish(k, w, solid_in)?,
|
||||
Err(Ok(_)) => Err(Error::Unsupported),
|
||||
Err(Err(e)) => Err(e),
|
||||
})
|
||||
@@ -1634,4 +1666,24 @@ mod tests {
|
||||
let folds = offset(OffsetFailure::Folds { face: solidcore::topology::FaceId::from_index(1), distance: 2.0, largest: 1.0 });
|
||||
assert!(!matches!(status(folds), Error::Status(_)));
|
||||
}
|
||||
|
||||
/// A blend routine's failure passes on as the status it replaced, as the `Upstream` it
|
||||
/// replaced did, whether or not it names an edge.
|
||||
#[test]
|
||||
fn blend_failures_pass_on_as_their_status() {
|
||||
use solidcore::error::Failure;
|
||||
use solidcore::error::blend::BlendFailure;
|
||||
let blend = |b| Failure::new("blend::feature_executive", b).into_error();
|
||||
assert!(matches!(status(blend(BlendFailure::Failed("x"))), Error::Status(1001)));
|
||||
assert!(matches!(status(blend(BlendFailure::Missing("the edge use's edge"))), Error::Status(1006)));
|
||||
assert!(matches!(status(blend(BlendFailure::InvalidValue("x"))), Error::Status(1007)));
|
||||
assert!(matches!(status(blend(BlendFailure::Inconsistent("x"))), Error::Status(1005)));
|
||||
assert!(matches!(status(blend(BlendFailure::BadIntersections("x"))), Error::Status(1027)));
|
||||
assert!(matches!(status(blend(BlendFailure::NotConverged("x"))), Error::Status(1022)));
|
||||
assert!(matches!(status(blend(BlendFailure::PassedOn { what: "x", status: 1020 })), Error::Status(1020)));
|
||||
let at = Failure::new("blend::fillet_executive::p3", BlendFailure::Failed("x"))
|
||||
.at(solidcore::EntityRef::Edge(solidcore::topology::EdgeId::from_index(2)))
|
||||
.into_error();
|
||||
assert!(matches!(status(at), Error::Status(1001)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,19 @@ pub(crate) fn boolean(a: &Body, b: &Body, op: BooleanOp) -> Result<Option<Body>>
|
||||
return Err(SolidError::invalid_input("smlib_boolean", "no operand"));
|
||||
};
|
||||
let solid_in = p2::is_manifold_solid(&ca)? && p2::is_manifold_solid(&cb)?;
|
||||
let r = sm::boolean(k, ca, cb, operation(op))?;
|
||||
let mut r = sm::boolean(k, ca, cb, operation(op))?;
|
||||
// Grow the result's tolerances to cover marginal edge/vertex, vertex/face and
|
||||
// edge/face gaps, so an otherwise-sound union is not refused as "Bad Gap" for a
|
||||
// sub-micron gap (SolidCraft issue #10, fuzzer seed 73). Tolerances only grow:
|
||||
// a body that already passes is unchanged; a marginal failure becomes sound.
|
||||
// Offset and shell already grow their results' tolerances this way.
|
||||
solidcore::topology::brep::validation::validate_and_update_tolerances(
|
||||
k,
|
||||
&mut r.result,
|
||||
false,
|
||||
None,
|
||||
solidcore::topology::brep::validation::CheckLevel::Level2 as u8,
|
||||
)?;
|
||||
finish(r.result, solid_in, true)
|
||||
})?;
|
||||
result(d, Ok(None))
|
||||
|
||||
@@ -173,6 +173,6 @@ pub(crate) fn merge_faces(body: &Body, keep: &[[f64; 3]], tolerance: f64) -> Res
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
b.refresh_cached_topology();
|
||||
finish(b, solid_in)
|
||||
finish(k, b, solid_in)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ fn trims(face: &TrimFace<'_>) -> Result<Vec<Vec<Curve2>>, String> {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// The least margin an edge's tolerance covers its gap with where [`SURFACE_GAP_MARGIN`] would
|
||||
/// pass SMLib's limit: it takes the limit instead, as the STEP reader does.
|
||||
const LEAST_GAP_MARGIN: f64 = 1.5;
|
||||
|
||||
/// Each bare use's edge measured against the face's surface as SMLib measures it (over the
|
||||
/// surface's own domain: an edge run a few um past its patch measures how far), and its
|
||||
/// `EDGE_TOLERANCE` in `arrays` raised to cover that gap with [`SURFACE_GAP_MARGIN`]: SMLib keeps
|
||||
@@ -85,15 +89,17 @@ fn cover_gaps(body: &crate::Brep, face: &TrimFace<'_>, bare: &HashSet<(u32, u32,
|
||||
let mut raised = false;
|
||||
for e in edges {
|
||||
let gap = body.edge_face_distance(e, 0).map_err(|e| e.to_string())?;
|
||||
let need = SURFACE_GAP_MARGIN * gap;
|
||||
// The margin up to SMLib's limit; refused only where even the least margin, the reader's
|
||||
// (1.5), would pass it.
|
||||
if !(LEAST_GAP_MARGIN * gap <= crate::flat::MAX_TOLERANCE) {
|
||||
return Err(format!("an edge lies {gap} mm off the face's surface"));
|
||||
}
|
||||
let need = (SURFACE_GAP_MARGIN * gap).min(crate::flat::MAX_TOLERANCE);
|
||||
let tolerances = arrays.reals.get_mut(&crate::flat::EDGE_TOLERANCE).ok_or("edge tolerances")?;
|
||||
let given = tolerances.get_mut(e).ok_or("edge reference")?;
|
||||
if need <= *given {
|
||||
continue;
|
||||
}
|
||||
if !(need <= crate::flat::MAX_TOLERANCE) {
|
||||
return Err(format!("an edge lies {gap} mm off the face's surface"));
|
||||
}
|
||||
*given = need;
|
||||
raised = true;
|
||||
}
|
||||
|
||||
+25
-25
@@ -1074,7 +1074,7 @@ pub struct Tokens {
|
||||
pub origin_plane_hover: Color32,
|
||||
pub origin_point: Color32,
|
||||
/// The X, Y and Z colours, one set for every axis drawn (#976): the origin axes, the view
|
||||
/// triad, the navigator's triad, the Move gizmo, a 3D sketch point's triad, joint
|
||||
/// triad, the navigation cube's triad, the Move gizmo, a 3D sketch point's triad, joint
|
||||
/// origins, measured distances along the axes and the headless picture's origin axes.
|
||||
pub axis_x: Color32,
|
||||
pub axis_y: Color32,
|
||||
@@ -1100,12 +1100,12 @@ pub struct Tokens {
|
||||
pub preview_sketch: Color32,
|
||||
/// Removed material in a feature preview (translucent).
|
||||
pub preview_cut: Color32,
|
||||
/// Navigator faces, outline and labels.
|
||||
pub navigator_face: Color32,
|
||||
pub navigator_border: Color32,
|
||||
pub navigator_text: Color32,
|
||||
/// Light blue hover fill, readable on the pale navigator in either theme.
|
||||
pub navigator_hover: Color32,
|
||||
/// Navigation cube faces, outline and labels.
|
||||
pub cube_face: Color32,
|
||||
pub cube_border: Color32,
|
||||
pub cube_text: Color32,
|
||||
/// Light blue hover fill, readable on the pale cube in either theme.
|
||||
pub cube_hover: Color32,
|
||||
// ---- viewport ----
|
||||
/// Viewport background (top and bottom of the gradient; equal for a flat fill).
|
||||
pub viewport_top: Color32,
|
||||
@@ -1178,11 +1178,11 @@ pub struct Tokens {
|
||||
pub figure_highlight: Color32,
|
||||
/// Neutral shadow cast by a model in a Preferences figure, independent of theme polarity.
|
||||
pub figure_shadow: Color32,
|
||||
/// Top face of the Preferences figure box.
|
||||
/// Top face of the Preferences figure cube.
|
||||
pub figure_face_top: Color32,
|
||||
/// Left face of the Preferences figure box.
|
||||
/// Left face of the Preferences figure cube.
|
||||
pub figure_face_left: Color32,
|
||||
/// Right face of the Preferences figure box.
|
||||
/// Right face of the Preferences figure cube.
|
||||
pub figure_face_right: Color32,
|
||||
/// Paper, panels and pads drawn inside Preferences figures.
|
||||
pub figure_card: Color32,
|
||||
@@ -1378,7 +1378,7 @@ impl Tokens {
|
||||
&mut self.nav_field,
|
||||
&mut self.nav_viewport,
|
||||
&mut self.glyph_bg,
|
||||
&mut self.navigator_face,
|
||||
&mut self.cube_face,
|
||||
&mut self.viewport_top,
|
||||
&mut self.viewport_bottom,
|
||||
&mut self.grid,
|
||||
@@ -1389,7 +1389,7 @@ impl Tokens {
|
||||
&mut self.control_border,
|
||||
&mut self.app_bar_separator,
|
||||
&mut self.nav_tip_key_border,
|
||||
&mut self.navigator_border,
|
||||
&mut self.cube_border,
|
||||
&mut self.file_background,
|
||||
&mut self.file_surface,
|
||||
&mut self.file_border,
|
||||
@@ -1425,7 +1425,7 @@ impl Tokens {
|
||||
|
||||
/// These tokens with the colours drawn in the 3D view taken from `view`: the background and
|
||||
/// grid, bodies, selection and hover, the origin and construction geometry, previews, sketches
|
||||
/// and their dimensions and glyphs, the navigator and triads, box selection and the
|
||||
/// and their dimensions and glyphs, the navigation cube and triads, box selection and the
|
||||
/// analysis colours. The window keeps its own: panels, dialogs, menus, and the chips and cards
|
||||
/// over the view (`overlay` with its text, icons and borders).
|
||||
pub fn with_viewport(self, view: Tokens) -> Tokens {
|
||||
@@ -1459,10 +1459,10 @@ impl Tokens {
|
||||
preview_new: view.preview_new,
|
||||
preview_sketch: view.preview_sketch,
|
||||
preview_cut: view.preview_cut,
|
||||
navigator_face: view.navigator_face,
|
||||
navigator_border: view.navigator_border,
|
||||
navigator_text: view.navigator_text,
|
||||
navigator_hover: view.navigator_hover,
|
||||
cube_face: view.cube_face,
|
||||
cube_border: view.cube_border,
|
||||
cube_text: view.cube_text,
|
||||
cube_hover: view.cube_hover,
|
||||
viewport_top: view.viewport_top,
|
||||
viewport_bottom: view.viewport_bottom,
|
||||
grid: view.grid,
|
||||
@@ -1536,15 +1536,15 @@ impl Tokens {
|
||||
t
|
||||
}
|
||||
|
||||
/// Fade idle navigator colours into the viewport while retaining opaque facets.
|
||||
pub fn navigator(self, active: bool) -> Tokens {
|
||||
/// Fade idle cube colours into the viewport while retaining opaque facets.
|
||||
pub fn navigation_cube(self, active: bool) -> Tokens {
|
||||
if active {
|
||||
return self;
|
||||
}
|
||||
let mut t = self;
|
||||
t.navigator_face = t.navigator_face.lerp_to_gamma(t.viewport_top, 0.55);
|
||||
t.navigator_border = t.navigator_border.lerp_to_gamma(t.viewport_top, 0.55);
|
||||
t.navigator_text = t.navigator_text.lerp_to_gamma(t.viewport_top, 0.55);
|
||||
t.cube_face = t.cube_face.lerp_to_gamma(t.viewport_top, 0.55);
|
||||
t.cube_border = t.cube_border.lerp_to_gamma(t.viewport_top, 0.55);
|
||||
t.cube_text = t.cube_text.lerp_to_gamma(t.viewport_top, 0.55);
|
||||
t.axis_x = t.axis_x.lerp_to_gamma(t.viewport_top, 0.55);
|
||||
t.axis_y = t.axis_y.lerp_to_gamma(t.viewport_top, 0.55);
|
||||
t.axis_z = t.axis_z.lerp_to_gamma(t.viewport_top, 0.55);
|
||||
@@ -1931,7 +1931,7 @@ mod tests {
|
||||
// The two sides differ, so the checks below tell them apart.
|
||||
assert_ne!((w.viewport_top, w.panel), (v.viewport_top, v.panel));
|
||||
// The view's: background and grid, bodies, selection and hover, sketches with their
|
||||
// dimensions and glyphs, the origin, the navigator and triads, box selection, analysis.
|
||||
// dimensions and glyphs, the origin, the cube and triads, box selection, analysis.
|
||||
assert_eq!((t.viewport_top, t.viewport_bottom, t.grid, t.grid_major), (v.viewport_top, v.viewport_bottom, v.grid, v.grid_major));
|
||||
assert_eq!(
|
||||
(t.body, t.body_edge, t.body_selected, t.sel_face, t.sel_edge, t.hover_edge),
|
||||
@@ -1942,8 +1942,8 @@ mod tests {
|
||||
(v.sketch, v.sketch_fixed, v.dimension, v.glyph_bg, v.glyph_text, v.profile_fill)
|
||||
);
|
||||
assert_eq!(
|
||||
(t.origin_plane, t.axis_x, t.navigator_face, t.navigator_text, t.axis_y, t.axis_z),
|
||||
(v.origin_plane, v.axis_x, v.navigator_face, v.navigator_text, v.axis_y, v.axis_z)
|
||||
(t.origin_plane, t.axis_x, t.cube_face, t.cube_text, t.axis_y, t.axis_z),
|
||||
(v.origin_plane, v.axis_x, v.cube_face, v.cube_text, v.axis_y, v.axis_z)
|
||||
);
|
||||
assert_eq!(
|
||||
(t.box_window, t.section_cap, t.preview_add, t.sketch_construction, t.continuity),
|
||||
|
||||
@@ -196,10 +196,10 @@ impl Tokens {
|
||||
preview_new: Color32::from_rgb(240, 225, 105),
|
||||
preview_sketch: Color32::from_rgb(32, 190, 96),
|
||||
preview_cut: Color32::from_rgba_unmultiplied_const(232, 84, 44, 110),
|
||||
navigator_face: Color32::from_rgb(236, 239, 244),
|
||||
navigator_border: Color32::from_rgb(85, 94, 110),
|
||||
navigator_text: Color32::from_rgb(38, 42, 50),
|
||||
navigator_hover: Color32::from_rgb(190, 220, 252),
|
||||
cube_face: Color32::from_rgb(236, 239, 244),
|
||||
cube_border: Color32::from_rgb(85, 94, 110),
|
||||
cube_text: Color32::from_rgb(38, 42, 50),
|
||||
cube_hover: Color32::from_rgb(190, 220, 252),
|
||||
viewport_top: Color32::from_rgb(240, 243, 247),
|
||||
viewport_bottom: Color32::from_rgb(203, 210, 221),
|
||||
grid: Color32::from_rgb(196, 202, 212),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! The custom view gizmo: the body chosen in [`crate::view_gizmo`] drawn in place of the signed
|
||||
//! axes, each face shaded by how squarely it meets the eye. Its texture is the theme's navigator
|
||||
//! face and border colours, the hovered face in the hover colour and accent (themed), or the
|
||||
//! body's own appearance with the view's edge colour, the hovered face lit as the view lights a
|
||||
//! face under the pointer (modeled); orbit and roll arrows and a Home button go around it when
|
||||
//! the `navigator_arrows` and `navigator_home` preferences say so. While the Custom View Navigator
|
||||
//! panel is open it shows the panel's choices.
|
||||
//! The custom view gizmo: the body chosen in [`crate::view_gizmo`] drawn in place of the navigation
|
||||
//! cube, made the way the cube is: the same size, each face shaded by how squarely it meets the
|
||||
//! eye. Its texture is the cube's face and border colours, the hovered face in the cube's hover
|
||||
//! colour and accent (themed), or the body's own appearance with the view's edge colour, the
|
||||
//! hovered face lit as the view lights a face under the pointer (modeled); the cube's orbit and roll
|
||||
//! arrows and its Home button go around it when the `navigator_arrows` and `navigator_home`
|
||||
//! preferences say so. While the Custom View Navigator panel is open it shows the panel's choices.
|
||||
//! A click on a face looks straight at it; a drag orbits.
|
||||
//!
|
||||
//! A concave body cannot be drawn back to front as a box's six faces are, so the faces go
|
||||
//! A concave body cannot be drawn back to front as the cube's six faces are, so the faces go
|
||||
//! through a small depth-buffered raster (drawn on the CPU, shown as a texture): hidden faces
|
||||
//! stay hidden and each sample knows its triangle, so hover and click name the face actually
|
||||
//! seen. Edges stay crisp vector lines, cut where the raster says something nearer covers them.
|
||||
@@ -17,7 +17,7 @@ use crate::view_gizmo::Shape;
|
||||
use solidcraft_engine::geom::Mesh;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// The body's bounding sphere, in the navigator's design units: the navigator's own (half
|
||||
/// The body's bounding sphere, in the navigation cube's design units: the cube's own (half
|
||||
/// side 40, so its corners lie 40√3 from the centre).
|
||||
const RADIUS: f32 = 69.28;
|
||||
/// Samples per pixel along each side: smooth silhouettes.
|
||||
@@ -27,13 +27,13 @@ const MAX_PIXELS: usize = 512;
|
||||
/// No triangle.
|
||||
const NONE: u32 = u32::MAX;
|
||||
|
||||
/// Centre, radius (points) of the disc the body fills, and the navigator's design scale.
|
||||
/// Centre, radius (points) of the disc the body fills, and the cube's design scale.
|
||||
fn disc(app: &SolveApp, rect: Rect) -> (Pos2, f32, f32) {
|
||||
let scale = axis_rect(app, rect).width() / navigator_controls::DESIGN_SIZE;
|
||||
let scale = axis_rect(app, rect).width() / navigation_cube::DESIGN_SIZE;
|
||||
(axis_center(app, rect), scale * RADIUS, scale)
|
||||
}
|
||||
|
||||
/// A press anywhere on the body's disc orbits, as a press on the axes does.
|
||||
/// A press anywhere on the body's disc orbits, as a press on the cube does.
|
||||
pub(super) fn drag_origin(app: &SolveApp, rect: Rect, p: Pos2) -> bool {
|
||||
let (c, r, _) = disc(app, rect);
|
||||
c.distance(p) <= r
|
||||
@@ -102,7 +102,7 @@ fn raster(mesh: &Mesh, cam: &Camera, side: usize) -> Raster {
|
||||
let mut out = Raster { side, depth: vec![f64::NEG_INFINITY; n], tri: vec![NONE; n], shade: vec![0.0; n], edges: Vec::new() };
|
||||
let Some(view) = View::new(mesh, cam, side) else { return out };
|
||||
let back = view.basis.2;
|
||||
// The face shading: a face square to the eye is the full face colour, an edge-on one 72 %.
|
||||
// The cube's shading: a face square to the eye is the full face colour, an edge-on one 72 %.
|
||||
let shade = |n: Vec3| 0.72 + 0.28 * n.dot(back).clamp(0.0, 1.0) as f32;
|
||||
let limit = (side - 1) as f64;
|
||||
for (i, t) in mesh.triangles.iter().enumerate() {
|
||||
@@ -208,17 +208,17 @@ struct Look {
|
||||
hovered: Option<u32>,
|
||||
texture: NavigatorTexture,
|
||||
active: bool,
|
||||
/// Face, hover, the theme's body colour and the view's background.
|
||||
/// Cube face, cube hover, the theme's body colour and the view's background.
|
||||
colours: [Color32; 4],
|
||||
/// How much lighter the view draws a face under the pointer (grey levels; modeled).
|
||||
lift: u8,
|
||||
}
|
||||
|
||||
/// The hovered face's outline: the navigator's accent (themed), or the view's mouse-over edge colour
|
||||
/// The hovered face's outline: the cube's accent (themed), or the view's mouse-over edge colour
|
||||
/// (modeled).
|
||||
fn hover_outline(texture: NavigatorTexture, look: &Tokens, view: &Tokens) -> Color32 {
|
||||
fn hover_outline(texture: NavigatorTexture, cube: &Tokens, view: &Tokens) -> Color32 {
|
||||
match texture {
|
||||
NavigatorTexture::Themed => look.accent,
|
||||
NavigatorTexture::Themed => cube.accent,
|
||||
NavigatorTexture::Modeled => view.hover_edge,
|
||||
}
|
||||
}
|
||||
@@ -230,12 +230,12 @@ fn rgb(r: u8, g: u8, b: u8) -> Color32 {
|
||||
|
||||
/// The colour face `face` is painted before shading.
|
||||
fn face_colour(shape: &Shape, face: Option<u32>, look: &Look) -> Color32 {
|
||||
let [navigator_face, hover, body, background] = look.colours;
|
||||
let [cube_face, hover, body, background] = look.colours;
|
||||
let hovered = face.is_some() && face == look.hovered;
|
||||
match look.texture {
|
||||
// As on the axes, the hovered target is one flat colour.
|
||||
// As on the cube, the hovered target is one flat colour.
|
||||
NavigatorTexture::Themed if hovered => hover,
|
||||
NavigatorTexture::Themed => navigator_face,
|
||||
NavigatorTexture::Themed => cube_face,
|
||||
NavigatorTexture::Modeled => {
|
||||
let own = shape.colour_of(face).map_or(body, |[r, g, b]| rgb(r, g, b));
|
||||
if hovered {
|
||||
@@ -243,7 +243,7 @@ fn face_colour(shape: &Shape, face: Option<u32>, look: &Look) -> Color32 {
|
||||
let k = look.lift;
|
||||
return rgb(own.r().saturating_add(k), own.g().saturating_add(k), own.b().saturating_add(k));
|
||||
}
|
||||
// Idle, it fades into the view as the axes does.
|
||||
// Idle, it fades into the view as the cube does.
|
||||
if look.active { own } else { own.lerp_to_gamma(background, 0.55) }
|
||||
}
|
||||
}
|
||||
@@ -324,7 +324,7 @@ type Shared = Arc<Mutex<Option<Cache>>>;
|
||||
|
||||
/// Draw the custom navigator as `shown` says (the panel's choices while it is open).
|
||||
pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, shown: crate::view_gizmo::Shown) {
|
||||
let Some(shape) = shown.shape else { return };
|
||||
let Some(shape) = shown.shape else { return navigation_cube::show(app, ui, rect) };
|
||||
let outer = axis_rect(app, rect);
|
||||
if !outer.is_positive() {
|
||||
app.viewport.axis_drag = None;
|
||||
@@ -335,8 +335,8 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, shown: cra
|
||||
let (c, radius, scale) = disc(app, rect);
|
||||
let orbiting = axis_view_orbit(app, ui, rect);
|
||||
let active = orbiting || ui.rect_contains_pointer(outer);
|
||||
let t = Tokens::get().navigator(active);
|
||||
crate::scenario::publish_count("navigator:active", f64::from(u8::from(active)));
|
||||
let t = Tokens::get().navigation_cube(active);
|
||||
crate::scenario::publish_count("navigation_cube:active", f64::from(u8::from(active)));
|
||||
let ppp = ui.ctx().pixels_per_point();
|
||||
let px = ((2.0 * radius * ppp).ceil().max(1.0) as usize).min(MAX_PIXELS);
|
||||
let side = px * SS;
|
||||
@@ -373,7 +373,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, shown: cra
|
||||
hovered: face,
|
||||
texture,
|
||||
active,
|
||||
colours: [t.navigator_face, t.navigator_hover, view.body, view.viewport_top],
|
||||
colours: [t.cube_face, t.cube_hover, view.body, view.viewport_top],
|
||||
lift: view.hover_face_lift,
|
||||
};
|
||||
if cache.look != Some(look) || cache.texture.is_none() {
|
||||
@@ -387,12 +387,12 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, shown: cra
|
||||
if let Some(tex) = &cache.texture {
|
||||
painter.image(tex.id(), image, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
}
|
||||
// The body's edges in the navigator's border colour; the hovered face's in the accent, on top.
|
||||
// The body's edges in the cube's border colour; the hovered face's in the accent, on top.
|
||||
let of_hovered = |e: usize| face.is_some_and(|f| mesh.edge_faces.get(e).is_some_and(|fs| fs.contains(&f)));
|
||||
for lit in [false, true] {
|
||||
for (_, a, b) in cache.raster.edges.iter().filter(|(e, _, _)| of_hovered(*e) == lit) {
|
||||
let edge = match texture {
|
||||
NavigatorTexture::Themed => t.navigator_border,
|
||||
NavigatorTexture::Themed => t.cube_border,
|
||||
// Idle, the model's edges fade into the view with its faces.
|
||||
NavigatorTexture::Modeled if active => view.body_edge,
|
||||
NavigatorTexture::Modeled => view.body_edge.lerp_to_gamma(view.viewport_top, 0.55),
|
||||
@@ -416,14 +416,14 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, shown: cra
|
||||
if ui.input(|i| i.pointer.primary_clicked())
|
||||
&& let Some(normal) = shape.face_normal(tri)
|
||||
{
|
||||
navigator_controls::navigate(app, None, Some(app.cam.to_frame(normal)));
|
||||
navigation_cube::navigate(app, None, Some(app.cam.to_frame(normal)));
|
||||
}
|
||||
}
|
||||
if shown.arrows {
|
||||
navigator_controls::arrows(app, ui, &painter, c, scale, active, t);
|
||||
navigation_cube::arrows(app, ui, &painter, c, scale, active, false, t);
|
||||
}
|
||||
if shown.home {
|
||||
navigator_controls::home(app, ui, &painter, c, scale, active, t);
|
||||
navigation_cube::home(app, ui, &painter, c, scale, active, t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,26 +517,26 @@ mod tests {
|
||||
fn the_rotation_arrows_and_home_follow_their_settings_and_turn_the_view() {
|
||||
let mut h = boxed();
|
||||
custom(&mut h);
|
||||
assert!(crate::scenario::handle_at("navigator:control:left").is_some(), "arrows by default");
|
||||
assert!(crate::scenario::handle_at("navigator:control:home").is_some(), "Home by default");
|
||||
assert!(crate::scenario::handle_at("navigation_cube:control:left").is_some(), "arrows by default");
|
||||
assert!(crate::scenario::handle_at("navigation_cube:control:home").is_some(), "Home by default");
|
||||
h.app.run("view.navigator", json!({"arrows": false, "home": false})).unwrap();
|
||||
h.frames(2);
|
||||
assert!(crate::scenario::handle_at("navigator:control:left").is_none(), "arrows off");
|
||||
assert!(crate::scenario::handle_at("navigator:control:home").is_none(), "Home off");
|
||||
assert!(crate::scenario::handle_at("navigation_cube:control:left").is_none(), "arrows off");
|
||||
assert!(crate::scenario::handle_at("navigation_cube:control:home").is_none(), "Home off");
|
||||
h.app.run("view.navigator", json!({"arrows": true, "home": true})).unwrap();
|
||||
h.frames(2);
|
||||
let p = crate::scenario::handle_at("navigator:control:clockwise").expect("the roll arrows are there");
|
||||
let p = crate::scenario::handle_at("navigation_cube:control:clockwise").expect("the roll arrows are there");
|
||||
let camera = h.app.cam;
|
||||
h.call("ui.click", json!({"x": p.x, "y": p.y}));
|
||||
assert!((h.app.cam.roll - camera.roll - std::f64::consts::FRAC_PI_2).abs() < 1e-6);
|
||||
let p = crate::scenario::handle_at("navigator:control:home").expect("Home is there");
|
||||
let p = crate::scenario::handle_at("navigation_cube:control:home").expect("Home is there");
|
||||
h.call("ui.click", json!({"x": p.x, "y": p.y}));
|
||||
assert!(h.app.cam.back().dist(Camera::default().back()) < 1e-6, "Home turns to the home view");
|
||||
assert!(crate::scenario::handle_at("navigator:control:fit").is_none(), "Fit is not offered");
|
||||
assert!(crate::scenario::handle_at("navigation_cube:control:fit").is_none(), "Fit stays the cube's");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modeled_texture_paints_the_body_in_its_own_colour_and_themed_in_the_theme_colours() {
|
||||
fn modeled_texture_paints_the_body_in_its_own_colour_and_themed_in_the_cubes() {
|
||||
let mut h = boxed();
|
||||
let name = body_name(&h);
|
||||
let mut shape = Shape::of_body(&h.app.session, &name).unwrap();
|
||||
@@ -547,11 +547,11 @@ mod tests {
|
||||
hovered: None,
|
||||
texture,
|
||||
active: true,
|
||||
colours: [t.navigator_face, t.navigator_hover, t.body, t.viewport_top],
|
||||
colours: [t.cube_face, t.cube_hover, t.body, t.viewport_top],
|
||||
lift: t.hover_face_lift,
|
||||
};
|
||||
assert_ne!(t.body_edge, t.navigator_border, "the two textures' edges can be told apart");
|
||||
// A hovered face: the navigator's hover colours when themed, the view's mouse-over when modeled.
|
||||
assert_ne!(t.body_edge, t.cube_border, "the two textures' edges can be told apart");
|
||||
// A hovered face: the cube's hover colours when themed, the view's mouse-over when modeled.
|
||||
assert_eq!(hover_outline(NavigatorTexture::Themed, &t, &t), t.accent);
|
||||
assert_eq!(hover_outline(NavigatorTexture::Modeled, &t, &t), t.hover_edge);
|
||||
assert!(t.hover_face_lift > 0);
|
||||
@@ -569,8 +569,8 @@ mod tests {
|
||||
let red = middle(&compose(&r, &shape, &look(NavigatorTexture::Modeled)));
|
||||
assert!(red.r() > red.g() * 3 && red.r() > red.b() * 3, "the body's red: {red:?}");
|
||||
let themed = middle(&compose(&r, &shape, &look(NavigatorTexture::Themed)));
|
||||
let shade = f32::from(themed.r()) / f32::from(t.navigator_face.r().max(1));
|
||||
assert!((0.7..=1.01).contains(&shade), "the navigator's face colour, shaded: {themed:?}");
|
||||
let shade = f32::from(themed.r()) / f32::from(t.cube_face.r().max(1));
|
||||
assert!((0.7..=1.01).contains(&shade), "the cube's face colour, shaded: {themed:?}");
|
||||
// The gizmo draws without a frame failing in both textures.
|
||||
custom(&mut h);
|
||||
for texture in ["modeled", "themed"] {
|
||||
@@ -581,11 +581,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_without_a_body_draws_the_signed_axes() {
|
||||
fn custom_without_a_body_draws_the_cube() {
|
||||
let mut h = boxed();
|
||||
h.call("ui.prefs", json!({"navigator_style": "custom"}));
|
||||
h.frames(2);
|
||||
assert!(crate::scenario::handle_at("axis_view:z").is_some());
|
||||
assert!(crate::scenario::handle_at("navigation_cube:front").is_some() || crate::scenario::handle_at("navigation_cube:top").is_some());
|
||||
assert_eq!(crate::scenario::count_of("body_gizmo:faces"), None);
|
||||
}
|
||||
}
|
||||
|
||||
+174
-50
@@ -2,9 +2,9 @@
|
||||
//! construction planes, plus document settings and the origin.
|
||||
//!
|
||||
//! Rows are drawn in code (icons from [`icons`]) and act through commands. A click selects
|
||||
//! (Ctrl adds, Shift extends over the visible rows), hovering a body row lights it up in the
|
||||
//! viewport, a double-click on a sketch edits it, and a right-click opens the item's menu
|
||||
//! ([`crate::context_menu`]).
|
||||
//! (Ctrl adds, Shift extends over the visible rows), a slower second click on a name starts
|
||||
//! renaming it, hovering a body row lights it up in the viewport, a double-click on a sketch
|
||||
//! edits it, and a right-click opens the item's menu ([`crate::context_menu`]).
|
||||
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::{Container, Surface};
|
||||
@@ -784,7 +784,15 @@ fn compact_tree(ui: &egui::Ui) -> bool {
|
||||
|
||||
/// Where tree rows put their eye this frame (Preferences › Interface, #8718).
|
||||
pub(crate) fn publish_row_prefs(app: &SolveApp, ctx: &egui::Context) {
|
||||
ctx.data_mut(|d| d.insert_temp(egui::Id::unique("sc_tree_eye_left"), app.preferences.browser_eye_left));
|
||||
ctx.data_mut(|d| {
|
||||
d.insert_temp(egui::Id::unique("sc_tree_eye_left"), app.preferences.browser_eye_left);
|
||||
d.insert_temp(egui::Id::unique("sc_tree_dialog_open"), app.dialog.is_some());
|
||||
});
|
||||
}
|
||||
|
||||
/// With a dialog open a second click on a row picks again, so it never waits to rename.
|
||||
fn dialog_open(ui: &egui::Ui) -> bool {
|
||||
ui.ctx().data(|d| d.get_temp::<bool>(egui::Id::unique("sc_tree_dialog_open"))).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn eye_left(ui: &egui::Ui) -> bool {
|
||||
@@ -800,9 +808,69 @@ pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row<'_>) -> RowRes
|
||||
let fold = row.fold.map(|_| Rect::from_min_max(pos2(x - 4.0, r.top()), pos2(x + 15.0, r.bottom())));
|
||||
crate::semantic_content::record_row(ui, (row.label.to_string(), r, fold, row.fold), &out);
|
||||
}
|
||||
let clicked = out.resp.as_ref().is_some_and(|resp| resp.clicked());
|
||||
// Only a plain click: Ctrl and Shift keep adding to and extending the selection.
|
||||
let plain = !ui.input(|i| i.modifiers.command || i.modifiers.ctrl || i.modifiers.shift);
|
||||
let name_click = row.rename && out.clicked && !out.double && plain && !dialog_open(ui);
|
||||
out.slow_rename = slow_rename_click(ui, id, clicked, name_click, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// A second click on the same name later than this is a new gesture, not a rename.
|
||||
const SLOW_RENAME_WINDOW: f64 = 3.0;
|
||||
|
||||
/// A slower second click on a row's name renames it, once the double-click delay has passed
|
||||
/// with no further click: the click is held back until then, so the first click of a
|
||||
/// double-click never starts a rename and the double-click keeps editing or opening the row.
|
||||
/// The held click comes back as `clicked` with `slow_rename` set. A double-click, or a press
|
||||
/// anywhere else, drops it.
|
||||
fn slow_rename_click(ui: &egui::Ui, row: egui::Id, clicked: bool, name_click: bool, out: &mut RowResp) -> bool {
|
||||
let last_key = egui::Id::unique("sc_browser_last_name_click");
|
||||
let pending_key = egui::Id::unique("sc_browser_pending_rename");
|
||||
let now = ui.input(|i| i.time);
|
||||
if !now.is_finite() {
|
||||
return false;
|
||||
}
|
||||
let delay = ui.ctx().options(|options| options.input_options.max_double_click_delay);
|
||||
let pressed = ui.input(|i| i.pointer.any_pressed());
|
||||
// (row, due, made)
|
||||
if let Some((pending_row, due, made)) = ui.ctx().data(|data| data.get_temp::<(egui::Id, f64, f64)>(pending_key)) {
|
||||
let stale = !(due.is_finite() && made.is_finite()) || now > due + SLOW_RENAME_WINDOW;
|
||||
if stale || (pressed && now > made) || (pending_row == row && out.double) {
|
||||
ui.ctx().data_mut(|data| data.remove::<(egui::Id, f64, f64)>(pending_key));
|
||||
} else if pending_row == row {
|
||||
if now >= due {
|
||||
ui.ctx().data_mut(|data| data.remove::<(egui::Id, f64, f64)>(pending_key));
|
||||
out.clicked = true;
|
||||
return true;
|
||||
}
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_secs_f64((due - now).clamp(0.0, SLOW_RENAME_WINDOW)));
|
||||
}
|
||||
}
|
||||
if !clicked {
|
||||
return false;
|
||||
}
|
||||
let previous = ui.ctx().data_mut(|data| {
|
||||
let previous = data.get_persisted::<(egui::Id, f64, bool)>(last_key);
|
||||
data.insert_persisted(last_key, (row, now, name_click));
|
||||
previous
|
||||
});
|
||||
let slow = name_click
|
||||
&& previous.is_some_and(|(previous_row, previous_time, previous_was_name)| {
|
||||
previous_row == row
|
||||
&& previous_was_name
|
||||
&& previous_time.is_finite()
|
||||
&& now - previous_time > delay
|
||||
&& now - previous_time <= SLOW_RENAME_WINDOW
|
||||
});
|
||||
if slow {
|
||||
ui.ctx().data_mut(|data| data.insert_temp(pending_key, (row, now + delay, now)));
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_secs_f64(delay.clamp(0.0, SLOW_RENAME_WINDOW)));
|
||||
out.clicked = false;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// A Ctrl/Shift-aware click on a selectable row.
|
||||
fn click_select(app: &mut SolveApp, ui: &egui::Ui, key: String, sel: Option<Sel>) {
|
||||
let mods = ui.input(|i| i.modifiers);
|
||||
@@ -1010,9 +1078,30 @@ struct Lit {
|
||||
/// The placements holding a lit body, and the components holding a lit sketch or plane.
|
||||
paths: Vec<Vec<u64>>,
|
||||
components: Vec<u64>,
|
||||
/// The feature of what is under the pointer in the 3D view, and of the face or edge picked
|
||||
/// there ([`Lit::features_from_view`]), once worked out this frame.
|
||||
from_view: Option<[Option<u64>; 2]>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// How often the features lit from the 3D view were worked out (the tests count them).
|
||||
static FROM_VIEW_WORKED_OUT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
impl Lit {
|
||||
/// The feature that made what is under the pointer in the 3D view, and the one that made the
|
||||
/// face or edge picked there, as the history lights them. Worked out for the first feature row
|
||||
/// that asks in a frame: every row and folder looked them up again, the pick naming every face
|
||||
/// of its body each time (#14850).
|
||||
fn features_from_view(&mut self, app: &SolveApp) -> [Option<u64>; 2] {
|
||||
*self.from_view.get_or_insert_with(|| {
|
||||
#[cfg(test)]
|
||||
FROM_VIEW_WORKED_OUT.with(|n| n.set(n.get() + 1));
|
||||
[crate::timeline::hovered_feature(app), crate::timeline::picked_feature(app)]
|
||||
})
|
||||
}
|
||||
|
||||
fn of(app: &mut SolveApp) -> Lit {
|
||||
use crate::viewport::Hit;
|
||||
let mut selected = std::collections::HashSet::new();
|
||||
@@ -1104,7 +1193,7 @@ impl Lit {
|
||||
// A body both holding the selection and under the pointer leads to its placement once.
|
||||
paths.sort_unstable();
|
||||
paths.dedup();
|
||||
Lit { selected, holding, hovered, paths, components }
|
||||
Lit { selected, holding, hovered, paths, components, from_view: None }
|
||||
}
|
||||
|
||||
fn any(&self) -> bool {
|
||||
@@ -1848,6 +1937,7 @@ fn component_rows_inner(
|
||||
icon: "component",
|
||||
label: name,
|
||||
name: true,
|
||||
rename: true,
|
||||
selected: app.tree.picked_components.contains(&id),
|
||||
dim: faded,
|
||||
radio: Some(active),
|
||||
@@ -1876,7 +1966,9 @@ fn component_rows_inner(
|
||||
app.session.visibility.hidden_bodies.retain(|b| !bodies.contains(b));
|
||||
}
|
||||
}
|
||||
if r.clicked && !r.double {
|
||||
// With a dialog open a second click picks again: the dialog takes the component's bodies.
|
||||
let rename_here = r.slow_rename && app.dialog.is_none();
|
||||
if r.clicked && !r.double && !rename_here {
|
||||
let names = bodies_of_component(app, id);
|
||||
if !offer_bodies(app, &names, Take::Toggle) {
|
||||
let ctrl = ui.input(|i| i.modifiers.command || i.modifiers.ctrl);
|
||||
@@ -1891,13 +1983,16 @@ fn component_rows_inner(
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.double {
|
||||
if rename_here {
|
||||
start_inline_rename(app, crate::context_menu::RenameWhat::Component(id), &r);
|
||||
} else if r.double {
|
||||
if app.session.doc.parts.contains_key(&id) {
|
||||
crate::dialogs_part_size::open(app, id);
|
||||
} else {
|
||||
crate::edit_in_place::double_click_component(app, json!({ "component": id }));
|
||||
}
|
||||
}
|
||||
inline_rename(app, ui, &r, name);
|
||||
if r.hovered {
|
||||
acts.hover(bodies.iter());
|
||||
}
|
||||
@@ -2017,6 +2112,7 @@ fn occurrence_rows_inner(
|
||||
secondary: &appearance.name,
|
||||
count: &appearance.count,
|
||||
name: true,
|
||||
rename: true,
|
||||
selected,
|
||||
dim: faded || !visible,
|
||||
cut,
|
||||
@@ -2070,17 +2166,22 @@ fn occurrence_rows_inner(
|
||||
if r.double && !takes_components && app.session.doc.parts.contains_key(&placement.component) {
|
||||
crate::dialogs_part_size::open(app, placement.component);
|
||||
}
|
||||
let rename_here = r.slow_rename && app.dialog.is_none();
|
||||
if r.clicked && (takes_components || crate::dialogs_move::picks_components(app)) {
|
||||
if let Some(mut d) = app.dialog.take() {
|
||||
d.pick(&app.session, Sel::Occurrence { path: path.to_vec() });
|
||||
app.dialog = Some(d);
|
||||
}
|
||||
} else if r.clicked && !r.double {
|
||||
} else if r.clicked && !r.double && !rename_here {
|
||||
let names = app.session.occurrence_bodies(path, true).unwrap_or_default();
|
||||
if !offer_bodies(app, &names, Take::Toggle) {
|
||||
click_select(app, ui, key.clone(), Some(Sel::Occurrence { path: path.to_vec() }));
|
||||
}
|
||||
}
|
||||
if rename_here {
|
||||
start_inline_rename(app, crate::context_menu::RenameWhat::Component(placement.component), &r);
|
||||
}
|
||||
inline_rename(app, ui, &r, &placement.label);
|
||||
if r.hovered {
|
||||
let bodies = placement.hover_bodies.get_or_insert_with(|| app.session.occurrence_bodies(path, true).unwrap_or_default());
|
||||
acts.hover(bodies.iter());
|
||||
@@ -2360,7 +2461,7 @@ fn origin_rows_at(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mu
|
||||
pub const STANDARD_VIEWS: [(&str, &str); 4] = [("Top", "top"), ("Front", "front"), ("Right", "right"), ("Home", "home")];
|
||||
|
||||
/// Named Views: the standard views, then the design's saved cameras. A double-click restores
|
||||
/// one (animated).
|
||||
/// one (animated); a slower second click renames a saved view.
|
||||
fn named_view_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut Actions) {
|
||||
let key = "namedviews";
|
||||
let open = is_open(app, key, false);
|
||||
@@ -2388,11 +2489,14 @@ fn named_view_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &m
|
||||
let r = draw_row(
|
||||
ui,
|
||||
ui.scope_id().with(("view", &name)),
|
||||
&Row { depth: depth + 1, icon: "perspective", label: &name, name: true, ..Default::default() },
|
||||
&Row { depth: depth + 1, icon: "perspective", label: &name, name: true, rename: true, ..Default::default() },
|
||||
);
|
||||
if r.double {
|
||||
if r.slow_rename {
|
||||
start_inline_rename(app, crate::context_menu::RenameWhat::View(name.clone()), &r);
|
||||
} else if r.double {
|
||||
restore_view(app, &name);
|
||||
}
|
||||
inline_rename(app, ui, &r, &name);
|
||||
if let Some(p) = r.secondary {
|
||||
acts.menu = Some((p, Target::NamedView { name }));
|
||||
}
|
||||
@@ -2569,6 +2673,7 @@ fn canvas_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut A
|
||||
icon: "canvas",
|
||||
label: &name,
|
||||
name: true,
|
||||
rename: true,
|
||||
selected,
|
||||
dim: !visible || !shown,
|
||||
badges,
|
||||
@@ -2579,23 +2684,30 @@ fn canvas_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut A
|
||||
let _ = app.run("canvas.edit", json!({ "canvas": id, "visible": !visible }));
|
||||
}
|
||||
if r.clicked {
|
||||
if !ui.input(|i| i.modifiers.command || i.modifiers.ctrl) {
|
||||
app.tree.picked_canvases.clear();
|
||||
app.tree.picked_components.clear();
|
||||
app.tree.picked_joints.clear();
|
||||
let _ = app.run("select.clear", json!({}));
|
||||
}
|
||||
if selected {
|
||||
app.tree.picked_canvases.retain(|c| *c != id);
|
||||
if r.slow_rename {
|
||||
app.tree.picked_canvases = vec![id];
|
||||
} else {
|
||||
app.tree.picked_canvases.push(id);
|
||||
if !ui.input(|i| i.modifiers.command || i.modifiers.ctrl) {
|
||||
app.tree.picked_canvases.clear();
|
||||
app.tree.picked_components.clear();
|
||||
app.tree.picked_joints.clear();
|
||||
let _ = app.run("select.clear", json!({}));
|
||||
}
|
||||
if selected {
|
||||
app.tree.picked_canvases.retain(|c| *c != id);
|
||||
} else {
|
||||
app.tree.picked_canvases.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.double && locked {
|
||||
if r.slow_rename {
|
||||
start_inline_rename(app, crate::context_menu::RenameWhat::Canvas(id), &r);
|
||||
} else if r.double && locked {
|
||||
app.set_status(solidcraft_ui_text::tr("browser.canvas_locked"), true);
|
||||
} else if r.double {
|
||||
app.tree.canvas_panel = Some(CanvasPanel::new(app, id, false));
|
||||
}
|
||||
inline_rename(app, ui, &r, &name);
|
||||
if let Some(p) = r.secondary {
|
||||
if !selected {
|
||||
app.tree.picked_canvases = vec![id];
|
||||
@@ -3216,13 +3328,17 @@ fn folder_level(
|
||||
icon: if render { "" } else { "folder" },
|
||||
label,
|
||||
name: true,
|
||||
rename: true,
|
||||
dim: group_faded || (render && !gopen),
|
||||
count: &count,
|
||||
mark: if acts.lit.any() { acts.lit.branch(app, group.entries()) } else { Mark::None },
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
if r.fold {
|
||||
// With a dialog open a second click picks again: the dialog takes the group's bodies.
|
||||
if r.slow_rename && app.dialog.is_none() {
|
||||
start_inline_rename(app, crate::context_menu::RenameWhat::Group(id), &r);
|
||||
} else if r.fold {
|
||||
toggle(app, gkey, start_open);
|
||||
} else if r.clicked {
|
||||
let names: Vec<String> = group.entries().into_iter().flat_map(|e| e.bodies.clone()).collect();
|
||||
@@ -3231,6 +3347,7 @@ fn folder_level(
|
||||
toggle(app, gkey, start_open);
|
||||
}
|
||||
}
|
||||
inline_rename(app, ui, &r, &group.label);
|
||||
if r.eye {
|
||||
toggle_folder(app, gkey);
|
||||
}
|
||||
@@ -3299,20 +3416,25 @@ fn order_before(top: &[BrowserEntry], dragged: &[BrowserEntry], before: Option<&
|
||||
order
|
||||
}
|
||||
|
||||
/// The body's name edited in its row (started by a double-click): Enter or leaving the box keeps
|
||||
/// the name, Escape cancels.
|
||||
fn inline_rename(app: &mut SolveApp, ui: &mut egui::Ui, row: Rect, label_left: f32, e: &Entry) {
|
||||
let editing = app.menu.rename.as_ref().is_some_and(|r| r.inline && matches!(&r.what, crate::context_menu::RenameWhat::Body(b) if *b == e.key));
|
||||
pub(crate) fn start_inline_rename(app: &mut SolveApp, what: crate::context_menu::RenameWhat, row: &RowResp) {
|
||||
if let Some(row_id) = row.row_id {
|
||||
crate::context_menu::start_inline_rename(app, what, row.rect.map(|rect| rect.left_bottom()).unwrap_or_default(), row_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Edit a browser name in its row: Enter or leaving the box keeps the name, Escape cancels.
|
||||
pub(crate) fn inline_rename(app: &mut SolveApp, ui: &mut egui::Ui, row: &RowResp, label: &str) {
|
||||
let (Some(edit_rect), Some(row_id)) = (row.label_rect, row.row_id) else { return };
|
||||
let editing = app.menu.rename.as_ref().is_some_and(|rename| rename.inline && rename.inline_id == Some(row_id));
|
||||
if !editing {
|
||||
return;
|
||||
}
|
||||
let Some(mut rename) = app.menu.rename.take() else { return };
|
||||
let edit_rect = Rect::from_min_max(pos2(label_left, row.top() + 1.0), pos2(row.right() - 3.0, row.bottom() - 1.0));
|
||||
ui.painter().rect_filled(edit_rect, 3.0, Tokens::get().field);
|
||||
let id = ui.scope_id().with(("browser_rename", &e.row_key));
|
||||
let id = ui.scope_id().with(("browser_rename", row_id));
|
||||
let response = crate::ui_kit::inline_name(ui, edit_rect, id, &mut rename.text);
|
||||
crate::panel_runtime::register_content_focus(ui.ctx(), &response);
|
||||
crate::scenario::publish_handle(&format!("rename:{}", e.key), edit_rect.center());
|
||||
crate::scenario::publish_handle(&format!("rename:{label}"), edit_rect.center());
|
||||
let was_focused = rename.focused;
|
||||
if !rename.focused {
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), id) {
|
||||
@@ -3406,6 +3528,7 @@ fn entry_row(
|
||||
kind_tip: e.tip,
|
||||
label: &e.label,
|
||||
name: true,
|
||||
rename: true,
|
||||
selected,
|
||||
dim: !visible || parent_hidden,
|
||||
cut,
|
||||
@@ -3447,11 +3570,19 @@ fn entry_row(
|
||||
if r.eye {
|
||||
set_visible(app, folder, &[e], !visible);
|
||||
}
|
||||
if r.double && folder == "sketches" {
|
||||
if let Ok(id) = e.key.parse() {
|
||||
app.edit_sketch_in(id, path.map(|path| (comp, path)));
|
||||
}
|
||||
} else if r.clicked {
|
||||
if r.double
|
||||
&& folder == "sketches"
|
||||
&& let Ok(id) = e.key.parse()
|
||||
{
|
||||
app.edit_sketch_in(id, path.map(|path| (comp, path)));
|
||||
}
|
||||
// A click a dialog takes as a pick (below) leaves the selection as it was: nothing to rename.
|
||||
// With no dialog open, a construction row is selected like any other and renames.
|
||||
let mut dialog_pick = r.clicked && app.dialog.is_some() && {
|
||||
let shift = ui.input(|i| i.modifiers.shift);
|
||||
folder == "construction" || (folder == "bodies" && !shift)
|
||||
};
|
||||
if r.clicked && !(r.double && folder == "sketches") {
|
||||
let shift = ui.input(|i| i.modifiers.shift);
|
||||
// A body row with a dialog open is a pick, as Combine does (#4452), unless Shift is
|
||||
// asking for the rows between this one and the anchor.
|
||||
@@ -3468,26 +3599,23 @@ fn entry_row(
|
||||
&& pick_sketch_into_dialog(app, id)
|
||||
{
|
||||
// Extrude, Revolve or Emboss took all the sketch's profiles (#6106).
|
||||
dialog_pick = true;
|
||||
} else {
|
||||
click_select(app, ui, e.row_key.clone(), e.sel.clone());
|
||||
}
|
||||
// A double-click renames the row (sketch rows open for editing instead), like F2. A body
|
||||
// is edited in its own row.
|
||||
if r.double {
|
||||
// A double-click or a slower second click renames the row (sketch double-clicks edit
|
||||
// instead), like F2.
|
||||
if (r.double || r.slow_rename) && !dialog_pick {
|
||||
crate::context_menu::rename_selection(app, r.rect.map(|x| x.left_bottom()).unwrap_or_default());
|
||||
if folder == "bodies"
|
||||
&& let Some(rename) = app.menu.rename.as_mut()
|
||||
&& matches!(&rename.what, crate::context_menu::RenameWhat::Body(b) if *b == e.key)
|
||||
if let Some(rename) = app.menu.rename.as_mut()
|
||||
&& let Some(row_id) = r.row_id
|
||||
{
|
||||
rename.inline = true;
|
||||
rename.inline_id = Some(row_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if folder == "bodies"
|
||||
&& let (Some(rect), Some(label_left)) = (r.rect, r.label_left)
|
||||
{
|
||||
inline_rename(app, ui, rect, label_left, e);
|
||||
}
|
||||
inline_rename(app, ui, &r, &e.label);
|
||||
if r.hovered && !e.bodies.is_empty() {
|
||||
acts.hover(e.bodies.iter());
|
||||
}
|
||||
@@ -3868,11 +3996,7 @@ pub fn dims_overlay(app: &SolveApp, ctx: &egui::Context) {
|
||||
DimFrame::Radial { center, .. } | DimFrame::ArcLength { center, .. } | DimFrame::HalfAxis { center, .. } => center,
|
||||
DimFrame::Angular { vertex, .. } => vertex,
|
||||
};
|
||||
let (Some(a), Some(b)) = (to(anchor), to(anchor + Vec2::X)) else { continue };
|
||||
let px = 1.0 / f64::from(a.distance(b)).max(1e-9);
|
||||
if !px.is_finite() {
|
||||
continue;
|
||||
}
|
||||
let Some(px) = crate::dim_view::mm_per_pixel(to, anchor) else { continue };
|
||||
let expr = c.param.as_ref().and_then(|p| app.session.doc.param(p)).map(|p| p.expr.as_str());
|
||||
let col = if c.driven { tk.dimension_driven } else { tk.dimension };
|
||||
let galley = painter.layout_no_wrap(
|
||||
|
||||
@@ -16,6 +16,8 @@ pub(crate) struct Reveal {
|
||||
thread_local! {
|
||||
/// Folded Features folders whose mark was worked out (the tests count them).
|
||||
static MARKS_WORKED_OUT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
/// Feature rows built, on screen or next to it (the tests count them).
|
||||
static ROWS_WORKED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// Open the Features folder that lists this feature and scroll to its row, without changing the
|
||||
@@ -79,7 +81,8 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, component: u64, path:
|
||||
// marker, in place of the timeline.
|
||||
let compact = app.preferences.history_in_browser;
|
||||
let shown = shown_features(app, compact);
|
||||
let has_features = match app.tree.model.as_ref().filter(|m| m.matches(&app.session) && !compact) {
|
||||
let has_features = match current_model(app) {
|
||||
Some(model) if compact => !model.history_features(&app.session, component).is_empty(),
|
||||
Some(model) => model.features.get(&component).is_some_and(|features| features.iter().any(|&index| index < shown)),
|
||||
None => app
|
||||
.session
|
||||
@@ -100,20 +103,30 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, component: u64, path:
|
||||
// Folded, it carries the worst mark of what it holds. A row scrolled out of view paints
|
||||
// nothing, so its mark is not worked out: in a long tree of placements of a part with a long
|
||||
// history, every folder's mark scanned the design's whole history on every frame (#6083).
|
||||
let ids: Vec<u64> = if open || !ui.is_rect_visible(row_rect(ui)) {
|
||||
Vec::new()
|
||||
// With the model, only the folder's own features are read (#14850).
|
||||
let (status, tip) = if open || !ui.is_rect_visible(row_rect(ui)) {
|
||||
(None, None)
|
||||
} else {
|
||||
#[cfg(test)]
|
||||
MARKS_WORKED_OUT.with(|n| n.set(n.get() + 1));
|
||||
app.session
|
||||
.doc
|
||||
.features
|
||||
.iter()
|
||||
.filter(|f| f.component == component && (compact || !matches!(f.kind, FeatureKind::Sketch { .. })))
|
||||
.map(|f| f.id)
|
||||
.collect()
|
||||
match current_model(app) {
|
||||
Some(model) => {
|
||||
let worst = model.folder_status(app, component, compact);
|
||||
(worst.map(|(status, _)| status), worst.and_then(|(_, id)| crate::timeline::feature_message(app, id)))
|
||||
}
|
||||
None => {
|
||||
let ids: Vec<u64> = app
|
||||
.session
|
||||
.doc
|
||||
.features
|
||||
.iter()
|
||||
.filter(|f| f.component == component && (compact || !matches!(f.kind, FeatureKind::Sketch { .. })))
|
||||
.map(|f| f.id)
|
||||
.collect();
|
||||
crate::timeline::worst_status(app, &ids)
|
||||
}
|
||||
}
|
||||
};
|
||||
let (status, tip) = crate::timeline::worst_status(app, &ids);
|
||||
let r = draw_row(
|
||||
ui,
|
||||
ui.scope_id().with(&key),
|
||||
@@ -128,7 +141,8 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, component: u64, path:
|
||||
if !open {
|
||||
return;
|
||||
}
|
||||
let indices = match app.tree.model.as_ref().filter(|m| m.matches(&app.session) && !compact) {
|
||||
let indices = match current_model(app) {
|
||||
Some(model) if compact => model.history_features(&app.session, component).to_vec(),
|
||||
Some(model) => model.features.get(&component).cloned().unwrap_or_default(),
|
||||
None => app
|
||||
.session
|
||||
@@ -143,16 +157,27 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, component: u64, path:
|
||||
let here = Here { component, path, scope: &scope, body_row: None };
|
||||
// The feature that made what is under the cursor or picked in the viewport, as the history
|
||||
// lights it, and in the history layout what a selected history row lights.
|
||||
let from_view = [crate::timeline::hovered_feature(app), crate::timeline::picked_feature(app)];
|
||||
let from_view = acts.lit.features_from_view(app);
|
||||
let lit = if compact { lit_by_selection(app) } else { BTreeSet::new() };
|
||||
let mark = |app: &SolveApp, index: usize| {
|
||||
let id = app.session.doc.features.get(index).map(|f| f.id);
|
||||
if id.is_some_and(|id| lit.contains(&id) || from_view.contains(&Some(id))) { Mark::Here } else { Mark::None }
|
||||
};
|
||||
// Only the rows in view, and one either side, are built; the others only take their place
|
||||
// (#14850). One order key buffer serves every row passed over.
|
||||
let mut window = Window::new(ui);
|
||||
let mut key = String::new();
|
||||
if !compact {
|
||||
for index in indices.into_iter().take_while(|&index| index < shown) {
|
||||
let _ = feature_row(app, ui, acts, &here, index, depth + 1, None, mark(app, index), None);
|
||||
if window.builds() {
|
||||
window.catch_up(ui);
|
||||
let _ = feature_row(app, ui, acts, &here, index, depth + 1, None, mark(app, index), None);
|
||||
window.catch_up(ui);
|
||||
} else {
|
||||
pass_over(app, ui, &here, index, &mut window, &mut key);
|
||||
}
|
||||
}
|
||||
window.catch_up(ui);
|
||||
return;
|
||||
}
|
||||
let n = app.session.doc.features.len();
|
||||
@@ -161,7 +186,7 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, component: u64, path:
|
||||
// `plan` counts in the component's own features; a timeline position is a global index.
|
||||
let global = |at: usize| indices.get(at).copied().unwrap_or_else(|| indices.last().map_or(n, |last| last + 1));
|
||||
// Where the marker can be dropped: the top of each row (it and the sketches listed under it
|
||||
// first rolled back) and below the last.
|
||||
// first rolled back) and below the last. Every row has its slots, built or not.
|
||||
let mut slots: Vec<(f32, usize)> = Vec::new();
|
||||
// Where dragged rows can be dropped, as in the History at the left: these slots and one
|
||||
// before the marker, each with the folder the rows join (#9565).
|
||||
@@ -171,44 +196,163 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, component: u64, path:
|
||||
let mut marker_resp = None;
|
||||
for row in &plan.rows {
|
||||
let index = global(row.index);
|
||||
let drop = DropSlot {
|
||||
y: ui.cursor().min.y,
|
||||
position: global(row.slot.min(row.index)),
|
||||
group: crate::timeline_left::group_for(&app.session.doc.timeline_groups, row.feature),
|
||||
};
|
||||
let drop = DropSlot { y: window.top, position: global(row.slot.min(row.index)), feature: Some(row.feature) };
|
||||
if marker_resp.is_none() && index >= marker {
|
||||
drops.push(drop);
|
||||
// The marker row is always built: its drag must not lose its row.
|
||||
window.catch_up(ui);
|
||||
marker_resp = Some(marker_row(app, ui, depth + 1));
|
||||
window.catch_up(ui);
|
||||
}
|
||||
slots.push((ui.cursor().min.y, global(row.slot)));
|
||||
drops.push(DropSlot { y: ui.cursor().min.y, ..drop });
|
||||
// The same fold as in the vertical History, so it stays open when the layout changes.
|
||||
let key = crate::timeline_left::sketches_key(row.feature);
|
||||
let open = is_open(app, &key, false);
|
||||
let listed: Vec<usize> = row.listed.iter().filter(|listed| !listed.reference).map(|listed| global(listed.index)).collect();
|
||||
let fold = (!listed.is_empty()).then_some(open);
|
||||
if feature_row(app, ui, acts, &here, index, depth + 1, fold, mark(app, index), Some(&mut dragged)) {
|
||||
toggle(app, &key, false);
|
||||
slots.push((window.top, global(row.slot)));
|
||||
drops.push(DropSlot { y: window.top, ..drop });
|
||||
// The same fold as in the vertical History, so it stays open when the layout changes; a
|
||||
// row without sketches listed under it has none.
|
||||
let fold = row.listed.iter().any(|listed| !listed.reference).then(|| is_open(app, &crate::timeline_left::sketches_key(row.feature), false));
|
||||
if window.builds() {
|
||||
window.catch_up(ui);
|
||||
if feature_row(app, ui, acts, &here, index, depth + 1, fold, mark(app, index), Some(&mut dragged)) {
|
||||
toggle(app, &crate::timeline_left::sketches_key(row.feature), false);
|
||||
}
|
||||
window.catch_up(ui);
|
||||
} else {
|
||||
pass_over(app, ui, &here, index, &mut window, &mut key);
|
||||
}
|
||||
if open {
|
||||
for index in listed {
|
||||
if fold != Some(true) {
|
||||
continue;
|
||||
}
|
||||
for listed in row.listed.iter().filter(|listed| !listed.reference) {
|
||||
let index = global(listed.index);
|
||||
if window.builds() {
|
||||
window.catch_up(ui);
|
||||
let _ = feature_row(app, ui, acts, &here, index, depth + 2, None, mark(app, index), Some(&mut dragged));
|
||||
window.catch_up(ui);
|
||||
} else {
|
||||
pass_over(app, ui, &here, index, &mut window, &mut key);
|
||||
}
|
||||
}
|
||||
}
|
||||
let end = global(indices.len());
|
||||
if marker_resp.is_none() {
|
||||
drops.push(DropSlot { y: ui.cursor().min.y, position: end, group: None });
|
||||
drops.push(DropSlot { y: window.top, position: end, feature: None });
|
||||
}
|
||||
window.catch_up(ui);
|
||||
let resp = marker_resp.unwrap_or_else(|| marker_row(app, ui, depth + 1));
|
||||
if !plan.rows.is_empty() {
|
||||
slots.push((ui.cursor().min.y, end));
|
||||
drops.push(DropSlot { y: ui.cursor().min.y, position: end, group: None });
|
||||
drops.push(DropSlot { y: ui.cursor().min.y, position: end, feature: None });
|
||||
}
|
||||
drag_marker(app, ui, &resp, &slots, marker);
|
||||
drag_rows(app, ui, &plan, drag_id, dragged, &drops, resp.rect.center().y);
|
||||
}
|
||||
|
||||
/// The Browser's model of the design as it is now: `None` while it is missing or made for
|
||||
/// another, and the rows look up what they show without it.
|
||||
fn current_model(app: &SolveApp) -> Option<&model::Model> {
|
||||
app.tree.model.as_ref().filter(|model| model.matches(&app.session))
|
||||
}
|
||||
|
||||
/// A run of feature rows laid out without building the rows out of view (#14850). A row is built
|
||||
/// only in the scroll area's visible band or one row either side of it; the others only take
|
||||
/// their space, each run of them in one allocation. Every row's place, the rows after them and
|
||||
/// the scroll height stay as if every row were built.
|
||||
struct Window {
|
||||
/// The top of the next row.
|
||||
top: f32,
|
||||
/// A row's height, and the space after each row.
|
||||
height: f32,
|
||||
gap: f32,
|
||||
/// The visible band, one row wider on either side.
|
||||
band: egui::Rangef,
|
||||
/// The rows passed over whose space is not taken yet.
|
||||
passed: usize,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
fn new(ui: &egui::Ui) -> Self {
|
||||
let row = row_rect(ui);
|
||||
let gap = ui.spacing().item_spacing.y;
|
||||
let clip = ui.clip_rect();
|
||||
let reach = row.height() + gap;
|
||||
Window { top: row.top(), height: row.height(), gap, band: egui::Rangef::new(clip.top() - reach, clip.bottom() + reach), passed: 0 }
|
||||
}
|
||||
|
||||
/// Whether the next row is built: it is in view, or next to it.
|
||||
fn builds(&self) -> bool {
|
||||
self.top <= self.band.max && self.top + self.height >= self.band.min
|
||||
}
|
||||
|
||||
/// Pass over the next row: where it would be.
|
||||
fn pass(&mut self, ui: &egui::Ui) -> Rect {
|
||||
let rect = Rect::from_min_size(pos2(ui.cursor().min.x, self.top), vec2(ui.available_width(), self.height));
|
||||
self.passed += 1;
|
||||
self.top += self.height + self.gap;
|
||||
rect
|
||||
}
|
||||
|
||||
/// Take the space of the rows passed over, and go on from the next row.
|
||||
fn catch_up(&mut self, ui: &mut egui::Ui) {
|
||||
if self.passed > 0 {
|
||||
let rows = self.passed as f32;
|
||||
ui.allocate_space(vec2(ui.available_width(), (rows * self.height + (rows - 1.0) * self.gap).max(0.0)));
|
||||
// As many automatic ids as the rows would have taken: the widgets after them (the
|
||||
// History's marker row) keep theirs while the rows scroll.
|
||||
ui.skip_ahead_auto_ids(self.passed - 1);
|
||||
self.passed = 0;
|
||||
}
|
||||
self.top = ui.cursor().min.y;
|
||||
}
|
||||
}
|
||||
|
||||
/// A feature row out of view, passed over: it takes its place, its order key is remembered (so a
|
||||
/// Shift range across it is whole), and a reveal waiting for it scrolls to where it is.
|
||||
fn pass_over(app: &mut SolveApp, ui: &egui::Ui, here: &Here, index: usize, window: &mut Window, key: &mut String) {
|
||||
let Some(id) = app.session.doc.features.get(index).map(|f| f.id) else { return };
|
||||
let rect = window.pass(ui);
|
||||
write_order_key(here, id, key);
|
||||
app.tree.remember(key);
|
||||
if app.tree.reveal_feature.is_some() || app.tree.timeline_reveal.is_some() {
|
||||
acknowledge_reveals(app, ui, here.path, id, rect);
|
||||
}
|
||||
}
|
||||
|
||||
/// A feature row's key in the Browser's row order, written over `key`.
|
||||
fn write_order_key(here: &Here, id: u64, key: &mut String) {
|
||||
use std::fmt::Write as _;
|
||||
key.clear();
|
||||
let _ = match here.body_row {
|
||||
Some(row) => write!(key, "{row}/s:{id}"),
|
||||
None => write!(key, "f:{id}"),
|
||||
};
|
||||
}
|
||||
|
||||
/// Whether feature `id` made bodies in `placement` ([`solidcraft_engine::Session::feature_bodies`])
|
||||
/// and whether any of them is shown. With the model, their names are worked out once for each row
|
||||
/// drawn rather than from every body of the design on every frame (#14850).
|
||||
fn made_bodies_shown(app: &mut SolveApp, id: u64, placement: Option<(u64, &[u64])>) -> (bool, bool) {
|
||||
let hidden = &app.session.visibility.hidden_bodies;
|
||||
let shown = |names: &[String]| (!names.is_empty(), names.iter().any(|name| !hidden.contains(name)));
|
||||
match app.tree.model.as_mut().filter(|model| model.matches(&app.session)) {
|
||||
Some(model) => shown(model.feature_bodies(id, placement)),
|
||||
None => shown(&app.session.feature_bodies(&[id], placement)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find in Timeline, and with the history in the browser the History bar's own reveal, wait for
|
||||
/// this feature's row at `rect`: scroll to it, until it is on screen.
|
||||
fn acknowledge_reveals(app: &mut SolveApp, ui: &egui::Ui, path: &[u64], id: u64, rect: Rect) {
|
||||
let history = app.preferences.history_in_browser;
|
||||
if app.tree.content.browser_presentation()
|
||||
&& app.tree.reveal_feature.as_ref().is_some_and(|target| target.id == id && target.path == path && target.history == history)
|
||||
&& crate::semantic_content::scroll_acknowledged(ui, rect)
|
||||
{
|
||||
app.tree.reveal_feature = None;
|
||||
}
|
||||
if history && app.tree.timeline_reveal == Some(id) && crate::semantic_content::scroll_acknowledged(ui, rect) {
|
||||
app.tree.timeline_reveal = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The features that built a body, one level in under its row (#11945): the feature rows the
|
||||
/// Features folder draws, without dragging, each lit as there when the view lights it.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -224,11 +368,15 @@ pub(super) fn body_rows(
|
||||
) {
|
||||
let scope = if path.is_empty() { format!("c{component}") } else { occurrence_key(path) };
|
||||
let here = Here { component, path, scope: &scope, body_row: Some(body_row) };
|
||||
let from_view = [crate::timeline::hovered_feature(app), crate::timeline::picked_feature(app)];
|
||||
let from_view = acts.lit.features_from_view(app);
|
||||
// A feature that built several bodies is listed under each: its rows need ids of their own.
|
||||
ui.push_id(("body_features", body_row), |ui| {
|
||||
for id in features {
|
||||
let Some(index) = app.session.doc.features.iter().position(|f| f.id == *id) else { continue };
|
||||
let position = match current_model(app) {
|
||||
Some(model) => model.position(&app.session, *id),
|
||||
None => app.session.doc.features.iter().position(|f| f.id == *id),
|
||||
};
|
||||
let Some(index) = position else { continue };
|
||||
let mark = if from_view.contains(&Some(*id)) { Mark::Here } else { Mark::None };
|
||||
let _ = feature_row(app, ui, acts, &here, index, depth, None, mark, None);
|
||||
}
|
||||
@@ -236,12 +384,13 @@ pub(super) fn body_rows(
|
||||
}
|
||||
|
||||
/// A place in the browser's History where dragged rows can land: its height, the timeline
|
||||
/// position they move to and the folder they join (`None`: the timeline root).
|
||||
/// position they move to and the feature whose folder they join (`None`: the timeline root). The
|
||||
/// folder is looked up only for the slot the rows land on (#14850).
|
||||
#[derive(Clone, Copy)]
|
||||
struct DropSlot {
|
||||
y: f32,
|
||||
position: usize,
|
||||
group: Option<u64>,
|
||||
feature: Option<u64>,
|
||||
}
|
||||
|
||||
/// Where a history row is drawn: its component and placement.
|
||||
@@ -298,6 +447,8 @@ fn feature_row(
|
||||
drag: Option<&mut Option<FeatureDrag>>,
|
||||
) -> bool {
|
||||
let Some(feature) = app.session.doc.features.get(index) else { return false };
|
||||
#[cfg(test)]
|
||||
ROWS_WORKED.with(|n| n.set(n.get() + 1));
|
||||
let (component, path) = (here.component, here.path);
|
||||
let (id, label, icon) = (feature.id, feature.name.clone(), crate::timeline::icon_of(&feature.kind));
|
||||
let is_sketch = matches!(feature.kind, FeatureKind::Sketch { .. });
|
||||
@@ -307,19 +458,22 @@ fn feature_row(
|
||||
// A feature picked by its geometry (a face or edge of it chosen in the view) shows as
|
||||
// selected in the tree, as NX marks the feature of a picked face; the session's selection
|
||||
// stays the face or edge.
|
||||
let by_geometry = !app.session.selection.contains(&Sel::Feature { id }) && crate::timeline::picked_feature(app) == Some(id);
|
||||
let [_, picked] = acts.lit.features_from_view(app);
|
||||
let by_geometry = !app.session.selection.contains(&Sel::Feature { id }) && picked == Some(id);
|
||||
let selected = app.session.selection.contains(&Sel::Feature { id }) || by_geometry;
|
||||
let order_key = here.body_row.map_or_else(|| format!("f:{id}"), |row| format!("{row}/s:{id}"));
|
||||
let mut order_key = String::new();
|
||||
write_order_key(here, id, &mut order_key);
|
||||
app.tree.remember(&order_key);
|
||||
let action = sketch.map(|_| ("sketch", "Edit Profile Sketch"));
|
||||
// A sketch row has the eye its Sketches folder row has. A feature's eye shows and hides the
|
||||
// bodies it made (#5099); a component listed on its own stands for all of its placements.
|
||||
let placement = (component == 0 || !path.is_empty()).then_some((component, path));
|
||||
let bodies = if is_sketch { Vec::new() } else { app.session.feature_bodies(&[id], placement) };
|
||||
let visible =
|
||||
if is_sketch { crate::browser::sketch_visible(app, id) } else { bodies.iter().any(|b| !app.session.visibility.hidden_bodies.contains(b)) };
|
||||
let eye = (is_sketch || !bodies.is_empty()).then_some(visible);
|
||||
let status = crate::timeline::feature_status(app, id);
|
||||
let (has_bodies, visible) = if is_sketch { (false, crate::browser::sketch_visible(app, id)) } else { made_bodies_shown(app, id, placement) };
|
||||
let eye = (is_sketch || has_bodies).then_some(visible);
|
||||
let status = match current_model(app) {
|
||||
Some(model) => model.status(app, id),
|
||||
None => crate::timeline::feature_status(app, id),
|
||||
};
|
||||
let tip = status.and_then(|_| crate::timeline::feature_message(app, id));
|
||||
// A cut feature has a dashed outline until Paste moves it.
|
||||
let cut = app.cut.features.contains(&id);
|
||||
@@ -332,6 +486,8 @@ fn feature_row(
|
||||
eye,
|
||||
icon,
|
||||
label: &label,
|
||||
name: true,
|
||||
rename: true,
|
||||
selected,
|
||||
dim: dim || (!is_sketch && eye == Some(false)),
|
||||
cut,
|
||||
@@ -360,18 +516,12 @@ fn feature_row(
|
||||
let scope = here.scope;
|
||||
if let Some(rect) = r.rect {
|
||||
crate::scenario::publish_handle(&format!("row:browser-feature:{scope}:{id}"), rect.center());
|
||||
let history = app.preferences.history_in_browser;
|
||||
if app.tree.content.browser_presentation()
|
||||
&& app.tree.reveal_feature.as_ref().is_some_and(|target| target.id == id && target.path == path && target.history == history)
|
||||
&& crate::semantic_content::scroll_acknowledged(ui, rect)
|
||||
{
|
||||
app.tree.reveal_feature = None;
|
||||
}
|
||||
if history && app.tree.timeline_reveal == Some(id) && crate::semantic_content::scroll_acknowledged(ui, rect) {
|
||||
app.tree.timeline_reveal = None;
|
||||
}
|
||||
acknowledge_reveals(app, ui, path, id, rect);
|
||||
}
|
||||
if r.double {
|
||||
if r.slow_rename {
|
||||
click_select(app, ui, order_key, Some(Sel::Feature { id }));
|
||||
start_inline_rename(app, crate::context_menu::RenameWhat::Feature(id), &r);
|
||||
} else if r.double {
|
||||
// Inside a placement, the feature's component is active while it is edited, and the one
|
||||
// active before comes back when the editor ends (#12720).
|
||||
app.edit_feature_in(id, Some((component, path)));
|
||||
@@ -380,6 +530,7 @@ fn feature_row(
|
||||
// row, as in the other History layouts (#13170).
|
||||
click_select(app, ui, order_key, Some(Sel::Feature { id }));
|
||||
}
|
||||
inline_rename(app, ui, &r, &label);
|
||||
if r.hovered {
|
||||
acts.hover_feature = Some(id);
|
||||
acts.hovered = true;
|
||||
@@ -468,7 +619,8 @@ fn drag_rows(
|
||||
.and_then(|p| drops.iter().min_by(|a, b| (a.y - p.y).abs().total_cmp(&(b.y - p.y).abs())).copied());
|
||||
if released {
|
||||
if let Some(drop) = slot {
|
||||
crate::timeline_left::drop_items(app, plan, &drag.features, &[], drop.group, drop.position, drop.y < marker_y);
|
||||
let group = drop.feature.and_then(|feature| crate::timeline_left::group_for(&app.session.doc.timeline_groups, feature));
|
||||
crate::timeline_left::drop_items(app, plan, &drag.features, &[], group, drop.position, drop.y < marker_y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -514,6 +666,85 @@ mod tests {
|
||||
drawn_rows().into_iter().map(|(label, ..)| label).collect()
|
||||
}
|
||||
|
||||
/// `n` construction planes, cheap features of the design's own Features folder (its
|
||||
/// Construction folder, which lists them too, folded away): their ids and names.
|
||||
fn planes(h: &mut Harness, n: usize) -> (Vec<u64>, Vec<String>) {
|
||||
let names: Vec<String> = (0..n).map(|i| format!("Plane{i}")).collect();
|
||||
let ids = names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, name)| {
|
||||
let made = h.app.run("construct.plane.offset", json!({"base":"XY","offset":i + 1,"name":name})).unwrap();
|
||||
made["feature"].as_u64().unwrap()
|
||||
})
|
||||
.collect();
|
||||
h.app.tree.collapsed.insert("c0/construction".into());
|
||||
(ids, names)
|
||||
}
|
||||
|
||||
/// The design's Features rows in a scroll area 300 points tall, scrolled `offset` points down:
|
||||
/// the scroll height, the feature rows built in the last pass, and the rows drawn.
|
||||
fn rows_in_view(h: &mut Harness, offset: f32) -> (f32, usize, Vec<crate::semantic_content::DrawnRow>) {
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), ..Default::default() };
|
||||
let mut height = 0.0;
|
||||
h.ctx
|
||||
.run_ui(raw, |ui| {
|
||||
use crate::semantic_content::{self, ContentOwner, ContentRequest};
|
||||
ROWS_WORKED.with(|n| n.set(0));
|
||||
semantic_content::begin(&mut h.app, ui.ctx());
|
||||
semantic_content::render_scope(&mut h.app, ui, ContentOwner::BROWSER, ContentRequest::Browser { shrink_height: false }, |app, ui| {
|
||||
prepare_model(app);
|
||||
let mut actions = Actions::default();
|
||||
let scrolled = egui::ScrollArea::vertical().max_height(300.0).vertical_scroll_offset(offset).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
rows(app, ui, 0, &[], 0, &mut actions);
|
||||
});
|
||||
height = scrolled.content_size.y;
|
||||
actions
|
||||
});
|
||||
semantic_content::finalize(&mut h.app, ui.ctx());
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
(height, ROWS_WORKED.with(std::cell::Cell::get), drawn_rows())
|
||||
}
|
||||
|
||||
/// #14850: in a long Features folder only the rows in view, and one either side, are built.
|
||||
/// The rows passed over still take their place and keep it in the row order, so the scroll
|
||||
/// height, where each row is drawn and the last row are as when every row was built.
|
||||
#[test]
|
||||
fn only_the_feature_rows_in_view_are_built_in_a_long_folder() {
|
||||
let mut h = Harness::new();
|
||||
let (ids, names) = planes(&mut h, 240);
|
||||
h.app.tree.expanded.insert("c0/features".into());
|
||||
let row = crate::ui_kit::TREE_ROW_HEIGHT;
|
||||
// The folder's own row, then a row for each feature.
|
||||
let full = (names.len() + 1) as f32 * row;
|
||||
let in_view = (300.0 / row).ceil() as usize + 1;
|
||||
let (height, worked, drawn) = rows_in_view(&mut h, 0.0);
|
||||
assert_eq!(height, full, "every row takes its place");
|
||||
assert!(worked + 1 >= in_view && worked <= in_view + 1, "{worked} feature rows built, {in_view} rows in view");
|
||||
let order = h.app.tree.content.owner(crate::semantic_content::ContentOwner::BROWSER).unwrap().order.clone();
|
||||
assert_eq!(order, ids.iter().map(|id| format!("f:{id}")).collect::<Vec<_>>(), "every row keeps its place in the row order");
|
||||
let top = drawn.first().map(|(label, rect, ..)| (label.clone(), rect.top())).unwrap();
|
||||
assert_eq!(top.0, "Features");
|
||||
for offset in [full / 2.0 + 7.0, full - 300.0] {
|
||||
let (height, worked, drawn) = rows_in_view(&mut h, offset);
|
||||
assert_eq!(height, full, "scrolled {offset}");
|
||||
assert!(worked <= in_view + 2, "scrolled {offset}: {worked} feature rows built, {in_view} rows in view");
|
||||
assert!(drawn.len() + 1 >= in_view, "scrolled {offset}: {drawn:?}");
|
||||
for (label, rect, ..) in &drawn {
|
||||
let at = names.iter().position(|name| name == label).unwrap() + 1;
|
||||
assert!((rect.top() - (top.1 + at as f32 * row - offset)).abs() < 0.01, "scrolled {offset}: {label} at {rect:?}");
|
||||
}
|
||||
if offset == full - 300.0 {
|
||||
let (label, rect, ..) = drawn.last().unwrap();
|
||||
assert_eq!(label, names.last().unwrap(), "the last row ends the folder");
|
||||
assert!((rect.bottom() - (top.1 + 300.0)).abs() < 0.01, "and the view: {rect:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #5099: a feature row's eye shows and hides the bodies the feature made.
|
||||
#[test]
|
||||
fn a_feature_rows_eye_hides_the_bodies_it_made() {
|
||||
@@ -645,6 +876,53 @@ mod tests {
|
||||
assert_eq!(sorted(&h.app.session.selection), sorted(&all), "Shift+click below the anchor");
|
||||
}
|
||||
|
||||
/// #14850: Shift+click takes every row from the anchor to the row clicked, the rows scrolled
|
||||
/// out of view between them too, which are not built.
|
||||
#[test]
|
||||
fn browser_feature_shift_click_takes_the_rows_scrolled_out_of_view() {
|
||||
let mut h = Harness::new();
|
||||
let (ids, _) = planes(&mut h, 90);
|
||||
h.app.tree.expanded.insert("c0/features".into());
|
||||
h.settle();
|
||||
let row = |id: u64| json!({"handle": format!("row:browser-feature:c0:{id}")});
|
||||
let all: Vec<Sel> = ids.iter().map(|&id| Sel::Feature { id }).collect();
|
||||
let sorted = |s: &[Sel]| {
|
||||
let mut v = s.to_vec();
|
||||
v.sort_by_key(|x| format!("{x:?}"));
|
||||
v
|
||||
};
|
||||
let scroll = |h: &mut Harness, delta: f32| {
|
||||
let card = crate::scenario::rect_of("browser:card").unwrap();
|
||||
h.call("ui.scroll", json!({"x": card.center().x, "y": card.center().y, "delta": delta}));
|
||||
// A wheel's scroll eases in over several frames.
|
||||
h.settle();
|
||||
h.frames(60);
|
||||
};
|
||||
let (first, last) = (ids[0], ids[ids.len() - 1]);
|
||||
// Down: anchor on the first row, scroll to the end, Shift+click the last row.
|
||||
h.step(&json!({"click": row(first)})).unwrap();
|
||||
h.frames(2);
|
||||
scroll(&mut h, -5000.0);
|
||||
ROWS_WORKED.with(|n| n.set(0));
|
||||
h.frame();
|
||||
let worked = ROWS_WORKED.with(std::cell::Cell::get);
|
||||
assert!(worked < ids.len() / 2, "{worked} of {} feature rows built", ids.len());
|
||||
assert!(crate::scenario::handle_at(&format!("row:browser-feature:c0:{first}")).is_none(), "the anchor's row is not built");
|
||||
h.step(&json!({"click": row(last), "shift": true})).unwrap();
|
||||
h.frames(2);
|
||||
assert_eq!(sorted(&h.app.session.selection), sorted(&all), "Shift+click below the anchor, out of view");
|
||||
// Up: anchor on the last row, scroll back to the top, Shift+click the first row. Long
|
||||
// enough after the last click on that row that this one is not a double-click.
|
||||
h.frames(40);
|
||||
h.step(&json!({"click": row(last)})).unwrap();
|
||||
h.frames(2);
|
||||
scroll(&mut h, 5000.0);
|
||||
assert!(crate::scenario::handle_at(&format!("row:browser-feature:c0:{last}")).is_none(), "the anchor's row is not built");
|
||||
h.step(&json!({"click": row(first), "shift": true})).unwrap();
|
||||
h.frames(2);
|
||||
assert_eq!(sorted(&h.app.session.selection), sorted(&all), "Shift+click above the anchor, out of view");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_feature_hover_lights_what_its_timeline_item_lights() {
|
||||
let mut h = Harness::new();
|
||||
@@ -680,6 +958,39 @@ mod tests {
|
||||
assert_eq!(h.app.viewport.hover_feature, Some(round));
|
||||
}
|
||||
|
||||
/// #14850: what a face picked in the view lights in the Features folders (its feature) is
|
||||
/// worked out once a frame, not again for every folder and every row: each time named every
|
||||
/// face of the picked body.
|
||||
#[test]
|
||||
fn a_picked_faces_feature_is_worked_out_once_a_frame() {
|
||||
let mut h = Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.app.ui.show_browser = true;
|
||||
let base = h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Plate"})).unwrap()["feature"].as_u64().unwrap();
|
||||
planes(&mut h, 20);
|
||||
let part = h.app.run("component.create", json!({"name":"Part"})).unwrap()["occurrence"].as_u64().unwrap();
|
||||
h.app.run("solid.box", json!({"length":5,"width":5,"height":5,"corner":[30,0,0],"body_name":"Block"})).unwrap();
|
||||
h.app.run("component.activate", json!({"component":0})).unwrap();
|
||||
let placement = occurrence_key(&[part]);
|
||||
h.app.tree.expanded.extend(["c0/features".to_string(), format!("{placement}/features")]);
|
||||
h.app.tree.open_row(&placement);
|
||||
let state = h.app.session.world_state().unwrap();
|
||||
let faces = solidcraft_engine::doc::naming::face_names(state.body("Plate").unwrap());
|
||||
let index = faces.iter().position(|name| name.starts_with(&format!("F{base}:"))).unwrap();
|
||||
h.app.session.selection = vec![Sel::Face { body: "Plate".into(), index, point: solidcore_record::Vec3::ZERO }];
|
||||
h.frames(2);
|
||||
let passes = h.ctx.cumulative_pass_nr();
|
||||
FROM_VIEW_WORKED_OUT.with(|n| n.set(0));
|
||||
ROWS_WORKED.with(|n| n.set(0));
|
||||
h.frame();
|
||||
let passes = usize::try_from(h.ctx.cumulative_pass_nr() - passes).unwrap();
|
||||
let rows = ROWS_WORKED.with(std::cell::Cell::get);
|
||||
assert!(rows >= 10 * passes, "{rows} feature rows built in {passes} passes");
|
||||
assert_eq!(FROM_VIEW_WORKED_OUT.with(std::cell::Cell::get), passes, "once a frame, for {rows} feature rows");
|
||||
let mut lit = Lit::of(&mut h.app);
|
||||
assert_eq!(lit.features_from_view(&h.app), [None, Some(base)], "the picked face's feature");
|
||||
}
|
||||
|
||||
fn find_in_timeline(app: &mut SolveApp, feature: u64, body: &str) {
|
||||
let state = app.session.world_state().unwrap();
|
||||
let model_body = state.body(body).unwrap();
|
||||
@@ -772,6 +1083,33 @@ mod tests {
|
||||
assert_eq!(h.app.session.selection, [Sel::Feature { id: base }]);
|
||||
}
|
||||
|
||||
/// #14850: Find in Timeline scrolls a long Features folder to a feature far down it. Its row
|
||||
/// is not built until the rows scroll near it; until then the reveal scrolls to where it is.
|
||||
#[test]
|
||||
fn find_in_timeline_scrolls_to_a_feature_far_down_a_long_folder() {
|
||||
let mut h = Harness::new();
|
||||
let (ids, _) = planes(&mut h, 120);
|
||||
let base = h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Plate"})).unwrap()["feature"].as_u64().unwrap();
|
||||
let name = h.app.session.doc.feature(base).unwrap().name.clone();
|
||||
h.frames(2);
|
||||
find_in_timeline(&mut h.app, base, "Plate");
|
||||
ROWS_WORKED.with(|n| n.set(0));
|
||||
h.frame();
|
||||
let worked = ROWS_WORKED.with(std::cell::Cell::get);
|
||||
assert!(worked < ids.len() / 2, "{worked} of {} feature rows built", ids.len() + 1);
|
||||
assert!(h.app.tree.reveal_feature.is_some(), "the row is far below the view");
|
||||
let arrived = (0..240).any(|_| {
|
||||
h.frames(1);
|
||||
h.app.tree.reveal_feature.is_none()
|
||||
});
|
||||
assert!(arrived, "the far row was never scrolled to");
|
||||
h.frames(2);
|
||||
assert!(drawn_rows().iter().any(|(label, ..)| *label == name), "{:?}", drawn_rows());
|
||||
let at = h.call("ui.at", json!({"handle":format!("row:browser-feature:c0:{base}")}));
|
||||
assert!(at["ok"].as_bool().unwrap_or(false), "{at}");
|
||||
assert_eq!(h.app.session.selection, [Sel::Feature { id: base }]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_find_from_a_body_or_its_face_reveals_the_body() {
|
||||
let mut h = Harness::new();
|
||||
@@ -847,7 +1185,8 @@ mod tests {
|
||||
|
||||
/// #6083: in a long tree of placements, only the folded Features folders on screen work out
|
||||
/// their mark (each scanned the design's whole history, every frame); those on screen still
|
||||
/// show it.
|
||||
/// show it. #14850: on a frame with the same model none looks through the history again; an
|
||||
/// error set in place still shows, as the marks read the results as they are.
|
||||
#[test]
|
||||
fn only_folded_features_folders_on_screen_work_out_their_mark() {
|
||||
let mut h = Harness::new();
|
||||
@@ -864,9 +1203,11 @@ mod tests {
|
||||
let result = h.app.session.model.results.iter_mut().find(|r| r.id == feature).unwrap();
|
||||
result.error = Some("the blend is larger than the neighbouring faces".into());
|
||||
MARKS_WORKED_OUT.with(|n| n.set(0));
|
||||
model::PLACES_BUILT.with(|n| n.set(0));
|
||||
h.frame();
|
||||
let worked = MARKS_WORKED_OUT.with(std::cell::Cell::get);
|
||||
assert!(worked > 0 && worked < 40, "{worked} of 81 folded Features folders worked out their mark");
|
||||
assert_eq!(model::PLACES_BUILT.with(std::cell::Cell::get), 0, "the model is the same: the history is not looked through again");
|
||||
assert!(!crate::scenario::handles("status:error:Features").is_empty(), "a folder on screen shows its mark");
|
||||
}
|
||||
|
||||
@@ -1417,6 +1758,54 @@ mod tests {
|
||||
assert_eq!(order(&h), [ids[2], ids[0], ids[1]], "rolling back moves no feature");
|
||||
}
|
||||
|
||||
/// #14850: a long History in the Browser builds only its rows in view, and a row dragged in
|
||||
/// the middle of it, below rows passed over, lands where it is let go; the marker still drags.
|
||||
#[test]
|
||||
fn a_long_browser_history_builds_only_its_rows_in_view_and_still_drags() {
|
||||
let mut h = Harness::new();
|
||||
let (ids, _) = planes(&mut h, 90);
|
||||
h.app.run("view.history_layout", json!({"dock":"browser"})).unwrap();
|
||||
settle_heavy(&mut h);
|
||||
let order = |h: &Harness| h.app.session.doc.features.iter().map(|f| f.id).collect::<Vec<_>>();
|
||||
let handle = |id: u64| format!("row:browser-feature:c0:{id}");
|
||||
ROWS_WORKED.with(|n| n.set(0));
|
||||
h.frame();
|
||||
let worked = ROWS_WORKED.with(std::cell::Cell::get);
|
||||
assert!(worked < ids.len() / 2, "{worked} of {} rows built", ids.len());
|
||||
let remembered = h.app.tree.content.owner(crate::semantic_content::ContentOwner::BROWSER).unwrap().order.clone();
|
||||
assert!(ids.iter().all(|id| remembered.contains(&format!("f:{id}"))), "every row keeps its place in the row order");
|
||||
|
||||
// Scrolled to the middle: the rows above were passed over, the rows in view are built.
|
||||
let card = crate::scenario::rect_of("browser:card").unwrap();
|
||||
h.call("ui.scroll", json!({"x": card.center().x, "y": card.center().y, "delta": -1000}));
|
||||
// A wheel's scroll eases in over several frames.
|
||||
h.settle();
|
||||
h.frames(60);
|
||||
assert!(crate::scenario::handle_at(&handle(ids[0])).is_none(), "the first row is out of view");
|
||||
let drawn: Vec<usize> = drawn_rows()
|
||||
.iter()
|
||||
.filter_map(|(label, rect, ..)| (card.y_range().contains(rect.center().y)).then_some(label))
|
||||
.filter_map(|label| ids.iter().position(|id| h.app.session.doc.feature(*id).is_some_and(|f| f.name == *label)))
|
||||
.collect();
|
||||
let (upper, lower) = (drawn[2], drawn[5]);
|
||||
let to = crate::scenario::handle_at(&handle(ids[upper])).unwrap();
|
||||
h.step(&json!({"drag":[{"handle":handle(ids[lower])},[to.x, to.y - 8.0]]})).unwrap();
|
||||
settle_heavy(&mut h);
|
||||
let mut moved = ids.clone();
|
||||
let row = moved.remove(lower);
|
||||
moved.insert(upper, row);
|
||||
assert_eq!(order(&h), moved, "the row dropped above {upper} moves there");
|
||||
|
||||
// The marker, rolled into view, still drags.
|
||||
h.app.run("timeline.roll_to", json!({"position": lower + 2})).unwrap();
|
||||
settle_heavy(&mut h);
|
||||
let above = crate::scenario::handle_at(&handle(moved[upper + 1])).unwrap();
|
||||
h.step(&json!({"drag":[{"handle":"browser:rollback"},[above.x, above.y - 8.0]]})).unwrap();
|
||||
settle_heavy(&mut h);
|
||||
assert_eq!(h.app.session.doc.marker, Some(upper + 1), "the marker drag rolls back to above that row");
|
||||
assert_eq!(order(&h), moved, "rolling back moves no feature");
|
||||
}
|
||||
|
||||
/// #11945: with Features under bodies on, a body row folds open on the features that built it,
|
||||
/// in timeline order and only those; a click on one selects it. Off, the default, body rows
|
||||
/// have no fold arrow and list nothing.
|
||||
|
||||
@@ -296,9 +296,9 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
_ => Paint::One,
|
||||
};
|
||||
let changes_of = c.colored_property().filter(|n| c.complex.integer(n)).map(str::to_string);
|
||||
let at = panel_pos(app);
|
||||
let (pivot, at) = crate::shell::floating_editor_anchor(app, ctx).unwrap_or_else(|| (egui::Align2::LEFT_TOP, panel_pos(app)));
|
||||
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_point_cloud_panel"), "point", &title)
|
||||
.placed(egui::Align2::LEFT_TOP, at)
|
||||
.placed(pivot, at)
|
||||
.fields()
|
||||
.actions("", solidcraft_ui_text::tr("ui.close"), None)
|
||||
.show(ctx, true, |ui| {
|
||||
|
||||
@@ -32,7 +32,7 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
"browser.edit_canvas_title"
|
||||
};
|
||||
let title = solidcraft_ui_text::format(title_id, &[("name", &c.name)]);
|
||||
let at = panel_pos(app);
|
||||
let (pivot, at) = crate::shell::floating_editor_anchor(app, ctx).unwrap_or_else(|| (egui::Align2::LEFT_TOP, panel_pos(app)));
|
||||
let (picks, straighten, name) = (m.picks_points(), m.straighten, c.id);
|
||||
let (ok, cancel) = if straighten {
|
||||
(solidcraft_ui_text::tr("ui.straighten"), solidcraft_ui_text::tr("ui.cancel"))
|
||||
@@ -42,7 +42,7 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
("", solidcraft_ui_text::tr("ui.close"))
|
||||
};
|
||||
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_canvas_panel"), "canvas", &title)
|
||||
.placed(egui::Align2::LEFT_TOP, at)
|
||||
.placed(pivot, at)
|
||||
.fields()
|
||||
.actions(ok, cancel, None)
|
||||
.show(ctx, true, |ui| {
|
||||
|
||||
@@ -118,14 +118,13 @@ fn a_refused_inline_rename_keeps_the_editor_and_shows_the_error() {
|
||||
h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"B"})).unwrap();
|
||||
h.app.preferences.browser_bodies_folders = crate::prefs::BodiesFolders::Always;
|
||||
h.settle();
|
||||
h.app.menu.rename = Some(crate::context_menu::Rename {
|
||||
what: crate::context_menu::RenameWhat::Body("B".into()),
|
||||
text: "A".into(),
|
||||
at: egui::Pos2::ZERO,
|
||||
focused: true,
|
||||
inline: true,
|
||||
});
|
||||
h.frames(2);
|
||||
// Open B's name for editing in its row (a delayed second click) and type a taken name.
|
||||
h.step(&json!({"click":{"handle":"row:B"}})).unwrap();
|
||||
h.frames(30);
|
||||
h.step(&json!({"click":{"handle":"row:B"}})).unwrap();
|
||||
h.frames(30);
|
||||
assert!(h.app.menu.rename.as_ref().is_some_and(|r| r.inline), "B's row is being renamed");
|
||||
h.step(&json!({"text":"A"})).unwrap();
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
h.frames(2);
|
||||
assert!(h.app.menu.rename.is_some(), "the editor stays open after a refused rename");
|
||||
@@ -134,6 +133,36 @@ fn a_refused_inline_rename_keeps_the_editor_and_shows_the_error() {
|
||||
assert!(h.app.session.world_state().unwrap().body("B").is_some(), "the body keeps its name");
|
||||
}
|
||||
|
||||
/// Renaming a component to a name another component has takes the next free number; a name
|
||||
/// nobody has stays as typed.
|
||||
#[test]
|
||||
fn a_component_renamed_to_a_taken_name_takes_the_next_number() {
|
||||
let mut h = Harness::new();
|
||||
let id = |v: Value| v["component"].as_u64().unwrap();
|
||||
let a = id(h.app.run("component.create", json!({"name": "Bracket"})).unwrap());
|
||||
h.app.run("component.activate", json!({"component": 0})).unwrap();
|
||||
let b = id(h.app.run("component.create", json!({"name": "Plate"})).unwrap());
|
||||
let name_of = |h: &Harness, c: u64| h.app.session.doc.components.iter().find(|k| k.id == c).map(|k| k.name.clone()).unwrap();
|
||||
let rename = |h: &mut Harness, c: u64, text: &str| {
|
||||
let r = crate::context_menu::Rename {
|
||||
what: crate::context_menu::RenameWhat::Component(c),
|
||||
text: text.into(),
|
||||
at: egui::Pos2::ZERO,
|
||||
focused: false,
|
||||
inline: true,
|
||||
inline_id: None,
|
||||
};
|
||||
crate::context_menu::commit_rename(&mut h.app, &r).unwrap();
|
||||
};
|
||||
rename(&mut h, b, "Bracket");
|
||||
assert_eq!(name_of(&h, b), "Bracket (2)");
|
||||
assert_eq!(name_of(&h, a), "Bracket", "the other keeps its name");
|
||||
rename(&mut h, b, "Bracket (2)");
|
||||
assert_eq!(name_of(&h, b), "Bracket (2)", "its own name is not taken");
|
||||
rename(&mut h, a, "Base");
|
||||
assert_eq!(name_of(&h, a), "Base");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folder_visibility_target_clears_when_the_user_moves_on() {
|
||||
for action in ["body click", "selection command", "empty click", "escape", "design tab", "model edit", "tool"] {
|
||||
@@ -268,6 +297,88 @@ fn browser_body_clicks_respect_the_active_dialog_input() {
|
||||
assert!(h.app.session.selection.is_empty(), "an incompatible pick does not leak into the global selection");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_name_rows_rename_on_a_delayed_second_click() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"One"})).unwrap();
|
||||
h.app.tree.expanded.insert("c0/features".into());
|
||||
h.settle();
|
||||
|
||||
h.step(&json!({"click":{"handle":"row:One"}})).unwrap();
|
||||
assert!(h.app.menu.rename.is_none(), "the first click only selects the body");
|
||||
h.frames(30);
|
||||
h.step(&json!({"click":{"handle":"row:One"}})).unwrap();
|
||||
h.frames(30);
|
||||
assert!(matches!(
|
||||
h.app.menu.rename.as_ref(),
|
||||
Some(crate::context_menu::Rename { what: crate::context_menu::RenameWhat::Body(name), inline: true, .. }) if name == "One"
|
||||
));
|
||||
|
||||
h.step(&json!({"key":"Escape"})).unwrap();
|
||||
let feature = h.app.session.doc.features.first().map(|feature| feature.id).unwrap();
|
||||
let feature_row = format!("row:browser-feature:c0:{feature}");
|
||||
h.step(&json!({"click":{"handle":feature_row}})).unwrap();
|
||||
assert!(h.app.menu.rename.is_none(), "the first click only selects the feature");
|
||||
h.frames(30);
|
||||
h.step(&json!({"click":{"handle":feature_row}})).unwrap();
|
||||
h.frames(30);
|
||||
assert!(matches!(
|
||||
h.app.menu.rename.as_ref(),
|
||||
Some(crate::context_menu::Rename { what: crate::context_menu::RenameWhat::Feature(id), inline: true, .. }) if *id == feature
|
||||
));
|
||||
let feature_name = h.app.session.doc.feature(feature).map(|feature| feature.name.clone()).unwrap_or_default();
|
||||
assert!(crate::scenario::handle_at(&format!("rename:{feature_name}")).is_some(), "the feature name is edited in its row");
|
||||
h.step(&json!({"text":"Renamed feature"})).unwrap();
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
assert_eq!(h.app.session.doc.feature(feature).map(|feature| feature.name.as_str()), Some("Renamed feature"));
|
||||
}
|
||||
|
||||
/// With no dialog open a construction row is selected like any other row, so a delayed second
|
||||
/// click renames it.
|
||||
#[test]
|
||||
fn a_construction_row_renames_on_a_delayed_second_click() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("construct.plane.offset", json!({"base":"XY","offset":20})).unwrap();
|
||||
h.app.tree.expanded.insert("c0/construction".into());
|
||||
h.settle();
|
||||
let feature = h.app.session.doc.features.last().map(|feature| feature.id).unwrap();
|
||||
let name = h.app.session.doc.feature(feature).map(|feature| feature.name.clone()).unwrap_or_default();
|
||||
let row = format!("row:{name}");
|
||||
h.step(&json!({"click":{"handle":row}})).unwrap();
|
||||
assert!(h.app.menu.rename.is_none(), "the first click only selects the plane");
|
||||
h.frames(30);
|
||||
h.step(&json!({"click":{"handle":row}})).unwrap();
|
||||
h.frames(30);
|
||||
assert!(
|
||||
matches!(h.app.menu.rename.as_ref(), Some(crate::context_menu::Rename { inline: true, .. })),
|
||||
"{:?}",
|
||||
h.app.menu.rename.as_ref().map(|r| &r.what)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_sketch_rename_stays_inline() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane":"XY","name":"Profile"})).unwrap();
|
||||
h.app.run("sketch.finish", json!({})).unwrap();
|
||||
h.app.tree.expanded.insert("c0/sketches".into());
|
||||
h.settle();
|
||||
|
||||
h.step(&json!({"click":{"handle":"row:Profile"}})).unwrap();
|
||||
assert!(h.app.menu.rename.is_none(), "the first click only selects the sketch");
|
||||
h.frames(30);
|
||||
h.step(&json!({"click":{"handle":"row:Profile"}})).unwrap();
|
||||
h.frames(30);
|
||||
assert!(matches!(
|
||||
h.app.menu.rename.as_ref(),
|
||||
Some(crate::context_menu::Rename { what: crate::context_menu::RenameWhat::Feature(_), inline: true, .. })
|
||||
));
|
||||
assert!(crate::scenario::handle_at("rename:Profile").is_some(), "the sketch name is edited in its row");
|
||||
h.step(&json!({"text":"Profile renamed"})).unwrap();
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
assert_eq!(h.app.session.doc.features.first().map(|feature| feature.name.as_str()), Some("Profile renamed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_sketches_after_history_marker_are_not_listed() {
|
||||
let mut h = Harness::new();
|
||||
@@ -2220,6 +2331,33 @@ fn parked_reveals_are_pruned_against_their_own_document_when_the_target_is_remov
|
||||
assert_no_pending_reveals(&h);
|
||||
}
|
||||
|
||||
/// #7533: the first click of a double-click that comes a little after a click on the same name
|
||||
/// does not rename it; the double-click edits the feature, and a rename waits out the
|
||||
/// double-click delay.
|
||||
#[test]
|
||||
fn a_double_click_after_a_slow_click_edits_instead_of_renaming() {
|
||||
let mut h = Harness::new();
|
||||
let feature = h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"One"})).unwrap()["feature"].as_u64().unwrap();
|
||||
h.app.tree.expanded.insert("c0/features".into());
|
||||
h.settle();
|
||||
let row = json!({"handle": format!("row:browser-feature:c0:{feature}")});
|
||||
h.step(&json!({"click": row})).unwrap();
|
||||
h.frames(30);
|
||||
h.step(&json!({"click": row})).unwrap();
|
||||
assert!(h.app.menu.rename.is_none(), "the rename waits out the double-click delay");
|
||||
h.step(&json!({"click": row, "double": true})).unwrap();
|
||||
h.frames(30);
|
||||
assert!(h.app.menu.rename.is_none(), "a double-click drops the held rename");
|
||||
assert!(h.app.dialog.as_ref().is_some_and(|d| d.editing.is_some_and(|(id, _)| id == feature)), "the double-click edits the feature");
|
||||
crate::dialogs::cancel(&mut h.app);
|
||||
h.frames(40);
|
||||
// A double-click a while after an earlier double-click edits again.
|
||||
h.step(&json!({"click": row, "double": true})).unwrap();
|
||||
h.frames(2);
|
||||
assert!(h.app.menu.rename.is_none());
|
||||
assert!(h.app.dialog.as_ref().is_some_and(|d| d.editing.is_some_and(|(id, _)| id == feature)), "the second double-click edits too");
|
||||
}
|
||||
|
||||
/// A component's body placed twice is two body rows in the Browser; the header's count agrees
|
||||
/// with the rows it lists instead of counting the model's bodies ("1 body" over two rows).
|
||||
#[test]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user