@hanica-dwa/sijs-diagram-manipulator
TypeScript icon, indicating that this package has built-in type declarations

1.3.0 • Public • Published

Diagram Manipulation Module

The Diagram Manipulation Module is responsible for manipulating the diagram. Manipulating the diagram means making edits to the various data structures in the diagram, aswell as making sure the user is able to register their own custom diagram module. It also contains multiple interfaces and types aswell as implementations for the most basic shapes such as 'shape' which is being extended by other, more complex shapes in order to inherit it's properties.

In order for collaboration to work it is required to use some of these methods together with websockets on the server. Through the websocket you can broadcast the AutoMerge changes, after receiving the changes the diagramManipulationModule will merge them with the current project status per user.

Developing

  1. Install the dependencies.
    npm i
  2. Run the tests.
    npm run test
  3. Build the module.
    npm run build
  4. Publish the module.

DiagramAutoMerger methods available through DiagramManipulator

In this readme we will take a look at the different methods provided by the module that can be used to create a diagram application. This readme also contains an explanation to the various interfaces and types declared in this module that the user should use if they want to create their own custom modules.

The DiagramManipulator extends the DiagramAutoMerger class, meaning it has access to all the DiagramAutoMerger's methods. The DiagramAutoMerger class is responsible for applying the different changes made to the diagram by using the AutoMerge API.

resetToEmptyProject()

This method simply creates a new, blank AutoMerge state using the AutoMerge.init method provided by AutoMerge. This method can be used when joining someone's project to make sure that the users project state is completely blank before fetching the new project data.

Usage

    renderer.resetToEmptyProject();

replaceProject()

The replaceProject() method creates a new autoMergeState and uses the changeData method to replace the current project data with the new project data passed as a parameter. This method can be used to create a new project.

Usage

    renderer.replaceProject({
      diagram: new Diagram({
        drawables: [],
        name: projectName,
        type: selectedDiagramType,
        uuid: uuid(),
      }),
      userData: {
        users: [newUser],
        uuid: uuid(),
      },
      uuid: uuid(),
    });
    e.preventDefault();
  };

addDrawable()

The addDrawable() method uses the changeData() method to add a drawable to the project so that it can be renderer by the renderer. Right now the addDrawable() method is used when creating a new shape or line and can be used when writing new drawable creators.

Usage

The addDrawable() method takes 1 parameter of type Drawable.

        renderer.addDrawable(
          renderer.convertToClassInstance(diagramType, {
            ...shape,
            x: dragSelection.originX,
            y: dragSelection.originY,
            width,
            height,
          } as ShapeDataStructure) as Shape
        );

removeDrawable()

The removeDrawable() method removes a drawable from the canvas and can be used when the user wants to delete a drawable from the canvas.

Usage

The removeDrawable() method takes 1 parameter of type Drawable.

          renderer.removeDrawable(
            renderer.convertToClassInstance(diagramType, selectedDrawable)
          );

changeDrawable()

The changeDrawable() method can be used to change the properties of a specific drawable. It does so by calling the changeFunction() described for each drawableType.

Usage

The changeDrawable() method takes 1 parameter of type Drawable.

           renderer.changeDrawable(drawable);

changeUserData()

The method changeUserData() can be used to change the userData of the application. This means for instance adding a user to the userData or modifying attributes of a specific user.

Usage

The method changeUserData() takes 1 parameter which is a callback function that describes what to edit in the userData array. In the example below we decide to push a newUser (of type User) into the existing userData array.

      renderer.changeUserData((userData) => {
        userData.users.push(newUser);
        return userData;
      });

applyChanges()

The 'applyChanges()' method is used by the back and front-end to apply the changes made to a project to the current project opened by the user. It is the first method that is called when a websocket connection is opened.

Usage

The applyChanges() method takes 2 parameters of which one can be null. The first parameter is of type Change[] (as described in the AutoMerge API) and the second is of type Doc (also as described in the AutoMerge API). If the latter is not given the currentProject will be used to apply the changes to.

	
      newRenderer.applyChanges(message.payload.changes);

DiagramManipulator.ts

The DiagramManipulator class extends the DiagramAutoMerger class meaning that through this class all methods from the DiagramAutoMerger are accessible. It also delivers some other methods that are important

registerModule()

In order for the DiagramManipulator to work with a specific module, the module needs to be registered with the manipulator. This can be done by calling the 'registerModule()' method. Once a module is registered the various manipulation methods provided by the DiagramManipulator can be used to edit data for the registered module.

Usage

The registerModule() method takes one parameter of type DiagramManipulatorModuleAPI, in this instance we pass our own diagram render API called BlockDiagramRenderAPI.

diagramRenderer.registerModule(new BlockDiagramRenderAPI());

getAvailableShapeTypes()

The method getAvailableShapeTypes() can be used to get all the available shape types for a specific module. This data can then be used to render the corresponding shapes by calling the method renderShape() of the renderer class.

Usage

The getAvailableShapeTypes() method takes 1 parameter of type string to determine the diagramType.

diagramManipulator.getAvailableShapeTypes(
      BLOCK_DIAGRAM_TYPE
    );

getNewShape(), getExampleShape(), getNewLine() & getExampleLine()

The methods getNewShape(), getExampleShape(), getNewLine() & getExampleLine() are all quite similar, we will only take a closer look at getNewShape.

getNewShape receives a diagramType and a shapeType and will check if the diagramType is supported by a module. If this is true AND the shape is supported by the module a new uuid will be created and a new shape (or example shape in case of the getExampleShape()/getExampleLine() method..) will be returned. This can be useful for things such as displaying shapes in a sidebar (you could take the exampleShape for this) or for statically placing shapes on the canvas.

Usage

The getNewShape() method gets 2 parameters, the diagramType which should be a string and a shapeType of type string. For the lines it works a bit different, they receive a lineType of type string.

renderer.getNewShape(diagramType, selectedShape);
renderer.getNewLine(diagramType, selectedLine);

convertToClassInstance()

The method convertToClassInstance() can be used to convert a specific drawable to an instance of the corresponding class, this is useful when placing or deleting drawables.

Usage

The method 'convertToClassInstance()' takes two parameters, the diagramType and dataStructure.

renderer.convertToClassInstance(diagramType, selectedDrawable)

//Usage when using together with addDrawable()

        renderer.addDrawable(
          renderer.convertToClassInstance(BLOCK_DIAGRAM_TYPE, {
            ...shape,
            x: dragSelection.originX,
            y: dragSelection.originY,
            width,
            height,
          } as ShapeDataStructure) as Shape
        );

Interfaces and types

The DiagramManipulator provides multiple interfaces and types of which some are required to be usued in order for the typing to be secure.

Project.ts

Project.ts simply declares an interface of the Project datastructure. A Project is basically the entity that contains all information such as the diagram, userData and an uuid to identify the project with. When writing your own diagram editor you can add the typing of 'Project' to the various state methods in order for it to be typesafe.

export interface Project {
  diagram: DiagramDataStructure;
  userData: UserData;
  uuid: string;
}

Diagram.ts

Diagram.ts declares a class which implements the DiagramDataStructure. The DiagramDataStructure contains the basic types of the Diagram such as an uuid, name, type and an array of drawables which are based on the DrawableDataStructure.

Usage

When creating a new Project a new Diagram has to be initialized. In our case we set the type based on a selection box and the name based on the project name submitted when creating the new diagram. Since we're creating a new Project we will replace the old one by calling 'renderer.replaceProject()' which is described further above in this readme. In this example we set the drawables array to empty since we're creating a blank object.

    renderer.replaceProject({
      diagram: new Diagram({
        drawables: [],
        name: projectName,
        type: selectedDiagramType,
        uuid: uuid(),
      }),
      userData: {
        users: [newUser],
        uuid: uuid(),
      },
      uuid: uuid(),
    });

UserData.ts

The file UserData.ts contains two interfaces, User and UserData.

The User interface contains the basic attributes of an user such as the uuid, username, color (the color which indicates which user is who) and a selectedShape. The UserData interface simply contains an array of User and an uuid. When creating custom Shapes the UserData interface is quite important since it can be used to be able to show, based on the users, which user selected which shape. This process is explained in further detail on our Wiki.

ShapeDataStructure.ts & Shape.ts

ShapeDataStructure

The ShapeDataStructure.ts file contains an interface for the ShapeDataStructure. This datastructure extends from the most primal datastructure which is Drawable data structure. ShapeDataStructure is used when creating new shapes or when specificying the return type of a method. The ShapeDataStructure inherits all properties from DrawableDataStructure such as uuid, drawableType and zIndex. It adds a drawableType, shapeType, x, y, width, height, isDraggable and isSelectable to this. When creating your own Shapes it is important to let that datastructure extend the ShapeDataStructure in order for the various manipulation methods to work.

Usage

To apply the ShapeDataStructure to our custom shape's data structure we simply let it extend from ShapeDataStructure. This will let the custom shape inherit all properties of the ShapeDataStructure and we only have to declare new properties if we desire so.

export interface BlockDataStructure extends ShapeDataStructure {
  color: string;
}

Shape

Shape.ts is a file which contains the basic class for a shape. When creating a shape it is important that the Shape extends from the Shape class. The Shape class provides basic functionality to edit the properties of a shape.

Usage

Simply let your custom shape extend from Shape like so:

class Block extends Shape implements BlockDataStructure { }

The process of creating a custom Shape is explained in further detail on the wiki. Wiki The wiki contains a complete a-z tutorial on how to create a custom Shape and explain more about using the Shape class and the ShapeDataStructure interface.

LineDataStructure.ts & Line.ts

The Line file acts the same as the Shape files except it applies to Lines. It provides basic functionality for changing the default data of a Line. When creating a custom Line it is important to let it extend the Line class. The same applies for the LineDataStructure.

The LineDataStructure interface is quite a lot different than the ShapeDataStructure so we will take a look at this one. It extends the DrawableDataStructure and adds quite a few different attributes to it. The startCoords, endCoords, startShapeUUID, endShapeUUID and waypoints are interesting.

The first 4 are optional, a line does however need atleast 2 of these in order to be drawable on the canvas. If a line is NOT connected to a shape the startCoords and endCoords should be defined. If this is not the case the startShapeUUID and endShapeUUID is sufficient since these will lock the coordinates of the Line to the coordinates of the connected Shapes. The waypoints contain an array of Coordinates which consists of a X and Y. When adding a waypoint to a line the X and Y of this waypoint are pushed into this array.

interface LineDataStructure extends DrawableDataStructure {
  drawableType: LineDrawableType;
  lineType: string;
  startCoords?: Coordinates;
  endCoords?: Coordinates;
  startShapeUUID?: string;
  endShapeUUID?: string;
  waypoints: Coordinates[];
}

PropertyDefinitions.ts

The PropertyDefinitions file contains 7 type declarations which are used for editing the properties of a drawable. When creating a custom shape the programmer has to create a property definition of their shape. To make this type safe the custom made property definition has to have 'PropertiesDefinition' as their return type. A shape contains multiple properties thus why it is an array of property definitions.

export declare type PropertyDefinition = {
  key: string;
  name: string;
  value: PropertyValue;
};

export declare type PropertiesDefinition = {
  properties: PropertyDefinition[];
};

Usage

The PropertiesDefinition interface can be used by letting the custom properties definition return a value of type PropertiesDefinition. This will make sure all the values in the properties array are compatible with the properties in order for them to be able to change.

const BlockPropertiesDefinition: PropertiesDefinition = {
  properties: [
    { key: 'color', name: 'Color', value: '#00ff00' },
    { key: 'x', name: 'x position', value: 0 },
    { key: 'y', name: 'y position', value: 0 },
  ],
};

Misc

DiagramManipulatorModuleAPI.ts

In the DiagramManipulatorModuleAPI file the interface for the DiagramManipulatorModuleAPI is declared and exported. This interface contains multiple method declarations. This interface MUST be implemented if the user where to make their own diagramManipulationAPI in order for it to work with the DiagramManipulator and renderer.

ManipulationCallbacks.ts

The ManipulationCallbacks file contains an interface which holds declarations for 2 methods, updateShapePosition and selectShape. This interface MUST be included when making a new shape in order for the shape to be selectable and be able to be drag- and drop-able.

DefaultDiagramManipulationModule

The diagramManipulator also provides the DefaultDiagramManipulationModule for a few simple shapes which can be applied to any diagram. The DefaultDiagramManipulationModule works the same as any other DiagramManipulationModule so we will not go into further detail. On the (wiki)[https://github.com/HANICA-DWA/feb2020-project-sijs/wiki/11.-Tutorials] the chapter 'Creating your own manipulation module' provides an extensive overview of how a diagram manipulation module works.

Readme

Keywords

none

Package Sidebar

Install

npm i @hanica-dwa/sijs-diagram-manipulator

Weekly Downloads

1

Version

1.3.0

License

ISC

Unpacked Size

117 kB

Total Files

96

Last publish

Collaborators

  • rbrtrbrt
  • raymond.debruine
  • sanderl
  • bartvanderwal
  • blastinvoke
  • jeroenkleingeltink
  • dmvanderuit
  • geert-jan
  • sjoerdhaerkens