Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ <h2 data-i18n="sections.displacementMap">Displacement Map</h2>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span data-i18n="ui.uploadCustomMap">Upload custom map</span>
</label>
<input type="file" id="texture-file-input" accept="image/*" hidden />
<input type="file" id="texture-file-input" accept="image/*" multiple hidden />
<div id="active-map-name" class="active-map-name" data-i18n="ui.noMapSelected">No map selected</div>
<div class="form-row slider-row">
<label for="texture-smoothing" data-i18n="labels.textureSmoothing" data-i18n-title="tooltips.textureSmoothing" title="Applies a Gaussian blur to the displacement map. Higher values produce softer, more gradual surface detail. 0 = off.">Texture Smoothing ⓘ</label>
Expand Down
64 changes: 56 additions & 8 deletions js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 ──
Expand Down
6 changes: 5 additions & 1 deletion js/presetTextures.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
34 changes: 32 additions & 2 deletions style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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;
Expand Down