Files

261 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
NUMEX KAŞE STÜDYO — GERÇEK ENGRAVE MOTORU (raster fill)
------------------------------------------------------------
NEDEN VAR (2026-09-12, kök): eski G-code her harfi BOŞ DİKDÖRTGEN
olarak çiziyordu (server.js:gcodeUret 4-köşe kutu) → makinede
"İŞYERİ ADI" değil yan yana kutular yanıyordu. Ekran gerçek fontu
gösteriyor ama çıktı sahteydi.
ÇÖZÜM: Tasarımı (gerçek fontla) offscreen bitmap'e döküp SATIR SATIR
tarıyoruz (raster). Siyah piksellerin üstünde lazeri M4 ile yakıyoruz,
boşlukta G0 ile atlıyoruz. Böylece EKRANDA NE VARSA O YANAR (WYSIWYG) —
gerçek harfler. LightBurn'ün "Fill/Scan" moduyla aynı mantık.
Modlar:
• 'kase' : AYNA + NEGATİF — zemini kazır, harfler KABARIK kalır,
mürekkeple basınca düz okunur (gerçek kaşe/damga).
• 'kazima' : POZİTİF — harfleri doğrudan kazır (yüzey markalama, ayna yok).
• 'kesme' : VEKTÖR — dikdörtgen/çerçeve kenarlarını keser (delip çıkarır).
Makine: Creality Falcon2 Pro 40W · GRBL · $32=1 lazer modu · M4 dinamik güç.
============================================================ */
'use strict';
(function (glob) {
const SURUM = '2.0.0';
/* mm cinsinden tasarım sınır kutusu (tüm nesneleri kapsar). */
function sinirKutu(nesneler, kutuFn) {
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
for (const o of nesneler) {
const k = kutuFn(o);
x0 = Math.min(x0, k.x); y0 = Math.min(y0, k.y);
x1 = Math.max(x1, k.x + k.g); y1 = Math.max(y1, k.y + k.h);
}
if (!isFinite(x0)) return null;
return { x: x0, y: y0, w: Math.max(1, x1 - x0), h: Math.max(1, y1 - y0) };
}
/* Tasarımı offscreen canvas'a çizer (SİYAH tasarım / BEYAZ zemin).
alan: {x,y,w,h} mm — çizim penceresi (genelde sınır + pay, ya da çerçeve).
res : px/mm (çözünürlük; aralık'tan türetilir).
ayna: true ise TÜM plaka yatay çevrilir (kaşe basınca düz okunsun). */
function bitmapCiz(nesneler, alan, res, ayna, kutuFn) {
const W = Math.max(1, Math.round(alan.w * res));
const H = Math.max(1, Math.round(alan.h * res));
const cv = document.createElement('canvas');
cv.width = W; cv.height = H;
const c = cv.getContext('2d', { willReadFrequently: true });
c.fillStyle = '#fff'; c.fillRect(0, 0, W, H);
c.fillStyle = '#000'; c.strokeStyle = '#000';
const X = (m) => (m - alan.x) * res;
const Y = (m) => (m - alan.y) * res;
const SC = (m) => m * res;
for (const o of nesneler) {
c.save();
// Nesne kendi merkezinde döndürülmüşse (aci) uygula
if (o.aci) {
const k = kutuFn(o);
c.translate(X(k.x + k.g / 2), Y(k.y + k.h / 2));
c.rotate(o.aci * Math.PI / 180);
c.translate(-X(k.x + k.g / 2), -Y(k.y + k.h / 2));
}
if (o.tip === 'metin') {
// "Boy" = GERÇEK harf yüksekliği olsun (9mm gir → 9mm yan). Font em'i,
// metnin ÖLÇÜLEN mürekkep yüksekliği = boy olacak şekilde ölçeklenir.
// (Aksi halde büyük harf cap-height ~0.7×em → 9mm ayar 6.3mm yanardı.)
const font = (o.font || 'sans-serif');
const kalin = o.kalin ? 'bold ' : '';
const hedefPx = o.boy * res;
let em = hedefPx;
c.font = kalin + em + 'px ' + font;
let m = c.measureText(o.icerik || '');
let inkH = (m.actualBoundingBoxAscent || 0) + (m.actualBoundingBoxDescent || 0);
if (inkH > 0.5) {
em = em * (hedefPx / inkH);
c.font = kalin + em + 'px ' + font;
m = c.measureText(o.icerik || '');
inkH = (m.actualBoundingBoxAscent || 0) + (m.actualBoundingBoxDescent || 0);
}
// Mürekkebin ÜST kenarı o.y'ye otursun (alphabetic baseline + ascent kaydırma)
const asc = m.actualBoundingBoxAscent || em * 0.8;
c.textBaseline = 'alphabetic';
c.fillText(o.icerik || '', X(o.x), Y(o.y) + asc);
} else if (o.tip === 'cizgi') {
c.lineWidth = Math.max(1, SC((o.boy || 1) * 0.25));
c.beginPath(); c.moveTo(X(o.x), Y(o.y)); c.lineTo(X(o.x + o.g), Y(o.y + o.h)); c.stroke();
} else if (o.tip === 'dikdortgen') {
c.lineWidth = Math.max(1, SC((o.boy || 1) * 0.18));
c.strokeRect(X(o.x), Y(o.y), SC(o.g), SC(o.h));
} else if (o.tip === 'logo') {
const cx = X(o.x + o.g / 2), cy = Y(o.y + o.h / 2);
const r = SC(Math.min(o.g, o.h) / 2) * 0.8;
c.lineWidth = Math.max(1, SC((o.boy || 1) * 0.16));
c.beginPath(); c.arc(cx, cy, r, 0, Math.PI * 2); c.stroke();
c.beginPath();
c.moveTo(cx, cy - r); c.lineTo(cx, cy + r);
c.moveTo(cx - r, cy); c.lineTo(cx + r, cy);
c.stroke();
}
c.restore();
}
let out = cv;
if (ayna) {
const cv2 = document.createElement('canvas');
cv2.width = W; cv2.height = H;
const c2 = cv2.getContext('2d', { willReadFrequently: true });
c2.translate(W, 0); c2.scale(-1, 1);
c2.drawImage(cv, 0, 0);
out = cv2;
}
return { canvas: out, W, H };
}
/* Bir bitmap satırındaki (py) yakılacak "koşu"ları (run) bulur.
negatif=true → BEYAZ (zemin) yakılır (kabartma); false → SİYAH (harf) yakılır. */
function satirKosulari(data, W, py, negatif) {
const runs = [];
let bas = -1;
for (let px = 0; px < W; px++) {
const i = (py * W + px) * 4;
const lum = data[i] * 0.3 + data[i + 1] * 0.59 + data[i + 2] * 0.11;
const yak = negatif ? (lum > 127) : (lum < 128);
if (yak && bas < 0) bas = px;
else if (!yak && bas >= 0) { runs.push([bas, px - 1]); bas = -1; }
}
if (bas >= 0) runs.push([bas, W - 1]);
return runs;
}
function yuvarla(n) { return Math.round(n * 100) / 100; }
/* ============ ANA ÜRETİCİ ============ */
function uret(nesneler, ayar) {
ayar = ayar || {};
const kutuFn = ayar.kutuFn || defaultKutu;
const mod = ayar.mod || 'kase';
const metinList = (nesneler || []).filter((o) => o && (o.icerik || o.tip !== 'metin'));
if (!nesneler || !nesneler.length) return { hata: 'Tablada öğe yok.' };
const guc = clamp(ayar.guc, 1, 100, 78);
const hiz = clamp(ayar.hiz, 1, 25000, 900); // Falcon2 Pro 40W max ~25000 mm/dk
const pas = clamp(ayar.pas, 1, 10, 1);
const aralik = clamp(ayar.aralik, 0.03, 0.5, 0.10); // satır aralığı mm (spot ~0.08)
const res = Math.max(1, Math.round(1 / aralik)); // px/mm — aralık ile eşleş
const kesmeGuc = clamp(ayar.kesmeGuc, 1, 100, 100);
const kesmeHiz = clamp(ayar.kesmeHiz, 1, 25000, 600);
const kesmePas = clamp(ayar.kesmePas, 1, 20, 3);
// Kaşe plakası sınırı: çerçeve (dikdortgen) varsa onu, yoksa tasarım sınırı + pay.
const cerceve = nesneler.find((o) => o.tip === 'dikdortgen');
let alan;
if (cerceve) {
alan = { x: cerceve.x, y: cerceve.y, w: cerceve.g, h: cerceve.h };
} else {
const s = sinirKutu(nesneler, kutuFn);
if (!s) return { hata: 'Çizilecek geometri yok.' };
const pay = 2; // mm kenar payı
alan = { x: s.x - pay, y: s.y - pay, w: s.w + 2 * pay, h: s.h + 2 * pay };
}
const L = [];
L.push('; NUMEX KAŞE STÜDYO — gerçek engrave (raster)');
L.push('; Motor: engrave.js v' + SURUM + ' | Makine: Creality Falcon2 Pro 40W');
L.push('; Tarih: ' + new Date().toISOString());
L.push('; Mod: ' + mod + (mod === 'kase' ? ' (ayna + negatif — harfler kabarık)' : ''));
L.push('; Islem alani: ' + yuvarla(alan.w) + ' x ' + yuvarla(alan.h) + ' mm @ ' + yuvarla(alan.x) + ',' + yuvarla(alan.y));
L.push('; Cozunurluk: ' + res + ' px/mm (satir araligi ' + aralik + ' mm)');
/* Makinenin KENDİ çalıştırdığı format (flash bellekteki LightBurn/Creality
40W örnekleriyle birebir): G54 = İŞ koordinatı → HOME gerekmez, origin
kullanıcının koyduğu yerdir. M4 bir kez → dinamik güç; hareketlerde G0=lazer
kapalı, G1 S..=yakma. $32/$X/mutlak-0,0 YOK (ALARM'ın kökü buydu). */
L.push('G00 G17 G40 G21 G54 ; kurulum (LightBurn/Creality 40W uyumlu)');
L.push('G90 ; mutlak konum');
L.push('M4 ; dinamik lazer modu (guc hiza gore ayarlanir)');
if (mod === 'kesme') {
vektorKes(L, nesneler, kutuFn, kesmeGuc, kesmeHiz, kesmePas, !!ayar.ayna, alan);
} else {
// Raster: kase => ayna + negatif; kazima => pozitif, ayna yok
const ayna = mod === 'kase' ? true : !!ayar.ayna;
const negatif = mod === 'kase';
const bmp = bitmapCiz(nesneler, alan, res, ayna, kutuFn);
const data = bmp.canvas.getContext('2d', { willReadFrequently: true }).getImageData(0, 0, bmp.W, bmp.H).data;
const S = Math.max(1, Math.min(1000, Math.round(guc * 10)));
const F = Math.round(hiz);
const stepPx = Math.max(1, Math.round(aralik * res)); // = 1 (res=1/aralik)
let toplamKosu = 0;
for (let p = 0; p < pas; p++) {
L.push('; --- gecis ' + (p + 1) + '/' + pas + ' ---');
let dir = 1;
for (let py = 0; py < bmp.H; py += stepPx) {
const ymm = alan.y + py / res;
let runs = satirKosulari(data, bmp.W, py, negatif);
if (!runs.length) { dir = -dir; continue; }
if (dir < 0) runs = runs.slice().reverse();
for (const r of runs) {
// bidirectional: sağ→sol turda run'ı ters yak
const xaMm = alan.x + (dir > 0 ? r[0] : r[1] + 1) / res;
const xbMm = alan.x + (dir > 0 ? r[1] + 1 : r[0]) / res;
L.push('G0 X' + yuvarla(xaMm) + ' Y' + yuvarla(ymm)); // hizli git (M4'te lazer kapali)
L.push('G1 X' + yuvarla(xbMm) + ' Y' + yuvarla(ymm) + ' S' + S + ' F' + F); // yak (S=guc)
L.push('G1 S0'); // koşu sonu: güç sıfır (travel kapalı)
toplamKosu++;
}
dir = -dir;
}
}
L.push('; toplam yakma kosusu: ' + toplamKosu);
}
L.push('G1 S0 ; guc sifir');
L.push('M5 ; lazer kapali');
L.push('G90');
L.push('G0 X0 Y0 ; baslangica don');
L.push('M2 ; program sonu');
return { gcode: L.join('\n') + '\n', alan: alan, mod: mod };
}
/* Vektör kesim: dikdörtgen çerçeveleri kenar boyunca keser (çok pas). */
function vektorKes(L, nesneler, kutuFn, guc, hiz, pas, ayna, alan) {
const S = Math.max(1, Math.min(1000, Math.round(guc * 10)));
const F = Math.round(hiz);
const mx = (x) => ayna ? yuvarla(2 * alan.x + alan.w - x) : yuvarla(x); // ayna: X'i plaka etrafında çevir
const dikdortgenler = nesneler.filter((o) => o.tip === 'dikdortgen' || o.tip === 'cizgi');
if (!dikdortgenler.length) { L.push('; KESME: kesilecek çerçeve/çizgi yok.'); return; }
for (const o of dikdortgenler) {
L.push('; kesim: ' + o.tip);
const pts = o.tip === 'cizgi'
? [[o.x, o.y], [o.x + o.g, o.y + o.h]]
: [[o.x, o.y], [o.x + o.g, o.y], [o.x + o.g, o.y + o.h], [o.x, o.y + o.h], [o.x, o.y]];
for (let p = 0; p < pas; p++) {
L.push('; kesim gecis ' + (p + 1) + '/' + pas);
L.push('G0 X' + mx(pts[0][0]) + ' Y' + yuvarla(pts[0][1]));
for (let i = 1; i < pts.length; i++) L.push('G1 X' + mx(pts[i][0]) + ' Y' + yuvarla(pts[i][1]) + ' S' + S + ' F' + F);
L.push('G1 S0');
}
}
}
function clamp(v, lo, hi, def) {
v = Number(v);
if (!isFinite(v)) return def;
return Math.max(lo, Math.min(hi, v));
}
/* Fallback kutu (index.html nesneKutu verilmezse) — kaba tahmin. */
function defaultKutu(o) {
if (o.tip === 'metin') {
const n = String(o.icerik || '').length || 1;
return { x: o.x, y: o.y, g: n * o.boy * 0.6, h: o.boy };
}
return { x: o.x, y: o.y, g: o.g || o.boy || 1, h: o.h || o.boy || 1 };
}
glob.Engrave = { uret, bitmapCiz, sinirKutu, SURUM };
})(typeof window !== 'undefined' ? window : globalThis);