frontend: Diagramm als Bild (PNG) in die Zwischenablage kopieren

Der Diagramm-Kopieren-Button liefert nun ein grafisches Format statt Text:
Das gerenderte Diagramm wird aus der Live-Geometrie in ein eigenständiges
SVG (nur Formen + Text, keine externen Ressourcen) nachgezeichnet und als
PNG in die Zwischenablage gelegt. Knotenfarben, Größen-Badges, Tags und der
Geister-Knoten werden übernommen; Verbindungslinien je Gate neu gezogen
(und = durchgezogen Tinte, oder = gestrichelt Grau) und treffen so in allen
Modi die Knoten. Fallback: SVG-Quelltext, wenn kein Bild-Clipboard verfügbar.

Der bisherige ASCII-Text-Export (treeToText, CODE_BY_KEY) entfällt.
Tooltip-Texte auf "als Bild" aktualisiert, SPEC §9 entsprechend ersetzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-07-21 12:39:56 +02:00
co-authored by Claude Opus 4.8
parent 8398e8ea64
commit 57fb72cbe4
2 changed files with 133 additions and 51 deletions
+13 -8
View File
@@ -134,16 +134,21 @@ ist grau (`#6B7A8C`) — kein Petrol mehr im Diagramm. Der Modus ändert nur die
+ halbe Knotenhöhe). Abzweige zielen auf den **Knoten**, nie auf die Mitte + halbe Knotenhöhe). Abzweige zielen auf den **Knoten**, nie auf die Mitte
des Teilbaums. des Teilbaums.
### Textexport des Diagramms ### Grafikexport des Diagramms
Das Diagramm ist per Icon-Schaltfläche als einfacher, überall einsetzbarer Das Diagramm ist per Icon-Schaltfläche als **Bild** in die Zwischenablage
ASCII-Baum in die Zwischenablage kopierbar. Format: kopierbar (grafisches Format, in Dokumente/Chats einfügbar):
- Eingerückter Baum mit `├─`/`└─` (all-of-Abzweige) bzw. `├◇`/`└◇` - Das gerenderte Diagramm wird aus der Live-Geometrie in ein eigenständiges
(any-of-Alternativen); Fortsetzungsspalten mit `│`. SVG (nur Formen + Text, keine externen Ressourcen) nachgezeichnet und als
- Je Knoten: `[Status] Label (Größe) @tags` — jeder Teil optional, Reihenfolge **PNG** (`image/png`) in die Zwischenablage gelegt. Fällt der Bild-Clipboard
wie hier. Der Status-Code entspricht §4 (`[ ]`, `[x]`, `[~]`, …). aus (fehlende `ClipboardItem`-Unterstützung), wird der **SVG-Quelltext**
kopiert (ebenfalls ein Grafikformat).
- Übernommen werden Knotenfarben (Status §4), Größen-Badge, Tags und der
Geister-Knoten; die Verbindungslinien werden je Gate neu gezogen
(und = durchgezogen Tinte, oder = gestrichelt Grau) und treffen die Knoten
unabhängig vom Darstellungsmodus.
- Es wird genau die **sichtbare** Struktur exportiert (der „verworfene - Es wird genau die **sichtbare** Struktur exportiert (der „verworfene
einblenden“-Filter wirkt auch hier). Mehrere Wurzeln durch Leerzeile getrennt. einblenden“-Filter wirkt auch hier).
## 10. Beispiel (kanonisch) ## 10. Beispiel (kanonisch)
+120 -43
View File
@@ -564,7 +564,7 @@
<span><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="9.5" width="5" height="5" rx="1"/><rect x="16" y="3" width="6" height="4" rx="1"/><rect x="16" y="17" width="6" height="4" rx="1"/><path d="M7.5 12H12M12 5V19M12 5H16M12 19H16"/></svg></span> <span><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="9.5" width="5" height="5" rx="1"/><rect x="16" y="3" width="6" height="4" rx="1"/><rect x="16" y="17" width="6" height="4" rx="1"/><path d="M7.5 12H12M12 5V19M12 5H16M12 19H16"/></svg></span>
</label> </label>
</div> </div>
<button type="button" class="copybtn" id="copyDiagram" data-i18n-title="copyDiagramTooltip" data-i18n-aria="copyDiagramTooltip" title="Diagramm als Textbaum kopieren" aria-label="Diagramm als Textbaum kopieren"> <button type="button" class="copybtn" id="copyDiagram" data-i18n-title="copyDiagramTooltip" data-i18n-aria="copyDiagramTooltip" title="Diagramm als Bild kopieren" aria-label="Diagramm als Bild kopieren">
<svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> <svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg> <svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
</button> </button>
@@ -615,9 +615,6 @@ const STATUS_BY_CODE = {
'-': {key:'verworfen', name:'verworfen'} '-': {key:'verworfen', name:'verworfen'}
}; };
/* Rück-Abbildung Status-Key → Code (für den Text-Export des Diagramms) */
const CODE_BY_KEY = {idee:'?', geplant:' ', arbeit:'~', durchstich:'/', fertig:'x', prod:'^', verworfen:'-'};
const src = document.getElementById('src'); const src = document.getElementById('src');
const out = document.getElementById('out'); const out = document.getElementById('out');
const warnBox = document.getElementById('warn'); const warnBox = document.getElementById('warn');
@@ -738,41 +735,121 @@ function render(){
warnBox.innerHTML = warnings.map(w => `<div>⚠ ${w}</div>`).join(''); warnBox.innerHTML = warnings.map(w => `<div>⚠ ${w}</div>`).join('');
} }
/* ---------- Diagramm als einfacher Text-Baum ---------- */ /* ---------- Diagramm als Grafik (SVG → PNG) ---------- */
/* Einfachstes, aber überall einsetzbares Format: eingerückter ASCII-Baum. /* Das gerenderte Diagramm wird aus der Live-Geometrie in ein eigenständiges
all-of-Abzweige mit ├─/└─, any-of-Alternativen mit ├◇/└◇. Je Knoten: SVG (nur Formen + Text, keine externen Ressourcen) nachgezeichnet und als
[Status] Label (Größe) @tags. Respektiert den „verworfene“-Filter, gibt PNG in die Zwischenablage gelegt. Knotenfarben, Größen-Badges, Tags und
also genau die sichtbare Struktur wieder. */ der Geister-Knoten werden übernommen; Verbindungslinien werden je Gate
function nodeLabelText(n){ (und = durchgezogen Tinte, oder = gestrichelt Grau) neu gezogen und treffen
let s = n.status ? '[' + (CODE_BY_KEY[n.status.key] ?? ' ') + '] ' : ''; so garantiert die Knoten — unabhängig vom Darstellungsmodus. */
s += n.label; function diagramToSvg(){
if(n.size) s += ' (' + n.size + ')'; const treeRect = out.getBoundingClientRect();
if(n.tags && n.tags.length) s += ' ' + n.tags.map(x => '@' + x).join(' '); const PAD = 24;
return s; const W = Math.ceil(treeRect.width) + PAD*2;
const H = Math.ceil(treeRect.height) + PAD*2;
const ox = -treeRect.left + PAD, oy = -treeRect.top + PAD;
const R = el => { const r = el.getBoundingClientRect();
return {x:r.left+ox, y:r.top+oy, w:r.width, h:r.height,
cx:r.left+ox+r.width/2, cy:r.top+oy+r.height/2, r:r.right+ox, b:r.bottom+oy}; };
const parts = [`<rect x="0" y="0" width="${W}" height="${H}" fill="#ffffff"/>`];
const nodes = [...out.querySelectorAll('.node')];
/* 1) Verbindungslinien (hinter den Knoten) */
const seg = (x1,y1,x2,y2,stroke,dash) =>
`<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${stroke}" stroke-width="2"${dash?' stroke-dasharray="5 4"':''} stroke-linecap="round"/>`;
nodes.forEach(parentEl => {
const li = parentEl.closest('li');
const childUl = li && [...li.children].find(c => c.tagName === 'UL');
if(!childUl) return;
const gate = childUl.classList.contains('or') ? 'or' : 'and';
const stroke = gate === 'or' ? '#6B7A8C' : '#41556E';
const dash = gate === 'or';
const kids = [...childUl.children]
.map(cli => cli.querySelector(':scope > .node, :scope > a.node')).filter(Boolean).map(R);
if(!kids.length) return;
const p = R(parentEl);
const avgdx = kids.reduce((s,k)=>s+(k.cx-p.cx),0)/kids.length;
const avgdy = kids.reduce((s,k)=>s+(k.cy-p.cy),0)/kids.length;
if(Math.abs(avgdx) >= Math.abs(avgdy)){ /* links→rechts */
const toRight = avgdx >= 0;
const px = toRight ? p.r : p.x;
const busX = toRight ? Math.min(...kids.map(k=>k.x))-14 : Math.max(...kids.map(k=>k.r))+14;
const ys = kids.map(k=>k.cy).concat(p.cy);
parts.push(seg(px, p.cy, busX, p.cy, stroke, dash));
parts.push(seg(busX, Math.min(...ys), busX, Math.max(...ys), stroke, dash));
kids.forEach(k => parts.push(seg(busX, k.cy, toRight?k.x:k.r, k.cy, stroke, dash)));
} else { /* oben→unten */
const toDown = avgdy >= 0;
const py = toDown ? p.b : p.y;
const busY = toDown ? Math.min(...kids.map(k=>k.y))-14 : Math.max(...kids.map(k=>k.b))+14;
const xs = kids.map(k=>k.cx).concat(p.cx);
parts.push(seg(p.cx, py, p.cx, busY, stroke, dash));
parts.push(seg(Math.min(...xs), busY, Math.max(...xs), busY, stroke, dash));
kids.forEach(k => parts.push(seg(k.cx, busY, k.cx, toDown?k.y:k.b, stroke, dash)));
} }
function walkText(node, prefix, lines){ });
const kids = visibleChildren(node);
const gate = gateOf(kids); /* 2) Badge/Pille (Größe, Tags) */
kids.forEach((k, i) => { const drawBadge = (el, fill, textColor, strokeColor) => {
const last = i === kids.length - 1; const b = R(el);
const conn = gate === 'or' parts.push(`<rect x="${b.x.toFixed(1)}" y="${b.y.toFixed(1)}" width="${b.w.toFixed(1)}" height="${b.h.toFixed(1)}" rx="${Math.min(b.h/2,9).toFixed(1)}" fill="${fill}"${strokeColor?` stroke="${strokeColor}" stroke-width="1.2"`:''}/>`);
? (last ? '└◇ ' : '├◇ ') parts.push(`<text x="${b.cx.toFixed(1)}" y="${(b.cy+3.2).toFixed(1)}" text-anchor="middle" fill="${textColor}" font-size="9" font-family="'IBM Plex Mono',monospace">${esc(el.textContent.trim())}</text>`);
: (last ? '└─ ' : '├─ '); };
lines.push(prefix + conn + nodeLabelText(k));
walkText(k, prefix + (last ? ' ' : '│ '), lines); /* 3) Knoten */
nodes.forEach(node => {
const b = R(node), cs = getComputedStyle(node);
const dashed = cs.borderTopStyle === 'dashed';
parts.push(`<rect x="${b.x.toFixed(1)}" y="${b.y.toFixed(1)}" width="${b.w.toFixed(1)}" height="${b.h.toFixed(1)}" rx="8" fill="${cs.backgroundColor}" stroke="${cs.borderTopColor}" stroke-width="${parseFloat(cs.borderTopWidth)||1.5}"${dashed?' stroke-dasharray="4 3"':''}/>`);
const clone = node.cloneNode(true);
clone.querySelectorAll('.size,.tags,.ext').forEach(e => e.remove());
const label = clone.textContent.replace(/\s+/g,' ').trim();
const deco = cs.textDecorationLine.includes('line-through') ? ' text-decoration="line-through"' : '';
parts.push(`<text x="${b.cx.toFixed(1)}" y="${(b.cy+5).toFixed(1)}" text-anchor="middle" fill="${cs.color}" font-size="14" font-weight="${cs.fontWeight}"${deco}>${esc(label)}</text>`);
const sizeEl = node.querySelector('.size');
if(sizeEl) drawBadge(sizeEl, '#0F766E', '#ffffff');
node.querySelectorAll('.tag').forEach(tg => {
const t = getComputedStyle(tg);
drawBadge(tg, t.backgroundColor, t.color, t.borderTopColor);
});
});
/* 4) Geister-Knoten „Untergliederung fehlt“ */
out.querySelectorAll('.ghost-node').forEach(g => {
const b = R(g);
parts.push(seg(b.cx, b.y-14, b.cx, b.y, '#B45309', true));
parts.push(`<rect x="${b.x.toFixed(1)}" y="${b.y.toFixed(1)}" width="${b.w.toFixed(1)}" height="${b.h.toFixed(1)}" rx="8" fill="rgba(180,83,9,0.06)" stroke="#B45309" stroke-width="1.5" stroke-dasharray="4 3"/>`);
parts.push(`<text x="${b.cx.toFixed(1)}" y="${(b.cy+4).toFixed(1)}" text-anchor="middle" fill="#B45309" font-size="11" font-style="italic">${esc(g.textContent.trim())}</text>`);
});
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family="'IBM Plex Sans',system-ui,sans-serif">${parts.join('')}</svg>`;
return {svg, W, H};
}
function svgToPngBlob(svg, W, H, scale){
return new Promise(resolve => {
const img = new Image();
img.onload = () => {
const c = document.createElement('canvas');
c.width = Math.round(W*scale); c.height = Math.round(H*scale);
const ctx = c.getContext('2d');
ctx.scale(scale, scale);
ctx.drawImage(img, 0, 0);
c.toBlob(b => resolve(b), 'image/png');
};
img.onerror = () => resolve(null);
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
}); });
} }
function treeToText(){ async function copyDiagramImage(){
let {roots} = parse(src.value); const {svg, W, H} = diagramToSvg();
if(!showc.checked){ const blob = await svgToPngBlob(svg, W, H, 2);
roots = roots.filter(r => !r.status || r.status.key !== 'verworfen'); if(blob && navigator.clipboard && window.ClipboardItem){
try{
await navigator.clipboard.write([new ClipboardItem({'image/png': blob})]);
return;
}catch(_){}
} }
const blocks = roots.map(root => { await writeClipboard(svg); /* Fallback: SVG-Quelltext (ebenfalls Grafik) */
const lines = [nodeLabelText(root)];
walkText(root, '', lines);
return lines.join('\n');
});
return blocks.join('\n\n'); /* mehrere Wurzeln durch Leerzeile trennen */
} }
/* Tab-Taste rückt ein statt den Fokus zu wechseln */ /* Tab-Taste rückt ein statt den Fokus zu wechseln */
@@ -937,7 +1014,7 @@ document.getElementById('copy').addEventListener('click', async () => {
flashCopied(document.getElementById('copy'), 'copyTooltip'); flashCopied(document.getElementById('copy'), 'copyTooltip');
}); });
document.getElementById('copyDiagram').addEventListener('click', async () => { document.getElementById('copyDiagram').addEventListener('click', async () => {
await writeClipboard(treeToText()); await copyDiagramImage();
flashCopied(document.getElementById('copyDiagram'), 'copyDiagramTooltip'); flashCopied(document.getElementById('copyDiagram'), 'copyDiagramTooltip');
}); });
@@ -951,7 +1028,7 @@ const I18N = {
brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).", brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).",
editorTitle:"Struktur (Text)", diagramTitle:"Diagramm", editorTitle:"Struktur (Text)", diagramTitle:"Diagramm",
copy:"kopieren", copyDone:"kopiert ✓", copyTooltip:"Text in die Zwischenablage kopieren", copy:"kopieren", copyDone:"kopiert ✓", copyTooltip:"Text in die Zwischenablage kopieren",
copyDiagramTooltip:"Diagramm als Textbaum kopieren", copyDiagramTooltip:"Diagramm als Bild kopieren",
minimize:"minimieren", normal:"normal", maximize:"maximieren", minimize:"minimieren", normal:"normal", maximize:"maximieren",
agenda:"Agenda", discarded:"verworfene", agenda:"Agenda", discarded:"verworfene",
modeHorizontal:"Horizontal Organigramm, Diagramm über dem Editor", modeHorizontal:"Horizontal Organigramm, Diagramm über dem Editor",
@@ -980,7 +1057,7 @@ const I18N = {
brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).", brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).",
editorTitle:"Structure (text)", diagramTitle:"Diagram", editorTitle:"Structure (text)", diagramTitle:"Diagram",
copy:"copy", copyDone:"copied ✓", copyTooltip:"Copy text to clipboard", copy:"copy", copyDone:"copied ✓", copyTooltip:"Copy text to clipboard",
copyDiagramTooltip:"Copy diagram as a text tree", copyDiagramTooltip:"Copy diagram as an image",
minimize:"minimize", normal:"normal", maximize:"maximize", minimize:"minimize", normal:"normal", maximize:"maximize",
agenda:"Legend", discarded:"discarded", agenda:"Legend", discarded:"discarded",
modeHorizontal:"Horizontal org chart, diagram above the editor", modeHorizontal:"Horizontal org chart, diagram above the editor",
@@ -1009,7 +1086,7 @@ const I18N = {
brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo — el árbol de la estructura de desglose del trabajo (EDT).", brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo — el árbol de la estructura de desglose del trabajo (EDT).",
editorTitle:"Estructura (texto)", diagramTitle:"Diagrama", editorTitle:"Estructura (texto)", diagramTitle:"Diagrama",
copy:"copiar", copyDone:"copiado ✓", copyTooltip:"Copiar el texto al portapapeles", copy:"copiar", copyDone:"copiado ✓", copyTooltip:"Copiar el texto al portapapeles",
copyDiagramTooltip:"Copiar el diagrama como árbol de texto", copyDiagramTooltip:"Copiar el diagrama como imagen",
minimize:"minimizar", normal:"normal", maximize:"maximizar", minimize:"minimizar", normal:"normal", maximize:"maximizar",
agenda:"Leyenda", discarded:"descartados", agenda:"Leyenda", discarded:"descartados",
modeHorizontal:"Horizontal organigrama, diagrama sobre el editor", modeHorizontal:"Horizontal organigrama, diagrama sobre el editor",
@@ -1038,7 +1115,7 @@ const I18N = {
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).", brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).",
editorTitle:"Structure (texte)", diagramTitle:"Diagramme", editorTitle:"Structure (texte)", diagramTitle:"Diagramme",
copy:"copier", copyDone:"copié ✓", copyTooltip:"Copier le texte dans le presse-papiers", copy:"copier", copyDone:"copié ✓", copyTooltip:"Copier le texte dans le presse-papiers",
copyDiagramTooltip:"Copier le diagramme en arbre de texte", copyDiagramTooltip:"Copier le diagramme comme image",
minimize:"réduire", normal:"normal", maximize:"agrandir", minimize:"réduire", normal:"normal", maximize:"agrandir",
agenda:"Légende", discarded:"abandonnés", agenda:"Légende", discarded:"abandonnés",
modeHorizontal:"Horizontal organigramme, diagramme au-dessus de l'éditeur", modeHorizontal:"Horizontal organigramme, diagramme au-dessus de l'éditeur",
@@ -1067,7 +1144,7 @@ const I18N = {
brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).", brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).",
editorTitle:"Struktura (tekst)", diagramTitle:"Diagram", editorTitle:"Struktura (tekst)", diagramTitle:"Diagram",
copy:"kopiuj", copyDone:"skopiowano ✓", copyTooltip:"Kopiuj tekst do schowka", copy:"kopiuj", copyDone:"skopiowano ✓", copyTooltip:"Kopiuj tekst do schowka",
copyDiagramTooltip:"Kopiuj diagram jako drzewo tekstowe", copyDiagramTooltip:"Kopiuj diagram jako obraz",
minimize:"minimalizuj", normal:"normalny", maximize:"maksymalizuj", minimize:"minimalizuj", normal:"normalny", maximize:"maksymalizuj",
agenda:"Legenda", discarded:"odrzucone", agenda:"Legenda", discarded:"odrzucone",
modeHorizontal:"Poziomo schemat organizacyjny, diagram nad edytorem", modeHorizontal:"Poziomo schemat organizacyjny, diagram nad edytorem",
@@ -1096,7 +1173,7 @@ const I18N = {
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).", brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
editorTitle:"Структура (текст)", diagramTitle:"Диаграмма", editorTitle:"Структура (текст)", diagramTitle:"Диаграмма",
copy:"копировать", copyDone:"скопировано ✓", copyTooltip:"Скопировать текст в буфер обмена", copy:"копировать", copyDone:"скопировано ✓", copyTooltip:"Скопировать текст в буфер обмена",
copyDiagramTooltip:"Скопировать диаграмму как текстовое дерево", copyDiagramTooltip:"Скопировать диаграмму как изображение",
minimize:"свернуть", normal:"обычный", maximize:"развернуть", minimize:"свернуть", normal:"обычный", maximize:"развернуть",
agenda:"Легенда", discarded:"отклонённые", agenda:"Легенда", discarded:"отклонённые",
modeHorizontal:"Горизонтально – оргсхема, диаграмма над редактором", modeHorizontal:"Горизонтально – оргсхема, диаграмма над редактором",
@@ -1125,7 +1202,7 @@ const I18N = {
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।", brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख", editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख",
copy:"कॉपी करें", copyDone:"कॉपी हो गया ✓", copyTooltip:"टेक्स्ट को क्लिपबोर्ड पर कॉपी करें", copy:"कॉपी करें", copyDone:"कॉपी हो गया ✓", copyTooltip:"टेक्स्ट को क्लिपबोर्ड पर कॉपी करें",
copyDiagramTooltip:"आरेख को टेक्स्ट-ट्री के रूप में कॉपी करें", copyDiagramTooltip:"आरेख को छवि के रूप में कॉपी करें",
minimize:"छोटा करें", normal:"सामान्य", maximize:"बड़ा करें", minimize:"छोटा करें", normal:"सामान्य", maximize:"बड़ा करें",
agenda:"लेजेंड", discarded:"अस्वीकृत", agenda:"लेजेंड", discarded:"अस्वीकृत",
modeHorizontal:"क्षैतिज – संगठन-चार्ट, संपादक के ऊपर आरेख", modeHorizontal:"क्षैतिज – संगठन-चार्ट, संपादक के ऊपर आरेख",