diff --git a/Cargo.lock b/Cargo.lock index 808da0125..8ec00d60f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.1" -source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=5b2ae66#5b2ae66d0bd7b0da2d13392d3c3332f8a39caa1f" +source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=211f830#211f830e1478c2f38d22591957cc69a4d4dd1edc" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index dc49bfb1c..4255d3e71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "211f830", features = ["serde"] } cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "b2b1d4b", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" @@ -61,7 +61,7 @@ iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aa iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } [patch."https://github.com/HakanSeven12/cadcodec.git"] -acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66" } +acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "211f830" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] } diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 99c315347..532f3b07f 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -1818,10 +1818,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED حدد أصل خط الامتداد الأول أو اضغط Enter لتحديد كائن: .dimaligned-specify-second-extension-line-origi = DIMALIGNED حدد أصل خط الامتداد الثاني: .dimaligned-specify-text-angle-degrees = DIMALIGNED حدد زاوية النص (بالدرجات): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR أدخل نص البُعد (فارغ = القيمة المقاسة): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR أدخل نص البُعد المنسق (فارغ = القيمة المقاسة): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR حدد قوسًا أو دائرة أو خطًا، أو حدد رأس زاوية: + .dimangular-select-second-line = DIMANGULAR حدد الخط الثاني: .dimangular-specify-angle-vertex = DIMANGULAR حدد رأس الزاوية: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR حدد موقع قوس البُعد [Mtext/نص/زاوية/ربع]: .dimangular-specify-dimension-arc-location = DIMANGULAR حدد موقع قوس البُعد: .dimangular-specify-first-extension-line-point = DIMANGULAR حدد نقطة خط الامتداد الأول: + .dimangular-specify-quadrant = DIMANGULAR حدد الربع: .dimangular-specify-second-extension-line-point = DIMANGULAR حدد نقطة خط الامتداد الثاني: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR حدد النقطة الثانية لزاوية النص: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR حدد زاوية النص أو النقطة الأولى: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE لم يُعثر على بُعد أساسي. ضع بُعدًا أولًا. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE حدد أصل خط امتداد ثانٍ (Enter للخروج): .dimbreak-select-dimension-to-break = DIMBREAK حدد البُعد المراد قطعه: diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 46178777f..4e5bdcfb3 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -1805,10 +1805,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Посочи начало на първата удължителна линия или натисни Enter за избор на обект: .dimaligned-specify-second-extension-line-origi = DIMALIGNED Посочи начало на втората удължителна линия: .dimaligned-specify-text-angle-degrees = DIMALIGNED Посочи ъгъл на текста (градуси): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Въведи текст на оразмеряване (празно = измерена стойност): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Въведи форматиран текст на оразмеряване (празно = измерена стойност): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Избери дъга, окръжност, линия или посочи връх на ъгъла: + .dimangular-select-second-line = DIMANGULAR Избери втората линия: .dimangular-specify-angle-vertex = DIMANGULAR Посочи връх на ъгъла: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Посочи местоположение на дъгата за оразмеряване [Mtext/Текст/Ъгъл/Квадрант]: .dimangular-specify-dimension-arc-location = DIMANGULAR Посочи местоположение на дъгата за оразмеряване: .dimangular-specify-first-extension-line-point = DIMANGULAR Посочи точка на първата удължителна линия: + .dimangular-specify-quadrant = DIMANGULAR Посочи квадрант: .dimangular-specify-second-extension-line-point = DIMANGULAR Посочи точка на втората удължителна линия: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Посочи втора точка за ъгъла на текста: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Посочи ъгъл на текста или първа точка: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE Не е намерено базово оразмеряване. Първо постави оразмеряване. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Посочи начало на втора удължителна линия (Enter за изход): .dimbreak-select-dimension-to-break = DIMBREAK Избери оразмеряване за прекъсване: diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 18c6d2457..47f98e8d5 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -1805,10 +1805,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Zadejte počátek první vynášecí čáry nebo stiskněte Enter pro výběr objektu: .dimaligned-specify-second-extension-line-origi = DIMALIGNED Zadejte počátek druhé vynášecí čáry: .dimaligned-specify-text-angle-degrees = DIMALIGNED Zadejte úhel textu (stupně): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Zadejte text kóty (prázdné = naměřená hodnota): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Zadejte formátovaný text kóty (prázdné = naměřená hodnota): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Vyberte oblouk, kružnici či čáru, nebo zadejte vrchol úhlu: + .dimangular-select-second-line = DIMANGULAR Vyberte druhou čáru: .dimangular-specify-angle-vertex = DIMANGULAR Zadejte vrchol úhlu: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Zadejte umístění oblouku kóty [Mtext/Text/Úhel/Kvadrant]: .dimangular-specify-dimension-arc-location = DIMANGULAR Zadejte umístění oblouku kóty: .dimangular-specify-first-extension-line-point = DIMANGULAR Zadejte první bod vynášecí čáry: + .dimangular-specify-quadrant = DIMANGULAR Zadejte kvadrant: .dimangular-specify-second-extension-line-point = DIMANGULAR Zadejte druhý bod vynášecí čáry: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Zadejte druhý bod úhlu textu: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Zadejte úhel textu nebo první bod: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE Nebyl nalezen žádný rozměr základny. Nejprve umístěte kótu. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Zadejte počátek druhé prodlužovací čáry (Enter pro ukončení): .dimbreak-select-dimension-to-break = DIMBREAK Vyberte rozměr, který chcete přerušit: diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 6be54b175..9c48f2aff 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -1806,10 +1806,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Ursprung der ersten Maßhilfslinie angeben oder Enter drücken, um ein Objekt auszuwählen: .dimaligned-specify-second-extension-line-origi = DIMALIGNED Ursprung der zweiten Maßhilfslinie angeben: .dimaligned-specify-text-angle-degrees = DIMALIGNED Textwinkel angeben (Grad): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Bemaßungstext eingeben (leer = Messwert): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Formatierten Bemaßungstext eingeben (leer = Messwert): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Bogen, Kreis oder Linie wählen oder Winkelspitze angeben: + .dimangular-select-second-line = DIMANGULAR Zweite Linie wählen: .dimangular-specify-angle-vertex = DIMANGULAR Winkelspitze angeben: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Bemaßungsbogenlage angeben [Mtext/Text/Winkel/Quadrant]: .dimangular-specify-dimension-arc-location = DIMANGULAR Bemaßungsbogenlage angeben: .dimangular-specify-first-extension-line-point = DIMANGULAR Geben Sie den ersten Verlängerungslinienpunkt an: + .dimangular-specify-quadrant = DIMANGULAR Quadrant angeben: .dimangular-specify-second-extension-line-point = DIMANGULAR Zweiter Verlängerungslinienpunkt angeben: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Zweiten Punkt für Textwinkel angeben: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Textwinkel oder ersten Punkt angeben: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE Keine Basisdimension gefunden. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Ursprung der zweiten Maßhilfslinie angeben (Eingabe zum Beenden): .dimbreak-select-dimension-to-break = DIMBREAK Bemaßung auswählen, um zu brechen: diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 27c6a9aa7..67485420f 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -1836,10 +1836,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Specify first extension line origin or press Enter to select object: .dimaligned-specify-second-extension-line-origi = DIMALIGNED Specify second extension line origin: .dimaligned-specify-text-angle-degrees = DIMALIGNED Specify text angle (degrees): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Enter dimension text (blank = measured value): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Enter formatted dimension text (blank = measured value): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Select arc, circle, line, or specify an angle vertex: + .dimangular-select-second-line = DIMANGULAR Select second line: .dimangular-specify-angle-vertex = DIMANGULAR Specify angle vertex: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Specify dimension arc location [Mtext/Text/Angle/Quadrant]: .dimangular-specify-dimension-arc-location = DIMANGULAR Specify dimension arc location: .dimangular-specify-first-extension-line-point = DIMANGULAR Specify first extension line point: + .dimangular-specify-quadrant = DIMANGULAR Specify quadrant: .dimangular-specify-second-extension-line-point = DIMANGULAR Specify second extension line point: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Specify second point for text angle: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Specify text angle or first point: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE No base dimension found. Place a dimension first. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Specify a second extension line origin (Enter to exit): .dimbreak-select-dimension-to-break = DIMBREAK Select dimension to break: diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 143c77571..e9c664b45 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -1806,10 +1806,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Especifique el origen de la primera línea de referencia o pulse Enter para seleccionar un objeto: .dimaligned-specify-second-extension-line-origi = DIMALIGNED Especifique el origen de la segunda línea de referencia: .dimaligned-specify-text-angle-degrees = DIMALIGNED Especifique el ángulo del texto (grados): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Introduzca el texto de cota (vacío = valor medido): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Introduzca el texto de cota con formato (vacío = valor medido): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Seleccione arco, círculo, línea o especifique un vértice de ángulo: + .dimangular-select-second-line = DIMANGULAR Seleccione la segunda línea: .dimangular-specify-angle-vertex = DIMANGULAR Especifique el vértice del ángulo: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Especifique la ubicación del arco de cota [Mtext/Texto/Ángulo/Cuadrante]: .dimangular-specify-dimension-arc-location = DIMANGULAR Especifique la ubicación del arco de cota: .dimangular-specify-first-extension-line-point = DIMANGULAR Especifique un punto de la primera línea de referencia: + .dimangular-specify-quadrant = DIMANGULAR Especifique el cuadrante: .dimangular-specify-second-extension-line-point = DIMANGULAR Especifique un punto de la segunda línea de referencia: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Especifique el segundo punto para el ángulo del texto: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Especifique el ángulo del texto o el primer punto: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE No se encontró ninguna cota base. Coloque primero una cota. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Especifique el origen de una segunda línea de referencia (Enter para salir): .dimbreak-select-dimension-to-break = DIMBREAK Seleccione la cota que desea partir: diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 33ac69515..108abf86a 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -1805,10 +1805,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Määritä ensimmäisen apuviivan alkupiste tai valitse objekti painamalla Enter: .dimaligned-specify-second-extension-line-origi = DIMALIGNED Määritä toisen apuviivan alkupiste: .dimaligned-specify-text-angle-degrees = DIMALIGNED Määritä tekstin kulma (asteita): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Syötä mittateksti (tyhjä = mitattu arvo): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Syötä muotoiltu mittateksti (tyhjä = mitattu arvo): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Valitse kaari, ympyrä tai viiva tai määritä kulman kärkipiste: + .dimangular-select-second-line = DIMANGULAR Valitse toinen viiva: .dimangular-specify-angle-vertex = DIMANGULAR Määritä kulman kärkipiste: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Määritä mittakaaren sijainti [Mtext/Teksti/Kulma/Neljännes]: .dimangular-specify-dimension-arc-location = DIMANGULAR Määritä mittakaaren sijainti: .dimangular-specify-first-extension-line-point = DIMANGULAR Määritä ensimmäinen jatkolinjan piste: + .dimangular-specify-quadrant = DIMANGULAR Määritä neljännes: .dimangular-specify-second-extension-line-point = DIMANGULAR Määritä toinen jatkolinjan piste: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Määritä tekstikulman toinen piste: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Määritä tekstikulma tai ensimmäinen piste: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE Perusmittaa ei löytynyt. Aseta ensin mitta. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Määritä toinen alalinjan lähtökohta (Enter poistuaksesi): .dimbreak-select-dimension-to-break = DIMBREAK Valitse katkaistava mitta: diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 16ec5469f..62593cc60 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -1806,10 +1806,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Spécifiez l'origine de la première ligne d'attache ou appuyez sur Entrée pour sélectionner un objet : .dimaligned-specify-second-extension-line-origi = DIMALIGNED Spécifiez l'origine de la deuxième ligne d'attache : .dimaligned-specify-text-angle-degrees = DIMALIGNED Spécifiez l'angle de texte (degrés): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Saisissez le texte de cote (vide = valeur mesurée) : + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Saisissez le texte de cote formaté (vide = valeur mesurée) : + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Sélectionnez un arc, un cercle ou une ligne, ou spécifiez un sommet d'angle : + .dimangular-select-second-line = DIMANGULAR Sélectionnez la deuxième ligne : .dimangular-specify-angle-vertex = DIMANGULAR Spécifiez le sommet d'angle: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Spécifiez l'emplacement de l'arc de cote [Mtext/Texte/Angle/Quadrant] : .dimangular-specify-dimension-arc-location = DIMANGULAR Spécifier l'emplacement de l'arc dimensionnel: .dimangular-specify-first-extension-line-point = DIMANGULAR Spécifiez le premier point de ligne d'extension: + .dimangular-specify-quadrant = DIMANGULAR Spécifiez le quadrant : .dimangular-specify-second-extension-line-point = DIMANGULAR Spécifiez le deuxième point de la ligne d'extension: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Spécifiez le deuxième point pour l'angle du texte : + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Spécifiez l'angle du texte ou le premier point : .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE Aucune cote de base trouvée. Créez d’abord une cote. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Spécifiez une deuxième ligne d'extension d'origine (Entrer pour sortir): .dimbreak-select-dimension-to-break = DIMBREAK Sélectionnez la dimension à casser: diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 4728471ba..67c172be3 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -1794,10 +1794,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED पहली एक्सटेंशन रेखा का मूल निर्दिष्ट करें या ऑब्जेक्ट चुनने के लिए Enter दबाएँ: .dimaligned-specify-second-extension-line-origi = DIMALIGNED दूसरी एक्सटेंशन रेखा का मूल निर्दिष्ट करें: .dimaligned-specify-text-angle-degrees = DIMALIGNED पाठ कोण निर्दिष्ट करें (डिग्री): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR आयाम टेक्स्ट दर्ज करें (खाली = मापा मान): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR स्वरूपित आयाम टेक्स्ट दर्ज करें (खाली = मापा मान): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR चाप, वृत्त या रेखा चुनें, या कोण का शीर्ष निर्दिष्ट करें: + .dimangular-select-second-line = DIMANGULAR दूसरी रेखा चुनें: .dimangular-specify-angle-vertex = DIMANGULAR कोण वर्टेक्स निर्दिष्ट करें: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR आयाम आर्क स्थान निर्दिष्ट करें [Mtext/टेक्स्ट/कोण/चतुर्थांश]: .dimangular-specify-dimension-arc-location = DIMANGULAR आयाम आर्क स्थान निर्दिष्ट करें: .dimangular-specify-first-extension-line-point = DIMANGULAR पहली एक्सटेंशन लाइन बिंदु निर्दिष्ट करें: + .dimangular-specify-quadrant = DIMANGULAR चतुर्थांश निर्दिष्ट करें: .dimangular-specify-second-extension-line-point = DIMANGULAR दूसरी एक्सटेंशन लाइन बिंदु निर्दिष्ट करें: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR टेक्स्ट कोण के लिए दूसरा बिंदु निर्दिष्ट करें: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR टेक्स्ट कोण या पहला बिंदु निर्दिष्ट करें: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE कोई आधार आयाम नहीं मिला। पहले आयाम दें। .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE एक दूसरी विस्तार लाइन मूल निर्दिष्ट करें (बाहर निकलने के लिए बेहतर): .dimbreak-select-dimension-to-break = DIMBREAK तोड़ने के लिए आयाम का चयन करें: diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 85cde8dd8..30dd2649f 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -1805,10 +1805,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Adja meg az első segédvonal kezdőpontját, vagy nyomja meg az Entert egy objektum kiválasztásához: .dimaligned-specify-second-extension-line-origi = DIMALIGNED Adja meg a második segédvonal kezdőpontját: .dimaligned-specify-text-angle-degrees = DIMALIGNED Adja meg a szöveg szögét (fokban): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Adja meg a méretszöveget (üres = mért érték): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Adja meg a formázott méretszöveget (üres = mért érték): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Válasszon ívet, kört vagy vonalat, vagy adjon meg egy szögcsúcsot: + .dimangular-select-second-line = DIMANGULAR Válassza ki a második vonalat: .dimangular-specify-angle-vertex = DIMANGULAR Szögcsúcs megadása: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Adja meg a méretív helyét [Mtext/Szöveg/Szög/Kvadráns]: .dimangular-specify-dimension-arc-location = DIMANGULAR A méretív helyének megadása: .dimangular-specify-first-extension-line-point = DIMANGULAR Adja meg az első hosszabbító vonalpontot: + .dimangular-specify-quadrant = DIMANGULAR Adja meg a kvadránst: .dimangular-specify-second-extension-line-point = DIMANGULAR Adja meg a második kiterjesztési vonalpontot: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Adja meg a szövegszög második pontját: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Adja meg a szövegszöget vagy az első pontot: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE Nem található alapméret. Először helyezzen el egy méretet. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Adjon meg egy második mellékvonal kezdőpontját (Enter a kilépéshez): .dimbreak-select-dimension-to-break = DIMBREAK Válassza ki a törni kívánt méretet: diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 8b9a01534..cd493211d 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -1805,10 +1805,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Specificare l'origine della prima linea di estensione o premere Invio per selezionare un oggetto: .dimaligned-specify-second-extension-line-origi = DIMALIGNED Specificare l'origine della seconda linea di estensione: .dimaligned-specify-text-angle-degrees = DIMALIGNED Specificare l'angolo del testo (gradi): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Immettere il testo di quota (vuoto = valore misurato): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Immettere il testo di quota formattato (vuoto = valore misurato): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Selezionare un arco, un cerchio o una linea oppure specificare un vertice dell'angolo: + .dimangular-select-second-line = DIMANGULAR Selezionare la seconda linea: .dimangular-specify-angle-vertex = DIMANGULAR Specificare il vertice dell'angolo: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Specificare la posizione dell'arco di quota [Mtext/Testo/Angolo/Quadrante]: .dimangular-specify-dimension-arc-location = DIMANGULAR Specificare la posizione dell'arco di quota: .dimangular-specify-first-extension-line-point = DIMANGULAR Specificare il primo punto della linea di estensione: + .dimangular-specify-quadrant = DIMANGULAR Specificare il quadrante: .dimangular-specify-second-extension-line-point = DIMANGULAR Specificare il secondo punto della linea di estensione: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Specificare il secondo punto per l'angolo del testo: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Specificare l'angolo del testo o il primo punto: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE Nessuna dimensione base trovata. Posiziona prima una dimensione. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Specificare una seconda origine della linea di estensione (Enter per uscire): .dimbreak-select-dimension-to-break = DIMBREAK Selezionare la dimensione da interrompere: diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 765120b96..c0f4a8686 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -1796,10 +1796,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED 1 本目の寸法補助線の起点を指定、または Enter でオブジェクトを選択: .dimaligned-specify-second-extension-line-origi = DIMALIGNED 2 本目の寸法補助線の起点を指定: .dimaligned-specify-text-angle-degrees = DIMALIGNED 文字角度を指定(度): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR 寸法値を入力(空白 = 計測値): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR 書式付き寸法文字を入力(空白 = 計測値): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR 円弧、円、線分を選択、または角度の頂点を指定: + .dimangular-select-second-line = DIMANGULAR 2 本目の線分を選択: .dimangular-specify-angle-vertex = DIMANGULAR 角度の頂点を指定: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR 寸法円弧の位置を指定 [Mtext/文字/角度/象限]: .dimangular-specify-dimension-arc-location = DIMANGULAR 寸法円弧の位置を指定: .dimangular-specify-first-extension-line-point = DIMANGULAR 1 本目の寸法補助線の点を指定: + .dimangular-specify-quadrant = DIMANGULAR 象限を指定: .dimangular-specify-second-extension-line-point = DIMANGULAR 2 本目の寸法補助線の点を指定: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR 文字角度の 2 点目を指定: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR 文字角度または 1 点目を指定: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE 基準寸法がありません。先に寸法を配置してください。 .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE 2 本目の寸法補助線の起点を指定(Enter で終了): .dimbreak-select-dimension-to-break = DIMBREAK 分割する寸法を選択: diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index de17b1ec9..63a9253d7 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -1805,10 +1805,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED 첫 번째 치수 보조선 원점을 지정하거나 Enter를 눌러 객체 선택: .dimaligned-specify-second-extension-line-origi = DIMALIGNED 두 번째 치수 보조선 원점 지정: .dimaligned-specify-text-angle-degrees = DIMALIGNED 텍스트 각도(도) 지정: + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR 치수 문자를 입력하십시오(비워 두면 측정값): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR 서식 있는 치수 문자를 입력하십시오(비워 두면 측정값): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR 호, 원 또는 선을 선택하거나 각도 꼭지점을 지정하십시오: + .dimangular-select-second-line = DIMANGULAR 두 번째 선을 선택하십시오: .dimangular-specify-angle-vertex = DIMANGULAR 각도 꼭지점 지정: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR 치수 호 위치 지정 [Mtext/문자/각도/사분면]: .dimangular-specify-dimension-arc-location = DIMANGULAR 치수 호 위치를 지정합니다. .dimangular-specify-first-extension-line-point = DIMANGULAR 첫 번째 연장선 점 지정: + .dimangular-specify-quadrant = DIMANGULAR 사분면을 지정하십시오: .dimangular-specify-second-extension-line-point = DIMANGULAR 두 번째 연장선 점을 지정합니다. + .dimangular-specify-second-point-for-text-angle = DIMANGULAR 문자 각도의 두 번째 점을 지정하십시오: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR 문자 각도 또는 첫 번째 점을 지정하십시오: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE 기본 치수를 찾을 수 없습니다. 먼저 치수를 배치합니다. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE 두 번째 연장선 원점을 지정합니다(종료하려면 Enter). .dimbreak-select-dimension-to-break = DIMBREAK 끊을 치수 선택: diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 2a52a1be4..368f91286 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -1806,10 +1806,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Geef het beginpunt van de eerste hulplijn op of druk op Enter om een object te selecteren: .dimaligned-specify-second-extension-line-origi = DIMALIGNED Geef het beginpunt van de tweede hulplijn op: .dimaligned-specify-text-angle-degrees = DIMALIGNED Teksthoek (graden) specificeren: + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Voer maattekst in (leeg = gemeten waarde): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Voer opgemaakte maattekst in (leeg = gemeten waarde): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Selecteer een boog, cirkel of lijn, of geef een hoekpunt op: + .dimangular-select-second-line = DIMANGULAR Selecteer de tweede lijn: .dimangular-specify-angle-vertex = DIMANGULAR Specificeer hoekvertex: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Geef de positie van de maatboog op [Mtext/Tekst/Hoek/Kwadrant]: .dimangular-specify-dimension-arc-location = DIMANGULAR Geef de positie van de maatboog op: .dimangular-specify-first-extension-line-point = DIMANGULAR Geef eerste extensieregelpunt aan: + .dimangular-specify-quadrant = DIMANGULAR Geef het kwadrant op: .dimangular-specify-second-extension-line-point = DIMANGULAR Geef tweede extensieregelpunt: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Geef het tweede punt voor de teksthoek op: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Geef de teksthoek of het eerste punt op: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE Geen basismaat gevonden. Plaats eerst een maat. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Vermeld een tweede extensielijn (tot afsluiten overgaan): .dimbreak-select-dimension-to-break = DIMBREAK Selecteer de te onderbreken maat: diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 5b667202a..d16788f1e 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -1805,10 +1805,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Określ początek pierwszej linii pomocniczej lub naciśnij Enter, aby wybrać obiekt: .dimaligned-specify-second-extension-line-origi = DIMALIGNED Określ początek drugiej linii pomocniczej: .dimaligned-specify-text-angle-degrees = DIMALIGNED Określ kąt tekstu (w stopniach): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Wprowadź tekst wymiaru (puste = zmierzona wartość): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Wprowadź sformatowany tekst wymiaru (puste = zmierzona wartość): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Wybierz łuk, okrąg lub linię albo określ wierzchołek kąta: + .dimangular-select-second-line = DIMANGULAR Wybierz drugą linię: .dimangular-specify-angle-vertex = DIMANGULAR Określ wierzchołek kąta: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Określ położenie łuku wymiarowego [Mtext/Tekst/Kąt/Ćwiartka]: .dimangular-specify-dimension-arc-location = DIMANGULAR Określ położenie łuku wymiarowego: .dimangular-specify-first-extension-line-point = DIMANGULAR Określ pierwszy punkt linii pomocniczej: + .dimangular-specify-quadrant = DIMANGULAR Określ ćwiartkę: .dimangular-specify-second-extension-line-point = DIMANGULAR Określ drugi punkt linii pomocniczej: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Określ drugi punkt kąta tekstu: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Określ kąt tekstu lub pierwszy punkt: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE Nie znaleziono wymiaru podstawowego. Najpierw umieść wymiar. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Określ początek drugiej linii pomocniczej (Enter, aby wyjść): .dimbreak-select-dimension-to-break = DIMBREAK Wybierz wymiar do przerwania: diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 54a743027..8c401c557 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -1806,10 +1806,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Especifique a origem da primeira linha de extensão ou pressione Enter para selecionar um objeto: .dimaligned-specify-second-extension-line-origi = DIMALIGNED Especifique a origem da segunda linha de extensão: .dimaligned-specify-text-angle-degrees = DIMALIGNED Especifique o ângulo do texto (graus): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Insira o texto da cota (vazio = valor medido): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Insira o texto formatado da cota (vazio = valor medido): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Selecione arco, círculo, linha ou especifique um vértice de ângulo: + .dimangular-select-second-line = DIMANGULAR Selecione a segunda linha: .dimangular-specify-angle-vertex = DIMANGULAR Especifique o vértice do ângulo: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Especifique a localização do arco de cota [Mtext/Texto/Ângulo/Quadrante]: .dimangular-specify-dimension-arc-location = DIMANGULAR Especifique a localização do arco de cota: .dimangular-specify-first-extension-line-point = DIMANGULAR Especifique o ponto da primeira linha de extensão: + .dimangular-specify-quadrant = DIMANGULAR Especifique o quadrante: .dimangular-specify-second-extension-line-point = DIMANGULAR Especifique o ponto da segunda linha de extensão: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Especifique o segundo ponto para o ângulo do texto: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Especifique o ângulo do texto ou o primeiro ponto: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE Nenhuma cota base encontrada. Coloque uma cota primeiro. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Especifique a origem de uma segunda linha de extensão (Enter para sair): .dimbreak-select-dimension-to-break = DIMBREAK Selecione a cota a quebrar: diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index fbd17f382..0b7904580 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -1809,10 +1809,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Укажите начало первой выносной линии или нажмите Enter для выбора объекта: .dimaligned-specify-second-extension-line-origi = DIMALIGNED Укажите начало второй выносной линии: .dimaligned-specify-text-angle-degrees = DIMALIGNED Укажите угол текста (градусы): - .dimangular-specify-angle-vertex = DIMANGULAR Укажите угол вершины: - .dimangular-specify-dimension-arc-location = DIMANGULAR Укажите местоположение дуги размерности: - .dimangular-specify-first-extension-line-point = DIMANGULAR Укажите первую точку расширения: - .dimangular-specify-second-extension-line-point = DIMANGULAR Укажите вторую точку расширения: + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Введите текст размера (пусто = измеренное значение): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Введите форматированный текст размера (пусто = измеренное значение): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Выберите дугу, окружность, линию или укажите вершину угла: + .dimangular-select-second-line = DIMANGULAR Выберите вторую линию: + .dimangular-specify-angle-vertex = DIMANGULAR Укажите вершину угла: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Укажите положение размерной дуги [Mtext/Текст/Угол/Квадрант]: + .dimangular-specify-dimension-arc-location = DIMANGULAR Укажите положение размерной дуги: + .dimangular-specify-first-extension-line-point = DIMANGULAR Укажите первую точку выносной линии: + .dimangular-specify-quadrant = DIMANGULAR Укажите квадрант: + .dimangular-specify-second-extension-line-point = DIMANGULAR Укажите вторую точку выносной линии: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Укажите вторую точку угла текста: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Укажите угол текста или первую точку: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE Базовый размер не найден. Сначала создайте размер. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE Укажите происхождение второй линии расширения (Введите для выхода): .dimbreak-select-dimension-to-break = DIMBREAK Выберите размер для разбиения: diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 4699aef5b..6f778ce81 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -1816,10 +1816,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED Birinci uzatma çizgisinin başlangıcını belirtin veya nesne seçmek için Enter'a basın: .dimaligned-specify-second-extension-line-origi = DIMALIGNED İkinci uzatma çizgisinin başlangıcını belirtin: .dimaligned-specify-text-angle-degrees = DIMALIGNED Metin açısını belirtin (derece): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR Ölçü metnini girin (boş = ölçülen değer): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR Biçimlendirilmiş ölçü metnini girin (boş = ölçülen değer): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR Yay, çember veya çizgi seçin ya da açı köşesi belirtin: + .dimangular-select-second-line = DIMANGULAR İkinci çizgiyi seçin: .dimangular-specify-angle-vertex = DIMANGULAR Açı köşesini belirtin: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR Ölçü yayının konumunu belirtin [Mtext/Metin/Açı/Kadran]: .dimangular-specify-dimension-arc-location = DIMANGULAR Ölçü yayının konumunu belirtin: .dimangular-specify-first-extension-line-point = DIMANGULAR Birinci uzatma çizgisi noktasını belirtin: + .dimangular-specify-quadrant = DIMANGULAR Kadranı belirtin: .dimangular-specify-second-extension-line-point = DIMANGULAR İkinci uzatma çizgisi noktasını belirtin: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR Metin açısının ikinci noktasını belirtin: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR Metin açısını veya ilk noktayı belirtin: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE Temel ölçü bulunamadı. Önce bir ölçü yerleştirin. .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE İkinci uzatma çizgisinin başlangıcını belirtin (çıkmak için Enter): .dimbreak-select-dimension-to-break = DIMBREAK Kesilecek ölçüyü seçin: diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 9653ccf7b..b7ed375e0 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -1793,10 +1793,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED 指定第一条尺寸界线原点,或按 Enter 选择对象: .dimaligned-specify-second-extension-line-origi = DIMALIGNED 指定第二条尺寸界线原点: .dimaligned-specify-text-angle-degrees = DIMALIGNED 指定文字角度(度): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR 输入标注文字(空白 = 测量值): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR 输入带格式的标注文字(空白 = 测量值): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR 选择圆弧、圆或直线,或指定角度顶点: + .dimangular-select-second-line = DIMANGULAR 选择第二条直线: .dimangular-specify-angle-vertex = DIMANGULAR 指定角度顶点: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR 指定标注圆弧位置 [Mtext/文字/角度/象限]: .dimangular-specify-dimension-arc-location = DIMANGULAR 指定标注圆弧位置: .dimangular-specify-first-extension-line-point = DIMANGULAR 指定第一条尺寸界线点: + .dimangular-specify-quadrant = DIMANGULAR 指定象限: .dimangular-specify-second-extension-line-point = DIMANGULAR 指定第二条尺寸界线点: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR 指定文字角度的第二点: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR 指定文字角度或第一点: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE 未找到基准标注。请先放置标注。 .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE 指定第二条尺寸界线原点(按 Enter 退出): .dimbreak-select-dimension-to-break = DIMBREAK 选择要打断的标注: diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 9acbbdfc4..877550b1e 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -1805,10 +1805,18 @@ annotate = .dimaligned-specify-first-extension-line-origin = DIMALIGNED 指定第一條尺寸界線原點,或按 Enter 選取物件: .dimaligned-specify-second-extension-line-origi = DIMALIGNED 指定第二條尺寸界線原點: .dimaligned-specify-text-angle-degrees = DIMALIGNED 指定文字角度(度): + .dimangular-enter-dimension-text-blank-measured = DIMANGULAR 輸入標註文字(空白 = 測量值): + .dimangular-enter-formatted-dimension-text-blank-measured = DIMANGULAR 輸入帶格式的標註文字(空白 = 測量值): + .dimangular-select-arc-circle-line-or-specify-angle-vertex = DIMANGULAR 選取圓弧、圓或直線,或指定角度頂點: + .dimangular-select-second-line = DIMANGULAR 選取第二條直線: .dimangular-specify-angle-vertex = DIMANGULAR 指定角度頂點: + .dimangular-specify-dimension-arc-location-options = DIMANGULAR 指定標註圓弧位置 [Mtext/文字/角度/象限]: .dimangular-specify-dimension-arc-location = DIMANGULAR 指定標註圓弧位置: .dimangular-specify-first-extension-line-point = DIMANGULAR 指定第一條尺寸界線點: + .dimangular-specify-quadrant = DIMANGULAR 指定象限: .dimangular-specify-second-extension-line-point = DIMANGULAR 指定第二條尺寸界線點: + .dimangular-specify-second-point-for-text-angle = DIMANGULAR 指定文字角度的第二點: + .dimangular-specify-text-angle-or-first-point = DIMANGULAR 指定文字角度或第一點: .dimbaseline-no-base-dimension-found-place-a-di = DIMBASELINE 未找到基準標註。請先放置標註。 .dimbaseline-specify-a-second-extension-line-or = DIMBASELINE 指定第二條尺寸界線原點(按 Enter 退出): .dimbreak-select-dimension-to-break = DIMBREAK 選擇要打斷的標註: diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index d35a32259..de0f12724 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -581,6 +581,36 @@ impl OpenCADStudio { let _ = self.apply_cmd_result(r); return; } + let accepts_points = self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|command| command.entity_pick_accepts_points()); + if accepts_points { + if let Some((coord, kind)) = super::helpers::parse_coord(token) { + let ucs = self.tabs[i].active_ucs.clone(); + let wcs = match ( + matches!(kind, super::helpers::CoordKind::Relative), + self.last_point, + ) { + (true, Some(base)) => { + base + match &ucs { + Some(value) => super::helpers::ucs_rotate_vec(coord, value), + None => coord, + } + } + _ => match &ucs { + Some(value) => super::helpers::ucs_to_wcs(coord, value), + None => coord, + }, + }; + if self.command_point_allowed(i, wcs) { + self.last_point = Some(wcs); + self.push_ucs_to_cmd(i); + let _ = self.feed_command(StepInput::Point(wcs)); + } + return; + } + } if let Ok(v) = u64::from_str_radix(token.trim_start_matches("0x"), 16) { let handle = Handle::new(v); let pt = self.tabs[i] @@ -818,7 +848,7 @@ impl OpenCADStudio { .infer_dimension_sources(handle); self.tabs[i] .scene - .attach_dimension_association(handle, sources); + .attach_dimension_association(handle, sources.to_vec()); } } self.tabs[i].dirty = true; @@ -1227,7 +1257,7 @@ impl OpenCADStudio { .infer_dimension_sources(handle); self.tabs[i] .scene - .attach_dimension_association(handle, sources); + .attach_dimension_association(handle, sources.to_vec()); } } self.tabs[i].dirty = true; @@ -1239,7 +1269,10 @@ impl OpenCADStudio { self.commit_undo_delta(i, pd); } } - CmdResult::CommitDimension { mut entity, source } => { + CmdResult::CommitDimension { + mut entity, + association, + } => { let label = self.history_label_from_active_cmd(i, "DIMENSION"); let association_mode = self.tabs[i] .scene @@ -1293,19 +1326,37 @@ impl OpenCADStudio { let pending = self.begin_undo(i, label, 1, delta_safe); if let Some(handle) = self.commit_entity_handle(entity) { if association_mode == 2 { - let sources = source.map_or_else( - || self.tabs[i].scene.infer_dimension_sources(handle), - |source| [Some(source), Some(source)], - ); - self.tabs[i] - .scene - .attach_dimension_association(handle, sources); let mut changes = vec![ (handle, crate::scene::ChangeKind::Modified), ]; - changes.extend(sources.into_iter().flatten().map(|source| { - (source, crate::scene::ChangeKind::Modified) - })); + match association { + crate::command::DimensionAssociationInput::Infer(source) => { + let sources: Vec<_> = source.map_or_else( + || { + self.tabs[i] + .scene + .infer_dimension_sources(handle) + .into_iter() + .collect() + }, + |source| vec![Some(source), Some(source)], + ); + self.tabs[i] + .scene + .attach_dimension_association(handle, sources.clone()); + changes.extend(sources.into_iter().flatten().map(|source| { + (source, crate::scene::ChangeKind::Modified) + })); + } + crate::command::DimensionAssociationInput::Explicit(sources) => { + self.tabs[i] + .scene + .attach_dimension_association_sources(handle, sources.clone()); + changes.extend(sources.into_iter().flatten().map(|source| { + (source.handle, crate::scene::ChangeKind::Modified) + })); + } + } changes.sort_by_key(|(handle, _)| handle.value()); changes.dedup_by_key(|(handle, _)| handle.value()); self.tabs[i].scene.bump_entities(&changes); @@ -3637,6 +3688,19 @@ impl OpenCADStudio { self.tabs[i].snap_result = None; self.open_mtext_editor(pos, handle, &initial, height); } + CmdResult::SuspendForMTextInput { + pos, + initial, + height, + } => { + self.tabs[i].suspended_cmd = self.tabs[i].active_cmd.take(); + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + self.command_mtext_input = true; + self.pending_command_editor_text = None; + self.open_mtext_editor(pos, None, &initial, height); + } CmdResult::OpenTextEditor { pos, handle, diff --git a/src/app/mod.rs b/src/app/mod.rs index 3830c21dc..518d32946 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -764,6 +764,9 @@ pub(super) struct OpenCADStudio { ctrl_down: bool, /// Open in-place MText editor (toolbar + text area + live preview), if any. mtext_editor: Option, + /// Return rich text to a suspended drawing command. + command_mtext_input: bool, + pending_command_editor_text: Option, /// Open in-place single-line TEXT editor (plain text-entry box), if any. text_inline: Option, /// Cursor-anchored one-shot snap override menu (Shift+RMB): the canvas @@ -3252,6 +3255,8 @@ impl OpenCADStudio { ctrl_down: false, cont_anchor: None, mtext_editor: None, + command_mtext_input: false, + pending_command_editor_text: None, text_inline: None, snap_override_popup: None, axis_lock_dir: None, diff --git a/src/app/mtext_editor.rs b/src/app/mtext_editor.rs index f4566d813..f5dae78ee 100644 --- a/src/app/mtext_editor.rs +++ b/src/app/mtext_editor.rs @@ -1209,6 +1209,12 @@ impl super::OpenCADStudio { pub(super) fn mtext_commit(&mut self) -> bool { let i = self.active_tab; let Some(ed) = self.mtext_editor.take() else { return false }; + if self.command_mtext_input { + self.pending_command_editor_text = Some(ed.build_mtext().value); + self.command_mtext_input = false; + self.refresh_properties(); + return true; + } let body_empty = ed.content.text().trim().is_empty(); let mut mt = ed.build_mtext(); let annotative = ed.editing.is_none() @@ -1302,6 +1308,10 @@ impl super::OpenCADStudio { /// then updated in place on later applies, so repeated Apply never leaves /// duplicate entities behind. pub(super) fn mtext_apply(&mut self) { + if self.command_mtext_input { + self.rebuild_mtext_preview(); + return; + } let i = self.active_tab; let (mut mt, editing, body_empty) = match self.mtext_editor.as_ref() { Some(ed) => ( @@ -1398,6 +1408,8 @@ impl super::OpenCADStudio { /// Discard the editor without changing the drawing. pub(super) fn mtext_cancel(&mut self) { self.mtext_editor = None; + self.command_mtext_input = false; + self.pending_command_editor_text = None; self.reset_modal_geometry(); } } diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 05ce1d1b5..e95f6edbd 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1144,6 +1144,31 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { | GripMenuAction::MoveWithLeader | GripMenuAction::MoveIndependent ) { + let is_dimension = matches!( + self.tabs[i].scene.document.get_entity(popup.handle), + Some(acadrust::EntityType::Dimension(_)) + ); + if is_dimension { + let movement = match item.action { + GripMenuAction::MoveWithDimLine => Some("Keep dim line with text"), + GripMenuAction::MoveWithLeader => Some("Move text, add leader"), + GripMenuAction::MoveIndependent => Some("Move text, no leader"), + _ => None, + }; + if let Some(movement) = movement { + crate::entities::dim_override::set_property( + &mut self.tabs[i].scene.document, + popup.handle, + "dim_text_movement", + movement, + ); + self.tabs[i].dirty = true; + self.tabs[i].scene.bump_entities(&[( + popup.handle, + crate::scene::ChangeKind::Modified, + )]); + } + } // Stretch / Move = grab this grip. Engage it so the next // click places it (click-move-click) — same as picking the // grip directly in the viewport. Without this the menu just @@ -1157,10 +1182,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // "Move with Leader" drags the whole multileader; the // others move just the picked grip. let (grip_id, is_translate) = - if matches!(item.action, GripMenuAction::MoveWithLeader) { + if matches!(item.action, GripMenuAction::MoveWithLeader) + && !is_dimension + { (crate::entities::multileader::MOVE_ALL_GRIP, true) } else { - (popup.grip_id, g.is_midpoint) + (popup.grip_id, if is_dimension { false } else { g.is_midpoint }) }; self.tabs[i].active_grip = Some(GripEdit::single( popup.handle, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 8615be0e8..10a95940e 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -492,6 +492,13 @@ impl OpenCADStudio { self.reset_modal_geometry(); let i = self.active_tab; if let Some(mut cmd) = self.tabs[i].suspended_cmd.take() { + if committed { + if let Some(value) = self.pending_command_editor_text.take() { + cmd.on_editor_text(value); + } + } else { + self.pending_command_editor_text = None; + } let res = cmd.on_editor_closed(committed); self.tabs[i].active_cmd = Some(cmd); self.apply_cmd_result(res) diff --git a/src/command.rs b/src/command.rs index 797b27b4b..9dcbdc20c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -169,6 +169,38 @@ pub struct SelectionEntity { pub surface_area: Option, } +/// Association source with an optional sub-entity marker. +#[derive(Clone, Copy, Debug)] +pub struct DimensionAssociationSource { + pub handle: Handle, + pub marker: Option, + pub parameter: f64, +} + +impl DimensionAssociationSource { + pub const fn inferred(handle: Handle) -> Self { + Self { + handle, + marker: None, + parameter: 0.0, + } + } + + pub const fn explicit(handle: Handle, marker: i32, parameter: f64) -> Self { + Self { + handle, + marker: Some(marker), + parameter, + } + } +} + +#[derive(Clone, Debug)] +pub enum DimensionAssociationInput { + Infer(Option), + Explicit(Vec>), +} + #[derive(Clone)] pub enum AreaPreviewSource { Handles(Vec), @@ -1212,11 +1244,10 @@ pub enum CmdResult { CommitEntitiesAndExit(Vec), /// Commit an acadrust entity to the document and end the command. CommitAndExit(EntityType), - /// Commit a linear/aligned dimension using the drawing's association mode. - /// Object-selection workflows may retain their source. + /// Commit a dimension using the drawing's association mode. CommitDimension { entity: EntityType, - source: Option, + association: DimensionAssociationInput, }, /// Commit a Model-tab 3D solid: the acadrust entity (for selection / /// persistence) plus its B-rep (cached for boolean ops + shaded @@ -1397,6 +1428,12 @@ pub enum CmdResult { initial: String, height: f64, }, + /// Collect rich text without creating an MText entity. + SuspendForMTextInput { + pos: DVec3, + initial: String, + height: f64, + }, /// Open the in-place single-line TEXT editor (a plain text-entry box, no /// formatting toolbar). `handle` is `Some` when editing an existing Text, /// `None` when creating a new one at `pos`. @@ -1778,6 +1815,11 @@ pub trait CadCommand: Send { false } + /// Accept typed coordinates while object picking. + fn entity_pick_accepts_points(&self) -> bool { + false + } + /// Include filled hatch / DXF SOLID regions in the entity hit-test. /// /// Most entity-pick commands operate on curve geometry and intentionally @@ -1806,6 +1848,9 @@ pub trait CadCommand: Send { CmdResult::Cancel } + /// Resume the command with collected rich text. + fn on_editor_text(&mut self, _value: String) {} + /// Called when the user clicks and `needs_entity_pick()` is true. /// `handle` is the nearest wire's entity handle (Handle::NULL if nothing found). fn on_entity_pick(&mut self, _handle: Handle, _pt: DVec3) -> CmdResult { diff --git a/src/entities/dim_override.rs b/src/entities/dim_override.rs index 13ce51b8a..aa568dcb4 100644 --- a/src/entities/dim_override.rs +++ b/src/entities/dim_override.rs @@ -274,6 +274,8 @@ pub fn property_int_code(field: &str) -> Option { "dim_line_inside" => DIMSOXD, "dim_units" => DIMLUNIT, "dim_precision" => DIMDEC, + "dim_angle_units" => DIMAUNIT, + "dim_angle_precision" => DIMADEC, "dim_decimal_separator" => DIMDSEP, "dim_fractional_type" => DIMFRAC, "dim_text_view_direction" => DIMTXTDIRECTION, @@ -330,6 +332,9 @@ fn inherited_int(doc: &CadDocument, handle: Handle, code: i16) -> i16 { DIMTOL => style.dimtol as i16, DIMLIM => style.dimlim as i16, DIMALT => style.dimalt as i16, + DIMAZIN => style.dimazin, + DIMAUNIT => style.dimaunit, + DIMADEC => style.dimadec, DIMTFILL => style.dimtfill, DIMTALN => 0, _ => 0, @@ -451,6 +456,8 @@ pub fn set_property( "dim_tolerance_suppress_trailing_zeros" => Some((DIMTZIN, 8)), "dim_alt_tolerance_suppress_leading_zeros" => Some((DIMALTTZ, 4)), "dim_alt_tolerance_suppress_trailing_zeros" => Some((DIMALTTZ, 8)), + "dim_angle_suppress_leading_zeros" => Some((DIMAZIN, 1)), + "dim_angle_suppress_trailing_zeros" => Some((DIMAZIN, 2)), _ => None, }; if let Some((code, bit)) = bit_field { @@ -624,6 +631,13 @@ pub fn set_property( "desktop" => Some(6), _ => trimmed.parse().ok(), }, + "dim_angle_units" => match trimmed.to_ascii_lowercase().as_str() { + "decimal degrees" => Some(0), + "degrees/minutes/seconds" => Some(1), + "gradians" => Some(2), + "radians" => Some(3), + _ => trimmed.parse().ok(), + }, "dim_fractional_type" => match trimmed.to_ascii_lowercase().as_str() { "horizontal" => Some(0), "diagonal" => Some(1), diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index ae3032661..ec2dcda70 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -124,6 +124,18 @@ fn properties(dim: &Dimension) -> Vec { ], }]; } + if matches!(dim, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)) { + return vec![PropSection { + title: t!("Misc").into_owned(), + props: vec![crate::scene::model::object::Property { + label: t!("Dimension style").into_owned(), + field: "style_name", + value: crate::scene::model::object::PropValue::PlainText( + dim.base().style_name.clone(), + ), + }], + }]; + } let mut props = base_props(dim.base()); match dim { Dimension::Aligned(d) => { @@ -1064,6 +1076,12 @@ fn restore_dim_text_relative_position( } fn dimension_line_grip_position(dim: &Dimension) -> Option { + match dim { + Dimension::Angular2Ln(d) => return Some(dv3(&d.dimension_arc)), + Dimension::Angular3Pt(d) => return Some(dv3(&d.definition_point)), + Dimension::Arc(d) => return Some(dv3(&d.definition_point)), + _ => {} + } let (first, second, defpt, ax, ay) = match dim { Dimension::Linear(d) => ( d.first_point, @@ -1117,6 +1135,14 @@ fn dimension_line_grip_position(dim: &Dimension) -> Option { } fn above_dimension_text_position(dim: &Dimension) -> Option { + if let Some((vertex, start, end, radius)) = angular_dimension_frame(dim) { + let angle = (start + end) * 0.5; + let direction = DVec3::new(angle.cos() as f64, angle.sin() as f64, 0.0); + return Some( + DVec3::new(vertex.x as f64, vertex.y as f64, vertex.z as f64) + + direction * (radius as f64 + 1.0), + ); + } let center = dimension_line_grip_position(dim)?; let (ax, ay) = match dim { Dimension::Linear(d) => (d.rotation.cos(), d.rotation.sin()), @@ -1179,11 +1205,12 @@ impl Grippable for Dimension { center_grip(2, text), ], Dimension::Angular2Ln(d) => vec![ - square_grip(0, dv3(&d.angle_vertex)), - center_grip(1, dv3(&d.first_point)), - center_grip(2, dv3(&d.second_point)), + square_grip(0, dv3(&d.first_point)), + center_grip(1, dv3(&d.second_point)), + center_grip(2, dv3(&d.angle_vertex)), center_grip(3, dv3(&d.definition_point)), - center_grip(4, text), + center_grip(4, dv3(&d.dimension_arc)), + center_grip(5, text), ], Dimension::Angular3Pt(d) => vec![ square_grip(0, dv3(&d.angle_vertex)), @@ -1231,7 +1258,8 @@ impl Grippable for Dimension { let text_grip = match self { Dimension::Linear(_) | Dimension::Aligned(_) => 3, Dimension::Radius(_) | Dimension::Diameter(_) => 2, - Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => 4, + Dimension::Angular2Ln(_) => 5, + Dimension::Angular3Pt(_) => 4, Dimension::Ordinate(_) => 3, Dimension::Arc(d) => if d.has_leader { 6 } else { 4 }, Dimension::LargeRadial(_) => 4, @@ -1274,10 +1302,11 @@ impl Grippable for Dimension { _ => {} }, Dimension::Angular2Ln(d) => match grip_id { - 0 => apply_to_v3(&mut d.angle_vertex, &apply), - 1 => apply_to_v3(&mut d.first_point, &apply), - 2 => apply_to_v3(&mut d.second_point, &apply), + 0 => apply_to_v3(&mut d.first_point, &apply), + 1 => apply_to_v3(&mut d.second_point, &apply), + 2 => apply_to_v3(&mut d.angle_vertex, &apply), 3 => apply_to_v3(&mut d.definition_point, &apply), + 4 => apply_to_v3(&mut d.dimension_arc, &apply), _ => {} }, Dimension::Angular3Pt(d) => match grip_id { @@ -1325,7 +1354,8 @@ impl Grippable for Dimension { let (dim_line_grip, text_grip) = match self { Dimension::Linear(_) | Dimension::Aligned(_) => (2, 3), Dimension::Radius(_) | Dimension::Diameter(_) => (1, 2), - Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => (3, 4), + Dimension::Angular2Ln(_) => (4, 5), + Dimension::Angular3Pt(_) => (3, 4), Dimension::Ordinate(_) => (0, 3), Dimension::Arc(d) => (3, if d.has_leader { 6 } else { 4 }), Dimension::LargeRadial(_) => (3, 4), @@ -1393,7 +1423,8 @@ impl Grippable for Dimension { let (_dim_line_grip, text_grip) = match self { Dimension::Linear(_) | Dimension::Aligned(_) => (2, 3), Dimension::Radius(_) | Dimension::Diameter(_) => (1, 2), - Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => (3, 4), + Dimension::Angular2Ln(_) => (4, 5), + Dimension::Angular3Pt(_) => (3, 4), Dimension::Ordinate(_) => (0, 3), Dimension::Arc(d) => (3, if d.has_leader { 6 } else { 4 }), Dimension::LargeRadial(_) => (3, 4), @@ -1440,7 +1471,8 @@ impl Grippable for Dimension { let text_grip = match self { Dimension::Linear(_) | Dimension::Aligned(_) => 3, Dimension::Radius(_) | Dimension::Diameter(_) => 2, - Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => 4, + Dimension::Angular2Ln(_) => 5, + Dimension::Angular3Pt(_) => 4, Dimension::Ordinate(_) => 3, Dimension::Arc(d) => if d.has_leader { 6 } else { 4 }, Dimension::LargeRadial(_) => 4, @@ -1682,7 +1714,7 @@ pub fn style_sections( _ => choice_value("None", &["None", "Background", "Color"]), }; - vec![ + let mut sections = vec![ PropSection { title: t!("Lines & Arrows").into_owned(), props: vec![ @@ -2009,19 +2041,19 @@ pub fn style_sections( text( t!("Decimal separator").as_ref(), "dim_decimal_separator", - decimal_separator, + decimal_separator.clone(), true, ), text( - t!("Dim prefix").as_ref(), + t!("Prefix").as_ref(), "dim_prefix", - dim_prefix, + dim_prefix.clone(), true, ), text( - t!("Dim suffix").as_ref(), + t!("Suffix").as_ref(), "dim_suffix", - dim_suffix, + dim_suffix.clone(), true, ), text( @@ -2312,7 +2344,81 @@ pub fn style_sections( ), ], }, - ] + ]; + + if matches!(dimension, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)) { + if let Some(text_section) = sections + .iter_mut() + .find(|section| section.title == t!("Text").as_ref()) + { + text_section + .props + .retain(|property| property.field != "dim_fractional_type"); + } + + let angle_zero_suppression = int(ov::DIMAZIN, s.dimazin); + let angle_unit = int(ov::DIMAUNIT, s.dimaunit); + if let Some(primary_units) = sections + .iter_mut() + .find(|section| section.title == t!("Primary Units").as_ref()) + { + primary_units.props = vec![ + text( + t!("Decimal separator").as_ref(), + "dim_decimal_separator", + decimal_separator, + true, + ), + text(t!("Prefix").as_ref(), "dim_prefix", dim_prefix, true), + text(t!("Suffix").as_ref(), "dim_suffix", dim_suffix, true), + choice( + t!("Angle format").as_ref(), + "dim_angle_units", + angular_unit_label(angle_unit), + &[ + "Decimal degrees", + "Degrees/minutes/seconds", + "Gradians", + "Radians", + ], + true, + ), + choice( + t!("Suppress leading zeros").as_ref(), + "dim_angle_suppress_leading_zeros", + yes(angle_zero_suppression & 1 != 0), + &["Yes", "No"], + true, + ), + choice( + t!("Suppress trailing zeros").as_ref(), + "dim_angle_suppress_trailing_zeros", + yes(angle_zero_suppression & 2 != 0), + &["Yes", "No"], + true, + ), + property( + t!("Precision").as_ref(), + "dim_angle_precision", + PropValue::Choice { + selected: int(ov::DIMADEC, s.dimadec).to_string(), + options: (-1..=8).map(|value| value.to_string()).collect(), + }, + ), + ]; + } + sections.retain(|section| section.title != t!("Alternate Units").as_ref()); + if let Some(tolerances) = sections + .iter_mut() + .find(|section| section.title == t!("Tolerances").as_ref()) + { + tolerances + .props + .retain(|property| !property.field.starts_with("dim_alt_tolerance_")); + } + } + + sections } fn property(label: &str, field: &'static str, value: PropValue) -> Property { @@ -2396,6 +2502,15 @@ fn alternate_unit_label(value: i16) -> &'static str { } } +fn angular_unit_label(value: i16) -> &'static str { + match value { + 1 => "Degrees/minutes/seconds", + 2 => "Gradians", + 3 => "Radians", + _ => "Decimal degrees", + } +} + fn tolerance_vertical_label(value: i16) -> &'static str { match value { 0 => "Bottom", @@ -2458,7 +2573,7 @@ use acadrust::{CadDocument, EntityType, Handle}; use crate::scene::convert::tess_util::aci_to_rgba; use crate::scene::convert::tessellate::{ - add_polyline, add_segment, append_arrow, arrow_from_block_with_deferred_hatch, + add_segment, append_arrow, arrow_from_block_with_deferred_hatch, normalized_or, ArrowKind, DimGeom, }; use crate::scene::model::wire_model::{SnapHint, WireModel}; @@ -3410,6 +3525,8 @@ fn dimtmove_leader_endpoints(dim: &Dimension) -> Option<(Vec3, Vec3)> { } Dimension::Radius(d) => lv(d.definition_point), Dimension::Diameter(d) => (lv(d.angle_vertex) + lv(d.definition_point)) * 0.5, + Dimension::Angular2Ln(d) => lv(d.dimension_arc), + Dimension::Angular3Pt(d) => lv(d.definition_point), _ => return None, }; Some((anchor, lv(txt))) @@ -3612,12 +3729,14 @@ fn dimension_geometry( Some((vertex, start, end)) => append_angular_dimension( &mut g, vertex, - vertex + Vec3::new(start.cos(), start.sin(), 0.0) * vertex.distance(arc_point), - vertex + Vec3::new(end.cos(), end.sin(), 0.0) * vertex.distance(arc_point), + vertex, + vertex, arc_point, arrow1, arrow2, Some((start, end)), + params, + suppress, ), // Parallel lines have no vertex and so no angle to draw; the // extension lines alone say where the dimension was. @@ -3628,15 +3747,29 @@ fn dimension_geometry( } } Dimension::Angular3Pt(d) => { + let vertex = lv(d.angle_vertex); + let first = lv(d.first_point); + let second = lv(d.second_point); + let arc_point = lv(d.definition_point); + let explicit_sweep = two_line_angle_frame( + vertex, + first, + vertex, + second, + arc_point, + ) + .map(|(_, start, end)| (start, end)); append_angular_dimension( &mut g, - lv(d.angle_vertex), - lv(d.first_point), - lv(d.second_point), - lv(d.definition_point), + vertex, + first, + second, + arc_point, arrow1, arrow2, - None, + explicit_sweep, + params, + suppress, ); } Dimension::Ordinate(d) => { @@ -3664,6 +3797,8 @@ fn dimension_geometry( d.arc_start_parameter as f32, d.arc_end_parameter as f32, )), + params, + suppress, ); if d.has_leader { add_segment( @@ -3924,14 +4059,13 @@ fn two_line_angle_frame( arc_point: Vec3, ) -> Option<(Vec3, f32, f32)> { let (u, v) = (a2 - a1, b2 - b1); - let denominator = u.x * v.y - u.y * v.x; - // Near-parallel: the crossing runs away to infinity and the sweep with it. - if denominator.abs() <= 1e-9 * u.length().max(v.length()).max(1.0) { - return None; - } - let w = b1 - a1; - let t = (w.x * v.y - w.y * v.x) / denominator; - let vertex = a1 + u * t; + let (t, _) = cadkernel::geom2d::line_line( + [a1.x as f64, a1.y as f64], + [u.x as f64, u.y as f64], + [b1.x as f64, b1.y as f64], + [v.x as f64, v.y as f64], + )?; + let vertex = a1 + u * t as f32; // Two lines cross at four angles; the arc point picks one. Each line // contributes its direction and its reverse, so try the four pairs and keep @@ -3965,6 +4099,38 @@ fn two_line_angle_frame( Some((vertex, start, end)) } +fn angular_dimension_frame(dim: &Dimension) -> Option<(Vec3, f32, f32, f32)> { + let (vertex, start, end, arc_point) = match dim { + Dimension::Angular2Ln(value) => { + let first_start = vec3_local(value.first_point); + let first_end = vec3_local(value.second_point); + let second_start = vec3_local(value.angle_vertex); + let second_end = vec3_local(value.definition_point); + let arc_point = vec3_local(value.dimension_arc); + let (vertex, start, end) = two_line_angle_frame( + first_start, + first_end, + second_start, + second_end, + arc_point, + )?; + (vertex, start, end, arc_point) + } + Dimension::Angular3Pt(value) => { + let vertex = vec3_local(value.angle_vertex); + let first = vec3_local(value.first_point); + let second = vec3_local(value.second_point); + let arc_point = vec3_local(value.definition_point); + let (vertex, start, end) = + two_line_angle_frame(vertex, first, vertex, second, arc_point)?; + (vertex, start, end, arc_point) + } + _ => return None, + }; + let radius = vertex.distance(arc_point); + (radius > 1.0e-6).then_some((vertex, start, end, radius)) +} + fn append_angular_dimension( g: &mut DimGeom, vertex: Vec3, @@ -3974,6 +4140,8 @@ fn append_angular_dimension( arrow1: &ArrowKind, arrow2: &ArrowKind, explicit_sweep: Option<(f32, f32)>, + params: DimLineParams, + suppress: SuppressFlags, ) { let radius = vertex.distance(arc_point); if radius <= 1e-6 { @@ -3988,8 +4156,30 @@ fn append_angular_dimension( let (start, mut end) = explicit_sweep.unwrap_or((measured_start, measured_end)); let dir1 = Vec3::new(start.cos(), start.sin(), 0.0); let dir2 = Vec3::new(end.cos(), end.sin(), 0.0); - add_segment(&mut g.ext_lines, first, vertex + dir1 * radius); - add_segment(&mut g.ext_lines, second, vertex + dir2 * radius); + let arc_start = vertex + dir1 * radius; + let arc_end = vertex + dir2 * radius; + let extension = |origin: Vec3, endpoint: Vec3, direction: Vec3| { + if params.dimfxlon { + ( + endpoint - direction * params.dimfxl.max(0.0), + endpoint + direction * params.dimexe, + ) + } else { + let toward_arc = normalized_or(endpoint - origin, direction); + ( + origin + toward_arc * params.dimexo, + endpoint + toward_arc * params.dimexe, + ) + } + }; + let (ext1_start, ext1_end) = extension(first, arc_start, dir1); + let (ext2_start, ext2_end) = extension(second, arc_end, dir2); + if !suppress.ext1 { + add_segment(&mut g.ext_lines, ext1_start, ext1_end); + } + if !suppress.ext2 { + add_segment(&mut g.ext_lines, ext2_start, ext2_end); + } let mut delta = end - start; // Wrap a negative sweep forwards, but leave a zero one alone: two rays that @@ -4006,32 +4196,158 @@ fn append_angular_dimension( delta = end - start; } - let steps = 32; - let mut arc_pts = Vec::with_capacity((steps + 1) as usize); - for i in 0..=steps { - let t = i as f32 / steps as f32; - let a = start + delta * t; - arc_pts.push(vertex + Vec3::new(a.cos() * radius, a.sin() * radius, 0.0)); + let arc_length = radius * delta.abs(); + let arrows_outside = if params.ticks || params.arrow_len <= 1.0e-6 { + false + } else if arc_length < params.arrow_len * 2.0 { + true + } else if arc_length < params.text_width + params.arrow_len * 2.0 { + match params.dimatfit { + 0 | 1 => true, + 2 => false, + _ => params.text_width <= arc_length, + } + } else { + false + }; + let draw_inside_line = !arrows_outside || params.dimtofl; + let arc_extension = if params.ticks { params.dimdle / radius } else { 0.0 }; + let direction = delta.signum(); + let draw_start = start - direction * arc_extension; + let draw_delta = delta + direction * arc_extension * 2.0; + let arc_pts = sample_angular_arc(vertex, radius, draw_start, draw_delta); + let steps = arc_pts.len().saturating_sub(1); + if draw_inside_line { + for index in 0..steps { + let t = (index as f32 + 0.5) / steps as f32; + if (t < 0.5 && suppress.dim1) || (t >= 0.5 && suppress.dim2) { + continue; + } + let a = arc_pts[index]; + let b = arc_pts[index + 1]; + let hidden_by_text = params.text_break.is_some_and(|(center, half_width, half_height)| { + let angle = draw_start + draw_delta * t; + let radial = Vec3::new(angle.cos(), angle.sin(), 0.0); + let tangent = Vec3::new(-angle.sin(), angle.cos(), 0.0); + let offset = center - (a + b) * 0.5; + offset.dot(tangent).abs() <= half_width + && offset.dot(radial).abs() <= half_height + }); + if !hidden_by_text { + add_segment(&mut g.dim_lines, a, b); + } + } } - add_polyline(&mut g.dim_lines, &arc_pts); - if arc_pts.len() >= 2 { + if arrows_outside && !params.dimsoxd { + let stub_angle = (params.arrow_len * 2.0 / radius).min(std::f32::consts::FRAC_PI_2); + if !suppress.dim1 { + append_sampled_arc( + &mut g.dim_lines, + vertex, + radius, + start - direction * stub_angle, + start, + ); + } + if !suppress.dim2 { + append_sampled_arc( + &mut g.dim_lines, + vertex, + radius, + end, + end + direction * stub_angle, + ); + } + } + + if let Some((text_center, half_width, _)) = params.text_break { + let text_angle = (text_center.y - vertex.y).atan2(text_center.x - vertex.x); + let into = (text_angle - start).rem_euclid(std::f32::consts::TAU); + if into > delta.abs() + 1.0e-6 { + let back_to_start = (start - text_angle).rem_euclid(std::f32::consts::TAU); + let forward_from_end = (text_angle - end).rem_euclid(std::f32::consts::TAU); + let text_gap = (half_width / radius).min(std::f32::consts::FRAC_PI_2); + if back_to_start <= forward_from_end && !suppress.dim1 { + append_sampled_arc( + &mut g.dim_lines, + vertex, + radius, + text_angle + text_gap, + start, + ); + } else if !suppress.dim2 { + append_sampled_arc( + &mut g.dim_lines, + vertex, + radius, + end, + text_angle - text_gap, + ); + } + } + } + + let start_tangent = Vec3::new(-start.sin(), start.cos(), 0.0) * direction; + let end_tangent = Vec3::new(-end.sin(), end.cos(), 0.0) * direction; + let draw_arrows = !arrows_outside || !params.dimsoxd; + if draw_arrows && !suppress.dim1 { append_arrow( g, - arc_pts[0], - normalized_or(arc_pts[1] - arc_pts[0], Vec3::X), + arc_start, + if arrows_outside { -start_tangent } else { start_tangent }, arrow1, ); - let n = arc_pts.len(); + } + if draw_arrows && !suppress.dim2 { append_arrow( g, - arc_pts[n - 1], - normalized_or(arc_pts[n - 2] - arc_pts[n - 1], Vec3::X), + arc_end, + if arrows_outside { end_tangent } else { -end_tangent }, arrow2, ); } } +fn append_sampled_arc( + lines: &mut Vec<[f32; 3]>, + vertex: Vec3, + radius: f32, + start: f32, + end: f32, +) { + for pair in sample_angular_arc(vertex, radius, start, end - start).windows(2) { + add_segment(lines, pair[0], pair[1]); + } +} + +fn sample_angular_arc(vertex: Vec3, radius: f32, start: f32, sweep: f32) -> Vec { + if sweep.abs() <= 1.0e-6 { + return vec![vertex + Vec3::new(start.cos() * radius, start.sin() * radius, 0.0)]; + } + let end = start + sweep; + let (from, to, reverse) = if sweep >= 0.0 { + (start, end, false) + } else { + (end, start, true) + }; + let mut points: Vec<_> = cadkernel::geom2d::tessellate::arc( + [vertex.x as f64, vertex.y as f64], + radius as f64, + from as f64, + to as f64, + vertex.z as f64, + cadkernel::geom2d::tessellate::DEFAULT_SEGMENTS_PER_RADIAN, + ) + .into_iter() + .map(|point| Vec3::new(point[0] as f32, point[1] as f32, point[2] as f32)) + .collect(); + if reverse { + points.reverse(); + } + points +} + fn dimension_snap_pts(dim: &Dimension) -> Vec<(glam::DVec3, SnapHint)> { let lv = |v: acadrust::types::Vector3| glam::DVec3::new(v.x, v.y, v.z); let node = |v: acadrust::types::Vector3| (lv(v), SnapHint::Node); @@ -4049,10 +4365,11 @@ fn dimension_snap_pts(dim: &Dimension) -> Vec<(glam::DVec3, SnapHint)> { Dimension::Radius(d) => vec![node(d.angle_vertex), node(d.definition_point)], Dimension::Diameter(d) => vec![node(d.angle_vertex), node(d.definition_point)], Dimension::Angular2Ln(d) => vec![ - node(d.angle_vertex), node(d.first_point), node(d.second_point), + node(d.angle_vertex), node(d.definition_point), + node(d.dimension_arc), ], Dimension::Angular3Pt(d) => vec![ node(d.angle_vertex), @@ -4288,6 +4605,32 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool let Some(style) = style else { return false; }; + if let Some((vertex, start, end, radius)) = angular_dimension_frame(dim) { + if dim.base().text_user_positioned { + let text = vec3_local(dim.base().text_middle_point); + let angle = (text.y - vertex.y).atan2(text.x - vertex.x); + return (angle - start).rem_euclid(std::f32::consts::TAU) + > end - start + 1.0e-6; + } + if style.dimtix { + return false; + } + let scale = if style.dimscale > 1e-9 { style.dimscale } else { 1.0 }; + let height = style.dimtxt * scale; + let gap = style.dimgap.abs() * scale; + let text_width = dimension_text_value(dim, Some(style)) + .map(|value| value.chars().count() as f64 * height * 0.6 + gap * 2.0) + .unwrap_or(0.0); + let arrow = style.dimasz * scale; + let span = radius as f64 * (end - start).abs() as f64; + let insufficient = text_width + arrow * 2.0 > span; + return insufficient + && match style.dimatfit { + 0 | 2 => true, + 1 | 3 => text_width > span, + _ => text_width > span, + }; + } let (first, second, axis) = match dim { Dimension::Linear(d) => ( d.first_point, @@ -4338,6 +4681,11 @@ fn dimension_text_natural_rotation(dim: &Dimension) -> f64 { let dy = d.second_point.y - d.first_point.y; dy.atan2(dx) } + Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => angular_dimension_frame(dim) + .map(|(_, start, end, _)| { + ((start + end) * 0.5 + std::f32::consts::FRAC_PI_2) as f64 + }) + .unwrap_or(0.0), _ => 0.0, }; // Clamp to (-π/2, π/2] so text never appears upside-down. @@ -4822,54 +5170,62 @@ fn reduce_fraction(mut n: u64, mut d: u64) -> String { } } -/// Format an angular measurement (input in degrees as Dimension::measurement -/// returns for angular variants) honouring DIMAUNIT, DIMADEC, DIMAZIN. +/// Format an angular measurement in degrees using the angular style settings. fn format_angular_value(measurement_deg: f64, style: Option<&DimStyle>) -> String { - let (aunit, adec, azin) = style - .map(|s| (s.dimaunit, s.dimadec, s.dimazin)) - .unwrap_or((0, 2, 0)); - let adec = adec.max(0) as usize; + let (aunit, adec, azin, decimal_separator) = style + .map(|s| { + let precision = if s.dimadec < 0 { s.dimdec } else { s.dimadec }; + (s.dimaunit, precision, s.dimazin, s.dimdsep) + }) + .unwrap_or((0, 2, 0, b'.' as i16)); + let adec = adec.clamp(0, 8) as usize; match aunit { // 1 = Degrees / Minutes / Seconds - 1 => format_dms(measurement_deg, adec, azin), + 1 => format_dms(measurement_deg, adec, azin, decimal_separator), // 2 = Gradians 2 => { let g = measurement_deg / 0.9; let raw = format!("{:.*}", adec, g); - format!("{}g", apply_angular_zero_suppression(&raw, azin)) + format!( + "{}g", + swap_decimal_sep(&apply_angular_zero_suppression(&raw, azin), decimal_separator) + ) } // 3 = Radians 3 => { let r = measurement_deg.to_radians(); let raw = format!("{:.*}", adec, r); - format!("{}r", apply_angular_zero_suppression(&raw, azin)) + format!( + "{}r", + swap_decimal_sep(&apply_angular_zero_suppression(&raw, azin), decimal_separator) + ) } // 0 or unknown = Decimal Degrees _ => { let raw = format!("{:.*}", adec, measurement_deg); - format!("{}°", apply_angular_zero_suppression(&raw, azin)) + format!( + "{}°", + swap_decimal_sep(&apply_angular_zero_suppression(&raw, azin), decimal_separator) + ) } } } -fn format_dms(deg: f64, sec_dec: usize, azin: i16) -> String { +fn format_dms(deg: f64, sec_dec: usize, azin: i16, decimal_separator: i16) -> String { let sign = if deg < 0.0 { "-" } else { "" }; - let abs = deg.abs(); - let d = abs.floor(); - let m_full = (abs - d) * 60.0; - let m = m_full.floor(); - let s = (m_full - m) * 60.0; + let scale = 10_u64.pow(sec_dec.min(8) as u32) as f64; + let total_ticks = (deg.abs() * 3600.0 * scale).round(); + let d = (total_ticks / (3600.0 * scale)).floor(); + let remaining = total_ticks - d * 3600.0 * scale; + let m = (remaining / (60.0 * scale)).floor(); + let s = (remaining - m * 60.0 * scale) / scale; let s_str = format!("{:.*}", sec_dec, s); - let mut out = format!("{}{:.0}°{:.0}'{}\"", sign, d, m, s_str); - if azin & 4 != 0 { - // suppress 0° / 0' parts - if d == 0.0 { - out = out.trim_start_matches('0').to_string(); - out = out.replacen("°", "", 1); - } - } - out + let s_str = swap_decimal_sep( + &apply_angular_zero_suppression(&s_str, azin), + decimal_separator, + ); + format!("{}{:.0}°{:.0}'{}\"", sign, d, m, s_str) } /// Apply DIMZIN bit flags to a formatted linear value. @@ -5119,6 +5475,32 @@ fn dimension_text_pos_f64( dimtad, ) } + Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => { + let Some((vertex, start, end, radius)) = angular_dimension_frame(dim) else { + return base.text_middle_point; + }; + let span = radius as f64 * (end - start).abs() as f64; + let insufficient = text_w + arrow * 2.0 > span; + let move_outside = !dimtix + && insufficient + && match dimatfit { + 0 | 2 => true, + 1 | 3 => text_w > span, + _ => text_w > span, + }; + let angle = if move_outside { + end as f64 + (text_w * 0.5 + dimgap + arrow) / (radius as f64).max(1.0e-12) + } else { + ((start + end) * 0.5) as f64 + }; + let radial_offset = if dimtad == 4 { -perp_off } else { perp_off }; + let text_radius = (radius as f64 + radial_offset).max(0.0); + Vector3::new( + vertex.x as f64 + angle.cos() * text_radius, + vertex.y as f64 + angle.sin() * text_radius, + vertex.z as f64, + ) + } _ => { // Non-linear (radius / diameter / angular / ordinate): lift the // natural mid point straight up by the style offset. A user-dragged diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 4592c358b..3ce5d6137 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -737,10 +737,18 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "DIMALIGNED Specify second extension line origin:" => Some(("annotate", "dimaligned-specify-second-extension-line-origi")), "DIMALIGNED Specify second extension line origin [Text/Angle]:" => Some(("annotate", "dimaligned-specify-second-extension-line-origi")), "DIMALIGNED Specify text angle (degrees):" => Some(("annotate", "dimaligned-specify-text-angle-degrees")), + "DIMANGULAR Enter dimension text (blank = measured value):" => Some(("annotate", "dimangular-enter-dimension-text-blank-measured")), + "DIMANGULAR Enter formatted dimension text (blank = measured value):" => Some(("annotate", "dimangular-enter-formatted-dimension-text-blank-measured")), + "DIMANGULAR Select arc, circle, line, or specify an angle vertex:" => Some(("annotate", "dimangular-select-arc-circle-line-or-specify-angle-vertex")), + "DIMANGULAR Select second line:" => Some(("annotate", "dimangular-select-second-line")), "DIMANGULAR Specify angle vertex:" => Some(("annotate", "dimangular-specify-angle-vertex")), + "DIMANGULAR Specify dimension arc location [Mtext/Text/Angle/Quadrant]:" => Some(("annotate", "dimangular-specify-dimension-arc-location-options")), "DIMANGULAR Specify dimension arc location:" => Some(("annotate", "dimangular-specify-dimension-arc-location")), "DIMANGULAR Specify first extension line point:" => Some(("annotate", "dimangular-specify-first-extension-line-point")), + "DIMANGULAR Specify quadrant:" => Some(("annotate", "dimangular-specify-quadrant")), "DIMANGULAR Specify second extension line point:" => Some(("annotate", "dimangular-specify-second-extension-line-point")), + "DIMANGULAR Specify second point for text angle:" => Some(("annotate", "dimangular-specify-second-point-for-text-angle")), + "DIMANGULAR Specify text angle or first point:" => Some(("annotate", "dimangular-specify-text-angle-or-first-point")), "DIMBASELINE No base dimension found. Place a dimension first." => Some(("annotate", "dimbaseline-no-base-dimension-found-place-a-di")), "DIMBASELINE Specify a second extension line origin (Enter to exit):" => Some(("annotate", "dimbaseline-specify-a-second-extension-line-or")), "DIMBREAK Select dimension to break:" => Some(("annotate", "dimbreak-select-dimension-to-break")), diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index c7cac1cdc..2ec212d1e 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -5,7 +5,7 @@ use acadrust::types::{Handle, Vector3}; use acadrust::EntityType; use glam::DVec3; -use crate::command::{CadCommand, CmdResult, WorkingPlane}; +use crate::command::{CadCommand, CmdResult, DimensionAssociationInput, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -134,7 +134,7 @@ impl CadCommand for AlignedDimensionCommand { } CmdResult::CommitDimension { entity: self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim))), - source: self.source_handle, + association: DimensionAssociationInput::Infer(self.source_handle), } } } diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 36a917e45..6bd3ef2e4 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -1,12 +1,19 @@ -use acadrust::entities::{Dimension, DimensionAngular3Pt}; -use acadrust::types::Vector3; +use acadrust::entities::{Dimension, DimensionAngular2Ln, DimensionAngular3Pt}; +use acadrust::types::{Handle, Vector3}; use acadrust::EntityType; +use cadkernel::geom2d::{ + closest_point, line_line, Arc as KernelArc, BulgeArc, Curve as KernelCurve, + Line as KernelLine, +}; -use crate::command::{CadCommand, CmdResult, WorkingPlane}; +use crate::command::{ + CadCommand, CmdOption, CmdResult, DimensionAssociationInput, + DimensionAssociationSource, WorkingPlane, +}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; -use glam::{DVec3, Vec3}; use crate::t; +use glam::DVec3; pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_angular.svg")); @@ -22,20 +29,86 @@ pub fn tool() -> ToolDef { enum Step { Vertex, FirstRay(DVec3), - SecondRay { + SecondRay { vertex: DVec3, first: DVec3 }, + CircleSecondRay { vertex: DVec3, first: DVec3, + source: Handle, }, - ArcPoint { + SecondLine { + first_start: DVec3, + first_end: DVec3, + first_source: Handle, + first_refs: [SourceLocator; 2], + }, + ArcPoint3 { vertex: DVec3, first: DVec3, second: DVec3, }, + ArcPoint2 { + first_start: DVec3, + first_end: DVec3, + second_start: DVec3, + second_end: DVec3, + }, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +struct SourceLocator { + marker: Option, + parameter: f64, +} + +impl SourceLocator { + const INFERRED: Self = Self { marker: None, parameter: 0.0 }; + + const fn explicit(marker: i32, parameter: f64) -> Self { + Self { marker: Some(marker), parameter } + } + + fn bind(self, handle: Handle) -> DimensionAssociationSource { + match self.marker { + Some(marker) => DimensionAssociationSource::explicit(handle, marker, self.parameter), + None => DimensionAssociationSource::inferred(handle), + } + } +} + +enum PickedCurve { + Line { + start: DVec3, + end: DVec3, + refs: [SourceLocator; 2], + normal: Option, + }, + Arc { + center: DVec3, + first: DVec3, + second: DVec3, + refs: [SourceLocator; 3], + normal: DVec3, + }, + Circle { + center: DVec3, + normal: DVec3, + }, } pub struct AngularDimensionCommand { step: Step, plane: WorkingPlane, + selecting_object: bool, + picked_entity: Option, + source_refs: Vec>, + text_override: Option, + awaiting_text: bool, + mtext_override: bool, + text_angle: Option, + awaiting_angle: bool, + angle_origin: Option, + awaiting_quadrant: bool, + quadrant_lock: Option<(f64, f64)>, } impl AngularDimensionCommand { @@ -43,6 +116,203 @@ impl AngularDimensionCommand { Self { step: Step::Vertex, plane: WorkingPlane::default(), + selecting_object: true, + picked_entity: None, + source_refs: Vec::new(), + text_override: None, + awaiting_text: false, + mtext_override: false, + text_angle: None, + awaiting_angle: false, + angle_origin: None, + awaiting_quadrant: false, + quadrant_lock: None, + } + } + + fn finish_three_point( + &self, + vertex: DVec3, + first: DVec3, + second: DVec3, + arc_point: DVec3, + ) -> CmdResult { + let vertex = self.plane.to_local(vertex); + let first = self.plane.to_local(first); + let second = self.plane.to_local(second); + let picked_text = self.plane.to_local(arc_point); + let arc_point = locked_arc_point(vertex, picked_text, self.quadrant_lock); + if two_line_frame(vertex, first, vertex, second, arc_point).is_none() { + return CmdResult::NeedPoint; + } + let mut dim = DimensionAngular3Pt::new(v3(vertex), v3(first), v3(second)); + dim.definition_point = v3(arc_point); + dim.base.definition_point = dim.definition_point; + dim.base.text_middle_point = dim.definition_point; + dim.base.insertion_point = dim.definition_point; + if self.quadrant_lock.is_some_and(|frame| { + !point_angle_in_frame(vertex, picked_text, frame) + }) { + dim.base.text_middle_point = v3(picked_text); + dim.base.insertion_point = dim.base.text_middle_point; + dim.base.text_user_positioned = true; + } + dim.base.actual_measurement = dim.measurement_degrees(); + crate::entities::dimension::set_dimension_text_override( + &mut dim.base, + self.text_override.clone(), + ); + if let Some(angle) = self.text_angle { + dim.base.text_rotation = angle; + } + self.commit(Dimension::Angular3Pt(dim)) + } + + fn finish_two_line( + &self, + first_start: DVec3, + first_end: DVec3, + second_start: DVec3, + second_end: DVec3, + arc_point: DVec3, + ) -> CmdResult { + let first_start = self.plane.to_local(first_start); + let first_end = self.plane.to_local(first_end); + let second_start = self.plane.to_local(second_start); + let second_end = self.plane.to_local(second_end); + let picked_text = self.plane.to_local(arc_point); + let Some((vertex, _, _)) = two_line_frame( + first_start, + first_end, + second_start, + second_end, + picked_text, + ) else { + return CmdResult::NeedPoint; + }; + let arc_point = locked_arc_point(vertex, picked_text, self.quadrant_lock); + if two_line_frame( + first_start, + first_end, + second_start, + second_end, + arc_point, + ) + .is_none() + { + return CmdResult::NeedPoint; + } + let mut dim = DimensionAngular2Ln::default(); + dim.first_point = v3(first_start); + dim.second_point = v3(first_end); + dim.angle_vertex = v3(second_start); + dim.definition_point = v3(second_end); + dim.dimension_arc = v3(arc_point); + dim.base.definition_point = dim.definition_point; + dim.base.text_middle_point = dim.dimension_arc; + dim.base.insertion_point = dim.dimension_arc; + if self.quadrant_lock.is_some_and(|frame| { + !point_angle_in_frame(vertex, picked_text, frame) + }) { + dim.base.text_middle_point = v3(picked_text); + dim.base.insertion_point = dim.base.text_middle_point; + dim.base.text_user_positioned = true; + } + dim.base.actual_measurement = dim.measurement_degrees(); + crate::entities::dimension::set_dimension_text_override( + &mut dim.base, + self.text_override.clone(), + ); + if let Some(angle) = self.text_angle { + dim.base.text_rotation = angle; + } + self.commit(Dimension::Angular2Ln(dim)) + } + + fn commit(&self, dimension: Dimension) -> CmdResult { + let entity = self.plane.place_entity(EntityType::Dimension(dimension)); + CmdResult::CommitDimension { + entity, + association: DimensionAssociationInput::Explicit(self.source_refs.clone()), + } + } + + fn placement_step(&self) -> bool { + matches!(self.step, Step::ArcPoint3 { .. } | Step::ArcPoint2 { .. }) + } + + fn editor_anchor(&self) -> DVec3 { + match self.step { + Step::ArcPoint3 { vertex, first, second } => { + let radius = vertex.distance(first).max(vertex.distance(second)).max(1.0); + let first_angle = self.plane.angle(vertex, first).unwrap_or(0.0); + let second_angle = self.plane.angle(vertex, second).unwrap_or(first_angle); + let sweep = (second_angle - first_angle).rem_euclid(std::f64::consts::TAU); + let angle = first_angle + sweep * 0.5; + self.plane.to_world( + self.plane.to_local(vertex) + + DVec3::new(angle.cos() * radius, angle.sin() * radius, 0.0), + ) + } + Step::ArcPoint2 { + first_start, + first_end, + second_start, + second_end, + } => { + let first_start = self.plane.to_local(first_start); + let first_end = self.plane.to_local(first_end); + let second_start = self.plane.to_local(second_start); + let second_end = self.plane.to_local(second_end); + two_line_frame( + first_start, + first_end, + second_start, + second_end, + (first_start + second_start) * 0.5, + ) + .map(|(vertex, start, end)| { + self.plane.to_world(vertex + DVec3::new( + ((start + end) * 0.5).cos(), + ((start + end) * 0.5).sin(), + 0.0, + )) + }) + .unwrap_or_else(|| self.plane.to_world(first_start)) + } + _ => self.plane.origin, + } + } + + fn set_quadrant_from_point(&mut self, point: DVec3) -> bool { + let point = self.plane.to_local(point); + let frame = match self.step { + Step::ArcPoint3 { vertex, first, second } => two_line_frame( + self.plane.to_local(vertex), + self.plane.to_local(first), + self.plane.to_local(vertex), + self.plane.to_local(second), + point, + ), + Step::ArcPoint2 { + first_start, + first_end, + second_start, + second_end, + } => two_line_frame( + self.plane.to_local(first_start), + self.plane.to_local(first_end), + self.plane.to_local(second_start), + self.plane.to_local(second_end), + point, + ), + _ => None, + }; + if let Some((_, start, end)) = frame { + self.quadrant_lock = Some((start, end)); + true + } else { + false } } } @@ -57,59 +327,156 @@ impl CadCommand for AngularDimensionCommand { } fn prompt(&self) -> String { + if self.awaiting_quadrant { + return t!("DIMANGULAR Specify quadrant:").into_owned(); + } + if self.awaiting_text { + return if self.mtext_override { + t!("DIMANGULAR Enter formatted dimension text (blank = measured value):") + .into_owned() + } else { + t!("DIMANGULAR Enter dimension text (blank = measured value):").into_owned() + }; + } + if self.awaiting_angle { + return if self.angle_origin.is_some() { + t!("DIMANGULAR Specify second point for text angle:").into_owned() + } else { + t!("DIMANGULAR Specify text angle or first point:").into_owned() + }; + } + if self.selecting_object { + return match self.step { + Step::SecondLine { .. } => t!("DIMANGULAR Select second line:").into_owned(), + _ => t!( + "DIMANGULAR Select arc, circle, line, or specify an angle vertex:" + ) + .into_owned(), + }; + } match self.step { - Step::Vertex => t!("DIMANGULAR Specify angle vertex:").into_owned(), + Step::Vertex => t!( + "DIMANGULAR Specify angle vertex:" + ) + .into_owned(), Step::FirstRay(_) => { t!("DIMANGULAR Specify first extension line point:").into_owned() } - Step::SecondRay { .. } => { + Step::SecondRay { .. } | Step::CircleSecondRay { .. } => { t!("DIMANGULAR Specify second extension line point:").into_owned() } - Step::ArcPoint { .. } => t!("DIMANGULAR Specify dimension arc location:").into_owned(), + Step::SecondLine { .. } => t!("DIMANGULAR Select second line:").into_owned(), + Step::ArcPoint3 { .. } | Step::ArcPoint2 { .. } => t!( + "DIMANGULAR Specify dimension arc location [Mtext/Text/Angle/Quadrant]:" + ) + .into_owned(), } } - fn on_point(&mut self, pt: DVec3) -> CmdResult { + fn on_point(&mut self, point: DVec3) -> CmdResult { + if self.awaiting_quadrant { + if self.set_quadrant_from_point(point) { + self.awaiting_quadrant = false; + } + return CmdResult::NeedPoint; + } + if self.awaiting_angle { + if let Some(origin) = self.angle_origin { + if let Some(angle) = self.plane.angle(origin, point) { + self.text_angle = Some(angle); + self.awaiting_angle = false; + self.angle_origin = None; + } + } else { + self.angle_origin = Some(point); + } + return CmdResult::NeedPoint; + } + if self.selecting_object && matches!(self.step, Step::Vertex) { + self.selecting_object = false; + self.step = Step::FirstRay(point); + return CmdResult::NeedPoint; + } match self.step { Step::Vertex => { - self.step = Step::FirstRay(pt); + self.step = Step::FirstRay(point); CmdResult::NeedPoint } Step::FirstRay(vertex) => { - self.step = Step::SecondRay { vertex, first: pt }; + if point.distance_squared(vertex) <= 1.0e-24 { + return CmdResult::NeedPoint; + } + self.step = Step::SecondRay { vertex, first: point }; CmdResult::NeedPoint } Step::SecondRay { vertex, first } => { - self.step = Step::ArcPoint { - vertex, - first, - second: pt, - }; + if point.distance_squared(vertex) <= 1.0e-24 { + return CmdResult::NeedPoint; + } + let first_local = self.plane.vector_to_local(first - vertex); + let second_local = self.plane.vector_to_local(point - vertex); + if line_line( + [0.0, 0.0], + [first_local.x, first_local.y], + [0.0, 0.0], + [second_local.x, second_local.y], + ) + .is_none() + { + return CmdResult::NeedPoint; + } + self.step = Step::ArcPoint3 { vertex, first, second: point }; CmdResult::NeedPoint } - Step::ArcPoint { - vertex, - first, - second, - } => { - let vertex = self.plane.to_local(vertex); - let first = self.plane.to_local(first); - let second = self.plane.to_local(second); - let pt = self.plane.to_local(pt); - let mut dim = DimensionAngular3Pt::new(v3(vertex), v3(first), v3(second)); - dim.definition_point = v3(pt); - dim.base.definition_point = v3(pt); - dim.base.text_middle_point = v3(pt); - dim.base.insertion_point = v3(pt); - dim.base.actual_measurement = dim.measurement_degrees(); - CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension( - Dimension::Angular3Pt(dim), - ))) + Step::CircleSecondRay { vertex, first, source } => { + if point.distance_squared(vertex) <= 1.0e-24 { + return CmdResult::NeedPoint; + } + self.source_refs = vec![ + Some(DimensionAssociationSource::inferred(source)), + Some(DimensionAssociationSource::inferred(source)), + None, + ]; + self.step = Step::ArcPoint3 { vertex, first, second: point }; + CmdResult::NeedPoint } + Step::SecondLine { .. } => CmdResult::NeedPoint, + Step::ArcPoint3 { vertex, first, second } => { + self.finish_three_point(vertex, first, second, point) + } + Step::ArcPoint2 { + first_start, + first_end, + second_start, + second_end, + } => self.finish_two_line( + first_start, + first_end, + second_start, + second_end, + point, + ), } } fn on_enter(&mut self) -> CmdResult { + if self.awaiting_quadrant { + self.awaiting_quadrant = false; + return CmdResult::NeedPoint; + } + if self.awaiting_text { + self.awaiting_text = false; + return CmdResult::NeedPoint; + } + if self.awaiting_angle { + self.awaiting_angle = false; + self.angle_origin = None; + return CmdResult::NeedPoint; + } + if matches!(self.step, Step::Vertex) { + self.selecting_object = !self.selecting_object; + return CmdResult::NeedPoint; + } CmdResult::Cancel } @@ -117,49 +484,651 @@ impl CadCommand for AngularDimensionCommand { CmdResult::Cancel } - fn on_mouse_move(&mut self, pt: DVec3) -> Option { - let pt = pt.as_vec3(); - match self.step { - Step::Vertex => None, - Step::FirstRay(vertex) => Some(preview_wire(vec![vertex.as_vec3(), pt])), - Step::SecondRay { vertex, first } => Some(preview_wire(vec![ - vertex.as_vec3(), - first.as_vec3(), - Vec3::new(f32::NAN, f32::NAN, f32::NAN), - vertex.as_vec3(), - pt, - ])), - Step::ArcPoint { - vertex, - first, - second, + fn wants_text_input(&self) -> bool { + true + } + + fn wants_text_with_spaces(&self) -> bool { + self.awaiting_text + } + + fn options(&self) -> Vec { + if self.placement_step() && !self.awaiting_text && !self.awaiting_angle && !self.awaiting_quadrant { + vec![ + CmdOption::new("MText", "MTEXT"), + CmdOption::new("Text", "TEXT"), + CmdOption::new("Angle", "ANGLE"), + CmdOption::new("Quadrant", "QUADRANT"), + ] + } else { + Vec::new() + } + } + + fn point_step_accepts_keywords(&self) -> bool { + self.placement_step() + && !self.awaiting_text + && !self.awaiting_angle + && !self.awaiting_quadrant + } + + fn on_text_input(&mut self, text: &str) -> Option { + if self.awaiting_text { + let value = text.trim(); + self.text_override = if value.is_empty() || value == "<>" { + None + } else { + Some(value.to_string()) + }; + self.awaiting_text = false; + return Some(CmdResult::NeedPoint); + } + if self.awaiting_angle { + self.text_angle = if text.trim().is_empty() { + None + } else { + crate::entities::common::parse_typed_angle(text.trim()) + }; + self.awaiting_angle = false; + self.angle_origin = None; + return Some(CmdResult::NeedPoint); + } + if !self.placement_step() { + return None; + } + match text.trim().to_ascii_uppercase().as_str() { + "T" | "TEXT" => { + self.mtext_override = false; + self.awaiting_text = true; + Some(CmdResult::NeedPoint) + } + "M" | "MTEXT" => { + self.mtext_override = true; + Some(CmdResult::SuspendForMTextInput { + pos: self.editor_anchor(), + initial: self.text_override.clone().unwrap_or_default(), + height: 2.5, + }) + } + "A" | "ANGLE" => { + self.awaiting_angle = true; + self.angle_origin = None; + Some(CmdResult::NeedPoint) + } + "Q" | "QUADRANT" => { + self.awaiting_quadrant = true; + Some(CmdResult::NeedPoint) + } + _ => None, + } + } + + fn on_editor_text(&mut self, value: String) { + let value = value.trim(); + self.text_override = if value.is_empty() || value == "<>" { + None + } else { + Some(value.to_string()) + }; + } + + fn on_editor_closed(&mut self, _committed: bool) -> CmdResult { + CmdResult::NeedPoint + } + + fn needs_entity_pick(&self) -> bool { + self.selecting_object + } + + fn entity_pick_accepts_points(&self) -> bool { + self.selecting_object && matches!(self.step, Step::Vertex) + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn inject_before_entity_pick(&self) -> bool { + true + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + self.picked_entity = Some(entity); + } + + fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { + if handle.is_null() { + if matches!(self.step, Step::Vertex) { + self.selecting_object = false; + self.step = Step::FirstRay(point); + } + return CmdResult::NeedPoint; + } + let Some(entity) = self.picked_entity.take() else { + return CmdResult::NeedPoint; + }; + let Some(curve) = picked_curve(&entity, point) else { + return CmdResult::NeedPoint; + }; + if !curve_is_coplanar(&curve, self.plane) { + return CmdResult::NeedPoint; + } + + match (&self.step, curve) { + ( + Step::SecondLine { + first_start, + first_end, + first_source, + first_refs, + }, + PickedCurve::Line { + start: second_start, + end: second_end, + refs: second_refs, + .. + }, + ) => { + if (*first_source == handle && *first_refs == second_refs) + || lines_parallel_in_plane( + self.plane, + *first_start, + *first_end, + second_start, + second_end, + ) + { + return CmdResult::NeedPoint; + } + self.source_refs = vec![ + Some(first_refs[0].bind(*first_source)), + Some(first_refs[1].bind(*first_source)), + Some(second_refs[0].bind(handle)), + Some(second_refs[1].bind(handle)), + ]; + self.selecting_object = false; + self.step = Step::ArcPoint2 { + first_start: *first_start, + first_end: *first_end, + second_start, + second_end, + }; + CmdResult::NeedPoint + } + ( + Step::Vertex, + PickedCurve::Line { + start: first_start, + end: first_end, + refs, + .. + }, + ) => { + self.step = Step::SecondLine { + first_start, + first_end, + first_source: handle, + first_refs: refs, + }; + CmdResult::NeedPoint + } + ( + Step::Vertex, + PickedCurve::Arc { + center, + first, + second, + refs, + .. + }, + ) => { + self.source_refs = refs + .into_iter() + .map(|reference| Some(reference.bind(handle))) + .collect(); + self.selecting_object = false; + self.step = Step::ArcPoint3 { vertex: center, first, second }; + CmdResult::NeedPoint + } + (Step::Vertex, PickedCurve::Circle { center, .. }) => { + if point.distance_squared(center) <= 1.0e-24 { + return CmdResult::NeedPoint; + } + self.selecting_object = false; + self.step = Step::CircleSecondRay { + vertex: center, + first: point, + source: handle, + }; + CmdResult::NeedPoint + } + _ => CmdResult::NeedPoint, + } + } + + fn on_mouse_move(&mut self, point: DVec3) -> Option { + if self.awaiting_angle { + return self + .angle_origin + .map(|origin| preview_wire(vec![origin, point])); + } + let points = match self.step { + Step::Vertex | Step::SecondLine { .. } => return None, + Step::FirstRay(vertex) => vec![vertex, point], + Step::SecondRay { vertex, first } => vec![vertex, first, nan(), vertex, point], + Step::CircleSecondRay { vertex, first, .. } => { + vec![vertex, first, nan(), vertex, point] + } + Step::ArcPoint3 { vertex, first, second } => { + let vertex = self.plane.to_local(vertex); + let first = self.plane.to_local(first); + let second = self.plane.to_local(second); + let point = locked_arc_point( + vertex, + self.plane.to_local(point), + self.quadrant_lock, + ); + angular_preview(vertex, first, second, point) + .into_iter() + .map(|value| if value.is_nan() { value } else { self.plane.to_world(value) }) + .collect() + } + Step::ArcPoint2 { + first_start, + first_end, + second_start, + second_end, } => { - let points = angular_preview( - self.plane.to_local(vertex).as_vec3(), - self.plane.to_local(first).as_vec3(), - self.plane.to_local(second).as_vec3(), - self.plane.to_local(pt.as_dvec3()).as_vec3(), + let first_start = self.plane.to_local(first_start); + let first_end = self.plane.to_local(first_end); + let second_start = self.plane.to_local(second_start); + let second_end = self.plane.to_local(second_end); + let point = self.plane.to_local(point); + let point = two_line_frame( + first_start, + first_end, + second_start, + second_end, + point, + ) + .map(|(vertex, _, _)| locked_arc_point(vertex, point, self.quadrant_lock)) + .unwrap_or(point); + two_line_preview( + first_start, + first_end, + second_start, + second_end, + point, ) .into_iter() - .map(|point| { - if point.x.is_nan() { - point - } else { - self.plane.to_world(point.as_dvec3()).as_vec3() - } - }) - .collect(); - Some(preview_wire(points)) + .map(|value| if value.is_nan() { value } else { self.plane.to_world(value) }) + .collect() } - } + }; + Some(preview_wire(points)) } } -fn v3(pt: DVec3) -> Vector3 { - Vector3::new(pt.x, pt.y, pt.z) +fn picked_curve(entity: &EntityType, click: DVec3) -> Option { + let point = |value: Vector3| DVec3::new(value.x, value.y, value.z); + let normal = |value: Vector3| DVec3::new(value.x, value.y, value.z); + match entity { + EntityType::Line(line) => { + let start = point(line.start); + let end = point(line.end); + (start.distance_squared(end) > 1.0e-24).then_some(PickedCurve::Line { + start, + end, + refs: [SourceLocator::INFERRED; 2], + normal: None, + }) + } + EntityType::Arc(arc) => Some(PickedCurve::Arc { + center: point(arc.center_wcs()), + first: point(arc.start_point_wcs()), + second: point(arc.end_point_wcs()), + refs: [SourceLocator::INFERRED; 3], + normal: normal(arc.normal), + }), + EntityType::Circle(circle) => Some(PickedCurve::Circle { + center: point(circle.center_wcs()), + normal: normal(circle.normal), + }), + EntityType::LwPolyline(polyline) => { + let click = crate::scene::view::transform::wcs_point_to_ocs( + (click.x, click.y, click.z), + (polyline.normal.x, polyline.normal.y, polyline.normal.z), + ); + let points: Vec<_> = polyline + .vertices + .iter() + .map(|vertex| [vertex.location.x, vertex.location.y]) + .collect(); + let bulges: Vec<_> = polyline.vertices.iter().map(|vertex| vertex.bulge).collect(); + let segment = nearest_segment_index( + &points, + &bulges, + polyline.is_closed, + [click.0, click.1], + )?; + let first = polyline.vertices[segment]; + let second = polyline.vertices[(segment + 1) % polyline.vertices.len()]; + polyline_segment( + [first.location.x, first.location.y], + [second.location.x, second.location.y], + first.bulge, + polyline.elevation, + polyline.normal, + segment, + polyline.vertices.len(), + ) + } + EntityType::Polyline2D(polyline) => { + let vertices = &polyline.vertices; + let click = crate::scene::view::transform::wcs_point_to_ocs( + (click.x, click.y, click.z), + (polyline.normal.x, polyline.normal.y, polyline.normal.z), + ); + let points: Vec<_> = vertices + .iter() + .map(|vertex| [vertex.location.x, vertex.location.y]) + .collect(); + let bulges: Vec<_> = vertices.iter().map(|vertex| vertex.bulge).collect(); + let segment = nearest_segment_index( + &points, + &bulges, + polyline.is_closed(), + [click.0, click.1], + )?; + let first = &vertices[segment]; + let second = &vertices[(segment + 1) % vertices.len()]; + polyline_segment( + [first.location.x, first.location.y], + [second.location.x, second.location.y], + first.bulge, + polyline.elevation, + polyline.normal, + segment, + vertices.len(), + ) + } + _ => None, + } } -fn preview_wire(points: Vec) -> WireModel { +fn nearest_segment_index( + points: &[[f64; 2]], + bulges: &[f64], + closed: bool, + click: [f64; 2], +) -> Option { + if points.len() < 2 { + return None; + } + let count = if closed { points.len() } else { points.len() - 1 }; + (0..count).min_by(|first, second| { + polyline_segment_distance_squared( + points[*first], + points[(*first + 1) % points.len()], + bulges.get(*first).copied().unwrap_or(0.0), + click, + ) + .total_cmp(&polyline_segment_distance_squared( + points[*second], + points[(*second + 1) % points.len()], + bulges.get(*second).copied().unwrap_or(0.0), + click, + )) + }) +} + +fn polyline_segment_curve(first: [f64; 2], second: [f64; 2], bulge: f64) -> KernelCurve { + if let Some(arc) = BulgeArc::from_bulge(first, second, bulge) { + let (start_angle, end_angle) = if arc.sweep >= 0.0 { + (arc.start_angle, arc.end_angle) + } else { + (arc.end_angle, arc.start_angle) + }; + KernelCurve::Arc(KernelArc { + centre: arc.center, + radius: arc.radius, + start_angle, + end_angle, + }) + } else { + KernelCurve::Line(KernelLine { start: first, end: second }) + } +} + +fn polyline_segment_distance_squared( + first: [f64; 2], + second: [f64; 2], + bulge: f64, + point: [f64; 2], +) -> f64 { + closest_point(&polyline_segment_curve(first, second, bulge), point) + .distance + .powi(2) +} + +fn polyline_segment( + first: [f64; 2], + second: [f64; 2], + bulge: f64, + elevation: f64, + normal: Vector3, + segment: usize, + vertex_count: usize, +) -> Option { + let to_world = |point: [f64; 2]| { + let value = crate::scene::view::transform::ocs_point_to_wcs( + (point[0], point[1], elevation), + (normal.x, normal.y, normal.z), + ); + DVec3::new(value.0, value.1, value.2) + }; + let first_world = to_world(first); + let second_world = to_world(second); + if first_world.distance_squared(second_world) <= 1.0e-24 { + return None; + } + let refs = [ + SourceLocator::explicit(segment as i32, 0.0), + SourceLocator::explicit(((segment + 1) % vertex_count) as i32, 0.0), + ]; + let normal_world = DVec3::new(normal.x, normal.y, normal.z); + if bulge.abs() <= 1.0e-12 { + return Some(PickedCurve::Line { + start: first_world, + end: second_world, + refs, + normal: Some(normal_world), + }); + } + let center = BulgeArc::from_bulge(first, second, bulge)?.center; + Some(PickedCurve::Arc { + center: to_world(center), + first: first_world, + second: second_world, + refs: [ + SourceLocator::explicit(-4, segment as f64), + refs[0], + refs[1], + ], + normal: normal_world, + }) +} + +fn curve_is_coplanar(curve: &PickedCurve, plane: WorkingPlane) -> bool { + let (points, normal): (Vec, Option) = match curve { + PickedCurve::Line { start, end, normal, .. } => (vec![*start, *end], *normal), + PickedCurve::Arc { center, first, second, normal, .. } => { + (vec![*center, *first, *second], Some(*normal)) + } + PickedCurve::Circle { center, normal } => (vec![*center], Some(*normal)), + }; + if let Some(normal) = normal { + let alignment = normal.normalize_or_zero().dot(plane.z).abs(); + if alignment < 1.0 - 1.0e-7 { + return false; + } + } + let local: Vec<_> = points.into_iter().map(|point| plane.to_local(point)).collect(); + let scale = local + .iter() + .map(|point| point.x.abs().max(point.y.abs()).max(1.0)) + .fold(1.0, f64::max); + local.iter().all(|point| point.z.abs() <= 1.0e-9 * scale) +} + +fn lines_parallel_in_plane( + plane: WorkingPlane, + first_start: DVec3, + first_end: DVec3, + second_start: DVec3, + second_end: DVec3, +) -> bool { + let first_start = plane.to_local(first_start); + let first_end = plane.to_local(first_end); + let second_start = plane.to_local(second_start); + let second_end = plane.to_local(second_end); + let first = first_end - first_start; + let second = second_end - second_start; + line_line( + [first_start.x, first_start.y], + [first.x, first.y], + [second_start.x, second_start.y], + [second.x, second.y], + ) + .is_none() +} + +fn locked_arc_point( + vertex: DVec3, + point: DVec3, + frame: Option<(f64, f64)>, +) -> DVec3 { + let Some((start, end)) = frame else { return point }; + let radius = ((point.x - vertex.x).powi(2) + (point.y - vertex.y).powi(2)).sqrt(); + if radius <= 1.0e-12 { + return point; + } + let angle = (start + end) * 0.5; + DVec3::new( + vertex.x + angle.cos() * radius, + vertex.y + angle.sin() * radius, + vertex.z, + ) +} + +fn point_angle_in_frame(vertex: DVec3, point: DVec3, frame: (f64, f64)) -> bool { + let angle = (point.y - vertex.y).atan2(point.x - vertex.x); + let sweep = frame.1 - frame.0; + (angle - frame.0).rem_euclid(std::f64::consts::TAU) <= sweep + 1.0e-9 +} + +fn two_line_frame( + first_start: DVec3, + first_end: DVec3, + second_start: DVec3, + second_end: DVec3, + arc_point: DVec3, +) -> Option<(DVec3, f64, f64)> { + let first_direction = first_end - first_start; + let second_direction = second_end - second_start; + let (t, _) = line_line( + [first_start.x, first_start.y], + [first_direction.x, first_direction.y], + [second_start.x, second_start.y], + [second_direction.x, second_direction.y], + )?; + let vertex = first_start + first_direction * t; + let target = (arc_point.y - vertex.y).atan2(arc_point.x - vertex.x); + let mut best: Option<(f64, f64, f64)> = None; + for first_sign in [1.0, -1.0] { + for second_sign in [1.0, -1.0] { + let first = first_direction * first_sign; + let second = second_direction * second_sign; + for (start, end) in [ + (first.y.atan2(first.x), second.y.atan2(second.x)), + (second.y.atan2(second.x), first.y.atan2(first.x)), + ] { + let sweep = (end - start).rem_euclid(std::f64::consts::TAU); + if sweep <= 1.0e-9 || sweep > std::f64::consts::PI { + continue; + } + let selected = (target - start).rem_euclid(std::f64::consts::TAU); + if selected <= sweep + 1.0e-9 && best.is_none_or(|known| sweep < known.2) { + best = Some((start, start + sweep, sweep)); + } + } + } + } + best.map(|(start, end, _)| (vertex, start, end)) +} + +fn angular_preview(vertex: DVec3, first: DVec3, second: DVec3, arc_point: DVec3) -> Vec { + let Some((_, start, end)) = two_line_frame(vertex, first, vertex, second, arc_point) else { + return vec![vertex, first, nan(), vertex, second]; + }; + angular_preview_with_frame(vertex, first, second, arc_point, start, end) +} + +fn two_line_preview( + first_start: DVec3, + first_end: DVec3, + second_start: DVec3, + second_end: DVec3, + arc_point: DVec3, +) -> Vec { + let Some((vertex, start, end)) = two_line_frame( + first_start, + first_end, + second_start, + second_end, + arc_point, + ) else { + return vec![first_start, first_end, nan(), second_start, second_end]; + }; + angular_preview_with_frame(vertex, first_start, second_start, arc_point, start, end) +} + +fn angular_preview_with_frame( + vertex: DVec3, + first: DVec3, + second: DVec3, + arc_point: DVec3, + start: f64, + end: f64, +) -> Vec { + let radius = vertex.distance(arc_point); + let first_end = vertex + DVec3::new(start.cos(), start.sin(), 0.0) * radius; + let second_end = vertex + DVec3::new(end.cos(), end.sin(), 0.0) * radius; + let mut points = vec![first, first_end, nan(), second, second_end, nan()]; + points.extend( + cadkernel::geom2d::tessellate::arc( + [vertex.x, vertex.y], + radius, + start, + end, + vertex.z, + cadkernel::geom2d::tessellate::DEFAULT_SEGMENTS_PER_RADIAN, + ) + .into_iter() + .map(DVec3::from_array), + ); + points +} + +fn v3(point: DVec3) -> Vector3 { + Vector3::new(point.x, point.y, point.z) +} + +fn nan() -> DVec3 { + DVec3::splat(f64::NAN) +} + +fn preview_wire(points: Vec) -> WireModel { WireModel { point_marker: None, taper_widths: Vec::new(), @@ -173,11 +1142,11 @@ fn preview_wire(points: Vec) -> WireModel { render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), - dash_from_start: false, - dash_align_end: None, - text_verts: Vec::new(), + dash_from_start: false, + dash_align_end: None, + text_verts: Vec::new(), name: "dimangular_preview".to_string(), - points: points.into_iter().map(|p| [p.x, p.y, p.z]).collect(), + points: points.into_iter().map(|point| [point.x as f32, point.y as f32, point.z as f32]).collect(), points_low: Vec::new(), color: WireModel::CYAN, selected: false, @@ -195,38 +1164,4 @@ fn preview_wire(points: Vec) -> WireModel { } } -fn angular_preview(vertex: Vec3, first: Vec3, second: Vec3, arc_pt: Vec3) -> Vec { - let mut points = vec![ - vertex, - first, - Vec3::new(f32::NAN, f32::NAN, f32::NAN), - vertex, - second, - Vec3::new(f32::NAN, f32::NAN, f32::NAN), - ]; - let r = vertex.distance(arc_pt); - if r <= 1e-6 { - return points; - } - let a0 = (first.y - vertex.y).atan2(first.x - vertex.x); - let mut a1 = (second.y - vertex.y).atan2(second.x - vertex.x); - let mut delta = a1 - a0; - while delta <= 0.0 { - delta += std::f32::consts::TAU; - } - if delta > std::f32::consts::PI { - a1 -= std::f32::consts::TAU; - delta = a1 - a0; - } - let steps = 24; - for i in 0..=steps { - let t = i as f32 / steps as f32; - let a = a0 + delta * t; - points.push(vertex + Vec3::new(a.cos() * r, a.sin() * r, 0.0)); - } - points -} - - -// ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DIMANGULAR"] }); // AngularDimensionCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMANGULAR"] }); diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index 00c2bd56f..c2f927c80 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -5,7 +5,7 @@ use cadkernel::geom2d::{ closest_point, Circle as KernelCircle, Curve, Line as KernelLine, }; -use crate::command::{CadCommand, CmdResult, WorkingPlane}; +use crate::command::{CadCommand, CmdResult, DimensionAssociationInput, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use glam::DVec3; @@ -186,7 +186,7 @@ impl CadCommand for LinearDimensionCommand { )); CmdResult::CommitDimension { entity, - source: self.source_handle, + association: DimensionAssociationInput::Infer(self.source_handle), } } } diff --git a/src/scene/dimension_assoc.rs b/src/scene/dimension_assoc.rs index 2814695ea..019a46e44 100644 --- a/src/scene/dimension_assoc.rs +++ b/src/scene/dimension_assoc.rs @@ -6,10 +6,12 @@ use acadrust::objects::{ use acadrust::types::{Handle, Vector3}; use acadrust::EntityType; use cadkernel::geom2d::{ - closest_point, Circle as KernelCircle, Curve as KernelCurve, + closest_point, BulgeArc, Circle as KernelCircle, Curve as KernelCurve, }; use std::f64::consts::TAU; +use crate::command::DimensionAssociationSource; + use super::{ChangeKind, Scene}; fn point_distance_squared(first: Vector3, second: Vector3) -> f64 { @@ -34,10 +36,66 @@ fn circle_curve(circle: &acadrust::entities::Circle) -> KernelCurve { }) } +fn bulge_center_world( + first: [f64; 2], + second: [f64; 2], + bulge: f64, + elevation: f64, + normal: Vector3, +) -> Option { + let center = BulgeArc::from_bulge(first, second, bulge)?.center; + Some(ocs_point(center[0], center[1], elevation, normal)) +} + +fn next_segment_index(count: usize, closed: bool, segment: usize) -> Option { + if segment + 1 < count { + Some(segment + 1) + } else if closed && segment < count { + Some(0) + } else { + None + } +} + +fn polyline_arc_center(entity: &EntityType, segment: usize) -> Option { + match entity { + EntityType::LwPolyline(polyline) => { + let count = polyline.vertices.len(); + let first = *polyline.vertices.get(segment)?; + let second = *polyline + .vertices + .get(next_segment_index(count, polyline.is_closed, segment)?)?; + bulge_center_world( + [first.location.x, first.location.y], + [second.location.x, second.location.y], + first.bulge, + polyline.elevation, + polyline.normal, + ) + } + EntityType::Polyline2D(polyline) => { + let count = polyline.vertices.len(); + let first = polyline.vertices.get(segment)?; + let second = polyline + .vertices + .get(next_segment_index(count, polyline.is_closed(), segment)?)?; + bulge_center_world( + [first.location.x, first.location.y], + [second.location.x, second.location.y], + first.bulge, + polyline.elevation, + polyline.normal, + ) + } + _ => None, + } +} + fn source_points(entity: &EntityType) -> Vec { match entity { EntityType::Line(line) => vec![line.start, line.end], EntityType::Arc(arc) => vec![arc.start_point_wcs(), arc.end_point_wcs()], + EntityType::Circle(_) => Vec::new(), EntityType::LwPolyline(polyline) => polyline .vertices .iter() @@ -66,10 +124,38 @@ fn source_points(entity: &EntityType) -> Vec { } } -fn source_marker(entity: &EntityType, point: Vector3) -> Option { - if matches!(entity, EntityType::Circle(_)) { - return Some(0); +fn source_reference(entity: &EntityType, point: Vector3) -> Option<(i32, f64)> { + let curve_parameter = match entity { + EntityType::Circle(circle) => { + let center = circle.center_wcs(); + if point_distance_squared(center, point) <= 1e-16 { + return Some((-3, 0.0)); + } + let (axis_x, axis_y) = circle.axes_wcs(); + let offset = point - center; + Some(offset.dot(&axis_y).atan2(offset.dot(&axis_x))) + } + EntityType::Arc(arc) => { + let local = crate::scene::view::transform::wcs_point_to_ocs( + (point.x, point.y, point.z), + (arc.normal.x, arc.normal.y, arc.normal.z), + ); + let dx = local.0 - arc.center.x; + let dy = local.1 - arc.center.y; + if dx * dx + dy * dy <= 1e-16 { + return Some((-3, 0.0)); + } + Some(dy.atan2(dx)) + } + _ => None, + }; + if let Some(parameter) = curve_parameter { + return Some((-2, parameter)); } + source_marker(entity, point).map(|marker| (marker, 0.0)) +} + +fn source_marker(entity: &EntityType, point: Vector3) -> Option { source_points(entity) .into_iter() .enumerate() @@ -83,6 +169,26 @@ fn source_marker(entity: &EntityType, point: Vector3) -> Option { fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Option { let source = *reference.xrefs.first()?; let entity = scene.document.get_entity(source)?; + if reference.main_gs_marker == -4 { + let segment = reference.osnap_distance.round().max(0.0) as usize; + return polyline_arc_center(entity, segment); + } + if reference.main_gs_marker == -3 { + return match entity { + EntityType::Circle(circle) => Some(circle.center_wcs()), + EntityType::Arc(arc) => Some(arc.center_wcs()), + _ => None, + }; + } + if reference.main_gs_marker == -2 { + return match entity { + EntityType::Circle(circle) => { + Some(circle.point_at_angle_wcs(reference.osnap_distance)) + } + EntityType::Arc(arc) => Some(arc.point_at_angle_wcs(reference.osnap_distance)), + _ => None, + }; + } if let EntityType::Circle(circle) = entity { let stored = crate::scene::view::transform::wcs_point_to_ocs( ( @@ -115,7 +221,7 @@ fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Opti .copied() } -fn dimension_points(dimension: &Dimension) -> Option<[Vector3; 2]> { +fn dimension_inference_points(dimension: &Dimension) -> Option<[Vector3; 2]> { match dimension { Dimension::Linear(linear) => Some([linear.first_point, linear.second_point]), Dimension::Aligned(aligned) => Some([aligned.first_point, aligned.second_point]), @@ -139,15 +245,23 @@ fn source_distance_squared(entity: &EntityType, point: Vector3) -> Option { .min_by(f64::total_cmp) } -fn source_parameter(entity: &EntityType, point: Vector3) -> f64 { - let EntityType::Circle(circle) = entity else { - return 0.0; - }; - let point = crate::scene::view::transform::wcs_point_to_ocs( - (point.x, point.y, point.z), - (circle.normal.x, circle.normal.y, circle.normal.z), - ); - circle_curve(circle).parameter_at([point.0, point.1]) * TAU +fn dimension_reference_points(dimension: &Dimension) -> Vec { + match dimension { + Dimension::Linear(linear) => vec![linear.first_point, linear.second_point], + Dimension::Aligned(aligned) => vec![aligned.first_point, aligned.second_point], + Dimension::Angular3Pt(angular) => vec![ + angular.angle_vertex, + angular.first_point, + angular.second_point, + ], + Dimension::Angular2Ln(angular) => vec![ + angular.first_point, + angular.second_point, + angular.angle_vertex, + angular.definition_point, + ], + _ => Vec::new(), + } } pub(crate) fn dimension_is_associative( @@ -176,31 +290,50 @@ impl Scene { pub(crate) fn attach_dimension_association( &mut self, dimension: Handle, - sources: [Option; 2], + sources: Vec>, ) { - let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension) + self.attach_dimension_association_sources( + dimension, + sources + .into_iter() + .map(|source| source.map(DimensionAssociationSource::inferred)) + .collect(), + ); + } + + pub(crate) fn attach_dimension_association_sources( + &mut self, + dimension: Handle, + sources: Vec>, + ) { + let Some(EntityType::Dimension(dimension_entity)) = + self.document.get_entity(dimension) else { return; }; - let Some(source_data) = dimension_points(entity) else { + let source_data = dimension_reference_points(dimension_entity); + if source_data.is_empty() { return; - }; - let resolved: [Option<(Handle, i32, f64, u8)>; 2] = std::array::from_fn(|index| { - let source = sources[index]?; - let entity = self.document.get_entity(source)?; - source_marker(entity, source_data[index]).map(|marker| { - ( - source, - marker, - source_parameter(entity, source_data[index]), - if matches!(entity, EntityType::Circle(_)) { - 10 - } else { - 1 - }, - ) + } + let resolved: Vec> = source_data + .iter() + .take(4) + .enumerate() + .map(|(index, point)| { + let source = sources.get(index).copied().flatten()?; + let entity = self.document.get_entity(source.handle)?; + let (marker, parameter) = match source.marker { + Some(marker) => (marker, source.parameter), + None => source_reference(entity, *point)?, + }; + let osnap_type = if matches!(entity, EntityType::Circle(_)) { + 10 + } else { + 1 + }; + Some((source.handle, marker, parameter, osnap_type)) }) - }); + .collect(); if resolved.iter().all(Option::is_none) { return; } @@ -223,6 +356,9 @@ impl Scene { std::array::from_fn(|_| Vec::new()); let mut associativity = 0; for (index, resolved) in resolved.into_iter().enumerate() { + if index >= references.len() { + break; + } if let Some((source, marker, parameter, osnap_type)) = resolved { associativity |= 1 << index; references[index].push(reference( @@ -250,7 +386,7 @@ impl Scene { .insert(association_handle, ObjectType::Associative(object)); let mut reactor_targets = vec![dimension]; - reactor_targets.extend(sources.into_iter().flatten()); + reactor_targets.extend(sources.into_iter().flatten().map(|source| source.handle)); reactor_targets.sort_by_key(|handle| handle.value()); reactor_targets.dedup(); for handle in reactor_targets { @@ -270,7 +406,7 @@ impl Scene { else { return [None, None]; }; - let Some(points) = dimension_points(entity) else { + let Some(points) = dimension_inference_points(entity) else { return [None, None]; }; points.map(|point| { @@ -318,13 +454,12 @@ impl Scene { let mut refreshed = Vec::new(); for association in associations { - let first = association.references[0] - .first() - .and_then(|reference| resolve_reference(self, reference)); - let second = association.references[1] - .first() - .and_then(|reference| resolve_reference(self, reference)); - if first.is_none() && second.is_none() { + let resolved: [Option; 4] = std::array::from_fn(|index| { + association.references[index] + .first() + .and_then(|reference| resolve_reference(self, reference)) + }); + if resolved.iter().all(Option::is_none) { continue; } let Some(EntityType::Dimension(dimension)) = @@ -334,27 +469,53 @@ impl Scene { }; match dimension { Dimension::Linear(linear) => { - if let Some(first) = first { - linear.first_point = first; + if let Some(point) = resolved[0] { + linear.first_point = point; } - if let Some(second) = second { - linear.second_point = second; + if let Some(point) = resolved[1] { + linear.second_point = point; } - linear.base.actual_measurement = linear.measurement(); linear.base.definition_point = linear.definition_point; } Dimension::Aligned(aligned) => { - if let Some(first) = first { - aligned.first_point = first; + if let Some(point) = resolved[0] { + aligned.first_point = point; } - if let Some(second) = second { - aligned.second_point = second; + if let Some(point) = resolved[1] { + aligned.second_point = point; } - aligned.base.actual_measurement = aligned.measurement(); aligned.base.definition_point = aligned.definition_point; } + Dimension::Angular3Pt(angular) => { + if let Some(point) = resolved[0] { + angular.angle_vertex = point; + } + if let Some(point) = resolved[1] { + angular.first_point = point; + } + if let Some(point) = resolved[2] { + angular.second_point = point; + } + angular.base.definition_point = angular.definition_point; + } + Dimension::Angular2Ln(angular) => { + if let Some(point) = resolved[0] { + angular.first_point = point; + } + if let Some(point) = resolved[1] { + angular.second_point = point; + } + if let Some(point) = resolved[2] { + angular.angle_vertex = point; + } + if let Some(point) = resolved[3] { + angular.definition_point = point; + } + angular.base.definition_point = angular.definition_point; + } _ => continue, } + dimension.base_mut().actual_measurement = dimension.measurement(); refreshed.push((association.dimension, ChangeKind::Modified)); } refreshed