You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

770 lines
24 KiB
C#

/*******************************************************************************
* Author : Angus Johnson *
* Date : 22 January 2025 *
* Website : https://www.angusj.com *
* Copyright : Angus Johnson 2010-2025 *
* Purpose : Path Offset (Inflate/Shrink) *
* License : https://www.boost.org/LICENSE_1_0.txt *
*******************************************************************************/
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Clipper2Lib
{
public enum JoinType
{
Miter,
Square,
Bevel,
Round
}
public enum EndType
{
Polygon,
Joined,
Butt,
Square,
Round
}
public class ClipperOffset
{
private class Group
{
internal Paths64 inPaths;
internal JoinType joinType;
internal EndType endType;
internal bool pathsReversed;
internal int lowestPathIdx;
public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon)
{
this.joinType = joinType;
this.endType = endType;
bool isJoined = ((endType == EndType.Polygon) || (endType == EndType.Joined));
inPaths = new Paths64(paths.Count);
foreach(Path64 path in paths)
inPaths.Add(Clipper.StripDuplicates(path, isJoined));
if (endType == EndType.Polygon)
{
lowestPathIdx = GetLowestPathIdx(inPaths);
// the lowermost path must be an outer path, so if its orientation is negative,
// then flag that the whole group is 'reversed' (will negate delta etc.)
// as this is much more efficient than reversing every path.
pathsReversed = (lowestPathIdx >= 0) && (Clipper.Area(inPaths[lowestPathIdx]) < 0);
}
else
{
lowestPathIdx = -1;
pathsReversed = false;
}
}
}
private const double Tolerance = 1.0E-12;
// Clipper2 approximates arcs by using series of relatively short straight
//line segments. And logically, shorter line segments will produce better arc
// approximations. But very short segments can degrade performance, usually
// with little or no discernable improvement in curve quality. Very short
// segments can even detract from curve quality, due to the effects of integer
// rounding. Since there isn't an optimal number of line segments for any given
// arc radius (that perfectly balances curve approximation with performance),
// arc tolerance is user defined. Nevertheless, when the user doesn't define
// an arc tolerance (ie leaves alone the 0 default value), the calculated
// default arc tolerance (offset_radius / 500) generally produces good (smooth)
// arc approximations without producing excessively small segment lengths.
// See also: https://www.angusj.com/clipper2/Docs/Trigonometry.htm
const double arc_const = 0.002; // <-- 1/500
private readonly List<Group> _groupList = new List<Group>();
private Path64 pathOut = new Path64();
private readonly PathD _normals = new PathD();
private Paths64 _solution = new Paths64();
private PolyTree64? _solutionTree;
private double _groupDelta; //*0.5 for open paths; *-1.0 for negative areas
private double _delta;
private double _mitLimSqr;
private double _stepsPerRad;
private double _stepSin;
private double _stepCos;
private JoinType _joinType;
private EndType _endType;
public double ArcTolerance { get; set; }
public bool MergeGroups { get; set; }
public double MiterLimit { get; set; }
public bool PreserveCollinear { get; set; }
public bool ReverseSolution { get; set; }
public delegate double DeltaCallback64(Path64 path,
PathD path_norms, int currPt, int prevPt);
public DeltaCallback64? DeltaCallback { get; set; }
#if USINGZ
internal void ZCB(Point64 bot1, Point64 top1,
Point64 bot2, Point64 top2, ref Point64 ip)
{
if (bot1.Z != 0 &&
((bot1.Z == bot2.Z) || (bot1.Z == top2.Z))) ip.Z = bot1.Z;
else if (bot2.Z != 0 && bot2.Z == top1.Z) ip.Z = bot2.Z;
else if (top1.Z != 0 && top1.Z == top2.Z) ip.Z = top1.Z;
else ZCallback?.Invoke(bot1, top1, bot2, top2, ref ip);
}
public ClipperBase.ZCallback64? ZCallback { get; set; }
#endif
public ClipperOffset(double miterLimit = 2.0,
double arcTolerance = 0.0, bool
preserveCollinear = false, bool reverseSolution = false)
{
MiterLimit = miterLimit;
ArcTolerance = arcTolerance;
MergeGroups = true;
PreserveCollinear = preserveCollinear;
ReverseSolution = reverseSolution;
#if USINGZ
ZCallback = null;
#endif
}
public void Clear()
{
_groupList.Clear();
}
public void AddPath(Path64 path, JoinType joinType, EndType endType)
{
int cnt = path.Count;
if (cnt == 0) return;
Paths64 pp = new Paths64(1) { path };
AddPaths(pp, joinType, endType);
}
public void AddPaths(Paths64 paths, JoinType joinType, EndType endType)
{
int cnt = paths.Count;
if (cnt == 0) return;
_groupList.Add(new Group(paths, joinType, endType));
}
private int CalcSolutionCapacity()
{
int result = 0;
foreach (Group g in _groupList)
result += (g.endType == EndType.Joined) ? g.inPaths.Count * 2 : g.inPaths.Count;
return result;
}
internal bool CheckPathsReversed()
{
bool result = false;
foreach (Group g in _groupList)
if (g.endType == EndType.Polygon)
{
result = g.pathsReversed;
break;
}
return result;
}
private void ExecuteInternal(double delta)
{
if (_groupList.Count == 0) return;
_solution.EnsureCapacity(CalcSolutionCapacity());
// make sure the offset delta is significant
if (Math.Abs(delta) < 0.5)
{
foreach (Group group in _groupList)
foreach (Path64 path in group.inPaths)
_solution.Add(path);
return;
}
_delta = delta;
_mitLimSqr = (MiterLimit <= 1 ?
2.0 : 2.0 / Clipper.Sqr(MiterLimit));
foreach (Group group in _groupList)
DoGroupOffset(group);
if (_groupList.Count == 0) return;
bool pathsReversed = CheckPathsReversed();
FillRule fillRule = pathsReversed ? FillRule.Negative : FillRule.Positive;
// clean up self-intersections ...
Clipper64 c = new Clipper64 { PreserveCollinear = PreserveCollinear, // the solution should retain the orientation of the input
ReverseSolution = ReverseSolution != pathsReversed };
#if USINGZ
c.ZCallback = ZCB;
#endif
c.AddSubject(_solution);
if (_solutionTree != null)
c.Execute(ClipType.Union, fillRule, _solutionTree);
else
c.Execute(ClipType.Union, fillRule, _solution);
}
public void Execute(double delta, Paths64 solution)
{
solution.Clear();
_solution = solution;
ExecuteInternal(delta);
}
public void Execute(double delta, PolyTree64 solutionTree)
{
solutionTree.Clear();
_solutionTree = solutionTree;
_solution.Clear();
ExecuteInternal(delta);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static PointD GetUnitNormal(Point64 pt1, Point64 pt2)
{
double dx = (pt2.X - pt1.X);
double dy = (pt2.Y - pt1.Y);
if ((dx == 0) && (dy == 0)) return new PointD();
double f = 1.0 / Math.Sqrt(dx * dx + dy * dy);
dx *= f;
dy *= f;
return new PointD(dy, -dx);
}
public void Execute(DeltaCallback64 deltaCallback, Paths64 solution)
{
DeltaCallback = deltaCallback;
Execute(1.0, solution);
}
internal static int GetLowestPathIdx(Paths64 paths)
{
int result = -1;
Point64 botPt = new Point64(long.MaxValue, long.MinValue);
for (int i = 0; i < paths.Count; ++i)
{
foreach (Point64 pt in paths[i])
{
if ((pt.Y < botPt.Y) ||
((pt.Y == botPt.Y) && (pt.X >= botPt.X))) continue;
result = i;
botPt.X = pt.X;
botPt.Y = pt.Y;
}
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static PointD TranslatePoint(PointD pt, double dx, double dy)
{
#if USINGZ
return new PointD(pt.x + dx, pt.y + dy, pt.z);
#else
return new PointD(pt.x + dx, pt.y + dy);
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static PointD ReflectPoint(PointD pt, PointD pivot)
{
#if USINGZ
return new PointD(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y), pt.z);
#else
return new PointD(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y));
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool AlmostZero(double value, double epsilon = 0.001)
{
return Math.Abs(value) < epsilon;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double Hypotenuse(double x, double y)
{
return Math.Sqrt(Math.Pow(x, 2) + Math.Pow(y, 2));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static PointD NormalizeVector(PointD vec)
{
double h = Hypotenuse(vec.x, vec.y);
if (AlmostZero(h)) return new PointD(0,0);
double inverseHypot = 1 / h;
return new PointD(vec.x* inverseHypot, vec.y* inverseHypot);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static PointD GetAvgUnitVector(PointD vec1, PointD vec2)
{
return NormalizeVector(new PointD(vec1.x + vec2.x, vec1.y + vec2.y));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static PointD IntersectPoint(PointD pt1a, PointD pt1b, PointD pt2a, PointD pt2b)
{
if (InternalClipper.IsAlmostZero(pt1a.x - pt1b.x)) //vertical
{
if (InternalClipper.IsAlmostZero(pt2a.x - pt2b.x)) return new PointD(0, 0);
double m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x);
double b2 = pt2a.y - m2 * pt2a.x;
return new PointD(pt1a.x, m2* pt1a.x + b2);
}
if (InternalClipper.IsAlmostZero(pt2a.x - pt2b.x)) //vertical
{
double m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x);
double b1 = pt1a.y - m1 * pt1a.x;
return new PointD(pt2a.x, m1* pt2a.x + b1);
}
else
{
double m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x);
double b1 = pt1a.y - m1 * pt1a.x;
double m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x);
double b2 = pt2a.y - m2 * pt2a.x;
if (InternalClipper.IsAlmostZero(m1 - m2)) return new PointD(0, 0);
double x = (b2 - b1) / (m1 - m2);
return new PointD(x, m1 * x + b1);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Point64 GetPerpendic(Point64 pt, PointD norm)
{
#if USINGZ
return new Point64(pt.X + norm.x * _groupDelta,
pt.Y + norm.y * _groupDelta, pt.Z);
#else
return new Point64(pt.X + norm.x * _groupDelta,
pt.Y + norm.y * _groupDelta);
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private PointD GetPerpendicD(Point64 pt, PointD norm)
{
#if USINGZ
return new PointD(pt.X + norm.x * _groupDelta,
pt.Y + norm.y * _groupDelta, pt.Z);
#else
return new PointD(pt.X + norm.x * _groupDelta,
pt.Y + norm.y * _groupDelta);
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DoBevel(Path64 path, int j, int k)
{
Point64 pt1, pt2;
if (j == k)
{
double absDelta = Math.Abs(_groupDelta);
#if USINGZ
pt1 = new Point64(
path[j].X - absDelta * _normals[j].x,
path[j].Y - absDelta * _normals[j].y, path[j].Z);
pt2 = new Point64(
path[j].X + absDelta * _normals[j].x,
path[j].Y + absDelta * _normals[j].y, path[j].Z);
#else
pt1 = new Point64(
path[j].X - absDelta * _normals[j].x,
path[j].Y - absDelta * _normals[j].y);
pt2 = new Point64(
path[j].X + absDelta * _normals[j].x,
path[j].Y + absDelta * _normals[j].y);
#endif
}
else
{
#if USINGZ
pt1 = new Point64(
path[j].X + _groupDelta * _normals[k].x,
path[j].Y + _groupDelta * _normals[k].y, path[j].Z);
pt2 = new Point64(
path[j].X + _groupDelta * _normals[j].x,
path[j].Y + _groupDelta * _normals[j].y, path[j].Z);
#else
pt1 = new Point64(
path[j].X + _groupDelta * _normals[k].x,
path[j].Y + _groupDelta * _normals[k].y);
pt2 = new Point64(
path[j].X + _groupDelta * _normals[j].x,
path[j].Y + _groupDelta * _normals[j].y);
#endif
}
pathOut.Add(pt1);
pathOut.Add(pt2);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DoSquare(Path64 path, int j, int k)
{
PointD vec;
if (j == k)
{
vec = new PointD(_normals[j].y, -_normals[j].x);
}
else
{
vec = GetAvgUnitVector(
new PointD(-_normals[k].y, _normals[k].x),
new PointD(_normals[j].y, -_normals[j].x));
}
double absDelta = Math.Abs(_groupDelta);
// now offset the original vertex delta units along unit vector
PointD ptQ = new PointD(path[j]);
ptQ = TranslatePoint(ptQ, absDelta * vec.x, absDelta * vec.y);
// get perpendicular vertices
PointD pt1 = TranslatePoint(ptQ, _groupDelta * vec.y, _groupDelta * -vec.x);
PointD pt2 = TranslatePoint(ptQ, _groupDelta * -vec.y, _groupDelta * vec.x);
// get 2 vertices along one edge offset
PointD pt3 = GetPerpendicD(path[k], _normals[k]);
if (j == k)
{
PointD pt4 = new PointD(
pt3.x + vec.x * _groupDelta,
pt3.y + vec.y * _groupDelta);
PointD pt = IntersectPoint(pt1, pt2, pt3, pt4);
#if USINGZ
pt.z = ptQ.z;
#endif
//get the second intersect point through reflecion
pathOut.Add(new Point64(ReflectPoint(pt, ptQ)));
pathOut.Add(new Point64(pt));
}
else
{
PointD pt4 = GetPerpendicD(path[j], _normals[k]);
PointD pt = IntersectPoint(pt1, pt2, pt3, pt4);
#if USINGZ
pt.z = ptQ.z;
#endif
pathOut.Add(new Point64(pt));
//get the second intersect point through reflecion
pathOut.Add(new Point64(ReflectPoint(pt, ptQ)));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DoMiter(Path64 path, int j, int k, double cosA)
{
double q = _groupDelta / (cosA + 1);
#if USINGZ
pathOut.Add(new Point64(
path[j].X + (_normals[k].x + _normals[j].x) * q,
path[j].Y + (_normals[k].y + _normals[j].y) * q,
path[j].Z));
#else
pathOut.Add(new Point64(
path[j].X + (_normals[k].x + _normals[j].x) * q,
path[j].Y + (_normals[k].y + _normals[j].y) * q));
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DoRound(Path64 path, int j, int k, double angle)
{
if (DeltaCallback != null)
{
// when DeltaCallback is assigned, _groupDelta won't be constant,
// so we'll need to do the following calculations for *every* vertex.
double absDelta = Math.Abs(_groupDelta);
double arcTol = ArcTolerance > 0.01 ? ArcTolerance : absDelta * arc_const;
double stepsPer360 = Math.PI / Math.Acos(1 - arcTol / absDelta);
_stepSin = Math.Sin((2 * Math.PI) / stepsPer360);
_stepCos = Math.Cos((2 * Math.PI) / stepsPer360);
if (_groupDelta < 0.0) _stepSin = -_stepSin;
_stepsPerRad = stepsPer360 / (2 * Math.PI);
}
Point64 pt = path[j];
PointD offsetVec = new PointD(_normals[k].x * _groupDelta, _normals[k].y * _groupDelta);
if (j == k) offsetVec.Negate();
#if USINGZ
pathOut.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y, pt.Z));
#else
pathOut.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y));
#endif
int steps = (int) Math.Ceiling(_stepsPerRad * Math.Abs(angle));
for (int i = 1; i < steps; i++) // ie 1 less than steps
{
offsetVec = new PointD(offsetVec.x * _stepCos - _stepSin * offsetVec.y,
offsetVec.x * _stepSin + offsetVec.y * _stepCos);
#if USINGZ
pathOut.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y, pt.Z));
#else
pathOut.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y));
#endif
}
pathOut.Add(GetPerpendic(pt, _normals[j]));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BuildNormals(Path64 path)
{
int cnt = path.Count;
_normals.Clear();
if (cnt == 0) return;
_normals.EnsureCapacity(cnt);
for (int i = 0; i < cnt - 1; i++)
_normals.Add(GetUnitNormal(path[i], path[i + 1]));
_normals.Add(GetUnitNormal(path[cnt - 1], path[0]));
}
private void OffsetPoint(Group group, Path64 path, int j, ref int k)
{
if (path[j] == path[k]) { k = j; return; }
// Let A = change in angle where edges join
// A == 0: ie no change in angle (flat join)
// A == PI: edges 'spike'
// sin(A) < 0: right turning
// cos(A) < 0: change in angle is more than 90 degree
double sinA = InternalClipper.CrossProduct(_normals[j], _normals[k]);
double cosA = InternalClipper.DotProduct(_normals[j], _normals[k]);
if (sinA > 1.0) sinA = 1.0;
else if (sinA < -1.0) sinA = -1.0;
if (DeltaCallback != null)
{
_groupDelta = DeltaCallback(path, _normals, j, k);
if (group.pathsReversed) _groupDelta = -_groupDelta;
}
if (Math.Abs(_groupDelta) < Tolerance)
{
pathOut.Add(path[j]);
return;
}
if (cosA > -0.999 && (sinA * _groupDelta < 0)) // test for concavity first (#593)
{
// is concave
// by far the simplest way to construct concave joins, especially those joining very
// short segments, is to insert 3 points that produce negative regions. These regions
// will be removed later by the finishing union operation. This is also the best way
// to ensure that path reversals (ie over-shrunk paths) are removed.
pathOut.Add(GetPerpendic(path[j], _normals[k]));
pathOut.Add(path[j]); // (#405, #873, #916)
pathOut.Add(GetPerpendic(path[j], _normals[j]));
}
else if ((cosA > 0.999) && (_joinType != JoinType.Round))
{
// almost straight - less than 2.5 degree (#424, #482, #526 & #724)
DoMiter(path, j, k, cosA);
}
else switch (_joinType)
{
// miter unless the angle is sufficiently acute to exceed ML
case JoinType.Miter when cosA > _mitLimSqr - 1:
DoMiter(path, j, k, cosA);
break;
case JoinType.Miter:
DoSquare(path, j, k);
break;
case JoinType.Round:
DoRound(path, j, k, Math.Atan2(sinA, cosA));
break;
case JoinType.Bevel:
DoBevel(path, j, k);
break;
default:
DoSquare(path, j, k);
break;
}
k = j;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void OffsetPolygon(Group group, Path64 path)
{
pathOut = new Path64();
int cnt = path.Count, prev = cnt - 1;
for (int i = 0; i < cnt; i++)
OffsetPoint(group, path, i, ref prev);
_solution.Add(pathOut);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void OffsetOpenJoined(Group group, Path64 path)
{
OffsetPolygon(group, path);
path = Clipper.ReversePath(path);
BuildNormals(path);
OffsetPolygon(group, path);
}
private void OffsetOpenPath(Group group, Path64 path)
{
pathOut = new Path64();
int highI = path.Count - 1;
if (DeltaCallback != null)
_groupDelta = DeltaCallback(path, _normals, 0, 0);
// do the line start cap
if (Math.Abs(_groupDelta) < Tolerance)
pathOut.Add(path[0]);
else
switch (_endType)
{
case EndType.Butt:
DoBevel(path, 0, 0);
break;
case EndType.Round:
DoRound(path, 0, 0, Math.PI);
break;
default:
DoSquare(path, 0, 0);
break;
}
// offset the left side going forward
for (int i = 1, k = 0; i < highI; i++)
OffsetPoint(group, path, i, ref k);
// reverse normals ...
for (int i = highI; i > 0; i--)
_normals[i] = new PointD(-_normals[i - 1].x, -_normals[i - 1].y);
_normals[0] = _normals[highI];
if (DeltaCallback != null)
_groupDelta = DeltaCallback(path, _normals, highI, highI);
// do the line end cap
if (Math.Abs(_groupDelta) < Tolerance)
pathOut.Add(path[highI]);
else
switch (_endType)
{
case EndType.Butt:
DoBevel(path, highI, highI);
break;
case EndType.Round:
DoRound(path, highI, highI, Math.PI);
break;
default:
DoSquare(path, highI, highI);
break;
}
// offset the left side going back
for (int i = highI -1, k = highI; i > 0; i--)
OffsetPoint(group, path, i, ref k);
_solution.Add(pathOut);
}
private void DoGroupOffset(Group group)
{
if (group.endType == EndType.Polygon)
{
// a straight path (2 points) can now also be 'polygon' offset
// where the ends will be treated as (180 deg.) joins
if (group.lowestPathIdx < 0) _delta = Math.Abs(_delta);
_groupDelta = (group.pathsReversed) ? -_delta : _delta;
}
else
_groupDelta = Math.Abs(_delta);
double absDelta = Math.Abs(_groupDelta);
_joinType = group.joinType;
_endType = group.endType;
if (group.joinType == JoinType.Round || group.endType == EndType.Round)
{
double arcTol = ArcTolerance > 0.01 ? ArcTolerance : absDelta * arc_const;
double stepsPer360 = Math.PI / Math.Acos(1 - arcTol / absDelta);
_stepSin = Math.Sin((2 * Math.PI) / stepsPer360);
_stepCos = Math.Cos((2 * Math.PI) / stepsPer360);
if (_groupDelta < 0.0) _stepSin = -_stepSin;
_stepsPerRad = stepsPer360 / (2 * Math.PI);
}
using List<Path64>.Enumerator pathIt = group.inPaths.GetEnumerator();
while (pathIt.MoveNext())
{
Path64 p = pathIt.Current!;
pathOut = new Path64();
int cnt = p.Count;
switch (cnt)
{
case 1:
{
Point64 pt = p[0];
if (DeltaCallback != null)
{
_groupDelta = DeltaCallback(p, _normals, 0, 0);
if (group.pathsReversed) _groupDelta = -_groupDelta;
absDelta = Math.Abs(_groupDelta);
}
// single vertex so build a circle or square ...
if (group.endType == EndType.Round)
{
int steps = (int) Math.Ceiling(_stepsPerRad * 2 * Math.PI);
pathOut = Clipper.Ellipse(pt, absDelta, absDelta, steps);
#if USINGZ
pathOut = InternalClipper.SetZ(pathOut, pt.Z);
#endif
}
else
{
int d = (int) Math.Ceiling(_groupDelta);
Rect64 r = new Rect64(pt.X - d, pt.Y - d, pt.X + d, pt.Y + d);
pathOut = r.AsPath();
#if USINGZ
pathOut = InternalClipper.SetZ(pathOut, pt.Z);
#endif
}
_solution.Add(pathOut);
continue; // end of offsetting a single point
}
case 2 when group.endType == EndType.Joined:
_endType = (group.joinType == JoinType.Round) ?
EndType.Round :
EndType.Square;
break;
}
BuildNormals(p);
switch (_endType)
{
case EndType.Polygon:
OffsetPolygon(group, p);
break;
case EndType.Joined:
OffsetOpenJoined(group, p);
break;
default:
OffsetOpenPath(group, p);
break;
}
}
}
}
} // namespace