feat: enhance canvas handling with dynamic dimensions and cover-scaling for image presets

This commit is contained in:
CNCKitchen
2026-03-20 17:21:16 +01:00
parent 981a72af4d
commit 30b3e3a257
2 changed files with 39 additions and 16 deletions
+11 -5
View File
@@ -136,14 +136,20 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
tmpPos.fromBufferAttribute(posAttr, t + v); tmpPos.fromBufferAttribute(posAttr, t + v);
const k = posKey(tmpPos.x, tmpPos.y, tmpPos.z); const k = posKey(tmpPos.x, tmpPos.y, tmpPos.z);
if (userExcluded && excludedPosSet) excludedPosSet.add(k); 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); const existing = smoothNrmMap.get(k);
if (existing) { if (existing) {
existing[0] += tmpNrm.x * faceArea; existing[0] += faceNrm.x;
existing[1] += tmpNrm.y * faceArea; existing[1] += faceNrm.y;
existing[2] += tmpNrm.z * faceArea; existing[2] += faceNrm.z;
} else { } 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) { if (czX > 1e-12 || czY > 1e-12 || czZ > 1e-12) {
const za = zoneAreaMap.get(k); const za = zoneAreaMap.get(k);
+28 -11
View File
@@ -5,12 +5,27 @@ const THUMB = 80;
// ── Helpers ────────────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────────────
function makeCanvas(size = SIZE) { function makeCanvas(w, h = w) {
const c = document.createElement('canvas'); const c = document.createElement('canvas');
c.width = c.height = size; c.width = w;
c.height = h;
return c; 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 ─────────────────────────────────────────────────────── // ── Image-based presets ───────────────────────────────────────────────────────
const IMAGE_PRESETS = [ const IMAGE_PRESETS = [
@@ -32,18 +47,19 @@ function loadImagePreset({ name, url }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
const full = makeCanvas(SIZE); const { w, h } = fitDimensions(img.width, img.height);
full.getContext('2d').drawImage(img, 0, 0, SIZE, SIZE); const full = makeCanvas(w, h);
full.getContext('2d').drawImage(img, 0, 0, w, h);
const thumb = makeCanvas(THUMB); 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); const texture = new THREE.CanvasTexture(full);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping; texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.name = name; 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.onerror = () => reject(new Error(`Failed to load preset image: ${url}`));
img.src = url; img.src = url;
@@ -64,14 +80,15 @@ export function loadCustomTexture(file) {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
img.onload = () => { img.onload = () => {
URL.revokeObjectURL(url); 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'); const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, SIZE, SIZE); ctx.drawImage(img, 0, 0, w, h);
const imageData = ctx.getImageData(0, 0, SIZE, SIZE); const imageData = ctx.getImageData(0, 0, w, h);
const texture = new THREE.CanvasTexture(canvas); const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping; texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.name = file.name; 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.onerror = () => { URL.revokeObjectURL(url); reject(new Error('Failed to load image')); };
img.src = url; img.src = url;