From 01aa360790fed6e43b1cbec18f6bbefc5a4210a0 Mon Sep 17 00:00:00 2001 From: VANSH3104 Date: Fri, 6 Mar 2026 10:44:48 +0530 Subject: [PATCH] Add ShapePrimitive support for arcs and ellipses --- src/core/p5.Renderer2D.js | 60 ++------------- src/shape/custom_shapes.js | 151 +++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 52 deletions(-) diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index fbe5747449..206d187f5f 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -8,7 +8,7 @@ import { MediaElement } from '../dom/p5.MediaElement'; import { RGBHDR } from '../color/creating_reading'; import FilterRenderer2D from '../image/filterRenderer2D'; import { Matrix } from '../math/p5.Matrix'; -import { PrimitiveToPath2DConverter } from '../shape/custom_shapes'; +import { PrimitiveToPath2DConverter, ArcPrimitive, EllipsePrimitive } from '../shape/custom_shapes'; import { DefaultFill, textCoreConstants } from '../type/textCore'; @@ -661,13 +661,6 @@ class Renderer2D extends Renderer { * start <= stop < start + TWO_PI */ arc(x, y, w, h, start, stop, mode) { - const ctx = this.drawingContext; - const rx = w / 2.0; - const ry = h / 2.0; - const epsilon = 0.00001; // Smallest visible angle on displays up to 4K. - let arcToDraw = 0; - const curves = []; - const centerX = x + w / 2, centerY = y + h / 2, radiusX = w / 2, @@ -681,48 +674,16 @@ class Renderer2D extends Renderer { this.clipPath.addPath(tempPath, relativeTransform); return this; } - // Determines whether to add a line to the center, which should be done - // when the mode is PIE or default; as well as when the start and end - // angles do not form a full circle. - const createPieSlice = ! ( - mode === constants.CHORD || - mode === constants.OPEN || - (stop - start) % constants.TWO_PI === 0 - ); - - // Fill curves - if (this.states.fillColor) { - ctx.beginPath(); - ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop); - if (createPieSlice) ctx.lineTo(centerX, centerY); - ctx.closePath(); - ctx.fill(); - } - // Stroke curves - if (this.states.strokeColor) { - ctx.beginPath(); - ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop); - - if (mode === constants.PIE && createPieSlice) { - // In PIE mode, stroke is added to the center and back to path, - // unless the pie forms a complete ellipse (see: createPieSlice) - ctx.lineTo(centerX, centerY); - } - - if (mode === constants.PIE || mode === constants.CHORD) { - // Stroke connects back to path begin for both PIE and CHORD - ctx.closePath(); - } - ctx.stroke(); - } + const primitive = new ArcPrimitive(x, y, w, h, start, stop, mode); + const shape = { accept(visitor) { primitive.accept(visitor); } }; + this.drawShape(shape); return this; } ellipse(args) { - const ctx = this.drawingContext; const doFill = !!this.states.fillColor, doStroke = this.states.strokeColor; const x = parseFloat(args[0]), @@ -751,15 +712,10 @@ class Renderer2D extends Renderer { this.clipPath.addPath(tempPath, relativeTransform); return this; } - ctx.beginPath(); - ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); - ctx.closePath(); - if (doFill) { - ctx.fill(); - } - if (doStroke) { - ctx.stroke(); - } + + const primitive = new EllipsePrimitive(x, y, w, h); + const shape = { accept(visitor) { primitive.accept(visitor); } }; + this.drawShape(shape); return this; } diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index 3a09200f75..61ffe2ba4c 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -466,6 +466,77 @@ class Quad extends ShapePrimitive { } } +class ArcPrimitive extends ShapePrimitive { + #x; + #y; + #w; + #h; + #start; + #stop; + #mode; + // vertexCapacity 0 means this primitive should not accumulate normal path vertices + #vertexCapacity = 0; + + constructor(x, y, w, h, start, stop, mode) { + // ShapePrimitive requires at least one vertex; pass a placeholder + super(new Vertex({ position: new Vector(x + w / 2, y + h / 2) })); + this.#x = x; + this.#y = y; + this.#w = w; + this.#h = h; + this.#start = start; + this.#stop = stop; + this.#mode = mode; + } + + get x() { return this.#x; } + get y() { return this.#y; } + get w() { return this.#w; } + get h() { return this.#h; } + get start() { return this.#start; } + get stop() { return this.#stop; } + get mode() { return this.#mode; } + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitArcPrimitive(this); + } +} + +class EllipsePrimitive extends ShapePrimitive { + #x; + #y; + #w; + #h; + // vertexCapacity 0 means this primitive should not accumulate normal path vertices + #vertexCapacity = 0; + + constructor(x, y, w, h) { + + super(new Vertex({ position: new Vector(x + w / 2, y + h / 2) })); + this.#x = x; + this.#y = y; + this.#w = w; + this.#h = h; + } + + get x() { return this.#x; } + get y() { return this.#y; } + get w() { return this.#w; } + get h() { return this.#h; } + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitEllipsePrimitive(this); + } +} + // ---- TESSELLATION PRIMITIVES ---- class TriangleFan extends ShapePrimitive { @@ -1003,6 +1074,12 @@ class PrimitiveVisitor { visitArcSegment(arcSegment) { throw new Error('Method visitArcSegment() has not been implemented.'); } + visitArcPrimitive(arc) { + throw new Error('Method visitArcPrimitive() has not been implemented.'); + } + visitEllipsePrimitive(ellipse) { + throw new Error('Method visitEllipsePrimitive() has not been implemented.'); + } // isolated primitives visitPoint(point) { @@ -1151,6 +1228,34 @@ class PrimitiveToPath2DConverter extends PrimitiveVisitor { this.path.closePath(); } } + visitArcPrimitive(arc) { + const centerX = arc.x + arc.w / 2; + const centerY = arc.y + arc.h / 2; + const radiusX = arc.w / 2; + const radiusY = arc.h / 2; + + this.path.ellipse( + centerX, centerY, radiusX, radiusY, 0, arc.start, arc.stop + ); + + if (arc.mode === constants.OPEN) { + // OPEN: leave path open — arc stroke/fill is just the curve + } else if (arc.mode === constants.CHORD) { + + this.path.closePath(); + } else { + this.path.lineTo(centerX, centerY); + this.path.closePath(); + } + } + visitEllipsePrimitive(ellipse) { + const centerX = ellipse.x + ellipse.w / 2; + const centerY = ellipse.y + ellipse.h / 2; + const radiusX = ellipse.w / 2; + const radiusY = ellipse.h / 2; + + this.path.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); + } visitQuadStrip(quadStrip) { for (let i = 0; i < quadStrip.vertices.length - 3; i += 2) { const v0 = quadStrip.vertices[i]; @@ -1277,6 +1382,50 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { // WebGL itself interprets the vertices as a strip, no reformatting needed this.contours.push(quadStrip.vertices.slice()); } + visitArcPrimitive(arc) { + const centerX = arc.x + arc.w / 2; + const centerY = arc.y + arc.h / 2; + const radiusX = arc.w / 2; + const radiusY = arc.h / 2; + const numPoints = Math.max(3, this.curveDetail); + const verts = []; + + if (arc.mode === constants.PIE) { + verts.push(new Vertex({ position: new Vector(centerX, centerY) })); + } + + for (let i = 0; i <= numPoints; i++) { + const angle = arc.start + (arc.stop - arc.start) * (i / numPoints); + verts.push(new Vertex({ + position: new Vector( + centerX + radiusX * Math.cos(angle), + centerY + radiusY * Math.sin(angle) + ) + })); + } + + this.contours.push(verts); + } + visitEllipsePrimitive(ellipse) { + const centerX = ellipse.x + ellipse.w / 2; + const centerY = ellipse.y + ellipse.h / 2; + const radiusX = ellipse.w / 2; + const radiusY = ellipse.h / 2; + const numPoints = Math.max(3, this.curveDetail); + const verts = []; + + for (let i = 0; i <= numPoints; i++) { + const angle = (2 * Math.PI * i) / numPoints; + verts.push(new Vertex({ + position: new Vector( + centerX + radiusX * Math.cos(angle), + centerY + radiusY * Math.sin(angle) + ) + })); + } + + this.contours.push(verts); + } } class PointAtLengthGetter extends PrimitiveVisitor { @@ -2793,6 +2942,8 @@ export { Line, Triangle, Quad, + ArcPrimitive, + EllipsePrimitive, TriangleFan, TriangleStrip, QuadStrip,