Checkpoint optimized book shape spline
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user