Checkpoint deterministic page support

This commit is contained in:
2026-06-05 16:22:12 +02:00
parent e88ab8c48b
commit ee14916661
2 changed files with 139 additions and 76 deletions
+137 -74
View File
@@ -78,15 +78,6 @@ const PAGE_DEPTH = 2.24;
const COVER_DEPTH = 2.30; const COVER_DEPTH = 2.30;
const COVER_OVERHANG = (COVER_DEPTH - PAGE_DEPTH) * 0.5; const COVER_OVERHANG = (COVER_DEPTH - PAGE_DEPTH) * 0.5;
const COVER_SUPPORT_OVERHANG = COVER_OVERHANG; const COVER_SUPPORT_OVERHANG = COVER_OVERHANG;
const SUPPORT_ANGLE_STEPS = 720;
const SUPPORT_ANGLE_CANDIDATES = Array.from({ length: SUPPORT_ANGLE_STEPS }, (_, sample) => {
const angle = sample / SUPPORT_ANGLE_STEPS * Math.PI * 2;
return {
angle,
cos: Math.cos(angle),
sin: Math.sin(angle)
};
});
const maximumPageCount = calculateMaximumPageCount(); const maximumPageCount = calculateMaximumPageCount();
@@ -638,82 +629,154 @@ function restingTarget(side, foreEdgeX, rank, sideCount, bundleSpacing) {
function buildSupportSolvedLine(anchor, target, lowerLine, side, segments, segmentLengths, bundleCount, bundleSpacing) { function buildSupportSolvedLine(anchor, target, lowerLine, side, segments, segmentLengths, bundleCount, bundleSpacing) {
const points = [{ x: anchor.x, y: anchor.y }]; const points = [{ x: anchor.x, y: anchor.y }];
let tangent = coverTangentAtX(anchor.x, side); const supportPath = createLineSupportPath(anchor, lowerLine, side, bundleCount, bundleSpacing);
const support = createMeasuredPath(supportPath);
let cursor = 0;
for (let index = 1; index <= segments; index += 1) { for (let index = 1; index <= segments; index += 1) {
const u = index / segments;
const stepLength = segmentLengths[index - 1]; const stepLength = segmentLengths[index - 1];
const supportTangent = lowerLine ? lineTangentAt(lowerLine.points, index) : coverTangentAtX(points[index - 1].x, side); const next = nextPointOnSupportPath(support, cursor, points[index - 1], stepLength);
const point = chooseClosestSupportedPoint(points[index - 1], tangent, supportTangent, target, lowerLine, index, side, stepLength, bundleCount, bundleSpacing, u); points.push(next.point);
points.push(point); cursor = next.cursor;
tangent = normalizedVector(point.x - points[index - 1].x, point.y - points[index - 1].y);
} }
return points; return points;
} }
function chooseClosestSupportedPoint(previous, tangent, supportTangent, target, lowerLine, index, side, stepLength, bundleCount, bundleSpacing, u) { function createLineSupportPath(anchor, lowerLine, side, bundleCount, bundleSpacing) {
const blendTangent = normalizedVector(tangent.x + supportTangent.x * 2, tangent.y + supportTangent.y * 2); const path = [{ x: anchor.x, y: anchor.y }];
const angleHint = Math.atan2(blendTangent.y, blendTangent.x); const source = lowerLine
let best = null; ? offsetPaperSupportPath(lowerLine.points, bundleSpacing)
for (const sample of SUPPORT_ANGLE_CANDIDATES) { : coverBaseSupportPath(anchor, side, bundleCount);
const candidate = { source.forEach((point) => {
x: previous.x + sample.cos * stepLength, if (side * (point.x - anchor.x) >= -0.0001) {
y: previous.y + sample.sin * stepLength path.push(point);
};
const score = scoreSupportedPoint(candidate, previous, tangent, supportTangent, sample.angle, angleHint, target, lowerLine, index, side, bundleCount, bundleSpacing, u);
if (best === null || score < best.score) best = { point: candidate, score };
} }
if (Number.isFinite(best?.score)) return best.point; });
return compactPath(path);
let fallback = null;
for (const sample of SUPPORT_ANGLE_CANDIDATES) {
const candidate = {
x: previous.x + sample.cos * stepLength,
y: previous.y + sample.sin * stepLength
};
const fallbackScore = scoreSupportedPoint(candidate, previous, tangent, supportTangent, sample.angle, angleHint, target, lowerLine, index, side, bundleCount, bundleSpacing, u, true);
if (fallback === null || fallbackScore < fallback.score) fallback = { point: candidate, score: fallbackScore };
}
return fallback.point;
} }
function scoreSupportedPoint(candidate, previous, tangent, supportTangent, angle, angleHint, target, lowerLine, index, side, bundleCount, bundleSpacing, u, allowViolation = false) { function coverBaseSupportPath(anchor, side, bundleCount) {
const backward = Math.max(0, side * (previous.x - candidate.x)); const path = [];
if (!allowViolation && backward > 0.00001) return Number.POSITIVE_INFINITY;
let supportError;
let supportViolation = 0;
if (lowerLine) {
const closest = closestPointOnPolyline(candidate, lowerLine.points);
const closestDistance = Math.hypot(candidate.x - closest.x, candidate.y - closest.y);
supportViolation = Math.max(0, bundleSpacing - closestDistance) + Math.max(0, closest.y - candidate.y);
if (!allowViolation && supportViolation > 0.00001) return Number.POSITIVE_INFINITY;
supportError = closestDistance - bundleSpacing;
} else {
const floor = coverTopYAtX(candidate.x) + coverClearance(bundleCount);
supportViolation = coverSegmentViolation(previous, candidate, bundleCount);
if (!allowViolation && supportViolation > 0.00001) return Number.POSITIVE_INFINITY;
supportError = candidate.y - floor;
}
const candidateTangent = normalizedVector(candidate.x - previous.x, candidate.y - previous.y);
const bend = 1 - Math.max(-1, Math.min(1, candidateTangent.x * tangent.x + candidateTangent.y * tangent.y));
const supportAlignment = 1 - Math.max(-1, Math.min(1, candidateTangent.x * supportTangent.x + candidateTangent.y * supportTangent.y));
const angleDelta = Math.abs(Math.atan2(Math.sin(angle - angleHint), Math.cos(angle - angleHint)));
const outwardTarget = Math.max(0, side * (target.x - candidate.x));
const targetHeight = Math.abs(candidate.y - target.y);
return Math.abs(supportError) * 1200 + supportViolation * 100000 + backward * 100000 + supportAlignment * 0.85 + bend * 0.22 + angleDelta * 0.04 + outwardTarget * 0.01 + targetHeight * 0.006;
}
function coverSegmentViolation(previous, candidate, bundleCount) {
const clearance = coverClearance(bundleCount); const clearance = coverClearance(bundleCount);
let violation = 0; const steps = 16;
for (let sample = 1; sample <= 6; sample += 1) { for (let sample = 1; sample <= steps; sample += 1) {
const t = sample / 6; const u = sample / steps;
const x = THREE.MathUtils.lerp(previous.x, candidate.x, t); const t = side > 0
const y = THREE.MathUtils.lerp(previous.y, candidate.y, t); ? THREE.MathUtils.lerp(anchor.t, 1, u)
violation = Math.max(violation, coverTopYAtX(x) + clearance - y); : THREE.MathUtils.lerp(anchor.t, 0, u);
const point = spineCurvePoint(t, activeSpineHalf / 0.42);
path.push({ x: point.x, y: point.y + clearance });
} }
return Math.max(0, violation); const profile = coverProfilePointsFromFrame(currentSpineHalf(), activeCoverOuterX)
.filter((point) => side < 0 ? point.x <= -currentSpineHalf() : point.x >= currentSpineHalf())
.sort((a, b) => side < 0 ? b.x - a.x : a.x - b.x);
profile.forEach((point) => path.push({ x: point.x, y: point.y + clearance }));
return path;
}
function offsetPaperSupportPath(points, distance) {
return points.map((point, index) => {
const normal = upwardNormalAt(points, index);
return {
x: point.x + normal.x * distance,
y: point.y + normal.y * distance
};
});
}
function compactPath(path) {
const compacted = [];
path.forEach((point) => {
const previous = compacted[compacted.length - 1];
if (!previous || Math.hypot(point.x - previous.x, point.y - previous.y) > 0.000001) {
compacted.push(point);
}
});
return compacted;
}
function createMeasuredPath(points) {
const lengths = [0];
for (let index = 1; index < points.length; index += 1) {
const previous = points[index - 1];
const point = points[index];
lengths[index] = lengths[index - 1] + Math.hypot(point.x - previous.x, point.y - previous.y);
}
return { points, lengths, totalLength: lengths[lengths.length - 1] ?? 0 };
}
function nextPointOnSupportPath(support, cursor, previous, segmentLength) {
let segmentIndex = Math.max(0, support.lengths.findIndex((length) => length > cursor) - 1);
if (segmentIndex < 0) segmentIndex = support.points.length - 2;
let startDistance = cursor;
let from = pointAtMeasuredPathDistance(support, cursor);
while (segmentIndex < support.points.length - 1) {
const to = support.points[segmentIndex + 1];
const endDistance = support.lengths[segmentIndex + 1];
const hit = circleSegmentIntersection(previous, from, to, segmentLength);
if (hit !== null) {
return {
point: hit.point,
cursor: THREE.MathUtils.lerp(startDistance, endDistance, hit.t)
};
}
segmentIndex += 1;
from = support.points[segmentIndex];
startDistance = support.lengths[segmentIndex];
}
return extendSupportPathEnd(support, previous, segmentLength);
}
function pointAtMeasuredPathDistance(support, distance) {
const target = THREE.MathUtils.clamp(distance, 0, support.totalLength);
for (let index = 0; index < support.points.length - 1; index += 1) {
if (target <= support.lengths[index + 1]) {
const from = support.points[index];
const to = support.points[index + 1];
const span = support.lengths[index + 1] - support.lengths[index] || 1;
const t = (target - support.lengths[index]) / span;
return {
x: THREE.MathUtils.lerp(from.x, to.x, t),
y: THREE.MathUtils.lerp(from.y, to.y, t)
};
}
}
return { ...support.points[support.points.length - 1] };
}
function circleSegmentIntersection(center, from, to, radius) {
const dx = to.x - from.x;
const dy = to.y - from.y;
const fx = from.x - center.x;
const fy = from.y - center.y;
const a = dx * dx + dy * dy;
const b = 2 * (fx * dx + fy * dy);
const c = fx * fx + fy * fy - radius * radius;
const discriminant = b * b - 4 * a * c;
if (a <= 0 || discriminant < 0) return null;
const root = Math.sqrt(discriminant);
const t0 = (-b - root) / (2 * a);
const t1 = (-b + root) / (2 * a);
const t = [t0, t1].filter((value) => value >= -0.000001 && value <= 1.000001).sort((left, right) => left - right)[0];
if (t === undefined) return null;
const clamped = THREE.MathUtils.clamp(t, 0, 1);
return {
t: clamped,
point: {
x: THREE.MathUtils.lerp(from.x, to.x, clamped),
y: THREE.MathUtils.lerp(from.y, to.y, clamped)
}
};
}
function extendSupportPathEnd(support, previous, segmentLength) {
const last = support.points[support.points.length - 1];
const before = support.points[Math.max(0, support.points.length - 2)];
const direction = normalizedVector(last.x - before.x, last.y - before.y);
const point = {
x: previous.x + direction.x * segmentLength,
y: previous.y + direction.y * segmentLength
};
return { point, cursor: support.totalLength };
} }
function closestPointOnPolyline(point, polyline) { function closestPointOnPolyline(point, polyline) {
+1 -1
View File
@@ -74,6 +74,6 @@
<button id="fast_forward" type="button">Fast Forward</button> <button id="fast_forward" type="button">Fast Forward</button>
<output id="flip_count">0 / 10</output> <output id="flip_count">0 / 10</output>
</div> </div>
<script type="module" src="/js/webgl-book-shape-lab.js?v=double-page-segments-1"></script> <script type="module" src="/js/webgl-book-shape-lab.js?v=deterministic-page-support-1"></script>
</body> </body>
</html> </html>