diff --git a/public/js/webgl-book-shape-lab.js b/public/js/webgl-book-shape-lab.js
index ae34266..3817ca0 100644
--- a/public/js/webgl-book-shape-lab.js
+++ b/public/js/webgl-book-shape-lab.js
@@ -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) {
diff --git a/public/webgl-book-shape-lab.html b/public/webgl-book-shape-lab.html
index 16f1986..0da28c1 100644
--- a/public/webgl-book-shape-lab.html
+++ b/public/webgl-book-shape-lab.html
@@ -74,6 +74,6 @@
-
+