@mlightcad/geometry-engine 3.2.38 → 3.2.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/geometry-engine.umd.cjs +1 -1
- package/lib/geometry/AcGeCircArc3d.d.ts +7 -0
- package/lib/geometry/AcGeCircArc3d.d.ts.map +1 -1
- package/lib/geometry/AcGeCircArc3d.js +12 -0
- package/lib/geometry/AcGeCircArc3d.js.map +1 -1
- package/lib/geometry/AcGeEllipseArc3d.d.ts +7 -0
- package/lib/geometry/AcGeEllipseArc3d.d.ts.map +1 -1
- package/lib/geometry/AcGeEllipseArc3d.js +13 -0
- package/lib/geometry/AcGeEllipseArc3d.js.map +1 -1
- package/lib/geometry/AcGeLine3d.d.ts +7 -0
- package/lib/geometry/AcGeLine3d.d.ts.map +1 -1
- package/lib/geometry/AcGeLine3d.js +16 -0
- package/lib/geometry/AcGeLine3d.js.map +1 -1
- package/lib/geometry/AcGeNurbsCurve.d.ts +55 -1
- package/lib/geometry/AcGeNurbsCurve.d.ts.map +1 -1
- package/lib/geometry/AcGeNurbsCurve.js +146 -4
- package/lib/geometry/AcGeNurbsCurve.js.map +1 -1
- package/lib/geometry/AcGePolyline2d.d.ts +7 -0
- package/lib/geometry/AcGePolyline2d.d.ts.map +1 -1
- package/lib/geometry/AcGePolyline2d.js +10 -0
- package/lib/geometry/AcGePolyline2d.js.map +1 -1
- package/lib/geometry/AcGePolyline2dOffset.d.ts +28 -0
- package/lib/geometry/AcGePolyline2dOffset.d.ts.map +1 -0
- package/lib/geometry/AcGePolyline2dOffset.js +886 -0
- package/lib/geometry/AcGePolyline2dOffset.js.map +1 -0
- package/lib/geometry/AcGeSpline3d.d.ts +9 -1
- package/lib/geometry/AcGeSpline3d.d.ts.map +1 -1
- package/lib/geometry/AcGeSpline3d.js +7 -0
- package/lib/geometry/AcGeSpline3d.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/util/AcGeCurveOffsetUtil.d.ts +22 -0
- package/lib/util/AcGeCurveOffsetUtil.d.ts.map +1 -0
- package/lib/util/AcGeCurveOffsetUtil.js +37 -0
- package/lib/util/AcGeCurveOffsetUtil.js.map +1 -0
- package/lib/util/AcGeNurbsUtil.d.ts +13 -0
- package/lib/util/AcGeNurbsUtil.d.ts.map +1 -1
- package/lib/util/AcGeNurbsUtil.js +105 -0
- package/lib/util/AcGeNurbsUtil.js.map +1 -1
- package/lib/util/AcGeSampledCurveOffsetUtil.d.ts +21 -0
- package/lib/util/AcGeSampledCurveOffsetUtil.d.ts.map +1 -0
- package/lib/util/AcGeSampledCurveOffsetUtil.js +302 -0
- package/lib/util/AcGeSampledCurveOffsetUtil.js.map +1 -0
- package/lib/util/AcGeTol.d.ts +8 -0
- package/lib/util/AcGeTol.d.ts.map +1 -1
- package/lib/util/AcGeTol.js +14 -0
- package/lib/util/AcGeTol.js.map +1 -1
- package/lib/util/index.d.ts +3 -1
- package/lib/util/index.d.ts.map +1 -1
- package/lib/util/index.js +3 -1
- package/lib/util/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
var __assign = (this && this.__assign) || function () {
|
|
2
|
+
__assign = Object.assign || function(t) {
|
|
3
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
4
|
+
s = arguments[i];
|
|
5
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
6
|
+
t[p] = s[p];
|
|
7
|
+
}
|
|
8
|
+
return t;
|
|
9
|
+
};
|
|
10
|
+
return __assign.apply(this, arguments);
|
|
11
|
+
};
|
|
12
|
+
var __values = (this && this.__values) || function(o) {
|
|
13
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
14
|
+
if (m) return m.call(o);
|
|
15
|
+
if (o && typeof o.length === "number") return {
|
|
16
|
+
next: function () {
|
|
17
|
+
if (o && i >= o.length) o = void 0;
|
|
18
|
+
return { value: o && o[i++], done: !o };
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
22
|
+
};
|
|
23
|
+
import { AcGePoint2d } from '../math/AcGePoint2d';
|
|
24
|
+
import { AcGeMathUtil, AcGeTol, TAU } from '../util';
|
|
25
|
+
import { AcGeCircArc2d } from './AcGeCircArc2d';
|
|
26
|
+
import { AcGePolyline2d } from './AcGePolyline2d';
|
|
27
|
+
/** Epsilon for circle–circle intersection and tangency tests during arc joins. */
|
|
28
|
+
var ROUND_JOIN_POINT_EPSILON = 1e-6;
|
|
29
|
+
/**
|
|
30
|
+
* Creates offset curves for a 2D polyline in the XY plane.
|
|
31
|
+
*
|
|
32
|
+
* Routing depends on polyline content:
|
|
33
|
+
* - Polylines with bulge (arc) segments use analytic line/arc offset and custom joins.
|
|
34
|
+
* - Vertex-only polylines (open or closed) use parallel edge offset with miter intersections.
|
|
35
|
+
*
|
|
36
|
+
* Sign convention: positive `offsetDist` offsets to the left of the travel direction
|
|
37
|
+
* along each segment (consistent with AutoCAD-style polyline offset).
|
|
38
|
+
*
|
|
39
|
+
* @param polyline - Source polyline; may be open or closed, with or without bulge arcs
|
|
40
|
+
* @param offsetDist - Signed offset distance in drawing units (left = positive)
|
|
41
|
+
* @returns One or more offset polylines; empty array when offsetting fails or input is degenerate
|
|
42
|
+
*/
|
|
43
|
+
export function offsetAcGePolyline2d(polyline, offsetDist) {
|
|
44
|
+
if (hasArcSegments(polyline)) {
|
|
45
|
+
var result_1 = offsetPolylineWithArcSegments(polyline, offsetDist);
|
|
46
|
+
return result_1 ? [result_1] : [];
|
|
47
|
+
}
|
|
48
|
+
var source = normalizeVertexPolyline(polyline);
|
|
49
|
+
var n = source.numberOfVertices;
|
|
50
|
+
if (n < 2)
|
|
51
|
+
return [];
|
|
52
|
+
var result = createVertexOnlyPolylineOffset(source, offsetDist);
|
|
53
|
+
return result ? [result] : [];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Normalizes a vertex-only polyline before offset or other geometric processing.
|
|
57
|
+
*
|
|
58
|
+
* Removes consecutive duplicate vertices and, for closed paths, drops a closing
|
|
59
|
+
* vertex that coincides with the first vertex so downstream algorithms see a
|
|
60
|
+
* minimal vertex ring.
|
|
61
|
+
*
|
|
62
|
+
* @param polyline - Input polyline (typically straight segments only)
|
|
63
|
+
* @returns The same instance if already normalized, otherwise a new polyline with cleaned vertices
|
|
64
|
+
*/
|
|
65
|
+
export function preparePolylineForOffset(polyline) {
|
|
66
|
+
return normalizeVertexPolyline(polyline);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Offsets a polyline that contains one or more bulge-defined arc segments.
|
|
70
|
+
*
|
|
71
|
+
* Each segment is offset individually (parallel line or concentric arc), then
|
|
72
|
+
* segment ends are joined at original vertices with miter, tangent, or round
|
|
73
|
+
* fallbacks as appropriate.
|
|
74
|
+
*
|
|
75
|
+
* @param polyline - Source polyline with at least one non-zero bulge
|
|
76
|
+
* @param offsetDist - Signed offset distance in drawing units
|
|
77
|
+
* @returns A single closed or open offset polyline, or `null` if any segment offset or join fails
|
|
78
|
+
*/
|
|
79
|
+
function offsetPolylineWithArcSegments(polyline, offsetDist) {
|
|
80
|
+
var e_1, _a;
|
|
81
|
+
var segments = collectPlanarSegments(polyline);
|
|
82
|
+
if (segments.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
var offsetSegments = [];
|
|
85
|
+
try {
|
|
86
|
+
for (var segments_1 = __values(segments), segments_1_1 = segments_1.next(); !segments_1_1.done; segments_1_1 = segments_1.next()) {
|
|
87
|
+
var segment = segments_1_1.value;
|
|
88
|
+
var offsetSegment = offsetPlanarSegment(segment, offsetDist);
|
|
89
|
+
if (!offsetSegment)
|
|
90
|
+
return null;
|
|
91
|
+
offsetSegments.push(offsetSegment);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
95
|
+
finally {
|
|
96
|
+
try {
|
|
97
|
+
if (segments_1_1 && !segments_1_1.done && (_a = segments_1.return)) _a.call(segments_1);
|
|
98
|
+
}
|
|
99
|
+
finally { if (e_1) throw e_1.error; }
|
|
100
|
+
}
|
|
101
|
+
var joinVertices = collectJoinVertices(polyline);
|
|
102
|
+
var points = buildJoinedOffsetPath(offsetSegments, joinVertices, polyline.closed, offsetDist);
|
|
103
|
+
if (points.length < 2)
|
|
104
|
+
return null;
|
|
105
|
+
var result = new AcGePolyline2d();
|
|
106
|
+
points.forEach(function (point, index) {
|
|
107
|
+
result.addVertexAt(index, { x: point.x, y: point.y });
|
|
108
|
+
});
|
|
109
|
+
result.closed = polyline.closed;
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Decomposes a polyline into ordered planar segments in traversal order.
|
|
114
|
+
*
|
|
115
|
+
* Each edge from vertex `i` to `(i + 1) % n` becomes either a line chord (zero bulge)
|
|
116
|
+
* or a circular arc constructed from start vertex, end vertex, and bulge.
|
|
117
|
+
*
|
|
118
|
+
* @param polyline - Source polyline
|
|
119
|
+
* @returns Array of line and arc segments; empty when the polyline has fewer than two vertices (open) or one vertex (closed)
|
|
120
|
+
*/
|
|
121
|
+
function collectPlanarSegments(polyline) {
|
|
122
|
+
var _a;
|
|
123
|
+
var segments = [];
|
|
124
|
+
var count = polyline.numberOfVertices;
|
|
125
|
+
var segmentCount = polyline.closed ? count : count - 1;
|
|
126
|
+
for (var i = 0; i < segmentCount; i++) {
|
|
127
|
+
var start = polyline.vertices[i];
|
|
128
|
+
var end = polyline.vertices[(i + 1) % count];
|
|
129
|
+
if (AcGeTol.isPositive(Math.abs((_a = start.bulge) !== null && _a !== void 0 ? _a : 0))) {
|
|
130
|
+
segments.push({
|
|
131
|
+
kind: 'arc',
|
|
132
|
+
arc: new AcGeCircArc2d(start, end, start.bulge)
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
segments.push({
|
|
137
|
+
kind: 'line',
|
|
138
|
+
start: new AcGePoint2d(start.x, start.y),
|
|
139
|
+
end: new AcGePoint2d(end.x, end.y)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return segments;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Collects the 2D positions of all polyline vertices in index order.
|
|
147
|
+
*
|
|
148
|
+
* Used as join anchors when connecting offset segments at original corners.
|
|
149
|
+
*
|
|
150
|
+
* @param polyline - Source polyline
|
|
151
|
+
* @returns Clone-free point list matching `polyline.numberOfVertices` entries
|
|
152
|
+
*/
|
|
153
|
+
function collectJoinVertices(polyline) {
|
|
154
|
+
var vertices = [];
|
|
155
|
+
for (var i = 0; i < polyline.numberOfVertices; i++) {
|
|
156
|
+
vertices.push(polyline.getPointAt(i));
|
|
157
|
+
}
|
|
158
|
+
return vertices;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Offsets a single planar segment by the signed distance.
|
|
162
|
+
*
|
|
163
|
+
* @param segment - Line chord or bulge arc segment
|
|
164
|
+
* @param offsetDist - Signed offset distance (left of travel = positive)
|
|
165
|
+
* @returns Offset line (with direction vector) or offset arc, or `null` if degenerate
|
|
166
|
+
*/
|
|
167
|
+
function offsetPlanarSegment(segment, offsetDist) {
|
|
168
|
+
if (segment.kind === 'line') {
|
|
169
|
+
var offsetLine = offsetLineSegment(segment.start, segment.end, offsetDist);
|
|
170
|
+
if (!offsetLine)
|
|
171
|
+
return null;
|
|
172
|
+
return __assign({ kind: 'line' }, offsetLine);
|
|
173
|
+
}
|
|
174
|
+
var offsetArc = offsetCircArc2d(segment.arc, segment.arc.startPoint, segment.arc.endPoint, offsetDist);
|
|
175
|
+
if (!offsetArc)
|
|
176
|
+
return null;
|
|
177
|
+
return { kind: 'arc', arc: offsetArc };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Builds a concentric circular arc at the requested offset from a source arc.
|
|
181
|
+
*
|
|
182
|
+
* Radius change follows the same left-hand rule as line offset via {@link getArcRadiusDelta}.
|
|
183
|
+
*
|
|
184
|
+
* @param arc - Source circular arc
|
|
185
|
+
* @param start - Start point of the arc segment (chord start)
|
|
186
|
+
* @param end - End point of the arc segment (chord end)
|
|
187
|
+
* @param offsetDist - Signed offset distance
|
|
188
|
+
* @returns New arc sharing center and angles, or `null` if the offset radius is non-positive
|
|
189
|
+
*/
|
|
190
|
+
function offsetCircArc2d(arc, start, end, offsetDist) {
|
|
191
|
+
var radiusDelta = getArcRadiusDelta(arc, start, end, offsetDist);
|
|
192
|
+
var radius = arc.radius + radiusDelta;
|
|
193
|
+
if (AcGeTol.isNonPositive(radius))
|
|
194
|
+
return null;
|
|
195
|
+
return new AcGeCircArc2d(arc.center, radius, arc.startAngle, arc.endAngle, arc.clockwise);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Computes the signed radius change for a concentric arc offset.
|
|
199
|
+
*
|
|
200
|
+
* Applies the same left-hand offset rule used for line segments: positive
|
|
201
|
+
* `offsetDist` shifts the arc to the left of its travel direction.
|
|
202
|
+
*
|
|
203
|
+
* Concentric offset direction depends on which side of the chord the center
|
|
204
|
+
* lies: when the center is on the left, shrink the radius; when on the right,
|
|
205
|
+
* expand it. Degenerate zero-length chords fall back to clockwise flag only.
|
|
206
|
+
*
|
|
207
|
+
* @param arc - Source circular arc
|
|
208
|
+
* @param start - Chord start point
|
|
209
|
+
* @param end - Chord end point
|
|
210
|
+
* @param offsetDist - Signed offset distance
|
|
211
|
+
* @returns Signed delta to add to `arc.radius` (not the new radius itself)
|
|
212
|
+
*/
|
|
213
|
+
function getArcRadiusDelta(arc, start, end, offsetDist) {
|
|
214
|
+
var dx = end.x - start.x;
|
|
215
|
+
var dy = end.y - start.y;
|
|
216
|
+
var len = Math.hypot(dx, dy);
|
|
217
|
+
if (AcGeTol.isNonPositive(len)) {
|
|
218
|
+
return arc.clockwise ? -offsetDist : offsetDist;
|
|
219
|
+
}
|
|
220
|
+
var leftNx = -dy / len;
|
|
221
|
+
var leftNy = dx / len;
|
|
222
|
+
var toCenterX = arc.center.x - (start.x + end.x) / 2;
|
|
223
|
+
var toCenterY = arc.center.y - (start.y + end.y) / 2;
|
|
224
|
+
var centerOnLeft = toCenterX * leftNx + toCenterY * leftNy > 0;
|
|
225
|
+
return centerOnLeft ? -offsetDist : offsetDist;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Offsets a finite line segment parallel to itself.
|
|
229
|
+
*
|
|
230
|
+
* The offset direction is the left normal scaled by `offsetDist`. The returned
|
|
231
|
+
* `dx`/`dy` match the original edge direction for use in later miter joins.
|
|
232
|
+
*
|
|
233
|
+
* @param start - Segment start
|
|
234
|
+
* @param end - Segment end
|
|
235
|
+
* @param offsetDist - Signed offset distance
|
|
236
|
+
* @returns Offset endpoints plus original direction components, or `null` if the segment length is zero
|
|
237
|
+
*/
|
|
238
|
+
function offsetLineSegment(start, end, offsetDist) {
|
|
239
|
+
var dx = end.x - start.x;
|
|
240
|
+
var dy = end.y - start.y;
|
|
241
|
+
var len = Math.hypot(dx, dy);
|
|
242
|
+
if (AcGeTol.isNonPositive(len))
|
|
243
|
+
return null;
|
|
244
|
+
var nx = (-dy / len) * offsetDist;
|
|
245
|
+
var ny = (dx / len) * offsetDist;
|
|
246
|
+
return {
|
|
247
|
+
start: new AcGePoint2d(start.x + nx, start.y + ny),
|
|
248
|
+
end: new AcGePoint2d(end.x + nx, end.y + ny),
|
|
249
|
+
dx: dx,
|
|
250
|
+
dy: dy
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Assembles a continuous polyline path from offset segments and per-vertex join data.
|
|
255
|
+
*
|
|
256
|
+
* Walks segments in order, emitting join points, optional round fillet samples,
|
|
257
|
+
* and interior samples along offset arcs between join limits.
|
|
258
|
+
*
|
|
259
|
+
* @param offsetSegments - Per-edge offset geometry in path order
|
|
260
|
+
* @param joinVertices - Original polyline vertices used as join anchors
|
|
261
|
+
* @param closed - Whether the path wraps from last segment back to first
|
|
262
|
+
* @param offsetDist - Signed offset distance (used for round-join radius)
|
|
263
|
+
* @returns Ordered vertex chain with consecutive duplicates removed
|
|
264
|
+
*/
|
|
265
|
+
function buildJoinedOffsetPath(offsetSegments, joinVertices, closed, offsetDist) {
|
|
266
|
+
var _a, _b, _c;
|
|
267
|
+
var count = offsetSegments.length;
|
|
268
|
+
if (count === 0)
|
|
269
|
+
return [];
|
|
270
|
+
var joins = computeSegmentJoins(offsetSegments, joinVertices, closed, offsetDist);
|
|
271
|
+
var points = [
|
|
272
|
+
(_b = (_a = joins[0].nextSegmentStart) === null || _a === void 0 ? void 0 : _a.clone()) !== null && _b !== void 0 ? _b : joins[0].point.clone()
|
|
273
|
+
];
|
|
274
|
+
for (var i = 0; i < count; i++) {
|
|
275
|
+
var segment = offsetSegments[i];
|
|
276
|
+
var joinAtEnd = i === count - 1 && !closed
|
|
277
|
+
? {
|
|
278
|
+
point: getOffsetSegmentEnd(segment)
|
|
279
|
+
}
|
|
280
|
+
: joins[(i + 1) % count];
|
|
281
|
+
if (i > 0 && joins[i].filletPoints) {
|
|
282
|
+
joins[i].filletPoints.forEach(function (point) { return points.push(point.clone()); });
|
|
283
|
+
}
|
|
284
|
+
if (segment.kind === 'arc') {
|
|
285
|
+
var arcStart = (_c = joins[i].nextSegmentStart) !== null && _c !== void 0 ? _c : joins[i].point;
|
|
286
|
+
var arcEnd = joinAtEnd.point;
|
|
287
|
+
var arcSamples = sampleArcBetweenPoints(segment.arc, arcStart, arcEnd, Math.max(16, 32));
|
|
288
|
+
for (var j = 1; j < arcSamples.length; j++) {
|
|
289
|
+
points.push(arcSamples[j]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
points.push(joinAtEnd.point.clone());
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return dedupeConsecutivePoints(points);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Computes join information at every vertex between consecutive offset segments.
|
|
300
|
+
*
|
|
301
|
+
* For closed polylines, index `0` joins the last and first offset segments at
|
|
302
|
+
* `joinVertices[0]`. For open polylines, the start uses the first segment's
|
|
303
|
+
* offset start without a prior join.
|
|
304
|
+
*
|
|
305
|
+
* @param offsetSegments - Offset segments in path order
|
|
306
|
+
* @param joinVertices - Original corner vertices
|
|
307
|
+
* @param closed - Whether the polyline is closed
|
|
308
|
+
* @param offsetDist - Signed offset distance for round joins
|
|
309
|
+
* @returns One {@link SegmentJoinInfo} per segment, aligned with segment indices
|
|
310
|
+
*/
|
|
311
|
+
function computeSegmentJoins(offsetSegments, joinVertices, closed, offsetDist) {
|
|
312
|
+
var count = offsetSegments.length;
|
|
313
|
+
var joins = [];
|
|
314
|
+
for (var i = 0; i < count; i++) {
|
|
315
|
+
if (i === 0) {
|
|
316
|
+
joins.push(closed
|
|
317
|
+
? joinOffsetSegments(offsetSegments[count - 1], offsetSegments[0], joinVertices[0], offsetDist)
|
|
318
|
+
: { point: getOffsetSegmentStart(offsetSegments[0]) });
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
joins.push(joinOffsetSegments(offsetSegments[i - 1], offsetSegments[i], joinVertices[i], offsetDist));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return joins;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Samples points along a circular arc between two arbitrary points on the arc.
|
|
328
|
+
*
|
|
329
|
+
* Projects `start` and `end` onto the arc, measures sweep in internal angle
|
|
330
|
+
* space, and interpolates at uniform parameter steps. Endpoints are included.
|
|
331
|
+
*
|
|
332
|
+
* @param arc - Circular arc to sample
|
|
333
|
+
* @param start - Approximate start of the sampled span (need not lie exactly on the arc)
|
|
334
|
+
* @param end - Approximate end of the sampled span
|
|
335
|
+
* @param numPoints - Number of interior divisions (total samples = `numPoints + 1`)
|
|
336
|
+
* @returns Polyline of sampled points from start to end along the arc
|
|
337
|
+
*/
|
|
338
|
+
function sampleArcBetweenPoints(arc, start, end, numPoints) {
|
|
339
|
+
var startOnArc = arc.nearestPoint(start);
|
|
340
|
+
var endOnArc = arc.nearestPoint(end);
|
|
341
|
+
var startInternal = internalAngleAtPoint(arc, startOnArc);
|
|
342
|
+
var endInternal = internalAngleAtPoint(arc, endOnArc);
|
|
343
|
+
var sweep = sweepAlongArc(arc, startInternal, endInternal);
|
|
344
|
+
var points = [];
|
|
345
|
+
for (var i = 0; i <= numPoints; i++) {
|
|
346
|
+
var t = i / numPoints;
|
|
347
|
+
var internalAngle = arc.clockwise
|
|
348
|
+
? startInternal - sweep * t
|
|
349
|
+
: startInternal + sweep * t;
|
|
350
|
+
points.push(arc.getPointAtAngle(publicAngleFromInternal(arc, internalAngle)));
|
|
351
|
+
}
|
|
352
|
+
return points;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Returns the mathematical polar angle from the arc center to a point.
|
|
356
|
+
*
|
|
357
|
+
* @param arc - Circular arc providing the center
|
|
358
|
+
* @param point - Point in the arc plane
|
|
359
|
+
* @returns Angle in radians in `[-π, π]`, consistent with `Math.atan2`
|
|
360
|
+
*/
|
|
361
|
+
function internalAngleAtPoint(arc, point) {
|
|
362
|
+
return Math.atan2(point.y - arc.center.y, point.x - arc.center.x);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Converts an internal (mathematical) angle to the public angle used by {@link AcGeCircArc2d}.
|
|
366
|
+
*
|
|
367
|
+
* Counterclockwise arcs use normalized mathematical angles; clockwise arcs apply
|
|
368
|
+
* the arc's mirrored angle convention.
|
|
369
|
+
*
|
|
370
|
+
* @param arc - Circular arc defining clockwise vs counterclockwise storage
|
|
371
|
+
* @param internalAngle - Angle in internal/mathematical space
|
|
372
|
+
* @returns Angle suitable for `arc.getPointAtAngle`
|
|
373
|
+
*/
|
|
374
|
+
function publicAngleFromInternal(arc, internalAngle) {
|
|
375
|
+
var normalized = AcGeMathUtil.normalizeAngle(internalAngle);
|
|
376
|
+
return arc.clockwise ? mirrorAngle(normalized) : normalized;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Mirrors a mathematical angle into the clockwise arc angle convention.
|
|
380
|
+
*
|
|
381
|
+
* @param angle - Angle in radians (mathematical convention)
|
|
382
|
+
* @returns Mirrored angle in radians for clockwise arc parameterization
|
|
383
|
+
*/
|
|
384
|
+
function mirrorAngle(angle) {
|
|
385
|
+
var degrees = (angle * 180) / Math.PI;
|
|
386
|
+
return ((360 - degrees) % 360) * (Math.PI / 180);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Computes the positive sweep from one internal angle to another along the arc.
|
|
390
|
+
*
|
|
391
|
+
* @param arc - Circular arc (direction from `clockwise`)
|
|
392
|
+
* @param fromInternal - Start internal angle
|
|
393
|
+
* @param toInternal - End internal angle
|
|
394
|
+
* @returns Positive sweep magnitude in radians along the arc direction
|
|
395
|
+
*/
|
|
396
|
+
function sweepAlongArc(arc, fromInternal, toInternal) {
|
|
397
|
+
if (arc.clockwise) {
|
|
398
|
+
var sweep_1 = fromInternal - toInternal;
|
|
399
|
+
if (AcGeTol.isNonPositive(sweep_1)) {
|
|
400
|
+
sweep_1 += TAU;
|
|
401
|
+
}
|
|
402
|
+
return sweep_1;
|
|
403
|
+
}
|
|
404
|
+
var sweep = toInternal - fromInternal;
|
|
405
|
+
if (AcGeTol.isNonPositive(sweep)) {
|
|
406
|
+
sweep += TAU;
|
|
407
|
+
}
|
|
408
|
+
return sweep;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Returns the start point of an offset segment in path order.
|
|
412
|
+
*
|
|
413
|
+
* @param segment - Line or arc offset segment
|
|
414
|
+
* @returns Cloned start point of the offset geometry
|
|
415
|
+
*/
|
|
416
|
+
function getOffsetSegmentStart(segment) {
|
|
417
|
+
return segment.kind === 'line'
|
|
418
|
+
? segment.start.clone()
|
|
419
|
+
: segment.arc.startPoint.clone();
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Returns the end point of an offset segment in path order.
|
|
423
|
+
*
|
|
424
|
+
* @param segment - Line or arc offset segment
|
|
425
|
+
* @returns Cloned end point of the offset geometry
|
|
426
|
+
*/
|
|
427
|
+
function getOffsetSegmentEnd(segment) {
|
|
428
|
+
return segment.kind === 'line'
|
|
429
|
+
? segment.end.clone()
|
|
430
|
+
: segment.arc.endPoint.clone();
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Resolves the connection between two consecutive offset segments at a corner vertex.
|
|
434
|
+
*
|
|
435
|
+
* Dispatches to line–line miter, line–arc, arc–line, or arc–arc join logic.
|
|
436
|
+
* Falls back to the original vertex when segment kinds are unexpected.
|
|
437
|
+
*
|
|
438
|
+
* @param previous - Offset segment entering the vertex
|
|
439
|
+
* @param next - Offset segment leaving the vertex
|
|
440
|
+
* @param vertex - Original polyline corner
|
|
441
|
+
* @param offsetDist - Signed offset distance (magnitude used for round joins)
|
|
442
|
+
* @returns Join point and optional fillet bridging to the next segment
|
|
443
|
+
*/
|
|
444
|
+
function joinOffsetSegments(previous, next, vertex, offsetDist) {
|
|
445
|
+
if (previous.kind === 'line' && next.kind === 'line') {
|
|
446
|
+
return {
|
|
447
|
+
point: intersectPolylineOffsetSegments({
|
|
448
|
+
start: previous.start,
|
|
449
|
+
end: previous.end,
|
|
450
|
+
dx: previous.dx,
|
|
451
|
+
dy: previous.dy
|
|
452
|
+
}, {
|
|
453
|
+
start: next.start,
|
|
454
|
+
end: next.end,
|
|
455
|
+
dx: next.dx,
|
|
456
|
+
dy: next.dy
|
|
457
|
+
})
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (previous.kind === 'line' && next.kind === 'arc') {
|
|
461
|
+
return joinLineWithArcOffset(previous, next.arc, vertex, offsetDist, true);
|
|
462
|
+
}
|
|
463
|
+
if (previous.kind === 'arc' && next.kind === 'line') {
|
|
464
|
+
return joinLineWithArcOffset(next, previous.arc, vertex, offsetDist, false);
|
|
465
|
+
}
|
|
466
|
+
if (previous.kind === 'arc' && next.kind === 'arc') {
|
|
467
|
+
return joinArcWithArcOffset(previous.arc, next.arc, vertex, offsetDist);
|
|
468
|
+
}
|
|
469
|
+
return { point: vertex.clone() };
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Joins two offset circular arcs meeting at a common vertex.
|
|
473
|
+
*
|
|
474
|
+
* Prefers a point on both offset arcs near the gap midpoint (circle–circle
|
|
475
|
+
* intersection). If none exists, inserts a round join sampled around the vertex.
|
|
476
|
+
*
|
|
477
|
+
* @param previous - Offset arc ending at the vertex
|
|
478
|
+
* @param next - Offset arc starting at the vertex
|
|
479
|
+
* @param vertex - Original corner vertex
|
|
480
|
+
* @param offsetDist - Signed offset distance (absolute value = fillet radius)
|
|
481
|
+
* @returns Miter point on both arcs, or round join with separate segment endpoints
|
|
482
|
+
*/
|
|
483
|
+
function joinArcWithArcOffset(previous, next, vertex, offsetDist) {
|
|
484
|
+
var near = midpoint(previous.endPoint, next.startPoint);
|
|
485
|
+
var candidates = intersectOffsetCircles(previous, next);
|
|
486
|
+
var onBoth = candidates.filter(function (candidate) {
|
|
487
|
+
return isPointOnArc(previous, candidate) && isPointOnArc(next, candidate);
|
|
488
|
+
});
|
|
489
|
+
if (onBoth.length > 0) {
|
|
490
|
+
return { point: pickClosestPoint(onBoth, near) };
|
|
491
|
+
}
|
|
492
|
+
var from = previous.endPoint.clone();
|
|
493
|
+
var to = next.startPoint.clone();
|
|
494
|
+
var filletPoints = sampleVertexRoundJoin(vertex, Math.abs(offsetDist), from, to);
|
|
495
|
+
return {
|
|
496
|
+
point: from,
|
|
497
|
+
filletPoints: filletPoints,
|
|
498
|
+
nextSegmentStart: to
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Joins an offset line segment with an offset arc at a shared vertex.
|
|
503
|
+
*
|
|
504
|
+
* @param line - Offset line segment (includes direction `dx`/`dy`)
|
|
505
|
+
* @param arc - Offset circular arc on the other side of the vertex
|
|
506
|
+
* @param vertex - Original corner vertex
|
|
507
|
+
* @param offsetDist - Signed offset distance
|
|
508
|
+
* @param lineBeforeArc - `true` if `line` is the incoming segment and `arc` is outgoing
|
|
509
|
+
* @returns Intersection on both primitives, or round join with optional `nextSegmentStart`
|
|
510
|
+
*/
|
|
511
|
+
function joinLineWithArcOffset(line, arc, vertex, offsetDist, lineBeforeArc) {
|
|
512
|
+
var near = midpoint(lineBeforeArc ? line.end : line.start, lineBeforeArc ? arc.startPoint : arc.endPoint);
|
|
513
|
+
var intersection = intersectLineWithCircle(line, arc, near);
|
|
514
|
+
if (isPointOnOffsetLine(line, intersection) &&
|
|
515
|
+
isPointOnArc(arc, intersection)) {
|
|
516
|
+
return { point: intersection };
|
|
517
|
+
}
|
|
518
|
+
var lineSide = getLineOffsetPointAtVertex(line, vertex, offsetDist);
|
|
519
|
+
var arcSide = lineBeforeArc ? arc.startPoint.clone() : arc.endPoint.clone();
|
|
520
|
+
var filletPoints = sampleVertexRoundJoin(vertex, Math.abs(offsetDist), lineBeforeArc ? lineSide : arcSide, lineBeforeArc ? arcSide : lineSide);
|
|
521
|
+
if (lineBeforeArc) {
|
|
522
|
+
return {
|
|
523
|
+
point: lineSide,
|
|
524
|
+
filletPoints: filletPoints,
|
|
525
|
+
nextSegmentStart: arcSide
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
point: arcSide,
|
|
530
|
+
filletPoints: filletPoints,
|
|
531
|
+
nextSegmentStart: lineSide
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Computes the offset line point closest to a vertex along the left normal.
|
|
536
|
+
*
|
|
537
|
+
* @param line - Offset line with original edge direction `dx`/`dy`
|
|
538
|
+
* @param vertex - Corner on the source polyline
|
|
539
|
+
* @param offsetDist - Signed offset distance
|
|
540
|
+
* @returns Point on the infinite offset line through the vertex
|
|
541
|
+
*/
|
|
542
|
+
function getLineOffsetPointAtVertex(line, vertex, offsetDist) {
|
|
543
|
+
var len = Math.hypot(line.dx, line.dy);
|
|
544
|
+
var nx = (-line.dy / len) * offsetDist;
|
|
545
|
+
var ny = (line.dx / len) * offsetDist;
|
|
546
|
+
return new AcGePoint2d(vertex.x + nx, vertex.y + ny);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Tests whether a point lies on the infinite line supporting an offset segment.
|
|
550
|
+
*
|
|
551
|
+
* @param line - Offset line with start and direction
|
|
552
|
+
* @param point - Candidate point
|
|
553
|
+
* @returns `true` if perpendicular distance to the line is within tolerance
|
|
554
|
+
*/
|
|
555
|
+
function isPointOnOffsetLine(line, point) {
|
|
556
|
+
var len = Math.hypot(line.dx, line.dy);
|
|
557
|
+
if (AcGeTol.isNonPositive(len))
|
|
558
|
+
return false;
|
|
559
|
+
var ux = line.dx / len;
|
|
560
|
+
var uy = line.dy / len;
|
|
561
|
+
var px = point.x - line.start.x;
|
|
562
|
+
var py = point.y - line.start.y;
|
|
563
|
+
var perp = Math.abs(px * uy - py * ux);
|
|
564
|
+
return AcGeTol.equalToZero(perp);
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Samples interior points of a circular round join at a vertex.
|
|
568
|
+
*
|
|
569
|
+
* Arc spans the shorter rotation from `from` to `to` about `vertex` at `radius`.
|
|
570
|
+
* Endpoints are excluded from the returned list.
|
|
571
|
+
*
|
|
572
|
+
* @param vertex - Center of the round join
|
|
573
|
+
* @param radius - Fillet radius (positive)
|
|
574
|
+
* @param from - Start direction point on the fillet circle
|
|
575
|
+
* @param to - End direction point on the fillet circle
|
|
576
|
+
* @returns Interior fillet samples; empty when start and end directions coincide
|
|
577
|
+
*/
|
|
578
|
+
function sampleVertexRoundJoin(vertex, radius, from, to) {
|
|
579
|
+
var a1 = Math.atan2(from.y - vertex.y, from.x - vertex.x);
|
|
580
|
+
var a2 = Math.atan2(to.y - vertex.y, to.x - vertex.x);
|
|
581
|
+
var sweep = shortestAngleSweep(a1, a2);
|
|
582
|
+
if (AcGeTol.equalToZero(Math.abs(sweep))) {
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
585
|
+
var points = [];
|
|
586
|
+
var steps = Math.max(2, Math.ceil((Math.abs(sweep) / Math.PI) * 16));
|
|
587
|
+
for (var i = 1; i < steps; i++) {
|
|
588
|
+
var t = i / steps;
|
|
589
|
+
var angle = a1 + sweep * t;
|
|
590
|
+
points.push(new AcGePoint2d(vertex.x + radius * Math.cos(angle), vertex.y + radius * Math.sin(angle)));
|
|
591
|
+
}
|
|
592
|
+
return points;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Returns the shortest signed angular sweep from one direction to another.
|
|
596
|
+
*
|
|
597
|
+
* @param from - Start angle in radians
|
|
598
|
+
* @param to - End angle in radians
|
|
599
|
+
* @returns Signed sweep in `(-π, π]`
|
|
600
|
+
*/
|
|
601
|
+
function shortestAngleSweep(from, to) {
|
|
602
|
+
var sweep = to - from;
|
|
603
|
+
while (sweep > Math.PI) {
|
|
604
|
+
sweep -= TAU;
|
|
605
|
+
}
|
|
606
|
+
while (sweep <= -Math.PI) {
|
|
607
|
+
sweep += TAU;
|
|
608
|
+
}
|
|
609
|
+
return sweep;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Intersects an infinite offset line with a circle (offset arc boundary).
|
|
613
|
+
*
|
|
614
|
+
* @param line - Offset line segment defining origin and direction
|
|
615
|
+
* @param arc - Circular arc whose radius and center define the circle
|
|
616
|
+
* @param near - Preference point when choosing among multiple intersections
|
|
617
|
+
* @returns Closest valid intersection to `near`, or `near` when no real roots exist
|
|
618
|
+
*/
|
|
619
|
+
function intersectLineWithCircle(line, arc, near) {
|
|
620
|
+
var dirLen = Math.hypot(line.dx, line.dy);
|
|
621
|
+
if (AcGeTol.isNonPositive(dirLen)) {
|
|
622
|
+
return near.clone();
|
|
623
|
+
}
|
|
624
|
+
var ux = line.dx / dirLen;
|
|
625
|
+
var uy = line.dy / dirLen;
|
|
626
|
+
var fx = line.start.x - arc.center.x;
|
|
627
|
+
var fy = line.start.y - arc.center.y;
|
|
628
|
+
var b = 2 * (fx * ux + fy * uy);
|
|
629
|
+
var c = fx * fx + fy * fy - arc.radius * arc.radius;
|
|
630
|
+
var discriminant = b * b - 4 * c;
|
|
631
|
+
if (discriminant < 0) {
|
|
632
|
+
return near.clone();
|
|
633
|
+
}
|
|
634
|
+
var sqrtDisc = Math.sqrt(discriminant);
|
|
635
|
+
var candidates = [(-b - sqrtDisc) / 2, (-b + sqrtDisc) / 2].map(function (t) { return new AcGePoint2d(line.start.x + ux * t, line.start.y + uy * t); });
|
|
636
|
+
var onArc = candidates.filter(function (candidate) { return isPointOnArc(arc, candidate); });
|
|
637
|
+
return pickClosestPoint(onArc.length > 0 ? onArc : candidates, near);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Tests whether a point lies on a circular arc segment (not merely on the full circle).
|
|
641
|
+
*
|
|
642
|
+
* @param arc - Circular arc with defined start/end and clockwise flag
|
|
643
|
+
* @param point - Candidate point
|
|
644
|
+
* @returns `true` if distance to center matches radius and angle is within the arc span
|
|
645
|
+
*/
|
|
646
|
+
function isPointOnArc(arc, point) {
|
|
647
|
+
var radiusDelta = Math.abs(Math.hypot(point.x - arc.center.x, point.y - arc.center.y) - arc.radius);
|
|
648
|
+
if (AcGeTol.isPositive(radiusDelta)) {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
var internalAngle = internalAngleAtPoint(arc, point);
|
|
652
|
+
var internalStart = arc.clockwise
|
|
653
|
+
? mirrorAngle(AcGeMathUtil.normalizeAngle(arc.startAngle))
|
|
654
|
+
: AcGeMathUtil.normalizeAngle(arc.startAngle);
|
|
655
|
+
var internalEnd = arc.clockwise
|
|
656
|
+
? mirrorAngle(AcGeMathUtil.normalizeAngle(arc.endAngle))
|
|
657
|
+
: AcGeMathUtil.normalizeAngle(arc.endAngle);
|
|
658
|
+
return AcGeMathUtil.isBetweenAngle(internalAngle, internalStart, internalEnd, arc.clockwise);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Intersects two circles defined by offset arc centers and radii.
|
|
662
|
+
*
|
|
663
|
+
* @param a - First offset arc (circle at `a.center` with radius `a.radius`)
|
|
664
|
+
* @param b - Second offset arc
|
|
665
|
+
* @returns Zero, one (tangent), or two intersection points; empty when circles are separated or nested beyond tolerance
|
|
666
|
+
*/
|
|
667
|
+
function intersectOffsetCircles(a, b) {
|
|
668
|
+
var dx = b.center.x - a.center.x;
|
|
669
|
+
var dy = b.center.y - a.center.y;
|
|
670
|
+
var dist = Math.hypot(dx, dy);
|
|
671
|
+
if (AcGeTol.isNonPositive(dist)) {
|
|
672
|
+
return [];
|
|
673
|
+
}
|
|
674
|
+
var radiusSum = a.radius + b.radius;
|
|
675
|
+
var radiusDiff = Math.abs(a.radius - b.radius);
|
|
676
|
+
if (dist > radiusSum + ROUND_JOIN_POINT_EPSILON) {
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
if (dist < radiusDiff - ROUND_JOIN_POINT_EPSILON) {
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
var aTerm = (a.radius * a.radius - b.radius * b.radius + dist * dist) / (2 * dist);
|
|
683
|
+
var hSquared = a.radius * a.radius - aTerm * aTerm;
|
|
684
|
+
if (hSquared < -ROUND_JOIN_POINT_EPSILON) {
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
var mx = a.center.x + (aTerm * dx) / dist;
|
|
688
|
+
var my = a.center.y + (aTerm * dy) / dist;
|
|
689
|
+
if (hSquared <= ROUND_JOIN_POINT_EPSILON) {
|
|
690
|
+
return [new AcGePoint2d(mx, my)];
|
|
691
|
+
}
|
|
692
|
+
var h = Math.sqrt(hSquared);
|
|
693
|
+
var rx = (-dy * h) / dist;
|
|
694
|
+
var ry = (dx * h) / dist;
|
|
695
|
+
return [new AcGePoint2d(mx + rx, my + ry), new AcGePoint2d(mx - rx, my - ry)];
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Selects the candidate point closest to a reference location.
|
|
699
|
+
*
|
|
700
|
+
* @param points - Non-empty list of candidates
|
|
701
|
+
* @param near - Reference point for distance comparison
|
|
702
|
+
* @returns Clone of the closest candidate
|
|
703
|
+
*/
|
|
704
|
+
function pickClosestPoint(points, near) {
|
|
705
|
+
var best = points[0];
|
|
706
|
+
var bestDist = best.distanceToSquared(near);
|
|
707
|
+
for (var i = 1; i < points.length; i++) {
|
|
708
|
+
var dist = points[i].distanceToSquared(near);
|
|
709
|
+
if (dist < bestDist) {
|
|
710
|
+
best = points[i];
|
|
711
|
+
bestDist = dist;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return best.clone();
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Computes the component-wise midpoint between two points.
|
|
718
|
+
*
|
|
719
|
+
* @param a - First point
|
|
720
|
+
* @param b - Second point
|
|
721
|
+
* @returns New point at `(a + b) / 2`
|
|
722
|
+
*/
|
|
723
|
+
function midpoint(a, b) {
|
|
724
|
+
return new AcGePoint2d((a.x + b.x) / 2, (a.y + b.y) / 2);
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Determines whether a polyline contains any bulge-defined arc segments.
|
|
728
|
+
*
|
|
729
|
+
* @param polyline - Polyline to inspect
|
|
730
|
+
* @returns `true` if any edge has a non-zero bulge within tolerance
|
|
731
|
+
*/
|
|
732
|
+
function hasArcSegments(polyline) {
|
|
733
|
+
var _a;
|
|
734
|
+
var count = polyline.numberOfVertices;
|
|
735
|
+
var segmentCount = polyline.closed ? count : count - 1;
|
|
736
|
+
for (var i = 0; i < segmentCount; i++) {
|
|
737
|
+
var vertex = polyline.vertices[i];
|
|
738
|
+
if (AcGeTol.isPositive(Math.abs((_a = vertex.bulge) !== null && _a !== void 0 ? _a : 0))) {
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Returns a vertex-only polyline with normalized path points.
|
|
746
|
+
*
|
|
747
|
+
* @param polyline - Input polyline
|
|
748
|
+
* @returns `polyline` unchanged when already normalized, else a new instance
|
|
749
|
+
*/
|
|
750
|
+
function normalizeVertexPolyline(polyline) {
|
|
751
|
+
var points = normalizePathPoints(polyline.vertices.map(function (vertex) { return new AcGePoint2d(vertex.x, vertex.y); }), polyline.closed);
|
|
752
|
+
if (points.length === polyline.numberOfVertices) {
|
|
753
|
+
return polyline;
|
|
754
|
+
}
|
|
755
|
+
return new AcGePolyline2d(points.map(function (point) { return ({ x: point.x, y: point.y }); }), polyline.closed);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Deduplicates consecutive vertices and removes redundant closure for closed paths.
|
|
759
|
+
*
|
|
760
|
+
* @param points - Vertex chain in order
|
|
761
|
+
* @param closed - Whether the path is closed
|
|
762
|
+
* @returns Cleaned vertex list
|
|
763
|
+
*/
|
|
764
|
+
function normalizePathPoints(points, closed) {
|
|
765
|
+
var deduped = dedupeConsecutivePoints(points);
|
|
766
|
+
if (!closed || deduped.length < 2)
|
|
767
|
+
return deduped;
|
|
768
|
+
var first = deduped[0];
|
|
769
|
+
var last = deduped[deduped.length - 1];
|
|
770
|
+
if (AcGeTol.isNonPositive(Math.hypot(first.x - last.x, first.y - last.y))) {
|
|
771
|
+
deduped.pop();
|
|
772
|
+
}
|
|
773
|
+
return deduped;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Removes consecutive vertices that coincide within geometric tolerance.
|
|
777
|
+
*
|
|
778
|
+
* @param points - Input vertex chain (may contain duplicates)
|
|
779
|
+
* @returns New array with only distinct consecutive points
|
|
780
|
+
*/
|
|
781
|
+
function dedupeConsecutivePoints(points) {
|
|
782
|
+
var result = [];
|
|
783
|
+
points.forEach(function (point) {
|
|
784
|
+
var last = result[result.length - 1];
|
|
785
|
+
if (!last ||
|
|
786
|
+
AcGeTol.isPositive(Math.hypot(last.x - point.x, last.y - point.y))) {
|
|
787
|
+
result.push(new AcGePoint2d(point.x, point.y));
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Offsets a vertex-only polyline (open or closed) composed of straight segments.
|
|
794
|
+
*
|
|
795
|
+
* Each edge is offset parallel to itself; consecutive offset lines are joined with
|
|
796
|
+
* miter intersections. Open paths keep raw offset starts/ends; closed paths join
|
|
797
|
+
* cyclically at every corner.
|
|
798
|
+
*
|
|
799
|
+
* @param polyline - Vertex-only polyline with at least two vertices
|
|
800
|
+
* @param offsetDist - Signed offset distance
|
|
801
|
+
* @returns Offset polyline, or `null` if no valid edges remain after skipping zero-length segments
|
|
802
|
+
*/
|
|
803
|
+
function createVertexOnlyPolylineOffset(polyline, offsetDist) {
|
|
804
|
+
var sourcePoints = [];
|
|
805
|
+
for (var i = 0; i < polyline.numberOfVertices; i++) {
|
|
806
|
+
sourcePoints.push(polyline.getPointAt(i));
|
|
807
|
+
}
|
|
808
|
+
var offsetSegments = buildPolylineOffsetSegments(sourcePoints, polyline.closed, offsetDist);
|
|
809
|
+
if (offsetSegments.length === 0)
|
|
810
|
+
return null;
|
|
811
|
+
var resultPoints = [];
|
|
812
|
+
if (polyline.closed) {
|
|
813
|
+
if (offsetSegments.length < 2)
|
|
814
|
+
return null;
|
|
815
|
+
var count = offsetSegments.length;
|
|
816
|
+
for (var i = 0; i < count; i++) {
|
|
817
|
+
var prev = offsetSegments[(i - 1 + count) % count];
|
|
818
|
+
var curr = offsetSegments[i];
|
|
819
|
+
resultPoints.push(intersectPolylineOffsetSegments(prev, curr));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
resultPoints.push(offsetSegments[0].start);
|
|
824
|
+
for (var i = 1; i < offsetSegments.length; i++) {
|
|
825
|
+
resultPoints.push(intersectPolylineOffsetSegments(offsetSegments[i - 1], offsetSegments[i]));
|
|
826
|
+
}
|
|
827
|
+
resultPoints.push(offsetSegments[offsetSegments.length - 1].end);
|
|
828
|
+
}
|
|
829
|
+
var result = new AcGePolyline2d();
|
|
830
|
+
resultPoints.forEach(function (point, index) {
|
|
831
|
+
result.addVertexAt(index, { x: point.x, y: point.y });
|
|
832
|
+
});
|
|
833
|
+
result.closed = polyline.closed;
|
|
834
|
+
return result;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Builds parallel offset segments for each non-degenerate edge of a vertex path.
|
|
838
|
+
*
|
|
839
|
+
* @param sourcePoints - Vertices in path order
|
|
840
|
+
* @param closed - Whether edges wrap from the last vertex back to the first
|
|
841
|
+
* @param offsetDist - Signed offset distance (left of travel = positive)
|
|
842
|
+
* @returns One offset segment per valid edge, in path order
|
|
843
|
+
*/
|
|
844
|
+
function buildPolylineOffsetSegments(sourcePoints, closed, offsetDist) {
|
|
845
|
+
var offsetSegments = [];
|
|
846
|
+
var edgeCount = closed ? sourcePoints.length : sourcePoints.length - 1;
|
|
847
|
+
for (var i = 0; i < edgeCount; i++) {
|
|
848
|
+
var start = sourcePoints[i];
|
|
849
|
+
var end = sourcePoints[closed ? (i + 1) % sourcePoints.length : i + 1];
|
|
850
|
+
var dx = end.x - start.x;
|
|
851
|
+
var dy = end.y - start.y;
|
|
852
|
+
var len = Math.hypot(dx, dy);
|
|
853
|
+
if (AcGeTol.isNonPositive(len))
|
|
854
|
+
continue;
|
|
855
|
+
var nx = (-dy / len) * offsetDist;
|
|
856
|
+
var ny = (dx / len) * offsetDist;
|
|
857
|
+
offsetSegments.push({
|
|
858
|
+
start: new AcGePoint2d(start.x + nx, start.y + ny),
|
|
859
|
+
end: new AcGePoint2d(end.x + nx, end.y + ny),
|
|
860
|
+
dx: dx,
|
|
861
|
+
dy: dy
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
return offsetSegments;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Computes the intersection of two infinite offset lines from consecutive polyline edges.
|
|
868
|
+
*
|
|
869
|
+
* Each segment supplies a point on the line (`start`) and direction (`dx`, `dy`).
|
|
870
|
+
* Parallel edges return the midpoint of the gap between segment ends as a fallback.
|
|
871
|
+
*
|
|
872
|
+
* @param a - First offset edge
|
|
873
|
+
* @param b - Second offset edge
|
|
874
|
+
* @returns Intersection point (miter) of the two supporting lines
|
|
875
|
+
*/
|
|
876
|
+
function intersectPolylineOffsetSegments(a, b) {
|
|
877
|
+
var det = a.dx * b.dy - a.dy * b.dx;
|
|
878
|
+
if (AcGeTol.equalToZero(det)) {
|
|
879
|
+
return new AcGePoint2d((a.end.x + b.start.x) / 2, (a.end.y + b.start.y) / 2);
|
|
880
|
+
}
|
|
881
|
+
var qpx = b.start.x - a.start.x;
|
|
882
|
+
var qpy = b.start.y - a.start.y;
|
|
883
|
+
var t = (qpx * b.dy - qpy * b.dx) / det;
|
|
884
|
+
return new AcGePoint2d(a.start.x + t * a.dx, a.start.y + t * a.dy);
|
|
885
|
+
}
|
|
886
|
+
//# sourceMappingURL=AcGePolyline2dOffset.js.map
|