diff --git a/lib/base.js b/lib/base.js
index c255e33e..bba399af 100644
--- a/lib/base.js
+++ b/lib/base.js
@@ -12,6 +12,7 @@ import TokenCountModule from './features/token-count';
import SetAnimationSpeedModule from './features/set-animation-speed';
import ExclusiveGatewaySettingsModule from './features/exclusive-gateway-settings';
+import InclusiveGatewaySettingsModule from './features/inclusive-gateway-settings';
import PreserveElementColorsModule from './features/preserve-element-colors';
import TokenSimulationPaletteModule from './features/palette';
@@ -30,6 +31,7 @@ export default {
TokenCountModule,
SetAnimationSpeedModule,
ExclusiveGatewaySettingsModule,
+ InclusiveGatewaySettingsModule,
PreserveElementColorsModule,
TokenSimulationPaletteModule
]
diff --git a/lib/features/context-pads/ContextPads.js b/lib/features/context-pads/ContextPads.js
index 0f133755..145b62d6 100644
--- a/lib/features/context-pads/ContextPads.js
+++ b/lib/features/context-pads/ContextPads.js
@@ -17,6 +17,7 @@ import {
} from 'min-dom';
import ExclusiveGatewayHandler from './handler/ExclusiveGatewayHandler';
+import InclusiveGatewayHandler from './handler/InclusiveGatewayHandler';
import PauseHandler from './handler/PauseHandler';
import TriggerHandler from './handler/TriggerHandler';
@@ -45,6 +46,7 @@ export default function ContextPads(
this._handlers = [];
this.registerHandler('bpmn:ExclusiveGateway', ExclusiveGatewayHandler);
+ this.registerHandler('bpmn:InclusiveGateway', InclusiveGatewayHandler);
this.registerHandler('bpmn:Activity', PauseHandler);
diff --git a/lib/features/context-pads/handler/InclusiveGatewayHandler.js b/lib/features/context-pads/handler/InclusiveGatewayHandler.js
new file mode 100644
index 00000000..d9e142ef
--- /dev/null
+++ b/lib/features/context-pads/handler/InclusiveGatewayHandler.js
@@ -0,0 +1,47 @@
+import {
+ ForkIcon
+} from '../../../icons';
+
+import { getBusinessObject } from '../../../util/ElementHelper';
+import { isSequenceFlow } from '../../../simulator/util/ModelUtil';
+
+export default function InclusiveGatewayHandler(inclusiveGatewaySettings) {
+ this._inclusiveGatewaySettings = inclusiveGatewaySettings;
+}
+
+InclusiveGatewayHandler.prototype.createContextPads = function(element) {
+ const outgoingFlows = element.outgoing.filter(isSequenceFlow);
+
+ if (outgoingFlows.length < 2) {
+ return;
+ }
+
+ const nonDefaultFlows = outgoingFlows.filter(outgoing => {
+ const flowBo = getBusinessObject(outgoing),
+ gatewayBo = getBusinessObject(element);
+
+ return gatewayBo.default !== flowBo;
+ });
+
+ const html = `
+
+ ${ForkIcon()}
+
+ `;
+
+ return nonDefaultFlows.map(sequenceFlow => {
+ const action = () => {
+ this._inclusiveGatewaySettings.toggleSequenceFlow(element, sequenceFlow);
+ };
+
+ return {
+ action,
+ element: sequenceFlow,
+ html
+ };
+ });
+};
+
+InclusiveGatewayHandler.$inject = [
+ 'inclusiveGatewaySettings'
+];
\ No newline at end of file
diff --git a/lib/features/element-support/ElementSupport.js b/lib/features/element-support/ElementSupport.js
index a7443273..915cd47b 100644
--- a/lib/features/element-support/ElementSupport.js
+++ b/lib/features/element-support/ElementSupport.js
@@ -16,7 +16,6 @@ import {
const UNSUPPORTED_ELEMENTS = [
- 'bpmn:InclusiveGateway',
'bpmn:ComplexGateway'
];
diff --git a/lib/features/inclusive-gateway-settings/InclusiveGatewaySettings.js b/lib/features/inclusive-gateway-settings/InclusiveGatewaySettings.js
new file mode 100644
index 00000000..dba76a0a
--- /dev/null
+++ b/lib/features/inclusive-gateway-settings/InclusiveGatewaySettings.js
@@ -0,0 +1,139 @@
+import {
+ TOGGLE_MODE_EVENT
+} from '../../util/EventHelper';
+
+
+const SELECTED_COLOR = '--token-simulation-grey-darken-30';
+const NOT_SELECTED_COLOR = '--token-simulation-grey-lighten-56';
+
+import {
+ getBusinessObject,
+ is,
+ isSequenceFlow
+} from '../../simulator/util/ModelUtil';
+
+
+export default function InclusiveGatewaySettings(
+ eventBus, elementRegistry,
+ elementColors, simulator, simulationStyles) {
+
+ this._elementRegistry = elementRegistry;
+ this._elementColors = elementColors;
+ this._simulator = simulator;
+ this._simulationStyles = simulationStyles;
+
+ eventBus.on(TOGGLE_MODE_EVENT, event => {
+ if (event.active) {
+ this.setDefaults();
+ } else {
+ this.reset();
+ }
+ });
+}
+
+InclusiveGatewaySettings.prototype.setDefaults = function() {
+ const inclusiveGateways = this._elementRegistry.filter(element => {
+ return is(element, 'bpmn:InclusiveGateway');
+ });
+
+ inclusiveGateways.forEach(inclusiveGateway => {
+ if (inclusiveGateway.outgoing.filter(isSequenceFlow).length > 1) {
+ this._setGatewayDefaults(inclusiveGateway);
+ }
+ });
+};
+
+InclusiveGatewaySettings.prototype.reset = function() {
+ const inclusiveGateways = this._elementRegistry.filter(element => {
+ return is(element, 'bpmn:InclusiveGateway');
+ });
+
+ inclusiveGateways.forEach(inclusiveGateway => {
+ if (inclusiveGateway.outgoing.filter(isSequenceFlow).length > 1) {
+ this._resetGateway(inclusiveGateway);
+ }
+ });
+};
+
+InclusiveGatewaySettings.prototype.toggleSequenceFlow = function(gateway, sequenceFlow) {
+ const activeOutgoing = this._getActiveOutgoing(gateway),
+ defaultFlow = getDefaultFlow(gateway);
+
+ let newActiveOutgoing;
+ if (activeOutgoing.includes(sequenceFlow)) {
+ newActiveOutgoing = without(activeOutgoing, sequenceFlow);
+ } else {
+ newActiveOutgoing = without(activeOutgoing, defaultFlow).concat(sequenceFlow);
+ }
+
+ if (!newActiveOutgoing.length && defaultFlow) {
+ return this._setActiveOutgoing(gateway, [ defaultFlow ]);
+ }
+
+ this._setActiveOutgoing(gateway, newActiveOutgoing);
+};
+
+InclusiveGatewaySettings.prototype._getActiveOutgoing = function(gateway) {
+ const {
+ activeOutgoing
+ } = this._simulator.getConfig(gateway);
+
+ return activeOutgoing;
+};
+
+InclusiveGatewaySettings.prototype._setActiveOutgoing = function(gateway, activeOutgoing) {
+ this._simulator.setConfig(gateway, { activeOutgoing });
+
+ const sequenceFlows = gateway.outgoing.filter(isSequenceFlow);
+
+ // set colors
+ sequenceFlows.forEach(outgoing => {
+
+ const style = (!activeOutgoing || activeOutgoing.includes(outgoing)) ?
+ SELECTED_COLOR : NOT_SELECTED_COLOR;
+ const stroke = this._simulationStyles.get(style);
+
+ this._elementColors.set(outgoing, {
+ stroke
+ });
+ });
+};
+
+InclusiveGatewaySettings.prototype._setGatewayDefaults = function(gateway) {
+ const sequenceFlows = gateway.outgoing.filter(isSequenceFlow);
+
+ const defaultFlow = getDefaultFlow(gateway);
+ const nonDefaultFlows = without(sequenceFlows, defaultFlow);
+
+ this._setActiveOutgoing(gateway, nonDefaultFlows);
+};
+
+InclusiveGatewaySettings.prototype._resetGateway = function(gateway) {
+ this._setActiveOutgoing(gateway, undefined);
+};
+
+InclusiveGatewaySettings.$inject = [
+ 'eventBus',
+ 'elementRegistry',
+ 'elementColors',
+ 'simulator',
+ 'simulationStyles'
+];
+
+function getDefaultFlow(gateway) {
+ const defaultFlow = getBusinessObject(gateway).default;
+
+ if (!defaultFlow) {
+ return;
+ }
+
+ return gateway.outgoing.find(flow => {
+ const flowBo = getBusinessObject(flow);
+
+ return flowBo === defaultFlow;
+ });
+}
+
+function without(array, element) {
+ return array.filter(arrayElement => arrayElement !== element);
+}
diff --git a/lib/features/inclusive-gateway-settings/index.js b/lib/features/inclusive-gateway-settings/index.js
new file mode 100644
index 00000000..9b338b1c
--- /dev/null
+++ b/lib/features/inclusive-gateway-settings/index.js
@@ -0,0 +1,11 @@
+import InclusiveGatewaySettings from './InclusiveGatewaySettings';
+import ElementColorsModule from '../element-colors';
+import SimlationStylesModule from '../simulation-styles';
+
+export default {
+ __depends__: [
+ ElementColorsModule,
+ SimlationStylesModule
+ ],
+ inclusiveGatewaySettings: [ 'type', InclusiveGatewaySettings ]
+};
\ No newline at end of file
diff --git a/lib/simulator/behaviors/InclusiveGatewayBehavior.js b/lib/simulator/behaviors/InclusiveGatewayBehavior.js
new file mode 100644
index 00000000..dfea7f37
--- /dev/null
+++ b/lib/simulator/behaviors/InclusiveGatewayBehavior.js
@@ -0,0 +1,146 @@
+import {
+ filterSequenceFlows, isSequenceFlow
+} from '../util/ModelUtil';
+
+
+export default function InclusiveGatewayBehavior(
+ simulator,
+ activityBehavior) {
+
+ this._simulator = simulator;
+ this._activityBehavior = activityBehavior;
+
+ simulator.registerBehavior('bpmn:InclusiveGateway', this);
+}
+
+InclusiveGatewayBehavior.prototype.enter = function(context) {
+ const {
+ scope,
+ element
+ } = context;
+
+ if (this._isGuaranteedToJoin(context)) {
+ return this._join(context);
+ }
+
+ const {
+ parent: parentScope
+ } = scope;
+
+ const sameParentScopes = this._simulator.findScopes(scope => (
+ scope.parent === parentScope && scope.element !== element));
+
+ // There are still some tokens to wait for.
+ if (this._canReachAnyScope(sameParentScopes, element)) {
+ return;
+ }
+
+ this._join(context);
+};
+
+InclusiveGatewayBehavior.prototype.exit = function(context) {
+
+ const {
+ element,
+ scope
+ } = context;
+
+ // depends on UI to properly configure activeOutgoing for
+ // each inclusive gateway
+
+ const outgoings = filterSequenceFlows(element.outgoing);
+
+ if (outgoings.length === 1) {
+ return this._simulator.enter({
+ element: outgoings[0],
+ scope: scope.parent
+ });
+ }
+
+ const {
+ activeOutgoing
+ } = this._simulator.getConfig(element);
+
+ if (!activeOutgoing.length) {
+ throw new Error('no outgoing configured');
+ }
+
+ for (const outgoing of activeOutgoing) {
+ this._simulator.enter({
+ element: outgoing,
+ scope: scope.parent
+ });
+ }
+};
+
+/**
+ * Number of tokens waiting at the gateway cannot be higher than the number of incoming flows.
+ */
+InclusiveGatewayBehavior.prototype._isGuaranteedToJoin = function(context) {
+ const elementScopes = this._getElementScopes(context);
+
+ const incomingSequenceFlows = filterSequenceFlows(context.element.incoming);
+
+ return elementScopes.length >= incomingSequenceFlows.length;
+};
+
+InclusiveGatewayBehavior.prototype._join = function(context) {
+ const elementScopes = this._getElementScopes(context);
+
+ for (const childScope of elementScopes) {
+
+ if (childScope !== context.scope) {
+
+ // complete joining child scope
+ this._simulator.destroyScope(childScope.complete(), context.scope);
+ }
+ }
+
+ this._simulator.exit(context);
+};
+
+InclusiveGatewayBehavior.prototype._getElementScopes = function(context) {
+ const {
+ element,
+ parent
+ } = context;
+
+ return this._simulator.findScopes({
+ parent,
+ element
+ });
+};
+
+InclusiveGatewayBehavior.prototype._canReachAnyScope = function(scopes, currentElement, traversed = new Set()) {
+ if (traversed.has(currentElement)) {
+ return false;
+ }
+
+ if (anyScopeIsOnElement(scopes, currentElement)) {
+ return true;
+ }
+
+ if (isSequenceFlow(currentElement)) {
+ return this._canReachAnyScope(scopes, currentElement.source, traversed);
+ }
+
+
+ const incomingFlows = filterSequenceFlows(currentElement.incoming);
+
+ for (const flow of incomingFlows) {
+ if (this._canReachAnyScope(scopes, flow, traversed)) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
+InclusiveGatewayBehavior.$inject = [
+ 'simulator',
+ 'activityBehavior'
+];
+
+function anyScopeIsOnElement(scopes, element) {
+ return scopes.some(scope => scope.element === element);
+}
diff --git a/lib/simulator/behaviors/index.js b/lib/simulator/behaviors/index.js
index c2a30cfc..9ffd87ac 100644
--- a/lib/simulator/behaviors/index.js
+++ b/lib/simulator/behaviors/index.js
@@ -7,6 +7,7 @@ import IntermediateThrowEventBehavior from './IntermediateThrowEventBehavior';
import ExclusiveGatewayBehavior from './ExclusiveGatewayBehavior';
import ParallelGatewayBehavior from './ParallelGatewayBehavior';
import EventBasedGatewayBehavior from './EventBasedGatewayBehavior';
+import InclusiveGatewayBehavior from './InclusiveGatewayBehavior';
import ActivityBehavior from './ActivityBehavior';
import SubProcessBehavior from './SubProcessBehavior';
@@ -31,6 +32,7 @@ export default {
'exclusiveGatewayBehavior',
'parallelGatewayBehavior',
'eventBasedGatewayBehavior',
+ 'inclusiveGatewayBehavior',
'subProcessBehavior',
'sequenceFlowBehavior',
'messageFlowBehavior',
@@ -44,6 +46,7 @@ export default {
exclusiveGatewayBehavior: [ 'type', ExclusiveGatewayBehavior ],
parallelGatewayBehavior: [ 'type', ParallelGatewayBehavior ],
eventBasedGatewayBehavior: [ 'type', EventBasedGatewayBehavior ],
+ inclusiveGatewayBehavior: [ 'type', InclusiveGatewayBehavior ],
activityBehavior: [ 'type', ActivityBehavior ],
subProcessBehavior: [ 'type', SubProcessBehavior ],
sequenceFlowBehavior: [ 'type', SequenceFlowBehavior ],
diff --git a/test/spec/ModelerSpec.js b/test/spec/ModelerSpec.js
index 06fe1cd3..72a03de3 100644
--- a/test/spec/ModelerSpec.js
+++ b/test/spec/ModelerSpec.js
@@ -21,8 +21,10 @@ describe('modeler extension', function() {
const diagram = require('./simple.bpmn');
beforeEach(bootstrapModeler(diagram, {
+ keyboard: {
+ bindTo: document
+ },
additionalModules: [
- ...Modeler.prototype._modules,
TokenSimulationModelerModules
]
}));
@@ -118,7 +120,7 @@ describe('modeler extension', function() {
// then
expect(
elementSupport.getUnsupportedElements()
- ).to.have.length(2);
+ ).to.have.length(1);
}));
});
diff --git a/test/spec/ViewerSpec.js b/test/spec/ViewerSpec.js
index 80bba18c..96bd01ec 100644
--- a/test/spec/ViewerSpec.js
+++ b/test/spec/ViewerSpec.js
@@ -53,7 +53,7 @@ describe('viewer extension', function() {
// then
expect(
elementSupport.getUnsupportedElements()
- ).to.have.length(2);
+ ).to.have.length(1);
}));
});
diff --git a/test/spec/simulator/Simulator.inclusive-gateway-single-token-pass-through.bpmn b/test/spec/simulator/Simulator.inclusive-gateway-single-token-pass-through.bpmn
new file mode 100644
index 00000000..4bf34083
--- /dev/null
+++ b/test/spec/simulator/Simulator.inclusive-gateway-single-token-pass-through.bpmn
@@ -0,0 +1,60 @@
+
+
+
+
+ Flow_2
+
+
+
+ Flow_3
+
+
+
+ Flow_1
+
+
+
+ Flow_2
+ Flow_3
+ Flow_1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/spec/simulator/Simulator.inclusive-gateway-single-token-pass-through.json b/test/spec/simulator/Simulator.inclusive-gateway-single-token-pass-through.json
new file mode 100644
index 00000000..812ae0de
--- /dev/null
+++ b/test/spec/simulator/Simulator.inclusive-gateway-single-token-pass-through.json
@@ -0,0 +1,26 @@
+[
+ "createScope:Process_1:null",
+ "signal:Process_1:B",
+ "createScope:START:B",
+ "signal:START:C",
+ "exit:START:C",
+ "createScope:Flow_2:B",
+ "destroyScope:START:C",
+ "enter:Flow_2:B",
+ "exit:Flow_2:D",
+ "createScope:GATE:B",
+ "destroyScope:Flow_2:D",
+ "enter:GATE:B",
+ "exit:GATE:E",
+ "createScope:Flow_1:B",
+ "destroyScope:GATE:E",
+ "enter:Flow_1:B",
+ "exit:Flow_1:F",
+ "createScope:END:B",
+ "destroyScope:Flow_1:F",
+ "enter:END:B",
+ "exit:END:G",
+ "destroyScope:END:G",
+ "exit:Process_1:B",
+ "destroyScope:Process_1:B"
+]
\ No newline at end of file
diff --git a/test/spec/simulator/Simulator.inclusive-gateway-sync.bpmn b/test/spec/simulator/Simulator.inclusive-gateway-sync.bpmn
new file mode 100644
index 00000000..2560882f
--- /dev/null
+++ b/test/spec/simulator/Simulator.inclusive-gateway-sync.bpmn
@@ -0,0 +1,102 @@
+
+
+
+
+ Flow_2
+
+
+ Flow_1
+
+
+
+
+
+
+
+ Flow_2
+ Flow_3
+ Flow_4
+ Flow_5
+
+
+ Flow_3
+ Flow_4
+ Flow_5
+ Flow_1
+
+
+ FAKE
+
+
+
+ FAKE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/spec/simulator/Simulator.inclusive-gateway-sync.json b/test/spec/simulator/Simulator.inclusive-gateway-sync.json
new file mode 100644
index 00000000..72d41b52
--- /dev/null
+++ b/test/spec/simulator/Simulator.inclusive-gateway-sync.json
@@ -0,0 +1,41 @@
+[
+ "createScope:Process_1:null",
+ "signal:Process_1:B",
+ "createScope:START:B",
+ "signal:START:C",
+ "exit:START:C",
+ "createScope:Flow_2:B",
+ "destroyScope:START:C",
+ "enter:Flow_2:B",
+ "exit:Flow_2:D",
+ "createScope:F_GATE:B",
+ "destroyScope:Flow_2:D",
+ "enter:F_GATE:B",
+ "exit:F_GATE:E",
+ "createScope:Flow_3:B",
+ "createScope:Flow_5:B",
+ "destroyScope:F_GATE:E",
+ "enter:Flow_3:B",
+ "enter:Flow_5:B",
+ "exit:Flow_3:F",
+ "createScope:J_GATE:B",
+ "destroyScope:Flow_3:F",
+ "exit:Flow_5:G",
+ "createScope:J_GATE:B",
+ "destroyScope:Flow_5:G",
+ "enter:J_GATE:B",
+ "enter:J_GATE:B",
+ "destroyScope:J_GATE:H",
+ "exit:J_GATE:I",
+ "createScope:Flow_1:B",
+ "destroyScope:J_GATE:I",
+ "enter:Flow_1:B",
+ "exit:Flow_1:J",
+ "createScope:END:B",
+ "destroyScope:Flow_1:J",
+ "enter:END:B",
+ "exit:END:K",
+ "destroyScope:END:K",
+ "exit:Process_1:B",
+ "destroyScope:Process_1:B"
+]
\ No newline at end of file
diff --git a/test/spec/simulator/SimulatorSpec.js b/test/spec/simulator/SimulatorSpec.js
index 48db2611..6f6ad913 100644
--- a/test/spec/simulator/SimulatorSpec.js
+++ b/test/spec/simulator/SimulatorSpec.js
@@ -657,6 +657,37 @@ describe('simulator', function() {
});
+ describe('inclusive gateway', function() {
+
+ verify('inclusive-gateway-sync', (fixture) => {
+
+ // given
+ setConfig(element('F_GATE'), {
+ activeOutgoing: [ element('Flow_3'), element('Flow_5') ]
+ });
+
+ // when
+ trigger({
+ element: element('START')
+ });
+
+ // then
+ expectTrace(fixture());
+ });
+
+ verify('inclusive-gateway-single-token-pass-through', (fixture) => {
+
+ // when
+ trigger({
+ element: element('START')
+ });
+
+ // then
+ expectTrace(fixture());
+ });
+ });
+
+
describe('end event', function() {
verify('end-event', (fixture) => {