Skip to content

Commit

Permalink
closes #494; allow deletions/insertions to be added to all domains in…
Browse files Browse the repository at this point in the history
… a HelixGroup occupying the clicked offset, which helps with implementing twist correcting-deletions for flat square-lattice origami, for instance
  • Loading branch information
dave-doty committed Nov 5, 2020
1 parent 758ec7d commit b68b9f4
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 86 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,11 @@ There are different edit modes available, shown on the right side of the screen.
then adding/removing an insertion/deletion at that offset adds/removes on both bound domains.
The Python scripting library lets one specify insertions/deletions on one bound domain but not the other,
but this is currently [unsupported](https://github.com/UC-Davis-molecular-computing/scadnano/issues/90) in the web interface to create such a solitary deletion/insertion directly.
(If necessary, one hack is to move one domain out of the way, add the deletion/insertion to the other, and then move the first back.)
(If necessary, one hack is to move one domain out of the way, add the deletion/insertion to the other domain, and then move the first domain back.)

A useful shortcut is to press Ctrl while clicking a Domain in deletion (respectively, insertion) mode.
If Ctrl is pressed while clicking on a domain, then deletions (respectively, insertions) will be added at *all* domains on all helices in the same HelixGroup that overlap the same offset (unless that offset is the start or end of the domain, which is disallowed from having a deletion/insertion).
This helps to implement, for example, twist correction in DNA origami (see https://doi.org/10.1038/nchem.1070 for a description), where vertical "columns" of deletions need to be added to every domain in every helix at a given offset.

*Note for cadnano users:* From the user's perspective, cadnano associates each deletion/insertion to an "address", i.e., a helix and offset on that helix. For instance, it is possible to have a "deletion" where there is no DNA strand, and if DNA strand(s) are later placed there, they will have the deletion. By contrast, insertions and deletions in scadnano are associated to a bound domain. If the whole strand moves or is copied, the insertions/deletions move along with it.

Expand Down
93 changes: 71 additions & 22 deletions lib/src/actions/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -235,15 +235,19 @@ abstract class LocalStorageDesignChoiceSet

abstract class ClearHelixSelectionWhenLoadingNewDesignSet
with BuiltJsonSerializable
implements Action, Built<ClearHelixSelectionWhenLoadingNewDesignSet, ClearHelixSelectionWhenLoadingNewDesignSetBuilder> {
implements
Action,
Built<ClearHelixSelectionWhenLoadingNewDesignSet, ClearHelixSelectionWhenLoadingNewDesignSetBuilder> {
bool get clear;

/************************ begin BuiltValue boilerplate ************************/
factory ClearHelixSelectionWhenLoadingNewDesignSet({bool clear}) = _$ClearHelixSelectionWhenLoadingNewDesignSet._;
factory ClearHelixSelectionWhenLoadingNewDesignSet({bool clear}) =
_$ClearHelixSelectionWhenLoadingNewDesignSet._;

ClearHelixSelectionWhenLoadingNewDesignSet._();

static Serializer<ClearHelixSelectionWhenLoadingNewDesignSet> get serializer => _$clearHelixSelectionWhenLoadingNewDesignSetSerializer;
static Serializer<ClearHelixSelectionWhenLoadingNewDesignSet> get serializer =>
_$clearHelixSelectionWhenLoadingNewDesignSetSerializer;
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -368,8 +372,7 @@ abstract class ShowDomainNamesSet
factory ShowDomainNamesSet(bool show) => ShowDomainNamesSet.from((b) => b..show = show);

/************************ begin BuiltValue boilerplate ************************/
factory ShowDomainNamesSet.from([void Function(ShowDomainNamesSetBuilder) updates]) =
_$ShowDomainNamesSet;
factory ShowDomainNamesSet.from([void Function(ShowDomainNamesSetBuilder) updates]) = _$ShowDomainNamesSet;

ShowDomainNamesSet._();

Expand Down Expand Up @@ -494,10 +497,12 @@ abstract class ShowDomainNameMismatchesSet
implements Action, Built<ShowDomainNameMismatchesSet, ShowDomainNameMismatchesSetBuilder> {
bool get show_domain_name_mismatches;

factory ShowDomainNameMismatchesSet(bool show_domain_name_mismatches) => ShowDomainNameMismatchesSet.from((b) => b..show_domain_name_mismatches = show_domain_name_mismatches);
factory ShowDomainNameMismatchesSet(bool show_domain_name_mismatches) =>
ShowDomainNameMismatchesSet.from((b) => b..show_domain_name_mismatches = show_domain_name_mismatches);

/************************ begin BuiltValue boilerplate ************************/
factory ShowDomainNameMismatchesSet.from([void Function(ShowDomainNameMismatchesSetBuilder) updates]) = _$ShowDomainNameMismatchesSet;
factory ShowDomainNameMismatchesSet.from([void Function(ShowDomainNameMismatchesSetBuilder) updates]) =
_$ShowDomainNameMismatchesSet;

ShowDomainNameMismatchesSet._();

Expand Down Expand Up @@ -2093,9 +2098,11 @@ abstract class InsertionOrDeletionAction implements UndoableAction, StrandPartAc

int get offset;

bool get all_helices;

StrandPart get strand_part; // => substrand;

InsertionOrDeletionAction clone_for_adjacent_substrand(Domain other_domain);
InsertionOrDeletionAction clone_for_other_domain(Domain other_domain);
}

abstract class InsertionAdd
Expand All @@ -2105,12 +2112,14 @@ abstract class InsertionAdd

int get offset;

bool get all_helices;

StrandPart get strand_part => domain;

InsertionAdd clone_for_adjacent_substrand(Domain domain) => InsertionAdd(domain: domain, offset: offset);
InsertionAdd clone_for_other_domain(Domain domain) => rebuild((b) => b..domain.replace(domain));

/************************ begin BuiltValue boilerplate ************************/
factory InsertionAdd({Domain domain, int offset}) = _$InsertionAdd._;
factory InsertionAdd({Domain domain, int offset, bool all_helices}) = _$InsertionAdd._;

InsertionAdd._();

Expand All @@ -2126,18 +2135,31 @@ abstract class InsertionLengthChange

int get length;

bool get all_helices;

int get offset => insertion.offset;

StrandPart get strand_part => domain;

InsertionLengthChange clone_for_adjacent_substrand(Domain other_domain) => InsertionLengthChange(
InsertionLengthChange clone_for_other_domain(Domain other_domain) => InsertionLengthChange(
domain: other_domain,
insertion: other_domain.insertions.firstWhere((i) => i.offset == offset),
length: length,
);

/************************ begin BuiltValue boilerplate ************************/
factory InsertionLengthChange({Domain domain, Insertion insertion, int length}) = _$InsertionLengthChange._;
factory InsertionLengthChange({Domain domain, Insertion insertion, int length}) {
return InsertionLengthChange.from((b) => b
..domain.replace(domain)
..insertion.replace(insertion)
..length = length
..all_helices = false);
}

factory InsertionLengthChange.from([void Function(InsertionLengthChangeBuilder) updates]) =
_$InsertionLengthChange;

// factory InsertionLengthChange({Domain domain, Insertion insertion, int length}) = _$InsertionLengthChange._;

InsertionLengthChange._();

Expand All @@ -2151,13 +2173,14 @@ abstract class DeletionAdd

int get offset;

bool get all_helices;

StrandPart get strand_part => domain;

DeletionAdd clone_for_adjacent_substrand(Domain other_domain) =>
DeletionAdd(domain: other_domain, offset: offset);
DeletionAdd clone_for_other_domain(Domain domain) => rebuild((b) => b..domain.replace(domain));

/************************ begin BuiltValue boilerplate ************************/
factory DeletionAdd({Domain domain, int offset}) = _$DeletionAdd._;
factory DeletionAdd({Domain domain, int offset, bool all_helices}) = _$DeletionAdd._;

DeletionAdd._();

Expand All @@ -2171,17 +2194,26 @@ abstract class InsertionRemove

Insertion get insertion;

bool get all_helices;

int get offset => insertion.offset;

StrandPart get strand_part => domain;

InsertionRemove clone_for_adjacent_substrand(Domain other_domain) => InsertionRemove(
InsertionRemove clone_for_other_domain(Domain other_domain) => InsertionRemove(
domain: other_domain,
insertion: other_domain.insertions.firstWhere((i) => i.offset == offset),
);

/************************ begin BuiltValue boilerplate ************************/
factory InsertionRemove({Domain domain, Insertion insertion}) = _$InsertionRemove._;
factory InsertionRemove({Domain domain, Insertion insertion}) {
return InsertionRemove.from((b) => b
..domain.replace(domain)
..insertion.replace(insertion)
..all_helices = false);
}

factory InsertionRemove.from([void Function(InsertionRemoveBuilder) updates]) = _$InsertionRemove;

InsertionRemove._();

Expand All @@ -2195,13 +2227,24 @@ abstract class DeletionRemove

int get offset;

bool get all_helices;

StrandPart get strand_part => domain;

DeletionRemove clone_for_adjacent_substrand(Domain other_domain) =>
DeletionRemove clone_for_other_domain(Domain other_domain) =>
DeletionRemove(domain: other_domain, offset: offset);

/************************ begin BuiltValue boilerplate ************************/
factory DeletionRemove({Domain domain, int offset}) = _$DeletionRemove._;
factory DeletionRemove({Domain domain, int offset}) {
return DeletionRemove.from((b) => b
..domain.replace(domain)
..offset = offset
..all_helices = false);
}

factory DeletionRemove.from([void Function(DeletionRemoveBuilder) updates]) = _$DeletionRemove;

// factory DeletionRemove({Domain domain, int offset}) = _$DeletionRemove._;

DeletionRemove._();

Expand Down Expand Up @@ -2403,7 +2446,9 @@ abstract class ContextMenuHide
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// strand color picker

abstract class StrandColorPickerShow with BuiltJsonSerializable implements Action, Built<StrandColorPickerShow, StrandColorPickerShowBuilder> {
abstract class StrandColorPickerShow
with BuiltJsonSerializable
implements Action, Built<StrandColorPickerShow, StrandColorPickerShowBuilder> {
Strand get strand;

/************************ begin BuiltValue boilerplate ************************/
Expand All @@ -2414,10 +2459,14 @@ abstract class StrandColorPickerShow with BuiltJsonSerializable implements Actio
static Serializer<StrandColorPickerShow> get serializer => _$strandColorPickerShowSerializer;
}

abstract class StrandColorPickerHide with BuiltJsonSerializable implements Action, Built<StrandColorPickerHide, StrandColorPickerHideBuilder> {
abstract class StrandColorPickerHide
with BuiltJsonSerializable
implements Action, Built<StrandColorPickerHide, StrandColorPickerHideBuilder> {
/************************ begin BuiltValue boilerplate ************************/
factory StrandColorPickerHide() => StrandColorPickerHide.from((b) => b);
factory StrandColorPickerHide.from([void Function(StrandColorPickerHideBuilder) updates]) = _$StrandColorPickerHide;

factory StrandColorPickerHide.from([void Function(StrandColorPickerHideBuilder) updates]) =
_$StrandColorPickerHide;

StrandColorPickerHide._();

Expand Down
4 changes: 2 additions & 2 deletions lib/src/middleware/all_middleware.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import 'helix_grid_change.dart';
import 'helix_hide_all.dart';
import 'helix_idxs_change.dart';
import 'helix_offsets_change.dart';
import 'insertion_deletion_pairing.dart';
import 'insertion_deletion_batching.dart';
import 'load_file.dart';
import 'periodic_save_design_local_storage.dart';
import 'reselect_moved_dna_ends.dart';
Expand Down Expand Up @@ -55,7 +55,7 @@ final all_middleware = List<Middleware<AppState>>.unmodifiable([
reselect_moved_dna_ends_middleware,
reselect_moved_strands_middleware,
selections_intersect_box_compute_middleware,
insertion_deletion_pairing_middleware,
insertion_deletion_batching_middleware,
adjust_grid_position_middleware,
invalidate_png_middleware,
check_reflect_strands_legal_middleware,
Expand Down
72 changes: 72 additions & 0 deletions lib/src/middleware/insertion_deletion_batching.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'package:redux/redux.dart';
import 'package:scadnano/src/state/group.dart';
import '../state/domain.dart';
import '../state/design.dart';

import '../actions/actions.dart' as actions;
import '../state/app_state.dart';

/// Ensures that actions on insertions and deletions happen in pairs on adjacent domains.
/// Alternately, if Ctrl is pressed when the deletion/insertion is added, then it is added to every
/// domain in that HelixGroup at that offset.
insertion_deletion_batching_middleware(Store<AppState> store, dynamic action, NextDispatcher next) {
if (action is actions.InsertionOrDeletionAction) {
if (action.all_helices) {
// add deletion/insertion to all domains at this offset on all helices in same helix group
List<Domain> other_domains = find_other_domains(store.state.design, action.domain, action.offset);
if (other_domains.isEmpty) {
next(action);
} else {
var other_actions = [
for (var other_domain in other_domains) action.clone_for_other_domain(other_domain)
];
var batch_action = actions.BatchAction([action] + other_actions);
store.dispatch(batch_action);
}
} else {
// add deletion/insertion to other domain at this offset on this helix, if it exists
Domain paired_domain = find_paired_domain(store.state.design, action.domain, action.offset);
if (paired_domain == null) {
next(action);
} else {
var other_action = action.clone_for_other_domain(paired_domain);
var batch_action = actions.BatchAction([action, other_action]);
store.dispatch(batch_action);
}
}
} else {
next(action);
}
}

/// find all domains on other helices (including other direction on this helix)
/// in same HelixGroup as [domain].
List<Domain> find_other_domains(Design design, Domain domain, int offset) {
String group_name = design.group_name_of_domain(domain);
var helix_idxs_in_group = design.helix_idxs_in_group[group_name];
List<Domain> other_domains = [];
for (var helix_idx in helix_idxs_in_group) {
if (helix_idx == domain.helix) {
var paired_domain = find_paired_domain(design, domain, offset);
if (paired_domain != null) {
other_domains.add(paired_domain);
}
} else {
// only add to domains that don't have this offset as start or end
var other_domains_on_helix = design.domains_on_helix_at_offset_internal(helix_idx, offset);
other_domains.addAll(other_domains_on_helix);
}
}
return other_domains;
}

Domain find_paired_domain(Design design, Domain domain, int offset) {
var other_domains = design.domains_on_helix_at_offset_internal(domain.helix, offset);
for (var other_domain in other_domains) {
if (other_domain != domain) {
assert(other_domain.forward != domain.forward);
return other_domain;
}
}
return null;
}
33 changes: 0 additions & 33 deletions lib/src/middleware/insertion_deletion_pairing.dart

This file was deleted.

6 changes: 4 additions & 2 deletions lib/src/reducers/app_state_reducer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ AppState app_state_reducer(AppState state, action) {
..ui_state.replace(ui_state_global_reducer(state.ui_state, original_state, action)));

// Batch actions are grouped together but should just have one entry on the undo stack.
// So we set a variable telling the Undo reducer not to put anything on the stack for any of these atomic actions.
// So we set a variable telling the Undo reducer not to put anything on the stack
// for any of these atomic actions.
// However, it has already put something on the stack above for the BatchAction itself.
// This seems like it belongs in undo_redo_reducer, but the logic was tricky to handle, so we make a special case.
// This seems like it belongs in undo_redo_reducer, but the logic was tricky to handle,
// so we make a special case.
if (action is actions.BatchAction) {
for (actions.UndoableAction atomic_action in action.actions) {
state = app_state_reducer(state, actions.SkipUndo(atomic_action));
Expand Down
Loading

0 comments on commit b68b9f4

Please sign in to comment.