private static TextOutline LeftAlign(TextOutline outline) { var center = outline.GetCenter(); var first = outline.GetFirstGlyphCenter(); return(outline.Translate(center.X - first.X, center.Y - first.Y)); }
/// <summary> /// Generate an annotation 'label' for an atom (located at 'basePoint'). The label is offset from /// the basePoint by the provided 'distance' and 'direction'. /// </summary> /// <param name="basePoint">the relative (0,0) reference</param> /// <param name="label">the annotation text</param> /// <param name="direction">the direction along which the label is laid out</param> /// <param name="distance">the distance along the direct to travel</param> /// <param name="scale">the font scale of the label</param> /// <param name="font">the font to use</param> /// <param name="symbol">the atom symbol to avoid overlap with</param> /// <returns>the position text outline for the annotation</returns> internal static TextOutline GenerateAnnotation(Vector2 basePoint, string label, Vector2 direction, double distance, double scale, Typeface font, double emSize, AtomSymbol symbol) { var italicHint = label.StartsWith(ItalicDisplayPrefix); label = italicHint ? label.Substring(ItalicDisplayPrefix.Length) : label; var annFont = italicHint ? new Typeface(font.FontFamily, WPF.FontStyles.Italic, font.Weight, font.Stretch) : font; var annOutline = new TextOutline(label, annFont, emSize).Resize(scale, -scale); // align to the first or last character of the annotation depending on the direction var center = direction.X > 0.3 ? annOutline.GetFirstGlyphCenter() : direction.X < -0.3 ? annOutline.GetLastGlyphCenter() : annOutline.GetCenter(); // Avoid atom symbol if (symbol != null) { var intersect = symbol.GetConvexHull().Intersect(VecmathUtil.ToPoint(basePoint), VecmathUtil.ToPoint(VecmathUtil.Sum(basePoint, direction))); // intersect should never be null be check against this if (intersect != null) { basePoint = VecmathUtil.ToVector(intersect); } } direction *= distance; direction += basePoint; // move to position return(annOutline.Translate(direction.X - center.X, direction.Y - center.Y)); }
public TextOutline PositionAfter(TextOutline before, TextOutline after) { var fixedBounds = before.GetBounds(); var movableBounds = after.GetBounds(); after = after.Translate((fixedBounds.Right + padding) - movableBounds.Left, 0); return(after); }
/// <summary> /// Position the mass label relative to the element label. The mass adjunct is position to the /// top left of the element label. /// </summary> /// <param name="massLabel">mass label outline</param> /// <param name="elementLabel">element label outline</param> /// <returns>positioned mass label</returns> public TextOutline PositionMassLabel(TextOutline massLabel, TextOutline elementLabel) { var elementBounds = elementLabel.GetBounds(); var massBounds = massLabel.GetBounds(); return(massLabel.Translate((elementBounds.Left - padding) - massBounds.Right, (elementBounds.Top - (massBounds.Height / 2)) - massBounds.Top)); }
public TextOutline PositionSuperscript(TextOutline label, TextOutline superscript) { var labelBounds = label.GetBounds(); var superscriptBounds = superscript.GetBounds(); superscript = superscript.Translate((labelBounds.Right + padding) - superscriptBounds.Left, (labelBounds.Top - (superscriptBounds.Height / 2)) - superscriptBounds.Top); return(superscript); }
/// <summary> /// Positions an outline in the subscript position relative to another 'primary' label. /// </summary> /// <param name="label">a label outline</param> /// <param name="subscript">the label outline to position as subscript</param> /// <returns>positioned subscript outline</returns> public TextOutline PositionSubscript(TextOutline label, TextOutline subscript) { var hydrogenBounds = label.GetBounds(); var hydrogenCountBounds = subscript.GetBounds(); subscript = subscript.Translate((hydrogenBounds.Right + padding) - hydrogenCountBounds.Left, (hydrogenBounds.Bottom + (hydrogenCountBounds.Height / 2)) - hydrogenCountBounds.Bottom); return(subscript); }
public void TransformedCenter() { TextOutline original = new TextOutline("Cl", font, emSize); TextOutline transformed = original.Translate(0, -5); var oCenter = original.GetCenter(); var tCenter = transformed.GetCenter(); Assert.AreEqual(oCenter.X, tCenter.X, 0.01); Assert.AreEqual(oCenter.Y - 5, tCenter.Y, 0.01); }
public void BoundsTransformedWithYTranslation() { TextOutline original = new TextOutline("Cl", font, emSize); TextOutline transformed = original.Translate(0, -5); var oBounds = original.GetBounds(); var tBounds = transformed.GetBounds(); Assert.AreEqual(oBounds.X, tBounds.X, 0.01); Assert.AreEqual(oBounds.Y - 5, tBounds.Y, 0.01); Assert.AreEqual(oBounds.Width, tBounds.Width, 0.01); Assert.AreEqual(oBounds.Height, tBounds.Height, 0.01); }
/// <summary> /// Position the hydrogen label relative to the element label. /// </summary> /// <param name="position">relative position where the hydrogen is placed</param> /// <param name="element">the outline of the element label</param> /// <param name="hydrogen">the outline of the hydrogen</param> /// <returns>positioned hydrogen label</returns> public TextOutline PositionHydrogenLabel(HydrogenPosition position, TextOutline element, TextOutline hydrogen) { var elementBounds = element.GetBounds(); var hydrogenBounds = hydrogen.GetBounds(); switch (position) { case HydrogenPosition.Above: return(hydrogen.Translate(0, (elementBounds.Top - padding) - hydrogenBounds.Bottom)); case HydrogenPosition.Right: return(hydrogen.Translate((elementBounds.Right + padding) - hydrogenBounds.Left, 0)); case HydrogenPosition.Below: return(hydrogen.Translate(0, (elementBounds.Bottom + padding) - hydrogenBounds.Top)); case HydrogenPosition.Left: return(hydrogen.Translate((elementBounds.Left - padding) - hydrogenBounds.Right, 0)); } return(hydrogen); // never reached }
/// <summary> /// Generate an atom symbol for a periodic element with the specified number of hydrogens, ionic /// charge, mass, /// </summary> /// <param name="number">atomic number</param> /// <param name="hydrogens">labelled hydrogen count</param> /// <param name="mass">atomic mass</param> /// <param name="charge">ionic formal charge</param> /// <param name="unpaired">number of unpaired electrons</param> /// <param name="position">placement of hydrogen</param> /// <returns>laid out atom symbol</returns> public AtomSymbol GeneratePeriodicSymbol(int number, int hydrogens, int mass, int charge, int unpaired, HydrogenPosition position) { var element = number == 0 ? new TextOutline("*", font, emSize) : new TextOutline(ChemicalElement.Of(number).Symbol, font, emSize); var hydrogenAdjunct = defaultHydrogenLabel; // the hydrogen count, charge, and mass adjuncts are script size var hydrogenCount = new TextOutline(hydrogens.ToString(), font, emSize).Resize(scriptSize, scriptSize); var chargeAdjunct = new TextOutline(ChargeAdjunctText(charge, unpaired), font, emSize).Resize(scriptSize, scriptSize); var massAdjunct = new TextOutline(mass.ToString(), font, emSize).Resize(scriptSize, scriptSize); // position each adjunct relative to the element label and each other hydrogenAdjunct = PositionHydrogenLabel(position, element, hydrogenAdjunct); hydrogenCount = PositionSubscript(hydrogenAdjunct, hydrogenCount); chargeAdjunct = PositionChargeLabel(hydrogens, position, chargeAdjunct, element, hydrogenAdjunct); massAdjunct = PositionMassLabel(massAdjunct, element); // when the hydrogen label is positioned to the left we may need to nudge it // over to account for the hydrogen count and/or the mass adjunct colliding // with the element label if (position == HydrogenPosition.Left) { var nudgeX = HydrogenXDodge(hydrogens, mass, element, hydrogenAdjunct, hydrogenCount, massAdjunct); hydrogenAdjunct = hydrogenAdjunct.Translate(nudgeX, 0); hydrogenCount = hydrogenCount.Translate(nudgeX, 0); } var adjuncts = new List <TextOutline>(4); if (hydrogens > 0) { adjuncts.Add(hydrogenAdjunct); } if (hydrogens > 1) { adjuncts.Add(hydrogenCount); } if (charge != 0 || unpaired > 0) { adjuncts.Add(chargeAdjunct); } if (mass > 0) { adjuncts.Add(massAdjunct); } return(new AtomSymbol(element, adjuncts)); }
public void PositionOfChargeWhenHydrogensAreRight() { // hydrogen is arbitrarily moved to ensure x/y are different from the element var charge = new TextOutline("+", font, emSize); var localHydrogen = hydrogen.Translate(10, 10); var positioned = atomGenerator.PositionChargeLabel(1, HydrogenPosition.Right, charge, element, localHydrogen); var hydrogenBounds = localHydrogen.GetBounds(); var chargeBounds = positioned.GetBounds(); Assert.IsTrue(chargeBounds.Left > hydrogenBounds.Left); Assert.AreEqual(hydrogenBounds.Top, chargeBounds.CenterY(), 0.01); }
/// <summary> /// Position the charge label on the top right of either the element or hydrogen label. Where the /// charge is placed depends on the number of hydrogens and their position relative to the /// element symbol. /// </summary> /// <param name="hydrogens">number of hydrogen</param> /// <param name="position">position of hydrogen</param> /// <param name="charge">the charge label outline (to be positioned)</param> /// <param name="element">the element label outline</param> /// <param name="hydrogen">the hydrogen label outline</param> /// <returns>positioned charge label</returns> public TextOutline PositionChargeLabel(int hydrogens, HydrogenPosition position, TextOutline charge, TextOutline element, TextOutline hydrogen) { var chargeBounds = charge.GetBounds(); // the charge is placed to the top right of the element symbol // unless either the hydrogen label or the hydrogen count label // are in the way - in which case we place it relative to the // hydrogen var referenceBounds = element.GetBounds(); if (hydrogens > 0 && position == HydrogenPosition.Right) { referenceBounds = hydrogen.GetBounds(); } else if (hydrogens > 1 && position == HydrogenPosition.Above) { referenceBounds = hydrogen.GetBounds(); } return(charge.Translate((referenceBounds.Right + padding) - chargeBounds.Left, (referenceBounds.Top - (chargeBounds.Height / 2)) - chargeBounds.Top)); }
/// <summary> /// Make an embedded text label for display in a CDK renderer. If a piece of text contains newlines /// they are centred aligned below each other with a line height of 1.4. /// </summary> /// <param name="font">the font to embedded</param> /// <param name="text">the text label</param> /// <param name="color">the color</param> /// <param name="scale">the resize, should include the model scale</param> /// <returns>pre-rendered element</returns> public static IRenderingElement EmbedText(Typeface font, double emSize, string text, Color color, double scale) { var lines = text.Split('\n'); var group = new ElementGroup(); double yOffset = 0; double lineHeight = 1.4d; foreach (var line in lines) { var outline = new TextOutline(line, font, emSize).Resize(scale, -scale); var center = outline.GetCenter(); outline = outline.Translate(-center.X, -(center.Y + yOffset)); yOffset += lineHeight * outline.GetBounds().Height; group.Add(GeneralPath.ShapeOf(outline.GetOutline(), color)); var logicalBounds = outline.LogicalBounds; group.Add(new Bounds(logicalBounds.Left, logicalBounds.Top, logicalBounds.Right, logicalBounds.Bottom)); } return(group); }
private IRenderingElement GenerateSgroupBrackets(Sgroup sgroup, IList <SgroupBracket> brackets, IReadOnlyDictionary <IAtom, AtomSymbol> symbols, string subscriptSuffix, string superscriptSuffix) { // brackets are square by default (style:0) var style = (int?)sgroup.GetValue(SgroupKey.CtabBracketStyle); bool round = style != null && style == 1; var result = new ElementGroup(); var atoms = sgroup.Atoms; var crossingBonds = sgroup.Bonds; // easy to depict in correct orientation, we just // point each bracket at the atom of a crossing // bond that is 'in' the group - this scales // to more than two brackets // first we need to pair the brackets with the bonds var pairs = crossingBonds.Count == brackets.Count ? BracketBondPairs(brackets, crossingBonds) : Dictionaries.Empty <SgroupBracket, IBond>(); // override bracket layout around single atoms to bring them in closer if (atoms.Count == 1) { var atom = atoms.First(); // e.g. 2 HCL, 8 H2O etc. if (IsUnsignedInt(subscriptSuffix) && !crossingBonds.Any() && symbols.ContainsKey(atom)) { var prefix = new TextOutline('·' + subscriptSuffix, font, emSize).Resize(1 / scale, 1 / -scale); var prefixBounds = prefix.LogicalBounds; var symbol = symbols[atom]; var bounds = symbol.GetConvexHull().Outline.Bounds; // make slightly large bounds = new Rect(bounds.Bottom - 2 * stroke, bounds.Left - 2 * stroke, bounds.Width + 4 * stroke, bounds.Height + 4 * stroke); prefix = prefix.Translate(bounds.Bottom - prefixBounds.Top, symbol.GetAlignmentCenter().Y - prefixBounds.CenterY()); result.Add(GeneralPath.ShapeOf(prefix.GetOutline(), foreground)); } // e.g. CC(O)nCC else if (crossingBonds.Count > 0) { var scriptscale = labelScale; var leftBracket = new TextOutline("(", font, emSize).Resize(1 / scale, 1 / -scale); var rightBracket = new TextOutline(")", font, emSize).Resize(1 / scale, 1 / -scale); var leftCenter = leftBracket.GetCenter(); var rightCenter = rightBracket.GetCenter(); if (symbols.ContainsKey(atom)) { var symbol = symbols[atom]; var bounds = symbol.GetConvexHull().Outline.Bounds; // make slightly large bounds = new Rect(bounds.Left - 2 * stroke, bounds.Top - 2 * stroke, bounds.Width + 4 * stroke, bounds.Height + 4 * stroke); leftBracket = leftBracket.Translate(bounds.Left - 0.1 - leftCenter.X, symbol.GetAlignmentCenter().Y - leftCenter.Y); rightBracket = rightBracket.Translate(bounds.Right + 0.1 - rightCenter.X, symbol.GetAlignmentCenter().Y - rightCenter.Y); } else { var p = atoms.First().Point2D.Value; leftBracket = leftBracket.Translate(p.X - 0.2 - leftCenter.X, p.Y - leftCenter.Y); rightBracket = rightBracket.Translate(p.X + 0.2 - rightCenter.X, p.Y - rightCenter.Y); } result.Add(GeneralPath.ShapeOf(leftBracket.GetOutline(), foreground)); result.Add(GeneralPath.ShapeOf(rightBracket.GetOutline(), foreground)); var rightBracketBounds = rightBracket.GetBounds(); // subscript/superscript suffix annotation if (subscriptSuffix != null && subscriptSuffix.Any()) { TextOutline subscriptOutline = LeftAlign(MakeText(subscriptSuffix.ToLowerInvariant(), new Vector2(rightBracketBounds.Right, rightBracketBounds.Top - 0.1), new Vector2(-0.5 * rightBracketBounds.Width, 0), scriptscale)); result.Add(GeneralPath.ShapeOf(subscriptOutline.GetOutline(), foreground)); } if (superscriptSuffix != null && superscriptSuffix.Any()) { var superscriptOutline = LeftAlign(MakeText(superscriptSuffix.ToLowerInvariant(), new Vector2(rightBracketBounds.Right, rightBracketBounds.Bottom + 0.1), new Vector2(-rightBracketBounds.Width, 0), scriptscale)); result.Add(GeneralPath.ShapeOf(superscriptOutline.GetOutline(), foreground)); } } } else if (pairs.Any()) { SgroupBracket suffixBracket = null; Vector2? suffixBracketPerp = null; foreach (var e in pairs) { var bracket = e.Key; var bond = e.Value; var inGroupAtom = atoms.Contains(bond.Begin) ? bond.Begin : bond.End; var p1 = bracket.FirstPoint; var p2 = bracket.SecondPoint; var perp = VecmathUtil.NewPerpendicularVector(VecmathUtil.NewUnitVector(p1, p2)); // point the vector at the atom group var midpoint = VecmathUtil.Midpoint(p1, p2); if (Vector2.Dot(perp, VecmathUtil.NewUnitVector(midpoint, inGroupAtom.Point2D.Value)) < 0) { perp = Vector2.Negate(perp); } perp *= bracketDepth; if (round) { result.Add(CreateRoundBracket(p1, p2, perp, midpoint)); } else { result.Add(CreateSquareBracket(p1, p2, perp)); } if (suffixBracket == null) { suffixBracket = bracket; suffixBracketPerp = perp; } else { // is this bracket better as a suffix? var sp1 = suffixBracket.FirstPoint; var sp2 = suffixBracket.SecondPoint; var bestMaxX = Math.Max(sp1.X, sp2.X); var thisMaxX = Math.Max(p1.X, p2.X); var bestMaxY = Math.Max(sp1.Y, sp2.Y); var thisMaxY = Math.Max(p1.Y, p2.Y); // choose the most eastern or.. the most southern var xDiff = thisMaxX - bestMaxX; var yDiff = thisMaxY - bestMaxY; if (xDiff > EQUIV_THRESHOLD || (xDiff > -EQUIV_THRESHOLD && yDiff < -EQUIV_THRESHOLD)) { suffixBracket = bracket; suffixBracketPerp = perp; } } } // write the labels if (suffixBracket != null) { var subSufPnt = suffixBracket.FirstPoint; var supSufPnt = suffixBracket.SecondPoint; // try to put the subscript on the bottom var xDiff = subSufPnt.X - supSufPnt.X; var yDiff = subSufPnt.Y - supSufPnt.Y; if (yDiff > EQUIV_THRESHOLD || (yDiff > -EQUIV_THRESHOLD && xDiff > EQUIV_THRESHOLD)) { var tmpP = subSufPnt; subSufPnt = supSufPnt; supSufPnt = tmpP; } // subscript/superscript suffix annotation if (subscriptSuffix != null && subscriptSuffix.Any()) { var subscriptOutline = LeftAlign(MakeText(subscriptSuffix.ToLowerInvariant(), subSufPnt, suffixBracketPerp.Value, labelScale)); result.Add(GeneralPath.ShapeOf(subscriptOutline.GetOutline(), foreground)); } if (superscriptSuffix != null && superscriptSuffix.Any()) { var superscriptOutline = LeftAlign(MakeText(superscriptSuffix.ToLowerInvariant(), supSufPnt, suffixBracketPerp.Value, labelScale)); result.Add(GeneralPath.ShapeOf(superscriptOutline.GetOutline(), foreground)); } } } else if (brackets.Count == 2) { var b1p1 = brackets[0].FirstPoint; var b1p2 = brackets[0].SecondPoint; var b2p1 = brackets[1].FirstPoint; var b2p2 = brackets[1].SecondPoint; var b1vec = VecmathUtil.NewUnitVector(b1p1, b1p2); var b2vec = VecmathUtil.NewUnitVector(b2p1, b2p2); var b1pvec = VecmathUtil.NewPerpendicularVector(b1vec); var b2pvec = VecmathUtil.NewPerpendicularVector(b2vec); // Point the vectors at each other if (Vector2.Dot(b1pvec, VecmathUtil.NewUnitVector(b1p1, b2p1)) < 0) { b1pvec = Vector2.Negate(b1pvec); } if (Vector2.Dot(b2pvec, VecmathUtil.NewUnitVector(b2p1, b1p1)) < 0) { b2pvec = Vector2.Negate(b2pvec); } // scale perpendicular vectors by how deep the brackets need to be b1pvec *= bracketDepth; b2pvec *= bracketDepth; // bad brackets if (double.IsNaN(b1pvec.X) || double.IsNaN(b1pvec.Y) || double.IsNaN(b2pvec.X) || double.IsNaN(b2pvec.Y)) { return(result); } { var path = new PathGeometry(); if (round) { { // bracket 1 (cp: control point) var pf = new PathFigure { StartPoint = new Point(b1p1.X + b1pvec.X, b1p1.Y + b1pvec.Y) }; Vector2 cpb1 = VecmathUtil.Midpoint(b1p1, b1p2); cpb1 += VecmathUtil.Negate(b1pvec); var seg = new QuadraticBezierSegment { Point1 = new Point(cpb1.X, cpb1.Y), Point2 = new Point(b1p2.X + b1pvec.X, b1p2.Y + b1pvec.Y) }; pf.Segments.Add(seg); path.Figures.Add(pf); } { // bracket 2 (cp: control point) var pf = new PathFigure { StartPoint = new Point(b2p1.X + b2pvec.X, b2p1.Y + b2pvec.Y) }; var cpb2 = VecmathUtil.Midpoint(b2p1, b2p2); cpb2 += VecmathUtil.Negate(b2pvec); var seg = new QuadraticBezierSegment { Point1 = new Point(cpb2.X, cpb2.Y), Point2 = new Point(b2p2.X + b2pvec.X, b2p2.Y + b2pvec.Y) }; pf.Segments.Add(seg); path.Figures.Add(pf); } } else { { // bracket 1 var pf = new PathFigure { StartPoint = new Point(b1p1.X + b1pvec.X, b1p1.Y + b1pvec.Y) }; var seg = new PolyLineSegment(); seg.Points.Add(new Point(b1p1.X, b1p1.Y)); seg.Points.Add(new Point(b1p2.X, b1p2.Y)); seg.Points.Add(new Point(b1p2.X + b1pvec.X, b1p2.Y + b1pvec.Y)); pf.Segments.Add(seg); path.Figures.Add(pf); } { // bracket 2 var pf = new PathFigure { StartPoint = new Point(b2p1.X + b2pvec.X, b2p1.Y + b2pvec.Y) }; var seg = new PolyLineSegment(); seg.Points.Add(new Point(b2p1.X, b2p1.Y)); seg.Points.Add(new Point(b2p2.X, b2p2.Y)); seg.Points.Add(new Point(b2p2.X + b2pvec.X, b2p2.Y + b2pvec.Y)); pf.Segments.Add(seg); path.Figures.Add(pf); } } result.Add(GeneralPath.OutlineOf(path, stroke, foreground)); } // work out where to put the suffix labels (e.g. ht/hh/eu) superscript // and (e.g. n, xl, c, mix) subscript // TODO: could be improved var b1MaxX = Math.Max(b1p1.X, b1p2.X); var b2MaxX = Math.Max(b2p1.X, b2p2.X); var b1MaxY = Math.Max(b1p1.Y, b1p2.Y); var b2MaxY = Math.Max(b2p1.Y, b2p2.Y); var subSufPnt = b2p2; var supSufPnt = b2p1; var subpvec = b2pvec; var bXDiff = b1MaxX - b2MaxX; var bYDiff = b1MaxY - b2MaxY; if (bXDiff > EQUIV_THRESHOLD || (bXDiff > -EQUIV_THRESHOLD && bYDiff < -EQUIV_THRESHOLD)) { subSufPnt = b1p2; supSufPnt = b1p1; subpvec = b1pvec; } var xDiff = subSufPnt.X - supSufPnt.X; var yDiff = subSufPnt.Y - supSufPnt.Y; if (yDiff > EQUIV_THRESHOLD || (yDiff > -EQUIV_THRESHOLD && xDiff > EQUIV_THRESHOLD)) { var tmpP = subSufPnt; subSufPnt = supSufPnt; supSufPnt = tmpP; } // subscript/superscript suffix annotation if (subscriptSuffix != null && subscriptSuffix.Any()) { var subscriptOutline = LeftAlign(MakeText(subscriptSuffix.ToLowerInvariant(), subSufPnt, subpvec, labelScale)); result.Add(GeneralPath.ShapeOf(subscriptOutline.GetOutline(), foreground)); } if (superscriptSuffix != null && superscriptSuffix.Any()) { var superscriptOutline = LeftAlign(MakeText(superscriptSuffix.ToLowerInvariant(), supSufPnt, subpvec, labelScale)); result.Add(GeneralPath.ShapeOf(superscriptOutline.GetOutline(), foreground)); } } return(result); }
public IRenderingElement Generate() { if (!delocalisedDonuts) { return(null); } var group = new ElementGroup(); smallest = Cycles.EdgeShort.Find(mol).ToRingSet(); foreach (var ring in smallest) { if (!CanDelocalise(ring)) { continue; } foreach (var bond in ring.Bonds) { bonds.Add(bond); } int charge = 0; int unpaired = 0; foreach (var atom in ring.Atoms) { var q = atom.FormalCharge ?? 0; if (q == 0) { continue; } int nCyclic = 0; foreach (var bond in mol.GetConnectedBonds(atom)) { if (bond.IsInRing) { nCyclic++; } } if (nCyclic > 2) { continue; } atoms.Add(atom); charge += q; } var p2 = GeometryUtil.Get2DCenter(ring); if (charge != 0) { var qText = charge < 0 ? "–" : "+"; if (charge < -1) { qText = Math.Abs(charge) + qText; } else if (charge > +1) { qText = Math.Abs(charge) + qText; } TextOutline qSym = new TextOutline(qText, font, emSize); qSym = qSym.Resize(1 / scale, -1 / scale); qSym = qSym.Translate(p2.X - qSym.GetCenter().X, p2.Y - qSym.GetCenter().Y); group.Add(GeneralPath.ShapeOf(qSym.GetOutline(), fgColor)); } double s = GeometryUtil.GetBondLengthMedian(ring); double n = ring.Bonds.Count; double r = s / (2 * Math.Tan(Math.PI / n)); group.Add(new OvalElement(new WPF::Point(p2.X, p2.Y), r - 1.5 * dbSpacing, stroke, false, fgColor)); } return(group); }