/* * Copyright 1998-2025 by Northwoods Software Corporation. All Rights Reserved. */ /* * This is an extension and not part of the main GoJS library. * The source code for this is at extensionsJSM/GuidedDraggingTool.ts. * Note that the API for this class may change with any version, even point releases. * If you intend to use an extension in production, you should copy the code to your own source directory. * Extensions can be found in the GoJS kit under the extensions or extensionsJSM folders. * See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information. */ /** * This replacement for the DragSelectingTool operates similarly but instead of drawing a rectangular area * where Parts may be selected, follows the mouse to draw a polygon. * * Instead of the DragSelectingTool.box this tool has the LassoSelectingTool.shape * whose Shape.fill and Shape.stroke may be styled. * * Install by replacing the CommandHandler.dragSelectingTool. For example: * ```js * new go.Diagram("myDiagramDiv", { * dragSelectingTool: new LassoSelectingTool(), * . . . * }) * ``` * * @category Tool Extension */ class LassoSelectingTool extends go.Tool { constructor(init) { super(); // this is the Shape that is shown during a drawing operation this._shape = new go.Shape({ name: 'SHAPE', fill: "rgba(256,0,256,0.1)", stroke: "magenta", strokeWidth: 1.5 }); // the Shape has to be inside a temporary Part that is used during the drawing operation new go.Part({ layerName: 'Tool', selectable: false }).add(this._shape); this._delay = 175; this.name = 'LassoSelecting'; if (init) Object.assign(this, init); } /** * Gets or sets the Shape that is used to hold the line as it is being drawn. * * The default value is a simple Shape drawing a translucent polygon. * The shape may not be null. */ get shape() { return this._shape; } set shape(val) { if (this._shape !== val && val !== null) { val.name = 'SHAPE'; const panel = this._shape.panel; if (panel !== null) { panel.remove(this._shape); this._shape = val; panel.add(this._shape); } } } /** * Gets or sets the time in milliseconds for which the mouse must be stationary * before this tool can be started. * The default value is 175 milliseconds. * Setting this property does not raise any events. */ get delay() { return this._delay; } set delay(value) { this._delay = value; } /** */ canStart() { if (!this.isEnabled) return false; const diagram = this.diagram; if (!diagram.allowSelect) return false; const e = diagram.lastInput; // require left button & that it has moved far enough away from the mouse down point, so it isn't a click if (!e.left) return false; // don't include the following checks when this tool is running modally if (diagram.currentTool !== this) { if (!this.isBeyondDragSize()) return false; // must wait for "delay" milliseconds before that tool can run if (e.timestamp - diagram.firstInput.timestamp < this.delay) return false; // don't start if we're over a selectable part if (diagram.findPartAt(e.documentPoint, true) !== null) return false; } return true; } /** * Capture the mouse and use a "crosshair" cursor. */ doActivate() { super.doActivate(); this.diagram.isMouseCaptured = true; this.diagram.currentCursor = 'crosshair'; } /** * Release the mouse and reset the cursor. */ doDeactivate() { super.doDeactivate(); if (this.shape !== null && this.shape.part !== null) { this.diagram.remove(this.shape.part); } this.diagram.currentCursor = ''; this.diagram.isMouseCaptured = false; } /** * This adds a Point to the {@link shape}'s geometry. * * If the Shape is not yet in the Diagram, its geometry is initialized and * its parent Part is added to the Diagram. * * If the point is less than half a pixel away from the previous point, it is ignored. */ addPoint(p) { const shape = this.shape; if (shape === null) return; const part = shape.part; if (part === null) return; // for the temporary Shape, normalize the geometry to be in the viewport const viewpt = this.diagram.viewportBounds.position; const q = new go.Point(p.x - viewpt.x, p.y - viewpt.y); if (part.diagram === null) { const f = new go.PathFigure(q.x, q.y, true); // possibly filled, depending on Shape.fill const g = new go.Geometry().add(f); // the Shape.geometry consists of a single PathFigure shape.geometry = g; // position the Shape's Part, accounting for the strokeWidth part.moveTo(viewpt.x - shape.strokeWidth / 2, viewpt.y - shape.strokeWidth / 2); this.diagram.add(part); } // only add a point if it isn't too close to the last one const geo = shape.geometry; if (geo !== null) { const fig = geo.figures.first(); if (fig !== null) { const segs = fig.segments; const idx = segs.count - 1; if (idx >= 0) { const last = segs.elt(idx); if (Math.abs(q.x - last.endX) < 0.5 && Math.abs(q.y - last.endY) < 0.5) return; } // must copy whole Geometry in order to add a PathSegment const geo2 = geo.copy(); const fig2 = geo2.figures.first(); if (fig2 !== null) { fig2.add(new go.PathSegment(go.SegmentType.Line, q.x, q.y)); shape.geometry = geo2; } } } } /** * Start drawing the line by starting to accumulate points in the {@link shape}'s geometry. */ doMouseDown() { if (!this.isActive) { this.doActivate(); // the first point this.addPoint(this.diagram.lastInput.documentPoint); } } /** * Keep accumulating points in the {@link shape}'s geometry. */ doMouseMove() { if (this.isActive) { this.addPoint(this.diagram.lastInput.documentPoint); } } /** * Finish drawing the line by selecting Parts whose {@link Part.selectionObject} is within the drawn polygon. */ doMouseUp() { var _a; const diagram = this.diagram; if (this.isActive) { try { // the last point this.addPoint(diagram.lastInput.documentPoint); // normalize geometry and node position (_a = this.shape.part) === null || _a === void 0 ? void 0 : _a.ensureBounds(); diagram.currentCursor = 'wait'; diagram.raiseDiagramEvent('ChangingSelection', diagram.selection); this.selectInShape(this.shape); diagram.raiseDiagramEvent('ChangedSelection', diagram.selection); } finally { diagram.currentCursor = ''; } } this.stopTool(); } /** * Modify the diagram's selection according to whether a Part is within the given Shape. * This is called from {@link doMouseUp}. * * This method may be overridden. * @virtual * @param shp normally this will be {@link shape} */ selectInShape(shp) { var _a; const diagram = this.diagram; if (!diagram || ((_a = shp.part) === null || _a === void 0 ? void 0 : _a.diagram) !== diagram) return; const e = diagram.lastInput; const temp = new go.Rect(); const parts = diagram.findPartsIn(shp.part.actualBounds, false /*this.isPartialInclusion*/); if (e.meta || e.control) { // toggle or deselect if (e.shift) { // deselect only const it = parts.iterator; while (it.next()) { const p = it.value; p.selectionObject.getDocumentBounds(temp); if (p.isSelected && shp.polygonContainsRect(temp)) p.isSelected = false; } } else { // toggle selectedness of parts const it = parts.iterator; while (it.next()) { const tp = it.value; tp.selectionObject.getDocumentBounds(temp); if (shp.polygonContainsRect(temp)) tp.isSelected = !tp.isSelected; } } } else if (e.shift) { // extend selection only const it = parts.iterator; while (it.next()) { const ep = it.value; ep.selectionObject.getDocumentBounds(temp); if (!ep.isSelected && shp.polygonContainsRect(temp)) ep.isSelected = true; } } else { // select parts, and unselect all other previously selected parts // this tries to avoid deselecting and then reselecting any Part const tounselect = new go.List(); const sit = diagram.selection.iterator; while (sit.next()) { const sp = sit.value; if (!parts.has(sp)) tounselect.add(sp); } const uit = tounselect.iterator; while (uit.next()) { const up = uit.value; up.isSelected = false; } const it = parts.iterator; while (it.next()) { const ps = it.value; ps.selectionObject.getDocumentBounds(temp); if (!ps.isSelected && shp.polygonContainsRect(temp)) ps.isSelected = true; } } } }