From 56de4cde6b646a42a4fffff0b1408777a7fea757 Mon Sep 17 00:00:00 2001 From: rounittxx Date: Mon, 1 Dec 2025 18:03:39 +0530 Subject: [PATCH] fixed issue 8277 --- package-lock.json | 12 + src/core/p5.Renderer2D.js | 75 ++++--- src/shape/custom_shapes.js | 208 +++++++++++++++++- src/webgl/3d_primitives.js | 140 ++---------- src/webgl/ShapeBuilder.js | 6 +- src/webgl/p5.RendererGL.js | 4 +- .../Shape Modes/Shape arc/Mode CENTER/000.png | Bin 1195 -> 2245 bytes .../Shape Modes/Shape arc/Mode CORNER/000.png | Bin 1195 -> 2245 bytes .../Shape arc/Mode CORNERS/000.png | Bin 1195 -> 2245 bytes .../Shape Modes/Shape arc/Mode RADIUS/000.png | Bin 1195 -> 2245 bytes 10 files changed, 287 insertions(+), 158 deletions(-) diff --git a/package-lock.json b/package-lock.json index 23fe1225bb..320846519d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,6 +102,7 @@ "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -2936,6 +2937,7 @@ "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3049,6 +3051,7 @@ "integrity": "sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -3344,6 +3347,7 @@ "integrity": "sha512-OWVvEJThRgxlNMYNVLEK/9qVkpRcLvyuKLngIV3Hob01P56NjPHprVBYn+rx4xAJudbM9yrCrywPIEuA3Xyo8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.5.2", @@ -3779,6 +3783,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4276,6 +4281,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -5800,6 +5806,7 @@ "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -11659,6 +11666,7 @@ "integrity": "sha512-+4C/cgJ9w6sudisA0nZz0+O7lTP9a3CzNLsoDwaRumM8QHwghUsu6tqHXiTmNUp/rqNiM14++7dkzHDyCRs0Jg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.6" }, @@ -12761,6 +12769,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13237,6 +13246,7 @@ "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -13333,6 +13343,7 @@ "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "2.1.8", "@vitest/mocker": "2.1.8", @@ -13482,6 +13493,7 @@ "integrity": "sha512-zpGCn+pb63w9ZltCzIeGfEUSLLFwEsr0N4R25BdDFlBPQ5467VugPdSw/hWGTwgx7BzeKSdUgbKHqZMxb77nbQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "^20.11.30", "@types/sinonjs__fake-timers": "^8.1.5", diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index b9a7e12d00..d128d17cc4 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -8,7 +8,8 @@ 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, Vertex, Ellipse, Arc } from '../shape/custom_shapes'; +import { Vector } from '../math/p5.Vector'; const styleEmpty = 'rgba(0,0,0,0)'; @@ -651,7 +652,7 @@ class Renderer2D extends Renderer { * start <= stop < start + TWO_PI */ arc(x, y, w, h, start, stop, mode) { - const ctx = this.clipPa || this.drawingContext; + const ctx = this.clipPath || this.drawingContext; const rx = w / 2.0; const ry = h / 2.0; const epsilon = 0.00001; // Smallest visible angle on displays up to 4K. @@ -666,38 +667,33 @@ class Renderer2D extends Renderer { // 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 = ! ( + const createPieSlice = !( mode === constants.CHORD || mode === constants.OPEN || (stop - start) % constants.TWO_PI === 0 ); - // Fill curves - if (this.states.fillColor) { - if (!this._clipping) ctx.beginPath(); - ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop); - if (createPieSlice) ctx.lineTo(centerX, centerY); - ctx.closePath(); - if (!this._clipping) ctx.fill(); - } - - // Stroke curves - if (this.states.strokeColor) { - if (!this._clipping) 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); + // Create Arc primitive and use visitor pattern + const vertex = new Vertex({ position: new Vector(centerX, centerY) }); + const arcPrimitive = new Arc(x, y, w, h, start, stop, mode, vertex); + + const visitor = new PrimitiveToPath2DConverter({ + strokeWeight: this.states.strokeWeight + }); + arcPrimitive.accept(visitor); + + if (this._clipping) { + this.clipPath.addPath(visitor.path); + } else { + // Fill curves + if (this.states.fillColor) { + if (!this._clipping) ctx.fill(visitor.path); } - if (mode === constants.PIE || mode === constants.CHORD) { - // Stroke connects back to path begin for both PIE and CHORD - ctx.closePath(); + // Stroke curves + if (this.states.strokeColor) { + if (!this._clipping) ctx.stroke(visitor.path); } - - if (!this._clipping) ctx.stroke(); } return this; @@ -725,16 +721,25 @@ class Renderer2D extends Renderer { centerY = y + h / 2, radiusX = w / 2, radiusY = h / 2; - if (!this._clipping) ctx.beginPath(); - ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); - ctx.closePath(); - - if (!this._clipping && doFill) { - ctx.fill(); - } - if (!this._clipping && doStroke) { - ctx.stroke(); + // Create Ellipse primitive and use visitor pattern + const vertex = new Vertex({ position: new Vector(centerX, centerY) }); + const ellipsePrimitive = new Ellipse(x, y, w, h, vertex); + + const visitor = new PrimitiveToPath2DConverter({ + strokeWeight: this.states.strokeWeight + }); + ellipsePrimitive.accept(visitor); + + if (this._clipping) { + this.clipPath.addPath(visitor.path); + } else { + if (doFill) { + ctx.fill(visitor.path); + } + if (doStroke) { + ctx.stroke(visitor.path); + } } } diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index b65a363a76..9e4c34c745 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -466,6 +466,74 @@ class Quad extends ShapePrimitive { } } +class Ellipse extends ShapePrimitive { + #vertexCapacity = 1; + #x; + #y; + #w; + #h; + + constructor(x, y, w, h, ...vertices) { + super(...vertices); + this.#x = x; + this.#y = y; + this.#w = w; + this.#h = h; + } + + get vertexCapacity() { + return this.#vertexCapacity; + } + + get x() { return this.#x; } + get y() { return this.#y; } + get w() { return this.#w; } + get h() { return this.#h; } + + accept(visitor) { + visitor.visitEllipse(this); + } +} + +class Arc extends ShapePrimitive { + #vertexCapacity = 1; + #x; + #y; + #w; + #h; + #start; + #stop; + #mode; + + constructor(x, y, w, h, start, stop, mode, ...vertices) { + super(...vertices); + this.#x = x; + this.#y = y; + this.#w = w; + this.#h = h; + this.#start = start; + this.#stop = stop; + this.#mode = mode; + } + + get vertexCapacity() { + return this.#vertexCapacity; + } + + 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; } + + accept(visitor) { + visitor.visitArc(this); + } +} + + // ---- TESSELLATION PRIMITIVES ---- class TriangleFan extends ShapePrimitive { @@ -1017,6 +1085,12 @@ class PrimitiveVisitor { visitQuad(quad) { throw new Error('Method visitQuad() has not been implemented.'); } + visitEllipse(ellipse) { + throw new Error('Method visitEllipse() has not been implemented.'); + } + visitArc(arc) { + throw new Error('Method visitArc() has not been implemented.'); + } // tessellation primitives visitTriangleFan(triangleFan) { @@ -1129,6 +1203,27 @@ class PrimitiveToPath2DConverter extends PrimitiveVisitor { this.path.lineTo(v3.position.x, v3.position.y); this.path.closePath(); } + visitEllipse(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); + } + visitArc(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); + // Handle PIE and CHORD modes + if (arc.mode === constants.PIE || (arc.stop - arc.start) % constants.TWO_PI !== 0) { + if (!arc.mode || arc.mode === constants.PIE) { + this.path.lineTo(centerX, centerY); + } + this.path.closePath(); + } + } visitTriangleFan(triangleFan) { const [v0, ...rest] = triangleFan.vertices; for (let i = 0; i < rest.length - 1; i++) { @@ -1171,9 +1266,11 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { contours = []; curveDetail; - constructor({ curveDetail = 1 } = {}) { + constructor({ curveDetail = 1, fillColor, strokeColor } = {}) { super(); this.curveDetail = curveDetail; + this.fillColor = fillColor || new Color([0, 0, 0, 0]); + this.strokeColor = strokeColor || new Color([0, 0, 0, 0]); } lastContour() { @@ -1271,6 +1368,106 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { // WebGL itself interprets the vertices as a strip, no reformatting needed this.contours.push(quadStrip.vertices.slice()); } + visitEllipse(ellipse) { + const contour = []; + this.contours.push(contour); + const detail = Math.max( + 6, + Math.ceil(Math.max(ellipse.w, ellipse.h) * 2 * this.curveDetail) + ); + const centerX = ellipse.x + ellipse.w / 2; + const centerY = ellipse.y + ellipse.h / 2; + const radiusX = ellipse.w / 2; + const radiusY = ellipse.h / 2; + + for (let i = 0; i < detail; i++) { + const t = (i / detail) * 2 * Math.PI; + const x = centerX + radiusX * Math.cos(t); + const y = centerY + radiusY * Math.sin(t); + const u = (x - ellipse.x) / ellipse.w; + const v = (y - ellipse.y) / ellipse.h; + const vertex = new Vertex({ + position: new Vector(x, y, 0), + textureCoordinates: new Vector(u, v), + fill: this.fillColor, + stroke: this.strokeColor + }); + contour.push(vertex); + } + // Close the loop + contour.push(contour[0]); + } + visitArc(arc) { + const contour = []; + this.contours.push(contour); + // Calculate detail based on arc length and size + const arcLength = Math.abs(arc.stop - arc.start) * Math.max(arc.w, arc.h) / 2; + const detail = Math.max( + 4, + Math.ceil(arcLength * this.curveDetail) + ); + + const centerX = arc.x + arc.w / 2; + const centerY = arc.y + arc.h / 2; + const radiusX = arc.w / 2; + const radiusY = arc.h / 2; + + // Add center point for PIE mode + if (arc.mode === constants.PIE) { + const u = (centerX - arc.x) / arc.w; + const v = (centerY - arc.y) / arc.h; + const centerVertex = new Vertex({ + position: new Vector(centerX, centerY, 0), + fill: this.fillColor, + stroke: this.strokeColor, + textureCoordinates: new Vector(u, v) + }); + contour.push(centerVertex); + } + + for (let i = 0; i <= detail; i++) { + const t = arc.start + (i / detail) * (arc.stop - arc.start); + const x = centerX + radiusX * Math.cos(t); + const y = centerY + radiusY * Math.sin(t); + const u = (x - arc.x) / arc.w; + const v = (y - arc.y) / arc.h; + const vertex = new Vertex({ + position: new Vector(x, y, 0), + textureCoordinates: new Vector(u, v), + fill: this.fillColor, + stroke: this.strokeColor + }); + contour.push(vertex); + } + + // Close the loop for PIE and CHORD + if (arc.mode === constants.PIE || arc.mode === constants.CHORD) { + if (arc.mode === constants.PIE) { + const u = (centerX - arc.x) / arc.w; + const v = (centerY - arc.y) / arc.h; + const centerVertex = new Vertex({ + position: new Vector(centerX, centerY, 0), + textureCoordinates: new Vector(u, v), + fill: this.fillColor, + stroke: this.strokeColor + }); + contour.push(centerVertex); + } else if (arc.mode === constants.CHORD) { + const t = arc.start; + const x = centerX + radiusX * Math.cos(t); + const y = centerY + radiusY * Math.sin(t); + const u = (x - arc.x) / arc.w; + const v = (y - arc.y) / arc.h; + const vertex = new Vertex({ + position: new Vector(x, y, 0), + textureCoordinates: new Vector(u, v), + fill: this.fillColor, + stroke: this.strokeColor + }); + contour.push(vertex); + } + } + } } class PointAtLengthGetter extends PrimitiveVisitor { @@ -2856,10 +3053,9 @@ function customShapes(p5, fn) { export default customShapes; export { - Shape, - Contour, - ShapePrimitive, Vertex, + ShapePrimitive, + Contour, Anchor, Segment, LineSegment, @@ -2869,9 +3065,13 @@ export { Line, Triangle, Quad, + Ellipse, + Arc, TriangleFan, TriangleStrip, QuadStrip, + PrimitiveShapeCreators, + Shape, PrimitiveVisitor, PrimitiveToPath2DConverter, PrimitiveToVerticesConverter, diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index a671550f13..ba692c3831 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -11,6 +11,7 @@ import { RendererGL } from './p5.RendererGL'; import { Vector } from '../math/p5.Vector'; import { Geometry } from './p5.Geometry'; import { Matrix } from '../math/p5.Matrix'; +import { Vertex, Ellipse, Arc } from '../shape/custom_shapes'; function primitives3D(p5, fn){ /** @@ -1735,134 +1736,39 @@ function primitives3D(p5, fn){ }; RendererGL.prototype.ellipse = function(args) { - this.arc( - args[0], - args[1], - args[2], - args[3], - 0, - constants.TWO_PI, - constants.OPEN, - args[4] - ); + const x = args[0]; + const y = args[1]; + const w = args[2]; + const h = args[3]; + const detail = args[4] || 25; // detail is not used in ShapeEllipse directly but could be passed via curveDetail if needed, or handled in visitor + + // Create Ellipse primitive + const centerX = x + w / 2; + const centerY = y + h / 2; + const vertex = new Vertex({ position: new Vector(centerX, centerY) }); + const ellipsePrimitive = new Ellipse(x, y, w, h, vertex); + + this.drawShape(ellipsePrimitive); + return this; }; RendererGL.prototype.arc = function(...args) { const x = args[0]; const y = args[1]; - const width = args[2]; - const height = args[3]; + const w = args[2]; + const h = args[3]; const start = args[4]; const stop = args[5]; const mode = args[6]; const detail = args[7] || 25; - let shape; - let gid; - - // check if it is an ellipse or an arc - if (Math.abs(stop - start) >= constants.TWO_PI) { - shape = 'ellipse'; - gid = `${shape}|${detail}|`; - } else { - shape = 'arc'; - gid = `${shape}|${start}|${stop}|${mode}|${detail}|`; - } - - if (!this.geometryInHash(gid)) { - const _arc = function() { - - // if the start and stop angles are not the same, push vertices to the array - if (start.toFixed(10) !== stop.toFixed(10)) { - // if the mode specified is PIE or null, push the mid point of the arc in vertices - if (mode === constants.PIE || typeof mode === 'undefined') { - this.vertices.push(new Vector(0.5, 0.5, 0)); - this.uvs.push([0.5, 0.5]); - } - - // vertices for the perimeter of the circle - for (let i = 0; i <= detail; i++) { - const u = i / detail; - const theta = (stop - start) * u + start; - - const _x = 0.5 + Math.cos(theta) / 2; - const _y = 0.5 + Math.sin(theta) / 2; - - this.vertices.push(new Vector(_x, _y, 0)); - this.uvs.push([_x, _y]); - - if (i < detail - 1) { - this.faces.push([0, i + 1, i + 2]); - this.edges.push([i + 1, i + 2]); - } - } - - // check the mode specified in order to push vertices and faces, different for each mode - switch (mode) { - case constants.PIE: - this.faces.push([ - 0, - this.vertices.length - 2, - this.vertices.length - 1 - ]); - this.edges.push([0, 1]); - this.edges.push([ - this.vertices.length - 2, - this.vertices.length - 1 - ]); - this.edges.push([0, this.vertices.length - 1]); - break; - - case constants.CHORD: - this.edges.push([0, 1]); - this.edges.push([0, this.vertices.length - 1]); - break; - - case constants.OPEN: - this.edges.push([0, 1]); - break; - - default: - this.faces.push([ - 0, - this.vertices.length - 2, - this.vertices.length - 1 - ]); - this.edges.push([ - this.vertices.length - 2, - this.vertices.length - 1 - ]); - } - } - }; - - const arcGeom = new Geometry(detail, 1, _arc, this); - arcGeom.computeNormals(); - - if (detail <= 50) { - arcGeom._edgesToVertices(arcGeom); - } else if (this.states.strokeColor) { - console.log( - `Cannot apply a stroke to an ${shape} with more than 50 detail` - ); - } - - arcGeom.gid = gid; - this.geometryBufferCache.ensureCached(arcGeom); - } - - const uModelMatrix = this.states.uModelMatrix; - this.states.setValue('uModelMatrix', this.states.uModelMatrix.clone()); - - try { - this.states.uModelMatrix.translate([x, y, 0]); - this.states.uModelMatrix.scale(width, height, 1); - - this.model(this.geometryBufferCache.getGeometryByID(gid)); - } finally { - this.states.setValue('uModelMatrix', uModelMatrix); - } + // Create Arc primitive + const centerX = x + w / 2; + const centerY = y + h / 2; + const vertex = new Vertex({ position: new Vector(centerX, centerY) }); + const arcPrimitive = new Arc(x, y, w, h, start, stop, mode, vertex); + this.drawShape(arcPrimitive); return this; }; diff --git a/src/webgl/ShapeBuilder.js b/src/webgl/ShapeBuilder.js index 4b0099db50..e8b9744700 100644 --- a/src/webgl/ShapeBuilder.js +++ b/src/webgl/ShapeBuilder.js @@ -52,7 +52,11 @@ export class ShapeBuilder { this.geometry.reset(); this.contourIndices = []; // TODO: handle just some contours having non-PATH mode - this.shapeMode = shape.contours[0].kind; + if (shape.contours && shape.contours.length > 0) { + this.shapeMode = shape.contours[0].kind; + } else { + this.shapeMode = constants.PATH; + } const shouldProcessEdges = !!this.renderer.states.strokeColor; const userVertexPropertyHelpers = {}; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 6008c0e3c2..354e53b3c4 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -562,7 +562,9 @@ class RendererGL extends Renderer { drawShape(shape) { const visitor = new PrimitiveToVerticesConverter({ - curveDetail: this.states.curveDetail + curveDetail: this.states.curveDetail, + fillColor: this.states.fillColor, + strokeColor: this.states.strokeColor }); shape.accept(visitor); this.shapeBuilder.constructFromContours(shape, visitor.contours); diff --git a/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CENTER/000.png b/test/unit/visual/screenshots/Shape Modes/Shape arc/Mode CENTER/000.png index d624061419aaaacd85af49537aadf960584abb21..f5ddd5ef3509c104332090ceca244fc674c96147 100644 GIT binary patch literal 2245 zcma);X*8Q@AI6hrgfHc5W@4BA*rW4*4zDH>f z2n2%Lov?BNb~9kbOF)6|)*6Tn0!hT$S>aq`!2BZT?6P)5N+0)3VXk$p_Vb~QVn^kQ zHWLV<_iD-Y(u+uEAlD2*lc@-nBn;W6%iOj|dDofKw%MCCOe?8FD zc(wRafyU`pdm|W@uPzWWxH=S`mNJyiJ}l@wH5Sk^b4yFXKr0+ul+%cr@My3j6TX%u zJLtB?5!gDEglGnWJjD&e&I?jJkR{YrjgUla)T+@btNqKcd-lJ~3i^g=Y$NFf`znhG zIsGpGmd_+5VV+E#*Q@YzIrqJcJ3BiiZwvi%x7^h;Ao5bqquvB2TXY3dItKB?i#|5r zlN5^4G3gyX!vtR%&k%#Z;IZC@ZLqj`J!z#Vn0yAnANf_(o4h0& z!(Crn&u#7uquvjO^?wkg6;)TCNiL^K4vr|YcG(90@j>OW##|_Hq3?Om$H3$xOSKCI7x#zjbB z2^$cgi)}3qV&9FEeg>!y$H{+jnEc6`F9>6Zebl@6wK~U7Kny9T%wB-<-#JI@Pcr2k zhxa--efUoAqGcx%#yEAh1rp|Uc}?zDqNnunZME*fa4mmoZri->ky(AGFeG!;KdJ>? znU50JMx_Z)NGa|uWw2yH53tEtC>l_R`a_}1)-iweH@ZtwMwD6!!p)%Mw7t9D+}eFJ zkwFk~?L7KE8B5->s_s+x)u_aKqV8K>kCux69$G0l@Hkc>Ldzk|^pzvPb476M#lrm{ zlzDo_3hmCV#AWw_O5v+i!n3jdq<(XL^Ur~^P{wQ9*uj#*s4TaXb;J-RN*#(G7F<92 zd(>fm^9*(AOYwlEDsDJ9>`(>6#2J|0#)_|nuzBR30e0kNL62D; zSHFb3T!A9@ZYx zBJazKlZK7^UsutoV6c(orCU)zx z=Ma6pSPZp}%2n1Z?FVIE_WY`%wR4oYcI4*8XTH1`=}b)q*V{12^)(E& z%i=MM7X^gRFFLbF+ck4N#k8PiR$zVp6Bg0i<}#mo_10`OzZs49=XU9geuZBf7UcC< z9kR(iSGW+l*TQdo{7Flv&L(HMWz8Arpu1b2nG-<*yrwa)!L!bwJ6hc-k_En^#CnwW zLa4{~LX4eEhbW}?#s95Zj8mcB#DJw2Ea&{X!GJJ~AHujN6E2$t_mS3Wv*McGiXyJ! zla%%UMBE36xCy7nWyrb=jJhfxE#vG@R(xHYz*4?5N!2~MH3lWKvB+zB_>WSR1_$Wf zJ{>DW*O>$vM}TDE)=R%bj}uHS#k=`}X|oAd7xIEPc|Q|tB5R04vg6Pfn6bl$(VVje z(T6c=(P0NBa$ORJgMZX0fP&4226~HkL*T7eoSizWDT2#?EikvfsenD^@o%7MN{Q9S z;f%3kecs7$GN-uV45A)MjsqLQDss~PgVOzm5vYo&SXttz=v*@A0Hjj`Ld+@y zO?U|{e=p}Wp|=A6y3!xkU4OE0K#3FQJ0Z3vGheFaD;M#AJ@8&t^XKK0pBZdHad^ID zYGlRB{mV+vJk|K3^0$VP!pkX2sfD_no~)ne^^A_yijh--Mz1Y?r%cxw7sc0MUVyuU zB=faWMIP@2)eE$5$@OVuV$^{J_w^dYJ&chTV0nnl%*8bC1k;`p_5I$oLj zS0JJxL6w2!6t}Y9-r_v^>U+tMC~r4gE#$yND7#LA@=aKk+K6c&J7=^m2y(L6^Eb{A z>^HwmbwpPI|3|=BC|@Ieg;Y+7l|)DfgMFBu5Ao?MPDzYI`t9>dsh3U%k4|?Albg21 nL7?sJ2I;B*;4uOfy+R-$r%+r8DEpi}a00Tk##_}}`Xv4X?Mo&n delta 1176 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 delta 1176 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 delta 1176 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 delta 1176 zcmV;J1ZVrj5vvK1BYyw^b5ch_0Itp)=>Px(Wl2OqRCr$PUC(P1K@@&NN$8hq8IJIP;ybRXGx)+)uVV+6j~DW;z3$%p@uYvcxa&z zDJ!$x#Z72;-elj<)qa7{(9Yz0-}~mx?wdC|RdmQ+>X7jzE`Q z^sh#vaf687SrT@NYZ}t01_uXABO@bc3xz^{Vqzi_UWaqJoPQl|Hk)(RYW1$Z4lgY& z{a#sFX@At~^-8T)dqhOvEXjt=o#C1YhXx!aqKo=YtGBoJLQhYRzSFuB+-bcaqVetR z?GKSVt)E>I?d&Fw%Oxyi|G|F^2p_x#ZY_Yq`}kKpO=IdPj%!lbS(;`b>R}KHs2(PtSCJkjs#;oImXIDIJ>2yGs)zP7 z7t%wdhxVh!?_o7dcZrdzZXld0IUE?nRam@=$K)c8Ya;Bdif16~3>VeIi07SF@nRHq zihpYw(rtE0R`HlI>^GCdwIUof64gT@Y%V1tY%XQQE;g4+ZffODTH>2L8A|IW;<#MG zO#9N`4209KQ8Ejma5Dd;zc&<4zedR{gu==Em;T;dg~ivFn644WH4%2UwuCjUzaGE? z(;ZKsCwfrZG^DYn6?y<`T3FNa4?Vz|7JuXVi#08*Y3&|~)7698R+lBla7=v@5Kg^B z2@Qn8`#4ZMO=Airj%!jF>0zJ@cEkscz>auktj0FOmWKrAc$p@xy%qpO&b6{pUUDaX z9AT&DTE!4{iW^7RX_^*8*eR}QNITn^ZWz+ea7~0myTrF1Fk@I8mrIywU;3MYaDVzW zN@gJxMtX<`ru{L1^bqM`vdPq0)w1hZ8r4Ho4^chjMm5qyR1Z--