2026-04-20 11:31:17 +02:00
import type { ElementBounds } from "@/preview/element-bounds" ;
2026-04-15 15:45:50 +02:00
2026-04-29 18:41:15 +02:00
export interface FreeformPathPoint {
2026-04-15 15:45:50 +02:00
id : string ;
x : number ;
y : number ;
inX : number ;
inY : number ;
outX : number ;
outY : number ;
}
2026-04-29 18:41:15 +02:00
function isFreeformPathPoint ( value : unknown ) : value is FreeformPathPoint {
2026-04-15 15:45:50 +02:00
if ( ! value || typeof value !== "object" ) {
return false ;
}
return (
2026-04-29 18:31:24 +02:00
"id" in value &&
typeof value . id === "string" &&
"x" in value &&
typeof value . x === "number" &&
"y" in value &&
typeof value . y === "number" &&
"inX" in value &&
typeof value . inX === "number" &&
"inY" in value &&
typeof value . inY === "number" &&
"outX" in value &&
typeof value . outX === "number" &&
"outY" in value &&
typeof value . outY === "number"
2026-04-15 15:45:50 +02:00
);
}
2026-04-29 18:41:15 +02:00
export function parseFreeformPath ({
2026-04-15 15:45:50 +02:00
path ,
} : {
path : string ;
2026-04-29 18:41:15 +02:00
}) : FreeformPathPoint [] {
2026-04-15 15:45:50 +02:00
if ( ! path ) {
return [];
}
try {
const parsed = JSON . parse ( path );
2026-04-29 18:41:15 +02:00
return Array . isArray ( parsed ) ? parsed . filter ( isFreeformPathPoint ) : [];
2026-04-15 15:45:50 +02:00
} catch {
return [];
}
}
2026-04-29 18:41:15 +02:00
export function serializeFreeformPath ({
2026-04-15 15:45:50 +02:00
points ,
} : {
2026-04-29 18:41:15 +02:00
points : FreeformPathPoint [];
2026-04-15 15:45:50 +02:00
}) : string {
return JSON . stringify ( points );
}
2026-04-29 18:41:15 +02:00
export function removeFreeformPathPoints ({
2026-04-15 15:45:50 +02:00
points ,
pointIds ,
} : {
2026-04-29 18:41:15 +02:00
points : FreeformPathPoint [];
2026-04-15 15:45:50 +02:00
pointIds : string [];
2026-04-29 18:41:15 +02:00
}) : FreeformPathPoint [] {
2026-04-15 15:45:50 +02:00
if ( pointIds . length === 0 ) {
return points ;
}
const pointIdsToRemove = new Set ( pointIds );
return points . filter (( point ) => ! pointIdsToRemove . has ( point . id ));
}
2026-04-29 18:41:15 +02:00
export function getFreeformPathClosedStateAfterPointRemoval ({
2026-04-15 15:45:50 +02:00
wasClosed ,
remainingPointCount ,
} : {
wasClosed : boolean ;
remainingPointCount : number ;
}) : boolean {
return wasClosed && remainingPointCount >= 3 ;
}
function rotatePoint ({
x ,
y ,
rotationDegrees ,
} : {
x : number ;
y : number ;
rotationDegrees : number ;
}) : { x : number ; y : number } {
const angleRad = ( rotationDegrees * Math . PI ) / 180 ;
const cos = Math . cos ( angleRad );
const sin = Math . sin ( angleRad );
return {
x : x * cos - y * sin ,
y : x * sin + y * cos ,
};
}
2026-04-29 18:41:15 +02:00
export function getFreeformCenterCanvasPoint ({
2026-04-15 15:45:50 +02:00
centerX ,
centerY ,
bounds ,
} : {
centerX : number ;
centerY : number ;
bounds : ElementBounds ;
}) : { x : number ; y : number } {
return {
x : bounds.cx + centerX * bounds . width ,
y : bounds.cy + centerY * bounds . height ,
};
}
2026-04-29 18:41:15 +02:00
export function freeformLocalPointToCanvas ({
2026-04-15 15:45:50 +02:00
point ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
} : {
point : { x : number ; y : number };
centerX : number ;
centerY : number ;
rotation : number ;
scale : number ;
bounds : ElementBounds ;
}) : { x : number ; y : number } {
2026-04-29 18:41:15 +02:00
const center = getFreeformCenterCanvasPoint ({ centerX , centerY , bounds });
2026-04-15 15:45:50 +02:00
const scaledLocal = {
x : point.x * bounds . width * scale ,
y : point.y * bounds . height * scale ,
};
const rotated = rotatePoint ({
x : scaledLocal.x ,
y : scaledLocal.y ,
rotationDegrees : rotation ,
});
return {
x : center.x + rotated . x ,
y : center.y + rotated . y ,
};
}
2026-04-29 18:41:15 +02:00
export function freeformCanvasPointToLocal ({
2026-04-15 15:45:50 +02:00
point ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
} : {
point : { x : number ; y : number };
centerX : number ;
centerY : number ;
rotation : number ;
scale : number ;
bounds : ElementBounds ;
}) : { x : number ; y : number } {
2026-04-29 18:41:15 +02:00
const center = getFreeformCenterCanvasPoint ({ centerX , centerY , bounds });
2026-04-15 15:45:50 +02:00
const translated = {
x : point.x - center . x ,
y : point.y - center . y ,
};
const rotated = rotatePoint ({
x : translated.x ,
y : translated.y ,
rotationDegrees : - rotation ,
});
return {
x : bounds.width === 0 ? 0 : rotated.x / ( bounds . width * scale ),
y : bounds.height === 0 ? 0 : rotated.y / ( bounds . height * scale ),
};
}
2026-04-29 18:41:15 +02:00
export function getFreeformCanvasGeometry ({
2026-04-15 15:45:50 +02:00
points ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
} : {
2026-04-29 18:41:15 +02:00
points : FreeformPathPoint [];
2026-04-15 15:45:50 +02:00
centerX : number ;
centerY : number ;
rotation : number ;
scale : number ;
bounds : ElementBounds ;
}) {
const anchors = points . map (( point ) => ({
id : point.id ,
2026-04-29 18:41:15 +02:00
anchor : freeformLocalPointToCanvas ({
2026-04-15 15:45:50 +02:00
point : { x : point.x , y : point.y },
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
}),
2026-04-29 18:41:15 +02:00
inHandle : freeformLocalPointToCanvas ({
2026-04-15 15:45:50 +02:00
point : { x : point.x + point . inX , y : point.y + point . inY },
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
}),
2026-04-29 18:41:15 +02:00
outHandle : freeformLocalPointToCanvas ({
2026-04-15 15:45:50 +02:00
point : { x : point.x + point . outX , y : point.y + point . outY },
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
}),
}));
const geometryPoints = anchors . flatMap (( point ) => [
point . anchor ,
point . inHandle ,
point . outHandle ,
]);
if ( geometryPoints . length === 0 ) {
return {
anchors ,
bounds : null ,
};
}
const geometryBounds = getCanvasPointBounds ({
points : geometryPoints ,
});
return {
anchors ,
bounds : geometryBounds ,
};
}
export interface CanvasPoint {
x : number ;
y : number ;
}
function getCanvasPointBounds ({ points } : { points : CanvasPoint [] }) : {
minX : number ;
maxX : number ;
minY : number ;
maxY : number ;
width : number ;
height : number ;
centerX : number ;
centerY : number ;
} | null {
if ( points . length === 0 ) {
return null ;
}
const [ firstPoint , ... restPoints ] = points ;
let minX = firstPoint . x ;
let maxX = firstPoint . x ;
let minY = firstPoint . y ;
let maxY = firstPoint . y ;
for ( const point of restPoints ) {
minX = Math . min ( minX , point . x );
maxX = Math . max ( maxX , point . x );
minY = Math . min ( minY , point . y );
maxY = Math . max ( maxY , point . y );
}
return {
minX ,
maxX ,
minY ,
maxY ,
width : Math.max ( 1 , maxX - minX ),
height : Math.max ( 1 , maxY - minY ),
centerX : ( minX + maxX ) / 2 ,
centerY : ( minY + maxY ) / 2 ,
};
}
2026-04-29 18:41:15 +02:00
export interface FreeformCanvasSegment {
2026-04-15 15:45:50 +02:00
index : number ;
startPointId : string ;
endPointId : string ;
start : CanvasPoint ;
startOut : CanvasPoint ;
endIn : CanvasPoint ;
end : CanvasPoint ;
pathData : string ;
}
function clampUnit ( value : number ) : number {
return Math . min ( 1 , Math . max ( 0 , value ));
}
2026-04-29 00:37:56 +02:00
function getDistanceSquared ({
a ,
b ,
} : {
a : CanvasPoint ;
b : CanvasPoint ;
}) : number {
2026-04-15 15:45:50 +02:00
const dx = a . x - b . x ;
const dy = a . y - b . y ;
return dx * dx + dy * dy ;
}
2026-04-29 00:37:56 +02:00
function lerpPoint ({
a ,
b ,
t ,
} : {
a : CanvasPoint ;
b : CanvasPoint ;
t : number ;
}) : CanvasPoint {
2026-04-15 15:45:50 +02:00
return {
x : a.x + ( b . x - a . x ) * t ,
y : a.y + ( b . y - a . y ) * t ,
};
}
function evaluateCubicBezier ({
p0 ,
p1 ,
p2 ,
p3 ,
t ,
} : {
p0 : CanvasPoint ;
p1 : CanvasPoint ;
p2 : CanvasPoint ;
p3 : CanvasPoint ;
t : number ;
}) : CanvasPoint {
const oneMinusT = 1 - t ;
return {
x :
oneMinusT ** 3 * p0 . x +
3 * oneMinusT ** 2 * t * p1 . x +
3 * oneMinusT * t ** 2 * p2 . x +
t ** 3 * p3 . x ,
y :
oneMinusT ** 3 * p0 . y +
3 * oneMinusT ** 2 * t * p1 . y +
3 * oneMinusT * t ** 2 * p2 . y +
t ** 3 * p3 . y ,
};
}
2026-04-29 18:41:15 +02:00
function getFreeformSegmentIndices ({
2026-04-15 15:45:50 +02:00
points ,
segmentIndex ,
closed ,
} : {
2026-04-29 18:41:15 +02:00
points : FreeformPathPoint [];
2026-04-15 15:45:50 +02:00
segmentIndex : number ;
closed : boolean ;
}) : { startIndex : number ; endIndex : number } | null {
2026-04-29 18:41:15 +02:00
const segmentCount = getFreeformSegmentCount ({ points , closed });
2026-04-15 15:45:50 +02:00
if ( segmentIndex < 0 || segmentIndex >= segmentCount ) {
return null ;
}
return {
startIndex : segmentIndex ,
endIndex : ( segmentIndex + 1 ) % points . length ,
};
}
2026-04-29 18:41:15 +02:00
export function getFreeformSegmentCount ({
2026-04-15 15:45:50 +02:00
points ,
closed ,
} : {
2026-04-29 18:41:15 +02:00
points : FreeformPathPoint [];
2026-04-15 15:45:50 +02:00
closed : boolean ;
}) : number {
if ( points . length < 2 ) {
return 0 ;
}
return closed ? points.length : points.length - 1 ;
}
2026-04-29 18:41:15 +02:00
export function getFreeformCanvasSegments ({
2026-04-15 15:45:50 +02:00
points ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
closed ,
} : {
2026-04-29 18:41:15 +02:00
points : FreeformPathPoint [];
2026-04-15 15:45:50 +02:00
centerX : number ;
centerY : number ;
rotation : number ;
scale : number ;
bounds : ElementBounds ;
closed : boolean ;
2026-04-29 18:41:15 +02:00
}) : FreeformCanvasSegment [] {
const geometry = getFreeformCanvasGeometry ({
2026-04-15 15:45:50 +02:00
points ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
});
2026-04-29 18:41:15 +02:00
const segmentCount = getFreeformSegmentCount ({ points , closed });
2026-04-15 15:45:50 +02:00
return Array . from ({ length : segmentCount }, ( _ , segmentIndex ) => {
const start = geometry . anchors [ segmentIndex ];
const end = geometry . anchors [( segmentIndex + 1 ) % geometry . anchors . length ];
return {
index : segmentIndex ,
startPointId : start.id ,
endPointId : end.id ,
start : start.anchor ,
startOut : start.outHandle ,
endIn : end.inHandle ,
end : end.anchor ,
pathData : `M ${ start . anchor . x } , ${ start . anchor . y } C ${ start . outHandle . x } , ${ start . outHandle . y } ${ end . inHandle . x } , ${ end . inHandle . y } ${ end . anchor . x } , ${ end . anchor . y } ` ,
};
});
}
2026-04-29 18:41:15 +02:00
export function findClosestPointOnFreeformSegment ({
2026-04-15 15:45:50 +02:00
points ,
segmentIndex ,
canvasPoint ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
closed ,
} : {
2026-04-29 18:41:15 +02:00
points : FreeformPathPoint [];
2026-04-15 15:45:50 +02:00
segmentIndex : number ;
canvasPoint : CanvasPoint ;
centerX : number ;
centerY : number ;
rotation : number ;
scale : number ;
bounds : ElementBounds ;
closed : boolean ;
}) : { t : number ; point : CanvasPoint } | null {
2026-04-29 18:41:15 +02:00
const segment = getFreeformCanvasSegments ({
2026-04-15 15:45:50 +02:00
points ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
closed ,
}). find (( candidate ) => candidate . index === segmentIndex );
if ( ! segment ) {
return null ;
}
const sampleCount = 24 ;
let bestT = 0 ;
2026-04-29 00:37:56 +02:00
let bestDistanceSquared = getDistanceSquared ({
a : canvasPoint ,
b : segment.start ,
});
2026-04-15 15:45:50 +02:00
for ( let step = 0 ; step <= sampleCount ; step ++ ) {
const t = step / sampleCount ;
const point = evaluateCubicBezier ({
p0 : segment.start ,
p1 : segment.startOut ,
p2 : segment.endIn ,
p3 : segment.end ,
t ,
});
2026-04-29 00:37:56 +02:00
const distanceSquared = getDistanceSquared ({ a : canvasPoint , b : point });
2026-04-15 15:45:50 +02:00
if ( distanceSquared < bestDistanceSquared ) {
bestDistanceSquared = distanceSquared ;
bestT = t ;
}
}
let searchStep = 1 / sampleCount ;
for ( let iteration = 0 ; iteration < 8 ; iteration ++ ) {
const candidates = [ bestT - searchStep , bestT , bestT + searchStep ]
. map ( clampUnit )
. map (( t ) => ({
t ,
point : evaluateCubicBezier ({
p0 : segment.start ,
p1 : segment.startOut ,
p2 : segment.endIn ,
p3 : segment.end ,
t ,
}),
}));
for ( const candidate of candidates ) {
2026-04-29 00:37:56 +02:00
const distanceSquared = getDistanceSquared ({
a : canvasPoint ,
b : candidate.point ,
});
2026-04-15 15:45:50 +02:00
if ( distanceSquared < bestDistanceSquared ) {
bestDistanceSquared = distanceSquared ;
bestT = candidate . t ;
}
}
searchStep /= 2 ;
}
const clampedT = Math . min ( 0.999 , Math . max ( 0.001 , bestT ));
return {
t : clampedT ,
point : evaluateCubicBezier ({
p0 : segment.start ,
p1 : segment.startOut ,
p2 : segment.endIn ,
p3 : segment.end ,
t : clampedT ,
}),
};
}
2026-04-29 18:41:15 +02:00
export function insertPointIntoFreeformSegment ({
2026-04-15 15:45:50 +02:00
points ,
segmentIndex ,
pointId ,
t ,
closed ,
} : {
2026-04-29 18:41:15 +02:00
points : FreeformPathPoint [];
2026-04-15 15:45:50 +02:00
segmentIndex : number ;
pointId : string ;
t : number ;
closed : boolean ;
2026-04-29 18:41:15 +02:00
}) : FreeformPathPoint [] {
const indices = getFreeformSegmentIndices ({
2026-04-15 15:45:50 +02:00
points ,
segmentIndex ,
closed ,
});
if ( ! indices ) {
return points ;
}
const startPoint = points [ indices . startIndex ];
const endPoint = points [ indices . endIndex ];
const clampedT = Math . min ( 0.999 , Math . max ( 0.001 , t ));
const p0 = { x : startPoint.x , y : startPoint.y };
const p1 = {
x : startPoint.x + startPoint . outX ,
y : startPoint.y + startPoint . outY ,
};
const p2 = {
x : endPoint.x + endPoint . inX ,
y : endPoint.y + endPoint . inY ,
};
const p3 = { x : endPoint.x , y : endPoint.y };
2026-04-29 00:37:56 +02:00
const p01 = lerpPoint ({ a : p0 , b : p1 , t : clampedT });
const p12 = lerpPoint ({ a : p1 , b : p2 , t : clampedT });
const p23 = lerpPoint ({ a : p2 , b : p3 , t : clampedT });
const p012 = lerpPoint ({ a : p01 , b : p12 , t : clampedT });
const p123 = lerpPoint ({ a : p12 , b : p23 , t : clampedT });
const splitPoint = lerpPoint ({ a : p012 , b : p123 , t : clampedT });
2026-04-15 15:45:50 +02:00
const nextPoints = [... points ];
nextPoints [ indices . startIndex ] = {
... startPoint ,
outX : p01.x - startPoint . x ,
outY : p01.y - startPoint . y ,
};
nextPoints [ indices . endIndex ] = {
... endPoint ,
inX : p23.x - endPoint . x ,
inY : p23.y - endPoint . y ,
};
nextPoints . splice ( indices . endIndex , 0 , {
id : pointId ,
x : splitPoint.x ,
y : splitPoint.y ,
inX : p012.x - splitPoint . x ,
inY : p012.y - splitPoint . y ,
outX : p123.x - splitPoint . x ,
outY : p123.y - splitPoint . y ,
});
return nextPoints ;
}
2026-04-29 18:41:15 +02:00
export function getFreeformLocalBounds ({
2026-04-15 15:45:50 +02:00
points ,
bounds ,
} : {
2026-04-29 18:41:15 +02:00
points : FreeformPathPoint [];
2026-04-15 15:45:50 +02:00
bounds : ElementBounds ;
}) {
if ( points . length === 0 ) {
return null ;
}
const values = points . flatMap (( point ) => [
{ x : point.x * bounds . width , y : point.y * bounds . height },
{
x : ( point . x + point . inX ) * bounds . width ,
y : ( point . y + point . inY ) * bounds . height ,
},
{
x : ( point . x + point . outX ) * bounds . width ,
y : ( point . y + point . outY ) * bounds . height ,
},
]);
const localBounds = getCanvasPointBounds ({
points : values ,
});
if ( ! localBounds ) {
return null ;
}
return {
width : localBounds.width ,
height : localBounds.height ,
};
}
2026-04-29 18:41:15 +02:00
export function recenterFreeformPath ({
2026-04-15 15:45:50 +02:00
points ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
} : {
2026-04-29 18:41:15 +02:00
points : FreeformPathPoint [];
2026-04-15 15:45:50 +02:00
centerX : number ;
centerY : number ;
rotation : number ;
scale : number ;
bounds : ElementBounds ;
}) {
if ( points . length === 0 ) {
return { centerX , centerY , points };
}
2026-04-29 18:41:15 +02:00
const geometry = getFreeformCanvasGeometry ({
2026-04-15 15:45:50 +02:00
points ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
});
if ( ! geometry . bounds ) {
return { centerX , centerY , points };
}
const nextCenterCanvas = {
x : geometry.bounds.centerX ,
y : geometry.bounds.centerY ,
};
const nextCenterLocal = {
x : bounds.width === 0 ? 0 : ( nextCenterCanvas . x - bounds . cx ) / bounds . width ,
y :
bounds.height === 0
? 0
: ( nextCenterCanvas . y - bounds . cy ) / bounds . height ,
};
const nextPoints = geometry . anchors . map (( point ) => {
2026-04-29 18:41:15 +02:00
const anchor = freeformCanvasPointToLocal ({
2026-04-15 15:45:50 +02:00
point : point.anchor ,
centerX : nextCenterLocal.x ,
centerY : nextCenterLocal.y ,
rotation ,
scale ,
bounds ,
});
2026-04-29 18:41:15 +02:00
const inHandle = freeformCanvasPointToLocal ({
2026-04-15 15:45:50 +02:00
point : point.inHandle ,
centerX : nextCenterLocal.x ,
centerY : nextCenterLocal.y ,
rotation ,
scale ,
bounds ,
});
2026-04-29 18:41:15 +02:00
const outHandle = freeformCanvasPointToLocal ({
2026-04-15 15:45:50 +02:00
point : point.outHandle ,
centerX : nextCenterLocal.x ,
centerY : nextCenterLocal.y ,
rotation ,
scale ,
bounds ,
});
return {
id : point.id ,
x : anchor.x ,
y : anchor.y ,
inX : inHandle.x - anchor . x ,
inY : inHandle.y - anchor . y ,
outX : outHandle.x - anchor . x ,
outY : outHandle.y - anchor . y ,
};
});
return {
centerX : nextCenterLocal.x ,
centerY : nextCenterLocal.y ,
points : nextPoints ,
};
}
2026-04-29 18:41:15 +02:00
export function buildFreeformPath2D ({
2026-04-15 15:45:50 +02:00
points ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
closed ,
} : {
2026-04-29 18:41:15 +02:00
points : FreeformPathPoint [];
2026-04-15 15:45:50 +02:00
centerX : number ;
centerY : number ;
rotation : number ;
scale : number ;
bounds : ElementBounds ;
closed : boolean ;
}) : Path2D {
const path = new Path2D ();
if ( points . length === 0 ) {
return path ;
}
2026-04-29 18:41:15 +02:00
const geometry = getFreeformCanvasGeometry ({
2026-04-15 15:45:50 +02:00
points ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
});
const anchors = geometry . anchors ;
path . moveTo ( anchors [ 0 ]. anchor . x , anchors [ 0 ]. anchor . y );
for ( let index = 1 ; index < anchors . length ; index ++ ) {
const previous = anchors [ index - 1 ];
const current = anchors [ index ];
path . bezierCurveTo (
previous . outHandle . x ,
previous . outHandle . y ,
current . inHandle . x ,
current . inHandle . y ,
current . anchor . x ,
current . anchor . y ,
);
}
if ( closed && anchors . length > 1 ) {
const last = anchors [ anchors . length - 1 ];
const first = anchors [ 0 ];
path . bezierCurveTo (
last . outHandle . x ,
last . outHandle . y ,
first . inHandle . x ,
first . inHandle . y ,
first . anchor . x ,
first . anchor . y ,
);
path . closePath ();
}
return path ;
}
2026-04-29 18:41:15 +02:00
export function buildFreeformSvgPath ({
2026-04-15 15:45:50 +02:00
points ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
closed ,
} : {
2026-04-29 18:41:15 +02:00
points : FreeformPathPoint [];
2026-04-15 15:45:50 +02:00
centerX : number ;
centerY : number ;
rotation : number ;
scale : number ;
bounds : ElementBounds ;
closed : boolean ;
}) : string {
if ( points . length === 0 ) {
return "" ;
}
2026-04-29 18:41:15 +02:00
const geometry = getFreeformCanvasGeometry ({
2026-04-15 15:45:50 +02:00
points ,
centerX ,
centerY ,
rotation ,
scale ,
bounds ,
});
const anchors = geometry . anchors ;
const segments = [ `M ${ anchors [ 0 ]. anchor . x } , ${ anchors [ 0 ]. anchor . y } ` ];
for ( let index = 1 ; index < anchors . length ; index ++ ) {
const previous = anchors [ index - 1 ];
const current = anchors [ index ];
segments . push (
`C ${ previous . outHandle . x } , ${ previous . outHandle . y } ${ current . inHandle . x } , ${ current . inHandle . y } ${ current . anchor . x } , ${ current . anchor . y } ` ,
);
}
if ( closed && anchors . length > 1 ) {
const last = anchors [ anchors . length - 1 ];
const first = anchors [ 0 ];
segments . push (
`C ${ last . outHandle . x } , ${ last . outHandle . y } ${ first . inHandle . x } , ${ first . inHandle . y } ${ first . anchor . x } , ${ first . anchor . y } ` ,
);
segments . push ( "Z" );
}
return segments . join ( " " );
}