-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathRemoveRedundantPoints.V1.8.jsx
executable file
·1209 lines (1089 loc) · 65.9 KB
/
RemoveRedundantPoints.V1.8.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
RemovetRedundantPoints.jsx
A Javascript for Adobe Illustrator
Author:
Jim Heck
Purpose:
Remove anchorpoints on selected path that are coincident in location.
Finds and optionally removes redundant points from each selected PathItem.
Useful for cleaning up after Offset Path and Outline Stroke commands in CS3.
To Use:
Select the paths, including compound paths to be affected. If no path is
selected, the script will run for all paths in the document that are not
locked. Run the script.
Rights:
This work is licensed under the Creative Commons Attribution 3.0 United States
License. To view a copy of this license, visit
http://creativecommons.org/licenses/by/3.0/us/
or send a letter to Creative Commons, 171 Second Street, Suite 300,
San Francisco, California, 94105, USA.
Version History:
1.8 120108 More fixes to the logic for keeping only a single point.
The direction handles for the remaining point are now correctly calculated
based on relative angle and distance to the original anchor to which a
handle related. Also if a SMOOTH point is remaining, the angle of the
two direction handles has been tweaked to be exactly 180 degrees for
consistency with the definition of a SMOOTH point.
1.7 120106 Change the way direction handles and PointType are handled
when keeping only one point. Retract handles less than 0.5 points to keep
consistent angles for SMOOTH point types. If keepLeadingPoint or
keepTrailingPoint is specified, try to keep the PointType of that point.
In the absence of other indicators, base PointType on the point (leading
or trailing) that has an extended handle.
1.6.1 090914 Tweak defaults to make sense for my work style
1.6 090411 Fix a bug in creating a temporary path. Fix a bug in
findRedundantPoints(), when searching backwards, the tolerance was always
applied against the first point on the path instead of the adjacent point
along the path. Change selection options so that there is more control over
which points are processed on a given path. The new options allow for ignoring
selection of points, restricting processing to only selected points, or
processing redundant points if at least one of them is selected. Correct count
of redundant points removed when both leading and trailing points retained.
1.5 090404 Change default action to remove. Fix a major performance issue
in the docGetSelectedPaths() routine. Searching through all the paths in very
complex files takes a LONG time. Instead, search the document groups array.
BETA: Lots of hacking on the removeRedundantPoints() routine to improve the
look of the resulting curve when applied to the oxbow problem.
1.4.1 090331 Fix a bug in removeRedundantPoints(), needed to compare absolute
error to a tolerance. Also, loosen up the error criteria so less smooth
points are mischaracterized as corners. Tolerance is now 0.02 radians,
which is about 1.15 degrees. For some reason, the redundant points on
arbitrary (non-geometric) outlined paths have lots of slop in the
direction handles.
1.4 090331 Add options to control which points are left on removal, leading,
trailing or both. If neither is retained, the redundant point positions
are averaged. If both are retained, intermediate points are removed, and
inward facing control handles are synthesized (see comments below).
Switched to using the atan2 function instead of atan for calculating theta.
Fixed bugs in docGetSelectedPaths(). CompoundPathItem objects don't seem
to have properly formed pathItems arrays when they contain what appear to
be groups of paths. Also, layer objects can contain layers.
1.3.1 090327 Limit user entered tolerance value to something sane.
1.3 090327 Add user controls to specify a tolerance in points for identifying
redundant points.
1.2.3 090327 Improve results dialog verbiage.
1.2.2 090326 Bug fix. Observe proper unlocking and locking order for
handling of locked objects.
1.2.1 090326 Minor bug fixes including, restricting selection only option
to not be allowed with selection function.
1.2 090326 Add option to remove only redundant points sets with at least one
selected point. Fix broken option to remove locked redundant points.
Add results dialog OK button.
1.1 090325 Improve select action to work across selection or entire document.
Handle nested layers in docGetSelectedPaths(). Clean whitespace.
1.0.1 090325 Minor bug fix.
1.0 090311 Initial release.
*/
/*******************************************************************************
* Function: getPairTheta
* Description:
* Return the angle relative to the X axis from the line formed between
* two points, which are passed in as arguments. The angle is measured
* relative to point A (as if A were relocated to the origin, and the angle
* is measured to the X axis itself). The arguments are expected to be
* arrays of two numbers (X, Y) defining the point. The return value is in
* radians (PI to -PI)
*/
function getPairTheta(pairA,pairB){
var deltaX=pairB[0]-pairA[0];
var deltaY=pairB[1]-pairA[1];
/*alert("deltaX="+deltaX+" deltaY="+deltaY);*/
return(Math.atan2(deltaY, deltaX));
}
/*******************************************************************************
* Function: getPairDistance
* Description:
* Return the distance between two points. The arguments are expected to be
* arrays of two numbers (X, Y) defining the point. The return value is the
* distance in units relative to the inputs.
*/
function getPairDistance(pairA,pairB){
var deltaX=pairB[0]-pairA[0];
var deltaY=pairB[1]-pairA[1];
return(Math.sqrt((deltaX*deltaX)+(deltaY*deltaY)));
}
/*******************************************************************************
* Function: findRedundantPoints
* Description:
* Find all sets of redundant points for the input path. A redundant point
* is defined as one that has the same anchor location as a neighboring point.
* The arguments are a path to work on, the tolerance in points to apply to
* determine a point is redundant, and a boolean to indicate that only
* groups of redundant points where at least one point is selected should
* be considered. The return value is an Array of Arrays containing the
* indicies of the redundant pathPoint objects found in the path.
*/
function findRedundantPoints(path, tolerance, anySelected, allSelected){
var anchorDistance = 0;
var redundantPointSets = new Array();
var redundantPoint = new Array();
var selectedRedundantPointSets = new Array();
var selectedRedundantPoint = new Array();
var i = 0;
var j = 0;
var k = 0;
var index;
var selected = false;
if(path.pathPoints.length > 1) {
/*
* The first path point may be coincident with some at the end of the path
* so we check going backwards first. Redundant points pushed on the
* front of the array so they stay in order leftmost to rightmost.
*/
redundantPoint.push(0);
index = 0;
for (i=path.pathPoints.length-1; i>0; i--) {
/*
* Get distance and round to nearest hundredth of a point.
* If points are closer than the tolerance, consider them
* coincident.
*/
anchorDistance = getPairDistance(path.pathPoints[index].anchor, path.pathPoints[i].anchor);
anchorDistance = roundToPrecision(anchorDistance, 0.01);
if (anchorDistance < tolerance) {
redundantPoint.unshift(i);
}
else {
break;
}
index = i;
}
/*
* If we haven't used up all the points, start searching forwards
* up to the point we stopped searching backwards. Test the
* current point against the next point. If the next point matches push
* its index onto the redundantPoint array. When the first one doesn't match,
* check if the redundantPoint array has more than one index. If so add it
* to the redundantPointSets array. Then clean the redundantPoint array
* and push on the next point index.
*/
if(i > 0) {
for (j=0; j<i; j++) {
anchorDistance = getPairDistance(path.pathPoints[j].anchor, path.pathPoints[j+1].anchor);
anchorDistance = roundToPrecision(anchorDistance, 0.01);
if (anchorDistance < tolerance) {
redundantPoint.push(j+1);
}
else {
if (redundantPoint.length > 1) {
redundantPointSets.push(redundantPoint);
}
redundantPoint = [];
redundantPoint.push(j+1);
}
}
}
/*
* Push the last redundantPoint array onto the redundantPointSets array if
* its length is greater than one.
*/
if (redundantPoint.length > 1) {
redundantPointSets.push(redundantPoint);
}
}
if (anySelected) {
for (i=0; i<redundantPointSets.length; i++) {
var currentPointSet = redundantPointSets[i];
selected = false;
for (j=0; j<currentPointSet.length; j++) {
if (path.pathPoints[currentPointSet[j]].selected ==
PathPointSelection.ANCHORPOINT) {
selected = true;
}
}
if (selected) {
selectedRedundantPointSets.push(currentPointSet);
}
}
}
else if (allSelected) {
for (i=0; i<redundantPointSets.length; i++) {
var currentPointSet = redundantPointSets[i];
for (j=currentPointSet.length-1; j>=0; j--) {
var currentPoint = path.pathPoints[currentPointSet[j]];
if (currentPoint.selected == PathPointSelection.ANCHORPOINT) {
selectedRedundantPoint.unshift(currentPointSet[j]);
}
else {
break;
}
}
if (j > 0) {
for (k=0; k<j; k++) {
var currentPoint = path.pathPoints[currentPointSet[k]];
if (currentPoint.selected == PathPointSelection.ANCHORPOINT) {
selectedRedundantPoint.push(currentPointSet[k]);
}
else {
if (selectedRedundantPoint.length > 1) {
selectedRedundantPointSets.push(selectedRedundantPoint);
}
selectedRedundantPoint = [];
}
}
}
if (selectedRedundantPoint.length > 1) {
selectedRedundantPointSets.push(selectedRedundantPoint);
}
selectedRedundantPoint = [];
}
}
else {
selectedRedundantPointSets = redundantPointSets;
}
return(selectedRedundantPointSets);
}
/*******************************************************************************
* Function: countRedundantPoints
* Description:
* Count the number of redundant points given a redundantPointSets array as
* the first parameter.
*/
function countRedundantPoints(redundantPointSets, doKeepLeadingPoint, doKeepTrailingPoint) {
var i = 0;
var redundantPoints = 0;
var pointsKept = 1;
if (doKeepLeadingPoint && doKeepTrailingPoint) {
pointsKept = 2;
}
for (i=0; i<redundantPointSets.length; i++) {
redundantPoints += redundantPointSets[i].length - pointsKept;
}
return (redundantPoints);
}
/*******************************************************************************
* Function: countSelectedPoints
* Description:
* Count the number of selected anchor points given a path as the first parameter.
*/
function countSelectedPoints(path) {
var i = 0;
var selectedPoints = 0;
for (i=0; i<path.pathPoints.length; i++) {
if (path.pathPoints[i].selected == PathPointSelection.ANCHORPOINT) {
selectedPoints++;
}
}
return (selectedPoints);
}
/*******************************************************************************
* Function: removeRedundantPoints
* Description:
* Remove redundant points from a path input as the first parameter. The
* second input parameter should be an array of arrays containing the
* indicies of redundant points, as returned from function
* findRedundantPoints(). From each set of indicies, the first point is
* retained, and the subsequent points are removed from the path. Care is
* taken to preserve the proper leftDirection and rightDirection handles,
* as well as the proper PointType for the remaining point. Returns
* the number of points removed.
*/
function removeRedundantPoints(path, redundantPointSets, keepLeadingPoint, keepTrailingPoint, keepAveragedPoint){
var i = 0;
var j = 0;
var pointsToRemove = new Array();
var tempLayer;
var tempPath;
/*
* For each array of redundant point indicies in array redundantPointSets,
* modify the leadingPoint to have all the properties needed to properly
* describe the set of coincident points.
*/
for (i=0; i<redundantPointSets.length; i++) {
var x = 0;
var y = 0;
var currentPointSet = redundantPointSets[i];
var leadingPoint = path.pathPoints[currentPointSet[0]];
var trailingPoint = path.pathPoints[currentPointSet[currentPointSet.length-1]];
if (keepLeadingPoint && keepTrailingPoint) {
/*
* JAH 090401 REVISIT COMMENT WHEN DONE
* If we are keeping two points, the leftDirection of the leading point
* and rightDirection of the trailing point are already fixed. We have to
* synthesize the inward facing handles, and choose pointType of the two points.
* To allow easy manipultion of the inner handles without disturbing the fixed
* handles, make the points PointType.CORNER. For the direction handles, make
* them parallel to their respective paired handle, and extend them half the
* distance between the two remaining points.
*/
var averagedPoint;
var theta;
var deltaX;
var deltaY;
var pairDistance;
var leftDistance;
var rightDistance;
var firstRemovedIndex = 1;
if (currentPointSet.length > 2) {
averagedPoint = path.pathPoints[currentPointSet[1]];
}
else {
tempLayer = app.activeDocument.layers.add();
tempLayer.name = "Temp";
tempPath = tempLayer.pathItems.add();
averagedPoint = tempPath.pathPoints.add();
}
if( currentPointSet.length <= 2 || !keepAveragedPoint ) {
/*
* Use just the leading and trailing points. Create inward facing
* direction handles for the two endpoints based on the relationship
* of the angles between each endpoint and the average point.
*
* For each endpoint, calcualte the angle of the endpoint to the
* average point, and the endpoint to the other endpoint. Combine
* the angles. The base angle for the inward facing direction handle
* is the angle that points it towards the average point. Add to this
* angle, a multiple of the difference between the angle just mentioned,
* and the angle to the other endpoint. Adding this difference angle
* will bias the curve towards the average point. Finally, set the
* length of the direction handle as the distance from the endpoint
* to the average point multiplied by a factor.
*/
var thetaAverage;
var thetaPair;
var tweakThetaToOppositeEndpoint = 1.0;
var tweakPairDistance = 0.5;
/*
* Since the leading and trailing points will have direction handles pointing
* in different directions, these points must be corner points by necessity.
*/
leadingPoint.pointType = PointType.CORNER;
trailingPoint.pointType = PointType.CORNER;
/*
* Create new average point.
*/
for (j=0; j<currentPointSet.length; j++) {
x += path.pathPoints[currentPointSet[j]].anchor[0];
y += path.pathPoints[currentPointSet[j]].anchor[1];
}
x /= currentPointSet.length;
y /= currentPointSet.length;
averagedPoint.anchor = Array(x, y);
averagedPoint.leftDirection = Array( averagedPoint.anchor[0], averagedPoint.anchor[1]);
averagedPoint.rightDirection = Array( averagedPoint.anchor[0], averagedPoint.anchor[1]);
averagedPoint.pointType = PointType.CORNER;
/* Calcualte new leading point rightDirection */
pairDistance = getPairDistance(leadingPoint.anchor, averagedPoint.anchor);
thetaAverage = getPairTheta(leadingPoint.anchor, averagedPoint.anchor);
thetaPair = getPairTheta(leadingPoint.anchor, trailingPoint.anchor);
theta = thetaAverage + tweakThetaToOppositeEndpoint * (thetaAverage - thetaPair);
/*alert("thetaAverage="+thetaAverage+" thetaPair="+thetaPair" theta="+theta);*/
deltaX = Math.cos(theta) * tweakPairDistance * pairDistance;
deltaY = Math.sin(theta) * tweakPairDistance * pairDistance;
leadingPoint.rightDirection = Array(leadingPoint.anchor[0]+deltaX, leadingPoint.anchor[1]+deltaY);
/* Calcualte new trailing point leftDirection */
pairDistance = getPairDistance(trailingPoint.anchor, averagedPoint.anchor);
thetaAverage = getPairTheta(trailingPoint.anchor, averagedPoint.anchor);
thetaPair = getPairTheta(trailingPoint.anchor, leadingPoint.anchor);
theta = thetaAverage + tweakThetaToOppositeEndpoint * (thetaAverage - thetaPair);
/*alert("thetaAverage="+thetaAverage+" thetaPair="+thetaPair" theta="+theta);*/
deltaX = Math.cos(theta) * tweakPairDistance * pairDistance;
deltaY = Math.sin(theta) * tweakPairDistance * pairDistance;
trailingPoint.leftDirection = Array(trailingPoint.anchor[0]+deltaX, trailingPoint.anchor[1]+deltaY);
}
else {
/*
* Use just the leading and trailing points, along with a third point added
* at the average of all the removed points. This point will act to anchor
* the curve at the average point. It will also allow the leading and
* trailing points to be smooth points, allowing for a continuous
* curve through them.
*
* The inward facing direction handles for the two endpoints will be
* shortened extensions of the outward facing direction handles for these
* points. The length of the handles will be a multiple of the
* distance from the direction handle to the average point.
*
* For the average point, the direction handles will be parallel to the
* angle formed by the angle between the two endpoints. The length
* of the direction handles for this point will be a different multiple
* of the length from each endpoint to the average point.
*/
var thetaAverage;
var thetaPair;
var tweakPairDistanceForAveraged = 0.5;
var tweakPairDistanceForEndpoint = 0.25;
/*
* Since the leading and trailing points will have direction handles that
* are parallel, make them smooth points.
*/
leadingPoint.pointType = PointType.SMOOTH;
trailingPoint.pointType = PointType.SMOOTH;
/* We will be keeping one more point, the averaged point. */
firstRemovedIndex = 2;
/*
* Create new average point.
*/
for (j=0; j<currentPointSet.length; j++) {
x += path.pathPoints[currentPointSet[j]].anchor[0];
y += path.pathPoints[currentPointSet[j]].anchor[1];
}
x /= currentPointSet.length;
y /= currentPointSet.length;
averagedPoint.anchor = Array(x, y);
averagedPoint.leftDirection = Array( averagedPoint.anchor[0], averagedPoint.anchor[1]);
averagedPoint.rightDirection = Array( averagedPoint.anchor[0], averagedPoint.anchor[1]);
averagedPoint.pointType = PointType.SMOOTH;
/* Calcualte new averaged point leftDirection */
pairDistance = getPairDistance(leadingPoint.anchor, averagedPoint.anchor);
theta = getPairTheta(leadingPoint.anchor, trailingPoint.anchor);
/*alert("theta="+theta);*/
if (theta > 0) {
theta += Math.PI;
}
else {
theta += -Math.PI;
}
deltaX = Math.cos(theta) * tweakPairDistanceForAveraged * pairDistance;
deltaY = Math.sin(theta) * tweakPairDistanceForAveraged * pairDistance;
averagedPoint.leftDirection = Array(averagedPoint.anchor[0]+deltaX, averagedPoint.anchor[1]+deltaY);
/* Calcualte new averaged point rightDirection */
pairDistance = getPairDistance(trailingPoint.anchor, averagedPoint.anchor);
theta = getPairTheta(trailingPoint.anchor, averagedPoint.anchor);
/*alert("theta="+theta);*/
if (theta > 0) {
theta += Math.PI;
}
else {
theta += -Math.PI;
}
deltaX = Math.cos(theta) * tweakPairDistanceForAveraged * pairDistance;
deltaY = Math.sin(theta) * tweakPairDistanceForAveraged * pairDistance;
averagedPoint.rightDirection = Array(averagedPoint.anchor[0]+deltaX, averagedPoint.anchor[1]+deltaY);
/* Calculate direction handles for leading and trailing points */
pairDistance = getPairDistance(leadingPoint.anchor, trailingPoint.anchor);
leftDistance = getPairDistance(leadingPoint.anchor, leadingPoint.leftDirection);
if (leftDistance > 0) {
theta = getPairTheta(leadingPoint.anchor, leadingPoint.leftDirection);
/*alert("theta="+theta);*/
if (theta > 0) {
theta += Math.PI;
}
else {
theta += -Math.PI;
}
pairDistance = getPairDistance(leadingPoint.anchor, averagedPoint.anchor);
deltaX = Math.cos(theta) * tweakPairDistanceForEndpoint * pairDistance;
deltaY = Math.sin(theta) * tweakPairDistanceForEndpoint * pairDistance;
leadingPoint.rightDirection = Array(leadingPoint.anchor[0]+deltaX, leadingPoint.anchor[1]+deltaY);
}
else {
leadingPoint.rightDirection = leadingPoint.anchor;
}
rightDistance = getPairDistance(trailingPoint.anchor, trailingPoint.rightDirection);
if (rightDistance > 0) {
theta = getPairTheta(trailingPoint.anchor, trailingPoint.rightDirection);
if (theta > 0) {
theta += Math.PI;
}
else {
theta += -Math.PI;
}
pairDistance = getPairDistance(trailingPoint.anchor, averagedPoint.anchor);
deltaX = Math.cos(theta) * tweakPairDistanceForEndpoint * pairDistance;
deltaY = Math.sin(theta) * tweakPairDistanceForEndpoint * pairDistance;
trailingPoint.leftDirection = Array(trailingPoint.anchor[0]+deltaX, trailingPoint.anchor[1]+deltaY);
}
else {
trailingPoint.leftDirection = trailingPoint.anchor;
}
}
/*
* Push all points other than the leading and trailing onto the pointsToRemove array
* for later removal. We can't remove them while we are working with later sets.
*/
for (j=firstRemovedIndex; j<currentPointSet.length-1; j++) {
pointsToRemove.push(currentPointSet[j]);
}
}
else {
/*
* If we are only keeping one point, we will work with the leading point.
* First, calculate the relative distances and angles of the direction handle for
* the leadingPoint leftDirection handle and the trailingPoint rightDirection
* handle. These values will be used to help properly construct the remaining
* point.
*/
var leftDistance = getPairDistance(leadingPoint.anchor, leadingPoint.leftDirection);
var rightDistance = getPairDistance(trailingPoint.anchor, trailingPoint.rightDirection);
var leftTheta = getPairTheta(leadingPoint.anchor, leadingPoint.leftDirection);
var rightTheta = getPairTheta(trailingPoint.anchor, trailingPoint.rightDirection);
/*
* If we are keeping the leadingPoint, calculate a relative rightDirection handle
* based on the trailingPoint rightDistance and rightTheta. If we are keeping the
* trailingPoint, copy its anchor and rightDirection handle to the leadingPoint,
* and calculate a relative leftDirection handle based on the leadingPoint
* leftDistance and leftTheta. If we are to keep neither leading or trailing point,
* average the position of all the redundant points and calcuate direction handles
* based on the appropriate values.
*/
if (keepLeadingPoint) {
x = leadingPoint.anchor[0] + (Math.cos(rightTheta) * rightDistance);
y = leadingPoint.anchor[1] + (Math.sin(rightTheta) * rightDistance);
leadingPoint.rightDirection = Array(x, y);
}
else if (keepTrailingPoint) {
leadingPoint.anchor = trailingPoint.anchor;
leadingPoint.rightDirection = trailingPoint.rightDirection;
x = leadingPoint.anchor[0] + (Math.cos(leftTheta) * leftDistance);
y = leadingPoint.anchor[1] + (Math.sin(leftTheta) * leftDistance);
leadingPoint.leftDirection = Array(x, y);
}
else {
for (j=0; j<currentPointSet.length; j++) {
x += path.pathPoints[currentPointSet[j]].anchor[0];
y += path.pathPoints[currentPointSet[j]].anchor[1];
}
x /= currentPointSet.length;
y /= currentPointSet.length;
leadingPoint.anchor = Array(x, y);
x = leadingPoint.anchor[0] + (Math.cos(leftTheta) * leftDistance);
y = leadingPoint.anchor[1] + (Math.sin(leftTheta) * leftDistance);
leadingPoint.leftDirection = Array(x, y);
x = leadingPoint.anchor[0] + (Math.cos(rightTheta) * rightDistance);
y = leadingPoint.anchor[1] + (Math.sin(rightTheta) * rightDistance);
leadingPoint.rightDirection = Array(x, y);
}
/*
* If the distance for a handle is less than half a point and rounds to zero,
* retract that handle fully by setting that direction handle equal to the anchor
* point. This will keep angles consistent for smooth points.
*/
if (Math.round(leftDistance) == 0) {
leadingPoint.leftDirection = leadingPoint.anchor;
}
if (Math.round(rightDistance) == 0) {
leadingPoint.rightDirection = leadingPoint.anchor;
}
/*
* Handle the PointType in a minimal manner. If keeping the leadingPoint or keeping
* the trailingPoint, keep the PointType of that point if possible. If both handles
* are extended, measure the angles of the two direction handles. If both handles
* have the same angle relative to the X axis within a tolerance, the PointType
* can be SMOOTH, otherwise it must be CORNER. If the point type is SMOOTH, ensure
* the direction handles are corrected to be exactly 180 degrees apart.
*
* If not specifically keeping the leading or trailing point and only one handle is
* extended, base the pointType on the the leadingPoint if only the left handle is
* extended and the trailingPoint if only the right handle is extended.
*/
if (Math.round(leftDistance) > 0 && Math.round(rightDistance) > 0) {
var absdiff = Math.abs(leftTheta-rightTheta);
var error = Math.PI - absdiff;
/*alert("leftTheta="+leftTheta+" rightTheta="+rightTheta+" absdiff="+absdiff+" error="+error);*/
if (Math.abs(error) < 0.02) {
if (keepTrailingPoint) {
leadingPoint.pointType = trailingPoint.pointType;
}
else if (!keepLeadingPoint) {
leadingPoint.pointType = PointType.SMOOTH;
}
if (leadingPoint.pointType == PointType.SMOOTH) {
if (keepTrailingPoint) {
x = leadingPoint.anchor[0] + (Math.cos(Math.PI + rightTheta) * leftDistance);
y = leadingPoint.anchor[1] + (Math.sin(Math.PI + rightTheta) * leftDistance);
leadingPoint.leftDirection = Array(x, y);
}
else {
x = leadingPoint.anchor[0] + (Math.cos(Math.PI + leftTheta) * rightDistance);
y = leadingPoint.anchor[1] + (Math.sin(Math.PI + leftTheta) * rightDistance);
leadingPoint.rightDirection = Array(x, y);
}
}
}
else {
leadingPoint.pointType = PointType.CORNER;
}
}
else if (keepTrailingPoint) {
leadingPoint.pointType = trailingPoint.pointType;
}
else if (!keepLeadingPoint && rightDistance > 0) {
leadingPoint.pointType = trailingPoint.pointType;
}
/*
* Push all other points onto the pointsToRemove array for later removal. We can't
* remove them while we are working with later sets.
*/
for (j=1; j<currentPointSet.length; j++) {
pointsToRemove.push(currentPointSet[j]);
}
}
}
/*
* Sort the pointsToRemove array and then remove the points in reverse order, so the indicies
* remain coherent during the removal.
*/
pointsToRemove.sort(function (a,b) { return a-b });
for (i=pointsToRemove.length-1; i>=0; i--) {
var pointToRemove = path.pathPoints[pointsToRemove[i]];
pointToRemove.remove();
}
if (tempPath) {
tempPath.remove();
}
if (tempLayer) {
tempLayer.remove();
}
return (pointsToRemove.length);
}
/*******************************************************************************
* Function: selectRedundantPoints
* Description:
* Select redundant points on a path input as the first parameter. The
* second input parameter should be an array of arrays containing the
* indicies of redundant points, as returned from function
* findRedundantPoints(). If there are redundant points, deselect all points
* on the path and select the ANCHORPOINT of each redundant point. If there
* are no redundant points on the path, do nothing.
*/
function selectRedundantPoints(path, redundantPointSets){
var i = 0;
var j = 0;
if (redundantPointSets.length > 0) {
for (i=0; i<path.pathPoints.length; i++) {
path.pathPoints[i].selected = PathPointSelection.NOSELECTION;
}
for (i=0; i<redundantPointSets.length; i++) {
var currentPointSet = redundantPointSets[i];
for (j=0; j<currentPointSet.length; j++) {
path.pathPoints[currentPointSet[j]].selected = PathPointSelection.ANCHORPOINT;
}
}
}
}
/*******************************************************************************
* Function: unlockPath
* Description:
* For a path input as the first parameter, unlock the path and any locked
* parent object. Return an array of objects that have been unlocked.
*/
function unlockPath(path){
var unlockedObjects = new Array();
var parentObjects = new Array();
var currentObject = path;
var i = 0;
while (currentObject.typename != "Document") {
parentObjects.unshift(currentObject);
currentObject = currentObject.parent;
}
for (i=0; i<parentObjects.length; i++) {
if (parentObjects[i].locked) {
parentObjects[i].locked = false;
unlockedObjects.unshift(parentObjects[i]);
}
}
return unlockedObjects;
}
/*******************************************************************************
* Function: lockObjects
* Description:
* For a set of objects as the first parameter, lock each object.
*/
function lockObjects(objects){
var i = 0;
for (i=0; i<objects.length; i++) {
objects[i].locked = true;
}
}
/*******************************************************************************
* Function: docGetSelectedPaths
* Description:
* Get all the selected paths for the docRef argument passed in as
* a parameter. The second parameter is a boolean that controls if compound
* path items are included (default true), and the third parameter is a
* boolean that controls if locked objects are included (default false).
* Returns an array of paths.
*/
function docGetSelectedPaths(docRef, includeCompound, includeLocked){
var qualifiedPaths = new Array();
var i = 0;
var j = 0;
var nextPath = null;
var currentSelection = new Array();
var nextSelection = docRef.selection;
if (includeCompound == null) {
includeCompound = true;
}
if (includeLocked == null) {
includeLocked = false;
}
do {
currentSelection = nextSelection;
nextSelection = [];
for(i=0; i<currentSelection.length; i++){
var currentObject=currentSelection[i];
if (currentObject.typename == "PathItem") {
if (includeLocked || !(currentObject.locked ||
currentObject.layer.locked)) {
qualifiedPaths.push(currentObject);
}
}
else if (currentObject.typename == "CompoundPathItem") {
if (includeCompound &&
(includeLocked || !(currentObject.locked ||
currentObject.layer.locked))) {
/*
* For more complex compound paths (e.g. concentric circular bands),
* in CS3 the CompoundPathItem object's pathItems array is empty.
* Inspection of the paths in a document shows the paths contained
* in the CompoundPathItem have groups as parents. To get around
* this seeming bug, in addition to using the pathItems array,
* which still contains individual paths, we also search through
* all the groups in the document adding paths whose parent
* is the CompoundPathItem object.
*
* WARNING this takes non-negligible time in large documents.
*/
for (j=0; j<currentObject.pathItems.length; j++) {
qualifiedPaths.push(currentObject.pathItems[j]);
}
for (j=0; j<docRef.groupItems.length; j++) {
if (docRef.groupItems[j].parent == currentObject) {
nextSelection.push(docRef.groupItems[j]);
}
}
}
}
else if (currentObject.typename == "GroupItem") {
for (j=0; j<currentObject.pathItems.length; j++){
nextSelection.push(currentObject.pathItems[j]);
}
for (j=0; j<currentObject.compoundPathItems.length; j++){
nextSelection.push(currentObject.compoundPathItems[j]);
}
for (j=0; j<currentObject.groupItems.length; j++){
nextSelection.push(currentObject.groupItems[j]);
}
}
else if (currentObject.typename == "Layer") {
for (j=0; j<currentObject.pathItems.length; j++){
nextSelection.push(currentObject.pathItems[j]);
}
for (j=0; j<currentObject.compoundPathItems.length; j++){
nextSelection.push(currentObject.compoundPathItems[j]);
}
for (j=0; j<currentObject.groupItems.length; j++){
nextSelection.push(currentObject.groupItems[j]);
}
for (j=0; j<currentObject.layers.length; j++){
nextSelection.push(currentObject.layers[j]);
}
}
}
} while (nextSelection.length > 0);
return qualifiedPaths;
}
/*******************************************************************************
* Function: docGetAllPaths
* Description:
* Get all the paths for the docRef argument passed in as a parameter.
* The second parameter is a boolean that controls if compound path items are
* included (default true), and the third parameter is a boolean that controls
* if locked objects are included (default false). Returns an array of paths.
*/
function docGetAllPaths(docRef, includeCompound, includeLocked) {
var qualifiedPaths = new Array();
var i = 0;
var nextPath = null;
if (includeCompound == null) {
includeCompound = true;
}
if (includeLocked == null) {
includeLocked = false;
}
for (i=0; i<docRef.pathItems.length; i++) {
nextPath = docRef.pathItems[i];
if (!includeCompound && nextPath.parent.typename == "CompoundPathItem") {
continue;
}
if (!includeLocked && (nextPath.layer.locked == true || nextPath.locked == true)) {
continue;
}
qualifiedPaths.push(nextPath);
}
return qualifiedPaths;
}
/*******************************************************************************
* Function: roundToPrecision
* Description:
* Round a number input as the first parameter to a given precision. The
* second input parameter is the precision to round to (typically a power of
* 10, like 0.1). Returns the rounded value.
*/
function roundToPrecision(value, precision) {
var result;
result = value / precision;
result = Math.round(result);
result = result * precision;
return (result);
}
/*******************************************************************************
/*******************************************************************************
/*******************************************************************************
* Main code
*/
var dlgInit = new Window('dialog', 'Redundant Path Points');
doInitDialog(dlgInit);
var exitError;
var tolerance = 1 * (dlgInit.tolerancePnl.editText.text);
var doAnalyze = dlgInit.functionPnl.doAnalyze.value;
var doRemove = dlgInit.functionPnl.doRemove.value;
var doSelect = dlgInit.functionPnl.doSelect.value;
var doKeepLeadingPoint = dlgInit.removalPnl.doKeepLeadingPoint.value;
var doKeepTrailingPoint = dlgInit.removalPnl.doKeepTrailingPoint.value;
var doKeepAveragedPoint = dlgInit.removalPnl.doKeepAveragedPoint.value;
var includeCompound = dlgInit.optionPnl.includeCompound.value;
var includeLocked = dlgInit.optionPnl.includeLocked.value;
var ignoreSelected = dlgInit.selectionPnl.ignoreSelected.value;
var anySelected = dlgInit.selectionPnl.anySelected.value;
var allSelected = dlgInit.selectionPnl.allSelected.value;
var docRef=app.activeDocument;
var pathsToProcess = new Array();
var i = 0;
var j = 0;
var totalPaths = 0;
var totalPointsWithRedundancy = 0;
var totalPointsToRemove = 0;
var totalPointsRemoved = 0;
var totalPointsStarting = 0;
var totalPointsRemaining = 0;
var totalPointsSelected = 0;
var redundantPointSets = new Array();
var unlockedObjects = new Array();
try {
if (exitError != 0) {
throw("exit");
}
exitError = 99;
if (docRef.selection.length > 0) {
pathsToProcess = docGetSelectedPaths(docRef, includeCompound, includeLocked);
}
else {
var doAll = confirm("Run script for all paths in document?");
if (doAll) {
pathsToProcess = docGetAllPaths(docRef, includeCompound, includeLocked);
}
}
if (doSelect) {
if (includeLocked) {
exitError = 2;
throw("exit");
}
if (!ignoreSelected) {
exitError = 3;
throw("exit");
}
docRef.selection = null;
}
for (i=0; i<pathsToProcess.length; i++) {
redundantPointSets = findRedundantPoints(pathsToProcess[i], tolerance, anySelected, allSelected);
totalPaths++;
totalPointsWithRedundancy += redundantPointSets.length;
totalPointsToRemove += countRedundantPoints(redundantPointSets, doKeepLeadingPoint, doKeepTrailingPoint);
totalPointsStarting += pathsToProcess[i].pathPoints.length;
totalPointsSelected += countSelectedPoints(pathsToProcess[i]);
if (doRemove) {
if (includeLocked) {
unlockedObjects = unlockPath(pathsToProcess[i]);
}
else {
unlockedObjects = [];
}
totalPointsRemoved += removeRedundantPoints(pathsToProcess[i], redundantPointSets, doKeepLeadingPoint, doKeepTrailingPoint, doKeepAveragedPoint);
if (unlockedObjects.length > 0) {
lockObjects(unlockedObjects);
}
}
if (doSelect) {
selectRedundantPoints(pathsToProcess[i], redundantPointSets);
}
totalPointsRemaining += pathsToProcess[i].pathPoints.length;
}