Damalı zemin ızgarası, CAD görüntüleyicilerinde (FreeCAD, Fusion 360, Blender’ın görünüm penceresi) 3D modellerin altında gördüğünüz zemin referans düzlemidir. İki amaca hizmet eder: izleyiciye ölçek duygusu verir ve modelin dünya uzayında nerede durduğunu söyleyebilmek için uzamsal bir referans sağlar. Izgara tipik olarak hızlı görsel hücre sayımı için dama deseni, düzenli aralıklarla çizgi işaretleri, sınır kenarlarında çentik işaretleri ve koordinatları okuyabilmeniz için sayısal ölçek etiketleri içerir.
Bu yazı, ölçek işaretleri, sayısal etiketler ve yörünge kamerası içeren
damalı bir zemin ızgarası render eden minimal, bağımlılıksız bir WebGPU
uygulaması gösterir. Tam kaynak en altta; aşağıdaki canlı demo
/scripts/floor-grid-webgpu.js’ten
birebir aynı script’i yükler.
Yörünge için sürükle, yakınlaştırma için kaydır. Izgara 200×300 birim, 10 birimlik hücreler (20×30 bölme). WebGPU uyumlu bir tarayıcı gerektirir (Chrome / Edge 113+).
Nasıl çalışır
Izgara, tek bir render pass içinde üç pass’te render edilir:
- Damalı dörtgen — z = 0’daki ızgara alanını kaplayan tek bir düz dörtgen (iki üçgen). Dama deseni texture’dan değil, dünya uzayı konumundan fragment shader’da prosedürel olarak hesaplanır. Bu, texture tahsisi olmadığı, UV eşleme olmadığı ve desenin çözünürlükten bağımsız olduğu anlamına gelir (yakınlaştırınca pikselleşme yok).
- Izgara çizgileri — vertex başına renkli bir line-list. Normal ızgara çizgileri açık gri, iki eksen kenarı daha koyu, sınırdaki çentik işaretleri orta tonda.
- Ölçek etiketleri — başlatma sırasında 2D canvas’tan üretilen glif atlası kullanılarak, ızgarayla aynı düzlemde (z = 0) 3D dokulu dörtgenler olarak render edilen sayısal çentik etiketleri. Etiketler gerçek 3D metindir: ekran alanı kaplaması değil, kamerayla birlikte ölçeklenir ve döner. X ekseni etiketleri Y yönü boyunca uzanması için -90° döndürülür; Y ekseni etiketleri ızgaranın solunda sağa hizalanır. Etiket inceltme, dağınıklığı önlemek için her 2. çentiği gösterir.
Tüm pass’ler, yarı saydam öğelerin canvas arka planıyla doğru birleşmesi için önceden çarpılmış alfa harmanlama kullanır.
Ana teknikler:
- Fragment shader’da prosedürel dama. Vertex shader, dünya uzayı
XY konumunu varying olarak fragment shader’a geçirir. Fragment
shader,
floor(worldPos / cellSize)ile fragment’in hangi hücreye ait olduğunu hesaplar ve(cellX + cellY) % 2‘ye göre iki gri tonu arasında geçiş yapar. Hücre boyutu (GRID_SIZE / GRID_DIVISIONS), shader oluşturma zamanında JavaScript template literal interpolasyonu ile WGSL kaynağına gömülür — ekstra uniform gerekmez. - Örülmüş çizgi geometrisi. Izgara çizgileri, eksen kenarları ve
çentik işaretlerinin tümü, örülmüş
[x, y, z, r, g, b]düzenli (vertex başına 24 bayt) tek bir line-list vertex buffer’ına paketlenir. Vertex başına renk, ayrı draw çağrıları olmadan her çizgi tipinin kendi tonuna sahip olmasını sağlar. - Ölçek etiketleri için glif atlası.
0123456789.-karakterleri için bitmap atlası, başlatma sırasında 2D canvas’ta üretilir ve WebGPU texture’ı olarak yüklenir. Her glif ayrı ölçüldüğünden atlas değişken genişlikli hücreler kullanır (sabit genişlikli görünüm yok). Etiket fragment shader’ı atlası örnekler ve kırmızı kanalı alfa olarak kullanır (saydam arka planda beyaz metin), uniform renkle renklendirilir. - Yörünge kamerası. Kamera konumu küresel koordinatlardan (yaw,
pitch, mesafe) hesaplanır ve
lookAtmatrisine verilir. Sürükleme yaw/pitch’i; fare tekerleği yörünge yarıçapını kontrol eder. Pitch, kamera hiç yerin altına gitmesin veya doğrudan aşağı bakmasın diye 0’ın biraz üstüne ve 90°‘nin biraz altına kenetlenir. - Derinlik yazımı devre dışı. Tüm pipeline’lar
depthWriteEnabled: falsekullanır; böylece ızgara kendinden sonra çizilen geometriyi kapatmaz. Tam bir 3D görüntüleyicide ızgara önce çizilir, sonra üstüne model çizilir. - Önceden çarpılmış alfa harmanlama. Canvas
alphaMode: 'premultiplied'kullandığından tüm fragment shader’lar(color * alpha, alpha)çıktılar ve blend durumusrcFactor: 'one'('src-alpha'değil) kullanır. Bu, önceden çarpılmış alfa için doğru blend kurulumudur — burada düz alfa harmanlama kullanmak alfa kanalını çift koyulaştırarak yarı saydam nesneleri neredeyse görünmez yapardı.
Kendi sayfanızda kullanın
/scripts/floor-grid-webgpu.js‘i statik
klasörünüze koyun ve nav-grid-canvas sınıflı bir canvas ekleyin —
script sayfadaki eşleşen her canvas’ı otomatik başlatır:
<canvas class="nav-grid-canvas"
style="width:960px;height:720px;"></canvas>
<script src="/scripts/floor-grid-webgpu.js" defer></script>Canvas’ı CSS (width/height) ile boyutlandırın; script
devicePixelRatio ölçeklemesini ve derinlik texture’ı yeniden
boyutlandırmasını dahili olarak halleder. Izgara boyutunu veya bölme
sayısını değiştirmek için script’in başındaki GRID_SIZE_X,
GRID_SIZE_Y, GRID_DIVISIONS_X ve GRID_DIVISIONS_Y sabitlerini
düzenleyin — dama hücre boyutu eksen başına otomatik türetilir. Kare
dama hücreleri için iki hücre boyutu eşit olmalıdır (GRID_SIZE_X / GRID_DIVISIONS_X === GRID_SIZE_Y / GRID_DIVISIONS_Y).
Tam kaynak
// SPDX-FileCopyrightText: 2026 Uli Köhler <gitlab@techoverflow.net>
// SPDX-License-Identifier: CC0-1.0
//
// Minimal WebGPU damalı zemin ızgarası (ölçek etiketleriyle).
// XY düzleminde zemin referans ızgarası render eder:
// - Prosedürel dama deseni (fragment shader'da hesaplanır)
// - Düzenli aralıklı ızgara çizgileri
// - Orijinden geçen iki kenar boyunca koyu eksen çizgileri
// - Ölçek okuma için sınır kenarlarında çentik işaretleri
// - 3D dokulu dörtgenler olarak render edilen sayısal ölçek
// etiketleri (glif atlası)
// Kamera orijini turlar; döndürmek için sürükle, yakınlaştırma için
// fare tekerleği.
// Sayfadaki her <canvas class="nav-grid-canvas">'ı otomatik başlatır.
//
// Neden IIFE? Yardımcıların, sabitlerin veya WGSL kaynağının global
// kapsama sızmasını önlemek için her şeyi anında çağrılan fonksiyon
// ifadesiyle sarıyoruz. Script'in <script src=... defer> ile olduğu
// gibi, muhtemelen aynı sayfadaki diğer script'lerle birlikte dahil
// edilmesi amaçlanır, bu yüzden izolasyon önemli.
(function () {
'use strict';
// ─────────────────────────────────────────────────────────────────────
// Izgara yapılandırması
// ─────────────────────────────────────────────────────────────────────
// Izgara XY düzleminde (z = 0) yatar ve orijin merkezlidir.
// X'te [-GRID_SIZE_X/2, +GRID_SIZE_X/2] ve Y'de
// [-GRID_SIZE_Y/2, +GRID_SIZE_Y/2] uzanır.
//
// GRID_DIVISIONS_X / GRID_DIVISIONS_Y eksen başına hücre sayısını
// kontrol eder. Her hücre X'te GRID_SIZE_X / GRID_DIVISIONS_X birim,
// Y'de GRID_SIZE_Y / GRID_DIVISIONS_Y birim genişliğindedir. Kare
// hücreli dama için iki hücre boyutu eşit olmalıdır — burada
// 200/20 = 300/30 = 10.
//
// Gerçek bir CAD görüntüleyicisinde bunları tipik olarak yapılandırılabilir
// yapar ve ızgarayı yüklenen geometrinin sınırlayıcı kutusuna yeniden
// merkezlersiniz (WebGCodeViewer'ın GridRenderer.setCenter'ına bakın).
// Burada demo canvas'ta iyi görünen sabit değerlerle basit tutuyoruz.
const GRID_SIZE_X = 200; // dünya biriminde toplam X uzanımı (örn. mm)
const GRID_SIZE_Y = 300; // dünya biriminde toplam Y uzanımı (örn. mm)
const GRID_DIVISIONS_X = 20; // X boyunca hücreler → hücre boyutu = 10.0 birim
const GRID_DIVISIONS_Y = 30; // Y boyunca hücreler → hücre boyutu = 10.0 birim
const TICK_LENGTH = 0.3; // hücre boyutunun kesri olarak çentik uzunluğu
// Izgara öğelerinin renkleri. Hepsi koyu griler, böylece ızgara ön
// plan nesnesi değil, ince bir referans katmanı olarak okunur.
const GRID_COLOR = [0.25, 0.25, 0.27]; // normal ızgara çizgileri
const AXIS_COLOR = [0.15, 0.15, 0.17]; // daha koyu — iki kenar ekseni
const TICK_COLOR = [0.20, 0.20, 0.22]; // sınırdaki çentik işaretleri
const LABEL_COLOR = [0.25, 0.25, 0.27]; // sayısal etiketler (ızgarayla aynı)
// ─────────────────────────────────────────────────────────────────────
// Etiket yapılandırması
// ─────────────────────────────────────────────────────────────────────
// Etiketler, başlatma sırasında 2D canvas'tan üretilen glif atlası
// kullanılarak ızgarayla aynı düzlemde (z=0) dokulu dörtgenler olarak
// render edilir. Bu gerçek 3D metindir — ekran alanı kaplaması değil,
// kamerayla birlikte ölçeklenir ve döner.
const GLYPHS = '0123456789.-'; // atlasın desteklediği karakterler
const GLYPH_PX_H = 32; // her glif hücresinin piksel yüksekliği
const GLYPH_PAD = 2; // her glif etrafında px dolgu (taşma önleme)
const CHAR_HEIGHT = 7.5; // dünya uzayı karakter yüksekliği (mm)
const LABEL_MARGIN = 2; // ızgara kenarından etikete boşluk (mm)
const TICK_STRIDE = 2; // dağınıklığı önlemek için her N. çentiği göster
// ─────────────────────────────────────────────────────────────────────
// Minimal mat4 yardımcıları (sütun-öncelikli, WebGPU NDC z [0,1]'de)
// ─────────────────────────────────────────────────────────────────────
// WebGPU / WGSL matrisleri sütun-öncelikli, yani eleman [col*4+row]
// olarak saklar. Float32Array'i transpoz adımı olmadan doğrudan uniform
// buffer'a yükleyebilmek için burada aynı düzeni yansıtıyoruz.
// Yalnızca ihtiyacımız olan birkaç matrisi uyguluyoruz: birim, çarpma,
// perspektif ve lookAt. gl-matrix veya benzerine bağımlılık yok —
// hedef tek bir kendi kendine yeten dosya.
function mat4Identity() {
const m = new Float32Array(16);
m[0] = m[5] = m[10] = m[15] = 1;
return m;
}
// (a * b) * v'nin önce b'yi, sonra a'yı uyguladığı anlamında a * b
// döndürür. Bu dosyada her yerde kullanılan kural budur: view-projection
// proj * view olarak kurulur, yani nokta view→clip olarak dönüştürülür.
function mat4Multiply(a, b) {
const r = new Float32Array(16);
for (let col = 0; col < 4; col++) {
for (let row = 0; row < 4; row++) {
let s = 0;
for (let k = 0; k < 4; k++) s += a[k * 4 + row] * b[col * 4 + k];
r[col * 4 + row] = s;
}
}
return r;
}
// Sağ elli perspektif projeksiyon matrisi.
// Kamera -Z'ye bakar, bu yüzden görünür noktalarda z < 0.
// Bu WebGPU kuralıdır: clip space z [0, 1] içindedir (OpenGL'deki
// gibi [-1, 1] değil), bu yüzden m[10] = far/(near-far) ve m[14]'te
// ek near*far terimi var. Bunu yanlış yapmak, WebGL kodunu WebGPU'ya
// taşırken "geometrim kayboldu" hatalarının bir numaralı kaynağı.
function mat4Perspective(fovy, aspect, near, far) {
const f = 1 / Math.tan(fovy / 2);
const m = new Float32Array(16);
m[0] = f / aspect;
m[5] = f;
m[10] = far / (near - far);
m[11] = -1;
m[14] = (far * near) / (near - far);
return m;
}
// Sağ elli lookAt matrisi: kamerayı 'eye'a yerleştirip 'target'a
// baktırır, 'up' ekran-yukarı yönüdür. view matrisi (world→view)
// döndürür. eye, target, up düz {x,y,z} nesneleridir.
//
// Matematik:
// forward = normalize(eye - target) (target'tan eye'a işaret eder)
// right = normalize(cross(up, forward))
// trueUp = cross(forward, right) (yeniden dikleştirilmiş up)
// Matris satırları [right | trueUp | forward], öteleme sütunu ise her
// eksen için -dot(axis, eye) — dünyayı, kamera orijine oturacak şekilde
// hareket ettirir.
function mat4LookAt(eye, target, up) {
const zx = eye.x - target.x, zy = eye.y - target.y, zz = eye.z - target.z;
let zl = Math.hypot(zx, zy, zz);
const fx = zx / zl, fy = zy / zl, fz = zz / zl;
// right = normalize(cross(up, forward))
let rx = up.y * fz - up.z * fy;
let ry = up.z * fx - up.x * fz;
let rz = up.x * fy - up.y * fx;
let rl = Math.hypot(rx, ry, rz);
// Dejenere durum: up, forward'a paralel. NaN üretmemek için
// (1,0,0)'a geri dön.
if (rl < 1e-6) { rx = 1; ry = 0; rz = 0; rl = 1; }
rx /= rl; ry /= rl; rz /= rl;
// trueUp = cross(forward, right) — zaten birim uzunlukta.
const ux = fy * rz - fz * ry;
const uy = fz * rx - fx * rz;
const uz = fx * ry - fy * rx;
const m = mat4Identity();
m[0] = rx; m[1] = ux; m[2] = fx;
m[4] = ry; m[5] = uy; m[6] = fy;
m[8] = rz; m[9] = uz; m[10] = fz;
m[12] = -(rx * eye.x + ry * eye.y + rz * eye.z);
m[13] = -(ux * eye.x + uy * eye.y + uz * eye.z);
m[14] = -(fx * eye.x + fy * eye.y + fz * eye.z);
return m;
}
// ─────────────────────────────────────────────────────────────────────
// Geometri: ızgara çizgileri + damalı dörtgen
// ─────────────────────────────────────────────────────────────────────
// Izgara üç pass'te çizilir:
// 1. Dama — ızgara alanını kaplayan tek dörtgen (2 üçgen). Dama
// deseni fragment shader'da dünya uzayı konumundan prosedürel
// hesaplanır; texture veya hücre başına geometri gerekmez.
// 2. Izgara çizgileri — vertex başına renkli line-list. Normal
// ızgara çizgileri gri, iki eksen kenarı daha koyu, sınırdaki
// çentik işaretleri orta tonda.
// 3. Etiketler — sayısal çentik etiketleri için dokulu dörtgenler;
// glif atlası kullanılarak render edilir (buildGlyphAtlas /
// buildLabelVertices'e bakın).
//
// Izgara çizgilerinin vertex düzeni örülmüştür:
// vertex başına [x, y, z, r, g, b] = 24 bayt (arrayStride: 24)
// Damalı dörtgende yalnız konum var:
// vertex başına [x, y, z] = 12 bayt (arrayStride: 12)
// Etiket dörtgenlerinde konum + UV var:
// vertex başına [x, y, z, u, v] = 20 bayt (arrayStride: 20)
// Izgara çizgisi geometrisini kur: normal çizgiler, eksen kenarları,
// çentik işaretleri. Örülmüş [x,y,z, r,g,b] vertex'lerin
// Float32Array'ini döndürür.
function buildGridLines() {
const sizeX = GRID_SIZE_X;
const sizeY = GRID_SIZE_Y;
const stepX = GRID_SIZE_X / GRID_DIVISIONS_X;
const stepY = GRID_SIZE_Y / GRID_DIVISIONS_Y;
const tickLenX = stepX * TICK_LENGTH;
const tickLenY = stepY * TICK_LENGTH;
// Izgara her eksende -size/2'den +size/2'ye uzanır (orijin merkezli).
const x0 = -sizeX / 2;
const y0 = -sizeY / 2;
const lines = [];
// Yardımcı: iki uç noktası tek renkli bir çizgi parçası ekle.
function addLine(x1, y1, z1, x2, y2, z2, color) {
lines.push(x1, y1, z1, color[0], color[1], color[2]);
lines.push(x2, y2, z2, color[0], color[1], color[2]);
}
// ── Normal ızgara çizgileri ──
// Dikey çizgiler (sabit X) stepX aralıklı, tüm Y uzanımını kaplar.
// Yatay çizgiler (sabit Y) stepY aralıklı, tüm X uzanımını kaplar.
for (let i = 0; i <= GRID_DIVISIONS_X; i++) {
const p = x0 + i * stepX;
addLine(p, y0, 0, p, y0 + sizeY, 0, GRID_COLOR); // dikey
}
for (let i = 0; i <= GRID_DIVISIONS_Y; i++) {
const q = y0 + i * stepY;
addLine(x0, q, 0, x0 + sizeX, q, 0, GRID_COLOR); // yatay
}
// ── Orijinden geçen iki kenar boyunca eksen çizgileri ──
// X=0 ve Y=0 çizgileri referans eksenleri olarak öne çıksın diye
// biraz daha koyu çizilir. Bu merkezli düzende sol alt köşe
// (-sizeX/2, -sizeY/2)'dedir, yani "eksenler" sol ve alt sınır
// kenarlarıdır.
addLine(x0, y0, 0, x0 + sizeX, y0, 0, AXIS_COLOR); // alt kenar (X)
addLine(x0, y0, 0, x0, y0 + sizeY, 0, AXIS_COLOR); // sol kenar (Y)
// ── Sınır kenarlarındaki çentik işaretleri ──
// Çentikler, alt ve sol kenarlarda içe doğru işaret eden kısa
// işaretlerdir; her ızgara çizgisi kesişiminde bir tane. Cetvel
// çentikleri gibi koordinatları bir bakışta okumayı kolaylaştırır.
//
// X ekseni çentikleri: alt kenar boyunca (Y = y0), içe doğru (+Y)
// uzanır.
for (let i = 0; i <= GRID_DIVISIONS_X; i++) {
const p = x0 + i * stepX;
addLine(p, y0, 0, p, y0 + tickLenY, 0, TICK_COLOR);
}
// Y ekseni çentikleri: sol kenar boyunca (X = x0), içe doğru (+X)
// uzanır.
for (let i = 0; i <= GRID_DIVISIONS_Y; i++) {
const p = y0 + i * stepY;
addLine(x0, p, 0, x0 + tickLenX, p, 0, TICK_COLOR);
}
return new Float32Array(lines);
}
// Damalı dörtgeni kur: tüm ızgara alanını kaplayan iki üçgen.
// Gerçek dama deseni fragment shader'da dünya uzayı koordinatlarından
// üretilir, bu yüzden geometri z = 0'da düz bir dörtgendir.
function buildCheckerQuad() {
const sx = GRID_SIZE_X;
const sy = GRID_SIZE_Y;
const x0 = -sx / 2;
const y0 = -sy / 2;
// Dörtgeni oluşturan iki üçgen:
// (x0,y0) → (x0+sx, y0) → (x0+sx, y0+sy) [ilk üçgen]
// (x0,y0) → (x0+sx, y0+sy) → (x0, y0+sy) [ikinci üçgen]
return new Float32Array([
x0, y0, 0,
x0 + sx, y0, 0,
x0 + sx, y0 + sy, 0,
x0, y0, 0,
x0 + sx, y0 + sy, 0,
x0, y0 + sy, 0,
]);
}
// ─────────────────────────────────────────────────────────────────────
// Glif atlası üretimi
// ─────────────────────────────────────────────────────────────────────
// '0123456789.-' karakterleri için başlatma sırasında 2D canvas'ta
// bitmap atlası üretir, sonra WebGPU texture'ı olarak yükleriz. Her
// glif ayrı ölçüldüğünden atlas değişken genişlikli hücreler kullanır
// (sabit genişlikli görünüm yok). Her glifin dünya uzayı genişliği
// piksel genişliğiyle orantılıdır ve fontun doğal en-boy oranını korur.
//
// Atlas tek bir yatay şerittir. Her glifin UV koordinatları ve dünya
// uzayı genişliği, sonraki dörtgen üretimi için glyphMap'te saklanır.
// Glif atlasını 2D canvas'ta kur ve canvas ile karakter →
// {u0, v0, u1, v1, worldW} haritasını döndür.
function buildGlyphAtlas() {
const fontPx = Math.floor(GLYPH_PX_H * 0.8);
// CAD görünümü için serif / matematik dostu font yığını.
const fontStack = '"Cambria Math", "STIX Two Math", "Latin Modern Math", "Times New Roman", Georgia, serif';
// İlk pass: hücre genişliklerini belirlemek için her glifi ölç.
const measureCanvas = document.createElement('canvas');
const mctx = measureCanvas.getContext('2d');
mctx.font = fontPx + 'px ' + fontStack;
mctx.textAlign = 'center';
mctx.textBaseline = 'middle';
const cellWidths = [];
let totalW = 0;
for (let i = 0; i < GLYPHS.length; i++) {
const w = Math.ceil(mctx.measureText(GLYPHS[i]).width) + 2 * GLYPH_PAD;
cellWidths.push(w);
totalW += w;
}
const atlasW = totalW;
const atlasH = GLYPH_PX_H;
// İkinci pass: atlası çiz (saydam arka planda beyaz metin).
const atlasCanvas = document.createElement('canvas');
atlasCanvas.width = atlasW;
atlasCanvas.height = atlasH;
const ctx = atlasCanvas.getContext('2d');
ctx.clearRect(0, 0, atlasW, atlasH);
ctx.fillStyle = 'white';
ctx.font = fontPx + 'px ' + fontStack;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const glyphMap = new Map();
let xOff = 0;
for (let i = 0; i < GLYPHS.length; i++) {
const cw = cellWidths[i];
ctx.fillText(GLYPHS[i], xOff + cw / 2, atlasH / 2);
// UV'leri + dünya uzayı genişliğini kaydet (piksel genişliğiyle orantılı).
glyphMap.set(GLYPHS[i], {
u0: xOff / atlasW,
v0: 0,
u1: (xOff + cw) / atlasW,
v1: 1,
worldW: CHAR_HEIGHT * cw / GLYPH_PX_H,
});
xOff += cw;
}
return { atlasCanvas, atlasW, atlasH, glyphMap };
}
// Çentik değerini etiket string'i olarak görüntülemek için biçimle.
// Gereksiz ondalık basamakları kırparken en az bir basamak korur.
function formatTickValue(v) {
if (Math.abs(v) < 1e-9) return '0';
if (Math.abs(v) >= 100) return v.toFixed(0);
if (Math.abs(v) >= 10) return v.toFixed(1);
return v.toFixed(1);
}
// Tüm görünür çentik etiketleri için dörtgen vertex'leri kur.
//
// Etiketler ızgaranın DIŞINA yerleştirilir:
// X ekseni etiketleri: alt kenarın altında (Y < y0), metin Y ekseni
// boyunca (etiketlenen X eksenine dik) uzansın diye Z etrafında
// -90° döndürülür.
// Y ekseni etiketleri: sol kenarın solunda (X < x0), sağa hizalı.
//
// Etiket inceltme: yalnızca her TICK_STRIDE. çentik etiketlenir, artı
// son çentik her zaman dahil edilir. Izgara çok bölmeli olduğunda
// dağınıklığı önler.
//
// [x,y,z, u,v] vertex'lerinin Float32Array'ini döndürür (vertex başına
// 20 bayt).
function buildLabelVertices(glyphMap) {
const stepX = GRID_SIZE_X / GRID_DIVISIONS_X;
const stepY = GRID_SIZE_Y / GRID_DIVISIONS_Y;
const x0 = -GRID_SIZE_X / 2;
const y0 = -GRID_SIZE_Y / 2;
// İki eksen için çentik konumları ve değerleri üret.
const xTicks = [];
for (let i = 0; i <= GRID_DIVISIONS_X; i++) {
const p = x0 + i * stepX;
xTicks.push({ position: [p, y0, 0], value: p, axis: 0 });
}
const yTicks = [];
for (let i = 0; i <= GRID_DIVISIONS_Y; i++) {
const p = y0 + i * stepY;
yTicks.push({ position: [x0, p, 0], value: p, axis: 1 });
}
// İncelt: her TICK_STRIDE. çentiği göster, sonuncuyu her zaman dahil et.
const thin = (arr) => arr.filter((_, i) =>
i % TICK_STRIDE === 0 || i === arr.length - 1);
const thinned = [...thin(xTicks), ...thin(yTicks)];
const vertices = [];
for (const tick of thinned) {
const label = formatTickValue(tick.value);
// Atlastan glif bilgisine bak (UV'ler + değişken dünya genişliği).
function glyphFor(ch) {
return glyphMap.get(ch) || null;
}
// Toplam etiket genişliği = glif başına ilerleme genişliklerinin toplamı.
let labelWidth = 0;
for (const ch of label) {
const g = glyphFor(ch);
labelWidth += g ? g.worldW : CHAR_HEIGHT * 0.5;
}
if (tick.axis === 0) {
// X ekseni: metin Y boyunca (etiketlenen X eksenine normal)
// uzansın diye etiketi Z etrafında -90° döndür; ızgara düzlemiyle
// (z=0) aynı düzlemde kalır. Etiket X'teki çentiğe merkezlenir
// ve ızgara kenarının hemen altına (Y < y0) yerleştirilir.
//
// Z etrafında -90° dönüş sonrası: (x,y) → (y, -x). Yatay
// offset cx'teki bir glif, pivota göre dikey offset -cx'e gelir.
const pivotX = tick.value;
const pivotY = y0 - LABEL_MARGIN - labelWidth / 2;
let advance = 0;
for (const ch of label) {
const g = glyphFor(ch);
const w = g ? g.worldW : CHAR_HEIGHT * 0.5;
// Etiket merkezine göre döndürülmemiş merkez X.
const cx = advance + w / 2 - labelWidth / 2;
// Z etrafında -90° sonrası: merkez pivota göre (0, -cx)'e
// eşlenir.
const yCenter = pivotY - cx;
// Dönüş sonrası yarı uzanımlar: X = CHAR_HEIGHT/2, Y = w/2.
const hx = CHAR_HEIGHT / 2;
const hy = w / 2;
const u0 = g ? g.u0 : 0, u1 = g ? g.u1 : 0;
const v0 = g ? g.v0 : 0, v1 = g ? g.v1 : 1;
vertices.push(
// Üçgen 1: BL, BR, TR
pivotX - hx, yCenter + hy, 0, u0, v1,
pivotX - hx, yCenter - hy, 0, u1, v1,
pivotX + hx, yCenter - hy, 0, u1, v0,
// Üçgen 2: BL, TR, TL
pivotX - hx, yCenter + hy, 0, u0, v1,
pivotX + hx, yCenter - hy, 0, u1, v0,
pivotX + hx, yCenter + hy, 0, u0, v0,
);
advance += w;
}
} else {
// Y ekseni: etiketi ızgara kenarının soluna (X < x0) sağa
// hizala. Dönüş yok — metin soldan sağa okunur.
const originX = x0 - LABEL_MARGIN - labelWidth;
const originY = tick.value - CHAR_HEIGHT / 2;
let cursorX = originX;
for (const ch of label) {
const g = glyphFor(ch);
const w = g ? g.worldW : CHAR_HEIGHT * 0.5;
const xa = cursorX;
const xb = cursorX + w;
const ya = originY;
const yb = originY + CHAR_HEIGHT;
const u0 = g ? g.u0 : 0, u1 = g ? g.u1 : 0;
const v0 = g ? g.v0 : 0, v1 = g ? g.v1 : 1;
// İki üçgen: (xa,ya) (xb,ya) (xb,yb) (xa,yb)
// UV: sol-üst=(u0,v0), sağ-üst=(u1,v0),
// sağ-alt=(u1,v1), sol-alt=(u0,v1)
vertices.push(
// Üçgen 1: sol-alt, sağ-alt, sağ-üst
xa, ya, 0, u0, v1,
xb, ya, 0, u1, v1,
xb, yb, 0, u1, v0,
// Üçgen 2: sol-alt, sağ-üst, sol-üst
xa, ya, 0, u0, v1,
xb, yb, 0, u1, v0,
xa, yb, 0, u0, v0,
);
cursorX += w;
}
}
}
return new Float32Array(vertices);
}
// ─────────────────────────────────────────────────────────────────────
// WGSL shader
// ─────────────────────────────────────────────────────────────────────
// Tek shader modülü, üç pipeline (dama + ızgara çizgileri + etiketler).
//
// Izgara çizgisi pipeline'ı konumları view-projection matrisiyle
// dönüştürür ve vertex başına rengi geçirir; fragment shader önceden
// çarpılmış alfayla çıktılar.
//
// Dama pipeline'ı dünya uzayı XY konumunu varying olarak fragment
// shader'a geçirir. Fragment shader, floor(worldPos / cellSize)
// kullanarak fragment'in hangi dama hücresine düştüğünü hesaplar ve
// (cx + cy) % 2'ye göre iki gri tonu arasında geçiş yapar.
//
// Etiket pipeline'ı glif atlası texture'ını örnekler ve kırmızı kanalı
// alfa olarak kullanır (saydam arka planda beyaz metin). Metin rengi
// uniform'dan gelir.
//
// Üç pipeline da önceden çarpılmış alfa harmanlama kullanır: fragment
// shader'lar (color * alpha, alpha) çıktılar ve blend durumu
// srcFactor: 'one' ('src-alpha' değil) kullanır. Bu, canvas'taki
// alphaMode: 'premultiplied' için doğru kurulumdur — onsuz, alfa
// kanalı çift koyulaşacağından dama ve etiketler neredeyse görünmez
// olurdu.
//
// Aşağıdaki ${...} template literal'ları shader oluşturma zamanında
// JavaScript tarafından interpolasyonlanır ve ızgara boyutu ile bölme
// sayısını doğrudan WGSL kaynağına gömer. Bu, ekstra uniform'ları önler.
const SHADER = /* wgsl */ `
struct Uniforms {
viewProj: mat4x4<f32>,
};
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
// ── Izgara çizgisi pipeline'ı ──
struct LineVertexInput {
@location(0) position: vec3<f32>,
@location(1) color: vec3<f32>,
};
struct LineVertexOutput {
@builtin(position) clipPosition: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vs_line(input: LineVertexInput) -> LineVertexOutput {
var out: LineVertexOutput;
out.clipPosition = uniforms.viewProj * vec4<f32>(input.position, 1.0);
out.color = input.color;
return out;
}
@fragment
fn fs_line(input: LineVertexOutput) -> @location(0) vec4<f32> {
// Önceden çarpılmış alfa: blend durumunun (srcFactor: 'one') doğru
// birleştirmesi için (color * alpha, alpha) çıktıla.
let alpha = 0.7;
return vec4<f32>(input.color * alpha, alpha);
}
// ── Dama pipeline'ı ──
// Vertex shader, dama hücresi koordinatlarını hesaplayabilmesi için
// dünya uzayı XY'yi fragment shader'a geçirir.
struct CheckerVertexOutput {
@builtin(position) clipPosition: vec4<f32>,
@location(0) worldPos: vec2<f32>,
};
@vertex
fn vs_checker(@location(0) position: vec3<f32>) -> CheckerVertexOutput {
var out: CheckerVertexOutput;
out.clipPosition = uniforms.viewProj * vec4<f32>(position, 1.0);
// Dünya uzayı XY konumunu varying olarak geçir. Fragment shader bunu,
// fragment'in hangi dama hücresine ait olduğunu belirlemek için
// kullanır.
out.worldPos = position.xy;
return out;
}
@fragment
fn fs_checker(input: CheckerVertexOutput) -> @location(0) vec4<f32> {
// Dünya birimindeki hücre boyutu = eksen başına GRID_SIZE /
// GRID_DIVISIONS. Bu sabitler shader oluşturma zamanında gömülür.
// Kare dama hücreleri için ikisi eşit olmalı (burada
// 200/20 = 300/30 = 10).
let cellSizeX = ${GRID_SIZE_X.toFixed(1)} / ${GRID_DIVISIONS_X}.0;
let cellSizeY = ${GRID_SIZE_Y.toFixed(1)} / ${GRID_DIVISIONS_Y}.0;
// Tam sayı hücre koordinatları: bu fragment hangi hücreye düşüyor?
let cx = floor(input.worldPos.x / cellSizeX);
let cy = floor(input.worldPos.y / cellSizeY);
// (cx + cy) % 2 komşu hücrelerde 0 ve 1 arasında geçiş yaparak
// klasik dama desenini üretir.
let checker = (cx + cy) % 2.0;
// Dama kareleri için iki gri tonu. Değerler ince seçilmiştir —
// ızgara ön plan nesnesi değil arka plan referansıdır. Alpha = 0.35
// yarı saydam yapar; canvas arka planı görünür.
let lightSquare = vec3<f32>(0.62, 0.63, 0.67);
let darkSquare = vec3<f32>(0.52, 0.53, 0.57);
let color = select(darkSquare, lightSquare, checker > 0.5);
// Önceden çarpılmış alfa çıktısı.
let alpha = 0.35;
return vec4<f32>(color * alpha, alpha);
}
// ── Etiket pipeline'ı ──
// viewProj + metin rengi içeren ayrı uniform struct kullanır.
struct LabelUniforms {
viewProj: mat4x4<f32>,
color: vec3<f32>,
_pad: f32,
};
@group(0) @binding(0) var<uniform> labelUniforms: LabelUniforms;
@group(0) @binding(1) var atlas: texture_2d<f32>;
@group(0) @binding(2) var atlasSampler: sampler;
struct LabelVertexInput {
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
};
struct LabelVertexOutput {
@builtin(position) clipPosition: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs_label(input: LabelVertexInput) -> LabelVertexOutput {
var out: LabelVertexOutput;
out.clipPosition = labelUniforms.viewProj * vec4<f32>(input.position, 1.0);
out.uv = input.uv;
return out;
}
@fragment
fn fs_label(input: LabelVertexOutput) -> @location(0) vec4<f32> {
// Glif atlasını örnekle. Atlas saydam arka planda beyaz metin
// olduğundan kırmızı kanal glif kapsamını (alfa) kodlar.
let sampled = textureSample(atlas, atlasSampler, input.uv);
let alpha = sampled.r * 0.7;
// Önceden çarpılmış alfa: color * alpha.
return vec4<f32>(labelUniforms.color * alpha, alpha);
}
`;
// ─────────────────────────────────────────────────────────────────────
// Canvas başına başlatma
// ─────────────────────────────────────────────────────────────────────
// Sayfadaki her <canvas class="nav-grid-canvas"> için bir kez çağrılır.
// WebGPU cihazını, pipeline'ları, geometri buffer'larını, glif
// atlasını, etkileşimi kurar ve render döngüsünü başlatır. Tüm GPU
// kaynakları closure'da yakalanır; böylece render döngüsü global durum
// olmadan erişebilir.
async function initCanvas(canvas) {
// --- WebGPU başlatma ---
// navigator.gpu giriş noktasıdır. Yoksa tarayıcı WebGPU'yu hiç
// desteklemiyor demektir (örn. flags'siz Firefox veya eski bir
// tarayıcı). Sayfada bozuk boş bir dikdörtgen kalmaması için
// canvas'ı metin düğümüyle değiştiriyoruz.
if (!navigator.gpu) {
canvas.replaceWith(document.createTextNode('WebGPU is not supported in this browser.'));
return;
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
canvas.replaceWith(document.createTextNode('No WebGPU adapter available.'));
return;
}
const device = await adapter.requestDevice();
// Canvas'ın WebGPU context'ini yapılandır. alphaMode: 'premultiplied',
// canvas'ın sayfa arka planıyla harmanlanmasını sağlar — dama ve
// etiketler yarı saydam olduğundan ve canvas arka planının (beyaz)
// görünmesini istediğimiz için burada önemli. Tüm fragment
// shader'lar bu moda uygun önceden çarpılmış alfa çıktılar.
const ctx = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
ctx.configure({ device, format, alphaMode: 'premultiplied' });
// WGSL shader'ı shader modülüne derle.
const shader = device.createShaderModule({ code: SHADER });
// --- Blend durumu (önceden çarpılmış alfa) ---
// Önceden çarpılmış alfada fragment shader (color * alpha, alpha)
// çıktılar ve blend durumu srcFactor: 'one' ('src-alpha' değil)
// kullanır. Bu, canvas'taki alphaMode: 'premultiplied' için doğru
// blend kurulumudur. Burada düz alfa harmanlama kullanmak alfa
// kanalını çift koyulaştırarak yarı saydam nesneleri neredeyse
// görünmez yapardı.
const blendState = {
color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
};
// --- Pipeline'lar ---
// Aynı shader modülünü paylaşan üç render pipeline'ı:
// checkerPipeline — dama dörtgenini render eder (üçgenler)
// linePipeline — ızgara çizgileri, eksenler, çentikler (çizgiler)
// labelPipeline — sayısal çentik etiketleri (dokulu üçgenler)
//
// checker ve line pipeline'ları layout: 'auto' kullanır ve tek
// uniform buffer'ı paylaşır (yalnız viewProj, 64 bayt). label
// pipeline'ı, uniform buffer'a (viewProj + color, 80 bayt) ek olarak
// atlas texture'ı ve sampler'a ihtiyaç duyduğundan kendi bind group
// layout'una sahiptir.
//
// Tüm pipeline'lar depthWriteEnabled: false kullanır; böylece ızgara
// kendinden sonra çizilebilecek diğer geometriyi kapatmaz. Tam bir
// 3D görüntüleyicide ızgara önce çizilir, sonra üstüne toolpath /
// model çizilir. depthCompare: 'less' hâlâ derinlik buffer'ıyla
// test eder, yani ızgaradan önce çizilen geometri onu kapatabilir.
// Dama pipeline'ı: tek dörtgen render eder, desen fragment
// shader'da.
const checkerPipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shader, entryPoint: 'vs_checker',
// Yalnız konum: vertex başına 3 float = 12 bayt.
buffers: [{
arrayStride: 12,
attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
}],
},
fragment: {
module: shader, entryPoint: 'fs_checker',
targets: [{ format, blend: blendState }],
},
primitive: { topology: 'triangle-list', cullMode: 'none' },
depthStencil: { format: 'depth24plus', depthCompare: 'less', depthWriteEnabled: false },
});
// Izgara çizgisi pipeline'ı: vertex başına renkli line-list render eder.
const linePipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shader, entryPoint: 'vs_line',
// Örülmüş düzen: konum (float32x3) + renk (float32x3)
// = vertex başına 24 bayt.
buffers: [{
arrayStride: 24,
attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
{ shaderLocation: 1, offset: 12, format: 'float32x3' },
],
}],
},
fragment: {
module: shader, entryPoint: 'fs_line',
targets: [{ format, blend: blendState }],
},
primitive: { topology: 'line-list' },
depthStencil: { format: 'depth24plus', depthCompare: 'less', depthWriteEnabled: false },
});
// --- Glif atlası ---
// Bitmap atlasını 2D canvas'ta üret ve WebGPU texture'ı olarak yükle.
// Atlas, saydam arka planda beyaz gliflerin tek yatay şerididir.
// Etiket fragment shader'ı onu örnekler ve kırmızı kanalı alfa
// olarak kullanır.
const { atlasCanvas, atlasW, atlasH, glyphMap } = buildGlyphAtlas();
const atlasTexture = device.createTexture({
size: [atlasW, atlasH],
format: 'rgba8unorm',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
});
device.queue.copyExternalImageToTexture(
{ source: atlasCanvas },
{ texture: atlasTexture },
[atlasW, atlasH],
);
const atlasSampler = device.createSampler({
magFilter: 'linear',
minFilter: 'linear',
});
// Etiket pipeline'ı: glif atlasını kullanan dokulu dörtgenler render eder.
// Uniform buffer'a ek olarak texture + sampler binding'leri
// gerektirdiğinden kendi bind group layout'una sahiptir.
const labelPipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shader, entryPoint: 'vs_label',
// Konum (float32x3) + UV (float32x2) = vertex başına 20 bayt.
buffers: [{
arrayStride: 20,
attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
{ shaderLocation: 1, offset: 12, format: 'float32x2' },
],
}],
},
fragment: {
module: shader, entryPoint: 'fs_label',
targets: [{ format, blend: blendState }],
},
primitive: { topology: 'triangle-list' },
depthStencil: { format: 'depth24plus', depthCompare: 'less', depthWriteEnabled: false },
});
// --- Geometriyi GPU buffer'larına yükle ---
// Tüm geometri statik — bir kez yüklenir, hiç değiştirilmez.
// Dama dörtgeni: 6 vertex × 3 float × 4 bayt = 72 bayt.
const checkerVerts = buildCheckerQuad();
const checkerVbuf = device.createBuffer({
size: checkerVerts.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(checkerVbuf, 0, checkerVerts);
// Izgara çizgileri: vertex başına örülmüş [x,y,z, r,g,b].
const lineVerts = buildGridLines();
const lineVbuf = device.createBuffer({
size: lineVerts.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(lineVbuf, 0, lineVerts);
// Etiket dörtgenleri: vertex başına örülmüş [x,y,z, u,v].
const labelVerts = buildLabelVertices(glyphMap);
const labelVbuf = device.createBuffer({
size: labelVerts.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(labelVbuf, 0, labelVerts);
// --- Uniform buffer'lar ---
// Checker + line pipeline'ları 64 baytlık uniform buffer'ı paylaşır
// (yalnız viewProj). Label pipeline'ı kendi 80 baytlık uniform
// buffer'ına sahiptir (viewProj + color + pad).
const ubuf = device.createBuffer({
size: 64,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const checkerBindGroup = device.createBindGroup({
layout: checkerPipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: ubuf } }],
});
const lineBindGroup = device.createBindGroup({
layout: linePipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: ubuf } }],
});
// Etiket uniform buffer'ı: 64 bayt (viewProj) + 12 bayt (color) + 4 bayt (pad) = 80 bayt.
const labelUbuf = device.createBuffer({
size: 80,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const labelBindGroup = device.createBindGroup({
layout: labelPipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: labelUbuf } },
{ binding: 1, resource: atlasTexture.createView() },
{ binding: 2, resource: atlasSampler },
],
});
// --- Derinlik texture'ı ---
// Canvas boyutu değiştiğinde yeniden oluşturulur. Tüm pipeline'lar
// depthStencil durumu bildirdiğinden gereklidir (depthWriteEnabled:
// false olsa bile, pipeline belirtirse WebGPU hâlâ derinlik eki
// ister).
let depthTex = null;
function ensureDepth(w, h) {
if (depthTex && depthTex.width === w && depthTex.height === h) return;
if (depthTex) depthTex.destroy();
depthTex = device.createTexture({
size: [w, h, 1], format: 'depth24plus', usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
}
// --- Etkileşim: yörünge kamerası ---
// Kamera küresel koordinatlarla orijini turlar:
// yaw — yatay açı (sola/sağa sürükle)
// pitch — dikey açı (yukarı/aşağı sürükle), (0, π/2)'ye kenetli
// dist — orijinden mesafe (fare tekerleği)
//
// (yaw, pitch, dist)'i Kartezyen göz konumuna çevirip mat4LookAt'a
// veriyoruz. Up her zaman +Z (CAD kuralı: Z yukarıdır).
//
// setPointerCapture, sürükleme sırasında imleç canvas'ı terk etse
// bile pointermove event'lerini almaya devam etmemizi sağlar.
let yaw = Math.PI / 4, pitch = Math.PI / 3.5, dist = 400;
let dragging = false, lastX = 0, lastY = 0;
canvas.style.cursor = 'grab';
canvas.style.touchAction = 'none'; // dokunmatik cihazlarda kaydırmayı engelle
canvas.addEventListener('pointerdown', (e) => {
dragging = true; lastX = e.clientX; lastY = e.clientY;
canvas.setPointerCapture(e.pointerId);
canvas.style.cursor = 'grabbing';
});
canvas.addEventListener('pointerup', () => {
dragging = false; canvas.style.cursor = 'grab';
});
canvas.addEventListener('pointermove', (e) => {
if (!dragging) return;
const dx = e.clientX - lastX, dy = e.clientY - lastY;
lastX = e.clientX; lastY = e.clientY;
// Piksel başına 0.01 rad — tam ekran genişliğinde sürükleme
// ≈ 6.3 rad ≈ 360°.
yaw -= dx * 0.01;
pitch += dy * 0.01;
// pitch'i (0.05, π/2 - 0.05)'e kenetle; böylece kamera hiç
// ızgara düzleminin altına gitmez ve doğrudan aşağı bakmaz
// (bu ızgarayı bir kareye indirgerdi).
pitch = Math.max(0.05, Math.min(Math.PI / 2 - 0.05, pitch));
});
// Fare tekerleği yakınlaştırması: yörünge yarıçapını ayarla.
// Çentik başına 1.1 faktörü akıcı yakınlaştırma hissi verir.
// dist'i [50, 2000]'e kenetliyoruz; böylece kullanıcı ızgaranın
// içine yakınlaşamaz veya far düzleminin ötesine uzaklaşamaz.
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const factor = e.deltaY > 0 ? 1.1 : 1 / 1.1;
dist = Math.max(50, Math.min(2000, dist * factor));
}, { passive: false });
// --- Render döngüsü ---
// requestAnimationFrame ile animasyon karesi başına bir kez çalışır.
// Her karede:
// 1. CSS boyutu değiştiyse canvas + derinlik texture'ını yeniden
// boyutlandır.
// 2. Yörünge kamera konumunu ve view-projection matrisini hesapla.
// 3. view-projection'ı uniform buffer'lara yükle.
// 4. Render pass kaydet: dama, ızgara çizgileri, etiketler çiz.
// 5. Gönder ve sonraki kareyi zamanla.
function frame() {
// HiDPI'yı işle: devicePixelResolution'da render et ama yüksek
// DPI'lı telefonlarda aşırı fill rate'i önlemek için 2×'te sınırla.
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const cw = Math.max(1, Math.floor(canvas.clientWidth * dpr));
const ch = Math.max(1, Math.floor(canvas.clientHeight * dpr));
if (canvas.width !== cw || canvas.height !== ch) {
canvas.width = cw; canvas.height = ch;
}
ensureDepth(cw, ch);
// Küresel yörünge koordinatlarını Kartezyen göz konumuna çevir.
// x = dist * cos(pitch) * cos(yaw)
// y = dist * cos(pitch) * sin(yaw)
// z = dist * sin(pitch)
// up = +Z ile kamera XY düzleminin üstünden ızgaraya açıyla bakar.
// Varsayılan yaw=45°, pitch≈51° standart CAD izometrik benzeri
// görünüm verir.
const eye = {
x: dist * Math.cos(pitch) * Math.cos(yaw),
y: dist * Math.cos(pitch) * Math.sin(yaw),
z: dist * Math.sin(pitch),
};
// view-projection matrisini kur:
// viewProj = proj * view
// burada:
// proj = perspective(45°, aspect, 0.1, 10000)
// view = lookAt(eye, origin, +Z)
//
// far düzlemi (10000) cömerttir; dist = 2000'e kadar
// uzaklaştırılsa bile ızgara hiç kırpılmaz.
const aspect = cw / ch;
const proj = mat4Perspective(Math.PI / 4, aspect, 0.1, 10000);
const view = mat4LookAt(eye, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 1 });
const viewProj = mat4Multiply(proj, view);
// viewProj'ı checker/line uniform buffer'ına (64 bayt) yükle.
device.queue.writeBuffer(ubuf, 0, viewProj);
// viewProj + etiket rengini etiket uniform buffer'ına (80 bayt)
// yükle.
// Düzen: [viewProj (16 float)] [color (3 float)] [pad (1 float)]
const labelUniform = new Float32Array(20);
labelUniform.set(viewProj, 0);
labelUniform[16] = LABEL_COLOR[0];
labelUniform[17] = LABEL_COLOR[1];
labelUniform[18] = LABEL_COLOR[2];
labelUniform[19] = 0; // dolgu
device.queue.writeBuffer(labelUbuf, 0, labelUniform);
// Render pass'i kaydet. Canvas'ın CSS arka planı görünsün diye
// şeffafa (alfa = 0) temizle. Derinlik buffer'ı 1.0'a (en uzağa)
// temizlenir.
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: ctx.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0, b: 0, a: 0 },
loadOp: 'clear', storeOp: 'store',
}],
depthStencilAttachment: {
view: depthTex.createView(),
depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store',
},
});
// Pass 1: dama dörtgenini çiz (6 vertex = 2 üçgen).
// Izgara çizgileri ve etiketler üstüne render edilsin diye önce
// çizilir.
pass.setPipeline(checkerPipeline);
pass.setBindGroup(0, checkerBindGroup);
pass.setVertexBuffer(0, checkerVbuf);
pass.draw(6);
// Pass 2: tüm ızgara çizgilerini, eksen kenarlarını ve çentik
// işaretlerini çiz.
// lineVerts örülmüş [x,y,z, r,g,b] olduğundan vertex sayısı =
// dizi uzunluğu / 6.
pass.setPipeline(linePipeline);
pass.setBindGroup(0, lineBindGroup);
pass.setVertexBuffer(0, lineVbuf);
pass.draw(lineVerts.length / 6);
// Pass 3: sayısal çentik etiketlerini çiz (dokulu dörtgenler).
// labelVerts örülmüş [x,y,z, u,v] olduğundan vertex sayısı =
// dizi uzunluğu / 5.
pass.setPipeline(labelPipeline);
pass.setBindGroup(0, labelBindGroup);
pass.setVertexBuffer(0, labelVbuf);
pass.draw(labelVerts.length / 5);
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
// ─────────────────────────────────────────────────────────────────────
// Otomatik başlatma
// ─────────────────────────────────────────────────────────────────────
// Sayfadaki her <canvas class="nav-grid-canvas">'ı bul ve başlat.
// Belge hâlâ yükleniyorsa (script <head>'den veya defer ile çalıştı),
// canvas'ların var olması için DOMContentLoaded'ı bekle. Belge zaten
// hazırsa (script geç enjekte edildiyse) hemen başlat.
function initAll() {
document.querySelectorAll('canvas.nav-grid-canvas').forEach(initCanvas);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAll);
} else {
initAll();
}
})();Script ve yukarıdaki satır içi liste aynıdır ve ikisi de CC0-1.0
altında yayınlanmıştır. Bu uygulama, WebGCodeViewer projesindeki
GridRenderer ve GridLabelRenderer‘ın bağımlılıksız bir portudur.
Orijinal sürüm sınırlayıcı kutuya dinamik yeniden merkezlemeyi de
destekler; netlik için burada çıkarıldı. Temel teknikler (fragment
shader’da prosedürel dama, line-list ızgara geometrisi, glif atlaslı
ölçek etiketleri, derinlik yazımı kapalı önceden çarpılmış alfa
harmanlı katman) aynıdır.