[OpenJDK 2D-Dev] RFR JDK-8184429: Path clipper added in Marlin2D & MarlinFX 0.8.0
Laurent Bourgès
bourges.laurent at gmail.com
Tue Sep 5 20:41:12 UTC 2017
Hi Jim,
I made good progress on my PathClipFilter that works now perfectly with
many test maps for the NZ rule (EO has artefacts if I enable the filter in
that case).
Here is the updated code below to illustrate the approach:
- use a new IndexStack in (D)Helpers to store only corner indexes (0/1 for
Top/Bottom Left, 2/3 for Top/Bottom Right)
- when the segment is out, I now check (L/R case) if the segment ends have
different outcodes to insert needed corners that can be removed later if a
segment does the same in the reverse order (same repeated corner is
cancelled out): see IndexStack.push(int)
- PathClipFilter:
static final class PathClipFilter implements DPathConsumer2D {
private DPathConsumer2D out;
// Bounds of the drawing region, at pixel precision.
private final double[] clipRect;
private final double[] corners = new double[8];
private boolean init_corners = false;
private final IndexStack stack;
// the outcode of the starting point
private int sOutCode = 0;
// the current outcode of the current sub path
private int cOutCode = 0;
private boolean outside = false;
private double cx0, cy0;
PathClipFilter(final DRendererContext rdrCtx) {
this.clipRect = rdrCtx.clipRect;
this.stack = (rdrCtx.stats != null) ?
new IndexStack(rdrCtx,
rdrCtx.stats.stat_pcf_idxstack_indices,
rdrCtx.stats.hist_pcf_idxstack_indices,
rdrCtx.stats.stat_array_pcf_idxstack_indices)
: new IndexStack(rdrCtx);
}
PathClipFilter init(final DPathConsumer2D out) {
this.out = out;
// Adjust the clipping rectangle with the renderer offsets
final double rdrOffX = DRenderer.RDR_OFFSET_X;
final double rdrOffY = DRenderer.RDR_OFFSET_Y;
// add a small rounding error:
final double margin = 1e-3d;
final double[] _clipRect = this.clipRect;
_clipRect[0] -= margin - rdrOffY;
_clipRect[1] += margin + rdrOffY;
_clipRect[2] -= margin - rdrOffX;
_clipRect[3] += margin + rdrOffX;
init_corners = true;
return this; // fluent API
}
/**
* Disposes this instance:
* clean up before reusing this instance
*/
void dispose() {
stack.dispose();
}
@Override
public void pathDone() {
out.pathDone();
// TODO: fix possible leak if exception happened
// Dispose this instance:
dispose();
}
@Override
public void closePath() {
if (outside) {
this.outside = false;
if (sOutCode == 0) {
finish();
} else {
stack.reset();
}
}
out.closePath();
this.cOutCode = sOutCode;
}
private void finish() {
if (!stack.isEmpty()) {
if (init_corners) {
init_corners = false;
// Top Left (0):
corners[0] = clipRect[2];
corners[1] = clipRect[0];
// Bottom Left (1):
corners[2] = clipRect[2];
corners[3] = clipRect[1];
// Top right (2):
corners[4] = clipRect[3];
corners[5] = clipRect[0];
// Bottom Right (3):
corners[6] = clipRect[3];
corners[7] = clipRect[1];
}
stack.pullAll(corners, out);
}
out.lineTo(cx0, cy0);
}
@Override
public void moveTo(final double x0, final double y0) {
final int outcode = DHelpers.outcode(x0, y0, clipRect);
this.sOutCode = outcode;
this.cOutCode = outcode;
this.outside = false;
out.moveTo(x0, y0);
}
@Override
public void lineTo(final double xe, final double ye) {
final int outcode0 = this.cOutCode;
final int outcode1 = DHelpers.outcode(xe, ye, clipRect);
this.cOutCode = outcode1;
final int sideCode = (outcode0 & outcode1);
// basic rejection criteria:
if (sideCode != 0) {
// keep last point coordinate before entering the clip
again:
this.outside = true;
this.cx0 = xe;
this.cy0 = ye;
clip(sideCode, outcode0, outcode1);
return;
}
if (outside) {
this.outside = false;
finish();
}
// clipping disabled:
out.lineTo(xe, ye);
}
private void clip(final int sideCode,
final int outcode0,
final int outcode1)
{
// corner or cross-boundary on left or right side:
if ((outcode0 != outcode1)
&& ((sideCode & DHelpers.OUTCODE_MASK_T_B) != 0))
{
// combine outcodes:
final int mergeCode = (outcode0 | outcode1);
final int tbCode = mergeCode & DHelpers.OUTCODE_MASK_T_B;
final int lrCode = mergeCode & DHelpers.OUTCODE_MASK_L_R;
// add corners to outside stack:
final int off = (lrCode == DHelpers.OUTCODE_LEFT) ? 0 : 2;
switch (tbCode) {
case DHelpers.OUTCODE_TOP:
stack.push(off); // top
return;
case DHelpers.OUTCODE_BOTTOM:
stack.push(off + 1); // bottom
return;
default:
// both TOP / BOTTOM:
if ((outcode0 & DHelpers.OUTCODE_TOP) != 0) {
// top to bottom
stack.push(off); // top
stack.push(off + 1); // bottom
} else {
// bottom to top
stack.push(off + 1); // bottom
stack.push(off); // top
}
}
}
}
@Override
public void curveTo(final double x1, final double y1,
final double x2, final double y2,
final double xe, final double ye)
{
final int outcode0 = this.cOutCode;
final int outcode3 = DHelpers.outcode(xe, ye, clipRect);
this.cOutCode = outcode3;
int sideCode = outcode0 & outcode3;
if (sideCode != 0) {
sideCode &= DHelpers.outcode(x1, y1, clipRect);
sideCode &= DHelpers.outcode(x2, y2, clipRect);
// basic rejection criteria:
if (sideCode != 0) {
// keep last point coordinate before entering the clip
again:
this.outside = true;
this.cx0 = xe;
this.cy0 = ye;
clip(sideCode, outcode0, outcode3);
return;
}
}
if (outside) {
this.outside = false;
finish();
}
// clipping disabled:
out.curveTo(x1, y1, x2, y2, xe, ye);
}
@Override
public void quadTo(final double x1, final double y1,
final double xe, final double ye)
{
final int outcode0 = this.cOutCode;
final int outcode2 = DHelpers.outcode(xe, ye, clipRect);
this.cOutCode = outcode2;
int sideCode = outcode0 & outcode2;
if (outcode2 != 0) {
sideCode &= DHelpers.outcode(x1, y1, clipRect);
// basic rejection criteria:
if (sideCode != 0) {
// keep last point coordinate before entering the clip
again:
this.outside = true;
this.cx0 = xe;
this.cy0 = ye;
clip(sideCode, outcode0, outcode2);
return;
}
}
if (outside) {
this.outside = false;
finish();
}
// clipping disabled:
out.quadTo(x1, y1, xe, ye);
}
@Override
public long getNativeConsumer() {
throw new InternalError("Not using a native peer");
}
}
- DHelpers.IndexStack:
// a stack of integer indices
static final class IndexStack {
// integer capacity = edges count / 4 ~ 1024
private static final int INITIAL_COUNT = INITIAL_EDGES_COUNT >> 2;
private int end;
private int[] indices;
// indices ref (dirty)
private final IntArrayCache.Reference indices_ref;
// used marks (stats only)
private int indicesUseMark;
private final StatLong stat_idxstack_indices;
private final Histogram hist_idxstack_indices;
private final StatLong stat_array_idxstack_indices;
IndexStack(final DRendererContext rdrCtx) {
this(rdrCtx, null, null, null);
}
IndexStack(final DRendererContext rdrCtx,
final StatLong stat_idxstack_indices,
final Histogram hist_idxstack_indices,
final StatLong stat_array_idxstack_indices)
{
indices_ref = rdrCtx.newDirtyIntArrayRef(INITIAL_COUNT); // 4K
indices = indices_ref.initial;
end = 0;
if (DO_STATS) {
indicesUseMark = 0;
}
this.stat_idxstack_indices = stat_idxstack_indices;
this.hist_idxstack_indices = hist_idxstack_indices;
this.stat_array_idxstack_indices = stat_array_idxstack_indices;
}
/**
* Disposes this PolyStack:
* clean up before reusing this instance
*/
void dispose() {
end = 0;
if (DO_STATS) {
stat_idxstack_indices.add(indicesUseMark);
hist_idxstack_indices.add(indicesUseMark);
// reset marks
indicesUseMark = 0;
}
// Return arrays:
// values is kept dirty
indices = indices_ref.putArray(indices);
}
boolean isEmpty() {
return (end == 0);
}
void reset() {
end = 0;
}
void push(final int v) {
// remove redundant values (reverse order):
int[] _values = indices;
final int nc = end;
if (nc != 0) {
if (_values[nc - 1] == v) {
// remove both duplicated values:
end--;
return;
}
}
if (_values.length <= nc) {
if (DO_STATS) {
stat_array_idxstack_indices.add(nc + 1);
}
indices = _values = indices_ref.widenArray(_values, nc, nc
+ 1);
}
_values[end++] = v;
if (DO_STATS) {
// update used marks:
if (end > indicesUseMark) {
indicesUseMark = end;
}
}
}
void pullAll(final double[] points, final DPathConsumer2D io) {
final int nc = end;
if (nc == 0) {
return;
}
final int[] _values = indices;
for (int i = 0, j; i < nc; i++) {
j = _values[i] << 1;
io.lineTo(points[j], points[j + 1]);
}
end = 0;
}
}
Here is a screenshot illustrating the remaining paths in Renderer after
> clipping a 4000x4000 spiral converted as stroked shape:
> http://cr.openjdk.java.net/~lbourges/png/SpiralTest-dash-false.ser.png
>
Now all useless rounds are totally discarded from the path sent to the
Renderer (removing lots of edges on the left/right sides)
> clip off: ~ 145ms
> clip on: ~ 106ms
>
clip on: ~ 68ms for this huge filled spiral ~ 50% faster
Could you answer my previous email on EO questions ?
How to deal with self intersections or is it possible to skip left segments
in the EO case or not ?
(I am a bit lost)
I need a simple path to test clipping with the EO rule (redudant segments);
any idea ?
Cheers,
Laurent
More information about the openjfx-dev
mailing list