Gozintograph: Unterschied zwischen den Versionen

Markierung: Zurückgesetzt
Markierung: Manuelle Zurücksetzung
Zeile 26: Zeile 26:
<html>
<html>
<style>
<style>
    #gozinto_svg {
  .gozinto-wrap { width:95vw; max-width:1000px; height:70vw; max-height:600px; border:0px solid #ccc; }
        width: 100%;
  svg { width:100%; height:100%; touch-action:none; user-select:none; }
        height: 520px;
  .node-rect { fill:#3498db; stroke:#1f4e78; stroke-width:2; cursor:grab; }
        border: 1px solid #ccc;
  .node-text { font-family: sans-serif; font-size:13px; fill:#000; pointer-events:none; }
        margin-top: 0;
  .edge-line { stroke:#000; stroke-width:2; fill:none; }
        padding-top: 0;
  .edge-arrow { fill:#000; }
        display: block;
  .count-circle { fill:#fff; stroke:#000; stroke-width:1.5; }
    }
  .count-text { font-family:sans-serif; font-size:12px; text-anchor:middle; dominant-baseline:middle; pointer-events:none; }
 
    .draggable {
        cursor: move;
    }
 
    text {
        user-select: none;
        font-family: Arial, sans-serif;
    }
</style>
</style>


<svg id="gozinto_svg"></svg>
<div class="gozinto-wrap">
<svg id="gozinto_svg" viewBox="0 0 1400 700" preserveAspectRatio="xMidYMid meet">
  <!-- edges will be inserted dynamically -->
</svg>
</div>


<script>
<script>
// ====================================================================
(function(){
// Hilfsfunktionen
  const svg = document.getElementById('gozinto_svg');
// ====================================================================
 
function createRect(svg, x, y, w, h, text) {
  // Layout scale (convert logical coords to svg px)
    const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
  const scale = 100; // 1 unit = 100px
    g.classList.add("draggable");
  const yOffset = 50;
    g.dataset.type = "rect";


     const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
  // Utility: create SVG element
     rect.setAttribute("x", x);
  function svgEl(name, attrs){
    rect.setAttribute("y", y);
     const el = document.createElementNS('http://www.w3.org/2000/svg', name);
    rect.setAttribute("width", w);
     for(const k in (attrs||{})) el.setAttribute(k, attrs[k]);
    rect.setAttribute("height", h);
     return el;
     rect.setAttribute("rx", 6);
  }
    rect.setAttribute("fill", "#3498db");
    rect.setAttribute("stroke", "#1f4e78");
    rect.setAttribute("stroke-width", 2);
    rect.dataset.role = "body";


     const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
  // Node factory: creates a rect group with fixed size but movable
     label.setAttribute("x", x + w / 2);
  function createNode(id, cx, cy, w, h, label){
    label.setAttribute("y", y + h / 2 + 4);
     const g = svgEl('g', {class:'node', 'data-id':id});
     label.setAttribute("text-anchor", "middle");
     const rect = svgEl('rect', {
    label.setAttribute("fill", "black");
      class:'node-rect',
    label.setAttribute("font-size", "14");
      x: (cx - w/2)*scale, y: (cy - h/2)*scale + yOffset,
     label.textContent = text;
      width: w*scale, height: h*scale,
      rx:6, ry:6
    });
     const text = svgEl('text', {class:'node-text', x: cx*scale, y: cy*scale + yOffset, 'text-anchor':'middle', 'dominant-baseline':'middle'});
     text.textContent = label;


     g.appendChild(rect);
     g.appendChild(rect);
     g.appendChild(label);
     g.appendChild(text);
     svg.appendChild(g);
     svg.appendChild(g);


     return g;
     // state
}
    const node = {
      id, cx, cy, w, h, g, rect, text
    };


function createCircle(svg, x, y, labelText) {
     // dragging
     const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
     let dragging = false;
     g.classList.add("draggable");
     let start = null;
     g.dataset.type = "circle";


     const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
     rect.addEventListener('pointerdown', function(e){
    circle.setAttribute("cx", x);
      rect.setPointerCapture(e.pointerId);
     circle.setAttribute("cy", y);
      dragging = true;
     circle.setAttribute("r", 12); // KLEINERER KREIS
      start = {x:e.clientX, y:e.clientY, cx:node.cx, cy:node.cy};
    circle.setAttribute("fill", "white");
     });
    circle.setAttribute("stroke", "black");
     rect.addEventListener('pointermove', function(e){
    circle.setAttribute("stroke-width", 2);
      if(!dragging) return;
    circle.dataset.role = "body";
      const dx = (e.clientX - start.x)/scale;
 
      const dy = (e.clientY - start.y)/scale;
    const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
      node.cx = start.cx + dx;
    label.setAttribute("x", x);
      node.cy = start.cy + dy;
     label.setAttribute("y", y + 4);
      updateNode(node);
     label.setAttribute("text-anchor", "middle");
      updateAllEdges();
    label.setAttribute("fill", "black");
     });
     label.setAttribute("font-size", "10");
     rect.addEventListener('pointerup', function(e){
     label.textContent = labelText;
      dragging = false;
      rect.releasePointerCapture(e.pointerId);
     });
     rect.addEventListener('pointercancel', function(e){ dragging=false; });


     g.appendChild(circle);
     return node;
    g.appendChild(label);
  }
    svg.appendChild(g);


     return g;
  function updateNode(node){
}
    node.rect.setAttribute('x', (node.cx - node.w/2)*scale);
    node.rect.setAttribute('y', (node.cy - node.h/2)*scale + yOffset);
    node.text.setAttribute('x', node.cx*scale);
     node.text.setAttribute('y', node.cy*scale + yOffset);
  }


function createLine(svg, from, to, arrowEnd) {
  // compute intersection of ray center->target with rectangle border (axis-aligned)
     const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
  function intersectRectBorder(node, targetX, targetY){
     line.setAttribute("stroke", "black");
     const cx = node.cx, cy = node.cy;
     line.setAttribute("stroke-width", 2);
     const dx = targetX - cx, dy = targetY - cy;
 
     const xMin = node.cx - node.w/2, xMax = node.cx + node.w/2;
     line.dataset.from = from;
     const yMin = node.cy - node.h/2, yMax = node.cy + node.h/2;
    line.dataset.to = to;
     let tCandidates = [];
     line.dataset.arrow = arrowEnd ? "1" : "0";
     if(Math.abs(dx) > 1e-9){
 
      let t1 = (xMin - cx)/dx;
     svg.appendChild(line);
      let y1 = cy + t1*dy;
    updateLine(line);
      if(t1>0 && y1>=yMin-1e-9 && y1<=yMax+1e-9) tCandidates.push({t:t1,x:xMin,y:y1});
    return line;
      let t2 = (xMax - cx)/dx;
}
      let y2 = cy + t2*dy;
 
      if(t2>0 && y2>=yMin-1e-9 && y2<=yMax+1e-9) tCandidates.push({t:t2,x:xMax,y:y2});
function centerOf(element) {
    const body = element.querySelector("[data-role='body']");
    if (body.tagName === "rect") {
        return {
            x: parseFloat(body.getAttribute("x")) + parseFloat(body.getAttribute("width")) / 2,
            y: parseFloat(body.getAttribute("y")) + parseFloat(body.getAttribute("height")) / 2
        };
     }
     }
     if (body.tagName === "circle") {
     if(Math.abs(dy) > 1e-9){
        return {
      let t3 = (yMin - cy)/dy;
            x: parseFloat(body.getAttribute("cx")),
      let x3 = cx + t3*dx;
            y: parseFloat(body.getAttribute("cy"))
      if(t3>0 && x3>=xMin-1e-9 && x3<=xMax+1e-9) tCandidates.push({t:t3,x:x3,y:yMin});
        };
      let t4 = (yMax - cy)/dy;
      let x4 = cx + t4*dx;
      if(t4>0 && x4>=xMin-1e-9 && x4<=xMax+1e-9) tCandidates.push({t:t4,x:x4,y:yMax});
     }
     }
}
    if(tCandidates.length===0) return {x:cx,y:cy};
    tCandidates.sort((a,b)=>a.t-b.t);
    return {x:tCandidates[0].x, y:tCandidates[0].y};
  }


function updateLine(line) {
  // circle boundary point toward target
     const fromEl = document.getElementById(line.dataset.from);
  function pointOnCircle(cx, cy, R, tx, ty){
     const toEl = document.getElementById(line.dataset.to);
     const dx = tx - cx, dy = ty - cy;
    const d = Math.sqrt(dx*dx + dy*dy);
     if(d < 1e-9) return {x:cx, y:cy};
    return {x: cx + R*dx/d, y: cy + R*dy/d};
  }


     const A = centerOf(fromEl);
  // arrowhead path (triangle) at (x,y) pointing to direction (ux,uy)
     const B = centerOf(toEl);
  function makeArrowHead(x, y, ux, uy, size){
    // perpendicular
    const px = -uy, py = ux;
    const p1x = x, p1y = y;
     const p2x = x - ux*size + px*size*0.5;
    const p2y = y - uy*size + py*size*0.5;
    const p3x = x - ux*size - px*size*0.5;
     const p3y = y - uy*size - py*size*0.5;
    return `M ${p1x} ${p1y} L ${p2x} ${p2y} L ${p3x} ${p3y} Z`;
  }


    const dx = B.x - A.x;
  // Edge structure: {fromNode, toNode, amount, circle, lineA, lineB, arrow}
    const dy = B.y - A.y;
  const edges = [];
    const len = Math.sqrt(dx * dx + dy * dy);


     const ux = dx / len;
  function makeConnection(fromNode, toNode, amount, yMid, xOffset){
     const uy = dy / len;
    const group = svgEl('g', {});
     const circle = svgEl('circle', {class:'count-circle'});
    const text = svgEl('text', {class:'count-text'});
    const lineA = svgEl('path', {class:'edge-line', fill:'none'}); // from rect -> circle (path to allow potential future styling)
    const lineB = svgEl('path', {class:'edge-line', fill:'none'});
     const arrow = svgEl('path', {class:'edge-arrow'});
    group.appendChild(lineA);
    group.appendChild(lineB);
    group.appendChild(circle);
    group.appendChild(text);
    group.appendChild(arrow);
    svg.appendChild(group);


     const offsetA = 25; // Abstand vom Rechteck/Kreis
     const e = {fromNode, toNode, amount, circle, text, lineA, lineB, arrow, yMid, xOffset};
     const offsetB = line.dataset.arrow === "1" ? 25 : 25;
     edges.push(e);
    updateEdge(e);
  }


     line.setAttribute("x1", A.x + ux * offsetA);
  function updateEdge(e){
     line.setAttribute("y1", A.y + uy * offsetA);
    // circle center is midpoint between centers with offset
     line.setAttribute("x2", B.x - ux * offsetB);
     const cx = (e.fromNode.cx + e.toNode.cx)/2 + (e.xOffset||0);
     line.setAttribute("y2", B.y - uy * offsetB);
     const cy = e.yMid;
}
    const R = 0.35;
    // compute in logical coords
    const pFrom = intersectRectBorder(e.fromNode, cx, cy);
    const pTo  = intersectRectBorder(e.toNode, cx, cy);
     const cIn = pointOnCircle(cx, cy, R, pFrom.x, pFrom.y);
     const cOut = pointOnCircle(cx, cy, R, pTo.x, pTo.y);


function enableDragging(svg) {
     // convert to px
     let selected = null;
     function px(p){ return [p.x*scale, p.y*scale + yOffset]; }
     let offsetX = 0, offsetY = 0;
     const pf = px(pFrom), pcIn = px(cIn), pcOut = px(cOut), pt = px(pTo);
 
    svg.addEventListener("mousedown", e => {
        if (!e.target.closest(".draggable")) return;
        selected = e.target.closest(".draggable");
 
        const body = selected.querySelector("[data-role='body']");
        const box = body.getBoundingClientRect();
        offsetX = e.clientX - box.x;
        offsetY = e.clientY - box.y;
 
        e.preventDefault();
    });
 
     window.addEventListener("mousemove", e => {
        if (!selected) return;
 
        const body = selected.querySelector("[data-role='body']");
 
        if (body.tagName === "rect") {
            const w = parseFloat(body.getAttribute("width"));
            const h = parseFloat(body.getAttribute("height"));
            const newX = e.clientX - offsetX;
            const newY = e.clientY - offsetY;
 
            body.setAttribute("x", newX);
            body.setAttribute("y", newY);
 
            const text = selected.querySelector("text");
            text.setAttribute("x", newX + w / 2);
            text.setAttribute("y", newY + h / 2 + 4);
        }
 
        if (body.tagName === "circle") {
            const newX = e.clientX - offsetX + 12;
            const newY = e.clientY - offsetY + 12;
 
            body.setAttribute("cx", newX);
            body.setAttribute("cy", newY);
 
            const text = selected.querySelector("text");
            text.setAttribute("x", newX);
            text.setAttribute("y", newY + 4);
        }
 
        document.querySelectorAll("line").forEach(updateLine);
    });


     window.addEventListener("mouseup", () => selected = null);
     // line A: from rect edge -> circle edge (no arrow)
}
    lineAPath = `M ${pf[0]} ${pf[1]} L ${pcIn[0]} ${pcIn[1]}`;
    e.lineA.setAttribute('d', lineAPath);


// ====================================================================
    // line B: circle edge -> rect edge (arrowhead drawn separately)
// Gozintograph aufbauen
    e.lineB.setAttribute('d', `M ${pcOut[0]} ${pcOut[1]} L ${pt[0]} ${pt[1]}`);
// ====================================================================
const svg = document.getElementById("gozinto_svg");


// Einzelteile (oben)
    // circle
const E1 = createRect(svg, 30, 10, 80, 35, "E1");
    e.circle.setAttribute('cx', (cx*scale));
E1.id = "E1";
    e.circle.setAttribute('cy', (cy*scale + yOffset));
const E2 = createRect(svg, 140, 10, 80, 35, "E2");
    e.circle.setAttribute('r', R*scale);
E2.id = "E2";
    e.circle.setAttribute('class','count-circle');
const E3 = createRect(svg, 250, 10, 80, 35, "E3");
E3.id = "E3";
const E4 = createRect(svg, 360, 10, 80, 35, "E4");
E4.id = "E4";


// Bauteile (unten)
    // text
const B1 = createRect(svg, 30, 260, 80, 35, "B1"); B1.id = "B1";
    e.text.setAttribute('x', cx*scale);
const B2 = createRect(svg, 140, 260, 80, 35, "B2"); B2.id = "B2";
    e.text.setAttribute('y', cy*scale + yOffset);
const B3 = createRect(svg, 250, 260, 80, 35, "B3"); B3.id = "B3";
    e.text.textContent = e.amount;
const B4 = createRect(svg, 360, 260, 80, 35, "B4"); B4.id = "B4";
const B5 = createRect(svg, 470, 260, 80, 35, "B5"); B5.id = "B5";


// Verbindungen (mit Kreisen)
    // arrow head: compute unit vector from cOut -> pTo
function connect(E, B, amount) {
    var ux = (pt[0]-pcOut[0]), uy = (pt[1]-pcOut[1]);
     const eC = centerOf(E);
    var L = Math.sqrt(ux*ux + uy*uy);
     const bC = centerOf(B);
    if(L<1e-6) L=1;
    ux/=L; uy/=L;
    const arrowSize = 12;
    const arrowPath = makeArrowHead(pt[0], pt[1], ux, uy, arrowSize);
     e.arrow.setAttribute('d', arrowPath);
     e.arrow.setAttribute('class','edge-arrow');
  }


    const midY = (eC.y + bC.y) / 2;
  // create nodes (logical coords)
    const midX = (eC.x + bC.x) / 2;
  const nodes = {};
  nodes.E1 = createNode('E1', 0, 6.8, 1.0, 0.6, 'E1');
  nodes.E2 = createNode('E2', 2.5, 6.8, 1.0, 0.6, 'E2');
  nodes.E3 = createNode('E3', 5, 6.8, 1.0, 0.6, 'E3');
  nodes.E4 = createNode('E4', 7.5, 6.8, 1.0, 0.6, 'E4');


    const circle = createCircle(svg, midX, midY, amount);
  nodes.B1 = createNode('B1', 0.75, 3, 1.0, 0.6, 'B1');
    const idC = "C" + Math.random().toString(36).substring(2);
  nodes.B2 = createNode('B2', 2.5, 3, 1.0, 0.6, 'B2');
    circle.id = idC;
  nodes.B3 = createNode('B3', 5, 3, 1.0, 0.6, 'B3');
  nodes.B4 = createNode('B4', 7.5, 3, 1.0, 0.6, 'B4');
  nodes.B5 = createNode('B5', 10, 3, 1.0, 0.6, 'B5');


    createLine(svg, E.id, idC, false);
  // create edges (with small xOffsets to avoid overlap)
    createLine(svg, idC, B.id, true);
  makeConnection(nodes.E1, nodes.B1, '2', 5.6, -0.2);
}
  makeConnection(nodes.E2, nodes.B1, '1', 5.6, 0.2);


// B1: E1 + E2
  makeConnection(nodes.E1, nodes.B2, '2', 5.6, -0.2);
connect(E1, B1, "2");
  makeConnection(nodes.E2, nodes.B2, '1', 5.6,  0.2);
connect(E2, B1, "1");


// B2: E1 + E2
  makeConnection(nodes.E1, nodes.B3, '1', 5.6, -0.3);
connect(E1, B2, "2");
  makeConnection(nodes.E2, nodes.B3, '1', 5.6, 0.0);
connect(E2, B2, "1");
  makeConnection(nodes.E3, nodes.B3, '1', 5.6,  0.3);


// B3: E1 + E2 + E3
  makeConnection(nodes.E1, nodes.B4, '2', 5.6, -0.3);
connect(E1, B3, "1");
  makeConnection(nodes.E3, nodes.B4, '1', 5.6,  0.0);
connect(E2, B3, "1");
  makeConnection(nodes.E4, nodes.B4, '1', 5.6,  0.3);
connect(E3, B3, "1");


// B4: E1 + E3 + E4
  makeConnection(nodes.E1, nodes.B5, '1', 5.6, -0.2);
connect(E1, B4, "2");
  makeConnection(nodes.E4, nodes.B5, '2', 5.6, 0.2);
connect(E3, B4, "1");
connect(E4, B4, "1");


// B5: E1 + E4
  // update all edges
connect(E1, B5, "1");
  function updateAllEdges(){ edges.forEach(e=>updateEdge(e)); }
connect(E4, B5, "2");


// Dragging aktivieren
  // initial update
enableDragging(svg);
  updateAllEdges();


  // re-update edges also when window resizes (visual)
  window.addEventListener('resize', updateAllEdges);
})();
</script>
</script>
</html>
</html>