Skip to content

Commit b57fee3

Browse files
committed
Refactor path subdivision to use cubic segments
Updated path subdivision logic to generate cubic Bezier segments instead of lines, improving curve accuracy and handle placement. Refactored related utility functions and updated SVG interpreter tests to reflect new vertex structure and control points.
1 parent 821af31 commit b57fee3

File tree

4 files changed

+2797
-793
lines changed

4 files changed

+2797
-793
lines changed

src/path.js

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ import {
3434
} from './utils/hit-test.js';
3535
import {
3636
clearHandleComponent,
37+
setHandleComponent,
3738
inheritRelative,
3839
isSegmentCurved,
39-
setSubdivisionHandles,
40+
splitSubdivisionSegment,
4041
applyGlobalSmooth,
4142
applyLocalSmooth,
4243
} from './utils/path.js';
@@ -1202,26 +1203,74 @@ export class Path extends Shape {
12021203
}
12031204

12041205
const isCurve = isSegmentCurved(currentOriginal, prevOriginal);
1205-
const subdivided = getSubdivisions(currentOriginal, prevOriginal, limit);
1206-
const steps = subdivided.length;
1207-
1208-
for (let j = 1; j < steps; j += 1) {
1209-
const anchor = subdivided[j];
1210-
inheritRelative(anchor, prevOriginal);
1211-
if (isCurve) {
1212-
const t = steps ? j / steps : 0;
1213-
setSubdivisionHandles(anchor, prevOriginal, currentOriginal, t);
1214-
anchor.command = Commands.curve;
1206+
1207+
if (isCurve) {
1208+
const subdivided = getSubdivisions(currentOriginal, prevOriginal, limit);
1209+
const steps = subdivided.length;
1210+
const prevClone = points[points.length - 1];
1211+
let startSegment = prevClone.clone();
1212+
let endSegment = currentOriginal.clone();
1213+
let prevCloneRef = prevClone;
1214+
let prevT = 0;
1215+
1216+
if (steps <= 1) {
1217+
const currentClone = currentOriginal.clone();
1218+
points.push(currentClone);
12151219
} else {
1220+
for (let j = 1; j < steps; j += 1) {
1221+
const globalT = j / steps;
1222+
const denom = 1 - prevT;
1223+
const localT =
1224+
denom <= Number.EPSILON ? globalT : (globalT - prevT) / denom;
1225+
1226+
const split = splitSubdivisionSegment(
1227+
startSegment,
1228+
endSegment,
1229+
localT
1230+
);
1231+
1232+
setHandleComponent(
1233+
prevCloneRef,
1234+
'right',
1235+
split.startOut.x - prevCloneRef.x,
1236+
split.startOut.y - prevCloneRef.y
1237+
);
1238+
1239+
const newAnchor = split.anchor;
1240+
points.push(newAnchor);
1241+
1242+
prevCloneRef = newAnchor;
1243+
startSegment = newAnchor.clone();
1244+
prevT = globalT;
1245+
1246+
setHandleComponent(
1247+
endSegment,
1248+
'left',
1249+
split.endIn.x - endSegment.x,
1250+
split.endIn.y - endSegment.y
1251+
);
1252+
}
1253+
1254+
const currentClone = currentOriginal.clone();
1255+
currentClone.controls.left.copy(endSegment.controls.left);
1256+
points.push(currentClone);
1257+
}
1258+
} else {
1259+
const subdivided = getSubdivisions(currentOriginal, prevOriginal, limit);
1260+
1261+
for (let j = 1; j < subdivided.length; j += 1) {
1262+
const anchor = subdivided[j];
1263+
inheritRelative(anchor, prevOriginal);
12161264
clearHandleComponent(anchor, 'left');
12171265
clearHandleComponent(anchor, 'right');
12181266
anchor.command = Commands.line;
1267+
points.push(anchor);
12191268
}
1220-
points.push(anchor);
1269+
1270+
const currentClone = currentOriginal.clone();
1271+
points.push(currentClone);
12211272
}
12221273

1223-
const currentClone = currentOriginal.clone();
1224-
points.push(currentClone);
12251274
prevOriginal = currentOriginal;
12261275

12271276
if (currentOriginal.command === Commands.close) {

src/utils/path.js

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { lerp, mod } from './math.js';
22
import { Commands } from './path-commands.js';
33

44
import { Vector } from '../vector.js';
5+
import { Anchor } from '../anchor.js';
56

67
const EPSILON = Number.EPSILON;
78

@@ -84,50 +85,53 @@ function isSegmentCurved(a, b) {
8485
);
8586
}
8687

87-
function setSubdivisionHandles(anchor, start, end, t) {
88-
const right = start.controls && start.controls.right;
89-
const left = end.controls && end.controls.left;
88+
function lerpPoint(a, b, t) {
89+
return {
90+
x: lerp(a.x, b.x, t),
91+
y: lerp(a.y, b.y, t),
92+
};
93+
}
9094

91-
let x1 = start.x;
92-
let y1 = start.y;
93-
let x2 = (right || start).x;
94-
let y2 = (right || start).y;
95-
let x3 = (left || end).x;
96-
let y3 = (left || end).y;
97-
let x4 = end.x;
98-
let y4 = end.y;
99-
100-
if (right && isRelativeAnchor(start)) {
101-
x2 += start.x;
102-
y2 += start.y;
95+
function getAbsoluteHandle(anchor, side) {
96+
const controls = anchor.controls && anchor.controls[side];
97+
if (!controls) {
98+
return { x: anchor.x, y: anchor.y };
10399
}
104-
105-
if (left && isRelativeAnchor(end)) {
106-
x3 += end.x;
107-
y3 += end.y;
100+
if (isRelativeAnchor(anchor)) {
101+
return { x: anchor.x + controls.x, y: anchor.y + controls.y };
108102
}
103+
return { x: controls.x, y: controls.y };
104+
}
105+
106+
function splitSubdivisionSegment(start, end, t) {
107+
const right = start.controls && start.controls.right;
108+
const left = end.controls && end.controls.left;
109109

110-
const t1x = lerp(x1, x2, t);
111-
const t1y = lerp(y1, y2, t);
112-
const t2x = lerp(x2, x3, t);
113-
const t2y = lerp(y2, y3, t);
114-
const t3x = lerp(x3, x4, t);
115-
const t3y = lerp(y3, y4, t);
110+
const p0 = { x: start.x, y: start.y };
111+
const p1 = right ? getAbsoluteHandle(start, 'right') : { ...p0 };
112+
const p3 = { x: end.x, y: end.y };
113+
const p2 = left ? getAbsoluteHandle(end, 'left') : { ...p3 };
116114

117-
const brx = lerp(t1x, t2x, t);
118-
const bry = lerp(t1y, t2y, t);
119-
const alx = lerp(t2x, t3x, t);
120-
const aly = lerp(t2y, t3y, t);
121-
const px = lerp(brx, alx, t);
122-
const py = lerp(bry, aly, t);
115+
const q0 = lerpPoint(p0, p1, t);
116+
const q1 = lerpPoint(p1, p2, t);
117+
const q2 = lerpPoint(p2, p3, t);
123118

124-
anchor.x = px;
125-
anchor.y = py;
119+
const r0 = lerpPoint(q0, q1, t);
120+
const r1 = lerpPoint(q1, q2, t);
126121

122+
const point = lerpPoint(r0, r1, t);
123+
124+
const anchor = new Anchor(point.x, point.y);
127125
inheritRelative(anchor, start);
126+
setHandleComponent(anchor, 'left', r0.x - point.x, r0.y - point.y);
127+
setHandleComponent(anchor, 'right', r1.x - point.x, r1.y - point.y);
128+
anchor.command = Commands.curve;
128129

129-
setHandleComponent(anchor, 'left', brx - px, bry - py);
130-
setHandleComponent(anchor, 'right', alx - px, aly - py);
130+
return {
131+
anchor,
132+
startOut: q0,
133+
endIn: q2,
134+
};
131135
}
132136

133137
function applyGlobalSmooth(vertices, from, to, closed, loop, asymmetric) {
@@ -332,7 +336,7 @@ export {
332336
updateAnchorCommand,
333337
inheritRelative,
334338
isSegmentCurved,
335-
setSubdivisionHandles,
339+
splitSubdivisionSegment,
336340
applyGlobalSmooth,
337341
applyLocalSmooth,
338342
};

0 commit comments

Comments
 (0)