/** * Copyright (c) 2011-2018 by Andrew Mustun. All rights reserved. * * This file is part of the QCAD project. * * QCAD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * QCAD is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with QCAD. */ include("scripts/Modify/Modify.js"); include("scripts/ShapeAlgorithms.js"); function Explode(guiAction) { Modify.call(this, guiAction); } Explode.prototype = new Modify(); Explode.getPreferencesCategory = function() { return [qsTr("Modify"), qsTr("Explode")]; }; Explode.initPreferences = function(pageWidget, calledByPrefDialog, document) { var widgets = getWidgets(pageWidget); if (!RTextBasedData.hasProxy()) { // remove MultilineTextToSimpleText option: widgets["MultilineTextToSimpleText"].visible = false; } if (!RSpline.hasProxy()) { widgets["SplineTolerance"].visible = false; widgets["SplineTolerance_Label"].visible = false; widgets["SplinesToLineSegments"].visible = false; widgets["Indent"].destroy(); pageWidget.findChild("GridLayout").addWidget(widgets["SplineSegments_Label"], 2, 0, 1, 2); } else { widgets["SplinesToLineSegments"].toggled.connect(function(state) { widgets["SplineSegments"].enabled = state; widgets["SplineSegments_Label"].enabled = state; }); widgets["SplineSegments"].enabled = widgets["SplinesToLineSegments"].checked; widgets["SplineSegments_Label"].enabled = widgets["SplinesToLineSegments"].checked; } }; Explode.prototype.beginEvent = function() { Modify.prototype.beginEvent.call(this); var di = this.getDocumentInterface(); Explode.explodeSelection(di, this.getToolTitle()); this.terminate(); }; /** * Explodes all selected entities. * \param di Document interface * \param toolTitle Tool title used for undo/redo information. */ Explode.explodeSelection = function(di, toolTitle) { var document = di.getDocument(); var storage = document.getStorage(); var ids = document.querySelectedEntities(); var i, k, e, n; var polyline, shapes; var options = {}; options["splineTolerance"] = RSettings.getDoubleValue("Explode/SplineTolerance", 0.01); options["splineSegments"] = RSettings.getIntValue("Explode/SplineSegments", 64); options["ellipseSegments"] = RSettings.getIntValue("Explode/EllipseSegments", 32); options["splinesToLineSegments"] = RSettings.getBoolValue("Explode/SplinesToLineSegments", false); options["textToPolylines"] = RSettings.getBoolValue("Explode/TextToPolylines", true); options["multilineTextToSimpleText"] = RSettings.getBoolValue("Explode/MultilineTextToSimpleText", true); options["circlesToPolylines"] = RSettings.getBoolValue("Explode/CirclesToPolylines", true); // ExplodeDialog.ui seems to be out of use !? // CVH added 'TextSplinesToLineArcSpline' preference to PreferencesPage.ui to make it optional: // See: https://qcad.org/rsforum/viewtopic.php?t=6458 // Added to options object options["textSplinesToLineArcSpline"] = RSettings.getBoolValue("Explode/TextSplinesToLineArcSpline", true); // Default =True var op = new RAddObjectsOperation(); if (!isNull(toolTitle)) { op.setText(toolTitle); } // map old block reference IDs to block reference entities: var blockReferenceMap = {}; // list of attribute entities: var attributeEntities = []; for (i=0; i0) { op = new RAddObjectsOperation(); //op.setTransactionGroup(doc.getTransactionGroup()); if (!isNull(toolTitle)) { op.setText(toolTitle); } // fix attribute links to block references: for (i=0; i textSplinesToLineArcSpline = RSettings.getBoolValue("Explode/TextSplinesToLineArcSpline", true); // Default =True } // End NOT existing // explode ellipse into polyline with arc segments: if (isEllipseEntity(entity)) { if (REllipse.hasProxy()) { var ellipse = entity.getData().castToShape(); var polyline = ellipse.approximateWithArcs(ellipseSegments); if (!polyline.isEmpty()) { ret.push(polyline); } } else { return undefined; } } // explode circle into polyline with two arc segments: else if (isCircleEntity(entity)) { if (circlesToPolylines) { var circle = entity.getData().castToShape(); polyline = new RPolyline(); polyline.appendShape(new RArc(circle.getCenter(), circle.getRadius(), 0.0, Math.PI, false)); polyline.appendShape(new RArc(circle.getCenter(), circle.getRadius(), Math.PI, 2*Math.PI, false)); polyline.autoClose(); ret.push(polyline); } else { return undefined; } } // explode polyline into line and arc segments: else if (isPolylineEntity(entity)) { polyline = entity.getData().castToShape(); // explode polyline with segment widths: if (RPolyline.hasProxy() && polyline.hasWidths()) { // Retrieve list of polyline pairs (left / right): var pls = polyline.getLeftRightOutline(); // For outlines of polylines with Widths the newer getLeftRightOutline() method // is a far better representation for bulging and changing widths // In 3.24 this was: var pls = polyline.getOutline(); // 12/01/2020: Not yet documented -> // https://www.qcad.org/doc/qcad/latest/developer/class_r_polyline.html // Outlines method by CVH, The whole idea is now: // ---------------------------------------------- // - To include 'Connectors' between outlines at both side when continuous // - To include both loop 'Terminators' when not continuous // Without casting any null or near null length lines/outline e.g. FS#2168 // https://qcad.org/bugtracker/index.php?do=details&task_id=2168 // Avoiding empty outlines e.g. FS#2155 // https://www.ribbonsoft.com/bugtracker/index.php?do=details&task_id=2155 // WIN debug: Push handle to Command History for debugging: EAction.handleUserInfo("Entity handle: 0x" + entity.getHandle().toString(16)); // Skip exploding a polyline without outline sets: var outlineSets = pls.length; if (outlineSets === 0) { // Without outlines > // Copy of original warning: Default, Not translated qWarning("Empty result from RPolyline::getLeftRightOutline"); // WIN: Warn on Command History: (Not translated as Default) EAction.handleUserWarning("Empty result from RPolyline::getLeftRightOutline"); // Return an empty result return ret; } // End without // -> Continue with at least 1 outline set // Retrieve first valid start positions (left / right): // # Issue Solved # First outlines may be empty with NO start positions ! // Solution: Calculate positions directly from polyline and local Width var startPointPl = polyline.getStartPoint(); var orthoRv = new RVector(); orthoRv.setPolar(polyline.getStartWidthAt(0) / 2, polyline.getDirection1() + Math.PI / 2); var startLeftRv = startPointPl.operator_add(orthoRv); var startRightRv = startPointPl.operator_subtract(orthoRv); // Skip explosion without valid start positions: if (isNull(startLeftRv) || isNull(startRightRv)) { // With any startpoint missing > qWarning("No offsets from Polyline start"); // Not translated as Default // WIN: Warn on Command History: (Not translated as Default) EAction.handleUserWarning("No valid offsets from Polyline startpoint"); // Return an empty result: return ret; } // End any missing // -> Continue with both start positions known // Define polyline is fully continuous or NOT: var continuousPoly = polyline.isGeometricallyClosed(); // Default =RS.PointTolerance // Diversify =GeoOpen or =GeoClosed + continuous: if (continuousPoly) { // When GeoClosed > var ori1 = polyline.getDirection1(); // Presumed normalized var ori2 = RMath.getNormalizedAngle(polyline.getDirection2() + Math.PI); // # Issue Fixed # getNormalizedAngle of a sum may rarely return 2PI !? // Fixed with setting result to zero. Default =RS.AngleTolerance if (RMath.fuzzyAngleCompare(ori2, 2 * Math.PI)) { // When angle is near 2PI > ori2 = 0; } // End Fix // Clear flag when directions don't match. Default =RS.AngleTolerance continuousPoly = RMath.fuzzyAngleCompare(ori1, ori2); } // End when GeoClosed // Initial values: var lastLeftRv = startLeftRv; var lastRightRv = startRightRv; var continuous = continuousPoly; // Define polyline vertices count: var vertices = polyline.countVertices(); // Define left/right collectors as empty polyline: var leftNwPoly = new RPolyline(); var rightNwPoly = new RPolyline(); // Process all outline sets: for (k=0; k // Original warning: Default, Not translated qWarning("invalid result from RPolyline::getLeftRightOutline"); continue; // Skip to next set } // End with no set // -> Continue with a set // Retrieve outline set: var leftPl = pls[k][0]; var rightPl = pls[k][1]; // Validate outlines with a distinct length: var leftPlValid = !leftPl.isEmpty() && leftPl.getLength() > RS.PointTolerance; var rightPlValid = !rightPl.isEmpty() && rightPl.getLength() > RS.PointTolerance; // Include set starting Connectors: if (continuous) { // When continuous: 2 Connectors > // Left side: if (leftPlValid) { // With a valid left outline > var connector = new RLine(lastLeftRv, leftPl.getStartPoint()); if (connector.getLength() > RS.PointTolerance) { // With a distinct length > leftNwPoly.appendShape(connector, false); // NOTprepend } // End with a length } // End when valid // Right side: if (rightPlValid) { // With a valid right outline > var connector = new RLine(lastRightRv, rightPl.getStartPoint()); if (connector.getLength() > RS.PointTolerance) { // With a distinct length > rightNwPoly.appendShape(connector, false); // NOTprepend } // End with a length } // End when valid } // End when continuous // Replace last positions by any valid outline start position: if (leftPlValid) { // with a distinct left outline lastLeftRv = leftPl.getStartPoint(); } // End with a left outline if (rightPlValid) { // with a distinct right outline lastRightRv = rightPl.getStartPoint(); } // End with a right outline // # Issue Solved # Not all outline sets begin at the polyline startpoint !? // Initial values may be off. Outlines should start & end with the polyline ! // The returned outlines are considered as a flaw !!! // This occurs at stable Width, with non-bulging outlines as chained line-segments // Solution: Verifying local tangency if (k === 0) { // With the first set > if (leftPlValid && rightPlValid) { // When both outlines are valid > var midVector = RVector.getAverage(leftPl.getStartPoint(), rightPl.getStartPoint()); // Compare the middle with the startpoint. Default =RS.PointTolerance if (!midVector.equalsFuzzy(startPointPl)) { // When NOT coincide with the polyline startpoint > var noTangency = polyline.verifyTangency(RS.AngleTolerance, Math.PI*2 - RS.AngleTolerance); var listed = RVector.containsFuzzy(noTangency, midVector, 0.00001); // # Issue # Default fuzzy compare vectors tolerance 0.00001 ! if (listed) { // When listed > continuous = false; // WIN debug: Report in Command History: (Not translated as Default) EAction.handleUserMessage("Tangency listed for Set " + k + " : " + midVector + " = " + listed); } // End when listed else { // When NOT listed > continuous = true; startLeftRv = leftPl.getStartPoint(); startRightRv = rightPl.getStartPoint(); } // End when NOT listed } // End when NOT coincide } // End when both valid } // End with first set // -> Continue with fixed continuous flag // Include a set starting Terminator: if (!continuous && (leftPlValid || rightPlValid)) { // When NOT continuous and one outline is valid > var terminator = new RLine(lastRightRv, lastLeftRv); if (terminator.getLength() > RS.PointTolerance) { // With a distinct length > leftNwPoly.appendShape(terminator, false); // NOTprepend // WIN debug: Report in Command History: (Not translated as Default) EAction.handleUserMessage(("Included set %1 starting Terminator").arg(k)); } // End with a length } // End NOT continuous and valid // Include both outlines when not empty and having a distinct length: // Left side: if (leftPlValid) { // with a distinct left outline leftNwPoly.appendShape(leftPl, false); // NOTprepend // Store the valid left end position: lastLeftRv = leftPl.getEndPoint(); } // End with a left outline // Right side: if (rightPlValid) { // with a distinct right outline rightNwPoly.appendShape(rightPl, false); // NOTprepend // Store the valid right end position: lastRightRv = rightPl.getEndPoint(); } // End with a right outline // Check if the original polyline was continuous at the last outline endings: var midVector = RVector.getAverage(lastLeftRv, lastRightRv); var closestVertex = polyline.getClosestVertex(midVector); // # Issue # Successive vertices may occupy the same spot ! // Presumed is that the lowest match is returned !? if (closestVertex === 0) { // With the first vertex > continuous = continuousPoly; } // End with the first vertex > else { // With NOT the first vertex > var segmentShape1 = polyline.getSegmentAt(closestVertex - 1); for (n=closestVertex+1; n break; // Break out cycling remaining vertices } // End NO match } // Loop remaining vertices // -> Continue with next index + 1 var segmentShape2 = polyline.getSegmentAt(n - 1); if (!isNull(segmentShape1) && !isNull(segmentShape2)) { // With both segments valid var ori1 = segmentShape1.getDirection2(); // Presumed normalized var ori2 = RMath.getNormalizedAngle(segmentShape2.getDirection1() + Math.PI); // # Issue Fixed # getNormalizedAngle of a sum may rarely return 2PI !? // Fixed with setting result to zero. Default =RS.AngleTolerance if (RMath.fuzzyAngleCompare(ori2, 2 * Math.PI)) { // When angle is near 2PI > ori2 = 0; } // End Fix continuous = RMath.fuzzyAngleCompare(ori1, ori2, 0.01); // # Issue # Default fuzzy compare 0.01rad = 0.573deg !? } // End with both valid } // End with another vertex as the first // WIN debug: Report in Command History: (Not translated as Default) EAction.handleUserMessage("Cont.Poly at Set " + k + " | Vertex " + closestVertex + " : " + midVector + " = " + continuous); // Include a set ending Terminator: if (!continuous && (leftPlValid || rightPlValid)) { // When NOT continuous and any is valid > var terminator = new RLine(lastLeftRv, lastRightRv); if (terminator.getLength() > RS.PointTolerance) { // With a distinct length > leftNwPoly.appendShape(terminator, false); // NOTprepend // Merge outlines in a single closed outline and cast as one: // Merging: if (!rightNwPoly.isEmpty() && rightNwPoly.getLength() > RS.PointTolerance) { rightNwPoly.reverse(); leftNwPoly.appendShape(rightNwPoly, false); // NOTprepend } // Casting: if (!leftNwPoly.isEmpty() && leftNwPoly.getLength() > RS.PointTolerance) { leftNwPoly.setClosed(true); ret.push(leftNwPoly); } // Define new collectors as empty polyline: leftNwPoly = new RPolyline(); rightNwPoly = new RPolyline(); // WIN debug: Report in Command History: (Not translated as Default) EAction.handleUserMessage(("Included set %1 ending Terminator").arg(k)); } // End with a length } // End when NOT continuous } // Loop outline sets // NOT IMPLEMENTED // Optionally force Terminators where the Widths are different: // if (continuous && OPTIONAL) { // Optionally by preferences // var endWidth = polyline.getEndWidthAt(closestVertex - 1); // var startWidth = polyline.getStartWidthAt(closestVertex); // // # Issue Fixed # Widths may be NaN or blank // // Fixed with setting them equal to zero // if(!isNumber(endWidth)) { // When type is number, isNOT NaN, isNOT [+infinity, -infinity, NaN] // endWidth = 0; // } // End Fix // if(!isNumber(StartWidth)) { // When type is number, isNOT NaN, isNOT [+infinity, -infinity, NaN] // endWidth = 0; // } // End Fix // continuous = RMath.fuzzyCompare(endWidth, startWidth) // Default =RS.PointTolerance // } // End Optionally // Force continuousPoly if both remaining outlines are continuous: if (!leftNwPoly.isEmpty() && !rightNwPoly.isEmpty()) { // With both remaining outlines: if (lastLeftRv.equalsFuzzy(leftNwPoly.getStartPoint()) && lastRightRv.equalsFuzzy(rightNwPoly.getStartPoint())) { continuousPoly = true; } } // WIN Debug: Report in Command History: (Not translated as Default) EAction.handleUserMessage("continuous: " + continuous + " | continuousPoly: " + continuousPoly); // Include polyline end connectors and cast: if (continuousPoly) { // With a continuous poly: 2 end Connectors > // Left side: var connector = new RLine(lastLeftRv, startLeftRv); if (connector.getLength() > RS.PointTolerance) { // With a distinct length > leftNwPoly.appendShape(connector, false); // NOTprepend } // End with a length // Right side: var connector = new RLine(lastRightRv, startRightRv); if (connector.getLength() > RS.PointTolerance) { // With a distinct length > rightNwPoly.appendShape(connector, false); // NOTprepend } // End with a length // Cast both left/right as individual closed outlines: // Left side: if (!leftNwPoly.isEmpty() && leftNwPoly.getLength() > RS.PointTolerance) { leftNwPoly.setClosed(true); ret.push(leftNwPoly); } // Right side: if (!rightNwPoly.isEmpty() && rightNwPoly.getLength() > RS.PointTolerance) { rightNwPoly.setClosed(true); ret.push(rightNwPoly); } } // End continuous else if (!leftNwPoly.isEmpty() && !rightNwPoly.isEmpty()) { // With both remaining outlines: var terminator = new RLine(lastLeftRv, lastRightRv); if (terminator.getLength() > RS.PointTolerance) { // With a distinct length > leftNwPoly.appendShape(terminator, false); // NOTprepend // Merge outlines in a single closed outline and cast as one: if (rightNwPoly.getLength() > RS.PointTolerance) { rightNwPoly.reverse(); leftNwPoly.appendShape(rightNwPoly, false); // NOTprepend } if (leftNwPoly.getLength() > RS.PointTolerance) { leftNwPoly.setClosed(true); ret.push(leftNwPoly); } // WIN debug: Report in Command History: (Not translated as Default) EAction.handleUserMessage(("Included set %1 final Terminator").arg(k)); } // End with a length } } else { var polySegments = polyline.getExploded(); if (polySegments.length>0) { for (k=0; k shape = ShapeAlgorithms.splineToLineOrArc(shape, splineTolerance); } // End when preferred // In consideration (CVH): Forcing cubic splines from text to degree 2. // if (shape.getDegree() === 3) { // When cubic > // shape.setDegree(2); // } // End when cubic if (textToPolylines) { // spline to polyline with arcs: if (isSplineShape(shape)) { if (RSpline.hasProxy() && !splinesToLineSegments) { shape = shape.approximateWithArcs(splineTolerance); } else { shape = shape.toPolyline(splineSegments); } } } } if (!isNull(shape)) { var sc = shape.clone(); sc.color = col; if (textToPolylines) { // explode to polylines: if (!isNull(plText) && plText.getEndPoint().equalsFuzzy(shape.getStartPoint())) { plText.appendShape(sc); } else { if (!isNull(plText)) { plText.toLogicallyClosed(); } plText = new RPolyline(); ret.push(plText); plText.appendShape(sc); } } else { // explode to lines, arcs, polylines: ret.push(sc); } } } if (textToPolylines) { if (!isNull(plText)) { plText.toLogicallyClosed(); } } } } } // explode attribute entities into texts: else if (isAttributeEntity(entity)) { // create text data from attribute data: d = new RTextData(entity.getData()); // unlink from parent (block ref): d.setParentId(RObject.INVALID_ID); e = new RTextEntity(document, d); e.setSelected(true); e.copyAttributesFrom(entity.data()); ret.push(e); } // explode block reference or block reference array: else if (isBlockReferenceEntity(entity)) { var data = entity.getData(); var pos = entity.getPosition(); // explode array into multiple block references: if (data.getColumnCount()>1 || data.getRowCount()>1) { for (col=0; col