928 lines
49 KiB
JavaScript
928 lines
49 KiB
JavaScript
|
|
function updateVariantUi() {
|
|||
|
|
[...ui.fanVariants.querySelectorAll('[data-variant]')].forEach( (button) => {
|
|||
|
|
var active = button.dataset.variant === state.fanVariant;
|
|||
|
|
button.classList.toggle('active', active);
|
|||
|
|
button.setAttribute('aria-pressed', String(active));
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
[...ui.bladeCountOptions.querySelectorAll('[data-blade-count]')].forEach( (button) => {
|
|||
|
|
var active = Number(button.dataset.bladeCount) === state.bladeCount;
|
|||
|
|
button.classList.toggle('active', active);
|
|||
|
|
button.setAttribute('aria-checked', String(active));
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
var spread = (state.bladeCount - 1) * BLADE_ANGLE_STEP;
|
|||
|
|
var bounds = modelBounds();
|
|||
|
|
ui.variantSeal.textContent = FAN.seal;
|
|||
|
|
ui.modelTag.textContent = `实时预览 · ${FAN.name} ${FAN.angles.length} 骨`;
|
|||
|
|
ui.metricSize.textContent = `${Math.round(bounds.xMax - bounds.xMin)} × ${Math.round(bounds.yMax - bounds.yMin)}`;
|
|||
|
|
ui.bladeCountSummary.textContent = `${state.bladeCount}片 · 展开${spread}°`;
|
|||
|
|
ui.bladeOrderHint.textContent = `1 右侧 · ${state.bladeCount} 左侧`;
|
|||
|
|
ui.frameToggleLabel.textContent = `包含「${FAN.name}」${state.bladeCount}片骨架(不含转轴),并将浮雕焊接到扇叶`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function loadFrameVariant(key) {
|
|||
|
|
var variant = FAN_VARIANTS[key];
|
|||
|
|
if (!variant)
|
|||
|
|
throw new Error('未找到所选扇形');
|
|||
|
|
FAN = variant;
|
|||
|
|
FAN.angles = anglesForBladeCount(state.bladeCount);
|
|||
|
|
state.fanVariant = key;
|
|||
|
|
state.maskLookup = null;
|
|||
|
|
state.frameWelds = null;
|
|||
|
|
updateActiveBounds();
|
|||
|
|
var templateAssets = FRAME_BLADE_STL_BASE64[key];
|
|||
|
|
state.frameTemplates = {
|
|||
|
|
left: parseBinarySTL(templateAssets.left),
|
|||
|
|
inner: parseBinarySTL(templateAssets.inner),
|
|||
|
|
right: parseBinarySTL(templateAssets.right)
|
|||
|
|
};
|
|||
|
|
rebuildFrameFromTemplates();
|
|||
|
|
updateVariantUi();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setFanVariant(key) {
|
|||
|
|
if (key === state.fanVariant || !FAN_VARIANTS[key])
|
|||
|
|
return;
|
|||
|
|
hideEditorPointerPreviews();
|
|||
|
|
showBusy(true, '正在切换扇形骨架');
|
|||
|
|
window.setTimeout( () => {
|
|||
|
|
try {
|
|||
|
|
loadFrameVariant(key);
|
|||
|
|
updatePrecisionGuide();
|
|||
|
|
drawSource();
|
|||
|
|
buildRelief();
|
|||
|
|
ui.status.textContent = `已切换为${FAN.name}(${FAN.shape}),图片和编辑内容已保留。`;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(error);
|
|||
|
|
ui.status.textContent = `扇形切换失败:${error.message}`;
|
|||
|
|
} finally {
|
|||
|
|
showBusy(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
, 20);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderBladeButtons() {
|
|||
|
|
ui.bladeGrid.replaceChildren();
|
|||
|
|
FAN.angles.forEach( (angle, index) => {
|
|||
|
|
var button = document.createElement('button');
|
|||
|
|
button.type = 'button';
|
|||
|
|
button.className = 'blade-button';
|
|||
|
|
button.textContent = String(index + 1);
|
|||
|
|
button.addEventListener('click', () => selectBlade(index));
|
|||
|
|
ui.bladeGrid.appendChild(button);
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
updateBladeButtons();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setBladeCount(nextCount) {
|
|||
|
|
var count = Number(nextCount);
|
|||
|
|
if (!SUPPORTED_BLADE_COUNTS.includes(count) || count === state.bladeCount)
|
|||
|
|
return;
|
|||
|
|
hideEditorPointerPreviews();
|
|||
|
|
syncActiveConfig();
|
|||
|
|
var previousAngle = FAN.angles[state.selectedBlade] ?? 0;
|
|||
|
|
showBusy(true, `正在调整为 ${count} 片扇叶`);
|
|||
|
|
window.setTimeout( () => {
|
|||
|
|
try {
|
|||
|
|
state.bladeCount = count;
|
|||
|
|
FAN.angles = anglesForBladeCount(count);
|
|||
|
|
syncActiveBladeConfigs();
|
|||
|
|
state.selectedBlade = FAN.angles.reduce( (best, angle, index) => Math.abs(angle - previousAngle) < Math.abs(FAN.angles[best] - previousAngle) ? index : best, 0);
|
|||
|
|
state.maskLookup = null;
|
|||
|
|
updateActiveBounds();
|
|||
|
|
if (state.frameTemplates)
|
|||
|
|
rebuildFrameFromTemplates();
|
|||
|
|
renderBladeButtons();
|
|||
|
|
updateVariantUi();
|
|||
|
|
updatePrecisionGuide();
|
|||
|
|
applyConfigToControls();
|
|||
|
|
drawSource();
|
|||
|
|
buildRelief();
|
|||
|
|
update3MFStructureHint();
|
|||
|
|
ui.status.textContent = `已切换为 ${count} 片扇叶 · 展开 ${(count - 1) * BLADE_ANGLE_STEP}° · 图片和编辑内容已按中心位置保留。`;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(error);
|
|||
|
|
ui.status.textContent = `扇叶数量切换失败:${error.message}`;
|
|||
|
|
} finally {
|
|||
|
|
showBusy(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
, 20);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function syncActiveConfig() {
|
|||
|
|
var config = activeConfig();
|
|||
|
|
config.scale = Number(ui.imageScale.value);
|
|||
|
|
config.offsetX = Number(ui.offsetX.value);
|
|||
|
|
config.offsetY = Number(ui.offsetY.value);
|
|||
|
|
config.rotation = Number(ui.imageRotation.value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderConfig(config, forceEditorLayer=false) {
|
|||
|
|
var canvas = config.canvas;
|
|||
|
|
var ctx = canvas.getContext('2d', {
|
|||
|
|
willReadFrequently: true
|
|||
|
|
});
|
|||
|
|
ctx.save();
|
|||
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|||
|
|
ctx.fillStyle = '#fff';
|
|||
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|||
|
|
|
|||
|
|
if (config.image && config.image.complete) {
|
|||
|
|
var img = config.image;
|
|||
|
|
var rotation = ((config.rotation % 360) + 360) % 360;
|
|||
|
|
var radians = deg(rotation);
|
|||
|
|
var iw = Math.abs(img.naturalWidth * Math.cos(radians)) + Math.abs(img.naturalHeight * Math.sin(radians));
|
|||
|
|
var ih = Math.abs(img.naturalWidth * Math.sin(radians)) + Math.abs(img.naturalHeight * Math.cos(radians));
|
|||
|
|
var baseScale = config.fit === 'cover' ? Math.max(canvas.width / iw, canvas.height / ih) : Math.min(canvas.width / iw, canvas.height / ih);
|
|||
|
|
var scale = baseScale * config.scale / 100;
|
|||
|
|
var ox = config.offsetX / 100 * canvas.width;
|
|||
|
|
var oy = config.offsetY / 100 * canvas.height;
|
|||
|
|
ctx.translate(canvas.width / 2 + ox, canvas.height / 2 + oy);
|
|||
|
|
ctx.rotate(deg(rotation));
|
|||
|
|
ctx.filter = `blur(${Number(ui.blur.value)}px)`;
|
|||
|
|
ctx.drawImage(img, -img.naturalWidth * scale / 2, -img.naturalHeight * scale / 2, img.naturalWidth * scale, img.naturalHeight * scale);
|
|||
|
|
}
|
|||
|
|
ctx.restore();
|
|||
|
|
if (config.editDirty || forceEditorLayer)
|
|||
|
|
ctx.drawImage(config.editCanvas, 0, 0, canvas.width, canvas.height);
|
|||
|
|
config.pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function update3MFStructureHint() {
|
|||
|
|
var structure = ui.threeMFStructure.value;
|
|||
|
|
var partCount = FAN.angles.length + (ui.includeFrame.checked ? 1 : 0);
|
|||
|
|
if (structure === 'parts') {
|
|||
|
|
ui.threeMFHint.textContent = `1 个总成内含 ${partCount} 个命名零件,相对位置锁定`;
|
|||
|
|
} else if (structure === 'objects') {
|
|||
|
|
ui.threeMFHint.textContent = `${partCount} 个独立对象,可在切片软件中逐件选择`;
|
|||
|
|
} else {
|
|||
|
|
ui.threeMFHint.textContent = '焊接为 1 个完整网格,适合直接切片';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updatePrecisionGuide() {
|
|||
|
|
var detail = DETAIL[ui.detail.value];
|
|||
|
|
var maxHalf = Math.max(...FAN.profile.map( (point) => point[1])) - currentReliefMargin();
|
|||
|
|
var radialStep = (FAN.rMax - FAN.rMin) / (detail.radial - 1);
|
|||
|
|
var acrossStep = 2 * maxHalf / (detail.across - 1);
|
|||
|
|
ui.precisionHint.textContent = `采样间距约 ${radialStep.toFixed(2)} × ${acrossStep.toFixed(2)} mm`;
|
|||
|
|
ui.precisionGrid.textContent = `${detail.radial} × ${detail.across} / 片`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showBusy(show, label='正在重建浮雕') {
|
|||
|
|
ui.busy.classList.toggle('show', show);
|
|||
|
|
ui.exportBtn.disabled = show;
|
|||
|
|
ui.export3mfBtn.disabled = show;
|
|||
|
|
if (show)
|
|||
|
|
ui.busyText.textContent = label;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function scheduleRebuild({redrawImage=false}={}) {
|
|||
|
|
window.clearTimeout(state.rebuildTimer);
|
|||
|
|
if (redrawImage) {
|
|||
|
|
try {
|
|||
|
|
drawSource();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(error);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
showBusy(true);
|
|||
|
|
}
|
|||
|
|
state.rebuildTimer = window.setTimeout( () => {
|
|||
|
|
try {
|
|||
|
|
if (redrawImage)
|
|||
|
|
showBusy(true);
|
|||
|
|
buildRelief();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(error);
|
|||
|
|
ui.status.textContent = `生成失败:${error.message}`;
|
|||
|
|
} finally {
|
|||
|
|
showBusy(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
, 90);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateOutput(input, suffix='') {
|
|||
|
|
var output = document.querySelector(`output[for="${input.id}"]`);
|
|||
|
|
if (output)
|
|||
|
|
output.textContent = `${input.value}${suffix}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setImageControl(input, value) {
|
|||
|
|
var min = Number(input.min);
|
|||
|
|
var max = Number(input.max);
|
|||
|
|
input.value = String(clamp(value, min, max));
|
|||
|
|
updateOutput(input, '%');
|
|||
|
|
updateFloatingTransformStatus();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function normalizeRotation(value) {
|
|||
|
|
var normalized = ((value + 180) % 360 + 360) % 360 - 180;
|
|||
|
|
return normalized === -180 && value > 0 ? 180 : normalized;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setRotationControl(value) {
|
|||
|
|
var rotation = normalizeRotation(value);
|
|||
|
|
ui.imageRotation.value = String(rotation);
|
|||
|
|
activeConfig().rotation = rotation;
|
|||
|
|
updateOutput(ui.imageRotation, '°');
|
|||
|
|
updateFloatingTransformStatus();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resetImageTransform() {
|
|||
|
|
var config = activeConfig();
|
|||
|
|
config.rotation = 0;
|
|||
|
|
config.scale = 100;
|
|||
|
|
config.offsetX = 0;
|
|||
|
|
config.offsetY = 0;
|
|||
|
|
setImageControl(ui.imageScale, 100);
|
|||
|
|
setImageControl(ui.offsetX, 0);
|
|||
|
|
setImageControl(ui.offsetY, 0);
|
|||
|
|
setRotationControl(0);
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function bindRange(input, suffix, redrawImage) {
|
|||
|
|
input.addEventListener('input', () => {
|
|||
|
|
updateOutput(input, suffix);
|
|||
|
|
if (redrawImage)
|
|||
|
|
syncActiveConfig();
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setFit(mode) {
|
|||
|
|
activeConfig().fit = mode;
|
|||
|
|
ui.coverBtn.classList.toggle('active', mode === 'cover');
|
|||
|
|
ui.containBtn.classList.toggle('active', mode === 'contain');
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateBladeButtons() {
|
|||
|
|
[...ui.bladeGrid.children].forEach( (button, index) => {
|
|||
|
|
button.classList.toggle('active', index === state.selectedBlade);
|
|||
|
|
button.classList.toggle('assigned', Boolean(state.bladeConfigs[index].image));
|
|||
|
|
var name = state.bladeConfigs[index].name || '尚未设置图片';
|
|||
|
|
button.title = `叶片 ${index + 1}:${name}`;
|
|||
|
|
button.setAttribute('aria-pressed', String(index === state.selectedBlade));
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateImageUi() {
|
|||
|
|
if (state.imageMode === 'whole') {
|
|||
|
|
var config = state.globalConfig;
|
|||
|
|
ui.coverBtn.textContent = '铺满扇面';
|
|||
|
|
ui.containBtn.textContent = '完整图片';
|
|||
|
|
ui.dropTitle.textContent = '选择或拖入一张图片';
|
|||
|
|
ui.dropHint.textContent = '页面全程离线,图片不会上传';
|
|||
|
|
if (config.image && !config.isDemo) {
|
|||
|
|
ui.fileChip.textContent = `${config.name} · ${config.image.naturalWidth} × ${config.image.naturalHeight}px`;
|
|||
|
|
ui.fileChip.classList.add('show');
|
|||
|
|
ui.demoLabel.style.display = 'none';
|
|||
|
|
} else {
|
|||
|
|
ui.fileChip.classList.remove('show');
|
|||
|
|
ui.demoLabel.textContent = '内置示例';
|
|||
|
|
ui.demoLabel.style.display = '';
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
var config = activeConfig();
|
|||
|
|
var assigned = state.bladeConfigs.filter( (item) => item.image).length;
|
|||
|
|
ui.coverBtn.textContent = '铺满叶片';
|
|||
|
|
ui.containBtn.textContent = '完整图片';
|
|||
|
|
ui.dropTitle.textContent = `为第 ${state.selectedBlade + 1} 片选择图片`;
|
|||
|
|
ui.dropHint.textContent = '可多选图片,将按叶片顺序自动填入';
|
|||
|
|
ui.fileChip.textContent = config.image ? `叶片 ${state.selectedBlade + 1} · ${config.name} · ${config.image.naturalWidth} × ${config.image.naturalHeight}px` : `叶片 ${state.selectedBlade + 1} 尚未设置 · 已完成 ${assigned} / ${FAN.angles.length} 片`;
|
|||
|
|
ui.fileChip.classList.add('show');
|
|||
|
|
ui.demoLabel.textContent = `叶片 ${state.selectedBlade + 1}`;
|
|||
|
|
ui.demoLabel.style.display = '';
|
|||
|
|
}
|
|||
|
|
updateBladeButtons();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function applyConfigToControls() {
|
|||
|
|
var config = activeConfig();
|
|||
|
|
setImageControl(ui.imageScale, config.scale);
|
|||
|
|
setImageControl(ui.offsetX, config.offsetX);
|
|||
|
|
setImageControl(ui.offsetY, config.offsetY);
|
|||
|
|
setRotationControl(config.rotation);
|
|||
|
|
ui.coverBtn.classList.toggle('active', config.fit === 'cover');
|
|||
|
|
ui.containBtn.classList.toggle('active', config.fit === 'contain');
|
|||
|
|
updateImageUi();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function selectBlade(index) {
|
|||
|
|
hideEditorPointerPreviews();
|
|||
|
|
syncActiveConfig();
|
|||
|
|
state.selectedBlade = clamp(index, 0, FAN.angles.length - 1);
|
|||
|
|
applyConfigToControls();
|
|||
|
|
drawSource();
|
|||
|
|
updateSelectionUi();
|
|||
|
|
ui.status.textContent = `正在编辑第 ${state.selectedBlade + 1} 片叶片,可单独缩放、移动和旋转。`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setImageMode(mode) {
|
|||
|
|
if (mode === state.imageMode)
|
|||
|
|
return;
|
|||
|
|
hideEditorPointerPreviews();
|
|||
|
|
syncActiveConfig();
|
|||
|
|
state.imageMode = mode;
|
|||
|
|
ui.wholeModeBtn.classList.toggle('active', mode === 'whole');
|
|||
|
|
ui.bladeModeBtn.classList.toggle('active', mode === 'blade');
|
|||
|
|
ui.bladePanel.hidden = mode === 'whole';
|
|||
|
|
ui.fileInput.multiple = mode === 'blade';
|
|||
|
|
applyConfigToControls();
|
|||
|
|
updateSelectionUi();
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function readImageFile(file) {
|
|||
|
|
return new Promise( (resolve, reject) => {
|
|||
|
|
var reader = new FileReader();
|
|||
|
|
reader.onload = () => {
|
|||
|
|
var img = new Image();
|
|||
|
|
img.onload = () => resolve({
|
|||
|
|
file,
|
|||
|
|
img
|
|||
|
|
});
|
|||
|
|
img.onerror = () => reject(new Error(`${file.name} 无法读取`));
|
|||
|
|
img.src = reader.result;
|
|||
|
|
}
|
|||
|
|
;
|
|||
|
|
reader.onerror = () => reject(new Error(`${file.name} 无法读取`));
|
|||
|
|
reader.readAsDataURL(file);
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadFiles(fileList) {
|
|||
|
|
var files = [...(fileList || [])].filter( (file) => file.type.startsWith('image/'));
|
|||
|
|
if (!files.length) {
|
|||
|
|
ui.status.textContent = '请选择 JPG、PNG 或 WEBP 图片。';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
showBusy(true);
|
|||
|
|
if (state.imageMode === 'whole') {
|
|||
|
|
var loaded = await readImageFile(files[0]);
|
|||
|
|
state.globalConfig.image = loaded.img;
|
|||
|
|
state.globalConfig.name = loaded.file.name;
|
|||
|
|
state.globalConfig.isDemo = false;
|
|||
|
|
ui.status.textContent = '整扇图片已载入,正在生成浮雕。';
|
|||
|
|
} else {
|
|||
|
|
var available = FAN.angles.length - state.selectedBlade;
|
|||
|
|
var loaded = await Promise.all(files.slice(0, available).map(readImageFile));
|
|||
|
|
loaded.forEach( ({file, img}, offset) => {
|
|||
|
|
var config = state.bladeConfigs[state.selectedBlade + offset];
|
|||
|
|
config.image = img;
|
|||
|
|
config.name = file.name;
|
|||
|
|
config.isDemo = false;
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.status.textContent = `已从第 ${state.selectedBlade + 1} 片开始载入 ${loaded.length} 张图片。`;
|
|||
|
|
}
|
|||
|
|
ui.fileInput.value = '';
|
|||
|
|
applyConfigToControls();
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(error);
|
|||
|
|
ui.status.textContent = `图片载入失败:${error.message}`;
|
|||
|
|
showBusy(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resetAll() {
|
|||
|
|
[state.globalConfig, ...state.bladeConfigSlots].forEach( (config) => {
|
|||
|
|
config.fit = 'cover';
|
|||
|
|
config.rotation = 0;
|
|||
|
|
config.scale = 100;
|
|||
|
|
config.offsetX = 0;
|
|||
|
|
config.offsetY = 0;
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
var values = {
|
|||
|
|
imageScale: 100,
|
|||
|
|
offsetX: 0,
|
|||
|
|
offsetY: 0,
|
|||
|
|
minThickness: .60,
|
|||
|
|
maxThickness: 2.20,
|
|||
|
|
reliefMargin: 0,
|
|||
|
|
contrast: 135,
|
|||
|
|
reliefGamma: .95,
|
|||
|
|
blur: .5,
|
|||
|
|
detail: 'ultra'
|
|||
|
|
};
|
|||
|
|
Object.entries(values).forEach( ([key,value]) => {
|
|||
|
|
ui[key].value = value;
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.invert.checked = false;
|
|||
|
|
ui.includeFrame.checked = true;
|
|||
|
|
ui.threeMFStructure.value = 'merged';
|
|||
|
|
applyConfigToControls();
|
|||
|
|
updateOutput(ui.imageScale, '%');
|
|||
|
|
updateOutput(ui.offsetX, '%');
|
|||
|
|
updateOutput(ui.offsetY, '%');
|
|||
|
|
updateOutput(ui.minThickness);
|
|||
|
|
updateOutput(ui.maxThickness);
|
|||
|
|
updateOutput(ui.contrast, '%');
|
|||
|
|
updateOutput(ui.reliefMargin, ' mm');
|
|||
|
|
updateOutput(ui.reliefGamma);
|
|||
|
|
updateOutput(ui.blur, ' px');
|
|||
|
|
updatePrecisionGuide();
|
|||
|
|
update3MFStructureHint();
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function bindEvents() {
|
|||
|
|
ui.fanVariants.addEventListener('click', (event) => {
|
|||
|
|
var button = event.target.closest('[data-variant]');
|
|||
|
|
if (button)
|
|||
|
|
setFanVariant(button.dataset.variant);
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.bladeCountOptions.addEventListener('click', (event) => {
|
|||
|
|
var button = event.target.closest('[data-blade-count]');
|
|||
|
|
if (button)
|
|||
|
|
setBladeCount(button.dataset.bladeCount);
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.wholeModeBtn.addEventListener('click', () => setImageMode('whole'));
|
|||
|
|
ui.bladeModeBtn.addEventListener('click', () => setImageMode('blade'));
|
|||
|
|
ui.fileInput.addEventListener('change', () => loadFiles(ui.fileInput.files));
|
|||
|
|
['dragenter', 'dragover'].forEach( (name) => ui.dropzone.addEventListener(name, (event) => {
|
|||
|
|
event.preventDefault();
|
|||
|
|
ui.dropzone.classList.add('is-dragging');
|
|||
|
|
}
|
|||
|
|
));
|
|||
|
|
['dragleave', 'drop'].forEach( (name) => ui.dropzone.addEventListener(name, (event) => {
|
|||
|
|
event.preventDefault();
|
|||
|
|
ui.dropzone.classList.remove('is-dragging');
|
|||
|
|
}
|
|||
|
|
));
|
|||
|
|
ui.dropzone.addEventListener('drop', (event) => loadFiles(event.dataTransfer.files));
|
|||
|
|
ui.coverBtn.addEventListener('click', () => setFit('cover'));
|
|||
|
|
ui.containBtn.addEventListener('click', () => setFit('contain'));
|
|||
|
|
ui.rotateLeft.addEventListener('click', () => {
|
|||
|
|
setRotationControl(Number(ui.imageRotation.value) - 90);
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.rotateRight.addEventListener('click', () => {
|
|||
|
|
setRotationControl(Number(ui.imageRotation.value) + 90);
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.zoomOut.addEventListener('click', () => {
|
|||
|
|
setImageControl(ui.imageScale, Number(ui.imageScale.value) - 10);
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.zoomIn.addEventListener('click', () => {
|
|||
|
|
setImageControl(ui.imageScale, Number(ui.imageScale.value) + 10);
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.resetImage.addEventListener('click', resetImageTransform);
|
|||
|
|
|
|||
|
|
document.querySelectorAll('[data-editor-tool]').forEach( (button) => {
|
|||
|
|
button.addEventListener('click', () => setEditorTool(button.dataset.editorTool));
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
document.querySelectorAll('[data-shape-ratio]').forEach( (button) => {
|
|||
|
|
button.addEventListener('click', () => setShapeRatioMode(button.dataset.shapeRatio));
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.undoEdit.addEventListener('click', undoEditorChange);
|
|||
|
|
ui.clearEdit.addEventListener('click', clearEditorChanges);
|
|||
|
|
ui.deleteSelected.addEventListener('click', deleteSelectedObject);
|
|||
|
|
ui.brushSize.addEventListener('input', () => {
|
|||
|
|
updateOutput(ui.brushSize, ' px');
|
|||
|
|
ui.floatBrushSize.value = ui.brushSize.value;
|
|||
|
|
ui.floatBrushSizeValue.textContent = ui.brushSize.value;
|
|||
|
|
refreshBrushCursorSize();
|
|||
|
|
refreshEditorPointerPreview();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.textSize.addEventListener('input', () => {
|
|||
|
|
updateOutput(ui.textSize, ' px');
|
|||
|
|
ui.floatTextSize.value = ui.textSize.value;
|
|||
|
|
ui.floatTextSizeValue.textContent = ui.textSize.value;
|
|||
|
|
applyTextControlsToSelected();
|
|||
|
|
refreshEditorPointerPreview();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.textValue.addEventListener('input', () => {
|
|||
|
|
ui.floatTextValue.value = ui.textValue.value;
|
|||
|
|
applyTextControlsToSelected();
|
|||
|
|
refreshEditorPointerPreview();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.textFontChips.querySelectorAll('[data-text-font]').forEach( (button) => {
|
|||
|
|
button.addEventListener('click', () => setTextFont(button.dataset.textFont));
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.textStyle.addEventListener('change', () => setTextStyle(ui.textStyle.value));
|
|||
|
|
ui.textArc.addEventListener('input', () => setTextArc(ui.textArc.value));
|
|||
|
|
ui.polygonSides.addEventListener('input', () => {
|
|||
|
|
updateOutput(ui.polygonSides, ' 边');
|
|||
|
|
ui.floatPolygonSides.value = ui.polygonSides.value;
|
|||
|
|
ui.floatPolygonSidesValue.textContent = ui.polygonSides.value;
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.splineTension.addEventListener('input', () => {
|
|||
|
|
updateOutput(ui.splineTension, '%');
|
|||
|
|
ui.floatSplineTension.value = ui.splineTension.value;
|
|||
|
|
ui.floatSplineTensionValue.textContent = `${ui.splineTension.value}%`;
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.editorColor.addEventListener('input', () => {
|
|||
|
|
ui.floatEditorColor.value = ui.editorColor.value;
|
|||
|
|
refreshBrushCursorSize();
|
|||
|
|
applyTextControlsToSelected();
|
|||
|
|
refreshEditorPointerPreview();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.floatBrushSize.addEventListener('input', () => {
|
|||
|
|
ui.brushSize.value = ui.floatBrushSize.value;
|
|||
|
|
ui.floatBrushSizeValue.textContent = ui.floatBrushSize.value;
|
|||
|
|
updateOutput(ui.brushSize, ' px');
|
|||
|
|
refreshBrushCursorSize();
|
|||
|
|
refreshEditorPointerPreview();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.floatTextValue.addEventListener('input', () => {
|
|||
|
|
ui.textValue.value = ui.floatTextValue.value;
|
|||
|
|
applyTextControlsToSelected();
|
|||
|
|
refreshEditorPointerPreview();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.floatTextFont.addEventListener('change', () => setTextFont(ui.floatTextFont.value));
|
|||
|
|
ui.floatTextStyle.addEventListener('change', () => setTextStyle(ui.floatTextStyle.value));
|
|||
|
|
ui.floatTextSize.addEventListener('input', () => {
|
|||
|
|
ui.textSize.value = ui.floatTextSize.value;
|
|||
|
|
ui.floatTextSizeValue.textContent = ui.floatTextSize.value;
|
|||
|
|
updateOutput(ui.textSize, ' px');
|
|||
|
|
applyTextControlsToSelected();
|
|||
|
|
refreshEditorPointerPreview();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.floatTextArc.addEventListener('input', () => setTextArc(ui.floatTextArc.value));
|
|||
|
|
ui.floatPolygonSides.addEventListener('input', () => {
|
|||
|
|
ui.polygonSides.value = ui.floatPolygonSides.value;
|
|||
|
|
ui.floatPolygonSidesValue.textContent = ui.floatPolygonSides.value;
|
|||
|
|
updateOutput(ui.polygonSides, ' 边');
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.floatSplineTension.addEventListener('input', () => {
|
|||
|
|
ui.splineTension.value = ui.floatSplineTension.value;
|
|||
|
|
ui.floatSplineTensionValue.textContent = `${ui.floatSplineTension.value}%`;
|
|||
|
|
updateOutput(ui.splineTension, '%');
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.floatShapeRatio.addEventListener('change', () => setShapeRatioMode(ui.floatShapeRatio.value));
|
|||
|
|
ui.floatEditorColor.addEventListener('input', () => {
|
|||
|
|
ui.editorColor.value = ui.floatEditorColor.value;
|
|||
|
|
refreshBrushCursorSize();
|
|||
|
|
applyTextControlsToSelected();
|
|||
|
|
refreshEditorPointerPreview();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.floatRotateLeft.addEventListener('click', () => {
|
|||
|
|
setRotationControl(Number(ui.imageRotation.value) - 90);
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.floatRotateRight.addEventListener('click', () => {
|
|||
|
|
setRotationControl(Number(ui.imageRotation.value) + 90);
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.floatImageRotation.addEventListener('input', () => {
|
|||
|
|
setRotationControl(Number(ui.floatImageRotation.value));
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.floatZoomOut.addEventListener('click', () => {
|
|||
|
|
setImageControl(ui.imageScale, Number(ui.imageScale.value) - 10);
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.floatZoomIn.addEventListener('click', () => {
|
|||
|
|
setImageControl(ui.imageScale, Number(ui.imageScale.value) + 10);
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.floatResetImage.addEventListener('click', resetImageTransform);
|
|||
|
|
ui.floatUndoEdit.addEventListener('click', undoEditorChange);
|
|||
|
|
ui.floatDeleteSelected.addEventListener('click', deleteSelectedObject);
|
|||
|
|
ui.floatClearEdit.addEventListener('click', clearEditorChanges);
|
|||
|
|
ui.expandMask.addEventListener('click', () => setMaskExpanded(!state.maskExpanded));
|
|||
|
|
ui.maskBackdrop.addEventListener('click', () => setMaskExpanded(false));
|
|||
|
|
window.addEventListener('resize', fitExpandedMask);
|
|||
|
|
document.addEventListener('keydown', (event) => {
|
|||
|
|
if (event.key === 'Escape' && state.maskExpanded)
|
|||
|
|
setMaskExpanded(false);
|
|||
|
|
if ((event.key === 'Delete' || event.key === 'Backspace') && state.editorTool === 'select' && !/INPUT|TEXTAREA|SELECT/.test(event.target.tagName)) {
|
|||
|
|
event.preventDefault();
|
|||
|
|
deleteSelectedObject();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
ui.maskCanvas.addEventListener('pointerdown', (event) => {
|
|||
|
|
event.preventDefault();
|
|||
|
|
updateEditorPointerPreview(event);
|
|||
|
|
if (state.editorTool !== 'move') {
|
|||
|
|
var point = state.editorTool === 'text' ? pointerOnTextEditCanvas(event) : pointerOnActiveEditCanvas(event);
|
|||
|
|
if (!point) {
|
|||
|
|
ui.status.textContent = state.imageMode === 'blade' ? `请在第 ${state.selectedBlade + 1} 片叶片的高亮范围内编辑。` : '请在扇面范围内编辑。';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var config = activeConfig();
|
|||
|
|
if (state.editorTool === 'select') {
|
|||
|
|
clearEditorPreview(false);
|
|||
|
|
var object = hitTestVectorObject(config, point);
|
|||
|
|
config.selectedObjectId = object ? object.id : null;
|
|||
|
|
if (object) {
|
|||
|
|
pushEditHistory(config);
|
|||
|
|
state.editorObjectDragging = true;
|
|||
|
|
state.editorPointerId = event.pointerId;
|
|||
|
|
state.editorObjectDragStart = point;
|
|||
|
|
state.editorObjectOriginal = JSON.parse(JSON.stringify(object));
|
|||
|
|
state.editorObjectMoved = false;
|
|||
|
|
if (object.type === 'text')
|
|||
|
|
syncTextControlsFromSelected(object);
|
|||
|
|
ui.maskCanvas.setPointerCapture(event.pointerId);
|
|||
|
|
}
|
|||
|
|
setEditorTool('select');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
pushEditHistory(config);
|
|||
|
|
if (state.editorTool === 'text') {
|
|||
|
|
if (!placeEditorText(config, point))
|
|||
|
|
config.editHistory.pop();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
state.editorDrawing = true;
|
|||
|
|
state.editorPointerId = event.pointerId;
|
|||
|
|
state.editorLastPoint = point;
|
|||
|
|
if (VECTOR_SHAPE_TOOLS.includes(state.editorTool)) {
|
|||
|
|
state.editorShapeStart = point;
|
|||
|
|
state.editorShapePoints = [point];
|
|||
|
|
var preview = createShapeObject(config, state.editorTool, point, point, state.editorShapePoints);
|
|||
|
|
renderEditorPreview(config, preview);
|
|||
|
|
} else {
|
|||
|
|
paintEditorSegment(config, point, point, state.editorTool === 'eraser');
|
|||
|
|
}
|
|||
|
|
ui.maskCanvas.setPointerCapture(event.pointerId);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
state.imageDragging = true;
|
|||
|
|
state.imagePointerX = event.clientX;
|
|||
|
|
state.imagePointerY = event.clientY;
|
|||
|
|
state.imageStartX = Number(ui.offsetX.value);
|
|||
|
|
state.imageStartY = Number(ui.offsetY.value);
|
|||
|
|
ui.maskWrap.classList.add('is-adjusting');
|
|||
|
|
ui.maskCanvas.setPointerCapture(event.pointerId);
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.maskCanvas.addEventListener('pointerenter', updateEditorPointerPreview);
|
|||
|
|
ui.maskCanvas.addEventListener('pointermove', (event) => {
|
|||
|
|
updateEditorPointerPreview(event);
|
|||
|
|
if (state.editorObjectDragging && event.pointerId === state.editorPointerId) {
|
|||
|
|
var point = pointerOnActiveEditCanvas(event);
|
|||
|
|
var config = activeConfig();
|
|||
|
|
var object = selectedVectorObject(config);
|
|||
|
|
if (point && object && state.editorObjectOriginal) {
|
|||
|
|
Object.keys(object).forEach( (key) => delete object[key]);
|
|||
|
|
Object.assign(object, JSON.parse(JSON.stringify(state.editorObjectOriginal)));
|
|||
|
|
var dx = point[0] - state.editorObjectDragStart[0];
|
|||
|
|
var dy = point[1] - state.editorObjectDragStart[1];
|
|||
|
|
translateVectorObject(object, dx, dy);
|
|||
|
|
state.editorObjectMoved = Math.hypot(dx, dy) > .5;
|
|||
|
|
refreshCommittedEditor(config, true);
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (state.editorDrawing && event.pointerId === state.editorPointerId) {
|
|||
|
|
var point = pointerOnActiveEditCanvas(event);
|
|||
|
|
if (point) {
|
|||
|
|
var config = activeConfig();
|
|||
|
|
if (VECTOR_SHAPE_TOOLS.includes(state.editorTool)) {
|
|||
|
|
if (state.editorTool === 'spline') {
|
|||
|
|
var last = state.editorShapePoints[state.editorShapePoints.length - 1];
|
|||
|
|
if (!last || Math.hypot(point[0] - last[0], point[1] - last[1]) > 2)
|
|||
|
|
state.editorShapePoints.push(point);
|
|||
|
|
}
|
|||
|
|
var preview = createShapeObject(config, state.editorTool, state.editorShapeStart, point, state.editorShapePoints);
|
|||
|
|
renderEditorPreview(config, preview);
|
|||
|
|
} else
|
|||
|
|
paintEditorSegment(config, state.editorLastPoint || point, point, state.editorTool === 'eraser');
|
|||
|
|
state.editorLastPoint = point;
|
|||
|
|
} else
|
|||
|
|
state.editorLastPoint = null;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!state.imageDragging)
|
|||
|
|
return;
|
|||
|
|
var rect = ui.maskCanvas.getBoundingClientRect();
|
|||
|
|
var pointerDX = event.clientX - state.imagePointerX;
|
|||
|
|
var pointerDY = event.clientY - state.imagePointerY;
|
|||
|
|
var dx;
|
|||
|
|
var dy;
|
|||
|
|
if (state.imageMode === 'blade') {
|
|||
|
|
// Convert the screen drag to the selected blade's own radial/across axes.
|
|||
|
|
// Source X runs from root to tip; source Y runs from upper to lower edge.
|
|||
|
|
var bounds = modelBounds();
|
|||
|
|
var modelDX = pointerDY / rect.height * (bounds.xMax - bounds.xMin);
|
|||
|
|
var modelDY = pointerDX / rect.width * (bounds.yMax - bounds.yMin);
|
|||
|
|
var local = rotatePoint(modelDX, modelDY, -FAN.angles[state.selectedBlade]);
|
|||
|
|
var maxHalf = Math.max(...FAN.profile.map( (point) => point[1]));
|
|||
|
|
dx = -local[0] / (FAN.rMax - FAN.rMin) * 100;
|
|||
|
|
dy = -local[1] / (2 * maxHalf) * 100;
|
|||
|
|
} else {
|
|||
|
|
dx = pointerDX / rect.width * 100;
|
|||
|
|
dy = pointerDY / rect.height * 100;
|
|||
|
|
}
|
|||
|
|
setImageControl(ui.offsetX, Math.round(state.imageStartX + dx));
|
|||
|
|
setImageControl(ui.offsetY, Math.round(state.imageStartY + dy));
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
var stopImageDrag = () => {
|
|||
|
|
var config = activeConfig();
|
|||
|
|
if (state.editorObjectDragging) {
|
|||
|
|
if (!state.editorObjectMoved)
|
|||
|
|
config.editHistory.pop();
|
|||
|
|
state.editorObjectDragging = false;
|
|||
|
|
state.editorPointerId = null;
|
|||
|
|
state.editorObjectDragStart = null;
|
|||
|
|
state.editorObjectOriginal = null;
|
|||
|
|
if (state.editorObjectMoved)
|
|||
|
|
scheduleRebuild();
|
|||
|
|
state.editorObjectMoved = false;
|
|||
|
|
refreshCommittedEditor(config, true);
|
|||
|
|
}
|
|||
|
|
if (state.editorDrawing && VECTOR_SHAPE_TOOLS.includes(state.editorTool)) {
|
|||
|
|
var preview = state.editorPreview?.object;
|
|||
|
|
var start = state.editorShapeStart;
|
|||
|
|
var end = state.editorLastPoint || start;
|
|||
|
|
var valid = preview && (state.editorTool === 'spline' ? (preview.points || []).length > 2 : Math.hypot(end[0] - start[0], end[1] - start[1]) > 3);
|
|||
|
|
state.editorDrawing = false;
|
|||
|
|
state.editorPointerId = null;
|
|||
|
|
state.editorShapeStart = null;
|
|||
|
|
state.editorShapePoints = null;
|
|||
|
|
state.editorLastPoint = null;
|
|||
|
|
if (valid)
|
|||
|
|
commitVectorObject(config, JSON.parse(JSON.stringify(preview)));
|
|||
|
|
else {
|
|||
|
|
config.editHistory.pop();
|
|||
|
|
clearEditorPreview(true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
state.imageDragging = false;
|
|||
|
|
ui.maskWrap.classList.remove('is-adjusting');
|
|||
|
|
finishEditorStroke();
|
|||
|
|
}
|
|||
|
|
;
|
|||
|
|
ui.maskCanvas.addEventListener('pointerup', stopImageDrag);
|
|||
|
|
ui.maskCanvas.addEventListener('pointercancel', stopImageDrag);
|
|||
|
|
ui.maskCanvas.addEventListener('pointerleave', () => {
|
|||
|
|
if (state.editorTool === 'text')
|
|||
|
|
return;
|
|||
|
|
if (!state.editorDrawing && !state.editorObjectDragging && !state.imageDragging)
|
|||
|
|
hideEditorPointerPreviews();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.maskCanvas.addEventListener('wheel', (event) => {
|
|||
|
|
if (state.editorTool !== 'move')
|
|||
|
|
return;
|
|||
|
|
event.preventDefault();
|
|||
|
|
var factor = Math.exp(-event.deltaY * .0015);
|
|||
|
|
setImageControl(ui.imageScale, Math.round(Number(ui.imageScale.value) * factor));
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
, {
|
|||
|
|
passive: false
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
bindRange(ui.imageScale, '%', true);
|
|||
|
|
bindRange(ui.offsetX, '%', true);
|
|||
|
|
bindRange(ui.offsetY, '%', true);
|
|||
|
|
bindRange(ui.imageRotation, '°', true);
|
|||
|
|
bindRange(ui.blur, ' px', true);
|
|||
|
|
bindRange(ui.contrast, '%', false);
|
|||
|
|
bindRange(ui.minThickness, '', false);
|
|||
|
|
bindRange(ui.maxThickness, '', false);
|
|||
|
|
bindRange(ui.reliefGamma, '', false);
|
|||
|
|
ui.reliefMargin.addEventListener('input', () => {
|
|||
|
|
updateOutput(ui.reliefMargin, ' mm');
|
|||
|
|
updatePrecisionGuide();
|
|||
|
|
scheduleRebuild({
|
|||
|
|
redrawImage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.invert.addEventListener('change', () => scheduleRebuild());
|
|||
|
|
ui.detail.addEventListener('change', () => {
|
|||
|
|
updatePrecisionGuide();
|
|||
|
|
scheduleRebuild();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.threeMFStructure.addEventListener('change', update3MFStructureHint);
|
|||
|
|
ui.includeFrame.addEventListener('change', () => {
|
|||
|
|
update3MFStructureHint();
|
|||
|
|
updateMeshBuffers();
|
|||
|
|
scheduleRebuild();
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.resetAll.addEventListener('click', resetAll);
|
|||
|
|
ui.exportBtn.addEventListener('click', writeSTL);
|
|||
|
|
ui.export3mfBtn.addEventListener('click', write3MF);
|
|||
|
|
|
|||
|
|
ui.glCanvas.addEventListener('pointerdown', (event) => {
|
|||
|
|
state.dragging = true;
|
|||
|
|
state.lastX = event.clientX;
|
|||
|
|
state.lastY = event.clientY;
|
|||
|
|
ui.glCanvas.setPointerCapture(event.pointerId);
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.glCanvas.addEventListener('pointermove', (event) => {
|
|||
|
|
if (!state.dragging)
|
|||
|
|
return;
|
|||
|
|
state.camera.yaw += (event.clientX - state.lastX) * .007;
|
|||
|
|
state.camera.pitch = clamp(state.camera.pitch + (event.clientY - state.lastY) * .007, -1.45, 1.45);
|
|||
|
|
state.lastX = event.clientX;
|
|||
|
|
state.lastY = event.clientY;
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.glCanvas.addEventListener('pointerup', () => {
|
|||
|
|
state.dragging = false;
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.glCanvas.addEventListener('wheel', (event) => {
|
|||
|
|
event.preventDefault();
|
|||
|
|
state.camera.zoom = clamp(state.camera.zoom * Math.exp(event.deltaY * .001), 260, 880);
|
|||
|
|
}
|
|||
|
|
, {
|
|||
|
|
passive: false
|
|||
|
|
});
|
|||
|
|
ui.topView.addEventListener('click', () => {
|
|||
|
|
state.camera.pitch = 0;
|
|||
|
|
state.camera.yaw = -Math.PI / 2;
|
|||
|
|
state.camera.zoom = 690;
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.isoView.addEventListener('click', () => {
|
|||
|
|
state.camera.pitch = .60;
|
|||
|
|
state.camera.yaw = -Math.PI / 2 - .08;
|
|||
|
|
state.camera.zoom = 620;
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
ui.fitView.addEventListener('click', () => {
|
|||
|
|
state.camera.zoom = 620;
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
}
|