/****************************************************************************** * Copyright (c) 2012 Jan Rheinländer * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library 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 Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ******************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "FeatureLinearPattern.h" #include "DatumLine.h" #include "DatumPlane.h" using namespace PartDesign; namespace PartDesign { PROPERTY_SOURCE(PartDesign::LinearPattern, PartDesign::Transformed) const App::PropertyIntegerConstraint::Constraints LinearPattern::intOccurrences = {1, std::numeric_limits::max(), 1}; const char* LinearPattern::ModeEnums[] = {"Extent", "Spacing", nullptr}; LinearPattern::LinearPattern() { auto initialMode = LinearPatternMode::Extent; ADD_PROPERTY_TYPE( Direction, (nullptr), "Direction 1", App::Prop_None, "The first direction of the pattern. This can be a straight edge, a datum " "line, a sketch axis, or the normal of a planar face." ); ADD_PROPERTY_TYPE( Reversed, (0), "Direction 1", App::Prop_None, "Reverse the first direction of the pattern" ); ADD_PROPERTY_TYPE( Mode, (long(initialMode)), "Direction 1", App::Prop_None, "Selects how the pattern is dimensioned.\n'Extent': Uses the total length from the first " "to the last instance.\n'Spacing': Uses the distance between consecutive instances." ); ADD_PROPERTY_TYPE( Length, (100.0), "Direction 1", App::Prop_None, "The total length of the pattern, measured from the first to the last " "instance. This is only used when the Mode is set to 'Extent'." ); ADD_PROPERTY_TYPE( Offset, (10.0), "Direction 1", App::Prop_None, "The distance between each instance of the pattern. This is only used when " "the Mode is set to 'Spacing'." ); ADD_PROPERTY_TYPE( Spacings, ({-1.0}), "Direction 1", App::Prop_None, "A list of custom spacings between instances. If a value is -1, the global " "'Offset' is used for that spacing. The list should have one less item than " "the number of occurrences." ); ADD_PROPERTY_TYPE( SpacingPattern, ({}), "Direction 1", App::Prop_None, "(Experimental and subject to change. To enable " "this in the UI you can add a boolean parameter 'ExperiementalFeature' in " "Preferences/Mod/Part)\nDefines a repeating pattern of spacings for the second direction. " "For example, a list of [10, 20] will create alternating spacings of 10mm and 20mm." ); ADD_PROPERTY_TYPE( Occurrences, (2), "Direction 1", App::Prop_None, "The total number of instances in the first direction, including the original feature." ); Occurrences.setConstraints(&intOccurrences); Mode.setEnums(ModeEnums); setReadWriteStatusForMode(LinearPatternDirection::First); ADD_PROPERTY_TYPE( Direction2, (nullptr), "Direction 2", App::Prop_None, "The second direction of the pattern. This can be a straight edge, a datum " "line, a sketch axis, or the normal of a planar face." ); ADD_PROPERTY_TYPE( Reversed2, (0), "Direction 2", App::Prop_None, "Reverse the second direction of the pattern" ); ADD_PROPERTY_TYPE( Mode2, (long(initialMode)), "Direction 2", App::Prop_None, "Selects how the pattern is dimensioned in the second direction.\n'Extent': " "Uses the total length.\n'Spacing': Uses the distance between instances." ); ADD_PROPERTY_TYPE( Length2, (100.0), "Direction 2", App::Prop_None, "The total length of the pattern in the second direction, measured from the first to the " "last instance. This is only used when the Mode is set to 'Extent'." ); ADD_PROPERTY_TYPE( Offset2, (10.0), "Direction 2", App::Prop_None, "The distance between each instance of the pattern in the second direction. " "This is only used when the Mode is set to 'Spacing'." ); ADD_PROPERTY_TYPE( Spacings2, ({}), "Direction 2", App::Prop_None, "A list of custom spacings for the second direction. If a value is -1, the global 'Offset' " "is used. The list should have one less item than the number of occurrences." ); ADD_PROPERTY_TYPE( SpacingPattern2, ({}), "Direction 2", App::Prop_None, "(Experimental and subject to change. To enable " "this in the UI you can add a boolean parameter 'ExperiementalFeature' in " "Preferences/Mod/Part)\nDefines a repeating pattern of spacings for the second direction. " "For example, a list of [10, 20] will create alternating spacings of 10mm and 20mm." ); ADD_PROPERTY_TYPE( Occurrences2, (1), "Direction 2", App::Prop_None, "The total number of instances in the second direction, including the original feature." ); Occurrences2.setConstraints(&intOccurrences); Mode2.setEnums(ModeEnums); setReadWriteStatusForMode(LinearPatternDirection::Second); } short LinearPattern::mustExecute() const { if (Direction.isTouched() || Reversed.isTouched() || Mode.isTouched() || // Length and Offset are mutually exclusive, only one could be updated at once Length.isTouched() || Offset.isTouched() || Spacings.isTouched() || SpacingPattern.isTouched() || Occurrences.isTouched() || Direction2.isTouched() || Reversed2.isTouched() || Mode2.isTouched() || Length2.isTouched() || Offset2.isTouched() || Spacings2.isTouched() || SpacingPattern2.isTouched() || Occurrences2.isTouched()) { return 1; } return Transformed::mustExecute(); } void LinearPattern::setReadWriteStatusForMode(LinearPatternDirection dir) { bool isExtentMode = false; if (dir == LinearPatternDirection::First) { isExtentMode = (Mode.getValue() == static_cast(LinearPatternMode::Extent)); Length.setReadOnly(!isExtentMode); Offset.setReadOnly(isExtentMode); } else { isExtentMode = (Mode2.getValue() == static_cast(LinearPatternMode::Extent)); Length2.setReadOnly(!isExtentMode); Offset2.setReadOnly(isExtentMode); } } const std::list LinearPattern::getTransformations(const std::vector) { int occurrences = Occurrences.getValue(); int occurrences2 = Occurrences2.getValue(); if (occurrences < 1 || occurrences2 < 1) { throw Base::ValueError("At least one occurrence required"); } if (occurrences == 1 && occurrences2 == 1) { return {gp_Trsf()}; } // make sure spacings are correct size : updateSpacings(); // Calculate the base offset vector and final step positions for each direction gp_Vec offset1 = calculateOffsetVector(LinearPatternDirection::First); std::vector steps1 = calculateSteps(LinearPatternDirection::First, offset1); gp_Vec offset2 = calculateOffsetVector(LinearPatternDirection::Second); std::vector steps2 = calculateSteps(LinearPatternDirection::Second, offset2); // Combine the steps from both directions std::list transformations; for (const auto& step1 : steps1) { for (const auto& step2 : steps2) { gp_Trsf trans; trans.SetTranslation(step1 + step2); transformations.push_back(trans); } } return transformations; } gp_Vec LinearPattern::calculateOffsetVector(LinearPatternDirection dir) const { bool firstDir = dir == LinearPatternDirection::First; const auto& occurrencesProp = firstDir ? Occurrences : Occurrences2; int occurrences = occurrencesProp.getValue(); if (occurrences <= 1) { return gp_Vec(); // Return zero vector if no transformation is needed } const auto& dirProp = firstDir ? Direction : Direction2; if (!dirProp.getValue()) { return gp_Vec(); } const auto& reversedProp = firstDir ? Reversed : Reversed2; const auto& modeProp = firstDir ? Mode : Mode2; const auto& lengthProp = firstDir ? Length : Length2; double distance = lengthProp.getValue(); if (distance < Precision::Confusion()) { throw Base::ValueError("Pattern length too small"); } gp_Vec offset = getDirectionFromProperty(dirProp); if (reversedProp.getValue()) { offset.Reverse(); } // For 'Extent' mode, the vector represents one full step. // For 'Spacing' mode, it's just a normalized direction vector. if (static_cast(modeProp.getValue()) == LinearPatternMode::Extent) { offset *= distance / (occurrences - 1); } return offset; } std::vector LinearPattern::calculateSteps( LinearPatternDirection dir, const gp_Vec& offsetVector ) const { const auto& occurrencesProp = (dir == LinearPatternDirection::First) ? Occurrences : Occurrences2; const auto& modeProp = (dir == LinearPatternDirection::First) ? Mode : Mode2; const auto& offsetValueProp = (dir == LinearPatternDirection::First) ? Offset : Offset2; const auto& spacingsProp = (dir == LinearPatternDirection::First) ? Spacings : Spacings2; const auto& spacingPatternProp = (dir == LinearPatternDirection::First) ? SpacingPattern : SpacingPattern2; int occurrences = occurrencesProp.getValue(); std::vector steps {gp_Vec()}; // First step is always zero steps.reserve(occurrences); if (occurrences <= 1) { return steps; } if (modeProp.getValue() == static_cast(LinearPatternMode::Spacing)) { const std::vector spacings = spacingsProp.getValues(); const std::vector pattern = spacingPatternProp.getValues(); bool usePattern = pattern.size() > 1; double cumulativeDistance = 0.0; // Spacing priority: individual spacing > pattern > global offset const auto spacingAt = [&](unsigned i) { if (spacings.at(i - 1) != -1.0) { return spacings.at(i - 1); } if (usePattern) { return pattern.at(static_cast(fmod(i - 1, pattern.size()))); } return offsetValueProp.getValue(); }; for (int i = 1; i < occurrences; ++i) { cumulativeDistance += spacingAt(i); steps.push_back(offsetVector * cumulativeDistance); } } else { // Extent Mode for (int i = 1; i < occurrences; ++i) { steps.push_back(offsetVector * i); } } return steps; } gp_Dir LinearPattern::getDirectionFromProperty(const App::PropertyLinkSub& dirProp) const { App::DocumentObject* refObject = dirProp.getValue(); if (!refObject) { throw Base::ValueError("No direction reference specified"); } std::vector subStrings = dirProp.getSubValues(); if (subStrings.empty()) { throw Base::ValueError("No direction reference specified"); } gp_Dir dir; if (auto* refSketch = freecad_cast(refObject)) { Base::Axis axis; if (subStrings[0] == "H_Axis") { axis = refSketch->getAxis(Part::Part2DObject::H_Axis); axis *= refSketch->Placement.getValue(); } else if (subStrings[0] == "V_Axis") { axis = refSketch->getAxis(Part::Part2DObject::V_Axis); axis *= refSketch->Placement.getValue(); } else if (subStrings[0] == "N_Axis") { axis = refSketch->getAxis(Part::Part2DObject::N_Axis); axis *= refSketch->Placement.getValue(); } else if (subStrings[0].compare(0, 4, "Axis") == 0) { int AxId = std::atoi(subStrings[0].substr(4, 4000).c_str()); if (AxId >= 0 && AxId < refSketch->getAxisCount()) { axis = refSketch->getAxis(AxId); axis *= refSketch->Placement.getValue(); } } else if (subStrings[0].compare(0, 4, "Edge") == 0) { Part::TopoShape refShape = refSketch->Shape.getShape(); TopoDS_Shape ref = refShape.getSubShape(subStrings[0].c_str()); TopoDS_Edge refEdge = TopoDS::Edge(ref); if (refEdge.IsNull()) { throw Base::ValueError("Failed to extract direction edge"); } BRepAdaptor_Curve adapt(refEdge); if (adapt.GetType() != GeomAbs_Line) { throw Base::TypeError("Direction edge must be a straight line"); } gp_Pnt p = adapt.Line().Location(); gp_Dir d = adapt.Line().Direction(); // the axis is not given in local coordinates and mustn't be multiplied with the // placement axis.setBase(Base::Vector3d(p.X(), p.Y(), p.Z())); axis.setDirection(Base::Vector3d(d.X(), d.Y(), d.Z())); } dir = gp_Dir(axis.getDirection().x, axis.getDirection().y, axis.getDirection().z); } else if (auto* plane = freecad_cast(refObject)) { Base::Vector3d d = plane->getNormal(); dir = gp_Dir(d.x, d.y, d.z); } else if (auto* line = freecad_cast(refObject)) { Base::Vector3d d = line->getDirection(); dir = gp_Dir(d.x, d.y, d.z); } else if (auto* plane = freecad_cast(refObject)) { Base::Vector3d d = plane->getDirection(); dir = gp_Dir(d.x, d.y, d.z); } else if (auto* line = freecad_cast(refObject)) { Base::Vector3d d = line->getDirection(); dir = gp_Dir(d.x, d.y, d.z); } else if (auto* refFeature = freecad_cast(refObject)) { if (subStrings[0].empty()) { throw Base::ValueError("No direction reference specified"); } Part::TopoShape refShape = refFeature->Shape.getShape(); TopoDS_Shape ref = refShape.getSubShape(subStrings[0].c_str()); if (ref.ShapeType() == TopAbs_FACE) { TopoDS_Face refFace = TopoDS::Face(ref); if (refFace.IsNull()) { throw Base::ValueError("Failed to extract direction plane"); } BRepAdaptor_Surface adapt(refFace); if (adapt.GetType() != GeomAbs_Plane) { throw Base::TypeError("Direction face must be planar"); } dir = adapt.Plane().Axis().Direction(); } else if (ref.ShapeType() == TopAbs_EDGE) { TopoDS_Edge refEdge = TopoDS::Edge(ref); if (refEdge.IsNull()) { throw Base::ValueError("Failed to extract direction edge"); } BRepAdaptor_Curve adapt(refEdge); if (adapt.GetType() != GeomAbs_Line) { throw Base::ValueError("Direction edge must be a straight line"); } dir = adapt.Line().Direction(); } else { throw Base::ValueError("Direction reference must be edge or face"); } } else { throw Base::ValueError( "Direction reference must be edge/face of a feature or a datum line/plane" ); } TopLoc_Location invObjLoc = this->getLocation().Inverted(); dir.Transform(invObjLoc.Transformation()); return Base::convertTo(dir); } void LinearPattern::updateSpacings() { updateSpacings(LinearPatternDirection::First); updateSpacings(LinearPatternDirection::Second); } void LinearPattern::updateSpacings(LinearPatternDirection dir) { bool isSecondDir = dir == LinearPatternDirection::Second; App::PropertyFloatList& spacingsProp = isSecondDir ? Spacings2 : Spacings; App::PropertyLength& offsetProp = isSecondDir ? Offset2 : Offset; const App::PropertyIntegerConstraint& occurrencesProp = isSecondDir ? Occurrences2 : Occurrences; std::vector spacings = spacingsProp.getValues(); size_t targetCount = occurrencesProp.getValue() - 1; // 1 less spacing than there are occurrences. for (auto& spacing : spacings) { if (spacing == offsetProp.getValue()) { spacing = -1.0; } } if (spacings.size() == targetCount) { return; } if (spacings.size() < targetCount) { spacings.reserve(targetCount); while (spacings.size() < targetCount) { spacings.push_back(-1.0); } } else { spacings.resize(targetCount); } spacingsProp.setValues(spacings); } void LinearPattern::handleChangedPropertyType( Base::XMLReader& reader, const char* TypeName, App::Property* prop ) // transforms properties that had been changed { // property Occurrences had the App::PropertyInteger and was changed to // App::PropertyIntegerConstraint if (prop == &Occurrences && strcmp(TypeName, "App::PropertyInteger") == 0) { App::PropertyInteger OccurrencesProperty; // restore the PropertyInteger to be able to set its value OccurrencesProperty.Restore(reader); Occurrences.setValue(OccurrencesProperty.getValue()); } else { Transformed::handleChangedPropertyType(reader, TypeName, prop); } } void LinearPattern::onChanged(const App::Property* prop) { auto mode = static_cast(Mode.getValue()); auto mode2 = static_cast(Mode2.getValue()); if (prop == &Mode) { setReadWriteStatusForMode(LinearPatternDirection::First); } else if (prop == &Occurrences) { updateSpacings(LinearPatternDirection::First); syncLengthAndOffset(LinearPatternDirection::First); } else if (prop == &Offset && mode == LinearPatternMode::Spacing) { syncLengthAndOffset(LinearPatternDirection::First); } else if (prop == &Length && mode == LinearPatternMode::Extent) { syncLengthAndOffset(LinearPatternDirection::First); } else if (prop == &Mode2) { setReadWriteStatusForMode(LinearPatternDirection::Second); } else if (prop == &Occurrences2) { updateSpacings(LinearPatternDirection::Second); syncLengthAndOffset(LinearPatternDirection::Second); } else if (prop == &Offset2 && mode2 == LinearPatternMode::Spacing) { syncLengthAndOffset(LinearPatternDirection::Second); } else if (prop == &Length2 && mode2 == LinearPatternMode::Extent) { syncLengthAndOffset(LinearPatternDirection::Second); } Transformed::onChanged(prop); } void LinearPattern::syncLengthAndOffset(LinearPatternDirection dir) { // Get references to the correct properties based on the direction auto& modeProp = (dir == LinearPatternDirection::First) ? Mode : Mode2; auto& lengthProp = (dir == LinearPatternDirection::First) ? Length : Length2; auto& offsetProp = (dir == LinearPatternDirection::First) ? Offset : Offset2; auto& occurrencesProp = (dir == LinearPatternDirection::First) ? Occurrences : Occurrences2; auto mode = static_cast(modeProp.getValue()); int occurrences = occurrencesProp.getValue(); occurrences = occurrences <= 1 ? 1 : occurrences - 1; if (mode == LinearPatternMode::Spacing && !lengthProp.testStatus(App::Property::Status::Immutable)) { lengthProp.setValue(offsetProp.getValue() * occurrences); } else if (mode == LinearPatternMode::Extent && !offsetProp.testStatus(App::Property::Status::Immutable)) { offsetProp.setValue(lengthProp.getValue() / occurrences); } } } // namespace PartDesign