From a071097d5798b83b453235612d271601cb3f8469 Mon Sep 17 00:00:00 2001 From: David Doty Date: Fri, 13 Oct 2023 08:12:25 -0700 Subject: [PATCH 01/37] bumped version --- lib/src/constants.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/constants.dart b/lib/src/constants.dart index b9e5426eb..ce1214dad 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -8,12 +8,12 @@ import 'state/grid.dart'; // WARNING: Do not modify line below, except for the version string // (and also add new version string to scadnano_versions_to_link). -const String CURRENT_VERSION = "0.19.1"; +const String CURRENT_VERSION = "0.19.2"; const String INITIAL_VERSION = "0.1.0"; // scadnano versions that we deploy so that older versions can be used. final scadnano_older_versions_to_link = [ - "0.19.0", + "0.19.1", "0.18.10", "0.17.14", // "0.17.13", From 58cd26ec4262ffd14063dbd8f8fde1a7a93ff46d Mon Sep 17 00:00:00 2001 From: David Doty Date: Sat, 14 Oct 2023 12:07:58 -0700 Subject: [PATCH 02/37] added css class for darker stroke on crossovers within the same helix --- lib/src/constants.dart | 1 + lib/src/view/design_main_strand_crossover.dart | 7 +++++++ web/scadnano-styles.css | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/lib/src/constants.dart b/lib/src/constants.dart index ce1214dad..656112868 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -430,6 +430,7 @@ const css_selector_domain = 'domain-line'; const css_selector_base_pair_line = 'base-pair-line'; const css_selector_extension = 'extension-line'; const css_selector_crossover = 'crossover-curve'; +const css_selector_crossover_same_helix = 'crossover-curve-same-helix'; const css_selector_loopout = 'loopout-curve'; const css_selector_end_5p_strand = 'five-prime-end-first-substrand'; const css_selector_end_3p_strand = 'three-prime-end-last-substrand'; diff --git a/lib/src/view/design_main_strand_crossover.dart b/lib/src/view/design_main_strand_crossover.dart index 9a9f02413..bd38a03ef 100644 --- a/lib/src/view/design_main_strand_crossover.dart +++ b/lib/src/view/design_main_strand_crossover.dart @@ -69,6 +69,13 @@ class DesignMainStrandCrossoverComponent classname += ' ' + constants.css_selector_scaffold; } + // Want to display crossovers within a helix a little darker since it's hard to see them; + // This is for "crossovers" that really are just buffered space between domains on the same helix. + if (props.prev_domain.helix == props.next_domain.helix && + props.prev_domain.forward == props.next_domain.forward) { + classname += ' ' + constants.css_selector_crossover_same_helix; + } + var prev_group = props.helices[props.prev_domain.helix].group; var next_group = props.helices[props.next_domain.helix].group; bool within_group = prev_group == next_group; diff --git a/web/scadnano-styles.css b/web/scadnano-styles.css index ee21607cd..c697a24a7 100644 --- a/web/scadnano-styles.css +++ b/web/scadnano-styles.css @@ -742,6 +742,10 @@ label + select { fill: none; } +.crossover-curve-same-helix { + stroke-opacity: 0.55; +} + .potential-vertical-crossover-curve { stroke-width: 4; fill: none; From e48745733370af533fede19d7b6b8988a0b2b814 Mon Sep 17 00:00:00 2001 From: Raybipse Date: Sat, 21 Oct 2023 16:58:39 -0700 Subject: [PATCH 03/37] fixed batch remove label, remove name, set name --- lib/src/view/design_main_strand.dart | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/src/view/design_main_strand.dart b/lib/src/view/design_main_strand.dart index 31a47e7ff..8730b0a27 100644 --- a/lib/src/view/design_main_strand.dart +++ b/lib/src/view/design_main_strand.dart @@ -273,7 +273,8 @@ class DesignMainStrandComponent extends UiComponent2 assign_plate_well_fields() => app.disable_keyboard_shortcuts_while(ask_for_assign_plate_well_fields); - set_strand_name() => app.disable_keyboard_shortcuts_while(ask_for_strand_name); + set_strand_name() => app.disable_keyboard_shortcuts_while( + () => ask_for_strand_name(props.strand, app.state.ui_state.selectables_store.selected_strands)); set_strand_label() => app.disable_keyboard_shortcuts_while( () => ask_for_label(props.strand, null, app.state.ui_state.selectables_store.selected_strands)); @@ -474,7 +475,11 @@ assigned, assign the complementary DNA sequence to this strand. if (props.strand.name != null) ContextMenuItem( title: 'remove strand name', - on_click: () => app.dispatch(actions.StrandNameSet(name: null, strand: props.strand))), + on_click: () => app.dispatch(batch_if_multiple_selected( + (strand) => actions.StrandNameSet(name: null, strand: strand), + props.strand, + app.state.ui_state.selectables_store.selected_strands, + 'remove strand label'))), ContextMenuItem( title: 'set domain name', on_click: () => set_substrand_name(substrand), @@ -503,7 +508,11 @@ feature for individual domains, set select mode to domain. if (props.strand.label != null) ContextMenuItem( title: 'remove strand label', - on_click: () => app.dispatch(actions.StrandLabelSet(label: null, strand: props.strand))), + on_click: () => app.dispatch(batch_if_multiple_selected( + (strand) => actions.StrandLabelSet(label: null, strand: strand), + props.strand, + app.state.ui_state.selectables_store.selected_strands, + 'remove strand label'))), ContextMenuItem( title: 'set domain label', on_click: () => set_substrand_label(substrand), @@ -901,7 +910,7 @@ PAGEHPLC : Dual PAGE & HPLC app.dispatch(batch_action); } - Future ask_for_strand_name() async { + Future ask_for_strand_name(Strand strand, BuiltSet selected_strands) async { int name_idx = 0; var items = List.filled(1, null); items[name_idx] = DialogText(label: 'name', value: props.strand.name ?? ''); @@ -912,7 +921,8 @@ PAGEHPLC : Dual PAGE & HPLC if (results == null) return; String name = (results[name_idx] as DialogText).value; - actions.UndoableAction action = actions.StrandNameSet(name: name, strand: props.strand); + actions.UndoableAction action = batch_if_multiple_selected( + name_set_strand_action_creator(name), strand, selected_strands, "set strand name"); app.dispatch(action); } @@ -1016,6 +1026,9 @@ StrandActionCreator color_set_substrand_action_creator(Substrand substrand, Stri ((Strand strand) => actions.StrandOrSubstrandColorSet(strand: strand, substrand: substrand, color: Color.hex(color_hex))); +StrandActionCreator name_set_strand_action_creator(String name) => + ((Strand strand) => actions.StrandNameSet(strand: strand, name: name)); + StrandActionCreator label_set_strand_action_creator(String label) => ((Strand strand) => actions.StrandLabelSet(strand: strand, label: label)); From abcecd2d7db8c55bdd8da552fd8be3a5b2a979ed Mon Sep 17 00:00:00 2001 From: Raybipse Date: Tue, 24 Oct 2023 10:10:26 -0700 Subject: [PATCH 04/37] fixes #953 --- lib/src/view/design_main_strand.dart | 57 ++++++++++++++++++---------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/lib/src/view/design_main_strand.dart b/lib/src/view/design_main_strand.dart index 8730b0a27..ea373ce51 100644 --- a/lib/src/view/design_main_strand.dart +++ b/lib/src/view/design_main_strand.dart @@ -279,11 +279,13 @@ class DesignMainStrandComponent extends UiComponent2 set_strand_label() => app.disable_keyboard_shortcuts_while( () => ask_for_label(props.strand, null, app.state.ui_state.selectables_store.selected_strands)); - set_substrand_name(Substrand substrand) => - app.disable_keyboard_shortcuts_while(() => ask_for_substrand_name(substrand)); + set_domain_names(BuiltSet domains) => + app.disable_keyboard_shortcuts_while(() => ask_for_domain_names(domains)); - set_substrand_label(Substrand substrand) => app.disable_keyboard_shortcuts_while( - () => ask_for_label(props.strand, substrand, app.state.ui_state.selectables_store.selected_substrands)); + set_domain_labels(Substrand substrand, BuiltSet domains) { + return app.disable_keyboard_shortcuts_while( + () => ask_for_label(props.strand, substrand, get_selected_domains())); + } ReactElement _insertions() { List paths = []; @@ -482,7 +484,7 @@ assigned, assign the complementary DNA sequence to this strand. 'remove strand label'))), ContextMenuItem( title: 'set domain name', - on_click: () => set_substrand_name(substrand), + on_click: () => set_domain_names(get_selected_domains()), ), ContextMenuItem( title: 'assign domain name complement from bound strands', @@ -496,7 +498,9 @@ feature for individual domains, set select mode to domain. if (substrand.name != null) ContextMenuItem( title: 'remove domain name', - on_click: () => app.dispatch(actions.SubstrandNameSet(name: null, substrand: substrand))), + on_click: () => app.dispatch(actions.BatchAction( + get_selected_domains().map((d) => actions.SubstrandNameSet(name: null, substrand: d)), + 'remove domain names'))), ].build()), ContextMenuItem( title: 'edit label', @@ -515,12 +519,14 @@ feature for individual domains, set select mode to domain. 'remove strand label'))), ContextMenuItem( title: 'set domain label', - on_click: () => set_substrand_label(substrand), + on_click: () => set_domain_labels(substrand, get_selected_domains()), ), if (substrand.label != null) ContextMenuItem( title: 'remove domain label', - on_click: () => app.dispatch(actions.SubstrandLabelSet(label: null, substrand: substrand))), + on_click: () => app.dispatch(actions.BatchAction( + get_selected_domains().map((d) => actions.SubstrandLabelSet(label: null, substrand: d)), + 'remove domain labels'))), ].build()), ContextMenuItem( title: 'reflect', @@ -926,13 +932,12 @@ PAGEHPLC : Dual PAGE & HPLC app.dispatch(action); } - Future ask_for_substrand_name(Substrand substrand) async { + Future ask_for_domain_names(BuiltSet domains) async { int name_idx = 0; var items = List.filled(1, null); - - items[name_idx] = DialogText(label: 'name', value: substrand.name ?? ''); + items[name_idx] = DialogText(label: 'name', value: ""); var dialog = Dialog( - title: 'set ${substrand.type_description()} name', + title: 'set ${domains.first.type_description()} name', items: items, type: DialogType.set_domain_name, use_saved_response: false); @@ -941,12 +946,14 @@ PAGEHPLC : Dual PAGE & HPLC if (results == null) return; String name = (results[name_idx] as DialogText).value; - actions.UndoableAction action = actions.SubstrandNameSet(name: name, substrand: substrand); - app.dispatch(action); + return app.dispatch(actions.BatchAction( + domains.map((d) => actions.SubstrandNameSet(name: name, substrand: d)), "set domain names")); } } -Future ask_for_label(Strand strand, Substrand substrand, BuiltSet selected_strands) async { +Future ask_for_label( + Strand strand, Substrand substrand, BuiltSet selected_strands) async { + // T is expected to be Strand or Domain String part_name = 'strand'; if (substrand != null) { part_name = substrand.type_description(); @@ -982,12 +989,12 @@ Future ask_for_label(Strand strand, Substrand substrand, BuiltSet actions.UndoableAction action; if (substrand == null) { - action = batch_if_multiple_selected( - label_set_strand_action_creator(label), strand, selected_strands, "set strand label"); + action = batch_if_multiple_selected(label_set_strand_action_creator(label), strand, + selected_strands as BuiltSet, "set strand label"); } else { - action = actions.SubstrandLabelSet(label: label, substrand: substrand); - // action = batch_if_multiple_selected( - // label_set_strand_action_creator(label), props.strand, selected_strands, "set domain label"); + action = actions.BatchAction( + selected_strands.map((s) => actions.SubstrandLabelSet(label: label, substrand: (s as Substrand))), + "set domain labels"); } app.dispatch(action); @@ -1010,6 +1017,16 @@ actions.UndoableAction batch_if_multiple_selected(StrandActionCreator action_cre return action; } +// this is so if user selects with the domain select tool, it still returns the selected domains +// as opposed to just using "selected_strands" +BuiltSet get_selected_domains() { + return BuiltSet(app.state.ui_state.selectables_store.selected_strands + .map((s) => s.substrands) + .expand((l) => l) + .toBuiltSet()) + .union(app.state.ui_state.selectables_store.selected_domains); +} + typedef StrandActionCreator = actions.UndoableAction Function(Strand strand); StrandActionCreator scaffold_set_strand_action_creator(bool is_scaffold) => From 22ae227b5a3c30f199d4897a299da25962c7d25f Mon Sep 17 00:00:00 2001 From: Raybipse Date: Sat, 28 Oct 2023 19:01:32 -0700 Subject: [PATCH 05/37] Fixed batch remove/set for extensions and loopouts --- lib/src/view/design_main_strand.dart | 5 ++--- lib/src/view/design_main_strand_extension.dart | 18 +++++++++++++----- lib/src/view/design_main_strand_loopout.dart | 18 +++++++++++++----- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/lib/src/view/design_main_strand.dart b/lib/src/view/design_main_strand.dart index ea373ce51..ad57d6543 100644 --- a/lib/src/view/design_main_strand.dart +++ b/lib/src/view/design_main_strand.dart @@ -928,7 +928,7 @@ PAGEHPLC : Dual PAGE & HPLC String name = (results[name_idx] as DialogText).value; actions.UndoableAction action = batch_if_multiple_selected( - name_set_strand_action_creator(name), strand, selected_strands, "set strand name"); + name_set_strand_action_creator(name), strand, selected_strands, "set strand names"); app.dispatch(action); } @@ -953,7 +953,6 @@ PAGEHPLC : Dual PAGE & HPLC Future ask_for_label( Strand strand, Substrand substrand, BuiltSet selected_strands) async { - // T is expected to be Strand or Domain String part_name = 'strand'; if (substrand != null) { part_name = substrand.type_description(); @@ -994,7 +993,7 @@ Future ask_for_label( } else { action = actions.BatchAction( selected_strands.map((s) => actions.SubstrandLabelSet(label: label, substrand: (s as Substrand))), - "set domain labels"); + "set substrand labels"); } app.dispatch(action); diff --git a/lib/src/view/design_main_strand_extension.dart b/lib/src/view/design_main_strand_extension.dart index 43437b758..c8fe49be6 100644 --- a/lib/src/view/design_main_strand_extension.dart +++ b/lib/src/view/design_main_strand_extension.dart @@ -173,7 +173,10 @@ class DesignMainExtensionComponent extends UiComponent2 app.dispatch(actions.SubstrandNameSet(name: null, substrand: props.ext))), + on_click: () => app.dispatch(actions.BatchAction( + app.state.ui_state.selectables_store.selected_extensions + .map((e) => actions.SubstrandNameSet(name: null, substrand: e)), + "remove extension names"))), ContextMenuItem( title: 'set extension label', on_click: set_extension_label, @@ -181,7 +184,10 @@ class DesignMainExtensionComponent extends UiComponent2 app.dispatch(actions.SubstrandLabelSet(label: null, substrand: props.ext))), + on_click: () => app.dispatch(actions.BatchAction( + app.state.ui_state.selectables_store.selected_extensions + .map((e) => actions.SubstrandLabelSet(label: null, substrand: e)), + "remove extension labels"))), ContextMenuItem( title: 'set extension color', on_click: () => app @@ -217,7 +223,7 @@ class DesignMainExtensionComponent extends UiComponent2 app.disable_keyboard_shortcuts_while(() => design_main_strand.ask_for_label( props.strand, props.ext, - app.state.ui_state.selectables_store.selected_substrands, + app.state.ui_state.selectables_store.selected_extensions, )); Future ask_for_extension_name() async { @@ -230,8 +236,10 @@ class DesignMainExtensionComponent extends UiComponent2 actions.SubstrandNameSet(name: name, substrand: e)), + "set extension names")); } extension_display_length_and_angle_change() => diff --git a/lib/src/view/design_main_strand_loopout.dart b/lib/src/view/design_main_strand_loopout.dart index 42ae99531..5b55b4c5b 100644 --- a/lib/src/view/design_main_strand_loopout.dart +++ b/lib/src/view/design_main_strand_loopout.dart @@ -166,7 +166,10 @@ class DesignMainLoopoutComponent extends UiStatefulComponent2 app.dispatch(actions.SubstrandNameSet(name: null, substrand: props.loopout))), + on_click: () => app.dispatch(actions.BatchAction( + app.state.ui_state.selectables_store.selected_loopouts + .map((l) => actions.SubstrandNameSet(name: null, substrand: l)), + "remove loopout names"))), ContextMenuItem( title: 'set loopout label', on_click: set_loopout_label, @@ -174,7 +177,10 @@ class DesignMainLoopoutComponent extends UiStatefulComponent2 app.dispatch(actions.SubstrandLabelSet(substrand: props.loopout, label: null))), + on_click: () => app.dispatch(actions.BatchAction( + app.state.ui_state.selectables_store.selected_loopouts + .map((l) => actions.SubstrandLabelSet(label: null, substrand: l)), + "remove loopout names"))), ContextMenuItem( title: 'set loopout color', on_click: () => app.dispatch( @@ -224,7 +230,7 @@ class DesignMainLoopoutComponent extends UiStatefulComponent2 app.disable_keyboard_shortcuts_while(() => design_main_strand.ask_for_label( props.strand, props.loopout, - app.state.ui_state.selectables_store.selected_substrands, + app.state.ui_state.selectables_store.selected_loopouts, )); Future ask_for_loopout_name() async { @@ -237,8 +243,10 @@ class DesignMainLoopoutComponent extends UiStatefulComponent2 actions.SubstrandNameSet(name: name, substrand: l)), + "set loopout names")); } String loopout_path_description_between_groups() { From f72c32ca19d714c7b61617bfe81858602d38a4dd Mon Sep 17 00:00:00 2001 From: Raybipse Date: Sat, 28 Oct 2023 21:57:09 -0700 Subject: [PATCH 06/37] implemented logic for filtering connected base pairs --- lib/src/middleware/export_svg.dart | 21 ++++++++++++++++-- lib/src/state/design.dart | 35 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/lib/src/middleware/export_svg.dart b/lib/src/middleware/export_svg.dart index 83d3f8636..1332aea5a 100644 --- a/lib/src/middleware/export_svg.dart +++ b/lib/src/middleware/export_svg.dart @@ -2,8 +2,12 @@ import 'dart:html'; import 'dart:svg' as svg; import 'dart:svg'; +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/domain.dart'; +import 'package:scadnano/src/view/design_main_base_pair_lines.dart'; import '../app.dart'; import '../state/app_state.dart'; @@ -85,6 +89,18 @@ SvgSvgElement get_cloned_svg_element_with_style(List selected_elts) { var cloned_svg_element_with_style = SvgSvgElement() ..children = selected_elts.map(clone_and_apply_style).toList(); + List base_pairs_elements = [ + // TEMPORARY, JUST FOR TESTING + document.getElementsByClassName('base-pair-lines-main-view')[0] as Element + ]; + cloned_svg_element_with_style.children.addAll(base_pairs_elements.map(clone_and_apply_style)); + + print(app.state.design.base_pairs); + print(app.state.design.strand_to_index); + + print(app.state.design.get_selected_base_pairs( + app.state.design.base_pairs, app.state.ui_state.selectables_store.selected_strands)); + // we can't get bbox without it being added to the DOM first document.body.append(cloned_svg_element_with_style); var bbox = cloned_svg_element_with_style.getBBox(); @@ -131,10 +147,11 @@ _copy_from_elements(List svg_elements) { _export_from_element(Element svg_element, String filename_append) { var cloned_svg_element_with_style; - if (filename_append != "selected") + if (filename_append != "selected") { cloned_svg_element_with_style = clone_and_apply_style(svg_element); - else + } else { cloned_svg_element_with_style = svg_element; + } // if element is not an svg element (it can be a child element of svg e.g. groups, lines, text, etc), wrap in svg tag if (!(svg_element is svg.SvgSvgElement)) cloned_svg_element_with_style = SvgSvgElement()..children = [cloned_svg_element_with_style]; diff --git a/lib/src/state/design.dart b/lib/src/state/design.dart index f9f766c3e..533fca1bb 100644 --- a/lib/src/state/design.dart +++ b/lib/src/state/design.dart @@ -2108,6 +2108,41 @@ abstract class Design with UnusedFields implements Built, @memoized BuiltMap> get base_pairs_with_mismatches => this._base_pairs(true); + /// given `base_pairs`, returns a filtered map that is connected by at least 2 strands in `selected_strands` + BuiltMap> get_selected_base_pairs( + BuiltMap> base_pairs, BuiltSet selected_strands) { + Map> connect_cnt = {}; + + helices.forEach((i, v) { + connect_cnt[i] = List.filled(v.max_offset + 1, 0); + }); + selected_strands.forEach((strand) { + strand.substrands.forEach((substrand) { + if (substrand is Domain) { + connect_cnt[substrand.helix][substrand.start] += 1; + connect_cnt[substrand.helix][substrand.end + 1] -= 1; + } + }); + }); + connect_cnt.updateAll((i, list) { + for (int i = 1; i < list.length; ++i) { + list[i] += list[i - 1]; + } + return list; + }); + Map> connected_base_pairs = {}; + base_pairs.forEach((i, list) { + List base_pairs = []; + list.forEach((j) { + if (connect_cnt[i][j] == 2) { + base_pairs.add(j); + } + connected_base_pairs[i] = base_pairs.build(); + }); + }); + return connected_base_pairs.build(); + } + BuiltMap> _base_pairs(bool allow_mismatches) { var base_pairs = Map>(); for (int idx in this.helices.keys) { From 8657d041667af909a678c473db8e70ac55649cb4 Mon Sep 17 00:00:00 2001 From: Raybipse Date: Sun, 29 Oct 2023 15:08:44 -0700 Subject: [PATCH 07/37] Fixed typo in action name for remove strand name --- lib/src/view/design_main_strand.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/design_main_strand.dart b/lib/src/view/design_main_strand.dart index ad57d6543..c7225853c 100644 --- a/lib/src/view/design_main_strand.dart +++ b/lib/src/view/design_main_strand.dart @@ -481,7 +481,7 @@ assigned, assign the complementary DNA sequence to this strand. (strand) => actions.StrandNameSet(name: null, strand: strand), props.strand, app.state.ui_state.selectables_store.selected_strands, - 'remove strand label'))), + 'remove strand name'))), ContextMenuItem( title: 'set domain name', on_click: () => set_domain_names(get_selected_domains()), From d291063f4db69e9a227f793f2447207e8a03cfbb Mon Sep 17 00:00:00 2001 From: Edwin Chang Date: Sun, 5 Nov 2023 22:02:43 -0800 Subject: [PATCH 08/37] add menu option for retaining color on strand selection --- lib/src/actions/actions.dart | 19 +++++++++++++++++++ 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/design_main.dart | 3 +++ lib/src/view/design_main_dna_sequences.dart | 1 + lib/src/view/menu.dart | 15 +++++++++++++++ 8 files changed, 48 insertions(+) diff --git a/lib/src/actions/actions.dart b/lib/src/actions/actions.dart index fb83be0c4..822c6d855 100644 --- a/lib/src/actions/actions.dart +++ b/lib/src/actions/actions.dart @@ -4348,6 +4348,25 @@ abstract class DisablePngCachingDnaSequencesSet _$disablePngCachingDnaSequencesSetSerializer; } +abstract class RetainStrandColorOnSelectionSet + with BuiltJsonSerializable + implements Action, Built { + bool get retain_strand_color_on_selection; + + /************************ begin BuiltValue boilerplate ************************/ + factory RetainStrandColorOnSelectionSet(bool retain_strand_color_on_selection) => + RetainStrandColorOnSelectionSet.from( + (b) => b..retain_strand_color_on_selection = retain_strand_color_on_selection); + + factory RetainStrandColorOnSelectionSet.from( + [void Function(RetainStrandColorOnSelectionSetBuilder) updates]) = _$RetainStrandColorOnSelectionSet; + + RetainStrandColorOnSelectionSet._(); + + static Serializer get serializer => + _$retainStrandColorOnSelectionSetSerializer; +} + abstract class DisplayReverseDNARightSideUpSet with BuiltJsonSerializable implements Action, Built { diff --git a/lib/src/reducers/app_ui_state_reducer.dart b/lib/src/reducers/app_ui_state_reducer.dart index 0a29b397e..88d1add21 100644 --- a/lib/src/reducers/app_ui_state_reducer.dart +++ b/lib/src/reducers/app_ui_state_reducer.dart @@ -213,6 +213,9 @@ int slice_bar_offset_set_reducer(int _, actions.SliceBarOffsetSet action) => act bool disable_png_caching_dna_sequences_reducer(bool _, actions.DisablePngCachingDnaSequencesSet action) => action.disable_png_caching_dna_sequences; +bool retain_strand_color_on_selection_reducer(bool _, actions.RetainStrandColorOnSelectionSet action) => + action.retain_strand_color_on_selection; + bool display_reverse_DNA_right_side_up_reducer(bool _, actions.DisplayReverseDNARightSideUpSet action) => action.display_reverse_DNA_right_side_up; @@ -434,6 +437,7 @@ AppUIStateStorables app_ui_state_storable_local_reducer(AppUIStateStorables stor ..show_slice_bar = TypedReducer(show_slice_bar_reducer)(storables.show_slice_bar, action) ..slice_bar_offset = TypedReducer(slice_bar_offset_set_reducer)(storables.slice_bar_offset, action) ..disable_png_caching_dna_sequences = TypedReducer(disable_png_caching_dna_sequences_reducer)(storables.disable_png_caching_dna_sequences, action) + ..retain_strand_color_on_selection = TypedReducer(retain_strand_color_on_selection_reducer)(storables.retain_strand_color_on_selection, action) ..display_reverse_DNA_right_side_up = TypedReducer(display_reverse_DNA_right_side_up_reducer)(storables.display_reverse_DNA_right_side_up, action) ..local_storage_design_choice = TypedReducer(local_storage_design_choice_reducer)(storables.local_storage_design_choice, action).toBuilder() ..clear_helix_selection_when_loading_new_design = TypedReducer(clear_helix_selection_when_loading_new_design_set_reducer)(storables.clear_helix_selection_when_loading_new_design, action) diff --git a/lib/src/serializers.dart b/lib/src/serializers.dart index cf843132f..2857f7d6c 100644 --- a/lib/src/serializers.dart +++ b/lib/src/serializers.dart @@ -348,6 +348,7 @@ part 'serializers.g.dart'; SetDisplayMajorTickWidthsAllHelices, SliceBarOffsetSet, DisablePngCachingDnaSequencesSet, + RetainStrandColorOnSelectionSet, DisplayReverseDNARightSideUpSet, SliceBarMoveStart, SliceBarMoveStop, diff --git a/lib/src/state/app_ui_state.dart b/lib/src/state/app_ui_state.dart index a4077c49a..660e45299 100644 --- a/lib/src/state/app_ui_state.dart +++ b/lib/src/state/app_ui_state.dart @@ -233,6 +233,8 @@ abstract class AppUIState with BuiltJsonSerializable implements Built storables.disable_png_caching_dna_sequences; + bool get retain_strand_color_on_selection => storables.retain_strand_color_on_selection; + bool get display_reverse_DNA_right_side_up => storables.display_reverse_DNA_right_side_up; bool get show_mouseover_data => storables.show_mouseover_data; diff --git a/lib/src/state/app_ui_state_storables.dart b/lib/src/state/app_ui_state_storables.dart index ef429f106..fc68fbf0f 100644 --- a/lib/src/state/app_ui_state_storables.dart +++ b/lib/src/state/app_ui_state_storables.dart @@ -119,6 +119,8 @@ abstract class AppUIStateStorables bool get disable_png_caching_dna_sequences; + bool get retain_strand_color_on_selection; + bool get display_reverse_DNA_right_side_up; bool get selection_box_intersection; @@ -173,6 +175,7 @@ abstract class AppUIStateStorables b.show_slice_bar = false; b.slice_bar_offset = null; b.disable_png_caching_dna_sequences = false; + b.retain_strand_color_on_selection = false; b.display_reverse_DNA_right_side_up = false; b.local_storage_design_choice = LocalStorageDesignChoice().toBuilder(); b.clear_helix_selection_when_loading_new_design = false; diff --git a/lib/src/view/design_main.dart b/lib/src/view/design_main.dart index c9772dbbe..68f8706c2 100644 --- a/lib/src/view/design_main.dart +++ b/lib/src/view/design_main.dart @@ -88,6 +88,7 @@ UiFactory ConnectedDesignMain = connect> helix_idx_to_svg_position_map; bool invert_y; @@ -263,6 +265,7 @@ class DesignMainComponent extends UiComponent2 { ..only_display_selected_helices = props.only_display_selected_helices ..helix_idx_to_svg_position_map = props.helix_idx_to_svg_position_map ..disable_png_caching_dna_sequences = props.disable_png_caching_dna_sequences + ..retain_strand_color_on_selection = props.retain_strand_color_on_selection ..display_reverse_DNA_right_side_up = props.display_reverse_DNA_right_side_up ..key = 'dna-sequences')(), diff --git a/lib/src/view/design_main_dna_sequences.dart b/lib/src/view/design_main_dna_sequences.dart index c4e80fd0e..45369a9c6 100644 --- a/lib/src/view/design_main_dna_sequences.dart +++ b/lib/src/view/design_main_dna_sequences.dart @@ -33,6 +33,7 @@ mixin DesignMainDNASequencesProps on UiProps { bool only_display_selected_helices; BuiltMap> helix_idx_to_svg_position_map; bool disable_png_caching_dna_sequences; + bool retain_strand_color_on_selection; bool display_reverse_DNA_right_side_up; } diff --git a/lib/src/view/menu.dart b/lib/src/view/menu.dart index 5575f9f66..adf96dcd6 100644 --- a/lib/src/view/menu.dart +++ b/lib/src/view/menu.dart @@ -89,6 +89,7 @@ UiFactory ConnectedMenu = connect( ..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 + ..retain_strand_color_on_selection = state.ui_state.retain_strand_color_on_selection ..display_reverse_DNA_right_side_up = state.ui_state.display_reverse_DNA_right_side_up ..local_storage_design_choice = state.ui_state.local_storage_design_choice ..clear_helix_selection_when_loading_new_design = @@ -151,6 +152,7 @@ mixin MenuPropsMixin on UiProps { bool show_loopout_extension_length; bool show_mouseover_data; bool disable_png_caching_dna_sequences; + bool retain_strand_color_on_selection; 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; @@ -1141,6 +1143,19 @@ debugging, but be warned that it will be very slow to render a large number of D props.dispatch(actions.DisablePngCachingDnaSequencesSet(!props.disable_png_caching_dna_sequences)); } ..key = 'disable-png-caching-dna-sequences')(), + (MenuBoolean() + ..value = props.retain_strand_color_on_selection + ..display = 'Retain strand color on selection' + ..tooltip = '''\ +Selected strands are normally highlighted in hot pink, which overrides the strand's color. +Select this option to not override the strand's color when it is selected. +A highlighting effect will still appear. + ''' + ..name = 'retain-strand-color-on-selection' + ..onChange = (_) { + props.dispatch(actions.RetainStrandColorOnSelectionSet(!props.retain_strand_color_on_selection)); + } + ..key = 'retain-strand-color-on-selection')(), ]; } From 1471f3fa0ef106eeb5d36d1da227e9f008bf1fc8 Mon Sep 17 00:00:00 2001 From: Raybipse Date: Sat, 28 Oct 2023 21:57:09 -0700 Subject: [PATCH 09/37] implemented logic for filtering connected base pairs --- lib/src/middleware/export_svg.dart | 21 ++++++++++++++++-- lib/src/state/design.dart | 35 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/lib/src/middleware/export_svg.dart b/lib/src/middleware/export_svg.dart index 83d3f8636..1332aea5a 100644 --- a/lib/src/middleware/export_svg.dart +++ b/lib/src/middleware/export_svg.dart @@ -2,8 +2,12 @@ import 'dart:html'; import 'dart:svg' as svg; import 'dart:svg'; +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/domain.dart'; +import 'package:scadnano/src/view/design_main_base_pair_lines.dart'; import '../app.dart'; import '../state/app_state.dart'; @@ -85,6 +89,18 @@ SvgSvgElement get_cloned_svg_element_with_style(List selected_elts) { var cloned_svg_element_with_style = SvgSvgElement() ..children = selected_elts.map(clone_and_apply_style).toList(); + List base_pairs_elements = [ + // TEMPORARY, JUST FOR TESTING + document.getElementsByClassName('base-pair-lines-main-view')[0] as Element + ]; + cloned_svg_element_with_style.children.addAll(base_pairs_elements.map(clone_and_apply_style)); + + print(app.state.design.base_pairs); + print(app.state.design.strand_to_index); + + print(app.state.design.get_selected_base_pairs( + app.state.design.base_pairs, app.state.ui_state.selectables_store.selected_strands)); + // we can't get bbox without it being added to the DOM first document.body.append(cloned_svg_element_with_style); var bbox = cloned_svg_element_with_style.getBBox(); @@ -131,10 +147,11 @@ _copy_from_elements(List svg_elements) { _export_from_element(Element svg_element, String filename_append) { var cloned_svg_element_with_style; - if (filename_append != "selected") + if (filename_append != "selected") { cloned_svg_element_with_style = clone_and_apply_style(svg_element); - else + } else { cloned_svg_element_with_style = svg_element; + } // if element is not an svg element (it can be a child element of svg e.g. groups, lines, text, etc), wrap in svg tag if (!(svg_element is svg.SvgSvgElement)) cloned_svg_element_with_style = SvgSvgElement()..children = [cloned_svg_element_with_style]; diff --git a/lib/src/state/design.dart b/lib/src/state/design.dart index f9f766c3e..533fca1bb 100644 --- a/lib/src/state/design.dart +++ b/lib/src/state/design.dart @@ -2108,6 +2108,41 @@ abstract class Design with UnusedFields implements Built, @memoized BuiltMap> get base_pairs_with_mismatches => this._base_pairs(true); + /// given `base_pairs`, returns a filtered map that is connected by at least 2 strands in `selected_strands` + BuiltMap> get_selected_base_pairs( + BuiltMap> base_pairs, BuiltSet selected_strands) { + Map> connect_cnt = {}; + + helices.forEach((i, v) { + connect_cnt[i] = List.filled(v.max_offset + 1, 0); + }); + selected_strands.forEach((strand) { + strand.substrands.forEach((substrand) { + if (substrand is Domain) { + connect_cnt[substrand.helix][substrand.start] += 1; + connect_cnt[substrand.helix][substrand.end + 1] -= 1; + } + }); + }); + connect_cnt.updateAll((i, list) { + for (int i = 1; i < list.length; ++i) { + list[i] += list[i - 1]; + } + return list; + }); + Map> connected_base_pairs = {}; + base_pairs.forEach((i, list) { + List base_pairs = []; + list.forEach((j) { + if (connect_cnt[i][j] == 2) { + base_pairs.add(j); + } + connected_base_pairs[i] = base_pairs.build(); + }); + }); + return connected_base_pairs.build(); + } + BuiltMap> _base_pairs(bool allow_mismatches) { var base_pairs = Map>(); for (int idx in this.helices.keys) { From 17f944b6808c27d764027ee8c93aa4fa9cc9dec4 Mon Sep 17 00:00:00 2001 From: Raybipse Date: Sat, 11 Nov 2023 15:36:14 -0800 Subject: [PATCH 10/37] closes #895 selected base pair lines is exported --- lib/src/middleware/export_svg.dart | 28 +++++----- lib/src/state/design.dart | 55 +++++++------------ lib/src/view/design_main_base_pair_lines.dart | 12 ++-- 3 files changed, 42 insertions(+), 53 deletions(-) diff --git a/lib/src/middleware/export_svg.dart b/lib/src/middleware/export_svg.dart index 1332aea5a..74bed0cd1 100644 --- a/lib/src/middleware/export_svg.dart +++ b/lib/src/middleware/export_svg.dart @@ -4,6 +4,7 @@ import 'dart:svg'; import 'package:built_collection/built_collection.dart'; import 'package:over_react/over_react.dart'; +import 'package:react/react_client/react_interop.dart'; import 'package:redux/redux.dart'; import 'package:scadnano/src/middleware/system_clipboard.dart'; import 'package:scadnano/src/state/domain.dart'; @@ -44,7 +45,7 @@ export_svg_middleware(Store store, dynamic action, NextDispatcher next action.type == actions.ExportSvgType.selected) { var elt = document.getElementById("main-view-svg"); if (action.type == actions.ExportSvgType.selected) { - List selected_elts = get_selected_strands(store); + List selected_elts = get_selected_base_pairs(store)..addAll(get_selected_strands(store)); if (selected_elts.length == 0) { window.alert("No strands are selected, so there is nothing to export.\n" "Please select some strands before choosing this option."); @@ -85,22 +86,23 @@ List get_selected_strands(Store store) { return selected_elts; } +List get_selected_base_pairs(Store store) { + var selected_strands = store.state.ui_state.selectables_store.selected_strands; + var base_pairs = store.state.ui_state.show_base_pair_lines_with_mismatches + ? store.state.design.selected_base_pairs_with_mismatches(selected_strands) + : store.state.design.selected_base_pairs(selected_strands); + List selected_elts = []; + for (int helix in base_pairs.keys) { + selected_elts + .addAll(base_pairs[helix].map((offset) => document.getElementById('base_pair-${helix}-${offset}'))); + } + return selected_elts; +} + SvgSvgElement get_cloned_svg_element_with_style(List selected_elts) { var cloned_svg_element_with_style = SvgSvgElement() ..children = selected_elts.map(clone_and_apply_style).toList(); - List base_pairs_elements = [ - // TEMPORARY, JUST FOR TESTING - document.getElementsByClassName('base-pair-lines-main-view')[0] as Element - ]; - cloned_svg_element_with_style.children.addAll(base_pairs_elements.map(clone_and_apply_style)); - - print(app.state.design.base_pairs); - print(app.state.design.strand_to_index); - - print(app.state.design.get_selected_base_pairs( - app.state.design.base_pairs, app.state.ui_state.selectables_store.selected_strands)); - // we can't get bbox without it being added to the DOM first document.body.append(cloned_svg_element_with_style); var bbox = cloned_svg_element_with_style.getBBox(); diff --git a/lib/src/state/design.dart b/lib/src/state/design.dart index 533fca1bb..d97aad80a 100644 --- a/lib/src/state/design.dart +++ b/lib/src/state/design.dart @@ -2101,56 +2101,39 @@ abstract class Design with UnusedFields implements Built, /// maps each helix_idx to a list of offsets where there is a complementary base pair on each strand @memoized - BuiltMap> get base_pairs => this._base_pairs(false); + BuiltMap> get base_pairs => this._base_pairs(false, strands.toBuiltSet()); /// maps each helix_idx to a list of offsets where there is a base on each strand, /// NOT necessarily complementary @memoized - BuiltMap> get base_pairs_with_mismatches => this._base_pairs(true); + BuiltMap> get base_pairs_with_mismatches => + this._base_pairs(true, strands.toBuiltSet()); - /// given `base_pairs`, returns a filtered map that is connected by at least 2 strands in `selected_strands` - BuiltMap> get_selected_base_pairs( - BuiltMap> base_pairs, BuiltSet selected_strands) { - Map> connect_cnt = {}; + // returns a subset of base_pairs that is connected to selected_strands + BuiltMap> selected_base_pairs(BuiltSet selected_strands) => + this._base_pairs(false, selected_strands); - helices.forEach((i, v) { - connect_cnt[i] = List.filled(v.max_offset + 1, 0); - }); - selected_strands.forEach((strand) { - strand.substrands.forEach((substrand) { - if (substrand is Domain) { - connect_cnt[substrand.helix][substrand.start] += 1; - connect_cnt[substrand.helix][substrand.end + 1] -= 1; - } - }); - }); - connect_cnt.updateAll((i, list) { - for (int i = 1; i < list.length; ++i) { - list[i] += list[i - 1]; - } - return list; - }); - Map> connected_base_pairs = {}; - base_pairs.forEach((i, list) { - List base_pairs = []; - list.forEach((j) { - if (connect_cnt[i][j] == 2) { - base_pairs.add(j); - } - connected_base_pairs[i] = base_pairs.build(); - }); - }); - return connected_base_pairs.build(); - } + // returns a subset of base_pairs_with_mismatches that is connected to selected_strands + BuiltMap> selected_base_pairs_with_mismatches(BuiltSet selected_strands) => + this._base_pairs(true, selected_strands); - BuiltMap> _base_pairs(bool allow_mismatches) { + BuiltMap> _base_pairs(bool allow_mismatches, BuiltSet selected_strands) { var base_pairs = Map>(); + BuiltSet selected_domains = selected_strands + .map((s) => s.substrands) + .expand((x) => x) + .where((x) => x is Domain) + .map((x) => x as Domain) + .toBuiltSet(); for (int idx in this.helices.keys) { List offsets = []; List> overlapping_domains = find_overlapping_domains_on_helix(idx); for (var domain_pair in overlapping_domains) { Domain dom1 = domain_pair.item1; Domain dom2 = domain_pair.item2; + if (!selected_domains.contains(dom1) || !selected_domains.contains(dom2)) { + continue; + } var start_and_end = dom1.compute_overlap(dom2); int start = start_and_end.item1; int end = start_and_end.item2; diff --git a/lib/src/view/design_main_base_pair_lines.dart b/lib/src/view/design_main_base_pair_lines.dart index 6ffcc1481..a4f77667d 100644 --- a/lib/src/view/design_main_base_pair_lines.dart +++ b/lib/src/view/design_main_base_pair_lines.dart @@ -2,6 +2,7 @@ import 'dart:html'; import 'package:over_react/over_react.dart'; import 'package:built_collection/built_collection.dart'; +import 'package:scadnano/scadnano.dart'; import 'package:scadnano/src/state/group.dart'; import 'package:scadnano/src/state/helix.dart'; @@ -28,14 +29,16 @@ mixin DesignMainBasePairLinesProps on UiProps { class DesignMainBasePairLinesComponent extends UiComponent2 with PureComponent { @override render() { - List base_pair_lines_components = this._create_base_pair_lines_components(); + List base_pair_lines_components = + this.create_base_pair_lines_components(app.state.design.strands.toBuiltSet()); return (Dom.g()..className = 'base-pair-lines-main-view')(base_pair_lines_components); } - List _create_base_pair_lines_components() { + List create_base_pair_lines_components(BuiltSet strands) { List base_pair_lines_components = []; - var base_pairs = - props.with_mismatches ? props.design.base_pairs_with_mismatches : props.design.base_pairs; + BuiltMap> base_pairs = props.with_mismatches + ? props.design.selected_base_pairs_with_mismatches(strands) + : props.design.selected_base_pairs(strands); for (int helix_idx in base_pairs.keys) { if (!props.only_display_selected_helices || props.side_selected_helix_idxs.contains(helix_idx)) { @@ -52,6 +55,7 @@ class DesignMainBasePairLinesComponent extends UiComponent2 Date: Wed, 15 Nov 2023 20:54:28 -0800 Subject: [PATCH 11/37] Fixed copy image not displaying base lines --- lib/src/middleware/export_svg.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/middleware/export_svg.dart b/lib/src/middleware/export_svg.dart index 2206fe9f7..bb17dd368 100644 --- a/lib/src/middleware/export_svg.dart +++ b/lib/src/middleware/export_svg.dart @@ -64,7 +64,10 @@ export_svg_middleware(Store store, dynamic action, NextDispatcher next _export_from_element(elt, 'side'); } } else if (action is actions.CopySelectedStandsToClipboardImage) { - List selected_elts = get_selected_strands(store); + List selected_elts = (!app.state.ui_state.show_base_pair_lines + ? [] + : get_selected_base_pairs(store)) + ..addAll(get_selected_strands(store)); if (selected_elts.length != 0) { _copy_from_elements(selected_elts); } From b3dd9c772aed39784529a80099d776923fbf32ca Mon Sep 17 00:00:00 2001 From: Raybipse Date: Wed, 15 Nov 2023 22:39:11 -0800 Subject: [PATCH 12/37] 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, 19 Nov 2023 21:05:26 -0800 Subject: [PATCH 13/37] closes #870: Feature request: selection preserves color --- lib/src/constants.dart | 1 + lib/src/view/design_main_strand.dart | 13 ++++- .../view/design_main_strand_crossover.dart | 7 ++- lib/src/view/design_main_strand_deletion.dart | 7 ++- lib/src/view/design_main_strand_dna_end.dart | 7 ++- lib/src/view/design_main_strand_domain.dart | 7 ++- .../view/design_main_strand_extension.dart | 7 ++- .../view/design_main_strand_insertion.dart | 8 ++- lib/src/view/design_main_strand_loopout.dart | 7 ++- .../view/design_main_strand_modification.dart | 8 ++- .../design_main_strand_modifications.dart | 5 ++ lib/src/view/design_main_strand_paths.dart | 6 ++ lib/src/view/design_main_strands.dart | 5 +- web/scadnano-styles.css | 57 +++++++++++++------ 14 files changed, 117 insertions(+), 28 deletions(-) diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 656112868..96fb85401 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -466,6 +466,7 @@ const css_selector_deletion = 'deletion-cross'; const css_selector_insertion_group = 'insertion-group'; const css_selector_deletion_group = 'deletion-group'; const css_selector_selected = 'selected'; +const css_selector_selected_pink = 'selected-pink'; const css_selector_context_menu_item_disabled = 'context_menu_item_disabled'; diff --git a/lib/src/view/design_main_strand.dart b/lib/src/view/design_main_strand.dart index 31a47e7ff..2e1a5c24b 100644 --- a/lib/src/view/design_main_strand.dart +++ b/lib/src/view/design_main_strand.dart @@ -85,6 +85,7 @@ mixin DesignMainStrandPropsMixin on UiProps { num modification_font_size; bool invert_y; BuiltMap> helix_idx_to_svg_position_map; + bool retain_strand_color_on_selection; } class DesignMainStrandProps = UiProps with DesignMainStrandPropsMixin, TransformByHelixGroupPropsMixin; @@ -100,7 +101,11 @@ class DesignMainStrandComponent extends UiComponent2 var classname = constants.css_selector_strand; if (props.selected) { - classname += ' ' + constants.css_selector_selected; + if (props.retain_strand_color_on_selection) { + classname += ' ' + constants.css_selector_selected; + } else { + classname += ' ' + constants.css_selector_selected_pink; + } } if (props.strand.is_scaffold) { classname += ' ' + constants.css_selector_scaffold; @@ -149,7 +154,8 @@ class DesignMainStrandComponent extends UiComponent2 ..moving_dna_ends = props.moving_dna_ends ..geometry = props.geometry ..helix_idx_to_svg_position_map = helix_idx_to_svg_position_y_map_on_strand - ..only_display_selected_helices = props.only_display_selected_helices)(), + ..only_display_selected_helices = props.only_display_selected_helices + ..retain_strand_color_on_selection = props.retain_strand_color_on_selection)(), _insertions(), _deletions(), if (props.show_domain_names || @@ -188,6 +194,7 @@ class DesignMainStrandComponent extends UiComponent2 ..display_connector = props.modification_display_connector ..helix_idx_to_svg_position_y_map = props.helix_idx_to_svg_position_map.map((i, p) => MapEntry(i, p.y)) + ..retain_strand_color_on_selection = props.retain_strand_color_on_selection ..key = 'modifications')(), ]); } @@ -298,6 +305,7 @@ class DesignMainStrandComponent extends UiComponent2 ..transform = transform_of_helix(domain.helix) ..svg_position_y = props.helix_idx_to_svg_position_map[helix.idx].y ..display_reverse_DNA_right_side_up = props.display_reverse_DNA_right_side_up + ..retain_strand_color_on_selection = props.retain_strand_color_on_selection ..key = util.id_insertion(domain, selectable_insertion.insertion.offset))()); } } @@ -332,6 +340,7 @@ class DesignMainStrandComponent extends UiComponent2 ..selected = props.selected_deletions_in_strand.contains(selectable_deletion) ..transform = transform_of_helix(domain.helix) ..svg_position_y = props.helix_idx_to_svg_position_map[domain.helix].y + ..retain_strand_color_on_selection = props.retain_strand_color_on_selection ..key = id)()); } } diff --git a/lib/src/view/design_main_strand_crossover.dart b/lib/src/view/design_main_strand_crossover.dart index bd38a03ef..29489ec7d 100644 --- a/lib/src/view/design_main_strand_crossover.dart +++ b/lib/src/view/design_main_strand_crossover.dart @@ -38,6 +38,7 @@ mixin DesignMainStrandCrossoverPropsMixin on UiProps { Geometry geometry; num prev_domain_helix_svg_position_y; num next_domain_helix_svg_position_y; + bool retain_strand_color_on_selection; } class DesignMainStrandCrossoverProps = UiProps @@ -63,7 +64,11 @@ class DesignMainStrandCrossoverComponent var classname = constants.css_selector_crossover; if (props.selected) { - classname += ' ' + constants.css_selector_selected; + if (props.retain_strand_color_on_selection) { + classname += ' ' + constants.css_selector_selected; + } else { + classname += ' ' + constants.css_selector_selected_pink; + } } if (props.strand.is_scaffold) { classname += ' ' + constants.css_selector_scaffold; diff --git a/lib/src/view/design_main_strand_deletion.dart b/lib/src/view/design_main_strand_deletion.dart index e4300bc25..a2372cd7c 100644 --- a/lib/src/view/design_main_strand_deletion.dart +++ b/lib/src/view/design_main_strand_deletion.dart @@ -26,6 +26,7 @@ mixin DesignMainStrandDeletionPropsMixin on UiProps { Domain get domain => selectable_deletion.domain; int get deletion => selectable_deletion.offset; num svg_position_y; + bool retain_strand_color_on_selection; } class DesignMainStrandDeletionProps = UiProps with DesignMainStrandDeletionPropsMixin; @@ -55,7 +56,11 @@ class DesignMainStrandDeletionComponent extends UiComponent2 helix_svg_position; + bool retain_strand_color_on_selection; } class DesignMainDNAEndProps = UiProps with DesignMainDNAEndPropsMixin; @@ -94,7 +95,11 @@ class DesignMainDNAEndComponent extends UiComponent2 with } if (props.selected) { - classname += ' ' + constants.css_selector_selected; + if (props.retain_strand_color_on_selection) { + classname += ' ' + constants.css_selector_selected; + } else { + classname += ' ' + constants.css_selector_selected_pink; + } } if (props.is_scaffold) { classname += ' ' + constants.css_selector_scaffold; diff --git a/lib/src/view/design_main_strand_domain.dart b/lib/src/view/design_main_strand_domain.dart index 96dff5df8..3cf08fd4c 100644 --- a/lib/src/view/design_main_strand_domain.dart +++ b/lib/src/view/design_main_strand_domain.dart @@ -51,6 +51,7 @@ mixin DesignMainDomainPropsMixin on UiProps { BuiltMap helices; BuiltMap groups; Geometry geometry; + bool retain_strand_color_on_selection; } class DesignMainDomainProps = UiProps with DesignMainDomainPropsMixin, TransformByHelixGroupPropsMixin; @@ -70,7 +71,11 @@ class DesignMainDomainComponent extends UiComponent2 var classname = constants.css_selector_domain; if (props.selected) { - classname += ' ' + constants.css_selector_selected; + if (props.retain_strand_color_on_selection) { + classname += ' ' + constants.css_selector_selected; + } else { + classname += ' ' + constants.css_selector_selected_pink; + } } if (props.strand.is_scaffold) { classname += ' ' + constants.css_selector_scaffold; diff --git a/lib/src/view/design_main_strand_extension.dart b/lib/src/view/design_main_strand_extension.dart index 43437b758..a5e77efbf 100644 --- a/lib/src/view/design_main_strand_extension.dart +++ b/lib/src/view/design_main_strand_extension.dart @@ -53,6 +53,7 @@ mixin DesignMainExtensionPropsMixin on UiProps { BuiltMap helices; BuiltMap groups; Geometry geometry; + bool retain_strand_color_on_selection; } class DesignMainExtensionProps = UiProps with DesignMainExtensionPropsMixin, TransformByHelixGroupPropsMixin; @@ -76,7 +77,11 @@ class DesignMainExtensionComponent extends UiComponent2 selectable_insertion.insertion; Domain get domain => selectable_insertion.domain; + + bool retain_strand_color_on_selection; } class DesignMainStrandInsertionProps = UiProps with DesignMainStrandInsertionPropsMixin; @@ -50,7 +52,11 @@ class DesignMainStrandInsertionComponent extends UiComponent2 { @@ -87,7 +89,11 @@ class DesignMainStrandModificationComponent extends UiComponent2 selected_modifications_in_strand; BuiltMap helix_idx_to_svg_position_y_map; + + bool retain_strand_color_on_selection; } class DesignMainStrandModificationsProps = UiProps @@ -65,6 +67,7 @@ class DesignMainStrandModificationsComponent extends UiComponent2 Function(Strand strand, {Substrand substrand, Address address, ModificationType type}) context_menu_strand; BuiltMap> helix_idx_to_svg_position_map; + bool retain_strand_color_on_selection; } class DesignMainStrandPathsProps = UiProps @@ -115,6 +116,7 @@ class DesignMainStrandPathsComponent extends UiComponent2 ConnectedDesignMainStrands = ..domain_label_font_size = state.ui_state.domain_label_font_size ..helix_idx_to_svg_position_map = state.helix_idx_to_svg_position_map ..display_reverse_DNA_right_side_up = state.ui_state.display_reverse_DNA_right_side_up - ..geometry = state.design.geometry; + ..geometry = state.design.geometry + ..retain_strand_color_on_selection = state.ui_state.retain_strand_color_on_selection; })(DesignMainStrands); UiFactory DesignMainStrands = _$DesignMainStrands; @@ -73,6 +74,7 @@ mixin DesignMainStrandsProps on UiProps { bool display_reverse_DNA_right_side_up; Geometry geometry; BuiltMap> helix_idx_to_svg_position_map; + bool retain_strand_color_on_selection; } class DesignMainStrandsComponent extends UiComponent2 with PureComponent { @@ -131,6 +133,7 @@ class DesignMainStrandsComponent extends UiComponent2 wi ..geometry = props.geometry ..helix_idx_to_svg_position_map = props.helix_idx_to_svg_position_map ..display_reverse_DNA_right_side_up = props.display_reverse_DNA_right_side_up + ..retain_strand_color_on_selection = props.retain_strand_color_on_selection ..key = strand.toString())()); } diff --git a/web/scadnano-styles.css b/web/scadnano-styles.css index c697a24a7..ffab8b271 100644 --- a/web/scadnano-styles.css +++ b/web/scadnano-styles.css @@ -684,7 +684,6 @@ label + select { fill: lightgray; } - /* https://www.w3schools.com/cssref/css_selectors.asp */ /* These are needed to make parts pink when they are selected individually. */ .selected.crossover-curve, @@ -693,7 +692,31 @@ label + select { .selected.deletion-cross, .selected.modification, .selected.domain-line, -.selected.extension-line { +.selected.extension-line, +.selected .crossover-curve, +.selected .loopout-curve, +.selected .insertion-curve, +.selected .deletion-cross, +.selected .modification, +.selected .domain-line, +.selected .extension-line { + stroke-width: 5pt; +} + +.selected-pink.crossover-curve, +.selected-pink.loopout-curve, +.selected-pink.insertion-curve, +.selected-pink.deletion-cross, +.selected-pink.modification, +.selected-pink.domain-line, +.selected-pink.extension-line, +.selected-pink .crossover-curve, +.selected-pink .loopout-curve, +.selected-pink .insertion-curve, +.selected-pink .deletion-cross, +.selected-pink .modification, +.selected-pink .domain-line, +.selected-pink .extension-line { stroke: hotpink; stroke-width: 5pt; } @@ -702,36 +725,36 @@ label + select { .selected.three-prime-end, .selected.five-prime-end-first-substrand, .selected.three-prime-end-last-substrand { - stroke: red; - fill: hotpink; visibility: visible; stroke-width: 1pt; } -/* https://www.w3schools.com/cssref/css_selectors.asp */ -/* These are needed to make all parts pink when whole strand is selected, */ -/* i.e., a parent of these components is selected */ -.selected .crossover-curve, -.selected .loopout-curve, -.selected .insertion-curve, -.selected .deletion-cross, -.selected .modification, -.selected .domain-line, -.selected .extension-line { - stroke: hotpink; - stroke-width: 5pt; +.selected-pink.five-prime-end, +.selected-pink.three-prime-end, +.selected-pink.five-prime-end-first-substrand, +.selected-pink.three-prime-end-last-substrand { + stroke: red; + fill: hotpink; + visibility: visible; + stroke-width: 1pt; } .selected .five-prime-end, .selected .three-prime-end, .selected .five-prime-end-first-substrand, .selected .three-prime-end-last-substrand { + stroke-width: 1pt; +} + +.selected-pink .five-prime-end, +.selected-pink .three-prime-end, +.selected-pink .five-prime-end-first-substrand, +.selected-pink .three-prime-end-last-substrand { stroke: red; fill: hotpink; stroke-width: 1pt; } - /* end formatting of Strand elements when selected/selectable/not selectable */ /*****************************************************************************/ From d68babcfb552644d57dd5abc5a909bb6807ecc2b Mon Sep 17 00:00:00 2001 From: Edwin Chang Date: Mon, 20 Nov 2023 11:50:21 -0800 Subject: [PATCH 14/37] reformat code --- lib/src/actions/actions.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/actions/actions.dart b/lib/src/actions/actions.dart index 822c6d855..87530deb5 100644 --- a/lib/src/actions/actions.dart +++ b/lib/src/actions/actions.dart @@ -4357,7 +4357,7 @@ abstract class RetainStrandColorOnSelectionSet factory RetainStrandColorOnSelectionSet(bool retain_strand_color_on_selection) => RetainStrandColorOnSelectionSet.from( (b) => b..retain_strand_color_on_selection = retain_strand_color_on_selection); - + factory RetainStrandColorOnSelectionSet.from( [void Function(RetainStrandColorOnSelectionSetBuilder) updates]) = _$RetainStrandColorOnSelectionSet; From 94c0b0f3e3e368bbb60f114b8f86d3cc30b1bc33 Mon Sep 17 00:00:00 2001 From: RayBipse Date: Mon, 20 Nov 2023 23:02:24 -0800 Subject: [PATCH 15/37] Refractored code for readability and consistency --- lib/src/middleware/export_svg.dart | 48 ++++++++++++++++-------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/lib/src/middleware/export_svg.dart b/lib/src/middleware/export_svg.dart index bb17dd368..9515d09c2 100644 --- a/lib/src/middleware/export_svg.dart +++ b/lib/src/middleware/export_svg.dart @@ -8,6 +8,7 @@ import 'package:react/react_client/react_interop.dart'; import 'package:redux/redux.dart'; import 'package:scadnano/src/middleware/system_clipboard.dart'; import 'package:scadnano/src/state/domain.dart'; +import 'package:scadnano/src/state/strand.dart'; import 'package:scadnano/src/view/design_main_base_pair_lines.dart'; import '../app.dart'; @@ -45,10 +46,7 @@ export_svg_middleware(Store store, dynamic action, NextDispatcher next action.type == actions.ExportSvgType.selected) { var elt = document.getElementById("main-view-svg"); if (action.type == actions.ExportSvgType.selected) { - List selected_elts = (!app.state.ui_state.show_base_pair_lines - ? [] - : get_selected_base_pairs(store)) - ..addAll(get_selected_strands(store)); + List selected_elts = get_selected_svg_elements(store.state); if (selected_elts.length == 0) { window.alert("No strands are selected, so there is nothing to export.\n" "Please select some strands before choosing this option."); @@ -64,10 +62,7 @@ export_svg_middleware(Store store, dynamic action, NextDispatcher next _export_from_element(elt, 'side'); } } else if (action is actions.CopySelectedStandsToClipboardImage) { - List selected_elts = (!app.state.ui_state.show_base_pair_lines - ? [] - : get_selected_base_pairs(store)) - ..addAll(get_selected_strands(store)); + List selected_elts = get_selected_svg_elements(store.state); if (selected_elts.length != 0) { _copy_from_elements(selected_elts); } @@ -78,31 +73,38 @@ export_svg_middleware(Store store, dynamic action, NextDispatcher next } } -List get_selected_strands(Store store) { - var selected_strands = store.state.ui_state.selectables_store.selected_strands; +List get_selected_svg_elements(AppState state) { + BuiltSet selected_strands = state.ui_state.selectables_store.selected_strands; List selected_elts = []; - if (selected_strands.length != 0) { - for (var strand in selected_strands) { + if (app.state.ui_state.show_base_pair_lines) { + var base_pairs = state.ui_state.show_base_pair_lines_with_mismatches + ? state.design.selected_base_pairs_with_mismatches(selected_strands) + : state.design.selected_base_pairs(selected_strands); + selected_elts.addAll(get_svg_elements_of_base_pairs(base_pairs)); + } + selected_elts.addAll(get_svg_elements_of_strands(selected_strands)); + return selected_elts; +} + +List get_svg_elements_of_strands(BuiltSet strands) { + List elts = []; + if (strands.length != 0) { + for (var strand in strands) { var strand_elt = document.getElementById(strand.id); var dna_seq_elt = document.getElementById('dna-sequence-${strand.id}'); var mismatch_elts = document.getElementsByClassName('mismatch-${strand.id}'); - selected_elts.addAll([strand_elt, if (dna_seq_elt != null) dna_seq_elt, ...mismatch_elts]); + elts.addAll([strand_elt, if (dna_seq_elt != null) dna_seq_elt, ...mismatch_elts]); } } - return selected_elts; + return elts; } -List get_selected_base_pairs(Store store) { - var selected_strands = store.state.ui_state.selectables_store.selected_strands; - var base_pairs = store.state.ui_state.show_base_pair_lines_with_mismatches - ? store.state.design.selected_base_pairs_with_mismatches(selected_strands) - : store.state.design.selected_base_pairs(selected_strands); - List selected_elts = []; +List get_svg_elements_of_base_pairs(BuiltMap> base_pairs) { + List elts = []; for (int helix in base_pairs.keys) { - selected_elts - .addAll(base_pairs[helix].map((offset) => document.getElementById('base_pair-${helix}-${offset}'))); + elts.addAll(base_pairs[helix].map((offset) => document.getElementById('base_pair-${helix}-${offset}'))); } - return selected_elts; + return elts; } SvgSvgElement get_cloned_svg_element_with_style(List selected_elts) { From 1a662866036386ec3d5cf5d4714ccde7eba2e1b1 Mon Sep 17 00:00:00 2001 From: RayBipse Date: Sun, 17 Dec 2023 16:29:34 -0800 Subject: [PATCH 16/37] 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 17/37] 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 18/37] 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: Fri, 22 Dec 2023 10:54:26 -0600 Subject: [PATCH 19/37] fixes #959: display DNA sequence on extension in order 5' to 3' --- .../view/design_main_strand_extension.dart | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/src/view/design_main_strand_extension.dart b/lib/src/view/design_main_strand_extension.dart index 19dfd1073..4521bd4f1 100644 --- a/lib/src/view/design_main_strand_extension.dart +++ b/lib/src/view/design_main_strand_extension.dart @@ -89,12 +89,24 @@ class DesignMainExtensionComponent extends UiComponent2 right_svg.x) { - var swap = left_svg; - left_svg = right_svg; - right_svg = swap; + //NOTE: this causes the DNA to appear backwards in cases when the 3' end appears to the left + // of the 5' end. For now we will ditch this and draw the extension path from 5' to 3' + // (see svg_5p and svg_3p below), and figure out later how to display the name properly. + // Leaving this code here to help with that later. + // var left_svg = extension_free_end_svg; + // var right_svg = extension_attached_end_svg; + // if (left_svg.x > right_svg.x) { + // var swap = left_svg; + // left_svg = right_svg; + // right_svg = swap; + // } + + var svg_5p = extension_free_end_svg; + var svg_3p = extension_attached_end_svg; + if (!ext.is_5p) { + var swap = svg_5p; + svg_5p = svg_3p; + svg_3p = swap; } var color = ext.color ?? props.strand_color; @@ -102,8 +114,10 @@ class DesignMainExtensionComponent extends UiComponent2 Date: Fri, 22 Dec 2023 11:15:18 -0600 Subject: [PATCH 20/37] fixes bug with setting extension name when no extensions are selected --- lib/src/view/design_main_strand_extension.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/src/view/design_main_strand_extension.dart b/lib/src/view/design_main_strand_extension.dart index 4521bd4f1..47894e680 100644 --- a/lib/src/view/design_main_strand_extension.dart +++ b/lib/src/view/design_main_strand_extension.dart @@ -255,10 +255,16 @@ class DesignMainExtensionComponent extends UiComponent2 actions.SubstrandNameSet(name: name, substrand: e)), - "set extension names")); + var selected_exts = app.state.ui_state.selectables_store.selected_extensions; + var action; + if (selected_exts.length > 1) { + action = actions.BatchAction( + selected_exts.map((e) => actions.SubstrandNameSet(name: name, substrand: e)), + "set extension names"); + } else { + action = actions.SubstrandNameSet(name: name, substrand: props.ext); + } + app.dispatch(action); } extension_display_length_and_angle_change() => From 35f4e9f44ab63b244c6ffa690155dd85e4691708 Mon Sep 17 00:00:00 2001 From: David Doty Date: Fri, 22 Dec 2023 11:16:09 -0600 Subject: [PATCH 21/37] fixes bug with selecting strand "pieces" (domains/extensions/ends/etc) with new property `retain_strand_color_on_selection`, which was not being properly prop-drilled into those view components --- lib/src/view/design_main_strand_paths.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/view/design_main_strand_paths.dart b/lib/src/view/design_main_strand_paths.dart index 3c3fa5503..6ac9606f1 100644 --- a/lib/src/view/design_main_strand_paths.dart +++ b/lib/src/view/design_main_strand_paths.dart @@ -114,6 +114,7 @@ class DesignMainStrandPathsComponent extends UiComponent2 Date: Fri, 22 Dec 2023 11:34:47 -0600 Subject: [PATCH 22/37] fixed some variable/file names --- lib/src/view/design_main_strand_and_domain_texts.dart | 10 +++++----- ...ame.dart => design_main_strand_extension_text.dart} | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename lib/src/view/{design_main_strand_extension_name.dart => design_main_strand_extension_text.dart} (96%) diff --git a/lib/src/view/design_main_strand_and_domain_texts.dart b/lib/src/view/design_main_strand_and_domain_texts.dart index 2ecc3646d..97b8477f0 100644 --- a/lib/src/view/design_main_strand_and_domain_texts.dart +++ b/lib/src/view/design_main_strand_and_domain_texts.dart @@ -16,7 +16,7 @@ import '../state/loopout.dart'; import 'design_main_strand_paths.dart'; import 'design_main_strand_domain_text.dart'; import 'design_main_strand_loopout_name.dart'; -import 'design_main_strand_extension_name.dart'; +import 'design_main_strand_extension_text.dart'; import 'pure_component.dart'; import '../state/strand.dart'; import '../state/helix.dart'; @@ -241,11 +241,11 @@ class DesignMainStrandAndDomainTextsComponent extends UiComponent2 DesignMainStrandExtensionText = _$DesignMainStrandExtensionText; From c9c28945a6808e64df5672d19105f55a871ab259 Mon Sep 17 00:00:00 2001 From: David Doty Date: Fri, 22 Dec 2023 11:45:30 -0600 Subject: [PATCH 23/37] fixed problem with non-unique React key when a strand has a 5' and a 3' extension adjacent to the same domain --- lib/src/view/design_main_dna_sequence.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/design_main_dna_sequence.dart b/lib/src/view/design_main_dna_sequence.dart index 26aedb26e..d5b8cce35 100644 --- a/lib/src/view/design_main_dna_sequence.dart +++ b/lib/src/view/design_main_dna_sequence.dart @@ -244,7 +244,7 @@ class DesignMainDNASequenceComponent extends UiComponent2 Date: Thu, 11 Jan 2024 22:09:16 -0800 Subject: [PATCH 24/37] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 49a0b0f42..18bc41898 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # scadnano [scadnano](http://scadnano.org) -(" -scriptable-cadnano", [source code repository here](https://github.com/UC-Davis-molecular-computing/scadnano)) +("scriptable-cadnano", [source code repository here](https://github.com/UC-Davis-molecular-computing/scadnano)) is a program for designing synthetic DNA structures such as DNA origami. The scadnano project is developed and maintained by the UC Davis Molecular Computing group. Note that [cadnano](https://cadnano.org) is a separate project, developed and maintained by From 1e78e1e211794e47c596534960f6df8f935728bf Mon Sep 17 00:00:00 2001 From: RayBipse Date: Sat, 27 Jan 2024 12:30:41 -0800 Subject: [PATCH 25/37] 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 26/37] 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 27/37] 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 28/37] 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 29/37] 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 30/37] 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 31/37] 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." From 34ba098fe6b686859805fc039d83308a0a77ec0c Mon Sep 17 00:00:00 2001 From: David Doty Date: Sat, 10 Feb 2024 16:12:25 -0800 Subject: [PATCH 32/37] added ability to exclude scaffolds from selection when using "select all with same..." --- lib/src/actions/actions.dart | 8 ++++++-- lib/src/reducers/selection_reducer.dart | 1 + lib/src/state/selectable.dart | 7 ++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/src/actions/actions.dart b/lib/src/actions/actions.dart index 595719924..7740a1ab2 100644 --- a/lib/src/actions/actions.dart +++ b/lib/src/actions/actions.dart @@ -1571,9 +1571,13 @@ abstract class SelectAllWithSameAsSelected BuiltList get traits; + bool get exclude_scaffolds; + /************************ begin BuiltValue boilerplate ************************/ - factory SelectAllWithSameAsSelected({BuiltList templates, BuiltList traits}) = - _$SelectAllWithSameAsSelected._; + factory SelectAllWithSameAsSelected( + {BuiltList templates, + BuiltList traits, + bool exclude_scaffolds}) = _$SelectAllWithSameAsSelected._; SelectAllWithSameAsSelected._(); diff --git a/lib/src/reducers/selection_reducer.dart b/lib/src/reducers/selection_reducer.dart index 40383d496..bec748a2d 100644 --- a/lib/src/reducers/selection_reducer.dart +++ b/lib/src/reducers/selection_reducer.dart @@ -159,6 +159,7 @@ SelectablesStore select_all_with_same_reducer( var selected_strands = selectables_store.selected_strands.toList(); for (var strand in state.design.strands) { if (selected_strands.contains(strand)) continue; + if (action.exclude_scaffolds && strand.is_scaffold) continue; bool include_strand = true; // by default include the strand; now go looking for a trait where it doesn't match for (var trait in trait_values.keys) { diff --git a/lib/src/state/selectable.dart b/lib/src/state/selectable.dart index 66182b373..e80c417ef 100644 --- a/lib/src/state/selectable.dart +++ b/lib/src/state/selectable.dart @@ -670,12 +670,15 @@ Future ask_for_select_all_with_same_as_selected() async { } var all_traits = List.from(SelectableTrait.values); - var items = List.filled(all_traits.length, null); + var items = List.filled(all_traits.length + 1, null); for (int idx = 0; idx < all_traits.length; idx++) { var trait = all_traits[idx]; items[idx] = DialogCheckbox(label: trait.description, value: false); } + items[all_traits.length] = DialogCheckbox(label: '(Exclude scaffold(s))', value: false, tooltip: '''\ +If checked, then only strands that are not scaffolds will be selected. +However, *currently* selected scaffold strands will remain selected.'''); var dialog = Dialog( title: "Select all strands with same traits as currently selected strand(s)", @@ -693,10 +696,12 @@ Future ask_for_select_all_with_same_as_selected() async { traits_for_selection.add(trait); } } + bool exclude_scaffolds = (results[all_traits.length] as DialogCheckbox).value; var action = actions.SelectAllWithSameAsSelected( templates: selected_strands, traits: traits_for_selection.build(), + exclude_scaffolds: exclude_scaffolds, ); app.dispatch(action); } From 33a56308a87079f326e24244a722b6093a12345c Mon Sep 17 00:00:00 2001 From: Dave Doty Date: Thu, 22 Feb 2024 14:55:11 -0800 Subject: [PATCH 33/37] closes #964: exclude selected strands from export DNA sequences --- lib/src/actions/actions.dart | 5 ++++ lib/src/middleware/export_dna_sequences.dart | 31 +++++++++++++++----- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/lib/src/actions/actions.dart b/lib/src/actions/actions.dart index 7740a1ab2..9d2c2b9e3 100644 --- a/lib/src/actions/actions.dart +++ b/lib/src/actions/actions.dart @@ -2080,6 +2080,8 @@ abstract class ExportDNA with BuiltJsonSerializable implements Action, Built b ..include_scaffold = include_scaffold ..include_only_selected_strands = include_only_selected_strands + ..exclude_selected_strands = exclude_selected_strands ..export_dna_format = export_dna_format ..delimiter = delimiter ..domain_delimiter = domain_delimiter diff --git a/lib/src/middleware/export_dna_sequences.dart b/lib/src/middleware/export_dna_sequences.dart index 5c8e1c1b5..86a50c2ad 100644 --- a/lib/src/middleware/export_dna_sequences.dart +++ b/lib/src/middleware/export_dna_sequences.dart @@ -45,6 +45,11 @@ export_dna_sequences_middleware(Store store, action, NextDispatcher ne List strands; if (action.include_only_selected_strands) { strands = state.ui_state.selectables_store.selected_strands.toList(); + } else if (action.exclude_selected_strands) { + strands = state.design.strands.toList(); + for (var strand in state.ui_state.selectables_store.selected_strands) { + strands.remove(strand); + } } else { strands = state.design.strands.toList(); } @@ -107,15 +112,16 @@ Future export_dna() async { int idx_include_scaffold = 0; int idx_include_only_selected_strands = 1; - int idx_format_str = 2; - int idx_delimiter = 3; - int idx_domain_delimiter = 4; - int idx_column_major_plate = 5; - int idx_sort = 6; - int idx_column_major_strand = 7; - int idx_strand_order_str = 8; + int idx_exclude_selected_strands = 2; + int idx_format_str = 3; + int idx_delimiter = 4; + int idx_domain_delimiter = 5; + int idx_column_major_plate = 6; + int idx_sort = 7; + int idx_column_major_strand = 8; + int idx_strand_order_str = 9; - List items = List.filled(9, null); + List items = List.filled(idx_strand_order_str + 1, null); items[idx_delimiter] = DialogText(label: 'delimiter between IDT fields', value: ',', tooltip: '''\ Delimiter to separate IDT fields in a "bulk input" text file, for instance if set to ";", then a line @@ -132,6 +138,7 @@ if it had three domains each of length 5.'''); items[idx_include_scaffold] = DialogCheckbox(label: 'include scaffold', value: false); items[idx_include_only_selected_strands] = DialogCheckbox(label: 'include only selected strands', value: false); + items[idx_exclude_selected_strands] = DialogCheckbox(label: 'exclude selected strands', value: false); items[idx_format_str] = DialogRadio( label: 'export format', options: export_options, @@ -178,6 +185,10 @@ which part of the strand to use as the address. title: 'export DNA sequences', type: DialogType.export_dna_sequences, items: items, + disable_when_any_checkboxes_on: { + idx_include_only_selected_strands: [idx_exclude_selected_strands], + idx_exclude_selected_strands: [idx_include_only_selected_strands], + }, disable_when_any_checkboxes_off: { idx_column_major_strand: [idx_sort], idx_strand_order_str: [idx_sort], @@ -199,6 +210,7 @@ which part of the strand to use as the address. bool include_scaffold = (results[idx_include_scaffold] as DialogCheckbox).value; bool include_only_selected_strands = (results[idx_include_only_selected_strands] as DialogCheckbox).value; + bool exclude_selected_strands = (results[idx_exclude_selected_strands] as DialogCheckbox).value; String format_str = (results[idx_format_str] as DialogRadio).value; bool sort = (results[idx_sort] as DialogCheckbox).value; StrandOrder strand_order = null; @@ -213,9 +225,12 @@ which part of the strand to use as the address. String delimiter = (results[idx_delimiter] as DialogText).value; String domain_delimiter = (results[idx_domain_delimiter] as DialogText).value; + assert(!(include_only_selected_strands && exclude_selected_strands)); + app.dispatch(actions.ExportDNA( include_scaffold: include_scaffold, include_only_selected_strands: include_only_selected_strands, + exclude_selected_strands: exclude_selected_strands, export_dna_format: format, delimiter: delimiter, domain_delimiter: domain_delimiter, From 77e21efabf2c179aaae6fa780b58e0c57d63039f Mon Sep 17 00:00:00 2001 From: RayBipse Date: Sun, 3 Mar 2024 17:08:57 -0800 Subject: [PATCH 34/37] Added the button for base pair display --- lib/src/actions/actions.dart | 16 +++++++ lib/src/reducers/app_ui_state_reducer.dart | 6 +++ lib/src/serializers.dart | 3 ++ lib/src/state/app_ui_state.dart | 3 ++ lib/src/state/app_ui_state_storables.dart | 4 ++ lib/src/state/base_pair_display_type.dart | 54 ++++++++++++++++++++++ lib/src/state/dialog.dart | 1 + lib/src/view/menu.dart | 20 ++++++++ 8 files changed, 107 insertions(+) create mode 100644 lib/src/state/base_pair_display_type.dart diff --git a/lib/src/actions/actions.dart b/lib/src/actions/actions.dart index 9d2c2b9e3..493ad6bb9 100644 --- a/lib/src/actions/actions.dart +++ b/lib/src/actions/actions.dart @@ -4011,6 +4011,22 @@ abstract class ExampleDesignsLoad static Serializer get serializer => _$exampleDesignsLoadSerializer; } +///////////////////////////////////////////////////////////////////////////////////////////////////////////// +// pair lines display + +abstract class BasePairTypeSet + with BuiltJsonSerializable + implements Action, Built { + int get selected_idx; + + /************************ begin BuiltValue boilerplate ************************/ + factory BasePairTypeSet({int selected_idx}) = _$BasePairTypeSet._; + + BasePairTypeSet._(); + + static Serializer get serializer => _$basePairTypeSetSerializer; +} + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // change helix position diff --git a/lib/src/reducers/app_ui_state_reducer.dart b/lib/src/reducers/app_ui_state_reducer.dart index 6bd4442d7..2f5f329c0 100644 --- a/lib/src/reducers/app_ui_state_reducer.dart +++ b/lib/src/reducers/app_ui_state_reducer.dart @@ -4,6 +4,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:redux/redux.dart'; import 'package:scadnano/src/reducers/design_reducer.dart'; import 'package:scadnano/src/reducers/strands_copy_info_reducer.dart'; +import 'package:scadnano/src/state/base_pair_display_type.dart'; import 'package:scadnano/src/state/dna_assign_options.dart'; import 'package:scadnano/src/state/modification.dart'; import 'package:scadnano/src/state/strand.dart'; @@ -230,6 +231,10 @@ bool display_major_tick_widths_all_helices_reducer( bool _, actions.SetDisplayMajorTickWidthsAllHelices action) => action.show; +BasePairDisplayType base_pair_type_idx_reducer( + BasePairDisplayType set_base_pair_display, actions.BasePairTypeSet action) => + BasePairDisplayType.types[action.selected_idx]; + bool show_base_pair_lines_reducer(bool _, actions.ShowBasePairLinesSet action) => action.show_base_pair_lines; bool show_base_pair_lines_with_mismatches_reducer( @@ -451,6 +456,7 @@ AppUIStateStorables app_ui_state_storable_local_reducer(AppUIStateStorables stor ..display_base_offsets_of_major_ticks_only_first_helix = TypedReducer(display_base_offsets_of_major_ticks_only_first_helix_reducer)(storables.display_base_offsets_of_major_ticks_only_first_helix, action) ..display_major_tick_widths = TypedReducer(display_major_tick_widths_reducer)(storables.display_major_tick_widths, action) ..display_major_tick_widths_all_helices = TypedReducer(display_major_tick_widths_all_helices_reducer)(storables.display_major_tick_widths_all_helices, action) + ..base_pair_display_type = TypedReducer(base_pair_type_idx_reducer)(storables.base_pair_display_type, 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) diff --git a/lib/src/serializers.dart b/lib/src/serializers.dart index fff3cc414..3e3adf1de 100644 --- a/lib/src/serializers.dart +++ b/lib/src/serializers.dart @@ -6,6 +6,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/standard_json_plugin.dart'; import 'package:color/color.dart'; import 'package:scadnano/src/dna_file_type.dart'; +import 'package:scadnano/src/state/base_pair_display_type.dart'; import 'package:scadnano/src/state/dna_extensions_move.dart'; import 'package:scadnano/src/state/undo_redo.dart'; import 'package:tuple/tuple.dart'; @@ -137,6 +138,7 @@ part 'serializers.g.dart'; SelectionBoxRemove, SelectionRope, Line, + BasePairDisplayType, SelectionRopeCreate, SelectionRopeMouseMove, SelectionRopeAddPoint, @@ -316,6 +318,7 @@ part 'serializers.g.dart'; StrandPasteKeepColorSet, ExampleDesigns, ExampleDesignsLoad, + BasePairTypeSet, HelixPositionSet, HelixGridPositionSet, InlineInsertionsDeletions, diff --git a/lib/src/state/app_ui_state.dart b/lib/src/state/app_ui_state.dart index 38a9c4a0d..259835580 100644 --- a/lib/src/state/app_ui_state.dart +++ b/lib/src/state/app_ui_state.dart @@ -12,6 +12,7 @@ import '../state/local_storage_design_choice.dart'; import 'app_ui_state_storables.dart'; import '../serializers.dart'; +import 'base_pair_display_type.dart'; import 'context_menu.dart'; import 'dialog.dart'; import 'design.dart'; @@ -162,6 +163,8 @@ abstract class AppUIState with BuiltJsonSerializable implements Built storables.show_domain_labels; + BasePairDisplayType get base_pair_display_type => storables.base_pair_display_type; + bool get show_base_pair_lines => storables.show_base_pair_lines; bool get show_base_pair_lines_with_mismatches => storables.show_base_pair_lines_with_mismatches; diff --git a/lib/src/state/app_ui_state_storables.dart b/lib/src/state/app_ui_state_storables.dart index b83fd76ec..fdac0928a 100644 --- a/lib/src/state/app_ui_state_storables.dart +++ b/lib/src/state/app_ui_state_storables.dart @@ -4,6 +4,7 @@ import 'package:built_value/built_value.dart'; import '../state/local_storage_design_choice.dart'; import '../serializers.dart'; +import 'base_pair_display_type.dart'; import 'select_mode_state.dart'; import 'edit_mode.dart'; import '../constants.dart' as constants; @@ -34,6 +35,8 @@ abstract class AppUIStateStorables bool get show_domain_labels; + BasePairDisplayType get base_pair_display_type; + bool get show_base_pair_lines; bool get show_base_pair_lines_with_mismatches; @@ -139,6 +142,7 @@ abstract class AppUIStateStorables b.show_strand_labels = false; b.show_domain_names = false; b.show_domain_labels = false; + b.base_pair_display_type = BasePairDisplayType.none; b.show_base_pair_lines = false; b.show_base_pair_lines_with_mismatches = false; b.strand_name_font_size = constants.default_strand_name_font_size; diff --git a/lib/src/state/base_pair_display_type.dart b/lib/src/state/base_pair_display_type.dart new file mode 100644 index 000000000..8f664e936 --- /dev/null +++ b/lib/src/state/base_pair_display_type.dart @@ -0,0 +1,54 @@ +import 'dart:html'; + +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; +import 'package:built_collection/built_collection.dart'; + +part 'base_pair_display_type.g.dart'; + +class BasePairDisplayType extends EnumClass { + const BasePairDisplayType._(String name) : super(name); + + @memoized + int get hashCode; + + static Serializer get serializer => _$basePairDisplayTypeSerializer; + + /******************** end BuiltValue boilerplate *********************/ + + static const BasePairDisplayType none = _$none; + static const BasePairDisplayType lines = _$lines; + static const BasePairDisplayType rectangle = _$rectangle; // used to join two Domains with Crossover + static BuiltList types = const [none, lines, rectangle].toBuiltList(); + + static BuiltSet get values => _$values; + + static BasePairDisplayType valueOf(String name) => _$valueOf(name); + + String to_json() => name; + + String display_name() { + // edit this to display a different string than the identifier name above + switch (this) { + case none: + return 'none'; + case lines: + return 'lines'; + case rectangle: + return 'rectangle'; + } + return super.toString(); + } + + @override + String toString() => display_name(); + + static BasePairDisplayType from_json(String the_name) { + for (var val in values) { + if (val.name == the_name) { + return val; + } + } + throw ArgumentError('there is no base pair display type with name "${the_name}"'); + } +} diff --git a/lib/src/state/dialog.dart b/lib/src/state/dialog.dart index 74c53ebb6..ca37e13c4 100644 --- a/lib/src/state/dialog.dart +++ b/lib/src/state/dialog.dart @@ -47,6 +47,7 @@ class DialogType extends EnumClass { static const DialogType move_selected_helices_to_group = _$move_selected_helices_to_group; static const DialogType export_dna_sequences = _$export_dna_sequences; static const DialogType load_example_dna_design = _$load_example_dna_design; + static const DialogType base_pair_display = _$base_pair_display; static const DialogType add_extension = _$add_extension; static const DialogType set_extension_name = _$set_extension_name; static const DialogType set_extension_display_length_angle = _$set_extension_display_length_angle; diff --git a/lib/src/view/menu.dart b/lib/src/view/menu.dart index 08e98c79f..1d84fdb9a 100644 --- a/lib/src/view/menu.dart +++ b/lib/src/view/menu.dart @@ -4,6 +4,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:path/path.dart' as path; import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; +import 'package:scadnano/src/state/base_pair_display_type.dart'; import 'react_bootstrap.dart'; @@ -134,6 +135,7 @@ mixin MenuPropsMixin on UiProps { bool autofit; bool only_display_selected_helices; ExampleDesigns example_designs; + // SetBasePairDisplay base_pair_display_types; bool design_has_insertions_or_deletions; bool undo_stack_empty; bool redo_stack_empty; @@ -1001,6 +1003,10 @@ or real coordinates in nanometers, depending on whether a grid is selected).''' ..id = 'view_menu_base_pairs' ..key = 'view_menu_base_pairs-dropdown' ..className = 'submenu_item')([ + (MenuDropdownItem() + ..on_click = ((_) => app.disable_keyboard_shortcuts_while(base_pair_display_dialog)) + ..display = 'Base pair display' + ..key = 'base-pair-display')(), (MenuBoolean() ..value = props.show_base_pair_lines ..display = 'Base pair lines' @@ -1436,6 +1442,20 @@ However, it may be less stable than the main site.''' int selected_idx = (results[0] as DialogRadio).selected_idx; props.dispatch(actions.ExampleDesignsLoad(selected_idx: selected_idx)); } + + Future base_pair_display_dialog() async { + var dialog = Dialog(title: 'Base pair display', type: DialogType.base_pair_display, items: [ + DialogRadio( + label: 'types', + options: BasePairDisplayType.types.map((v) => v.display_name()), + ), + ]); + List results = await util.dialog(dialog); + if (results == null) return; + + int selected_idx = (results[0] as DialogRadio).selected_idx; + props.dispatch(actions.BasePairTypeSet(selected_idx: selected_idx)); + } } typedef ActionFromIntCreator = actions.Action Function(int); From 7c5645aa01cadaf5386591ebd0259b036e158c95 Mon Sep 17 00:00:00 2001 From: RayBipse Date: Sun, 3 Mar 2024 18:43:54 -0800 Subject: [PATCH 35/37] Closes #897 --- lib/src/constants.dart | 1 + lib/src/middleware/export_svg.dart | 3 +- lib/src/state/base_pair_display_type.dart | 12 +++ lib/src/view/design_main.dart | 16 ++- .../view/design_main_base_pair_rectangle.dart | 99 +++++++++++++++++++ lib/src/view/menu.dart | 32 +++--- 6 files changed, 142 insertions(+), 21 deletions(-) create mode 100644 lib/src/view/design_main_base_pair_rectangle.dart diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 96fb85401..6342bcb80 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -428,6 +428,7 @@ const css_selector_scaffold = 'scaffold'; const css_selector_staple = 'staple'; const css_selector_domain = 'domain-line'; const css_selector_base_pair_line = 'base-pair-line'; +const css_selector_base_pair_rect = 'base-pair-rect'; const css_selector_extension = 'extension-line'; const css_selector_crossover = 'crossover-curve'; const css_selector_crossover_same_helix = 'crossover-curve-same-helix'; diff --git a/lib/src/middleware/export_svg.dart b/lib/src/middleware/export_svg.dart index bd8297e28..92e7f3135 100644 --- a/lib/src/middleware/export_svg.dart +++ b/lib/src/middleware/export_svg.dart @@ -7,6 +7,7 @@ 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/base_pair_display_type.dart'; import 'package:scadnano/src/state/strand.dart'; import 'package:scadnano/src/view/design_main_dna_sequence.dart'; @@ -80,7 +81,7 @@ export_svg_middleware(Store store, dynamic action, NextDispatcher next List get_selected_svg_elements(AppState state) { BuiltSet selected_strands = state.ui_state.selectables_store.selected_strands; List selected_elts = []; - if (app.state.ui_state.show_base_pair_lines) { + if (app.state.ui_state.base_pair_display_type != BasePairDisplayType.none) { var base_pairs = state.ui_state.show_base_pair_lines_with_mismatches ? state.design.selected_base_pairs_with_mismatches(selected_strands) : state.design.selected_base_pairs(selected_strands); diff --git a/lib/src/state/base_pair_display_type.dart b/lib/src/state/base_pair_display_type.dart index 8f664e936..cabba9bd5 100644 --- a/lib/src/state/base_pair_display_type.dart +++ b/lib/src/state/base_pair_display_type.dart @@ -27,6 +27,18 @@ class BasePairDisplayType extends EnumClass { String to_json() => name; + int toIndex() { + switch (this) { + case none: + return 0; + case lines: + return 1; + case rectangle: + return 2; + } + return 0; + } + String display_name() { // edit this to display a different string than the identifier name above switch (this) { diff --git a/lib/src/view/design_main.dart b/lib/src/view/design_main.dart index 68f8706c2..ae1f8c5e1 100644 --- a/lib/src/view/design_main.dart +++ b/lib/src/view/design_main.dart @@ -6,11 +6,13 @@ import 'package:built_collection/built_collection.dart'; import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; import 'package:react/react_client/react_interop.dart'; +import 'package:scadnano/src/state/base_pair_display_type.dart'; import 'package:scadnano/src/view/design_main_unpaired_insertion_deletions.dart'; import 'package:scadnano/src/view/design_main_slice_bar.dart'; import 'package:scadnano/src/view/potential_extensions_view.dart'; import '../state/selection_rope.dart'; +import 'design_main_base_pair_rectangle.dart'; import 'design_main_domains_moving.dart'; import 'selection_rope_view.dart'; import '../actions/actions.dart' as actions; @@ -65,6 +67,7 @@ UiFactory ConnectedDesignMain = connect { .map((helix_idx, svg_position) => MapEntry(helix_idx, svg_position.y)) ..key = 'unpaired-insertion-deletions')(), - if (props.show_base_pair_lines) + if (props.base_pair_display_type == BasePairDisplayType.lines) (DesignMainBasePairLines() ..with_mismatches = props.show_base_pair_lines_with_mismatches ..design = props.design @@ -220,6 +224,16 @@ class DesignMainComponent extends UiComponent2 { .map((helix_idx, svg_position) => MapEntry(helix_idx, svg_position.y)) ..key = 'base-pair-lines')(), + if (props.base_pair_display_type == BasePairDisplayType.rectangle) + (DesignMainBasePairRectangle() + ..with_mismatches = props.show_base_pair_lines_with_mismatches + ..design = props.design + ..only_display_selected_helices = props.only_display_selected_helices + ..side_selected_helix_idxs = props.side_selected_helix_idxs + ..helix_idx_to_svg_position_y_map = props.helix_idx_to_svg_position_map + .map((helix_idx, svg_position) => MapEntry(helix_idx, svg_position.y)) + ..key = 'base-pair-rectangle')(), + (ConnectedDesignMainStrands()..key = 'strands')(), // after strands so can click when crossover overlaps potential crossover diff --git a/lib/src/view/design_main_base_pair_rectangle.dart b/lib/src/view/design_main_base_pair_rectangle.dart new file mode 100644 index 000000000..81af7289f --- /dev/null +++ b/lib/src/view/design_main_base_pair_rectangle.dart @@ -0,0 +1,99 @@ +import 'dart:html'; + +import 'package:over_react/over_react.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:scadnano/scadnano.dart'; +import 'package:scadnano/src/state/group.dart'; +import 'package:scadnano/src/state/helix.dart'; + +import '../state/design.dart'; +import '../state/strand.dart'; +import '../state/domain.dart'; +import 'pure_component.dart'; +import 'design_main_warning_star.dart'; +import '../util.dart' as util; +import '../constants.dart' as constants; + +part 'design_main_base_pair_rectangle.over_react.g.dart'; + +UiFactory DesignMainBasePairRectangle = _$DesignMainBasePairRectangle; + +mixin DesignMainBasePairRectangleProps on UiProps { + bool with_mismatches; + Design design; + bool only_display_selected_helices; + BuiltSet side_selected_helix_idxs; + BuiltMap helix_idx_to_svg_position_y_map; +} + +class DesignMainBasePairRectangleComponent extends UiComponent2 + with PureComponent { + @override + render() { + List base_pair_lines_components = + this.create_base_pair_lines_components(app.state.design.strands.toBuiltSet()); + return (Dom.g()..className = 'base-pair-lines-main-view')(base_pair_lines_components); + } + + List create_base_pair_lines_components(BuiltSet strands) { + List base_pair_lines_components = []; + BuiltMap> base_pairs = props.with_mismatches + ? props.design.selected_base_pairs_with_mismatches(strands) + : props.design.selected_base_pairs(strands); + + for (int helix_idx in base_pairs.keys) { + if (!props.only_display_selected_helices || props.side_selected_helix_idxs.contains(helix_idx)) { + var helix = props.design.helices[helix_idx]; + HelixGroup group = props.design.groups[helix.group]; + String transform_str = group.transform_str(props.design.geometry); + + // code below draws one line for each base pair, should render somewhat slowly + // however, this makes it easier to associate base pair lines to individual strands, + // convenient when exporting SVG + List helix_components = []; + int last_offset = -2; + var last_svg_forward_pos = null; + for (int offset in base_pairs[helix_idx]) { + var svg_position_y = props.helix_idx_to_svg_position_y_map[helix_idx]; + var base_svg_forward_pos = helix.svg_base_pos(offset, true, svg_position_y); + var base_svg_reverse_pos = helix.svg_base_pos(offset, false, svg_position_y); + + var base_pair_ele = null; + + if (offset - last_offset == 1) { + base_pair_ele = (Dom.rect() + ..id = 'base_pair-${helix_idx}-${offset}' + ..x = last_svg_forward_pos.x - 0.5 + ..y = base_svg_forward_pos.y + ..width = base_svg_reverse_pos.x - last_svg_forward_pos.x + 0.8 + ..height = base_svg_reverse_pos.y - base_svg_forward_pos.y + ..className = constants.css_selector_base_pair_rect + ..fill = 'grey' + ..key = 'base-pair-rect-H${helix_idx}-${offset}')(); + } else { + base_pair_ele = (Dom.line() + ..id = 'base_pair-${helix_idx}-${offset}' + ..x1 = base_svg_forward_pos.x + ..y1 = base_svg_forward_pos.y + ..x2 = base_svg_reverse_pos.x + ..y2 = base_svg_reverse_pos.y + ..className = constants.css_selector_base_pair_line + ..stroke = 'grey' + ..key = 'base-pair-line-H${helix_idx}-${offset}')(); + } + + helix_components.add(base_pair_ele); + last_offset = offset; + last_svg_forward_pos = base_svg_forward_pos; + } + var helix_dom_group = (Dom.g() + ..transform = transform_str + ..className = 'base-pair-lines-components-in-helix' + ..key = 'base-pair-lines-components-in-helix-H${helix_idx}')(helix_components); + base_pair_lines_components.add(helix_dom_group); + } + } + + return base_pair_lines_components; + } +} diff --git a/lib/src/view/menu.dart b/lib/src/view/menu.dart index 1d84fdb9a..e478aadcc 100644 --- a/lib/src/view/menu.dart +++ b/lib/src/view/menu.dart @@ -48,6 +48,7 @@ UiFactory ConnectedMenu = connect( ..no_grid_is_none = state.design == null ? false : state.design.groups.values.every((group) => group.grid != Grid.none) ..show_dna = state.ui_state.show_dna + ..base_pair_display_type = state.ui_state.base_pair_display_type ..show_strand_names = state.ui_state.show_strand_names ..show_strand_labels = state.ui_state.show_strand_labels ..show_domain_names = state.ui_state.show_domain_names @@ -135,7 +136,7 @@ mixin MenuPropsMixin on UiProps { bool autofit; bool only_display_selected_helices; ExampleDesigns example_designs; - // SetBasePairDisplay base_pair_display_types; + BasePairDisplayType base_pair_display_type; bool design_has_insertions_or_deletions; bool undo_stack_empty; bool redo_stack_empty; @@ -1007,24 +1008,6 @@ or real coordinates in nanometers, depending on whether a grid is selected).''' ..on_click = ((_) => app.disable_keyboard_shortcuts_while(base_pair_display_dialog)) ..display = 'Base pair display' ..key = 'base-pair-display')(), - (MenuBoolean() - ..value = props.show_base_pair_lines - ..display = 'Base pair lines' - ..tooltip = 'Draw vertical lines between pairs of bases at the same offset on the same helix.' - ..onChange = ((_) => - props.dispatch(actions.ShowBasePairLinesSet(show_base_pair_lines: !props.show_base_pair_lines))) - ..key = 'base_pair_lines')(), - (MenuBoolean() - ..value = props.show_base_pair_lines_with_mismatches - ..hide = !props.show_base_pair_lines - ..display = '... even if bases mismatch' - ..tooltip = '''\ -Lines are drawn between all pairs of bases at the same offset on the same helix, -regardless of whether the bases are complementary. If unchecked then lines are -only shown between pairs of complementary bases.''' - ..onChange = ((_) => props.dispatch(actions.ShowBasePairLinesWithMismatchesSet( - show_base_pair_lines_with_mismatches: !props.show_base_pair_lines_with_mismatches))) - ..key = 'base_pair_lines_mismatches')(), ]); } @@ -1448,13 +1431,24 @@ However, it may be less stable than the main site.''' DialogRadio( label: 'types', options: BasePairDisplayType.types.map((v) => v.display_name()), + selected_idx: props.base_pair_display_type.toIndex(), ), + DialogCheckbox( + label: 'display even if bases mismatch', + value: props.show_base_pair_lines_with_mismatches, + tooltip: '''\ +Lines are drawn between all pairs of bases at the same offset on the same helix, +regardless of whether the bases are complementary. If unchecked then lines are +only shown between pairs of complementary bases.''', + ) ]); List results = await util.dialog(dialog); if (results == null) return; int selected_idx = (results[0] as DialogRadio).selected_idx; props.dispatch(actions.BasePairTypeSet(selected_idx: selected_idx)); + props.dispatch((actions.ShowBasePairLinesWithMismatchesSet( + show_base_pair_lines_with_mismatches: (results[1] as DialogCheckbox).value))); } } From f842e25167ecdacd3340ae13d513765c0fab9fff Mon Sep 17 00:00:00 2001 From: RayBipse Date: Mon, 4 Mar 2024 18:20:50 -0800 Subject: [PATCH 36/37] Changed the layout for base pair display to checkboxes --- lib/src/view/menu.dart | 62 ++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/lib/src/view/menu.dart b/lib/src/view/menu.dart index e478aadcc..e787db00d 100644 --- a/lib/src/view/menu.dart +++ b/lib/src/view/menu.dart @@ -1004,10 +1004,39 @@ or real coordinates in nanometers, depending on whether a grid is selected).''' ..id = 'view_menu_base_pairs' ..key = 'view_menu_base_pairs-dropdown' ..className = 'submenu_item')([ - (MenuDropdownItem() - ..on_click = ((_) => app.disable_keyboard_shortcuts_while(base_pair_display_dialog)) - ..display = 'Base pair display' - ..key = 'base-pair-display')(), + (MenuBoolean() + ..value = props.base_pair_display_type.toIndex() == 1 + ..display = 'Display as ${BasePairDisplayType.lines.display_name()}' + ..key = 'base-pair-display-lines' + ..onChange = (_) { + if (props.base_pair_display_type == BasePairDisplayType.lines) { + props.dispatch(actions.BasePairTypeSet(selected_idx: BasePairDisplayType.none.toIndex())); + } else if (props.base_pair_display_type == BasePairDisplayType.none) { + props.dispatch(actions.BasePairTypeSet(selected_idx: BasePairDisplayType.lines.toIndex())); + } + })(), + (MenuBoolean() + ..value = props.base_pair_display_type.toIndex() == 2 + ..display = 'Display as ${BasePairDisplayType.rectangle.display_name()}' + ..key = 'base-pair-display-rectangle' + ..onChange = (_) { + if (props.base_pair_display_type == BasePairDisplayType.rectangle) { + props.dispatch(actions.BasePairTypeSet(selected_idx: BasePairDisplayType.none.toIndex())); + } else if (props.base_pair_display_type == BasePairDisplayType.none) { + props.dispatch(actions.BasePairTypeSet(selected_idx: BasePairDisplayType.rectangle.toIndex())); + } + })(), + (MenuBoolean() + ..value = props.show_base_pair_lines_with_mismatches + ..display = '... even if bases mismatch' + ..key = 'base-pair-display-even-if-bases-mismatch' + ..hide = props.base_pair_display_type.toIndex() == 0 + ..tooltip = '''\ +Lines are drawn between all pairs of bases at the same offset on the same helix, +regardless of whether the bases are complementary. If unchecked then lines are +only shown between pairs of complementary bases.''' + ..onChange = (_) => props.dispatch(actions.ShowBasePairLinesWithMismatchesSet( + show_base_pair_lines_with_mismatches: !props.show_base_pair_lines_with_mismatches)))(), ]); } @@ -1425,31 +1454,6 @@ However, it may be less stable than the main site.''' int selected_idx = (results[0] as DialogRadio).selected_idx; props.dispatch(actions.ExampleDesignsLoad(selected_idx: selected_idx)); } - - Future base_pair_display_dialog() async { - var dialog = Dialog(title: 'Base pair display', type: DialogType.base_pair_display, items: [ - DialogRadio( - label: 'types', - options: BasePairDisplayType.types.map((v) => v.display_name()), - selected_idx: props.base_pair_display_type.toIndex(), - ), - DialogCheckbox( - label: 'display even if bases mismatch', - value: props.show_base_pair_lines_with_mismatches, - tooltip: '''\ -Lines are drawn between all pairs of bases at the same offset on the same helix, -regardless of whether the bases are complementary. If unchecked then lines are -only shown between pairs of complementary bases.''', - ) - ]); - List results = await util.dialog(dialog); - if (results == null) return; - - int selected_idx = (results[0] as DialogRadio).selected_idx; - props.dispatch(actions.BasePairTypeSet(selected_idx: selected_idx)); - props.dispatch((actions.ShowBasePairLinesWithMismatchesSet( - show_base_pair_lines_with_mismatches: (results[1] as DialogCheckbox).value))); - } } typedef ActionFromIntCreator = actions.Action Function(int); From 4e75ec269794b55141ead2b93cae347c23e2926d Mon Sep 17 00:00:00 2001 From: David Doty Date: Thu, 28 Mar 2024 14:11:29 -0700 Subject: [PATCH 37/37] closes #588: export to oxView format --- lib/src/actions/actions.dart | 37 +++++ lib/src/middleware/oxdna_export.dart | 166 +++++++++++++++++++-- lib/src/reducers/app_ui_state_reducer.dart | 4 + lib/src/serializers.dart | 2 + lib/src/state/app_ui_state.dart | 2 + lib/src/state/app_ui_state_storables.dart | 3 + lib/src/state/strand.dart | 53 +++++++ lib/src/view/menu.dart | 34 +++-- 8 files changed, 278 insertions(+), 23 deletions(-) diff --git a/lib/src/actions/actions.dart b/lib/src/actions/actions.dart index 493ad6bb9..a4feaba79 100644 --- a/lib/src/actions/actions.dart +++ b/lib/src/actions/actions.dart @@ -4523,3 +4523,40 @@ abstract class OxdnaExport @memoized int get hashCode; } + +abstract class OxviewExport + with BuiltJsonSerializable + implements Action, Built { + bool get selected_strands_only; + + /************************ begin BuiltValue boilerplate ************************/ + factory OxviewExport({bool selected_strands_only = false}) { + return OxviewExport.from((b) => b..selected_strands_only = selected_strands_only); + } + + OxviewExport._(); + + factory OxviewExport.from([void Function(OxviewExportBuilder) updates]) = _$OxviewExport; + + static Serializer get serializer => _$oxviewExportSerializer; + + @memoized + int get hashCode; +} + +abstract class OxExportOnlySelectedStrandsSet + with BuiltJsonSerializable + implements Action, Built { + bool get only_selected; + + /************************ begin BuiltValue boilerplate ************************/ + factory OxExportOnlySelectedStrandsSet({bool only_selected}) = _$OxExportOnlySelectedStrandsSet._; + + OxExportOnlySelectedStrandsSet._(); + + static Serializer get serializer => + _$oxExportOnlySelectedStrandsSetSerializer; + + @memoized + int get hashCode; +} diff --git a/lib/src/middleware/oxdna_export.dart b/lib/src/middleware/oxdna_export.dart index 6483933e0..eb1692828 100644 --- a/lib/src/middleware/oxdna_export.dart +++ b/lib/src/middleware/oxdna_export.dart @@ -1,6 +1,8 @@ +import 'dart:convert'; import 'dart:html'; import 'dart:math'; import 'package:path/path.dart' as path; +import 'package:quiver/iterables.dart' as quiver; import 'package:redux/redux.dart'; import 'package:scadnano/src/state/design.dart'; @@ -16,9 +18,11 @@ import '../state/app_state.dart'; import '../actions/actions.dart' as actions; import '../state/helix.dart'; import '../util.dart' as util; +import 'export_cadnano_or_codenano_file.dart' as export_cadnano; +import '../constants.dart' as constants; oxdna_export_middleware(Store store, dynamic action, NextDispatcher next) { - if (action is actions.OxdnaExport) { + if (action is actions.OxdnaExport || action is actions.OxviewExport) { AppState state = store.state; List strands_to_export; @@ -34,18 +38,160 @@ First select some strands, or choose Export🡒oxDNA to export all strands in th strands_to_export = state.design.strands.toList(); } - Tuple2 dat_top = to_oxdna_format(state.design, strands_to_export); - String dat = dat_top.item1; - String top = dat_top.item2; + if (action is actions.OxdnaExport) { + Tuple2 dat_top = to_oxdna_format(state.design, strands_to_export); + String dat = dat_top.item1; + String top = dat_top.item2; + + String default_filename = state.ui_state.loaded_filename; + String default_filename_dat = path.setExtension(default_filename, '.dat'); + String default_filename_top = path.setExtension(default_filename, '.top'); + + util.save_file(default_filename_dat, dat); + util.save_file(default_filename_top, top); + } else if (action is actions.OxviewExport) { + String content = to_oxview_format(state.design, strands_to_export); + String default_filename = state.ui_state.loaded_filename; + String default_filename_ext = path.setExtension(default_filename, '.oxview'); + util.save_file(default_filename_ext, content); + } + } + next(action); +} - String default_filename = state.ui_state.loaded_filename; - String default_filename_dat = path.setExtension(default_filename, '.dat'); - String default_filename_top = path.setExtension(default_filename, '.top'); +String to_oxview_format(Design design, List strands_to_export) { + OxdnaSystem system = convert_design_to_oxdna_system(design, strands_to_export); + List> oxview_strands = []; + int nuc_count = 0; + int strand_count = 0; + List strand_nuc_start = [-1]; + assert(strands_to_export.length == system.strands.length); + + for (int i = 0; i < strands_to_export.length; i++) { + Strand sc_strand = strands_to_export[i]; + OxdnaStrand oxdna_strand = system.strands[i]; + + strand_count += 1; + List> oxvnucs = []; + strand_nuc_start.add(nuc_count); + Map oxvstrand = { + 'id': strand_count, + 'class': 'NucleicAcidStrand', + 'end5': nuc_count, + 'end3': nuc_count + system.strands[i].nucleotides.length, + 'monomers': oxvnucs + }; + + int scolor; + if (sc_strand.color != null) { + scolor = export_cadnano.to_cadnano_v2_int_hex(sc_strand.color); + } else { + scolor = null; + } - util.save_file(default_filename_dat, dat); - util.save_file(default_filename_top, top); + for (int index_in_strand = 0; index_in_strand < oxdna_strand.nucleotides.length; index_in_strand++) { + OxdnaNucleotide nuc = oxdna_strand.nucleotides[index_in_strand]; + Map oxvnuc = { + 'id': nuc_count, + 'p': [nuc.r.x, nuc.r.y, nuc.r.z], + 'a1': [nuc.b.x, nuc.b.y, nuc.b.z], + 'a3': [nuc.n.x, nuc.n.y, nuc.n.z], + 'class': 'DNA', + 'type': nuc.base, + 'cluster': 1, + }; + if (index_in_strand != 0) { + oxvnuc['n5'] = nuc_count - 1; + } + if (index_in_strand != oxdna_strand.nucleotides.length - 1) { + oxvnuc['n3'] = nuc_count + 1; + } + if (scolor != null) { + oxvnuc['color'] = scolor; + } + nuc_count += 1; + oxvnucs.add(oxvnuc); + } + oxview_strands.add(oxvstrand); } - next(action); + + for (int si1 = 0; si1 < strands_to_export.length; si1++) { + Strand sc_strand1 = strands_to_export[si1]; + Map oxv_strand1 = oxview_strands[si1]; + for (int si2 = 0; si2 < strands_to_export.length; si2++) { + Strand sc_strand2 = strands_to_export[si2]; + if (!sc_strand1.overlaps(sc_strand2)) { + continue; + } + int s1_nuc_idx = strand_nuc_start[si1 + 1]; + for (var domain1 in sc_strand1.domains) { + if (domain1 is Loopout || domain1 is Extension) { + continue; + } + int s2_nuc_idx = strand_nuc_start[si2 + 1]; + for (var domain2 in sc_strand2.domains) { + if (domain2 is Loopout || domain2 is Extension) { + continue; + } + if (!domain1.overlaps(domain2)) { + continue; + } + Tuple2 overlap = domain1.compute_overlap(domain2); + int overlap_left = overlap.item1; + int overlap_right = overlap.item2; + int s1_left = sc_strand1.domain_offset_to_strand_dna_idx(domain1, overlap_left, false); + int s1_right = sc_strand1.domain_offset_to_strand_dna_idx(domain1, overlap_right, false); + int s2_left = sc_strand2.domain_offset_to_strand_dna_idx(domain2, overlap_left, false); + int s2_right = sc_strand2.domain_offset_to_strand_dna_idx(domain2, overlap_right, false); + List d1range; + List d2range; + if (domain1.forward) { + d1range = List.from(quiver.range(s1_left, s1_right)); + d2range = List.from(quiver.range(s2_left, s2_right, -1)); + } else { + d1range = List.from(quiver.range(s1_right + 1, s1_left + 1)); + d2range = List.from(quiver.range(s2_right - 1, s2_left - 1, -1)); + } + assert(d1range.length == d2range.length); + + // Check for mismatches, and do not add a pair if the bases are *known* + // to mismatch. (FIXME: this must be changed if scadnano later supports + // degenerate base codes.) + for (int i = 0; i < d1range.length; i++) { + int d1 = d1range[i]; + int d2 = d2range[i]; + if (sc_strand1.dna_sequence != null && + sc_strand2.dna_sequence != null && + sc_strand1.dna_sequence[d1] != constants.DNA_BASE_WILDCARD && + sc_strand2.dna_sequence[d2] != constants.DNA_BASE_WILDCARD && + util.wc(sc_strand1.dna_sequence[d1]) != sc_strand2.dna_sequence[d2]) { + continue; + } + oxv_strand1['monomers'][d1]['bp'] = s2_nuc_idx + d2; + if (oxview_strands[si2]['monomers'][d2].containsKey('bp')) { + if (oxview_strands[si2]['monomers'][d2]['bp'] != s1_nuc_idx + d1) { + print('${s2_nuc_idx + d2} ${s1_nuc_idx + d1} ' + '${oxview_strands[si2]['monomers'][d2]['bp']} ${domain1} ${domain2}'); + } + } + } + } + } + } + } + var b = system.compute_bounding_box(); + Map oxvsystem = { + 'box': [b.x, b.y, b.z], + 'date': DateTime.now().toIso8601String(), + 'systems': [ + {'id': 0, 'strands': oxview_strands} + ], + 'forces': [], + 'selections': [], + }; + String content = jsonEncode(oxvsystem); + + return content; } Tuple2 to_oxdna_format(Design design, [List strands_to_export = null]) { diff --git a/lib/src/reducers/app_ui_state_reducer.dart b/lib/src/reducers/app_ui_state_reducer.dart index 2f5f329c0..a0dffa766 100644 --- a/lib/src/reducers/app_ui_state_reducer.dart +++ b/lib/src/reducers/app_ui_state_reducer.dart @@ -244,6 +244,9 @@ bool show_base_pair_lines_with_mismatches_reducer( bool export_svg_text_separately_reducer(bool _, actions.ExportSvgTextSeparatelySet action) => action.export_svg_text_separately; +bool ox_export_only_selected_strands_reducer(bool _, actions.OxExportOnlySelectedStrandsSet action) => + action.only_selected; + bool display_major_tick_widths_reducer(bool _, actions.SetDisplayMajorTickWidths action) => action.show; bool strand_paste_keep_color_reducer(bool _, actions.StrandPasteKeepColorSet action) => action.keep; @@ -460,6 +463,7 @@ AppUIStateStorables app_ui_state_storable_local_reducer(AppUIStateStorables stor ..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) + ..ox_export_only_selected_strands = TypedReducer(ox_export_only_selected_strands_reducer)(storables.ox_export_only_selected_strands, 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 3e3adf1de..efc6231bc 100644 --- a/lib/src/serializers.dart +++ b/lib/src/serializers.dart @@ -363,6 +363,8 @@ part 'serializers.g.dart'; ZoomSpeedSet, NewDesignSet, OxdnaExport, + OxviewExport, + OxExportOnlySelectedStrandsSet, Design, AssignDomainNameComplementFromBoundStrands, AssignDomainNameComplementFromBoundDomains, diff --git a/lib/src/state/app_ui_state.dart b/lib/src/state/app_ui_state.dart index 259835580..3a6d03f95 100644 --- a/lib/src/state/app_ui_state.dart +++ b/lib/src/state/app_ui_state.dart @@ -246,6 +246,8 @@ abstract class AppUIState with BuiltJsonSerializable implements Built storables.selection_box_intersection; + bool get ox_export_only_selected_strands => storables.ox_export_only_selected_strands; + static void _initializeBuilder(AppUIStateBuilder b) { b.copy_info = null; b.last_mod_5p = null; diff --git a/lib/src/state/app_ui_state_storables.dart b/lib/src/state/app_ui_state_storables.dart index fdac0928a..dfbba962a 100644 --- a/lib/src/state/app_ui_state_storables.dart +++ b/lib/src/state/app_ui_state_storables.dart @@ -130,6 +130,8 @@ abstract class AppUIStateStorables bool get export_svg_text_separately; + bool get ox_export_only_selected_strands; + 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. @@ -188,6 +190,7 @@ abstract class AppUIStateStorables b.show_mouseover_data = false; b.selection_box_intersection = false; b.export_svg_text_separately = false; + b.ox_export_only_selected_strands = false; } /************************ begin BuiltValue boilerplate ************************/ diff --git a/lib/src/state/strand.dart b/lib/src/state/strand.dart index 52ee4f9e5..96d3e955d 100644 --- a/lib/src/state/strand.dart +++ b/lib/src/state/strand.dart @@ -753,6 +753,59 @@ abstract class Strand return rebuild((strand) => strand..substrands.replace(substrands_new)); } + /// Convert from offset on the given Domain's Helix to string index on the parent Strand's DNA sequence. + /// If `offset_closer_to_5p` is ``true``, (this only matters if `offset` contains an insertion) + /// then the only leftmost string index corresponding to this offset is included, + /// otherwise up to the rightmost string index (including all insertions) is included. + int domain_offset_to_strand_dna_idx(Domain domain, int offset, bool offset_closer_to_5p) { + if (domain.deletions.contains(offset)) { + throw ArgumentError('offset ${offset} illegally contains a deletion from ${domain.deletions}'); + } + + int len_adjust = this._net_ins_del_length_increase_from_5p_to(domain, offset, offset_closer_to_5p); + + int domain_str_idx; + if (domain.forward) { + offset += len_adjust; + domain_str_idx = offset - domain.start; + } else { + offset -= len_adjust; + domain_str_idx = domain.end - 1 - offset; + } + + return domain_str_idx + get_seq_start_idx(domain); + } + + /// Net number of insertions from 5'/3' end to offset_edge, + /// INCLUSIVE on 5'/3' end, EXCLUSIVE on offset_edge. + /// Set `five_p` ``= False`` to test from 3' end to `offset_edge`. + int _net_ins_del_length_increase_from_5p_to(Domain domain, int offset_edge, bool offset_closer_to_5p) { + int length_increase = 0; + for (int deletion in domain.deletions) { + if (_between_5p_and_offset(domain, deletion, offset_edge)) { + length_increase -= 1; + } + } + for (var insertion in domain.insertions) { + if (_between_5p_and_offset(domain, insertion.offset, offset_edge)) { + length_increase += insertion.length; + } + } + if (!offset_closer_to_5p) { + Map insertion_map = + Map.fromIterable(domain.insertions, key: (e) => e.offset, value: (e) => e.length); + if (insertion_map.containsKey(offset_edge)) { + int insertion_length = insertion_map[offset_edge]; + length_increase += insertion_length; + } + } + return length_increase; + } + + bool _between_5p_and_offset(Domain domain, int offset_to_test, int offset_edge) => + (domain.forward && domain.start <= offset_to_test && offset_to_test < offset_edge) || + (!domain.forward && offset_edge < offset_to_test && offset_to_test < domain.end); + String _trim_or_pad_sequence_to_desired_length(String dna_sequence_new, int desired_length) { // truncate dna_sequence_new if too long; pad with ?'s if to short int seq_len = dna_sequence_new.length; diff --git a/lib/src/view/menu.dart b/lib/src/view/menu.dart index e787db00d..973f8d482 100644 --- a/lib/src/view/menu.dart +++ b/lib/src/view/menu.dart @@ -102,6 +102,7 @@ UiFactory ConnectedMenu = connect( ..default_crossover_type_staple_for_setting_helix_rolls = state.ui_state.default_crossover_type_staple_for_setting_helix_rolls ..selection_box_intersection = state.ui_state.selection_box_intersection + ..ox_export_only_selected_strands = state.ui_state.ox_export_only_selected_strands ..undo_redo = state.undo_redo); }, // Used for component test. @@ -161,6 +162,7 @@ mixin MenuPropsMixin on UiProps { bool default_crossover_type_scaffold_for_setting_helix_rolls; bool default_crossover_type_staple_for_setting_helix_rolls; bool export_svg_text_separately; + bool ox_export_only_selected_strands; LocalStorageDesignChoice local_storage_design_choice; bool clear_helix_selection_when_loading_new_design; bool show_slice_bar; @@ -1251,22 +1253,28 @@ cadnano files that have whitespace. ("Bad .json file format is detected in ..key = 'export-cadnano-no-whitespace')(), DropdownDivider({'key': 'divider-cadnano'}), (MenuDropdownItem() - ..on_click = ((_) => props.dispatch(actions.OxdnaExport())) + ..on_click = ((_) => props + .dispatch(actions.OxviewExport(selected_strands_only: props.ox_export_only_selected_strands))) + ..tooltip = "Export design to oxView files, which can be loaded in oxView." + ..display = 'oxView' + ..key = 'export-oxview')(), + (MenuDropdownItem() + ..on_click = ((_) => + props.dispatch(actions.OxdnaExport(selected_strands_only: props.ox_export_only_selected_strands))) ..tooltip = "Export design to oxDNA .dat and .top files, which can be loaded in oxDNA or oxView." ..display = 'oxDNA' ..key = 'export-oxdna')(), - (MenuDropdownItem() - ..on_click = ((_) => props.dispatch(actions.OxdnaExport(selected_strands_only: true))) - ..tooltip = "Export design to oxDNA .dat and .top files, which can be loaded in oxDNA or oxView.\n" - "Only exports the currently selected strands." - ..display = 'oxDNA (selected strands)' - ..key = 'export-oxdna-selected-strands')(), - //TODO: figure out if ENSnano is close to codenano format; if so this might work for exporting to it. - // (MenuDropdownItem() - // ..on_click = ((_) => props.dispatch(actions.ExportCodenanoFile())) - // ..tooltip = "Export design to codenano format." - // ..display = 'codenano' - // ..key = 'export-codenano')(), + (MenuBoolean() + ..value = props.ox_export_only_selected_strands + ..display = 'export only selected strands' + ..tooltip = '''\ +When selected, only selected strands will be exported to oxDNA or oxView formats.''' + ..name = 'ox-export-only-selected-strands' + ..onChange = (_) { + props.dispatch( + actions.OxExportOnlySelectedStrandsSet(only_selected: !props.ox_export_only_selected_strands)); + } + ..key = 'ox-export-only-selected-strands')(), ); }