modern-path2d 1.8.0 → 1.8.2
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.cjs +226 -119
- package/dist/index.d.cts +21 -2
- package/dist/index.d.mts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +2 -2
- package/dist/index.mjs +226 -120
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -485,6 +485,195 @@ function cubicBezier(t, p0, p1, p2, p3) {
|
|
|
485
485
|
return cubicBezierP0(t, p0) + cubicBezierP1(t, p1) + cubicBezierP2(t, p2) + cubicBezierP3(t, p3);
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
function isLeft(ax, ay, bx, by, px, py) {
|
|
489
|
+
return (bx - ax) * (py - ay) - (px - ax) * (by - ay);
|
|
490
|
+
}
|
|
491
|
+
function windingNumber$1(px, py, vertices) {
|
|
492
|
+
const len = vertices.length;
|
|
493
|
+
let wn = 0;
|
|
494
|
+
for (let i = 0; i < len; i += 2) {
|
|
495
|
+
const ax = vertices[i];
|
|
496
|
+
const ay = vertices[i + 1];
|
|
497
|
+
const k = (i + 2) % len;
|
|
498
|
+
const bx = vertices[k];
|
|
499
|
+
const by = vertices[k + 1];
|
|
500
|
+
if (ay <= py) {
|
|
501
|
+
if (by > py && isLeft(ax, ay, bx, by, px, py) > 0) {
|
|
502
|
+
wn++;
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
if (by <= py && isLeft(ax, ay, bx, by, px, py) < 0) {
|
|
506
|
+
wn--;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return wn;
|
|
511
|
+
}
|
|
512
|
+
function crossingNumber(px, py, vertices) {
|
|
513
|
+
const len = vertices.length;
|
|
514
|
+
let cn = 0;
|
|
515
|
+
for (let i = 0; i < len; i += 2) {
|
|
516
|
+
const ax = vertices[i];
|
|
517
|
+
const ay = vertices[i + 1];
|
|
518
|
+
const k = (i + 2) % len;
|
|
519
|
+
const bx = vertices[k];
|
|
520
|
+
const by = vertices[k + 1];
|
|
521
|
+
if (ay <= py && by > py || ay > py && by <= py) {
|
|
522
|
+
const t = (py - ay) / (by - ay);
|
|
523
|
+
if (px < ax + t * (bx - ax)) {
|
|
524
|
+
cn++;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return cn;
|
|
529
|
+
}
|
|
530
|
+
function segmentDistance(px, py, ax, ay, bx, by) {
|
|
531
|
+
const dx = bx - ax;
|
|
532
|
+
const dy = by - ay;
|
|
533
|
+
const lenSq = dx * dx + dy * dy;
|
|
534
|
+
let t = lenSq === 0 ? 0 : ((px - ax) * dx + (py - ay) * dy) / lenSq;
|
|
535
|
+
if (t < 0) {
|
|
536
|
+
t = 0;
|
|
537
|
+
} else if (t > 1) {
|
|
538
|
+
t = 1;
|
|
539
|
+
}
|
|
540
|
+
const cx = ax + t * dx;
|
|
541
|
+
const cy = ay + t * dy;
|
|
542
|
+
return Math.hypot(px - cx, py - cy);
|
|
543
|
+
}
|
|
544
|
+
function pointInPolygon(point, vertices, fillRule = "nonzero") {
|
|
545
|
+
if (vertices.length < 6) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
if (fillRule === "evenodd") {
|
|
549
|
+
return (crossingNumber(point.x, point.y, vertices) & 1) === 1;
|
|
550
|
+
}
|
|
551
|
+
return windingNumber$1(point.x, point.y, vertices) !== 0;
|
|
552
|
+
}
|
|
553
|
+
function pointInPolygons(point, polygons, fillRule = "nonzero") {
|
|
554
|
+
const { x, y } = point;
|
|
555
|
+
if (fillRule === "evenodd") {
|
|
556
|
+
let cn = 0;
|
|
557
|
+
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
558
|
+
const ring = polygons[i];
|
|
559
|
+
if (ring.length >= 6) {
|
|
560
|
+
cn += crossingNumber(x, y, ring);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return (cn & 1) === 1;
|
|
564
|
+
}
|
|
565
|
+
let wn = 0;
|
|
566
|
+
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
567
|
+
const ring = polygons[i];
|
|
568
|
+
if (ring.length >= 6) {
|
|
569
|
+
wn += windingNumber$1(x, y, ring);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return wn !== 0;
|
|
573
|
+
}
|
|
574
|
+
function pointToSegmentDistance(point, a, b) {
|
|
575
|
+
return segmentDistance(point.x, point.y, a.x, a.y, b.x, b.y);
|
|
576
|
+
}
|
|
577
|
+
function pointToPolylineDistance(point, vertices, closed = false) {
|
|
578
|
+
const len = vertices.length;
|
|
579
|
+
if (len < 2) {
|
|
580
|
+
return Infinity;
|
|
581
|
+
}
|
|
582
|
+
const { x: px, y: py } = point;
|
|
583
|
+
if (len === 2) {
|
|
584
|
+
return Math.hypot(px - vertices[0], py - vertices[1]);
|
|
585
|
+
}
|
|
586
|
+
let min = Infinity;
|
|
587
|
+
for (let i = 0; i < len - 2; i += 2) {
|
|
588
|
+
const d = segmentDistance(px, py, vertices[i], vertices[i + 1], vertices[i + 2], vertices[i + 3]);
|
|
589
|
+
if (d < min) {
|
|
590
|
+
min = d;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (closed && len >= 6) {
|
|
594
|
+
const d = segmentDistance(px, py, vertices[len - 2], vertices[len - 1], vertices[0], vertices[1]);
|
|
595
|
+
if (d < min) {
|
|
596
|
+
min = d;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return min;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function boundsOf(ring) {
|
|
603
|
+
if (ring.length < 6) {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
let minX = Infinity;
|
|
607
|
+
let minY = Infinity;
|
|
608
|
+
let maxX = -Infinity;
|
|
609
|
+
let maxY = -Infinity;
|
|
610
|
+
for (let i = 0; i < ring.length; i += 2) {
|
|
611
|
+
const x = ring[i];
|
|
612
|
+
const y = ring[i + 1];
|
|
613
|
+
if (x < minX)
|
|
614
|
+
minX = x;
|
|
615
|
+
if (y < minY)
|
|
616
|
+
minY = y;
|
|
617
|
+
if (x > maxX)
|
|
618
|
+
maxX = x;
|
|
619
|
+
if (y > maxY)
|
|
620
|
+
maxY = y;
|
|
621
|
+
}
|
|
622
|
+
return { minX, minY, maxX, maxY };
|
|
623
|
+
}
|
|
624
|
+
function bboxInside(inner, outer) {
|
|
625
|
+
return inner.minX >= outer.minX && inner.maxX <= outer.maxX && inner.minY >= outer.minY && inner.maxY <= outer.maxY;
|
|
626
|
+
}
|
|
627
|
+
function ringInsideRing(inner, outer) {
|
|
628
|
+
const n = inner.length / 2;
|
|
629
|
+
const step = Math.max(1, Math.floor(n / 9));
|
|
630
|
+
let tested = 0;
|
|
631
|
+
let inside = 0;
|
|
632
|
+
for (let i = 0; i < n; i += step) {
|
|
633
|
+
tested++;
|
|
634
|
+
if (pointInPolygon({ x: inner[i * 2], y: inner[i * 2 + 1] }, outer, "evenodd")) {
|
|
635
|
+
inside++;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return tested > 0 && inside * 2 > tested;
|
|
639
|
+
}
|
|
640
|
+
function evenoddFillRule(paths) {
|
|
641
|
+
const len = paths.length;
|
|
642
|
+
const bboxes = paths.map(boundsOf);
|
|
643
|
+
const depth = Array.from({ length: len }).fill(0);
|
|
644
|
+
const containers = paths.map(() => []);
|
|
645
|
+
for (let i = 0; i < len; i++) {
|
|
646
|
+
const bi = bboxes[i];
|
|
647
|
+
if (!bi) {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
for (let j = 0; j < len; j++) {
|
|
651
|
+
if (i === j) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const bj = bboxes[j];
|
|
655
|
+
if (!bj || !bboxInside(bi, bj)) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (ringInsideRing(paths[i], paths[j])) {
|
|
659
|
+
depth[i]++;
|
|
660
|
+
containers[i].push(j);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return paths.map((_, i) => {
|
|
665
|
+
let parentIndex = -1;
|
|
666
|
+
let bestDepth = -1;
|
|
667
|
+
for (const j of containers[i]) {
|
|
668
|
+
if (depth[j] > bestDepth) {
|
|
669
|
+
bestDepth = depth[j];
|
|
670
|
+
parentIndex = j;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return { index: i, depth: depth[i], parentIndex };
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
488
677
|
function fillTriangulate(pointArray, options = {}) {
|
|
489
678
|
let {
|
|
490
679
|
vertices = [],
|
|
@@ -647,7 +836,7 @@ function getDirectedArea(vertices) {
|
|
|
647
836
|
function cross(ax, ay, bx, by, cx, cy) {
|
|
648
837
|
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
649
838
|
}
|
|
650
|
-
function windingNumber
|
|
839
|
+
function windingNumber(px, py, polygon) {
|
|
651
840
|
const polygonLen = polygon.length;
|
|
652
841
|
let wn = 0;
|
|
653
842
|
for (let i = 0, j = polygonLen - 2; i < polygonLen; j = i, i += 2) {
|
|
@@ -768,7 +957,7 @@ function nonzeroFillRule(paths) {
|
|
|
768
957
|
const wnList = [];
|
|
769
958
|
for (let p = 0, pLen = testPoints.length; p < pLen; p++) {
|
|
770
959
|
const [x, y] = testPoints[p];
|
|
771
|
-
const winding = windingNumber
|
|
960
|
+
const winding = windingNumber(x, y, paths[j]);
|
|
772
961
|
wnMap[winding] = (wnMap[winding] ?? 0) + 1;
|
|
773
962
|
wnList.push(winding);
|
|
774
963
|
}
|
|
@@ -791,120 +980,6 @@ function nonzeroFillRule(paths) {
|
|
|
791
980
|
return results;
|
|
792
981
|
}
|
|
793
982
|
|
|
794
|
-
function isLeft(ax, ay, bx, by, px, py) {
|
|
795
|
-
return (bx - ax) * (py - ay) - (px - ax) * (by - ay);
|
|
796
|
-
}
|
|
797
|
-
function windingNumber(px, py, vertices) {
|
|
798
|
-
const len = vertices.length;
|
|
799
|
-
let wn = 0;
|
|
800
|
-
for (let i = 0; i < len; i += 2) {
|
|
801
|
-
const ax = vertices[i];
|
|
802
|
-
const ay = vertices[i + 1];
|
|
803
|
-
const k = (i + 2) % len;
|
|
804
|
-
const bx = vertices[k];
|
|
805
|
-
const by = vertices[k + 1];
|
|
806
|
-
if (ay <= py) {
|
|
807
|
-
if (by > py && isLeft(ax, ay, bx, by, px, py) > 0) {
|
|
808
|
-
wn++;
|
|
809
|
-
}
|
|
810
|
-
} else {
|
|
811
|
-
if (by <= py && isLeft(ax, ay, bx, by, px, py) < 0) {
|
|
812
|
-
wn--;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
return wn;
|
|
817
|
-
}
|
|
818
|
-
function crossingNumber(px, py, vertices) {
|
|
819
|
-
const len = vertices.length;
|
|
820
|
-
let cn = 0;
|
|
821
|
-
for (let i = 0; i < len; i += 2) {
|
|
822
|
-
const ax = vertices[i];
|
|
823
|
-
const ay = vertices[i + 1];
|
|
824
|
-
const k = (i + 2) % len;
|
|
825
|
-
const bx = vertices[k];
|
|
826
|
-
const by = vertices[k + 1];
|
|
827
|
-
if (ay <= py && by > py || ay > py && by <= py) {
|
|
828
|
-
const t = (py - ay) / (by - ay);
|
|
829
|
-
if (px < ax + t * (bx - ax)) {
|
|
830
|
-
cn++;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
return cn;
|
|
835
|
-
}
|
|
836
|
-
function segmentDistance(px, py, ax, ay, bx, by) {
|
|
837
|
-
const dx = bx - ax;
|
|
838
|
-
const dy = by - ay;
|
|
839
|
-
const lenSq = dx * dx + dy * dy;
|
|
840
|
-
let t = lenSq === 0 ? 0 : ((px - ax) * dx + (py - ay) * dy) / lenSq;
|
|
841
|
-
if (t < 0) {
|
|
842
|
-
t = 0;
|
|
843
|
-
} else if (t > 1) {
|
|
844
|
-
t = 1;
|
|
845
|
-
}
|
|
846
|
-
const cx = ax + t * dx;
|
|
847
|
-
const cy = ay + t * dy;
|
|
848
|
-
return Math.hypot(px - cx, py - cy);
|
|
849
|
-
}
|
|
850
|
-
function pointInPolygon(point, vertices, fillRule = "nonzero") {
|
|
851
|
-
if (vertices.length < 6) {
|
|
852
|
-
return false;
|
|
853
|
-
}
|
|
854
|
-
if (fillRule === "evenodd") {
|
|
855
|
-
return (crossingNumber(point.x, point.y, vertices) & 1) === 1;
|
|
856
|
-
}
|
|
857
|
-
return windingNumber(point.x, point.y, vertices) !== 0;
|
|
858
|
-
}
|
|
859
|
-
function pointInPolygons(point, polygons, fillRule = "nonzero") {
|
|
860
|
-
const { x, y } = point;
|
|
861
|
-
if (fillRule === "evenodd") {
|
|
862
|
-
let cn = 0;
|
|
863
|
-
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
864
|
-
const ring = polygons[i];
|
|
865
|
-
if (ring.length >= 6) {
|
|
866
|
-
cn += crossingNumber(x, y, ring);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
return (cn & 1) === 1;
|
|
870
|
-
}
|
|
871
|
-
let wn = 0;
|
|
872
|
-
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
873
|
-
const ring = polygons[i];
|
|
874
|
-
if (ring.length >= 6) {
|
|
875
|
-
wn += windingNumber(x, y, ring);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
return wn !== 0;
|
|
879
|
-
}
|
|
880
|
-
function pointToSegmentDistance(point, a, b) {
|
|
881
|
-
return segmentDistance(point.x, point.y, a.x, a.y, b.x, b.y);
|
|
882
|
-
}
|
|
883
|
-
function pointToPolylineDistance(point, vertices, closed = false) {
|
|
884
|
-
const len = vertices.length;
|
|
885
|
-
if (len < 2) {
|
|
886
|
-
return Infinity;
|
|
887
|
-
}
|
|
888
|
-
const { x: px, y: py } = point;
|
|
889
|
-
if (len === 2) {
|
|
890
|
-
return Math.hypot(px - vertices[0], py - vertices[1]);
|
|
891
|
-
}
|
|
892
|
-
let min = Infinity;
|
|
893
|
-
for (let i = 0; i < len - 2; i += 2) {
|
|
894
|
-
const d = segmentDistance(px, py, vertices[i], vertices[i + 1], vertices[i + 2], vertices[i + 3]);
|
|
895
|
-
if (d < min) {
|
|
896
|
-
min = d;
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
if (closed && len >= 6) {
|
|
900
|
-
const d = segmentDistance(px, py, vertices[len - 2], vertices[len - 1], vertices[0], vertices[1]);
|
|
901
|
-
if (d < min) {
|
|
902
|
-
min = d;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
return min;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
983
|
function quadraticBezierP0(t, p) {
|
|
909
984
|
const k = 1 - t;
|
|
910
985
|
return k * k * p;
|
|
@@ -952,6 +1027,10 @@ function strokeTriangulate(points, options = {}) {
|
|
|
952
1027
|
if (points.length === 0) {
|
|
953
1028
|
return { vertices, indices };
|
|
954
1029
|
}
|
|
1030
|
+
points = dedupeConsecutivePoints(points, eps);
|
|
1031
|
+
if (points.length < 4) {
|
|
1032
|
+
return { vertices, indices };
|
|
1033
|
+
}
|
|
955
1034
|
const style = lineStyle;
|
|
956
1035
|
let alignment = style.alignment;
|
|
957
1036
|
if (lineStyle.alignment !== 0.5) {
|
|
@@ -1241,6 +1320,17 @@ function strokeTriangulate(points, options = {}) {
|
|
|
1241
1320
|
indices
|
|
1242
1321
|
};
|
|
1243
1322
|
}
|
|
1323
|
+
function dedupeConsecutivePoints(points, eps) {
|
|
1324
|
+
const out = [points[0], points[1]];
|
|
1325
|
+
for (let i = 2; i < points.length; i += 2) {
|
|
1326
|
+
const x = points[i];
|
|
1327
|
+
const y = points[i + 1];
|
|
1328
|
+
if (Math.abs(x - out[out.length - 2]) >= eps || Math.abs(y - out[out.length - 1]) >= eps) {
|
|
1329
|
+
out.push(x, y);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return out;
|
|
1333
|
+
}
|
|
1244
1334
|
function getOrientationOfPoints(points) {
|
|
1245
1335
|
const m = points.length;
|
|
1246
1336
|
if (m < 6) {
|
|
@@ -4935,14 +5025,30 @@ class Path2D extends CompositeCurve {
|
|
|
4935
5025
|
});
|
|
4936
5026
|
}
|
|
4937
5027
|
} else {
|
|
4938
|
-
this.curves.
|
|
4939
|
-
|
|
5028
|
+
const paths = this.curves.map((curve) => curve.getFillVertices(_options));
|
|
5029
|
+
const groups = evenoddFillRule(paths);
|
|
5030
|
+
const groupsLen = groups.length;
|
|
5031
|
+
for (let i = 0; i < groupsLen; i++) {
|
|
5032
|
+
const pointArray = paths[i];
|
|
5033
|
+
if ((groups[i].depth & 1) === 1 || !pointArray.length) {
|
|
5034
|
+
continue;
|
|
5035
|
+
}
|
|
5036
|
+
const _pointArray = pointArray.slice();
|
|
5037
|
+
const holes = [];
|
|
5038
|
+
for (let j = 0; j < groupsLen; j++) {
|
|
5039
|
+
if (groups[j].parentIndex === i && (groups[j].depth & 1) === 1) {
|
|
5040
|
+
holes.push(_pointArray.length / 2);
|
|
5041
|
+
_pointArray.push(...paths[j]);
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
fillTriangulate(_pointArray, {
|
|
4940
5045
|
...options,
|
|
4941
5046
|
indices,
|
|
4942
5047
|
vertices,
|
|
5048
|
+
holes,
|
|
4943
5049
|
style: { ...this.style }
|
|
4944
5050
|
});
|
|
4945
|
-
}
|
|
5051
|
+
}
|
|
4946
5052
|
}
|
|
4947
5053
|
return { indices, vertices };
|
|
4948
5054
|
}
|
|
@@ -5259,4 +5365,4 @@ function applyFFD(point, grid, width = grid.width, height = grid.height) {
|
|
|
5259
5365
|
point.set(x, y);
|
|
5260
5366
|
}
|
|
5261
5367
|
|
|
5262
|
-
export { ArcCurve, BoundingBox, CompositeCurve, CubicBezierCurve, Curve, CurvePath, EllipseCurve, EquilateralPolygonCurve, FFDControlGrid, LineCurve, PI, PI_2, Path2D, Path2DSet, PathMeasure, PolygonCurve, QuadraticBezierCurve, RectangleCurve, RoundRectangleCurve, SplineCurve, Transform2D, Vector2, applyFFD, catmullRom, cubicBezier, drawPoint, fillTriangulate, getAdaptiveCubicBezierCurvePoints, getAdaptiveQuadraticBezierCurvePoints, getDirectedArea, getIntersectionPoint, nonzeroFillRule, parseArcCommand, parseCssArg, parseCssArgs, parseCssFunctions, parsePathDataArgs, pointInPolygon, pointInPolygons, pointToPolylineDistance, pointToSegmentDistance, polygonBoolean, quadraticBezier, resolveLineStyle, setCanvasContext, strokeTriangulate, svgPathCommandsAddToPath2D, svgPathCommandsToData, svgPathDataToCommands, svgToDom, svgToPath2DSet, toKebabCase };
|
|
5368
|
+
export { ArcCurve, BoundingBox, CompositeCurve, CubicBezierCurve, Curve, CurvePath, EllipseCurve, EquilateralPolygonCurve, FFDControlGrid, LineCurve, PI, PI_2, Path2D, Path2DSet, PathMeasure, PolygonCurve, QuadraticBezierCurve, RectangleCurve, RoundRectangleCurve, SplineCurve, Transform2D, Vector2, applyFFD, catmullRom, cubicBezier, drawPoint, evenoddFillRule, fillTriangulate, getAdaptiveCubicBezierCurvePoints, getAdaptiveQuadraticBezierCurvePoints, getDirectedArea, getIntersectionPoint, nonzeroFillRule, parseArcCommand, parseCssArg, parseCssArgs, parseCssFunctions, parsePathDataArgs, pointInPolygon, pointInPolygons, pointToPolylineDistance, pointToSegmentDistance, polygonBoolean, quadraticBezier, resolveLineStyle, setCanvasContext, strokeTriangulate, svgPathCommandsAddToPath2D, svgPathCommandsToData, svgPathDataToCommands, svgToDom, svgToPath2DSet, toKebabCase };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modern-path2d",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.8.
|
|
4
|
+
"version": "1.8.2",
|
|
5
5
|
"packageManager": "pnpm@9.15.1",
|
|
6
6
|
"description": "A Path2D library, fully compatible with Web Path2D, with additional support for triangulate、animation、deformation etc.",
|
|
7
7
|
"author": "wxm",
|