parent
5252726307
commit
61e5b66dac
@ -12,15 +12,21 @@ import { getShortcutKey } from "../utils";
|
|||||||
export const actionDuplicateSelection = register({
|
export const actionDuplicateSelection = register({
|
||||||
name: "duplicateSelection",
|
name: "duplicateSelection",
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
|
const groupIdMap = new Map();
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: elements.reduce(
|
elements: elements.reduce(
|
||||||
(acc: Array<ExcalidrawElement>, element: ExcalidrawElement) => {
|
(acc: Array<ExcalidrawElement>, element: ExcalidrawElement) => {
|
||||||
if (appState.selectedElementIds[element.id]) {
|
if (appState.selectedElementIds[element.id]) {
|
||||||
const newElement = duplicateElement(element, {
|
const newElement = duplicateElement(
|
||||||
x: element.x + 10,
|
appState.editingGroupId,
|
||||||
y: element.y + 10,
|
groupIdMap,
|
||||||
});
|
element,
|
||||||
|
{
|
||||||
|
x: element.x + 10,
|
||||||
|
y: element.y + 10,
|
||||||
|
},
|
||||||
|
);
|
||||||
appState.selectedElementIds[newElement.id] = true;
|
appState.selectedElementIds[newElement.id] = true;
|
||||||
delete appState.selectedElementIds[element.id];
|
delete appState.selectedElementIds[element.id];
|
||||||
return acc.concat([element, newElement]);
|
return acc.concat([element, newElement]);
|
||||||
|
119
src/actions/actionGroup.ts
Normal file
119
src/actions/actionGroup.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { KEYS } from "../keys";
|
||||||
|
import { register } from "./register";
|
||||||
|
import nanoid from "nanoid";
|
||||||
|
import { newElementWith } from "../element/mutateElement";
|
||||||
|
import { getSelectedElements } from "../scene";
|
||||||
|
import {
|
||||||
|
getSelectedGroupIds,
|
||||||
|
selectGroup,
|
||||||
|
selectGroupsForSelectedElements,
|
||||||
|
getElementsInGroup,
|
||||||
|
addToGroup,
|
||||||
|
removeFromSelectedGroups,
|
||||||
|
} from "../groups";
|
||||||
|
import { getNonDeletedElements } from "../element";
|
||||||
|
|
||||||
|
export const actionGroup = register({
|
||||||
|
name: "group",
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
if (selectedElements.length < 2) {
|
||||||
|
// nothing to group
|
||||||
|
return { appState, elements, commitToHistory: false };
|
||||||
|
}
|
||||||
|
// if everything is already grouped into 1 group, there is nothing to do
|
||||||
|
const selectedGroupIds = getSelectedGroupIds(appState);
|
||||||
|
if (selectedGroupIds.length === 1) {
|
||||||
|
const selectedGroupId = selectedGroupIds[0];
|
||||||
|
const elementIdsInGroup = new Set(
|
||||||
|
getElementsInGroup(elements, selectedGroupId).map(
|
||||||
|
(element) => element.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const selectedElementIds = new Set(
|
||||||
|
selectedElements.map((element) => element.id),
|
||||||
|
);
|
||||||
|
const combinedSet = new Set([
|
||||||
|
...Array.from(elementIdsInGroup),
|
||||||
|
...Array.from(selectedElementIds),
|
||||||
|
]);
|
||||||
|
if (combinedSet.size === elementIdsInGroup.size) {
|
||||||
|
// no incremental ids in the selected ids
|
||||||
|
return { appState, elements, commitToHistory: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newGroupId = nanoid();
|
||||||
|
const updatedElements = elements.map((element) => {
|
||||||
|
if (!appState.selectedElementIds[element.id]) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
return newElementWith(element, {
|
||||||
|
groupIds: addToGroup(
|
||||||
|
element.groupIds,
|
||||||
|
newGroupId,
|
||||||
|
appState.editingGroupId,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
appState: selectGroup(
|
||||||
|
newGroupId,
|
||||||
|
{ ...appState, selectedGroupIds: {} },
|
||||||
|
getNonDeletedElements(updatedElements),
|
||||||
|
),
|
||||||
|
elements: updatedElements,
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contextMenuOrder: 4,
|
||||||
|
contextItemLabel: "labels.group",
|
||||||
|
keyTest: (event) => {
|
||||||
|
return (
|
||||||
|
!event.shiftKey &&
|
||||||
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
event.keyCode === KEYS.G_KEY_CODE
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionUngroup = register({
|
||||||
|
name: "ungroup",
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
const groupIds = getSelectedGroupIds(appState);
|
||||||
|
if (groupIds.length === 0) {
|
||||||
|
return { appState, elements, commitToHistory: false };
|
||||||
|
}
|
||||||
|
const nextElements = elements.map((element) => {
|
||||||
|
const nextGroupIds = removeFromSelectedGroups(
|
||||||
|
element.groupIds,
|
||||||
|
appState.selectedGroupIds,
|
||||||
|
);
|
||||||
|
if (nextGroupIds.length === element.groupIds.length) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
return newElementWith(element, {
|
||||||
|
groupIds: nextGroupIds,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
appState: selectGroupsForSelectedElements(
|
||||||
|
{ ...appState, selectedGroupIds: {} },
|
||||||
|
getNonDeletedElements(nextElements),
|
||||||
|
),
|
||||||
|
elements: nextElements,
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
keyTest: (event) => {
|
||||||
|
return (
|
||||||
|
event.shiftKey &&
|
||||||
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
event.keyCode === KEYS.G_KEY_CODE
|
||||||
|
);
|
||||||
|
},
|
||||||
|
contextMenuOrder: 5,
|
||||||
|
contextItemLabel: "labels.ungroup",
|
||||||
|
});
|
@ -1,19 +1,25 @@
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
import { selectGroupsForSelectedElements } from "../groups";
|
||||||
|
import { getNonDeletedElements } from "../element";
|
||||||
|
|
||||||
export const actionSelectAll = register({
|
export const actionSelectAll = register({
|
||||||
name: "selectAll",
|
name: "selectAll",
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: selectGroupsForSelectedElements(
|
||||||
...appState,
|
{
|
||||||
selectedElementIds: elements.reduce((map, element) => {
|
...appState,
|
||||||
if (!element.isDeleted) {
|
editingGroupId: null,
|
||||||
map[element.id] = true;
|
selectedElementIds: elements.reduce((map, element) => {
|
||||||
}
|
if (!element.isDeleted) {
|
||||||
return map;
|
map[element.id] = true;
|
||||||
}, {} as any),
|
}
|
||||||
},
|
return map;
|
||||||
|
}, {} as any),
|
||||||
|
},
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -44,3 +44,5 @@ export {
|
|||||||
actionFullScreen,
|
actionFullScreen,
|
||||||
actionShortcuts,
|
actionShortcuts,
|
||||||
} from "./actionMenu";
|
} from "./actionMenu";
|
||||||
|
|
||||||
|
export { actionGroup, actionUngroup } from "./actionGroup";
|
||||||
|
@ -55,7 +55,9 @@ export type ActionName =
|
|||||||
| "changeFontFamily"
|
| "changeFontFamily"
|
||||||
| "changeTextAlign"
|
| "changeTextAlign"
|
||||||
| "toggleFullScreen"
|
| "toggleFullScreen"
|
||||||
| "toggleShortcuts";
|
| "toggleShortcuts"
|
||||||
|
| "group"
|
||||||
|
| "ungroup";
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: ActionName;
|
name: ActionName;
|
||||||
|
@ -48,6 +48,8 @@ export const getDefaultAppState = (): AppState => {
|
|||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
showShortcutsDialog: false,
|
showShortcutsDialog: false,
|
||||||
zenModeEnabled: false,
|
zenModeEnabled: false,
|
||||||
|
editingGroupId: null,
|
||||||
|
selectedGroupIds: {},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -131,6 +131,12 @@ import {
|
|||||||
} from "../data/localStorage";
|
} from "../data/localStorage";
|
||||||
|
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
|
import {
|
||||||
|
getSelectedGroupIds,
|
||||||
|
selectGroupsForSelectedElements,
|
||||||
|
isElementInGroup,
|
||||||
|
getSelectedGroupIdForElement,
|
||||||
|
} from "../groups";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param func handler taking at most single parameter (event).
|
* @param func handler taking at most single parameter (event).
|
||||||
@ -704,9 +710,10 @@ class App extends React.Component<any, AppState> {
|
|||||||
|
|
||||||
const dx = x - elementsCenterX;
|
const dx = x - elementsCenterX;
|
||||||
const dy = y - elementsCenterY;
|
const dy = y - elementsCenterY;
|
||||||
|
const groupIdMap = new Map();
|
||||||
|
|
||||||
const newElements = clipboardElements.map((element) =>
|
const newElements = clipboardElements.map((element) =>
|
||||||
duplicateElement(element, {
|
duplicateElement(this.state.editingGroupId, groupIdMap, element, {
|
||||||
x: element.x + dx - minX,
|
x: element.x + dx - minX,
|
||||||
y: element.y + dy - minY,
|
y: element.y + dy - minY,
|
||||||
}),
|
}),
|
||||||
@ -1212,7 +1219,11 @@ class App extends React.Component<any, AppState> {
|
|||||||
resetCursor();
|
resetCursor();
|
||||||
} else {
|
} else {
|
||||||
setCursorForShape(this.state.elementType);
|
setCursorForShape(this.state.elementType);
|
||||||
this.setState({ selectedElementIds: {} });
|
this.setState({
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
editingGroupId: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
isHoldingSpace = false;
|
isHoldingSpace = false;
|
||||||
}
|
}
|
||||||
@ -1226,7 +1237,12 @@ class App extends React.Component<any, AppState> {
|
|||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
}
|
}
|
||||||
if (elementType !== "selection") {
|
if (elementType !== "selection") {
|
||||||
this.setState({ elementType, selectedElementIds: {} });
|
this.setState({
|
||||||
|
elementType,
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
editingGroupId: null,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({ elementType });
|
this.setState({ elementType });
|
||||||
}
|
}
|
||||||
@ -1337,7 +1353,11 @@ class App extends React.Component<any, AppState> {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
// deselect all other elements when inserting text
|
// deselect all other elements when inserting text
|
||||||
this.setState({ selectedElementIds: {} });
|
this.setState({
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
editingGroupId: null,
|
||||||
|
});
|
||||||
|
|
||||||
// do an initial update to re-initialize element position since we were
|
// do an initial update to re-initialize element position since we were
|
||||||
// modifying element's x/y for sake of editor (case: syncing to remote)
|
// modifying element's x/y for sake of editor (case: syncing to remote)
|
||||||
@ -1459,8 +1479,6 @@ class App extends React.Component<any, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetCursor();
|
|
||||||
|
|
||||||
const { x, y } = viewportCoordsToSceneCoords(
|
const { x, y } = viewportCoordsToSceneCoords(
|
||||||
event,
|
event,
|
||||||
this.state,
|
this.state,
|
||||||
@ -1468,6 +1486,40 @@ class App extends React.Component<any, AppState> {
|
|||||||
window.devicePixelRatio,
|
window.devicePixelRatio,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const selectedGroupIds = getSelectedGroupIds(this.state);
|
||||||
|
|
||||||
|
if (selectedGroupIds.length > 0) {
|
||||||
|
const elements = globalSceneState.getElements();
|
||||||
|
const hitElement = getElementAtPosition(
|
||||||
|
elements,
|
||||||
|
this.state,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
this.state.zoom,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedGroupId =
|
||||||
|
hitElement &&
|
||||||
|
getSelectedGroupIdForElement(hitElement, this.state.selectedGroupIds);
|
||||||
|
|
||||||
|
if (selectedGroupId) {
|
||||||
|
this.setState((prevState) =>
|
||||||
|
selectGroupsForSelectedElements(
|
||||||
|
{
|
||||||
|
...prevState,
|
||||||
|
editingGroupId: selectedGroupId,
|
||||||
|
selectedElementIds: { [hitElement!.id]: true },
|
||||||
|
selectedGroupIds: {},
|
||||||
|
},
|
||||||
|
globalSceneState.getElements(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetCursor();
|
||||||
|
|
||||||
this.startTextEditing({
|
this.startTextEditing({
|
||||||
x: x,
|
x: x,
|
||||||
y: y,
|
y: y,
|
||||||
@ -1942,7 +1994,16 @@ class App extends React.Component<any, AppState> {
|
|||||||
!(hitElement && this.state.selectedElementIds[hitElement.id]) &&
|
!(hitElement && this.state.selectedElementIds[hitElement.id]) &&
|
||||||
!event.shiftKey
|
!event.shiftKey
|
||||||
) {
|
) {
|
||||||
this.setState({ selectedElementIds: {} });
|
this.setState((prevState) => ({
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
editingGroupId:
|
||||||
|
prevState.editingGroupId &&
|
||||||
|
hitElement &&
|
||||||
|
isElementInGroup(hitElement, prevState.editingGroupId)
|
||||||
|
? prevState.editingGroupId
|
||||||
|
: null,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we click on something
|
// If we click on something
|
||||||
@ -1952,12 +2013,32 @@ class App extends React.Component<any, AppState> {
|
|||||||
// otherwise, it will trigger selection based on current
|
// otherwise, it will trigger selection based on current
|
||||||
// state of the box
|
// state of the box
|
||||||
if (!this.state.selectedElementIds[hitElement.id]) {
|
if (!this.state.selectedElementIds[hitElement.id]) {
|
||||||
this.setState((prevState) => ({
|
// if we are currently editing a group, treat all selections outside of the group
|
||||||
selectedElementIds: {
|
// as exiting editing mode.
|
||||||
...prevState.selectedElementIds,
|
if (
|
||||||
[hitElement!.id]: true,
|
this.state.editingGroupId &&
|
||||||
},
|
!isElementInGroup(hitElement, this.state.editingGroupId)
|
||||||
}));
|
) {
|
||||||
|
this.setState({
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
editingGroupId: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState((prevState) => {
|
||||||
|
return selectGroupsForSelectedElements(
|
||||||
|
{
|
||||||
|
...prevState,
|
||||||
|
selectedElementIds: {
|
||||||
|
...prevState.selectedElementIds,
|
||||||
|
[hitElement!.id]: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
globalSceneState.getElements(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// TODO: this is strange...
|
||||||
globalSceneState.replaceAllElements(
|
globalSceneState.replaceAllElements(
|
||||||
globalSceneState.getElementsIncludingDeleted(),
|
globalSceneState.getElementsIncludingDeleted(),
|
||||||
);
|
);
|
||||||
@ -1966,7 +2047,11 @@ class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setState({ selectedElementIds: {} });
|
this.setState({
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
editingGroupId: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.elementType === "text") {
|
if (this.state.elementType === "text") {
|
||||||
@ -2218,6 +2303,7 @@ class App extends React.Component<any, AppState> {
|
|||||||
|
|
||||||
const nextElements = [];
|
const nextElements = [];
|
||||||
const elementsToAppend = [];
|
const elementsToAppend = [];
|
||||||
|
const groupIdMap = new Map();
|
||||||
for (const element of globalSceneState.getElementsIncludingDeleted()) {
|
for (const element of globalSceneState.getElementsIncludingDeleted()) {
|
||||||
if (
|
if (
|
||||||
this.state.selectedElementIds[element.id] ||
|
this.state.selectedElementIds[element.id] ||
|
||||||
@ -2225,7 +2311,11 @@ class App extends React.Component<any, AppState> {
|
|||||||
// updated yet by the time this mousemove event is fired
|
// updated yet by the time this mousemove event is fired
|
||||||
(element.id === hitElement.id && hitElementWasAddedToSelection)
|
(element.id === hitElement.id && hitElementWasAddedToSelection)
|
||||||
) {
|
) {
|
||||||
const duplicatedElement = duplicateElement(element);
|
const duplicatedElement = duplicateElement(
|
||||||
|
this.state.editingGroupId,
|
||||||
|
groupIdMap,
|
||||||
|
element,
|
||||||
|
);
|
||||||
mutateElement(duplicatedElement, {
|
mutateElement(duplicatedElement, {
|
||||||
x: duplicatedElement.x + (originX - lastX),
|
x: duplicatedElement.x + (originX - lastX),
|
||||||
y: duplicatedElement.y + (originY - lastY),
|
y: duplicatedElement.y + (originY - lastY),
|
||||||
@ -2316,21 +2406,31 @@ class App extends React.Component<any, AppState> {
|
|||||||
if (this.state.elementType === "selection") {
|
if (this.state.elementType === "selection") {
|
||||||
const elements = globalSceneState.getElements();
|
const elements = globalSceneState.getElements();
|
||||||
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
|
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
|
||||||
this.setState({ selectedElementIds: {} });
|
this.setState({
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
editingGroupId: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const elementsWithinSelection = getElementsWithinSelection(
|
const elementsWithinSelection = getElementsWithinSelection(
|
||||||
elements,
|
elements,
|
||||||
draggingElement,
|
draggingElement,
|
||||||
);
|
);
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) =>
|
||||||
selectedElementIds: {
|
selectGroupsForSelectedElements(
|
||||||
...prevState.selectedElementIds,
|
{
|
||||||
...elementsWithinSelection.reduce((map, element) => {
|
...prevState,
|
||||||
map[element.id] = true;
|
selectedElementIds: {
|
||||||
return map;
|
...prevState.selectedElementIds,
|
||||||
}, {} as any),
|
...elementsWithinSelection.reduce((map, element) => {
|
||||||
},
|
map[element.id] = true;
|
||||||
}));
|
return map;
|
||||||
|
}, {} as any),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
globalSceneState.getElements(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2445,7 +2545,12 @@ class App extends React.Component<any, AppState> {
|
|||||||
// If click occurred and elements were dragged or some element
|
// If click occurred and elements were dragged or some element
|
||||||
// was added to selection (on pointerdown phase) we need to keep
|
// was added to selection (on pointerdown phase) we need to keep
|
||||||
// selection unchanged
|
// selection unchanged
|
||||||
if (hitElement && !draggingOccurred && !hitElementWasAddedToSelection) {
|
if (
|
||||||
|
getSelectedGroupIds(this.state).length === 0 &&
|
||||||
|
hitElement &&
|
||||||
|
!draggingOccurred &&
|
||||||
|
!hitElementWasAddedToSelection
|
||||||
|
) {
|
||||||
if (childEvent.shiftKey) {
|
if (childEvent.shiftKey) {
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
selectedElementIds: {
|
selectedElementIds: {
|
||||||
@ -2462,7 +2567,11 @@ class App extends React.Component<any, AppState> {
|
|||||||
|
|
||||||
if (draggingElement === null) {
|
if (draggingElement === null) {
|
||||||
// if no element is clicked, clear the selection and redraw
|
// if no element is clicked, clear the selection and redraw
|
||||||
this.setState({ selectedElementIds: {} });
|
this.setState({
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
editingGroupId: null,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,6 +318,14 @@ export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("buttons.redo")}
|
label={t("buttons.redo")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Z")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Z")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.group")}
|
||||||
|
shortcuts={[getShortcutKey("CtrlOrCmd+G")]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.ungroup")}
|
||||||
|
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
|
||||||
|
/>
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
</Column>
|
</Column>
|
||||||
</Columns>
|
</Columns>
|
||||||
|
@ -71,7 +71,8 @@ export const restore = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...element,
|
...element,
|
||||||
// all elements must have version > 0 so getDrawingVersion() will pick up newly added elements
|
// all elements must have version > 0 so getDrawingVersion() will pick
|
||||||
|
// up newly added elements
|
||||||
version: element.version || 1,
|
version: element.version || 1,
|
||||||
id: element.id || randomId(),
|
id: element.id || randomId(),
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
@ -84,6 +85,7 @@ export const restore = (
|
|||||||
? 100
|
? 100
|
||||||
: element.opacity,
|
: element.opacity,
|
||||||
angle: element.angle ?? 0,
|
angle: element.angle ?? 0,
|
||||||
|
groupIds: element.groupIds || [],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ it("clones arrow element", () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const copy = duplicateElement(element);
|
const copy = duplicateElement(null, new Map(), element);
|
||||||
|
|
||||||
assertCloneObjects(element, copy);
|
assertCloneObjects(element, copy);
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ it("clones text element", () => {
|
|||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
});
|
});
|
||||||
|
|
||||||
const copy = duplicateElement(element);
|
const copy = duplicateElement(null, new Map(), element);
|
||||||
|
|
||||||
assertCloneObjects(element, copy);
|
assertCloneObjects(element, copy);
|
||||||
|
|
||||||
|
@ -5,10 +5,13 @@ import {
|
|||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
TextAlign,
|
TextAlign,
|
||||||
|
GroupId,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { measureText } from "../utils";
|
import { measureText } from "../utils";
|
||||||
import { randomInteger, randomId } from "../random";
|
import { randomInteger, randomId } from "../random";
|
||||||
import { newElementWith } from "./mutateElement";
|
import { newElementWith } from "./mutateElement";
|
||||||
|
import nanoid from "nanoid";
|
||||||
|
import { getNewGroupIdsForDuplication } from "../groups";
|
||||||
|
|
||||||
type ElementConstructorOpts = {
|
type ElementConstructorOpts = {
|
||||||
x: ExcalidrawGenericElement["x"];
|
x: ExcalidrawGenericElement["x"];
|
||||||
@ -61,6 +64,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
|||||||
version: rest.version || 1,
|
version: rest.version || 1,
|
||||||
versionNonce: rest.versionNonce ?? 0,
|
versionNonce: rest.versionNonce ?? 0,
|
||||||
isDeleted: false as false,
|
isDeleted: false as false,
|
||||||
|
groupIds: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const newElement = (
|
export const newElement = (
|
||||||
@ -148,13 +152,39 @@ export const deepCopyElement = (val: any, depth: number = 0) => {
|
|||||||
return val;
|
return val;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate an element, often used in the alt-drag operation.
|
||||||
|
* Note that this method has gotten a bit complicated since the
|
||||||
|
* introduction of gruoping/ungrouping elements.
|
||||||
|
* @param editingGroupId The current group being edited. The new
|
||||||
|
* element will inherit this group and its
|
||||||
|
* parents.
|
||||||
|
* @param groupIdMapForOperation A Map that maps old group IDs to
|
||||||
|
* duplicated ones. If you are duplicating
|
||||||
|
* multiple elements at once, share this map
|
||||||
|
* amongst all of them
|
||||||
|
* @param element Element to duplicate
|
||||||
|
* @param overrides Any element properties to override
|
||||||
|
*/
|
||||||
export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
|
editingGroupId: GroupId | null,
|
||||||
|
groupIdMapForOperation: Map<GroupId, GroupId>,
|
||||||
element: TElement,
|
element: TElement,
|
||||||
overrides?: Partial<TElement>,
|
overrides?: Partial<TElement>,
|
||||||
): TElement => {
|
): TElement => {
|
||||||
let copy: TElement = deepCopyElement(element);
|
let copy: TElement = deepCopyElement(element);
|
||||||
copy.id = randomId();
|
copy.id = randomId();
|
||||||
copy.seed = randomInteger();
|
copy.seed = randomInteger();
|
||||||
|
copy.groupIds = getNewGroupIdsForDuplication(
|
||||||
|
copy.groupIds,
|
||||||
|
editingGroupId,
|
||||||
|
(groupId) => {
|
||||||
|
if (!groupIdMapForOperation.has(groupId)) {
|
||||||
|
groupIdMapForOperation.set(groupId, nanoid());
|
||||||
|
}
|
||||||
|
return groupIdMapForOperation.get(groupId)!;
|
||||||
|
},
|
||||||
|
);
|
||||||
if (overrides) {
|
if (overrides) {
|
||||||
copy = Object.assign(copy, overrides);
|
copy = Object.assign(copy, overrides);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
|
|
||||||
|
export type GroupId = string;
|
||||||
|
|
||||||
type _ExcalidrawElementBase = Readonly<{
|
type _ExcalidrawElementBase = Readonly<{
|
||||||
id: string;
|
id: string;
|
||||||
x: number;
|
x: number;
|
||||||
@ -18,8 +20,12 @@ type _ExcalidrawElementBase = Readonly<{
|
|||||||
version: number;
|
version: number;
|
||||||
versionNonce: number;
|
versionNonce: number;
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
|
groupIds: GroupId[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are elements that don't have any additional properties.
|
||||||
|
*/
|
||||||
export type ExcalidrawGenericElement = _ExcalidrawElementBase & {
|
export type ExcalidrawGenericElement = _ExcalidrawElementBase & {
|
||||||
type: "selection" | "rectangle" | "diamond" | "ellipse";
|
type: "selection" | "rectangle" | "diamond" | "ellipse";
|
||||||
};
|
};
|
||||||
|
130
src/groups.ts
Normal file
130
src/groups.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
|
||||||
|
import { AppState } from "./types";
|
||||||
|
import { getSelectedElements } from "./scene";
|
||||||
|
|
||||||
|
export function selectGroup(
|
||||||
|
groupId: GroupId,
|
||||||
|
appState: AppState,
|
||||||
|
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||||
|
): AppState {
|
||||||
|
return {
|
||||||
|
...appState,
|
||||||
|
selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: true },
|
||||||
|
selectedElementIds: {
|
||||||
|
...appState.selectedElementIds,
|
||||||
|
...Object.fromEntries(
|
||||||
|
elements
|
||||||
|
.filter((element) => element.groupIds.includes(groupId))
|
||||||
|
.map((element) => [element.id, true]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the element's group is selected, don't render an individual
|
||||||
|
* selection border around it.
|
||||||
|
*/
|
||||||
|
export function isSelectedViaGroup(
|
||||||
|
appState: AppState,
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
) {
|
||||||
|
return !!element.groupIds
|
||||||
|
.filter((groupId) => groupId !== appState.editingGroupId)
|
||||||
|
.find((groupId) => appState.selectedGroupIds[groupId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSelectedGroupIds(appState: AppState): GroupId[] {
|
||||||
|
return Object.entries(appState.selectedGroupIds)
|
||||||
|
.filter(([groupId, isSelected]) => isSelected)
|
||||||
|
.map(([groupId, isSelected]) => groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When you select an element, you often want to actually select the whole group it's in, unless
|
||||||
|
* you're currently editing that group.
|
||||||
|
*/
|
||||||
|
export function selectGroupsForSelectedElements(
|
||||||
|
appState: AppState,
|
||||||
|
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||||
|
): AppState {
|
||||||
|
let nextAppState = { ...appState };
|
||||||
|
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
|
for (const selectedElement of selectedElements) {
|
||||||
|
let groupIds = selectedElement.groupIds;
|
||||||
|
if (appState.editingGroupId) {
|
||||||
|
// handle the case where a group is nested within a group
|
||||||
|
const indexOfEditingGroup = groupIds.indexOf(appState.editingGroupId);
|
||||||
|
if (indexOfEditingGroup > -1) {
|
||||||
|
groupIds = groupIds.slice(0, indexOfEditingGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (groupIds.length > 0) {
|
||||||
|
const groupId = groupIds[groupIds.length - 1];
|
||||||
|
nextAppState = selectGroup(groupId, nextAppState, elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextAppState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isElementInGroup(element: ExcalidrawElement, groupId: string) {
|
||||||
|
return element.groupIds.includes(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getElementsInGroup(
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
groupId: string,
|
||||||
|
) {
|
||||||
|
return elements.filter((element) => isElementInGroup(element, groupId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSelectedGroupIdForElement(
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
selectedGroupIds: { [groupId: string]: boolean },
|
||||||
|
) {
|
||||||
|
return element.groupIds.find((groupId) => selectedGroupIds[groupId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNewGroupIdsForDuplication(
|
||||||
|
groupIds: GroupId[],
|
||||||
|
editingGroupId: GroupId | null,
|
||||||
|
mapper: (groupId: GroupId) => GroupId,
|
||||||
|
) {
|
||||||
|
const copy = [...groupIds];
|
||||||
|
const positionOfEditingGroupId = editingGroupId
|
||||||
|
? groupIds.indexOf(editingGroupId)
|
||||||
|
: -1;
|
||||||
|
const endIndex =
|
||||||
|
positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
|
||||||
|
for (let i = 0; i < endIndex; i++) {
|
||||||
|
copy[i] = mapper(copy[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addToGroup(
|
||||||
|
prevGroupIds: GroupId[],
|
||||||
|
newGroupId: GroupId,
|
||||||
|
editingGroupId: GroupId | null,
|
||||||
|
) {
|
||||||
|
// insert before the editingGroupId, or push to the end.
|
||||||
|
const groupIds = [...prevGroupIds];
|
||||||
|
const positionOfEditingGroupId = editingGroupId
|
||||||
|
? groupIds.indexOf(editingGroupId)
|
||||||
|
: -1;
|
||||||
|
const positionToInsert =
|
||||||
|
positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
|
||||||
|
groupIds.splice(positionToInsert, 0, newGroupId);
|
||||||
|
return groupIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeFromSelectedGroups(
|
||||||
|
groupIds: GroupId[],
|
||||||
|
selectedGroupIds: { [groupId: string]: boolean },
|
||||||
|
) {
|
||||||
|
return groupIds.filter((groupId) => !selectedGroupIds[groupId]);
|
||||||
|
}
|
@ -16,6 +16,7 @@ export const KEYS = {
|
|||||||
F_KEY_CODE: 70,
|
F_KEY_CODE: 70,
|
||||||
ALT_KEY_CODE: 18,
|
ALT_KEY_CODE: 18,
|
||||||
Z_KEY_CODE: 90,
|
Z_KEY_CODE: 90,
|
||||||
|
G_KEY_CODE: 71,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type Key = keyof typeof KEYS;
|
export type Key = keyof typeof KEYS;
|
||||||
|
@ -59,7 +59,9 @@
|
|||||||
"untitled": "Untitled",
|
"untitled": "Untitled",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"yourName": "Your name",
|
"yourName": "Your name",
|
||||||
"madeWithExcalidraw": "Made with Excalidraw"
|
"madeWithExcalidraw": "Made with Excalidraw",
|
||||||
|
"group": "Group selection",
|
||||||
|
"ungroup": "Ungroup selection"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
|
@ -6,6 +6,7 @@ import { FlooredNumber, AppState } from "../types";
|
|||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
|
GroupId,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import {
|
import {
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
@ -27,6 +28,11 @@ import { getSelectedElements } from "../scene/selection";
|
|||||||
|
|
||||||
import { renderElement, renderElementToSvg } from "./renderElement";
|
import { renderElement, renderElementToSvg } from "./renderElement";
|
||||||
import colors from "../colors";
|
import colors from "../colors";
|
||||||
|
import {
|
||||||
|
isSelectedViaGroup,
|
||||||
|
getSelectedGroupIds,
|
||||||
|
getElementsInGroup,
|
||||||
|
} from "../groups";
|
||||||
|
|
||||||
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
|
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
|
||||||
|
|
||||||
@ -167,7 +173,10 @@ export const renderScene = (
|
|||||||
const selections = elements.reduce((acc, element) => {
|
const selections = elements.reduce((acc, element) => {
|
||||||
const selectionColors = [];
|
const selectionColors = [];
|
||||||
// local user
|
// local user
|
||||||
if (appState.selectedElementIds[element.id]) {
|
if (
|
||||||
|
appState.selectedElementIds[element.id] &&
|
||||||
|
!isSelectedViaGroup(appState, element)
|
||||||
|
) {
|
||||||
selectionColors.push(oc.black);
|
selectionColors.push(oc.black);
|
||||||
}
|
}
|
||||||
// remote users
|
// remote users
|
||||||
@ -180,57 +189,96 @@ export const renderScene = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (selectionColors.length) {
|
if (selectionColors.length) {
|
||||||
acc.push({ element, selectionColors });
|
const [
|
||||||
|
elementX1,
|
||||||
|
elementY1,
|
||||||
|
elementX2,
|
||||||
|
elementY2,
|
||||||
|
] = getElementAbsoluteCoords(element);
|
||||||
|
acc.push({
|
||||||
|
angle: element.angle,
|
||||||
|
elementX1,
|
||||||
|
elementY1,
|
||||||
|
elementX2,
|
||||||
|
elementY2,
|
||||||
|
selectionColors,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as { element: ExcalidrawElement; selectionColors: string[] }[]);
|
}, [] as { angle: number; elementX1: number; elementY1: number; elementX2: number; elementY2: number; selectionColors: string[] }[]);
|
||||||
|
|
||||||
selections.forEach(({ element, selectionColors }) => {
|
function addSelectionForGroupId(groupId: GroupId) {
|
||||||
const [
|
const groupElements = getElementsInGroup(elements, groupId);
|
||||||
|
const [elementX1, elementY1, elementX2, elementY2] = getCommonBounds(
|
||||||
|
groupElements,
|
||||||
|
);
|
||||||
|
selections.push({
|
||||||
|
angle: 0,
|
||||||
|
elementX1,
|
||||||
|
elementX2,
|
||||||
|
elementY1,
|
||||||
|
elementY2,
|
||||||
|
selectionColors: [oc.black],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const groupId of getSelectedGroupIds(appState)) {
|
||||||
|
// TODO: support multiplayer selected group IDs
|
||||||
|
addSelectionForGroupId(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appState.editingGroupId) {
|
||||||
|
addSelectionForGroupId(appState.editingGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
selections.forEach(
|
||||||
|
({
|
||||||
|
angle,
|
||||||
elementX1,
|
elementX1,
|
||||||
elementY1,
|
elementY1,
|
||||||
elementX2,
|
elementX2,
|
||||||
elementY2,
|
elementY2,
|
||||||
] = getElementAbsoluteCoords(element);
|
selectionColors,
|
||||||
|
}) => {
|
||||||
|
const elementWidth = elementX2 - elementX1;
|
||||||
|
const elementHeight = elementY2 - elementY1;
|
||||||
|
|
||||||
const elementWidth = elementX2 - elementX1;
|
const initialLineDash = context.getLineDash();
|
||||||
const elementHeight = elementY2 - elementY1;
|
const lineWidth = context.lineWidth;
|
||||||
|
const lineDashOffset = context.lineDashOffset;
|
||||||
|
const strokeStyle = context.strokeStyle;
|
||||||
|
|
||||||
const initialLineDash = context.getLineDash();
|
const dashedLinePadding = 4 / sceneState.zoom;
|
||||||
const lineWidth = context.lineWidth;
|
const dashWidth = 8 / sceneState.zoom;
|
||||||
const lineDashOffset = context.lineDashOffset;
|
const spaceWidth = 4 / sceneState.zoom;
|
||||||
const strokeStyle = context.strokeStyle;
|
|
||||||
|
|
||||||
const dashedLinePadding = 4 / sceneState.zoom;
|
context.lineWidth = 1 / sceneState.zoom;
|
||||||
const dashWidth = 8 / sceneState.zoom;
|
|
||||||
const spaceWidth = 4 / sceneState.zoom;
|
|
||||||
|
|
||||||
context.lineWidth = 1 / sceneState.zoom;
|
const count = selectionColors.length;
|
||||||
|
for (var i = 0; i < count; ++i) {
|
||||||
const count = selectionColors.length;
|
context.strokeStyle = selectionColors[i];
|
||||||
for (var i = 0; i < count; ++i) {
|
context.setLineDash([
|
||||||
context.strokeStyle = selectionColors[i];
|
dashWidth,
|
||||||
context.setLineDash([
|
spaceWidth + (dashWidth + spaceWidth) * (count - 1),
|
||||||
dashWidth,
|
]);
|
||||||
spaceWidth + (dashWidth + spaceWidth) * (count - 1),
|
context.lineDashOffset = (dashWidth + spaceWidth) * i;
|
||||||
]);
|
strokeRectWithRotation(
|
||||||
context.lineDashOffset = (dashWidth + spaceWidth) * i;
|
context,
|
||||||
strokeRectWithRotation(
|
elementX1 - dashedLinePadding,
|
||||||
context,
|
elementY1 - dashedLinePadding,
|
||||||
elementX1 - dashedLinePadding,
|
elementWidth + dashedLinePadding * 2,
|
||||||
elementY1 - dashedLinePadding,
|
elementHeight + dashedLinePadding * 2,
|
||||||
elementWidth + dashedLinePadding * 2,
|
elementX1 + elementWidth / 2,
|
||||||
elementHeight + dashedLinePadding * 2,
|
elementY1 + elementHeight / 2,
|
||||||
elementX1 + elementWidth / 2,
|
angle,
|
||||||
elementY1 + elementHeight / 2,
|
);
|
||||||
element.angle,
|
}
|
||||||
);
|
context.lineDashOffset = lineDashOffset;
|
||||||
}
|
context.strokeStyle = strokeStyle;
|
||||||
context.lineDashOffset = lineDashOffset;
|
context.lineWidth = lineWidth;
|
||||||
context.strokeStyle = strokeStyle;
|
context.setLineDash(initialLineDash);
|
||||||
context.lineWidth = lineWidth;
|
},
|
||||||
context.setLineDash(initialLineDash);
|
);
|
||||||
});
|
|
||||||
context.translate(-sceneState.scrollX, -sceneState.scrollY);
|
context.translate(-sceneState.scrollX, -sceneState.scrollY);
|
||||||
|
|
||||||
const locallySelectedElements = getSelectedElements(elements, appState);
|
const locallySelectedElements = getSelectedElements(elements, appState);
|
||||||
|
@ -7,6 +7,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -43,6 +44,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -68,6 +70,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -91,6 +94,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -127,6 +131,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
@ -5,6 +5,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id2",
|
"id": "id2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -28,6 +29,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -51,6 +53,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
@ -5,6 +5,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 110,
|
"height": 110,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -46,6 +47,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 110,
|
"height": 110,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -28,6 +29,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
@ -5,6 +5,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -39,6 +40,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -73,6 +75,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -96,6 +99,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -119,6 +123,7 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
NonDeleted,
|
NonDeleted,
|
||||||
TextAlign,
|
TextAlign,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
GroupId,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { SHAPES } from "./shapes";
|
import { SHAPES } from "./shapes";
|
||||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
@ -67,6 +68,10 @@ export type AppState = {
|
|||||||
shouldCacheIgnoreZoom: boolean;
|
shouldCacheIgnoreZoom: boolean;
|
||||||
showShortcutsDialog: boolean;
|
showShortcutsDialog: boolean;
|
||||||
zenModeEnabled: boolean;
|
zenModeEnabled: boolean;
|
||||||
|
|
||||||
|
// groups
|
||||||
|
selectedGroupIds: { [groupId: string]: boolean };
|
||||||
|
editingGroupId: GroupId | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerCoords = Readonly<{
|
export type PointerCoords = Readonly<{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user