diff --git a/.npmignore b/.npmignore index 3af550f..0c81515 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,6 @@ apireference node_modules +.vscode samples .gitignore apireference.js diff --git a/apireference/algorithms.md b/apireference/algorithms.md index 78c9d00..ee3217f 100644 --- a/apireference/algorithms.md +++ b/apireference/algorithms.md @@ -586,7 +586,7 @@ Validates internal structure consistency. ## Graph -Creates graph structure +Creates an undirected graph structure backed by adjacency lists. Each edge stores a context object provided by the caller. `Graph` @@ -594,114 +594,118 @@ Creates graph structure `Graph()` -Creates graph structure +Creates an undirected graph structure backed by adjacency lists. Each edge stores a context object provided by the caller. - Returns: `Graph` - returns graph object + Returns: `Graph` - a new graph instance ### Functions `addEdge(from, to, edge)` -Adds edge to the graph +Adds an undirected edge between two nodes. If the edge already exists, it will not be replaced. | Param | Type | Default | Description | | --- | --- | --- | --- | - | `from` | string | `` | The id of the start node | - | `to` | string | `` | The id of the end node | - | `edge` | object | `` | The edge contextual object | + | `from` | string | `` | The starting node id | + | `to` | string | `` | The ending node id | + | `edge` | object | `` | The edge context object | `dfsLoop(thisArg, startNode, onEdge, onNode)` -Depth first search loop +Performs a depth-first traversal starting at a node. Edge usability is determined by the onEdge callback. | Param | Type | Default | Description | | --- | --- | --- | --- | - | `thisArg` | object | `` | The callback function invocation context | + | `thisArg` | object | `` | Execution context | | `startNode` | string | `` | The start node id | - | `onEdge` | onPathEdgeCallback | `` | A callback function to call for every edge of the graph | - | `onNode` | onNodeCallback | `` | A callback function to be called for every neighboring node | + | `onEdge` | onPathEdgeCallback | `` | Callback deciding edge usability | + | `onNode` | onNodeCallback | `` | Callback invoked for each newly visited node | **Callbacks** `onPathEdgeCallback(from, to, edge)` -Callback for iterating path edges +Callback used for filtering usable edges during DFS path search. - Returns: `boolean` - returns true if edge is usable + Returns: `boolean` - true if the edge may be used in traversal | Param | Type | Default | Description | | --- | --- | --- | --- | - | `from` | string | `` | The from node id | - | `to` | string | `` | The to node id | - | `edge` | Object | `` | The edge's context object | + | `from` | string | `` | The start node id | + | `to` | string | `` | The end node id | + | `edge` | Object | `` | The edge context object | `onNodeCallback(to)` -Callback function for iterating graphs nodes +Callback for node iteration functions. - Returns: `boolean` - returns true to break loop + Returns: `boolean` - return true to stop traversal early | Param | Type | Default | Description | | --- | --- | --- | --- | - | `to` | string | `` | The next neighboring node id | + | `to` | string | `` | The node id visited | `dfsPath(thisArg, startNode, endNode, onEdge)` -Search any path from node to node using depth first search +Finds any path between two nodes using depth-first search. + + Returns: `string[]` - array of node ids forming the found path | Param | Type | Default | Description | | --- | --- | --- | --- | - | `thisArg` | object | `` | The callback function invocation context | + | `thisArg` | object | `` | Execution context for callbacks | | `startNode` | string | `` | The start node id | - | `endNode` | string | `` | The end node id. | - | `onEdge` | onPathEdgeCallback | `` | A callback function to call for every edge of the node | + | `endNode` | string | `` | The end node id | + | `onEdge` | onPathEdgeCallback | `` | Callback deciding whether an edge is usable | **Callbacks** `onPathEdgeCallback(from, to, edge)` -Callback for iterating path edges +Callback used for filtering usable edges during DFS path search. - Returns: `boolean` - returns true if edge is usable + Returns: `boolean` - true if the edge may be used in traversal | Param | Type | Default | Description | | --- | --- | --- | --- | - | `from` | string | `` | The from node id | - | `to` | string | `` | The to node id | - | `edge` | Object | `` | The edge's context object | + | `from` | string | `` | The start node id | + | `to` | string | `` | The end node id | + | `edge` | Object | `` | The edge context object | `edge(from, to)` -Returns edge context object +Retrieves the stored edge context object for a given pair of nodes. - Returns: `object` - the edge's context object + Returns: `object|null` - the edge's context object, or null if none exists | Param | Type | Default | Description | | --- | --- | --- | --- | - | `from` | string | `` | The edge's from node id | - | `to` | string | `` | The edge's to node id | + | `from` | string | `` | The source node id | + | `to` | string | `` | The target node id | `getLevelGraph(thisArg, startNode, onEdge)` -Get Level Graph starting with `startNode` +Computes a level graph starting from a given node. Levels are assigned via BFS using only edges allowed by the callback. + + Returns: `Graph` - a new graph representing the level structure | Param | Type | Default | Description | | --- | --- | --- | --- | - | `thisArg` | object | `` | The callback function invocation context | + | `thisArg` | object | `` | Execution context | | `startNode` | string | `` | The start node id | - | `onEdge` | onPathEdgeCallback | `` | A callback function to call for every edge of the graph | + | `onEdge` | onPathEdgeCallback | `` | Callback deciding if an edge is valid to traverse | **Callbacks** `onPathEdgeCallback(from, to, edge)` -Callback for iterating path edges +Callback used for filtering usable edges during DFS path search. - Returns: `boolean` - returns true if edge is usable + Returns: `boolean` - true if the edge may be used in traversal | Param | Type | Default | Description | | --- | --- | --- | --- | - | `from` | string | `` | The from node id | - | `to` | string | `` | The to node id | - | `edge` | Object | `` | The edge's context object | + | `from` | string | `` | The start node id | + | `to` | string | `` | The end node id | + | `edge` | Object | `` | The edge context object | `getMinimumWeightGrowthSequence(thisArg, startNode, onEdgeWeight, onItem)` @@ -717,83 +721,83 @@ Get minimum weight graph growth sequence. The sequence of the traversing order o `getGraphEdgeWeightCallback(edge, fromItem, toItem)` -Callback for finding edge weight +Returns edge weight used in certain algorithms. - Returns: `number` - returns weight of the edge + Returns: `number` - the weight of the edge | Param | Type | Default | Description | | --- | --- | --- | --- | | `edge` | object | `` | The edge context object | - | `fromItem` | string | `` | The edge's start node id | - | `toItem` | string | `` | The edge's end node id | + | `fromItem` | string | `` | The start node id | + | `toItem` | string | `` | The end node id | `onNodeCallback(to)` -Callback function for iterating graphs nodes +Callback for node iteration functions. - Returns: `boolean` - returns true to break loop + Returns: `boolean` - return true to stop traversal early | Param | Type | Default | Description | | --- | --- | --- | --- | - | `to` | string | `` | The next neighboring node id | + | `to` | string | `` | The node id visited | `getShortestPath(thisArg, startNode, endNodes, getWeightFunc, onPathFound)` -Get shortest path between two nodes in graph. The start and the end nodes are supposed to have connection path. +Computes the shortest paths from a start node to one or more target nodes. | Param | Type | Default | Description | | --- | --- | --- | --- | - | `thisArg` | object | `` | The callback function invocation context | - | `startNode` | string | `` | The start node id | - | `endNodes` | string[] | `` | The array of end node ids. | - | `getWeightFunc` | getGraphEdgeWeightCallback | `` | Callback function to get weight of an edge. | - | `onPathFound` | onPathFoundCallback | `` | A callback function to be called for every end node with the optimal connection path | + | `thisArg` | object | `` | Execution context for callbacks | + | `startNode` | string | `` | Starting node id | + | `endNodes` | string[] | `` | Target node ids | + | `getWeightFunc` | getGraphEdgeWeightCallback | `` | Optional function returning edge weight | + | `onPathFound` | onPathFoundCallback | `` | Callback invoked when a target path is found | **Callbacks** `getGraphEdgeWeightCallback(edge, fromItem, toItem)` -Callback for finding edge weight +Returns edge weight used in certain algorithms. - Returns: `number` - returns weight of the edge + Returns: `number` - the weight of the edge | Param | Type | Default | Description | | --- | --- | --- | --- | | `edge` | object | `` | The edge context object | - | `fromItem` | string | `` | The edge's start node id | - | `toItem` | string | `` | The edge's end node id | + | `fromItem` | string | `` | The start node id | + | `toItem` | string | `` | The end node id | `onPathFoundCallback(path, to)` -Callback for returning optimal connection path for every end node. +Callback invoked when a full path has been reconstructed. | Param | Type | Default | Description | | --- | --- | --- | --- | - | `path` | string[] | `` | An array of connection path node ids. | - | `to` | string | `` | The end node id, the connection path is found for. | + | `path` | string[] | `` | The node sequence forming the path | + | `to` | string | `` | The end node id | `getSpanningTree(startNode, getWeightFunc)` -Get maximum spanning tree. Graph may have disconnected sub graphs, so start node is necessary. +Computes a maximum spanning tree using a priority queue. The graph may be disconnected; a start node is required. - Returns: `tree` - returns tree structure containing maximum spanning tree of the graph + Returns: `tree` - a tree structure containing the maximum spanning tree | Param | Type | Default | Description | | --- | --- | --- | --- | - | `startNode` | string | `` | The node to start searching for maximum spanning tree. Graph is not necessary connected | - | `getWeightFunc` | getGraphEdgeWeightCallback | `` | Callback function to get weight of an edge. | + | `startNode` | string | `` | Node to begin spanning tree search | + | `getWeightFunc` | getGraphEdgeWeightCallback | `` | Function returning edge weight | **Callbacks** `getGraphEdgeWeightCallback(edge, fromItem, toItem)` -Callback for finding edge weight +Returns edge weight used in certain algorithms. - Returns: `number` - returns weight of the edge + Returns: `number` - the weight of the edge | Param | Type | Default | Description | | --- | --- | --- | --- | | `edge` | object | `` | The edge context object | - | `fromItem` | string | `` | The edge's start node id | - | `toItem` | string | `` | The edge's end node id | + | `fromItem` | string | `` | The start node id | + | `toItem` | string | `` | The end node id | `getTotalWeightGrowthSequence(thisArg, onEdgeWeight, onItem)` @@ -808,76 +812,97 @@ Get graph growth sequence. The sequence of graph traversing order. `getGraphEdgeWeightCallback(edge, fromItem, toItem)` -Callback for finding edge weight +Returns edge weight used in certain algorithms. - Returns: `number` - returns weight of the edge + Returns: `number` - the weight of the edge | Param | Type | Default | Description | | --- | --- | --- | --- | | `edge` | object | `` | The edge context object | - | `fromItem` | string | `` | The edge's start node id | - | `toItem` | string | `` | The edge's end node id | + | `fromItem` | string | `` | The start node id | + | `toItem` | string | `` | The end node id | `onNodeCallback(to)` -Callback function for iterating graphs nodes +Callback for node iteration functions. - Returns: `boolean` - returns true to break loop + Returns: `boolean` - return true to stop traversal early | Param | Type | Default | Description | | --- | --- | --- | --- | - | `to` | string | `` | The next neighboring node id | + | `to` | string | `` | The node id visited | `hasNode(from)` -Returns true if node exists in the graph +Checks whether a node exists in the graph. - Returns: `boolean` - returns true if node exists + Returns: `boolean` - true if the node is present in the graph | Param | Type | Default | Description | | --- | --- | --- | --- | | `from` | string | `` | The node id | + `loopEdges(thisArg, onEdge)` + +Iterates over all edges in the graph. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | + | `thisArg` | object | `` | Execution context for the callback | + | `onEdge` | onEdgeCallback | `` | Callback invoked for each edge | +**Callbacks** + + `onEdgeCallback(from, to, edge)` + +Callback invoked for each edge during iteration. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | + | `from` | string | `` | The start node id | + | `to` | string | `` | The end node id | + | `edge` | Object | `` | The edge context object | + `loopNodeEdges(thisArg, itemid, onEdge)` -Loop edges of the node +Iterates over all edges connected to a specific node. | Param | Type | Default | Description | | --- | --- | --- | --- | - | `thisArg` | object | `` | The callback function invocation context | - | `itemid` | string | `` | The node id | - | `onEdge` | onEdgeCallback | `` | A callback function to call for every edge of the node | + | `thisArg` | object | `` | Execution context for the callback | + | `itemid` | string | `` | The node id whose edges to iterate | + | `onEdge` | onEdgeCallback | `` | Callback invoked for each connected edge | **Callbacks** - `onEdgeCallback(to, edge)` + `onEdgeCallback(from, to, edge)` -Callback for iterating edges of the graph's node +Callback invoked for each edge during iteration. | Param | Type | Default | Description | | --- | --- | --- | --- | - | `to` | string | `` | The neighboring node id | - | `edge` | Object | `` | The edge's context object | + | `from` | string | `` | The start node id | + | `to` | string | `` | The end node id | + | `edge` | Object | `` | The edge context object | `loopNodes(thisArg, startNode, onItem)` -Loop nodes of the graph +Traverses all connected nodes starting from the given node. If no start node is provided, traversal begins with the first available node. | Param | Type | Default | Description | | --- | --- | --- | --- | - | `thisArg` | object | `` | The callback function invocation context | - | `itemid` | string | `undefined` | The optional start node id. If start node is undefined, function loops graphs node starting from first available node | - | `onItem` | onNodeCallback | `` | A callback function to be called for every neighboring node | + | `thisArg` | object | `` | Execution context for the callback | + | `startNode` | string | `` | Optional starting node id | + | `onItem` | onNodeCallback | `` | Callback invoked for each visited node | **Callbacks** `onNodeCallback(to)` -Callback function for iterating graphs nodes +Callback for node iteration functions. - Returns: `boolean` - returns true to break loop + Returns: `boolean` - return true to stop traversal early | Param | Type | Default | Description | | --- | --- | --- | --- | - | `to` | string | `` | The next neighboring node id | + | `to` | string | `` | The node id visited | ## LCA Creates Lowest Common Ancestor Structure for the given tree diff --git a/apireference/functions.md b/apireference/functions.md index 3688f9c..d863ef6 100644 --- a/apireference/functions.md +++ b/apireference/functions.md @@ -111,13 +111,14 @@ Callback function to iterate over pairs of crossing rectangles ## getFamilyLoops -This function finds [optimal collection of feedback edges](https://en.wikipedia.org/wiki/Feedback_arc_set) needed to be cut in order to eliminate loops in family structure. +Computes the optimal set of feedback edges that must be removed to eliminate all cycles in a family structure. This corresponds to [finding a minimum feedback arc set in the directed graph](https://en.wikipedia.org/wiki/Feedback_arc_set) formed by the family relationships. The function analyzes the directed dependencies inside the family, detects all cycles, and returns the smallest collection of edges whose removal makes the structure acyclic. - Returns: `Edge[]` - returns optimal collection of feedback loops + Returns: `Edge[]` - the minimal set of edges whose removal breaks all cycles. | Param | Type | Default | Description | | --- | --- | --- | --- | - | `family` | Family | `` | Family structure | + | `family` | Family | `` | - The family structure represented as a directed graph. | + | `debug` | boolean | `false` | - If true, enables diagnostic output. | ## getGreen diff --git a/changelog.md b/changelog.md index 70ff1c7..c5a156b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,5 @@ +#### Version 6.6.2 +* Fixed computation of the optimal set of feedback edges that must be removed to eliminate all cycles in a family structure. #### Version 6.6.0 * Added templated end points to connector annotations. Added `showFromEndpoint`, `showToEndpoint`, `context` properties to to `ConnectorAnnotationConfig`. Added `onEndPointRender`, `showEndPoints`, `endPointSize`, `endPointCornerRadius`, `endPointOpacity` properties to `OrgConfig`. Added drag and drop sample creating and editing connector annotations. * Changed CSS styles for connector annotations, so when they are rendered over diagram nodes, they should be transparent for mouse events. diff --git a/package.json b/package.json index e4ce295..7cef1f5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "basicprimitives", "sideEffects": false, "homepage": "https://www.basicprimitives.com/", - "version": "6.6.0", + "version": "6.6.2", "author": "Basic Primitives Inc. (https://www.basicprimitives.com)", "description": "Basic Primitives Diagrams for JavaScript - data visualization components library that implements organizational chart and multi-parent dependency diagrams, contains implementations of JavaScript Controls and PDF rendering plugins.", "repository": { diff --git a/readme.md b/readme.md index bf353b3..0bb5df7 100644 --- a/readme.md +++ b/readme.md @@ -16,7 +16,7 @@ Data visualization diagramming components library for dependencies visualization * Webpack [Tree Shaking](https://webpack.js.org/guides/tree-shaking/) support ### Basic Primitives Diagrams for [React](https://reactjs.org/) -* [create-react-app](https://github.com/facebook/create-react-app#readme) compatible +* [Vite](https://vite.dev/) Fast build tool for modern web development. * User [JSX](https://reactjs.org/docs/introducing-jsx.html) templates * 100% [Virtual DOM](https://reactjs.org/docs/faq-internals.html) rendering cycle * [react-dnd](http://react-dnd.github.io/react-dnd/about) & [React Context](https://reactjs.org/docs/context.html) compatible diff --git a/samples/ConnectorAnnotation.md b/samples/ConnectorAnnotation.md index ddc42d1..fc14331 100644 --- a/samples/ConnectorAnnotation.md +++ b/samples/ConnectorAnnotation.md @@ -8,6 +8,7 @@ The following sample demonstrates connection annotation drawn in the offbeat sty ![Screenshot](javascript.controls/__image_snapshots__/CaseConnectorAnnotation-snap.png) +## Drag & Drop The following sample demonstrates how drag-and-drop can be used to create or modify connector annotations in a diagram. * Drag nodes to create new annotations. diff --git a/samples/FirstOrganizationalChart.md b/samples/FirstOrganizationalChart.md index 0c6bf71..19b463d 100644 --- a/samples/FirstOrganizationalChart.md +++ b/samples/FirstOrganizationalChart.md @@ -94,17 +94,16 @@ The control is an interactive component by design, so it needs to add event list control.destroy(); ``` -## PDFKit +## PDFKit Integration -Basic Primitives library provides plugins for [PDFkit](www.PDFkit.org) (MIT License) - it is JavaScript PDF generation library for NodeJS and client-side rendering in browser. +The Basic Primitives library offers plugins for [PDFKit](https://www.PDFkit.org) (MIT License), a robust JavaScript library designed for generating PDFs in Node.js and client-side rendering in browsers. -PDFKit library provides the complete experience for rendering documents in PDF format. Basic Primitives library has two plugins for PDFkit to generate Diagrams on PDF page: -* OrgDiagramPdfkit - Organizational Chart PDFkit Plugin -* FamDiagramPdfkit - Family Diagram PDFkit Plugin +PDFKit provides a comprehensive solution for rendering documents in PDF format. The Basic Primitives library includes two plugins for generating diagrams on PDF pages using PDFKit: +* **OrgDiagramPdfkit** – Organizational Chart PDFKit Plugin +* **FamDiagramPdfkit** – Family Diagram PDFKit Plugin -PDFkit Plugins are stand-alone products. They share many API options with Basic Primitives Controls, but they don't have any interactivity, and their rendering engine uses PDFkit's library vector graphics capabilities, see PDFkit site for reference. +These PDFKit plugins are standalone products. While they share many API options with Basic Primitives controls, they do not include interactivity. Instead, their rendering engine utilizes PDFKit’s vector graphics capabilities. For further reference, consult the PDFKit documentation. -The following example is a minimal code needed to create a new empty PDF file on the browser's client-side using the PDFkit library. ```JavaScript const PDFDocument = require('pdfkit'); @@ -186,4 +185,25 @@ Plugins are part of the Basic Primitives distribution package. [JavaScript](javascript.controls/CaseFirstOrganizationalChart.html) [PDFKit](pdfkit.plugins/FirstOrganizationalChart.html) -![Screenshot](javascript.controls/__image_snapshots__/CaseFirstOrganizationalChart-snap.png) \ No newline at end of file +![Screenshot](javascript.controls/__image_snapshots__/CaseFirstOrganizationalChart-snap.png) + +### Browser Usage: PDFKit and Recommended Setup + +For use in browser environments, especially in frameworks like **React** and **Angular**, PDFKit developers recommend using the standalone version of the library for optimal performance. + +1. **`pdfkit.standalone.js`** + This file contains the standalone version of [PDFKit](https://github.com/foliojs/pdfkit/releases), enabling PDF generation directly in the browser or on the server. It’s ideal for environments like React and Angular, where the regular Node.js version might not be compatible. + +2. **`blob-stream.js`** + This file is part of the [Blob Stream](https://github.com/devongovett/blob-stream/releases) library, which allows you to create binary blobs in the browser from streams. This is particularly useful when generating downloadable files such as PDFs. + +### React Demo Repository + +For a practical example, our **React-demo repository** includes a sample integration of **pdfkit.standalone.js** into a React application built with **Vite**. This demo showcases how to set up and use PDFKit in a modern React environment, providing a simple yet complete solution for PDF generation in client-side applications. + +You can explore the sample integration here: [React Demo Repository](https://github.com/BasicPrimitives/react-demo/blob/master/client/src/components/PdfViewDialog/PdfViewDialog.jsx) + +For further details on these libraries, you can refer to the following GitHub pages: + +- [PDFKit Releases](https://github.com/foliojs/pdfkit/releases) +- [Blob Stream Releases](https://github.com/devongovett/blob-stream/releases) \ No newline at end of file diff --git a/src/ConnectorAnnotationControl.js b/src/ConnectorAnnotationControl.js index 1424c32..a967daf 100644 --- a/src/ConnectorAnnotationControl.js +++ b/src/ConnectorAnnotationControl.js @@ -1,17 +1,9 @@ import Rect from './graphics/structs/Rect'; -import PaletteItem from './graphics/structs/PaletteItem'; -import { ConnectorPlacementType } from './enums'; import createGraphics from './graphics/createGraphics'; -import { getFixOfPixelAlignment, getInnerSize, getElementOffset } from './graphics/dom'; +import { getFixOfPixelAlignment, getInnerSize } from './graphics/dom'; import JsonML from './common/jsonml-html'; -import Transform from './graphics/Transform'; -import PolylinesBuffer from './graphics/structs/PolylinesBuffer'; -import ConnectorAnnotationOffsetResolver from './tasks/renders/offsetResolver/ConnectorAnnotationOffsetResolver'; -import ConnectorOffbeat from './graphics/shapes/ConnectorOffbeat'; -import ConnectorStraight from './graphics/shapes/ConnectorStraight'; +import ConnectorAnnotation from './graphics/annotations/ConnectorAnnotation'; import AnnotationLabelTemplate from './templates/html/AnnotationLabelTemplate'; -import { isNullOrEmpty } from './common'; -import RenderEventArgs from './events/RenderEventArgs'; /** * Creates JavaScript Connector Annotation Control @@ -30,7 +22,8 @@ export default function ConnectorAnnotationControl(element, options) { placeholder: null, panelSize: null, graphics: null, - labelTemplate: null + labelTemplate: null, + connector: null }; if(!element) { @@ -70,6 +63,7 @@ export default function ConnectorAnnotationControl(element, options) { ); _data.graphics = createGraphics(_data.element); + _data.connector = new ConnectorAnnotation(); }; function cleanLayout() { @@ -106,127 +100,19 @@ export default function ConnectorAnnotationControl(element, options) { _data.labelTemplate = AnnotationLabelTemplate(_data.options); cleanLayout(); createLayout(); - redraw(); + _data.graphics.begin(); + _data.connector.draw(_data.options, _data.graphics, _data.panelSize, _data.labelTemplate); + _data.graphics.end(); } else { updateLayout(); _data.graphics.resize("placeholder", _data.panelSize.width, _data.panelSize.height); _data.graphics.begin(); - redraw(); + _data.connector.draw(_data.options, _data.graphics, _data.panelSize, _data.labelTemplate); _data.graphics.end(); } } - function redraw() { - var annotationConfig = _data.options, - shape, - uiHash, - transform = new Transform(), - panel = _data.graphics.activate("placeholder"), - buffer = new PolylinesBuffer(), - connectorAnnotationOffsetResolver = ConnectorAnnotationOffsetResolver(); - - transform.size = new Size(_data.panelSize.width, _data.panelSize.height); - transform.setOrientation(annotationConfig.orientationType); - - if (annotationConfig.fromRectangle != null && annotationConfig.toRectangle != null) { - var fromRect = annotationConfig.fromRectangle, - toRect = annotationConfig.toRectangle; - - /* translate rectangles to Top orientation */ - /* from rectangle */ - transform.transformRect(fromRect.x, fromRect.y, fromRect.width, fromRect.height, false, - this, function (x, y, width, height) { - fromRect = new Rect(x, y, width, height); - }); - - /* to rectangle */ - transform.transformRect(toRect.x, toRect.y, toRect.width, toRect.height, false, - this, function (x, y, width, height) { - toRect = new Rect(x, y, width, height); - }); - - switch (annotationConfig.connectorPlacementType) { - case ConnectorPlacementType.Offbeat: - shape = new ConnectorOffbeat(); - break; - case ConnectorPlacementType.Straight: - shape = new ConnectorStraight(); - break; - } - - /* rotate label size to user orientation */ - var labelSize; - transform.transformRect(0, 0, annotationConfig.labelSize.width, annotationConfig.labelSize.height, false, - this, function (x, y, width, height) { - labelSize = new Size(width, height); - }); - - /* rotate panel size to user orientation */ - var panelSize = null; - transform.transformRect(0, 0, panel.size.width, panel.size.height, false, - this, function (x, y, width, height) { - panelSize = new Size(width, height); - }); - - var linePaletteItem = new PaletteItem({ - lineColor: annotationConfig.color, - lineWidth: annotationConfig.lineWidth, - lineType: annotationConfig.lineType - }); - - var hasLabel = !isNullOrEmpty(annotationConfig.label); - - /* offset rectangles */ - fromRect = new Rect(fromRect).offset(annotationConfig.offset); - toRect = new Rect(toRect).offset(annotationConfig.offset); - - var linesOffset = annotationConfig.lineWidth * 6; - - /* create connection lines */ - shape.draw(buffer, linePaletteItem, fromRect, toRect, linesOffset, 0, labelSize, panelSize, - annotationConfig.connectorShapeType, annotationConfig.labelOffset, annotationConfig.labelPlacementType, hasLabel, - connectorAnnotationOffsetResolver, function (labelPlacement, labelConfig) { - var hasLabel = !isNullOrEmpty(labelConfig.label); - if (hasLabel && labelPlacement != null) { - /* translate result label placement back to users orientation */ - transform.transformRect(labelPlacement.x, labelPlacement.y, labelPlacement.width, labelPlacement.height, true, - self, function (x, y, width, height) { - labelPlacement = new Rect(x, y, width, height); - }); - - uiHash = new RenderEventArgs(); - uiHash.context = labelConfig; - - /* draw label */ - _data.graphics.template( - labelPlacement.x - , labelPlacement.y - , 0 - , 0 - , 0 - , 0 - , labelPlacement.width - , labelPlacement.height - , _data.labelTemplate.template() - , _data.labelTemplate.getHashCode() - , _data.labelTemplate.render - , uiHash - , null - ); - } - }, annotationConfig); - connectorAnnotationOffsetResolver.resolve(); - } - - /* translate result polylines back to users orientation */ - buffer.transform(transform, true); - /* draw background polylines */ - _data.graphics.polylinesBuffer(buffer); - - _data.graphics.end(); - } - function destroy() { cleanLayout(); }; diff --git a/src/RotatedTextControl.js b/src/RotatedTextControl.js index b5a4343..94e5a7a 100644 --- a/src/RotatedTextControl.js +++ b/src/RotatedTextControl.js @@ -2,11 +2,7 @@ import Rect from './graphics/structs/Rect'; import createGraphics from './graphics/createGraphics'; import { getFixOfPixelAlignment, getInnerSize } from './graphics/dom'; import JsonML from './common/jsonml-html'; -import Transform from './graphics/Transform'; -import Shape from './graphics/shapes/Shape'; import AnnotationLabelTemplate from './templates/html/AnnotationLabelTemplate'; -import { isNullOrEmpty } from './common'; -import RenderEventArgs from './events/RenderEventArgs'; /** * Creates JavaScript Rotated Text Control diff --git a/src/ShapeAnnotationControl.js b/src/ShapeAnnotationControl.js index 35f31a6..009d602 100644 --- a/src/ShapeAnnotationControl.js +++ b/src/ShapeAnnotationControl.js @@ -2,11 +2,8 @@ import Rect from './graphics/structs/Rect'; import createGraphics from './graphics/createGraphics'; import { getFixOfPixelAlignment, getInnerSize } from './graphics/dom'; import JsonML from './common/jsonml-html'; -import Transform from './graphics/Transform'; -import Shape from './graphics/shapes/Shape'; +import ShapeAnnotation from './graphics/annotations/ShapeAnnotation'; import AnnotationLabelTemplate from './templates/html/AnnotationLabelTemplate'; -import { isNullOrEmpty } from './common'; -import RenderEventArgs from './events/RenderEventArgs'; /** * Creates JavaScript Shape Annotation Control @@ -25,7 +22,8 @@ export default function ShapeAnnotationControl(element, options) { placeholder: null, panelSize: null, graphics: null, - labelTemplate: null + labelTemplate: null, + shape: null }; if(!element) { @@ -65,6 +63,7 @@ export default function ShapeAnnotationControl(element, options) { ); _data.graphics = createGraphics(_data.element); + _data.shape = new ShapeAnnotation(); }; function cleanLayout() { @@ -101,64 +100,17 @@ export default function ShapeAnnotationControl(element, options) { _data.labelTemplate = AnnotationLabelTemplate(_data.options); cleanLayout(); createLayout(); - redraw(); + _data.shape.draw(_data.options, _data.graphics, _data.panelSize, _data.labelTemplate); } else { updateLayout(); _data.graphics.resize("placeholder", _data.panelSize.width, _data.panelSize.height); _data.graphics.begin(); - redraw(); + _data.shape.draw(_data.options, _data.graphics, _data.panelSize, _data.labelTemplate); _data.graphics.end(); } } - function redraw() { - var annotationConfig = _data.options, - shape, - uiHash, - transform = new Transform(), - panel = _data.graphics.activate("placeholder"); - - transform.size = new Size(_data.panelSize.width, _data.panelSize.height); - transform.setOrientation(annotationConfig.orientationType); - - if (annotationConfig.position != null) { - shape = new Shape(_data.graphics); - primitives.mergeObjects(shape, options); - - /* rotate label size to user orientation */ - transform.transformRect(0, 0, annotationConfig.labelSize.width, annotationConfig.labelSize.height, false, - this, function (x, y, width, height) { - shape.labelSize = new Size(width, height); - }); - - /* rotate panel size to user orientation */ - transform.transformRect(0, 0, panel.size.width, panel.size.height, false, - this, function (x, y, width, height) { - shape.panelSize = new Size(width, height); - }); - - shape.hasLabel = !isNullOrEmpty(annotationConfig.label); - - var position = annotationConfig.position; - - /* translate position to Top orientation */ - transform.transformRect(position.x, position.y, position.width, position.height, false, - this, function (x, y, width, height) { - position = new Rect(x, y, width, height); - }); - - /* offset position */ - position = new Rect(annotationConfig.position).offset(annotationConfig.offset); - - shape.labelTemplate = _data.labelTemplate; - - uiHash = new RenderEventArgs(); - uiHash.context = annotationConfig; - shape.draw(position, uiHash); - } - } - function destroy() { cleanLayout(); }; diff --git a/src/algorithms/Graph.js b/src/algorithms/Graph.js index f46dd3a..8d4ae76 100644 --- a/src/algorithms/Graph.js +++ b/src/algorithms/Graph.js @@ -1,10 +1,12 @@ import Tree from './Tree'; import FibonacciHeap from './FibonacciHeap'; + /** - * Creates graph structure + * Creates an undirected graph structure backed by adjacency lists. + * Each edge stores a context object provided by the caller. + * * @class Graph - * - * @returns {Graph} Returns graph object + * @returns {Graph} A new graph instance */ export default function Graph() { var _edges = {}, @@ -12,10 +14,12 @@ export default function Graph() { MINIMUMWEIGHT = 2; /** - * Adds edge to the graph - * @param {string} from The id of the start node - * @param {string} to The id of the end node - * @param {object} edge The edge contextual object + * Adds an undirected edge between two nodes. + * If the edge already exists, it will not be replaced. + * + * @param {string} from The starting node id + * @param {string} to The ending node id + * @param {object} edge The edge context object */ function addEdge(from, to, edge) { if ((_edges[from] == null || _edges[from][to] == null) && edge != null) { @@ -33,11 +37,11 @@ export default function Graph() { } /** - * Returns edge context object - * - * @param {string} from The edge's from node id - * @param {string} to The edge's to node id - * @returns {object} The edge's context object + * Retrieves the stored edge context object for a given pair of nodes. + * + * @param {string} from The source node id + * @param {string} to The target node id + * @returns {object|null} The edge's context object, or null if none exists */ function edge(from, to) { var result = null; @@ -48,29 +52,54 @@ export default function Graph() { } /** - * Returns true if node exists in the graph - * + * Checks whether a node exists in the graph. + * * @param {string} from The node id - * @returns {boolean} Returns true if node exists + * @returns {boolean} True if the node is present in the graph */ function hasNode(from) { return _edges.hasOwnProperty(from); } /** - * Callback for iterating edges of the graph's node - * + * Callback invoked for each edge during iteration. + * * @callback onEdgeCallback - * @param {string} to The neighboring node id - * @param {Object} edge The edge's context object + * @param {string} from The start node id + * @param {string} to The end node id + * @param {Object} edge The edge context object */ /** - * Loop edges of the node - * - * @param {object} thisArg The callback function invocation context - * @param {string} itemid The node id - * @param {onEdgeCallback} onEdge A callback function to call for every edge of the node + * Iterates over all edges in the graph. + * + * @param {object} thisArg Execution context for the callback + * @param {onEdgeCallback} onEdge Callback invoked for each edge + */ + function loopEdges(thisArg, onEdge) { + var neighbours, fromKey, toKey; + if (onEdge != null) { + for (fromKey in _edges) { + if (_edges.hasOwnProperty(fromKey)) { + neighbours = _edges[fromKey]; + if (neighbours != null) { + for (toKey in neighbours) { + if (neighbours.hasOwnProperty(toKey)) { + onEdge.call(thisArg, fromKey, toKey, neighbours[toKey]); + } + } + } + } + } + } + } + + /** + * Iterates over all edges connected to a specific node. + * + * @param {object} thisArg Execution context for the callback + * @param {string} itemid The node id whose edges to iterate + * @param {onEdgeCallback} onEdge Callback invoked for each connected edge */ function loopNodeEdges(thisArg, itemid, onEdge) { var neighbours, neighbourKey; @@ -87,20 +116,20 @@ export default function Graph() { } /** - * Callback function for iterating graphs nodes - * + * Callback for node iteration functions. + * * @callback onNodeCallback - * @param {string} to The next neighboring node id - * @returns {boolean} Returns true to break loop + * @param {string} to The node id visited + * @returns {boolean} Return true to stop traversal early */ /** - * Loop nodes of the graph - * - * @param {object} thisArg The callback function invocation context - * @param {string} [itemid=undefined] The optional start node id. If start node is undefined, - * function loops graphs node starting from first available node - * @param {onNodeCallback} onItem A callback function to be called for every neighboring node + * Traverses all connected nodes starting from the given node. + * If no start node is provided, traversal begins with the first available node. + * + * @param {object} thisArg Execution context for the callback + * @param {string} [startNode] Optional starting node id + * @param {onNodeCallback} onItem Callback invoked for each visited node */ function loopNodes(thisArg, startNode, onItem) { var processed = {}; @@ -152,21 +181,22 @@ export default function Graph() { } /** - * Callback for finding edge weight - * + * Returns edge weight used in certain algorithms. + * * @callback getGraphEdgeWeightCallback * @param {object} edge The edge context object - * @param {string} fromItem The edge's start node id - * @param {string} toItem The edge's end node id - * @returns {number} Returns weight of the edge + * @param {string} fromItem The start node id + * @param {string} toItem The end node id + * @returns {number} The weight of the edge */ /** - * Get maximum spanning tree. Graph may have disconnected sub graphs, so start node is necessary. - * - * @param {string} startNode The node to start searching for maximum spanning tree. Graph is not necessary connected - * @param {getGraphEdgeWeightCallback} getWeightFunc Callback function to get weight of an edge. - * @returns {tree} Returns tree structure containing maximum spanning tree of the graph + * Computes a maximum spanning tree using a priority queue. + * The graph may be disconnected; a start node is required. + * + * @param {string} startNode Node to begin spanning tree search + * @param {getGraphEdgeWeightCallback} getWeightFunc Function returning edge weight + * @returns {tree} A Tree structure containing the maximum spanning tree */ function getSpanningTree(startNode, getWeightFunc) { var result = Tree(), @@ -362,22 +392,21 @@ export default function Graph() { } /** - * Callback for returning optimal connection path for every end node. - * + * Callback invoked when a full path has been reconstructed. + * * @callback onPathFoundCallback - * @param {string[]} path An array of connection path node ids. - * @param {string} to The end node id, the connection path is found for. + * @param {string[]} path The node sequence forming the path + * @param {string} to The end node id */ /** - * Get shortest path between two nodes in graph. The start and the end nodes are supposed to have connection path. - * - * @param {object} thisArg The callback function invocation context - * @param {string} startNode The start node id - * @param {string[]} endNodes The array of end node ids. - * @param {getGraphEdgeWeightCallback} getWeightFunc Callback function to get weight of an edge. - * @param {onPathFoundCallback} onPathFound A callback function to be called for every end node - * with the optimal connection path + * Computes the shortest paths from a start node to one or more target nodes. + * + * @param {object} thisArg Execution context for callbacks + * @param {string} startNode Starting node id + * @param {string[]} endNodes Target node ids + * @param {getGraphEdgeWeightCallback} getWeightFunc Optional function returning edge weight + * @param {onPathFoundCallback} onPathFound Callback invoked when a target path is found */ function getShortestPath(thisArg, startNode, endNodes, getWeightFunc, onPathFound) { var margin = FibonacciHeap(false), @@ -457,22 +486,23 @@ export default function Graph() { } /** - * Callback for iterating path edges - * + * Callback used for filtering usable edges during DFS path search. + * * @callback onPathEdgeCallback - * @param {string} from The from node id - * @param {string} to The to node id - * @param {Object} edge The edge's context object - * @returns {boolean} Returns true if edge is usable + * @param {string} from The start node id + * @param {string} to The end node id + * @param {Object} edge The edge context object + * @returns {boolean} True if the edge may be used in traversal */ /** - * Search any path from node to node using depth first search - * - * @param {object} thisArg The callback function invocation context - * @param {string} startNode The start node id - * @param {string} endNode The end node id. - * @param {onPathEdgeCallback} onEdge A callback function to call for every edge of the node + * Finds any path between two nodes using depth-first search. + * + * @param {object} thisArg Execution context for callbacks + * @param {string} startNode The start node id + * @param {string} endNode The end node id + * @param {onPathEdgeCallback} onEdge Callback deciding whether an edge is usable + * @returns {string[]} Array of node ids forming the found path */ function dfsPath(thisArg, startNode, endNode, onEdge) { var margin = [], @@ -522,11 +552,13 @@ export default function Graph() { } /** - * Get Level Graph starting with `startNode` - * - * @param {object} thisArg The callback function invocation context - * @param {string} startNode The start node id - * @param {onPathEdgeCallback} onEdge A callback function to call for every edge of the graph + * Computes a level graph starting from a given node. + * Levels are assigned via BFS using only edges allowed by the callback. + * + * @param {object} thisArg Execution context + * @param {string} startNode The start node id + * @param {onPathEdgeCallback} onEdge Callback deciding if an edge is valid to traverse + * @returns {Graph} A new graph representing the level structure */ function getLevelGraph(thisArg, startNode, onEdge) { var level = {}, @@ -560,7 +592,7 @@ export default function Graph() { } // Create level graph, copy existing edges to the new graph - var levelGraph = Graph(); + var levelGraph = Graph(); for (currentNode in _edges) { if (level.hasOwnProperty(currentNode)) { currentLevel = level[currentNode]; @@ -580,12 +612,13 @@ export default function Graph() { } /** - * Depth first search loop - * - * @param {object} thisArg The callback function invocation context - * @param {string} startNode The start node id - * @param {onPathEdgeCallback} onEdge A callback function to call for every edge of the graph - * @param {onNodeCallback} onNode A callback function to be called for every neighboring node + * Performs a depth-first traversal starting at a node. + * Edge usability is determined by the onEdge callback. + * + * @param {object} thisArg Execution context + * @param {string} startNode The start node id + * @param {onPathEdgeCallback} onEdge Callback deciding edge usability + * @param {onNodeCallback} onNode Callback invoked for each newly visited node */ function dfsLoop(thisArg, startNode, onEdge, onNode) { var margin = [], @@ -625,6 +658,7 @@ export default function Graph() { edge: edge, hasNode: hasNode, loopNodes: loopNodes, + loopEdges: loopEdges, loopNodeEdges: loopNodeEdges, getSpanningTree: getSpanningTree, getTotalWeightGrowthSequence: getTotalWeightGrowthSequence, diff --git a/src/algorithms/Graph.test.js b/src/algorithms/Graph.test.js index 39d7b53..7be2b0c 100644 --- a/src/algorithms/Graph.test.js +++ b/src/algorithms/Graph.test.js @@ -295,3 +295,36 @@ test('dfsLoop searches graph nodes using depth first order', () => { expect(result).toBe(true); }); + +test('loopEdges function test', () => { + var items = [ + { from: 'A', to: 'B' }, { from: 'A', to: 'C' }, { from: 'A', to: 'D' }, + { from: 'B', to: 'E' }, + { from: 'C', to: 'E' }, { from: 'C', to: 'D' }, + { from: 'D', to: 'F' }, { from: 'D', to: 'J' }, + { from: 'E', to: 'Z' }, + { from: 'Z', to: 'F' } + ]; + + var graph = Graph(); + for (var index = 0; index < items.length; index += 1) { + var item = items[index]; + graph.addEdge(item.from, item.to, item); + } + + var result = []; + var expected = []; + for(var index = 0; index < items.length; index+=1) { + var item = items[index]; + expected.push([item.from, item.to]); + expected.push([item.to, item.from]); + } + + graph.loopEdges(this, function (fromNode, toNode, edge) { + result.push([fromNode, toNode]); + }); + + result.sort((a, b) => (a[0] + a[1]).localeCompare(b[0] + b[1])); + expected.sort((a, b) => (a[0] + a[1]).localeCompare(b[0] + b[1])); + expect(result).toEqual(expected); +}); \ No newline at end of file diff --git a/src/algorithms/__image_snapshots__/FamilyAlignment-Family diagram horizontal alignment with multiple cycles-snap.png b/src/algorithms/__image_snapshots__/FamilyAlignment-Family diagram horizontal alignment with multiple cycles-snap.png index 995e249..e1aa82f 100644 Binary files a/src/algorithms/__image_snapshots__/FamilyAlignment-Family diagram horizontal alignment with multiple cycles-snap.png and b/src/algorithms/__image_snapshots__/FamilyAlignment-Family diagram horizontal alignment with multiple cycles-snap.png differ diff --git a/src/algorithms/__image_snapshots__/FamilyAlignment-Family unit overlaps node between children-snap.png b/src/algorithms/__image_snapshots__/FamilyAlignment-Family unit overlaps node between children-snap.png index 551c94d..9a9d973 100644 Binary files a/src/algorithms/__image_snapshots__/FamilyAlignment-Family unit overlaps node between children-snap.png and b/src/algorithms/__image_snapshots__/FamilyAlignment-Family unit overlaps node between children-snap.png differ diff --git a/src/algorithms/__image_snapshots__/FamilyAlignment-Side by side 2 families where the left one starts one generation below-snap.png b/src/algorithms/__image_snapshots__/FamilyAlignment-Side by side 2 families where the left one starts one generation below-snap.png index 59913cc..e37ca1a 100644 Binary files a/src/algorithms/__image_snapshots__/FamilyAlignment-Side by side 2 families where the left one starts one generation below-snap.png and b/src/algorithms/__image_snapshots__/FamilyAlignment-Side by side 2 families where the left one starts one generation below-snap.png differ diff --git a/src/algorithms/__image_snapshots__/FamilyAlignment-Side by side and upside-down families-snap.png b/src/algorithms/__image_snapshots__/FamilyAlignment-Side by side and upside-down families-snap.png index b7a4447..07a2a75 100644 Binary files a/src/algorithms/__image_snapshots__/FamilyAlignment-Side by side and upside-down families-snap.png and b/src/algorithms/__image_snapshots__/FamilyAlignment-Side by side and upside-down families-snap.png differ diff --git a/src/algorithms/__image_snapshots__/FamilyAlignment-Skewed rombus layout-snap.png b/src/algorithms/__image_snapshots__/FamilyAlignment-Skewed rombus layout-snap.png index 25e251c..ba5061b 100644 Binary files a/src/algorithms/__image_snapshots__/FamilyAlignment-Skewed rombus layout-snap.png and b/src/algorithms/__image_snapshots__/FamilyAlignment-Skewed rombus layout-snap.png differ diff --git a/src/algorithms/__image_snapshots__/FamilyAlignment-Upside-down tree family layout-snap.png b/src/algorithms/__image_snapshots__/FamilyAlignment-Upside-down tree family layout-snap.png index 90fa533..0b47b8b 100644 Binary files a/src/algorithms/__image_snapshots__/FamilyAlignment-Upside-down tree family layout-snap.png and b/src/algorithms/__image_snapshots__/FamilyAlignment-Upside-down tree family layout-snap.png differ diff --git a/src/algorithms/__image_snapshots__/SpatialIndex-Matrix nesting test-snap.png b/src/algorithms/__image_snapshots__/SpatialIndex-Matrix nesting test-snap.png index fbb3589..d0254b1 100644 Binary files a/src/algorithms/__image_snapshots__/SpatialIndex-Matrix nesting test-snap.png and b/src/algorithms/__image_snapshots__/SpatialIndex-Matrix nesting test-snap.png differ diff --git a/src/algorithms/__image_snapshots__/SpatialIndex-Multi-layer test case-snap.png b/src/algorithms/__image_snapshots__/SpatialIndex-Multi-layer test case-snap.png index 54431a8..dd920aa 100644 Binary files a/src/algorithms/__image_snapshots__/SpatialIndex-Multi-layer test case-snap.png and b/src/algorithms/__image_snapshots__/SpatialIndex-Multi-layer test case-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getCrossingRectangles-Two aligned disconnected rectangles 2-snap.png b/src/algorithms/__image_snapshots__/getCrossingRectangles-Two aligned disconnected rectangles 2-snap.png index dfa18b7..876e933 100644 Binary files a/src/algorithms/__image_snapshots__/getCrossingRectangles-Two aligned disconnected rectangles 2-snap.png and b/src/algorithms/__image_snapshots__/getCrossingRectangles-Two aligned disconnected rectangles 2-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getCrossingRectangles-Two aligned disconnected rectangles-snap.png b/src/algorithms/__image_snapshots__/getCrossingRectangles-Two aligned disconnected rectangles-snap.png index d238ac5..4f615ca 100644 Binary files a/src/algorithms/__image_snapshots__/getCrossingRectangles-Two aligned disconnected rectangles-snap.png and b/src/algorithms/__image_snapshots__/getCrossingRectangles-Two aligned disconnected rectangles-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getCrossingRectangles-Two disconnected rectangles-snap.png b/src/algorithms/__image_snapshots__/getCrossingRectangles-Two disconnected rectangles-snap.png index 6fdaf19..b254065 100644 Binary files a/src/algorithms/__image_snapshots__/getCrossingRectangles-Two disconnected rectangles-snap.png and b/src/algorithms/__image_snapshots__/getCrossingRectangles-Two disconnected rectangles-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getCrossingRectangles-Two overlapping rectangles 2-snap.png b/src/algorithms/__image_snapshots__/getCrossingRectangles-Two overlapping rectangles 2-snap.png index a792f41..9488265 100644 Binary files a/src/algorithms/__image_snapshots__/getCrossingRectangles-Two overlapping rectangles 2-snap.png and b/src/algorithms/__image_snapshots__/getCrossingRectangles-Two overlapping rectangles 2-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getCrossingRectangles-Two overlapping rectangles-snap.png b/src/algorithms/__image_snapshots__/getCrossingRectangles-Two overlapping rectangles-snap.png index 79ecba8..dc2f1f8 100644 Binary files a/src/algorithms/__image_snapshots__/getCrossingRectangles-Two overlapping rectangles-snap.png and b/src/algorithms/__image_snapshots__/getCrossingRectangles-Two overlapping rectangles-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getCrossingRectangles-Window 2-snap.png b/src/algorithms/__image_snapshots__/getCrossingRectangles-Window 2-snap.png index c75f8ad..0cfdad6 100644 Binary files a/src/algorithms/__image_snapshots__/getCrossingRectangles-Window 2-snap.png and b/src/algorithms/__image_snapshots__/getCrossingRectangles-Window 2-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getCrossingRectangles-Window-snap.png b/src/algorithms/__image_snapshots__/getCrossingRectangles-Window-snap.png index 5bba78f..4b7efdb 100644 Binary files a/src/algorithms/__image_snapshots__/getCrossingRectangles-Window-snap.png and b/src/algorithms/__image_snapshots__/getCrossingRectangles-Window-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMergedRectangles-Dumbbell-snap.png b/src/algorithms/__image_snapshots__/getMergedRectangles-Dumbbell-snap.png index 17e5985..7a07325 100644 Binary files a/src/algorithms/__image_snapshots__/getMergedRectangles-Dumbbell-snap.png and b/src/algorithms/__image_snapshots__/getMergedRectangles-Dumbbell-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge 5 rectangles 2-snap.png b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge 5 rectangles 2-snap.png index 1a990bc..c93ef7c 100644 Binary files a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge 5 rectangles 2-snap.png and b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge 5 rectangles 2-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge E shape rectangles 2-snap.png b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge E shape rectangles 2-snap.png index cb3e3f9..b0d08a7 100644 Binary files a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge E shape rectangles 2-snap.png and b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge E shape rectangles 2-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge E shape rectangles-snap.png b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge E shape rectangles-snap.png index 96a0a72..58715a0 100644 Binary files a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge E shape rectangles-snap.png and b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge E shape rectangles-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge single rectangle-snap.png b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge single rectangle-snap.png index f55630b..659ce24 100644 Binary files a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge single rectangle-snap.png and b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge single rectangle-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two aligned disconnected rectangles 2-snap.png b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two aligned disconnected rectangles 2-snap.png index 4ae3809..90415fc 100644 Binary files a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two aligned disconnected rectangles 2-snap.png and b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two aligned disconnected rectangles 2-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two aligned disconnected rectangles-snap.png b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two aligned disconnected rectangles-snap.png index 91bd535..e750f7f 100644 Binary files a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two aligned disconnected rectangles-snap.png and b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two aligned disconnected rectangles-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two disconnected rectangles-snap.png b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two disconnected rectangles-snap.png index c2f87fb..480b995 100644 Binary files a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two disconnected rectangles-snap.png and b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two disconnected rectangles-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two overlapping rectangles 2-snap.png b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two overlapping rectangles 2-snap.png index fe63b60..713b5d0 100644 Binary files a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two overlapping rectangles 2-snap.png and b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two overlapping rectangles 2-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two overlapping rectangles-snap.png b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two overlapping rectangles-snap.png index 1eae0ab..e3e2893 100644 Binary files a/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two overlapping rectangles-snap.png and b/src/algorithms/__image_snapshots__/getMergedRectangles-Merge two overlapping rectangles-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMergedRectangles-Window 2-snap.png b/src/algorithms/__image_snapshots__/getMergedRectangles-Window 2-snap.png index b3b1e47..19e9200 100644 Binary files a/src/algorithms/__image_snapshots__/getMergedRectangles-Window 2-snap.png and b/src/algorithms/__image_snapshots__/getMergedRectangles-Window 2-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMergedRectangles-Window-snap.png b/src/algorithms/__image_snapshots__/getMergedRectangles-Window-snap.png index 054baee..17d9f41 100644 Binary files a/src/algorithms/__image_snapshots__/getMergedRectangles-Window-snap.png and b/src/algorithms/__image_snapshots__/getMergedRectangles-Window-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMinimumCrossingRows-Basic test case-snap.png b/src/algorithms/__image_snapshots__/getMinimumCrossingRows-Basic test case-snap.png index 37d71d3..11f3de0 100644 Binary files a/src/algorithms/__image_snapshots__/getMinimumCrossingRows-Basic test case-snap.png and b/src/algorithms/__image_snapshots__/getMinimumCrossingRows-Basic test case-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMinimumCrossingRows-Multi-layer test case-snap.png b/src/algorithms/__image_snapshots__/getMinimumCrossingRows-Multi-layer test case-snap.png index 8d22543..24a9b18 100644 Binary files a/src/algorithms/__image_snapshots__/getMinimumCrossingRows-Multi-layer test case-snap.png and b/src/algorithms/__image_snapshots__/getMinimumCrossingRows-Multi-layer test case-snap.png differ diff --git a/src/algorithms/__image_snapshots__/getMinimumCrossingRows-Nested block test case-snap.png b/src/algorithms/__image_snapshots__/getMinimumCrossingRows-Nested block test case-snap.png index 412c8ab..b10f3dc 100644 Binary files a/src/algorithms/__image_snapshots__/getMinimumCrossingRows-Nested block test case-snap.png and b/src/algorithms/__image_snapshots__/getMinimumCrossingRows-Nested block test case-snap.png differ diff --git a/src/algorithms/getFamilyLoops.js b/src/algorithms/getFamilyLoops.js index 911c25f..c141e11 100644 --- a/src/algorithms/getFamilyLoops.js +++ b/src/algorithms/getFamilyLoops.js @@ -10,11 +10,18 @@ export function Edge(from, to) { } /** - * This function finds [optimal collection of feedback edges](https://en.wikipedia.org/wiki/Feedback_arc_set) needed to be cut in - * order to eliminate loops in family structure. - * - * @param {Family} family Family structure - * @returns {Edge[]} Returns optimal collection of feedback loops + * Computes the optimal set of feedback edges that must be removed + * to eliminate all cycles in a family structure. This corresponds to + * [finding a minimum feedback arc set in the directed graph](https://en.wikipedia.org/wiki/Feedback_arc_set) formed by + * the family relationships. + * + * The function analyzes the directed dependencies inside the family, + * detects all cycles, and returns the smallest collection of edges + * whose removal makes the structure acyclic. + * + * @param {Family} family - The family structure represented as a directed graph. + * @param {boolean} [debug=false] - If true, enables diagnostic output. + * @returns {Edge[]} The minimal set of edges whose removal breaks all cycles. */ export default function getFamilyLoops(family, debug) { var loops = [], loop, @@ -24,6 +31,11 @@ export default function getFamilyLoops(family, debug) { var tempFamily = family.clone(); + /* Cleaning loops stage: use topological sorting to remove all nodes without parents. + Then use reversed topological sorting to remove all nodes without children. + The final temp family should contain only nodes that are in loops, + having both children and parents at the same time. + */ family.loopTopo(this, function (itemid) { tempFamily.removeNode(itemid); }) @@ -32,13 +44,20 @@ export default function getFamilyLoops(family, debug) { }) var cleanFamily = tempFamily.clone(); + /* Take any node in the temp family and break all its parent relations, storing them in the loops collection. + After that, repeat the previous cleaning stage: use topological and reversed topological sorting + to remove nodes that are no longer part of loops. + Repeat this until the temp family becomes empty. + */ cleanFamily.loop(this, function (itemid) { + /* take any node in temp family and break its parents relations*/ if (tempFamily.node(itemid) != null) { tempFamily.loopParents(this, itemid, function (parentid) { loops.push(new Edge(parentid, itemid)); tempFamily.removeChildRelation(parentid, itemid); return tempFamily.SKIP; }); + /* clean nodes in broken loops */ var itemsToRemove = []; tempFamily.loopTopo(this, function (itemid) { itemsToRemove.push(itemid); @@ -52,7 +71,10 @@ export default function getFamilyLoops(family, debug) { } }); - /* Invert loops */ + /* At this stage we have a copy of the original clean family + and a collection of loops we need to break to make the clean family acyclic. + So we remove the broken loops (edges) from the clean family here. + */ for (index = 0, len = loops.length; index < len; index += 1) { loop = loops[index]; if (!cleanFamily.removeChildRelation(loop.from, loop.to)) { @@ -68,6 +90,12 @@ export default function getFamilyLoops(family, debug) { return { from: from, to: to, capacity: 1, flow: 0 }; }); + /* Create two nodes, `from` and `to`, for finding the maximum flow in the graph + around and through the broken loops. + For each broken loop we create two edges: the parent node of the broken loop + is connected to the `from` node, and the child node of the broken loop + is connected to the `to` node. + */ var from = "__1000__"; var to = "__2000__"; var defaultMinimalFlow = loops.length; @@ -89,6 +117,7 @@ export default function getFamilyLoops(family, debug) { } } + /*Now, use standard algorithm to find maximum flow between `from` and `to` nodes */ var totalFlow = 0; var levelGraph = null; while (true) { @@ -150,14 +179,44 @@ export default function getFamilyLoops(family, debug) { } } + /* If the maximum flow is less than the number of initially broken loops, + then it means the graph has a more optimal set of edges to break. + */ if (totalFlow < defaultMinimalFlow) { - var residueGraph = graph.getLevelGraph(this, from, function (fromNode, toNode, edge) { - if (fromNode == edge.from) { - return edge.capacity > edge.flow; + /* Collect residue graph nodes from the graph used to find the maximum flow. + Start collecting edges from the `from` node. + Use the same logic as for searching maximum flow: + - if edge capacity > edge.flow, we can go forward + - if edge.flow > 0, we can go backward through the edge + Stop when no more new nodes are available. + The `to` node cannot be reached this time. + */ + var residueGraphNodes = {}; + residueGraphNodes[from] = true; + graph.dfsLoop(this, from, function (fromNode, toNode2, edge) { + if (fromNode == edge.from) { + return edge.capacity > edge.flow; + } else { + return edge.flow > 0; + } + }, function (foundid) { + if (!residueGraphNodes.hasOwnProperty(foundid)) { + residueGraphNodes[foundid] = true; } return false; }); + /* The minimum cut of the directed graph is the set of edges + between residue graph nodes and the inaccessible nodes of the graph. + */ + var edgesToBreak = []; + graph.loopEdges(this, function (fromKey, toKey, edge) { + if (residueGraphNodes.hasOwnProperty(fromKey) && !residueGraphNodes.hasOwnProperty(toKey) && edge.capacity == edge.flow) { + edgesToBreak.push(new Edge(fromKey, toKey)); + // console.log("Edge to break: fromKey: " + fromKey + ", toKey: " + toKey + ", edge=" + JSON.stringify(edge)); + } + }); + // graph.loopNodes(this, from, function (nodeid) { // console.log("Nodeid: " + nodeid); // graph.loopNodeEdges(this, nodeid, function (neighbour, edge) { @@ -167,41 +226,11 @@ export default function getFamilyLoops(family, debug) { // }) // }); - // var resedueNodes = []; - // residueGraph.loopNodes(this, from, function (nodeid) { - // resedueNodes.push(nodeid); - // }); - // console.log("Residue graph: " + resedueNodes.join(", ")); - - var edgesToBreak = []; - residueGraph.loopNodes(this, from, function (nodeid) { - graph.loopNodeEdges(this, nodeid, function (toNode, edge) { - if (edge.to == toNode) { - if (!residueGraph.hasNode(toNode)) { - // console.log("Edge to test: from: " + nodeid + ", to " + toNode); - var isIsolated = false; - graph.dfsLoop(this, toNode, function (fromNode, toNode2, edge) { - if (edge.from == fromNode && !residueGraph.hasNode(fromNode)) { - return true; - } - return false; - }, function (foundid) { - if (foundid == to) { - // console.log("Isolated: " + toNode + ", may access exit node" + foundid); - isIsolated = true; - return true; - } - return false; - }); - if (isIsolated) { - edgesToBreak.push(new Edge(nodeid, toNode)); - } - } - } - }); - }); + // console.log("Residue graph nodes: " + Object.keys(residueGraphNodes).join(", ")); - // collect loops to break + /* Collect loops to break. If a broken edge contains the `from` or `to` nodes, + recover the original edge from the family structure. + */ var optimizedLoops = []; var validatedFlow = 0; for (index = 0, len = edgesToBreak.length; index < len; index += 1) { diff --git a/src/algorithms/getFamilyLoops.md b/src/algorithms/getFamilyLoops.md new file mode 100644 index 0000000..e10dd75 --- /dev/null +++ b/src/algorithms/getFamilyLoops.md @@ -0,0 +1,41 @@ +# Minimum Feedback Arc Set Algorithm (getFamilyLoops) + +This algorithm finds a **minimum set of edges to remove from a directed graph** to make it acyclic. It is based on iterative loop-breaking combined with a max-flow / min-cut optimization. + +--- + +## How it works + +1. **Identify cycles in the graph** + - Remove nodes with no parents (topological sorting). + - Remove nodes with no children (reversed topological sorting). + - Remaining nodes are only those participating in cycles. + +2. **Break cycles iteratively** + - Pick a node in the remaining “temp family.” + - Remove all its parent edges and store them in a `loops` collection. + - Repeat cleaning until no nodes remain. + +3. **Construct a clean copy of the family without loops** + - Remove edges that were broken during the iterative loop-breaking. + +4. **Model broken loops in a flow network** + - Add two super-nodes (`from` and `to`). + - Connect parent nodes of broken edges to `from` and child nodes to `to`. + - Each edge keeps track of its original source/target for recovery. + +5. **Compute maximum flow in the network** + - Each unit of flow corresponds to a “cycle that must be broken.” + - If total flow < number of initially broken loops, some edges are redundant. + +6. **Determine minimum cut** + - Perform DFS from `from` node in the residual graph. + - Identify edges crossing from reachable to unreachable nodes. + - Recover original edges from the family structure. + - Replace initial loops collection with this optimized set. + +7. **Return the final set of edges** + - The result is a **minimum set of `Edge(from, to)` objects** that remove all cycles from the graph. + - It does **not find all possible minimal sets**, but the returned set is guaranteed to be minimal. + +--- \ No newline at end of file diff --git a/src/algorithms/getFamilyLoops.test.js b/src/algorithms/getFamilyLoops.test.js index c3702b9..badd933 100644 --- a/src/algorithms/getFamilyLoops.test.js +++ b/src/algorithms/getFamilyLoops.test.js @@ -142,4 +142,19 @@ test('getFamilyLoops - find optimal set of loops in the interlinked set of nodes }); expect(containsAllNodes).toBe(true); +}); + +test('getFamilyLoops - find optimal set of loops in the looped linked list, nodes should stay connected after loops removal', () => { + var family = getFamily([ + { id: 'A', parents: ['C'] }, + { id: 'D', parents: ['C'] }, + { id: 'B', parents: ['A', 'D'] }, + { id: 'C', parents: ['B'] } + ]); + + var result = getFamilyLoops(family); + + var expected = [new Edge('B', 'C')]; + + expect(result).toEqual(expected); }); \ No newline at end of file diff --git a/src/configs/RotatedTextControlConfig.js b/src/configs/RotatedTextControlConfig.js index 31e97fd..534ddfd 100644 --- a/src/configs/RotatedTextControlConfig.js +++ b/src/configs/RotatedTextControlConfig.js @@ -1,4 +1,4 @@ -import {VerticalAlignmentType, TextOrientationType, HorizontalAlignmentType} from '../enums'; +import {VerticalAlignmentType, TextOrientationType, HorizontalAlignmentType, Colors} from '../enums'; /** * @class RotatedTextControlConfig diff --git a/src/graphics/__image_snapshots__/Graphics - Draw shape having island-snap.png b/src/graphics/__image_snapshots__/Graphics - Draw shape having island-snap.png index fe4fb90..8fda29e 100644 Binary files a/src/graphics/__image_snapshots__/Graphics - Draw shape having island-snap.png and b/src/graphics/__image_snapshots__/Graphics - Draw shape having island-snap.png differ diff --git a/src/graphics/__image_snapshots__/Graphics - Draw shape-snap.png b/src/graphics/__image_snapshots__/Graphics - Draw shape-snap.png index 75ae738..02b4afa 100644 Binary files a/src/graphics/__image_snapshots__/Graphics - Draw shape-snap.png and b/src/graphics/__image_snapshots__/Graphics - Draw shape-snap.png differ diff --git a/src/graphics/__image_snapshots__/Graphics - Merge single rectangle-snap.png b/src/graphics/__image_snapshots__/Graphics - Merge single rectangle-snap.png index fa9853f..9b84132 100644 Binary files a/src/graphics/__image_snapshots__/Graphics - Merge single rectangle-snap.png and b/src/graphics/__image_snapshots__/Graphics - Merge single rectangle-snap.png differ diff --git a/src/graphics/annotations/ConnectorAnnotation.js b/src/graphics/annotations/ConnectorAnnotation.js new file mode 100644 index 0000000..7c9af7f --- /dev/null +++ b/src/graphics/annotations/ConnectorAnnotation.js @@ -0,0 +1,124 @@ +import Rect from '../structs/Rect'; +import Size from '../structs/Size'; +import PaletteItem from '../structs/PaletteItem'; +import { ConnectorPlacementType } from '../../enums'; +import Transform from '../Transform'; +import PolylinesBuffer from '../structs/PolylinesBuffer'; +import ConnectorAnnotationOffsetResolver from '../../tasks/renders/offsetResolver/ConnectorAnnotationOffsetResolver'; +import ConnectorOffbeat from '../shapes/ConnectorOffbeat'; +import ConnectorStraight from '../shapes/ConnectorStraight'; +import { isNullOrEmpty } from '../../common'; +import RenderEventArgs from '../../events/RenderEventArgs'; + +export default function ConnectorAnnotation() { + +}; + +ConnectorAnnotation.prototype.draw = function (annotationConfig, graphics, controlSize, labelTemplate) { + var shape, + uiHash, + transform = new Transform(), + panel, + buffer = new PolylinesBuffer(), + connectorAnnotationOffsetResolver = ConnectorAnnotationOffsetResolver(); + + graphics.reset("placeholder"); + panel = graphics.activate("placeholder"); + transform.size = new Size(controlSize.width, controlSize.height); + transform.setOrientation(annotationConfig.orientationType); + + if (annotationConfig.fromRectangle != null && annotationConfig.toRectangle != null) { + var fromRect = annotationConfig.fromRectangle, + toRect = annotationConfig.toRectangle; + + /* translate rectangles to Top orientation */ + /* from rectangle */ + transform.transformRect(fromRect.x, fromRect.y, fromRect.width, fromRect.height, false, + this, function (x, y, width, height) { + fromRect = new Rect(x, y, width, height); + }); + + /* to rectangle */ + transform.transformRect(toRect.x, toRect.y, toRect.width, toRect.height, false, + this, function (x, y, width, height) { + toRect = new Rect(x, y, width, height); + }); + + switch (annotationConfig.connectorPlacementType) { + case ConnectorPlacementType.Offbeat: + shape = new ConnectorOffbeat(); + break; + case ConnectorPlacementType.Straight: + shape = new ConnectorStraight(); + break; + } + + /* rotate label size to user orientation */ + var labelSize; + transform.transformRect(0, 0, annotationConfig.labelSize.width, annotationConfig.labelSize.height, false, + this, function (x, y, width, height) { + labelSize = new Size(width, height); + }); + + /* rotate panel size to user orientation */ + var panelSize = null; + transform.transformRect(0, 0, panel.size.width, panel.size.height, false, + this, function (x, y, width, height) { + panelSize = new Size(width, height); + }); + + var linePaletteItem = new PaletteItem({ + lineColor: annotationConfig.color, + lineWidth: annotationConfig.lineWidth, + lineType: annotationConfig.lineType + }); + + var hasLabel = !isNullOrEmpty(annotationConfig.label); + + /* offset rectangles */ + fromRect = new Rect(fromRect).offset(annotationConfig.offset); + toRect = new Rect(toRect).offset(annotationConfig.offset); + + var linesOffset = annotationConfig.lineWidth * 6; + + /* create connection lines */ + shape.draw(buffer, linePaletteItem, fromRect, toRect, linesOffset, 0, labelSize, panelSize, + annotationConfig.connectorShapeType, annotationConfig.labelOffset, annotationConfig.labelPlacementType, hasLabel, + connectorAnnotationOffsetResolver, function (labelPlacement, labelConfig) { + var hasLabel = !isNullOrEmpty(labelConfig.label); + if (hasLabel && labelPlacement != null) { + /* translate result label placement back to users orientation */ + transform.transformRect(labelPlacement.x, labelPlacement.y, labelPlacement.width, labelPlacement.height, true, + self, function (x, y, width, height) { + labelPlacement = new Rect(x, y, width, height); + }); + + uiHash = new RenderEventArgs(); + uiHash.context = labelConfig; + + /* draw label */ + graphics.template( + labelPlacement.x + , labelPlacement.y + , 0 + , 0 + , 0 + , 0 + , labelPlacement.width + , labelPlacement.height + , labelTemplate.template() + , labelTemplate.getHashCode() + , labelTemplate.render + , uiHash + , null + ); + } + }, annotationConfig); + connectorAnnotationOffsetResolver.resolve(); + } + + /* translate result polylines back to users orientation */ + buffer.transform(transform, true); + /* draw background polylines */ + graphics.polylinesBuffer(buffer); + } \ No newline at end of file diff --git a/src/graphics/annotations/ShapeAnnotation.js b/src/graphics/annotations/ShapeAnnotation.js new file mode 100644 index 0000000..7fe154b --- /dev/null +++ b/src/graphics/annotations/ShapeAnnotation.js @@ -0,0 +1,56 @@ +import Rect from '../structs/Rect'; +import Size from '../structs/Size'; +import Transform from '../Transform'; +import { isNullOrEmpty } from '../../common'; +import RenderEventArgs from '../../events/RenderEventArgs'; +import Shape from '../shapes/Shape' + +export default function ShapeAnnotation() { + +}; + +ShapeAnnotation.prototype.draw = function (annotationConfig, graphics, controlSize, labelTemplate) { + var shape, + uiHash, + transform = new Transform(), + panel = graphics.activate("placeholder"); + + transform.size = new Size(controlSize.width, controlSize.height); + transform.setOrientation(annotationConfig.orientationType); + + if (annotationConfig.position != null) { + shape = new Shape(graphics); + Object.assign(shape, annotationConfig); + + /* rotate label size to user orientation */ + transform.transformRect(0, 0, annotationConfig.labelSize.width, annotationConfig.labelSize.height, false, + this, function (x, y, width, height) { + shape.labelSize = new Size(width, height); + }); + + /* rotate panel size to user orientation */ + transform.transformRect(0, 0, panel.size.width, panel.size.height, false, + this, function (x, y, width, height) { + shape.panelSize = new Size(width, height); + }); + + shape.hasLabel = !isNullOrEmpty(annotationConfig.label); + + var position = annotationConfig.position; + + /* translate position to Top orientation */ + transform.transformRect(position.x, position.y, position.width, position.height, false, + this, function (x, y, width, height) { + position = new Rect(x, y, width, height); + }); + + /* offset position */ + position = new Rect(annotationConfig.position).offset(annotationConfig.offset); + + shape.labelTemplate = labelTemplate; + + uiHash = new RenderEventArgs(); + uiHash.context = annotationConfig; + shape.draw(position, uiHash); + } +} \ No newline at end of file diff --git a/src/graphics/shapes/ConnectorStraight.js b/src/graphics/shapes/ConnectorStraight.js index e3613c6..5955e13 100644 --- a/src/graphics/shapes/ConnectorStraight.js +++ b/src/graphics/shapes/ConnectorStraight.js @@ -23,6 +23,9 @@ ConnectorStraight.prototype.draw = function (buffer, linePaletteItem, fromRect, self = this; vector = new Vector(fromRect.centerPoint(), toRect.centerPoint()); + if (vector.isNull()) { + return; + } fromRect.loopEdges(function (sideVector, placementType) { fromPoint = sideVector.getIntersectionPoint(vector, true, 1.0); @@ -36,7 +39,7 @@ ConnectorStraight.prototype.draw = function (buffer, linePaletteItem, fromRect, return (toPoint != null); }); - if (fromPoint != null && toPoint != null) { + if (fromPoint != null && toPoint != null && !fromPoint.equalTo(toPoint)) { var baseVector = new Vector(fromPoint, toPoint); connectorAnnotationOffsetResolver.getOffset(baseVector, function (offsetIndex, bundleSize, direction) { var tempOffset = (offsetIndex * bundleOffset - (bundleSize - 1) * bundleOffset / 2.0) * direction; diff --git a/src/graphics/shapes/__image_snapshots__/MergedRectangles-Merge 5 rectangles-snap.png b/src/graphics/shapes/__image_snapshots__/MergedRectangles-Merge 5 rectangles-snap.png index 6d61c43..47e5fda 100644 Binary files a/src/graphics/shapes/__image_snapshots__/MergedRectangles-Merge 5 rectangles-snap.png and b/src/graphics/shapes/__image_snapshots__/MergedRectangles-Merge 5 rectangles-snap.png differ diff --git a/src/graphics/shapes/__image_snapshots__/MergedRectangles-Window 2-snap.png b/src/graphics/shapes/__image_snapshots__/MergedRectangles-Window 2-snap.png index 9442fd0..e9ee7a4 100644 Binary files a/src/graphics/shapes/__image_snapshots__/MergedRectangles-Window 2-snap.png and b/src/graphics/shapes/__image_snapshots__/MergedRectangles-Window 2-snap.png differ diff --git a/src/graphics/shapes/__image_snapshots__/MergedRectangles-Window-snap.png b/src/graphics/shapes/__image_snapshots__/MergedRectangles-Window-snap.png index da0034f..58ec388 100644 Binary files a/src/graphics/shapes/__image_snapshots__/MergedRectangles-Window-snap.png and b/src/graphics/shapes/__image_snapshots__/MergedRectangles-Window-snap.png differ diff --git a/src/graphics/structs/Vector.js b/src/graphics/structs/Vector.js index c237280..ef32d49 100644 --- a/src/graphics/structs/Vector.js +++ b/src/graphics/structs/Vector.js @@ -48,7 +48,7 @@ export default function Vector(arg0, arg1) { * @returns {boolean} Returns true if start and end points are the same. */ Vector.prototype.isNull = function () { - return this.from.x == this.to && this.from.y == this.to.y; + return this.from.x == this.to.x && this.from.y == this.to.y; }; /** diff --git a/src/index.js b/src/index.js index d401f46..e02b8b2 100644 --- a/src/index.js +++ b/src/index.js @@ -71,4 +71,7 @@ export { default as RotatedTextControlConfig } from './configs/RotatedTextContro export { default as RotatedTextControl } from './RotatedTextControl'; export { default as FamTaskManagerFactory } from './FamTaskManagerFactory'; -export { default as OrgTaskManagerFactory } from './OrgTaskManagerFactory'; \ No newline at end of file +export { default as OrgTaskManagerFactory } from './OrgTaskManagerFactory'; +export { default as ConnectorAnnotation} from './graphics/annotations/ConnectorAnnotation'; +export { default as ShapeAnnotation} from './graphics/annotations/ShapeAnnotation'; +export { default as Callout} from './graphics/shapes/Callout';