mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-12-22 12:37:41 +01:00
Compare commits
9 Commits
fix/mobile
...
feat/bette
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bddaa469d3 | ||
|
|
2ec8ac7f79 | ||
|
|
1285a93990 | ||
|
|
937220e442 | ||
|
|
06e9b093b9 | ||
|
|
859207b8bc | ||
|
|
becaabfa0f | ||
|
|
f06484c6ab | ||
|
|
bf4c65f483 |
4
.github/workflows/autorelease-excalidraw.yml
vendored
4
.github/workflows/autorelease-excalidraw.yml
vendored
@@ -12,10 +12,10 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Setup Node.js 18.x
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- name: Set up publish access
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -9,10 +9,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install and lint
|
||||
run: |
|
||||
|
||||
4
.github/workflows/locales-coverage.yml
vendored
4
.github/workflows/locales-coverage.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Create report file
|
||||
run: |
|
||||
|
||||
4
.github/workflows/sentry-production.yml
vendored
4
.github/workflows/sentry-production.yml
vendored
@@ -10,10 +10,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js 18.x
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- name: Install and build
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
|
||||
4
.github/workflows/size-limit.yml
vendored
4
.github/workflows/size-limit.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Node.js 18.x
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- name: Install in packages/excalidraw
|
||||
run: yarn
|
||||
working-directory: packages/excalidraw
|
||||
|
||||
@@ -746,7 +746,10 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
||||
): ReconciledExcalidrawElement[] => {
|
||||
const localElements = this.getSceneElementsIncludingDeleted();
|
||||
const appState = this.excalidrawAPI.getAppState();
|
||||
const restoredRemoteElements = restoreElements(remoteElements, null);
|
||||
const restoredRemoteElements = restoreElements(
|
||||
remoteElements,
|
||||
this.excalidrawAPI.getSceneElementsMapIncludingDeleted(),
|
||||
);
|
||||
const reconciledElements = reconcileElements(
|
||||
localElements,
|
||||
restoredRemoteElements as RemoteExcalidrawElement[],
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/common": "0.18.0",
|
||||
"@excalidraw/math": "0.18.0"
|
||||
"@excalidraw/math": "0.18.0",
|
||||
"polygon-clipping": "0.15.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,17 +146,22 @@ export const isBindingEnabled = (appState: AppState): boolean => {
|
||||
export const bindOrUnbindBindingElement = (
|
||||
arrow: NonDeleted<ExcalidrawArrowElement>,
|
||||
draggingPoints: PointsPositionUpdates,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
scene: Scene,
|
||||
appState: AppState,
|
||||
opts?: {
|
||||
newArrow?: boolean;
|
||||
altKey?: boolean;
|
||||
angleLocked?: boolean;
|
||||
initialBinding?: boolean;
|
||||
},
|
||||
) => {
|
||||
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
|
||||
arrow,
|
||||
draggingPoints,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
scene.getNonDeletedElements(),
|
||||
appState,
|
||||
@@ -556,12 +561,14 @@ const bindingStrategyForSimpleArrowEndpointDragging_complex = (
|
||||
export const getBindingStrategyForDraggingBindingElementEndpoints = (
|
||||
arrow: NonDeleted<ExcalidrawArrowElement>,
|
||||
draggingPoints: PointsPositionUpdates,
|
||||
screenPointerX: number,
|
||||
screenPointerY: number,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
appState: AppState,
|
||||
opts?: {
|
||||
newArrow?: boolean;
|
||||
shiftKey?: boolean;
|
||||
angleLocked?: boolean;
|
||||
altKey?: boolean;
|
||||
finalize?: boolean;
|
||||
initialBinding?: boolean;
|
||||
@@ -582,6 +589,8 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
|
||||
return getBindingStrategyForDraggingBindingElementEndpoints_simple(
|
||||
arrow,
|
||||
draggingPoints,
|
||||
screenPointerX,
|
||||
screenPointerY,
|
||||
elementsMap,
|
||||
elements,
|
||||
appState,
|
||||
@@ -592,12 +601,14 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
|
||||
const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
arrow: NonDeleted<ExcalidrawArrowElement>,
|
||||
draggingPoints: PointsPositionUpdates,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
appState: AppState,
|
||||
opts?: {
|
||||
newArrow?: boolean;
|
||||
shiftKey?: boolean;
|
||||
angleLocked?: boolean;
|
||||
altKey?: boolean;
|
||||
finalize?: boolean;
|
||||
initialBinding?: boolean;
|
||||
@@ -669,7 +680,15 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
elementsMap,
|
||||
(e) => maxBindingDistance_simple(appState.zoom),
|
||||
);
|
||||
const pointInElement = hit && isPointInElement(globalPoint, hit, elementsMap);
|
||||
const pointInElement =
|
||||
hit &&
|
||||
(opts?.angleLocked
|
||||
? isPointInElement(
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
hit,
|
||||
elementsMap,
|
||||
)
|
||||
: isPointInElement(globalPoint, hit, elementsMap));
|
||||
const otherBindableElement = otherBinding
|
||||
? (elementsMap.get(
|
||||
otherBinding.elementId,
|
||||
@@ -770,6 +789,12 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
}
|
||||
: { mode: null };
|
||||
|
||||
const otherEndpoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
arrow,
|
||||
startDragged ? -1 : 0,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const other: BindingStrategy =
|
||||
otherBindableElement &&
|
||||
!otherFocusPointIsInElement &&
|
||||
@@ -779,6 +804,19 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
element: otherBindableElement,
|
||||
focusPoint: appState.selectedLinearElement.initialState.altFocusPoint,
|
||||
}
|
||||
: opts?.angleLocked && otherBindableElement
|
||||
? {
|
||||
mode: "orbit",
|
||||
element: otherBindableElement,
|
||||
focusPoint:
|
||||
projectFixedPointOntoDiagonal(
|
||||
arrow,
|
||||
otherEndpoint,
|
||||
otherBindableElement,
|
||||
startDragged ? "end" : "start",
|
||||
elementsMap,
|
||||
) || otherEndpoint,
|
||||
}
|
||||
: { mode: undefined };
|
||||
|
||||
return {
|
||||
@@ -924,6 +962,8 @@ export const bindOrUnbindBindingElements = (
|
||||
bindOrUnbindBindingElement(
|
||||
arrow,
|
||||
new Map(), // No dragging points in this case
|
||||
Infinity,
|
||||
Infinity,
|
||||
scene,
|
||||
appState,
|
||||
);
|
||||
@@ -1126,7 +1166,14 @@ export const updateBindings = (
|
||||
},
|
||||
) => {
|
||||
if (isArrowElement(latestElement)) {
|
||||
bindOrUnbindBindingElement(latestElement, new Map(), scene, appState);
|
||||
bindOrUnbindBindingElement(
|
||||
latestElement,
|
||||
new Map(),
|
||||
Infinity,
|
||||
Infinity,
|
||||
scene,
|
||||
appState,
|
||||
);
|
||||
} else {
|
||||
updateBoundElements(latestElement, scene, {
|
||||
...options,
|
||||
|
||||
470
packages/element/src/freedraw.ts
Normal file
470
packages/element/src/freedraw.ts
Normal file
@@ -0,0 +1,470 @@
|
||||
import polygonClipping from "polygon-clipping";
|
||||
import {
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
type LocalPoint,
|
||||
pointFrom,
|
||||
pointFromVector,
|
||||
vector,
|
||||
vectorAntiNormal,
|
||||
vectorFromPoint,
|
||||
vectorNormal,
|
||||
vectorNormalize,
|
||||
vectorScale,
|
||||
} from "@excalidraw/math";
|
||||
import { debugDrawLine } from "@excalidraw/common";
|
||||
|
||||
import type { GlobalPoint, LineSegment, Vector } from "@excalidraw/math";
|
||||
|
||||
import type { ExcalidrawFreeDrawElement } from "./types";
|
||||
|
||||
const detectSelfIntersection = (
|
||||
stride: LocalPoint[],
|
||||
segment: LineSegment<LocalPoint>,
|
||||
) => {
|
||||
return stride.findIndex((p, i) => {
|
||||
if (i === stride.length - 1) {
|
||||
return false;
|
||||
}
|
||||
const a = lineSegment(stride[i], stride[i + 1]);
|
||||
return lineSegmentIntersectionPoints(a, segment);
|
||||
});
|
||||
};
|
||||
|
||||
const cutUpStrideAtIntersections = (
|
||||
left: LocalPoint[],
|
||||
right: LocalPoint[],
|
||||
): [LocalPoint[][], LocalPoint[][]] => {
|
||||
const collectSelfIntersectionIndices = (stride: LocalPoint[]) => {
|
||||
const indices = new Set<number>();
|
||||
for (let i = 0; i < stride.length - 1; i++) {
|
||||
const segment = lineSegment(stride[i], stride[i + 1]);
|
||||
for (let j = i + 2; j < stride.length - 1; j++) {
|
||||
const otherSegment = lineSegment(stride[j], stride[j + 1]);
|
||||
if (lineSegmentIntersectionPoints(segment, otherSegment)) {
|
||||
indices.add(i);
|
||||
indices.add(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
return indices;
|
||||
};
|
||||
|
||||
const intersectionIndices = new Set<number>();
|
||||
for (let r = 0; r < right.length - 1; r++) {
|
||||
const intersectionIndex = detectSelfIntersection(
|
||||
left,
|
||||
lineSegment(right[r], right[r + 1]),
|
||||
);
|
||||
if (intersectionIndex >= 0) {
|
||||
intersectionIndices.add(intersectionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
for (const index of collectSelfIntersectionIndices(left)) {
|
||||
intersectionIndices.add(index);
|
||||
}
|
||||
for (const index of collectSelfIntersectionIndices(right)) {
|
||||
intersectionIndices.add(index);
|
||||
}
|
||||
|
||||
if (intersectionIndices.size === 0) {
|
||||
return [[left], [right]];
|
||||
}
|
||||
|
||||
const sortedIndices = Array.from(intersectionIndices).sort((a, b) => a - b);
|
||||
const leftStrides: LocalPoint[][] = [];
|
||||
const rightStrides: LocalPoint[][] = [];
|
||||
let startIndex = 0;
|
||||
for (const intersectionIndex of sortedIndices) {
|
||||
leftStrides.push(left.slice(startIndex, intersectionIndex));
|
||||
rightStrides.push(right.slice(startIndex, intersectionIndex));
|
||||
startIndex = intersectionIndex - 1;
|
||||
}
|
||||
leftStrides.push(left.slice(startIndex));
|
||||
rightStrides.push(right.slice(startIndex));
|
||||
|
||||
// if (intersectionIndex !== -1) {
|
||||
// for (let l = 0; l < left.length - 2; l++) {
|
||||
// const [left, right] = cutUpStrideAtIntersections(
|
||||
// leftStrides[l],
|
||||
// rightStrides[l],
|
||||
// );
|
||||
// leftStrides = [
|
||||
// ...leftStrides.slice(0, l - 1),
|
||||
// ...left,
|
||||
// ...leftStrides.slice(l + 1),
|
||||
// ];
|
||||
// rightStrides = [
|
||||
// ...rightStrides.slice(0, l - 1),
|
||||
// ...right,
|
||||
// ...rightStrides.slice(l + 1),
|
||||
// ];
|
||||
// }
|
||||
// }
|
||||
|
||||
return [leftStrides, rightStrides];
|
||||
};
|
||||
|
||||
const catmullRom = (
|
||||
p0: number,
|
||||
p1: number,
|
||||
p2: number,
|
||||
p3: number,
|
||||
t: number,
|
||||
) => {
|
||||
const t2 = t * t;
|
||||
const t3 = t2 * t;
|
||||
return (
|
||||
0.5 *
|
||||
(2 * p1 +
|
||||
(-p0 + p2) * t +
|
||||
(2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 +
|
||||
(-p0 + 3 * p1 - 3 * p2 + p3) * t3)
|
||||
);
|
||||
};
|
||||
|
||||
const midpoint = (a: LocalPoint, b: LocalPoint): LocalPoint =>
|
||||
[(a[0] + b[0]) / 2, (a[1] + b[1]) / 2] as LocalPoint;
|
||||
|
||||
const buildRoundedCapPoints = (
|
||||
capLeft: LocalPoint,
|
||||
capRight: LocalPoint,
|
||||
capDir: Vector,
|
||||
): LocalPoint[] => {
|
||||
const center = midpoint(capLeft, capRight);
|
||||
const radius = Math.hypot(capLeft[0] - center[0], capLeft[1] - center[1]);
|
||||
if (radius === 0) {
|
||||
return [capLeft, capRight];
|
||||
}
|
||||
|
||||
const leftAngle = Math.atan2(capLeft[1] - center[1], capLeft[0] - center[0]);
|
||||
const capDirNorm = vectorNormalize(capDir);
|
||||
let sign = 1;
|
||||
if (capDirNorm[0] !== 0 || capDirNorm[1] !== 0) {
|
||||
const midAngleA = leftAngle + Math.PI / 2;
|
||||
const midAngleB = leftAngle - Math.PI / 2;
|
||||
const dotA =
|
||||
Math.cos(midAngleA) * capDirNorm[0] + Math.sin(midAngleA) * capDirNorm[1];
|
||||
const dotB =
|
||||
Math.cos(midAngleB) * capDirNorm[0] + Math.sin(midAngleB) * capDirNorm[1];
|
||||
sign = dotA >= dotB ? 1 : -1;
|
||||
}
|
||||
|
||||
const angles = [
|
||||
leftAngle - sign * (Math.PI / 2),
|
||||
leftAngle,
|
||||
leftAngle + sign * (Math.PI / 2),
|
||||
leftAngle + sign * Math.PI,
|
||||
leftAngle + sign * (Math.PI * 1.5),
|
||||
];
|
||||
|
||||
const stepsPerSegment = 6;
|
||||
const points: LocalPoint[] = [];
|
||||
for (let i = 1; i < angles.length - 2; i++) {
|
||||
const p0 = angles[i - 1];
|
||||
const p1 = angles[i];
|
||||
const p2 = angles[i + 1];
|
||||
const p3 = angles[i + 2];
|
||||
for (let step = 0; step <= stepsPerSegment; step++) {
|
||||
if (i > 1 && step === 0) {
|
||||
continue;
|
||||
}
|
||||
const t = step / stepsPerSegment;
|
||||
const angle = catmullRom(p0, p1, p2, p3, t);
|
||||
points.push(
|
||||
pointFrom<LocalPoint>(
|
||||
center[0] + Math.cos(angle) * radius,
|
||||
center[1] + Math.sin(angle) * radius,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
};
|
||||
|
||||
const addCapToOutlinePoints = (
|
||||
left: LocalPoint[],
|
||||
right: LocalPoint[],
|
||||
): LocalPoint[] => {
|
||||
if (left.length === 0 || right.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const getCapDirection = (isStart: boolean): Vector => {
|
||||
if (left.length < 2 || right.length < 2) {
|
||||
return vector(0, 0);
|
||||
}
|
||||
const index = isStart ? 0 : left.length - 1;
|
||||
const adjacentIndex = isStart ? 1 : left.length - 2;
|
||||
const mid = midpoint(left[index], right[index]);
|
||||
const adjacentMid = midpoint(left[adjacentIndex], right[adjacentIndex]);
|
||||
const dir = isStart
|
||||
? vectorFromPoint(adjacentMid, mid)
|
||||
: vectorFromPoint(mid, adjacentMid);
|
||||
return vectorNormalize(dir);
|
||||
};
|
||||
|
||||
const startDir = getCapDirection(true);
|
||||
const endDir = getCapDirection(false);
|
||||
|
||||
const endCap = buildRoundedCapPoints(
|
||||
left[left.length - 1],
|
||||
right[right.length - 1],
|
||||
endDir,
|
||||
);
|
||||
const startCap = buildRoundedCapPoints(
|
||||
left[0],
|
||||
right[0],
|
||||
vector(-startDir[0], -startDir[1]),
|
||||
).reverse();
|
||||
|
||||
const rightReversed = right.slice().reverse();
|
||||
const outline = [
|
||||
...left,
|
||||
...endCap.slice(1, -1),
|
||||
...rightReversed,
|
||||
...startCap.slice(1, -1),
|
||||
left[0],
|
||||
];
|
||||
|
||||
return outline;
|
||||
};
|
||||
|
||||
const normalizeUnionRing = (points: LocalPoint[]): LocalPoint[] => {
|
||||
if (points.length < 2) {
|
||||
return points;
|
||||
}
|
||||
const first = points[0];
|
||||
const last = points[points.length - 1];
|
||||
if (first[0] === last[0] && first[1] === last[1]) {
|
||||
return points.slice(0, -1);
|
||||
}
|
||||
return points;
|
||||
};
|
||||
|
||||
const closeUnionRing = (points: LocalPoint[]): LocalPoint[] => {
|
||||
if (points.length < 2) {
|
||||
return points;
|
||||
}
|
||||
const first = points[0];
|
||||
const last = points[points.length - 1];
|
||||
if (first[0] === last[0] && first[1] === last[1]) {
|
||||
return points;
|
||||
}
|
||||
return [...points, first];
|
||||
};
|
||||
|
||||
const getRadiusFromPressure = (
|
||||
pressure: number | undefined,
|
||||
prevPoint: LocalPoint,
|
||||
nextPoint: LocalPoint,
|
||||
strokeWidth: number,
|
||||
) => {
|
||||
return (pressure ?? 1) * strokeWidth * 2;
|
||||
};
|
||||
|
||||
const streamlinePoints = (
|
||||
points: readonly LocalPoint[],
|
||||
streamline: number,
|
||||
): LocalPoint[] => {
|
||||
if (streamline <= 0 || points.length < 2) {
|
||||
return [...points];
|
||||
}
|
||||
|
||||
const streamlined: LocalPoint[] = [points[0]];
|
||||
const t = 1 - streamline;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const prev = streamlined[streamlined.length - 1];
|
||||
const next: LocalPoint = pointFrom<LocalPoint>(
|
||||
prev[0] + (points[i][0] - prev[0]) * t,
|
||||
prev[1] + (points[i][1] - prev[1]) * t,
|
||||
);
|
||||
streamlined.push(next);
|
||||
}
|
||||
|
||||
return streamlined;
|
||||
};
|
||||
|
||||
const densifyPoints = (
|
||||
points: readonly LocalPoint[],
|
||||
insertsPerSegment: number,
|
||||
): LocalPoint[] => {
|
||||
if (points.length < 2 || insertsPerSegment <= 0) {
|
||||
return [...points];
|
||||
}
|
||||
|
||||
const densified: LocalPoint[] = [];
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1] ?? points[i];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1];
|
||||
const p3 = points[i + 2] ?? points[i + 1];
|
||||
|
||||
densified.push(p1);
|
||||
for (let step = 1; step <= insertsPerSegment; step++) {
|
||||
const t = step / (insertsPerSegment + 1);
|
||||
densified.push(
|
||||
pointFrom<LocalPoint>(
|
||||
catmullRom(p0[0], p1[0], p2[0], p3[0], t),
|
||||
catmullRom(p0[1], p1[1], p2[1], p3[1], t),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
densified.push(points[points.length - 1]);
|
||||
return densified;
|
||||
};
|
||||
|
||||
export const getFreedrawOutlinePoints = (
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
) => {
|
||||
return getFreedrawOutlinePolygons(element).flat();
|
||||
};
|
||||
|
||||
export const getFreedrawOutlinePolygons = (
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
): [number, number][][] => {
|
||||
if (element.points.length < 2) {
|
||||
return [];
|
||||
}
|
||||
const simulatedPressure = element.pressures.length === 0;
|
||||
const points = streamlinePoints(element.points, 0.2);
|
||||
//const points = densifyPoints(streamlinePoints(element.points, 0.2), 2);
|
||||
const leftOutlinePoints: LocalPoint[] = [];
|
||||
const rightOutlinePoints: LocalPoint[] = [];
|
||||
for (let i = 0; i < points.length - 2; i++) {
|
||||
const radius =
|
||||
!simulatedPressure && i === 0
|
||||
? getRadiusFromPressure(
|
||||
element.pressures[i + 1],
|
||||
points[i + 1],
|
||||
points[i + 2] ?? points[i + 1],
|
||||
element.strokeWidth,
|
||||
)
|
||||
: !simulatedPressure && points.length > 2 && i === points.length - 1
|
||||
? getRadiusFromPressure(
|
||||
element.pressures[i - 1],
|
||||
points[i - 1],
|
||||
points[i] ?? points[i - 1],
|
||||
element.strokeWidth,
|
||||
)
|
||||
: getRadiusFromPressure(
|
||||
element.pressures[i],
|
||||
points[i],
|
||||
points[i + 1],
|
||||
element.strokeWidth,
|
||||
);
|
||||
const unit = vectorNormalize(vectorFromPoint(points[i + 1], points[i]));
|
||||
const v = vectorScale(unit, radius);
|
||||
|
||||
leftOutlinePoints.push(
|
||||
pointFromVector<LocalPoint>(vectorAntiNormal(v), points[i]),
|
||||
);
|
||||
rightOutlinePoints.push(
|
||||
pointFromVector<LocalPoint>(vectorNormal(v), points[i]),
|
||||
);
|
||||
|
||||
const COLORS = [
|
||||
"#FF0000",
|
||||
"#00FF00",
|
||||
"#0000FF",
|
||||
"#FFFF00",
|
||||
"#FF00FF",
|
||||
"#00FFFF",
|
||||
];
|
||||
if (i > 0 && i < points.length - 2) {
|
||||
debugDrawLine(
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + points[i - 1][0],
|
||||
element.y + points[i - 1][1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + points[i][0],
|
||||
element.y + points[i][1],
|
||||
),
|
||||
),
|
||||
{ permanent: true, color: COLORS[i % COLORS.length] },
|
||||
);
|
||||
debugDrawLine(
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + leftOutlinePoints[i - 1][0],
|
||||
element.y + leftOutlinePoints[i - 1][1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + leftOutlinePoints[i][0],
|
||||
element.y + leftOutlinePoints[i][1],
|
||||
),
|
||||
),
|
||||
{ permanent: true, color: COLORS[i % COLORS.length] },
|
||||
);
|
||||
debugDrawLine(
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + rightOutlinePoints[i - 1][0],
|
||||
element.y + rightOutlinePoints[i - 1][1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + rightOutlinePoints[i][0],
|
||||
element.y + rightOutlinePoints[i][1],
|
||||
),
|
||||
),
|
||||
{ permanent: true, color: COLORS[i % COLORS.length] },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const outline = addCapToOutlinePoints(leftOutlinePoints, rightOutlinePoints);
|
||||
if (outline.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (outline.length < 4) {
|
||||
return [outline];
|
||||
}
|
||||
|
||||
const segmentOutlines: LocalPoint[][] = [];
|
||||
for (let i = 0; i < leftOutlinePoints.length - 1; i++) {
|
||||
const segmentOutline = addCapToOutlinePoints(
|
||||
[leftOutlinePoints[i], leftOutlinePoints[i + 1]],
|
||||
[rightOutlinePoints[i], rightOutlinePoints[i + 1]],
|
||||
);
|
||||
if (segmentOutline.length > 0) {
|
||||
segmentOutlines.push(segmentOutline);
|
||||
}
|
||||
}
|
||||
if (segmentOutlines.length === 0) {
|
||||
return [outline];
|
||||
}
|
||||
|
||||
let unioned = [];
|
||||
try {
|
||||
unioned = polygonClipping.union(
|
||||
// @ts-ignore
|
||||
...segmentOutlines.map((segment) => [normalizeUnionRing(segment)]),
|
||||
) as [number, number][][][];
|
||||
} catch {
|
||||
return [outline];
|
||||
}
|
||||
if (unioned.length === 0) {
|
||||
return [outline];
|
||||
}
|
||||
|
||||
const result: [number, number][][] = [];
|
||||
for (const polygon of unioned) {
|
||||
if (polygon.length === 0) {
|
||||
continue;
|
||||
}
|
||||
for (const ring of polygon) {
|
||||
if (ring.length === 0) {
|
||||
continue;
|
||||
}
|
||||
result.push(closeUnionRing(ring as LocalPoint[]) as [number, number][]);
|
||||
}
|
||||
}
|
||||
|
||||
return result.length > 0 ? result : [outline];
|
||||
};
|
||||
@@ -71,6 +71,7 @@ export * from "./elementLink";
|
||||
export * from "./embeddable";
|
||||
export * from "./flowchart";
|
||||
export * from "./fractionalIndex";
|
||||
export * from "./freedraw";
|
||||
export * from "./frame";
|
||||
export * from "./groups";
|
||||
export * from "./heading";
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
|
||||
import {
|
||||
deconstructLinearOrFreeDrawElement,
|
||||
getHoveredElementForBinding,
|
||||
isPathALoop,
|
||||
moveArrowAboveBindable,
|
||||
projectFixedPointOntoDiagonal,
|
||||
@@ -306,21 +305,11 @@ export class LinearElementEditor {
|
||||
const customLineAngle =
|
||||
linearElementEditor.customLineAngle ??
|
||||
determineCustomLinearAngle(pivotPoint, element.points[idx]);
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
elements,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
// Determine if point movement should happen and how much
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
if (
|
||||
shouldRotateWithDiscreteAngle(event) &&
|
||||
!hoveredElement &&
|
||||
!element.startBinding &&
|
||||
!element.endBinding
|
||||
) {
|
||||
if (shouldRotateWithDiscreteAngle(event)) {
|
||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||
element,
|
||||
elementsMap,
|
||||
@@ -354,11 +343,13 @@ export class LinearElementEditor {
|
||||
[idx],
|
||||
deltaX,
|
||||
deltaY,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
elementsMap,
|
||||
element,
|
||||
elements,
|
||||
app,
|
||||
event.shiftKey,
|
||||
shouldRotateWithDiscreteAngle(event),
|
||||
event.altKey,
|
||||
);
|
||||
|
||||
@@ -492,22 +483,11 @@ export class LinearElementEditor {
|
||||
const endIsSelected = selectedPointsIndices.includes(
|
||||
element.points.length - 1,
|
||||
);
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
elements,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
// Determine if point movement should happen and how much
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
if (
|
||||
shouldRotateWithDiscreteAngle(event) &&
|
||||
singlePointDragged &&
|
||||
!hoveredElement &&
|
||||
!element.startBinding &&
|
||||
!element.endBinding
|
||||
) {
|
||||
if (shouldRotateWithDiscreteAngle(event) && singlePointDragged) {
|
||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||
element,
|
||||
elementsMap,
|
||||
@@ -520,7 +500,6 @@ export class LinearElementEditor {
|
||||
width + pivotPoint[0],
|
||||
height + pivotPoint[1],
|
||||
);
|
||||
|
||||
deltaX = target[0] - draggingPoint[0];
|
||||
deltaY = target[1] - draggingPoint[1];
|
||||
} else {
|
||||
@@ -541,11 +520,13 @@ export class LinearElementEditor {
|
||||
selectedPointsIndices,
|
||||
deltaX,
|
||||
deltaY,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
elementsMap,
|
||||
element,
|
||||
elements,
|
||||
app,
|
||||
event.shiftKey,
|
||||
shouldRotateWithDiscreteAngle(event) && singlePointDragged,
|
||||
event.altKey,
|
||||
);
|
||||
|
||||
@@ -2088,11 +2069,13 @@ const pointDraggingUpdates = (
|
||||
selectedPointsIndices: readonly number[],
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
app: AppClassProperties,
|
||||
shiftKey: boolean,
|
||||
angleLocked: boolean,
|
||||
altKey: boolean,
|
||||
): {
|
||||
positions: PointsPositionUpdates;
|
||||
@@ -2128,12 +2111,14 @@ const pointDraggingUpdates = (
|
||||
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
|
||||
element,
|
||||
naiveDraggingPoints,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
elementsMap,
|
||||
elements,
|
||||
app.state,
|
||||
{
|
||||
newArrow: !!app.state.newElement,
|
||||
shiftKey,
|
||||
angleLocked,
|
||||
altKey,
|
||||
},
|
||||
);
|
||||
@@ -2250,10 +2235,15 @@ const pointDraggingUpdates = (
|
||||
// We need to use a custom intersector to ensure that if there is a big "jump"
|
||||
// in the arrow's position, we can position it with outline avoidance
|
||||
// pixel-perfectly and avoid "dancing" arrows.
|
||||
const customIntersector =
|
||||
// NOTE: Direction matters here, so we create two intersectors
|
||||
const startCustomIntersector =
|
||||
start.focusPoint && end.focusPoint
|
||||
? lineSegment(start.focusPoint, end.focusPoint)
|
||||
: undefined;
|
||||
const endCustomIntersector =
|
||||
start.focusPoint && end.focusPoint
|
||||
? lineSegment(end.focusPoint, start.focusPoint)
|
||||
: undefined;
|
||||
|
||||
// Needed to handle a special case where an existing arrow is dragged over
|
||||
// the same element it is bound to on the other side
|
||||
@@ -2290,7 +2280,7 @@ const pointDraggingUpdates = (
|
||||
nextArrow.endBinding,
|
||||
endBindable,
|
||||
elementsMap,
|
||||
customIntersector,
|
||||
endCustomIntersector,
|
||||
) || nextArrow.points[nextArrow.points.length - 1]
|
||||
: nextArrow.points[nextArrow.points.length - 1];
|
||||
|
||||
@@ -2321,7 +2311,7 @@ const pointDraggingUpdates = (
|
||||
nextArrow.startBinding,
|
||||
startBindable,
|
||||
elementsMap,
|
||||
customIntersector,
|
||||
startCustomIntersector,
|
||||
) || nextArrow.points[0]
|
||||
: nextArrow.points[0];
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import rough from "roughjs/bin/rough";
|
||||
import { getStroke } from "perfect-freehand";
|
||||
|
||||
import {
|
||||
type GlobalPoint,
|
||||
@@ -63,6 +62,7 @@ import {
|
||||
} from "./typeChecks";
|
||||
import { getContainingFrame } from "./frame";
|
||||
import { getCornerRadius } from "./utils";
|
||||
import { getFreedrawOutlinePolygons } from "./freedraw";
|
||||
|
||||
import { ShapeCache } from "./shape";
|
||||
|
||||
@@ -78,7 +78,6 @@ import type {
|
||||
ElementsMap,
|
||||
} from "./types";
|
||||
|
||||
import type { StrokeOptions } from "perfect-freehand";
|
||||
import type { RoughCanvas } from "roughjs/bin/canvas";
|
||||
|
||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||
@@ -88,6 +87,18 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
||||
export const IMAGE_INVERT_FILTER =
|
||||
"invert(100%) hue-rotate(180deg) saturate(1.25)";
|
||||
|
||||
const DEBUG_FREEDRAW_COLORS = [
|
||||
"#e53935",
|
||||
"#8e24aa",
|
||||
"#3949ab",
|
||||
"#1e88e5",
|
||||
"#00897b",
|
||||
"#43a047",
|
||||
"#fdd835",
|
||||
"#fb8c00",
|
||||
"#6d4c41",
|
||||
];
|
||||
|
||||
const isPendingImageElement = (
|
||||
element: ExcalidrawElement,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
@@ -439,15 +450,19 @@ const drawElementOnCanvas = (
|
||||
context.save();
|
||||
context.fillStyle = element.strokeColor;
|
||||
|
||||
const path = getFreeDrawPath2D(element) as Path2D;
|
||||
const fillShape = ShapeCache.get(element);
|
||||
const polygons = getFreedrawOutlinePolygons(element);
|
||||
|
||||
const fillShape = ShapeCache.get(element);
|
||||
if (fillShape) {
|
||||
rc.draw(fillShape);
|
||||
}
|
||||
|
||||
context.fillStyle = element.strokeColor;
|
||||
context.fill(path);
|
||||
|
||||
const path =
|
||||
getFreeDrawPath2D(element) ??
|
||||
(generateFreeDrawShape(element) as Path2D);
|
||||
context.fill(path, "evenodd");
|
||||
|
||||
context.restore();
|
||||
break;
|
||||
@@ -1040,7 +1055,10 @@ export function getFreeDrawPath2D(element: ExcalidrawFreeDrawElement) {
|
||||
}
|
||||
|
||||
export function getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement) {
|
||||
return getSvgPathFromStroke(getFreedrawOutlinePoints(element));
|
||||
return getFreedrawOutlinePolygons(element)
|
||||
.map((polygon) => getSvgPathFromStroke(polygon))
|
||||
.filter((path) => path.length > 0)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function getFreedrawOutlineAsSegments(
|
||||
@@ -1099,28 +1117,6 @@ 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]];
|
||||
|
||||
// Consider changing the options for simulated pressure vs real pressure
|
||||
const options: StrokeOptions = {
|
||||
simulatePressure: element.simulatePressure,
|
||||
size: element.strokeWidth * 4.25,
|
||||
thinning: 0.6,
|
||||
smoothing: 0.5,
|
||||
streamline: 0.5,
|
||||
easing: (t) => Math.sin((t * Math.PI) / 2), // https://easings.net/#easeOutSine
|
||||
last: true,
|
||||
};
|
||||
|
||||
return getStroke(inputPoints as number[][], options) as [number, number][];
|
||||
}
|
||||
|
||||
function med(A: number[], B: number[]) {
|
||||
return [(A[0] + B[0]) / 2, (A[1] + B[1]) / 2];
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
KEYS,
|
||||
arrayToMap,
|
||||
invariant,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
updateActiveTool,
|
||||
} from "@excalidraw/common";
|
||||
import { isPathALoop } from "@excalidraw/element";
|
||||
@@ -102,10 +103,19 @@ export const actionFinalize = register<FormData>({
|
||||
return map;
|
||||
}, new Map()) ?? new Map();
|
||||
|
||||
bindOrUnbindBindingElement(element, draggedPoints, scene, appState, {
|
||||
newArrow,
|
||||
altKey: event.altKey,
|
||||
});
|
||||
bindOrUnbindBindingElement(
|
||||
element,
|
||||
draggedPoints,
|
||||
sceneCoords.x,
|
||||
sceneCoords.y,
|
||||
scene,
|
||||
appState,
|
||||
{
|
||||
newArrow,
|
||||
altKey: event.altKey,
|
||||
angleLocked: shouldRotateWithDiscreteAngle(event),
|
||||
},
|
||||
);
|
||||
} else if (isLineElement(element)) {
|
||||
if (
|
||||
appState.selectedLinearElement?.isEditing &&
|
||||
|
||||
@@ -8617,9 +8617,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
],
|
||||
]),
|
||||
point[0],
|
||||
point[1],
|
||||
this.scene,
|
||||
this.state,
|
||||
{ newArrow: true, altKey: event.altKey, initialBinding: true },
|
||||
{
|
||||
newArrow: true,
|
||||
altKey: event.altKey,
|
||||
initialBinding: true,
|
||||
angleLocked: shouldRotateWithDiscreteAngle(event.nativeEvent),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ElementsMapOrArray,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
@@ -129,7 +130,8 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
||||
const repairBinding = <T extends ExcalidrawArrowElement>(
|
||||
element: T,
|
||||
binding: FixedPointBinding | null,
|
||||
elementsMap: Readonly<ElementsMap>,
|
||||
targetElementsMap: Readonly<ElementsMap>,
|
||||
localElementsMap: Readonly<ElementsMap> | null | undefined,
|
||||
startOrEnd: "start" | "end",
|
||||
): FixedPointBinding | null => {
|
||||
if (!binding) {
|
||||
@@ -148,18 +150,27 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
|
||||
return fixedPointBinding;
|
||||
}
|
||||
|
||||
const boundElement =
|
||||
(elementsMap.get(binding.elementId) as ExcalidrawBindableElement) ||
|
||||
undefined;
|
||||
if (boundElement) {
|
||||
if (binding.mode) {
|
||||
return {
|
||||
elementId: binding.elementId,
|
||||
mode: binding.mode || "orbit",
|
||||
fixedPoint: normalizeFixedPoint(binding.fixedPoint || [0.5, 0.5]),
|
||||
} as FixedPointBinding | null;
|
||||
}
|
||||
// Fallback if the bound element is missing but the binding is at least
|
||||
// looking like a valid one shape-wise
|
||||
if (binding.mode && binding.fixedPoint && binding.elementId) {
|
||||
return {
|
||||
elementId: binding.elementId,
|
||||
mode: binding.mode,
|
||||
fixedPoint: normalizeFixedPoint(binding.fixedPoint || [0.5, 0.5]),
|
||||
} as FixedPointBinding | null;
|
||||
}
|
||||
|
||||
const targetBoundElement =
|
||||
(targetElementsMap.get(binding.elementId) as ExcalidrawBindableElement) ||
|
||||
undefined;
|
||||
const boundElement =
|
||||
targetBoundElement ||
|
||||
(localElementsMap?.get(binding.elementId) as ExcalidrawBindableElement) ||
|
||||
undefined;
|
||||
const elementsMap = targetBoundElement ? targetElementsMap : localElementsMap;
|
||||
|
||||
// migrating legacy focus point bindings
|
||||
if (boundElement && elementsMap) {
|
||||
const p = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
element,
|
||||
startOrEnd === "start" ? 0 : element.points.length - 1,
|
||||
@@ -193,6 +204,8 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
|
||||
};
|
||||
}
|
||||
|
||||
console.error(`could not repair binding for element`);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -284,7 +297,8 @@ const restoreElementWithProperties = <
|
||||
|
||||
export const restoreElement = (
|
||||
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||
elementsMap: Readonly<ElementsMap>,
|
||||
targetElementsMap: Readonly<ElementsMap>,
|
||||
localElementsMap: Readonly<ElementsMap> | null | undefined,
|
||||
opts?: {
|
||||
deleteInvisibleElements?: boolean;
|
||||
},
|
||||
@@ -405,13 +419,15 @@ export const restoreElement = (
|
||||
startBinding: repairBinding(
|
||||
element as ExcalidrawArrowElement,
|
||||
element.startBinding,
|
||||
elementsMap,
|
||||
targetElementsMap,
|
||||
localElementsMap,
|
||||
"start",
|
||||
),
|
||||
endBinding: repairBinding(
|
||||
element as ExcalidrawArrowElement,
|
||||
element.endBinding,
|
||||
elementsMap,
|
||||
targetElementsMap,
|
||||
localElementsMap,
|
||||
"end",
|
||||
),
|
||||
startArrowhead,
|
||||
@@ -575,7 +591,7 @@ const repairFrameMembership = (
|
||||
export const restoreElements = (
|
||||
targetElements: ImportedDataState["elements"],
|
||||
/** NOTE doesn't serve for reconciliation */
|
||||
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||
localElements: Readonly<ElementsMapOrArray> | null | undefined,
|
||||
opts?:
|
||||
| {
|
||||
refreshDimensions?: boolean;
|
||||
@@ -586,7 +602,7 @@ export const restoreElements = (
|
||||
): OrderedExcalidrawElement[] => {
|
||||
// used to detect duplicate top-level element ids
|
||||
const existingIds = new Set<string>();
|
||||
const elementsMap = arrayToMap(targetElements || []);
|
||||
const targetElementsMap = arrayToMap(targetElements || []);
|
||||
const localElementsMap = localElements ? arrayToMap(localElements) : null;
|
||||
const restoredElements = syncInvalidIndices(
|
||||
(targetElements || []).reduce((elements, element) => {
|
||||
@@ -598,7 +614,8 @@ export const restoreElements = (
|
||||
|
||||
let migratedElement: ExcalidrawElement | null = restoreElement(
|
||||
element,
|
||||
elementsMap,
|
||||
targetElementsMap,
|
||||
localElementsMap,
|
||||
{
|
||||
deleteInvisibleElements: opts?.deleteInvisibleElements,
|
||||
},
|
||||
|
||||
@@ -398,6 +398,7 @@ const renderElementToSvg = (
|
||||
node.setAttribute("stroke", "none");
|
||||
const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
|
||||
path.setAttribute("fill", element.strokeColor);
|
||||
path.setAttribute("fill-rule", "evenodd");
|
||||
path.setAttribute("d", getFreeDrawSvgPath(element));
|
||||
node.appendChild(path);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GlobalPoint, LocalPoint, Vector } from "./types";
|
||||
import type { GlobalPoint, LocalPoint, Radians, Vector } from "./types";
|
||||
|
||||
/**
|
||||
* Create a vector from the x and y coordiante elements.
|
||||
@@ -158,3 +158,21 @@ export const vectorNormalize = (v: Vector): Vector => {
|
||||
* Calculate the right-hand normal of the vector.
|
||||
*/
|
||||
export const vectorNormal = (v: Vector): Vector => vector(v[1], -v[0]);
|
||||
|
||||
/**
|
||||
* Calculate the left-hand normal of the vector.
|
||||
*/
|
||||
export const vectorAntiNormal = (v: Vector): Vector => vector(-v[1], v[0]);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param v The vector to rotate
|
||||
* @param angle The angle to rotate the vector by in radians
|
||||
* @returns The rotated vector
|
||||
*/
|
||||
export const vectorRotate = (v: Vector, angle: Radians): Vector => {
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
|
||||
return vector(v[0] * cos - v[1] * sin, v[0] * sin + v[1] * cos);
|
||||
};
|
||||
|
||||
13
yarn.lock
13
yarn.lock
@@ -7877,6 +7877,14 @@ points-on-path@^0.2.1:
|
||||
path-data-parser "0.1.0"
|
||||
points-on-curve "0.2.0"
|
||||
|
||||
polygon-clipping@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/polygon-clipping/-/polygon-clipping-0.15.7.tgz#3823ca1e372566f350795ce9dd9a7b19e97bdaad"
|
||||
integrity sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA==
|
||||
dependencies:
|
||||
robust-predicates "^3.0.2"
|
||||
splaytree "^3.1.0"
|
||||
|
||||
portfinder@^1.0.28:
|
||||
version "1.0.33"
|
||||
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.33.tgz#03dbc51455aa8f83ad9fb86af8345e063bb51101"
|
||||
@@ -8756,6 +8764,11 @@ sourcemap-codec@^1.4.8:
|
||||
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
||||
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
||||
|
||||
splaytree@^3.1.0:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/splaytree/-/splaytree-3.1.2.tgz#d1db2691665a3c69d630de98d55145a6546dc166"
|
||||
integrity sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A==
|
||||
|
||||
split.js@^1.6.0:
|
||||
version "1.6.5"
|
||||
resolved "https://registry.yarnpkg.com/split.js/-/split.js-1.6.5.tgz#f7f61da1044c9984cb42947df4de4fadb5a3f300"
|
||||
|
||||
Reference in New Issue
Block a user