feat: drag, resize, and rotate after selecting in lasso (#9732)

* feat: drag, resize, and rotate after selecting in lasso

* alternative ux: drag with lasso right away

* fix: lasso dragging should snap too

* fix: alt+cmd getting stuck

* test: snapshots

* alternatvie: keep lasso drag to only mobile

* alternative: drag after selection on PCs

* improve mobile dection

* add mobile lasso icon

* add default selection tool

* render according to default selection tool

* return to default selection tool after deletion

* reset to default tool after clearing out the canvas

* return to default tool after eraser toggle

* if default lasso, close lasso toggle

* finalize to default selection tool

* toggle between laser and default selection

* return to default selection tool after creation

* double click to add text when using default selection tool

* set to default selection tool after unlocking tool

* paste to center on touch screen

* switch to default selection tool after pasting

* lint

* fix tests

* show welcome screen when using default selection tool

* fix tests

* fix snapshots

* fix context menu not opening

* prevent potential displacement issue

* prevent element jumping during lasso selection

* fix dragging on mobile

* use same selection icon

* fix alt+cmd lasso getting cut off

* fix: shortcut handling

* lint

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Ryan Di
2025-08-20 00:03:02 +02:00
committed by GitHub
co-authored by dwelle
parent c6f8ef9ad2
commit b4903a7eab
10 changed files with 353 additions and 107 deletions
+72 -64
View File
@@ -46,7 +46,7 @@ import {
hasStrokeWidth,
} from "../scene";
import { SHAPES } from "./shapes";
import { getToolbarTools } from "./shapes";
import "./Actions.scss";
@@ -295,7 +295,8 @@ export const ShapesSwitcher = ({
const frameToolSelected = activeTool.type === "frame";
const laserToolSelected = activeTool.type === "laser";
const lassoToolSelected = activeTool.type === "lasso";
const lassoToolSelected =
activeTool.type === "lasso" && app.defaultSelectionTool !== "lasso";
const embeddableToolSelected = activeTool.type === "embeddable";
@@ -303,63 +304,68 @@ export const ShapesSwitcher = ({
return (
<>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
if (
UIOptions.tools?.[
value as Extract<typeof value, keyof AppProps["UIOptions"]["tools"]>
] === false
) {
return null;
}
{getToolbarTools(app).map(
({ value, icon, key, numericKey, fillable }, index) => {
if (
UIOptions.tools?.[
value as Extract<
typeof value,
keyof AppProps["UIOptions"]["tools"]
>
] === false
) {
return null;
}
const label = t(`toolBar.${value}`);
const letter =
key && capitalizeString(typeof key === "string" ? key : key[0]);
const shortcut = letter
? `${letter} ${t("helpDialog.or")} ${numericKey}`
: `${numericKey}`;
const label = t(`toolBar.${value}`);
const letter =
key && capitalizeString(typeof key === "string" ? key : key[0]);
const shortcut = letter
? `${letter} ${t("helpDialog.or")} ${numericKey}`
: `${numericKey}`;
return (
<ToolButton
className={clsx("Shape", { fillable })}
key={value}
type="radio"
icon={icon}
checked={activeTool.type === value}
name="editor-current-shape"
title={`${capitalizeString(label)} — ${shortcut}`}
keyBindingLabel={numericKey || letter}
aria-label={capitalizeString(label)}
aria-keyshortcuts={shortcut}
data-testid={`toolbar-${value}`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
app.togglePenMode(true);
}
if (value === "selection") {
if (appState.activeTool.type === "selection") {
app.setActiveTool({ type: "lasso" });
} else {
app.setActiveTool({ type: "selection" });
return (
<ToolButton
className={clsx("Shape", { fillable })}
key={value}
type="radio"
icon={icon}
checked={activeTool.type === value}
name="editor-current-shape"
title={`${capitalizeString(label)} — ${shortcut}`}
keyBindingLabel={numericKey || letter}
aria-label={capitalizeString(label)}
aria-keyshortcuts={shortcut}
data-testid={`toolbar-${value}`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
app.togglePenMode(true);
}
}
}}
onChange={({ pointerType }) => {
if (appState.activeTool.type !== value) {
trackEvent("toolbar", value, "ui");
}
if (value === "image") {
app.setActiveTool({
type: value,
});
} else {
app.setActiveTool({ type: value });
}
}}
/>
);
})}
if (value === "selection") {
if (appState.activeTool.type === "selection") {
app.setActiveTool({ type: "lasso" });
} else {
app.setActiveTool({ type: "selection" });
}
}
}}
onChange={({ pointerType }) => {
if (appState.activeTool.type !== value) {
trackEvent("toolbar", value, "ui");
}
if (value === "image") {
app.setActiveTool({
type: value,
});
} else {
app.setActiveTool({ type: value });
}
}}
/>
);
},
)}
<div className="App-toolbar__divider" />
<DropdownMenu open={isExtraToolsMenuOpen}>
@@ -418,14 +424,16 @@ export const ShapesSwitcher = ({
>
{t("toolBar.laser")}
</DropdownMenu.Item>
<DropdownMenu.Item
onSelect={() => app.setActiveTool({ type: "lasso" })}
icon={LassoIcon}
data-testid="toolbar-lasso"
selected={lassoToolSelected}
>
{t("toolBar.lasso")}
</DropdownMenu.Item>
{app.defaultSelectionTool !== "lasso" && (
<DropdownMenu.Item
onSelect={() => app.setActiveTool({ type: "lasso" })}
icon={LassoIcon}
data-testid="toolbar-lasso"
selected={lassoToolSelected}
>
{t("toolBar.lasso")}
</DropdownMenu.Item>
)}
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
Generate
</div>