Skip to content

Creating an OPM GEF Editor – Part 13: Adding Procedural Links

Last updated on 2014-09-07

Previous Tutorial: Creating a GEF Editor – Part 12: Enable Save Action on the Editor

In this tutorial we’ll be adding more link kinds to our editor. OPM has to main link kinds:

  • Procedural Links: links that denote some kind of procedural relation between two things in the diagram. They normally connect between an Object and a Process.
  • Structural Links: links that denote some kind of structural relation between two things in the diagram.

Procedural links are easy to add since they act like normal links just with small changes in the decorations that the links have. Structural links are more complicated and a full tutorial will be dedicated to adding them.

The hardest part in adding things to the editor is doing changes in the model without breaking things because the EMF generator, although very advanced, has some drawbacks that leave a lot of work for the developer (like deleting generated classes of model entities that were deleted). It also doesn’t support refactoring, which is a big drawback. But still the added benefit of using it overcome the problems. While preparing for this tutorial I also prepared the model for structural links, so please ignore them.

But enough mumblings, lets get going!

  1. First we are going to do some small changes and additions to the model. In OPM there are a number of entities that can be connected with links, and we only modeled this in things. Therefore we’ll create a new EClass that will be supertype of all classes that can have connections. We’ll call this class OPMNode and transfer the incomingLinks and outgoingLinks references to this class. Furthermore, we have to update the OPMThing class so that it is now derived from OPMNode which lets usa remove the local incomingLinks and outgoingLinks references). We must also update the type of OPMLink ‘s references to OPMNode. Furthermore, we add two more classes to our model: OPMProceduralLink – which models a procedural link and extends OPMLink; and OPMStructuralLinkAggregator – which will be used in the next tutorial to create structural links. Lastly, both structural links (actually the aggregators) and procedural links can be of many kinds, which is modeled by having a kindenum defined in the model (OPMStructuralLinkAggregatorKind and OPMProceduralLinkKind respectively).
    A purist OOP programmer would say that procedural links should be implemented with inheritance, where every link kind would have a class of itself and would implements it’s own functionality. But from my view is that the most important this is understandability, and having so many entities in the model that are differentiated by very small functionality (as you will see later) is not good. Better to have a couple of small switch statements in the OPMProceduralLink than creating a larger hierarchy that confuses the reader of the code.
    To save you some work (and me writing all of the specific changes here), the updated model file can be found here
  2. After re-generating the code from the model, there are two editor classes that need modification: OPMLinkDeleteCommand uses a OPMThing handle to store the source and target nodes, but the newly generated classes return OPMNode instances, therefore we must change these references. A very similar thing happens in OPMThingDeleteCommand, where the existing links are disconnected and their sources and targets are saved as OPMThing instances, which must now be generalized to OPMNode (note that at a later stage we should pass all commands and check which ones should be refactored to accept OPMNode instead of OPMThing and we should also probably create a OPMNodeEditPart to abstain from duplicating code).
    Since this changes are not too interesting they are not showed and the final files can be accessed here: OPMLinkDeleteCommand, OPMThingDeleteCommand.
  3. Now for some real work. The agent and instrument procedural links are drawn like regular links only with a circle decoration at their target (black filled for the agent, white filled for the instrument). Since there is currently no circle decoration in GEF, we had to implement our own, which was very interesting stuff. A link decoration is a figure that can be rotated relative to a reference point, as prescribed in the RotatableDecoration interface. While a circle does not need rotation, the location of the circle in the diagram must be calculated when the link endpoint moves. Here is how I implemented it:
    package com.vainolo.phd.opm.gef.editor.figure;
    
    import org.eclipse.draw2d.Ellipse;
    import org.eclipse.draw2d.PolylineConnection;
    import org.eclipse.draw2d.RotatableDecoration;
    import org.eclipse.draw2d.geometry.Point;
    import org.eclipse.draw2d.geometry.Rectangle;
    
    /**
     * A decoration for {@link PolylineConnection} endpoints, which draws a circlE
     * whose center is located on the link and is distanced RADIUS (internal parameter)
     * units from the end of the connection.
     */
    public class CircleDecoration extends Ellipse implements RotatableDecoration {
    
        private static final int RADIUS = 5;
        private Point location = new Point();
    
        public CircleDecoration() {
            super();
        }
    
        @Override public void setLocation(Point p) {
            location = p;
            Rectangle bounds = new Rectangle(location.x-RADIUS, location.y-RADIUS, RADIUS*2, RADIUS*2);
            setBounds(bounds);
        }
    
        @Override public void setReferencePoint(Point p) {
            // length of line between reference point and location
            double d = Math.sqrt(Math.pow((location.x-p.x), 2)+Math.pow(location.y-p.y,2));
    
            // do nothing if link is too short.
            if(d < RADIUS)
                return;
    
            //
            // using pythagoras theorem, we have a triangle like this:
            //
            //      |       figure       |
            //      |                    |
            //      |_____(l.x,l.y)______|
            //                (\)
            //                | \(r.x,r.y)
            //                | |\
            //                | | \
            //                | |  \
            //                | |   \
            //                |_|(p.x,p.y)
            //
            // We want to find a point that at RADIUS distance from l (location) on the line between l and p
            // and center our circle at this point.
            //
            // I you remember your school math, let the distance between l and p (calculated
            // using pythagoras theorem) be defined as d. We want to find point r where the
            // distance between r and p is d-RADIUS (the same as saying that the distance
            // between l and r is RADIUS). We can do this using triangle identities.
            //     |px-rx|/|px-lx|=|py-ry|/|py-ly|=|d-RADIUS|/d
            //
            // we use
            //  k = |d-RADIUS|/d
            //  longx = |px-lx|
            //  longy = |py-xy|
            //
            // remember that d > RADIUS.
            //
            double k = (d-RADIUS)/d;
            double longx = Math.abs(p.x-location.x);
            double longy = Math.abs(p.y-location.y);
    
            double shortx = k*longx;
            double shorty = k*longy;
    
            // now create locate the new point using the distances depending on the location of the original points.
            int rx, ry;
            if(location.x < p.x) {
                rx = p.x - (int)shortx;
            } else {
                rx = p.x + (int)shortx;
            }
            if(location.y > p.y) {
                ry = p.y + (int)shorty;
            } else {
                ry = p.y - (int)shorty;
            }
    
            // For reasons that are still unknown to me, I had to increase the radius
            // of the circle for the graphics to look right.
            setBounds(new Rectangle(rx-RADIUS, ry-RADIUS, (int)(RADIUS*2.5), (int)(RADIUS*2.5)));
        }
    }
    

    The final result is not completely to my expectations, but value for money it is good enough.

  4. All the other decorations that we need are provided by the framework, so we can now implement the OPMProceduralLinkEditPart whose only job is to decorate the link created by OPMLinkEditPart with the correct decoration depending on the kind of the link in the model:
    package com.vainolo.phd.opm.gef.editor.part;
    
    import org.eclipse.draw2d.ColorConstants;
    import org.eclipse.draw2d.PolylineConnection;
    import org.eclipse.draw2d.PolylineDecoration;
    
    import com.vainolo.phd.opm.gef.editor.figure.CircleDecoration;
    import com.vainolo.phd.opm.model.OPMProceduralLink;
    import com.vainolo.phd.opm.model.OPMProceduralLinkKind;
    
    /**
     * An extension of a {@link OPMLinkEditPart} used for
     * {@link OPMProceduralLink} instances.
     * It add endpoint decorations to the regular link figure.
     * @author vainolo
     *
     */
    public class OPMProceduralLinkEditPart extends OPMLinkEditPart {
    
        /**
         * Extend the connection creted by {@link OPMLinkEditPart#createFigure()} by adding
         * decorations depending on the link kind.
         * An agent link is decorated at the target with black filled {@link CircleDecoration}.
         * An instrument link is decorated at the target with a white filled {@link CircleDecoration}.
         * A consumption or result link is decorated at the target with a {@link PolylineDecoration}
         * (which is an arrow).
         * An effect link link is decorated at the source and target with a {@link PolylineDecoration}.
         * @return a decorated {@link PolylineConnection} figure.
         */
        @Override
        protected PolylineConnection createFigure() {
            PolylineConnection connection = super.createFigure();
            OPMProceduralLink model = (OPMProceduralLink) getModel();
            decorateConnection(connection, model.getKind());
            return connection;
        }
    
        /**
         * Decorate a connection depending on its kind.
         * @param connection the {@link PolylineConnection} to decorate.
         * @param kind the {@link OPMProceduralLinkKind} of the model entity.
         */
        private void decorateConnection(PolylineConnection connection, OPMProceduralLinkKind kind) {
            switch (kind) {
            case AGENT:
                CircleDecoration agentDecoration = new CircleDecoration();
                agentDecoration.setBackgroundColor(ColorConstants.black);
                agentDecoration.setFill(true);
                connection.setTargetDecoration(agentDecoration);
                break;
            case INSTRUMENT:
                CircleDecoration instrumentDecoration = new CircleDecoration();
                instrumentDecoration.setBackgroundColor(ColorConstants.white);
                instrumentDecoration.setFill(true);
                connection.setTargetDecoration(instrumentDecoration);
                break;
            case CONSUMPTION:
            case RESULT:
                connection.setTargetDecoration(new PolylineDecoration());
                break;
            case EFFECT:
                connection.setSourceDecoration(new PolylineDecoration());
                connection.setTargetDecoration(new PolylineDecoration());
                break;
            default:
                throw new IllegalArgumentException("No case for kind "+kind);
            }
        }
    }
    
  5. The newly created EditPart must be created by the EditPartFactory when a OPMProceduralLink model instance is received by the framework, so we must update the OPMEditPartFactory. We also added a runtime exception when the factory is given a model class that it doesn’t recognize. I added this because while the framework fails when the factory provides it null edit parts, this way the problem can be found faster and tracing it is easier.
    package com.vainolo.phd.opm.gef.editor.part;
    
    import com.vainolo.phd.opm.model.OPMLink;
    import com.vainolo.phd.opm.model.OPMObject;
    import com.vainolo.phd.opm.model.OPMObjectProcessDiagram;
    import com.vainolo.phd.opm.model.OPMProceduralLink;
    import com.vainolo.phd.opm.model.OPMProcess;
    
    import org.eclipse.gef.EditPart;
    import org.eclipse.gef.EditPartFactory;
    
    public class OPMEditPartFactory implements EditPartFactory {
    
        @Override
        public EditPart createEditPart(EditPart context, Object model) {
            EditPart part = null;
    
            if (model instanceof OPMObjectProcessDiagram) {
                part = new OPMObjectProcessDiagramEditPart();
            } else if (model instanceof OPMObject) {
                part = new OPMObjectEditPart();
            } else if (model instanceof OPMProcess) {
                part = new OPMProcessEditPart();
            } else if (model instanceof OPMProceduralLink) {
                // It is important for OPMProceduralLink to be before OPMLink because
                // they have an is-a relation and we would get the wrong EditPart.
                part = new OPMProceduralLinkEditPart();
            } else if (model instanceof OPMLink) {
                part = new OPMLinkEditPart();
            } else {
                throw new IllegalArgumentException("Model class "+model.getClass()+" not supported yet.");
            }
    
            if (part != null) {
                part.setModel(model);
            }
    
            return part;
        }
    }
    
  6. We must now provide new tools to create the procedural links. This is the place where the links are differentiated – in the CreationFactory that produces them (“Ah!” you are probably saying, “so you still had to implement lots of classes”. Well yes, but these are pure utility classes that are written once and forgotten for eternity. Unlike this, a class that exists in the model hierarchy has a large influence on the readability of the model. And 5 classes are even worse). We create one factory for each procedural link kind:
    package com.vainolo.phd.opm.gef.editor.factory;
    
    import org.eclipse.gef.requests.CreationFactory;
    
    import com.vainolo.phd.opm.model.OPMFactory;
    import com.vainolo.phd.opm.model.OPMProceduralLink;
    import com.vainolo.phd.opm.model.OPMProceduralLinkKind;
    
    /**
     * Factory used by palette tools to create {@link OPMProceduralLink} of
     * {@link OPMProceduralLinkKind#AGENT} kind.
     */
    public class OPMAgentLinkFactory implements CreationFactory {
    
        @Override
        public Object getNewObject() {
            OPMProceduralLink link = OPMFactory.eINSTANCE.createOPMProceduralLink();
            link.setKind(OPMProceduralLinkKind.AGENT);
            return link;
        }
    
        @Override
        public Object getObjectType() {
            return OPMProceduralLink.class;
        }
    }
    
    package com.vainolo.phd.opm.gef.editor.factory;
    
    import org.eclipse.gef.requests.CreationFactory;
    
    import com.vainolo.phd.opm.model.OPMFactory;
    import com.vainolo.phd.opm.model.OPMProceduralLink;
    import com.vainolo.phd.opm.model.OPMProceduralLinkKind;
    
    /**
     * Factory used by palette tools to create {@link OPMProceduralLink} of
     * {@link OPMProceduralLinkKind#CONSUMPTION} kind.
     */
    public class OPMConsumptionLinkFactory implements CreationFactory {
    
        @Override
        public Object getNewObject() {
            OPMProceduralLink link = OPMFactory.eINSTANCE.createOPMProceduralLink();
            link.setKind(OPMProceduralLinkKind.CONSUMPTION);
            return link;
        }
    
        @Override
        public Object getObjectType() {
            return OPMProceduralLink.class;
        }
    }
    
    package com.vainolo.phd.opm.gef.editor.factory;
    
    import org.eclipse.gef.requests.CreationFactory;
    
    import com.vainolo.phd.opm.model.OPMFactory;
    import com.vainolo.phd.opm.model.OPMProceduralLink;
    import com.vainolo.phd.opm.model.OPMProceduralLinkKind;
    
    /**
     * Factory used by palette tools to create {@link OPMProceduralLink} of
     * {@link OPMProceduralLinkKind#EFFECT} kind.
     */
    public class OPMEffectLinkFactory implements CreationFactory {
    
        @Override
        public Object getNewObject() {
            OPMProceduralLink link = OPMFactory.eINSTANCE.createOPMProceduralLink();
            link.setKind(OPMProceduralLinkKind.EFFECT);
            return link;
        }
    
        @Override
        public Object getObjectType() {
            return OPMProceduralLink.class;
        }
    }
    
    package com.vainolo.phd.opm.gef.editor.factory;
    
    import org.eclipse.gef.requests.CreationFactory;
    
    import com.vainolo.phd.opm.model.OPMFactory;
    import com.vainolo.phd.opm.model.OPMProceduralLink;
    import com.vainolo.phd.opm.model.OPMProceduralLinkKind;
    
    /**
     * Factory used by palette tools to create {@link OPMProceduralLink} of
     * {@link OPMProceduralLinkKind#INSTRUMENT} kind.
     */
    public class OPMInstrumentLinkFactory implements CreationFactory {
    
        @Override
        public Object getNewObject() {
            OPMProceduralLink link = OPMFactory.eINSTANCE.createOPMProceduralLink();
            link.setKind(OPMProceduralLinkKind.INSTRUMENT);
            return link;
        }
    
        @Override
        public Object getObjectType() {
            return OPMProceduralLink.class;
        }
    }
    
    package com.vainolo.phd.opm.gef.editor.factory;
    
    import org.eclipse.gef.requests.CreationFactory;
    
    import com.vainolo.phd.opm.model.OPMFactory;
    import com.vainolo.phd.opm.model.OPMProceduralLink;
    import com.vainolo.phd.opm.model.OPMProceduralLinkKind;
    
    /**
     * Factory used by palette tools to create {@link OPMProceduralLink} of
     * {@link OPMProceduralLinkKind#RESULT} kind.
     */
    public class OPMResultLinkFactory implements CreationFactory {
    
        @Override
        public Object getNewObject() {
            OPMProceduralLink link = OPMFactory.eINSTANCE.createOPMProceduralLink();
            link.setKind(OPMProceduralLinkKind.RESULT);
            return link;
        }
    
        @Override
        public Object getObjectType() {
            return OPMProceduralLink.class;
        }
    }
    

    And finally, we create the tools for the links:

    package com.vainolo.phd.opm.gef.editor;
    
    import org.eclipse.gef.palette.ConnectionCreationToolEntry;
    import org.eclipse.gef.palette.CreationToolEntry;
    import org.eclipse.gef.palette.PaletteGroup;
    import org.eclipse.gef.palette.PaletteRoot;
    import org.eclipse.gef.palette.SelectionToolEntry;
    
    import com.vainolo.phd.opm.gef.editor.factory.OPMAgentLinkFactory;
    import com.vainolo.phd.opm.gef.editor.factory.OPMConsumptionLinkFactory;
    import com.vainolo.phd.opm.gef.editor.factory.OPMEffectLinkFactory;
    import com.vainolo.phd.opm.gef.editor.factory.OPMInstrumentLinkFactory;
    import com.vainolo.phd.opm.gef.editor.factory.OPMLinkFactory;
    import com.vainolo.phd.opm.gef.editor.factory.OPMObjectFactory;
    import com.vainolo.phd.opm.gef.editor.factory.OPMProcessFactory;
    import com.vainolo.phd.opm.gef.editor.factory.OPMResultLinkFactory;
    import com.vainolo.phd.opm.gef.editor.tool.CreationAndDirectEditTool;
    
    /**
     * Tool pallete for the {@link OPMGraphicalEditor}.
     */
    public class OPMGraphicalEditorPalette extends PaletteRoot {
    
        PaletteGroup group;
    
        public OPMGraphicalEditorPalette() {
            addGroup();
            addSelectionTool();
            addOPMObjectTool();
            addOPMProcessTool();
            addOPMLinkTool();
            addOPMProceduralLinkTools();
        }
    
        private void addSelectionTool() {
            SelectionToolEntry entry = new SelectionToolEntry();
            group.add(entry);
            setDefaultEntry(entry);
        }
    
        private void addGroup() {
            group = new PaletteGroup("OPM Controls");
            add(group);
        }
    
        private void addOPMObjectTool() {
            CreationToolEntry entry = new CreationToolEntry("OPMObject", "Create a new Object", new OPMObjectFactory(), null, null);
            entry.setToolClass(CreationAndDirectEditTool.class);
            group.add(entry);
        }
    
        private void addOPMProcessTool() {
            CreationToolEntry entry = new CreationToolEntry("OPMProcess", "Create a new Process", new OPMProcessFactory(), null, null);
            entry.setToolClass(CreationAndDirectEditTool.class);
            group.add(entry);
        }
    
        private void addOPMLinkTool() {
            ConnectionCreationToolEntry entry = new ConnectionCreationToolEntry("Link", "Creates a new link", new OPMLinkFactory(), null, null);
            group.add(entry);
        }
    
        /**
         * Add tools to create procedural links in the diagram.
         */
        private void addOPMProceduralLinkTools() {
            ConnectionCreationToolEntry entry;
            entry = new ConnectionCreationToolEntry("Agent", "Create a new Agent link", new OPMAgentLinkFactory(), null, null);
            group.add(entry);
            entry = new ConnectionCreationToolEntry("Instrument", "Create a new Instrument link", new OPMInstrumentLinkFactory(), null, null);
            group.add(entry);
            entry = new ConnectionCreationToolEntry("Consumption", "Create a new Consumption link", new OPMConsumptionLinkFactory(), null, null);
            group.add(entry);
            entry = new ConnectionCreationToolEntry("Result", "Create a new Result link", new OPMResultLinkFactory(), null, null);
            group.add(entry);
            entry = new ConnectionCreationToolEntry("Effect", "Create a new Effect link", new OPMEffectLinkFactory(), null, null);
            group.add(entry);
        }
    }
    
  7. That’s it!. Fire your editor and add some procedural links. Check how this looks in my editor:

    Finally starting to look like a real editor.

You can find the final project files here.

Next Tutorial: Creating an OPM GEF Editor – Part 14: Refactoring, Refactoring and More Refactoring

Published inProgramming

6 Comments

  1. Ups Ups

    The model file you link at the end of step 1 is not the right model for this tutorial. It seems like a much more complicated model. The correct one is included in the project files at the end though.

    In the CircleDecoration setReferencePoint method, the code can be simplified a bit like so:

    double k = (d-RADIUS)/d;
    double longx = p.x-location.x;
    double longy = p.y-location.y;
    double shortx = k*longx;
    double shorty = k*longy;

    int rx, ry;
    rx = p.x – (int)shortx;
    ry = p.y – (int)shorty;

    Here longx and longy give us the relative coordinates of r instead of the distances so we don’t need the if statements.

    • admin admin

      Thanks! I knew there had to be a nicer way to do it. I also fixed the link; Thanks for pointing it out.

  2. Jon Sorensen Jon Sorensen

    In case you are interested here’s what I think is a more object oriented way to support the various types of Procedural Links with an enum.

    Instead of defining the Enum in the ECore model I defined a plain old Java Enum as follows. A nice feature of JAVA is that an Enum can have an abstract interface which each enumeration must implement. This way does away with switch statements because the calling logic will call the interface of the Enum for a given function (see OPMProceduralLinkEditPart.createFigure() in Step 2). It also centralizes the implementation for the various cases (enumerations), which I believe is easier to maintain.

    // Step 1
    // Define a JAVA Enum with the interface needed to support all of the functional behaviours
    // exhibited by the various types of procedural links. In this case we just have decorateConnection(…).

    package jon.sandbox.model.opm.helper;

    import org.eclipse.draw2d.ColorConstants;
    import org.eclipse.draw2d.PolylineConnection;
    import org.eclipse.draw2d.PolylineDecoration;

    public enum OPMProceduralLinkKind2
    {
    eAgent
    {
    @Override
    public void decorateConnection(PolylineConnection connection)
    {
    CircleDecoration agentDecoration = new CircleDecoration();
    agentDecoration.setBackgroundColor(ColorConstants.black);
    agentDecoration.setFill(true);
    connection.setTargetDecoration(agentDecoration);
    }
    },
    eInstrument
    {
    @Override
    public void decorateConnection(PolylineConnection connection)
    {
    CircleDecoration instrumentDecoration = new CircleDecoration();
    instrumentDecoration.setBackgroundColor(ColorConstants.white);
    instrumentDecoration.setFill(true);
    connection.setTargetDecoration(instrumentDecoration);
    }
    },
    eConsumption
    {
    @Override
    public void decorateConnection(PolylineConnection connection)
    {
    connection.setTargetDecoration(new PolylineDecoration());
    }
    }
    ,
    eResult
    {
    @Override
    public void decorateConnection(PolylineConnection connection)
    {
    connection.setTargetDecoration(new PolylineDecoration());
    }
    },
    eEffect
    {
    @Override
    public void decorateConnection(PolylineConnection connection)
    {
    connection.setSourceDecoration(new PolylineDecoration());
    connection.setTargetDecoration(new PolylineDecoration());
    }
    };

    public abstract void decorateConnection(PolylineConnection connection);
    }

    // Step 2
    // a. Modify the ECore model to have an EDataType that wraps OPMProceduralLinkKind2
    // b. Update OPMProceduralLink to use OPMProceduralLinkKind2 for the “kind”
    // c. Re-generate the model and fix the various compilation errors

    public class OPMProceduralLinkEditPart extends OPMLinkEditPart
    {
    @Override
    protected PolylineConnection createFigure()
    {
    PolylineConnection connection = super.createFigure();
    OPMProceduralLink model = (OPMProceduralLink) getModel();
    //decorateConnection(connection, model.getKind());
    model.getKind().decorateConnection(connection); // How cool is this!?
    return connection;
    }

    public class OPMInstrumentLinkFactory implements CreationFactory
    {
    @Override
    public Object getNewObject()
    {
    OPMProceduralLink link = OpmFactory.eINSTANCE.createOPMProceduralLink();
    // link.setKind(OPMProceduralLinkKind.INSTRUMENT);
    link.setKind(OPMProceduralLinkKind2.eInstrument);
    return link;
    }

    }

    ….

    // Step 3
    // In OpmFactoryImpl implement the create/convert methods for OPMProceduralLinkKind2

    /**
    *
    *
    * @generated NOT
    */
    public jon.sandbox.model.opm.helper.OPMProceduralLinkKind2 createOPMProceduralLinkKind2FromString(
    EDataType eDataType, String initialValue)
    {
    OPMProceduralLinkKind2 rtn = OPMProceduralLinkKind2.valueOf(initialValue);
    return rtn;
    }

    /**
    *
    *
    * @generated NOT
    */
    public String convertOPMProceduralLinkKind2ToString(EDataType eDataType, Object instanceValue)
    {
    String str = super.convertToString(eDataType, instanceValue);
    return str;
    }

    For a long time I wanted to learn GEF and now that I have some free time to do, I have found your tutorial incredibly helpful. Thank you very much for creating this tutorial and all of the hard work you put into it.

    • Hi Jon. Nice you enjoyed the tutorial. And thanks for the implementation – it’s good to learn new things every day.

  3. vira vira

    Hey,

    I tried implementing decorated circle as a node. It doesn’t works. In many forums, people say that it works only at the end of a polyline. Is this true ? Is there a better way to do this ?

Leave a Reply to adminCancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from Musings of a Strange Loop

Subscribe now to keep reading and get access to the full archive.

Continue reading