Files
archived-stlTexturizer/index.html
T
2026-03-18 12:47:49 +01:00

328 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en" data-lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CNC Kitchen STL Texturizer</title>
<link rel="stylesheet" href="style.css" />
<script>
// Apply saved theme before first paint to avoid flash
(function() {
const t = localStorage.getItem('stlt-theme');
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
})();
// Apply saved language before first paint to avoid flash
(function() {
const l = localStorage.getItem('stlt-lang');
if (l === 'de' || l === 'en') {
document.documentElement.setAttribute('data-lang', l);
document.documentElement.setAttribute('lang', l);
} else if (navigator.language && navigator.language.toLowerCase().startsWith('de')) {
document.documentElement.setAttribute('data-lang', 'de');
document.documentElement.setAttribute('lang', 'de');
}
})();
</script>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
}
}
</script>
</head>
<body>
<header>
<div class="logo">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="#7c6aff" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="#7c6aff" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="#a08cff" stroke-width="1.5" stroke-linejoin="round"/>
</svg>
<span>CNC Kitchen STL Texturizer</span>
</div>
<div class="header-actions">
<div class="lang-seg">
<button class="lang-btn active" data-lang-code="en">EN</button>
<button class="lang-btn" data-lang-code="de">DE</button>
</div>
<button id="theme-toggle" class="theme-toggle"
data-i18n-title="theme.toggleTitle"
data-i18n-aria-label="theme.toggleAriaLabel"
title="Toggle light / dark mode" aria-label="Toggle light/dark mode">
<span class="icon-moon" data-i18n="theme.dark">Dark Theme</span>
<span class="icon-sun" data-i18n="theme.light">Light Theme</span>
</button>
</div>
</header>
<main>
<!-- ─── 3-D Viewport ─────────────────────────────────────────── -->
<section id="viewport-section">
<div id="drop-zone" class="drop-zone">
<div id="drop-hint" class="drop-hint">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="#555" stroke-width="1.2" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="#555" stroke-width="1.2" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="#555" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
<p data-i18n-html="dropHint.text">Drop an <strong>.stl</strong> file here<br/>or <label for="stl-file-input" class="link-label">click to browse</label></p>
<input type="file" id="stl-file-input" accept=".stl" hidden />
</div>
<canvas id="viewport"></canvas>
<div id="brush-cursor"></div>
</div>
<div id="viewport-footer">
<span id="mesh-info" class="mesh-info"></span>
<label class="wireframe-toggle">
<input type="checkbox" id="wireframe-toggle" />
<span data-i18n="ui.wireframe">Wireframe</span>
</label>
<div class="viewport-controls-hint" data-i18n="ui.controlsHint">Left drag: orbit &nbsp;·&nbsp; Right drag: pan &nbsp;·&nbsp; Scroll: zoom</div>
</div>
</section>
<!-- ─── Settings Panel ───────────────────────────────────────── -->
<aside id="settings-panel">
<!-- Load STL -->
<section class="panel-section">
<label class="upload-btn" for="stl-file-input" style="width:100%;box-sizing:border-box;justify-content:center;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M12 2L2 7l10 5 10-5-10-5Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M2 17l10 5 10-5M2 12l10 5 10-5" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
<span data-i18n="ui.loadStl">Load STL…</span>
</label>
</section>
<!-- Displacement Map -->
<section class="panel-section">
<h2 data-i18n="sections.displacementMap">Displacement Map</h2>
<!-- Preset grid -->
<div id="preset-grid" class="preset-grid">
<!-- filled by js/presetTextures.js -->
</div>
<!-- Custom upload -->
<label class="upload-btn" for="texture-file-input">
<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 />
<div id="active-map-name" class="active-map-name" data-i18n="ui.noMapSelected">No map selected</div>
</section>
<!-- Projection -->
<section class="panel-section">
<h2 data-i18n="sections.projection">Projection</h2>
<div class="form-row">
<label for="mapping-mode" data-i18n="labels.mode">Mode</label>
<select id="mapping-mode">
<option value="5" selected data-i18n-opt="projection.triplanar">Triplanar</option>
<option value="6" data-i18n-opt="projection.cubic">Cubic (Box)</option>
<option value="3" data-i18n-opt="projection.cylindrical">Cylindrical</option>
<option value="4" data-i18n-opt="projection.spherical">Spherical</option>
<option value="0" data-i18n-opt="projection.planarXY">Planar XY</option>
<option value="1" data-i18n-opt="projection.planarXZ">Planar XZ</option>
<option value="2" data-i18n-opt="projection.planarYZ">Planar YZ</option>
</select>
</div>
</section>
<!-- Transform -->
<section class="panel-section">
<h2 data-i18n="sections.transform">Transform</h2>
<div class="form-row slider-row">
<label for="scale-u" data-i18n="labels.scaleU">Scale U</label>
<input type="range" id="scale-u" min="0" max="1000" step="1" value="500" />
<input type="number" class="val" id="scale-u-val" value="1" min="0.1" max="10" step="0.1" />
</div>
<div class="lock-row">
<div class="lock-line"></div>
<button id="lock-scale" class="lock-btn active" data-i18n-title="tooltips.proportionalScaling" data-i18n-aria-label="tooltips.proportionalScalingAria" title="Proportional scaling (U = V)" aria-pressed="true">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none">
<rect x="3" y="11" width="18" height="11" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<div class="lock-line"></div>
</div>
<div class="form-row slider-row">
<label for="scale-v" data-i18n="labels.scaleV">Scale V</label>
<input type="range" id="scale-v" min="0" max="1000" step="1" value="500" />
<input type="number" class="val" id="scale-v-val" value="1" min="0.1" max="10" step="0.1" />
</div>
<div class="form-row slider-row">
<label for="offset-u" data-i18n="labels.offsetU">Offset U</label>
<input type="range" id="offset-u" min="-1" max="1" step="0.01" value="0" />
<input type="number" class="val" id="offset-u-val" value="0" min="-1" max="1" step="0.01" />
</div>
<div class="form-row slider-row">
<label for="offset-v" data-i18n="labels.offsetV">Offset V</label>
<input type="range" id="offset-v" min="-1" max="1" step="0.01" value="0" />
<input type="number" class="val" id="offset-v-val" value="0" min="-1" max="1" step="0.01" />
</div>
<div class="form-row slider-row">
<label for="rotation" data-i18n="labels.rotation">Rotation</label>
<input type="range" id="rotation" min="0" max="360" step="1" value="0" />
<input type="number" class="val" id="rotation-val" value="0" min="0" max="360" step="1" />
</div>
</section>
<!-- Displacement -->
<section class="panel-section">
<h2 data-i18n="sections.displacement">Displacement</h2>
<div class="form-row slider-row">
<label for="amplitude" data-i18n="labels.amplitude">Amplitude</label>
<input type="range" id="amplitude" min="-2" max="2" step="0.01" value="0.5" />
<input type="number" class="val" id="amplitude-val" value="0.5" min="-100" max="100" step="0.01" />
</div>
</section>
<!-- Surface Mask -->
<section class="panel-section">
<h2 data-i18n="sections.surfaceMask" data-i18n-title="tooltips.surfaceMask" title="0° = no masking. Surfaces within this angle of horizontal will not be textured.">Surface Mask ⓘ</h2>
<div class="form-row slider-row">
<label for="bottom-angle-limit" data-i18n="labels.bottomFaces" data-i18n-title="tooltips.bottomFaces" title="Suppress texture on downward-facing surfaces within this angle of horizontal">Bottom faces</label>
<input type="range" id="bottom-angle-limit" min="0" max="90" step="1" value="5" />
<input type="number" class="val" id="bottom-angle-limit-val" value="5" min="0" max="90" step="1" />
</div>
<div class="form-row slider-row">
<label for="top-angle-limit" data-i18n="labels.topFaces" data-i18n-title="tooltips.topFaces" title="Suppress texture on upward-facing surfaces within this angle of horizontal">Top faces</label>
<input type="range" id="top-angle-limit" min="0" max="90" step="1" value="0" />
<input type="number" class="val" id="top-angle-limit-val" value="0" min="0" max="90" step="1" />
</div>
</section>
<!-- Surface Exclusions -->
<section class="panel-section">
<h2 id="excl-section-heading" data-i18n="sections.surfaceExclusions" data-i18n-title="tooltips.surfaceExclusions" title="Excluded surfaces appear orange and will not receive displacement during export.">Surface Exclusions ⓘ</h2>
<p id="excl-hint" class="excl-hint" style="display:none"></p>
<!-- Mode toggle: Exclude / Include Only -->
<div class="excl-mode-row">
<div class="excl-seg">
<button id="excl-mode-exclude" class="excl-seg-btn active" aria-pressed="true"
data-i18n="excl.modeExclude" data-i18n-title="excl.modeExcludeTitle"
title="Exclude mode: painted surfaces will not receive texture displacement">Exclude</button>
<button id="excl-mode-include" class="excl-seg-btn" aria-pressed="false"
data-i18n="excl.modeIncludeOnly" data-i18n-title="excl.modeIncludeOnlyTitle"
title="Include Only mode: only painted surfaces will receive texture displacement">Include Only</button>
</div>
</div>
<!-- Tool buttons -->
<div class="excl-tools">
<button id="excl-brush-btn" class="excl-tool-btn" aria-pressed="false"
data-i18n-title="excl.toolBrushTitle"
title="Brush: paint triangles to exclude">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h2c1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/>
<path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h2c1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/>
</svg>
<span data-i18n="excl.toolBrush">Brush</span>
</button>
<button id="excl-bucket-btn" class="excl-tool-btn" aria-pressed="false"
data-i18n-title="excl.toolFillTitle"
title="Bucket fill: flood-fill surface up to a threshold angle">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 11V4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h9"/>
<path d="M13 17h8m-4-4 4 4-4 4"/>
</svg>
<span data-i18n="excl.toolFill">Fill</span>
</button>
<button id="excl-erase-toggle" class="excl-tool-btn" aria-pressed="false"
data-i18n-title="excl.toolEraseTitle"
title="Toggle: mark or erase mode">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/>
<path d="M22 21H7"/>
<path d="m5 11 9 9"/>
</svg>
<span data-i18n="excl.toolErase">Erase</span>
</button>
</div>
<!-- Brush type switcher (shown only when Brush is active) -->
<div id="excl-brush-type-row" class="form-row hidden">
<label data-i18n="labels.type">Type</label>
<div class="excl-seg">
<button id="excl-brush-single" class="excl-seg-btn active" data-i18n="brushType.single">Single</button>
<button id="excl-brush-radius-btn" class="excl-seg-btn" data-i18n="brushType.radius">Radius</button>
</div>
</div>
<!-- Radius slider (shown when Brush + Radius) -->
<div id="excl-radius-row" class="form-row slider-row hidden">
<label for="excl-brush-radius-slider" data-i18n="labels.radius">Radius</label>
<input type="range" id="excl-brush-radius-slider" min="0.1" max="50" step="0.1" value="5" />
<input type="number" class="val" id="excl-brush-radius-val" value="5" min="0.1" max="50" step="0.1" />
</div>
<!-- Bucket threshold (shown when Fill is active) -->
<div id="excl-threshold-row" class="form-row slider-row hidden">
<label for="excl-threshold-slider" data-i18n="labels.maxAngle" data-i18n-title="tooltips.maxAngle" title="Maximum dihedral angle between adjacent triangles for the fill to cross">Max angle</label>
<input type="range" id="excl-threshold-slider" min="0" max="180" step="1" value="30" />
<input type="number" class="val" id="excl-threshold-val" value="30" min="0" max="180" step="1" />
</div>
<!-- Footer: count + clear -->
<div class="excl-footer">
<span id="excl-count" class="excl-count">0 faces excluded</span>
<button id="excl-clear-btn" class="excl-clear-btn" data-i18n="ui.clearAll">Clear All</button>
</div>
</section>
<!-- Export -->
<section class="panel-section">
<h2 data-i18n="sections.export" data-i18n-title="tooltips.export" title="Smaller edge length = finer displacement detail. Output is then decimated to the triangle limit.">Export ⓘ</h2>
<div class="form-row slider-row">
<label for="refine-length" data-i18n="labels.resolution" data-i18n-title="tooltips.resolution" title="Edges longer than this value will be split during export">Resolution</label>
<input type="range" id="refine-length" min="0.1" max="5" step="0.1" value="1" />
<input type="number" class="val" id="refine-length-val" value="1" min="0.1" max="5" step="0.1" />
</div>
<div class="form-row slider-row">
<label for="max-triangles" data-i18n="labels.outputTriangles" data-i18n-title="tooltips.outputTriangles" title="Mesh is fully subdivided first, then decimated down to this count">Output Triangles</label>
<input type="range" id="max-triangles" min="10000" max="5000000" step="10000" value="1000000" />
<span class="val" id="max-triangles-val">1.0 M</span>
</div>
<div id="tri-limit-warning" class="tri-limit-warning hidden" data-i18n="warnings.safetyCapHit">
⚠ 5M-triangle safety cap hit during subdivision — result may still be coarser than requested edge length.
</div>
<div id="export-progress" class="export-progress hidden">
<div class="export-progress-track">
<div id="export-progress-bar" class="export-progress-bar"></div>
<span id="export-progress-pct" class="export-progress-pct"></span>
</div>
<span id="export-progress-label" data-i18n="progress.processing">Processing…</span>
</div>
<button id="export-btn" class="export-btn" disabled data-i18n="ui.exportStl">Export STL</button>
</section>
</aside>
</main>
<!-- Sponsor popup -->
<div id="sponsor-overlay" class="sponsor-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="sponsor-title">
<div class="sponsor-modal">
<h2 id="sponsor-title" data-i18n="sponsor.title">Thanks for using CNC Kitchen STL Texturizer!</h2>
<p data-i18n-html="sponsor.body">This tool is provided <strong>completely free</strong> by CNC Kitchen.<br>
While your STL is being processed, why not check out the store that helps us keep making cool stuff for you?</p>
<a href="https://geni.us/CNCStoreTexture" target="_blank" rel="noopener noreferrer" class="sponsor-link" data-i18n="sponsor.visitStore">
🛒 Visit CNCKitchen.STORE
</a>
<label class="sponsor-no-show">
<input type="checkbox" id="sponsor-dont-show" />
<span data-i18n="sponsor.dontShow">Don't show this again</span>
</label>
<button id="sponsor-close" class="sponsor-close-btn" data-i18n-html="sponsor.closeAndContinue">Close &amp; Continue</button>
</div>
</div>
<script type="module" src="js/main.js"></script>
</body>
</html>