@orbat-mapper/control-measures 0.28.0 → 0.29.0
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/{index-DTx5ZF4Q.d.mts → index-Da8nAQ7C.d.mts} +124 -16
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +52 -6
- package/dist/preview/index.d.mts +1 -1
- package/dist/preview/index.mjs +1 -1
- package/dist/{renderControlMeasure-D1kIdove.mjs → renderControlMeasure-ZpYrUlKM.mjs} +840 -271
- package/package.json +1 -1
|
@@ -144,40 +144,45 @@ const destinationPoint = (origin, distance, bearing) => {
|
|
|
144
144
|
* Offsets a polyline to both sides at once, using Miter Joins or Round Joins.
|
|
145
145
|
* `offsets` carries one half-width per vertex, so the outline may taper along
|
|
146
146
|
* its centerline; both sides read the same array rather than materialising a
|
|
147
|
-
* negated copy per geometry pass.
|
|
147
|
+
* negated copy per geometry pass. Pass `rightOffsets` to give the right side
|
|
148
|
+
* its own half-widths (an asymmetric outline).
|
|
149
|
+
*
|
|
150
|
+
* `leftStarts` / `rightStarts` give, per centerline vertex, the index of its
|
|
151
|
+
* first point in that edge (rounded joins emit several), after any loop was
|
|
152
|
+
* trimmed out: `edge.slice(0, starts[i])` is the edge before vertex `i`.
|
|
148
153
|
*/
|
|
149
|
-
function offsetPolylineSides(points, offsets, rounded = false, segments = 5) {
|
|
154
|
+
function offsetPolylineSides(points, offsets, rounded = false, segments = 5, rightOffsets = offsets) {
|
|
155
|
+
const left = offsetPolylineWithOffsets(points, offsets, rounded, segments, 1);
|
|
156
|
+
const right = offsetPolylineWithOffsets(points, rightOffsets, rounded, segments, -1);
|
|
150
157
|
return {
|
|
151
|
-
left:
|
|
152
|
-
right:
|
|
158
|
+
left: left.edge,
|
|
159
|
+
right: right.edge,
|
|
160
|
+
leftStarts: left.starts,
|
|
161
|
+
rightStarts: right.starts
|
|
153
162
|
};
|
|
154
163
|
}
|
|
155
164
|
/** One side of {@link offsetPolylineSides}; `sign` picks which. */
|
|
156
165
|
function offsetPolylineWithOffsets(points, offsets, rounded, segments, sign) {
|
|
157
|
-
if (points.length < 2) return
|
|
158
|
-
|
|
166
|
+
if (points.length < 2 || offsets.length !== points.length) return {
|
|
167
|
+
edge: points,
|
|
168
|
+
starts: points.map((_, i) => i)
|
|
169
|
+
};
|
|
159
170
|
const result = [];
|
|
160
171
|
const N = points.length;
|
|
172
|
+
const vertexStarts = new Array(N);
|
|
161
173
|
for (let i = 0; i < N; i++) {
|
|
174
|
+
vertexStarts[i] = result.length;
|
|
162
175
|
const p = points[i];
|
|
163
176
|
if (!p) continue;
|
|
164
177
|
const offset = offsets[i] * sign;
|
|
165
178
|
if (i === 0) {
|
|
166
179
|
const next = points[i + 1];
|
|
167
|
-
if (next)
|
|
168
|
-
const dir = vecNorm(vecSub(next, p));
|
|
169
|
-
const normal = [-dir[1], dir[0]];
|
|
170
|
-
result.push(vecAdd(p, vecScale(normal, offset)));
|
|
171
|
-
}
|
|
180
|
+
if (next) result.push(vecAdd(p, vecScale(segmentNormal(p, next), offset)));
|
|
172
181
|
continue;
|
|
173
182
|
}
|
|
174
183
|
if (i === N - 1) {
|
|
175
184
|
const prev = points[i - 1];
|
|
176
|
-
if (prev)
|
|
177
|
-
const dir = vecNorm(vecSub(p, prev));
|
|
178
|
-
const normal = [-dir[1], dir[0]];
|
|
179
|
-
result.push(vecAdd(p, vecScale(normal, offset)));
|
|
180
|
-
}
|
|
185
|
+
if (prev) result.push(vecAdd(p, vecScale(segmentNormal(prev, p), offset)));
|
|
181
186
|
continue;
|
|
182
187
|
}
|
|
183
188
|
const prev = points[i - 1];
|
|
@@ -185,10 +190,9 @@ function offsetPolylineWithOffsets(points, offsets, rounded, segments, sign) {
|
|
|
185
190
|
if (!prev || !next) continue;
|
|
186
191
|
const v1 = vecNorm(vecSub(p, prev));
|
|
187
192
|
const v2 = vecNorm(vecSub(next, p));
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (rounded && (offset > 0 && cross < -1e-6 || offset < 0 && cross > 1e-6)) {
|
|
193
|
+
if (rounded && isOuterJoin(v1, v2, offset)) {
|
|
194
|
+
const n1 = [-v1[1], v1[0]];
|
|
195
|
+
const n2 = [-v2[1], v2[0]];
|
|
192
196
|
const angle1 = Math.atan2(n1[1], n1[0]);
|
|
193
197
|
let diff = Math.atan2(n2[1], n2[0]) - angle1;
|
|
194
198
|
if (offset > 0) {
|
|
@@ -207,27 +211,154 @@ function offsetPolylineWithOffsets(points, offsets, rounded, segments, sign) {
|
|
|
207
211
|
result.push([p[0] + nx * offset, p[1] + ny * offset]);
|
|
208
212
|
}
|
|
209
213
|
} else {
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
const dot = vecDot(miter, n1);
|
|
213
|
-
const miterLimit = 3;
|
|
214
|
-
const scaleFactor = Math.abs(dot) < 1e-6 ? 1 : 1 / dot;
|
|
215
|
-
const normal = vecScale(miter, Math.min(scaleFactor, miterLimit));
|
|
214
|
+
const { miter, scale } = miterJoin(v1, v2);
|
|
215
|
+
const normal = vecScale(miter, scale);
|
|
216
216
|
result.push(vecAdd(p, vecScale(normal, offset)));
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
|
+
const folded = foldedSegments(points, result, vertexStarts);
|
|
220
|
+
if (folded.length === 0) return {
|
|
221
|
+
edge: result,
|
|
222
|
+
starts: vertexStarts
|
|
223
|
+
};
|
|
224
|
+
let maxOffset = 0;
|
|
225
|
+
for (const offset of offsets) maxOffset = Math.max(maxOffset, Math.abs(offset));
|
|
226
|
+
return {
|
|
227
|
+
edge: trimFolds(result, folded, vertexStarts, FOLD_REACH_OFFSETS * MITER_LIMIT * maxOffset),
|
|
228
|
+
starts: vertexStarts
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Edge segments that run against the centerline. The segment from vertex
|
|
233
|
+
* `i - 1`'s last edge point to vertex `i`'s first offsets centerline segment
|
|
234
|
+
* `i - 1 → i`; where the bend is tighter than the offset it points backwards
|
|
235
|
+
* and the edge folds over itself.
|
|
236
|
+
*/
|
|
237
|
+
function foldedSegments(points, edge, vertexStarts) {
|
|
238
|
+
const folded = [];
|
|
239
|
+
for (let i = 1; i < points.length; i++) {
|
|
240
|
+
const k = vertexStarts[i] - 1;
|
|
241
|
+
const from = edge[k];
|
|
242
|
+
const to = edge[k + 1];
|
|
243
|
+
if (!from || !to) continue;
|
|
244
|
+
if (vecDot(vecSub(to, from), vecSub(points[i], points[i - 1])) < 0) folded.push(k);
|
|
245
|
+
}
|
|
246
|
+
return folded;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Cuts the loop each fold makes: the edge runs back across itself, so the
|
|
250
|
+
* nearest pair of segments around a folded run that cross are joined at their
|
|
251
|
+
* crossing and everything between them dropped. The inner edge then comes to
|
|
252
|
+
* a point in a tight bend instead of looping. Edges without folds, and
|
|
253
|
+
* folds that never cross back, are returned unchanged; the two end points are
|
|
254
|
+
* always kept. `starts` (edge indices, as in {@link offsetPolylineSides}) is
|
|
255
|
+
* updated in place to follow the cuts; a start whose point was cut moves to
|
|
256
|
+
* just past the crossing, which replaces it.
|
|
257
|
+
*/
|
|
258
|
+
function trimFolds(edge, folded, starts, reach) {
|
|
259
|
+
let result = edge;
|
|
260
|
+
let limit = edge.length - 1;
|
|
261
|
+
for (let r = folded.length - 1; r >= 0; r--) {
|
|
262
|
+
const last = folded[r];
|
|
263
|
+
let first = last;
|
|
264
|
+
while (r > 0 && folded[r - 1] === first - 1) first = folded[--r];
|
|
265
|
+
if (last >= limit) continue;
|
|
266
|
+
const cut = foldCrossing(result, first, last, limit, reach);
|
|
267
|
+
if (!cut) continue;
|
|
268
|
+
result = [
|
|
269
|
+
...result.slice(0, cut.from + 1),
|
|
270
|
+
cut.point,
|
|
271
|
+
...result.slice(cut.to + 1)
|
|
272
|
+
];
|
|
273
|
+
for (let i = 0; i < starts.length; i++) {
|
|
274
|
+
const start = starts[i];
|
|
275
|
+
if (start > cut.to) starts[i] = start - (cut.to - cut.from) + 1;
|
|
276
|
+
else if (start > cut.from) starts[i] = cut.from + 2;
|
|
277
|
+
}
|
|
278
|
+
limit = cut.from;
|
|
279
|
+
}
|
|
219
280
|
return result;
|
|
220
281
|
}
|
|
221
|
-
/**
|
|
222
|
-
|
|
223
|
-
|
|
282
|
+
/** Edge length, in offsets, within which {@link foldCrossing} looks for a fold's loop. */
|
|
283
|
+
const FOLD_REACH_OFFSETS = 4;
|
|
284
|
+
/**
|
|
285
|
+
* The crossing that closes the loop around folded segments `first … last`:
|
|
286
|
+
* segment `from` at or before the run crossing segment `to` at or after it,
|
|
287
|
+
* picking the tightest such pair. Only segments within `reach` of edge length
|
|
288
|
+
* of the run are tried, which keeps the search local on densely sampled
|
|
289
|
+
* curves.
|
|
290
|
+
*/
|
|
291
|
+
function foldCrossing(edge, first, last, limit, reach) {
|
|
292
|
+
let best = null;
|
|
293
|
+
let back = 0;
|
|
294
|
+
for (let from = first; from >= 0; from--) {
|
|
295
|
+
if (from < first - 1) back += vecMag(vecSub(edge[from + 2], edge[from + 1]));
|
|
296
|
+
if (back > reach) break;
|
|
297
|
+
let ahead = 0;
|
|
298
|
+
for (let to = Math.max(last, from + 2); to < limit; to++) {
|
|
299
|
+
if (to > last + 1) ahead += vecMag(vecSub(edge[to], edge[to - 1]));
|
|
300
|
+
if (ahead > reach) break;
|
|
301
|
+
if (best && to - from >= best.to - best.from) break;
|
|
302
|
+
const point = lineIntersection(edge[from], edge[from + 1], edge[to], edge[to + 1]);
|
|
303
|
+
if (point) best = {
|
|
304
|
+
from,
|
|
305
|
+
to,
|
|
306
|
+
point
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (best && last - from >= best.to - best.from) break;
|
|
310
|
+
}
|
|
311
|
+
return best;
|
|
312
|
+
}
|
|
313
|
+
const MITER_LIMIT = 3;
|
|
314
|
+
/** Unit left normal of the segment `a → b`. */
|
|
315
|
+
function segmentNormal(a, b) {
|
|
316
|
+
const dir = vecNorm(vecSub(b, a));
|
|
317
|
+
return [-dir[1], dir[0]];
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The unit bisector normal at a join of unit directions `v1` → `v2`, and the
|
|
321
|
+
* miter-limited factor that scales it to one unit of perpendicular offset.
|
|
322
|
+
*/
|
|
323
|
+
function miterJoin(v1, v2) {
|
|
324
|
+
const tangent = vecNorm(vecAdd(v1, v2));
|
|
325
|
+
const miter = [-tangent[1], tangent[0]];
|
|
326
|
+
const dot = vecDot(miter, [-v1[1], v1[0]]);
|
|
327
|
+
const scaleFactor = Math.abs(dot) < 1e-6 ? 1 : 1 / dot;
|
|
328
|
+
return {
|
|
329
|
+
miter,
|
|
330
|
+
scale: Math.min(scaleFactor, MITER_LIMIT)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/** Whether an offset of sign `side` lies on the outside of the `v1` → `v2` turn. */
|
|
334
|
+
function isOuterJoin(v1, v2, side) {
|
|
335
|
+
const cross = v1[0] * v2[1] - v1[1] * v2[0];
|
|
336
|
+
return side > 0 && cross < -1e-6 || side < 0 && cross > 1e-6;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* The offset vector that {@link offsetPolylineSides} applies per unit half-width
|
|
340
|
+
* at vertex `index` on side `sign` (`1` left, `-1` right): the end-segment
|
|
341
|
+
* normal at the ends, the capped miter at interior vertices, and the unit
|
|
342
|
+
* bisector on the outer side of a rounded join (the midpoint of its arc). So
|
|
343
|
+
* `vertex + direction * halfWidth` lies on the drawn edge.
|
|
344
|
+
*/
|
|
345
|
+
function offsetDirectionAt(points, index, sign, rounded) {
|
|
346
|
+
const p = points[index];
|
|
347
|
+
if (!p || points.length < 2) return null;
|
|
348
|
+
const prev = points[index - 1];
|
|
349
|
+
const next = points[index + 1];
|
|
350
|
+
if (!prev) return vecScale(segmentNormal(p, next), sign);
|
|
351
|
+
if (!next) return vecScale(segmentNormal(prev, p), sign);
|
|
352
|
+
const v1 = vecNorm(vecSub(p, prev));
|
|
353
|
+
const v2 = vecNorm(vecSub(next, p));
|
|
354
|
+
const { miter, scale } = miterJoin(v1, v2);
|
|
355
|
+
return vecScale(miter, rounded && isOuterJoin(v1, v2, sign) ? sign : scale * sign);
|
|
356
|
+
}
|
|
357
|
+
/** Cumulative arc length at each polyline vertex (`0` at the first). */
|
|
358
|
+
function cumulativeDistances(points) {
|
|
359
|
+
const distances = points.length > 0 ? [0] : [];
|
|
224
360
|
for (let i = 1; i < points.length; i++) distances.push(distances[i - 1] + vecMag(vecSub(points[i], points[i - 1])));
|
|
225
|
-
|
|
226
|
-
if (totalLength < 1e-6) return points.map(() => endValue);
|
|
227
|
-
return distances.map((distance) => {
|
|
228
|
-
const progress = distance / totalLength;
|
|
229
|
-
return startValue + (endValue - startValue) * progress;
|
|
230
|
-
});
|
|
361
|
+
return distances;
|
|
231
362
|
}
|
|
232
363
|
function vecAdd(a, b) {
|
|
233
364
|
return [a[0] + b[0], a[1] + b[1]];
|
|
@@ -1353,11 +1484,464 @@ const supportByFireDrawRule = {
|
|
|
1353
1484
|
}
|
|
1354
1485
|
};
|
|
1355
1486
|
//#endregion
|
|
1487
|
+
//#region src/internal/sketch.ts
|
|
1488
|
+
/**
|
|
1489
|
+
* Projects `coordinates` and derives the arrow's tip, heading and total length.
|
|
1490
|
+
* Returns `null` for degenerate input (fewer than two points, a zero-length
|
|
1491
|
+
* path, or a zero-length final segment) so the caller can decide what to emit.
|
|
1492
|
+
*/
|
|
1493
|
+
function arrowAxis(coordinates) {
|
|
1494
|
+
if (coordinates.length < 2) return null;
|
|
1495
|
+
const pts = coordinates.map((c) => project(c[0], c[1]));
|
|
1496
|
+
let pathLength = 0;
|
|
1497
|
+
for (let i = 0; i < pts.length - 1; i++) pathLength += Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]);
|
|
1498
|
+
const tip = pts[pts.length - 1];
|
|
1499
|
+
const prev = pts[pts.length - 2];
|
|
1500
|
+
const hx = tip[0] - prev[0];
|
|
1501
|
+
const hy = tip[1] - prev[1];
|
|
1502
|
+
const segLength = Math.hypot(hx, hy);
|
|
1503
|
+
if (segLength === 0 || pathLength === 0) return null;
|
|
1504
|
+
const dir = [hx / segLength, hy / segLength];
|
|
1505
|
+
const perp = [-dir[1], dir[0]];
|
|
1506
|
+
return {
|
|
1507
|
+
pts,
|
|
1508
|
+
pathLength,
|
|
1509
|
+
tip,
|
|
1510
|
+
dir,
|
|
1511
|
+
perp,
|
|
1512
|
+
segLength
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Clamps and rounds a smooth-resolution option (samples per segment) onto
|
|
1517
|
+
* `[MIN_SMOOTH_RESOLUTION, MAX_SMOOTH_RESOLUTION]`, falling back to `fallback`
|
|
1518
|
+
* for non-finite input. The default varies per measure, so it's passed in.
|
|
1519
|
+
*/
|
|
1520
|
+
function normalizeSmoothResolution$1(value, fallback) {
|
|
1521
|
+
if (!Number.isFinite(value)) return fallback;
|
|
1522
|
+
return Math.min(64, Math.max(2, Math.round(value)));
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Shared param descriptors for {@link SmoothLineOptions}, modeled on
|
|
1526
|
+
* `SMOOTH_PATH_PARAMS` (cm99-generic-graphics/params.ts). Measures that add
|
|
1527
|
+
* their own params should spread these at the top of the array.
|
|
1528
|
+
*/
|
|
1529
|
+
const SMOOTH_LINE_PARAMS = [{
|
|
1530
|
+
key: "smooth",
|
|
1531
|
+
label: "Smooth",
|
|
1532
|
+
description: "Round the line's corners by curving the path through the control points.",
|
|
1533
|
+
type: "boolean"
|
|
1534
|
+
}, {
|
|
1535
|
+
key: "smoothResolution",
|
|
1536
|
+
presentationTier: "advanced",
|
|
1537
|
+
label: "Smooth resolution",
|
|
1538
|
+
description: "Number of samples per segment when smooth mode is enabled.",
|
|
1539
|
+
type: "number",
|
|
1540
|
+
min: 2,
|
|
1541
|
+
max: 64,
|
|
1542
|
+
step: 1,
|
|
1543
|
+
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
1544
|
+
}];
|
|
1545
|
+
/**
|
|
1546
|
+
* Applies {@link SmoothLineOptions} to projected vertices `verts`: runs
|
|
1547
|
+
* {@link catmullRom} at the resolved (clamped) sample resolution when `smooth`
|
|
1548
|
+
* is set, else returns `verts` unchanged.
|
|
1549
|
+
*/
|
|
1550
|
+
function smoothLineVerts(verts, options, defaultResolution) {
|
|
1551
|
+
return options.smooth ? catmullRom(verts, normalizeSmoothResolution$1(options.smoothResolution ?? defaultResolution, defaultResolution)) : verts;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Resamples a path into equal arc-length segments.
|
|
1555
|
+
*
|
|
1556
|
+
* This is useful for a repeated motif (such as a fortified tooth or FLOT
|
|
1557
|
+
* scallop) after smoothing. Catmull-Rom samples are evenly spaced in spline
|
|
1558
|
+
* parameter, not distance, so using them directly as motif boundaries causes
|
|
1559
|
+
* the motifs to bunch up where the curve's samples are closer together.
|
|
1560
|
+
*/
|
|
1561
|
+
function evenlySpacePath(points, targetSegmentLength) {
|
|
1562
|
+
if (points.length < 2) return points;
|
|
1563
|
+
const segmentLengths = [];
|
|
1564
|
+
let totalLength = 0;
|
|
1565
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1566
|
+
const length = vecMag(vecSub(points[i + 1], points[i]));
|
|
1567
|
+
segmentLengths.push(length);
|
|
1568
|
+
totalLength += length;
|
|
1569
|
+
}
|
|
1570
|
+
if (totalLength < 1e-6) return [points[0]];
|
|
1571
|
+
const segmentCount = Math.max(1, Math.round(totalLength / targetSegmentLength));
|
|
1572
|
+
const segmentLength = totalLength / segmentCount;
|
|
1573
|
+
const out = [points[0]];
|
|
1574
|
+
let sourceIndex = 0;
|
|
1575
|
+
let sourceStart = points[0];
|
|
1576
|
+
let consumed = 0;
|
|
1577
|
+
for (let segment = 1; segment < segmentCount; segment++) {
|
|
1578
|
+
const target = segment * segmentLength;
|
|
1579
|
+
while (sourceIndex < segmentLengths.length - 1 && consumed + segmentLengths[sourceIndex] < target) {
|
|
1580
|
+
consumed += segmentLengths[sourceIndex];
|
|
1581
|
+
sourceIndex++;
|
|
1582
|
+
sourceStart = points[sourceIndex];
|
|
1583
|
+
}
|
|
1584
|
+
const length = segmentLengths[sourceIndex];
|
|
1585
|
+
if (length < 1e-6) continue;
|
|
1586
|
+
const t = (target - consumed) / length;
|
|
1587
|
+
const sourceEnd = points[sourceIndex + 1];
|
|
1588
|
+
out.push([sourceStart[0] + (sourceEnd[0] - sourceStart[0]) * t, sourceStart[1] + (sourceEnd[1] - sourceStart[1]) * t]);
|
|
1589
|
+
}
|
|
1590
|
+
out.push(points[points.length - 1]);
|
|
1591
|
+
return out;
|
|
1592
|
+
}
|
|
1593
|
+
/** One centripetal Catmull-Rom sample at parameter `t` over control points `p0..p3`. */
|
|
1594
|
+
function catmullRomAt(p0, p1, p2, p3, t) {
|
|
1595
|
+
const t2 = t * t;
|
|
1596
|
+
const t3 = t2 * t;
|
|
1597
|
+
return [.5 * (2 * p1[0] + (-p0[0] + p2[0]) * t + (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 + (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3), .5 * (2 * p1[1] + (-p0[1] + p2[1]) * t + (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 + (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3)];
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Centripetal Catmull-Rom spline through `points`, sampled `samples` times per
|
|
1601
|
+
* segment. Endpoints are duplicated so the curve passes through them, so the
|
|
1602
|
+
* first and last points are preserved exactly. With `samples <= 1` (or fewer
|
|
1603
|
+
* than 3 points) it returns the input unchanged.
|
|
1604
|
+
*/
|
|
1605
|
+
function catmullRom(points, samples) {
|
|
1606
|
+
if (points.length < 3 || samples <= 1) return points;
|
|
1607
|
+
const pts = [
|
|
1608
|
+
points[0],
|
|
1609
|
+
...points,
|
|
1610
|
+
points[points.length - 1]
|
|
1611
|
+
];
|
|
1612
|
+
const out = [points[0]];
|
|
1613
|
+
for (let i = 1; i < pts.length - 2; i++) {
|
|
1614
|
+
const p0 = pts[i - 1];
|
|
1615
|
+
const p1 = pts[i];
|
|
1616
|
+
const p2 = pts[i + 1];
|
|
1617
|
+
const p3 = pts[i + 2];
|
|
1618
|
+
for (let s = 1; s <= samples; s++) out.push(catmullRomAt(p0, p1, p2, p3, s / samples));
|
|
1619
|
+
}
|
|
1620
|
+
return out;
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Closed centripetal Catmull-Rom: smooths a polygon ring through every vertex,
|
|
1624
|
+
* wrapping the control points around the seam. Returns a ring whose first and
|
|
1625
|
+
* last positions coincide (explicit closure). With `samples <= 1` (or fewer
|
|
1626
|
+
* than 3 points) it returns the input unchanged.
|
|
1627
|
+
*/
|
|
1628
|
+
function closedCatmullRom(points, samples) {
|
|
1629
|
+
const n = points.length;
|
|
1630
|
+
if (n < 3 || samples <= 1) return points;
|
|
1631
|
+
const out = [];
|
|
1632
|
+
for (let i = 0; i < n; i++) {
|
|
1633
|
+
const p0 = points[(i - 1 + n) % n];
|
|
1634
|
+
const p1 = points[i];
|
|
1635
|
+
const p2 = points[(i + 1) % n];
|
|
1636
|
+
const p3 = points[(i + 2) % n];
|
|
1637
|
+
for (let s = 0; s < samples; s++) out.push(catmullRomAt(p0, p1, p2, p3, s / samples));
|
|
1638
|
+
}
|
|
1639
|
+
out.push([...out[0]]);
|
|
1640
|
+
return out;
|
|
1641
|
+
}
|
|
1642
|
+
//#endregion
|
|
1643
|
+
//#region src/internal/vertex-widths.ts
|
|
1644
|
+
/** The option key holding each side's per-vertex widths. */
|
|
1645
|
+
const VERTEX_WIDTH_OPTION_KEYS = {
|
|
1646
|
+
left: "vertexLeftWidthRatios",
|
|
1647
|
+
right: "vertexRightWidthRatios"
|
|
1648
|
+
};
|
|
1649
|
+
/** The `vertexAlignedOptions` every variable-width arrow body declares. */
|
|
1650
|
+
const VERTEX_WIDTH_ALIGNED_OPTIONS = [VERTEX_WIDTH_OPTION_KEYS.left, VERTEX_WIDTH_OPTION_KEYS.right];
|
|
1651
|
+
/** Maps a body-spine index (rear = 0) to its Axis1 control-point index. */
|
|
1652
|
+
function spineIndexToControlPoint(spineIndex, controlPointCount) {
|
|
1653
|
+
return controlPointCount - 2 - spineIndex;
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Resolves both sides' per-vertex widths onto the body spine (rear → neck),
|
|
1657
|
+
* clamping each set ratio to `[min, max]`. Returns `null` when no shaft vertex
|
|
1658
|
+
* carries a usable width on either side, which callers treat as "use the plain
|
|
1659
|
+
* rear → shaft taper" so graphics without the options run exactly the code they
|
|
1660
|
+
* ran before.
|
|
1661
|
+
*/
|
|
1662
|
+
function resolveSpineWidthRatios(options, controlPointCount, min, max) {
|
|
1663
|
+
const left = resolveSide(options.vertexLeftWidthRatios, controlPointCount, min, max);
|
|
1664
|
+
const right = resolveSide(options.vertexRightWidthRatios, controlPointCount, min, max);
|
|
1665
|
+
return left || right ? {
|
|
1666
|
+
left,
|
|
1667
|
+
right
|
|
1668
|
+
} : null;
|
|
1669
|
+
}
|
|
1670
|
+
function resolveSide(ratios, controlPointCount, min, max) {
|
|
1671
|
+
if (!ratios?.length) return null;
|
|
1672
|
+
const spineCount = controlPointCount - 2;
|
|
1673
|
+
if (spineCount < 1) return null;
|
|
1674
|
+
let found = false;
|
|
1675
|
+
const resolved = new Array(spineCount).fill(null);
|
|
1676
|
+
for (let j = 0; j < spineCount; j++) {
|
|
1677
|
+
const value = ratios[spineIndexToControlPoint(j, controlPointCount)];
|
|
1678
|
+
if (!isFiniteNumber(value)) continue;
|
|
1679
|
+
resolved[j] = clamp(value, min, max);
|
|
1680
|
+
found = true;
|
|
1681
|
+
}
|
|
1682
|
+
return found ? resolved : null;
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* One value per polyline vertex, given each vertex's cumulative arc length
|
|
1686
|
+
* (see {@link cumulativeDistances}): pinned to `startValue` / `endValue` at the
|
|
1687
|
+
* two ends, taken from `setValues` where set, and interpolated linearly by arc
|
|
1688
|
+
* length between set neighbours elsewhere. `setValues` is parallel to the
|
|
1689
|
+
* vertices; its first and last entries are ignored and missing entries count
|
|
1690
|
+
* as unset.
|
|
1691
|
+
*/
|
|
1692
|
+
function anchoredPolylineValues(distances, startValue, endValue, setValues) {
|
|
1693
|
+
const n = distances.length;
|
|
1694
|
+
if (n === 0) return [];
|
|
1695
|
+
if (n === 1) return [endValue];
|
|
1696
|
+
const values = new Array(n);
|
|
1697
|
+
values[0] = startValue;
|
|
1698
|
+
let previous = 0;
|
|
1699
|
+
for (let j = 1; j < n; j++) {
|
|
1700
|
+
const set = j === n - 1 ? endValue : setValues[j];
|
|
1701
|
+
if (set === null || set === void 0) continue;
|
|
1702
|
+
values[j] = set;
|
|
1703
|
+
const from = values[previous];
|
|
1704
|
+
const span = distances[j] - distances[previous];
|
|
1705
|
+
for (let k = previous + 1; k < j; k++) {
|
|
1706
|
+
const t = span < 1e-6 ? 1 : (distances[k] - distances[previous]) / span;
|
|
1707
|
+
values[k] = from + (set - from) * t;
|
|
1708
|
+
}
|
|
1709
|
+
previous = j;
|
|
1710
|
+
}
|
|
1711
|
+
return values;
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Linearly reads per-vertex `values` at arc-length fraction `t`, given each
|
|
1715
|
+
* vertex's cumulative arc length (see {@link cumulativeDistances}).
|
|
1716
|
+
*/
|
|
1717
|
+
function valueAtFraction(distances, values, t) {
|
|
1718
|
+
const n = Math.min(distances.length, values.length);
|
|
1719
|
+
if (n === 0) return 0;
|
|
1720
|
+
if (n === 1) return values[0];
|
|
1721
|
+
const target = clamp(t, 0, 1) * distances[n - 1];
|
|
1722
|
+
for (let i = 1; i < n; i++) if (target <= distances[i]) {
|
|
1723
|
+
const length = distances[i] - distances[i - 1];
|
|
1724
|
+
const f = length < 1e-6 ? 1 : (target - distances[i - 1]) / length;
|
|
1725
|
+
return values[i - 1] + (values[i] - values[i - 1]) * f;
|
|
1726
|
+
}
|
|
1727
|
+
return values[n - 1];
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Resamples an authored shaft centerline onto a Catmull-Rom curve with
|
|
1731
|
+
* `samples` drawn points per segment, so authored vertex `j` lands at drawn
|
|
1732
|
+
* index `j * stride`. Fewer than three vertices or two samples leave the spine
|
|
1733
|
+
* as authored, with a stride of 1.
|
|
1734
|
+
*/
|
|
1735
|
+
function curveSpine(authored, samples) {
|
|
1736
|
+
if (authored.length < 3 || samples < 2) return {
|
|
1737
|
+
spine: authored,
|
|
1738
|
+
stride: 1
|
|
1739
|
+
};
|
|
1740
|
+
return {
|
|
1741
|
+
spine: catmullRom(authored, samples),
|
|
1742
|
+
stride: samples
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Outlines a shaft whose two sides each run from the rear half-width to the
|
|
1747
|
+
* base half-width, following their own set ratios in between. Only the
|
|
1748
|
+
* authored vertices before the shaft end may carry a set width (the head may
|
|
1749
|
+
* have swallowed the rest; `ratios` is a longer prefix then). A side's own rear
|
|
1750
|
+
* entry overrides the shared rear width.
|
|
1751
|
+
*
|
|
1752
|
+
* With `stride` 1 widths change linearly between the authored vertices; on a
|
|
1753
|
+
* resampled curve they ease smoothly between set vertices (see
|
|
1754
|
+
* {@link smoothAnchoredValues}). Without ratios both sides share the plain
|
|
1755
|
+
* rear → base taper.
|
|
1756
|
+
*/
|
|
1757
|
+
function variableWidthShaft(input) {
|
|
1758
|
+
const { spine, stride, rearHalfWidth, baseHalfWidth, ratios, ratioToHalfWidth } = input;
|
|
1759
|
+
const distances = cumulativeDistances(spine);
|
|
1760
|
+
const setCount = (spine.length - 1) / stride;
|
|
1761
|
+
const side = (sideRatios) => {
|
|
1762
|
+
const set = [];
|
|
1763
|
+
for (let j = 0; j < setCount; j++) {
|
|
1764
|
+
const ratio = sideRatios?.[j];
|
|
1765
|
+
set.push(ratio === null || ratio === void 0 ? null : ratio * ratioToHalfWidth);
|
|
1766
|
+
}
|
|
1767
|
+
const rear = set[0] ?? rearHalfWidth;
|
|
1768
|
+
return stride === 1 ? anchoredPolylineValues(distances, rear, baseHalfWidth, set) : smoothAnchoredValues(distances, rear, baseHalfWidth, set, stride);
|
|
1769
|
+
};
|
|
1770
|
+
const left = side(ratios?.left);
|
|
1771
|
+
const right = ratios ? side(ratios.right) : left;
|
|
1772
|
+
const edges = offsetPolylineSides(spine, left, input.rounded, input.roundSegments, right);
|
|
1773
|
+
return {
|
|
1774
|
+
leftEdge: edges.left,
|
|
1775
|
+
rightEdge: edges.right,
|
|
1776
|
+
profile: {
|
|
1777
|
+
spine,
|
|
1778
|
+
stride,
|
|
1779
|
+
left,
|
|
1780
|
+
right,
|
|
1781
|
+
ratios,
|
|
1782
|
+
rounded: input.rounded,
|
|
1783
|
+
edgeStarts: {
|
|
1784
|
+
left: edges.leftStarts,
|
|
1785
|
+
right: edges.rightStarts
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
const REAR_GRIP_SIDES = ["left"];
|
|
1791
|
+
const VERTEX_GRIP_SIDES = ["left", "right"];
|
|
1792
|
+
/** Authored vertices behind a profile's drawn spine (rear … shaft end). */
|
|
1793
|
+
function authoredCount(profile) {
|
|
1794
|
+
return (profile.spine.length - 1) / profile.stride + 1;
|
|
1795
|
+
}
|
|
1796
|
+
function vertexWidthGrip(profile, controlPointCount, side, spineIndex) {
|
|
1797
|
+
const index = spineIndex * profile.stride;
|
|
1798
|
+
const vertex = profile.spine[index];
|
|
1799
|
+
const halfWidth = profile[side][index];
|
|
1800
|
+
if (!vertex || halfWidth === void 0) return null;
|
|
1801
|
+
const direction = offsetDirectionAt(profile.spine, index, side === "left" ? 1 : -1, profile.rounded);
|
|
1802
|
+
if (!direction) return null;
|
|
1803
|
+
return {
|
|
1804
|
+
controlPointIndex: spineIndexToControlPoint(spineIndex, controlPointCount),
|
|
1805
|
+
side,
|
|
1806
|
+
vertex,
|
|
1807
|
+
direction,
|
|
1808
|
+
halfWidth,
|
|
1809
|
+
set: isFiniteNumber(profile.ratios?.[side]?.[spineIndex])
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* The width grips for a shaft: a left and a right grip per interior vertex that
|
|
1814
|
+
* came from a control point, each on its own edge, plus a left grip at the rear
|
|
1815
|
+
* (the shared `rear-width` handle already sits on the right rear corner and
|
|
1816
|
+
* moves both sides until the left is set). The shaft end belongs to
|
|
1817
|
+
* `shaftWidthRatio` and gets none.
|
|
1818
|
+
*/
|
|
1819
|
+
function buildVertexWidthGrips(profile, controlPointCount) {
|
|
1820
|
+
const grips = [];
|
|
1821
|
+
const count = authoredCount(profile);
|
|
1822
|
+
for (let j = 0; j < count - 1; j++) for (const side of j === 0 ? REAR_GRIP_SIDES : VERTEX_GRIP_SIDES) {
|
|
1823
|
+
const grip = vertexWidthGrip(profile, controlPointCount, side, j);
|
|
1824
|
+
if (grip) grips.push(grip);
|
|
1825
|
+
}
|
|
1826
|
+
return grips;
|
|
1827
|
+
}
|
|
1828
|
+
/** The one grip of {@link buildVertexWidthGrips} for `side` at control point `index`. */
|
|
1829
|
+
function findVertexWidthGrip(profile, controlPointCount, side, controlPointIndex) {
|
|
1830
|
+
const j = spineIndexToControlPoint(controlPointIndex, controlPointCount);
|
|
1831
|
+
if (j < 0 || j >= authoredCount(profile) - 1 || j === 0 && side === "right") return null;
|
|
1832
|
+
return vertexWidthGrip(profile, controlPointCount, side, j);
|
|
1833
|
+
}
|
|
1834
|
+
const VERTEX_WIDTH_HANDLE_PREFIX = "vertex-width-";
|
|
1835
|
+
const VERTEX_WIDTH_HANDLE_PATTERN = new RegExp(`^${VERTEX_WIDTH_HANDLE_PREFIX}(left|right)-(\\d+)$`);
|
|
1836
|
+
/** Handle id for the `side` width grip stored under control point `index`. */
|
|
1837
|
+
function vertexWidthHandleId(side, controlPointIndex) {
|
|
1838
|
+
return `${VERTEX_WIDTH_HANDLE_PREFIX}${side}-${controlPointIndex}`;
|
|
1839
|
+
}
|
|
1840
|
+
/** Parses a {@link vertexWidthHandleId}; `null` for any other handle id. */
|
|
1841
|
+
function parseVertexWidthHandleId(handleId) {
|
|
1842
|
+
const match = VERTEX_WIDTH_HANDLE_PATTERN.exec(handleId);
|
|
1843
|
+
if (!match) return null;
|
|
1844
|
+
return {
|
|
1845
|
+
side: match[1],
|
|
1846
|
+
controlPointIndex: Number(match[2])
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Returns `ratios` with entry `index` replaced by `value`, padding with `null`
|
|
1851
|
+
* as needed. A `null` value trims trailing `null`s, and an all-`null` result
|
|
1852
|
+
* collapses to `undefined` so a fully reset option drops out of the options.
|
|
1853
|
+
*/
|
|
1854
|
+
function withVertexWidth(ratios, index, value) {
|
|
1855
|
+
const next = ratios ? [...ratios] : [];
|
|
1856
|
+
while (next.length <= index) next.push(null);
|
|
1857
|
+
next[index] = value;
|
|
1858
|
+
while (next.length > 0 && !isFiniteNumber(next.at(-1))) next.pop();
|
|
1859
|
+
return next.length > 0 ? next : void 0;
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Smooth counterpart of {@link anchoredPolylineValues} for a curved (densely
|
|
1863
|
+
* sampled) spine, given each drawn vertex's cumulative arc length. The knots are the two ends and every set authored vertex;
|
|
1864
|
+
* values in between follow a monotone cubic in arc length, so the width eases
|
|
1865
|
+
* in and out of each set vertex without overshooting (a pinch never dips
|
|
1866
|
+
* below its own width). Authored vertex `j` sits at drawn index `j * stride`;
|
|
1867
|
+
* `setValues` is parallel to the authored vertices.
|
|
1868
|
+
*/
|
|
1869
|
+
function smoothAnchoredValues(distances, startValue, endValue, setValues, stride) {
|
|
1870
|
+
const n = distances.length;
|
|
1871
|
+
if (n === 0) return [];
|
|
1872
|
+
if (n === 1) return [endValue];
|
|
1873
|
+
const xs = [0];
|
|
1874
|
+
const ys = [startValue];
|
|
1875
|
+
for (let j = 1; j * stride < n - 1; j++) {
|
|
1876
|
+
const value = setValues[j];
|
|
1877
|
+
if (value === null || value === void 0) continue;
|
|
1878
|
+
const x = distances[j * stride];
|
|
1879
|
+
if (x - xs.at(-1) < 1e-6) continue;
|
|
1880
|
+
xs.push(x);
|
|
1881
|
+
ys.push(value);
|
|
1882
|
+
}
|
|
1883
|
+
const total = distances[n - 1];
|
|
1884
|
+
if (total - xs.at(-1) < 1e-6) ys[ys.length - 1] = endValue;
|
|
1885
|
+
else {
|
|
1886
|
+
xs.push(total);
|
|
1887
|
+
ys.push(endValue);
|
|
1888
|
+
}
|
|
1889
|
+
if (xs.length < 2) return distances.map(() => endValue);
|
|
1890
|
+
const slopes = monotoneTangents(xs, ys);
|
|
1891
|
+
const values = new Array(n);
|
|
1892
|
+
let k = 0;
|
|
1893
|
+
for (let i = 0; i < n; i++) {
|
|
1894
|
+
const x = distances[i];
|
|
1895
|
+
while (k < xs.length - 2 && x > xs[k + 1]) k++;
|
|
1896
|
+
const h = xs[k + 1] - xs[k];
|
|
1897
|
+
const t = clamp((x - xs[k]) / h, 0, 1);
|
|
1898
|
+
const t2 = t * t;
|
|
1899
|
+
const t3 = t2 * t;
|
|
1900
|
+
values[i] = (2 * t3 - 3 * t2 + 1) * ys[k] + (t3 - 2 * t2 + t) * h * slopes[k] + (-2 * t3 + 3 * t2) * ys[k + 1] + (t3 - t2) * h * slopes[k + 1];
|
|
1901
|
+
}
|
|
1902
|
+
return values;
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Knot tangents for a monotone piecewise-cubic Hermite interpolant
|
|
1906
|
+
* (Fritsch–Butland): zero at local extrema, a weighted harmonic mean of the
|
|
1907
|
+
* neighbouring secants elsewhere, and the adjacent secant at the two ends.
|
|
1908
|
+
*/
|
|
1909
|
+
function monotoneTangents(xs, ys) {
|
|
1910
|
+
const m = xs.length;
|
|
1911
|
+
const secants = [];
|
|
1912
|
+
for (let k = 0; k < m - 1; k++) secants.push((ys[k + 1] - ys[k]) / (xs[k + 1] - xs[k]));
|
|
1913
|
+
const tangents = [secants[0]];
|
|
1914
|
+
for (let k = 1; k < m - 1; k++) {
|
|
1915
|
+
const d0 = secants[k - 1];
|
|
1916
|
+
const d1 = secants[k];
|
|
1917
|
+
if (d0 * d1 <= 0) {
|
|
1918
|
+
tangents.push(0);
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
const h0 = xs[k] - xs[k - 1];
|
|
1922
|
+
const h1 = xs[k + 1] - xs[k];
|
|
1923
|
+
tangents.push(3 * (h0 + h1) / ((2 * h1 + h0) / d0 + (h1 + 2 * h0) / d1));
|
|
1924
|
+
}
|
|
1925
|
+
tangents.push(secants[m - 2]);
|
|
1926
|
+
return tangents;
|
|
1927
|
+
}
|
|
1928
|
+
//#endregion
|
|
1356
1929
|
//#region src/attack-utils.ts
|
|
1357
1930
|
const DEFAULT_SHAFT_WIDTH_RATIO$1 = .6;
|
|
1358
1931
|
const DEFAULT_REAR_WIDTH_RATIO = DEFAULT_SHAFT_WIDTH_RATIO$1;
|
|
1359
1932
|
const SHAFT_MIN_RATIO = .1;
|
|
1360
1933
|
const SHAFT_MAX_RATIO = .9;
|
|
1934
|
+
const DEFAULT_ATTACK_SMOOTH_MODE = "rounded-corners";
|
|
1935
|
+
/** Curve samples per shaft segment for each unit of `smoothResolution`. */
|
|
1936
|
+
const CURVE_SAMPLES_PER_RESOLUTION = 4;
|
|
1937
|
+
/** Shaft defaults shared by every variable-width attack body. */
|
|
1938
|
+
const DEFAULT_ATTACK_SHAFT_OPTIONS = {
|
|
1939
|
+
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO$1,
|
|
1940
|
+
rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
|
|
1941
|
+
smooth: false,
|
|
1942
|
+
smoothMode: DEFAULT_ATTACK_SMOOTH_MODE,
|
|
1943
|
+
smoothResolution: 5
|
|
1944
|
+
};
|
|
1361
1945
|
const REAR_WIDTH_OPTION_HANDLE_ID = "rear-width";
|
|
1362
1946
|
const SHAFT_WIDTH_OPTION_HANDLE_ID = "shaft-width";
|
|
1363
1947
|
/**
|
|
@@ -1368,30 +1952,72 @@ const SHAFT_WIDTH_OPTION_HANDLE_ID = "shaft-width";
|
|
|
1368
1952
|
* matching centerline segment, over the reference half-width) live here once.
|
|
1369
1953
|
*/
|
|
1370
1954
|
function createWidthOptionHandles(config) {
|
|
1955
|
+
const vertexWidthPatch = (options, side, index, value) => {
|
|
1956
|
+
const key = VERTEX_WIDTH_OPTION_KEYS[side];
|
|
1957
|
+
return { [key]: withVertexWidth(options[key], index, value) };
|
|
1958
|
+
};
|
|
1959
|
+
const rightRearOverride = (controlPoints, options) => {
|
|
1960
|
+
const rearIndex = spineIndexToControlPoint(0, controlPoints.length);
|
|
1961
|
+
return isFiniteNumber(options.vertexRightWidthRatios?.[rearIndex]) ? rearIndex : null;
|
|
1962
|
+
};
|
|
1371
1963
|
return {
|
|
1372
|
-
get(controlPoints, options) {
|
|
1964
|
+
get(controlPoints, options, query) {
|
|
1373
1965
|
const geometry = config.geometry(controlPoints, options);
|
|
1374
1966
|
const rearRight = geometry?.rightEdge[0];
|
|
1375
1967
|
const neckRight = geometry?.rightEdge.at(-1);
|
|
1376
|
-
if (!rearRight || !neckRight) return [];
|
|
1377
|
-
|
|
1968
|
+
if (!geometry || !rearRight || !neckRight) return [];
|
|
1969
|
+
const handles = [{
|
|
1378
1970
|
id: REAR_WIDTH_OPTION_HANDLE_ID,
|
|
1379
1971
|
position: unproject(rearRight[0], rearRight[1])
|
|
1380
1972
|
}, {
|
|
1381
1973
|
id: SHAFT_WIDTH_OPTION_HANDLE_ID,
|
|
1382
1974
|
position: unproject(neckRight[0], neckRight[1])
|
|
1383
1975
|
}];
|
|
1976
|
+
if (!query?.detail) return handles;
|
|
1977
|
+
for (const grip of buildVertexWidthGrips(geometry.profile, geometry.controlPointCount)) {
|
|
1978
|
+
const point = vecAdd(grip.vertex, vecScale(grip.direction, grip.halfWidth));
|
|
1979
|
+
handles.push({
|
|
1980
|
+
id: vertexWidthHandleId(grip.side, grip.controlPointIndex),
|
|
1981
|
+
position: unproject(point[0], point[1]),
|
|
1982
|
+
...grip.set ? {} : { unset: true }
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
return handles;
|
|
1384
1986
|
},
|
|
1385
1987
|
drag({ controlPoints, options, handleId, position }) {
|
|
1988
|
+
const target = parseVertexWidthHandleId(handleId);
|
|
1989
|
+
if (target) {
|
|
1990
|
+
const geometry = config.geometry(controlPoints, options);
|
|
1991
|
+
if (!geometry || geometry.referenceHalfWidth < 1e-6) return void 0;
|
|
1992
|
+
const grip = findVertexWidthGrip(geometry.profile, geometry.controlPointCount, target.side, target.controlPointIndex);
|
|
1993
|
+
if (!grip) return void 0;
|
|
1994
|
+
const lengthSq = vecDot(grip.direction, grip.direction);
|
|
1995
|
+
if (lengthSq < 1e-6) return void 0;
|
|
1996
|
+
const ratio = clamp(vecDot(vecSub(project(position[0], position[1]), grip.vertex), grip.direction) / lengthSq / geometry.referenceHalfWidth, config.minRatio, config.maxRearRatio);
|
|
1997
|
+
return vertexWidthPatch(options, target.side, target.controlPointIndex, ratio);
|
|
1998
|
+
}
|
|
1386
1999
|
const rear = handleId === REAR_WIDTH_OPTION_HANDLE_ID;
|
|
1387
2000
|
if (!rear && !(handleId === "shaft-width")) return void 0;
|
|
1388
2001
|
const geometry = config.geometry(controlPoints, options);
|
|
1389
2002
|
if (!geometry || geometry.referenceHalfWidth < 1e-6) return void 0;
|
|
1390
|
-
const
|
|
1391
|
-
const
|
|
2003
|
+
const { spine } = geometry.profile;
|
|
2004
|
+
const from = rear ? spine[0] : spine.at(-2);
|
|
2005
|
+
const to = rear ? spine[1] : spine.at(-1);
|
|
1392
2006
|
if (!from || !to) return void 0;
|
|
1393
2007
|
const ratio = clamp(pointToInfiniteLineDistance(project(position[0], position[1]), from, to) / geometry.referenceHalfWidth, config.minRatio, rear ? config.maxRearRatio : config.maxShaftRatio);
|
|
1394
|
-
|
|
2008
|
+
if (!rear) return { shaftWidthRatio: ratio };
|
|
2009
|
+
const rearIndex = rightRearOverride(controlPoints, options);
|
|
2010
|
+
if (rearIndex !== null) return vertexWidthPatch(options, "right", rearIndex, ratio);
|
|
2011
|
+
return { rearWidthRatio: ratio };
|
|
2012
|
+
},
|
|
2013
|
+
reset({ controlPoints, options, handleId }) {
|
|
2014
|
+
if (handleId === "rear-width") {
|
|
2015
|
+
const rearIndex = rightRearOverride(controlPoints, options);
|
|
2016
|
+
return rearIndex === null ? void 0 : vertexWidthPatch(options, "right", rearIndex, null);
|
|
2017
|
+
}
|
|
2018
|
+
const target = parseVertexWidthHandleId(handleId);
|
|
2019
|
+
if (!target || !options[VERTEX_WIDTH_OPTION_KEYS[target.side]]) return void 0;
|
|
2020
|
+
return vertexWidthPatch(options, target.side, target.controlPointIndex, null);
|
|
1395
2021
|
}
|
|
1396
2022
|
};
|
|
1397
2023
|
}
|
|
@@ -1410,9 +2036,10 @@ function createVariableWidthAttackOptionHandles(resolveCoordinates = (points) =>
|
|
|
1410
2036
|
const outerLeft = geometry?.headRing[0];
|
|
1411
2037
|
if (!geometry || !outerLeft) return null;
|
|
1412
2038
|
return {
|
|
1413
|
-
|
|
2039
|
+
profile: geometry.shaftProfile,
|
|
1414
2040
|
rightEdge: geometry.shaftRight,
|
|
1415
|
-
referenceHalfWidth: vecMag(vecSub(outerLeft, geometry.ptBase))
|
|
2041
|
+
referenceHalfWidth: vecMag(vecSub(outerLeft, geometry.ptBase)),
|
|
2042
|
+
controlPointCount: coordinates.length
|
|
1416
2043
|
};
|
|
1417
2044
|
},
|
|
1418
2045
|
minRatio: SHAFT_MIN_RATIO,
|
|
@@ -1429,11 +2056,48 @@ const variableWidthAttackOptionHandles = createVariableWidthAttackOptionHandles(
|
|
|
1429
2056
|
function resolveRearWidthRatio(options, shaftRatio) {
|
|
1430
2057
|
return options.rearWidthRatio ?? shaftRatio;
|
|
1431
2058
|
}
|
|
2059
|
+
/** The options {@link processAttackGeometry} reads; the cache compares only these. */
|
|
2060
|
+
const ATTACK_GEOMETRY_OPTION_KEYS = [
|
|
2061
|
+
"shaftWidthRatio",
|
|
2062
|
+
"rearWidthRatio",
|
|
2063
|
+
"smooth",
|
|
2064
|
+
"smoothResolution",
|
|
2065
|
+
"smoothMode",
|
|
2066
|
+
"vertexLeftWidthRatios",
|
|
2067
|
+
"vertexRightWidthRatios"
|
|
2068
|
+
];
|
|
2069
|
+
/**
|
|
2070
|
+
* The last body built, by value. A width-grip drag builds the same body up to
|
|
2071
|
+
* three times per pointer move (the drag, the render, the next handle
|
|
2072
|
+
* placement), so one entry is enough. The result is shared, never mutated.
|
|
2073
|
+
*/
|
|
2074
|
+
let lastAttackGeometry = null;
|
|
2075
|
+
function sameOptionValue(a, b) {
|
|
2076
|
+
if (Object.is(a, b)) return true;
|
|
2077
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
2078
|
+
return a.every((value, i) => Object.is(value, b[i]));
|
|
2079
|
+
}
|
|
1432
2080
|
/**
|
|
1433
2081
|
* Processes input coordinates and options to generate the core symbol geometry.
|
|
1434
2082
|
* This handles validation, default values, projection, and geometry calculation.
|
|
1435
2083
|
*/
|
|
1436
2084
|
function processAttackGeometry(coordinates, options = {}) {
|
|
2085
|
+
const last = lastAttackGeometry;
|
|
2086
|
+
if (last && last.coordinates.length === coordinates.length * 2 && coordinates.every((c, i) => c[0] === last.coordinates[2 * i] && c[1] === last.coordinates[2 * i + 1]) && ATTACK_GEOMETRY_OPTION_KEYS.every((key) => sameOptionValue(options[key], last.options[key]))) return last.result;
|
|
2087
|
+
const result = buildAttackGeometry(coordinates, options);
|
|
2088
|
+
const snapshot = {};
|
|
2089
|
+
for (const key of ATTACK_GEOMETRY_OPTION_KEYS) {
|
|
2090
|
+
const value = options[key];
|
|
2091
|
+
snapshot[key] = Array.isArray(value) ? [...value] : value;
|
|
2092
|
+
}
|
|
2093
|
+
lastAttackGeometry = {
|
|
2094
|
+
coordinates: coordinates.flatMap((c) => [c[0], c[1]]),
|
|
2095
|
+
options: snapshot,
|
|
2096
|
+
result
|
|
2097
|
+
};
|
|
2098
|
+
return result;
|
|
2099
|
+
}
|
|
2100
|
+
function buildAttackGeometry(coordinates, options) {
|
|
1437
2101
|
const shaftRatio = clamp(options.shaftWidthRatio ?? .6, SHAFT_MIN_RATIO, SHAFT_MAX_RATIO);
|
|
1438
2102
|
const smooth = options.smooth ?? false;
|
|
1439
2103
|
const smoothResolution = options.smoothResolution ?? 5;
|
|
@@ -1447,9 +2111,9 @@ function processAttackGeometry(coordinates, options = {}) {
|
|
|
1447
2111
|
const p = points[i];
|
|
1448
2112
|
if (p) spinePoints.push(p);
|
|
1449
2113
|
}
|
|
1450
|
-
return { geometry: calculateSymbolGeometry(ptTip, ptWidth, spinePoints, shaftRatio, rearRatio, smooth, smoothResolution) };
|
|
2114
|
+
return { geometry: calculateSymbolGeometry(ptTip, ptWidth, spinePoints, shaftRatio, rearRatio, smooth ? options.smoothMode ?? "rounded-corners" : null, smoothResolution, resolveSpineWidthRatios(options, numPoints, SHAFT_MIN_RATIO, 3)) };
|
|
1451
2115
|
}
|
|
1452
|
-
function calculateSymbolGeometry(ptTip, ptWidth, spine, shaftRatio, rearWidthRatio,
|
|
2116
|
+
function calculateSymbolGeometry(ptTip, ptWidth, spine, shaftRatio, rearWidthRatio, smoothing, smoothResolution, spineWidthRatios = null) {
|
|
1453
2117
|
const initialNeck = spine[spine.length - 1];
|
|
1454
2118
|
if (!initialNeck) return null;
|
|
1455
2119
|
const initialTipDir = vecNorm(vecSub(ptTip, initialNeck));
|
|
@@ -1502,15 +2166,46 @@ function calculateSymbolGeometry(ptTip, ptWidth, spine, shaftRatio, rearWidthRat
|
|
|
1502
2166
|
outerLeft
|
|
1503
2167
|
];
|
|
1504
2168
|
const fullSpine = [...remainingSpine, shaftEndCenter];
|
|
1505
|
-
const
|
|
2169
|
+
const rearHalfWidth = headHalfWidth * rearWidthRatio;
|
|
2170
|
+
const { spine: drawnSpine, stride } = smoothing === "curve" ? curveSpine(fullSpine, normalizeSmoothResolution$1(smoothResolution * CURVE_SAMPLES_PER_RESOLUTION, 5 * CURVE_SAMPLES_PER_RESOLUTION)) : {
|
|
2171
|
+
spine: fullSpine,
|
|
2172
|
+
stride: 1
|
|
2173
|
+
};
|
|
2174
|
+
const { leftEdge: shaftLeft, rightEdge: shaftRight, profile: shaftProfile } = variableWidthShaft({
|
|
2175
|
+
spine: drawnSpine,
|
|
2176
|
+
stride,
|
|
2177
|
+
rearHalfWidth,
|
|
2178
|
+
baseHalfWidth: shaftHalfWidth,
|
|
2179
|
+
ratios: spineWidthRatios,
|
|
2180
|
+
ratioToHalfWidth: headHalfWidth,
|
|
2181
|
+
rounded: smoothing !== null && !(stride > 1),
|
|
2182
|
+
roundSegments: smoothResolution
|
|
2183
|
+
});
|
|
1506
2184
|
return {
|
|
1507
|
-
shaftCenterline:
|
|
2185
|
+
shaftCenterline: drawnSpine,
|
|
1508
2186
|
shaftLeft,
|
|
1509
2187
|
shaftRight,
|
|
1510
2188
|
headRing,
|
|
1511
2189
|
ptTip,
|
|
1512
2190
|
ptNeck,
|
|
1513
|
-
ptBase
|
|
2191
|
+
ptBase,
|
|
2192
|
+
shaftProfile
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* The two shaft edges cut at the neck, for bodies whose outline crosses over
|
|
2197
|
+
* from the neck to the far side of the head (Airborne Attack, Attack
|
|
2198
|
+
* Helicopter). The edges run on to the head base; this keeps each edge up to
|
|
2199
|
+
* the neck, the last authored vertex before the base, which sits one `stride`
|
|
2200
|
+
* of drawn vertices back.
|
|
2201
|
+
*/
|
|
2202
|
+
function shaftEdgesToNeck(geometry) {
|
|
2203
|
+
const { spine, stride, edgeStarts } = geometry.shaftProfile;
|
|
2204
|
+
const afterNeck = spine.length - stride;
|
|
2205
|
+
const toNeck = (edge, starts) => edge.slice(0, Math.max(1, starts[afterNeck] ?? edge.length));
|
|
2206
|
+
return {
|
|
2207
|
+
left: toNeck(geometry.shaftLeft, edgeStarts.left),
|
|
2208
|
+
right: toNeck(geometry.shaftRight, edgeStarts.right)
|
|
1514
2209
|
};
|
|
1515
2210
|
}
|
|
1516
2211
|
/**
|
|
@@ -1877,162 +2572,6 @@ function keepTextLeftToRight(rotation) {
|
|
|
1877
2572
|
return normalized;
|
|
1878
2573
|
}
|
|
1879
2574
|
//#endregion
|
|
1880
|
-
//#region src/internal/sketch.ts
|
|
1881
|
-
/**
|
|
1882
|
-
* Projects `coordinates` and derives the arrow's tip, heading and total length.
|
|
1883
|
-
* Returns `null` for degenerate input (fewer than two points, a zero-length
|
|
1884
|
-
* path, or a zero-length final segment) so the caller can decide what to emit.
|
|
1885
|
-
*/
|
|
1886
|
-
function arrowAxis(coordinates) {
|
|
1887
|
-
if (coordinates.length < 2) return null;
|
|
1888
|
-
const pts = coordinates.map((c) => project(c[0], c[1]));
|
|
1889
|
-
let pathLength = 0;
|
|
1890
|
-
for (let i = 0; i < pts.length - 1; i++) pathLength += Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]);
|
|
1891
|
-
const tip = pts[pts.length - 1];
|
|
1892
|
-
const prev = pts[pts.length - 2];
|
|
1893
|
-
const hx = tip[0] - prev[0];
|
|
1894
|
-
const hy = tip[1] - prev[1];
|
|
1895
|
-
const segLength = Math.hypot(hx, hy);
|
|
1896
|
-
if (segLength === 0 || pathLength === 0) return null;
|
|
1897
|
-
const dir = [hx / segLength, hy / segLength];
|
|
1898
|
-
const perp = [-dir[1], dir[0]];
|
|
1899
|
-
return {
|
|
1900
|
-
pts,
|
|
1901
|
-
pathLength,
|
|
1902
|
-
tip,
|
|
1903
|
-
dir,
|
|
1904
|
-
perp,
|
|
1905
|
-
segLength
|
|
1906
|
-
};
|
|
1907
|
-
}
|
|
1908
|
-
/**
|
|
1909
|
-
* Clamps and rounds a smooth-resolution option (samples per segment) onto
|
|
1910
|
-
* `[MIN_SMOOTH_RESOLUTION, MAX_SMOOTH_RESOLUTION]`, falling back to `fallback`
|
|
1911
|
-
* for non-finite input. The default varies per measure, so it's passed in.
|
|
1912
|
-
*/
|
|
1913
|
-
function normalizeSmoothResolution$2(value, fallback) {
|
|
1914
|
-
if (!Number.isFinite(value)) return fallback;
|
|
1915
|
-
return Math.min(64, Math.max(2, Math.round(value)));
|
|
1916
|
-
}
|
|
1917
|
-
/**
|
|
1918
|
-
* Shared param descriptors for {@link SmoothLineOptions}, modeled on
|
|
1919
|
-
* `SMOOTH_PATH_PARAMS` (cm99-generic-graphics/params.ts). Measures that add
|
|
1920
|
-
* their own params should spread these at the top of the array.
|
|
1921
|
-
*/
|
|
1922
|
-
const SMOOTH_LINE_PARAMS = [{
|
|
1923
|
-
key: "smooth",
|
|
1924
|
-
label: "Smooth",
|
|
1925
|
-
description: "Round the line's corners by curving the path through the control points.",
|
|
1926
|
-
type: "boolean"
|
|
1927
|
-
}, {
|
|
1928
|
-
key: "smoothResolution",
|
|
1929
|
-
presentationTier: "advanced",
|
|
1930
|
-
label: "Smooth resolution",
|
|
1931
|
-
description: "Number of samples per segment when smooth mode is enabled.",
|
|
1932
|
-
type: "number",
|
|
1933
|
-
min: 2,
|
|
1934
|
-
max: 64,
|
|
1935
|
-
step: 1,
|
|
1936
|
-
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
1937
|
-
}];
|
|
1938
|
-
/**
|
|
1939
|
-
* Applies {@link SmoothLineOptions} to projected vertices `verts`: runs
|
|
1940
|
-
* {@link catmullRom} at the resolved (clamped) sample resolution when `smooth`
|
|
1941
|
-
* is set, else returns `verts` unchanged.
|
|
1942
|
-
*/
|
|
1943
|
-
function smoothLineVerts(verts, options, defaultResolution) {
|
|
1944
|
-
return options.smooth ? catmullRom(verts, normalizeSmoothResolution$2(options.smoothResolution ?? defaultResolution, defaultResolution)) : verts;
|
|
1945
|
-
}
|
|
1946
|
-
/**
|
|
1947
|
-
* Resamples a path into equal arc-length segments.
|
|
1948
|
-
*
|
|
1949
|
-
* This is useful for a repeated motif (such as a fortified tooth or FLOT
|
|
1950
|
-
* scallop) after smoothing. Catmull-Rom samples are evenly spaced in spline
|
|
1951
|
-
* parameter, not distance, so using them directly as motif boundaries causes
|
|
1952
|
-
* the motifs to bunch up where the curve's samples are closer together.
|
|
1953
|
-
*/
|
|
1954
|
-
function evenlySpacePath(points, targetSegmentLength) {
|
|
1955
|
-
if (points.length < 2) return points;
|
|
1956
|
-
const segmentLengths = [];
|
|
1957
|
-
let totalLength = 0;
|
|
1958
|
-
for (let i = 0; i < points.length - 1; i++) {
|
|
1959
|
-
const length = vecMag(vecSub(points[i + 1], points[i]));
|
|
1960
|
-
segmentLengths.push(length);
|
|
1961
|
-
totalLength += length;
|
|
1962
|
-
}
|
|
1963
|
-
if (totalLength < 1e-6) return [points[0]];
|
|
1964
|
-
const segmentCount = Math.max(1, Math.round(totalLength / targetSegmentLength));
|
|
1965
|
-
const segmentLength = totalLength / segmentCount;
|
|
1966
|
-
const out = [points[0]];
|
|
1967
|
-
let sourceIndex = 0;
|
|
1968
|
-
let sourceStart = points[0];
|
|
1969
|
-
let consumed = 0;
|
|
1970
|
-
for (let segment = 1; segment < segmentCount; segment++) {
|
|
1971
|
-
const target = segment * segmentLength;
|
|
1972
|
-
while (sourceIndex < segmentLengths.length - 1 && consumed + segmentLengths[sourceIndex] < target) {
|
|
1973
|
-
consumed += segmentLengths[sourceIndex];
|
|
1974
|
-
sourceIndex++;
|
|
1975
|
-
sourceStart = points[sourceIndex];
|
|
1976
|
-
}
|
|
1977
|
-
const length = segmentLengths[sourceIndex];
|
|
1978
|
-
if (length < 1e-6) continue;
|
|
1979
|
-
const t = (target - consumed) / length;
|
|
1980
|
-
const sourceEnd = points[sourceIndex + 1];
|
|
1981
|
-
out.push([sourceStart[0] + (sourceEnd[0] - sourceStart[0]) * t, sourceStart[1] + (sourceEnd[1] - sourceStart[1]) * t]);
|
|
1982
|
-
}
|
|
1983
|
-
out.push(points[points.length - 1]);
|
|
1984
|
-
return out;
|
|
1985
|
-
}
|
|
1986
|
-
/** One centripetal Catmull-Rom sample at parameter `t` over control points `p0..p3`. */
|
|
1987
|
-
function catmullRomAt(p0, p1, p2, p3, t) {
|
|
1988
|
-
const t2 = t * t;
|
|
1989
|
-
const t3 = t2 * t;
|
|
1990
|
-
return [.5 * (2 * p1[0] + (-p0[0] + p2[0]) * t + (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 + (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3), .5 * (2 * p1[1] + (-p0[1] + p2[1]) * t + (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 + (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3)];
|
|
1991
|
-
}
|
|
1992
|
-
/**
|
|
1993
|
-
* Centripetal Catmull-Rom spline through `points`, sampled `samples` times per
|
|
1994
|
-
* segment. Endpoints are duplicated so the curve passes through them, so the
|
|
1995
|
-
* first and last points are preserved exactly. With `samples <= 1` (or fewer
|
|
1996
|
-
* than 3 points) it returns the input unchanged.
|
|
1997
|
-
*/
|
|
1998
|
-
function catmullRom(points, samples) {
|
|
1999
|
-
if (points.length < 3 || samples <= 1) return points;
|
|
2000
|
-
const pts = [
|
|
2001
|
-
points[0],
|
|
2002
|
-
...points,
|
|
2003
|
-
points[points.length - 1]
|
|
2004
|
-
];
|
|
2005
|
-
const out = [points[0]];
|
|
2006
|
-
for (let i = 1; i < pts.length - 2; i++) {
|
|
2007
|
-
const p0 = pts[i - 1];
|
|
2008
|
-
const p1 = pts[i];
|
|
2009
|
-
const p2 = pts[i + 1];
|
|
2010
|
-
const p3 = pts[i + 2];
|
|
2011
|
-
for (let s = 1; s <= samples; s++) out.push(catmullRomAt(p0, p1, p2, p3, s / samples));
|
|
2012
|
-
}
|
|
2013
|
-
return out;
|
|
2014
|
-
}
|
|
2015
|
-
/**
|
|
2016
|
-
* Closed centripetal Catmull-Rom: smooths a polygon ring through every vertex,
|
|
2017
|
-
* wrapping the control points around the seam. Returns a ring whose first and
|
|
2018
|
-
* last positions coincide (explicit closure). With `samples <= 1` (or fewer
|
|
2019
|
-
* than 3 points) it returns the input unchanged.
|
|
2020
|
-
*/
|
|
2021
|
-
function closedCatmullRom(points, samples) {
|
|
2022
|
-
const n = points.length;
|
|
2023
|
-
if (n < 3 || samples <= 1) return points;
|
|
2024
|
-
const out = [];
|
|
2025
|
-
for (let i = 0; i < n; i++) {
|
|
2026
|
-
const p0 = points[(i - 1 + n) % n];
|
|
2027
|
-
const p1 = points[i];
|
|
2028
|
-
const p2 = points[(i + 1) % n];
|
|
2029
|
-
const p3 = points[(i + 2) % n];
|
|
2030
|
-
for (let s = 0; s < samples; s++) out.push(catmullRomAt(p0, p1, p2, p3, s / samples));
|
|
2031
|
-
}
|
|
2032
|
-
out.push([...out[0]]);
|
|
2033
|
-
return out;
|
|
2034
|
-
}
|
|
2035
|
-
//#endregion
|
|
2036
2575
|
//#region src/internal/line-labels.ts
|
|
2037
2576
|
/**
|
|
2038
2577
|
* Stored label rotation for text running along projected direction `dir`.
|
|
@@ -3879,12 +4418,26 @@ const ATTACK_SHAFT_PARAMS = [
|
|
|
3879
4418
|
key: "smoothResolution",
|
|
3880
4419
|
presentationTier: "advanced",
|
|
3881
4420
|
label: "Smooth resolution",
|
|
3882
|
-
description: "Number of segments approximating each rounded bend when smooth mode is enabled.",
|
|
4421
|
+
description: "Number of segments approximating each rounded bend when smooth mode is enabled. In curve style, each shaft segment is sampled at four times this resolution.",
|
|
3883
4422
|
type: "number",
|
|
3884
4423
|
min: 1,
|
|
3885
4424
|
max: 20,
|
|
3886
4425
|
step: 1,
|
|
3887
4426
|
visibleWhen: (opts) => opts.smooth === true
|
|
4427
|
+
},
|
|
4428
|
+
{
|
|
4429
|
+
key: "smoothMode",
|
|
4430
|
+
label: "Smooth style",
|
|
4431
|
+
description: "Round only the corners where the shaft changes direction, or curve the whole shaft through the control points.",
|
|
4432
|
+
type: "enum",
|
|
4433
|
+
options: [{
|
|
4434
|
+
label: "Rounded corners",
|
|
4435
|
+
value: "rounded-corners"
|
|
4436
|
+
}, {
|
|
4437
|
+
label: "Curve",
|
|
4438
|
+
value: "curve"
|
|
4439
|
+
}],
|
|
4440
|
+
visibleWhen: (opts) => opts.smooth === true
|
|
3888
4441
|
}
|
|
3889
4442
|
];
|
|
3890
4443
|
const FIRE_ARROW_PARAMS = [
|
|
@@ -4122,12 +4675,7 @@ const AREA_TEXT_AMPLIFIERS_NO_ADDITIONAL_INFO = AREA_TEXT_AMPLIFIERS.filter((d)
|
|
|
4122
4675
|
const AREA_TEXT_AMPLIFIERS_DESIGNATION_HOSTILE = AREA_TEXT_AMPLIFIERS.filter((d) => d.key === "T" || d.key === "N");
|
|
4123
4676
|
//#endregion
|
|
4124
4677
|
//#region src/generators/cm15-maneuver-areas/airborneAttack.ts
|
|
4125
|
-
const DEFAULT_AIRBORNE_ATTACK_OPTIONS = {
|
|
4126
|
-
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO$1,
|
|
4127
|
-
rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
|
|
4128
|
-
smooth: false,
|
|
4129
|
-
smoothResolution: 5
|
|
4130
|
-
};
|
|
4678
|
+
const DEFAULT_AIRBORNE_ATTACK_OPTIONS = { ...DEFAULT_ATTACK_SHAFT_OPTIONS };
|
|
4131
4679
|
const AIRBORNE_ATTACK_METADATA = {
|
|
4132
4680
|
id: "airborne-attack",
|
|
4133
4681
|
name: "Airborne Attack",
|
|
@@ -4163,8 +4711,7 @@ function createAirborneAttack(coordinates, options = {}) {
|
|
|
4163
4711
|
type: "FeatureCollection",
|
|
4164
4712
|
features: []
|
|
4165
4713
|
};
|
|
4166
|
-
const shaftLeftToNeck = geometry
|
|
4167
|
-
const shaftRightToNeck = geometry.shaftRight.slice(0, -1);
|
|
4714
|
+
const { left: shaftLeftToNeck, right: shaftRightToNeck } = shaftEdgesToNeck(geometry);
|
|
4168
4715
|
return {
|
|
4169
4716
|
type: "FeatureCollection",
|
|
4170
4717
|
features: [{
|
|
@@ -4190,7 +4737,8 @@ const AIRBORNE_ATTACK = defineControlMeasure({
|
|
|
4190
4737
|
generator: createAirborneAttack,
|
|
4191
4738
|
defaultOptions: DEFAULT_AIRBORNE_ATTACK_OPTIONS,
|
|
4192
4739
|
rule: axis1DrawRule,
|
|
4193
|
-
optionHandles: variableWidthAttackOptionHandles
|
|
4740
|
+
optionHandles: variableWidthAttackOptionHandles,
|
|
4741
|
+
vertexAlignedOptions: VERTEX_WIDTH_ALIGNED_OPTIONS
|
|
4194
4742
|
});
|
|
4195
4743
|
/**
|
|
4196
4744
|
* Reacts to an **input-contract** violation according to `mode`: `throw`
|
|
@@ -5205,7 +5753,7 @@ function projectRingPoints(positions) {
|
|
|
5205
5753
|
*/
|
|
5206
5754
|
function buildClosedRing(positions, smooth, smoothResolution, defaultSmoothResolution) {
|
|
5207
5755
|
const ringPts = projectRingPoints(positions);
|
|
5208
|
-
return smooth ? closedCatmullRom(ringPts, normalizeSmoothResolution$
|
|
5756
|
+
return smooth ? closedCatmullRom(ringPts, normalizeSmoothResolution$1(smoothResolution, defaultSmoothResolution)) : [...ringPts, ringPts[0]];
|
|
5209
5757
|
}
|
|
5210
5758
|
/**
|
|
5211
5759
|
* Walks a closed ring into per-segment metadata (unit direction, outward
|
|
@@ -7337,10 +7885,7 @@ const ATTACK_BY_FIRE = defineControlMeasure({
|
|
|
7337
7885
|
//#endregion
|
|
7338
7886
|
//#region src/generators/cm15-maneuver-areas/attackHelicopter.ts
|
|
7339
7887
|
const DEFAULT_ATTACK_HELICOPTER_OPTIONS = {
|
|
7340
|
-
|
|
7341
|
-
rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
|
|
7342
|
-
smooth: false,
|
|
7343
|
-
smoothResolution: 5,
|
|
7888
|
+
...DEFAULT_ATTACK_SHAFT_OPTIONS,
|
|
7344
7889
|
symbolHeightRatio: .45,
|
|
7345
7890
|
triangleSizeRatio: .25,
|
|
7346
7891
|
bottomBarWidthRatio: 1.5,
|
|
@@ -7423,15 +7968,14 @@ function createAttackHelicopter(coordinates, options = {}) {
|
|
|
7423
7968
|
type: "FeatureCollection",
|
|
7424
7969
|
features: []
|
|
7425
7970
|
};
|
|
7426
|
-
const shaftLeftToNeck = geometry.shaftLeft.slice(0, -1);
|
|
7427
|
-
const shaftRightToNeck = geometry.shaftRight.slice(0, -1);
|
|
7428
7971
|
if (geometry.shaftLeft.length < 2 || geometry.shaftRight.length < 2) return {
|
|
7429
7972
|
type: "FeatureCollection",
|
|
7430
7973
|
features: []
|
|
7431
7974
|
};
|
|
7432
|
-
const
|
|
7975
|
+
const { left: shaftLeftToNeck, right: shaftRightToNeck } = shaftEdgesToNeck(geometry);
|
|
7976
|
+
const pL = shaftLeftToNeck.at(-1);
|
|
7433
7977
|
const pIR = geometry.headRing[3];
|
|
7434
|
-
const pR =
|
|
7978
|
+
const pR = shaftRightToNeck.at(-1);
|
|
7435
7979
|
const pIL = geometry.headRing[5];
|
|
7436
7980
|
if (!pL || !pIR || !pR || !pIL) return {
|
|
7437
7981
|
type: "FeatureCollection",
|
|
@@ -7558,7 +8102,8 @@ const ATTACK_HELICOPTER = defineControlMeasure({
|
|
|
7558
8102
|
generator: createAttackHelicopter,
|
|
7559
8103
|
defaultOptions: DEFAULT_ATTACK_HELICOPTER_OPTIONS,
|
|
7560
8104
|
rule: axis1DrawRule,
|
|
7561
|
-
optionHandles: variableWidthAttackOptionHandles
|
|
8105
|
+
optionHandles: variableWidthAttackOptionHandles,
|
|
8106
|
+
vertexAlignedOptions: VERTEX_WIDTH_ALIGNED_OPTIONS
|
|
7562
8107
|
});
|
|
7563
8108
|
//#endregion
|
|
7564
8109
|
//#region src/generators/cm15-maneuver-areas/battlePosition.ts
|
|
@@ -8322,11 +8867,14 @@ const ROADBLOCK_COMPLETE = definition$1("roadblock-complete", "271204", "complet
|
|
|
8322
8867
|
//#endregion
|
|
8323
8868
|
//#region src/generators/cm99-generic-arrows/blockArrow.ts
|
|
8324
8869
|
const DEFAULT_SMOOTH_RESOLUTION$4 = 12;
|
|
8325
|
-
const MIN_SMOOTH_RESOLUTION$1 = 2;
|
|
8326
|
-
const MAX_SMOOTH_RESOLUTION$1 = 64;
|
|
8327
8870
|
const DEFAULT_SHAFT_WIDTH_RATIO = .06;
|
|
8328
8871
|
const MIN_SHAFT_WIDTH_RATIO = .01;
|
|
8329
8872
|
const MAX_SHAFT_WIDTH_RATIO = .3;
|
|
8873
|
+
/**
|
|
8874
|
+
* Rear and per-vertex widths may flare well past the arrowhead; only the shaft
|
|
8875
|
+
* end, which meets the head, keeps the tighter shaft cap.
|
|
8876
|
+
*/
|
|
8877
|
+
const MAX_FLARE_WIDTH_RATIO = 1;
|
|
8330
8878
|
const DEFAULT_BLOCK_ARROW_OPTIONS = {
|
|
8331
8879
|
shaftWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO,
|
|
8332
8880
|
rearWidthRatio: DEFAULT_SHAFT_WIDTH_RATIO,
|
|
@@ -8335,6 +8883,7 @@ const DEFAULT_BLOCK_ARROW_OPTIONS = {
|
|
|
8335
8883
|
arrowheadLengthRatio: .22,
|
|
8336
8884
|
smooth: false,
|
|
8337
8885
|
smoothResolution: DEFAULT_SMOOTH_RESOLUTION$4,
|
|
8886
|
+
smoothMode: "curve",
|
|
8338
8887
|
filled: true
|
|
8339
8888
|
};
|
|
8340
8889
|
const BLOCK_ARROW_METADATA = {
|
|
@@ -8372,7 +8921,7 @@ const BLOCK_ARROW_METADATA = {
|
|
|
8372
8921
|
description: "Width at the rear of the shaft as a fraction of the total path length",
|
|
8373
8922
|
type: "number",
|
|
8374
8923
|
min: MIN_SHAFT_WIDTH_RATIO,
|
|
8375
|
-
max:
|
|
8924
|
+
max: MAX_FLARE_WIDTH_RATIO,
|
|
8376
8925
|
step: .01
|
|
8377
8926
|
},
|
|
8378
8927
|
{
|
|
@@ -8434,20 +8983,34 @@ const BLOCK_ARROW_METADATA = {
|
|
|
8434
8983
|
{
|
|
8435
8984
|
key: "smooth",
|
|
8436
8985
|
label: "Smooth",
|
|
8437
|
-
description: "
|
|
8986
|
+
description: "Curve the shaft through the control points, or round its corners.",
|
|
8438
8987
|
type: "boolean"
|
|
8439
8988
|
},
|
|
8440
8989
|
{
|
|
8441
8990
|
key: "smoothResolution",
|
|
8442
8991
|
presentationTier: "advanced",
|
|
8443
8992
|
label: "Smooth resolution",
|
|
8444
|
-
description: "Number of samples per segment when smooth mode is enabled",
|
|
8993
|
+
description: "Number of samples per segment when smooth mode is enabled. In rounded-corners style, the number of segments approximating each rounded corner.",
|
|
8445
8994
|
type: "number",
|
|
8446
8995
|
min: 2,
|
|
8447
8996
|
max: 64,
|
|
8448
8997
|
step: 1,
|
|
8449
8998
|
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
8450
8999
|
},
|
|
9000
|
+
{
|
|
9001
|
+
key: "smoothMode",
|
|
9002
|
+
label: "Smooth style",
|
|
9003
|
+
description: "Curve the whole shaft through the control points, or round only the corners where it changes direction.",
|
|
9004
|
+
type: "enum",
|
|
9005
|
+
options: [{
|
|
9006
|
+
label: "Curve",
|
|
9007
|
+
value: "curve"
|
|
9008
|
+
}, {
|
|
9009
|
+
label: "Rounded corners",
|
|
9010
|
+
value: "rounded-corners"
|
|
9011
|
+
}],
|
|
9012
|
+
visibleWhen: (opts) => Boolean(opts.smooth)
|
|
9013
|
+
},
|
|
8451
9014
|
{
|
|
8452
9015
|
key: "filled",
|
|
8453
9016
|
label: "Filled",
|
|
@@ -8555,10 +9118,23 @@ function calculateBlockArrowGeometry(coordinates, options) {
|
|
|
8555
9118
|
];
|
|
8556
9119
|
break;
|
|
8557
9120
|
}
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
const
|
|
8561
|
-
const {
|
|
9121
|
+
const authored = [...pts.slice(0, -1), onAxis(shaftEndDist)];
|
|
9122
|
+
const samples = normalizeSmoothResolution$1(options.smoothResolution, DEFAULT_SMOOTH_RESOLUTION$4);
|
|
9123
|
+
const curved = options.smooth && options.smoothMode === "curve";
|
|
9124
|
+
const { spine, stride } = curved ? curveSpine(authored, samples) : {
|
|
9125
|
+
spine: authored,
|
|
9126
|
+
stride: 1
|
|
9127
|
+
};
|
|
9128
|
+
const { leftEdge: leftSide, rightEdge: rightSide, profile } = variableWidthShaft({
|
|
9129
|
+
spine,
|
|
9130
|
+
stride,
|
|
9131
|
+
rearHalfWidth: halfRear,
|
|
9132
|
+
baseHalfWidth: halfShaft,
|
|
9133
|
+
ratios: resolveSpineWidthRatios(options, coordinates.length, MIN_SHAFT_WIDTH_RATIO, MAX_FLARE_WIDTH_RATIO),
|
|
9134
|
+
ratioToHalfWidth: pathLength / 2,
|
|
9135
|
+
rounded: options.smooth && !curved,
|
|
9136
|
+
roundSegments: samples
|
|
9137
|
+
});
|
|
8562
9138
|
const ring = [
|
|
8563
9139
|
...leftSide,
|
|
8564
9140
|
...headPts,
|
|
@@ -8567,10 +9143,9 @@ function calculateBlockArrowGeometry(coordinates, options) {
|
|
|
8567
9143
|
ring.push(ring[0]);
|
|
8568
9144
|
return {
|
|
8569
9145
|
ring,
|
|
8570
|
-
spine,
|
|
8571
|
-
leftSide,
|
|
8572
9146
|
rightSide,
|
|
8573
|
-
pathLength
|
|
9147
|
+
pathLength,
|
|
9148
|
+
profile
|
|
8574
9149
|
};
|
|
8575
9150
|
}
|
|
8576
9151
|
function resolveBlockArrowOptions(options) {
|
|
@@ -8593,15 +9168,17 @@ const BLOCK_ARROW = defineControlMeasure({
|
|
|
8593
9168
|
const geometry = calculateBlockArrowGeometry(controlPoints, resolveBlockArrowOptions(options));
|
|
8594
9169
|
if (!geometry) return null;
|
|
8595
9170
|
return {
|
|
8596
|
-
|
|
9171
|
+
profile: geometry.profile,
|
|
8597
9172
|
rightEdge: geometry.rightSide,
|
|
8598
|
-
referenceHalfWidth: geometry.pathLength / 2
|
|
9173
|
+
referenceHalfWidth: geometry.pathLength / 2,
|
|
9174
|
+
controlPointCount: controlPoints.length
|
|
8599
9175
|
};
|
|
8600
9176
|
},
|
|
8601
9177
|
minRatio: MIN_SHAFT_WIDTH_RATIO,
|
|
8602
|
-
maxRearRatio:
|
|
9178
|
+
maxRearRatio: MAX_FLARE_WIDTH_RATIO,
|
|
8603
9179
|
maxShaftRatio: MAX_SHAFT_WIDTH_RATIO
|
|
8604
9180
|
}),
|
|
9181
|
+
vertexAlignedOptions: VERTEX_WIDTH_ALIGNED_OPTIONS,
|
|
8605
9182
|
previewSample: {
|
|
8606
9183
|
controlPoints: [
|
|
8607
9184
|
[1.4, 0],
|
|
@@ -8623,10 +9200,6 @@ function axis1Geometry(coordinates) {
|
|
|
8623
9200
|
...metrics && Math.abs(metrics.lateral) > 1e-6 ? { headHalfWidth: Math.abs(metrics.lateral) } : {}
|
|
8624
9201
|
};
|
|
8625
9202
|
}
|
|
8626
|
-
function normalizeSmoothResolution$1(value) {
|
|
8627
|
-
if (!Number.isFinite(value)) return DEFAULT_SMOOTH_RESOLUTION$4;
|
|
8628
|
-
return Math.min(MAX_SMOOTH_RESOLUTION$1, Math.max(MIN_SMOOTH_RESOLUTION$1, Math.round(value)));
|
|
8629
|
-
}
|
|
8630
9203
|
//#endregion
|
|
8631
9204
|
//#region src/internal/line-echelon.ts
|
|
8632
9205
|
/**
|
|
@@ -10265,7 +10838,7 @@ function createGenericLine(coordinates, options = {}) {
|
|
|
10265
10838
|
...DEFAULT_GENERIC_LINE_OPTIONS,
|
|
10266
10839
|
...options
|
|
10267
10840
|
};
|
|
10268
|
-
const resolution = normalizeSmoothResolution$
|
|
10841
|
+
const resolution = normalizeSmoothResolution$1(smoothResolution, 12);
|
|
10269
10842
|
return {
|
|
10270
10843
|
type: "FeatureCollection",
|
|
10271
10844
|
features: [{
|
|
@@ -10326,7 +10899,7 @@ function createGenericPolygon(coordinates, options = {}) {
|
|
|
10326
10899
|
...DEFAULT_GENERIC_POLYGON_OPTIONS,
|
|
10327
10900
|
...options
|
|
10328
10901
|
};
|
|
10329
|
-
const resolution = normalizeSmoothResolution$
|
|
10902
|
+
const resolution = normalizeSmoothResolution$1(smoothResolution, 12);
|
|
10330
10903
|
const ring = smooth ? closedCatmullRom(ringPoints, resolution) : [...ringPoints, ringPoints[0]];
|
|
10331
10904
|
return {
|
|
10332
10905
|
type: "FeatureCollection",
|
|
@@ -11243,10 +11816,7 @@ const DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS = {
|
|
|
11243
11816
|
labelPadding: 0
|
|
11244
11817
|
};
|
|
11245
11818
|
const DEFAULT_MANEUVER_ARROW_TASK_OPTIONS = {
|
|
11246
|
-
|
|
11247
|
-
rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
|
|
11248
|
-
smooth: false,
|
|
11249
|
-
smoothResolution: 5,
|
|
11819
|
+
...DEFAULT_ATTACK_SHAFT_OPTIONS,
|
|
11250
11820
|
crossbarLengthRatio: 1.1,
|
|
11251
11821
|
...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS
|
|
11252
11822
|
};
|
|
@@ -11312,8 +11882,9 @@ function createManeuverArrowTask(coordinates, options, textAmplifiers, config) {
|
|
|
11312
11882
|
const crossbarFrame = pointAlongPolyline(segments, totalLength, shaftPosition);
|
|
11313
11883
|
crossbarCenter = crossbarFrame.point;
|
|
11314
11884
|
crossbarAlong = crossbarFrame.along;
|
|
11315
|
-
const
|
|
11316
|
-
|
|
11885
|
+
const { left, right } = geometry.shaftProfile;
|
|
11886
|
+
const distances = cumulativeDistances(centerline);
|
|
11887
|
+
crossbarBaseWidth = 2 * Math.max(valueAtFraction(distances, left, shaftPosition), valueAtFraction(distances, right, shaftPosition));
|
|
11317
11888
|
}
|
|
11318
11889
|
const crossbarLength = crossbarBaseWidth * Math.max(1, resolved.crossbarLengthRatio);
|
|
11319
11890
|
const crossbarPerp = [-crossbarAlong[1], crossbarAlong[0]];
|
|
@@ -11414,10 +11985,7 @@ const MANEUVER_ARROW_TASK_TEXT_AMPLIFIERS = [
|
|
|
11414
11985
|
//#endregion
|
|
11415
11986
|
//#region src/generators/cm15-maneuver-areas/supportingAttack.ts
|
|
11416
11987
|
const DEFAULT_SUPPORTING_ATTACK_OPTIONS = {
|
|
11417
|
-
|
|
11418
|
-
rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
|
|
11419
|
-
smooth: false,
|
|
11420
|
-
smoothResolution: 5,
|
|
11988
|
+
...DEFAULT_ATTACK_SHAFT_OPTIONS,
|
|
11421
11989
|
...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS
|
|
11422
11990
|
};
|
|
11423
11991
|
const SUPPORTING_ATTACK_METADATA = {
|
|
@@ -11492,6 +12060,7 @@ const SUPPORTING_ATTACK = defineControlMeasure({
|
|
|
11492
12060
|
defaultOptions: DEFAULT_SUPPORTING_ATTACK_OPTIONS,
|
|
11493
12061
|
rule: axis1DrawRule,
|
|
11494
12062
|
optionHandles: variableWidthAttackOptionHandles,
|
|
12063
|
+
vertexAlignedOptions: VERTEX_WIDTH_ALIGNED_OPTIONS,
|
|
11495
12064
|
previewSample: {
|
|
11496
12065
|
controlPoints: [
|
|
11497
12066
|
[1, 0],
|
|
@@ -11510,10 +12079,7 @@ const SUPPORTING_ATTACK = defineControlMeasure({
|
|
|
11510
12079
|
//#endregion
|
|
11511
12080
|
//#region src/generators/cm34-mission-tasks/counterattack.ts
|
|
11512
12081
|
const DEFAULT_COUNTERATTACK_OPTIONS = {
|
|
11513
|
-
|
|
11514
|
-
rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
|
|
11515
|
-
smooth: false,
|
|
11516
|
-
smoothResolution: 5,
|
|
12082
|
+
...DEFAULT_ATTACK_SHAFT_OPTIONS,
|
|
11517
12083
|
labelPosition: 1
|
|
11518
12084
|
};
|
|
11519
12085
|
/** Intrinsic dash and gap lengths for the complete Counterattack outline. */
|
|
@@ -11603,6 +12169,7 @@ const COUNTERATTACK = defineControlMeasure({
|
|
|
11603
12169
|
defaultOptions: DEFAULT_COUNTERATTACK_OPTIONS,
|
|
11604
12170
|
rule: axis1DrawRule,
|
|
11605
12171
|
optionHandles: variableWidthAttackOptionHandles,
|
|
12172
|
+
vertexAlignedOptions: VERTEX_WIDTH_ALIGNED_OPTIONS,
|
|
11606
12173
|
previewSample: {
|
|
11607
12174
|
controlPoints: [
|
|
11608
12175
|
[1, 0],
|
|
@@ -11764,6 +12331,7 @@ const COUNTERATTACK_BY_FIRE = defineControlMeasure({
|
|
|
11764
12331
|
defaultOptions: DEFAULT_COUNTERATTACK_BY_FIRE_OPTIONS,
|
|
11765
12332
|
rule: counterattackByFireDrawRule,
|
|
11766
12333
|
optionHandles: createVariableWidthAttackOptionHandles(counterattackByFireBodyCoordinates),
|
|
12334
|
+
vertexAlignedOptions: VERTEX_WIDTH_ALIGNED_OPTIONS,
|
|
11767
12335
|
previewSample: {
|
|
11768
12336
|
controlPoints: [
|
|
11769
12337
|
[1.35, 0],
|
|
@@ -15402,7 +15970,7 @@ function createFortifiedLine(positions, options = {}, textAmplifiers = {}, conte
|
|
|
15402
15970
|
const { smooth = DEFAULT_FORTIFIED_LINE_OPTIONS.smooth, smoothResolution = DEFAULT_FORTIFIED_LINE_OPTIONS.smoothResolution } = options;
|
|
15403
15971
|
const safeSize = calculateEffectiveSize(options);
|
|
15404
15972
|
const projectedPoints = positions.map((p) => project(p[0], p[1]));
|
|
15405
|
-
const smoothedPath = smooth ? catmullRom(projectedPoints, normalizeSmoothResolution$
|
|
15973
|
+
const smoothedPath = smooth ? catmullRom(projectedPoints, normalizeSmoothResolution$1(smoothResolution, DEFAULT_SMOOTH_RESOLUTION$1)) : projectedPoints;
|
|
15406
15974
|
const feature = {
|
|
15407
15975
|
type: "Feature",
|
|
15408
15976
|
properties: {},
|
|
@@ -15479,7 +16047,7 @@ function createFortifiedArea(positions, options = {}, textAmplifiers = {}, conte
|
|
|
15479
16047
|
const safeSize = calculateEffectiveSize(options);
|
|
15480
16048
|
const projected = positions.map((p) => project(p[0], p[1]));
|
|
15481
16049
|
const ringPts = projected.length > 1 && vecMag(vecSub(projected[0], projected[projected.length - 1])) < 1e-6 ? projected.slice(0, -1) : projected;
|
|
15482
|
-
const boundaryInput = smooth ? evenlySpacePath(closedCatmullRom(ringPts, normalizeSmoothResolution$
|
|
16050
|
+
const boundaryInput = smooth ? evenlySpacePath(closedCatmullRom(ringPts, normalizeSmoothResolution$1(smoothResolution, DEFAULT_FORTIFIED_AREA_OPTIONS.smoothResolution)), 2 * safeSize) : [...ringPts, ringPts[0]];
|
|
15483
16051
|
const boundaryPoints = generateFortifiedPoints(boundaryInput, safeSize);
|
|
15484
16052
|
if (boundaryPoints.length > 0) {
|
|
15485
16053
|
const firstPt = boundaryPoints[0];
|
|
@@ -15559,6 +16127,7 @@ const FRONTAL_ATTACK = defineControlMeasure({
|
|
|
15559
16127
|
defaultOptions: DEFAULT_FRONTAL_ATTACK_OPTIONS,
|
|
15560
16128
|
rule: axis1DrawRule,
|
|
15561
16129
|
optionHandles: variableWidthAttackOptionHandles,
|
|
16130
|
+
vertexAlignedOptions: VERTEX_WIDTH_ALIGNED_OPTIONS,
|
|
15562
16131
|
previewSample: {
|
|
15563
16132
|
controlPoints: [
|
|
15564
16133
|
[1, 0],
|
|
@@ -15624,10 +16193,7 @@ const MOVEMENT_TO_CONTACT_BOLT_HANDLE_ID = "bolt-tip";
|
|
|
15624
16193
|
const MOVEMENT_TO_CONTACT_STEM_SETBACK_RATIO = .22;
|
|
15625
16194
|
const MOVEMENT_TO_CONTACT_STEM_OFFSET_RATIO = .55;
|
|
15626
16195
|
const DEFAULT_MOVEMENT_TO_CONTACT_OPTIONS = {
|
|
15627
|
-
|
|
15628
|
-
rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
|
|
15629
|
-
smooth: false,
|
|
15630
|
-
smoothResolution: 5,
|
|
16196
|
+
...DEFAULT_ATTACK_SHAFT_OPTIONS,
|
|
15631
16197
|
...DEFAULT_TACTICAL_ARROW_OPTIONS,
|
|
15632
16198
|
...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS,
|
|
15633
16199
|
boltLengthRatio: MOVEMENT_TO_CONTACT_BOLT_LENGTH_RATIO,
|
|
@@ -15771,8 +16337,8 @@ const MOVEMENT_TO_CONTACT = defineControlMeasure({
|
|
|
15771
16337
|
defaultOptions: DEFAULT_MOVEMENT_TO_CONTACT_OPTIONS,
|
|
15772
16338
|
rule: axis1DrawRule,
|
|
15773
16339
|
optionHandles: {
|
|
15774
|
-
get(controlPoints, options) {
|
|
15775
|
-
const widthHandles = variableWidthAttackOptionHandles.get(controlPoints, options);
|
|
16340
|
+
get(controlPoints, options, query) {
|
|
16341
|
+
const widthHandles = variableWidthAttackOptionHandles.get(controlPoints, options, query);
|
|
15776
16342
|
const resolved = resolveMovementToContactGeometry(controlPoints, options);
|
|
15777
16343
|
if (!resolved) return widthHandles;
|
|
15778
16344
|
return [...widthHandles, {
|
|
@@ -15793,8 +16359,12 @@ const MOVEMENT_TO_CONTACT = defineControlMeasure({
|
|
|
15793
16359
|
boltLengthRatio: length / resolved.arrowBaseWidth,
|
|
15794
16360
|
boltAngle: clamp(Math.abs(Math.atan2(cross, dot) * 180 / Math.PI), 0, 90)
|
|
15795
16361
|
};
|
|
16362
|
+
},
|
|
16363
|
+
reset(event) {
|
|
16364
|
+
return variableWidthAttackOptionHandles.reset?.(event);
|
|
15796
16365
|
}
|
|
15797
16366
|
},
|
|
16367
|
+
vertexAlignedOptions: VERTEX_WIDTH_ALIGNED_OPTIONS,
|
|
15798
16368
|
previewSample: {
|
|
15799
16369
|
controlPoints: [
|
|
15800
16370
|
[1, 0],
|
|
@@ -17390,10 +17960,7 @@ const NO_FIRE_AREA_IRREGULAR = defineControlMeasure({
|
|
|
17390
17960
|
//#endregion
|
|
17391
17961
|
//#region src/generators/cm15-maneuver-areas/mainAttack.ts
|
|
17392
17962
|
const DEFAULT_MAIN_ATTACK_OPTIONS = {
|
|
17393
|
-
|
|
17394
|
-
rearWidthRatio: DEFAULT_REAR_WIDTH_RATIO,
|
|
17395
|
-
smooth: false,
|
|
17396
|
-
smoothResolution: 5,
|
|
17963
|
+
...DEFAULT_ATTACK_SHAFT_OPTIONS,
|
|
17397
17964
|
...DEFAULT_MANEUVER_ARROW_TASK_AMPLIFIER_OPTIONS
|
|
17398
17965
|
};
|
|
17399
17966
|
const MAIN_ATTACK_METADATA = {
|
|
@@ -17463,6 +18030,7 @@ const MAIN_ATTACK = defineControlMeasure({
|
|
|
17463
18030
|
defaultOptions: DEFAULT_MAIN_ATTACK_OPTIONS,
|
|
17464
18031
|
rule: axis1DrawRule,
|
|
17465
18032
|
optionHandles: variableWidthAttackOptionHandles,
|
|
18033
|
+
vertexAlignedOptions: VERTEX_WIDTH_ALIGNED_OPTIONS,
|
|
17466
18034
|
previewSample: {
|
|
17467
18035
|
controlPoints: [
|
|
17468
18036
|
[1, 0],
|
|
@@ -19935,6 +20503,7 @@ const DEFINITIONS = {
|
|
|
19935
20503
|
defaultOptions: DEFAULT_TURNING_MOVEMENT_OPTIONS,
|
|
19936
20504
|
rule: axis1DrawRule,
|
|
19937
20505
|
optionHandles: variableWidthAttackOptionHandles,
|
|
20506
|
+
vertexAlignedOptions: VERTEX_WIDTH_ALIGNED_OPTIONS,
|
|
19938
20507
|
previewSample: {
|
|
19939
20508
|
controlPoints: [
|
|
19940
20509
|
[1, 0],
|