feat: Basic outlining polygon

This commit is contained in:
Mark Tolmacs
2025-12-02 18:02:45 +00:00
parent d080833f4d
commit 6f4081e371
3 changed files with 103 additions and 6 deletions

View File

@@ -0,0 +1,91 @@
import {
type LineSegment,
lineSegment,
type LocalPoint,
pointFrom,
pointFromVector,
vectorAntiNormal,
vectorFromPoint,
vectorNormal,
vectorNormalize,
vectorScale,
} from "@excalidraw/math";
import { type ExcalidrawFreeDrawElement } from "./types";
function generateSegments(
input:
| readonly [x: number, y: number, pressure: number][]
| readonly [x: number, y: number][],
): LineSegment<LocalPoint>[] {
if (input.length < 2) {
return [];
}
const offset = (
x: number,
y: number,
pressure: number,
direction: "left" | "right",
) => {
const p = pointFrom<LocalPoint>(x, y);
const v = vectorNormalize(vectorFromPoint(p));
const normal = direction === "left" ? vectorAntiNormal(v) : vectorNormal(v);
const scaled = vectorScale(normal, pressure / 2);
return pointFromVector(scaled, p);
};
let idx = 0;
const segments = Array(input.length * 2 - 1);
segments[idx++] = lineSegment(
offset(input[0][0], input[0][1], input[0][2] ?? 5, "left"),
offset(input[1][0], input[1][1], input[0][2] ?? 5, "left"),
);
for (let i = 2; i < input.length; i++) {
const point = input[i];
const prev = segments[idx - 1][1];
segments[idx++] = lineSegment(
prev,
offset(point[0], point[1], point[2] ?? 5, "left"),
);
}
const prev = segments[idx - 1][1];
segments[idx++] = lineSegment(
prev,
offset(
input[input.length - 1][0],
input[input.length - 1][1],
input[input.length - 1][2] ?? 5,
"right",
),
);
for (let i = input.length - 2; i >= 0; i--) {
const point = input[i];
const prev = segments[idx - 1][1];
segments[idx++] = lineSegment(
prev,
offset(point[0], point[1], point[2] ?? 5, "right"),
);
}
return segments;
}
export function getStroke(
input:
| readonly [x: number, y: number, pressure: number][]
| readonly [x: number, y: number][],
options: any,
element: ExcalidrawFreeDrawElement,
): LocalPoint[] {
const segments = generateSegments(input);
return [segments[0][0], ...segments.map((s) => s[1])];
}

View File

@@ -1,5 +1,5 @@
import rough from "roughjs/bin/rough";
import { getStroke } from "perfect-freehand";
//import { getStroke } from "perfect-freehand";
import {
type GlobalPoint,
@@ -80,6 +80,7 @@ import type {
import type { StrokeOptions } from "perfect-freehand";
import type { RoughCanvas } from "roughjs/bin/canvas";
import { getStroke } from "./freedraw";
// using a stronger invert (100% vs our regular 93%) and saturate
// as a temp hack to make images in dark theme look closer to original
@@ -1102,10 +1103,10 @@ export function getFreedrawOutlineAsSegments(
export function getFreedrawOutlinePoints(element: ExcalidrawFreeDrawElement) {
// If input points are empty (should they ever be?) return a dot
const inputPoints = element.simulatePressure
? element.points
: element.points.length
? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
: [[0, 0, 0.5]];
? (element.points as readonly [number, number][])
: ((element.points.length
? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
: [[0, 0, 0.5]]) as [number, number, number][]);
// Consider changing the options for simulated pressure vs real pressure
const options: StrokeOptions = {
@@ -1118,7 +1119,7 @@ export function getFreedrawOutlinePoints(element: ExcalidrawFreeDrawElement) {
last: true,
};
return getStroke(inputPoints as number[][], options) as [number, number][];
return getStroke(inputPoints, options, element) as [number, number][];
}
function med(A: number[], B: number[]) {