Checkpoint optimized book shape spline

This commit is contained in:
2026-06-05 01:03:27 +02:00
parent 5a5464e0b4
commit 5283f0007e
+175 -23
View File
@@ -135,7 +135,7 @@ function setPageCount(value) {
function rebuildBook() { function rebuildBook() {
clearGroup(book); clearGroup(book);
const coverDepth = 2.34; const coverDepth = 2.30;
const coverThickness = BOOK_PROFILE.coverThickness; const coverThickness = BOOK_PROFILE.coverThickness;
const pageWidth = 1.62; const pageWidth = 1.62;
const pageDepth = 2.24; const pageDepth = 2.24;
@@ -167,7 +167,7 @@ function addCoverAssembly(pageWidth, depth, thickness, spineWidth) {
} }
function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth) { function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth) {
const overhang = 0.055; const overhang = 0.13;
const spineHalf = spineWidth * 0.5; const spineHalf = spineWidth * 0.5;
const hingeInset = 0.07; const hingeInset = 0.07;
const outerX = pageWidth + overhang; const outerX = pageWidth + overhang;
@@ -276,7 +276,7 @@ function spineCurvePoint(t, spineWidth) {
function simulatePageLines(bundleCount, pageWidth, spineWidth) { function simulatePageLines(bundleCount, pageWidth, spineWidth) {
const lines = []; const lines = [];
const segments = 16; const segments = 24;
const stepLength = pageWidth / segments; const stepLength = pageWidth / segments;
const entries = []; const entries = [];
const spineSamples = sampleSpineByArc(bundleCount, spineWidth); const spineSamples = sampleSpineByArc(bundleCount, spineWidth);
@@ -299,23 +299,16 @@ function simulatePageLines(bundleCount, pageWidth, spineWidth) {
.sort((a, b) => side < 0 ? a.t - b.t : b.t - a.t); .sort((a, b) => side < 0 ? a.t - b.t : b.t - a.t);
if (!sideEntries.length) return; if (!sideEntries.length) return;
const bottomEntry = sideEntries[0]; let lowerLine = null;
const bottomAnchor = spineCurvePoint(bottomEntry.t, spineWidth);
const bottomTarget = restingTarget(side, pageWidth, 0, sideEntries.length);
const bottomPoints = initialPageLine(bottomAnchor, bottomTarget, segments);
relaxPageLine(bottomPoints, bottomAnchor, stepLength, side, 0, bundleCount);
keepPageAboveCover(bottomPoints, side, bundleCount);
sideEntries.forEach((entry, rank) => { sideEntries.forEach((entry, rank) => {
const anchor = spineCurvePoint(entry.t, spineWidth); const anchor = spineCurvePoint(entry.t, spineWidth);
const points = rank === 0 const target = restingTarget(side, pageWidth, rank, sideEntries.length);
? bottomPoints.map((point) => ({ ...point })) const points = buildOptimizedSplineLine(anchor, target, lowerLine, side, segments, stepLength, bundleCount);
: offsetPageLine(bottomPoints, anchor, rank * BOOK_PROFILE.bundleSpacing); const line = { index: entry.index, t: entry.t, side, anchor, points, endpoint: points[points.length - 1] };
keepPageAboveCover(points, side, bundleCount); lines.push(line);
lines.push({ index: entry.index, t: entry.t, side, anchor, points, endpoint: points[points.length - 1] }); lowerLine = line;
}); });
}); });
enforceStackConstraints(lines, stepLength, bundleCount);
return lines; return lines;
} }
@@ -338,7 +331,7 @@ function measureStackSpacingError(lines) {
for (let row = 1; row < sideLines.length; row += 1) { for (let row = 1; row < sideLines.length; row += 1) {
const lower = sideLines[row - 1]; const lower = sideLines[row - 1];
const upper = sideLines[row]; const upper = sideLines[row];
for (let col = 0; 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 distance = Math.hypot(upper.points[col].x - lower.points[col].x, upper.points[col].y - lower.points[col].y);
maxError = Math.max(maxError, Math.abs(distance - BOOK_PROFILE.bundleSpacing)); maxError = Math.max(maxError, Math.abs(distance - BOOK_PROFILE.bundleSpacing));
} }
@@ -398,6 +391,148 @@ function restingTarget(side, pageWidth, rank, sideCount) {
return { x, y }; return { x, y };
} }
function buildOptimizedSplineLine(anchor, target, lowerLine, side, segments, stepLength, bundleCount) {
const points = initialSplineGuess(anchor, target, lowerLine, side, segments, bundleCount);
const iterations = lowerLine ? 120 : 90;
for (let iteration = 0; iteration < iterations; iteration += 1) {
smoothSplineCurvature(points, iteration < iterations * 0.65 ? 0.34 : 0.14);
attractSplineToSupport(points, lowerLine, side, bundleCount, lowerLine ? 0.46 : 0.18);
attractEndpoint(points, target, 0.08);
enforceOutwardProgress(points, side);
enforceLineLength(points, anchor, stepLength, 5);
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;
}
function initialSplineGuess(anchor, target, lowerLine, side, segments, bundleCount) {
const points = [];
for (let i = 0; i <= segments; i += 1) {
const u = i / segments;
if (lowerLine) {
const lowerPoint = lowerLine.points[i];
const normal = upwardNormalAt(lowerLine.points, i);
const anchorBlend = Math.pow(1 - u, 2.2);
points.push({
x: lowerPoint.x + normal.x * BOOK_PROFILE.bundleSpacing + (anchor.x - (lowerLine.anchor.x + normal.x * BOOK_PROFILE.bundleSpacing)) * anchorBlend,
y: lowerPoint.y + normal.y * BOOK_PROFILE.bundleSpacing + (anchor.y - (lowerLine.anchor.y + normal.y * BOOK_PROFILE.bundleSpacing)) * anchorBlend
});
} else {
const crown = 0.026 * Math.sin(Math.PI * u) * Math.pow(u, 0.7);
points.push({
x: THREE.MathUtils.lerp(anchor.x, target.x, u),
y: THREE.MathUtils.lerp(anchor.y, target.y, u) + crown
});
}
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 nextPoints = points.map((point) => ({ x: point.x, y: point.y }));
for (let i = 1; i < points.length - 1; i += 1) {
const midpointX = (points[i - 1].x + points[i + 1].x) * 0.5;
const midpointY = (points[i - 1].y + points[i + 1].y) * 0.5;
nextPoints[i].x = THREE.MathUtils.lerp(points[i].x, midpointX, strength);
nextPoints[i].y = THREE.MathUtils.lerp(points[i].y, midpointY, strength);
}
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) {
let best = polyline[0];
let bestDistance = Number.POSITIVE_INFINITY;
for (let i = 0; i < polyline.length - 1; i += 1) {
const candidate = closestPointOnSegment(point, polyline[i], polyline[i + 1]);
const distance = Math.hypot(point.x - candidate.x, point.y - candidate.y);
if (distance < bestDistance) {
best = candidate;
bestDistance = distance;
}
}
return best;
}
function closestPointOnSegment(point, a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const lengthSquared = dx * dx + dy * dy || 0.0001;
const t = THREE.MathUtils.clamp(((point.x - a.x) * dx + (point.y - a.y) * dy) / lengthSquared, 0, 1);
return {
x: a.x + dx * t,
y: a.y + dy * t
};
}
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);
@@ -464,14 +599,31 @@ function constrainSegment(a, b, length, anchorA) {
b.y -= dy * correction * 0.5; b.y -= dy * correction * 0.5;
} }
function enforceForwardLineLength(points, anchor, stepLength) {
points[0].x = anchor.x;
points[0].y = anchor.y;
for (let i = 1; i < points.length; i += 1) {
const previous = points[i - 1];
const current = points[i];
const dx = current.x - previous.x;
const dy = current.y - previous.y;
const distance = Math.hypot(dx, dy) || 0.0001;
current.x = previous.x + dx / distance * stepLength;
current.y = previous.y + dy / distance * stepLength;
}
}
function keepPageAboveCover(points, side, bundleCount) { function keepPageAboveCover(points, side, bundleCount) {
for (let i = 1; i < points.length; i += 1) { for (let i = 1; i < points.length; i += 1) {
const clearance = BOOK_PROFILE.paperContactOffset + 0.0002 * bundleCount; points[i].y = Math.max(points[i].y, coverTopYAtX(points[i].x) + coverClearance(bundleCount));
points[i].y = Math.max(points[i].y, coverTopYAtX(points[i].x) + clearance);
points[i].x = side < 0 ? Math.min(points[i].x, -0.01) : Math.max(points[i].x, 0.01); points[i].x = side < 0 ? Math.min(points[i].x, -0.01) : Math.max(points[i].x, 0.01);
} }
} }
function coverClearance(bundleCount) {
return BOOK_PROFILE.paperContactOffset + 0.0002 * bundleCount;
}
function enforceStackConstraints(lines, stepLength, bundleCount) { function enforceStackConstraints(lines, stepLength, bundleCount) {
const iterations = 44; const iterations = 44;
[-1, 1].forEach((side) => { [-1, 1].forEach((side) => {
@@ -563,7 +715,7 @@ function currentSpineHalf() {
function addSimulatedPageLines(lines, depth) { function addSimulatedPageLines(lines, depth) {
const leftMaterial = new THREE.LineBasicMaterial({ color: 0x8f7750, transparent: true, opacity: 0.72 }); const leftMaterial = new THREE.LineBasicMaterial({ color: 0x8f7750, transparent: true, opacity: 0.72 });
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.11; 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 = smoothLinePoints(line.points, 4).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));
@@ -589,8 +741,8 @@ function createLoftedLineBody(lines, depth) {
positions.push(point.x, point.y, z); positions.push(point.x, point.y, z);
return index; return index;
}; };
const front = smoothLines.map((points) => points.map((point) => push(point, depth * 0.5 + 0.09))); const front = smoothLines.map((points) => points.map((point) => push(point, depth * 0.5)));
const back = smoothLines.map((points) => points.map((point) => push(point, -depth * 0.5 + 0.09))); const back = smoothLines.map((points) => points.map((point) => push(point, -depth * 0.5)));
for (let row = 0; row < smoothLines.length - 1; row += 1) { for (let row = 0; row < smoothLines.length - 1; row += 1) {
for (let col = 0; col < smoothLines[row].length - 1; col += 1) { for (let col = 0; col < smoothLines[row].length - 1; col += 1) {
indices.push(front[row][col], front[row + 1][col], front[row][col + 1]); indices.push(front[row][col], front[row + 1][col], front[row][col + 1]);
@@ -624,7 +776,7 @@ function createLoftedLineBody(lines, depth) {
} }
function createEndpointPolyline(lines, depth) { function createEndpointPolyline(lines, depth) {
const points = lines.map((line) => new THREE.Vector3(line.endpoint.x, line.endpoint.y, depth * 0.5 + 0.112)); const points = lines.map((line) => new THREE.Vector3(line.endpoint.x, line.endpoint.y, depth * 0.5 + 0.008));
return new THREE.BufferGeometry().setFromPoints(points); return new THREE.BufferGeometry().setFromPoints(points);
} }