feat: Attempt simple chaining of ovoids
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
@@ -6,28 +6,193 @@ import {
|
|||||||
vectorNormalize,
|
vectorNormalize,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
|
type Polygon,
|
||||||
|
polygonFromPoints,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import { debugDrawLine, debugDrawPolygon } from "@excalidraw/common";
|
import { debugDrawLine, debugDrawPolygon } from "@excalidraw/common";
|
||||||
import polygonClipping from "polygon-clipping";
|
|
||||||
|
|
||||||
import type { Polygon, MultiPolygon, Ring } from "polygon-clipping";
|
|
||||||
import type { ExcalidrawFreeDrawElement } from "./types";
|
import type { ExcalidrawFreeDrawElement } from "./types";
|
||||||
|
|
||||||
// Number of segments to approximate each semicircular cap
|
// Number of segments to approximate each semicircular cap
|
||||||
const CAP_SEGMENTS = 8;
|
const CAP_SEGMENTS = 20;
|
||||||
|
|
||||||
// Minimum radius to avoid degenerate shapes
|
// Minimum radius to avoid degenerate shapes
|
||||||
const MIN_RADIUS = 0.5;
|
const MIN_RADIUS = 0.05;
|
||||||
|
|
||||||
// Pressure to radius multiplier (scaled by strokeWidth)
|
// Pressure to radius multiplier (scaled by strokeWidth)
|
||||||
const PRESSURE_RADIUS_MULTIPLIER = 2.0;
|
const PRESSURE_RADIUS_MULTIPLIER = 2.0;
|
||||||
|
|
||||||
// Minimum distance between points to avoid numerical instability
|
// Minimum distance between points to avoid numerical instability
|
||||||
const MIN_POINT_DISTANCE = 0.1;
|
const MIN_POINT_DISTANCE = 0.001;
|
||||||
|
|
||||||
// Epsilon for filtering near-duplicate points in polygons
|
// Epsilon for filtering near-duplicate points in polygons
|
||||||
const EPSILON = 0.01;
|
const EPSILON = 0.01;
|
||||||
|
|
||||||
|
// Simple union implementation taking advantage of the following facts:
|
||||||
|
// - The ovoids are generated in sequence along the stroke path
|
||||||
|
// - Each ovoid overlaps only with its immediate neighbors
|
||||||
|
// - The ovoids are convex shapes
|
||||||
|
|
||||||
|
// Therefore, we can simply stitch together the outer edges of the ovoids
|
||||||
|
// by taking the first half of the first ovoid and the second half of the last ovoid,
|
||||||
|
// and connecting them with the outer edges of the intermediate ovoids. The overlapping
|
||||||
|
// ovoid caps are always the same radius at the shared points, so they align perfectly.
|
||||||
|
// It should be easy to find the closest point to the side of the previous side segment and
|
||||||
|
// one of the closest points on the next ovoid's start cap.
|
||||||
|
function chainOvoidsIntoSinglePolygon<P extends LocalPoint | GlobalPoint>(
|
||||||
|
records: {
|
||||||
|
polygon: Polygon<P>;
|
||||||
|
firstPoint: P;
|
||||||
|
secondPoint: P;
|
||||||
|
}[],
|
||||||
|
): Polygon<P> | null {
|
||||||
|
if (records.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (records.length === 1) {
|
||||||
|
return records[0].polygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const capPointCount = CAP_SEGMENTS + 1;
|
||||||
|
|
||||||
|
const isClosedPolygon = (points: P[]) => {
|
||||||
|
if (points.length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const first = points[0];
|
||||||
|
const last = points[points.length - 1];
|
||||||
|
return first[0] === last[0] && first[1] === last[1];
|
||||||
|
};
|
||||||
|
|
||||||
|
const openPolygon = (points: P[]) =>
|
||||||
|
isClosedPolygon(points) ? points.slice(0, -1) : points.slice();
|
||||||
|
|
||||||
|
const distanceSq = (a: P, b: P) => {
|
||||||
|
const dx = a[0] - b[0];
|
||||||
|
const dy = a[1] - b[1];
|
||||||
|
return dx * dx + dy * dy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const distanceToSegmentSq = (p: P, a: P, b: P) => {
|
||||||
|
const abx = b[0] - a[0];
|
||||||
|
const aby = b[1] - a[1];
|
||||||
|
const apx = p[0] - a[0];
|
||||||
|
const apy = p[1] - a[1];
|
||||||
|
const abLenSq = abx * abx + aby * aby;
|
||||||
|
|
||||||
|
if (abLenSq === 0) {
|
||||||
|
return distanceSq(p, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / abLenSq));
|
||||||
|
const closest = pointFrom<P>(a[0] + abx * t, a[1] + aby * t);
|
||||||
|
return distanceSq(p, closest);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closestIndexToSegment = (points: P[], a: P, b: P) => {
|
||||||
|
let bestIndex = 0;
|
||||||
|
let bestDistance = Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
const dist = distanceToSegmentSq(points[i], a, b);
|
||||||
|
if (dist < bestDistance) {
|
||||||
|
bestDistance = dist;
|
||||||
|
bestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pushIfDistinct = (points: P[], point: P) => {
|
||||||
|
if (points.length === 0) {
|
||||||
|
points.push(point);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (distanceSq(points[points.length - 1], point) > EPSILON * EPSILON) {
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ovoids = records.map((record) => {
|
||||||
|
const open = openPolygon(record.polygon);
|
||||||
|
|
||||||
|
if (open.length < capPointCount * 2) {
|
||||||
|
return {
|
||||||
|
cap1: open,
|
||||||
|
cap2: [] as P[],
|
||||||
|
p1Right: open[0],
|
||||||
|
p1Left: open[open.length - 1],
|
||||||
|
p2Left: open[0],
|
||||||
|
p2Right: open[open.length - 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cap1 = open.slice(0, capPointCount);
|
||||||
|
const cap2 = open.slice(capPointCount, capPointCount * 2);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cap1,
|
||||||
|
cap2,
|
||||||
|
p1Right: cap1[0],
|
||||||
|
p1Left: cap1[cap1.length - 1],
|
||||||
|
p2Left: cap2[0],
|
||||||
|
p2Right: cap2[cap2.length - 1],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const leftChain: P[] = [];
|
||||||
|
const rightChain: P[] = [];
|
||||||
|
|
||||||
|
ovoids[0].cap1.forEach((point) => pushIfDistinct(leftChain, point));
|
||||||
|
pushIfDistinct(rightChain, ovoids[0].p1Right);
|
||||||
|
|
||||||
|
for (let i = 0; i < ovoids.length; i++) {
|
||||||
|
const current = ovoids[i];
|
||||||
|
|
||||||
|
pushIfDistinct(leftChain, current.p2Left);
|
||||||
|
pushIfDistinct(rightChain, current.p2Right);
|
||||||
|
|
||||||
|
if (i + 1 >= ovoids.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = ovoids[i + 1];
|
||||||
|
|
||||||
|
const leftIndex = closestIndexToSegment(
|
||||||
|
next.cap1,
|
||||||
|
current.p1Left,
|
||||||
|
current.p2Left,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let j = leftIndex; j < next.cap1.length; j++) {
|
||||||
|
pushIfDistinct(leftChain, next.cap1[j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightIndex = closestIndexToSegment(
|
||||||
|
next.cap1,
|
||||||
|
current.p1Right,
|
||||||
|
current.p2Right,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let j = rightIndex; j >= 0; j--) {
|
||||||
|
pushIfDistinct(rightChain, next.cap1[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastOvoid = ovoids[ovoids.length - 1];
|
||||||
|
lastOvoid.cap2.forEach((point) => pushIfDistinct(leftChain, point));
|
||||||
|
|
||||||
|
const rightChainReversed = rightChain.slice(0, -1).reverse();
|
||||||
|
const outline = filterNearDuplicates<P>([
|
||||||
|
...leftChain,
|
||||||
|
...rightChainReversed,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return polygonFromPoints<P>(outline);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the radius for a point based on pressure and strokeWidth.
|
* Compute the radius for a point based on pressure and strokeWidth.
|
||||||
* Pressure is typically in [0, 1] range, default to 0.5 if simulating.
|
* Pressure is typically in [0, 1] range, default to 0.5 if simulating.
|
||||||
@@ -50,20 +215,20 @@ function getRadiusForPressure(pressure: number, strokeWidth: number): number {
|
|||||||
* @param segments - Number of segments to divide the arc into
|
* @param segments - Number of segments to divide the arc into
|
||||||
* @returns Array of points along the arc
|
* @returns Array of points along the arc
|
||||||
*/
|
*/
|
||||||
function generateArcPoints(
|
function generateArcPoints<P extends LocalPoint | GlobalPoint>(
|
||||||
center: LocalPoint,
|
center: LocalPoint,
|
||||||
radius: number,
|
radius: number,
|
||||||
startAngle: number,
|
startAngle: number,
|
||||||
endAngle: number,
|
endAngle: number,
|
||||||
segments: number,
|
segments: number,
|
||||||
): LocalPoint[] {
|
): P[] {
|
||||||
const points: LocalPoint[] = [];
|
const points: P[] = [];
|
||||||
const angleStep = (endAngle - startAngle) / segments;
|
const angleStep = (endAngle - startAngle) / segments;
|
||||||
|
|
||||||
for (let i = 0; i <= segments; i++) {
|
for (let i = 0; i <= segments; i++) {
|
||||||
const angle = startAngle + i * angleStep;
|
const angle = startAngle + i * angleStep;
|
||||||
points.push(
|
points.push(
|
||||||
pointFrom<LocalPoint>(
|
pointFrom<P>(
|
||||||
center[0] + radius * Math.cos(angle),
|
center[0] + radius * Math.cos(angle),
|
||||||
center[1] + radius * Math.sin(angle),
|
center[1] + radius * Math.sin(angle),
|
||||||
),
|
),
|
||||||
@@ -88,20 +253,22 @@ function generateArcPoints(
|
|||||||
* @param forcePerpendicularCap2 - Force cap2 to be perpendicular (for stroke end)
|
* @param forcePerpendicularCap2 - Force cap2 to be perpendicular (for stroke end)
|
||||||
* @returns Array of points forming the ovoid polygon
|
* @returns Array of points forming the ovoid polygon
|
||||||
*/
|
*/
|
||||||
function createOvoid(
|
function createOvoid<P extends LocalPoint | GlobalPoint>(
|
||||||
p1: LocalPoint,
|
p1: LocalPoint,
|
||||||
r1: number,
|
r1: number,
|
||||||
p2: LocalPoint,
|
p2: LocalPoint,
|
||||||
r2: number,
|
r2: number,
|
||||||
forcePerpendicularCap1: boolean = false,
|
forcePerpendicularCap1: boolean = false,
|
||||||
forcePerpendicularCap2: boolean = false,
|
forcePerpendicularCap2: boolean = false,
|
||||||
): LocalPoint[] {
|
): Polygon<P> {
|
||||||
const dist = pointDistance(p1, p2);
|
const dist = pointDistance(p1, p2);
|
||||||
|
|
||||||
// If points are too close, create a circle at the midpoint
|
// If points are too close, create a circle at the midpoint
|
||||||
if (dist < MIN_POINT_DISTANCE) {
|
if (dist < MIN_POINT_DISTANCE) {
|
||||||
const avgRadius = (r1 + r2) / 2;
|
const avgRadius = (r1 + r2) / 2;
|
||||||
return generateArcPoints(p1, avgRadius, 0, Math.PI * 2, CAP_SEGMENTS * 2);
|
return polygonFromPoints<P>(
|
||||||
|
generateArcPoints(p1, avgRadius, 0, Math.PI * 2, CAP_SEGMENTS * 2),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direction vector from p1 to p2
|
// Direction vector from p1 to p2
|
||||||
@@ -130,7 +297,7 @@ function createOvoid(
|
|||||||
const p1PerpAngle = forcePerpendicularCap1 ? purePerpAngle : tangentPerpAngle;
|
const p1PerpAngle = forcePerpendicularCap1 ? purePerpAngle : tangentPerpAngle;
|
||||||
const p1RightAngle = p1PerpAngle;
|
const p1RightAngle = p1PerpAngle;
|
||||||
const p1LeftAngle = p1PerpAngle + Math.PI;
|
const p1LeftAngle = p1PerpAngle + Math.PI;
|
||||||
const cap1Points = generateArcPoints(
|
const cap1Points = generateArcPoints<P>(
|
||||||
p1,
|
p1,
|
||||||
r1,
|
r1,
|
||||||
p1RightAngle,
|
p1RightAngle,
|
||||||
@@ -143,7 +310,7 @@ function createOvoid(
|
|||||||
const p2PerpAngle = forcePerpendicularCap2 ? purePerpAngle : tangentPerpAngle;
|
const p2PerpAngle = forcePerpendicularCap2 ? purePerpAngle : tangentPerpAngle;
|
||||||
const p2LeftAngle = p2PerpAngle + Math.PI;
|
const p2LeftAngle = p2PerpAngle + Math.PI;
|
||||||
const p2RightAngle = p2LeftAngle + Math.PI;
|
const p2RightAngle = p2LeftAngle + Math.PI;
|
||||||
const cap2Points = generateArcPoints(
|
const cap2Points = generateArcPoints<P>(
|
||||||
p2,
|
p2,
|
||||||
r2,
|
r2,
|
||||||
p2LeftAngle,
|
p2LeftAngle,
|
||||||
@@ -154,25 +321,27 @@ function createOvoid(
|
|||||||
// Assemble the ovoid polygon:
|
// Assemble the ovoid polygon:
|
||||||
// cap1 goes around the back of p1, cap2 goes around the front of p2
|
// cap1 goes around the back of p1, cap2 goes around the front of p2
|
||||||
// The arc endpoints naturally connect with the tangent lines
|
// The arc endpoints naturally connect with the tangent lines
|
||||||
const ovoidPoints: LocalPoint[] = [
|
const ovoidPoints: P[] = [
|
||||||
...cap1Points, // p1's back cap
|
...cap1Points, // p1's back cap
|
||||||
...cap2Points, // p2's front cap
|
...cap2Points, // p2's front cap
|
||||||
];
|
];
|
||||||
|
|
||||||
// Filter out near-duplicate consecutive points to avoid numerical issues
|
// Filter out near-duplicate consecutive points to avoid numerical issues
|
||||||
return filterNearDuplicates(ovoidPoints);
|
return polygonFromPoints<P>(filterNearDuplicates<P>(ovoidPoints));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter out consecutive points that are too close together.
|
* Filter out consecutive points that are too close together.
|
||||||
* This prevents numerical instability in polygon boolean operations.
|
* This prevents numerical instability in polygon boolean operations.
|
||||||
*/
|
*/
|
||||||
function filterNearDuplicates(points: LocalPoint[]): LocalPoint[] {
|
function filterNearDuplicates<P extends LocalPoint | GlobalPoint>(
|
||||||
|
points: P[],
|
||||||
|
): P[] {
|
||||||
if (points.length < 2) {
|
if (points.length < 2) {
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered: LocalPoint[] = [points[0]];
|
const filtered: P[] = [points[0]];
|
||||||
|
|
||||||
for (let i = 1; i < points.length; i++) {
|
for (let i = 1; i < points.length; i++) {
|
||||||
const prev = filtered[filtered.length - 1];
|
const prev = filtered[filtered.length - 1];
|
||||||
@@ -203,39 +372,6 @@ function filterNearDuplicates(points: LocalPoint[]): LocalPoint[] {
|
|||||||
return filtered.length >= 3 ? filtered : points;
|
return filtered.length >= 3 ? filtered : points;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a polygon (array of LocalPoints) to polygon-clipping format.
|
|
||||||
* polygon-clipping expects: [[[x,y], [x,y], ...]]
|
|
||||||
*/
|
|
||||||
function toClipperPolygon(points: LocalPoint[]): Polygon {
|
|
||||||
if (points.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// Ensure the polygon is closed
|
|
||||||
const ring: Ring = points.map((p) => [p[0], p[1]]);
|
|
||||||
|
|
||||||
// Close the ring if not already closed
|
|
||||||
if (
|
|
||||||
ring.length > 0 &&
|
|
||||||
(ring[0][0] !== ring[ring.length - 1][0] ||
|
|
||||||
ring[0][1] !== ring[ring.length - 1][1])
|
|
||||||
) {
|
|
||||||
ring.push([ring[0][0], ring[0][1]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [ring];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert polygon-clipping result back to LocalPoint arrays.
|
|
||||||
* Returns the largest polygon (by point count) from the result.
|
|
||||||
*/
|
|
||||||
function fromClipperResult(result: MultiPolygon): LocalPoint[][] {
|
|
||||||
return result.map((polygon) =>
|
|
||||||
polygon[0].map((coord) => pointFrom<LocalPoint>(coord[0], coord[1])),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the outline of a freedraw element using the ovoid-union approach.
|
* Generate the outline of a freedraw element using the ovoid-union approach.
|
||||||
*
|
*
|
||||||
@@ -257,21 +393,21 @@ export function generateFreeDrawOvoidOutline(
|
|||||||
// Debug draw the raw segments from the freedraw element points
|
// Debug draw the raw segments from the freedraw element points
|
||||||
const colors = ["red", "green", "blue", "orange", "purple"];
|
const colors = ["red", "green", "blue", "orange", "purple"];
|
||||||
|
|
||||||
// points.forEach((pt, i) => {
|
points.forEach((pt, i) => {
|
||||||
// if (i === points.length - 1) {
|
if (i === points.length - 1) {
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
// debugDrawLine(
|
debugDrawLine(
|
||||||
// lineSegment(
|
lineSegment(
|
||||||
// pointFrom<GlobalPoint>(x + pt[0], y + pt[1]),
|
pointFrom<GlobalPoint>(x + pt[0], y + pt[1]),
|
||||||
// pointFrom<GlobalPoint>(x + points[i + 1][0], y + points[i + 1][1]),
|
pointFrom<GlobalPoint>(x + points[i + 1][0], y + points[i + 1][1]),
|
||||||
// ),
|
),
|
||||||
// {
|
{
|
||||||
// color: colors[i % colors.length],
|
color: colors[i % colors.length],
|
||||||
// permanent: true,
|
permanent: true,
|
||||||
// },
|
},
|
||||||
// );
|
);
|
||||||
// });
|
});
|
||||||
|
|
||||||
if (points.length === 0) {
|
if (points.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@@ -292,7 +428,7 @@ export function generateFreeDrawOvoidOutline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate ovoids for each consecutive pair of points
|
// Generate ovoids for each consecutive pair of points
|
||||||
const ovoids: Polygon[] = [];
|
const ovoids: Polygon<LocalPoint>[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < points.length - 1; i++) {
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
const p1 = points[i] as LocalPoint;
|
const p1 = points[i] as LocalPoint;
|
||||||
@@ -309,7 +445,7 @@ export function generateFreeDrawOvoidOutline(
|
|||||||
const isFirstSegment = i === 0;
|
const isFirstSegment = i === 0;
|
||||||
const isLastSegment = i === points.length - 2;
|
const isLastSegment = i === points.length - 2;
|
||||||
|
|
||||||
const ovoidPoints = createOvoid(
|
const ovoidPoints = createOvoid<LocalPoint>(
|
||||||
p1,
|
p1,
|
||||||
r1,
|
r1,
|
||||||
p2,
|
p2,
|
||||||
@@ -328,7 +464,7 @@ export function generateFreeDrawOvoidOutline(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (ovoidPoints.length >= 3) {
|
if (ovoidPoints.length >= 3) {
|
||||||
ovoids.push(toClipperPolygon(ovoidPoints));
|
ovoids.push(ovoidPoints);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,38 +473,24 @@ export function generateFreeDrawOvoidOutline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Union all ovoids together
|
// Union all ovoids together
|
||||||
const result = polygonClipping.union(
|
const result = chainOvoidsIntoSinglePolygon<LocalPoint>(
|
||||||
[ovoids[0]],
|
ovoids.map((poly, i) => ({
|
||||||
...ovoids.slice(1).map((ovoid) => [ovoid]),
|
polygon: poly,
|
||||||
|
firstPoint: points[i],
|
||||||
|
secondPoint: points[i + 1],
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
// let result: MultiPolygon = ovoids[0] ? [ovoids[0]] : [];
|
if (result === null) {
|
||||||
|
|
||||||
// for (let i = 1; i < ovoids.length; i++) {
|
|
||||||
// if (ovoids[i]) {
|
|
||||||
// try {
|
|
||||||
// result = polygonClipping.union(result, [ovoids[i]]);
|
|
||||||
// } catch {
|
|
||||||
// // If union fails for this ovoid, skip it
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert back to point array
|
if (result === null) {
|
||||||
// Take the outer ring of the first (largest) polygon
|
|
||||||
const outlinePolygons = fromClipperResult(result);
|
|
||||||
|
|
||||||
if (outlinePolygons.length === 0 || outlinePolygons[0].length === 0) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the first (outer) polygon
|
// Return the first (outer) polygon
|
||||||
return outlinePolygons[0].map((p) => [p[0], p[1]]);
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user