bpmn-visualization-js icon indicating copy to clipboard operation
bpmn-visualization-js copied to clipboard

[FEAT] Render start event as Marker of Collapsed Event Sub-Processes

Open csouchet opened this issue 5 years ago • 1 comments
trafficstars

Describe the solution you'd like Update the MxGraph shape for Collapsed Event Sub-Processes, to render the start event marker.

What it's already done All the elements of a sub process are parsed (in ProcessConverter), but not displayed if the sub process is collapsed (bpmn-model-registry.ts & BpmnRenderer). To add a new style on a BPMN element, see StyleComputer. For more information, see the developpement documentation.

BPMN specification The Start Event of an Event Sub-Process MUST have a defined trigger. The trigger for a Start Event is designed to show the general mechanisms that will instantiate that particular Process.

A Start Event can also initiate an inline Event Sub-Process (see page 174). In that case, the same Event types as for boundary Events are allowed (see Table 10.86), namely: Message, Timer, Escalation, Error, Compensation, Conditional, Signal, Multiple, and Parallel.

If the Event Sub-Process is collapsed, then its Start Event will be used as a marker in the upper left corner of the shape (see Figure 10.30).

image

Note for implementation See the development documentation for details.

At parsing time, we already know that we have a collapsed event subprocess and we know the inner elements. It will probably easier for the rendering part that we compute the start event kind in this case during parsing. This will make the rendering easier: get the information and do the display.

Please start a discussion with a implementation proposal prior starting the actual implementation.

⚠️ Check the contribution guidelines for BPMN support

csouchet avatar Mar 13 '20 09:03 csouchet

I just checked the current code, and it's not easy to do this.

There are two possible approaches to achieve this:

  1. We can change the parsing implementation to add a new property for the event marker kind when the subprocess is collapsed into ShapeBpmnSubProcess of type ShapeBpmnEventDefinitionKind. We also need to modify computeActivityShapeStyle() to add this kind of this event to the style. However, it may be complicated to draw the different event shapes according to their type in SubProcessShape while reusing event-shapes.ts.

  2. We can modify the toRenderedModel function of BpmnModelRegistry, by adding the new proptery subprocessStartEventMarkers.

Here's an example of the modified code for BpmnModelRegistry:

function toRenderedModel(bpmnModel: BpmnModel): RenderedModel {
  const collapsedSubProcessIds: string[] = bpmnModel.flowNodes
    .filter(shape => {
      const bpmnElement = shape.bpmnElement;
      return ShapeUtil.isSubProcess(bpmnElement.kind) && (bpmnElement as ShapeBpmnSubProcess).markers.includes(ShapeBpmnMarkerKind.EXPAND);
    })
    .map(shape => shape.bpmnElement.id);

  const subprocesses: Shape[] = [];
  const boundaryEvents: Shape[] = [];
  const otherFlowNodes: Shape[] = [];
  const subprocessStartEventMarkers: Shape[] = [];
  bpmnModel.flowNodes.forEach(shape => {
    const kind = shape.bpmnElement.kind;
    if (ShapeUtil.isSubProcess(kind)) {
      subprocesses.push(shape);
    } else if (ShapeUtil.isBoundaryEvent(kind)) {
      boundaryEvents.push(shape);
    } else if (!collapsedSubProcessIds.includes(shape.bpmnElement.parentId)) {
      otherFlowNodes.push(shape);
    } else if (collapsedSubProcessIds.includes(shape.bpmnElement.parentId) && ShapeUtil.isStartEvent(shape.bpmnElement.kind)) {
      subprocessStartEventMarkers.push(shape);
    }
  });

  return {
    ...bpmnModel,
    boundaryEvents,
    otherFlowNodes,
    subprocesses,
    subprocessStartEventMarkers,
  };
}

And BpmnRenderer by adding a child vertex for this event directly when we create the vertex of a collapsed event sub-process:

  private insertShapesAndEdges({ pools, lanes, subprocesses, subprocessStartEventMarkers, otherFlowNodes, boundaryEvents, edges }: RenderedModel): void {
    this.graph.batchUpdate(() => {
      this.graph.getModel().clear(); // ensure to remove manual changes or already loaded graphs
      this.insertShapes(pools);
      this.insertShapes(lanes);
      this.insertShapes(subprocesses, subprocessStartEventMarkers);
      this.insertShapes(otherFlowNodes);
      // last shape as the boundary event parent must be in the model (subprocess or activity)
      this.insertShapes(boundaryEvents);
      // at last as edge source and target must be present in the model prior insertion, otherwise they are not rendered
      this.insertEdges(edges);
    });
  }

  private insertShapes(shapes: Shape[], shapeMarkers: Shape[]): void {
    shapes.forEach(shape => this.insertShape(shape, shapeMarkers.filter(shapeMarker => shapeMarker.bpmnElement.parentId === shape.bpmnElement.id)[0]));
  }

  private getParent(bpmnElement: ShapeBpmnElement): mxCell {
    const bpmnElementParent = this.getCell(bpmnElement.parentId);
    return bpmnElementParent ?? this.graph.getDefaultParent();
  }

  private insertShape(shape: Shape, shapeMarker?: Shape): void {
    const bpmnElement = shape.bpmnElement;
    const parent = this.getParent(bpmnElement);
    const bounds = shape.bounds;
    let labelBounds = shape.label?.bounds;
    // pool/lane label bounds are not managed for now (use hard coded values)
    labelBounds = ShapeUtil.isPoolOrLane(bpmnElement.kind) ? undefined : labelBounds;
    const style = this.styleComputer.computeStyle(shape, labelBounds);

    const mxCell = this.insertVertex(parent, bpmnElement.id, bpmnElement.name, bounds, labelBounds, style);

    if (shapeMarker) {
      // this.insertVertex(mxCell, shapeMarker.bpmnElement.id, undefined, undefined, undefined, style);

      // TODO: remove label

      const iconPaintingOriginX = bounds.x + bounds.width / 20;
      const iconPaintingOriginY = bounds.y + bounds.height / 20;

      const cell = this.graph.insertVertex(mxCell, shapeMarker.bpmnElement.id, undefined, 0, 0, 20, 20, this.styleComputer.computeStyle(shapeMarker, undefined));
      cell.geometry.relative = true;
      cell.geometry.offset = new mxPoint(iconPaintingOriginX, iconPaintingOriginY);
    }
  }

This way, we can use the existing event shape system, but we have to calculate the size and position of the event marker, and I find that it complicates the code.

csouchet avatar Jul 11 '23 15:07 csouchet