Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PojavLauncherTeam
GitHub Repository: PojavLauncherTeam/mobile
Path: blob/master/test/jdk/sun/java2d/marlin/ClipShapeTest.java
41149 views
1
/*
2
* Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved.
3
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4
*
5
* This code is free software; you can redistribute it and/or modify it
6
* under the terms of the GNU General Public License version 2 only, as
7
* published by the Free Software Foundation.
8
*
9
* This code is distributed in the hope that it will be useful, but WITHOUT
10
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12
* version 2 for more details (a copy is included in the LICENSE file that
13
* accompanied this code).
14
*
15
* You should have received a copy of the GNU General Public License version
16
* 2 along with this work; if not, write to the Free Software Foundation,
17
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18
*
19
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20
* or visit www.oracle.com if you need additional information or have any
21
* questions.
22
*/
23
import java.awt.BasicStroke;
24
import java.awt.Color;
25
import java.awt.Graphics2D;
26
import java.awt.RenderingHints;
27
import java.awt.Stroke;
28
import java.awt.Shape;
29
import java.awt.geom.CubicCurve2D;
30
import java.awt.geom.Ellipse2D;
31
import java.awt.geom.Line2D;
32
import java.awt.geom.Path2D;
33
import java.awt.geom.PathIterator;
34
import java.awt.geom.QuadCurve2D;
35
import java.awt.image.BufferedImage;
36
import java.awt.image.DataBufferInt;
37
import java.io.File;
38
import java.io.FileOutputStream;
39
import java.io.IOException;
40
import java.util.Arrays;
41
import java.util.Iterator;
42
import java.util.Locale;
43
import java.util.Random;
44
import java.util.concurrent.atomic.AtomicBoolean;
45
import java.util.concurrent.atomic.AtomicInteger;
46
import java.util.logging.Handler;
47
import java.util.logging.LogRecord;
48
import java.util.logging.Logger;
49
import javax.imageio.IIOImage;
50
import javax.imageio.ImageIO;
51
import javax.imageio.ImageWriteParam;
52
import javax.imageio.ImageWriter;
53
import javax.imageio.stream.ImageOutputStream;
54
55
/**
56
* @test
57
* @bug 8191814
58
* @summary Verifies that Marlin rendering generates the same
59
* images with and without clipping optimization with all possible
60
* stroke (cap/join) and/or dashes or fill modes (EO rules)
61
* for paths made of either 9 lines, 4 quads, 2 cubics (random)
62
* Note: Use the argument -slow to run more intensive tests (too much time)
63
*
64
* @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly
65
* @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly -doDash
66
* @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic
67
* @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic -doDash
68
*/
69
public final class ClipShapeTest {
70
71
// test options:
72
static int NUM_TESTS;
73
74
// shape settings:
75
static ShapeMode SHAPE_MODE;
76
77
static boolean USE_DASHES;
78
static boolean USE_VAR_STROKE;
79
80
static int THRESHOLD_DELTA;
81
static long THRESHOLD_NBPIX;
82
83
// constants:
84
static final boolean DO_FAIL = Boolean.valueOf(System.getProperty("ClipShapeTest.fail", "true"));
85
86
static final boolean TEST_STROKER = true;
87
static final boolean TEST_FILLER = true;
88
89
static final boolean SUBDIVIDE_CURVE = true;
90
static final double SUBDIVIDE_LEN_TH = 50.0;
91
static final boolean TRACE_SUBDIVIDE_CURVE = false;
92
93
static final int TESTW = 100;
94
static final int TESTH = 100;
95
96
// dump path on console:
97
static final boolean DUMP_SHAPE = true;
98
99
static final boolean SHOW_DETAILS = false; // disabled
100
static final boolean SHOW_OUTLINE = true;
101
static final boolean SHOW_POINTS = true;
102
static final boolean SHOW_INFO = false;
103
104
static final int MAX_SHOW_FRAMES = 10;
105
static final int MAX_SAVE_FRAMES = 100;
106
107
// use fixed seed to reproduce always same polygons between tests
108
static final boolean FIXED_SEED = true;
109
110
static final double RAND_SCALE = 3.0;
111
static final double RANDW = TESTW * RAND_SCALE;
112
static final double OFFW = (TESTW - RANDW) / 2.0;
113
static final double RANDH = TESTH * RAND_SCALE;
114
static final double OFFH = (TESTH - RANDH) / 2.0;
115
116
static enum ShapeMode {
117
TWO_CUBICS,
118
FOUR_QUADS,
119
FIVE_LINE_POLYS,
120
NINE_LINE_POLYS,
121
FIFTY_LINE_POLYS,
122
MIXED
123
}
124
125
static final long SEED = 1666133789L;
126
// Fixed seed to avoid any difference between runs:
127
static final Random RANDOM = new Random(SEED);
128
129
static final File OUTPUT_DIR = new File(".");
130
131
static final AtomicBoolean isMarlin = new AtomicBoolean();
132
static final AtomicBoolean isClipRuntime = new AtomicBoolean();
133
134
static {
135
Locale.setDefault(Locale.US);
136
137
// FIRST: Get Marlin runtime state from its log:
138
139
// initialize j.u.l Looger:
140
final Logger log = Logger.getLogger("sun.java2d.marlin");
141
log.addHandler(new Handler() {
142
@Override
143
public void publish(LogRecord record) {
144
final String msg = record.getMessage();
145
if (msg != null) {
146
// last space to avoid matching other settings:
147
if (msg.startsWith("sun.java2d.renderer ")) {
148
isMarlin.set(msg.contains("DMarlinRenderingEngine"));
149
}
150
if (msg.startsWith("sun.java2d.renderer.clip.runtime.enable")) {
151
isClipRuntime.set(msg.contains("true"));
152
}
153
}
154
155
final Throwable th = record.getThrown();
156
// detect any Throwable:
157
if (th != null) {
158
System.out.println("Test failed:\n" + record.getMessage());
159
th.printStackTrace(System.out);
160
161
throw new RuntimeException("Test failed: ", th);
162
}
163
}
164
165
@Override
166
public void flush() {
167
}
168
169
@Override
170
public void close() throws SecurityException {
171
}
172
});
173
174
// enable Marlin logging & internal checks:
175
System.setProperty("sun.java2d.renderer.log", "true");
176
System.setProperty("sun.java2d.renderer.useLogger", "true");
177
178
// disable static clipping setting:
179
System.setProperty("sun.java2d.renderer.clip", "false");
180
System.setProperty("sun.java2d.renderer.clip.runtime.enable", "true");
181
182
// enable subdivider:
183
System.setProperty("sun.java2d.renderer.clip.subdivider", "true");
184
185
// disable min length check: always subdivide curves at clip edges
186
System.setProperty("sun.java2d.renderer.clip.subdivider.minLength", "-1");
187
188
// If any curve, increase curve accuracy:
189
// curve length max error:
190
System.setProperty("sun.java2d.renderer.curve_len_err", "1e-4");
191
192
// cubic min/max error:
193
System.setProperty("sun.java2d.renderer.cubic_dec_d2", "1e-3");
194
System.setProperty("sun.java2d.renderer.cubic_inc_d1", "1e-4");
195
196
// quad max error:
197
System.setProperty("sun.java2d.renderer.quad_dec_d2", "5e-4");
198
}
199
200
private static void resetOptions() {
201
NUM_TESTS = Integer.getInteger("ClipShapeTest.numTests", 5000);
202
203
// shape settings:
204
SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
205
206
USE_DASHES = false;
207
USE_VAR_STROKE = false;
208
}
209
210
/**
211
* Test
212
* @param args
213
*/
214
public static void main(String[] args) {
215
{
216
// Bootstrap: init Renderer now:
217
final BufferedImage img = newImage(TESTW, TESTH);
218
final Graphics2D g2d = initialize(img, null);
219
220
try {
221
paintShape(new Line2D.Double(0,0,100,100), g2d, true, false);
222
} finally {
223
g2d.dispose();
224
}
225
226
if (!isMarlin.get()) {
227
throw new RuntimeException("Marlin renderer not used at runtime !");
228
}
229
if (!isClipRuntime.get()) {
230
throw new RuntimeException("Marlin clipping not enabled at runtime !");
231
}
232
}
233
234
System.out.println("---------------------------------------");
235
System.out.println("ClipShapeTest: image = " + TESTW + " x " + TESTH);
236
237
resetOptions();
238
239
boolean runSlowTests = false;
240
241
for (String arg : args) {
242
if ("-slow".equals(arg)) {
243
runSlowTests = true;
244
} else if ("-doDash".equals(arg)) {
245
USE_DASHES = true;
246
} else if ("-doVarStroke".equals(arg)) {
247
USE_VAR_STROKE = true;
248
} else {
249
// shape mode:
250
if (arg.equalsIgnoreCase("-poly")) {
251
SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
252
} else if (arg.equalsIgnoreCase("-bigpoly")) {
253
SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS;
254
} else if (arg.equalsIgnoreCase("-quad")) {
255
SHAPE_MODE = ShapeMode.FOUR_QUADS;
256
} else if (arg.equalsIgnoreCase("-cubic")) {
257
SHAPE_MODE = ShapeMode.TWO_CUBICS;
258
} else if (arg.equalsIgnoreCase("-mixed")) {
259
SHAPE_MODE = ShapeMode.MIXED;
260
}
261
}
262
}
263
264
System.out.println("Shape mode: " + SHAPE_MODE);
265
266
// adjust image comparison thresholds:
267
switch (SHAPE_MODE) {
268
case TWO_CUBICS:
269
// Define uncertainty for curves:
270
THRESHOLD_DELTA = 32;
271
THRESHOLD_NBPIX = (USE_DASHES) ? 50 : 200;
272
if (SUBDIVIDE_CURVE) {
273
THRESHOLD_NBPIX = 4;
274
}
275
break;
276
case FOUR_QUADS:
277
case MIXED:
278
// Define uncertainty for quads:
279
// curve subdivision causes curves to be smaller
280
// then curve offsets are different (more accurate)
281
THRESHOLD_DELTA = 64;
282
THRESHOLD_NBPIX = (USE_DASHES) ? 40 : 420;
283
if (SUBDIVIDE_CURVE) {
284
THRESHOLD_NBPIX = 10;
285
}
286
break;
287
default:
288
// Define uncertainty for lines:
289
// float variant have higher uncertainty
290
THRESHOLD_DELTA = 2;
291
THRESHOLD_NBPIX = (USE_DASHES) ? 6 : 0;
292
}
293
294
// Visual inspection (low threshold):
295
// THRESHOLD_NBPIX = 2;
296
297
System.out.println("THRESHOLD_DELTA: " + THRESHOLD_DELTA);
298
System.out.println("THRESHOLD_NBPIX: " + THRESHOLD_NBPIX);
299
300
if (runSlowTests) {
301
NUM_TESTS = 10000; // or 100000 (very slow)
302
USE_VAR_STROKE = true;
303
}
304
305
System.out.println("NUM_TESTS: " + NUM_TESTS);
306
307
if (USE_DASHES) {
308
System.out.println("USE_DASHES: enabled.");
309
}
310
if (USE_VAR_STROKE) {
311
System.out.println("USE_VAR_STROKE: enabled.");
312
}
313
if (!DO_FAIL) {
314
System.out.println("DO_FAIL: disabled.");
315
}
316
317
System.out.println("---------------------------------------");
318
319
final DiffContext allCtx = new DiffContext("All Test setups");
320
final DiffContext allWorstCtx = new DiffContext("Worst(All Test setups)");
321
322
int failures = 0;
323
final long start = System.nanoTime();
324
try {
325
if (TEST_STROKER) {
326
final float[][] dashArrays = (USE_DASHES) ?
327
// small
328
// new float[][]{new float[]{1f, 2f}}
329
// normal
330
new float[][]{new float[]{13f, 7f}}
331
// large (prime)
332
// new float[][]{new float[]{41f, 7f}}
333
// none
334
: new float[][]{null};
335
336
System.out.println("dashes: " + Arrays.deepToString(dashArrays));
337
338
final float[] strokeWidths = (USE_VAR_STROKE)
339
? new float[5] :
340
new float[]{10f};
341
342
int nsw = 0;
343
if (USE_VAR_STROKE) {
344
for (float width = 0.25f; width < 110f; width *= 5f) {
345
strokeWidths[nsw++] = width;
346
}
347
} else {
348
nsw = 1;
349
}
350
351
System.out.println("stroke widths: " + Arrays.toString(strokeWidths));
352
353
// Stroker tests:
354
for (int w = 0; w < nsw; w++) {
355
final float width = strokeWidths[w];
356
357
for (float[] dashes : dashArrays) {
358
359
for (int cap = 0; cap <= 2; cap++) {
360
361
for (int join = 0; join <= 2; join++) {
362
363
failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, width, cap, join, dashes));
364
failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, width, cap, join, dashes));
365
}
366
}
367
}
368
}
369
}
370
371
if (TEST_FILLER) {
372
// Filler tests:
373
failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO));
374
failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO));
375
376
failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD));
377
failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD));
378
}
379
} catch (IOException ioe) {
380
throw new RuntimeException(ioe);
381
}
382
System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");
383
384
allWorstCtx.dump();
385
allCtx.dump();
386
387
if (DO_FAIL && (failures != 0)) {
388
throw new RuntimeException("Clip test failures : " + failures);
389
}
390
}
391
392
static int paintPaths(final DiffContext allCtx, final DiffContext allWorstCtx, final TestSetup ts) throws IOException {
393
final long start = System.nanoTime();
394
395
if (FIXED_SEED) {
396
// Reset seed for random numbers:
397
RANDOM.setSeed(SEED);
398
}
399
400
System.out.println("paintPaths: " + NUM_TESTS
401
+ " paths (" + SHAPE_MODE + ") - setup: " + ts);
402
403
final boolean fill = !ts.isStroke();
404
final Path2D p2d = new Path2D.Double(ts.windingRule);
405
406
final Stroke stroke = (!fill) ? createStroke(ts) : null;
407
408
final BufferedImage imgOn = newImage(TESTW, TESTH);
409
final Graphics2D g2dOn = initialize(imgOn, stroke);
410
411
final BufferedImage imgOff = newImage(TESTW, TESTH);
412
final Graphics2D g2dOff = initialize(imgOff, stroke);
413
414
final BufferedImage imgDiff = newImage(TESTW, TESTH);
415
416
final DiffContext testSetupCtx = new DiffContext("Test setup");
417
final DiffContext testWorstCtx = new DiffContext("Worst");
418
final DiffContext testWorstThCtx = new DiffContext("Worst(>threshold)");
419
420
int nd = 0;
421
try {
422
final DiffContext testCtx = new DiffContext("Test");
423
final DiffContext testThCtx = new DiffContext("Test(>threshold)");
424
BufferedImage diffImage;
425
426
for (int n = 0; n < NUM_TESTS; n++) {
427
genShape(p2d, ts);
428
429
// Runtime clip setting OFF:
430
paintShape(p2d, g2dOff, fill, false);
431
432
// Runtime clip setting ON:
433
paintShape(p2d, g2dOn, fill, true);
434
435
/* compute image difference if possible */
436
diffImage = computeDiffImage(testCtx, testThCtx, imgOn, imgOff, imgDiff);
437
438
// Worst (total)
439
if (testCtx.isDiff()) {
440
if (testWorstCtx.isWorse(testCtx, false)) {
441
testWorstCtx.set(testCtx);
442
}
443
if (testWorstThCtx.isWorse(testCtx, true)) {
444
testWorstThCtx.set(testCtx);
445
}
446
// accumulate data:
447
testSetupCtx.add(testCtx);
448
}
449
if (diffImage != null) {
450
nd++;
451
452
testThCtx.dump();
453
testCtx.dump();
454
455
if (nd < MAX_SHOW_FRAMES) {
456
if (SHOW_DETAILS) {
457
paintShapeDetails(g2dOff, p2d);
458
paintShapeDetails(g2dOn, p2d);
459
}
460
461
if (nd < MAX_SAVE_FRAMES) {
462
if (DUMP_SHAPE) {
463
dumpShape(p2d);
464
}
465
466
final String testName = "Setup_" + ts.id + "_test_" + n;
467
468
saveImage(imgOff, OUTPUT_DIR, testName + "-off.png");
469
saveImage(imgOn, OUTPUT_DIR, testName + "-on.png");
470
saveImage(imgDiff, OUTPUT_DIR, testName + "-diff.png");
471
}
472
}
473
}
474
}
475
} finally {
476
g2dOff.dispose();
477
g2dOn.dispose();
478
479
if (nd != 0) {
480
System.out.println("paintPaths: " + NUM_TESTS + " paths - "
481
+ "Number of differences = " + nd
482
+ " ratio = " + (100f * nd) / NUM_TESTS + " %");
483
}
484
485
if (testWorstCtx.isDiff()) {
486
testWorstCtx.dump();
487
if (testWorstThCtx.isDiff() && testWorstThCtx.histPix.sum != testWorstCtx.histPix.sum) {
488
testWorstThCtx.dump();
489
}
490
if (allWorstCtx.isWorse(testWorstThCtx, true)) {
491
allWorstCtx.set(testWorstThCtx);
492
}
493
}
494
testSetupCtx.dump();
495
496
// accumulate data:
497
allCtx.add(testSetupCtx);
498
}
499
System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");
500
return nd;
501
}
502
503
private static void paintShape(final Shape p2d, final Graphics2D g2d,
504
final boolean fill, final boolean clip) {
505
reset(g2d);
506
507
setClip(g2d, clip);
508
509
if (fill) {
510
g2d.fill(p2d);
511
} else {
512
g2d.draw(p2d);
513
}
514
}
515
516
private static Graphics2D initialize(final BufferedImage img,
517
final Stroke s) {
518
final Graphics2D g2d = (Graphics2D) img.getGraphics();
519
g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
520
RenderingHints.VALUE_RENDER_QUALITY);
521
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
522
// Test normalize:
523
// RenderingHints.VALUE_STROKE_NORMALIZE
524
RenderingHints.VALUE_STROKE_PURE
525
);
526
527
if (s != null) {
528
g2d.setStroke(s);
529
}
530
g2d.setColor(Color.BLACK);
531
532
return g2d;
533
}
534
535
private static void reset(final Graphics2D g2d) {
536
// Disable antialiasing:
537
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
538
RenderingHints.VALUE_ANTIALIAS_OFF);
539
g2d.setBackground(Color.WHITE);
540
g2d.clearRect(0, 0, TESTW, TESTH);
541
}
542
543
private static void setClip(final Graphics2D g2d, final boolean clip) {
544
// Enable antialiasing:
545
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
546
RenderingHints.VALUE_ANTIALIAS_ON);
547
548
// Enable or Disable clipping:
549
System.setProperty("sun.java2d.renderer.clip.runtime", (clip) ? "true" : "false");
550
}
551
552
static void genShape(final Path2D p2d, final TestSetup ts) {
553
p2d.reset();
554
555
/*
556
Test closed path:
557
0: moveTo + (draw)To + closePath
558
1: (draw)To + closePath (closePath + (draw)To sequence)
559
*/
560
final int end = (ts.closed) ? 2 : 1;
561
562
final double[] in = new double[8];
563
564
double sx0 = 0.0, sy0 = 0.0, x0 = 0.0, y0 = 0.0;
565
566
for (int p = 0; p < end; p++) {
567
if (p <= 0) {
568
x0 = randX(); y0 = randY();
569
p2d.moveTo(x0, y0);
570
sx0 = x0; sy0 = y0;
571
}
572
573
switch (ts.shapeMode) {
574
case MIXED:
575
case FIVE_LINE_POLYS:
576
case NINE_LINE_POLYS:
577
case FIFTY_LINE_POLYS:
578
p2d.lineTo(randX(), randY());
579
p2d.lineTo(randX(), randY());
580
p2d.lineTo(randX(), randY());
581
p2d.lineTo(randX(), randY());
582
x0 = randX(); y0 = randY();
583
p2d.lineTo(x0, y0);
584
if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) {
585
// And an implicit close makes 5 lines
586
break;
587
}
588
p2d.lineTo(randX(), randY());
589
p2d.lineTo(randX(), randY());
590
p2d.lineTo(randX(), randY());
591
x0 = randX(); y0 = randY();
592
p2d.lineTo(x0, y0);
593
if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) {
594
// And an implicit close makes 9 lines
595
break;
596
}
597
if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) {
598
for (int i = 0; i < 41; i++) {
599
x0 = randX(); y0 = randY();
600
p2d.lineTo(x0, y0);
601
}
602
// And an implicit close makes 50 lines
603
break;
604
}
605
case TWO_CUBICS:
606
if (SUBDIVIDE_CURVE) {
607
in[0] = x0; in[1] = y0;
608
in[2] = randX(); in[3] = randY();
609
in[4] = randX(); in[5] = randY();
610
x0 = randX(); y0 = randY();
611
in[6] = x0; in[7] = y0;
612
subdivide(p2d, 8, in);
613
in[0] = x0; in[1] = y0;
614
in[2] = randX(); in[3] = randY();
615
in[4] = randX(); in[5] = randY();
616
x0 = randX(); y0 = randY();
617
in[6] = x0; in[7] = y0;
618
subdivide(p2d, 8, in);
619
} else {
620
x0 = randX(); y0 = randY();
621
p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0);
622
x0 = randX(); y0 = randY();
623
p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0);
624
}
625
if (ts.shapeMode == ShapeMode.TWO_CUBICS) {
626
break;
627
}
628
case FOUR_QUADS:
629
if (SUBDIVIDE_CURVE) {
630
in[0] = x0; in[1] = y0;
631
in[2] = randX(); in[3] = randY();
632
x0 = randX(); y0 = randY();
633
in[4] = x0; in[5] = y0;
634
subdivide(p2d, 6, in);
635
in[0] = x0; in[1] = y0;
636
in[2] = randX(); in[3] = randY();
637
x0 = randX(); y0 = randY();
638
in[4] = x0; in[5] = y0;
639
subdivide(p2d, 6, in);
640
in[0] = x0; in[1] = y0;
641
in[2] = randX(); in[3] = randY();
642
x0 = randX(); y0 = randY();
643
in[4] = x0; in[5] = y0;
644
subdivide(p2d, 6, in);
645
in[0] = x0; in[1] = y0;
646
in[2] = randX(); in[3] = randY();
647
x0 = randX(); y0 = randY();
648
in[4] = x0; in[5] = y0;
649
subdivide(p2d, 6, in);
650
} else {
651
x0 = randX(); y0 = randY();
652
p2d.quadTo(randX(), randY(), x0, y0);
653
x0 = randX(); y0 = randY();
654
p2d.quadTo(randX(), randY(), x0, y0);
655
x0 = randX(); y0 = randY();
656
p2d.quadTo(randX(), randY(), x0, y0);
657
x0 = randX(); y0 = randY();
658
p2d.quadTo(randX(), randY(), x0, y0);
659
}
660
if (ts.shapeMode == ShapeMode.FOUR_QUADS) {
661
break;
662
}
663
default:
664
}
665
666
if (ts.closed) {
667
p2d.closePath();
668
x0 = sx0; y0 = sy0;
669
}
670
}
671
}
672
673
static final int SUBDIVIDE_LIMIT = 5;
674
static final double[][] SUBDIVIDE_CURVES = new double[SUBDIVIDE_LIMIT + 1][];
675
676
static {
677
for (int i = 0, n = 1; i < SUBDIVIDE_LIMIT; i++, n *= 2) {
678
SUBDIVIDE_CURVES[i] = new double[8 * n];
679
}
680
}
681
682
static void subdivide(final Path2D p2d, final int type, final double[] in) {
683
if (TRACE_SUBDIVIDE_CURVE) {
684
System.out.println("subdivide: " + Arrays.toString(Arrays.copyOf(in, type)));
685
}
686
687
double curveLen = ((type == 8)
688
? curvelen(in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7])
689
: quadlen(in[0], in[1], in[2], in[3], in[4], in[5]));
690
691
if (curveLen > SUBDIVIDE_LEN_TH) {
692
if (TRACE_SUBDIVIDE_CURVE) {
693
System.out.println("curvelen: " + curveLen);
694
}
695
696
System.arraycopy(in, 0, SUBDIVIDE_CURVES[0], 0, 8);
697
698
int level = 0;
699
while (curveLen >= SUBDIVIDE_LEN_TH) {
700
level++;
701
curveLen /= 2.0;
702
if (TRACE_SUBDIVIDE_CURVE) {
703
System.out.println("curvelen: " + curveLen);
704
}
705
}
706
707
if (TRACE_SUBDIVIDE_CURVE) {
708
System.out.println("level: " + level);
709
}
710
711
if (level > SUBDIVIDE_LIMIT) {
712
if (TRACE_SUBDIVIDE_CURVE) {
713
System.out.println("max level reached : " + level);
714
}
715
level = SUBDIVIDE_LIMIT;
716
}
717
718
for (int l = 0; l < level; l++) {
719
if (TRACE_SUBDIVIDE_CURVE) {
720
System.out.println("level: " + l);
721
}
722
723
double[] src = SUBDIVIDE_CURVES[l];
724
double[] dst = SUBDIVIDE_CURVES[l + 1];
725
726
for (int i = 0, j = 0; i < src.length; i += 8, j += 16) {
727
if (TRACE_SUBDIVIDE_CURVE) {
728
System.out.println("subdivide: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type)));
729
}
730
if (type == 8) {
731
CubicCurve2D.subdivide(src, i, dst, j, dst, j + 8);
732
} else {
733
QuadCurve2D.subdivide(src, i, dst, j, dst, j + 8);
734
}
735
if (TRACE_SUBDIVIDE_CURVE) {
736
System.out.println("left: " + Arrays.toString(Arrays.copyOfRange(dst, j, j + type)));
737
System.out.println("right: " + Arrays.toString(Arrays.copyOfRange(dst, j + 8, j + 8 + type)));
738
}
739
}
740
}
741
742
// Emit curves at last level:
743
double[] src = SUBDIVIDE_CURVES[level];
744
745
double len = 0.0;
746
747
for (int i = 0; i < src.length; i += 8) {
748
if (TRACE_SUBDIVIDE_CURVE) {
749
System.out.println("curve: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type)));
750
}
751
752
if (type == 8) {
753
if (TRACE_SUBDIVIDE_CURVE) {
754
len += curvelen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]);
755
}
756
p2d.curveTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]);
757
} else {
758
if (TRACE_SUBDIVIDE_CURVE) {
759
len += quadlen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5]);
760
}
761
p2d.quadTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5]);
762
}
763
}
764
765
if (TRACE_SUBDIVIDE_CURVE) {
766
System.out.println("curveLen (final) = " + len);
767
}
768
} else {
769
if (type == 8) {
770
p2d.curveTo(in[2], in[3], in[4], in[5], in[6], in[7]);
771
} else {
772
p2d.quadTo(in[2], in[3], in[4], in[5]);
773
}
774
}
775
}
776
777
static final float POINT_RADIUS = 2f;
778
static final float LINE_WIDTH = 1f;
779
780
static final Stroke OUTLINE_STROKE = new BasicStroke(LINE_WIDTH);
781
static final int COLOR_ALPHA = 128;
782
static final Color COLOR_MOVETO = new Color(255, 0, 0, COLOR_ALPHA);
783
static final Color COLOR_LINETO_ODD = new Color(0, 0, 255, COLOR_ALPHA);
784
static final Color COLOR_LINETO_EVEN = new Color(0, 255, 0, COLOR_ALPHA);
785
786
static final Ellipse2D.Float ELL_POINT = new Ellipse2D.Float();
787
788
private static void paintShapeDetails(final Graphics2D g2d, final Shape shape) {
789
790
final Stroke oldStroke = g2d.getStroke();
791
final Color oldColor = g2d.getColor();
792
793
setClip(g2d, false);
794
795
if (SHOW_OUTLINE) {
796
g2d.setStroke(OUTLINE_STROKE);
797
g2d.setColor(COLOR_LINETO_ODD);
798
g2d.draw(shape);
799
}
800
801
final float[] coords = new float[6];
802
float px, py;
803
804
int nMove = 0;
805
int nLine = 0;
806
int n = 0;
807
808
for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) {
809
int type = it.currentSegment(coords);
810
switch (type) {
811
case PathIterator.SEG_MOVETO:
812
if (SHOW_POINTS) {
813
g2d.setColor(COLOR_MOVETO);
814
}
815
break;
816
case PathIterator.SEG_LINETO:
817
case PathIterator.SEG_QUADTO:
818
case PathIterator.SEG_CUBICTO:
819
if (SHOW_POINTS) {
820
g2d.setColor((nLine % 2 == 0) ? COLOR_LINETO_ODD : COLOR_LINETO_EVEN);
821
}
822
nLine++;
823
break;
824
case PathIterator.SEG_CLOSE:
825
continue;
826
default:
827
System.out.println("unsupported segment type= " + type);
828
continue;
829
}
830
px = coords[0];
831
py = coords[1];
832
833
if (SHOW_INFO) {
834
System.out.println("point[" + (n++) + "|seg=" + type + "]: " + px + " " + py);
835
}
836
837
if (SHOW_POINTS) {
838
ELL_POINT.setFrame(px - POINT_RADIUS, py - POINT_RADIUS,
839
POINT_RADIUS * 2f, POINT_RADIUS * 2f);
840
g2d.fill(ELL_POINT);
841
}
842
}
843
if (SHOW_INFO) {
844
System.out.println("Path moveTo=" + nMove + ", lineTo=" + nLine);
845
System.out.println("--------------------------------------------------");
846
}
847
848
g2d.setStroke(oldStroke);
849
g2d.setColor(oldColor);
850
}
851
852
private static void dumpShape(final Shape shape) {
853
final float[] coords = new float[6];
854
855
for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) {
856
final int type = it.currentSegment(coords);
857
switch (type) {
858
case PathIterator.SEG_MOVETO:
859
System.out.println("p2d.moveTo(" + coords[0] + ", " + coords[1] + ");");
860
break;
861
case PathIterator.SEG_LINETO:
862
System.out.println("p2d.lineTo(" + coords[0] + ", " + coords[1] + ");");
863
break;
864
case PathIterator.SEG_QUADTO:
865
System.out.println("p2d.quadTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ");");
866
break;
867
case PathIterator.SEG_CUBICTO:
868
System.out.println("p2d.curveTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ", " + coords[4] + ", " + coords[5] + ");");
869
break;
870
case PathIterator.SEG_CLOSE:
871
System.out.println("p2d.closePath();");
872
break;
873
default:
874
System.out.println("// Unsupported segment type= " + type);
875
}
876
}
877
System.out.println("--------------------------------------------------");
878
}
879
880
static double randX() {
881
return RANDOM.nextDouble() * RANDW + OFFW;
882
}
883
884
static double randY() {
885
return RANDOM.nextDouble() * RANDH + OFFH;
886
}
887
888
private static BasicStroke createStroke(final TestSetup ts) {
889
return new BasicStroke(ts.strokeWidth, ts.strokeCap, ts.strokeJoin, 10.0f, ts.dashes, 0.0f);
890
}
891
892
private final static class TestSetup {
893
894
static final AtomicInteger COUNT = new AtomicInteger();
895
896
final int id;
897
final ShapeMode shapeMode;
898
final boolean closed;
899
// stroke
900
final float strokeWidth;
901
final int strokeCap;
902
final int strokeJoin;
903
final float[] dashes;
904
// fill
905
final int windingRule;
906
907
TestSetup(ShapeMode shapeMode, final boolean closed,
908
final float strokeWidth, final int strokeCap, final int strokeJoin, final float[] dashes) {
909
this.id = COUNT.incrementAndGet();
910
this.shapeMode = shapeMode;
911
this.closed = closed;
912
this.strokeWidth = strokeWidth;
913
this.strokeCap = strokeCap;
914
this.strokeJoin = strokeJoin;
915
this.dashes = dashes;
916
this.windingRule = Path2D.WIND_NON_ZERO;
917
}
918
919
TestSetup(ShapeMode shapeMode, final boolean closed, final int windingRule) {
920
this.id = COUNT.incrementAndGet();
921
this.shapeMode = shapeMode;
922
this.closed = closed;
923
this.strokeWidth = 0f;
924
this.strokeCap = this.strokeJoin = -1; // invalid
925
this.dashes = null;
926
this.windingRule = windingRule;
927
}
928
929
boolean isStroke() {
930
return this.strokeWidth > 0f;
931
}
932
933
@Override
934
public String toString() {
935
if (isStroke()) {
936
return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed
937
+ ", strokeWidth=" + strokeWidth + ", strokeCap=" + getCap(strokeCap) + ", strokeJoin=" + getJoin(strokeJoin)
938
+ ((dashes != null) ? ", dashes: " + Arrays.toString(dashes) : "")
939
+ '}';
940
}
941
return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed
942
+ ", fill"
943
+ ", windingRule=" + getWindingRule(windingRule) + '}';
944
}
945
946
private static String getCap(final int cap) {
947
switch (cap) {
948
case BasicStroke.CAP_BUTT:
949
return "CAP_BUTT";
950
case BasicStroke.CAP_ROUND:
951
return "CAP_ROUND";
952
case BasicStroke.CAP_SQUARE:
953
return "CAP_SQUARE";
954
default:
955
return "";
956
}
957
958
}
959
960
private static String getJoin(final int join) {
961
switch (join) {
962
case BasicStroke.JOIN_MITER:
963
return "JOIN_MITER";
964
case BasicStroke.JOIN_ROUND:
965
return "JOIN_ROUND";
966
case BasicStroke.JOIN_BEVEL:
967
return "JOIN_BEVEL";
968
default:
969
return "";
970
}
971
972
}
973
974
private static String getWindingRule(final int rule) {
975
switch (rule) {
976
case PathIterator.WIND_EVEN_ODD:
977
return "WIND_EVEN_ODD";
978
case PathIterator.WIND_NON_ZERO:
979
return "WIND_NON_ZERO";
980
default:
981
return "";
982
}
983
}
984
}
985
986
// --- utilities ---
987
private static final int DCM_ALPHA_MASK = 0xff000000;
988
989
public static BufferedImage newImage(final int w, final int h) {
990
return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
991
}
992
993
public static BufferedImage computeDiffImage(final DiffContext testCtx,
994
final DiffContext testThCtx,
995
final BufferedImage tstImage,
996
final BufferedImage refImage,
997
final BufferedImage diffImage) {
998
999
final int[] aRefPix = ((DataBufferInt) refImage.getRaster().getDataBuffer()).getData();
1000
final int[] aTstPix = ((DataBufferInt) tstImage.getRaster().getDataBuffer()).getData();
1001
final int[] aDifPix = ((DataBufferInt) diffImage.getRaster().getDataBuffer()).getData();
1002
1003
// reset diff contexts:
1004
testCtx.reset();
1005
testThCtx.reset();
1006
1007
int ref, tst, dg, v;
1008
for (int i = 0, len = aRefPix.length; i < len; i++) {
1009
ref = aRefPix[i];
1010
tst = aTstPix[i];
1011
1012
// grayscale diff:
1013
dg = (r(ref) + g(ref) + b(ref)) - (r(tst) + g(tst) + b(tst));
1014
1015
// max difference on grayscale values:
1016
v = (int) Math.ceil(Math.abs(dg / 3.0));
1017
if (v <= THRESHOLD_DELTA) {
1018
aDifPix[i] = 0;
1019
} else {
1020
aDifPix[i] = toInt(v, v, v);
1021
testThCtx.add(v);
1022
}
1023
1024
if (v != 0) {
1025
testCtx.add(v);
1026
}
1027
}
1028
1029
testCtx.addNbPix(testThCtx.histPix.count);
1030
1031
if (!testThCtx.isDiff() || (testThCtx.histPix.count <= THRESHOLD_NBPIX)) {
1032
return null;
1033
}
1034
1035
return diffImage;
1036
}
1037
1038
static void saveImage(final BufferedImage image, final File resDirectory, final String imageFileName) throws IOException {
1039
final Iterator<ImageWriter> itWriters = ImageIO.getImageWritersByFormatName("PNG");
1040
if (itWriters.hasNext()) {
1041
final ImageWriter writer = itWriters.next();
1042
1043
final ImageWriteParam writerParams = writer.getDefaultWriteParam();
1044
writerParams.setProgressiveMode(ImageWriteParam.MODE_DISABLED);
1045
1046
final File imgFile = new File(resDirectory, imageFileName);
1047
1048
if (!imgFile.exists() || imgFile.canWrite()) {
1049
System.out.println("saveImage: saving image as PNG [" + imgFile + "]...");
1050
imgFile.delete();
1051
1052
// disable cache in temporary files:
1053
ImageIO.setUseCache(false);
1054
1055
final long start = System.nanoTime();
1056
1057
// PNG uses already buffering:
1058
final ImageOutputStream imgOutStream = ImageIO.createImageOutputStream(new FileOutputStream(imgFile));
1059
1060
writer.setOutput(imgOutStream);
1061
try {
1062
writer.write(null, new IIOImage(image, null, null), writerParams);
1063
} finally {
1064
imgOutStream.close();
1065
1066
final long time = System.nanoTime() - start;
1067
System.out.println("saveImage: duration= " + (time / 1000000l) + " ms.");
1068
}
1069
}
1070
}
1071
}
1072
1073
static int r(final int v) {
1074
return (v >> 16 & 0xff);
1075
}
1076
1077
static int g(final int v) {
1078
return (v >> 8 & 0xff);
1079
}
1080
1081
static int b(final int v) {
1082
return (v & 0xff);
1083
}
1084
1085
static int clamp127(final int v) {
1086
return (v < 128) ? (v > -127 ? (v + 127) : 0) : 255;
1087
}
1088
1089
static int toInt(final int r, final int g, final int b) {
1090
return DCM_ALPHA_MASK | (r << 16) | (g << 8) | b;
1091
}
1092
1093
/* stats */
1094
static class StatInteger {
1095
1096
public final String name;
1097
public long count = 0l;
1098
public long sum = 0l;
1099
public long min = Integer.MAX_VALUE;
1100
public long max = Integer.MIN_VALUE;
1101
1102
StatInteger(String name) {
1103
this.name = name;
1104
}
1105
1106
void reset() {
1107
count = 0l;
1108
sum = 0l;
1109
min = Integer.MAX_VALUE;
1110
max = Integer.MIN_VALUE;
1111
}
1112
1113
void add(int val) {
1114
count++;
1115
sum += val;
1116
if (val < min) {
1117
min = val;
1118
}
1119
if (val > max) {
1120
max = val;
1121
}
1122
}
1123
1124
void add(long val) {
1125
count++;
1126
sum += val;
1127
if (val < min) {
1128
min = val;
1129
}
1130
if (val > max) {
1131
max = val;
1132
}
1133
}
1134
1135
void add(StatInteger stat) {
1136
count += stat.count;
1137
sum += stat.sum;
1138
if (stat.min < min) {
1139
min = stat.min;
1140
}
1141
if (stat.max > max) {
1142
max = stat.max;
1143
}
1144
}
1145
1146
public final double average() {
1147
return ((double) sum) / count;
1148
}
1149
1150
@Override
1151
public String toString() {
1152
final StringBuilder sb = new StringBuilder(128);
1153
toString(sb);
1154
return sb.toString();
1155
}
1156
1157
public final StringBuilder toString(final StringBuilder sb) {
1158
sb.append(name).append("[n: ").append(count);
1159
sb.append("] ");
1160
if (count != 0) {
1161
sb.append("sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average()));
1162
sb.append(" [").append(min).append(" | ").append(max).append("]");
1163
}
1164
return sb;
1165
}
1166
1167
}
1168
1169
final static class Histogram extends StatInteger {
1170
1171
static final int BUCKET = 2;
1172
static final int MAX = 20;
1173
static final int LAST = MAX - 1;
1174
static final int[] STEPS = new int[MAX];
1175
static final int BUCKET_TH;
1176
1177
static {
1178
STEPS[0] = 0;
1179
STEPS[1] = 1;
1180
1181
for (int i = 2; i < MAX; i++) {
1182
STEPS[i] = STEPS[i - 1] * BUCKET;
1183
}
1184
// System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS));
1185
1186
if (THRESHOLD_DELTA % 2 != 0) {
1187
throw new IllegalStateException("THRESHOLD_DELTA must be odd");
1188
}
1189
1190
BUCKET_TH = bucket(THRESHOLD_DELTA);
1191
}
1192
1193
static int bucket(int val) {
1194
for (int i = 1; i < MAX; i++) {
1195
if (val < STEPS[i]) {
1196
return i - 1;
1197
}
1198
}
1199
return LAST;
1200
}
1201
1202
private final StatInteger[] stats = new StatInteger[MAX];
1203
1204
public Histogram(String name) {
1205
super(name);
1206
for (int i = 0; i < MAX; i++) {
1207
stats[i] = new StatInteger(String.format("%5s .. %5s", STEPS[i], ((i + 1 < MAX) ? STEPS[i + 1] : "~")));
1208
}
1209
}
1210
1211
@Override
1212
final void reset() {
1213
super.reset();
1214
for (int i = 0; i < MAX; i++) {
1215
stats[i].reset();
1216
}
1217
}
1218
1219
@Override
1220
final void add(int val) {
1221
super.add(val);
1222
stats[bucket(val)].add(val);
1223
}
1224
1225
@Override
1226
final void add(long val) {
1227
add((int) val);
1228
}
1229
1230
void add(Histogram hist) {
1231
super.add(hist);
1232
for (int i = 0; i < MAX; i++) {
1233
stats[i].add(hist.stats[i]);
1234
}
1235
}
1236
1237
boolean isWorse(Histogram hist, boolean useTh) {
1238
boolean worst = false;
1239
if (!useTh && (hist.sum > sum)) {
1240
worst = true;
1241
} else {
1242
long sumLoc = 0l;
1243
long sumHist = 0l;
1244
// use running sum:
1245
for (int i = MAX - 1; i >= BUCKET_TH; i--) {
1246
sumLoc += stats[i].sum;
1247
sumHist += hist.stats[i].sum;
1248
}
1249
if (sumHist > sumLoc) {
1250
worst = true;
1251
}
1252
}
1253
/*
1254
System.out.println("running sum worst:");
1255
System.out.println("this ? " + toString());
1256
System.out.println("worst ? " + hist.toString());
1257
*/
1258
return worst;
1259
}
1260
1261
@Override
1262
public final String toString() {
1263
final StringBuilder sb = new StringBuilder(2048);
1264
super.toString(sb).append(" { ");
1265
1266
for (int i = 0; i < MAX; i++) {
1267
if (stats[i].count != 0l) {
1268
sb.append("\n ").append(stats[i].toString());
1269
}
1270
}
1271
1272
return sb.append(" }").toString();
1273
}
1274
}
1275
1276
/**
1277
* Adjust the given double value to keep only 3 decimal digits
1278
* @param value value to adjust
1279
* @return double value with only 3 decimal digits
1280
*/
1281
static double trimTo3Digits(final double value) {
1282
return ((long) (1e3d * value)) / 1e3d;
1283
}
1284
1285
static final class DiffContext {
1286
1287
public final Histogram histPix;
1288
1289
public final StatInteger nbPix;
1290
1291
DiffContext(String name) {
1292
histPix = new Histogram("Diff Pixels [" + name + "]");
1293
nbPix = new StatInteger("NbPixels [" + name + "]");
1294
}
1295
1296
void reset() {
1297
histPix.reset();
1298
nbPix.reset();
1299
}
1300
1301
void dump() {
1302
if (isDiff()) {
1303
System.out.println("Differences [" + histPix.name + "]:\n"
1304
+ ((nbPix.count != 0) ? (nbPix.toString() + "\n") : "")
1305
+ histPix.toString()
1306
);
1307
} else {
1308
System.out.println("No difference for [" + histPix.name + "].");
1309
}
1310
}
1311
1312
void add(int val) {
1313
histPix.add(val);
1314
}
1315
1316
void add(DiffContext ctx) {
1317
histPix.add(ctx.histPix);
1318
if (ctx.nbPix.count != 0L) {
1319
nbPix.add(ctx.nbPix);
1320
}
1321
}
1322
1323
void addNbPix(long val) {
1324
if (val != 0L) {
1325
nbPix.add(val);
1326
}
1327
}
1328
1329
void set(DiffContext ctx) {
1330
reset();
1331
add(ctx);
1332
}
1333
1334
boolean isWorse(DiffContext ctx, boolean useTh) {
1335
return histPix.isWorse(ctx.histPix, useTh);
1336
}
1337
1338
boolean isDiff() {
1339
return histPix.sum != 0l;
1340
}
1341
}
1342
1343
1344
static double linelen(final double x0, final double y0,
1345
final double x1, final double y1)
1346
{
1347
final double dx = x1 - x0;
1348
final double dy = y1 - y0;
1349
return Math.sqrt(dx * dx + dy * dy);
1350
}
1351
1352
static double quadlen(final double x0, final double y0,
1353
final double x1, final double y1,
1354
final double x2, final double y2)
1355
{
1356
return (linelen(x0, y0, x1, y1)
1357
+ linelen(x1, y1, x2, y2)
1358
+ linelen(x0, y0, x2, y2)) / 2.0d;
1359
}
1360
1361
static double curvelen(final double x0, final double y0,
1362
final double x1, final double y1,
1363
final double x2, final double y2,
1364
final double x3, final double y3)
1365
{
1366
return (linelen(x0, y0, x1, y1)
1367
+ linelen(x1, y1, x2, y2)
1368
+ linelen(x2, y2, x3, y3)
1369
+ linelen(x0, y0, x3, y3)) / 2.0d;
1370
}
1371
}
1372
1373