From 65dbbdd093882620ac1895b90a389f8254d62719 Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Fri, 5 Jun 2026 12:39:23 +0200 Subject: [PATCH] Checkpoint hinge-relative book geometry --- public/js/webgl-book-shape-lab.js | 42 ++++++++++++++++--------------- public/webgl-book-shape-lab.html | 2 +- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/public/js/webgl-book-shape-lab.js b/public/js/webgl-book-shape-lab.js index 29c60d1..63f51e0 100644 --- a/public/js/webgl-book-shape-lab.js +++ b/public/js/webgl-book-shape-lab.js @@ -72,6 +72,7 @@ const PAGE_DEPTH = 2.24; const COVER_OVERHANG = 0.13; const COVER_SUPPORT_OVERHANG = 0.055; const HINGE_INSET = 0.07; +const MAX_SUPPORTED_STACK_RISE = PAGE_WIDTH * 0.5; 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; @@ -211,12 +212,13 @@ function rebuildBook() { const bundleCount = Math.max(4, Math.round(pageCount / 10)); const spineWidth = calculateSpineWidth(bundleCount); const leftCount = calculateLeftBundleCount(bundleCount); + const hingeX = spineWidth * 0.5 + HINGE_INSET; const bundleSpacing = calculateBundleSpacing(bundleCount, spineWidth, leftCount); activeSpineHalf = spineWidth * 0.5; - const lines = simulatePageLines(bundleCount, pageWidth, spineWidth, bundleSpacing, leftCount); + const lines = simulatePageLines(bundleCount, pageWidth, spineWidth, hingeX, bundleSpacing, leftCount); lastLengthError = measureLineLengthError(lines, pageWidth); lastSpacingError = measureStackSpacingError(lines, bundleSpacing); - lastBookModel = { coverDepth, pageWidth, pageDepth, bundleCount, spineWidth, bundleSpacing, lines }; + lastBookModel = { coverDepth, pageWidth, pageDepth, bundleCount, spineWidth, hingeX, bundleSpacing, lines }; addCoverAssembly(pageWidth, coverDepth, coverThickness, spineWidth); addClothSpine(pageDepth, spineWidth); @@ -241,8 +243,8 @@ function addCoverAssembly(pageWidth, depth, thickness, spineWidth) { function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth) { const spineHalf = spineWidth * 0.5; - const outerX = spineHalf + pageWidth + COVER_OVERHANG; const hingeX = spineHalf + HINGE_INSET; + const outerX = hingeX + pageWidth + COVER_OVERHANG; const outerTopY = BOOK_PROFILE.tableY + thickness; const connectionTopY = BOOK_PROFILE.raisedHingeY; const spineTopY = BOOK_PROFILE.tableY + thickness; @@ -378,12 +380,9 @@ function calculateMaximumPageCount() { function isBundleCountReachable(bundleCount) { const spineWidth = calculateSpineWidth(bundleCount); const bundleSpacing = calculateBundleSpacing(bundleCount, spineWidth, bundleCount); - const topRank = bundleCount - 1; - const target = restingTarget(1, PAGE_WIDTH, topRank, bundleCount, bundleSpacing); const anchor = spineCurvePoint(1, spineWidth); - const requiredDistance = Math.hypot(target.x - anchor.x, target.y - anchor.y); - const verticalRise = target.y - anchor.y; - return requiredDistance <= PAGE_WIDTH && verticalRise <= PAGE_WIDTH * 0.5; + const topLineY = BOOK_PROFILE.coverThickness + BOOK_PROFILE.paperContactOffset + (bundleCount - 1) * bundleSpacing; + return topLineY - anchor.y <= MAX_SUPPORTED_STACK_RISE; } function calculateBundleSpacing(bundleCount, spineWidth, leftCount) { @@ -409,7 +408,7 @@ function calculateLeftBundleCount(bundleCount) { return THREE.MathUtils.clamp(Math.round(bundleCount * readingProgress), 0, bundleCount); } -function simulatePageLines(bundleCount, pageWidth, spineWidth, bundleSpacing, leftCount) { +function simulatePageLines(bundleCount, pageWidth, spineWidth, hingeX, bundleSpacing, leftCount) { const lines = []; const segments = 24; const stepLength = pageWidth / segments; @@ -453,7 +452,7 @@ function simulatePageLines(bundleCount, pageWidth, spineWidth, bundleSpacing, le let lowerLine = null; sideEntries.forEach((entry, rank) => { const anchor = spineCurvePoint(entry.t, spineWidth); - const target = restingTarget(side, pageWidth, rank, sideEntries.length, bundleSpacing); + const target = restingTarget(side, pageWidth, hingeX, rank, sideEntries.length, bundleSpacing); const points = buildSupportSolvedLine(anchor, target, lowerLine, side, segments, stepLength, bundleCount, bundleSpacing); const line = { index: entry.index, t: entry.t, side, anchor, points, endpoint: points[points.length - 1], isHairPage: entry.isHairPage === true }; lines.push(line); @@ -541,10 +540,10 @@ function initialPageLine(anchor, target, segments) { return points; } -function restingTarget(side, pageWidth, rank, sideCount, bundleSpacing) { +function restingTarget(side, pageWidth, hingeX, rank, sideCount, bundleSpacing) { const local = sideCount <= 1 ? 0 : rank / (sideCount - 1); const foreCurve = 0.11 * Math.sin(Math.PI * local); - const x = side * (pageWidth - foreCurve); + const x = side * (hingeX + pageWidth - foreCurve); const y = BOOK_PROFILE.coverThickness + BOOK_PROFILE.paperContactOffset + rank * bundleSpacing + 0.002 * Math.sin(Math.PI * local); return { x, y }; } @@ -825,7 +824,7 @@ function coverTopYAtX(x) { const ax = Math.abs(x); const spineHalf = currentSpineHalf(); const hingeX = spineHalf + HINGE_INSET; - const outerX = spineHalf + PAGE_WIDTH + COVER_SUPPORT_OVERHANG; + const outerX = hingeX + PAGE_WIDTH + COVER_SUPPORT_OVERHANG; if (ax <= spineHalf) return BOOK_PROFILE.coverThickness; if (ax <= hingeX) { const t = (ax - spineHalf) / (hingeX - spineHalf); @@ -865,7 +864,7 @@ function createSinglePageBodyLines(line) { function createStackBodyMaterials(lines, side) { const baseColor = side < 0 ? '#d8c7a4' : '#e7d6b4'; const lineColor = '#9a8058'; - const layerTexture = createStackLayerTexture(lines.length, baseColor, lineColor); + const layerTexture = createStackLayerTexture(lastBookModel.bundleCount, baseColor, lineColor); return [ new THREE.MeshBasicMaterial({ map: layerTexture, side: THREE.DoubleSide }), new THREE.MeshBasicMaterial({ map: layerTexture, side: THREE.DoubleSide }), @@ -873,7 +872,7 @@ function createStackBodyMaterials(lines, side) { ]; } -function createStackLayerTexture(lineCount, baseColor, lineColor) { +function createStackLayerTexture(bundleCount, baseColor, lineColor) { const canvas = document.createElement('canvas'); canvas.width = 2048; canvas.height = 1024; @@ -884,8 +883,8 @@ function createStackLayerTexture(lineCount, baseColor, lineColor) { context.globalAlpha = 0.95; context.lineWidth = 4.2; context.lineCap = 'square'; - for (let row = 0; row < lineCount; row += 1) { - const v = lineCount <= 1 ? 0.5 : row / (lineCount - 1); + for (let row = 0; row < bundleCount; row += 1) { + const v = bundleCount <= 1 ? 0.5 : row / (bundleCount - 1); const y = (1 - v) * canvas.height; context.beginPath(); context.moveTo(-8, y); @@ -908,15 +907,18 @@ function createLoftedLineBody(lines, depth) { const uvs = []; const indices = []; const smoothLines = lines.map((line) => line.points); + const bundleCount = lastBookModel?.bundleCount ?? smoothLines.length; const push = (point, z, uv) => { const index = positions.length / 3; positions.push(point.x, point.y, z); uvs.push(uv.u, uv.v); return index; }; - const rowUv = (row) => ( - smoothLines.length <= 1 ? 0.5 : row / (smoothLines.length - 1) - ); + const rowUv = (row) => { + const line = lines[row]; + const index = line.isHairPage ? (line.side < 0 ? 0 : bundleCount - 1) : line.index; + return bundleCount <= 1 ? 0.5 : index / (bundleCount - 1); + }; const colUv = (points, col) => ( points.length <= 1 ? 0.5 : col / (points.length - 1) ); diff --git a/public/webgl-book-shape-lab.html b/public/webgl-book-shape-lab.html index aebb963..0238ebb 100644 --- a/public/webgl-book-shape-lab.html +++ b/public/webgl-book-shape-lab.html @@ -74,6 +74,6 @@ 0 / 10 - +