Skip to content

Commit

Permalink
Merge pull request #963 from UC-Davis-molecular-computing/941-dna-seq…
Browse files Browse the repository at this point in the history
…uences-justified

941 dna sequences justified
  • Loading branch information
rayzhuca authored Feb 5, 2024
2 parents 2cfac1f + a6b06ac commit e16239d
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 19 deletions.
21 changes: 21 additions & 0 deletions lib/src/actions/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2153,6 +2153,27 @@ abstract class ExportSvg with BuiltJsonSerializable implements Action, Built<Exp
ExportSvgType get type;
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Export every text in a DNA sequence separately

abstract class ExportSvgTextSeparatelySet
with BuiltJsonSerializable
implements Action, Built<ExportSvgTextSeparatelySet, ExportSvgTextSeparatelySetBuilder> {
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<ExportSvgTextSeparatelySet> get serializer => _$exportSvgTextSeparatelySetSerializer;
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Strand part action

Expand Down
186 changes: 170 additions & 16 deletions lib/src/middleware/export_svg.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import 'dart:html';
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: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 'package:scadnano/src/view/design_main_dna_sequence.dart';

import '../app.dart';
import '../state/app_state.dart';
Expand Down Expand Up @@ -51,11 +50,16 @@ export_svg_middleware(Store<AppState> 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 = get_cloned_svg_element_with_style([elt], store.state.ui_state.export_svg_text_separately);
}
_export_from_element(elt, 'main');
}
}
if (action.type == actions.ExportSvgType.side || action.type == actions.ExportSvgType.both) {
var elt = document.getElementById("side-view-svg");
Expand Down Expand Up @@ -107,9 +111,154 @@ List<Element> get_svg_elements_of_base_pairs(BuiltMap<int, BuiltList<int>> base_
return elts;
}

SvgSvgElement get_cloned_svg_element_with_style(List<Element> selected_elts) {
List<double> rotate_vector(List<double> 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 dominant_baseline_matrix(String dominant_baseline, double rot, String font) {
switch (dominant_baseline) {
case "ideographic":
return new DomMatrix([
1,
0,
0,
1,
...rotate_vector([0, (-3 * get_text_height(font)) / 12], rot)
]);
case "hanging":
return new DomMatrix([
1,
0,
0,
1,
...rotate_vector([0, (9 * get_text_height(font)) / 12], rot)
]);
case "central":
return new DomMatrix([
1,
0,
0,
1,
...rotate_vector([0, (4 * get_text_height(font)) / 12], rot)
]);
default:
return new DomMatrix([1, 0, 0, 1, 0, 0]);
}
}

Map matrix_to_map<T>(Matrix matrix) {
return {
"a": matrix.a,
"b": matrix.b,
"c": matrix.c,
"d": matrix.d,
"e": matrix.e,
"f": matrix.f,
};
}

Map dom_matrix_to_map(DomMatrix matrix) {
return {
"a": matrix.a,
"b": matrix.b,
"c": matrix.c,
"d": matrix.d,
"e": matrix.e,
"f": matrix.f,
};
}

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 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 (char_ele.style.getPropertyValue("dominant-baseline") != "") {
pos = pos.matrixTransform(dom_matrix_to_map(dominant_baseline_matrix(
char_ele.style.getPropertyValue("dominant-baseline"),
rot,
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");
}
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
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 text_ele = src_children[i] as TextContentElement;
if (text_ele.children.length == 1 && text_ele.children[0].tagName == "textPath") {
continue;
}
List<TextContentElement> 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 (text_ele is TextPathElement) {
// move TextPath children up and delete the TextPath
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();
}
portable_eles.forEach((v) => text_ele.parentNode.append(v));
text_ele.remove();
}
}
src.remove();
return src;
}

SvgSvgElement get_cloned_svg_element_with_style(List<Element> selected_elts, bool separate_text) {
var cloned_svg_element_with_style = SvgSvgElement()
..children = selected_elts.map(clone_and_apply_style).toList();
if (separate_text) {
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
document.body.append(cloned_svg_element_with_style);
Expand All @@ -118,7 +267,7 @@ SvgSvgElement get_cloned_svg_element_with_style(List<Element> selected_elts) {

// 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;
}
Expand Down Expand Up @@ -151,7 +300,7 @@ _export_svg(svg.SvgSvgElement svg_element, String filename_append) {
}

_copy_from_elements(List<Element> 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);
}

Expand Down Expand Up @@ -202,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
Expand All @@ -223,6 +372,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<Element> children_styled_to_remove = {};
var tag_name = elt_styled.tagName;

Expand All @@ -235,7 +385,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') {
Expand All @@ -249,14 +400,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');
}
Expand Down
4 changes: 4 additions & 0 deletions lib/src/reducers/app_ui_state_reducer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,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;
Expand Down Expand Up @@ -450,6 +453,7 @@ AppUIStateStorables app_ui_state_storable_local_reducer(AppUIStateStorables stor
..display_major_tick_widths_all_helices = TypedReducer<bool, actions.SetDisplayMajorTickWidthsAllHelices>(display_major_tick_widths_all_helices_reducer)(storables.display_major_tick_widths_all_helices, action)
..show_base_pair_lines = TypedReducer<bool, actions.ShowBasePairLinesSet>(show_base_pair_lines_reducer)(storables.show_base_pair_lines, action)
..show_base_pair_lines_with_mismatches = TypedReducer<bool, actions.ShowBasePairLinesWithMismatchesSet>(show_base_pair_lines_with_mismatches_reducer)(storables.show_base_pair_lines_with_mismatches, action)
..export_svg_text_separately = TypedReducer<bool, actions.ExportSvgTextSeparatelySet>(export_svg_text_separately_reducer)(storables.export_svg_text_separately, action)
..only_display_selected_helices = TypedReducer<bool, actions.SetOnlyDisplaySelectedHelices>(only_display_selected_helices_reducer)(storables.only_display_selected_helices, action)
..default_crossover_type_scaffold_for_setting_helix_rolls = TypedReducer<bool, actions.DefaultCrossoverTypeForSettingHelixRollsSet>(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<bool, actions.DefaultCrossoverTypeForSettingHelixRollsSet>(default_crossover_type_staple_for_setting_helix_rolls_reducer)(storables.default_crossover_type_staple_for_setting_helix_rolls, 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 @@ -123,6 +123,7 @@ part 'serializers.g.dart';
ShowModificationsSet,
ShowMismatchesSet,
SetShowEditor,
ExportSvgTextSeparatelySet,
SaveDNAFile,
PrepareToLoadDNAFile,
LoadDNAFile,
Expand Down
2 changes: 2 additions & 0 deletions lib/src/state/app_ui_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ abstract class AppUIState with BuiltJsonSerializable implements Built<AppUIState

bool get show_loopout_extension_length => 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;

Expand Down
3 changes: 3 additions & 0 deletions lib/src/state/app_ui_state_storables.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,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.
Expand Down Expand Up @@ -181,6 +183,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 ************************/
Expand Down
2 changes: 1 addition & 1 deletion lib/src/util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
5 changes: 4 additions & 1 deletion lib/src/view/design_main_dna_sequence.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -91,6 +92,7 @@ class DesignMainDNASequenceComponent extends UiComponent2<DesignMainDNASequenceP
}

static const classname_dna_sequence = 'dna-seq';
static const charWidth = 6.59375;

ReactElement _dna_sequence_on_domain(Domain domain) {
var seq_to_draw = domain.dna_sequence_deletions_insertions_to_spaces(
Expand Down Expand Up @@ -141,7 +143,8 @@ class DesignMainDNASequenceComponent extends UiComponent2<DesignMainDNASequenceP
..className = classname_dna_sequence
..x = '$x'
..y = '$y'
..textLength = '$text_length'
// ..textLength = '$text_length'
..letterSpacing = '${(text_length - charWidth * seq_to_draw.length) / (seq_to_draw.length - 1)}'
..transform = 'rotate(${rotate_degrees} ${rotate_x} ${rotate_y})'
..dy = '$dy')(seq_to_draw);
}
Expand Down
Loading

0 comments on commit e16239d

Please sign in to comment.