308 lines
11 KiB
Python
308 lines
11 KiB
Python
#***************************************************************************
|
|
#* *
|
|
#* Copyright (c) 2011, 2016 *
|
|
#* Jose Luis Cercos Pita <jlcercos@gmail.com> *
|
|
#* *
|
|
#* 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 *
|
|
#* *
|
|
#***************************************************************************
|
|
|
|
import time
|
|
from math import *
|
|
from PySide import QtGui, QtCore
|
|
import FreeCAD as App
|
|
import FreeCADGui as Gui
|
|
from FreeCAD import Base, Vector, Matrix, Placement, Rotation
|
|
import Part
|
|
import Units
|
|
from shipUtils import Paths, Math
|
|
import shipUtils.Units as USys
|
|
|
|
|
|
COMMON_BOOLEAN_ITERATIONS = 10
|
|
|
|
|
|
class Tank:
|
|
def __init__(self, obj, shapes, ship):
|
|
""" Transform a generic object to a ship instance.
|
|
|
|
Keyword arguments:
|
|
obj -- Part::FeaturePython created object which should be transformed
|
|
in a weight instance.
|
|
shapes -- Set of solid shapes which will compound the tank.
|
|
ship -- Ship where the tank is allocated.
|
|
"""
|
|
# Add an unique property to identify the Weight instances
|
|
tooltip = str(QtGui.QApplication.translate(
|
|
"ship_tank",
|
|
"True if it is a valid tank instance, False otherwise",
|
|
None,
|
|
QtGui.QApplication.UnicodeUTF8))
|
|
obj.addProperty("App::PropertyBool",
|
|
"IsTank",
|
|
"Tank",
|
|
tooltip).IsTank = True
|
|
# Set the subshapes
|
|
obj.Shape = Part.makeCompound(shapes)
|
|
|
|
obj.Proxy = self
|
|
|
|
def onChanged(self, fp, prop):
|
|
"""Detects the ship data changes.
|
|
|
|
Keyword arguments:
|
|
fp -- Part::FeaturePython object affected.
|
|
prop -- Modified property name.
|
|
"""
|
|
if prop == "Vol":
|
|
pass
|
|
|
|
def execute(self, fp):
|
|
"""Detects the entity recomputations.
|
|
|
|
Keyword arguments:
|
|
fp -- Part::FeaturePython object affected.
|
|
"""
|
|
pass
|
|
|
|
def getVolume(self, fp, level, return_shape=False):
|
|
"""Return the fluid volume inside the tank, provided the filling level.
|
|
|
|
Keyword arguments:
|
|
fp -- Part::FeaturePython object affected.
|
|
level -- Percentage of filling level (interval [0, 1]).
|
|
return_shape -- False if the tool should return the fluid volume value,
|
|
True if the tool should return the volume shape.
|
|
"""
|
|
if level <= 0.0:
|
|
if return_shape:
|
|
return Part.Vertex()
|
|
return Units.Quantity(0.0, Units.Volume)
|
|
if level >= 1.0:
|
|
if return_shape:
|
|
return fp.Shape.copy()
|
|
return Units.Quantity(fp.Shape.Volume, Units.Volume)
|
|
|
|
# Build up the cutting box
|
|
bbox = fp.Shape.BoundBox
|
|
dx = bbox.XMax - bbox.XMin
|
|
dy = bbox.YMax - bbox.YMin
|
|
dz = bbox.ZMax - bbox.ZMin
|
|
|
|
box = App.ActiveDocument.addObject("Part::Box","Box")
|
|
length_format = USys.getLengthFormat()
|
|
box.Placement = Placement(Vector(bbox.XMin - dx,
|
|
bbox.YMin - dy,
|
|
bbox.ZMin - dz),
|
|
Rotation(App.Vector(0,0,1),0))
|
|
box.Length = length_format.format(3.0 * dx)
|
|
box.Width = length_format.format(3.0 * dy)
|
|
box.Height = length_format.format((1.0 + level) * dz)
|
|
|
|
# Create a new object on top of a copy of the tank shape
|
|
Part.show(fp.Shape.copy())
|
|
tank = App.ActiveDocument.Objects[-1]
|
|
|
|
# Compute the common boolean operation
|
|
App.ActiveDocument.recompute()
|
|
common = App.activeDocument().addObject("Part::MultiCommon",
|
|
"TankVolHelper")
|
|
common.Shapes = [tank, box]
|
|
App.ActiveDocument.recompute()
|
|
if len(common.Shape.Solids) == 0:
|
|
# The common operation is failing, let's try moving a bit the free
|
|
# surface
|
|
msg = QtGui.QApplication.translate(
|
|
"ship_console",
|
|
"Tank volume operation failed. The tool is retrying that"
|
|
" slightly moving the free surface position",
|
|
None,
|
|
QtGui.QApplication.UnicodeUTF8)
|
|
App.Console.PrintWarning(msg + '\n')
|
|
rand_bounds = 0.01 * dz
|
|
i = 0
|
|
while len(common.Shape.Solids) == 0 and i < COMMON_BOOLEAN_ITERATIONS:
|
|
i += 1
|
|
box.Height = length_format.format(
|
|
(1.0 + level) * dz + random.uniform(-random_bounds,
|
|
random_bounds))
|
|
App.ActiveDocument.recompute()
|
|
|
|
if return_shape:
|
|
ret_value = common.Shape.copy()
|
|
else:
|
|
ret_value = Units.Quantity(common.Shape.Volume, Units.Volume)
|
|
|
|
App.ActiveDocument.removeObject(common.Name)
|
|
App.ActiveDocument.removeObject(tank.Name)
|
|
App.ActiveDocument.removeObject(box.Name)
|
|
App.ActiveDocument.recompute()
|
|
|
|
return ret_value
|
|
|
|
def getCoG(self, fp, vol, roll=Units.parseQuantity("0 deg"),
|
|
trim=Units.parseQuantity("0 deg")):
|
|
"""Return the fluid volume center of gravity, provided the volume of
|
|
fluid inside the tank.
|
|
|
|
The returned center of gravity is refered to the untransformed ship.
|
|
|
|
Keyword arguments:
|
|
fp -- Part::FeaturePython object affected.
|
|
vol -- Volume of fluid.
|
|
roll -- Ship roll angle.
|
|
trim -- Ship trim angle.
|
|
|
|
If the fluid volume is bigger than the total tank one, it will be
|
|
conveniently clamped.
|
|
"""
|
|
# Change the units of the volume, and clamp the value
|
|
if vol <= 0.0:
|
|
return Vector()
|
|
if vol >= fp.Shape.Volume:
|
|
vol = 0.0
|
|
for solid in fp.Shape.Solids:
|
|
vol += solid.Volume
|
|
sCoG = solid.CenterOfMass
|
|
cog.x = cog.x + sCoG.x * solid.Volume
|
|
cog.y = cog.y + sCoG.y * solid.Volume
|
|
cog.z = cog.z + sCoG.z * solid.Volume
|
|
cog.x = cog.x / vol
|
|
cog.y = cog.y / vol
|
|
cog.z = cog.z / vol
|
|
return cog
|
|
|
|
# Get a first estimation of the level
|
|
level = vol.Value / fp.Shape.Volume
|
|
|
|
# Transform the tank shape
|
|
current_placement = fp.Placement
|
|
m = current_placement.toMatrix()
|
|
m.rotateX(roll.getValueAs("rad"))
|
|
m.rotateY(-trim.getValueAs("rad"))
|
|
fp.Placement = Placement(m)
|
|
|
|
# Iterate to find the fluid shape
|
|
for i in range(COMMON_BOOLEAN_ITERATIONS):
|
|
shape = self.getVolume(fp, level, return_shape=True)
|
|
error = (vol.Value - shape.Volume) / fp.Shape.Volume
|
|
if abs(error) < 0.01:
|
|
break
|
|
level += error
|
|
|
|
# Get the center of gravity
|
|
vol = 0.0
|
|
cog = Vector()
|
|
if len(shape.Solids) > 0:
|
|
for solid in shape.Solids:
|
|
vol += solid.Volume
|
|
sCoG = solid.CenterOfMass
|
|
cog.x = cog.x + sCoG.x * solid.Volume
|
|
cog.y = cog.y + sCoG.y * solid.Volume
|
|
cog.z = cog.z + sCoG.z * solid.Volume
|
|
cog.x = cog.x / vol
|
|
cog.y = cog.y / vol
|
|
cog.z = cog.z / vol
|
|
|
|
# Untransform the object to retrieve the original position
|
|
fp.Placement = current_placement
|
|
p = Part.Point(cog)
|
|
m = Matrix()
|
|
m.rotateY(trim.getValueAs("rad"))
|
|
m.rotateX(-roll.getValueAs("rad"))
|
|
p.rotate(Placement(m))
|
|
|
|
return Vector(p.X, p.Y, p.Z)
|
|
|
|
|
|
class ViewProviderTank:
|
|
def __init__(self, obj):
|
|
"""Add this view provider to the selected object.
|
|
|
|
Keyword arguments:
|
|
obj -- Object which must be modified.
|
|
"""
|
|
obj.Proxy = self
|
|
|
|
def attach(self, obj):
|
|
"""Setup the scene sub-graph of the view provider, this method is
|
|
mandatory.
|
|
"""
|
|
return
|
|
|
|
def updateData(self, fp, prop):
|
|
"""If a property of the handled feature has changed we have the chance
|
|
to handle this here.
|
|
|
|
Keyword arguments:
|
|
fp -- Part::FeaturePython object affected.
|
|
prop -- Modified property name.
|
|
"""
|
|
return
|
|
|
|
def getDisplayModes(self, obj):
|
|
"""Return a list of display modes.
|
|
|
|
Keyword arguments:
|
|
obj -- Object associated with the view provider.
|
|
"""
|
|
modes = []
|
|
return modes
|
|
|
|
def getDefaultDisplayMode(self):
|
|
"""Return the name of the default display mode. It must be defined in
|
|
getDisplayModes."""
|
|
return "Flat Lines"
|
|
|
|
def setDisplayMode(self, mode):
|
|
"""Map the display mode defined in attach with those defined in
|
|
getDisplayModes. Since they have the same names nothing needs to be
|
|
done. This method is optinal.
|
|
|
|
Keyword arguments:
|
|
mode -- Mode to be activated.
|
|
"""
|
|
return mode
|
|
|
|
def onChanged(self, vp, prop):
|
|
"""Detects the ship view provider data changes.
|
|
|
|
Keyword arguments:
|
|
vp -- View provider object affected.
|
|
prop -- Modified property name.
|
|
"""
|
|
pass
|
|
|
|
def __getstate__(self):
|
|
"""When saving the document this object gets stored using Python's
|
|
cPickle module. Since we have some un-pickable here (the Coin stuff)
|
|
we must define this method to return a tuple of all pickable objects
|
|
or None.
|
|
"""
|
|
return None
|
|
|
|
def __setstate__(self, state):
|
|
"""When restoring the pickled object from document we have the chance
|
|
to set some internals here. Since no data were pickled nothing needs
|
|
to be done here.
|
|
"""
|
|
return None
|
|
|
|
def getIcon(self):
|
|
"""Returns the icon for this kind of objects."""
|
|
return ":/icons/Ship_Tank.svg"
|