Skip to content

Commit

Permalink
added ability to convert all selected crossovers to loopouts via righ…
Browse files Browse the repository at this point in the history
…t-click context menu (haven't yet implemented changing all loopout lengths at once, including setting length to 0 to convert back to crossover)
  • Loading branch information
dave-doty committed Nov 3, 2020
1 parent cf4ab54 commit 9d681a1
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 35 deletions.
21 changes: 21 additions & 0 deletions lib/src/actions/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1540,6 +1540,27 @@ abstract class ConvertCrossoverToLoopout
static Serializer<ConvertCrossoverToLoopout> get serializer => _$convertCrossoverToLoopoutSerializer;
}

abstract class ConvertCrossoversToLoopouts
with BuiltJsonSerializable, UndoableAction
implements Built<ConvertCrossoversToLoopouts, ConvertCrossoversToLoopoutsBuilder> {
BuiltList<Crossover> get crossovers;

int get length;

/************************ begin BuiltValue boilerplate ************************/
factory ConvertCrossoversToLoopouts(Iterable<Crossover> crossovers, int length) =>
ConvertCrossoversToLoopouts.from((b) => b
..crossovers.replace(crossovers)
..length = length);

factory ConvertCrossoversToLoopouts.from([void Function(ConvertCrossoversToLoopoutsBuilder) updates]) =
_$ConvertCrossoversToLoopouts;

ConvertCrossoversToLoopouts._();

static Serializer<ConvertCrossoversToLoopouts> get serializer => _$convertCrossoversToLoopoutsSerializer;
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// nick/join

Expand Down
48 changes: 48 additions & 0 deletions lib/src/reducers/change_loopout_length.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import 'package:built_collection/built_collection.dart';
import 'package:scadnano/src/state/app_state.dart';
import 'package:scadnano/src/state/crossover.dart';

import '../state/loopout.dart';
import '../state/strand.dart';

Expand All @@ -16,6 +20,50 @@ Strand convert_crossover_to_loopout_reducer(Strand strand, actions.ConvertCrosso
return strand;
}

BuiltList<Strand> convert_crossovers_to_loopouts_reducer(BuiltList<Strand> strands, AppState state,
actions.ConvertCrossoversToLoopouts action) {
// TODO: need to dynamically calculate index of crossover as strand is being converted,
// since inserting loopouts earlier will increase indices

Map<String, List<Crossover>> crossovers_on_strand_id = {};
for (var crossover in action.crossovers) {
String strand_id = crossover.strand_id;
if (!crossovers_on_strand_id.containsKey(strand_id)) {
crossovers_on_strand_id[strand_id] = [];
}
crossovers_on_strand_id[strand_id].add(crossover);
}

var strands_builder = strands.toBuilder();
for (String strand_id in crossovers_on_strand_id.keys) {
Strand strand = state.design.strands_by_id[strand_id];
int strand_idx = strands.indexOf(strand);
var substrands_builder = strand.substrands.toBuilder();

List<Crossover> crossovers = crossovers_on_strand_id[strand_id];
// must sort by crossover order for this logic to work with num_crossovers_processed_on_strand variable
crossovers.sort((c1, c2) => c1.prev_domain_idx - c2.prev_domain_idx);
int num_crossovers_processed_on_strand = 0;
for (var crossover in crossovers) {
int prev_domain_idx = crossover.prev_domain_idx + num_crossovers_processed_on_strand;
int next_domain_idx = crossover.next_domain_idx + num_crossovers_processed_on_strand;
Loopout loopout_new = Loopout(
loopout_length: action.length,
prev_domain_idx: prev_domain_idx,
next_domain_idx: next_domain_idx + 1,
is_scaffold: strand.is_scaffold,
);
substrands_builder.insert(next_domain_idx, loopout_new);
num_crossovers_processed_on_strand++;
}
var new_strand = strand.rebuild((s) => s..substrands = substrands_builder);
new_strand = new_strand.initialize();
strands_builder[strand_idx] = new_strand;
}

return strands_builder.build();
}

Strand loopout_length_change_reducer(Strand strand, actions.LoopoutLengthChange action) {
int loopout_idx = strand.substrands.indexOf(action.loopout);
var substrands_builder = strand.substrands.toBuilder();
Expand Down
2 changes: 2 additions & 0 deletions lib/src/reducers/strands_reducer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ GlobalReducer<BuiltList<Strand>, AppState> strands_global_reducer = combineGloba
TypedGlobalReducer<BuiltList<Strand>, AppState, actions.Ligate>(ligate_reducer),
TypedGlobalReducer<BuiltList<Strand>, AppState, actions.JoinStrandsByCrossover>(
join_strands_by_crossover_reducer),
TypedGlobalReducer<BuiltList<Strand>, AppState, actions.ConvertCrossoversToLoopouts>(
convert_crossovers_to_loopouts_reducer),
]);

BuiltList<Strand> replace_strands_reducer(BuiltList<Strand> strands, actions.ReplaceStrands action) {
Expand Down
1 change: 1 addition & 0 deletions lib/src/serializers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ part 'serializers.g.dart';
Loopout,
LoopoutLengthChange,
ConvertCrossoverToLoopout,
ConvertCrossoversToLoopouts,
EditModeChoice,
EditModeToggle,
EditModesSet,
Expand Down
30 changes: 3 additions & 27 deletions lib/src/state/design.dart
Original file line number Diff line number Diff line change
Expand Up @@ -955,33 +955,9 @@ abstract class Design with UnusedFields implements Built<Design, DesignBuilder>,

_check_loopouts_not_consecutive_or_singletons_or_zero_length() {
for (var strand in strands) {
Design._check_loopout_not_singleton(strand);
Design._check_two_consecutive_loopouts(strand);
Design._check_loopouts_length(strand);
}
}

static _check_loopout_not_singleton(Strand strand) {
if (strand.substrands.length == 1 && strand.first_domain().is_loopout()) {
throw StrandError(strand, 'strand cannot have a single Loopout as its only domain');
}
}

static _check_two_consecutive_loopouts(Strand strand) {
for (int i = 0; i < strand.substrands.length - 1; i++) {
var domain1 = strand.substrands[i];
var domain2 = strand.substrands[i + 1];
if (domain1.is_loopout() && domain2.is_loopout()) {
throw StrandError(strand, 'cannot have two consecutive Loopouts in a strand');
}
}
}

static _check_loopouts_length(Strand strand) {
for (var loopout in strand.loopouts()) {
if (loopout.loopout_length <= 0) {
throw StrandError(strand, 'loopout length must be positive but is ${loopout.loopout_length}');
}
strand.check_loopout_not_singleton();
strand.check_two_consecutive_loopouts();
strand.check_loopouts_length();
}
}

Expand Down
19 changes: 12 additions & 7 deletions lib/src/state/selectable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'select_mode.dart';
import 'edit_mode.dart';
import 'strand.dart';
import '../app.dart';
import '../constants.dart' as constants;

part 'selectable.g.dart';

Expand Down Expand Up @@ -144,11 +145,13 @@ mixin Selectable {
// ctrlKey, metaKey, and shiftKey properties we need to check for.
// handle_selection(react.SyntheticPointerEvent event) {
handle_selection_mouse_down(MouseEvent event) {
if (event.ctrlKey || event.metaKey) {
app.dispatch(actions.Select(this, toggle: true));
} else {
// add to selection on mouse down
app.dispatch(actions.Select(this, toggle: false));
if (event.button == constants.LEFT_CLICK_BUTTON) {
if (event.ctrlKey || event.metaKey) {
app.dispatch(actions.Select(this, toggle: true));
} else {
// add to selection on mouse down
app.dispatch(actions.Select(this, toggle: false));
}
}
}

Expand All @@ -157,8 +160,10 @@ mixin Selectable {
// Shift or Ctrl key, so if we deselected whenever the user clicks without those keys, we would not be
// able to move multiple items.
handle_selection_mouse_up(MouseEvent event) {
if (!(event.ctrlKey || event.metaKey || event.shiftKey)) {
app.dispatch(actions.Select(this, toggle: false, only: true));
if (event.button == constants.LEFT_CLICK_BUTTON) {
if (!(event.ctrlKey || event.metaKey || event.shiftKey)) {
app.dispatch(actions.Select(this, toggle: false, only: true));
}
}
}
}
Expand Down
32 changes: 32 additions & 0 deletions lib/src/state/strand.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,41 @@ abstract class Strand
strand = strand.rebuild((s) => s..substrands = substrands_new);
}

_ensure_loopouts_legal();

return strand;
}

_ensure_loopouts_legal() {
check_loopout_not_singleton();
check_two_consecutive_loopouts();
check_loopouts_length();
}

check_loopout_not_singleton() {
if (substrands.length == 1 && first_domain().is_loopout()) {
throw StrandError(this, 'strand cannot have a single Loopout as its only domain');
}
}

check_two_consecutive_loopouts() {
for (int i = 0; i < substrands.length - 1; i++) {
var domain1 = substrands[i];
var domain2 = substrands[i + 1];
if (domain1.is_loopout() && domain2.is_loopout()) {
throw StrandError(this, 'cannot have two consecutive Loopouts in a strand');
}
}
}

check_loopouts_length() {
for (var loopout in loopouts()) {
if (loopout.loopout_length <= 0) {
throw StrandError(this, 'loopout length must be positive but is ${loopout.loopout_length}');
}
}
}

BuiltList<Substrand> get substrands;

@nullable
Expand Down
9 changes: 8 additions & 1 deletion lib/src/view/design_main_strand_crossover.dart
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,13 @@ class DesignMainStrandCrossoverComponent
if (new_length == null || new_length == 0) {
return;
}
app.dispatch(actions.ConvertCrossoverToLoopout(props.crossover, new_length));
var selected_crossovers = app.state.ui_state.selectables_store.selected_crossovers;
actions.UndoableAction action;
if (selected_crossovers.length > 0) {
action = actions.ConvertCrossoversToLoopouts(selected_crossovers, new_length);
} else {
action = actions.ConvertCrossoverToLoopout(props.crossover, new_length);
}
app.dispatch(action);
}
}

0 comments on commit 9d681a1

Please sign in to comment.