using System; using System.Collections.Generic; using System.Linq; using System.Drawing; namespace Contours { public class Shape { class Exception: System.Exception { } class Position { public Point point; public readonly Circuit links; public Position() { links = new Circuit(this); } public Position(Point point): this() { this.point = point; } } class Contour { public bool inverted = false; public Contour parent; public readonly List childs = new List(); public readonly Circuit forward; public readonly Circuit backward; public Contour() { forward = new Circuit(this); backward = new Circuit(this); } } class Link { public bool forward = false; public Position nextPosition = null; public readonly Circuit.Entry position; public readonly Circuit.Entry contour; public Link() { position = new Circuit.Entry(this); contour = new Circuit.Entry(this); } public Link split(Shape shape, Position position) { if (position.point == this.position.getParent().point) return this; if (position.point == nextPosition.point) return nextPosition.links.getFirst(); shape.log.linkSplitted(this, position); Link link = new Link(); link.position.insertBack(position.links); link.contour.insertAfterOf(contour); link.forward = forward; link.nextPosition = nextPosition; nextPosition = position; shape.links.Add(link); return link; } public Link split(Shape shape, Link positionLink) { return split(shape, positionLink.position.getParent()); } } class Log { public bool enabled = true; public string filename = "log.txt"; public void line(string line) { if (!enabled) return; System.IO.File.AppendAllLines(filename, new string[] { line }); } public void linkCreated(Link link) { if (!enabled) return; line(string.Format( "Link created {0} {1} {2}", link.forward ? "->" : "<-", link.position.getParent().point, link.nextPosition.point )); } public void linkRemoved(Link link) { if (!enabled) return; line(string.Format( "Link removed {0} {1} {2}", link.forward ? "->" : "<-", link.position.getParent().point, link.nextPosition.point )); } public void linkSplitted(Link link, Position position) { if (!enabled) return; line(string.Format( "Link splitted {0} {1}/{3}/{2}", link.forward ? "->" : "<-", link.position.getParent().point, link.nextPosition.point, position.point )); } public void linkSwapped(Link a, Link b) { if (!enabled) return; line(string.Format( "Link swapped {0} {1} {2} - {3} {4} {5}", a.forward ? "->" : "<-", a.position.getParent().point, a.nextPosition.point, b.forward ? "->" : "<-", b.position.getParent().point, b.nextPosition.point )); } } readonly List positions = new List(); readonly List links = new List(); readonly List contours = new List(); readonly List rootContours = new List(); readonly Log log = new Log(); public void clear() { positions.Clear(); links.Clear(); contours.Clear(); rootContours.Clear(); } public void setContour(IEnumerable contour) { setContours(new IEnumerable[] { contour }); } public void setContours(IEnumerable>> contours) { List> list = new List>(); foreach(IEnumerable> c in contours) list.AddRange(c); setContours(list); } public void setContours(IEnumerable> contours) { clear(); log.line("---- setContours begin ----"); foreach(IEnumerable contour in contours) { if (contour.Count() >= 3) { Link firstForward = null; Link firstBackward = null; Link previousForward = null; Link previousBackward = null; foreach(Point point in contour) { Position position = new Position(point); positions.Add(position); Link forward = new Link(); forward.forward = true; forward.position.insertBack(position.links); if (previousForward != null) previousForward.nextPosition = forward.position.getParent(); links.Add(forward); Link backward = new Link(); backward.forward = false; backward.position.insertBack(position.links); if (previousBackward != null) backward.nextPosition = previousBackward.position.getParent(); links.Add(backward); if (firstForward == null) firstForward = forward; if (firstBackward == null) firstBackward = backward; previousForward = forward; previousBackward = backward; } previousForward.nextPosition = firstForward.position.getParent(); firstBackward.nextPosition = previousBackward.position.getParent(); } } foreach(Link link in links) log.linkCreated(link); calculate(true); log.line("---- setContours end ----"); } // returns list of root contour groups // first contour in group is outer contour, others - inner public List>> getContours() { List>> groups = new List>>(); foreach(Contour root in rootContours) { List> list = new List>(); list.Add(contourToList(root)); foreach(Contour c in root.childs) list.Add(contourToList(c)); groups.Add(list); } return groups; } List contourToList(Contour contour) { List list = new List(); for(Link link = contour.forward.getFirst(); link != null; link = link.contour.getNextLinear()) list.Add(link.position.getParent().point); return list; } void resetTraceInformation() { for(int i = 0; i < contours.Count; ++i) { contours[i].forward.clear(); contours[i].backward.clear(); contours[i].childs.Clear(); contours[i].parent = null; } contours.Clear(); rootContours.Clear(); } void removeLink(Link link) { log.linkRemoved(link); link.position.unlink(); link.contour.unlink(); link.nextPosition = null; links.Remove(link); } void removeEmptyPositions() { for(int i = 0; i < positions.Count; ++i) if (positions[i].links.empty()) positions.RemoveAt(i--); } void findIntersections() { bool retry = true; while(retry) { retry = false; // merge positions // this procedure may create zero-length links for(int i = 0; i < positions.Count; ++i) { for(int j = i+1; j < positions.Count; ++j) { if (positions[i].point == positions[j].point) { while(positions[j].links.getFirst() != null) positions[j].links.getFirst().position.insertBack(positions[i].links); positions.RemoveAt(j--); } } } // remove zero-length links // this procedure may create empty positions for(int i = 0; i < links.Count; ++i) if (links[i].position.getParent() == links[i].nextPosition) removeLink(links[i--]); // so we need to... removeEmptyPositions(); // check intersections // this procedure may create new positions, new links and ne intersections // so we need to repeat all cycle when intersection found for(int i = 0; i < links.Count; ++i) { for(int j = i+1; j < links.Count; ++j) { Link a = links[i]; Link b = links[j]; Position a0 = a.position.getParent(); Position a1 = a.nextPosition; Position b0 = b.position.getParent(); Position b1 = b.nextPosition; Point c = new Point(0, 0); retry = true; // will reset to false if no intersection switch(Geometry.findIntersection(a0.point, a1.point, b0.point, b1.point, out c)) { case Geometry.IntersectionType.Cross: Position p = new Position(c); positions.Add(p); a.split(this, p); b.split(this, p); break; case Geometry.IntersectionType.Touch_a0: b.split(this, a0); break; case Geometry.IntersectionType.Touch_a1: b.split(this, a1); break; case Geometry.IntersectionType.Touch_b0: a.split(this, b0); break; case Geometry.IntersectionType.Touch_b1: a.split(this, b1); break; case Geometry.IntersectionType.Along_a0_b0_a1_b1: a.split(this, b0); b.split(this, a1); break; case Geometry.IntersectionType.Along_a0_b0_b1_a1: a.split(this, b0).split(this, b1); break; case Geometry.IntersectionType.Along_a0_b1_a1_b0: a.split(this, b1); b.split(this, a1); break; case Geometry.IntersectionType.Along_a0_b1_b0_a1: a.split(this, b1).split(this, b0); break; case Geometry.IntersectionType.Along_b0_a0_a1_b1: b.split(this, a0).split(this, a1); break; case Geometry.IntersectionType.Along_b0_a0_b1_a1: a.split(this, b1); b.split(this, a0); break; case Geometry.IntersectionType.Along_b1_a0_a1_b0: b.split(this, a1).split(this, a0); break; case Geometry.IntersectionType.Along_b1_a0_b0_a1: a.split(this, b0); b.split(this, a0); break; default: retry = false; break; } } } } } // This function removes link from it position // and also recursive removes part of contour to nearest fork void removeLinkFromPosition(Link link) { Position position = link.position.getParent(); Link nextToRemove = null; // remove back link if (link.nextPosition != null) { Link backLink = link.nextPosition.links.getFirst(); while (backLink != null) { if ( backLink.forward != link.forward && backLink.nextPosition == position ) { removeLink(backLink); break; } backLink = backLink.position.getNextLinear(); } if (link.nextPosition.links.getCount() == 1) nextToRemove = link.nextPosition.links.getFirst(); } // remove removeLink(link); // remove next if (nextToRemove != null) removeLinkFromPosition(nextToRemove); } // Before trace contours we can (and should) to do some actions // to optimize links in each vertes. // So we can remove duplicated contour chunks // and chunks which placed inside contours of similar type // and not affecting to final image void optimizePositions() { foreach(Position position in positions) { // remove duplicates for(Link a = position.links.getFirst(); a != null; a = a.position.getNextLinear()) { Position otherPosition = a.nextPosition; // count forward and backward links int count = 0; Link forwardLink = null; Link backwardLink = null; Link b = a; do { if (b.nextPosition == otherPosition) { if (b.forward) { forwardLink = b; ++count; } else { backwardLink = b; --count; } } b.position.getNext(); } while(b != a); // remove extra links Link linkToSave = count > 0 ? forwardLink : count < 0 ? backwardLink : null; b = position.links.getFirst(); while(b != null) { if (b.nextPosition == otherPosition && b != linkToSave) { // remove link, a and b becomes invalid removeLinkFromPosition(b); // so reset a and b to repeat the current and parent cycles b = a = position.links.getFirst(); } else { b = b.position.getNextLinear(); } } } if (position.links.getCount() >= 3) { // sort links Link first = position.links.getFirst(); Link a = first; while (true) { Link b = a.position.getNext(); Link c = b.position.getNext(); if ( !Geometry.isCCW( position.point, a.nextPosition.point, b.nextPosition.point, c.nextPosition.point )) { log.linkSwapped(b, c); b.position.swapWith(c.position); first = a = c; continue; } a = b; if (a == first) break; }; // remove invisible contours from position a = first = position.links.getFirst(); while(true) { bool previous = a.position.getPrevious().forward; bool current = a.forward; bool next = a.position.getNext().forward; if ( ( previous && current && !next) || (!previous && !current && next) ) { // remove link removeLinkFromPosition(a); if (position.links.getCount() < 3) break; a = first = position.links.getFirst(); } else { a = a.position.getNext(); if (a == first) break; } }; } } } void buildContours() { for(int i = 0; i < links.Count; ++i) { if (links[i].forward && links[i].contour.getParent() == null) { Contour contour = new Contour(); contours.Add(contour); Link forwardLink = links[i]; Link backwardLink = null; do { if (forwardLink == null || !forwardLink.forward || forwardLink.contour.getParent() != null) throw new Exception(); // find pair for(Link l = forwardLink.nextPosition.links.getFirst(); l != null; l = l.position.getNextLinear()) if (l.nextPosition == forwardLink.position.getParent()) { backwardLink = l; break; } if (backwardLink == null || backwardLink.forward || backwardLink.contour.getParent() != null) throw new Exception(); forwardLink.contour.insertBack(contour.forward); backwardLink.contour.insertBack(contour.backward); // select first link by the left (links should be CCW-ordered) forwardLink = backwardLink.position.getNext(); } while (forwardLink != links[i]); } } } // Normally we should not have free links after tracing void removeFreeLinks() { for(int i = 0; i < links.Count; ++i) if (links[i].contour.getParent() == null) throw new Exception(); } int countIntersectionsWithContour(Point p0, Point p1, Contour contour) { int count = 0; for(Link link = contour.forward.getFirst(); link != null; link = link.contour.getNextLinear()) { Point pp0 = link.position.getParent().point; Point pp1 = link.contour.getNext().position.getParent().point; int d = Math.Sign( (long)(p0.Y - p1.Y)*(long)(pp1.X - pp0.X) + (long)(p1.X - p0.X)*(long)(pp1.Y - pp0.Y) ); Point c; switch(Geometry.findIntersection(p0, p1, pp0, pp1, out c)) { case Geometry.IntersectionType.Cross: count += 2*d; break; case Geometry.IntersectionType.Touch_b0: count += d; break; case Geometry.IntersectionType.Touch_b1: count -= d; break; default: break; } } return count; } bool isContourInverted(Contour contour) { Link first = contour.forward.getFirst(); Point p0 = first.position.getParent().point; Point p1 = first.contour.getNext().position.getParent().point; Geometry.makeLongestLine(p0, ref p1); return countIntersectionsWithContour(p0, p1, contour) < 0; } bool isContourInside(Contour inner, Contour outer) { Link first = inner.forward.getFirst(); Point p0 = first.position.getParent().point; Point p1 = first.contour.getNext().position.getParent().point; Geometry.makeLongestLine(p0, ref p1); return countIntersectionsWithContour(p0, p1, outer) != 0; } void organizeChildContours(Contour contour, Contour parent) { // set parent contour.parent = parent; // remove sub-childs from parent list if (parent != null) foreach(Contour c in contour.childs) parent.childs.Remove(c); // sub-calls foreach(Contour c in contour.childs) organizeChildContours(c, contour); } void invertContour(Contour contour) { contour.inverted = !contour.inverted; contour.forward.swapWith(contour.backward); for(Link link = contour.forward.getFirst(); link != null; link.contour.getNextLinear()) link.forward = !link.forward; for(Link link = contour.backward.getFirst(); link != null; link.contour.getNextLinear()) link.forward = !link.forward; } void buildContoursHierarhy(bool simple) { // calculate directions of contours foreach(Contour contour in contours) contour.inverted = isContourInverted(contour); // find childs rootContours.AddRange(contours); for(int i = 0; i < contours.Count; ++i) { for(int j = 0; j < contours.Count; ++j) { if (isContourInside(contours[i], contours[j])) { contours[j].childs.Add(contours[i]); rootContours.Remove(contours[i]); } else if (isContourInside(contours[j], contours[i])) { contours[i].childs.Add(contours[j]); rootContours.Remove(contours[j]); } } } // organize childs foreach(Contour c in rootContours) organizeChildContours(c, null); // remove invisible contours for(int i = 0; i < contours.Count; ++i) { bool parentInverted = contours[i].parent == null || contours[i].parent.inverted; if (parentInverted == contours[i].inverted) { if (simple) { // if simple then invert invisible contours instead of removing invertContour(contours[i]); } else { // remove contour foreach(Contour c in contours[i].childs) c.parent = contours[i].parent; List parentList = contours[i].parent == null ? rootContours : contours[i].parent.childs; parentList.AddRange(contours[i].childs); contours[i].parent = null; contours[i].childs.Clear(); while(!contours[i].forward.empty()) removeLink(contours[i].forward.getFirst()); while(!contours[i].backward.empty()) removeLink(contours[i].backward.getFirst()); contours.RemoveAt(i--); } } } // move contours in the holes to root for(int i = 0; i < rootContours.Count; ++i) { Contour contourA = rootContours[i]; foreach(Contour contourB in contourA.childs) { if (contourB.childs.Count != 0) { foreach(Contour c in contourB.childs) c.parent = null; rootContours.AddRange(contourB.childs); } } } } void calculate(bool simple) { resetTraceInformation(); findIntersections(); optimizePositions(); buildContours(); removeFreeLinks(); buildContoursHierarhy(simple); removeEmptyPositions(); } public void invert() { foreach(Contour contour in contours) invertContour(contour); } static Shape mix(Shape a, bool invertA, Shape b, bool invertB) { Shape sum = new Shape(); sum.log.enabled = a.log.enabled && b.log.enabled; sum.log.line("---- mix begin ----"); // clone a for(int i = 0; i < a.positions.Count; ++i) sum.positions.Add(new Position(a.positions[i].point)); for(int i = 0; i < a.positions.Count; ++i) { for(Link link = a.positions[i].links.getFirst(); link != null; link = link.position.getNextLinear()) { Link l = new Link(); l.forward = invertA != link.forward; l.nextPosition = sum.positions[a.positions.IndexOf(link.nextPosition)]; l.position.insertBack(sum.positions[i].links); sum.links.Add(l); } } // clone b for(int i = 0; i < b.positions.Count; ++i) sum.positions.Add(new Position(b.positions[i].point)); for(int i = 0; i < b.positions.Count; ++i) { for(Link link = b.positions[i].links.getFirst(); link != null; link = link.position.getNextLinear()) { Link l = new Link(); l.forward = invertB != link.forward; l.nextPosition = sum.positions[a.positions.Count + b.positions.IndexOf(link.nextPosition)]; l.position.insertBack(sum.positions[a.positions.Count + i].links); sum.links.Add(l); } } foreach(Link link in sum.links) sum.log.linkCreated(link); sum.calculate(false); sum.log.line("---- mix end ----"); return sum; } public static Shape add(Shape a, Shape b) { return mix(a, false, b, false); } public static Shape subtract(Shape a, Shape b) { return mix(a, false, b, true); } public static Shape xor(Shape a, Shape b) { return add(subtract(a, b), subtract(b, a)); } public static Shape intersection(Shape a, Shape b) { return subtract(add(a, b), xor(a, b)); } } }