mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8537f1c658 | |||
| ded10b52da | |||
| 4bc5bdc791 | |||
| 1baaebd3ef | |||
| 97e467f2d1 | |||
| 665ff84495 | |||
| 07b3b92703 | |||
| ddf56e1246 | |||
| 0ddbe87935 | |||
| 19dc8b6881 | |||
| 3e2e229dd1 | |||
| c8d9a4bfdd | |||
| 6c764dce06 | |||
| 655e771455 | |||
| 15f74b7981 | |||
| 9b607f6f1f |
@@ -2072,6 +2072,8 @@
|
||||
"prefs.view_confirm_corner": "موافق وإلغاء في زاوية العرض",
|
||||
"prefs.view_confirm_corner_note": "أثناء فتح مربع حوار أو رسم، تؤدي علامة الصح وعلامة الإكس أعلى يمين العرض ما يؤديه زرّا موافق وإلغاء في مربع الحوار، أو تنهي الرسم وتلغيه. وينتقل متصفح العرض إلى أسفلهما.",
|
||||
"prefs.viewport": "منطقة العرض",
|
||||
"prefs.viewport_analysis_status": "إظهار حالة التحليل في منفذ العرض",
|
||||
"prefs.viewport_analysis_status_help": "عرض التحليلات النشطة أعلى منفذ العرض وفتح مفتاح كل تحليل من عنصره. ينهي Esc أحدث تحليل. أوقف الخيار لاستعادة المفاتيح في الزاوية وسلوك Esc السابق.",
|
||||
"prefs.viewport_background": "الخلفية",
|
||||
"prefs.viewport_background_colour": "لون",
|
||||
"prefs.viewport_background_theme": "تدرج السمة",
|
||||
@@ -4646,6 +4648,8 @@
|
||||
"ui.view_centre": "مركز العرض",
|
||||
"ui.viewport_status_clear": "إيقاف {name}",
|
||||
"ui.viewport_status_clear_hint": "إيقاف {name} · Esc",
|
||||
"ui.viewport_status_edges": "{count} حواف",
|
||||
"ui.viewport_status_from": "من {direction}",
|
||||
"ui.viewport_status_key": "إظهار مفتاح الألوان لـ {name}",
|
||||
"ui.viewport_status_minimum_radius": "أقل نصف قطر",
|
||||
"ui.viewport_status_pairs": "{count} أزواج",
|
||||
|
||||
@@ -1942,6 +1942,8 @@
|
||||
"prefs.trackpad_zoom_modifier.ctrl": "Strg",
|
||||
"prefs.trackpad_zoom_modifier.help": "Nur diese Taste gedrückt halten, um durch vertikales Scrollen am Zeiger zu zoomen. Aus behält das bisherige Scrollverhalten bei.",
|
||||
"prefs.trackpad_zoom_modifier.off": "Aus",
|
||||
"prefs.viewport_analysis_status": "Analysestatus im Ansichtsfenster anzeigen",
|
||||
"prefs.viewport_analysis_status_help": "Zeigt aktive Analysen über dem Ansichtsfenster und ihre Legenden am jeweiligen Eintrag. Esc beendet die neueste Analyse. Ausschalten stellt Ecklegenden und das bisherige Esc-Verhalten wieder her.",
|
||||
"prefs.viewport_background": "Hintergrund",
|
||||
"prefs.viewport_background_colour": "Farbe",
|
||||
"prefs.viewport_background_theme": "Verlauf des Designs",
|
||||
@@ -3939,6 +3941,8 @@
|
||||
"ui.view_centre": "Ansichtszentrum",
|
||||
"ui.viewport_status_clear": "{name} ausschalten",
|
||||
"ui.viewport_status_clear_hint": "{name} ausschalten · Esc",
|
||||
"ui.viewport_status_edges": "{count} Kanten",
|
||||
"ui.viewport_status_from": "aus {direction}",
|
||||
"ui.viewport_status_key": "Legende für {name} anzeigen",
|
||||
"ui.viewport_status_minimum_radius": "Mindestradius",
|
||||
"ui.viewport_status_pairs": "{count} Paare",
|
||||
|
||||
@@ -2072,6 +2072,8 @@
|
||||
"prefs.view_confirm_corner": "OK and Cancel in the view's corner",
|
||||
"prefs.view_confirm_corner_note": "While a dialog or a sketch is open, a check and a cross at the top right of the view do what the dialog's OK and Cancel do, or finish and cancel the sketch. The view navigator moves down below them.",
|
||||
"prefs.viewport": "Viewport",
|
||||
"prefs.viewport_analysis_status": "Show viewport analysis status",
|
||||
"prefs.viewport_analysis_status_help": "Show active analyses above the viewport, open their legends from each item, and use Esc to turn off the newest analysis. Turn off to restore corner legends and the previous Esc behaviour.",
|
||||
"prefs.viewport_background": "Background",
|
||||
"prefs.viewport_background_colour": "Color",
|
||||
"prefs.viewport_background_theme": "Theme's gradient",
|
||||
@@ -4646,6 +4648,8 @@
|
||||
"ui.view_centre": "View center",
|
||||
"ui.viewport_status_clear": "Turn off {name}",
|
||||
"ui.viewport_status_clear_hint": "Turn off {name} · Esc",
|
||||
"ui.viewport_status_edges": "{count} edges",
|
||||
"ui.viewport_status_from": "from {direction}",
|
||||
"ui.viewport_status_key": "Show key for {name}",
|
||||
"ui.viewport_status_minimum_radius": "Minimum radius",
|
||||
"ui.viewport_status_pairs": "{count} pairs",
|
||||
|
||||
@@ -2072,6 +2072,8 @@
|
||||
"prefs.view_confirm_corner": "אישור וביטול בפינת התצוגה",
|
||||
"prefs.view_confirm_corner_note": "כשתיבת דו־שיח או סקיצה פתוחות, סימן וי וסימן איקס בפינה הימנית העליונה של התצוגה עושים את מה שעושים אישור וביטול של תיבת הדו־שיח, או מסיימים ומבטלים את הסקיצה. ניווט התצוגה זז אל מתחתיהם.",
|
||||
"prefs.viewport": "אזור התצוגה",
|
||||
"prefs.viewport_analysis_status": "הצגת מצב ניתוח בחלון התצוגה",
|
||||
"prefs.viewport_analysis_status_help": "הצגת ניתוחים פעילים מעל חלון התצוגה ופתיחת המקרא מכל פריט. Esc מכבה את הניתוח האחרון. כיבוי האפשרות משחזר את המקרא בפינה ואת פעולת Esc הקודמת.",
|
||||
"prefs.viewport_background": "רקע",
|
||||
"prefs.viewport_background_colour": "צבע",
|
||||
"prefs.viewport_background_theme": "מעבר הצבעים של ערכת הנושא",
|
||||
@@ -4646,6 +4648,8 @@
|
||||
"ui.view_centre": "מרכז המבט",
|
||||
"ui.viewport_status_clear": "כיבוי {name}",
|
||||
"ui.viewport_status_clear_hint": "כיבוי {name} · Esc",
|
||||
"ui.viewport_status_edges": "{count} קצוות",
|
||||
"ui.viewport_status_from": "מכיוון {direction}",
|
||||
"ui.viewport_status_key": "הצגת מקרא עבור {name}",
|
||||
"ui.viewport_status_minimum_radius": "רדיוס מינימלי",
|
||||
"ui.viewport_status_pairs": "{count} זוגות",
|
||||
|
||||
@@ -775,6 +775,8 @@
|
||||
"at the face": "na licu",
|
||||
"bom.group_children": "Podređeni dijelovi u retku svojeg dijela",
|
||||
"browser.body_name": "Tijelo{number}",
|
||||
"browser.calibrate_click_a": "Kliknite sliku kako biste zadali točku A ili upišite njezine koordinate.",
|
||||
"browser.calibrate_click_b": "Kliknite sliku kako biste zadali točku B ili upišite njezine koordinate.",
|
||||
"browser.calibrate_title": "Kalibriraj: {name}",
|
||||
"browser.capture_many": "Pomaknute komponente bez zabilježenog položaja: {count}.",
|
||||
"browser.capture_note": "Zabilježi zadržava novi položaj; Vrati ga vraća nazad.",
|
||||
@@ -1399,6 +1401,16 @@
|
||||
"help.occurrence.move": "Pomiče dio sklopa samo u stupnjevima slobode koje dopuštaju njegovi spojevi. Odaberite lice dijela, a zatim povucite dostupnu strelicu ili luk.",
|
||||
"help.parameters.change": "Otvara tablicu imenovanih vrijednosti i izraza koji upravljaju kotama dizajna. Upotrijebite je kako biste promijenili mjeru na jednom mjestu, a sve značajke koje je koriste je slijede.",
|
||||
"help.pointcloud.insert": "Umetnite PLY, XYZ ili PTS skeniranje kao oblak točaka uz model. Koristite ga kao referencu pri izradi modela iz izmjerenih podataka.",
|
||||
"help.sketch.construction_line": "Izradite beskonačni pomoćni pravac kroz dvije točke. Upotrijebite ga kako biste rasporedili skicu bez razdvajanja profila.",
|
||||
"help.sketch.construction_line.angle": "Izradite beskonačni pomoćni pravac pod kutom od osi X skice. Odaberite točku i upišite kut ili odaberite smjer.",
|
||||
"help.sketch.construction_line.bisector": "Odaberite dvije ravne linije skice koje se sijeku, pa jednu od dviju simetrala kuta. Rezultat prati izvorne linije.",
|
||||
"help.sketch.construction_line.horizontal": "Izradite beskonačni vodoravni pomoćni pravac kroz točku. Njegovo ograničenje vodoravnosti ostaje na snazi pri kasnijim promjenama.",
|
||||
"help.sketch.construction_line.parallel": "Odaberite ravnu liniju skice, pa postavite paralelni beskonačni pomoćni pravac. Desnim klikom odaberite drugu referencu.",
|
||||
"help.sketch.construction_line.perpendicular": "Odaberite ravnu liniju skice, pa postavite beskonačni pomoćni pravac okomit na nju. Desnim klikom odaberite drugu referencu.",
|
||||
"help.sketch.construction_line.tangent_at": "Izradite tangentu u točki na kružnici ili kružnom luku, ili u krajnjoj točki otvorenog splinea ili konike. Odaberite krivulju, pa točku dodira.",
|
||||
"help.sketch.construction_line.tangent_external": "Odaberite kružnicu ili kružni luk, pa vanjsku točku i rješenje tangente. Pregledi prikazuju samo dodirne točke na nacrtanom luku.",
|
||||
"help.sketch.construction_line.tangent_two": "Odaberite dvije kružnice ili kružna luka, pa u pregledu odaberite zajedničku tangentu. Dodirne točke moraju ležati na nacrtanim krivuljama.",
|
||||
"help.sketch.construction_line.vertical": "Izradite beskonačni okomiti pomoćni pravac kroz točku. Njegovo ograničenje okomitosti ostaje na snazi pri kasnijim promjenama.",
|
||||
"help.sketch.equation_curve": "Crta krivulju prema formulama: x(t) i y(t) ili y = f(x), u zadanom rasponu. Formule mogu koristiti parametre i krivulja ih prati: upotrijebite je za sinusoidu, spiralu ili evolventu zuba zupčanika.",
|
||||
"help.solid.align": "Pomiče odabrana tijela tako da se točka, lice ili ravnina na njima poklopi s točkom, licem ili ravninom negdje drugdje. Upotrijebite ga kako biste postavili dio uz drugi bez mjerenja odmaka.",
|
||||
"help.solid.chamfer": "Odsijeca odabrane bridove ravnim skošenjem zadane udaljenosti. Koristite ga kako biste ublažili oštre bridove ili olakšali uvođenje zatika ili vijka.",
|
||||
@@ -1935,6 +1947,8 @@
|
||||
"prefs.origin_plane_names": "Nazivi ravnina ishodišta",
|
||||
"prefs.origin_plane_names_note": "Nazivi prikazani u stablu modela, dijalozima i popisima. {top} je XY, {front} je XZ, a {right} je YZ, sa Z prema gore. Naredbe, skripte i spremljeni dizajni zadržavaju XY, XZ i YZ.",
|
||||
"prefs.overall_speed": "Ukupna brzina",
|
||||
"prefs.persistent_ribbon_pressed": "Istakni aktivne naredbe na vrpci",
|
||||
"prefs.persistent_ribbon_pressed_help": "Ističe naredbe na vrpci dok su njihovi efekti uključeni. Isključite kako biste zadržali prethodni neutralni izgled i pomoć za naredbe.",
|
||||
"prefs.perspective_fov": "Vidno polje",
|
||||
"prefs.placement": "Postavljanje",
|
||||
"prefs.prefer_free_sketch_points": "Daj prednost pomičnim točkama skice kad se točke preklapaju",
|
||||
@@ -1984,11 +1998,14 @@
|
||||
"prefs.sketch_align_points_note": "Tijekom crtanja točka se istodobno poravnava vodoravno s jednom točkom skice i okomito s drugom, uz točkastu vodilicu, a klik zadržava oba poravnanja. Isključeno: samo s prethodnom točkom, po jedna os.",
|
||||
"prefs.sketch_conflict_colour": "Geometrija u sukobu u boji sukoba",
|
||||
"prefs.sketch_conflict_colour_note": "Dok su ograničenja skice u sukobu, krivulje i točke koje drži neuspjelo ograničenje crtaju se i u boji sukoba, a ne samo njegov simbol ili kota. Isključeno: zadržavaju uobičajene boje.",
|
||||
"prefs.sketch_construction_lines": "Prošireni alati za pomoćne pravce",
|
||||
"prefs.sketch_exit_view": "Kad se skica završi",
|
||||
"prefs.sketch_exit_view.keep": "Zadrži trenutni prikaz",
|
||||
"prefs.sketch_exit_view.previous": "Vrati se na prethodni prikaz",
|
||||
"prefs.sketch_exit_view.unless_rotated": "Vrati se osim ako je rotirano",
|
||||
"prefs.sketch_exit_view_note": "Kamo prikaz ide nakon Završi skicu, Odustani ili dvostrukog Esc.",
|
||||
"prefs.sketch_open_look_at": "Okreni kameru prema skici pri otvaranju",
|
||||
"prefs.sketch_open_look_at_note": "Isključeno: kad izradite ili uredite skicu, prikaz ostaje onakav kakav jest. Pogledaj okreće prikaz prema skici kad to želite.",
|
||||
"prefs.sketch_recent_snap": "Automatski privlači na nedavna središta i središnje točke",
|
||||
"prefs.sketch_recent_snap_note": "Tijekom crtanja pokazivač se privlači ondje gdje se sijeku vodilice dvaju posljednjih središta kružnica ili središnjih točaka ravnih bridova. Isključeno: te vodilice služe samo za prikaz i pokazivač se ne pomiče.",
|
||||
"prefs.sketch_selection": "Skica i odabir",
|
||||
@@ -2064,6 +2081,10 @@
|
||||
"properties.title": "Svojstva: {name}",
|
||||
"recovery.days_ago": "prije {count} dana",
|
||||
"recovery.detail": "{file} · značajki: {features} · automatski spremljeno {when}",
|
||||
"recovery.discard_all": "Odbaci sve",
|
||||
"recovery.discard_all_detail": "Ovo se ne može poništiti.",
|
||||
"recovery.discard_all_title": "Odbaciti automatska spremanja ({count})?",
|
||||
"recovery.discard_all_title_one": "Odbaciti 1 automatsko spremanje?",
|
||||
"recovery.hours_ago": "prije {count} h",
|
||||
"recovery.just_now": "upravo",
|
||||
"recovery.min_ago": "prije {count} min",
|
||||
@@ -3983,6 +4004,14 @@
|
||||
"ui.constant": "Konstantno",
|
||||
"ui.constraints_conflict": "Ograničenja su u sukobu",
|
||||
"ui.construction": "Pomoćna",
|
||||
"ui.construction.angle_hint": "Pomoćni pravac: kliknite točku, pa upišite kut ili odaberite smjer",
|
||||
"ui.construction.bisector_hint": "Simetrala: odaberite dvije ravne linije koje se sijeku, pa željeno rješenje",
|
||||
"ui.construction.common_hint": "Zajednička tangenta: odaberite dvije kružnice ili dva luka, pa željeno rješenje",
|
||||
"ui.construction.contact_hint": "Tangenta: odaberite kružnicu/luk ili otvoreni spline/koniku, pa točku dodira (krivulje slobodnog oblika: krajnje točke)",
|
||||
"ui.construction.external_hint": "Tangenta: odaberite kružnicu ili luk, vanjsku točku, pa željeno rješenje",
|
||||
"ui.construction.free_hint": "Pomoćni pravac: kliknite dvije točke; desnim klikom počnite ispočetka",
|
||||
"ui.construction.point_hint": "Pomoćni pravac: kliknite točku kroz koju prolazi",
|
||||
"ui.construction.reference_hint": "Pomoćni pravac: odaberite ravnu liniju, pa točku; desnim klikom promijenite referencu",
|
||||
"ui.construction.ribbon_label": "Pomoćne",
|
||||
"ui.context_menu": "Kontekstni izbornik",
|
||||
"ui.continuity_g0": "G0 pregib",
|
||||
|
||||
@@ -2072,6 +2072,8 @@
|
||||
"prefs.view_confirm_corner": "ビューの隅に OK とキャンセル",
|
||||
"prefs.view_confirm_corner_note": "ダイアログまたはスケッチを開いている間、ビュー右上のチェックとバツが、ダイアログの OK とキャンセルと同じ操作を行うか、スケッチを終了またはキャンセルします。ビューナビゲーターはその下に移動します。",
|
||||
"prefs.viewport": "ビューポート",
|
||||
"prefs.viewport_analysis_status": "ビューポートの解析状態を表示",
|
||||
"prefs.viewport_analysis_status_help": "有効な解析をビューポートの上部に表示し、各項目から凡例を開きます。Esc で最新の解析を解除します。無効にすると隅の凡例と従来の Esc 動作に戻ります。",
|
||||
"prefs.viewport_background": "背景",
|
||||
"prefs.viewport_background_colour": "色",
|
||||
"prefs.viewport_background_theme": "テーマのグラデーション",
|
||||
@@ -4646,6 +4648,8 @@
|
||||
"ui.view_centre": "ビュー中心",
|
||||
"ui.viewport_status_clear": "{name}をオフにする",
|
||||
"ui.viewport_status_clear_hint": "{name}をオフにする · Esc",
|
||||
"ui.viewport_status_edges": "{count} エッジ",
|
||||
"ui.viewport_status_from": "方向 {direction}",
|
||||
"ui.viewport_status_key": "{name}の凡例を表示",
|
||||
"ui.viewport_status_minimum_radius": "最小半径",
|
||||
"ui.viewport_status_pairs": "{count} 組",
|
||||
|
||||
@@ -1699,6 +1699,8 @@
|
||||
"prefs.view_confirm_corner": "뷰 모서리에 확인 및 취소",
|
||||
"prefs.view_confirm_corner_note": "대화 상자나 스케치가 열려 있는 동안 뷰 오른쪽 위의 체크와 X 표시가 대화 상자의 확인과 취소와 같은 동작을 하거나 스케치를 마치고 취소합니다. 뷰 탐색기는 그 아래로 내려갑니다.",
|
||||
"prefs.viewport": "뷰포트",
|
||||
"prefs.viewport_analysis_status": "뷰포트 해석 상태 표시",
|
||||
"prefs.viewport_analysis_status_help": "활성 해석을 뷰포트 위에 표시하고 각 항목에서 범례를 엽니다. Esc로 최신 해석을 끕니다. 끄면 모서리 범례와 이전 Esc 동작으로 돌아갑니다.",
|
||||
"prefs.wheel": "휠",
|
||||
"prefs.wheel_trackpad": "휠 및 트랙패드",
|
||||
"prefs.workspace": "작업 공간",
|
||||
@@ -3681,6 +3683,8 @@
|
||||
"ui.view_centre": "뷰 중심",
|
||||
"ui.viewport_status_clear": "{name} 끄기",
|
||||
"ui.viewport_status_clear_hint": "{name} 끄기 · Esc",
|
||||
"ui.viewport_status_edges": "모서리 {count}개",
|
||||
"ui.viewport_status_from": "{direction} 방향에서",
|
||||
"ui.viewport_status_key": "{name} 범례 표시",
|
||||
"ui.viewport_status_minimum_radius": "최소 반지름",
|
||||
"ui.viewport_status_pairs": "{count}쌍",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"bs": {
|
||||
"browser.calibrate_click_a": "Kliknite sliku da zadate tačku A ili upišite njene koordinate.",
|
||||
"browser.calibrate_click_b": "Kliknite sliku da zadate tačku B ili upišite njene koordinate.",
|
||||
"help.sketch.construction_line": "Napravite beskonačnu pomoćnu pravu kroz dvije tačke. Koristite je da rasporedite skicu bez razdvajanja profila.",
|
||||
"help.sketch.construction_line.angle": "Napravite beskonačnu pomoćnu pravu pod uglom od ose X skice. Izaberite tačku i upišite ugao ili izaberite smjer.",
|
||||
"help.sketch.construction_line.bisector": "Izaberite dvije prave linije skice koje se sijeku, pa jednu od dvije simetrale ugla. Rezultat prati izvorne linije.",
|
||||
"help.sketch.construction_line.horizontal": "Napravite beskonačnu horizontalnu pomoćnu pravu kroz tačku. Njeno ograničenje horizontalnosti ostaje na snazi pri kasnijim izmjenama.",
|
||||
"help.sketch.construction_line.parallel": "Izaberite pravu liniju skice, pa postavite paralelnu beskonačnu pomoćnu pravu. Desnim klikom izaberite drugu referencu.",
|
||||
"help.sketch.construction_line.perpendicular": "Izaberite pravu liniju skice, pa postavite okomitu beskonačnu pomoćnu pravu. Desnim klikom izaberite drugu referencu.",
|
||||
"help.sketch.construction_line.tangent_at": "Napravite tangentu u tački na krugu ili kružnom luku, ili u krajnjoj tački otvorenog splajna ili konike. Izaberite krivu, pa tačku dodira.",
|
||||
"help.sketch.construction_line.tangent_external": "Izaberite krug ili kružni luk, pa vanjsku tačku i rješenje tangente. Pregledi prikazuju samo dodirne tačke na nacrtanom luku.",
|
||||
"help.sketch.construction_line.tangent_two": "Izaberite dva kruga ili kružna luka, pa u pregledu izaberite zajedničku tangentu. Dodirne tačke moraju ležati na nacrtanim krivama.",
|
||||
"help.sketch.construction_line.vertical": "Napravite beskonačnu vertikalnu pomoćnu pravu kroz tačku. Njeno ograničenje vertikalnosti ostaje na snazi pri kasnijim izmjenama.",
|
||||
"prefs.persistent_ribbon_pressed": "Istakni aktivne komande na traci",
|
||||
"prefs.persistent_ribbon_pressed_help": "Ističe komande na traci dok su njihovi efekti uključeni. Isključite da biste zadržali prethodni neutralni izgled i pomoć za komande.",
|
||||
"prefs.sketch_construction_lines": "Prošireni alati za pomoćne prave",
|
||||
"prefs.sketch_open_look_at": "Okreni kameru prema skici kad se otvori",
|
||||
"prefs.sketch_open_look_at_note": "Isključeno: kad napravite ili uredite skicu, prikaz ostaje onakav kakav jest. Pogledaj okreće prikaz prema skici kad to želite.",
|
||||
"recovery.discard_all": "Odbaci sve",
|
||||
"recovery.discard_all_detail": "Ovo se ne može poništiti.",
|
||||
"recovery.discard_all_title": "Odbaciti automatska čuvanja ({count})?",
|
||||
"recovery.discard_all_title_one": "Odbaciti 1 automatsko čuvanje?",
|
||||
"ui.construction.angle_hint": "Pomoćna prava: kliknite tačku, pa upišite ugao ili izaberite smjer",
|
||||
"ui.construction.bisector_hint": "Simetrala: izaberite dvije prave linije koje se sijeku, pa željeno rješenje",
|
||||
"ui.construction.common_hint": "Zajednička tangenta: izaberite dva kruga ili luka, pa željeno rješenje",
|
||||
"ui.construction.contact_hint": "Tangenta: izaberite krug/luk ili otvoreni splajn/koniku, pa tačku dodira (krive slobodnog oblika: krajnje tačke)",
|
||||
"ui.construction.external_hint": "Tangenta: izaberite krug ili luk, vanjsku tačku, pa željeno rješenje",
|
||||
"ui.construction.free_hint": "Pomoćna prava: kliknite dvije tačke; desnim klikom počnite ispočetka",
|
||||
"ui.construction.point_hint": "Pomoćna prava: kliknite tačku kroz koju prolazi",
|
||||
"ui.construction.reference_hint": "Pomoćna prava: izaberite pravu liniju, pa tačku; desnim klikom promijenite referencu"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"gl": {
|
||||
"browser.calibrate_click_a": "Fai clic na imaxe para o punto A, ou escribe as súas coordenadas.",
|
||||
"browser.calibrate_click_b": "Fai clic na imaxe para o punto B, ou escribe as súas coordenadas.",
|
||||
"help.sketch.construction_line": "Crea unha recta de construción infinita por dous puntos. Úsaa para organizar un bosquexo sen dividir os perfís.",
|
||||
"help.sketch.construction_line.angle": "Crea unha recta de construción infinita cun ángulo respecto do eixe X do bosquexo. Escolle o punto e escribe o ángulo, ou escolle a súa dirección.",
|
||||
"help.sketch.construction_line.bisector": "Escolle dúas rectas do bosquexo que se crucen e despois unha das dúas bisectrices. O resultado segue as súas rectas de orixe.",
|
||||
"help.sketch.construction_line.horizontal": "Crea unha recta de construción horizontal infinita por un punto. A súa restrición horizontal mantense nas edicións posteriores.",
|
||||
"help.sketch.construction_line.parallel": "Escolle unha recta do bosquexo e despois coloca unha recta de construción infinita paralela. Fai clic co botón dereito para escoller outra referencia.",
|
||||
"help.sketch.construction_line.perpendicular": "Escolle unha recta do bosquexo e despois coloca unha recta de construción infinita perpendicular. Fai clic co botón dereito para escoller outra referencia.",
|
||||
"help.sketch.construction_line.tangent_at": "Crea unha tanxente nun punto dun círculo ou dun arco circular, ou no extremo dunha spline ou dunha cónica abertas. Escolle a curva e despois o seu punto de tanxencia.",
|
||||
"help.sketch.construction_line.tangent_external": "Escolle un círculo ou un arco circular e despois un punto exterior e unha solución de tanxente. As vistas previas só mostran os puntos de tanxencia sobre o arco debuxado.",
|
||||
"help.sketch.construction_line.tangent_two": "Escolle dous círculos ou arcos circulares e despois unha tanxente común na vista previa. Os puntos de tanxencia deben estar sobre as curvas debuxadas.",
|
||||
"help.sketch.construction_line.vertical": "Crea unha recta de construción vertical infinita por un punto. A súa restrición vertical mantense nas edicións posteriores.",
|
||||
"prefs.persistent_ribbon_pressed": "Resaltar as ordes activas da cinta",
|
||||
"prefs.persistent_ribbon_pressed_help": "Resalta as ordes da cinta mentres os seus efectos están activados. Desactívao para manter o aspecto neutro e a axuda das ordes anteriores.",
|
||||
"prefs.sketch_construction_lines": "Ferramentas ampliadas de rectas de construción",
|
||||
"prefs.sketch_open_look_at": "Xirar para mirar cara ao bosquexo cando se abre",
|
||||
"prefs.sketch_open_look_at_note": "Desactivado: crear ou editar un bosquexo deixa a vista tal e como está. Mirar cara a orienta a vista cara ao bosquexo cando o queres.",
|
||||
"recovery.discard_all": "Descartar todo",
|
||||
"recovery.discard_all_detail": "Esta acción non se pode desfacer.",
|
||||
"recovery.discard_all_title": "Descartar {count} gardados automáticos?",
|
||||
"recovery.discard_all_title_one": "Descartar 1 gardado automático?",
|
||||
"ui.construction.angle_hint": "Recta de construción: fai clic nun punto e despois escribe un ángulo ou escolle unha dirección",
|
||||
"ui.construction.bisector_hint": "Bisectriz: escolle dúas rectas que se crucen e despois a solución desexada",
|
||||
"ui.construction.common_hint": "Tanxente común: escolle dous círculos/arcos e despois a solución desexada",
|
||||
"ui.construction.contact_hint": "Tanxente: escolle un círculo/arco ou unha spline/cónica aberta e despois o punto de tanxencia (curvas de forma libre: extremos)",
|
||||
"ui.construction.external_hint": "Tanxente: escolle un círculo ou arco, un punto exterior e despois a solución desexada",
|
||||
"ui.construction.free_hint": "Recta de construción: fai clic en dous puntos; clic dereito para reiniciar",
|
||||
"ui.construction.point_hint": "Recta de construción: fai clic no punto polo que debe pasar",
|
||||
"ui.construction.reference_hint": "Recta de construción: escolle unha recta e despois un punto; clic dereito para cambiar a referencia"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": {
|
||||
"browser.calibrate_click_a": "Klik gambar untuk Titik A, atau ketik koordinatnya.",
|
||||
"browser.calibrate_click_b": "Klik gambar untuk Titik B, atau ketik koordinatnya.",
|
||||
"help.sketch.construction_line": "Buat garis bantu tak hingga melalui dua titik. Gunakan untuk menata sketsa tanpa memecah profil.",
|
||||
"help.sketch.construction_line.angle": "Buat garis bantu tak hingga dengan sudut terhadap sumbu X sketsa. Pilih titiknya dan ketik sudutnya, atau pilih arahnya.",
|
||||
"help.sketch.construction_line.bisector": "Pilih dua garis sketsa lurus yang berpotongan, lalu pilih salah satu garis bagi sudut. Hasilnya mengikuti garis sumbernya.",
|
||||
"help.sketch.construction_line.horizontal": "Buat garis bantu horizontal tak hingga melalui sebuah titik. Batasan horizontalnya tetap berlaku pada pengeditan berikutnya.",
|
||||
"help.sketch.construction_line.parallel": "Pilih sebuah garis sketsa lurus, lalu tempatkan garis bantu tak hingga yang sejajar. Klik kanan untuk memilih referensi lain.",
|
||||
"help.sketch.construction_line.perpendicular": "Pilih sebuah garis sketsa lurus, lalu tempatkan garis bantu tak hingga yang tegak lurus. Klik kanan untuk memilih referensi lain.",
|
||||
"help.sketch.construction_line.tangent_at": "Buat garis singgung pada sebuah titik di lingkaran atau busur lingkaran, atau pada titik ujung spline atau kurva konik yang terbuka. Pilih kurvanya, lalu titik singgungnya.",
|
||||
"help.sketch.construction_line.tangent_external": "Pilih lingkaran atau busur lingkaran, lalu sebuah titik luar dan satu solusi garis singgung. Pratinjau hanya menampilkan titik singgung pada busur yang digambar.",
|
||||
"help.sketch.construction_line.tangent_two": "Pilih dua lingkaran atau busur lingkaran, lalu pilih garis singgung persekutuan di pratinjau. Titik singgung harus berada pada kurva yang digambar.",
|
||||
"help.sketch.construction_line.vertical": "Buat garis bantu vertikal tak hingga melalui sebuah titik. Batasan vertikalnya tetap berlaku pada pengeditan berikutnya.",
|
||||
"prefs.persistent_ribbon_pressed": "Sorot perintah ribbon yang aktif",
|
||||
"prefs.persistent_ribbon_pressed_help": "Sorot perintah ribbon selama efeknya aktif. Matikan untuk mempertahankan tampilan netral dan bantuan perintah sebelumnya.",
|
||||
"prefs.sketch_construction_lines": "Alat garis bantu lanjutan",
|
||||
"prefs.sketch_open_look_at": "Putar untuk menghadap sketsa saat dibuka",
|
||||
"prefs.sketch_open_look_at_note": "Mati: membuat atau mengedit sketsa membiarkan tampilan apa adanya. Look At menghadapkan tampilan ke sketsa saat Anda menginginkannya.",
|
||||
"recovery.discard_all": "Buang semua",
|
||||
"recovery.discard_all_detail": "Aksi ini tidak dapat diurungkan.",
|
||||
"recovery.discard_all_title": "Buang {count} simpan otomatis?",
|
||||
"recovery.discard_all_title_one": "Buang 1 simpan otomatis?",
|
||||
"ui.construction.angle_hint": "Garis bantu: klik sebuah titik, lalu ketik sudut atau pilih arah",
|
||||
"ui.construction.bisector_hint": "Garis bagi sudut: pilih dua garis lurus yang berpotongan, lalu solusi yang diinginkan",
|
||||
"ui.construction.common_hint": "Garis singgung persekutuan: pilih dua lingkaran/busur, lalu solusi yang diinginkan",
|
||||
"ui.construction.contact_hint": "Garis singgung: pilih lingkaran/busur atau spline/kurva konik terbuka, lalu titik singgung (kurva bentuk bebas: titik ujung)",
|
||||
"ui.construction.external_hint": "Garis singgung: pilih lingkaran atau busur, sebuah titik luar, lalu solusi yang diinginkan",
|
||||
"ui.construction.free_hint": "Garis bantu: klik dua titik; klik kanan untuk mengulang",
|
||||
"ui.construction.point_hint": "Garis bantu: klik titik yang akan dilalui",
|
||||
"ui.construction.reference_hint": "Garis bantu: pilih garis lurus, lalu sebuah titik; klik kanan untuk mengganti referensi"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"ms": {
|
||||
"browser.calibrate_click_a": "Klik imej untuk Titik A, atau taip koordinatnya.",
|
||||
"browser.calibrate_click_b": "Klik imej untuk Titik B, atau taip koordinatnya.",
|
||||
"help.sketch.construction_line": "Cipta garis binaan tak terhingga melalui dua titik. Gunakannya untuk menyusun lakaran tanpa memisahkan profil.",
|
||||
"help.sketch.construction_line.angle": "Cipta garis binaan tak terhingga pada sudut daripada paksi X lakaran. Pilih titik dan taip sudutnya, atau pilih arahnya.",
|
||||
"help.sketch.construction_line.bisector": "Pilih dua garis lakaran lurus yang bersilang, kemudian pilih salah satu pembahagi dua sudut. Hasilnya mengikut garis sumbernya.",
|
||||
"help.sketch.construction_line.horizontal": "Cipta garis binaan mendatar tak terhingga melalui satu titik. Kekangan mendatarnya kekal apabila disunting kemudian.",
|
||||
"help.sketch.construction_line.parallel": "Pilih garis lakaran lurus, kemudian letakkan garis binaan tak terhingga yang selari. Klik kanan untuk memilih rujukan lain.",
|
||||
"help.sketch.construction_line.perpendicular": "Pilih garis lakaran lurus, kemudian letakkan garis binaan tak terhingga yang serenjang. Klik kanan untuk memilih rujukan lain.",
|
||||
"help.sketch.construction_line.tangent_at": "Cipta tangen pada satu titik pada bulatan atau lengkok bulat, atau pada titik hujung splin atau lengkung kon yang terbuka. Pilih lengkung itu, kemudian titik sentuhnya.",
|
||||
"help.sketch.construction_line.tangent_external": "Pilih bulatan atau lengkok bulat, kemudian satu titik luar dan satu penyelesaian tangen. Pratonton hanya menunjukkan titik sentuh pada lengkok yang dilukis.",
|
||||
"help.sketch.construction_line.tangent_two": "Pilih dua bulatan atau lengkok bulat, kemudian pilih tangen sepunya dalam pratonton. Titik sentuh mesti terletak pada lengkung yang dilukis.",
|
||||
"help.sketch.construction_line.vertical": "Cipta garis binaan menegak tak terhingga melalui satu titik. Kekangan menegaknya kekal apabila disunting kemudian.",
|
||||
"prefs.persistent_ribbon_pressed": "Serlahkan arahan reben yang aktif",
|
||||
"prefs.persistent_ribbon_pressed_help": "Serlahkan arahan reben selagi kesannya dihidupkan. Matikan untuk mengekalkan rupa neutral dan bantuan arahan yang terdahulu.",
|
||||
"prefs.sketch_construction_lines": "Alat garis binaan lanjutan",
|
||||
"prefs.sketch_open_look_at": "Putar menghadap lakaran apabila dibuka",
|
||||
"prefs.sketch_open_look_at_note": "Mati: mencipta atau menyunting lakaran membiarkan pandangan seperti sedia ada. Pandang Tepat menghadapkan pandangan ke lakaran apabila anda mahu.",
|
||||
"recovery.discard_all": "Buang semua",
|
||||
"recovery.discard_all_detail": "Tindakan ini tidak boleh dibuat asal.",
|
||||
"recovery.discard_all_title": "Buang {count} simpanan automatik?",
|
||||
"recovery.discard_all_title_one": "Buang 1 simpanan automatik?",
|
||||
"ui.construction.angle_hint": "Garis binaan: klik satu titik, kemudian taip sudut atau pilih arah",
|
||||
"ui.construction.bisector_hint": "Pembahagi dua sudut: pilih dua garis lurus yang bersilang, kemudian penyelesaian yang dikehendaki",
|
||||
"ui.construction.common_hint": "Tangen sepunya: pilih dua bulatan/lengkok, kemudian penyelesaian yang dikehendaki",
|
||||
"ui.construction.contact_hint": "Tangen: pilih bulatan/lengkok atau splin/lengkung kon terbuka, kemudian titik sentuh (lengkung bentuk bebas: titik hujung)",
|
||||
"ui.construction.external_hint": "Tangen: pilih bulatan atau lengkok, satu titik luar, kemudian penyelesaian yang dikehendaki",
|
||||
"ui.construction.free_hint": "Garis binaan: klik dua titik; klik kanan untuk mula semula",
|
||||
"ui.construction.point_hint": "Garis binaan: klik titik yang akan dilalui",
|
||||
"ui.construction.reference_hint": "Garis binaan: pilih garis lurus, kemudian satu titik; klik kanan untuk menukar rujukan"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"nb": {
|
||||
"browser.calibrate_click_a": "Klikk på bildet for punkt A, eller skriv inn koordinatene.",
|
||||
"browser.calibrate_click_b": "Klikk på bildet for punkt B, eller skriv inn koordinatene.",
|
||||
"help.sketch.construction_line": "Opprett en uendelig hjelpelinje gjennom to punkter. Bruk den til å legge opp en skisse uten å dele opp profilene.",
|
||||
"help.sketch.construction_line.angle": "Opprett en uendelig hjelpelinje i en vinkel fra skissens X-akse. Velg punktet og skriv inn vinkelen, eller velg retningen.",
|
||||
"help.sketch.construction_line.bisector": "Velg to rette skisselinjer som krysser hverandre, og velg deretter en av vinkelhalveringslinjene. Resultatet følger kildelinjene sine.",
|
||||
"help.sketch.construction_line.horizontal": "Opprett en uendelig vannrett hjelpelinje gjennom et punkt. Den vannrette bindingen følger med ved senere endringer.",
|
||||
"help.sketch.construction_line.parallel": "Velg en rett skisselinje, og plasser deretter en parallell uendelig hjelpelinje. Høyreklikk for å velge en annen referanse.",
|
||||
"help.sketch.construction_line.perpendicular": "Velg en rett skisselinje, og plasser deretter en vinkelrett uendelig hjelpelinje. Høyreklikk for å velge en annen referanse.",
|
||||
"help.sketch.construction_line.tangent_at": "Opprett en tangent i et punkt på en sirkel eller sirkelbue, eller i endepunktet på en åpen spline eller kjeglesnittkurve. Velg kurven, og deretter berøringspunktet.",
|
||||
"help.sketch.construction_line.tangent_external": "Velg en sirkel eller sirkelbue, deretter et eksternt punkt og en tangentløsning. Forhåndsvisningene viser bare berøringspunkter på den tegnede buen.",
|
||||
"help.sketch.construction_line.tangent_two": "Velg to sirkler eller sirkelbuer, og velg deretter en felles tangent i forhåndsvisningen. Berøringspunktene må ligge på de tegnede kurvene.",
|
||||
"help.sketch.construction_line.vertical": "Opprett en uendelig loddrett hjelpelinje gjennom et punkt. Den loddrette bindingen følger med ved senere endringer.",
|
||||
"prefs.persistent_ribbon_pressed": "Fremhev aktive kommandoer på båndet",
|
||||
"prefs.persistent_ribbon_pressed_help": "Fremhev kommandoer på båndet mens effektene deres er slått på. Slå av for å beholde det forrige nøytrale utseendet og den forrige kommandohjelpen.",
|
||||
"prefs.sketch_construction_lines": "Utvidede hjelpelinjeverktøy",
|
||||
"prefs.sketch_open_look_at": "Orienter mot skissen når den åpnes",
|
||||
"prefs.sketch_open_look_at_note": "Av: å opprette eller redigere en skisse lar visningen stå som den er. Orienter mot vender visningen mot skissen når du vil.",
|
||||
"recovery.discard_all": "Forkast alle",
|
||||
"recovery.discard_all_detail": "Dette kan ikke angres.",
|
||||
"recovery.discard_all_title": "Forkast {count} automatiske lagringer?",
|
||||
"recovery.discard_all_title_one": "Forkast 1 automatisk lagring?",
|
||||
"ui.construction.angle_hint": "Hjelpelinje: klikk på et punkt, og skriv deretter inn en vinkel eller velg en retning",
|
||||
"ui.construction.bisector_hint": "Vinkelhalveringslinje: velg to rette linjer som krysser hverandre, og deretter ønsket løsning",
|
||||
"ui.construction.common_hint": "Felles tangent: velg to sirkler/buer, og deretter ønsket løsning",
|
||||
"ui.construction.contact_hint": "Tangent: velg en sirkel/bue eller åpen spline/kjeglesnittkurve, og deretter berøringspunktet (frie kurver: endepunkter)",
|
||||
"ui.construction.external_hint": "Tangent: velg en sirkel eller bue, et eksternt punkt, og deretter ønsket løsning",
|
||||
"ui.construction.free_hint": "Hjelpelinje: klikk på to punkter; høyreklikk for å starte på nytt",
|
||||
"ui.construction.point_hint": "Hjelpelinje: klikk på punktet linjen skal gå gjennom",
|
||||
"ui.construction.reference_hint": "Hjelpelinje: velg en rett linje, og deretter et punkt; høyreklikk for å bytte referanse"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"zh-TW": {
|
||||
"browser.calibrate_click_a": "按一下影像以指定點 A,或輸入其座標。",
|
||||
"browser.calibrate_click_b": "按一下影像以指定點 B,或輸入其座標。",
|
||||
"help.sketch.construction_line": "建立通過兩點的無限長建構直線。可用來規劃草圖佈局,而不會分割輪廓。",
|
||||
"help.sketch.construction_line.angle": "建立與草圖 X 軸夾指定角度的無限長建構直線。請選取點並輸入角度,或選取其方向。",
|
||||
"help.sketch.construction_line.bisector": "選取兩條相交的草圖直線,再選擇其中一條角平分線。結果會跟隨其來源直線。",
|
||||
"help.sketch.construction_line.horizontal": "建立通過某一點的水平無限長建構直線。其水平拘束會在後續編輯中持續維持。",
|
||||
"help.sketch.construction_line.parallel": "選取一條草圖直線,再放置與其平行的無限長建構直線。按右鍵可選擇其他參考。",
|
||||
"help.sketch.construction_line.perpendicular": "選取一條草圖直線,再放置與其垂直的無限長建構直線。按右鍵可選擇其他參考。",
|
||||
"help.sketch.construction_line.tangent_at": "在圓或圓弧上的某一點,或在開放雲形線或圓錐曲線的端點建立切線。請先選取曲線,再選取其切點。",
|
||||
"help.sketch.construction_line.tangent_external": "選取圓或圓弧,再選取外部點與一個切線解。預覽只會顯示位於所繪圓弧上的切點。",
|
||||
"help.sketch.construction_line.tangent_two": "選取兩個圓或圓弧,再於預覽中選擇一條公切線。切點必須位於所繪曲線上。",
|
||||
"help.sketch.construction_line.vertical": "建立通過某一點的鉛直無限長建構直線。其鉛直拘束會在後續編輯中持續維持。",
|
||||
"prefs.persistent_ribbon_pressed": "醒目顯示作用中的功能區命令",
|
||||
"prefs.persistent_ribbon_pressed_help": "在功能區命令的效果開啟期間醒目顯示這些命令。關閉可維持先前的中性外觀與命令說明。",
|
||||
"prefs.sketch_construction_lines": "延伸建構直線工具",
|
||||
"prefs.sketch_open_look_at": "開啟草圖時轉向正對草圖",
|
||||
"prefs.sketch_open_look_at_note": "關閉:建立或編輯草圖時維持目前的檢視。需要時可用「注視」正對草圖。",
|
||||
"recovery.discard_all": "全部捨棄",
|
||||
"recovery.discard_all_detail": "此操作無法復原。",
|
||||
"recovery.discard_all_title": "捨棄 {count} 個自動儲存檔?",
|
||||
"recovery.discard_all_title_one": "捨棄 1 個自動儲存檔?",
|
||||
"ui.construction.angle_hint": "建構直線:按一下一點,再輸入角度或選取方向",
|
||||
"ui.construction.bisector_hint": "角平分線:選取兩條相交的直線,再選取所需的解",
|
||||
"ui.construction.common_hint": "公切線:選取兩個圓/圓弧,再選取所需的解",
|
||||
"ui.construction.contact_hint": "切線:選取圓/圓弧或開放雲形線/圓錐曲線,再選取切點(自由曲線:端點)",
|
||||
"ui.construction.external_hint": "切線:選取圓或圓弧、一個外部點,再選取所需的解",
|
||||
"ui.construction.free_hint": "建構直線:按一下兩點;按右鍵可重新開始",
|
||||
"ui.construction.point_hint": "建構直線:按一下要通過的點",
|
||||
"ui.construction.reference_hint": "建構直線:選取一條直線,再選取一點;按右鍵可變更參考"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"tr": {
|
||||
"browser.calibrate_click_a": "A Noktası için görüntüye tıklayın veya koordinatlarını yazın.",
|
||||
"browser.calibrate_click_b": "B Noktası için görüntüye tıklayın veya koordinatlarını yazın.",
|
||||
"help.sketch.construction_line": "İki noktadan geçen sonsuz bir yardımcı doğru oluşturun. Profilleri bölmeden bir eskizi kurgulamak için kullanın.",
|
||||
"help.sketch.construction_line.angle": "Eskizin X ekseninden belirli bir açıyla sonsuz bir yardımcı doğru oluşturun. Noktayı seçip açıyı yazın veya yönünü seçin.",
|
||||
"help.sketch.construction_line.bisector": "Kesişen iki düz eskiz çizgisini seçin, ardından iki açıortaydan birini seçin. Sonuç, kaynak çizgilerini izler.",
|
||||
"help.sketch.construction_line.horizontal": "Bir noktadan geçen sonsuz yatay bir yardımcı doğru oluşturun. Yatay kısıtlaması sonraki düzenlemelerde de korunur.",
|
||||
"help.sketch.construction_line.parallel": "Düz bir eskiz çizgisi seçin, ardından paralel bir sonsuz yardımcı doğru yerleştirin. Başka bir referans seçmek için sağ tıklayın.",
|
||||
"help.sketch.construction_line.perpendicular": "Düz bir eskiz çizgisi seçin, ardından dik bir sonsuz yardımcı doğru yerleştirin. Başka bir referans seçmek için sağ tıklayın.",
|
||||
"help.sketch.construction_line.tangent_at": "Bir daire veya dairesel yay üzerindeki bir noktada ya da açık bir spline veya konik eğrinin uç noktasında teğet oluşturun. Önce eğriyi, sonra değme noktasını seçin.",
|
||||
"help.sketch.construction_line.tangent_external": "Bir daire veya dairesel yay seçin, ardından bir dış nokta ve bir teğet çözümü seçin. Önizlemeler yalnızca çizilen yay üzerindeki değme noktalarını gösterir.",
|
||||
"help.sketch.construction_line.tangent_two": "İki daire veya dairesel yay seçin, ardından önizlemede ortak bir teğet seçin. Değme noktaları çizilen eğriler üzerinde olmalıdır.",
|
||||
"help.sketch.construction_line.vertical": "Bir noktadan geçen sonsuz dikey bir yardımcı doğru oluşturun. Dikey kısıtlaması sonraki düzenlemelerde de korunur.",
|
||||
"prefs.persistent_ribbon_pressed": "Etkin şerit komutlarını vurgula",
|
||||
"prefs.persistent_ribbon_pressed_help": "Şerit komutlarını, etkileri açık olduğu sürece vurgula. Önceki nötr görünümü ve komut yardımını korumak için kapatın.",
|
||||
"prefs.sketch_construction_lines": "Genişletilmiş yardımcı doğru araçları",
|
||||
"prefs.sketch_open_look_at": "Eskiz açıldığında görünümü ona doğru döndür",
|
||||
"prefs.sketch_open_look_at_note": "Kapalı: eskiz oluşturmak veya düzenlemek görünümü olduğu gibi bırakır. Bak komutu, istediğinizde görünümü eskize çevirir.",
|
||||
"recovery.discard_all": "Tümünü at",
|
||||
"recovery.discard_all_detail": "Bu işlem geri alınamaz.",
|
||||
"recovery.discard_all_title": "{count} otomatik kayıt atılsın mı?",
|
||||
"recovery.discard_all_title_one": "1 otomatik kayıt atılsın mı?",
|
||||
"ui.construction.angle_hint": "Yardımcı doğru: bir noktaya tıklayın, ardından bir açı yazın veya bir yön seçin",
|
||||
"ui.construction.bisector_hint": "Açıortay: kesişen iki düz çizgiyi, ardından istenen çözümü seçin",
|
||||
"ui.construction.common_hint": "Ortak teğet: iki daire/yay, ardından istenen çözümü seçin",
|
||||
"ui.construction.contact_hint": "Teğet: bir daire/yay veya açık spline/konik eğri, ardından değme noktasını seçin (serbest biçimli eğriler: uç noktalar)",
|
||||
"ui.construction.external_hint": "Teğet: bir daire veya yay, bir dış nokta, ardından istenen çözümü seçin",
|
||||
"ui.construction.free_hint": "Yardımcı doğru: iki noktaya tıklayın; yeniden başlamak için sağ tıklayın",
|
||||
"ui.construction.point_hint": "Yardımcı doğru: geçeceği noktaya tıklayın",
|
||||
"ui.construction.reference_hint": "Yardımcı doğru: düz bir çizgi, ardından bir nokta seçin; referansı değiştirmek için sağ tıklayın"
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"prefs.viewport_analysis_status": "Show viewport analysis status",
|
||||
"prefs.viewport_analysis_status_help": "Show active analyses above the viewport, open their legends from each item, and use Esc to turn off the newest analysis. Turn off to restore corner legends and the previous Esc behaviour.",
|
||||
"ui.viewport_status_edges": "{count} edges",
|
||||
"ui.viewport_status_from": "from {direction}"
|
||||
},
|
||||
"ar": {
|
||||
"prefs.viewport_analysis_status": "إظهار حالة التحليل في منفذ العرض",
|
||||
"prefs.viewport_analysis_status_help": "عرض التحليلات النشطة أعلى منفذ العرض وفتح مفتاح كل تحليل من عنصره. ينهي Esc أحدث تحليل. أوقف الخيار لاستعادة المفاتيح في الزاوية وسلوك Esc السابق.",
|
||||
"ui.viewport_status_edges": "{count} حواف",
|
||||
"ui.viewport_status_from": "من {direction}"
|
||||
},
|
||||
"de": {
|
||||
"prefs.viewport_analysis_status": "Analysestatus im Ansichtsfenster anzeigen",
|
||||
"prefs.viewport_analysis_status_help": "Zeigt aktive Analysen über dem Ansichtsfenster und ihre Legenden am jeweiligen Eintrag. Esc beendet die neueste Analyse. Ausschalten stellt Ecklegenden und das bisherige Esc-Verhalten wieder her.",
|
||||
"ui.viewport_status_edges": "{count} Kanten",
|
||||
"ui.viewport_status_from": "aus {direction}"
|
||||
},
|
||||
"he": {
|
||||
"prefs.viewport_analysis_status": "הצגת מצב ניתוח בחלון התצוגה",
|
||||
"prefs.viewport_analysis_status_help": "הצגת ניתוחים פעילים מעל חלון התצוגה ופתיחת המקרא מכל פריט. Esc מכבה את הניתוח האחרון. כיבוי האפשרות משחזר את המקרא בפינה ואת פעולת Esc הקודמת.",
|
||||
"ui.viewport_status_edges": "{count} קצוות",
|
||||
"ui.viewport_status_from": "מכיוון {direction}"
|
||||
},
|
||||
"ja": {
|
||||
"prefs.viewport_analysis_status": "ビューポートの解析状態を表示",
|
||||
"prefs.viewport_analysis_status_help": "有効な解析をビューポートの上部に表示し、各項目から凡例を開きます。Esc で最新の解析を解除します。無効にすると隅の凡例と従来の Esc 動作に戻ります。",
|
||||
"ui.viewport_status_edges": "{count} エッジ",
|
||||
"ui.viewport_status_from": "方向 {direction}"
|
||||
},
|
||||
"ko": {
|
||||
"prefs.viewport_analysis_status": "뷰포트 해석 상태 표시",
|
||||
"prefs.viewport_analysis_status_help": "활성 해석을 뷰포트 위에 표시하고 각 항목에서 범례를 엽니다. Esc로 최신 해석을 끕니다. 끄면 모서리 범례와 이전 Esc 동작으로 돌아갑니다.",
|
||||
"ui.viewport_status_edges": "모서리 {count}개",
|
||||
"ui.viewport_status_from": "{direction} 방향에서"
|
||||
},
|
||||
"vi": {
|
||||
"prefs.viewport_analysis_status": "Hiện trạng thái phân tích trong khung nhìn",
|
||||
"prefs.viewport_analysis_status_help": "Hiện các phân tích đang bật phía trên khung nhìn và mở chú giải từ từng mục. Esc tắt phân tích mới nhất. Tắt tùy chọn để khôi phục chú giải ở góc và hành vi Esc trước đây.",
|
||||
"ui.viewport_status_edges": "{count} cạnh",
|
||||
"ui.viewport_status_from": "từ {direction}"
|
||||
},
|
||||
"zh": {
|
||||
"prefs.viewport_analysis_status": "显示视口分析状态",
|
||||
"prefs.viewport_analysis_status_help": "在视口上方显示启用的分析,从各项目打开图例,并按 Esc 关闭最近启用的分析。关闭此选项可恢复角落图例和原来的 Esc 行为。",
|
||||
"ui.viewport_status_edges": "{count} 条边",
|
||||
"ui.viewport_status_from": "从 {direction}"
|
||||
}
|
||||
}
|
||||
@@ -775,6 +775,8 @@
|
||||
"at the face": "на лицу",
|
||||
"bom.group_children": "Подделови у реду свог дела",
|
||||
"browser.body_name": "Тело{number}",
|
||||
"browser.calibrate_click_a": "Кликните слику да зададете тачку A или упишите њене координате.",
|
||||
"browser.calibrate_click_b": "Кликните слику да зададете тачку B или упишите њене координате.",
|
||||
"browser.calibrate_title": "Калибриши: {name}",
|
||||
"browser.capture_many": "Померене компоненте без забележеног положаја: {count}.",
|
||||
"browser.capture_note": "Забележи задржава нови положај; Врати га враћа назад.",
|
||||
@@ -1399,6 +1401,16 @@
|
||||
"help.occurrence.move": "Помера део склопа само дуж слобода које дозвољавају његови спојеви. Изаберите лице дела, а затим превуците доступну стрелицу или лук.",
|
||||
"help.parameters.change": "Отвара табелу именованих вредности и израза који управљају котама дизајна. Користите је да промените меру на једном месту, а све операције које је користе је прате.",
|
||||
"help.pointcloud.insert": "Уметните PLY, XYZ или PTS скенирање као облак тачака поред модела. Користите га као референцу при изради модела на основу измерених података.",
|
||||
"help.sketch.construction_line": "Направите бесконачну помоћну праву кроз две тачке. Користите је да распоредите скицу без раздвајања профила.",
|
||||
"help.sketch.construction_line.angle": "Направите бесконачну помоћну праву под углом од осе X скице. Изаберите тачку и упишите угао или изаберите смер.",
|
||||
"help.sketch.construction_line.bisector": "Изаберите две праве линије скице које се секу, па једну од две симетрале угла. Резултат прати изворне линије.",
|
||||
"help.sketch.construction_line.horizontal": "Направите бесконачну хоризонталну помоћну праву кроз тачку. Њено ограничење хоризонталности остаје на снази при каснијим изменама.",
|
||||
"help.sketch.construction_line.parallel": "Изаберите праву линију скице, па поставите паралелну бесконачну помоћну праву. Десним кликом изаберите другу референцу.",
|
||||
"help.sketch.construction_line.perpendicular": "Изаберите праву линију скице, па поставите нормалну бесконачну помоћну праву. Десним кликом изаберите другу референцу.",
|
||||
"help.sketch.construction_line.tangent_at": "Направите тангенту у тачки на кругу или кружном луку, или у крајњој тачки отвореног сплајна или конике. Изаберите криву, па тачку додира.",
|
||||
"help.sketch.construction_line.tangent_external": "Изаберите круг или кружни лук, па спољашњу тачку и решење тангенте. Прегледи приказују само тачке додира на нацртаном луку.",
|
||||
"help.sketch.construction_line.tangent_two": "Изаберите два круга или кружна лука, па у прегледу изаберите заједничку тангенту. Тачке додира морају лежати на нацртаним кривама.",
|
||||
"help.sketch.construction_line.vertical": "Направите бесконачну вертикалну помоћну праву кроз тачку. Њено ограничење вертикалности остаје на снази при каснијим изменама.",
|
||||
"help.sketch.equation_curve": "Црта криву из формула: x(t) и y(t), или y = f(x), у задатом опсегу. Формуле могу да користе параметре и крива их прати: користите је за синусоиду, спиралу или евольвенту зупца зупчаника.",
|
||||
"help.solid.align": "Помера изабрана тела тако да се тачка, лице или раван на њима поклопи са тачком, лицем или равни негде другде. Користите га да поставите део уз други без мерења одмака.",
|
||||
"help.solid.chamfer": "Одсеца изабране ивице равним скошењем задатог растојања. Користите га да ублажите оштре ивице или олакшате увођење чивије или завртња.",
|
||||
@@ -1935,6 +1947,8 @@
|
||||
"prefs.origin_plane_names": "Називи равни исходишта",
|
||||
"prefs.origin_plane_names_note": "Називи приказани у стаблу модела, дијалозима и листама. {top} је XY, {front} је XZ, а {right} је YZ, са Z нагоре. Команде, скрипте и сачувани дизајни задржавају XY, XZ и YZ.",
|
||||
"prefs.overall_speed": "Укупна брзина",
|
||||
"prefs.persistent_ribbon_pressed": "Истакни активне команде на траци",
|
||||
"prefs.persistent_ribbon_pressed_help": "Истиче команде на траци док су њихови ефекти укључени. Искључите да бисте задржали претходни неутрални изглед и помоћ за команде.",
|
||||
"prefs.perspective_fov": "Видно поље",
|
||||
"prefs.placement": "Постављање",
|
||||
"prefs.prefer_free_sketch_points": "Дај предност покретним тачкама скице када се тачке преклапају",
|
||||
@@ -1984,11 +1998,14 @@
|
||||
"prefs.sketch_align_points_note": "Током цртања тачка се истовремено поравнава хоризонтално са једном тачком скице и вертикално са другом, уз тачкасти водич, а клик задржава оба поравнања. Искључено: само са претходном тачком, по једна оса.",
|
||||
"prefs.sketch_conflict_colour": "Геометрија у сукобу у боји сукоба",
|
||||
"prefs.sketch_conflict_colour_note": "Док су ограничења скице у сукобу, криве и тачке које држи ограничење које не успева исцртавају се такође у боји сукоба, а не само његов симбол или кота. Искључено: задржавају уобичајене боје.",
|
||||
"prefs.sketch_construction_lines": "Проширени алати за помоћне праве",
|
||||
"prefs.sketch_exit_view": "Када се скица заврши",
|
||||
"prefs.sketch_exit_view.keep": "Задржи тренутни приказ",
|
||||
"prefs.sketch_exit_view.previous": "Врати се на претходни приказ",
|
||||
"prefs.sketch_exit_view.unless_rotated": "Врати се осим ако је ротирано",
|
||||
"prefs.sketch_exit_view_note": "Куда приказ иде након Заврши скицу, Откажи или двоструког Esc.",
|
||||
"prefs.sketch_open_look_at": "Окрени камеру према скици када се отвори",
|
||||
"prefs.sketch_open_look_at_note": "Искључено: када направите или измените скицу, приказ остаје онакав какав јесте. Погледај окреће приказ према скици када то желите.",
|
||||
"prefs.sketch_recent_snap": "Аутоматски привлачи на недавне центре и средишње тачке",
|
||||
"prefs.sketch_recent_snap_note": "Током цртања показивач се привлачи тамо где се секу водичи двају последњих центара кругова или средишњих тачака правих ивица. Искључено: ти водичи служе само за приказ и показивач се не помера.",
|
||||
"prefs.sketch_selection": "Скица и избор",
|
||||
@@ -2064,6 +2081,10 @@
|
||||
"properties.title": "Својства: {name}",
|
||||
"recovery.days_ago": "пре {count} дана",
|
||||
"recovery.detail": "{file} · операција: {features} · аутоматски сачувано {when}",
|
||||
"recovery.discard_all": "Одбаци све",
|
||||
"recovery.discard_all_detail": "Ово се не може поништити.",
|
||||
"recovery.discard_all_title": "Одбацити аутоматска чувања ({count})?",
|
||||
"recovery.discard_all_title_one": "Одбацити 1 аутоматско чување?",
|
||||
"recovery.hours_ago": "пре {count} h",
|
||||
"recovery.just_now": "управо сада",
|
||||
"recovery.min_ago": "пре {count} min",
|
||||
@@ -3983,6 +4004,14 @@
|
||||
"ui.constant": "Константно",
|
||||
"ui.constraints_conflict": "Ограничења су у сукобу",
|
||||
"ui.construction": "Помоћна",
|
||||
"ui.construction.angle_hint": "Помоћна права: кликните тачку, па упишите угао или изаберите смер",
|
||||
"ui.construction.bisector_hint": "Симетрала: изаберите две праве линије које се секу, па жељено решење",
|
||||
"ui.construction.common_hint": "Заједничка тангента: изаберите два круга или лука, па жељено решење",
|
||||
"ui.construction.contact_hint": "Тангента: изаберите круг/лук или отворени сплајн/конику, па тачку додира (криве слободног облика: крајње тачке)",
|
||||
"ui.construction.external_hint": "Тангента: изаберите круг или лук, спољашњу тачку, па жељено решење",
|
||||
"ui.construction.free_hint": "Помоћна права: кликните две тачке; десним кликом почните испочетка",
|
||||
"ui.construction.point_hint": "Помоћна права: кликните тачку кроз коју пролази",
|
||||
"ui.construction.reference_hint": "Помоћна права: изаберите праву линију, па тачку; десним кликом промените референцу",
|
||||
"ui.construction.ribbon_label": "Пом. праве",
|
||||
"ui.context_menu": "Контекстни мени",
|
||||
"ui.continuity_g0": "G0 прегиб",
|
||||
|
||||
@@ -2072,6 +2072,8 @@
|
||||
"prefs.view_confirm_corner": "Nút Đồng ý và Hủy bỏ ở góc khung nhìn",
|
||||
"prefs.view_confirm_corner_note": "Khi hộp thoại hoặc môi trường phác thảo đang mở, dấu tích và dấu chéo ở góc trên bên phải khung nhìn có chức năng tương tự nút Đồng ý và Hủy bỏ của hộp thoại, hoặc để hoàn tất và hủy phác thảo. Khối điều hướng góc nhìn sẽ dời xuống bên dưới chúng.",
|
||||
"prefs.viewport": "Khung nhìn 3D",
|
||||
"prefs.viewport_analysis_status": "Hiện trạng thái phân tích trong khung nhìn",
|
||||
"prefs.viewport_analysis_status_help": "Hiện các phân tích đang bật phía trên khung nhìn và mở chú giải từ từng mục. Esc tắt phân tích mới nhất. Tắt tùy chọn để khôi phục chú giải ở góc và hành vi Esc trước đây.",
|
||||
"prefs.viewport_background": "Nền",
|
||||
"prefs.viewport_background_colour": "Màu",
|
||||
"prefs.viewport_background_theme": "Dải màu của chủ đề",
|
||||
@@ -4646,6 +4648,8 @@
|
||||
"ui.view_centre": "Tâm góc nhìn",
|
||||
"ui.viewport_status_clear": "Tắt {name}",
|
||||
"ui.viewport_status_clear_hint": "Tắt {name} · Esc",
|
||||
"ui.viewport_status_edges": "{count} cạnh",
|
||||
"ui.viewport_status_from": "từ {direction}",
|
||||
"ui.viewport_status_key": "Hiển thị chú giải cho {name}",
|
||||
"ui.viewport_status_minimum_radius": "Bán kính nhỏ nhất",
|
||||
"ui.viewport_status_pairs": "{count} cặp",
|
||||
|
||||
@@ -2072,6 +2072,8 @@
|
||||
"prefs.view_confirm_corner": "在视图角落显示确定和取消",
|
||||
"prefs.view_confirm_corner_note": "打开对话框或草图时,视图右上角的勾号和叉号与对话框的确定和取消作用相同,或完成和取消草图。视图导航器会下移到它们下方。",
|
||||
"prefs.viewport": "视口",
|
||||
"prefs.viewport_analysis_status": "显示视口分析状态",
|
||||
"prefs.viewport_analysis_status_help": "在视口上方显示启用的分析,从各项目打开图例,并按 Esc 关闭最近启用的分析。关闭此选项可恢复角落图例和原来的 Esc 行为。",
|
||||
"prefs.viewport_background": "背景",
|
||||
"prefs.viewport_background_colour": "颜色",
|
||||
"prefs.viewport_background_theme": "主题渐变",
|
||||
@@ -4646,6 +4648,8 @@
|
||||
"ui.view_centre": "视图中心",
|
||||
"ui.viewport_status_clear": "关闭{name}",
|
||||
"ui.viewport_status_clear_hint": "关闭{name} · Esc",
|
||||
"ui.viewport_status_edges": "{count} 条边",
|
||||
"ui.viewport_status_from": "从 {direction}",
|
||||
"ui.viewport_status_key": "显示{name}的图例",
|
||||
"ui.viewport_status_minimum_radius": "最小半径",
|
||||
"ui.viewport_status_pairs": "{count} 对",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"error": "#fff0ca",
|
||||
"field": "#100b04",
|
||||
"form_accent": "#ca892b",
|
||||
"form_dim": "#9b6922",
|
||||
"form_dim": "#ad7a2c",
|
||||
"form_error": "#fff0ca",
|
||||
"form_error_fill": "#46300f",
|
||||
"form_fill": "#e19e45",
|
||||
@@ -180,7 +180,7 @@
|
||||
"surface_soft": "#2a1e0b",
|
||||
"tab_on_fill_dark": "#170d03",
|
||||
"text": "#f3ca87",
|
||||
"text_dim": "#9b6922",
|
||||
"text_dim": "#ad7a2c",
|
||||
"timeline": "#090602",
|
||||
"timeline_item": "#1d1406",
|
||||
"timeline_item_error": "#46300f",
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"form_dim": "#bdae93",
|
||||
"form_primary": "#83a598",
|
||||
"form_error": "#fb4934",
|
||||
"form_error_fill": "#702b28",
|
||||
"form_error_fill": "#5f2320",
|
||||
"form_success": "#b8bb26",
|
||||
"form_success_fill": "#343a28",
|
||||
"dialog_body": "#32302f",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"form_accent": "#cfa7c7",
|
||||
"form_dim": "#a4b0c2",
|
||||
"form_error": "#bf616a",
|
||||
"form_error_fill": "#4b3d48",
|
||||
"form_error_fill": "#352b33",
|
||||
"form_fill": "#e7bfe0",
|
||||
"form_ink": "#eceff4",
|
||||
"form_primary": "#88c0d0",
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
"surface_soft": "#363332",
|
||||
"tab_on_fill_dark": "#000000",
|
||||
"text": "#abb2bf",
|
||||
"text_dim": "#7f848e",
|
||||
"text_dim": "#90959f",
|
||||
"timeline": "#1e2227",
|
||||
"timeline_item": "#323842",
|
||||
"timeline_item_error": "#5c343d",
|
||||
|
||||
@@ -59,12 +59,12 @@
|
||||
"form_accent": "#6b70c3",
|
||||
"form_dim": "#657b83",
|
||||
"form_error": "#dc322f",
|
||||
"form_error_fill": "#eac4b4",
|
||||
"form_error_fill": "#f7ddd3",
|
||||
"form_fill": "#a2a9f8",
|
||||
"form_ink": "#586e75",
|
||||
"form_primary": "#268bd2",
|
||||
"form_soft": "#e4ded4",
|
||||
"form_success": "#859900",
|
||||
"form_success": "#5f6e00",
|
||||
"form_success_fill": "#dcdbb1",
|
||||
"form_tab_ink": "#565a9d",
|
||||
"glyph_bg": "#e3ddc9",
|
||||
@@ -130,6 +130,8 @@
|
||||
"mesh_fill": "#d7aa3c",
|
||||
"mesh_ink": "#7b5d00",
|
||||
"mesh_soft": "#e9e0c4",
|
||||
"nav_tip": "#073642",
|
||||
"nav_tip_ink": "#eee8d5",
|
||||
"on_accent": "#ffffff",
|
||||
"origin_plane": "#b5890028",
|
||||
"origin_plane_edge": "#b5890088",
|
||||
@@ -179,7 +181,7 @@
|
||||
"surface_ink": "#a83e12",
|
||||
"surface_soft": "#ebdbc6",
|
||||
"tab_on_fill_dark": "#fdf6e3",
|
||||
"text": "#586e75",
|
||||
"text": "#536970",
|
||||
"text_dim": "#657b83",
|
||||
"timeline": "#e3ddc9",
|
||||
"timeline_item": "#e3ddc9",
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
"form_ink": "#000000",
|
||||
"form_primary": "#000080",
|
||||
"form_soft": "#bbb1bb",
|
||||
"form_success": "#008000",
|
||||
"form_success": "#006000",
|
||||
"form_success_fill": "#c0dcc0",
|
||||
"form_tab_ink": "#7d007d",
|
||||
"glyph_bg": "#ffffff",
|
||||
@@ -130,6 +130,10 @@
|
||||
"mesh_fill": "#808000",
|
||||
"mesh_ink": "#4c4c00",
|
||||
"mesh_soft": "#bbbbb1",
|
||||
"nav_press": "#a0a0a0",
|
||||
"nav_tip": "#ffffe1",
|
||||
"nav_tip_ink": "#000000",
|
||||
"nav_tip_key_border": "#808080",
|
||||
"on_accent": "#ffffff",
|
||||
"origin_plane": "#ffff005e",
|
||||
"origin_plane_edge": "#808000c8",
|
||||
@@ -180,7 +184,7 @@
|
||||
"surface_soft": "#bbb6b1",
|
||||
"tab_on_fill_dark": "#000000",
|
||||
"text": "#000000",
|
||||
"text_dim": "#808080",
|
||||
"text_dim": "#606060",
|
||||
"timeline": "#c0c0c0",
|
||||
"timeline_item": "#c0c0c0",
|
||||
"timeline_item_error": "#ffffc0",
|
||||
|
||||
@@ -847,8 +847,8 @@ fn desktop_body(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let frame = Container::new(ui.ctx(), Surface::Viewport).edge_frame(egui::Color32::TRANSPARENT);
|
||||
let composer = Container::new(ui.ctx(), Surface::Viewport).edge(Id::new("sc_agent_chat_composer"), Edge::Bottom);
|
||||
composer.resizable(false).show_separator_line(false).frame(frame).show(ui, |ui| {
|
||||
ui_kit::assistant_composer_footer(ui, |ui| {
|
||||
ui_kit::assistant_composer(ui, focused, |ui| {
|
||||
let footer = ui_kit::assistant_composer_footer(ui, |ui| {
|
||||
let boxed = ui_kit::assistant_composer(ui, focused, |ui| {
|
||||
// What Pick context chose for this message, each item with an ×.
|
||||
if !app.agent_chat.picked_context.is_empty() {
|
||||
let tray = app.agent_chat.picked_context.clone();
|
||||
@@ -987,7 +987,9 @@ fn desktop_body(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
},
|
||||
);
|
||||
});
|
||||
crate::scenario::publish_rect("agent_chat:composer_box", boxed.response.rect);
|
||||
});
|
||||
crate::scenario::publish_rect("agent_chat:composer_footer", footer.response.rect);
|
||||
});
|
||||
if let Some(at) = remove_picked_at {
|
||||
remove_picked(app, at);
|
||||
|
||||
@@ -2844,3 +2844,61 @@ fn a_screenshot_card_hides_the_path_until_details() {
|
||||
h.frames(3);
|
||||
assert!(h.widget("Result").is_some() && h.widget(path).is_some(), "Details holds the full path");
|
||||
}
|
||||
|
||||
/// The composer's input box fills its parent composer area on all four sides, in every layout:
|
||||
/// the only inset left is the text's own padding inside the box (no second margin around it).
|
||||
#[test]
|
||||
fn the_composer_box_fills_its_parent_in_every_layout() {
|
||||
use crate::scenario::rect_of;
|
||||
for (layout, popped) in [("floating", false), ("docked", false), ("floating", true), ("docked", true)] {
|
||||
let mut h = narrow_chat();
|
||||
h.call("ui.resize", json!({"width": 1600, "height": 1000}));
|
||||
h.call("ui.set", json!({"layout": layout}));
|
||||
h.app.preferences.agent_chat_popped = popped;
|
||||
h.app.agent_chat.state = Some(State::Ready);
|
||||
h.frames(4);
|
||||
let footer = rect_of("agent_chat:composer_footer").unwrap_or_else(|| panic!("{layout} popped={popped}: the composer area"));
|
||||
let boxed = rect_of("agent_chat:composer_box").expect("the input box");
|
||||
let (l, r, t, b) =
|
||||
(boxed.left() - footer.left(), footer.right() - boxed.right(), boxed.top() - footer.top(), footer.bottom() - boxed.bottom());
|
||||
for (edge, gap) in [("left", l), ("right", r), ("top", t), ("bottom", b)] {
|
||||
assert!(gap.abs() <= 1.0, "{layout} popped={popped}: box {edge} edge is {gap} from its parent ({boxed:?} in {footer:?})");
|
||||
}
|
||||
let input = rect_of("agent_chat:input").expect("the text");
|
||||
assert!(input.left() > boxed.left() + 2.0, "{layout} popped={popped}: the text keeps its own padding");
|
||||
let send = rect_of("agent_chat:send").expect("Send");
|
||||
assert!(boxed.contains_rect(send), "{layout} popped={popped}: Send stays inside the box");
|
||||
}
|
||||
}
|
||||
|
||||
/// The composer hint (idle and busy, in every language) stays on one row, even at the narrowest
|
||||
/// panel, and is cut with an ellipsis instead of wrapping.
|
||||
#[test]
|
||||
fn the_composer_hint_stays_on_one_row_at_the_narrowest_width() {
|
||||
let mut h = narrow_chat();
|
||||
h.app.agent_chat.state = Some(State::Ready);
|
||||
h.frames(4);
|
||||
let width = crate::scenario::rect_of("agent_chat:input").expect("the text box").width();
|
||||
let ctx = crate::fonts::test_context();
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/locales");
|
||||
let mut checked = 0;
|
||||
for entry in std::fs::read_dir(dir).unwrap().filter_map(Result::ok) {
|
||||
let path = entry.path();
|
||||
if path.extension().is_none_or(|x| x != "json") {
|
||||
continue;
|
||||
}
|
||||
let language = path.file_stem().unwrap().to_string_lossy().into_owned();
|
||||
let _scope = crate::i18n::scope(&language);
|
||||
for key in ["ui.agent_chat_placeholder", "ui.agent_chat_placeholder_busy"] {
|
||||
let hint = crate::i18n::tr(key).to_owned();
|
||||
let mut rows = 0;
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
rows = crate::ui_kit::assistant_hint_galley(ui, &hint, width - 2.0 * crate::theme::Tokens::get().form_inset).rows.len();
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
assert_eq!(rows, 1, "{language} {key} at {width} wide: {hint}");
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
assert!(checked >= 20, "every language was checked");
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ fn paint_icon(ui: &egui::Ui, rect: egui::Rect, tex: Option<egui::TextureId>, nam
|
||||
egui::Align2::CENTER_CENTER,
|
||||
letter,
|
||||
egui::FontId::proportional(crate::theme::Tokens::get().font_small),
|
||||
t.text,
|
||||
crate::theme::readable(&[t.chip], crate::theme::TEXT_CONTRAST, &[t.text, t.on_accent]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,17 +851,7 @@ pub(crate) fn device(app: &mut SolveApp, rect: Rect, caps: NavCaps) {
|
||||
}
|
||||
});
|
||||
if let Some(queued) = app.viewport.device_motion.take() {
|
||||
device_camera(
|
||||
&mut app.cam,
|
||||
queued.motion,
|
||||
queued.seconds,
|
||||
height,
|
||||
turn,
|
||||
app.preferences.zoom_reverse,
|
||||
app.preferences.free_orbit,
|
||||
anchor,
|
||||
pivot,
|
||||
);
|
||||
device_camera(&mut app.cam, queued.motion, queued.seconds, height, turn, app.preferences.free_orbit, anchor, pivot);
|
||||
}
|
||||
// A device app's motion (the navigation bridge), on the same terms.
|
||||
if let Some(queued) = app.viewport.bridge_motion.take() {
|
||||
@@ -920,13 +910,14 @@ pub(crate) fn bridge_camera(
|
||||
/// the frame rate. With `turn` off the view keeps its orientation. `anchor` is the model point the
|
||||
/// view holds while it zooms, as the wheel's cursor point (none zooms through the model, #7435).
|
||||
/// `pivot` is the point the view turns around and keeps still on screen (none: the view's centre).
|
||||
/// Reverse zoom direction is a wheel setting and leaves the cap alone: the cap's own direction is
|
||||
/// its per-axis invert, already in `motion` (#8993).
|
||||
pub(crate) fn device_camera(
|
||||
cam: &mut Camera,
|
||||
motion: Motion,
|
||||
seconds: f64,
|
||||
height: f64,
|
||||
turn: bool,
|
||||
reverse_zoom: bool,
|
||||
free_orbit: bool,
|
||||
anchor: Option<Vec3>,
|
||||
pivot: Option<Vec3>,
|
||||
@@ -939,8 +930,7 @@ pub(crate) fn device_camera(
|
||||
// Logical 3DxWare coordinates are X = left/right, Y = zoom and Z = up/down. The user's
|
||||
// profile can swap the physical forward/back and up/down axes before they reach here.
|
||||
cam.pan(x * height * 0.6 * seconds, z * height * 0.6 * seconds, height);
|
||||
let zoom_sign = if reverse_zoom { -1.0 } else { 1.0 };
|
||||
cam.zoom_at((-y * zoom_sign * 1.5 * seconds).exp(), anchor);
|
||||
cam.zoom_at((-y * 1.5 * seconds).exp(), anchor);
|
||||
if turn {
|
||||
// These are logical 3DxWare coordinates after the profile mapping: Rx is Tilt, Ry is
|
||||
// Roll and Rz is Spin. The KMJ profile swaps physical Ry/Rz for the selected navigation
|
||||
@@ -1710,7 +1700,7 @@ mod tests {
|
||||
}
|
||||
// Other presets keep the existing upright camera behavior.
|
||||
let mut cam = Camera { yaw: 0.0, pitch: 1.5, ..Default::default() };
|
||||
device_camera(&mut cam, Motion { translation: [0.0; 3], rotation: [1.0, 0.0, 0.0] }, 0.1, 600.0, true, false, false, None, None);
|
||||
device_camera(&mut cam, Motion { translation: [0.0; 3], rotation: [1.0, 0.0, 0.0] }, 0.1, 600.0, true, false, None, None);
|
||||
assert!(cam.pitch < std::f64::consts::FRAC_PI_2 && cam.pitch > 1.5);
|
||||
}
|
||||
|
||||
|
||||
@@ -2729,7 +2729,7 @@ fn space_mouse_rows(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
// The calibrated device moving as it does, with the test model beside it, then its dead zones.
|
||||
let (device, raw) = (app.space_mouse.device.clone(), app.space_mouse.raw.into());
|
||||
let (boat, motion, reverse) = (&mut app.space_mouse.boat, app.space_mouse.motion, (app.preferences.zoom_reverse, app.preferences.free_orbit));
|
||||
let (boat, motion, free_orbit) = (&mut app.space_mouse.boat, app.space_mouse.motion, app.preferences.free_orbit);
|
||||
if let Some(calibration) = device.as_ref().and_then(|device| app.preferences.space_mouse_calibrations.get_mut(device)) {
|
||||
ui_kit::divider(ui, "space_mouse_picture");
|
||||
// Under each circle's axes, what the view does with them: the rows' own labels.
|
||||
@@ -2739,11 +2739,11 @@ fn space_mouse_rows(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
crate::space_mouse_calibration::dead_zone_pads(ui, calibration, raw, false, &stories);
|
||||
ui_kit::text(ui, &help, ui_kit::Text::Hint("space_mouse_circles_help"));
|
||||
};
|
||||
picture_and_boat(ui, boat, motion, reverse, true, |ui| crate::space_mouse_model::live(ui, calibration, raw), pads);
|
||||
picture_and_boat(ui, boat, motion, free_orbit, true, |ui| crate::space_mouse_model::live(ui, calibration, raw), pads);
|
||||
} else if device.is_some() {
|
||||
// Not calibrated: the test model alone.
|
||||
ui_kit::divider(ui, "space_mouse_picture");
|
||||
picture_and_boat(ui, boat, motion, reverse, false, |_| {}, |_| {});
|
||||
picture_and_boat(ui, boat, motion, free_orbit, false, |_| {}, |_| {});
|
||||
}
|
||||
ui_kit::settings_form(ui, FORM_LABEL_WIDTH, |ui| {
|
||||
hint_under_controls(ui, crate::i18n::tr_label("Move and rotate the view together; sketches use pan and zoom only."), "space_mouse");
|
||||
@@ -2817,7 +2817,7 @@ fn picture_and_boat(
|
||||
ui: &mut egui::Ui,
|
||||
boat: &mut Option<crate::test_boat::Boat>,
|
||||
motion: Option<crate::space_mouse::DeviceMotion>,
|
||||
(reverse_zoom, free_orbit): (bool, bool),
|
||||
free_orbit: bool,
|
||||
has_picture: bool,
|
||||
picture: impl FnOnce(&mut egui::Ui),
|
||||
pads: impl FnOnce(&mut egui::Ui),
|
||||
@@ -2832,7 +2832,7 @@ fn picture_and_boat(
|
||||
let width = if beside { (room - PICTURE_WIDTH - gap).min(BOAT_MAX) } else { room.min(BOAT_MAX) };
|
||||
let mut boat_and_reset = |ui: &mut egui::Ui| {
|
||||
ui.vertical(|ui| {
|
||||
crate::test_boat::show(ui, boat, motion, reverse_zoom, free_orbit, egui::vec2(width.max(0.0), BOAT_HEIGHT));
|
||||
crate::test_boat::show(ui, boat, motion, free_orbit, egui::vec2(width.max(0.0), BOAT_HEIGHT));
|
||||
if ui_kit::button(ui, crate::i18n::tr("ui.3d_mouse_reset_view"), ui_kit::Button::Normal).clicked() {
|
||||
crate::test_boat::reset(boat);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ use solidcraft_engine::sketch::PatternAlignment;
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::dialogs::{CHAMFER_TYPES, Dialog, Kind};
|
||||
use crate::selection::{AXES, CURVES, SKETCH_POINTS, SelInput};
|
||||
use crate::selection::{AXES, CURVES, PROFILES, SKETCH_POINTS, SelInput};
|
||||
use crate::tool_modifiers::{Audit, Scope, ToolModifiers};
|
||||
use crate::ui_kit;
|
||||
use crate::viewport::{Hit, Proj};
|
||||
@@ -185,21 +185,25 @@ impl Mod {
|
||||
|
||||
fn inputs(k: &Mod) -> Vec<SelInput> {
|
||||
match k {
|
||||
Mod::Scale { .. } => vec![SelInput::new("Entities", CURVES | SKETCH_POINTS, true), SelInput::new("Base point", SKETCH_POINTS, false)],
|
||||
Mod::Scale { .. } => {
|
||||
vec![SelInput::new("Entities", CURVES | SKETCH_POINTS | PROFILES, true), SelInput::new("Base point", SKETCH_POINTS, false)]
|
||||
}
|
||||
Mod::Offset { .. } => vec![SelInput::new("Curves", CURVES, true)],
|
||||
// AXES is the mask that takes a straight sketch curve (`selection::SelInput::accepts`).
|
||||
Mod::Mirror => vec![SelInput::new("Entities", CURVES | SKETCH_POINTS, true), SelInput::new("Mirror line", AXES, false)],
|
||||
Mod::Move { .. } => vec![SelInput::new("Entities", CURVES | SKETCH_POINTS, true), SelInput::new("Center", SKETCH_POINTS, false)],
|
||||
Mod::Transform { .. } => vec![SelInput::new("Entities", CURVES | SKETCH_POINTS, true)],
|
||||
Mod::Mirror => vec![SelInput::new("Entities", CURVES | SKETCH_POINTS | PROFILES, true), SelInput::new("Mirror line", AXES, false)],
|
||||
Mod::Move { .. } => vec![SelInput::new("Entities", CURVES | SKETCH_POINTS | PROFILES, true), SelInput::new("Center", SKETCH_POINTS, false)],
|
||||
Mod::Transform { .. } => vec![SelInput::new("Entities", CURVES | SKETCH_POINTS | PROFILES, true)],
|
||||
// The direction is optional: `sketch.pattern.rectangular` documents sketch x as its
|
||||
// default, the way the engine reads it.
|
||||
Mod::PatternRect { .. } => vec![
|
||||
SelInput::new("Entities", CURVES | SKETCH_POINTS, true),
|
||||
SelInput::new("Entities", CURVES | SKETCH_POINTS | PROFILES, true),
|
||||
SelInput::new("Direction", AXES, false),
|
||||
SelInput::new("Direction 2", AXES, false),
|
||||
],
|
||||
// The centre is required: `sketch.pattern.circular` has no default for it.
|
||||
Mod::PatternCirc { .. } => vec![SelInput::new("Entities", CURVES | SKETCH_POINTS, true), SelInput::new("Center", SKETCH_POINTS, false)],
|
||||
Mod::PatternCirc { .. } => {
|
||||
vec![SelInput::new("Entities", CURVES | SKETCH_POINTS | PROFILES, true), SelInput::new("Center", SKETCH_POINTS, false)]
|
||||
}
|
||||
// A fillet rounds any number of corners to one radius (`points`). Each chamfer command
|
||||
// cuts one corner: its point, or its two lines. Two lines name the corner between them
|
||||
// where more lines meet at its point (#1824), for both.
|
||||
@@ -308,7 +312,7 @@ pub(crate) fn selected_equation(app: &SolveApp) -> Option<String> {
|
||||
pub fn candidate(d: &Dialog, session: &solidcraft_engine::Session, hit: &Hit) -> Option<Sel> {
|
||||
let active = session.active_sketch?;
|
||||
match hit {
|
||||
Hit::SketchCurve { sketch, .. } | Hit::SketchPoint { sketch, .. } if *sketch != active => None,
|
||||
Hit::SketchCurve { sketch, .. } | Hit::SketchPoint { sketch, .. } | Hit::Profile { sketch, .. } if *sketch != active => None,
|
||||
Hit::SketchCurve { id, .. } if matches!(d.kind, Kind::SketchModify(Mod::Corner { .. })) => {
|
||||
let state = session.world_state();
|
||||
let sk = &state.sketch(active)?.sketch;
|
||||
@@ -395,6 +399,50 @@ pub fn double_click_pattern(app: &mut SolveApp, pos: Pos2) -> bool {
|
||||
id.is_some_and(|id| edit_pattern(app, &id))
|
||||
}
|
||||
|
||||
/// Double-click a sketch curve to add its whole connected chain to the shared Entities input of
|
||||
/// Mirror, Move/Copy, Scale and the two patterns, as a profile click adds a whole closed region
|
||||
/// (#8390). The single-curve pick of the double-click's two clicks cancels itself out (each click
|
||||
/// toggles the same curve), so the chain toggle lands on the state before the double-click.
|
||||
pub fn double_click_entities(app: &mut SolveApp) -> bool {
|
||||
let Some(Hit::SketchCurve { sketch, id, .. }) = app.viewport.hover.clone() else {
|
||||
return false;
|
||||
};
|
||||
if app.session.active_sketch != Some(sketch) {
|
||||
return false;
|
||||
}
|
||||
let Some(dialog) = app.dialog.as_mut() else {
|
||||
return false;
|
||||
};
|
||||
// Only the commands whose first input is the shared Entities list take a chain here; Offset
|
||||
// has its own Chain option, and the corner tools take one corner at a time.
|
||||
let entities = matches!(
|
||||
&dialog.kind,
|
||||
Kind::SketchModify(
|
||||
Mod::Scale { .. } | Mod::Mirror | Mod::Move { .. } | Mod::Transform { .. } | Mod::PatternRect { .. } | Mod::PatternCirc { .. }
|
||||
)
|
||||
);
|
||||
if !entities || dialog.active != 0 {
|
||||
return false;
|
||||
}
|
||||
let Ok(v) = app.session.execute("sketch.chain", &json!({ "curve": id })) else {
|
||||
return false;
|
||||
};
|
||||
let items: Vec<Sel> = v["curves"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|c| c.as_str())
|
||||
.map(|id| Sel::SketchCurve { id: id.to_string(), sketch: None })
|
||||
.collect();
|
||||
if items.is_empty() {
|
||||
return false;
|
||||
}
|
||||
if let Some(input) = dialog.inputs.get_mut(0) {
|
||||
input.toggle(items);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Place a circular pattern centre in empty sketch space without creating a document point.
|
||||
pub fn pick_position(d: &mut Dialog, session: &solidcraft_engine::Session, point: Vec3) -> bool {
|
||||
if !matches!(d.kind, Kind::SketchModify(Mod::PatternCirc { .. })) || d.active != 1 {
|
||||
@@ -668,15 +716,26 @@ pub(crate) fn point_id<'a>(session: &solidcraft_engine::Session, sel: &'a Sel) -
|
||||
}
|
||||
}
|
||||
|
||||
/// The curves and lone points picked in input `i`, as the engine's `entities` list.
|
||||
/// The curves and lone points picked in input `i`, as the engine's `entities` list. A profile
|
||||
/// pick expands to every curve of its boundary and its holes (#8390).
|
||||
fn entity_ids(session: &solidcraft_engine::Session, inputs: &[SelInput], i: usize) -> Vec<String> {
|
||||
items(inputs, i)
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
Sel::SketchCurve { id, .. } => Some(id.clone()),
|
||||
_ => point_id(session, x).map(str::to_string),
|
||||
})
|
||||
.collect()
|
||||
let mut out = Vec::new();
|
||||
for x in items(inputs, i) {
|
||||
match x {
|
||||
Sel::SketchCurve { id, .. } => out.push(id.clone()),
|
||||
Sel::Profile { sketch, index } => {
|
||||
if let Some(ids) = session.world_state().sketch(*sketch).and_then(|ss| ss.profiles.get(*index)).map(|p| p.curve_ids()) {
|
||||
out.extend(ids);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(id) = point_id(session, x) {
|
||||
out.push(id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Sketch curve ids picked in input `i`.
|
||||
@@ -889,9 +948,8 @@ pub fn commands(app: &SolveApp, k: &Mod, inputs: &[SelInput], extra: &Map<String
|
||||
Some(_) => return Err("The mirror line must be a straight curve of this sketch or an origin axis in its plane".into()),
|
||||
None => return Err("Select the mirror line".into()),
|
||||
};
|
||||
if line.as_str().is_some_and(|line| entities.iter().any(|entity| entity == line)) {
|
||||
return Err("The mirror line cannot also be mirrored".into());
|
||||
}
|
||||
// The mirror line may itself be among the entities (a chain or profile the user
|
||||
// picked in one action): the command leaves the line in place and mirrors the rest.
|
||||
("sketch.mirror", json!({"entities": entities, "line": line}))
|
||||
}
|
||||
Mod::Move { x, y, angle, copy } => {
|
||||
|
||||
@@ -232,12 +232,12 @@ fn a_half_filled_window_is_an_error_instead_of_a_command() {
|
||||
origin_axis[0].items = vec![Sel::SketchCurve { id: ids[1].clone(), sketch: None }];
|
||||
origin_axis[1].items = vec![Sel::Axis { name: "Z".into() }];
|
||||
assert!(commands(&h.app, &Mod::Mirror, &origin_axis, &serde_json::Map::new()).is_err());
|
||||
// The line itself must not be in the entities: the old click flow could not express that
|
||||
// either, and the engine silently dropped it.
|
||||
// The mirror line may itself be among the entities (a chain or profile picked in one action):
|
||||
// the command leaves the line in place and mirrors the rest.
|
||||
let mut line_in_both = mirror_inputs();
|
||||
line_in_both[0].items = vec![Sel::SketchCurve { id: ids[0].clone(), sketch: None }];
|
||||
line_in_both[1].items = vec![Sel::SketchCurve { id: ids[0].clone(), sketch: None }];
|
||||
assert!(commands(&h.app, &Mod::Mirror, &line_in_both, &serde_json::Map::new()).is_err());
|
||||
assert_eq!(commands(&h.app, &Mod::Mirror, &line_in_both, &serde_json::Map::new()).unwrap()[0].1, json!({"entities": [ids[0]], "line": ids[0]}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -693,8 +693,8 @@ mod tests {
|
||||
|
||||
/// The camera math the page applies the motion with (`navigation::device`): `sketch` keeps
|
||||
/// the orientation, as editing a sketch does.
|
||||
fn navigate(cam: &mut Camera, motion: Motion, seconds: f64, height: f64, sketch: bool, reverse_zoom: bool) {
|
||||
crate::navigation::device_camera(cam, motion, seconds, height, !sketch, reverse_zoom, false, None, None);
|
||||
fn navigate(cam: &mut Camera, motion: Motion, seconds: f64, height: f64, sketch: bool) {
|
||||
crate::navigation::device_camera(cam, motion, seconds, height, !sketch, false, None, None);
|
||||
}
|
||||
|
||||
/// A window that has run a frame, with or without focus. A context that has never run one
|
||||
@@ -1174,14 +1174,14 @@ mod tests {
|
||||
fn six_axes_move_the_camera_and_sketches_keep_their_orientation() {
|
||||
let mut cam = Camera::default();
|
||||
let before = cam;
|
||||
navigate(&mut cam, raw(Axes { translation: [350, 350, 350], rotation: [350, 350, 350] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut cam, raw(Axes { translation: [350, 350, 350], rotation: [350, 350, 350] }), 0.02, 600.0, false);
|
||||
assert_ne!(cam.target, before.target);
|
||||
assert!(cam.distance < before.distance);
|
||||
assert_ne!(cam.yaw, before.yaw);
|
||||
assert_ne!(cam.pitch, before.pitch);
|
||||
assert_ne!(cam.roll, before.roll);
|
||||
let orientation = (cam.yaw, cam.pitch, cam.roll);
|
||||
navigate(&mut cam, raw(Axes { translation: [350, 0, 0], rotation: [350, 350, 350] }), 0.02, 600.0, true, false);
|
||||
navigate(&mut cam, raw(Axes { translation: [350, 0, 0], rotation: [350, 350, 350] }), 0.02, 600.0, true);
|
||||
assert_eq!((cam.yaw, cam.pitch, cam.roll), orientation);
|
||||
assert!(cam.is_valid());
|
||||
}
|
||||
@@ -1191,57 +1191,79 @@ mod tests {
|
||||
let before = Camera::default();
|
||||
|
||||
let mut slide = before;
|
||||
navigate(&mut slide, raw(Axes { translation: [350, 0, 0], rotation: [0; 3] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut slide, raw(Axes { translation: [350, 0, 0], rotation: [0; 3] }), 0.02, 600.0, false);
|
||||
assert_ne!(slide.target, before.target);
|
||||
assert_eq!(slide.distance, before.distance);
|
||||
|
||||
let mut push = before;
|
||||
navigate(&mut push, raw(Axes { translation: [0, 350, 0], rotation: [0; 3] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut push, raw(Axes { translation: [0, 350, 0], rotation: [0; 3] }), 0.02, 600.0, false);
|
||||
assert_eq!(push.target, before.target);
|
||||
assert!(push.distance < before.distance);
|
||||
|
||||
let mut lift = before;
|
||||
navigate(&mut lift, raw(Axes { translation: [0, 0, 350], rotation: [0; 3] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut lift, raw(Axes { translation: [0, 0, 350], rotation: [0; 3] }), 0.02, 600.0, false);
|
||||
assert_ne!(lift.target, before.target);
|
||||
assert_eq!(lift.distance, before.distance);
|
||||
|
||||
let mut tilt = before;
|
||||
navigate(&mut tilt, raw(Axes { translation: [0; 3], rotation: [350, 0, 0] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut tilt, raw(Axes { translation: [0; 3], rotation: [350, 0, 0] }), 0.02, 600.0, false);
|
||||
assert_eq!((tilt.yaw, tilt.roll), (before.yaw, before.roll));
|
||||
assert_ne!(tilt.pitch, before.pitch);
|
||||
|
||||
let mut roll = before;
|
||||
navigate(&mut roll, raw(Axes { translation: [0; 3], rotation: [0, 350, 0] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut roll, raw(Axes { translation: [0; 3], rotation: [0, 350, 0] }), 0.02, 600.0, false);
|
||||
assert_eq!((roll.pitch, roll.roll), (before.pitch, before.roll));
|
||||
assert_ne!(roll.yaw, before.yaw);
|
||||
|
||||
let mut twist = before;
|
||||
navigate(&mut twist, raw(Axes { translation: [0; 3], rotation: [0, 0, 350] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut twist, raw(Axes { translation: [0; 3], rotation: [0, 0, 350] }), 0.02, 600.0, false);
|
||||
assert_eq!((twist.yaw, twist.pitch), (before.yaw, before.pitch));
|
||||
assert_ne!(twist.roll, before.roll);
|
||||
}
|
||||
|
||||
/// Reverse zoom direction is a wheel and trackpad setting: it turns the wheel's zoom around
|
||||
/// and leaves the cap's, which has its own invert switch (#8993).
|
||||
#[test]
|
||||
fn reverse_zoom_setting_applies_to_spacemouse() {
|
||||
let before = Camera::default();
|
||||
let motion = raw(Axes { translation: [0, 350, 0], rotation: [0; 3] });
|
||||
let mut normal = before;
|
||||
navigate(&mut normal, motion, 0.02, 600.0, false, false);
|
||||
let mut reversed = before;
|
||||
navigate(&mut reversed, motion, 0.02, 600.0, false, true);
|
||||
assert!(normal.distance < before.distance);
|
||||
assert!(reversed.distance > before.distance);
|
||||
fn reverse_zoom_setting_flips_the_wheel_and_leaves_the_cap() {
|
||||
let wheel_zooms_in = |reverse: bool| {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.preferences.zoom_reverse = reverse;
|
||||
h.app.preferences.smooth_wheel_zoom = false;
|
||||
h.call("ui.move", serde_json::json!({"x":800,"y":500}));
|
||||
let before = h.app.cam.half_height();
|
||||
h.call("ui.scroll", serde_json::json!({"x":800,"y":500,"delta":120}));
|
||||
h.app.cam.half_height() < before
|
||||
};
|
||||
assert!(wheel_zooms_in(false));
|
||||
assert!(!wheel_zooms_in(true), "the wheel turns around");
|
||||
// The cap's zoom axis pushed forward with the wheel reversed or not, and with the cap's own
|
||||
// invert switch for that axis on or off.
|
||||
let cap_distance = |wheel_reversed: bool, cap_inverted: bool| {
|
||||
let mut app = app();
|
||||
app.preferences.zoom_reverse = wheel_reversed;
|
||||
app.preferences.space_mouse_reverse[1] = cap_inverted;
|
||||
let ctx = window(true);
|
||||
app.space_mouse.device = Some("hid 046d:c62b".into());
|
||||
let before = app.cam.distance;
|
||||
frame(&mut app, &ctx, DriverMapping::default(), Axes { translation: [0, 350, 0], rotation: [0; 3] }, 0.02);
|
||||
(before, app.cam.distance)
|
||||
};
|
||||
let (before, normal) = cap_distance(false, false);
|
||||
assert!(normal < before, "pushing the cap forward zooms in");
|
||||
assert_eq!(cap_distance(true, false).1, normal, "the cap zooms the same way with the wheel reversed");
|
||||
assert!(cap_distance(false, true).1 > before, "the cap's own invert switch turns it around");
|
||||
assert_eq!(cap_distance(true, true).1, cap_distance(false, true).1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_mode_tilts_roll_and_twists_spin() {
|
||||
let before = Camera::default();
|
||||
let mut tilt = before;
|
||||
navigate(&mut tilt, DriverMapping::object_mode().apply(Axes { translation: [0; 3], rotation: [0, 350, 0] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut tilt, DriverMapping::object_mode().apply(Axes { translation: [0; 3], rotation: [0, 350, 0] }), 0.02, 600.0, false);
|
||||
assert_eq!((tilt.yaw, tilt.pitch), (before.yaw, before.pitch));
|
||||
assert_ne!(tilt.roll, before.roll);
|
||||
let mut twist = before;
|
||||
navigate(&mut twist, DriverMapping::object_mode().apply(Axes { translation: [0; 3], rotation: [0, 0, 350] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut twist, DriverMapping::object_mode().apply(Axes { translation: [0; 3], rotation: [0, 0, 350] }), 0.02, 600.0, false);
|
||||
assert_ne!(twist.yaw, before.yaw);
|
||||
assert_eq!((twist.pitch, twist.roll), (before.pitch, before.roll));
|
||||
// The physical tilt right (Ry−) rolls one way and the clockwise twist (Rz+) spins the
|
||||
|
||||
@@ -109,17 +109,8 @@ impl Boat {
|
||||
|
||||
/// `motion` for `seconds` on the boat's camera in a box `aspect` wide for its height of `height`
|
||||
/// points, with pan and zoom held so a sphere of `radius` stays inside it.
|
||||
pub(crate) fn step(
|
||||
cam: &mut Camera,
|
||||
motion: crate::space_mouse::Motion,
|
||||
seconds: f64,
|
||||
height: f64,
|
||||
radius: f64,
|
||||
aspect: f64,
|
||||
reverse_zoom: bool,
|
||||
free_orbit: bool,
|
||||
) {
|
||||
crate::navigation::device_camera(cam, motion, seconds, height, true, reverse_zoom, free_orbit, None, None);
|
||||
pub(crate) fn step(cam: &mut Camera, motion: crate::space_mouse::Motion, seconds: f64, height: f64, radius: f64, aspect: f64, free_orbit: bool) {
|
||||
crate::navigation::device_camera(cam, motion, seconds, height, true, free_orbit, None, None);
|
||||
cam.fov = FOV;
|
||||
// Zoom within limits: the nearest still shows the whole boat in the shorter side of the box.
|
||||
let shorter = aspect.min(1.0);
|
||||
@@ -171,14 +162,7 @@ pub(crate) fn project(model: &Model, cam: &Camera, rect: Rect) -> Frame {
|
||||
}
|
||||
|
||||
/// Draws the boat in a box of `size` and moves it by this frame's motion. `boat` is the page's state.
|
||||
pub(crate) fn show(
|
||||
ui: &mut egui::Ui,
|
||||
boat: &mut Option<Boat>,
|
||||
motion: Option<DeviceMotion>,
|
||||
reverse_zoom: bool,
|
||||
free_orbit: bool,
|
||||
size: egui::Vec2,
|
||||
) -> Option<Rect> {
|
||||
pub(crate) fn show(ui: &mut egui::Ui, boat: &mut Option<Boat>, motion: Option<DeviceMotion>, free_orbit: bool, size: egui::Vec2) -> Option<Rect> {
|
||||
let model = model()?;
|
||||
let t = Tokens::get();
|
||||
let now = ui.input(|i| i.time);
|
||||
@@ -188,7 +172,7 @@ pub(crate) fn show(
|
||||
state.seen = now;
|
||||
if let Some(motion) = motion {
|
||||
let aspect = f64::from(rect.width()) / f64::from(rect.height().max(1.0));
|
||||
step(&mut state.cam, motion.motion, motion.seconds, f64::from(rect.height()), model.radius, aspect, reverse_zoom, free_orbit);
|
||||
step(&mut state.cam, motion.motion, motion.seconds, f64::from(rect.height()), model.radius, aspect, free_orbit);
|
||||
}
|
||||
let painter = ui.painter_at(rect);
|
||||
painter.rect(rect, t.radius_sm, t.dialog_body, egui::Stroke::new(1.0, t.dialog_rule), egui::StrokeKind::Inside);
|
||||
|
||||
@@ -51,7 +51,7 @@ fn test_boat_step_orbits_pans_zooms_rolls_and_holds_the_boat_in_its_box() {
|
||||
let aspect = 1.4;
|
||||
let run = |m: Motion, seconds: f64| {
|
||||
let mut cam = start;
|
||||
step(&mut cam, m, seconds, 150.0, radius, aspect, false, false);
|
||||
step(&mut cam, m, seconds, 150.0, radius, aspect, false);
|
||||
cam
|
||||
};
|
||||
// Nothing moves nothing.
|
||||
@@ -70,17 +70,17 @@ fn test_boat_step_orbits_pans_zooms_rolls_and_holds_the_boat_in_its_box() {
|
||||
// Held down for a long time, the limits hold: nearest, farthest, and the pan room.
|
||||
let mut cam = start;
|
||||
for _ in 0..400 {
|
||||
step(&mut cam, motion([1.0, 1.0, 1.0], [0.0; 3]), 0.05, 150.0, radius, aspect, false, false);
|
||||
step(&mut cam, motion([1.0, 1.0, 1.0], [0.0; 3]), 0.05, 150.0, radius, aspect, false);
|
||||
}
|
||||
assert!(cam.distance >= radius * NEAREST / 1.0 - 1e-6 && cam.target.len() <= cam.distance * (FOV / 2.0).tan() + 1e-6);
|
||||
let mut cam = start;
|
||||
for _ in 0..400 {
|
||||
step(&mut cam, motion([-1.0, -1.0, -1.0], [0.0; 3]), 0.05, 150.0, radius, aspect, false, false);
|
||||
step(&mut cam, motion([-1.0, -1.0, -1.0], [0.0; 3]), 0.05, 150.0, radius, aspect, false);
|
||||
}
|
||||
assert!(cam.distance <= radius * FARTHEST + 1e-6);
|
||||
// Hostile numbers leave a valid camera.
|
||||
let mut cam = start;
|
||||
step(&mut cam, motion([f64::NAN, f64::INFINITY, 1e300], [f64::NAN; 3]), 0.05, 150.0, radius, aspect, false, false);
|
||||
step(&mut cam, motion([f64::NAN, f64::INFINITY, 1e300], [f64::NAN; 3]), 0.05, 150.0, radius, aspect, false);
|
||||
assert!(cam.is_valid());
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ fn test_boat_draws_inside_its_box_at_every_size_and_pose() {
|
||||
// Through the extremes of what the 3D mouse can do.
|
||||
for (translation, rotation) in [([0.0; 3], [0.0; 3]), ([1.0, -1.0, 1.0], [1.0, 0.0, 1.0]), ([-1.0, 1.0, -1.0], [0.0, 1.0, -1.0])] {
|
||||
for _ in 0..200 {
|
||||
step(&mut cam, motion(translation, rotation), 0.05, f64::from(h), m.radius, aspect, false, false);
|
||||
step(&mut cam, motion(translation, rotation), 0.05, f64::from(h), m.radius, aspect, false);
|
||||
}
|
||||
let frame = project(m, &cam, rect.shrink(1.0));
|
||||
assert!(!frame.indices.is_empty(), "{w}x{h}: something is drawn");
|
||||
@@ -141,7 +141,7 @@ fn test_boat_far_faces_come_first_and_a_page_opened_again_starts_afresh() {
|
||||
let moved = Some(DeviceMotion { motion: motion([0.0; 3], [1.0, 0.0, 0.0]), seconds: 0.05 });
|
||||
let draw = |time: f64, motion: Option<DeviceMotion>, state: &mut Option<Boat>| {
|
||||
let mut output = ctx.run_ui(egui::RawInput { time: Some(time), ..Default::default() }, |ui| {
|
||||
show(ui, state, motion, false, false, vec2(200.0, 150.0));
|
||||
show(ui, state, motion, false, vec2(200.0, 150.0));
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
};
|
||||
|
||||
@@ -286,6 +286,28 @@ pub(crate) fn contrast_ratio(a: Color32, b: Color32) -> f64 {
|
||||
(a.max(b) + 0.05) / (a.min(b) + 0.05)
|
||||
}
|
||||
|
||||
/// WCAG floors: body text, and icons, bold labels and other large marks.
|
||||
pub(crate) const TEXT_CONTRAST: f64 = 4.5;
|
||||
pub(crate) const MARK_CONTRAST: f64 = 3.0;
|
||||
|
||||
/// The first of `inks` that reads at `min` on every one of `fills`, else whichever of them reads
|
||||
/// best on its worst fill. Themes older than a surface can pair a fill with an ink that vanishes
|
||||
/// on it (SolidCraft 95's navy accent on its navy selection); this keeps such a pair readable
|
||||
/// without changing any theme where the preferred ink already reads.
|
||||
pub(crate) fn readable(fills: &[Color32], min: f64, inks: &[Color32]) -> Color32 {
|
||||
let worst = |ink: Color32| fills.iter().map(|fill| contrast_ratio(ink, *fill)).fold(f64::INFINITY, f64::min);
|
||||
inks.iter()
|
||||
.copied()
|
||||
.find(|ink| worst(*ink) >= min)
|
||||
.or_else(|| inks.iter().copied().max_by(|a, b| worst(*a).total_cmp(&worst(*b))))
|
||||
.unwrap_or(Color32::BLACK)
|
||||
}
|
||||
|
||||
/// Is `fill` dark enough that light artwork reads better on it?
|
||||
pub(crate) fn is_dark_fill(fill: Color32) -> bool {
|
||||
contrast_ratio(Color32::WHITE, fill) > contrast_ratio(Color32::BLACK, fill)
|
||||
}
|
||||
|
||||
impl TabAccent {
|
||||
pub fn for_tab(t: &Tokens, tab: &str) -> Self {
|
||||
let (color, ink, soft) = match tab {
|
||||
@@ -872,6 +894,17 @@ impl Tokens {
|
||||
egui::Shadow { offset: [0, self.space_2 as i8], blur: self.space_8 as u8, spread: 0, color: self.app_bar.gamma_multiply(0.18) }
|
||||
}
|
||||
|
||||
/// The accent ink (`form_primary`) on `fill`, or the theme's selected or body ink where the
|
||||
/// accent does not read there: a theme whose selection or highlight is its accent colour.
|
||||
pub(crate) fn accent_on(&self, fill: Color32) -> Color32 {
|
||||
readable(&[fill], MARK_CONTRAST, &[self.form_primary, self.selected_ink, self.form_ink])
|
||||
}
|
||||
|
||||
/// `ink` on the selected fill, or `selected_ink` where `ink` does not read at `min` there.
|
||||
pub(crate) fn on_selected(&self, ink: Color32, min: f64) -> Color32 {
|
||||
readable(&[self.selected_bg], min, &[ink, self.selected_ink])
|
||||
}
|
||||
|
||||
/// The X, Y and Z axis colours, in that order.
|
||||
pub const fn axes(&self) -> [Color32; 3] {
|
||||
[self.axis_x, self.axis_y, self.axis_z]
|
||||
|
||||
@@ -509,10 +509,38 @@ pub fn resolve(theme: &ThemeFile) -> Tokens {
|
||||
// A file that sets no colours is its base palette, exactly.
|
||||
if !theme.colors.is_empty() {
|
||||
file_view_from_theme(&mut t, theme.base.is_dark(), |k| theme.colors.contains_key(k));
|
||||
navigation_from_theme(&mut t, |k| theme.colors.contains_key(k));
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
/// The navigation bar and its menus are newer than most theme files: the navigation colours a
|
||||
/// theme leaves unset follow its own hover, dim text, control and dialog colours instead of its
|
||||
/// base's blue-greys, and its tooltip is the theme's ink and dialog colours turned around.
|
||||
fn navigation_from_theme(t: &mut Tokens, set: impl Fn(&str) -> bool) {
|
||||
if !set("nav_hover") {
|
||||
t.nav_hover = t.hover;
|
||||
}
|
||||
if !set("nav_press") {
|
||||
t.nav_press = t.nav_hover.lerp_to_gamma(t.form_ink, 0.12);
|
||||
}
|
||||
if !set("nav_dim") {
|
||||
t.nav_dim = t.text_dim;
|
||||
}
|
||||
if !set("nav_field") {
|
||||
t.nav_field = t.control_fill;
|
||||
}
|
||||
if !set("nav_viewport") {
|
||||
t.nav_viewport = t.dialog_body;
|
||||
}
|
||||
if !set("nav_tip") {
|
||||
t.nav_tip = t.form_ink;
|
||||
}
|
||||
if !set("nav_tip_ink") {
|
||||
t.nav_tip_ink = t.dialog_body;
|
||||
}
|
||||
}
|
||||
|
||||
/// The File view's colours a theme leaves unset follow the theme's own panel, border, accent and
|
||||
/// text, not its base's fixed blue rail and grey cards (#4868). Each ink is whichever candidate
|
||||
/// reads best on the fills it sits on.
|
||||
|
||||
@@ -290,7 +290,13 @@ pub(crate) fn offer_pill(
|
||||
let glyph = Rect::from_center_size(pos2(glyph_x, rect.center().y), vec2(16.0, 16.0));
|
||||
icons::paint_on(ui.painter(), glyph, icon, Some(true), 1.0);
|
||||
if let Some((text, _)) = label {
|
||||
ui.painter().text(pos2(rect.left() + text_x, rect.center().y), Align2::LEFT_CENTER, text, font, t.app_bar_text);
|
||||
// The pill is the theme's accent: a light accent (the phosphor themes) needs dark ink.
|
||||
let ink = if hot {
|
||||
t.app_bar_text
|
||||
} else {
|
||||
crate::theme::readable(&[fill], crate::theme::MARK_CONTRAST, &[t.app_bar_text, t.on_accent, t.tab_on_fill_dark])
|
||||
};
|
||||
ui.painter().text(pos2(rect.left() + text_x, rect.center().y), Align2::LEFT_CENTER, text, font, ink);
|
||||
}
|
||||
Some((rect, response))
|
||||
}
|
||||
|
||||
@@ -269,6 +269,8 @@ fn action_button(ui: &mut Ui, label: &str, kind: Action, look: &ActionLook, padd
|
||||
look.primary_ink
|
||||
} else if primary || !live {
|
||||
t.form_dim
|
||||
} else if marked {
|
||||
t.on_selected(t.form_ink, crate::theme::TEXT_CONTRAST)
|
||||
} else {
|
||||
t.form_ink
|
||||
});
|
||||
@@ -403,22 +405,26 @@ pub(crate) fn button_pair(ui: &mut egui::Ui, key: &str, states: [bool; 2], butto
|
||||
egui::StrokeKind::Inside,
|
||||
);
|
||||
let label = crate::i18n::tr_label(button.label);
|
||||
let galley = ui.painter().layout_no_wrap(label.to_string(), egui::FontId::proportional(t.font_label), t.form_ink);
|
||||
let ink = if on { t.on_selected(t.form_ink, crate::theme::TEXT_CONTRAST) } else { t.form_ink };
|
||||
let galley = ui.painter().layout_no_wrap(label.to_string(), egui::FontId::proportional(t.font_label), ink);
|
||||
let icon_width = if button.icon.is_some() { t.sketch_footer_icon_size + t.space_4 } else { 0.0 };
|
||||
let left = rect.center().x - (galley.size().x + icon_width) / 2.0;
|
||||
let painter = ui.painter().with_clip_rect(ui.clip_rect().intersect(rect));
|
||||
if let Some(icon) = button.icon {
|
||||
crate::icons::paint_icon(
|
||||
// A selection darker or lighter than the theme takes that side's artwork.
|
||||
let side = (on && crate::theme::is_dark_fill(t.selected_bg) != crate::theme::is_dark()).then_some(!crate::theme::is_dark());
|
||||
crate::icons::paint_on(
|
||||
&painter,
|
||||
egui::Rect::from_center_size(
|
||||
egui::pos2(left + t.sketch_footer_icon_size / 2.0, rect.center().y),
|
||||
vec2(t.sketch_footer_icon_size, t.sketch_footer_icon_size),
|
||||
),
|
||||
icon,
|
||||
side,
|
||||
f32::from(t.icon.a()) / 255.0,
|
||||
);
|
||||
}
|
||||
painter.galley(egui::pos2(left + icon_width, rect.center().y - galley.size().y / 2.0), galley, t.form_ink);
|
||||
painter.galley(egui::pos2(left + icon_width, rect.center().y - galley.size().y / 2.0), galley, ink);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), on, label));
|
||||
t.button_bevel(ui, &response, on);
|
||||
pointer_on_hover(ui, &response);
|
||||
@@ -602,7 +608,7 @@ pub(crate) fn compact_button(ui: &mut Ui, glyph: &str, tooltip: &str, enabled: b
|
||||
let ink = if !enabled {
|
||||
t.form_dim.gamma_multiply(0.35)
|
||||
} else if selected {
|
||||
t.form_primary
|
||||
t.accent_on(t.selected_bg)
|
||||
} else {
|
||||
t.form_dim
|
||||
};
|
||||
|
||||
@@ -232,21 +232,37 @@ pub(crate) fn assistant_approval<R>(ui: &mut Ui, title: &str, body: impl FnOnce(
|
||||
#[cfg(any(not(target_arch = "wasm32"), test))]
|
||||
pub(crate) fn assistant_input(ui: &mut Ui, id: egui::Id, value: &mut String, hint: &str) -> Response {
|
||||
let t = Tokens::get();
|
||||
// The hint is painted here, not by the TextEdit: a multiline edit wraps its hint onto
|
||||
// several rows, and the hint must stay on one row, ending in an ellipsis when it does not fit.
|
||||
let mut response = ui.add(
|
||||
super::fields::text_input(egui::TextEdit::multiline(value))
|
||||
.id(id)
|
||||
.frame(egui::Frame::NONE)
|
||||
.background_color(egui::Color32::TRANSPARENT)
|
||||
.hint_text(hint)
|
||||
.desired_rows(2)
|
||||
.desired_width(f32::INFINITY)
|
||||
.min_size(vec2(0.0, t.control_height + t.space_8))
|
||||
.font(egui::FontId::proportional(t.font_label)),
|
||||
);
|
||||
super::text_menu::menu(ui, &mut response, value, super::text_menu::Field::LINES);
|
||||
if value.is_empty() {
|
||||
let width = (response.rect.width() - 2.0 * t.form_inset).max(0.0);
|
||||
let galley = assistant_hint_galley(ui, hint, width);
|
||||
let pos = egui::pos2(response.rect.left() + t.form_inset, response.rect.center().y - galley.size().y / 2.0);
|
||||
ui.painter().galley(pos, galley, ui.visuals().weak_text_color());
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
/// The composer hint laid out on one row of at most `width`, cut with an ellipsis.
|
||||
#[cfg(any(not(target_arch = "wasm32"), test))]
|
||||
pub(crate) fn assistant_hint_galley(ui: &Ui, hint: &str, width: f32) -> std::sync::Arc<egui::Galley> {
|
||||
let t = Tokens::get();
|
||||
let mut job = egui::text::LayoutJob::simple_singleline(hint.to_owned(), egui::FontId::proportional(t.font_label), ui.visuals().weak_text_color());
|
||||
job.wrap = egui::text::TextWrapping { max_width: width, max_rows: 1, break_anywhere: true, overflow_character: Some('\u{2026}') };
|
||||
ui.fonts_mut(|f| f.layout_job(job))
|
||||
}
|
||||
|
||||
/// Compact right-aligned approval actions; wrapping grows by control rows, not the remaining card height.
|
||||
#[cfg(any(not(target_arch = "wasm32"), test))]
|
||||
pub(crate) fn assistant_actions<R>(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
|
||||
@@ -258,6 +274,13 @@ pub(crate) fn assistant_actions<R>(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R)
|
||||
)
|
||||
}
|
||||
|
||||
/// Bottom corners follow the window; the top meets the footer rule square.
|
||||
#[cfg(any(not(target_arch = "wasm32"), test))]
|
||||
fn composer_corners(t: &Tokens) -> egui::CornerRadius {
|
||||
let r = t.radius_xl.saturating_sub(1);
|
||||
egui::CornerRadius { nw: 0, ne: 0, sw: r, se: r }
|
||||
}
|
||||
|
||||
/// Composer's rounded field surface with an accent border only while focused.
|
||||
#[cfg(any(not(target_arch = "wasm32"), test))]
|
||||
pub(crate) fn assistant_composer<R>(ui: &mut Ui, focused: bool, add: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
|
||||
@@ -265,7 +288,7 @@ pub(crate) fn assistant_composer<R>(ui: &mut Ui, focused: bool, add: impl FnOnce
|
||||
egui::Frame::NONE
|
||||
.fill(t.control_fill)
|
||||
.stroke(if focused { egui::Stroke::new(1.0, t.form_primary) } else { t.control_border_stroke(t.control_border) })
|
||||
.corner_radius(t.radius_xl)
|
||||
.corner_radius(composer_corners(&t))
|
||||
.inner_margin(egui::Margin::symmetric(t.space_10 as i8, t.space_8 as i8))
|
||||
.show(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width().max(0.0));
|
||||
@@ -303,8 +326,10 @@ pub(crate) fn assistant_composer_footer<R>(ui: &mut Ui, add: impl FnOnce(&mut Ui
|
||||
let t = Tokens::get();
|
||||
let output = egui::Frame::NONE
|
||||
.fill(t.dialog_footer)
|
||||
.corner_radius(egui::CornerRadius { nw: 0, ne: 0, sw: t.radius_xl.saturating_sub(1), se: t.radius_xl.saturating_sub(1) })
|
||||
.inner_margin(t.space_10)
|
||||
.corner_radius(composer_corners(&t))
|
||||
// No margin: the composer box inside fills this footer edge to edge. The text padding
|
||||
// lives on the box itself, so there is one inset, not two.
|
||||
.inner_margin(0)
|
||||
.show(ui, add);
|
||||
t.secondary_hline(ui.painter(), output.response.rect.x_range(), output.response.rect.top(), t.dialog_rule);
|
||||
output
|
||||
|
||||
@@ -164,7 +164,18 @@ pub(crate) fn segmented(ui: &mut Ui, selected: &mut usize, options: &[&str], con
|
||||
egui::pos2(text_rect.left() - t.space_6 - t.space_14 / 2.0, segment.center().y),
|
||||
vec2(t.space_14, t.space_14),
|
||||
);
|
||||
crate::icons::paint_ink(ui.painter(), icon_rect, icon, if on && workspace_accent { accent.ink } else { t.form_ink });
|
||||
crate::icons::paint_ink(
|
||||
ui.painter(),
|
||||
icon_rect,
|
||||
icon,
|
||||
if on && workspace_accent {
|
||||
accent.ink
|
||||
} else if on && !primary {
|
||||
t.on_selected(t.form_ink, crate::theme::MARK_CONTRAST)
|
||||
} else {
|
||||
t.form_ink
|
||||
},
|
||||
);
|
||||
}
|
||||
let text_rect = if compact { text_rect.intersect(segment.shrink2(vec2(t.space_4, 0.0))) } else { text_rect };
|
||||
paint_text(
|
||||
@@ -262,6 +273,10 @@ pub(crate) fn segmented_icons(ui: &mut Ui, selected: &mut usize, options: &[Icon
|
||||
if catalog {
|
||||
let icon_rect = egui::Rect::from_center_size(segment.center(), vec2(t.space_14, t.space_14));
|
||||
crate::icons::paint_ink(ui.painter(), icon_rect, option.icon, if on { accent.ink } else { t.form_dim });
|
||||
} else if on && crate::theme::is_dark_fill(t.selected_bg) != crate::theme::is_dark() {
|
||||
// A selection darker or lighter than the theme (SolidCraft 95's navy) takes the
|
||||
// artwork drawn for that side, so the option's line or dash still shows.
|
||||
crate::icons::paint_on(ui.painter(), icon_rect, option.icon, Some(!crate::theme::is_dark()), f32::from(t.icon.a()) / 255.0);
|
||||
} else {
|
||||
crate::icons::paint_icon(ui.painter(), icon_rect, option.icon, f32::from(t.icon.a()) / 255.0);
|
||||
}
|
||||
@@ -662,8 +677,16 @@ pub(crate) fn popup_list(
|
||||
inner.min,
|
||||
egui::pos2((inner.right() - kind_width - t.space_8).max(inner.left()), inner.bottom()),
|
||||
);
|
||||
super::text::paint_text(ui, text_rect, option.text, mono.clone(), if picked { t.form_primary } else { option.color }, false);
|
||||
ui.painter().text(inner.right_center(), egui::Align2::RIGHT_CENTER, kind, kind_font.clone(), t.form_dim);
|
||||
super::text::paint_text(
|
||||
ui,
|
||||
text_rect,
|
||||
option.text,
|
||||
mono.clone(),
|
||||
if picked { t.accent_on(t.selected_bg) } else { option.color },
|
||||
false,
|
||||
);
|
||||
let kind_ink = if picked { t.on_selected(t.form_dim, crate::theme::MARK_CONTRAST) } else { t.form_dim };
|
||||
ui.painter().text(inner.right_center(), egui::Align2::RIGHT_CENTER, kind, kind_font.clone(), kind_ink);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, ui.is_enabled(), picked, option.text));
|
||||
// Tip the full name first: long identifiers still clip inside a capped popup.
|
||||
let tip = if option.detail.is_empty() { option.text.to_owned() } else { format!("{}\n{}", option.text, option.detail) };
|
||||
|
||||
@@ -56,7 +56,13 @@ pub(crate) fn count_field(ui: &mut Ui, value: &mut String, limits: &Count<'_>) -
|
||||
if enabled && arrow.hovered() {
|
||||
painter.rect_filled(half.shrink(1.0), t.control_radius, t.selected_bg);
|
||||
}
|
||||
let ink = if enabled { t.form_ink } else { t.form_dim };
|
||||
let ink = if !enabled {
|
||||
t.form_dim
|
||||
} else if arrow.hovered() {
|
||||
t.on_selected(t.form_ink, crate::theme::MARK_CONTRAST)
|
||||
} else {
|
||||
t.form_ink
|
||||
};
|
||||
let (c, w, h) = (half.center(), (reserve * 0.18).max(2.0), (half.height() * 0.2).max(2.0));
|
||||
let tip = if delta > 0 { -h } else { h };
|
||||
painter.add(egui::Shape::convex_polygon(
|
||||
|
||||
@@ -277,14 +277,14 @@ impl<'a> MenuItem<'a> {
|
||||
let ink = if self.danger {
|
||||
t.form_error
|
||||
} else if hot {
|
||||
t.form_primary
|
||||
t.accent_on(t.accent_soft)
|
||||
} else {
|
||||
t.form_ink
|
||||
};
|
||||
let dim = if self.danger {
|
||||
t.form_error
|
||||
} else if hot {
|
||||
t.form_primary
|
||||
t.accent_on(t.accent_soft)
|
||||
} else {
|
||||
t.form_dim
|
||||
};
|
||||
@@ -299,7 +299,7 @@ impl<'a> MenuItem<'a> {
|
||||
let icon = Rect::from_center_size(pos2(r.left() + t.control_height / 2.0, r.center().y), vec2(t.space_16, t.space_16));
|
||||
if let Some(on) = self.checked {
|
||||
if on {
|
||||
super::checks::paint_check_glyph(&painter, icon, false, t.form_primary);
|
||||
super::checks::paint_check_glyph(&painter, icon, false, if hot { ink } else { t.form_primary });
|
||||
}
|
||||
} else if let Some(color) = self.swatch {
|
||||
painter.rect(icon, t.radius_sm, color, t.control_border_stroke(t.control_border), egui::StrokeKind::Inside);
|
||||
@@ -419,7 +419,7 @@ pub(crate) fn workspace_menu(ui: &mut egui::Ui, items: &[Item], key_row: Option<
|
||||
let badge = ui.painter().layout_no_wrap(
|
||||
badge.to_string(),
|
||||
FontId::proportional(crate::theme::Tokens::get().font_small),
|
||||
if current { t.on_accent } else { accent },
|
||||
if current { crate::theme::readable(&[accent], crate::theme::MARK_CONTRAST, &[t.on_accent, t.tab_on_fill_dark]) } else { accent },
|
||||
);
|
||||
let badge_rect = Rect::from_min_size(pos2(badge_x, y - 8.0), badge.size() + vec2(t.space_12, t.space_3));
|
||||
ui.painter().rect_filled(badge_rect, t.radius_lg, if current { accent } else { soft });
|
||||
|
||||
@@ -104,11 +104,16 @@ pub(crate) fn navigation_fill(t: &Tokens, fill: NavigationFill) -> Option<egui::
|
||||
/// The label and icon colour of a slot. An active slot keeps the accent ink on its selected fill
|
||||
/// where that reads; a theme whose selected fill is as dark as its accent (SolidCraft 95's navy)
|
||||
/// gets the theme's selected-text ink instead, so an active toggle never turns into a blank block.
|
||||
/// A hovered or pressed slot keeps the body ink unless the theme's press colour is as dark as
|
||||
/// it (1-bit's black press), where the selected-text ink takes over too.
|
||||
pub(crate) fn navigation_ink(t: &Tokens, fill: NavigationFill) -> egui::Color32 {
|
||||
if fill != NavigationFill::Active {
|
||||
return t.form_ink;
|
||||
let readable = |fill, ink| crate::theme::readable(&[fill], NAV_ACTIVE_MIN_CONTRAST, &[ink, t.selected_ink]);
|
||||
match fill {
|
||||
NavigationFill::None => t.form_ink,
|
||||
NavigationFill::Hover => readable(t.nav_hover, t.form_ink),
|
||||
NavigationFill::Press => readable(t.nav_press, t.form_ink),
|
||||
NavigationFill::Active => readable(t.selected_bg, t.form_primary),
|
||||
}
|
||||
if crate::theme::contrast_ratio(t.form_primary, t.selected_bg) >= NAV_ACTIVE_MIN_CONTRAST { t.form_primary } else { t.selected_ink }
|
||||
}
|
||||
|
||||
/// WCAG small-text contrast for an active slot's label on its selected fill.
|
||||
@@ -246,4 +251,88 @@ mod active_ink_tests {
|
||||
}
|
||||
assert!(failures.is_empty(), "active navigation slot unreadable:\n{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
/// Every fill the newer UI paints text or an icon on, with the ink it paints there, reads in
|
||||
/// every built-in theme: the navigation bar and its menus, viewport status items, the
|
||||
/// ribbon's drop-downs and workspace menu, the Render stages, toggles and segmented options,
|
||||
/// badges and the sketch status drawn on the view. A theme older than one of these surfaces
|
||||
/// left it with its base palette's colour, or with its accent on an accent-coloured fill
|
||||
/// (SolidCraft 95's navy on navy).
|
||||
#[test]
|
||||
fn newer_ui_text_and_icons_stay_readable_in_every_built_in_theme() {
|
||||
use crate::theme::{MARK_CONTRAST, TEXT_CONTRAST, TabAccent, Tokens, contrast_ratio, readable};
|
||||
use egui::Color32;
|
||||
type Pick = fn(&Tokens) -> Color32;
|
||||
let status: Pick = |t| readable(&[t.viewport_top, t.viewport_bottom], MARK_CONTRAST, &[t.sketch_accent, t.sketch_ink, t.text]);
|
||||
let conflict: Pick = |t| readable(&[t.viewport_top, t.viewport_bottom], MARK_CONTRAST, &[t.error, t.form_error, t.text]);
|
||||
// (where, fill, ink, floor)
|
||||
let pairs: &[(&str, Pick, Pick, f64)] = &[
|
||||
("selected label: navigation menu, Preferences pages, picked rows", |t| t.selected_bg, |t| t.accent_on(t.selected_bg), MARK_CONTRAST),
|
||||
("navigation menu: selected row detail", |t| t.selected_bg, |t| t.on_selected(t.form_dim, MARK_CONTRAST), MARK_CONTRAST),
|
||||
("navigation bar: hovered slot", |t| t.nav_hover, |t| navigation_ink(t, NavigationFill::Hover), TEXT_CONTRAST),
|
||||
("navigation bar: pressed slot", |t| t.nav_press, |t| navigation_ink(t, NavigationFill::Press), TEXT_CONTRAST),
|
||||
("navigation menu: hovered row icon", |t| t.nav_hover, |t| t.form_dim, MARK_CONTRAST),
|
||||
("navigation settings: add slot", |t| t.nav_hover, |t| t.accent_on(t.nav_hover), MARK_CONTRAST),
|
||||
("navigation tooltip", |t| t.nav_tip, |t| t.nav_tip_ink, TEXT_CONTRAST),
|
||||
("viewport status item: name", |t| t.overlay, |t| t.text, TEXT_CONTRAST),
|
||||
("viewport status item: setting", |t| t.overlay, |t| t.text_dim, MARK_CONTRAST),
|
||||
("ribbon: section label", |t| t.toolbar, |t| t.text_dim, MARK_CONTRAST),
|
||||
("ribbon: hovered section label", |t| t.hover, |t| t.text_dim, MARK_CONTRAST),
|
||||
("panels: secondary text", |t| t.panel, |t| t.text_dim, MARK_CONTRAST),
|
||||
("ribbon drop-down: highlighted row", |t| t.accent_soft, |t| t.accent_on(t.accent_soft), MARK_CONTRAST),
|
||||
(
|
||||
"workspace menu: current badge",
|
||||
|t| t.accent,
|
||||
|t| readable(&[t.accent], MARK_CONTRAST, &[t.on_accent, t.tab_on_fill_dark]),
|
||||
MARK_CONTRAST,
|
||||
),
|
||||
(
|
||||
"Render stage bar: current stage number",
|
||||
|t| TabAccent::for_tab(t, "RENDER").color,
|
||||
|t| TabAccent::for_tab(t, "RENDER").on_fill(t),
|
||||
MARK_CONTRAST,
|
||||
),
|
||||
("toggle buttons, keyboard-marked action: on", |t| t.selected_bg, |t| t.on_selected(t.form_ink, TEXT_CONTRAST), TEXT_CONTRAST),
|
||||
("toggle and segmented icons: on", |t| t.selected_bg, |t| t.on_selected(t.form_ink, MARK_CONTRAST), MARK_CONTRAST),
|
||||
("segmented choice: selected label", |t| t.selected_bg, |t| t.selected_ink, TEXT_CONTRAST),
|
||||
("settings and search badges: valid", |t| t.form_success_fill, |t| t.form_success, MARK_CONTRAST),
|
||||
("search badges: invalid", |t| t.form_error_fill, |t| t.form_error, MARK_CONTRAST),
|
||||
("dialogs: success text", |t| t.dialog_body, |t| t.form_success, MARK_CONTRAST),
|
||||
(
|
||||
"title bar: offer pill",
|
||||
|t| t.accent,
|
||||
|t| readable(&[t.accent], MARK_CONTRAST, &[t.app_bar_text, t.on_accent, t.tab_on_fill_dark]),
|
||||
MARK_CONTRAST,
|
||||
),
|
||||
("save mesh: lettered tile", |t| t.chip, |t| readable(&[t.chip], TEXT_CONTRAST, &[t.text, t.on_accent]), MARK_CONTRAST),
|
||||
("sketch status on the view, top", |t| t.viewport_top, status, MARK_CONTRAST),
|
||||
("sketch status on the view, bottom", |t| t.viewport_bottom, status, MARK_CONTRAST),
|
||||
("sketch conflict status on the view, top", |t| t.viewport_top, conflict, MARK_CONTRAST),
|
||||
("sketch conflict status on the view, bottom", |t| t.viewport_bottom, conflict, MARK_CONTRAST),
|
||||
];
|
||||
// A translucent fill is seen over the panel it floats on, translucent ink over its fill.
|
||||
let solid = |c: Color32, under: Color32| {
|
||||
if c.a() == 255 {
|
||||
return c;
|
||||
}
|
||||
let k = f32::from(c.a()) / 255.0;
|
||||
let [r, g, b, _] = c.to_srgba_unmultiplied();
|
||||
let mix = |x: u8, y: u8| (f32::from(x) * k + f32::from(y) * (1.0 - k)).round() as u8;
|
||||
Color32::from_rgb(mix(r, under.r()), mix(g, under.g()), mix(b, under.b()))
|
||||
};
|
||||
let mut themes = vec![("Dark".to_owned(), Tokens::dark()), ("Light".to_owned(), Tokens::light())];
|
||||
themes.extend(crate::theme_files::builtin().iter().map(|theme| (theme.name.clone(), crate::theme_files::resolve(theme))));
|
||||
let mut failures = Vec::new();
|
||||
for (name, t) in &themes {
|
||||
for (place, fill, ink, floor) in pairs {
|
||||
let fill = solid(fill(t), t.panel);
|
||||
let ink = solid(ink(t), fill);
|
||||
let ratio = contrast_ratio(ink, fill);
|
||||
if ratio < *floor {
|
||||
failures.push(format!("{name}: {place}: {ratio:.2}:1 ({ink:?} on {fill:?})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "unreadable in a built-in theme:\n{}", failures.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ impl<'a> NavigationMenuRow<'a> {
|
||||
let t = Tokens::get();
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width().max(0.0), NAV_MENU_ROW), Sense::hover());
|
||||
let response = ui.interact(rect, ui.id().with(self.id), if self.enabled { Sense::click() } else { Sense::hover() });
|
||||
let ink = if self.selected { t.form_primary } else { t.form_ink };
|
||||
let ink = if self.selected { t.accent_on(t.selected_bg) } else { t.form_ink };
|
||||
let bg = if self.selected {
|
||||
Some(t.selected_bg)
|
||||
} else if response.hovered() && self.enabled {
|
||||
@@ -69,7 +69,13 @@ impl<'a> NavigationMenuRow<'a> {
|
||||
Align2::RIGHT_CENTER,
|
||||
detail,
|
||||
FontId::proportional(if self.enabled { t.font_body } else { t.font_small }),
|
||||
if self.enabled { t.form_dim } else { t.nav_dim },
|
||||
if !self.enabled {
|
||||
t.nav_dim
|
||||
} else if self.selected {
|
||||
t.on_selected(t.form_dim, crate::theme::MARK_CONTRAST)
|
||||
} else {
|
||||
t.form_dim
|
||||
},
|
||||
);
|
||||
end = end.min(detail.left() - t.space_8);
|
||||
}
|
||||
@@ -81,7 +87,7 @@ impl<'a> NavigationMenuRow<'a> {
|
||||
ui.painter().galley(pos2(left, rect.center().y - galley.size().y / 2.0), galley, ink);
|
||||
let tail = Rect::from_center_size(pos2(rect.right() - NAV_MENU_INSET - t.space_14 / 2.0, rect.center().y), vec2(t.space_14, t.space_14));
|
||||
if self.selected {
|
||||
super::checks::paint_check_glyph(ui.painter(), tail, false, t.form_primary);
|
||||
super::checks::paint_check_glyph(ui.painter(), tail, false, ink);
|
||||
} else if self.submenu {
|
||||
let arrow =
|
||||
Rect::from_center_size(pos2(rect.right() - NAV_MENU_INSET - NAV_CHEVRON / 2.0, rect.center().y), vec2(NAV_CHEVRON, NAV_CHEVRON));
|
||||
|
||||
@@ -63,7 +63,7 @@ pub(crate) fn navigation_tile(ui: &mut Ui, id: &str, label: &str, icon: &str, dr
|
||||
if add.hovered() {
|
||||
painter.rect_filled(add_rect, t.control_radius.saturating_sub(NAV_BORDER as u8), t.nav_hover);
|
||||
}
|
||||
icons::paint_ink(&painter, Rect::from_center_size(add_rect.center(), vec2(t.space_14, t.space_14)), "plus", t.form_primary);
|
||||
icons::paint_ink(&painter, Rect::from_center_size(add_rect.center(), vec2(t.space_14, t.space_14)), "plus", t.accent_on(t.nav_hover));
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), label));
|
||||
add.widget_info(|| {
|
||||
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), crate::i18n::format("navbar.custom.add_slot", &[("slot", label)]))
|
||||
|
||||
@@ -1234,7 +1234,7 @@ fn page_list_sized<S>(
|
||||
if on || response.hovered() || response.has_focus() {
|
||||
ui.painter().rect_filled(rect, t.control_radius, if on { t.selected_bg } else { t.hover });
|
||||
}
|
||||
let ink = if on { t.form_primary } else { t.form_ink };
|
||||
let ink = if on { t.accent_on(t.selected_bg) } else { t.form_ink };
|
||||
let icon =
|
||||
egui::Rect::from_center_size(egui::pos2(rect.left() + t.space_10 + t.space_8, rect.center().y), vec2(t.space_16, t.space_16));
|
||||
crate::icons::paint_ink(ui.painter(), icon, page.icon, if on { ink } else { t.form_dim });
|
||||
|
||||
@@ -186,7 +186,7 @@ impl StageBar {
|
||||
},
|
||||
&(index + 1).to_string(),
|
||||
crate::fonts::semibold(t.font_small),
|
||||
if on { t.on_accent } else { t.text_dim },
|
||||
if on { accent.on_fill(&t) } else { t.text_dim },
|
||||
false,
|
||||
);
|
||||
let text = Rect::from_min_max(pos2(badge.right() + t.space_6 + 1.0, chip.top()), chip.right_bottom() - vec2(t.space_14, 0.0));
|
||||
|
||||
@@ -352,7 +352,7 @@ pub(crate) fn plus_header<R>(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> Opt
|
||||
let response = super::actions::icon_button_colored(ui, rect, "plus", "Add Column", Some(t.form_dim));
|
||||
if response.hovered() {
|
||||
ui.painter().rect_filled(rect, t.control_radius, t.selected_bg);
|
||||
crate::icons::paint_ink(ui.painter(), rect.shrink(t.form_icon_inset), "plus", t.form_primary);
|
||||
crate::icons::paint_ink(ui.painter(), rect.shrink(t.form_icon_inset), "plus", t.accent_on(t.selected_bg));
|
||||
for x in [rect.left() - t.form_gap_y, rect.right() + t.form_gap_y] {
|
||||
ui.painter().vline(x, rect.y_range(), egui::Stroke::new(t.space_2, t.form_primary));
|
||||
}
|
||||
|
||||
@@ -889,6 +889,12 @@ fn poll_install(app: &mut SolveApp) {
|
||||
Err(TryRecvError::Disconnected) => Err("the update stopped".into()),
|
||||
};
|
||||
app.updater.job = None;
|
||||
// Cancel already cleared the job in the UI; ignore a late "cancelled" result so Offer returns
|
||||
// without an error banner (#8908).
|
||||
if matches!(&result, Err(e) if e == "cancelled" || e.contains("cancelled")) {
|
||||
app.updater.installed = None;
|
||||
return;
|
||||
}
|
||||
match &result {
|
||||
Ok(install::Outcome::Installer(program, args)) => {
|
||||
// The installer replaces this program: start it once SolidCraft has closed. On
|
||||
@@ -1084,9 +1090,12 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
Some("restart") => restart(app),
|
||||
Some("close") => app.updater.open = false,
|
||||
Some("cancel_download") => {
|
||||
if let Some((_, progress)) = &app.updater.job {
|
||||
// Drop the job from the UI at once so Cancel leaves "Downloading" immediately; the
|
||||
// worker still sees the flag and stops at the next feed/checksum/chunk check (#8908).
|
||||
if let Some((_, progress)) = app.updater.job.take() {
|
||||
progress.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
app.updater.installed = None;
|
||||
}
|
||||
_ if closed => app.updater.open = false,
|
||||
_ => {}
|
||||
|
||||
@@ -172,6 +172,19 @@ pub struct Net {
|
||||
pub feed: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// GET `url` unless Cancel was pressed; check again after the response so a cancel during the
|
||||
/// wait still ends the install without starting the package download (#8908).
|
||||
fn get_unless_cancelled(net: &Net, progress: &Progress, url: &str) -> Result<Vec<u8>, FetchError> {
|
||||
if progress.cancel.load(Ordering::Relaxed) {
|
||||
return Err(FetchError::Failed("cancelled".into()));
|
||||
}
|
||||
let body = (net.get)(url)?;
|
||||
if progress.cancel.load(Ordering::Relaxed) {
|
||||
return Err(FetchError::Failed("cancelled".into()));
|
||||
}
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Refresh the feed, then download, verify and apply the newest release for `method`, falling
|
||||
/// back to `release` when the check fails. Runs on a background thread. `staging` holds downloads
|
||||
/// that are not written next to their target (the `.msi`, the `.dmg`).
|
||||
@@ -184,6 +197,11 @@ pub fn run(
|
||||
staging: &Path,
|
||||
progress: &Progress,
|
||||
) -> Result<Outcome, String> {
|
||||
// Honour Cancel before each network wait: feed refresh and checksum fetches used to run
|
||||
// without looking at the flag, so Cancel looked dead until the body download started (#8908).
|
||||
if progress.cancel.load(Ordering::Relaxed) {
|
||||
return Err("cancelled".into());
|
||||
}
|
||||
// The release shown may be stale by now: install the newest one the feed names, or the one
|
||||
// shown when the feed can't be read.
|
||||
let (release, fallback) = match super::fetch_latest(net.feed.as_deref(), Some(&net.get)) {
|
||||
@@ -200,7 +218,7 @@ pub fn run(
|
||||
return Err(format!("{} is too large ({} bytes)", asset.name, asset.size));
|
||||
}
|
||||
let sums_url = release.assets.iter().find(|a| a.name == SUMS).map(|a| a.url.clone()).ok_or_else(|| format!("this release has no {SUMS}"))?;
|
||||
let sums = (net.get)(&sums_url).map_err(|e| format!("{SUMS}: {e}"))?;
|
||||
let sums = get_unless_cancelled(net, progress, &sums_url).map_err(|e| format!("{SUMS}: {e}"))?;
|
||||
if let Some(key) = PUBLIC_KEY {
|
||||
let sig_url = release
|
||||
.assets
|
||||
@@ -208,7 +226,7 @@ pub fn run(
|
||||
.find(|a| a.name == SUMS_SIG)
|
||||
.map(|a| a.url.clone())
|
||||
.ok_or_else(|| format!("this release isn't signed (no {SUMS_SIG})"))?;
|
||||
let sig = (net.get)(&sig_url).map_err(|e| format!("{SUMS_SIG}: {e}"))?;
|
||||
let sig = get_unless_cancelled(net, progress, &sig_url).map_err(|e| format!("{SUMS_SIG}: {e}"))?;
|
||||
verify_signature(&sums, &String::from_utf8_lossy(&sig), key, &release.tag)?;
|
||||
}
|
||||
let sums = String::from_utf8_lossy(&sums);
|
||||
|
||||
@@ -129,12 +129,41 @@ fn an_appimage_is_replaced_only_when_it_verifies() {
|
||||
let bare = release_with(&[(name, new.len())]);
|
||||
assert!(run(&bare, &kind, &m, "x86_64", &n, &dir, &Progress::default()).unwrap_err().contains(SUMS));
|
||||
|
||||
// Cancelled: nothing changes.
|
||||
// Cancelled before any network work: nothing changes.
|
||||
let good = format!("{} {name}\n", hex(&new));
|
||||
let n = net(vec![(name.into(), new.clone()), (SUMS.into(), good.clone().into_bytes())]);
|
||||
let p = Progress::default();
|
||||
p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
assert!(run(&r, &kind, &m, "x86_64", &n, &dir, &p).is_err());
|
||||
let err = run(&r, &kind, &m, "x86_64", &n, &dir, &p).unwrap_err();
|
||||
assert!(err.contains("cancelled"), "{err}");
|
||||
assert_eq!(std::fs::read(&target).unwrap(), b"old version");
|
||||
|
||||
// Cancel during the checksum GET: package download never starts (#8908).
|
||||
let progress = Arc::new(Progress::default());
|
||||
let flag = progress.clone();
|
||||
let downloaded = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let hit = downloaded.clone();
|
||||
let files = Arc::new(vec![(name.into(), new.clone()), (SUMS.into(), good.clone().into_bytes())]);
|
||||
let f2 = files.clone();
|
||||
let find = |files: &[(String, Vec<u8>)], url: &str| files.iter().find(|(n, _)| url == format!("{BASE}{n}")).map(|(_, b)| b.clone());
|
||||
let n = Net {
|
||||
feed: None,
|
||||
get: Arc::new(move |url: &str| {
|
||||
let body = find(&files, url).ok_or_else(|| FetchError::Failed(format!("404 {url}")))?;
|
||||
if url.ends_with(SUMS) {
|
||||
flag.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
Ok(body)
|
||||
}),
|
||||
download: Arc::new(move |url: &str, to: &Path, _| {
|
||||
hit.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let body = find(&f2, url).ok_or_else(|| FetchError::Failed(format!("404 {url}")))?;
|
||||
std::fs::write(to, body).map_err(|e| FetchError::Failed(e.to_string()))
|
||||
}),
|
||||
};
|
||||
let err = run(&r, &kind, &m, "x86_64", &n, &dir, &progress).unwrap_err();
|
||||
assert!(err.contains("cancelled"), "{err}");
|
||||
assert!(!downloaded.load(std::sync::atomic::Ordering::Relaxed), "package must not start after Cancel");
|
||||
assert_eq!(std::fs::read(&target).unwrap(), b"old version");
|
||||
|
||||
// Verified: renamed over the old one, executable, restart offered.
|
||||
|
||||
@@ -750,3 +750,48 @@ fn install_selection_refreshes_the_button_and_progress_before_completion() {
|
||||
assert!(h.app.updater.installed.is_none());
|
||||
tx.send(()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_download_leaves_downloading_immediately() {
|
||||
// Cancel must clear the job from the UI at once so the card leaves "Downloading" without
|
||||
// waiting for a stalled feed or checksum GET (#8908).
|
||||
let dir = scratch();
|
||||
let target = dir.0.join("SolidCraft.AppImage");
|
||||
let name = asset_name(&InstallKind::AppImage(target.clone()), std::env::consts::ARCH, "999.1.0").unwrap();
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.preferences.check_updates = false;
|
||||
let mut shown = release();
|
||||
shown.tag = "v999.0.0".into();
|
||||
h.app.updater.release = Some(shown);
|
||||
h.app.updater.kind = Some(InstallKind::AppImage(target));
|
||||
h.app.updater.open = true;
|
||||
let (gate_tx, gate_rx) = std::sync::mpsc::channel();
|
||||
let gate_rx = std::sync::Mutex::new(gate_rx);
|
||||
h.app.services.http_get = Some(Arc::new(move |url| {
|
||||
if url == LATEST_API {
|
||||
return Ok(json!({"tag_name":"v999.1.0", "assets":[
|
||||
{"name": name, "browser_download_url":format!("https://github.com/tangency-labs/solidcraft/releases/download/v999.1.0/{name}")},
|
||||
{"name": install::SUMS, "browser_download_url":"https://github.com/tangency-labs/solidcraft/releases/download/v999.1.0/SHA256SUMS.txt"}
|
||||
]}).to_string().into_bytes());
|
||||
}
|
||||
// Stall checksum GET until the test ends so Cancel has to work before the body download.
|
||||
let _ = gate_rx.lock().unwrap().recv();
|
||||
Ok(format!("{} {name}\n", "a".repeat(64)).into_bytes())
|
||||
}));
|
||||
h.app.services.http_download = Some(Arc::new(|_, _, _| Err(FetchError::Failed("download must not start".into()))));
|
||||
start_install(&mut h.app, &egui::Context::default());
|
||||
assert!(h.app.updater.job.is_some());
|
||||
h.frames(2);
|
||||
let cancel = h.widget("Cancel").expect("Cancel while Downloading");
|
||||
h.call("ui.click", json!({"x": cancel[0], "y": cancel[1]}));
|
||||
assert!(h.app.updater.job.is_none(), "Cancel clears the job immediately");
|
||||
assert!(h.app.updater.installed.is_none());
|
||||
assert!(h.app.updater.open, "Cancel leaves the Update window open on Offer");
|
||||
h.frames(2);
|
||||
assert!(
|
||||
h.widget("Remind me later").is_some() || h.widget("Skip this version").is_some() || h.widget("Update now").is_some(),
|
||||
"Offer returns after Cancel"
|
||||
);
|
||||
assert!(h.widget("Updating to 999.1.0.").is_none());
|
||||
let _ = gate_tx.send(());
|
||||
}
|
||||
|
||||
@@ -4536,6 +4536,11 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
.collect();
|
||||
let _ = app.run("select.set", json!({ "items": items, "add": add }));
|
||||
}
|
||||
// Double-click a curve while a Mirror/Move/Copy/Scale/pattern Entities input is open:
|
||||
// add the whole connected chain to that input (#8390).
|
||||
if resp.double_clicked() && !held && !dim_edit && app.tool.is_none() && app.dialog.is_some() {
|
||||
let _ = crate::sketch_modify_ui::double_click_entities(app);
|
||||
}
|
||||
// Double-click an edge: select the edges that continue it smoothly (the loop).
|
||||
if resp.double_clicked()
|
||||
&& !held
|
||||
@@ -5944,7 +5949,10 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
crate::i18n::format("sketch.degrees_of_freedom", &[("count", &ss.report.dof.to_string())])
|
||||
};
|
||||
let msg = format!("{} — {status}", crate::i18n::tr_name(&ss.name));
|
||||
let c = if ss.report.ok() { v.sketch_accent } else { v.error };
|
||||
// Straight on the view's gradient: a theme whose sketch or error colour is close to its
|
||||
// background (SolidCraft 95's green and red on teal) gets the view's text colour.
|
||||
let inks = if ss.report.ok() { [v.sketch_accent, v.sketch_ink, v.text] } else { [v.error, v.form_error, v.text] };
|
||||
let c = crate::theme::readable(&[v.viewport_top, v.viewport_bottom], crate::theme::MARK_CONTRAST, &inks);
|
||||
painter.text(pos2(r.left() + 12.0, r.top() + 12.0), Align2::LEFT_TOP, msg, FontId::proportional(t.font_button), c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[
|
||||
{"note": "#8390: one click inside a closed profile selects its whole boundary for the shared Entities input; a double-click selects a curve's chain."},
|
||||
{"queued": "engine.execute", "params": {"command": "sketch.create", "params": {"plane": "XY"}}},
|
||||
{"queued": "engine.execute", "params": {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [40, 30]}}},
|
||||
{"queued": "engine.execute", "params": {"command": "sketch.line", "params": {"points": [[-10, -40], [-10, 40]]}}},
|
||||
{"expect": {"errors": 0, "sketch_curves": {"own": 5}}},
|
||||
|
||||
{"note": "Open Mirror; a single click inside the rectangle selects the whole profile."},
|
||||
{"start": "sketch.mirror"},
|
||||
{"expect": {"dialog": "SketchModify(Mirror"}},
|
||||
{"click": {"sketch": [20, 15]}},
|
||||
{"expect": {"dialog": "SketchModify(Mirror", "errors": 0}},
|
||||
|
||||
{"note": "Pick the line as the mirror line and OK: all four rectangle sides reflect (4 -> 8, plus the line)."},
|
||||
{"click": {"handle": "dialog:Mirror line"}},
|
||||
{"click": {"sketch": [-10, 0]}},
|
||||
{"click": {"handle": "dialog:OK"}},
|
||||
{"expect": {"dialog": null, "errors": 0, "sketch_curves": {"own": 9}}}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{"note": "A mirror line that is itself among the entities (a chain or profile picked in one action) is not rejected: the command leaves the line in place and mirrors the rest."},
|
||||
{"queued": "engine.execute", "params": {"command": "sketch.create", "params": {"plane": "XY"}}},
|
||||
{"queued": "engine.execute", "params": {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [40, 30]}}},
|
||||
{"expect": {"errors": 0, "sketch_curves": {"own": 4}}},
|
||||
|
||||
{"note": "One click inside the rectangle selects the whole profile (including its left edge)."},
|
||||
{"start": "sketch.mirror"},
|
||||
{"click": {"sketch": [20, 15]}},
|
||||
{"expect": {"dialog": "SketchModify(Mirror"}},
|
||||
|
||||
{"note": "Pick the rectangle's own left edge as the mirror line and OK: the left edge stays put and the other three sides reflect (4 -> 7)."},
|
||||
{"click": {"handle": "dialog:Mirror line"}},
|
||||
{"click": {"sketch": [0, 15]}},
|
||||
{"click": {"handle": "dialog:OK"}},
|
||||
{"expect": {"dialog": null, "errors": 0, "sketch_curves": {"own": 7}}}
|
||||
]
|
||||
+2
-1
@@ -19,7 +19,8 @@ versions. Two preferences opt into holding the model:
|
||||
While editing a sketch, the cap pans and zooms without changing the sketch's initial view. After you
|
||||
adjust the view with the navigation cube, the cap can rotate it too. The Navigation
|
||||
page in Preferences has an on/off switch, a speed control and, for each of the cap's six axes, an
|
||||
invert switch and a speed for the 3D mouse. The web app reads
|
||||
invert switch and a speed for the 3D mouse. The cap's zoom direction comes from its own invert
|
||||
switch; **Reverse zoom direction** under Wheel and trackpad reverses the wheel only (#8993). The web app reads
|
||||
it too, through the browser; Firefox needs a 3DxWare setting first (see
|
||||
[Getting started › 3D mouse in the browser](book/src/guide/getting-started.md#3d-mouse-in-the-browser)).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user