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 @@ 0 / 10 - +