diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index c11676a47..1fdd169af 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = التطبيق الافتراضي .iced-theme = سمة Iced: .theme = السمة + .right-click-customization = تخصيص النقر بالزر الأيمن + .right-click-in-drawing-area = النقر بالزر الأيمن في منطقة الرسم + .hold-duration = مدة الضغط + .right-click-mode-help = قائمة الاختصارات: النقر بالزر الأيمن يفتح القائمة دائمًا. حساس للوقت: النقرة السريعة تعني Enter، والضغط المطوّل يفتح القائمة (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = قائمة الاختصارات + .right-click-mode-time-sensitive = حساس للوقت (نقرة سريعة = Enter) + .right-click-mode-enter-first = Enter أولًا، النقرة الثانية = القائمة command-line = .ready = ‏Open CAD Studio جاهز. @@ -249,6 +256,7 @@ command-move = *[other] { $count } كائن }]: .target = MOVE حدد الوجهة [الأساس { $x },{ $y }]: + .displacement = MOVE حدد الإزاحة: command-copy = .array-count = COPY أدخل عدد عناصر المصفوفة: @@ -271,6 +279,8 @@ command-copy = [many] { $count } نسخة حتى الآن *[other] { $count } نسخة حتى الآن } | مصفوفة | Enter=إنهاء | الأساس { $x },{ $y }]: + .displacement = COPY حدد الإزاحة: + .mode = COPY أدخل خيار وضع النسخ [Single/Multiple] : common = .jogged-radius = نصف قطر منكسر @@ -2130,7 +2140,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2836,6 +2846,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = إزالة + .pline-specify-starting-width = PLINE حدد عرض البداية <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE حدد عرض النهاية <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE حدد نصف عرض البداية <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE حدد نصف عرض النهاية <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (خط) حدد النقطة التالية: + .pline-arc-specify-endpoint-of-arc = PLINE (قوس) حدد نقطة نهاية القوس: + .pline-specify-length-of-line = PLINE حدد طول الخط: + .pline-specify-included-angle = PLINE حدد الزاوية المحصورة: + .pline-specify-endpoint-of-arc = PLINE حدد نقطة نهاية القوس: + .pline-specify-center-point-of-arc = PLINE حدد مركز القوس: + .pline-specify-radius-of-arc = PLINE حدد نصف قطر القوس: + .pline-specify-direction-of-chord-for-arc = PLINE حدد اتجاه وتر القوس: + .pline-specify-length-of-chord = PLINE حدد طول الوتر: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE حدد اتجاه المماس عند نقطة بداية القوس: + .pline-specify-second-point-on-arc = PLINE حدد النقطة الثانية على القوس: + .halfwidth = نصف العرض + .center-keyword = المركز + .close-keyword = إغلاق + .second-pt = النقطة الثانية + .arc-specify-start-point-of-arc = ARC حدد نقطة بداية القوس: + .arc-specify-second-point-of-arc = ARC حدد النقطة الثانية للقوس: + .arc-specify-end-point-of-arc = ARC حدد نقطة نهاية القوس: + .arc-specify-center-point-of-arc = ARC حدد مركز القوس: + .arc-specify-included-angle = ARC حدد الزاوية المحصورة: + .arc-specify-length-of-chord = ARC حدد طول الوتر: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC حدد اتجاه المماس عند نقطة بداية القوس: + .arc-specify-radius-of-arc = ARC حدد نصف قطر القوس: + .chord-length = طول الوتر modify = .add-vertex = إضافة رأس @@ -2988,6 +3026,13 @@ modify = .tol-scale-dimtfac = مقياس التفاوت (DIMTFAC) .trim = قص .exit = خروج + .area-keyword = المساحة + .displacement = إزاحة + .mode-keyword = الوضع + .array-keyword = مصفوفة + .use-first-point-as-displacement = استخدام النقطة الأولى كإزاحة + .offset-erase-source-object-after-offsetting = OFFSET مسح الكائن المصدر بعد الإزاحة؟ [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET أدخل خيار الطبقة للكائنات المزاحة [Current/Source] : groups = .group-enter-group-name-name = GROUP أدخل اسم المجموعة [__ocs_arg_name__]: @@ -5277,6 +5322,19 @@ ui = .what-s-new = ما الجديد .width-value = العرض: __ocs_arg_value__ .working = جارٍ العمل… + .recent-input = الإدخالات الأخيرة + .copy-with-base-point = نسخ مع نقطة أساس + .undo-label = تراجع عن __ocs_arg_label__ + .redo-label = إعادة __ocs_arg_label__ + .copy-selection = نسخ التحديد + .isolate = عزل + .deselect-all = إلغاء تحديد الكل + .options-ellipsis = خيارات... + .base-point = نقطة الأساس + .snap-overrides = تجاوزات الالتقاط + .mid-between-2-points = المنتصف بين نقطتين + .osnap-settings = إعدادات الالتقاط... + .snap-override-none-next-pick-only = تجاوز الالتقاط: بدون (للاختيار التالي فقط). dimstyle = .keep-text-between-extension-lines = إبقاء النص دائمًا بين خطوط الامتداد diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 1eb0a901d..7b0c44e00 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Приложение по подразбиране .iced-theme = Iced тема: .theme = Тема + .right-click-customization = Настройка на десния бутон + .right-click-in-drawing-area = Десен бутон в чертожната област + .hold-duration = Време на задържане + .right-click-mode-help = Контекстно меню: десният бутон винаги отваря менюто. Зависимо от времето: бързо щракване е Enter, по-дълго задържане отваря менюто (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Контекстно меню + .right-click-mode-time-sensitive = Зависимо от времето (бързо щракване = Enter) + .right-click-mode-enter-first = Първо Enter, второ щракване = меню command-line = .ready = Open CAD Studio е готов. @@ -245,6 +252,7 @@ command-move = *[other] { $count } обекта }]: .target = MOVE Посочи местоназначение [база { $x },{ $y }]: + .displacement = MOVE Задайте преместване: command-copy = .array-count = COPY Въведи брой елементи за масив: @@ -259,6 +267,8 @@ command-copy = [one] { $count } копие досега *[other] { $count } копия досега } | Масив | Enter=готово | база { $x },{ $y }]: + .displacement = COPY Задайте преместване: + .mode = COPY Въведете режим на копиране [Single/Multiple] : common = .jogged-radius = Радиус с чупка .none-use-shortcuts-set-key-command = (няма — използвай: SHORTCUTS SET <клавиш> <команда>) @@ -2117,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2823,6 +2833,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Премахване + .pline-specify-starting-width = PLINE Задайте начална ширина <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Задайте крайна ширина <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Задайте начална полуширина <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Задайте крайна полуширина <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Линия) Задайте следваща точка: + .pline-arc-specify-endpoint-of-arc = PLINE (Дъга) Задайте крайна точка на дъгата: + .pline-specify-length-of-line = PLINE Задайте дължина на линията: + .pline-specify-included-angle = PLINE Задайте вписан ъгъл: + .pline-specify-endpoint-of-arc = PLINE Задайте крайна точка на дъгата: + .pline-specify-center-point-of-arc = PLINE Задайте център на дъгата: + .pline-specify-radius-of-arc = PLINE Задайте радиус на дъгата: + .pline-specify-direction-of-chord-for-arc = PLINE Задайте посока на хордата на дъгата: + .pline-specify-length-of-chord = PLINE Задайте дължина на хордата: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Задайте посока на допирателната в началото на дъгата: + .pline-specify-second-point-on-arc = PLINE Задайте втора точка от дъгата: + .halfwidth = Полуширина + .center-keyword = Център + .close-keyword = Затвори + .second-pt = Втора точка + .arc-specify-start-point-of-arc = ARC Задайте начална точка на дъгата: + .arc-specify-second-point-of-arc = ARC Задайте втора точка на дъгата: + .arc-specify-end-point-of-arc = ARC Задайте крайна точка на дъгата: + .arc-specify-center-point-of-arc = ARC Задайте център на дъгата: + .arc-specify-included-angle = ARC Задайте вписан ъгъл: + .arc-specify-length-of-chord = ARC Задайте дължина на хордата: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Задайте посока на допирателната в началото на дъгата: + .arc-specify-radius-of-arc = ARC Задайте радиус на дъгата: + .chord-length = Дължина на хордата modify = .add-vertex = Добавяне на връх @@ -2975,6 +3013,13 @@ modify = .tol-scale-dimtfac = Мащаб на толеранс (DIMTFAC) .trim = Изрежи .exit = иЗход + .area-keyword = Площ + .displacement = Преместване + .mode-keyword = Режим + .array-keyword = Масив + .use-first-point-as-displacement = Първата точка като преместване + .offset-erase-source-object-after-offsetting = OFFSET Изтриване на изходния обект след отместване? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Слой за отместените обекти [Current/Source] : groups = .group-enter-group-name-name = GROUP Въведи име на групата [__ocs_arg_name__]: @@ -5265,6 +5310,19 @@ ui = .what-s-new = Какво е новото .width-value = Ширина: __ocs_arg_value__ .working = Обработка… + .recent-input = Последни въведени + .copy-with-base-point = Копиране с базова точка + .undo-label = Отмени __ocs_arg_label__ + .redo-label = Повтори __ocs_arg_label__ + .copy-selection = Копиране на селекцията + .isolate = Изолиране + .deselect-all = Отмяна на избора + .options-ellipsis = Опции... + .base-point = Базова точка + .snap-overrides = Временни прихващания + .mid-between-2-points = Средата между 2 точки + .osnap-settings = Настройки на прихващане... + .snap-override-none-next-pick-only = Временно прихващане: Няма (само за следващата точка). dimstyle = .keep-text-between-extension-lines = Винаги пази текста между удължителните линии diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 5157d3f79..dcd36aaa6 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Výchozí aplikace .iced-theme = Motiv Iced: .theme = Motiv + .right-click-customization = Přizpůsobení pravého tlačítka + .right-click-in-drawing-area = Pravé tlačítko v kreslicí ploše + .hold-duration = Doba podržení + .right-click-mode-help = Kontextová nabídka: pravé tlačítko vždy otevře nabídku. Časově citlivé: krátké kliknutí je Enter, delší podržení otevře nabídku (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Kontextová nabídka + .right-click-mode-time-sensitive = Časově citlivé (krátké kliknutí = Enter) + .right-click-mode-enter-first = Nejprve Enter, druhé kliknutí = nabídka command-line = .ready = Open CAD Studio je připraveno. @@ -244,6 +251,7 @@ command-move = *[other] { $count } objektů }]: .target = MOVE Zadejte cíl [základ { $x },{ $y }]: + .displacement = MOVE Zadejte posunutí: command-copy = .array-count = COPY Enter počet položek do pole: @@ -258,6 +266,8 @@ command-copy = [one] zatím { $count } kopie *[other] zatím { $count } kopií } | Pole | Enter=hotovo | základna { $x },{ $y }]: + .displacement = COPY Zadejte posunutí: + .mode = COPY Zadejte režim kopírování [Single/Multiple] : common = .jogged-radius = Zalomený poloměr @@ -2117,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2823,6 +2833,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Odebrat + .pline-specify-starting-width = PLINE Zadejte počáteční šířku <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Zadejte koncovou šířku <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Zadejte počáteční poloviční šířku <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Zadejte koncovou poloviční šířku <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Úsečka) Zadejte další bod: + .pline-arc-specify-endpoint-of-arc = PLINE (Oblouk) Zadejte koncový bod oblouku: + .pline-specify-length-of-line = PLINE Zadejte délku úsečky: + .pline-specify-included-angle = PLINE Zadejte středový úhel: + .pline-specify-endpoint-of-arc = PLINE Zadejte koncový bod oblouku: + .pline-specify-center-point-of-arc = PLINE Zadejte střed oblouku: + .pline-specify-radius-of-arc = PLINE Zadejte poloměr oblouku: + .pline-specify-direction-of-chord-for-arc = PLINE Zadejte směr tětivy oblouku: + .pline-specify-length-of-chord = PLINE Zadejte délku tětivy: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Zadejte směr tečny v počátečním bodě oblouku: + .pline-specify-second-point-on-arc = PLINE Zadejte druhý bod na oblouku: + .halfwidth = Poloviční šířka + .center-keyword = Střed + .close-keyword = Uzavřít + .second-pt = Druhý bod + .arc-specify-start-point-of-arc = ARC Zadejte počáteční bod oblouku: + .arc-specify-second-point-of-arc = ARC Zadejte druhý bod oblouku: + .arc-specify-end-point-of-arc = ARC Zadejte koncový bod oblouku: + .arc-specify-center-point-of-arc = ARC Zadejte střed oblouku: + .arc-specify-included-angle = ARC Zadejte středový úhel: + .arc-specify-length-of-chord = ARC Zadejte délku tětivy: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Zadejte směr tečny v počátečním bodě oblouku: + .arc-specify-radius-of-arc = ARC Zadejte poloměr oblouku: + .chord-length = Délka tětivy modify = .add-vertex = Přidat vrchol @@ -2975,6 +3013,13 @@ modify = .tol-scale-dimtfac = Tol. měřítko (DIMTFAC) .trim = Oříznout .exit = výstup + .area-keyword = Plocha + .displacement = Posunutí + .mode-keyword = Režim + .array-keyword = Pole + .use-first-point-as-displacement = První bod jako posunutí + .offset-erase-source-object-after-offsetting = OFFSET Vymazat zdrojový objekt po odsazení? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Hladina odsazených objektů [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter název skupiny [__ocs_arg_name__]: @@ -5262,6 +5307,19 @@ ui = .what-s-new = Co je nového .width-value = Šířka: __ocs_arg_value__ .working = Práce… + .recent-input = Poslední zadání + .copy-with-base-point = Kopírovat s referenčním bodem + .undo-label = Zpět __ocs_arg_label__ + .redo-label = Znovu __ocs_arg_label__ + .copy-selection = Kopírovat výběr + .isolate = Izolovat + .deselect-all = Zrušit výběr + .options-ellipsis = Možnosti... + .base-point = Referenční bod + .snap-overrides = Dočasné uchopení + .mid-between-2-points = Střed mezi 2 body + .osnap-settings = Nastavení uchopení... + .snap-override-none-next-pick-only = Dočasné uchopení: Žádné (jen pro další bod). dimstyle = .keep-text-between-extension-lines = Vždy ponechte text mezi vynášecími řádky diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 9c4911cd8..e4264e697 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Standardanwendung .iced-theme = Iced-Motiv: .theme = Motiv + .right-click-customization = Rechtsklick-Anpassung + .right-click-in-drawing-area = Rechtsklick im Zeichenbereich + .hold-duration = Haltedauer + .right-click-mode-help = Kontextmenü: Rechtsklick öffnet immer das Menü. Zeitabhängig: ein kurzer Klick ist Enter, längeres Halten öffnet das Menü (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Kontextmenü + .right-click-mode-time-sensitive = Zeitabhängig (kurzer Klick = Enter) + .right-click-mode-enter-first = Erst Enter, zweiter Klick = Menü command-line = .ready = Open CAD Studio ist bereit. @@ -245,6 +252,7 @@ command-move = *[other] { $count } Objekte }]: .target = MOVE Zielpunkt angeben [Basis { $x },{ $y }]: + .displacement = MOVE Verschiebung angeben: command-copy = .array-count = COPY Anzahl der Elemente im Array eingeben: @@ -259,6 +267,8 @@ command-copy = [one] bisher { $count } Kopie *[other] bisher { $count } Kopien } | Array | Eingabe=fertig | Basis { $x },{ $y }]: + .displacement = COPY Verschiebung angeben: + .mode = COPY Kopiermodus eingeben [Single/Multiple] : common = .jogged-radius = Geknickter Radius @@ -2118,7 +2128,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2824,6 +2834,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Entfernen + .pline-specify-starting-width = PLINE Anfangsbreite angeben <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Endbreite angeben <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Anfangs-Halbbreite angeben <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE End-Halbbreite angeben <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Linie) Nächsten Punkt angeben: + .pline-arc-specify-endpoint-of-arc = PLINE (Bogen) Endpunkt des Bogens angeben: + .pline-specify-length-of-line = PLINE Länge der Linie angeben: + .pline-specify-included-angle = PLINE Eingeschlossenen Winkel angeben: + .pline-specify-endpoint-of-arc = PLINE Endpunkt des Bogens angeben: + .pline-specify-center-point-of-arc = PLINE Mittelpunkt des Bogens angeben: + .pline-specify-radius-of-arc = PLINE Radius des Bogens angeben: + .pline-specify-direction-of-chord-for-arc = PLINE Richtung der Bogensehne angeben: + .pline-specify-length-of-chord = PLINE Sehnenlänge angeben: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Tangentenrichtung am Bogenanfang angeben: + .pline-specify-second-point-on-arc = PLINE Zweiten Punkt auf dem Bogen angeben: + .halfwidth = Halbbreite + .center-keyword = Zentrum + .close-keyword = Schließen + .second-pt = Zweiter Punkt + .arc-specify-start-point-of-arc = ARC Startpunkt des Bogens angeben: + .arc-specify-second-point-of-arc = ARC Zweiten Punkt des Bogens angeben: + .arc-specify-end-point-of-arc = ARC Endpunkt des Bogens angeben: + .arc-specify-center-point-of-arc = ARC Mittelpunkt des Bogens angeben: + .arc-specify-included-angle = ARC Eingeschlossenen Winkel angeben: + .arc-specify-length-of-chord = ARC Sehnenlänge angeben: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Tangentenrichtung am Bogenanfang angeben: + .arc-specify-radius-of-arc = ARC Radius des Bogens angeben: + .chord-length = Sehnenlänge modify = .add-vertex = Scheitelpunkt hinzufügen @@ -2976,6 +3014,13 @@ modify = .tol-scale-dimtfac = Tol.-Skala (DIMTFAC) .trim = Trimmen .exit = Beenden + .area-keyword = Fläche + .displacement = Verschiebung + .mode-keyword = Modus + .array-keyword = Reihe + .use-first-point-as-displacement = Ersten Punkt als Verschiebung + .offset-erase-source-object-after-offsetting = OFFSET Quellobjekt nach dem Versetzen löschen? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Layer für versetzte Objekte [Current/Source] : groups = .group-enter-group-name-name = GROUP Gruppenname eingeben [__ocs_arg_name__]: @@ -5263,6 +5308,19 @@ ui = .what-s-new = Was ist neu .width-value = Breite: __ocs_arg_value__ .working = Arbeitend... + .recent-input = Letzte Eingaben + .copy-with-base-point = Mit Basispunkt kopieren + .undo-label = Rückgängig: __ocs_arg_label__ + .redo-label = Wiederherstellen: __ocs_arg_label__ + .copy-selection = Auswahl kopieren + .isolate = Isolieren + .deselect-all = Auswahl aufheben + .options-ellipsis = Optionen... + .base-point = Basispunkt + .snap-overrides = Fang-Überschreibungen + .mid-between-2-points = Mitte zwischen 2 Punkten + .osnap-settings = Objektfang-Einstellungen... + .snap-override-none-next-pick-only = Fang-Überschreibung: Keine (nur nächster Punkt). dimstyle = .keep-text-between-extension-lines = Text immer zwischen Hilfslinien halten diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index edb6d9e6e..1dfe08503 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Προεπιλεγμένη εφαρμογή .iced-theme = Θέμα Iced: .theme = Θέμα + .right-click-customization = Προσαρμογή δεξιού κλικ + .right-click-in-drawing-area = Δεξί κλικ στην περιοχή σχεδίασης + .hold-duration = Διάρκεια παρατεταμένου πατήματος + .right-click-mode-help = Μενού συντόμευσης: το δεξί κλικ ανοίγει πάντα το μενού. Χρονοευαίσθητο: γρήγορο κλικ = Enter, παρατεταμένο πάτημα ανοίγει το μενού (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Μενού συντόμευσης + .right-click-mode-time-sensitive = Χρονοευαίσθητο (γρήγορο κλικ = Enter) + .right-click-mode-enter-first = Πρώτα Enter, δεύτερο κλικ = μενού command-line = .ready = Το Open CAD Studio είναι έτοιμο. @@ -244,6 +251,7 @@ command-move = *[other] { $count } αντικείμενα }]: .target = MOVE Ορίστε προορισμό [βάση { $x },{ $y }]: + .displacement = MOVE Ορίστε μετατόπιση: command-copy = .array-count = COPY Δώστε πλήθος στοιχείων διάταξης: @@ -258,6 +266,8 @@ command-copy = [one] { $count } αντίγραφο μέχρι τώρα *[other] { $count } αντίγραφα μέχρι τώρα } | Διάταξη | Enter=τέλος | βάση { $x },{ $y }]: + .displacement = COPY Ορίστε μετατόπιση: + .mode = COPY Επιλέξτε λειτουργία αντιγραφής [Single/Multiple] : common = .revolve-created-objects-failed-sources = REVOLVE: δημιουργήθηκαν __ocs_arg_created__ αντικείμενα· __ocs_arg_failed__ αρχικά αντικείμενα δεν μπόρεσαν να περιστραφούν. @@ -2117,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2824,6 +2834,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Αφαίρεση + .pline-specify-starting-width = PLINE Ορίστε αρχικό πλάτος <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Ορίστε τελικό πλάτος <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Ορίστε αρχικό ημιπλάτος <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Ορίστε τελικό ημιπλάτος <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Γραμμή) Ορίστε επόμενο σημείο: + .pline-arc-specify-endpoint-of-arc = PLINE (Τόξο) Ορίστε τελικό σημείο τόξου: + .pline-specify-length-of-line = PLINE Ορίστε μήκος γραμμής: + .pline-specify-included-angle = PLINE Ορίστε περιεχόμενη γωνία: + .pline-specify-endpoint-of-arc = PLINE Ορίστε τελικό σημείο τόξου: + .pline-specify-center-point-of-arc = PLINE Ορίστε κέντρο τόξου: + .pline-specify-radius-of-arc = PLINE Ορίστε ακτίνα τόξου: + .pline-specify-direction-of-chord-for-arc = PLINE Ορίστε διεύθυνση χορδής τόξου: + .pline-specify-length-of-chord = PLINE Ορίστε μήκος χορδής: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Ορίστε διεύθυνση εφαπτομένης στην αρχή του τόξου: + .pline-specify-second-point-on-arc = PLINE Ορίστε δεύτερο σημείο του τόξου: + .halfwidth = Ημιπλάτος + .center-keyword = Κέντρο + .close-keyword = Κλείσιμο + .second-pt = Δεύτερο σημείο + .arc-specify-start-point-of-arc = ARC Ορίστε αρχικό σημείο τόξου: + .arc-specify-second-point-of-arc = ARC Ορίστε δεύτερο σημείο τόξου: + .arc-specify-end-point-of-arc = ARC Ορίστε τελικό σημείο τόξου: + .arc-specify-center-point-of-arc = ARC Ορίστε κέντρο τόξου: + .arc-specify-included-angle = ARC Ορίστε περιεχόμενη γωνία: + .arc-specify-length-of-chord = ARC Ορίστε μήκος χορδής: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Ορίστε διεύθυνση εφαπτομένης στην αρχή του τόξου: + .arc-specify-radius-of-arc = ARC Ορίστε ακτίνα τόξου: + .chord-length = Μήκος χορδής modify = .count-objects = (__ocs_arg_count__ αντικείμενα) @@ -2976,6 +3014,13 @@ modify = .tol-scale-dimtfac = Κλίμακα ανοχής (DIMTFAC) .trim = Αποκοπή .exit = Έξοδος + .area-keyword = Εμβαδόν + .displacement = Μετατόπιση + .mode-keyword = Λειτουργία + .array-keyword = Διάταξη + .use-first-point-as-displacement = Πρώτο σημείο ως μετατόπιση + .offset-erase-source-object-after-offsetting = OFFSET Διαγραφή αρχικού αντικειμένου μετά την παράλληλη μετατόπιση; [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Επιλογή στρώσης για τα νέα αντικείμενα [Current/Source] : groups = .group-enter-group-name-name = GROUP Δώστε όνομα ομάδας [__ocs_arg_name__]: @@ -5265,6 +5310,19 @@ ui = .what-s-new = Τι νέο υπάρχει .width-value = Πλάτος: __ocs_arg_value__ .working = Εκτέλεση… + .recent-input = Πρόσφατες εισαγωγές + .copy-with-base-point = Αντιγραφή με σημείο βάσης + .undo-label = Αναίρεση __ocs_arg_label__ + .redo-label = Επανάληψη __ocs_arg_label__ + .copy-selection = Αντιγραφή επιλογής + .isolate = Απομόνωση + .deselect-all = Αποεπιλογή όλων + .options-ellipsis = Επιλογές... + .base-point = Σημείο βάσης + .snap-overrides = Προσωρινές έλξεις + .mid-between-2-points = Μέσο μεταξύ 2 σημείων + .osnap-settings = Ρυθμίσεις έλξης... + .snap-override-none-next-pick-only = Προσωρινή έλξη: Καμία (μόνο για το επόμενο σημείο). dimstyle = .keep-text-between-extension-lines = Το κείμενο να παραμένει πάντα μεταξύ των γραμμών προέκτασης diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 559a55db3..c959f870b 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Default Application .iced-theme = Iced theme: .theme = Theme + .right-click-customization = Right-click Customization + .right-click-in-drawing-area = Right-click in drawing area + .hold-duration = Hold duration + .right-click-mode-help = Shortcut menu: right-click always opens the menu. Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Shortcut menu + .right-click-mode-time-sensitive = Time-sensitive (quick click = Enter) + .right-click-mode-enter-first = Enter first, second click = menu command-line = .ready = Open CAD Studio ready. @@ -244,6 +251,7 @@ command-move = *[other] { $count } objects }]: .target = MOVE Specify destination [base { $x },{ $y }]: + .displacement = MOVE Specify displacement: command-copy = .array-count = COPY Enter number of items to array: @@ -258,6 +266,8 @@ command-copy = [one] { $count } copy so far *[other] { $count } copies so far } | Array | Enter=done | base { $x },{ $y }]: + .displacement = COPY Specify displacement: + .mode = COPY Enter a copy mode option [Single/Multiple] : common = .none-use-shortcuts-set-key-command = (none — use: SHORTCUTS SET ) @@ -2118,7 +2128,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2825,6 +2835,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Remove + .pline-specify-starting-width = PLINE Specify starting width <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Specify ending width <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Specify starting half-width <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Specify ending half-width <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Line) Specify next point: + .pline-arc-specify-endpoint-of-arc = PLINE (Arc) Specify endpoint of arc: + .pline-specify-length-of-line = PLINE Specify length of line: + .pline-specify-included-angle = PLINE Specify included angle: + .pline-specify-endpoint-of-arc = PLINE Specify endpoint of arc: + .pline-specify-center-point-of-arc = PLINE Specify center point of arc: + .pline-specify-radius-of-arc = PLINE Specify radius of arc: + .pline-specify-direction-of-chord-for-arc = PLINE Specify direction of chord for arc: + .pline-specify-length-of-chord = PLINE Specify length of chord: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Specify tangent direction for the start point of arc: + .pline-specify-second-point-on-arc = PLINE Specify second point on arc: + .halfwidth = Halfwidth + .center-keyword = CEnter + .close-keyword = CLose + .second-pt = Second pt + .arc-specify-start-point-of-arc = ARC Specify start point of arc: + .arc-specify-second-point-of-arc = ARC Specify second point of arc: + .arc-specify-end-point-of-arc = ARC Specify end point of arc: + .arc-specify-center-point-of-arc = ARC Specify center point of arc: + .arc-specify-included-angle = ARC Specify included angle: + .arc-specify-length-of-chord = ARC Specify length of chord: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Specify tangent direction for the start point of arc: + .arc-specify-radius-of-arc = ARC Specify radius of arc: + .chord-length = chord Length modify = .count-objects = (__ocs_arg_count__ objects) @@ -2977,6 +3015,13 @@ modify = .tol-scale-dimtfac = Tol. scale (DIMTFAC) .trim = Trim .exit = eXit + .area-keyword = ARea + .displacement = Displacement + .mode-keyword = mOde + .array-keyword = Array + .use-first-point-as-displacement = Use first point as displacement + .offset-erase-source-object-after-offsetting = OFFSET Erase source object after offsetting? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Enter layer option for offset objects [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter group name [__ocs_arg_name__]: @@ -5266,6 +5311,19 @@ ui = .what-s-new = What's new .width-value = Width: __ocs_arg_value__ .working = Working… + .recent-input = Recent Input + .copy-with-base-point = Copy with Base Point + .undo-label = Undo __ocs_arg_label__ + .redo-label = Redo __ocs_arg_label__ + .copy-selection = Copy Selection + .isolate = Isolate + .deselect-all = Deselect All + .options-ellipsis = Options... + .base-point = Base Point + .snap-overrides = Snap Overrides + .mid-between-2-points = Mid Between 2 Points + .osnap-settings = Osnap Settings... + .snap-override-none-next-pick-only = Snap override: None (next pick only). dimstyle = .keep-text-between-extension-lines = Always keep text between extension lines diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 597e224bd..e9f255d51 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Aplicación predeterminada .iced-theme = Tema Iced: .theme = Tema + .right-click-customization = Personalización del botón derecho + .right-click-in-drawing-area = Botón derecho en el área de dibujo + .hold-duration = Duración de pulsación + .right-click-mode-help = Menú contextual: el botón derecho siempre abre el menú. Sensible al tiempo: un clic rápido es Intro, mantener pulsado abre el menú (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Menú contextual + .right-click-mode-time-sensitive = Sensible al tiempo (clic rápido = Intro) + .right-click-mode-enter-first = Primero Intro, segundo clic = menú command-line = .ready = Open CAD Studio está listo. @@ -245,6 +252,7 @@ command-move = *[other] { $count } objetos }]: .target = MOVE Especifique el destino [base { $x },{ $y }]: + .displacement = MOVE Precise desplazamiento: command-copy = .array-count = COPY Introduzca el número de elementos de la matriz: @@ -259,6 +267,8 @@ command-copy = [one] { $count } copia hasta ahora *[other] { $count } copias hasta ahora } | Matriz | Enter=terminar | base { $x },{ $y }]: + .displacement = COPY Precise desplazamiento: + .mode = COPY Indique modo de copia [Single/Multiple] : common = .jogged-radius = Radio con recodo @@ -2118,7 +2128,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2824,6 +2834,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Quitar + .pline-specify-starting-width = PLINE Precise grosor inicial <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Precise grosor final <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Precise semigrosor inicial <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Precise semigrosor final <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Línea) Precise punto siguiente: + .pline-arc-specify-endpoint-of-arc = PLINE (Arco) Precise punto final del arco: + .pline-specify-length-of-line = PLINE Precise longitud de la línea: + .pline-specify-included-angle = PLINE Precise ángulo incluido: + .pline-specify-endpoint-of-arc = PLINE Precise punto final del arco: + .pline-specify-center-point-of-arc = PLINE Precise centro del arco: + .pline-specify-radius-of-arc = PLINE Precise radio del arco: + .pline-specify-direction-of-chord-for-arc = PLINE Precise dirección de la cuerda del arco: + .pline-specify-length-of-chord = PLINE Precise longitud de la cuerda: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Precise dirección tangente en el punto inicial del arco: + .pline-specify-second-point-on-arc = PLINE Precise segundo punto del arco: + .halfwidth = Semigrosor + .center-keyword = Centro + .close-keyword = Cerrar + .second-pt = Segundo punto + .arc-specify-start-point-of-arc = ARC Precise punto inicial del arco: + .arc-specify-second-point-of-arc = ARC Precise segundo punto del arco: + .arc-specify-end-point-of-arc = ARC Precise punto final del arco: + .arc-specify-center-point-of-arc = ARC Precise centro del arco: + .arc-specify-included-angle = ARC Precise ángulo incluido: + .arc-specify-length-of-chord = ARC Precise longitud de la cuerda: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Precise dirección tangente en el punto inicial del arco: + .arc-specify-radius-of-arc = ARC Precise radio del arco: + .chord-length = Longitud de cuerda modify = .add-vertex = Añadir vértice @@ -2976,6 +3014,13 @@ modify = .tol-scale-dimtfac = Escala de tolerancia (DIMTFAC) .trim = Recortar .exit = Salir + .area-keyword = Área + .displacement = Desplazamiento + .mode-keyword = Modo + .array-keyword = Matriz + .use-first-point-as-displacement = Usar primer punto como desplazamiento + .offset-erase-source-object-after-offsetting = OFFSET ¿Borrar objeto de origen tras desfasar? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Indique capa para los objetos desfasados [Current/Source] : groups = .group-enter-group-name-name = GROUP Introduzca el nombre del grupo [__ocs_arg_name__]: @@ -5265,6 +5310,19 @@ ui = .what-s-new = Novedades .width-value = Anchura: __ocs_arg_value__ .working = Procesando… + .recent-input = Entradas recientes + .copy-with-base-point = Copiar con punto base + .undo-label = Deshacer __ocs_arg_label__ + .redo-label = Rehacer __ocs_arg_label__ + .copy-selection = Copiar selección + .isolate = Aislar + .deselect-all = Anular selección + .options-ellipsis = Opciones... + .base-point = Punto base + .snap-overrides = Referencias temporales + .mid-between-2-points = Punto medio entre 2 puntos + .osnap-settings = Parámetros de referencia... + .snap-override-none-next-pick-only = Referencia temporal: Ninguna (solo el siguiente punto). dimstyle = .keep-text-between-extension-lines = Mantener siempre el texto entre las líneas de referencia diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 8c306f69b..b0bc8c856 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Oletussovellus .iced-theme = Iced-teema: .theme = Teema + .right-click-customization = Hiiren oikean painikkeen mukautus + .right-click-in-drawing-area = Oikea painike piirtoalueella + .hold-duration = Painalluksen kesto + .right-click-mode-help = Pikavalikko: oikea painike avaa aina valikon. Aikaherkkä: nopea napsautus on Enter, pidempi painallus avaa valikon (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Pikavalikko + .right-click-mode-time-sensitive = Aikaherkkä (nopea napsautus = Enter) + .right-click-mode-enter-first = Ensin Enter, toinen napsautus = valikko command-line = .ready = Open CAD Studio on valmis. @@ -244,6 +251,7 @@ command-move = *[other] { $count } objektit }]: .target = MOVE Määritä kohde [tukikohta { $x },{ $y }]: + .displacement = MOVE Määritä siirtymä: command-copy = .array-count = COPY Enter taulukon kohteiden määrä: @@ -258,6 +266,8 @@ command-copy = [one] { $count } kopio tähän mennessä *[other] { $count } kopiot tähän mennessä } | taulukko | Enter=valmis | pohja { $x },{ $y }]: + .displacement = COPY Määritä siirtymä: + .mode = COPY Valitse kopiointitila [Single/Multiple] : common = .jogged-radius = Katkaistu säde @@ -2117,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2823,6 +2833,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Poista + .pline-specify-starting-width = PLINE Määritä alkuleveys <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Määritä loppuleveys <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Määritä alun puolileveys <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Määritä lopun puolileveys <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Viiva) Määritä seuraava piste: + .pline-arc-specify-endpoint-of-arc = PLINE (Kaari) Määritä kaaren päätepiste: + .pline-specify-length-of-line = PLINE Määritä viivan pituus: + .pline-specify-included-angle = PLINE Määritä keskuskulma: + .pline-specify-endpoint-of-arc = PLINE Määritä kaaren päätepiste: + .pline-specify-center-point-of-arc = PLINE Määritä kaaren keskipiste: + .pline-specify-radius-of-arc = PLINE Määritä kaaren säde: + .pline-specify-direction-of-chord-for-arc = PLINE Määritä kaaren jänteen suunta: + .pline-specify-length-of-chord = PLINE Määritä jänteen pituus: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Määritä tangentin suunta kaaren alkupisteessä: + .pline-specify-second-point-on-arc = PLINE Määritä kaaren toinen piste: + .halfwidth = Puolileveys + .center-keyword = Keskipiste + .close-keyword = Sulje + .second-pt = Toinen piste + .arc-specify-start-point-of-arc = ARC Määritä kaaren alkupiste: + .arc-specify-second-point-of-arc = ARC Määritä kaaren toinen piste: + .arc-specify-end-point-of-arc = ARC Määritä kaaren päätepiste: + .arc-specify-center-point-of-arc = ARC Määritä kaaren keskipiste: + .arc-specify-included-angle = ARC Määritä keskuskulma: + .arc-specify-length-of-chord = ARC Määritä jänteen pituus: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Määritä tangentin suunta kaaren alkupisteessä: + .arc-specify-radius-of-arc = ARC Määritä kaaren säde: + .chord-length = Jänteen pituus modify = .add-vertex = Lisää kärkipiste @@ -2975,6 +3013,13 @@ modify = .tol-scale-dimtfac = Tol. asteikko (DIMTFAC) .trim = Leikkaa .exit = poistu + .area-keyword = Pinta-ala + .displacement = Siirtymä + .mode-keyword = Tila + .array-keyword = Sarja + .use-first-point-as-displacement = Ensimmäinen piste siirtymänä + .offset-erase-source-object-after-offsetting = OFFSET Poistetaanko lähdeobjekti siirron jälkeen? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Siirrettyjen objektien taso [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter ryhmän nimi [__ocs_arg_name__]: @@ -5262,6 +5307,19 @@ ui = .what-s-new = Mitä uutta? .width-value = Leveys: __ocs_arg_value__ .working = Työskentely… + .recent-input = Viimeisimmät syötteet + .copy-with-base-point = Kopioi peruspisteellä + .undo-label = Kumoa __ocs_arg_label__ + .redo-label = Tee uudelleen __ocs_arg_label__ + .copy-selection = Kopioi valinta + .isolate = Eristä + .deselect-all = Poista valinta + .options-ellipsis = Asetukset... + .base-point = Peruspiste + .snap-overrides = Tilapäiset tartunnat + .mid-between-2-points = 2 pisteen keskikohta + .osnap-settings = Tartunta-asetukset... + .snap-override-none-next-pick-only = Tilapäinen tartunta: Ei mitään (vain seuraava piste). dimstyle = .keep-text-between-extension-lines = Pidä teksti aina jatkorivien välissä diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index b9f0044c8..5094cc756 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Application par défaut .iced-theme = Thème iced : .theme = Thème + .right-click-customization = Personnalisation du clic droit + .right-click-in-drawing-area = Clic droit dans la zone de dessin + .hold-duration = Durée de maintien + .right-click-mode-help = Menu contextuel : le clic droit ouvre toujours le menu. Sensible au temps : un clic bref vaut Entrée, un appui prolongé ouvre le menu (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Menu contextuel + .right-click-mode-time-sensitive = Sensible au temps (clic bref = Entrée) + .right-click-mode-enter-first = Entrée d'abord, second clic = menu command-line = .ready = Open CAD Studio est prêt. @@ -245,6 +252,7 @@ command-move = *[other] { $count } objets }] : .target = MOVE Spécifiez la destination [base { $x },{ $y }] : + .displacement = MOVE Spécifiez le déplacement : command-copy = .array-count = COPY Saisissez le nombre d'éléments du réseau : @@ -259,6 +267,8 @@ command-copy = [one] { $count } copie jusqu'ici *[other] { $count } copies jusqu'ici } | Réseau | Entrée=terminer | base { $x },{ $y }] : + .displacement = COPY Spécifiez le déplacement : + .mode = COPY Entrez le mode de copie [Single/Multiple] : common = .jogged-radius = Rayon raccourci @@ -2118,7 +2128,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2824,6 +2834,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Retirer + .pline-specify-starting-width = PLINE Spécifiez la largeur de départ <__ocs_arg_w__> : + .pline-specify-ending-width = PLINE Spécifiez la largeur de fin <__ocs_arg_w__> : + .pline-specify-starting-half-width = PLINE Spécifiez la demi-largeur de départ <__ocs_arg_w__> : + .pline-specify-ending-half-width = PLINE Spécifiez la demi-largeur de fin <__ocs_arg_w__> : + .pline-line-specify-next-point = PLINE (Ligne) Spécifiez le point suivant : + .pline-arc-specify-endpoint-of-arc = PLINE (Arc) Spécifiez l'extrémité de l'arc : + .pline-specify-length-of-line = PLINE Spécifiez la longueur de la ligne : + .pline-specify-included-angle = PLINE Spécifiez l'angle inscrit : + .pline-specify-endpoint-of-arc = PLINE Spécifiez l'extrémité de l'arc : + .pline-specify-center-point-of-arc = PLINE Spécifiez le centre de l'arc : + .pline-specify-radius-of-arc = PLINE Spécifiez le rayon de l'arc : + .pline-specify-direction-of-chord-for-arc = PLINE Spécifiez la direction de la corde de l'arc : + .pline-specify-length-of-chord = PLINE Spécifiez la longueur de la corde : + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Spécifiez la direction tangente au point de départ de l'arc : + .pline-specify-second-point-on-arc = PLINE Spécifiez le second point de l'arc : + .halfwidth = Demi-largeur + .center-keyword = Centre + .close-keyword = Clore + .second-pt = Second point + .arc-specify-start-point-of-arc = ARC Spécifiez le point de départ de l'arc : + .arc-specify-second-point-of-arc = ARC Spécifiez le second point de l'arc : + .arc-specify-end-point-of-arc = ARC Spécifiez l'extrémité de l'arc : + .arc-specify-center-point-of-arc = ARC Spécifiez le centre de l'arc : + .arc-specify-included-angle = ARC Spécifiez l'angle inscrit : + .arc-specify-length-of-chord = ARC Spécifiez la longueur de la corde : + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Spécifiez la direction tangente au point de départ de l'arc : + .arc-specify-radius-of-arc = ARC Spécifiez le rayon de l'arc : + .chord-length = Longueur de corde modify = .add-vertex = Ajouter un sommet @@ -2976,6 +3014,13 @@ modify = .tol-scale-dimtfac = Échelle de référence (DIMTFAC) .trim = Ajuster .exit = Quitter + .area-keyword = Aire + .displacement = Déplacement + .mode-keyword = Mode + .array-keyword = Réseau + .use-first-point-as-displacement = Premier point comme déplacement + .offset-erase-source-object-after-offsetting = OFFSET Effacer l'objet source après le décalage ? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Calque des objets décalés [Current/Source] : groups = .group-enter-group-name-name = GROUP Entrez le nom du groupe [__ocs_arg_name__]: @@ -5264,6 +5309,19 @@ ui = .what-s-new = Quoi de neuf ? .width-value = Largeur: __ocs_arg_value__ .working = Travailler... + .recent-input = Saisies récentes + .copy-with-base-point = Copier avec point de base + .undo-label = Annuler __ocs_arg_label__ + .redo-label = Rétablir __ocs_arg_label__ + .copy-selection = Copier la sélection + .isolate = Isoler + .deselect-all = Tout désélectionner + .options-ellipsis = Options... + .base-point = Point de base + .snap-overrides = Remplacements d'accrochage + .mid-between-2-points = Milieu entre 2 points + .osnap-settings = Paramètres d'accrochage... + .snap-override-none-next-pick-only = Remplacement d'accrochage : Aucun (prochain point seulement). dimstyle = .keep-text-between-extension-lines = Toujours conserver le texte entre les lignes d’attache diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index d799d20d0..242447240 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = डिफ़ॉल्ट अनुप्रयोग .iced-theme = आइस्ड थीम: .theme = विषय + .right-click-customization = राइट-क्लिक अनुकूलन + .right-click-in-drawing-area = ड्रॉइंग क्षेत्र में राइट-क्लिक + .hold-duration = दबाए रखने की अवधि + .right-click-mode-help = शॉर्टकट मेनू: राइट-क्लिक हमेशा मेनू खोलता है। समय-संवेदी: त्वरित क्लिक Enter है, देर तक दबाए रखने से मेनू खुलता है (SHORTCUTMENUDURATION)। + .right-click-mode-shortcut-menu = शॉर्टकट मेनू + .right-click-mode-time-sensitive = समय-संवेदी (त्वरित क्लिक = Enter) + .right-click-mode-enter-first = पहले Enter, दूसरा क्लिक = मेनू command-line = .ready = Open CAD Studio तैयार है। @@ -241,12 +248,15 @@ area = command-move = .base = MOVE आधार बिंदु निर्दिष्ट करें [{ $count } ऑब्जेक्ट]: .target = MOVE गंतव्य निर्दिष्ट करें [आधार { $x },{ $y }]: + .displacement = MOVE विस्थापन निर्दिष्ट करें: command-copy = .array-count = COPY ऐरे में आइटमों की संख्या दर्ज करें: .base = COPY आधार बिंदु निर्दिष्ट करें [{ $count } ऑब्जेक्ट]: .array-target = COPY { $count }-आइटम ऐरे के लिए दूसरा बिंदु निर्दिष्ट करें [आधार { $x },{ $y }]: .target = COPY गंतव्य निर्दिष्ट करें [अब तक { $count } प्रतियाँ | ऐरे | Enter=पूर्ण | आधार { $x },{ $y }]: + .displacement = COPY विस्थापन निर्दिष्ट करें: + .mode = COPY कॉपी मोड विकल्प दर्ज करें [Single/Multiple] : common = .jogged-radius = मोड़ वाला त्रिज्या माप @@ -2106,7 +2116,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2812,6 +2822,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = हटाएँ + .pline-specify-starting-width = PLINE प्रारंभिक चौड़ाई निर्दिष्ट करें <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE अंतिम चौड़ाई निर्दिष्ट करें <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE प्रारंभिक अर्ध-चौड़ाई निर्दिष्ट करें <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE अंतिम अर्ध-चौड़ाई निर्दिष्ट करें <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (रेखा) अगला बिंदु निर्दिष्ट करें: + .pline-arc-specify-endpoint-of-arc = PLINE (चाप) चाप का अंतिम बिंदु निर्दिष्ट करें: + .pline-specify-length-of-line = PLINE रेखा की लंबाई निर्दिष्ट करें: + .pline-specify-included-angle = PLINE अंतर्निहित कोण निर्दिष्ट करें: + .pline-specify-endpoint-of-arc = PLINE चाप का अंतिम बिंदु निर्दिष्ट करें: + .pline-specify-center-point-of-arc = PLINE चाप का केंद्र बिंदु निर्दिष्ट करें: + .pline-specify-radius-of-arc = PLINE चाप की त्रिज्या निर्दिष्ट करें: + .pline-specify-direction-of-chord-for-arc = PLINE चाप की जीवा की दिशा निर्दिष्ट करें: + .pline-specify-length-of-chord = PLINE जीवा की लंबाई निर्दिष्ट करें: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE चाप के प्रारंभिक बिंदु पर स्पर्शरेखा की दिशा निर्दिष्ट करें: + .pline-specify-second-point-on-arc = PLINE चाप पर दूसरा बिंदु निर्दिष्ट करें: + .halfwidth = अर्ध-चौड़ाई + .center-keyword = केंद्र + .close-keyword = बंद करें + .second-pt = दूसरा बिंदु + .arc-specify-start-point-of-arc = ARC चाप का प्रारंभिक बिंदु निर्दिष्ट करें: + .arc-specify-second-point-of-arc = ARC चाप का दूसरा बिंदु निर्दिष्ट करें: + .arc-specify-end-point-of-arc = ARC चाप का अंतिम बिंदु निर्दिष्ट करें: + .arc-specify-center-point-of-arc = ARC चाप का केंद्र बिंदु निर्दिष्ट करें: + .arc-specify-included-angle = ARC अंतर्निहित कोण निर्दिष्ट करें: + .arc-specify-length-of-chord = ARC जीवा की लंबाई निर्दिष्ट करें: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC चाप के प्रारंभिक बिंदु पर स्पर्शरेखा की दिशा निर्दिष्ट करें: + .arc-specify-radius-of-arc = ARC चाप की त्रिज्या निर्दिष्ट करें: + .chord-length = जीवा की लंबाई modify = .add-vertex = शीर्ष जोड़ें @@ -2964,6 +3002,13 @@ modify = .tol-scale-dimtfac = Tol. पैमाने (DIMTFAC) .trim = ट्रिम .exit = बाहर निकलें + .area-keyword = क्षेत्रफल + .displacement = विस्थापन + .mode-keyword = मोड + .array-keyword = सारणी + .use-first-point-as-displacement = पहले बिंदु को विस्थापन के रूप में उपयोग करें + .offset-erase-source-object-after-offsetting = OFFSET ऑफ़सेट के बाद स्रोत ऑब्जेक्ट मिटाएँ? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET ऑफ़सेट ऑब्जेक्ट के लिए परत विकल्प दर्ज करें [Current/Source] : groups = .group-enter-group-name-name = GROUP समूह का नाम दर्ज करें [__ocs_arg_name__]: @@ -5253,6 +5298,19 @@ ui = .what-s-new = नया क्या है .width-value = चौड़ाई: __ocs_arg_value__ .working = कार्य... + .recent-input = हाल के इनपुट + .copy-with-base-point = आधार बिंदु के साथ कॉपी करें + .undo-label = पूर्ववत करें __ocs_arg_label__ + .redo-label = फिर से करें __ocs_arg_label__ + .copy-selection = चयन कॉपी करें + .isolate = अलग करें + .deselect-all = सभी का चयन हटाएँ + .options-ellipsis = विकल्प... + .base-point = आधार बिंदु + .snap-overrides = स्नैप ओवरराइड + .mid-between-2-points = 2 बिंदुओं के बीच मध्य + .osnap-settings = ऑब्जेक्ट स्नैप सेटिंग्स... + .snap-override-none-next-pick-only = स्नैप ओवरराइड: कोई नहीं (केवल अगले चयन के लिए)। dimstyle = .keep-text-between-extension-lines = पाठ को हमेशा विस्तार रेखाओं के बीच रखें diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 4e2820102..635e974cd 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Alapértelmezett alkalmazás .iced-theme = Iced-téma: .theme = Téma + .right-click-customization = Jobb kattintás testreszabása + .right-click-in-drawing-area = Jobb kattintás a rajzterületen + .hold-duration = Nyomva tartás ideje + .right-click-mode-help = Helyi menü: a jobb kattintás mindig a menüt nyitja. Időérzékeny: a rövid kattintás Enter, a hosszabb nyomva tartás a menüt nyitja (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Helyi menü + .right-click-mode-time-sensitive = Időérzékeny (rövid kattintás = Enter) + .right-click-mode-enter-first = Előbb Enter, második kattintás = menü command-line = .ready = Az Open CAD Studio használatra kész. @@ -244,6 +251,7 @@ command-move = *[other] { $count } objektumok }]: .target = MOVE Adja meg a rendeltetési helyet [bázis { $x },{ $y }]: + .displacement = MOVE Adja meg az eltolást: command-copy = .array-count = COPY Enter a tömbben lévő elemek száma: @@ -258,6 +266,8 @@ command-copy = [one] { $count } példány eddig *[other] { $count } példányok eddig } | tömb | Enter=kész | alap { $x },{ $y }]: + .displacement = COPY Adja meg az eltolást: + .mode = COPY Adja meg a másolási módot [Single/Multiple] : common = .jogged-radius = Tört sugarú méret @@ -2117,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2823,6 +2833,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Eltávolítás + .pline-specify-starting-width = PLINE Adja meg a kezdő szélességet <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Adja meg a záró szélességet <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Adja meg a kezdő félszélességet <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Adja meg a záró félszélességet <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Vonal) Adja meg a következő pontot: + .pline-arc-specify-endpoint-of-arc = PLINE (Ív) Adja meg az ív végpontját: + .pline-specify-length-of-line = PLINE Adja meg a vonal hosszát: + .pline-specify-included-angle = PLINE Adja meg a középponti szöget: + .pline-specify-endpoint-of-arc = PLINE Adja meg az ív végpontját: + .pline-specify-center-point-of-arc = PLINE Adja meg az ív középpontját: + .pline-specify-radius-of-arc = PLINE Adja meg az ív sugarát: + .pline-specify-direction-of-chord-for-arc = PLINE Adja meg az ív húrjának irányát: + .pline-specify-length-of-chord = PLINE Adja meg a húr hosszát: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Adja meg az érintő irányát az ív kezdőpontjában: + .pline-specify-second-point-on-arc = PLINE Adja meg az ív második pontját: + .halfwidth = Félszélesség + .center-keyword = Középpont + .close-keyword = Zárás + .second-pt = Második pont + .arc-specify-start-point-of-arc = ARC Adja meg az ív kezdőpontját: + .arc-specify-second-point-of-arc = ARC Adja meg az ív második pontját: + .arc-specify-end-point-of-arc = ARC Adja meg az ív végpontját: + .arc-specify-center-point-of-arc = ARC Adja meg az ív középpontját: + .arc-specify-included-angle = ARC Adja meg a középponti szöget: + .arc-specify-length-of-chord = ARC Adja meg a húr hosszát: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Adja meg az érintő irányát az ív kezdőpontjában: + .arc-specify-radius-of-arc = ARC Adja meg az ív sugarát: + .chord-length = Húrhossz modify = .add-vertex = Csúcspont hozzáadása @@ -2975,6 +3013,13 @@ modify = .tol-scale-dimtfac = Tol. mérleg (DIMTFAC) .trim = Vágás .exit = kilépés + .area-keyword = Terület + .displacement = Eltolás + .mode-keyword = Mód + .array-keyword = Kiosztás + .use-first-point-as-displacement = Első pont eltolásként + .offset-erase-source-object-after-offsetting = OFFSET Törli a forrásobjektumot eltolás után? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Az eltolt objektumok fóliája [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter csoport neve [__ocs_arg_name__]: @@ -5264,6 +5309,19 @@ ui = .what-s-new = Újdonságok .width-value = Szélesség: __ocs_arg_value__ .working = Dolgozik… + .recent-input = Legutóbbi bevitel + .copy-with-base-point = Másolás bázisponttal + .undo-label = Visszavonás: __ocs_arg_label__ + .redo-label = Újra: __ocs_arg_label__ + .copy-selection = Kijelölés másolása + .isolate = Elkülönítés + .deselect-all = Kijelölés megszüntetése + .options-ellipsis = Beállítások... + .base-point = Bázispont + .snap-overrides = Ideiglenes tárgyraszter + .mid-between-2-points = 2 pont közötti felezőpont + .osnap-settings = Tárgyraszter beállítások... + .snap-override-none-next-pick-only = Ideiglenes tárgyraszter: Nincs (csak a következő pontra). dimstyle = .keep-text-between-extension-lines = Mindig legyen szöveg a hosszabbító sorok között diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 4cc8190c9..1da38e16a 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Applicazione predefinita .iced-theme = Tema Iced: .theme = Tema + .right-click-customization = Personalizzazione clic destro + .right-click-in-drawing-area = Clic destro nell'area di disegno + .hold-duration = Durata pressione + .right-click-mode-help = Menu di scelta rapida: il clic destro apre sempre il menu. Sensibile al tempo: un clic rapido è Invio, tenere premuto apre il menu (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Menu di scelta rapida + .right-click-mode-time-sensitive = Sensibile al tempo (clic rapido = Invio) + .right-click-mode-enter-first = Prima Invio, secondo clic = menu command-line = .ready = Open CAD Studio è pronto. @@ -244,6 +251,7 @@ command-move = *[other] { $count } oggetti }]: .target = MOVE Specificare la destinazione [base { $x },{ $y }]: + .displacement = MOVE Specificare spostamento: command-copy = .array-count = COPY Enter numero di elementi da mettere in serie: @@ -258,6 +266,8 @@ command-copy = [one] { $count } copia finora *[other] { $count } copie finora } | Serie | Enter=completato | base { $x },{ $y }]: + .displacement = COPY Specificare spostamento: + .mode = COPY Specificare modalità di copia [Single/Multiple] : common = .jogged-radius = Raggio spezzato @@ -2117,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2823,6 +2833,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Rimuovi + .pline-specify-starting-width = PLINE Specificare larghezza iniziale <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Specificare larghezza finale <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Specificare semilarghezza iniziale <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Specificare semilarghezza finale <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Linea) Specificare punto successivo: + .pline-arc-specify-endpoint-of-arc = PLINE (Arco) Specificare punto finale dell'arco: + .pline-specify-length-of-line = PLINE Specificare lunghezza della linea: + .pline-specify-included-angle = PLINE Specificare angolo inscritto: + .pline-specify-endpoint-of-arc = PLINE Specificare punto finale dell'arco: + .pline-specify-center-point-of-arc = PLINE Specificare centro dell'arco: + .pline-specify-radius-of-arc = PLINE Specificare raggio dell'arco: + .pline-specify-direction-of-chord-for-arc = PLINE Specificare direzione della corda dell'arco: + .pline-specify-length-of-chord = PLINE Specificare lunghezza della corda: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Specificare direzione tangente nel punto iniziale dell'arco: + .pline-specify-second-point-on-arc = PLINE Specificare secondo punto dell'arco: + .halfwidth = Semilarghezza + .center-keyword = Centro + .close-keyword = Chiudi + .second-pt = Secondo punto + .arc-specify-start-point-of-arc = ARC Specificare punto iniziale dell'arco: + .arc-specify-second-point-of-arc = ARC Specificare secondo punto dell'arco: + .arc-specify-end-point-of-arc = ARC Specificare punto finale dell'arco: + .arc-specify-center-point-of-arc = ARC Specificare centro dell'arco: + .arc-specify-included-angle = ARC Specificare angolo inscritto: + .arc-specify-length-of-chord = ARC Specificare lunghezza della corda: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Specificare direzione tangente nel punto iniziale dell'arco: + .arc-specify-radius-of-arc = ARC Specificare raggio dell'arco: + .chord-length = Lunghezza corda modify = .add-vertex = Aggiungi vertice @@ -2975,6 +3013,13 @@ modify = .tol-scale-dimtfac = Tol. scala (DIMTFAC) .trim = Tagliare .exit = esci + .area-keyword = Area + .displacement = Spostamento + .mode-keyword = Modalità + .array-keyword = Serie + .use-first-point-as-displacement = Primo punto come spostamento + .offset-erase-source-object-after-offsetting = OFFSET Cancellare l'oggetto di origine dopo l'offset? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Layer degli oggetti di offset [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter nome del gruppo [__ocs_arg_name__]: @@ -5264,6 +5309,19 @@ ui = .what-s-new = Cosa c'è di nuovo .width-value = Larghezza: __ocs_arg_value__ .working = Lavorando… + .recent-input = Input recenti + .copy-with-base-point = Copia con punto base + .undo-label = Annulla __ocs_arg_label__ + .redo-label = Ripeti __ocs_arg_label__ + .copy-selection = Copia selezione + .isolate = Isola + .deselect-all = Deseleziona tutto + .options-ellipsis = Opzioni... + .base-point = Punto base + .snap-overrides = Snap temporanei + .mid-between-2-points = Punto medio tra 2 punti + .osnap-settings = Impostazioni snap... + .snap-override-none-next-pick-only = Snap temporaneo: Nessuno (solo il prossimo punto). dimstyle = .keep-text-between-extension-lines = Mantieni sempre il testo tra le linee di estensione diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index ce01e9dda..22d894f31 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = 既定のアプリケーション .iced-theme = Iced テーマ: .theme = テーマ + .right-click-customization = 右クリックのカスタマイズ + .right-click-in-drawing-area = 作図領域での右クリック + .hold-duration = 長押し時間 + .right-click-mode-help = ショートカットメニュー: 右クリックで常にメニューを開く。時間依存: 短いクリックは Enter、長押しでメニューを開く (SHORTCUTMENUDURATION)。 + .right-click-mode-shortcut-menu = ショートカットメニュー + .right-click-mode-time-sensitive = 時間依存 (短いクリック = Enter) + .right-click-mode-enter-first = 1 回目は Enter、2 回目でメニュー command-line = .ready = Open CAD Studio の準備ができました。 @@ -242,6 +249,7 @@ command-move = .base = MOVE 基点を指定 [{ $count } 個のオブジェクト]: .target = MOVE 移動先を指定 [基点 { $x },{ $y }]: + .displacement = MOVE 変位を指定: command-copy = .array-count = COPY 配列複写する項目数を入力: @@ -249,6 +257,8 @@ command-copy = COPY 基点を指定 [{ $count } 個のオブジェクト]: .array-target = COPY { $count } 項目の配列複写の2点目を指定 [基点 { $x },{ $y }]: .target = COPY 移動先を指定 [これまでに { $count } 個複写 | 配列 | Enter=完了 | 基点 { $x },{ $y }]: + .displacement = COPY 変位を指定: + .mode = COPY コピーモードを入力 [Single/Multiple] : common = .jogged-radius = 折れ曲がり半径 @@ -2108,7 +2118,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2814,6 +2824,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = 除去 + .pline-specify-starting-width = PLINE 始点の幅を指定 <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE 終点の幅を指定 <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE 始点の半幅を指定 <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE 終点の半幅を指定 <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (線分) 次の点を指定: + .pline-arc-specify-endpoint-of-arc = PLINE (円弧) 円弧の終点を指定: + .pline-specify-length-of-line = PLINE 線分の長さを指定: + .pline-specify-included-angle = PLINE 中心角を指定: + .pline-specify-endpoint-of-arc = PLINE 円弧の終点を指定: + .pline-specify-center-point-of-arc = PLINE 円弧の中心を指定: + .pline-specify-radius-of-arc = PLINE 円弧の半径を指定: + .pline-specify-direction-of-chord-for-arc = PLINE 円弧の弦の方向を指定: + .pline-specify-length-of-chord = PLINE 弦の長さを指定: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE 円弧の始点での接線方向を指定: + .pline-specify-second-point-on-arc = PLINE 円弧上の 2 点目を指定: + .halfwidth = 半幅 + .center-keyword = 中心 + .close-keyword = 閉じる + .second-pt = 2 点目 + .arc-specify-start-point-of-arc = ARC 円弧の始点を指定: + .arc-specify-second-point-of-arc = ARC 円弧の 2 点目を指定: + .arc-specify-end-point-of-arc = ARC 円弧の終点を指定: + .arc-specify-center-point-of-arc = ARC 円弧の中心を指定: + .arc-specify-included-angle = ARC 中心角を指定: + .arc-specify-length-of-chord = ARC 弦の長さを指定: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC 円弧の始点での接線方向を指定: + .arc-specify-radius-of-arc = ARC 円弧の半径を指定: + .chord-length = 弦の長さ modify = .add-vertex = 頂点を追加 @@ -2966,6 +3004,13 @@ modify = .tol-scale-dimtfac = 許容差尺度 (DIMTFAC) .trim = トリム .exit = 終了 + .area-keyword = 面積 + .displacement = 変位 + .mode-keyword = モード + .array-keyword = 配列 + .use-first-point-as-displacement = 1 点目を変位として使用 + .offset-erase-source-object-after-offsetting = OFFSET オフセット後に元のオブジェクトを削除しますか? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET オフセットオブジェクトの画層オプションを入力 [Current/Source] : groups = .group-enter-group-name-name = GROUP グループ名を入力 [__ocs_arg_name__]: @@ -5253,6 +5298,19 @@ ui = .what-s-new = 新機能 .width-value = 幅: __ocs_arg_value__ .working = 処理中… + .recent-input = 最近の入力 + .copy-with-base-point = 基点を指定してコピー + .undo-label = 元に戻す __ocs_arg_label__ + .redo-label = やり直し __ocs_arg_label__ + .copy-selection = 選択をコピー + .isolate = 分離 + .deselect-all = 選択解除 + .options-ellipsis = オプション... + .base-point = 基点 + .snap-overrides = 優先オブジェクトスナップ + .mid-between-2-points = 2 点間の中点 + .osnap-settings = オブジェクトスナップ設定... + .snap-override-none-next-pick-only = 優先スナップ: なし (次の点のみ)。 dimstyle = .keep-text-between-extension-lines = 寸法値を常に寸法補助線の間に配置 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index e5a6fb083..2d04ba139 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = 기본 애플리케이션 .iced-theme = Iced 테마: .theme = 테마 + .right-click-customization = 오른쪽 클릭 사용자화 + .right-click-in-drawing-area = 도면 영역에서 오른쪽 클릭 + .hold-duration = 누르는 시간 + .right-click-mode-help = 바로 가기 메뉴: 오른쪽 클릭 시 항상 메뉴를 엽니다. 시간 민감: 짧은 클릭은 Enter, 길게 누르면 메뉴가 열립니다 (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = 바로 가기 메뉴 + .right-click-mode-time-sensitive = 시간 민감 (짧은 클릭 = Enter) + .right-click-mode-enter-first = 먼저 Enter, 두 번째 클릭 = 메뉴 command-line = .ready = Open CAD Studio가 준비되었습니다. @@ -244,6 +251,7 @@ command-move = *[other] { $count } 객체 }]: .target = MOVE 대상 지정 [기본 { $x },{ $y }]: + .displacement = MOVE 변위 지정: command-copy = .array-count = COPY Enter 배열할 항목 수: @@ -258,6 +266,8 @@ command-copy = [one] { $count } 지금까지 복사 *[other] { $count } 지금까지의 사본 } | 배열 | Enter=완료 | 베이스 { $x },{ $y }]: + .displacement = COPY 변위 지정: + .mode = COPY 복사 모드 옵션 입력 [Single/Multiple] : common = .jogged-radius = 꺾인 반지름 @@ -2117,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2823,6 +2833,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = 제거 + .pline-specify-starting-width = PLINE 시작 폭 지정 <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE 끝 폭 지정 <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE 시작 반폭 지정 <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE 끝 반폭 지정 <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (선) 다음 점 지정: + .pline-arc-specify-endpoint-of-arc = PLINE (호) 호의 끝점 지정: + .pline-specify-length-of-line = PLINE 선의 길이 지정: + .pline-specify-included-angle = PLINE 사이각 지정: + .pline-specify-endpoint-of-arc = PLINE 호의 끝점 지정: + .pline-specify-center-point-of-arc = PLINE 호의 중심점 지정: + .pline-specify-radius-of-arc = PLINE 호의 반지름 지정: + .pline-specify-direction-of-chord-for-arc = PLINE 호의 현 방향 지정: + .pline-specify-length-of-chord = PLINE 현의 길이 지정: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE 호 시작점의 접선 방향 지정: + .pline-specify-second-point-on-arc = PLINE 호의 두 번째 점 지정: + .halfwidth = 반폭 + .center-keyword = 중심 + .close-keyword = 닫기 + .second-pt = 두 번째 점 + .arc-specify-start-point-of-arc = ARC 호의 시작점 지정: + .arc-specify-second-point-of-arc = ARC 호의 두 번째 점 지정: + .arc-specify-end-point-of-arc = ARC 호의 끝점 지정: + .arc-specify-center-point-of-arc = ARC 호의 중심점 지정: + .arc-specify-included-angle = ARC 사이각 지정: + .arc-specify-length-of-chord = ARC 현의 길이 지정: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC 호 시작점의 접선 방향 지정: + .arc-specify-radius-of-arc = ARC 호의 반지름 지정: + .chord-length = 현 길이 modify = .add-vertex = 정점 추가 @@ -2975,6 +3013,13 @@ modify = .tol-scale-dimtfac = 톨. 스케일(DIMTFAC) .trim = 트림 .exit = 종료 + .area-keyword = 면적 + .displacement = 변위 + .mode-keyword = 모드 + .array-keyword = 배열 + .use-first-point-as-displacement = 첫 번째 점을 변위로 사용 + .offset-erase-source-object-after-offsetting = OFFSET 간격띄우기 후 원본 객체를 지우시겠습니까? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET 간격띄우기 객체의 도면층 옵션 입력 [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter 그룹 이름 [__ocs_arg_name__]: @@ -5264,6 +5309,19 @@ ui = .what-s-new = 새로운 소식 .width-value = 폭: __ocs_arg_value__ .working = 일하는 중… + .recent-input = 최근 입력 + .copy-with-base-point = 기준점과 함께 복사 + .undo-label = 명령 취소 __ocs_arg_label__ + .redo-label = 명령 복구 __ocs_arg_label__ + .copy-selection = 선택 복사 + .isolate = 분리 + .deselect-all = 선택 해제 + .options-ellipsis = 옵션... + .base-point = 기준점 + .snap-overrides = 스냅 재지정 + .mid-between-2-points = 두 점 사이의 중간 + .osnap-settings = 객체 스냅 설정... + .snap-override-none-next-pick-only = 스냅 재지정: 없음 (다음 선택만). dimstyle = .keep-text-between-extension-lines = 보조선 사이에 항상 텍스트 유지 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index f22b95fe8..555d2247b 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Standaardtoepassing .iced-theme = Iced thema: .theme = Thema + .right-click-customization = Rechtsklik aanpassen + .right-click-in-drawing-area = Rechtsklik in tekengebied + .hold-duration = Vasthoudduur + .right-click-mode-help = Snelmenu: rechtsklik opent altijd het menu. Tijdgevoelig: een korte klik is Enter, langer vasthouden opent het menu (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Snelmenu + .right-click-mode-time-sensitive = Tijdgevoelig (korte klik = Enter) + .right-click-mode-enter-first = Eerst Enter, tweede klik = menu command-line = .ready = Open CAD Studio is gereed. @@ -245,6 +252,7 @@ command-move = *[other] { $count } objecten }]: .target = MOVE Geef de bestemming op [basis { $x },{ $y }]: + .displacement = MOVE Geef verplaatsing op: command-copy = .array-count = COPY Voer het aantal items in de reeks in: @@ -259,6 +267,8 @@ command-copy = [one] { $count } kopie tot nu toe *[other] { $count } kopieën tot nu toe } | Reeks | Enter=gereed | basis { $x },{ $y }]: + .displacement = COPY Geef verplaatsing op: + .mode = COPY Geef kopieermodus op [Single/Multiple] : common = .jogged-radius = Geknikte straal @@ -2118,7 +2128,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2824,6 +2834,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Verwijderen + .pline-specify-starting-width = PLINE Geef beginbreedte op <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Geef eindbreedte op <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Geef halve beginbreedte op <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Geef halve eindbreedte op <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Lijn) Geef volgend punt op: + .pline-arc-specify-endpoint-of-arc = PLINE (Boog) Geef eindpunt van boog op: + .pline-specify-length-of-line = PLINE Geef lengte van lijn op: + .pline-specify-included-angle = PLINE Geef ingesloten hoek op: + .pline-specify-endpoint-of-arc = PLINE Geef eindpunt van boog op: + .pline-specify-center-point-of-arc = PLINE Geef middelpunt van boog op: + .pline-specify-radius-of-arc = PLINE Geef straal van boog op: + .pline-specify-direction-of-chord-for-arc = PLINE Geef richting van koorde van boog op: + .pline-specify-length-of-chord = PLINE Geef lengte van koorde op: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Geef raaklijnrichting in beginpunt van boog op: + .pline-specify-second-point-on-arc = PLINE Geef tweede punt op boog op: + .halfwidth = Halve breedte + .center-keyword = Centrum + .close-keyword = Sluiten + .second-pt = Tweede punt + .arc-specify-start-point-of-arc = ARC Geef beginpunt van boog op: + .arc-specify-second-point-of-arc = ARC Geef tweede punt van boog op: + .arc-specify-end-point-of-arc = ARC Geef eindpunt van boog op: + .arc-specify-center-point-of-arc = ARC Geef middelpunt van boog op: + .arc-specify-included-angle = ARC Geef ingesloten hoek op: + .arc-specify-length-of-chord = ARC Geef lengte van koorde op: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Geef raaklijnrichting in beginpunt van boog op: + .arc-specify-radius-of-arc = ARC Geef straal van boog op: + .chord-length = Koordelengte modify = .add-vertex = Hoekpunt toevoegen @@ -2976,6 +3014,13 @@ modify = .tol-scale-dimtfac = Tol. schaal (DIMTFAC) .trim = Trimmen .exit = Afsluiten + .area-keyword = Oppervlakte + .displacement = Verplaatsing + .mode-keyword = Modus + .array-keyword = Patroon + .use-first-point-as-displacement = Eerste punt als verplaatsing + .offset-erase-source-object-after-offsetting = OFFSET Bronobject wissen na verschuiven? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Laag voor verschoven objecten [Current/Source] : groups = .group-enter-group-name-name = GROUP Groepsnaam invoeren [__ocs_arg_name__]: @@ -5263,6 +5308,19 @@ ui = .what-s-new = Wat is er nieuw? .width-value = Breedte: __ocs_arg_value__ .working = Werken... + .recent-input = Recente invoer + .copy-with-base-point = Kopiëren met basispunt + .undo-label = Ongedaan maken __ocs_arg_label__ + .redo-label = Opnieuw __ocs_arg_label__ + .copy-selection = Selectie kopiëren + .isolate = Isoleren + .deselect-all = Alles deselecteren + .options-ellipsis = Opties... + .base-point = Basispunt + .snap-overrides = Tijdelijke snaps + .mid-between-2-points = Midden tussen 2 punten + .osnap-settings = Objectsnap-instellingen... + .snap-override-none-next-pick-only = Tijdelijke snap: Geen (alleen volgend punt). dimstyle = .keep-text-between-extension-lines = Tekst altijd tussen hulplijnen houden diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index eb1491f71..3f7438b28 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Aplikacja domyślna .iced-theme = Motyw Iced: .theme = Motyw + .right-click-customization = Dostosowanie prawego przycisku + .right-click-in-drawing-area = Prawy przycisk w obszarze rysunku + .hold-duration = Czas przytrzymania + .right-click-mode-help = Menu skrótów: prawy przycisk zawsze otwiera menu. Zależne od czasu: krótkie kliknięcie to Enter, dłuższe przytrzymanie otwiera menu (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Menu skrótów + .right-click-mode-time-sensitive = Zależne od czasu (krótkie kliknięcie = Enter) + .right-click-mode-enter-first = Najpierw Enter, drugie kliknięcie = menu command-line = .ready = Open CAD Studio jest gotowe. @@ -244,6 +251,7 @@ command-move = *[other] { $count } obiekty }]: .target = MOVE Określ miejsce docelowe [podstawa { $x },{ $y }]: + .displacement = MOVE Określ przesunięcie: command-copy = .array-count = COPY Enter liczba elementów do tablicy: @@ -258,6 +266,8 @@ command-copy = [one] { $count } kopia do tej pory *[other] { $count } kopii do tej pory } | Tablica | Enter=gotowe | baza { $x },{ $y }]: + .displacement = COPY Określ przesunięcie: + .mode = COPY Podaj tryb kopiowania [Single/Multiple] : common = .jogged-radius = Promień łamany @@ -2117,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2823,6 +2833,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Usuń + .pline-specify-starting-width = PLINE Określ szerokość początkową <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Określ szerokość końcową <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Określ początkową połowę szerokości <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Określ końcową połowę szerokości <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Linia) Określ następny punkt: + .pline-arc-specify-endpoint-of-arc = PLINE (Łuk) Określ punkt końcowy łuku: + .pline-specify-length-of-line = PLINE Określ długość linii: + .pline-specify-included-angle = PLINE Określ kąt rozwarcia: + .pline-specify-endpoint-of-arc = PLINE Określ punkt końcowy łuku: + .pline-specify-center-point-of-arc = PLINE Określ środek łuku: + .pline-specify-radius-of-arc = PLINE Określ promień łuku: + .pline-specify-direction-of-chord-for-arc = PLINE Określ kierunek cięciwy łuku: + .pline-specify-length-of-chord = PLINE Określ długość cięciwy: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Określ kierunek stycznej w punkcie początkowym łuku: + .pline-specify-second-point-on-arc = PLINE Określ drugi punkt na łuku: + .halfwidth = Połowa szerokości + .center-keyword = Środek + .close-keyword = Zamknij + .second-pt = Drugi punkt + .arc-specify-start-point-of-arc = ARC Określ punkt początkowy łuku: + .arc-specify-second-point-of-arc = ARC Określ drugi punkt łuku: + .arc-specify-end-point-of-arc = ARC Określ punkt końcowy łuku: + .arc-specify-center-point-of-arc = ARC Określ środek łuku: + .arc-specify-included-angle = ARC Określ kąt rozwarcia: + .arc-specify-length-of-chord = ARC Określ długość cięciwy: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Określ kierunek stycznej w punkcie początkowym łuku: + .arc-specify-radius-of-arc = ARC Określ promień łuku: + .chord-length = Długość cięciwy modify = .add-vertex = Dodaj wierzchołek @@ -2975,6 +3013,13 @@ modify = .tol-scale-dimtfac = Toł. skala (DIMTFAC) .trim = Przytnij .exit = Wyjdź + .area-keyword = Pole + .displacement = Przesunięcie + .mode-keyword = Tryb + .array-keyword = Szyk + .use-first-point-as-displacement = Pierwszy punkt jako przesunięcie + .offset-erase-source-object-after-offsetting = OFFSET Usunąć obiekt źródłowy po odsunięciu? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Warstwa odsuniętych obiektów [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter nazwa grupy [__ocs_arg_name__]: @@ -5264,6 +5309,19 @@ ui = .what-s-new = Co nowego .width-value = Szerokość: __ocs_arg_value__ .working = Pracuję… + .recent-input = Ostatnie wpisy + .copy-with-base-point = Kopiuj z punktem bazowym + .undo-label = Cofnij __ocs_arg_label__ + .redo-label = Ponów __ocs_arg_label__ + .copy-selection = Kopiuj zaznaczenie + .isolate = Izoluj + .deselect-all = Odznacz wszystko + .options-ellipsis = Opcje... + .base-point = Punkt bazowy + .snap-overrides = Tymczasowe lokalizacje + .mid-between-2-points = Środek między 2 punktami + .osnap-settings = Ustawienia lokalizacji... + .snap-override-none-next-pick-only = Tymczasowa lokalizacja: Brak (tylko następny punkt). dimstyle = .keep-text-between-extension-lines = Zawsze przechowuj tekst pomiędzy liniami pomocniczymi diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 43df0a1d6..a2a30fb6e 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Aplicativo padrão .iced-theme = Tema gelado: .theme = Tema + .right-click-customization = Personalização do clique direito + .right-click-in-drawing-area = Clique direito na área de desenho + .hold-duration = Duração do pressionamento + .right-click-mode-help = Menu de atalho: o clique direito sempre abre o menu. Sensível ao tempo: um clique rápido é Enter, segurar mais tempo abre o menu (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Menu de atalho + .right-click-mode-time-sensitive = Sensível ao tempo (clique rápido = Enter) + .right-click-mode-enter-first = Primeiro Enter, segundo clique = menu command-line = .ready = Open CAD Studio pronto. @@ -245,6 +252,7 @@ command-move = *[other] { $count } objetos }]: .target = MOVE Especifique o destino [base { $x },{ $y }]: + .displacement = MOVE Especifique o deslocamento: command-copy = .array-count = COPY Informe a quantidade de itens da matriz: @@ -259,6 +267,8 @@ command-copy = [one] { $count } cópia até agora *[other] { $count } cópias até agora } | Matriz | Enter=concluir | base { $x },{ $y }]: + .displacement = COPY Especifique o deslocamento: + .mode = COPY Informe o modo de cópia [Single/Multiple] : common = .jogged-radius = Raio com desvio @@ -2118,7 +2128,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2824,6 +2834,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Remover + .pline-specify-starting-width = PLINE Especifique a largura inicial <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Especifique a largura final <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Especifique a meia largura inicial <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Especifique a meia largura final <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Linha) Especifique o próximo ponto: + .pline-arc-specify-endpoint-of-arc = PLINE (Arco) Especifique o ponto final do arco: + .pline-specify-length-of-line = PLINE Especifique o comprimento da linha: + .pline-specify-included-angle = PLINE Especifique o ângulo incluído: + .pline-specify-endpoint-of-arc = PLINE Especifique o ponto final do arco: + .pline-specify-center-point-of-arc = PLINE Especifique o centro do arco: + .pline-specify-radius-of-arc = PLINE Especifique o raio do arco: + .pline-specify-direction-of-chord-for-arc = PLINE Especifique a direção da corda do arco: + .pline-specify-length-of-chord = PLINE Especifique o comprimento da corda: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Especifique a direção tangente no ponto inicial do arco: + .pline-specify-second-point-on-arc = PLINE Especifique o segundo ponto do arco: + .halfwidth = Meia largura + .center-keyword = Centro + .close-keyword = Fechar + .second-pt = Segundo ponto + .arc-specify-start-point-of-arc = ARC Especifique o ponto inicial do arco: + .arc-specify-second-point-of-arc = ARC Especifique o segundo ponto do arco: + .arc-specify-end-point-of-arc = ARC Especifique o ponto final do arco: + .arc-specify-center-point-of-arc = ARC Especifique o centro do arco: + .arc-specify-included-angle = ARC Especifique o ângulo incluído: + .arc-specify-length-of-chord = ARC Especifique o comprimento da corda: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Especifique a direção tangente no ponto inicial do arco: + .arc-specify-radius-of-arc = ARC Especifique o raio do arco: + .chord-length = Comprimento da corda modify = .add-vertex = Adicionar vértice @@ -2976,6 +3014,13 @@ modify = .tol-scale-dimtfac = Escala de tolerância (DIMTFAC) .trim = Aparar .exit = Sair + .area-keyword = Área + .displacement = Deslocamento + .mode-keyword = Modo + .array-keyword = Matriz + .use-first-point-as-displacement = Usar o primeiro ponto como deslocamento + .offset-erase-source-object-after-offsetting = OFFSET Apagar o objeto de origem após deslocar? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Camada dos objetos deslocados [Current/Source] : groups = .group-enter-group-name-name = GROUP Insira o nome do grupo [__ocs_arg_name__]: @@ -5265,6 +5310,19 @@ ui = .what-s-new = Novidades .width-value = Largura: __ocs_arg_value__ .working = Processando… + .recent-input = Entradas recentes + .copy-with-base-point = Copiar com ponto base + .undo-label = Desfazer __ocs_arg_label__ + .redo-label = Refazer __ocs_arg_label__ + .copy-selection = Copiar seleção + .isolate = Isolar + .deselect-all = Desmarcar tudo + .options-ellipsis = Opções... + .base-point = Ponto base + .snap-overrides = Substituições de snap + .mid-between-2-points = Meio entre 2 pontos + .osnap-settings = Configurações de snap... + .snap-override-none-next-pick-only = Substituição de snap: Nenhum (somente o próximo ponto). dimstyle = .keep-text-between-extension-lines = Sempre manter o texto entre as linhas de extensão diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index b89d617d4..3ee0e3ad4 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Применение по умолчанию .iced-theme = Тема ICED: .theme = Тема + .right-click-customization = Настройка правой кнопки мыши + .right-click-in-drawing-area = Правая кнопка в области чертежа + .hold-duration = Длительность удержания + .right-click-mode-help = Контекстное меню: правая кнопка всегда открывает меню. С учётом времени: короткий щелчок — Enter, удержание открывает меню (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Контекстное меню + .right-click-mode-time-sensitive = С учётом времени (короткий щелчок = Enter) + .right-click-mode-enter-first = Сначала Enter, второй щелчок = меню command-line = .ready = Open CAD Studio готов. @@ -246,6 +253,7 @@ command-move = *[many] { $count } объектов }]: .target = MOVE Укажите точку назначения [база { $x },{ $y }]: + .displacement = MOVE Укажите перемещение: command-copy = .array-count = COPY Введите число элементов массива: @@ -262,6 +270,8 @@ command-copy = [few] создано { $count } копии *[many] создано { $count } копий } | Массив | Enter=готово | база { $x },{ $y }]: + .displacement = COPY Укажите перемещение: + .mode = COPY Задайте режим копирования [Single/Multiple] : common = .jogged-radius = Радиус с изломом @@ -2121,7 +2131,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2827,6 +2837,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Удалить + .pline-specify-starting-width = PLINE Укажите начальную ширину <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Укажите конечную ширину <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Укажите начальную полуширину <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Укажите конечную полуширину <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Линия) Укажите следующую точку: + .pline-arc-specify-endpoint-of-arc = PLINE (Дуга) Укажите конечную точку дуги: + .pline-specify-length-of-line = PLINE Укажите длину линии: + .pline-specify-included-angle = PLINE Укажите центральный угол: + .pline-specify-endpoint-of-arc = PLINE Укажите конечную точку дуги: + .pline-specify-center-point-of-arc = PLINE Укажите центр дуги: + .pline-specify-radius-of-arc = PLINE Укажите радиус дуги: + .pline-specify-direction-of-chord-for-arc = PLINE Укажите направление хорды дуги: + .pline-specify-length-of-chord = PLINE Укажите длину хорды: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Укажите направление касательной в начальной точке дуги: + .pline-specify-second-point-on-arc = PLINE Укажите вторую точку дуги: + .halfwidth = Полуширина + .center-keyword = Центр + .close-keyword = Замкнуть + .second-pt = Вторая точка + .arc-specify-start-point-of-arc = ARC Укажите начальную точку дуги: + .arc-specify-second-point-of-arc = ARC Укажите вторую точку дуги: + .arc-specify-end-point-of-arc = ARC Укажите конечную точку дуги: + .arc-specify-center-point-of-arc = ARC Укажите центр дуги: + .arc-specify-included-angle = ARC Укажите центральный угол: + .arc-specify-length-of-chord = ARC Укажите длину хорды: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Укажите направление касательной в начальной точке дуги: + .arc-specify-radius-of-arc = ARC Укажите радиус дуги: + .chord-length = Длина хорды modify = .add-vertex = Добавить вершину @@ -2979,6 +3017,13 @@ modify = .tol-scale-dimtfac = Масштаб допуска (DIMTFAC) .trim = Усечь .exit = ЭКСИТ + .area-keyword = Площадь + .displacement = Перемещение + .mode-keyword = Режим + .array-keyword = Массив + .use-first-point-as-displacement = Первая точка как перемещение + .offset-erase-source-object-after-offsetting = OFFSET Удалить исходный объект после смещения? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Слой для смещённых объектов [Current/Source] : groups = .group-enter-group-name-name = GROUP Введите название группы [__ocs_arg_name__]: @@ -5266,6 +5311,19 @@ ui = .what-s-new = Что нового .width-value = Ширина: __ocs_arg_value__ .working = Работать... + .recent-input = Недавний ввод + .copy-with-base-point = Копировать с базовой точкой + .undo-label = Отменить __ocs_arg_label__ + .redo-label = Повторить __ocs_arg_label__ + .copy-selection = Копировать выбранное + .isolate = Изолировать + .deselect-all = Снять выделение + .options-ellipsis = Параметры... + .base-point = Базовая точка + .snap-overrides = Разовые привязки + .mid-between-2-points = Середина между 2 точками + .osnap-settings = Настройки привязки... + .snap-override-none-next-pick-only = Разовая привязка: Нет (только следующая точка). dimstyle = .keep-text-between-extension-lines = Всегда размещать текст между выносными линиями diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index fb9160dcc..cea64bba1 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Varsayılan Uygulama .iced-theme = Iced teması: .theme = Tema + .right-click-customization = Sağ tık özelleştirme + .right-click-in-drawing-area = Çizim alanında sağ tık + .hold-duration = Basılı tutma süresi + .right-click-mode-help = Kısayol menüsü: sağ tık her zaman menüyü açar. Zamana duyarlı: kısa tık Enter, uzun basma menüyü açar (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Kısayol menüsü + .right-click-mode-time-sensitive = Zamana duyarlı (kısa tık = Enter) + .right-click-mode-enter-first = Önce Enter, ikinci tık = menü command-line = .ready = Open CAD Studio hazır. @@ -238,12 +245,15 @@ area = command-move = .base = MOVE Temel noktayı belirtin [{ $count } nesne]: .target = MOVE Hedef noktayı belirtin [temel { $x },{ $y }]: + .displacement = MOVE Yer değiştirmeyi belirtin: command-copy = .array-count = COPY Dizideki öğe sayısını girin: .base = COPY Temel noktayı belirtin [{ $count } nesne]: .array-target = COPY { $count } öğeli dizi için ikinci noktayı belirtin [temel { $x },{ $y }]: .target = COPY Hedef noktayı belirtin [şimdiye kadar { $count } kopya | Dizi | Enter=tamam | temel { $x },{ $y }]: + .displacement = COPY Yer değiştirmeyi belirtin: + .mode = COPY Kopyalama modu seçin [Single/Multiple] : common = .none-use-shortcuts-set-key-command = (none – use: SHORTCUTS SET ) @@ -2102,7 +2112,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2806,6 +2816,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Kaldır + .pline-specify-starting-width = PLINE Başlangıç genişliğini belirtin <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Bitiş genişliğini belirtin <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Başlangıç yarı genişliğini belirtin <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Bitiş yarı genişliğini belirtin <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Çizgi) Sonraki noktayı belirtin: + .pline-arc-specify-endpoint-of-arc = PLINE (Yay) Yayın bitiş noktasını belirtin: + .pline-specify-length-of-line = PLINE Çizgi uzunluğunu belirtin: + .pline-specify-included-angle = PLINE İç açıyı belirtin: + .pline-specify-endpoint-of-arc = PLINE Yayın bitiş noktasını belirtin: + .pline-specify-center-point-of-arc = PLINE Yayın merkez noktasını belirtin: + .pline-specify-radius-of-arc = PLINE Yayın yarıçapını belirtin: + .pline-specify-direction-of-chord-for-arc = PLINE Yay kirişinin yönünü belirtin: + .pline-specify-length-of-chord = PLINE Kiriş uzunluğunu belirtin: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Yayın başlangıç noktasındaki teğet yönünü belirtin: + .pline-specify-second-point-on-arc = PLINE Yay üzerindeki ikinci noktayı belirtin: + .halfwidth = Yarı genişlik + .center-keyword = Merkez + .close-keyword = Kapat + .second-pt = İkinci nokta + .arc-specify-start-point-of-arc = ARC Yayın başlangıç noktasını belirtin: + .arc-specify-second-point-of-arc = ARC Yayın ikinci noktasını belirtin: + .arc-specify-end-point-of-arc = ARC Yayın bitiş noktasını belirtin: + .arc-specify-center-point-of-arc = ARC Yayın merkez noktasını belirtin: + .arc-specify-included-angle = ARC İç açıyı belirtin: + .arc-specify-length-of-chord = ARC Kiriş uzunluğunu belirtin: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Yayın başlangıç noktasındaki teğet yönünü belirtin: + .arc-specify-radius-of-arc = ARC Yayın yarıçapını belirtin: + .chord-length = Kiriş uzunluğu modify = .count-objects = (__ocs_arg_count__ nesne) @@ -2958,6 +2996,13 @@ modify = .tol-scale-dimtfac = Tolerans ölçeği (DIMTFAC) .trim = Kırp .exit = Çık + .area-keyword = Alan + .displacement = Yer değiştirme + .mode-keyword = Mod + .array-keyword = Dizi + .use-first-point-as-displacement = İlk noktayı yer değiştirme olarak kullan + .offset-erase-source-object-after-offsetting = OFFSET Ötelemeden sonra kaynak nesne silinsin mi? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Ötelenen nesnelerin katmanı [Current/Source] : groups = .group-enter-group-name-name = GROUP Grup adını girin [__ocs_arg_name__]: @@ -5188,6 +5233,19 @@ ui = .what-s-new = Yenilikler .width-value = Genişlik: __ocs_arg_value__ .working = Çalışıyor… + .recent-input = Son girişler + .copy-with-base-point = Taban noktasıyla kopyala + .undo-label = Geri al __ocs_arg_label__ + .redo-label = Yinele __ocs_arg_label__ + .copy-selection = Seçimi kopyala + .isolate = Yalıt + .deselect-all = Seçimi kaldır + .options-ellipsis = Seçenekler... + .base-point = Taban noktası + .snap-overrides = Geçici kenetlemeler + .mid-between-2-points = 2 nokta arası orta + .osnap-settings = Nesne kenetleme ayarları... + .snap-override-none-next-pick-only = Geçici kenetleme: Yok (yalnızca sonraki nokta). dimstyle = .keep-text-between-extension-lines = Metni her zaman uzatma çizgileri arasında tut diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 77a8561f4..70dc709d3 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = 默认应用程序 .iced-theme = Iced 主题: .theme = 主题 + .right-click-customization = 右键单击自定义 + .right-click-in-drawing-area = 在绘图区域中单击右键 + .hold-duration = 按住时长 + .right-click-mode-help = 快捷菜单:右键单击始终打开菜单。时间敏感:快速单击等于 Enter,长按打开菜单 (SHORTCUTMENUDURATION)。 + .right-click-mode-shortcut-menu = 快捷菜单 + .right-click-mode-time-sensitive = 时间敏感(快速单击 = Enter) + .right-click-mode-enter-first = 先 Enter,第二次单击 = 菜单 command-line = .ready = Open CAD Studio 已就绪。 @@ -240,12 +247,15 @@ area = command-move = .base = MOVE 指定基点 [{ $count } 个对象]: .target = MOVE 指定目标点 [基点 { $x },{ $y }]: + .displacement = MOVE 指定位移: command-copy = .array-count = COPY 输入阵列项目数: .base = COPY 指定基点 [{ $count } 个对象]: .array-target = COPY 为 { $count } 项阵列指定第二点 [基点 { $x },{ $y }]: .target = COPY 指定目标点 [已复制 { $count } 个 | 阵列 | 回车=完成 | 基点 { $x },{ $y }]: + .displacement = COPY 指定位移: + .mode = COPY 输入复制模式选项 [Single/Multiple] : common = .jogged-radius = 折弯半径 @@ -2105,7 +2115,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2811,6 +2821,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = 移除 + .pline-specify-starting-width = PLINE 指定起点宽度 <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE 指定端点宽度 <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE 指定起点半宽 <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE 指定端点半宽 <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (直线) 指定下一点: + .pline-arc-specify-endpoint-of-arc = PLINE (圆弧) 指定圆弧的端点: + .pline-specify-length-of-line = PLINE 指定直线的长度: + .pline-specify-included-angle = PLINE 指定夹角: + .pline-specify-endpoint-of-arc = PLINE 指定圆弧的端点: + .pline-specify-center-point-of-arc = PLINE 指定圆弧的圆心: + .pline-specify-radius-of-arc = PLINE 指定圆弧的半径: + .pline-specify-direction-of-chord-for-arc = PLINE 指定圆弧的弦方向: + .pline-specify-length-of-chord = PLINE 指定弦长: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE 指定圆弧起点的切线方向: + .pline-specify-second-point-on-arc = PLINE 指定圆弧上的第二点: + .halfwidth = 半宽 + .center-keyword = 圆心 + .close-keyword = 闭合 + .second-pt = 第二点 + .arc-specify-start-point-of-arc = ARC 指定圆弧的起点: + .arc-specify-second-point-of-arc = ARC 指定圆弧的第二点: + .arc-specify-end-point-of-arc = ARC 指定圆弧的端点: + .arc-specify-center-point-of-arc = ARC 指定圆弧的圆心: + .arc-specify-included-angle = ARC 指定夹角: + .arc-specify-length-of-chord = ARC 指定弦长: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC 指定圆弧起点的切线方向: + .arc-specify-radius-of-arc = ARC 指定圆弧的半径: + .chord-length = 弦长 modify = .add-vertex = 添加顶点 @@ -2963,6 +3001,13 @@ modify = .tol-scale-dimtfac = 公差比例 (DIMTFAC) .trim = 修剪 .exit = 退出 + .area-keyword = 面积 + .displacement = 位移 + .mode-keyword = 模式 + .array-keyword = 阵列 + .use-first-point-as-displacement = 将第一点用作位移 + .offset-erase-source-object-after-offsetting = OFFSET 偏移后是否删除源对象? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET 输入偏移对象的图层选项 [Current/Source] : groups = .group-enter-group-name-name = GROUP 输入组名 [__ocs_arg_name__]: @@ -5248,6 +5293,19 @@ ui = .what-s-new = 更新内容 .width-value = 宽度:__ocs_arg_value__ .working = 正在处理… + .recent-input = 最近的输入 + .copy-with-base-point = 带基点复制 + .undo-label = 放弃 __ocs_arg_label__ + .redo-label = 重做 __ocs_arg_label__ + .copy-selection = 复制选择 + .isolate = 隔离 + .deselect-all = 全部取消选择 + .options-ellipsis = 选项... + .base-point = 基点 + .snap-overrides = 捕捉替代 + .mid-between-2-points = 两点之间的中点 + .osnap-settings = 对象捕捉设置... + .snap-override-none-next-pick-only = 捕捉替代:无(仅下一次拾取)。 dimstyle = .keep-text-between-extension-lines = 始终将文字置于尺寸界线之间 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 7925576ab..041b2911f 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = 預設應用程式 .iced-theme = Iced 主題: .theme = 主題 + .right-click-customization = 右鍵自訂 + .right-click-in-drawing-area = 在繪圖區域按右鍵 + .hold-duration = 按住時間 + .right-click-mode-help = 快捷功能表:按右鍵一律開啟功能表。時間敏感:快速按一下等於 Enter,長按開啟功能表 (SHORTCUTMENUDURATION)。 + .right-click-mode-shortcut-menu = 快捷功能表 + .right-click-mode-time-sensitive = 時間敏感(快速按一下 = Enter) + .right-click-mode-enter-first = 先 Enter,第二次按一下 = 功能表 command-line = .ready = Open CAD Studio 已就緒。 @@ -244,6 +251,7 @@ command-move = *[other] { $count } 個物件 }]: .target = MOVE 指定目標點 [基點 { $x },{ $y }]: + .displacement = MOVE 指定位移: command-copy = .array-count = COPY 輸入陣列項目數: @@ -258,6 +266,8 @@ command-copy = [one] 目前 { $count } 個複本 *[other] 目前 { $count } 個複本 } | 陣列 | Enter=完成 | 基準點 { $x },{ $y }]: + .displacement = COPY 指定位移: + .mode = COPY 輸入複製模式選項 [Single/Multiple] : common = .jogged-radius = 折彎半徑 @@ -2117,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2823,6 +2833,34 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = 移除 + .pline-specify-starting-width = PLINE 指定起點寬度 <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE 指定端點寬度 <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE 指定起點半寬 <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE 指定端點半寬 <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (直線) 指定下一點: + .pline-arc-specify-endpoint-of-arc = PLINE (弧) 指定弧的端點: + .pline-specify-length-of-line = PLINE 指定直線的長度: + .pline-specify-included-angle = PLINE 指定夾角: + .pline-specify-endpoint-of-arc = PLINE 指定弧的端點: + .pline-specify-center-point-of-arc = PLINE 指定弧的圓心: + .pline-specify-radius-of-arc = PLINE 指定弧的半徑: + .pline-specify-direction-of-chord-for-arc = PLINE 指定弧的弦方向: + .pline-specify-length-of-chord = PLINE 指定弦長: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE 指定弧起點的切線方向: + .pline-specify-second-point-on-arc = PLINE 指定弧上的第二點: + .halfwidth = 半寬 + .center-keyword = 圓心 + .close-keyword = 閉合 + .second-pt = 第二點 + .arc-specify-start-point-of-arc = ARC 指定弧的起點: + .arc-specify-second-point-of-arc = ARC 指定弧的第二點: + .arc-specify-end-point-of-arc = ARC 指定弧的端點: + .arc-specify-center-point-of-arc = ARC 指定弧的圓心: + .arc-specify-included-angle = ARC 指定夾角: + .arc-specify-length-of-chord = ARC 指定弦長: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC 指定弧起點的切線方向: + .arc-specify-radius-of-arc = ARC 指定弧的半徑: + .chord-length = 弦長 modify = .add-vertex = 新增頂點 @@ -2975,6 +3013,13 @@ modify = .tol-scale-dimtfac = 公差比例 (DIMTFAC) .trim = 修剪 .exit = 退出 + .area-keyword = 面積 + .displacement = 位移 + .mode-keyword = 模式 + .array-keyword = 陣列 + .use-first-point-as-displacement = 將第一點用作位移 + .offset-erase-source-object-after-offsetting = OFFSET 偏移後是否刪除來源物件? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET 輸入偏移物件的圖層選項 [Current/Source] : groups = .group-enter-group-name-name = GROUP 輸入組名 [__ocs_arg_name__]: @@ -5260,6 +5305,19 @@ ui = .what-s-new = 更新內容 .width-value = 寬度:__ocs_arg_value__ .working = 正在處理… + .recent-input = 最近的輸入 + .copy-with-base-point = 帶基準點複製 + .undo-label = 復原 __ocs_arg_label__ + .redo-label = 重做 __ocs_arg_label__ + .copy-selection = 複製選取 + .isolate = 隔離 + .deselect-all = 全部取消選取 + .options-ellipsis = 選項... + .base-point = 基準點 + .snap-overrides = 鎖點取代 + .mid-between-2-points = 兩點之間的中點 + .osnap-settings = 物件鎖點設定... + .snap-override-none-next-pick-only = 鎖點取代:無(僅下一次點選)。 dimstyle = .keep-text-between-extension-lines = 始終將文字置於尺寸界線之間 diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 178b41a0a..2f8a5d3f9 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -176,6 +176,9 @@ impl OpenCADStudio { pub(super) fn cancel_active_grip_edit(&mut self) -> bool { let i = self.active_tab; + // Grip-menu toggles live only as long as the gesture. + self.tabs[i].grip_copy = false; + self.tabs[i].grip_base_pending = false; let had_grip = self.tabs[i].active_grip.take().is_some() || self.grip_add_provisional.is_some() || !self.grip_preview_handles.is_empty() @@ -783,6 +786,9 @@ impl OpenCADStudio { } return Task::none(); } + // Keywords, distances and points fed here (option buttons, the + // context menu, scripts) join the Recent Input list like typed ones. + self.command_line.record_recent_input(token); let is_mtp = token.trim_start_matches('_').eq_ignore_ascii_case("MTP") || token.trim_start_matches('_').eq_ignore_ascii_case("M2P"); if is_mtp { @@ -914,6 +920,9 @@ impl OpenCADStudio { ); let task = self.apply_cmd_result_inner(result); let i = self.active_tab; + // A transparent zoom that just finished hands control back to the + // command it interrupted before the "command ended" bookkeeping below. + self.resume_transparent_parent(i); let preview_hidden = self.tabs[i] .active_cmd .as_ref() @@ -1388,6 +1397,38 @@ impl OpenCADStudio { Task::none() } + /// Bring back the command a transparent zoom parked, once the zoom is + /// over (no command active any more). No-op otherwise. + pub(in crate::app) fn resume_transparent_parent(&mut self, i: usize) { + if !self.tabs[i].transparent_resume { + return; + } + if self.tabs[i].active_cmd.is_some() { + // The transparent prompt is still up (or the parent was already + // restored by a Cancel). + if self.tabs[i].suspended_cmd.is_none() { + self.tabs[i].transparent_resume = false; + } + return; + } + self.tabs[i].transparent_resume = false; + let Some(parent) = self.tabs[i].suspended_cmd.take() else { + return; + }; + self.tabs[i].active_cmd = Some(parent); + let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); + if let Some(p) = prompt { + self.command_line.push_info(&p); + } + let opts = self.tabs[i] + .active_cmd + .as_ref() + .map(|c| c.options()) + .unwrap_or_default(); + self.command_line.set_step_options(opts); + self.refresh_active_cmd_preview(i); + } + pub(in crate::app) fn start_mtp_modifier(&mut self, i: usize) { let parent = self.tabs[i].active_cmd.take(); self.tabs[i].suspended_cmd = parent; @@ -6596,6 +6637,12 @@ impl OpenCADStudio { } } } + { + let tab = &mut self.tabs[i]; + if let Some(command) = tab.active_cmd.as_mut() { + command.on_document_undone(&tab.scene.document); + } + } let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 4c747e0a1..67a06ab81 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -311,8 +311,8 @@ impl OpenCADStudio { } "ARC" => { - use crate::modules::draw::draw::arc::Arc3PCommand; - let new_cmd = Arc3PCommand::new(); + use crate::modules::draw::draw::arc::ArcCommand; + let new_cmd = ArcCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } @@ -323,8 +323,8 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARC_CSE" => { - use crate::modules::draw::draw::arc::ArcCommand; - let new_cmd = ArcCommand::new(); + use crate::modules::draw::draw::arc::ArcCSECommand; + let new_cmd = ArcCSECommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index be8281535..7d3d3c611 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -63,6 +63,12 @@ impl OpenCADStudio { // routing, so every path below (Start-tab gate, plugins, all dispatch // families, the Repeat menu) sees the canonical command. Arguments after // the first space are left untouched. A non-alias passes through as-is. + // The transparent prefix of commercial solutions: `'PAN` / `'ZOOM` run in the middle + // of another command and hand control back to it afterwards. + let (cmd, quoted) = match cmd.trim().strip_prefix('\'') { + Some(rest) => (rest.trim(), true), + None => (cmd, false), + }; let resolved = self.resolve_alias(cmd); let cmd = resolved.as_deref().unwrap_or(cmd); if is_spacemouse_command(cmd) { @@ -72,11 +78,22 @@ impl OpenCADStudio { // already running: pressing F8 partway through a LINE means "constrain // the rest of this line", not "abandon it". Everything below tears the // running command down, so a transparent one skips straight past it. - // (#677) - if is_transparent(cmd) { - return self - .dispatch_families(cmd, i) - .unwrap_or_else(Task::none); + // (#677) A quoted navigation command does the same; an interactive + // one (ZOOM Window) parks the running command and resumes it when the + // zoom ends, as does a ZOOM the zoom prompt itself hands off to. + let transparent = is_transparent(cmd) + || (quoted && is_transparent_capable(cmd)) + || (self.tabs[i].transparent_resume && cmd.starts_with("ZOOM")); + if transparent { + if zoom_prompts(cmd) && self.tabs[i].active_cmd.is_some() { + self.tabs[i].suspended_cmd = self.tabs[i].active_cmd.take(); + self.tabs[i].transparent_resume = true; + self.tabs[i].scene.clear_preview_wire(); + } + let task = self.dispatch_families(cmd, i).unwrap_or_else(Task::none); + // A one-shot zoom is over already — resume straight away. + self.resume_transparent_parent(i); + return task; } // A new command abandons any grip edit and its numeric input. let had_pending_grip_input = self.grip_pending.take().is_some(); @@ -115,6 +132,9 @@ impl OpenCADStudio { // template-property override too (#239). self.restore_add_selected_defaults(); } + // A command parked behind a transparent zoom / MTP goes with it. + self.tabs[i].suspended_cmd = None; + self.tabs[i].transparent_resume = false; // Starting any command leaves interactive navigation modes (their own // command arms below re-enable the selected one). self.tabs[i].pan_mode = false; @@ -275,6 +295,21 @@ impl OpenCADStudio { /// point, so F8 during a MOVE both ended the move and made the dragged ghost /// vanish. Nothing here starts a command, opens a document or reads geometry, /// so there is nothing for the teardown to protect. (#677) +/// Commands that may run transparently (`'PAN`, `'ZOOM …`): the navigation +/// set, which never edits the drawing. +fn is_transparent_capable(cmd: &str) -> bool { + cmd == "PAN" || cmd == "ZOOM" || cmd.starts_with("ZOOM ") || matches!(cmd, "ZW" | "ZE" | "ZA" | "ZP" | "ZI" | "ZO" | "ZD" | "ZEA" | "ZOBJ") +} + +/// ZOOM forms that install an interactive prompt (window corners, object +/// pick) and so need the running command parked while they last. +fn zoom_prompts(cmd: &str) -> bool { + matches!( + cmd, + "ZOOM" | "ZOOM WINDOW" | "ZOOM W" | "ZW" | "ZOOM OBJECT" | "ZOOM O" | "ZOBJ" + ) +} + pub fn is_transparent(cmd: &str) -> bool { is_spacemouse_command(cmd) || matches!( @@ -503,6 +538,8 @@ inventory::submit!(crate::command::CommandRegistration { "MIRRTEXT", "ZOOMWHEEL", "ZOOMFACTOR", + "SHORTCUTMENU", + "SHORTCUTMENUDURATION", "CURSORSIZE", "PICKBOX", "CURSORTYPE", diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index 751d2e5d2..f3dd496cd 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -956,6 +956,8 @@ impl OpenCADStudio { "MIRRTEXT" | "ZOOMWHEEL" | "ZOOMFACTOR" + | "SHORTCUTMENU" + | "SHORTCUTMENUDURATION" | "CURSORSIZE" | "PICKBOX" | "CURSORTYPE" @@ -1071,7 +1073,7 @@ impl OpenCADStudio { let value = it.next().map(|s| s.trim().to_string()); if name.is_empty() || name == "?" { self.command_line.push_info( - crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), + crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), ); } else { if name == "CETRANSPARENCY" { @@ -1490,6 +1492,51 @@ impl OpenCADStudio { Ok((format!("ZOOMFACTOR = {}", self.zoom_factor), false)) } }, + // Bit code of commercial solutions: 0 = no shortcut menus (right-click + // is Enter), 16 = time-sensitive; everything else is the + // regular shortcut menu. Reported as the defaults of commercial solutions + // (11 = default+edit+command menus, +16 when + // time-sensitive). + "SHORTCUTMENU" => { + use crate::app::settings::RightClickMode; + let code = |mode: RightClickMode| match mode { + RightClickMode::EnterFirst => 0, + RightClickMode::ShortcutMenu => 11, + RightClickMode::TimeSensitive => 27, + }; + match &value { + Some(v) => match v.parse::() { + Ok(bits) if (0..=31).contains(&bits) => { + self.right_click_mode = if bits == 0 { + RightClickMode::EnterFirst + } else if bits & 16 != 0 { + RightClickMode::TimeSensitive + } else { + RightClickMode::ShortcutMenu + }; + Ok((format!("SHORTCUTMENU = {}", code(self.right_click_mode)), true)) + } + _ => Err("SETVAR: integer from 0 to 31 required.".into()), + }, + None => Ok(( + format!("SHORTCUTMENU = {}", code(self.right_click_mode)), + false, + )), + } + } + "SHORTCUTMENUDURATION" => match &value { + Some(v) => match v.parse::() { + Ok(ms) if (100..=1000).contains(&ms) => { + self.right_click_hold_ms = ms; + Ok((format!("SHORTCUTMENUDURATION = {ms}"), true)) + } + _ => Err("SETVAR: integer from 100 to 1000 required.".into()), + }, + None => Ok(( + format!("SHORTCUTMENUDURATION = {}", self.right_click_hold_ms), + false, + )), + }, "CURSORSIZE" => match &value { Some(v) => match v.parse::() { Ok(size) if (1..=100).contains(&size) => { @@ -2347,6 +2394,8 @@ impl OpenCADStudio { name.as_str(), "ZOOMWHEEL" | "ZOOMFACTOR" + | "SHORTCUTMENU" + | "SHORTCUTMENUDURATION" | "CURSORSIZE" | "PICKBOX" | "CURSORTYPE" diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index 8c45f84d6..b7b73d36b 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -267,7 +267,8 @@ impl OpenCADStudio { "mtext_cancel" => Message::MTextCancel, "text_input" => Message::TextInlineInput(string(req, "value")?.into()), "text_commit" => Message::TextInlineOk, - "pointer_move" | "pointer_press" | "pointer_release" => { + "pointer_move" | "pointer_press" | "pointer_release" | "pointer_right_press" + | "pointer_right_release" => { let x = req["x"] .as_f64() .filter(|v| v.is_finite()) @@ -289,6 +290,10 @@ impl OpenCADStudio { let event = match name { "pointer_press" => self.update(Message::ViewportLeftPress), "pointer_release" => self.update(Message::ViewportLeftRelease), + // Right button: the context menu / Enter behaviour chosen in + // Options (see `Message::ViewportRightRelease`). + "pointer_right_press" => self.update(Message::ViewportRightPress), + "pointer_right_release" => self.update(Message::ViewportRightRelease), _ => Task::none(), }; return Ok(Task::batch([move_task, event])); diff --git a/src/app/document.rs b/src/app/document.rs index 24e43f196..6b90aaf54 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -146,6 +146,12 @@ pub(super) struct DocumentTab { pub(super) selected_grip_handles: Vec, /// Shift-selected grips, keyed by entity and object-local grip id. pub(super) hot_grips: rustc_hash::FxHashSet<(Handle, usize)>, + /// Grip-mode "Copy" toggle (context menu): each grip placement leaves the + /// original in place and adds a modified copy, until Enter / Esc. + pub(super) grip_copy: bool, + /// Grip-mode "Base Point" (context menu): the next left-click re-bases + /// the active grip edit instead of committing it. + pub(super) grip_base_pending: bool, pub(super) selected_handle: Option, /// Dynamic-block visibility grip for the current single selection. pub(super) visibility_grip: Option, @@ -225,6 +231,9 @@ pub(super) struct DocumentTab { #[cfg_attr(target_arch = "wasm32", allow(dead_code))] pub(super) plugin_state: HashMap<&'static str, Box>, pub(super) suspended_cmd: Option>, + /// `suspended_cmd` was parked by a transparent command (`'ZOOM`) and is + /// restored as soon as the transparent one ends. + pub(super) transparent_resume: bool, } impl DocumentTab { @@ -601,6 +610,8 @@ impl DocumentTab { selected_grips: vec![], selected_grip_handles: vec![], hot_grips: rustc_hash::FxHashSet::default(), + grip_copy: false, + grip_base_pending: false, selected_handle: None, visibility_grip: None, wireframe: false, @@ -634,6 +645,7 @@ impl DocumentTab { zoom_dynamic_mode: false, plugin_state: HashMap::new(), suspended_cmd: None, + transparent_resume: false, } } diff --git a/src/app/mod.rs b/src/app/mod.rs index aff2a2089..ff481c411 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -89,6 +89,17 @@ pub const HOVER_DWELL_MS: u128 = 500; /// threshold gates that extra redraw-avoidance behavior. pub const HOVER_DWELL_DENSE_WIRES: usize = 50_000; +/// Keyboard navigation inside the open right-click context menu. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ContextMenuNav { + Up, + Down, + /// Pick the highlighted row (or the default row when none is highlighted). + Enter, + /// Pick the next row whose mnemonic letter matches. + Mnemonic(char), +} + /// Open multi-functional-grip popup state. #[derive(Clone, Debug)] pub struct GripPopup { @@ -518,6 +529,10 @@ pub(super) struct OpenCADStudio { double_click_block_refedit: bool, /// Open ATTEDIT when double-clicking a block with attributes. double_click_block_attedit: bool, + /// What a right-click in the drawing area does (SHORTCUTMENU). + right_click_mode: settings::RightClickMode, + /// Time-sensitive right-click hold threshold, ms (SHORTCUTMENUDURATION). + right_click_hold_ms: i32, /// Selected-object count past which grips stop being generated /// (GRIPOBJLIMIT, 0..=32767; 0 = no limit). grip_object_limit: i32, @@ -2027,6 +2042,8 @@ pub enum Message { /// Mid Between 2 Points from the snap menu: modal 2-pick modifier over /// the active point prompt. SnapOverrideMtp, + /// Snap Overrides ▸ None: the next pick ignores object snaps. + SnapOverrideNone, /// Close the one-shot snap override menu without picking. SnapOverrideClose, /// Open a path from the Start tab's recent-documents list (skips the @@ -2158,6 +2175,10 @@ pub enum Message { ZoomWheelReversedChanged(bool), /// Change how far one wheel notch zooms (ZOOMFACTOR, 3..=100). ZoomFactorChanged(i32), + /// Options → User Preferences: right-click behaviour (SHORTCUTMENU). + RightClickModeChanged(settings::RightClickMode), + /// Options → User Preferences: time-sensitive hold threshold, ms. + RightClickHoldMsChanged(i32), /// Toggle TEXTEDIT ending after one object (TEXTEDITMODE). TextEditModeChanged(bool), /// Toggle continued dimensions inheriting the base style (DIMCONTINUEMODE). @@ -3291,9 +3312,15 @@ pub enum Message { TextInlineInput(String), /// Commit the editor: create or update the TEXT entity. TextInlineOk, - // ── Draw Order context menu ───────────────────────────────────────── - /// Toggle the Draw Order sub-items in the viewport context menu. - DrawOrderSubmenuToggle, + // ── Viewport right-click context menu ─────────────────────────────── + /// A context-menu row was picked (mouse or keyboard). Closes the menu and + /// runs the row's action through the same message the equivalent typed + /// input / shortcut would have produced. + ContextMenuPick(crate::ui::popup::context_menu::MenuAction), + /// Expand / collapse an accordion submenu of the open context menu. + ContextMenuSubmenuToggle(crate::ui::popup::context_menu::SubmenuId), + /// Keyboard navigation inside the open context menu. + ContextMenuNavigate(ContextMenuNav), /// Begin an interactive reference-object pick to move the current /// selection above (`true`) or below (`false`) the picked object. DrawOrderPickRef(bool), @@ -3808,6 +3835,8 @@ impl OpenCADStudio { pick_box: 3, double_click_block_refedit: false, double_click_block_attedit: true, + right_click_mode: settings::RightClickMode::ShortcutMenu, + right_click_hold_ms: 250, grip_object_limit: settings::DEFAULT_GRIP_OBJECT_LIMIT, ncopy_bind: false, cursor_type: settings::CursorType::Crosshair, diff --git a/src/app/settings.rs b/src/app/settings.rs index e0590355c..30c262533 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -132,6 +132,43 @@ impl CursorType { } } +/// What a right-click in the drawing area does (SHORTCUTMENU in commercial solutions / +/// "Right-click Customization"). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum RightClickMode { + /// The default of commercial solutions: a right-click always opens the shortcut menu (Enter / + /// Cancel / the command's options while a command runs; Repeat / edit + /// tools when idle). + #[default] + ShortcutMenu, + /// The "time-sensitive right-click" of commercial solutions: a quick click is Enter (or + /// repeats the last command when idle); holding the button longer than + /// `right_click_hold_ms` opens the shortcut menu. + TimeSensitive, + /// Original Open CAD Studio behaviour: while a command runs the first + /// right-click is Enter and a second consecutive one opens the menu; + /// when idle a right-click opens the menu. + EnterFirst, +} + +impl RightClickMode { + pub const ALL: [Self; 3] = [Self::ShortcutMenu, Self::TimeSensitive, Self::EnterFirst]; + + pub fn label(self) -> &'static str { + match self { + Self::ShortcutMenu => "Shortcut menu", + Self::TimeSensitive => "Time-sensitive (quick click = Enter)", + Self::EnterFirst => "Enter first, second click = menu", + } + } +} + +/// SHORTCUTMENUDURATION bounds: below 100 ms every click reads as a hold, +/// above 1000 ms the menu becomes unreachable in practice. +pub fn clamp_right_click_hold_ms(v: i32) -> i32 { + v.clamp(100, 1000) +} + /// Active pair of axes while isometric drafting is enabled. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum IsoPlane { @@ -312,6 +349,11 @@ pub struct UserSettings { pub double_click_block_refedit: bool, /// When true, double-clicking a block with attributes opens ATTEDIT. pub double_click_block_attedit: bool, + /// What a right-click in the drawing area does (SHORTCUTMENU). + pub right_click_mode: RightClickMode, + /// Hold duration that turns a time-sensitive right-click into the menu, + /// in milliseconds (SHORTCUTMENUDURATION, 100..=1000). + pub right_click_hold_ms: i32, /// GRIPOBJLIMIT: past this many selected objects, no grips are drawn at /// all. 0 means no limit. The drawing header carries no slot for it. pub grip_object_limit: i32, @@ -583,6 +625,8 @@ impl Default for UserSettings { selection_cycling: false, double_click_block_refedit: false, double_click_block_attedit: true, + right_click_mode: RightClickMode::ShortcutMenu, + right_click_hold_ms: 250, grip_object_limit: DEFAULT_GRIP_OBJECT_LIMIT, ncopy_bind: false, cursor_type: CursorType::Crosshair, diff --git a/src/app/update/command.rs b/src/app/update/command.rs index a01824f79..ec238814d 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -727,6 +727,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { crate::app::expr_eval::eval_to_string(&raw) }; self.command_line.input.clear(); + // Remember the token for the context menu's Recent Input + // list (prose steps excluded: a table cell or text body + // is not a reusable value). + let is_prose = self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|c| c.input_kind().is_free_text()); + if !is_prose { + self.command_line.record_recent_input(&text); + } // Offer the typed text to the command's option handler // first (keywords like PLINE's A/L/C, a radius, …). If it @@ -932,6 +942,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { if grip_dyn_locked { return self.update(Message::CommandSubmit); } + // Enter while a grip is hot places it where it is (or keeps + // it hot when it has not moved) — it must not fall through to + // "repeat the last command", which would discard the edit. + if self.tabs[i].active_grip.is_some() && self.tabs[i].active_cmd.is_none() { + return self.commit_active_grip_edit(); + } // Normal command Dynamic Input commit. if let Some(task) = self.try_dyn_commit() { diff --git a/src/app/update/context_menu.rs b/src/app/update/context_menu.rs new file mode 100644 index 000000000..ea609b439 --- /dev/null +++ b/src/app/update/context_menu.rs @@ -0,0 +1,943 @@ +//! Viewport right-click context menu: gathering the menu's inputs from app +//! state, running a picked row, and keyboard navigation while it is open. +//! +//! The rows themselves are built by `ui::popup::context_menu` (a pure model); +//! this module is the glue between that model and the app's messages. + +use crate::app::{ArrowKey, ContextMenuNav, Message, OpenCADStudio}; +use crate::command::StepInput; +use crate::scene::pick::grip::GripEditMode; +use crate::ui::popup::context_menu::{ + build_context_menu, ContextMenu, GripMenuCmd, GripMenuContext, MenuAction, MenuContext, + SubmenuId, +}; +use acadrust::Handle; +use iced::Task; + +impl OpenCADStudio { + /// True while the viewport context menu is open on the active tab. + pub(in crate::app) fn context_menu_open(&self) -> bool { + self.tabs + .get(self.active_tab) + .is_some_and(|tab| tab.scene.selection.borrow().context_menu.is_some()) + } + + /// Everything the menu builder needs, read from the active tab. Called by + /// the view (to render) and by the keyboard handler (to resolve picks), so + /// both always see the same rows. + pub(in crate::app) fn context_menu_context(&self) -> MenuContext { + let tab = &self.tabs[self.active_tab]; + if let Some(grip) = tab.active_grip.as_ref() { + return MenuContext::Grip(GripMenuContext { + mode: grip.mode, + copy_on: tab.grip_copy, + moved: grip.last_world != grip.origin_world, + }); + } + if let Some(cmd) = tab.active_cmd.as_ref() { + // Same guard as typed MTP/M2P and SnapOverrideMtp: the step must + // take a point pick (possibly alongside keywords), not an entity. + let has_point_step = (!cmd.input_kind().wants_text() + || cmd.point_step_accepts_keywords()) + && !cmd.needs_entity_pick(); + return MenuContext::Command { + options: cmd.options(), + has_point_step, + recent_inputs: self.command_line.recent_inputs.clone(), + }; + } + MenuContext::Idle { + has_selection: !tab.scene.selected.is_empty(), + selected_constraint: tab.scene.selected_constraint, + isolation_active: tab.scene.is_isolation_active(), + recent_cmds: self + .command_line + .recent_commands + .iter() + .rev() + .take(crate::ui::popup::context_menu::RECENT_LIMIT) + .cloned() + .collect(), + clipboard_nonempty: !self.clipboard.is_empty(), + undo_label: tab.history.undo_stack.last().map(|s| s.label().to_string()), + redo_label: tab.history.redo_stack.last().map(|s| s.label().to_string()), + props_open: self.show_properties, + } + } + + /// The menu as currently shown (rows + expanded submenu). + pub(in crate::app) fn current_context_menu(&self) -> ContextMenu { + let open = self.tabs[self.active_tab] + .scene + .selection + .borrow() + .context_menu_ui + .open_submenu; + build_context_menu(&self.context_menu_context(), open) + } + + /// Close the menu and give the keyboard back to the command line. + pub(in crate::app) fn close_context_menu(&mut self) -> Task { + let i = self.active_tab; + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + if sel.context_menu.is_none() { + return Task::none(); + } + sel.close_context_menu(); + drop(sel); + self.focus_cmd_input() + } + + pub(in crate::app) fn on_context_menu_submenu_toggle(&mut self, id: SubmenuId) -> Task { + let i = self.active_tab; + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + let ui = &mut sel.context_menu_ui; + ui.open_submenu = if ui.open_submenu == Some(id) { None } else { Some(id) }; + drop(sel); + // Keep the keyboard highlight on the header that was toggled. + let menu = self.current_context_menu(); + let header = menu + .selectable() + .iter() + .position(|s| s.header == Some(id)); + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + if sel.context_menu_ui.highlighted.is_some() { + sel.context_menu_ui.highlighted = header; + } + Task::none() + } + + /// Run a picked row. The menu closes first so the action sees the same + /// state a typed keyword / shortcut would (no menu open, command line + /// focused), then the action is routed through the existing message for + /// that behaviour. + pub(in crate::app) fn on_context_menu_pick(&mut self, action: MenuAction) -> Task { + if let MenuAction::ToggleSubmenu(id) = action { + return self.on_context_menu_submenu_toggle(id); + } + let i = self.active_tab; + { + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.close_context_menu(); + // A pick counts as "another interaction" for the Enter-first + // right-click cycle: the next right-click acts as Enter again. + sel.right_click_entered = false; + } + let focus = self.focus_cmd_input(); + let task = match action { + MenuAction::Enter if self.tabs[i].active_grip.is_some() => { + self.commit_active_grip_edit() + } + MenuAction::Enter => self.update(Message::CommandFinalize), + MenuAction::Cancel => self.update(Message::CommandEscape), + MenuAction::Option(kw) => self.update(Message::CommandOptionPick(kw)), + MenuAction::FeedInput(token) => { + if self.tabs[i].active_cmd.is_some() { + self.feed_active_cmd(&token) + } else { + self.run_command_line(&token) + } + } + MenuAction::Command(cmd) => self.update(Message::Command(cmd)), + MenuAction::Mtp => self.update(Message::SnapOverrideMtp), + MenuAction::SnapOverride(t) => self.update(Message::SnapOverridePick(t)), + MenuAction::SnapOverrideNone => self.update(Message::SnapOverrideNone), + MenuAction::DeleteSelected => self.update(Message::DeleteSelected), + MenuAction::SelectSimilar => self.update(Message::SelectSimilar), + MenuAction::InvertSelection => self.update(Message::InvertSelection), + MenuAction::DeselectAll => { + self.tabs[i].scene.deselect_all(); + self.refresh_properties(); + Task::none() + } + MenuAction::QuickSelect => self.update(Message::QSelectOpen), + MenuAction::Properties => self.update(Message::ToggleProperties), + MenuAction::Options => self.update(Message::OptionsOpen), + MenuAction::Undo => self.update(Message::Undo), + MenuAction::Redo => self.update(Message::Redo), + MenuAction::DrawOrderPickRef(above) => self.update(Message::DrawOrderPickRef(above)), + MenuAction::ConstraintDelete(id) => self.update(Message::PropConstraintDelete(id)), + MenuAction::Grip(cmd) => self.on_grip_context_pick(cmd), + MenuAction::ToggleSubmenu(_) => Task::none(), + }; + Task::batch(vec![focus, task]) + } + + /// Keyboard navigation while the menu is open. + pub(in crate::app) fn on_context_menu_navigate(&mut self, nav: ContextMenuNav) -> Task { + if !self.context_menu_open() { + return Task::none(); + } + let i = self.active_tab; + let menu = self.current_context_menu(); + let rows = menu.selectable(); + if rows.is_empty() { + return Task::none(); + } + let current = self.tabs[i].scene.selection.borrow().context_menu_ui.highlighted; + let next_enabled = |from: usize, step: isize| -> usize { + let n = rows.len() as isize; + let mut idx = from as isize; + for _ in 0..n { + idx = (idx + step).rem_euclid(n); + if rows[idx as usize].enabled { + return idx as usize; + } + } + from + }; + match nav { + ContextMenuNav::Down => { + let idx = match current { + // First press lands on the default row itself. + None => menu.default_index(), + Some(cur) => next_enabled(cur, 1), + }; + self.tabs[i].scene.selection.borrow_mut().context_menu_ui.highlighted = Some(idx); + Task::none() + } + ContextMenuNav::Up => { + let idx = match current { + None => menu.default_index(), + Some(cur) => next_enabled(cur, -1), + }; + self.tabs[i].scene.selection.borrow_mut().context_menu_ui.highlighted = Some(idx); + Task::none() + } + ContextMenuNav::Enter => { + let idx = current.unwrap_or_else(|| menu.default_index()); + let row = &rows[idx]; + if !row.enabled { + return Task::none(); + } + self.on_context_menu_pick(row.action.clone()) + } + ContextMenuNav::Mnemonic(ch) => match menu.find_mnemonic(ch, current) { + Some(idx) => { + // A letter shared by several rows (PLINE's CEnter / CLose) + // cycles the highlight; a unique one picks at once. + let unique = menu.find_mnemonic(ch, Some(idx)) == Some(idx); + if unique { + self.on_context_menu_pick(rows[idx].action.clone()) + } else { + self.tabs[i].scene.selection.borrow_mut().context_menu_ui.highlighted = + Some(idx); + Task::none() + } + } + None => Task::none(), + }, + } + } + + /// Route keyboard-derived messages to the open menu. Returns `Some(task)` + /// when the message was consumed; `None` lets it fall through to the + /// normal handlers (after closing the menu when the key is not a menu key, + /// so typing a command name or a coordinate just works — the menu gets + /// out of the way exactly as it does in commercial solutions). + pub(in crate::app) fn intercept_context_menu_key(&mut self, msg: &Message) -> Option> { + match msg { + Message::ArrowKeyPressed { direction, .. } + | Message::CommandLineArrowProbe { direction, .. } => match direction { + ArrowKey::Up => Some(self.on_context_menu_navigate(ContextMenuNav::Up)), + ArrowKey::Down => Some(self.on_context_menu_navigate(ContextMenuNav::Down)), + ArrowKey::Left | ArrowKey::Right => { + // On a submenu header, Right expands and Left collapses. + let i = self.active_tab; + let ui = self.tabs[i].scene.selection.borrow().context_menu_ui.clone(); + let menu = self.current_context_menu(); + let rows = menu.selectable(); + let header = ui.highlighted.and_then(|c| rows.get(c)).and_then(|r| r.header); + let want_open = matches!(direction, ArrowKey::Right); + match header { + Some(id) if (ui.open_submenu == Some(id)) != want_open => { + Some(self.on_context_menu_submenu_toggle(id)) + } + _ => Some(Task::none()), + } + } + }, + Message::DynTabNext => Some(self.on_context_menu_navigate(ContextMenuNav::Down)), + Message::CommandFinalize | Message::CommandSpace => { + if self.command_line.input.trim().is_empty() { + Some(self.on_context_menu_navigate(ContextMenuNav::Enter)) + } else { + // Typed text pending: Enter runs it, the menu just closes. + let close = self.close_context_menu(); + let run = self.update(Message::CommandFinalize); + Some(Task::batch(vec![close, run])) + } + } + Message::CommandSubmit => { + if self.command_line.input.trim().is_empty() { + Some(self.on_context_menu_navigate(ContextMenuNav::Enter)) + } else { + let _ = self.close_context_menu(); + None + } + } + Message::CommandAppendChar(s) => self.context_menu_typed(s.chars().next()), + Message::CommandInput(buf) => { + // The focused command-line field delivers the whole buffer; + // only a single appended character is a candidate mnemonic. + let appended = buf + .strip_prefix(self.command_line.input.as_str()) + .filter(|rest| rest.chars().count() == 1) + .and_then(|rest| rest.chars().next()); + match appended { + Some(ch) => self.context_menu_typed(Some(ch)), + None => { + let _ = self.close_context_menu(); + None + } + } + } + _ => None, + } + } + + /// A printable key while the menu is open: a mnemonic picks its row; + /// anything else closes the menu and is typed into the command line. + fn context_menu_typed(&mut self, ch: Option) -> Option> { + let ch = ch?; + let menu = self.current_context_menu(); + if ch.is_ascii_alphanumeric() && menu.find_mnemonic(ch, None).is_some() { + return Some(self.on_context_menu_navigate(ContextMenuNav::Mnemonic(ch))); + } + let _ = self.close_context_menu(); + None + } + + /// Grip-mode shortcut menu (the grip menu of commercial solutions): the grip stays hot + /// for Stretch / Base Point / Copy / Undo; Move / Rotate / Scale / Mirror + /// hand the selection to the matching command with the grip as its base + /// point; Exit drops the edit. + pub(in crate::app) fn on_grip_context_pick(&mut self, cmd: GripMenuCmd) -> Task { + let i = self.active_tab; + let Some(grip) = self.tabs[i].active_grip.clone() else { + return Task::none(); + }; + match cmd { + GripMenuCmd::Exit => { + self.cancel_active_grip_edit(); + self.tabs[i].grip_copy = false; + self.tabs[i].grip_base_pending = false; + Task::none() + } + GripMenuCmd::Stretch => { + if let Some(active) = self.tabs[i].active_grip.as_mut() { + active.mode = GripEditMode::Stretch; + active.axis = None; + } + Task::none() + } + GripMenuCmd::Move | GripMenuCmd::Rotate | GripMenuCmd::Scale | GripMenuCmd::Mirror => { + let base = grip.origin_world; + let handles: Vec = grip.targets.iter().map(|t| t.handle).collect(); + self.cancel_active_grip_edit(); + self.tabs[i].grip_copy = false; + self.tabs[i].grip_base_pending = false; + // The command edits the current selection; make sure the + // gripped objects are in it. + if self.tabs[i].scene.selected.is_empty() { + self.tabs[i].scene.select_entities(&handles); + } + let name = match cmd { + GripMenuCmd::Move => "MOVE", + GripMenuCmd::Rotate => "ROTATE", + GripMenuCmd::Scale => "SCALE", + _ => "MIRROR", + }; + let start = self.dispatch_command(name); + if self.tabs[i].active_cmd.is_none() { + return start; + } + // The grip is the base point, so the command opens directly + // at its second prompt. + self.last_point = Some(base); + let fed = self.feed_command(StepInput::Point(base)); + Task::batch(vec![start, fed]) + } + GripMenuCmd::BasePoint => { + self.tabs[i].grip_base_pending = true; + Task::none() + } + GripMenuCmd::CopyToggle => { + self.tabs[i].grip_copy = !self.tabs[i].grip_copy; + Task::none() + } + GripMenuCmd::Undo => { + // Put the shapes back where the gesture started and keep the + // grip hot there (the Copy toggle survives). + let copy = self.tabs[i].grip_copy; + self.cancel_active_grip_edit(); + self.tabs[i].grip_copy = copy; + let mut again = grip; + let delta = again.last_world - again.origin_world; + again.last_world = again.origin_world; + for target in &mut again.targets { + target.last_world -= delta; + } + self.tabs[i].active_grip = Some(again); + Task::none() + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::app::settings::RightClickMode; + use crate::command::{CadCommand, StepInput}; + use crate::ui::popup::context_menu::MenuAction; + use glam::DVec3; + use iced::Point; + + /// `update` recursion (right-click → CommandFinalize → CommandSubmit → + /// apply) is deeper than the default test-thread stack; production + /// threads are sized for it (see `.cargo/config.toml`), so tests get an + /// equivalent stack here. + fn with_stack(f: impl FnOnce() + Send + 'static) { + std::thread::Builder::new() + .stack_size(32 * 1024 * 1024) + .spawn(f) + .expect("spawn test thread") + .join() + .unwrap_or_else(|e| std::panic::resume_unwind(e)); + } + + fn app() -> OpenCADStudio { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + app.tabs[0].scene.selection.borrow_mut().vp_size = (800.0, 600.0); + let _ = app.update(Message::ViewportMove(Point::new(200.0, 150.0))); + app + } + + fn start(app: &mut OpenCADStudio, cmd: &str, points: &[(f64, f64)]) { + let _ = app.dispatch_command(cmd); + for &(x, y) in points { + let _ = app.feed_command(StepInput::Point(DVec3::new(x, y, 0.0))); + } + } + + fn right_click(app: &mut OpenCADStudio) { + let _ = app.update(Message::ViewportRightPress); + let _ = app.update(Message::ViewportRightRelease); + } + + fn menu_open(app: &OpenCADStudio) -> bool { + app.tabs[0].scene.selection.borrow().context_menu.is_some() + } + + fn active(app: &OpenCADStudio) -> Option<&'static str> { + app.tabs[0].active_cmd.as_ref().map(|c| c.name()) + } + + fn menu_actions(app: &OpenCADStudio) -> Vec { + app.current_context_menu() + .selectable() + .into_iter() + .map(|s| s.action) + .collect() + } + + #[test] + fn shortcut_menu_mode_single_rmb_opens_menu_during_command() { + with_stack(|| { + let mut app = app(); + assert_eq!(app.right_click_mode, RightClickMode::ShortcutMenu); + start(&mut app, "LINE", &[(0.0, 0.0), (10.0, 0.0)]); + right_click(&mut app); + assert!(menu_open(&app)); + assert_eq!(active(&app), Some("LINE")); + let acts = menu_actions(&app); + assert_eq!(acts[0], MenuAction::Enter); + assert_eq!(acts[1], MenuAction::Cancel); + assert!(acts.contains(&MenuAction::Option("C".into()))); + assert!(acts.contains(&MenuAction::Option("U".into()))); + assert!(acts.contains(&MenuAction::ToggleSubmenu(SubmenuId::SnapOverrides))); + assert!(acts.contains(&MenuAction::Command("'PAN".into()))); + }); + } + + #[test] + fn enter_first_mode_first_rmb_is_enter_second_opens_menu() { + with_stack(|| { + let mut app = app(); + app.right_click_mode = RightClickMode::EnterFirst; + start(&mut app, "LINE", &[(0.0, 0.0), (10.0, 0.0)]); + right_click(&mut app); + // Enter ends LINE. + assert!(!menu_open(&app)); + assert_eq!(active(&app), None); + // Idle: the next right-click opens the menu. + right_click(&mut app); + assert!(menu_open(&app)); + let acts = menu_actions(&app); + assert_eq!(acts[0], MenuAction::Command("LINE".into()), "Repeat LINE first"); + }); + } + + #[test] + fn time_sensitive_quick_click_is_enter_and_hold_opens_menu() { + with_stack(|| { + let mut app = app(); + app.right_click_mode = RightClickMode::TimeSensitive; + app.right_click_hold_ms = 250; + start(&mut app, "PLINE", &[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]); + // Held: opens the menu, PLINE keeps running. + let _ = app.update(Message::ViewportRightPress); + { + let mut sel = app.tabs[0].scene.selection.borrow_mut(); + sel.right_press_time = iced::time::Instant::now() + .checked_sub(std::time::Duration::from_millis(400)); + } + let _ = app.update(Message::ViewportRightRelease); + assert!(menu_open(&app)); + assert_eq!(active(&app), Some("PLINE")); + // Dismiss, then a quick click finishes the polyline. + let _ = app.update(Message::CommandEscape); + assert!(!menu_open(&app)); + assert_eq!(active(&app), Some("PLINE")); + right_click(&mut app); + assert!(!menu_open(&app)); + assert_eq!(active(&app), None); + }); + } + + #[test] + fn pending_text_runs_as_enter_in_all_modes() { + with_stack(|| { + for mode in RightClickMode::ALL { + let mut app = app(); + app.right_click_mode = mode; + start(&mut app, "LINE", &[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]); + app.command_line.input = "C".into(); + right_click(&mut app); + assert!(!menu_open(&app), "{mode:?}"); + assert_eq!(active(&app), None, "{mode:?}: Close should end LINE"); + let lines = app.tabs[0] + .scene + .document + .entities() + .filter(|e| matches!(e, acadrust::EntityType::Line(_))) + .count(); + assert_eq!(lines, 3, "{mode:?}"); + } + }); + } + + #[test] + fn context_menu_pick_option_feeds_keyword() { + with_stack(|| { + let mut app = app(); + start(&mut app, "PLINE", &[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]); + right_click(&mut app); + assert!(menu_open(&app)); + let _ = app.update(Message::ContextMenuPick(MenuAction::Option("C".into()))); + assert!(!menu_open(&app)); + assert_eq!(active(&app), None); + let closed = app.tabs[0].scene.document.entities().any(|e| { + matches!(e, acadrust::EntityType::LwPolyline(p) if p.is_closed && p.vertices.len() == 3) + }); + assert!(closed, "PLINE should have been closed by the menu pick"); + }); + } + + #[test] + fn context_menu_pick_closes_menu_and_resets_enter_cycle() { + with_stack(|| { + let mut app = app(); + app.right_click_mode = RightClickMode::EnterFirst; + start(&mut app, "LINE", &[(0.0, 0.0), (10.0, 0.0)]); + right_click(&mut app); // Enter → LINE ends + right_click(&mut app); // idle → menu + assert!(menu_open(&app)); + let _ = app.update(Message::ContextMenuPick(MenuAction::Command("LINE".into()))); + assert!(!menu_open(&app)); + assert_eq!(active(&app), Some("LINE")); + assert!(!app.tabs[0].scene.selection.borrow().right_click_entered); + }); + } + + #[test] + fn menu_enter_with_no_highlight_picks_default() { + with_stack(|| { + let mut app = app(); + start(&mut app, "PLINE", &[(0.0, 0.0), (10.0, 0.0)]); + right_click(&mut app); + let _ = app.update(Message::CommandFinalize); + assert!(!menu_open(&app)); + assert_eq!(active(&app), None, "default row (Done) finishes PLINE"); + }); + } + + #[test] + fn menu_arrow_down_then_enter_picks_cancel() { + with_stack(|| { + let mut app = app(); + start(&mut app, "LINE", &[(0.0, 0.0), (10.0, 0.0)]); + right_click(&mut app); + let _ = app.update(Message::ContextMenuNavigate(ContextMenuNav::Down)); // Enter row + assert_eq!( + app.tabs[0].scene.selection.borrow().context_menu_ui.highlighted, + Some(0) + ); + let _ = app.update(Message::ContextMenuNavigate(ContextMenuNav::Down)); // Cancel + let _ = app.update(Message::ContextMenuNavigate(ContextMenuNav::Enter)); + assert!(!menu_open(&app)); + assert_eq!(active(&app), None); + }); + } + + #[test] + fn menu_mnemonic_c_closes_pline() { + with_stack(|| { + let mut app = app(); + start(&mut app, "PLINE", &[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]); + right_click(&mut app); + let _ = app.update(Message::CommandAppendChar("c".into())); + assert!(!menu_open(&app)); + assert_eq!(active(&app), None); + let closed = app.tabs[0].scene.document.entities().any( + |e| matches!(e, acadrust::EntityType::LwPolyline(p) if p.is_closed), + ); + assert!(closed); + }); + } + + #[test] + fn menu_unmatched_char_closes_menu_and_types() { + with_stack(|| { + let mut app = app(); + app.dyn_input = false; + start(&mut app, "LINE", &[(0.0, 0.0)]); + right_click(&mut app); + assert!(menu_open(&app)); + let _ = app.update(Message::CommandAppendChar("7".into())); + assert!(!menu_open(&app)); + assert_eq!(active(&app), Some("LINE")); + assert!(app.command_line.input.contains('7'), "{:?}", app.command_line.input); + }); + } + + #[test] + fn recent_input_pick_refeeds_token() { + with_stack(|| { + let mut app = app(); + start(&mut app, "LINE", &[(0.0, 0.0)]); + let _ = app.feed_active_cmd("10,10"); + assert_eq!(app.command_line.recent_inputs, vec!["10,10".to_string()]); + let _ = app.feed_active_cmd("20,0"); + right_click(&mut app); + let _ = app.update(Message::ContextMenuSubmenuToggle(SubmenuId::RecentInput)); + let acts = menu_actions(&app); + assert!(acts.contains(&MenuAction::FeedInput("10,10".into()))); + let _ = app.update(Message::ContextMenuPick(MenuAction::FeedInput("10,10".into()))); + assert!(!menu_open(&app)); + let lines = app.tabs[0] + .scene + .document + .entities() + .filter(|e| matches!(e, acadrust::EntityType::Line(_))) + .count(); + assert_eq!(lines, 3); + }); + } + + #[test] + fn idle_selection_menu_erase_deletes_selection() { + with_stack(|| { + let mut app = app(); + start(&mut app, "LINE", &[(0.0, 0.0), (10.0, 0.0)]); + let _ = app.update(Message::CommandEscape); + let handle = app.tabs[0] + .scene + .document + .entities() + .find_map(|e| match e { + acadrust::EntityType::Line(l) => Some(l.common.handle), + _ => None, + }) + .expect("a line"); + app.tabs[0].scene.select_entities(&[handle]); + right_click(&mut app); + let acts = menu_actions(&app); + assert!(acts.contains(&MenuAction::DeleteSelected)); + assert!(acts.contains(&MenuAction::Command("ROTATE".into()))); + let _ = app.update(Message::ContextMenuPick(MenuAction::DeleteSelected)); + assert!(!menu_open(&app)); + let lines = app.tabs[0] + .scene + .document + .entities() + .filter(|e| matches!(e, acadrust::EntityType::Line(_))) + .count(); + assert_eq!(lines, 0); + }); + } + + // ── Grip menu ───────────────────────────────────────────────────── + + /// A drawing with one line, selected, its start grip hot at the origin. + fn grip_app() -> (OpenCADStudio, Handle) { + use acadrust::{entities::Line, types::Vector3, EntityType}; + let mut app = app(); + app.snapper.snap_enabled = false; + app.snapper.grid_snap_on = false; + app.snapper.otrack_enabled = false; + app.ortho_mode = false; + app.polar_mode = false; + let handle = app.tabs[0].scene.add_entity(EntityType::Line(Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(10.0, 0.0, 0.0), + ))); + app.tabs[0].scene.selected.insert(handle); + app.refresh_selected_grips(); + app.tabs[0].active_grip = Some(crate::scene::pick::grip::GripEdit::single( + handle, + 0, + false, + DVec3::ZERO, + )); + (app, handle) + } + + fn move_cursor_to(app: &mut OpenCADStudio, world: DVec3) { + let cursor = app.tabs[0] + .scene + .camera + .borrow() + .project(world, iced::Rectangle::with_size(iced::Size::new(800.0, 600.0))) + .unwrap(); + let _ = app.on_viewport_move(Point::new(cursor.x, cursor.y)); + } + + fn line_start(app: &OpenCADStudio, handle: Handle) -> (f64, f64) { + match app.tabs[0].scene.document.get_entity(handle) { + Some(acadrust::EntityType::Line(l)) => (l.start.x, l.start.y), + other => panic!("{other:?}"), + } + } + + #[test] + fn rmb_on_hot_grip_opens_grip_menu() { + with_stack(|| { + let (mut app, _) = grip_app(); + right_click(&mut app); + assert!(menu_open(&app)); + let acts = menu_actions(&app); + assert_eq!(acts[0], MenuAction::Enter); + assert!(acts.contains(&MenuAction::Grip(GripMenuCmd::Move))); + assert!(acts.contains(&MenuAction::Grip(GripMenuCmd::Exit))); + assert!(app.tabs[0].active_grip.is_some(), "the grip stays hot under the menu"); + }); + } + + #[test] + fn grip_menu_move_dispatches_move_with_grip_base() { + with_stack(|| { + let (mut app, _) = grip_app(); + right_click(&mut app); + let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::Move))); + assert!(!menu_open(&app)); + assert!(app.tabs[0].active_grip.is_none()); + assert_eq!(active(&app), Some("MOVE")); + let prompt = app.tabs[0].active_cmd.as_ref().map(|c| c.prompt()).unwrap_or_default(); + let base_prompt = crate::tr!("command-move", "base", count = 1i64); + assert_ne!(prompt, base_prompt, "base already consumed"); + assert_eq!(app.last_point, Some(DVec3::ZERO)); + }); + } + + #[test] + fn grip_menu_exit_restores_original_geometry() { + with_stack(|| { + let (mut app, handle) = grip_app(); + move_cursor_to(&mut app, DVec3::new(3.0, 4.0, 0.0)); + let (x, y) = line_start(&app, handle); + assert!((x - 3.0).abs() < 1e-3 && (y - 4.0).abs() < 1e-3, "drag applied"); + right_click(&mut app); + let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::Exit))); + assert!(app.tabs[0].active_grip.is_none()); + let (x, y) = line_start(&app, handle); + assert!(x.abs() < 1e-9 && y.abs() < 1e-9, "Exit restores the original"); + }); + } + + #[test] + fn grip_menu_undo_snaps_back_but_stays_hot() { + with_stack(|| { + let (mut app, handle) = grip_app(); + move_cursor_to(&mut app, DVec3::new(3.0, 4.0, 0.0)); + right_click(&mut app); + let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::Undo))); + let (x, y) = line_start(&app, handle); + assert!(x.abs() < 1e-9 && y.abs() < 1e-9); + let grip = app.tabs[0].active_grip.as_ref().expect("grip still hot"); + assert_eq!(grip.last_world, grip.origin_world); + }); + } + + #[test] + fn grip_menu_copy_leaves_original_and_adds_copy() { + with_stack(|| { + let (mut app, handle) = grip_app(); + right_click(&mut app); + let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::CopyToggle))); + assert!(app.tabs[0].grip_copy); + move_cursor_to(&mut app, DVec3::new(3.0, 4.0, 0.0)); + let _ = app.update(Message::CommandFinalize); + let lines: Vec<(f64, f64)> = app.tabs[0] + .scene + .document + .entities() + .filter_map(|e| match e { + acadrust::EntityType::Line(l) => Some((l.start.x, l.start.y)), + _ => None, + }) + .collect(); + assert_eq!(lines.len(), 2, "{lines:?}"); + let (x, y) = line_start(&app, handle); + assert!(x.abs() < 1e-9 && y.abs() < 1e-9, "original untouched"); + assert!(lines.iter().any(|&(x, y)| (x - 3.0).abs() < 1e-3 && (y - 4.0).abs() < 1e-3)); + assert!(app.tabs[0].active_grip.is_some(), "grip re-armed for the next copy"); + // One undo step removes the copy again. + app.undo_steps(1); + let count = app.tabs[0] + .scene + .document + .entities() + .filter(|e| matches!(e, acadrust::EntityType::Line(_))) + .count(); + assert_eq!(count, 1); + }); + } + + #[test] + fn grip_menu_base_point_rebases_without_placing() { + with_stack(|| { + let (mut app, handle) = grip_app(); + right_click(&mut app); + let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::BasePoint))); + assert!(app.tabs[0].grip_base_pending); + move_cursor_to(&mut app, DVec3::new(1.0, 1.0, 0.0)); + let _ = app.update(Message::ViewportLeftPress); + let _ = app.update(Message::ViewportLeftRelease); + let grip = app.tabs[0].active_grip.as_ref().expect("grip still hot"); + assert_eq!(grip.origin_world, grip.last_world); + assert!(!app.tabs[0].grip_base_pending); + let (x, y) = line_start(&app, handle); + assert!((x - 1.0).abs() < 1e-3 && (y - 1.0).abs() < 1e-3); + }); + } +} + +#[cfg(test)] +mod transparent_tests { + use super::*; + use crate::command::StepInput; + use glam::DVec3; + + fn with_stack(f: impl FnOnce() + Send + 'static) { + std::thread::Builder::new() + .stack_size(32 * 1024 * 1024) + .spawn(f) + .expect("spawn test thread") + .join() + .unwrap_or_else(|e| std::panic::resume_unwind(e)); + } + + fn line_app() -> OpenCADStudio { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + app.tabs[0].scene.selection.borrow_mut().vp_size = (800.0, 600.0); + let _ = app.dispatch_command("LINE"); + let _ = app.feed_command(StepInput::Point(DVec3::ZERO)); + app + } + + fn active(app: &OpenCADStudio) -> Option<&'static str> { + app.tabs[0].active_cmd.as_ref().map(|c| c.name()) + } + + #[test] + fn transparent_pan_keeps_the_command_running() { + with_stack(|| { + let mut app = line_app(); + let _ = app.update(Message::ContextMenuPick(MenuAction::Command("'PAN".into()))); + assert!(app.tabs[0].pan_mode); + assert_eq!(active(&app), Some("LINE")); + // Esc leaves pan mode; the line is still being drawn. + let _ = app.update(Message::CommandEscape); + assert!(!app.tabs[0].pan_mode); + assert_eq!(active(&app), Some("LINE")); + }); + } + + #[test] + fn transparent_zoom_window_resumes_the_command() { + with_stack(|| { + let mut app = line_app(); + let _ = app.dispatch_command("'ZOOM W"); + assert_eq!(active(&app), Some("ZOOM WINDOW")); + assert!(app.tabs[0].transparent_resume); + let _ = app.feed_command(StepInput::Point(DVec3::new(-5.0, -5.0, 0.0))); + let _ = app.feed_command(StepInput::Point(DVec3::new(5.0, 5.0, 0.0))); + assert_eq!(active(&app), Some("LINE"), "LINE resumes after the zoom"); + assert!(!app.tabs[0].transparent_resume); + assert!(app.tabs[0].suspended_cmd.is_none()); + // The line still has its first point: the next pick draws a segment. + let _ = app.feed_command(StepInput::Point(DVec3::new(10.0, 0.0, 0.0))); + let lines = app.tabs[0] + .scene + .document + .entities() + .filter(|e| matches!(e, acadrust::EntityType::Line(_))) + .count(); + assert_eq!(lines, 1); + }); + } + + #[test] + fn zoom_prompt_handoff_and_escape_both_resume() { + with_stack(|| { + let mut app = line_app(); + let _ = app.dispatch_command("'ZOOM"); + let _ = app.feed_active_cmd("E"); // → ZOOM EXTENTS, one-shot + assert_eq!(active(&app), Some("LINE")); + let _ = app.dispatch_command("'ZOOM"); + assert_eq!(active(&app), Some("ZOOM WINDOW")); + let _ = app.update(Message::CommandEscape); + assert_eq!(active(&app), Some("LINE")); + assert!(!app.tabs[0].transparent_resume); + }); + } + + #[test] + fn new_command_drops_a_parked_one() { + with_stack(|| { + let mut app = line_app(); + let _ = app.dispatch_command("'ZOOM"); + let _ = app.dispatch_command("CIRCLE"); + assert_eq!(active(&app), Some("CIRCLE")); + assert!(app.tabs[0].suspended_cmd.is_none()); + let _ = app.update(Message::CommandEscape); + assert_eq!(active(&app), None, "no stale LINE comes back"); + }); + } + + #[test] + fn snap_override_none_is_one_shot() { + with_stack(|| { + let mut app = line_app(); + app.snapper.snap_enabled = true; + let _ = app.update(Message::ContextMenuPick(MenuAction::SnapOverrideNone)); + assert!(!app.snapper.snap_enabled); + app.snapper.clear_override(); + assert!(app.snapper.snap_enabled); + }); + } +} diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 681626d54..5d560690d 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -481,6 +481,8 @@ impl OpenCADStudio { selection_cycling: self.selection_cycling, double_click_block_refedit: self.double_click_block_refedit, double_click_block_attedit: self.double_click_block_attedit, + right_click_mode: self.right_click_mode, + right_click_hold_ms: self.right_click_hold_ms, grip_object_limit: self.grip_object_limit, ncopy_bind: self.ncopy_bind, cursor_type: self.cursor_type, @@ -561,6 +563,8 @@ impl OpenCADStudio { self.selection_cycling = s.selection_cycling; self.double_click_block_refedit = s.double_click_block_refedit; self.double_click_block_attedit = s.double_click_block_attedit; + self.right_click_mode = s.right_click_mode; + self.right_click_hold_ms = super::super::settings::clamp_right_click_hold_ms(s.right_click_hold_ms); self.grip_object_limit = s.grip_object_limit.clamp(0, 32767); self.ncopy_bind = s.ncopy_bind; self.cursor_type = s.cursor_type; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 98e77c62e..4cbb1056d 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -90,6 +90,7 @@ fn reorder_insertion_index(from: usize, to: usize, after: bool, len: usize) -> O } mod command; +mod context_menu; mod dialog; mod dynamic; mod file; @@ -301,6 +302,14 @@ impl OpenCADStudio { return Task::none(); } } + // The open right-click context menu owns the keyboard the same way: + // arrows / Enter / mnemonic letters drive it, any other key closes it + // and falls through to the command line (the behaviour of commercial solutions). + if self.context_menu_open() { + if let Some(task) = self.intercept_context_menu_key(&msg) { + return task; + } + } let task = self.update_inner(msg); self.refresh_gpu_status(); self.show_next_startup_modal(); @@ -745,6 +754,14 @@ impl OpenCADStudio { Task::none() } + Message::SnapOverrideNone => { + self.snap_override_popup = None; + self.snapper.set_override_none(); + self.command_line + .push_info(crate::t!("Snap override: None (next pick only).").as_ref()); + Task::none() + } + Message::SnapOverrideMtp => { self.snap_override_popup = None; self.tabs[self.active_tab] @@ -3536,6 +3553,10 @@ impl OpenCADStudio { return Task::none(); } let was_click = !sel.right_dragging; + // How long the button was held, for the time-sensitive mode. + let held_ms = sel + .right_press_time + .map_or(0, |t| t.elapsed().as_millis() as i32); sel.right_down = false; sel.right_press_pos = None; sel.right_press_time = None; @@ -3560,21 +3581,43 @@ impl OpenCADStudio { drop(sel); return self.update(Message::CommandFinalize); } - // A right-click (no orbit). While a command is active the first - // right-click acts as Enter (commit / close); a second - // consecutive right-click opens the context menu instead. When - // idle it always opens the menu. (Right-drag, handled above, - // always orbits.) Any other interaction — a left-click pick or a - // new command — resets the cycle so the next right-click is Enter. - if self.tabs[i].active_cmd.is_some() && !sel.right_click_entered { + // A right-click. What it does is the user's choice (Options → + // User Preferences, SHORTCUTMENU in commercial solutions): + // • Shortcut menu — always open the context menu, whose + // default row (Enter / Repeat) sits under the pointer. + // • Time-sensitive — a quick click is Enter while a command + // runs (repeat the last command when idle); a held click + // opens the menu. + // • Enter first — while a command is active the first + // right-click acts as Enter and a second consecutive one + // opens the menu; idle always opens the menu. Any other + // interaction — a left-click pick or a new command — resets + // that cycle so the next right-click is Enter again. + let has_cmd = self.tabs[i].active_cmd.is_some(); + let open_menu = match self.right_click_mode { + super::settings::RightClickMode::ShortcutMenu => true, + super::settings::RightClickMode::TimeSensitive => { + held_ms >= self.right_click_hold_ms + } + super::settings::RightClickMode::EnterFirst => { + !(has_cmd && !sel.right_click_entered) + } + }; + if !open_menu { sel.right_click_entered = true; drop(sel); + // CommandFinalize is Enter during a command and "repeat + // the last command" when idle — exactly the quick + // right-click. return self.update(Message::CommandFinalize); } sel.right_click_entered = false; - sel.context_menu = Some(click_pos); - sel.draworder_submenu = false; - Task::none() + sel.open_context_menu(click_pos); + drop(sel); + // Take the keyboard away from the command-line field so keys + // reach the menu through the global subscription; the field + // is re-focused when the menu closes. + self.unfocus_widgets() } Message::ViewportMiddlePress => self.on_viewport_middle_press(), @@ -5336,12 +5379,9 @@ impl OpenCADStudio { self.post_editor_closed(committed) } - Message::DrawOrderSubmenuToggle => { - let i = self.active_tab; - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.draworder_submenu = !sel.draworder_submenu; - Task::none() - } + Message::ContextMenuPick(action) => self.on_context_menu_pick(action), + Message::ContextMenuSubmenuToggle(id) => self.on_context_menu_submenu_toggle(id), + Message::ContextMenuNavigate(nav) => self.on_context_menu_navigate(nav), Message::DrawOrderPickRef(above) => { let i = self.active_tab; @@ -7236,6 +7276,18 @@ impl OpenCADStudio { Task::none() } + Message::RightClickModeChanged(mode) => { + self.right_click_mode = mode; + self.persist_settings_if_changed(); + Task::none() + } + + Message::RightClickHoldMsChanged(ms) => { + self.right_click_hold_ms = super::settings::clamp_right_click_hold_ms(ms); + self.persist_settings_if_changed(); + Task::none() + } + Message::TextEditModeChanged(single) => { self.texteditmode = single; self.persist_settings_if_changed(); diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index a5477488b..d508a0537 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -3451,102 +3451,21 @@ impl OpenCADStudio { let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.clear_left_selection_gesture(); } + // Grip-menu Base Point: this click re-bases the gesture at the + // cursor instead of placing anything. + if self.tabs[i].grip_base_pending { + self.tabs[i].grip_base_pending = false; + if let Some(active) = self.tabs[i].active_grip.as_mut() { + active.origin_world = active.last_world; + } + return Task::none(); + } let moved = grip.last_world != grip.origin_world; if is_click && !moved { // Engaging click — stay hot, wait for the placement click. return Task::none(); } - if matches!( - grip.mode, - GripEditMode::Lengthen - | GripEditMode::Radius - | GripEditMode::ArcLength - | GripEditMode::RectangleWidth - | GripEditMode::RectangleHeight - | GripEditMode::MoveParallel - ) { - self.grip_pending = None; - self.command_line.input.clear(); - } - let added_vertex_focus = grip.targets.iter().find_map(|target| { - let original = self - .grip_originals - .iter() - .find(|(handle, _)| *handle == target.handle) - .map(|(_, entity)| entity)?; - let current = self.tabs[i].scene.document.get_entity(target.handle)?; - is_added_polyline_vertex(original, current, target.grip_id) - .then_some(target.grip_id) - }); - // Keep originals available until every history shape has a valid - // final display. A rejected rebuild cancels the entire gesture. - let history_handles: Vec<_> = self - .grip_preview_handles - .iter() - .copied() - .filter(|handle| { - self.tabs[i] - .scene - .document - .solid_history_operation(*handle) - .is_some() - }) - .collect(); - for handle in history_handles { - if !self.tabs[i].scene.finalize_solid_history(handle) { - self.cancel_active_grip_edit(); - self.command_line.push_error(crate::t!("The edited shape could not be displayed; the original geometry was restored.").as_ref()); - self.refresh_properties(); - return Task::none(); - } - } - self.tabs[i].active_grip = None; - // Commit the grip drag as one undoable group, then put every - // edited entity back into the resident tessellation. - let handles = std::mem::take(&mut self.grip_preview_handles); - let originals = std::mem::take(&mut self.grip_originals); - let history_originals = std::mem::take(&mut self.grip_history_originals); - let dirty_before = self.grip_dirty_before.take().unwrap_or(self.tabs[i].dirty); - if !handles.is_empty() { - if !originals.is_empty() { - self.push_entity_group_history( - i, - "GRIP", - originals - .into_iter() - .map(|(handle, entity)| (handle, std::sync::Arc::new(entity))) - .collect(), - history_originals - .into_iter() - .flat_map(|(_, objects)| objects) - .collect(), - dirty_before, - ); - self.tabs[i].dirty = true; - } - self.grip_reference_wires.clear(); - self.grip_text_verts = Vec::new(); - self.grip_text_slide = false; - for &handle in &handles { - self.tabs[i].scene.preview_hidden.remove(&handle); - } - self.tabs[i].scene.clear_preview_wire(); - let changes: Vec<_> = handles - .into_iter() - .map(|handle| (handle, crate::scene::ChangeKind::Modified)) - .collect(); - self.tabs[i].scene.bump_entities_after_parametric_solve(&changes); - } - // Placement confirmed — keep the just-added leader. - self.grip_add_provisional = None; - self.tabs[i].snap_result = None; - if let Some(vertex_id) = added_vertex_focus { - self.tabs[i].properties.prop_vertex = vertex_id; - self.tabs[i].properties.prop_vertex_indicator_active = true; - crate::scene::view::dispatch::set_prop_current_vertex(vertex_id); - } - self.refresh_properties(); - return Task::none(); + return self.commit_active_grip_edit(); } // Map the release point into the active Model tile so the @@ -6219,6 +6138,164 @@ properties={:.1}ms picked={}", } Task::none() } + + /// Commit the active grip edit at its current (already applied) position: + /// one undoable group per gesture, every edited entity back into the + /// resident tessellation. With the grip-menu Copy toggle on, the + /// originals are put back and the moved shapes are added as new + /// entities, and the grip stays hot for the next copy. + pub(in crate::app) fn commit_active_grip_edit(&mut self) -> Task { + let i = self.active_tab; + let Some(grip) = self.tabs[i].active_grip.clone() else { + return Task::none(); + }; + if grip.last_world == grip.origin_world { + // Nothing placed yet — the grip simply stays hot. + return Task::none(); + } + if self.tabs[i].grip_copy { + return self.commit_active_grip_edit_as_copy(grip); + } + self.tabs[i].grip_base_pending = false; + if matches!( + grip.mode, + GripEditMode::Lengthen + | GripEditMode::Radius + | GripEditMode::ArcLength + | GripEditMode::RectangleWidth + | GripEditMode::RectangleHeight + | GripEditMode::MoveParallel + ) { + self.grip_pending = None; + self.command_line.input.clear(); + } + let added_vertex_focus = grip.targets.iter().find_map(|target| { + let original = self + .grip_originals + .iter() + .find(|(handle, _)| *handle == target.handle) + .map(|(_, entity)| entity)?; + let current = self.tabs[i].scene.document.get_entity(target.handle)?; + is_added_polyline_vertex(original, current, target.grip_id) + .then_some(target.grip_id) + }); + // Keep originals available until every history shape has a valid + // final display. A rejected rebuild cancels the entire gesture. + let history_handles: Vec<_> = self + .grip_preview_handles + .iter() + .copied() + .filter(|handle| { + self.tabs[i] + .scene + .document + .solid_history_operation(*handle) + .is_some() + }) + .collect(); + for handle in history_handles { + if !self.tabs[i].scene.finalize_solid_history(handle) { + self.cancel_active_grip_edit(); + self.command_line.push_error(crate::t!("The edited shape could not be displayed; the original geometry was restored.").as_ref()); + self.refresh_properties(); + return Task::none(); + } + } + self.tabs[i].active_grip = None; + // Commit the grip drag as one undoable group, then put every + // edited entity back into the resident tessellation. + let handles = std::mem::take(&mut self.grip_preview_handles); + let originals = std::mem::take(&mut self.grip_originals); + let history_originals = std::mem::take(&mut self.grip_history_originals); + let dirty_before = self.grip_dirty_before.take().unwrap_or(self.tabs[i].dirty); + if !handles.is_empty() { + if !originals.is_empty() { + self.push_entity_group_history( + i, + "GRIP", + originals + .into_iter() + .map(|(handle, entity)| (handle, std::sync::Arc::new(entity))) + .collect(), + history_originals + .into_iter() + .flat_map(|(_, objects)| objects) + .collect(), + dirty_before, + ); + self.tabs[i].dirty = true; + } + self.grip_reference_wires.clear(); + self.grip_text_verts = Vec::new(); + self.grip_text_slide = false; + for &handle in &handles { + self.tabs[i].scene.preview_hidden.remove(&handle); + } + self.tabs[i].scene.clear_preview_wire(); + let changes: Vec<_> = handles + .into_iter() + .map(|handle| (handle, crate::scene::ChangeKind::Modified)) + .collect(); + self.tabs[i].scene.bump_entities_after_parametric_solve(&changes); + } + // Placement confirmed — keep the just-added leader. + self.grip_add_provisional = None; + self.tabs[i].snap_result = None; + if let Some(vertex_id) = added_vertex_focus { + self.tabs[i].properties.prop_vertex = vertex_id; + self.tabs[i].properties.prop_vertex_indicator_active = true; + crate::scene::view::dispatch::set_prop_current_vertex(vertex_id); + } + self.refresh_properties(); + Task::none() + } + + /// Grip-menu Copy: leave the originals where they were and add the + /// edited shapes as new entities, then re-arm the same grip at its + /// origin so the next placement makes another copy (grip Copy, as in commercial solutions). + fn commit_active_grip_edit_as_copy(&mut self, grip: GripEdit) -> Task { + let i = self.active_tab; + let handles = std::mem::take(&mut self.grip_preview_handles); + let originals = std::mem::take(&mut self.grip_originals); + let _ = std::mem::take(&mut self.grip_history_originals); + self.grip_dirty_before = None; + self.grip_add_provisional = None; + self.grip_reference_wires.clear(); + self.grip_text_verts = Vec::new(); + self.grip_text_slide = false; + for &handle in &handles { + self.tabs[i].scene.preview_hidden.remove(&handle); + } + self.tabs[i].scene.clear_preview_wire(); + if !originals.is_empty() { + self.push_undo_snapshot(i, "GRIP"); + let mut restored = Vec::with_capacity(originals.len()); + for (handle, original) in &originals { + let Some(moved) = self.tabs[i].scene.document.get_entity(*handle).cloned() else { + continue; + }; + if self.tabs[i].scene.update_entity(original.clone()) { + restored.push((*handle, crate::scene::ChangeKind::Modified)); + } + self.tabs[i].scene.add_entity_clone(moved); + } + self.tabs[i].scene.bump_entities_after_parametric_solve(&restored); + self.finish_pending_history(i); + self.tabs[i].dirty = true; + } + self.tabs[i].snap_result = None; + // Re-arm: the grip sits at its origin again over the restored shape. + let mut again = grip; + let delta = again.last_world - again.origin_world; + again.last_world = again.origin_world; + for target in &mut again.targets { + target.last_world -= delta; + } + self.tabs[i].active_grip = Some(again); + self.refresh_properties(); + Task::none() + } + } #[cfg(test)] diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index c241e3aac..8ca29a3a2 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1617,7 +1617,7 @@ bg={bg_ms:.1}ms n={view_count}" crate::ui::command_line::history_max_height(self.win_size.1), ) + 72.0 } else { - 34.0 + 34.0 + self.command_line.overlay_lines_height() }; // Quick Properties: stay near the selection cursor, flipping around // it as needed to remain inside the visible drawing area. @@ -1766,37 +1766,17 @@ bg={bg_ms:.1}ms n={view_count}" // the cursor position (canvas-relative) anchors the menu under // the cursor instead of drifting into window-relative space. if !tab.is_start { - let (ctx_pos, draworder_open) = { + let (ctx_pos, highlighted) = { let sel = tab.scene.selection.borrow(); - (sel.context_menu, sel.draworder_submenu) + (sel.context_menu, sel.context_menu_ui.highlighted) }; if let Some(p) = ctx_pos { - let has_cmd = tab.active_cmd.is_some(); - // Same guard as typed MTP/M2P and SnapOverrideMtp. - let has_point_step = tab.active_cmd.as_ref().is_some_and(|c| { - (!c.input_kind().wants_text() || c.point_step_accepts_keywords()) - && !c.needs_entity_pick() - }); - let has_selection = !tab.scene.selected.is_empty(); - let isolation_active = tab.scene.is_isolation_active(); - let last_cmds: Vec = self - .command_line - .recent_commands - .iter() - .rev() - .take(3) - .cloned() - .collect(); + let menu = self.current_context_menu(); viewport_stack = viewport_stack.push(viewport_context_menu_overlay( p, command_line_inset, - has_cmd, - has_selection, - tab.scene.selected_constraint, - isolation_active, - last_cmds, - draworder_open, - has_point_step, + &menu, + highlighted, )); } } diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 157334d76..04fe270ec 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -297,6 +297,8 @@ impl OpenCADStudio { show_viewcube: self.show_viewcube, show_ucs_icon: self.show_ucs_icon, ucs_icon_at_origin: self.ucs_icon_at_origin, + right_click_mode: self.right_click_mode, + right_click_hold_ms: self.right_click_hold_ms, }, crate::ui::window::options::spacemouse::view( self.spacemouse_preferences, self.spacemouse.status(), diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index 77c57b46c..413dfd4e9 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -11,6 +11,7 @@ use iced::widget::{ }; use iced::{Background, Border, Color, Element, Event, Fill, Length, Rectangle, Size, Theme, Vector}; use crate::t; +use crate::ui::popup::context_menu::{ContextMenu, MenuIcon, MenuItem, MenuRow, MENU_PAD_TOP, MENU_ROW_H}; pub(super) fn position_canvas_overlay<'a>( anchor: iced::Point, @@ -912,9 +913,15 @@ fn mtext_editor_content<'a>( /// panel is laid out first, so edge flipping uses its actual translated size /// instead of estimates. `bottom_inset` reserves overlaid controls such as the /// command line. +/// +/// `offset` shifts the preferred placement from the anchor before the fit / +/// flip logic runs (e.g. to put a specific row of a menu under the pointer). +/// When the shifted panel does not fit it flips to the other side of the +/// *original* anchor, so the pointer never ends up over an unintended row. fn position_canvas_overlay_clamped<'a>( anchor: iced::Point, bottom_inset: f32, + offset: Vector, panel: Element<'a, Message>, ) -> Element<'a, Message> { Element::new(ClampedPin { @@ -922,6 +929,7 @@ fn position_canvas_overlay_clamped<'a>( anchor, bottom_inset: bottom_inset.max(0.0), gap: 0.0, + offset, }) } @@ -937,6 +945,7 @@ pub(super) fn position_canvas_overlay_near_cursor<'a>( anchor: cursor, bottom_inset: bottom_inset.max(0.0), gap: 12.0, + offset: Vector::ZERO, }) } @@ -945,6 +954,8 @@ struct ClampedPin<'a> { anchor: iced::Point, bottom_inset: f32, gap: f32, + /// Preferred placement shift from the anchor (applied before fitting). + offset: Vector, } impl Widget for ClampedPin<'_> { @@ -983,9 +994,10 @@ impl Widget for ClampedPin<'_> { &layout::Limits::new(Size::ZERO, safe), ); let content = node.size(); + let bottom_inset = self.bottom_inset; let max_x = (max.width - MARGIN - content.width).max(MARGIN); - let right = self.anchor.x + self.gap; + let right = self.anchor.x + self.gap + self.offset.x; let left = self.anchor.x - self.gap - content.width; let x = if right <= max_x { right.max(MARGIN) @@ -995,9 +1007,8 @@ impl Widget for ClampedPin<'_> { max_x }; - let max_y = - (max.height - self.bottom_inset - MARGIN - content.height).max(MARGIN); - let below = self.anchor.y + self.gap; + let max_y = (max.height - bottom_inset - MARGIN - content.height).max(MARGIN); + let below = self.anchor.y + self.gap + self.offset.y; let above = self.anchor.y - self.gap - content.height; let y = if below <= max_y { below.max(MARGIN) @@ -1109,26 +1120,44 @@ impl Widget for ClampedPin<'_> { // ── Viewport right-click context menu ────────────────────────────────────── +/// Horizontal inset so the pointer rests inside the default row, not on the +/// panel's border, when the menu opens. +const MENU_CURSOR_INSET_X: f32 = 24.0; +/// Width of the icon gutter every row reserves so labels line up whether or +/// not the row carries a glyph (object snaps, Pan / Zoom). +const MENU_GUTTER_W: f32 = 18.0; +const MENU_ICON_SIZE: f32 = 14.0; + +/// The gutter cell: the row's glyph, or empty space of the same width. +fn context_menu_gutter(icon: Option) -> Element<'static, Message> { + let bytes = icon.map(|icon| match icon { + MenuIcon::Snap(t) => crate::ui::icons::osnap(t), + MenuIcon::Mtp => crate::ui::icons::mtp_icon(), + MenuIcon::Pan => crate::ui::icons::pan_icon(), + MenuIcon::Zoom => crate::ui::icons::zoom_icon(), + }); + let cell: Element<'static, Message> = match bytes { + Some(bytes) => crate::ui::icons::themed::(bytes, MENU_ICON_SIZE), + None => iced::widget::Space::new().width(MENU_ICON_SIZE).height(MENU_ICON_SIZE).into(), + }; + container(cell) + .width(Length::Fixed(MENU_GUTTER_W)) + .align_x(iced::Center) + .align_y(iced::Center) + .into() +} + +/// Render the right-click context menu (rows from +/// `ui::popup::context_menu::build_context_menu`). The panel is placed so the +/// default row sits under the pointer: right-click then left-click in place +/// completes the step without moving the mouse (commercial solutions anchor their shortcut +/// menu the same way). `highlighted` is the keyboard highlight, if any. pub(super) fn viewport_context_menu_overlay( pos: iced::Point, bottom_inset: f32, - has_cmd: bool, - has_selection: bool, - selected_constraint: Option, - isolation_active: bool, - last_cmds: Vec, - draworder_open: bool, - has_point_step: bool, + menu: &ContextMenu, + highlighted: Option, ) -> Element<'static, Message> { - let item = |label: String, msg: Message| -> Element<'static, Message> { - button(text(label).size(12)) - .on_press(msg) - .style(button::subtle) - .padding([4, 12]) - .width(Fill) - .into() - }; - let sep = || -> Element<'static, Message> { container(iced::widget::Space::new().width(Fill).height(1)) .style(|theme: &Theme| container::Style { @@ -1143,142 +1172,67 @@ pub(super) fn viewport_context_menu_overlay( .into() }; - // Indented variant for sub-menu rows (e.g. Draw Order children). - let subitem = |label: String, msg: Message| -> Element<'static, Message> { - button(text(label).size(12)) - .on_press(msg) - .style(button::subtle) - .padding(iced::Padding { - top: 4.0, - right: 12.0, - bottom: 4.0, - left: 26.0, - }) - .width(Fill) - .into() - }; - + // Index into `menu.selectable()` of the row being rendered, so the + // keyboard highlight lands on the same row the model counts. + let mut sel_idx = 0usize; let mut items: Vec> = Vec::new(); - if has_cmd { - items.push(item(t!("Cancel").into_owned(), Message::CommandEscape)); - items.push(item(t!("Enter").into_owned(), Message::CommandFinalize)); - // MTP (`_M2P`) goes last, after Parallel: two picks, so not a - // `SnapType`, but same icon-only cell with hover tooltip. - if has_point_step { - items.push(sep()); - items.push(item( - t!("Mid Between 2 Points (M2P)").into_owned(), - Message::SnapOverrideMtp, - )); - } - } else { - if !last_cmds.is_empty() { - let last = last_cmds[0].clone(); - items.push(item( - t!("Repeat %{last}", last = last).into_owned(), - Message::Command(last.to_uppercase()), - )); - if last_cmds.len() > 1 { - for cmd in last_cmds.iter().skip(1) { - let c = cmd.clone(); - items.push(item(c.clone(), Message::Command(c.to_uppercase()))); + for row in &menu.rows { + match row { + MenuRow::Item(item) => { + let is_hl = highlighted == Some(sel_idx); + items.push(context_menu_row(item, 0.0, is_hl)); + sel_idx += 1; + } + MenuRow::Separator => items.push(sep()), + MenuRow::Submenu { + id, + label, + items: children, + open, + } => { + let is_hl = highlighted == Some(sel_idx); + let caret = if *open { + crate::ui::icons::themed_arrow_down(9.0) + } else { + crate::ui::icons::themed_arrow_right(9.0) + }; + let content = row![ + context_menu_gutter(None), + text(label.clone()).size(12), + iced::widget::Space::new().width(Fill), + caret, + ] + .spacing(4) + .align_y(iced::Center); + let enabled = !children.is_empty(); + let mut btn = button(content) + .padding(iced::Padding { + top: 3.0, + right: 12.0, + bottom: 3.0, + left: 6.0, + }) + .width(Fill) + .height(Length::Fixed(MENU_ROW_H)) + .style(move |theme: &Theme, status| context_menu_row_style(theme, status, is_hl)); + if enabled { + btn = btn.on_press(Message::ContextMenuSubmenuToggle(*id)); + } + items.push(btn.into()); + sel_idx += 1; + if *open { + for child in children { + let is_hl = highlighted == Some(sel_idx); + items.push(context_menu_row(child, 14.0, is_hl)); + sel_idx += 1; + } } } - items.push(sep()); } - if has_selection { - items.push(item(t!("Delete").into_owned(), Message::DeleteSelected)); - items.push(item( - t!("Move").into_owned(), - Message::Command("MOVE".to_string()), - )); - items.push(item( - t!("Copy").into_owned(), - Message::Command("COPY".to_string()), - )); - items.push(sep()); - let do_caret = if draworder_open { - crate::ui::icons::themed_arrow_down(9.0) - } else { - crate::ui::icons::themed_arrow_right(9.0) - }; - items.push( - button( - row![ - text(t!("Draw Order").into_owned()).size(12), - iced::widget::Space::new().width(Fill), - do_caret, - ] - .align_y(iced::Center), - ) - .on_press(Message::DrawOrderSubmenuToggle) - .style(button::subtle) - .padding([4, 12]) - .width(Fill) - .into(), - ); - if draworder_open { - items.push(subitem( - t!("Bring to Front").into_owned(), - Message::Command("DRAWORDER F".to_string()), - )); - items.push(subitem( - t!("Send to Back").into_owned(), - Message::Command("DRAWORDER B".to_string()), - )); - items.push(subitem( - t!("Bring Above Object").into_owned(), - Message::DrawOrderPickRef(true), - )); - items.push(subitem( - t!("Send Under Object").into_owned(), - Message::DrawOrderPickRef(false), - )); - } - items.push(sep()); - items.push(item( - t!("Isolate Objects").into_owned(), - Message::Command("ISOLATEOBJECTS".to_string()), - )); - items.push(item( - t!("Hide Objects").into_owned(), - Message::Command("HIDEOBJECTS".to_string()), - )); - items.push(sep()); - items.push(item(t!("Select Similar").into_owned(), Message::SelectSimilar)); - items.push(item( - t!("Invert Selection").into_owned(), - Message::InvertSelection, - )); - } else if let Some(id) = selected_constraint { - // A selected constraint-glyph pill gets its own minimal menu — - // none of the entity-selection actions above (Move/Copy/Isolate/ - // Select Similar) apply to it. - items.push(item( - t!("Delete").into_owned(), - Message::PropConstraintDelete(id), - )); - items.push(sep()); - } - if isolation_active { - items.push(item( - t!("End Object Isolation").into_owned(), - Message::Command("UNISOLATEOBJECTS".to_string()), - )); - } - items.push(item( - t!("Select All").into_owned(), - Message::Command("SELECTALL".to_string()), - )); - items.push(item(t!("Quick Select...").into_owned(), Message::QSelectOpen)); - items.push(item( - t!("Zoom Extents").into_owned(), - Message::Command("ZOOM EXTENTS".to_string()), - )); } - let menu_col = column(items).spacing(0).width(Length::Fixed(180.0)); + let menu_col = column(items).spacing(0).width(Length::Fixed(menu.width)); let menu_col = scrollable(menu_col) .height(Length::Shrink) .direction(scrollable::Direction::Vertical( @@ -1287,12 +1241,103 @@ pub(super) fn viewport_context_menu_overlay( .scroller_width(6), )); - let menu = container(menu_col) + let panel = container(menu_col) .style(container::bordered_box) - .padding([4, 0]) - .width(Length::Fixed(180.0)); + .padding([MENU_PAD_TOP as u16, 0]) + .width(Length::Fixed(menu.width)); - position_canvas_overlay_clamped(pos, bottom_inset, menu.into()) + // Shift the panel so the default row's centre line is under the pointer. + let offset = Vector::new( + -MENU_CURSOR_INSET_X, + -(menu.default_row_y() + MENU_ROW_H * 0.5), + ); + position_canvas_overlay_clamped(pos, bottom_inset, offset, panel.into()) +} + +/// One menu row: label (bold for the default row, "✓ "-prefixed when +/// checked), and the keyword hint right-aligned and dimmed. Disabled rows +/// render without a press handler and with faded text. +fn context_menu_row(item: &MenuItem, indent: f32, highlighted: bool) -> Element<'static, Message> { + let label = if item.checked { + format!("✓ {}", item.label) + } else { + item.label.clone() + }; + let mut label_text = text(label).size(12); + if item.default { + label_text = label_text.font(iced::Font { + weight: iced::font::Weight::Bold, + ..iced::Font::DEFAULT + }); + } + let enabled = item.enabled; + let mut content = row![context_menu_gutter(item.icon), label_text] + .spacing(4) + .align_y(iced::Center); + if let Some(hint) = item.hint.as_ref() { + content = content + .push(iced::widget::Space::new().width(Fill)) + .push( + text(hint.clone()) + .size(11) + .style(move |theme: &Theme| { + let palette = theme.palette(); + let base = if highlighted { + palette.primary.strong.text + } else { + palette.background.base.text + }; + iced::widget::text::Style { + color: Some(base.scale_alpha(if enabled { 0.6 } else { 0.35 })), + } + }), + ); + } + let mut btn = button(content) + .padding(iced::Padding { + top: 3.0, + right: 12.0, + bottom: 3.0, + left: 6.0 + indent, + }) + .width(Fill) + .height(Length::Fixed(MENU_ROW_H)) + .style(move |theme: &Theme, status| context_menu_row_style(theme, status, highlighted)); + if enabled { + btn = btn.on_press(Message::ContextMenuPick(item.action.clone())); + } + btn.into() +} + +/// Row colours: keyboard highlight uses the accent (as the grip popup does), +/// hover the subtle background tint, disabled rows faded text. +fn context_menu_row_style(theme: &Theme, status: button::Status, highlighted: bool) -> button::Style { + let palette = theme.palette(); + let (background, text_color) = if highlighted { + ( + Some(Background::Color(palette.primary.strong.color)), + palette.primary.strong.text, + ) + } else { + match status { + button::Status::Hovered | button::Status::Pressed => ( + Some(Background::Color(palette.background.weak.color)), + palette.background.weak.text, + ), + button::Status::Disabled => (None, palette.background.base.text.scale_alpha(0.42)), + button::Status::Active => (None, palette.background.base.text), + } + }; + button::Style { + background, + text_color, + border: Border { + color: Color::TRANSPARENT, + width: 0.0, + radius: 0.0.into(), + }, + ..Default::default() + } } /// One-shot snap override menu (Shift+RMB, #337): a cursor-anchored grid of diff --git a/src/command.rs b/src/command.rs index a39f6a171..1704fc0ea 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2667,6 +2667,12 @@ pub trait CadCommand: Send { /// Default: no-op. fn inject_picked_entity(&mut self, _entity: acadrust::EntityType) {} + /// The host undid one document step on the command's behalf + /// (`CmdResult::UndoDocument`) and the command stays active. Commands + /// that cache document entities (FILLET, CHAMFER) refresh them here so + /// the next pick sees the restored geometry. Default: no-op. + fn on_document_undone(&mut self, _document: &acadrust::CadDocument) {} + /// Supply the tessellated surface area associated with the picked entity. /// Commands that measure mesh-backed objects can opt in without owning the /// scene's render cache. diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index ca31119d5..17536c4f7 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -4953,7 +4953,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "SECTIONPLANE Specify through point:" => Some(("common", "sectionplane-specify-through-point")), "SETBYLAYER: no properties required changes." => Some(("common", "setbylayer-no-properties-required-changes")), "SETBYLAYER: reset {} entity/entities to ByLayer." => Some(("common", "setbylayer-reset-entity-entities-to-bylayer")), - "SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)" => Some(("common", "setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only")), + "SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)" => Some(("common", "setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only")), "SHELL supports rectangular boxes, circular cylinders, and spheres." => Some(("common", "shell-supports-rectangular-boxes-circular-cylinders-and-spheres")), "SHELL: solid updated." => Some(("common", "shell-solid-updated")), "SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] :" => Some(("common", "splinedit-fit-data-add-delete-move-purge-tangents-exit-exit")), @@ -5093,6 +5093,61 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "{} row(s) skipped: duplicate parameter name." => Some(("common", "row-s-skipped-duplicate-parameter-name")), "{} surface(s) cannot be thickened because the offset intersects itself." => Some(("common", "surface-s-cannot-be-thickened-because-the-offset-intersects-itself")), "⚠ {parametric_conflicts} conflicting" => Some(("common", "conflicting")), + "PLINE Specify starting width <%{w}>:" => Some(("draw", "pline-specify-starting-width")), + "PLINE Specify ending width <%{w}>:" => Some(("draw", "pline-specify-ending-width")), + "PLINE Specify starting half-width <%{w}>:" => Some(("draw", "pline-specify-starting-half-width")), + "PLINE Specify ending half-width <%{w}>:" => Some(("draw", "pline-specify-ending-half-width")), + "PLINE (Line) Specify next point:" => Some(("draw", "pline-line-specify-next-point")), + "PLINE (Arc) Specify endpoint of arc:" => Some(("draw", "pline-arc-specify-endpoint-of-arc")), + "PLINE Specify length of line:" => Some(("draw", "pline-specify-length-of-line")), + "PLINE Specify included angle:" => Some(("draw", "pline-specify-included-angle")), + "PLINE Specify endpoint of arc:" => Some(("draw", "pline-specify-endpoint-of-arc")), + "PLINE Specify center point of arc:" => Some(("draw", "pline-specify-center-point-of-arc")), + "PLINE Specify radius of arc:" => Some(("draw", "pline-specify-radius-of-arc")), + "PLINE Specify direction of chord for arc:" => Some(("draw", "pline-specify-direction-of-chord-for-arc")), + "PLINE Specify length of chord:" => Some(("draw", "pline-specify-length-of-chord")), + "PLINE Specify tangent direction for the start point of arc:" => Some(("draw", "pline-specify-tangent-direction-for-the-start-point-of-arc")), + "PLINE Specify second point on arc:" => Some(("draw", "pline-specify-second-point-on-arc")), + "Halfwidth" => Some(("draw", "halfwidth")), + "CEnter" => Some(("draw", "center-keyword")), + "CLose" => Some(("draw", "close-keyword")), + "Second pt" => Some(("draw", "second-pt")), + "ARea" => Some(("modify", "area-keyword")), + "Displacement" => Some(("modify", "displacement")), + "mOde" => Some(("modify", "mode-keyword")), + "Array" => Some(("modify", "array-keyword")), + "Use first point as displacement" => Some(("modify", "use-first-point-as-displacement")), + "OFFSET Erase source object after offsetting? [Yes/No] :" => Some(("modify", "offset-erase-source-object-after-offsetting")), + "OFFSET Enter layer option for offset objects [Current/Source] :" => Some(("modify", "offset-enter-layer-option-for-offset-objects")), + "Recent Input" => Some(("ui", "recent-input")), + "Copy with Base Point" => Some(("ui", "copy-with-base-point")), + "Undo %{label}" => Some(("ui", "undo-label")), + "Redo %{label}" => Some(("ui", "redo-label")), + "Copy Selection" => Some(("ui", "copy-selection")), + "Isolate" => Some(("ui", "isolate")), + "Deselect All" => Some(("ui", "deselect-all")), + "Options..." => Some(("ui", "options-ellipsis")), + "Base Point" => Some(("ui", "base-point")), + "Right-click Customization" => Some(("options", "right-click-customization")), + "Right-click in drawing area" => Some(("options", "right-click-in-drawing-area")), + "Hold duration" => Some(("options", "hold-duration")), + "Shortcut menu: right-click always opens the menu. Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION)." => Some(("options", "right-click-mode-help")), + "Shortcut menu" => Some(("options", "right-click-mode-shortcut-menu")), + "Time-sensitive (quick click = Enter)" => Some(("options", "right-click-mode-time-sensitive")), + "Enter first, second click = menu" => Some(("options", "right-click-mode-enter-first")), + "ARC Specify start point of arc:" => Some(("draw", "arc-specify-start-point-of-arc")), + "ARC Specify second point of arc:" => Some(("draw", "arc-specify-second-point-of-arc")), + "ARC Specify end point of arc:" => Some(("draw", "arc-specify-end-point-of-arc")), + "ARC Specify center point of arc:" => Some(("draw", "arc-specify-center-point-of-arc")), + "ARC Specify included angle:" => Some(("draw", "arc-specify-included-angle")), + "ARC Specify length of chord:" => Some(("draw", "arc-specify-length-of-chord")), + "ARC Specify tangent direction for the start point of arc:" => Some(("draw", "arc-specify-tangent-direction-for-the-start-point-of-arc")), + "ARC Specify radius of arc:" => Some(("draw", "arc-specify-radius-of-arc")), + "chord Length" => Some(("draw", "chord-length")), + "Snap Overrides" => Some(("ui", "snap-overrides")), + "Mid Between 2 Points" => Some(("ui", "mid-between-2-points")), + "Osnap Settings..." => Some(("ui", "osnap-settings")), + "Snap override: None (next pick only)." => Some(("ui", "snap-override-none-next-pick-only")), _ => None, } } diff --git a/src/modules/draw/defaults.rs b/src/modules/draw/defaults.rs index 5331ac714..5666c0013 100644 --- a/src/modules/draw/defaults.rs +++ b/src/modules/draw/defaults.rs @@ -21,6 +21,7 @@ thread_local! { static RECT_ELEVATION: Cell = Cell::new(0.0); static RECT_THICKNESS: Cell = Cell::new(0.0); static RECT_WIDTH: Cell = Cell::new(0.0); + static PLINE_WIDTH: Cell = Cell::new(0.0); // PLINEWID static RECT_ROTATION: Cell = Cell::new(0.0); // degrees static RECT_CHAMFER1: Cell = Cell::new(0.0); static RECT_CHAMFER2: Cell = Cell::new(0.0); @@ -69,6 +70,7 @@ accessors!(get_polygon_sides, set_polygon_sides, POLYGON_SIDES); accessors!(get_rect_elevation, set_rect_elevation, RECT_ELEVATION); accessors!(get_rect_thickness, set_rect_thickness, RECT_THICKNESS); accessors!(get_rect_width, set_rect_width, RECT_WIDTH); +accessors!(get_pline_width, set_pline_width, PLINE_WIDTH); accessors!(get_rect_rotation, set_rect_rotation, RECT_ROTATION); accessors!(get_rect_chamfer1, set_rect_chamfer1, RECT_CHAMFER1); accessors!(get_rect_chamfer2, set_rect_chamfer2, RECT_CHAMFER2); diff --git a/src/modules/draw/draw/arc.rs b/src/modules/draw/draw/arc.rs index ecf09bdba..df06aa0e0 100644 --- a/src/modules/draw/draw/arc.rs +++ b/src/modules/draw/draw/arc.rs @@ -274,7 +274,7 @@ fn end_angle_from_chord_len(start_angle: f64, chord: f64, r: f64) -> Option // ── Command 1: Center, Start, End ───────────────────────────────────────── -pub struct ArcCommand { +pub struct ArcCSECommand { step: u8, c: DVec3, r: f64, @@ -283,7 +283,7 @@ pub struct ArcCommand { plane: WorkingPlane, } -impl ArcCommand { +impl ArcCSECommand { pub fn new() -> Self { Self { step: 0, @@ -296,7 +296,7 @@ impl ArcCommand { } } -impl CadCommand for ArcCommand { +impl CadCommand for ArcCSECommand { fn set_working_plane(&mut self, plane: WorkingPlane) { self.plane = plane; } @@ -411,6 +411,443 @@ impl CadCommand for ArcCommand { } } +// ── ARC: compatible prompt flow ─────────────────────────────────────────── +// +// The plain ARC command follows the prompts of commercial solutions, so every keyword a user +// knows is there at the same step: +// Specify start point of arc or [Center] +// Specify second point of arc or [Center/End] +// Specify end point of arc +// … after Center: Specify end point of arc or [Angle/chord Length] +// … after End: Specify center point of arc or [Angle/Direction/Radius] +// Enter at the start prompt continues tangent from the last line/arc. +// Holding Ctrl flips the sweep direction where it is not fixed by the picks. + +#[derive(Clone, Copy, Debug, PartialEq)] +enum ArcStep { + Start, + Second { s: DVec3 }, + End3 { s: DVec3, m: DVec3 }, + CenterFirst, + StartAfterCenter { c: DVec3 }, + CenterAfterStart { s: DVec3 }, + /// Start and centre known: end point [Angle / chord Length]. + EndSC { s: DVec3, c: DVec3 }, + AngleSC { s: DVec3, c: DVec3 }, + LengthSC { s: DVec3, c: DVec3 }, + EndAfterStart { s: DVec3 }, + /// Start and end known: centre point [Angle / Direction / Radius]. + CenterSE { s: DVec3, e: DVec3 }, + AngleSE { s: DVec3, e: DVec3 }, + DirectionSE { s: DVec3, e: DVec3 }, + RadiusSE { s: DVec3, e: DVec3 }, +} + +pub struct ArcCommand { + step: ArcStep, + cw: bool, + plane: WorkingPlane, +} + +impl ArcCommand { + pub fn new() -> Self { + Self { + step: ArcStep::Start, + cw: false, + plane: WorkingPlane::default(), + } + } + + /// Arc about `c` from `s` towards the direction of `e` (Ctrl flips). + fn sc_end(&self, s: DVec3, c: DVec3, e: DVec3) -> Option<(DVec3, f64, f64, f64)> { + let r = c.distance(s); + if r < 1e-9 { + return None; + } + let sa = angle_xy(c, s, self.plane); + let ea = angle_xy(c, e, self.plane); + Some(if self.cw { (c, r, ea, sa) } else { (c, r, sa, ea) }) + } + + /// Arc about `c` from `s` sweeping the signed `span` (radians, CCW > 0). + fn sc_angle(&self, s: DVec3, c: DVec3, span: f64) -> Option<(DVec3, f64, f64, f64)> { + let r = c.distance(s); + if r < 1e-9 || span.abs() < 1e-9 { + return None; + } + let sa = angle_xy(c, s, self.plane); + let ea = sa + span; + Some(if span < 0.0 { (c, r, ea, sa) } else { (c, r, sa, ea) }) + } + + /// Arc about `c` from `s` with chord `len` (Ctrl flips). + fn sc_length(&self, s: DVec3, c: DVec3, len: f64) -> Option<(DVec3, f64, f64, f64)> { + let r = c.distance(s); + if r < 1e-9 || len == 0.0 { + return None; + } + let sa = angle_xy(c, s, self.plane); + let ea = end_angle_from_chord_len(sa, len, r)?; + Some(if self.cw { (c, r, ea, sa) } else { (c, r, sa, ea) }) + } + + /// Sweep from `s` to the cursor about `c`, going the way Ctrl says. + fn cursor_span(&self, s: DVec3, c: DVec3, cursor: DVec3) -> f64 { + let span = (angle_xy(c, cursor, self.plane) - angle_xy(c, s, self.plane)).rem_euclid(TAU); + if self.cw { + span - TAU + } else { + span + } + } + + fn typed_number(text: &str) -> Option { + crate::entities::common::parse_typed_length(text.trim()) + } + + fn typed_angle(text: &str) -> Option { + crate::entities::common::parse_typed_angle(text.trim()) + } + + fn commit(&self, arc: Option<(DVec3, f64, f64, f64)>) -> CmdResult { + match arc { + Some((c, r, sa, ea)) => arc_result(c, r, sa, ea, self.plane), + None => CmdResult::NeedPoint, + } + } + + fn preview(&self, arc: Option<(DVec3, f64, f64, f64)>, fallback: WireModel) -> Option { + match arc { + Some((c, r, sa, ea)) => arc_preview(c, r, sa, ea, self.plane).or(Some(fallback)), + None => Some(fallback), + } + } +} + +impl CadCommand for ArcCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn set_ctrl(&mut self, ctrl: bool) { + self.cw = ctrl; + } + + fn name(&self) -> &'static str { + "ARC" + } + + fn prompt(&self) -> String { + match self.step { + ArcStep::Start => t!("ARC Specify start point of arc:"), + ArcStep::Second { .. } => t!("ARC Specify second point of arc:"), + ArcStep::End3 { .. } | ArcStep::EndSC { .. } | ArcStep::EndAfterStart { .. } => { + t!("ARC Specify end point of arc:") + } + ArcStep::CenterFirst | ArcStep::CenterAfterStart { .. } | ArcStep::CenterSE { .. } => { + t!("ARC Specify center point of arc:") + } + ArcStep::StartAfterCenter { .. } => t!("ARC Specify start point of arc:"), + ArcStep::AngleSC { .. } | ArcStep::AngleSE { .. } => t!("ARC Specify included angle:"), + ArcStep::LengthSC { .. } => t!("ARC Specify length of chord:"), + ArcStep::DirectionSE { .. } => { + t!("ARC Specify tangent direction for the start point of arc:") + } + ArcStep::RadiusSE { .. } => t!("ARC Specify radius of arc:"), + } + .into_owned() + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + match self.step { + ArcStep::Start => vec![CmdOption::new("Center", "C")], + ArcStep::Second { .. } => vec![CmdOption::new("Center", "C"), CmdOption::new("End", "E")], + ArcStep::EndSC { .. } => vec![ + CmdOption::new("Angle", "A"), + CmdOption::new("chord Length", "L"), + ], + ArcStep::CenterSE { .. } => vec![ + CmdOption::new("Angle", "A"), + CmdOption::new("Direction", "D"), + CmdOption::new("Radius", "R"), + ], + _ => Vec::new(), + } + } + + fn wants_text_input(&self) -> bool { + !matches!( + self.step, + ArcStep::End3 { .. } + | ArcStep::CenterFirst + | ArcStep::StartAfterCenter { .. } + | ArcStep::CenterAfterStart { .. } + | ArcStep::EndAfterStart { .. } + ) + } + + fn point_step_accepts_keywords(&self) -> bool { + matches!( + self.step, + ArcStep::Start | ArcStep::Second { .. } | ArcStep::EndSC { .. } | ArcStep::CenterSE { .. } + ) + } + + fn on_point(&mut self, pt: DVec3) -> CmdResult { + match self.step { + ArcStep::Start => { + self.step = ArcStep::Second { s: pt }; + CmdResult::NeedPoint + } + ArcStep::Second { s } => { + self.step = ArcStep::End3 { s, m: pt }; + CmdResult::NeedPoint + } + ArcStep::End3 { s, m } => match arc_through_points(s, m, pt, self.plane) { + Some((c, r, sa, ea)) => arc_result(c, r, sa, ea, self.plane), + None => CmdResult::NeedPoint, + }, + ArcStep::CenterFirst => { + self.step = ArcStep::StartAfterCenter { c: pt }; + CmdResult::NeedPoint + } + ArcStep::StartAfterCenter { c } => { + self.step = ArcStep::EndSC { s: pt, c }; + CmdResult::NeedPoint + } + ArcStep::CenterAfterStart { s } => { + self.step = ArcStep::EndSC { s, c: pt }; + CmdResult::NeedPoint + } + ArcStep::EndSC { s, c } => self.commit(self.sc_end(s, c, pt)), + // At a typed prompt a pick reads as commercial solutions do: the cursor's + // direction from the centre (angle) or its distance (length, + // radius), the cursor itself for a direction / sagitta. + ArcStep::AngleSC { s, c } => { + let span = self.cursor_span(s, c, pt); + self.commit(self.sc_angle(s, c, span)) + } + ArcStep::LengthSC { s, c } => self.commit(self.sc_length(s, c, s.distance(pt))), + ArcStep::EndAfterStart { s } => { + self.step = ArcStep::CenterSE { s, e: pt }; + CmdResult::NeedPoint + } + ArcStep::CenterSE { s, e } => self.commit(self.sc_end(s, pt, e)), + ArcStep::AngleSE { s, e } => { + self.commit(arc_from_sagitta(s, e, pt, self.cw, self.plane)) + } + ArcStep::DirectionSE { s, e } => { + self.commit(arc_continue(s, pt - s, e, self.cw, self.plane)) + } + ArcStep::RadiusSE { s, e } => { + self.commit(arc_from_se_radius(s, e, pt, self.cw, self.plane)) + } + } + } + + fn on_text_input(&mut self, text: &str) -> Option { + let upper = text.trim().to_uppercase(); + match self.step { + ArcStep::Start => match upper.as_str() { + "C" | "CE" | "CENTER" | "CENTRE" => { + self.step = ArcStep::CenterFirst; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + ArcStep::Second { s } => match upper.as_str() { + "C" | "CE" | "CENTER" | "CENTRE" => { + self.step = ArcStep::CenterAfterStart { s }; + Some(CmdResult::NeedPoint) + } + "E" | "END" => { + self.step = ArcStep::EndAfterStart { s }; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + ArcStep::EndSC { s, c } => match upper.as_str() { + "A" | "ANGLE" => { + self.step = ArcStep::AngleSC { s, c }; + Some(CmdResult::NeedPoint) + } + "L" | "LENGTH" => { + self.step = ArcStep::LengthSC { s, c }; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + ArcStep::CenterSE { s, e } => match upper.as_str() { + "A" | "ANGLE" => { + self.step = ArcStep::AngleSE { s, e }; + Some(CmdResult::NeedPoint) + } + "D" | "DIRECTION" => { + self.step = ArcStep::DirectionSE { s, e }; + Some(CmdResult::NeedPoint) + } + "R" | "RADIUS" => { + self.step = ArcStep::RadiusSE { s, e }; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + ArcStep::AngleSC { s, c } => { + let mut span = Self::typed_angle(text)?; + if self.cw { + span = -span; + } + Some(self.commit(self.sc_angle(s, c, span))) + } + ArcStep::LengthSC { s, c } => { + let len = Self::typed_number(text)?; + Some(self.commit(self.sc_length(s, c, len))) + } + ArcStep::AngleSE { s, e } => { + let mut included = Self::typed_angle(text)?; + if self.cw { + included = -included; + } + Some(self.commit(arc_from_endpoints_angle(s, e, included, self.plane))) + } + ArcStep::DirectionSE { s, e } => { + let angle = Self::typed_angle(text)?; + let tangent = self.plane.x * angle.cos() + self.plane.y * angle.sin(); + Some(self.commit(arc_continue(s, tangent, e, self.cw, self.plane))) + } + ArcStep::RadiusSE { s, e } => { + let r = Self::typed_number(text)?; + Some(self.commit(arc_from_endpoints_radius(s, e, r, self.cw, self.plane))) + } + _ => None, + } + } + + fn on_enter(&mut self) -> CmdResult { + match self.step { + // Enter at the start prompt: continue tangent from the last + // line or arc, as commercial solutions do. + ArcStep::Start => CmdResult::Dispatch("ARC_CONT".into()), + _ => CmdResult::Cancel, + } + } + fn enter_accepts_default_start(&self) -> bool { + false + } + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn dyn_field(&self) -> crate::command::DynField { + use crate::command::DynField; + match self.step { + ArcStep::AngleSC { .. } | ArcStep::AngleSE { .. } | ArcStep::DirectionSE { .. } => { + DynField::Angle + } + ArcStep::LengthSC { .. } | ArcStep::RadiusSE { .. } => DynField::Distance, + _ => DynField::Point, + } + } + + fn dyn_spec(&self) -> Option { + use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec}; + match self.step { + ArcStep::AngleSC { s, c } => Some(DynSpec { + anchor: DynAnchor::Point(c), + fields: vec![DynFieldSpec::new(DynRole::Angle)], + guide: DynGuide::Polar, + ref_point: Some(s), + }), + ArcStep::LengthSC { s, .. } => Some(DynSpec { + anchor: DynAnchor::Point(s), + fields: vec![DynFieldSpec::new(DynRole::Distance)], + guide: DynGuide::Radius, + ref_point: None, + }), + ArcStep::AngleSE { s, e } => Some(DynSpec { + anchor: DynAnchor::Point((s + e) * 0.5), + fields: vec![DynFieldSpec::new(DynRole::Angle)], + guide: DynGuide::None, + ref_point: Some(s), + }), + ArcStep::DirectionSE { s, .. } => Some(DynSpec { + anchor: DynAnchor::Point(s), + fields: vec![DynFieldSpec::new(DynRole::Angle)], + guide: DynGuide::Polar, + ref_point: Some(s + self.plane.x), + }), + ArcStep::RadiusSE { s, .. } => Some(DynSpec { + anchor: DynAnchor::Point(s), + fields: vec![DynFieldSpec::new(DynRole::Radius)], + guide: DynGuide::None, + ref_point: None, + }), + _ => None, + } + } + + fn dyn_commit_as_text(&self) -> bool { + matches!( + self.step, + ArcStep::AngleSC { .. } + | ArcStep::LengthSC { .. } + | ArcStep::AngleSE { .. } + | ArcStep::DirectionSE { .. } + | ArcStep::RadiusSE { .. } + ) + } + + fn dyn_live_value(&self, cursor: DVec3) -> Option { + match self.step { + ArcStep::AngleSC { s, c } => Some(crate::command::dyn_display_angle_deg( + (angle_xy(c, cursor, self.plane) - angle_xy(c, s, self.plane)) as f32, + ) as f64), + ArcStep::LengthSC { s, .. } => Some(s.distance(cursor)), + ArcStep::RadiusSE { s, e } => { + arc_from_se_radius(s, e, cursor, self.cw, self.plane).map(|(_, r, _, _)| r) + } + _ => None, + } + } + + fn on_mouse_move(&mut self, pt: DVec3) -> Option { + match self.step { + ArcStep::Start | ArcStep::CenterFirst => None, + ArcStep::Second { s } | ArcStep::EndAfterStart { s } | ArcStep::CenterAfterStart { s } => { + Some(line_wire(s, pt)) + } + ArcStep::StartAfterCenter { c } => Some(line_wire(c, pt)), + ArcStep::End3 { s, m } => { + let fallback = WireModel::solid_f64( + "rubber_band".into(), + vec![[s.x, s.y, s.z], [m.x, m.y, m.z], [pt.x, pt.y, pt.z]], + WireModel::CYAN, + false, + ); + self.preview(arc_through_points(s, m, pt, self.plane), fallback) + } + ArcStep::EndSC { s, c } => self.preview(self.sc_end(s, c, pt), line_wire(c, pt)), + ArcStep::AngleSC { s, c } => { + let span = self.cursor_span(s, c, pt); + self.preview(self.sc_angle(s, c, span), line_wire(c, pt)) + } + ArcStep::LengthSC { s, c } => { + self.preview(self.sc_length(s, c, s.distance(pt)), line_wire(s, pt)) + } + ArcStep::CenterSE { s, e } => self.preview(self.sc_end(s, pt, e), line_wire(s, e)), + ArcStep::AngleSE { s, e } => { + self.preview(arc_from_sagitta(s, e, pt, self.cw, self.plane), line_wire(s, e)) + } + ArcStep::DirectionSE { s, e } => { + self.preview(arc_continue(s, pt - s, e, self.cw, self.plane), line_wire(s, e)) + } + ArcStep::RadiusSE { s, e } => { + self.preview(arc_from_se_radius(s, e, pt, self.cw, self.plane), line_wire(s, e)) + } + } + } +} + // ── Command 2: 3-Point (ARC_3P) ────────────────────────────────────────── pub struct Arc3PCommand { @@ -1531,7 +1968,7 @@ impl CadCommand for ArcContCommand { inventory::submit!(crate::command::CommandRegistration { names: &["ARC", "ARC_3P"] }); // Arc3PCommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSA"] }); // ArcCSACommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSL"] }); // ArcCSLCommand -inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSE"] }); // ArcCommand +inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSE"] }); // ArcCSECommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCA"] }); // ArcSCACommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCE"] }); // ArcSCECommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCL"] }); // ArcSCLCommand @@ -1539,3 +1976,126 @@ inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SEA"] }); inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SED"] }); // ArcSEDCommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SER"] }); // ArcSERCommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CONT"] }); // ArcContCommand + +#[cfg(test)] +mod acad_flow_tests { + use super::*; + + fn keywords(cmd: &ArcCommand) -> Vec { + cmd.options().into_iter().map(|o| o.keyword).collect() + } + + fn p(x: f64, y: f64) -> DVec3 { + DVec3::new(x, y, 0.0) + } + + fn committed_arc(res: CmdResult) -> CadArc { + match res { + CmdResult::CommitAndExit(EntityType::Arc(a)) => a, + _ => panic!("expected a committed arc"), + } + } + + #[test] + fn prompts_follow_compatible_keywords() { + let mut cmd = ArcCommand::new(); + assert_eq!(keywords(&cmd), ["C"]); + cmd.on_point(p(0.0, 0.0)); + assert_eq!(keywords(&cmd), ["C", "E"]); + assert!(matches!(cmd.on_text_input("C"), Some(CmdResult::NeedPoint))); + assert!(keywords(&cmd).is_empty(), "centre pick has no keywords"); + cmd.on_point(p(5.0, 0.0)); + assert_eq!(keywords(&cmd), ["A", "L"]); + + let mut cmd = ArcCommand::new(); + cmd.on_point(p(0.0, 0.0)); + cmd.on_text_input("E"); + cmd.on_point(p(10.0, 0.0)); + assert_eq!(keywords(&cmd), ["A", "D", "R"]); + } + + #[test] + fn three_points_make_an_arc() { + let mut cmd = ArcCommand::new(); + cmd.on_point(p(0.0, 0.0)); + cmd.on_point(p(5.0, 5.0)); + let arc = committed_arc(cmd.on_point(p(10.0, 0.0))); + assert!((arc.radius - 5.0).abs() < 1e-9); + assert!((arc.center.x - 5.0).abs() < 1e-9 && arc.center.y.abs() < 1e-9); + } + + #[test] + fn center_first_then_start_and_end() { + let mut cmd = ArcCommand::new(); + cmd.on_text_input("C"); + cmd.on_point(p(5.0, 0.0)); + cmd.on_point(p(10.0, 0.0)); + assert_eq!(keywords(&cmd), ["A", "L"]); + let arc = committed_arc(cmd.on_point(p(5.0, 10.0))); + assert!((arc.radius - 5.0).abs() < 1e-9); + assert!(arc.start_angle.abs() < 1e-9); + assert!((arc.end_angle - std::f64::consts::FRAC_PI_2).abs() < 1e-9); + } + + #[test] + fn start_center_angle_and_chord_length_typed() { + let mut cmd = ArcCommand::new(); + cmd.on_point(p(10.0, 0.0)); + cmd.on_text_input("C"); + cmd.on_point(p(5.0, 0.0)); + cmd.on_text_input("A"); + let arc = committed_arc(cmd.on_text_input("90").expect("angle consumed")); + assert!((arc.end_angle - arc.start_angle - std::f64::consts::FRAC_PI_2).abs() < 1e-9); + + let mut cmd = ArcCommand::new(); + cmd.on_point(p(10.0, 0.0)); + cmd.on_text_input("C"); + cmd.on_point(p(5.0, 0.0)); + cmd.on_text_input("L"); + // Chord 10 on radius 5: a semicircle. + let arc = committed_arc(cmd.on_text_input("10").expect("length consumed")); + assert!((arc.end_angle - arc.start_angle - std::f64::consts::PI).abs() < 1e-9); + } + + #[test] + fn start_end_then_radius_direction_and_angle() { + let mut cmd = ArcCommand::new(); + cmd.on_point(p(0.0, 0.0)); + cmd.on_text_input("E"); + cmd.on_point(p(10.0, 0.0)); + cmd.on_text_input("R"); + let arc = committed_arc(cmd.on_text_input("5").expect("radius consumed")); + assert!((arc.radius - 5.0).abs() < 1e-9); + + let mut cmd = ArcCommand::new(); + cmd.on_point(p(0.0, 0.0)); + cmd.on_text_input("E"); + cmd.on_point(p(10.0, 0.0)); + cmd.on_text_input("D"); + // Tangent straight up at the start: a semicircle over the chord. + let arc = committed_arc(cmd.on_text_input("90").expect("direction consumed")); + assert!((arc.radius - 5.0).abs() < 1e-6); + + let mut cmd = ArcCommand::new(); + cmd.on_point(p(0.0, 0.0)); + cmd.on_text_input("E"); + cmd.on_point(p(10.0, 0.0)); + cmd.on_text_input("A"); + let arc = committed_arc(cmd.on_text_input("180").expect("angle consumed")); + assert!((arc.radius - 5.0).abs() < 1e-6); + + // Centre pick after start / end. + let mut cmd = ArcCommand::new(); + cmd.on_point(p(0.0, 0.0)); + cmd.on_text_input("E"); + cmd.on_point(p(10.0, 0.0)); + let arc = committed_arc(cmd.on_point(p(5.0, 0.0))); + assert!((arc.radius - 5.0).abs() < 1e-9); + } + + #[test] + fn enter_at_start_continues_from_last_entity() { + let mut cmd = ArcCommand::new(); + assert!(matches!(cmd.on_enter(), CmdResult::Dispatch(c) if c == "ARC_CONT")); + } +} diff --git a/src/modules/draw/draw/line.rs b/src/modules/draw/draw/line.rs index 09615df52..cc809dc57 100644 --- a/src/modules/draw/draw/line.rs +++ b/src/modules/draw/draw/line.rs @@ -71,6 +71,21 @@ impl CadCommand for LineCommand { } } + fn options(&self) -> Vec { + use crate::command::CmdOption; + // Covers the LINE prompt of commercial solutions: Undo once a point is placed, Close + // once a closing segment is possible, eXit always. + match self.points.len() { + 0 => Vec::new(), + 1 => vec![CmdOption::new("Undo", "U"), CmdOption::new("eXit", "X")], + _ => vec![ + CmdOption::new("Close", "C"), + CmdOption::new("Undo", "U"), + CmdOption::new("eXit", "X"), + ], + } + } + fn on_point(&mut self, pt: DVec3) -> CmdResult { if let Some(&last) = self.points.last() { let line = Self::line_between(last, pt); @@ -185,6 +200,9 @@ impl CadCommand for LineCommand { Some(CmdResult::NeedPoint) } } + // eXit: end the command keeping every segment drawn so far + // (the third LINE keyword of commercial solutions). + "X" | "EXIT" => Some(CmdResult::Cancel), "U" | "UNDO" => { if self.points.len() >= 2 { // Drop the last vertex and revert its committed segment. diff --git a/src/modules/draw/draw/poly3d.rs b/src/modules/draw/draw/poly3d.rs index 773a53ce8..58f5cfe90 100644 --- a/src/modules/draw/draw/poly3d.rs +++ b/src/modules/draw/draw/poly3d.rs @@ -89,6 +89,18 @@ impl CadCommand for Poly3dCommand { !self.points.is_empty() } + fn options(&self) -> Vec { + use crate::command::CmdOption; + let mut opts = Vec::new(); + if self.points.len() >= 3 { + opts.push(CmdOption::new("Close", "C")); + } + if !self.points.is_empty() { + opts.push(CmdOption::new("Undo", "U")); + } + opts + } + fn on_text_input(&mut self, text: &str) -> Option { match text.trim().to_uppercase().as_str() { "C" | "CLOSE" if self.points.len() >= 3 => self diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index 852c42400..c195fc3a6 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -1,13 +1,17 @@ // Polyline tool — ribbon definition + interactive command. // // Command: PLINE (PL) -// Each click adds a vertex. -// Type A = switch to Arc segment mode. -// Type L = switch back to Line segment mode. -// Enter / C = close and commit. Escape = commit as-is (if ≥2 vertices). +// Each click adds a vertex. Keywords match commercial solutions: +// Line mode: Arc / Close / Halfwidth / Length / Undo / Width +// Arc mode: Angle / CEnter / CLose / Direction / Halfwidth / Line / +// Radius / Second pt / Undo / Width +// Enter = finish, Escape = finish as-is (a keyword sub-prompt is left +// first), C / CL = close and finish. // -// Arc mode: arcs are tangent-continuous with the preceding segment. +// Arc mode: arcs are tangent-continuous with the preceding segment unless a +// keyword (Angle / CEnter / Direction / Radius / Second pt) defines them. // Bulge is stored per vertex (segment i→i+1); positive = CCW, negative = CW. +// Widths are stored per segment as (start, end). use acadrust::entities::LwVertex; use acadrust::types::Vector2; @@ -38,13 +42,82 @@ enum SegMode { Arc, } +/// A keyword sub-step inside PLINE (the option prompts of commercial solutions). `None` is the +/// regular "specify next point" step for the current segment mode. Every +/// sub-step returns to `None` once it has produced a vertex, or when Undo / +/// Enter / Escape backs out of it. +#[derive(Clone, Copy, PartialEq, Debug)] +enum Sub { + None, + /// Width / Halfwidth: starting width, then ending width. `half` scales the + /// prompt and the typed value by two. + WidthStart { half: bool }, + WidthEnd { half: bool, start: f64 }, + /// Length: a line segment of the typed length along the last tangent. + Length, + /// Arc → Angle: typed included angle, then an endpoint [CEnter/Radius]. + ArcAngle, + ArcAngleEnd { angle: f64 }, + ArcAngleCenter { angle: f64 }, + ArcAngleRadius { angle: f64 }, + ArcAngleRadiusDir { angle: f64, r: f64 }, + /// Arc → CEnter: centre pick, then an endpoint [Angle/Length]. + ArcCenter, + ArcCenterEnd { c: DVec2 }, + ArcCenterAngle { c: DVec2 }, + ArcCenterLength { c: DVec2 }, + /// Arc → Direction: a tangent-direction pick, then the endpoint. + ArcDirection, + ArcDirectionEnd { dir: DVec2 }, + /// Arc → Radius: typed radius, then an endpoint [Angle]. + ArcRadius, + ArcRadiusEnd { r: f64 }, + ArcRadiusAngle { r: f64 }, + ArcRadiusAngleDir { r: f64, angle: f64 }, + /// Arc → Second pt: a point on the arc, then the endpoint (3-point arc). + ArcSecond, + ArcSecondEnd { s: DVec2 }, +} + +impl Sub { + /// The step reads a single typed value (width, length, angle, radius). + fn is_scalar(self) -> bool { + matches!( + self, + Sub::WidthStart { .. } + | Sub::WidthEnd { .. } + | Sub::Length + | Sub::ArcAngle + | Sub::ArcAngleRadius { .. } + | Sub::ArcCenterAngle { .. } + | Sub::ArcCenterLength { .. } + | Sub::ArcRadius + | Sub::ArcRadiusAngle { .. } + ) + } + + /// The typed value is an angle (degrees) rather than a length. + fn is_angle(self) -> bool { + matches!( + self, + Sub::ArcAngle | Sub::ArcCenterAngle { .. } | Sub::ArcRadiusAngle { .. } + ) + } +} + // ── Command implementation ───────────────────────────────────────────────── pub struct PlineCommand { vertices: Vec, /// Bulge for segment i → i+1 (one entry per vertex; last entry unused on commit). bulges: Vec, + /// `(start, end)` width for segment i → i+1, parallel to `bulges`. + widths: Vec<(f64, f64)>, + /// Width pair applied to the next segment. After a segment lands its end + /// width becomes the next start width, as in commercial solutions. + cur_width: (f64, f64), mode: SegMode, + sub: Sub, /// Unit direction of the last committed segment (for arc tangent continuity). last_tangent: Option, /// Handle of the live polyline entity once it exists (created at the 2nd @@ -57,10 +130,14 @@ pub struct PlineCommand { impl PlineCommand { pub fn new() -> Self { + let w = super::super::defaults::get_pline_width(); Self { vertices: Vec::new(), bulges: Vec::new(), + widths: Vec::new(), + cur_width: (w, w), mode: SegMode::Line, + sub: Sub::None, last_tangent: None, live_handle: None, plane: WorkingPlane::default(), @@ -76,6 +153,10 @@ impl PlineCommand { } self.vertices.pop(); self.bulges.pop(); + if let Some((start, _)) = self.widths.pop() { + // The undone segment's start width is the current one again. + self.cur_width = (start, self.cur_width.1); + } // Restore the exit tangent of the segment that is now last so a // following Arc segment stays tangent-continuous. let n = self.vertices.len(); @@ -133,6 +214,9 @@ impl PlineCommand { .map(|(i, v)| { let mut lv = LwVertex::new(Vector2::new(v.x, v.y)); lv.bulge = self.bulges.get(i).copied().unwrap_or(0.0); + let (sw, ew) = self.widths.get(i).copied().unwrap_or((0.0, 0.0)); + lv.start_width = sw; + lv.end_width = ew; lv }) .collect(); @@ -144,6 +228,356 @@ impl PlineCommand { }; Some(self.plane.place_entity(EntityType::LwPolyline(pline))) } + + // ── Sub-step geometry (all in working-plane local 2-D) ───────────────── + + /// Last vertex in local 2-D plus its Z (every vertex shares the plane). + fn last_local(&self) -> Option<(DVec2, f64)> { + let last = self.plane.to_local(*self.vertices.last()?); + Some((DVec2::new(last.x, last.y), last.z)) + } + + fn local2(&self, pt: DVec3) -> DVec2 { + let p = self.plane.to_local(pt); + DVec2::new(p.x, p.y) + } + + fn tangent2(&self) -> DVec2 { + self.last_tangent + .map(|t| t.as_dvec2()) + .filter(|t| t.length_squared() > 1e-12) + .unwrap_or(DVec2::X) + } + + /// Sign for an arc whose direction is not fixed by its definition + /// (CEnter / Radius endpoints, chord length): bend the way the previous + /// segment was heading so the polyline flows on, else counter-clockwise. + fn free_arc_sign(&self, a: DVec2, e: DVec2) -> f64 { + let chord = e - a; + let cross = match self.last_tangent { + Some(t) => t.as_dvec2().perp_dot(chord), + None => 1.0, + }; + if cross < 0.0 { + -1.0 + } else { + 1.0 + } + } + + /// Bulge of the arc from `a` to `e` through `s` (3-point arc). `None` when + /// the points are collinear or coincident. + fn bulge_through(a: DVec2, s: DVec2, e: DVec2) -> Option { + let d = 2.0 * (a.x * (s.y - e.y) + s.x * (e.y - a.y) + e.x * (a.y - s.y)); + if d.abs() < 1e-12 { + return None; + } + let a2 = a.length_squared(); + let s2 = s.length_squared(); + let e2 = e.length_squared(); + let cx = (a2 * (s.y - e.y) + s2 * (e.y - a.y) + e2 * (a.y - s.y)) / d; + let cy = (a2 * (e.x - s.x) + s2 * (a.x - e.x) + e2 * (s.x - a.x)) / d; + let c = DVec2::new(cx, cy); + let start = (a - c).y.atan2((a - c).x); + let end = (e - c).y.atan2((e - c).x); + // The arc bends towards `s`: with `s` right of the chord a → e the + // arc runs counter-clockwise, left of it clockwise. + let ccw = (e - a).perp_dot(s - a) < 0.0; + let mut sweep = end - start; + if ccw { + if sweep <= 0.0 { + sweep += std::f64::consts::TAU; + } + } else if sweep >= 0.0 { + sweep -= std::f64::consts::TAU; + } + Some((sweep / 4.0).tan()) + } + + /// Endpoint of the arc about `c` starting at `a` with signed `sweep`. + fn rotate_about(c: DVec2, a: DVec2, sweep: f64) -> DVec2 { + let (sn, cs) = sweep.sin_cos(); + let v = a - c; + c + DVec2::new(v.x * cs - v.y * sn, v.x * sn + v.y * cs) + } + + /// Resolve the arc a sub-step describes for a candidate `cursor`/point: + /// `(endpoint, bulge)`. `None` when the geometry is degenerate for that + /// input (e.g. a chord longer than the diameter). + fn arc_for(&self, sub: Sub, a: DVec2, p: DVec2) -> Option<(DVec2, f64)> { + let sweep_ok = |s: f64| s.abs() > 1e-9 && s.abs() < std::f64::consts::TAU; + match sub { + Sub::ArcAngleEnd { angle } if sweep_ok(angle) => { + if (p - a).length_squared() < 1e-12 { + return None; + } + Some((p, (angle / 4.0).tan())) + } + Sub::ArcAngleCenter { angle } if sweep_ok(angle) => { + if (a - p).length_squared() < 1e-12 { + return None; + } + Some((Self::rotate_about(p, a, angle), (angle / 4.0).tan())) + } + Sub::ArcAngleRadiusDir { angle, r } if sweep_ok(angle) && r > 0.0 => { + let dir = (p - a).try_normalize()?; + let chord = 2.0 * r * (angle.abs() * 0.5).sin(); + Some((a + dir * chord, (angle / 4.0).tan())) + } + Sub::ArcCenterEnd { c } => { + let r = (a - c).length(); + if r < 1e-9 { + return None; + } + let dir = (p - c).try_normalize()?; + let e = c + dir * r; + let start = (a - c).y.atan2((a - c).x); + let end = (e - c).y.atan2((e - c).x); + let sign = self.free_arc_sign(a, e); + let mut sweep = (end - start).rem_euclid(std::f64::consts::TAU); + if sign < 0.0 { + sweep -= std::f64::consts::TAU; + } + if !sweep_ok(sweep) { + return None; + } + Some((e, (sweep / 4.0).tan())) + } + Sub::ArcCenterAngle { c } => { + // `p` carries the typed angle in `x`. + let angle = p.x; + if (a - c).length_squared() < 1e-12 || !sweep_ok(angle) { + return None; + } + Some((Self::rotate_about(c, a, angle), (angle / 4.0).tan())) + } + Sub::ArcCenterLength { c } => { + // `p` carries the typed chord length in `x`. + let len = p.x; + let r = (a - c).length(); + if r < 1e-9 || len <= 0.0 || len > 2.0 * r { + return None; + } + let sweep = 2.0 * (len / (2.0 * r)).asin(); + // Minor arc; bend on the side the previous segment favours. + let e_ccw = Self::rotate_about(c, a, sweep); + let sign = self.free_arc_sign(a, e_ccw); + let sweep = sweep * sign; + Some((Self::rotate_about(c, a, sweep), (sweep / 4.0).tan())) + } + Sub::ArcDirectionEnd { dir } => { + if (p - a).length_squared() < 1e-12 { + return None; + } + Some((p, compute_bulge(a, dir, p))) + } + Sub::ArcRadiusEnd { r } if r > 0.0 => { + let chord = (p - a).length(); + if chord < 1e-9 || chord > 2.0 * r { + return None; + } + let sweep = 2.0 * (chord / (2.0 * r)).asin() * self.free_arc_sign(a, p); + Some((p, (sweep / 4.0).tan())) + } + Sub::ArcRadiusAngleDir { r, angle } if r > 0.0 && sweep_ok(angle) => { + let dir = (p - a).try_normalize()?; + let chord = 2.0 * r * (angle.abs() * 0.5).sin(); + Some((a + dir * chord, (angle / 4.0).tan())) + } + Sub::ArcSecondEnd { s } => { + if (p - a).length_squared() < 1e-12 { + return None; + } + Some((p, Self::bulge_through(a, s, p)?)) + } + _ => None, + } + } + + /// Land a segment ending at local `e` with `bulge`, publish it, and + /// return to the regular next-point step. + fn push_segment(&mut self, e: DVec2, z: f64, bulge: f64) -> CmdResult { + let last_idx = self.vertices.len() - 1; + let a = self.local2(self.vertices[last_idx]); + self.bulges[last_idx] = bulge; + self.last_tangent = seg_exit_tangent( + DVec3::new(a.x, a.y, z), + DVec3::new(e.x, e.y, z), + bulge, + ); + self.sub = Sub::None; + self.push_vertex(self.plane.to_world(DVec3::new(e.x, e.y, z))) + } + + /// Append a vertex (bulge/width of the segment *into* it are already set + /// on the previous entry) and publish. + fn push_vertex(&mut self, pt: DVec3) -> CmdResult { + if !self.vertices.is_empty() { + let last = self.widths.len() - 1; + self.widths[last] = self.cur_width; + // The end width flows into the next segment's start width. + self.cur_width.0 = self.cur_width.1; + } + self.vertices.push(pt); + self.bulges.push(0.0); + self.widths.push(self.cur_width); + // Publish to the document as soon as the polyline has a segment so it + // becomes snappable while the rest is drawn. (#119) + if self.vertices.len() >= 2 { + self.sync_live(false, false) + } else { + CmdResult::NeedPoint + } + } + + /// A typed value for a scalar sub-step. Widths are lengths ≥ 0; the angle + /// steps read degrees (positive = counter-clockwise); Length may be + /// negative (draws backwards). + fn on_scalar(&mut self, value: f64) -> Option { + if !value.is_finite() { + return None; + } + let (a, z) = self.last_local()?; + match self.sub { + Sub::WidthStart { half } => { + if value < 0.0 { + return None; + } + let start = if half { value * 2.0 } else { value }; + self.sub = Sub::WidthEnd { half, start }; + Some(CmdResult::NeedPoint) + } + Sub::WidthEnd { half, start } => { + if value < 0.0 { + return None; + } + let end = if half { value * 2.0 } else { value }; + self.cur_width = (start, end); + super::super::defaults::set_pline_width(end); + self.sub = Sub::None; + Some(CmdResult::NeedPoint) + } + Sub::Length => { + if value.abs() < 1e-9 { + return None; + } + let e = a + self.tangent2() * value; + Some(self.push_segment(e, z, 0.0)) + } + Sub::ArcAngle => { + let angle = value.to_radians(); + if angle.abs() < 1e-9 || angle.abs() >= std::f64::consts::TAU { + return None; + } + self.sub = Sub::ArcAngleEnd { angle }; + Some(CmdResult::NeedPoint) + } + Sub::ArcAngleRadius { angle } => { + if value <= 0.0 { + return None; + } + self.sub = Sub::ArcAngleRadiusDir { angle, r: value }; + Some(CmdResult::NeedPoint) + } + Sub::ArcCenterAngle { c } => { + let (e, bulge) = + self.arc_for(Sub::ArcCenterAngle { c }, a, DVec2::new(value.to_radians(), 0.0))?; + Some(self.push_segment(e, z, bulge)) + } + Sub::ArcCenterLength { c } => { + let (e, bulge) = self.arc_for(Sub::ArcCenterLength { c }, a, DVec2::new(value, 0.0))?; + Some(self.push_segment(e, z, bulge)) + } + Sub::ArcRadius => { + if value <= 0.0 { + return None; + } + self.sub = Sub::ArcRadiusEnd { r: value }; + Some(CmdResult::NeedPoint) + } + Sub::ArcRadiusAngle { r } => { + let angle = value.to_radians(); + if angle.abs() < 1e-9 || angle.abs() >= std::f64::consts::TAU { + return None; + } + self.sub = Sub::ArcRadiusAngleDir { r, angle }; + Some(CmdResult::NeedPoint) + } + _ => None, + } + } + + /// Leave the current sub-step without placing anything. + fn leave_sub(&mut self) -> CmdResult { + self.sub = Sub::None; + CmdResult::NeedPoint + } + + /// Rubber-band wire for the arc a → e with `bulge` (falls back to a line + /// for a degenerate bulge). + fn arc_wire(&self, a: DVec2, e: DVec2, z: f64, bulge: f64) -> WireModel { + let last = Vec3::new(a.x as f32, a.y as f32, z as f32); + let pt = Vec3::new(e.x as f32, e.y as f32, z as f32); + let mut pts: Vec<[f32; 3]> = Vec::new(); + let mut tangent_geom = None; + if bulge.abs() >= 1e-9 && (e - a).length_squared() >= 1e-12 { + if let Some(arc) = + crate::entities::common::BulgeArc::from_bulge([a.x, a.y], [e.x, e.y], bulge) + { + for s in arc.tessellate_angle(std::f64::consts::TAU / 64.0) { + pts.push([s[0] as f32, s[1] as f32, last.z]); + } + let (sa, ea) = if arc.sweep >= 0.0 { + (arc.start_angle, arc.end_angle) + } else { + (arc.end_angle, arc.start_angle) + }; + let world_center = self + .plane + .to_world(DVec3::new(arc.center[0], arc.center[1], z)); + if arc.radius > 0.0 + && arc.radius.is_finite() + && arc.radius <= 1e6 + && sa.is_finite() + && ea.is_finite() + { + tangent_geom = Some(TangentGeom::Arc { + center: world_center.to_array(), + axis_x: self.plane.x.to_array(), + axis_y: self.plane.y.to_array(), + radius: arc.radius, + start_angle: sa, + end_angle: ea, + }); + } + } else { + pts.extend_from_slice(&arc_sample_points(last, bulge, pt, 64)); + } + } else { + pts.push([last.x, last.y, last.z]); + pts.push([pt.x, pt.y, pt.z]); + } + let world_points = pts + .into_iter() + .map(|point| self.plane.to_world(Vec3::from_array(point).as_dvec3()).as_vec3().to_array()) + .collect(); + let mut wire = WireModel::solid("rubber_band".into(), world_points, WireModel::CYAN, false); + if let Some(tg) = tangent_geom { + wire.tangent_geoms.push(tg); + } + wire + } + + fn line_wire(&self, a: DVec2, e: DVec2, z: f64) -> WireModel { + let a = self.plane.to_world(DVec3::new(a.x, a.y, z)).as_vec3(); + let e = self.plane.to_world(DVec3::new(e.x, e.y, z)).as_vec3(); + WireModel::solid( + "rubber_band".into(), + vec![a.to_array(), e.to_array()], + WireModel::CYAN, + false, + ) + } } // ── Arc geometry helpers ─────────────────────────────────────────────────── @@ -260,6 +694,37 @@ pub(crate) fn arc_sample_points(a: Vec3, bulge: f64, b: Vec3, n: usize) -> Vec<[ // ── CadCommand impl ──────────────────────────────────────────────────────── +impl PlineCommand { + /// Close the polyline: in Arc mode the closing segment is an arc tangent + /// to the last segment (CLose in commercial solutions), in Line mode a straight one. + fn close(&mut self) -> CmdResult { + if self.vertices.len() < 2 { + return CmdResult::NeedPoint; + } + let last_idx = self.vertices.len() - 1; + let a = self.local2(self.vertices[last_idx]); + let first = self.local2(self.vertices[0]); + self.bulges[last_idx] = match self.mode { + SegMode::Line => 0.0, + SegMode::Arc => compute_bulge(a, self.tangent2(), first), + }; + self.widths[last_idx] = self.cur_width; + self.sub = Sub::None; + self.sync_live(true, true) + } + + fn width_prompt(&self, half: bool, ending: bool, default: f64) -> String { + let shown = crate::entities::common::format_length(if half { default * 0.5 } else { default }); + match (half, ending) { + (false, false) => t!("PLINE Specify starting width <%{w}>:", w = shown), + (false, true) => t!("PLINE Specify ending width <%{w}>:", w = shown), + (true, false) => t!("PLINE Specify starting half-width <%{w}>:", w = shown), + (true, true) => t!("PLINE Specify ending half-width <%{w}>:", w = shown), + } + .into_owned() + } +} + impl CadCommand for PlineCommand { fn set_working_plane(&mut self, plane: WorkingPlane) { self.plane = plane; @@ -270,70 +735,176 @@ impl CadCommand for PlineCommand { } fn prompt(&self) -> String { - let mode_tag = match self.mode { - SegMode::Line => t!("Line"), - SegMode::Arc => t!("Arc"), - }; if self.vertices.is_empty() { - t!("PLINE Specify start point:").into_owned() - } else { - t!( - "PLINE [%{mode}] Next pt [%{count}pts]:", - mode = mode_tag, - count = self.vertices.len() - ) - .into_owned() + return t!("PLINE Specify start point:").into_owned(); + } + match self.sub { + Sub::None => match self.mode { + SegMode::Line => t!("PLINE (Line) Specify next point:").into_owned(), + SegMode::Arc => t!("PLINE (Arc) Specify endpoint of arc:").into_owned(), + }, + Sub::WidthStart { half } => self.width_prompt(half, false, self.cur_width.0), + Sub::WidthEnd { half, start } => self.width_prompt(half, true, start), + Sub::Length => t!("PLINE Specify length of line:").into_owned(), + Sub::ArcAngle | Sub::ArcCenterAngle { .. } | Sub::ArcRadiusAngle { .. } => { + t!("PLINE Specify included angle:").into_owned() + } + Sub::ArcAngleEnd { .. } + | Sub::ArcCenterEnd { .. } + | Sub::ArcDirectionEnd { .. } + | Sub::ArcRadiusEnd { .. } + | Sub::ArcSecondEnd { .. } => t!("PLINE Specify endpoint of arc:").into_owned(), + Sub::ArcAngleCenter { .. } | Sub::ArcCenter => { + t!("PLINE Specify center point of arc:").into_owned() + } + Sub::ArcAngleRadius { .. } | Sub::ArcRadius => { + t!("PLINE Specify radius of arc:").into_owned() + } + Sub::ArcAngleRadiusDir { .. } | Sub::ArcRadiusAngleDir { .. } => { + t!("PLINE Specify direction of chord for arc:").into_owned() + } + Sub::ArcCenterLength { .. } => t!("PLINE Specify length of chord:").into_owned(), + Sub::ArcDirection => { + t!("PLINE Specify tangent direction for the start point of arc:").into_owned() + } + Sub::ArcSecond => t!("PLINE Specify second point on arc:").into_owned(), } } fn options(&self) -> Vec { use crate::command::CmdOption; - // Options appear once at least one vertex is placed: the segment step - // accepts A / L / C, and Enter finishes. if self.vertices.is_empty() { - Vec::new() - } else { - vec![ - CmdOption::new(t!("Arc").as_ref(), "A"), - CmdOption::new(t!("Line").as_ref(), "L"), - CmdOption::new(t!("Close").as_ref(), "C"), - CmdOption::new(t!("Undo").as_ref(), "U"), - CmdOption::enter(t!("Done").as_ref()), - ] + return Vec::new(); + } + let can_close = self.vertices.len() >= 3 + || (self.vertices.len() == 2 && matches!(self.mode, SegMode::Arc)); + match self.sub { + // The keyword sets of commercial solutions, in their order. + Sub::None => match self.mode { + SegMode::Line => { + let mut opts = vec![CmdOption::new("Arc", "A")]; + if can_close { + opts.push(CmdOption::new("Close", "C")); + } + opts.extend([ + CmdOption::new("Halfwidth", "H"), + CmdOption::new("Length", "L"), + CmdOption::new("Undo", "U"), + CmdOption::new("Width", "W"), + CmdOption::enter("Done"), + ]); + opts + } + SegMode::Arc => { + let mut opts = vec![ + CmdOption::new("Angle", "A"), + CmdOption::new("CEnter", "CE"), + ]; + if can_close { + opts.push(CmdOption::new("CLose", "CL")); + } + opts.extend([ + CmdOption::new("Direction", "D"), + CmdOption::new("Halfwidth", "H"), + CmdOption::new("Line", "L"), + CmdOption::new("Radius", "R"), + CmdOption::new("Second pt", "S"), + CmdOption::new("Undo", "U"), + CmdOption::new("Width", "W"), + CmdOption::enter("Done"), + ]); + opts + } + }, + Sub::WidthStart { half } => { + let d = if half { self.cur_width.0 * 0.5 } else { self.cur_width.0 }; + vec![CmdOption::enter(&format!("<{}>", crate::entities::common::format_length(d)))] + } + Sub::WidthEnd { half, start } => { + let d = if half { start * 0.5 } else { start }; + vec![CmdOption::enter(&format!("<{}>", crate::entities::common::format_length(d)))] + } + Sub::ArcAngleEnd { .. } => vec![ + CmdOption::new("CEnter", "CE"), + CmdOption::new("Radius", "R"), + ], + Sub::ArcCenterEnd { .. } => vec![ + CmdOption::new("Angle", "A"), + CmdOption::new("Length", "L"), + ], + Sub::ArcRadiusEnd { .. } => vec![CmdOption::new("Angle", "A")], + _ => Vec::new(), } } fn on_point(&mut self, pt: DVec3) -> CmdResult { - if !self.vertices.is_empty() { - let last = *self.vertices.last().unwrap(); - let last_local = self.plane.to_local(last); - let pt_local = self.plane.to_local(pt); - let last_idx = self.vertices.len() - 1; - - let bulge = match self.mode { - SegMode::Line => { - let d = DVec2::new(pt_local.x - last_local.x, pt_local.y - last_local.y); - if d.length_squared() > 1e-10 { - // Direction only — f32 is sufficient for tangent continuity. - self.last_tangent = Some(d.normalize().as_vec2()); - } - 0.0 - } - SegMode::Arc => { - let a = DVec2::new(last_local.x, last_local.y); - let b = DVec2::new(pt_local.x, pt_local.y); - let tangent = self - .last_tangent - .map(|t| t.as_dvec2()) - // No previous tangent: default to pointing right (arbitrary). - .unwrap_or(DVec2::new(1.0, 0.0)); - let bulge = compute_bulge(a, tangent, b); - update_tangent_after_arc(&mut self.last_tangent, bulge); - bulge - } - }; - self.bulges[last_idx] = bulge; + if self.vertices.is_empty() { + return self.push_vertex(pt); } + let Some((a, z)) = self.last_local() else { + return CmdResult::NeedPoint; + }; + let p = self.local2(pt); + + match self.sub { + Sub::None => {} + // A picked point at a typed-value prompt reads as the distance + // (or angle) from the last vertex, as commercial solutions allow. + sub if sub.is_scalar() => { + let value = if sub.is_angle() { + (p - a).y.atan2((p - a).x).to_degrees() + } else { + (p - a).length() + }; + return self.on_scalar(value).unwrap_or(CmdResult::NeedPoint); + } + Sub::ArcCenter => { + if (p - a).length_squared() < 1e-12 { + return CmdResult::NeedPoint; + } + self.sub = Sub::ArcCenterEnd { c: p }; + return CmdResult::NeedPoint; + } + Sub::ArcDirection => { + let Some(dir) = (p - a).try_normalize() else { + return CmdResult::NeedPoint; + }; + self.sub = Sub::ArcDirectionEnd { dir }; + return CmdResult::NeedPoint; + } + Sub::ArcSecond => { + if (p - a).length_squared() < 1e-12 { + return CmdResult::NeedPoint; + } + self.sub = Sub::ArcSecondEnd { s: p }; + return CmdResult::NeedPoint; + } + sub => { + // Endpoint / centre / chord-direction pick that completes an arc. + return match self.arc_for(sub, a, p) { + Some((e, bulge)) => self.push_segment(e, z, bulge), + None => CmdResult::NeedPoint, + }; + } + } + + let last_idx = self.vertices.len() - 1; + let bulge = match self.mode { + SegMode::Line => { + let d = p - a; + if d.length_squared() > 1e-10 { + // Direction only — f32 is sufficient for tangent continuity. + self.last_tangent = Some(d.normalize().as_vec2()); + } + 0.0 + } + SegMode::Arc => { + let bulge = compute_bulge(a, self.tangent2(), p); + update_tangent_after_arc(&mut self.last_tangent, bulge); + bulge + } + }; + self.bulges[last_idx] = bulge; // A point landing back on the FIRST vertex (endpoint snap) closes the // polyline instead of stacking a duplicate vertex there — the segment @@ -345,19 +916,12 @@ impl CadCommand for PlineCommand { || (self.vertices.len() == 2 && matches!(self.mode, SegMode::Arc)); let d2 = (pt.x - first.x).powi(2) + (pt.y - first.y).powi(2); if enough && d2 < 1e-12 { + self.widths[last_idx] = self.cur_width; return self.sync_live(true, true); } } - self.vertices.push(pt); - self.bulges.push(0.0); - // Publish to the document as soon as the polyline has a segment so it - // becomes snappable while the rest is drawn. (#119) - if self.vertices.len() >= 2 { - self.sync_live(false, false) - } else { - CmdResult::NeedPoint - } + self.push_vertex(pt) } fn set_live_handle(&mut self, handle: Handle) { @@ -365,6 +929,22 @@ impl CadCommand for PlineCommand { } fn on_enter(&mut self) -> CmdResult { + match self.sub { + // Enter keeps the offered default width. + Sub::WidthStart { half } => { + let start = self.cur_width.0; + self.sub = Sub::WidthEnd { half, start }; + return CmdResult::NeedPoint; + } + Sub::WidthEnd { start, .. } => { + self.cur_width = (start, start); + super::super::defaults::set_pline_width(start); + return self.leave_sub(); + } + // Enter at any other sub-prompt backs out of it. + Sub::None => {} + _ => return self.leave_sub(), + } // Every landed vertex is already published into the live document // entity. Finishing must only close its history/command state; replacing // the identical polyline again would advance geometry_epoch and make @@ -380,6 +960,11 @@ impl CadCommand for PlineCommand { } fn on_escape(&mut self) -> CmdResult { + // Escape inside a keyword sub-prompt returns to the next-point step; + // a second Escape ends the command keeping what is drawn. + if self.sub != Sub::None { + return self.leave_sub(); + } match self.live_handle { Some(handle) => CmdResult::FinalizeLiveEntity(handle), None => CmdResult::Cancel, @@ -394,31 +979,148 @@ impl CadCommand for PlineCommand { } fn wants_text_input(&self) -> bool { - // Accept A / L / C once we have at least the first point. + // Keywords once the first point is placed; typed values in the + // scalar sub-steps. !self.vertices.is_empty() } fn point_step_accepts_keywords(&self) -> bool { - // Each segment is a point pick that also accepts A / L / C / U, so the - // polar dynamic-input distance/angle stays visible. - !self.vertices.is_empty() + // Each segment is a point pick that also accepts keywords, so the + // polar dynamic-input distance/angle stays visible. Scalar sub-steps + // are pure typed-value prompts. + !self.vertices.is_empty() && !self.sub.is_scalar() + } + + fn dyn_field(&self) -> crate::command::DynField { + use crate::command::DynField; + match self.sub { + Sub::WidthStart { .. } | Sub::WidthEnd { .. } => DynField::Scalar, + Sub::Length | Sub::ArcRadius | Sub::ArcAngleRadius { .. } | Sub::ArcCenterLength { .. } => { + DynField::Distance + } + sub if sub.is_angle() => DynField::Angle, + _ => DynField::Point, + } + } + + fn dyn_commit_as_text(&self) -> bool { + self.sub.is_scalar() + } + + fn dyn_live_value(&self, cursor: DVec3) -> Option { + let (a, _) = self.last_local()?; + let p = self.local2(cursor); + match self.sub { + // Length: the cursor's projection onto the continuation direction. + Sub::Length => Some((p - a).dot(self.tangent2())), + Sub::ArcRadius | Sub::ArcAngleRadius { .. } | Sub::ArcCenterLength { .. } => { + Some((p - a).length()) + } + _ => None, + } } fn on_text_input(&mut self, text: &str) -> Option { - match text.trim().to_uppercase().as_str() { - "A" | "ARC" => { - self.mode = SegMode::Arc; - Some(CmdResult::NeedPoint) + let upper = text.trim().to_uppercase(); + if self.vertices.is_empty() { + return None; + } + // Keywords valid at any step: Undo backs out of a sub-prompt first, + // then pops vertices. + match upper.as_str() { + "U" | "UNDO" => { + return Some(if self.sub != Sub::None { + self.leave_sub() + } else { + self.undo_last_vertex() + }); } - "L" | "LINE" => { - self.mode = SegMode::Line; - Some(CmdResult::NeedPoint) + _ => {} + } + match self.sub { + Sub::None => match (self.mode, upper.as_str()) { + (_, "W" | "WIDTH") => { + self.sub = Sub::WidthStart { half: false }; + Some(CmdResult::NeedPoint) + } + (_, "H" | "HALFWIDTH") => { + self.sub = Sub::WidthStart { half: true }; + Some(CmdResult::NeedPoint) + } + (SegMode::Line, "A" | "ARC") => { + self.mode = SegMode::Arc; + Some(CmdResult::NeedPoint) + } + (SegMode::Line, "C" | "CLOSE") => Some(self.close()), + (SegMode::Line, "L" | "LENGTH") => { + self.sub = Sub::Length; + Some(CmdResult::NeedPoint) + } + (SegMode::Arc, "L" | "LINE") => { + self.mode = SegMode::Line; + Some(CmdResult::NeedPoint) + } + (SegMode::Arc, "A" | "ANGLE") => { + self.sub = Sub::ArcAngle; + Some(CmdResult::NeedPoint) + } + (SegMode::Arc, "CE" | "CENTER" | "CENTRE") => { + self.sub = Sub::ArcCenter; + Some(CmdResult::NeedPoint) + } + // Commercial solutions want CL in arc mode; a bare C is accepted as well. + (SegMode::Arc, "CL" | "C" | "CLOSE") => Some(self.close()), + (SegMode::Arc, "D" | "DIRECTION") => { + self.sub = Sub::ArcDirection; + Some(CmdResult::NeedPoint) + } + (SegMode::Arc, "R" | "RADIUS") => { + self.sub = Sub::ArcRadius; + Some(CmdResult::NeedPoint) + } + (SegMode::Arc, "S" | "SECOND" | "SECONDPT") => { + self.sub = Sub::ArcSecond; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + Sub::ArcAngleEnd { angle } => match upper.as_str() { + "CE" | "CENTER" | "CENTRE" => { + self.sub = Sub::ArcAngleCenter { angle }; + Some(CmdResult::NeedPoint) + } + "R" | "RADIUS" => { + self.sub = Sub::ArcAngleRadius { angle }; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + Sub::ArcCenterEnd { c } => match upper.as_str() { + "A" | "ANGLE" => { + self.sub = Sub::ArcCenterAngle { c }; + Some(CmdResult::NeedPoint) + } + "L" | "LENGTH" => { + self.sub = Sub::ArcCenterLength { c }; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + Sub::ArcRadiusEnd { r } => match upper.as_str() { + "A" | "ANGLE" => { + self.sub = Sub::ArcRadiusAngle { r }; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + sub if sub.is_scalar() => { + let value = if sub.is_angle() { + crate::entities::common::parse_typed_angle(text.trim()).map(f64::to_degrees) + } else { + crate::entities::common::parse_typed_length(text.trim()) + }?; + self.on_scalar(value) } - "C" | "CLOSE" => Some(self.sync_live(true, true)), - // Undo: drop the last placed vertex but keep drawing (#352). - // Repeatable — each U backs off one more segment, down to (and - // including) the start point. - "U" | "UNDO" => Some(self.undo_last_vertex()), _ => None, } } @@ -428,94 +1130,49 @@ impl CadCommand for PlineCommand { // yet the document undo takes over. if self.vertices.is_empty() { None + } else if self.sub != Sub::None { + Some(self.leave_sub()) } else { Some(self.undo_last_vertex()) } } fn on_mouse_move(&mut self, pt: DVec3) -> Option { - let pt_world = pt; // The committed vertices already render as a real document entity, so // the preview is just the pending segment from the last vertex to the - // cursor. (#119) Downcast to f32 for the pixel-space rubber band. - let last_world = *self.vertices.last()?; - let last = self.plane.to_local(last_world).as_vec3(); - let pt = self.plane.to_local(pt_world).as_vec3(); - - let mut pts: Vec<[f32; 3]> = Vec::new(); - let mut tangent_geom = None; - match self.mode { - SegMode::Line => { - pts.push([last.x, last.y, last.z]); - pts.push([pt.x, pt.y, pt.z]); + // cursor. (#119) + let (a, z) = self.last_local()?; + let p = self.local2(pt); + match self.sub { + Sub::None => match self.mode { + SegMode::Line => Some(self.line_wire(a, p, z)), + SegMode::Arc => { + let bulge = compute_bulge(a, self.tangent2(), p); + Some(self.arc_wire(a, p, z, bulge)) + } + }, + // The continuation line, as far as the cursor projects onto it. + Sub::Length => { + let e = a + self.tangent2() * (p - a).dot(self.tangent2()); + Some(self.line_wire(a, e, z)) } - SegMode::Arc => { - let a = DVec2::new(last.x as f64, last.y as f64); - let b = DVec2::new(pt.x as f64, pt.y as f64); - let tangent = self - .last_tangent - .map(|t| t.as_dvec2()) - .unwrap_or(DVec2::new(1.0, 0.0)); - let bulge = compute_bulge(a, tangent, b); - if bulge.abs() >= 1e-9 && (b - a).length_squared() >= 1e-12 { - if let Some(arc) = - crate::entities::common::BulgeArc::from_bulge([a.x, a.y], [b.x, b.y], bulge) - { - for s in arc.tessellate_angle(std::f64::consts::TAU / 64.0) { - pts.push([s[0] as f32, s[1] as f32, last.z]); - } - let (sa, ea) = if arc.sweep >= 0.0 { - (arc.start_angle, arc.end_angle) - } else { - (arc.end_angle, arc.start_angle) - }; - let world_center = self - .plane - .to_world(DVec3::new(arc.center[0], arc.center[1], last.z as f64)); - if arc.radius > 0.0 - && arc.radius.is_finite() - && arc.radius <= 1e6 - && sa.is_finite() - && ea.is_finite() - { - tangent_geom = Some(TangentGeom::Arc { - center: world_center.to_array(), - axis_x: self.plane.x.to_array(), - axis_y: self.plane.y.to_array(), - radius: arc.radius, - start_angle: sa, - end_angle: ea, - }); - } - } else { - let arc_pts = arc_sample_points(last, bulge, pt, 64); - pts.extend_from_slice(&arc_pts); - } - } else { - pts.push([last.x, last.y, last.z]); - pts.push([pt.x, pt.y, pt.z]); + // Picks that only fix a centre / direction / second point show + // the radius / tangent / chord line to the cursor. + Sub::ArcCenter | Sub::ArcDirection | Sub::ArcSecond | Sub::ArcAngleCenter { .. } => { + match self.arc_for(self.sub, a, p) { + Some((e, bulge)) => Some(self.arc_wire(a, e, z, bulge)), + None => Some(self.line_wire(a, p, z)), } } + sub if sub.is_scalar() => None, + sub => match self.arc_for(sub, a, p) { + Some((e, bulge)) => Some(self.arc_wire(a, e, z, bulge)), + None => Some(self.line_wire(a, p, z)), + }, } - - let world_points = pts - .into_iter() - .map(|point| self.plane.to_world(Vec3::from_array(point).as_dvec3()).as_vec3().to_array()) - .collect(); - let mut wire = WireModel::solid( - "rubber_band".into(), - world_points, - WireModel::CYAN, - false, - ); - if let Some(tg) = tangent_geom { - wire.tangent_geoms.push(tg); - } - Some(wire) } } - // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["PLINE"] }); // PlineCommand @@ -590,6 +1247,209 @@ mod tests { )); } + fn pl(points: &[(f64, f64)]) -> PlineCommand { + let mut cmd = PlineCommand::new(); + for &(x, y) in points { + cmd.on_point(DVec3::new(x, y, 0.0)); + } + cmd.set_live_handle(Handle::new(1)); + cmd + } + + fn entity_of(res: CmdResult) -> LwPolyline { + match res { + CmdResult::UpdateLiveEntity { + entity: EntityType::LwPolyline(p), + .. + } + | CmdResult::CommitLiveEntity(EntityType::LwPolyline(p)) => p, + other => panic!("expected a polyline result, got {}", std::any::type_name_of_val(&other)), + } + } + + fn keywords(cmd: &PlineCommand) -> Vec { + cmd.options().into_iter().map(|o| o.keyword).collect() + } + + #[test] + fn options_per_mode_match_compatible_keywords() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + assert_eq!(keywords(&cmd), ["A", "H", "L", "U", "W", ""]); + cmd.on_point(DVec3::new(10.0, 5.0, 0.0)); + assert_eq!(keywords(&cmd), ["A", "C", "H", "L", "U", "W", ""]); + cmd.on_text_input("A"); + assert_eq!( + keywords(&cmd), + ["A", "CE", "CL", "D", "H", "L", "R", "S", "U", "W", ""] + ); + cmd.on_text_input("A"); + assert!(keywords(&cmd).is_empty(), "typed-angle prompt has no keywords"); + cmd.on_text_input("90"); + assert_eq!(keywords(&cmd), ["CE", "R"]); + cmd.on_text_input("CE"); + assert!(keywords(&cmd).is_empty()); + } + + #[test] + fn width_option_sets_segment_widths_in_entity() { + let mut cmd = pl(&[(0.0, 0.0)]); + assert!(matches!(cmd.on_text_input("W"), Some(CmdResult::NeedPoint))); + assert!(matches!(cmd.sub, Sub::WidthStart { half: false })); + assert!(matches!(cmd.on_text_input("1"), Some(CmdResult::NeedPoint))); + assert!(matches!(cmd.sub, Sub::WidthEnd { half: false, .. })); + assert!(matches!(cmd.on_text_input("2"), Some(CmdResult::NeedPoint))); + assert!(matches!(cmd.sub, Sub::None)); + let e = entity_of(cmd.on_point(DVec3::new(10.0, 0.0, 0.0))); + assert_eq!((e.vertices[0].start_width, e.vertices[0].end_width), (1.0, 2.0)); + // The end width carries over as the next segment's start width. + let e = entity_of(cmd.on_point(DVec3::new(20.0, 0.0, 0.0))); + assert_eq!((e.vertices[1].start_width, e.vertices[1].end_width), (2.0, 2.0)); + assert_eq!(super::super::super::defaults::get_pline_width(), 2.0); + super::super::super::defaults::set_pline_width(0.0); + } + + #[test] + fn halfwidth_doubles_value_and_enter_keeps_default() { + let mut cmd = pl(&[(0.0, 0.0)]); + cmd.on_text_input("H"); + assert!(matches!(cmd.sub, Sub::WidthStart { half: true })); + cmd.on_text_input("0.5"); + // Enter at the ending prompt keeps the starting value. + assert!(matches!(cmd.on_enter(), CmdResult::NeedPoint)); + assert_eq!(cmd.cur_width, (1.0, 1.0)); + super::super::super::defaults::set_pline_width(0.0); + } + + #[test] + fn length_extends_along_last_tangent() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("L"); + assert!(matches!(cmd.sub, Sub::Length)); + let e = entity_of(cmd.on_text_input("5").expect("length consumed")); + assert_eq!(e.vertices.len(), 3); + assert!((e.vertices[2].location.x - 15.0).abs() < 1e-9); + assert!(e.vertices[2].location.y.abs() < 1e-9); + } + + #[test] + fn length_after_arc_follows_exit_tangent() { + // Line east, then a tangent semicircle up to (10,10): exit heads west. + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_point(DVec3::new(10.0, 10.0, 0.0)); + cmd.on_text_input("L"); // back to Line mode + cmd.on_text_input("L"); // Length + let e = entity_of(cmd.on_text_input("5").expect("length consumed")); + assert!((e.vertices[3].location.x - 5.0).abs() < 1e-6, "{:?}", e.vertices[3].location); + assert!((e.vertices[3].location.y - 10.0).abs() < 1e-6); + } + + #[test] + fn arc_close_closes_with_bulge() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]); + cmd.on_text_input("A"); + let res = cmd.on_text_input("CL").expect("CLose consumed"); + match res { + CmdResult::UpdateLiveEntity { entity: EntityType::LwPolyline(p), finish, .. } => { + assert!(finish); + assert!(p.is_closed); + assert!(p.vertices[2].bulge.abs() > 1e-6, "closing segment should be an arc"); + } + _ => panic!("expected a finishing update"), + } + } + + #[test] + fn arc_angle_endpoint_bulge_is_tan_quarter() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_text_input("A"); + cmd.on_text_input("90"); + let e = entity_of(cmd.on_point(DVec3::new(10.0, 10.0, 0.0))); + let expect = (std::f64::consts::FRAC_PI_2 / 4.0).tan(); + assert!((e.vertices[1].bulge - expect).abs() < 1e-9); + assert_eq!(e.vertices.len(), 3); + } + + #[test] + fn arc_center_endpoint_projects_to_radius() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_text_input("CE"); + assert!(matches!(cmd.sub, Sub::ArcCenter)); + cmd.on_point(DVec3::new(10.0, 5.0, 0.0)); + assert_eq!(keywords(&cmd), ["A", "L"]); + let e = entity_of(cmd.on_point(DVec3::new(20.0, 5.0, 0.0))); + let v = e.vertices[2].location; + assert!((v.x - 15.0).abs() < 1e-9 && (v.y - 5.0).abs() < 1e-9, "{v:?}"); + let expect = (std::f64::consts::FRAC_PI_2 / 4.0).tan(); + assert!((e.vertices[1].bulge - expect).abs() < 1e-9, "quarter turn CCW"); + } + + #[test] + fn arc_center_length_uses_chord() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_text_input("CE"); + cmd.on_point(DVec3::new(10.0, 5.0, 0.0)); + cmd.on_text_input("L"); + // Chord 10 on a radius 5 circle: a semicircle ending at (10,10). + let e = entity_of(cmd.on_text_input("10").expect("chord length")); + let v = e.vertices[2].location; + assert!((v.x - 10.0).abs() < 1e-6 && (v.y - 10.0).abs() < 1e-6, "{v:?}"); + } + + #[test] + fn arc_radius_rejects_long_chord() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_text_input("R"); + cmd.on_text_input("3"); + assert!(matches!(cmd.on_point(DVec3::new(20.0, 0.0, 0.0)), CmdResult::NeedPoint)); + assert_eq!(cmd.vertices.len(), 2, "chord longer than the diameter is refused"); + let e = entity_of(cmd.on_point(DVec3::new(16.0, 0.0, 0.0))); + assert_eq!(e.vertices.len(), 3); + assert!(e.vertices[1].bulge.abs() > 0.0); + } + + #[test] + fn arc_second_point_three_point_arc_passes_through_second() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_text_input("S"); + cmd.on_point(DVec3::new(15.0, 5.0, 0.0)); + let e = entity_of(cmd.on_point(DVec3::new(20.0, 0.0, 0.0))); + // Semicircle over the chord, running clockwise (bulge -1). + assert!((e.vertices[1].bulge + 1.0).abs() < 1e-9, "{}", e.vertices[1].bulge); + } + + #[test] + fn arc_direction_uses_picked_tangent() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_text_input("D"); + cmd.on_point(DVec3::new(10.0, 10.0, 0.0)); // tangent +Y + let e = entity_of(cmd.on_point(DVec3::new(20.0, 0.0, 0.0))); + assert!((e.vertices[1].bulge + 1.0).abs() < 1e-9, "{}", e.vertices[1].bulge); + } + + #[test] + fn undo_in_substep_only_leaves_substep() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("W"); + assert!(matches!(cmd.on_text_input("U"), Some(CmdResult::NeedPoint))); + assert!(matches!(cmd.sub, Sub::None)); + assert_eq!(cmd.vertices.len(), 2); + // Escape likewise backs out of a sub-prompt without finishing. + cmd.on_text_input("A"); + cmd.on_text_input("R"); + assert!(matches!(cmd.on_escape(), CmdResult::NeedPoint)); + assert!(matches!(cmd.sub, Sub::None)); + // A second Undo pops the vertex. + cmd.on_text_input("U"); + assert_eq!(cmd.vertices.len(), 1); + } + #[test] fn test_draw_line_then_arc() { let mut cmd = PlineCommand::new(); diff --git a/src/modules/draw/draw/trace.rs b/src/modules/draw/draw/trace.rs index 16e3f9116..c0b4c85e3 100644 --- a/src/modules/draw/draw/trace.rs +++ b/src/modules/draw/draw/trace.rs @@ -144,6 +144,15 @@ impl CadCommand for TraceCommand { self.width_set && !self.points.is_empty() } + fn options(&self) -> Vec { + use crate::command::CmdOption; + if self.width_set && !self.points.is_empty() { + vec![CmdOption::new("Undo", "U")] + } else { + Vec::new() + } + } + fn on_text_input(&mut self, text: &str) -> Option { let t = text.trim(); if !self.width_set { diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index fd5844d51..73a355ff7 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -227,6 +227,19 @@ impl CadCommand for MeasureGeomCommand { self.mode == Mode::Choose } + fn options(&self) -> Vec { + use crate::command::CmdOption; + if self.mode != Mode::Choose { + return Vec::new(); + } + vec![ + CmdOption::new("Distance", "D"), + CmdOption::new("Radius", "R"), + CmdOption::new("Angle", "A"), + CmdOption::new("ARea", "AR"), + ] + } + fn on_text_input(&mut self, text: &str) -> Option { if self.mode != Mode::Choose { return None; diff --git a/src/modules/draw/modify/copy.rs b/src/modules/draw/modify/copy.rs index 130cf48dc..95bce7e51 100644 --- a/src/modules/draw/modify/copy.rs +++ b/src/modules/draw/modify/copy.rs @@ -28,6 +28,8 @@ pub fn tool() -> ToolDef { enum Step { Base, Placing(DVec3), + /// Displacement option: the next point / typed coordinate is the vector. + Displacement, } pub struct CopyCommand { @@ -39,6 +41,10 @@ pub struct CopyCommand { array_count: Option, /// True while the next typed value is captured as the array item count. awaiting_count: bool, + /// True while the mOde prompt (Single / Multiple) is up. + awaiting_mode: bool, + /// COPYMODE: Single ends the command after the first copy. + single: bool, } impl CopyCommand { @@ -50,6 +56,8 @@ impl CopyCommand { count: 0, array_count: None, awaiting_count: false, + awaiting_mode: false, + single: false, } } } @@ -63,6 +71,9 @@ impl CadCommand for CopyCommand { if self.awaiting_count { return crate::tr!("command-copy", "array-count"); } + if self.awaiting_mode { + return crate::tr!("command-copy", "mode"); + } match &self.step { Step::Base => crate::tr!( "command-copy", "base", @@ -85,6 +96,35 @@ impl CadCommand for CopyCommand { ) } } + Step::Displacement => crate::tr!("command-copy", "displacement"), + } + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + if self.awaiting_count { + return Vec::new(); + } + if self.awaiting_mode { + return vec![ + CmdOption::new("Single", "S"), + CmdOption::new("Multiple", "M"), + ]; + } + match self.step { + Step::Base => vec![ + CmdOption::new("Displacement", "D"), + CmdOption::new("mOde", "O"), + ], + Step::Placing(_) => { + let mut opts = vec![CmdOption::new("Array", "A")]; + if self.count > 0 { + opts.push(CmdOption::new("Undo", "U")); + } + opts.push(CmdOption::enter("Exit")); + opts + } + Step::Displacement => Vec::new(), } } @@ -106,6 +146,12 @@ impl CadCommand for CopyCommand { .map(|k| EntityTransform::Translate(delta * k as f64)) .collect(); CmdResult::BatchCopy(self.handles.clone(), transforms) + } else if self.single { + // Single mode: one copy, then done. + CmdResult::BatchCopy( + self.handles.clone(), + vec![EntityTransform::Translate(delta)], + ) } else { self.count += 1; CmdResult::CopySelected( @@ -114,15 +160,24 @@ impl CadCommand for CopyCommand { ) } } + // The point (typically typed as `dx,dy`) is the displacement itself. + Step::Displacement => CmdResult::BatchCopy( + self.handles.clone(), + vec![EntityTransform::Translate(pt)], + ), } } fn on_enter(&mut self) -> CmdResult { - // Cancel an in-progress Array count entry; otherwise finish. + // Cancel an in-progress Array count / mode entry; otherwise finish. if self.awaiting_count { self.awaiting_count = false; return CmdResult::NeedPoint; } + if self.awaiting_mode { + self.awaiting_mode = false; + return CmdResult::NeedPoint; + } CmdResult::Cancel } fn on_escape(&mut self) -> CmdResult { @@ -130,13 +185,15 @@ impl CadCommand for CopyCommand { } fn wants_text_input(&self) -> bool { - matches!(self.step, Step::Placing(_)) + matches!(self.step, Step::Placing(_) | Step::Base) } fn point_step_accepts_keywords(&self) -> bool { - // Accept the Array keyword while placing, but route the typed item - // count straight through on_text_input. - matches!(self.step, Step::Placing(_)) && !self.awaiting_count + // Keywords ride along with the point picks, but a typed item count + // or mode answer goes straight through on_text_input. + matches!(self.step, Step::Placing(_) | Step::Base) + && !self.awaiting_count + && !self.awaiting_mode } fn on_text_input(&mut self, text: &str) -> Option { @@ -149,15 +206,46 @@ impl CadCommand for CopyCommand { self.awaiting_count = false; return Some(CmdResult::NeedPoint); } - match text.trim().to_uppercase().as_str() { - "A" | "ARRAY" => { + let upper = text.trim().to_uppercase(); + if self.awaiting_mode { + match upper.as_str() { + "S" | "SINGLE" => self.single = true, + "M" | "MULTIPLE" => self.single = false, + _ => return Some(CmdResult::NeedPoint), + } + self.awaiting_mode = false; + return Some(CmdResult::NeedPoint); + } + match (&self.step, upper.as_str()) { + (Step::Base, "D" | "DISPLACEMENT") => { + self.step = Step::Displacement; + Some(CmdResult::NeedPoint) + } + (Step::Base, "O" | "MODE") => { + self.awaiting_mode = true; + Some(CmdResult::NeedPoint) + } + (Step::Placing(_), "A" | "ARRAY") => { self.awaiting_count = true; Some(CmdResult::NeedPoint) } + // Undo: take back the last copy placed by this command. + (Step::Placing(_), "U" | "UNDO") if self.count > 0 => { + self.count -= 1; + Some(CmdResult::UndoDocument) + } _ => None, } } + fn on_undo_step(&mut self) -> Option { + if matches!(self.step, Step::Placing(_)) && self.count > 0 { + self.on_text_input("U") + } else { + None + } + } + fn on_preview_wires(&mut self, pt: DVec3) -> Vec { let Step::Placing(base) = &self.step else { return vec![]; @@ -180,3 +268,53 @@ impl CadCommand for CopyCommand { out } } + +#[cfg(test)] +mod tests { + use super::*; + + fn keywords(cmd: &CopyCommand) -> Vec { + cmd.options().into_iter().map(|o| o.keyword).collect() + } + + #[test] + fn mode_single_exits_after_one_copy() { + let mut cmd = CopyCommand::new(vec![Handle::new(1)], vec![]); + assert_eq!(keywords(&cmd), ["D", "O"]); + assert!(matches!(cmd.on_text_input("O"), Some(CmdResult::NeedPoint))); + assert_eq!(keywords(&cmd), ["S", "M"]); + assert!(matches!(cmd.on_text_input("S"), Some(CmdResult::NeedPoint))); + cmd.on_point(DVec3::ZERO); + match cmd.on_point(DVec3::new(10.0, 0.0, 0.0)) { + CmdResult::BatchCopy(_, transforms) => assert_eq!(transforms.len(), 1), + _ => panic!("single mode should copy once and end"), + } + } + + #[test] + fn multiple_mode_keeps_copying_and_undo_takes_one_back() { + let mut cmd = CopyCommand::new(vec![Handle::new(1)], vec![]); + cmd.on_point(DVec3::ZERO); + assert_eq!(keywords(&cmd), ["A", ""], "no Undo before a copy is placed"); + assert!(matches!( + cmd.on_point(DVec3::new(10.0, 0.0, 0.0)), + CmdResult::CopySelected(..) + )); + assert_eq!(keywords(&cmd), ["A", "U", ""]); + assert!(matches!(cmd.on_text_input("U"), Some(CmdResult::UndoDocument))); + assert_eq!(keywords(&cmd), ["A", ""]); + assert!(cmd.on_text_input("U").is_none(), "nothing left to undo"); + } + + #[test] + fn displacement_option_copies_by_vector() { + let mut cmd = CopyCommand::new(vec![Handle::new(1)], vec![]); + cmd.on_text_input("D"); + match cmd.on_point(DVec3::new(1.0, 2.0, 0.0)) { + CmdResult::BatchCopy(_, transforms) => { + assert!(matches!(transforms[0], EntityTransform::Translate(d) if d == DVec3::new(1.0, 2.0, 0.0))); + } + _ => panic!("expected one copy by the displacement"), + } + } +} diff --git a/src/modules/draw/modify/fillet.rs b/src/modules/draw/modify/fillet.rs index ec19c9728..1ca03fd84 100644 --- a/src/modules/draw/modify/fillet.rs +++ b/src/modules/draw/modify/fillet.rs @@ -1320,6 +1320,8 @@ pub struct FilletCommand { /// (i.e. "R" pressed after the first object was already picked), so the /// command resumes at the second pick instead of restarting selection. resume_second: Option<(Handle, FilletEntity, [f64; 2])>, + /// Fillets applied by this command that Undo can still take back. + made: usize, } impl FilletCommand { @@ -1335,6 +1337,7 @@ impl FilletCommand { all_entities, entity_index, resume_second: None, + made: 0, } } @@ -1372,8 +1375,20 @@ impl FilletCommand { self.entity_index = ModifyEntityIndex::build(&self.all_entities); self.step = FilletStep::First; self.resume_second = None; + self.made += 1; CmdResult::ReplaceManyContinue(replacements) } + + /// Undo option: take back the last fillet of this command. The host + /// undoes the document step and then hands the restored entities back + /// through `on_document_undone`. + fn undo_last(&mut self) -> Option { + if self.made == 0 { + return None; + } + self.made -= 1; + Some(CmdResult::UndoDocument) + } } impl CadCommand for FilletCommand { @@ -1433,10 +1448,15 @@ impl CadCommand for FilletCommand { use crate::command::CmdOption; match self.step { - FilletStep::First => vec![ - CmdOption::new("Polyline", "P"), - CmdOption::new("Radius", "R"), - ], + FilletStep::First => { + let mut opts = Vec::new(); + if self.made > 0 { + opts.push(CmdOption::new("Undo", "U")); + } + opts.push(CmdOption::new("Polyline", "P")); + opts.push(CmdOption::new("Radius", "R")); + opts + } FilletStep::Second { .. } => { vec![CmdOption::new("Radius", "R")] } @@ -1485,6 +1505,9 @@ impl CadCommand for FilletCommand { self.step = FilletStep::Polyline; return Some(CmdResult::NeedPoint); } + if upper == "U" || upper == "UNDO" { + return self.undo_last(); + } // "R" alone → enter sub-step to collect radius if upper == "R" { self.enter_radius_substep(); @@ -1520,6 +1543,20 @@ impl CadCommand for FilletCommand { ) } + fn on_undo_step(&mut self) -> Option { + self.undo_last() + } + + fn on_document_undone(&mut self, document: &acadrust::CadDocument) { + self.all_entities = document + .entities() + .map(crate::entities::curve::entity_with_lwpolyline_world_xy) + .collect(); + self.entity_index = ModifyEntityIndex::build(&self.all_entities); + self.step = FilletStep::First; + self.resume_second = None; + } + fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult { if handle.is_null() { return CmdResult::NeedPoint; @@ -1848,6 +1885,10 @@ pub struct ChamferCommand { /// distance entry made mid-selection, so the command resumes at the /// second pick instead of restarting selection. resume_pick: Option, + /// Multiple option: keep chamfering until Enter / Esc. + multiple: bool, + /// Chamfers applied by this command that Undo can still take back. + made: usize, } impl ChamferCommand { @@ -1864,6 +1905,8 @@ impl ChamferCommand { all_entities, entity_index, resume_pick: None, + multiple: false, + made: 0, } } @@ -1890,6 +1933,37 @@ impl ChamferCommand { fn resume_after_dist(&mut self) { self.step = self.resume_pick.take().unwrap_or(ChamferStep::First); } + + /// Publish a chamfer. Single mode replaces and ends; Multiple mode + /// replaces, refreshes the cached entities and returns to the first pick. + fn commit(&mut self, replacements: Vec<(Handle, Vec)>) -> CmdResult { + if !self.multiple { + return CmdResult::ReplaceMany(replacements, vec![]); + } + self.all_entities.retain(|entity| { + !replacements + .iter() + .any(|(handle, _)| entity.common().handle == *handle) + }); + self.all_entities.extend( + replacements + .iter() + .flat_map(|(_, entities)| entities.iter().cloned()), + ); + self.entity_index = ModifyEntityIndex::build(&self.all_entities); + self.step = ChamferStep::First; + self.resume_pick = None; + self.made += 1; + CmdResult::ReplaceManyContinue(replacements) + } + + fn undo_last(&mut self) -> Option { + if self.made == 0 { + return None; + } + self.made -= 1; + Some(CmdResult::UndoDocument) + } } impl CadCommand for ChamferCommand { @@ -1944,10 +2018,19 @@ impl CadCommand for ChamferCommand { use crate::command::CmdOption; match self.step { - ChamferStep::First - | ChamferStep::Second { .. } - | ChamferStep::SecondPoly { .. } => { - vec![CmdOption::new(t!("Distance").as_ref(), "D")] + ChamferStep::First => { + let mut opts = Vec::new(); + if self.made > 0 { + opts.push(CmdOption::new("Undo", "U")); + } + opts.push(CmdOption::new("Distance", "D")); + if !self.multiple { + opts.push(CmdOption::new("Multiple", "M")); + } + opts + } + ChamferStep::Second { .. } | ChamferStep::SecondPoly { .. } => { + vec![CmdOption::new("Distance", "D")] } ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => vec![], } @@ -2010,6 +2093,16 @@ impl CadCommand for ChamferCommand { | ChamferStep::SecondPoly { .. } => { let t = text.trim(); let upper = t.to_uppercase(); + if matches!(self.step, ChamferStep::First) { + match upper.as_str() { + "M" | "MULTIPLE" => { + self.multiple = true; + return Some(CmdResult::NeedPoint); + } + "U" | "UNDO" => return self.undo_last(), + _ => {} + } + } // "D" alone → enter sub-step to collect distances if upper == "D" { self.enter_dist_substep(); @@ -2045,6 +2138,20 @@ impl CadCommand for ChamferCommand { } } + fn on_undo_step(&mut self) -> Option { + self.undo_last() + } + + fn on_document_undone(&mut self, document: &acadrust::CadDocument) { + self.all_entities = document + .entities() + .map(crate::entities::curve::entity_with_lwpolyline_world_xy) + .collect(); + self.entity_index = ModifyEntityIndex::build(&self.all_entities); + self.step = ChamferStep::First; + self.resume_pick = None; + } + fn needs_entity_pick(&self) -> bool { !matches!( self.step, @@ -2093,7 +2200,7 @@ impl CadCommand for ChamferCommand { return CmdResult::NeedPoint; } match chamfer_lwpoly_corner(&poly, click1, self.dist1, click, self.dist2) { - Some(new_poly) => CmdResult::ReplaceMany(vec![(h1, vec![new_poly])], vec![]), + Some(new_poly) => self.commit(vec![(h1, vec![new_poly])]), None => CmdResult::NeedPoint, } } @@ -2118,10 +2225,22 @@ impl CadCommand for ChamferCommand { if let Some(l2) = l2 { match compute_chamfer(&l1, click1, self.dist1, &l2, click, self.dist2) { - Some((new_l1, new_l2, chamfer_line)) => CmdResult::ReplaceMany( - vec![(h1, vec![new_l1]), (handle, vec![new_l2])], - vec![chamfer_line], - ), + Some((new_l1, new_l2, chamfer_line)) => { + // Single mode adds the chamfer line as a new entity; + // Multiple folds it into the first line's replacement + // so the command can carry on. + if self.multiple { + self.commit(vec![ + (h1, vec![new_l1, chamfer_line]), + (handle, vec![new_l2]), + ]) + } else { + CmdResult::ReplaceMany( + vec![(h1, vec![new_l1]), (handle, vec![new_l2])], + vec![chamfer_line], + ) + } + } None => CmdResult::NeedPoint, } } else { @@ -2245,6 +2364,61 @@ mod tests { EntityType::Line(line) } + fn keywords(cmd: &dyn CadCommand) -> Vec { + cmd.options().into_iter().map(|o| o.keyword).collect() + } + + #[test] + fn fillet_undo_option_appears_after_a_fillet_and_refreshes_from_document() { + let mut command = FilletCommand::new( + 1.0, + vec![line(0.0, 0.0, 10.0, 0.0, 1), line(0.0, 0.0, 0.0, 10.0, 2)], + ); + assert_eq!(keywords(&command), ["P", "R"]); + assert!(command.on_text_input("U").is_none(), "nothing to undo yet"); + command.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)); + assert!(matches!( + command.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)), + CmdResult::ReplaceManyContinue(_) + )); + assert_eq!(keywords(&command), ["U", "P", "R"]); + assert!(matches!(command.on_text_input("U"), Some(CmdResult::UndoDocument))); + assert_eq!(keywords(&command), ["P", "R"]); + // The host hands the restored document back; the cache follows it. + let mut doc = acadrust::CadDocument::new(); + doc.add_entity(line(0.0, 0.0, 20.0, 0.0, 7)); + command.on_document_undone(&doc); + assert_eq!(command.all_entities.len(), 1); + assert_eq!(command.all_entities[0].common().handle, Handle::new(7)); + } + + #[test] + fn chamfer_multiple_keeps_going_and_single_ends() { + let lines = || vec![line(0.0, 0.0, 10.0, 0.0, 1), line(0.0, 0.0, 0.0, 10.0, 2)]; + let mut single = ChamferCommand::new(1.0, lines()); + assert_eq!(keywords(&single), ["D", "M"]); + single.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)); + assert!(matches!( + single.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)), + CmdResult::ReplaceMany(..) + )); + + let mut multi = ChamferCommand::new(1.0, lines()); + assert!(matches!(multi.on_text_input("M"), Some(CmdResult::NeedPoint))); + assert_eq!(keywords(&multi), ["D"], "Multiple is not offered twice"); + multi.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)); + match multi.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)) { + CmdResult::ReplaceManyContinue(replacements) => { + // The chamfer line rides along with the first line's replacement. + assert_eq!(replacements[0].1.len(), 2); + assert_eq!(replacements[1].1.len(), 1); + } + _ => panic!("multiple mode should keep the command active"), + } + assert_eq!(keywords(&multi), ["U", "D"]); + assert!(matches!(multi.on_text_input("U"), Some(CmdResult::UndoDocument))); + } + #[test] fn fillet_continues_with_replacement_entities() { let first = Handle::new(1); diff --git a/src/modules/draw/modify/mirror.rs b/src/modules/draw/modify/mirror.rs index 4b7621e38..2cb2bc2bd 100644 --- a/src/modules/draw/modify/mirror.rs +++ b/src/modules/draw/modify/mirror.rs @@ -125,6 +125,15 @@ impl CadCommand for MirrorCommand { matches!(self.step, Step::AskErase { .. }) } + fn options(&self) -> Vec { + use crate::command::CmdOption; + if matches!(self.step, Step::AskErase { .. }) { + vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")] + } else { + Vec::new() + } + } + fn on_text_input(&mut self, text: &str) -> Option { let Step::AskErase { p1, p2 } = &self.step else { return None; diff --git a/src/modules/draw/modify/offset.rs b/src/modules/draw/modify/offset.rs index a2118f12b..72bead8cd 100644 --- a/src/modules/draw/modify/offset.rs +++ b/src/modules/draw/modify/offset.rs @@ -443,6 +443,24 @@ pub struct OffsetCommand { /// Pre-selected offsettable objects (pick-first, #422); consumed when the /// distance step resolves. preselected: Vec, + /// A Yes/No or Current/Source answer is being collected for this option. + awaiting: Option, + /// Erase option: replace the source object with its offset. + erase_source: bool, + /// Layer option: put offsets on the current layer instead of the source's. + layer_current: bool, + /// The side step's targets are document objects (not chained offsets), so + /// Erase can replace them. + targets_are_sources: bool, + /// Offsets committed by this command that Undo can still take back. + made: usize, +} + +/// Which option prompt is waiting for an answer. +#[derive(Clone, Copy, PartialEq)] +enum Await { + Erase, + Layer, } /// The entity types `compute_offsets` can offset. @@ -468,6 +486,11 @@ impl OffsetCommand { entity_index, picked: None, preselected: Vec::new(), + awaiting: None, + erase_source: false, + layer_current: false, + targets_are_sources: false, + made: 0, } } @@ -481,6 +504,11 @@ impl OffsetCommand { entity_index, picked: None, preselected: targets, + awaiting: None, + erase_source: false, + layer_current: false, + targets_are_sources: false, + made: 0, } } @@ -496,9 +524,24 @@ impl OffsetCommand { locked, multiple: false, }; + self.targets_are_sources = true; } CmdResult::NeedPoint } + + /// Undo option: take back the last offset and return to the object pick. + fn undo_last(&mut self) -> Option { + if self.made == 0 { + return None; + } + self.made -= 1; + let locked = match &self.step { + Step::SelectObject { locked } | Step::PickSide { locked, .. } => *locked, + _ => return None, + }; + self.step = Step::SelectObject { locked }; + Some(CmdResult::UndoDocument) + } fn accept_distance(&mut self, distance: f64) -> CmdResult { let distance = distance.abs().max(1e-9); @@ -515,13 +558,23 @@ impl OffsetCommand { impl CadCommand for OffsetCommand { fn preserve_commit_layer(&self) -> bool { - true + !self.layer_current } fn name(&self) -> &'static str { "OFFSET" } fn prompt(&self) -> String { + match self.awaiting { + Some(Await::Erase) => { + return t!("OFFSET Erase source object after offsetting? [Yes/No] :").into_owned() + } + Some(Await::Layer) => { + return t!("OFFSET Enter layer option for offset objects [Current/Source] :") + .into_owned() + } + None => {} + } match &self.step { Step::Distance => { let d = format!("{:.4}", defaults::get_offset_dist()); @@ -580,21 +633,50 @@ impl CadCommand for OffsetCommand { } fn options(&self) -> Vec { + use crate::command::CmdOption; + match self.awaiting { + Some(Await::Erase) => { + return vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")] + } + Some(Await::Layer) => { + return vec![CmdOption::new("Current", "C"), CmdOption::new("Source", "S")] + } + None => {} + } match &self.step { Step::Distance => vec![ - crate::command::CmdOption::new(t!("Through").as_ref(), "T"), - crate::command::CmdOption::enter(&format!( - "{:.4}", - defaults::get_offset_dist() - )), + CmdOption::new("Through", "T"), + CmdOption::new("Erase", "E"), + CmdOption::new("Layer", "L"), + CmdOption::enter(&format!("{:.4}", defaults::get_offset_dist())), ], - Step::PickSide { - multiple: false, .. - } => vec![crate::command::CmdOption::new(t!("Multiple").as_ref(), "M")], - _ => Vec::new(), + Step::SelectObject { .. } => { + let mut opts = Vec::new(); + if self.made > 0 { + opts.push(CmdOption::new("Undo", "U")); + } + opts.push(CmdOption::enter("Exit")); + opts + } + Step::PickSide { multiple, .. } => { + let mut opts = Vec::new(); + if !multiple { + opts.push(CmdOption::new("Multiple", "M")); + } + if self.made > 0 { + opts.push(CmdOption::new("Undo", "U")); + } + opts.push(CmdOption::enter("Exit")); + opts + } + Step::ReferenceSecond { .. } => Vec::new(), } } + fn on_undo_step(&mut self) -> Option { + self.undo_last() + } + fn needs_entity_pick(&self) -> bool { matches!(self.step, Step::SelectObject { .. }) } @@ -631,6 +713,7 @@ impl CadCommand for OffsetCommand { locked, multiple: false, }; + self.targets_are_sources = true; CmdResult::NeedPoint } _ => CmdResult::NeedPoint, @@ -638,7 +721,7 @@ impl CadCommand for OffsetCommand { } // The distance step takes a typed magnitude; the side step accepts one - // too, re-locking the distance mid-command. + // too, re-locking the distance mid-command. The object pick takes Undo. fn wants_text_input(&self) -> bool { matches!(self.step, Step::Distance | Step::PickSide { .. }) } @@ -667,11 +750,46 @@ impl CadCommand for OffsetCommand { fn on_text_input(&mut self, text: &str) -> Option { let t = text.trim().replace(',', "."); + let upper = t.to_uppercase(); + match self.awaiting { + Some(Await::Erase) => { + match upper.as_str() { + "Y" | "YES" => self.erase_source = true, + "N" | "NO" | "" => self.erase_source = false, + _ => return Some(CmdResult::NeedPoint), + } + self.awaiting = None; + return Some(CmdResult::NeedPoint); + } + Some(Await::Layer) => { + match upper.as_str() { + "C" | "CURRENT" => self.layer_current = true, + "S" | "SOURCE" | "" => self.layer_current = false, + _ => return Some(CmdResult::NeedPoint), + } + self.awaiting = None; + return Some(CmdResult::NeedPoint); + } + None => {} + } + if matches!(upper.as_str(), "U" | "UNDO") + && matches!(self.step, Step::SelectObject { .. } | Step::PickSide { .. }) + { + return self.undo_last(); + } match &mut self.step { Step::Distance => { if t.eq_ignore_ascii_case("t") || t.eq_ignore_ascii_case("through") { return Some(self.advance_from_distance(None)); } + if upper == "E" || upper == "ERASE" { + self.awaiting = Some(Await::Erase); + return Some(CmdResult::NeedPoint); + } + if upper == "L" || upper == "LAYER" { + self.awaiting = Some(Await::Layer); + return Some(CmdResult::NeedPoint); + } if let Some(d) = crate::entities::common::parse_typed_length(&t) { return Some(self.accept_distance(d)); } @@ -751,17 +869,30 @@ impl CadCommand for OffsetCommand { }; // Each target offsets by its own through-distance (or the locked // magnitude), toward the clicked side. - let mut news: Vec = Vec::new(); - for entity in &targets { - let mag = locked.unwrap_or_else(|| perp_distance(entity, pt.as_vec3())); - if mag < 1e-9 { - continue; - } - news.extend(compute_offsets(entity, mag, pt.as_vec3())); - } + let per_target: Vec<(Handle, Vec)> = targets + .iter() + .filter_map(|entity| { + let mag = locked.unwrap_or_else(|| perp_distance(entity, pt.as_vec3())); + if mag < 1e-9 { + return None; + } + let offsets = compute_offsets(entity, mag, pt.as_vec3()); + (!offsets.is_empty()).then(|| (entity.common().handle, offsets)) + }) + .collect(); + let mut news: Vec = per_target + .iter() + .flat_map(|(_, offsets)| offsets.iter().cloned()) + .collect(); if news.is_empty() { return CmdResult::NeedPoint; } + self.made += 1; + // Erase option: the offset replaces its source object. Only the + // objects picked from the drawing qualify — a Multiple chain's + // targets are the offsets just made, which are not replaced again. + let erase = self.erase_source && self.targets_are_sources; + self.targets_are_sources = false; if multiple { // Multiple mode chains from the result just created, matching // OFFSET's repeated fixed-distance behavior (parallel/concentric @@ -771,15 +902,14 @@ impl CadCommand for OffsetCommand { locked, multiple: true, }; - return if news.len() == 1 { - CmdResult::CommitEntity(news.pop().unwrap()) - } else { - CmdResult::CommitEntities(news) - }; + } else { + // Classic loop (#418): commit this offset and go back to the object + // pick at the same distance, until Enter / Esc finishes. + self.step = Step::SelectObject { locked }; + } + if erase { + return CmdResult::ReplaceManyContinue(per_target); } - // Classic loop (#418): commit this offset and go back to the object - // pick at the same distance, until Enter / Esc finishes. - self.step = Step::SelectObject { locked }; if news.len() == 1 { CmdResult::CommitEntity(news.pop().unwrap()) } else { @@ -913,3 +1043,55 @@ mod offset_tests { assert!(approx(offset_bbox(&ccw, Vec3::new(50.0, -10.0, 0.0)), out)); } } + +#[cfg(test)] +mod option_tests { + use super::*; + + fn line(x1: f64, y1: f64, x2: f64, y2: f64, handle: u64) -> EntityType { + let mut line = acadrust::entities::Line::from_coords(x1, y1, 0.0, x2, y2, 0.0); + line.common.handle = Handle::new(handle); + EntityType::Line(line) + } + + fn keywords(cmd: &OffsetCommand) -> Vec { + cmd.options().into_iter().map(|o| o.keyword).collect() + } + + #[test] + fn erase_replaces_the_source_object() { + let mut cmd = OffsetCommand::new(vec![line(0.0, 0.0, 10.0, 0.0, 1)]); + assert_eq!(keywords(&cmd), ["T", "E", "L", ""]); + assert!(matches!(cmd.on_text_input("E"), Some(CmdResult::NeedPoint))); + assert_eq!(keywords(&cmd), ["Y", "N"]); + assert!(matches!(cmd.on_text_input("Y"), Some(CmdResult::NeedPoint))); + assert!(matches!(cmd.on_text_input("2"), Some(CmdResult::ReportMeasurement(_)))); + assert!(matches!(cmd.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)), CmdResult::NeedPoint)); + match cmd.on_point(DVec3::new(5.0, 5.0, 0.0)) { + CmdResult::ReplaceManyContinue(replacements) => { + assert_eq!(replacements.len(), 1); + assert_eq!(replacements[0].0, Handle::new(1)); + assert_eq!(replacements[0].1.len(), 1); + } + _ => panic!("erase should replace the source with its offset"), + } + // Back at the object pick, with Undo now available. + assert_eq!(keywords(&cmd), ["U", ""]); + assert!(matches!(cmd.on_text_input("U"), Some(CmdResult::UndoDocument))); + assert_eq!(keywords(&cmd), [""]); + } + + #[test] + fn layer_current_drops_source_layer_preservation() { + let mut cmd = OffsetCommand::new(vec![line(0.0, 0.0, 10.0, 0.0, 1)]); + assert!(cmd.preserve_commit_layer()); + cmd.on_text_input("L"); + assert_eq!(keywords(&cmd), ["C", "S"]); + cmd.on_text_input("C"); + assert!(!cmd.preserve_commit_layer()); + assert!(matches!(cmd.step, Step::Distance)); + cmd.on_text_input("2"); + cmd.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)); + assert!(matches!(cmd.on_point(DVec3::new(5.0, 5.0, 0.0)), CmdResult::CommitEntity(_))); + } +} diff --git a/src/modules/draw/modify/translate.rs b/src/modules/draw/modify/translate.rs index 061d2dc82..7c52b7209 100644 --- a/src/modules/draw/modify/translate.rs +++ b/src/modules/draw/modify/translate.rs @@ -2,8 +2,9 @@ // // Command: MOVE (M) // Requires at least one entity selected before starting. -// Step 1: pick base point -// Step 2: pick destination → translates all selected entities by (dest - base) +// Step 1: pick base point, or D = type the displacement vector directly +// Step 2: pick destination → translates all selected entities by (dest - base); +// Enter uses the base point as the displacement (commercial solutions). use acadrust::Handle; use glam::DVec3; @@ -28,6 +29,8 @@ pub fn tool() -> ToolDef { enum Step { Base, Target(DVec3), + /// Displacement option: the next point / typed coordinate is the vector. + Displacement, } pub struct MoveCommand { @@ -62,6 +65,16 @@ impl CadCommand for MoveCommand { x = format!("{:.3}", base.x), y = format!("{:.3}", base.y), ), + Step::Displacement => crate::tr!("command-move", "displacement"), + } + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + match self.step { + Step::Base => vec![CmdOption::new("Displacement", "D")], + Step::Target(_) => vec![CmdOption::enter("Use first point as displacement")], + Step::Displacement => Vec::new(), } } @@ -78,11 +91,42 @@ impl CadCommand for MoveCommand { EntityTransform::Translate(delta), ) } + // The point (typically typed as `dx,dy`) is the displacement itself. + Step::Displacement => CmdResult::TransformSelected( + self.handles.clone(), + EntityTransform::Translate(pt), + ), + } + } + + fn wants_text_input(&self) -> bool { + matches!(self.step, Step::Base) + } + + fn point_step_accepts_keywords(&self) -> bool { + matches!(self.step, Step::Base) + } + + fn on_text_input(&mut self, text: &str) -> Option { + match (&self.step, text.trim().to_uppercase().as_str()) { + (Step::Base, "D" | "DISPLACEMENT") => { + self.step = Step::Displacement; + Some(CmdResult::NeedPoint) + } + _ => None, } } fn on_enter(&mut self) -> CmdResult { - CmdResult::Cancel + match self.step { + // Commercial solutions: Enter at the second point uses the base point as the + // displacement from the origin. + Step::Target(base) => CmdResult::TransformSelected( + self.handles.clone(), + EntityTransform::Translate(base), + ), + _ => CmdResult::Cancel, + } } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel @@ -111,3 +155,40 @@ impl CadCommand for MoveCommand { out } } + +#[cfg(test)] +mod tests { + use super::*; + + fn keywords(cmd: &MoveCommand) -> Vec { + cmd.options().into_iter().map(|o| o.keyword).collect() + } + + #[test] + fn displacement_option_translates_by_vector() { + let mut cmd = MoveCommand::new(vec![Handle::new(1)], vec![]); + assert_eq!(keywords(&cmd), ["D"]); + assert!(matches!(cmd.on_text_input("D"), Some(CmdResult::NeedPoint))); + assert!(keywords(&cmd).is_empty()); + match cmd.on_point(DVec3::new(3.0, 4.0, 0.0)) { + CmdResult::TransformSelected(handles, EntityTransform::Translate(d)) => { + assert_eq!(handles, vec![Handle::new(1)]); + assert_eq!(d, DVec3::new(3.0, 4.0, 0.0)); + } + _ => panic!("expected a translation"), + } + } + + #[test] + fn enter_at_second_point_uses_base_as_displacement() { + let mut cmd = MoveCommand::new(vec![Handle::new(1)], vec![]); + cmd.on_point(DVec3::new(5.0, -2.0, 0.0)); + assert_eq!(keywords(&cmd), [""], "Enter is the only option at the second point"); + match cmd.on_enter() { + CmdResult::TransformSelected(_, EntityTransform::Translate(d)) => { + assert_eq!(d, DVec3::new(5.0, -2.0, 0.0)); + } + _ => panic!("expected a translation by the base point"), + } + } +} diff --git a/src/scene/pick/selection_state.rs b/src/scene/pick/selection_state.rs index 067a9c2c2..22faca9a5 100644 --- a/src/scene/pick/selection_state.rs +++ b/src/scene/pick/selection_state.rs @@ -30,9 +30,13 @@ pub struct SelectionState { pub poly_points: Vec, pub poly_crossing: bool, pub poly_last_crossing: bool, + /// Canvas position the right-click context menu is anchored at while it + /// is open. `None` = closed. Its rows are rebuilt from app state every + /// frame (`ui::popup::context_menu::build_context_menu`). pub context_menu: Option, - /// True while the context menu's Draw Order sub-items are expanded. - pub draworder_submenu: bool, + /// Transient UI state of the open context menu (expanded submenu, + /// keyboard highlight). Reset every time the menu opens. + pub context_menu_ui: ContextMenuUi, pub last_move_pos: Option, pub left_down: bool, pub left_press_pos: Option, @@ -57,7 +61,31 @@ pub struct SelectionState { pub middle_last_press_time: Option, } +/// Transient state of the open right-click context menu. +#[derive(Clone, Default)] +pub struct ContextMenuUi { + /// The accordion submenu currently expanded (at most one). + pub open_submenu: Option, + /// Keyboard highlight as an index into `ContextMenu::selectable()`. + /// `None` until an arrow key / mnemonic is pressed, so a mouse user is + /// never shown a highlighted row that is not under the pointer. + pub highlighted: Option, +} + impl SelectionState { + /// Open the context menu at `at`, discarding the previous menu's + /// expanded-submenu / highlight state. + pub fn open_context_menu(&mut self, at: Point) { + self.context_menu = Some(at); + self.context_menu_ui = ContextMenuUi::default(); + } + + /// Close the context menu (no-op when it is not open). + pub fn close_context_menu(&mut self) { + self.context_menu = None; + self.context_menu_ui = ContextMenuUi::default(); + } + /// End every left-button selection gesture without disturbing the previous /// completed-window record or command-owned preview marquee. Grip editing /// owns the left button while engaged and calls this before/after placement diff --git a/src/snap.rs b/src/snap.rs index 728af19c3..78e215382 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -416,6 +416,17 @@ impl Snapper { self.snap_enabled = true; } + /// One-shot "None" override (Snap Overrides ▸ None): the next pick takes + /// the raw cursor point, ignoring every running object snap, after which + /// `clear_override` restores the saved configuration. + pub fn set_override_none(&mut self) { + if self.override_saved.is_none() { + self.override_saved = Some((self.enabled.clone(), self.snap_enabled)); + } + self.enabled.clear(); + self.snap_enabled = false; + } + /// Restore the pre-override snap configuration. No-op when inactive. pub fn clear_override(&mut self) { if let Some((enabled, on)) = self.override_saved.take() { diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index da54abc54..a703c3800 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -14,6 +14,9 @@ use std::ops::Range; use crate::ui::style::common::accessible_accent_threshold; +/// Maximum entries kept for the context menu's "Recent Input" list. +pub const RECENT_INPUT_CAP: usize = 10; + pub const CMD_INPUT_ID: &str = "cmd_input"; pub const HISTORY_SCROLL_ID: &str = "command_history_scroll"; @@ -68,15 +71,28 @@ fn mcp_status(enabled: bool, busy: bool) -> (&'static str, Color) { } /// Drop a prompt's "[A / B / …]" option listing — used for the pinned line -/// when the same options render as clickable buttons beside it. Collapses the -/// leftover double spaces and the orphaned " :" the removal leaves behind. -fn strip_option_listing(s: &str) -> String { +/// when the same options render as clickable buttons beside it. Only a +/// bracket group that reads as a listing goes (several choices, or a single +/// choice that is one of `options`); an informational group such as +/// "[3 objects]" or "[base 1.0,2.0]" stays. Collapses the leftover double +/// spaces and the orphaned " :" the removal leaves behind. +fn strip_option_listing(s: &str, options: &[CmdOption]) -> String { let Some(a) = s.find('[') else { return s.to_string(); }; let Some(off) = s[a..].find(']') else { return s.to_string(); }; + let inner = s[a + 1..a + off].trim(); + let is_listing = inner.contains('/') + || inner.contains('|') + || inner.contains('=') + || options.iter().any(|o| { + o.label.eq_ignore_ascii_case(inner) || o.keyword.eq_ignore_ascii_case(inner) + }); + if !is_listing { + return s.to_string(); + } let mut out = format!("{}{}", &s[..a], &s[a + off + 1..]); while out.contains(" ") { out = out.replace(" ", " "); @@ -120,6 +136,10 @@ pub struct CommandLine { /// context menu, shortcuts), newest last. Drives the right-click "Repeat" /// menu so it reflects every command source, not only typed ones. pub recent_commands: Vec, + /// Tokens typed into a running command (points, distances, keywords), + /// newest first, de-duplicated. Drives the context menu's "Recent Input" + /// submenu so a value can be re-fed with one click. Not persisted. + pub recent_inputs: Vec, /// Current position in `cmd_recall` while navigating (None = not navigating). recall_cursor: Option, /// Saved draft input before the user started navigating history. @@ -168,6 +188,7 @@ impl Default for CommandLine { last_error: None, cmd_recall: Vec::new(), recent_commands: Vec::new(), + recent_inputs: Vec::new(), recall_cursor: None, recall_draft: String::new(), history_open: false, @@ -309,6 +330,22 @@ impl CommandLine { self.cancel_history_navigation(); } + /// Remember a token fed to a running command for the "Recent Input" + /// submenu: newest first, case-insensitive move-to-front de-duplication, + /// capped at `RECENT_INPUT_CAP`. Empty, overly long and entity-handle + /// tokens are skipped (a handle is meaningless outside the pick it + /// answered). + pub fn record_recent_input(&mut self, token: &str) { + let token = token.trim(); + if token.is_empty() || token.chars().count() > 40 { + return; + } + let upper = token.to_uppercase(); + self.recent_inputs.retain(|t| t.to_uppercase() != upper); + self.recent_inputs.insert(0, token.to_string()); + self.recent_inputs.truncate(RECENT_INPUT_CAP); + } + pub fn history_navigation_active(&self) -> bool { self.recall_cursor.is_some() } @@ -463,6 +500,24 @@ impl CommandLine { e.pinned || (self.fade_ms > 0 && e.created_at.elapsed().as_secs_f32() < self.fade_secs()) } + /// Height of the overlaid prompt lines currently shown above the input + /// row (CLIPROMPTLINES, minus lines that have faded out), in pixels. Lets + /// cursor-anchored panels stay clear of the whole command area, not just + /// the input row. + pub fn overlay_lines_height(&self) -> f32 { + if self.cliprompt_lines == 0 || self.history_open { + return 0.0; + } + let visible = self + .history + .iter() + .filter(|e| self.entry_visible(e)) + .count() + .min(self.cliprompt_lines as usize); + // Text size 12 (line height ≈ 15.6) plus the 1 px padding each side. + visible as f32 * 18.0 + } + /// Visible overlay count respecting CLIPROMPTLINES (0–50). Used in tests. #[cfg(test)] pub fn visible_history_count(&self) -> usize { @@ -614,7 +669,7 @@ impl CommandLine { // already name every choice, so the prompt's own "[A / B / …]" // listing is dropped here (the history log keeps the full text). if entry.pinned && !self.step_options.is_empty() { - let shown = strip_option_listing(&entry.text); + let shown = strip_option_listing(&entry.text, &self.step_options); let mut r = row![entry_text(shown)].spacing(6).align_y(iced::Center); for opt in &self.step_options { let btn = button(text(opt.label.to_uppercase()).size(11)) diff --git a/src/ui/icons.rs b/src/ui/icons.rs index 883108e27..9879c0564 100644 --- a/src/ui/icons.rs +++ b/src/ui/icons.rs @@ -44,6 +44,8 @@ static OSNAP_APPARENT: &[u8] = include_bytes!("../../assets/icons/osnap/apparent static OSNAP_PARALLEL: &[u8] = include_bytes!("../../assets/icons/osnap/parallel.svg"); static OSNAP_GRID: &[u8] = include_bytes!("../../assets/icons/osnap/grid.svg"); static OSNAP_MTP: &[u8] = include_bytes!("../../assets/icons/osnap/mtp.svg"); +static NAV_PAN: &[u8] = include_bytes!("../../assets/icons/pan.svg"); +static NAV_ZOOM: &[u8] = include_bytes!("../../assets/icons/zoom_in.svg"); static LAY_ON: &[u8] = include_bytes!("../../assets/icons/layers/layon.svg"); static LAY_OFF: &[u8] = include_bytes!("../../assets/icons/layers/layoff.svg"); @@ -643,6 +645,15 @@ pub fn mtp_icon() -> &'static [u8] { OSNAP_MTP } +/// Context-menu glyphs for the transparent navigation rows. +pub fn pan_icon() -> &'static [u8] { + NAV_PAN +} + +pub fn zoom_icon() -> &'static [u8] { + NAV_ZOOM +} + /// Layer visibility icon bytes (on / off). pub fn layer_visible(visible: bool) -> &'static [u8] { if visible { diff --git a/src/ui/popup/context_menu.rs b/src/ui/popup/context_menu.rs new file mode 100644 index 000000000..539e28378 --- /dev/null +++ b/src/ui/popup/context_menu.rs @@ -0,0 +1,1061 @@ +//! Viewport right-click context menu — the *model*. +//! +//! `build_context_menu` is a pure function from the app's current state +//! (`MenuContext`) to the rows the menu shows (`ContextMenu`). It knows nothing +//! about iced: the view renders the rows, and the keyboard handler in `update` +//! walks the same rows to resolve Up/Down/Enter/mnemonic picks, so what the +//! user sees and what a keypress selects can never diverge. +//! +//! The layout covers the shortcut menus of commercial solutions, which the user base already +//! knows: while a command runs the menu offers Enter / Cancel / the command's +//! own keyword options (with the keyword shown as a hint, so the menu also +//! teaches the typed shortcut); when idle it offers Repeat / Recent Input / +//! Clipboard / Undo-Redo / Isolate / Pan-Zoom / selection tools; during a grip +//! edit it offers the grip modes. + +use crate::command::CmdOption; +use crate::scene::parametric_constraints::ConstraintId; +use crate::scene::pick::grip::GripEditMode; +use crate::snap::SnapType; +use crate::t; + +/// Fixed row height used by the renderer, so `ContextMenu::default_row_y` +/// (which anchors the default row under the cursor) agrees with the layout. +pub const MENU_ROW_H: f32 = 22.0; +/// Height of a separator row (1 px line plus its vertical padding). +pub const MENU_SEP_H: f32 = 7.0; +/// Vertical padding inside the menu panel above the first row. +pub const MENU_PAD_TOP: f32 = 4.0; +/// Column width without keyword hints (the width the menu always had). +pub const MENU_WIDTH: f32 = 200.0; +/// Column width when any row carries a right-aligned keyword hint. +pub const MENU_WIDTH_WITH_HINTS: f32 = 236.0; +/// How many recent entries the Recent Input / Repeat lists show. +pub const RECENT_LIMIT: usize = 10; + +/// Identifies an inline-expanding (accordion) submenu. Only one is open at a +/// time; the open one is tracked in `SelectionState::context_menu_ui`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SubmenuId { + RecentInput, + Clipboard, + DrawOrder, + Isolate, + SnapOverrides, +} + +/// Glyph drawn in a row's icon gutter (commercial solutions show one for object snaps +/// and the navigation rows). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MenuIcon { + Snap(SnapType), + Mtp, + Pan, + Zoom, +} + +/// What a grip-menu row does (the grip-mode shortcut menu of commercial solutions). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GripMenuCmd { + Stretch, + Move, + Rotate, + Scale, + Mirror, + BasePoint, + CopyToggle, + Undo, + Exit, +} + +/// Everything a menu row can do. Translated into the app's existing messages +/// by `OpenCADStudio::on_context_menu_pick`, so a row does exactly what the +/// equivalent typed input / shortcut does. +#[derive(Clone, Debug, PartialEq)] +pub enum MenuAction { + /// Enter — finish / accept the current step (`CommandFinalize`). + Enter, + /// Escape — cancel the running command (`CommandEscape`). + Cancel, + /// Feed a command option keyword (`CommandOptionPick`). Empty = Enter. + Option(String), + /// Re-feed a recent typed token (point, distance, keyword) to the + /// running command. + FeedInput(String), + /// Run a command line string (`Message::Command`). + Command(String), + /// One-shot "Mid Between 2 Points" snap override. + Mtp, + /// One-shot object snap override for the next pick. + SnapOverride(SnapType), + /// Next pick ignores object snaps. + SnapOverrideNone, + DeleteSelected, + SelectSimilar, + InvertSelection, + DeselectAll, + QuickSelect, + Properties, + Options, + Undo, + Redo, + DrawOrderPickRef(bool), + ConstraintDelete(ConstraintId), + Grip(GripMenuCmd), + /// Expand / collapse an accordion submenu (never "picked" as a command). + ToggleSubmenu(SubmenuId), +} + +/// One selectable row. +#[derive(Clone, Debug, PartialEq)] +pub struct MenuItem { + pub label: String, + /// Right-aligned keyword hint, e.g. `"C"` for Close, `"⏎"` for the Enter + /// row. Teaches the typed shortcut without cluttering the label. + pub hint: Option, + /// Uppercase letter that picks this row from the keyboard while the menu + /// is open. Derived from the option keyword's first character. + pub mnemonic: Option, + pub action: MenuAction, + /// The row Enter picks when nothing is highlighted; rendered bold and + /// anchored under the cursor when the menu opens. + pub default: bool, + pub enabled: bool, + /// Rendered with a leading check mark (toggle / current-state rows). + pub checked: bool, + /// Glyph in the icon gutter. + pub icon: Option, +} + +impl MenuItem { + fn new(label: impl Into, action: MenuAction) -> Self { + Self { + label: label.into(), + hint: None, + mnemonic: None, + action, + default: false, + enabled: true, + checked: false, + icon: None, + } + } + + fn icon(mut self, icon: MenuIcon) -> Self { + self.icon = Some(icon); + self + } + + fn hint(mut self, hint: impl Into) -> Self { + self.hint = Some(hint.into()); + self + } + + fn mnemonic(mut self, ch: Option) -> Self { + self.mnemonic = ch; + self + } + + fn default(mut self) -> Self { + self.default = true; + self + } + + fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + fn checked(mut self, checked: bool) -> Self { + self.checked = checked; + self + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum MenuRow { + Item(MenuItem), + Separator, + Submenu { + id: SubmenuId, + label: String, + items: Vec, + open: bool, + }, +} + +/// A keyboard-navigable row in display order: either an item or a submenu +/// header. Separators and the children of collapsed submenus are skipped. +#[derive(Clone, Debug, PartialEq)] +pub struct Selectable { + pub action: MenuAction, + pub mnemonic: Option, + pub enabled: bool, + /// `Some(id)` for a submenu header row. + pub header: Option, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ContextMenu { + pub rows: Vec, + pub width: f32, +} + +impl ContextMenu { + /// Keyboard-navigable rows in display order. The index into this list is + /// the highlight index kept in `ContextMenuUi::highlighted`. + pub fn selectable(&self) -> Vec { + let mut out = Vec::new(); + for row in &self.rows { + match row { + MenuRow::Item(item) => out.push(Selectable { + action: item.action.clone(), + mnemonic: item.mnemonic, + enabled: item.enabled, + header: None, + }), + MenuRow::Separator => {} + MenuRow::Submenu { id, items, open, .. } => { + out.push(Selectable { + action: MenuAction::ToggleSubmenu(*id), + mnemonic: None, + enabled: !items.is_empty(), + header: Some(*id), + }); + if *open { + for item in items { + out.push(Selectable { + action: item.action.clone(), + mnemonic: item.mnemonic, + enabled: item.enabled, + header: None, + }); + } + } + } + } + } + out + } + + /// Index (into `selectable()`) of the default row, else the first row. + pub fn default_index(&self) -> usize { + let mut idx = 0; + for row in &self.rows { + match row { + MenuRow::Item(item) => { + if item.default { + return idx; + } + idx += 1; + } + MenuRow::Separator => {} + MenuRow::Submenu { items, open, .. } => { + idx += 1; + if *open { + for item in items { + if item.default { + return idx; + } + idx += 1; + } + } + } + } + } + 0 + } + + /// The enabled row whose mnemonic is `ch`, searching forward from (and + /// excluding) `from` and wrapping, so repeated presses cycle through rows + /// sharing a letter (PLINE's CEnter / CLose both answer to `C`). + pub fn find_mnemonic(&self, ch: char, from: Option) -> Option { + let ch = ch.to_ascii_uppercase(); + let rows = self.selectable(); + if rows.is_empty() { + return None; + } + let start = from.map_or(0, |f| (f + 1) % rows.len()); + (0..rows.len()) + .map(|k| (start + k) % rows.len()) + .find(|&i| rows[i].enabled && rows[i].mnemonic == Some(ch)) + } + + /// Vertical offset from the panel's top edge to the top of the default + /// row, using the renderer's fixed row metrics. + pub fn default_row_y(&self) -> f32 { + let mut y = MENU_PAD_TOP; + for row in &self.rows { + match row { + MenuRow::Item(item) => { + if item.default { + return y; + } + y += MENU_ROW_H; + } + MenuRow::Separator => y += MENU_SEP_H, + MenuRow::Submenu { items, open, .. } => { + y += MENU_ROW_H; + if *open { + for item in items { + if item.default { + return y; + } + y += MENU_ROW_H; + } + } + } + } + } + MENU_PAD_TOP + } + + /// Total panel height (rows plus the panel's vertical padding). + pub fn height(&self) -> f32 { + let mut h = MENU_PAD_TOP * 2.0; + for row in &self.rows { + match row { + MenuRow::Item(_) => h += MENU_ROW_H, + MenuRow::Separator => h += MENU_SEP_H, + MenuRow::Submenu { items, open, .. } => { + h += MENU_ROW_H; + if *open { + h += items.len() as f32 * MENU_ROW_H; + } + } + } + } + h + } + + fn has_hints(&self) -> bool { + self.rows.iter().any(|row| match row { + MenuRow::Item(item) => item.hint.is_some(), + MenuRow::Separator => false, + MenuRow::Submenu { items, .. } => items.iter().any(|item| item.hint.is_some()), + }) + } +} + +/// State of a grip edit the menu was opened over. +#[derive(Clone, Debug, PartialEq)] +pub struct GripMenuContext { + pub mode: GripEditMode, + pub copy_on: bool, + /// The grip has been dragged away from its origin (Undo snaps it back). + pub moved: bool, +} + +/// Inputs the builder needs, gathered by `OpenCADStudio::context_menu_context`. +#[derive(Clone, Debug)] +pub enum MenuContext { + /// A command is running: its current step's options. + Command { + options: Vec, + /// The step takes a point pick (the M2P override applies). + has_point_step: bool, + /// Recently typed tokens (newest first) for the Recent Input submenu. + recent_inputs: Vec, + }, + /// Nothing running. + Idle { + has_selection: bool, + selected_constraint: Option, + isolation_active: bool, + /// Recently run commands, newest first. + recent_cmds: Vec, + clipboard_nonempty: bool, + undo_label: Option, + redo_label: Option, + props_open: bool, + }, + /// A grip is hot / being dragged. + Grip(GripMenuContext), +} + +/// Build the menu rows for `ctx`, with `open_submenu` expanded. +pub fn build_context_menu(ctx: &MenuContext, open_submenu: Option) -> ContextMenu { + let rows = match ctx { + MenuContext::Command { + options, + has_point_step, + recent_inputs, + } => command_rows(options, *has_point_step, recent_inputs, open_submenu), + MenuContext::Idle { + has_selection, + selected_constraint, + isolation_active, + recent_cmds, + clipboard_nonempty, + undo_label, + redo_label, + props_open, + } => idle_rows( + *has_selection, + *selected_constraint, + *isolation_active, + recent_cmds, + *clipboard_nonempty, + undo_label.as_deref(), + redo_label.as_deref(), + *props_open, + open_submenu, + ), + MenuContext::Grip(grip) => grip_rows(grip), + }; + let mut menu = ContextMenu { rows, width: MENU_WIDTH }; + if menu.has_hints() { + menu.width = MENU_WIDTH_WITH_HINTS; + } + menu +} + +/// Keyboard letter for an option keyword: its first ASCII alphanumeric. +fn mnemonic_for(keyword: &str) -> Option { + keyword + .chars() + .find(|c| c.is_ascii_alphanumeric()) + .map(|c| c.to_ascii_uppercase()) +} + +fn command_rows( + options: &[CmdOption], + has_point_step: bool, + recent_inputs: &[String], + open_submenu: Option, +) -> Vec { + let mut rows = Vec::new(); + // Ordered by how often a drafter reaches for each row: Enter / Cancel + // under the pointer, the command's own keywords right below them (the + // choices that change from step to step), then the recall and snap + // helpers, and the transparent navigation commands last. + // + // A command may name its own finish action (PLINE "Done", OFFSET's + // "<0.5000>" default). That row *is* Enter: it is not listed twice, and + // a default value is shown as the Enter row's hint. + let finish = options.iter().find(|o| o.keyword.is_empty()); + let enter_hint = match finish { + Some(o) if o.label.starts_with('<') => o.label.clone(), + _ => "⏎".to_string(), + }; + rows.push(MenuRow::Item( + MenuItem::new(t!("Enter").into_owned(), MenuAction::Enter).hint(enter_hint).default(), + )); + rows.push(MenuRow::Item( + MenuItem::new(t!("Cancel").into_owned(), MenuAction::Cancel).hint("Esc"), + )); + + let keyword_opts: Vec<&CmdOption> = options.iter().filter(|o| !o.keyword.is_empty()).collect(); + if !keyword_opts.is_empty() { + rows.push(MenuRow::Separator); + for opt in keyword_opts { + rows.push(MenuRow::Item( + MenuItem::new(opt.label.clone(), MenuAction::Option(opt.keyword.clone())) + .hint(opt.keyword.clone()) + .mnemonic(mnemonic_for(&opt.keyword)), + )); + } + } + + rows.push(MenuRow::Separator); + rows.push(recent_input_submenu( + recent_inputs + .iter() + .take(RECENT_LIMIT) + .map(|tok| MenuItem::new(tok.clone(), MenuAction::FeedInput(tok.clone()))) + .collect(), + open_submenu, + )); + if has_point_step { + rows.push(MenuRow::Submenu { + id: SubmenuId::SnapOverrides, + label: t!("Snap Overrides").into_owned(), + items: snap_override_items(), + open: open_submenu == Some(SubmenuId::SnapOverrides), + }); + } + + rows.push(MenuRow::Separator); + rows.extend(navigation_rows(true)); + rows +} + +/// Recent Input ▸ — always listed (commercial solutions keep the entry even when there +/// is nothing to recall yet; it is simply disabled then). +fn recent_input_submenu(items: Vec, open_submenu: Option) -> MenuRow { + MenuRow::Submenu { + id: SubmenuId::RecentInput, + label: t!("Recent Input").into_owned(), + items, + open: open_submenu == Some(SubmenuId::RecentInput), + } +} + +/// Snap Overrides ▸: one-shot object snaps for the next pick, in the +/// grouping (M2P, then the edge snaps, the curve snaps, the relation snaps, +/// None) plus the settings dialog. +fn snap_override_items() -> Vec { + let snap = |label: &str, t: SnapType| { + MenuItem::new(t!(label).into_owned(), MenuAction::SnapOverride(t)).icon(MenuIcon::Snap(t)) + }; + vec![ + MenuItem::new(t!("Mid Between 2 Points").into_owned(), MenuAction::Mtp).icon(MenuIcon::Mtp), + snap("Endpoint", SnapType::Endpoint), + snap("Midpoint", SnapType::Midpoint), + snap("Intersection", SnapType::Intersection), + snap("Apparent Intersection", SnapType::ApparentIntersection), + snap("Extension", SnapType::Extension), + snap("Center", SnapType::Center), + snap("Quadrant", SnapType::Quadrant), + snap("Tangent", SnapType::Tangent), + snap("Perpendicular", SnapType::Perpendicular), + snap("Parallel", SnapType::Parallel), + snap("Node", SnapType::Node), + snap("Insertion", SnapType::Insertion), + snap("Nearest", SnapType::Nearest), + MenuItem::new(t!("None").into_owned(), MenuAction::SnapOverrideNone), + MenuItem::new( + t!("Osnap Settings...").into_owned(), + MenuAction::Command("OSNAP".to_string()), + ), + ] +} + +/// Pan / Zoom rows. While a command runs they execute transparently (the +/// `'` prefix, as in commercial solutions) so the command resumes afterwards. +fn navigation_rows(transparent: bool) -> Vec { + let prefix = if transparent { "'" } else { "" }; + vec![ + MenuRow::Item( + MenuItem::new(t!("Pan").into_owned(), MenuAction::Command(format!("{prefix}PAN"))) + .icon(MenuIcon::Pan), + ), + MenuRow::Item( + MenuItem::new( + t!("Zoom").into_owned(), + MenuAction::Command(format!("{prefix}ZOOM DYNAMIC")), + ) + .icon(MenuIcon::Zoom), + ), + ] +} + +#[allow(clippy::too_many_arguments)] +fn idle_rows( + has_selection: bool, + selected_constraint: Option, + isolation_active: bool, + recent_cmds: &[String], + clipboard_nonempty: bool, + undo_label: Option<&str>, + redo_label: Option<&str>, + props_open: bool, + open_submenu: Option, +) -> Vec { + let cmd = |label: String, name: &str| MenuItem::new(label, MenuAction::Command(name.to_string())); + let mut rows = Vec::new(); + + // ── Repeat / Recent Input ────────────────────────────────────────── + if let Some(last) = recent_cmds.first() { + rows.push(MenuRow::Item( + MenuItem::new( + t!("Repeat %{last}", last = last.clone()).into_owned(), + MenuAction::Command(last.to_uppercase()), + ) + .default(), + )); + } + rows.push(recent_input_submenu( + recent_cmds + .iter() + .take(RECENT_LIMIT) + .map(|c| MenuItem::new(c.clone(), MenuAction::Command(c.to_uppercase()))) + .collect(), + open_submenu, + )); + rows.push(MenuRow::Separator); + + // ── Clipboard ────────────────────────────────────────────────────── + rows.push(MenuRow::Submenu { + id: SubmenuId::Clipboard, + label: t!("Clipboard").into_owned(), + items: vec![ + cmd(t!("Cut").into_owned(), "CUTCLIP").enabled(has_selection), + cmd(t!("Copy").into_owned(), "COPYCLIP").enabled(has_selection), + cmd(t!("Copy with Base Point").into_owned(), "COPYBASE").enabled(has_selection), + cmd(t!("Paste").into_owned(), "PASTECLIP").enabled(clipboard_nonempty), + cmd(t!("Paste as Block").into_owned(), "PASTEBLOCK").enabled(clipboard_nonempty), + cmd(t!("Paste to Original Coordinates").into_owned(), "PASTEORIG") + .enabled(clipboard_nonempty), + ], + open: open_submenu == Some(SubmenuId::Clipboard), + }); + rows.push(MenuRow::Separator); + + // ── Undo / Redo ──────────────────────────────────────────────────── + let undo = match undo_label { + Some(label) => t!("Undo %{label}", label = label.to_string()).into_owned(), + None => t!("Undo").into_owned(), + }; + let redo = match redo_label { + Some(label) => t!("Redo %{label}", label = label.to_string()).into_owned(), + None => t!("Redo").into_owned(), + }; + rows.push(MenuRow::Item( + MenuItem::new(undo, MenuAction::Undo).hint("Ctrl+Z").enabled(undo_label.is_some()), + )); + rows.push(MenuRow::Item( + MenuItem::new(redo, MenuAction::Redo).hint("Ctrl+Y").enabled(redo_label.is_some()), + )); + rows.push(MenuRow::Separator); + + // ── Selection edit block (the "Edit" shortcut menu of commercial solutions) ── + if has_selection { + rows.push(MenuRow::Item(MenuItem::new( + t!("Erase").into_owned(), + MenuAction::DeleteSelected, + ))); + rows.push(MenuRow::Item(cmd(t!("Move").into_owned(), "MOVE"))); + rows.push(MenuRow::Item(cmd(t!("Copy Selection").into_owned(), "COPY"))); + rows.push(MenuRow::Item(cmd(t!("Scale").into_owned(), "SCALE"))); + rows.push(MenuRow::Item(cmd(t!("Rotate").into_owned(), "ROTATE"))); + rows.push(MenuRow::Item(cmd(t!("Mirror").into_owned(), "MIRROR"))); + rows.push(MenuRow::Submenu { + id: SubmenuId::DrawOrder, + label: t!("Draw Order").into_owned(), + items: vec![ + cmd(t!("Bring to Front").into_owned(), "DRAWORDER F"), + cmd(t!("Send to Back").into_owned(), "DRAWORDER B"), + MenuItem::new( + t!("Bring Above Object").into_owned(), + MenuAction::DrawOrderPickRef(true), + ), + MenuItem::new( + t!("Send Under Object").into_owned(), + MenuAction::DrawOrderPickRef(false), + ), + ], + open: open_submenu == Some(SubmenuId::DrawOrder), + }); + rows.push(MenuRow::Separator); + } else if let Some(id) = selected_constraint { + // A selected constraint-glyph pill gets a minimal edit block — none + // of the entity actions above apply to it. + rows.push(MenuRow::Item(MenuItem::new( + t!("Delete").into_owned(), + MenuAction::ConstraintDelete(id), + ))); + rows.push(MenuRow::Separator); + } + + // ── Isolate ──────────────────────────────────────────────────────── + rows.push(MenuRow::Submenu { + id: SubmenuId::Isolate, + label: t!("Isolate").into_owned(), + items: vec![ + cmd(t!("Isolate Objects").into_owned(), "ISOLATEOBJECTS").enabled(has_selection), + cmd(t!("Hide Objects").into_owned(), "HIDEOBJECTS").enabled(has_selection), + cmd(t!("End Object Isolation").into_owned(), "UNISOLATEOBJECTS") + .enabled(isolation_active), + ], + open: open_submenu == Some(SubmenuId::Isolate), + }); + rows.push(MenuRow::Separator); + + // ── Navigation ───────────────────────────────────────────────────── + rows.extend(navigation_rows(false)); + // The edit menu is long already; Zoom Extents stays on the idle menu + // (and on the middle button's double-click), as in commercial solutions. + if !has_selection { + rows.push(MenuRow::Item(cmd(t!("Zoom Extents").into_owned(), "ZOOM EXTENTS"))); + } + rows.push(MenuRow::Separator); + + // ── Selection tools ──────────────────────────────────────────────── + if has_selection { + rows.push(MenuRow::Item(MenuItem::new( + t!("Select Similar").into_owned(), + MenuAction::SelectSimilar, + ))); + rows.push(MenuRow::Item(MenuItem::new( + t!("Invert Selection").into_owned(), + MenuAction::InvertSelection, + ))); + rows.push(MenuRow::Item(MenuItem::new( + t!("Deselect All").into_owned(), + MenuAction::DeselectAll, + ))); + } + if !has_selection { + rows.push(MenuRow::Item(cmd(t!("Select All").into_owned(), "SELECTALL"))); + } + rows.push(MenuRow::Item(MenuItem::new( + t!("Quick Select...").into_owned(), + MenuAction::QuickSelect, + ))); + rows.push(MenuRow::Separator); + + // ── Panels ───────────────────────────────────────────────────────── + rows.push(MenuRow::Item( + MenuItem::new(t!("Properties").into_owned(), MenuAction::Properties).checked(props_open), + )); + rows.push(MenuRow::Item(MenuItem::new( + t!("Options...").into_owned(), + MenuAction::Options, + ))); + rows +} + +fn grip_rows(grip: &GripMenuContext) -> Vec { + let g = |label: String, cmd: GripMenuCmd| MenuItem::new(label, MenuAction::Grip(cmd)); + vec![ + MenuRow::Item(MenuItem::new(t!("Enter").into_owned(), MenuAction::Enter).hint("⏎").default()), + MenuRow::Separator, + MenuRow::Item( + g(t!("Stretch").into_owned(), GripMenuCmd::Stretch) + .checked(matches!(grip.mode, GripEditMode::Stretch)), + ), + MenuRow::Item(g(t!("Move").into_owned(), GripMenuCmd::Move)), + MenuRow::Item(g(t!("Rotate").into_owned(), GripMenuCmd::Rotate)), + MenuRow::Item(g(t!("Scale").into_owned(), GripMenuCmd::Scale)), + MenuRow::Item(g(t!("Mirror").into_owned(), GripMenuCmd::Mirror)), + MenuRow::Separator, + MenuRow::Item(g(t!("Base Point").into_owned(), GripMenuCmd::BasePoint)), + MenuRow::Item(g(t!("Copy").into_owned(), GripMenuCmd::CopyToggle).checked(grip.copy_on)), + MenuRow::Item(g(t!("Undo").into_owned(), GripMenuCmd::Undo).enabled(grip.moved)), + MenuRow::Separator, + MenuRow::Item(g(t!("Exit").into_owned(), GripMenuCmd::Exit).hint("Esc")), + ] +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Labels are translated at build time, so expectations go through the + /// same catalog (tests run under whatever UI language the host has). + fn tl(source: &str) -> String { + t!(source).into_owned() + } + + fn labels(menu: &ContextMenu) -> Vec { + let mut out = Vec::new(); + for row in &menu.rows { + match row { + MenuRow::Item(item) => out.push(item.label.clone()), + MenuRow::Separator => out.push("---".into()), + MenuRow::Submenu { label, items, open, .. } => { + out.push(format!("{label} >")); + if *open { + out.extend(items.iter().map(|i| format!(" {}", i.label))); + } + } + } + } + out + } + + fn actions(menu: &ContextMenu) -> Vec { + menu.selectable().into_iter().map(|s| s.action).collect() + } + + fn item_at(menu: &ContextMenu, idx: usize) -> &MenuItem { + match &menu.rows[idx] { + MenuRow::Item(item) => item, + other => panic!("expected item at {idx}, got {other:?}"), + } + } + + fn line_ctx() -> MenuContext { + MenuContext::Command { + options: vec![CmdOption::new("Close", "C"), CmdOption::new("Undo", "U")], + has_point_step: true, + recent_inputs: Vec::new(), + } + } + + #[test] + fn command_menu_lists_options_with_hints() { + let menu = build_context_menu(&line_ctx(), None); + // Enter / Cancel, the keywords, then the recall / snap helpers and + // the navigation rows. + assert_eq!( + labels(&menu), + vec![ + tl("Enter"), + tl("Cancel"), + "---".into(), + tl("Close"), + tl("Undo"), + "---".into(), + format!("{} >", tl("Recent Input")), + format!("{} >", tl("Snap Overrides")), + "---".into(), + tl("Pan"), + tl("Zoom"), + ] + ); + let close = item_at(&menu, 3); + assert_eq!(close.hint.as_deref(), Some("C")); + assert_eq!(close.mnemonic, Some('C')); + assert_eq!(close.action, MenuAction::Option("C".into())); + assert!(item_at(&menu, 0).default); + assert_eq!(menu.default_index(), 0); + assert_eq!(menu.width, MENU_WIDTH_WITH_HINTS); + } + + #[test] + fn enter_option_becomes_default_row() { + let ctx = MenuContext::Command { + options: vec![CmdOption::new("Arc", "A"), CmdOption::enter("Done")], + has_point_step: false, + recent_inputs: Vec::new(), + }; + let menu = build_context_menu(&ctx, None); + // The finish option is the Enter row itself — listed once, as "Enter". + let done = item_at(&menu, 0); + assert_eq!(done.label, tl("Enter")); + assert!(done.default); + assert_eq!(done.action, MenuAction::Enter); + assert_eq!(done.hint.as_deref(), Some("⏎")); + assert_eq!( + actions(&menu)[..4], + [ + MenuAction::Enter, + MenuAction::Cancel, + MenuAction::Option("A".into()), + MenuAction::ToggleSubmenu(SubmenuId::RecentInput), + ] + ); + // A default value shows on the Enter row as its hint (OFFSET ). + let ctx = MenuContext::Command { + options: vec![CmdOption::enter("<0.5000>")], + has_point_step: false, + recent_inputs: Vec::new(), + }; + let menu = build_context_menu(&ctx, None); + assert_eq!(item_at(&menu, 0).hint.as_deref(), Some("<0.5000>")); + } + + #[test] + fn command_menu_without_options_has_only_enter_cancel() { + let ctx = MenuContext::Command { + options: Vec::new(), + has_point_step: false, + recent_inputs: Vec::new(), + }; + let menu = build_context_menu(&ctx, None); + assert_eq!( + actions(&menu), + vec![ + MenuAction::Enter, + MenuAction::Cancel, + MenuAction::ToggleSubmenu(SubmenuId::RecentInput), + MenuAction::Command("'PAN".into()), + MenuAction::Command("'ZOOM DYNAMIC".into()), + ] + ); + // Nothing to recall yet: the Recent Input header is disabled. + assert!(!menu.selectable()[2].enabled); + } + + #[test] + fn recent_input_submenu_lists_tokens_when_open() { + let ctx = MenuContext::Command { + options: Vec::new(), + has_point_step: false, + recent_inputs: vec!["10,20".into(), "C".into()], + }; + let closed = build_context_menu(&ctx, None); + assert_eq!( + actions(&closed)[..3], + [ + MenuAction::Enter, + MenuAction::Cancel, + MenuAction::ToggleSubmenu(SubmenuId::RecentInput) + ] + ); + assert!(closed.selectable()[2].enabled); + let open = build_context_menu(&ctx, Some(SubmenuId::RecentInput)); + assert_eq!( + actions(&open)[..5], + [ + MenuAction::Enter, + MenuAction::Cancel, + MenuAction::ToggleSubmenu(SubmenuId::RecentInput), + MenuAction::FeedInput("10,20".into()), + MenuAction::FeedInput("C".into()), + ] + ); + } + + #[test] + fn snap_overrides_submenu_lists_every_object_snap() { + let ctx = MenuContext::Command { + options: Vec::new(), + has_point_step: true, + recent_inputs: Vec::new(), + }; + let menu = build_context_menu(&ctx, Some(SubmenuId::SnapOverrides)); + let acts = actions(&menu); + assert!(acts.contains(&MenuAction::Mtp)); + for &(t, _, _) in crate::snap::ALL_SNAP_MODES { + assert!(acts.contains(&MenuAction::SnapOverride(t)), "missing {t:?}"); + } + assert!(acts.contains(&MenuAction::SnapOverrideNone)); + assert!(acts.contains(&MenuAction::Command("OSNAP".into()))); + // An entity-pick step offers no snap overrides. + let ctx = MenuContext::Command { + options: Vec::new(), + has_point_step: false, + recent_inputs: Vec::new(), + }; + let menu = build_context_menu(&ctx, None); + assert!(!actions(&menu).contains(&MenuAction::ToggleSubmenu(SubmenuId::SnapOverrides))); + } + + fn idle_ctx(has_selection: bool) -> MenuContext { + MenuContext::Idle { + has_selection, + selected_constraint: None, + isolation_active: false, + recent_cmds: vec!["LINE".into(), "CIRCLE".into()], + clipboard_nonempty: false, + undo_label: Some("LINE".into()), + redo_label: None, + props_open: false, + } + } + + #[test] + fn idle_menu_without_selection_has_repeat_default() { + let menu = build_context_menu(&idle_ctx(false), None); + let first = item_at(&menu, 0); + assert_eq!(first.action, MenuAction::Command("LINE".into())); + assert!(first.default); + assert_eq!(menu.default_index(), 0); + let acts = actions(&menu); + assert!(acts.contains(&MenuAction::Command("PAN".into()))); + assert!(acts.contains(&MenuAction::Options)); + assert!(!acts.contains(&MenuAction::DeleteSelected)); + assert!(!acts.contains(&MenuAction::DeselectAll)); + } + + #[test] + fn selection_menu_lists_modify_actions() { + let menu = build_context_menu(&idle_ctx(true), Some(SubmenuId::DrawOrder)); + let acts = actions(&menu); + for want in [ + MenuAction::DeleteSelected, + MenuAction::Command("MOVE".into()), + MenuAction::Command("COPY".into()), + MenuAction::Command("SCALE".into()), + MenuAction::Command("ROTATE".into()), + MenuAction::Command("MIRROR".into()), + MenuAction::ToggleSubmenu(SubmenuId::DrawOrder), + MenuAction::Command("DRAWORDER F".into()), + MenuAction::DrawOrderPickRef(false), + MenuAction::SelectSimilar, + MenuAction::DeselectAll, + ] { + assert!(acts.contains(&want), "missing {want:?} in {acts:?}"); + } + } + + #[test] + fn clipboard_rows_disabled_when_empty() { + let menu = build_context_menu(&idle_ctx(false), Some(SubmenuId::Clipboard)); + let sel = menu.selectable(); + let paste = sel + .iter() + .find(|s| s.action == MenuAction::Command("PASTECLIP".into())) + .expect("paste row"); + assert!(!paste.enabled); + let cut = sel + .iter() + .find(|s| s.action == MenuAction::Command("CUTCLIP".into())) + .expect("cut row"); + assert!(!cut.enabled); + } + + #[test] + fn undo_row_uses_history_label() { + let menu = build_context_menu(&idle_ctx(false), None); + let rows = labels(&menu); + let undo = t!("Undo %{label}", label = "LINE").into_owned(); + assert!(rows.contains(&undo), "{rows:?}"); + let sel = menu.selectable(); + let redo = sel.iter().find(|s| s.action == MenuAction::Redo).expect("redo row"); + assert!(!redo.enabled); + let undo = sel.iter().find(|s| s.action == MenuAction::Undo).expect("undo row"); + assert!(undo.enabled); + } + + #[test] + fn selectable_skips_separators_and_closed_submenus() { + let menu = build_context_menu(&idle_ctx(false), None); + let sel = menu.selectable(); + assert!(sel.iter().all(|s| s.action != MenuAction::Command("PASTECLIP".into()))); + assert!(sel.iter().any(|s| s.header == Some(SubmenuId::Clipboard))); + let open = build_context_menu(&idle_ctx(false), Some(SubmenuId::Clipboard)); + assert!(open + .selectable() + .iter() + .any(|s| s.action == MenuAction::Command("PASTECLIP".into()))); + } + + #[test] + fn find_mnemonic_cycles_duplicates() { + let ctx = MenuContext::Command { + options: vec![ + CmdOption::new("Angle", "A"), + CmdOption::new("CEnter", "CE"), + CmdOption::new("CLose", "CL"), + ], + has_point_step: false, + recent_inputs: Vec::new(), + }; + let menu = build_context_menu(&ctx, None); + // rows: Enter(0) Cancel(1) Angle(2) CEnter(3) CLose(4) … + assert_eq!(menu.find_mnemonic('c', None), Some(3)); + assert_eq!(menu.find_mnemonic('C', Some(3)), Some(4)); + assert_eq!(menu.find_mnemonic('C', Some(4)), Some(3)); + assert_eq!(menu.find_mnemonic('Z', None), None); + } + + #[test] + fn default_row_y_accounts_for_separators() { + let menu = build_context_menu(&line_ctx(), None); + assert_eq!(menu.default_row_y(), MENU_PAD_TOP); + let menu = build_context_menu(&idle_ctx(false), None); + assert_eq!(menu.default_row_y(), MENU_PAD_TOP); + // 10 items + 3 submenu headers + 6 separators, plus panel padding. + let expected = MENU_PAD_TOP * 2.0 + 13.0 * MENU_ROW_H + 6.0 * MENU_SEP_H; + assert_eq!(menu.height(), expected); + } + + #[test] + fn grip_menu_marks_current_mode() { + let menu = build_context_menu( + &MenuContext::Grip(GripMenuContext { + mode: GripEditMode::Stretch, + copy_on: true, + moved: false, + }), + None, + ); + assert_eq!(item_at(&menu, 0).action, MenuAction::Enter); + assert!(item_at(&menu, 2).checked); + let sel = menu.selectable(); + let stretch = sel + .iter() + .position(|s| s.action == MenuAction::Grip(GripMenuCmd::Stretch)) + .unwrap(); + assert_eq!(stretch, 1); + let undo = sel + .iter() + .find(|s| s.action == MenuAction::Grip(GripMenuCmd::Undo)) + .unwrap(); + assert!(!undo.enabled); + } +} diff --git a/src/ui/popup/mod.rs b/src/ui/popup/mod.rs index e5ff95d10..b541650d7 100644 --- a/src/ui/popup/mod.rs +++ b/src/ui/popup/mod.rs @@ -1,3 +1,4 @@ +pub mod context_menu; pub mod cycle_popup; pub mod isolate_popup; pub mod polar_popup; diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index c3e1c0d8e..aa490a9f7 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -1,6 +1,6 @@ pub(crate) mod spacemouse; use crate::app::config::UiThemeConfig; -use crate::app::settings::CursorType; +use crate::app::settings::{CursorType, RightClickMode}; use crate::app::Message; use iced::widget::{ button, column, container, row, scrollable, slider, text, text_input, Space, @@ -73,6 +73,10 @@ pub struct AppPrefs { pub show_ucs_icon: bool, /// UCSICON ORigin: draw it at the origin rather than the corner. pub ucs_icon_at_origin: bool, + /// SHORTCUTMENU: what a right-click in the drawing area does. + pub right_click_mode: RightClickMode, + /// SHORTCUTMENUDURATION: time-sensitive hold threshold, ms. + pub right_click_hold_ms: i32, } /// The fixed locations the Files page lists. @@ -218,6 +222,18 @@ pub fn view_window<'a>( .find(|choice| choice.value == cursor_type) .cloned(); + let right_click_options = RightClickMode::ALL + .into_iter() + .map(|value: RightClickMode| Labelled { + label: crate::t!(value.label()).into_owned(), + value, + }) + .collect::>(); + let selected_right_click = right_click_options + .iter() + .find(|choice| choice.value == prefs.right_click_mode) + .cloned(); + let palette = ui_theme.palette.to_iced(); let colors = [ (crate::tr!("options", "color-background"), palette.background), @@ -1221,6 +1237,43 @@ pub fn view_window<'a>( .size(11) .width(sizing.width), Space::new().height(24), + text(crate::t!("Right-click Customization")).size(15), + Space::new().height(10), + row![ + text(crate::t!("Right-click in drawing area")).size(12).width(150), + iced::widget::pick_list( + selected_right_click, + right_click_options, + |choice| choice.label.clone(), + ) + .on_select(|choice| Message::RightClickModeChanged(choice.value)) + .width(Fill), + ] + .spacing(10) + .align_y(iced::Center), + Space::new().height(12), + row![ + text(crate::t!("Hold duration")).size(12).width(150), + slider( + 100..=1000, + prefs.right_click_hold_ms.clamp(100, 1000), + Message::RightClickHoldMsChanged + ) + .step(50) + .width(Fill), + text(format!("{} ms", prefs.right_click_hold_ms.clamp(100, 1000))) + .size(11) + .width(52), + ] + .spacing(10) + .align_y(iced::Center), + Space::new().height(6), + text(crate::t!( + "Shortcut menu: right-click always opens the menu. Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION)." + )) + .size(11) + .width(sizing.width), + Space::new().height(24), text(crate::t!("Block Edit")).size(15), Space::new().height(10), row![