From e03a4b2c59daa664a34ce3a5ce75525ad446930a Mon Sep 17 00:00:00 2001 From: Philipp Fromme Date: Mon, 2 Mar 2020 14:26:09 +0100 Subject: [PATCH] chore(auto-place): move common feature to diagram-js https://github.com/bpmn-io/dmn-js/issues/470 --- lib/features/auto-place/AutoPlace.js | 58 --- .../auto-place/AutoPlaceSelectionBehavior.js | 18 - lib/features/auto-place/AutoPlaceUtil.js | 430 ------------------ lib/features/auto-place/BpmnAutoPlace.js | 18 + lib/features/auto-place/BpmnAutoPlaceUtil.js | 134 ++++++ lib/features/auto-place/index.js | 11 +- .../behavior/AutoPlaceBehavior.js | 6 +- ...pmn => BpmnAutoPlace.boundary-events.bpmn} | 0 .../{AutoPlace.bpmn => BpmnAutoPlace.bpmn} | 0 ...mn => BpmnAutoPlace.multi-connection.bpmn} | 0 ...{AutoPlaceSpec.js => BpmnAutoPlaceSpec.js} | 118 +---- 11 files changed, 169 insertions(+), 624 deletions(-) delete mode 100644 lib/features/auto-place/AutoPlace.js delete mode 100644 lib/features/auto-place/AutoPlaceSelectionBehavior.js delete mode 100644 lib/features/auto-place/AutoPlaceUtil.js create mode 100644 lib/features/auto-place/BpmnAutoPlace.js create mode 100644 lib/features/auto-place/BpmnAutoPlaceUtil.js rename test/spec/features/auto-place/{AutoPlace.boundary-events.bpmn => BpmnAutoPlace.boundary-events.bpmn} (100%) rename test/spec/features/auto-place/{AutoPlace.bpmn => BpmnAutoPlace.bpmn} (100%) rename test/spec/features/auto-place/{AutoPlace.multi-connection.bpmn => BpmnAutoPlace.multi-connection.bpmn} (100%) rename test/spec/features/auto-place/{AutoPlaceSpec.js => BpmnAutoPlaceSpec.js} (72%) diff --git a/lib/features/auto-place/AutoPlace.js b/lib/features/auto-place/AutoPlace.js deleted file mode 100644 index 9c7349e3a4..0000000000 --- a/lib/features/auto-place/AutoPlace.js +++ /dev/null @@ -1,58 +0,0 @@ -import { getNewShapePosition } from './AutoPlaceUtil'; - - -/** - * A service that places elements connected to existing ones - * to an appropriate position in an _automated_ fashion. - * - * @param {EventBus} eventBus - * @param {Modeling} modeling - */ -export default function AutoPlace(eventBus, modeling) { - - function emit(event, payload) { - return eventBus.fire(event, payload); - } - - - /** - * Append shape to source at appropriate position. - * - * @param {djs.model.Shape} source - * @param {djs.model.Shape} shape - * - * @return {djs.model.Shape} appended shape - */ - this.append = function(source, shape) { - - emit('autoPlace.start', { - source: source, - shape: shape - }); - - // allow others to provide the position - var position = emit('autoPlace', { - source: source, - shape: shape - }); - - if (!position) { - position = getNewShapePosition(source, shape); - } - - var newShape = modeling.appendShape(source, shape, position, source.parent); - - emit('autoPlace.end', { - source: source, - shape: newShape - }); - - return newShape; - }; - -} - -AutoPlace.$inject = [ - 'eventBus', - 'modeling' -]; \ No newline at end of file diff --git a/lib/features/auto-place/AutoPlaceSelectionBehavior.js b/lib/features/auto-place/AutoPlaceSelectionBehavior.js deleted file mode 100644 index 6024694c4b..0000000000 --- a/lib/features/auto-place/AutoPlaceSelectionBehavior.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Select element after auto placement. - * - * @param {EventBus} eventBus - * @param {Selection} selection - */ -export default function AutoPlaceSelectionBehavior(eventBus, selection) { - - eventBus.on('autoPlace.end', 500, function(e) { - selection.select(e.shape); - }); - -} - -AutoPlaceSelectionBehavior.$inject = [ - 'eventBus', - 'selection' -]; \ No newline at end of file diff --git a/lib/features/auto-place/AutoPlaceUtil.js b/lib/features/auto-place/AutoPlaceUtil.js deleted file mode 100644 index bee66ef6dd..0000000000 --- a/lib/features/auto-place/AutoPlaceUtil.js +++ /dev/null @@ -1,430 +0,0 @@ -import { is } from '../../util/ModelUtil'; -import { isAny } from '../modeling/util/ModelingUtil'; - -import { - getMid, - asTRBL, - getOrientation -} from 'diagram-js/lib/layout/LayoutUtil'; - -import { - find, - reduce -} from 'min-dash'; - -var DEFAULT_HORIZONTAL_DISTANCE = 50; - -var MAX_HORIZONTAL_DISTANCE = 250; - -// padding to detect element placement -var PLACEMENT_DETECTION_PAD = 10; - -/** - * Find the new position for the target element to - * connect to source. - * - * @param {djs.model.Shape} source - * @param {djs.model.Shape} element - * - * @return {Point} - */ -export function getNewShapePosition(source, element) { - - if (is(element, 'bpmn:TextAnnotation')) { - return getTextAnnotationPosition(source, element); - } - - if (isAny(element, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ])) { - return getDataElementPosition(source, element); - } - - if (is(element, 'bpmn:FlowNode')) { - return getFlowNodePosition(source, element); - } - - return getDefaultPosition(source, element); -} - -/** - * Always try to place element right of source; - * compute actual distance from previous nodes in flow. - */ -export function getFlowNodePosition(source, element) { - - var sourceTrbl = asTRBL(source); - var sourceMid = getMid(source); - - var horizontalDistance = getFlowNodeDistance(source, element); - - var orientation = 'left', - rowSize = 80, - margin = 30; - - if (is(source, 'bpmn:BoundaryEvent')) { - orientation = getOrientation(source, source.host, -25); - - if (orientation.indexOf('top') !== -1) { - margin *= -1; - } - } - - function getVerticalDistance(orient) { - if (orient.indexOf('top') != -1) { - return -1 * rowSize; - } else if (orient.indexOf('bottom') != -1) { - return rowSize; - } else { - return 0; - } - } - - var position = { - x: sourceTrbl.right + horizontalDistance + element.width / 2, - y: sourceMid.y + getVerticalDistance(orientation) - }; - - var escapeDirection = { - y: { - margin: margin, - rowSize: rowSize - } - }; - - return deconflictPosition(source, element, position, escapeDirection); -} - - -/** - * Compute best distance between source and target, - * based on existing connections to and from source. - * - * @param {djs.model.Shape} source - * @param {djs.model.Shape} element - * - * @return {number} distance - */ -export function getFlowNodeDistance(source, element) { - - var sourceTrbl = asTRBL(source); - - // is connection a reference to consider? - function isReference(c) { - return is(c, 'bpmn:SequenceFlow'); - } - - function toTargetNode(weight) { - - return function(shape) { - return { - shape: shape, - weight: weight, - distanceTo: function(shape) { - var shapeTrbl = asTRBL(shape); - - return shapeTrbl.left - sourceTrbl.right; - } - }; - }; - } - - function toSourceNode(weight) { - return function(shape) { - return { - shape: shape, - weight: weight, - distanceTo: function(shape) { - var shapeTrbl = asTRBL(shape); - - return sourceTrbl.left - shapeTrbl.right; - } - }; - }; - } - - // we create a list of nodes to take into consideration - // for calculating the optimal flow node distance - // - // * weight existing target nodes higher than source nodes - // * only take into account individual nodes once - // - var nodes = reduce([].concat( - getTargets(source, isReference).map(toTargetNode(5)), - getSources(source, isReference).map(toSourceNode(1)) - ), function(nodes, node) { - - // filter out shapes connected twice via source or target - nodes[node.shape.id + '__weight_' + node.weight] = node; - - return nodes; - }, {}); - - // compute distances between source and incoming nodes; - // group at the same time by distance and expose the - // favourite distance as { fav: { count, value } }. - var distancesGrouped = reduce(nodes, function(result, node) { - - var shape = node.shape, - weight = node.weight, - distanceTo = node.distanceTo; - - var fav = result.fav, - currentDistance, - currentDistanceCount, - currentDistanceEntry; - - currentDistance = distanceTo(shape); - - // ignore too far away peers - // or non-left to right modeled nodes - if (currentDistance < 0 || currentDistance > MAX_HORIZONTAL_DISTANCE) { - return result; - } - - currentDistanceEntry = result[String(currentDistance)] = - result[String(currentDistance)] || { - value: currentDistance, - count: 0 - }; - - // inc diff count - currentDistanceCount = currentDistanceEntry.count += 1 * weight; - - if (!fav || fav.count < currentDistanceCount) { - result.fav = currentDistanceEntry; - } - - return result; - }, { }); - - - if (distancesGrouped.fav) { - return distancesGrouped.fav.value; - } else { - return DEFAULT_HORIZONTAL_DISTANCE; - } -} - - -/** - * Always try to place text annotations top right of source. - */ -export function getTextAnnotationPosition(source, element) { - - var sourceTrbl = asTRBL(source); - - var position = { - x: sourceTrbl.right + element.width / 2, - y: sourceTrbl.top - 50 - element.height / 2 - }; - - var escapeDirection = { - y: { - margin: -30, - rowSize: 20 - } - }; - - return deconflictPosition(source, element, position, escapeDirection); -} - - -/** - * Always put element bottom right of source. - */ -export function getDataElementPosition(source, element) { - - var sourceTrbl = asTRBL(source); - - var position = { - x: sourceTrbl.right - 10 + element.width / 2, - y: sourceTrbl.bottom + 40 + element.width / 2 - }; - - var escapeDirection = { - x: { - margin: 30, - rowSize: 30 - } - }; - - return deconflictPosition(source, element, position, escapeDirection); -} - - -/** - * Always put element right of source per default. - */ -export function getDefaultPosition(source, element) { - - var sourceTrbl = asTRBL(source); - - var sourceMid = getMid(source); - - // simply put element right next to source - return { - x: sourceTrbl.right + DEFAULT_HORIZONTAL_DISTANCE + element.width / 2, - y: sourceMid.y - }; -} - - -/** - * Returns all connected elements around the given source. - * - * This includes: - * - * - connected elements - * - host connected elements - * - attachers connected elements - * - * @param {djs.model.Shape} source - * @param {djs.model.Shape} element - * - * @return {Array} - */ -function getAutoPlaceClosure(source, element) { - - var allConnected = getConnected(source); - - if (source.host) { - allConnected = allConnected.concat(getConnected(source.host)); - } - - if (source.attachers) { - allConnected = allConnected.concat(source.attachers.reduce(function(shapes, attacher) { - return shapes.concat(getConnected(attacher)); - }, [])); - } - - return allConnected; -} - -/** - * Return target at given position, if defined. - * - * This takes connected elements from host and attachers - * into account, too. - */ -export function getConnectedAtPosition(source, position, element) { - - var bounds = { - x: position.x - (element.width / 2), - y: position.y - (element.height / 2), - width: element.width, - height: element.height - }; - - var closure = getAutoPlaceClosure(source, element); - - return find(closure, function(target) { - - if (target === element) { - return false; - } - - var orientation = getOrientation(target, bounds, PLACEMENT_DETECTION_PAD); - - return orientation === 'intersect'; - }); -} - - -/** - * Returns a new, position for the given element - * based on the given element that is not occupied - * by some element connected to source. - * - * Take into account the escapeDirection (where to move - * on positioning clashes) in the computation. - * - * @param {djs.model.Shape} source - * @param {djs.model.Shape} element - * @param {Point} position - * @param {Object} escapeDelta - * - * @return {Point} - */ -export function deconflictPosition(source, element, position, escapeDelta) { - - function nextPosition(existingElement) { - - var newPosition = { - x: position.x, - y: position.y - }; - - [ 'x', 'y' ].forEach(function(axis) { - - var axisDelta = escapeDelta[axis]; - - if (!axisDelta) { - return; - } - - var dimension = axis === 'x' ? 'width' : 'height'; - - var margin = axisDelta.margin, - rowSize = axisDelta.rowSize; - - if (margin < 0) { - newPosition[axis] = Math.min( - existingElement[axis] + margin - element[dimension] / 2, - position[axis] - rowSize + margin - ); - } else { - newPosition[axis] = Math.max( - existingTarget[axis] + existingTarget[dimension] + margin + element[dimension] / 2, - position[axis] + rowSize + margin - ); - } - }); - - return newPosition; - } - - var existingTarget; - - // deconflict position until free slot is found - while ((existingTarget = getConnectedAtPosition(source, position, element))) { - position = nextPosition(existingTarget); - } - - return position; -} - - - -// helpers ////////////////////// - -function noneFilter() { - return true; -} - -function getConnected(element, connectionFilter) { - return [].concat( - getTargets(element, connectionFilter), - getSources(element, connectionFilter) - ); -} - -function getSources(shape, connectionFilter) { - - if (!connectionFilter) { - connectionFilter = noneFilter; - } - - return shape.incoming.filter(connectionFilter).map(function(c) { - return c.source; - }); -} - -function getTargets(shape, connectionFilter) { - - if (!connectionFilter) { - connectionFilter = noneFilter; - } - - return shape.outgoing.filter(connectionFilter).map(function(c) { - return c.target; - }); -} \ No newline at end of file diff --git a/lib/features/auto-place/BpmnAutoPlace.js b/lib/features/auto-place/BpmnAutoPlace.js new file mode 100644 index 0000000000..cd2e117110 --- /dev/null +++ b/lib/features/auto-place/BpmnAutoPlace.js @@ -0,0 +1,18 @@ +import { getNewShapePosition } from './BpmnAutoPlaceUtil'; + + +/** + * BPMN auto-place behavior. + * + * @param {EventBus} eventBus + */ +export default function AutoPlace(eventBus) { + eventBus.on('autoPlace', function(context) { + var shape = context.shape, + source = context.source; + + return getNewShapePosition(source, shape); + }); +} + +AutoPlace.$inject = [ 'eventBus' ]; \ No newline at end of file diff --git a/lib/features/auto-place/BpmnAutoPlaceUtil.js b/lib/features/auto-place/BpmnAutoPlaceUtil.js new file mode 100644 index 0000000000..1e6af72db0 --- /dev/null +++ b/lib/features/auto-place/BpmnAutoPlaceUtil.js @@ -0,0 +1,134 @@ +import { is } from '../../util/ModelUtil'; +import { isAny } from '../modeling/util/ModelingUtil'; + +import { + getMid, + asTRBL, + getOrientation +} from 'diagram-js/lib/layout/LayoutUtil'; + +import { + deconflictPosition, + getConnectedDistance +} from 'diagram-js/lib/features/auto-place/AutoPlaceUtil'; + + +/** + * Find the new position for the target element to + * connect to source. + * + * @param {djs.model.Shape} source + * @param {djs.model.Shape} element + * + * @return {Point} + */ +export function getNewShapePosition(source, element) { + + if (is(element, 'bpmn:TextAnnotation')) { + return getTextAnnotationPosition(source, element); + } + + if (isAny(element, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ])) { + return getDataElementPosition(source, element); + } + + if (is(element, 'bpmn:FlowNode')) { + return getFlowNodePosition(source, element); + } +} + +/** + * Always try to place element right of source; + * compute actual distance from previous nodes in flow. + */ +export function getFlowNodePosition(source, element) { + + var sourceTrbl = asTRBL(source); + var sourceMid = getMid(source); + + var horizontalDistance = getConnectedDistance(source, 'x', function(connection) { + return is(connection, 'bpmn:SequenceFlow'); + }); + + var orientation = 'left', + rowSize = 80, + margin = 30; + + if (is(source, 'bpmn:BoundaryEvent')) { + orientation = getOrientation(source, source.host, -25); + + if (orientation.indexOf('top') !== -1) { + margin *= -1; + } + } + + function getVerticalDistance(orient) { + if (orient.indexOf('top') != -1) { + return -1 * rowSize; + } else if (orient.indexOf('bottom') != -1) { + return rowSize; + } else { + return 0; + } + } + + var position = { + x: sourceTrbl.right + horizontalDistance + element.width / 2, + y: sourceMid.y + getVerticalDistance(orientation) + }; + + var escapeDirection = { + y: { + margin: margin, + rowSize: rowSize + } + }; + + return deconflictPosition(source, element, position, escapeDirection); +} + + +/** + * Always try to place text annotations top right of source. + */ +export function getTextAnnotationPosition(source, element) { + + var sourceTrbl = asTRBL(source); + + var position = { + x: sourceTrbl.right + element.width / 2, + y: sourceTrbl.top - 50 - element.height / 2 + }; + + var escapeDirection = { + y: { + margin: -30, + rowSize: 20 + } + }; + + return deconflictPosition(source, element, position, escapeDirection); +} + + +/** + * Always put element bottom right of source. + */ +export function getDataElementPosition(source, element) { + + var sourceTrbl = asTRBL(source); + + var position = { + x: sourceTrbl.right - 10 + element.width / 2, + y: sourceTrbl.bottom + 40 + element.width / 2 + }; + + var escapeDirection = { + x: { + margin: 30, + rowSize: 30 + } + }; + + return deconflictPosition(source, element, position, escapeDirection); +} \ No newline at end of file diff --git a/lib/features/auto-place/index.js b/lib/features/auto-place/index.js index cf9a7a0b0a..4751650048 100644 --- a/lib/features/auto-place/index.js +++ b/lib/features/auto-place/index.js @@ -1,8 +1,9 @@ -import AutoPlace from './AutoPlace'; -import AutoPlaceSelectionBehavior from './AutoPlaceSelectionBehavior'; +import AutoPlaceModule from 'diagram-js/lib/features/auto-place'; + +import BpmnAutoPlace from './BpmnAutoPlace'; export default { - __init__: [ 'autoPlaceSelectionBehavior' ], - autoPlace: [ 'type', AutoPlace ], - autoPlaceSelectionBehavior: [ 'type', AutoPlaceSelectionBehavior ] + __depends__: [ AutoPlaceModule ], + __init__: [ 'bpmnAutoPlace' ], + bpmnAutoPlace: [ 'type', BpmnAutoPlace ] }; \ No newline at end of file diff --git a/lib/features/grid-snapping/behavior/AutoPlaceBehavior.js b/lib/features/grid-snapping/behavior/AutoPlaceBehavior.js index 820b1a01d1..63c9e5d83b 100644 --- a/lib/features/grid-snapping/behavior/AutoPlaceBehavior.js +++ b/lib/features/grid-snapping/behavior/AutoPlaceBehavior.js @@ -1,11 +1,13 @@ -import { getNewShapePosition } from '../../auto-place/AutoPlaceUtil'; +import { getNewShapePosition } from '../../auto-place/BpmnAutoPlaceUtil'; import { getMid } from 'diagram-js/lib/layout/LayoutUtil'; import { is } from '../../../util/ModelUtil'; +var HIGH_PRIORITY = 2000; + export default function AutoPlaceBehavior(eventBus, gridSnapping) { - eventBus.on('autoPlace', function(context) { + eventBus.on('autoPlace', HIGH_PRIORITY, function(context) { var source = context.source, sourceMid = getMid(source), shape = context.shape; diff --git a/test/spec/features/auto-place/AutoPlace.boundary-events.bpmn b/test/spec/features/auto-place/BpmnAutoPlace.boundary-events.bpmn similarity index 100% rename from test/spec/features/auto-place/AutoPlace.boundary-events.bpmn rename to test/spec/features/auto-place/BpmnAutoPlace.boundary-events.bpmn diff --git a/test/spec/features/auto-place/AutoPlace.bpmn b/test/spec/features/auto-place/BpmnAutoPlace.bpmn similarity index 100% rename from test/spec/features/auto-place/AutoPlace.bpmn rename to test/spec/features/auto-place/BpmnAutoPlace.bpmn diff --git a/test/spec/features/auto-place/AutoPlace.multi-connection.bpmn b/test/spec/features/auto-place/BpmnAutoPlace.multi-connection.bpmn similarity index 100% rename from test/spec/features/auto-place/AutoPlace.multi-connection.bpmn rename to test/spec/features/auto-place/BpmnAutoPlace.multi-connection.bpmn diff --git a/test/spec/features/auto-place/AutoPlaceSpec.js b/test/spec/features/auto-place/BpmnAutoPlaceSpec.js similarity index 72% rename from test/spec/features/auto-place/AutoPlaceSpec.js rename to test/spec/features/auto-place/BpmnAutoPlaceSpec.js index 993b45dbc7..a672802e12 100644 --- a/test/spec/features/auto-place/AutoPlaceSpec.js +++ b/test/spec/features/auto-place/BpmnAutoPlaceSpec.js @@ -11,14 +11,12 @@ import selectionModule from 'diagram-js/lib/features/selection'; import { getBusinessObject } from '../../../../lib/util/ModelUtil'; -import { getMid } from 'diagram-js/lib/layout/LayoutUtil'; - describe('features/auto-place', function() { describe('element placement', function() { - var diagramXML = require('./AutoPlace.bpmn'); + var diagramXML = require('./BpmnAutoPlace.bpmn'); before(bootstrapModeler(diagramXML, { modules: [ @@ -116,7 +114,7 @@ describe('features/auto-place', function() { describe('integration', function() { - var diagramXML = require('./AutoPlace.bpmn'); + var diagramXML = require('./BpmnAutoPlace.bpmn'); before(bootstrapModeler(diagramXML, { modules: [ @@ -174,7 +172,7 @@ describe('features/auto-place', function() { describe('multi connection handling', function() { - var diagramXML = require('./AutoPlace.multi-connection.bpmn'); + var diagramXML = require('./BpmnAutoPlace.multi-connection.bpmn'); before(bootstrapModeler(diagramXML, { modules: [ @@ -209,7 +207,7 @@ describe('features/auto-place', function() { describe('boundary event connection handling', function() { - var diagramXML = require('./AutoPlace.boundary-events.bpmn'); + var diagramXML = require('./BpmnAutoPlace.boundary-events.bpmn'); before(bootstrapModeler(diagramXML, { modules: [ @@ -241,12 +239,14 @@ describe('features/auto-place', function() { expectedBounds: { x: 242, y: -27, width: 100, height: 80 } })); + it('should place top right of BOUNDARY_TOP_RIGHT without infinite loop', autoPlace({ element: 'bpmn:Task', behind: 'BOUNDARY_TOP_RIGHT', expectedBounds: { x: 473, y: -27, width: 100, height: 80 } })); + it('should place top right of BOUNDARY_SUBPROCESS_TOP', autoPlace({ element: 'bpmn:Task', behind: 'BOUNDARY_SUBPROCESS_TOP', @@ -255,114 +255,10 @@ describe('features/auto-place', function() { }); - - describe('eventbus integration', function() { - - var diagramXML = require('./AutoPlace.bpmn'); - - beforeEach(bootstrapModeler(diagramXML, { - modules: [ - autoPlaceModule, - coreModule, - labelEditingModule, - modelingModule, - selectionModule - ] - })); - - - it('', inject( - function(autoPlace, elementFactory, elementRegistry, eventBus) { - - // given - var element = elementFactory.createShape({ type: 'bpmn:Task' }); - - var source = elementRegistry.get('TASK_2'); - - var listener = sinon.spy(function(event) { - - // then - expect(event.shape).to.equal(element); - expect(event.source).to.equal(source); - }); - - eventBus.on('autoPlace.start', listener); - - // when - autoPlace.append(source, element); - - expect(listener).to.have.been.called; - } - )); - - - it('', inject( - function(autoPlace, elementFactory, elementRegistry, eventBus) { - - // given - var element = elementFactory.createShape({ type: 'bpmn:Task' }); - - var source = elementRegistry.get('TASK_2'); - - var listener = sinon.spy(function(event) { - - // then - expect(event.shape).to.equal(element); - expect(event.source).to.equal(source); - - return { - x: 0, - y: 0 - }; - }); - - eventBus.on('autoPlace', listener); - - // when - autoPlace.append(source, element); - - expect(listener).to.have.been.called; - - expect(getMid(element)).to.eql({ - x: 0, - y: 0 - }); - } - )); - - - it('', inject( - function(autoPlace, elementFactory, elementRegistry, eventBus) { - - // given - var element = elementFactory.createShape({ type: 'bpmn:Task' }); - - var source = elementRegistry.get('TASK_2'); - - var listener = sinon.spy(function(event) { - - // then - expect(event.shape).to.equal(element); - expect(event.source).to.equal(source); - }); - - eventBus.on('autoPlace.end', listener); - - // when - autoPlace.append(source, element); - - expect(listener).to.have.been.called; - } - )); - - }); - }); - - -// helpers ////////////////////// +// helpers ////////// function autoPlace(cfg) {