// Determine where each grapheme (essentially character) will go when drawing text along a path. // The algorithm for how text is laid out along a path makes sense, with one MAJOR weirdness. // The basic algorithm is that the width of each character to draw is determined. Starting where the last // character ends, the path is followed for that character width (around corners if needed), and the point along // the path at that point is connected to the start point of the character with a line. The character is drawn along that // baseline, starting at the start point. The end point is used as the start of the next character. If there are bends, // of course, the character won't end right at that end point, but we ignore that. // // THe weirdness is this: instead of measuring distance along the path correctly with the Pythagorian formula, a // strange alternate metric of dx + 1/2 dy is used, where dx is the larger ordinate delta and dy is the smaller ordinate // delta. Thus, text along diagonals is squished together more than it should be. I have no explanation as to why // this might work but it reproduces what OCAD does. See the function "BizzarroDistance" in SymPath. private List<GraphemePlacement> GetLineTextGraphemePlacement(SymPath path, string text) { float totalWidth = 0; List<GraphemePlacement> graphemeList = new List<GraphemePlacement>(); float pathLength = path.BizzarroLength; if (pathLength == 0) return graphemeList; // nothing to draw. // First, determine all the graphemes and their width TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(text); while (enumerator.MoveNext()) { string grapheme = enumerator.GetTextElement(); float graphemeWidth; if (grapheme == " ") graphemeWidth = wordSpacing * spaceWidth; else { float width = MeasureStringWidth(grapheme); graphemeWidth = width + charSpacing * spaceWidth; } graphemeList.Add(new GraphemePlacement(grapheme, graphemeWidth, new PointF(), 0)); totalWidth += graphemeWidth; if (totalWidth + 0.01F >= pathLength && fontAlign != TextSymDefAlignment.Justified) break; // We don't have any room for more characters. (0.01 prevents a very small tail at the end.) } // For OCAD compatibility, truncate right aligned text if too big to fit so the whole // string fits. (Note that left-aligned text will typically show one more character than this.) if (pathLength < totalWidth && fontAlign != TextSymDefAlignment.Left && fontAlign != TextSymDefAlignment.Justified) { totalWidth -= graphemeList[graphemeList.Count - 1].width; if (fontAlign == TextSymDefAlignment.Right) graphemeList.RemoveAt(graphemeList.Count - 1); } // Where does the text begin? float startingDistance = 0; if (fontAlign == TextSymDefAlignment.Left || fontAlign == TextSymDefAlignment.Justified) startingDistance = 0; else if (fontAlign == TextSymDefAlignment.Right) startingDistance = pathLength - totalWidth; else if (fontAlign == TextSymDefAlignment.Center) startingDistance = (pathLength - totalWidth) / 2; // For justified (all-line) text, adjust the widths of each character so they all fit. if (fontAlign == TextSymDefAlignment.Justified && graphemeList.Count > 1) { if (charSpacing > 0) { // last character doesn't have space added. GraphemePlacement graphemePlacement = graphemeList[graphemeList.Count - 1]; graphemePlacement.width -= charSpacing * spaceWidth; totalWidth -= charSpacing * spaceWidth; graphemeList[graphemeList.Count - 1] = graphemePlacement; } float adjustment = (pathLength - totalWidth) / (graphemeList.Count - 1); for (int i = 0; i < graphemeList.Count - 1; ++i) { GraphemePlacement graphemePlacement = graphemeList[i]; graphemePlacement.width += adjustment; graphemeList[i] = graphemePlacement; } } // Find points along the path that are the start/end of each grapheme. PointF[] points = new PointF[graphemeList.Count + 1]; float curDistance = startingDistance; for (int i = 0; i < graphemeList.Count; ++i) { points[i] = path.PointAtLengthBizzarro(curDistance); curDistance += graphemeList[i].width; } points[graphemeList.Count] = path.PointAtLengthBizzarro(Math.Min(curDistance, pathLength)); // Fill in graphemeList with start points and angles for (int i = 0; i < graphemeList.Count; ++i) { GraphemePlacement graphemePlacement = graphemeList[i]; graphemePlacement.pointStart = points[i]; float distX = points[i + 1].X - points[i].X; float distY = points[i + 1].Y - points[i].Y; graphemePlacement.angle = (float) (Math.Atan2(distY, distX) * 360.0 / (Math.PI * 2)); graphemeList[i] = graphemePlacement; } return graphemeList; }