Checkpoint page support solver
This commit is contained in:
@@ -303,7 +303,7 @@ function simulatePageLines(bundleCount, pageWidth, spineWidth) {
|
|||||||
sideEntries.forEach((entry, rank) => {
|
sideEntries.forEach((entry, rank) => {
|
||||||
const anchor = spineCurvePoint(entry.t, spineWidth);
|
const anchor = spineCurvePoint(entry.t, spineWidth);
|
||||||
const target = restingTarget(side, pageWidth, rank, sideEntries.length);
|
const target = restingTarget(side, pageWidth, rank, sideEntries.length);
|
||||||
const points = buildOptimizedSplineLine(anchor, target, lowerLine, side, segments, stepLength, bundleCount);
|
const points = buildSupportSolvedLine(anchor, target, lowerLine, side, segments, stepLength, bundleCount);
|
||||||
const line = { index: entry.index, t: entry.t, side, anchor, points, endpoint: points[points.length - 1] };
|
const line = { index: entry.index, t: entry.t, side, anchor, points, endpoint: points[points.length - 1] };
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
lowerLine = line;
|
lowerLine = line;
|
||||||
@@ -323,7 +323,7 @@ function measureLineLengthError(lines, pageWidth) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function measureStackSpacingError(lines) {
|
function measureStackSpacingError(lines) {
|
||||||
let maxError = 0;
|
let maxViolation = 0;
|
||||||
[-1, 1].forEach((side) => {
|
[-1, 1].forEach((side) => {
|
||||||
const sideLines = lines
|
const sideLines = lines
|
||||||
.filter((line) => line.side === side)
|
.filter((line) => line.side === side)
|
||||||
@@ -332,12 +332,13 @@ function measureStackSpacingError(lines) {
|
|||||||
const lower = sideLines[row - 1];
|
const lower = sideLines[row - 1];
|
||||||
const upper = sideLines[row];
|
const upper = sideLines[row];
|
||||||
for (let col = 1; col < upper.points.length; col += 1) {
|
for (let col = 1; col < upper.points.length; col += 1) {
|
||||||
const distance = Math.hypot(upper.points[col].x - lower.points[col].x, upper.points[col].y - lower.points[col].y);
|
const closest = closestPointOnPolyline(upper.points[col], lower.points);
|
||||||
maxError = Math.max(maxError, Math.abs(distance - BOOK_PROFILE.bundleSpacing));
|
const distance = Math.hypot(upper.points[col].x - closest.x, upper.points[col].y - closest.y);
|
||||||
|
maxViolation = Math.max(maxViolation, Math.max(0, BOOK_PROFILE.bundleSpacing - distance));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return maxError;
|
return maxViolation;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sampleSpineByArc(count, spineWidth) {
|
function sampleSpineByArc(count, spineWidth) {
|
||||||
@@ -391,121 +392,64 @@ function restingTarget(side, pageWidth, rank, sideCount) {
|
|||||||
return { x, y };
|
return { x, y };
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOptimizedSplineLine(anchor, target, lowerLine, side, segments, stepLength, bundleCount) {
|
function buildSupportSolvedLine(anchor, target, lowerLine, side, segments, stepLength, bundleCount) {
|
||||||
const points = initialSplineGuess(anchor, target, lowerLine, side, segments, bundleCount);
|
const points = [{ x: anchor.x, y: anchor.y }];
|
||||||
const iterations = lowerLine ? 120 : 90;
|
let tangent = coverTangentAtX(anchor.x, side);
|
||||||
for (let iteration = 0; iteration < iterations; iteration += 1) {
|
for (let index = 1; index <= segments; index += 1) {
|
||||||
smoothSplineCurvature(points, iteration < iterations * 0.65 ? 0.34 : 0.14);
|
const u = index / segments;
|
||||||
attractSplineToSupport(points, lowerLine, side, bundleCount, lowerLine ? 0.46 : 0.18);
|
const supportTangent = lowerLine ? lineTangentAt(lowerLine.points, index) : coverTangentAtX(points[index - 1].x, side);
|
||||||
attractEndpoint(points, target, 0.08);
|
const point = chooseClosestSupportedPoint(points[index - 1], tangent, supportTangent, target, lowerLine, index, side, stepLength, bundleCount, u);
|
||||||
enforceOutwardProgress(points, side);
|
points.push(point);
|
||||||
enforceLineLength(points, anchor, stepLength, 5);
|
tangent = normalizedVector(point.x - points[index - 1].x, point.y - points[index - 1].y);
|
||||||
keepSplineOnSupport(points, lowerLine, side, bundleCount);
|
|
||||||
enforceLineLength(points, anchor, stepLength, 3);
|
|
||||||
}
|
}
|
||||||
smoothSplineCurvature(points, 0.08);
|
|
||||||
enforceLineLength(points, anchor, stepLength, 12);
|
|
||||||
keepSplineOnSupport(points, lowerLine, side, bundleCount);
|
|
||||||
enforceLineLength(points, anchor, stepLength, 12);
|
|
||||||
enforceForwardLineLength(points, anchor, stepLength);
|
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initialSplineGuess(anchor, target, lowerLine, side, segments, bundleCount) {
|
function chooseClosestSupportedPoint(previous, tangent, supportTangent, target, lowerLine, index, side, stepLength, bundleCount, u) {
|
||||||
const points = [];
|
const blendTangent = normalizedVector(tangent.x + supportTangent.x * 2, tangent.y + supportTangent.y * 2);
|
||||||
for (let i = 0; i <= segments; i += 1) {
|
const angleHint = Math.atan2(blendTangent.y, blendTangent.x);
|
||||||
const u = i / segments;
|
let best = null;
|
||||||
|
let fallback = null;
|
||||||
|
for (let sample = 0; sample < 720; sample += 1) {
|
||||||
|
const angle = sample / 720 * Math.PI * 2;
|
||||||
|
const candidate = {
|
||||||
|
x: previous.x + Math.cos(angle) * stepLength,
|
||||||
|
y: previous.y + Math.sin(angle) * stepLength
|
||||||
|
};
|
||||||
|
const score = scoreSupportedPoint(candidate, previous, tangent, supportTangent, angle, angleHint, target, lowerLine, index, side, bundleCount, u);
|
||||||
|
if (best === null || score < best.score) best = { point: candidate, score };
|
||||||
|
const fallbackScore = scoreSupportedPoint(candidate, previous, tangent, supportTangent, angle, angleHint, target, lowerLine, index, side, bundleCount, u, true);
|
||||||
|
if (fallback === null || fallbackScore < fallback.score) fallback = { point: candidate, score: fallbackScore };
|
||||||
|
}
|
||||||
|
return Number.isFinite(best?.score) ? best.point : fallback.point;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreSupportedPoint(candidate, previous, tangent, supportTangent, angle, angleHint, target, lowerLine, index, side, bundleCount, u, allowViolation = false) {
|
||||||
|
const backward = Math.max(0, side * (previous.x - candidate.x));
|
||||||
|
if (!allowViolation && backward > 0.00001) return Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
let supportError;
|
||||||
|
let supportViolation = 0;
|
||||||
if (lowerLine) {
|
if (lowerLine) {
|
||||||
const lowerPoint = lowerLine.points[i];
|
const closest = closestPointOnPolyline(candidate, lowerLine.points);
|
||||||
const normal = upwardNormalAt(lowerLine.points, i);
|
const closestDistance = Math.hypot(candidate.x - closest.x, candidate.y - closest.y);
|
||||||
const anchorBlend = Math.pow(1 - u, 2.2);
|
supportViolation = Math.max(0, BOOK_PROFILE.bundleSpacing - closestDistance) + Math.max(0, closest.y - candidate.y);
|
||||||
points.push({
|
if (!allowViolation && supportViolation > 0.00001) return Number.POSITIVE_INFINITY;
|
||||||
x: lowerPoint.x + normal.x * BOOK_PROFILE.bundleSpacing + (anchor.x - (lowerLine.anchor.x + normal.x * BOOK_PROFILE.bundleSpacing)) * anchorBlend,
|
supportError = closestDistance - BOOK_PROFILE.bundleSpacing;
|
||||||
y: lowerPoint.y + normal.y * BOOK_PROFILE.bundleSpacing + (anchor.y - (lowerLine.anchor.y + normal.y * BOOK_PROFILE.bundleSpacing)) * anchorBlend
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const crown = 0.026 * Math.sin(Math.PI * u) * Math.pow(u, 0.7);
|
const floor = coverTopYAtX(candidate.x) + coverClearance(bundleCount);
|
||||||
points.push({
|
supportViolation = Math.max(0, floor - candidate.y);
|
||||||
x: THREE.MathUtils.lerp(anchor.x, target.x, u),
|
if (!allowViolation && supportViolation > 0.00001) return Number.POSITIVE_INFINITY;
|
||||||
y: THREE.MathUtils.lerp(anchor.y, target.y, u) + crown
|
supportError = candidate.y - floor;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
points[i].x = side < 0 ? Math.min(points[i].x, -0.01) : Math.max(points[i].x, 0.01);
|
|
||||||
points[i].y = Math.max(points[i].y, coverTopYAtX(points[i].x) + coverClearance(bundleCount));
|
|
||||||
}
|
|
||||||
points[0].x = anchor.x;
|
|
||||||
points[0].y = anchor.y;
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
|
|
||||||
function smoothSplineCurvature(points, strength) {
|
const candidateTangent = normalizedVector(candidate.x - previous.x, candidate.y - previous.y);
|
||||||
const nextPoints = points.map((point) => ({ x: point.x, y: point.y }));
|
const bend = 1 - Math.max(-1, Math.min(1, candidateTangent.x * tangent.x + candidateTangent.y * tangent.y));
|
||||||
for (let i = 1; i < points.length - 1; i += 1) {
|
const supportAlignment = 1 - Math.max(-1, Math.min(1, candidateTangent.x * supportTangent.x + candidateTangent.y * supportTangent.y));
|
||||||
const midpointX = (points[i - 1].x + points[i + 1].x) * 0.5;
|
const angleDelta = Math.abs(Math.atan2(Math.sin(angle - angleHint), Math.cos(angle - angleHint)));
|
||||||
const midpointY = (points[i - 1].y + points[i + 1].y) * 0.5;
|
const outwardTarget = Math.max(0, side * (target.x - candidate.x));
|
||||||
nextPoints[i].x = THREE.MathUtils.lerp(points[i].x, midpointX, strength);
|
const targetHeight = Math.abs(candidate.y - target.y);
|
||||||
nextPoints[i].y = THREE.MathUtils.lerp(points[i].y, midpointY, strength);
|
return Math.abs(supportError) * 1200 + supportViolation * 100000 + backward * 100000 + supportAlignment * 0.85 + bend * 0.22 + angleDelta * 0.04 + outwardTarget * 0.01 + targetHeight * 0.006;
|
||||||
}
|
|
||||||
for (let i = 1; i < points.length - 1; i += 1) {
|
|
||||||
points[i].x = nextPoints[i].x;
|
|
||||||
points[i].y = nextPoints[i].y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function attractSplineToSupport(points, lowerLine, side, bundleCount, strength) {
|
|
||||||
for (let i = 1; i < points.length; i += 1) {
|
|
||||||
if (lowerLine) {
|
|
||||||
const support = lowerLine.points[i];
|
|
||||||
const normal = upwardNormalAt(lowerLine.points, i);
|
|
||||||
const targetX = support.x + normal.x * BOOK_PROFILE.bundleSpacing;
|
|
||||||
const targetY = support.y + normal.y * BOOK_PROFILE.bundleSpacing;
|
|
||||||
points[i].x = THREE.MathUtils.lerp(points[i].x, targetX, strength);
|
|
||||||
points[i].y = THREE.MathUtils.lerp(points[i].y, targetY, strength);
|
|
||||||
} else {
|
|
||||||
const floor = coverTopYAtX(points[i].x) + coverClearance(bundleCount);
|
|
||||||
points[i].y = THREE.MathUtils.lerp(points[i].y, floor, strength * 0.12);
|
|
||||||
}
|
|
||||||
points[i].x = side < 0 ? Math.min(points[i].x, -0.01) : Math.max(points[i].x, 0.01);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function attractEndpoint(points, target, strength) {
|
|
||||||
const end = points[points.length - 1];
|
|
||||||
end.x = THREE.MathUtils.lerp(end.x, target.x, strength);
|
|
||||||
end.y = THREE.MathUtils.lerp(end.y, target.y, strength);
|
|
||||||
}
|
|
||||||
|
|
||||||
function enforceOutwardProgress(points, side) {
|
|
||||||
for (let i = 1; i < points.length; i += 1) {
|
|
||||||
if (side < 0) {
|
|
||||||
points[i].x = Math.min(points[i].x, points[i - 1].x - 0.0005);
|
|
||||||
} else {
|
|
||||||
points[i].x = Math.max(points[i].x, points[i - 1].x + 0.0005);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function keepSplineOnSupport(points, lowerLine, side, bundleCount) {
|
|
||||||
for (let i = 1; i < points.length; i += 1) {
|
|
||||||
points[i].x = side < 0 ? Math.min(points[i].x, -0.01) : Math.max(points[i].x, 0.01);
|
|
||||||
points[i].y = Math.max(points[i].y, coverTopYAtX(points[i].x) + coverClearance(bundleCount));
|
|
||||||
if (lowerLine) {
|
|
||||||
const support = lowerLine.points[i];
|
|
||||||
const normal = upwardNormalAt(lowerLine.points, i);
|
|
||||||
const dx = points[i].x - support.x;
|
|
||||||
const dy = points[i].y - support.y;
|
|
||||||
const normalDistance = dx * normal.x + dy * normal.y;
|
|
||||||
if (normalDistance < BOOK_PROFILE.bundleSpacing) {
|
|
||||||
points[i].x += normal.x * (BOOK_PROFILE.bundleSpacing - normalDistance);
|
|
||||||
points[i].y += normal.y * (BOOK_PROFILE.bundleSpacing - normalDistance);
|
|
||||||
}
|
|
||||||
const nearest = closestPointOnPolyline(points[i], lowerLine.points);
|
|
||||||
const nearestDistance = Math.hypot(points[i].x - nearest.x, points[i].y - nearest.y);
|
|
||||||
if (nearestDistance < BOOK_PROFILE.bundleSpacing * 0.72) {
|
|
||||||
points[i].y += BOOK_PROFILE.bundleSpacing * 0.72 - nearestDistance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closestPointOnPolyline(point, polyline) {
|
function closestPointOnPolyline(point, polyline) {
|
||||||
@@ -533,6 +477,24 @@ function closestPointOnSegment(point, a, b) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function coverTangentAtX(x, side) {
|
||||||
|
const delta = 0.002;
|
||||||
|
const y0 = coverTopYAtX(x - delta);
|
||||||
|
const y1 = coverTopYAtX(x + delta);
|
||||||
|
return normalizedVector(side * delta * 2, y1 - y0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineTangentAt(points, index) {
|
||||||
|
const previous = points[Math.max(0, index - 1)];
|
||||||
|
const next = points[Math.min(points.length - 1, index + 1)];
|
||||||
|
return normalizedVector(next.x - previous.x, next.y - previous.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedVector(x, y) {
|
||||||
|
const length = Math.hypot(x, y) || 0.0001;
|
||||||
|
return { x: x / length, y: y / length };
|
||||||
|
}
|
||||||
|
|
||||||
function relaxPageLine(points, anchor, stepLength, side, local, bundleCount) {
|
function relaxPageLine(points, anchor, stepLength, side, local, bundleCount) {
|
||||||
const gravity = 0.00072;
|
const gravity = 0.00072;
|
||||||
const stackPressure = 0.0011 * (1 - local);
|
const stackPressure = 0.0011 * (1 - local);
|
||||||
@@ -717,7 +679,7 @@ function addSimulatedPageLines(lines, depth) {
|
|||||||
const rightMaterial = new THREE.LineBasicMaterial({ color: 0x9a8058, transparent: true, opacity: 0.72 });
|
const rightMaterial = new THREE.LineBasicMaterial({ color: 0x9a8058, transparent: true, opacity: 0.72 });
|
||||||
const z = depth * 0.5 + 0.006;
|
const z = depth * 0.5 + 0.006;
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
const points = smoothLinePoints(line.points, 4).map((point) => new THREE.Vector3(point.x, point.y, z));
|
const points = line.points.map((point) => new THREE.Vector3(point.x, point.y, z));
|
||||||
book.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), line.side < 0 ? leftMaterial : rightMaterial));
|
book.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), line.side < 0 ? leftMaterial : rightMaterial));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -735,7 +697,7 @@ function addSimulatedStackBodies(lines, depth) {
|
|||||||
function createLoftedLineBody(lines, depth) {
|
function createLoftedLineBody(lines, depth) {
|
||||||
const positions = [];
|
const positions = [];
|
||||||
const indices = [];
|
const indices = [];
|
||||||
const smoothLines = lines.map((line) => smoothLinePoints(line.points, 4));
|
const smoothLines = lines.map((line) => line.points);
|
||||||
const push = (point, z) => {
|
const push = (point, z) => {
|
||||||
const index = positions.length / 3;
|
const index = positions.length / 3;
|
||||||
positions.push(point.x, point.y, z);
|
positions.push(point.x, point.y, z);
|
||||||
@@ -780,31 +742,6 @@ function createEndpointPolyline(lines, depth) {
|
|||||||
return new THREE.BufferGeometry().setFromPoints(points);
|
return new THREE.BufferGeometry().setFromPoints(points);
|
||||||
}
|
}
|
||||||
|
|
||||||
function smoothLinePoints(points, subdivisions) {
|
|
||||||
const result = [];
|
|
||||||
for (let i = 0; i < points.length - 1; i += 1) {
|
|
||||||
const p0 = points[Math.max(0, i - 1)];
|
|
||||||
const p1 = points[i];
|
|
||||||
const p2 = points[i + 1];
|
|
||||||
const p3 = points[Math.min(points.length - 1, i + 2)];
|
|
||||||
for (let step = 0; step < subdivisions; step += 1) {
|
|
||||||
const t = step / subdivisions;
|
|
||||||
result.push(catmullRomPoint(p0, p1, p2, p3, t));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.push(points[points.length - 1]);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function catmullRomPoint(p0, p1, p2, p3, t) {
|
|
||||||
const tt = t * t;
|
|
||||||
const ttt = tt * t;
|
|
||||||
return {
|
|
||||||
x: 0.5 * ((2 * p1.x) + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * tt + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * ttt),
|
|
||||||
y: 0.5 * ((2 * p1.y) + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * tt + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * ttt)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
const width = window.innerWidth;
|
const width = window.innerWidth;
|
||||||
const height = window.innerHeight;
|
const height = window.innerHeight;
|
||||||
|
|||||||
Reference in New Issue
Block a user