diff --git a/js/displacement.js b/js/displacement.js index 5429e7b..de93a5c 100644 --- a/js/displacement.js +++ b/js/displacement.js @@ -136,14 +136,20 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett tmpPos.fromBufferAttribute(posAttr, t + v); const k = posKey(tmpPos.x, tmpPos.y, tmpPos.z); if (userExcluded && excludedPosSet) excludedPosSet.add(k); - tmpNrm.fromBufferAttribute(nrmAttr, t + v); + // Use the geometric face normal (faceNrm = cross product, length ∝ 2×area) + // instead of the buffer normal. The subdivision pipeline interpolates + // smooth normals at midpoints, which propagates the 45° edge tilt deep + // into the face interior across iterations. Using the true face normal + // ensures interior vertices on flat faces get a perfectly perpendicular + // smooth normal, limiting the angled displacement to the single outermost + // vertex row at each hard edge — matching addSmoothNormals() in main.js. const existing = smoothNrmMap.get(k); if (existing) { - existing[0] += tmpNrm.x * faceArea; - existing[1] += tmpNrm.y * faceArea; - existing[2] += tmpNrm.z * faceArea; + existing[0] += faceNrm.x; + existing[1] += faceNrm.y; + existing[2] += faceNrm.z; } else { - smoothNrmMap.set(k, [tmpNrm.x * faceArea, tmpNrm.y * faceArea, tmpNrm.z * faceArea]); + smoothNrmMap.set(k, [faceNrm.x, faceNrm.y, faceNrm.z]); } if (czX > 1e-12 || czY > 1e-12 || czZ > 1e-12) { const za = zoneAreaMap.get(k); diff --git a/js/presetTextures.js b/js/presetTextures.js index 2b8ffa6..c4bbd25 100644 --- a/js/presetTextures.js +++ b/js/presetTextures.js @@ -5,12 +5,27 @@ const THUMB = 80; // ── Helpers ────────────────────────────────────────────────────────────────── -function makeCanvas(size = SIZE) { +function makeCanvas(w, h = w) { const c = document.createElement('canvas'); - c.width = c.height = size; + c.width = w; + c.height = h; return c; } +/** Draw img into a square canvas using cover-scaling (preserves aspect ratio, center-crops). */ +function drawCover(ctx, img, size) { + const scale = Math.max(size / img.width, size / img.height); + const w = img.width * scale; + const h = img.height * scale; + ctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h); +} + +/** Return { w, h } capped at SIZE on the longest side, preserving aspect ratio. */ +function fitDimensions(imgW, imgH) { + const scale = Math.min(SIZE / imgW, SIZE / imgH, 1); + return { w: Math.round(imgW * scale), h: Math.round(imgH * scale) }; +} + // ── Image-based presets ─────────────────────────────────────────────────────── const IMAGE_PRESETS = [ @@ -32,18 +47,19 @@ function loadImagePreset({ name, url }) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { - const full = makeCanvas(SIZE); - full.getContext('2d').drawImage(img, 0, 0, SIZE, SIZE); + const { w, h } = fitDimensions(img.width, img.height); + const full = makeCanvas(w, h); + full.getContext('2d').drawImage(img, 0, 0, w, h); const thumb = makeCanvas(THUMB); - thumb.getContext('2d').drawImage(img, 0, 0, THUMB, THUMB); + drawCover(thumb.getContext('2d'), img, THUMB); - const imageData = full.getContext('2d').getImageData(0, 0, SIZE, SIZE); + const imageData = full.getContext('2d').getImageData(0, 0, w, h); const texture = new THREE.CanvasTexture(full); texture.wrapS = texture.wrapT = THREE.RepeatWrapping; texture.name = name; - resolve({ name, thumbCanvas: thumb, fullCanvas: full, texture, imageData, width: SIZE, height: SIZE }); + resolve({ name, thumbCanvas: thumb, fullCanvas: full, texture, imageData, width: w, height: h }); }; img.onerror = () => reject(new Error(`Failed to load preset image: ${url}`)); img.src = url; @@ -64,14 +80,15 @@ export function loadCustomTexture(file) { const url = URL.createObjectURL(file); img.onload = () => { URL.revokeObjectURL(url); - const canvas = makeCanvas(SIZE); + const { w, h } = fitDimensions(img.width, img.height); + const canvas = makeCanvas(w, h); const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, SIZE, SIZE); - const imageData = ctx.getImageData(0, 0, SIZE, SIZE); + ctx.drawImage(img, 0, 0, w, h); + const imageData = ctx.getImageData(0, 0, w, h); const texture = new THREE.CanvasTexture(canvas); texture.wrapS = texture.wrapT = THREE.RepeatWrapping; texture.name = file.name; - resolve({ name: file.name, fullCanvas: canvas, texture, imageData, width: SIZE, height: SIZE }); + resolve({ name: file.name, fullCanvas: canvas, texture, imageData, width: w, height: h }); }; img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('Failed to load image')); }; img.src = url;