@rig-c/parts 1.36.0 → 1.38.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/cli.ts +5 -1
- package/docs/AUTHORING.md +21 -3
- package/docs/MIGRATION.md +19 -0
- package/docs/STABILITY.md +2 -2
- package/package.json +2 -2
- package/src/assemble.ts +591 -12
- package/src/build.ts +27 -3
- package/src/check.ts +280 -1
- package/src/config.ts +58 -8
- package/src/propose.ts +128 -15
- package/src/tags.ts +15 -6
package/src/propose.ts
CHANGED
|
@@ -1364,8 +1364,15 @@ const SIDES = [
|
|
|
1364
1364
|
* which rule stood in; without it every value and note is the one this
|
|
1365
1365
|
* function wrote before the parameter existed.
|
|
1366
1366
|
*/
|
|
1367
|
-
export function propose(
|
|
1368
|
-
|
|
1367
|
+
export function propose(
|
|
1368
|
+
P: PartSet,
|
|
1369
|
+
joints?: RigJoints,
|
|
1370
|
+
wristSnap: number | null = null,
|
|
1371
|
+
gridRule: GridRule = DEFAULT_GRID_RULE,
|
|
1372
|
+
cloth: ClothRecipes | null = null,
|
|
1373
|
+
chainRule = true,
|
|
1374
|
+
): Proposal {
|
|
1375
|
+
return proposeWithBasis(P, joints, wristSnap, gridRule, cloth, chainRule).proposal;
|
|
1369
1376
|
}
|
|
1370
1377
|
|
|
1371
1378
|
/**
|
|
@@ -1384,6 +1391,7 @@ export function proposeWithBasis(
|
|
|
1384
1391
|
wristSnap: number | null = null,
|
|
1385
1392
|
gridRule: GridRule = DEFAULT_GRID_RULE,
|
|
1386
1393
|
cloth: ClothRecipes | null = null,
|
|
1394
|
+
chainRule = true,
|
|
1387
1395
|
): { proposal: Proposal; basis: ProposalBasis } {
|
|
1388
1396
|
// Issue #230: a piece is not given its base's rule a second time, which
|
|
1389
1397
|
// needs to know which part is the piece. A file that cannot say is refused
|
|
@@ -2537,7 +2545,7 @@ export function proposeWithBasis(
|
|
|
2537
2545
|
// Issue #75: arms from joints, when every handwear part carries the wrist of
|
|
2538
2546
|
// an arm whose shoulder, elbow and wrist are all given; otherwise the rule
|
|
2539
2547
|
// below places every sleeve chain, and the notes say why.
|
|
2540
|
-
const armPlan = joints !== undefined && clasped === null && hw.length > 0 ? armsFromJoints(P, hw, joints, wristSnap) : null;
|
|
2548
|
+
const armPlan = joints !== undefined && clasped === null && hw.length > 0 ? armsFromJoints(P, hw, joints, wristSnap, chainRule) : null;
|
|
2541
2549
|
let sleeveRule = '';
|
|
2542
2550
|
if (clasped !== null) {
|
|
2543
2551
|
sleeveRule = 'the clasped-hands rule (the handwear is a region on hip, with no sleeve chain)';
|
|
@@ -2547,7 +2555,9 @@ export function proposeWithBasis(
|
|
|
2547
2555
|
for (const p of hw) regions[p.name] = 'hip';
|
|
2548
2556
|
notes.push(clasped);
|
|
2549
2557
|
} else if (armPlan !== null && armPlan.arms !== null) {
|
|
2550
|
-
// Each chain runs shoulder -> elbow with its tip at the wrist
|
|
2558
|
+
// Each chain runs shoulder -> elbow with its tip at the wrist (or, when
|
|
2559
|
+
// the wrist fell outside the arm layer onto a skirt or top layer, at the
|
|
2560
|
+
// arm layer's bottom extent, issue #491): two links,
|
|
2551
2561
|
// the upper arm and the forearm, each with the amplitude the two-blob rule
|
|
2552
2562
|
// gives its link of the same index. Its part's mesh takes the chain alone,
|
|
2553
2563
|
// as the two-blob rule's does since issue #347 (no chest stub: a stub on
|
|
@@ -2555,7 +2565,14 @@ export function proposeWithBasis(
|
|
|
2555
2565
|
for (const a of armPlan.arms) {
|
|
2556
2566
|
const sgn = a.side === 'r' ? 1 : -1;
|
|
2557
2567
|
const used = (n: KeypointName): Placement => placement('joint', `the ${n} joint, ${usedAs((joints as RigJoints).joints[n])}`, { joints: [jointRef(n)] });
|
|
2558
|
-
|
|
2568
|
+
const tipWhy =
|
|
2569
|
+
a.offLayer === null
|
|
2570
|
+
? used(`${a.side}_wrist`)
|
|
2571
|
+
: placement('mask', `3 px above ${a.part.name}'s bottom row, at its centroid x in the band 6 px above that row`, {
|
|
2572
|
+
parts: [a.part.name],
|
|
2573
|
+
fallback: a.tipFallback,
|
|
2574
|
+
});
|
|
2575
|
+
C(`sleeve_${a.side}`, 'chest', [a.shoulder, a.elbow], a.tip, [used(`${a.side}_shoulder`), used(`${a.side}_elbow`)], tipWhy);
|
|
2559
2576
|
tracks.push({ chain: `sleeve_${a.side}`, amps: [sgn * 0.2, sgn * 0.6], period: 4.0, phase: 0.22 + (a.side === 'l' ? 0.05 : 0), lag: 0.08 });
|
|
2560
2577
|
}
|
|
2561
2578
|
// Issue #349: each arm is a limb — the upper arm on link 0 and the forearm (with the hand) on link 1, each rigid on
|
|
@@ -2662,6 +2679,15 @@ export function proposeWithBasis(
|
|
|
2662
2679
|
}
|
|
2663
2680
|
if (hw.length === 1) notes.push(`handwear is one blob (${hw[0].name}): both sleeves in one mesh, split at the eye axis x=${pyFixed(axis, 0)}`);
|
|
2664
2681
|
}
|
|
2682
|
+
for (const [s] of SIDES) {
|
|
2683
|
+
const sc = bones.find((b): b is Extract<BoneEntry, { chain: string }> => 'chain' in b && b.chain === `sleeve_${s}`);
|
|
2684
|
+
if (sc === undefined) continue;
|
|
2685
|
+
const bone = `sleeve_${s}${sc.points.length - 1}`;
|
|
2686
|
+
for (const hp of P.byTag(`hand-${s}`)) {
|
|
2687
|
+
regions[hp.name] = bone;
|
|
2688
|
+
notes.push(`${hp.name} (${hp.from}): split hand -> region on ${bone}, the tip link of sleeve_${s}`);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2665
2691
|
|
|
2666
2692
|
// Issue #402: the topwear part config.cloth.bottomwear.part names takes the skirt's rule and recipe below; every
|
|
2667
2693
|
// other topwear shaped like a skirt is named in a note when a recipe is in play, never drafted on by its shape.
|
|
@@ -3460,8 +3486,14 @@ interface JointArm {
|
|
|
3460
3486
|
shoulder: [number, number];
|
|
3461
3487
|
elbow: [number, number];
|
|
3462
3488
|
wrist: [number, number];
|
|
3489
|
+
tip: [number, number];
|
|
3490
|
+
tipFallback: string | undefined;
|
|
3463
3491
|
part: PartRecord;
|
|
3464
3492
|
snap: number | null;
|
|
3493
|
+
offLayer: {
|
|
3494
|
+
distance: number;
|
|
3495
|
+
landedOn: readonly PartRecord[];
|
|
3496
|
+
} | null;
|
|
3465
3497
|
}
|
|
3466
3498
|
|
|
3467
3499
|
/**
|
|
@@ -3507,11 +3539,27 @@ function artDistance(m: Mask, at: readonly [number, number]): number {
|
|
|
3507
3539
|
* `wristSnap` (an integer above {@link WRIST_ON_PART_PX}, the CLI's to hold)
|
|
3508
3540
|
* it is bound to the one nearest part when that part is at most `wristSnap`
|
|
3509
3541
|
* px away; two parts tied nearest bind neither, as a wrist on two parts
|
|
3510
|
-
* does not.
|
|
3511
|
-
*
|
|
3542
|
+
* does not.
|
|
3543
|
+
*
|
|
3544
|
+
* Issue #491: when a hanging arm's wrist keypoint falls outside its handwear
|
|
3545
|
+
* layer's mask (`elbow[1] > shoulder[1]`, `wrist[1] > elbow[1]`, the elbow lies
|
|
3546
|
+
* within {@link WRIST_ON_PART_PX} of that side's handwear part, and the part's
|
|
3547
|
+
* bottom extent `bot - 3` reaches at least {@link WRIST_ON_PART_PX} below the
|
|
3548
|
+
* elbow — e.g. when a skin-coloured wrist read lands on a light skirt or top
|
|
3549
|
+
* layer below a hanging sleeve), the arm binds its handwear part with its
|
|
3550
|
+
* chain tip placed at the layer's bottom extent `[bandX(mask, bot - 6), bot - 3]`
|
|
3551
|
+
* so `limbs.arm_<s>` cuts at the elbow from the limb chain (`PROPOSE_WRIST_OFF_LAYER`).
|
|
3552
|
+
* Otherwise the why names the nearest part and its distance, the tolerance
|
|
3553
|
+
* that would have bound it.
|
|
3512
3554
|
*/
|
|
3513
|
-
function armsFromJoints(
|
|
3514
|
-
|
|
3555
|
+
function armsFromJoints(
|
|
3556
|
+
P: PartSet,
|
|
3557
|
+
hw: readonly PartRecord[],
|
|
3558
|
+
joints: RigJoints,
|
|
3559
|
+
wristSnap: number | null,
|
|
3560
|
+
chainRule = true,
|
|
3561
|
+
): { arms: JointArm[] | null; why: string | null } {
|
|
3562
|
+
const complete: Array<Omit<JointArm, 'tip' | 'tipFallback' | 'part' | 'snap' | 'offLayer'>> = [];
|
|
3515
3563
|
const gaps: string[] = [];
|
|
3516
3564
|
for (const s of ['r', 'l'] as const) {
|
|
3517
3565
|
const sh = joints.joints[`${s}_shoulder`].at;
|
|
@@ -3524,7 +3572,18 @@ function armsFromJoints(P: PartSet, hw: readonly PartRecord[], joints: RigJoints
|
|
|
3524
3572
|
else complete.push({ side: s, shoulder: sh, elbow: el, wrist: wr });
|
|
3525
3573
|
}
|
|
3526
3574
|
if (complete.length === 0) return { arms: null, why: `no arm has its shoulder, elbow and wrist all given (${gaps.join('; ')})` };
|
|
3527
|
-
const
|
|
3575
|
+
const armAlpha = new Map(
|
|
3576
|
+
hw.map((p) => {
|
|
3577
|
+
const tag = splitFrom(p.from)[1];
|
|
3578
|
+
const side = tag === 'handwear-r' ? 'r' : tag === 'handwear-l' ? 'l' : null;
|
|
3579
|
+
let m = P.alpha(p);
|
|
3580
|
+
if (side !== null) {
|
|
3581
|
+
for (const h of P.byTag(`hand-${side}`)) m = union(m, P.alpha(h));
|
|
3582
|
+
}
|
|
3583
|
+
return [p, m] as const;
|
|
3584
|
+
}),
|
|
3585
|
+
);
|
|
3586
|
+
const near = new Map(hw.map((p) => [p, dilate(armAlpha.get(p) as Mask, 2 * WRIST_ON_PART_PX + 1)]));
|
|
3528
3587
|
const arms: JointArm[] = [];
|
|
3529
3588
|
for (const a of complete) {
|
|
3530
3589
|
const raw = hw.filter((p) => isOn(near.get(p) as Mask, a.wrist[0], a.wrist[1]));
|
|
@@ -3536,21 +3595,46 @@ function armsFromJoints(P: PartSet, hw: readonly PartRecord[], joints: RigJoints
|
|
|
3536
3595
|
const t = splitFrom(p.from)[1];
|
|
3537
3596
|
return t === ownTag || t === oppTag;
|
|
3538
3597
|
})
|
|
3539
|
-
? raw.filter((p) => splitFrom(p.from)[1] === ownTag && isOn(
|
|
3598
|
+
? raw.filter((p) => splitFrom(p.from)[1] === ownTag && isOn(armAlpha.get(p) as Mask, a.wrist[0], a.wrist[1]))
|
|
3540
3599
|
: [];
|
|
3541
3600
|
const on = own.length === 1 ? own : raw;
|
|
3542
3601
|
if (on.length > 1) return { arms: null, why: `${a.side}_wrist at ${fmtAt(a.wrist)} lies on the art of ${on.length} handwear parts (${on.map((p) => p.name).join(', ')}), so it does not say which sleeve it ends` };
|
|
3543
3602
|
if (on.length === 1) {
|
|
3544
|
-
arms.push({ ...a, part: on[0], snap: null });
|
|
3603
|
+
arms.push({ ...a, tip: a.wrist, tipFallback: undefined, part: on[0], snap: null, offLayer: null });
|
|
3545
3604
|
continue;
|
|
3546
3605
|
}
|
|
3547
|
-
const far = hw.map((p) => ({ p, d: artDistance(
|
|
3606
|
+
const far = hw.map((p) => ({ p, d: artDistance(armAlpha.get(p) as Mask, a.wrist) }));
|
|
3548
3607
|
const d = Math.min(...far.map((q) => q.d));
|
|
3549
3608
|
const nearest = far.filter((q) => q.d === d).map((q) => q.p);
|
|
3550
3609
|
if (wristSnap !== null && d <= wristSnap && nearest.length === 1) {
|
|
3551
|
-
arms.push({ ...a, part: nearest[0], snap: d });
|
|
3610
|
+
arms.push({ ...a, tip: a.wrist, tipFallback: undefined, part: nearest[0], snap: d, offLayer: null });
|
|
3552
3611
|
continue;
|
|
3553
3612
|
}
|
|
3613
|
+
if (chainRule) {
|
|
3614
|
+
const byTag = hw.filter((p) => splitFrom(p.from)[1] === ownTag);
|
|
3615
|
+
const byElbow = hw.filter((p) => isOn(near.get(p) as Mask, a.elbow[0], a.elbow[1]));
|
|
3616
|
+
const sidePart =
|
|
3617
|
+
byTag.length === 1 && isOn(near.get(byTag[0]) as Mask, a.elbow[0], a.elbow[1])
|
|
3618
|
+
? byTag[0]
|
|
3619
|
+
: byElbow.length === 1
|
|
3620
|
+
? byElbow[0]
|
|
3621
|
+
: null;
|
|
3622
|
+
if (sidePart !== null) {
|
|
3623
|
+
const m = armAlpha.get(sidePart) as Mask;
|
|
3624
|
+
const ys = rowsAny(m);
|
|
3625
|
+
const bot = ys[ys.length - 1];
|
|
3626
|
+
if (bot !== undefined && a.elbow[1] > a.shoulder[1] && a.wrist[1] > a.elbow[1] && bot - 3 - a.elbow[1] >= WRIST_ON_PART_PX) {
|
|
3627
|
+
const bx = bandX(m, bot - 6);
|
|
3628
|
+
const tipX = or(bx, a.elbow[0]);
|
|
3629
|
+
const tipFallback = bx === null || bx === 0 ? `no art in the band: the ${a.side}_elbow joint's x` : undefined;
|
|
3630
|
+
const tip: [number, number] = [pyRound(tipX), bot - 3];
|
|
3631
|
+
const landedOn = P.layered().filter((p) => p !== sidePart && isOn(P.alpha(p), a.wrist[0], a.wrist[1]));
|
|
3632
|
+
const partDist = artDistance(m, a.wrist);
|
|
3633
|
+
arms.push({ ...a, tip, tipFallback, part: sidePart, snap: null, offLayer: { distance: partDist, landedOn } });
|
|
3634
|
+
continue;
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3554
3638
|
const gap = 'by the larger of the x and y gaps';
|
|
3555
3639
|
const reach = wristSnap === null ? `--wrist-snap ${Math.max(d, WRIST_ON_PART_PX + 1)} would bind it` : `beyond --wrist-snap ${wristSnap}`;
|
|
3556
3640
|
const nearestIs =
|
|
@@ -3727,7 +3811,25 @@ function jointNotes(joints: RigJoints, f: JointNoteFacts): string[] {
|
|
|
3727
3811
|
[`${s}_wrist`, 'the tip'],
|
|
3728
3812
|
] as const) {
|
|
3729
3813
|
const j = J[joint];
|
|
3730
|
-
|
|
3814
|
+
if (arm !== null && arm.offLayer !== null && joint === `${s}_wrist`) {
|
|
3815
|
+
const landed =
|
|
3816
|
+
arm.offLayer.landedOn.length > 0
|
|
3817
|
+
? `, on ${arm.offLayer.landedOn.map((p) => `${p.name} (${p.from})`).join(', ')}`
|
|
3818
|
+
: '';
|
|
3819
|
+
tell(
|
|
3820
|
+
joint,
|
|
3821
|
+
`${said(joint, j)}: not used — ${s}_wrist at ${fmtAt(arm.wrist)} lies outside ${arm.part.name} (${arm.part.from})'s mask (${arm.offLayer.distance} px from its art by the larger of the x and y gaps${landed}); chain sleeve_${s}'s tip is placed at ${arm.part.name}'s bottom extent [${arm.tip[0]}, ${arm.tip[1]}] instead`,
|
|
3822
|
+
);
|
|
3823
|
+
continue;
|
|
3824
|
+
}
|
|
3825
|
+
const on =
|
|
3826
|
+
arm === null
|
|
3827
|
+
? ''
|
|
3828
|
+
: arm.offLayer !== null
|
|
3829
|
+
? `the elbow lies on ${arm.part.name}; ${s}_wrist lies outside ${arm.part.name}'s mask, so sleeve_${s}'s tip is ${arm.part.name}'s bottom extent [${arm.tip[0]}, ${arm.tip[1]}]`
|
|
3830
|
+
: arm.snap === null
|
|
3831
|
+
? `the wrist lies on ${arm.part.name}`
|
|
3832
|
+
: `the wrist is bound to ${arm.part.name} by --wrist-snap`;
|
|
3731
3833
|
if (arm !== null) tell(joint, `${said(joint, j)}: ${usedAs(j)} for chain sleeve_${s}, ${role} (${on})`);
|
|
3732
3834
|
else if (j.at !== null && f.armWhy !== null) tell(joint, `${said(joint, j)}: not used — ${f.armWhy}; ${standIn}`);
|
|
3733
3835
|
else if (j.at !== null) tell(joint, `${said(joint, j)}: not used — ${standIn}`);
|
|
@@ -3739,6 +3841,17 @@ function jointNotes(joints: RigJoints, f: JointNoteFacts): string[] {
|
|
|
3739
3841
|
`wrist snap: ${s}_wrist at ${fmtAt(arm.wrist)} lies on no handwear part within ${WRIST_ON_PART_PX} px; --wrist-snap ${f.wristSnap} bound it to ${arm.part.name}, the nearest, ${arm.snap} px from its art by the larger of the x and y gaps`,
|
|
3740
3842
|
);
|
|
3741
3843
|
}
|
|
3844
|
+
// Issue #491: when the wrist keypoint lies outside the arm layer's mask, name the wrist, the arm layer, where it
|
|
3845
|
+
// landed, the chain tip at the layer's bottom extent, and the chain-derived elbow cut position.
|
|
3846
|
+
if (arm !== null && arm.offLayer !== null) {
|
|
3847
|
+
const landed =
|
|
3848
|
+
arm.offLayer.landedOn.length > 0
|
|
3849
|
+
? `, on ${arm.offLayer.landedOn.map((p) => `${p.name} (${p.from})`).join(', ')}`
|
|
3850
|
+
: '';
|
|
3851
|
+
out.push(
|
|
3852
|
+
`PROPOSE_WRIST_OFF_LAYER: ${s}_wrist at ${fmtAt(arm.wrist)} lies outside ${arm.part.name} (${arm.part.from})'s mask (${arm.offLayer.distance} px from its art by the larger of the x and y gaps${landed}); chain sleeve_${s} places its tip at ${arm.part.name}'s bottom extent [${arm.tip[0]}, ${arm.tip[1]}] instead of ${s}_wrist, and limb arm_${s} places the elbow cut at [${pyRound(arm.elbow[0])}, ${pyRound(arm.elbow[1])}] (${fmtAt(arm.elbow)}) from the limb chain (${s}_shoulder [${pyRound(arm.shoulder[0])}, ${pyRound(arm.shoulder[1])}] -> ${s}_elbow [${pyRound(arm.elbow[0])}, ${pyRound(arm.elbow[1])}] -> [${arm.tip[0]}, ${arm.tip[1]}])`,
|
|
3853
|
+
);
|
|
3854
|
+
}
|
|
3742
3855
|
}
|
|
3743
3856
|
// Issue #199: two chains on one part are said, not left to share its mesh unremarked.
|
|
3744
3857
|
const both = f.arms !== null && f.arms.length === 2 && f.arms[0].part === f.arms[1].part ? f.arms[0].part.name : null;
|
package/src/tags.ts
CHANGED
|
@@ -74,7 +74,11 @@ export const HEAD_TAGS = [
|
|
|
74
74
|
'mouth',
|
|
75
75
|
] as const;
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
/** The tags `splitMergedArmRuns` (issue #490) splits out of a merged `handwear-r` / `handwear-l` layer: `<tag>-r` / `<tag>-l`. */
|
|
78
|
+
export const ARM_MERGED_SPLIT_TAGS = ['hand', 'drape'] as const;
|
|
79
|
+
export type ArmMergedSplitTag = (typeof ARM_MERGED_SPLIT_TAGS)[number];
|
|
80
|
+
|
|
81
|
+
export type BaseTag = (typeof BODY_TAGS)[number] | (typeof HEAD_TAGS)[number] | ArmMergedSplitTag;
|
|
78
82
|
|
|
79
83
|
/** The tags upstream splits into `-r` / `-l` halves. */
|
|
80
84
|
export const SPLIT_TAGS = ['handwear', 'eyewhite', 'irides', 'eyelash', 'eyebrow', 'ears'] as const;
|
|
@@ -97,10 +101,11 @@ export interface TagReading {
|
|
|
97
101
|
/**
|
|
98
102
|
* Read a layer name as a v3 tag, or `null` when it is not one.
|
|
99
103
|
*
|
|
100
|
-
* Accepted: any of the 24 base tags,
|
|
101
|
-
* split tags
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
+
* Accepted: any of the 24 base tags, `<tag>-r` / `<tag>-l` for the six
|
|
105
|
+
* split tags, and `<tag>-r` / `<tag>-l` for the merged-arm split tags
|
|
106
|
+
* (`hand-r`, `hand-l`, `drape-r`, `drape-l`, issue #490). A suffix on a tag
|
|
107
|
+
* upstream never splits (`topwear-l`) is not a tag — upstream cannot have
|
|
108
|
+
* produced it, so accepting it would be accepting a layer somebody renamed.
|
|
104
109
|
*/
|
|
105
110
|
export function readTag(name: string): TagReading | null {
|
|
106
111
|
const split = /^(.+)-([rl])$/.exec(name);
|
|
@@ -108,6 +113,10 @@ export function readTag(name: string): TagReading | null {
|
|
|
108
113
|
const base = split[1] as SplitTag;
|
|
109
114
|
return { name, base, side: split[2] as Side, group: groupOf(base) };
|
|
110
115
|
}
|
|
116
|
+
if (split !== null && (ARM_MERGED_SPLIT_TAGS as readonly string[]).includes(split[1])) {
|
|
117
|
+
const base = split[1] as ArmMergedSplitTag;
|
|
118
|
+
return { name, base, side: split[2] as Side, group: 'body' };
|
|
119
|
+
}
|
|
111
120
|
if ((V3_TAGS as readonly string[]).includes(name)) {
|
|
112
121
|
const base = name as BaseTag;
|
|
113
122
|
return { name, base, side: null, group: groupOf(base) };
|
|
@@ -122,7 +131,7 @@ function groupOf(tag: BaseTag): 'body' | 'head' {
|
|
|
122
131
|
/** Every name `readTag` accepts, sorted — what a refusal quotes as "required". */
|
|
123
132
|
export function acceptedTagNames(): string[] {
|
|
124
133
|
const names = new Set<string>(V3_TAGS);
|
|
125
|
-
for (const t of SPLIT_TAGS) {
|
|
134
|
+
for (const t of [...SPLIT_TAGS, ...ARM_MERGED_SPLIT_TAGS]) {
|
|
126
135
|
names.add(`${t}-r`);
|
|
127
136
|
names.add(`${t}-l`);
|
|
128
137
|
}
|