class LatticeScLERP(lattice2BaseFeature.LatticeFeature): "The Lattice ScLERP object" def derivedInit(self, host): host.addProperty( "App::PropertyLink", "Placement1Ref", "Lattice ScLERP", "First placement, or an array of two placements to interpolate between." ) host.addProperty("App::PropertyLink", "Placement2Ref", "Lattice ScLERP", "Second placement to interpolate between.") host.addProperty( "App::PropertyBool", "Shorten", "Lattice ScLERP", "Use shortest path. (if not, and angle difference of two placement exceeds 180 degrees, longer path will be taken)" ) host.Shorten = True self.assureGenerator(host) host.ValuesSource = "Generator" host.GeneratorMode = "SpanN" host.EndInclusive = True host.SpanStart = 0.0 host.SpanEnd = 1.0 host.Step = 1.0 / 11 host.Count = 11 def assureGenerator(self, host): '''Adds an instance of value series generator, if one doesn't exist yet.''' if hasattr(self, "generator"): return self.generator = ValueSeriesGenerator(host) self.generator.addProperties( groupname="Lattice Array", groupname_gen="Lattice Series Generator", valuesdoc= "List of parameter values. Values should be in range 0..1 for interpolation, and can be outside for extrapolation.", valuestype="App::PropertyFloat") def updateReadonlyness(self, host): super(LatticeScLERP, self).updateReadonlyness(host) self.assureGenerator(host) self.generator.updateReadonlyness() #host.setEditorMode('ReferenceValue', 0 if host.ReferencePlacementOption == 'at custom value' else 2) def derivedExecute(self, host): self.assureGenerator(host) self.generator.execute() values = [float(strv) for strv in host.Values] input = lattice2BaseFeature.getPlacementsList(host.Placement1Ref) if host.Placement2Ref is not None: input.extend( lattice2BaseFeature.getPlacementsList(host.Placement2Ref)) if len(input) != 2: raise ValueError("Need exactly 2 placements. {n} provided.".format( n=len(input))) plm1, plm2 = input # construct interpolation functions # prepare lists of input samples def plmByVal(val): return plm1.sclerp(plm2, val, host.Shorten) output = [plmByVal(val) for val in values] ## update reference placement #ref = host.ReferencePlacementOption #if ref == 'external': # pass #elif ref == 'origin': # self.setReferencePlm(host, None) #elif ref == 'inherit': # self.setReferencePlm(host, lattice2BaseFeature.getReferencePlm(host.Base)) #elif ref == 'SpanStart': # self.setReferencePlm(host, plmByVal(float(host.SpanStart))) #elif ref == 'SpanEnd': # self.setReferencePlm(host, plmByVal(float(host.SpanEnd))) #elif ref == 'at custom value': # self.setReferencePlm(host, plmByVal(float(host.ReferenceValue))) #elif ref == 'first placement': # self.setReferencePlm(host, output[0]) #elif ref == 'last placement': # self.setReferencePlm(host, output[-1]) #else: # raise NotImplementedError("Reference option not implemented: " + ref) # return output
class PolarArray(APlm.AttachableFeature): """The Lattice Polar Array of placements""" def derivedInit(self, selfobj): super(PolarArray, self).derivedInit(selfobj) selfobj.addProperty( 'App::PropertyLength', 'Radius', "Polar Array", "Radius of the array (set to zero for just rotation).") selfobj.Radius = 3 selfobj.addProperty( 'App::PropertyEnumeration', 'UseArcRange', "Polar Array", "If attachment mode is concentric, supporting arc's range can be used as array's Span or Step." ) selfobj.UseArcRange = ['ignore', 'as Span', 'as Step'] selfobj.addProperty( 'App::PropertyBool', 'UseArcRadius', "Polar Array", "If True, and attachment mode is concentric, supporting arc's radius is used as array radius." ) selfobj.addProperty( 'App::PropertyEnumeration', 'OrientMode', "Polar Array", "Orientation of placements. Zero - aligns with origin. Static - aligns with self placement." ) selfobj.OrientMode = [ 'Zero', 'Static', 'Radial', 'Vortex', 'Centrifuge', 'Launchpad', 'Dominoes' ] selfobj.OrientMode = 'Radial' selfobj.addProperty('App::PropertyBool', 'Reverse', "Polar Array", "Reverses array direction.") selfobj.addProperty('App::PropertyBool', 'FlipX', "Polar Array", "Reverses x axis of every placement.") selfobj.addProperty('App::PropertyBool', 'FlipZ', "Polar Array", "Reverses z axis of every placement.") self.assureGenerator(selfobj) selfobj.ValuesSource = 'Generator' selfobj.SpanStart = 0 selfobj.SpanEnd = 360 selfobj.EndInclusive = False selfobj.Step = 55 selfobj.Count = 7 def assureGenerator(self, selfobj): '''Adds an instance of value series generator, if one doesn't exist yet.''' if hasattr(self, 'generator'): return self.generator = ValueSeriesGenerator(selfobj) self.generator.addProperties(groupname="Polar Array", groupname_gen="Lattice Series Generator", valuesdoc="List of angles, in degrees.", valuestype='App::PropertyFloat') self.updateReadonlyness(selfobj) def updateReadonlyness(self, selfobj): self.generator.updateReadonlyness() arc = self.fetchArc(selfobj) if self.isOnArc(selfobj) else None selfobj.setEditorMode('Radius', 1 if arc and selfobj.UseArcRadius else 0) self.generator.setPropertyWritable( 'SpanEnd', False if arc and selfobj.UseArcRange == 'as Span' else True) self.generator.setPropertyWritable( 'SpanStart', False if arc and selfobj.UseArcRange == 'as Span' else True) self.generator.setPropertyWritable( 'Step', False if arc and selfobj.UseArcRange == 'as Step' else True) def fetchArc(self, selfobj): """returns None, or tuple (arc_span, arc_radius)""" if selfobj.Support: lnkobj, sub = selfobj.Support[0] sub = sub[0] #resolve the link return fetchArc(lnkobj, sub) def derivedExecute(self, selfobj): self.assureGenerator(selfobj) self.updateReadonlyness(selfobj) selfobj.positionBySupport() # Apply links if (selfobj.UseArcRange != 'ignore' or selfobj.UseArcRadius) and self.isOnArc(selfobj): range, radius = self.fetchArc(selfobj) if selfobj.UseArcRange == 'as Span': selfobj.SpanStart = 0.0 selfobj.SpanEnd = range / turn * 360 elif selfobj.UseArcRange == 'as Step': selfobj.Step = range / turn * 360 if selfobj.UseArcRadius: selfobj.Radius = radius self.generator.execute() # cache properties into variables radius = float(selfobj.Radius) values = [float(strv) for strv in selfobj.Values] irot = selfobj.Placement.inverse().Rotation # compute internam placement, one behind OrientMode property baseplm = App.Placement() is_zero = selfobj.OrientMode == 'Zero' is_static = selfobj.OrientMode == 'Static' if is_zero or is_static: pass elif selfobj.OrientMode == 'Radial': baseplm = App.Placement() elif selfobj.OrientMode == 'Vortex': baseplm = App.Placement(V(), App.Rotation(V(0, 1, 0), V(), V(0, 0, 1))) elif selfobj.OrientMode == 'Centrifuge': baseplm = App.Placement(V(), App.Rotation(V(0, 1, 0), V(), V(-1, 0, 0))) elif selfobj.OrientMode == 'Launchpad': baseplm = App.Placement(V(), App.Rotation(V(0, 0, 1), V(), V(1, 0, 0))) elif selfobj.OrientMode == 'Dominoes': baseplm = App.Placement(V(), App.Rotation(V(0, 0, 1), V(), V(0, -1, 0))) else: raise NotImplementedError() flipX = selfobj.FlipX flipZ = selfobj.FlipZ flipY = flipX ^ flipZ flipplm = App.Placement( V(), App.Rotation(V(-1 if flipX else 1, 0, 0), V(0, -1 if flipY else 1, 0), V(0, 0, -1 if flipZ else 1))) baseplm = baseplm.multiply(flipplm) # Make the array on_arc = self.isOnArc(selfobj) angleplus = -90.0 if on_arc else 0.0 mm = -1.0 if selfobj.Reverse else +1.0 output = [] # list of placements for ang in values: localrot = App.Rotation(App.Vector(0, 0, 1), ang * mm + angleplus) localtransl = localrot.multVec(App.Vector(radius, 0, 0)) localplm = App.Placement(localtransl, localrot) resultplm = localplm.multiply(baseplm) if is_zero: resultplm.Rotation = irot resultplm = resultplm.multiply(flipplm) elif is_static: resultplm.Rotation = App.Rotation() resultplm = resultplm.multiply(flipplm) output.append(resultplm) return output def isOnArc(self, selfobj): return selfobj.MapMode == 'Concentric' and len( linkSubList_convertToOldStyle(selfobj.Support)) == 1 def onChanged(self, selfobj, propname): super(PolarArray, self).onChanged(selfobj, propname) if 'Restore' in selfobj.State: return if propname == 'Reverse' and self.isOnArc(selfobj): if selfobj.Reverse == True and abs(selfobj.MapPathParameter - 0.0) < ParaConfusion: selfobj.MapPathParameter = 1.0 elif selfobj.Reverse == False and abs(selfobj.MapPathParameter - 1.0) < ParaConfusion: selfobj.MapPathParameter = 0.0
class LinearArray(lattice2BaseFeature.LatticeFeature): "The Lattice LinearArray object" def derivedInit(self, obj): self.Type = "LatticeLinearArray" obj.addProperty("App::PropertyVector", "Dir", "Lattice Array", "Vector that defines axis direction") obj.Dir = App.Vector(1, 0, 0) obj.addProperty( "App::PropertyVector", "Point", "Lattice Array", "Position of base (the point through which the axis passes, and from which positions of elements are measured)" ) obj.addProperty("App::PropertyLink", "Link", "Lattice Array", "Link to the axis (Edge1 is used for the axis).") obj.addProperty("App::PropertyString", "LinkSubelement", "Lattice Array", "subelement to take from axis link shape") obj.addProperty("App::PropertyBool", "Reverse", "Lattice Array", "Set to true to reverse direction") obj.addProperty("App::PropertyBool", "DirIsDriven", "Lattice Array", "If True, Dir property is driven by link.") obj.DirIsDriven = True obj.addProperty( "App::PropertyBool", "PointIsDriven", "Lattice Array", "If True, AxisPoint is not updated based on the link.") obj.PointIsDriven = True obj.addProperty( "App::PropertyEnumeration", "DrivenProperty", "Lattice Array", "Select, which property is to be driven by length of axis link.") obj.DrivenProperty = ['None', 'Span', 'SpanStart', 'SpanEnd', 'Step'] obj.DrivenProperty = 'Span' obj.addProperty("App::PropertyEnumeration", "OrientMode", "Lattice Array", "Orientation of elements") obj.OrientMode = ['None', 'Along axis'] obj.OrientMode = 'Along axis' self.assureGenerator(obj) obj.ValuesSource = "Generator" obj.GeneratorMode = "StepN" obj.EndInclusive = True obj.SpanStart = 0.0 obj.SpanEnd = 12.0 obj.Step = 3.0 obj.Count = 5.0 self.assureProperties(obj) def updateReadonlyness(self, obj): link = screen(obj.Link) obj.setEditorMode("Dir", 1 if (link and obj.DirIsDriven) else 0) obj.setEditorMode("Point", 1 if (link and obj.PointIsDriven) else 0) obj.setEditorMode("DirIsDriven", 0 if link else 1) obj.setEditorMode("PointIsDriven", 0 if link else 1) obj.setEditorMode("DrivenProperty", 0 if link else 1) self.generator.updateReadonlyness() def assureGenerator(self, obj): '''Adds an instance of value series generator, if one doesn't exist yet.''' if hasattr(self, "generator"): return self.generator = ValueSeriesGenerator(obj) self.generator.addProperties( groupname="Lattice Array", groupname_gen="Lattice Series Generator", valuesdoc= "List of distances. Distance is measured from Point, along Dir, in millimeters.", valuestype="App::PropertyDistance") self.updateReadonlyness(obj) def assureProperties(self, selfobj): assureProperty( selfobj, "App::PropertyLinkSub", "SubLink", sublinkFromApart(screen(selfobj.Link), selfobj.LinkSubelement), "Lattice Array", "Mirror of Object+SubNames properties") def derivedExecute(self, obj): self.assureGenerator(obj) self.assureProperties(obj) self.updateReadonlyness(obj) # Apply links if screen(obj.Link): if lattice2BaseFeature.isObjectLattice(screen(obj.Link)): lattice2Executer.warning( obj, "For polar array, axis link is expected to be a regular shape. Lattice objct was supplied instead, it's going to be treated as a generic shape." ) #resolve the link if len(obj.LinkSubelement) > 0: linkedShape = screen(obj.Link).Shape.getElement( obj.LinkSubelement) else: linkedShape = screen(obj.Link).Shape #Type check if linkedShape.ShapeType != 'Edge': raise ValueError('Axis link must be an edge; it is ' + linkedShape.ShapeType + ' instead.') if type(linkedShape.Curve) is not Part.Line: raise ValueError('Axis link must be a line; it is ' + type(linkedShape.Curve) + ' instead.') #obtain start_point = linkedShape.valueAt(linkedShape.FirstParameter) end_point = linkedShape.valueAt(linkedShape.LastParameter) dir = end_point - start_point point = start_point if not obj.Reverse else end_point if obj.DirIsDriven: obj.Dir = dir if obj.PointIsDriven: obj.Point = point if obj.DrivenProperty != 'None': if obj.DrivenProperty == 'Span': propname = "SpanEnd" obj.SpanEnd = obj.SpanStart + App.Units.Quantity( 'mm') * dir.Length else: propname = obj.DrivenProperty setattr(obj, propname, dir.Length) if self.generator.isPropertyControlledByGenerator(propname): lattice2Executer.warning( obj, "Property " + propname + " is driven by both generator and link. Generator has priority." ) # Generate series of values self.generator.execute() values = [float(strv) for strv in obj.Values] #Apply reversal if obj.Reverse: obj.Dir = obj.Dir * (-1.0) if not (obj.DirIsDriven and screen(obj.Link)): obj.Reverse = False # precompute orientation if obj.OrientMode == 'Along axis': ori = lattice2GeomUtils.makeOrientationFromLocalAxes( ZAx=obj.Dir).multiply( lattice2GeomUtils.makeOrientationFromLocalAxes( ZAx=App.Vector(1, 0, 0), XAx=App.Vector(0, 0, 1))) else: ori = App.Rotation() dir = obj.Dir dir.normalize() # Make the array output = [] # list of placements for v in values: output.append(App.Placement(obj.Point + obj.Dir * v, ori)) return output def onChanged(self, selfobj, prop): #prop is a string - name of the property # synchronize SubLink and Object+SubNames properties syncSublinkApart(selfobj, prop, 'SubLink', 'Link', 'LinkSubelement') return lattice2BaseFeature.LatticeFeature.onChanged( self, selfobj, prop)
class PolarArray(lattice2BaseFeature.LatticeFeature): "The Lattice PolarArray object" def derivedInit(self,obj): self.Type = "LatticePolarArray" obj.addProperty("App::PropertyLength","Radius","Lattice Array","Radius of the array (set to zero for just rotation).") obj.Radius = 3 obj.addProperty("App::PropertyVector","AxisDir","Lattice Array","Vector that defines axis direction") obj.AxisDir = App.Vector(0,0,1) obj.addProperty("App::PropertyVector","AxisPoint","Lattice Array","Center of rotation") obj.addProperty("App::PropertyLink","AxisLink","Lattice Array","Link to the axis (Edge1 is used for the axis).") obj.addProperty("App::PropertyString","AxisLinkSubelement","Lattice Array","subelement to take from axis link shape") obj.addProperty("App::PropertyBool","AxisDirIsDriven","Lattice Array","If True, AxisDir is not updated based on the link.") obj.addProperty("App::PropertyBool","AxisPointIsDriven","Lattice Array","If True, AxisPoint is not updated based on the link.") obj.AxisDirIsDriven = True obj.AxisPointIsDriven = True obj.addProperty("App::PropertyEnumeration","OrientMode","Lattice Array","Orientation of elements") obj.OrientMode = ['None','Against axis'] obj.OrientMode = 'Against axis' self.assureGenerator(obj) obj.ValuesSource = "Generator" obj.SpanStart = 0 obj.SpanEnd = 360 obj.EndInclusive = False obj.Count = 5 def assureGenerator(self, obj): '''Adds an instance of value series generator, if one doesn't exist yet.''' if hasattr(self,"generator"): return self.generator = ValueSeriesGenerator(obj) self.generator.addProperties(groupname= "Lattice Array", groupname_gen= "Lattice Series Generator", valuesdoc= "List of angles, in degrees.", valuestype= "App::PropertyFloat") self.updateReadonlyness(obj) def updateReadonlyness(self, obj): obj.setEditorMode("AxisDir", 1 if (obj.AxisLink and obj.AxisDirIsDriven) else 0) obj.setEditorMode("AxisPoint", 1 if (obj.AxisLink and obj.AxisPointIsDriven) else 0) obj.setEditorMode("AxisDirIsDriven", 0 if obj.AxisLink else 1) obj.setEditorMode("AxisPointIsDriven", 0 if obj.AxisLink else 1) self.generator.updateReadonlyness() def derivedExecute(self,obj): self.assureGenerator(obj) self.updateReadonlyness(obj) # Apply links if obj.AxisLink: if lattice2BaseFeature.isObjectLattice(obj.AxisLink): lattice2Executer.warning(obj,"For polar array, axis link is expected to be a regular shape. Lattice objct was supplied instead, it's going to be treated as a generic shape.") #resolve the link if len(obj.AxisLinkSubelement) > 0: linkedShape = obj.AxisLink.Shape.getElement(obj.AxisLinkSubelement) else: linkedShape = obj.AxisLink.Shape #Type check if linkedShape.ShapeType != 'Edge': raise ValueError('Axis link must be an edge; it is '+linkedShape.ShapeType+' instead.') #prepare dir = App.Vector() point = App.Vector() if isinstance(linkedShape.Curve, Part.Line): dir = linkedShape.Curve.EndPoint - linkedShape.Curve.StartPoint point = linkedShape.Curve.StartPoint elif isinstance(linkedShape.Curve, Part.Circle): dir = linkedShape.Curve.Axis point = linkedShape.Curve.Center else: raise ValueError("Edge " + repr(linkedShape) + " can't be used to derive an axis. It must be either a line or a circle/arc.") #apply if obj.AxisDirIsDriven: obj.AxisDir = dir if obj.AxisPointIsDriven: obj.AxisPoint = point self.generator.execute() # cache properties into variables radius = float(obj.Radius) values = [float(strv) for strv in obj.Values] # compute initial vector. It is to be perpendicular to Axis rot_ini = lattice2GeomUtils.makeOrientationFromLocalAxes(ZAx= obj.AxisDir) overallPlacement = App.Placement(obj.AxisPoint, rot_ini) # Make the array output = [] # list of placements for ang in values: p = Part.Vertex() localrot = App.Rotation(App.Vector(0,0,1), ang) localtransl = localrot.multVec(App.Vector(radius,0,0)) localplm = App.Placement(localtransl, localrot) resultplm = overallPlacement.multiply(localplm) if obj.OrientMode == 'None': resultplm.Rotation = App.Rotation() output.append(resultplm) return output
class PolarArray(lattice2BaseFeature.LatticeFeature): "The Lattice PolarArray object" def derivedInit(self, obj): self.Type = "LatticePolarArray" obj.addProperty( "App::PropertyLength", "Radius", "Lattice Array", "Radius of the array (set to zero for just rotation).") obj.Radius = 3 obj.addProperty("App::PropertyVector", "AxisDir", "Lattice Array", "Vector that defines axis direction") obj.AxisDir = App.Vector(0, 0, 1) obj.addProperty("App::PropertyVector", "AxisPoint", "Lattice Array", "Center of rotation") obj.addProperty("App::PropertyLink", "AxisLink", "Lattice Array", "Link to the axis (Edge1 is used for the axis).") obj.addProperty("App::PropertyString", "AxisLinkSubelement", "Lattice Array", "subelement to take from axis link shape") obj.addProperty("App::PropertyBool", "AxisDirIsDriven", "Lattice Array", "If True, AxisDir is not updated based on the link.") obj.addProperty( "App::PropertyBool", "AxisPointIsDriven", "Lattice Array", "If True, AxisPoint is not updated based on the link.") obj.AxisDirIsDriven = True obj.AxisPointIsDriven = True obj.addProperty("App::PropertyEnumeration", "OrientMode", "Lattice Array", "Orientation of elements") obj.OrientMode = ['None', 'Against axis'] obj.OrientMode = 'Against axis' self.assureGenerator(obj) obj.ValuesSource = "Generator" obj.SpanStart = 0 obj.SpanEnd = 360 obj.EndInclusive = False obj.Count = 5 self.assureProperties(obj) def assureGenerator(self, obj): '''Adds an instance of value series generator, if one doesn't exist yet.''' if hasattr(self, "generator"): return self.generator = ValueSeriesGenerator(obj) self.generator.addProperties(groupname="Lattice Array", groupname_gen="Lattice Series Generator", valuesdoc="List of angles, in degrees.", valuestype="App::PropertyFloat") self.updateReadonlyness(obj) def updateReadonlyness(self, obj): axislink = screen(obj.AxisLink) obj.setEditorMode("AxisDir", 1 if (axislink and obj.AxisDirIsDriven) else 0) obj.setEditorMode("AxisPoint", 1 if (axislink and obj.AxisPointIsDriven) else 0) obj.setEditorMode("AxisDirIsDriven", 0 if axislink else 1) obj.setEditorMode("AxisPointIsDriven", 0 if axislink else 1) self.generator.updateReadonlyness() def assureProperties(self, selfobj): assureProperty( selfobj, "App::PropertyLinkSub", "AxisSubLink", sublinkFromApart(screen(selfobj.AxisLink), selfobj.AxisLinkSubelement), "Lattice Array", "Mirror of Object+SubNames properties") def derivedExecute(self, obj): self.assureGenerator(obj) self.assureProperties(obj) self.updateReadonlyness(obj) # Apply links if screen(obj.AxisLink): if lattice2BaseFeature.isObjectLattice(screen(obj.AxisLink)): lattice2Executer.warning( obj, "For polar array, axis link is expected to be a regular shape. Lattice objct was supplied instead, it's going to be treated as a generic shape." ) #resolve the link if len(obj.AxisLinkSubelement) > 0: linkedShape = screen(obj.AxisLink).Shape.getElement( obj.AxisLinkSubelement) else: linkedShape = screen(obj.AxisLink).Shape #Type check if linkedShape.ShapeType != 'Edge': raise ValueError('Axis link must be an edge; it is ' + linkedShape.ShapeType + ' instead.') #prepare dir = App.Vector() point = App.Vector() if isinstance(linkedShape.Curve, Part.Line): start_point = linkedShape.valueAt(linkedShape.FirstParameter) end_point = linkedShape.valueAt(linkedShape.LastParameter) dir = end_point - start_point point = start_point elif isinstance(linkedShape.Curve, Part.Circle): dir = linkedShape.Curve.Axis point = linkedShape.Curve.Center else: raise ValueError( "Edge " + repr(linkedShape) + " can't be used to derive an axis. It must be either a line or a circle/arc." ) #apply if obj.AxisDirIsDriven: obj.AxisDir = dir if obj.AxisPointIsDriven: obj.AxisPoint = point self.generator.execute() # cache properties into variables radius = float(obj.Radius) values = [float(strv) for strv in obj.Values] # compute initial vector. It is to be perpendicular to Axis rot_ini = lattice2GeomUtils.makeOrientationFromLocalAxes( ZAx=obj.AxisDir) overallPlacement = App.Placement(obj.AxisPoint, rot_ini) # Make the array output = [] # list of placements for ang in values: p = Part.Vertex() localrot = App.Rotation(App.Vector(0, 0, 1), ang) localtransl = localrot.multVec(App.Vector(radius, 0, 0)) localplm = App.Placement(localtransl, localrot) resultplm = overallPlacement.multiply(localplm) if obj.OrientMode == 'None': resultplm.Rotation = App.Rotation() output.append(resultplm) return output def onChanged(self, selfobj, prop): #prop is a string - name of the property # synchronize SubLink and Object+SubNames properties syncSublinkApart(selfobj, prop, 'AxisSubLink', 'AxisLink', 'AxisLinkSubelement') return lattice2BaseFeature.LatticeFeature.onChanged( self, selfobj, prop)
class LatticeParaSeries(lattice2BaseFeature.LatticeFeature): "The Lattice ParaSeries object" def derivedInit(self,obj): self.Type = "LatticeParaSeries" obj.addProperty("App::PropertyLink","Object","Lattice ParaSeries","Object to make series from. Can be any generic shape, as well as an array of placements.") obj.addProperty("App::PropertyEnumeration","ParameterType","Lattice ParaSeries","Data type of parameter to vary.") obj.ParameterType = ['float','int','string'] obj.addProperty("App::PropertyString","ParameterRef","Lattice ParaSeries","Reference to the parameter to vary. Syntax: ObjectName.Property. Examples: 'Box.Height'; 'Sketch.Constaints.myLength'.") obj.addProperty("App::PropertyEnumeration","Recomputing","Lattice ParaSeries","Sets recomputing policy.") obj.Recomputing = ["Disabled", "Recompute Once", "Enabled"] obj.Recomputing = "Disabled" # recomputing ParaSeries can be very long, so disable it by default self.assureGenerator(obj) def assureGenerator(self, obj): '''Adds an instance of value series generator, if one doesn't exist yet.''' if hasattr(self,"generator"): return self.generator = ValueSeriesGenerator(obj) self.generator.addProperties(groupname= "Lattice ParaSeries", groupname_gen= "Lattice ParaSeries Generator", valuesdoc= "List of parameter values to compute object for.") self.generator.updateReadonlyness() def derivedExecute(self,selfobj): # values generator should be functional even if recomputing is disabled, so do it first self.assureGenerator(selfobj) self.generator.updateReadonlyness() self.generator.execute() if selfobj.Recomputing == "Disabled": raise ValueError(selfobj.Name+": recomputing of this object is currently disabled. Modify 'Recomputing' property to enable it.") try: #test parameter references and read out their current values refstr = selfobj.ParameterRef #dict(selfobj.ExpressionEngine)["ParameterRef"] refstrs = refstr.replace(";","\t").split("\t") defvalues = [] for refstr in refstrs: refstr = refstr.strip(); val = None; try: val = getParameter(selfobj.Document,refstr) except Exception as err: App.Console.PrintError("{obj}: failed to read out parameter '{param}': {err}\n" .format(obj= selfobj.Name, param= refstr, err= err.message)) defvalues.append(val) N_params = len(defvalues) if N_params == 0: raise ValueError(selfobj.Name+": ParameterRef is not set. It is required.") #parse values values = [] for strrow in selfobj.Values: if len(strrow) == 0: break; row = strrow.split(";") row = [(strv.strip() if len(strv.strip())>0 else None) for strv in row] # clean out spaces and replace empty strings with None if len(row) < N_params: row += [None]*(N_params - len(row)) values.append(row) # convert values to type, filling in defaults where values are missing for row in values: for icol in range(N_params): strv = row[icol] val = None if strv is None: val = defvalues[icol] elif selfobj.ParameterType == 'float' or selfobj.ParameterType == 'int': val = float(strv.replace(",",".")) if selfobj.ParameterType == 'int': val = int(round(val)) elif selfobj.ParameterType == 'string': val = strv.strip() else: raise ValueError(selfobj.Name + ": ParameterType option not implemented: "+selfobj.ParameterType) row[icol] = val if len(values) == 0: scale = 1.0 try: if not selfobj.Object.Shape.isNull(): scale = selfobj.Object.Shape.BoundBox.DiagonalLength/math.sqrt(3) except Exception: pass if scale < DistConfusion * 100: scale = 1.0 selfobj.Shape = markers.getNullShapeShape(scale) raise ValueError(selfobj.Name + ": list of values is empty.") bGui = False #bool(App.GuiUp) #disabled temporarily, because it causes a crash if property edits are approved by hitting Enter if bGui: import PySide progress = PySide.QtGui.QProgressDialog(u"Recomputing "+selfobj.Label, u"Abort", 0, len(values)+1) progress.setModal(True) progress.show() doc1 = selfobj.Document doc2 = App.newDocument() object_in_doc2 = None # define the variable, to prevent del() in finally block from raising another error try: doc2.copyObject(selfobj.Object, True) #if there are nested paraseries in the dependencies, make sure to enable them for objd2 in doc2.Objects: if hasattr(objd2,"Recomputing"): try: objd2.Recomputing = "Enabled" objd2.purgeTouched() except exception: lattice2Executer.warning(selfobj,"Failed to enable recomputing of "+objd2.Name) object_in_doc2 = doc2.getObject(selfobj.Object.Name) if bGui: progress.setValue(1) output_shapes = [] for row in values: for icol in range(len(row)): setParameter(doc2, refstrs[icol].strip(), row[icol]) #recompute doc2.recompute() #get shape shape = None for obj in doc2.Objects: if 'Invalid' in obj.State: lattice2Executer.error(obj,"Recomputing shape for parameter value of "+repr(row)+" failed.") scale = 1.0 try: if not selfobj.Object.Shape.isNull(): scale = selfobj.Object.Shape.BoundBox.DiagonalLength/math.sqrt(3) except Exception: pass if scale < DistConfusion * 100: scale = 1.0 shape = markers.getNullShapeShape(scale) if shape is None: shape = object_in_doc2.Shape.copy() output_shapes.append(shape) #update progress if bGui: progress.setValue(progress.value()+1) if progress.wasCanceled(): raise lattice2Executer.CancelError() finally: #delete all references, before destroying the document. Probably not required, but to be sure... del(object_in_doc2) doc2_name = doc2.Name del(doc2) App.closeDocument(doc2_name) if bGui: progress.setValue(len(values)+1) selfobj.Shape = Part.makeCompound(output_shapes) output_is_lattice = lattice2BaseFeature.isObjectLattice(selfobj.Object) if 'Auto' in selfobj.isLattice: new_isLattice = 'Auto-On' if output_is_lattice else 'Auto-Off' if selfobj.isLattice != new_isLattice:#check, to not cause onChanged without necessity (onChange messes with colors, it's better to keep user color) selfobj.isLattice = new_isLattice finally: if selfobj.Recomputing == "Recompute Once": selfobj.Recomputing = "Disabled" return "suppress" # "suppress" disables most convenience code of lattice2BaseFeature. We do it because we build a nested array, which are not yet supported by lattice WB.
class LatticeParaSeries(lattice2BaseFeature.LatticeFeature): "The Lattice ParaSeries object" def derivedInit(self, obj): self.Type = "LatticeParaSeries" obj.addProperty( "App::PropertyLink", "Object", "Lattice ParaSeries", "Object to make series from. Can be any generic shape, as well as an array of placements." ) obj.addProperty("App::PropertyEnumeration", "ParameterType", "Lattice ParaSeries", "Data type of parameter to vary.") obj.ParameterType = ['float', 'int', 'string'] obj.addProperty( "App::PropertyString", "ParameterRef", "Lattice ParaSeries", "Reference to the parameter to vary. Syntax: ObjectName.Property. Examples: 'Box.Height'; 'Sketch.Constraints.myLength'." ) obj.addProperty("App::PropertyEnumeration", "Recomputing", "Lattice ParaSeries", "Sets recomputing policy.") obj.Recomputing = ["Disabled", "Recompute Once", "Enabled"] obj.Recomputing = "Disabled" # recomputing ParaSeries can be very long, so disable it by default self.assureGenerator(obj) def assureGenerator(self, obj): '''Adds an instance of value series generator, if one doesn't exist yet.''' if hasattr(self, "generator"): return self.generator = ValueSeriesGenerator(obj) self.generator.addProperties( groupname="Lattice ParaSeries", groupname_gen="Lattice ParaSeries Generator", valuesdoc="List of parameter values to compute object for.") self.generator.updateReadonlyness() def derivedExecute(self, selfobj): # values generator should be functional even if recomputing is disabled, so do it first self.assureGenerator(selfobj) self.generator.updateReadonlyness() self.generator.execute() if selfobj.Recomputing == "Disabled": raise ValueError( selfobj.Name + ": recomputing of this object is currently disabled. Modify 'Recomputing' property to enable it." ) try: #test parameter references and read out their current values refstr = selfobj.ParameterRef #dict(selfobj.ExpressionEngine)["ParameterRef"] refstrs = refstr.replace(";", "\t").split("\t") defvalues = [] for refstr in refstrs: refstr = refstr.strip() val = None try: val = getParameter(selfobj.Document, refstr) except Exception as err: App.Console.PrintError( "{obj}: failed to read out parameter '{param}': {err}\n" .format(obj=selfobj.Name, param=refstr, err=str(err))) defvalues.append(val) N_params = len(defvalues) if N_params == 0: raise ValueError(selfobj.Name + ": ParameterRef is not set. It is required.") #parse values values = [] for strrow in selfobj.Values: if len(strrow) == 0: break row = strrow.split(";") row = [ (strv.strip() if len(strv.strip()) > 0 else None) for strv in row ] # clean out spaces and replace empty strings with None if len(row) < N_params: row += [None] * (N_params - len(row)) values.append(row) # convert values to type, filling in defaults where values are missing for row in values: for icol in range(N_params): strv = row[icol] val = None if strv is None: val = defvalues[icol] elif selfobj.ParameterType == 'float' or selfobj.ParameterType == 'int': val = float(strv.replace(",", ".")) if selfobj.ParameterType == 'int': val = int(round(val)) elif selfobj.ParameterType == 'string': val = strv.strip() else: raise ValueError( selfobj.Name + ": ParameterType option not implemented: " + selfobj.ParameterType) row[icol] = val if len(values) == 0: scale = 1.0 try: if not screen(selfobj.Object).Shape.isNull(): scale = screen( selfobj.Object ).Shape.BoundBox.DiagonalLength / math.sqrt(3) except Exception: pass if scale < DistConfusion * 100: scale = 1.0 selfobj.Shape = markers.getNullShapeShape(scale) raise ValueError(selfobj.Name + ": list of values is empty.") bGui = False #bool(App.GuiUp) #disabled temporarily, because it causes a crash if property edits are approved by hitting Enter if bGui: import PySide progress = PySide.QtGui.QProgressDialog( u"Recomputing " + selfobj.Label, u"Abort", 0, len(values) + 1) progress.setModal(True) progress.show() doc1 = selfobj.Document doc2 = App.newDocument( ) #create temporary doc to do the computations # assign doc's filename before copying objects, otherwise we get errors with xlinks try: doc2.FileName = doc1.FileName except Exception as err: pass #in old FreeCADs, FileName property is read-only, we can safely ignore that object_in_doc2 = None # define the variable, to prevent del() in finally block from raising another error try: doc2.copyObject(screen(selfobj.Object), True) #if there are nested paraseries in the dependencies, make sure to enable them for objd2 in doc2.Objects: if hasattr(objd2, "Recomputing"): try: objd2.Recomputing = "Enabled" objd2.purgeTouched() except exception: lattice2Executer.warning( selfobj, "Failed to enable recomputing of " + objd2.Name) object_in_doc2 = doc2.getObject(screen(selfobj.Object).Name) if bGui: progress.setValue(1) output_shapes = [] for row in values: for icol in range(len(row)): setParameter(doc2, refstrs[icol].strip(), row[icol]) #recompute doc2.recompute() #get shape shape = None for obj in doc2.Objects: if 'Invalid' in obj.State: lattice2Executer.error( obj, "Recomputing shape for parameter value of " + repr(row) + " failed.") scale = 1.0 try: if not screen(selfobj.Object).Shape.isNull(): scale = screen( selfobj.Object ).Shape.BoundBox.DiagonalLength / math.sqrt( 3) except Exception: pass if scale < DistConfusion * 100: scale = 1.0 shape = markers.getNullShapeShape(scale) if shape is None: shape = object_in_doc2.Shape.copy() output_shapes.append(shape) #update progress if bGui: progress.setValue(progress.value() + 1) if progress.wasCanceled(): raise lattice2Executer.CancelError() finally: #delete all references, before destroying the document. Probably not required, but to be sure... del (object_in_doc2) doc2_name = doc2.Name del (doc2) App.closeDocument(doc2_name) if bGui: progress.setValue(len(values) + 1) selfobj.Shape = Part.makeCompound(output_shapes) output_is_lattice = lattice2BaseFeature.isObjectLattice( screen(selfobj.Object)) if 'Auto' in selfobj.isLattice: new_isLattice = 'Auto-On' if output_is_lattice else 'Auto-Off' if selfobj.isLattice != new_isLattice: #check, to not cause onChanged without necessity (onChange messes with colors, it's better to keep user color) selfobj.isLattice = new_isLattice finally: if selfobj.Recomputing == "Recompute Once": selfobj.Recomputing = "Disabled" return "suppress" # "suppress" disables most convenience code of lattice2BaseFeature. We do it because we build a nested array, which are not yet supported by lattice WB.
class LinearArray(lattice2BaseFeature.LatticeFeature): "The Lattice LinearArray object" def derivedInit(self,obj): self.Type = "LatticeLinearArray" obj.addProperty("App::PropertyVector","Dir","Lattice Array","Vector that defines axis direction") obj.Dir = App.Vector(1,0,0) obj.addProperty("App::PropertyVector","Point","Lattice Array","Position of base (the point through which the axis passes, and from which positions of elements are measured)") obj.addProperty("App::PropertyLink","Link","Lattice Array","Link to the axis (Edge1 is used for the axis).") obj.addProperty("App::PropertyString","LinkSubelement","Lattice Array","subelement to take from axis link shape") obj.addProperty("App::PropertyBool","Reverse","Lattice Array","Set to true to reverse direction") obj.addProperty("App::PropertyBool","DirIsDriven","Lattice Array","If True, Dir property is driven by link.") obj.DirIsDriven = True obj.addProperty("App::PropertyBool","PointIsDriven","Lattice Array","If True, AxisPoint is not updated based on the link.") obj.PointIsDriven = True obj.addProperty("App::PropertyEnumeration","DrivenProperty","Lattice Array","Select, which property is to be driven by length of axis link.") obj.DrivenProperty = ['None','Span','SpanStart','SpanEnd','Step'] obj.DrivenProperty = 'Span' obj.addProperty("App::PropertyEnumeration","OrientMode","Lattice Array","Orientation of elements") obj.OrientMode = ['None','Along axis'] obj.OrientMode = 'Along axis' self.assureGenerator(obj) obj.ValuesSource = "Generator" obj.GeneratorMode = "StepN" obj.EndInclusive = True obj.SpanStart = 0.0 obj.SpanEnd = 12.0 obj.Step = 3.0 obj.Count = 5.0 def updateReadonlyness(self, obj): obj.setEditorMode("Dir", 1 if (obj.Link and obj.DirIsDriven) else 0) obj.setEditorMode("Point", 1 if (obj.Link and obj.PointIsDriven) else 0) obj.setEditorMode("DirIsDriven", 0 if obj.Link else 1) obj.setEditorMode("PointIsDriven", 0 if obj.Link else 1) obj.setEditorMode("DrivenProperty", 0 if obj.Link else 1) self.generator.updateReadonlyness() def assureGenerator(self, obj): '''Adds an instance of value series generator, if one doesn't exist yet.''' if hasattr(self,"generator"): return self.generator = ValueSeriesGenerator(obj) self.generator.addProperties(groupname= "Lattice Array", groupname_gen= "Lattice Series Generator", valuesdoc= "List of distances. Distance is measured from Point, along Dir, in millimeters.", valuestype= "App::PropertyDistance") self.updateReadonlyness(obj) def derivedExecute(self,obj): self.assureGenerator(obj) self.updateReadonlyness(obj) # Apply links if obj.Link: if lattice2BaseFeature.isObjectLattice(obj.Link): lattice2Executer.warning(obj,"For polar array, axis link is expected to be a regular shape. Lattice objct was supplied instead, it's going to be treated as a generic shape.") #resolve the link if len(obj.LinkSubelement) > 0: linkedShape = obj.Link.Shape.getElement(obj.LinkSubelement) else: linkedShape = obj.Link.Shape #Type check if linkedShape.ShapeType != 'Edge': raise ValueError('Axis link must be an edge; it is '+linkedShape.ShapeType+' instead.') if type(linkedShape.Curve) is not Part.Line: raise ValueError('Axis link must be a line; it is '+type(linkedShape.Curve)+' instead.') #obtain dir = linkedShape.Curve.EndPoint - linkedShape.Curve.StartPoint point = linkedShape.Curve.StartPoint if not obj.Reverse else linkedShape.Curve.EndPoint if obj.DirIsDriven: obj.Dir = dir if obj.PointIsDriven: obj.Point = point if obj.DrivenProperty != 'None': if obj.DrivenProperty == 'Span': propname = "SpanEnd" obj.SpanEnd = obj.SpanStart + App.Units.Quantity('mm')*dir.Length else: propname = obj.DrivenProperty setattr(obj, propname, dir.Length) if self.generator.isPropertyControlledByGenerator(propname): lattice2Executer.warning(obj, "Property "+propname+" is driven by both generator and link. Generator has priority.") # Generate series of values self.generator.execute() values = [float(strv) for strv in obj.Values] #Apply reversal if obj.Reverse: obj.Dir = obj.Dir*(-1.0) if not(obj.DirIsDriven and obj.Link): obj.Reverse = False # precompute orientation if obj.OrientMode == 'Along axis': ori = lattice2GeomUtils.makeOrientationFromLocalAxes(ZAx= obj.Dir).multiply( lattice2GeomUtils.makeOrientationFromLocalAxes(ZAx= App.Vector(1,0,0), XAx= App.Vector(0,0,1)) ) else: ori = App.Rotation() dir = obj.Dir dir.normalize() # Make the array output = [] # list of placements for v in values: output.append( App.Placement(obj.Point + obj.Dir*v, ori) ) return output