Last updated

Scene Editing Tools

The Basic Tools tutorial covered user interaction and scene control tools. This tutorial shows how editing/selection tools allows for quick addition of functionality to canvases.

# Scene Editor

The following canvas demonstrates the editing/selection tools such as shape selection, Rubber Band selection, polygon selection and tool for creating, moving and resizing shapes.

import { isNode } from "@int/geotoolkit/scene/Node.ts";
import { Plot } from "@int/geotoolkit/plot/Plot.ts";
import { Panning } from "@int/geotoolkit/controls/tools/Panning.ts";
import { Transformation } from "@int/geotoolkit/util/Transformation.ts";
import { Events as SelectionEvents, Selection } from "@int/geotoolkit/controls/tools/Selection.ts";
import { Shape } from "@int/geotoolkit/scene/shapes/Shape.ts";
import { Events as AbstractToolEvents } from "@int/geotoolkit/controls/tools/AbstractTool.ts";
import { PolygonSelection } from "@int/geotoolkit/controls/tools/PolygonSelection.ts";
import { Layer } from "@int/geotoolkit/scene/Layer.ts";
import { CompositeTool } from "@int/geotoolkit/controls/tools/CompositeTool.ts";
import { RubberBandShapeMode } from "@int/geotoolkit/controls/tools/RubberBandShapeMode.ts";
import { CircularMode } from "@int/geotoolkit/controls/tools/CircularMode.ts";
import { EditMode } from "@int/geotoolkit/controls/tools/EditMode.ts";
import { SelectionMode } from "@int/geotoolkit/controls/tools/SelectionMode.ts";
import { RubberBandMode } from "@int/geotoolkit/controls/tools/RubberBandMode.ts";
import { initializeDoubleClickHandler, initializeRubberBandZoom } from "/src/code/Carnac/Tools/SceneEditing/ts/tools.ts";
import { DATA_LAYER_ID } from "/src/code/Carnac/Tools/SceneEditing/ts/model.ts";
import { EditEvents } from "@int/geotoolkit/controls/tools/EditEvents.ts";
import { Helpers } from "/src/code/Carnac/Tools/SceneEditing/ts/Helpers.ts";
import { Rect } from "@int/geotoolkit/util/Rect.ts";
import { CustomShape } from "/src/code/Carnac/Tools/SceneEditing/ts/shapes/CustomShape.ts";
import { Rectangle } from "@int/geotoolkit/scene/shapes/Rectangle.ts";
import { KnownColors } from "@int/geotoolkit/util/ColorUtil.ts";
import { Spline } from "@int/geotoolkit/scene/shapes/Spline.ts";
import { Polyline } from "@int/geotoolkit/scene/shapes/Polyline.ts";
import { Text } from "@int/geotoolkit/scene/shapes/Text.ts";
import { BaseLineStyle } from "@int/geotoolkit/attributes/TextStyle.ts";
import { Line } from "@int/geotoolkit/scene/shapes/Line.ts";
import { Point } from "@int/geotoolkit/util/Point.ts";
import { SymbolShape } from "@int/geotoolkit/scene/shapes/SymbolShape.ts";
import { CirclePainter } from "@int/geotoolkit/scene/shapes/painters/CirclePainter.ts";
export const SelectionToolsMode = {
  Panning: "Panning",
  RubberBandZoom: "RubberBandZoom",
  PointSelection: "PointSelection",
  RubberBandSelection: "RubberBandSelection",
  PolygonSelection: "PolygonSelection",
  CircularSelection: "CircularSelection"
};
class SceneEditor {
  constructor(canvas, update) {
    this._annotatedWidget = Helpers.createAnnotatedWidget(
      new Rect(0, 0, 400, 400),
      [
        new CustomShape({
          "bounds": new Rect(300, 200, 400, 300),
          "linestyle": {
            "color": "black",
            "pixelsnapmode": true
          },
          "fillstyle": "rgb(144, 238, 144)"
        }),
        new Rectangle({
          "left": 60,
          "top": 60,
          "width": 150,
          "height": 150,
          "fillstyle": KnownColors.Blue
        }),
        new Spline({
          "x": [100, 50, 150, 200, 300, 350],
          "y": [350, 300, 200, 150, 50, 100],
          "linestyle": {
            "color": KnownColors.Orange,
            "width": 2
          }
        }),
        new Polyline({
          "x": [200, 250, 300],
          "y": [350, 300, 350],
          "linestyle": KnownColors.Red
        }),
        new Text({
          "text": "select me",
          "ax": 220,
          "ay": 280,
          "textstyle": {
            "baseline": BaseLineStyle.Alphabetic
          }
        }).setUserSize(80, 20),
        new Line({
          "from": new Point(230, 230),
          "to": new Point(330, 130),
          "linestyle": KnownColors.Green
        }),
        new SymbolShape({
          "ax": 150,
          "ay": 330,
          "width": 50,
          "height": 50,
          "linestyle": "black",
          "fillstyle": "white",
          "painter": CirclePainter
        })
      ]
    );
    this._plot = new Plot({
      "canvaselement": canvas,
      "root": this._annotatedWidget
    });
    this.initializeTools();
    this._shapeManipulator.getHistory().on(EditEvents.CommandApplied, update);
  }
  initializeTools() {
    this._panning = this._annotatedWidget.getToolByType(Panning);
    const manipulatorLayer = this._annotatedWidget.getManipulatorLayer();
    const selectionLayer = new Layer();
    this._annotatedWidget.getModel().addChild([
      selectionLayer
    ]);
    this._manipulators = [
      this._rubberBandZoom = initializeRubberBandZoom(selectionLayer),
      this._rubberBandSelection = this.initializeRubberBandSelection(selectionLayer),
      this._polygonSelection = this.initializePolygonSelection(selectionLayer)
    ];
    const dataLayer = this._annotatedWidget.getModel().getChildren((child) => child.getId() === DATA_LAYER_ID).next();
    this._annotatedWidget.getTool().insert(0, [
      initializeDoubleClickHandler(this._plot, selectionLayer),
      new CompositeTool(this._annotatedWidget.getModel(), "shapeManipulator").add([
        this._shapeManipulator = Helpers.createShapeManipulator(dataLayer, manipulatorLayer)
      ]),
      new CompositeTool(this._annotatedWidget.getModel(), "selectionTools").add(this._manipulators)
    ]);
  }
  onSelectionEnd(selectedItems) {
    const selection = selectedItems.filter(isNode);
    if (selection == null || selection.length === 0) {
      return this;
    }
    const shapes = [];
    for (let i = 0; i < selection.length; i++) {
      const parent = selection[i].getParent();
      if (parent != null && parent.getId() === DATA_LAYER_ID) {
        shapes.push(selection[i]);
      }
    }
    const node = shapes.length > 0 ? shapes.length > 1 ? shapes : shapes[0] : null;
    this._shapeManipulator.editNode(node);
    return this;
  }
  initializeRubberBandSelection(manipulatorLayer) {
    return new Selection({
      "name": "RubberBandSelection",
      "layer": manipulatorLayer
    }).setEnabled(false).setAutoDisabled(true).setSelectionMode(SelectionMode.RubberBand).setRubberBandMode(RubberBandMode.Inside).on(SelectionEvents.onSelectionEnd, (evt, sender, event) => {
      this.onSelectionEnd(event.getSelection());
    }).on(AbstractToolEvents.onEnabledStateChanged, (evt, sender) => {
      if (sender.isEnabled() === false) {
        this._shapeManipulator.setEnabled(true);
        this.toggleTool(SelectionToolsMode.Panning);
      }
    });
  }
  initializePolygonSelection(manipulatorLayer) {
    return new PolygonSelection({
      "name": "PolygonSelection",
      "layer": manipulatorLayer
    }).setEnabled(false).setAutoDisabled(true).setNodeFilter((nodes) => nodes.filter((visual) => visual instanceof Shape)).on(SelectionEvents.onSelectionEnd, (eventType, sender, event) => {
      this.onSelectionEnd(event.getSelection());
      const editor = this._shapeManipulator.getEditor();
      if (editor != null) {
        editor.setEditMode(EditMode.EditNode);
      }
    }).on(AbstractToolEvents.onEnabledStateChanged, (eventType, sender) => {
      if (sender.isEnabled() === false) {
        this._shapeManipulator.setEnabled(true);
        this.toggleTool(SelectionToolsMode.Panning);
      }
    });
  }
  execute(command) {
    this._shapeManipulator.getHistory().push(command);
  }
  undo() {
    this._shapeManipulator.undo();
  }
  getUIOptions() {
    return {
      canUndo: this._shapeManipulator.canUndo(),
      canRedo: this._shapeManipulator.canRedo()
    };
  }
  redo() {
    this._shapeManipulator.redo();
  }
  toggleGhostMode(ghostMode) {
    let mode = this._shapeManipulator.getEditMode();
    if (ghostMode === true) {
      const editor = this._shapeManipulator.getEditor();
      if (editor != null) {
        editor.setEditMode(EditMode.EditNode);
      }
      mode |= EditMode.Ghost;
    } else if (ghostMode === false) {
      mode &= ~EditMode.Ghost;
    } else {
      mode ^= EditMode.Ghost;
    }
    this._shapeManipulator.setEditMode(mode);
  }
  insertNode(nodeId) {
    this._shapeManipulator.insertNode(nodeId);
  }
  scaleModel(scale) {
    if (scale == null) {
      this._annotatedWidget.setModelTransformation(new Transformation());
    } else {
      this._annotatedWidget.scaleModel(scale, scale);
    }
  }
  getToolByName(toolName) {
    return this._plot.getTool().getToolByName(toolName);
  }
  toggleTool(toolType) {
    this._manipulators.forEach((tool) => {
      tool.setEnabled(false);
    });
    let activeTool = SelectionToolsMode.Panning;
    switch (toolType) {
      case SelectionToolsMode.RubberBandZoom:
        if (this._activeTool !== toolType) {
          activeTool = toolType;
          this._rubberBandZoom.setEnabled(true);
        }
        break;
      case SelectionToolsMode.PointSelection:
        if (this._activeTool !== toolType) {
          activeTool = toolType;
          this._rubberBandSelection.setEnabled(false);
          this._polygonSelection.setEnabled(false);
          this._shapeManipulator.setEnabled(true);
        }
        break;
      case SelectionToolsMode.RubberBandSelection:
        if (this._activeTool !== toolType) {
          activeTool = toolType;
          this._rubberBandSelection.setEnabled(true);
          this._rubberBandSelection.setRubberBandShapeMode(RubberBandShapeMode.Rectangular);
          this._shapeManipulator.setEnabled(false);
        } else {
          this._shapeManipulator.setEnabled(true);
        }
        break;
      case SelectionToolsMode.PolygonSelection:
        if (this._activeTool !== toolType) {
          activeTool = toolType;
          this._polygonSelection.setEnabled(true);
          this._shapeManipulator.setEnabled(false);
        } else {
          this._shapeManipulator.setEnabled(true);
        }
        break;
      case SelectionToolsMode.CircularSelection:
        if (this._activeTool !== toolType) {
          activeTool = toolType;
          this._rubberBandSelection.setEnabled(true);
          this._rubberBandSelection.setRubberBandShapeMode(RubberBandShapeMode.Circular);
          this._rubberBandSelection.setCircularMode(CircularMode.Corner);
          this._rubberBandSelection.setLineStyle({
            color: "red",
            width: 2
          });
          this._rubberBandSelection.setTextStyle("red");
          this._rubberBandSelection.setMeasureCallback((size) => size > 40 ? Math.round(size) + " px" : null);
          this._shapeManipulator.setEnabled(false);
        } else {
          this._shapeManipulator.setEnabled(true);
        }
        break;
      default:
        break;
    }
    if (activeTool === SelectionToolsMode.Panning) {
      this._panning.setEnabled(true);
    }
    this._activeTool = activeTool;
    return this;
  }
  changeShape(shape) {
    if (shape === "circular") {
      this._rubberBandSelection.setRubberBandShapeMode(RubberBandShapeMode.Circular);
    } else if (shape === "rectangular") {
      this._rubberBandSelection.setRubberBandShapeMode(RubberBandShapeMode.Rectangular);
    }
  }
  getActiveTool() {
    return this._activeTool;
  }
  getPlot() {
    return this._plot;
  }
}
function createScene(canvas, update) {
  const app = new SceneEditor(canvas, update);
  return {
    "plot": app.getPlot(),
    "editor": app
  };
}
export { createScene };

createScene(document.querySelector('[ref="plot"]'), this.updateUI.bind(this));

# Managing Commands

The example below demonstrates how to manage the editing process via geotoolkit/controls/tools/editors/commands/AbstractCommand.reject() method.

Each editing action (resize, translate, drag point, etc.) raises the appropriate event set that can be used to manage (reject) this action if necessary. The event flow for every editing action is the following (in order of appearance):

  • geotoolkit/control/tools/EditEvents.BeforeCommandApplied - Before any command was applied (duplicates any other command event). The applied command can be rejected at this point.
  • geotoolkit/control/tools/EditEvents.CommandApplying - After any command was applied (duplicates any other command event) but before geotoolkit/control/tools/EditEvents.CommandApplied event was raised. The applied command still can be rejected at this point.
  • geotoolkit/control/tools/EditEvents.CommandApplied - After any command was applied and pushed to the commands history (duplicates any other command event). The applied command can't be rejected at this point.

Let's add two rectangle shapes and implement the following common behavior for them:

  1. rectangles cannot intersect each other.
  2. rectangles cannot be resized horizontally.

Note that for the option 1 we use the geotoolkit/control/tools/EditEvents.CommandApplying event as it is raised after the command was applied; for the option 2 we use the geotoolkit/control/tools/EditEvents.BeforeCommandApplied event as it is raised before the command was applied.

import { Plot } from "@int/geotoolkit/plot/Plot.ts";
import { Helpers } from "/src/code/Carnac/Tools/SceneEditing/ts/Helpers.ts";
import { Rect } from "@int/geotoolkit/util/Rect.ts";
import { Rectangle } from "@int/geotoolkit/scene/shapes/Rectangle.ts";
import { KnownColors } from "@int/geotoolkit/util/ColorUtil.ts";
import { DATA_LAYER_ID } from "/src/code/Carnac/Tools/SceneEditing/ts/model.ts";
import { EditEvents } from "@int/geotoolkit/controls/tools/EditEvents.ts";
import { Resize } from "@int/geotoolkit/controls/tools/editors/commands/Resize.ts";
import { Shape } from "@int/geotoolkit/scene/shapes/Shape.ts";
const hasBounds = (node) => node instanceof Shape && typeof node.getBounds === "function";
function createScene(canvas) {
  const widget = Helpers.createAnnotatedWidget(
    new Rect(0, 0, 400, 400),
    [
      new Rectangle({
        "left": 150,
        "top": 60,
        "width": 100,
        "height": 100,
        "fillstyle": KnownColors.Blue
      }),
      new Rectangle({
        "left": 150,
        "top": 250,
        "width": 100,
        "height": 100,
        "fillstyle": KnownColors.Green
      })
    ]
  );
  initShapeManipulator(widget);
  return new Plot({
    "canvaselement": canvas,
    "root": widget
  });
}
function initShapeManipulator(widget) {
  const rejectHorizontalResize = (eventType, sender, args) => {
    const command = args.getCommand();
    if (command instanceof Resize) {
      const isHorizontalResize = command.getResizeDirections().some((direction) => Math.abs(direction.vx) !== 0);
      if (isHorizontalResize) {
        command.reject();
      }
    }
  };
  const rejectShapesCollision = (eventType, sender, args) => {
    const command = args.getCommand();
    const appliedEventName = command.getEventName();
    if (appliedEventName !== EditEvents.Translated && appliedEventName !== EditEvents.Resized) {
      return;
    }
    const currShape = args.getNode();
    if (!hasBounds(currShape) || currShape.getBounds() == null) {
      return;
    }
    const sceneTr = currShape.getSceneTransform();
    const currNodeDeviceBounds = sceneTr.transformRect(currShape.getBounds());
    const checkCollision = (node) => {
      if (node === currShape || !hasBounds(node) || node.getBounds() == null) {
        return false;
      }
      const nodeDeviceBounds = node.getSceneTransform().transformRect(node.getBounds());
      return currNodeDeviceBounds.intersects(nodeDeviceBounds);
    };
    const hasCollision = sender.getDataLayer().getChildren(checkCollision).hasNext();
    if (hasCollision) {
      command.reject();
    }
  };
  const dataLayer = widget.getModel().getChildren((child) => child.getId() === DATA_LAYER_ID).next();
  const manipulatorLayer = widget.getManipulatorLayer();
  const shapeManipulator = Helpers.createShapeManipulator(dataLayer, manipulatorLayer);
  shapeManipulator.on(EditEvents.BeforeCommandApplied, rejectHorizontalResize).on(EditEvents.CommandApplying, rejectShapesCollision);
  widget.getTool().insert(0, shapeManipulator);
}
export { createScene };

createScene(document.querySelector('[ref="plot"]'));

# Edit Handles Customization

This section shows how to customize edit handles styles. The sample property object for edit handles customization is shown below:

Geotoolkit provides 2 ways to set custom properties on the edit handles that are currently captured ('active') and the ones that are hovered over with the cursor ('hover'). The approach with the property object was shown above. Below the css style for the manipulator layer approach is shown:

Note that these 2 approaches can be used both separately and simultaneously but the property object always overrides css style in case of styles collision.

# Rubber Band Selection

This section shows how to use the SelectionMode in Selection tool. It also sets the RubberBandMode.Inside so only the objects completely inside the selection rectangle will be selected.

# Polygon Selection

# Shape Editor