@emasoft/svg-matrix 1.0.19 → 1.0.20
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/README.md +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +8 -2
- package/src/animation-optimization.js +394 -0
- package/src/animation-references.js +440 -0
- package/src/arc-length.js +940 -0
- package/src/bezier-analysis.js +1626 -0
- package/src/bezier-intersections.js +1369 -0
- package/src/clip-path-resolver.js +110 -2
- package/src/convert-path-data.js +583 -0
- package/src/css-specificity.js +443 -0
- package/src/douglas-peucker.js +356 -0
- package/src/flatten-pipeline.js +109 -4
- package/src/geometry-to-path.js +126 -16
- package/src/gjk-collision.js +840 -0
- package/src/index.js +175 -2
- package/src/off-canvas-detection.js +1222 -0
- package/src/path-analysis.js +1241 -0
- package/src/path-data-plugins.js +928 -0
- package/src/path-optimization.js +825 -0
- package/src/path-simplification.js +1140 -0
- package/src/polygon-clip.js +376 -99
- package/src/svg-boolean-ops.js +898 -0
- package/src/svg-collections.js +910 -0
- package/src/svg-parser.js +175 -16
- package/src/svg-rendering-context.js +627 -0
- package/src/svg-toolbox.js +7495 -0
- package/src/svg-validation-data.js +944 -0
- package/src/transform-decomposition.js +810 -0
- package/src/transform-optimization.js +936 -0
- package/src/use-symbol-resolver.js +75 -7
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
ellipseToPathDataHP
|
|
47
47
|
} from './geometry-to-path.js';
|
|
48
48
|
import { Logger } from './logger.js';
|
|
49
|
+
import { FillRule, pointInPolygonWithRule } from './svg-boolean-ops.js';
|
|
49
50
|
|
|
50
51
|
// Alias for cleaner code
|
|
51
52
|
const parseTransform = parseTransformAttribute;
|
|
@@ -566,6 +567,97 @@ export function resolveClipPath(clipPathDef, targetElement, ctm = null, options
|
|
|
566
567
|
return unified;
|
|
567
568
|
}
|
|
568
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Clip a polygon using a clip polygon with clip-rule support.
|
|
572
|
+
*
|
|
573
|
+
* For simple (non-self-intersecting) clip polygons, this is equivalent to
|
|
574
|
+
* polygonIntersection. For self-intersecting clip paths, the clip-rule
|
|
575
|
+
* determines which regions are inside the clip:
|
|
576
|
+
*
|
|
577
|
+
* - 'nonzero': All regions where winding number != 0 are inside
|
|
578
|
+
* - 'evenodd': Only regions with odd crossing count are inside
|
|
579
|
+
*
|
|
580
|
+
* The algorithm:
|
|
581
|
+
* 1. For simple clip polygons, use standard polygon intersection
|
|
582
|
+
* 2. For self-intersecting clip polygons with evenodd rule, we need to
|
|
583
|
+
* compute which parts of the element polygon are in the "filled" regions
|
|
584
|
+
* of the clip polygon according to the evenodd rule
|
|
585
|
+
*
|
|
586
|
+
* @private
|
|
587
|
+
* @param {Array} elementPolygon - The polygon to be clipped
|
|
588
|
+
* @param {Array} clipPolygon - The clipping polygon (may be self-intersecting)
|
|
589
|
+
* @param {string} clipRule - 'nonzero' or 'evenodd'
|
|
590
|
+
* @returns {Array} Clipped polygon(s), or array of polygon arrays for multi-region results
|
|
591
|
+
*/
|
|
592
|
+
function clipPolygonWithRule(elementPolygon, clipPolygon, clipRule) {
|
|
593
|
+
// For nonzero rule, standard intersection works correctly
|
|
594
|
+
// because polygonIntersection uses the winding number test internally
|
|
595
|
+
if (clipRule === 'nonzero') {
|
|
596
|
+
return PolygonClip.polygonIntersection(elementPolygon, clipPolygon);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// For evenodd rule with self-intersecting clip paths, we need a different approach
|
|
600
|
+
// The idea: filter vertices of the intersection result by the evenodd test
|
|
601
|
+
// First get the standard intersection
|
|
602
|
+
const intersection = PolygonClip.polygonIntersection(elementPolygon, clipPolygon);
|
|
603
|
+
if (intersection.length === 0) return [];
|
|
604
|
+
|
|
605
|
+
// For each resulting polygon, check if its centroid is inside according to evenodd
|
|
606
|
+
// This handles the case where the clip path has holes due to evenodd rule
|
|
607
|
+
const result = [];
|
|
608
|
+
for (const poly of intersection) {
|
|
609
|
+
if (poly.length < 3) continue;
|
|
610
|
+
|
|
611
|
+
// Compute centroid of the polygon
|
|
612
|
+
const centroid = computeCentroid(poly);
|
|
613
|
+
|
|
614
|
+
// Test if centroid is inside the clip polygon according to evenodd rule
|
|
615
|
+
const fillRule = clipRule === 'evenodd' ? FillRule.EVENODD : FillRule.NONZERO;
|
|
616
|
+
const inside = pointInPolygonWithRule(centroid, clipPolygon, fillRule);
|
|
617
|
+
|
|
618
|
+
// If centroid is inside (1) or on boundary (0), keep this polygon
|
|
619
|
+
if (inside >= 0) {
|
|
620
|
+
result.push(poly);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return result.length === 1 ? result[0] : result;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Compute the centroid (center of mass) of a polygon.
|
|
629
|
+
* @private
|
|
630
|
+
*/
|
|
631
|
+
function computeCentroid(polygon) {
|
|
632
|
+
let cx = new Decimal(0);
|
|
633
|
+
let cy = new Decimal(0);
|
|
634
|
+
let area = new Decimal(0);
|
|
635
|
+
|
|
636
|
+
for (let i = 0; i < polygon.length; i++) {
|
|
637
|
+
const p1 = polygon[i];
|
|
638
|
+
const p2 = polygon[(i + 1) % polygon.length];
|
|
639
|
+
const cross = p1.x.times(p2.y).minus(p2.x.times(p1.y));
|
|
640
|
+
area = area.plus(cross);
|
|
641
|
+
cx = cx.plus(p1.x.plus(p2.x).times(cross));
|
|
642
|
+
cy = cy.plus(p1.y.plus(p2.y).times(cross));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
area = area.div(2);
|
|
646
|
+
if (area.abs().lt(1e-10)) {
|
|
647
|
+
// Degenerate polygon - return average of vertices
|
|
648
|
+
let sumX = new Decimal(0);
|
|
649
|
+
let sumY = new Decimal(0);
|
|
650
|
+
for (const p of polygon) {
|
|
651
|
+
sumX = sumX.plus(p.x);
|
|
652
|
+
sumY = sumY.plus(p.y);
|
|
653
|
+
}
|
|
654
|
+
return PolygonClip.point(sumX.div(polygon.length), sumY.div(polygon.length));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const factor = new Decimal(1).div(area.times(6));
|
|
658
|
+
return PolygonClip.point(cx.times(factor), cy.times(factor));
|
|
659
|
+
}
|
|
660
|
+
|
|
569
661
|
/**
|
|
570
662
|
* Apply a clipPath to an element, returning the clipped geometry.
|
|
571
663
|
*
|
|
@@ -577,11 +669,17 @@ export function resolveClipPath(clipPathDef, targetElement, ctm = null, options
|
|
|
577
669
|
* This is the main function for applying clip paths to elements. The result is a
|
|
578
670
|
* polygon representing the visible portion of the element after clipping.
|
|
579
671
|
*
|
|
672
|
+
* The clip-rule property determines how the clipping region is calculated for
|
|
673
|
+
* self-intersecting paths:
|
|
674
|
+
* - 'nonzero' (default): Uses winding number rule (SVG default)
|
|
675
|
+
* - 'evenodd': Uses even-odd rule (creates holes in self-intersecting paths)
|
|
676
|
+
*
|
|
580
677
|
* @param {Object} element - SVG element to be clipped (rect, circle, path, etc.)
|
|
581
678
|
* @param {Object} clipPathDef - clipPath definition object (see resolveClipPath)
|
|
582
679
|
* @param {Matrix|null} [ctm=null] - Current transformation matrix (3x3) to apply
|
|
583
680
|
* @param {Object} [options={}] - Additional options
|
|
584
681
|
* @param {number} [options.samples=20] - Number of sample points per curve segment
|
|
682
|
+
* @param {string} [options.clipRule='nonzero'] - Clip rule: 'nonzero' or 'evenodd'
|
|
585
683
|
* @returns {Array<{x: Decimal, y: Decimal}>} Clipped polygon representing the intersection
|
|
586
684
|
* of the element and clipPath. Empty array if no intersection or invalid input.
|
|
587
685
|
*
|
|
@@ -595,6 +693,15 @@ export function resolveClipPath(clipPathDef, targetElement, ctm = null, options
|
|
|
595
693
|
* // Returns polygon approximating the intersection (rounded corners of rect)
|
|
596
694
|
*
|
|
597
695
|
* @example
|
|
696
|
+
* // Clip with evenodd rule (creates holes in self-intersecting clip paths)
|
|
697
|
+
* const rect = { type: 'rect', x: 0, y: 0, width: 100, height: 100 };
|
|
698
|
+
* const clipDef = {
|
|
699
|
+
* children: [{ type: 'path', d: 'M 0,0 L 100,100 L 100,0 L 0,100 Z' }] // Self-intersecting
|
|
700
|
+
* };
|
|
701
|
+
* const clipped = applyClipPath(rect, clipDef, null, { clipRule: 'evenodd' });
|
|
702
|
+
* // Center region will NOT be clipped (evenodd creates hole there)
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
598
705
|
* // Clip with objectBoundingBox coordinate system
|
|
599
706
|
* const ellipse = { type: 'ellipse', cx: 100, cy: 100, rx: 80, ry: 60 };
|
|
600
707
|
* const clipDef = {
|
|
@@ -604,14 +711,15 @@ export function resolveClipPath(clipPathDef, targetElement, ctm = null, options
|
|
|
604
711
|
* const clipped = applyClipPath(ellipse, clipDef, null, { samples: 50 });
|
|
605
712
|
*/
|
|
606
713
|
export function applyClipPath(element, clipPathDef, ctm = null, options = {}) {
|
|
607
|
-
const { samples = DEFAULT_CURVE_SAMPLES } = options;
|
|
714
|
+
const { samples = DEFAULT_CURVE_SAMPLES, clipRule = 'nonzero' } = options;
|
|
608
715
|
const clipPolygon = resolveClipPath(clipPathDef, element, ctm, options);
|
|
609
716
|
if (clipPolygon.length < 3) return [];
|
|
610
717
|
|
|
611
718
|
const elementPolygon = shapeToPolygon(element, ctm, samples);
|
|
612
719
|
if (elementPolygon.length < 3) return [];
|
|
613
720
|
|
|
614
|
-
|
|
721
|
+
// Use clip-rule aware intersection for self-intersecting clip paths
|
|
722
|
+
return clipPolygonWithRule(elementPolygon, clipPolygon, clipRule);
|
|
615
723
|
}
|
|
616
724
|
|
|
617
725
|
/**
|