diff --git a/lattice2Base/Autosize.py b/lattice2Base/Autosize.py new file mode 100644 index 0000000..3637b41 --- /dev/null +++ b/lattice2Base/Autosize.py @@ -0,0 +1,205 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 - Victor Titov (DeepSOIC) * +#* * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* This program 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 program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +__title__= "Lattice2 Autosize module" +__author__ = "DeepSOIC" +__url__ = "" +__doc__ = ( +"""helper module for Lattice add-on workbench for FreeCAD. Routines used to guess sizes for primitives. +""" +) + +from . import Rounder +from . import Containers + +import FreeCAD as App +import math +from math import radians + + +def convenientModelWidth(): + """convenientModelWidth(): returns a size that will conveniently fit in the width of screen""" + return Autosize().convenientModelWidth() +def convenientModelSize(): + """convenientModelSize(): returns a size of a box that will conveniently fit in the screen""" + return Autosize().convenientModelSize() +def minimalSize(): + """minimalSize(): returns a size that will be barely recognizable on the screen (it is a rounded pick radius in model space)""" + return Autosize().minimalSize() +def convenientMarkerSize(): + """convenientMarkerSize(): size of object to be able to comfortably select faces""" + return Autosize().convenientMarkerSize() +def convenientFeatureSize(): + """convenientFeatureSize(): size in between marker size and model size. Should be reasonable to edit, but not fill the whole screen.""" + return Autosize().convenientFeatureSize() +def convenientPosition(): + return Autosize().convenientPosition() + +def getLocalOriginPosition(): + ac = Containers.activeContainer() + if ac is None: + return App.Vector() + elif ac.isDerivedFrom('App::Document'): #special case for v0.16 + return App.Vector() + else: + return Containers.Container(ac).getFullTransform().Base + +class ViewportInfo(object): + camera_type = 'perspective' #string: perspective or orthographic + camera_placement = App.Placement(App.Vector(0,0,1), App.Rotation()) + camera_focalplacement = App.Placement() + camera_focaldist = 1.0 + camera_heightangle = radians(60) #total horizontal view angle, in radians (for perspective camera) + camera_height = 1 #screen height in model space (mm), for orthographic camera + viewport_size_px = (1800,1000) #width, height of viewport, in pixels + viewport_size_mm = (1.8,1.0) #width, height of viewport (on focal plane), in mm + mm_per_px = 1.0 / 1000.0 #rough mm-to-pixel ratio (accurate on focal plane) + false_viewport = False # if true, no actual viewport was queried (e.g. non-gui mode, or activeview is non-3d) + + pickradius_px = 5 + pickradius_mm = 0.1 + + def __init__(self, viewer = None): + try: + if not(App.GuiUp or viewer is not None): + return + + if viewer == None: + import FreeCADGui as Gui + viewer = Gui.ActiveDocument.ActiveView + + if not hasattr(viewer, 'getCameraNode'): + return + + import pivy + cam = viewer.getCameraNode() + self.camera_type = 'perspective' if isinstance(cam, pivy.coin.SoPerspectiveCamera) else 'orthographic' + self.camera_placement = App.Placement( + App.Vector(cam.position.getValue().getValue()), + App.Rotation(*cam.orientation.getValue().getValue()) + ) + self.camera_focaldist = cam.focalDistance.getValue() + self.camera_focalplacement = self.camera_placement.multiply(App.Placement(App.Vector(0,0,-self.camera_focaldist), App.Rotation())) + + if self.camera_type == 'perspective': + self.camera_heightangle = cam.heightAngle.getValue() + self.camera_height = math.tan(self.camera_heightangle / 2) * self.camera_focaldist * 2 + else: + self.camera_height = cam.height.getValue() + + self.false_viewport = False + + rman = viewer.getViewer().getSoRenderManager() + self.viewport_size_px = tuple(rman.getWindowSize()) + + mmppx = self.camera_height/self.viewport_size_px[1] + self.mm_per_px = mmppx + + self.viewport_size_mm = (self.viewport_size_px[0]*mmppx, self.viewport_size_px[1]*mmppx) + + self.pickradius_px = App.ParamGet("User parameter:BaseApp/Preferences/View").GetFloat("PickRadius", 5.0) + self.pickradius_mm = self.pickradius_px * mmppx + + except Exception as err: + import traceback + tb = traceback.format_exc() + App.Console.PrintError("Lattice Autosize: failed to query viewport: {err}\n{tb}\n\n".format(err= str(err), tb= tb)) + +class Autosize(ViewportInfo): + convenient_model_size_multiplier = 0.4 + def __init__(self, viewer = None): + super(Autosize, self).__init__(viewer) + + def convenientModelWidth(self): + """convenientModelWidth(): returns a size that will conveniently fit in the width of screen""" + return Rounder.roundToNiceValue(self._convenientModelWidth()) + def convenientModelSize(self): + """convenientModelSize(): returns a size of a box that will conveniently fit in the screen""" + return Rounder.roundToNiceValue(self._convenientModelSize()) + def minimalSize(self): + """minimalSize(): returns a size that will be barely recognizable on the screen (it is a rounded pick radius in model space)""" + return Rounder.roundToNiceValue(self._minimalSize()) + def convenientMarkerSize(self): + """convenientMarkerSize(): size of object to be able to comfortably select faces""" + return Rounder.roundToNiceValue(self._convenientMarkerSize()) + def convenientFeatureSize(self): + """convenientFeatureSize(): size in between marker size and model size. Should be reasonable to edit, but not fill the whole screen.""" + return Rounder.roundToNiceValue(self._convenientFeatureSize()) + + def convenientPosition(self): + if self.isPointInWorkingArea(getLocalOriginPosition()): + return App.Vector() + else: + roundfocal = Rounder.roundToNiceValue(self.camera_focaldist*0.5) + result = App.Vector( + [Rounder.roundToPrecision(coord, roundfocal) for coord in tuple(self.camera_focalplacement.Base)] + ) + print result + return result + + def _convenientModelWidth(self): + if self.false_viewport: + return 10.0 + else: + return self.viewport_size_mm[0] * self.convenient_model_size_multiplier + def _convenientModelSize(self): + """_convenientMarkerSize(): (unrounded) returns size of an object that would fill most of the working area""" + if self.false_viewport: + return 10.0 + else: + return min(self.viewport_size_mm[0], self.viewport_size_mm[1]) * self.convenient_model_size_multiplier + + def _minimalSize(self): + """_minimalSize(): (unrounded) returns minimum object size that can be seen on screen on focal plane""" + if self.false_viewport: + return 0.1 + else: + return self.pickradius_mm + + def _convenientMarkerSize(self): + """_convenientMarkerSize(): (unrounded) returns maker size that is usefully large to select faces and understand its rotation""" + if self is None: + self = ViewportInfo() + if self.false_viewport: + return 1.0 + else: + return self.pickradius_mm * 10 + + def _convenientFeatureSize(self): + """_convenientMarkerSize(): (unrounded) returns size of an object that would fill most of the working area""" + if self is None: + self = ViewportInfo() + return math.sqrt(self._convenientModelSize() * self._convenientMarkerSize()) + + def isPointInWorkingArea(self, point = App.Vector()): + """isPointInWorkingArea(): returns True if point is not far from the visible area of focal plane. Point should be given in document coordinate system.""" + p_foc = self.camera_focalplacement.inverse().multVec(point) + #p_foc is point in focal-plane CS. X and Y are along focal plane. Z is against view direction (positive = towards the camera). + msize = self._convenientModelSize() + mwidth = self._convenientModelWidth() + f = self.camera_focaldist + if abs(p_foc.x) > mwidth*0.5 or abs(p_foc.y) > msize*0.5 or p_foc.z > f*0.5 or p_foc.z < -2*f: + return False + else: + return True + diff --git a/lattice2Base/Containers.py b/lattice2Base/Containers.py new file mode 100644 index 0000000..0c3cb6c --- /dev/null +++ b/lattice2Base/Containers.py @@ -0,0 +1,278 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2018 * +# * * +# * 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 * +# * * +# ***************************************************************************/ + +#This is a temporary replacement for C++-powered Container class that should be eventually introduced into FreeCAD + +import FreeCAD as App + +class Container(object): + """Container class: a unified interface for container objects, such as Group, Part, Body, or Document. + This is a temporary implementation.""" + Object = None #DocumentObject or Document, the actual container + + def __init__(self, obj): + self.Object = obj + + def self_check(self): + if self.Object is None: + raise ValueError("Null!") + if not isAContainer(self.Object): + raise NotAContainerError(self.Object) + + def getAllChildren(self): + """Returns all objects directly contained by the container. all = static + dynamic.""" + return self.getStaticChildren() + self.getDynamicChildren() + + def getStaticChildren(self): + """Returns children tightly bound to the container, such as Origin. The key thing + about them is that they are not supposed to be removed or added from/to the container.""" + + self.self_check() + container = self.Object + if container.isDerivedFrom('App::Document'): + return [] + elif container.hasExtension('App::OriginGroupExtension'): + if container.Origin is not None: + return [container.Origin] + else: + return [] + elif container.isDerivedFrom('App::Origin'): + return container.OriginFeatures + elif container.hasExtension('App::GroupExtension'): + return [] + raise RuntimeError("getStaticChildren: unexpected container type!") + + def getDynamicChildren(self): + """Returns dynamic children, i.e. the stuff that can be removed from the container.""" + self.self_check() + container = self.Object + + if container.isDerivedFrom('App::Document'): + # find all objects not contained by any Part or Body + result = set(container.Objects) + for obj in container.Objects: + if isAContainer(obj): + children = set(Container(obj).getAllChildren()) + result = result - children + return list(result) + elif container.hasExtension('App::GroupExtension'): + result = container.Group + if container.hasExtension('App::GeoFeatureGroupExtension'): + #geofeaturegroup's group contains all objects within the CS, we don't want that + result = [obj for obj in result if obj.getParentGroup() is not container] + return result + elif container.isDerivedFrom('App::Origin'): + return [] + raise RuntimeError("getDynamicChildren: unexpected container type!") + + def isACS(self): + """isACS(): returns true if the container forms internal coordinate system.""" + self.self_check() + container = self.Object + + if container.isDerivedFrom('App::Document'): + return True #Document is a special thing... is it a CS or not is a matter of coding convenience. + elif container.hasExtension('App::GeoFeatureGroupExtension'): + return True + else: + return False + + def isAVisGroup(self): + """isAVisGroup(): returns True if the container consumes viewproviders of children, and thus affects their visibility.""" + self.self_check() + container = self.Object + + if container.isDerivedFrom('App::Document'): + return True #Document is a special thing... Return value is a matter of coding convenience. + elif container.hasExtension('App::GeoFeatureGroupExtension'): + return True + elif container.isDerivedFrom('App::Origin'): + return True + else: + return False + + def getCSChildren(self): + if not self.isACS(): + raise TypeError("Container is not a coordinate system") + container = self.Object + return _getMetacontainerChildren(self, Container.isACS) + + def getVisGroupChildren(self): + if not self.isAVisGroup(): + raise TypeError("Container is not a visibility group") + container = self.Object + return _getMetacontainerChildren(self, Container.isAVisGroup) + + def hasObject(self, obj): + """Returns True if the container contains specified object directly.""" + return obj in self.getAllChildren() + + def hasObjectRecursive(self, obj): + return self.Object in ContainerChain(obj) + + def Placement(self): + if self.isACS(): + if hasattr(self.Object, 'Placement'): + return self.Object.Placement + return App.Placement() + + def getFullTransform(self): + """getFullTransform(): returns Placement that converts coordinates of objects in this container to global CS.""" + chain = ContainerChain(self.Object) + [self.Object] + plm = App.Placement() + for cnt in chain: + plm = plm.multiply(Container(cnt).Placement()) + return plm + +def _getMetacontainerChildren(container, isrightcontainer_func): + """Gathers up children of metacontainer - a container structure formed by containers of specific type. + For example, coordinate systems form a kind of container structure. + + container: instance of Container class + isrightcontainer_func: a function f(cnt)->bool, where cnt is a Container object.""" + + result = [] + list_traversing_now = [container] #list of Container instances + list_to_be_traversed_next = [] #list of Container instances + visited_containers = set([container.Object]) #set of DocumentObjects + + while len(list_traversing_now) > 0: + list_to_be_traversed_next = [] + for itcnt in list_traversing_now: + children = itcnt.getAllChildren() + result.extend(children) + for child in children: + if isAContainer(child): + newcnt = Container(child) + if not isrightcontainer_func(newcnt): + list_to_be_traversed_next.append(newcnt) + list_traversing_now = list_to_be_traversed_next + + return result + + + +def isAContainer(obj): + '''isAContainer(obj): returns True if obj is an object container, such as + Group, Part, Body. The important characterisic of an object being a + container is that it can be activated to receive new objects. Documents + are considered containers, too.''' + + if obj.isDerivedFrom('App::Document'): + return True + if obj.hasExtension('App::GroupExtension'): + return True + if obj.isDerivedFrom('App::Origin'): + return True + return False + +#from Part-o-magic... +def ContainerOf(obj): + """ContainerOf(obj): returns the container that immediately has obj.""" + cnt = None + for dep in obj.InList: + if isAContainer(dep): + if Container(dep).hasObject(obj): + if cnt is not None and dep is not cnt: + raise ContainerTreeError("Container tree is not a tree") + cnt = dep + if cnt is None: + return obj.Document + return cnt + +def getVisGroupOf(obj): + chain = VisGroupChain(obj) + return chain[-1] + +#from Part-o-magic... over-engineered, but proven to work +def ContainerChain(feat): + '''ContainerChain(feat): container path to feat (not including feat itself). + Last container directly contains the feature. + Example of output: [,,,]''' + + if feat.isDerivedFrom('App::Document'): + return [] + + list_traversing_now = [feat] + set_of_deps = set() + list_of_deps = [] + + while len(list_traversing_now) > 0: + list_to_be_traversed_next = [] + for feat in list_traversing_now: + for dep in feat.InList: + if isAContainer(dep) and Container(dep).hasObject(feat): + if not (dep in set_of_deps): + set_of_deps.add(dep) + list_of_deps.append(dep) + list_to_be_traversed_next.append(dep) + if len(list_to_be_traversed_next) > 1: + raise ContainerTreeError("Container tree is not a tree") + list_traversing_now = list_to_be_traversed_next + + return [feat.Document] + list_of_deps[::-1] + +def CSChain(feat): + cnt_chain = ContainerChain(feat) + return [cnt for cnt in cnt_chain if Container(cnt).isACS()] + +def VisGroupChain(feat): + cnt_chain = ContainerChain(feat) + return [cnt for cnt in cnt_chain if Container(cnt).isAVisGroup()] + +def activeContainer(): + '''activeContainer(): returns active container. + If there is an active body, it is returned as active container. ActivePart is ignored. + If there is no active body, active Part is returned. + If there is no active Part either, active Document is returned. + If no active document, None is returned.''' + import FreeCAD as App + import FreeCADGui as Gui + + if hasattr(App, "ActiveContainer"): + return App.ActiveContainer.Object + + if Gui.ActiveDocument is None: + return None + vw = Gui.ActiveDocument.ActiveView + if vw is None: + return None + if not hasattr(vw, 'getActiveObject'): #v0.16 + return App.ActiveDocument + activeBody = vw.getActiveObject("pdbody") + activePart = vw.getActiveObject("part") + if activeBody: + return activeBody + elif activePart: + return activePart + else: + return App.ActiveDocument + + +class ContainerError(RuntimeError): + pass +class NotAContainerError(ContainerError): + def __init__(self): + ContainerError.__init__(self, u"{obj} is not recognized as container".format(obj.Name)) +class ContainerTreeError(ContainerError): + pass diff --git a/lattice2Base/Rounder.py b/lattice2Base/Rounder.py new file mode 100644 index 0000000..2baf235 --- /dev/null +++ b/lattice2Base/Rounder.py @@ -0,0 +1,56 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 - Victor Titov (DeepSOIC) * +#* * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* This program 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 program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +__title__= "Lattice2 rounder module" +__author__ = "DeepSOIC" +__url__ = "" +__doc__ = "helper module for Lattice add-on workbench for FreeCAD. Provides special rounding routines." + +import math +from math import log + +nice_numbers = [1.0, 2.0, 5.0] +nice_magnitudes = [] +for degree in range(-3,6): + order = 10.0 ** degree + nice_magnitudes.extend([order * val for val in nice_numbers]) + +def roundToNiceValue(value, nice_value_list = nice_magnitudes): + if value == 0.0: + return 0.0 + + bestmatch_logdist = log(1e10) + bestmatch = None + + for nice_val in nice_value_list: + logdist = abs(log(abs(value)) - log(nice_val)) + if logdist < bestmatch_logdist: + bestmatch_logdist = logdist + bestmatch = nice_val + + return math.copysign(bestmatch, value) + +def roundToPrecision(value, precision): + if precision < 1e-12: + return value + return round(value/precision)*precision \ No newline at end of file diff --git a/lattice2Base/__init__.py b/lattice2Base/__init__.py new file mode 100644 index 0000000..e69de29