Repository: workcraft/workcraft
Issue: Contracting a circuit component works incorrectly if circuit integrity is violated

E.g. when a connection from an input port to output port is created as the result of a contraction, or a fork to two output port. Maybe it would be better to report an error and leave the circuit unchanged.


Changed files:
CircuitPlugin/src/org/workcraft/plugins/circuit/tools/ComponentContractionTool.java
CircuitPlugin/src/org/workcraft/plugins/circuit/VisualCircuit.java

diff --git a/CircuitPlugin/src/org/workcraft/plugins/circuit/VisualCircuit.java b/CircuitPlugin/src/org/workcraft/plugins/circuit/VisualCircuit.java
index bede8cc5f..9075b87a2 100644
--- a/CircuitPlugin/src/org/workcraft/plugins/circuit/VisualCircuit.java
+++ b/CircuitPlugin/src/org/workcraft/plugins/circuit/VisualCircuit.java
@@ -143,7 +143,7 @@ public class VisualCircuit extends AbstractVisualModel {
             if (driven.isOutput() && driven.isPort()) {
                 outputPortCount++;
                 if (outputPortCount > 1) {
-                    throw new InvalidConnectionException("Fork on the output port is not allowed.");
+                    throw new InvalidConnectionException("Fork on output ports is not allowed.");
                 }
                 if ((driver != null) && driver.isInput() && driver.isPort()) {
                     throw new InvalidConnectionException("Direct connection from input port to output port is not allowed.");
@@ -164,7 +164,7 @@ public class VisualCircuit extends AbstractVisualModel {
             if (second instanceof VisualContact) {
                 VisualContact secondContact = (VisualContact) second;
                 if (firstComponent.getIsZeroDelay() && secondContact.isPort() && secondContact.isOutput()) {
-                    throw new InvalidConnectionException("Zero delay components cannot be connected to the output ports.");
+                    throw new InvalidConnectionException("Zero delay components cannot be connected to output ports.");
                 }
             }
         }
diff --git a/CircuitPlugin/src/org/workcraft/plugins/circuit/tools/ComponentContractionTool.java b/CircuitPlugin/src/org/workcraft/plugins/circuit/tools/ComponentContractionTool.java
index 4459e9a90..68c68c10e 100644
--- a/CircuitPlugin/src/org/workcraft/plugins/circuit/tools/ComponentContractionTool.java
+++ b/CircuitPlugin/src/org/workcraft/plugins/circuit/tools/ComponentContractionTool.java
@@ -24,6 +24,7 @@ package org.workcraft.plugins.circuit.tools;
 import java.awt.geom.Point2D;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.LinkedList;
 
 import org.workcraft.NodeTransformer;
@@ -35,6 +36,10 @@ import org.workcraft.dom.visual.ConnectionHelper;
 import org.workcraft.dom.visual.VisualModel;
 import org.workcraft.dom.visual.connections.VisualConnection;
 import org.workcraft.exceptions.InvalidConnectionException;
+import org.workcraft.plugins.circuit.Circuit;
+import org.workcraft.plugins.circuit.CircuitUtils;
+import org.workcraft.plugins.circuit.Contact;
+import org.workcraft.plugins.circuit.FunctionComponent;
 import org.workcraft.plugins.circuit.VisualCircuit;
 import org.workcraft.plugins.circuit.VisualCircuitComponent;
 import org.workcraft.plugins.circuit.VisualCircuitConnection;
@@ -48,7 +53,7 @@ public class ComponentContractionTool extends TransformationTool implements Node
 
     @Override
     public String getDisplayName() {
-        return "Contract selected single-input component";
+        return "Contract selected single-input/single-output components";
     }
 
     @Override
@@ -102,35 +107,93 @@ public class ComponentContractionTool extends TransformationTool implements Node
         if ((model instanceof VisualCircuit) && (node instanceof VisualCircuitComponent)) {
             VisualCircuit circuit = (VisualCircuit) model;
             VisualCircuitComponent component = (VisualCircuitComponent) node;
-            Collection<VisualContact> inputContacts = component.getVisualInputs();
-            if (inputContacts.size() < 2) {
+            if (isValidContraction(circuit, component)) {
                 VisualContact inputContact = component.getFirstVisualInput();
                 for (VisualContact outputContact: component.getVisualOutputs()) {
                     connectContacts(circuit, inputContact, outputContact);
                 }
                 circuit.remove(component);
-            } else {
-                LogUtils.logWarningLine("Cannot contract a component with more than 1 input.");
             }
         }
     }
 
-    private void connectContacts(VisualCircuit circuit, VisualContact inputContact, VisualContact outputContact) {
-        if ((inputContact != null) && (outputContact != null)) {
-            for (Connection inputConnection: circuit.getConnections(inputContact)) {
-                Node fromNode = inputConnection.getFirst();
-                for (Connection outputConnection: new ArrayList<>(circuit.getConnections(outputContact))) {
-                    Node toNode = outputConnection.getSecond();
-                    LinkedList<Point2D> locations = ConnectionHelper.getMergedControlPoints((VisualContact) outputContact,
-                            (VisualConnection) inputConnection, (VisualConnection) outputConnection);
-                    circuit.remove(outputConnection);
-                    try {
-                        VisualConnection newConnection = (VisualCircuitConnection) circuit.connect(fromNode, toNode);
-                        newConnection.mixStyle((VisualConnection) inputConnection, (VisualConnection) outputConnection);
-                        ConnectionHelper.addControlPoints(newConnection, locations);
-                    } catch (InvalidConnectionException e) {
-                        LogUtils.logWarningLine(e.getMessage());
+    private boolean isValidContraction(VisualCircuit circuit, VisualCircuitComponent component) {
+        Collection<VisualContact> inputContacts = component.getVisualInputs();
+        String componentName = circuit.getMathName(component);
+        if (inputContacts.size() > 2) {
+            LogUtils.logErrorLine("Cannot contract component '" + componentName + "' with " + inputContacts.size() + " inputs.");
+            return false;
+        }
+        VisualContact inputContact = component.getFirstVisualInput();
+        Collection<VisualContact> outputContacts = component.getVisualOutputs();
+        if (outputContacts.size() > 2) {
+            LogUtils.logErrorLine("Cannot contract component '" + componentName + "' with " + outputContacts.size() + " outputs.");
+            return false;
+        }
+        VisualContact outputContact = component.getFirstVisualOutput();
+
+        // Input and output ports
+        Circuit mathCircuit = (Circuit) circuit.getMathModel();
+        Contact driver = CircuitUtils.findDriver(mathCircuit, inputContact.getReferencedComponent(), true);
+        HashSet<Contact> drivenSet = new HashSet<>();
+        drivenSet.addAll(CircuitUtils.findDriven(mathCircuit, driver, true));
+        drivenSet.addAll(CircuitUtils.findDriven(mathCircuit, outputContact.getReferencedContact(), true));
+        int outputPortCount = 0;
+        for (Contact driven: drivenSet) {
+            if (driven.isOutput() && driven.isPort()) {
+                outputPortCount++;
+                if (outputPortCount > 1) {
+                    LogUtils.logErrorLine("Cannot contract component '" + componentName + "' as it leads to fork on output ports.");
+                    return false;
+                }
+                if ((driver != null) && driver.isInput() && driver.isPort()) {
+                    LogUtils.logErrorLine("Cannot contract component '" + componentName + "' as it leads to direct connection from input port to output port.");
+                    return false;
+                }
+            }
+        }
+
+        // Handle zero-delay components
+        Contact directDriver = CircuitUtils.findDriver(mathCircuit, inputContact.getReferencedComponent(), false);
+        Node directDriverParent = directDriver.getParent();
+        if (directDriverParent instanceof FunctionComponent) {
+            FunctionComponent directDriverComponent = (FunctionComponent) directDriverParent;
+            if (directDriverComponent.getIsZeroDelay()) {
+                Collection<Contact> directDrivenSet = CircuitUtils.findDriven(mathCircuit, outputContact.getReferencedContact(), false);
+                for (Contact directDriven: directDrivenSet) {
+                    if (directDriven.isOutput() && directDriven.isPort()) {
+                        LogUtils.logErrorLine("Cannot contract component '" + componentName + "' as it leads to connection of zero delay component to output port.");
+                        return false;
                     }
+                    Node directDrivenParent = directDriven.getParent();
+                    if (directDrivenParent instanceof FunctionComponent) {
+                        FunctionComponent directDrivenComponent = (FunctionComponent) directDrivenParent;
+                        if (directDrivenComponent.getIsZeroDelay()) {
+                            LogUtils.logErrorLine("Cannot contract component '" + componentName + "' as it leads to connection between zero delay components.");
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+
+        return true;
+    }
+
+    private void connectContacts(VisualCircuit circuit, VisualContact inputContact, VisualContact outputContact) {
+        for (Connection inputConnection: circuit.getConnections(inputContact)) {
+            Node fromNode = inputConnection.getFirst();
+            for (Connection outputConnection: new ArrayList<>(circuit.getConnections(outputContact))) {
+                Node toNode = outputConnection.getSecond();
+                LinkedList<Point2D> locations = ConnectionHelper.getMergedControlPoints((VisualContact) outputContact,
+                        (VisualConnection) inputConnection, (VisualConnection) outputConnection);
+                circuit.remove(outputConnection);
+                try {
+                    VisualConnection newConnection = (VisualCircuitConnection) circuit.connect(fromNode, toNode);
+                    newConnection.mixStyle((VisualConnection) inputConnection, (VisualConnection) outputConnection);
+                    ConnectionHelper.addControlPoints(newConnection, locations);
+                } catch (InvalidConnectionException e) {
+                    LogUtils.logWarningLine(e.getMessage());
                 }
             }
         }