diff --git a/lib/src/actions/actions.dart b/lib/src/actions/actions.dart index daf1ad519..39e8d7a90 100644 --- a/lib/src/actions/actions.dart +++ b/lib/src/actions/actions.dart @@ -2172,8 +2172,12 @@ abstract class DomainsMoveStartSelectedDomains implements Action, Built { Address get address; + BuiltMap get original_helices_view_order_inverse; + /************************ begin BuiltValue boilerplate ************************/ - factory DomainsMoveStartSelectedDomains({Address address}) = _$DomainsMoveStartSelectedDomains._; + factory DomainsMoveStartSelectedDomains( + {Address address, + BuiltMap original_helices_view_order_inverse}) = _$DomainsMoveStartSelectedDomains._; DomainsMoveStartSelectedDomains._(); diff --git a/lib/src/middleware/all_middleware.dart b/lib/src/middleware/all_middleware.dart index 3ea8a99d6..301d09f83 100644 --- a/lib/src/middleware/all_middleware.dart +++ b/lib/src/middleware/all_middleware.dart @@ -23,6 +23,7 @@ import 'oxdna_export.dart'; import 'periodic_save_design_local_storage.dart'; import 'reselect_moved_dna_ends.dart'; import 'reselect_moved_copied_strands.dart'; +import 'reselect_moved_domains.dart'; import 'save_file.dart'; import 'export_svg.dart'; import 'local_storage.dart'; @@ -60,6 +61,7 @@ final all_middleware = List>.unmodifiable([ export_dna_sequences_middleware, reselect_moved_dna_ends_middleware, reselect_moved_copied_strands_middleware, + reselect_moved_domains_middleware, selections_intersect_box_compute_middleware, insertion_deletion_batching_middleware, adjust_grid_position_middleware, diff --git a/lib/src/middleware/reselect_moved_domains.dart b/lib/src/middleware/reselect_moved_domains.dart new file mode 100644 index 000000000..c0234b5c3 --- /dev/null +++ b/lib/src/middleware/reselect_moved_domains.dart @@ -0,0 +1,70 @@ +import 'package:redux/redux.dart'; +import 'package:built_collection/built_collection.dart'; + +import '../reducers/domains_move_reducer.dart' as domains_move_reducer; +import '../state/domains_move.dart'; +import '../state/design.dart'; +import '../state/address.dart'; +import '../state/domain.dart'; +import '../state/dna_end.dart'; +import '../state/strand.dart'; +import '../state/strands_move.dart'; +import '../actions/actions.dart' as actions; +import '../state/app_state.dart'; + +reselect_moved_domains_middleware(Store store, action, NextDispatcher next) { + // only reselect if there is more than 1 selected + // otherwise, if the user repeatedly clicks and drags one at a time, + // this builds up many selected items as they click each new one, moving all of them + if ((action is actions.DomainsMoveCommit) && action.domains_move.domains_moving.length > 1) { + Design old_design = store.state.design; + DomainsMove domains_move = action.domains_move; + + if (!(domains_move_reducer.in_bounds(old_design, domains_move) && + domains_move_reducer.is_allowable(old_design, domains_move) && + domains_move.is_nontrivial)) { + return; + } + + List
addresses = []; + + var new_address_helix_idx = domains_move.current_address.helix_idx; + var new_helix = old_design.helices[new_address_helix_idx]; + var new_group = old_design.groups[new_helix.group]; + + BuiltList new_helices_view_order = new_group.helices_view_order; + BuiltMap old_helices_view_order_inverse = + domains_move.original_helices_view_order_inverse; + + // first collect old addresses while design.end_to_substrand is still valid, convert them to + // their new addresses so we can look them up + for (Domain old_domain in domains_move.domains_moving) { + // Domain old_domain = strand.first_domain; + DNAEnd old_5p_end = old_domain.dnaend_5p; + int old_helix_view_order = old_helices_view_order_inverse[old_domain.helix]; + int new_helix_view_order = old_helix_view_order + domains_move.delta_view_order; + int new_helix_idx = new_helices_view_order[new_helix_view_order]; + int new_offset = old_5p_end.offset_inclusive + domains_move.delta_offset; + var new_forward = domains_move.delta_forward != old_domain.forward; + var address = Address(helix_idx: new_helix_idx, offset: new_offset, forward: new_forward); + addresses.add(address); + } + + // then apply action to commit the move + next(action); + + // now find new ends at given addresses + List new_domains = []; + Design new_design = store.state.design; + // if domain polarity switched, the 3' end of each domain will now be where the 5' end was + BuiltMap address_to_domain = + domains_move.delta_forward ? new_design.address_3p_to_domain : new_design.address_5p_to_domain; + for (var address in addresses) { + Domain new_domain = address_to_domain[address]; + new_domains.add(new_domain); + } + store.dispatch(actions.SelectAll(selectables: new_domains.toBuiltList(), only: true)); + } else { + next(action); + } +} diff --git a/lib/src/reducers/domains_move_reducer.dart b/lib/src/reducers/domains_move_reducer.dart index 18723f8d1..4b5603746 100644 --- a/lib/src/reducers/domains_move_reducer.dart +++ b/lib/src/reducers/domains_move_reducer.dart @@ -36,6 +36,7 @@ DomainsMove domains_move_start_selected_domains_reducer( domains_moving: selected_domains.toBuiltList(), all_domains: state.design.all_domains, strands_with_domains_moving: strands_of_selected_domains.toBuiltList(), + original_helices_view_order_inverse: action.original_helices_view_order_inverse, helices: state.design.helices, groups: state.design.groups, original_address: action.address); @@ -47,6 +48,9 @@ DomainsMove domains_move_stop_reducer(DomainsMove domains_move, actions.DomainsM // other domains // - is_allowable checks whether the domain overlaps other domains +bool in_bounds_and_allowable(Design design, DomainsMove domains_move) => + in_bounds(design, domains_move) && is_allowable(design, domains_move); + DomainsMove domains_adjust_address_reducer( DomainsMove domains_move, AppState state, actions.DomainsMoveAdjustAddress action) { DomainsMove new_domains_move = domains_move.rebuild((b) => b..current_address.replace(action.address)); diff --git a/lib/src/state/design.dart b/lib/src/state/design.dart index 80d8c3190..3ad168e7d 100644 --- a/lib/src/state/design.dart +++ b/lib/src/state/design.dart @@ -72,16 +72,15 @@ abstract class Design with UnusedFields implements Built, if (helices != null) { helix_builders = helices.map((e) => e.toBuilder()); } else if (num_helices != null) { - helix_builders = [ - for (int idx in Iterable.generate(num_helices)) - HelixBuilder() - ..idx = idx - ..grid = grid - ..geometry = geometry.toBuilder() - ..grid_position = (grid == Grid.none ? null : default_grid_position(idx).toBuilder()) - ..position_ = grid != Grid.none ? null : default_position(geometry, idx).toBuilder() - ]; + for (int idx in Iterable.generate(num_helices)) + HelixBuilder() + ..idx = idx + ..grid = grid + ..geometry = geometry.toBuilder() + ..grid_position = (grid == Grid.none ? null : default_grid_position(idx).toBuilder()) + ..position_ = grid != Grid.none ? null : default_position(geometry, idx).toBuilder() + ]; } else if (helix_builders != null) { // We will use the parameter directly } else { @@ -95,7 +94,6 @@ abstract class Design with UnusedFields implements Built, set_helices_min_max_offsets(helix_builders_map, strands); - if (groups == null) { groups = _calculate_groups_from_helix_builder(helix_builders, grid); } @@ -106,9 +104,7 @@ abstract class Design with UnusedFields implements Built, var helices_map = {for (var helix in helices) helix.idx: helix}; for (var key in helices_map.keys) { - helices_map[key] = helices_map[key].rebuild((b) => b - ..geometry.replace(geometry) - ); + helices_map[key] = helices_map[key].rebuild((b) => b..geometry.replace(geometry)); } var design = Design.from((b) => b @@ -631,6 +627,32 @@ abstract class Design with UnusedFields implements Built, return map.build(); } + /// Gets Domain with 5p end at given address (helix,offset,forward) + /// Offset is inclusive, i.e., dna_end.offset_inclusive + @memoized + BuiltMap get address_5p_to_domain { + var map = Map(); + for (Domain domain in domains_by_id.values) { + var key = Address( + helix_idx: domain.helix, offset: domain.dnaend_5p.offset_inclusive, forward: domain.forward); + map[key] = domain; + } + return map.build(); + } + + /// Gets Domain with 3p end at given address (helix,offset,forward) + /// Offset is inclusive, i.e., dna_end.offset_inclusive + @memoized + BuiltMap get address_3p_to_domain { + var map = Map(); + for (Domain domain in domains_by_id.values) { + var key = Address( + helix_idx: domain.helix, offset: domain.dnaend_3p.offset_inclusive, forward: domain.forward); + map[key] = domain; + } + return map.build(); + } + /// Maps Addresses to PotentialVerticalCrossovers. /// The end on TOP (i.e., lower helix idx) has the address with the key in the map. @memoized @@ -1188,7 +1210,6 @@ abstract class Design with UnusedFields implements Built, strands.add(strand); } - // build groups Map groups_map = group_builders_map.map((key, value) => MapEntry(key, value.build())); @@ -1887,7 +1908,8 @@ abstract class Design with UnusedFields implements Built, } } - return Design(helix_builders: helix_builders.values, strands: strands, grid: grid_type, invert_y: invert_y); + return Design( + helix_builders: helix_builders.values, strands: strands, grid: grid_type, invert_y: invert_y); } /// Routine that will follow a cadnano v2 strand accross helices and create @@ -2072,7 +2094,8 @@ abstract class Design with UnusedFields implements Built, } } -Map _calculate_groups_from_helix_builder(Iterable helix_builders, Grid grid) { +Map _calculate_groups_from_helix_builder( + Iterable helix_builders, Grid grid) { if (helix_builders.isEmpty) { return {constants.default_group_name: HelixGroup(grid: grid, helices_view_order: [])}; } @@ -2336,6 +2359,7 @@ class StrandError extends IllegalDesignError { class HelixPitchYaw { double pitch; double yaw; + // Helix idx of the first helix found with this pitch and yaw value int helix_idx; diff --git a/lib/src/state/domains_move.dart b/lib/src/state/domains_move.dart index 530a0ad18..7f3e1eddd 100644 --- a/lib/src/state/domains_move.dart +++ b/lib/src/state/domains_move.dart @@ -26,20 +26,26 @@ abstract class DomainsMove with BuiltJsonSerializable implements Built domains_moving, BuiltList all_domains, - BuiltList strands_with_domains_moving, + BuiltList strands_with_domains_moving, BuiltMap helices, BuiltMap groups, + BuiltMap original_helices_view_order_inverse, Address original_address, bool copy = false, bool keep_color = true}) { - var domains_fixed = - copy ? all_domains : [for (var domain in all_domains) if (!domains_moving.contains(domain)) domain]; + var domains_fixed = copy + ? all_domains + : [ + for (var domain in all_domains) + if (!domains_moving.contains(domain)) domain + ]; return DomainsMove.from((b) => b ..domains_moving.replace(domains_moving) ..domains_fixed.replace(domains_fixed) ..strands_with_domains_moving.replace(strands_with_domains_moving) ..helices.replace(helices) ..groups.replace(groups) + ..original_helices_view_order_inverse.replace(original_helices_view_order_inverse) ..original_address.replace(original_address) ..current_address.replace(original_address) ..copy = copy @@ -57,6 +63,10 @@ abstract class DomainsMove with BuiltJsonSerializable implements Built get strands_with_domains_moving; + // Since copied Domains may come from a different Design with different groups, we need to + // store this to know how to position them in new HelixGroups. + BuiltMap get original_helices_view_order_inverse; + Address get original_address; Address get current_address; diff --git a/lib/src/view/design_main_strand_domain.dart b/lib/src/view/design_main_strand_domain.dart index e7cb72cad..d660ade44 100644 --- a/lib/src/view/design_main_strand_domain.dart +++ b/lib/src/view/design_main_strand_domain.dart @@ -42,7 +42,9 @@ mixin DesignMainDomainPropsMixin on UiProps { Point helix_svg_position; List Function(Strand strand, - {@required Domain domain, @required Address address, @required ModificationType type}) context_menu_strand; + {@required Domain domain, + @required Address address, + @required ModificationType type}) context_menu_strand; bool selected; @@ -61,8 +63,10 @@ class DesignMainDomainComponent extends UiComponent2 Domain domain = props.domain; String id = domain.id; - Point start_svg = props.helix.svg_base_pos(domain.offset_5p, domain.forward, props.helix_svg_position.y); - Point end_svg = props.helix.svg_base_pos(domain.offset_3p, domain.forward, props.helix_svg_position.y); + Point start_svg = + props.helix.svg_base_pos(domain.offset_5p, domain.forward, props.helix_svg_position.y); + Point end_svg = + props.helix.svg_base_pos(domain.offset_3p, domain.forward, props.helix_svg_position.y); var classname = constants.css_selector_domain; if (props.selected) { @@ -131,9 +135,13 @@ class DesignMainDomainComponent extends UiComponent2 if (domain_selectable(props.domain)) { // select/deselect props.domain.handle_selection_mouse_down(event); - // set up drag detection for moving DNA ends - var address = util.find_closest_address(event, [props.helix], props.groups, props.geometry, {props.helix.idx: props.helix_svg_position}.build()); - app.dispatch(actions.DomainsMoveStartSelectedDomains(address: address)); + // set up drag detection for moving domains + var address = util.find_closest_address(event, [props.helix], props.groups, props.geometry, + {props.helix.idx: props.helix_svg_position}.build()); + HelixGroup group = app.state.design.group_of_domain(props.domain); + var view_order_inverse = group.helices_view_order_inverse; + app.dispatch(actions.DomainsMoveStartSelectedDomains( + address: address, original_helices_view_order_inverse: view_order_inverse)); } } } @@ -173,8 +181,8 @@ class DesignMainDomainComponent extends UiComponent2 if (!event.shiftKey) { event.preventDefault(); event.stopPropagation(); - Address address = - util.get_address_on_helix(event, props.helix, props.groups[props.helix.group], props.geometry, props.helix_svg_position); + Address address = util.get_address_on_helix( + event, props.helix, props.groups[props.helix.group], props.geometry, props.helix_svg_position); app.dispatch(actions.ContextMenuShow( context_menu: ContextMenu( items: props.context_menu_strand(props.strand, domain: props.domain, address: address).build(),