Checkpoint optimized book shape spline
This commit is contained in:
@@ -135,7 +135,7 @@ function setPageCount(value) {
|
||||
function rebuildBook() {
|
||||
clearGroup(book);
|
||||
|
||||
const coverDepth = 2.34;
|
||||
const coverDepth = 2.30;
|
||||
const coverThickness = BOOK_PROFILE.coverThickness;
|
||||
const pageWidth = 1.62;
|
||||
const pageDepth = 2.24;
|
||||
@@ -167,7 +167,7 @@ function addCoverAssembly(pageWidth, depth, thickness, spineWidth) {
|
||||
}
|
||||
|
||||
function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth) {
|
||||
const overhang = 0.055;
|
||||
const overhang = 0.13;
|
||||
const spineHalf = spineWidth * 0.5;
|
||||
const hingeInset = 0.07;
|
||||
const outerX = pageWidth + overhang;
|
||||
@@ -276,7 +276,7 @@ function spineCurvePoint(t, spineWidth) {
|
||||
|
||||
function simulatePageLines(bundleCount, pageWidth, spineWidth) {
|
||||
const lines = [];
|
||||
const segments = 16;
|
||||
const segments = 24;
|
||||
const stepLength = pageWidth / segments;
|
||||
const entries = [];
|
||||
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);
|
||||
if (!sideEntries.length) return;
|
||||
|
||||
const bottomEntry = sideEntries[0];
|
||||
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);
|
||||
|
||||
let lowerLine = null;
|
||||
sideEntries.forEach((entry, rank) => {
|
||||
const anchor = spineCurvePoint(entry.t, spineWidth);
|
||||
const points = rank === 0
|
||||
? bottomPoints.map((point) => ({ ...point }))
|
||||
: offsetPageLine(bottomPoints, anchor, rank * BOOK_PROFILE.bundleSpacing);
|
||||
keepPageAboveCover(points, side, bundleCount);
|
||||
lines.push({ index: entry.index, t: entry.t, side, anchor, points, endpoint: points[points.length - 1] });
|
||||
const target = restingTarget(side, pageWidth, rank, sideEntries.length);
|
||||
const points = buildOptimizedSplineLine(anchor, target, lowerLine, side, segments, stepLength, bundleCount);
|
||||
const line = { index: entry.index, t: entry.t, side, anchor, points, endpoint: points[points.length - 1] };
|
||||
lines.push(line);
|
||||
lowerLine = line;
|
||||
});
|
||||
});
|
||||
enforceStackConstraints(lines, stepLength, bundleCount);
|
||||
return lines;
|
||||
}
|
||||
|
||||
@@ -338,7 +331,7 @@ function measureStackSpacingError(lines) {
|
||||
for (let row = 1; row < sideLines.length; row += 1) {
|
||||
const lower = sideLines[row - 1];
|
||||
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);
|
||||
maxError = Math.max(maxError, Math.abs(distance - BOOK_PROFILE.bundleSpacing));
|
||||
}
|
||||
@@ -398,6 +391,148 @@ function restingTarget(side, pageWidth, rank, sideCount) {
|
||||
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) {
|
||||
const gravity = 0.00072;
|
||||
const stackPressure = 0.0011 * (1 - local);
|
||||
@@ -464,14 +599,31 @@ function constrainSegment(a, b, length, anchorA) {
|
||||
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) {
|
||||
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) + clearance);
|
||||
points[i].y = Math.max(points[i].y, coverTopYAtX(points[i].x) + coverClearance(bundleCount));
|
||||
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) {
|
||||
const iterations = 44;
|
||||
[-1, 1].forEach((side) => {
|
||||
@@ -563,7 +715,7 @@ function currentSpineHalf() {
|
||||
function addSimulatedPageLines(lines, depth) {
|
||||
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 z = depth * 0.5 + 0.11;
|
||||
const z = depth * 0.5 + 0.006;
|
||||
lines.forEach((line) => {
|
||||
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));
|
||||
@@ -589,8 +741,8 @@ function createLoftedLineBody(lines, depth) {
|
||||
positions.push(point.x, point.y, z);
|
||||
return index;
|
||||
};
|
||||
const front = 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 + 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)));
|
||||
for (let row = 0; row < smoothLines.length - 1; row += 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]);
|
||||
@@ -624,7 +776,7 @@ function createLoftedLineBody(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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user