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",
"react": "18.2.0",
"react-dom": "18.2.0",
"roughjs": "4.6.4",
"roughjs": "4.6.5",
"sass": "1.51.0",
"socket.io-client": "2.3.1",
"tunnel-rat": "0.1.2"

View File

@ -3750,9 +3750,32 @@ class App extends React.Component<AppProps, AppState> {
return;
}
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
event,
this.state,
);
const selectedElements = this.scene.getSelectedElements(this.state);
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 (
event[KEYS.CTRL_OR_CMD] &&
(!this.state.editingLinearElement ||
@ -3776,11 +3799,6 @@ class App extends React.Component<AppProps, AppState> {
resetCursor(this.interactiveCanvas);
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
event,
this.state,
);
const selectedGroupIds = getSelectedGroupIds(this.state);
if (selectedGroupIds.length > 0) {

View File

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

View File

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

View File

@ -547,7 +547,10 @@ export class LinearElementEditor {
endPointIndex: number,
) {
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(
element,
element.points[endPointIndex],
@ -1042,13 +1045,15 @@ export class LinearElementEditor {
let offsetX = 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
// positions of the rest with respect to it
if (isDeletingOriginPoint) {
const firstNonDeletedPoint = element.points.find((point, idx) => {
return !pointIndices.includes(idx);
return !indexSet.has(idx);
});
if (firstNonDeletedPoint) {
offsetX = firstNonDeletedPoint[0];
@ -1057,7 +1062,7 @@ export class LinearElementEditor {
}
const nextPoints = element.points.reduce((acc: Point[], point, idx) => {
if (!pointIndices.includes(idx)) {
if (!indexSet.has(idx)) {
acc.push(
!acc.length ? [0, 0] : [point[0] - offsetX, point[1] - offsetY],
);
@ -1065,7 +1070,22 @@ export class LinearElementEditor {
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(
@ -1204,9 +1224,13 @@ export class LinearElementEditor {
midpoint,
...element.points.slice(segmentMidpoint.index!),
];
const splits = (element.segmentSplitIndices || []).map((index) =>
index >= segmentMidpoint.index! ? index + 1 : index,
);
mutateElement(element, {
points,
segmentSplitIndices: splits.sort((a, b) => a - b),
});
ret.pointerDownState = {
@ -1226,7 +1250,11 @@ export class LinearElementEditor {
nextPoints: readonly Point[],
offsetX: number,
offsetY: number,
otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
otherUpdates?: {
startBinding?: PointBinding;
endBinding?: PointBinding;
segmentSplitIndices?: number[];
},
) {
const nextCoords = getElementPointsCoords(element, nextPoints);
const prevCoords = getElementPointsCoords(element, element.points);
@ -1472,6 +1500,27 @@ export class LinearElementEditor {
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 = (

View File

@ -25,7 +25,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
// casting to any because can't use `in` operator
// (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") {
updates = { ...getSizeFromPoints(points), ...updates };
@ -86,6 +86,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
if (
typeof updates.height !== "undefined" ||
typeof updates.width !== "undefined" ||
typeof segmentSplitIndices !== "undefined" ||
typeof fileId != "undefined" ||
typeof points !== "undefined"
) {

View File

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

View File

@ -195,6 +195,7 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
Readonly<{
type: "line" | "arrow";
points: readonly Point[];
segmentSplitIndices: readonly number[] | null;
lastCommittedPoint: Point | null;
startBinding: 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 = (
context: CanvasRenderingContext2D,
gridSize: number,
@ -224,6 +239,7 @@ const renderSingleLinearPoint = (
point: Point,
radius: number,
isSelected: boolean,
renderAsSquare: boolean,
isPhantomPoint = false,
) => {
context.strokeStyle = "#5e5ad8";
@ -235,13 +251,29 @@ const renderSingleLinearPoint = (
context.fillStyle = "rgba(177, 151, 252, 0.7)";
}
fillCircle(
const effectiveRadius = radius / appState.zoom.value;
if (renderAsSquare) {
fillSquare(
context,
point[0],
point[1],
radius / appState.zoom.value,
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 = (
@ -265,7 +297,14 @@ const renderLinearPointHandles = (
const isSelected =
!!appState.editingLinearElement?.selectedPointsIndices?.includes(idx);
renderSingleLinearPoint(context, appState, point, radius, isSelected);
renderSingleLinearPoint(
context,
appState,
point,
radius,
isSelected,
isLinearPointAtIndexSquared(element, idx),
);
});
//Rendering segment mid points
@ -293,6 +332,7 @@ const renderLinearPointHandles = (
segmentMidPoint,
radius,
false,
false,
);
highlightPoint(segmentMidPoint, context, appState);
} else {
@ -303,6 +343,7 @@ const renderLinearPointHandles = (
segmentMidPoint,
radius,
false,
false,
);
}
} else if (appState.editingLinearElement || points.length === 2) {
@ -312,6 +353,7 @@ const renderLinearPointHandles = (
segmentMidPoint,
POINT_HANDLE_SIZE / 2,
false,
false,
true,
);
}
@ -324,16 +366,16 @@ const highlightPoint = (
point: Point,
context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState,
renderAsSquare = false,
) => {
context.fillStyle = "rgba(105, 101, 219, 0.4)";
const radius = LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value;
fillCircle(
context,
point[0],
point[1],
LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value,
false,
);
if (renderAsSquare) {
fillSquare(context, point[0], point[1], radius * 2, false);
} else {
fillCircle(context, point[0], point[1], radius, false);
}
};
const renderLinearElementPointHighlight = (
context: CanvasRenderingContext2D,
@ -355,10 +397,15 @@ const renderLinearElementPointHighlight = (
element,
hoverPointIndex,
);
context.save();
context.translate(appState.scrollX, appState.scrollY);
highlightPoint(point, context, appState);
highlightPoint(
point,
context,
appState,
isLinearPointAtIndexSquared(element, hoverPointIndex),
);
context.restore();
};

View File

@ -14,6 +14,7 @@ import { generateFreeDrawShape } from "../renderer/renderElement";
import { isTransparent, assertNever } from "../utils";
import { simplify } from "points-on-curve";
import { ROUGHNESS } from "../constants";
import { Point } from "../types";
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
// 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
// this simplifies finding the curve for an element
const splits = element.segmentSplitIndices || [];
if (!element.roundness) {
if (splits.length === 0) {
if (options.fill) {
shape = [generator.polygon(points as [number, number][], options)];
} else {
shape = [generator.linearPath(points as [number, number][], options)];
shape = [
generator.linearPath(points as [number, number][], options),
];
}
} else {
shape = [generator.curve(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 {
shape = [
generator.curve(
computeMultipleCurvesFromSplits(points, splits),
options,
),
];
}
// 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:
fsevents "~2.3.2"
roughjs@4.6.4:
version "4.6.4"
resolved "https://registry.yarnpkg.com/roughjs/-/roughjs-4.6.4.tgz#b6f39b44645854a6e0a4a28b078368701eb7f939"
integrity sha512-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw==
roughjs@4.6.5:
version "4.6.5"
resolved "https://registry.yarnpkg.com/roughjs/-/roughjs-4.6.5.tgz#1db965cf1a043cb7f05181dd7d119f7960fba8d8"
integrity sha512-4Q6XBbZWlp8yj1uipq2bQ1CPlxMhW/ukufwkuhh+2L79utk+O5kMSbfVh4UNBMtKJ3PxHQ9Ou3ncNt1iQcphJA==
dependencies:
hachure-fill "^0.5.2"
path-data-parser "^0.1.0"