Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"build": "node-gyp rebuild",
"build:prebuild": "node scripts/prebuild.js",
"test": "npm run test:unit && npm run test:window && npm run test:shell && npm run test:a11y && npm run test:identity && npm run test:pane && npm run test:gpu",
"test:unit": "node test/smoke.js && node test/text.js && node test/bezels.js && node test/primitives.js && node test/path-stream.js && node test/dashes.js",
"test:unit": "node test/smoke.js && node test/text.js && node test/bezels.js && node test/primitives.js && node test/clip-rule.js && node test/path-stream.js && node test/dashes.js",
"test:window": "node test/clips.js && node test/states.js && node test/dnd.js && node test/ime.js && node test/popup.js",
"test:shell": "node test/shell.js",
"test:a11y": "node test/uia.js",
Expand Down
10 changes: 8 additions & 2 deletions src/surface.cc
Original file line number Diff line number Diff line change
Expand Up @@ -901,15 +901,21 @@ Napi::Value CtxStroke(const Napi::CallbackInfo& info) {
return info.Env().Undefined();
}

// ctxClip(surface, evenOdd): the flag ctxFill takes, so canvas's
// clip(path, 'evenodd') — an SVG <clipPath> with clip-rule="evenodd" — cuts
// a ring rather than the square around it. Absent is nonzero, as before.
Napi::Value CtxClip(const Napi::CallbackInfo& info) {
Surface* s = Arg(info);
if (!s || !s->dc) return info.Env().Undefined();
ID2D1PathGeometry* geometry = BuildPath(s, false);
const bool evenOdd = info.Length() > 1 && info[1].ToBoolean().Value();
ID2D1PathGeometry* geometry = BuildPath(s, evenOdd);
if (!geometry) return info.Env().Undefined();

// An axis-aligned rectangle under an axis-aligned transform is the clip
// every paint pass sets, and PushAxisAlignedClip is far cheaper than a
// layer with a geometric mask. Anything else takes the layer.
// layer with a geometric mask. Anything else takes the layer. The rule
// does not enter into it: one rect is one figure wound once, which either
// fill mode cuts alike.
D2D1_RECT_F bounds = {};
bool rectangular = false;
if (s->path.size() == 1 && s->path[0].op == PathOp::Rect) {
Expand Down
75 changes: 75 additions & 0 deletions test/clip-rule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict';

// ctxClip(surface, evenOdd): the fill rule of a clip, as ctxFill takes it.
// canvas's clip(path, 'evenodd') is how ntk's SvgView cuts an element to a
// <clipPath> with clip-rule="evenodd" (sidorares/ntk#529), and react-x11's
// context had nothing to hand it to — CtxClip always built its geometry with
// the winding fill mode, so a ring clipped a fill to the whole square around
// it.
//
// Checked: a ring of two rects wound the same way clips evenodd to the ring,
// its hole left alone; without the flag, or with it false, the same path
// clips nonzero, hole and all; and one rect — which takes
// PushAxisAlignedClip rather than a layer — clips the same under either rule.

const assert = require('node:assert');
const win32 = require('../index.js');

win32.start(() => {});

const W = 60;

/** Fill the whole surface white through a clip to `path`, over black. */
function clipped(path, ...args) {
const s = win32.createSurface(W, W, 1);
win32.ctxSetFillColor(s, 0, 0, 0, 1);
win32.ctxFillRect(s, 0, 0, W, W);
win32.ctxSave(s);
win32.ctxBeginPath(s);
path(s);
win32.ctxClip(s, ...args);
win32.ctxSetFillColor(s, 1, 1, 1, 1);
win32.ctxFillRect(s, 0, 0, W, W);
win32.ctxRestore(s);
const pixels = win32.ctxGetImageData(s, 0, 0, W, W);
win32.releaseSurface(s);
return pixels;
}
const red = (pixels, x, y) => pixels[(y * W + x) * 4];

// a 40px square with a 20px hole, both wound the same way: nonzero fills the
// hole, evenodd leaves it
const ring = (s) => {
win32.ctxRect(s, 10, 10, 40, 40);
win32.ctxRect(s, 20, 20, 20, 20);
};
const square = (s) => win32.ctxRect(s, 10, 10, 40, 40);

{
const p = clipped(ring, true);
assert.equal(red(p, 15, 15), 255, 'evenodd: the ring is drawn');
assert.equal(red(p, 30, 30), 0, 'evenodd: the hole is filled — the clip was cut nonzero');
assert.equal(red(p, 5, 5), 0, 'evenodd: something outside the ring was drawn');
}

for (const [name, args] of [
['no flag', []],
['false', [false]],
]) {
const p = clipped(ring, ...args);
assert.equal(red(p, 15, 15), 255, `${name}: the ring is drawn`);
assert.equal(red(p, 30, 30), 255, `${name}: nonzero leaves the hole filled`);
assert.equal(red(p, 5, 5), 0, `${name}: something outside the ring was drawn`);
}

{
// one rect is one figure wound once: either rule cuts the same pixels
const a = clipped(square, true);
const b = clipped(square, false);
let differ = 0;
for (let i = 0; i < a.length; i += 4) if (a[i] !== b[i]) differ++;
assert.equal(differ, 0, `one rect clips differently under the two rules, ${differ} pixels apart`);
}

win32.stop();
console.log('clip-rule: ok');
Loading