diff --git a/lib/src/middleware/check_mirror_strands_legal.dart b/lib/src/middleware/check_mirror_strands_legal.dart index e200d743f..55142195f 100644 --- a/lib/src/middleware/check_mirror_strands_legal.dart +++ b/lib/src/middleware/check_mirror_strands_legal.dart @@ -35,6 +35,7 @@ check_reflect_strands_legal_middleware(Store store, action, NextDispat var altered_design = design.remove_strands(strands_to_reflect); altered_design = altered_design.add_strands(reflected_strands); + // check overlapping strands try { altered_design.check_strands_overlap_legally(); } on IllegalDesignError catch (e) { @@ -44,6 +45,17 @@ check_reflect_strands_legal_middleware(Store store, action, NextDispat return; } + // check out of bounds helix + try { + altered_design.check_strands_in_bounds(); + } on IllegalDesignError catch (e) { + var msg = 'Cannot mirror these strands ${action.horizontal ? "horizontally" : "vertically"}\n' + 'Strands would go out of bounds:\n\n${e.cause}'; + window.alert(msg); + return; + } + + Map new_strands = {}; int idx_mirrored_strand = 0; for (var strand in strands_to_reflect) { diff --git a/lib/src/state/design.dart b/lib/src/state/design.dart index c00ea627e..f3f8c2cf5 100644 --- a/lib/src/state/design.dart +++ b/lib/src/state/design.dart @@ -1488,6 +1488,25 @@ abstract class Design with UnusedFields implements Built, } } + check_strands_in_bounds() { + String err_msg(Domain domain, int h_idx) { + return "domain found out of bounds on helix ${h_idx}: " + "\n${domain}"; + } + + for (int helix_idx in helices.keys) { + var domains = this.domains_on_helix(helix_idx); + var helix = helices[helix_idx]; + if (domains.length == 0) continue; + + for (var domain in domains) { + if (domain.start < helix.min_offset || domain.end > helix.max_offset) { + throw IllegalDesignError(err_msg(domain, helix_idx)); + } + } + } + } + @memoized BuiltMap> get helix_idxs_in_group { Map> map = {for (var name in groups.keys) name: []}; diff --git a/test/strand_mirror_unit_test.dart b/test/strand_mirror_unit_test.dart new file mode 100644 index 000000000..cd7896236 --- /dev/null +++ b/test/strand_mirror_unit_test.dart @@ -0,0 +1,341 @@ +// @dart=2.9 + +import 'dart:convert'; + +import 'package:built_collection/built_collection.dart'; +import 'package:redux/redux.dart'; +import 'package:scadnano/src/actions/actions.dart'; +import 'package:scadnano/src/json_serializable.dart'; +import 'package:scadnano/src/reducers/app_state_reducer.dart'; +import 'package:scadnano/src/reducers/change_loopout_length.dart'; +import 'package:scadnano/src/reducers/delete_reducer.dart'; +import 'package:scadnano/src/reducers/nick_ligate_join_by_crossover_reducers.dart'; +import 'package:scadnano/src/reducers/assign_domain_names_reducer.dart'; +import 'package:scadnano/src/reducers/strands_reducer.dart'; +import 'package:scadnano/src/state/address.dart'; +import 'package:scadnano/src/state/app_state.dart'; +import 'package:scadnano/src/state/domain.dart'; +import 'package:scadnano/src/state/helix.dart'; +import 'package:scadnano/src/state/grid.dart'; +import 'package:scadnano/src/state/loopout.dart'; +import 'package:scadnano/src/state/select_mode.dart'; +import 'package:scadnano/src/state/selectable.dart'; +import 'package:scadnano/src/state/strand.dart'; +import 'package:scadnano/src/state/strands_move.dart'; +import 'package:test/test.dart'; + +import 'package:scadnano/src/state/design.dart'; +import 'package:scadnano/src/actions/actions.dart' as actions; + +import 'utils.dart'; + +main() { + + group('StrandReflectInvalid', () { + List helices; + Design orig_design; + + setUp(() { + /* + + * initial design + + 0 5 10 + |----|----| + + 0 [---------\ + | + | + | + 1 | + <----/ + + */ + + helices = [ + Helix(idx: 0, min_offset: 0, max_offset: 10, grid: Grid.square), + Helix(idx: 1, min_offset: 5, max_offset: 10, grid: Grid.square) + ]; + orig_design = Design(helices: helices, grid: Grid.square); + + orig_design = orig_design + .strand(0, 0) + .move(10) + .cross(1) + .move(-5) + .commit(); + }); + + test('strand_reflect_horizontally_no_polarity_reverse', () { + + Store store = store_from_design(orig_design, initialize_app_instance: false); + store.dispatch( + actions.StrandsReflect( + strands: orig_design.strands, horizontal: true, reverse_polarity: false)); + + /* + + * hypothetical reflect --- but notice domain on helix 1 is out of bounds! (min_offset is 5) + * therefore, reflect action should be cancelled + + 0 5 10 + |----|----| + + 0 + /---------] + | + | + 1 \----> + + */ + expect_design_equal(store.state.design, orig_design); + }); + + test('strand_reflect_vertically_no_polarity_reverse', () { + + Store store = store_from_design(orig_design, initialize_app_instance: false); + store.dispatch( + actions.StrandsReflect( + strands: orig_design.strands, horizontal: false, reverse_polarity: false)); + + /* + + * hypothetical reflect --- but notice domain on helix 1 is out of bounds! (min_offset is 5) + * therefore, reflect action should be cancelled + + 0 5 10 + |----|----| + + 0 [----\ + | + | + | + 1 | + <---------/ + + */ + expect_design_equal(store.state.design, orig_design); + }); + + test('strand_reflect_horizontally_polarity_reverse', () { + + Store store = store_from_design(orig_design, initialize_app_instance: false); + store.dispatch( + actions.StrandsReflect( + strands: orig_design.strands, horizontal: true, reverse_polarity: true)); + + /* + + * hypothetical reflect --- but notice domain on helix 1 is out of bounds! (min_offset is 5) + * therefore, reflect action should be cancelled + + 0 5 10 + |----|----| + + 0 /---------> + | + | + | + 1 | + \----] + + */ + expect_design_equal(store.state.design, orig_design); + }); + + test('strand_reflect_vertically_polarity_reverse', () { + + Store store = store_from_design(orig_design, initialize_app_instance: false); + store.dispatch( + actions.StrandsReflect( + strands: orig_design.strands, horizontal: false, reverse_polarity: true)); + + /* + + * hypothetical reflect --- but notice domain on helix 1 is out of bounds! (min_offset is 5) + * therefore, reflect action should be cancelled + + 0 5 10 + |----|----| + + 0 + <----\ + | + | + 1 [---------/ + + */ + expect_design_equal(store.state.design, orig_design); + }); + }); + + group('StrandReflectValid', () { + List helices; + Design orig_design; + + setUp(() { + /* + + * initial design + + 0 5 10 + |----|----| + + 0 [---------\ + | + | + | + 1 | + <----/ + + */ + + helices = [ + Helix(idx: 0, min_offset: 0, max_offset: 10, grid: Grid.square), + Helix(idx: 1, min_offset: 0, max_offset: 10, grid: Grid.square) + ]; + orig_design = Design(helices: helices, grid: Grid.square); + + orig_design = orig_design + .strand(0, 0) + .move(10) + .cross(1) + .move(-5) + .commit(); + }); + + test('strand_reflect_horizontally_no_polarity_reverse', () { + + Store store = store_from_design(orig_design, initialize_app_instance: false); + store.dispatch( + actions.StrandsReflect( + strands: orig_design.strands, horizontal: true, reverse_polarity: false)); + + /* + + * resulting design + + 0 5 10 + |----|----| + + 0 + /---------] + | + | + 1 \----> + + */ + + var expected_design = Design(helices: helices, grid: Grid.square); + + expected_design = expected_design + .strand(0, 10) + .move(-10) + .cross(1) + .move(5) + .commit(); + expect_design_equal(store.state.design, expected_design); + }); + + test('strand_reflect_vertically_no_polarity_reverse', () { + + Store store = store_from_design(orig_design, initialize_app_instance: false); + store.dispatch( + actions.StrandsReflect( + strands: orig_design.strands, horizontal: false, reverse_polarity: false)); + + /* + + * resulting design + + 0 5 10 + |----|----| + + 0 [----\ + | + | + | + 1 | + <---------/ + + */ + + var expected_design = Design(helices: helices, grid: Grid.square); + + expected_design = expected_design + .strand(0, 5) + .move(5) + .cross(1) + .move(-10) + .commit(); + expect_design_equal(store.state.design, expected_design); + }); + + test('strand_reflect_horizontally_polarity_reverse', () { + + Store store = store_from_design(orig_design, initialize_app_instance: false); + store.dispatch( + actions.StrandsReflect( + strands: orig_design.strands, horizontal: true, reverse_polarity: true)); + + /* + + * resulting design + + 0 5 10 + |----|----| + + 0 /---------> + | + | + | + 1 | + \----] + + */ + + var expected_design = Design(helices: helices, grid: Grid.square); + + expected_design = expected_design + .strand(1, 5) + .move(-5) + .cross(0) + .move(10) + .commit(); + expect_design_equal(store.state.design, expected_design); + }); + + test('strand_reflect_vertically_polarity_reverse', () { + + Store store = store_from_design(orig_design, initialize_app_instance: false); + store.dispatch( + actions.StrandsReflect( + strands: orig_design.strands, horizontal: false, reverse_polarity: true)); + + /* + + * resulting design + + 0 5 10 + |----|----| + + 0 + <----\ + | + | + 1 [---------/ + + */ + + var expected_design = Design(helices: helices, grid: Grid.square); + + expected_design = expected_design + .strand(1, 0) + .move(10) + .cross(0) + .move(-5) + .commit(); + expect_design_equal(store.state.design, expected_design); + }); + }); +}