mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
added different languages
This commit is contained in:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
+86
-57
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
@@ -11,6 +11,17 @@
|
|||||||
const t = localStorage.getItem('stlt-theme');
|
const t = localStorage.getItem('stlt-theme');
|
||||||
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
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>
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
@@ -31,10 +42,19 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>CNC Kitchen STL Texturizer</span>
|
<span>CNC Kitchen STL Texturizer</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="theme-toggle" class="theme-toggle" title="Toggle light / dark mode" aria-label="Toggle light/dark mode" style="margin-left:auto">
|
<div class="header-actions">
|
||||||
<span class="icon-moon">Dark Theme</span>
|
<div class="lang-seg">
|
||||||
<span class="icon-sun">Light Theme</span>
|
<button class="lang-btn active" data-lang-code="en">EN</button>
|
||||||
</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>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
@@ -47,7 +67,7 @@
|
|||||||
<path d="M2 17L12 22L22 17" 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"/>
|
<path d="M2 12L12 17L22 12" stroke="#555" stroke-width="1.2" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
<p>Drop an <strong>.stl</strong> file here<br/>or <label for="stl-file-input" class="link-label">click to browse</label></p>
|
<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 />
|
<input type="file" id="stl-file-input" accept=".stl" hidden />
|
||||||
</div>
|
</div>
|
||||||
<canvas id="viewport"></canvas>
|
<canvas id="viewport"></canvas>
|
||||||
@@ -57,9 +77,9 @@
|
|||||||
<span id="mesh-info" class="mesh-info"></span>
|
<span id="mesh-info" class="mesh-info"></span>
|
||||||
<label class="wireframe-toggle">
|
<label class="wireframe-toggle">
|
||||||
<input type="checkbox" id="wireframe-toggle" />
|
<input type="checkbox" id="wireframe-toggle" />
|
||||||
Wireframe
|
<span data-i18n="ui.wireframe">Wireframe</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="viewport-controls-hint">Left drag: orbit · Right drag: pan · Scroll: zoom</div>
|
<div class="viewport-controls-hint" data-i18n="ui.controlsHint">Left drag: orbit · Right drag: pan · Scroll: zoom</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -70,13 +90,13 @@
|
|||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<label class="upload-btn" for="stl-file-input" style="width:100%;box-sizing:border-box;justify-content:center;">
|
<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>
|
<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>
|
||||||
Load STL…
|
<span data-i18n="ui.loadStl">Load STL…</span>
|
||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Displacement Map -->
|
<!-- Displacement Map -->
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h2>Displacement Map</h2>
|
<h2 data-i18n="sections.displacementMap">Displacement Map</h2>
|
||||||
|
|
||||||
<!-- Preset grid -->
|
<!-- Preset grid -->
|
||||||
<div id="preset-grid" class="preset-grid">
|
<div id="preset-grid" class="preset-grid">
|
||||||
@@ -86,41 +106,41 @@
|
|||||||
<!-- Custom upload -->
|
<!-- Custom upload -->
|
||||||
<label class="upload-btn" for="texture-file-input">
|
<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>
|
<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>
|
||||||
Upload custom map
|
<span data-i18n="ui.uploadCustomMap">Upload custom map</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="file" id="texture-file-input" accept="image/*" hidden />
|
<input type="file" id="texture-file-input" accept="image/*" hidden />
|
||||||
<div id="active-map-name" class="active-map-name">No map selected</div>
|
<div id="active-map-name" class="active-map-name" data-i18n="ui.noMapSelected">No map selected</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Projection -->
|
<!-- Projection -->
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h2>Projection</h2>
|
<h2 data-i18n="sections.projection">Projection</h2>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="mapping-mode">Mode</label>
|
<label for="mapping-mode" data-i18n="labels.mode">Mode</label>
|
||||||
<select id="mapping-mode">
|
<select id="mapping-mode">
|
||||||
<option value="5" selected>Triplanar</option>
|
<option value="5" selected data-i18n-opt="projection.triplanar">Triplanar</option>
|
||||||
<option value="6">Cubic (Box)</option>
|
<option value="6" data-i18n-opt="projection.cubic">Cubic (Box)</option>
|
||||||
<option value="3">Cylindrical</option>
|
<option value="3" data-i18n-opt="projection.cylindrical">Cylindrical</option>
|
||||||
<option value="4">Spherical</option>
|
<option value="4" data-i18n-opt="projection.spherical">Spherical</option>
|
||||||
<option value="0">Planar XY</option>
|
<option value="0" data-i18n-opt="projection.planarXY">Planar XY</option>
|
||||||
<option value="1">Planar XZ</option>
|
<option value="1" data-i18n-opt="projection.planarXZ">Planar XZ</option>
|
||||||
<option value="2">Planar YZ</option>
|
<option value="2" data-i18n-opt="projection.planarYZ">Planar YZ</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Transform -->
|
<!-- Transform -->
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h2>Transform</h2>
|
<h2 data-i18n="sections.transform">Transform</h2>
|
||||||
|
|
||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="scale-u">Scale U</label>
|
<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="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" />
|
<input type="number" class="val" id="scale-u-val" value="1" min="0.1" max="10" step="0.1" />
|
||||||
</div>
|
</div>
|
||||||
<div class="lock-row">
|
<div class="lock-row">
|
||||||
<div class="lock-line"></div>
|
<div class="lock-line"></div>
|
||||||
<button id="lock-scale" class="lock-btn active" title="Proportional scaling (U = V)" aria-pressed="true">
|
<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">
|
<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"/>
|
<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"/>
|
<path d="M7 11V7a5 5 0 0 1 10 0v4" stroke="currentColor" stroke-width="2"/>
|
||||||
@@ -130,22 +150,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="scale-v">Scale V</label>
|
<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="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" />
|
<input type="number" class="val" id="scale-v-val" value="1" min="0.1" max="10" step="0.1" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="offset-u">Offset U</label>
|
<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="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" />
|
<input type="number" class="val" id="offset-u-val" value="0" min="-1" max="1" step="0.01" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="offset-v">Offset V</label>
|
<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="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" />
|
<input type="number" class="val" id="offset-v-val" value="0" min="-1" max="1" step="0.01" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="rotation">Rotation</label>
|
<label for="rotation" data-i18n="labels.rotation">Rotation</label>
|
||||||
<input type="range" id="rotation" min="0" max="360" step="1" value="0" />
|
<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" />
|
<input type="number" class="val" id="rotation-val" value="0" min="0" max="360" step="1" />
|
||||||
</div>
|
</div>
|
||||||
@@ -153,9 +173,9 @@
|
|||||||
|
|
||||||
<!-- Displacement -->
|
<!-- Displacement -->
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h2>Displacement</h2>
|
<h2 data-i18n="sections.displacement">Displacement</h2>
|
||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="amplitude">Amplitude</label>
|
<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="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" />
|
<input type="number" class="val" id="amplitude-val" value="0.5" min="-100" max="100" step="0.01" />
|
||||||
</div>
|
</div>
|
||||||
@@ -163,14 +183,14 @@
|
|||||||
|
|
||||||
<!-- Surface Mask -->
|
<!-- Surface Mask -->
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h2 title="0° = no masking. Surfaces within this angle of horizontal will not be textured.">Surface Mask ⓘ</h2>
|
<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">
|
<div class="form-row slider-row">
|
||||||
<label for="bottom-angle-limit" title="Suppress texture on downward-facing surfaces within this angle of horizontal">Bottom faces</label>
|
<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="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" />
|
<input type="number" class="val" id="bottom-angle-limit-val" value="5" min="0" max="90" step="1" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="top-angle-limit" title="Suppress texture on upward-facing surfaces within this angle of horizontal">Top faces</label>
|
<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="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" />
|
<input type="number" class="val" id="top-angle-limit-val" value="0" min="0" max="90" step="1" />
|
||||||
</div>
|
</div>
|
||||||
@@ -178,63 +198,72 @@
|
|||||||
|
|
||||||
<!-- Surface Exclusions -->
|
<!-- Surface Exclusions -->
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h2 id="excl-section-heading" title="Excluded surfaces appear orange and will not receive displacement during export.">Surface Exclusions ⓘ</h2>
|
<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 -->
|
<!-- Mode toggle: Exclude / Include Only -->
|
||||||
<div class="excl-mode-row">
|
<div class="excl-mode-row">
|
||||||
<div class="excl-seg">
|
<div class="excl-seg">
|
||||||
<button id="excl-mode-exclude" class="excl-seg-btn active" aria-pressed="true"
|
<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>
|
title="Exclude mode: painted surfaces will not receive texture displacement">Exclude</button>
|
||||||
<button id="excl-mode-include" class="excl-seg-btn" aria-pressed="false"
|
<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>
|
title="Include Only mode: only painted surfaces will receive texture displacement">Include Only</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tool buttons -->
|
<!-- Tool buttons -->
|
||||||
<div class="excl-tools">
|
<div class="excl-tools">
|
||||||
<button id="excl-brush-btn" class="excl-tool-btn" title="Brush: paint triangles to exclude" aria-pressed="false">
|
<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">
|
<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="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"/>
|
<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>
|
</svg>
|
||||||
Brush
|
<span data-i18n="excl.toolBrush">Brush</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="excl-bucket-btn" class="excl-tool-btn" title="Bucket fill: flood-fill surface up to a threshold angle" aria-pressed="false">
|
<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">
|
<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="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"/>
|
<path d="M13 17h8m-4-4 4 4-4 4"/>
|
||||||
</svg>
|
</svg>
|
||||||
Fill
|
<span data-i18n="excl.toolFill">Fill</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="excl-erase-toggle" class="excl-tool-btn" title="Toggle: mark or erase mode" aria-pressed="false">
|
<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">
|
<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="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="M22 21H7"/>
|
||||||
<path d="m5 11 9 9"/>
|
<path d="m5 11 9 9"/>
|
||||||
</svg>
|
</svg>
|
||||||
Erase
|
<span data-i18n="excl.toolErase">Erase</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Brush type switcher (shown only when Brush is active) -->
|
<!-- Brush type switcher (shown only when Brush is active) -->
|
||||||
<div id="excl-brush-type-row" class="form-row hidden">
|
<div id="excl-brush-type-row" class="form-row hidden">
|
||||||
<label>Type</label>
|
<label data-i18n="labels.type">Type</label>
|
||||||
<div class="excl-seg">
|
<div class="excl-seg">
|
||||||
<button id="excl-brush-single" class="excl-seg-btn active">Single</button>
|
<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">Radius</button>
|
<button id="excl-brush-radius-btn" class="excl-seg-btn" data-i18n="brushType.radius">Radius</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Radius slider (shown when Brush + Radius) -->
|
<!-- Radius slider (shown when Brush + Radius) -->
|
||||||
<div id="excl-radius-row" class="form-row slider-row hidden">
|
<div id="excl-radius-row" class="form-row slider-row hidden">
|
||||||
<label for="excl-brush-radius-slider">Radius</label>
|
<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="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" />
|
<input type="number" class="val" id="excl-brush-radius-val" value="5" min="0.1" max="50" step="0.1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bucket threshold (shown when Fill is active) -->
|
<!-- Bucket threshold (shown when Fill is active) -->
|
||||||
<div id="excl-threshold-row" class="form-row slider-row hidden">
|
<div id="excl-threshold-row" class="form-row slider-row hidden">
|
||||||
<label for="excl-threshold-slider" title="Maximum dihedral angle between adjacent triangles for the fill to cross">Max angle</label>
|
<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="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" />
|
<input type="number" class="val" id="excl-threshold-val" value="30" min="0" max="180" step="1" />
|
||||||
</div>
|
</div>
|
||||||
@@ -242,24 +271,24 @@
|
|||||||
<!-- Footer: count + clear -->
|
<!-- Footer: count + clear -->
|
||||||
<div class="excl-footer">
|
<div class="excl-footer">
|
||||||
<span id="excl-count" class="excl-count">0 faces excluded</span>
|
<span id="excl-count" class="excl-count">0 faces excluded</span>
|
||||||
<button id="excl-clear-btn" class="excl-clear-btn">Clear All</button>
|
<button id="excl-clear-btn" class="excl-clear-btn" data-i18n="ui.clearAll">Clear All</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Export -->
|
<!-- Export -->
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h2 title="Smaller edge length = finer displacement detail. Output is then decimated to the triangle limit.">Export ⓘ</h2>
|
<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">
|
<div class="form-row slider-row">
|
||||||
<label for="refine-length" title="Edges longer than this value will be split during export">Max Edge Length</label>
|
<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="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" />
|
<input type="number" class="val" id="refine-length-val" value="1" min="0.1" max="5" step="0.1" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="max-triangles" title="Mesh is fully subdivided first, then decimated down to this count">Output Triangles</label>
|
<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" />
|
<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>
|
<span class="val" id="max-triangles-val">1.0 M</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="tri-limit-warning" class="tri-limit-warning hidden">
|
<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.
|
⚠ 5M-triangle safety cap hit during subdivision — result may still be coarser than requested edge length.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -268,9 +297,9 @@
|
|||||||
<div id="export-progress-bar" class="export-progress-bar"></div>
|
<div id="export-progress-bar" class="export-progress-bar"></div>
|
||||||
<span id="export-progress-pct" class="export-progress-pct"></span>
|
<span id="export-progress-pct" class="export-progress-pct"></span>
|
||||||
</div>
|
</div>
|
||||||
<span id="export-progress-label">Processing…</span>
|
<span id="export-progress-label" data-i18n="progress.processing">Processing…</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="export-btn" class="export-btn" disabled>Export STL</button>
|
<button id="export-btn" class="export-btn" disabled data-i18n="ui.exportStl">Export STL</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
@@ -279,17 +308,17 @@
|
|||||||
<!-- Sponsor popup -->
|
<!-- Sponsor popup -->
|
||||||
<div id="sponsor-overlay" class="sponsor-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="sponsor-title">
|
<div id="sponsor-overlay" class="sponsor-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="sponsor-title">
|
||||||
<div class="sponsor-modal">
|
<div class="sponsor-modal">
|
||||||
<h2 id="sponsor-title">Thanks for using CNC Kitchen STL Texturizer!</h2>
|
<h2 id="sponsor-title" data-i18n="sponsor.title">Thanks for using CNC Kitchen STL Texturizer!</h2>
|
||||||
<p>This tool is provided <strong>completely free</strong> by CNC Kitchen.<br>
|
<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>
|
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">
|
<a href="https://geni.us/CNCStoreTexture" target="_blank" rel="noopener noreferrer" class="sponsor-link" data-i18n="sponsor.visitStore">
|
||||||
🛒 Visit CNCKitchen.STORE
|
🛒 Visit CNCKitchen.STORE
|
||||||
</a>
|
</a>
|
||||||
<label class="sponsor-no-show">
|
<label class="sponsor-no-show">
|
||||||
<input type="checkbox" id="sponsor-dont-show" />
|
<input type="checkbox" id="sponsor-dont-show" />
|
||||||
Don't show this again
|
<span data-i18n="sponsor.dontShow">Don't show this again</span>
|
||||||
</label>
|
</label>
|
||||||
<button id="sponsor-close" class="sponsor-close-btn">Close & Continue</button>
|
<button id="sponsor-close" class="sponsor-close-btn" data-i18n-html="sponsor.closeAndContinue">Close & Continue</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+316
@@ -0,0 +1,316 @@
|
|||||||
|
// ── Translations ──────────────────────────────────────────────────────────────
|
||||||
|
// All UI strings in one place. Use {placeholder} syntax for dynamic values.
|
||||||
|
|
||||||
|
export const TRANSLATIONS = {
|
||||||
|
en: {
|
||||||
|
// Theme toggle
|
||||||
|
'theme.dark': 'Dark Theme',
|
||||||
|
'theme.light': 'Light Theme',
|
||||||
|
'theme.toggleTitle': 'Toggle light / dark mode',
|
||||||
|
'theme.toggleAriaLabel': 'Toggle light/dark mode',
|
||||||
|
|
||||||
|
// Drop zone
|
||||||
|
'dropHint.text': 'Drop an <strong>.stl</strong> file here<br/>or <label for="stl-file-input" class="link-label">click to browse</label>',
|
||||||
|
|
||||||
|
// Viewport footer
|
||||||
|
'ui.wireframe': 'Wireframe',
|
||||||
|
'ui.controlsHint': 'Left drag: orbit \u00a0·\u00a0 Right drag: pan \u00a0·\u00a0 Scroll: zoom',
|
||||||
|
'ui.meshInfo': '{n} triangles · {mb} MB',
|
||||||
|
|
||||||
|
// Load STL button
|
||||||
|
'ui.loadStl': 'Load STL\u2026',
|
||||||
|
|
||||||
|
// Displacement map section
|
||||||
|
'sections.displacementMap': 'Displacement Map',
|
||||||
|
'ui.uploadCustomMap': 'Upload custom map',
|
||||||
|
'ui.noMapSelected': 'No map selected',
|
||||||
|
|
||||||
|
// Projection section
|
||||||
|
'sections.projection': 'Projection',
|
||||||
|
'labels.mode': 'Mode',
|
||||||
|
'projection.triplanar': 'Triplanar',
|
||||||
|
'projection.cubic': 'Cubic (Box)',
|
||||||
|
'projection.cylindrical':'Cylindrical',
|
||||||
|
'projection.spherical': 'Spherical',
|
||||||
|
'projection.planarXY': 'Planar XY',
|
||||||
|
'projection.planarXZ': 'Planar XZ',
|
||||||
|
'projection.planarYZ': 'Planar YZ',
|
||||||
|
|
||||||
|
// Transform section
|
||||||
|
'sections.transform': 'Transform',
|
||||||
|
'labels.scaleU': 'Scale U',
|
||||||
|
'labels.scaleV': 'Scale V',
|
||||||
|
'labels.offsetU': 'Offset U',
|
||||||
|
'labels.offsetV': 'Offset V',
|
||||||
|
'labels.rotation': 'Rotation',
|
||||||
|
'tooltips.proportionalScaling': 'Proportional scaling (U = V)',
|
||||||
|
'tooltips.proportionalScalingAria': 'Proportional scaling (U = V)',
|
||||||
|
|
||||||
|
// Displacement section
|
||||||
|
'sections.displacement': 'Displacement',
|
||||||
|
'labels.amplitude': 'Amplitude',
|
||||||
|
|
||||||
|
// Surface mask section
|
||||||
|
'sections.surfaceMask': 'Surface Mask \u24d8',
|
||||||
|
'tooltips.surfaceMask': '0° = no masking. Surfaces within this angle of horizontal will not be textured.',
|
||||||
|
'labels.bottomFaces': 'Bottom faces',
|
||||||
|
'tooltips.bottomFaces': 'Suppress texture on downward-facing surfaces within this angle of horizontal',
|
||||||
|
'labels.topFaces': 'Top faces',
|
||||||
|
'tooltips.topFaces': 'Suppress texture on upward-facing surfaces within this angle of horizontal',
|
||||||
|
|
||||||
|
// Surface exclusions section
|
||||||
|
'sections.surfaceExclusions': 'Surface Exclusions \u24d8',
|
||||||
|
'sections.surfaceSelection': 'Surface Selection',
|
||||||
|
'tooltips.surfaceExclusions': 'Excluded surfaces appear orange and will not receive displacement during export.',
|
||||||
|
'tooltips.surfaceSelection': 'Selected surfaces appear green and will be the only ones to receive displacement during export.',
|
||||||
|
'excl.modeExclude': 'Exclude',
|
||||||
|
'excl.modeExcludeTitle': 'Exclude mode: painted surfaces will not receive texture displacement',
|
||||||
|
'excl.modeIncludeOnly': 'Include Only',
|
||||||
|
'excl.modeIncludeOnlyTitle': 'Include Only mode: only painted surfaces will receive texture displacement',
|
||||||
|
'excl.toolBrush': 'Brush',
|
||||||
|
'excl.toolBrushTitle': 'Brush: paint triangles to exclude',
|
||||||
|
'excl.toolFill': 'Fill',
|
||||||
|
'excl.toolFillTitle': 'Bucket fill: flood-fill surface up to a threshold angle',
|
||||||
|
'excl.toolErase': 'Erase',
|
||||||
|
'excl.toolEraseTitle': 'Toggle: mark or erase mode',
|
||||||
|
'labels.type': 'Type',
|
||||||
|
'brushType.single': 'Single',
|
||||||
|
'brushType.radius': 'Radius',
|
||||||
|
'labels.radius': 'Radius',
|
||||||
|
'labels.maxAngle': 'Max angle',
|
||||||
|
'tooltips.maxAngle': 'Maximum dihedral angle between adjacent triangles for the fill to cross',
|
||||||
|
'ui.clearAll': 'Clear All',
|
||||||
|
'excl.initExcluded': '0 faces excluded',
|
||||||
|
'excl.faceExcluded': '{n} face excluded',
|
||||||
|
'excl.facesExcluded': '{n} faces excluded',
|
||||||
|
'excl.faceSelected': '{n} face selected',
|
||||||
|
'excl.facesSelected': '{n} faces selected',
|
||||||
|
'excl.hintExclude': 'Excluded surfaces appear orange and will not receive displacement during export.',
|
||||||
|
'excl.hintInclude': 'Selected surfaces appear green and will be the only ones to receive displacement during export.',
|
||||||
|
|
||||||
|
// Export section
|
||||||
|
'sections.export': 'Export \u24d8',
|
||||||
|
'tooltips.export': 'Smaller edge length = finer displacement detail. Output is then decimated to the triangle limit.',
|
||||||
|
'labels.resolution': 'Resolution',
|
||||||
|
'tooltips.resolution': 'Edges longer than this value will be split during export',
|
||||||
|
'labels.outputTriangles': 'Output Triangles',
|
||||||
|
'tooltips.outputTriangles': 'Mesh is fully subdivided first, then decimated down to this count',
|
||||||
|
'warnings.safetyCapHit': '\u26a0 5M-triangle safety cap hit during subdivision \u2014 result may still be coarser than requested edge length.',
|
||||||
|
'ui.exportStl': 'Export STL',
|
||||||
|
|
||||||
|
// Export progress stages
|
||||||
|
'progress.subdividing': 'Subdividing mesh\u2026',
|
||||||
|
'progress.applyingDisplacement': 'Applying displacement to {n} triangles\u2026',
|
||||||
|
'progress.displacingVertices': 'Displacing vertices\u2026',
|
||||||
|
'progress.decimatingTo': 'Decimating {from} \u2192 {to} triangles\u2026',
|
||||||
|
'progress.decimating': 'Decimating: {cur} \u2192 {to} triangles',
|
||||||
|
'progress.writingStl': 'Writing STL\u2026',
|
||||||
|
'progress.done': 'Done!',
|
||||||
|
'progress.processing': 'Processing\u2026',
|
||||||
|
|
||||||
|
// Sponsor modal
|
||||||
|
'sponsor.title': 'Thanks for using CNC Kitchen STL Texturizer!',
|
||||||
|
'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?',
|
||||||
|
'sponsor.visitStore': '\uD83D\uDED2 Visit CNCKitchen.STORE',
|
||||||
|
'sponsor.dontShow': "Don\u2019t show this again",
|
||||||
|
'sponsor.closeAndContinue':'Close & Continue',
|
||||||
|
|
||||||
|
// Alerts
|
||||||
|
'alerts.loadFailed': 'Could not load STL: {msg}',
|
||||||
|
'alerts.exportFailed': 'Export failed: {msg}',
|
||||||
|
},
|
||||||
|
|
||||||
|
de: {
|
||||||
|
// Theme toggle
|
||||||
|
'theme.dark': 'Dunkles Design',
|
||||||
|
'theme.light': 'Helles Design',
|
||||||
|
'theme.toggleTitle': 'Hell/Dunkel-Modus wechseln',
|
||||||
|
'theme.toggleAriaLabel': 'Hell/Dunkel-Modus wechseln',
|
||||||
|
|
||||||
|
// Drop zone
|
||||||
|
'dropHint.text': '<strong>.stl</strong>-Datei hier ablegen<br/>oder <label for="stl-file-input" class="link-label">zum Durchsuchen klicken</label>',
|
||||||
|
|
||||||
|
// Viewport footer
|
||||||
|
'ui.wireframe': 'Drahtgitter',
|
||||||
|
'ui.controlsHint': 'Links ziehen: Drehen \u00a0·\u00a0 Rechts ziehen: Verschieben \u00a0·\u00a0 Scrollen: Zoomen',
|
||||||
|
'ui.meshInfo': '{n} Dreiecke · {mb} MB',
|
||||||
|
|
||||||
|
// Load STL button
|
||||||
|
'ui.loadStl': 'STL laden\u2026',
|
||||||
|
|
||||||
|
// Displacement map section
|
||||||
|
'sections.displacementMap': 'Verschiebungstextur',
|
||||||
|
'ui.uploadCustomMap': 'Eigene Textur hochladen',
|
||||||
|
'ui.noMapSelected': 'Keine Textur ausgew\u00e4hlt',
|
||||||
|
|
||||||
|
// Projection section
|
||||||
|
'sections.projection': 'Projektion',
|
||||||
|
'labels.mode': 'Modus',
|
||||||
|
'projection.triplanar': 'Triplanar',
|
||||||
|
'projection.cubic': 'Kubisch (Box)',
|
||||||
|
'projection.cylindrical':'Zylindrisch',
|
||||||
|
'projection.spherical': 'Sph\u00e4risch',
|
||||||
|
'projection.planarXY': 'Planar XY',
|
||||||
|
'projection.planarXZ': 'Planar XZ',
|
||||||
|
'projection.planarYZ': 'Planar YZ',
|
||||||
|
|
||||||
|
// Transform section
|
||||||
|
'sections.transform': 'Transformation',
|
||||||
|
'labels.scaleU': 'Skalierung U',
|
||||||
|
'labels.scaleV': 'Skalierung V',
|
||||||
|
'labels.offsetU': 'Versatz U',
|
||||||
|
'labels.offsetV': 'Versatz V',
|
||||||
|
'labels.rotation': 'Rotation',
|
||||||
|
'tooltips.proportionalScaling': 'Proportionale Skalierung (U = V)',
|
||||||
|
'tooltips.proportionalScalingAria': 'Proportionale Skalierung (U = V)',
|
||||||
|
|
||||||
|
// Displacement section
|
||||||
|
'sections.displacement': 'Verschiebung',
|
||||||
|
'labels.amplitude': 'Amplitude',
|
||||||
|
|
||||||
|
// Surface mask section
|
||||||
|
'sections.surfaceMask': 'Fl\u00e4chenmaske \u24d8',
|
||||||
|
'tooltips.surfaceMask': '0° = keine Maskierung. Fl\u00e4chen innerhalb dieses Winkels zur Horizontalen werden nicht texturiert.',
|
||||||
|
'labels.bottomFaces': 'Unterseiten',
|
||||||
|
'tooltips.bottomFaces': 'Textur auf nach unten gerichteten Fl\u00e4chen innerhalb dieses Winkels zur Horizontalen unterdr\u00fccken',
|
||||||
|
'labels.topFaces': 'Oberseiten',
|
||||||
|
'tooltips.topFaces': 'Textur auf nach oben gerichteten Fl\u00e4chen innerhalb dieses Winkels zur Horizontalen unterdr\u00fccken',
|
||||||
|
|
||||||
|
// Surface exclusions section
|
||||||
|
'sections.surfaceExclusions': 'Fl\u00e4chenausschluss \u24d8',
|
||||||
|
'sections.surfaceSelection': 'Fl\u00e4chenauswahl',
|
||||||
|
'tooltips.surfaceExclusions': 'Ausgeschlossene Fl\u00e4chen erscheinen orange und erhalten beim Export keine Verschiebung.',
|
||||||
|
'tooltips.surfaceSelection': 'Ausgew\u00e4hlte Fl\u00e4chen erscheinen gr\u00fcn und sind die einzigen, die beim Export eine Verschiebung erhalten.',
|
||||||
|
'excl.modeExclude': 'Ausschlie\u00dfen',
|
||||||
|
'excl.modeExcludeTitle': 'Ausschlussmodus: bemalte Fl\u00e4chen erhalten keine Texturverschiebung',
|
||||||
|
'excl.modeIncludeOnly': 'Nur einschlie\u00dfen',
|
||||||
|
'excl.modeIncludeOnlyTitle': 'Nur-einschlie\u00dfen-Modus: nur bemalte Fl\u00e4chen erhalten Texturverschiebung',
|
||||||
|
'excl.toolBrush': 'Pinsel',
|
||||||
|
'excl.toolBrushTitle': 'Pinsel: Dreiecke zum Ausschlie\u00dfen einf\u00e4rben',
|
||||||
|
'excl.toolFill': 'F\u00fcllen',
|
||||||
|
'excl.toolFillTitle': 'F\u00fcllen: Fl\u00e4che bis zu einem Winkel fluten',
|
||||||
|
'excl.toolErase': 'Radieren',
|
||||||
|
'excl.toolEraseTitle': 'Umschalten: Markieren oder Radieren',
|
||||||
|
'labels.type': 'Typ',
|
||||||
|
'brushType.single': 'Einzeln',
|
||||||
|
'brushType.radius': 'Radius',
|
||||||
|
'labels.radius': 'Radius',
|
||||||
|
'labels.maxAngle': 'Max. Winkel',
|
||||||
|
'tooltips.maxAngle': 'Maximaler Di\u00e4dralwinkel zwischen angrenzenden Dreiecken f\u00fcr die F\u00fcllung',
|
||||||
|
'ui.clearAll': 'Alles l\u00f6schen',
|
||||||
|
'excl.initExcluded': '0 Fl\u00e4chen ausgeschlossen',
|
||||||
|
'excl.faceExcluded': '{n} Fl\u00e4che ausgeschlossen',
|
||||||
|
'excl.facesExcluded': '{n} Fl\u00e4chen ausgeschlossen',
|
||||||
|
'excl.faceSelected': '{n} Fl\u00e4che ausgew\u00e4hlt',
|
||||||
|
'excl.facesSelected': '{n} Fl\u00e4chen ausgew\u00e4hlt',
|
||||||
|
'excl.hintExclude': 'Ausgeschlossene Fl\u00e4chen erscheinen orange und erhalten beim Export keine Verschiebung.',
|
||||||
|
'excl.hintInclude': 'Ausgew\u00e4hlte Fl\u00e4chen erscheinen gr\u00fcn und sind die einzigen, die beim Export eine Verschiebung erhalten.',
|
||||||
|
|
||||||
|
// Export section
|
||||||
|
'sections.export': 'Export \u24d8',
|
||||||
|
'tooltips.export': 'Kleinere Kantenl\u00e4nge = feineres Verschiebungsdetail. Die Ausgabe wird dann auf das Dreieckslimit dezimiert.',
|
||||||
|
'labels.resolution': 'Aufl\u00f6sung',
|
||||||
|
'tooltips.resolution': 'Kanten l\u00e4nger als dieser Wert werden beim Export unterteilt',
|
||||||
|
'labels.outputTriangles': 'Ausgabe-Dreiecke',
|
||||||
|
'tooltips.outputTriangles': 'Das Netz wird zuerst vollst\u00e4ndig unterteilt, dann auf diese Anzahl dezimiert',
|
||||||
|
'warnings.safetyCapHit': '\u26a0 5-Mio.-Dreiecke-Sicherheitsgrenze bei der Unterteilung erreicht \u2014 Ergebnis kann gr\u00f6ber als gew\u00fcnschte Kantenl\u00e4nge sein.',
|
||||||
|
'ui.exportStl': 'STL exportieren',
|
||||||
|
|
||||||
|
// Export progress stages
|
||||||
|
'progress.subdividing': 'Netz wird unterteilt\u2026',
|
||||||
|
'progress.applyingDisplacement': 'Verschiebung auf {n} Dreiecke anwenden\u2026',
|
||||||
|
'progress.displacingVertices': 'Vertices werden verschoben\u2026',
|
||||||
|
'progress.decimatingTo': '{from} \u2192 {to} Dreiecke dezimieren\u2026',
|
||||||
|
'progress.decimating': 'Dezimieren: {cur} \u2192 {to} Dreiecke',
|
||||||
|
'progress.writingStl': 'STL schreiben\u2026',
|
||||||
|
'progress.done': 'Fertig!',
|
||||||
|
'progress.processing': 'Verarbeitung\u2026',
|
||||||
|
|
||||||
|
// Sponsor modal
|
||||||
|
'sponsor.title': 'Danke f\u00fcr die Nutzung des CNC Kitchen STL Texturizers!',
|
||||||
|
'sponsor.body': 'Dieses Tool wird von CNC Kitchen <strong>komplett kostenlos</strong> bereitgestellt.<br>W\u00e4hrend dein STL verarbeitet wird, schau doch mal im Shop vorbei, der uns hilft, coole Sachen f\u00fcr dich zu machen!',
|
||||||
|
'sponsor.visitStore': '\uD83D\uDED2 CNCKitchen.STORE besuchen',
|
||||||
|
'sponsor.dontShow': 'Nicht mehr anzeigen',
|
||||||
|
'sponsor.closeAndContinue':'Schlie\u00dfen & Weiter',
|
||||||
|
|
||||||
|
// Alerts
|
||||||
|
'alerts.loadFailed': 'STL konnte nicht geladen werden: {msg}',
|
||||||
|
'alerts.exportFailed': 'Export fehlgeschlagen: {msg}',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── State ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let _currentLang = 'en';
|
||||||
|
|
||||||
|
// ── Core API ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a translation key in the current language, falling back to English.
|
||||||
|
* Replace {placeholder} tokens with values from `params`.
|
||||||
|
*/
|
||||||
|
export function t(key, params = {}) {
|
||||||
|
const lang = TRANSLATIONS[_currentLang] || TRANSLATIONS.en;
|
||||||
|
let str = lang[key] ?? TRANSLATIONS.en[key] ?? key;
|
||||||
|
for (const [k, v] of Object.entries(params)) {
|
||||||
|
str = str.replaceAll(`{${k}}`, v);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLang() {
|
||||||
|
return _currentLang;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLang(lang) {
|
||||||
|
if (!TRANSLATIONS[lang]) return;
|
||||||
|
_currentLang = lang;
|
||||||
|
localStorage.setItem('stlt-lang', lang);
|
||||||
|
document.documentElement.setAttribute('data-lang', lang);
|
||||||
|
document.documentElement.setAttribute('lang', lang);
|
||||||
|
applyTranslations();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk the DOM and apply translations to elements carrying data-i18n* attributes.
|
||||||
|
*/
|
||||||
|
export function applyTranslations() {
|
||||||
|
// textContent
|
||||||
|
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||||
|
el.textContent = t(el.dataset.i18n);
|
||||||
|
});
|
||||||
|
// innerHTML (safe: all values are hardcoded in this file, not user input)
|
||||||
|
document.querySelectorAll('[data-i18n-html]').forEach(el => {
|
||||||
|
el.innerHTML = t(el.dataset.i18nHtml);
|
||||||
|
});
|
||||||
|
// title attribute
|
||||||
|
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||||
|
el.title = t(el.dataset.i18nTitle);
|
||||||
|
});
|
||||||
|
// aria-label attribute
|
||||||
|
document.querySelectorAll('[data-i18n-aria-label]').forEach(el => {
|
||||||
|
el.setAttribute('aria-label', t(el.dataset.i18nAriaLabel));
|
||||||
|
});
|
||||||
|
// <option> elements (textContent doesn't work via data-i18n on options in some browsers)
|
||||||
|
document.querySelectorAll('option[data-i18n-opt]').forEach(opt => {
|
||||||
|
opt.textContent = t(opt.dataset.i18nOpt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect language from localStorage or browser preference and apply.
|
||||||
|
* Call once on startup before first render.
|
||||||
|
*/
|
||||||
|
export function initLang() {
|
||||||
|
const saved = localStorage.getItem('stlt-lang');
|
||||||
|
if (saved && TRANSLATIONS[saved]) {
|
||||||
|
_currentLang = saved;
|
||||||
|
} else if (navigator.language && navigator.language.toLowerCase().startsWith('de')) {
|
||||||
|
_currentLang = 'de';
|
||||||
|
} else {
|
||||||
|
_currentLang = 'en';
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute('data-lang', _currentLang);
|
||||||
|
document.documentElement.setAttribute('lang', _currentLang);
|
||||||
|
applyTranslations();
|
||||||
|
}
|
||||||
+47
-17
@@ -11,6 +11,7 @@ import { decimate } from './decimation.js';
|
|||||||
import { exportSTL } from './exporter.js';
|
import { exportSTL } from './exporter.js';
|
||||||
import { buildAdjacency, bucketFill,
|
import { buildAdjacency, bucketFill,
|
||||||
buildExclusionOverlayGeo, buildFaceWeights } from './exclusion.js';
|
buildExclusionOverlayGeo, buildFaceWeights } from './exclusion.js';
|
||||||
|
import { t, initLang, setLang, getLang, applyTranslations } from './i18n.js';
|
||||||
|
|
||||||
// ── State ─────────────────────────────────────────────────────────────────────
|
// ── State ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -132,6 +133,17 @@ initViewer(canvas);
|
|||||||
// Apply saved theme to 3D viewport on startup
|
// Apply saved theme to 3D viewport on startup
|
||||||
setViewerTheme(document.documentElement.getAttribute('data-theme') === 'light');
|
setViewerTheme(document.documentElement.getAttribute('data-theme') === 'light');
|
||||||
|
|
||||||
|
// Initialise language (reads localStorage / browser preference, applies translations)
|
||||||
|
initLang();
|
||||||
|
|
||||||
|
// Sync lang buttons to current language
|
||||||
|
(function() {
|
||||||
|
const lang = getLang();
|
||||||
|
document.querySelectorAll('.lang-btn').forEach(btn => {
|
||||||
|
btn.classList.toggle('active', btn.dataset.langCode === lang);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
// Theme toggle
|
// Theme toggle
|
||||||
document.getElementById('theme-toggle').addEventListener('click', () => {
|
document.getElementById('theme-toggle').addEventListener('click', () => {
|
||||||
const isLight = document.documentElement.getAttribute('data-theme') !== 'light';
|
const isLight = document.documentElement.getAttribute('data-theme') !== 'light';
|
||||||
@@ -187,6 +199,22 @@ function selectPreset(idx, swatchEl) {
|
|||||||
// ── Event wiring ──────────────────────────────────────────────────────────────
|
// ── Event wiring ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function wireEvents() {
|
function wireEvents() {
|
||||||
|
// ── Language toggle ──
|
||||||
|
document.querySelectorAll('.lang-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const lang = btn.dataset.langCode;
|
||||||
|
setLang(lang);
|
||||||
|
document.querySelectorAll('.lang-btn').forEach(b =>
|
||||||
|
b.classList.toggle('active', b.dataset.langCode === lang));
|
||||||
|
// Re-translate <option> elements (innerHTML won't reach these)
|
||||||
|
document.querySelectorAll('select[id="mapping-mode"] option[data-i18n-opt]').forEach(opt => {
|
||||||
|
opt.textContent = t(opt.dataset.i18nOpt);
|
||||||
|
});
|
||||||
|
// Refresh dynamic count text to current language
|
||||||
|
if (currentGeometry) refreshExclusionOverlay();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── STL loading ──
|
// ── STL loading ──
|
||||||
stlFileInput.addEventListener('change', (e) => {
|
stlFileInput.addEventListener('change', (e) => {
|
||||||
if (e.target.files[0]) handleSTL(e.target.files[0]);
|
if (e.target.files[0]) handleSTL(e.target.files[0]);
|
||||||
@@ -429,10 +457,10 @@ function setSelectionMode(include) {
|
|||||||
exclModeIncludeBtn.classList.toggle('active', selectionMode);
|
exclModeIncludeBtn.classList.toggle('active', selectionMode);
|
||||||
exclModeExcludeBtn.setAttribute('aria-pressed', String(!selectionMode));
|
exclModeExcludeBtn.setAttribute('aria-pressed', String(!selectionMode));
|
||||||
exclModeIncludeBtn.setAttribute('aria-pressed', String(selectionMode));
|
exclModeIncludeBtn.setAttribute('aria-pressed', String(selectionMode));
|
||||||
exclSectionHeading.textContent = selectionMode ? 'Surface Selection' : 'Surface Exclusions';
|
exclSectionHeading.textContent = selectionMode ? t('sections.surfaceSelection') : t('sections.surfaceExclusions');
|
||||||
exclHint.textContent = selectionMode
|
exclHint.textContent = selectionMode
|
||||||
? 'Selected surfaces appear green and will be the only ones to receive displacement during export.'
|
? t('excl.hintInclude')
|
||||||
: 'Excluded surfaces appear orange and will not receive displacement during export.';
|
: t('excl.hintExclude');
|
||||||
// Clear the painted set — faces had opposite semantics in the previous mode
|
// Clear the painted set — faces had opposite semantics in the previous mode
|
||||||
excludedFaces = new Set();
|
excludedFaces = new Set();
|
||||||
refreshExclusionOverlay();
|
refreshExclusionOverlay();
|
||||||
@@ -537,8 +565,8 @@ function refreshExclusionOverlay() {
|
|||||||
}
|
}
|
||||||
const n = excludedFaces.size;
|
const n = excludedFaces.size;
|
||||||
exclCount.textContent = selectionMode
|
exclCount.textContent = selectionMode
|
||||||
? `${n.toLocaleString()} face${n === 1 ? '' : 's'} selected`
|
? t(n === 1 ? 'excl.faceSelected' : 'excl.facesSelected', { n: n.toLocaleString() })
|
||||||
: `${n.toLocaleString()} face${n === 1 ? '' : 's'} excluded`;
|
: t(n === 1 ? 'excl.faceExcluded' : 'excl.facesExcluded', { n: n.toLocaleString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBrushCursor(e) {
|
function updateBrushCursor(e) {
|
||||||
@@ -676,7 +704,7 @@ async function handleSTL(file) {
|
|||||||
setExclusionOverlay(null);
|
setExclusionOverlay(null);
|
||||||
setHoverPreview(null);
|
setHoverPreview(null);
|
||||||
_lastHoverTriIdx = -1;
|
_lastHoverTriIdx = -1;
|
||||||
exclCount.textContent = '0 faces excluded';
|
exclCount.textContent = t('excl.initExcluded');
|
||||||
// Build adjacency data for brush/bucket tools (synchronous; fast enough for
|
// Build adjacency data for brush/bucket tools (synchronous; fast enough for
|
||||||
// typical STL sizes processed by this tool)
|
// typical STL sizes processed by this tool)
|
||||||
const adjData = buildAdjacency(geometry);
|
const adjData = buildAdjacency(geometry);
|
||||||
@@ -703,13 +731,13 @@ async function handleSTL(file) {
|
|||||||
|
|
||||||
const triCount = getTriangleCount(geometry);
|
const triCount = getTriangleCount(geometry);
|
||||||
const mb = ((geometry.attributes.position.array.byteLength) / 1024 / 1024).toFixed(2);
|
const mb = ((geometry.attributes.position.array.byteLength) / 1024 / 1024).toFixed(2);
|
||||||
meshInfo.textContent = `${triCount.toLocaleString()} triangles · ${mb} MB`;
|
meshInfo.textContent = t('ui.meshInfo', { n: triCount.toLocaleString(), mb });
|
||||||
|
|
||||||
exportBtn.disabled = (activeMapEntry === null);
|
exportBtn.disabled = (activeMapEntry === null);
|
||||||
updatePreview();
|
updatePreview();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load STL:', err);
|
console.error('Failed to load STL:', err);
|
||||||
alert(`Could not load STL: ${err.message}`);
|
alert(t('alerts.loadFailed', { msg: err.message }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -792,7 +820,7 @@ async function handleExport() {
|
|||||||
exportProgress.classList.remove('hidden');
|
exportProgress.classList.remove('hidden');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setProgress(0.02, 'Subdividing mesh…');
|
setProgress(0.02, t('progress.subdividing'));
|
||||||
await yieldFrame();
|
await yieldFrame();
|
||||||
|
|
||||||
// Build per-vertex exclusion weights combining user-painted exclusion + angle masking.
|
// Build per-vertex exclusion weights combining user-painted exclusion + angle masking.
|
||||||
@@ -806,12 +834,12 @@ async function handleExport() {
|
|||||||
|
|
||||||
const { geometry: subdivided, safetyCapHit } = await subdivide(
|
const { geometry: subdivided, safetyCapHit } = await subdivide(
|
||||||
currentGeometry, settings.refineLength,
|
currentGeometry, settings.refineLength,
|
||||||
(p) => setProgress(0.02 + p * 0.35, 'Subdividing mesh…'),
|
(p) => setProgress(0.02 + p * 0.35, t('progress.subdividing')),
|
||||||
faceWeights
|
faceWeights
|
||||||
);
|
);
|
||||||
|
|
||||||
const subTriCount = subdivided.attributes.position.count / 3;
|
const subTriCount = subdivided.attributes.position.count / 3;
|
||||||
setProgress(0.38, `Applying displacement to ${subTriCount.toLocaleString()} triangles…`);
|
setProgress(0.38, t('progress.applyingDisplacement', { n: subTriCount.toLocaleString() }));
|
||||||
|
|
||||||
const displaced = await runAsync(() =>
|
const displaced = await runAsync(() =>
|
||||||
applyDisplacement(
|
applyDisplacement(
|
||||||
@@ -821,17 +849,19 @@ async function handleExport() {
|
|||||||
activeMapEntry.height,
|
activeMapEntry.height,
|
||||||
settings,
|
settings,
|
||||||
currentBounds,
|
currentBounds,
|
||||||
(p) => setProgress(0.38 + p * 0.32, `Displacing vertices…`)
|
(p) => setProgress(0.38 + p * 0.32, t('progress.displacingVertices'))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const dispTriCount = displaced.attributes.position.count / 3;
|
const dispTriCount = displaced.attributes.position.count / 3;
|
||||||
const needsDecimation = dispTriCount > settings.maxTriangles;
|
const needsDecimation = dispTriCount > settings.maxTriangles;
|
||||||
triLimitWarning.classList.toggle('hidden', !safetyCapHit);
|
triLimitWarning.classList.toggle('hidden', !safetyCapHit);
|
||||||
|
// Re-apply translated warning text in case language changed since last export
|
||||||
|
triLimitWarning.textContent = t('warnings.safetyCapHit');
|
||||||
|
|
||||||
let finalGeometry = displaced;
|
let finalGeometry = displaced;
|
||||||
if (needsDecimation) {
|
if (needsDecimation) {
|
||||||
setProgress(0.71, `Decimating ${dispTriCount.toLocaleString()} → ${settings.maxTriangles.toLocaleString()} triangles…`);
|
setProgress(0.71, t('progress.decimatingTo', { from: dispTriCount.toLocaleString(), to: settings.maxTriangles.toLocaleString() }));
|
||||||
finalGeometry = await runAsync(() =>
|
finalGeometry = await runAsync(() =>
|
||||||
decimate(
|
decimate(
|
||||||
displaced,
|
displaced,
|
||||||
@@ -840,7 +870,7 @@ async function handleExport() {
|
|||||||
const cur = Math.round(dispTriCount - (dispTriCount - settings.maxTriangles) * p);
|
const cur = Math.round(dispTriCount - (dispTriCount - settings.maxTriangles) * p);
|
||||||
setProgress(
|
setProgress(
|
||||||
0.71 + p * 0.25,
|
0.71 + p * 0.25,
|
||||||
`Decimating: ${cur.toLocaleString()} → ${settings.maxTriangles.toLocaleString()} triangles`
|
t('progress.decimating', { cur: cur.toLocaleString(), to: settings.maxTriangles.toLocaleString() })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -873,7 +903,7 @@ async function handleExport() {
|
|||||||
else finalGeometry.attributes.normal.needsUpdate = true;
|
else finalGeometry.attributes.normal.needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setProgress(0.97, 'Writing STL…');
|
setProgress(0.97, t('progress.writingStl'));
|
||||||
await yieldFrame();
|
await yieldFrame();
|
||||||
|
|
||||||
const texLabel = activeMapEntry.isCustom ? 'custom' : activeMapEntry.name.replace(/\s+/g, '-');
|
const texLabel = activeMapEntry.isCustom ? 'custom' : activeMapEntry.name.replace(/\s+/g, '-');
|
||||||
@@ -881,14 +911,14 @@ async function handleExport() {
|
|||||||
const exportName = `${currentStlName}_${texLabel}_amp${ampLabel}.stl`;
|
const exportName = `${currentStlName}_${texLabel}_amp${ampLabel}.stl`;
|
||||||
exportSTL(finalGeometry, exportName);
|
exportSTL(finalGeometry, exportName);
|
||||||
|
|
||||||
setProgress(1.0, 'Done!');
|
setProgress(1.0, t('progress.done'));
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
exportProgress.classList.add('hidden');
|
exportProgress.classList.add('hidden');
|
||||||
setProgress(0, '');
|
setProgress(0, '');
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Export failed:', err);
|
console.error('Export failed:', err);
|
||||||
alert(`Export failed: ${err.message}`);
|
alert(t('alerts.exportFailed', { msg: err.message }));
|
||||||
exportProgress.classList.add('hidden');
|
exportProgress.classList.add('hidden');
|
||||||
} finally {
|
} finally {
|
||||||
isExporting = false;
|
isExporting = false;
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -30,6 +30,50 @@
|
|||||||
--success: #1a7f3c;
|
--success: #1a7f3c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Header actions (language selector + theme toggle) ──────────────── */
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-seg {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
background: var(--surface2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn:not(:last-child) {
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn.active {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Theme toggle button ─────────────────────────────────────────────── */
|
/* ── Theme toggle button ─────────────────────────────────────────────── */
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user