.net与Graphicspath.widen()相反
-
13-10-2019 - |
题
我需要相反的 GraphicsPath.Widen()
.NET中的方法:
public GraphicsPath Widen()
这 Widen()
方法不接受负参数,因此我需要等效于 Inset
方法:
public GraphicsPath Inset()
您可以在开源Inkscape应用程序(www.inkscape.org)中进行此操作,然后选择菜单并选择“路径 /插图”(插图量存储在inkscape属性属性对话框中)。由于Inkscape是开源的,因此应该可以在C#.NET中执行此操作,但是我不能遵循我生命的Inkscape C ++源(我只需要这个功能,所以我无法证明学习C ++是合理的完成此功能)。
基本上,我需要使用此签名的GraphicSpath扩展方法:
public static GraphicsPath Inset(this GraphicsPath original, float amount)
{
//implementation
}
正如签名所述,它将需要 GraphicsPath
对象和 .Inset()
通行的道路……就像今天的Inkscape一样。如果简化了任何事情,则相关图形路径都是从 .PolyBezier
方法(也没有其他),因此除非您想做完整性,否则无需考虑矩,椭圆或任何其他形状。
不幸的是,我没有C ++代码的经验,因此我几乎不可能遵循Inkscape中包含的C ++逻辑。
.
EDIT:]根据要求,这是“ Mackoffset” Inkscape代码。第二个参数(double dec)对于插图为负,该参数的绝对值是要带入形状的数量。
我知道这里有很多依赖关系。如果您需要查看更多inkscape源文件,它们就在这里: http://sourceforge.net/projects/inkscape/files/inkscape/0.48/
int
Shape::MakeOffset (Shape * a, double dec, JoinType join, double miter, bool do_profile, double cx, double cy, double radius, Geom::Matrix *i2doc)
{
Reset (0, 0);
MakeBackData(a->_has_back_data);
bool done_something = false;
if (dec == 0)
{
_pts = a->_pts;
if (numberOfPoints() > maxPt)
{
maxPt = numberOfPoints();
if (_has_points_data) {
pData.resize(maxPt);
_point_data_initialised = false;
_bbox_up_to_date = false;
}
}
_aretes = a->_aretes;
if (numberOfEdges() > maxAr)
{
maxAr = numberOfEdges();
if (_has_edges_data)
eData.resize(maxAr);
if (_has_sweep_src_data)
swsData.resize(maxAr);
if (_has_sweep_dest_data)
swdData.resize(maxAr);
if (_has_raster_data)
swrData.resize(maxAr);
if (_has_back_data)
ebData.resize(maxAr);
}
return 0;
}
if (a->numberOfPoints() <= 1 || a->numberOfEdges() <= 1 || a->type != shape_polygon)
return shape_input_err;
a->SortEdges ();
a->MakeSweepDestData (true);
a->MakeSweepSrcData (true);
for (int i = 0; i < a->numberOfEdges(); i++)
{
// int stP=a->swsData[i].stPt/*,enP=a->swsData[i].enPt*/;
int stB = -1, enB = -1;
if (dec > 0)
{
stB = a->CycleNextAt (a->getEdge(i).st, i);
enB = a->CyclePrevAt (a->getEdge(i).en, i);
}
else
{
stB = a->CyclePrevAt (a->getEdge(i).st, i);
enB = a->CycleNextAt (a->getEdge(i).en, i);
}
Geom::Point stD, seD, enD;
double stL, seL, enL;
stD = a->getEdge(stB).dx;
seD = a->getEdge(i).dx;
enD = a->getEdge(enB).dx;
stL = sqrt (dot(stD,stD));
seL = sqrt (dot(seD,seD));
enL = sqrt (dot(enD,enD));
MiscNormalize (stD);
MiscNormalize (enD);
MiscNormalize (seD);
Geom::Point ptP;
int stNo, enNo;
ptP = a->getPoint(a->getEdge(i).st).x;
double this_dec;
if (do_profile && i2doc) {
double alpha = 1;
double x = (Geom::L2(ptP * (*i2doc) - Geom::Point(cx,cy))/radius);
if (x > 1) {
this_dec = 0;
} else if (x <= 0) {
this_dec = dec;
} else {
this_dec = dec * (0.5 * cos (M_PI * (pow(x, alpha))) + 0.5);
}
} else {
this_dec = dec;
}
if (this_dec != 0)
done_something = true;
int usePathID=-1;
int usePieceID=0;
double useT=0.0;
if ( a->_has_back_data ) {
if ( a->ebData[i].pathID >= 0 && a->ebData[stB].pathID == a->ebData[i].pathID && a->ebData[stB].pieceID == a->ebData[i].pieceID
&& a->ebData[stB].tEn == a->ebData[i].tSt ) {
usePathID=a->ebData[i].pathID;
usePieceID=a->ebData[i].pieceID;
useT=a->ebData[i].tSt;
} else {
usePathID=a->ebData[i].pathID;
usePieceID=0;
useT=0;
}
}
if (dec > 0)
{
Path::DoRightJoin (this, this_dec, join, ptP, stD, seD, miter, stL, seL,
stNo, enNo,usePathID,usePieceID,useT);
a->swsData[i].stPt = enNo;
a->swsData[stB].enPt = stNo;
}
else
{
Path::DoLeftJoin (this, -this_dec, join, ptP, stD, seD, miter, stL, seL,
stNo, enNo,usePathID,usePieceID,useT);
a->swsData[i].stPt = enNo;
a->swsData[stB].enPt = stNo;
}
}
if (dec < 0)
{
for (int i = 0; i < numberOfEdges(); i++)
Inverse (i);
}
if ( _has_back_data ) {
for (int i = 0; i < a->numberOfEdges(); i++)
{
int nEd=AddEdge (a->swsData[i].stPt, a->swsData[i].enPt);
ebData[nEd]=a->ebData[i];
}
} else {
for (int i = 0; i < a->numberOfEdges(); i++)
{
AddEdge (a->swsData[i].stPt, a->swsData[i].enPt);
}
}
a->MakeSweepSrcData (false);
a->MakeSweepDestData (false);
return (done_something? 0 : shape_nothing_to_do);
}
.
编辑
@Simon哀悼者 - 惊人的工作。代码甚至可以干净可读!好,先生。不过,我确实有几个问题。
首先,金额代表的正数是多少?我当时认为对于偏移方法,积极的是“一开始”,而负面是“插图”,但是您的榜样似乎相反。
其次,我进行了一些基本的测试(只是扩展了您的样本),并发现了一些奇怪的测试。
当偏移增长时,“ L”中会发生“ L”(对于如此简单的字母,它肯定喜欢引起问题!)。
...以及复制该代码的代码:
private void Form1_Paint(object sender, PaintEventArgs e)
{
GraphicsPath path = new GraphicsPath();
path.AddString("cool", new FontFamily("Arial"), 0, 200, new PointF(), StringFormat.GenericDefault);
GraphicsPath offset1 = path.Offset(32);
e.Graphics.DrawPath(new Pen(Color.Black, 1), path);
e.Graphics.DrawPath(new Pen(Color.Red, 1), offset1);
}
最后,有些不同。这是翅膀的“ s”角色(看起来像泪落下降):
这是代码:
private void Form1_Paint(object sender, PaintEventArgs e)
{
GraphicsPath path = new GraphicsPath();
path.AddString("S", new FontFamily("Wingdings"), 0, 200, new PointF(), StringFormat.GenericDefault);
GraphicsPath offset1 = path.Offset(20);
e.Graphics.DrawPath(new Pen(Color.Black, 1), path);
e.Graphics.DrawPath(new Pen(Color.Red, 1), offset1);
}
伙计,这是如此近,这让我想哭。不过,它仍然不起作用。
我认为,查看插图矢量何时相交并停止插入该点是什么解决的。如果插图量是如此之大(或小路径如此之小),以至于没有剩下的,则该路径应该消失(无效),而不是自身反转并重新扩展。
同样,我并没有以任何方式敲打您所做的事情,但是我想知道您是否知道这些示例可能发生了什么。
(PS-我添加了“此”关键字以使其成为扩展方法,因此您可能需要使用方法(参数)符号调用代码以使这些示例运行)
.
@ranRAN通过重新使用Graphicspath本地方法来提出类似的输出。伙计,这很艰难。他们两个都很接近。
这是两个示例的屏幕截图,使用wingdings的字符“ S”:
@Simon在左侧, @ran右侧。
这是在Inkscape中执行“插图”后的同样的泪落“ S”角色。插图很干净:
顺便说一句,这是 @ran测试的代码:
private void Form1_Paint(object sender, PaintEventArgs e)
{
GraphicsPath path = new GraphicsPath();
path.AddString("S", new FontFamily("Wingdings"), 0, 200, new PointF(), StringFormat.GenericDefault);
e.Graphics.DrawPath(new Pen(Color.Black, 1), path);
GraphicsPath offset1 = path.Shrink(20);
e.Graphics.DrawPath(new Pen(Color.Red, 1), offset1);
}
解决方案
即使它并不完美,我仍然会发布我的新解决方案,其中一些需要解决的问题列表。也许您将要采取一部分并改进它们,或者也许有一些学习价值。
首先,图片 - 我最好的插图泪珠符号:
我做了什么
我用了
GraphicsPath.Widen
生成给定图的“内”和“外部”边缘。我扫描了结果的点
GraphicsPath
, ,卸下外边缘并仅保留内部边缘。我使用
GraphicsPath.Flatten
因此,这些数字仅由线段组成(无曲线)。然后,我扫描内部路径上的所有点,以及每个当前段:
4.1。如果当前点 p 在原始路径之外,或者离原始路径上的段太近,我在当前边缘上计算一个新点,该点位于所需的距离原始路径的距离上,而我将其视为这一点而不是 p, 并将其连接到我已经扫描的部分。
4.2。解决方案中的当前限制:我从计算的点继续扫描。这意味着对带有孔的形状没有良好的支持(例如Arial“ O”)。为了解决这个问题,必须维护“断开连接”数字的列表,并重新连接在同一点结束的数字(或彼此“足够接近”的结尾)。
问题
首先,我将指定最大的问题和局限性,然后将代码本身发布。
看起来
GraphicsPath.Widen
不会产生干净的形状。我得到的内在人物具有很小的(但主要是看不见的)“锯齿状”。这样做的意义是a)我的剔除算法会产生更多的噪音,b)该图具有更多的点,因此性能降低。目前,表现几乎是可以接受的,如果有的话。我目前以非常幼稚的方式扫描的解决方案(在 o(n^n))找到内边缘上候选点“太近”的线段。这会导致算法非常慢。可以通过维护某些数据结构来改进,其中段是由 X, ,使距离计算的数量大大减少。
我没有费心优化要使用的代码
structs
而且还有许多其他地方可以优化代码,以更快得多。没有孔形状的支撑,内部图必须“拆分”成几个图(例如arial“ o”)。我知道如何实施它,但是它需要更多的时间:)
我将考虑适应Simon的移动现有点以获取内部数字的方法,并以我清理该路径的方法。 (但是,由于Simon的解决方案中的错误,我目前无法做到这一点,例如,这会导致撕裂符号的尖端移动到形状内部的有效位置。我的算法认为此位置是有效的,并且不清理它)。
编码
我无法避免提出自己的一些数学/几何实用程序。所以这是代码...
就个人而言,我认为这可能是值得的,尽管它不是一个完美的解决方案... :)
public class LineSegment
{
private readonly LineEquation line;
private RectangleF bindingRectangle;
public PointF A { get; private set; }
public PointF B { get; private set; }
public LineSegment(PointF a, PointF b)
{
A = a;
B = b;
line = new LineEquation(a, b);
bindingRectangle = new RectangleF(
Math.Min(a.X, b.X), Math.Min(a.Y, b.Y),
Math.Abs(a.X - b.X), Math.Abs(a.Y - b.Y));
}
public PointF? Intersect(LineSegment other)
{
var p = line.Intersect(other.line);
if (p == null) return null;
if (bindingRectangle.Contains(p.Value) &&
other.bindingRectangle.Contains(p.Value))
{
return p;
}
return null;
}
public float Distance(PointF p)
{
if (LineEquation.IsBetween(line.GetNormalAt(A), p, line.GetNormalAt(B)))
{
return line.Distance(p);
}
return Math.Min(Distance(A, p), Distance(B, p));
}
static float Distance(PointF p1, PointF p2)
{
var x = p1.X - p2.X;
var y = p1.Y - p2.Y;
return (float) Math.Sqrt(x*x + y*y);
}
public PointF? IntersectAtDistance(LineSegment segmentToCut, float width)
{
// always assuming other.A is the farthest end
var distance = width* (line.IsAboveOrRightOf(segmentToCut.A) ? 1 : -1);
var parallelLine = line.GetParallelLine(distance);
var p = parallelLine.Intersect(segmentToCut.line);
if (p.HasValue)
{
if (LineEquation.IsBetween(line.GetNormalAt(A), p.Value, line.GetNormalAt(B)) &&
segmentToCut.bindingRectangle.Contains(p.Value))
{
return p;
}
}
List<PointF> points = new List<PointF>();
points.AddRange(segmentToCut.line.Intersect(new CircleEquation(width, A)));
points.AddRange(segmentToCut.line.Intersect(new CircleEquation(width, B)));
return GetNearestPoint(segmentToCut.A, points);
}
public static PointF GetNearestPoint(PointF p, IEnumerable<PointF> points)
{
float minDistance = float.MaxValue;
PointF nearestPoint = p;
foreach (var point in points)
{
var d = Distance(p, point);
if (d < minDistance)
{
minDistance = d;
nearestPoint = point;
}
}
return nearestPoint;
}
}
public class LineEquation
{
private readonly float a;
private readonly float b;
private readonly bool isVertical;
private readonly float xConstForVertical;
public LineEquation(float a, float b)
{
this.a = a;
this.b = b;
isVertical = false;
}
public LineEquation(float xConstant)
{
isVertical = true;
xConstForVertical = xConstant;
}
public LineEquation(float a, PointF p)
{
this.a = a;
b = p.Y - a*p.X;
isVertical = false;
}
public LineEquation(PointF p1, PointF p2)
{
if (p1.X == p2.X)
{
isVertical = true;
xConstForVertical = p1.X;
return;
}
a = (p1.Y - p2.Y)/(p1.X - p2.X);
b = p1.Y - a * p1.X;
isVertical = false;
}
public PointF? Intersect(float x)
{
if (isVertical)
{
return null;
}
return new PointF(x, a*x + b);
}
public PointF? Intersect(LineEquation other)
{
if (isVertical && other.isVertical) return null;
if (a == other.a) return null;
if (isVertical) return other.Intersect(xConstForVertical);
if (other.isVertical) return Intersect(other.xConstForVertical);
// both have slopes and are not parallel
var x = (b - other.b) / (other.a - a);
return Intersect(x);
}
public float Distance(PointF p)
{
if (isVertical)
{
return Math.Abs(p.X - xConstForVertical);
}
var p1 = Intersect(0).Value;
var p2 = Intersect(100).Value;
var x1 = p.X - p1.X;
var y1 = p.Y - p1.Y;
var x2 = p2.X - p1.X;
var y2 = p2.Y - p1.Y;
return (float) (Math.Abs(x1*y2 - x2*y1) / Math.Sqrt(x2*x2 + y2*y2));
}
public bool IsAboveOrRightOf(PointF p)
{
return isVertical ?
xConstForVertical > p.X :
a*p.X + b > p.Y;
}
public static bool IsBetween(LineEquation l1, PointF p, LineEquation l2)
{
return l1.IsAboveOrRightOf(p) ^ l2.IsAboveOrRightOf(p);
}
public LineEquation GetParallelLine(float distance)
{
if (isVertical) return new LineEquation(xConstForVertical + distance);
var angle = Math.Atan(a);
float dy = (float) (distance/Math.Sin(angle));
return new LineEquation(a, b - dy);
}
public LineEquation GetNormalAt(PointF p)
{
if (isVertical) return new LineEquation(p.X);
var newA = -1/a;
var newB = (a + 1/a)*p.X + b;
return new LineEquation(newA, newB);
}
public PointF[] Intersect(CircleEquation circle)
{
var cx = circle.Center.X;
var cy = circle.Center.Y;
var r = circle.Radius;
if (isVertical)
{
var distance = Math.Abs(cx - xConstForVertical);
if (distance > r) return new PointF[0];
if (distance == r) return new[] {new PointF(xConstForVertical, cy) };
// two intersections
var dx = cx - xConstForVertical;
var qe = new QuadraticEquation(
1,
-2 * cy,
r * r - dx * dx);
return qe.Solve();
}
var t = b - cy;
var q = new QuadraticEquation(
1 + a*a,
2*a*t - 2*cx,
cx*cx + t*t - r*r);
var solutions = q.Solve();
for (var i = 0; i < solutions.Length; i++)
solutions[i] = Intersect(solutions[i].X).Value;
return solutions;
}
}
public class CircleEquation
{
public float Radius { get; private set; }
public PointF Center { get; private set; }
public CircleEquation(float radius, PointF center)
{
Radius = radius;
Center = center;
}
}
public class QuadraticEquation
{
public float A { get; private set; }
public float B { get; private set; }
public float C { get; private set; }
public QuadraticEquation(float a, float b, float c)
{
A = a;
B = b;
C = c;
}
public PointF Intersect(float x)
{
return new PointF(x, A*x*x + B*x + C);
}
public PointF[] Solve()
{
var d = B*B - 4*A*C;
if (d < 0) return new PointF[0];
if (d == 0)
{
var x = -B / (2*A);
return new[] { Intersect(x) };
}
var sd = Math.Sqrt(d);
var x1 = (float) ((-B - sd) / (2f*A));
var x2 = (float) ((-B + sd) / (2*A));
return new[] { Intersect(x1), Intersect(x2) };
}
}
public static class GraphicsPathExtension
{
public static GraphicsPath Shrink(this GraphicsPath originalPath, float width)
{
originalPath.CloseAllFigures();
originalPath.Flatten();
var parts = originalPath.SplitFigures();
var shrunkPaths = new List<GraphicsPath>();
foreach (var part in parts)
{
using (var widePath = new GraphicsPath(part.PathPoints, part.PathTypes))
{
// widen the figure
widePath.Widen(new Pen(Color.Black, width * 2));
// pick the inner edge
var innerEdge = widePath.SplitFigures()[1];
var fixedPath = CleanPath(innerEdge, part, width);
if (fixedPath.PointCount > 0)
shrunkPaths.Add(fixedPath);
}
}
// build the result
originalPath.Reset();
foreach (var p in shrunkPaths)
{
originalPath.AddPath(p, false);
}
return originalPath;
}
public static IList<GraphicsPath> SplitFigures(this GraphicsPath path)
{
var paths = new List<GraphicsPath>();
var position = 0;
while (position < path.PointCount)
{
var figureCount = CountNextFigure(path.PathData, position);
var points = new PointF[figureCount];
var types = new byte[figureCount];
Array.Copy(path.PathPoints, position, points, 0, figureCount);
Array.Copy(path.PathTypes, position, types, 0, figureCount);
position += figureCount;
paths.Add(new GraphicsPath(points, types));
}
return paths;
}
static int CountNextFigure(PathData data, int position)
{
var count = 0;
for (var i = position; i < data.Types.Length; i++)
{
count++;
if (0 != (data.Types[i] & (int)PathPointType.CloseSubpath))
{
return count;
}
}
return count;
}
static GraphicsPath CleanPath(GraphicsPath innerPath, GraphicsPath originalPath, float width)
{
var points = new List<PointF>();
Region originalRegion = new Region(originalPath);
// find first valid point
int firstValidPoint = 0;
IEnumerable<LineSegment> segs;
while (IsPointTooClose(
innerPath.PathPoints[firstValidPoint],
originalPath, originalRegion, width, out segs))
{
firstValidPoint++;
if (firstValidPoint == innerPath.PointCount) return new GraphicsPath();
}
var prevP = innerPath.PathPoints[firstValidPoint];
points.Add(prevP);
for (int i = 1; i < innerPath.PointCount; i++)
{
var p = innerPath.PathPoints[(firstValidPoint + i) % innerPath.PointCount];
if (!IsPointTooClose(p, originalPath, originalRegion, width, out segs))
{
prevP = p;
points.Add(p);
continue;
}
var invalidSegment = new LineSegment(prevP, p);
// found invalid point (too close or external to original figure)
IEnumerable<PointF> cutPoints =
segs.Select(seg => seg.IntersectAtDistance(invalidSegment, width).Value);
var cutPoint = LineSegment.GetNearestPoint(prevP, cutPoints);
// now add the cutPoint instead of 'p'.
points.Add(cutPoint);
prevP = cutPoint;
}
var types = new List<byte>();
for (int i = 0; i < points.Count - 1; i++)
{
types.Add(1);
}
types.Add(129);
return points.Count == 0 ?
new GraphicsPath() :
new GraphicsPath(points.ToArray(), types.ToArray());
}
static bool IsPointTooClose(
PointF p, GraphicsPath path, Region region,
float distance, out IEnumerable<LineSegment> breakingSegments)
{
if (!region.IsVisible(p))
{
breakingSegments = new LineSegment[0];
return true;
}
var segs = new List<LineSegment>();
foreach (var seg in GetSegments(path))
{
if (seg.Distance(p) < distance)
{
segs.Add(seg);
}
}
breakingSegments = segs;
return segs.Count > 0;
}
static public IEnumerable<LineSegment> GetSegments(GraphicsPath path)
{
for (var i = 0; i < path.PointCount; i++)
{
yield return
new LineSegment(path.PathPoints[i], path.PathPoints[(i + 1) % path.PointCount]);
}
}
}
其他提示
这是一个不错的选择。它不像 @Simon的那样复杂,但是通过更简单的代码,它可以提供不错的结果(可以进一步改进)。
这个想法是重复使用现有的功能 GraphicsPath.Widen
为了获得积分。
当我们打电话时 Widen
在 GraphicsPath
由此组成 n 封闭的数字,结果路径具有 2n 边缘。每个原始图的外部和内部边缘。
因此,我创建了一个临时路径,扩大它,并仅复制内部边缘。
这是代码:
public static GraphicsPath Shrink(this GraphicsPath path, float width)
{
using (var p = new GraphicsPath())
{
p.AddPath(path, false);
p.CloseAllFigures();
p.Widen(new Pen(Color.Black, width*2));
var position = 0;
var result = new GraphicsPath();
while (position < p.PointCount)
{
// skip outer edge
position += CountNextFigure(p.PathData, position);
// count inner edge
var figureCount = CountNextFigure(p.PathData, position);
var points = new PointF[figureCount];
var types = new byte[figureCount];
Array.Copy(p.PathPoints, position, points, 0, figureCount);
Array.Copy(p.PathTypes, position, types, 0, figureCount);
position += figureCount;
result.AddPath(new GraphicsPath(points, types), false);
}
path.Reset();
path.AddPath(result, false);
return path;
}
}
static int CountNextFigure(PathData data, int position)
{
int count = 0;
for (var i = position; i < data.Types.Length; i++)
{
count++;
if (0 != (data.Types[i] & (int) PathPointType.CloseSubpath))
{
return count;
}
}
return count;
}
这是一个例子:
GraphicsPath path = new GraphicsPath();
path.AddString("cool", new FontFamily("Times New Roman"), 0, 300,
new PointF(), StringFormat.GenericDefault);
e.Graphics.DrawPath(new Pen(Color.Black, 1), path);
path.Shrink(3);
e.Graphics.DrawPath(new Pen(Color.Red), path);
诚然,当偏移足够大以使形状与自身相交时,我的解决方案也具有不希望的人工制品。
编辑:
我可以轻松地检测O中的所有相交点(n^2),或者付出一些努力 - 在o中检测到它们(n logn),使用扫描线算法(n 是点数)。
但是,一旦找到了交叉点,我就不确定如何确定要删除路径的哪些部分。有人有主意吗? :)
编辑2:
实际上,我们真的不需要找到这些数字的交集。
我们能做的就是扫描图上的所有点。一旦我们找到了一个要么在原始图之外的点,要么离原始图上的边缘太近,就必须修复它。
为了固定一个点,我们查看了这一点和上一个点之间的边缘,我们必须切割这个边缘,以便现在以新的点结束,距原始图的正确距离。
我已经使用了该算法进行了一些实验(带有一种粗糙但简单的算法,我完全删除了“关闭”点,而不是移动它们以缩短其边缘,我检查了原始图的距离,而不是TO TO边缘)。这获得了删除大多数不需要的文物的好结果。
实施完整的解决方案可能需要几个小时...
编辑3:
尽管我仍然远非完美,但我在单独的答案中发布了改进的解决方案。
这是一个似乎有效的代码。它支持封闭和开放的数字(这是困难的部分...),正面和负面偏移。
基本上,在路径中的每个点,它计算一个偏移点。偏移点是使用正常向量确定的,但实际上,它是使用两个偏移线(等效)的相交进行计算的。在某些情况下,它不会很好地显示(如果路径块太近,比偏移更接近)。
请注意,它没有组合/合并以相交的数字,但这是另一个故事。理论文章可以在这里找到: 用于多线曲线的偏移算法.
您可以在此示例中尝试一下:
protected override void OnPaint(PaintEventArgs e)
{
GraphicsPath path = new GraphicsPath();
path.AddString("cool", new FontFamily("Arial"), 0, 200, new PointF(), StringFormat.GenericDefault);
path.AddEllipse(150, 50, 80, 80);
path.AddEllipse(150 + 100, 50 + 100, 80 + 100, 80 + 100);
GraphicsPath offset1 = Offset(path, -5);
GraphicsPath offset2 = Offset(path, 5);
e.Graphics.DrawPath(new Pen(Color.Black, 1), path);
e.Graphics.DrawPath(new Pen(Color.Red, 1), offset1);
e.Graphics.DrawPath(new Pen(Color.Blue, 1), offset2);
}
完整的代码:
public static GraphicsPath Offset(GraphicsPath path, float offset)
{
if (path == null)
throw new ArgumentNullException("path");
// death from natural causes
if (path.PointCount < 2)
throw new ArgumentException(null, "path");
PointF[] points = new PointF[path.PointCount];
for (int i = 0; i < path.PointCount; i++)
{
PointF current = path.PathPoints[i];
PointF prev = GetPreviousPoint(path, i);
PointF next = GetNextPoint(path, i);
PointF offsetPoint = Offset(prev, current, next, offset);
points[i] = offsetPoint;
}
GraphicsPath newPath = new GraphicsPath(points, path.PathTypes);
return newPath;
}
// get the closing point for a figure or null if none was found
private static PointF? GetClosingPoint(GraphicsPath path, ref int index)
{
for (int i = index + 1; i < path.PointCount; i++)
{
if (IsClosingPoint(path, i))
{
index = i;
return path.PathPoints[i];
}
}
return null;
}
// get the starting point for a figure or null if none was found
private static PointF? GetStartingPoint(GraphicsPath path, ref int index)
{
for (int i = index - 1; i >= 0; i--)
{
if (IsStartingPoint(path, i))
{
index = i;
return path.PathPoints[i];
}
}
return null;
}
// get a previous point to compute normal vector at specified index
private static PointF GetPreviousPoint(GraphicsPath path, int index)
{
if (IsStartingPoint(path, index))
{
int closingIndex = index;
PointF? closing = GetClosingPoint(path, index, ref closingIndex);
if (closing.HasValue)
{
if (closing.Value != path.PathPoints[index])
return closing.Value;
return GetPreviousPoint(path, closingIndex);
}
}
else
{
return path.PathPoints[index - 1];
}
// we are on an unclosed end point, emulate a prev point on the same line using next point
PointF point = path.PathPoints[index];
PointF next = path.PathPoints[index + 1];
return VectorF.Add(point, VectorF.Substract(point, next));
}
// get a next point to compute normal vector at specified index
private static PointF GetNextPoint(GraphicsPath path, int index)
{
if (IsClosingPoint(path, index))
{
int startingIndex = index;
PointF? starting = GetStartingPoint(path, ref startingIndex);
if (starting.HasValue)
{
// some figures (Ellipse) are closed with the same point as the starting point
// in this case, we need the starting point's next point
if (starting.Value != path.PathPoints[index])
return starting.Value;
return GetNextPoint(path, startingIndex);
}
}
else if ((index != (path.PointCount - 1)) && (!IsStartingPoint(path, index + 1)))
{
return path.PathPoints[index + 1];
}
// we are on an unclosed end point, emulate a next point on the same line using previous point
PointF point = path.PathPoints[index];
PointF prev = path.PathPoints[index - 1];
return VectorF.Add(point, VectorF.Substract(point, prev));
}
// determine if a point is a closing point
private static bool IsClosingPoint(GraphicsPath path, int index)
{
return (path.PathTypes[index] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath;
}
// determine if a point is a starting point
private static bool IsStartingPoint(GraphicsPath path, int index)
{
return (path.PathTypes[index] == (byte)PathPointType.Start);
}
// offsets a Point using the normal vector (actually computed using intersection or 90° rotated vectors)
private static PointF Offset(PointF prev, PointF current, PointF next, float offset)
{
VectorF vnext = VectorF.Substract(next, current);
vnext = vnext.DegreeRotate(Math.Sign(offset) * 90);
vnext = vnext.Normalize() * Math.Abs(offset);
PointF pnext1 = current + vnext;
PointF pnext2 = next + vnext;
VectorF vprev = VectorF.Substract(prev, current);
vprev = vprev.DegreeRotate(-Math.Sign(offset) * 90);
vprev = vprev.Normalize() * Math.Abs(offset);
PointF pprev1 = current + vprev;
PointF pprev2 = prev + vprev;
PointF ix = VectorF.GetIntersection(pnext1, pnext2, pprev1, pprev2);
if (ix.IsEmpty)
{
// 3 points on the same line, just translate (both vectors are identical)
ix = current + vnext;
}
return ix;
}
// a useful Vector class (does not exists in GDI+, why?)
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct VectorF : IFormattable, IEquatable<VectorF>
{
private float _x;
private float _y;
public VectorF(float x, float y)
{
_x = x;
_y = y;
}
public float X
{
get
{
return _x;
}
set
{
_x = value;
}
}
public float Y
{
get
{
return _y;
}
set
{
_y = value;
}
}
public double Length
{
get
{
return Math.Sqrt(_x * _x + _y * _y);
}
}
public VectorF Rotate(double angle)
{
float cos = (float)Math.Cos(angle);
float sin = (float)Math.Sin(angle);
return new VectorF(_x * cos - _y * sin, _x * sin + _y * cos);
}
public VectorF DegreeRotate(double angle)
{
return Rotate(DegreeToGradiant(angle));
}
public static PointF GetIntersection(PointF start1, PointF end1, PointF start2, PointF end2)
{
float denominator = ((end1.X - start1.X) * (end2.Y - start2.Y)) - ((end1.Y - start1.Y) * (end2.X - start2.X));
if (denominator == 0) // parallel
return PointF.Empty;
float numerator = ((start1.Y - start2.Y) * (end2.X - start2.X)) - ((start1.X - start2.X) * (end2.Y - start2.Y));
float r = numerator / denominator;
PointF result = new PointF();
result.X = start1.X + (r * (end1.X - start1.X));
result.Y = start1.Y + (r * (end1.Y - start1.Y));
return result;
}
public static PointF Add(PointF point, VectorF vector)
{
return new PointF(point.X + vector._x, point.Y + vector._y);
}
public static VectorF Add(VectorF vector1, VectorF vector2)
{
return new VectorF(vector1._x + vector2._x, vector1._y + vector2._y);
}
public static VectorF Divide(VectorF vector, float scalar)
{
return vector * (1.0f / scalar);
}
public static VectorF Multiply(float scalar, VectorF vector)
{
return new VectorF(vector._x * scalar, vector._y * scalar);
}
public static VectorF Multiply(VectorF vector, float scalar)
{
return Multiply(scalar, vector);
}
public static VectorF operator *(float scalar, VectorF vector)
{
return Multiply(scalar, vector);
}
public static VectorF operator *(VectorF vector, float scalar)
{
return Multiply(scalar, vector);
}
public static PointF operator -(PointF point, VectorF vector)
{
return Substract(point, vector);
}
public static PointF operator +(VectorF vector, PointF point)
{
return Add(point, vector);
}
public static PointF operator +(PointF point, VectorF vector)
{
return Add(point, vector);
}
public static VectorF operator +(VectorF vector1, VectorF vector2)
{
return Add(vector1, vector2);
}
public static VectorF operator /(VectorF vector, float scalar)
{
return Divide(vector, scalar);
}
public static VectorF Substract(PointF point1, PointF point2)
{
return new VectorF(point1.X - point2.X, point1.Y - point2.Y);
}
public static PointF Substract(PointF point, VectorF vector)
{
return new PointF(point.X - vector._x, point.Y - vector._y);
}
public static double AngleBetween(VectorF vector1, VectorF vector2)
{
double y = (vector1._x * vector2._y) - (vector2._x * vector1._y);
double x = (vector1._x * vector2._x) + (vector1._y * vector2._y);
return Math.Atan2(y, x);
}
private static double GradiantToDegree(double angle)
{
return (angle * 180) / Math.PI;
}
private static double DegreeToGradiant(double angle)
{
return (angle * Math.PI) / 180;
}
public static double DegreeAngleBetween(VectorF vector1, VectorF vector2)
{
return GradiantToDegree(AngleBetween(vector1, vector2));
}
public VectorF Normalize()
{
if (Length == 0)
return this;
VectorF vector = this / (float)Length;
return vector;
}
public override string ToString()
{
return ToString(null, null);
}
public string ToString(string format, IFormatProvider provider)
{
return string.Format(provider, "{0:" + format + "};{1:" + format + "}", _x, _y);
}
public override int GetHashCode()
{
return _x.GetHashCode() ^ _y.GetHashCode();
}
public override bool Equals(object obj)
{
if ((obj == null) || !(obj is VectorF))
return false;
return Equals(this, (VectorF)obj);
}
public bool Equals(VectorF value)
{
return Equals(this, value);
}
public static bool Equals(VectorF vector1, VectorF vector2)
{
return (vector1._x.Equals(vector2._x) && vector1._y.Equals(vector2._y));
}
}
好的,我想我为你们有领先地位...但是它朝着完全不同的方向。
无论如何,我意识到一条较大路径的“子路”实际上在一个过程中缩小(插图) .Widen
操作,所以我决定看看是否有任何富有成果的道路(没有双关语)。
真的,这里的想法是 .Widen
路径...从外面!
如果我们拿了原始的 GraphicsPath
然后将其“包裹”在更大的 Rectangle
(做一个 Inflate
10 .GetBounds
的 GraphicsPath
应该给我们一个简单的包装器)。
然后,首先添加包装器,然后 GraphicsPath
是添加的作为子路径。整个事情都得到了 .Widen
, ,最后是一个新的 GraphicsPath
是从头开始创建的,使用 .PathPoints
和 .PathTypes
宽阔的路径,它消除了无用的包装器(幸运的是, GraphicsPath
接受 PathPoints
和 PathTypes
在其中一个构造函数过载中)。
我将在一天余下的时间里离开办公室,所以我看不到这一点,但是这是领导者。
只需将此代码放入常规形式:
private void Form1_Paint(object sender, PaintEventArgs e)
{
GraphicsPath g = new GraphicsPath();
g.AddRectangle(new Rectangle(0, 0, 200, 200));
g.AddEllipse(50, 50, 100, 100);
//Original path
e.Graphics.DrawPath(new Pen(Color.Black,2), g);
//"Inset" path
g.Widen(new Pen(Color.Black, 10));
e.Graphics.DrawPath(new Pen(Color.Red, 2), g);
}
从这个简单的实验中,您将看到目标路径(圆圈)现在具有难以捉摸的插图(红色)!
还有其他我在那里没有真正了解的废话(矩形包装器上也出现),但是从 PathPoints
和 PathTypes
, ,当创建Virgin GraphicsPath时,应该有可能迭代数组并删除垃圾(或者找出垃圾来自何处并防止其发生)。然后退回新的干净 GraphicsPath
.
该技术避免了所有复杂的数学,但它有点远。