ljmotta commented on code in PR #2130:
URL: 
https://github.com/apache/incubator-kie-tools/pull/2130#discussion_r1475102941


##########
packages/dmn-editor/src/DmnEditor.css:
##########
@@ -318,17 +318,51 @@
 }
 /* (end) version popover */
 
-/* (begin) overlays panel toggle */
-.kie-dmn-editor--overlay-panel-popover .pf-c-popover__body {
+/* (begin) autolayout panel button */
+.kie-dmn-editor--autolayout-panel-toggle {
+  width: 42px;
+  background: #fff;
+  border-radius: 40px;
+  border: 1px solid black;
+}
+.kie-dmn-editor--autolayout-panel-toggle:not(:last-child) {
+  margin-right: 16px;
+}
+
+.kie-dmn-editor--autolayout-panel-toggle:hover {
+  box-shadow: -4px 4px 2px 0px rgb(127 127 127);
+}
+.kie-dmn-editor--autolayout-panel-toggle-button {
+  margin: 0;
+  width: 40px;
+  height: 40px;
+  max-width: 40px;
+  border-radius: 40px;
+  border: 0;
+  background: white;
+}
+.kie-dmn-editor--autolayout-panel-toggle-button:hover {
+  filter: brightness(95%);
+}
+.kie-dmn-editor--autolayout-panel-toggle-button:active {
+  filter: brightness(90%);
+}
+/* (end) autolayout panel toggle *
+/
+/* (begin) autolayout panel toggle */

Review Comment:
   I guess you forgot to change the comments here.



##########
packages/dmn-editor/src/diagram/edges/Edges.tsx:
##########
@@ -225,9 +220,12 @@ export const KnowledgeRequirementEdge = React.memo((props: 
RF.EdgeProps<DmnDiagr
       {isHovered && potentialWaypoint && <PotentialWaypoint 
point={potentialWaypoint.point} />}
     </>
   );
-});
+}, propsHaveSameValuesDeep);
 
 export const AuthorityRequirementEdge = React.memo((props: 
RF.EdgeProps<DmnDiagramEdgeData>) => {
+  const renderCount = useRef<number>(0);
+  renderCount.current++;

Review Comment:
   same as above.



##########
packages/dmn-editor/src/diagram/edges/Edges.tsx:
##########
@@ -179,9 +172,12 @@ export const InformationRequirementEdge = 
React.memo((props: RF.EdgeProps<DmnDia
       {isHovered && potentialWaypoint && <PotentialWaypoint 
point={potentialWaypoint.point} />}
     </>
   );
-});
+}, propsHaveSameValuesDeep);
 
 export const KnowledgeRequirementEdge = React.memo((props: 
RF.EdgeProps<DmnDiagramEdgeData>) => {
+  const renderCount = useRef<number>(0);
+  renderCount.current++;

Review Comment:
   same as above.



##########
packages/dmn-editor/src/autolayout/AutolayoutButton.tsx:
##########
@@ -0,0 +1,559 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api";
+import { DC__Bounds } from 
"@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import OptimizeIcon from "@patternfly/react-icons/dist/js/icons/optimize-icon";
+import ELK, * as Elk from "elkjs/lib/elk.bundled.js";
+import * as React from "react";
+import { PositionalNodeHandleId } from 
"../diagram/connections/PositionalNodeHandles";
+import { EdgeType, NodeType } from "../diagram/connections/graphStructure";
+import { getAdjMatrix, traverse } from "../diagram/graph/graph";
+import { getContainmentRelationship } from "../diagram/maths/DmnMaths";
+import { DEFAULT_NODE_SIZES, MIN_NODE_SIZES } from 
"../diagram/nodes/DefaultSizes";
+import { NODE_TYPES } from "../diagram/nodes/NodeTypes";
+import { useExternalModels } from 
"../includedModels/DmnEditorDependenciesContext";
+import { addEdge } from "../mutations/addEdge";
+import { repositionNode } from "../mutations/repositionNode";
+import { resizeNode } from "../mutations/resizeNode";
+import { updateDecisionServiceDividerLine } from 
"../mutations/updateDecisionServiceDividerLine";
+import { useDmnEditorStoreApi } from "../store/StoreContext";
+
+const elk = new ELK();
+
+export const ELK_OPTIONS = {
+  "elk.algorithm": "layered",
+  "elk.direction": "UP",
+  // By making width a lot bigger than height, we make sure disjoint graph 
components are placed horizontally, never vertically
+  "elk.aspectRatio": "9999999999",
+  // spacing
+  "elk.spacing.nodeNode": "60",
+  "elk.spacing.componentComponent": "200",
+  "layered.spacing.edgeEdgeBetweenLayers": "0",
+  "layered.spacing.edgeNodeBetweenLayers": "0",
+  "layered.spacing.nodeNodeBetweenLayers": "100",
+  // edges
+  "elk.edgeRouting": "ORTHOGONAL",
+  "elk.layered.mergeEdges": "true", // we need this to make sure space is 
consistent between layers.
+  "elk.layered.mergeHierarchyEdges": "true",
+  // positioning
+  "elk.partitioning.activate": "true",
+  "elk.nodePlacement.favorStraightEdges": "true",
+  "elk.nodePlacement.bk.fixedAlignment": "LEFTDOWN",
+  "elk.nodePlacement.bk.edgeStraightening": "IMPROVE_STRAIGHTNESS",
+  //
+  "layering.strategy": "LONGEST_PATH_SOURCE",
+};
+
+const PARENT_NODE_ELK_OPTIONS = {
+  "elk.padding": "[left=60, top=60, right=80, bottom=60]",
+  "elk.spacing.componentComponent": "60",
+};
+
+export interface AutolayoutParentNode {
+  decisionServiceSection: "output" | "encapsulated" | "n/a";
+  elkNode: Elk.ElkNode;
+  contained: Set<string>;
+  dependents: Set<string>;
+  dependencies: Set<string>;
+  contains: (otherNode: { id: string; bounds: DC__Bounds | undefined }) => {
+    isInside: boolean;
+    decisionServiceSection: AutolayoutParentNode["decisionServiceSection"];
+  };
+  hasDependencyTo: (otherNode: { id: string }) => boolean;
+  isDependencyOf: (otherNode: { id: string }) => boolean;
+}
+
+const FAKE_MARKER = "__$FAKE$__";
+
+export function AutolayoutButton() {
+  const dmnEditorStoreApi = useDmnEditorStoreApi();
+  const { externalModelsByNamespace } = useExternalModels();
+
+  const onApply = React.useCallback(async () => {
+    const parentNodesById = new Map<string, AutolayoutParentNode>();
+    const nodeParentsById = new Map<string, Set<string>>();
+
+    /**
+      Used to tell ELK that dependencies of nodes' children should be 
considered the node's dependency too.
+      This allows us to not rely on INCLUDE_STRATEGY hierarchy handling on 
ELK, keeping disjoint graph components separate, rendering side-by-side.
+     */
+    const fakeEdgesForElk = new Set<Elk.ElkExtendedEdge>();
+
+    const state = dmnEditorStoreApi.getState();
+
+    const snapGrid = state.diagram.snapGrid;
+    const nodesById = 
state.computed(state).getDiagramData(externalModelsByNamespace).nodesById;
+    const edgesById = 
state.computed(state).getDiagramData(externalModelsByNamespace).edgesById;
+    const nodes = 
state.computed(state).getDiagramData(externalModelsByNamespace).nodes;
+    const edges = 
state.computed(state).getDiagramData(externalModelsByNamespace).edges;
+    const drgEdges = 
state.computed(state).getDiagramData(externalModelsByNamespace).drgEdges;
+
+    const adjMatrix = getAdjMatrix(drgEdges);
+
+    // 1. First we populate the `parentNodesById` map so that we know exactly 
what parent nodes we're dealing with. Decision Service nodes have two fake 
nodes to represent Output and Encapsulated sections.
+    for (const node of nodes) {
+      const dependencies = new Set<string>();
+      const dependents = new Set<string>();
+
+      if (node.data?.dmnObject?.__$$element === "decisionService") {
+        const outputs = new Set([...(node.data.dmnObject.outputDecision ?? 
[]).map((s) => s["@_href"])]);
+        const encapsulated = new 
Set([...(node.data.dmnObject.encapsulatedDecision ?? []).map((s) => 
s["@_href"])]);
+
+        const idOfFakeNodeForOutputSection = 
`${node.id}${FAKE_MARKER}dsOutput`;
+        const idOfFakeNodeForEncapsulatedSection = 
`${node.id}${FAKE_MARKER}dsEncapsulated`;
+
+        const dsSize = MIN_NODE_SIZES[NODE_TYPES.decisionService](snapGrid);
+        parentNodesById.set(node.id, {
+          elkNode: {
+            id: node.id,
+            width: dsSize["@_width"],
+            height: dsSize["@_height"],
+            children: [
+              {
+                id: idOfFakeNodeForOutputSection,
+                width: dsSize["@_width"],
+                height: dsSize["@_height"] / 2,
+                children: [],
+                layoutOptions: {
+                  ...ELK_OPTIONS,
+                  ...PARENT_NODE_ELK_OPTIONS,
+                },
+              },
+              {
+                id: idOfFakeNodeForEncapsulatedSection,
+                width: dsSize["@_width"],
+                height: dsSize["@_height"] / 2,
+                children: [],
+                layoutOptions: {
+                  ...ELK_OPTIONS,
+                  ...PARENT_NODE_ELK_OPTIONS,
+                },
+              },
+            ],
+            layoutOptions: {
+              "elk.algorithm": "layered",
+              "elk.direction": "UP",
+              "elk.aspectRatio": "9999999999",
+              "elk.partitioning.activate": "true",
+              "elk.spacing.nodeNode": "0",
+              "elk.spacing.componentComponent": "0",
+              "layered.spacing.edgeEdgeBetweenLayers": "0",
+              "layered.spacing.edgeNodeBetweenLayers": "0",
+              "layered.spacing.nodeNodeBetweenLayers": "0",
+              "elk.padding": "[left=0, top=0, right=0, bottom=0]",
+            },
+          },
+          decisionServiceSection: "output",
+          dependencies,
+          dependents,
+          contained: outputs,
+          contains: ({ id }) => ({
+            isInside: outputs.has(id) || encapsulated.has(id),
+            decisionServiceSection: outputs.has(id) ? "output" : 
encapsulated.has(id) ? "encapsulated" : "n/a",
+          }),
+          isDependencyOf: ({ id }) => dependents.has(id),
+          hasDependencyTo: ({ id }) => dependencies.has(id),
+        });
+
+        fakeEdgesForElk.add({
+          id: `${node.id}${FAKE_MARKER}fakeOutputEncapsulatedEdge`,
+          sources: [idOfFakeNodeForEncapsulatedSection],
+          targets: [idOfFakeNodeForOutputSection],
+        });
+      } else if (node.data?.dmnObject?.__$$element === "group") {
+        const groupSize = DEFAULT_NODE_SIZES[NODE_TYPES.group](snapGrid);
+        const groupBounds = node.data.shape["dc:Bounds"];
+        parentNodesById.set(node.id, {
+          decisionServiceSection: "n/a",
+          elkNode: {
+            id: node.id,
+            width: groupBounds?.["@_width"] ?? groupSize["@_width"],
+            height: groupBounds?.["@_height"] ?? groupSize["@_height"],
+            children: [],
+            layoutOptions: {
+              ...ELK_OPTIONS,
+              ...PARENT_NODE_ELK_OPTIONS,
+            },
+          },
+          dependencies,
+          dependents,
+          contained: new Set(),
+          contains: ({ id, bounds }) => ({
+            isInside: getContainmentRelationship({
+              bounds: bounds!,
+              container: groupBounds!,
+              snapGrid,
+              containerMinSizes: MIN_NODE_SIZES[NODE_TYPES.group],
+              boundsMinSizes: MIN_NODE_SIZES[nodesById.get(id)?.type as 
NodeType],
+            }).isInside,
+            decisionServiceSection: "n/a",
+          }),
+          isDependencyOf: ({ id }) => dependents.has(id),
+          hasDependencyTo: ({ id }) => dependencies.has(id),
+        });
+      }
+    }
+
+    // 2. Then we map all the nodes to elkNodes, including the parents. We 
mutate parents on the fly when iterating over the nodes list.
+    const elkNodes = nodes.flatMap((node) => {
+      const parent = parentNodesById.get(node.id);
+      if (parent) {
+        return [];
+      }
+
+      const defaultSize = DEFAULT_NODE_SIZES[node.type as NodeType](snapGrid);
+      const elkNode: Elk.ElkNode = {
+        id: node.id,
+        width: node.data.shape["dc:Bounds"]?.["@_width"] ?? 
defaultSize["@_width"],
+        height: node.data.shape["dc:Bounds"]?.["@_height"] ?? 
defaultSize["@_height"],
+        children: [],
+        layoutOptions: {
+          "partitioning.partition":
+            // Since textAnnotations and knowledgeSources are not related to 
the logic, we leave them at the bottom.
+            (node.type as NodeType) === NODE_TYPES.textAnnotation ||
+            (node.type as NodeType) === NODE_TYPES.knowledgeSource
+              ? "0"
+              : "1",
+        },
+      };
+
+      // FIXME: Tiago --> Improve performance here.

Review Comment:
   Maybe create an issue for this improvement?



##########
packages/dmn-editor/src/diagram/edges/Edges.tsx:
##########
@@ -136,6 +127,9 @@ const interactionStrokeProps: 
Partial<React.SVGAttributes<SVGPathElement>> = {
 };
 
 export const InformationRequirementEdge = React.memo((props: 
RF.EdgeProps<DmnDiagramEdgeData>) => {
+  const renderCount = useRef<number>(0);
+  renderCount.current++;

Review Comment:
   Should we keep this as a debug log?



##########
packages/dmn-editor/src/diagram/Diagram.tsx:
##########
@@ -414,24 +404,73 @@ export const Diagram = React.forwardRef<DiagramRef, { 
container: React.RefObject
                 },
               },
             });
+
+            // TODO: Decide what to do with Decision Services and multiple 
DRDs.
+            //
+            // When adding a Decision Service to a DRD, we need to bring all 
its encapsulated and output Decisions with it, already formatted with 
autolayout.
+            //
+            // if (drgElement.__$$element === "decisionService") {
+            //   const defaultDecisionDimensions = 
DEFAULT_NODE_SIZES[NODE_TYPES.decision](state.diagram.snapGrid);
+
+            //   let i = 0;
+            //   for (const decision of [
+            //     ...(drgElement.outputDecision ?? []),
+            //     ...(drgElement.encapsulatedDecision ?? []),
+            //   ]) {
+            //     i++;
+
+            //     const decisionHref = parseXmlHref(decision["@_href"]);
+
+            //     const decisionQNamePrefix = decisionHref.namespace
+            //       ? getXmlNamespaceDeclarationName({
+            //           model: state.dmn.model.definitions,
+            //           namespace: decisionHref.namespace,
+            //         })
+            //       : undefined;
+
+            //     // If there's a shape for this Decision in the DRD already, 
we don't need to add anything.
+            //     // If not, we add a new shape for it.
+            //     if 
(!state.computed(state).indexes().dmnShapesByHref.has(decision["@_href"])) {
+            //       addShape({
+            //         definitions: state.dmn.model.definitions,
+            //         drdIndex: state.diagram.drdIndex,
+            //         nodeType,
+            //         shape: {
+            //           "@_dmnElementRef": buildXmlQName({
+            //             type: "xml-qname",
+            //             localPart: decisionHref.id,
+            //             prefix: decisionQNamePrefix,
+            //           }),
+            //           "dc:Bounds": {
+            //             "@_x": dropPoint.x + 40 * i,
+            //             "@_y": dropPoint.y + 40 * i,
+            //             "@_width": defaultDecisionDimensions["@_width"],
+            //             "@_height": defaultDecisionDimensions["@_height"],
+            //           },
+            //         },
+            //       });
+            //     }
+            //   }
+            // }

Review Comment:
   Maybe creating an issue? I don't think it's a good idea to keep it commented.



##########
packages/dmn-editor/src/diagram/edges/Edges.tsx:
##########
@@ -276,9 +273,12 @@ export const AuthorityRequirementEdge = React.memo((props: 
RF.EdgeProps<DmnDiagr
       {isHovered && potentialWaypoint && <PotentialWaypoint 
point={potentialWaypoint.point} />}
     </>
   );
-});
+}, propsHaveSameValuesDeep);
 
 export const AssociationEdge = React.memo((props: 
RF.EdgeProps<DmnDiagramEdgeData>) => {
+  const renderCount = useRef<number>(0);
+  renderCount.current++;

Review Comment:
   same.



##########
packages/dmn-editor/src/autolayout/AutolayoutButton.tsx:
##########
@@ -0,0 +1,559 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api";
+import { DC__Bounds } from 
"@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import OptimizeIcon from "@patternfly/react-icons/dist/js/icons/optimize-icon";
+import ELK, * as Elk from "elkjs/lib/elk.bundled.js";
+import * as React from "react";
+import { PositionalNodeHandleId } from 
"../diagram/connections/PositionalNodeHandles";
+import { EdgeType, NodeType } from "../diagram/connections/graphStructure";
+import { getAdjMatrix, traverse } from "../diagram/graph/graph";
+import { getContainmentRelationship } from "../diagram/maths/DmnMaths";
+import { DEFAULT_NODE_SIZES, MIN_NODE_SIZES } from 
"../diagram/nodes/DefaultSizes";
+import { NODE_TYPES } from "../diagram/nodes/NodeTypes";
+import { useExternalModels } from 
"../includedModels/DmnEditorDependenciesContext";
+import { addEdge } from "../mutations/addEdge";
+import { repositionNode } from "../mutations/repositionNode";
+import { resizeNode } from "../mutations/resizeNode";
+import { updateDecisionServiceDividerLine } from 
"../mutations/updateDecisionServiceDividerLine";
+import { useDmnEditorStoreApi } from "../store/StoreContext";
+
+const elk = new ELK();
+
+export const ELK_OPTIONS = {
+  "elk.algorithm": "layered",
+  "elk.direction": "UP",
+  // By making width a lot bigger than height, we make sure disjoint graph 
components are placed horizontally, never vertically
+  "elk.aspectRatio": "9999999999",
+  // spacing
+  "elk.spacing.nodeNode": "60",
+  "elk.spacing.componentComponent": "200",
+  "layered.spacing.edgeEdgeBetweenLayers": "0",
+  "layered.spacing.edgeNodeBetweenLayers": "0",
+  "layered.spacing.nodeNodeBetweenLayers": "100",
+  // edges
+  "elk.edgeRouting": "ORTHOGONAL",
+  "elk.layered.mergeEdges": "true", // we need this to make sure space is 
consistent between layers.
+  "elk.layered.mergeHierarchyEdges": "true",
+  // positioning
+  "elk.partitioning.activate": "true",
+  "elk.nodePlacement.favorStraightEdges": "true",
+  "elk.nodePlacement.bk.fixedAlignment": "LEFTDOWN",
+  "elk.nodePlacement.bk.edgeStraightening": "IMPROVE_STRAIGHTNESS",
+  //
+  "layering.strategy": "LONGEST_PATH_SOURCE",
+};
+
+const PARENT_NODE_ELK_OPTIONS = {
+  "elk.padding": "[left=60, top=60, right=80, bottom=60]",
+  "elk.spacing.componentComponent": "60",
+};
+
+export interface AutolayoutParentNode {
+  decisionServiceSection: "output" | "encapsulated" | "n/a";
+  elkNode: Elk.ElkNode;
+  contained: Set<string>;
+  dependents: Set<string>;
+  dependencies: Set<string>;
+  contains: (otherNode: { id: string; bounds: DC__Bounds | undefined }) => {
+    isInside: boolean;
+    decisionServiceSection: AutolayoutParentNode["decisionServiceSection"];
+  };
+  hasDependencyTo: (otherNode: { id: string }) => boolean;
+  isDependencyOf: (otherNode: { id: string }) => boolean;
+}
+
+const FAKE_MARKER = "__$FAKE$__";
+
+export function AutolayoutButton() {
+  const dmnEditorStoreApi = useDmnEditorStoreApi();
+  const { externalModelsByNamespace } = useExternalModels();
+
+  const onApply = React.useCallback(async () => {
+    const parentNodesById = new Map<string, AutolayoutParentNode>();
+    const nodeParentsById = new Map<string, Set<string>>();
+
+    /**
+      Used to tell ELK that dependencies of nodes' children should be 
considered the node's dependency too.
+      This allows us to not rely on INCLUDE_STRATEGY hierarchy handling on 
ELK, keeping disjoint graph components separate, rendering side-by-side.
+     */
+    const fakeEdgesForElk = new Set<Elk.ElkExtendedEdge>();
+
+    const state = dmnEditorStoreApi.getState();
+
+    const snapGrid = state.diagram.snapGrid;
+    const nodesById = 
state.computed(state).getDiagramData(externalModelsByNamespace).nodesById;
+    const edgesById = 
state.computed(state).getDiagramData(externalModelsByNamespace).edgesById;
+    const nodes = 
state.computed(state).getDiagramData(externalModelsByNamespace).nodes;
+    const edges = 
state.computed(state).getDiagramData(externalModelsByNamespace).edges;
+    const drgEdges = 
state.computed(state).getDiagramData(externalModelsByNamespace).drgEdges;
+
+    const adjMatrix = getAdjMatrix(drgEdges);
+
+    // 1. First we populate the `parentNodesById` map so that we know exactly 
what parent nodes we're dealing with. Decision Service nodes have two fake 
nodes to represent Output and Encapsulated sections.
+    for (const node of nodes) {
+      const dependencies = new Set<string>();
+      const dependents = new Set<string>();
+
+      if (node.data?.dmnObject?.__$$element === "decisionService") {
+        const outputs = new Set([...(node.data.dmnObject.outputDecision ?? 
[]).map((s) => s["@_href"])]);
+        const encapsulated = new 
Set([...(node.data.dmnObject.encapsulatedDecision ?? []).map((s) => 
s["@_href"])]);
+
+        const idOfFakeNodeForOutputSection = 
`${node.id}${FAKE_MARKER}dsOutput`;
+        const idOfFakeNodeForEncapsulatedSection = 
`${node.id}${FAKE_MARKER}dsEncapsulated`;
+
+        const dsSize = MIN_NODE_SIZES[NODE_TYPES.decisionService](snapGrid);
+        parentNodesById.set(node.id, {
+          elkNode: {
+            id: node.id,
+            width: dsSize["@_width"],
+            height: dsSize["@_height"],
+            children: [
+              {
+                id: idOfFakeNodeForOutputSection,
+                width: dsSize["@_width"],
+                height: dsSize["@_height"] / 2,
+                children: [],
+                layoutOptions: {
+                  ...ELK_OPTIONS,
+                  ...PARENT_NODE_ELK_OPTIONS,
+                },
+              },
+              {
+                id: idOfFakeNodeForEncapsulatedSection,
+                width: dsSize["@_width"],
+                height: dsSize["@_height"] / 2,
+                children: [],
+                layoutOptions: {
+                  ...ELK_OPTIONS,
+                  ...PARENT_NODE_ELK_OPTIONS,
+                },
+              },
+            ],
+            layoutOptions: {
+              "elk.algorithm": "layered",
+              "elk.direction": "UP",
+              "elk.aspectRatio": "9999999999",
+              "elk.partitioning.activate": "true",
+              "elk.spacing.nodeNode": "0",
+              "elk.spacing.componentComponent": "0",
+              "layered.spacing.edgeEdgeBetweenLayers": "0",
+              "layered.spacing.edgeNodeBetweenLayers": "0",
+              "layered.spacing.nodeNodeBetweenLayers": "0",
+              "elk.padding": "[left=0, top=0, right=0, bottom=0]",
+            },
+          },
+          decisionServiceSection: "output",
+          dependencies,
+          dependents,
+          contained: outputs,
+          contains: ({ id }) => ({
+            isInside: outputs.has(id) || encapsulated.has(id),
+            decisionServiceSection: outputs.has(id) ? "output" : 
encapsulated.has(id) ? "encapsulated" : "n/a",
+          }),
+          isDependencyOf: ({ id }) => dependents.has(id),
+          hasDependencyTo: ({ id }) => dependencies.has(id),
+        });
+
+        fakeEdgesForElk.add({
+          id: `${node.id}${FAKE_MARKER}fakeOutputEncapsulatedEdge`,
+          sources: [idOfFakeNodeForEncapsulatedSection],
+          targets: [idOfFakeNodeForOutputSection],
+        });
+      } else if (node.data?.dmnObject?.__$$element === "group") {
+        const groupSize = DEFAULT_NODE_SIZES[NODE_TYPES.group](snapGrid);
+        const groupBounds = node.data.shape["dc:Bounds"];
+        parentNodesById.set(node.id, {
+          decisionServiceSection: "n/a",
+          elkNode: {
+            id: node.id,
+            width: groupBounds?.["@_width"] ?? groupSize["@_width"],
+            height: groupBounds?.["@_height"] ?? groupSize["@_height"],
+            children: [],
+            layoutOptions: {
+              ...ELK_OPTIONS,
+              ...PARENT_NODE_ELK_OPTIONS,
+            },
+          },
+          dependencies,
+          dependents,
+          contained: new Set(),
+          contains: ({ id, bounds }) => ({
+            isInside: getContainmentRelationship({
+              bounds: bounds!,
+              container: groupBounds!,
+              snapGrid,
+              containerMinSizes: MIN_NODE_SIZES[NODE_TYPES.group],
+              boundsMinSizes: MIN_NODE_SIZES[nodesById.get(id)?.type as 
NodeType],
+            }).isInside,
+            decisionServiceSection: "n/a",
+          }),
+          isDependencyOf: ({ id }) => dependents.has(id),
+          hasDependencyTo: ({ id }) => dependencies.has(id),
+        });
+      }
+    }
+
+    // 2. Then we map all the nodes to elkNodes, including the parents. We 
mutate parents on the fly when iterating over the nodes list.
+    const elkNodes = nodes.flatMap((node) => {
+      const parent = parentNodesById.get(node.id);
+      if (parent) {
+        return [];
+      }
+
+      const defaultSize = DEFAULT_NODE_SIZES[node.type as NodeType](snapGrid);
+      const elkNode: Elk.ElkNode = {
+        id: node.id,
+        width: node.data.shape["dc:Bounds"]?.["@_width"] ?? 
defaultSize["@_width"],
+        height: node.data.shape["dc:Bounds"]?.["@_height"] ?? 
defaultSize["@_height"],
+        children: [],
+        layoutOptions: {
+          "partitioning.partition":
+            // Since textAnnotations and knowledgeSources are not related to 
the logic, we leave them at the bottom.
+            (node.type as NodeType) === NODE_TYPES.textAnnotation ||
+            (node.type as NodeType) === NODE_TYPES.knowledgeSource
+              ? "0"
+              : "1",
+        },
+      };
+
+      // FIXME: Tiago --> Improve performance here.
+      const parents = [...parentNodesById.values()].filter(
+        (p) => p.contains({ id: elkNode.id, bounds: 
node.data.shape["dc:Bounds"] }).isInside
+      );
+      if (parents.length > 0) {
+        const decisionServiceSection = parents[0].contains({
+          id: elkNode.id,
+          bounds: node.data.shape["dc:Bounds"],
+        }).decisionServiceSection;
+
+        // The only relationship that ELK will know about is the first 
matching container for this node.
+        if (decisionServiceSection === "n/a") {
+          parents[0].elkNode.children?.push(elkNode);
+        } else if (decisionServiceSection === "output") {
+          parents[0].elkNode.children?.[0].children?.push(elkNode);
+        } else if (decisionServiceSection === "encapsulated") {
+          parents[0].elkNode.children?.[1].children?.push(elkNode);
+        } else {
+          throw new Error(`Unknown decisionServiceSection 
${decisionServiceSection}`);
+        }
+
+        for (const p of parents) {
+          p.contained?.add(elkNode.id); // We need to keep track of nodes that 
are contained by multiple groups, but ELK will only know about one of those 
containment relationships.
+          nodeParentsById.set(node.id, new 
Set([...(nodeParentsById.get(node.id) ?? []), p.elkNode.id]));
+        }
+        return [];
+      }
+
+      return [elkNode];
+    });
+
+    // 3. After we have all containment relationships defined, we can proceed 
to resolving the hierarchical relationships.
+    for (const [_, parentNode] of parentNodesById) {
+      traverse(adjMatrix, parentNode.contained, [...parentNode.contained], 
"down", (n) => {
+        parentNode.dependencies.add(n);
+      });
+      traverse(adjMatrix, parentNode.contained, [...parentNode.contained], 
"up", (n) => {
+        parentNode.dependents.add(n);
+      });
+
+      const p = nodesById.get(parentNode.elkNode.id);
+      if (p?.type === NODE_TYPES.group && parentNode.elkNode.children?.length 
=== 0) {
+        continue; // Ignore empty group nodes.
+      } else {
+        elkNodes.push(parentNode.elkNode);
+      }
+    }
+
+    // 4. After we have all containment and hierarchical relationships 
defined, we can add the fake edges so that ELK creates the structure correctly.
+    for (const node of nodes) {
+      const parentNodes = [...parentNodesById.values()];
+
+      const dependents = parentNodes.filter((p) => p.hasDependencyTo({ id: 
node.id }));
+      for (const dependent of dependents) {
+        // Not all nodes are present in all DRD
+        if (nodesById.has(node.id) && nodesById.has(dependent.elkNode.id)) {
+          fakeEdgesForElk.add({
+            id: `${generateUuid()}${FAKE_MARKER}__fake`,
+            sources: [node.id],
+            targets: [dependent.elkNode.id],
+          });
+        }
+
+        for (const p of nodeParentsById.get(node.id) ?? []) {
+          // Not all nodes are present in all DRD
+          if (nodesById.has(p) && nodesById.has(dependent.elkNode.id)) {
+            fakeEdgesForElk.add({
+              id: `${generateUuid()}${FAKE_MARKER}__fake`,
+              sources: [p],
+              targets: [dependent.elkNode.id],
+            });
+          }
+        }
+      }
+
+      const dependencies = parentNodes.filter((p) => p.isDependencyOf({ id: 
node.id }));
+      for (const dependency of dependencies) {
+        // Not all nodes are present in all DRD
+        if (nodesById.has(node.id) && nodesById.has(dependency.elkNode.id)) {
+          fakeEdgesForElk.add({
+            id: `${generateUuid()}${FAKE_MARKER}__fake`,
+            sources: [dependency.elkNode.id],
+            targets: [node.id],
+          });
+        }
+
+        for (const p of nodeParentsById.get(node.id) ?? []) {
+          // Not all nodes are present in all DRD
+          if (nodesById.has(p) && nodesById.has(dependency.elkNode.id)) {
+            fakeEdgesForElk.add({
+              id: `${generateUuid()}${FAKE_MARKER}__fake`,
+              sources: [dependency.elkNode.id],
+              targets: [p],
+            });
+          }
+        }
+      }
+    }
+
+    // 5. Concatenate real and fake edges to pass to ELK.
+    const elkEdges = [
+      ...fakeEdgesForElk,
+      ...[...edgesById.values()].flatMap((e) => {
+        // Not all nodes are present in all DRD
+        if (nodesById.has(e.source) && nodesById.has(e.target)) {
+          return {
+            id: e.id,
+            sources: [e.source],
+            targets: [e.target],
+          };
+        } else {
+          return [];
+        }
+      }),
+    ];
+
+    // 6. Run ELK.
+    const autolayouted = await runElk(elkNodes, elkEdges, ELK_OPTIONS);
+
+    // 7. Update all nodes positions skipping empty groups, which will be 
positioned manually after all nodes are done being repositioned.
+    dmnEditorStoreApi.setState((s) => {
+      const autolayoutedElkNodesById = new Map<string, Elk.ElkNode>();
+
+      for (const topLevelElkNode of autolayouted.nodes ?? []) {
+        visitNodeAndNested(topLevelElkNode, { x: 100, y: 100 }, (elkNode, 
positionOffset) => {
+          if (elkNode.id.includes(FAKE_MARKER)) {
+            return;
+          }
+
+          autolayoutedElkNodesById.set(elkNode.id, elkNode);
+
+          const nodeId = elkNode.id;
+          const node = 
s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(nodeId)!;
+
+          repositionNode({
+            definitions: s.dmn.model.definitions,
+            drdIndex: s.diagram.drdIndex,
+            controlWaypointsByEdge: new Map(),
+            change: {
+              nodeType: node.type as NodeType,
+              type: "absolute",
+              position: {
+                x: elkNode.x! + positionOffset.x,
+                y: elkNode.y! + positionOffset.y,
+              },
+              selectedEdges: [...edgesById.keys()],
+              shapeIndex: node.data?.shape.index,
+              sourceEdgeIndexes: edges.flatMap((e) =>
+                e.source === nodeId && e.data?.dmnEdge ? 
[e.data.dmnEdge.index] : []
+              ),
+              targetEdgeIndexes: edges.flatMap((e) =>
+                e.target === nodeId && e.data?.dmnEdge ? 
[e.data.dmnEdge.index] : []
+              ),
+            },
+          });
+        });
+      }
+
+      // 8. Resize all nodes using the sizes calculated by ELK.
+      for (const topLevelElkNode of autolayouted.nodes ?? []) {
+        visitNodeAndNested(topLevelElkNode, { x: 0, y: 0 }, (elkNode) => {
+          if (elkNode.id.includes(FAKE_MARKER)) {
+            return;
+          }
+
+          const nodeId = elkNode.id;
+          const node = 
s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(nodeId)!;
+
+          resizeNode({
+            definitions: s.dmn.model.definitions,
+            drdIndex: s.diagram.drdIndex,
+            dmnShapesByHref: s.computed(s).indexes().dmnShapesByHref,
+            snapGrid,
+            change: {
+              index: node.data.index,
+              isExternal: !!node.data.dmnObjectQName.prefix,
+              nodeType: node.type as NodeType,
+              dimension: {
+                "@_width": elkNode.width!,
+                "@_height": elkNode.height!,
+              },
+              shapeIndex: node.data?.shape.index,
+              sourceEdgeIndexes: edges.flatMap((e) =>
+                e.source === nodeId && e.data?.dmnEdge ? 
[e.data.dmnEdge.index] : []
+              ),
+              targetEdgeIndexes: edges.flatMap((e) =>
+                e.target === nodeId && e.data?.dmnEdge ? 
[e.data.dmnEdge.index] : []
+              ),
+            },
+          });
+        });
+      }
+
+      // 9. Updating Decision Service divider lines after all nodes are 
repositioned and resized.
+      for (const [parentNodeId] of parentNodesById) {
+        const parentNode = 
s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(parentNodeId);
+        if (parentNode?.type !== NODE_TYPES.decisionService) {
+          continue;
+        }
+
+        const elkNode = autolayoutedElkNodesById.get(parentNodeId);
+        if (!elkNode) {
+          throw new Error(`Couldn't find Decision Service with id 
${parentNode.id} at the autolayouted nodes map`);
+        }
+
+        /**
+         * The second children of a Decision Service elkNode is a node 
representing the Encapsulated section.
+         * It's Y position will be exactly where the divider line should be.
+         */
+        const dividerLinerLocalYPosition = elkNode.children?.[1]?.y;
+        if (!dividerLinerLocalYPosition) {
+          throw new Error(
+            `Couldn't find second child (which represents the Encapuslated 
Decision section) of Decision Service with id ${parentNode.id} at the 
autolayouted nodes map`
+          );
+        }
+
+        updateDecisionServiceDividerLine({
+          definitions: s.dmn.model.definitions,
+          drdIndex: s.diagram.drdIndex,
+          dmnShapesByHref: s.computed(s).indexes().dmnShapesByHref,
+          drgElementIndex: parentNode.data.index,
+          shapeIndex: parentNode.data.shape.index,
+          snapGrid,
+          localYPosition: dividerLinerLocalYPosition,
+        });
+      }
+
+      // 10. Update the edges. Edges always go from top to bottom, removing 
waypoints.
+      for (const elkEdge of autolayouted.edges ?? []) {
+        if (elkEdge.id.includes(FAKE_MARKER)) {
+          continue;
+        }
+
+        const edge = 
s.computed(s).getDiagramData(externalModelsByNamespace).edgesById.get(elkEdge.id)!;
+
+        const sourceNode = 
s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(elkEdge.sources[0])!;
+        const targetNode = 
s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(elkEdge.targets[0])!;
+
+        // If the target is an external node, we don't have to create the edge.
+        if (targetNode.data.dmnObjectQName.prefix) {
+          continue;
+        }
+
+        addEdge({
+          definitions: s.dmn.model.definitions,
+          drdIndex: s.diagram.drdIndex,
+          edge: {
+            autoPositionedEdgeMarker: undefined,
+            type: edge.type as EdgeType,
+            targetHandle: PositionalNodeHandleId.Bottom,
+            sourceHandle: PositionalNodeHandleId.Top,
+          },
+          sourceNode: {
+            type: sourceNode.type as NodeType,
+            href: sourceNode.id,
+            data: sourceNode.data,
+            bounds: sourceNode.data.shape["dc:Bounds"]!,
+            shapeId: sourceNode.data.shape["@_id"],
+          },
+          targetNode: {
+            type: targetNode.type as NodeType,
+            href: targetNode.id,
+            data: targetNode.data,
+            bounds: targetNode.data.shape["dc:Bounds"]!,
+            index: targetNode.data.index,
+            shapeId: targetNode.data.shape["@_id"],
+          },
+          keepWaypoints: false,
+        });
+      }
+
+      // 11. After all nodes have been repositioned, it's time for the empty 
groups to be repositioned.
+    });
+  }, [dmnEditorStoreApi, externalModelsByNamespace]);
+
+  return (
+    <button className={"kie-dmn-editor--autolayout-panel-toggle-button"} 
onClick={onApply}>
+      <OptimizeIcon />
+    </button>
+  );
+}
+
+//
+
+export async function runElk(
+  nodes: Elk.ElkNode[],
+  edges: { id: string; sources: string[]; targets: string[] }[],
+  options: Elk.LayoutOptions = {}
+): Promise<{ isHorizontal: boolean; nodes: Elk.ElkNode[] | undefined; edges: 
Elk.ElkExtendedEdge[] | undefined }> {
+  const isHorizontal = options?.["elk.direction"] === "RIGHT";
+
+  const graph: Elk.ElkNode = {
+    id: "root",
+    layoutOptions: (window as any).elkOptions ?? options, // FIXME: Tiago --> 
Remove this. Debug only.

Review Comment:
   Leftovers?



##########
packages/dmn-editor/src/autolayout/AutolayoutButton.tsx:
##########
@@ -0,0 +1,559 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api";
+import { DC__Bounds } from 
"@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import OptimizeIcon from "@patternfly/react-icons/dist/js/icons/optimize-icon";
+import ELK, * as Elk from "elkjs/lib/elk.bundled.js";
+import * as React from "react";
+import { PositionalNodeHandleId } from 
"../diagram/connections/PositionalNodeHandles";
+import { EdgeType, NodeType } from "../diagram/connections/graphStructure";
+import { getAdjMatrix, traverse } from "../diagram/graph/graph";
+import { getContainmentRelationship } from "../diagram/maths/DmnMaths";
+import { DEFAULT_NODE_SIZES, MIN_NODE_SIZES } from 
"../diagram/nodes/DefaultSizes";
+import { NODE_TYPES } from "../diagram/nodes/NodeTypes";
+import { useExternalModels } from 
"../includedModels/DmnEditorDependenciesContext";
+import { addEdge } from "../mutations/addEdge";
+import { repositionNode } from "../mutations/repositionNode";
+import { resizeNode } from "../mutations/resizeNode";
+import { updateDecisionServiceDividerLine } from 
"../mutations/updateDecisionServiceDividerLine";
+import { useDmnEditorStoreApi } from "../store/StoreContext";
+
+const elk = new ELK();
+
+export const ELK_OPTIONS = {
+  "elk.algorithm": "layered",
+  "elk.direction": "UP",
+  // By making width a lot bigger than height, we make sure disjoint graph 
components are placed horizontally, never vertically
+  "elk.aspectRatio": "9999999999",
+  // spacing
+  "elk.spacing.nodeNode": "60",
+  "elk.spacing.componentComponent": "200",
+  "layered.spacing.edgeEdgeBetweenLayers": "0",
+  "layered.spacing.edgeNodeBetweenLayers": "0",
+  "layered.spacing.nodeNodeBetweenLayers": "100",
+  // edges
+  "elk.edgeRouting": "ORTHOGONAL",
+  "elk.layered.mergeEdges": "true", // we need this to make sure space is 
consistent between layers.
+  "elk.layered.mergeHierarchyEdges": "true",
+  // positioning
+  "elk.partitioning.activate": "true",
+  "elk.nodePlacement.favorStraightEdges": "true",
+  "elk.nodePlacement.bk.fixedAlignment": "LEFTDOWN",
+  "elk.nodePlacement.bk.edgeStraightening": "IMPROVE_STRAIGHTNESS",
+  //
+  "layering.strategy": "LONGEST_PATH_SOURCE",
+};
+
+const PARENT_NODE_ELK_OPTIONS = {
+  "elk.padding": "[left=60, top=60, right=80, bottom=60]",
+  "elk.spacing.componentComponent": "60",
+};
+
+export interface AutolayoutParentNode {
+  decisionServiceSection: "output" | "encapsulated" | "n/a";
+  elkNode: Elk.ElkNode;
+  contained: Set<string>;
+  dependents: Set<string>;
+  dependencies: Set<string>;
+  contains: (otherNode: { id: string; bounds: DC__Bounds | undefined }) => {
+    isInside: boolean;
+    decisionServiceSection: AutolayoutParentNode["decisionServiceSection"];
+  };
+  hasDependencyTo: (otherNode: { id: string }) => boolean;
+  isDependencyOf: (otherNode: { id: string }) => boolean;
+}
+
+const FAKE_MARKER = "__$FAKE$__";
+
+export function AutolayoutButton() {
+  const dmnEditorStoreApi = useDmnEditorStoreApi();
+  const { externalModelsByNamespace } = useExternalModels();
+
+  const onApply = React.useCallback(async () => {
+    const parentNodesById = new Map<string, AutolayoutParentNode>();
+    const nodeParentsById = new Map<string, Set<string>>();
+
+    /**
+      Used to tell ELK that dependencies of nodes' children should be 
considered the node's dependency too.
+      This allows us to not rely on INCLUDE_STRATEGY hierarchy handling on 
ELK, keeping disjoint graph components separate, rendering side-by-side.
+     */
+    const fakeEdgesForElk = new Set<Elk.ElkExtendedEdge>();
+
+    const state = dmnEditorStoreApi.getState();
+
+    const snapGrid = state.diagram.snapGrid;
+    const nodesById = 
state.computed(state).getDiagramData(externalModelsByNamespace).nodesById;
+    const edgesById = 
state.computed(state).getDiagramData(externalModelsByNamespace).edgesById;
+    const nodes = 
state.computed(state).getDiagramData(externalModelsByNamespace).nodes;
+    const edges = 
state.computed(state).getDiagramData(externalModelsByNamespace).edges;
+    const drgEdges = 
state.computed(state).getDiagramData(externalModelsByNamespace).drgEdges;
+
+    const adjMatrix = getAdjMatrix(drgEdges);
+
+    // 1. First we populate the `parentNodesById` map so that we know exactly 
what parent nodes we're dealing with. Decision Service nodes have two fake 
nodes to represent Output and Encapsulated sections.
+    for (const node of nodes) {
+      const dependencies = new Set<string>();
+      const dependents = new Set<string>();
+
+      if (node.data?.dmnObject?.__$$element === "decisionService") {
+        const outputs = new Set([...(node.data.dmnObject.outputDecision ?? 
[]).map((s) => s["@_href"])]);
+        const encapsulated = new 
Set([...(node.data.dmnObject.encapsulatedDecision ?? []).map((s) => 
s["@_href"])]);
+
+        const idOfFakeNodeForOutputSection = 
`${node.id}${FAKE_MARKER}dsOutput`;
+        const idOfFakeNodeForEncapsulatedSection = 
`${node.id}${FAKE_MARKER}dsEncapsulated`;
+
+        const dsSize = MIN_NODE_SIZES[NODE_TYPES.decisionService](snapGrid);
+        parentNodesById.set(node.id, {
+          elkNode: {
+            id: node.id,
+            width: dsSize["@_width"],
+            height: dsSize["@_height"],
+            children: [
+              {
+                id: idOfFakeNodeForOutputSection,
+                width: dsSize["@_width"],
+                height: dsSize["@_height"] / 2,
+                children: [],
+                layoutOptions: {
+                  ...ELK_OPTIONS,
+                  ...PARENT_NODE_ELK_OPTIONS,
+                },
+              },
+              {
+                id: idOfFakeNodeForEncapsulatedSection,
+                width: dsSize["@_width"],
+                height: dsSize["@_height"] / 2,
+                children: [],
+                layoutOptions: {
+                  ...ELK_OPTIONS,
+                  ...PARENT_NODE_ELK_OPTIONS,
+                },
+              },
+            ],
+            layoutOptions: {
+              "elk.algorithm": "layered",
+              "elk.direction": "UP",
+              "elk.aspectRatio": "9999999999",
+              "elk.partitioning.activate": "true",
+              "elk.spacing.nodeNode": "0",
+              "elk.spacing.componentComponent": "0",
+              "layered.spacing.edgeEdgeBetweenLayers": "0",
+              "layered.spacing.edgeNodeBetweenLayers": "0",
+              "layered.spacing.nodeNodeBetweenLayers": "0",
+              "elk.padding": "[left=0, top=0, right=0, bottom=0]",
+            },
+          },
+          decisionServiceSection: "output",
+          dependencies,
+          dependents,
+          contained: outputs,
+          contains: ({ id }) => ({
+            isInside: outputs.has(id) || encapsulated.has(id),
+            decisionServiceSection: outputs.has(id) ? "output" : 
encapsulated.has(id) ? "encapsulated" : "n/a",
+          }),
+          isDependencyOf: ({ id }) => dependents.has(id),
+          hasDependencyTo: ({ id }) => dependencies.has(id),
+        });
+
+        fakeEdgesForElk.add({
+          id: `${node.id}${FAKE_MARKER}fakeOutputEncapsulatedEdge`,
+          sources: [idOfFakeNodeForEncapsulatedSection],
+          targets: [idOfFakeNodeForOutputSection],
+        });
+      } else if (node.data?.dmnObject?.__$$element === "group") {
+        const groupSize = DEFAULT_NODE_SIZES[NODE_TYPES.group](snapGrid);
+        const groupBounds = node.data.shape["dc:Bounds"];
+        parentNodesById.set(node.id, {
+          decisionServiceSection: "n/a",
+          elkNode: {
+            id: node.id,
+            width: groupBounds?.["@_width"] ?? groupSize["@_width"],
+            height: groupBounds?.["@_height"] ?? groupSize["@_height"],
+            children: [],
+            layoutOptions: {
+              ...ELK_OPTIONS,
+              ...PARENT_NODE_ELK_OPTIONS,
+            },
+          },
+          dependencies,
+          dependents,
+          contained: new Set(),
+          contains: ({ id, bounds }) => ({
+            isInside: getContainmentRelationship({
+              bounds: bounds!,
+              container: groupBounds!,
+              snapGrid,
+              containerMinSizes: MIN_NODE_SIZES[NODE_TYPES.group],
+              boundsMinSizes: MIN_NODE_SIZES[nodesById.get(id)?.type as 
NodeType],
+            }).isInside,
+            decisionServiceSection: "n/a",
+          }),
+          isDependencyOf: ({ id }) => dependents.has(id),
+          hasDependencyTo: ({ id }) => dependencies.has(id),
+        });
+      }
+    }
+
+    // 2. Then we map all the nodes to elkNodes, including the parents. We 
mutate parents on the fly when iterating over the nodes list.
+    const elkNodes = nodes.flatMap((node) => {
+      const parent = parentNodesById.get(node.id);
+      if (parent) {
+        return [];
+      }
+
+      const defaultSize = DEFAULT_NODE_SIZES[node.type as NodeType](snapGrid);
+      const elkNode: Elk.ElkNode = {
+        id: node.id,
+        width: node.data.shape["dc:Bounds"]?.["@_width"] ?? 
defaultSize["@_width"],
+        height: node.data.shape["dc:Bounds"]?.["@_height"] ?? 
defaultSize["@_height"],
+        children: [],
+        layoutOptions: {
+          "partitioning.partition":
+            // Since textAnnotations and knowledgeSources are not related to 
the logic, we leave them at the bottom.
+            (node.type as NodeType) === NODE_TYPES.textAnnotation ||
+            (node.type as NodeType) === NODE_TYPES.knowledgeSource
+              ? "0"
+              : "1",
+        },
+      };
+
+      // FIXME: Tiago --> Improve performance here.
+      const parents = [...parentNodesById.values()].filter(
+        (p) => p.contains({ id: elkNode.id, bounds: 
node.data.shape["dc:Bounds"] }).isInside
+      );
+      if (parents.length > 0) {
+        const decisionServiceSection = parents[0].contains({
+          id: elkNode.id,
+          bounds: node.data.shape["dc:Bounds"],
+        }).decisionServiceSection;
+
+        // The only relationship that ELK will know about is the first 
matching container for this node.
+        if (decisionServiceSection === "n/a") {
+          parents[0].elkNode.children?.push(elkNode);
+        } else if (decisionServiceSection === "output") {
+          parents[0].elkNode.children?.[0].children?.push(elkNode);
+        } else if (decisionServiceSection === "encapsulated") {
+          parents[0].elkNode.children?.[1].children?.push(elkNode);
+        } else {
+          throw new Error(`Unknown decisionServiceSection 
${decisionServiceSection}`);
+        }
+
+        for (const p of parents) {
+          p.contained?.add(elkNode.id); // We need to keep track of nodes that 
are contained by multiple groups, but ELK will only know about one of those 
containment relationships.
+          nodeParentsById.set(node.id, new 
Set([...(nodeParentsById.get(node.id) ?? []), p.elkNode.id]));
+        }
+        return [];
+      }
+
+      return [elkNode];
+    });
+
+    // 3. After we have all containment relationships defined, we can proceed 
to resolving the hierarchical relationships.
+    for (const [_, parentNode] of parentNodesById) {
+      traverse(adjMatrix, parentNode.contained, [...parentNode.contained], 
"down", (n) => {
+        parentNode.dependencies.add(n);
+      });
+      traverse(adjMatrix, parentNode.contained, [...parentNode.contained], 
"up", (n) => {
+        parentNode.dependents.add(n);
+      });
+
+      const p = nodesById.get(parentNode.elkNode.id);
+      if (p?.type === NODE_TYPES.group && parentNode.elkNode.children?.length 
=== 0) {
+        continue; // Ignore empty group nodes.
+      } else {
+        elkNodes.push(parentNode.elkNode);
+      }
+    }
+
+    // 4. After we have all containment and hierarchical relationships 
defined, we can add the fake edges so that ELK creates the structure correctly.
+    for (const node of nodes) {
+      const parentNodes = [...parentNodesById.values()];
+
+      const dependents = parentNodes.filter((p) => p.hasDependencyTo({ id: 
node.id }));
+      for (const dependent of dependents) {
+        // Not all nodes are present in all DRD
+        if (nodesById.has(node.id) && nodesById.has(dependent.elkNode.id)) {
+          fakeEdgesForElk.add({
+            id: `${generateUuid()}${FAKE_MARKER}__fake`,
+            sources: [node.id],
+            targets: [dependent.elkNode.id],
+          });
+        }
+
+        for (const p of nodeParentsById.get(node.id) ?? []) {
+          // Not all nodes are present in all DRD
+          if (nodesById.has(p) && nodesById.has(dependent.elkNode.id)) {
+            fakeEdgesForElk.add({
+              id: `${generateUuid()}${FAKE_MARKER}__fake`,
+              sources: [p],
+              targets: [dependent.elkNode.id],
+            });
+          }
+        }
+      }
+
+      const dependencies = parentNodes.filter((p) => p.isDependencyOf({ id: 
node.id }));
+      for (const dependency of dependencies) {
+        // Not all nodes are present in all DRD
+        if (nodesById.has(node.id) && nodesById.has(dependency.elkNode.id)) {
+          fakeEdgesForElk.add({
+            id: `${generateUuid()}${FAKE_MARKER}__fake`,
+            sources: [dependency.elkNode.id],
+            targets: [node.id],
+          });
+        }
+
+        for (const p of nodeParentsById.get(node.id) ?? []) {
+          // Not all nodes are present in all DRD
+          if (nodesById.has(p) && nodesById.has(dependency.elkNode.id)) {
+            fakeEdgesForElk.add({
+              id: `${generateUuid()}${FAKE_MARKER}__fake`,
+              sources: [dependency.elkNode.id],
+              targets: [p],
+            });
+          }
+        }
+      }
+    }
+
+    // 5. Concatenate real and fake edges to pass to ELK.
+    const elkEdges = [
+      ...fakeEdgesForElk,
+      ...[...edgesById.values()].flatMap((e) => {
+        // Not all nodes are present in all DRD
+        if (nodesById.has(e.source) && nodesById.has(e.target)) {
+          return {
+            id: e.id,
+            sources: [e.source],
+            targets: [e.target],
+          };
+        } else {
+          return [];
+        }
+      }),
+    ];
+
+    // 6. Run ELK.
+    const autolayouted = await runElk(elkNodes, elkEdges, ELK_OPTIONS);
+
+    // 7. Update all nodes positions skipping empty groups, which will be 
positioned manually after all nodes are done being repositioned.
+    dmnEditorStoreApi.setState((s) => {
+      const autolayoutedElkNodesById = new Map<string, Elk.ElkNode>();
+
+      for (const topLevelElkNode of autolayouted.nodes ?? []) {
+        visitNodeAndNested(topLevelElkNode, { x: 100, y: 100 }, (elkNode, 
positionOffset) => {
+          if (elkNode.id.includes(FAKE_MARKER)) {
+            return;
+          }
+
+          autolayoutedElkNodesById.set(elkNode.id, elkNode);
+
+          const nodeId = elkNode.id;
+          const node = 
s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(nodeId)!;
+
+          repositionNode({
+            definitions: s.dmn.model.definitions,
+            drdIndex: s.diagram.drdIndex,
+            controlWaypointsByEdge: new Map(),
+            change: {
+              nodeType: node.type as NodeType,
+              type: "absolute",
+              position: {
+                x: elkNode.x! + positionOffset.x,
+                y: elkNode.y! + positionOffset.y,
+              },
+              selectedEdges: [...edgesById.keys()],
+              shapeIndex: node.data?.shape.index,
+              sourceEdgeIndexes: edges.flatMap((e) =>
+                e.source === nodeId && e.data?.dmnEdge ? 
[e.data.dmnEdge.index] : []
+              ),
+              targetEdgeIndexes: edges.flatMap((e) =>
+                e.target === nodeId && e.data?.dmnEdge ? 
[e.data.dmnEdge.index] : []
+              ),
+            },
+          });
+        });
+      }
+
+      // 8. Resize all nodes using the sizes calculated by ELK.
+      for (const topLevelElkNode of autolayouted.nodes ?? []) {
+        visitNodeAndNested(topLevelElkNode, { x: 0, y: 0 }, (elkNode) => {
+          if (elkNode.id.includes(FAKE_MARKER)) {
+            return;
+          }
+
+          const nodeId = elkNode.id;
+          const node = 
s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(nodeId)!;
+
+          resizeNode({
+            definitions: s.dmn.model.definitions,
+            drdIndex: s.diagram.drdIndex,
+            dmnShapesByHref: s.computed(s).indexes().dmnShapesByHref,
+            snapGrid,
+            change: {
+              index: node.data.index,
+              isExternal: !!node.data.dmnObjectQName.prefix,
+              nodeType: node.type as NodeType,
+              dimension: {
+                "@_width": elkNode.width!,
+                "@_height": elkNode.height!,
+              },
+              shapeIndex: node.data?.shape.index,
+              sourceEdgeIndexes: edges.flatMap((e) =>
+                e.source === nodeId && e.data?.dmnEdge ? 
[e.data.dmnEdge.index] : []
+              ),
+              targetEdgeIndexes: edges.flatMap((e) =>
+                e.target === nodeId && e.data?.dmnEdge ? 
[e.data.dmnEdge.index] : []
+              ),
+            },
+          });
+        });
+      }
+
+      // 9. Updating Decision Service divider lines after all nodes are 
repositioned and resized.
+      for (const [parentNodeId] of parentNodesById) {
+        const parentNode = 
s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(parentNodeId);
+        if (parentNode?.type !== NODE_TYPES.decisionService) {
+          continue;
+        }
+
+        const elkNode = autolayoutedElkNodesById.get(parentNodeId);
+        if (!elkNode) {
+          throw new Error(`Couldn't find Decision Service with id 
${parentNode.id} at the autolayouted nodes map`);
+        }
+
+        /**
+         * The second children of a Decision Service elkNode is a node 
representing the Encapsulated section.
+         * It's Y position will be exactly where the divider line should be.
+         */
+        const dividerLinerLocalYPosition = elkNode.children?.[1]?.y;
+        if (!dividerLinerLocalYPosition) {
+          throw new Error(
+            `Couldn't find second child (which represents the Encapuslated 
Decision section) of Decision Service with id ${parentNode.id} at the 
autolayouted nodes map`
+          );
+        }
+
+        updateDecisionServiceDividerLine({
+          definitions: s.dmn.model.definitions,
+          drdIndex: s.diagram.drdIndex,
+          dmnShapesByHref: s.computed(s).indexes().dmnShapesByHref,
+          drgElementIndex: parentNode.data.index,
+          shapeIndex: parentNode.data.shape.index,
+          snapGrid,
+          localYPosition: dividerLinerLocalYPosition,
+        });
+      }
+
+      // 10. Update the edges. Edges always go from top to bottom, removing 
waypoints.
+      for (const elkEdge of autolayouted.edges ?? []) {
+        if (elkEdge.id.includes(FAKE_MARKER)) {
+          continue;
+        }
+
+        const edge = 
s.computed(s).getDiagramData(externalModelsByNamespace).edgesById.get(elkEdge.id)!;
+
+        const sourceNode = 
s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(elkEdge.sources[0])!;
+        const targetNode = 
s.computed(s).getDiagramData(externalModelsByNamespace).nodesById.get(elkEdge.targets[0])!;
+
+        // If the target is an external node, we don't have to create the edge.
+        if (targetNode.data.dmnObjectQName.prefix) {
+          continue;
+        }
+
+        addEdge({
+          definitions: s.dmn.model.definitions,
+          drdIndex: s.diagram.drdIndex,
+          edge: {
+            autoPositionedEdgeMarker: undefined,
+            type: edge.type as EdgeType,
+            targetHandle: PositionalNodeHandleId.Bottom,
+            sourceHandle: PositionalNodeHandleId.Top,
+          },
+          sourceNode: {
+            type: sourceNode.type as NodeType,
+            href: sourceNode.id,
+            data: sourceNode.data,
+            bounds: sourceNode.data.shape["dc:Bounds"]!,
+            shapeId: sourceNode.data.shape["@_id"],
+          },
+          targetNode: {
+            type: targetNode.type as NodeType,
+            href: targetNode.id,
+            data: targetNode.data,
+            bounds: targetNode.data.shape["dc:Bounds"]!,
+            index: targetNode.data.index,
+            shapeId: targetNode.data.shape["@_id"],
+          },
+          keepWaypoints: false,
+        });
+      }
+
+      // 11. After all nodes have been repositioned, it's time for the empty 
groups to be repositioned.

Review Comment:
   Leftovers?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to