Checkpoint deterministic page support
This commit is contained in:
@@ -78,15 +78,6 @@ const PAGE_DEPTH = 2.24;
|
||||
const COVER_DEPTH = 2.30;
|
||||
const COVER_OVERHANG = (COVER_DEPTH - PAGE_DEPTH) * 0.5;
|
||||
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();
|
||||
|
||||
@@ -638,82 +629,154 @@ function restingTarget(side, foreEdgeX, rank, sideCount, bundleSpacing) {
|
||||
|
||||
function buildSupportSolvedLine(anchor, target, lowerLine, side, segments, segmentLengths, bundleCount, bundleSpacing) {
|
||||
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) {
|
||||
const u = index / segments;
|
||||
const stepLength = segmentLengths[index - 1];
|
||||
const supportTangent = lowerLine ? lineTangentAt(lowerLine.points, index) : coverTangentAtX(points[index - 1].x, side);
|
||||
const point = chooseClosestSupportedPoint(points[index - 1], tangent, supportTangent, target, lowerLine, index, side, stepLength, bundleCount, bundleSpacing, u);
|
||||
points.push(point);
|
||||
tangent = normalizedVector(point.x - points[index - 1].x, point.y - points[index - 1].y);
|
||||
const next = nextPointOnSupportPath(support, cursor, points[index - 1], stepLength);
|
||||
points.push(next.point);
|
||||
cursor = next.cursor;
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
function chooseClosestSupportedPoint(previous, tangent, supportTangent, target, lowerLine, index, side, stepLength, bundleCount, bundleSpacing, u) {
|
||||
const blendTangent = normalizedVector(tangent.x + supportTangent.x * 2, tangent.y + supportTangent.y * 2);
|
||||
const angleHint = Math.atan2(blendTangent.y, blendTangent.x);
|
||||
let best = null;
|
||||
for (const sample of SUPPORT_ANGLE_CANDIDATES) {
|
||||
const candidate = {
|
||||
x: previous.x + sample.cos * stepLength,
|
||||
y: previous.y + sample.sin * stepLength
|
||||
};
|
||||
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;
|
||||
|
||||
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 createLineSupportPath(anchor, lowerLine, side, bundleCount, bundleSpacing) {
|
||||
const path = [{ x: anchor.x, y: anchor.y }];
|
||||
const source = lowerLine
|
||||
? offsetPaperSupportPath(lowerLine.points, bundleSpacing)
|
||||
: coverBaseSupportPath(anchor, side, bundleCount);
|
||||
source.forEach((point) => {
|
||||
if (side * (point.x - anchor.x) >= -0.0001) {
|
||||
path.push(point);
|
||||
}
|
||||
});
|
||||
return compactPath(path);
|
||||
}
|
||||
|
||||
function scoreSupportedPoint(candidate, previous, tangent, supportTangent, angle, angleHint, target, lowerLine, index, side, bundleCount, bundleSpacing, 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) {
|
||||
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) {
|
||||
function coverBaseSupportPath(anchor, side, bundleCount) {
|
||||
const path = [];
|
||||
const clearance = coverClearance(bundleCount);
|
||||
let violation = 0;
|
||||
for (let sample = 1; sample <= 6; sample += 1) {
|
||||
const t = sample / 6;
|
||||
const x = THREE.MathUtils.lerp(previous.x, candidate.x, t);
|
||||
const y = THREE.MathUtils.lerp(previous.y, candidate.y, t);
|
||||
violation = Math.max(violation, coverTopYAtX(x) + clearance - y);
|
||||
const steps = 16;
|
||||
for (let sample = 1; sample <= steps; sample += 1) {
|
||||
const u = sample / steps;
|
||||
const t = side > 0
|
||||
? THREE.MathUtils.lerp(anchor.t, 1, u)
|
||||
: 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) {
|
||||
|
||||
Reference in New Issue
Block a user