mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-05 08:36:59 +02:00
Compare commits
8 Commits
fix-better
...
ryan-di/mo
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0ef2611633 | ||
![]() |
46c12a9f8c | ||
![]() |
20dae101e9 | ||
![]() |
022c407e24 | ||
![]() |
7cad3645a0 | ||
![]() |
5921ebc416 | ||
![]() |
864353be5f | ||
![]() |
db2911c6c4 |
@@ -44,6 +44,8 @@ const RE_TWITTER_EMBED =
|
||||
const RE_VALTOWN =
|
||||
/^https:\/\/(?:www\.)?val\.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/;
|
||||
|
||||
const RE_SLIDO = /^https:\/\/app\.sli\.do\/event\/([a-zA-Z0-9]+)/;
|
||||
|
||||
const RE_GENERIC_EMBED =
|
||||
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
|
||||
|
||||
@@ -56,7 +58,7 @@ const RE_REDDIT =
|
||||
const RE_REDDIT_EMBED =
|
||||
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
|
||||
|
||||
const ALLOWED_DOMAINS = new Set([
|
||||
const ALLOWED_EMBED_HOSTS = new Set([
|
||||
"youtube.com",
|
||||
"youtu.be",
|
||||
"vimeo.com",
|
||||
@@ -72,6 +74,9 @@ const ALLOWED_DOMAINS = new Set([
|
||||
"giphy.com",
|
||||
"reddit.com",
|
||||
"forms.microsoft.com",
|
||||
"forms.gle",
|
||||
"docs.google.com/forms",
|
||||
"app.sli.do",
|
||||
]);
|
||||
|
||||
const ALLOW_SAME_ORIGIN = new Set([
|
||||
@@ -86,6 +91,9 @@ const ALLOW_SAME_ORIGIN = new Set([
|
||||
"stackblitz.com",
|
||||
"reddit.com",
|
||||
"forms.microsoft.com",
|
||||
"forms.gle",
|
||||
"docs.google.com",
|
||||
"app.sli.do",
|
||||
]);
|
||||
|
||||
export const createSrcDoc = (body: string) => {
|
||||
@@ -278,6 +286,23 @@ export const getEmbedLink = (
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (RE_SLIDO.test(link)) {
|
||||
const [, eventId] = link.match(RE_SLIDO)!;
|
||||
link = `https://app.sli.do/event/${eventId}`;
|
||||
embeddedLinkCache.set(originalLink, {
|
||||
link,
|
||||
intrinsicSize: aspectRatio,
|
||||
type,
|
||||
sandbox: { allowSameOrigin },
|
||||
});
|
||||
return {
|
||||
link,
|
||||
intrinsicSize: aspectRatio,
|
||||
type,
|
||||
sandbox: { allowSameOrigin },
|
||||
};
|
||||
}
|
||||
|
||||
embeddedLinkCache.set(link, {
|
||||
link,
|
||||
intrinsicSize: aspectRatio,
|
||||
@@ -335,20 +360,29 @@ const matchHostname = (
|
||||
allowedHostnames: Set<string> | string,
|
||||
): string | null => {
|
||||
try {
|
||||
const { hostname } = new URL(url);
|
||||
const { hostname, pathname } = new URL(url);
|
||||
|
||||
const bareDomain = hostname.replace(/^www\./, "");
|
||||
|
||||
if (allowedHostnames instanceof Set) {
|
||||
if (ALLOWED_DOMAINS.has(bareDomain)) {
|
||||
// Check for exact domain match
|
||||
if (ALLOWED_EMBED_HOSTS.has(bareDomain)) {
|
||||
return bareDomain;
|
||||
}
|
||||
|
||||
// Check for path-based match (e.g., docs.google.com/forms)
|
||||
const domainWithPath = `${bareDomain}${
|
||||
pathname.split("/")[1] ? `/${pathname.split("/")[1]}` : ""
|
||||
}`;
|
||||
if (ALLOWED_EMBED_HOSTS.has(domainWithPath)) {
|
||||
return domainWithPath;
|
||||
}
|
||||
|
||||
const bareDomainWithFirstSubdomainWildcarded = bareDomain.replace(
|
||||
/^([^.]+)/,
|
||||
"*",
|
||||
);
|
||||
if (ALLOWED_DOMAINS.has(bareDomainWithFirstSubdomainWildcarded)) {
|
||||
if (ALLOWED_EMBED_HOSTS.has(bareDomainWithFirstSubdomainWildcarded)) {
|
||||
return bareDomainWithFirstSubdomainWildcarded;
|
||||
}
|
||||
return null;
|
||||
@@ -399,6 +433,7 @@ export const embeddableURLValidator = (
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validateEmbeddable != null) {
|
||||
if (typeof validateEmbeddable === "function") {
|
||||
const ret = validateEmbeddable(url);
|
||||
@@ -424,5 +459,5 @@ export const embeddableURLValidator = (
|
||||
}
|
||||
}
|
||||
|
||||
return !!matchHostname(url, ALLOWED_DOMAINS);
|
||||
return !!matchHostname(url, ALLOWED_EMBED_HOSTS);
|
||||
};
|
||||
|
@@ -149,6 +149,7 @@ export class LinearElementEditor {
|
||||
public readonly hoverPointIndex: number;
|
||||
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||
public readonly elbowed: boolean;
|
||||
public readonly customLineAngle: number | null;
|
||||
|
||||
constructor(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
@@ -186,6 +187,7 @@ export class LinearElementEditor {
|
||||
this.hoverPointIndex = -1;
|
||||
this.segmentMidPointHoveredCoords = null;
|
||||
this.elbowed = isElbowArrow(element) && element.elbowed;
|
||||
this.customLineAngle = null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -289,6 +291,7 @@ export class LinearElementEditor {
|
||||
const { elementId } = linearElementEditor;
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
let customLineAngle = linearElementEditor.customLineAngle;
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
@@ -329,6 +332,12 @@ export class LinearElementEditor {
|
||||
const selectedIndex = selectedPointsIndices[0];
|
||||
const referencePoint =
|
||||
element.points[selectedIndex === 0 ? 1 : selectedIndex - 1];
|
||||
customLineAngle =
|
||||
linearElementEditor.customLineAngle ??
|
||||
Math.atan2(
|
||||
element.points[selectedIndex][1] - referencePoint[1],
|
||||
element.points[selectedIndex][0] - referencePoint[0],
|
||||
);
|
||||
|
||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||
element,
|
||||
@@ -336,6 +345,7 @@ export class LinearElementEditor {
|
||||
referencePoint,
|
||||
pointFrom(scenePointerX, scenePointerY),
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
customLineAngle,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(
|
||||
@@ -457,6 +467,7 @@ export class LinearElementEditor {
|
||||
? lastClickedPoint
|
||||
: -1,
|
||||
isDragging: true,
|
||||
customLineAngle,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -551,6 +562,8 @@ export class LinearElementEditor {
|
||||
return {
|
||||
...editingLinearElement,
|
||||
...bindings,
|
||||
segmentMidPointHoveredCoords: null,
|
||||
hoverPointIndex: -1,
|
||||
// if clicking without previously dragging a point(s), and not holding
|
||||
// shift, deselect all points except the one clicked. If holding shift,
|
||||
// toggle the point.
|
||||
@@ -572,6 +585,7 @@ export class LinearElementEditor {
|
||||
: selectedPointsIndices,
|
||||
isDragging: false,
|
||||
pointerOffset: { x: 0, y: 0 },
|
||||
customLineAngle: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1593,6 +1607,7 @@ export class LinearElementEditor {
|
||||
referencePoint: LocalPoint,
|
||||
scenePointer: GlobalPoint,
|
||||
gridSize: NullableGridSize,
|
||||
customLineAngle?: number,
|
||||
) {
|
||||
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
@@ -1618,6 +1633,7 @@ export class LinearElementEditor {
|
||||
referencePointCoords[1],
|
||||
gridX,
|
||||
gridY,
|
||||
customLineAngle,
|
||||
);
|
||||
|
||||
return pointRotateRads(
|
||||
|
@@ -2,6 +2,12 @@ import {
|
||||
SHIFT_LOCKING_ANGLE,
|
||||
viewportCoordsToSceneCoords,
|
||||
} from "@excalidraw/common";
|
||||
import {
|
||||
normalizeRadians,
|
||||
radiansBetweenAngles,
|
||||
radiansDifference,
|
||||
type Radians,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { pointsEqual } from "@excalidraw/math";
|
||||
|
||||
@@ -152,13 +158,42 @@ export const getLockedLinearCursorAlignSize = (
|
||||
originY: number,
|
||||
x: number,
|
||||
y: number,
|
||||
customAngle?: number,
|
||||
) => {
|
||||
let width = x - originX;
|
||||
let height = y - originY;
|
||||
|
||||
const lockedAngle =
|
||||
Math.round(Math.atan(height / width) / SHIFT_LOCKING_ANGLE) *
|
||||
SHIFT_LOCKING_ANGLE;
|
||||
const angle = Math.atan2(height, width) as Radians;
|
||||
let lockedAngle = (Math.round(angle / SHIFT_LOCKING_ANGLE) *
|
||||
SHIFT_LOCKING_ANGLE) as Radians;
|
||||
|
||||
if (customAngle) {
|
||||
// If custom angle is provided, we check if the angle is close to the
|
||||
// custom angle, snap to that if close engough, otherwise snap to the
|
||||
// higher or lower angle depending on the current angle vs custom angle.
|
||||
const lower = (Math.floor(customAngle / SHIFT_LOCKING_ANGLE) *
|
||||
SHIFT_LOCKING_ANGLE) as Radians;
|
||||
if (
|
||||
radiansBetweenAngles(
|
||||
angle,
|
||||
lower,
|
||||
(lower + SHIFT_LOCKING_ANGLE) as Radians,
|
||||
)
|
||||
) {
|
||||
if (
|
||||
radiansDifference(angle, customAngle as Radians) <
|
||||
SHIFT_LOCKING_ANGLE / 6
|
||||
) {
|
||||
lockedAngle = customAngle as Radians;
|
||||
} else if (
|
||||
normalizeRadians(angle) > normalizeRadians(customAngle as Radians)
|
||||
) {
|
||||
lockedAngle = (lower + SHIFT_LOCKING_ANGLE) as Radians;
|
||||
} else {
|
||||
lockedAngle = lower;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lockedAngle === 0) {
|
||||
height = 0;
|
||||
|
@@ -292,7 +292,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`12`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
|
||||
expect(line.points.length).toEqual(3);
|
||||
expect(line.points).toMatchInlineSnapshot(`
|
||||
@@ -333,7 +333,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`9`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
|
||||
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
|
||||
h.elements[0] as ExcalidrawLinearElement,
|
||||
@@ -394,7 +394,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`12`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
|
||||
expect([line.x, line.y]).toEqual([
|
||||
points[0][0] + deltaX,
|
||||
@@ -462,7 +462,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`16`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
|
||||
expect(line.points.length).toEqual(5);
|
||||
|
||||
@@ -513,7 +513,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`12`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
@@ -554,7 +554,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`12`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
@@ -602,7 +602,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`18`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
@@ -660,7 +660,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`16`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(line.points.length).toEqual(5);
|
||||
|
||||
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
||||
@@ -758,7 +758,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`12`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
@@ -1411,5 +1411,55 @@ describe("Test Linear Elements", () => {
|
||||
expect(line.points[line.points.length - 1][0]).toBe(20);
|
||||
expect(line.points[line.points.length - 1][1]).toBe(-20);
|
||||
});
|
||||
|
||||
it("should preserve original angle when dragging endpoint with SHIFT key", () => {
|
||||
createTwoPointerLinearElement("line");
|
||||
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||
enterLineEditingMode(line);
|
||||
|
||||
const elementsMap = arrayToMap(h.elements);
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
// Calculate original angle between first and last point
|
||||
const originalAngle = Math.atan2(
|
||||
points[1][1] - points[0][1],
|
||||
points[1][0] - points[0][0],
|
||||
);
|
||||
|
||||
// Drag the second point (endpoint) with SHIFT key pressed
|
||||
const startPoint = pointFrom<GlobalPoint>(points[1][0], points[1][1]);
|
||||
const endPoint = pointFrom<GlobalPoint>(
|
||||
startPoint[0] + 4,
|
||||
startPoint[1] + 4,
|
||||
);
|
||||
|
||||
// Perform drag with SHIFT key modifier
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.downAt(startPoint[0], startPoint[1]);
|
||||
mouse.moveTo(endPoint[0], endPoint[1]);
|
||||
mouse.upAt(endPoint[0], endPoint[1]);
|
||||
});
|
||||
|
||||
// Get updated points after drag
|
||||
const updatedPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
// Calculate new angle
|
||||
const newAngle = Math.atan2(
|
||||
updatedPoints[1][1] - updatedPoints[0][1],
|
||||
updatedPoints[1][0] - updatedPoints[0][0],
|
||||
);
|
||||
|
||||
// The angle should be preserved (within a small tolerance for floating point precision)
|
||||
const angleDifference = Math.abs(newAngle - originalAngle);
|
||||
const tolerance = 0.01; // Small tolerance for floating point precision
|
||||
|
||||
expect(angleDifference).toBeLessThan(tolerance);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -11,6 +11,7 @@ export const actionSetEmbeddableAsActiveTool = register({
|
||||
trackEvent: { category: "toolbar" },
|
||||
target: "Tool",
|
||||
label: "toolBar.embeddable",
|
||||
keywords: ["embeddable", "embed", "iframe"],
|
||||
perform: (elements, appState, _, app) => {
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "embeddable",
|
||||
|
@@ -7253,12 +7253,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
}
|
||||
|
||||
// Prioritize unlocked elements over locked ones
|
||||
if (unlockedHitElements.length > 0) {
|
||||
// If there are unlocked elements, use the topmost one
|
||||
pointerDownState.hit.element =
|
||||
unlockedHitElements[unlockedHitElements.length - 1];
|
||||
} else if (
|
||||
if (
|
||||
hitElementMightBeLocked &&
|
||||
hitElementMightBeLocked.locked &&
|
||||
!unlockedHitElements.some(
|
||||
@@ -9123,7 +9118,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (selectedELements.length > 1) {
|
||||
this.setState({ selectedLinearElement: null });
|
||||
}
|
||||
} else {
|
||||
} else if (this.state.selectedLinearElement.isDragging) {
|
||||
this.actionManager.executeAction(actionFinalize, "ui", {
|
||||
event: childEvent,
|
||||
});
|
||||
|
@@ -319,6 +319,7 @@ function CommandPaletteInner({
|
||||
actionManager.actions.toggleHandTool,
|
||||
actionManager.actions.setFrameAsActiveTool,
|
||||
actionManager.actions.toggleLassoTool,
|
||||
actionManager.actions.setEmbeddableAsActiveTool,
|
||||
].map((action) => actionToCommand(action, DEFAULT_CATEGORIES.tools));
|
||||
|
||||
const editorCommands: CommandPaletteItem[] = [
|
||||
|
@@ -8630,6 +8630,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {},
|
||||
"selectedLinearElement": LinearElementEditor {
|
||||
"customLineAngle": null,
|
||||
"elbowed": false,
|
||||
"elementId": "id0",
|
||||
"endBindingElement": "keep",
|
||||
@@ -8853,6 +8854,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {},
|
||||
"selectedLinearElement": LinearElementEditor {
|
||||
"customLineAngle": null,
|
||||
"elbowed": false,
|
||||
"elementId": "id0",
|
||||
"endBindingElement": "keep",
|
||||
@@ -9270,6 +9272,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {},
|
||||
"selectedLinearElement": LinearElementEditor {
|
||||
"customLineAngle": null,
|
||||
"elbowed": false,
|
||||
"elementId": "id0",
|
||||
"endBindingElement": "keep",
|
||||
@@ -9673,6 +9676,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {},
|
||||
"selectedLinearElement": LinearElementEditor {
|
||||
"customLineAngle": null,
|
||||
"elbowed": false,
|
||||
"elementId": "id0",
|
||||
"endBindingElement": "keep",
|
||||
|
@@ -426,7 +426,7 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@@ -470,7 +470,7 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
|
@@ -8,16 +8,10 @@ import type {
|
||||
Radians,
|
||||
} from "./types";
|
||||
|
||||
// TODO: Simplify with modulo and fix for angles beyond 4*Math.PI and - 4*Math.PI
|
||||
export const normalizeRadians = (angle: Radians): Radians => {
|
||||
if (angle < 0) {
|
||||
return (angle + 2 * Math.PI) as Radians;
|
||||
}
|
||||
if (angle >= 2 * Math.PI) {
|
||||
return (angle - 2 * Math.PI) as Radians;
|
||||
}
|
||||
return angle;
|
||||
};
|
||||
export const normalizeRadians = (angle: Radians): Radians =>
|
||||
angle < 0
|
||||
? (((angle % (2 * Math.PI)) + 2 * Math.PI) as Radians)
|
||||
: ((angle % (2 * Math.PI)) as Radians);
|
||||
|
||||
/**
|
||||
* Return the polar coordinates for the given cartesian point represented by
|
||||
@@ -49,3 +43,35 @@ export function radiansToDegrees(degrees: Radians): Degrees {
|
||||
export function isRightAngleRads(rads: Radians): boolean {
|
||||
return Math.abs(Math.sin(2 * rads)) < PRECISION;
|
||||
}
|
||||
|
||||
export function radiansBetweenAngles(
|
||||
a: Radians,
|
||||
min: Radians,
|
||||
max: Radians,
|
||||
): boolean {
|
||||
a = normalizeRadians(a);
|
||||
min = normalizeRadians(min);
|
||||
max = normalizeRadians(max);
|
||||
|
||||
if (min < max) {
|
||||
return a >= min && a <= max;
|
||||
}
|
||||
|
||||
// The range wraps around the 0 angle
|
||||
return a >= min || a <= max;
|
||||
}
|
||||
|
||||
export function radiansDifference(a: Radians, b: Radians): Radians {
|
||||
a = normalizeRadians(a);
|
||||
b = normalizeRadians(b);
|
||||
|
||||
let diff = a - b;
|
||||
|
||||
if (diff < -Math.PI) {
|
||||
diff = (diff + 2 * Math.PI) as Radians;
|
||||
} else if (diff > Math.PI) {
|
||||
diff = (diff - 2 * Math.PI) as Radians;
|
||||
}
|
||||
|
||||
return Math.abs(diff) as Radians;
|
||||
}
|
||||
|
Reference in New Issue
Block a user