Compare commits

...

16 Commits

Author SHA1 Message Date
Preet
c552ff4554 added an explanatory comment 2023-10-23 21:48:35 -07:00
Preet
26f9b54199 compute midpoints properly when dealing with split line indices 2023-10-23 21:41:00 -07:00
Preet
7f5b7bab69 split linear segments as curves 2023-10-23 21:29:53 -07:00
Preet
bf7c91536f render and toggle split points for linear elements as well 2023-10-23 21:15:25 -07:00
Preet
4372e992e0 highlight squares appropriately 2023-10-23 18:13:43 -07:00
Preet
1e4bfceb13 render split points as squares 2023-10-23 17:58:28 -07:00
Preet
539071fcfe ensure split indices are sorted 2023-10-23 17:29:12 -07:00
Preet
3700cf2d10 fix some linting/prettier issues 2023-10-23 10:50:52 -07:00
Preet
89218ba596 update indices when inserting/removing points 2023-10-22 17:39:51 -07:00
Preet
bc5436592e split curve only for rounded curves 2023-10-22 17:07:08 -07:00
Preet
750055ddfa draw split curves 2023-10-21 21:45:27 -07:00
Preet
93e4cb8d25 restore properly 2023-10-21 17:24:01 -07:00
Preet
a2dd3c6ea2 visual indicator that curve is being split 2023-10-21 16:57:56 -07:00
Preet
0360e64219 . 2023-10-21 16:41:32 -07:00
Preet
c2867c9a93 defined split array 2023-10-21 16:15:41 -07:00
Preet
14bca119f7 update rough to include hetrogeneous curves 2023-10-21 16:04:06 -07:00
11 changed files with 206 additions and 40 deletions

View File

@ -53,7 +53,7 @@
"pwacompat": "2.0.17", "pwacompat": "2.0.17",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"roughjs": "4.6.4", "roughjs": "4.6.5",
"sass": "1.51.0", "sass": "1.51.0",
"socket.io-client": "2.3.1", "socket.io-client": "2.3.1",
"tunnel-rat": "0.1.2" "tunnel-rat": "0.1.2"

View File

@ -3750,9 +3750,32 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
event,
this.state,
);
const selectedElements = this.scene.getSelectedElements(this.state); const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) { if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
if (!event[KEYS.CTRL_OR_CMD]) {
// If double clicked without any ctrl/cmd modifier on top of a point,
// toggle split mode for that point. Else, treat as regular double click.
const pointUnderCursorIndex =
LinearElementEditor.getPointIndexUnderCursor(
selectedElements[0],
this.state.zoom,
sceneX,
sceneY,
);
if (pointUnderCursorIndex >= 0) {
LinearElementEditor.toggleSegmentSplitAtIndex(
selectedElements[0],
pointUnderCursorIndex,
);
return;
}
}
if ( if (
event[KEYS.CTRL_OR_CMD] && event[KEYS.CTRL_OR_CMD] &&
(!this.state.editingLinearElement || (!this.state.editingLinearElement ||
@ -3776,11 +3799,6 @@ class App extends React.Component<AppProps, AppState> {
resetCursor(this.interactiveCanvas); resetCursor(this.interactiveCanvas);
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
event,
this.state,
);
const selectedGroupIds = getSelectedGroupIds(this.state); const selectedGroupIds = getSelectedGroupIds(this.state);
if (selectedGroupIds.length > 0) { if (selectedGroupIds.length > 0) {

View File

@ -285,6 +285,9 @@ const restoreElement = (
points, points,
x, x,
y, y,
segmentSplitIndices: element.segmentSplitIndices
? [...element.segmentSplitIndices]
: [],
}); });
} }

View File

@ -741,7 +741,7 @@ export const getElementPointsCoords = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
points: readonly (readonly [number, number])[], points: readonly (readonly [number, number])[],
): [number, number, number, number] => { ): [number, number, number, number] => {
// This might be computationally heavey // This might be computationally heavy
const gen = rough.generator(); const gen = rough.generator();
const curve = const curve =
element.roundness == null element.roundness == null

View File

@ -547,7 +547,10 @@ export class LinearElementEditor {
endPointIndex: number, endPointIndex: number,
) { ) {
let segmentMidPoint = centerPoint(startPoint, endPoint); let segmentMidPoint = centerPoint(startPoint, endPoint);
if (element.points.length > 2 && element.roundness) { const splits = element.segmentSplitIndices || [];
const treatAsCurve =
splits.includes(endPointIndex) || splits.includes(endPointIndex - 1);
if (element.points.length > 2 && (element.roundness || treatAsCurve)) {
const controlPoints = getControlPointsForBezierCurve( const controlPoints = getControlPointsForBezierCurve(
element, element,
element.points[endPointIndex], element.points[endPointIndex],
@ -1042,13 +1045,15 @@ export class LinearElementEditor {
let offsetX = 0; let offsetX = 0;
let offsetY = 0; let offsetY = 0;
const isDeletingOriginPoint = pointIndices.includes(0); const indexSet = new Set(pointIndices);
const isDeletingOriginPoint = indexSet.has(0);
// if deleting first point, make the next to be [0,0] and recalculate // if deleting first point, make the next to be [0,0] and recalculate
// positions of the rest with respect to it // positions of the rest with respect to it
if (isDeletingOriginPoint) { if (isDeletingOriginPoint) {
const firstNonDeletedPoint = element.points.find((point, idx) => { const firstNonDeletedPoint = element.points.find((point, idx) => {
return !pointIndices.includes(idx); return !indexSet.has(idx);
}); });
if (firstNonDeletedPoint) { if (firstNonDeletedPoint) {
offsetX = firstNonDeletedPoint[0]; offsetX = firstNonDeletedPoint[0];
@ -1057,7 +1062,7 @@ export class LinearElementEditor {
} }
const nextPoints = element.points.reduce((acc: Point[], point, idx) => { const nextPoints = element.points.reduce((acc: Point[], point, idx) => {
if (!pointIndices.includes(idx)) { if (!indexSet.has(idx)) {
acc.push( acc.push(
!acc.length ? [0, 0] : [point[0] - offsetX, point[1] - offsetY], !acc.length ? [0, 0] : [point[0] - offsetX, point[1] - offsetY],
); );
@ -1065,7 +1070,22 @@ export class LinearElementEditor {
return acc; return acc;
}, []); }, []);
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY); const splits: number[] = [];
(element.segmentSplitIndices || []).forEach((index) => {
if (!indexSet.has(index)) {
let shift = 0;
for (const pointIndex of pointIndices) {
if (index > pointIndex) {
shift++;
}
}
splits.push(index - shift);
}
});
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY, {
segmentSplitIndices: splits.sort((a, b) => a - b),
});
} }
static addPoints( static addPoints(
@ -1204,9 +1224,13 @@ export class LinearElementEditor {
midpoint, midpoint,
...element.points.slice(segmentMidpoint.index!), ...element.points.slice(segmentMidpoint.index!),
]; ];
const splits = (element.segmentSplitIndices || []).map((index) =>
index >= segmentMidpoint.index! ? index + 1 : index,
);
mutateElement(element, { mutateElement(element, {
points, points,
segmentSplitIndices: splits.sort((a, b) => a - b),
}); });
ret.pointerDownState = { ret.pointerDownState = {
@ -1226,7 +1250,11 @@ export class LinearElementEditor {
nextPoints: readonly Point[], nextPoints: readonly Point[],
offsetX: number, offsetX: number,
offsetY: number, offsetY: number,
otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding }, otherUpdates?: {
startBinding?: PointBinding;
endBinding?: PointBinding;
segmentSplitIndices?: number[];
},
) { ) {
const nextCoords = getElementPointsCoords(element, nextPoints); const nextCoords = getElementPointsCoords(element, nextPoints);
const prevCoords = getElementPointsCoords(element, element.points); const prevCoords = getElementPointsCoords(element, element.points);
@ -1472,6 +1500,27 @@ export class LinearElementEditor {
return coords; return coords;
}; };
static toggleSegmentSplitAtIndex(
element: NonDeleted<ExcalidrawLinearElement>,
index: number,
) {
let found = false;
const splitIndices = (element.segmentSplitIndices || []).filter((idx) => {
if (idx === index) {
found = true;
return false;
}
return true;
});
if (!found) {
splitIndices.push(index);
}
mutateElement(element, {
segmentSplitIndices: splitIndices.sort((a, b) => a - b),
});
}
} }
const normalizeSelectedPoints = ( const normalizeSelectedPoints = (

View File

@ -25,7 +25,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
// casting to any because can't use `in` operator // casting to any because can't use `in` operator
// (see https://github.com/microsoft/TypeScript/issues/21732) // (see https://github.com/microsoft/TypeScript/issues/21732)
const { points, fileId } = updates as any; const { points, fileId, segmentSplitIndices } = updates as any;
if (typeof points !== "undefined") { if (typeof points !== "undefined") {
updates = { ...getSizeFromPoints(points), ...updates }; updates = { ...getSizeFromPoints(points), ...updates };
@ -86,6 +86,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
if ( if (
typeof updates.height !== "undefined" || typeof updates.height !== "undefined" ||
typeof updates.width !== "undefined" || typeof updates.width !== "undefined" ||
typeof segmentSplitIndices !== "undefined" ||
typeof fileId != "undefined" || typeof fileId != "undefined" ||
typeof points !== "undefined" typeof points !== "undefined"
) { ) {

View File

@ -374,6 +374,7 @@ export const newLinearElement = (
endBinding: null, endBinding: null,
startArrowhead: opts.startArrowhead || null, startArrowhead: opts.startArrowhead || null,
endArrowhead: opts.endArrowhead || null, endArrowhead: opts.endArrowhead || null,
segmentSplitIndices: [],
}; };
}; };

View File

@ -195,6 +195,7 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
Readonly<{ Readonly<{
type: "line" | "arrow"; type: "line" | "arrow";
points: readonly Point[]; points: readonly Point[];
segmentSplitIndices: readonly number[] | null;
lastCommittedPoint: Point | null; lastCommittedPoint: Point | null;
startBinding: PointBinding | null; startBinding: PointBinding | null;
endBinding: PointBinding | null; endBinding: PointBinding | null;

View File

@ -166,6 +166,21 @@ const fillCircle = (
} }
}; };
const fillSquare = (
context: CanvasRenderingContext2D,
cx: number,
cy: number,
side: number,
stroke = true,
) => {
context.beginPath();
context.rect(cx - side / 2, cy - side / 2, side, side);
context.fill();
if (stroke) {
context.stroke();
}
};
const strokeGrid = ( const strokeGrid = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
gridSize: number, gridSize: number,
@ -224,6 +239,7 @@ const renderSingleLinearPoint = (
point: Point, point: Point,
radius: number, radius: number,
isSelected: boolean, isSelected: boolean,
renderAsSquare: boolean,
isPhantomPoint = false, isPhantomPoint = false,
) => { ) => {
context.strokeStyle = "#5e5ad8"; context.strokeStyle = "#5e5ad8";
@ -235,13 +251,29 @@ const renderSingleLinearPoint = (
context.fillStyle = "rgba(177, 151, 252, 0.7)"; context.fillStyle = "rgba(177, 151, 252, 0.7)";
} }
fillCircle( const effectiveRadius = radius / appState.zoom.value;
context,
point[0], if (renderAsSquare) {
point[1], fillSquare(
radius / appState.zoom.value, context,
!isPhantomPoint, point[0],
); point[1],
effectiveRadius * 2,
!isPhantomPoint,
);
} else {
fillCircle(context, point[0], point[1], effectiveRadius, !isPhantomPoint);
}
};
const isLinearPointAtIndexSquared = (
element: NonDeleted<ExcalidrawLinearElement>,
index: number,
) => {
const splitting = element.segmentSplitIndices
? element.segmentSplitIndices.includes(index)
: false;
return element.roundness ? splitting : !splitting;
}; };
const renderLinearPointHandles = ( const renderLinearPointHandles = (
@ -265,7 +297,14 @@ const renderLinearPointHandles = (
const isSelected = const isSelected =
!!appState.editingLinearElement?.selectedPointsIndices?.includes(idx); !!appState.editingLinearElement?.selectedPointsIndices?.includes(idx);
renderSingleLinearPoint(context, appState, point, radius, isSelected); renderSingleLinearPoint(
context,
appState,
point,
radius,
isSelected,
isLinearPointAtIndexSquared(element, idx),
);
}); });
//Rendering segment mid points //Rendering segment mid points
@ -293,6 +332,7 @@ const renderLinearPointHandles = (
segmentMidPoint, segmentMidPoint,
radius, radius,
false, false,
false,
); );
highlightPoint(segmentMidPoint, context, appState); highlightPoint(segmentMidPoint, context, appState);
} else { } else {
@ -303,6 +343,7 @@ const renderLinearPointHandles = (
segmentMidPoint, segmentMidPoint,
radius, radius,
false, false,
false,
); );
} }
} else if (appState.editingLinearElement || points.length === 2) { } else if (appState.editingLinearElement || points.length === 2) {
@ -312,6 +353,7 @@ const renderLinearPointHandles = (
segmentMidPoint, segmentMidPoint,
POINT_HANDLE_SIZE / 2, POINT_HANDLE_SIZE / 2,
false, false,
false,
true, true,
); );
} }
@ -324,16 +366,16 @@ const highlightPoint = (
point: Point, point: Point,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState, appState: InteractiveCanvasAppState,
renderAsSquare = false,
) => { ) => {
context.fillStyle = "rgba(105, 101, 219, 0.4)"; context.fillStyle = "rgba(105, 101, 219, 0.4)";
const radius = LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value;
fillCircle( if (renderAsSquare) {
context, fillSquare(context, point[0], point[1], radius * 2, false);
point[0], } else {
point[1], fillCircle(context, point[0], point[1], radius, false);
LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value, }
false,
);
}; };
const renderLinearElementPointHighlight = ( const renderLinearElementPointHighlight = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
@ -355,10 +397,15 @@ const renderLinearElementPointHighlight = (
element, element,
hoverPointIndex, hoverPointIndex,
); );
context.save(); context.save();
context.translate(appState.scrollX, appState.scrollY); context.translate(appState.scrollX, appState.scrollY);
highlightPoint(
highlightPoint(point, context, appState); point,
context,
appState,
isLinearPointAtIndexSquared(element, hoverPointIndex),
);
context.restore(); context.restore();
}; };

View File

@ -14,6 +14,7 @@ import { generateFreeDrawShape } from "../renderer/renderElement";
import { isTransparent, assertNever } from "../utils"; import { isTransparent, assertNever } from "../utils";
import { simplify } from "points-on-curve"; import { simplify } from "points-on-curve";
import { ROUGHNESS } from "../constants"; import { ROUGHNESS } from "../constants";
import { Point } from "../types";
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth]; const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
@ -228,18 +229,44 @@ export const _generateElementShape = (
// points array can be empty in the beginning, so it is important to add // points array can be empty in the beginning, so it is important to add
// initial position to it // initial position to it
const points = element.points.length ? element.points : [[0, 0]]; const points = element.points.length
? element.points
: ([[0, 0]] as Point[]);
// curve is always the first element // curve is always the first element
// this simplifies finding the curve for an element // this simplifies finding the curve for an element
const splits = element.segmentSplitIndices || [];
if (!element.roundness) { if (!element.roundness) {
if (options.fill) { if (splits.length === 0) {
shape = [generator.polygon(points as [number, number][], options)]; if (options.fill) {
shape = [generator.polygon(points as [number, number][], options)];
} else {
shape = [
generator.linearPath(points as [number, number][], options),
];
}
} else { } else {
shape = [generator.linearPath(points as [number, number][], options)]; const splitInverse: number[] = [];
const splitSet = new Set(splits);
for (let i = 0; i < points.length; i++) {
if (!splitSet.has(i)) {
splitInverse.push(i);
}
}
shape = [
generator.curve(
computeMultipleCurvesFromSplits(points, splitInverse),
options,
),
];
} }
} else { } else {
shape = [generator.curve(points as [number, number][], options)]; shape = [
generator.curve(
computeMultipleCurvesFromSplits(points, splits),
options,
),
];
} }
// add lines only in arrow // add lines only in arrow
@ -376,3 +403,22 @@ export const _generateElementShape = (
} }
} }
}; };
const computeMultipleCurvesFromSplits = (
points: readonly Point[],
splits: readonly number[],
): [number, number][][] => {
const pointList: Point[][] = [];
let currentIndex = 0;
for (const index of splits) {
const slice = points.slice(currentIndex, index + 1);
if (slice.length) {
pointList.push([...slice]);
}
currentIndex = index;
}
if (currentIndex < points.length - 1) {
pointList.push(points.slice(currentIndex));
}
return pointList as [number, number][][];
};

View File

@ -6459,10 +6459,10 @@ rollup@^3.25.2:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
roughjs@4.6.4: roughjs@4.6.5:
version "4.6.4" version "4.6.5"
resolved "https://registry.yarnpkg.com/roughjs/-/roughjs-4.6.4.tgz#b6f39b44645854a6e0a4a28b078368701eb7f939" resolved "https://registry.yarnpkg.com/roughjs/-/roughjs-4.6.5.tgz#1db965cf1a043cb7f05181dd7d119f7960fba8d8"
integrity sha512-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw== integrity sha512-4Q6XBbZWlp8yj1uipq2bQ1CPlxMhW/ukufwkuhh+2L79utk+O5kMSbfVh4UNBMtKJ3PxHQ9Ou3ncNt1iQcphJA==
dependencies: dependencies:
hachure-fill "^0.5.2" hachure-fill "^0.5.2"
path-data-parser "^0.1.0" path-data-parser "^0.1.0"