Draw a free line on layout

I want to be able to draw a free line by drag creation like we build walls in Floor Planner App, but want a node (a simple free line) rather than a group.
I know this might be too simple of a question, but I browsed the forum but can’t find a solution.

I started with the below code, but it draws me a rectangle –

this.floorPlan.nodeTemplateMap.add(
      "LineNode",
      this.makeLineNode()
    );

    this.floorPlan.toolManager.mouseMoveTools.insertAt(
      2,
      $(DragCreatingTool, {
        isEnabled: true,
        delay: 0,
        box: $(go.Part, "Spot",
           $(go.Shape, "MinusLine")
        ),
        insertPart: function (bounds) {
          this.archetypeNodeData.category = "LineNode";
          return DragCreatingTool.prototype.insertPart.call(this, bounds);
        },
      })
);

 makeLineNode() {
    var $ = go.GraphObject.make;
    return $(go.Node, 'Spot',
      {
        selectionObjectName: 'SHAPE',
        rotateObjectName: 'SHAPE',
        locationSpot: go.Spot.TopLeft,
        reshapable: true,
        minSize: new go.Size(1, 1),
        selectionAdorned: false,
        copyable: false
      },
      $(go.Shape,
        {
          name: 'SHAPE',
          fill: 'lightgray', stroke: 'black', strokeWidth: 1
        },
        new go.Binding('fill', 'color'),
        new go.Binding('stroke', 'isSelected', function (s, obj) {
          if (obj.part.containingGroup != null) {
            const group = obj.part.containingGroup;
            if (s) { group.data.isSelected = true; }
          }
          return s ? 'dodgerblue' : 'black';
        }).ofObject()
      )
    );
  }

Your node template does not include any Binding on the Shape.geometry or Shape.geonetryString, so it cannot know what “shape” to have. Nor where the node should be – there’s no binding on Node.location or Node.position either.

You might want to look at Freehand Drawing Tool

OK, I do not want a free hand line, only a straight line between two points by dragging.
I used DragCreationTool.ts instead of FreeHandDrawing.ts and changed the above code a little, but it still makes me a rectangle, what am I doing wrong ?

    const fp: Floorplan = this.floorPlan;

    fp.nodeTemplateMap.add("LineNode", this.makeLineNode());

    fp.toolManager.mouseMoveTools.insertAt(
      2,
      $(DragCreatingTool, {
        isEnabled: true, // disabled by the checkbox
        delay: 0, // always canStart(), so PanningTool never gets the chance to run
        archetypeNodeData: {
          stroke: "green",
          strokeWidth: 3,
          category: "LineNode"
        }
      }));
  }

  makeLineNode() {
    var $ = go.GraphObject.make;
    return $(go.Part,
      { locationSpot: go.Spot.Center, isLayoutPositioned: false },
      new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
      {
        selectionAdorned: true, selectionObjectName: "SHAPE",
        // selectionAdornmentTemplate:  // custom selection adornment: a blue rectangle
        //   $(go.Adornment, "Auto",
        //     $(go.Shape, { stroke: "dodgerblue", fill: null }),
        //     $(go.Placeholder, { margin: -1 }))
      },
      { resizable: false, resizeObjectName: "SHAPE" },
      { rotatable: false, rotateObjectName: "SHAPE" },
      { reshapable: false },
      $(go.Shape,
        { name: "SHAPE", fill: null, strokeWidth: 1.5 },
        new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
        new go.Binding("angle").makeTwoWay(),
        new go.Binding("geometryString", "geo").makeTwoWay(),
        new go.Binding("fill"),
        new go.Binding("stroke"),
        new go.Binding("strokeWidth"))
    );
  }

The archetypeNodeData doesn’t have all of the property values that you want copied into your new node data object. In particular you need it to have values for “loc” and “geo”, and maybe others too.

I added the below, still It is making me a rectangle everytime.

const fp: Floorplan = this.floorPlan;

archetypeNodeData: {
          stroke: "green",
          strokeWidth: 3,
          category: "LineNode",
          loc: go.Point.stringify(fp.firstInput.documentPoint),
          geo: new go.Geometry(go.Geometry.Line)
        }

You are Binding Shape.geometryString to data.geo, so the data value has to be a geometry path string, not a Geometry object.

I’m surprised you are not getting errors in the console output, telling you that there is this problem.

That means if I change it to geometry like below, that should be fine.

new go.Binding("geometry", "geo").makeTwoWay(),

But I get this, It looks like I am not even close.

draw_line

You have created an instance of Geometry, but you haven’t initialized it to have the geometry that you want.

I have passed the geometry of a line in the constructor, is it not how we initialize it ?
If not, how do we do it.

But you haven’t specified the end points of the line. That’s what the Geometry.startX, startY, endX, and endY properties are for.

Remember that the coordinates of a Geometry are unit-less and coordinate-less, but when used by a Shape they are really in the local coordinate system of the Shape, which is separate from the coordinate system of the Part that the Shape is in. So you should have the start and end points be in the first quadrant close to the origin (0,0), which is at the top-left corner of the quadrant.

I call this method on the click of a button when I want to turn this tool of drawing a line on, I dont know how will I bring the event object while intiliazing since I havent yet drawn a line, so I initialized to static coordinates (starting and endpoints), although still doesnt work.

 drawLine() {
    const $ = go.GraphObject.make;
    const cxt = this;
    const fp: Floorplan = this.floorPlan;

    fp.nodeTemplateMap.add("LineNode", this.makeLineNode());

    fp.toolManager.mouseMoveTools.insertAt(
      2,
      $(DragCreatingTool, {
        isEnabled: true, // disabled by the checkbox
        delay: 0, // always canStart(), so PanningTool never gets the chance to run
        archetypeNodeData: {
          stroke: "green",
          strokeWidth: 3,
          category: "LineNode",
          loc: new go.Point(0, 0),
          geo: new go.Geometry(go.Geometry.Line)
            .add(new go.PathFigure(0, 0)
              .add(new go.PathSegment(go.PathSegment.Line, 20, 20)))
        }
      }));
  }

A Geometry of type Geometry | GoJS API ignores any PathFigures. The start… and end… properties are necessary and sufficient.

OK, I changed the above to this, and It just draws me a diagonal line template, how do I change it to create a line from the mouse interaction points, just like we build walls in the FloorPlanner.

           loc: new go.Point(0, 0),
           geo: this.makeGeo(),
        }
      }));
  }

  makeGeo() {
    const data = new go.Geometry(go.Geometry.Line);
    
    data.startX = 0;
    data.startX = 0;
    data.endX = 1;
    data.endY = 1;

    return data;
  }

Use the Diagram.firstInput’s and Diagram.lastInput’s documentPoint values and then call Geometry.normalize. Use the negative of the return value to determine the Part.position. Although you may need to subtract off half the Shape.strokeWidth unless you set Shape.isGeometryPositioned to true.

I finally changed to below code, although it doesnt account for the new geo i provide, whats wrong here ?

drawLine() {
    const $ = go.GraphObject.make;
    const cxt = this;
    const fp: Floorplan = this.floorPlan;
    // let obj = this.layoutService.getCurrentObjectPart();
    // const part = obj.part;

    fp.nodeTemplateMap.add("ScaleLineNode", this.makeScaleLineNode());

    fp.toolManager.mouseMoveTools.insertAt(
      2,
      $(DragCreatingTool, {
        isEnabled: true, // disabled by the checkbox
        delay: 0, // always canStart(), so PanningTool never gets the chance to run
        archetypeNodeData: {
          stroke: "green",
          strokeWidth: 3,
          category: "ScaleLineNode",
          loc: new go.Point(0, 0),
          geo: this.makeGeo(),
        },
        insertPart(bounds) {
          const a = DragCreatingTool.prototype.insertPart.call(
            this,
            bounds
          );

          a.data.geo = cxt.makeNewGeo(this.diagram);
          return a;
        }
      }));
  }

  makeGeo() {
    const data = new go.Geometry(go.Geometry.Line);

    data.startX = 0;
    data.startY = 0;
    data.endX = 1;
    data.endY = 1;

    return data;
  }

  makeNewGeo(diag) {
    const data = new go.Geometry(go.Geometry.Line);

    data.startX = diag.firstInput.documentPoint.x;
    data.startY = diag.firstInput.documentPoint.y;
    data.endX = diag.lastInput.documentPoint.x;
    data.endY = diag.lastInput.documentPoint.y;
    data.normalize();
    return data;
  }

  makeScaleLineNode() {
    var $ = go.GraphObject.make;
    return $(go.Part,
      { locationSpot: go.Spot.Center, isLayoutPositioned: false },
      new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
      {
        selectionAdorned: true, selectionObjectName: "SHAPE",
        // selectionAdornmentTemplate:  // custom selection adornment: a blue rectangle
        //   $(go.Adornment, "Auto",
        //     $(go.Shape, { stroke: "dodgerblue", fill: null }),
        //     $(go.Placeholder, { margin: -1 }))
      },
      { resizable: false, resizeObjectName: "SHAPE" },
      { rotatable: false, rotateObjectName: "SHAPE" },
      { reshapable: false },  // GeometryReshapingTool assumes nonexistent Part.reshapeObjectName would be "SHAPE"
      $(go.Shape,
        { name: "SHAPE", fill: null, strokeWidth: 1.5 },
        new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
        new go.Binding("angle").makeTwoWay(),
        new go.Binding("geometry", "geo").makeTwoWay(),
        new go.Binding("fill"),
        new go.Binding("stroke"),
        new go.Binding("strokeWidth"))
    );
  }

You didn’t account for everything I said.

Something like below ?, although gives me error in the console at part.position = pos; and result is the same.

    insertPart(bounds) {
          const part = DragCreatingTool.prototype.insertPart.call(this, bounds);
          part.data.geo = cxt.makeNewGeo(this.diagram);
          const shape: go.Shape = part.findObject('SHAPE') as go.Shape;
          const pos = part.data.geo.copy();
          pos.x = - pos.startX - shape.strokeWidth / 2;
          pos.y = - pos.startY - shape.strokeWidth / 2;
          part.position = pos;

          return part;
        }

@walter can you point the mistake in the above ?

Does the error message give you a clue what is wrong?
What is the type of pos? Does it have x and y properties? Can it be used as the value of part.position?

Yes, the types seem to be different. So I changed it to below, the error goes away, but the result is still not desirable.

const pos = part.data.geo.copy();
part.position.x = - pos.startX - shape.strokeWidth / 2;
part.position.y = - pos.startY - shape.strokeWidth / 2;
return part;

1