Vaziyet Planı
Sol panelden görsel yükle
veya direkt çizmeye başla
100%
/ Zon Detayı
`; } /* ═══════════════════════════════════════════════════════ ZONE TYPE LIST ═══════════════════════════════════════════════════════ */ function buildZoneTypeList(){ const el = document.getElementById('zoneTypeList'); el.innerHTML = ZONE_TYPES.map(t=>`
${t.label} 0
`).join(''); } function selectType(id){ selectedTypeId = id; document.querySelectorAll('.zt-item').forEach(el=>el.classList.remove('selected')); document.getElementById('zt-'+id)?.classList.add('selected'); } /* ═══════════════════════════════════════════════════════ ZONE LIST ═══════════════════════════════════════════════════════ */ /* LandCrit v9 Faz A prune: removed duplicate definition (buildZoneList) */ /* ═══════════════════════════════════════════════════════ IMAGE LOAD ═══════════════════════════════════════════════════════ */ /* LandCrit v9 Faz A prune: removed duplicate definition (loadImage) */ function handleDrop(e){ e.preventDefault(); handleDragLeave(e); const f=e.dataTransfer.files[0]; if(f&&f.type.startsWith('image/')){ const ev={target:{files:[f]}}; loadImage(ev); } } function handleDragOver(e){ e.preventDefault(); document.getElementById('uploadZone').classList.add('drag'); } function handleDragLeave(e){ document.getElementById('uploadZone').classList.remove('drag'); } /* LandCrit v9 Faz A prune: removed duplicate definition (fitImageToCanvas) */ /* ═══════════════════════════════════════════════════════ RENDER ═══════════════════════════════════════════════════════ */ /* LandCrit v9 Faz A prune: removed duplicate definition (render) */ /* LandCrit v9 Faz A prune: removed duplicate definition (drawZone) */ /* ═══════════════════════════════════════════════════════ AREA CALC (Shoelace) ═══════════════════════════════════════════════════════ */ function calcArea(pts){ return polygonAreaPx(pts) * metersPerPx * metersPerPx; } /* ═══════════════════════════════════════════════════════ CONFLICT DETECTION ═══════════════════════════════════════════════════════ */ function findConflictRule(t1,t2){ return CONFLICT_RULES.find(r=> (r.a===t1 && r.b===t2) || (r.a===t2 && r.b===t1)) || null; } function polygonOverlap(ptsA, ptsB){ if(!ptsA||ptsA.length<3||!ptsB||ptsB.length<3) return false; // Edge intersection for(let i=0;i z.id!==zone.id && zonesAreNeighbors(zone,z,thresh)); nearby.forEach(z2=>{ const rule = findConflictRule(t1, z2.typeId); if(!rule) return; // bbox ön eleme: iki bbox arası boşluk, gerekli tamponun çok üstündeyse hesap yapma const reqM = Number(rule.minDistanceM||0); const reqPx = reqM / Math.max(metersPerPx, 0.0001); const b1 = bbox(zone.pts), b2 = bbox(z2.pts); const gapX = Math.max(0, Math.max(b1.minX,b2.minX)-Math.min(b1.maxX,b2.maxX)); const gapY = Math.max(0, Math.max(b1.minY,b2.minY)-Math.min(b1.maxY,b2.maxY)); if(Math.max(gapX,gapY) > (reqPx + 8)) return; const dPx = polygonMinDistancePx(zone.pts, z2.pts); const overlap = dPx===0; const dM = overlap ? 0 : (dPx * metersPerPx); if(overlap || dM < reqM){ results.push({ sev: rule.sev, note: rule.note, withLabel: z2.label, pair: [zone.id, z2.id].sort().join('|'), distM: dM, requiredM: reqM, overlap }); } }); return results; } function zonesAreNeighbors(z1,z2,threshPx){ const b1=bbox(z1.pts), b2=bbox(z2.pts); const thresh=(typeof threshPx==='number' && isFinite(threshPx)) ? threshPx : getNeighborThresholdPx(); const gx=Math.max(0, Math.max(b1.minX,b2.minX)-Math.min(b1.maxX,b2.maxX)); const gy=Math.max(0, Math.max(b1.minY,b2.minY)-Math.min(b1.maxY,b2.maxY)); return gx<=thresh && gy<=thresh; } function bbox(pts){ if(!pts||!pts.length) return {minX:0,maxX:0,minY:0,maxY:0}; return {minX:Math.min(...pts.map(p=>p.x)),maxX:Math.max(...pts.map(p=>p.x)), minY:Math.min(...pts.map(p=>p.y)),maxY:Math.max(...pts.map(p=>p.y))}; } /* ═══════════════════════════════════════════════════════ CANVAS → WORLD COORDS ═══════════════════════════════════════════════════════ */ function canvasToWorld(cx,cy){ return { x: (cx-offsetX)/scale, y: (cy-offsetY)/scale }; } function worldToCanvas(wx,wy){ return { x: wx*scale+offsetX, y: wy*scale+offsetY }; } function dist(a,b){ return Math.sqrt((a.x-b.x)**2+(a.y-b.y)**2); } /* ═══════════════════════════════════════════════════════ MOUSE EVENTS ═══════════════════════════════════════════════════════ */ /* LandCrit v9 Faz A prune: removed duplicate definition (onMouseMove) */ /* LandCrit v9 Faz A prune: removed duplicate definition (onMouseDown) */ /* LandCrit v9 Faz A prune: removed duplicate definition (onMouseUp) */ /* LandCrit v9 Faz A prune: removed duplicate definition (onCanvasClick) */ /* LandCrit v9 Faz A prune: removed duplicate definition (onCanvasDblClick) */ function onWheel(e){ e.preventDefault(); const r=canvas.getBoundingClientRect(); const cx=e.clientX-r.left, cy=e.clientY-r.top; const factor = e.deltaY<0 ? 1.12 : 0.89; const newScale=Math.min(Math.max(scale*factor, 0.1), 10); offsetX=cx - (cx-offsetX)*(newScale/scale); offsetY=cy - (cy-offsetY)*(newScale/scale); scale=newScale; markDirty(); } /* LandCrit v9 Faz A prune: removed duplicate definition (onKeyDown) */ function pointInPolygon(pt, pts){ if(!pts||pts.length<3) return false; let inside=false; for(let i=0,j=pts.length-1;ipt.y)!=(yj>pt.y))&&(pt.x<(xj-xi)*(pt.y-yi) /(yj-yi)+xi)) inside=!inside; } return inside; } /* ═══════════════════════════════════════════════════════ DRAWING WORKFLOW ═══════════════════════════════════════════════════════ */ /* LandCrit v9 Faz A prune: removed duplicate definition (finishDrawing) */ /* LandCrit v9 Faz A prune: removed duplicate definition (cancelDrawing) */ function showZoneNameInput(){ const el=document.getElementById('zoneNameInput'); const t=ZONE_TYPES.find(x=>x.id===selectedTypeId); document.getElementById('zoneNameField').value = t?.label||''; // Type quick pick document.getElementById('zniType').innerHTML = ZONE_TYPES.map(tp=> `` ).join(''); el.style.display='flex'; el.style.left='50%'; el.style.top='40%'; el.style.transform='translate(-50%,-50%)'; el.style.position='absolute'; document.getElementById('zoneNameField').focus(); document.getElementById('zoneNameField').select(); } function buildZniTypes(){ document.getElementById('zniType').innerHTML = ZONE_TYPES.map(tp=> `` ).join(''); } function hideZoneNameInput(){ document.getElementById('zoneNameInput').style.display='none'; } /* LandCrit v9 Faz A prune: removed duplicate definition (confirmZone) */ function cancelZone(){ cancelDrawing(); } function handleNameKey(e){ if(e.key==='Enter') confirmZone(); if(e.key==='Escape') cancelZone(); } /* ═══════════════════════════════════════════════════════ ZONE SELECT / DELETE ═══════════════════════════════════════════════════════ */ /* LandCrit v9 Faz A prune: removed duplicate definition (selectZone) */ /* LandCrit v9 Faz A prune: removed duplicate definition (deselectZone) */ /* LandCrit v9 Faz A prune: removed duplicate definition (deleteSelected) */ /* LandCrit v9 Faz A prune: removed duplicate definition (undoLast) */ /* ═══════════════════════════════════════════════════════ MODE TOGGLE ═══════════════════════════════════════════════════════ */ /* LandCrit v9 Faz A prune: removed duplicate definition (toggleMode) */ /* ═══════════════════════════════════════════════════════ ZOOM ═══════════════════════════════════════════════════════ */ function zoom(f){ const cx=canvas.width/2, cy=canvas.height/2; const ns=Math.min(Math.max(scale*f,.1),10); offsetX=cx-(cx-offsetX)*(ns/scale); offsetY=cy-(cy-offsetY)*(ns/scale); scale=ns; } function resetZoom(){ scale=1; offsetX=0; offsetY=0; if(bg) fitImageToCanvas(); saveState(); markDirty(); } function updateZoomLabel(){ document.getElementById('zoomLabel').textContent=Math.round(scale*100)+'%'; } /* ═══════════════════════════════════════════════════════ RIGHT PANEL ═══════════════════════════════════════════════════════ */ function openPanel(zoneId){ const z=zones.find(x=>x.id===zoneId); if(!z) return; const t=ZONE_TYPES.find(x=>x.id===z.typeId); const conflicts=detectConflicts(z); const assignedPlants = Array.isArray(z.plants) ? z.plants : []; const suggestedPlants = suggestionsForZoneType(z.typeId); const hasCritical=conflicts.some(c=>c.sev==='kritik'); document.getElementById('prLabel').textContent=`/ ${z.id} · ${t?.ana_tip||'Bilinmiyor'}`; document.getElementById('prTitle').textContent=z.label; document.getElementById('prMeta').textContent=`${Math.round(z.area||0)} m² · ${t?.label||'?'}`; const scoreColor = z.score>=85?'var(--a2)':z.score>=70?'var(--acc)':z.score>=55?'var(--a4)':'var(--danger)'; const circumference=157; const offset=circumference-(z.score/100)*circumference; const conflictsHTML = conflicts.length===0 ? `
TemizKomşu zon çatışması tespit edilmedi.
` : conflicts.map(c=>`
${c.sev} ${c.withLabel}${c.overlap?'ÇAKIŞIYOR':(typeof c.distM==='number'?c.distM.toFixed(1)+' m':'-')} · min ${typeof c.requiredM==='number'?c.requiredM.toFixed(1)+' m':'-'}
${c.note}
`).join(''); const assignedHTML = assignedPlants.length ? assignedPlants.map(ap=>`
${ap.tr || ap.name || 'Bitki'}
${ap.la || ap.sci || ''}
adet: ${ap.qty || 1}
`).join('') : `
K4Bu zona henüz bitki atanmadı. Aşağıdaki arama ile DB'den ekleyebilirsin.
`; const suggestedHTML = suggestedPlants.map(p=>`
${p.name}
${p.sci || ''}
${p.layer || '—'}
${p.score != null ? `
${p.score}
` : ``}
`).join(''); // #20 Enhanced conflict UI — renkli badge + özet sayaç const critCount = conflicts.filter(c=>c.sev==='kritik').length; const warnCount = conflicts.filter(c=>c.sev==='orta'||c.sev==='düşük').length; const conflictSummary = conflicts.length===0 ? `✓ Çatışma yok` : `${critCount>0?`⚠ ${critCount} KRİTİK`:''}${warnCount>0?`! ${warnCount} uyarı`:''}`; document.getElementById('prScroll').innerHTML = `
Skor
${z.score}
${z.score>=85?'Güçlü':z.score>=70?'Uygun':z.score>=55?'Kontrollü':'Riskli'}
${z.programs?.length?`
Programlar
${z.programs.map(p=>`${p}`).join('')} ${z.safety?`güvenli ✓`:''} ${hasCritical?`⚠ kritik`:''}
`:''}
Çatışma Taraması
${conflictSummary}
${conflictsHTML}
Bitkiler (K4)
${assignedHTML}
Atanan bitkiler projede saklanır.
Bitki Önerisi (API)
⟳ Yükleniyor...
${hasCritical?`
/ Kritik Karar
${conflicts.filter(c=>c.sev==='kritik').map(c=>c.note).join('
')}
`:''}
`; document.getElementById('rightPanel').classList.add('open'); bindPlantUI(z.id); // #18 + #19 — API çağrılarını başlat fetchZoneApiData(z, conflicts); } function closePanel(){ document.getElementById('rightPanel').classList.remove('open'); selectedZoneId=null; document.getElementById('deleteBtn').style.display='none'; } /* ──────────────────────────────────────────────────────── #18 + #19 — API-backed plant suggestions + K1-K4 mini panel ──────────────────────────────────────────────────────── */ const ZONE_TO_AREA_TYPE = { giris:'kent_meydani', oyun:'okul_bahcesi', havuz:'rezidans', otopark:'spor_alani', sosyal:'kent_meydani', yesil:'rezidans', spor:'spor_alani', terapi:'hastane_bahcesi', cati:'rezidans', karma:'rezidans' }; function getApiRegion(){ const site = (projectMetaV8?.site||'').toLowerCase(); if(site.match(/akdeniz|antalya|mersin|adana|alanya/)) return 'akdeniz'; if(site.match(/ege|izmir|bodrum|muğla/)) return 'ege'; if(site.match(/karadeniz|samsun|trabzon|rize/)) return 'karadeniz'; if(site.match(/konya|ankara|kayseri|iç anadolu/)) return 'ic_anadolu'; if(site.match(/gaziantep|şanlıurfa|diyarbakır|güneydoğu/)) return 'guneydogu'; return 'marmara'; // varsayılan } async function fetchZoneApiData(z, conflicts){ const area_type = ZONE_TO_AREA_TYPE[z.typeId] || 'rezidans'; const region = getApiRegion(); const area_m2 = Math.round(z.area || 0); const conflict_count = conflicts.length; const critical_conflict_count = conflicts.filter(c=>c.sev==='kritik').length; // #18 — bitki önerisi try { const r = await fetch( `/flask/v1/plants/suggest?region=${region}&area_type=${area_type}&count_per_layer=4&non_toxic=false&non_allergen=false` ); if(r.ok){ const data = await r.json(); const layers = data.layers || data; const sEl = document.getElementById('apiPlantSuggestContent'); if(!sEl) return; const layerOrder = ['Üst','Orta','Alt']; let html = ''; layerOrder.forEach(layer=>{ const list = layers[layer]; if(!Array.isArray(list)||!list.length) return; html += `
/ ${layer.toUpperCase()} KATMAN
${list.map(p=>`
${escHtml(p.name_tr||p.name_latin||'')}
${escHtml(p.name_latin||'')}
${p.water_need||''} su · ${p.sun_need||''}
+ ekle
`).join('')}
`; }); sEl.innerHTML = html || '
Öneri bulunamadı.
'; } } catch(e){ /* sessiz hata */ } // #19 — K1-K4 mini analiz if(!area_m2) return; try { const r = await fetch('/flask/v1/analyze/zone', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ zone_type: z.typeId, area_m2, region, area_type, existing_plants: (z.plants||[]).map(p=>p.plantId||p.tr), conflict_count, critical_conflict_count }) }); if(r.ok){ const d = await r.json(); const scores = d.scores || {}; const kPanel = document.getElementById('apiK1K4Panel'); const kContent = document.getElementById('apiK1K4Content'); if(!kPanel||!kContent) return; const kLabels = {k1_functional:'K1 İşlev',k2_spatial:'K2 Mekan',k3_technical:'K3 Teknik',k4_botanical:'K4 Botanik'}; kContent.innerHTML = Object.entries(kLabels).map(([k,lbl])=>{ const val = Math.round(scores[k]||0); const col = val>=85?'var(--a2)':val>=70?'var(--acc)':val>=55?'var(--a4)':'var(--danger)'; return `
${lbl}
${val}
${d.band||''}
`; }).join(''); kPanel.style.display='block'; } } catch(e){ /* sessiz hata */ } } /* ──────────────────────────────────────────────────────── #21 — "Analize Gönder" — sessionStorage → analiz.html ──────────────────────────────────────────────────────── */ function sendToAnaliz(){ if(!zones.length){ showToast('Önce zon çiz'); return; } const payload = buildReportPayloadV9(); try { sessionStorage.setItem('landcrit_zone_import', JSON.stringify(payload)); showToast('Veriler aktarıldı, analiz açılıyor…'); setTimeout(()=>{ window.location.href='/analiz.html'; }, 600); } catch(e){ showToast('sessionStorage hatası: ' + e.message); } } /* ──────────────────────────────────────────────────────── K4 — Zone Plant Assignment (manual) ──────────────────────────────────────────────────────── */ function escHtml(s){ return String(s ?? '') .replace(/&/g,'&') .replace(//g,'>') .replace(/"/g,'"') .replace(/'/g,'''); } function ensureZonePlantArray(zone){ if(!zone) return; if(!Array.isArray(zone.plants)) zone.plants = []; } function addPlantToZone(zoneId, plantId, tr, la){ const z = zones.find(x=>x.id===zoneId); if(!z) return; ensureZonePlantArray(z); const pid = String(plantId || '').trim(); if(!pid){ showToast('Bitki ID bulunamadı'); return; } history.push(JSON.parse(JSON.stringify(zones))); const existing = z.plants.find(p=>p.plantId===pid); if(existing){ existing.qty = Math.max(1, (existing.qty||1) + 1); } else { z.plants.push({ plantId: pid, tr: (tr||'').trim(), la: (la||'').trim(), qty: 1 }); } markDirty({persist:true, ui:true}); showToast('Bitki eklendi (K4)'); } function removePlantFromZone(zoneId, plantId){ const z = zones.find(x=>x.id===zoneId); if(!z) return; ensureZonePlantArray(z); history.push(JSON.parse(JSON.stringify(zones))); z.plants = z.plants.filter(p=>p.plantId!==plantId); markDirty({persist:true, ui:true}); showToast('Bitki kaldırıldı'); openPanel(zoneId); } function changePlantQty(zoneId, plantId, delta){ const z = zones.find(x=>x.id===zoneId); if(!z) return; ensureZonePlantArray(z); const p = z.plants.find(x=>x.plantId===plantId); if(!p) return; history.push(JSON.parse(JSON.stringify(zones))); p.qty = Math.max(1, (p.qty||1) + (delta||0)); markDirty({persist:true, ui:true}); openPanel(zoneId); } function bindPlantUI(zoneId){ const input = document.getElementById('plantSearchInput'); const results = document.getElementById('plantSearchResults'); if(!input || !results) return; ensurePlantDB(); const render = () => { const q = (input.value || '').trim(); if(!q){ results.innerHTML = `
Arama için yazmaya başla. Örn: lavanta, platanus, juniperus
`; return; } const hits = searchPlants(q, 10); if(!hits.length){ results.innerHTML = `
Sonuç yok. DB yüklemediysen 172 DB'yi yüklemeyi dene.
`; return; } results.innerHTML = hits.map(p=>`
${escHtml(p.tr || '—')}
${escHtml(p.la || '')}
tıkla → ekle
`).join(''); }; input.oninput = render; render(); results.onclick = (ev) => { const item = ev.target.closest('.plant-search-item'); if(!item) return; addPlantToZone(zoneId, item.dataset.id, item.dataset.tr, item.dataset.la); input.value = ''; render(); openPanel(zoneId); }; } function editZoneLabel(id){ const z=zones.find(x=>x.id===id); if(!z) return; const name=prompt('Yeni zon adı:', z.label); if(name&&name.trim()){ history.push(JSON.parse(JSON.stringify(zones))); z.label=name.trim(); buildZoneList(); openPanel(id); markDirty({persist:true}); showToast('Ad güncellendi'); } } /* ═══════════════════════════════════════════════════════ EXPORT / REPORT ═══════════════════════════════════════════════════════ */ function exportJSON(){ if(!zones.length){showToast('Henüz zon yok');return;} const data={ generated: new Date().toISOString(), calibration: { meters_per_px: metersPerPx, snap_enabled: snapEnabled, snap_grid_m: snapGridMeters, snap_radius_px: snapRadiusPx, edge_snap_enabled: edgeSnapEnabled, angle_snap_enabled: angleSnapEnabled, angle_step_deg: angleSnapStepDeg, orthogonal_mode: orthogonalMode, guide_assist_enabled: guideAssistEnabled, active_draw_tool: drawTool }, site: {name:"Proje Adı", source:"DXF/Görsel"}, zones: zones.map(z=>({ id:z.id, label:z.label, typeId:z.typeId, area_m2: Number((z.area||0).toFixed(2)), programs:z.programs, ana_tip:z.ana_tip, safety:z.safety, score:z.score, plants: Array.isArray(z.plants) ? z.plants : [], points_px: z.pts.map(p=>({x:Number(p.x.toFixed(2)), y:Number(p.y.toFixed(2))})), conflicts: detectConflicts(z).map(c=>({severity:c.sev,note:c.note,with:c.withLabel})), })), stats:{ total_zones:zones.length, total_area_m2: Number(zones.reduce((a,z)=>a+(z.area||0),0).toFixed(2)), conflict_count: [...new Set(zones.flatMap(z=>detectConflicts(z)).map(c=>c.pair))].length, }, report_payload_hint: { export: 'downloadReportPayloadV9()', schema_version: 'landcrit-report-payload-v1' } }; downloadBlob(JSON.stringify(data,null,2), 'landcrit-zonlar.json', 'application/json'); showToast('JSON indirildi'); } /* LandCrit v9 Faz A prune: removed duplicate definition (quickPrint) */ function goToReport(){ if(!zones.length){showToast('Önce zon çiz');return;} const payload = buildReportPayloadV9(); const fileBase = (payload.project.name || 'landcrit-zon-raporu').replace(/[^a-z0-9-_çğıöşü]/gi,'-').replace(/-+/g,'-').replace(/^-|-$/g,'') || 'landcrit-zon-raporu'; downloadBlob(generateReportHTML(), `${fileBase}.html`, 'text/html'); showToast('Rapor indirildi'); } /* ═══════════════════════════════════════════════════════ TOAST ═══════════════════════════════════════════════════════ */ let toastTimer; function showToast(msg){ const el=document.getElementById('toast');el.textContent=msg; el.classList.add('show'); clearTimeout(toastTimer); toastTimer=setTimeout(()=>el.classList.remove('show'),2200); } /* INIT (deferred to end-of-file boot script) */ window.__landcritDeferredBoot = true; /* ═════════════════════════ VERTEX EDIT + BUFFER + SPLINE ══════════════════════════ */ function toggleVertexEdit(){ vertexEditMode = !vertexEditMode; selectedVertex = null; draggedVertex = null; vertexDragSnapshot = null; const btn = document.getElementById('vertexEditBtn'); if(btn) btn.classList.toggle('btn-acc', vertexEditMode); showToast(`Köşe düzenleme ${vertexEditMode ? 'açıldı' : 'kapatıldı'}`); markDirty({ui:true}); } /* LandCrit v9 Faz A prune: removed duplicate definition (toggleForceClose) */ function hitTestVertex(pt, zone){ const r = Math.max(10/scale, 8/scale); for(let i=0;i 0; const sign = ccw ? 1 : -1; const distOff = d * sign; function lineFrom(a,b){ const vx = b.x-a.x, vy = b.y-a.y; const len = Math.hypot(vx,vy) || 1; // left normal for CCW const nx = -vy/len, ny = vx/len; // shift line by distOff along normal const a2 = { x: a.x + nx*distOff, y: a.y + ny*distOff }; const b2 = { x: b.x + nx*distOff, y: b.y + ny*distOff }; return { a:a2, b:b2 }; } const shifted = []; for(let i=0;i= pts.length) return pts[pts.length-1]; return pts[i]; } for(let i=0;i0.2/scale){ out.push({x,y}); } } } // If user wants closed, caller can set last = first return out; } // init buttons state from defaults (function initAdvancedUI(){ const vBtn = document.getElementById('vertexEditBtn'); if(vBtn) vBtn.classList.toggle('btn-acc', vertexEditMode); const cBtn = document.getElementById('closeWarnBtn'); if(cBtn) cBtn.classList.toggle('btn-acc', forceClose); })(); /* ═══════════════════════════════════════════════════════ V7 — TOOL PERSISTENCE + PROJECT SAVE + VISUAL SELECT + HELP ═══════════════════════════════════════════════════════ */ var bgMeta = typeof bgMeta !== 'undefined' ? bgMeta : null; var selectedVisual = typeof selectedVisual !== 'undefined' ? selectedVisual : null; // currently only: 'bg' var dragVisual = typeof dragVisual !== 'undefined' ? dragVisual : false; var visualDragStartWorld = typeof visualDragStartWorld !== 'undefined' ? visualDragStartWorld : null; var visualDragSnapshot = typeof visualDragSnapshot !== 'undefined' ? visualDragSnapshot : null; var selectPriority = (typeof selectPriority !== 'undefined' && ['auto','zone','image'].includes(selectPriority)) ? selectPriority : 'auto'; // auto | zone | image var helpOpen = typeof helpOpen !== 'undefined' ? helpOpen : false; var splinePreviewCurve = typeof splinePreviewCurve !== 'undefined' ? splinePreviewCurve : null; var activeProjectName = (typeof activeProjectName !== 'undefined' && activeProjectName) ? activeProjectName : 'landcrit-proje'; var PROJECT_EXT = '.landcritproject.json'; function installV7UI(){ injectV7Styles(); injectNavButtons(); injectDrawSectionControls(); injectHelpModal(); updateSelectPriorityUI(); updateVisualSelectionUI(); } function injectV7Styles(){ if(document.getElementById('v7EnhancementStyles')) return; const style = document.createElement('style'); style.id = 'v7EnhancementStyles'; style.textContent = ` .help-modal{position:fixed;inset:0;background:rgba(5,5,4,.76);display:none;align-items:center;justify-content:center;z-index:120} .help-modal.open{display:flex} .help-card{width:min(720px,92vw);max-height:84vh;overflow:auto;background:var(--s1);border:1px solid rgba(200,240,64,.35);box-shadow:0 24px 80px rgba(0,0,0,.45);padding:18px 18px 14px;border-radius:14px} .help-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin-top:10px} .help-item{border:1px solid var(--border);background:var(--s2);padding:10px;border-radius:10px} .help-key{font-family:var(--mono);font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--acc);margin-bottom:6px} .help-text{font-size:13px;color:var(--muted);line-height:1.5} .visual-chip{border-left-color:var(--a3)!important} .tool-extra-row{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:8px} `; document.head.appendChild(style); } function injectNavButtons(){ const navRight = document.querySelector('.nav-right'); if(!navRight || document.getElementById('saveProjectBtn')) return; navRight.insertAdjacentHTML('afterbegin', ` `); const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json,.landcritproject,.landcritproject.json'; fileInput.id = 'projectFileInput'; fileInput.style.display = 'none'; fileInput.addEventListener('change', loadProjectFile); document.body.appendChild(fileInput); } function injectDrawSectionControls(){ const drawSection = document.getElementById('toolPolygonBtn')?.closest('.sb-section'); if(!drawSection || document.getElementById('selectionPriorityBtn')) return; const holder = document.createElement('div'); holder.className = 'tool-extra-row'; holder.innerHTML = ` `; drawSection.appendChild(holder); } function injectHelpModal(){ if(document.getElementById('helpModal')) return; const modal = document.createElement('div'); modal.id = 'helpModal'; modal.className = 'help-modal'; modal.innerHTML = `
/ Yardım & Kısayollar
Çizim Kontrolleri
Araç değiştirirken aktif çizim korunur. Proje dosyası ile zonları ve plan görselini tekrar açabilirsin.
1 / 2 / 3 / 4
Poligon, Freehand, Arc ve Spline araçları arasında geçiş yapar. Çizim kaybolmaz.
A / O / E / G
Açı snap, Ortho, Edge snap ve kenar kılavuzunu aç/kapat.
V
Seçim önceliğini döndürür: Auto → Zon → Görsel. Plan görselini sürüklemek için Görsel modunu kullan.
H
Yardım penceresini açar/kapatır.
Shift + Kenar
Köşe düzenleme modunda yeni vertex ekler. Delete seçili köşeyi siler.
Esc / Ctrl+Z
Aktif çizimi iptal eder. Son zon düzenlemesini geri alır.
Spline kullanımı: mevcut ankordan sonra iki kontrol noktası ve bir bitiş noktası vererek eğri segment üretir. Başlangıç noktasına yaklaşırsan alan kapanır.
Proje dosyası: zonlar, görünüm, aktif araçlar, kapanmamış taslak çizim ve yüklediğin plan görseli tek JSON proje dosyasında saklanır.
`; modal.addEventListener('click', (e)=>{ if(e.target === modal) closeHelpModal(); }); document.body.appendChild(modal); } function toggleHelpModal(){ helpOpen ? closeHelpModal() : openHelpModal(); } function openHelpModal(){ const modal = document.getElementById('helpModal'); if(!modal) return; helpOpen = true; modal.classList.add('open'); } function closeHelpModal(){ const modal = document.getElementById('helpModal'); if(!modal) return; helpOpen = false; modal.classList.remove('open'); } function updateSelectPriorityUI(){ const btn = document.getElementById('selectionPriorityBtn'); const hintBtn = document.getElementById('visualSelectHintBtn'); if(btn){ const map = { auto:'Auto', zone:'Zon', image:'Görsel' }; btn.textContent = `⇄ Seçim: ${map[selectPriority] || 'Auto'}`; btn.classList.toggle('btn-acc', selectPriority !== 'auto'); } if(hintBtn){ hintBtn.classList.toggle('btn-acc', selectPriority === 'image'); } } function activateImageSelectionMode(){ if(drawMode) toggleMode(); if(selectPriority !== 'image') cycleSelectPriority('image'); else updateSelectPriorityUI(); } function cycleSelectPriority(forceMode){ const order = ['auto','zone','image']; if(forceMode && order.includes(forceMode)){ selectPriority = forceMode; }else{ const idx = order.indexOf(selectPriority); selectPriority = order[(idx + 1) % order.length]; } updateSelectPriorityUI(); markDirty({persist:true}); const label = selectPriority === 'auto' ? 'auto' : selectPriority === 'zone' ? 'zon' : 'görsel'; showToast(`Seçim önceliği: ${label}`); } function updateVisualSelectionUI(){ const visualBtn = document.getElementById('visualSelectHintBtn'); if(visualBtn){ const hasVisual = !!(bg && bgMeta); visualBtn.disabled = !hasVisual; visualBtn.style.opacity = hasVisual ? '1' : '.55'; visualBtn.title = hasVisual ? 'Görsel seçim önceliğini aç' : 'Önce görsel yükle'; } } function selectVisual(id){ selectedVisual = id; selectedZoneId = null; document.getElementById('deleteBtn').style.display = 'inline-block'; closePanel(); buildZoneList(); markDirty(); } function deselectVisual(){ selectedVisual = null; if(!selectedZoneId) document.getElementById('deleteBtn').style.display = 'none'; markDirty(); } function getBackgroundRect(){ if(!bg || !bgMeta) return null; return { x: bgMeta.x || 0, y: bgMeta.y || 0, width: bgMeta.width || bg.width, height: bgMeta.height || bg.height }; } /* LandCrit v9 Faz A prune: removed duplicate definition (hitTestBackgroundImage) */ function drawBackgroundSelection(){ const rect = getBackgroundRect(); if(!rect || selectedVisual !== 'bg') return; ctx.save(); ctx.lineWidth = 1.75 / scale; ctx.strokeStyle = '#40b4f0'; ctx.setLineDash([8/scale, 5/scale]); ctx.strokeRect(rect.x, rect.y, rect.width, rect.height); ctx.setLineDash([]); const r = 4 / scale; const corners = [ {x:rect.x,y:rect.y},{x:rect.x+rect.width,y:rect.y}, {x:rect.x+rect.width,y:rect.y+rect.height},{x:rect.x,y:rect.y+rect.height} ]; ctx.fillStyle = 'rgba(64,180,240,.95)'; corners.forEach(p=>{ ctx.beginPath(); ctx.arc(p.x,p.y,r,0,Math.PI*2); ctx.fill(); }); ctx.restore(); } function updateDrawHintText(){ const hint = document.getElementById('drawHint'); const help = document.getElementById('drawToolHelp'); if(!hint) return; let base = 'Köşeleri tıkla'; let helpText = 'Araç değiştirirken aktif çizim korunur. Poligon: köşe köşe çizim · Freehand: basılı tutarak serbest çizim · Arc: mevcut ankora eğri segment · Spline: iki kontrol + bitiş ile eğri segment.'; if(drawTool === 'freehand'){ base = 'Basılı tut ve serbest çiz'; helpText = `Freehand, mevcut çizime son kaldığı köşeden devam eder. Mouse bırakınca alan kapanır. Grid ${freehandGridSnapEnabled ? 'açık' : 'kapalı'} · Vertex ${freehandVertexSnapEnabled ? 'açık' : 'kapalı'} · Edge ${freehandEdgeSnapEnabled ? 'açık' : 'kapalı'}.`; }else if(drawTool === 'arc'){ base = arcDraft?.end ? 'Bombe noktasını tıkla' : 'Arc bitiş noktasını tıkla'; helpText = 'Arc, mevcut çizimi korur: son noktadan bitiş seç, sonra bombe noktasını ver. Başlangıca yaklaşınca alan kapanır.'; }else if(drawTool === 'spline'){ base = splineDraft && splineDraft.ctrlPts && splineDraft.ctrlPts.length >= 3 ? 'Spline bitiş noktasını tıkla' : '2 kontrol noktası seç'; helpText = 'Spline, aktif çizimi korur: ankordan sonra iki kontrol noktası ve bir bitiş noktası ile eğri segment üretir. Başlangıca yaklaşınca alan kapanır.'; }else if(drawTool === 'circle'){ base = isDrawing ? 'Yarıçapı belirlemek için ikinci noktayı tıkla' : 'Merkezi tıkla'; helpText = 'Circle, merkez ve yarıçap ile kapalı alan üretir. İkinci tıkta daire kapanır ve isim penceresi açılır.'; } const parts = [base]; if(snapEnabled) parts.push('Snap'); if(edgeSnapEnabled) parts.push('Edge'); if(angleSnapEnabled) parts.push(`Açı ${Math.round(angleSnapStepDeg)}°`); if(orthogonalMode) parts.push('Ortho'); if(guideAssistEnabled) parts.push('∥/⟂'); parts.push('Ölçü canlı'); if(!['freehand','circle'].includes(drawTool)) parts.push('Kapatmadan bitmez'); parts.push('ESC iptal'); hint.textContent = parts.join(' — '); if(help) help.textContent = helpText; } function setDrawTool(tool){ if(!['polygon','freehand','arc','spline','circle'].includes(tool) || drawTool === tool) return; if(tool === 'circle' && isDrawing && drawingPts.length > 1){ showToast('Circle için mevcut çizimi önce tamamla veya ESC ile iptal et'); return; } drawTool = tool; if(isDrawing && drawTool === 'spline' && (!splineDraft || !Array.isArray(splineDraft.ctrlPts) || !splineDraft.ctrlPts.length)){ const anchor = drawingPts.length ? drawingPts[drawingPts.length - 1] : null; if(anchor) splineDraft = { ctrlPts:[anchor], previewPts:[] }; } updateDrawToolUI(); updateDrawHintText(); markDirty({persist:true}); const label = tool === 'polygon' ? 'Poligon' : tool === 'freehand' ? 'Freehand' : tool === 'arc' ? 'Arc' : tool === 'spline' ? 'Spline' : 'Circle'; showToast(`${label} aracı seçildi`); } function ensureSplineDraftForPath(){ if(!isDrawing || !drawingPts.length) return; const anchor = drawingPts[drawingPts.length - 1]; if(!splineDraft || !Array.isArray(splineDraft.ctrlPts) || !splineDraft.ctrlPts.length){ splineDraft = { ctrlPts:[anchor], previewPts:[] }; return; } const first = splineDraft.ctrlPts[0]; if(!first || dist(first, anchor) > 0.001){ splineDraft = { ctrlPts:[anchor], previewPts:[] }; } } function commitSplineSegment(endPoint, closes){ ensureSplineDraftForPath(); if(!splineDraft || !drawingPts.length) return false; const anchor = drawingPts[drawingPts.length - 1]; const end = closes ? drawingPts[0] : endPoint; const ctrlPts = [...(splineDraft.ctrlPts || [anchor]), end]; let curve = buildSplinePoints(ctrlPts, 20); if(!Array.isArray(curve) || curve.length < 2){ curve=[anchor, end]; } const toAdd=curve.slice(1); if(toAdd.length){ drawingPts.push(...toAdd); } splinePreviewCurve=null; splineDraft={ ctrlPts:[drawingPts[drawingPts.length - 1]], previewPts:[] }; return true; } /* LandCrit v9 Faz A prune: removed duplicate definition (saveState) */ /* LandCrit v9 Faz A prune: removed duplicate definition (loadState) */ function setBackgroundFromData(src, meta={}, fitView=true){ if(!src){ bg=null; bgMeta=null; updateVisualSelectionUI(); markDirty(); return; } const img=new Image(); img.onload=()=> { bg = img; bgMeta = { src, x: Number.isFinite(meta.x) ? meta.x : 0, y: Number.isFinite(meta.y) ? meta.y : 0, width: Number.isFinite(meta.width) ? meta.width : img.width, height: Number.isFinite(meta.height) ? meta.height : img.height, naturalWidth: img.width, naturalHeight: img.height, label: meta.label || 'Vaziyet Planı' }; document.getElementById('placeholder').style.display = 'none'; if(fitView){ fitImageToCanvas(); }else{ updateVisualSelectionUI(); markDirty({persist:true}); } }; img.src = src; } function loadImage(e){ const f = e.target.files && e.target.files[0]; if(!f) return; activeProjectName = (f.name || 'landcrit-proje').replace(/\.[^.]+$/,'') || activeProjectName; const reader = new FileReader(); reader.onload = ev => { setBackgroundFromData(ev.target.result, { label: f.name || 'Vaziyet Planı' }, true); showToast('Görsel yüklendi'); }; reader.readAsDataURL(f); } function clearBackgroundImage(){ if(!bg && !bgMeta){ showToast('Silinecek referans görsel yok'); return; } if(selectedVisual === 'bg') deselectVisual(); setBackgroundFromData(null); const input = document.getElementById('fileInput'); if(input) input.value = ''; const placeholder = document.getElementById('placeholder'); if(placeholder) placeholder.style.display = zones.length ? 'none' : 'flex'; showToast('Referans görsel silindi'); } function fitImageToCanvas(){ if(!bg || !bgMeta) return; const r = wrap.getBoundingClientRect(); const scaleX = r.width / bgMeta.width; const scaleY = r.height / bgMeta.height; scale = Math.min(scaleX, scaleY) * 0.95; offsetX = (r.width - bgMeta.width * scale) / 2 - (bgMeta.x || 0) * scale; offsetY = (r.height - bgMeta.height * scale) / 2 - (bgMeta.y || 0) * scale; updateVisualSelectionUI(); markDirty({persist:true}); } /* LandCrit v9 Faz A prune: removed duplicate definition (render) */ /* LandCrit v9 Faz A prune: removed duplicate definition (findTopZoneAtPoint) */ function onMouseMove(e){ const r = canvas.getBoundingClientRect(); const cx = e.clientX - r.left; const cy = e.clientY - r.top; mouseX = cx; mouseY = cy; if(isPanning){ offsetX += cx - panStartX; offsetY += cy - panStartY; panStartX = cx; panStartY = cy; markDirty(); return; } const wp = canvasToWorld(cx,cy); if(dragVisual && bgMeta){ const dx = wp.x - visualDragStartWorld.x; const dy = wp.y - visualDragStartWorld.y; bgMeta.x += dx; bgMeta.y += dy; visualDragStartWorld = wp; snapPreview = null; hoverZoneId = null; canvas.style.cursor = 'grabbing'; markDirty({persist:true}); return; } if(draggedVertex){ const z = zones.find(it=>it.id===draggedVertex.zoneId); if(z){ const snapped = getSnappedWorldPoint(wp, { includeDrawing:false, allowFirstPoint:false, excludeZoneId:draggedVertex.zoneId }); z.pts[draggedVertex.index] = snapped.point; snapPreview = snapped.snap; markDirty({ui:true, persist:true}); return; } } if(dragZoneId){ const z = zones.find(item=>item.id===dragZoneId); if(z){ const snapped = getSnappedWorldPoint(wp, { includeDrawing:false, allowFirstPoint:false, excludeZoneId:dragZoneId }); const activePoint = snapped.point; snapPreview = snapped.snap; const dx = activePoint.x - dragStartWorld.x; const dy = activePoint.y - dragStartWorld.y; z.pts = z.pts.map(p=>({x:p.x+dx, y:p.y+dy})); z.area = calcArea(z.pts); dragStartWorld = activePoint; dragMoved = true; hoverZoneId = dragZoneId; if(selectedZoneId === dragZoneId && document.getElementById('rightPanel').classList.contains('open')) openPanel(dragZoneId); markDirty({ui:true}); } return; } hoverZoneId = null; const zoneHit = findTopZoneAtPoint(wp); if(zoneHit) hoverZoneId = zoneHit.id; if(drawMode){ const guided = getInteractiveDrawPoint(wp); snapPreview = guided.snap; if(drawTool === 'spline' && isDrawing && drawingPts.length){ ensureSplineDraftForPath(); const previewCtrl = [...(splineDraft?.ctrlPts || [drawingPts[drawingPts.length-1]]), guided.point]; splinePreviewCurve = previewCtrl.length >= 2 ? buildSplinePoints(previewCtrl, 20) : null; if(splineDraft) splineDraft.previewPts = Array.isArray(splinePreviewCurve) ? splinePreviewCurve : []; }else{ splinePreviewCurve = null; } if(drawTool === 'freehand' && isDrawing && (e.buttons & 1)){ const added = pushFreehandPoint(guided.point); if(added){ snapPreview = guided.snap; } } if(drawTool === 'arc' && isDrawing && arcDraft?.end){ arcPreviewPoints = buildArcPoints(arcDraft.start, arcDraft.end, guided.point, 22); }else if(drawTool !== 'arc'){ arcPreviewPoints = null; if(!guided.constraint) guideReferenceEdge = null; } } else { snapPreview = null; activeConstraint = null; guideReferenceEdge = null; arcPreviewPoints = null; splinePreviewCurve = null; } if(!drawMode){ const imageHit = hitTestBackgroundImage(wp); if((selectPriority === 'image' && imageHit) || (!zoneHit && imageHit && selectPriority !== 'zone')){ canvas.style.cursor = 'pointer'; }else{ canvas.style.cursor = hoverZoneId ? 'pointer' : 'default'; } }else{ canvas.style.cursor = 'crosshair'; } markDirty(); } function onMouseDown(e){ const r=canvas.getBoundingClientRect(); const cx=e.clientX-r.left, cy=e.clientY-r.top; if(e.button===1||(e.button===0&&e.altKey)){ isPanning = true; panStartX=cx; panStartY=cy; canvas.style.cursor='grabbing'; e.preventDefault(); return; } if(drawMode && drawTool === 'freehand' && e.button===0){ const wp = canvasToWorld(cx,cy); const guided = getInteractiveDrawPoint(wp); if(!isDrawing){ isDrawing = true; drawingPts = [guided.point]; }else if(drawingPts.length){ const last = drawingPts[drawingPts.length - 1]; if(dist(last, guided.point) > 0.001) drawingPts.push(guided.point); } snapPreview = guided.snap; document.getElementById('drawHint').classList.remove('hidden'); e.preventDefault(); markDirty(); return; } if(!drawMode && e.button===0){ const wp = canvasToWorld(cx,cy); const hitZone = findTopZoneAtPoint(wp); const imageHit = hitTestBackgroundImage(wp); const shouldPickImage = imageHit && ((selectPriority === 'image') || (!hitZone && selectPriority !== 'zone')); if(shouldPickImage){ selectVisual('bg'); dragVisual = true; visualDragStartWorld = wp; visualDragSnapshot = bgMeta ? { x:bgMeta.x, y:bgMeta.y } : null; canvas.style.cursor = 'grabbing'; e.preventDefault(); return; } } if(!drawMode && vertexEditMode && e.button===0){ const wp = canvasToWorld(cx,cy); const z = zones.find(it=>it.id===selectedZoneId); if(z){ const hitV = hitTestVertex(wp, z); if(hitV){ vertexDragSnapshot = JSON.parse(JSON.stringify(zones)); draggedVertex = { zoneId: z.id, index: hitV.index }; selectedVertex = { zoneId: z.id, index: hitV.index }; canvas.style.cursor = 'grabbing'; e.preventDefault(); markDirty({ui:true}); return; } if(e.shiftKey){ const insInfo = hitTestEdgeForInsert(wp, z); if(insInfo){ history.push(JSON.parse(JSON.stringify(zones))); z.pts.splice(insInfo.insertAt, 0, insInfo.point); selectedVertex = { zoneId: z.id, index: insInfo.insertAt }; showToast('Köşe eklendi'); markDirty({ui:true, persist:true}); e.preventDefault(); return; } } } } if(!drawMode && e.button===0){ const wp = canvasToWorld(cx,cy); const hit = findTopZoneAtPoint(wp); if(hit){ if(selectedZoneId !== hit.id) selectZone(hit.id); dragZoneId = hit.id; dragStartWorld = wp; draggingZoneSnapshot = JSON.parse(JSON.stringify(zones)); dragMoved = false; canvas.style.cursor='grabbing'; e.preventDefault(); } } } function onMouseUp(e){ if(isPanning){ isPanning=false; canvas.style.cursor=drawMode?'crosshair':'default'; saveState(); return; } if(dragVisual){ dragVisual = false; if(visualDragSnapshot && bgMeta && (visualDragSnapshot.x !== bgMeta.x || visualDragSnapshot.y !== bgMeta.y)){ showToast('Görsel taşındı'); saveState(); suppressNextClick = true; } visualDragStartWorld = null; visualDragSnapshot = null; canvas.style.cursor = drawMode ? 'crosshair' : 'default'; markDirty(); return; } if(drawMode && drawTool === 'freehand' && isDrawing){ suppressNextClick = true; if(drawingPts.length >= 3){ if(forceClose){ const closeThresh = Math.max(20/scale, snapRadiusPx/scale); if(dist(drawingPts[0], drawingPts[drawingPts.length-1]) > closeThresh){ drawingPts.push(drawingPts[0]); showToast('Freehand otomatik kapatıldı'); } } finishDrawing(); }else{ cancelDrawing(); } return; } if(draggedVertex){ if(vertexDragSnapshot){ history.push(vertexDragSnapshot); } saveState(); showToast('Köşe taşındı'); suppressNextClick = true; draggedVertex = null; vertexDragSnapshot = null; canvas.style.cursor = drawMode ? 'crosshair' : 'default'; markDirty({ui:true}); return; } if(dragZoneId){ const moved = dragMoved; if(moved && draggingZoneSnapshot){ history.push(draggingZoneSnapshot); saveState(); showToast('Zon taşındı'); suppressNextClick = true; } dragZoneId = null; dragStartWorld = null; draggingZoneSnapshot = null; dragMoved = false; canvas.style.cursor = drawMode ? 'crosshair' : (hoverZoneId?'pointer':'default'); markDirty({ui:true}); } } function onCanvasClick(e){ if(suppressNextClick){ suppressNextClick = false; return; } if(e.button!==0||isPanning||e.altKey||dragZoneId||dragVisual) return; if(drawMode && drawTool === 'freehand') return; const r=canvas.getBoundingClientRect(); const cx=e.clientX-r.left, cy=e.clientY-r.top; const rawPoint = canvasToWorld(cx,cy); if(!drawMode){ const hit = findTopZoneAtPoint(rawPoint); if(hit){ deselectVisual(); selectZone(hit.id); return; } if(hitTestBackgroundImage(rawPoint) && selectPriority !== 'zone'){ selectVisual('bg'); return; } deselectVisual(); deselectZone(); return; } const guided = getInteractiveDrawPoint(rawPoint); const wp = guided.point; snapPreview = guided.snap; if(drawTool === 'arc'){ if(!isDrawing){ isDrawing = true; drawingPts = [wp]; resetArcDraft(); document.getElementById('drawHint').classList.remove('hidden'); updateDrawHintText(); markDirty(); return; } if(arcDraft?.end){ const arcPts = buildArcPoints(arcDraft.start, arcDraft.end, wp, 22); const pointsToAdd = arcPts.length > 1 ? arcPts.slice(1) : [arcDraft.end]; if(pointsToAdd.length){ drawingPts.push(...pointsToAdd); } const closes = !!arcDraft.closes; resetArcDraft(); updateDrawHintText(); if(closes){ snapPreview = { point: drawingPts[0], type:'vertex', label:'Başlangıç noktası', distance:0 }; finishDrawing(); return; } markDirty(); return; } const lastPoint = drawingPts[drawingPts.length - 1]; if(lastPoint && dist(wp, lastPoint) < 0.001) return; const closeThresh=Math.max(20/scale, snapRadiusPx/scale); const closes=drawingPts.length>= 3 && (dist(wp, drawingPts[0]) < closeThresh || dist(rawPoint, drawingPts[0]) < closeThresh || (guided.baseSnap?.type==='vertex' && dist(guided.baseSnap.point, drawingPts[0]) < 0.001)); arcDraft={ start:lastPoint, end: closes ? drawingPts[0] : wp, closes }; arcPreviewPoints=[arcDraft.start, arcDraft.end]; updateDrawHintText(); markDirty(); return; } if(drawTool==='spline' ){ if(!isDrawing){ isDrawing=true; drawingPts=[wp]; splineDraft={ ctrlPts:[wp], previewPts:[] }; splinePreviewCurve=null; document.getElementById('drawHint').classList.remove('hidden'); updateDrawHintText(); markDirty(); return; } ensureSplineDraftForPath(); const closeThresh=Math.max(20/scale, snapRadiusPx/scale); const closes=drawingPts.length>= 3 && (dist(wp, drawingPts[0]) < closeThresh || dist(rawPoint, drawingPts[0]) < closeThresh || (guided.baseSnap?.type==='vertex' && dist(guided.baseSnap.point, drawingPts[0]) < 0.001)); if(closes && splineDraft && splineDraft.ctrlPts && splineDraft.ctrlPts.length>= 2){ commitSplineSegment(drawingPts[0], true); finishDrawing(); return; } if(!splineDraft || !Array.isArray(splineDraft.ctrlPts)){ splineDraft = { ctrlPts:[drawingPts[drawingPts.length - 1]], previewPts:[] }; } if(splineDraft.ctrlPts.length < 3){ splineDraft.ctrlPts.push(wp); updateDrawHintText(); markDirty(); return; } const committed=commitSplineSegment(wp, false); if(committed){ updateDrawHintText(); markDirty(); } return; } if(drawTool==='circle' ){ if(!isDrawing){ isDrawing=true; drawingPts=[wp]; document.getElementById('drawHint').classList.remove('hidden'); updateDrawHintText(); markDirty(); return; } const center=drawingPts[0]; const radius=dist(center, wp); if(radius < Math.max(4/scale, 0.001)){ showToast('Daire yarıçapı için biraz daha uzağa tıkla'); return; } pendingZone={ pts: buildCirclePoints(center, radius) }; isDrawing=false; drawingPts=[]; snapPreview=null; activeConstraint=null; document.getElementById('drawHint').classList.add('hidden'); showZoneNameInput(); markDirty(); return; } if(!isDrawing){ isDrawing=true; drawingPts=[wp]; document.getElementById('drawHint').classList.remove('hidden'); markDirty(); return; } const lastPoint=drawingPts[drawingPts.length-1]; if(lastPoint && dist(wp, lastPoint) < 0.001){ return; } const closeThresh=Math.max(20/scale, snapRadiusPx/scale); const clickedStart=drawingPts.length>=3 && ( dist(wp,drawingPts[0])=3 && dist(drawingPts[0], drawingPts[drawingPts.length-1]) <= closeThresh; if(drawTool==='circle' ) return; if(drawTool==='spline' && splineDraft?.ctrlPts?.length> 1 && !closedNow){ const committed = commitSplineSegment(drawingPts[0], true); if(!committed) return; } if(drawTool === 'arc'){ if(arcDraft?.end) return; if(drawingPts.length < 3) return; if(forceClose && !closedNow){ drawingPts.push({ ...drawingPts[0] }); } e.preventDefault(); finishDrawing(); return; } if(drawingPts.length<3) return; if(forceClose && !closedNow){ drawingPts.push({ ...drawingPts[0] }); } e.preventDefault(); finishDrawing(); } function cancelDrawing(){ isDrawing=false; drawingPts=[]; snapPreview=null; activeConstraint=null; guideReferenceEdge=null; resetArcDraft(); splineDraft=null; splinePreviewCurve=null; document.getElementById('drawHint').classList.add('hidden'); pendingZone=null;hideZoneNameInput(); updateDrawHintText(); markDirty(); } function selectZone(id){ selectedVisual = null; selectedZoneId=id; document.getElementById('deleteBtn').style.display='inline-block'; buildZoneList(); openPanel(id); markDirty(); } function deselectZone(){ selectedZoneId=null; document.getElementById('deleteBtn').style.display='none'; closePanel(); buildZoneList(); markDirty(); } function toggleMode(){ drawMode=!drawMode; if(!drawMode){ snapPreview = null; } document.getElementById('modeSelect').className='nav-tag '+(drawMode?'active-mode':'inactive-mode'); document.getElementById('modeSelect2').className='nav-tag '+(drawMode?'inactive-mode':'active-mode'); canvas.style.cursor=drawMode?'crosshair':'default'; updateDrawToolUI(); updateDrawHintText(); markDirty({persist:true}); } function finishDrawing(){ if(drawingPts.length<3){ cancelDrawing(); return; } if(forceClose){ const closeThresh=Math.max(20/scale, snapRadiusPx/scale); if(dist(drawingPts[0], drawingPts[drawingPts.length-1])> closeThresh){ drawingPts.push({ ...drawingPts[0] }); } } if(selfIntersects(drawingPts)){ showToast('Geçersiz poligon: çizgiler kesişiyor'); return; } isDrawing=false; activeConstraint = null; guideReferenceEdge = null; resetArcDraft(); splineDraft = null; splinePreviewCurve = null; document.getElementById('drawHint').classList.add('hidden'); pendingZone = { pts:[...drawingPts] }; drawingPts=[]; showZoneNameInput(); markDirty(); } function toggleForceClose(){ forceClose = !forceClose; const btn = document.getElementById('closeWarnBtn'); if(btn) btn.classList.toggle('btn-acc', forceClose); markDirty({persist:true}); showToast(`Kapatma zorunlu ${forceClose ? 'açık' : 'kapalı'}`); } /* LandCrit v9 Faz A prune: removed duplicate definition (saveProject) */ function triggerProjectLoad(){ const input = document.getElementById('projectFileInput'); if(!input) return; input.value = ''; input.click(); } function loadProjectFile(e){ const file = e.target.files && e.target.files[0]; if(!file) return; const reader = new FileReader(); reader.onload = ev => { try{ const data = JSON.parse(ev.target.result); applyProjectData(data, file.name || 'landcrit-proje'); }catch(err){ console.error(err); showToast('Proje dosyası okunamadı'); } }; reader.readAsText(file); } /* LandCrit v9 Faz A prune: removed duplicate definition (applyProjectData) */ function onKeyDown(e){ const activeEl = document.activeElement; const typing = !!(activeEl && activeEl.matches('input, textarea')); if(e.key==='Escape'){ if(helpOpen){ closeHelpModal(); return; } cancelDrawing(); closePanel(); deselectVisual(); } if((e.key==='Delete'||e.key==='Backspace') && !typing){ if(vertexEditMode && selectedVertex){ const z = zones.find(it=>it.id===selectedVertex.zoneId); if(z && z.pts.length>3){ history.push(JSON.parse(JSON.stringify(zones))); z.pts.splice(selectedVertex.index,1); selectedVertex = null; saveState(); showToast('Köşe silindi'); e.preventDefault(); return; }else{ showToast('En az 3 köşe olmalı'); e.preventDefault(); return; } } if(selectedZoneId) deleteSelected(); } if(e.key==='z'&&(e.ctrlKey||e.metaKey)){ e.preventDefault(); undoLast(); } if(e.key==='y'&&(e.ctrlKey||e.metaKey)){ e.preventDefault(); redoLast?.(); } if(!typing && e.key==='Enter' && drawMode && isDrawing && !['freehand','circle'].includes(drawTool)){ e.preventDefault(); if(drawTool === 'spline' && splineDraft?.ctrlPts?.length > 1){ const committed = commitSplineSegment(drawingPts[0], true); if(!committed) return; } if(drawingPts.length >= 3){ finishDrawing(); } else { showToast('Alanı kapatmak için en az 3 nokta gerekli'); } return; } if(!typing && !e.ctrlKey && !e.metaKey && !e.altKey){ const key = e.key.toLowerCase(); if(key==='a'){ e.preventDefault(); toggleAngleSnap(); } if(key==='o'){ e.preventDefault(); toggleOrthogonalMode(); } if(key==='e'){ e.preventDefault(); toggleEdgeSnap(); } if(key==='g'){ e.preventDefault(); toggleGuideAssist(); } if(key==='v'){ e.preventDefault(); cycleSelectPriority(); } if(key==='h' || key==='?'){ e.preventDefault(); toggleHelpModal(); } if(key==='1'){ e.preventDefault(); setDrawTool('polygon'); } if(key==='2'){ e.preventDefault(); setDrawTool('freehand'); } if(key==='3'){ e.preventDefault(); setDrawTool('arc'); } if(key==='4'){ e.preventDefault(); setDrawTool('spline'); } if(key==='5'){ e.preventDefault(); setDrawTool('circle'); } } } installV7UI(); `; w.document.open(); w.document.write(html); w.document.close(); showToast('Hızlı çıktı hazır'); } function landcritBaseApplyBuffer_v7(direction){ const z = zones.find(it=>it.id===selectedZoneId); if(!z){ showToast('Önce bir zon seç'); return; } const inp = document.getElementById('bufferMetersInput'); bufferMeters = Math.max(0, parseFloat(inp?.value || '1') || 1); const dPx = (bufferMeters / metersPerPx) * direction; const out = offsetPolygon(z.pts, dPx); if(!out || out.length<3){ showToast('Buffer üretilemedi (karmaşık poligon olabilir)'); return; } if(selfIntersects(out)){ showToast('Buffer sonucu geçersiz (kesişim oluştu)'); return; } const id=`Z${String(zoneCounter++).padStart(2,'0')}`; const label=`${z.label} ${direction> 0?'+':''}${direction*bufferMeters}m`; const zone = { id, label, typeId: z.typeId, pts: out, area: calcArea(out), score: z.score, programs: z.programs||[], ana_tip: z.ana_tip||'—', safety: z.safety||null, }; history.push(JSON.parse(JSON.stringify(zones))); zones.push(zone); saveState(); selectZone(id); markDirty({ui:true, persist:true}); showToast(`Buffer zonu oluşturuldu (${label})`); } function landcritBaseSaveState_v7(){ try{ localStorage.setItem(STORAGE_KEY, JSON.stringify({ zones, zoneCounter, selectedTypeId, metersPerPx, drawTool, forceClose, selectPriority, snap: { enabled:snapEnabled, gridMeters:snapGridMeters, radiusPx:snapRadiusPx, edgeEnabled:edgeSnapEnabled, showCanvasGrid, freehandGridSnapEnabled, freehandVertexSnapEnabled, freehandEdgeSnapEnabled, midpointSnapEnabled }, constraints: { angleEnabled:angleSnapEnabled, angleStepDeg, orthogonalMode, guideAssistEnabled }, view:{scale, offsetX, offsetY} })); }catch(err){ console.warn('State save failed', err); } } function landcritBaseLoadState_v7(){ try{ const raw = localStorage.getItem(STORAGE_KEY); if(!raw) return; const data = JSON.parse(raw); zones = Array.isArray(data.zones) ? data.zones : []; zoneCounter = Number.isFinite(data.zoneCounter) ? data.zoneCounter : 1; selectedTypeId = data.selectedTypeId || selectedTypeId; metersPerPx = Number.isFinite(data.metersPerPx) ? data.metersPerPx : metersPerPx; if(['polygon','freehand','arc','spline','circle'].includes(data.drawTool)) drawTool = data.drawTool; if(typeof data.forceClose === 'boolean') forceClose = data.forceClose; if(['auto','zone','image'].includes(data.selectPriority)) selectPriority = data.selectPriority; if(data.snap){ snapEnabled = typeof data.snap.enabled === 'boolean' ? data.snap.enabled : snapEnabled; snapGridMeters = Number.isFinite(data.snap.gridMeters) ? data.snap.gridMeters : snapGridMeters; snapRadiusPx = Number.isFinite(data.snap.radiusPx) ? data.snap.radiusPx : snapRadiusPx; edgeSnapEnabled = typeof data.snap.edgeEnabled === 'boolean' ? data.snap.edgeEnabled : edgeSnapEnabled; showCanvasGrid = typeof data.snap.showCanvasGrid === 'boolean' ? data.snap.showCanvasGrid : showCanvasGrid; freehandGridSnapEnabled = typeof data.snap.freehandGridSnapEnabled === 'boolean' ? data.snap.freehandGridSnapEnabled : freehandGridSnapEnabled; freehandVertexSnapEnabled = typeof data.snap.freehandVertexSnapEnabled === 'boolean' ? data.snap.freehandVertexSnapEnabled : freehandVertexSnapEnabled; freehandEdgeSnapEnabled = typeof data.snap.freehandEdgeSnapEnabled === 'boolean' ? data.snap.freehandEdgeSnapEnabled : freehandEdgeSnapEnabled; midpointSnapEnabled = typeof data.snap.midpointSnapEnabled === 'boolean' ? data.snap.midpointSnapEnabled : midpointSnapEnabled; } if(data.constraints){ angleSnapEnabled = typeof data.constraints.angleEnabled === 'boolean' ? data.constraints.angleEnabled : angleSnapEnabled; angleSnapStepDeg = Number.isFinite(data.constraints.angleStepDeg) ? data.constraints.angleStepDeg : angleSnapStepDeg; orthogonalMode = typeof data.constraints.orthogonalMode === 'boolean' ? data.constraints.orthogonalMode : orthogonalMode; guideAssistEnabled = typeof data.constraints.guideAssistEnabled === 'boolean' ? data.constraints.guideAssistEnabled : guideAssistEnabled; } if(data.view){ scale = Number.isFinite(data.view.scale) ? data.view.scale : scale; offsetX = Number.isFinite(data.view.offsetX) ? data.view.offsetX : offsetX; offsetY = Number.isFinite(data.view.offsetY) ? data.view.offsetY : offsetY; } zones.forEach(z=>{ z.area = calcArea(z.pts || []); }); if(zones.length){ document.getElementById('placeholder').style.display = 'none'; buildZoneList(); } }catch(err){ console.warn('State load failed', err); } } function landcritBaseApplyProjectData_v7(data, fileName){ activeProjectName = (data.project_name || fileName || 'landcrit-proje').replace(/\.[^.]+$/,''); zones = Array.isArray(data.zones) ? data.zones : []; zones.forEach(z=>{ z.area = calcArea(z.pts || []); }); zoneCounter = zones.reduce((max, z)=>{ const n = parseInt(String(z.id || '').replace(/\D+/g,''), 10); return Number.isFinite(n) ? Math.max(max, n + 1) : max; }, 1); const cal = data.calibration || {}; metersPerPx = Number.isFinite(cal.metersPerPx) ? cal.metersPerPx : metersPerPx; snapEnabled = typeof cal.snapEnabled === 'boolean' ? cal.snapEnabled : snapEnabled; snapGridMeters = Number.isFinite(cal.snapGridMeters) ? cal.snapGridMeters : snapGridMeters; snapRadiusPx = Number.isFinite(cal.snapRadiusPx) ? cal.snapRadiusPx : snapRadiusPx; edgeSnapEnabled = typeof cal.edgeSnapEnabled === 'boolean' ? cal.edgeSnapEnabled : edgeSnapEnabled; angleSnapEnabled = typeof cal.angleSnapEnabled === 'boolean' ? cal.angleSnapEnabled : angleSnapEnabled; angleSnapStepDeg = Number.isFinite(cal.angleSnapStepDeg) ? cal.angleSnapStepDeg : angleSnapStepDeg; orthogonalMode = typeof cal.orthogonalMode === 'boolean' ? cal.orthogonalMode : orthogonalMode; guideAssistEnabled = typeof cal.guideAssistEnabled === 'boolean' ? cal.guideAssistEnabled : guideAssistEnabled; showCanvasGrid = typeof cal.showCanvasGrid === 'boolean' ? cal.showCanvasGrid : showCanvasGrid; freehandGridSnapEnabled = typeof cal.freehandGridSnapEnabled === 'boolean' ? cal.freehandGridSnapEnabled : freehandGridSnapEnabled; freehandVertexSnapEnabled = typeof cal.freehandVertexSnapEnabled === 'boolean' ? cal.freehandVertexSnapEnabled : freehandVertexSnapEnabled; freehandEdgeSnapEnabled = typeof cal.freehandEdgeSnapEnabled === 'boolean' ? cal.freehandEdgeSnapEnabled : freehandEdgeSnapEnabled; midpointSnapEnabled = typeof cal.midpointSnapEnabled === 'boolean' ? cal.midpointSnapEnabled : midpointSnapEnabled; if(['polygon','freehand','arc','spline','circle'].includes(cal.drawTool)) drawTool = cal.drawTool; forceClose = typeof cal.forceClose === 'boolean' ? cal.forceClose : forceClose; if(['auto','zone','image'].includes(cal.selectPriority)) selectPriority = cal.selectPriority; const view = data.view || {}; scale = Number.isFinite(view.scale) ? view.scale : scale; offsetX = Number.isFinite(view.offsetX) ? view.offsetX : offsetX; offsetY = Number.isFinite(view.offsetY) ? view.offsetY : offsetY; const draft = data.draft || {}; isDrawing = !!draft.isDrawing; drawingPts = Array.isArray(draft.drawingPts) ? draft.drawingPts : []; pendingZone = draft.pendingZone || null; splineDraft = draft.splineDraft || null; arcDraft = draft.arcDraft || null; splinePreviewCurve = null; selectedZoneId = null; selectedVisual = null; dragZoneId = null; dragVisual = false; hoverZoneId = null; document.getElementById('placeholder').style.display = (zones.length || (data.image && data.image.src)) ? 'none' : 'flex'; buildZoneList(); updateCalibrationUI(); updateSnapUI(); updateConstraintUI(); updateDrawToolUI(); updateDrawHintText(); updateSelectPriorityUI();if(document.getElementById('closeWarnBtn')) document.getElementById('closeWarnBtn').classList.toggle('btn-acc', forceClose); if(data.image && data.image.src){ setBackgroundFromData(data.image.src, data.image, false); }else{ bg = null; bgMeta = null; updateVisualSelectionUI(); } saveState(); markDirty({ui:true}); } /* ═══════════════════════════════════════════════════════ V8 — LAYERS + TOPOLOGY VALIDATOR + PROJECT META ═══════════════════════════════════════════════════════ */ const __v7Render_v8 = landcritBaseRender_v7; const __v7DrawZone_v8 = landcritBaseDrawZone_v7; const __v7FindTopZoneAtPoint_v8 = landcritBaseFindTopZoneAtPoint_v7; const __v7DeleteSelected_v8 = landcritBaseDeleteSelected_v7; const __v7ApplyBuffer_v8 = landcritBaseApplyBuffer_v7; const __v7UndoLast_v8 = landcritBaseUndoLast_v7; const __v7QuickPrint_v8 = landcritBaseQuickPrint_v7; const __v7ApplyProjectData_v8 = landcritBaseApplyProjectData_v7; const __v7SaveState_v8 = landcritBaseSaveState_v7; const __v7LoadState_v8 = landcritBaseLoadState_v7; const __v7HitTestBackgroundImage_v8 = landcritBaseHitTestBackgroundImage_v7; var projectLayers = (typeof projectLayers !== 'undefined' && Array.isArray(projectLayers) && projectLayers.length) ? projectLayers : null; var activeLayerIdV8 = (typeof activeLayerIdV8 !== 'undefined' && activeLayerIdV8) ? activeLayerIdV8 : null; var projectMetaV8 = (typeof projectMetaV8 !== 'undefined' && projectMetaV8) ? projectMetaV8 : null; var topologyIssues = (typeof topologyIssues !== 'undefined' && Array.isArray(topologyIssues)) ? topologyIssues : []; var topologyMinAreaSqM = (typeof topologyMinAreaSqM === 'number' && Number.isFinite(topologyMinAreaSqM)) ? topologyMinAreaSqM : 1; var v8RenderContext = 'screen'; const V8_META_KEY = STORAGE_KEY + '-v8-meta'; function getDefaultLayersV8(){ return [ { id:'layer-zone-1', name:'Ana Zon', kind:'zone', visible:true, locked:false, printable:true, color:'#c8f040' }, { id:'layer-zone-2', name:'Alternatif Zon', kind:'zone', visible:true, locked:false, printable:true, color:'#40f0a0' }, { id:'layer-image-1', name:'Referans Görsel', kind:'image', visible:true, locked:false, printable:true, color:'#40b4f0' }, { id:'layer-note-1', name:'Ölçü / Not', kind:'annotation', visible:true, locked:false, printable:true, color:'#f0a040' } ]; } function ensureV8Defaults(){ if(!Array.isArray(projectLayers) || !projectLayers.length){ projectLayers = getDefaultLayersV8(); } const defaults = getDefaultLayersV8(); defaults.forEach(def=>{ if(!projectLayers.some(l=>l.id===def.id)) projectLayers.push(def); }); projectLayers = projectLayers.map(l=>({ visible: true, locked: false, printable: true, color: '#c8f040', ...l })); const fallbackZoneLayer = projectLayers.find(l=>l.kind==='zone') || projectLayers[0]; zones.forEach(z=>{ if(!z.layerId || !projectLayers.some(l=>l.id===z.layerId)) z.layerId = fallbackZoneLayer?.id || 'layer-zone-1'; }); if(!activeLayerIdV8 || !projectLayers.some(l=>l.id===activeLayerIdV8 && l.kind==='zone')){ activeLayerIdV8 = (projectLayers.find(l=>l.kind==='zone' && l.visible && !l.locked) || projectLayers.find(l=>l.kind==='zone') || projectLayers[0]).id; } if(!projectMetaV8 || typeof projectMetaV8 !== 'object'){ projectMetaV8 = { name: activeProjectName || 'landcrit-proje', client:'', revision:'R0', site:'' }; } if(!projectMetaV8.name) projectMetaV8.name = activeProjectName || 'landcrit-proje'; activeProjectName = projectMetaV8.name; } function getLayerByIdV8(id){ ensureV8Defaults(); return projectLayers.find(l=>l.id===id) || null; } function getZoneLayerV8(zone){ return getLayerByIdV8(zone?.layerId); } function getImageLayerV8(){ return getLayerByIdV8('layer-image-1'); } function isZoneRenderableV8(zone, opts = {}){ const layer = getZoneLayerV8(zone); if(!layer) return false; if(!layer.visible) return false; if(opts.forPrint && layer.printable === false) return false; return true; } function isImageRenderableV8(opts = {}){ const layer = getImageLayerV8(); if(!bg || !layer) return false; if(!layer.visible) return false; if(opts.forPrint && layer.printable === false) return false; return true; } function isZoneLockedV8(zone){ const layer = getZoneLayerV8(zone); return !!(layer && layer.locked); } function nextZoneLayerForDrawingV8(){ ensureV8Defaults(); return projectLayers.find(l=>l.id===activeLayerIdV8 && l.kind==='zone' && l.visible && !l.locked) || projectLayers.find(l=>l.kind==='zone' && l.visible && !l.locked) || projectLayers.find(l=>l.kind==='zone') || projectLayers[0]; } function installV8UI(){ ensureV8Defaults(); injectV8Styles(); injectProjectMetaSectionV8(); injectLayerSectionV8(); injectTopologySectionV8(); updateProjectMetaUIV8(); rebuildLayerListV8(); updateTopologyPanelV8(); upgradeHelpModalV8(); } function injectV8Styles(){ if(document.getElementById('v8EnhancementStyles')) return; const style = document.createElement('style'); style.id = 'v8EnhancementStyles'; style.textContent = ` .v8-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:6px} .v8-input,.v8-select{width:100%;background:var(--s2);border:1px solid var(--border2);color:var(--text);padding:7px 8px;font-family:var(--mono);font-size:13px;outline:none} .v8-input:focus,.v8-select:focus{border-color:var(--acc)} .layer-list-v8{display:flex;flex-direction:column;gap:6px;margin-top:8px} .layer-row-v8{border:1px solid var(--border);background:var(--s2);padding:7px 8px;border-radius:10px;transition:all .12s} .layer-row-v8.active{border-color:rgba(200,240,64,.45);background:rgba(200,240,64,.05)} .layer-row-v8.hidden{opacity:.58} .layer-head-v8{display:flex;align-items:center;gap:8px} .layer-swatch-v8{width:10px;height:10px;border-radius:3px;flex-shrink:0} .layer-name-v8{font-size:13px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .layer-kind-v8{font-family:var(--mono);font-size:12px;letter-spacing:1px;color:var(--muted);text-transform:uppercase} .layer-actions-v8{display:flex;gap:4px;margin-top:7px} .layer-mini-v8{flex:1;background:none;border:1px solid var(--border2);border-radius:8px;color:var(--muted);cursor:pointer;font-family:var(--mono);font-size:12px;padding:4px 0} .layer-mini-v8.active{border-color:rgba(200,240,64,.45);color:var(--acc);background:rgba(200,240,64,.05)} .layer-mini-v8.warn{border-color:rgba(239,68,68,.35);color:var(--danger)} .issue-list-v8{display:flex;flex-direction:column;gap:6px;margin-top:8px;max-height:200px;overflow:auto} .issue-item-v8{border:1px solid var(--border);background:var(--s2);padding:7px 8px;border-radius:10px;cursor:pointer} .issue-item-v8:hover{border-color:var(--acc)} .issue-item-v8.critical{border-color:rgba(239,68,68,.35);background:rgba(239,68,68,.05)} .issue-item-v8.warning{border-color:rgba(245,158,11,.35);background:rgba(245,158,11,.05)} .issue-head-v8{display:flex;align-items:center;gap:8px;margin-bottom:4px} .issue-sev-v8{font-family:var(--mono);font-size:12px;letter-spacing:1px;text-transform:uppercase;padding:2px 6px;border-radius:999px;border:1px solid currentColor} .issue-title-v8{font-size:13px;color:var(--text);flex:1} .issue-body-v8{font-size:13px;color:var(--muted);line-height:1.5} .v8-chip{display:inline-block;font-family:var(--mono);font-size:12px;letter-spacing:1px;text-transform:uppercase;padding:1px 5px;border-radius:999px;border:1px solid var(--border2);color:var(--muted);margin-left:6px} .zl-layer-v8{display:inline-block;margin-top:2px;font-family:var(--mono);font-size:12px;letter-spacing:1px;text-transform:uppercase;color:var(--muted)} .zl-flags-v8{font-size:13px;color:var(--muted);display:flex;gap:4px;align-items:center} `; document.head.appendChild(style); } function injectProjectMetaSectionV8(){ if(document.getElementById('projectMetaSectionV8')) return; const sidebar = document.querySelector('.sidebar-left'); const firstSection = sidebar?.querySelector('.sb-section'); if(!sidebar || !firstSection) return; const sec = document.createElement('div'); sec.className = 'sb-section'; sec.id = 'projectMetaSectionV8'; sec.dataset.workspaceGroup = 'settings'; sec.innerHTML = `
/ Proje Meta
Kaydet/yükle dosyasına proje adı, müşteri, revizyon ve lokasyon bilgisi eklenir.
`; firstSection.insertAdjacentElement('afterend', sec); ['projectNameV8','projectClientV8','projectRevisionV8','projectSiteV8'].forEach(id=>{ const el = document.getElementById(id); if(el) el.addEventListener('input', syncProjectMetaFromUIV8); }); } function injectLayerSectionV8(){ if(document.getElementById('layerSectionV8')) return; const zoneSection = document.querySelector('.zone-type-list')?.closest('.sb-section'); if(!zoneSection) return; const sec = document.createElement('div'); sec.className = 'sb-section'; sec.id = 'layerSectionV8'; sec.dataset.workspaceGroup = 'layers'; sec.innerHTML = `
/ Katmanlar
Aktif zon katmanı yeni çizimler için kullanılır. Gizli katman çizilmez; kilitli katman düzenlenemez.
`; zoneSection.insertAdjacentElement('afterend', sec); } function injectTopologySectionV8(){ if(document.getElementById('topologySectionV8')) return; const layerSection = document.getElementById('layerSectionV8') || document.querySelector('.zone-type-list')?.closest('.sb-section'); if(!layerSection) return; const sec = document.createElement('div'); sec.className = 'sb-section'; sec.id = 'topologySectionV8'; sec.dataset.workspaceGroup = 'layers'; sec.innerHTML = `
/ Topoloji Doğrula
Min. Alan (m²)
Doğrulama henüz çalıştırılmadı.
`; layerSection.insertAdjacentElement('afterend', sec); const minInput = document.getElementById('topologyMinAreaInputV8'); if(minInput){ minInput.value = String(topologyMinAreaSqM); minInput.addEventListener('change', ()=>{ const n = parseFloat(minInput.value); topologyMinAreaSqM = Number.isFinite(n) && n >= 0 ? n : topologyMinAreaSqM; minInput.value = String(topologyMinAreaSqM); saveState(); }); } } function updateProjectMetaUIV8(){ ensureV8Defaults(); const map = { projectNameV8: projectMetaV8.name || '', projectClientV8: projectMetaV8.client || '', projectRevisionV8: projectMetaV8.revision || '', projectSiteV8: projectMetaV8.site || '' }; Object.entries(map).forEach(([id,val])=>{ const el = document.getElementById(id); if(el && el.value !== val) el.value = val; }); const minInput = document.getElementById('topologyMinAreaInputV8'); if(minInput) minInput.value = String(topologyMinAreaSqM); } function syncProjectMetaFromUIV8(){ ensureV8Defaults(); projectMetaV8.name = (document.getElementById('projectNameV8')?.value || projectMetaV8.name || 'landcrit-proje').trim() || 'landcrit-proje'; projectMetaV8.client = (document.getElementById('projectClientV8')?.value || '').trim(); projectMetaV8.revision = (document.getElementById('projectRevisionV8')?.value || '').trim() || 'R0'; projectMetaV8.site = (document.getElementById('projectSiteV8')?.value || '').trim(); activeProjectName = projectMetaV8.name; saveState(); } function rebuildLayerListV8(){ ensureV8Defaults(); const el = document.getElementById('layerListV8'); if(!el) return; el.innerHTML = projectLayers.map(layer=>{ const isActive = layer.id === activeLayerIdV8; const classes = ['layer-row-v8']; if(isActive) classes.push('active'); if(!layer.visible) classes.push('hidden'); const lockLabel = layer.locked ? 'Kilitli' : 'Açık'; const printLabel = layer.printable === false ? 'No Plot' : 'Plot'; return `
${layer.name}
${layer.kind}
${layer.kind==='zone' ? 'Yeni çizim katmanı seçilebilir' : layer.kind==='image' ? 'Referans görsel bu katmanda' : 'Ölçü / not katmanı (yakında)' } · ${lockLabel} · ${printLabel}
`; }).join(''); } function setActiveLayerV8(id){ ensureV8Defaults(); const layer = getLayerByIdV8(id); if(!layer) return; if(layer.kind !== 'zone'){ showToast('Aktif çizim katmanı yalnızca zon katmanı olabilir'); return; } activeLayerIdV8 = id; rebuildLayerListV8(); saveState(); showToast(`${layer.name} aktif katman oldu`); } function toggleLayerVisibilityV8(id, ev){ ev?.stopPropagation?.(); const layer = getLayerByIdV8(id); if(!layer) return; layer.visible = !layer.visible; if(!layer.visible){ if(layer.kind === 'image' && selectedVisual === 'bg') deselectVisual(); if(selectedZoneId){ const selected = zones.find(z=>z.id===selectedZoneId); if(selected && selected.layerId === id) deselectZone(); } } if(layer.kind === 'zone' && activeLayerIdV8 === id && !layer.visible){ const fallback = nextZoneLayerForDrawingV8(); activeLayerIdV8 = fallback?.id || activeLayerIdV8; } rebuildLayerListV8(); buildZoneList(); validateTopologyV8({quiet:true}); saveState(); markDirty(); } function toggleLayerLockV8(id, ev){ ev?.stopPropagation?.(); const layer = getLayerByIdV8(id); if(!layer) return; layer.locked = !layer.locked; if(layer.kind === 'zone' && activeLayerIdV8 === id && layer.locked){ const fallback = nextZoneLayerForDrawingV8(); if(fallback && !fallback.locked) activeLayerIdV8 = fallback.id; } rebuildLayerListV8(); saveState(); markDirty(); } function toggleLayerPrintV8(id, ev){ ev?.stopPropagation?.(); const layer = getLayerByIdV8(id); if(!layer) return; layer.printable = layer.printable === false ? true : false; rebuildLayerListV8(); saveState(); markDirty(); } function addZoneLayerV8(){ ensureV8Defaults(); const name = (prompt('Yeni katman adı:', `Zon Katmanı ${projectLayers.filter(l=>l.kind==='zone').length + 1}`) || '').trim(); if(!name) return; const id = 'layer-zone-' + Date.now().toString(36); const palette = ['#c8f040','#40f0a0','#40b4f0','#f0a040','#a040f0','#f04080']; const layer = { id, name, kind:'zone', visible:true, locked:false, printable:true, color:palette[projectLayers.length % palette.length] }; projectLayers.unshift(layer); activeLayerIdV8 = id; rebuildLayerListV8(); saveState(); showToast('Yeni katman eklendi'); } function renameActiveLayerV8(){ ensureV8Defaults(); const layer = getLayerByIdV8(activeLayerIdV8); if(!layer || layer.kind !== 'zone') return; const name = (prompt('Katman adı:', layer.name) || '').trim(); if(!name) return; layer.name = name; rebuildLayerListV8(); buildZoneList(); saveState(); } function toggleImageLayerSelectionV8(){ if(drawMode) toggleMode(); if(selectPriority !== 'image') cycleSelectPriority('image'); rebuildLayerListV8(); showToast('Görsel seçimi aktif'); } function moveSelectedZoneToActiveLayerV8(){ const zone = zones.find(z=>z.id===selectedZoneId); const layer = getLayerByIdV8(activeLayerIdV8); if(!zone){ showToast('Önce bir zon seç'); return; } if(!layer || layer.kind !== 'zone'){ showToast('Geçerli bir zon katmanı seç'); return; } const oldLayer = getZoneLayerV8(zone); if(oldLayer?.locked){ showToast('Bu zon kilitli katmanda'); return; } history.push(JSON.parse(JSON.stringify(zones))); zone.layerId = layer.id; buildZoneList(); validateTopologyV8({quiet:true}); saveState(); markDirty(); showToast(`${zone.label} → ${layer.name}`); } function issueSeverityRankV8(sev){ return sev === 'critical' ? 3 : sev === 'warning' ? 2 : 1; } function pointOnSegmentV8(p, a, b, eps = 1e-6){ const cross = (b.x-a.x)*(p.y-a.y) - (b.y-a.y)*(p.x-a.x); if(Math.abs(cross) > eps) return false; const dot = (p.x-a.x)*(p.x-b.x) + (p.y-a.y)*(p.y-b.y); return dot <= eps; } function pointOnPolygonEdgeV8(pt, poly){ if(!poly?.length) return false; for(let i=0;i (p2.x-p1.x)*(p3.y-p1.y) - (p2.y-p1.y)*(p3.x-p1.x); const o1 = orient(a,b,c), o2 = orient(a,b,d), o3 = orient(c,d,a), o4 = orient(c,d,b); const eps = 1e-9; if(Math.abs(o1) < eps || Math.abs(o2) < eps || Math.abs(o3) < eps || Math.abs(o4) < eps){ return false; } return ((o1> 0) !== (o2 > 0)) && ((o3 > 0) !== (o4 > 0)); } function bboxesOverlapV8(a, b){ const ab = bbox(a), bb = bbox(b); return !(ab.maxX < bb.minX || ab.minX> bb.maxX || ab.maxY < bb.minY || ab.minY> bb.maxY); } function polygonsOverlapV8(polyA, polyB){ if(!polyA?.length || !polyB?.length) return false; if(!bboxesOverlapV8(polyA, polyB)) return false; for(let i=0;i p.x < rect.x || p.x> rect.x + rect.width || p.y < rect.y || p.y> rect.y + rect.height); } function validateTopologyV8(options = {}){ ensureV8Defaults(); const quiet = !!options.quiet; const issues = []; const inspectZones = zones.filter(z=>isZoneRenderableV8(z)); inspectZones.forEach(z=>{ if(!z.pts || z.pts.length < 3){ issues.push({ severity:'critical', title:'Geçersiz poligon', message:`${z.label} en az 3 köşe içermiyor.`, zoneIds:[z.id], code:'invalid_polygon' }); return; } if(selfIntersects(z.pts)){ issues.push({ severity:'critical', title:'Kendi kendini kesiyor', message:`${z.label} poligonu kendi içinde kesişiyor.`, zoneIds:[z.id], code:'self_intersection' }); } const area=calcArea(z.pts); z.area=area; if(area < topologyMinAreaSqM){ issues.push({ severity:'warning', title:'Çok küçük alan', message:`${z.label} ${area.toFixed(2).replace('.',',')} m². Min. eşik ${topologyMinAreaSqM.toFixed(1).replace('.',',')} m².`, zoneIds:[z.id], code:'sliver_area' }); } if(bg && bgMeta && zoneOutsideImageV8(z)){ issues.push({ severity:'warning', title:'Altlık dışına taşıyor', message:`${z.label} referans görsel sınırının dışına çıkıyor.`, zoneIds:[z.id], code:'outside_reference' }); } }); for(let i=0;ii.severity==='critical').length; const warning = topologyIssues.filter(i=>i.severity==='warning').length; summary.innerHTML = `${critical} kritik · ${warning} uyarı · haritada kırmızı/sarı highlight gösterilir.`; } } if(list){ if(!topologyIssues.length){ list.innerHTML = `
Sorun yok. İstersen katmanları gizleyip tekrar doğrulayabilirsin.
`; }else{ list.innerHTML = topologyIssues.map((issue, idx)=>{ const sevClass = issue.severity === 'critical' ? 'critical' : 'warning'; const sevColor = issue.severity === 'critical' ? 'var(--danger)' : 'var(--warn)'; return `
${issue.severity === 'critical' ? 'kritik' : 'uyarı'}
${issue.title}
${issue.message}
`; }).join(''); } } } function focusTopologyIssueV8(index){ const issue = topologyIssues[index]; if(!issue) return; const first = issue.zoneIds?.[0]; if(first) selectZone(first); if(issue.zoneIds?.length > 1){ showToast(`${issue.zoneIds.join(' / ')} ilişkisini kontrol et`); } markDirty(); } function zoneIssueSummaryV8(zoneId){ const related = topologyIssues.filter(issue=>issue.zoneIds?.includes(zoneId)); if(!related.length) return null; const top = related.reduce((best, cur)=> issueSeverityRankV8(cur.severity) > issueSeverityRankV8(best.severity) ? cur : best, related[0]); return { count: related.length, severity: top.severity }; } function buildZoneList(){ ensureV8Defaults(); const el = document.getElementById('zoneListEl'); ZONE_TYPES.forEach(t=>{ const cnt = zones.filter(z=>z.typeId===t.id).length; const el2 = document.getElementById('ztc-'+t.id); if(el2) el2.textContent = cnt || ''; }); if(!zones.length){ el.innerHTML = `
Henüz zon çizilmedi
`; document.getElementById('miniStats').style.display = 'none'; return; } el.innerHTML = zones.map(z=>{ const t = ZONE_TYPES.find(x=>x.id===z.typeId); const conflicts = detectConflicts(z); const layer = getZoneLayerV8(z); const topo = zoneIssueSummaryV8(z.id); const hidden = !isZoneRenderableV8(z); const riskColor = topo?.severity === 'critical' ? 'var(--danger)' : topo?.severity === 'warning' ? 'var(--warn)' : (conflicts.some(c=>c.sev==='kritik') ? 'var(--danger)' : conflicts.length ? 'var(--warn)' : 'var(--a2)'); const flags = [hidden ? '🙈' : '', layer?.locked ? '🔒' : '', topo ? '⚠' : ''].filter(Boolean).join(' '); return `
${z.label}${topo ? `${topo.count} issue` : ''}
${Math.round(z.area||0)} m² · ${t?.label||'?'}
${layer?.name || 'Katmansız'}
${flags || ``}
`; }).join(''); const visibleZones = zones.filter(z=>isZoneRenderableV8(z)); const totalArea = visibleZones.reduce((a,z)=>a+(z.area||0),0); const allConflicts = visibleZones.flatMap(z=>detectConflicts(z)); const uniqueConflicts = [...new Set(allConflicts.map(c=>c.pair))]; document.getElementById('msZones').textContent = visibleZones.length; document.getElementById('msArea').textContent = Math.round(totalArea).toLocaleString('tr'); document.getElementById('msConflicts').textContent = Math.max(uniqueConflicts.length, topologyIssues.length); if(visibleZones.length){ const avgScore = Math.round(visibleZones.reduce((a,z)=>a+(z.score||75),0)/visibleZones.length); document.getElementById('msScore').textContent = avgScore; }else{ document.getElementById('msScore').textContent = '—'; } document.getElementById('miniStats').style.display = 'flex'; } function renderTopologyOverlayV8(){ if(!topologyIssues.length) return; const byZone = new Map(); topologyIssues.forEach(issue=>{ issue.zoneIds?.forEach(id=>{ const prev = byZone.get(id); if(!prev || issueSeverityRankV8(issue.severity) > issueSeverityRankV8(prev)) byZone.set(id, issue.severity); }); }); ctx.save(); ctx.setTransform(scale, 0, 0, scale, offsetX, offsetY); byZone.forEach((sev, zoneId)=>{ const zone = zones.find(z=>z.id===zoneId); if(!zone || !isZoneRenderableV8(zone)) return; ctx.beginPath(); zone.pts.forEach((p,i)=> i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)); ctx.closePath(); ctx.lineWidth = (sev === 'critical' ? 3.2 : 2.4) / scale; ctx.strokeStyle = sev === 'critical' ? 'rgba(239,68,68,.95)' : 'rgba(245,158,11,.95)'; ctx.setLineDash(sev === 'critical' ? [10/scale, 6/scale] : [6/scale, 6/scale]); ctx.stroke(); ctx.setLineDash([]); }); ctx.restore(); } function drawZone(z, selected, hovered){ if(!isZoneRenderableV8(z, {forPrint: v8RenderContext === 'print'})) return; __v7DrawZone_v8(z, selected, hovered); } function render(){ ensureV8Defaults(); const oldBg = bg; const oldSelectedVisual = selectedVisual; if(!isImageRenderableV8({forPrint: v8RenderContext === 'print'})){ if(selectedVisual === 'bg') selectedVisual = null; bg = null; } __v7Render_v8(); if(!isImageRenderableV8({forPrint: v8RenderContext === 'print'})){ bg = oldBg; selectedVisual = oldSelectedVisual; } renderTopologyOverlayV8(); } function findTopZoneAtPoint(pt){ for(let i=zones.length-1;i>=0;i--){ const zone = zones[i]; if(!isZoneRenderableV8(zone)) continue; if(!drawMode && isZoneLockedV8(zone)) continue; if(pointInPolygon(pt, zone.pts)) return zone; } return null; } function hitTestBackgroundImage(pt){ const layer = getImageLayerV8(); if(!layer || !layer.visible) return false; if(!drawMode && layer.locked) return false; return __v7HitTestBackgroundImage_v8(pt); } function confirmZone(){ if(!pendingZone) return; ensureV8Defaults(); const targetLayer = nextZoneLayerForDrawingV8(); if(!targetLayer){ showToast('Uygun çizim katmanı yok'); return; } const label = document.getElementById('zoneNameField').value.trim() || ZONE_TYPES.find(x=>x.id===selectedTypeId)?.label || 'Zon'; const t = ZONE_TYPES.find(x=>x.id===selectedTypeId); const id = `Z${String(zoneCounter++).padStart(2,'0')}`; const area = calcArea(pendingZone.pts); const zone = { id, label, typeId: selectedTypeId, pts: pendingZone.pts, area, score: 70 + Math.floor(Math.random()*25), programs: t?.programs||[], ana_tip: t?.ana_tip||'—', safety: t?.safety||null, layerId: targetLayer.id }; history.push(JSON.parse(JSON.stringify(zones))); zones.push(zone); pendingZone = null; hideZoneNameInput(); buildZoneList(); validateTopologyV8({quiet:true}); markDirty({ui:true, persist:true}); selectZone(id); showToast(`${id} — ${label} eklendi · ${targetLayer.name}`); } function deleteSelected(){ const zone = zones.find(z=>z.id===selectedZoneId); if(zone && isZoneLockedV8(zone)){ showToast('Kilitli katmandaki zon silinemez'); return; } __v7DeleteSelected_v8(); validateTopologyV8({quiet:true}); } function applyBuffer(direction){ const zone = zones.find(z=>z.id===selectedZoneId); if(zone && isZoneLockedV8(zone)){ showToast('Kilitli katmandaki zon buffer alamaz'); return; } const beforeCount = zones.length; __v7ApplyBuffer_v8(direction); if(zones.length > beforeCount){ const added = zones[zones.length - 1]; if(added && (!added.layerId || !getLayerByIdV8(added.layerId))){ added.layerId = zone?.layerId || nextZoneLayerForDrawingV8()?.id || 'layer-zone-1'; } } validateTopologyV8({quiet:true}); } function undoLast(){ __v7UndoLast_v8(); ensureV8Defaults(); buildZoneList(); validateTopologyV8({quiet:true}); } function quickPrint(){ if(isDrawing){ showToast('Önce aktif çizimi bitir veya ESC ile iptal et'); return; } ensureV8Defaults(); const printableZones = zones.filter(z=>isZoneRenderableV8(z, {forPrint:true})); if(!printableZones.length){ showToast('Çıktıya girecek görünür/plot katmanı yok'); return; } v8RenderContext = 'print'; render(); v8RenderContext = 'screen'; const totalArea = Math.round(printableZones.reduce((a,z)=>a+(z.area||0),0)); const printableConflicts = [...new Set(printableZones.flatMap(z=>detectConflicts(z)).map(c=>c.pair))].length; const avgScore = printableZones.length ? Math.round(printableZones.reduce((a,z)=>a+(z.score||0),0)/printableZones.length) : 0; const imgData = canvas.toDataURL('image/png'); const w = window.open('', '_blank', 'width=1400,height=900'); if(!w){ showToast('Popup engellendi'); markDirty(); return; } const metaLine = [projectMetaV8.client || '', projectMetaV8.site || '', projectMetaV8.revision || ''] .filter(Boolean) .join(' · '); const html = ` LandCrit Hızlı Çıktı

${projectMetaV8.name || 'LandCrit Zon Çıktısı'}

${new Date().toLocaleString('tr-TR')} · Kalibrasyon: 1 px = ${metersPerPx.toFixed(3).replace(/0+$/,'').replace(/\.$/,'')} m${metaLine ? ` · ${metaLine}` : ''}

Plot Zon
${printableZones.length}
Toplam Alan
${totalArea} m²
Çatışma
${Math.max(printableConflicts, topologyIssues.length)}
Ort. Skor
${avgScore}
LandCrit plan çıktısı
Plot kapalı katmanlar bu çıktıya dahil edilmedi. Tarayıcı çıktı ekranından PDF olarak da kaydedebilirsin.
`; w.document.open(); w.document.write(html); w.document.close(); showToast('Hızlı çıktı hazır'); markDirty(); } function saveProject(){ ensureV8Defaults(); syncProjectMetaFromUIV8(); const project = { version: '8.0', app: 'LandCrit Zon Haritası', saved_at: new Date().toISOString(), project_name: projectMetaV8.name, project_meta: projectMetaV8, layers: projectLayers, active_layer_id: activeLayerIdV8, topology: { minAreaSqM: topologyMinAreaSqM }, calibration: { metersPerPx, snapEnabled, snapGridMeters, snapRadiusPx, edgeSnapEnabled, angleSnapEnabled, angleSnapStepDeg, orthogonalMode, guideAssistEnabled, showCanvasGrid, freehandGridSnapEnabled, freehandVertexSnapEnabled, freehandEdgeSnapEnabled, midpointSnapEnabled, drawTool, forceClose, selectPriority }, view: { scale, offsetX, offsetY }, image: bg && bgMeta ? { src: bgMeta.src, x: bgMeta.x || 0, y: bgMeta.y || 0, width: bgMeta.width || bg.width, height: bgMeta.height || bg.height, label: bgMeta.label || 'Vaziyet Planı' } : null, plant_db_meta: PLANT_DB_META, zones: zones, draft: { isDrawing, drawingPts, pendingZone, splineDraft, arcDraft, drawTool } }; const fileNameBase = (projectMetaV8.name || activeProjectName || 'landcrit-proje').replace(/[^a-z0-9-_çğıöşü]/gi,'-').replace(/-+/g,'-').replace(/^-|-$/g,'') || 'landcrit-proje'; downloadBlob(JSON.stringify(project, null, 2), `${fileNameBase}${PROJECT_EXT}`, 'application/json'); showToast('V8 proje dosyası indirildi'); } function applyProjectData(data, fileName){ __v7ApplyProjectData_v8(data, fileName); projectLayers = Array.isArray(data.layers) && data.layers.length ? data.layers : getDefaultLayersV8(); activeLayerIdV8 = data.active_layer_id || activeLayerIdV8; const meta = data.project_meta || {}; projectMetaV8 = { name: meta.name || data.project_name || activeProjectName || 'landcrit-proje', client: meta.client || '', revision: meta.revision || 'R0', site: meta.site || '' }; if(data.plant_db_meta){ PLANT_DB_META = data.plant_db_meta; updatePlantDbStatus(); } topologyMinAreaSqM = Number.isFinite(data.topology?.minAreaSqM) ? data.topology.minAreaSqM : topologyMinAreaSqM; ensureV8Defaults(); updateProjectMetaUIV8(); rebuildLayerListV8(); validateTopologyV8({quiet:true}); saveState(); markDirty({ui:true}); showToast('V8 proje yüklendi'); } function saveState(){ __v7SaveState_v8(); ensureV8Defaults(); try{ localStorage.setItem(V8_META_KEY, JSON.stringify({ projectLayers, activeLayerIdV8, projectMetaV8, topologyMinAreaSqM, topologyIssues })); }catch(err){ /* ignore */ } } function loadState(){ __v7LoadState_v8(); try{ const raw = localStorage.getItem(V8_META_KEY); if(raw){ const data = JSON.parse(raw); if(Array.isArray(data.projectLayers) && data.projectLayers.length) projectLayers = data.projectLayers; if(data.activeLayerIdV8) activeLayerIdV8 = data.activeLayerIdV8; if(data.projectMetaV8) projectMetaV8 = data.projectMetaV8; if(Number.isFinite(data.topologyMinAreaSqM)) topologyMinAreaSqM = data.topologyMinAreaSqM; if(Array.isArray(data.topologyIssues)) topologyIssues = data.topologyIssues; } }catch(err){ /* ignore */ } ensureV8Defaults(); } function upgradeHelpModalV8(){ const modal = document.getElementById('helpModal'); if(!modal || document.getElementById('helpV8Extra')) return; const card = modal.querySelector('.help-card'); if(!card) return; const extra = document.createElement('div'); extra.id = 'helpV8Extra'; extra.style.cssText = 'margin-top:12px;border-top:1px solid var(--border);padding-top:12px;font-size:13px;color:var(--muted);line-height:1.6'; extra.innerHTML = ` V8 yenilikleri: Katmanları gizle / kilitle / plot dışı bırak. Topoloji paneli ile overlap ve küçük alan uyarılarını gör.
Katman kısayolu: V ile seçim önceliği değişir; aktif çizim katmanı sol panelde seçilir. Görünmeyen katman haritada çizilmez.
Doğrulama: Min. alan eşiğini ayarlayıp ✓ Doğrula ile sorunları listeleyebilir, listedeki maddeye tıklayıp ilgili zonu seçebilirsin. `; card.appendChild(extra); } function markDirtyV8Refresh(){ rebuildLayerListV8(); updateTopologyPanelV8(); buildZoneList(); updateProjectMetaUIV8(); } ensureV8Defaults(); installV8UI(); markDirtyV8Refresh(); validateTopologyV8({quiet:true});