Skip to content

Commit

Permalink
comments pin object and rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
Vivek Patel committed May 6, 2022
1 parent 1ed1529 commit 9c27a5f
Show file tree
Hide file tree
Showing 20 changed files with 300 additions and 47 deletions.
6 changes: 4 additions & 2 deletions src/actions/actionProperties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
} from "../element/textElement";
import {
isBoundToContainer,
isCommentElement,
isLinearElement,
isLinearElementType,
} from "../element/typeChecks";
Expand Down Expand Up @@ -92,8 +93,9 @@ const changeProperty = (
);
return elements.map((element) => {
if (
selectedElementIds.get(element.id) ||
element.id === appState.editingElement?.id
!isCommentElement(element) &&
(selectedElementIds.get(element.id) ||
element.id === appState.editingElement?.id)
) {
return callback(element);
}
Expand Down
2 changes: 2 additions & 0 deletions src/appState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const getDefaultAppState = (): Omit<
viewModeEnabled: false,
pendingImageElement: null,
showHyperlinkPopup: false,
activeComment: null,
};
};

Expand Down Expand Up @@ -178,6 +179,7 @@ const APP_STATE_STORAGE_CONF = (<
viewModeEnabled: { browser: false, export: false, server: false },
pendingImageElement: { browser: false, export: false, server: false },
showHyperlinkPopup: { browser: false, export: false, server: false },
activeComment: { browser: false, export: false, server: false },
});

const _clearAppStateForStorage = <
Expand Down
3 changes: 3 additions & 0 deletions src/components/Actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ export const ShapesSwitcher = ({
}) => (
<>
{SHAPES.map(({ value, icon, key }, index) => {
if (value === "comment") {
return null;
}
const label = t(`toolBar.${value}`);
const letter = key && (typeof key === "string" ? key : key[0]);
const shortcut = letter
Expand Down
40 changes: 39 additions & 1 deletion src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,17 @@ import {
} from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
import { mutateElement, newElementWith } from "../element/mutateElement";
import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
import {
deepCopyElement,
newCommentElement,
newFreeDrawElement,
} from "../element/newElement";
import {
hasBoundTextElement,
isBindingElement,
isBindingElementType,
isBoundToContainer,
isCommentElement,
isImageElement,
isInitializedImageElement,
isLinearElement,
Expand All @@ -144,6 +149,7 @@ import {
FileId,
NonDeletedExcalidrawElement,
ExcalidrawTextContainer,
ExcalidrawCommentElement,
} from "../element/types";
import { getCenter, getDistance } from "../gesture";
import {
Expand Down Expand Up @@ -3057,6 +3063,11 @@ class App extends React.Component<AppProps, AppState> {
this.state.activeTool.type,
pointerDownState,
);
} else if (this.state.activeTool.type === "comment") {
this.handleCommentElementOnPointerDown(
this.state.activeTool.type,
pointerDownState,
);
} else if (this.state.activeTool.type !== "eraser") {
this.createGenericElementOnPointerDown(
this.state.activeTool.type,
Expand Down Expand Up @@ -3557,10 +3568,14 @@ class App extends React.Component<AppProps, AppState> {
!someHitElementIsSelected &&
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
) {
const activeComment: ExcalidrawCommentElement | null =
isCommentElement(hitElement) ? hitElement : null;

this.setState((prevState) => {
return selectGroupsForSelectedElements(
{
...prevState,
activeComment,
selectedElementIds: {
...prevState.selectedElementIds,
[hitElement.id]: true,
Expand Down Expand Up @@ -3606,6 +3621,28 @@ class App extends React.Component<AppProps, AppState> {
);
}

private handleCommentElementOnPointerDown = (
elementType: "comment",
pointerDownState: PointerDownState,
): void => {
const [gridX, gridY] = getGridPoint(
pointerDownState.origin.x,
pointerDownState.origin.y,
this.state.gridSize,
);

const element = newCommentElement({
type: elementType,
x: gridX,
y: gridY,
});

this.scene.replaceAllElements([
...this.scene.getElementsIncludingDeleted(),
element,
]);
};

private handleTextOnPointerDown = (
event: React.PointerEvent<HTMLCanvasElement>,
pointerDownState: PointerDownState,
Expand Down Expand Up @@ -5192,6 +5229,7 @@ class App extends React.Component<AppProps, AppState> {
isElementInGroup(hitElement, prevState.editingGroupId)
? prevState.editingGroupId
: null,
activeComment: null,
}));
this.setState({
selectedElementIds: {},
Expand Down
50 changes: 50 additions & 0 deletions src/components/CommentButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from "react";
import clsx from "clsx";
import { AppState } from "../types";

const COMMENT_ICON = (
<svg
enableBackground="new 0 0 512 512"
height="24px"
id="Layer_1"
version="1.1"
viewBox="0 0 512 512"
width="24px"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M170.476,166.19h155.097c4.285,0,7.76-3.469,7.76-7.754s-3.475-7.765-7.76-7.765H170.476c-4.285,0-7.754,3.48-7.754,7.765 S166.191,166.19,170.476,166.19z" />
<path d="M348.088,203.362H202.74c-4.284,0-7.759,3.469-7.759,7.754s3.475,7.765,7.759,7.765h145.348c4.284,0,7.754-3.48,7.754-7.765 S352.372,203.362,348.088,203.362z" />
<path d="M306.695,256.052H170.476c-4.285,0-7.754,3.469-7.754,7.754c0,4.284,3.469,7.754,7.754,7.754h136.219 c4.279,0,7.754-3.47,7.754-7.754C314.448,259.521,310.974,256.052,306.695,256.052z" />
<path d="M396.776,86.288H115.225c-29.992,0-54.403,22.562-54.403,50.308v154.83c0,27.735,24.411,50.297,54.403,50.297h166.034 l119.812,83.989v-84.135c27.996-2.038,50.108-23.753,50.108-50.151v-154.83C451.179,108.85,426.768,86.288,396.776,86.288z M427.906,291.426c0,14.902-13.972,27.025-31.131,27.025h-18.978v62.523l-89.193-62.523h-173.38 c-17.164,0-31.131-12.123-31.131-27.025v-154.83c0-14.913,13.967-27.035,31.131-27.035h281.551 c17.159,0,31.131,12.123,31.131,27.035V291.426z" />
</svg>
);

export const CommentButton: React.FC<{
appState: AppState;
addComment: () => void;
isMobile?: boolean;
}> = ({ appState, addComment, isMobile }) => {
return (
<label
className={clsx(
"ToolIcon ToolIcon_type_floating ToolIcon__library",
`ToolIcon_size_medium`,
{
"is-mobile": isMobile,
},
)}
title={`Add Comment - c`}
>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
name="canvas-comment"
onChange={addComment}
checked={appState.activeTool.type === "comment"}
aria-label={"Add Comment"}
aria-keyshortcuts="c"
/>
<div className="ToolIcon__icon">{COMMENT_ICON}</div>
</label>
);
};
25 changes: 24 additions & 1 deletion src/components/LayerUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Language, t } from "../i18n";
import { calculateScrollCenter, getSelectedElements } from "../scene";
import { ExportType } from "../scene/types";
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
import { muteFSAbortError } from "../utils";
import { muteFSAbortError, setCursorForShape } from "../utils";
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
import CollabButton from "./CollabButton";
Expand Down Expand Up @@ -38,6 +38,7 @@ import "./Toolbar.scss";
import { PenModeButton } from "./PenModeButton";
import { trackEvent } from "../analytics";
import { useDeviceType } from "../components/App";
import { CommentButton } from "./CommentButton";

interface LayerUIProps {
actionManager: ActionManager;
Expand Down Expand Up @@ -290,6 +291,24 @@ const LayerUI = ({
/>
) : null;

const addComment = () => {
const nextActiveTool: AppState["activeTool"] = {
type: "comment",
lastActiveToolBeforeEraser:
appState.activeTool.lastActiveToolBeforeEraser,
locked: appState.activeTool.locked,
};
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
});
setCursorForShape(canvas, {
...appState,
activeTool: nextActiveTool,
});
};

const renderFixedSideContainer = () => {
const shouldRenderSelectedShapeActions = showSelectedShapeActions(
appState,
Expand Down Expand Up @@ -361,6 +380,10 @@ const LayerUI = ({
appState={appState}
setAppState={setAppState}
/>
<CommentButton
addComment={addComment}
appState={appState}
/>
</Stack.Row>
{libraryMenu}
</Stack.Col>
Expand Down
3 changes: 3 additions & 0 deletions src/data/restore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const AllowedExcalidrawActiveTools: Record<
arrow: true,
freedraw: true,
eraser: false,
comment: false,
};

export type RestoredDataState = {
Expand Down Expand Up @@ -205,6 +206,8 @@ const restoreElement = (
return restoreElementWithProperties(element, {});
case "diamond":
return restoreElementWithProperties(element, {});
case "comment":
return restoreElementWithProperties(element, {});

// Don't use default case so as to catch a missing an element type case.
// We also don't want to throw, but instead return void so we filter
Expand Down
25 changes: 21 additions & 4 deletions src/element/collision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
ExcalidrawFreeDrawElement,
ExcalidrawImageElement,
ExcalidrawLinearElement,
ExcalidrawCommentElement,
} from "./types";

import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
Expand Down Expand Up @@ -183,6 +184,12 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
case "arrow":
case "line":
return hitTestLinear(args);
case "comment":
const distance2 = distanceToBindableElement(
args.element as ExcalidrawBindableElement,
args.point,
);
return args.check(distance2, args.threshold);
case "selection":
console.warn(
"This should not happen, we need to investigate why it does.",
Expand All @@ -204,6 +211,8 @@ export const distanceToBindableElement = (
return distanceToDiamond(element, point);
case "ellipse":
return distanceToEllipse(element, point);
case "comment":
return distanceToEllipse(element, point);
}
};

Expand Down Expand Up @@ -248,15 +257,15 @@ const distanceToDiamond = (
};

const distanceToEllipse = (
element: ExcalidrawEllipseElement,
element: ExcalidrawEllipseElement | ExcalidrawCommentElement,
point: Point,
): number => {
const [pointRel, tangent] = ellipseParamsForTest(element, point);
return -GALine.sign(tangent) * GAPoint.distanceToLine(pointRel, tangent);
};

const ellipseParamsForTest = (
element: ExcalidrawEllipseElement,
element: ExcalidrawEllipseElement | ExcalidrawCommentElement,
point: Point,
): [GA.Point, GA.Line] => {
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
Expand Down Expand Up @@ -509,6 +518,8 @@ export const determineFocusDistance = (
return mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
case "ellipse":
return c / (hwidth * Math.sqrt(n ** 2 + q ** 2 * m ** 2));
case "comment":
return c / (hwidth * Math.sqrt(n ** 2 + q ** 2 * m ** 2));
}
};

Expand Down Expand Up @@ -541,6 +552,9 @@ export const determineFocusPoint = (
case "ellipse":
point = findFocusPointForEllipse(element, focus, adjecentPointRel);
break;
case "comment":
point = findFocusPointForEllipse(element, focus, adjecentPointRel);
break;
}
return GAPoint.toTuple(GATransform.apply(reverseRelateToCenter, point));
};
Expand Down Expand Up @@ -599,6 +613,9 @@ const getSortedElementLineIntersections = (
case "ellipse":
intersections = getEllipseIntersections(element, gap, line);
break;
case "comment":
intersections = getEllipseIntersections(element, gap, line);
break;
}
if (intersections.length < 2) {
// Ignore the "edge" case of only intersecting with a single corner
Expand Down Expand Up @@ -674,7 +691,7 @@ const offsetSegment = (
};

const getEllipseIntersections = (
element: ExcalidrawEllipseElement,
element: ExcalidrawEllipseElement | ExcalidrawCommentElement,
gap: number,
line: GA.Line,
): GA.Point[] => {
Expand Down Expand Up @@ -734,7 +751,7 @@ export const getCircleIntersections = (
// The focus point is the tangent point of the "focus image" of the
// `element`, where the tangent goes through `point`.
export const findFocusPointForEllipse = (
ellipse: ExcalidrawEllipseElement,
ellipse: ExcalidrawEllipseElement | ExcalidrawCommentElement,
// Between -1 and 1 (not 0) the relative size of the "focus image" of
// the element on which the focus point lies
relativeDistance: number,
Expand Down
28 changes: 28 additions & 0 deletions src/element/newElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ExcalidrawFreeDrawElement,
FontFamilyValues,
ExcalidrawRectangleElement,
ExcalidrawCommentElement,
} from "../element/types";
import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils";
import { randomInteger, randomId } from "../random";
Expand Down Expand Up @@ -118,6 +119,33 @@ const getTextElementPositionOffsets = (
};
};

export const newCommentElement = (opts: {
type: "comment";
x: number;
y: number;
}): NonDeleted<ExcalidrawCommentElement> => {
const height = 40;
const width = 40;
return {
..._newElementBase<ExcalidrawCommentElement>(opts.type, {
x: opts.x - width / 2,
y: opts.y - height / 2,
locked: false,
height,
width,
fillStyle: "solid",
strokeWidth: 4,
strokeStyle: "solid",
angle: 0,
opacity: 100,
strokeColor: "#495057",
backgroundColor: "#e64980",
strokeSharpness: "sharp",
roughness: 0,
}),
};
};

export const newTextElement = (
opts: {
text: string;
Expand Down
Loading

0 comments on commit 9c27a5f

Please sign in to comment.