From fdad324ec7b438678a483d5f1f2a06c114fa7c68 Mon Sep 17 00:00:00 2001 From: Jovansonlee Cesar Date: Tue, 11 Feb 2020 15:25:40 +0800 Subject: Add Architecture document --- Architecture.md | 803 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 803 insertions(+) create mode 100644 Architecture.md diff --git a/Architecture.md b/Architecture.md new file mode 100644 index 0000000..9a182e3 --- /dev/null +++ b/Architecture.md @@ -0,0 +1,803 @@ +# Svgbob Architecture and Design phases + +## Name inspiration: +- svg for svg document and drawing. +- bob for Alice and Bob as common characters in most diagrams + Bob Ross - a painter who like to draws happy little trees. + +## Library used +- [nalgebra](https://www.nalgebra.org/) and [ncollide2d](https://ncollide.org/) for geometric function calculations such as calculating whether lines are intersecting, collinear. Computing the clipping of lines and boxes. +- [pom](https://github.com/J-F-Liu/pom) for parsing the styling directives(Legend) at the bottom of the document +- [sauron](https://github.com/ivanceras/sauron) for building the svg document object tree. + + +# **Iterations, re-architecture rewrites** + +## Phase 1: Exploding if statements. This was in elm + [fullcode](https://github.com/ivanceras/elm-examples/blob/master/elm-bot-lines/Grid.elm) + +```elm +getElement x y model = + let + char = get x y model + in + case char of + Just char -> + if isVertical char + && not (isNeighbor left isAlphaNumeric) + && not (isNeighbor right isAlphaNumeric) then + Just Vertical + else if isHorizontal char + && not (isNeighbor left isAlphaNumeric) + && not (isNeighbor right isAlphaNumeric) then + Just Horizontal + else if isIntersection char then + let + isVerticalJunctionLeft = + isNeighbor top isVertical + && isNeighbor(bottomOf x y model) isVertical + && isNeighbor(leftOf x y model) isHorizontal + + isVerticalJunctionRight = + isNeighbor top isVertical + && isNeighbor bottom isVertical + && isNeighbor right isHorizontal + + isHorizontalJunctionTop = + isNeighbor left isHorizontal + && isNeighbor right isHorizontal + && isNeighbor top isVertical + + isHorizontalJunctionBot = + isNeighbor left isHorizontal + && isNeighbor right isHorizontal + && isNeighbor bottom isVertical + + isTopLeftIntersection = + isNeighbor bottom isVertical && isNeighbor right isHorizontal + + isTopRightIntersection = + isNeighbor bottom isVertical && isNeighbor left isHorizontal + + isBottomRightIntersection = + isNeighbor top isVertical && isNeighbor left isHorizontal + + isBottomLeftIntersection = + isNeighbor top isVertical && isNeighbor right isHorizontal + + isCrossIntersection = + isNeighbor top isVertical + && isNeighbor bottom isVertical + && isNeighbor left isHorizontal + && isNeighbor right isHorizontal + + ... 200 more lines... + +``` +Though elm is fast, but if you throw a lot of conditional branching to it, it will slow it down. +At least I don't get to have runtime errors here if it was written in js. +Adding an edgecase is just appending a new if else statement at the bottom of the statements. + +Pros: Very simple design. Just if statements and return the appropriate shape the character will take form + Adding edge case behaviour is just appending an `else if` to the nearest conditional(`if`) behavior. + +Caveats: The fragments/drawing elements are named. Naming is hard, we can not name all of them. Consistency is broken. + + + + +## Phase2: Now in rust. The character behavior is stored in a `Vec<(condition, drawing_elements)>` +This is already close to the current architecture. + + Improvements: + - Runs a lot faster than elm. Converting the code from elm to rust, accelerate my learning of the usage of functional programming in rust. + - Consumed elements, if certain group of elements matches a higher level shapes, those elements are consumed/remove from the grid to + avoid generating additional drawing elements when iterated with the rest of the characters in the grid. + + +```rust + //get the paths in the location x,y + //if non path, then see if it can return a text path + fn get_elements(&self, x:isize, y:isize, settings: &Settings) -> Option>{ + ... + //common path lines + let vertical = Element::solid_line(center_top, center_bottom); + let horizontal = Element::solid_line(mid_left, mid_right); + let slant_left = Element::solid_line(high_left, low_right); + let slant_right = Element::solid_line(low_left, high_right); + let low_horizontal = Element::solid_line(low_left, low_right); + + + let match_list: Vec<(bool, Vec)> = + vec![ + /* + .- + | + */ + (self.is_char(this, is_round) + && self.is_char(right, is_horizontal) + && self.is_char(bottom, is_vertical), + vec![cxdy_cxey.clone(), arc_excy_cxdy.clone()] + ), + /* + -. + | + */ + (self.is_char(this, is_round) + && self.is_char(left, is_horizontal) + && self.is_char(bottom, is_vertical), + vec![cxdy_cxey.clone(), arc_cxdy_axcy.clone()] + ), + /* + | + '- + */ + (self.is_char(this, is_round) + && self.is_char(right, is_horizontal) + && self.is_char(top, is_vertical), + vec![cxay_cxby.clone(), arc_cxby_excy.clone()] + ), + /* + | + -' + */ + (self.is_char(this, is_round) + && self.is_char(left, is_horizontal) + && self.is_char(top, is_vertical), + vec![cxay_cxby.clone(), arc_axcy_cxby.clone()] + ), + /* + .- + / + */ + (self.is_char(this, is_round) + && self.is_char(right, is_horizontal) + && self.is_char(bottom_left, is_slant_right), + vec![axey_bxdy.clone(), arc_excy_bxdy.clone()] + ), + /* + -. + \ + */ + (self.is_char(this, is_round) + && self.is_char(left, is_horizontal) + && self.is_char(bottom_right, is_slant_left), + vec![exey_dxdy.clone(), arc_dxdy_axcy.clone()] + ), + /* + -. + / + */ + (self.is_char(this, is_round) + && self.is_char(left, is_horizontal) + && self.is_char(bottom_left, is_slant_right), + vec![axey_bxdy.clone(), arc_bxdy_axcy.clone()] + ), + /* + .- + \ + */ + (self.is_char(this, is_round) + && self.is_char(right, is_horizontal) + && self.is_char(bottom_right, is_slant_left), + vec![exey_dxdy.clone(), arc_excy_dxdy.clone()] + ), + /* + \ + '- + */ + (self.is_char(this, is_round) + && self.is_char(right, is_horizontal) + && self.is_char(top_left, is_slant_left), + vec![axay_bxby.clone(), arc_bxby_excy.clone()] + ), + /* + / + '- + */ + (self.is_char(this, is_round) + && self.is_char(right, is_horizontal) + && self.is_char(top_right, is_slant_right), + vec![dxby_exay.clone(), arc_dxby_excy.clone()] + ), + /* + \ + -' + */ + (self.is_char(this, is_round) + && self.is_char(left, is_horizontal) + && self.is_char(top_left, is_slant_left), + vec![axay_bxby.clone(), arc_axcy_bxby.clone()] + ), + /* + / + -' + */ + (self.is_char(this, is_round) + && self.is_char(left, is_horizontal) + && self.is_char(top_right, is_slant_right), + vec![dxby_exay.clone(), arc_axcy_dxby.clone()] + ), + ] +``` + +```rust + + // Circle 12 + // _ + // .' '. + // ( + ) + // `._.' + if self.in_left(3).is('(') + && self.in_right(3).is(')') + && self.in_top(2).is('_') + && self.bottom().is('_') + && self.top().in_left(2).any(",.") + && self.top_left().is('\'') + && self.top_right().any("`'") + && self.top().in_right(2).is('.') + && self.bottom().in_left(2).any("`'") + && self.bottom_left().is('.') + && self.bottom_right().any(".,") + && self.bottom().in_right(2).is('\'') + { + elm.push(open_circle(m, 12)); + consumed.extend(vec![ + left3(), + right3(), + top2(), + bottom(), + top_left2(), + top_left(), + top_right(), + top_right2(), + bottom_left2(), + bottom_left(), + bottom_right(), + bottom_right2(), + ]); + } +``` +Caveats: + + - Merging of small fragments requires checking against all the other fragments of the entire grid. Runtime complexity is at least O(n^2) + - Endorsing to shapes requires a lot of if statement comparisons and every cell is checked even for cell that has only a few elements that couldn't form into a certain shapes is tested. + - Processing high level stage and low level fragment stage is one execution. + - Drawing elements are still named. + + + + +## Phase 3: Attempts to add a signal strength to characters depending on their +neighboring character whether they should connect or not. This makes the dynamic behavior flexible +but the control flow is not very intuitive. + + - Strong + Strong should connect + - Medium + Medium connects + - Medium + Weak may connect + - Weak + Weak should not connect. + +```rust + + /// get the characteristic of a character + /// it's behavior and the intended behavior + /// + /// ┌─┬─┬─┬─┬─┐ + /// │a│b│c│d│e│ + /// ├─┼─┼─┼─┼─┤ + /// │f│g│h│i│j│ + /// ├─┼─┼─┼─┼─┤ + /// │k│l│m│n│o│ + /// ├─┼─┼─┼─┼─┤ + /// │p│q│r│s│t│ + /// ├─┼─┼─┼─┼─┤ + /// │u│v│w│x│y│ + /// └─┴─┴─┴─┴─┘ + /// + fn get_characteristic(&self) -> Option { + /////////////////////////// + // + // ., dot or period and comma + // + /////////////////////////// + if self.any(".,") { + Some(Characteristic { + is_static: false, + intensify: vec![ + // -. +. + ( + K, + Condition { + loc: left(), + can: ConnectTo(O, Medium), + }, + ), + // .- .+ + ( + O, + Condition { + loc: right(), + can: ConnectTo(K, Medium), + }, + ), + // _. + ( + U, + Condition { + loc: left(), + can: ConnectTo(Y, Strong), + }, + ), + // ._ + ( + Y, + Condition { + loc: right(), + can: ConnectTo(U, Strong), + }, + ), + // . + // / + ( + U, + Condition { + loc: bottom_left(), + can: ConnectTo(E, Strong), + }, + ), + // / only for / else _ + // . . will connect + ( + E, + Condition { + loc: top_right(), + can: IsStrongAll(vec![E, U]), + }, + ), + // . + // \ + ( + Y, + Condition { + loc: bottom_right(), + can: ConnectTo(A, Strong), + }, + ), + // \ only \ or else this connects as well _ + // . . + ( + A, + Condition { + loc: top_left(), + can: IsStrongAll(vec![A, Y]), + }, + ), + // . . + // | ' + ( + W, + Condition { + loc: bottom(), + can: ConnectTo(C, Medium), + }, + ), + // | only | + // . + ( + C, + Condition { + loc: top(), + can: Is('|'), + }, + ), + // . only X + // X + ( + Y, + Condition { + loc: bottom_right(), + can: Is('X'), + }, + ), + // . only X + // X + ( + U, + Condition { + loc: bottom_left(), + can: Is('X'), + }, + ), + ], + intended_behavior: vec![ + // .- + // / + (vec![O, U], vec![arc(o, q, 4), line(q, u)]), + // .- + // \ + (vec![O, Y], vec![arc(o, s, 4), line(s, y)]), + // -. + // \ + (vec![K, Y], vec![arc(s, k, 4), line(s, y)]), + // -. + // / + (vec![K, U], vec![line(u, q), arc(q, k, 2)]), + // / + // . + // / + (vec![U, E], vec![line(u, e)]), + // \ + // . + // \ + (vec![A, Y], vec![line(a, y)]), + // \ + // . + // | + (vec![A, W], vec![line(a, g), arc(r, g, 8), line(r, w)]), + // \ + // . + // / + (vec![A, U], vec![line(a, g), arc(q, g, 8), line(q, u)]), + // / + // . + // \ + (vec![E, Y], vec![line(e, i), arc(i, s, 8), line(s, y)]), + // | + // . + // | + (vec![C, W], vec![line(c, w)]), + // / + // . + // | + (vec![E, W], vec![line(e, i), arc(i, r, 8), line(r, w)]), + // | + // . + // / + (vec![C, U], vec![line(u, q), arc(q, h, 8), line(h, c)]), + // | + // . + // \ + (vec![C, Y], vec![line(c, h), arc(h, s, 8), line(s, y)]), + // . + // / \ + (vec![U, Y], vec![line(m, u), line(m, y)]), + ], + properties: vec![ + (O, Weak, vec![arc(o, r, 2)]), + (K, Weak, vec![arc(r, k, 2)]), + (W, Medium, vec![line(r, w)]), + (U, Weak, vec![line(q, u)]), + (Y, Weak, vec![line(s, y)]), + (A, Weak, vec![line(m, a)]), + (E, Weak, vec![line(m, e)]), + (F, Weak, vec![line(m, f)]), + (J, Weak, vec![line(m, j)]), + ], + }) + } +``` + +Pros: + - Characters are assigned with certain properties. This allows similar characters such as dash(-) and line drawing (-) to have the same behavior + without explicitly coding for each of those variations. + + +## Phase 4. +- Uses of Buffers + - StringBuffer, input strings are slices into rows and columns + - CellBuffer, which cells contains which character. + - FragmentBuffer, which cell contains what fragments(drawing elements) + - PropertyBuffer, what is the property of each cell based on the the character it contains. + +PropertyBuffer is calculated only once for each character, so the succeeding lookup should not waste execution time to recompute. + +```bob + ++--------------+ +------------+ +----------------+ +-----------------+ +| StringBuffer |------> | CellBuffer |-------->| FragmentBuffer |--------->| Svg drawing | ++--------------+ +------------+ +----------------+ +-----------------+ + \ ^ + \ +-------+ / + `-->| Spans | / + +-------+ / + \ / + \ +---------------+ .---------------. / + `-->|Contact groups |---/ endorse shapes /--' + +---------------+ `---------------' +``` + +- Optimizations. + - Usage of span and contact groups. + Span group together that are neighbors. Contact groups group together fragments + that are touching together. Cells don't need to be checked against other cells + when they are far from each other. Merging of fragments such as lines into longer + lines needs to interact only elements that are within its group. +- Endorsing group of fragments into higher level shapes. + - rect, rounded rect, circles, arcs are higher level shapes that are from small fragment components: arc,lines, + +- Tagging shapes. + Text inside of a shape with the pattern "{", "}" will become a tag of the enclosing shape. + At the DOM level, the shape is an svg dom element such as: rect,circle,path and the tag is the element `class` + which you can use css to apply a style to the element. The legend part at the bottom of the document is parsed + and converted into css which is then appended to the svg document. + +```rust + + /// + /// 0 1 2 3 4 B C D + /// 0┌─┬─┬─┬─┐ A┌─┬─┬─┬─┐E + /// 1├─┼─┼─┼─┤ │ │ │ │ │ + /// 2├─┼─┼─┼─┤ F├─G─H─I─┤J + /// 3├─┼─┼─┼─┤ │ │ │ │ │ + /// 4├─┼─┼─┼─┤ K├─L─M─N─┤O + /// 5├─┼─┼─┼─┤ │ │ │ │ │ + /// 6├─┼─┼─┼─┤ P├─Q─R─S─┤T + /// 7├─┼─┼─┼─┤ │ │ │ │ │ + /// 8└─┴─┴─┴─┘ U└─┴─┴─┴─┘Y + /// V W X + pub static ref ASCII_PROPERTIES: BTreeMap = { + + ... + + vec![ + + ////////////////////// + // dot period . + ////////////////////// + ( + '.', + vec![ + (Medium, vec![line(m,w)]), // connects down + (Weak, vec![line(m,k)]), // connects left + (Weak, vec![line(m,o)]), // connects right + ], + Arc::new( + move|top_left, top, top_right, left, right, bottom_left, bottom, bottom_right| { + vec![ + // . + // | + (bottom.line_strongly_overlap(c,h), vec![line(r,w)]), + // . + // / \ + (bottom_left.line_strongly_overlap(e,i) && bottom_right.line_strongly_overlap(a,g), vec![line(m,u), line(m,y)]), + // .- + // | + (right.line_overlap(k,l) && bottom.line_overlap(c,h), vec![arc(o,r,unit2), line(r,w)]), + // .- + // | + (right.line_overlap(k,l) && bottom_left.line_overlap(c,h), vec![arc(m,cell.bottom_left().c(),unit4), line(m,o)]), + // -. + // | + (left.line_overlap(n,o) && bottom.line_overlap(c,h), vec![arc(r,k,unit2), line(r,w)]), + // -. + // | + // exemption that bottom right is not a backquote + (!bottom_right.is('`') && left.line_overlap(n,o) && bottom_right.line_overlap(c,h), vec![arc(cell.bottom_right().c(),m,unit4), line(k,m)]), + // .- + // / + (right.line_overlap(k,l) && bottom_left.line_overlap(e,i), vec![arc(o, q, unit4), line(q, u)]), + // .- + // \ + (right.line_overlap(k,l) && bottom_right.line_overlap(a,g) , vec![arc(o, s, between1_2), line(s, y)]), + // -. + // \ + (left.line_overlap(n,o) && bottom_right.line_overlap(a,g), vec![arc(s, k, unit4), line(s, y)]), + // -. + // / + (left.line_overlap(n,o) && bottom_left.line_overlap(e,i), vec![arc(q, k, between1_2), line(u, q)]), + // . + // ( + (bottom_left.arcs_to(e,y), vec![arc(o, q, unit4), line(q, u)]), + // . + // ) + (bottom_right.arcs_to(u,a),vec![arc(s, k, unit4), line(s, y)]), + + // _.- + (left.line_overlap(u,y) && right.line_overlap(k,o), vec![line(u,o)]), + // -._ + (left.line_overlap(k,o) && right.line_overlap(u,y), vec![line(k,y)]), + + // `. + // ` + (left.is('`') && bottom_right.is('`'), vec![broken_line(cell.left().c(), cell.bottom_right().c())]), + // .' + // ' + (right.is('\'') && bottom_left.is('\''),vec![broken_line(cell.right().c(), cell.bottom_left().c())]), + // '. `. + // \ \ + ((left.is('\'')||left.is('`')) && bottom_right.is('\\'),vec![arc(y, cell.left().a(), unit8 * 2.0)]), + // .' + // / + (right.is('\'') && bottom_left.is('/'), vec![arc(cell.right().e(), u, unit8 * 2.0)]), + // TODO: restrict left, right, bottom, top_right, is not connecting to here + // | + // . + // / + (top.is('|') && bottom_left.is('/'), vec![arc(q,h,unit8), line(c,h), line(q,u)]), + // TODO: restrict left, right, bottom,top, is not connecting to here + // / + // . + // / + (top_right.is('/') && bottom_left.is('/'), vec![line(u,e)]), + // TODO: restrict left, right, bottom, top_left, does not connect to + // here + // | + // . + // \ + (top.is('|') && bottom_right.is('\\'), vec![line(c,h), arc(h,s,unit8), line(s,y)]), + ]} + ) + ), +``` + +## Endorse to higher level shapes + +```rust + + /// First phase of endorsing to shapes, in this case, rects and rounded_rects + /// + /// This function is calling on endorse methods that is applicable + /// to fragments that are touching, to be promoted to a shape. + /// These includes: rect, roundedrect, + fn endorse_rects(groups: Vec) -> (Vec, Vec) { + let mut fragments = vec![]; + let mut un_endorsed_rect: Vec = vec![]; + for group in groups { + if let Some(fragment) = is_rect(group) { + fragments.push(fragment); + } else { + un_endorsed_rect.push(group); + } + } + (fragments, un_endorsed_rect) + } + + ... + + /// group of fragments can be check if they form: + /// - rectangle + fn is_rect(fragments: &Vec) -> bool { + if fragments.len() == 4 { + let parallels = parallel_aabb_group(fragments); + if parallels.len() == 2 { + let (a1, a2) = parallels[0]; + let (b1, b2) = parallels[1]; + let line_a1 = fragments[a1].as_line(); + let line_b1 = fragments[b1].as_line(); + let line_a2 = fragments[a2].as_line(); + let line_b2 = fragments[b2].as_line(); + line_a1.is_touching_aabb_perpendicular(line_b1) + && line_a2.is_touching_aabb_perpendicular(line_b2) + } else { + false + } + } else { + false + } + } + + ... + + /// [X](Done) TODO: search only the subset of contacts that matches the circle. + /// if it is a subset then the circle is matched and the non-matching ones are returned + pub fn endorse_circle(search: &Vec) -> Option<(&Circle, Vec)> { + FRAGMENTS_CIRCLE.iter().rev().find_map(|(contacts, circle)| { + let (matched, unmatched) = is_subset_of(contacts, search); + if matched { Some((circle, unmatched)) } else { None } + }) + } + + ... + + /// This function is calling on endorse algorithmn on fragments that + /// are neighbors, but not necessarily touching to be promoted to a shape. + /// These includes: circle, arc, and line with arrow heads. + fn endorse_circles_and_arcs(groups: Vec) -> (Vec, Vec) { + let mut fragments = vec![]; + let mut un_endorsed_circles: Vec = vec![]; + if let Some((circle, unmatched)) = circle_map::endorse_circle(&groups) { + fragments.push(circle.clone().into()); + for um in unmatched { + un_endorsed_circles.push(groups[um].clone()); + } + } else if let Some(arc) = circle_map::endorse_arc(&groups) { + fragments.push(arc.clone().into()); + } else { + un_endorsed_circles.extend(groups) + } + (fragments, un_endorsed_circles) + } +``` + +```rust + + // ascii art, Center Cell, Center Point, radius + pub static ref CIRCLE_MAP: Vec<(&'static str, Cell, Point, f32)> = + vec![ + // CIRCLE_1 + //center 0,0,o, radius = 0.5 + (r#" + () + "#, Cell::new(0,0), Cell::new(0,0).o(), 0.5), + + ... + + // CIRCLE_4 + //center: 2,1,m radius: 2.0 + (r#" + ,-. + ( ) + `-' + "#, Cell::new(2,1), Cell::new(2,1).m(), 2.0), + + + // CIRCLE_12 + //center:6,3,m radius: 6.0 + (r#" + _____ + ,' `. + / \ + ( ) + \ / + `._____.' + "#, Cell::new(6,3), Cell::new(6,3).m(), 6.0), + + // CIRCLE_17 + //center: 8,4,o radius: 8.5 + (r#" + .--------. + ,' `. + / \ + | | + | | + | | + \ / + `. .' + `--------' + "#, Cell::new(8,4), Cell::new(8,4).o(), 8.5), + + + ... + + // CIRCLE_20 + // center: 10,5,m radius: 10 + (r#" + _.-'''''''-._ + ,' `. + / \ + . . + | | + | | + | | + \ / + `._ _.' + '-.......-' + "#, Cell::new(10,5), Cell::new(10,5).m(), 10.0), + ]; + +``` + +## Flexibility: +- Adding behaviours and edge-cases is still simple +- Due to the grouping of spans and contacts, it is now more efficient to check whether a combination + of fragments can be endorsed into a high level shapes. +- Behavior can be coded according to the properties of their neighboring characters, + and/or can also specify that a neighbor should match a specific character. (ie: neighboring character top should be a caret `^`, then this is the behavior) + +## Modular: +- Adding more shapes it can endorse to, such as in the circle map is merely putting the ascii art +to right next to the existing ones, as oppused to the multiple if-statements in Phase 2 +- Adding endorse code to certain shapes is merely describing the filter rules on the combination of the fragments + +## Extensiblity: +- Since the new architecture is now implemented through the use of Buffers. It opens to a lot of possible improvements. +- Shapes are now properly endorsed, which can be styled with css standard. Which means, users can add crazy css-animation to the shapes. +- Making the cell buffer as a canvas. Meaning you can draw lines and shapes on it, while the system will try to match + the closest character appropriate to the input shape. A possibility of generating an ascii drawing from svg diagrams. + The reverse of the functionality of svgbob. + +## Adaption of svgbob +- As archlinux [package](https://aur.archlinux.org/packages/svgbob-git/) +- As diagram module for [asciidoctor](https://asciidoctor.org/docs/asciidoctor-diagram/) +- [Asciigrid](https://gitlab.com/mbarkhau/asciigrid/) +- [kroki.io](https://kroki.io/) -- cgit v1.2.3