Compare commits

..

4 Commits

Author SHA1 Message Date
Aakansha Doshi
9c10fe60f1 fix hitbox 2023-02-16 13:29:54 +05:30
Aakansha Doshi
96b172ebfa support rotation 2023-02-15 17:36:46 +05:30
Aakansha Doshi
16db74cedd support ellipse 2023-02-15 15:20:12 +05:30
Aakansha Doshi
4d1b31a171 feat: bind text to container when clicked on filled shape or element stroke 2023-02-15 13:32:47 +05:30
11 changed files with 142 additions and 203 deletions

136
README.md
View File

@@ -1,121 +1,29 @@
<a href="https://excalidraw.com/" target="_blank" rel="noopener">
<picture>
<source media="(prefers-color-scheme: dark)" alt="Excalidraw" srcset="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2FExcalidraw_Github_cover_dark.png" />
<img alt="Excalidraw" src="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2FExcalidraw_Github_cover.png" />
</picture>
</a>
<h4 align="center">
<a href="https://excalidraw.com">Excalidraw Editor</a> |
<a href="https://blog.excalidraw.com">Blog</a> |
<a href="https://docs.excalidraw.com">Documentation</a> |
<a href="https://plus.excalidraw.com">Excalidraw+</a>
</h4>
<div align="center">
<h2>
An open source virtual hand-drawn style whiteboard. </br>
Collaborative and end-to-end encrypted. </br>
<br />
</h3>
</div>
<br />
<p align="center">
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
<div align="center" style="display:flex;flex-direction:column;"}>
<a href="https://excalidraw.com">
<img width="540" src="./public/og-image-sm.png" alt="Excalidraw logo: Sketch handrawn like diagrams."/>
</a>
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
</a>
<a href="https://discord.gg/UexuTaE">
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
</a>
<a href="https://twitter.com/excalidraw">
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
</a>
</p>
<div align="center">
<figure>
<a href="https://excalidraw.com" target="_blank" rel="noopener">
<img src="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2Fproduct_showcase.png" alt="Product showcase" />
<h3>Virtual whiteboard for sketching hand-drawn like diagrams.<br/>Collaborative and end-to-end encrypted.</h3>
<p>
<a href="https://twitter.com/excalidraw">
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
</a>
<figcaption>
<p align="center">
Create beautiful hand-drawn like diagrams, wireframes, or whatever you like.
</p>
</figcaption>
</figure>
<a href="https://discord.gg/UexuTaE">
<img alt="Chat with us on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
</a>
</p>
</div>
## Features
## Try now
The Excalidraw editor (npm package) supports:
Visit [excalidraw.com](https://excalidraw.com) to start sketching.
- 💯&nbsp;Free & open-source.
- 🎨&nbsp;Infinite, canvas-based whiteboard.
- ✍️&nbsp;Hand-drawn like style.
- 🌓&nbsp;Dark mode.
- 🏗️&nbsp;Customizable.
- 📷&nbsp;Image support.
- 😀&nbsp;Shape libraries support.
- 👅&nbsp;Localization (i18n) support.
- 🖼️&nbsp;Export to PNG, SVG & clipboard.
- 💾&nbsp;Open format - export drawings as an `.excalidraw` json file.
- ⚒️&nbsp;Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
- ➡️&nbsp;Arrow-binding & labeled arrows.
- 🔙&nbsp;Undo / Redo.
- 🔍&nbsp;Zoom and panning support.
## Community
## Excalidraw.com
For latest updates, follow us on [twitter](https://twitter.com/excalidraw). If you need help or want to chat, join us on [Discord](https://discord.gg/UexuTaE). For releases and deep dives, check out our [blog](https://blog.excalidraw.com). Report bugs on [GitHub](https://github.com/excalidraw/excalidraw/issues).
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/maielo/new-readme/src/excalidraw-app) is part of this repository as well, and the app features:
## Supporting Excalidraw
- 📡&nbsp;PWA support (works offline).
- 🤼&nbsp;Real-time collaboration.
- 🔒&nbsp;End-to-end encryption.
- 💾&nbsp;Local-first support (autosaves to the browser).
- 🔗&nbsp;Shareable links (export to a readonly link you can share with others).
We'll be adding these features as drop-in plugins for the npm package in the future.
## Quick start
Install the [Excalidraw npm package](https://www.npmjs.com/package/@excalidraw/excalidraw):
```
npm install react react-dom @excalidraw/excalidraw
```
or via yarn
```
yarn add react react-dom @excalidraw/excalidraw
```
Don't forget to check out our [Documentation](https://docs.excalidraw.com)!
## Contributing
- Missing something or found a bug? [Report here](https://github.com/excalidraw/excalidraw/issues).
- Want to contribute? Check out our [contribution guide](https://docs.excalidraw.com/docs/introduction/contributing) or let us know on [Discord](https://discord.gg/UexuTaE).
- Want to help with translations? See the [translation guide](https://docs.excalidraw.com/docs/introduction/contributing#translating).
## Integrations
- [VScode extension](https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor)
- [npm package](https://www.npmjs.com/package/@excalidraw/excalidraw)
## Who's integrating Excalidraw
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) • and many others
## Sponsors & support
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw) or use [Excalidraw+](https://plus.excalidraw.com/).
## Thank you for supporting Excalidraw
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw).
[<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/10/website)
@@ -124,3 +32,13 @@ If you like the project, you can become a sponsor at [Open Collective](https://o
Last but not least, we're thankful to these companies for offering their services for free:
[![Vercel](./.github/assets/vercel.svg)](https://vercel.com) [![Sentry](./.github/assets/sentry.svg)](https://sentry.io) [![Crowdin](./.github/assets/crowdin.svg)](https://crowdin.com)
## Developers
You can integrate Excalidraw into your app by installing our [npm component](https://npmjs.com/package/@excalidraw/excalidraw).
Visit our documentation on [https://docs.excalidraw.com](https://docs.excalidraw.com).
## Who's integrating Excalidraw
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/)

View File

@@ -53,7 +53,7 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
**_Signature_**
<pre>
restore(
restoreElements(
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp;
localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>

View File

@@ -34,16 +34,14 @@ function App() {
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
The following worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
```jsx showLineNumbers
import { useState, useEffect } from "react";
export default function App() {
const [Excalidraw, setExcalidraw] = useState(null);
const [Comp, setComp] = useState(null);
useEffect(() => {
import("@excalidraw/excalidraw").then((comp) => setExcalidraw(comp.Excalidraw));
import("@excalidraw/excalidraw").then((comp) => setComp(comp.default));
}, []);
return <>{Excalidraw && <Excalidraw />}</>;
return <>{Comp && <Comp />}</>;
}
```

View File

@@ -1,6 +0,0 @@
---
title: Introduction to the codebase
slug: ../
---
This section is documenting the Excalidraw codebase itself for developers who want to contribute to the project.

View File

@@ -20,7 +20,7 @@ Pull requests are welcome. For major changes, please [open an issue](https://git
### Option 2 - CodeSandbox
1. Go to https://codesandbox.io/p/github/excalidraw/excalidraw
1. Go to https://codesandbox.io/s/github/excalidraw/excalidraw
1. Connect your GitHub account
1. Go to Git tab on left side
1. Tap on `Fork Sandbox`

View File

@@ -92,16 +92,6 @@ const sidebars = {
"@excalidraw/excalidraw/development",
],
},
{
type: "category",
label: "Excalidraw codebase",
link: {
type: "doc",
id: "codebase/introduction-to-the-codebase",
},
items: [],
},
],
};

View File

@@ -265,6 +265,7 @@ import {
getContainerCenter,
getContainerDims,
getTextBindableContainerAtPosition,
isHittingContainerStroke,
isValidTextContainer,
} from "../element/textElement";
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
@@ -2766,11 +2767,15 @@ class App extends React.Component<AppProps, AppState> {
if (
isArrowElement(container) ||
hasBoundTextElement(container) ||
!isTransparent(container.backgroundColor) ||
isHittingElementNotConsideringBoundingBox(container, this.state, [
!isTransparent(
(container as ExcalidrawTextContainer).backgroundColor,
) ||
isHittingContainerStroke(
sceneX,
sceneY,
])
container,
this.state.zoom.value,
)
) {
const midPoint = getContainerCenter(container, this.state);

View File

@@ -28,6 +28,7 @@ import {
resetOriginalContainerCache,
updateOriginalContainerCache,
} from "./textWysiwyg";
import { rotatePoint } from "../math";
export const normalizeText = (text: string) => {
return (
@@ -723,3 +724,93 @@ export const isValidTextContainer = (element: ExcalidrawElement) => {
isArrowElement(element)
);
};
export const isHittingContainerStroke = (
x: number,
y: number,
container: ExcalidrawTextContainer,
zoom: number,
) => {
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(container);
const topLeft = [x1, y1];
const topRight = [x2, y1];
const bottomLeft = [x1, y2];
const bottomRight = [x2, y2];
const [counterRotateX, counterRotateY] = rotatePoint(
[x, y],
[cx, cy],
-container.angle,
);
const strokeWidth = container.strokeWidth;
const threshold = 10 / zoom;
if (container.type === "ellipse") {
const h = (topLeft[0] + topRight[0]) / 2;
const k = (topLeft[1] + bottomLeft[1]) / 2;
let a = container.width / 2 + strokeWidth / 2 + threshold;
let b = container.height / 2 + strokeWidth / 2 + threshold;
const checkPointOnOuterEllipse =
Math.pow(counterRotateX - h, 2) / Math.pow(a, 2) +
Math.pow(counterRotateY - k, 2) / Math.pow(b, 2);
a = container.width / 2 - strokeWidth / 2 - threshold;
b = container.height / 2 - strokeWidth / 2 - threshold;
const checkPointOnInnerEllipse =
Math.pow(counterRotateX - h, 2) / Math.pow(a, 2) +
Math.pow(counterRotateY - k, 2) / Math.pow(b, 2);
// The expression evaluates to 1 means point is on ellipse,
// < 1 means inside ellipse and > 1 means outside ellipse
if (
checkPointOnInnerEllipse === 1 ||
checkPointOnOuterEllipse === 1 ||
(checkPointOnInnerEllipse > 1 && checkPointOnOuterEllipse < 1)
) {
return true;
}
return false;
}
// Left Stroke
if (
counterRotateX >= topLeft[0] - strokeWidth / 2 - threshold &&
counterRotateX <= topLeft[0] + strokeWidth / 2 + threshold &&
counterRotateY >= topLeft[1] - threshold &&
counterRotateY <= bottomRight[1] + threshold
) {
return true;
}
// Top stroke
if (
counterRotateX >= topLeft[0] - threshold &&
counterRotateX <= topRight[0] + threshold &&
counterRotateY >= topLeft[1] - threshold - strokeWidth / 2 &&
counterRotateY <= topLeft[1] + threshold + strokeWidth / 2
) {
return true;
}
// Right stroke
if (
counterRotateX >= topRight[0] - threshold - strokeWidth / 2 &&
counterRotateX <= topRight[0] + threshold + strokeWidth / 2 &&
counterRotateY >= topRight[1] - threshold &&
counterRotateY <= bottomRight[1] + threshold
) {
return true;
}
// Bottom Stroke
if (
counterRotateX >= bottomLeft[0] - threshold &&
counterRotateX <= bottomRight[0] + threshold &&
counterRotateY >= bottomLeft[1] - threshold - strokeWidth / 2 &&
counterRotateY <= bottomLeft[1] + threshold + strokeWidth / 2
) {
return true;
}
return false;
};

View File

@@ -463,21 +463,14 @@ describe("textWysiwyg", () => {
});
});
it("should bind text to container when double clicked inside filled container", async () => {
const rectangle = API.createElement({
type: "rectangle",
x: 10,
y: 20,
width: 90,
height: 75,
backgroundColor: "red",
});
h.elements = [rectangle];
it("should bind text to container when double clicked on center of filled container", async () => {
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(rectangle.id);
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
mouse.doubleClickAt(
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
expect(h.elements.length).toBe(2);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
@@ -511,37 +504,24 @@ describe("textWysiwyg", () => {
});
h.elements = [rectangle];
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
expect(h.elements.length).toBe(2);
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(null);
mouse.down();
let editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
editor.blur();
mouse.doubleClickAt(
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
expect(h.elements.length).toBe(3);
expect(h.elements.length).toBe(2);
text = h.elements[1] as ExcalidrawTextElementWithContainer;
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
mouse.down();
editor = document.querySelector(
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, { target: { value: "Hello World!" } });
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
@@ -571,43 +551,6 @@ describe("textWysiwyg", () => {
]);
});
it("should bind text to container when double clicked on container stroke", async () => {
const rectangle = API.createElement({
type: "rectangle",
x: 10,
y: 20,
width: 90,
height: 75,
strokeWidth: 4,
});
h.elements = [rectangle];
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(rectangle.id);
mouse.doubleClickAt(rectangle.x + 2, rectangle.y + 2);
expect(h.elements.length).toBe(2);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
mouse.down();
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, { target: { value: "Hello World!" } });
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
});
it("shouldn't bind to non-text-bindable containers", async () => {
const freedraw = API.createElement({
type: "freedraw",

View File

@@ -137,7 +137,7 @@ export const isExcalidrawElement = (element: any): boolean => {
export const hasBoundTextElement = (
element: ExcalidrawElement | null,
): element is MarkNonNullable<ExcalidrawBindableElement, "boundElements"> => {
): element is ExcalidrawBindableElement => {
return (
isBindableElement(element) &&
!!element.boundElements?.some(({ type }) => type === "text")

View File

@@ -18,7 +18,7 @@ Please add the latest change on the top under the correct section.
- [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) API now takes an optional parameter `opts` which currently supports the below attributes
```js
{ refreshDimensions?: boolean, repairBindings?: boolean }
{ refreshDimensions?: boolean, repair?: boolean }
```
The same `opts` param has been added to [`restore`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restore) API as well.