diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 887a2c132..28f435735 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -3599,6 +3599,7 @@ dimstyle = .move-text-outside-first = نقل النص إلى الخارج أولًا .period = نقطة .space = مسافة + .surveyors-units = وحدات المسّاح style-manager = .alignment-spacing = تباعد المحاذاة diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 900027361..793c311c2 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -3587,6 +3587,7 @@ dimstyle = .move-text-outside-first = Първо премести текста навън .period = Точка .space = Интервал + .surveyors-units = Геодезически единици style-manager = .alignment-spacing = Разстояние на подравняване diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 5ba65671a..654730c42 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -3586,6 +3586,7 @@ dimstyle = .move-text-outside-first = Nejprve přesuňte text ven .period = Období .space = Mezerník + .surveyors-units = Zeměměřické jednotky style-manager = .alignment-spacing = Rozteč zarovnání diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 4613c57a5..2e4326ac7 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -3585,6 +3585,7 @@ dimstyle = .move-text-outside-first = Zuerst Text nach außen verschieben .period = Punkt .space = Leerzeichen + .surveyors-units = Vermessungseinheiten style-manager = .alignment-spacing = Ausrichtungsabstand diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index a90b105d2..7a73eebd1 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -3588,6 +3588,7 @@ dimstyle = .move-text-outside-first = Move text outside first .period = Period .space = Space + .surveyors-units = Surveyor's units style-manager = .alignment-spacing = Alignment spacing diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index f525cf334..70f76226a 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -3587,6 +3587,7 @@ dimstyle = .move-text-outside-first = Mover primero el texto fuera .period = Punto .space = Espacio + .surveyors-units = Unidades de topografía style-manager = .alignment-spacing = Espaciado de alineación diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index c7a02fc60..0ad037e69 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -3586,6 +3586,7 @@ dimstyle = .move-text-outside-first = Siirrä teksti ensin ulos .period = Kausi .space = Välilyönti + .surveyors-units = Maanmittausyksiköt style-manager = .alignment-spacing = Tasausvälit diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index d44dbfa9a..f19223ccd 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -3586,6 +3586,7 @@ dimstyle = .move-text-outside-first = Déplacer d’abord le texte à l’extérieur .period = Point .space = Espace + .surveyors-units = Unités d’arpentage style-manager = .alignment-spacing = Espacement d’alignement diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 3605c51c3..dda9941b0 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -3575,6 +3575,7 @@ dimstyle = .move-text-outside-first = पहले पाठ बाहर ले जाएँ .period = दशमलव बिंदु .space = रिक्त स्थान + .surveyors-units = सर्वेक्षण इकाइयाँ style-manager = .alignment-spacing = संरेखण अंतराल diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index cec83afd6..4da10cd4c 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -3586,6 +3586,7 @@ dimstyle = .move-text-outside-first = Először mozgassa ki a szöveget .period = Időszak .space = Szóköz + .surveyors-units = Földmérési egységek style-manager = .alignment-spacing = Igazítási távolság diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 085593f51..f4d5c3da8 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -3586,6 +3586,7 @@ dimstyle = .move-text-outside-first = Sposta prima il testo all'esterno .period = Periodo .space = Spazio + .surveyors-units = Unità topografiche style-manager = .alignment-spacing = Spaziatura di allineamento diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 32ac87b24..d1aefb13e 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -3575,6 +3575,7 @@ dimstyle = .move-text-outside-first = 先に寸法値を外側へ移動 .period = ピリオド .space = 空白 + .surveyors-units = 測量単位 style-manager = .alignment-spacing = 位置合わせ間隔 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 1d5e02afd..36097d154 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -3586,6 +3586,7 @@ dimstyle = .move-text-outside-first = 먼저 텍스트를 외부로 이동하세요. .period = 기간 .space = 스페이스바 + .surveyors-units = 측량 단위 style-manager = .alignment-spacing = 정렬 간격 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 9ab4a95d7..8344d0d9c 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -3587,6 +3587,7 @@ dimstyle = .move-text-outside-first = Eerst tekst naar buiten verplaatsen .period = Punt .space = Spatie + .surveyors-units = Landmeeteenheden style-manager = .alignment-spacing = Uitlijningsafstand diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 48f6eeca3..b1374b8f7 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -3586,6 +3586,7 @@ dimstyle = .move-text-outside-first = Najpierw przenieś tekst na zewnątrz .period = Okres .space = Spacja + .surveyors-units = Jednostki geodezyjne style-manager = .alignment-spacing = Rozstaw wyrównania diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 2569877e3..406731e2a 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -3587,6 +3587,7 @@ dimstyle = .move-text-outside-first = Mover primeiro o texto para fora .period = Ponto .space = Espaço + .surveyors-units = Unidades de agrimensura style-manager = .alignment-spacing = Espaçamento de alinhamento diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index d52f53b18..779113fad 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -3590,6 +3590,7 @@ dimstyle = .move-text-outside-first = Сначала вынести текст .period = Точка .space = Пробел + .surveyors-units = Геодезические единицы style-manager = .alignment-spacing = Интервал выравнивания diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index b22e844fd..7a757079b 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -3511,6 +3511,7 @@ dimstyle = .move-text-outside-first = Önce metni dışarı taşı .period = Nokta .space = Boşluk + .surveyors-units = Haritacılık birimleri style-manager = .alignment-spacing = Hizalama aralığı diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 1d2df873e..d93c789ba 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -3570,6 +3570,7 @@ dimstyle = .move-text-outside-first = 优先将文字移到外部 .period = 句点 .space = 空格 + .surveyors-units = 测量单位 style-manager = .alignment-spacing = 对齐间距 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 1e3297546..89f6ab211 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -3582,6 +3582,7 @@ dimstyle = .move-text-outside-first = 優先將文字移到外部 .period = 句點 .space = 空白鍵 + .surveyors-units = 測量單位 style-manager = .alignment-spacing = 對齊間距 diff --git a/src/app/expr_eval.rs b/src/app/expr_eval.rs index 7e6163d52..be064efcd 100644 --- a/src/app/expr_eval.rs +++ b/src/app/expr_eval.rs @@ -207,7 +207,13 @@ impl Parser { Ok(left) } - // power → unary ('^' unary)? + // power → unary ('^' power)? + // + // The exponent recurses into `parse_power`, not `parse_unary`, so `^` is + // right-associative — `2^3^2` is `2^(3^2)`, the standard reading. Binding + // it to the left both gave the wrong number and left the trailing `^2` + // unconsumed, which the caller's scan then handed back as literal text: + // `2^3^2` evaluated to the string "8^2" instead of 512. fn parse_power(&mut self, depth: usize) -> Result { if depth > MAX_RECURSION { return Err(()); @@ -215,7 +221,7 @@ impl Parser { let base = self.parse_unary(depth + 1)?; if let Some(Token::Caret) = self.peek() { self.advance(); - let exp = self.parse_unary(depth + 1)?; + let exp = self.parse_power(depth + 1)?; return Ok(base.powf(exp)); } Ok(base) @@ -591,6 +597,21 @@ mod tests { assert_eq!(eval_to_string("(((10)))"), "10"); } + // `^` is right-associative, so a chained exponent evaluates as one number + // instead of stopping at the first `^` and leaving the rest as text. + #[test] + fn power_is_right_associative() { + assert_eq!(eval_to_string("2^3^2"), "512"); + assert_eq!(eval_to_string("2^3^2+1"), "513"); + assert_eq!(eval_to_string("4^0.5^2"), "1.4142135623730951"); // 4^(1/4) + assert_eq!(eval_to_string("2^2^2^2"), "65536"); + // A single exponent, and a parenthesised one, are unaffected. + assert_eq!(eval_to_string("2^3"), "8"); + assert_eq!(eval_to_string("2^(3^2)"), "512"); + assert_eq!(eval_to_string("(2^3)^2"), "64"); + assert_eq!(eval_to_string("2^-1"), "0.5"); + } + #[test] fn unary_ops() { assert_eq!(eval_to_string("--5"), "5"); diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 61364a568..730cb421c 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -3803,28 +3803,45 @@ fn tessellate_dimension_inner( ); // Per-spec colours: DIMCLRD (dim/arrows), DIMCLRE (ext), DIMCLRT (text). - // 0=ByBlock and 256=ByLayer fall through to entity_color. DIMCLRD also - // honours a per-object ACAD_DSTYLE override (code 176) so an edited - // dim-line colour renders even without touching the style. + // 0=ByBlock and 256=ByLayer fall through to entity_color. Each honours the + // per-object ACAD_DSTYLE override (codes 176/177/178) the Properties panel + // reads and writes, so an edited colour renders without touching the style. + let xd = &dim.base().common.extended_data; let dim_color = if selected { WireModel::SELECTED } else { - let dim_clr = crate::entities::dim_override::int( - &dim.base().common.extended_data, - crate::entities::dim_override::DIMCLRD, + resolve_dim_color( + dim_color_index( + xd, + crate::entities::dim_override::DIMCLRD, + style.map(|s| s.dimclrd).unwrap_or(0), + ), + entity_color, ) - .unwrap_or_else(|| style.map(|s| s.dimclrd).unwrap_or(0)); - resolve_dim_color(dim_clr, entity_color) }; let ext_color = if selected { WireModel::SELECTED } else { - resolve_dim_color(style.map(|s| s.dimclre).unwrap_or(0), entity_color) + resolve_dim_color( + dim_color_index( + xd, + crate::entities::dim_override::DIMCLRE, + style.map(|s| s.dimclre).unwrap_or(0), + ), + entity_color, + ) }; let text_color = if selected { entity_color // text wire color set by inner tessellate; keep entity tint } else { - resolve_dim_color(style.map(|s| s.dimclrt).unwrap_or(0), entity_color) + resolve_dim_color( + dim_color_index( + xd, + crate::entities::dim_override::DIMCLRT, + style.map(|s| s.dimclrt).unwrap_or(0), + ), + entity_color, + ) }; let snap_pts = dimension_snap_pts(dim); @@ -4194,6 +4211,18 @@ fn tessellate_dimension_inner( wires } +/// The ACI index a dimension colour renders with: the per-object ACAD_DSTYLE +/// override when the entity carries one, else the dimension style's value. +/// The Properties panel reads and writes these same overrides, so the renderer +/// has to consult them or an edited colour shows in the panel and nowhere else. +fn dim_color_index( + xd: &acadrust::xdata::ExtendedData, + code: i16, + inherited: i16, +) -> i16 { + crate::entities::dim_override::int(xd, code).unwrap_or(inherited) +} + fn resolve_dim_color(idx: i16, fallback: [f32; 4]) -> [f32; 4] { // DIMCLR* convention: 0 = BYBLOCK, 256 = BYLAYER → entity colour wins. if idx == 0 || idx == 256 { @@ -6432,10 +6461,16 @@ fn format_with_unit( } fn format_engineering(inches: f64, dec: usize) -> String { - let sign = if inches < 0.0 { "-" } else { "" }; - let abs = inches.abs(); - let feet = (abs / 12.0).trunc(); - let rem_in = abs - feet * 12.0; + // Round to the printed precision *first*, then split, so inches that round + // up to a full foot carry into the feet instead of printing an + // out-of-range "2'-12.00"". Same reasoning as `format_architectural`, + // where the grid is the fraction denominator rather than a decimal place. + let scale = 10f64.powi(dec.min(15) as i32); + let ticks = (inches.abs() * scale).round(); + let per_foot = 12.0 * scale; + let sign = if inches < 0.0 && ticks != 0.0 { "-" } else { "" }; + let feet = (ticks / per_foot).trunc(); + let rem_in = (ticks - feet * per_foot) / scale; format!("{}{:.0}'-{:.*}\"", sign, feet, dec, rem_in) } @@ -7349,9 +7384,71 @@ mod angular_unit_tests { } } +#[cfg(test)] +mod dim_color_override_tests { + use super::dim_color_index; + use crate::entities::dim_override::{DIMCLRD, DIMCLRE, DIMCLRT}; + use acadrust::xdata::{ExtendedData, ExtendedDataRecord, XDataValue}; + + // An ACAD/DSTYLE record holding the overrides the Properties panel writes. + fn xdata(overrides: &[(i16, i16)]) -> ExtendedData { + let mut values = vec![ + XDataValue::String("DSTYLE".to_string()), + XDataValue::ControlString("{".to_string()), + ]; + for (code, aci) in overrides { + values.push(XDataValue::Integer16(*code)); + values.push(XDataValue::Integer16(*aci)); + } + values.push(XDataValue::ControlString("}".to_string())); + let mut record = ExtendedDataRecord::new("ACAD"); + for value in values { + record.add_value(value); + } + let mut xd = ExtendedData::new(); + xd.add_record(record); + xd + } + + // Every dim colour honours its per-object override, not just DIMCLRD. + // Text and extension-line colours edited on the Properties panel used to + // show there and render with the style/layer colour anyway. (#898) + #[test] + fn override_wins_over_style_for_every_dim_colour() { + let xd = xdata(&[(DIMCLRD, 1), (DIMCLRE, 3), (DIMCLRT, 5)]); + assert_eq!(dim_color_index(&xd, DIMCLRD, 256), 1); + assert_eq!(dim_color_index(&xd, DIMCLRE, 256), 3); + assert_eq!(dim_color_index(&xd, DIMCLRT, 256), 5); + } + + // Only the overridden variable changes; the rest still inherit the style. + #[test] + fn absent_override_falls_back_to_the_style_value() { + let xd = xdata(&[(DIMCLRT, 5)]); + assert_eq!(dim_color_index(&xd, DIMCLRT, 256), 5); + assert_eq!(dim_color_index(&xd, DIMCLRD, 7), 7); + assert_eq!(dim_color_index(&xd, DIMCLRE, 256), 256); + } + + // An entity with no override record at all is unaffected. + #[test] + fn no_record_uses_the_style_value() { + let xd = ExtendedData::new(); + assert_eq!(dim_color_index(&xd, DIMCLRT, 2), 2); + } + + // ByLayer (256) and ByBlock (0) stay meaningful as override values, so an + // explicit "back to ByLayer" edit is not mistaken for "no override". + #[test] + fn bylayer_override_is_kept() { + let xd = xdata(&[(DIMCLRT, 256)]); + assert_eq!(dim_color_index(&xd, DIMCLRT, 5), 256); + } +} + #[cfg(test)] mod arch_format_tests { - use super::{format_architectural, format_fractional}; + use super::{format_architectural, format_engineering, format_fractional}; // A 3ft object that measures 35.99" (float noise) must carry the rounded // fraction up through inches into feet — not print "2'-11 1"". (Regression @@ -7384,4 +7481,27 @@ mod arch_format_tests { assert_eq!(format_fractional(11.999, 4, 2), "12"); assert_eq!(format_fractional(6.5, 4, 2), "6 1/2"); } + + // Engineering units round to DIMDEC before the feet/inches split, so a + // remainder that rounds up to 12" carries instead of printing "2'-12.00"". + #[test] + fn engineering_carries_inches_up_to_feet() { + assert_eq!(format_engineering(35.999, 2), "3'-0.00\""); + assert_eq!(format_engineering(11.999, 2), "1'-0.00\""); + assert_eq!(format_engineering(11.9, 0), "1'-0\""); + } + + #[test] + fn engineering_normal_values_unchanged() { + assert_eq!(format_engineering(30.5, 2), "2'-6.50\""); + assert_eq!(format_engineering(36.0, 2), "3'-0.00\""); + assert_eq!(format_engineering(0.0, 2), "0'-0.00\""); + assert_eq!(format_engineering(-30.25, 2), "-2'-6.25\""); + } + + // A magnitude that rounds away to zero must not keep a lone minus sign. + #[test] + fn engineering_negative_zero_has_no_sign() { + assert_eq!(format_engineering(-0.001, 2), "0'-0.00\""); + } } diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 5de56c86a..510dd9adf 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -3224,6 +3224,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Suppress second extension line" => Some(("dimstyle", "suppress-second-extension-line")), "Suppress zero feet and zero inches" => Some(("dimstyle", "suppress-zero-feet-and-inches")), "Suppress zero feet; show zero inches" => Some(("dimstyle", "suppress-zero-feet-show-zero-inches")), + "Surveyor's units" => Some(("dimstyle", "surveyors-units")), "Symbols and Arrows" => Some(("dimstyle", "symbols-and-arrows")), "Symmetrical" => Some(("dimstyle", "symmetrical")), "Text background" => Some(("dimstyle", "text-background")),