From f63450012124b5fd0e43834dfe86b4c9c3676eb1 Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Sat, 6 Jun 2026 02:48:57 +0200 Subject: [PATCH] Round WebGL book cover edges --- public/js/procedural-book-model.js | 212 ++++++++++++++++------------- 1 file changed, 117 insertions(+), 95 deletions(-) diff --git a/public/js/procedural-book-model.js b/public/js/procedural-book-model.js index c569b2d..e6af518 100644 --- a/public/js/procedural-book-model.js +++ b/public/js/procedural-book-model.js @@ -147,19 +147,29 @@ function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth, co const uvs = []; const indices = []; const groups = []; + const vertexCache = new Map(); const halfDepth = depth * 0.5; - const edgeRadius = Math.min(depth * 0.015, thickness * 0.45, 0.035); - const edgeProfile = [ - { inset: edgeRadius, drop: 0 }, - { inset: edgeRadius * 0.48, drop: edgeRadius * 0.16 }, - { inset: edgeRadius * 0.12, drop: edgeRadius * 0.42 }, - { inset: 0, drop: edgeRadius * 0.72 }, - ]; + const edgeRadius = Math.min(thickness * 0.5, PROCEDURAL_BOOK.COVER_OVERHANG * 0.5); + const cornerSteps = 8; + const sideSteps = 18; + const edgeSteps = 18; + const leftX = section[0].x; + const rightX = section[section.length - 1].x; const push = (point) => { + const key = [ + point.x.toFixed(5), + point.y.toFixed(5), + point.z.toFixed(5), + point.u.toFixed(5), + point.v.toFixed(5) + ].join('|'); + const cached = vertexCache.get(key); + if (cached !== undefined) return cached; const index = positions.length / 3; positions.push(point.x, point.y, point.z); uvs.push(point.u, point.v); + vertexCache.set(key, index); return index; }; @@ -169,106 +179,118 @@ function createCoverAssemblyGeometry(pageWidth, depth, thickness, spineWidth, co groups.push({ start, count: quadIndices.length, materialIndex }); }; - const pointAt = (source, y, z, u, v) => ({ x: source.x, y, z, u, v }); - - const edgePoint = (source, side, profilePoint, u, bottom = false) => { - const topY = source.y; - const bottomY = source.y - thickness; - return pointAt( - source, - bottom ? bottomY + profilePoint.drop : topY - profilePoint.drop, - side * (halfDepth - profilePoint.inset), - u, - side > 0 ? 1 : 0 - ); - }; - const addQuad = (materialIndex, a, b, c, d) => { - const base = positions.length / 3; - push(a); - push(b); - push(c); - push(d); - pushGroup(materialIndex, [base, base + 1, base + 2, base + 2, base + 1, base + 3]); + const aIndex = push(a); + const bIndex = push(b); + const cIndex = push(c); + const dIndex = push(d); + pushGroup(materialIndex, [aIndex, bIndex, cIndex, cIndex, bIndex, dIndex]); }; - for (let i = 0; i < section.length - 1; i += 1) { - const left = section[i]; - const right = section[i + 1]; - const leftU = i / (section.length - 1); - const rightU = (i + 1) / (section.length - 1); - const topMaterial = coverSegmentMaterialIndex(i); - const innerFront = halfDepth - edgeRadius; - const innerBack = -halfDepth + edgeRadius; + const coverYAtX = (x) => { + if (x <= leftX) return section[0].y; + for (let index = 0; index < section.length - 1; index += 1) { + const from = section[index]; + const to = section[index + 1]; + if (x <= to.x) { + const t = (x - from.x) / (to.x - from.x || 1); + return THREE.MathUtils.lerp(from.y, to.y, t); + } + } + return section[section.length - 1].y; + }; + const materialAtX = (x) => { + for (let index = 0; index < section.length - 1; index += 1) { + if (x <= section[index + 1].x) return coverSegmentMaterialIndex(index); + } + return coverSegmentMaterialIndex(section.length - 2); + }; + const uAt = (x) => (x - leftX) / (rightX - leftX || 1); + const vAt = (z) => (z + halfDepth) / depth; + const pointAt = (x, y, z) => ({ x, y, z, u: uAt(x), v: vAt(z) }); + const edgeProfile = Array.from({ length: edgeSteps + 1 }, (_, index) => { + const angle = Math.PI * 0.5 - (index / edgeSteps) * Math.PI; + return { + inset: edgeRadius - Math.cos(angle) * edgeRadius, + yOffset: -edgeRadius + Math.sin(angle) * edgeRadius + }; + }); + const roundedRectContour = (inset) => { + const hx = rightX - inset; + const hz = halfDepth - inset; + const cornerRadius = Math.max(0.0001, edgeRadius - inset); + const points = []; + const pushLinear = (fromX, fromZ, toX, toZ, steps) => { + for (let step = 0; step <= steps; step += 1) { + if (points.length && step === 0) continue; + const t = step / steps; + points.push({ + x: THREE.MathUtils.lerp(fromX, toX, t), + z: THREE.MathUtils.lerp(fromZ, toZ, t) + }); + } + }; + const pushCorner = (centerX, centerZ, fromAngle, toAngle) => { + for (let step = 1; step <= cornerSteps; step += 1) { + const t = step / cornerSteps; + const angle = THREE.MathUtils.lerp(fromAngle, toAngle, t); + points.push({ + x: centerX + Math.cos(angle) * cornerRadius, + z: centerZ + Math.sin(angle) * cornerRadius + }); + } + }; + pushLinear(-hx + cornerRadius, hz, hx - cornerRadius, hz, sideSteps); + pushCorner(hx - cornerRadius, hz - cornerRadius, Math.PI * 0.5, 0); + pushLinear(hx, hz - cornerRadius, hx, -hz + cornerRadius, sideSteps); + pushCorner(hx - cornerRadius, -hz + cornerRadius, 0, -Math.PI * 0.5); + pushLinear(hx - cornerRadius, -hz, -hx + cornerRadius, -hz, sideSteps); + pushCorner(-hx + cornerRadius, -hz + cornerRadius, -Math.PI * 0.5, -Math.PI); + pushLinear(-hx, -hz + cornerRadius, -hx, hz - cornerRadius, sideSteps); + pushCorner(-hx + cornerRadius, hz - cornerRadius, Math.PI, Math.PI * 0.5); + return points; + }; + + const profileContours = edgeProfile.map((profile) => roundedRectContour(profile.inset).map((point) => { + const topY = coverYAtX(point.x); + return pointAt(point.x, topY + profile.yOffset, point.z); + })); + + const topXs = [ + leftX + edgeRadius, + ...section.slice(1, -1).map((point) => point.x), + rightX - edgeRadius + ].sort((a, b) => a - b); + const topZs = [-halfDepth + edgeRadius, halfDepth - edgeRadius]; + for (let index = 0; index < topXs.length - 1; index += 1) { + const left = topXs[index]; + const right = topXs[index + 1]; + const materialIndex = materialAtX((left + right) * 0.5); addQuad( - topMaterial, - pointAt(left, left.y, innerFront, leftU, 1), - pointAt(right, right.y, innerFront, rightU, 1), - pointAt(left, left.y, innerBack, leftU, 0), - pointAt(right, right.y, innerBack, rightU, 0) + materialIndex, + pointAt(left, coverYAtX(left), topZs[1]), + pointAt(right, coverYAtX(right), topZs[1]), + pointAt(left, coverYAtX(left), topZs[0]), + pointAt(right, coverYAtX(right), topZs[0]) ); - addQuad( 3, - pointAt(left, left.y - thickness, innerBack, leftU, 0), - pointAt(right, right.y - thickness, innerBack, rightU, 0), - pointAt(left, left.y - thickness, innerFront, leftU, 1), - pointAt(right, right.y - thickness, innerFront, rightU, 1) + pointAt(left, coverYAtX(left) - thickness, topZs[0]), + pointAt(right, coverYAtX(right) - thickness, topZs[0]), + pointAt(left, coverYAtX(left) - thickness, topZs[1]), + pointAt(right, coverYAtX(right) - thickness, topZs[1]) ); - - [-1, 1].forEach((side) => { - for (let edgeIndex = 0; edgeIndex < edgeProfile.length - 1; edgeIndex += 1) { - const current = edgeProfile[edgeIndex]; - const next = edgeProfile[edgeIndex + 1]; - addQuad( - 3, - edgePoint(left, side, current, leftU, false), - edgePoint(right, side, current, rightU, false), - edgePoint(left, side, next, leftU, false), - edgePoint(right, side, next, rightU, false) - ); - addQuad( - 3, - edgePoint(left, side, next, leftU, true), - edgePoint(right, side, next, rightU, true), - edgePoint(left, side, current, leftU, true), - edgePoint(right, side, current, rightU, true) - ); - } - - const sideEdge = edgeProfile[edgeProfile.length - 1]; - addQuad( - 3, - edgePoint(left, side, sideEdge, leftU, false), - edgePoint(right, side, sideEdge, rightU, false), - edgePoint(left, side, sideEdge, leftU, true), - edgePoint(right, side, sideEdge, rightU, true) - ); - }); } - [0, section.length - 1].forEach((pointIndex) => { - const point = section[pointIndex]; - const u = pointIndex / (section.length - 1); - if (pointIndex === 0) { - addQuad( - 3, - pointAt(point, point.y, halfDepth, u, 1), - pointAt(point, point.y, -halfDepth, u, 0), - pointAt(point, point.y - thickness, halfDepth, u, 1), - pointAt(point, point.y - thickness, -halfDepth, u, 0) - ); - } else { - addQuad( - 3, - pointAt(point, point.y, halfDepth, u, 1), - pointAt(point, point.y - thickness, halfDepth, u, 1), - pointAt(point, point.y, -halfDepth, u, 0), - pointAt(point, point.y - thickness, -halfDepth, u, 0) - ); + for (let profileIndex = 0; profileIndex < profileContours.length - 1; profileIndex += 1) { + const current = profileContours[profileIndex]; + const next = profileContours[profileIndex + 1]; + for (let pointIndex = 0; pointIndex < current.length; pointIndex += 1) { + const nextPointIndex = (pointIndex + 1) % current.length; + addQuad(3, current[pointIndex], current[nextPointIndex], next[pointIndex], next[nextPointIndex]); } - }); + } const geometry = new THREE.BufferGeometry(); geometry.setIndex(indices);