This is an automated email from the ASF dual-hosted git repository.

tiagobento pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools.git


The following commit(s) were added to refs/heads/main by this push:
     new ea6c23d5027 kie-issues#451: Implement autolayout on the new 
React-based DMN Editor for DMN files without Diagram information - Part 1 
(#2341)
ea6c23d5027 is described below

commit ea6c23d502720b836422bfc3868c364f0e793c2d
Author: Luiz João Motta <[email protected]>
AuthorDate: Fri May 24 00:34:42 2024 -0300

    kie-issues#451: Implement autolayout on the new React-based DMN Editor for 
DMN files without Diagram information - Part 1 (#2341)
---
 packages/dmn-editor/src/DmnEditor.tsx              |   4 +
 .../dmn-editor/src/autolayout/AutoLayoutHook.ts    | 220 +++++++++
 .../dmn-editor/src/autolayout/AutolayoutButton.tsx | 542 +--------------------
 packages/dmn-editor/src/autolayout/autoLayout.ts   | 396 +++++++++++++++
 packages/dmn-editor/src/diagram/Diagram.tsx        | 142 +++++-
 packages/dmn-editor/src/diagram/nodes/Nodes.tsx    |   2 +-
 packages/dmn-editor/src/mutations/resizeNode.ts    |   8 +-
 .../mutations/updateDecisionServiceDividerLine.ts  |  10 +-
 .../src/normalization/autoGenerateDrd.ts           | 211 ++++++++
 packages/dmn-editor/src/store/Store.ts             |  18 +-
 .../dmn-editor/stories/dev/DevWebApp.stories.tsx   |  27 +
 11 files changed, 1046 insertions(+), 534 deletions(-)

diff --git a/packages/dmn-editor/src/DmnEditor.tsx 
b/packages/dmn-editor/src/DmnEditor.tsx
index 9f60ae5972d..3232ab0b562 100644
--- a/packages/dmn-editor/src/DmnEditor.tsx
+++ b/packages/dmn-editor/src/DmnEditor.tsx
@@ -250,6 +250,10 @@ export const DmnEditorInternal = ({
       if (model === original(state.dmn.model)) {
         return;
       }
+
+      state.diagram.autoLayout.canAutoGenerateDrd =
+        model.definitions["dmndi:DMNDI"]?.["dmndi:DMNDiagram"] === undefined &&
+        model.definitions.drgElement !== undefined;
       state.dmn.model = normalize(model);
 
       dmnModelBeforeEditingRef.current = state.dmn.model;
diff --git a/packages/dmn-editor/src/autolayout/AutoLayoutHook.ts 
b/packages/dmn-editor/src/autolayout/AutoLayoutHook.ts
new file mode 100644
index 00000000000..1034b2dcced
--- /dev/null
+++ b/packages/dmn-editor/src/autolayout/AutoLayoutHook.ts
@@ -0,0 +1,220 @@
+/*
+ * 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 * as Elk from "elkjs/lib/elk.bundled.js";
+import { useCallback } from "react";
+import { PositionalNodeHandleId } from 
"../diagram/connections/PositionalNodeHandles";
+import { EdgeType, NodeType } from "../diagram/connections/graphStructure";
+import { NODE_TYPES } from "../diagram/nodes/NodeTypes";
+import { addEdge } from "../mutations/addEdge";
+import { repositionNode } from "../mutations/repositionNode";
+import { resizeNode } from "../mutations/resizeNode";
+import { updateDecisionServiceDividerLine } from 
"../mutations/updateDecisionServiceDividerLine";
+import { AutolayoutParentNode, FAKE_MARKER, visitNodeAndNested } from 
"./autoLayout";
+import { State } from "../store/Store";
+import { DmnDiagramNodeData } from "../diagram/nodes/Nodes";
+import { DmnDiagramEdgeData } from "../diagram/edges/Edges";
+import { DMNDI15__DMNShape } from 
"@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { XmlQName } from "@kie-tools/xml-parser-ts/dist/qNames";
+import * as RF from "reactflow";
+import { Normalized } from "../normalization/normalize";
+
+export function useAutoLayout() {
+  return useCallback(
+    ({
+      s,
+      autolayouted,
+      parentNodesById,
+      nodesById,
+      edgesById,
+      edges,
+      dmnShapesByHref,
+    }: {
+      s: State;
+      autolayouted: {
+        isHorizontal: boolean;
+        nodes: Elk.ElkNode[] | undefined;
+        edges: Elk.ElkExtendedEdge[] | undefined;
+      };
+      parentNodesById: Map<string, AutolayoutParentNode>;
+      nodesById: Map<string, RF.Node<DmnDiagramNodeData, string | undefined>>;
+      edgesById: Map<string, RF.Edge<DmnDiagramEdgeData>>;
+      edges: RF.Edge<DmnDiagramEdgeData>[];
+      dmnShapesByHref: Map<
+        string,
+        Normalized<DMNDI15__DMNShape> & {
+          index: number;
+          dmnElementRefQName: XmlQName;
+        }
+      >;
+    }) => {
+      // 7. Update all nodes positions skipping empty groups, which will be 
positioned manually after all nodes are done being repositioned.
+      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 = nodesById.get(nodeId)!;
+
+          repositionNode({
+            definitions: s.dmn.model.definitions,
+            drdIndex: s.computed(s).getDrdIndex(),
+            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 = nodesById.get(nodeId)!;
+
+          resizeNode({
+            definitions: s.dmn.model.definitions,
+            drdIndex: s.computed(s).getDrdIndex(),
+            __readonly_dmnShapesByHref: dmnShapesByHref,
+            snapGrid: s.diagram.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 = 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.computed(s).getDrdIndex(),
+          __readonly_dmnShapesByHref: dmnShapesByHref,
+          drgElementIndex: parentNode.data.index,
+          shapeIndex: parentNode.data.shape.index,
+          snapGrid: s.diagram.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 = edgesById.get(elkEdge.id)!;
+
+        const sourceNode = nodesById.get(elkEdge.sources[0])!;
+        const targetNode = 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.computed(s).getDrdIndex(),
+          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,
+        });
+      }
+    },
+    []
+  );
+}
diff --git a/packages/dmn-editor/src/autolayout/AutolayoutButton.tsx 
b/packages/dmn-editor/src/autolayout/AutolayoutButton.tsx
index e2d88e4aef3..af4d703a8ab 100644
--- a/packages/dmn-editor/src/autolayout/AutolayoutButton.tsx
+++ b/packages/dmn-editor/src/autolayout/AutolayoutButton.tsx
@@ -17,543 +17,53 @@
  * 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 OptimizeIcon from "@patternfly/react-icons/dist/js/icons/optimize-icon";
+import { useAutoLayout } from "./AutoLayoutHook";
+import { useDmnEditorStoreApi } from "../store/StoreContext";
+import { autoLayout } from "./autoLayout";
 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 { useDmnEditorStore, 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 isAlternativeInputDataShape = useDmnEditorStore((s) => 
s.computed(s).isAlternativeInputDataShape());
 
-  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 applyAutoLayout = useAutoLayout();
 
+  const onClick = React.useCallback(async () => {
     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,
-              isAlternativeInputDataShape,
-              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, isAlternativeInputDataShape });
-      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 as part of 
https://github.com/apache/incubator-kie-issues/issues/451.
-      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];
+    const isAlternativeInputDataShape = 
state.computed(state).isAlternativeInputDataShape();
+
+    const { autolayouted, parentNodesById } = await autoLayout({
+      snapGrid,
+      nodesById,
+      edgesById,
+      nodes,
+      drgEdges,
+      isAlternativeInputDataShape,
     });
 
-    // 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.computed(s).getDrdIndex(),
-            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.computed(s).getDrdIndex(),
-            dmnShapesByHref: s.computed(s).indexedDrd().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.computed(s).getDrdIndex(),
-          dmnShapesByHref: s.computed(s).indexedDrd().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.computed(s).getDrdIndex(),
-          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,
-        });
-      }
+      applyAutoLayout({
+        s,
+        dmnShapesByHref: s.computed(s).indexedDrd().dmnShapesByHref,
+        edges: s.computed(s).getDiagramData(externalModelsByNamespace).edges,
+        edgesById: 
s.computed(s).getDiagramData(externalModelsByNamespace).edgesById,
+        nodesById: 
s.computed(s).getDiagramData(externalModelsByNamespace).nodesById,
+        autolayouted: autolayouted,
+        parentNodesById: parentNodesById,
+      });
     });
-  }, [dmnEditorStoreApi, externalModelsByNamespace, 
isAlternativeInputDataShape]);
+  }, [applyAutoLayout, dmnEditorStoreApi, externalModelsByNamespace]);
 
   return (
-    <button className={"kie-dmn-editor--autolayout-panel-toggle-button"} 
onClick={onApply} title={"Autolayout (beta)"}>
+    <button className={"kie-dmn-editor--autolayout-panel-toggle-button"} 
onClick={onClick} title={"Autolayout (beta)"}>
       <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: options,
-    children: nodes,
-    edges,
-  };
-
-  const layoutedGraph = await elk.layout(graph);
-  return {
-    isHorizontal,
-    nodes: layoutedGraph.children,
-    edges: layoutedGraph.edges as any[],
-  };
-}
-
-function visitNodeAndNested(
-  elkNode: Elk.ElkNode,
-  positionOffset: { x: number; y: number },
-  visitor: (elkNode: Elk.ElkNode, positionOffset: { x: number; y: number }) => 
void
-) {
-  visitor(elkNode, positionOffset);
-  for (const nestedNode of elkNode.children ?? []) {
-    visitNodeAndNested(
-      nestedNode,
-      {
-        x: elkNode.x! + positionOffset.x,
-        y: elkNode.y! + positionOffset.y,
-      },
-      visitor
-    );
-  }
-}
diff --git a/packages/dmn-editor/src/autolayout/autoLayout.ts 
b/packages/dmn-editor/src/autolayout/autoLayout.ts
new file mode 100644
index 00000000000..ebb672d10c3
--- /dev/null
+++ b/packages/dmn-editor/src/autolayout/autoLayout.ts
@@ -0,0 +1,396 @@
+/*
+ * 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 ELK, * as Elk from "elkjs/lib/elk.bundled.js";
+import { NodeType } from "../diagram/connections/graphStructure";
+import { DrgEdge, 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 { SnapGrid } from "../store/Store";
+import { DmnDiagramEdgeData } from "../diagram/edges/Edges";
+import { Edge, Node } from "reactflow";
+import { DmnDiagramNodeData } from "../diagram/nodes/Nodes";
+
+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;
+}
+
+export const FAKE_MARKER = "__$FAKE$__";
+
+export async function autoLayout({
+  snapGrid,
+  nodesById,
+  edgesById,
+  nodes,
+  drgEdges,
+  isAlternativeInputDataShape,
+}: {
+  snapGrid: SnapGrid;
+  nodesById: Map<string, Node<DmnDiagramNodeData, string | undefined>>;
+  edgesById: Map<string, Edge<DmnDiagramEdgeData>>;
+  nodes: Node<DmnDiagramNodeData, string | undefined>[];
+  drgEdges: DrgEdge[];
+  isAlternativeInputDataShape: boolean;
+}) {
+  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 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,
+            isAlternativeInputDataShape,
+            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, 
isAlternativeInputDataShape });
+    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 as part of 
https://github.com/apache/incubator-kie-issues/issues/451.
+    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);
+  return {
+    autolayouted,
+    parentNodesById,
+  };
+}
+
+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: options,
+    children: nodes,
+    edges,
+  };
+
+  const layoutedGraph = await elk.layout(graph);
+  return {
+    isHorizontal,
+    nodes: layoutedGraph.children,
+    edges: layoutedGraph.edges as any[],
+  };
+}
+
+export function visitNodeAndNested(
+  elkNode: Elk.ElkNode,
+  positionOffset: { x: number; y: number },
+  visitor: (elkNode: Elk.ElkNode, positionOffset: { x: number; y: number }) => 
void
+) {
+  visitor(elkNode, positionOffset);
+  for (const nestedNode of elkNode.children ?? []) {
+    visitNodeAndNested(
+      nestedNode,
+      {
+        x: elkNode.x! + positionOffset.x,
+        y: elkNode.y! + positionOffset.y,
+      },
+      visitor
+    );
+  }
+}
diff --git a/packages/dmn-editor/src/diagram/Diagram.tsx 
b/packages/dmn-editor/src/diagram/Diagram.tsx
index 6d907005290..57b4d7f0bbc 100644
--- a/packages/dmn-editor/src/diagram/Diagram.tsx
+++ b/packages/dmn-editor/src/diagram/Diagram.tsx
@@ -34,6 +34,7 @@ import {
   EmptyStateBody,
   EmptyStateIcon,
   EmptyStatePrimary,
+  EmptyStateVariant,
 } from "@patternfly/react-core/dist/js/components/EmptyState";
 import { Label } from "@patternfly/react-core/dist/js/components/Label";
 import { Popover } from "@patternfly/react-core/dist/js/components/Popover";
@@ -53,7 +54,7 @@ import {
   ExternalNode,
   MIME_TYPE_FOR_DMN_EDITOR_EXTERNAL_NODES_FROM_INCLUDED_MODELS,
 } from "../externalNodes/ExternalNodesPanel";
-import { NodeNature, nodeNatures } from "../mutations/NodeNature";
+import { nodeNatures } from "../mutations/NodeNature";
 import { addConnectedNode } from "../mutations/addConnectedNode";
 import { addDecisionToDecisionService } from 
"../mutations/addDecisionToDecisionService";
 import { addEdge } from "../mutations/addEdge";
@@ -61,12 +62,12 @@ import { addShape } from "../mutations/addShape";
 import { addStandaloneNode } from "../mutations/addStandaloneNode";
 import { deleteDecisionFromDecisionService } from 
"../mutations/deleteDecisionFromDecisionService";
 import { EdgeDeletionMode, deleteEdge } from "../mutations/deleteEdge";
-import { NodeDeletionMode, canRemoveNodeFromDrdOnly, deleteNode } from 
"../mutations/deleteNode";
+import { NodeDeletionMode, deleteNode } from "../mutations/deleteNode";
 import { repositionNode } from "../mutations/repositionNode";
 import { resizeNode } from "../mutations/resizeNode";
 import { updateExpression } from "../mutations/updateExpression";
 import { OverlaysPanel } from "../overlaysPanel/OverlaysPanel";
-import { DiagramLhsPanel, SnapGrid } from "../store/Store";
+import { DiagramLhsPanel, SnapGrid, State } from "../store/Store";
 import { useDmnEditorStore, useDmnEditorStoreApi } from 
"../store/StoreContext";
 import { Unpacked } from "../tsExt/tsExt";
 import { buildXmlHref, parseXmlHref } from "../xml/xmlHrefs";
@@ -94,8 +95,6 @@ import {
   getContainmentRelationship,
   getHandlePosition,
   getNodeTypeFromDmnObject,
-  getBounds,
-  CONTAINER_NODES_DESIRABLE_PADDING,
 } from "./maths/DmnMaths";
 import { DEFAULT_NODE_SIZES, MIN_NODE_SIZES } from "./nodes/DefaultSizes";
 import { NODE_TYPES } from "./nodes/NodeTypes";
@@ -118,7 +117,11 @@ import {
 } from "../mutations/addExistingDecisionServiceToDrd";
 import { updateExpressionWidths } from "../mutations/updateExpressionWidths";
 import { DiagramCommands } from "./DiagramCommands";
+import { autoLayout } from "../autolayout/autoLayout";
+import { useAutoLayout } from "../autolayout/AutoLayoutHook";
+import { autoGenerateDrd } from "../normalization/autoGenerateDrd";
 import { Normalized, normalize } from "../normalization/normalize";
+import OptimizeIcon from "@patternfly/react-icons/dist/js/icons/optimize-icon";
 
 const isFirefox = typeof (window as any).InstallTrigger !== "undefined"; // 
See 
https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browsers
 
@@ -638,7 +641,7 @@ export const Diagram = React.forwardRef<DiagramRef, { 
container: React.RefObject
                     resizeNode({
                       definitions: state.dmn.model.definitions,
                       drdIndex: state.computed(state).getDrdIndex(),
-                      dmnShapesByHref: 
state.computed(state).indexedDrd().dmnShapesByHref,
+                      __readonly_dmnShapesByHref: 
state.computed(state).indexedDrd().dmnShapesByHref,
                       snapGrid: state.diagram.snapGrid,
                       change: {
                         isExternal: !!node.data.dmnObjectQName.prefix,
@@ -1122,9 +1125,12 @@ export const Diagram = React.forwardRef<DiagramRef, { 
container: React.RefObject
     const isEmptyStateShowing =
       showEmptyState && nodes.length === 0 && 
drgElementsWithoutVisualRepresentationOnCurrentDrdLength === 0;
 
+    const canAutoGenerateDrd = useDmnEditorStore((s) => 
s.diagram.autoLayout.canAutoGenerateDrd);
+
     return (
       <>
-        {isEmptyStateShowing && <DmnDiagramEmptyState 
setShowEmptyState={setShowEmptyState} />}
+        {nodes.length === 0 && canAutoGenerateDrd && <DmnDiagramWithoutDrd />}
+        {isEmptyStateShowing && !canAutoGenerateDrd && <DmnDiagramEmptyState 
setShowEmptyState={setShowEmptyState} />}
         <DiagramContainerContextProvider container={container}>
           <svg style={{ position: "absolute", top: 0, left: 0 }}>
             <EdgeMarkers />
@@ -1193,6 +1199,128 @@ export const Diagram = React.forwardRef<DiagramRef, { 
container: React.RefObject
   }
 );
 
+function DmnDiagramWithoutDrd() {
+  const dmnEditorStoreApi = useDmnEditorStoreApi();
+  const { externalModelsByNamespace } = useExternalModels();
+  const applyAutoLayout = useAutoLayout();
+
+  return (
+    <Bullseye
+      style={{
+        position: "absolute",
+        width: "100%",
+        pointerEvents: "none",
+        zIndex: 1,
+        height: "auto",
+        marginTop: "120px",
+      }}
+    >
+      <div className={"kie-dmn-editor--diagram-empty-state"}>
+        <Button
+          title={"Close"}
+          style={{
+            position: "absolute",
+            top: "8px",
+            right: 0,
+          }}
+          variant={ButtonVariant.plain}
+          icon={<TimesIcon />}
+          onClick={() => {
+            dmnEditorStoreApi.setState((s) => {
+              s.diagram.autoLayout.canAutoGenerateDrd = false;
+            });
+          }}
+        />
+
+        <EmptyState variant={EmptyStateVariant.small}>
+          <EmptyStateIcon icon={BlueprintIcon} />
+          <Title size={"md"} headingLevel={"h4"}>
+            Empty Diagram
+          </Title>
+          <EmptyStateBody>
+            The current DMN does not have any Diagram associated with it. Do 
you want to auto-generate it?
+          </EmptyStateBody>
+
+          <EmptyStatePrimary>
+            <Button
+              variant={ButtonVariant.link}
+              icon={<OptimizeIcon />}
+              onClick={async () => {
+                const { computed, ...state } = dmnEditorStoreApi.getState();
+                const dereferencedState: State = { computed, 
...JSON.parse(JSON.stringify(state)) };
+
+                const externalModelTypesByNamespace = dereferencedState
+                  .computed(dereferencedState)
+                  .getExternalModelTypesByNamespace(externalModelsByNamespace);
+
+                autoGenerateDrd({
+                  model: dereferencedState.dmn.model,
+                  diagram: dereferencedState.diagram,
+                  externalModelsByNamespace,
+                  externalModelTypesByNamespace,
+                });
+
+                const snapGrid = dereferencedState.diagram.snapGrid;
+                const nodesById = dereferencedState
+                  .computed(dereferencedState)
+                  .getDiagramData(externalModelsByNamespace).nodesById;
+                const edgesById = dereferencedState
+                  .computed(dereferencedState)
+                  .getDiagramData(externalModelsByNamespace).edgesById;
+                const nodes = dereferencedState
+                  .computed(dereferencedState)
+                  .getDiagramData(externalModelsByNamespace).nodes;
+                const edges = dereferencedState
+                  .computed(dereferencedState)
+                  .getDiagramData(externalModelsByNamespace).edges;
+                const drgEdges = dereferencedState
+                  .computed(dereferencedState)
+                  .getDiagramData(externalModelsByNamespace).drgEdges;
+                const isAlternativeInputDataShape = dereferencedState
+                  .computed(dereferencedState)
+                  .isAlternativeInputDataShape();
+                const dmnShapesByHref = 
dereferencedState.computed(dereferencedState).indexedDrd().dmnShapesByHref;
+
+                // Auto layout the new DRD
+                const { autolayouted, parentNodesById } = await autoLayout({
+                  snapGrid,
+                  nodesById,
+                  edgesById,
+                  nodes,
+                  drgEdges,
+                  isAlternativeInputDataShape,
+                });
+
+                dmnEditorStoreApi.setState((s) => {
+                  s.diagram.autoLayout.canAutoGenerateDrd = false;
+                  applyAutoLayout({
+                    s: dereferencedState,
+                    dmnShapesByHref,
+                    edges: edges,
+                    edgesById: edgesById,
+                    nodesById: nodesById,
+                    autolayouted: autolayouted,
+                    parentNodesById: parentNodesById,
+                  });
+                  s.dmn.model = dereferencedState.dmn.model;
+                });
+              }}
+            >
+              Auto-generate Diagram
+            </Button>
+          </EmptyStatePrimary>
+
+          <br />
+          <EmptyStateBody style={{ fontSize: "12px", wordBreak: "break-word" 
}}>
+            Auto generating the diagram will automatically place the nodes 
with the default size and shape. You can also
+            manually build your diagram using the &quot;DRG Nodes&quot; option 
from the palette.
+          </EmptyStateBody>
+        </EmptyState>
+      </div>
+    </Bullseye>
+  );
+}
+
 function DmnDiagramEmptyState({
   setShowEmptyState,
 }: {
diff --git a/packages/dmn-editor/src/diagram/nodes/Nodes.tsx 
b/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
index a826ebeebce..b95c96b4bd5 100644
--- a/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
+++ b/packages/dmn-editor/src/diagram/nodes/Nodes.tsx
@@ -957,7 +957,7 @@ export const DecisionServiceNode = React.memo(
             updateDecisionServiceDividerLine({
               definitions: state.dmn.model.definitions,
               drdIndex: state.computed(state).getDrdIndex(),
-              dmnShapesByHref: 
state.computed(state).indexedDrd().dmnShapesByHref,
+              __readonly_dmnShapesByHref: 
state.computed(state).indexedDrd().dmnShapesByHref,
               drgElementIndex: index,
               shapeIndex: shape.index,
               localYPosition: e.y,
diff --git a/packages/dmn-editor/src/mutations/resizeNode.ts 
b/packages/dmn-editor/src/mutations/resizeNode.ts
index 8d6a1dda662..6199148f5d3 100644
--- a/packages/dmn-editor/src/mutations/resizeNode.ts
+++ b/packages/dmn-editor/src/mutations/resizeNode.ts
@@ -38,13 +38,13 @@ import { Normalized } from "../normalization/normalize";
 export function resizeNode({
   definitions,
   drdIndex,
-  dmnShapesByHref,
+  __readonly_dmnShapesByHref,
   snapGrid,
   change,
 }: {
   definitions: Normalized<DMN15__tDefinitions>;
   drdIndex: number;
-  dmnShapesByHref: Map<string, Normalized<DMNDI15__DMNShape> & { index: number 
}>;
+  __readonly_dmnShapesByHref: Map<string, Normalized<DMNDI15__DMNShape> & { 
index: number }>;
   snapGrid: SnapGrid;
   change: {
     nodeType: NodeType;
@@ -77,7 +77,7 @@ export function resizeNode({
     // We ignore handling the contents of the Decision Service when it is 
external
     if (!change.isExternal) {
       ds.encapsulatedDecision?.forEach((ed) => {
-        const edShape = dmnShapesByHref.get(ed["@_href"])!;
+        const edShape = __readonly_dmnShapesByHref.get(ed["@_href"])!;
         const dim = snapShapeDimensions(snapGrid, edShape, 
MIN_NODE_SIZES[NODE_TYPES.decision]({ snapGrid }));
         const pos = snapShapePosition(snapGrid, edShape);
         if (pos.x + dim.width > limit.x) {
@@ -91,7 +91,7 @@ export function resizeNode({
 
       // Output Decisions don't limit the resizing vertically, only 
horizontally.
       ds.outputDecision?.forEach((ed) => {
-        const edShape = dmnShapesByHref.get(ed["@_href"])!;
+        const edShape = __readonly_dmnShapesByHref.get(ed["@_href"])!;
         const dim = snapShapeDimensions(snapGrid, edShape, 
MIN_NODE_SIZES[NODE_TYPES.decision]({ snapGrid }));
         const pos = snapShapePosition(snapGrid, edShape);
         if (pos.x + dim.width > limit.x) {
diff --git 
a/packages/dmn-editor/src/mutations/updateDecisionServiceDividerLine.ts 
b/packages/dmn-editor/src/mutations/updateDecisionServiceDividerLine.ts
index 86ba8bd5e2a..5b0b3d2c684 100644
--- a/packages/dmn-editor/src/mutations/updateDecisionServiceDividerLine.ts
+++ b/packages/dmn-editor/src/mutations/updateDecisionServiceDividerLine.ts
@@ -37,7 +37,7 @@ export const DECISION_SERVICE_DIVIDER_LINE_PADDING = 100;
 export function updateDecisionServiceDividerLine({
   definitions,
   drdIndex,
-  dmnShapesByHref,
+  __readonly_dmnShapesByHref,
   shapeIndex,
   localYPosition,
   drgElementIndex,
@@ -45,7 +45,7 @@ export function updateDecisionServiceDividerLine({
 }: {
   definitions: Normalized<DMN15__tDefinitions>;
   drdIndex: number;
-  dmnShapesByHref: Map<string, Normalized<DMNDI15__DMNShape> & { index: number 
}>;
+  __readonly_dmnShapesByHref: Map<string, Normalized<DMNDI15__DMNShape> & { 
index: number }>;
   shapeIndex: number;
   localYPosition: number;
   drgElementIndex: number;
@@ -72,13 +72,13 @@ export function updateDecisionServiceDividerLine({
 
   const upperLimit = (ds.outputDecision ?? []).reduce((acc, od) => {
     const v =
-      snapShapePosition(snapGrid, dmnShapesByHref.get(od["@_href"])!).y +
-      snapShapeDimensions(snapGrid, dmnShapesByHref.get(od["@_href"])!, 
decisionMinSizes).height;
+      snapShapePosition(snapGrid, 
__readonly_dmnShapesByHref.get(od["@_href"])!).y +
+      snapShapeDimensions(snapGrid, 
__readonly_dmnShapesByHref.get(od["@_href"])!, decisionMinSizes).height;
     return v > acc ? v : acc;
   }, snappedPosition.y + DECISION_SERVICE_DIVIDER_LINE_PADDING);
 
   const lowerLimit = (ds.encapsulatedDecision ?? []).reduce((acc, ed) => {
-    const v = snapShapePosition(snapGrid, 
dmnShapesByHref.get(ed["@_href"])!).y;
+    const v = snapShapePosition(snapGrid, 
__readonly_dmnShapesByHref.get(ed["@_href"])!).y;
     return v < acc ? v : acc;
   }, snappedPosition.y + snappedDimensions.height - 
DECISION_SERVICE_DIVIDER_LINE_PADDING);
 
diff --git a/packages/dmn-editor/src/normalization/autoGenerateDrd.ts 
b/packages/dmn-editor/src/normalization/autoGenerateDrd.ts
new file mode 100644
index 00000000000..2053b0c7f9f
--- /dev/null
+++ b/packages/dmn-editor/src/normalization/autoGenerateDrd.ts
@@ -0,0 +1,211 @@
+/*
+ * 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 { ExternalDmnsIndex, ExternalModelsIndex, ExternalPmmlsIndex } from 
"../DmnEditor";
+import { computeDiagramData } from "../store/computed/computeDiagramData";
+import { State } from "../store/Store";
+import { MIN_NODE_SIZES } from "../diagram/nodes/DefaultSizes";
+import { getNodeTypeFromDmnObject } from "../diagram/maths/DmnMaths";
+import { DMN15__tDefinitions } from 
"@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types";
+import { DmnLatestModel } from "@kie-tools/dmn-marshaller";
+import { parseXmlHref } from "../xml/xmlHrefs";
+import { computeIndexedDrd } from "../store/computed/computeIndexes";
+import { getDefaultDrdName } from "../mutations/addOrGetDrd";
+import { addShape } from "../mutations/addShape";
+import { addEdge } from "../mutations/addEdge";
+import { EdgeType, NodeType } from "../diagram/connections/graphStructure";
+import { PositionalNodeHandleId } from 
"../diagram/connections/PositionalNodeHandles";
+import { Normalized } from "./normalize";
+
+export async function autoGenerateDrd(args: {
+  model: State["dmn"]["model"];
+  diagram: State["diagram"];
+  externalModelsByNamespace: ExternalModelsIndex | undefined;
+  externalModelTypesByNamespace: {
+    dmns: ExternalDmnsIndex;
+    pmmls: ExternalPmmlsIndex;
+  };
+}) {
+  // Create DRD
+  args.model.definitions["dmndi:DMNDI"] = {
+    ...args.model.definitions["dmndi:DMNDI"],
+    "dmndi:DMNDiagram": [
+      {
+        "@_id": generateUuid(),
+        "@_name": getDefaultDrdName({ drdIndex: 0 }),
+        "@_useAlternativeInputDataShape": false,
+        "dmndi:DMNDiagramElement": [],
+        "di:extension": { "kie:ComponentsWidthsExtension": { 
"kie:ComponentWidths": [{}] } },
+      },
+    ],
+  };
+
+  // 1. Add shapes from current DRG
+  args.model.definitions.drgElement?.forEach((drgElement) => {
+    const nodeType = getNodeTypeFromDmnObject(drgElement) ?? "node_unknown";
+    const minNodeSize = MIN_NODE_SIZES[nodeType]({
+      snapGrid: {
+        isEnabled: true,
+        x: 20,
+        y: 20,
+      },
+      isAlternativeInputDataShape: false,
+    });
+
+    addShape({
+      definitions: args.model.definitions,
+      drdIndex: 0,
+      nodeType: nodeType,
+      shape: {
+        "@_id": generateUuid(),
+        "@_dmnElementRef": drgElement["@_id"]!,
+        "dc:Bounds": {
+          "@_x": 0,
+          "@_y": 0,
+          ...minNodeSize,
+        },
+      },
+    });
+  });
+
+  // 2. Add shapes from external models;
+  const definedNamespaces = new Map(
+    Object.keys(args.model.definitions)
+      .filter((keys: keyof Normalized<DMN15__tDefinitions>) => 
String(keys).startsWith("@_xmlns:"))
+      .map((xmlnsKey: keyof Normalized<DMN15__tDefinitions>) => [
+        args.model.definitions[xmlnsKey],
+        xmlnsKey.split("@_xmlns:")[1],
+      ])
+  );
+
+  const updateIndexedDrdWithNodes = 
computeIndexedDrd(args.model.definitions["@_namespace"], 
args.model.definitions, 0);
+  const { nodesById: updatedNodesByIdWithNodes, drgEdges: 
updatedDrgEdgesWithNodes } = computeDiagramData(
+    args.diagram,
+    args.model.definitions,
+    args.externalModelTypesByNamespace,
+    updateIndexedDrdWithNodes,
+    false
+  );
+
+  // Search on edges for any node that isn't on the nodesByIdMap;
+  // Only external nodes should be added to the externalNodesHref;
+  const externalNodesHref = updatedDrgEdgesWithNodes.reduce((acc, drgEdge) => {
+    if (!updatedNodesByIdWithNodes.has(drgEdge.sourceId)) {
+      acc.add(drgEdge.sourceId);
+    }
+    if (!updatedNodesByIdWithNodes.has(drgEdge.targetId)) {
+      acc.add(drgEdge.targetId);
+    }
+    return acc;
+  }, new Set<string>());
+
+  // Add external shapes
+  externalNodesHref.forEach((href) => {
+    const { namespace, id } = parseXmlHref(href);
+    if (namespace) {
+      const externalModel = args.externalModelsByNamespace?.[namespace];
+      if (externalModel && (externalModel.model as 
Normalized<DmnLatestModel>).definitions) {
+        const drgElements = (externalModel.model as 
Normalized<DmnLatestModel>).definitions.drgElement;
+        const drgElement = drgElements?.filter((drgElement) => 
drgElement["@_id"] === id);
+
+        const nodeType = getNodeTypeFromDmnObject(drgElement![0]) ?? 
"node_unknown";
+        const minNodeSize = MIN_NODE_SIZES[nodeType]({
+          snapGrid: {
+            isEnabled: true,
+            x: 20,
+            y: 20,
+          },
+          isAlternativeInputDataShape: false,
+        });
+
+        addShape({
+          definitions: args.model.definitions,
+          drdIndex: 0,
+          nodeType: nodeType,
+          shape: {
+            "@_id": generateUuid(),
+            "@_dmnElementRef": `${definedNamespaces.get(namespace)}:${id}`,
+            "dc:Bounds": {
+              "@_x": 0,
+              "@_y": 0,
+              ...minNodeSize,
+            },
+          },
+        });
+      }
+    }
+  });
+
+  // 3. Add edges
+  const updatedIndexedDrdWithExternalNodes = computeIndexedDrd(
+    args.model.definitions["@_namespace"],
+    args.model.definitions,
+    0
+  );
+  const {
+    nodesById: updatedNodesByIdWithExternalNodes,
+    edgesById: updatedEdgesByIdWithExternalNodes,
+    drgEdges: updatedDrgEdgesWithExternalNodes,
+  } = computeDiagramData(
+    args.diagram,
+    args.model.definitions,
+    args.externalModelTypesByNamespace,
+    updatedIndexedDrdWithExternalNodes,
+    false
+  );
+
+  for (const drgEdge of updatedDrgEdgesWithExternalNodes) {
+    const edge = updatedEdgesByIdWithExternalNodes.get(drgEdge.id);
+    const sourceNode = updatedNodesByIdWithExternalNodes.get(drgEdge.sourceId);
+    const targetNode = updatedNodesByIdWithExternalNodes.get(drgEdge.targetId);
+
+    // Avoid missing nodes. Possible cause is an external model which couldn't 
be found.
+    if (!edge || !sourceNode || !targetNode) {
+      continue;
+    }
+
+    addEdge({
+      definitions: args.model.definitions,
+      drdIndex: 0,
+      keepWaypoints: false,
+      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"],
+      },
+    });
+  }
+}
diff --git a/packages/dmn-editor/src/store/Store.ts 
b/packages/dmn-editor/src/store/Store.ts
index 85657b44f8f..21f04b3bf74 100644
--- a/packages/dmn-editor/src/store/Store.ts
+++ b/packages/dmn-editor/src/store/Store.ts
@@ -88,6 +88,9 @@ export interface State {
     tab: DmnEditorTab;
   };
   diagram: {
+    autoLayout: {
+      canAutoGenerateDrd: boolean;
+    };
     __unsafeDrdIndex: number;
     edgeIdBeingUpdated: string | undefined;
     dropTargetNode: DropTargetNode;
@@ -187,6 +190,9 @@ export const defaultStaticState = (): Omit<State, "dmn" | 
"dispatch" | "computed
     expandedItemComponentIds: [],
   },
   diagram: {
+    autoLayout: {
+      canAutoGenerateDrd: false,
+    },
     __unsafeDrdIndex: 0,
     edgeIdBeingUpdated: undefined,
     dropTargetNode: undefined,
@@ -221,12 +227,22 @@ export const defaultStaticState = (): Omit<State, "dmn" | 
"dispatch" | "computed
 });
 
 export function createDmnEditorStore(model: DmnLatestModel, computedCache: 
ComputedStateCache<Computed>) {
+  const { diagram, ...defaultState } = defaultStaticState();
   return create(
     immer<State>(() => ({
       dmn: {
         model: normalize(model),
       },
-      ...defaultStaticState(),
+      ...defaultState,
+      diagram: {
+        ...diagram,
+        // A model without DRD and with DRG element can be auto generated
+        autoLayout: {
+          canAutoGenerateDrd:
+            model.definitions["dmndi:DMNDI"]?.["dmndi:DMNDiagram"] === 
undefined &&
+            model.definitions.drgElement !== undefined,
+        },
+      },
       dispatch(s: State) {
         return {
           dmn: {
diff --git a/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx 
b/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx
index 3a9c41928ca..262231b2523 100644
--- a/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx
+++ b/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx
@@ -31,6 +31,7 @@ import { loanPreQualificationDmn } from 
"../useCases/loanPreQualification/LoanPr
 import { DmnEditorWrapper } from "../dmnEditorStoriesWrapper";
 import {
   DmnEditorProps,
+  DmnEditorRef,
   ExternalModelsIndex,
   OnDmnModelChange,
   OnRequestExternalModelByPath,
@@ -40,6 +41,30 @@ import {
 
 const initialModel = generateEmptyDmn15();
 
+const emptyDrd = `<?xml version="1.0" encoding="UTF-8" ?>
+<definitions xmlns="https://www.omg.org/spec/DMN/20230324/MODEL/"; 
xmlns:dmndi="https://www.omg.org/spec/DMN/20230324/DMNDI/"; 
xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/"; 
xmlns:di="http://www.omg.org/spec/DMN/20180521/DI/"; 
xmlns:kie="https://kie.org/dmn/extensions/1.0"; 
xmlns:included0="https://kie.org/dmn/_125A5475-65CE-4574-822C-9CB2268F1393"; 
expressionLanguage="https://www.omg.org/spec/DMN/20230324/FEEL/"; 
namespace="https://kie.org/dmn/_2B849D68-E816-42F9-898A-1938B5D6B297"; id="_ 
[...]
+  <import id="_8079D96B-F569-4F4E-830B-7462B6AFE492" name="u" 
importType="http://www.omg.org/spec/DMN/20180521/MODEL/"; 
namespace="https://kie.org/dmn/_125A5475-65CE-4574-822C-9CB2268F1393"; 
locationURI="./Untitled-4.dmn" />
+  <inputData name="My Input" id="_9392B01E-8C6B-4E29-9CC4-21C16EFB2F6B">
+    <variable name="My Input" id="_9483BABF-708A-4357-AD78-18C7A770E292" 
typeRef="&lt;Undefined&gt;" />
+  </inputData>
+  <decision name="My Decision" id="_83A0C6FA-0951-4E1E-9DF1-74A9D2A95E98">
+    <variable id="_01C70F45-2955-474A-9FAC-14967ABAF475" 
typeRef="&lt;Undefined&gt;" name="My Decision" />
+    <informationRequirement id="_A7EAFD5D-BDF7-4D09-81A9-9C22711847C0">
+      <requiredInput 
href="https://kie.org/dmn/_125A5475-65CE-4574-822C-9CB2268F1393#_D9138F6E-E9DA-47AB-8DEF-5CD531B94ABE";
 />
+    </informationRequirement>
+    <informationRequirement id="_E4FE78BB-996B-46C4-9F9B-018163E9017A">
+      <requiredInput href="#_9392B01E-8C6B-4E29-9CC4-21C16EFB2F6B" />
+    </informationRequirement>
+    <informationRequirement id="_E52D5C34-172E-4E33-B2FE-7B2A7AFDF52C">
+      <requiredInput href="#_4072ADC3-E7CF-4D22-8179-7494EE22157C" />
+    </informationRequirement>
+  </decision>
+  <inputData name="Another Input" id="_4072ADC3-E7CF-4D22-8179-7494EE22157C">
+    <variable name="Another Input" id="_7490876B-8FA9-4FEC-B078-7563EF04F52B" 
typeRef="&lt;Undefined&gt;" />
+  </inputData>
+</definitions>
+`;
+
 function DevWebApp(args: DmnEditorProps) {
   const [state, setState] = useState<{
     marshaller: DmnMarshaller;
@@ -168,6 +193,8 @@ function DevWebApp(args: DmnEditorProps) {
                   <button onClick={() => 
onSelectModel(generateEmptyDmn15())}>Empty</button>
                   &nbsp; &nbsp;
                   <button onClick={() => 
onSelectModel(loanPreQualificationDmn)}>Loan Pre Qualification</button>
+                  &nbsp; &nbsp;
+                  <button onClick={() => onSelectModel(emptyDrd)}>Empty 
DRD</button>
                   &nbsp; &nbsp; | &nbsp; &nbsp;
                   <button disabled={!isUndoEnabled} style={{ opacity: 
isUndoEnabled ? 1 : 0.5 }} onClick={undo}>
                     {`Undo (${state.pointer})`}


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

Reply via email to