Gozintograph: Unterschied zwischen den Versionen

Aus FLBK-Wiki
Zur Navigation springen Zur Suche springen
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>

Version vom 14. November 2025, 11:11 Uhr

Ein Gozintograph (von engl. *goes into* = „geht hinein“) ist ein gerichteter Graph, der die Zerlegung eines Endprodukts in seine Einzelteile oder Komponenten beschreibt. Jede Kante stellt dabei eine „Gozinto“-Beziehung dar: Sie zeigt von einer Komponente (Teil) auf das Produkt, in das sie eingeht. Der Gozintograph ist ein zentrales Hilfsmittel in der Produktionsplanung und Stücklistenverwaltung.

Definition

Ein Gozintograph ist ein gerichteter, azyklischer Graph \( G = (V, E) \), wobei:

  • \( V \) die Menge der Knoten darstellt (Produkte oder Teile),
  • \( E \subseteq V \times V \) die gerichteten Kanten darstellt, welche „geht-in“-Beziehungen symbolisieren.

Eine Kante \( (v_i, v_j, a_{ij}) \) mit der Beschriftung \( a_{ij} \) zeigt an, dass zur Herstellung eines Teils \( v_j \) genau \( a_{ij} \) Einheiten von Teil \( v_i \) benötigt werden.

Zusammenhang zu Matrizen

Die Informationen eines Gozintographen lassen sich in einer sogenannten Gozintomatrix darstellen. Diese ist eine Matrix \( A = (a_{ij}) \), bei der das Element \( a_{ij} \) die Anzahl der Einheiten von Komponente \( i \) angibt, die für die Herstellung von Produkt \( j \) benötigt wird. In der Produktionsplanung kann die benötigte Gesamtmenge aller Einzelteile über die Gleichung

\[ \mathbf{x} = (I - A)^{-1} \mathbf{y} \]

bestimmt werden, wobei \( \mathbf{y} \) den Vektor der Endprodukte und \( \mathbf{x} \) den Vektor der benötigten Teilemengen beschreibt.

Beispiele

Produktion eines Produkts aus Einzelteilen

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.

Beispiel 2: Rezeptstruktur eines Gerichts

Rezeptstruktur eines Gerichts

Im nächsten Beispiel wird der Gozintograph genutzt, um die Zutatenstruktur eines Rezepts zu zeigen. Das Endprodukt „Pizza“ besteht aus mehreren Zwischenprodukten („Teig“, „Soße“) und Basiszutaten. Auch hier zeigen Pfeile mit Zahlen, welche Mengen von Zutaten in die jeweiligen Komponenten eingehen.