Skip to content

Commit

Permalink
feat: try to triangulate holes correctly #25
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoiver committed Jan 22, 2025
1 parent b7ba08b commit a99d8ae
Show file tree
Hide file tree
Showing 45 changed files with 588 additions and 116 deletions.
44 changes: 43 additions & 1 deletion __tests__/ssr/path.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import xmlserializer from 'xmlserializer';
// import getPixels from 'get-pixels';
import { getCanvas } from '../utils';
import '../useSnapshotMatchers';
import { Canvas, ImageExporter, Path } from '../../packages/core/src';
import {
Canvas,
ImageExporter,
Path,
TesselationMethod,
} from '../../packages/core/src';

const dir = `${__dirname}/snapshots`;
let $canvas: HTMLCanvasElement;
Expand Down Expand Up @@ -60,4 +65,41 @@ describe('Path', () => {
'path-d-changed',
);
});

it('should render holes correctly.', async () => {
const path = new Path({
d: 'M 0 0 L 100 0 L 100 100 L 0 100 Z M 50 50 L 50 75 L 75 75 L 75 50 Z M 25 25 L 25 50 L 50 50 Z',
fill: '#F67676',
});
canvas.appendChild(path);
canvas.render();

expect($canvas.getContext('webgl1')).toMatchWebGLSnapshot(
dir,
'path-holes',
);
expect(exporter.toSVG({ grid: true })).toMatchSVGSnapshot(
dir,
'path-holes',
);
});

it('should use libtess TesselationMethod correctly.', async () => {
const path = new Path({
d: 'M 0 0 L 100 0 L 100 100 L 0 100 Z M 50 50 L 50 75 L 75 75 L 75 50 Z M 25 25 L 25 50 L 50 50 Z',
fill: '#F67676',
tessellationMethod: TesselationMethod.LIBTESS,
});
canvas.appendChild(path);
canvas.render();

expect($canvas.getContext('webgl1')).toMatchWebGLSnapshot(
dir,
'path-holes-libtess',
);
expect(exporter.toSVG({ grid: true })).toMatchSVGSnapshot(
dir,
'path-holes-libtess',
);
});
});
Binary file modified __tests__/ssr/snapshots/circle-stroke-dasharray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __tests__/ssr/snapshots/ellipse-stroke-dasharray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added __tests__/ssr/snapshots/path-holes-libtess.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions __tests__/ssr/snapshots/path-holes-libtess.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added __tests__/ssr/snapshots/path-holes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions __tests__/ssr/snapshots/path-holes.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __tests__/ssr/snapshots/polyline-points-changed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __tests__/ssr/snapshots/polyline-stroke-dasharray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __tests__/ssr/snapshots/rough-circle-solid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions __tests__/unit/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
parsePath,
LineCurve,
containsEmoji,
isClockWise,
} from '../../packages/core/src/utils';

describe('Utils', () => {
Expand Down Expand Up @@ -142,4 +143,25 @@ describe('Utils', () => {
expect(containsEmoji('Hello, 😊!')).toBe(true);
});
});

describe('isClockWise', () => {
it('should check if a value isClockWise correctly.', () => {
expect(
isClockWise([
[0, 0],
[100, 0],
[100, 100],
[0, 100],
]),
).toBe(true);
expect(
isClockWise([
[0, 0],
[0, 100],
[100, 100],
[100, 0],
]),
).toBe(false);
});
});
});
44 changes: 38 additions & 6 deletions packages/core/src/drawcalls/Mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
TransparentBlack,
Texture,
StencilOp,
PrimitiveTopology,
} from '@antv/g-device-api';
import {
Path,
Expand All @@ -24,8 +25,8 @@ import {
} from '../shapes';
import { Drawcall, ZINDEX_FACTOR } from './Drawcall';
import { vert, frag, Location } from '../shaders/mesh';
import { isString, paddingMat3, triangulate } from '../utils';
import earcut, { flatten } from 'earcut';
import { isClockWise, isString, paddingMat3, triangulate } from '../utils';
import earcut from 'earcut';

const strokeAlignmentMap = {
center: 0,
Expand All @@ -39,6 +40,8 @@ export class Mesh extends Drawcall {

points: number[] = [];

instanced = false;

static useDash(shape: Path) {
const { strokeDasharray } = shape;
return (
Expand Down Expand Up @@ -75,8 +78,6 @@ export class Mesh extends Drawcall {
}

createGeometry(): void {
// Don't support instanced rendering for now.
this.instanced = false;
const instance = this.shapes[0];

let rawPoints: [number, number][][];
Expand All @@ -102,8 +103,38 @@ export class Mesh extends Drawcall {
}

if (tessellationMethod === TesselationMethod.EARCUT) {
const { vertices, holes, dimensions } = flatten(rawPoints);
const indices = earcut(vertices, holes, dimensions);
let holes = [];
let contours = [];
const indices = [];
let indexOffset = 0;

let firstClockWise = isClockWise(rawPoints[0]);

rawPoints.forEach((points) => {
const isHole = isClockWise(points) !== firstClockWise;
if (isHole) {
holes.push(contours.length);
} else {
firstClockWise = isClockWise(points);

if (holes.length > 0) {
indices.push(
...earcut(contours.flat(), holes).map((i) => i + indexOffset),
);
indexOffset += contours.length;
holes = [];
contours = [];
}
}
contours.push(...points);
});

if (contours.length) {
indices.push(
...earcut(contours.flat(), holes).map((i) => i + indexOffset),
);
}

this.indexBufferData = new Uint32Array(indices);
this.points = points;
// const err = deviation(vertices, holes, dimensions, indices);
Expand Down Expand Up @@ -168,6 +199,7 @@ export class Mesh extends Drawcall {
program: this.program,
colorAttachmentFormats: [Format.U8_RGBA_RT],
depthStencilAttachmentFormat: Format.D24_S8,
topology: PrimitiveTopology.TRIANGLES,
megaStateDescriptor: {
attachmentsState: [
{
Expand Down
16 changes: 15 additions & 1 deletion packages/core/src/drawcalls/SDFText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
BitmapFont,
GlyphPositions,
containsEmoji,
// yOffsetFromTextBaseline,
} from '../utils';

export class SDFText extends Drawcall {
Expand Down Expand Up @@ -437,14 +438,25 @@ export class SDFText extends Drawcall {
bitmapFont: BitmapFont;
fontScale: number;
}) {
const { textAlign = 'start', x = 0, y = 0, bitmapFontKerning } = object;
const {
textAlign,
// textBaseline,
x = 0,
y = 0,
bitmapFontKerning,
// metrics,
} = object;

const charUVOffsetBuffer: number[] = [];
const charPositionsBuffer: number[] = [];
const indexBuffer: number[] = [];

let i = indicesOffset;

// const dy =
// yOffsetFromTextBaseline(textBaseline, metrics.fontMetrics) +
// metrics.fontMetrics.fontBoundingBoxAscent;

const positionedGlyphs = this.#glyphManager.layout(
lines,
fontStack,
Expand All @@ -454,6 +466,8 @@ export class SDFText extends Drawcall {
bitmapFont,
fontScale,
bitmapFontKerning,
0,
0,
);

let positions: GlyphPositions;
Expand Down
32 changes: 22 additions & 10 deletions packages/core/src/drawcalls/SmoothPolyline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class SmoothPolyline extends Drawcall {
);
}

validate(shape: Shape) {
validate(_: Shape) {
return false;
}

Expand All @@ -67,6 +67,7 @@ export class SmoothPolyline extends Drawcall {
#segmentsBuffer: Buffer;
#uniformBuffer: Buffer;

instanced = false;
pointsBuffer: number[] = [];
travelBuffer: number[] = [];

Expand All @@ -90,9 +91,6 @@ export class SmoothPolyline extends Drawcall {
}

createGeometry(): void {
// Don't support instanced rendering for now.
this.instanced = false;

const indices: number[] = [];
const pointsBuffer: number[] = [];
const travelBuffer: number[] = [];
Expand Down Expand Up @@ -604,22 +602,36 @@ export function updateBuffer(object: Shape, useRoughStroke = true) {

const pointsBufferTotal: number[] = [];
const travelBufferTotal: number[] = [];
// let instancedCount = 0;

subPaths.forEach((points) => {
const pointsBuffer: number[] = [];
const travelBuffer: number[] = [];
let j = (Math.round(0 / stridePoints) + 2) * strideFloats;
let dist = 0;

// Account for Z command in path
let zCommand = false;
if (
points.length >= 6 &&
points[0] === points[points.length - 2] &&
points[1] === points[points.length - 1]
) {
const dir = [points[2] - points[0], points[3] - points[1]];
points.push(points[0] + epsilon * dir[0], points[1] + epsilon * dir[1]);
zCommand = true;
}

for (let i = 0; i < points.length; i += stridePoints) {
// calc travel
if (i > 1) {
dist += Math.sqrt(
Math.pow(points[i] - points[i - stridePoints], 2) +
Math.pow(points[i + 1] - points[i + 1 - stridePoints], 2),
);
if (!(zCommand && i >= points.length - stridePoints)) {
dist += Math.sqrt(
Math.pow(points[i] - points[i - stridePoints], 2) +
Math.pow(points[i + 1] - points[i + 1 - stridePoints], 2),
);
travelBuffer.push(dist);
}
}
travelBuffer.push(dist);

pointsBuffer[j++] = points[i];
pointsBuffer[j++] = points[i + 1];
Expand Down
8 changes: 3 additions & 5 deletions packages/core/src/shapes/Circle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ export function CircleWrapper<TBase extends GConstructor>(Base: TBase) {
#cy: number;
#r: number;

onGeometryChanged?: () => void;

static getGeometryBounds(
attributes: Partial<Pick<CircleAttributes, 'cx' | 'cy' | 'r'>>,
) {
Expand Down Expand Up @@ -69,7 +67,7 @@ export function CircleWrapper<TBase extends GConstructor>(Base: TBase) {
this.geometryBoundsDirtyFlag = true;
this.renderBoundsDirtyFlag = true;
this.boundsDirtyFlag = true;
this.onGeometryChanged?.();
this.geometryDirtyFlag = true;
}
}

Expand All @@ -84,7 +82,7 @@ export function CircleWrapper<TBase extends GConstructor>(Base: TBase) {
this.geometryBoundsDirtyFlag = true;
this.renderBoundsDirtyFlag = true;
this.boundsDirtyFlag = true;
this.onGeometryChanged?.();
this.geometryDirtyFlag = true;
}
}

Expand All @@ -99,7 +97,7 @@ export function CircleWrapper<TBase extends GConstructor>(Base: TBase) {
this.geometryBoundsDirtyFlag = true;
this.renderBoundsDirtyFlag = true;
this.boundsDirtyFlag = true;
this.onGeometryChanged?.();
this.geometryDirtyFlag = true;
}
}

Expand Down
Loading

0 comments on commit a99d8ae

Please sign in to comment.