From 25515fa33cf91109a55c6bde0f489bbfd4f1c01f Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 3 Aug 2026 02:01:47 +0300 Subject: [PATCH] feat(styles): overhaul style managers Unify editor layouts, live previews, comparisons, read-only behavior, and localized controls across all named style managers. Refs #629 --- locales/de-DE/opencadstudio.ftl | 182 ++++++ locales/en-US/opencadstudio.ftl | 182 ++++++ locales/fr-FR/opencadstudio.ftl | 182 ++++++ locales/hi-IN/opencadstudio.ftl | 182 ++++++ locales/nl-NL/opencadstudio.ftl | 182 ++++++ locales/ru-RU/opencadstudio.ftl | 182 ++++++ locales/tr-TR/opencadstudio.ftl | 182 ++++++ locales/zh-CN/opencadstudio.ftl | 182 ++++++ src/app/mod.rs | 61 +- src/app/style_ops.rs | 30 +- src/app/update/mod.rs | 216 ++++++- src/app/update/style.rs | 149 ++++- src/app/view/modal.rs | 470 ++++++++++++++-- src/entities/dim_override.rs | 77 +++ src/entities/dimension.rs | 211 +++++-- src/locale_catalog.rs | 182 ++++++ src/scene/cache/block_cache.rs | 119 ++-- src/scene/convert/tess.rs | 120 +--- src/scene/convert/tessellate.rs | 93 ++- src/scene/entity.rs | 6 +- src/ui/style/dimstyle.rs | 964 +++++++++++++++++++++++--------- src/ui/style/mleaderstyle.rs | 902 +++++++++++++++++++----------- src/ui/style/mlstyle.rs | 430 +++++++++++--- src/ui/style/style_list.rs | 12 +- src/ui/style/style_manager.rs | 159 +++++- src/ui/style/tablestyle.rs | 846 +++++++++++++++++----------- src/ui/style/textstyle.rs | 665 +++++++++++----------- 27 files changed, 5506 insertions(+), 1662 deletions(-) diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index ac21dd0ff..4fa197ed3 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -3193,3 +3193,185 @@ catalog-mod-shortcuts-4c54bbb78e = ── __ocs_arg_MOD__-Tastenkürzel ── catalog-custom-overrides-shortcuts-set-07be563dec = ── Benutzerdefinierte Überschreibungen (SHORTCUTS SET) catalog-function-keys-b5a2df24e9 = ── Funktionstasten ──────────────────────────────────── catalog-other-keys-a882812cf2 = ── Weitere Tasten ───────────────────────────────────── +dimstyle-keep-text-between-extension-lines = Text immer zwischen Hilfslinien halten +dimstyle-angular-dimensions = Winkelbemaßungen +dimstyle-arc-length-symbol = Bogenlängensymbol +dimstyle-arrowheads = Pfeilspitzen +dimstyle-available-style = Verfügbarer Stil +dimstyle-baseline-spacing = Basislinienabstand +dimstyle-basic = Theoretisch genau +dimstyle-both-arrowheads = Beide Pfeilspitzen +dimstyle-center-mark = Mittelpunktmarkierung +dimstyle-center-mark-size = Größe der Mittelpunktmarkierung +dimstyle-center-marks = Mittelpunktmarkierungen +dimstyle-centerlines = Mittellinien +dimstyle-compare-with = Vergleichen mit +dimstyle-current-style = Aktueller Stil +dimstyle-deviation = Abweichung +dimstyle-different-sections = Abweichende Bereiche +dimstyle-dimension-line-color = Maßlinienfarbe +dimstyle-dimension-linetype = Maßlinientyp +dimstyle-dimension-lineweight = Maßlinienstärke +dimstyle-display-alternate-units = Alternative Einheiten anzeigen +dimstyle-draw-dimension-line-between-extension-lines = Maßlinie zwischen Hilfslinien zeichnen +dimstyle-extend-beyond-dimension-line = Über Maßlinie hinaus verlängern +dimstyle-extend-beyond-ticks = Über Schrägstriche hinaus verlängern +dimstyle-extension-line-color = Hilfslinienfarbe +dimstyle-extension-lineweight = Hilfslinienstärke +dimstyle-feet-and-inches = Fuß und Zoll +dimstyle-fine-tuning = Feinabstimmung +dimstyle-first-arrowhead = Erste Pfeilspitze +dimstyle-first-extension-linetype = Linientyp der ersten Hilfslinie +dimstyle-fit-options = Einpassungsoptionen +dimstyle-fixed-length = Feste Länge +dimstyle-fraction-format = Bruchformat +dimstyle-height-scale = Höhenskalierung +dimstyle-horizontal-placement = Horizontale Platzierung +dimstyle-keep-text-horizontal-inside = Text innen horizontal halten +dimstyle-keep-text-horizontal-outside = Text außen horizontal halten +dimstyle-leader-arrowhead = Führungslinien-Pfeilspitze +dimstyle-limits = Grenzmaße +dimstyle-linear-dimensions = Lineare Bemaßungen +dimstyle-lower-value = Unterer Wert +dimstyle-measurement-scale = Maßstab der Messwerte +dimstyle-measurement-template = Messwertvorlage +dimstyle-method = Methode +dimstyle-multiplier = Multiplikator +dimstyle-no-differences = Keine Unterschiede +dimstyle-not-applicable = Nicht zutreffend +dimstyle-offset-from-measured-point = Abstand vom gemessenen Punkt +dimstyle-place-text-manually = Text beim Erstellen der Bemaßung manuell platzieren +dimstyle-radius-jog-angle = Winkel des Radiusknicks (°) +dimstyle-read-left-to-right = Von links nach rechts lesen +dimstyle-referenced-read-only = Referenzierter Stil · Schreibgeschützt +dimstyle-round-off = Rundung +dimstyle-second-arrowhead = Zweite Pfeilspitze +dimstyle-second-extension-linetype = Linientyp der zweiten Hilfslinie +dimstyle-show-zero-feet-and-inches = Null Fuß und null Zoll anzeigen +dimstyle-show-zero-feet-suppress-zero-inches = Null Fuß anzeigen; null Zoll unterdrücken +dimstyle-suppress-arrows-when-no-fit = Pfeile unterdrücken, wenn sie nicht passen +dimstyle-suppress-first-dimension-line = Erste Maßlinie unterdrücken +dimstyle-suppress-first-extension-line = Erste Hilfslinie unterdrücken +dimstyle-suppress-second-dimension-line = Zweite Maßlinie unterdrücken +dimstyle-suppress-second-extension-line = Zweite Hilfslinie unterdrücken +dimstyle-suppress-zero-feet-and-inches = Null Fuß und null Zoll unterdrücken +dimstyle-suppress-zero-feet-show-zero-inches = Null Fuß unterdrücken; null Zoll anzeigen +dimstyle-symbols-and-arrows = Symbole und Pfeile +dimstyle-symmetrical = Symmetrisch +dimstyle-text-background = Texthintergrund +dimstyle-text-gap = Textabstand +dimstyle-fixed-text-height-note = Der gewählte Textstil legt die Höhe auf __ocs_arg_height__ fest. +dimstyle-tick-size = Schrägstrichgröße +dimstyle-tolerance-zero-suppression = Nullunterdrückung für Toleranzen +dimstyle-unit-format = Einheitenformat +dimstyle-upper-value = Oberer Wert +dimstyle-measurement-template-help = Verwenden Sie <> für den Messwert; Text davor und danach wird zum Präfix bzw. Suffix. +dimstyle-use-different-arrowheads = Unterschiedliche erste und zweite Pfeilspitzen verwenden +dimstyle-use-fixed-length-extension-lines = Hilfslinien mit fester Länge verwenden +dimstyle-used-in-drawing = In Zeichnung verwendet +dimstyle-vertical-offset = Vertikaler Abstand +dimstyle-vertical-placement = Vertikale Platzierung +dimstyle-vertical-position = Vertikale Position +dimstyle-when-space-is-limited = Bei begrenztem Platz +dimstyle-when-text-is-moved = Wenn Text verschoben wird +dimstyle-zero-suppression = Nullunterdrückung +dimstyle-add-a-leader = Führungslinie hinzufügen +dimstyle-automatic = Automatisch +dimstyle-by-block = Von Block +dimstyle-comma = Komma +dimstyle-degrees-minutes-seconds = Grad, Minuten, Sekunden +dimstyle-keep-dimension-line-with-text = Maßlinie mit Text verschieben +dimstyle-leading-and-trailing = Führende und nachfolgende Nullen +dimstyle-move-arrows-outside-first = Zuerst Pfeile nach außen verschieben +dimstyle-move-text-and-arrows-outside = Text und Pfeile nach außen verschieben +dimstyle-move-text-freely = Text frei verschieben +dimstyle-move-text-outside-first = Zuerst Text nach außen verschieben +dimstyle-period = Punkt +dimstyle-space = Leerzeichen +style-manager-alignment-spacing = Ausrichtungsabstand +style-manager-always-left-align = Immer links ausrichten +style-manager-attachment-direction = Anschlussrichtung +style-manager-background-fill-enabled = Hintergrundfüllung aktiviert +style-manager-big-font = Großschrift +style-manager-block-scale-x = Blockskalierung X +style-manager-block-scale-y = Blockskalierung Y +style-manager-block-scale-z = Blockskalierung Z +style-manager-borders = Rahmenlinien +style-manager-bottom-attachment = Unterer Anschluss +style-manager-break-gap-size = Unterbrechungsabstand +style-manager-caps-and-fill = Abschlüsse und Füllung +style-manager-content-type = Inhaltstyp +style-manager-data-row = Datenzeile +style-manager-data-type = Datentyp +style-manager-default-text = Standardtext +style-manager-different = Unterschiedlich +style-manager-display-joints = Verbindungen anzeigen +style-manager-drawing-height = Zeichnungshöhe +style-manager-drawing-scale = Zeichnungsmaßstab +style-manager-effects = Effekte +style-manager-elements = Elemente +style-manager-end-caps = Endabschlüsse +style-manager-fill-and-joints = Füllung und Verbindungen +style-manager-fill-enabled = Füllung aktiviert +style-manager-first-segment-angle = Winkel des ersten Segments +style-manager-fixed-height = Feste Höhe +style-manager-flow-direction = Flussrichtung +style-manager-font-file = Schriftdatei +style-manager-fonts = Schriftarten +style-manager-format-string = Formatzeichenfolge +style-manager-header = Kopfzeile +style-manager-header-row = Kopfzeile +style-manager-horizontal-margin = Horizontaler Rand +style-manager-inner-arcs = Innere Bögen +style-manager-inside-horizontal = Innen horizontal +style-manager-inside-vertical = Innen vertikal +style-manager-leader-draw-order = Zeichnungsreihenfolge der Führungslinie +style-manager-leader-note = Führungslinienhinweis +style-manager-left-attachment = Linker Anschluss +style-manager-line-color = Linienfarbe +style-manager-line-type = Linientyp +style-manager-line-weight = Linienstärke +style-manager-maximum-leader-points = Maximale Führungslinienpunkte +style-manager-multileader-draw-order = Zeichnungsreihenfolge der Multi-Führungslinie +style-manager-no-preview = Keine Vorschau +style-manager-oblique-angle = Schrägstellwinkel +style-manager-outer-arc = Außenbogen +style-manager-paper-height = Papierhöhe +style-manager-paper-text-height = Papiertexthöhe +style-manager-path-type = Pfadtyp +style-manager-right-attachment = Rechter Anschluss +style-manager-scale-factor = Skalierungsfaktor +style-manager-second-segment-angle = Winkel des zweiten Segments +style-manager-size-and-effects = Größe und Effekte +style-manager-start-caps = Startabschlüsse +style-manager-stroke-fonts = Strichschriftarten +style-manager-suppress-header-row = Kopfzeile unterdrücken +style-manager-suppress-title-row = Titelzeile unterdrücken +style-manager-system-font = Systemschriftart +style-manager-system-fonts = Systemschriftarten +style-manager-text-alignment = Textausrichtung +style-manager-text-angle = Textwinkel +style-manager-title-row = Titelzeile +style-manager-top-attachment = Oberer Anschluss +style-manager-unit-type = Einheitentyp +style-manager-vertical-margin = Vertikaler Rand +style-manager-font-family-placeholder = Schriftfamilie… +style-manager-bottom-to-top = Von unten nach oben +style-manager-arrowhead-first = Pfeilspitze zuerst +style-manager-content-first = Inhalt zuerst +style-manager-leader-first = Führungslinie zuerst +style-manager-straight = Gerade +style-manager-parallel-last-segment = Parallel zum letzten Segment +style-manager-top-of-top-line = Oberkante der obersten Zeile +style-manager-middle-of-top-line = Mitte der obersten Zeile +style-manager-middle-of-text = Textmitte +style-manager-middle-of-bottom-line = Mitte der untersten Zeile +style-manager-bottom-of-bottom-line = Unterkante der untersten Zeile +style-manager-bottom-line = Unterste Zeile +style-manager-underline-bottom-line = Unterste Zeile unterstreichen +style-manager-underline-top-line = Oberste Zeile unterstreichen +style-manager-underline-all-text = Gesamten Text unterstreichen +style-manager-center-of-text = Textzentrum +style-manager-center-of-text-overline = Textzentrum mit Oberstrich +style-manager-block-extents = Blockgrenzen +style-manager-base-point = Basispunkt diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 87d3e3006..2820ec1b0 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -3195,3 +3195,185 @@ catalog-mod-shortcuts-4c54bbb78e = ── __ocs_arg_MOD__ Shortcuts ──── catalog-custom-overrides-shortcuts-set-07be563dec = ── Custom Overrides (SHORTCUTS SET) ────────────────── catalog-function-keys-b5a2df24e9 = ── Function Keys ────────────────────────────────────── catalog-other-keys-a882812cf2 = ── Other Keys ────────────────────────────────────────── +dimstyle-keep-text-between-extension-lines = Always keep text between extension lines +dimstyle-angular-dimensions = Angular dimensions +dimstyle-arc-length-symbol = Arc length symbol +dimstyle-arrowheads = Arrowheads +dimstyle-available-style = Available style +dimstyle-baseline-spacing = Baseline spacing +dimstyle-basic = Basic +dimstyle-both-arrowheads = Both arrowheads +dimstyle-center-mark = Center mark +dimstyle-center-mark-size = Center mark size +dimstyle-center-marks = Center marks +dimstyle-centerlines = Centerlines +dimstyle-compare-with = Compare with +dimstyle-current-style = Current style +dimstyle-deviation = Deviation +dimstyle-different-sections = Different sections +dimstyle-dimension-line-color = Dimension line color +dimstyle-dimension-linetype = Dimension linetype +dimstyle-dimension-lineweight = Dimension lineweight +dimstyle-display-alternate-units = Display alternate units +dimstyle-draw-dimension-line-between-extension-lines = Draw dimension line between extension lines +dimstyle-extend-beyond-dimension-line = Extend beyond dimension line +dimstyle-extend-beyond-ticks = Extend beyond ticks +dimstyle-extension-line-color = Extension line color +dimstyle-extension-lineweight = Extension lineweight +dimstyle-feet-and-inches = Feet and inches +dimstyle-fine-tuning = Fine tuning +dimstyle-first-arrowhead = First arrowhead +dimstyle-first-extension-linetype = First extension linetype +dimstyle-fit-options = Fit options +dimstyle-fixed-length = Fixed length +dimstyle-fraction-format = Fraction format +dimstyle-height-scale = Height scale +dimstyle-horizontal-placement = Horizontal placement +dimstyle-keep-text-horizontal-inside = Keep text horizontal when inside +dimstyle-keep-text-horizontal-outside = Keep text horizontal when outside +dimstyle-leader-arrowhead = Leader arrowhead +dimstyle-limits = Limits +dimstyle-linear-dimensions = Linear dimensions +dimstyle-lower-value = Lower value +dimstyle-measurement-scale = Measurement scale +dimstyle-measurement-template = Measurement template +dimstyle-method = Method +dimstyle-multiplier = Multiplier +dimstyle-no-differences = No differences +dimstyle-not-applicable = Not applicable +dimstyle-offset-from-measured-point = Offset from measured point +dimstyle-place-text-manually = Place text manually while creating dimensions +dimstyle-radius-jog-angle = Radius jog angle (°) +dimstyle-read-left-to-right = Read left to right +dimstyle-referenced-read-only = Referenced style · Read only +dimstyle-round-off = Round off +dimstyle-second-arrowhead = Second arrowhead +dimstyle-second-extension-linetype = Second extension linetype +dimstyle-show-zero-feet-and-inches = Show zero feet and zero inches +dimstyle-show-zero-feet-suppress-zero-inches = Show zero feet; suppress zero inches +dimstyle-suppress-arrows-when-no-fit = Suppress arrows when they do not fit +dimstyle-suppress-first-dimension-line = Suppress first dimension line +dimstyle-suppress-first-extension-line = Suppress first extension line +dimstyle-suppress-second-dimension-line = Suppress second dimension line +dimstyle-suppress-second-extension-line = Suppress second extension line +dimstyle-suppress-zero-feet-and-inches = Suppress zero feet and zero inches +dimstyle-suppress-zero-feet-show-zero-inches = Suppress zero feet; show zero inches +dimstyle-symbols-and-arrows = Symbols and Arrows +dimstyle-symmetrical = Symmetrical +dimstyle-text-background = Text background +dimstyle-text-gap = Text gap +dimstyle-fixed-text-height-note = The selected text style fixes the height at __ocs_arg_height__. +dimstyle-tick-size = Tick size +dimstyle-tolerance-zero-suppression = Tolerance zero suppression +dimstyle-unit-format = Unit format +dimstyle-upper-value = Upper value +dimstyle-measurement-template-help = Use <> as the measured value; text before and after it becomes the prefix and suffix. +dimstyle-use-different-arrowheads = Use different first and second arrowheads +dimstyle-use-fixed-length-extension-lines = Use fixed-length extension lines +dimstyle-used-in-drawing = Used in drawing +dimstyle-vertical-offset = Vertical offset +dimstyle-vertical-placement = Vertical placement +dimstyle-vertical-position = Vertical position +dimstyle-when-space-is-limited = When space is limited +dimstyle-when-text-is-moved = When text is moved +dimstyle-zero-suppression = Zero suppression +dimstyle-add-a-leader = Add a leader +dimstyle-automatic = Automatic +dimstyle-by-block = By block +dimstyle-comma = Comma +dimstyle-degrees-minutes-seconds = Degrees, minutes, seconds +dimstyle-keep-dimension-line-with-text = Keep dimension line with text +dimstyle-leading-and-trailing = Leading and trailing +dimstyle-move-arrows-outside-first = Move arrows outside first +dimstyle-move-text-and-arrows-outside = Move text and arrows outside +dimstyle-move-text-freely = Move text freely +dimstyle-move-text-outside-first = Move text outside first +dimstyle-period = Period +dimstyle-space = Space +style-manager-alignment-spacing = Alignment spacing +style-manager-always-left-align = Always left align +style-manager-attachment-direction = Attachment direction +style-manager-background-fill-enabled = Background fill enabled +style-manager-big-font = Big font +style-manager-block-scale-x = Block scale X +style-manager-block-scale-y = Block scale Y +style-manager-block-scale-z = Block scale Z +style-manager-borders = Borders +style-manager-bottom-attachment = Bottom attachment +style-manager-break-gap-size = Break gap size +style-manager-caps-and-fill = Caps and Fill +style-manager-content-type = Content type +style-manager-data-row = Data Row +style-manager-data-type = Data type +style-manager-default-text = Default text +style-manager-different = Different +style-manager-display-joints = Display joints +style-manager-drawing-height = Drawing height +style-manager-drawing-scale = Drawing scale +style-manager-effects = Effects +style-manager-elements = Elements +style-manager-end-caps = End caps +style-manager-fill-and-joints = Fill and joints +style-manager-fill-enabled = Fill enabled +style-manager-first-segment-angle = First segment angle +style-manager-fixed-height = Fixed height +style-manager-flow-direction = Flow direction +style-manager-font-file = Font file +style-manager-fonts = Fonts +style-manager-format-string = Format string +style-manager-header = Header +style-manager-header-row = Header Row +style-manager-horizontal-margin = Horizontal margin +style-manager-inner-arcs = Inner arcs +style-manager-inside-horizontal = Inside horizontal +style-manager-inside-vertical = Inside vertical +style-manager-leader-draw-order = Leader draw order +style-manager-leader-note = Leader note +style-manager-left-attachment = Left attachment +style-manager-line-color = Line color +style-manager-line-type = Line type +style-manager-line-weight = Line weight +style-manager-maximum-leader-points = Maximum leader points +style-manager-multileader-draw-order = Multileader draw order +style-manager-no-preview = No preview +style-manager-oblique-angle = Oblique angle +style-manager-outer-arc = Outer arc +style-manager-paper-height = Paper height +style-manager-paper-text-height = Paper text height +style-manager-path-type = Path type +style-manager-right-attachment = Right attachment +style-manager-scale-factor = Scale factor +style-manager-second-segment-angle = Second segment angle +style-manager-size-and-effects = Size and Effects +style-manager-start-caps = Start caps +style-manager-stroke-fonts = Stroke fonts +style-manager-suppress-header-row = Suppress header row +style-manager-suppress-title-row = Suppress title row +style-manager-system-font = System font +style-manager-system-fonts = System fonts +style-manager-text-alignment = Text alignment +style-manager-text-angle = Text angle +style-manager-title-row = Title Row +style-manager-top-attachment = Top attachment +style-manager-unit-type = Unit type +style-manager-vertical-margin = Vertical margin +style-manager-font-family-placeholder = font family… +style-manager-bottom-to-top = Bottom to top +style-manager-arrowhead-first = Arrowhead first +style-manager-content-first = Content first +style-manager-leader-first = Leader first +style-manager-straight = Straight +style-manager-parallel-last-segment = Parallel to last segment +style-manager-top-of-top-line = Top of top line +style-manager-middle-of-top-line = Middle of top line +style-manager-middle-of-text = Middle of text +style-manager-middle-of-bottom-line = Middle of bottom line +style-manager-bottom-of-bottom-line = Bottom of bottom line +style-manager-bottom-line = Bottom line +style-manager-underline-bottom-line = Underline bottom line +style-manager-underline-top-line = Underline top line +style-manager-underline-all-text = Underline all text +style-manager-center-of-text = Center of text +style-manager-center-of-text-overline = Center of text with overline +style-manager-block-extents = Block extents +style-manager-base-point = Base point diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 45e240a35..0939c8cfb 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -3194,3 +3194,185 @@ catalog-mod-shortcuts-4c54bbb78e = ── Raccourcis __ocs_arg_MOD__ ─── catalog-custom-overrides-shortcuts-set-07be563dec = ── Remplacements personnalisés (SHORTCUTS SET) ─────── catalog-function-keys-b5a2df24e9 = ── Touches de fonction ──────────────────────────────── catalog-other-keys-a882812cf2 = ── Autres touches ───────────────────────────────────── +dimstyle-keep-text-between-extension-lines = Toujours conserver le texte entre les lignes d’attache +dimstyle-angular-dimensions = Cotes angulaires +dimstyle-arc-length-symbol = Symbole de longueur d’arc +dimstyle-arrowheads = Pointes de flèche +dimstyle-available-style = Style disponible +dimstyle-baseline-spacing = Espacement des lignes de base +dimstyle-basic = Théorique exacte +dimstyle-both-arrowheads = Deux pointes de flèche +dimstyle-center-mark = Marque de centre +dimstyle-center-mark-size = Taille de la marque de centre +dimstyle-center-marks = Marques de centre +dimstyle-centerlines = Axes +dimstyle-compare-with = Comparer avec +dimstyle-current-style = Style courant +dimstyle-deviation = Écarts +dimstyle-different-sections = Sections différentes +dimstyle-dimension-line-color = Couleur de ligne de cote +dimstyle-dimension-linetype = Type de ligne de cote +dimstyle-dimension-lineweight = Épaisseur de ligne de cote +dimstyle-display-alternate-units = Afficher les unités alternatives +dimstyle-draw-dimension-line-between-extension-lines = Tracer la ligne de cote entre les lignes d’attache +dimstyle-extend-beyond-dimension-line = Dépassement de la ligne de cote +dimstyle-extend-beyond-ticks = Dépassement des obliques +dimstyle-extension-line-color = Couleur de ligne d’attache +dimstyle-extension-lineweight = Épaisseur de ligne d’attache +dimstyle-feet-and-inches = Pieds et pouces +dimstyle-fine-tuning = Ajustement fin +dimstyle-first-arrowhead = Première pointe de flèche +dimstyle-first-extension-linetype = Type de la première ligne d’attache +dimstyle-fit-options = Options d’ajustement +dimstyle-fixed-length = Longueur fixe +dimstyle-fraction-format = Format de fraction +dimstyle-height-scale = Échelle de hauteur +dimstyle-horizontal-placement = Placement horizontal +dimstyle-keep-text-horizontal-inside = Conserver le texte horizontal à l’intérieur +dimstyle-keep-text-horizontal-outside = Conserver le texte horizontal à l’extérieur +dimstyle-leader-arrowhead = Pointe de flèche de repère +dimstyle-limits = Limites +dimstyle-linear-dimensions = Cotes linéaires +dimstyle-lower-value = Valeur inférieure +dimstyle-measurement-scale = Échelle de mesure +dimstyle-measurement-template = Modèle de mesure +dimstyle-method = Méthode +dimstyle-multiplier = Multiplicateur +dimstyle-no-differences = Aucune différence +dimstyle-not-applicable = Sans objet +dimstyle-offset-from-measured-point = Décalage depuis le point mesuré +dimstyle-place-text-manually = Placer le texte manuellement lors de la cotation +dimstyle-radius-jog-angle = Angle de la ligne brisée de rayon (°) +dimstyle-read-left-to-right = Lire de gauche à droite +dimstyle-referenced-read-only = Style référencé · Lecture seule +dimstyle-round-off = Arrondi +dimstyle-second-arrowhead = Deuxième pointe de flèche +dimstyle-second-extension-linetype = Type de la deuxième ligne d’attache +dimstyle-show-zero-feet-and-inches = Afficher zéro pied et zéro pouce +dimstyle-show-zero-feet-suppress-zero-inches = Afficher zéro pied ; supprimer zéro pouce +dimstyle-suppress-arrows-when-no-fit = Supprimer les flèches si elles ne tiennent pas +dimstyle-suppress-first-dimension-line = Supprimer la première ligne de cote +dimstyle-suppress-first-extension-line = Supprimer la première ligne d’attache +dimstyle-suppress-second-dimension-line = Supprimer la deuxième ligne de cote +dimstyle-suppress-second-extension-line = Supprimer la deuxième ligne d’attache +dimstyle-suppress-zero-feet-and-inches = Supprimer zéro pied et zéro pouce +dimstyle-suppress-zero-feet-show-zero-inches = Supprimer zéro pied ; afficher zéro pouce +dimstyle-symbols-and-arrows = Symboles et flèches +dimstyle-symmetrical = Symétrique +dimstyle-text-background = Arrière-plan du texte +dimstyle-text-gap = Espacement du texte +dimstyle-fixed-text-height-note = Le style de texte sélectionné fixe la hauteur à __ocs_arg_height__. +dimstyle-tick-size = Taille des obliques +dimstyle-tolerance-zero-suppression = Suppression des zéros de tolérance +dimstyle-unit-format = Format d’unité +dimstyle-upper-value = Valeur supérieure +dimstyle-measurement-template-help = Utilisez <> pour la valeur mesurée ; le texte placé avant et après devient le préfixe et le suffixe. +dimstyle-use-different-arrowheads = Utiliser des pointes différentes aux deux extrémités +dimstyle-use-fixed-length-extension-lines = Utiliser des lignes d’attache de longueur fixe +dimstyle-used-in-drawing = Utilisé dans le dessin +dimstyle-vertical-offset = Décalage vertical +dimstyle-vertical-placement = Placement vertical +dimstyle-vertical-position = Position verticale +dimstyle-when-space-is-limited = Lorsque l’espace est limité +dimstyle-when-text-is-moved = Lorsque le texte est déplacé +dimstyle-zero-suppression = Suppression des zéros +dimstyle-add-a-leader = Ajouter une ligne de repère +dimstyle-automatic = Automatique +dimstyle-by-block = Du bloc +dimstyle-comma = Virgule +dimstyle-degrees-minutes-seconds = Degrés, minutes, secondes +dimstyle-keep-dimension-line-with-text = Déplacer la ligne de cote avec le texte +dimstyle-leading-and-trailing = Zéros de début et de fin +dimstyle-move-arrows-outside-first = Déplacer d’abord les flèches à l’extérieur +dimstyle-move-text-and-arrows-outside = Déplacer le texte et les flèches à l’extérieur +dimstyle-move-text-freely = Déplacer le texte librement +dimstyle-move-text-outside-first = Déplacer d’abord le texte à l’extérieur +dimstyle-period = Point +dimstyle-space = Espace +style-manager-alignment-spacing = Espacement d’alignement +style-manager-always-left-align = Toujours aligner à gauche +style-manager-attachment-direction = Direction d’attachement +style-manager-background-fill-enabled = Remplissage d’arrière-plan activé +style-manager-big-font = Police grande taille +style-manager-block-scale-x = Échelle X du bloc +style-manager-block-scale-y = Échelle Y du bloc +style-manager-block-scale-z = Échelle Z du bloc +style-manager-borders = Bordures +style-manager-bottom-attachment = Attachement inférieur +style-manager-break-gap-size = Taille de l’espacement de coupure +style-manager-caps-and-fill = Extrémités et remplissage +style-manager-content-type = Type de contenu +style-manager-data-row = Ligne de données +style-manager-data-type = Type de données +style-manager-default-text = Texte par défaut +style-manager-different = Différent +style-manager-display-joints = Afficher les jonctions +style-manager-drawing-height = Hauteur dans le dessin +style-manager-drawing-scale = Échelle du dessin +style-manager-effects = Effets +style-manager-elements = Éléments +style-manager-end-caps = Extrémités de fin +style-manager-fill-and-joints = Remplissage et jonctions +style-manager-fill-enabled = Remplissage activé +style-manager-first-segment-angle = Angle du premier segment +style-manager-fixed-height = Hauteur fixe +style-manager-flow-direction = Sens de lecture +style-manager-font-file = Fichier de police +style-manager-fonts = Polices +style-manager-format-string = Chaîne de format +style-manager-header = En-tête +style-manager-header-row = Ligne d’en-tête +style-manager-horizontal-margin = Marge horizontale +style-manager-inner-arcs = Arcs intérieurs +style-manager-inside-horizontal = Intérieur horizontal +style-manager-inside-vertical = Intérieur vertical +style-manager-leader-draw-order = Ordre de tracé de la ligne de repère +style-manager-leader-note = Note de repère +style-manager-left-attachment = Attachement gauche +style-manager-line-color = Couleur de ligne +style-manager-line-type = Type de ligne +style-manager-line-weight = Épaisseur de ligne +style-manager-maximum-leader-points = Nombre maximal de points de repère +style-manager-multileader-draw-order = Ordre de tracé du repère multiple +style-manager-no-preview = Aucun aperçu +style-manager-oblique-angle = Angle d’inclinaison +style-manager-outer-arc = Arc extérieur +style-manager-paper-height = Hauteur sur papier +style-manager-paper-text-height = Hauteur de texte sur papier +style-manager-path-type = Type de trajectoire +style-manager-right-attachment = Attachement droit +style-manager-scale-factor = Facteur d’échelle +style-manager-second-segment-angle = Angle du deuxième segment +style-manager-size-and-effects = Taille et effets +style-manager-start-caps = Extrémités de départ +style-manager-stroke-fonts = Polices filaires +style-manager-suppress-header-row = Masquer la ligne d’en-tête +style-manager-suppress-title-row = Masquer la ligne de titre +style-manager-system-font = Police système +style-manager-system-fonts = Polices système +style-manager-text-alignment = Alignement du texte +style-manager-text-angle = Angle du texte +style-manager-title-row = Ligne de titre +style-manager-top-attachment = Attachement supérieur +style-manager-unit-type = Type d’unité +style-manager-vertical-margin = Marge verticale +style-manager-font-family-placeholder = famille de polices… +style-manager-bottom-to-top = De bas en haut +style-manager-arrowhead-first = Pointe de flèche en premier +style-manager-content-first = Contenu en premier +style-manager-leader-first = Ligne de repère en premier +style-manager-straight = Droite +style-manager-parallel-last-segment = Parallèle au dernier segment +style-manager-top-of-top-line = Haut de la ligne supérieure +style-manager-middle-of-top-line = Milieu de la ligne supérieure +style-manager-middle-of-text = Milieu du texte +style-manager-middle-of-bottom-line = Milieu de la ligne inférieure +style-manager-bottom-of-bottom-line = Bas de la ligne inférieure +style-manager-bottom-line = Ligne inférieure +style-manager-underline-bottom-line = Souligner la ligne inférieure +style-manager-underline-top-line = Souligner la ligne supérieure +style-manager-underline-all-text = Souligner tout le texte +style-manager-center-of-text = Centre du texte +style-manager-center-of-text-overline = Centre du texte avec surlignement +style-manager-block-extents = Limites du bloc +style-manager-base-point = Point de base diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 15995a45e..44a8e6f7b 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -3183,3 +3183,185 @@ catalog-mod-shortcuts-4c54bbb78e = ── __ocs_arg_MOD__ शॉर्टकट catalog-custom-overrides-shortcuts-set-07be563dec = ── कस्टम ओवरराइड (SHORTCUTS SET) ──────────────────── catalog-function-keys-b5a2df24e9 = ── फ़ंक्शन कुंजियाँ ─────────────────────────────────── catalog-other-keys-a882812cf2 = ── अन्य कुंजियाँ ───────────────────────────────────── +dimstyle-keep-text-between-extension-lines = पाठ को हमेशा विस्तार रेखाओं के बीच रखें +dimstyle-angular-dimensions = कोणीय माप +dimstyle-arc-length-symbol = चाप लंबाई चिह्न +dimstyle-arrowheads = तीर शीर्ष +dimstyle-available-style = उपलब्ध शैली +dimstyle-baseline-spacing = आधार रेखा अंतर +dimstyle-basic = मूल +dimstyle-both-arrowheads = दोनों तीर शीर्ष +dimstyle-center-mark = केंद्र चिह्न +dimstyle-center-mark-size = केंद्र चिह्न का आकार +dimstyle-center-marks = केंद्र चिह्न +dimstyle-centerlines = केंद्र रेखाएँ +dimstyle-compare-with = इससे तुलना करें +dimstyle-current-style = वर्तमान शैली +dimstyle-deviation = विचलन +dimstyle-different-sections = भिन्न अनुभाग +dimstyle-dimension-line-color = माप रेखा का रंग +dimstyle-dimension-linetype = माप रेखा प्रकार +dimstyle-dimension-lineweight = माप रेखा मोटाई +dimstyle-display-alternate-units = वैकल्पिक इकाइयाँ दिखाएँ +dimstyle-draw-dimension-line-between-extension-lines = विस्तार रेखाओं के बीच माप रेखा बनाएँ +dimstyle-extend-beyond-dimension-line = माप रेखा से आगे बढ़ाएँ +dimstyle-extend-beyond-ticks = तिरछे चिह्नों से आगे बढ़ाएँ +dimstyle-extension-line-color = विस्तार रेखा का रंग +dimstyle-extension-lineweight = विस्तार रेखा मोटाई +dimstyle-feet-and-inches = फुट और इंच +dimstyle-fine-tuning = सूक्ष्म समायोजन +dimstyle-first-arrowhead = पहला तीर शीर्ष +dimstyle-first-extension-linetype = पहली विस्तार रेखा का प्रकार +dimstyle-fit-options = समायोजन विकल्प +dimstyle-fixed-length = निश्चित लंबाई +dimstyle-fraction-format = भिन्न प्रारूप +dimstyle-height-scale = ऊँचाई मापनी +dimstyle-horizontal-placement = क्षैतिज स्थान +dimstyle-keep-text-horizontal-inside = अंदर होने पर पाठ क्षैतिज रखें +dimstyle-keep-text-horizontal-outside = बाहर होने पर पाठ क्षैतिज रखें +dimstyle-leader-arrowhead = संकेतक तीर शीर्ष +dimstyle-limits = सीमाएँ +dimstyle-linear-dimensions = रैखिक माप +dimstyle-lower-value = निचला मान +dimstyle-measurement-scale = मापन मापनी +dimstyle-measurement-template = मापन साँचा +dimstyle-method = विधि +dimstyle-multiplier = गुणक +dimstyle-no-differences = कोई अंतर नहीं +dimstyle-not-applicable = लागू नहीं +dimstyle-offset-from-measured-point = मापे गए बिंदु से दूरी +dimstyle-place-text-manually = माप बनाते समय पाठ को स्वयं रखें +dimstyle-radius-jog-angle = त्रिज्या मोड़ कोण (°) +dimstyle-read-left-to-right = बाएँ से दाएँ पढ़ें +dimstyle-referenced-read-only = संदर्भित शैली · केवल पढ़ने योग्य +dimstyle-round-off = पूर्णांकन +dimstyle-second-arrowhead = दूसरा तीर शीर्ष +dimstyle-second-extension-linetype = दूसरी विस्तार रेखा का प्रकार +dimstyle-show-zero-feet-and-inches = शून्य फुट और शून्य इंच दिखाएँ +dimstyle-show-zero-feet-suppress-zero-inches = शून्य फुट दिखाएँ; शून्य इंच छिपाएँ +dimstyle-suppress-arrows-when-no-fit = जगह न होने पर तीर छिपाएँ +dimstyle-suppress-first-dimension-line = पहली माप रेखा छिपाएँ +dimstyle-suppress-first-extension-line = पहली विस्तार रेखा छिपाएँ +dimstyle-suppress-second-dimension-line = दूसरी माप रेखा छिपाएँ +dimstyle-suppress-second-extension-line = दूसरी विस्तार रेखा छिपाएँ +dimstyle-suppress-zero-feet-and-inches = शून्य फुट और शून्य इंच छिपाएँ +dimstyle-suppress-zero-feet-show-zero-inches = शून्य फुट छिपाएँ; शून्य इंच दिखाएँ +dimstyle-symbols-and-arrows = चिह्न और तीर +dimstyle-symmetrical = सममित +dimstyle-text-background = पाठ पृष्ठभूमि +dimstyle-text-gap = पाठ अंतर +dimstyle-fixed-text-height-note = चुनी गई पाठ शैली ऊँचाई को __ocs_arg_height__ पर स्थिर करती है। +dimstyle-tick-size = तिरछे चिह्न का आकार +dimstyle-tolerance-zero-suppression = सहनशीलता के शून्य छिपाना +dimstyle-unit-format = इकाई प्रारूप +dimstyle-upper-value = ऊपरी मान +dimstyle-measurement-template-help = मापे गए मान के लिए <> का उपयोग करें; इसके पहले और बाद का पाठ उपसर्ग और प्रत्यय बनता है। +dimstyle-use-different-arrowheads = पहले और दूसरे तीर शीर्ष अलग रखें +dimstyle-use-fixed-length-extension-lines = निश्चित लंबाई की विस्तार रेखाएँ उपयोग करें +dimstyle-used-in-drawing = आरेख में प्रयुक्त +dimstyle-vertical-offset = ऊर्ध्वाधर दूरी +dimstyle-vertical-placement = ऊर्ध्वाधर स्थान +dimstyle-vertical-position = ऊर्ध्वाधर स्थिति +dimstyle-when-space-is-limited = जब स्थान सीमित हो +dimstyle-when-text-is-moved = जब पाठ हटाया जाए +dimstyle-zero-suppression = शून्य छिपाना +dimstyle-add-a-leader = संकेतक रेखा जोड़ें +dimstyle-automatic = स्वचालित +dimstyle-by-block = ब्लॉक के अनुसार +dimstyle-comma = अल्पविराम +dimstyle-degrees-minutes-seconds = अंश, मिनट, सेकंड +dimstyle-keep-dimension-line-with-text = माप रेखा को पाठ के साथ रखें +dimstyle-leading-and-trailing = आरंभिक और अंतिम +dimstyle-move-arrows-outside-first = पहले तीर बाहर ले जाएँ +dimstyle-move-text-and-arrows-outside = पाठ और तीर बाहर ले जाएँ +dimstyle-move-text-freely = पाठ को स्वतंत्र रूप से ले जाएँ +dimstyle-move-text-outside-first = पहले पाठ बाहर ले जाएँ +dimstyle-period = दशमलव बिंदु +dimstyle-space = रिक्त स्थान +style-manager-alignment-spacing = संरेखण अंतराल +style-manager-always-left-align = हमेशा बाएँ संरेखित करें +style-manager-attachment-direction = संयोजन दिशा +style-manager-background-fill-enabled = पृष्ठभूमि भराव सक्षम +style-manager-big-font = बड़ा फ़ॉन्ट +style-manager-block-scale-x = ब्लॉक X स्केल +style-manager-block-scale-y = ब्लॉक Y स्केल +style-manager-block-scale-z = ब्लॉक Z स्केल +style-manager-borders = सीमाएँ +style-manager-bottom-attachment = निचला संयोजन +style-manager-break-gap-size = ब्रेक अंतराल आकार +style-manager-caps-and-fill = सिरे और भराव +style-manager-content-type = सामग्री प्रकार +style-manager-data-row = डेटा पंक्ति +style-manager-data-type = डेटा प्रकार +style-manager-default-text = डिफ़ॉल्ट टेक्स्ट +style-manager-different = भिन्न +style-manager-display-joints = जोड़ दिखाएँ +style-manager-drawing-height = ड्रॉइंग ऊँचाई +style-manager-drawing-scale = ड्रॉइंग स्केल +style-manager-effects = प्रभाव +style-manager-elements = अवयव +style-manager-end-caps = अंतिम सिरे +style-manager-fill-and-joints = भराव और जोड़ +style-manager-fill-enabled = भराव सक्षम +style-manager-first-segment-angle = पहले खंड का कोण +style-manager-fixed-height = निश्चित ऊँचाई +style-manager-flow-direction = प्रवाह दिशा +style-manager-font-file = फ़ॉन्ट फ़ाइल +style-manager-fonts = फ़ॉन्ट +style-manager-format-string = प्रारूप स्ट्रिंग +style-manager-header = शीर्षलेख +style-manager-header-row = शीर्षलेख पंक्ति +style-manager-horizontal-margin = क्षैतिज हाशिया +style-manager-inner-arcs = आंतरिक चाप +style-manager-inside-horizontal = आंतरिक क्षैतिज +style-manager-inside-vertical = आंतरिक ऊर्ध्वाधर +style-manager-leader-draw-order = लीडर आरेखन क्रम +style-manager-leader-note = लीडर टिप्पणी +style-manager-left-attachment = बायाँ संयोजन +style-manager-line-color = रेखा रंग +style-manager-line-type = रेखा प्रकार +style-manager-line-weight = रेखा मोटाई +style-manager-maximum-leader-points = अधिकतम लीडर बिंदु +style-manager-multileader-draw-order = मल्टीलीडर आरेखन क्रम +style-manager-no-preview = कोई पूर्वावलोकन नहीं +style-manager-oblique-angle = तिरछा कोण +style-manager-outer-arc = बाहरी चाप +style-manager-paper-height = काग़ज़ ऊँचाई +style-manager-paper-text-height = काग़ज़ पर टेक्स्ट ऊँचाई +style-manager-path-type = पथ प्रकार +style-manager-right-attachment = दायाँ संयोजन +style-manager-scale-factor = स्केल गुणक +style-manager-second-segment-angle = दूसरे खंड का कोण +style-manager-size-and-effects = आकार और प्रभाव +style-manager-start-caps = आरंभिक सिरे +style-manager-stroke-fonts = रेखीय फ़ॉन्ट +style-manager-suppress-header-row = शीर्षलेख पंक्ति छिपाएँ +style-manager-suppress-title-row = शीर्षक पंक्ति छिपाएँ +style-manager-system-font = सिस्टम फ़ॉन्ट +style-manager-system-fonts = सिस्टम फ़ॉन्ट +style-manager-text-alignment = टेक्स्ट संरेखण +style-manager-text-angle = टेक्स्ट कोण +style-manager-title-row = शीर्षक पंक्ति +style-manager-top-attachment = ऊपरी संयोजन +style-manager-unit-type = इकाई प्रकार +style-manager-vertical-margin = ऊर्ध्वाधर हाशिया +style-manager-font-family-placeholder = फ़ॉन्ट परिवार… +style-manager-bottom-to-top = नीचे से ऊपर +style-manager-arrowhead-first = पहले तीर का सिरा +style-manager-content-first = पहले सामग्री +style-manager-leader-first = पहले लीडर +style-manager-straight = सीधा +style-manager-parallel-last-segment = अंतिम खंड के समांतर +style-manager-top-of-top-line = शीर्ष पंक्ति का ऊपरी भाग +style-manager-middle-of-top-line = शीर्ष पंक्ति का मध्य +style-manager-middle-of-text = टेक्स्ट का मध्य +style-manager-middle-of-bottom-line = निचली पंक्ति का मध्य +style-manager-bottom-of-bottom-line = निचली पंक्ति का निचला भाग +style-manager-bottom-line = निचली पंक्ति +style-manager-underline-bottom-line = निचली पंक्ति के नीचे रेखा +style-manager-underline-top-line = शीर्ष पंक्ति के नीचे रेखा +style-manager-underline-all-text = सभी टेक्स्ट के नीचे रेखा +style-manager-center-of-text = टेक्स्ट का केंद्र +style-manager-center-of-text-overline = ऊपरी रेखा सहित टेक्स्ट का केंद्र +style-manager-block-extents = ब्लॉक परिमिति +style-manager-base-point = आधार बिंदु diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index d280e0305..0494f4ed4 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -3195,3 +3195,185 @@ catalog-mod-shortcuts-4c54bbb78e = ── __ocs_arg_MOD__-sneltoetsen ─── catalog-custom-overrides-shortcuts-set-07be563dec = ── Aangepaste overschrijvingen (SHORTCUTS SET) ──────── catalog-function-keys-b5a2df24e9 = ── Functietoetsen ───────────────────────────────────── catalog-other-keys-a882812cf2 = ── Overige toetsen ──────────────────────────────────── +dimstyle-keep-text-between-extension-lines = Tekst altijd tussen hulplijnen houden +dimstyle-angular-dimensions = Hoekmaten +dimstyle-arc-length-symbol = Booglengtesymbool +dimstyle-arrowheads = Pijlpunten +dimstyle-available-style = Beschikbare stijl +dimstyle-baseline-spacing = Basislijnafstand +dimstyle-basic = Basis +dimstyle-both-arrowheads = Beide pijlpunten +dimstyle-center-mark = Middelpuntmarkering +dimstyle-center-mark-size = Grootte van middelpuntmarkering +dimstyle-center-marks = Middelpuntmarkeringen +dimstyle-centerlines = Hartlijnen +dimstyle-compare-with = Vergelijken met +dimstyle-current-style = Huidige stijl +dimstyle-deviation = Afwijking +dimstyle-different-sections = Afwijkende secties +dimstyle-dimension-line-color = Kleur maatlijn +dimstyle-dimension-linetype = Lijntype maatlijn +dimstyle-dimension-lineweight = Lijndikte maatlijn +dimstyle-display-alternate-units = Alternatieve eenheden weergeven +dimstyle-draw-dimension-line-between-extension-lines = Maatlijn tussen hulplijnen tekenen +dimstyle-extend-beyond-dimension-line = Voorbij maatlijn verlengen +dimstyle-extend-beyond-ticks = Voorbij schuine streepjes verlengen +dimstyle-extension-line-color = Kleur hulplijn +dimstyle-extension-lineweight = Lijndikte hulplijn +dimstyle-feet-and-inches = Voet en inch +dimstyle-fine-tuning = Fijnafstelling +dimstyle-first-arrowhead = Eerste pijlpunt +dimstyle-first-extension-linetype = Lijntype eerste hulplijn +dimstyle-fit-options = Inpasopties +dimstyle-fixed-length = Vaste lengte +dimstyle-fraction-format = Breuknotatie +dimstyle-height-scale = Hoogteschaal +dimstyle-horizontal-placement = Horizontale plaatsing +dimstyle-keep-text-horizontal-inside = Tekst binnen horizontaal houden +dimstyle-keep-text-horizontal-outside = Tekst buiten horizontaal houden +dimstyle-leader-arrowhead = Pijlpunt van verwijslijn +dimstyle-limits = Grenswaarden +dimstyle-linear-dimensions = Lineaire maten +dimstyle-lower-value = Onderwaarde +dimstyle-measurement-scale = Meetschaal +dimstyle-measurement-template = Meetwaardesjabloon +dimstyle-method = Methode +dimstyle-multiplier = Vermenigvuldigingsfactor +dimstyle-no-differences = Geen verschillen +dimstyle-not-applicable = Niet van toepassing +dimstyle-offset-from-measured-point = Afstand vanaf meetpunt +dimstyle-place-text-manually = Tekst handmatig plaatsen tijdens het bematen +dimstyle-radius-jog-angle = Hoek van radiusknik (°) +dimstyle-read-left-to-right = Van links naar rechts lezen +dimstyle-referenced-read-only = Gerefereerde stijl · Alleen-lezen +dimstyle-round-off = Afronding +dimstyle-second-arrowhead = Tweede pijlpunt +dimstyle-second-extension-linetype = Lijntype tweede hulplijn +dimstyle-show-zero-feet-and-inches = Nul voet en nul inch weergeven +dimstyle-show-zero-feet-suppress-zero-inches = Nul voet weergeven; nul inch onderdrukken +dimstyle-suppress-arrows-when-no-fit = Pijlen onderdrukken als ze niet passen +dimstyle-suppress-first-dimension-line = Eerste maatlijn onderdrukken +dimstyle-suppress-first-extension-line = Eerste hulplijn onderdrukken +dimstyle-suppress-second-dimension-line = Tweede maatlijn onderdrukken +dimstyle-suppress-second-extension-line = Tweede hulplijn onderdrukken +dimstyle-suppress-zero-feet-and-inches = Nul voet en nul inch onderdrukken +dimstyle-suppress-zero-feet-show-zero-inches = Nul voet onderdrukken; nul inch weergeven +dimstyle-symbols-and-arrows = Symbolen en pijlen +dimstyle-symmetrical = Symmetrisch +dimstyle-text-background = Tekstachtergrond +dimstyle-text-gap = Tekstafstand +dimstyle-fixed-text-height-note = De gekozen tekststijl legt de hoogte vast op __ocs_arg_height__. +dimstyle-tick-size = Grootte schuine streep +dimstyle-tolerance-zero-suppression = Nulonderdrukking tolerantie +dimstyle-unit-format = Eenheidsnotatie +dimstyle-upper-value = Bovenwaarde +dimstyle-measurement-template-help = Gebruik <> voor de meetwaarde; tekst ervoor en erna wordt het voor- en achtervoegsel. +dimstyle-use-different-arrowheads = Verschillende eerste en tweede pijlpunten gebruiken +dimstyle-use-fixed-length-extension-lines = Hulplijnen met vaste lengte gebruiken +dimstyle-used-in-drawing = Gebruikt in tekening +dimstyle-vertical-offset = Verticale afstand +dimstyle-vertical-placement = Verticale plaatsing +dimstyle-vertical-position = Verticale positie +dimstyle-when-space-is-limited = Wanneer de ruimte beperkt is +dimstyle-when-text-is-moved = Wanneer tekst wordt verplaatst +dimstyle-zero-suppression = Nulonderdrukking +dimstyle-add-a-leader = Verwijslijn toevoegen +dimstyle-automatic = Automatisch +dimstyle-by-block = Per blok +dimstyle-comma = Komma +dimstyle-degrees-minutes-seconds = Graden, minuten, seconden +dimstyle-keep-dimension-line-with-text = Maatlijn met tekst meeverplaatsen +dimstyle-leading-and-trailing = Voorloop- en eindnullen +dimstyle-move-arrows-outside-first = Eerst pijlen naar buiten verplaatsen +dimstyle-move-text-and-arrows-outside = Tekst en pijlen naar buiten verplaatsen +dimstyle-move-text-freely = Tekst vrij verplaatsen +dimstyle-move-text-outside-first = Eerst tekst naar buiten verplaatsen +dimstyle-period = Punt +dimstyle-space = Spatie +style-manager-alignment-spacing = Uitlijningsafstand +style-manager-always-left-align = Altijd links uitlijnen +style-manager-attachment-direction = Aanhechtingsrichting +style-manager-background-fill-enabled = Achtergrondvulling ingeschakeld +style-manager-big-font = Groot lettertype +style-manager-block-scale-x = Blokschaal X +style-manager-block-scale-y = Blokschaal Y +style-manager-block-scale-z = Blokschaal Z +style-manager-borders = Randen +style-manager-bottom-attachment = Onderste aanhechting +style-manager-break-gap-size = Grootte van onderbrekingsruimte +style-manager-caps-and-fill = Eindkappen en vulling +style-manager-content-type = Inhoudstype +style-manager-data-row = Gegevensrij +style-manager-data-type = Gegevenstype +style-manager-default-text = Standaardtekst +style-manager-different = Verschillend +style-manager-display-joints = Verbindingen weergeven +style-manager-drawing-height = Tekeninghoogte +style-manager-drawing-scale = Tekenschaal +style-manager-effects = Effecten +style-manager-elements = Elementen +style-manager-end-caps = Eindkappen +style-manager-fill-and-joints = Vulling en verbindingen +style-manager-fill-enabled = Vulling ingeschakeld +style-manager-first-segment-angle = Hoek van eerste segment +style-manager-fixed-height = Vaste hoogte +style-manager-flow-direction = Leesrichting +style-manager-font-file = Lettertypebestand +style-manager-fonts = Lettertypen +style-manager-format-string = Opmaakreeks +style-manager-header = Kolomkop +style-manager-header-row = Kolomkoprij +style-manager-horizontal-margin = Horizontale marge +style-manager-inner-arcs = Binnenbogen +style-manager-inside-horizontal = Horizontaal binnen +style-manager-inside-vertical = Verticaal binnen +style-manager-leader-draw-order = Tekenvolgorde van verwijslijn +style-manager-leader-note = Verwijslijnnotitie +style-manager-left-attachment = Linker aanhechting +style-manager-line-color = Lijnkleur +style-manager-line-type = Lijntype +style-manager-line-weight = Lijndikte +style-manager-maximum-leader-points = Maximumaantal verwijslijnpunten +style-manager-multileader-draw-order = Tekenvolgorde van multiverwijslijn +style-manager-no-preview = Geen voorbeeld +style-manager-oblique-angle = Schuintehoek +style-manager-outer-arc = Buitenboog +style-manager-paper-height = Papierhoogte +style-manager-paper-text-height = Teksthoogte op papier +style-manager-path-type = Padtype +style-manager-right-attachment = Rechter aanhechting +style-manager-scale-factor = Schaalfactor +style-manager-second-segment-angle = Hoek van tweede segment +style-manager-size-and-effects = Grootte en effecten +style-manager-start-caps = Startkappen +style-manager-stroke-fonts = Lijnlettertypen +style-manager-suppress-header-row = Kolomkoprij verbergen +style-manager-suppress-title-row = Titelrij verbergen +style-manager-system-font = Systeemlettertype +style-manager-system-fonts = Systeemlettertypen +style-manager-text-alignment = Tekstuitlijning +style-manager-text-angle = Teksthoek +style-manager-title-row = Titelrij +style-manager-top-attachment = Bovenste aanhechting +style-manager-unit-type = Eenheidstype +style-manager-vertical-margin = Verticale marge +style-manager-font-family-placeholder = lettertypefamilie… +style-manager-bottom-to-top = Van onder naar boven +style-manager-arrowhead-first = Pijlpunt eerst +style-manager-content-first = Inhoud eerst +style-manager-leader-first = Verwijslijn eerst +style-manager-straight = Recht +style-manager-parallel-last-segment = Evenwijdig aan laatste segment +style-manager-top-of-top-line = Bovenkant van bovenste regel +style-manager-middle-of-top-line = Midden van bovenste regel +style-manager-middle-of-text = Midden van tekst +style-manager-middle-of-bottom-line = Midden van onderste regel +style-manager-bottom-of-bottom-line = Onderkant van onderste regel +style-manager-bottom-line = Onderste regel +style-manager-underline-bottom-line = Onderste regel onderstrepen +style-manager-underline-top-line = Bovenste regel onderstrepen +style-manager-underline-all-text = Alle tekst onderstrepen +style-manager-center-of-text = Tekstmiddelpunt +style-manager-center-of-text-overline = Tekstmiddelpunt met bovenstreep +style-manager-block-extents = Blokgrenzen +style-manager-base-point = Basispunt diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 0a6ba9d2d..35db3ceba 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -3198,3 +3198,185 @@ catalog-mod-shortcuts-4c54bbb78e = ── Сочетания __ocs_arg_MOD__ ─ catalog-custom-overrides-shortcuts-set-07be563dec = ── Пользовательские переопределения (SHORTCUTS SET) ─ catalog-function-keys-b5a2df24e9 = ── Функциональные клавиши ───────────────────────────── catalog-other-keys-a882812cf2 = ── Прочие клавиши ───────────────────────────────────── +dimstyle-keep-text-between-extension-lines = Всегда размещать текст между выносными линиями +dimstyle-angular-dimensions = Угловые размеры +dimstyle-arc-length-symbol = Символ длины дуги +dimstyle-arrowheads = Стрелки +dimstyle-available-style = Доступный стиль +dimstyle-baseline-spacing = Шаг базовых размеров +dimstyle-basic = Теоретически точный +dimstyle-both-arrowheads = Обе стрелки +dimstyle-center-mark = Маркер центра +dimstyle-center-mark-size = Размер маркера центра +dimstyle-center-marks = Маркеры центра +dimstyle-centerlines = Осевые линии +dimstyle-compare-with = Сравнить с +dimstyle-current-style = Текущий стиль +dimstyle-deviation = Отклонения +dimstyle-different-sections = Различающиеся разделы +dimstyle-dimension-line-color = Цвет размерной линии +dimstyle-dimension-linetype = Тип размерной линии +dimstyle-dimension-lineweight = Вес размерной линии +dimstyle-display-alternate-units = Показывать альтернативные единицы +dimstyle-draw-dimension-line-between-extension-lines = Рисовать размерную линию между выносными +dimstyle-extend-beyond-dimension-line = Выступ за размерную линию +dimstyle-extend-beyond-ticks = Выступ за засечки +dimstyle-extension-line-color = Цвет выносной линии +dimstyle-extension-lineweight = Вес выносной линии +dimstyle-feet-and-inches = Футы и дюймы +dimstyle-fine-tuning = Точная настройка +dimstyle-first-arrowhead = Первая стрелка +dimstyle-first-extension-linetype = Тип первой выносной линии +dimstyle-fit-options = Параметры размещения +dimstyle-fixed-length = Фиксированная длина +dimstyle-fraction-format = Формат дробей +dimstyle-height-scale = Масштаб высоты +dimstyle-horizontal-placement = Размещение по горизонтали +dimstyle-keep-text-horizontal-inside = Сохранять текст горизонтальным внутри +dimstyle-keep-text-horizontal-outside = Сохранять текст горизонтальным снаружи +dimstyle-leader-arrowhead = Стрелка выноски +dimstyle-limits = Предельные значения +dimstyle-linear-dimensions = Линейные размеры +dimstyle-lower-value = Нижнее значение +dimstyle-measurement-scale = Масштаб измерения +dimstyle-measurement-template = Шаблон значения +dimstyle-method = Способ +dimstyle-multiplier = Множитель +dimstyle-no-differences = Различий нет +dimstyle-not-applicable = Неприменимо +dimstyle-offset-from-measured-point = Отступ от измеряемой точки +dimstyle-place-text-manually = Размещать текст вручную при создании размера +dimstyle-radius-jog-angle = Угол излома радиуса (°) +dimstyle-read-left-to-right = Читать слева направо +dimstyle-referenced-read-only = Ссылочный стиль · Только чтение +dimstyle-round-off = Округление +dimstyle-second-arrowhead = Вторая стрелка +dimstyle-second-extension-linetype = Тип второй выносной линии +dimstyle-show-zero-feet-and-inches = Показывать нулевые футы и дюймы +dimstyle-show-zero-feet-suppress-zero-inches = Показывать нулевые футы; подавлять нулевые дюймы +dimstyle-suppress-arrows-when-no-fit = Подавлять стрелки, если они не помещаются +dimstyle-suppress-first-dimension-line = Подавлять первую размерную линию +dimstyle-suppress-first-extension-line = Подавлять первую выносную линию +dimstyle-suppress-second-dimension-line = Подавлять вторую размерную линию +dimstyle-suppress-second-extension-line = Подавлять вторую выносную линию +dimstyle-suppress-zero-feet-and-inches = Подавлять нулевые футы и дюймы +dimstyle-suppress-zero-feet-show-zero-inches = Подавлять нулевые футы; показывать нулевые дюймы +dimstyle-symbols-and-arrows = Символы и стрелки +dimstyle-symmetrical = Симметрично +dimstyle-text-background = Фон текста +dimstyle-text-gap = Отступ текста +dimstyle-fixed-text-height-note = Выбранный стиль текста фиксирует высоту: __ocs_arg_height__. +dimstyle-tick-size = Размер засечек +dimstyle-tolerance-zero-suppression = Подавление нулей допуска +dimstyle-unit-format = Формат единиц +dimstyle-upper-value = Верхнее значение +dimstyle-measurement-template-help = Используйте <> для измеренного значения; текст до и после него станет префиксом и суффиксом. +dimstyle-use-different-arrowheads = Использовать разные первую и вторую стрелки +dimstyle-use-fixed-length-extension-lines = Использовать выносные линии фиксированной длины +dimstyle-used-in-drawing = Используется в чертеже +dimstyle-vertical-offset = Вертикальный отступ +dimstyle-vertical-placement = Размещение по вертикали +dimstyle-vertical-position = Положение по вертикали +dimstyle-when-space-is-limited = При недостатке места +dimstyle-when-text-is-moved = При перемещении текста +dimstyle-zero-suppression = Подавление нулей +dimstyle-add-a-leader = Добавить выноску +dimstyle-automatic = Автоматически +dimstyle-by-block = По блоку +dimstyle-comma = Запятая +dimstyle-degrees-minutes-seconds = Градусы, минуты, секунды +dimstyle-keep-dimension-line-with-text = Перемещать размерную линию вместе с текстом +dimstyle-leading-and-trailing = Ведущие и конечные +dimstyle-move-arrows-outside-first = Сначала вынести стрелки +dimstyle-move-text-and-arrows-outside = Вынести текст и стрелки +dimstyle-move-text-freely = Свободно перемещать текст +dimstyle-move-text-outside-first = Сначала вынести текст +dimstyle-period = Точка +dimstyle-space = Пробел +style-manager-alignment-spacing = Интервал выравнивания +style-manager-always-left-align = Всегда выравнивать по левому краю +style-manager-attachment-direction = Направление присоединения +style-manager-background-fill-enabled = Фоновая заливка включена +style-manager-big-font = Большой шрифт +style-manager-block-scale-x = Масштаб блока X +style-manager-block-scale-y = Масштаб блока Y +style-manager-block-scale-z = Масштаб блока Z +style-manager-borders = Границы +style-manager-bottom-attachment = Нижнее присоединение +style-manager-break-gap-size = Размер зазора разрыва +style-manager-caps-and-fill = Торцы и заливка +style-manager-content-type = Тип содержимого +style-manager-data-row = Строка данных +style-manager-data-type = Тип данных +style-manager-default-text = Текст по умолчанию +style-manager-different = Отличается +style-manager-display-joints = Показывать стыки +style-manager-drawing-height = Высота в чертеже +style-manager-drawing-scale = Масштаб чертежа +style-manager-effects = Эффекты +style-manager-elements = Элементы +style-manager-end-caps = Конечные торцы +style-manager-fill-and-joints = Заливка и стыки +style-manager-fill-enabled = Заливка включена +style-manager-first-segment-angle = Угол первого сегмента +style-manager-fixed-height = Фиксированная высота +style-manager-flow-direction = Направление заполнения +style-manager-font-file = Файл шрифта +style-manager-fonts = Шрифты +style-manager-format-string = Строка формата +style-manager-header = Заголовок столбцов +style-manager-header-row = Строка заголовков +style-manager-horizontal-margin = Горизонтальное поле +style-manager-inner-arcs = Внутренние дуги +style-manager-inside-horizontal = Внутренняя горизонтальная +style-manager-inside-vertical = Внутренняя вертикальная +style-manager-leader-draw-order = Порядок отрисовки выноски +style-manager-leader-note = Примечание выноски +style-manager-left-attachment = Левое присоединение +style-manager-line-color = Цвет линии +style-manager-line-type = Тип линии +style-manager-line-weight = Вес линии +style-manager-maximum-leader-points = Максимум точек выноски +style-manager-multileader-draw-order = Порядок отрисовки мультивыноски +style-manager-no-preview = Нет предпросмотра +style-manager-oblique-angle = Угол наклона +style-manager-outer-arc = Внешняя дуга +style-manager-paper-height = Высота на бумаге +style-manager-paper-text-height = Высота текста на бумаге +style-manager-path-type = Тип траектории +style-manager-right-attachment = Правое присоединение +style-manager-scale-factor = Масштабный коэффициент +style-manager-second-segment-angle = Угол второго сегмента +style-manager-size-and-effects = Размер и эффекты +style-manager-start-caps = Начальные торцы +style-manager-stroke-fonts = Штриховые шрифты +style-manager-suppress-header-row = Скрыть строку заголовков +style-manager-suppress-title-row = Скрыть строку названия +style-manager-system-font = Системный шрифт +style-manager-system-fonts = Системные шрифты +style-manager-text-alignment = Выравнивание текста +style-manager-text-angle = Угол текста +style-manager-title-row = Строка названия +style-manager-top-attachment = Верхнее присоединение +style-manager-unit-type = Тип единиц +style-manager-vertical-margin = Вертикальное поле +style-manager-font-family-placeholder = семейство шрифтов… +style-manager-bottom-to-top = Снизу вверх +style-manager-arrowhead-first = Сначала стрелка +style-manager-content-first = Сначала содержимое +style-manager-leader-first = Сначала выноска +style-manager-straight = Прямая +style-manager-parallel-last-segment = Параллельно последнему сегменту +style-manager-top-of-top-line = Верх верхней строки +style-manager-middle-of-top-line = Середина верхней строки +style-manager-middle-of-text = Середина текста +style-manager-middle-of-bottom-line = Середина нижней строки +style-manager-bottom-of-bottom-line = Низ нижней строки +style-manager-bottom-line = Нижняя строка +style-manager-underline-bottom-line = Подчеркнуть нижнюю строку +style-manager-underline-top-line = Подчеркнуть верхнюю строку +style-manager-underline-all-text = Подчеркнуть весь текст +style-manager-center-of-text = Центр текста +style-manager-center-of-text-overline = Центр текста с надчёркиванием +style-manager-block-extents = Габариты блока +style-manager-base-point = Базовая точка diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 461dccf2f..d8b092a1d 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -3118,3 +3118,185 @@ catalog-mod-shortcuts-4c54bbb78e = ── __ocs_arg_MOD__ Kısayolları ── catalog-custom-overrides-shortcuts-set-07be563dec = ── Özel Geçersiz Kılmalar (SHORTCUTS SET) ───────────── catalog-function-keys-b5a2df24e9 = ── İşlev Tuşları ─────────────────────────────────────── catalog-other-keys-a882812cf2 = ── Diğer Tuşlar ──────────────────────────────────────── +dimstyle-keep-text-between-extension-lines = Metni her zaman uzatma çizgileri arasında tut +dimstyle-angular-dimensions = Açısal ölçüler +dimstyle-arc-length-symbol = Yay uzunluğu simgesi +dimstyle-arrowheads = Ok uçları +dimstyle-available-style = Kullanılabilir stil +dimstyle-baseline-spacing = Taban çizgisi aralığı +dimstyle-basic = Temel +dimstyle-both-arrowheads = Her iki ok ucu +dimstyle-center-mark = Merkez işareti +dimstyle-center-mark-size = Merkez işareti boyutu +dimstyle-center-marks = Merkez işaretleri +dimstyle-centerlines = Merkez çizgileri +dimstyle-compare-with = Karşılaştır +dimstyle-current-style = Geçerli stil +dimstyle-deviation = Sapma +dimstyle-different-sections = Farklı bölümler +dimstyle-dimension-line-color = Ölçü çizgisi rengi +dimstyle-dimension-linetype = Ölçü çizgisi türü +dimstyle-dimension-lineweight = Ölçü çizgisi kalınlığı +dimstyle-display-alternate-units = Alternatif birimleri göster +dimstyle-draw-dimension-line-between-extension-lines = Uzatma çizgileri arasına ölçü çizgisi çiz +dimstyle-extend-beyond-dimension-line = Ölçü çizgisini aşma +dimstyle-extend-beyond-ticks = İşaretleri aşma +dimstyle-extension-line-color = Uzatma çizgisi rengi +dimstyle-extension-lineweight = Uzatma çizgisi kalınlığı +dimstyle-feet-and-inches = Fit ve inç +dimstyle-fine-tuning = İnce ayar +dimstyle-first-arrowhead = Birinci ok ucu +dimstyle-first-extension-linetype = Birinci uzatma çizgisi türü +dimstyle-fit-options = Sığdırma seçenekleri +dimstyle-fixed-length = Sabit uzunluk +dimstyle-fraction-format = Kesir biçimi +dimstyle-height-scale = Yükseklik ölçeği +dimstyle-horizontal-placement = Yatay yerleşim +dimstyle-keep-text-horizontal-inside = İçerideyken metni yatay tut +dimstyle-keep-text-horizontal-outside = Dışarıdayken metni yatay tut +dimstyle-leader-arrowhead = Kılavuz ok ucu +dimstyle-limits = Sınırlar +dimstyle-linear-dimensions = Doğrusal ölçüler +dimstyle-lower-value = Alt değer +dimstyle-measurement-scale = Ölçüm ölçeği +dimstyle-measurement-template = Ölçüm şablonu +dimstyle-method = Yöntem +dimstyle-multiplier = Çarpan +dimstyle-no-differences = Fark yok +dimstyle-not-applicable = Uygulanamaz +dimstyle-offset-from-measured-point = Ölçülen noktadan uzaklık +dimstyle-place-text-manually = Ölçü oluştururken metni elle yerleştir +dimstyle-radius-jog-angle = Yarıçap kırılma açısı (°) +dimstyle-read-left-to-right = Soldan sağa oku +dimstyle-referenced-read-only = Başvurulan stil · Salt okunur +dimstyle-round-off = Yuvarlama +dimstyle-second-arrowhead = İkinci ok ucu +dimstyle-second-extension-linetype = İkinci uzatma çizgisi türü +dimstyle-show-zero-feet-and-inches = Sıfır fit ve sıfır inçi göster +dimstyle-show-zero-feet-suppress-zero-inches = Sıfır fiti göster; sıfır inçi gizle +dimstyle-suppress-arrows-when-no-fit = Sığmadığında okları gizle +dimstyle-suppress-first-dimension-line = Birinci ölçü çizgisini gizle +dimstyle-suppress-first-extension-line = Birinci uzatma çizgisini gizle +dimstyle-suppress-second-dimension-line = İkinci ölçü çizgisini gizle +dimstyle-suppress-second-extension-line = İkinci uzatma çizgisini gizle +dimstyle-suppress-zero-feet-and-inches = Sıfır fit ve sıfır inçi gizle +dimstyle-suppress-zero-feet-show-zero-inches = Sıfır fiti gizle; sıfır inçi göster +dimstyle-symbols-and-arrows = Simgeler ve Oklar +dimstyle-symmetrical = Simetrik +dimstyle-text-background = Metin arka planı +dimstyle-text-gap = Metin boşluğu +dimstyle-fixed-text-height-note = Seçili metin stili yüksekliği __ocs_arg_height__ olarak sabitler. +dimstyle-tick-size = İşaret boyutu +dimstyle-tolerance-zero-suppression = Tolerans sıfırlarını gizleme +dimstyle-unit-format = Birim biçimi +dimstyle-upper-value = Üst değer +dimstyle-measurement-template-help = Ölçülen değer için <> kullanın; önündeki ve arkasındaki metin, ön ek ve son ek olur. +dimstyle-use-different-arrowheads = Birinci ve ikinci ok uçlarını farklı kullan +dimstyle-use-fixed-length-extension-lines = Sabit uzunlukta uzatma çizgileri kullan +dimstyle-used-in-drawing = Çizimde kullanılıyor +dimstyle-vertical-offset = Dikey uzaklık +dimstyle-vertical-placement = Dikey yerleşim +dimstyle-vertical-position = Dikey konum +dimstyle-when-space-is-limited = Alan yetersiz olduğunda +dimstyle-when-text-is-moved = Metin taşındığında +dimstyle-zero-suppression = Sıfırları gizleme +dimstyle-add-a-leader = Kılavuz ekle +dimstyle-automatic = Otomatik +dimstyle-by-block = Bloğa göre +dimstyle-comma = Virgül +dimstyle-degrees-minutes-seconds = Derece, dakika, saniye +dimstyle-keep-dimension-line-with-text = Ölçü çizgisini metinle birlikte taşı +dimstyle-leading-and-trailing = Baştaki ve sondaki +dimstyle-move-arrows-outside-first = Önce okları dışarı taşı +dimstyle-move-text-and-arrows-outside = Metni ve okları dışarı taşı +dimstyle-move-text-freely = Metni serbestçe taşı +dimstyle-move-text-outside-first = Önce metni dışarı taşı +dimstyle-period = Nokta +dimstyle-space = Boşluk +style-manager-alignment-spacing = Hizalama aralığı +style-manager-always-left-align = Her zaman sola hizala +style-manager-attachment-direction = Bağlantı yönü +style-manager-background-fill-enabled = Arka plan dolgusu etkin +style-manager-big-font = Büyük yazı tipi +style-manager-block-scale-x = Blok X ölçeği +style-manager-block-scale-y = Blok Y ölçeği +style-manager-block-scale-z = Blok Z ölçeği +style-manager-borders = Kenarlar +style-manager-bottom-attachment = Alt bağlantı +style-manager-break-gap-size = Kesinti boşluğu boyutu +style-manager-caps-and-fill = Kapaklar ve Dolgu +style-manager-content-type = İçerik türü +style-manager-data-row = Veri Satırı +style-manager-data-type = Veri türü +style-manager-default-text = Varsayılan metin +style-manager-different = Farklı +style-manager-display-joints = Birleşimleri göster +style-manager-drawing-height = Çizim yüksekliği +style-manager-drawing-scale = Çizim ölçeği +style-manager-effects = Etkiler +style-manager-elements = Elemanlar +style-manager-end-caps = Bitiş kapakları +style-manager-fill-and-joints = Dolgu ve birleşimler +style-manager-fill-enabled = Dolgu etkin +style-manager-first-segment-angle = Birinci segment açısı +style-manager-fixed-height = Sabit yükseklik +style-manager-flow-direction = Akış yönü +style-manager-font-file = Yazı tipi dosyası +style-manager-fonts = Yazı tipleri +style-manager-format-string = Biçim dizgesi +style-manager-header = Sütun başlığı +style-manager-header-row = Sütun Başlığı Satırı +style-manager-horizontal-margin = Yatay kenar boşluğu +style-manager-inner-arcs = İç yaylar +style-manager-inside-horizontal = Yatay iç kenar +style-manager-inside-vertical = Dikey iç kenar +style-manager-leader-draw-order = Lider çizim sırası +style-manager-leader-note = Lider notu +style-manager-left-attachment = Sol bağlantı +style-manager-line-color = Çizgi rengi +style-manager-line-type = Çizgi türü +style-manager-line-weight = Çizgi kalınlığı +style-manager-maximum-leader-points = En fazla lider noktası +style-manager-multileader-draw-order = Çoklu lider çizim sırası +style-manager-no-preview = Önizleme yok +style-manager-oblique-angle = Eğiklik açısı +style-manager-outer-arc = Dış yay +style-manager-paper-height = Kağıt yüksekliği +style-manager-paper-text-height = Kağıt metin yüksekliği +style-manager-path-type = Yol türü +style-manager-right-attachment = Sağ bağlantı +style-manager-scale-factor = Ölçek çarpanı +style-manager-second-segment-angle = İkinci segment açısı +style-manager-size-and-effects = Boyut ve Etkiler +style-manager-start-caps = Başlangıç kapakları +style-manager-stroke-fonts = Çizgi yazı tipleri +style-manager-suppress-header-row = Sütun başlığı satırını gizle +style-manager-suppress-title-row = Tablo başlığı satırını gizle +style-manager-system-font = Sistem yazı tipi +style-manager-system-fonts = Sistem yazı tipleri +style-manager-text-alignment = Metin hizalaması +style-manager-text-angle = Metin açısı +style-manager-title-row = Tablo Başlığı Satırı +style-manager-top-attachment = Üst bağlantı +style-manager-unit-type = Birim türü +style-manager-vertical-margin = Dikey kenar boşluğu +style-manager-font-family-placeholder = yazı tipi ailesi… +style-manager-bottom-to-top = Aşağıdan yukarıya +style-manager-arrowhead-first = Önce ok ucu +style-manager-content-first = Önce içerik +style-manager-leader-first = Önce lider +style-manager-straight = Düz +style-manager-parallel-last-segment = Son segmente paralel +style-manager-top-of-top-line = Üst satırın üstü +style-manager-middle-of-top-line = Üst satırın ortası +style-manager-middle-of-text = Metnin ortası +style-manager-middle-of-bottom-line = Alt satırın ortası +style-manager-bottom-of-bottom-line = Alt satırın altı +style-manager-bottom-line = Alt satır +style-manager-underline-bottom-line = Alt satırın altını çiz +style-manager-underline-top-line = Üst satırın altını çiz +style-manager-underline-all-text = Tüm metnin altını çiz +style-manager-center-of-text = Metnin merkezi +style-manager-center-of-text-overline = Üst çizgili metin merkezi +style-manager-block-extents = Blok sınırları +style-manager-base-point = Taban noktası diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index f06bfb3c6..dc4d2b3e5 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -3180,3 +3180,185 @@ catalog-mod-shortcuts-4c54bbb78e = ── __ocs_arg_MOD__ 快捷键 ──── catalog-custom-overrides-shortcuts-set-07be563dec = ── 自定义覆盖 (SHORTCUTS SET) ────────────────── catalog-function-keys-b5a2df24e9 = ── 功能键 ────────────────────────────────────── catalog-other-keys-a882812cf2 = ── 其他按键 ────────────────────────────────────────── +dimstyle-keep-text-between-extension-lines = 始终将文字置于尺寸界线之间 +dimstyle-angular-dimensions = 角度标注 +dimstyle-arc-length-symbol = 弧长符号 +dimstyle-arrowheads = 箭头 +dimstyle-available-style = 可用样式 +dimstyle-baseline-spacing = 基线间距 +dimstyle-basic = 基本尺寸 +dimstyle-both-arrowheads = 两端箭头 +dimstyle-center-mark = 圆心标记 +dimstyle-center-mark-size = 圆心标记大小 +dimstyle-center-marks = 圆心标记 +dimstyle-centerlines = 中心线 +dimstyle-compare-with = 比较对象 +dimstyle-current-style = 当前样式 +dimstyle-deviation = 偏差 +dimstyle-different-sections = 存在差异的分区 +dimstyle-dimension-line-color = 尺寸线颜色 +dimstyle-dimension-linetype = 尺寸线线型 +dimstyle-dimension-lineweight = 尺寸线线宽 +dimstyle-display-alternate-units = 显示换算单位 +dimstyle-draw-dimension-line-between-extension-lines = 在尺寸界线之间绘制尺寸线 +dimstyle-extend-beyond-dimension-line = 超出尺寸线 +dimstyle-extend-beyond-ticks = 超出斜线 +dimstyle-extension-line-color = 尺寸界线颜色 +dimstyle-extension-lineweight = 尺寸界线线宽 +dimstyle-feet-and-inches = 英尺和英寸 +dimstyle-fine-tuning = 精细调整 +dimstyle-first-arrowhead = 第一个箭头 +dimstyle-first-extension-linetype = 第一条尺寸界线线型 +dimstyle-fit-options = 调整选项 +dimstyle-fixed-length = 固定长度 +dimstyle-fraction-format = 分数格式 +dimstyle-height-scale = 高度比例 +dimstyle-horizontal-placement = 水平位置 +dimstyle-keep-text-horizontal-inside = 位于内部时保持文字水平 +dimstyle-keep-text-horizontal-outside = 位于外部时保持文字水平 +dimstyle-leader-arrowhead = 引线箭头 +dimstyle-limits = 极限尺寸 +dimstyle-linear-dimensions = 线性标注 +dimstyle-lower-value = 下偏差 +dimstyle-measurement-scale = 测量比例 +dimstyle-measurement-template = 测量值模板 +dimstyle-method = 方式 +dimstyle-multiplier = 换算系数 +dimstyle-no-differences = 无差异 +dimstyle-not-applicable = 不适用 +dimstyle-offset-from-measured-point = 与测量点的偏移 +dimstyle-place-text-manually = 创建标注时手动放置文字 +dimstyle-radius-jog-angle = 半径折弯角度 (°) +dimstyle-read-left-to-right = 从左向右阅读 +dimstyle-referenced-read-only = 参照样式 · 只读 +dimstyle-round-off = 舍入值 +dimstyle-second-arrowhead = 第二个箭头 +dimstyle-second-extension-linetype = 第二条尺寸界线线型 +dimstyle-show-zero-feet-and-inches = 显示零英尺和零英寸 +dimstyle-show-zero-feet-suppress-zero-inches = 显示零英尺;抑制零英寸 +dimstyle-suppress-arrows-when-no-fit = 箭头无法容纳时将其隐藏 +dimstyle-suppress-first-dimension-line = 隐藏第一条尺寸线 +dimstyle-suppress-first-extension-line = 隐藏第一条尺寸界线 +dimstyle-suppress-second-dimension-line = 隐藏第二条尺寸线 +dimstyle-suppress-second-extension-line = 隐藏第二条尺寸界线 +dimstyle-suppress-zero-feet-and-inches = 抑制零英尺和零英寸 +dimstyle-suppress-zero-feet-show-zero-inches = 抑制零英尺;显示零英寸 +dimstyle-symbols-and-arrows = 符号和箭头 +dimstyle-symmetrical = 对称 +dimstyle-text-background = 文字背景 +dimstyle-text-gap = 文字间距 +dimstyle-fixed-text-height-note = 所选文字样式将高度固定为 __ocs_arg_height__。 +dimstyle-tick-size = 斜线大小 +dimstyle-tolerance-zero-suppression = 公差零值抑制 +dimstyle-unit-format = 单位格式 +dimstyle-upper-value = 上偏差 +dimstyle-measurement-template-help = 使用 <> 表示测量值;其前后的文字将分别成为前缀和后缀。 +dimstyle-use-different-arrowheads = 两端使用不同箭头 +dimstyle-use-fixed-length-extension-lines = 使用固定长度的尺寸界线 +dimstyle-used-in-drawing = 已在图形中使用 +dimstyle-vertical-offset = 垂直偏移 +dimstyle-vertical-placement = 垂直位置 +dimstyle-vertical-position = 垂直定位 +dimstyle-when-space-is-limited = 空间不足时 +dimstyle-when-text-is-moved = 移动文字时 +dimstyle-zero-suppression = 零值抑制 +dimstyle-add-a-leader = 添加引线 +dimstyle-automatic = 自动 +dimstyle-by-block = 随块 +dimstyle-comma = 逗号 +dimstyle-degrees-minutes-seconds = 度、分、秒 +dimstyle-keep-dimension-line-with-text = 尺寸线随文字移动 +dimstyle-leading-and-trailing = 前导和后续 +dimstyle-move-arrows-outside-first = 优先将箭头移到外部 +dimstyle-move-text-and-arrows-outside = 将文字和箭头移到外部 +dimstyle-move-text-freely = 自由移动文字 +dimstyle-move-text-outside-first = 优先将文字移到外部 +dimstyle-period = 句点 +dimstyle-space = 空格 +style-manager-alignment-spacing = 对齐间距 +style-manager-always-left-align = 始终左对齐 +style-manager-attachment-direction = 附着方向 +style-manager-background-fill-enabled = 启用背景填充 +style-manager-big-font = 大字体 +style-manager-block-scale-x = 块 X 比例 +style-manager-block-scale-y = 块 Y 比例 +style-manager-block-scale-z = 块 Z 比例 +style-manager-borders = 边框 +style-manager-bottom-attachment = 底部附着 +style-manager-break-gap-size = 打断间隙大小 +style-manager-caps-and-fill = 端盖和填充 +style-manager-content-type = 内容类型 +style-manager-data-row = 数据行 +style-manager-data-type = 数据类型 +style-manager-default-text = 默认文字 +style-manager-different = 不同 +style-manager-display-joints = 显示接合处 +style-manager-drawing-height = 图形高度 +style-manager-drawing-scale = 图形比例 +style-manager-effects = 效果 +style-manager-elements = 元素 +style-manager-end-caps = 终点端盖 +style-manager-fill-and-joints = 填充和接合处 +style-manager-fill-enabled = 启用填充 +style-manager-first-segment-angle = 第一段角度 +style-manager-fixed-height = 固定高度 +style-manager-flow-direction = 排列方向 +style-manager-font-file = 字体文件 +style-manager-fonts = 字体 +style-manager-format-string = 格式字符串 +style-manager-header = 列标题 +style-manager-header-row = 列标题行 +style-manager-horizontal-margin = 水平边距 +style-manager-inner-arcs = 内弧 +style-manager-inside-horizontal = 内部水平 +style-manager-inside-vertical = 内部垂直 +style-manager-leader-draw-order = 引线绘制顺序 +style-manager-leader-note = 引线注释 +style-manager-left-attachment = 左侧附着 +style-manager-line-color = 线颜色 +style-manager-line-type = 线型 +style-manager-line-weight = 线宽 +style-manager-maximum-leader-points = 最大引线点数 +style-manager-multileader-draw-order = 多重引线绘制顺序 +style-manager-no-preview = 无预览 +style-manager-oblique-angle = 倾斜角 +style-manager-outer-arc = 外弧 +style-manager-paper-height = 图纸高度 +style-manager-paper-text-height = 图纸文字高度 +style-manager-path-type = 路径类型 +style-manager-right-attachment = 右侧附着 +style-manager-scale-factor = 比例因子 +style-manager-second-segment-angle = 第二段角度 +style-manager-size-and-effects = 大小和效果 +style-manager-start-caps = 起点端盖 +style-manager-stroke-fonts = 线性字体 +style-manager-suppress-header-row = 隐藏列标题行 +style-manager-suppress-title-row = 隐藏标题行 +style-manager-system-font = 系统字体 +style-manager-system-fonts = 系统字体 +style-manager-text-alignment = 文字对齐 +style-manager-text-angle = 文字角度 +style-manager-title-row = 标题行 +style-manager-top-attachment = 顶部附着 +style-manager-unit-type = 单位类型 +style-manager-vertical-margin = 垂直边距 +style-manager-font-family-placeholder = 字体族… +style-manager-bottom-to-top = 从下到上 +style-manager-arrowhead-first = 先绘制箭头 +style-manager-content-first = 先绘制内容 +style-manager-leader-first = 先绘制引线 +style-manager-straight = 直线 +style-manager-parallel-last-segment = 平行于最后一段 +style-manager-top-of-top-line = 顶行顶部 +style-manager-middle-of-top-line = 顶行中部 +style-manager-middle-of-text = 文字中部 +style-manager-middle-of-bottom-line = 底行中部 +style-manager-bottom-of-bottom-line = 底行底部 +style-manager-bottom-line = 底行 +style-manager-underline-bottom-line = 底行下划线 +style-manager-underline-top-line = 顶行下划线 +style-manager-underline-all-text = 所有文字下划线 +style-manager-center-of-text = 文字中心 +style-manager-center-of-text-overline = 带上划线的文字中心 +style-manager-block-extents = 块范围 +style-manager-base-point = 基点 diff --git a/src/app/mod.rs b/src/app/mod.rs index 905989570..5c9665d0f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -670,9 +670,18 @@ pub(super) struct OpenCADStudio { // ── MLineStyle Dialog ───────────────────────────────────────────────── mlstyle_selected: String, + mlstyle_tab: u8, + mlstyle_compare: String, + mln_description: String, + mln_start_angle: String, + mln_end_angle: String, + mln_fill_color: String, + mln_elements: Vec<[String; 3]>, // ── MLeaderStyle Dialog ─────────────────────────────────────────────── mleaderstyle_selected: String, + mleaderstyle_tab: u8, + mleaderstyle_compare: String, /// Colour field whose expanded palette is open (line/text/block). mls_color_open: Option<&'static str>, mls_landing_distance: String, @@ -697,6 +706,8 @@ pub(super) struct OpenCADStudio { // ── TableStyle Dialog ───────────────────────────────────────────────── tablestyle_selected: String, + tablestyle_tab: u8, + tablestyle_compare: String, /// Edit buffers for the table style's general margins. ts_hmargin: String, ts_vmargin: String, @@ -732,6 +743,8 @@ pub(super) struct OpenCADStudio { // ── TextStyle Font Browser ──────────────────────────────────────────── textstyle_selected: String, + textstyle_tab: u8, + textstyle_compare: String, /// Edit buffer for font file name. textstyle_font: String, /// Edit buffer for width factor. @@ -868,8 +881,10 @@ pub(super) struct OpenCADStudio { dimstyle_selected: String, /// Which colour field currently has its expanded palette open (if any). ds_color_open: Option, - /// Active tab: 0=Lines, 1=Arrows, 2=Text, 3=Scale/Units, 4=Tolerances. + /// Active property group in the dimension style manager. dimstyle_tab: u8, + /// Style used by the manager's comparison summary. + dimstyle_compare: String, // Edit buffers (strings while typing): ds_dimdle: String, ds_dimdli: String, @@ -2438,6 +2453,8 @@ pub enum Message { #[allow(dead_code)] TextStyleDialogClose, TextStyleDialogSelect(String), + TextStyleDialogTab(u8), + TextStyleDialogCompare(String), TextStyleDialogSetCurrent, TextStyleDialogNew, TextStyleDialogCopy, @@ -2465,6 +2482,8 @@ pub enum Message { #[allow(dead_code)] TableStyleDialogClose, TableStyleDialogSelect(String), + TableStyleDialogTab(u8), + TableStyleDialogCompare(String), TableStyleDialogNew, TableStyleDialogCopy, TableStyleDialogDelete, @@ -2523,16 +2542,32 @@ pub enum Message { #[allow(dead_code)] MlStyleDialogClose, MlStyleDialogSelect(String), + MlStyleDialogTab(u8), + MlStyleDialogCompare(String), MlStyleDialogSetCurrent, MlStyleApply, MlStyleDialogNew, MlStyleDialogCopy, MlStyleDialogDelete, + MlStyleEdit { + field: &'static str, + value: String, + }, + MlStyleToggle(&'static str), + MlStyleElementEdit { + index: usize, + field: &'static str, + value: String, + }, + MlStyleElementAdd, + MlStyleElementDelete(usize), // ── MLeaderStyle Dialog ─────────────────────────────────────────────── MLeaderStyleDialogOpen, #[allow(dead_code)] MLeaderStyleDialogClose, MLeaderStyleDialogSelect(String), + MLeaderStyleDialogTab(u8), + MLeaderStyleDialogCompare(String), MLeaderStyleDialogSetCurrent, MLeaderStyleDialogNew, MLeaderStyleDialogCopy, @@ -2565,6 +2600,8 @@ pub enum Message { DimStyleDialogSelect(String), /// Switch the active tab. DimStyleDialogTab(u8), + /// Change the style used by the comparison summary. + DimStyleDialogCompare(String), /// Create a new empty style (prompts via command line). DimStyleDialogNew, DimStyleDialogCopy, @@ -2575,6 +2612,14 @@ pub enum Message { // Field edit messages: DsEdit(DsField, String), DsToggle(DsField), + /// Select the mutually-exclusive tolerance presentation. + DsToleranceMode(String), + /// Change the feet/inches mode in a zero-suppression bit field. + DsZeroBase(DsField, i16), + /// Toggle leading/trailing suppression in a zero-suppression bit field. + DsZeroFlag(DsField, i16), + /// Select no center mark, a mark, or centerlines. + DsCenterMarkMode(String), /// Toggle the expanded colour palette for a DimStyle colour field. DsColorMore(DsField), /// Open the `iced_aw` colour picker for a field and its current colour. @@ -2887,6 +2932,8 @@ impl OpenCADStudio { style_rename_buf: String::new(), style_stage: None, textstyle_selected: "Standard".to_string(), + textstyle_tab: 0, + textstyle_compare: String::new(), textstyle_font: String::new(), textstyle_width: "1.0".to_string(), textstyle_oblique: "0.0".to_string(), @@ -2895,6 +2942,8 @@ impl OpenCADStudio { textstyle_ttf: String::new(), // TableStyle dialog tablestyle_selected: "Standard".to_string(), + tablestyle_tab: 0, + tablestyle_compare: String::new(), ts_hmargin: "1.5".to_string(), ts_vmargin: "1.5".to_string(), ts_description: String::new(), @@ -2911,8 +2960,17 @@ impl OpenCADStudio { ts_border_spacing: Default::default(), // MLineStyle dialog mlstyle_selected: "Standard".to_string(), + mlstyle_tab: 0, + mlstyle_compare: String::new(), + mln_description: String::new(), + mln_start_angle: "90".to_string(), + mln_end_angle: "90".to_string(), + mln_fill_color: "256".to_string(), + mln_elements: Vec::new(), // MLeaderStyle dialog mleaderstyle_selected: "Standard".to_string(), + mleaderstyle_tab: 0, + mleaderstyle_compare: String::new(), mls_color_open: None, mls_landing_distance: String::new(), mls_landing_gap: String::new(), @@ -2937,6 +2995,7 @@ impl OpenCADStudio { dimstyle_selected: "Standard".to_string(), ds_color_open: None, dimstyle_tab: 0, + dimstyle_compare: String::new(), ds_dimdle: "0".to_string(), ds_dimdli: "3.75".to_string(), ds_dimgap: "0.625".to_string(), diff --git a/src/app/style_ops.rs b/src/app/style_ops.rs index 4c1d5ff97..cde445d37 100644 --- a/src/app/style_ops.rs +++ b/src/app/style_ops.rs @@ -301,7 +301,7 @@ impl OpenCADStudio { } } - fn style_in_use(&self, kind: StyleKind, name: &str) -> bool { + pub(super) fn style_in_use(&self, kind: StyleKind, name: &str) -> bool { use acadrust::entities::EntityType; let i = self.active_tab; @@ -613,6 +613,20 @@ impl OpenCADStudio { pub(super) fn style_delete(&mut self, kind: StyleKind) { let name = self.style_selected(kind); + if matches!(kind, StyleKind::Dim) + && self.tabs[self.active_tab] + .scene + .document + .dim_styles + .get(&name) + .is_some_and(|style| { + style.xref_reference || style.xref_dependent || !style.xref_handle.is_null() + }) + { + self.command_line + .push_error(crate::t!("Referenced styles are read only.").as_ref()); + return; + } if name.eq_ignore_ascii_case("Standard") { self.command_line .push_error(crate::t!("Cannot delete the Standard style.").as_ref()); @@ -657,6 +671,20 @@ impl OpenCADStudio { if new.is_empty() || new.eq_ignore_ascii_case(&old) { return; } + if matches!(kind, StyleKind::Dim) + && self.tabs[self.active_tab] + .scene + .document + .dim_styles + .get(&old) + .is_some_and(|style| { + style.xref_reference || style.xref_dependent || !style.xref_handle.is_null() + }) + { + self.command_line + .push_error(crate::t!("Referenced styles are read only.").as_ref()); + return; + } if old.eq_ignore_ascii_case("Standard") { self.command_line .push_error(crate::t!("Cannot rename the Standard style.").as_ref()); diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 97183e53e..8c7e30e59 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -5408,11 +5408,25 @@ impl OpenCADStudio { self.load_textstyle_bufs(i); Task::none() } + Message::TextStyleDialogTab(tab) => { + self.textstyle_tab = tab; + Task::none() + } + Message::TextStyleDialogCompare(name) => { + self.textstyle_compare = name; + Task::none() + } Message::TextStyleDialogSetCurrent => { // Staged: persists on Apply. let i = self.active_tab; let name = self.textstyle_selected.clone(); - if self.tabs[i].scene.document.text_styles.get(&name).is_some() { + if self.tabs[i] + .scene + .document + .text_styles + .get(&name) + .is_some_and(|style| !style.xref_dependent) + { self.tabs[i].scene.document.header.current_text_style_name = name.clone(); self.sync_ribbon_styles(); self.command_line @@ -5468,9 +5482,13 @@ impl OpenCADStudio { let i = self.active_tab; let name = self.textstyle_selected.clone(); if let Some(s) = self.tabs[i].scene.document.text_styles.get_mut(&name) { + if s.xref_dependent { + return Task::none(); + } match field { "backward" => s.flags.backward = !s.flags.backward, "upside_down" => s.flags.upside_down = !s.flags.upside_down, + "vertical" => s.is_vertical = !s.is_vertical, "annotative" => s.annotative = !s.annotative, _ => {} } @@ -5481,8 +5499,17 @@ impl OpenCADStudio { Message::TextStyleFontPick(font_file) => { // Staged: update the buffer + live style; persist on Apply. let i = self.active_tab; - self.textstyle_font = font_file.clone(); let name = self.textstyle_selected.clone(); + if self.tabs[i] + .scene + .document + .text_styles + .get(&name) + .is_some_and(|style| style.xref_dependent) + { + return Task::none(); + } + self.textstyle_font = font_file.clone(); if let Some(s) = self.tabs[i].scene.document.text_styles.get_mut(&name) { s.font_file = font_file; } @@ -5516,12 +5543,23 @@ impl OpenCADStudio { Task::none() } Message::TableStyleDialogSelect(name) => { + for row in 0..3 { + let _ = self.on_table_style_cell_apply(row); + } self.stage_tablestyle_bufs(); self.tablestyle_selected = name; let i = self.active_tab; self.load_tablestyle_bufs(i); Task::none() } + Message::TableStyleDialogTab(tab) => { + self.tablestyle_tab = tab; + Task::none() + } + Message::TableStyleDialogCompare(name) => { + self.tablestyle_compare = name; + Task::none() + } Message::TableStyleEdit { field, value } => { match field { @@ -5534,6 +5572,9 @@ impl OpenCADStudio { } Message::TableStyleApply => { + for row in 0..3 { + let _ = self.on_table_style_cell_apply(row); + } self.stage_tablestyle_bufs(); self.style_stage_commit(); Task::none() @@ -5728,7 +5769,18 @@ impl OpenCADStudio { Task::none() } Message::MlStyleDialogSelect(name) => { + self.stage_mlstyle_bufs(); self.mlstyle_selected = name; + let i = self.active_tab; + self.load_mlstyle_bufs(i); + Task::none() + } + Message::MlStyleDialogTab(tab) => { + self.mlstyle_tab = tab; + Task::none() + } + Message::MlStyleDialogCompare(name) => { + self.mlstyle_compare = name; Task::none() } Message::MlStyleDialogSetCurrent => { @@ -5749,13 +5801,8 @@ impl OpenCADStudio { } Task::none() } - // Placeholder so the Multiline manager has the same Set Current + - // Apply pair as every other style manager. The editor is currently - // read-only, so there is nothing to apply yet — wire this up when - // editable MLineStyle properties land. Message::MlStyleApply => { - // Multiline styles have no editable properties yet; Apply still - // commits any staged structural / current-style changes. + self.stage_mlstyle_bufs(); self.style_stage_commit(); Task::none() } @@ -5771,6 +5818,66 @@ impl OpenCADStudio { self.style_delete(crate::app::StyleKind::MLine); Task::none() } + Message::MlStyleEdit { field, value } => { + match field { + "description" => self.mln_description = value, + "start_angle" => self.mln_start_angle = value, + "end_angle" => self.mln_end_angle = value, + "fill_color" => self.mln_fill_color = value, + _ => {} + } + self.stage_mlstyle_bufs(); + Task::none() + } + Message::MlStyleToggle(field) => { + let i = self.active_tab; + if let Some(style) = self.mlstyle_mut(i) { + match field { + "fill" => style.flags.fill_on = !style.flags.fill_on, + "joints" => style.flags.display_joints = !style.flags.display_joints, + "start_square" => style.flags.start_square_cap = !style.flags.start_square_cap, + "start_inner" => style.flags.start_inner_arcs_cap = !style.flags.start_inner_arcs_cap, + "start_round" => style.flags.start_round_cap = !style.flags.start_round_cap, + "end_square" => style.flags.end_square_cap = !style.flags.end_square_cap, + "end_inner" => style.flags.end_inner_arcs_cap = !style.flags.end_inner_arcs_cap, + "end_round" => style.flags.end_round_cap = !style.flags.end_round_cap, + _ => {} + } + } + Task::none() + } + Message::MlStyleElementEdit { index, field, value } => { + if let Some(element) = self.mln_elements.get_mut(index) { + match field { + "offset" => element[0] = value, + "color" => element[1] = value, + "linetype" => element[2] = value, + _ => {} + } + } + self.stage_mlstyle_bufs(); + Task::none() + } + Message::MlStyleElementAdd => { + let i = self.active_tab; + if let Some(style) = self.mlstyle_mut(i) { + style + .elements + .push(acadrust::objects::MLineStyleElement::default()); + } + self.load_mlstyle_bufs(i); + Task::none() + } + Message::MlStyleElementDelete(index) => { + let i = self.active_tab; + if let Some(style) = self.mlstyle_mut(i) { + if style.elements.len() > 1 && index < style.elements.len() { + style.elements.remove(index); + } + } + self.load_mlstyle_bufs(i); + Task::none() + } // ── MLeaderStyle Dialog ─────────────────────────────────────────── Message::MLeaderStyleDialogOpen => self.on_mleader_style_dialog_open(), @@ -5785,6 +5892,14 @@ impl OpenCADStudio { self.load_mleaderstyle_bufs(i); Task::none() } + Message::MLeaderStyleDialogTab(tab) => { + self.mleaderstyle_tab = tab; + Task::none() + } + Message::MLeaderStyleDialogCompare(name) => { + self.mleaderstyle_compare = name; + Task::none() + } Message::MLeaderStyleDialogSetCurrent => self.on_mleader_style_dialog_set_current(), Message::MLeaderStyleDialogNew => { self.style_new(crate::app::StyleKind::MLeader); @@ -5864,6 +5979,10 @@ impl OpenCADStudio { self.dimstyle_tab = tab; Task::none() } + Message::DimStyleDialogCompare(name) => { + self.dimstyle_compare = name; + Task::none() + } Message::DimStyleDialogNew => { self.style_new(crate::app::StyleKind::Dim); Task::none() @@ -5875,6 +5994,19 @@ impl OpenCADStudio { Message::DimStyleDialogSetCurrent => { // Staged: persists on Apply. let i = self.active_tab; + let read_only = self.tabs[i] + .scene + .document + .dim_styles + .get(&self.dimstyle_selected) + .is_some_and(|style| { + style.xref_reference + || style.xref_dependent + || !style.xref_handle.is_null() + }); + if read_only { + return Task::none(); + } self.tabs[i].scene.document.header.current_dimstyle_name = self.dimstyle_selected.clone(); self.sync_ribbon_styles(); @@ -5894,7 +6026,75 @@ impl OpenCADStudio { Task::none() } Message::DsToggle(field) => { + let separate_arrows = field == crate::app::DsField::Dimsah; self.apply_ds_toggle(field); + if separate_arrows && self.ds_dimsah { + let i = self.active_tab; + if let Some(style) = self.tabs[i] + .scene + .document + .dim_styles + .get_mut(&self.dimstyle_selected) + { + if style.dimblk1.is_null() { + style.dimblk1 = style.dimblk; + } + if style.dimblk2.is_null() { + style.dimblk2 = style.dimblk1; + } + } + } + Task::none() + } + Message::DsToleranceMode(mode) => { + self.ds_dimlim = mode == "limits"; + self.ds_dimtol = matches!(mode.as_str(), "symmetrical" | "deviation"); + if mode == "symmetrical" { + self.ds_dimtm = self.ds_dimtp.clone(); + } + let gap = self.ds_dimgap.trim().parse::().unwrap_or(0.625).abs(); + self.ds_dimgap = if mode == "basic" { + format!("-{}", gap.max(f64::EPSILON)) + } else { + format!("{}", gap) + }; + Task::none() + } + Message::DsZeroBase(field, base) => { + let current = match &field { + crate::app::DsField::Dimzin => &self.ds_dimzin, + crate::app::DsField::Dimaltz => &self.ds_dimaltz, + crate::app::DsField::Dimalttz => &self.ds_dimalttz, + crate::app::DsField::Dimtzin => &self.ds_dimtzin, + _ => return Task::none(), + } + .trim() + .parse::() + .unwrap_or(0); + self.apply_ds_edit(field, ((current & !3) | (base & 3)).to_string()); + Task::none() + } + Message::DsZeroFlag(field, bit) => { + let current = match &field { + crate::app::DsField::Dimzin => &self.ds_dimzin, + crate::app::DsField::Dimaltz => &self.ds_dimaltz, + crate::app::DsField::Dimalttz => &self.ds_dimalttz, + crate::app::DsField::Dimtzin => &self.ds_dimtzin, + _ => return Task::none(), + } + .trim() + .parse::() + .unwrap_or(0); + self.apply_ds_edit(field, (current ^ bit).to_string()); + Task::none() + } + Message::DsCenterMarkMode(mode) => { + let size = self.ds_dimcen.trim().parse::().unwrap_or(0.09).abs(); + self.ds_dimcen = match mode.as_str() { + "mark" => size.max(f64::EPSILON).to_string(), + "lines" => format!("-{}", size.max(f64::EPSILON)), + _ => "0".to_string(), + }; Task::none() } Message::DsColorMore(field) => { diff --git a/src/app/update/style.rs b/src/app/update/style.rs index bfb551d1b..be8781683 100644 --- a/src/app/update/style.rs +++ b/src/app/update/style.rs @@ -22,6 +22,91 @@ use iced::{mouse, Point, Task}; impl OpenCADStudio { + pub(in crate::app) fn mlstyle_mut( + &mut self, + tab: usize, + ) -> Option<&mut acadrust::objects::MLineStyle> { + use acadrust::objects::ObjectType; + let name = self.mlstyle_selected.clone(); + self.tabs[tab] + .scene + .document + .objects + .values_mut() + .find_map(|object| match object { + ObjectType::MLineStyle(style) if style.name == name => Some(style), + _ => None, + }) + } + + pub(in crate::app) fn load_mlstyle_bufs(&mut self, tab: usize) { + use acadrust::objects::ObjectType; + let name = self.mlstyle_selected.clone(); + let Some((description, start_angle, end_angle, fill_color, elements)) = self.tabs[tab] + .scene + .document + .objects + .values() + .find_map(|object| match object { + ObjectType::MLineStyle(style) if style.name == name => Some(( + style.description.clone(), + style.start_angle.to_degrees(), + style.end_angle.to_degrees(), + style.fill_color.index().unwrap_or(256), + style + .elements + .iter() + .map(|element| { + [ + format!("{}", element.offset), + element.color.index().unwrap_or(256).to_string(), + element.linetype.clone(), + ] + }) + .collect::>(), + )), + _ => None, + }) + else { + return; + }; + self.mln_description = description; + self.mln_start_angle = format!("{start_angle:.4}"); + self.mln_end_angle = format!("{end_angle:.4}"); + self.mln_fill_color = fill_color.to_string(); + self.mln_elements = elements; + } + + pub(in crate::app) fn stage_mlstyle_bufs(&mut self) { + let tab = self.active_tab; + let description = self.mln_description.clone(); + let start_angle = self.mln_start_angle.trim().parse::().ok(); + let end_angle = self.mln_end_angle.trim().parse::().ok(); + let fill_color = self.mln_fill_color.trim().parse::().ok(); + let elements = self.mln_elements.clone(); + if let Some(style) = self.mlstyle_mut(tab) { + style.description = description; + if let Some(value) = start_angle { + style.start_angle = value.to_radians(); + } + if let Some(value) = end_angle { + style.end_angle = value.to_radians(); + } + if let Some(value) = fill_color { + style.fill_color = acadrust::types::Color::from_index(value); + } + for (element, values) in style.elements.iter_mut().zip(elements) { + if let Ok(value) = values[0].trim().parse::() { + element.offset = value; + } + if let Ok(value) = values[1].trim().parse::() { + element.color = acadrust::types::Color::from_index(value); + } + element.linetype = values[2].clone(); + } + } + } + pub(in crate::app) fn tablestyle_mut(&mut self, tab: usize) -> Option<&mut acadrust::objects::TableStyle> { use acadrust::objects::ObjectType; let name = self.tablestyle_selected.clone(); @@ -163,8 +248,8 @@ impl OpenCADStudio { self.mls_text_height = format!("{:.4}", s.text_height); self.mls_scale_factor = format!("{:.4}", s.scale_factor); self.mls_break_gap = format!("{:.4}", s.break_gap_size); - self.mls_first_seg_angle = format!("{:.4}", s.first_segment_angle); - self.mls_second_seg_angle = format!("{:.4}", s.second_segment_angle); + self.mls_first_seg_angle = format!("{:.4}", s.first_segment_angle.to_degrees()); + self.mls_second_seg_angle = format!("{:.4}", s.second_segment_angle.to_degrees()); self.mls_max_points = s.max_leader_points.to_string(); self.mls_default_text = s.default_text.clone(); self.mls_line_color = s @@ -184,7 +269,7 @@ impl OpenCADStudio { .index() .map(|c| c.to_string()) .unwrap_or_default(); - self.mls_block_rotation = format!("{:.4}", s.block_content_rotation); + self.mls_block_rotation = format!("{:.4}", s.block_content_rotation.to_degrees()); self.mls_block_scale_x = format!("{:.4}", s.block_content_scale_x); self.mls_block_scale_y = format!("{:.4}", s.block_content_scale_y); self.mls_block_scale_z = format!("{:.4}", s.block_content_scale_z); @@ -273,6 +358,20 @@ impl OpenCADStudio { pub(in crate::app) fn apply_dimstyle_bufs(&mut self, tab: usize) { let doc = &mut self.tabs[tab].scene.document; + let read_only = doc + .dim_styles + .get(&self.dimstyle_selected) + .is_some_and(|style| { + style.xref_reference || style.xref_dependent || !style.xref_handle.is_null() + }); + if read_only { + return; + } + let text_style_handle = doc + .text_styles + .get(&self.ds_dimtxsty) + .map(|style| style.handle) + .unwrap_or(acadrust::types::Handle::NULL); let Some(ds) = doc.dim_styles.get_mut(&self.dimstyle_selected) else { return; }; @@ -323,6 +422,7 @@ impl OpenCADStudio { ds.dimlim = self.ds_dimlim; ds.dimpost = self.ds_dimpost.clone(); ds.dimtxsty = self.ds_dimtxsty.clone(); + ds.dimtxsty_handle = text_style_handle; ds.annotative = self.ds_annotative; set_i16!(dimclrd, self.ds_dimclrd); set_i16!(dimlwd, self.ds_dimlwd); @@ -392,7 +492,15 @@ impl OpenCADStudio { Dimlunit => self.ds_dimlunit = val, Dimdec => self.ds_dimdec = val, Dimpost => self.ds_dimpost = val, - Dimtp => self.ds_dimtp = val, + Dimtp => { + let symmetrical = self.ds_dimtol + && !self.ds_dimlim + && self.ds_dimtp.trim() == self.ds_dimtm.trim(); + self.ds_dimtp = val.clone(); + if symmetrical { + self.ds_dimtm = val; + } + } Dimtm => self.ds_dimtm = val, Dimtdec => self.ds_dimtdec = val, Dimtfac => self.ds_dimtfac = val, @@ -518,6 +626,9 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { let bigfont = self.textstyle_bigfont.clone(); let ttf = self.textstyle_ttf.clone(); if let Some(s) = self.tabs[i].scene.document.text_styles.get_mut(&name) { + if s.xref_dependent { + return; + } s.font_file = font; s.big_font_file = bigfont; s.true_type_font = ttf; @@ -629,6 +740,7 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { .unwrap_or_else(|| "Standard".to_string()) }; self.active_modal = Some(crate::app::ModalKind::MlStyle); + self.load_mlstyle_bufs(i); self.style_stage_begin(); Task::none() } @@ -818,7 +930,10 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { pub(super) fn on_mleader_style_set_handle(&mut self, field: &'static str, value: String) -> Task { let i = self.active_tab; let doc = &self.tabs[i].scene.document; - let handle: Option = if value == "None" { + let handle: Option = if value == "None" + || value == "ByBlock" + || value == "Closed filled" + { None } else { match field { @@ -897,10 +1012,10 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { s.break_gap_size = v; } if let Some(v) = fsa { - s.first_segment_angle = v; + s.first_segment_angle = v.to_radians(); } if let Some(v) = ssa { - s.second_segment_angle = v; + s.second_segment_angle = v.to_radians(); } if let Some(v) = mp { s.max_leader_points = v; @@ -920,7 +1035,7 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { s.block_content_color = acadrust::types::Color::from_index(v); } if let Some(v) = brot { - s.block_content_rotation = v; + s.block_content_rotation = v.to_radians(); } if let Some(v) = bsx { s.block_content_scale_x = v; @@ -962,6 +1077,14 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { .unwrap_or_else(|| "Standard".to_string()) }; self.dimstyle_selected = selected.clone(); + self.dimstyle_compare = self.tabs[i] + .scene + .document + .dim_styles + .iter() + .map(|style| style.name.clone()) + .find(|name| !name.eq_ignore_ascii_case(&selected)) + .unwrap_or_default(); self.load_dimstyle_bufs(i); self.active_modal = Some(crate::app::ModalKind::DimStyle); self.style_stage_begin(); @@ -1018,6 +1141,11 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { "dimltex_handle" | "dimltex1_handle" | "dimltex2_handle" ); let doc = &self.tabs[i].scene.document; + if doc.dim_styles.get(&name).is_some_and(|style| { + style.xref_reference || style.xref_dependent || !style.xref_handle.is_null() + }) { + return Task::none(); + } let handle = if value == "Default" || value == "ByBlock" { acadrust::types::Handle::NULL } else if is_lt { @@ -1037,7 +1165,10 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { if let Some(ds) = self.tabs[i].scene.document.dim_styles.get_mut(&name) { match field { "dimblk" => ds.dimblk = handle, - "dimblk1" => ds.dimblk1 = handle, + "dimblk1" => { + ds.dimblk1 = handle; + ds.dimblk2 = handle; + } "dimblk2" => ds.dimblk2 = handle, "dimldrblk" => ds.dimldrblk = handle, "dimltex_handle" => ds.dimltex_handle = handle, diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 43c6bbdfa..4082e98c8 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -362,6 +362,7 @@ impl OpenCADStudio { ), super::super::ModalKind::TextStyle => { let tab = &self.tabs[self.active_tab]; + let doc = &tab.scene.document; let styles: Vec = tab .scene .document @@ -369,23 +370,77 @@ impl OpenCADStudio { .iter() .map(|s| s.name.clone()) .collect(); - let (backward, upside_down, annotative) = tab - .scene - .document + let selected_style = doc .text_styles - .get(&self.textstyle_selected) - .map(|s| (s.flags.backward, s.flags.upside_down, s.annotative)) - .unwrap_or((false, false, false)); + .get(&self.textstyle_selected); + let (backward, upside_down, vertical, annotative, read_only) = selected_style + .map(|style| { + ( + style.flags.backward, + style.flags.upside_down, + style.is_vertical, + style.annotative, + style.xref_dependent, + ) + }) + .unwrap_or((false, false, false, false, false)); + let in_use = self.style_in_use( + crate::app::StyleKind::Text, + &self.textstyle_selected, + ); + let compare_opts: Vec = styles + .iter() + .filter(|name| !name.eq_ignore_ascii_case(&self.textstyle_selected)) + .cloned() + .collect(); + let compare_name = compare_opts + .iter() + .find(|name| name.eq_ignore_ascii_case(&self.textstyle_compare)) + .cloned() + .or_else(|| compare_opts.first().cloned()) + .unwrap_or_default(); + let mut comparison_sections = Vec::new(); + if let (Some(a), Some(b)) = (selected_style, doc.text_styles.get(&compare_name)) { + if ( + &a.font_file, + &a.big_font_file, + &a.true_type_font, + a.is_shape_file, + ) != ( + &b.font_file, + &b.big_font_file, + &b.true_type_font, + b.is_shape_file, + ) { + comparison_sections.push(crate::i18n::translate("Fonts").into_owned()); + } + if (a.height, a.width_factor, a.oblique_angle, a.last_height) + != (b.height, b.width_factor, b.oblique_angle, b.last_height) + { + comparison_sections.push(crate::i18n::translate("Size").into_owned()); + } + if (a.flags, a.is_vertical, a.annotative) + != (b.flags, b.is_vertical, b.annotative) + { + comparison_sections.push(crate::i18n::translate("Effects").into_owned()); + } + } sized_flow( ex, - 860, - 480, + 960, + 680, |flow| { crate::ui::style::textstyle::view_window( crate::ui::style::textstyle::TextStyleView { styles: styles.clone(), selected: &self.textstyle_selected, current: &tab.scene.document.header.current_text_style_name, + tab: self.textstyle_tab, + compare_name: compare_name.clone(), + compare_opts: compare_opts.clone(), + comparison_sections: comparison_sections.clone(), + read_only, + in_use, font_buf: &self.textstyle_font, width_buf: &self.textstyle_width, oblique_buf: &self.textstyle_oblique, @@ -394,6 +449,7 @@ impl OpenCADStudio { ttf_buf: &self.textstyle_ttf, backward, upside_down, + vertical, annotative, rename_active: self.style_rename.as_deref(), rename_buf: &self.style_rename_buf, @@ -420,18 +476,60 @@ impl OpenCADStudio { ObjectType::MLineStyle(s) if s.name == self.mlstyle_selected => Some(s), _ => None, }); + let compare_opts: Vec = styles + .iter() + .filter(|name| !name.eq_ignore_ascii_case(&self.mlstyle_selected)) + .cloned() + .collect(); + let compare_name = compare_opts + .iter() + .find(|name| name.eq_ignore_ascii_case(&self.mlstyle_compare)) + .cloned() + .or_else(|| compare_opts.first().cloned()) + .unwrap_or_default(); + let compare_style = tab.scene.document.objects.values().find_map(|object| match object { + ObjectType::MLineStyle(style) if style.name == compare_name => Some(style), + _ => None, + }); + let mut comparison_sections = Vec::new(); + if let (Some(a), Some(b)) = (selected_style, compare_style) { + if (&a.description, a.flags, a.fill_color, a.start_angle, a.end_angle) + != (&b.description, b.flags, b.fill_color, b.start_angle, b.end_angle) + { + comparison_sections.push(crate::i18n::translate("Caps and Fill").into_owned()); + } + if a.elements != b.elements { + comparison_sections.push(crate::i18n::translate("Elements").into_owned()); + } + } + let in_use = self.style_in_use( + crate::app::StyleKind::MLine, + &self.mlstyle_selected, + ); sized_flow( ex, - 620, - 420, + 960, + 680, |flow| { crate::ui::style::mlstyle::view_window( - styles.clone(), - &self.mlstyle_selected, - selected_style, - tab.scene.document.header.multiline_style.clone(), - self.style_rename.as_deref(), - &self.style_rename_buf, + crate::ui::style::mlstyle::MlStyleView { + styles: styles.clone(), + selected: &self.mlstyle_selected, + style: selected_style, + current: tab.scene.document.header.multiline_style.clone(), + tab: self.mlstyle_tab, + compare_name: compare_name.clone(), + compare_opts: compare_opts.clone(), + comparison_sections: comparison_sections.clone(), + in_use, + description: &self.mln_description, + start_angle: &self.mln_start_angle, + end_angle: &self.mln_end_angle, + fill_color: &self.mln_fill_color, + elements: &self.mln_elements, + rename_active: self.style_rename.as_deref(), + rename_buf: &self.style_rename_buf, + }, flow, ) }, @@ -454,32 +552,89 @@ impl OpenCADStudio { ObjectType::TableStyle(s) if s.name == self.tablestyle_selected => Some(s), _ => None, }); + let compare_opts: Vec = styles + .iter() + .filter(|name| !name.eq_ignore_ascii_case(&self.tablestyle_selected)) + .cloned() + .collect(); + let compare_name = compare_opts + .iter() + .find(|name| name.eq_ignore_ascii_case(&self.tablestyle_compare)) + .cloned() + .or_else(|| compare_opts.first().cloned()) + .unwrap_or_default(); + let compare_style = tab.scene.document.objects.values().find_map(|object| match object { + ObjectType::TableStyle(style) if style.name == compare_name => Some(style), + _ => None, + }); + let mut comparison_sections = Vec::new(); + if let (Some(a), Some(b)) = (selected_style, compare_style) { + if ( + &a.description, + a.flow_direction, + a.horizontal_margin, + a.vertical_margin, + a.title_suppressed, + a.header_suppressed, + a.annotative, + ) != ( + &b.description, + b.flow_direction, + b.horizontal_margin, + b.vertical_margin, + b.title_suppressed, + b.header_suppressed, + b.annotative, + ) { + comparison_sections.push(crate::i18n::translate("General").into_owned()); + } + for (label, different) in [ + ("Data Row", a.data_row_style != b.data_row_style), + ("Header Row", a.header_row_style != b.header_row_style), + ("Title Row", a.title_row_style != b.title_row_style), + ] { + if different { + comparison_sections.push(crate::i18n::translate(label).into_owned()); + } + } + } + let in_use = self.style_in_use( + crate::app::StyleKind::Table, + &self.tablestyle_selected, + ); sized_flow( ex, - 620, - 420, + 960, + 680, |flow| { crate::ui::style::tablestyle::view_window( - styles.clone(), - &self.tablestyle_selected, - &self.ribbon.active_table_style, - selected_style, - &self.ts_hmargin, - &self.ts_vmargin, - &self.ts_description, - &self.ts_cell_textstyle, - &self.ts_cell_height, - &self.ts_cell_textcolor, - &self.ts_cell_fillcolor, - &self.ts_cell_datatype, - &self.ts_cell_unittype, - &self.ts_cell_format, - &self.ts_border_lw, - &self.ts_border_color, - &self.ts_border_spacing, - self.style_rename.as_deref(), - &self.style_rename_buf, - self.ts_color_open, + crate::ui::style::tablestyle::TableStyleView { + styles: styles.clone(), + selected: &self.tablestyle_selected, + current: &tab.scene.document.header.current_table_style_name, + style: selected_style, + tab: self.tablestyle_tab, + compare_name: compare_name.clone(), + compare_opts: compare_opts.clone(), + comparison_sections: comparison_sections.clone(), + in_use, + hmargin: &self.ts_hmargin, + vmargin: &self.ts_vmargin, + description: &self.ts_description, + cell_textstyle: &self.ts_cell_textstyle, + cell_height: &self.ts_cell_height, + cell_textcolor: &self.ts_cell_textcolor, + cell_fillcolor: &self.ts_cell_fillcolor, + cell_datatype: &self.ts_cell_datatype, + cell_unittype: &self.ts_cell_unittype, + cell_format: &self.ts_cell_format, + border_lw: &self.ts_border_lw, + border_color: &self.ts_border_color, + border_spacing: &self.ts_border_spacing, + rename_active: self.style_rename.as_deref(), + rename_buf: &self.style_rename_buf, + color_open: self.ts_color_open, + }, flow, ) }, @@ -507,7 +662,9 @@ impl OpenCADStudio { let doc = &tab.scene.document; let mut block_opts: Vec = vec!["None".to_string()]; block_opts.extend(doc.block_records.iter().map(|b| b.name.clone())); - let mut lt_opts: Vec = vec!["None".to_string()]; + let mut arrow_opts: Vec = vec!["Closed filled".to_string()]; + arrow_opts.extend(doc.block_records.iter().map(|b| b.name.clone())); + let mut lt_opts: Vec = vec!["ByBlock".to_string()]; lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone())); let mut textstyle_opts: Vec = vec!["None".to_string()]; textstyle_opts.extend(doc.text_styles.iter().map(|t| t.name.clone())); @@ -529,8 +686,8 @@ impl OpenCADStudio { .iter() .find(|lt| lt.handle == h) .map(|lt| lt.name.clone()) - .unwrap_or_else(|| "None".to_string()), - None => "None".to_string(), + .unwrap_or_else(|| "ByBlock".to_string()), + None => "ByBlock".to_string(), } }; let opt_ts = |h: Option| -> String { @@ -548,16 +705,153 @@ impl OpenCADStudio { match selected_style { Some(s) => ( opt_lt(s.line_type_handle), - opt_block(s.arrowhead_handle), + s.arrowhead_handle + .and_then(|handle| { + doc.block_records + .iter() + .find(|record| record.handle == handle) + .map(|record| record.name.clone()) + }) + .unwrap_or_else(|| "Closed filled".to_string()), opt_ts(s.text_style_handle), opt_block(s.block_content_handle), ), None => Default::default(), }; + let compare_opts: Vec = styles + .iter() + .filter(|name| !name.eq_ignore_ascii_case(&self.mleaderstyle_selected)) + .cloned() + .collect(); + let compare_name = compare_opts + .iter() + .find(|name| name.eq_ignore_ascii_case(&self.mleaderstyle_compare)) + .cloned() + .or_else(|| compare_opts.first().cloned()) + .unwrap_or_default(); + let compare_style = doc.objects.values().find_map(|object| match object { + ObjectType::MultiLeaderStyle(style) if style.name == compare_name => Some(style), + _ => None, + }); + let mut comparison_sections = Vec::new(); + if let (Some(a), Some(b)) = (selected_style, compare_style) { + if ( + &a.description, + a.path_type, + a.line_color, + a.line_type_handle, + a.line_weight, + a.arrowhead_handle, + a.arrowhead_size, + a.break_gap_size, + ) != ( + &b.description, + b.path_type, + b.line_color, + b.line_type_handle, + b.line_weight, + b.arrowhead_handle, + b.arrowhead_size, + b.break_gap_size, + ) { + comparison_sections.push(crate::i18n::translate("Leader Format").into_owned()); + } + if ( + a.enable_landing, + a.enable_dogleg, + a.landing_distance, + a.landing_gap, + a.scale_factor, + a.align_space, + a.max_leader_points, + a.first_segment_angle, + a.second_segment_angle, + a.leader_draw_order, + a.multileader_draw_order, + a.is_annotative, + ) != ( + b.enable_landing, + b.enable_dogleg, + b.landing_distance, + b.landing_gap, + b.scale_factor, + b.align_space, + b.max_leader_points, + b.first_segment_angle, + b.second_segment_angle, + b.leader_draw_order, + b.multileader_draw_order, + b.is_annotative, + ) { + comparison_sections.push(crate::i18n::translate("Leader Structure").into_owned()); + } + if ( + a.content_type, + &a.default_text, + a.text_style_handle, + a.text_height, + a.text_color, + a.text_angle_type, + a.text_alignment, + ) != ( + b.content_type, + &b.default_text, + b.text_style_handle, + b.text_height, + b.text_color, + b.text_angle_type, + b.text_alignment, + ) || ( + a.text_left_attachment, + a.text_right_attachment, + a.text_top_attachment, + a.text_bottom_attachment, + a.text_attachment_direction, + a.text_frame, + a.text_always_left, + ) != ( + b.text_left_attachment, + b.text_right_attachment, + b.text_top_attachment, + b.text_bottom_attachment, + b.text_attachment_direction, + b.text_frame, + b.text_always_left, + ) { + comparison_sections.push(crate::i18n::translate("Content").into_owned()); + } + if ( + a.block_content_handle, + a.block_content_color, + a.block_content_connection, + a.block_content_rotation, + a.block_content_scale_x, + a.block_content_scale_y, + a.block_content_scale_z, + a.enable_block_scale, + a.enable_block_rotation, + ) != ( + b.block_content_handle, + b.block_content_color, + b.block_content_connection, + b.block_content_rotation, + b.block_content_scale_x, + b.block_content_scale_y, + b.block_content_scale_z, + b.enable_block_scale, + b.enable_block_rotation, + ) { + comparison_sections.push(crate::i18n::translate("Block Content").into_owned()); + } + } + let in_use = self.style_in_use( + crate::app::StyleKind::MLeader, + &self.mleaderstyle_selected, + ); sized_flow( ex, - 560, - 560, + 960, + 680, |flow| { crate::ui::style::mleaderstyle::view_window( crate::ui::style::mleaderstyle::MLeaderStyleView { @@ -565,6 +859,11 @@ impl OpenCADStudio { selected: &self.mleaderstyle_selected, style: selected_style, current: tab.active_mleader_style.clone(), + tab: self.mleaderstyle_tab, + compare_name: compare_name.clone(), + compare_opts: compare_opts.clone(), + comparison_sections: comparison_sections.clone(), + in_use, landing_distance: &self.mls_landing_distance, landing_gap: &self.mls_landing_gap, arrowhead_size: &self.mls_arrowhead_size, @@ -585,6 +884,7 @@ impl OpenCADStudio { block_scale_y: &self.mls_block_scale_y, block_scale_z: &self.mls_block_scale_z, block_opts: block_opts.clone(), + arrow_opts: arrow_opts.clone(), lt_opts: lt_opts.clone(), textstyle_opts: textstyle_opts.clone(), line_type_name: line_type_name.clone(), @@ -630,6 +930,13 @@ impl OpenCADStudio { ); let mut lt_opts: Vec = vec!["ByBlock".to_string()]; lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone())); + let text_style_opts: Vec = + doc.text_styles.iter().map(|style| style.name.clone()).collect(); + let text_style_fixed_height = doc + .text_styles + .get(&self.ds_dimtxsty) + .map(|style| style.height) + .filter(|height| *height > 0.0); let blk_name = |h: acadrust::types::Handle| -> String { if h.is_null() { "Default".to_string() @@ -653,6 +960,70 @@ impl OpenCADStudio { } }; let ds_sel = doc.dim_styles.get(&self.dimstyle_selected); + let read_only = ds_sel.is_some_and(|style| { + style.xref_reference || style.xref_dependent || !style.xref_handle.is_null() + }); + let in_use = doc.entities().any(|entity| { + matches!(entity, acadrust::EntityType::Dimension(dimension) + if dimension.base().style_name.eq_ignore_ascii_case(&self.dimstyle_selected)) + }); + let compare_opts: Vec = styles + .iter() + .filter(|name| !name.eq_ignore_ascii_case(&self.dimstyle_selected)) + .cloned() + .collect(); + let compare_name = compare_opts + .iter() + .find(|name| name.eq_ignore_ascii_case(&self.dimstyle_compare)) + .cloned() + .or_else(|| compare_opts.first().cloned()) + .unwrap_or_default(); + let mut comparison_sections = Vec::new(); + if let (Some(a), Some(b)) = (ds_sel, doc.dim_styles.get(&compare_name)) { + if (a.dimdle, a.dimdli, a.dimgap, a.dimclrd, a.dimlwd, a.dimsd1, a.dimsd2) + != (b.dimdle, b.dimdli, b.dimgap, b.dimclrd, b.dimlwd, b.dimsd1, b.dimsd2) + || (a.dimexe, a.dimexo, a.dimclre, a.dimlwe, a.dimse1, a.dimse2, a.dimfxl, a.dimfxlon) + != (b.dimexe, b.dimexo, b.dimclre, b.dimlwe, b.dimse1, b.dimse2, b.dimfxl, b.dimfxlon) + || (a.dimltex_handle, a.dimltex1_handle, a.dimltex2_handle) + != (b.dimltex_handle, b.dimltex1_handle, b.dimltex2_handle) + { + comparison_sections.push(crate::i18n::translate("Lines").into_owned()); + } + if (a.dimasz, a.dimblk, a.dimblk1, a.dimblk2, a.dimldrblk, a.dimsah, a.dimcen, a.dimtsz) + != (b.dimasz, b.dimblk, b.dimblk1, b.dimblk2, b.dimldrblk, b.dimsah, b.dimcen, b.dimtsz) + || (a.dimarcsym, a.dimjogang) != (b.dimarcsym, b.dimjogang) + { + comparison_sections.push(crate::i18n::translate("Symbols and Arrows").into_owned()); + } + if (a.dimclrt, a.dimtxt, &a.dimtxsty, a.dimjust, a.dimtad, a.dimtvp) + != (b.dimclrt, b.dimtxt, &b.dimtxsty, b.dimjust, b.dimtad, b.dimtvp) + || (a.dimtih, a.dimtoh, a.dimtfill, a.dimtfillclr, a.dimtxtdirection) + != (b.dimtih, b.dimtoh, b.dimtfill, b.dimtfillclr, b.dimtxtdirection) + { + comparison_sections.push(crate::i18n::translate("Text").into_owned()); + } + if (a.dimatfit, a.dimtix, a.dimsoxd, a.dimtmove, a.dimupt, a.dimtofl, a.dimscale, a.annotative) + != (b.dimatfit, b.dimtix, b.dimsoxd, b.dimtmove, b.dimupt, b.dimtofl, b.dimscale, b.annotative) + { + comparison_sections.push(crate::i18n::translate("Fit").into_owned()); + } + if (a.dimlfac, a.dimlunit, a.dimdec, &a.dimpost, a.dimdsep, a.dimrnd, a.dimzin, a.dimfrac) + != (b.dimlfac, b.dimlunit, b.dimdec, &b.dimpost, b.dimdsep, b.dimrnd, b.dimzin, b.dimfrac) + || (a.dimaunit, a.dimadec, a.dimazin) != (b.dimaunit, b.dimadec, b.dimazin) + { + comparison_sections.push(crate::i18n::translate("Primary Units").into_owned()); + } + if (a.dimalt, a.dimaltf, a.dimaltd, a.dimaltu, a.dimalttd, a.dimaltrnd, &a.dimapost, a.dimaltz, a.dimalttz) + != (b.dimalt, b.dimaltf, b.dimaltd, b.dimaltu, b.dimalttd, b.dimaltrnd, &b.dimapost, b.dimaltz, b.dimalttz) + { + comparison_sections.push(crate::i18n::translate("Alternate Units").into_owned()); + } + if (a.dimtol, a.dimlim, a.dimtp, a.dimtm, a.dimtdec, a.dimtfac, a.dimtolj, a.dimtzin) + != (b.dimtol, b.dimlim, b.dimtp, b.dimtm, b.dimtdec, b.dimtfac, b.dimtolj, b.dimtzin) + { + comparison_sections.push(crate::i18n::translate("Tolerances").into_owned()); + } + } let ( dimblk_name, dimblk1_name, @@ -673,7 +1044,7 @@ impl OpenCADStudio { ), None => Default::default(), }; - sized_flow(ex, 720, 560, |flow| { + sized_flow(ex, 960, 680, |flow| { crate::ui::style::dimstyle::view_window( styles.clone(), &self.dimstyle_selected, @@ -734,14 +1105,12 @@ impl OpenCADStudio { dimtmove: &self.ds_dimtmove, dimupt: self.ds_dimupt, dimtofl: self.ds_dimtofl, - dimfit: &self.ds_dimfit, dimdsep: &self.ds_dimdsep, dimrnd: &self.ds_dimrnd, dimzin: &self.ds_dimzin, dimfrac: &self.ds_dimfrac, dimaunit: &self.ds_dimaunit, dimadec: &self.ds_dimadec, - dimunit: &self.ds_dimunit, dimazin: &self.ds_dimazin, dimalt: self.ds_dimalt, dimaltf: &self.ds_dimaltf, @@ -763,6 +1132,13 @@ impl OpenCADStudio { dimltex2_name: dimltex2_name.clone(), block_opts: block_opts.clone(), lt_opts: lt_opts.clone(), + text_style_opts: text_style_opts.clone(), + text_style_fixed_height, + compare_name: compare_name.clone(), + compare_opts: compare_opts.clone(), + comparison_sections: comparison_sections.clone(), + read_only, + in_use, color_open: self.ds_color_open.clone(), }, self.style_rename.as_deref(), diff --git a/src/entities/dim_override.rs b/src/entities/dim_override.rs index 9e2102eab..7535f1438 100644 --- a/src/entities/dim_override.rs +++ b/src/entities/dim_override.rs @@ -13,12 +13,79 @@ use acadrust::{CadDocument, Handle}; // DXF group codes of the dimension variables surfaced on the leader panel. pub const DIMSCALE: i16 = 40; // overall scale (real) +pub const DIMPOST: i16 = 3; +pub const DIMAPOST: i16 = 4; pub const DIMASZ: i16 = 41; // arrow size (real) pub const DIMTAD: i16 = 77; // text vertical pos (int16) pub const DIMCLRD: i16 = 176; // dim line colour (int16 = ACI index) pub const DIMGAP: i16 = 147; // text offset / gap (real) pub const DIMLWD: i16 = 371; // dim line lineweight (int16) pub const DIMLDRBLK: i16 = 341; // leader arrow block (handle) +pub const DIMEXO: i16 = 42; +pub const DIMDLI: i16 = 43; +pub const DIMEXE: i16 = 44; +pub const DIMRND: i16 = 45; +pub const DIMDLE: i16 = 46; +pub const DIMTP: i16 = 47; +pub const DIMTM: i16 = 48; +pub const DIMFXL: i16 = 49; +pub const DIMJOGANG: i16 = 50; +pub const DIMTOL: i16 = 71; +pub const DIMLIM: i16 = 72; +pub const DIMTIH: i16 = 73; +pub const DIMTOH: i16 = 74; +pub const DIMSE1: i16 = 75; +pub const DIMSE2: i16 = 76; +pub const DIMZIN: i16 = 78; +pub const DIMAZIN: i16 = 79; +pub const DIMARCSYM: i16 = 90; +pub const DIMTXT: i16 = 140; +pub const DIMCEN: i16 = 141; +pub const DIMTSZ: i16 = 142; +pub const DIMALTF: i16 = 143; +pub const DIMLFAC: i16 = 144; +pub const DIMTVP: i16 = 145; +pub const DIMTFAC: i16 = 146; +pub const DIMALTRND: i16 = 148; +pub const DIMALT: i16 = 170; +pub const DIMALTD: i16 = 171; +pub const DIMTOFL: i16 = 172; +pub const DIMSAH: i16 = 173; +pub const DIMTIX: i16 = 174; +pub const DIMSOXD: i16 = 175; +pub const DIMCLRE: i16 = 177; +pub const DIMCLRT: i16 = 178; +pub const DIMADEC: i16 = 179; +pub const DIMDEC: i16 = 271; +pub const DIMTDEC: i16 = 272; +pub const DIMALTU: i16 = 273; +pub const DIMALTTD: i16 = 274; +pub const DIMAUNIT: i16 = 275; +pub const DIMFRAC: i16 = 276; +pub const DIMLUNIT: i16 = 277; +pub const DIMDSEP: i16 = 278; +pub const DIMTMOVE: i16 = 279; +pub const DIMJUST: i16 = 280; +pub const DIMSD1: i16 = 281; +pub const DIMSD2: i16 = 282; +pub const DIMTOLJ: i16 = 283; +pub const DIMTZIN: i16 = 284; +pub const DIMALTZ: i16 = 285; +pub const DIMALTTZ: i16 = 286; +pub const DIMUPT: i16 = 288; +pub const DIMATFIT: i16 = 289; +pub const DIMFXLON: i16 = 290; +pub const DIMTFILL: i16 = 69; +pub const DIMTFILLCLR: i16 = 70; +pub const DIMTXTDIRECTION: i16 = 295; +pub const DIMTXSTY: i16 = 340; +pub const DIMBLK: i16 = 342; +pub const DIMBLK1: i16 = 343; +pub const DIMBLK2: i16 = 344; +pub const DIMLTYPE: i16 = 345; +pub const DIMLTEX1: i16 = 346; +pub const DIMLTEX2: i16 = 347; +pub const DIMLWE: i16 = 372; /// Every (code, value) override present in the `ACAD`/`DSTYLE` record. pub fn pairs(xd: &ExtendedData) -> Vec<(i16, XDataValue)> { @@ -90,6 +157,16 @@ pub fn handle(xd: &ExtendedData, code: i16) -> Option { }) } +pub fn string(xd: &ExtendedData, code: i16) -> Option { + pairs(xd) + .into_iter() + .find(|(c, _)| *c == code) + .and_then(|(_, value)| match value { + XDataValue::String(text) => Some(text), + _ => None, + }) +} + fn write_pairs(doc: &mut CadDocument, handle: Handle, pairs: Vec<(i16, XDataValue)>) { let Some(entity) = doc.get_entity(handle) else { return; diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index d87ecfebd..e8600dea4 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -1389,27 +1389,6 @@ fn apply_dimension_breaks( *lines = output; } -pub(crate) fn uses_custom_arrow_blocks(document: &CadDocument, dim: &Dimension) -> bool { - let style_name = &dim.base().style_name; - let Some(style) = document.dim_styles.iter().find(|style| { - style.name.eq_ignore_ascii_case(style_name) - || (style_name.trim().is_empty() && style.name.eq_ignore_ascii_case("Standard")) - }) else { - return false; - }; - if style.dimtsz > 1e-9 { - return false; - } - let is_custom = |handle| { - crate::scene::convert::tessellate::arrow_block_is_custom(document, handle) - }; - if style.dimsah { - is_custom(style.dimblk1) || is_custom(style.dimblk2) - } else { - is_custom(style.dimblk) - } -} - pub trait DimensionTess { fn tessellate( &self, @@ -1481,10 +1460,126 @@ fn tessellate_dimension_inner( // recursive call goes through the LOD ladder, not the truck path.) let style_name = &dim.base().style_name; - let style = document.dim_styles.iter().find(|s| { + let source_style = document.dim_styles.iter().find(|s| { s.name.eq_ignore_ascii_case(style_name) || (style_name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard")) }); + let mut effective_style = source_style.cloned(); + if let Some(style) = &mut effective_style { + use crate::entities::dim_override as ov; + let data = &dim.base().common.extended_data; + macro_rules! real { + ($field:ident, $code:ident) => { + if let Some(value) = ov::real(data, ov::$code) { + style.$field = value; + } + }; + } + macro_rules! int { + ($field:ident, $code:ident) => { + if let Some(value) = ov::int(data, ov::$code) { + style.$field = value; + } + }; + } + macro_rules! flag { + ($field:ident, $code:ident) => { + if let Some(value) = ov::int(data, ov::$code) { + style.$field = value != 0; + } + }; + } + macro_rules! handle { + ($field:ident, $code:ident) => { + if let Some(value) = ov::handle(data, ov::$code) { + style.$field = value; + } + }; + } + real!(dimscale, DIMSCALE); + real!(dimasz, DIMASZ); + real!(dimexo, DIMEXO); + real!(dimdli, DIMDLI); + real!(dimexe, DIMEXE); + real!(dimrnd, DIMRND); + real!(dimdle, DIMDLE); + real!(dimtp, DIMTP); + real!(dimtm, DIMTM); + real!(dimfxl, DIMFXL); + real!(dimjogang, DIMJOGANG); + real!(dimtxt, DIMTXT); + real!(dimcen, DIMCEN); + real!(dimtsz, DIMTSZ); + real!(dimaltf, DIMALTF); + real!(dimlfac, DIMLFAC); + real!(dimtvp, DIMTVP); + real!(dimtfac, DIMTFAC); + real!(dimgap, DIMGAP); + real!(dimaltrnd, DIMALTRND); + flag!(dimtol, DIMTOL); + flag!(dimlim, DIMLIM); + flag!(dimtih, DIMTIH); + flag!(dimtoh, DIMTOH); + flag!(dimse1, DIMSE1); + flag!(dimse2, DIMSE2); + flag!(dimalt, DIMALT); + flag!(dimtofl, DIMTOFL); + flag!(dimsah, DIMSAH); + flag!(dimtix, DIMTIX); + flag!(dimsoxd, DIMSOXD); + flag!(dimsd1, DIMSD1); + flag!(dimsd2, DIMSD2); + flag!(dimupt, DIMUPT); + flag!(dimfxlon, DIMFXLON); + flag!(dimtxtdirection, DIMTXTDIRECTION); + int!(dimzin, DIMZIN); + int!(dimazin, DIMAZIN); + int!(dimarcsym, DIMARCSYM); + int!(dimclrd, DIMCLRD); + int!(dimclre, DIMCLRE); + int!(dimclrt, DIMCLRT); + int!(dimadec, DIMADEC); + int!(dimaltd, DIMALTD); + int!(dimdec, DIMDEC); + int!(dimtdec, DIMTDEC); + int!(dimaltu, DIMALTU); + int!(dimalttd, DIMALTTD); + int!(dimaunit, DIMAUNIT); + int!(dimfrac, DIMFRAC); + int!(dimlunit, DIMLUNIT); + int!(dimdsep, DIMDSEP); + int!(dimtmove, DIMTMOVE); + int!(dimjust, DIMJUST); + int!(dimtolj, DIMTOLJ); + int!(dimtzin, DIMTZIN); + int!(dimaltz, DIMALTZ); + int!(dimalttz, DIMALTTZ); + int!(dimatfit, DIMATFIT); + int!(dimtfill, DIMTFILL); + int!(dimtfillclr, DIMTFILLCLR); + int!(dimlwd, DIMLWD); + int!(dimlwe, DIMLWE); + handle!(dimldrblk, DIMLDRBLK); + handle!(dimblk, DIMBLK); + handle!(dimblk1, DIMBLK1); + handle!(dimblk2, DIMBLK2); + handle!(dimltex_handle, DIMLTYPE); + handle!(dimltex1_handle, DIMLTEX1); + handle!(dimltex2_handle, DIMLTEX2); + if let Some(value) = ov::string(data, ov::DIMPOST) { + style.dimpost = value; + } + if let Some(value) = ov::string(data, ov::DIMAPOST) { + style.dimapost = value; + } + if let Some(value) = ov::handle(data, ov::DIMTXSTY) { + style.dimtxsty_handle = value; + if let Some(record) = document.text_styles.iter().find(|record| record.handle == value) { + style.dimtxsty = record.name.clone(); + } + } + } + let style = effective_style.as_ref(); // DIMSCALE rule: // dimstyle.dimscale > 0 → final multiplier; ignore anno_scale. @@ -1655,7 +1750,7 @@ fn tessellate_dimension_inner( // preserved on save: // - base.insertion_point: legacy anchor reference; render uses // text_middle_point + dim-line geometry instead. - // - base.block_name: AutoCAD-style "*D..." anonymous block name for + // - base.block_name: generated anonymous block name for // the dim graphics — we re-tessellate so don't need it. // - base.version: DXF format marker (metadata only). let _ = ( @@ -2391,7 +2486,7 @@ fn append_linear_dimension( // DIMDLE: dim line overshoots the ext line by `dimdle` at each end, // but only when ticks are in use (DIMTSZ > 0). With arrowheads this - // is ignored, matching AutoCAD. + // is ignored. let dle = if params.ticks { params.dimdle } else { 0.0 }; let dir_d1_to_d2 = normalized_or(d2 - d1, axis); let d1_out = d1 - dir_d1_to_d2 * dle; @@ -2404,30 +2499,50 @@ fn append_linear_dimension( let gap = (d2 - d1).length(); let arrows_outside = !params.ticks && params.arrow_len > 1e-6 && gap < 2.0 * params.arrow_len; - // DIMSD1/DIMSD2: when *both* set, omit the dim line entirely. AutoCAD - // splits at text otherwise — without that pivot info, leave as-is. - if !(suppress.dim1 && suppress.dim2) { - // Break the dim line where the text sits on it, so a DIMTFILL - // background reads over the line (2-D fills draw under all wires, so the - // line is gapped rather than z-masked). - let mut drew = false; - if let Some((tc, half_w, half_h)) = params.text_break { - let dir = normalized_or(d2_out - d1_out, axis); - let len = (d2_out - d1_out).length(); - let along = (tc - d1_out).dot(dir); - let perp_dist = (tc - (d1_out + dir * along)).length(); - if perp_dist < half_h && along - half_w > 0.0 && along + half_w < len { - add_segment(&mut g.dim_lines, d1_out, d1_out + dir * (along - half_w)); - add_segment(&mut g.dim_lines, d1_out + dir * (along + half_w), d2_out); - drew = true; + // The two suppression flags address the portions at the first and second + // measured points independently. The text gap is also the natural split; + // when there is no gap, split at the projected text position or midpoint. + let line_dir = normalized_or(d2_out - d1_out, axis); + let line_len = (d2_out - d1_out).length(); + let mut split = line_len * 0.5; + let mut left_end = split; + let mut right_start = split; + if let Some((text_center, half_width, half_height)) = params.text_break { + let along = (text_center - d1_out).dot(line_dir); + if along > 0.0 && along < line_len { + split = along; + left_end = split; + right_start = split; + let perpendicular = (text_center - (d1_out + line_dir * along)).length(); + if perpendicular < half_height + && along - half_width > 0.0 + && along + half_width < line_len + { + left_end = along - half_width; + right_start = along + half_width; } } - if !drew { - add_segment(&mut g.dim_lines, d1_out, d2_out); - } - if arrows_outside && !params.dimsoxd { - let stub = params.arrow_len * 2.0; + } + if !suppress.dim1 && left_end > 1e-6 { + add_segment( + &mut g.dim_lines, + d1_out, + d1_out + line_dir * left_end, + ); + } + if !suppress.dim2 && line_len - right_start > 1e-6 { + add_segment( + &mut g.dim_lines, + d1_out + line_dir * right_start, + d2_out, + ); + } + if arrows_outside && !params.dimsoxd { + let stub = params.arrow_len * 2.0; + if !suppress.dim1 { add_segment(&mut g.dim_lines, d1 - dir_d1_to_d2 * stub, d1); + } + if !suppress.dim2 { add_segment(&mut g.dim_lines, d2, d2 + dir_d1_to_d2 * stub); } } @@ -2838,8 +2953,8 @@ fn dimension_text_parts( let base = dim.base(); let is_angular = matches!(dim, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)); - // Auto-generated body (the value AutoCAD would emit if the user did not - // override it). Built first so user_text "<>" substitution can re-use it. + // Auto-generated body used when the user did not override it. Built first + // so user_text "<>" substitution can re-use it. let primary_raw = if is_angular { format_angular_value(dim.measurement(), style) } else { @@ -3168,7 +3283,7 @@ fn format_fractional(value: f64, dimfrac: i16) -> String { } /// Power-of-two denominator for architectural / fractional output. DIMFRAC is -/// the exponent-ish knob AutoCAD pairs with DIMLUNIT; clamp it to a readable +/// the exponent-like fraction-format knob; clamp it to a readable /// range (16ths … 1024ths). fn arch_denom(dimfrac: i16) -> u64 { let exp = (dimfrac.clamp(0, 8) as u32).max(2) + 2; // 4..=10 → 16..=1024 diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index e87bdfba0..75a8799e4 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -2982,6 +2982,188 @@ pub(super) fn message_id(source: &str) -> Option<&'static str> { "── Custom Overrides (SHORTCUTS SET) ──────────────────" => Some("catalog-custom-overrides-shortcuts-set-07be563dec"), "── Function Keys ──────────────────────────────────────" => Some("catalog-function-keys-b5a2df24e9"), "── Other Keys ──────────────────────────────────────────" => Some("catalog-other-keys-a882812cf2"), + "Always keep text between extension lines" => Some("dimstyle-keep-text-between-extension-lines"), + "Angular dimensions" => Some("dimstyle-angular-dimensions"), + "Arc length symbol" => Some("dimstyle-arc-length-symbol"), + "Arrowheads" => Some("dimstyle-arrowheads"), + "Available style" => Some("dimstyle-available-style"), + "Baseline spacing" => Some("dimstyle-baseline-spacing"), + "Basic" => Some("dimstyle-basic"), + "Both arrowheads" => Some("dimstyle-both-arrowheads"), + "Center mark" => Some("dimstyle-center-mark"), + "Center mark size" => Some("dimstyle-center-mark-size"), + "Center marks" => Some("dimstyle-center-marks"), + "Centerlines" => Some("dimstyle-centerlines"), + "Compare with" => Some("dimstyle-compare-with"), + "Current style" => Some("dimstyle-current-style"), + "Deviation" => Some("dimstyle-deviation"), + "Different sections" => Some("dimstyle-different-sections"), + "Dimension line color" => Some("dimstyle-dimension-line-color"), + "Dimension linetype" => Some("dimstyle-dimension-linetype"), + "Dimension lineweight" => Some("dimstyle-dimension-lineweight"), + "Display alternate units" => Some("dimstyle-display-alternate-units"), + "Draw dimension line between extension lines" => Some("dimstyle-draw-dimension-line-between-extension-lines"), + "Extend beyond dimension line" => Some("dimstyle-extend-beyond-dimension-line"), + "Extend beyond ticks" => Some("dimstyle-extend-beyond-ticks"), + "Extension line color" => Some("dimstyle-extension-line-color"), + "Extension lineweight" => Some("dimstyle-extension-lineweight"), + "Feet and inches" => Some("dimstyle-feet-and-inches"), + "Fine tuning" => Some("dimstyle-fine-tuning"), + "First arrowhead" => Some("dimstyle-first-arrowhead"), + "First extension linetype" => Some("dimstyle-first-extension-linetype"), + "Fit options" => Some("dimstyle-fit-options"), + "Fixed length" => Some("dimstyle-fixed-length"), + "Fraction format" => Some("dimstyle-fraction-format"), + "Height scale" => Some("dimstyle-height-scale"), + "Horizontal placement" => Some("dimstyle-horizontal-placement"), + "Keep text horizontal when inside" => Some("dimstyle-keep-text-horizontal-inside"), + "Keep text horizontal when outside" => Some("dimstyle-keep-text-horizontal-outside"), + "Leader arrowhead" => Some("dimstyle-leader-arrowhead"), + "Limits" => Some("dimstyle-limits"), + "Linear dimensions" => Some("dimstyle-linear-dimensions"), + "Lower value" => Some("dimstyle-lower-value"), + "Measurement scale" => Some("dimstyle-measurement-scale"), + "Measurement template" => Some("dimstyle-measurement-template"), + "Method" => Some("dimstyle-method"), + "Multiplier" => Some("dimstyle-multiplier"), + "No differences" => Some("dimstyle-no-differences"), + "Not applicable" => Some("dimstyle-not-applicable"), + "Offset from measured point" => Some("dimstyle-offset-from-measured-point"), + "Place text manually while creating dimensions" => Some("dimstyle-place-text-manually"), + "Radius jog angle (°)" => Some("dimstyle-radius-jog-angle"), + "Read left to right" => Some("dimstyle-read-left-to-right"), + "Referenced style · Read only" => Some("dimstyle-referenced-read-only"), + "Round off" => Some("dimstyle-round-off"), + "Second arrowhead" => Some("dimstyle-second-arrowhead"), + "Second extension linetype" => Some("dimstyle-second-extension-linetype"), + "Show zero feet and zero inches" => Some("dimstyle-show-zero-feet-and-inches"), + "Show zero feet; suppress zero inches" => Some("dimstyle-show-zero-feet-suppress-zero-inches"), + "Suppress arrows when they do not fit" => Some("dimstyle-suppress-arrows-when-no-fit"), + "Suppress first dimension line" => Some("dimstyle-suppress-first-dimension-line"), + "Suppress first extension line" => Some("dimstyle-suppress-first-extension-line"), + "Suppress second dimension line" => Some("dimstyle-suppress-second-dimension-line"), + "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"), + "Symbols and Arrows" => Some("dimstyle-symbols-and-arrows"), + "Symmetrical" => Some("dimstyle-symmetrical"), + "Text background" => Some("dimstyle-text-background"), + "Text gap" => Some("dimstyle-text-gap"), + "The selected text style fixes the height at %{height}." => Some("dimstyle-fixed-text-height-note"), + "Tick size" => Some("dimstyle-tick-size"), + "Tolerance zero suppression" => Some("dimstyle-tolerance-zero-suppression"), + "Unit format" => Some("dimstyle-unit-format"), + "Upper value" => Some("dimstyle-upper-value"), + "Use <> as the measured value; text before and after it becomes the prefix and suffix." => Some("dimstyle-measurement-template-help"), + "Use different first and second arrowheads" => Some("dimstyle-use-different-arrowheads"), + "Use fixed-length extension lines" => Some("dimstyle-use-fixed-length-extension-lines"), + "Used in drawing" => Some("dimstyle-used-in-drawing"), + "Vertical offset" => Some("dimstyle-vertical-offset"), + "Vertical placement" => Some("dimstyle-vertical-placement"), + "Vertical position" => Some("dimstyle-vertical-position"), + "When space is limited" => Some("dimstyle-when-space-is-limited"), + "When text is moved" => Some("dimstyle-when-text-is-moved"), + "Zero suppression" => Some("dimstyle-zero-suppression"), + "Add a leader" => Some("dimstyle-add-a-leader"), + "Automatic" => Some("dimstyle-automatic"), + "By block" => Some("dimstyle-by-block"), + "Comma" => Some("dimstyle-comma"), + "Degrees, minutes, seconds" => Some("dimstyle-degrees-minutes-seconds"), + "Keep dimension line with text" => Some("dimstyle-keep-dimension-line-with-text"), + "Leading and trailing" => Some("dimstyle-leading-and-trailing"), + "Move arrows outside first" => Some("dimstyle-move-arrows-outside-first"), + "Move text and arrows outside" => Some("dimstyle-move-text-and-arrows-outside"), + "Move text freely" => Some("dimstyle-move-text-freely"), + "Move text outside first" => Some("dimstyle-move-text-outside-first"), + "Period" => Some("dimstyle-period"), + "Space" => Some("dimstyle-space"), + "Alignment spacing" => Some("style-manager-alignment-spacing"), + "Always left align" => Some("style-manager-always-left-align"), + "Attachment direction" => Some("style-manager-attachment-direction"), + "Background fill enabled" => Some("style-manager-background-fill-enabled"), + "Big font" => Some("style-manager-big-font"), + "Block scale X" => Some("style-manager-block-scale-x"), + "Block scale Y" => Some("style-manager-block-scale-y"), + "Block scale Z" => Some("style-manager-block-scale-z"), + "Borders" => Some("style-manager-borders"), + "Bottom attachment" => Some("style-manager-bottom-attachment"), + "Break gap size" => Some("style-manager-break-gap-size"), + "Caps and Fill" => Some("style-manager-caps-and-fill"), + "Content type" => Some("style-manager-content-type"), + "Data Row" => Some("style-manager-data-row"), + "Data type" => Some("style-manager-data-type"), + "Default text" => Some("style-manager-default-text"), + "Different" => Some("style-manager-different"), + "Display joints" => Some("style-manager-display-joints"), + "Drawing height" => Some("style-manager-drawing-height"), + "Drawing scale" => Some("style-manager-drawing-scale"), + "Effects" => Some("style-manager-effects"), + "Elements" => Some("style-manager-elements"), + "End caps" => Some("style-manager-end-caps"), + "Fill and joints" => Some("style-manager-fill-and-joints"), + "Fill enabled" => Some("style-manager-fill-enabled"), + "First segment angle" => Some("style-manager-first-segment-angle"), + "Fixed height" => Some("style-manager-fixed-height"), + "Flow direction" => Some("style-manager-flow-direction"), + "Font file" => Some("style-manager-font-file"), + "Fonts" => Some("style-manager-fonts"), + "Format string" => Some("style-manager-format-string"), + "Header" => Some("style-manager-header"), + "Header Row" => Some("style-manager-header-row"), + "Horizontal margin" => Some("style-manager-horizontal-margin"), + "Inner arcs" => Some("style-manager-inner-arcs"), + "Inside horizontal" => Some("style-manager-inside-horizontal"), + "Inside vertical" => Some("style-manager-inside-vertical"), + "Leader draw order" => Some("style-manager-leader-draw-order"), + "Leader note" => Some("style-manager-leader-note"), + "Left attachment" => Some("style-manager-left-attachment"), + "Line color" => Some("style-manager-line-color"), + "Line type" => Some("style-manager-line-type"), + "Line weight" => Some("style-manager-line-weight"), + "Maximum leader points" => Some("style-manager-maximum-leader-points"), + "Multileader draw order" => Some("style-manager-multileader-draw-order"), + "No preview" => Some("style-manager-no-preview"), + "Oblique angle" => Some("style-manager-oblique-angle"), + "Outer arc" => Some("style-manager-outer-arc"), + "Paper height" => Some("style-manager-paper-height"), + "Paper text height" => Some("style-manager-paper-text-height"), + "Path type" => Some("style-manager-path-type"), + "Right attachment" => Some("style-manager-right-attachment"), + "Scale factor" => Some("style-manager-scale-factor"), + "Second segment angle" => Some("style-manager-second-segment-angle"), + "Size and Effects" => Some("style-manager-size-and-effects"), + "Start caps" => Some("style-manager-start-caps"), + "Stroke fonts" => Some("style-manager-stroke-fonts"), + "Suppress header row" => Some("style-manager-suppress-header-row"), + "Suppress title row" => Some("style-manager-suppress-title-row"), + "System font" => Some("style-manager-system-font"), + "System fonts" => Some("style-manager-system-fonts"), + "Text alignment" => Some("style-manager-text-alignment"), + "Text angle" => Some("style-manager-text-angle"), + "Title Row" => Some("style-manager-title-row"), + "Top attachment" => Some("style-manager-top-attachment"), + "Unit type" => Some("style-manager-unit-type"), + "Vertical margin" => Some("style-manager-vertical-margin"), + "font family…" => Some("style-manager-font-family-placeholder"), + "Bottom to top" => Some("style-manager-bottom-to-top"), + "Arrowhead first" => Some("style-manager-arrowhead-first"), + "Content first" => Some("style-manager-content-first"), + "Leader first" => Some("style-manager-leader-first"), + "Straight" => Some("style-manager-straight"), + "Parallel to last segment" => Some("style-manager-parallel-last-segment"), + "Top of top line" => Some("style-manager-top-of-top-line"), + "Middle of top line" => Some("style-manager-middle-of-top-line"), + "Middle of text" => Some("style-manager-middle-of-text"), + "Middle of bottom line" => Some("style-manager-middle-of-bottom-line"), + "Bottom of bottom line" => Some("style-manager-bottom-of-bottom-line"), + "Bottom line" => Some("style-manager-bottom-line"), + "Underline bottom line" => Some("style-manager-underline-bottom-line"), + "Underline top line" => Some("style-manager-underline-top-line"), + "Underline all text" => Some("style-manager-underline-all-text"), + "Center of text" => Some("style-manager-center-of-text"), + "Center of text with overline" => Some("style-manager-center-of-text-overline"), + "Block extents" => Some("style-manager-block-extents"), + "Base point" => Some("style-manager-base-point"), _ => None, } } diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 0f13f5130..7685ecad1 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -28,7 +28,7 @@ const MAX_NESTING_DEPTH: usize = 32; /// Skip wires whose world-AABB projects to fewer than this many pixels in /// the active view. Picks up tiny detail at zoom-out so the tessellator /// doesn't waste time on geometry that contributes a few sub-pixel marks -/// to the final image. 2 px is the AutoCAD-default "small element" floor +/// to the final image. Two pixels is a practical small-element floor: /// — visibly the same image, dramatically fewer wires. const MIN_PIXEL_SIZE: f32 = 2.0; @@ -426,66 +426,16 @@ fn build_defn( nested_ins, doc, bg_color, depth_map, ))); } - // A dimension nested in a block bakes its geometry (extension / - // dim lines, arrows, text) into a per-instance `*D` block, exactly - // like a top-level dimension. The top-level path expands that block - // in `tessellate_entity`; the block-expand path calls the plain - // `tessellate::tessellate`, which does NOT, so nested dimensions - // drew nothing. Expand the `*D` block's entities here as block-local - // subs so they transform with the parent insert. (Empty block_name - // — a non-baked dimension — falls through to the default arm.) - EntityType::Dimension(dim) - if !dim.base().block_name.trim().is_empty() - && !crate::entities::dimension::uses_custom_arrow_blocks(doc, dim) => - { - let dblk = doc - .block_records - .iter() - .find(|br| br.name.eq_ignore_ascii_case(&dim.base().block_name)); - if let Some(dblk) = dblk { - // The `*D` block content is baked in the coordinate space it - // occupied when the dimension was created — for a dimension - // inside a block that is often the ORIGINAL WCS, not the - // block-local space. The dimension's insertion_point (DXF - // 12) is the offset that maps the baked content into the - // dimension's own (block-local) space; it is zero for - // dimensions baked in place, so this is a no-op there. - let ins = dim.base().insertion_point; - for &deh in &dblk.entity_handles { - let Some(dsub) = doc.get_entity(deh) else { - continue; - }; - // Definition points are baked as POINTs on Defpoints — - // grip markers, never drawn (matches the top-level path). - if matches!(dsub, EntityType::Point(_)) { - continue; - } - if dsub.common().invisible || layer_hidden(doc, &dsub.common().layer) { - continue; - } - let mut placed = dsub.clone(); - placed.as_entity_mut().translate(ins); - // An arrowhead is baked as a nested INSERT of an arrow - // block — route it through the nested-ref machinery so - // the arrow block expands (tessellate_sub_local doesn't - // expand inserts). - if let EntityType::Insert(arrow) = &placed { - subs.push(LocalSub::Nested(build_nested_ref( - arrow, doc, bg_color, depth_map, - ))); - } else { - for lw in tessellate_sub_local( - doc, - &placed, - anno_scale, - annotation_scale_handle, - bg_color, - depth_map, - ) { - subs.push(LocalSub::Wire(lw)); - } - } - } + EntityType::Dimension(_) => { + for wire in tessellate_sub_local( + doc, + entity, + anno_scale, + annotation_scale_handle, + bg_color, + depth_map, + ) { + subs.push(LocalSub::Wire(wire)); } } // A table nested in a block bakes its geometry (gridlines, cell @@ -706,21 +656,38 @@ fn tessellate_sub_local( // Pass `local_offset` as the f64 world-offset so tessellate subtracts it // before casting to f32 — same precision-preservation trick used for // top-level entities, applied per-defn. - let wires_out = tessellate::tessellate( - doc, - h, - sub, - false, - sub_color, - pat_len, - pat, - lw_px, - anno_scale, - annotation_scale_handle, - None, - bg_color, - false, - ); + let wires_out = if let EntityType::Dimension(dimension) = sub { + use crate::entities::dimension::DimensionTess; + dimension.tessellate( + doc, + h, + false, + sub_color, + lw_px, + anno_scale, + &HashSet::default(), + None, + bg_color, + None, + None, + ) + } else { + tessellate::tessellate( + doc, + h, + sub, + false, + sub_color, + pat_len, + pat, + lw_px, + anno_scale, + annotation_scale_handle, + None, + bg_color, + false, + ) + }; if wires_out.is_empty() { return vec![]; } diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 1b0bcd233..67f092099 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -543,8 +543,8 @@ pub(crate) fn tessellate_entity( // An entity from an application we have no reader for (e.g. an Autodesk // Raster Design embedded raster image) arrives as `Unknown`. Its own data is // a private format we cannot decode — but it usually ships a proxy-graphics - // blob, the vector preview its author cached for exactly this case. AutoCAD - // draws that when the object enabler is missing; draw it too, so the entity + // blob, the vector preview its author cached for exactly this case. Draw it + // when the object enabler is missing, so the entity // occupies its real place instead of silently disappearing. let proxy_blob: Option> = match e { EntityType::Unknown(_) => e @@ -665,7 +665,7 @@ pub(crate) fn tessellate_entity( // // The DWG reader decodes the two cut-line endpoints, the signed end ticks // and the identifier. Synthesize the end ticks + arrowheads + label glyphs - // so the mark is visible on the layout, the way AutoCAD draws it. The raw + // so the mark is visible on the layout. The raw // record is still preserved for lossless write-back. if let EntityType::SectionSymbol(s) = e { return section_symbol_wires( @@ -736,118 +736,6 @@ pub(crate) fn tessellate_entity( }]; } - // ── Dimension baked-block fast path ───────────────────────────────────── - // - // A saved dimension may carry final geometry (extension lines, dim line, - // arrows and text) in a per-instance block — usually - // `*D`, but custom names like `DIMBLOCK###-4NP` also occur. When the - // block exists we render its contents through `tessellate_entity` so - // sub-Text/MText get the standard baseline/greek/full LOD ladder, and - // DIMTXT × DIMSCALE isn't re-applied on already-baked geometry. - if let EntityType::Dimension(dim) = e { - let block_name = &dim.base().block_name; - if !block_name.trim().is_empty() - && !crate::entities::dimension::uses_custom_arrow_blocks(document, dim) - { - if let Some(br) = document - .block_records - .iter() - .find(|br| br.name.eq_ignore_ascii_case(block_name)) - { - if !br.entity_handles.is_empty() { - let mut wires: Vec = Vec::with_capacity(br.entity_handles.len()); - // The Dimension's own layer style — layer-0 inheritance - // target for baked sub-entities on layer "0" (#221). - let dim_l0_color = view::render::adapt_to_bg( - view::render::layer_render_style(document, &e.common().layer).color, - bg_color, - ); - let dim_l0_aci = document - .layers - .get(&e.common().layer) - .map(|l| match &l.color { - acadrust::types::Color::Index(i) => *i, - _ => 0, - }) - .unwrap_or(0); - for &eh in &br.entity_handles { - let Some(sub) = document.get_entity(eh) else { - continue; - }; - // A dimension's definition points are baked into the - // block as POINTs on the Defpoints layer. They are grip - // markers rather than PDMODE display geometry, so - // geometry — so rendering them adds a stray tick at each - // measured point that makes the extension lines look like - // they run past the geometry. Skip them. - if matches!(sub, EntityType::Point(_)) { - continue; - } - // Sub-entities inside *D### / DIMBLOCK## blocks - // typically use ByBlock color/linetype/lineweight — - // they should inherit from the Dimension entity. - let has_book_color = - view::render::has_resolved_book_color(document, sub); - let sub_color_is_byblock = !has_book_color - && sub.common().color == acadrust::types::Color::ByBlock; - let sub_is_l0_bylayer = - !has_book_color - && view::render::is_effective_layer_zero(&sub.common().layer) - && sub.common().color == acadrust::types::Color::ByLayer; - let sub_wires = tessellate_entity( - document, - selected, - active_viewport, - bg_color, - // Block contents are baked at the final WCS size — - // don't let downstream paths re-apply anno_scale. - 1.0, - None, - sub, - block_cache, - view_aabb, - world_per_pixel, - paper_space, - ); - for mut w in sub_wires { - w.name = h.value().to_string(); - // Override ByBlock colour with the dim's resolved - // colour so text matches `DIMCLRT`-style behaviour - // (or layer colour) instead of the raw ByBlock - // fallback that render_style_for produces. A layer-0 - // sub inherits the dim's layer colour instead. - if sub_color_is_byblock { - w.color = if sel { - WireModel::SELECTED - } else { - entity_color - }; - w.aci = aci; - } else if sub_is_l0_bylayer && !sel { - w.color = dim_l0_color; - w.aci = dim_l0_aci; - } - wires.push(w); - } - } - if !wires.is_empty() { - let aabb = entity_aabb(e); - for w in &mut wires { - // Empty SDF-text cells keep their tight glyph-box - // AABB; only stroke/fill wires take the whole-block - // box as a broad-phase pick hint. - if !w.points.is_empty() || !w.fill_tris.is_empty() { - set_wire_aabb(w, aabb); - } - } - return wires; - } - } - } - } - // Fall through to the synthesis path below when no block is attached. - } - if let EntityType::Dimension(dim) = e { let aabb = entity_aabb(e); use crate::entities::dimension::DimensionTess; @@ -904,7 +792,7 @@ pub(crate) fn tessellate_entity( // ── Table baked-block fast path ───────────────────────────────────────── // - // AutoCAD bakes a Table's final rendered geometry (cell text, gridlines, + // A table may store final rendered geometry (cell text, gridlines, // fill) into a per-instance block (usually `*T###`) referenced through // `table.block_record_handle`. The block's text uses the *displayed* // height; synthesising cells from `self.rows + TableStyle` instead would diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 93e5d2146..a7716cfed 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -1538,28 +1538,6 @@ pub(crate) fn arrow_from_block( .unwrap_or_else(|| arrow_from_block_name(None, dimasz)) } -pub(crate) fn arrow_block_is_custom( - doc: &CadDocument, - handle: acadrust::types::Handle, -) -> bool { - if handle.is_null() { - return false; - } - doc.block_records - .iter() - .find(|record| record.handle == handle) - .is_some_and(|record| { - !record.entity_handles.is_empty() - && !record.is_layout() - && !record.is_model_space() - && !record.is_paper_space() - && !record.flags.is_xref - && !record.flags.is_xref_overlay - && !record.flags.is_external - && builtin_arrow_from_block_name(&record.name, 1.0).is_none() - }) -} - fn arrow_from_block_name(name: Option<&str>, dimasz: f32) -> ArrowKind { name.and_then(|name| builtin_arrow_from_block_name(name, dimasz)) .unwrap_or(ArrowKind::Triangle { @@ -1742,6 +1720,10 @@ fn collect_custom_arrow_entity( stack.pop(); return; } + EntityType::Hatch(hatch) => { + append_custom_hatch_fill(hatch, root_base, fill); + return; + } _ => {} } @@ -1766,6 +1748,69 @@ fn collect_custom_arrow_entity( } } +fn append_custom_hatch_fill( + hatch: &acadrust::entities::Hatch, + base: acadrust::types::Vector3, + out: &mut Vec<[f32; 3]>, +) { + use lyon_tessellation::math::point; + use lyon_tessellation::path::Path; + use lyon_tessellation::{ + BuffersBuilder, FillOptions, FillRule, FillTessellator, FillVertex, VertexBuffers, + }; + + let Some(model) = crate::scene::Scene::hatch_model_from_dxf(hatch, [1.0; 4]) else { + return; + }; + + let mut builder = Path::builder(); + let mut ring: Vec<[f32; 2]> = Vec::new(); + let mut finish_ring = |ring: &mut Vec<[f32; 2]>| { + if ring.len() >= 3 { + builder.begin(point(ring[0][0], ring[0][1])); + for p in &ring[1..] { + builder.line_to(point(p[0], p[1])); + } + builder.end(true); + } + ring.clear(); + }; + for &[x, y] in model.boundary.iter() { + if x.is_nan() || y.is_nan() { + finish_ring(&mut ring); + continue; + } + ring.push([ + (model.world_origin[0] + x as f64 - base.x) as f32, + (model.world_origin[1] + y as f64 - base.y) as f32, + ]); + } + finish_ring(&mut ring); + + let mut geometry: VertexBuffers<[f32; 2], u32> = VertexBuffers::new(); + let mut tessellator = FillTessellator::new(); + if tessellator + .tessellate_path( + &builder.build(), + &FillOptions::default().with_fill_rule(FillRule::EvenOdd), + &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| { + vertex.position().to_array() + }), + ) + .is_err() + { + return; + } + let z = -base.z as f32; + out.extend( + geometry + .indices + .iter() + .filter_map(|&index| geometry.vertices.get(index as usize)) + .map(|&[x, y]| [x, y, z]), + ); +} + fn append_custom_wire_points( points: &[[f32; 3]], points_low: &[[f32; 3]], @@ -1928,7 +1973,7 @@ fn fallback_geometry(entity: &EntityType) -> Geometry { /// Extract pre-computed edge-wire points from Solid3D / Region / Body entities. /// -/// AutoCAD stores explicit wire geometry (from SOLVIEW / 3DPLOT) alongside the +/// Some drawings store explicit wire geometry alongside the /// ACIS data. We use this as a visible fallback when the SAT tessellator /// produces no mesh (e.g. binary SAB data or unsupported geometry). fn solid_wire_fallback(entity: &EntityType) -> Vec<[f64; 3]> { @@ -2117,7 +2162,7 @@ pub(crate) fn add_polyline(points: &mut Vec<[f32; 3]>, polyline: &[Vec3]) { /// Returns the text position of a dimension in DXF world-space (f64, no offset applied). /// Used when building a synthetic Text entity so tessellate() can apply world_offset itself. -/// When the saved `text_middle_point` is zero (i.e. AutoCAD never wrote one), +/// When the saved `text_middle_point` is zero (no explicit point was written), /// computes a fallback from the dim geometry and applies DIMTAD/DIMGAP. pub(crate) fn normalized_or(v: Vec3, fallback: Vec3) -> Vec3 { if v.length_squared() <= 1e-12 { diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 0ec12b360..ee660c531 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1791,7 +1791,7 @@ impl Scene { } } - pub(super) fn hatch_model_from_dxf( + pub(crate) fn hatch_model_from_dxf( dxf: &DxfHatch, color: [f32; 4], ) -> Option { @@ -1814,9 +1814,9 @@ impl Scene { for path in &dxf.paths { // Skip TEXTBOX boundary paths (flag bit 3). These are text - // bounding-boxes AutoCAD derives for island detection; they are + // derived bounding boxes used for island detection; they are // never drawn or filled. Treating one as a fill boundary paints its - // rectangle solid — a phantom bar AutoCAD never shows. + // rectangle solid and creates a phantom bar. if path.flags.bits() & 8 != 0 { continue; } diff --git a/src/ui/style/dimstyle.rs b/src/ui/style/dimstyle.rs index 93577d85f..fc926a65c 100644 --- a/src/ui/style/dimstyle.rs +++ b/src/ui/style/dimstyle.rs @@ -2,9 +2,9 @@ use crate::app::{ColorPickTarget, DsField, Message}; use iced::widget::{ - button, checkbox, column, container, row, scrollable, text, text_input, Space, + button, canvas, checkbox, column, container, row, scrollable, text, text_input, Space, }; -use iced::{Background, Border, Element, Theme}; +use iced::{mouse, Background, Border, Element, Length, Point, Rectangle, Theme}; use crate::t; use std::borrow::Cow; use std::fmt; @@ -73,14 +73,12 @@ pub struct DimStyleValues<'a> { pub dimtmove: &'a str, pub dimupt: bool, pub dimtofl: bool, - pub dimfit: &'a str, pub dimdsep: &'a str, pub dimrnd: &'a str, pub dimzin: &'a str, pub dimfrac: &'a str, pub dimaunit: &'a str, pub dimadec: &'a str, - pub dimunit: &'a str, pub dimazin: &'a str, pub dimalt: bool, pub dimaltf: &'a str, @@ -104,6 +102,13 @@ pub struct DimStyleValues<'a> { // Dropdown option lists shared by the arrowhead / linetype fields. pub block_opts: Vec, pub lt_opts: Vec, + pub text_style_opts: Vec, + pub text_style_fixed_height: Option, + pub compare_name: String, + pub compare_opts: Vec, + pub comparison_sections: Vec, + pub read_only: bool, + pub in_use: bool, /// Colour field whose expanded palette is currently open. pub color_open: Option, } @@ -163,6 +168,106 @@ fn primary_style(theme: &Theme) -> iced::widget::text::Style { } } +struct DimensionPreview { + ext1: bool, + ext2: bool, + dim1: bool, + dim2: bool, + tick: bool, + arrow_size: f32, + text_above: bool, + basic: bool, + text: String, +} + +impl canvas::Program for DimensionPreview { + type State = (); + + fn draw( + &self, + _state: &(), + renderer: &iced::Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec { + let mut frame = canvas::Frame::new(renderer, bounds.size()); + let palette = theme.palette(); + let ink = palette.background.base.text.scale_alpha(0.88); + let guide = palette.primary.base.color; + let stroke = canvas::Stroke::default().with_color(ink).with_width(1.2); + let x1 = 52.0; + let x2 = (bounds.width - 52.0).max(x1 + 40.0); + let y = bounds.height * 0.58; + let object_y = bounds.height - 18.0; + + let line = |a: Point, b: Point| canvas::Path::line(a, b); + frame.stroke( + &line(Point::new(x1 - 18.0, object_y), Point::new(x2 + 18.0, object_y)), + canvas::Stroke::default().with_color(ink.scale_alpha(0.38)).with_width(1.0), + ); + if self.ext1 { + frame.stroke(&line(Point::new(x1, object_y), Point::new(x1, 22.0)), stroke.clone()); + } + if self.ext2 { + frame.stroke(&line(Point::new(x2, object_y), Point::new(x2, 22.0)), stroke.clone()); + } + + let text_half = (self.text.chars().count() as f32 * 3.4 + 8.0).min((x2 - x1) * 0.34); + if self.dim1 { + frame.stroke(&line(Point::new(x1, y), Point::new((bounds.width * 0.5 - text_half).max(x1), y)), stroke.clone()); + } + if self.dim2 { + frame.stroke(&line(Point::new((bounds.width * 0.5 + text_half).min(x2), y), Point::new(x2, y)), stroke.clone()); + } + + let size = self.arrow_size.clamp(5.0, 14.0); + for (tip_x, sign) in [(x1, 1.0_f32), (x2, -1.0_f32)] { + if self.tick { + frame.stroke( + &line( + Point::new(tip_x - size * 0.55, y + size * 0.75), + Point::new(tip_x + size * 0.55, y - size * 0.75), + ), + canvas::Stroke::default().with_color(guide).with_width(1.6), + ); + } else { + let arrow = canvas::Path::new(|path| { + path.move_to(Point::new(tip_x, y)); + path.line_to(Point::new(tip_x + sign * size, y - size * 0.42)); + path.line_to(Point::new(tip_x + sign * size, y + size * 0.42)); + path.close(); + }); + frame.fill(&arrow, guide); + } + } + + let text_y = if self.text_above { y - 16.0 } else { y }; + if self.basic { + let frame_width = (self.text.chars().count() as f32 * 7.0 + 12.0) + .min((x2 - x1) * 0.8); + frame.stroke( + &canvas::Path::rectangle( + Point::new(bounds.width * 0.5 - frame_width * 0.5, text_y - 9.0), + iced::Size::new(frame_width, 18.0), + ), + canvas::Stroke::default().with_color(ink).with_width(1.0), + ); + } + frame.fill_text(canvas::Text { + content: self.text.clone(), + position: Point::new(bounds.width * 0.5, text_y), + color: ink, + size: iced::Pixels(12.0), + align_x: iced::advanced::text::Alignment::Center, + align_y: iced::alignment::Vertical::Center, + shaping: iced::advanced::text::Shaping::Advanced, + ..Default::default() + }); + vec![frame.into_geometry()] + } +} + fn hdivider<'a>(width: iced::Length) -> Element<'a, Message> { container(Space::new().width(width).height(1)) .width(width) @@ -192,7 +297,7 @@ pub fn view_window<'a>( .on_press(Message::DimStyleDialogTab(0)) .style(tab_btn_style(tab == 0)) .padding([4, 10]), - button(text(t!("Arrows")).size(11)) + button(text(t!("Symbols and Arrows")).size(11)) .on_press(Message::DimStyleDialogTab(1)) .style(tab_btn_style(tab == 1)) .padding([4, 10]), @@ -200,39 +305,66 @@ pub fn view_window<'a>( .on_press(Message::DimStyleDialogTab(2)) .style(tab_btn_style(tab == 2)) .padding([4, 10]), - button(text(t!("Scale/Units")).size(11)) + button(text(t!("Fit")).size(11)) .on_press(Message::DimStyleDialogTab(3)) .style(tab_btn_style(tab == 3)) .padding([4, 10]), - button(text(t!("Tolerances")).size(11)) + button(text(t!("Primary Units")).size(11)) .on_press(Message::DimStyleDialogTab(4)) .style(tab_btn_style(tab == 4)) .padding([4, 10]), - button(text(t!("Alternate")).size(11)) + button(text(t!("Alternate Units")).size(11)) .on_press(Message::DimStyleDialogTab(5)) .style(tab_btn_style(tab == 5)) .padding([4, 10]), + button(text(t!("Tolerances")).size(11)) + .on_press(Message::DimStyleDialogTab(6)) + .style(tab_btn_style(tab == 6)) + .padding([4, 10]), ] .spacing(2); let lbl = |s: Cow<'static, str>| text(s).size(11).style(muted_style).width(180); let mk_field = |fld: DsField, val: &'a str| -> Element<'a, Message> { - text_input("", val) - .on_input(move |s| Message::DsEdit(fld.clone(), s)) - .style(field_style) - .size(11) - .width(100) - .into() + if vals.read_only { + container(text(val).size(11).style(muted_style)) + .padding([4, 7]) + .width(100) + .into() + } else { + text_input("", val) + .on_input(move |s| Message::DsEdit(fld.clone(), s)) + .style(field_style) + .size(11) + .width(100) + .into() + } + }; + + let mk_field_enabled = |fld: DsField, val: &'a str, enabled: bool| -> Element<'a, Message> { + if enabled && !vals.read_only { + text_input("", val) + .on_input(move |value| Message::DsEdit(fld.clone(), value)) + .style(field_style) + .size(11) + .width(100) + .into() + } else { + container(text(val).size(11).style(muted_style)) + .padding([4, 7]) + .width(100) + .into() + } }; let chk = |label: Cow<'static, str>, val: bool, fld: DsField| -> Element<'a, Message> { - checkbox(val) - .label(label) - .on_toggle(move |_| Message::DsToggle(fld.clone())) - .size(14) - .text_size(11) - .into() + let item = checkbox(val).label(label).size(14).text_size(11); + if vals.read_only { + item.into() + } else { + item.on_toggle(move |_| Message::DsToggle(fld.clone())).into() + } }; // Enum dropdown: maps the stored integer code to a named option and back, @@ -257,30 +389,26 @@ pub fn view_window<'a>( code: val.to_string(), label: val.to_string(), }); - row![ - lbl(label), + let choice: Element<'a, Message> = if vals.read_only { + container(text(cur.to_string()).size(11).style(muted_style)) + .padding([4, 7]) + .width(150) + .into() + } else { iced::widget::pick_list(Some(cur), options, |value| value.to_string()) - .on_select(move |chosen: DimEnumChoice| { - Message::DsEdit(fld.clone(), chosen.code) - }) - .text_size(11) - .width(150), - ] + .on_select(move |chosen: DimEnumChoice| { + Message::DsEdit(fld.clone(), chosen.code) + }) + .text_size(11) + .width(150) + .into() + }; + row![lbl(label), choice] .spacing(8) .align_y(iced::Center) .into() }; - // Zero-suppression codes shared by DIMZIN / DIMALTZ / DIMALTTZ / DIMTZIN. - const OPT_ZIN: &[(&str, &str)] = &[ - ("0", "Suppress none"), - ("1", "Suppress 0 ft & 0 in"), - ("2", "Keep 0 ft, drop 0 in"), - ("3", "Drop 0 ft, keep 0 in"), - ("4", "Suppress leading"), - ("8", "Suppress trailing"), - ("12", "Suppress leading & trailing"), - ]; // Linear unit formats shared by DIMLUNIT / DIMALTU. const OPT_LUNIT: &[(&str, &str)] = &[ ("1", "Scientific"), @@ -290,11 +418,109 @@ pub fn view_window<'a>( ("5", "Fractional"), ("6", "Windows desktop"), ]; + const OPT_LINEWEIGHT: &[(&str, &str)] = &[ + ("-1", "By layer"), + ("-2", "By block"), + ("-3", "Default"), + ("0", "0.00 mm"), + ("5", "0.05 mm"), + ("9", "0.09 mm"), + ("13", "0.13 mm"), + ("15", "0.15 mm"), + ("18", "0.18 mm"), + ("20", "0.20 mm"), + ("25", "0.25 mm"), + ("30", "0.30 mm"), + ("35", "0.35 mm"), + ("40", "0.40 mm"), + ("50", "0.50 mm"), + ("53", "0.53 mm"), + ("60", "0.60 mm"), + ("70", "0.70 mm"), + ("80", "0.80 mm"), + ("90", "0.90 mm"), + ("100", "1.00 mm"), + ("106", "1.06 mm"), + ("120", "1.20 mm"), + ("140", "1.40 mm"), + ("158", "1.58 mm"), + ("200", "2.00 mm"), + ("211", "2.11 mm"), + ]; + let alternate_unit_name = OPT_LUNIT + .iter() + .find(|(code, _)| *code == vals.dimaltu.trim()) + .map(|(_, label)| crate::i18n::translate(label).into_owned()) + .unwrap_or_else(|| vals.dimaltu.to_string()); + + let zero_controls = |field: DsField, + value: &'a str, + show_feet_inches: bool, + enabled: bool| + -> Element<'a, Message> { + let raw = value.trim().parse::().unwrap_or(0); + let feet_modes = vec![ + DimEnumChoice { code: "0".into(), label: t!("Suppress zero feet and zero inches").into_owned() }, + DimEnumChoice { code: "1".into(), label: t!("Show zero feet and zero inches").into_owned() }, + DimEnumChoice { code: "2".into(), label: t!("Show zero feet; suppress zero inches").into_owned() }, + DimEnumChoice { code: "3".into(), label: t!("Suppress zero feet; show zero inches").into_owned() }, + ]; + let feet_current = feet_modes[(raw & 3) as usize].clone(); + let feet: Element<'a, Message> = if show_feet_inches { + let list = iced::widget::pick_list(Some(feet_current.clone()), feet_modes, |choice| choice.to_string()) + .text_size(11) + .width(210); + let list: Element<'a, Message> = if enabled && !vals.read_only { + let target = field.clone(); + list.on_select(move |choice| { + Message::DsZeroBase(target.clone(), choice.code.parse().unwrap_or(0)) + }).into() + } else { + container(text(feet_current.to_string()).size(11).style(muted_style)) + .padding([4, 7]).width(210).into() + }; + row![lbl(t!("Feet and inches")), list] + .spacing(8).align_y(iced::Center).into() + } else { + Space::new().height(0).into() + }; + let leading = checkbox(raw & 4 != 0) + .label(t!("Suppress leading zeros")) + .size(14) + .text_size(11); + let trailing = checkbox(raw & 8 != 0) + .label(t!("Suppress trailing zeros")) + .size(14) + .text_size(11); + let leading: Element<'a, Message> = if enabled && !vals.read_only { + let target = field.clone(); + leading.on_toggle(move |_| Message::DsZeroFlag(target.clone(), 4)).into() + } else { + leading.into() + }; + let trailing: Element<'a, Message> = if enabled && !vals.read_only { + trailing.on_toggle(move |_| Message::DsZeroFlag(field.clone(), 8)).into() + } else { + trailing.into() + }; + column![feet, leading, trailing].spacing(6).into() + }; // Shared colour selector (main dropdown + "more" palette), reusing the // existing DsEdit path (the chosen colour is sent as an ACI string). let color_open = vals.color_open.clone(); let color_row = move |label: Cow<'static, str>, fld: DsField, _val: &'a str| -> Element<'a, Message> { + if vals.read_only { + return row![ + lbl(label), + container(text(_val).size(11).style(muted_style)) + .padding([4, 7]) + .width(150) + ] + .spacing(8) + .align_y(iced::Center) + .into(); + } let cur = crate::ui::color_select::aci_string_to_color(_val); let open = color_open.as_ref() == Some(&fld); let f_sel = fld.clone(); @@ -325,95 +551,195 @@ pub fn view_window<'a>( selected: String, field: &'static str| -> Element<'a, Message> { - row![ - lbl(label), + let list: Element<'a, Message> = if vals.read_only { + container(text(selected).size(11).style(muted_style)) + .padding([4, 7]) + .width(150) + .into() + } else { iced::widget::pick_list(Some(selected), options, |value| value.to_string()) - .on_select(move |value| Message::DsSetHandle { field, value }) - .text_size(11) - .width(150), - ] + .on_select(move |value| Message::DsSetHandle { field, value }) + .text_size(11) + .width(150) + .into() + }; + row![lbl(label), list] .spacing(8) .align_y(iced::Center) .into() }; + let hrow_enabled = move |label: Cow<'static, str>, + options: Vec, + selected: String, + field: &'static str, + enabled: bool| + -> Element<'a, Message> { + let list = iced::widget::pick_list(Some(selected.clone()), options, |value| value.to_string()) + .text_size(11) + .width(150); + let list: Element<'a, Message> = if enabled && !vals.read_only { + list.on_select(move |value| Message::DsSetHandle { field, value }).into() + } else { + container(text(selected).size(11).style(muted_style)) + .padding([4, 7]) + .width(150) + .into() + }; + row![lbl(label), list] + .spacing(8) + .align_y(iced::Center) + .into() + }; + + let text_style_field: Element<'a, Message> = if vals.read_only { + container(text(vals.dimtxsty).size(11).style(muted_style)) + .padding([4, 7]) + .width(150) + .into() + } else { + iced::widget::pick_list( + Some(vals.dimtxsty.to_string()), + vals.text_style_opts.clone(), + |value| value.to_string(), + ) + .on_select(|value| Message::DsEdit(DsField::Dimtxsty, value)) + .text_size(11) + .width(150) + .into() + }; + let text_height_note: Element<'a, Message> = if let Some(height) = vals.text_style_fixed_height { + text(t!("The selected text style fixes the height at %{height}.", height = height)) + .size(10) + .style(muted_style) + .into() + } else { + Space::new().height(0).into() + }; + let center_mode = vals.dimcen.trim().parse::().unwrap_or(0.0); + let tick_on = vals.dimtsz.trim().parse::().unwrap_or(0.0) > 0.0; + let center_choices = vec![ + DimEnumChoice { code: "none".into(), label: t!("None").into_owned() }, + DimEnumChoice { code: "mark".into(), label: t!("Center mark").into_owned() }, + DimEnumChoice { code: "lines".into(), label: t!("Centerlines").into_owned() }, + ]; + let center_current = if center_mode < 0.0 { + center_choices[2].clone() + } else if center_mode > 0.0 { + center_choices[1].clone() + } else { + center_choices[0].clone() + }; + let center_method_field: Element<'a, Message> = if vals.read_only { + container(text(center_current.to_string()).size(11).style(muted_style)) + .padding([4, 7]).width(150).into() + } else { + iced::widget::pick_list(Some(center_current), center_choices, |choice| choice.to_string()) + .on_select(|choice| Message::DsCenterMarkMode(choice.code)) + .text_size(11) + .width(150) + .into() + }; + let tolerance_choices = vec![ + DimEnumChoice { code: "none".into(), label: t!("None").into_owned() }, + DimEnumChoice { code: "symmetrical".into(), label: t!("Symmetrical").into_owned() }, + DimEnumChoice { code: "deviation".into(), label: t!("Deviation").into_owned() }, + DimEnumChoice { code: "limits".into(), label: t!("Limits").into_owned() }, + DimEnumChoice { code: "basic".into(), label: t!("Basic").into_owned() }, + ]; + let tolerance_current = if vals.dimgap.trim().starts_with('-') { + tolerance_choices[4].clone() + } else if vals.dimlim { + tolerance_choices[3].clone() + } else if vals.dimtol && vals.dimtp.trim() == vals.dimtm.trim() { + tolerance_choices[1].clone() + } else if vals.dimtol { + tolerance_choices[2].clone() + } else { + tolerance_choices[0].clone() + }; + let tolerance_method_field: Element<'a, Message> = if vals.read_only { + container(text(tolerance_current.to_string()).size(11).style(muted_style)) + .padding([4, 7]).width(150).into() + } else { + iced::widget::pick_list( + Some(tolerance_current), + tolerance_choices, + |choice| choice.to_string(), + ) + .on_select(|choice| Message::DsToleranceMode(choice.code)) + .text_size(11) + .width(150) + .into() + }; + let tab_content: Element<'_, Message> = match tab { 0 => column![ text(t!("Dimension Line")).size(11).style(primary_style), row![ - lbl(t!("Extension (DIMDLE)")), + lbl(t!("Extend beyond ticks")), mk_field(DsField::Dimdle, vals.dimdle) ] .spacing(8) .align_y(iced::Center), row![ - lbl(t!("Spacing (DIMDLI)")), + lbl(t!("Baseline spacing")), mk_field(DsField::Dimdli, vals.dimdli) ] .spacing(8) .align_y(iced::Center), row![ - lbl(t!("Text gap (DIMGAP)")), + lbl(t!("Text gap")), mk_field(DsField::Dimgap, vals.dimgap) ] .spacing(8) .align_y(iced::Center), - chk(t!("Suppress 1st line (DIMSD1)"), vals.dimsd1, DsField::Dimsd1), - chk(t!("Suppress 2nd line (DIMSD2)"), vals.dimsd2, DsField::Dimsd2), + chk(t!("Suppress first dimension line"), vals.dimsd1, DsField::Dimsd1), + chk(t!("Suppress second dimension line"), vals.dimsd2, DsField::Dimsd2), text(t!("Extension Line")).size(11).style(primary_style), row![ - lbl(t!("Extension (DIMEXE)")), + lbl(t!("Extend beyond dimension line")), mk_field(DsField::Dimexe, vals.dimexe) ] .spacing(8) .align_y(iced::Center), row![ - lbl(t!("Offset (DIMEXO)")), + lbl(t!("Offset from measured point")), mk_field(DsField::Dimexo, vals.dimexo) ] .spacing(8) .align_y(iced::Center), - chk(t!("Suppress 1st line (DIMSE1)"), vals.dimse1, DsField::Dimse1), - chk(t!("Suppress 2nd line (DIMSE2)"), vals.dimse2, DsField::Dimse2), - color_row(t!("Dim line color ACI (DIMCLRD)"), DsField::Dimclrd, vals.dimclrd), - row![ - lbl(t!("Dim line weight (DIMLWD)")), - mk_field(DsField::Dimlwd, vals.dimlwd) - ] - .spacing(8) - .align_y(iced::Center), - color_row(t!("Ext line color ACI (DIMCLRE)"), DsField::Dimclre, vals.dimclre), - row![ - lbl(t!("Ext line weight (DIMLWE)")), - mk_field(DsField::Dimlwe, vals.dimlwe) - ] - .spacing(8) - .align_y(iced::Center), + chk(t!("Suppress first extension line"), vals.dimse1, DsField::Dimse1), + chk(t!("Suppress second extension line"), vals.dimse2, DsField::Dimse2), + color_row(t!("Dimension line color"), DsField::Dimclrd, vals.dimclrd), + enum_field(t!("Dimension lineweight"), DsField::Dimlwd, vals.dimlwd, OPT_LINEWEIGHT), + color_row(t!("Extension line color"), DsField::Dimclre, vals.dimclre), + enum_field(t!("Extension lineweight"), DsField::Dimlwe, vals.dimlwe, OPT_LINEWEIGHT), chk( - t!("Fixed-length ext lines (DIMFXLON)"), + t!("Use fixed-length extension lines"), vals.dimfxlon, DsField::Dimfxlon ), row![ - lbl(t!("Fixed length (DIMFXL)")), - mk_field(DsField::Dimfxl, vals.dimfxl) + lbl(t!("Fixed length")), + mk_field_enabled(DsField::Dimfxl, vals.dimfxl, vals.dimfxlon) ] .spacing(8) .align_y(iced::Center), hrow( - t!("Dim line linetype (DIMLTYPE)"), + t!("Dimension linetype"), vals.lt_opts.clone(), vals.dimltex_name.clone(), "dimltex_handle" ), hrow( - t!("Ext line 1 linetype (DIMLTEX1)"), + t!("First extension linetype"), vals.lt_opts.clone(), vals.dimltex1_name.clone(), "dimltex1_handle" ), hrow( - t!("Ext line 2 linetype (DIMLTEX2)"), + t!("Second extension linetype"), vals.lt_opts.clone(), vals.dimltex2_name.clone(), "dimltex2_handle" @@ -422,62 +748,68 @@ pub fn view_window<'a>( .spacing(7) .into(), 1 => column![ - text(t!("Arrows")).size(11).style(primary_style), - hrow( - t!("Arrowhead (DIMBLK)"), + text(t!("Arrowheads")).size(11).style(primary_style), + hrow_enabled( + t!("Both arrowheads"), vals.block_opts.clone(), vals.dimblk_name.clone(), - "dimblk" + "dimblk", + !vals.dimsah && !tick_on ), - hrow( - t!("1st arrowhead (DIMBLK1)"), + hrow_enabled( + t!("First arrowhead"), vals.block_opts.clone(), vals.dimblk1_name.clone(), - "dimblk1" + "dimblk1", + vals.dimsah && !tick_on ), - hrow( - t!("2nd arrowhead (DIMBLK2)"), + hrow_enabled( + t!("Second arrowhead"), vals.block_opts.clone(), vals.dimblk2_name.clone(), - "dimblk2" + "dimblk2", + vals.dimsah && !tick_on ), hrow( - t!("Leader arrowhead (DIMLDRBLK)"), + t!("Leader arrowhead"), vals.block_opts.clone(), vals.dimldrblk_name.clone(), "dimldrblk" ), row![ - lbl(t!("Arrow size (DIMASZ)")), - mk_field(DsField::Dimasz, vals.dimasz) + lbl(t!("Arrow size")), + mk_field_enabled(DsField::Dimasz, vals.dimasz, !tick_on) + ] + .spacing(8) + .align_y(iced::Center), + row![lbl(t!("Center marks")), center_method_field] + .spacing(8) + .align_y(iced::Center), + row![ + lbl(t!("Center mark size")), + mk_field_enabled(DsField::Dimcen, vals.dimcen, center_mode != 0.0) ] .spacing(8) .align_y(iced::Center), row![ - lbl(t!("Center mark (DIMCEN)")), - mk_field(DsField::Dimcen, vals.dimcen) - ] - .spacing(8) - .align_y(iced::Center), - row![ - lbl(t!("Tick size (DIMTSZ)")), + lbl(t!("Tick size")), mk_field(DsField::Dimtsz, vals.dimtsz) ] .spacing(8) .align_y(iced::Center), chk( - t!("Separate arrow blocks (DIMSAH)"), + t!("Use different first and second arrowheads"), vals.dimsah, DsField::Dimsah ), enum_field( - t!("Arc length symbol (DIMARCSYM)"), + t!("Arc length symbol"), DsField::Dimarcsym, vals.dimarcsym, &[("0", "Before text"), ("1", "Above text"), ("2", "None")], ), row![ - lbl(t!("Jog angle ° (DIMJOGANG)")), + lbl(t!("Radius jog angle (°)")), mk_field(DsField::Dimjogang, vals.dimjogang) ] .spacing(8) @@ -488,19 +820,20 @@ pub fn view_window<'a>( 2 => column![ text(t!("Text")).size(11).style(primary_style), row![ - lbl(t!("Height (DIMTXT)")), - mk_field(DsField::Dimtxt, vals.dimtxt) + lbl(t!("Text height")), + mk_field_enabled(DsField::Dimtxt, vals.dimtxt, vals.text_style_fixed_height.is_none()) ] .spacing(8) .align_y(iced::Center), + text_height_note, row![ - lbl(t!("Style (DIMTXSTY)")), - mk_field(DsField::Dimtxsty, vals.dimtxsty) + lbl(t!("Text style")), + text_style_field ] .spacing(8) .align_y(iced::Center), enum_field( - t!("Vertical pos (DIMTAD)"), + t!("Vertical placement"), DsField::Dimtad, vals.dimtad, &[ @@ -511,11 +844,11 @@ pub fn view_window<'a>( ("4", "Below"), ], ), - chk(t!("Horizontal inside (DIMTIH)"), vals.dimtih, DsField::Dimtih), - chk(t!("Horizontal outside (DIMTOH)"), vals.dimtoh, DsField::Dimtoh), - color_row(t!("Text color ACI (DIMCLRT)"), DsField::Dimclrt, vals.dimclrt), + chk(t!("Keep text horizontal when inside"), vals.dimtih, DsField::Dimtih), + chk(t!("Keep text horizontal when outside"), vals.dimtoh, DsField::Dimtoh), + color_row(t!("Text color"), DsField::Dimclrt, vals.dimclrt), enum_field( - t!("Horizontal just (DIMJUST)"), + t!("Horizontal placement"), DsField::Dimjust, vals.dimjust, &[ @@ -527,13 +860,13 @@ pub fn view_window<'a>( ], ), row![ - lbl(t!("Vertical pos (DIMTVP)")), + lbl(t!("Vertical offset")), mk_field(DsField::Dimtvp, vals.dimtvp) ] .spacing(8) .align_y(iced::Center), enum_field( - t!("Text fill mode (DIMTFILL)"), + t!("Text background"), DsField::Dimtfill, vals.dimtfill, &[ @@ -542,9 +875,14 @@ pub fn view_window<'a>( ("2", "Color"), ], ), - color_row(t!("Fill color ACI (DIMTFILLCLR)"), DsField::Dimtfillclr, vals.dimtfillclr), + if vals.dimtfill.trim() == "2" { + color_row(t!("Background color"), DsField::Dimtfillclr, vals.dimtfillclr) + } else { + row![lbl(t!("Background color")), container(text(t!("Not applicable")).size(11).style(muted_style)).padding([4, 7]).width(150)] + .spacing(8).align_y(iced::Center).into() + }, chk( - t!("Left-to-right (DIMTXTDIRECTION)"), + t!("Read left to right"), vals.dimtxtdirection, DsField::Dimtxtdirection ), @@ -552,14 +890,38 @@ pub fn view_window<'a>( .spacing(7) .into(), 3 => column![ + text(t!("Fit options")).size(11).style(primary_style), + enum_field( + t!("When space is limited"), + DsField::Dimatfit, + vals.dimatfit, + &[ + ("0", "Move text and arrows outside"), + ("1", "Move arrows outside first"), + ("2", "Move text outside first"), + ("3", "Best fit"), + ], + ), + chk(t!("Always keep text between extension lines"), vals.dimtix, DsField::Dimtix), + chk(t!("Suppress arrows when they do not fit"), vals.dimsoxd, DsField::Dimsoxd), + text(t!("Text movement")).size(11).style(primary_style), + enum_field( + t!("When text is moved"), + DsField::Dimtmove, + vals.dimtmove, + &[ + ("0", "Keep dimension line with text"), + ("1", "Add a leader"), + ("2", "Move text freely"), + ], + ), text(t!("Scale")).size(11).style(primary_style), chk(t!("Annotative"), vals.annotative, DsField::Annotative), row![ - lbl(t!("Overall scale (DIMSCALE)")), + lbl(t!("Overall scale")), if vals.annotative { - text_input("", "0") - .style(field_style) - .size(11) + container(text(t!("Automatic")).size(11).style(muted_style)) + .padding([4, 7]) .width(100) .into() } else { @@ -568,232 +930,315 @@ pub fn view_window<'a>( ] .spacing(8) .align_y(iced::Center), - row![ - lbl(t!("Linear factor (DIMLFAC)")), - mk_field(DsField::Dimlfac, vals.dimlfac) - ] - .spacing(8) - .align_y(iced::Center), - text(t!("Units")).size(11).style(primary_style), - enum_field(t!("Format (DIMLUNIT)"), DsField::Dimlunit, vals.dimlunit, OPT_LUNIT), - row![ - lbl(t!("Decimals (DIMDEC)")), - mk_field(DsField::Dimdec, vals.dimdec) - ] - .spacing(8) - .align_y(iced::Center), - row![ - lbl(t!("Suffix (DIMPOST)")), - mk_field(DsField::Dimpost, vals.dimpost) - ] - .spacing(8) - .align_y(iced::Center), - row![ - lbl(t!("Decimal sep ASCII (DIMDSEP)")), - mk_field(DsField::Dimdsep, vals.dimdsep) - ] - .spacing(8) - .align_y(iced::Center), - row![ - lbl(t!("Round off (DIMRND)")), - mk_field(DsField::Dimrnd, vals.dimrnd) - ] - .spacing(8) - .align_y(iced::Center), - enum_field(t!("Zero suppress (DIMZIN)"), DsField::Dimzin, vals.dimzin, OPT_ZIN), - enum_field( - t!("Fraction format (DIMFRAC)"), - DsField::Dimfrac, - vals.dimfrac, - &[ - ("0", "Horizontal"), - ("1", "Diagonal"), - ("2", "Not stacked"), - ], + text(t!("Fine tuning")).size(11).style(primary_style), + chk(t!("Place text manually while creating dimensions"), vals.dimupt, DsField::Dimupt), + chk(t!("Draw dimension line between extension lines"), vals.dimtofl, DsField::Dimtofl), + ] + .spacing(8) + .into(), + 4 => column![ + text(t!("Linear dimensions")).size(11).style(primary_style), + enum_field(t!("Unit format"), DsField::Dimlunit, vals.dimlunit, OPT_LUNIT), + row![lbl(t!("Precision")), mk_field(DsField::Dimdec, vals.dimdec)] + .spacing(8).align_y(iced::Center), + if matches!(vals.dimlunit.trim(), "4" | "5") { + enum_field( + t!("Fraction format"), + DsField::Dimfrac, + vals.dimfrac, + &[("0", "Horizontal"), ("1", "Diagonal"), ("2", "Not stacked")], + ) + } else { + row![lbl(t!("Fraction format")), container(text(t!("Not applicable")).size(11).style(muted_style)).padding([4, 7]).width(150)] + .spacing(8).align_y(iced::Center).into() + }, + if vals.dimlunit.trim() == "2" { + enum_field( + t!("Decimal separator"), + DsField::Dimdsep, + vals.dimdsep, + &[("46", "Period"), ("44", "Comma"), ("32", "Space")], + ) + } else { + row![lbl(t!("Decimal separator")), container(text(t!("Not applicable")).size(11).style(muted_style)).padding([4, 7]).width(150)] + .spacing(8).align_y(iced::Center).into() + }, + row![lbl(t!("Round off")), mk_field(DsField::Dimrnd, vals.dimrnd)] + .spacing(8).align_y(iced::Center), + row![lbl(t!("Measurement template")), mk_field(DsField::Dimpost, vals.dimpost)] + .spacing(8).align_y(iced::Center), + text(t!("Use <> as the measured value; text before and after it becomes the prefix and suffix.")) + .size(10).style(muted_style), + row![lbl(t!("Measurement scale")), mk_field(DsField::Dimlfac, vals.dimlfac)] + .spacing(8).align_y(iced::Center), + text(t!("Zero suppression")).size(11).style(primary_style), + zero_controls( + DsField::Dimzin, + vals.dimzin, + matches!(vals.dimlunit.trim(), "3" | "4"), + true, ), + text(t!("Angular dimensions")).size(11).style(primary_style), enum_field( - t!("Angular unit (DIMAUNIT)"), + t!("Unit format"), DsField::Dimaunit, vals.dimaunit, - &[ - ("0", "Decimal degrees"), - ("1", "Deg/min/sec"), - ("2", "Gradians"), - ("3", "Radians"), - ], + &[("0", "Decimal degrees"), ("1", "Degrees, minutes, seconds"), ("2", "Gradians"), ("3", "Radians")], ), - row![ - lbl(t!("Angular decimals (DIMADEC)")), - mk_field(DsField::Dimadec, vals.dimadec) - ] - .spacing(8) - .align_y(iced::Center), - row![ - lbl(t!("Unit format (DIMUNIT)")), - mk_field(DsField::Dimunit, vals.dimunit) - ] - .spacing(8) - .align_y(iced::Center), + row![lbl(t!("Precision")), mk_field(DsField::Dimadec, vals.dimadec)] + .spacing(8).align_y(iced::Center), enum_field( - t!("Angular zero supp (DIMAZIN)"), + t!("Zero suppression"), DsField::Dimazin, vals.dimazin, - &[ - ("0", "None"), - ("1", "Leading"), - ("2", "Trailing"), - ("3", "Leading & trailing"), - ], - ), - text(t!("Fit")).size(11).style(primary_style), - enum_field( - t!("Fit (DIMATFIT)"), - DsField::Dimatfit, - vals.dimatfit, - &[ - ("0", "Move text & arrows out"), - ("1", "Arrows out first"), - ("2", "Text out first"), - ("3", "Best fit"), - ], - ), - enum_field( - t!("Text movement (DIMTMOVE)"), - DsField::Dimtmove, - vals.dimtmove, - &[ - ("0", "Keep with dim line"), - ("1", "Add leader"), - ("2", "Move freely"), - ], - ), - row![ - lbl(t!("Fit (legacy DIMFIT)")), - mk_field(DsField::Dimfit, vals.dimfit) - ] - .spacing(8) - .align_y(iced::Center), - chk(t!("Force text inside (DIMTIX)"), vals.dimtix, DsField::Dimtix), - chk( - t!("Suppress outside arrows (DIMSOXD)"), - vals.dimsoxd, - DsField::Dimsoxd - ), - chk(t!("Place text manually (DIMUPT)"), vals.dimupt, DsField::Dimupt), - chk( - t!("Dim line between ext (DIMTOFL)"), - vals.dimtofl, - DsField::Dimtofl + &[("0", "None"), ("1", "Leading"), ("2", "Trailing"), ("3", "Leading and trailing")], ), ] - .spacing(7) + .spacing(8) .into(), 5 => column![ text(t!("Alternate Units")).size(11).style(primary_style), chk( - t!("Enable alternate units (DIMALT)"), + t!("Display alternate units"), vals.dimalt, DsField::Dimalt ), row![ - lbl(t!("Multiplier (DIMALTF)")), - mk_field(DsField::Dimaltf, vals.dimaltf) + lbl(t!("Multiplier")), + mk_field_enabled(DsField::Dimaltf, vals.dimaltf, vals.dimalt) ] .spacing(8) .align_y(iced::Center), row![ - lbl(t!("Decimals (DIMALTD)")), - mk_field(DsField::Dimaltd, vals.dimaltd) + lbl(t!("Precision")), + mk_field_enabled(DsField::Dimaltd, vals.dimaltd, vals.dimalt) ] .spacing(8) .align_y(iced::Center), - enum_field(t!("Unit format (DIMALTU)"), DsField::Dimaltu, vals.dimaltu, OPT_LUNIT), + if vals.dimalt { + enum_field(t!("Unit format"), DsField::Dimaltu, vals.dimaltu, OPT_LUNIT) + } else { + row![lbl(t!("Unit format")), container(text(alternate_unit_name).size(11).style(muted_style)).padding([4, 7]).width(150)] + .spacing(8).align_y(iced::Center).into() + }, row![ - lbl(t!("Tol decimals (DIMALTTD)")), - mk_field(DsField::Dimalttd, vals.dimalttd) + lbl(t!("Tolerance precision")), + mk_field_enabled(DsField::Dimalttd, vals.dimalttd, vals.dimalt) ] .spacing(8) .align_y(iced::Center), row![ - lbl(t!("Round off (DIMALTRND)")), - mk_field(DsField::Dimaltrnd, vals.dimaltrnd) + lbl(t!("Round off")), + mk_field_enabled(DsField::Dimaltrnd, vals.dimaltrnd, vals.dimalt) ] .spacing(8) .align_y(iced::Center), row![ - lbl(t!("Suffix (DIMAPOST)")), - mk_field(DsField::Dimapost, vals.dimapost) + lbl(t!("Measurement template")), + mk_field_enabled(DsField::Dimapost, vals.dimapost, vals.dimalt) ] .spacing(8) .align_y(iced::Center), - enum_field(t!("Zero suppress (DIMALTZ)"), DsField::Dimaltz, vals.dimaltz, OPT_ZIN), - enum_field( - t!("Tol zero supp (DIMALTTZ)"), + text(t!("Zero suppression")).size(11).style(primary_style), + zero_controls( + DsField::Dimaltz, + vals.dimaltz, + matches!(vals.dimaltu.trim(), "3" | "4"), + vals.dimalt, + ), + text(t!("Tolerance zero suppression")).size(11).style(primary_style), + zero_controls( DsField::Dimalttz, vals.dimalttz, - OPT_ZIN, + matches!(vals.dimaltu.trim(), "3" | "4"), + vals.dimalt, ), ] .spacing(7) .into(), _ => column![ text(t!("Tolerances")).size(11).style(primary_style), - chk(t!("Generate tolerances (DIMTOL)"), vals.dimtol, DsField::Dimtol), - chk(t!("Limits generation (DIMLIM)"), vals.dimlim, DsField::Dimlim), row![ - lbl(t!("Plus tolerance (DIMTP)")), - mk_field(DsField::Dimtp, vals.dimtp) + lbl(t!("Method")), + tolerance_method_field ] .spacing(8) .align_y(iced::Center), row![ - lbl(t!("Minus tolerance (DIMTM)")), - mk_field(DsField::Dimtm, vals.dimtm) + lbl(t!("Upper value")), + mk_field_enabled(DsField::Dimtp, vals.dimtp, vals.dimtol || vals.dimlim) ] .spacing(8) .align_y(iced::Center), row![ - lbl(t!("Tol. decimals (DIMTDEC)")), - mk_field(DsField::Dimtdec, vals.dimtdec) + lbl(t!("Lower value")), + mk_field_enabled( + DsField::Dimtm, + vals.dimtm, + (vals.dimtol || vals.dimlim) + && !(vals.dimtol && vals.dimtp.trim() == vals.dimtm.trim()), + ) ] .spacing(8) .align_y(iced::Center), row![ - lbl(t!("Tol. scale (DIMTFAC)")), - mk_field(DsField::Dimtfac, vals.dimtfac) + lbl(t!("Precision")), + mk_field_enabled(DsField::Dimtdec, vals.dimtdec, vals.dimtol || vals.dimlim) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl(t!("Height scale")), + mk_field_enabled(DsField::Dimtfac, vals.dimtfac, vals.dimtol || vals.dimlim) ] .spacing(8) .align_y(iced::Center), enum_field( - t!("Tol. vert just (DIMTOLJ)"), + t!("Vertical position"), DsField::Dimtolj, vals.dimtolj, &[("0", "Bottom"), ("1", "Middle"), ("2", "Top")], ), - enum_field(t!("Tol. zero supp (DIMTZIN)"), DsField::Dimtzin, vals.dimtzin, OPT_ZIN), + text(t!("Zero suppression")).size(11).style(primary_style), + zero_controls( + DsField::Dimtzin, + vals.dimtzin, + matches!(vals.dimlunit.trim(), "3" | "4"), + vals.dimtol || vals.dimlim, + ), ] .spacing(7) .into(), }; - // ── Right panel: tabs + scrollable content ──────────────────────────── - // The scrollable itself fills the full width so its scrollbar sits flush - // against the window's right edge; horizontal insets live on the inner - // content container instead of the panel padding. + let precision = vals.dimdec.trim().parse::().unwrap_or(2).min(8); + let factor = vals.dimlfac.trim().parse::().unwrap_or(1.0); + let mut measured = format!("{:.*}", precision, 125.0 * factor); + let zero_flags = vals.dimzin.trim().parse::().unwrap_or(0); + if zero_flags & 8 != 0 && measured.contains('.') { + while measured.ends_with('0') { + measured.pop(); + } + if measured.ends_with('.') { + measured.pop(); + } + } + if zero_flags & 4 != 0 && measured.starts_with("0.") { + measured.remove(0); + } + if vals.dimdsep.trim() == "44" { + measured = measured.replace('.', ","); + } else if vals.dimdsep.trim() == "32" { + measured = measured.replace('.', " "); + } + let mut preview_text = if vals.dimpost.contains("<>") { + vals.dimpost.replace("<>", &measured) + } else { + format!("{measured}{}", vals.dimpost) + }; + if vals.dimalt { + let alternate = 125.0 * vals.dimaltf.trim().parse::().unwrap_or(1.0); + let alt_precision = vals.dimaltd.trim().parse::().unwrap_or(2).min(8); + let alt_value = format!("{:.*}", alt_precision, alternate); + let alternate_text = if vals.dimapost.contains("<>") { + vals.dimapost.replace("<>", &alt_value) + } else { + format!("{alt_value}{}", vals.dimapost) + }; + preview_text.push_str(&format!(" [{alternate_text}]")); + } + if vals.dimlim { + preview_text = format!( + "{} / {}", + preview_text, + vals.dimtm.trim() + ); + } else if vals.dimtol { + if vals.dimtp.trim() == vals.dimtm.trim() { + preview_text.push_str(&format!(" ±{}", vals.dimtp.trim())); + } else { + preview_text.push_str(&format!(" +{} −{}", vals.dimtp.trim(), vals.dimtm.trim())); + } + } + let preview = canvas(DimensionPreview { + ext1: !vals.dimse1, + ext2: !vals.dimse2, + dim1: !vals.dimsd1, + dim2: !vals.dimsd2, + tick: vals.dimtsz.trim().parse::().unwrap_or(0.0) > 0.0, + arrow_size: vals.dimasz.trim().parse::().unwrap_or(1.0).abs() * 5.0 + 5.0, + text_above: vals.dimtad.trim() != "0", + basic: vals.dimgap.trim().starts_with('-'), + text: preview_text, + }) + .width(Length::Fill) + .height(Length::Fixed(112.0)); + let preview_panel = container(preview) + .width(Length::Fill) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.weak.color)), + border: Border { + color: theme.palette().background.neutral.color, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + }); + let status = if vals.read_only { + t!("Referenced style · Read only") + } else if selected == current { + t!("Current style") + } else if vals.in_use { + t!("Used in drawing") + } else { + t!("Available style") + }; + let comparison: Element<'a, Message> = if vals.compare_opts.is_empty() { + Space::new().height(0).into() + } else { + let summary = if vals.comparison_sections.is_empty() { + t!("No differences").into_owned() + } else { + format!("{}: {}", t!("Different sections"), vals.comparison_sections.join(", ")) + }; + row![ + text(t!("Compare with")).size(10).style(muted_style), + iced::widget::pick_list( + Some(vals.compare_name.clone()), + vals.compare_opts.clone(), + |value| value.to_string(), + ) + .on_select(Message::DimStyleDialogCompare) + .text_size(11) + .width(150), + text(summary).size(10).style(muted_style), + ] + .spacing(8) + .align_y(iced::Center) + .into() + }; + let right_panel = container( column![ - text(t!("Editing: %{selected}", selected = selected)).size(11).style(muted_style), + row![ + text(selected).size(13).style(primary_style), + Space::new().width(Length::Fill), + text(status).size(10).style(muted_style), + ] + .align_y(iced::Center), + preview_panel, + comparison, tabs, - hdivider(sizing.width), - scrollable(container(tab_content).padding([12, 12]).width(sizing.width)) - .width(sizing.width) + hdivider(Length::Fill), + scrollable(container(tab_content).padding([12, 12]).width(Length::Fill)) + .width(Length::Fill) .height(sizing.height), ] .spacing(6) .height(sizing.height), ) .height(sizing.height) - .width(sizing.width) + .width(Length::Fill) .padding(iced::Padding { top: 12.0, right: 0.0, @@ -815,6 +1260,7 @@ pub fn view_window<'a>( on_select: Message::DimStyleDialogSelect, on_set_current: Message::DimStyleDialogSetCurrent, on_apply: Message::DimStyleDialogApply, + read_only: vals.read_only, editor: right_panel.into(), }) } diff --git a/src/ui/style/mleaderstyle.rs b/src/ui/style/mleaderstyle.rs index 7501905cb..8fee7375e 100644 --- a/src/ui/style/mleaderstyle.rs +++ b/src/ui/style/mleaderstyle.rs @@ -1,65 +1,44 @@ -//! Multileader Style Manager window — fills the entire OS window. +//! Multileader style manager. use crate::app::Message; -use iced::widget::{ - button, checkbox, column, container, row, scrollable, text, text_input, -}; -use iced::{Background, Border, Element, Theme}; use crate::t; +use iced::widget::{canvas, checkbox, column, container, row, text, text_input}; +use iced::{mouse, Color, Element, Length, Point, Rectangle, Size, Theme}; use std::borrow::Cow; use std::fmt; #[derive(Clone, Debug, Eq, PartialEq)] -struct EnumChoice(String); +struct EnumChoice { + code: String, + label: Cow<'static, str>, +} impl fmt::Display for EnumChoice { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(crate::i18n::translate(&self.0).as_ref()) + formatter.write_str(self.label.as_ref()) } } -fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |theme: &Theme, st| { - let palette = theme.palette(); - let pair = match (accent, st) { - (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong, - (false, button::Status::Hovered | button::Status::Pressed) => { - palette.background.strong - } - (true, _) => palette.primary.base, - _ => palette.background.weak, - }; - button::Style { - background: Some(Background::Color(pair.color)), - text_color: pair.text, - border: Border { - color: palette.background.neutral.color, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - } - } +fn choices(values: &[(&str, &str)]) -> Vec { + values + .iter() + .map(|(code, label)| EnumChoice { + code: (*code).to_string(), + label: crate::i18n::translate(label), + }) + .collect() } -fn muted_style(theme: &Theme) -> iced::widget::text::Style { - iced::widget::text::Style { - color: Some(theme.palette().background.base.text.scale_alpha(0.68)), - } -} - -fn primary_style(theme: &Theme) -> iced::widget::text::Style { - iced::widget::text::Style { - color: Some(theme.palette().primary.base.color), - } -} - -/// View-model borrowed from the app for one render of the dialog. pub struct MLeaderStyleView<'a> { pub styles: Vec, pub selected: &'a str, pub style: Option<&'a acadrust::objects::MultiLeaderStyle>, pub current: String, + pub tab: u8, + pub compare_name: String, + pub compare_opts: Vec, + pub comparison_sections: Vec, + pub in_use: bool, pub landing_distance: &'a str, pub landing_gap: &'a str, pub arrowhead_size: &'a str, @@ -79,84 +58,72 @@ pub struct MLeaderStyleView<'a> { pub block_scale_x: &'a str, pub block_scale_y: &'a str, pub block_scale_z: &'a str, - // Handle dropdown option lists + currently-selected record names. pub block_opts: Vec, + pub arrow_opts: Vec, pub lt_opts: Vec, pub textstyle_opts: Vec, pub line_type_name: String, pub arrowhead_name: String, pub text_style_name: String, pub block_content_name: String, - /// Name of the style being renamed inline (double-clicked), if any. pub rename_active: Option<&'a str>, - /// Edit buffer for the inline rename text input. pub rename_buf: &'a str, - /// Colour field whose expanded palette is currently open. pub color_open: Option<&'static str>, } -fn section<'a>(label: Cow<'static, str>) -> Element<'a, Message> { - text(label).size(11).style(primary_style).into() +fn muted_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.palette().background.base.text.scale_alpha(0.68)), + } } -fn num_row<'a>( +fn input_row<'a>( label: Cow<'static, str>, placeholder: &'static str, value: &'a str, field: &'static str, ) -> Element<'a, Message> { row![ - text(label).size(11).style(muted_style).width(150), + text(label).size(11).style(muted_style).width(165), text_input(placeholder, value) - .on_input(move |v| Message::MLeaderStyleEdit { field, value: v }) + .on_input(move |value| Message::MLeaderStyleEdit { field, value }) .size(11) - .width(110), + .width(190), ] .spacing(8) .align_y(iced::Center) .into() } -fn readonly_num_row<'a>( - label: Cow<'static, str>, - value: Cow<'static, str>, -) -> Element<'a, Message> { +fn readonly_row<'a>(label: Cow<'static, str>, value: Cow<'static, str>) -> Element<'a, Message> { row![ - text(label).size(11).style(muted_style).width(150), - text(value).size(11).style(muted_style), + text(label).size(11).style(muted_style).width(165), + container(text(value).size(11).style(muted_style)).padding([5, 7]), ] .spacing(8) .align_y(iced::Center) .into() } -/// Shared colour selector row. Reuses MLeaderStyleEdit by sending the chosen -/// colour as an ACI string; `open` shows the expanded palette. fn color_row<'a>( label: Cow<'static, str>, value: &'a str, field: &'static str, open: bool, ) -> Element<'a, Message> { - let cur = crate::ui::color_select::aci_string_to_color(value); + let current = crate::ui::color_select::aci_string_to_color(value); let selector = crate::ui::color_select::color_selector( - cur, + current, open, - crate::ui::color_select::ColorExtras { - by_layer: true, - by_block: true, - }, - move |c| Message::MLeaderStyleEdit { + crate::ui::color_select::ColorExtras { by_layer: true, by_block: true }, + move |color| Message::MLeaderStyleEdit { field, - value: crate::ui::color_select::color_to_aci_string(c), + value: crate::ui::color_select::color_to_aci_string(color), }, Message::MLeaderColorMore(field), - Message::OpenColorWindow( - crate::app::ColorPickTarget::MLeader(field), - cur, - ), + Message::OpenColorWindow(crate::app::ColorPickTarget::MLeader(field), current), ); - row![text(label).size(11).style(muted_style).width(150), selector] + row![text(label).size(11).style(muted_style).width(165), selector] .spacing(8) .align_y(iced::Center) .into() @@ -164,81 +131,68 @@ fn color_row<'a>( fn enum_row<'a>( label: Cow<'static, str>, - options: Vec, + values: &[(&str, &str)], selected: String, field: &'static str, ) -> Element<'a, Message> { + let selected_label = values + .iter() + .find(|(code, _)| *code == selected) + .map(|(_, label)| crate::i18n::translate(label)) + .unwrap_or_else(|| crate::i18n::translate(&selected)); row![ - text(label).size(11).style(muted_style).width(150), - iced::widget::pick_list(Some(EnumChoice(selected)), options, |value| value.to_string()) - .on_select(move |choice| Message::MLeaderStyleSetEnum { field, value: choice.0 }) - .text_size(11) - .width(190), - ] - .spacing(8) - .align_y(iced::Center) - .into() -} - -fn lineweight_row<'a>(selected: acadrust::types::LineWeight) -> Element<'a, Message> { - let selected = crate::ui::properties::LwItem(selected); - row![ - text(t!("Line weight:")).size(11).style(muted_style).width(150), + text(label).size(11).style(muted_style).width(165), iced::widget::pick_list( - Some(selected), - crate::ui::properties::lw_options(), + Some(EnumChoice { code: selected, label: selected_label }), + choices(values), |value| value.to_string(), ) - .on_select(|item| Message::MLeaderStyleLineWeightChanged(item.0)) + .on_select(move |choice| Message::MLeaderStyleSetEnum { field, value: choice.code }) .text_size(11) - .width(190), + .width(220), ] .spacing(8) .align_y(iced::Center) .into() } -/// The 11 horizontal text-attachment variants (debug names). -const ATTACH_OPTS: [&str; 11] = [ - "TopOfTopLine", - "MiddleOfTopLine", - "MiddleOfText", - "MiddleOfBottomLine", - "BottomOfBottomLine", - "BottomLine", - "BottomOfTopLineUnderlineBottomLine", - "BottomOfTopLineUnderlineTopLine", - "BottomOfTopLineUnderlineAll", - "CenterOfText", - "CenterOfTextOverline", -]; - -fn opts(list: &[&str]) -> Vec { - list.iter().map(|value| EnumChoice((*value).to_string())).collect() -} - -/// Dropdown for an Option field (linetype / arrowhead / text style / -/// block content). Options are record names; "None" clears the handle. fn handle_row<'a>( label: Cow<'static, str>, options: Vec, selected: String, field: &'static str, ) -> Element<'a, Message> { + let display_label = |value: &str| match value { + "None" | "ByBlock" | "ByLayer" | "Default" | "Closed filled" => { + crate::i18n::translate(value) + } + _ => Cow::Owned(value.to_string()), + }; + let selected = EnumChoice { + code: selected.clone(), + label: display_label(&selected), + }; + let options: Vec = options + .into_iter() + .map(|code| EnumChoice { + label: display_label(&code), + code, + }) + .collect(); row![ - text(label).size(11).style(muted_style).width(150), + text(label).size(11).style(muted_style).width(165), iced::widget::pick_list(Some(selected), options, |value| value.to_string()) - .on_select(move |value| Message::MLeaderStyleSetHandle { field, value }) - .text_size(11) - .width(190), + .on_select(move |value| Message::MLeaderStyleSetHandle { field, value: value.code }) + .text_size(11) + .width(220), ] .spacing(8) .align_y(iced::Center) .into() } -fn chk<'a>(label: Cow<'static, str>, val: bool, field: &'static str) -> Element<'a, Message> { - checkbox(val) +fn toggle<'a>(label: Cow<'static, str>, value: bool, field: &'static str) -> Element<'a, Message> { + checkbox(value) .label(label) .on_toggle(move |_| Message::MLeaderStyleToggle(field)) .size(14) @@ -246,203 +200,528 @@ fn chk<'a>(label: Cow<'static, str>, val: bool, field: &'static str) -> Element< .into() } +const ATTACHMENTS: [(&str, &str); 11] = [ + ("TopOfTopLine", "Top of top line"), + ("MiddleOfTopLine", "Middle of top line"), + ("MiddleOfText", "Middle of text"), + ("MiddleOfBottomLine", "Middle of bottom line"), + ("BottomOfBottomLine", "Bottom of bottom line"), + ("BottomLine", "Bottom line"), + ("BottomOfTopLineUnderlineBottomLine", "Underline bottom line"), + ("BottomOfTopLineUnderlineTopLine", "Underline top line"), + ("BottomOfTopLineUnderlineAll", "Underline all text"), + ("CenterOfText", "Center of text"), + ("CenterOfTextOverline", "Center of text with overline"), +]; + +const LEADER_DRAW_ORDERS: [(&str, &str); 2] = [ + ("LeaderHeadFirst", "Arrowhead first"), + ("LeaderTailFirst", "Content first"), +]; +const MULTILEADER_DRAW_ORDERS: [(&str, &str); 2] = [ + ("ContentFirst", "Content first"), + ("LeaderFirst", "Leader first"), +]; +const BLOCK_CONNECTIONS: [(&str, &str); 2] = [ + ("BlockExtents", "Block extents"), + ("BasePoint", "Base point"), +]; + +fn choice_label(values: &[(&str, &str)], selected: String) -> String { + values + .iter() + .find(|(code, _)| *code == selected) + .map(|(_, label)| crate::i18n::translate(label).into_owned()) + .unwrap_or(selected) +} + +fn handle_label(value: &str) -> String { + match value { + "None" | "ByBlock" | "ByLayer" | "Default" | "Closed filled" => { + crate::i18n::translate(value).into_owned() + } + _ => value.to_string(), + } +} + +fn aci_color(value: &str, fallback: Color) -> Color { + crate::ui::color_select::aci_string_to_color(value) + .rgb() + .map(|(r, g, b)| Color::from_rgb8(r, g, b)) + .unwrap_or(fallback) +} + +struct LeaderPreviewCanvas { + path_type: String, + content_type: String, + line_color: String, + line_weight: i16, + line_type: String, + arrow_name: String, + arrow_name_code: String, + arrow_size: String, + break_gap: String, + landing: bool, + dogleg: bool, + landing_distance: String, + landing_gap: String, + max_points: String, + first_angle: String, + second_angle: String, + scale: String, + annotative: bool, + align_space: String, + default_text: String, + text_style: String, + text_height: String, + text_color: String, + text_angle: String, + text_alignment: String, + attachment_direction: String, + left_attachment: String, + left_attachment_code: String, + right_attachment: String, + top_attachment: String, + bottom_attachment: String, + text_frame: bool, + text_always_left: bool, + block_name: String, + block_color: String, + block_connection: String, + block_connection_code: String, + block_rotation: String, + block_scale: [String; 3], + enable_block_scale: bool, + enable_block_rotation: bool, + leader_draw_order: String, + multileader_draw_order: String, + description: String, +} + +impl canvas::Program for LeaderPreviewCanvas { + type State = (); + + fn draw( + &self, + _state: &(), + renderer: &iced::Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec { + let mut frame = canvas::Frame::new(renderer, bounds.size()); + let ink = theme.palette().background.base.text; + let line_color = aci_color(&self.line_color, ink); + let scale = if self.annotative { + 1.0 + } else { + self.scale.trim().parse::().unwrap_or(1.0).abs().clamp(0.35, 2.2) + }; + let head = Point::new(34.0, bounds.height - 37.0); + let angle1 = self.first_angle.trim().parse::().unwrap_or(28.0).to_radians(); + let angle2 = self.second_angle.trim().parse::().unwrap_or(0.0).to_radians(); + let points = self.max_points.trim().parse::().unwrap_or(2).clamp(2, 5); + let elbow = Point::new( + head.x + 105.0 * scale * angle1.cos().abs().max(0.25), + head.y - 70.0 * scale * angle1.sin().abs().max(0.2), + ); + let landing_length = self + .landing_distance + .trim() + .parse::() + .unwrap_or(8.0) + .abs() + .mul_add(6.0, 18.0) + .clamp(24.0, 145.0) + * scale; + let tail = Point::new(elbow.x + landing_length * angle2.cos().abs(), elbow.y); + let width = (self.line_weight.abs() as f32 / 35.0).clamp(0.8, 4.0); + let dashed = !self.line_type.eq_ignore_ascii_case("None") + && !self.line_type.eq_ignore_ascii_case("ByBlock") + && !self.line_type.to_ascii_lowercase().contains("continuous"); + let stroke = canvas::Stroke { + style: canvas::Style::Solid(line_color), + width, + line_dash: if dashed { + canvas::LineDash { segments: &[7.0, 4.0], offset: 0 } + } else { + canvas::LineDash::default() + }, + ..Default::default() + }; + if self.path_type != "Invisible" { + if self.path_type == "Spline" { + let path = canvas::Path::new(|builder| { + builder.move_to(head); + for step in 1..=18 { + let t = step as f32 / 18.0; + let x = head.x + (elbow.x - head.x) * t; + let y = head.y + (elbow.y - head.y) * t - (t * std::f32::consts::PI).sin() * 15.0; + builder.line_to(Point::new(x, y)); + } + }); + frame.stroke(&path, stroke.clone()); + } else { + let path = canvas::Path::new(|builder| { + builder.move_to(head); + for index in 1..points { + let t = index as f32 / (points - 1) as f32; + let bend = if index + 1 == points { 0.0 } else { (index as f32 * 7.0) % 14.0 - 7.0 }; + builder.line_to(Point::new( + head.x + (elbow.x - head.x) * t, + head.y + (elbow.y - head.y) * t + bend, + )); + } + }); + frame.stroke(&path, stroke.clone()); + } + if self.landing && self.dogleg { + let gap = self.break_gap.trim().parse::().unwrap_or(0.0).abs().clamp(0.0, 8.0); + frame.stroke(&canvas::Path::line(elbow, Point::new((elbow.x + tail.x) * 0.5 - gap, elbow.y)), stroke.clone()); + frame.stroke(&canvas::Path::line(Point::new((elbow.x + tail.x) * 0.5 + gap, elbow.y), tail), stroke.clone()); + } + } + let arrow = self.arrow_size.trim().parse::().unwrap_or(0.18).abs().mul_add(12.0, 4.0).clamp(5.0, 18.0) * scale; + let arrow_code = self.arrow_name_code.to_ascii_uppercase(); + if arrow_code.contains("DOT") { + frame.fill(&canvas::Path::circle(head, arrow * 0.42), line_color); + } else if arrow_code.contains("TICK") || arrow_code.contains("OBLIQUE") { + frame.stroke( + &canvas::Path::line( + Point::new(head.x - arrow * 0.35, head.y + arrow * 0.65), + Point::new(head.x + arrow * 0.35, head.y - arrow * 0.65), + ), + canvas::Stroke::default().with_color(line_color).with_width(width), + ); + } else if arrow_code.contains("BOX") { + frame.fill( + &canvas::Path::rectangle( + Point::new(head.x - arrow * 0.35, head.y - arrow * 0.35), + Size::new(arrow * 0.7, arrow * 0.7), + ), + line_color, + ); + } else { + let arrow_path = canvas::Path::new(|builder| { + builder.move_to(head); + builder.line_to(Point::new(head.x + arrow, head.y - arrow * 0.45)); + builder.line_to(Point::new(head.x + arrow, head.y + arrow * 0.45)); + builder.close(); + }); + if arrow_code.contains("OPEN") { + let open_path = canvas::Path::new(|builder| { + builder.move_to(Point::new(head.x + arrow, head.y - arrow * 0.45)); + builder.line_to(head); + builder.line_to(Point::new(head.x + arrow, head.y + arrow * 0.45)); + }); + frame.stroke( + &open_path, + canvas::Stroke::default().with_color(line_color).with_width(width), + ); + } else { + frame.fill(&arrow_path, line_color); + } + } + let landing_gap = self.landing_gap.trim().parse::().unwrap_or(0.09).abs().mul_add(8.0, 5.0).clamp(5.0, 24.0); + let content_origin = Point::new(tail.x + landing_gap, tail.y); + if self.content_type == "Block" { + let scales = self.block_scale.iter().map(|value| value.trim().parse::().unwrap_or(1.0).abs()).collect::>(); + let (scale_x, scale_y) = if self.enable_block_scale { + (scales[0].clamp(0.3, 2.5), scales[1].clamp(0.3, 2.5)) + } else { + (1.0, 1.0) + }; + let size = Size::new(42.0 * scale_x, 30.0 * scale_y); + let rotation = if self.enable_block_rotation { + self.block_rotation.trim().parse::().unwrap_or(0.0).to_radians() + } else { + 0.0 + }; + let center_x = if self.block_connection_code == "BasePoint" { + content_origin.x + } else { + content_origin.x + size.width * 0.5 + }; + frame.with_save(|frame| { + frame.translate(iced::Vector::new( + center_x, + content_origin.y, + )); + frame.rotate(iced::Radians(rotation)); + let top_left = Point::new(-size.width * 0.5, -size.height * 0.5); + let rect = canvas::Path::rectangle(top_left, size); + let color = aci_color(&self.block_color, ink); + frame.stroke( + &rect, + canvas::Stroke::default().with_color(color).with_width(1.5), + ); + frame.stroke( + &canvas::Path::line( + top_left, + Point::new(size.width * 0.5, size.height * 0.5), + ), + canvas::Stroke::default().with_color(color), + ); + }); + } else if self.content_type != "None" { + let content = if self.content_type == "Tolerance" { + "⌀0.01 | A".to_string() + } else if self.default_text.trim().is_empty() { + t!("Leader note").into_owned() + } else { + self.default_text.clone() + }; + let text_size = self.text_height.trim().parse::().unwrap_or(0.18).abs().mul_add(22.0, 8.0).clamp(9.0, 19.0) * scale; + let box_width = (content.chars().count() as f32 * text_size * 0.55 + 14.0).clamp(60.0, bounds.width - content_origin.x - 8.0); + let box_height = text_size + 12.0 + self.align_space.trim().parse::().unwrap_or(0.0).abs().min(8.0); + let attachment_offset = if self.attachment_direction == "Horizontal" { + match self.left_attachment_code.as_str() { + "TopOfTopLine" | "BottomOfTopLineUnderlineTopLine" => box_height * 0.5, + "MiddleOfTopLine" => box_height * 0.25, + "MiddleOfBottomLine" => -box_height * 0.25, + "BottomOfBottomLine" | "BottomLine" + | "BottomOfTopLineUnderlineBottomLine" => -box_height * 0.5, + _ => 0.0, + } + } else { + 0.0 + }; + let content_y = content_origin.y + attachment_offset; + if self.text_frame { + frame.stroke( + &canvas::Path::rectangle(Point::new(content_origin.x, content_y - box_height * 0.5), Size::new(box_width, box_height)), + canvas::Stroke::default().with_color(aci_color(&self.text_color, ink)).with_width(1.0), + ); + } + let align = if self.text_always_left || self.text_alignment == "Left" { + iced::advanced::text::Alignment::Left + } else if self.text_alignment == "Right" { + iced::advanced::text::Alignment::Right + } else { + iced::advanced::text::Alignment::Center + }; + let x = match align { + iced::advanced::text::Alignment::Left => content_origin.x + 5.0, + iced::advanced::text::Alignment::Right => content_origin.x + box_width - 5.0, + _ => content_origin.x + box_width * 0.5, + }; + let rotation = if self.attachment_direction == "Vertical" { + std::f32::consts::FRAC_PI_2 + } else if self.text_angle == "ParallelToLastLeaderLine" { + -angle2 + } else { + 0.0 + }; + frame.with_save(|frame| { + frame.translate(iced::Vector::new(x, content_y)); + frame.rotate(iced::Radians(rotation)); + frame.fill_text(canvas::Text { + content, + position: Point::ORIGIN, + color: aci_color(&self.text_color, ink), + size: iced::Pixels(text_size), + align_x: align, + align_y: iced::alignment::Vertical::Center, + ..Default::default() + }); + }); + } + frame.fill_text(canvas::Text { + content: format!( + "{} · {} · {} · {} · {} / {} · {}", + handle_label(&self.line_type), + self.arrow_name, + self.text_style, + self.block_name, + self.leader_draw_order, + self.multileader_draw_order, + self.description, + ), + position: Point::new(10.0, bounds.height - 9.0), + color: ink.scale_alpha(0.62), + size: iced::Pixels(9.0), + align_y: iced::alignment::Vertical::Center, + ..Default::default() + }); + frame.fill_text(canvas::Text { + content: format!( + "{} / {} / {} / {} · {} · {}{} · z {} · {}", + self.left_attachment, + self.right_attachment, + self.top_attachment, + self.bottom_attachment, + self.block_connection, + if self.enable_block_rotation { &self.block_rotation } else { "0" }, + "°", + self.block_scale[2], + if self.annotative { t!("Annotative") } else { t!("Drawing scale") }, + ), + position: Point::new(10.0, 9.0), + color: ink.scale_alpha(0.55), + size: iced::Pixels(9.0), + align_y: iced::alignment::Vertical::Center, + ..Default::default() + }); + vec![frame.into_geometry()] + } +} + pub fn view_window<'a>( v: MLeaderStyleView<'a>, sizing: crate::ui::modal::ModalSizing, ) -> Element<'a, Message> { - // ── Right: Details panel ────────────────────────────────────────────── - let details: Element<'_, Message> = if let Some(s) = v.style { - scrollable( - column![ - row![ - text(t!("Name:")).size(11).style(muted_style).width(150), - text(s.name.clone()).size(11), - ] - .spacing(8), - num_row(t!("Description:"), "", v.description, "description"), - // Leader Format - section(t!("Leader Format")), - enum_row( - t!("Path type:"), - opts(&["Invisible", "StraightLineSegments", "Spline"]), - format!("{:?}", s.path_type), - "path_type" - ), - color_row(t!("Line color (ACI):"), v.line_color, "line_color", v.color_open == Some("line_color")), - lineweight_row(s.line_weight), - handle_row( - t!("Line type:"), - v.lt_opts.clone(), - v.line_type_name.clone(), - "line_type_handle" - ), - handle_row( - t!("Arrowhead block:"), - v.block_opts.clone(), - v.arrowhead_name.clone(), - "arrowhead_handle" - ), - num_row( - t!("Arrowhead size:"), - "0.18", - v.arrowhead_size, - "arrowhead_size" - ), - num_row(t!("Break gap size:"), "0.125", v.break_gap, "break_gap"), - // Leader Structure - section(t!("Leader Structure")), - chk(t!("Enable landing"), s.enable_landing, "enable_landing"), - chk(t!("Enable dogleg"), s.enable_dogleg, "enable_dogleg"), - num_row( - t!("Landing distance:"), - "8.0", - v.landing_distance, - "landing_distance" - ), - num_row(t!("Landing gap:"), "0.09", v.landing_gap, "landing_gap"), - num_row(t!("Max leader points:"), "2", v.max_points, "max_points"), - num_row( - t!("First seg. angle:"), - "0", - v.first_seg_angle, - "first_seg_angle" - ), - num_row( - t!("Second seg. angle:"), - "0", - v.second_seg_angle, - "second_seg_angle" - ), - if s.is_annotative { - readonly_num_row(t!("Scale factor:"), t!("By annotation scale")) - } else { - num_row(t!("Scale factor:"), "1.0", v.scale_factor, "scale_factor") - }, - num_row(t!("Align space:"), "4.0", v.align_space, "align_space"), - enum_row( - t!("Leader draw order:"), - opts(&["LeaderHeadFirst", "LeaderTailFirst"]), - format!("{:?}", s.leader_draw_order), - "leader_draw_order" - ), - enum_row( - t!("Multileader draw order:"), - opts(&["ContentFirst", "LeaderFirst"]), - format!("{:?}", s.multileader_draw_order), - "multileader_draw_order" - ), - chk(t!("Annotative"), s.is_annotative, "annotative"), - // Content - section(t!("Content")), - enum_row( - t!("Content type:"), - opts(&["None", "Block", "MText", "Tolerance"]), - format!("{:?}", s.content_type), - "content_type" - ), - num_row(t!("Default text:"), "", v.default_text, "default_text"), - handle_row( - t!("Text style:"), - v.textstyle_opts.clone(), - v.text_style_name.clone(), - "text_style_handle" - ), - num_row(t!("Text height:"), "0.18", v.text_height, "text_height"), - color_row(t!("Text color (ACI):"), v.text_color, "text_color", v.color_open == Some("text_color")), - enum_row( - t!("Text angle:"), - opts(&["ParallelToLastLeaderLine", "Horizontal", "Optimized"]), - format!("{:?}", s.text_angle_type), - "text_angle_type" - ), - enum_row( - t!("Text alignment:"), - opts(&["Left", "Center", "Right"]), - format!("{:?}", s.text_alignment), - "text_alignment" - ), - enum_row( - t!("Left attachment:"), - opts(&ATTACH_OPTS), - format!("{:?}", s.text_left_attachment), - "text_left_attachment" - ), - enum_row( - t!("Right attachment:"), - opts(&ATTACH_OPTS), - format!("{:?}", s.text_right_attachment), - "text_right_attachment" - ), - enum_row( - t!("Top attachment:"), - opts(&ATTACH_OPTS), - format!("{:?}", s.text_top_attachment), - "text_top_attachment" - ), - enum_row( - t!("Bottom attachment:"), - opts(&ATTACH_OPTS), - format!("{:?}", s.text_bottom_attachment), - "text_bottom_attachment" - ), - enum_row( - t!("Attachment direction:"), - opts(&["Horizontal", "Vertical"]), - format!("{:?}", s.text_attachment_direction), - "text_attachment_direction" - ), - chk(t!("Text frame"), s.text_frame, "text_frame"), - chk(t!("Text always left"), s.text_always_left, "text_always_left"), - // Block Content - section(t!("Block Content")), - handle_row( - t!("Block:"), - v.block_opts.clone(), - v.block_content_name.clone(), - "block_content_handle" - ), - color_row(t!("Block color (ACI):"), v.block_color, "block_color", v.color_open == Some("block_color")), - enum_row( - t!("Block connection:"), - opts(&["BlockExtents", "BasePoint"]), - format!("{:?}", s.block_content_connection), - "block_content_connection" - ), - num_row(t!("Block rotation:"), "0", v.block_rotation, "block_rotation"), - num_row(t!("Block scale X:"), "1.0", v.block_scale_x, "block_scale_x"), - num_row(t!("Block scale Y:"), "1.0", v.block_scale_y, "block_scale_y"), - num_row(t!("Block scale Z:"), "1.0", v.block_scale_z, "block_scale_z"), - chk( - t!("Enable block scale"), - s.enable_block_scale, - "enable_block_scale" - ), - chk( - t!("Enable block rotation"), - s.enable_block_rotation, - "enable_block_rotation" - ), - button(text(t!("Apply")).size(11)) - .on_press(Message::MLeaderStyleApply) - .style(btn_s(true)) - .padding([4, 14]), - ] - .spacing(6) - .padding([12, 12]), - ) - .width(sizing.width) - .height(sizing.height) + let style = v.style; + let content: Element<'a, Message> = match (v.tab, style) { + (_, None) => text(t!("Select a style to view details.")).size(11).into(), + (0, Some(style)) => column![ + input_row(t!("Description"), "", v.description, "description"), + enum_row(t!("Path type"), &[("Invisible", "None"), ("StraightLineSegments", "Straight"), ("Spline", "Spline")], format!("{:?}", style.path_type), "path_type"), + color_row(t!("Line color"), v.line_color, "line_color", v.color_open == Some("line_color")), + row![ + text(t!("Line weight")).size(11).style(muted_style).width(165), + iced::widget::pick_list( + Some(crate::ui::properties::LwItem(style.line_weight)), + crate::ui::properties::lw_options(), + |value| value.to_string(), + ) + .on_select(|item| Message::MLeaderStyleLineWeightChanged(item.0)) + .text_size(11).width(220), + ].spacing(8).align_y(iced::Center), + handle_row(t!("Line type"), v.lt_opts.clone(), v.line_type_name.clone(), "line_type_handle"), + handle_row(t!("Arrowhead"), v.arrow_opts.clone(), v.arrowhead_name.clone(), "arrowhead_handle"), + input_row(t!("Arrowhead size"), "0.18", v.arrowhead_size, "arrowhead_size"), + input_row(t!("Break gap size"), "0.125", v.break_gap, "break_gap"), + ].spacing(8).into(), + (1, Some(style)) => column![ + toggle(t!("Enable landing"), style.enable_landing, "enable_landing"), + toggle(t!("Enable dogleg"), style.enable_dogleg, "enable_dogleg"), + input_row(t!("Landing distance"), "8.0", v.landing_distance, "landing_distance"), + input_row(t!("Landing gap"), "0.09", v.landing_gap, "landing_gap"), + input_row(t!("Maximum leader points"), "2", v.max_points, "max_points"), + input_row(t!("First segment angle"), "0", v.first_seg_angle, "first_seg_angle"), + input_row(t!("Second segment angle"), "0", v.second_seg_angle, "second_seg_angle"), + if style.is_annotative { readonly_row(t!("Scale factor"), t!("By annotation scale")) } else { input_row(t!("Scale factor"), "1.0", v.scale_factor, "scale_factor") }, + input_row(t!("Alignment spacing"), "4.0", v.align_space, "align_space"), + enum_row(t!("Leader draw order"), &LEADER_DRAW_ORDERS, format!("{:?}", style.leader_draw_order), "leader_draw_order"), + enum_row(t!("Multileader draw order"), &MULTILEADER_DRAW_ORDERS, format!("{:?}", style.multileader_draw_order), "multileader_draw_order"), + toggle(t!("Annotative"), style.is_annotative, "annotative"), + ].spacing(8).into(), + (2, Some(style)) => column![ + enum_row(t!("Content type"), &[("None", "None"), ("Block", "Block"), ("MText", "Text"), ("Tolerance", "Tolerance")], format!("{:?}", style.content_type), "content_type"), + input_row(t!("Default text"), "", v.default_text, "default_text"), + handle_row(t!("Text style"), v.textstyle_opts.clone(), v.text_style_name.clone(), "text_style_handle"), + input_row(t!("Text height"), "0.18", v.text_height, "text_height"), + color_row(t!("Text color"), v.text_color, "text_color", v.color_open == Some("text_color")), + enum_row(t!("Text angle"), &[("ParallelToLastLeaderLine", "Parallel to last segment"), ("Horizontal", "Horizontal"), ("Optimized", "Optimized")], format!("{:?}", style.text_angle_type), "text_angle_type"), + enum_row(t!("Text alignment"), &[("Left", "Left"), ("Center", "Center"), ("Right", "Right")], format!("{:?}", style.text_alignment), "text_alignment"), + enum_row(t!("Attachment direction"), &[("Horizontal", "Horizontal"), ("Vertical", "Vertical")], format!("{:?}", style.text_attachment_direction), "text_attachment_direction"), + enum_row(t!("Left attachment"), &ATTACHMENTS, format!("{:?}", style.text_left_attachment), "text_left_attachment"), + enum_row(t!("Right attachment"), &ATTACHMENTS, format!("{:?}", style.text_right_attachment), "text_right_attachment"), + enum_row(t!("Top attachment"), &ATTACHMENTS, format!("{:?}", style.text_top_attachment), "text_top_attachment"), + enum_row(t!("Bottom attachment"), &ATTACHMENTS, format!("{:?}", style.text_bottom_attachment), "text_bottom_attachment"), + row![toggle(t!("Text frame"), style.text_frame, "text_frame"), toggle(t!("Always left align"), style.text_always_left, "text_always_left")].spacing(18), + ].spacing(8).into(), + (_, Some(style)) => column![ + handle_row(t!("Block"), v.block_opts.clone(), v.block_content_name.clone(), "block_content_handle"), + color_row(t!("Block color"), v.block_color, "block_color", v.color_open == Some("block_color")), + enum_row(t!("Block connection"), &BLOCK_CONNECTIONS, format!("{:?}", style.block_content_connection), "block_content_connection"), + input_row(t!("Block rotation"), "0", v.block_rotation, "block_rotation"), + input_row(t!("Block scale X"), "1.0", v.block_scale_x, "block_scale_x"), + input_row(t!("Block scale Y"), "1.0", v.block_scale_y, "block_scale_y"), + input_row(t!("Block scale Z"), "1.0", v.block_scale_z, "block_scale_z"), + toggle(t!("Enable block scale"), style.enable_block_scale, "enable_block_scale"), + toggle(t!("Enable block rotation"), style.enable_block_rotation, "enable_block_rotation"), + ].spacing(8).into(), + }; + let preview: Element<'a, Message> = if let Some(style) = style { + canvas(LeaderPreviewCanvas { + path_type: format!("{:?}", style.path_type), + content_type: format!("{:?}", style.content_type), + line_color: v.line_color.to_string(), + line_weight: style.line_weight.value(), + line_type: v.line_type_name.clone(), + arrow_name: handle_label(&v.arrowhead_name), + arrow_name_code: v.arrowhead_name.clone(), + arrow_size: v.arrowhead_size.to_string(), + break_gap: v.break_gap.to_string(), + landing: style.enable_landing, + dogleg: style.enable_dogleg, + landing_distance: v.landing_distance.to_string(), + landing_gap: v.landing_gap.to_string(), + max_points: v.max_points.to_string(), + first_angle: v.first_seg_angle.to_string(), + second_angle: v.second_seg_angle.to_string(), + scale: v.scale_factor.to_string(), + annotative: style.is_annotative, + align_space: v.align_space.to_string(), + default_text: v.default_text.to_string(), + text_style: handle_label(&v.text_style_name), + text_height: v.text_height.to_string(), + text_color: v.text_color.to_string(), + text_angle: format!("{:?}", style.text_angle_type), + text_alignment: format!("{:?}", style.text_alignment), + attachment_direction: format!("{:?}", style.text_attachment_direction), + left_attachment: choice_label(&ATTACHMENTS, format!("{:?}", style.text_left_attachment)), + left_attachment_code: format!("{:?}", style.text_left_attachment), + right_attachment: choice_label(&ATTACHMENTS, format!("{:?}", style.text_right_attachment)), + top_attachment: choice_label(&ATTACHMENTS, format!("{:?}", style.text_top_attachment)), + bottom_attachment: choice_label(&ATTACHMENTS, format!("{:?}", style.text_bottom_attachment)), + text_frame: style.text_frame, + text_always_left: style.text_always_left, + block_name: handle_label(&v.block_content_name), + block_color: v.block_color.to_string(), + block_connection: choice_label(&BLOCK_CONNECTIONS, format!("{:?}", style.block_content_connection)), + block_connection_code: format!("{:?}", style.block_content_connection), + block_rotation: v.block_rotation.to_string(), + block_scale: [v.block_scale_x.to_string(), v.block_scale_y.to_string(), v.block_scale_z.to_string()], + enable_block_scale: style.enable_block_scale, + enable_block_rotation: style.enable_block_rotation, + leader_draw_order: choice_label(&LEADER_DRAW_ORDERS, format!("{:?}", style.leader_draw_order)), + multileader_draw_order: choice_label(&MULTILEADER_DRAW_ORDERS, format!("{:?}", style.multileader_draw_order)), + description: v.description.to_string(), + }) + .width(Length::Fill) + .height(150) .into() } else { - container(text(t!("Select a style to view details.")).size(11).style(muted_style)) - .padding([12, 12]) - .into() + container(text(t!("No preview"))).height(150).into() }; - - let right_panel = container(details) - .width(sizing.width) - .height(sizing.height); - + let status = if v.selected.eq_ignore_ascii_case(&v.current) { + t!("Current style") + } else if v.in_use { + t!("Used in drawing") + } else { + t!("Available style") + }; + let summary = if v.comparison_sections.is_empty() { + t!("No differences").into_owned() + } else { + format!("{}: {}", t!("Different"), v.comparison_sections.join(", ")) + }; + let editor = crate::ui::style::style_manager::editor_shell( + crate::ui::style::style_manager::EditorShell { + sizing, + selected: v.selected.to_string(), + status, + preview, + comparison: Some(crate::ui::style::style_manager::EditorComparison { + selected: v.compare_name, + options: v.compare_opts, + summary, + on_select: Message::MLeaderStyleDialogCompare, + }), + tabs: [t!("Leader Format"), t!("Leader Structure"), t!("Content"), t!("Block Content")] + .into_iter() + .enumerate() + .map(|(tab, label)| crate::ui::style::style_manager::EditorTab { + label, + active: v.tab == tab as u8, + on_press: Message::MLeaderStyleDialogTab(tab as u8), + }) + .collect(), + content, + }, + ); crate::ui::style::style_manager::view(crate::ui::style::style_manager::Scaffold { sizing, kind: crate::app::StyleKind::MLeader, @@ -457,6 +736,7 @@ pub fn view_window<'a>( on_select: Message::MLeaderStyleDialogSelect, on_set_current: Message::MLeaderStyleDialogSetCurrent, on_apply: Message::MLeaderStyleApply, - editor: right_panel.into(), + read_only: false, + editor, }) } diff --git a/src/ui/style/mlstyle.rs b/src/ui/style/mlstyle.rs index 7c12ca340..15fc8c59f 100644 --- a/src/ui/style/mlstyle.rs +++ b/src/ui/style/mlstyle.rs @@ -1,103 +1,383 @@ -//! Multiline Style Manager window — fills the entire OS window. +//! Multiline style manager. use crate::app::Message; -use iced::widget::{column, container, row, scrollable, text}; -use iced::{Element, Theme}; use crate::t; +use iced::widget::{button, canvas, checkbox, column, container, row, text, text_input}; +use iced::{mouse, Color, Element, Length, Point, Rectangle, Theme}; use std::borrow::Cow; +pub struct MlStyleView<'a> { + pub styles: Vec, + pub selected: &'a str, + pub style: Option<&'a acadrust::objects::MLineStyle>, + pub current: String, + pub tab: u8, + pub compare_name: String, + pub compare_opts: Vec, + pub comparison_sections: Vec, + pub in_use: bool, + pub description: &'a str, + pub start_angle: &'a str, + pub end_angle: &'a str, + pub fill_color: &'a str, + pub elements: &'a [[String; 3]], + pub rename_active: Option<&'a str>, + pub rename_buf: &'a str, +} + fn muted_style(theme: &Theme) -> iced::widget::text::Style { iced::widget::text::Style { color: Some(theme.palette().background.base.text.scale_alpha(0.68)), } } -pub fn view_window<'a>( - styles: Vec, - selected: &'a str, - selected_style: Option<&'a acadrust::objects::MLineStyle>, - current_style: String, - rename_active: Option<&'a str>, - rename_buf: &'a str, - sizing: crate::ui::modal::ModalSizing, -) -> Element<'a, Message> { - // ── Right: Details panel ────────────────────────────────────────────── - let info_row = |label: Cow<'static, str>, val: String| -> Element<'_, Message> { - row![ - text(label).size(11).style(muted_style).width(120), - text(val).size(11), - ] - .spacing(8) - .align_y(iced::Center) - .into() - }; +fn primary_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.palette().primary.base.color), + } +} - let details: Element<'_, Message> = if let Some(s) = selected_style { - let elem_rows: Vec> = s +fn input_row<'a>( + label: Cow<'static, str>, + placeholder: &'static str, + value: &'a str, + field: &'static str, +) -> Element<'a, Message> { + row![ + text(label).size(11).style(muted_style).width(165), + text_input(placeholder, value) + .on_input(move |value| Message::MlStyleEdit { field, value }) + .size(11) + .width(190), + ] + .spacing(8) + .align_y(iced::Center) + .into() +} + +fn toggle<'a>(label: Cow<'static, str>, value: bool, field: &'static str) -> Element<'a, Message> { + checkbox(value) + .label(label) + .on_toggle(move |_| Message::MlStyleToggle(field)) + .size(14) + .text_size(11) + .into() +} + +fn aci_color(value: &str, fallback: Color) -> Color { + crate::ui::color_select::aci_string_to_color(value) + .rgb() + .map(|(r, g, b)| Color::from_rgb8(r, g, b)) + .unwrap_or(fallback) +} + +struct MLinePreviewCanvas { + description: String, + start_angle: String, + end_angle: String, + fill_color: String, + fill: bool, + joints: bool, + start_square: bool, + start_inner: bool, + start_round: bool, + end_square: bool, + end_inner: bool, + end_round: bool, + elements: Vec<[String; 3]>, +} + +fn element_points(offset: f32, bounds: Rectangle) -> [Point; 3] { + let y = bounds.height * 0.48 - offset * 26.0; + [ + Point::new(44.0, y), + Point::new(bounds.width * 0.52, y - 16.0), + Point::new(bounds.width - 44.0, y + 5.0), + ] +} + +fn draw_arc(frame: &mut canvas::Frame, center: Point, radius: f32, start_side: bool, color: Color) { + let path = canvas::Path::new(|builder| { + for step in 0..=18 { + let t = step as f32 / 18.0; + let angle = if start_side { + std::f32::consts::FRAC_PI_2 + t * std::f32::consts::PI + } else { + -std::f32::consts::FRAC_PI_2 + t * std::f32::consts::PI + }; + let point = Point::new(center.x + radius * angle.cos(), center.y + radius * angle.sin()); + if step == 0 { + builder.move_to(point); + } else { + builder.line_to(point); + } + } + }); + frame.stroke(&path, canvas::Stroke::default().with_color(color).with_width(1.1)); +} + +impl canvas::Program for MLinePreviewCanvas { + type State = (); + + fn draw( + &self, + _state: &(), + renderer: &iced::Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec { + let mut frame = canvas::Frame::new(renderer, bounds.size()); + let ink = theme.palette().background.base.text; + let mut parsed = self .elements .iter() - .enumerate() - .map(|(idx, e)| { - let color_str = match &e.color { - acadrust::types::Color::ByLayer => "ByLayer".into(), - acadrust::types::Color::None => "None".into(), - acadrust::types::Color::ByBlock => "ByBlock".into(), - acadrust::types::Color::Index(i) => format!("ACI {i}"), - acadrust::types::Color::Rgb { r, g, b } => format!("#{r:02X}{g:02X}{b:02X}"), - }; - let lt = if e.linetype.is_empty() { - "ByLayer" - } else { - &e.linetype - }; - row![ - text(format!(" {idx}:")).size(10).style(muted_style).width(24), - text(format!("{:+.3}", e.offset)).size(10).width(70), - text(color_str).size(10).width(90), - text(lt).size(10), - ] - .spacing(4) - .align_y(iced::Center) - .into() + .map(|element| { + ( + element[0].trim().parse::().unwrap_or(0.0), + element[1].clone(), + element[2].clone(), + ) }) - .collect(); + .collect::>(); + parsed.sort_by(|a, b| b.0.total_cmp(&a.0)); + if let (Some(top), Some(bottom)) = (parsed.first(), parsed.last()) { + if self.fill { + let upper = element_points(top.0, bounds); + let lower = element_points(bottom.0, bounds); + let fill = canvas::Path::new(|builder| { + builder.move_to(upper[0]); + builder.line_to(upper[1]); + builder.line_to(upper[2]); + builder.line_to(lower[2]); + builder.line_to(lower[1]); + builder.line_to(lower[0]); + builder.close(); + }); + frame.fill(&fill, aci_color(&self.fill_color, theme.palette().background.weak.color)); + } + } + for (offset, color, linetype) in &parsed { + let points = element_points(*offset, bounds); + let dashed = !linetype.trim().is_empty() + && !linetype.eq_ignore_ascii_case("ByLayer") + && !linetype.to_ascii_lowercase().contains("continuous"); + let stroke = canvas::Stroke { + style: canvas::Style::Solid(aci_color(color, ink)), + width: 1.4, + line_dash: if dashed { + canvas::LineDash { segments: &[6.0, 4.0], offset: 0 } + } else { + canvas::LineDash::default() + }, + ..Default::default() + }; + frame.stroke( + &canvas::Path::new(|builder| { + builder.move_to(points[0]); + builder.line_to(points[1]); + builder.line_to(points[2]); + }), + stroke, + ); + } + if let (Some(top), Some(bottom)) = (parsed.first(), parsed.last()) { + let top_points = element_points(top.0, bounds); + let bottom_points = element_points(bottom.0, bounds); + let start_top = top_points[0]; + let start_bottom = bottom_points[0]; + let end_top = top_points[2]; + let end_bottom = bottom_points[2]; + let start_angle = self.start_angle.trim().parse::().unwrap_or(90.0).to_radians(); + let end_angle = self.end_angle.trim().parse::().unwrap_or(90.0).to_radians(); + let cap = |a: Point, b: Point, angle: f32| { + let middle = Point::new((a.x + b.x) * 0.5, (a.y + b.y) * 0.5); + let half = ((a.y - b.y).abs() * 0.5).max(2.0); + let direction = Point::new(angle.cos() * half, angle.sin() * half); + (Point::new(middle.x - direction.x, middle.y - direction.y), Point::new(middle.x + direction.x, middle.y + direction.y)) + }; + if self.start_square { + let (a, b) = cap(start_top, start_bottom, start_angle); + frame.stroke(&canvas::Path::line(a, b), canvas::Stroke::default().with_color(ink)); + } + if self.end_square { + let (a, b) = cap(end_top, end_bottom, end_angle); + frame.stroke(&canvas::Path::line(a, b), canvas::Stroke::default().with_color(ink)); + } + let start_center = Point::new((start_top.x + start_bottom.x) * 0.5, (start_top.y + start_bottom.y) * 0.5); + let end_center = Point::new((end_top.x + end_bottom.x) * 0.5, (end_top.y + end_bottom.y) * 0.5); + let outer_radius = ((start_top.y - start_bottom.y).abs() * 0.5).max(3.0); + if self.start_round { + draw_arc(&mut frame, start_center, outer_radius, true, ink); + } + if self.end_round { + draw_arc(&mut frame, end_center, outer_radius, false, ink); + } + if self.start_inner && parsed.len() > 2 { + draw_arc(&mut frame, start_center, outer_radius * 0.55, true, ink); + } + if self.end_inner && parsed.len() > 2 { + draw_arc(&mut frame, end_center, outer_radius * 0.55, false, ink); + } + if self.joints { + let joint_top = top_points[1]; + let joint_bottom = bottom_points[1]; + frame.stroke(&canvas::Path::line(joint_top, joint_bottom), canvas::Stroke::default().with_color(ink).with_width(1.0)); + } + } + frame.fill_text(canvas::Text { + content: format!("{} · {} {}", self.description, t!("Elements"), self.elements.len()), + position: Point::new(10.0, bounds.height - 9.0), + color: ink.scale_alpha(0.62), + size: iced::Pixels(10.0), + align_y: iced::alignment::Vertical::Center, + ..Default::default() + }); + vec![frame.into_geometry()] + } +} - let mut col_items: Vec> = vec![ - info_row(t!("Name:"), s.name.clone()), - info_row(t!("Elements:"), s.elements.len().to_string()), - text(t!(" Off Color Ltype")) - .size(10) - .style(muted_style) - .into(), - ]; - col_items.extend(elem_rows); - scrollable(column(col_items).spacing(6).padding([12, 12])) - .height(sizing.height) - .into() - } else { - container(text(t!("Select a style to view details.")).size(11).style(muted_style)) - .padding([12, 12]) - .into() +pub fn view_window<'a>( + v: MlStyleView<'a>, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let style = v.style; + let content: Element<'a, Message> = match (v.tab, style) { + (_, None) => text(t!("Select a style to view details.")).size(11).into(), + (0, Some(_)) => { + let mut items = column![ + row![ + text(t!("Offset")).size(10).style(muted_style).width(110), + text(t!("Color")).size(10).style(muted_style).width(100), + text(t!("Line type")).size(10).style(muted_style).width(170), + button(text(t!("Add")).size(10)).on_press(Message::MlStyleElementAdd).padding([4, 12]), + ] + .spacing(6) + .align_y(iced::Center), + ] + .spacing(6); + for (index, element) in v.elements.iter().enumerate() { + items = items.push( + row![ + text_input("0.0", &element[0]) + .on_input(move |value| Message::MlStyleElementEdit { index, field: "offset", value }) + .size(11).width(110), + text_input("256", &element[1]) + .on_input(move |value| Message::MlStyleElementEdit { index, field: "color", value }) + .size(11).width(100), + text_input("ByLayer", &element[2]) + .on_input(move |value| Message::MlStyleElementEdit { index, field: "linetype", value }) + .size(11).width(170), + button(text(t!("Delete")).size(10)) + .on_press(Message::MlStyleElementDelete(index)) + .padding([4, 10]), + ] + .spacing(6) + .align_y(iced::Center), + ); + } + items.into() + } + (_, Some(style)) => column![ + input_row(t!("Description"), "", v.description, "description"), + text(t!("Fill and joints")).size(11).style(primary_style), + row![ + toggle(t!("Fill enabled"), style.flags.fill_on, "fill"), + toggle(t!("Display joints"), style.flags.display_joints, "joints"), + ].spacing(18), + input_row(t!("Fill color"), "256", v.fill_color, "fill_color"), + text(t!("Start caps")).size(11).style(primary_style), + row![ + toggle(t!("Line"), style.flags.start_square_cap, "start_square"), + toggle(t!("Inner arcs"), style.flags.start_inner_arcs_cap, "start_inner"), + toggle(t!("Outer arc"), style.flags.start_round_cap, "start_round"), + ].spacing(18), + input_row(t!("Start angle"), "90", v.start_angle, "start_angle"), + text(t!("End caps")).size(11).style(primary_style), + row![ + toggle(t!("Line"), style.flags.end_square_cap, "end_square"), + toggle(t!("Inner arcs"), style.flags.end_inner_arcs_cap, "end_inner"), + toggle(t!("Outer arc"), style.flags.end_round_cap, "end_round"), + ].spacing(18), + input_row(t!("End angle"), "90", v.end_angle, "end_angle"), + ].spacing(9).into(), }; - - let right_panel = container(details) - .width(sizing.width) - .height(sizing.height); - + let preview: Element<'a, Message> = if let Some(style) = style { + canvas(MLinePreviewCanvas { + description: v.description.to_string(), + start_angle: v.start_angle.to_string(), + end_angle: v.end_angle.to_string(), + fill_color: v.fill_color.to_string(), + fill: style.flags.fill_on, + joints: style.flags.display_joints, + start_square: style.flags.start_square_cap, + start_inner: style.flags.start_inner_arcs_cap, + start_round: style.flags.start_round_cap, + end_square: style.flags.end_square_cap, + end_inner: style.flags.end_inner_arcs_cap, + end_round: style.flags.end_round_cap, + elements: v.elements.to_vec(), + }) + .width(Length::Fill) + .height(150) + .into() + } else { + container(text(t!("No preview"))).height(150).into() + }; + let status = if v.selected.eq_ignore_ascii_case(&v.current) { + t!("Current style") + } else if v.in_use { + t!("Used in drawing") + } else { + t!("Available style") + }; + let summary = if v.comparison_sections.is_empty() { + t!("No differences").into_owned() + } else { + format!("{}: {}", t!("Different"), v.comparison_sections.join(", ")) + }; + let editor = crate::ui::style::style_manager::editor_shell( + crate::ui::style::style_manager::EditorShell { + sizing, + selected: v.selected.to_string(), + status, + preview, + comparison: Some(crate::ui::style::style_manager::EditorComparison { + selected: v.compare_name, + options: v.compare_opts, + summary, + on_select: Message::MlStyleDialogCompare, + }), + tabs: vec![ + crate::ui::style::style_manager::EditorTab { + label: t!("Elements"), + active: v.tab == 0, + on_press: Message::MlStyleDialogTab(0), + }, + crate::ui::style::style_manager::EditorTab { + label: t!("Caps and Fill"), + active: v.tab == 1, + on_press: Message::MlStyleDialogTab(1), + }, + ], + content, + }, + ); crate::ui::style::style_manager::view(crate::ui::style::style_manager::Scaffold { sizing, kind: crate::app::StyleKind::MLine, - styles: &styles, - selected, - current: Some(current_style.as_str()), - rename_active, - rename_buf, + styles: &v.styles, + selected: v.selected, + current: Some(v.current.as_str()), + rename_active: v.rename_active, + rename_buf: v.rename_buf, on_new: Message::MlStyleDialogNew, on_copy: Message::MlStyleDialogCopy, on_delete: Message::MlStyleDialogDelete, on_select: Message::MlStyleDialogSelect, on_set_current: Message::MlStyleDialogSetCurrent, on_apply: Message::MlStyleApply, - editor: right_panel.into(), + read_only: false, + editor, }) } diff --git a/src/ui/style/style_list.rs b/src/ui/style/style_list.rs index b83afda76..64a7ef4b9 100644 --- a/src/ui/style/style_list.rs +++ b/src/ui/style/style_list.rs @@ -29,6 +29,7 @@ pub fn item<'a>( on_select: Message, rename_active: Option<&str>, rename_buf: &'a str, + can_rename: bool, ) -> Element<'a, Message> { if rename_active == Some(name) { text_input("", rename_buf) @@ -56,9 +57,12 @@ pub fn item<'a>( ..Default::default() } }); - mouse_area(cell) - .on_press(on_select) - .on_double_click(Message::StyleRenameStart(kind, name.to_string())) - .into() + let area = mouse_area(cell).on_press(on_select); + if can_rename { + area.on_double_click(Message::StyleRenameStart(kind, name.to_string())) + .into() + } else { + area.into() + } } } diff --git a/src/ui/style/style_manager.rs b/src/ui/style/style_manager.rs index 88477b102..6b8041f10 100644 --- a/src/ui/style/style_manager.rs +++ b/src/ui/style/style_manager.rs @@ -10,7 +10,7 @@ use crate::app::{Message, StyleKind}; use iced::widget::button::{Status, Style}; use iced::widget::{button, column, container, row, scrollable, text, Space}; -use iced::{Background, Border, Element, Theme}; +use iced::{Background, Border, Element, Length, Theme}; use crate::t; use std::borrow::Cow; @@ -45,6 +45,9 @@ pub struct Scaffold<'a, 'b> { pub on_set_current: Message, /// "Apply" action (right side, primary). Every manager has one. pub on_apply: Message, + /// Referenced styles may be copied but not changed, renamed, deleted, or + /// made current. + pub read_only: bool, pub editor: Element<'a, Message>, } @@ -55,10 +58,10 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> { let bar = row![ tb_button(t!("New"), s.on_new, false), tb_button(t!("Copy"), s.on_copy, false), - tb_button(t!("Delete"), s.on_delete, false), + tb_button_enabled(t!("Delete"), s.on_delete, false, !s.read_only), Space::new().width(width), - tb_button(t!("Set Current"), s.on_set_current, false), - tb_button(t!("Apply"), s.on_apply, true), + tb_button_enabled(t!("Set Current"), s.on_set_current, false, !s.read_only), + tb_button_enabled(t!("Apply"), s.on_apply, true, !s.read_only), ] .spacing(4) .align_y(iced::Center); @@ -87,6 +90,7 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> { (s.on_select)(name.clone()), s.rename_active, s.rename_buf, + !(s.read_only && is_sel), ) }) .collect(); @@ -137,6 +141,102 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> { .into() } +pub struct EditorTab { + pub label: Cow<'static, str>, + pub active: bool, + pub on_press: Message, +} + +pub struct EditorComparison { + pub selected: String, + pub options: Vec, + pub summary: String, + pub on_select: fn(String) -> Message, +} + +/// Shared right-hand editor composition used by every named style manager. +/// The preview stays visible while the active property page scrolls. +pub struct EditorShell<'a> { + pub sizing: crate::ui::modal::ModalSizing, + pub selected: String, + pub status: Cow<'static, str>, + pub preview: Element<'a, Message>, + pub comparison: Option, + pub tabs: Vec, + pub content: Element<'a, Message>, +} + +pub fn editor_shell<'a>(shell: EditorShell<'a>) -> Element<'a, Message> { + let tabs: Vec> = shell + .tabs + .into_iter() + .map(|tab| { + button(text(tab.label).size(11)) + .on_press(tab.on_press) + .style(tab_button_style(tab.active)) + .padding([4, 10]) + .into() + }) + .collect(); + let comparison: Element<'a, Message> = match shell.comparison { + Some(comparison) if !comparison.options.is_empty() => row![ + text(t!("Compare with")).size(10).style(muted_text_style), + iced::widget::pick_list( + Some(comparison.selected), + comparison.options, + |value| value.to_string(), + ) + .on_select(comparison.on_select) + .text_size(11) + .width(150), + text(comparison.summary).size(10).style(muted_text_style), + ] + .spacing(8) + .align_y(iced::Center) + .into(), + _ => Space::new().height(0).into(), + }; + let preview = container(shell.preview) + .width(Length::Fill) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.weak.color)), + border: Border { + color: theme.palette().background.neutral.color, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + }); + container( + column![ + row![ + text(shell.selected).size(13).style(primary_text_style), + Space::new().width(Length::Fill), + text(shell.status).size(10).style(muted_text_style), + ] + .align_y(iced::Center), + preview, + comparison, + row(tabs).spacing(2), + hdivider(Length::Fill), + scrollable(container(shell.content).padding([12, 12]).width(Length::Fill)) + .width(Length::Fill) + .height(Length::Fill), + ] + .spacing(6) + .height(shell.sizing.height), + ) + .height(shell.sizing.height) + .width(Length::Fill) + .padding(iced::Padding { + top: 12.0, + right: 0.0, + bottom: 12.0, + left: 0.0, + }) + .into() +} + // ── Shared chrome ────────────────────────────────────────────────────────── pub(crate) fn tb_button( @@ -152,10 +252,55 @@ pub(crate) fn tb_button( .into() } +pub(crate) fn tb_button_enabled( + label: impl Into>, + msg: Message, + accent: bool, + enabled: bool, +) -> Element<'static, Message> { + let pad = if accent { [4, 14] } else { [4, 10] }; + let button = button(text(label.into()).size(11)) + .style(btn_s(accent)) + .padding(pad); + if enabled { + button.on_press(msg).into() + } else { + button.into() + } +} + +fn tab_button_style(active: bool) -> impl Fn(&Theme, Status) -> Style { + move |theme: &Theme, status| { + let palette = theme.palette(); + let pair = match (active, status) { + (true, _) => palette.primary.strong, + (false, Status::Hovered | Status::Pressed) => palette.background.strong, + _ => palette.background.weak, + }; + Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, + border: Border { + color: palette.background.neutral.color, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + } + } +} + +fn primary_text_style(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.palette().primary.base.color), + } +} + fn btn_s(accent: bool) -> impl Fn(&Theme, Status) -> Style { move |theme: &Theme, st| { let palette = theme.palette(); let pair = match (accent, st) { + (_, Status::Disabled) => palette.background.weak, (true, Status::Hovered | Status::Pressed) => palette.primary.strong, (false, Status::Hovered | Status::Pressed) => palette.background.strong, (true, _) => palette.primary.base, @@ -163,7 +308,11 @@ fn btn_s(accent: bool) -> impl Fn(&Theme, Status) -> Style { }; Style { background: Some(Background::Color(pair.color)), - text_color: pair.text, + text_color: if st == Status::Disabled { + pair.text.scale_alpha(0.45) + } else { + pair.text + }, border: Border { color: palette.background.neutral.color, width: 1.0, diff --git a/src/ui/style/tablestyle.rs b/src/ui/style/tablestyle.rs index fe11570ae..ce067b57a 100644 --- a/src/ui/style/tablestyle.rs +++ b/src/ui/style/tablestyle.rs @@ -1,49 +1,60 @@ -//! Table Style Manager window — fills the entire OS window. +//! Table style manager. use crate::app::Message; -use iced::widget::{ - button, checkbox, column, container, row, scrollable, text, text_input, Column, -}; -use iced::{Background, Border, Element, Theme}; use crate::t; +use iced::widget::{canvas, checkbox, column, row, text, text_input, Column}; +use iced::{mouse, Color, Element, Length, Point, Rectangle, Size, Theme}; use std::borrow::Cow; use std::fmt; #[derive(Clone, Debug, Eq, PartialEq)] -struct EnumChoice(String); +struct EnumChoice { + code: String, + label: Cow<'static, str>, +} impl fmt::Display for EnumChoice { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(crate::i18n::translate(&self.0).as_ref()) + formatter.write_str(self.label.as_ref()) } } -fn enum_choices(values: &[&str]) -> Vec { - values.iter().map(|value| EnumChoice((*value).to_string())).collect() +fn choices(values: &[(&str, &str)]) -> Vec { + values + .iter() + .map(|(code, label)| EnumChoice { + code: (*code).to_string(), + label: crate::i18n::translate(label), + }) + .collect() } -fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |theme: &Theme, st| { - let palette = theme.palette(); - let pair = match (accent, st) { - (true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong, - (false, button::Status::Hovered | button::Status::Pressed) => { - palette.background.strong - } - (true, _) => palette.primary.base, - _ => palette.background.weak, - }; - button::Style { - background: Some(Background::Color(pair.color)), - text_color: pair.text, - border: Border { - color: palette.background.neutral.color, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - } - } +pub struct TableStyleView<'a> { + pub styles: Vec, + pub selected: &'a str, + pub current: &'a str, + pub style: Option<&'a acadrust::objects::TableStyle>, + pub tab: u8, + pub compare_name: String, + pub compare_opts: Vec, + pub comparison_sections: Vec, + pub in_use: bool, + pub hmargin: &'a str, + pub vmargin: &'a str, + pub description: &'a str, + pub cell_textstyle: &'a [String; 3], + pub cell_height: &'a [String; 3], + pub cell_textcolor: &'a [String; 3], + pub cell_fillcolor: &'a [String; 3], + pub cell_datatype: &'a [String; 3], + pub cell_unittype: &'a [String; 3], + pub cell_format: &'a [String; 3], + pub border_lw: &'a [[String; 6]; 3], + pub border_color: &'a [[String; 6]; 3], + pub border_spacing: &'a [[String; 6]; 3], + pub rename_active: Option<&'a str>, + pub rename_buf: &'a str, + pub color_open: Option<(u8, &'static str)>, } fn muted_style(theme: &Theme) -> iced::widget::text::Style { @@ -58,323 +69,492 @@ fn primary_style(theme: &Theme) -> iced::widget::text::Style { } } -pub fn view_window<'a>( - styles: Vec, - selected: &'a str, - current: &'a str, - selected_style: Option<&'a acadrust::objects::TableStyle>, - hmargin_buf: &'a str, - vmargin_buf: &'a str, - description_buf: &'a str, - cell_textstyle: &'a [String; 3], - cell_height: &'a [String; 3], - cell_textcolor: &'a [String; 3], - cell_fillcolor: &'a [String; 3], - cell_datatype: &'a [String; 3], - cell_unittype: &'a [String; 3], - cell_format: &'a [String; 3], - border_lw: &'a [[String; 6]; 3], - border_color: &'a [[String; 6]; 3], - border_spacing: &'a [[String; 6]; 3], - rename_active: Option<&'a str>, - rename_buf: &'a str, - color_open: Option<(u8, &'static str)>, - sizing: crate::ui::modal::ModalSizing, -) -> Element<'a, Message> { - // ── Right: Details panel ────────────────────────────────────────────── - let info_row = |label: Cow<'static, str>, val: String| -> Element<'_, Message> { - row![ - text(label).size(11).style(muted_style).width(160), - text(val).size(11), - ] - .spacing(8) - .align_y(iced::Center) - .into() - }; +#[derive(Clone)] +struct PreviewBorder { + color: String, + weight: String, + spacing: String, + double: bool, + hidden: bool, +} - let cell_editor = |row_label: Cow<'static, str>, - row: u8, - rs: &acadrust::objects::RowCellStyle| - -> Element<'a, Message> { - let r = row as usize; - let cell_in = |label: Cow<'static, str>, - placeholder: &'static str, - value: &'a str, - field: &'static str| - -> Element<'a, Message> { - row![ - text(label).size(11).style(muted_style).width(150), - text_input(placeholder, value) - .on_input(move |v| Message::TableStyleCellEdit { - row, - field, - value: v - }) - .size(11) - .width(100), - ] +#[derive(Clone)] +struct PreviewRow { + label: String, + text_style: String, + text_height: String, + text_color: String, + fill_color: String, + fill: bool, + alignment: String, + data_type: String, + unit_type: String, + format: String, + borders: [PreviewBorder; 6], +} + +struct TablePreviewCanvas { + rows: [PreviewRow; 3], + active_row: usize, + hmargin: String, + vmargin: String, + description: String, + flow_up: bool, + title_suppressed: bool, + header_suppressed: bool, + annotative: bool, +} + +fn aci_color(value: &str, fallback: Color) -> Color { + crate::ui::color_select::aci_string_to_color(value) + .rgb() + .map(|(r, g, b)| Color::from_rgb8(r, g, b)) + .unwrap_or(fallback) +} + +fn border_width(value: &str) -> f32 { + value + .trim() + .parse::() + .map(|value| (value.abs() / 35.0).clamp(0.7, 4.5)) + .unwrap_or(1.0) +} + +fn draw_line( + frame: &mut canvas::Frame, + a: Point, + b: Point, + border: &PreviewBorder, + fallback: Color, +) { + if border.hidden { + return; + } + let stroke = canvas::Stroke::default() + .with_color(aci_color(&border.color, fallback)) + .with_width(border_width(&border.weight)); + frame.stroke(&canvas::Path::line(a, b), stroke.clone()); + if border.double { + let spacing = border + .spacing + .trim() + .parse::() + .unwrap_or(1.0) + .abs() + .clamp(1.0, 5.0); + let dx = b.x - a.x; + let dy = b.y - a.y; + let length = (dx * dx + dy * dy).sqrt().max(1.0); + let offset = Point::new(-dy / length * spacing, dx / length * spacing); + frame.stroke( + &canvas::Path::line( + Point::new(a.x + offset.x, a.y + offset.y), + Point::new(b.x + offset.x, b.y + offset.y), + ), + stroke, + ); + } +} + +impl canvas::Program for TablePreviewCanvas { + type State = (); + + fn draw( + &self, + _state: &(), + renderer: &iced::Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec { + let mut frame = canvas::Frame::new(renderer, bounds.size()); + let ink = theme.palette().background.base.text; + let mut order = vec![2usize, 1, 0]; + if self.title_suppressed { + order.retain(|row| *row != 2); + } + if self.header_suppressed { + order.retain(|row| *row != 1); + } + if self.flow_up { + order.reverse(); + } + let hmargin = self.hmargin.trim().parse::().unwrap_or(1.5).abs(); + let vmargin = self.vmargin.trim().parse::().unwrap_or(1.5).abs(); + let left = 18.0 + (hmargin * 1.2).clamp(0.0, 18.0); + let right = bounds.width - left; + let top = 9.0 + (vmargin * 0.8).clamp(0.0, 12.0); + let bottom = bounds.height - 24.0; + let row_height = ((bottom - top) / order.len().max(1) as f32).max(18.0); + let col_width = (right - left) / 3.0; + for (display_row, row_index) in order.iter().copied().enumerate() { + let row = &self.rows[row_index]; + let y0 = top + display_row as f32 * row_height; + let y1 = y0 + row_height; + let fallback_fill = theme.palette().background.weak.color; + if row.fill { + frame.fill( + &canvas::Path::rectangle( + Point::new(left, y0), + Size::new(right - left, row_height), + ), + aci_color(&row.fill_color, fallback_fill), + ); + } + let borders = &row.borders; + draw_line(&mut frame, Point::new(left, y0), Point::new(left, y1), &borders[0], ink); + draw_line(&mut frame, Point::new(right, y0), Point::new(right, y1), &borders[1], ink); + draw_line(&mut frame, Point::new(left, y0), Point::new(right, y0), &borders[2], ink); + draw_line(&mut frame, Point::new(left, y1), Point::new(right, y1), &borders[3], ink); + for column in 1..3 { + let x = left + column as f32 * col_width; + draw_line(&mut frame, Point::new(x, y0), Point::new(x, y1), &borders[5], ink); + } + if display_row + 1 < order.len() { + draw_line(&mut frame, Point::new(left, y1), Point::new(right, y1), &borders[4], ink); + } + for column in 0..3 { + let label = if row.format.trim().is_empty() { + format!("{} {}", row.label, column + 1) + } else { + row.format.replace("{}", &(column + 1).to_string()) + }; + let horizontal = if row.alignment.ends_with("Left") { + iced::advanced::text::Alignment::Left + } else if row.alignment.ends_with("Right") { + iced::advanced::text::Alignment::Right + } else { + iced::advanced::text::Alignment::Center + }; + let x = match horizontal { + iced::advanced::text::Alignment::Left => left + column as f32 * col_width + 5.0, + iced::advanced::text::Alignment::Right => left + (column + 1) as f32 * col_width - 5.0, + iced::advanced::text::Alignment::Center => left + (column as f32 + 0.5) * col_width, + _ => left + (column as f32 + 0.5) * col_width, + }; + let y = if row.alignment.starts_with("Top") { + y0 + 7.0 + } else if row.alignment.starts_with("Bottom") { + y1 - 7.0 + } else { + (y0 + y1) * 0.5 + }; + frame.fill_text(canvas::Text { + content: label, + position: Point::new(x, y), + color: aci_color(&row.text_color, ink), + size: iced::Pixels( + row.text_height + .trim() + .parse::() + .map(|height| (height * 34.0).clamp(7.0, 16.0)) + .unwrap_or(10.0), + ), + align_x: horizontal, + align_y: iced::alignment::Vertical::Center, + ..Default::default() + }); + } + } + let active = &self.rows[self.active_row.min(2)]; + frame.fill_text(canvas::Text { + content: format!( + "{} · {} · type {} / unit {}{}{}", + active.text_style, + if self.annotative { t!("Annotative") } else { t!("Drawing units") }, + active.data_type, + active.unit_type, + if self.description.trim().is_empty() { "" } else { " · " }, + self.description, + ), + position: Point::new(10.0, bounds.height - 9.0), + color: ink.scale_alpha(0.65), + size: iced::Pixels(10.0), + align_y: iced::alignment::Vertical::Center, + ..Default::default() + }); + vec![frame.into_geometry()] + } +} + +fn input_row<'a>( + label: Cow<'static, str>, + placeholder: &'static str, + value: &'a str, + message: impl Fn(String) -> Message + 'a, +) -> Element<'a, Message> { + row![ + text(label).size(11).style(muted_style).width(155), + text_input(placeholder, value) + .on_input(message) + .size(11) + .width(170), + ] + .spacing(8) + .align_y(iced::Center) + .into() +} + +fn cell_editor<'a>(v: &TableStyleView<'a>, row_index: u8) -> Element<'a, Message> { + let Some(style) = v.style else { + return text(t!("Select a style to view details.")).into(); + }; + let row_style = match row_index { + 0 => &style.data_row_style, + 1 => &style.header_row_style, + _ => &style.title_row_style, + }; + let index = row_index as usize; + let cell_input = |label, placeholder, value: &'a str, field| { + input_row(label, placeholder, value, move |value| Message::TableStyleCellEdit { + row: row_index, + field, + value, + }) + }; + let cell_color = |label: Cow<'static, str>, value: &'a str, field: &'static str| { + let current = crate::ui::color_select::aci_string_to_color(value); + let selector = crate::ui::color_select::color_selector( + current, + v.color_open == Some((row_index, field)), + crate::ui::color_select::ColorExtras { by_layer: true, by_block: true }, + move |color| Message::TableStyleCellEdit { + row: row_index, + field, + value: crate::ui::color_select::color_to_aci_string(color), + }, + Message::TableColorMore(row_index, field), + Message::OpenColorWindow(crate::app::ColorPickTarget::Table(row_index, field), current), + ); + row![text(label).size(11).style(muted_style).width(155), selector] .spacing(8) .align_y(iced::Center) - .into() - }; - // Shared colour selector — sends the chosen colour through the same - // cell edit as an ACI string. - let cell_color = |label: Cow<'static, str>, value: &'a str, field: &'static str| -> Element<'a, Message> { - let cur = crate::ui::color_select::aci_string_to_color(value); - let open = color_open == Some((row, field)); - let selector = crate::ui::color_select::color_selector( - cur, - open, - crate::ui::color_select::ColorExtras { - by_layer: true, - by_block: true, - }, - move |c| Message::TableStyleCellEdit { - row, - field, - value: crate::ui::color_select::color_to_aci_string(c), - }, - Message::TableColorMore(row, field), - Message::OpenColorWindow( - crate::app::ColorPickTarget::Table(row, field), - cur, - ), - ); - row![text(label).size(11).style(muted_style).width(150), selector] - .spacing(8) - .align_y(iced::Center) - .into() - }; - let mut col = Column::new() - .spacing(3) - .push(text(row_label).size(11).style(primary_style)) - .push(cell_in( - t!(" Text style:"), - "Standard", - &cell_textstyle[r], - "textstyle", - )) - .push(cell_in(t!(" Text height:"), "0.18", &cell_height[r], "height")) - .push(cell_color(t!(" Text color:"), &cell_textcolor[r], "textcolor")) - .push(cell_color(t!(" Fill color:"), &cell_fillcolor[r], "fillcolor")) - .push( - row![ - text(t!(" Alignment:")).size(11).style(muted_style).width(150), - iced::widget::pick_list( - Some(EnumChoice(format!("{:?}", rs.alignment))), - enum_choices(&[ - "TopLeft", - "TopCenter", - "TopRight", - "MiddleLeft", - "MiddleCenter", - "MiddleRight", - "BottomLeft", - "BottomCenter", - "BottomRight", - ]), - |value| value.to_string(), - ) - .on_select(move |choice| Message::TableStyleCellSetAlign { - row, - value: choice.0, - }) - .text_size(11) - .width(140), - ] - .spacing(8) - .align_y(iced::Center), - ) - .push( - checkbox(rs.fill_enabled) - .label(t!(" Background fill enabled")) - .on_toggle(move |_| Message::TableStyleCellToggleFill(row)) - .size(14) - .text_size(11), - ) - .push(cell_in(t!(" Data type:"), "0", &cell_datatype[r], "datatype")) - .push(cell_in(t!(" Unit type:"), "0", &cell_unittype[r], "unittype")) - .push(cell_in(t!(" Format string:"), "", &cell_format[r], "format")) - .push( - text(t!(" Borders (type / weight / color / spacing / hidden)")) - .size(10) - .style(muted_style), - ); - - let borders: [(&'static str, &acadrust::objects::TableCellBorder); 6] = [ - ("L", &rs.left_border), - ("R", &rs.right_border), - ("T", &rs.top_border), - ("B", &rs.bottom_border), - ("H", &rs.horizontal_inside_border), - ("V", &rs.vertical_inside_border), - ]; - for (b, (bname, bd)) in borders.into_iter().enumerate() { - let bu = b as u8; - col = col.push( - row![ - text(format!(" {bname}")).size(11).style(muted_style).width(28), - iced::widget::pick_list( - Some(EnumChoice(format!("{:?}", bd.border_type))), - enum_choices(&["Single", "Double"]), - |value| value.to_string(), - ) - .on_select(move |choice| Message::TableStyleBorderSetType { - cell: row, - border: bu, - value: choice.0, - }) - .text_size(10) - .width(74), - text_input("", &border_lw[r][b]) - .on_input(move |v| Message::TableStyleBorderEdit { - cell: row, - border: bu, - field: "lw", - value: v - }) - .size(10) - .width(46), - text_input("", &border_color[r][b]) - .on_input(move |v| Message::TableStyleBorderEdit { - cell: row, - border: bu, - field: "color", - value: v - }) - .size(10) - .width(46), - text_input("", &border_spacing[r][b]) - .on_input(move |v| Message::TableStyleBorderEdit { - cell: row, - border: bu, - field: "spacing", - value: v - }) - .size(10) - .width(46), - checkbox(bd.is_invisible) - .on_toggle(move |_| Message::TableStyleBorderToggleInvisible { - cell: row, - border: bu - }) - .size(13), - ] - .spacing(5) - .align_y(iced::Center), - ); - } - - col.push( - button(text(t!("Apply cell")).size(11)) - .on_press(Message::TableStyleCellApply(row)) - .style(btn_s(true)) - .padding([4, 12]), - ) - .into() }; - - let details: Element<'_, Message> = if let Some(s) = selected_style { - scrollable( - column![ - info_row(t!("Name:"), s.name.clone()), - row![ - text(t!("Description:")).size(11).style(muted_style).width(160), - text_input("", description_buf) - .on_input(|v| Message::TableStyleEdit { - field: "description", - value: v - }) - .size(11) - .width(160), - ] - .spacing(8) - .align_y(iced::Center), - row![ - text(t!("Flow direction:")).size(11).style(muted_style).width(160), - iced::widget::pick_list( - Some(EnumChoice(format!("{:?}", s.flow_direction))), - enum_choices(&["Down", "Up"]), - |value| value.to_string(), - ) - .on_select(|choice| Message::TableStyleSetFlow(choice.0)) - .text_size(11) - .width(100), - ] - .spacing(8) - .align_y(iced::Center), - row![ - text(t!("H Margin:")).size(11).style(muted_style).width(160), - text_input("1.5", hmargin_buf) - .on_input(|v| Message::TableStyleEdit { - field: "hmargin", - value: v - }) - .size(11) - .width(100), - ] - .spacing(8) - .align_y(iced::Center), - row![ - text(t!("V Margin:")).size(11).style(muted_style).width(160), - text_input("1.5", vmargin_buf) - .on_input(|v| Message::TableStyleEdit { - field: "vmargin", - value: v - }) - .size(11) - .width(100), - ] - .spacing(8) - .align_y(iced::Center), - checkbox(s.title_suppressed) - .label(t!("Title row suppressed")) - .on_toggle(|_| Message::TableStyleToggle("title_sup")) - .size(14) - .text_size(11), - checkbox(s.header_suppressed) - .label(t!("Header row suppressed")) - .on_toggle(|_| Message::TableStyleToggle("header_sup")) - .size(14) - .text_size(11), - button(text(t!("Apply margins")).size(11)) - .on_press(Message::TableStyleApply) - .style(btn_s(true)) - .padding([4, 12]), - cell_editor(t!("Data Row:"), 0, &s.data_row_style), - cell_editor(t!("Header Row:"), 1, &s.header_row_style), - cell_editor(t!("Title Row:"), 2, &s.title_row_style), + let selected_alignment = format!("{:?}", row_style.alignment); + let mut content = Column::new() + .spacing(7) + .push(cell_input(t!("Text style"), "Standard", &v.cell_textstyle[index], "textstyle")) + .push(cell_input(t!("Text height"), "0.18", &v.cell_height[index], "height")) + .push(cell_color(t!("Text color"), &v.cell_textcolor[index], "textcolor")) + .push(cell_color(t!("Fill color"), &v.cell_fillcolor[index], "fillcolor")) + .push( + row![ + text(t!("Alignment")).size(11).style(muted_style).width(155), + iced::widget::pick_list( + Some(EnumChoice { code: selected_alignment.clone(), label: crate::i18n::translate(&selected_alignment) }), + choices(&[ + ("TopLeft", "Top left"), ("TopCenter", "Top center"), ("TopRight", "Top right"), + ("MiddleLeft", "Middle left"), ("MiddleCenter", "Middle center"), ("MiddleRight", "Middle right"), + ("BottomLeft", "Bottom left"), ("BottomCenter", "Bottom center"), ("BottomRight", "Bottom right"), + ]), + |value| value.to_string(), + ) + .on_select(move |choice| Message::TableStyleCellSetAlign { row: row_index, value: choice.code }) + .text_size(11) + .width(170), ] - .spacing(6) - .padding([12, 12]), + .spacing(8) + .align_y(iced::Center), ) - .width(sizing.width) - .height(sizing.height) - .into() - } else { - container(text(t!("Select a style to view details.")).size(11).style(muted_style)) - .padding([12, 12]) - .into() + .push( + checkbox(row_style.fill_enabled) + .label(t!("Background fill enabled")) + .on_toggle(move |_| Message::TableStyleCellToggleFill(row_index)) + .size(14) + .text_size(11), + ) + .push(cell_input(t!("Data type"), "0", &v.cell_datatype[index], "datatype")) + .push(cell_input(t!("Unit type"), "0", &v.cell_unittype[index], "unittype")) + .push(cell_input(t!("Format string"), "", &v.cell_format[index], "format")) + .push(text(t!("Borders")).size(11).style(primary_style)); + let borders = [ + (t!("Left"), &row_style.left_border), + (t!("Right"), &row_style.right_border), + (t!("Top"), &row_style.top_border), + (t!("Bottom"), &row_style.bottom_border), + (t!("Inside horizontal"), &row_style.horizontal_inside_border), + (t!("Inside vertical"), &row_style.vertical_inside_border), + ]; + for (border_index, (label, border)) in borders.into_iter().enumerate() { + let border_index = border_index as u8; + let border_type = format!("{:?}", border.border_type); + content = content.push( + row![ + text(label).size(10).style(muted_style).width(96), + iced::widget::pick_list( + Some(EnumChoice { code: border_type.clone(), label: crate::i18n::translate(&border_type) }), + choices(&[("Single", "Single"), ("Double", "Double")]), + |value| value.to_string(), + ) + .on_select(move |choice| Message::TableStyleBorderSetType { cell: row_index, border: border_index, value: choice.code }) + .text_size(10) + .width(82), + text_input(t!("Weight").as_ref(), &v.border_lw[index][border_index as usize]) + .on_input(move |value| Message::TableStyleBorderEdit { cell: row_index, border: border_index, field: "lw", value }) + .size(10).width(68), + text_input(t!("Color").as_ref(), &v.border_color[index][border_index as usize]) + .on_input(move |value| Message::TableStyleBorderEdit { cell: row_index, border: border_index, field: "color", value }) + .size(10).width(62), + text_input(t!("Spacing").as_ref(), &v.border_spacing[index][border_index as usize]) + .on_input(move |value| Message::TableStyleBorderEdit { cell: row_index, border: border_index, field: "spacing", value }) + .size(10).width(68), + checkbox(border.is_invisible) + .label(t!("Hidden")) + .on_toggle(move |_| Message::TableStyleBorderToggleInvisible { cell: row_index, border: border_index }) + .size(13).text_size(10), + ] + .spacing(5) + .align_y(iced::Center), + ); + } + content.into() +} + +fn preview_rows(v: &TableStyleView<'_>) -> Option<[PreviewRow; 3]> { + let style = v.style?; + let source = [&style.data_row_style, &style.header_row_style, &style.title_row_style]; + Some(std::array::from_fn(|row| { + let borders = [ + &source[row].left_border, + &source[row].right_border, + &source[row].top_border, + &source[row].bottom_border, + &source[row].horizontal_inside_border, + &source[row].vertical_inside_border, + ]; + PreviewRow { + label: [t!("Data"), t!("Header"), t!("Title")][row].to_string(), + text_style: v.cell_textstyle[row].clone(), + text_height: v.cell_height[row].clone(), + text_color: v.cell_textcolor[row].clone(), + fill_color: v.cell_fillcolor[row].clone(), + fill: source[row].fill_enabled, + alignment: format!("{:?}", source[row].alignment), + data_type: v.cell_datatype[row].clone(), + unit_type: v.cell_unittype[row].clone(), + format: v.cell_format[row].clone(), + borders: std::array::from_fn(|border| PreviewBorder { + color: v.border_color[row][border].clone(), + weight: v.border_lw[row][border].clone(), + spacing: v.border_spacing[row][border].clone(), + double: format!("{:?}", borders[border].border_type) == "Double", + hidden: borders[border].is_invisible, + }), + } + })) +} + +pub fn view_window<'a>( + v: TableStyleView<'a>, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let style = v.style; + let content: Element<'a, Message> = match (v.tab, style) { + (_, None) => text(t!("Select a style to view details.")).size(11).into(), + (0, Some(style)) => column![ + input_row(t!("Description"), "", v.description, |value| Message::TableStyleEdit { field: "description", value }), + row![ + text(t!("Flow direction")).size(11).style(muted_style).width(155), + iced::widget::pick_list( + Some(EnumChoice { code: format!("{:?}", style.flow_direction), label: crate::i18n::translate(&format!("{:?}", style.flow_direction)) }), + choices(&[("Down", "Top to bottom"), ("Up", "Bottom to top")]), + |value| value.to_string(), + ) + .on_select(|choice| Message::TableStyleSetFlow(choice.code)) + .text_size(11).width(170), + ] + .spacing(8).align_y(iced::Center), + input_row(t!("Horizontal margin"), "1.5", v.hmargin, |value| Message::TableStyleEdit { field: "hmargin", value }), + input_row(t!("Vertical margin"), "1.5", v.vmargin, |value| Message::TableStyleEdit { field: "vmargin", value }), + checkbox(style.title_suppressed).label(t!("Suppress title row")).on_toggle(|_| Message::TableStyleToggle("title_sup")).size(14).text_size(11), + checkbox(style.header_suppressed).label(t!("Suppress header row")).on_toggle(|_| Message::TableStyleToggle("header_sup")).size(14).text_size(11), + checkbox(style.annotative).label(t!("Annotative")).on_toggle(|_| Message::TableStyleToggleAnnotative).size(14).text_size(11), + ] + .spacing(9) + .into(), + (tab, Some(_)) => cell_editor(&v, tab.saturating_sub(1).min(2)), }; - - let right_panel = container(details) - .width(sizing.width) - .height(sizing.height); - + let rows = preview_rows(&v).unwrap_or_else(|| { + std::array::from_fn(|row| PreviewRow { + label: ["Data", "Header", "Title"][row].to_string(), + text_style: String::new(), + text_height: String::new(), + text_color: String::new(), + fill_color: String::new(), + fill: false, + alignment: "MiddleCenter".to_string(), + data_type: String::new(), + unit_type: String::new(), + format: String::new(), + borders: std::array::from_fn(|_| PreviewBorder { + color: String::new(), weight: String::new(), spacing: String::new(), double: false, hidden: false, + }), + }) + }); + let preview = canvas(TablePreviewCanvas { + rows, + active_row: v.tab.saturating_sub(1).min(2) as usize, + hmargin: v.hmargin.to_string(), + vmargin: v.vmargin.to_string(), + description: v.description.to_string(), + flow_up: style.is_some_and(|style| format!("{:?}", style.flow_direction) == "Up"), + title_suppressed: style.is_some_and(|style| style.title_suppressed), + header_suppressed: style.is_some_and(|style| style.header_suppressed), + annotative: style.is_some_and(|style| style.annotative), + }) + .width(Length::Fill) + .height(150); + let status = if v.selected.eq_ignore_ascii_case(v.current) { + t!("Current style") + } else if v.in_use { + t!("Used in drawing") + } else { + t!("Available style") + }; + let summary = if v.comparison_sections.is_empty() { + t!("No differences").into_owned() + } else { + format!("{}: {}", t!("Different"), v.comparison_sections.join(", ")) + }; + let editor = crate::ui::style::style_manager::editor_shell( + crate::ui::style::style_manager::EditorShell { + sizing, + selected: v.selected.to_string(), + status, + preview: preview.into(), + comparison: Some(crate::ui::style::style_manager::EditorComparison { + selected: v.compare_name, + options: v.compare_opts, + summary, + on_select: Message::TableStyleDialogCompare, + }), + tabs: [t!("General"), t!("Data Row"), t!("Header Row"), t!("Title Row")] + .into_iter() + .enumerate() + .map(|(tab, label)| crate::ui::style::style_manager::EditorTab { + label, + active: v.tab == tab as u8, + on_press: Message::TableStyleDialogTab(tab as u8), + }) + .collect(), + content, + }, + ); crate::ui::style::style_manager::view(crate::ui::style::style_manager::Scaffold { sizing, kind: crate::app::StyleKind::Table, - styles: &styles, - selected, - current: Some(current), - rename_active, - rename_buf, + styles: &v.styles, + selected: v.selected, + current: Some(v.current), + rename_active: v.rename_active, + rename_buf: v.rename_buf, on_new: Message::TableStyleDialogNew, on_copy: Message::TableStyleDialogCopy, on_delete: Message::TableStyleDialogDelete, on_select: Message::TableStyleDialogSelect, on_set_current: Message::TableStyleDialogSetCurrent, on_apply: Message::TableStyleApply, - editor: right_panel.into(), + read_only: false, + editor, }) } diff --git a/src/ui/style/textstyle.rs b/src/ui/style/textstyle.rs index bc2c11704..bf0b9408e 100644 --- a/src/ui/style/textstyle.rs +++ b/src/ui/style/textstyle.rs @@ -1,20 +1,21 @@ -//! Text Style Font Browser window — fills the entire OS window. +//! Text style manager. -use crate::app::Message; -use crate::app::StyleKind; -use iced::widget::{ - button, canvas, checkbox, column, container, row, scrollable, text, text_input, Space, -}; -use iced::{mouse, Background, Border, Element, Length, Point, Rectangle, Theme}; +use crate::app::{Message, StyleKind}; use crate::t; +use iced::widget::{button, canvas, checkbox, column, container, row, scrollable, text, text_input}; +use iced::{mouse, Background, Border, Element, Length, Point, Rectangle, Theme}; use std::borrow::Cow; -/// View-model for the Text Style editor window. pub struct TextStyleView<'a> { pub styles: Vec, pub selected: &'a str, - /// Name of the current text style (marked with ✓ in the list). pub current: &'a str, + pub tab: u8, + pub compare_name: String, + pub compare_opts: Vec, + pub comparison_sections: Vec, + pub read_only: bool, + pub in_use: bool, pub font_buf: &'a str, pub width_buf: &'a str, pub oblique_buf: &'a str, @@ -23,57 +24,18 @@ pub struct TextStyleView<'a> { pub ttf_buf: &'a str, pub backward: bool, pub upside_down: bool, + pub vertical: bool, pub annotative: bool, - /// Name of the style being renamed inline (double-clicked), if any. pub rename_active: Option<&'a str>, - /// Edit buffer for the inline rename text input. pub rename_buf: &'a str, } const BUILTIN_FONTS: &[&str] = &[ - "Standard", "ISO", "Simplex", "RomanS", "RomanD", "RomanC", "RomanT", "ItalicC", "ItalicT", - "ScriptS", "ScriptC", "GothGBT", "GothGRT", "GothITT", "GreekC", "Symbol", - "ISO3098", "Unicode", + "Standard", "ISO", "Simplex", "RomanS", "RomanD", "RomanC", "RomanT", "ItalicC", + "ItalicT", "ScriptS", "ScriptC", "GothGBT", "GothGRT", "GothITT", "GreekC", + "Symbol", "ISO3098", "Unicode", ]; -fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |theme: &Theme, st| { - let palette = theme.palette(); - let pair = match (active, st) { - (true, _) => Some(palette.primary.strong), - (false, button::Status::Hovered | button::Status::Pressed) => { - Some(palette.background.strong) - } - _ => None, - }; - button::Style { - background: pair.map(|p| Background::Color(p.color)), - text_color: pair.map(|p| p.text).unwrap_or(palette.background.base.text), - ..Default::default() - } - } -} - -fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style { - let palette = theme.palette(); - let border = match status { - text_input::Status::Focused { .. } => palette.primary.base.color, - _ => palette.background.neutral.color, - }; - text_input::Style { - background: Background::Color(palette.background.base.color), - border: Border { - color: border, - width: 1.0, - radius: 3.0.into(), - }, - icon: palette.background.base.text, - placeholder: palette.background.base.text.scale_alpha(0.48), - value: palette.background.base.text, - selection: palette.primary.base.color.scale_alpha(0.5), - } -} - fn muted_style(theme: &Theme) -> iced::widget::text::Style { iced::widget::text::Style { color: Some(theme.palette().background.base.text.scale_alpha(0.68)), @@ -86,33 +48,55 @@ fn primary_style(theme: &Theme) -> iced::widget::text::Style { } } -fn vsep<'a>(height: Length) -> Element<'a, Message> { - container(Space::new().width(1).height(height)) - .width(1) - .height(height) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color( - theme.palette().background.neutral.color - )), +fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style { + let palette = theme.palette(); + text_input::Style { + background: Background::Color(palette.background.base.color), + border: Border { + color: if matches!(status, text_input::Status::Focused { .. }) { + palette.primary.base.color + } else { + palette.background.neutral.color + }, + width: 1.0, + radius: 3.0.into(), + }, + icon: palette.background.base.text, + placeholder: palette.background.base.text.scale_alpha(0.48), + value: palette.background.base.text, + selection: palette.primary.base.color.scale_alpha(0.5), + } +} + +fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style { + move |theme: &Theme, status| { + let palette = theme.palette(); + let pair = match (active, status) { + (true, _) => Some(palette.primary.strong), + (false, button::Status::Hovered | button::Status::Pressed) => { + Some(palette.background.strong) + } + _ => None, + }; + button::Style { + background: pair.map(|value| Background::Color(value.color)), + text_color: pair + .map(|value| value.text) + .unwrap_or(palette.background.base.text), ..Default::default() - }) - .into() + } + } } -/// Live sample-text preview. Tessellates a fixed sample string through the same -/// stroke path the text entities use (`lff::tessellate_text_ex`), so the chosen -/// font, width factor, oblique angle and backward/upside-down flags all show up, -/// then scales the result to fit the preview box (Y flipped: glyph space is -/// Y-up, screen is Y-down). Height is not bound — it only scales uniformly and -/// the fit cancels it. struct TextPreviewCanvas { font: String, - /// Negative mirrors left-right (backward). + big_font: String, width_factor: f32, - /// Radians. oblique: f32, - /// 0 or π (upside-down rotates 180° about the origin). - rotation: f32, + height: f32, + upside_down: bool, + vertical: bool, + annotative: bool, } impl canvas::Program for TextPreviewCanvas { @@ -127,314 +111,311 @@ impl canvas::Program for TextPreviewCanvas { _cursor: mouse::Cursor, ) -> Vec { let mut frame = canvas::Frame::new(renderer, bounds.size()); - let (strokes, _) = crate::scene::text::lff::tessellate_text_ex( - [0.0, 0.0], - 9.0, - self.rotation, - self.width_factor, - self.oblique, - &self.font, - "AaBbCc 0123", - ); + let ink = theme.palette().background.base.text; + let glyph_height = if self.height <= 0.0 { + 18.0 + } else { + (self.height * 7.0).clamp(6.0, 52.0) + }; + let rotation = if self.upside_down { + std::f32::consts::PI + } else { + 0.0 + }; + let strokes = if self.vertical { + let mut result = Vec::new(); + for (index, character) in "Ab3".chars().enumerate() { + let (mut glyph, _) = crate::scene::text::lff::tessellate_text_ex( + [0.0, -(index as f32) * glyph_height * 1.25], + glyph_height, + rotation, + self.width_factor, + self.oblique, + &self.font, + &character.to_string(), + ); + result.append(&mut glyph); + } + result + } else { + crate::scene::text::lff::tessellate_text_ex( + [0.0, 0.0], + glyph_height, + rotation, + self.width_factor, + self.oblique, + &self.font, + "AaBbCc 0123", + ) + .0 + }; let mut min = [f32::MAX; 2]; let mut max = [f32::MIN; 2]; - for s in &strokes { - for &[x, y] in s { + for stroke in &strokes { + for &[x, y] in stroke { min[0] = min[0].min(x); min[1] = min[1].min(y); max[0] = max[0].max(x); max[1] = max[1].max(y); } } - if min[0] > max[0] { - return vec![frame.into_geometry()]; - } - let pad = 10.0; - let span = [(max[0] - min[0]).max(1e-3), (max[1] - min[1]).max(1e-3)]; - let scale = ((bounds.width - 2.0 * pad) / span[0]) - .min((bounds.height - 2.0 * pad) / span[1]) - .max(0.0); - let mid = [(min[0] + max[0]) * 0.5, (min[1] + max[1]) * 0.5]; - let center = [bounds.width * 0.5, bounds.height * 0.5]; - let map = |x: f32, y: f32| { - Point::new( - center[0] + (x - mid[0]) * scale, - center[1] - (y - mid[1]) * scale, - ) - }; - let stroke = canvas::Stroke { - width: 1.4, - style: canvas::Style::Solid(theme.palette().background.base.text), - ..Default::default() - }; - for s in &strokes { - if s.len() < 2 { - continue; - } - let path = canvas::Path::new(|p| { - p.move_to(map(s[0][0], s[0][1])); - for &[x, y] in &s[1..] { - p.line_to(map(x, y)); + if min[0] <= max[0] { + let span = [(max[0] - min[0]).max(1.0), (max[1] - min[1]).max(1.0)]; + let available = [bounds.width - 24.0, bounds.height - 34.0]; + let scale = (available[0] / span[0]) + .min(available[1] / span[1]) + .min(1.0) + .max(0.0); + let middle = [(min[0] + max[0]) * 0.5, (min[1] + max[1]) * 0.5]; + let center = [bounds.width * 0.5, (bounds.height - 18.0) * 0.5]; + let map = |x: f32, y: f32| { + Point::new( + center[0] + (x - middle[0]) * scale, + center[1] - (y - middle[1]) * scale, + ) + }; + for stroke in &strokes { + if stroke.len() < 2 { + continue; } - }); - frame.stroke(&path, stroke.clone()); + let path = canvas::Path::new(|builder| { + builder.move_to(map(stroke[0][0], stroke[0][1])); + for &[x, y] in &stroke[1..] { + builder.line_to(map(x, y)); + } + }); + frame.stroke( + &path, + canvas::Stroke::default().with_color(ink).with_width(1.35), + ); + } } + let font_label = if self.big_font.trim().is_empty() { + self.font.clone() + } else { + format!("{} + {}", self.font, self.big_font) + }; + let mode = if self.annotative { + t!("Paper height") + } else { + t!("Drawing height") + }; + frame.fill_text(canvas::Text { + content: format!("{} · {} {:.3}", font_label, mode, self.height), + position: Point::new(10.0, bounds.height - 9.0), + color: ink.scale_alpha(0.65), + size: iced::Pixels(10.0), + align_y: iced::alignment::Vertical::Center, + ..Default::default() + }); vec![frame.into_geometry()] } } +fn input_row<'a>( + label: Cow<'static, str>, + placeholder: Cow<'static, str>, + value: &'a str, + field: &'static str, + read_only: bool, +) -> Element<'a, Message> { + let control: Element<'a, Message> = if read_only { + container(text(value).size(11)).padding([5, 7]).width(180).into() + } else { + text_input(placeholder.as_ref(), value) + .on_input(move |value| Message::TextStyleEdit { field, value }) + .style(field_style) + .size(11) + .width(180) + .into() + }; + row![text(label).size(11).style(muted_style).width(150), control] + .spacing(8) + .align_y(iced::Center) + .into() +} + +fn toggle<'a>( + label: Cow<'static, str>, + value: bool, + field: &'static str, + read_only: bool, +) -> Element<'a, Message> { + let checkbox = checkbox(value).label(label).size(14).text_size(11); + if read_only { + checkbox.into() + } else { + checkbox + .on_toggle(move |_| Message::TextStyleToggle(field)) + .into() + } +} + +fn font_list<'a>( + title: Cow<'static, str>, + values: Vec, + selected: &'a str, + system: bool, + read_only: bool, +) -> Element<'a, Message> { + let items: Vec> = values + .into_iter() + .map(|font| { + let active = selected.eq_ignore_ascii_case(&font); + let button = button(text(font.clone()).size(10)) + .style(list_item(active)) + .padding([3, 8]) + .width(Length::Fill); + if read_only { + button.into() + } else if system { + button + .on_press(Message::TextStyleEdit { + field: "ttf", + value: font, + }) + .into() + } else { + button.on_press(Message::TextStyleFontPick(font)).into() + } + }) + .collect(); + container(column![text(title).size(10).style(muted_style), scrollable(column(items).spacing(1)).height(250)].spacing(5)) + .width(Length::FillPortion(1)) + .padding(4) + .into() +} + pub fn view_window<'a>( v: TextStyleView<'a>, sizing: crate::ui::modal::ModalSizing, ) -> Element<'a, Message> { - let TextStyleView { - styles, - selected, - current, - font_buf, - width_buf, - oblique_buf, - height_buf, - bigfont_buf, - ttf_buf, - backward, - upside_down, - annotative, - rename_active, - rename_buf, - } = v; - // ── Middle: Font browser ────────────────────────────────────────────── - let font_items: Vec> = BUILTIN_FONTS - .iter() - .map(|&f| { - let is_sel = font_buf == f; - button(text(f).size(10)) - .on_press(Message::TextStyleFontPick(f.to_string())) - .style(list_item(is_sel)) - .padding([3, 8]) - .width(sizing.width) - .into() - }) - .collect(); - - let font_panel = container( - column![ - text(t!("Font File")).size(10).style(muted_style), - container(scrollable(column(font_items).spacing(1)).height(sizing.height)) - .style(|theme: &Theme| { - let palette = theme.palette(); - container::Style { - background: Some(Background::Color(palette.background.weak.color)), - border: Border { - color: palette.background.neutral.color, - width: 1.0, - radius: 3.0.into() - }, - ..Default::default() - } - }) - .width(sizing.width) - .height(sizing.height) - .padding(2), - text_input(t!("font file…").as_ref(), font_buf) - .on_input(|v| Message::TextStyleEdit { - field: "font", - value: v - }) - .style(field_style) - .size(11) - .width(sizing.width), - ] - .spacing(6) - .height(sizing.height), - ) - .width(190) - .height(sizing.height) - .padding([12, 8]); - - // Labeled numeric/text field row → TextStyleEdit { field, value }. - fn frow<'a>( - label: impl Into>, - ph: impl Into>, - buf: &'a str, - field: &'static str, - ) -> Element<'a, Message> { - row![ - text(label.into()).size(11).style(muted_style).width(120), - text_input(ph.into().as_ref(), buf) - .on_input(move |v| Message::TextStyleEdit { field, value: v }) - .style(field_style) - .size(11) - .width(140), - ] - .spacing(6) - .align_y(iced::Center) - .into() - } - - // Live preview reflecting the in-progress edits. Effective font follows the - // entity rule: TrueType name wins when set, else the stroke font file. - let eff_font = if !ttf_buf.trim().is_empty() { - ttf_buf + let effective_font = if v.ttf_buf.trim().is_empty() { + v.font_buf } else { - font_buf + v.ttf_buf }; - let prev_base_wf = width_buf + let width = v + .width_buf .trim() .parse::() .unwrap_or(1.0) .abs() .clamp(0.01, 100.0); let preview = canvas(TextPreviewCanvas { - font: eff_font.to_string(), - width_factor: if backward { -prev_base_wf } else { prev_base_wf }, - oblique: oblique_buf.trim().parse::().unwrap_or(0.0).to_radians(), - rotation: if upside_down { std::f32::consts::PI } else { 0.0 }, + font: effective_font.to_string(), + big_font: v.bigfont_buf.to_string(), + width_factor: if v.backward { -width } else { width }, + oblique: v.oblique_buf.trim().parse::().unwrap_or(0.0).to_radians(), + height: v.height_buf.trim().parse::().unwrap_or(0.0).max(0.0), + upside_down: v.upside_down, + vertical: v.vertical, + annotative: v.annotative, }) - .width(sizing.width) - .height(Length::Fixed(56.0)); + .width(Length::Fill) + .height(112); - // ── Right: Properties ───────────────────────────────────────────────── - let props_panel = container( - column![ - text(t!("Properties")).size(11).style(primary_style), - frow(t!("Big Font:"), t!("big-font file…"), bigfont_buf, "bigfont"), - frow(t!("TrueType Font:"), t!("e.g. Arial"), ttf_buf, "ttf"), - frow( - if annotative { - t!("Paper Text Height:") - } else { - t!("Fixed Height:") - }, - t!("0 = variable"), - height_buf, - "height", - ), - frow(t!("Width Factor:"), "1.0", width_buf, "width"), - frow(t!("Oblique (°):"), "0.0", oblique_buf, "oblique"), - row![ - checkbox(backward) - .label(t!("Backward")) - .on_toggle(|_| Message::TextStyleToggle("backward")) - .size(15) - .text_size(11), - checkbox(upside_down) - .label(t!("Upside down")) - .on_toggle(|_| Message::TextStyleToggle("upside_down")) - .size(15) - .text_size(11), + let content: Element<'a, Message> = match v.tab { + 0 => { + let built_in = BUILTIN_FONTS.iter().map(|value| (*value).to_string()).collect(); + let system = crate::scene::text::sysfont::families().to_vec(); + column![ + row![ + font_list(t!("Stroke fonts"), built_in, v.font_buf, false, v.read_only), + font_list(t!("System fonts"), system, v.ttf_buf, true, v.read_only), + ] + .spacing(10), + input_row(t!("Font file"), t!("font file…"), v.font_buf, "font", v.read_only), + input_row(t!("Big font"), t!("big-font file…"), v.bigfont_buf, "bigfont", v.read_only), + input_row(t!("System font"), t!("font family…"), v.ttf_buf, "ttf", v.read_only), ] - .spacing(16), - checkbox(annotative) - .label(t!("Annotative")) - .on_toggle(|_| Message::TextStyleToggle("annotative")) - .size(15) - .text_size(11), - Space::new().height(8), - text(t!("Preview")).size(10).style(muted_style), - container(preview) - .style(|theme: &Theme| { - let palette = theme.palette(); - container::Style { - background: Some(Background::Color(palette.background.base.color)), - border: Border { - color: palette.background.neutral.color, - width: 1.0, - radius: 4.0.into() - }, - ..Default::default() - } - }) - .padding(8) - .width(sizing.width), + .spacing(8) + .into() + } + _ => column![ + text(t!("Size")).size(11).style(primary_style), + input_row( + if v.annotative { t!("Paper text height") } else { t!("Fixed height") }, + t!("0 = variable"), + v.height_buf, + "height", + v.read_only, + ), + text(t!("Effects")).size(11).style(primary_style), + input_row(t!("Width factor"), "1.0".into(), v.width_buf, "width", v.read_only), + input_row(t!("Oblique angle"), "0.0".into(), v.oblique_buf, "oblique", v.read_only), + row![ + toggle(t!("Backward"), v.backward, "backward", v.read_only), + toggle(t!("Upside down"), v.upside_down, "upside_down", v.read_only), + toggle( + t!("Vertical"), + v.vertical, + "vertical", + v.read_only || !v.ttf_buf.trim().is_empty(), + ), + ] + .spacing(18), + toggle(t!("Annotative"), v.annotative, "annotative", v.read_only), ] - .spacing(10) - .height(sizing.height), - ) - .width(sizing.width) - .height(sizing.height) - .padding(iced::Padding { - top: 12.0, - right: 12.0, - bottom: 12.0, - left: 8.0, - }); - - // ── System TrueType fonts (read from the user's machine via fontdb) ── - let ttf_items: Vec> = crate::scene::text::sysfont::families() - .iter() - .map(|fam| { - let is_sel = ttf_buf.eq_ignore_ascii_case(fam); - button(text(fam).size(10)) - .on_press(Message::TextStyleEdit { - field: "ttf", - value: fam.clone(), - }) - .style(list_item(is_sel)) - .padding([3, 8]) - .width(sizing.width) - .into() - }) - .collect(); - - let ttf_panel = container( - column![ - text(t!("TrueType (system)")).size(10).style(muted_style), - container(scrollable(column(ttf_items).spacing(1)).height(sizing.height)) - .style(|theme: &Theme| { - let palette = theme.palette(); - container::Style { - background: Some(Background::Color(palette.background.weak.color)), - border: Border { - color: palette.background.neutral.color, - width: 1.0, - radius: 3.0.into() - }, - ..Default::default() - } - }) - .width(sizing.width) - .height(sizing.height) - .padding(2), - text_input(t!("TrueType font…").as_ref(), ttf_buf) - .on_input(|v| Message::TextStyleEdit { - field: "ttf", - value: v - }) - .style(field_style) - .size(11) - .width(sizing.width), - ] - .spacing(6) - .height(sizing.height), - ) - .width(190) - .height(sizing.height) - .padding([12, 8]); - - let editor = row![ - font_panel, - vsep(sizing.height), - ttf_panel, - vsep(sizing.height), - props_panel - ] - .height(sizing.height); - + .spacing(9) + .into(), + }; + let status = if v.read_only { + t!("Referenced style · Read only") + } else if v.selected.eq_ignore_ascii_case(v.current) { + t!("Current style") + } else if v.in_use { + t!("Used in drawing") + } else { + t!("Available style") + }; + let summary = if v.comparison_sections.is_empty() { + t!("No differences").into_owned() + } else { + format!("{}: {}", t!("Different"), v.comparison_sections.join(", ")) + }; + let editor = crate::ui::style::style_manager::editor_shell( + crate::ui::style::style_manager::EditorShell { + sizing, + selected: v.selected.to_string(), + status, + preview: preview.into(), + comparison: Some(crate::ui::style::style_manager::EditorComparison { + selected: v.compare_name, + options: v.compare_opts, + summary, + on_select: Message::TextStyleDialogCompare, + }), + tabs: vec![ + crate::ui::style::style_manager::EditorTab { + label: t!("Fonts"), + active: v.tab == 0, + on_press: Message::TextStyleDialogTab(0), + }, + crate::ui::style::style_manager::EditorTab { + label: t!("Size and Effects"), + active: v.tab == 1, + on_press: Message::TextStyleDialogTab(1), + }, + ], + content, + }, + ); crate::ui::style::style_manager::view(crate::ui::style::style_manager::Scaffold { sizing, kind: StyleKind::Text, - styles: &styles, - selected, - current: Some(current), - rename_active, - rename_buf, + styles: &v.styles, + selected: v.selected, + current: Some(v.current), + rename_active: v.rename_active, + rename_buf: v.rename_buf, on_new: Message::TextStyleDialogNew, on_copy: Message::TextStyleDialogCopy, on_delete: Message::TextStyleDialogDelete, on_select: Message::TextStyleDialogSelect, on_set_current: Message::TextStyleDialogSetCurrent, on_apply: Message::TextStyleApply, - editor: editor.into(), + read_only: v.read_only, + editor, }) }