diff --git a/header.jpg b/header.jpg new file mode 100644 index 0000000..1fd1f99 Binary files /dev/null and b/header.jpg differ diff --git a/index.html b/index.html index 8ab0aba..4099790 100644 --- a/index.html +++ b/index.html @@ -3,8 +3,15 @@ - STL Texturizer + CNC Kitchen STL Texturizer + diff --git a/js/main.js b/js/main.js index 6a17498..65f446c 100644 --- a/js/main.js +++ b/js/main.js @@ -1,7 +1,7 @@ import * as THREE from 'three'; import { initViewer, loadGeometry, setMeshMaterial, setWireframe, getControls, getCamera, getCurrentMesh, - setExclusionOverlay, setHoverPreview } from './viewer.js'; + setExclusionOverlay, setHoverPreview, setViewerTheme } from './viewer.js'; import { loadSTLFile, computeBounds, getTriangleCount } from './stlLoader.js'; import { loadPresets, loadCustomTexture } from './presetTextures.js'; import { createPreviewMaterial, updateMaterial } from './previewMaterial.js'; @@ -128,6 +128,18 @@ const posToScale = p => parseFloat(Math.exp(_LOG_MIN + (p / 1000) * (_LOG_MAX - let PRESETS = []; initViewer(canvas); + +// Apply saved theme to 3D viewport on startup +setViewerTheme(document.documentElement.getAttribute('data-theme') === 'light'); + +// Theme toggle +document.getElementById('theme-toggle').addEventListener('click', () => { + const isLight = document.documentElement.getAttribute('data-theme') !== 'light'; + document.documentElement.setAttribute('data-theme', isLight ? 'light' : 'dark'); + localStorage.setItem('stlt-theme', isLight ? 'light' : 'dark'); + setViewerTheme(isLight); +}); + wireEvents(); // Sync scale number inputs with the slider's initial position scaleUVal.value = posToScale(parseFloat(scaleUSlider.value)); @@ -136,6 +148,11 @@ scaleVVal.value = posToScale(parseFloat(scaleVSlider.value)); loadPresets().then(presets => { PRESETS = presets; buildPresetGrid(); + // Select Noise as the default preset + const noiseIdx = PRESETS.findIndex(p => p.name === 'Noise'); + const defaultIdx = noiseIdx !== -1 ? noiseIdx : 0; + const swatches = presetGrid.querySelectorAll('.preset-swatch'); + if (swatches[defaultIdx]) selectPreset(defaultIdx, swatches[defaultIdx]); }).catch(err => console.error('Failed to load preset textures:', err)); // ── Preset grid ─────────────────────────────────────────────────────────────── @@ -261,7 +278,28 @@ function wireEvents() { linkSlider(topAngleLimitSlider, topAngleLimitVal, v => { settings.topAngleLimit = v; return v; }); // ── Export ── - exportBtn.addEventListener('click', handleExport); + exportBtn.addEventListener('click', () => { + if (localStorage.getItem('stlt-no-sponsor') === '1') { + handleExport(); + return; + } + const overlay = document.getElementById('sponsor-overlay'); + const closeBtn = document.getElementById('sponsor-close'); + const storeLink = overlay.querySelector('.sponsor-link'); + overlay.classList.remove('hidden'); + + const dismiss = () => { + if (document.getElementById('sponsor-dont-show').checked) { + localStorage.setItem('stlt-no-sponsor', '1'); + } + overlay.classList.add('hidden'); + handleExport(); + }; + + closeBtn.onclick = dismiss; + // Also start processing when the user clicks through to the store + storeLink.onclick = () => setTimeout(dismiss, 150); + }); // ── Wireframe ── wireframeToggle.addEventListener('change', () => setWireframe(wireframeToggle.checked)); diff --git a/js/presetTextures.js b/js/presetTextures.js index 16f77cf..3656169 100644 --- a/js/presetTextures.js +++ b/js/presetTextures.js @@ -24,6 +24,7 @@ const IMAGE_PRESETS = [ { name: 'Leather 2', url: 'leather2.jpg' }, { name: 'Weave', url: 'weave.jpg' }, { name: 'Wood', url: 'wood.jpg' }, + { name: 'Noise', url: 'noise.jpg' }, ]; function loadImagePreset({ name, url }) { diff --git a/js/viewer.js b/js/viewer.js index b32be74..c4e9a97 100644 --- a/js/viewer.js +++ b/js/viewer.js @@ -253,6 +253,28 @@ export function getScene() { return scene; } export function getControls() { return controls; } export function getCurrentMesh() { return currentMesh; } +export function setSceneBackground(hexColor) { + if (scene) scene.background = new THREE.Color(hexColor); +} + +export function setViewerTheme(isLight) { + if (!scene) return; + scene.background = new THREE.Color(isLight ? 0xf0f0f5 : 0x111114); + if (grid) { + scene.remove(grid); + grid.geometry.dispose(); + grid.material.dispose(); + } + grid = new THREE.GridHelper( + 200, 40, + isLight ? 0xb0b0c8 : 0x222228, + isLight ? 0xd0d0e0 : 0x1e1e24 + ); + grid.rotation.x = Math.PI / 2; + grid.position.z = 0; + scene.add(grid); +} + /** * Replace (or clear) the flat orange exclusion overlay mesh. * overlayGeo must be a non-indexed BufferGeometry with a 'position' attribute, diff --git a/style.css b/style.css index ed756bb..606298c 100644 --- a/style.css +++ b/style.css @@ -17,6 +17,48 @@ --header-h: 48px; } +[data-theme="light"] { + --bg: #f0f0f5; + --surface: #ffffff; + --surface2: #eaeaf2; + --border: #d0d0df; + --accent: #6355e0; + --accent-hover: #7c6aff; + --text: #1a1a2e; + --text-muted: #66667a; + --danger: #d93025; + --success: #1a7f3c; +} + +/* ── Theme toggle button ─────────────────────────────────────────────── */ +.theme-toggle { + display: flex; + align-items: center; + justify-content: center; + height: 28px; + padding: 0 10px; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text-muted); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, color 0.15s; + flex-shrink: 0; +} + +.theme-toggle:hover { + border-color: var(--accent); + color: var(--accent); +} + +.theme-toggle .icon-moon { display: block; } +.theme-toggle .icon-sun { display: none; } +[data-theme="light"] .theme-toggle .icon-moon { display: none; } +[data-theme="light"] .theme-toggle .icon-sun { display: block; } + html, body { height: 100%; font-family: 'Segoe UI', system-ui, sans-serif; @@ -153,7 +195,7 @@ main { .viewport-controls-hint { margin-left: auto; font-size: 11px; - color: #555566; + color: var(--text-muted); } .wireframe-toggle { @@ -198,8 +240,8 @@ main { /* ── Preset grid ─────────────────────────────────────────────────────── */ .preset-grid { display: grid; - grid-template-columns: repeat(5, 1fr); - gap: 4px; + grid-template-columns: repeat(6, 1fr); + gap: 3px; margin-bottom: 10px; } @@ -407,6 +449,12 @@ input[type="number"].val:focus { outline: none; border-color: var(--accent); } pointer-events: none; } +[data-theme="light"] .export-progress-pct { + mix-blend-mode: normal; + color: #fff; + text-shadow: 0 0 4px rgba(0,0,0,0.5); +} + /* ── Export button ───────────────────────────────────────────────────── */ .export-btn { width: 100%; @@ -597,4 +645,92 @@ input[type="number"].val:focus { outline: none; border-color: var(--accent); } } /* Hide utility (used by JS to show/hide exclusion sub-rows) */ -.form-row.hidden { display: none; } \ No newline at end of file +.form-row.hidden { display: none; } + +/* ── Sponsor / popup overlay ─────────────────────────────────────────── */ +.sponsor-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + backdrop-filter: blur(3px); +} + +.sponsor-overlay.hidden { display: none; } + +.sponsor-modal { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 28px 32px; + max-width: 420px; + width: calc(100% - 40px); + display: flex; + flex-direction: column; + gap: 14px; + box-shadow: 0 20px 60px rgba(0,0,0,0.5); +} + +.sponsor-modal h2 { + font-size: 15px; + font-weight: 700; + color: var(--text); + text-transform: none; + letter-spacing: 0; + margin: 0; +} + +.sponsor-modal p { + font-size: 13px; + color: var(--text-muted); + line-height: 1.6; + margin: 0; +} + +.sponsor-link { + display: block; + text-align: center; + padding: 10px 16px; + background: var(--accent); + color: #fff; + border-radius: var(--radius); + font-size: 13px; + font-weight: 600; + text-decoration: none; + transition: background 0.15s; +} + +.sponsor-link:hover { background: var(--accent-hover); } + +.sponsor-no-show { + display: flex; + align-items: center; + gap: 7px; + font-size: 11px; + color: var(--text-muted); + cursor: pointer; + user-select: none; +} + +.sponsor-no-show input { cursor: pointer; accent-color: var(--accent); } + +.sponsor-close-btn { + padding: 8px 16px; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; + align-self: flex-end; +} + +.sponsor-close-btn:hover { + background: var(--border); + border-color: var(--accent); +} \ No newline at end of file