@@ -144,10 +144,6 @@ export interface ExcalidrawElementWithCanvas {
|
|||||||
imageCrop: ExcalidrawImageElement["crop"] | null;
|
imageCrop: ExcalidrawImageElement["crop"] | null;
|
||||||
containingFrameOpacity: number;
|
containingFrameOpacity: number;
|
||||||
boundTextCanvas: HTMLCanvasElement;
|
boundTextCanvas: HTMLCanvasElement;
|
||||||
/**
|
|
||||||
* When set, overrides the canvas origin placement in drawElementFromCanvas.
|
|
||||||
* Used by the incremental freedraw rendering path. Scene coordinates.
|
|
||||||
*/
|
|
||||||
canvasOriginSceneX?: number;
|
canvasOriginSceneX?: number;
|
||||||
canvasOriginSceneY?: number;
|
canvasOriginSceneY?: number;
|
||||||
}
|
}
|
||||||
@@ -390,29 +386,6 @@ const drawImagePlaceholder = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Freedraw predicted-point store ─────────────────────────────────────────
|
|
||||||
// Keyed by element ID. Coordinates are in element-local scene units
|
|
||||||
// (i.e. relative to element.x / element.y), matching the format of
|
|
||||||
// ExcalidrawFreeDrawElement.points.
|
|
||||||
const freedrawPredictedPoints = new Map<string, readonly [number, number]>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets (or clears) the predicted next point for a freedraw element that is
|
|
||||||
* currently being drawn. Call from the pointer-move handler using the first
|
|
||||||
* entry returned by PointerEvent.getPredictedEvents(). Call with `null` when
|
|
||||||
* the stroke is finalised so the ghost segment is no longer rendered.
|
|
||||||
*/
|
|
||||||
export const setFreeDrawPredictedPoint = (
|
|
||||||
elementId: string,
|
|
||||||
point: readonly [number, number] | null,
|
|
||||||
): void => {
|
|
||||||
if (point !== null) {
|
|
||||||
freedrawPredictedPoints.set(elementId, point);
|
|
||||||
} else {
|
|
||||||
freedrawPredictedPoints.delete(elementId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_FREEDRAW_PRESSURE = 0.5;
|
const DEFAULT_FREEDRAW_PRESSURE = 0.5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -939,19 +912,23 @@ export const elementWithCanvasCache = new WeakMap<
|
|||||||
ExcalidrawElementWithCanvas
|
ExcalidrawElementWithCanvas
|
||||||
>();
|
>();
|
||||||
|
|
||||||
// ─── Incremental freedraw canvas cache ────────────────────────────────────────
|
// ─── Incremental freedraw canvas cache ───────────────────────────────────────
|
||||||
// A separate WeakMap that survives ShapeCache.delete() calls so that the raster
|
// A separate WeakMap that survives ShapeCache.delete() calls so that the raster
|
||||||
// accumulates new capsule segments without full regeneration on every added point.
|
// accumulates new capsule segments without full regeneration on every added
|
||||||
|
// point.
|
||||||
|
|
||||||
const FREEDRAW_CANVAS_OVERSHOOT_MIN = 200; // screen pixels — minimum extra lookahead space on each side (divided by scale at use)
|
// screen pixels — minimum extra lookahead space on each side
|
||||||
const FREEDRAW_CANVAS_OVERSHOOT_FACTOR = 0.5; // allocate current_dimension * factor extra on each side
|
// (divided by scale at use)
|
||||||
|
const FREEDRAW_CANVAS_OVERSHOOT_MIN = 200;
|
||||||
|
|
||||||
|
// allocate current_dimension * factor extra on each side
|
||||||
|
const FREEDRAW_CANVAS_OVERSHOOT_FACTOR = 0.5;
|
||||||
|
|
||||||
interface FreeDrawIncrementalCanvas {
|
interface FreeDrawIncrementalCanvas {
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
lastRenderedPointCount: number;
|
lastRenderedPointCount: number;
|
||||||
canvasOriginSceneX: number;
|
canvasOriginSceneX: number;
|
||||||
canvasOriginSceneY: number;
|
canvasOriginSceneY: number;
|
||||||
/** Scene bounds guaranteed to be visible inside this canvas (without padding) */
|
|
||||||
canvasAllocX1: number;
|
canvasAllocX1: number;
|
||||||
canvasAllocY1: number;
|
canvasAllocY1: number;
|
||||||
canvasAllocX2: number;
|
canvasAllocX2: number;
|
||||||
@@ -971,8 +948,8 @@ const freedrawIncrementalCache = new WeakMap<
|
|||||||
* cache is NOT cleared on each mutateElement call, allowing new segments to be
|
* cache is NOT cleared on each mutateElement call, allowing new segments to be
|
||||||
* appended without full regeneration.
|
* appended without full regeneration.
|
||||||
*
|
*
|
||||||
* When element bounds grow beyond the over-allocated canvas, the existing raster
|
* When element bounds grow beyond the over-allocated canvas, the existing
|
||||||
* is copied into a new larger canvas before appending the next segments.
|
* raster is copied into a new larger canvas before appending the next segments
|
||||||
*/
|
*/
|
||||||
const generateOrUpdateFreeDrawIncrementalCanvas = (
|
const generateOrUpdateFreeDrawIncrementalCanvas = (
|
||||||
element: ExcalidrawFreeDrawElement,
|
element: ExcalidrawFreeDrawElement,
|
||||||
@@ -1100,9 +1077,6 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
|
|||||||
canvasScale = prevInc.scale;
|
canvasScale = prevInc.scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch predicted point for this element (set by the pointer-move handler).
|
|
||||||
const predictedPoint = freedrawPredictedPoints.get(element.id);
|
|
||||||
|
|
||||||
// Roll back 2 points so the Catmull-Rom look-ahead tangent at the last
|
// Roll back 2 points so the Catmull-Rom look-ahead tangent at the last
|
||||||
// committed segment is redrawn correctly when a new point arrives.
|
// committed segment is redrawn correctly when a new point arrives.
|
||||||
// Pressure smoothing is causal (one-sided) so it requires no extra rollback.
|
// Pressure smoothing is causal (one-sided) so it requires no extra rollback.
|
||||||
@@ -1111,7 +1085,7 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
|
|||||||
|
|
||||||
// Draw new (and possibly revised) segments plus the ghost toward the
|
// Draw new (and possibly revised) segments plus the ghost toward the
|
||||||
// predicted point.
|
// predicted point.
|
||||||
if (drawFrom < element.points.length || predictedPoint !== undefined) {
|
if (drawFrom < element.points.length) {
|
||||||
const ctx = canvas.getContext("2d")!;
|
const ctx = canvas.getContext("2d")!;
|
||||||
const canvasElemOffsetX =
|
const canvasElemOffsetX =
|
||||||
(element.x - canvasOriginSceneX) * dpr * canvasScale;
|
(element.x - canvasOriginSceneX) * dpr * canvasScale;
|
||||||
|
|||||||
+143
-52
@@ -1,5 +1,4 @@
|
|||||||
import { simplify } from "points-on-curve";
|
import { simplify } from "points-on-curve";
|
||||||
import { getStroke } from "perfect-freehand";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type GeometricShape,
|
type GeometricShape,
|
||||||
@@ -981,7 +980,7 @@ const _generateElementShape = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// (2) stroke — one path element per capsule, mirrors canvas fill() calls
|
// (2) stroke
|
||||||
shapes.push(...getFreeDrawCapsulePaths(element));
|
shapes.push(...getFreeDrawCapsulePaths(element));
|
||||||
|
|
||||||
return shapes;
|
return shapes;
|
||||||
@@ -1172,19 +1171,17 @@ export const toggleLinePolygonState = (
|
|||||||
// freedraw shape helper
|
// freedraw shape helper
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// ─── Capsule-based SVG export (mirrors renderElement.ts) ─────────────────────
|
|
||||||
// These constants and helpers must be kept in sync with renderElement.ts.
|
|
||||||
const FREEDRAW_DEFAULT_PRESSURE = 0.5;
|
const FREEDRAW_DEFAULT_PRESSURE = 0.5;
|
||||||
const FREEDRAW_BEZIER_SUBDIVIDE_TARGET_SPACING = 3;
|
const FREEDRAW_BEZIER_SUBDIVIDE_TARGET_SPACING = 3;
|
||||||
const FREEDRAW_PRESSURE_SMOOTHING_RADIUS = 6;
|
const FREEDRAW_PRESSURE_SMOOTHING_RADIUS = 6;
|
||||||
|
|
||||||
/** Round to 2 dp — sub-pixel accuracy at SVG 96 dpi, much shorter strings. */
|
// Round to 2 dp — sub-pixel accuracy at SVG 96 dpi
|
||||||
const r2 = (v: number) => Math.round(v * 100) / 100;
|
const r2 = (v: number) => Math.round(v * 100) / 100;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SVG path `d` string for a single tapered capsule — the SVG counterpart of
|
* SVG path `d` string for a single tapered capsule. Uses clockwise arcs
|
||||||
* `drawTaperedCapsule` in renderElement.ts. Uses clockwise arcs (sweep=1)
|
* (sweep=1) so the geometry matches the canvas 2D
|
||||||
* so the geometry matches the canvas 2D `arc(..., anticlockwise=false)` calls.
|
* `arc(..., anticlockwise=false)` calls.
|
||||||
*/
|
*/
|
||||||
const freedrawTaperedCapsulePath = (
|
const freedrawTaperedCapsulePath = (
|
||||||
x0: number,
|
x0: number,
|
||||||
@@ -1214,12 +1211,12 @@ const freedrawTaperedCapsulePath = (
|
|||||||
const px = -dy / len; // perpendicular unit x
|
const px = -dy / len; // perpendicular unit x
|
||||||
const py = dx / len; // perpendicular unit y
|
const py = dx / len; // perpendicular unit y
|
||||||
|
|
||||||
// P0 ± perp·r0 (start / back cap tangent points)
|
// P0 +/- perp·r0 (start / back cap tangent points)
|
||||||
const b0x = r2(x0 + px * r0);
|
const b0x = r2(x0 + px * r0);
|
||||||
const b0y = r2(y0 + py * r0);
|
const b0y = r2(y0 + py * r0);
|
||||||
const b1x = r2(x0 - px * r0);
|
const b1x = r2(x0 - px * r0);
|
||||||
const b1y = r2(y0 - py * r0);
|
const b1y = r2(y0 - py * r0);
|
||||||
// P1 ± perp·r1 (end / front cap tangent points)
|
// P1 +/- perp·r1 (end / front cap tangent points)
|
||||||
const f0x = r2(x1 - px * r1);
|
const f0x = r2(x1 - px * r1);
|
||||||
const f0y = r2(y1 - py * r1);
|
const f0y = r2(y1 - py * r1);
|
||||||
const f1x = r2(x1 + px * r1);
|
const f1x = r2(x1 + px * r1);
|
||||||
@@ -1242,7 +1239,7 @@ const freedrawTaperedCapsulePath = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Catmull-Rom tangent at points[i] — identical math to `getCatmullRomTangent`
|
* Catmull-Rom tangent at points[i].Identical math to `getCatmullRomTangent`
|
||||||
* in renderElement.ts (predictedPoint is not needed for finalised strokes).
|
* in renderElement.ts (predictedPoint is not needed for finalised strokes).
|
||||||
*/
|
*/
|
||||||
const freedrawCatmullRomTangent = (
|
const freedrawCatmullRomTangent = (
|
||||||
@@ -1296,40 +1293,49 @@ const freedrawCatmullRomTangent = (
|
|||||||
return [tx, ty];
|
return [tx, ty];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triangular-kernel causal weighted pressure average (backward-only window).
|
||||||
|
* When `simulatePressure` is true or pressures array is empty, returns the
|
||||||
|
* default constant pressure so the geometry mirrors constant-pressure rendering.
|
||||||
|
*/
|
||||||
|
const getFreeDrawSmoothedPressure = (
|
||||||
|
element: ExcalidrawFreeDrawElement,
|
||||||
|
i: number,
|
||||||
|
): number => {
|
||||||
|
const { pressures } = element;
|
||||||
|
if (element.simulatePressure || pressures.length === 0) {
|
||||||
|
return FREEDRAW_DEFAULT_PRESSURE;
|
||||||
|
}
|
||||||
|
let sum = 0;
|
||||||
|
let totalWeight = 0;
|
||||||
|
for (let k = -FREEDRAW_PRESSURE_SMOOTHING_RADIUS; k <= 0; k++) {
|
||||||
|
const idx = i + k;
|
||||||
|
if (idx < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const p =
|
||||||
|
idx < pressures.length ? pressures[idx] : FREEDRAW_DEFAULT_PRESSURE;
|
||||||
|
const w = FREEDRAW_PRESSURE_SMOOTHING_RADIUS + 1 + k;
|
||||||
|
sum += p * w;
|
||||||
|
totalWeight += w;
|
||||||
|
}
|
||||||
|
return totalWeight > 0 ? sum / totalWeight : FREEDRAW_DEFAULT_PRESSURE;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns one SVG path `d` string per tapered-capsule sub-segment for a
|
* Returns one SVG path `d` string per tapered-capsule sub-segment for a
|
||||||
* freedraw element, using the same Catmull-Rom Bezier subdivision and pressure
|
* freedraw element, using the same Catmull-Rom Bezier subdivision and pressure
|
||||||
* smoothing as the canvas renderer (`drawFreeDrawSegments` in renderElement.ts).
|
* smoothing as the canvas renderer.
|
||||||
* Returning individual paths (rather than a single compound `d` string) avoids
|
|
||||||
* fill-rule artifacts that arise when overlapping subpaths invert the winding
|
|
||||||
* direction inside a compound path.
|
|
||||||
*/
|
*/
|
||||||
const getFreeDrawCapsulePaths = (
|
const getFreeDrawCapsulePaths = (
|
||||||
element: ExcalidrawFreeDrawElement,
|
element: ExcalidrawFreeDrawElement,
|
||||||
): SVGPathString[] => {
|
): SVGPathString[] => {
|
||||||
const { points, pressures } = element;
|
const { points } = element;
|
||||||
const N = points.length;
|
const N = points.length;
|
||||||
const baseRadius = (element.strokeWidth * 1.25) / 2;
|
const baseRadius = (element.strokeWidth * 1.25) / 2;
|
||||||
|
|
||||||
const getSmoothedPressure = (i: number): number => {
|
const getSmoothedPressure = (i: number): number =>
|
||||||
if (element.simulatePressure || pressures.length === 0) {
|
getFreeDrawSmoothedPressure(element, i);
|
||||||
return FREEDRAW_DEFAULT_PRESSURE;
|
|
||||||
}
|
|
||||||
let sum = 0;
|
|
||||||
let totalWeight = 0;
|
|
||||||
for (let k = -FREEDRAW_PRESSURE_SMOOTHING_RADIUS; k <= 0; k++) {
|
|
||||||
const idx = i + k;
|
|
||||||
if (idx < 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const p =
|
|
||||||
idx < pressures.length ? pressures[idx] : FREEDRAW_DEFAULT_PRESSURE;
|
|
||||||
const w = FREEDRAW_PRESSURE_SMOOTHING_RADIUS + 1 + k;
|
|
||||||
sum += p * w;
|
|
||||||
totalWeight += w;
|
|
||||||
}
|
|
||||||
return totalWeight > 0 ? sum / totalWeight : FREEDRAW_DEFAULT_PRESSURE;
|
|
||||||
};
|
|
||||||
|
|
||||||
const paths: SVGPathString[] = [];
|
const paths: SVGPathString[] = [];
|
||||||
|
|
||||||
@@ -1357,7 +1363,7 @@ const getFreeDrawCapsulePaths = (
|
|||||||
const t0 = freedrawCatmullRomTangent(points, i - 1);
|
const t0 = freedrawCatmullRomTangent(points, i - 1);
|
||||||
const t1 = freedrawCatmullRomTangent(points, i);
|
const t1 = freedrawCatmullRomTangent(points, i);
|
||||||
|
|
||||||
// Bezier subdivision (mirrors drawSubdividedSegment).
|
// Bezier subdivision.
|
||||||
const segLen = Math.sqrt((p1[0] - p0[0]) ** 2 + (p1[1] - p0[1]) ** 2);
|
const segLen = Math.sqrt((p1[0] - p0[0]) ** 2 + (p1[1] - p0[1]) ** 2);
|
||||||
const nSubdiv = Math.max(
|
const nSubdiv = Math.max(
|
||||||
1,
|
1,
|
||||||
@@ -1406,25 +1412,110 @@ const getFreeDrawCapsulePaths = (
|
|||||||
return paths;
|
return paths;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an outline polygon for a freedraw element using the same
|
||||||
|
* Catmull-Rom Bezier subdivision and pressure smoothing as the canvas renderer.
|
||||||
|
* Returns `[x, y]` points in element-local coordinates that form a closed
|
||||||
|
* polygon around the stroke (left side + right side reversed), suitable for
|
||||||
|
* hit-testing and eraser intersection.
|
||||||
|
*/
|
||||||
export const getFreedrawOutlinePoints = (
|
export const getFreedrawOutlinePoints = (
|
||||||
element: ExcalidrawFreeDrawElement,
|
element: ExcalidrawFreeDrawElement,
|
||||||
) => {
|
): [number, number][] => {
|
||||||
// If input points are empty (should they ever be?) return a dot
|
const { points } = element;
|
||||||
const inputPoints = element.simulatePressure
|
const N = points.length;
|
||||||
? element.points
|
const baseRadius = (element.strokeWidth * 1.25) / 2;
|
||||||
: element.points.length
|
|
||||||
? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
|
|
||||||
: [[0, 0, 0.5]];
|
|
||||||
|
|
||||||
return getStroke(inputPoints as number[][], {
|
if (N === 0) {
|
||||||
simulatePressure: element.simulatePressure,
|
return [];
|
||||||
size: element.strokeWidth * 4.25,
|
}
|
||||||
thinning: 0.6,
|
|
||||||
smoothing: 0.5,
|
const radius0 = baseRadius * getFreeDrawSmoothedPressure(element, 0) * 2;
|
||||||
streamline: 0.5,
|
|
||||||
easing: (t) => Math.sin((t * Math.PI) / 2), // https://easings.net/#easeOutSine
|
if (N === 1) {
|
||||||
last: true,
|
// Single point case
|
||||||
}) as [number, number][];
|
const cx = points[0][0];
|
||||||
|
const cy = points[0][1];
|
||||||
|
const result: [number, number][] = [];
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const angle = (i / 8) * Math.PI * 2;
|
||||||
|
result.push([
|
||||||
|
cx + Math.cos(angle) * radius0,
|
||||||
|
cy + Math.sin(angle) * radius0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftPoints: [number, number][] = [];
|
||||||
|
const rightPoints: [number, number][] = [];
|
||||||
|
|
||||||
|
for (let i = 1; i < N; i++) {
|
||||||
|
const p0 = points[i - 1];
|
||||||
|
const p1 = points[i];
|
||||||
|
const r0 = baseRadius * getFreeDrawSmoothedPressure(element, i - 1) * 2;
|
||||||
|
const r1 = baseRadius * getFreeDrawSmoothedPressure(element, i) * 2;
|
||||||
|
|
||||||
|
const t0 = freedrawCatmullRomTangent(points, i - 1);
|
||||||
|
const t1 = freedrawCatmullRomTangent(points, i);
|
||||||
|
|
||||||
|
const segLen = Math.sqrt((p1[0] - p0[0]) ** 2 + (p1[1] - p0[1]) ** 2);
|
||||||
|
const nSubdiv = Math.max(
|
||||||
|
1,
|
||||||
|
Math.ceil(segLen / FREEDRAW_BEZIER_SUBDIVIDE_TARGET_SPACING),
|
||||||
|
);
|
||||||
|
|
||||||
|
const cp1x = p0[0] + t0[0] / 3;
|
||||||
|
const cp1y = p0[1] + t0[1] / 3;
|
||||||
|
const cp2x = p1[0] - t1[0] / 3;
|
||||||
|
const cp2y = p1[1] - t1[1] / 3;
|
||||||
|
|
||||||
|
// Include the start point of the first segment.
|
||||||
|
const kStart = i === 1 ? 0 : 1;
|
||||||
|
|
||||||
|
for (let k = kStart; k <= nSubdiv; k++) {
|
||||||
|
const tParam = k / nSubdiv;
|
||||||
|
const mt = 1 - tParam;
|
||||||
|
const mt2 = mt * mt;
|
||||||
|
const t2 = tParam * tParam;
|
||||||
|
const mt3 = mt2 * mt;
|
||||||
|
const t3 = t2 * tParam;
|
||||||
|
|
||||||
|
const x =
|
||||||
|
mt3 * p0[0] + 3 * mt2 * tParam * cp1x + 3 * mt * t2 * cp2x + t3 * p1[0];
|
||||||
|
const y =
|
||||||
|
mt3 * p0[1] + 3 * mt2 * tParam * cp1y + 3 * mt * t2 * cp2y + t3 * p1[1];
|
||||||
|
|
||||||
|
// Bezier first derivative for the tangent direction.
|
||||||
|
const dtx =
|
||||||
|
3 *
|
||||||
|
(mt2 * (cp1x - p0[0]) +
|
||||||
|
2 * mt * tParam * (cp2x - cp1x) +
|
||||||
|
t2 * (p1[0] - cp2x));
|
||||||
|
const dty =
|
||||||
|
3 *
|
||||||
|
(mt2 * (cp1y - p0[1]) +
|
||||||
|
2 * mt * tParam * (cp2y - cp1y) +
|
||||||
|
t2 * (p1[1] - cp2y));
|
||||||
|
|
||||||
|
const len = Math.sqrt(dtx * dtx + dty * dty);
|
||||||
|
if (len === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perpendicular (left = +, right = −).
|
||||||
|
const px = -dty / len;
|
||||||
|
const py = dtx / len;
|
||||||
|
|
||||||
|
const r = r0 + (r1 - r0) * tParam;
|
||||||
|
|
||||||
|
leftPoints.push([x + px * r, y + py * r]);
|
||||||
|
rightPoints.push([x - px * r, y - py * r]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closed polygon: left side (start -> end) + right side (end -> start).
|
||||||
|
return [...leftPoints, ...rightPoints.reverse()];
|
||||||
};
|
};
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ import {
|
|||||||
isLineElement,
|
isLineElement,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import { invalidateFreeDrawIncrementalCanvas } from "@excalidraw/element";
|
||||||
invalidateFreeDrawIncrementalCanvas,
|
|
||||||
setFreeDrawPredictedPoint,
|
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
KEYS,
|
KEYS,
|
||||||
@@ -320,7 +317,6 @@ export const actionFinalize = register<FormData>({
|
|||||||
|
|
||||||
if (element && isFreeDrawElement(element)) {
|
if (element && isFreeDrawElement(element)) {
|
||||||
invalidateFreeDrawIncrementalCanvas(element);
|
invalidateFreeDrawIncrementalCanvas(element);
|
||||||
setFreeDrawPredictedPoint(element.id, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -259,7 +259,6 @@ import {
|
|||||||
maybeHandleArrowPointlikeDrag,
|
maybeHandleArrowPointlikeDrag,
|
||||||
getUncroppedWidthAndHeight,
|
getUncroppedWidthAndHeight,
|
||||||
getActiveTextElement,
|
getActiveTextElement,
|
||||||
setFreeDrawPredictedPoint,
|
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||||
@@ -8837,7 +8836,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
points: [pointFrom<LocalPoint>(0, 0)],
|
points: [pointFrom<LocalPoint>(0, 0)],
|
||||||
pressures: simulatePressure ? [] : [event.pressure],
|
pressures: [event.pressure],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scene.insertElement(element);
|
this.scene.insertElement(element);
|
||||||
@@ -10172,17 +10171,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newElement.type === "freedraw") {
|
if (newElement.type === "freedraw") {
|
||||||
// Collect all coalesced pointer positions that accumulated between
|
|
||||||
// RAF frames (important for high-frequency Apple Pencil / stylus
|
|
||||||
// input). getCoalescedEvents() returns the intermediate samples that
|
|
||||||
// would otherwise be discarded by throttleRAF, giving a dense stroke
|
|
||||||
// without extra renders.
|
|
||||||
const coalescedEvents: PointerEvent[] =
|
const coalescedEvents: PointerEvent[] =
|
||||||
event.getCoalescedEvents?.() ?? [];
|
event.getCoalescedEvents?.() ?? [];
|
||||||
// The final event is always included; coalesced list may duplicate it
|
|
||||||
const allEvents =
|
const allEvents =
|
||||||
coalescedEvents.length > 0 ? coalescedEvents : [event];
|
coalescedEvents.length > 0 ? coalescedEvents : [event];
|
||||||
|
|
||||||
const newPoints: LocalPoint[] = [];
|
const newPoints: LocalPoint[] = [];
|
||||||
const newPressures: number[] = [];
|
const newPressures: number[] = [];
|
||||||
|
|
||||||
@@ -10203,32 +10195,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the predicted next point for smoother stroke rendering.
|
|
||||||
// getPredictedEvents() returns samples predicted by the browser for
|
|
||||||
// the current pointer position, compensating for event-delivery
|
|
||||||
// latency (especially useful with Apple Pencil / high-frequency
|
|
||||||
// stylus input). We pass the first predicted point to the renderer
|
|
||||||
// so it can (a) use it as a Catmull-Rom look-ahead tangent and
|
|
||||||
// (b) draw a ghost segment toward the predicted position.
|
|
||||||
const predictedEvents: PointerEvent[] =
|
|
||||||
event.getPredictedEvents?.() ?? [];
|
|
||||||
if (predictedEvents.length > 0) {
|
|
||||||
const predCoords = viewportCoordsToSceneCoords(
|
|
||||||
predictedEvents[0],
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
setFreeDrawPredictedPoint(newElement.id, [
|
|
||||||
predCoords.x - newElement.x,
|
|
||||||
predCoords.y - newElement.y,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
setFreeDrawPredictedPoint(newElement.id, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPoints.length > 0) {
|
if (newPoints.length > 0) {
|
||||||
const pressures = newElement.simulatePressure
|
const pressures = [...newElement.pressures, ...newPressures];
|
||||||
? newElement.pressures
|
|
||||||
: [...newElement.pressures, ...newPressures];
|
|
||||||
|
|
||||||
this.scene.mutateElement(
|
this.scene.mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
@@ -10698,9 +10666,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
dx += 0.0001;
|
dx += 0.0001;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pressures = newElement.simulatePressure
|
const pressures = [...newElement.pressures, childEvent.pressure];
|
||||||
? []
|
|
||||||
: [...newElement.pressures, childEvent.pressure];
|
|
||||||
|
|
||||||
this.scene.mutateElement(newElement, {
|
this.scene.mutateElement(newElement, {
|
||||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||||
|
|||||||
@@ -104,7 +104,6 @@
|
|||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
"nanoid": "3.3.3",
|
"nanoid": "3.3.3",
|
||||||
"pako": "2.0.3",
|
"pako": "2.0.3",
|
||||||
"perfect-freehand": "1.2.0",
|
|
||||||
"pica": "7.1.1",
|
"pica": "7.1.1",
|
||||||
"png-chunk-text": "1.0.0",
|
"png-chunk-text": "1.0.0",
|
||||||
"png-chunks-encode": "1.0.0",
|
"png-chunks-encode": "1.0.0",
|
||||||
|
|||||||
@@ -158,14 +158,3 @@ export const vectorNormalize = (v: Vector): Vector => {
|
|||||||
* Calculate the right-hand normal of the vector.
|
* Calculate the right-hand normal of the vector.
|
||||||
*/
|
*/
|
||||||
export const vectorNormal = (v: Vector): Vector => vector(v[1], -v[0]);
|
export const vectorNormal = (v: Vector): Vector => vector(v[1], -v[0]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Scalar projection of vector v onto normal.
|
|
||||||
*
|
|
||||||
* @param v
|
|
||||||
* @param normal
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const vectorReflect = (v: Vector, normal: Vector): Vector => {
|
|
||||||
return vectorSubtract(v, vectorScale(normal, 2 * vectorDot(v, normal)));
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -52,7 +52,6 @@
|
|||||||
"@excalidraw/laser-pointer": "1.3.1",
|
"@excalidraw/laser-pointer": "1.3.1",
|
||||||
"browser-fs-access": "0.38.0",
|
"browser-fs-access": "0.38.0",
|
||||||
"pako": "2.0.3",
|
"pako": "2.0.3",
|
||||||
"perfect-freehand": "1.2.0",
|
|
||||||
"png-chunk-text": "1.0.0",
|
"png-chunk-text": "1.0.0",
|
||||||
"png-chunks-encode": "1.0.0",
|
"png-chunks-encode": "1.0.0",
|
||||||
"png-chunks-extract": "1.0.0",
|
"png-chunks-extract": "1.0.0",
|
||||||
|
|||||||
@@ -8381,11 +8381,6 @@ pepjs@0.5.3:
|
|||||||
resolved "https://registry.yarnpkg.com/pepjs/-/pepjs-0.5.3.tgz#dc755f03d965c20e4b1bb65e42a03a97c382cfc7"
|
resolved "https://registry.yarnpkg.com/pepjs/-/pepjs-0.5.3.tgz#dc755f03d965c20e4b1bb65e42a03a97c382cfc7"
|
||||||
integrity sha512-5yHVB9OHqKd9fr/OIsn8ss0NgThQ9buaqrEuwr9Or5YjPp6h+WTDKWZI+xZLaBGZCtODTnFtlSHNmhFsq67THg==
|
integrity sha512-5yHVB9OHqKd9fr/OIsn8ss0NgThQ9buaqrEuwr9Or5YjPp6h+WTDKWZI+xZLaBGZCtODTnFtlSHNmhFsq67THg==
|
||||||
|
|
||||||
perfect-freehand@1.2.0:
|
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-1.2.0.tgz#706a0f854544f6175772440c51d3b0563eb3988a"
|
|
||||||
integrity sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw==
|
|
||||||
|
|
||||||
pica@7.1.1, pica@^7.1.0:
|
pica@7.1.1, pica@^7.1.0:
|
||||||
version "7.1.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/pica/-/pica-7.1.1.tgz#c68b42f5cfa6cc26eaec5cfa10cc0a5299ef3b7a"
|
resolved "https://registry.yarnpkg.com/pica/-/pica-7.1.1.tgz#c68b42f5cfa6cc26eaec5cfa10cc0a5299ef3b7a"
|
||||||
|
|||||||
Reference in New Issue
Block a user