Commit 66697d5e authored by promayon's avatar promayon
Browse files

NEW Mesh Quality action (well just a start, using only one quality metric only at the moment)

NEW Warp Out action (move external surface outward)
NEW Mesh RenderingMode has now a option to generate/display the surface normals
FIXED instanciation of MeshComponent with no initial mesh possible (but beware: you need to create the mesh at one stage anyway...)
FIXED PML ComponentDC and inherited are now MeshComponent (easier export to any other type of mesh)
FIXED crash in save as msh format when the mesh was empty
FIXED no need to be top level to be Saved As in a compatible file format
FIXED crash in bad PML SC picking 
FIXED remove date in copyright notice in some descriptions/comments (they were not always updated... and not needed)


git-svn-id: svn+ssh://scm.forge.imag.fr/var/lib/gforge/chroot/scmrepos/svn/camitk/trunk/camitk@208 ec899d31-69d1-42ba-9299-647d76f65fb3
parent 7b5d6379
......@@ -66,134 +66,130 @@ QWidget * SaveAsAction::getWidget() {
// --------------- apply -------------------
Action::ApplyStatus SaveAsAction::apply() {
// if the slot was call, it means it was callable,
// if it was callable, it means the QAction was enable, hence the selectemItem #0 was a top level component
// but just in case, do a little test!
Component *comp = Application::getSelectedComponents().last();
if ( comp->isTopLevel() ) {
Application::showStatusBarMessage( tr ( "Saving currently selected component under new filename or format..." ) );
Application::showStatusBarMessage( tr ( "Saving currently selected component under new filename or format..." ) );
// Get the possible save as extension... (compatible formats)
QSet<QString> filter;
// Get the possible save as extension... (compatible formats)
QSet<QString> filter;
// first check if there is a filename
QString compfileName = comp->getFileName();
// first check if there is a filename
QString compfileName = comp->getFileName();
// check the extension
QString extension = QFileInfo ( compfileName ).completeSuffix();
// check the extension
QString extension = QFileInfo ( compfileName ).completeSuffix();
// if no extension is found, look for the export of plugins which write directories
if (!compfileName.isEmpty() && extension.isEmpty() ) {
foreach ( QString cpName, ExtensionManager::getDataDirectoryExtNames() ) {
const ComponentExtension * cp = ExtensionManager::getComponentExtension ( cpName );
filter += cpName + cp->getFileExtensions().join(" ");
}
// if no extension is found, look for the export of plugins which write directories
if (!compfileName.isEmpty() && extension.isEmpty() && comp->isTopLevel()) {
foreach ( QString cpName, ExtensionManager::getDataDirectoryExtNames() ) {
const ComponentExtension * cp = ExtensionManager::getComponentExtension ( cpName );
filter += cpName + cp->getFileExtensions().join(" ");
}
}
// If the selected component is of type ImageComponent, use all ImageComponentExtension
const ImageComponent * compAsImg = dynamic_cast<const ImageComponent *> ( comp );
// If the selected component is of type ImageComponent, use all ImageComponentExtension
const ImageComponent * compAsImg = dynamic_cast<const ImageComponent *> ( comp );
if (compAsImg) {
foreach (ComponentExtension * pl, ExtensionManager::getComponentExtensions().values().toSet()) {
const ImageComponentExtension * imageExt = dynamic_cast<const ImageComponentExtension*> (pl);
if (compAsImg) {
foreach (ComponentExtension * pl, ExtensionManager::getComponentExtensions().values().toSet()) {
const ImageComponentExtension * imageExt = dynamic_cast<const ImageComponentExtension*> (pl);
if (imageExt) {
foreach (QString ext, imageExt->getFileExtensions()) {
filter += ExtensionManager::getComponentExtension ( ext )->getName() + " (*." + ext + ")";
}
if (imageExt) {
foreach (QString ext, imageExt->getFileExtensions()) {
filter += ExtensionManager::getComponentExtension ( ext )->getName() + " (*." + ext + ")";
}
}
}
else {
// If the selected component is of type MeshComponent, use all MeshComponentExtension
const MeshComponent * compAsMesh = dynamic_cast<const MeshComponent *> ( comp );
}
else {
// If the selected component is of type MeshComponent, use all MeshComponentExtension
const MeshComponent * compAsMesh = dynamic_cast<const MeshComponent *> ( comp );
if (compAsMesh) {
foreach (ComponentExtension * pl, ExtensionManager::getComponentExtensions().values().toSet()) {
const ComponentExtension * meshExt = dynamic_cast<const MeshComponentExtension*> (pl);
if (compAsMesh) {
foreach (ComponentExtension * pl, ExtensionManager::getComponentExtensions().values().toSet()) {
const ComponentExtension * meshExt = dynamic_cast<const MeshComponentExtension*> (pl);
if (meshExt) {
foreach (QString ext, meshExt->getFileExtensions()) {
filter += ExtensionManager::getComponentExtension(ext)->getName() + " (*." + ext + ")";
}
if (meshExt) {
foreach (QString ext, meshExt->getFileExtensions()) {
filter += ExtensionManager::getComponentExtension(ext)->getName() + " (*." + ext + ")";
}
}
}
else {
// the selected component is neither ImageComponent or MeshComponent, just
// ask the component that instanciated it
foreach ( QString ext, ExtensionManager::getComponentExtension ( extension )->getFileExtensions() ) {
filter += ExtensionManager::getComponentExtension ( ext )->getName() + " (*." + ext + ")";
}
}
else {
// the selected component is neither ImageComponent or MeshComponent, just
// ask the component that instanciated it
foreach ( QString ext, ExtensionManager::getComponentExtension ( extension )->getFileExtensions() ) {
filter += ExtensionManager::getComponentExtension ( ext )->getName() + " (*." + ext + ")";
}
}
}
// suggest a new name (code snippet from KSnapObject::autoincFilename(), from the ksnapshot project)
// Extract the filename from the path
QString suggestedName = QFileInfo(compfileName).fileName();
// suggest a new name (code snippet from KSnapObject::autoincFilename(), from the ksnapshot project)
// Extract the filename from the path
QString suggestedName = QFileInfo(compfileName).fileName();
// use the top level component name if there is no file name
if (suggestedName.isEmpty()) {
suggestedName = comp->getName();
// use the top level component name if there is no file name
if (suggestedName.isEmpty()) {
suggestedName = comp->getName();
}
else {
// If the name contains a number then increment it
QRegExp numSearch( "(^|[^\\d])(\\d+)" ); // we want to match as far left as possible, and when the number is at the start of the name
// Does it have a number?
int start = numSearch.lastIndexIn( suggestedName );
if (start != -1) {
// It has a number, increment it
start = numSearch.pos( 2 ); // we are only interested in the second group
QString numAsStr = numSearch.capturedTexts()[ 2 ];
QString number = QString::number( numAsStr.toInt() + 1 );
number = number.rightJustified( numAsStr.length(), '0' );
suggestedName.replace( start, numAsStr.length(), number );
}
else {
// If the name contains a number then increment it
QRegExp numSearch( "(^|[^\\d])(\\d+)" ); // we want to match as far left as possible, and when the number is at the start of the name
// Does it have a number?
int start = numSearch.lastIndexIn( suggestedName );
// no number
start = suggestedName.lastIndexOf('.');
if (start != -1) {
// It has a number, increment it
start = numSearch.pos( 2 ); // we are only interested in the second group
QString numAsStr = numSearch.capturedTexts()[ 2 ];
QString number = QString::number( numAsStr.toInt() + 1 );
number = number.rightJustified( numAsStr.length(), '0' );
suggestedName.replace( start, numAsStr.length(), number );
// has a . somewhere, e.g. it has an extension
suggestedName.insert(start, '1');
}
else {
// no number
start = suggestedName.lastIndexOf('.');
if (start != -1) {
// has a . somewhere, e.g. it has an extension
suggestedName.insert(start, '1');
}
else {
// no extension, just tack it on to the end
suggestedName += '1';
}
// no extension, just tack it on to the end
suggestedName += '1';
}
}
}
// format filter list, sort
QStringList filterList(filter.toList());
filterList.sort();
// format filter list, sort
QStringList filterList(filter.toList());
filterList.sort();
// get the file name
QString filename = QFileDialog::getSaveFileName ( NULL, tr ( "Save File As..." ), QFileInfo(compfileName).dir().canonicalPath() +"/"+ suggestedName, filterList.join(";;"));
// get the file name
QString filename = QFileDialog::getSaveFileName ( NULL, tr ( "Save File As..." ), QFileInfo(compfileName).dir().canonicalPath() +"/"+ suggestedName, filterList.join(";;"));
if ( !filename.isEmpty() ) {
// rename the filename of the component and save
comp->setFileName(filename);
// save the file as the new name
bool saveStatus = Application::save(comp);
if ( !filename.isEmpty() ) {
// rename the filename of the component and save
comp->setFileName(filename);
// save the file as the new name
bool saveStatus = Application::save(comp);
// reset name if there was a problem
if (saveStatus) {
return SUCCESS;
}
else {
comp->setFileName(compfileName);
return ERROR;
}
// reset name if there was a problem
if (saveStatus) {
return SUCCESS;
}
else {
Application::showStatusBarMessage( tr ( "Saving aborted" ), 2000 );
return ABORTED;
comp->setFileName(compfileName);
return ERROR;
}
}
else {
Application::showStatusBarMessage( tr ( "Saving aborted" ), 2000 );
return ABORTED;
}
return ERROR;
}
......
......@@ -37,7 +37,7 @@ using namespace camitk;
// --------------- constructor -------------------
ImageLutAction::ImageLutAction(ActionExtension* extension) : Action(extension) {
setName("Lut");
setDescription("Modify the LUT of an image components.<br>(c)TIMC-IMAG 2003-2012");
setDescription("Modify the LUT of an image components");
setComponent("ImageComponent");
setFamily("ImageLut");
addTag("Lut");
......
......@@ -28,6 +28,8 @@
#include "RenderingOption.h"
#include "ChangeColor.h"
#include "MeshPicking.h"
#include "CenterMesh.h"
#include "MeshQuality.h"
#include <Action.h>
......@@ -45,5 +47,7 @@ void BasicMeshExtension::init() {
registerNewAction(ChangeColor);
registerNewAction(RenderingOption);
registerNewAction(RigidTransform);
registerNewAction(MeshPicking);
registerNewAction(MeshPicking);
registerNewAction(CenterMesh);
registerNewAction(MeshQuality);
}
\ No newline at end of file
......@@ -35,7 +35,7 @@ using namespace camitk;
ChangeColor::ChangeColor(ActionExtension *extension) : Action(extension) {
setName("Change Color");
setEmbedded(false);
setDescription("Change the surface, wireframe or points colors of objects.<br>(c)TIMC-IMAG 2003-2012");
setDescription("Change the surface, wireframe or points colors of objects");
setComponent("MeshComponent");
setFamily("Basic Mesh");
setIcon(QPixmap(":/changeColor"));
......
......@@ -45,7 +45,6 @@ MeshPicking::MeshPicking(ActionExtension *extension) : Action(extension) {
setComponent("MeshComponent");
setFamily("Basic Mesh");
addTag("Picking");
addTag("Tutorial");
//-- widget lazy instanciation
informationFrame = NULL;
......
......@@ -28,15 +28,23 @@
#include <MeshComponent.h>
#include <InteractiveViewer.h>
#include <complex>
using namespace camitk;
//-- normal generation
#include <vtkDataSetSurfaceFilter.h>
#include <vtkPolyDataNormals.h>
#include <vtkDataSetMapper.h>
#include <vtkGlyph3D.h>
#include <vtkArrowSource.h>
// --------------- constructor -------------------
RenderingOption::RenderingOption(ActionExtension* extension) : Action(extension) {
setName("Rendering Option");
setIcon(QPixmap(":/renderingOption"));
setDescription("Change the rendering option (surface, wireframe, points, label...).<br>(c)TIMC-IMAG 2003-2012");
setDescription("Change the rendering option (surface, wireframe, points, label...)");
setComponent("MeshComponent");
setFamily("Basic Mesh");
addTag("RenderingMode");
......@@ -47,6 +55,7 @@ RenderingOption::RenderingOption(ActionExtension* extension) : Action(extension)
setProperty("Points", false);
setProperty("Label", false);
setProperty("Glyph", false);
setProperty("Normals", false);
}
RenderingOption::~RenderingOption() {
......@@ -67,6 +76,7 @@ QWidget * RenderingOption::getWidget() {
// more than one: they should all have the same rendering mode to set a proper state
InterfaceGeometry::RenderingModes m = getTargets().first()->getRenderingModes();
int i = 1;
while (i < getTargets().size() && (m == getTargets()[i]->getRenderingModes()))
i++;
......@@ -81,8 +91,10 @@ QWidget * RenderingOption::getWidget() {
if (getTargets().size()>0) {
setProperty("Glyph", (bool) (getTargets().first()->getProp("glyph")->GetVisibility()));
setProperty("Label", (bool) (getTargets().first()->getProp("label")->GetVisibility()));
// normal is an extra prop build by this action if needed, check if it exists first)
setProperty("Normals", getTargets().first()->getProp("normals")!=NULL && (bool) (getTargets().first()->getProp("normals")->GetVisibility()));
}
return Action::getWidget();
};
......@@ -91,10 +103,13 @@ QWidget * RenderingOption::getWidget() {
Action::ApplyStatus RenderingOption::apply() {
// get the current rendering mode from the property values
InterfaceGeometry::RenderingModes m = InterfaceGeometry::None;
if (property("Surface").toBool())
m |= InterfaceGeometry::Surface;
if (property("Wireframe").toBool())
m |= InterfaceGeometry::Wireframe;
if (property("Points").toBool())
m |= InterfaceGeometry::Points;
......@@ -104,11 +119,49 @@ Action::ApplyStatus RenderingOption::apply() {
// set the additional prop visibility
comp->getProp("glyph")->SetVisibility(property("Glyph").toBool());
comp->getProp("label")->SetVisibility(property("Label").toBool());
// normal is a specific prop, create it only if needed
if (property("Normals").toBool() && comp->getProp("normals") == NULL) {
//-- get the surface (in order to have polygon only)
vtkSmartPointer<vtkDataSetSurfaceFilter> surfaceFilter = vtkSmartPointer<vtkDataSetSurfaceFilter>::New();
surfaceFilter->SetInput(comp->getPointSet());
surfaceFilter->Update();
//-- generate the normals
vtkSmartPointer<vtkPolyDataNormals> normalGenerator = vtkSmartPointer<vtkPolyDataNormals>::New();
normalGenerator->SetInputConnection(surfaceFilter->GetOutputPort());
normalGenerator->ComputePointNormalsOn();
normalGenerator->ComputeCellNormalsOff();
normalGenerator->SplittingOff();
normalGenerator->Update();
//-- create the arrow glyph for the surface normal
vtkSmartPointer<vtkArrowSource> arrowSource = vtkSmartPointer<vtkArrowSource>::New();
vtkSmartPointer<vtkGlyph3D> arrowGlyph = vtkSmartPointer<vtkGlyph3D>::New();
arrowGlyph->ScalingOn();
arrowGlyph->SetScaleFactor(comp->getBoundingRadius()/10.0);
arrowGlyph->SetVectorModeToUseNormal();
arrowGlyph->SetScaleModeToScaleByVector();
arrowGlyph->OrientOn();
arrowGlyph->SetSourceConnection(arrowSource->GetOutputPort());
arrowGlyph->SetInputConnection(normalGenerator->GetOutputPort());
vtkSmartPointer<vtkDataSetMapper> normalMapper = vtkSmartPointer<vtkDataSetMapper>::New();
normalMapper->SetInputConnection(arrowGlyph->GetOutputPort());
vtkSmartPointer<vtkActor> normalActor = vtkSmartPointer<vtkActor>::New();
normalActor->SetMapper(normalMapper);
//-- add the prop to the Component
comp->addProp("normals",normalActor);
}
comp->getProp("normals")->SetVisibility(property("Normals").toBool());
}
// refresh the viewers (here only the viewer of the last component, but should be ok)
getTargets().last()->refresh();
return SUCCESS;
}
......@@ -42,7 +42,7 @@
RigidTransform::RigidTransform(ActionExtension* extension) : Action(extension) {
setName("Rigid Transform");
setEmbedded(false);
setDescription("Rigid transformation of Components.<br>(c)TIMC-IMAG 2003-2012");
setDescription("Rigid transformation of Components");
setComponent("MeshComponent");
setFamily("Basic Mesh");
addTag("Transform");
......@@ -101,7 +101,7 @@ RigidTransform::~RigidTransform() {
}
}
// --------------- apply -------------------
// --------------- getWidget -------------------
QWidget * RigidTransform::getWidget() {
if (!dialog)
init();
......@@ -192,18 +192,6 @@ Action::ApplyStatus RigidTransform::apply() {
// apply the transformation
unsigned int i = 0;
foreach(Component *comp, getTargets()) {
vtkSmartPointer<vtkPointSet> newData;
if (vtkUnstructuredGrid::SafeDownCast(comp->getPointSet())) {
newData = vtkSmartPointer<vtkUnstructuredGrid>::New();
}
else
if (vtkPolyData::SafeDownCast(comp->getPointSet())) {
newData = vtkSmartPointer<vtkPolyData>::New();
}
else {
CAMITK_INFO("RigidTransform", "applyTransform", "Error: Component point set is of unsupported type: " << comp->getPointSet()->GetClassName());
return ERROR;
}
// get the result from the corresponding filter
vtkSmartPointer<vtkPointSet> result = vtkPointSet::SafeDownCast(filterList[i]->GetOutputDataObject(0));
if (result) {
......
......@@ -40,13 +40,8 @@ using namespace camitk;
typedef vtkSmartPointer<vtkTransformFilter> vtkSmartPointerTransformFilter;
/** This operator allows you to apply a linear transformation (translation,rotation around axes,scaling)
* the currently selected Data Components (only works on Component that have a geometric representation).
*
* \note As this dialog is modeless, the selection can change while it is shown, but the
* tranformed Component will always remain the same, i.e. the Component that were selected at the instanciation time
* (stored in filteredComponent).
*
/** This action allows you to apply a linear transformation (translation,rotation around axes,scaling)
* the currently selected MeshComponents.
*/
class RigidTransform : public Action {
Q_OBJECT
......
......@@ -27,6 +27,7 @@
#include "CleanPolyData.h"
#include "FillWithPoints.h"
#include "Tetrahedralize.h"
#include "WarpOut.h"
#include "Application.h"
......@@ -39,6 +40,7 @@ void MeshProcessingExtension::init() {
registerNewAction(CleanPolyData);
registerNewAction(FillWithPoints);
registerNewAction(Tetrahedralize);
registerNewAction(WarpOut);
}
......
......@@ -260,23 +260,16 @@ void ImpMainWindow::initToolBar() {
// ------------------------ updateActionStates ----------------------------
void ImpMainWindow::updateActionStates() {
unsigned int nrOfSelectedItems = Application::getSelectedComponents().size();
bool selectedIsComponent = ( nrOfSelectedItems > 0 && Application::getSelectedComponents().last()->isTopLevel() );
bool selectedIsTopLevel = ( nrOfSelectedItems > 0 && Application::getSelectedComponents().last()->isTopLevel() );
unsigned int nrOfComponents = Application::getTopLevelComponents().size();
//-- update file menu
fileCloseAll->setEnabled ( nrOfComponents > 0 );
fileSaveAll->setEnabled ( nrOfComponents > 0 );
fileClose->setEnabled ( selectedIsComponent );
if ( nrOfSelectedItems > 0 ) {
fileSave->setEnabled ( selectedIsComponent && Application::getSelectedComponents().first()->getTopLevelComponent()->getModified() );
fileSaveAs->setEnabled ( selectedIsComponent );
}
else {
fileSaveAs->setEnabled ( false );
fileSave->setEnabled ( false );
}
fileSave->setEnabled ( selectedIsTopLevel && Application::getSelectedComponents().first()->getTopLevelComponent()->getModified() ); // save available only if needed
fileSaveAs->setEnabled ( nrOfSelectedItems > 0 ); // no need to be top level to be saved as in a compatible format
fileClose->setEnabled ( selectedIsTopLevel );
//-- update edit menu
editClearSelection->setEnabled ( nrOfSelectedItems > 0 );
......
......@@ -21,7 +21,7 @@ sshall sudo apt-get -y install wine
- STEP #2
Build the suitable mingw32 environment.
The main difference concerns Qt, that cannot be build in static mode because
The main difference concerns Qt, that cannot be built in static mode because
we need the plugin facilities.
- rm -f /opt/mingw/src/qt.mk # you need to be quite radical here
- cp qt.mk /opt/mingw/src # to compile qt as shared libs
......
......@@ -74,7 +74,7 @@ QString DicomComponentExtension::getName() const {
// --------------- getDescription -------------------
QString DicomComponentExtension::getDescription() const {
return "Manage Dicom directory in <b>CamiTK</b>.<br>(c) TIMC-IMAG 2009-2010";
return "Manage Dicom directory in <b>CamiTK</b>";
}
// --------------- hasDataDirectory -------------------
......
......@@ -52,9 +52,9 @@ QString ItkImageComponentExtension::getDescription() const {
+ "<li>LSM is a line of confocal laser scanning microscopes produced by the Zeiss company</li>"
// + "<li>ima is the Siemens Vision image format</li>"
// + "<li>mnc (MINC / MINC2) is developed over the past 15 years at the Brain Imaging Centre (or Centre) at the Montreal Neurological Institute</li>"
+ "<li>Nrrd is the \"Nearly Raw Raster Data\"</li>"
+ "<li>Nrrd is the \"Nearly Raw Raster Data\"</li>";
// + "<li>cub see <a href=\"http://www.insight-journal.org/browse/publication/118\">http://www.insight-journal.org/browse/publication/118</a></li></ul><br/>"
+ "(c)TIMC-IMAG 2009-2012";
}
// --------------- getFileExtensions -------------------
......
......@@ -39,7 +39,7 @@ QString MMLComponentExtension::getName() const {
// --------------- getDescription -------------------
QString MMLComponentExtension::getDescription() const {
return "Manage <em>.mml</em> document in <b>CamiTK</b>.<br/>Lots of things are possible with MML!<br>(c)TIMC-IMAG 2003-2010";
return "Manage <em>.mml</em> document in <b>CamiTK</b>.<br/>Lots of things are possible with MML!";
}
// --------------- getFileExtensions -------------------
......
......@@ -33,6 +33,7 @@
#include "MshExtension.h"
#include <QFileInfo>
#include <QMessageBox>
#include <vtkCellType.h>
#include <vtkCell.h>
......@@ -67,6 +68,11 @@ bool MshExtension::save(Component* component) const {
vtkSmartPointer<vtkPointSet> ps = component->getPointSet();
if (ps == NULL || ps->GetNumberOfPoints()==0 || ps->GetNumberOfCells() == 0) {
QMessageBox::warning(NULL, "Save As msh", "The selected component does not have any points or cells. This is an invalid mesh.", QMessageBox::Abort);
return false;
}
QString baseFilename = QFileInfo(component->getFileName()).absolutePath() + "/" + QFileInfo(component->getFileName()).completeBaseName();
QString mshFilename = baseFilename + ".msh";
......
......@@ -36,7 +36,7 @@ QString ObjExtension::getName() const {
// --------------- getDescription -------------------
QString ObjExtension::getDescription() const {
return "Manage Alias Wavefront OBJ <em>.obj</em> files in <b>CamiTK</b>.(very few support!) <br>(c)TIMC-IMAG UJF 2011";
return "Manage Alias Wavefront OBJ <em>.obj</em> files in <b>CamiTK</b>.<br/>(very few support!)";
}
// --------------- getFileExtensions -------------------
......
......@@ -31,7 +31,7 @@
#include <vtkPoints.h>
// -------------------- default constructor --------------------
ComponentDC::ComponentDC(camitk::Component *parent, PMManagerDC * pmManagerDC, ::Component *c) : myComponent(c), myPMManagerDC(pmManagerDC), Component(parent, "default PML component", camitk::Component::GEOMETRY) {
ComponentDC::ComponentDC(camitk::Component *parent, PMManagerDC * pmManagerDC, ::Component *c) : myComponent(c), myPMManagerDC(pmManagerDC), MeshComponent(parent, NULL, "default PML component") {
// set up Qt properties
Properties *componentProp = c->getProperties();
......
......@@ -28,7 +28,7 @@
#define ComponentDC_H
#include <pml/Component.h>
#include <Component.h>
#include <MeshComponent.h>
using namespace camitk;
class PMManagerDC;
......@@ -38,7 +38,7 @@ class PMManagerDC;
*
*/
class ComponentDC : public camitk::Component {
class ComponentDC : public camitk::MeshComponent {
Q_OBJECT
public:
/// constructors
......
<
......@@ -36,7 +36,7 @@ QString PMComponentExtension::getName() const {
// --------------- getDescription -------------------
QString PMComponentExtension::getDescription() const {
return "Manage Physical Model <em>.pml</em> files in <b>CamiTK</b>.<br/>CamiTK was initially mainly developped to support this format. Lots of things are possible with a physical model!<br>(c)TIMC-IMAG 2003-2010";
return "Manage Physical Model <em>.pml</em> files in <b>CamiTK</b>.<br/>CamiTK was initially mainly developped to support this format. Lots of things are possible with a physical model!";
}