@@ -144,10 +144,6 @@ export interface ExcalidrawElementWithCanvas {
|
||||
imageCrop: ExcalidrawImageElement["crop"] | null;
|
||||
containingFrameOpacity: number;
|
||||
boundTextCanvas: HTMLCanvasElement;
|
||||
/**
|
||||
* When set, overrides the canvas origin placement in drawElementFromCanvas.
|
||||
* Used by the incremental freedraw rendering path. Scene coordinates.
|
||||
*/
|
||||
canvasOriginSceneX?: 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;
|
||||
|
||||
/**
|
||||
@@ -939,19 +912,23 @@ export const elementWithCanvasCache = new WeakMap<
|
||||
ExcalidrawElementWithCanvas
|
||||
>();
|
||||
|
||||
// ─── Incremental freedraw canvas cache ────────────────────────────────────────
|
||||
// ─── Incremental freedraw canvas cache ───────────────────────────────────────
|
||||
// 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)
|
||||
const FREEDRAW_CANVAS_OVERSHOOT_FACTOR = 0.5; // allocate current_dimension * factor extra on each side
|
||||
// screen pixels — minimum extra lookahead space 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 {
|
||||
canvas: HTMLCanvasElement;
|
||||
lastRenderedPointCount: number;
|
||||
canvasOriginSceneX: number;
|
||||
canvasOriginSceneY: number;
|
||||
/** Scene bounds guaranteed to be visible inside this canvas (without padding) */
|
||||
canvasAllocX1: number;
|
||||
canvasAllocY1: number;
|
||||
canvasAllocX2: number;
|
||||
@@ -971,8 +948,8 @@ const freedrawIncrementalCache = new WeakMap<
|
||||
* cache is NOT cleared on each mutateElement call, allowing new segments to be
|
||||
* appended without full regeneration.
|
||||
*
|
||||
* When element bounds grow beyond the over-allocated canvas, the existing raster
|
||||
* is copied into a new larger canvas before appending the next segments.
|
||||
* When element bounds grow beyond the over-allocated canvas, the existing
|
||||
* raster is copied into a new larger canvas before appending the next segments
|
||||
*/
|
||||
const generateOrUpdateFreeDrawIncrementalCanvas = (
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
@@ -1100,9 +1077,6 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
|
||||
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
|
||||
// committed segment is redrawn correctly when a new point arrives.
|
||||
// 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
|
||||
// predicted point.
|
||||
if (drawFrom < element.points.length || predictedPoint !== undefined) {
|
||||
if (drawFrom < element.points.length) {
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const canvasElemOffsetX =
|
||||
(element.x - canvasOriginSceneX) * dpr * canvasScale;
|
||||
|
||||
+143
-52
@@ -1,5 +1,4 @@
|
||||
import { simplify } from "points-on-curve";
|
||||
import { getStroke } from "perfect-freehand";
|
||||
|
||||
import {
|
||||
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));
|
||||
|
||||
return shapes;
|
||||
@@ -1172,19 +1171,17 @@ export const toggleLinePolygonState = (
|
||||
// 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_BEZIER_SUBDIVIDE_TARGET_SPACING = 3;
|
||||
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;
|
||||
|
||||
/**
|
||||
* SVG path `d` string for a single tapered capsule — the SVG counterpart of
|
||||
* `drawTaperedCapsule` in renderElement.ts. Uses clockwise arcs (sweep=1)
|
||||
* so the geometry matches the canvas 2D `arc(..., anticlockwise=false)` calls.
|
||||
* SVG path `d` string for a single tapered capsule. Uses clockwise arcs
|
||||
* (sweep=1) so the geometry matches the canvas 2D
|
||||
* `arc(..., anticlockwise=false)` calls.
|
||||
*/
|
||||
const freedrawTaperedCapsulePath = (
|
||||
x0: number,
|
||||
@@ -1214,12 +1211,12 @@ const freedrawTaperedCapsulePath = (
|
||||
const px = -dy / len; // perpendicular unit x
|
||||
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 b0y = r2(y0 + py * r0);
|
||||
const b1x = r2(x0 - px * 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 f0y = r2(y1 - py * 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).
|
||||
*/
|
||||
const freedrawCatmullRomTangent = (
|
||||
@@ -1296,40 +1293,49 @@ const freedrawCatmullRomTangent = (
|
||||
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
|
||||
* freedraw element, using the same Catmull-Rom Bezier subdivision and pressure
|
||||
* smoothing as the canvas renderer (`drawFreeDrawSegments` in renderElement.ts).
|
||||
* 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.
|
||||
* smoothing as the canvas renderer.
|
||||
*/
|
||||
const getFreeDrawCapsulePaths = (
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
): SVGPathString[] => {
|
||||
const { points, pressures } = element;
|
||||
const { points } = element;
|
||||
const N = points.length;
|
||||
const baseRadius = (element.strokeWidth * 1.25) / 2;
|
||||
|
||||
const getSmoothedPressure = (i: number): number => {
|
||||
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;
|
||||
};
|
||||
const getSmoothedPressure = (i: number): number =>
|
||||
getFreeDrawSmoothedPressure(element, i);
|
||||
|
||||
const paths: SVGPathString[] = [];
|
||||
|
||||
@@ -1357,7 +1363,7 @@ const getFreeDrawCapsulePaths = (
|
||||
const t0 = freedrawCatmullRomTangent(points, i - 1);
|
||||
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 nSubdiv = Math.max(
|
||||
1,
|
||||
@@ -1406,25 +1412,110 @@ const getFreeDrawCapsulePaths = (
|
||||
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 = (
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
) => {
|
||||
// If input points are empty (should they ever be?) return a dot
|
||||
const inputPoints = element.simulatePressure
|
||||
? element.points
|
||||
: element.points.length
|
||||
? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
|
||||
: [[0, 0, 0.5]];
|
||||
): [number, number][] => {
|
||||
const { points } = element;
|
||||
const N = points.length;
|
||||
const baseRadius = (element.strokeWidth * 1.25) / 2;
|
||||
|
||||
return getStroke(inputPoints as number[][], {
|
||||
simulatePressure: element.simulatePressure,
|
||||
size: element.strokeWidth * 4.25,
|
||||
thinning: 0.6,
|
||||
smoothing: 0.5,
|
||||
streamline: 0.5,
|
||||
easing: (t) => Math.sin((t * Math.PI) / 2), // https://easings.net/#easeOutSine
|
||||
last: true,
|
||||
}) as [number, number][];
|
||||
if (N === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const radius0 = baseRadius * getFreeDrawSmoothedPressure(element, 0) * 2;
|
||||
|
||||
if (N === 1) {
|
||||
// Single point case
|
||||
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,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
invalidateFreeDrawIncrementalCanvas,
|
||||
setFreeDrawPredictedPoint,
|
||||
} from "@excalidraw/element";
|
||||
import { invalidateFreeDrawIncrementalCanvas } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
KEYS,
|
||||
@@ -320,7 +317,6 @@ export const actionFinalize = register<FormData>({
|
||||
|
||||
if (element && isFreeDrawElement(element)) {
|
||||
invalidateFreeDrawIncrementalCanvas(element);
|
||||
setFreeDrawPredictedPoint(element.id, null);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -259,7 +259,6 @@ import {
|
||||
maybeHandleArrowPointlikeDrag,
|
||||
getUncroppedWidthAndHeight,
|
||||
getActiveTextElement,
|
||||
setFreeDrawPredictedPoint,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||
@@ -8837,7 +8836,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
points: [pointFrom<LocalPoint>(0, 0)],
|
||||
pressures: simulatePressure ? [] : [event.pressure],
|
||||
pressures: [event.pressure],
|
||||
});
|
||||
|
||||
this.scene.insertElement(element);
|
||||
@@ -10172,17 +10171,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
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[] =
|
||||
event.getCoalescedEvents?.() ?? [];
|
||||
// The final event is always included; coalesced list may duplicate it
|
||||
const allEvents =
|
||||
coalescedEvents.length > 0 ? coalescedEvents : [event];
|
||||
|
||||
const newPoints: LocalPoint[] = [];
|
||||
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) {
|
||||
const pressures = newElement.simulatePressure
|
||||
? newElement.pressures
|
||||
: [...newElement.pressures, ...newPressures];
|
||||
const pressures = [...newElement.pressures, ...newPressures];
|
||||
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
@@ -10698,9 +10666,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
dx += 0.0001;
|
||||
}
|
||||
|
||||
const pressures = newElement.simulatePressure
|
||||
? []
|
||||
: [...newElement.pressures, childEvent.pressure];
|
||||
const pressures = [...newElement.pressures, childEvent.pressure];
|
||||
|
||||
this.scene.mutateElement(newElement, {
|
||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||
|
||||
@@ -104,7 +104,6 @@
|
||||
"lodash.throttle": "4.1.1",
|
||||
"nanoid": "3.3.3",
|
||||
"pako": "2.0.3",
|
||||
"perfect-freehand": "1.2.0",
|
||||
"pica": "7.1.1",
|
||||
"png-chunk-text": "1.0.0",
|
||||
"png-chunks-encode": "1.0.0",
|
||||
@@ -138,4 +137,4 @@
|
||||
"jest-diff": "29.7.0",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,14 +158,3 @@ export const vectorNormalize = (v: Vector): Vector => {
|
||||
* Calculate the right-hand normal of the vector.
|
||||
*/
|
||||
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",
|
||||
"browser-fs-access": "0.38.0",
|
||||
"pako": "2.0.3",
|
||||
"perfect-freehand": "1.2.0",
|
||||
"png-chunk-text": "1.0.0",
|
||||
"png-chunks-encode": "1.0.0",
|
||||
"png-chunks-extract": "1.0.0",
|
||||
@@ -71,4 +70,4 @@
|
||||
"gen:types": "rimraf types && tsc",
|
||||
"build:esm": "rimraf dist && node ../../scripts/buildUtils.js && yarn gen:types"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8381,11 +8381,6 @@ pepjs@0.5.3:
|
||||
resolved "https://registry.yarnpkg.com/pepjs/-/pepjs-0.5.3.tgz#dc755f03d965c20e4b1bb65e42a03a97c382cfc7"
|
||||
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:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pica/-/pica-7.1.1.tgz#c68b42f5cfa6cc26eaec5cfa10cc0a5299ef3b7a"
|
||||
|
||||
Reference in New Issue
Block a user