From b3dd9c772aed39784529a80099d776923fbf32ca Mon Sep 17 00:00:00 2001 From: Raybipse Date: Wed, 15 Nov 2023 22:39:11 -0800 Subject: [PATCH 01/11] Changed to letterSpacing --- lib/src/view/design_main_dna_sequence.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/view/design_main_dna_sequence.dart b/lib/src/view/design_main_dna_sequence.dart index 26aedb26e..a77d0a2b9 100644 --- a/lib/src/view/design_main_dna_sequence.dart +++ b/lib/src/view/design_main_dna_sequence.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:over_react/over_react.dart'; import 'package:built_collection/built_collection.dart'; import 'package:platform_detect/platform_detect.dart'; +import 'package:react/react_client/react_interop.dart'; import 'package:scadnano/src/state/group.dart'; import 'package:scadnano/src/view/transform_by_helix_group.dart'; import 'package:tuple/tuple.dart'; @@ -134,6 +135,7 @@ class DesignMainDNASequenceComponent extends UiComponent2 Date: Sun, 17 Dec 2023 16:29:34 -0800 Subject: [PATCH 02/11] Setup separate export setting --- lib/src/actions/actions.dart | 21 +++++++++++++++++++++ lib/src/reducers/app_ui_state_reducer.dart | 4 ++++ lib/src/serializers.dart | 1 + lib/src/state/app_ui_state.dart | 2 ++ lib/src/state/app_ui_state_storables.dart | 3 +++ lib/src/view/menu.dart | 13 +++++++++++++ 6 files changed, 44 insertions(+) diff --git a/lib/src/actions/actions.dart b/lib/src/actions/actions.dart index fb83be0c4..b1190b329 100644 --- a/lib/src/actions/actions.dart +++ b/lib/src/actions/actions.dart @@ -2153,6 +2153,27 @@ abstract class ExportSvg with BuiltJsonSerializable implements Action, Built { + bool get export_svg_text_separately; + + /************************ begin BuiltValue boilerplate ************************/ + factory ExportSvgTextSeparatelySet(bool export_svg_text_separately) => + ExportSvgTextSeparatelySet.from((b) => b..export_svg_text_separately = export_svg_text_separately); + + /************************ begin BuiltValue boilerplate ************************/ + factory ExportSvgTextSeparatelySet.from([void Function(ExportSvgTextSeparatelySetBuilder) updates]) = + _$ExportSvgTextSeparatelySet; + + ExportSvgTextSeparatelySet._(); + + static Serializer get serializer => _$exportSvgTextSeparatelySetSerializer; +} + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // Strand part action diff --git a/lib/src/reducers/app_ui_state_reducer.dart b/lib/src/reducers/app_ui_state_reducer.dart index 0a29b397e..3c9756c3d 100644 --- a/lib/src/reducers/app_ui_state_reducer.dart +++ b/lib/src/reducers/app_ui_state_reducer.dart @@ -233,6 +233,9 @@ bool show_base_pair_lines_with_mismatches_reducer( bool _, actions.ShowBasePairLinesWithMismatchesSet action) => action.show_base_pair_lines_with_mismatches; +bool export_svg_text_separately_reducer(bool _, actions.ExportSvgTextSeparatelySet action) => + action.export_svg_text_separately; + bool display_major_tick_widths_reducer(bool _, actions.SetDisplayMajorTickWidths action) => action.show; bool strand_paste_keep_color_reducer(bool _, actions.StrandPasteKeepColorSet action) => action.keep; @@ -446,6 +449,7 @@ AppUIStateStorables app_ui_state_storable_local_reducer(AppUIStateStorables stor ..display_major_tick_widths_all_helices = TypedReducer(display_major_tick_widths_all_helices_reducer)(storables.display_major_tick_widths_all_helices, action) ..show_base_pair_lines = TypedReducer(show_base_pair_lines_reducer)(storables.show_base_pair_lines, action) ..show_base_pair_lines_with_mismatches = TypedReducer(show_base_pair_lines_with_mismatches_reducer)(storables.show_base_pair_lines_with_mismatches, action) + ..export_svg_text_separately = TypedReducer(export_svg_text_separately_reducer)(storables.export_svg_text_separately, action) ..only_display_selected_helices = TypedReducer(only_display_selected_helices_reducer)(storables.only_display_selected_helices, action) ..default_crossover_type_scaffold_for_setting_helix_rolls = TypedReducer(default_crossover_type_scaffold_for_setting_helix_rolls_reducer)(storables.default_crossover_type_scaffold_for_setting_helix_rolls, action) ..default_crossover_type_staple_for_setting_helix_rolls = TypedReducer(default_crossover_type_staple_for_setting_helix_rolls_reducer)(storables.default_crossover_type_staple_for_setting_helix_rolls, action) diff --git a/lib/src/serializers.dart b/lib/src/serializers.dart index cf843132f..5ee6c9b91 100644 --- a/lib/src/serializers.dart +++ b/lib/src/serializers.dart @@ -123,6 +123,7 @@ part 'serializers.g.dart'; ShowModificationsSet, ShowMismatchesSet, SetShowEditor, + ExportSvgTextSeparatelySet, SaveDNAFile, PrepareToLoadDNAFile, LoadDNAFile, diff --git a/lib/src/state/app_ui_state.dart b/lib/src/state/app_ui_state.dart index a4077c49a..1fef4b1d4 100644 --- a/lib/src/state/app_ui_state.dart +++ b/lib/src/state/app_ui_state.dart @@ -214,6 +214,8 @@ abstract class AppUIState with BuiltJsonSerializable implements Built storables.show_loopout_extension_length; + bool get export_svg_text_separately => storables.export_svg_text_separately; + bool get default_crossover_type_scaffold_for_setting_helix_rolls => storables.default_crossover_type_scaffold_for_setting_helix_rolls; diff --git a/lib/src/state/app_ui_state_storables.dart b/lib/src/state/app_ui_state_storables.dart index ef429f106..bc09c3be0 100644 --- a/lib/src/state/app_ui_state_storables.dart +++ b/lib/src/state/app_ui_state_storables.dart @@ -123,6 +123,8 @@ abstract class AppUIStateStorables bool get selection_box_intersection; + bool get export_svg_text_separately; + static void _initializeBuilder(AppUIStateStorablesBuilder b) { // This ensures that even if these keys are not in localStorage (e.g., due to upgrading), // then they will be populated with a default value instead of raising an exception. @@ -178,6 +180,7 @@ abstract class AppUIStateStorables b.clear_helix_selection_when_loading_new_design = false; b.show_mouseover_data = false; b.selection_box_intersection = false; + b.export_svg_text_separately = false; } /************************ begin BuiltValue boilerplate ************************/ diff --git a/lib/src/view/menu.dart b/lib/src/view/menu.dart index 5575f9f66..0016b0ca2 100644 --- a/lib/src/view/menu.dart +++ b/lib/src/view/menu.dart @@ -86,6 +86,7 @@ UiFactory ConnectedMenu = connect( ..show_grid_coordinates_side_view = state.ui_state.show_grid_coordinates_side_view ..show_helices_axis_arrows = state.ui_state.show_helices_axis_arrows ..show_loopout_extension_length = state.ui_state.show_loopout_extension_length + ..export_svg_text_separately = state.ui_state.export_svg_text_separately ..show_slice_bar = state.ui_state.show_slice_bar ..show_mouseover_data = state.ui_state.show_mouseover_data ..disable_png_caching_dna_sequences = state.ui_state.disable_png_caching_dna_sequences @@ -154,6 +155,7 @@ mixin MenuPropsMixin on UiProps { bool display_reverse_DNA_right_side_up; bool default_crossover_type_scaffold_for_setting_helix_rolls; bool default_crossover_type_staple_for_setting_helix_rolls; + bool export_svg_text_separately; LocalStorageDesignChoice local_storage_design_choice; bool clear_helix_selection_when_loading_new_design; bool show_slice_bar; @@ -1165,6 +1167,17 @@ debugging, but be warned that it will be very slow to render a large number of D ..on_click = ((_) => props.dispatch(actions.ExportSvg(type: actions.ExportSvgType.selected))) ..tooltip = "Export SVG figure of selected strands" ..display = 'SVG of selected strands')(), + (MenuBoolean() + ..value = props.export_svg_text_separately + ..display = 'export svg text separately' + ..tooltip = '''\ +When selected, every character of the text in a DNA sequence is exported separately. +This is useful to circumvent an SVG bug found in Microsoft tools such as PowerPoint.''' + ..name = 'export-svg-text-separately' + ..onChange = (_) { + props.dispatch(actions.ExportSvgTextSeparatelySet(!props.export_svg_text_separately)); + } + ..key = 'export-svg-text-separately')(), (MenuDropdownItem() ..on_click = ((_) => app.disable_keyboard_shortcuts_while(export_dna_sequences.export_dna)) ..tooltip = "Export DNA sequences of strands to a file." From 692e01dfa1d0fc7fbd0a625b26c903b9e1c246f1 Mon Sep 17 00:00:00 2001 From: RayBipse Date: Sun, 17 Dec 2023 16:30:00 -0800 Subject: [PATCH 03/11] moved set separate text menu --- lib/src/view/menu.dart | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/src/view/menu.dart b/lib/src/view/menu.dart index 0016b0ca2..eaec3d0f6 100644 --- a/lib/src/view/menu.dart +++ b/lib/src/view/menu.dart @@ -1167,6 +1167,16 @@ debugging, but be warned that it will be very slow to render a large number of D ..on_click = ((_) => props.dispatch(actions.ExportSvg(type: actions.ExportSvgType.selected))) ..tooltip = "Export SVG figure of selected strands" ..display = 'SVG of selected strands')(), + (MenuDropdownItem() + ..on_click = ((_) => app.disable_keyboard_shortcuts_while(export_dna_sequences.export_dna)) + ..tooltip = "Export DNA sequences of strands to a file." + ..display = 'DNA sequences')(), + (MenuDropdownItem() + ..on_click = ((_) => props.dispatch(actions.ExportCanDoDNA())) + ..tooltip = "Export design's DNA sequences as a CSV in the same way as cadnano v2.\n" + "This is useful, for example, with CanDo's atomic model generator." + ..display = 'DNA sequences (cadnano v2 format)')(), + DropdownDivider({'key': 'divider-export-svg-settings'}), (MenuBoolean() ..value = props.export_svg_text_separately ..display = 'export svg text separately' @@ -1178,15 +1188,6 @@ This is useful to circumvent an SVG bug found in Microsoft tools such as PowerPo props.dispatch(actions.ExportSvgTextSeparatelySet(!props.export_svg_text_separately)); } ..key = 'export-svg-text-separately')(), - (MenuDropdownItem() - ..on_click = ((_) => app.disable_keyboard_shortcuts_while(export_dna_sequences.export_dna)) - ..tooltip = "Export DNA sequences of strands to a file." - ..display = 'DNA sequences')(), - (MenuDropdownItem() - ..on_click = ((_) => props.dispatch(actions.ExportCanDoDNA())) - ..tooltip = "Export design's DNA sequences as a CSV in the same way as cadnano v2.\n" - "This is useful, for example, with CanDo's atomic model generator." - ..display = 'DNA sequences (cadnano v2 format)')(), DropdownDivider({'key': 'divider-not-full-design'}), (MenuDropdownItem() ..on_click = ((_) => props.dispatch(actions.ExportCadnanoFile(whitespace: true))) From a0d3ac2cd399ebfdba2ce42b3a64b100f61fea83 Mon Sep 17 00:00:00 2001 From: RayBipse Date: Mon, 18 Dec 2023 12:02:12 -0800 Subject: [PATCH 04/11] Added separate text support --- lib/src/middleware/export_svg.dart | 64 +++++++++++++++++++--- lib/src/view/design_main_dna_sequence.dart | 2 +- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/lib/src/middleware/export_svg.dart b/lib/src/middleware/export_svg.dart index 83d3f8636..1d9a6142d 100644 --- a/lib/src/middleware/export_svg.dart +++ b/lib/src/middleware/export_svg.dart @@ -2,8 +2,10 @@ import 'dart:html'; import 'dart:svg' as svg; import 'dart:svg'; +import 'package:over_react/over_react.dart'; import 'package:redux/redux.dart'; import 'package:scadnano/src/middleware/system_clipboard.dart'; +import 'package:scadnano/src/view/design_main_dna_sequence.dart'; import '../app.dart'; import '../state/app_state.dart'; @@ -45,11 +47,16 @@ export_svg_middleware(Store store, dynamic action, NextDispatcher next window.alert("No strands are selected, so there is nothing to export.\n" "Please select some strands before choosing this option."); } else { - var cloned_svg_element_with_style = get_cloned_svg_element_with_style(selected_elts); + var cloned_svg_element_with_style = get_cloned_svg_element_with_style( + selected_elts, store.state.ui_state.export_svg_text_separately); _export_from_element(cloned_svg_element_with_style, 'selected'); } - } else + } else { + if (store.state.ui_state.export_svg_text_separately) { + elt = separate_if_svg_text(clone_and_apply_style(elt)); + } _export_from_element(elt, 'main'); + } } if (action.type == actions.ExportSvgType.side || action.type == actions.ExportSvgType.both) { var elt = document.getElementById("side-view-svg"); @@ -67,6 +74,37 @@ export_svg_middleware(Store store, dynamic action, NextDispatcher next } } +// this directly modifies ele +Node separate_if_svg_text(Node ele) { + if (ele is TextElement && ele is SvgElement) { + double letterSpacing = double.tryParse(ele.getAttribute('letter-spacing') ?? "null"); + if (letterSpacing != null) { + List children = []; + List dna_seq = ele.text.split(""); + double x = double.parse(ele.getAttribute('x')); + for (var i = 0; i < dna_seq.length; ++i) { + var child = clone_and_apply_style(ele) + ..id = ele.id + '-n-${i}' + ..text = dna_seq[i]; + child.setAttribute('x', x.toString()); + child.setAttribute('y', ele.getAttribute('y')); + child.setAttribute('dominant-baseline', 'text-top'); + child.classes.add(DesignMainDNASequenceComponent.classname_dna_sequence); + children.add(child); + x += letterSpacing + DesignMainDNASequenceComponent.charWidth; + } + return SvgElement.tag("g")..children = children; + } + } + if (ele is Element) { + if (ele.hasChildNodes()) { + List nodes = ele.nodes.map(separate_if_svg_text).toList(); + ele.nodes = nodes; + } + } + return ele; +} + List get_selected_strands(Store store) { var selected_strands = store.state.ui_state.selectables_store.selected_strands; List selected_elts = []; @@ -81,9 +119,12 @@ List get_selected_strands(Store store) { return selected_elts; } -SvgSvgElement get_cloned_svg_element_with_style(List selected_elts) { +SvgSvgElement get_cloned_svg_element_with_style(List selected_elts, bool separate_text) { var cloned_svg_element_with_style = SvgSvgElement() ..children = selected_elts.map(clone_and_apply_style).toList(); + if (separate_text) { + selected_elts = selected_elts.map(separate_if_svg_text).map((x) => x as Element).toList(); + } // we can't get bbox without it being added to the DOM first document.body.append(cloned_svg_element_with_style); @@ -125,7 +166,7 @@ _export_svg(svg.SvgSvgElement svg_element, String filename_append) { } _copy_from_elements(List svg_elements) { - var cloned_svg_element_with_style = get_cloned_svg_element_with_style(svg_elements); + var cloned_svg_element_with_style = get_cloned_svg_element_with_style(svg_elements, false); util.copy_svg_as_png(cloned_svg_element_with_style); } @@ -196,6 +237,7 @@ Element clone_and_apply_style(Element elt_orig) { } clone_and_apply_style_rec(Element elt_styled, Element elt_orig, {int depth = 0}) { + // print('elt_styled ${elt_styled.id} and elt_orig ${elt_orig.id}'); // Set children_styled_to_remove = {}; var tag_name = elt_styled.tagName; @@ -208,7 +250,8 @@ clone_and_apply_style_rec(Element elt_styled, Element elt_orig, {int depth = 0}) if (relevant_styles.keys.contains(tag_name)) { var style_def = elt_orig.getComputedStyle(); - + // print( + // 'id ${elt_styled.id} fill ${style_def.getPropertyValue("fill")} relevant_styles ${relevant_styles[tag_name]}'); //TODO: figure out how to remove nodes that aren't visible; // getting error "Unsupported operation: Cannot setRange on filtered list" when removing children // if (style_def.visibility == 'hidden') { @@ -222,14 +265,17 @@ clone_and_apply_style_rec(Element elt_styled, Element elt_orig, {int depth = 0}) // correcting for this bug in InkScape that causes it to render hidden SVG objects: // https://bugs.launchpad.net/inkscape/+bug/1577763 if (style_name == 'visibility' && style_value == 'hidden') { - style_strings.add('display: none'); + // style_strings.add('display: none'); + elt_styled.style.setProperty('display', 'none'); } - style_strings.add('${style_name}: ${style_value}'); + elt_styled.style.setProperty(style_name, style_value); + // style_strings.add('${style_name}: ${style_value};'); } } - var style_string = style_strings.join('; ') + ';'; + // var style_string = style_strings.join(' '); - elt_styled.setAttribute("style", style_string); + // elt_styled.setAttribute("style", style_string); + // print(elt_styled.styleMap); // print('${' ' * depth * 2} ${tag_name} ${elt_orig.classes.toList()} style: $style_string'); } diff --git a/lib/src/view/design_main_dna_sequence.dart b/lib/src/view/design_main_dna_sequence.dart index a77d0a2b9..792e4e20d 100644 --- a/lib/src/view/design_main_dna_sequence.dart +++ b/lib/src/view/design_main_dna_sequence.dart @@ -92,6 +92,7 @@ class DesignMainDNASequenceComponent extends UiComponent2 Date: Sat, 27 Jan 2024 12:30:41 -0800 Subject: [PATCH 05/11] Fixed the frequent "cannot read undefined" error in console --- lib/src/util.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/util.dart b/lib/src/util.dart index 4100f1c96..bfa87ed68 100644 --- a/lib/src/util.dart +++ b/lib/src/util.dart @@ -1053,7 +1053,7 @@ num current_zoom(bool is_main) => is_main ? current_zoom_main_js() : current_zoo CssStyleSheet get_scadnano_stylesheet() { for (var stylesheet in document.styleSheets) { - if (stylesheet.href.contains(constants.scadnano_css_stylesheet_name)) { + if (stylesheet.href != null && stylesheet.href.contains(constants.scadnano_css_stylesheet_name)) { return stylesheet; } } From 63b66af1d6a7c1d74a38f29d72fe7a2c1595dbe2 Mon Sep 17 00:00:00 2001 From: RayBipse Date: Sat, 27 Jan 2024 13:01:16 -0800 Subject: [PATCH 06/11] Fixes #941 --- lib/src/middleware/export_svg.dart | 170 +++++++++++++++++++++++------ 1 file changed, 137 insertions(+), 33 deletions(-) diff --git a/lib/src/middleware/export_svg.dart b/lib/src/middleware/export_svg.dart index 1d9a6142d..2b908731e 100644 --- a/lib/src/middleware/export_svg.dart +++ b/lib/src/middleware/export_svg.dart @@ -1,6 +1,7 @@ import 'dart:html'; import 'dart:svg' as svg; import 'dart:svg'; +import 'dart:math' as math; import 'package:over_react/over_react.dart'; import 'package:redux/redux.dart'; @@ -53,7 +54,7 @@ export_svg_middleware(Store store, dynamic action, NextDispatcher next } } else { if (store.state.ui_state.export_svg_text_separately) { - elt = separate_if_svg_text(clone_and_apply_style(elt)); + elt = make_portable(clone_and_apply_style(elt)); } _export_from_element(elt, 'main'); } @@ -74,37 +75,6 @@ export_svg_middleware(Store store, dynamic action, NextDispatcher next } } -// this directly modifies ele -Node separate_if_svg_text(Node ele) { - if (ele is TextElement && ele is SvgElement) { - double letterSpacing = double.tryParse(ele.getAttribute('letter-spacing') ?? "null"); - if (letterSpacing != null) { - List children = []; - List dna_seq = ele.text.split(""); - double x = double.parse(ele.getAttribute('x')); - for (var i = 0; i < dna_seq.length; ++i) { - var child = clone_and_apply_style(ele) - ..id = ele.id + '-n-${i}' - ..text = dna_seq[i]; - child.setAttribute('x', x.toString()); - child.setAttribute('y', ele.getAttribute('y')); - child.setAttribute('dominant-baseline', 'text-top'); - child.classes.add(DesignMainDNASequenceComponent.classname_dna_sequence); - children.add(child); - x += letterSpacing + DesignMainDNASequenceComponent.charWidth; - } - return SvgElement.tag("g")..children = children; - } - } - if (ele is Element) { - if (ele.hasChildNodes()) { - List nodes = ele.nodes.map(separate_if_svg_text).toList(); - ele.nodes = nodes; - } - } - return ele; -} - List get_selected_strands(Store store) { var selected_strands = store.state.ui_state.selectables_store.selected_strands; List selected_elts = []; @@ -119,11 +89,145 @@ List get_selected_strands(Store store) { return selected_elts; } +List rotateVector(List vec, double ang) { + ang = ang * (math.pi / 180); + var cos = math.cos(ang); + var sin = math.sin(ang); + return [vec[0] * cos - vec[1] * sin, vec[0] * sin + vec[1] * cos]; +} + +// gets the height of a character in font in px +double get_text_height(String font) { + CanvasElement element = document.createElement("canvas"); + CanvasRenderingContext2D context = element.getContext("2d"); + context.font = font; + return double.tryParse(context.font.replaceAll(RegExp(r'[^0-9\.]'), '')); +} + +// returns a matrix that represents the change made by dominant-baseline css property +DomMatrix dominantBaselineMatrix(String dominantBaseline, double rot, String font) { + switch (dominantBaseline) { + case "ideographic": + return new DomMatrix([ + 1, + 0, + 0, + 1, + ...rotateVector([0, (-3 * get_text_height(font)) / 12], rot) + ]); + case "hanging": + return new DomMatrix([ + 1, + 0, + 0, + 1, + ...rotateVector([0, (9 * get_text_height(font)) / 12], rot) + ]); + case "central": + return new DomMatrix([ + 1, + 0, + 0, + 1, + ...rotateVector([0, (4 * get_text_height(font)) / 12], rot) + ]); + default: + return new DomMatrix([1, 0, 0, 1, 0, 0]); + } +} + +Map matrixToMap(Matrix matrix) { + return { + "a": matrix.a, + "b": matrix.b, + "c": matrix.c, + "d": matrix.d, + "e": matrix.e, + "f": matrix.f, + }; +} + +Map domMatrixToMap(DomMatrix matrix) { + return { + "a": matrix.a, + "b": matrix.b, + "c": matrix.c, + "d": matrix.d, + "e": matrix.e, + "f": matrix.f, + }; +} + +Map pointToMap(svg.Point point) { + return {"x": point.x, "y": point.y}; +} + +// creates a new separate text svg for the jth character on a svg text element +TextElement createPortableElement(TextContentElement textEle, int j) { + TextElement charEle = document.createElementNS("http://www.w3.org/2000/svg", "text"); + charEle.text = textEle.text[j]; + charEle.setAttribute("style", textEle.style.cssText); + + var pos = DomPoint.fromPoint(pointToMap(textEle.getStartPositionOfChar(j))); + var rot = textEle.getRotationOfChar(j); + + for (int i = 0; i < textEle.transform.baseVal.numberOfItems; ++i) { + var item = textEle.transform.baseVal.getItem(i); + pos = pos.matrixTransform(matrixToMap(item.matrix)); + rot = item.angle; + } + if (charEle.style.getPropertyValue("dominant-baseline") != "") { + pos = pos.matrixTransform(domMatrixToMap(dominantBaselineMatrix( + charEle.style.getPropertyValue("dominant-baseline"), + rot, + textEle.style.fontSize + " " + textEle.style.fontFamily))); + } + charEle.style.setProperty("dominant-baseline", ""); + charEle.style.setProperty("text-anchor", "start"); + charEle.style.setProperty("text-shadow", + "-0.7px -0.7px 0 #fff, 0.7px -0.7px 0 #fff, -0.7px 0.7px 0 #fff, 0.7px 0.7px 0 #fff"); // doesn't work in PowerPoint + charEle.setAttribute("x", pos.x.toString()); + charEle.setAttribute("y", pos.y.toString()); + charEle.setAttribute("transform", "rotate(${rot} ${pos.x} ${pos.y})"); + return charEle; +} + +// makes a svg compatible for PowerPoint +Element make_portable(Element src) { + var src_children = src.querySelectorAll("*"); + document.body.append(src); + for (int i = 0; i < src_children.length; ++i) { + if (src_children[i] is svg.TextContentElement) { + TextContentElement textEle = src_children[i] as TextContentElement; + if (textEle.children.length == 1 && textEle.children[0].tagName == "textPath") { + continue; + } + List portableEles = []; + for (int j = 0; j < textEle.getNumberOfChars(); ++j) { + var charEle = createPortableElement(textEle, j); + portableEles.add(charEle); + } + if (textEle is TextPathElement) { + // move TextPath children up and delete the TextPath + var parent = textEle.parent; + var newParent = document.createElementNS("http://www.w3.org/2000/svg", "g"); + parent.parent.append(newParent); + newParent.append(textEle); + parent.remove(); + } + portableEles.forEach((v) => textEle.parentNode.append(v)); + textEle.remove(); + } + } + src.remove(); + return src; +} + SvgSvgElement get_cloned_svg_element_with_style(List selected_elts, bool separate_text) { var cloned_svg_element_with_style = SvgSvgElement() ..children = selected_elts.map(clone_and_apply_style).toList(); if (separate_text) { - selected_elts = selected_elts.map(separate_if_svg_text).map((x) => x as Element).toList(); + cloned_svg_element_with_style = make_portable(cloned_svg_element_with_style); } // we can't get bbox without it being added to the DOM first From b307a48866fccb984a2a7f7645b012fcdb1b4a0a Mon Sep 17 00:00:00 2001 From: RayBipse Date: Sat, 27 Jan 2024 13:54:51 -0800 Subject: [PATCH 07/11] Changed names to snakes case and fixed outline bug --- lib/src/middleware/export_svg.dart | 98 ++++++++++++++++-------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/lib/src/middleware/export_svg.dart b/lib/src/middleware/export_svg.dart index 2b908731e..a5607fddb 100644 --- a/lib/src/middleware/export_svg.dart +++ b/lib/src/middleware/export_svg.dart @@ -89,7 +89,7 @@ List get_selected_strands(Store store) { return selected_elts; } -List rotateVector(List vec, double ang) { +List rotate_vector(List vec, double ang) { ang = ang * (math.pi / 180); var cos = math.cos(ang); var sin = math.sin(ang); @@ -105,15 +105,15 @@ double get_text_height(String font) { } // returns a matrix that represents the change made by dominant-baseline css property -DomMatrix dominantBaselineMatrix(String dominantBaseline, double rot, String font) { - switch (dominantBaseline) { +DomMatrix dominant_baseline_matrix(String dominant_baseline, double rot, String font) { + switch (dominant_baseline) { case "ideographic": return new DomMatrix([ 1, 0, 0, 1, - ...rotateVector([0, (-3 * get_text_height(font)) / 12], rot) + ...rotate_vector([0, (-3 * get_text_height(font)) / 12], rot) ]); case "hanging": return new DomMatrix([ @@ -121,7 +121,7 @@ DomMatrix dominantBaselineMatrix(String dominantBaseline, double rot, String fon 0, 0, 1, - ...rotateVector([0, (9 * get_text_height(font)) / 12], rot) + ...rotate_vector([0, (9 * get_text_height(font)) / 12], rot) ]); case "central": return new DomMatrix([ @@ -129,14 +129,14 @@ DomMatrix dominantBaselineMatrix(String dominantBaseline, double rot, String fon 0, 0, 1, - ...rotateVector([0, (4 * get_text_height(font)) / 12], rot) + ...rotate_vector([0, (4 * get_text_height(font)) / 12], rot) ]); default: return new DomMatrix([1, 0, 0, 1, 0, 0]); } } -Map matrixToMap(Matrix matrix) { +Map matrix_to_map(Matrix matrix) { return { "a": matrix.a, "b": matrix.b, @@ -147,7 +147,7 @@ Map matrixToMap(Matrix matrix) { }; } -Map domMatrixToMap(DomMatrix matrix) { +Map dom_matrix_to_map(DomMatrix matrix) { return { "a": matrix.a, "b": matrix.b, @@ -158,38 +158,46 @@ Map domMatrixToMap(DomMatrix matrix) { }; } -Map pointToMap(svg.Point point) { +Map point_to_map(svg.Point point) { return {"x": point.x, "y": point.y}; } // creates a new separate text svg for the jth character on a svg text element -TextElement createPortableElement(TextContentElement textEle, int j) { - TextElement charEle = document.createElementNS("http://www.w3.org/2000/svg", "text"); - charEle.text = textEle.text[j]; - charEle.setAttribute("style", textEle.style.cssText); - - var pos = DomPoint.fromPoint(pointToMap(textEle.getStartPositionOfChar(j))); - var rot = textEle.getRotationOfChar(j); - - for (int i = 0; i < textEle.transform.baseVal.numberOfItems; ++i) { - var item = textEle.transform.baseVal.getItem(i); - pos = pos.matrixTransform(matrixToMap(item.matrix)); +TextElement create_portable_element(TextContentElement text_ele, int j) { + TextElement char_ele = document.createElementNS("http://www.w3.org/2000/svg", "text"); + char_ele.text = text_ele.text[j]; + char_ele.setAttribute("style", text_ele.style.cssText); + var pos = DomPoint.fromPoint(point_to_map(text_ele.getStartPositionOfChar(j))); + var rot = text_ele.getRotationOfChar(j); + + for (int i = 0; i < text_ele.transform.baseVal.numberOfItems; ++i) { + var item = text_ele.transform.baseVal.getItem(i); + pos = pos.matrixTransform(matrix_to_map(item.matrix)); rot = item.angle; } - if (charEle.style.getPropertyValue("dominant-baseline") != "") { - pos = pos.matrixTransform(domMatrixToMap(dominantBaselineMatrix( - charEle.style.getPropertyValue("dominant-baseline"), + if (char_ele.style.getPropertyValue("dominant-baseline") != "") { + pos = pos.matrixTransform(dom_matrix_to_map(dominant_baseline_matrix( + char_ele.style.getPropertyValue("dominant-baseline"), rot, - textEle.style.fontSize + " " + textEle.style.fontFamily))); + text_ele.style.fontSize + " " + text_ele.style.fontFamily))); + } + char_ele.style.setProperty("dominant-baseline", ""); + char_ele.style.setProperty("text-anchor", "start"); + if (text_ele.classes.any([ + "loopout-extension-length", + "dna-seq-insertion", + "dna-seq-loopout", + "dna-seq-extension", + "dna-seq" + ].contains)) { + char_ele.style.setProperty( + "text-shadow", // doesn't work in PowerPoint + "-0.7px -0.7px 0 #fff, 0.7px -0.7px 0 #fff, -0.7px 0.7px 0 #fff, 0.7px 0.7px 0 #fff"); } - charEle.style.setProperty("dominant-baseline", ""); - charEle.style.setProperty("text-anchor", "start"); - charEle.style.setProperty("text-shadow", - "-0.7px -0.7px 0 #fff, 0.7px -0.7px 0 #fff, -0.7px 0.7px 0 #fff, 0.7px 0.7px 0 #fff"); // doesn't work in PowerPoint - charEle.setAttribute("x", pos.x.toString()); - charEle.setAttribute("y", pos.y.toString()); - charEle.setAttribute("transform", "rotate(${rot} ${pos.x} ${pos.y})"); - return charEle; + char_ele.setAttribute("x", pos.x.toString()); + char_ele.setAttribute("y", pos.y.toString()); + char_ele.setAttribute("transform", "rotate(${rot} ${pos.x} ${pos.y})"); + return char_ele; } // makes a svg compatible for PowerPoint @@ -198,25 +206,25 @@ Element make_portable(Element src) { document.body.append(src); for (int i = 0; i < src_children.length; ++i) { if (src_children[i] is svg.TextContentElement) { - TextContentElement textEle = src_children[i] as TextContentElement; - if (textEle.children.length == 1 && textEle.children[0].tagName == "textPath") { + TextContentElement text_ele = src_children[i] as TextContentElement; + if (text_ele.children.length == 1 && text_ele.children[0].tagName == "textPath") { continue; } - List portableEles = []; - for (int j = 0; j < textEle.getNumberOfChars(); ++j) { - var charEle = createPortableElement(textEle, j); - portableEles.add(charEle); + List portable_eles = []; + for (int j = 0; j < text_ele.getNumberOfChars(); ++j) { + var char_ele = create_portable_element(text_ele, j); + portable_eles.add(char_ele); } - if (textEle is TextPathElement) { + if (text_ele is TextPathElement) { // move TextPath children up and delete the TextPath - var parent = textEle.parent; - var newParent = document.createElementNS("http://www.w3.org/2000/svg", "g"); - parent.parent.append(newParent); - newParent.append(textEle); + var parent = text_ele.parent; + var new_parent = document.createElementNS("http://www.w3.org/2000/svg", "g"); + parent.parent.append(new_parent); + new_parent.append(text_ele); parent.remove(); } - portableEles.forEach((v) => textEle.parentNode.append(v)); - textEle.remove(); + portable_eles.forEach((v) => text_ele.parentNode.append(v)); + text_ele.remove(); } } src.remove(); From bf718f25086dca9c8b3b5e299e2a0af26e5b36e4 Mon Sep 17 00:00:00 2001 From: RayBipse Date: Sat, 27 Jan 2024 14:12:07 -0800 Subject: [PATCH 08/11] Fixed bug export SVG main view doesn't contain a viewbox --- lib/src/middleware/export_svg.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/middleware/export_svg.dart b/lib/src/middleware/export_svg.dart index a5607fddb..77bd4543a 100644 --- a/lib/src/middleware/export_svg.dart +++ b/lib/src/middleware/export_svg.dart @@ -54,7 +54,7 @@ export_svg_middleware(Store store, dynamic action, NextDispatcher next } } else { if (store.state.ui_state.export_svg_text_separately) { - elt = make_portable(clone_and_apply_style(elt)); + elt = get_cloned_svg_element_with_style([elt], store.state.ui_state.export_svg_text_separately); } _export_from_element(elt, 'main'); } @@ -245,7 +245,7 @@ SvgSvgElement get_cloned_svg_element_with_style(List selected_elts, boo // have to add some padding to viewbox, for some reason bbox doesn't always fit it by a few pixels?? cloned_svg_element_with_style.setAttribute('viewBox', - '${bbox.x.floor() - 1} ${bbox.y.floor() - 1} ${bbox.width.ceil() + 3} ${bbox.height.ceil() + 3}'); + '${bbox.x.floor() - 1} ${bbox.y.floor() - 1} ${bbox.width.ceil() + 3} ${bbox.height.ceil() + 6}'); return cloned_svg_element_with_style; } From 3ce8c86d0b2fa437b0113c1eb78f2851d58cd8c1 Mon Sep 17 00:00:00 2001 From: RayBipse Date: Sat, 27 Jan 2024 14:59:30 -0800 Subject: [PATCH 09/11] Fixed wrong merge resolve --- lib/src/middleware/export_svg.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/middleware/export_svg.dart b/lib/src/middleware/export_svg.dart index 9a36dbfae..320a8ef40 100644 --- a/lib/src/middleware/export_svg.dart +++ b/lib/src/middleware/export_svg.dart @@ -3,9 +3,11 @@ import 'dart:svg' as svg; import 'dart:svg'; import 'dart:math' as math; +import 'package:built_collection/built_collection.dart'; import 'package:over_react/over_react.dart'; import 'package:redux/redux.dart'; import 'package:scadnano/src/middleware/system_clipboard.dart'; +import 'package:scadnano/src/state/strand.dart'; import 'package:scadnano/src/view/design_main_dna_sequence.dart'; import '../app.dart'; From 3850c48a04563431e8874c1c7c121715124ca309 Mon Sep 17 00:00:00 2001 From: RayBipse Date: Sat, 27 Jan 2024 15:20:01 -0800 Subject: [PATCH 10/11] Fixed bug where selected strands were pink --- lib/src/middleware/export_svg.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/middleware/export_svg.dart b/lib/src/middleware/export_svg.dart index 320a8ef40..bd8297e28 100644 --- a/lib/src/middleware/export_svg.dart +++ b/lib/src/middleware/export_svg.dart @@ -351,12 +351,12 @@ final relevant_styles = { Element clone_and_apply_style(Element elt_orig) { Element elt_styled = elt_orig.clone(true); - bool selected = elt_orig.classes.contains('selected'); + bool selected = elt_orig.classes.contains('selected-pink'); - elt_orig.classes.remove('selected'); + elt_orig.classes.remove('selected-pink'); clone_and_apply_style_rec(elt_styled, elt_orig); - if (selected) elt_orig.classes.add('selected'); + if (selected) elt_orig.classes.add('selected-pink'); // need to get from original since it has been rendered (styled hasn't been rendered so has 0 bounding box // also need to get from g element, not svg element, since svg element dimensions based on original From a6b06ac793fca9b28313cfe924e5c2406464527f Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 5 Feb 2024 10:31:47 -0800 Subject: [PATCH 11/11] re-ordered export menu --- lib/src/view/menu.dart | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/src/view/menu.dart b/lib/src/view/menu.dart index 6a9280ca7..08e98c79f 100644 --- a/lib/src/view/menu.dart +++ b/lib/src/view/menu.dart @@ -1182,6 +1182,19 @@ A highlighting effect will still appear. ..on_click = ((_) => props.dispatch(actions.ExportSvg(type: actions.ExportSvgType.selected))) ..tooltip = "Export SVG figure of selected strands" ..display = 'SVG of selected strands')(), + (MenuBoolean() + ..value = props.export_svg_text_separately + ..display = 'export svg text separately (PPT)' + ..tooltip = '''\ +When selected, every symbol of the text in a DNA sequence is exported as a separate +SVG text element. This is useful if the SVG will be imported into Powerpoint, which +is less expressive than SVG and can render the text strangely.''' + ..name = 'export-svg-text-separately' + ..onChange = (_) { + props.dispatch(actions.ExportSvgTextSeparatelySet(!props.export_svg_text_separately)); + } + ..key = 'export-svg-text-separately')(), + DropdownDivider({'key': 'divider-export-svg'}), (MenuDropdownItem() ..on_click = ((_) => app.disable_keyboard_shortcuts_while(export_dna_sequences.export_dna)) ..tooltip = "Export DNA sequences of strands to a file." @@ -1191,19 +1204,7 @@ A highlighting effect will still appear. ..tooltip = "Export design's DNA sequences as a CSV in the same way as cadnano v2.\n" "This is useful, for example, with CanDo's atomic model generator." ..display = 'DNA sequences (cadnano v2 format)')(), - DropdownDivider({'key': 'divider-export-svg-settings'}), - (MenuBoolean() - ..value = props.export_svg_text_separately - ..display = 'export svg text separately' - ..tooltip = '''\ -When selected, every character of the text in a DNA sequence is exported separately. -This is useful to circumvent an SVG bug found in Microsoft tools such as PowerPoint.''' - ..name = 'export-svg-text-separately' - ..onChange = (_) { - props.dispatch(actions.ExportSvgTextSeparatelySet(!props.export_svg_text_separately)); - } - ..key = 'export-svg-text-separately')(), - DropdownDivider({'key': 'divider-not-full-design'}), + DropdownDivider({'key': 'divider-export-dna'}), (MenuDropdownItem() ..on_click = ((_) => props.dispatch(actions.ExportCadnanoFile(whitespace: true))) ..tooltip = "Export design to cadnano (version 2) .json file." @@ -1230,6 +1231,7 @@ cadnano files that have whitespace. ("Bad .json file format is detected in 'structure.json'. Or no dsDNA or strand crossovers exist.")""" ..display = 'cadnano v2 no whitespace' ..key = 'export-cadnano-no-whitespace')(), + DropdownDivider({'key': 'divider-cadnano'}), (MenuDropdownItem() ..on_click = ((_) => props.dispatch(actions.OxdnaExport())) ..tooltip = "Export design to oxDNA .dat and .top files, which can be loaded in oxDNA or oxView."