From 2938e36d45f2340713ecc0aade5c07e29a6334bb Mon Sep 17 00:00:00 2001 From: 0xMDIV <0x.mdiv@gmail.com> Date: Mon, 6 Apr 2026 17:28:09 +0200 Subject: [PATCH] generate thumbnails for custom maps and make the displacement map view scrollable if we exceed the set max height --- index.html | 2 +- js/main.js | 64 ++++++++++++++++++++++++++++++++++++++------ js/presetTextures.js | 6 ++++- style.css | 34 +++++++++++++++++++++-- 4 files changed, 94 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index 9db2716..6dc2b11 100644 --- a/index.html +++ b/index.html @@ -111,7 +111,7 @@

Displacement Map

Upload custom map - +
No map selected
diff --git a/js/main.js b/js/main.js index 018284e..6863c39 100644 --- a/js/main.js +++ b/js/main.js @@ -19,6 +19,7 @@ let currentGeometry = null; // original loaded geometry let currentBounds = null; // bounds of the original geometry let currentStlName = 'model'; // base filename of the loaded STL (no extension) let activeMapEntry = null; // { name, texture, imageData, width, height, isCustom? } +let customMaps = []; // user-uploaded textures (session only) let previewMaterial = null; let isExporting = false; let previewDebounce = null; @@ -355,6 +356,49 @@ function buildPresetGrid() { }); } +function addCustomSwatch(entry) { + const swatch = document.createElement('div'); + swatch.className = 'preset-swatch custom-swatch'; + swatch.title = entry.name; + + swatch.appendChild(entry.thumbCanvas); + + const label = document.createElement('span'); + label.className = 'preset-label'; + label.textContent = entry.name; + swatch.appendChild(label); + + const removeBtn = document.createElement('button'); + removeBtn.className = 'custom-swatch-remove'; + removeBtn.textContent = '\u00d7'; + removeBtn.title = 'Remove'; + removeBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const idx = customMaps.indexOf(entry); + if (idx !== -1) customMaps.splice(idx, 1); + swatch.remove(); + if (activeMapEntry === entry) { + activeMapEntry = null; + activeMapName.textContent = t('ui.noMapSelected'); + updatePreview(); + } + }); + swatch.appendChild(removeBtn); + + swatch.addEventListener('click', () => selectCustomMap(entry, swatch)); + presetGrid.appendChild(swatch); + return swatch; +} + +function selectCustomMap(entry, swatchEl) { + document.querySelectorAll('.preset-swatch').forEach(s => s.classList.remove('active')); + swatchEl.classList.add('active'); + activeMapEntry = entry; + activeMapName.textContent = entry.name; + resetTextureSmoothing(); + updatePreview(); +} + function resetTextureSmoothing() { settings.textureSmoothing = 0; textureSmoothingSlider.value = 0; @@ -399,18 +443,22 @@ function wireEvents() { // ── Custom texture upload ── textureInput.addEventListener('change', async (e) => { - const file = e.target.files[0]; - if (!file) return; + const files = [...e.target.files]; + if (!files.length) return; try { - activeMapEntry = await loadCustomTexture(file); - activeMapEntry.isCustom = true; - activeMapName.textContent = file.name; - document.querySelectorAll('.preset-swatch').forEach(s => s.classList.remove('active')); - resetTextureSmoothing(); - updatePreview(); + let lastSwatch = null; + for (const file of files) { + if (customMaps.some(m => m.name === file.name && m.size === file.size)) continue; + const entry = await loadCustomTexture(file); + entry.size = file.size; + customMaps.push(entry); + lastSwatch = addCustomSwatch(entry); + } + if (lastSwatch) selectCustomMap(customMaps[customMaps.length - 1], lastSwatch); } catch (err) { console.error('Failed to load texture:', err); } + textureInput.value = ''; }); // ── Settings ── diff --git a/js/presetTextures.js b/js/presetTextures.js index 7ad1295..695747c 100644 --- a/js/presetTextures.js +++ b/js/presetTextures.js @@ -101,7 +101,11 @@ export function loadCustomTexture(file) { 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: w, height: h }); + + const thumb = makeCanvas(THUMB); + drawCover(thumb.getContext('2d'), img, THUMB); + + resolve({ name: file.name, thumbCanvas: thumb, fullCanvas: canvas, texture, imageData, width: w, height: h, isCustom: true }); }; img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('Failed to load image')); }; img.src = url; diff --git a/style.css b/style.css index 9285f91..2678f7d 100644 --- a/style.css +++ b/style.css @@ -397,6 +397,14 @@ main { grid-template-columns: repeat(6, 1fr); gap: 3px; margin-bottom: 10px; + margin-right: -10px; + padding-right: 7px; + /* Cap at 5 rows then scroll */ + max-height: calc((var(--sidebar-w) - 2 * 16px - 5 * 3px) / 6 * 5 + 4 * 3px); + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; } .preset-swatch { @@ -405,11 +413,11 @@ main { overflow: hidden; cursor: pointer; border: 2px solid transparent; - transition: border-color 0.15s, transform 0.1s; + transition: border-color 0.15s; position: relative; } -.preset-swatch:hover { border-color: var(--text-muted); transform: scale(1.04); } +.preset-swatch:hover { border-color: var(--text-muted); } .preset-swatch.active { border-color: var(--accent); } .preset-swatch canvas { @@ -435,6 +443,28 @@ main { .preset-swatch:hover .preset-label { opacity: 1; } +/* ── Custom swatch remove button ──────────────────────────────────────── */ +.custom-swatch-remove { + position: absolute; + top: 1px; + right: 1px; + width: 16px; + height: 16px; + border: none; + border-radius: 50%; + background: rgba(0,0,0,0.65); + color: #fff; + font-size: 12px; + line-height: 16px; + text-align: center; + padding: 0; + cursor: pointer; + opacity: 0; + transition: opacity 0.15s; +} +.custom-swatch:hover .custom-swatch-remove { opacity: 1; } +.custom-swatch-remove:hover { background: #e44; } + /* ── Custom upload button ─────────────────────────────────────────────── */ .upload-btn { display: inline-flex;