/*************************************************************************** * Copyright (c) 2010 Juergen Riegel * * 2020 David Österberg * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include "FeatureHelix.h" using namespace PartDesign; const char* Helix::ModeEnums[] = {"pitch-height-angle", "pitch-turns-angle", "height-turns-angle", "height-turns-growth", nullptr}; PROPERTY_SOURCE(PartDesign::Helix, PartDesign::ProfileBased) // we purposely use not FLT_MAX because this would not be computable const App::PropertyFloatConstraint::Constraints Helix::floatTurns = {Precision::Confusion(), std::numeric_limits::max(), 1.0}; const App::PropertyFloatConstraint::Constraints Helix::floatTolerance = {0.1, std::numeric_limits::max(), 1.0}; const App::PropertyAngle::Constraints Helix::floatAngle = {-89.0, 89.0, 1.0}; Helix::Helix() { addSubType = FeatureAddSub::Additive; auto initialMode = HelixMode::pitch_height_angle; const char* group = "Helix"; ADD_PROPERTY_TYPE( Base, (Base::Vector3d(0.0, 0.0, 0.0)), group, App::Prop_ReadOnly, QT_TRANSLATE_NOOP( "App::Property", "The center point of the helix' start; derived from the reference axis." ) ); ADD_PROPERTY_TYPE( Axis, (Base::Vector3d(0.0, 1.0, 0.0)), group, App::Prop_ReadOnly, QT_TRANSLATE_NOOP("App::Property", "The helix' direction; derived from the reference axis.") ); ADD_PROPERTY_TYPE( ReferenceAxis, (nullptr), group, App::Prop_None, QT_TRANSLATE_NOOP("App::Property", "The reference axis of the helix.") ); ADD_PROPERTY_TYPE( Mode, (long(initialMode)), group, App::Prop_None, QT_TRANSLATE_NOOP( "App::Property", "The helix input mode specifies which properties are set by the user.\n" "Dependent properties are then calculated." ) ); Mode.setEnums(ModeEnums); ADD_PROPERTY_TYPE( Pitch, (10.0), group, App::Prop_None, QT_TRANSLATE_NOOP("App::Property", "The axial distance between two turns.") ); ADD_PROPERTY_TYPE( Height, (30.0), group, App::Prop_None, QT_TRANSLATE_NOOP( "App::Property", "The height of the helix' path, not accounting for the extent of the profile." ) ); ADD_PROPERTY_TYPE( Turns, (3.0), group, App::Prop_None, QT_TRANSLATE_NOOP("App::Property", "The number of turns in the helix.") ); Turns.setConstraints(&floatTurns); ADD_PROPERTY_TYPE( Angle, (0.0), group, App::Prop_None, QT_TRANSLATE_NOOP( "App::Property", "The angle of the cone that forms a hull around the helix.\n" "Non-zero values turn the helix into a conical spiral.\n" "Positive values make the radius grow, negative shrinks." ) ); Angle.setConstraints(&floatAngle); ADD_PROPERTY_TYPE( Growth, (0.0), group, App::Prop_None, QT_TRANSLATE_NOOP( "App::Property", "The growth of the helix' radius per turn.\n" "Non-zero values turn the helix into a conical spiral." ) ); ADD_PROPERTY_TYPE( LeftHanded, (false), group, App::Prop_None, QT_TRANSLATE_NOOP( "App::Property", "Sets the turning direction to left handed,\n" "i.e. counter-clockwise when moving along its axis." ) ); ADD_PROPERTY_TYPE( Reversed, (false), group, App::Prop_None, QT_TRANSLATE_NOOP( "App::Property", "Determines whether the helix points in the opposite direction of the axis." ) ); ADD_PROPERTY_TYPE( Outside, (false), group, App::Prop_None, QT_TRANSLATE_NOOP( "App::Property", "If set, the result will be the intersection of the profile and the preexisting body." ) ); ADD_PROPERTY_TYPE( HasBeenEdited, (false), group, App::Prop_Hidden, QT_TRANSLATE_NOOP( "App::Property", "If false, the tool will propose an initial value for the pitch based on the profile " "bounding box,\n" "so that self intersection is avoided." ) ); ADD_PROPERTY_TYPE( Tolerance, (0.1), group, App::Prop_None, QT_TRANSLATE_NOOP("App::Property", "Fusion Tolerance for the Helix, increase if helical shape does not merge nicely with part.") ); Tolerance.setConstraints(&floatTolerance); setReadWriteStatusForMode(initialMode); } short Helix::mustExecute() const { if (Placement.isTouched() || ReferenceAxis.isTouched() || Axis.isTouched() || Base.isTouched() || Angle.isTouched()) { return 1; } return ProfileBased::mustExecute(); } App::DocumentObjectExecReturn* Helix::execute() { if (onlyHaveRefined()) { return App::DocumentObject::StdReturn; } // Validate and normalize parameters HelixMode mode = static_cast(Mode.getValue()); if (mode == HelixMode::pitch_height_angle) { if (Pitch.getValue() < Precision::Confusion()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: Pitch too small!") ); } if (Height.getValue() < Precision::Confusion()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: height too small!") ); } Turns.setValue(Height.getValue() / Pitch.getValue()); Growth.setValue(Pitch.getValue() * tan(Base::toRadians(Angle.getValue()))); } else if (mode == HelixMode::pitch_turns_angle) { if (Pitch.getValue() < Precision::Confusion()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: pitch too small!") ); } if (Turns.getValue() < Precision::Confusion()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: turns too small!") ); } Height.setValue(Turns.getValue() * Pitch.getValue()); Growth.setValue(Pitch.getValue() * tan(Base::toRadians(Angle.getValue()))); } else if (mode == HelixMode::height_turns_angle) { if (Height.getValue() < Precision::Confusion()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: height too small!") ); } if (Turns.getValue() < Precision::Confusion()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: turns too small!") ); } Pitch.setValue(Height.getValue() / Turns.getValue()); Growth.setValue(Pitch.getValue() * tan(Base::toRadians(Angle.getValue()))); } else if (mode == HelixMode::height_turns_growth) { if (Turns.getValue() < Precision::Confusion()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: turns too small!") ); } if ((Height.getValue() < Precision::Confusion()) && (abs(Growth.getValue()) < Precision::Confusion()) && Turns.getValue() > 1.0) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: either height or growth must not be zero!") ); } Pitch.setValue(Height.getValue() / Turns.getValue()); if (Height.getValue() > 0) { Angle.setValue( Base::toDegrees(atan(Turns.getValue() * Growth.getValue() / Height.getValue())) ); } else { // On purpose, we're doing nothing here; the else-branch is just for this comment. // - we don't print a warning, as for a flat spiral a zero-height is perfectly fine // - we don't void the angle (somehow) so that it keeps its value. This allows in // interactive usage to just go back to another mode and everything keeps working } } else { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: unsupported mode") ); } TopoDS_Shape sketchshape; // Fixme: Should this be TopoShape here and below? try { sketchshape = getVerifiedFace(); } catch (const Base::Exception& e) { return new App::DocumentObjectExecReturn(e.what()); } if (sketchshape.IsNull()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: No valid sketch or face") ); } else { // TODO: currently we only allow planar faces. the reason for this is that with other faces // in front, we could not use the current simulate approach and build the start and end face // from the wires. As the shell begins always at the spine and not the profile, the // sketchshape cannot be used directly as front face. We would need a method to translate // the front shape to match the shell starting position somehow... TopoDS_Face face = TopoDS::Face(sketchshape); BRepAdaptor_Surface adapt(face); if (adapt.GetType() != GeomAbs_Plane) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: Face must be planar") ); } } // if the Base property has a valid shape, fuse the AddShape into it TopoShape base; try { base = getBaseTopoShape(); } catch (const Base::Exception&) { // fall back to support (for legacy features) base = TopoShape(); } // update Axis from ReferenceAxis try { updateAxis(); } catch (const Base::Exception& e) { return new App::DocumentObjectExecReturn(e.what()); } try { this->positionByPrevious(); TopLoc_Location invObjLoc = this->getLocation().Inverted(); base.move(invObjLoc); TopoDS_Shape result; // generate the helix path TopoDS_Shape path; if (Angle.getValue() == 0.) { // breaking the path at each turn prevents an OCC issue path = generateHelixPath(); } else { // don't break the path or the generated solid is invalid path = generateHelixPath(1000.); } TopoDS_Shape face = sketchshape; face.Move(invObjLoc); Bnd_Box bounds; BRepBndLib::Add(path, bounds); double size = sqrt(bounds.SquareExtent()); ShapeFix_ShapeTolerance fix; fix.LimitTolerance(path, Precision::Confusion() * 1e-6 * size); // needed to produce valid // Pipe for very big parts // We introduce final part tolerance with the second call to LimitTolerance below, however // OCCT has a bug where the side-walls of the Pipe disappear with very large (km range) // pieces increasing a tiny bit of extra tolerance to the path fixes this. This will in any // case be less than the tolerance lower limit below, but sufficient to avoid the bug BRepOffsetAPI_MakePipe mkPS(TopoDS::Wire(path), face, GeomFill_Trihedron::GeomFill_IsFrenet, Standard_False); result = mkPS.Shape(); BRepClass3d_SolidClassifier SC(result); SC.PerformInfinitePoint(Precision::Confusion()); if (SC.State() == TopAbs_IN) { result.Reverse(); } fix.LimitTolerance( result, Precision::Confusion() * size * Tolerance.getValue() ); // significant precision reduction due to helical approximation - needed to allow fusion // to succeed // try to auto-fix possible invalid result ShapeFix_Solid fixer; fixer.Init(TopoDS::Solid(result)); if (fixer.Perform()) { result = fixer.Solid(); } AddSubShape.setValue(result); if (base.isNull()) { if (getAddSubType() == FeatureAddSub::Subtractive) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: There is nothing to subtract") ); } if (!isSingleSolidRuleSatisfied(result)) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: Result has multiple solids") ); } // store shape before refinement this->rawShape = result; Shape.setValue(getSolid(result)); return App::DocumentObject::StdReturn; } if (getAddSubType() == FeatureAddSub::Additive) { FCBRepAlgoAPI_Fuse mkFuse(base.getShape(), result); if (!mkFuse.IsDone()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: Adding the helix failed") ); } // we have to get the solids (fuse sometimes creates compounds) TopoShape boolOp = this->getSolid(mkFuse.Shape()); // lets check if the result is a solid if (boolOp.isNull()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: Result is not a solid") ); } if (!isSingleSolidRuleSatisfied(boolOp.getShape())) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: Result has multiple solids") ); } // store shape before refinement this->rawShape = boolOp; boolOp = refineShapeIfActive(boolOp, RefineErrorPolicy::Warn); Shape.setValue(getSolid(boolOp)); } else if (getAddSubType() == FeatureAddSub::Subtractive) { TopoShape boolOp; if (Outside.getValue()) { // are we subtracting the inside or the outside of the profile. FCBRepAlgoAPI_Common mkCom(result, base.getShape()); if (!mkCom.IsDone()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: Intersecting the helix failed") ); } boolOp = this->getSolid(mkCom.Shape()); } else { FCBRepAlgoAPI_Cut mkCut(base.getShape(), result); if (!mkCut.IsDone()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: Subtracting the helix failed") ); } boolOp = this->getSolid(mkCut.Shape()); } // lets check if the result is a solid if (boolOp.isNull()) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: Result is not a solid") ); } if (!isSingleSolidRuleSatisfied(boolOp.getShape())) { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: Result has multiple solids") ); } // store shape before refinement this->rawShape = boolOp; boolOp = refineShapeIfActive(boolOp, RefineErrorPolicy::Warn); Shape.setValue(getSolid(boolOp)); } return App::DocumentObject::StdReturn; } catch (Standard_Failure& e) { if (std::string(e.GetMessageString()) == "TopoDS::Face") { return new App::DocumentObjectExecReturn( QT_TRANSLATE_NOOP("Exception", "Error: Could not create face from sketch") ); } else { return new App::DocumentObjectExecReturn(e.GetMessageString()); } } catch (Base::Exception& e) { return new App::DocumentObjectExecReturn(e.what()); } } void Helix::updateAxis() { App::DocumentObject* pcReferenceAxis = ReferenceAxis.getValue(); const std::vector& subReferenceAxis = ReferenceAxis.getSubValues(); Base::Vector3d base; Base::Vector3d dir; getAxis(pcReferenceAxis, subReferenceAxis, base, dir, ForbiddenAxis::NoCheck); Base.setValue(base.x, base.y, base.z); Axis.setValue(dir.x, dir.y, dir.z); } TopoDS_Shape Helix::generateHelixPath(double breakAtTurn) { double turns = Turns.getValue(); double height = Height.getValue(); bool leftHanded = LeftHanded.getValue(); bool reversed = Reversed.getValue(); double angle = Angle.getValue(); double growth = Growth.getValue(); if (fabs(angle) < Precision::Confusion()) { angle = 0.0; } // get revolve axis Base::Vector3d baseVector = Base.getValue(); gp_Pnt pnt(baseVector.x, baseVector.y, baseVector.z); Base::Vector3d axisVector = Axis.getValue(); gp_Dir dir(axisVector.x, axisVector.y, axisVector.z); Base::Vector3d normal = getProfileNormal(); Base::Vector3d start = axisVector.Cross(normal); // pointing towards the desired helix start point. // if our axis is (nearly) aligned with the profile's normal, we're only interested in the // "twist" of the helix. The actual starting point, and thus the radius, isn't important as long // as it's somewhere in the profile's plane: an arbitrary vector perpendicular to the normal. if (start.IsNull()) { auto hopefullyNotParallel = Base::Vector3d(1.0, 2.0, 3.0); start = normal.Cross(hopefullyNotParallel); if (start.IsNull()) { // bad luck hopefullyNotParallel = Base::Vector3d(3.0, 2.0, 1.0); start = normal.Cross(hopefullyNotParallel); } } gp_Dir dir_start(start.x, start.y, start.z); // Find out in what quadrant relative to the axis the profile is located, and the exact position. Base::Vector3d profileCenter = getProfileCenterPoint(); // The factor of 100 below ensures that profile size is small compared to the curvature of the // helix. This improves the issue reported in // https://forum.freecad.org/viewtopic.php?f=10&t=65048 double axisOffset = 100.0 * (profileCenter * start - baseVector * start); double radius = std::fabs(axisOffset); bool turned = axisOffset < 0; // since the factor does not only change the radius but also the path position, we must shift // its offset back using the square of the factor double startOffset = 10000.0 * std::fabs((angle <= 0. ? 1. : 0.) * (profileCenter * axisVector) - baseVector * axisVector); if (radius < Precision::Confusion()) { // in this case ensure that axis is not in the sketch plane if (fabs(axisVector * normal) < Precision::Confusion()) { throw Base::ValueError("Error: Result is self intersecting"); } radius = 1000.0; // fallback to radius 1000 } bool growthMode = std::string(Mode.getValueAsString()).find("growth") != std::string::npos; double radiusTop; if (growthMode) { radiusTop = radius + turns * growth; } else { radiusTop = radius + height * tan(Base::toRadians(angle)); } // build the helix path TopoDS_Shape path = TopoShape().makeSpiralHelix(radius, radiusTop, height, turns, breakAtTurn, leftHanded); /* * The helix wire is created with the axis coinciding with z-axis and the start point at * (radius, 0, 0) We want to move it so that the axis becomes aligned with "dir" and "pnt", we * also want (radius,0,0) to map to the sketch plane. */ gp_Pnt origo(0.0, 0.0, 0.0); gp_Dir dir_axis1(0.0, 0.0, 1.0); // pointing along the helix axis, as created. gp_Dir dir_axis2(1.0, 0.0, 0.0); // pointing towards the helix start point, as created. gp_Trsf mov; if (abs(startOffset) > 0) { // translate the helix so that the starting point aligns with the // profile mov.SetTranslation(startOffset * gp_Vec(dir_axis1)); TopLoc_Location loc(mov); path.Move(loc); } // because of the radius factor we used above, we must reverse after the // startOffset movement (that brings the path back to the desired position) if (reversed) { mov.SetRotation(gp_Ax1(origo, dir_axis2), std::numbers::pi); TopLoc_Location loc(mov); path.Move(loc); } if (turned) { // turn the helix so that the starting point aligns with the profile mov.SetRotation(gp_Ax1(origo, dir_axis1), std::numbers::pi); TopLoc_Location loc(mov); path.Move(loc); } gp_Ax3 sourceCS(origo, dir_axis1, dir_axis2); gp_Ax3 targetCS(pnt, dir, dir_start); mov.SetTransformation(sourceCS, targetCS); TopLoc_Location loc(mov); path.Move(loc.Inverted()); TopLoc_Location invObjLoc = this->getLocation().Inverted(); path.Move(invObjLoc); return path; } // this function calculates self intersection safe pitch based on the profile bounding box. double Helix::safePitch() { Base::Vector3d axisVec = Axis.getValue(); Base::Vector3d startVec = axisVec.Cross(getProfileNormal()); // pointing towards the helix // start point HelixMode mode = static_cast(Mode.getValue()); double growthValue = Growth.getValue(); double turnsValue = Turns.getValue(); // handle case if axis is orthogonal to profile // since startVec.IsNull() fails sometimes, for e.g. (0.0, 0.0, -0.0) // we take the precision into account if (startVec.Length() < Precision::Confusion()) { // when not in growth mode any pitch > 0 is safe if (mode != HelixMode::height_turns_growth) { return Precision::Confusion(); } // if growth is not zero, there will in many cases be intersections // when the turn is >= 1, thus return an 'infinite' pitch // Note: The resulting helix body is in this case often garbage since // the OCC algorithm to create the helix fails. // Nevertheless, the result is a valid body so it should be valuable for users // to get this correct warning anyway. else { if (abs(turnsValue) >= 1.0 && abs(growthValue) > 0.0) { return Precision::Infinite(); } } } double angle = Base::toRadians(Angle.getValue()); gp_Dir direction(axisVec.x, axisVec.y, axisVec.z); gp_Dir directionStart(startVec.x, startVec.y, startVec.z); TopoDS_Shape sketchshape = getVerifiedFace(); Bnd_Box boundingBox; BRepBndLib::Add(sketchshape, boundingBox); // get boundary and dimensions of boundingBox double Xmin, Ymin, Zmin, Xmax, Ymax, Zmax; boundingBox.Get(Xmin, Ymin, Zmin, Xmax, Ymax, Zmax); double X = Xmax - Xmin, Y = Ymax - Ymin, Z = Zmax - Zmin; gp_Vec boundingBoxVec(X, Y, Z); // Below is an approximation because since we take the bounding box it is // impossible to calculate it precisely. For example a circle has as bounding // box a square and thus results in a larger pitch than really necessary // minimal safe pitch if the angle or growth is 0 double pitch0 = boundingBoxVec * direction; if (mode == HelixMode::height_turns_growth) { // if the distance perpendicular to axisVec // between two turns is larger than the bounding box size in this direction // the minimal necessary pitch is zero if (abs(growthValue) > abs(boundingBoxVec * directionStart)) { return 0.0; } else { // if less than one turn, every pitch is safe // Note: at the moment helices with a growth end with a plane // whose normal is the final direction of the helix path. // In case this might be changed in future, also 1.0 turn would be safe. if (turnsValue < 1.0) { return 0.0; } else { return pitch0; } } } else { // if the angle is so large that the distance perpendicular to axisVec // between two turns is larger than the bounding box size in this direction // the pitch can be smaller than pitch0 if (tan(abs(angle)) * pitch0 > abs(boundingBoxVec * directionStart)) { return abs(boundingBoxVec * directionStart) / tan(abs(angle)); } else { return pitch0; } } } // this function proposes pitch and height void Helix::proposeParameters(bool force) { if (force || !HasBeenEdited.getValue()) { TopoDS_Shape sketchshape = getVerifiedFace(); Bnd_Box bb; BRepBndLib::Add(sketchshape, bb); bb.SetGap(0.0); double pitch = 1.1 * sqrt(bb.SquareExtent()); Pitch.setValue(pitch); Height.setValue(pitch * 3.0); HasBeenEdited.setValue(true); } } Base::Vector3d Helix::getProfileCenterPoint() { TopoDS_Shape profileshape; profileshape = getVerifiedFace(); Bnd_Box box; BRepBndLib::Add(profileshape, box); box.SetGap(0.0); double xmin, ymin, zmin, xmax, ymax, zmax; box.Get(xmin, ymin, zmin, xmax, ymax, zmax); return Base::Vector3d(0.5 * (xmin + xmax), 0.5 * (ymin + ymax), 0.5 * (zmin + zmax)); } void Helix::handleChangedPropertyType(Base::XMLReader& reader, const char* TypeName, App::Property* prop) { // property Turns had the App::PropertyFloat and was changed to App::PropertyFloatConstraint if (prop == &Turns && strcmp(TypeName, "App::PropertyFloat") == 0) { App::PropertyFloat TurnsProperty; // restore the PropertyFloat to be able to set its value TurnsProperty.Restore(reader); Turns.setValue(TurnsProperty.getValue()); } // property Growth had the App::PropertyLength and was changed to App::PropertyDistance else if (prop == &Growth && strcmp(TypeName, "App::PropertyLength") == 0) { App::PropertyLength GrowthProperty; // restore the PropertyLength to be able to set its value GrowthProperty.Restore(reader); Growth.setValue(GrowthProperty.getValue()); } else { ProfileBased::handleChangedPropertyType(reader, TypeName, prop); } } void Helix::onChanged(const App::Property* prop) { if (prop == &Mode) { // Depending on the mode, the derived properties are set read-only auto inputMode = static_cast(Mode.getValue()); setReadWriteStatusForMode(inputMode); } ProfileBased::onChanged(prop); } void Helix::setReadWriteStatusForMode(HelixMode inputMode) { switch (inputMode) { case HelixMode::pitch_height_angle: // primary input: Pitch.setStatus(App::Property::ReadOnly, false); Height.setStatus(App::Property::ReadOnly, false); Angle.setStatus(App::Property::ReadOnly, false); // derived props: Turns.setStatus(App::Property::ReadOnly, true); Growth.setStatus(App::Property::ReadOnly, true); break; case HelixMode::pitch_turns_angle: // primary input: Pitch.setStatus(App::Property::ReadOnly, false); Turns.setStatus(App::Property::ReadOnly, false); Angle.setStatus(App::Property::ReadOnly, false); // derived props: Height.setStatus(App::Property::ReadOnly, true); Growth.setStatus(App::Property::ReadOnly, true); break; case HelixMode::height_turns_angle: // primary input: Height.setStatus(App::Property::ReadOnly, false); Turns.setStatus(App::Property::ReadOnly, false); Angle.setStatus(App::Property::ReadOnly, false); // derived props: Pitch.setStatus(App::Property::ReadOnly, true); Growth.setStatus(App::Property::ReadOnly, true); break; case HelixMode::height_turns_growth: // primary input: Height.setStatus(App::Property::ReadOnly, false); Turns.setStatus(App::Property::ReadOnly, false); Growth.setStatus(App::Property::ReadOnly, false); // derived props: Pitch.setStatus(App::Property::ReadOnly, true); Angle.setStatus(App::Property::ReadOnly, true); break; default: Pitch.setStatus(App::Property::ReadOnly, false); Height.setStatus(App::Property::ReadOnly, false); Turns.setStatus(App::Property::ReadOnly, false); Angle.setStatus(App::Property::ReadOnly, false); Growth.setStatus(App::Property::ReadOnly, false); break; } } PROPERTY_SOURCE(PartDesign::AdditiveHelix, PartDesign::Helix) AdditiveHelix::AdditiveHelix() { addSubType = Additive; Outside.setStatus(App::Property::Hidden, true); } PROPERTY_SOURCE(PartDesign::SubtractiveHelix, PartDesign::Helix) SubtractiveHelix::SubtractiveHelix() { addSubType = Subtractive; Outside.setStatus(App::Property::Hidden, false); }