Gozintograph: Unterschied zwischen den Versionen

Markierung: Zurückgesetzt
Markierung: Manuelle Zurücksetzung
Zeile 23: Zeile 23:
Im folgenden Beispiel werden fünf Bauteile \( B_1, B_2, B_3, B_4, B_5 \) aus vier Einzelteilen \( E_1, E_2, E_3, E_4 \) gefertigt.   
Im folgenden Beispiel werden fünf Bauteile \( B_1, B_2, B_3, B_4, B_5 \) aus vier Einzelteilen \( E_1, E_2, E_3, E_4 \) gefertigt.   
Die Pfeile zeigen, welche Einzelteile in welches Bauteil eingehen. Die Zahlen an den Pfeilen geben die Stückzahl an.
Die Pfeile zeigen, welche Einzelteile in welches Bauteil eingehen. Die Zahlen an den Pfeilen geben die Stückzahl an.
<!-- Variante B: Reines SVG + JS (kein JSXGraph) -->
<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>