motion_model.cpp
Go to the documentation of this file.
2 
3 #include <boost/property_tree/ptree.hpp>
4 #include <boost/property_tree/xml_parser.hpp>
5 
6 #include <fstream>
7 #include <stdio.h>
8 #include <cstdio>
9 #include <unordered_set>
10 
11 namespace pal
12 {
13 const std::string PrintPoint::TIME_KEY = "time_from_start";
14 const std::string PrintPoint::POSITIONS_KEY = "positions";
15 const std::string PrintMotion::JOINTS_KEY = "joints";
16 const std::string PrintMotion::POINTS_KEY = "points";
17 const std::string PrintMeta::META_KEY = "meta";
18 const std::string PrintMeta::NAME_KEY = "name";
19 const std::string PrintMeta::USAGE_KEY = "usage";
20 const std::string PrintMeta::DESC_KEY = "description";
21 
22 namespace
23 {
24 std::string exec(const char *cmd)
25 {
26  std::shared_ptr<FILE> pipe(popen(cmd, "r"), pclose);
27  if (!pipe)
28  return "Error";
29  char buffer[1024];
30  std::string result = "";
31  while (!feof(pipe.get()))
32  {
33  if (fgets(buffer, 128, pipe.get()) != NULL)
34  result += buffer;
35  }
36  return result;
37 }
38 } // namespace
39 
40 KeyFrame::KeyFrame(float time_increment) : time_increment_(time_increment)
41 {
42 }
43 
45 {
46  joints_ = k.joints_;
47 }
48 
49 void KeyFrame::addPosition(const std::string &name, double position)
50 {
51  joints_.push_back(JointPosition(name, position));
52 }
53 
54 double KeyFrame::getJointPosition(const std::string &joint) const
55 {
56  for (auto joint_position : joints_)
57  {
58  if (joint_position.joint_name_ == joint)
59  {
60  return joint_position.position_;
61  }
62  }
63 
64  return std::numeric_limits<double>::quiet_NaN();
65 }
66 
67 void KeyFrame::cleanUnused(const std::map<std::string, bool> &used_joints)
68 {
69  for (unsigned int i = joints_.size(); i > 0; --i) // Iterate backwards and remove unused
70  {
71  try
72  {
73  if (!used_joints.at(joints_[i - 1].joint_name_))
74  {
75  joints_.erase(joints_.begin() + i - 1);
76  }
77  }
78  catch (std::out_of_range &e)
79  {
80  ROS_ERROR_STREAM("Key: " << joints_[i - 1].joint_name_ << " is unknown");
81  throw e;
82  }
83  }
84 }
85 
86 PrintPoint KeyFrame::print(double basetime, double downshift,
87  const std::vector<std::string> &names) const
88 {
89  PrintPoint pp;
90  pp.time_from_start_ = basetime + (downshift * time_increment_);
91 
92  for (auto joint_name : names)
93  {
94  pp.positions_.push_back(getJointPosition(joint_name));
95  }
96 
97  return pp;
98 }
99 
100 PrintMotion KeyFrame::print(const std::vector<std::string> &names) const
101 {
102  PrintMotion pm;
103  PrintPoint pp;
104 
105  for (auto joint : joints_)
106  {
107  if (std::find(names.begin(), names.end(), joint.joint_name_) != names.end())
108  {
109  pm.joints_.push_back(joint.joint_name_);
110  pp.positions_.push_back(joint.position_);
111  }
112  }
113 
114  pp.time_from_start_ = 0.0;
115  pm.points_.push_back(pp);
116  pm.meta_.print_ = false;
117  return pm;
118 }
119 
120 const float Motion::DEFAULT_TIME = 5.0;
121 
122 Motion::Motion(const std::string &robot_description, const std::string &robot_description_semantic,
123  const std::vector<std::string> &extra_joints)
124  : tmp_name_("")
125 {
126  // Load groups
127  if (robot_description != "" && robot_description_semantic != "")
128  {
129  setMotionGroups(robot_description, robot_description_semantic);
130 
131  // Pre-select biggest group
132  uint size = 0;
133  for (const auto &group : joint_groups_)
134  {
135  if (group.second.size() >= size)
136  {
137  group_used_ = group.first;
138  size = group.second.size();
139  }
140  }
141  }
142 
143  // Collect extra joints
144  std::vector<std::string> existing_joints = getAvailableJoints();
145  for (const auto &joint : extra_joints)
146  {
147  // Check that the joint is not already loaded
148  if (std::find(existing_joints.begin(), existing_joints.end(), joint) ==
149  existing_joints.end())
150  extra_joints_[joint] = true;
151  }
152 }
153 
154 Motion::Motion(XmlRpc::XmlRpcValue &param, const std::string &robot_description,
155  const std::string &robot_description_semantic,
156  const std::vector<std::string> &extra_joints)
157  : Motion(robot_description, robot_description_semantic, extra_joints)
158 {
159  if (param["joints"].getType() == XmlRpc::XmlRpcValue::TypeArray)
160  {
161  // Load used joints
162  ROS_INFO("Joint used:");
163  std::vector<std::string> names;
164  for (int i = 0; i < param["joints"].size(); ++i)
165  {
166  names.push_back(static_cast<std::string>(param["joints"][i]));
167  ROS_INFO_STREAM(static_cast<std::string>(param["joints"][i]));
168  }
169 
170  // Set state for extra joints
171  for (auto &joint : extra_joints_)
172  {
173  joint.second = std::find(names.begin(), names.end(), joint.first) != names.end();
174  }
175 
176  // Find used group (biggest group with all joints used)
177  std::string group_used = "";
178  uint size = 0;
179  for (const auto &group : joint_groups_)
180  {
181  // Used group can't have more joints, only check if bigger than found
182  if (group.second.size() <= names.size() && group.second.size() > size)
183  {
184  // Check if all joints are used
185  bool all_used = true;
186  for (const std::string &joint : group.second)
187  {
188  if (std::find(names.begin(), names.end(), joint) == names.end())
189  {
190  ROS_INFO_STREAM("Joint " << joint << " from group " << group.first << " not in use");
191  all_used = false;
192  break;
193  }
194  }
195 
196  // If all used, choose this one
197  if (all_used)
198  {
199  size = group.second.size();
200  group_used = group.first;
201  ROS_INFO_STREAM("Found group candidate " << group_used);
202  }
203  }
204  else
205  {
206  ROS_INFO_STREAM("Group " << group.first << " discarded (" << group.second.size()
207  << "<=" << names.size() << " && " << group.second.size()
208  << ">" << size << ")");
209  }
210  }
211  group_used_ = group_used;
212 
213  // Populate motion
214  if (param["points"].getType() == XmlRpc::XmlRpcValue::TypeArray)
215  {
216  float last_time = 0.0f;
217  for (int i = 0; i < param["points"].size(); ++i)
218  {
219  KeyFrame k(toDouble(param["points"][i]["time_from_start"]) - last_time);
220  // Load joints in order
221  for (int j = 0; j < param["points"][i]["positions"].size(); ++j)
222  {
223  k.addPosition(names[j], toDouble(param["points"][i]["positions"][j]));
224  }
225  keyframes_.push_back(k);
226  last_time += k.getTime();
227  }
228  }
229  }
230 }
231 
232 void Motion::setMotionGroups(const std::string &robot_description,
233  const std::string &robot_description_semantic)
234 {
235  // Load XML robot's description
236  boost::property_tree::ptree tree_robot;
237  std::stringstream ss_robot(robot_description);
238  boost::property_tree::read_xml(ss_robot, tree_robot);
239 
240  // Find all actuable joints
241  std::unordered_set<std::string> actuable_joints;
242  for (boost::property_tree::ptree::value_type &joint : tree_robot.get_child("robot"))
243  {
244  if (joint.first != "joint") // Ignore others
245  continue;
246 
247  // Get joint name
248  std::string joint_name;
249  bool is_actuable = true;
250  for (boost::property_tree::ptree::value_type &joint_child : joint.second)
251  {
252  if (joint_child.first == "<xmlattr>")
253  {
254  for (boost::property_tree::ptree::value_type &joint_att : joint_child.second)
255  {
256  if (joint_att.first == "name")
257  {
258  joint_name = joint_att.second.data();
259  }
260  else if (joint_att.first == "type" && joint_att.second.data() == "fixed")
261  is_actuable = false;
262  }
263  }
264  }
265 
266  if (is_actuable)
267  {
268  actuable_joints.insert(joint_name);
269  ROS_DEBUG_STREAM("Actuable joint: " << joint_name);
270  }
271  }
272 
273  // Load XML groups description
274  boost::property_tree::ptree tree_semantic;
275  std::stringstream ss_semantic(robot_description_semantic);
276  boost::property_tree::read_xml(ss_semantic, tree_semantic);
277 
278  // Load groups
279  std::vector<std::pair<std::string, std::string>> pending_groups_;
280  for (boost::property_tree::ptree::value_type &group : tree_semantic.get_child("robot"))
281  {
282  if (group.first != "group") // Ignore others
283  continue;
284 
285  // Get group name
286  std::string group_name = "";
287  for (boost::property_tree::ptree::value_type &group_child : group.second)
288  {
289  if (group_child.first == "<xmlattr>")
290  {
291  for (boost::property_tree::ptree::value_type &group_att : group_child.second)
292  {
293  if (group_att.first == "name")
294  {
295  group_name = group_att.second.data();
296  ROS_DEBUG_STREAM("Group name: " << group_name);
297  }
298  }
299  }
300  else if (group_child.first == "joint")
301  {
302  for (boost::property_tree::ptree::value_type &joint_att :
303  group_child.second.get_child("<xmlattr>"))
304  {
305  // Add only actuable joints
306  if (joint_att.first == "name" &&
307  actuable_joints.find(joint_att.second.data()) != actuable_joints.end())
308  {
309  addJointToGroup(group_name, joint_att.second.data());
310  ROS_DEBUG_STREAM("Add joint " << joint_att.second.data() << " to " << group_name);
311  }
312  }
313  }
314  else if (group_child.first == "group")
315  {
316  for (boost::property_tree::ptree::value_type &subgroup_att :
317  group_child.second.get_child("<xmlattr>"))
318  {
319  if (subgroup_att.first == "name")
320  {
321  if (!addGroupToGroup(group_name, subgroup_att.second.data()))
322  {
323  pending_groups_.push_back(std::make_pair(group_name, subgroup_att.second.data()));
324  ROS_DEBUG_STREAM("Wait for group" << subgroup_att.second.data() << " to be loaded");
325  }
326  ROS_DEBUG_STREAM("Add group " << subgroup_att.second.data() << " to " << group_name);
327  }
328  }
329  }
330  }
331  }
332  // Process subgroups that weren't loaded in order
333  for (const auto &pair : pending_groups_)
334  addGroupToGroup(pair.first, pair.second);
335 
336  // Add empty group
337  joint_groups_["None"] = {};
338  ROS_DEBUG_STREAM("Joints loaded");
339 }
340 
342 {
343  std::string random = "";
344  static const char num[] = "0123456789";
345  for (int i = 0; i < 5; ++i)
346  {
347  random.append(std::to_string(num[rand() % (sizeof(num) - 1)]));
348  }
349 
350  tmp_name_ = "m_" + random;
351 }
352 
353 void Motion::addKeyFrame(const sensor_msgs::JointStateConstPtr &msg, float time_increment)
354 {
355  KeyFrame k(time_increment);
356 
357  if (keyframes_.size() == 0) // The first frame has default time 0
358  {
359  k.setTime(0.0);
360  }
361 
362  for (unsigned int i = 0; i < msg->name.size(); ++i)
363  {
364  if (isJointUsed(msg->name[i]))
365  {
366  k.addPosition(msg->name[i], msg->position[i]);
367  }
368  }
369 
370  keyframes_.push_back(k);
371 }
372 
373 void Motion::updateKeyFrame(const sensor_msgs::JointStateConstPtr &msg, int frame)
374 {
375  for (std::vector<JointPosition>::iterator it = keyframes_[frame].getJoints().begin();
376  it != keyframes_[frame].getJoints().end(); ++it)
377  {
378  for (unsigned int i = 0; i < msg->name.size(); ++i)
379  {
380  if (msg->name[i] == it->joint_name_)
381  {
382  it->position_ = msg->position[i];
383  break;
384  }
385  }
386  }
387 }
388 
389 void Motion::addJointModel(const std::string &joint_name, const JointModel &joint_model)
390 {
391  joint_models_[joint_name] = joint_model;
392 }
393 
394 void Motion::changeTime(int frame, float time_increment)
395 {
396  if ((unsigned long)frame >= keyframes_.size())
397  {
398  ROS_ERROR_STREAM("Keyframe " << frame << " doesn't exist");
399  throw ros::Exception("Keyframe " + std::to_string(frame) + " doesn't exist");
400  }
401 
402  keyframes_[frame].setTime(time_increment);
403 }
404 
405 double Motion::changeJoint(int frame, const std::string &joint_name, double position)
406 {
407  if ((unsigned long)frame >= keyframes_.size())
408  {
409  ROS_ERROR_STREAM("Keyframe " << frame << " doesn't exist");
410  throw ros::Exception("Keyframe " + std::to_string(frame) + " doesn't exist");
411  }
412 
413  for (std::vector<JointPosition>::iterator it = keyframes_[frame].getJoints().begin();
414  it != keyframes_[frame].getJoints().end(); ++it)
415  {
416  if (it->joint_name_ == joint_name)
417  {
418  if (joint_models_[joint_name].inLimits(position))
419  {
420  it->position_ = position;
421  }
422 
423  return it->position_; // Done
424  }
425  }
426 
427  ROS_ERROR_STREAM("Joint " << joint_name << " doesn't exist");
428  throw ros::Exception("Joint " + joint_name + " doesn't exist");
429 }
430 
431 void Motion::removeKeyFrame(int frame)
432 {
433  keyframes_.erase(keyframes_.begin() + frame);
434 }
435 
436 void Motion::copyFrame(int frame, int new_frame_pos)
437 {
438  KeyFrame k(keyframes_[frame]);
439 
440  if (k.getTime() == 0) // Only the first frame may have time 0
441  {
443  }
444 
445  if (new_frame_pos < 0)
446  {
447  keyframes_.push_back(k);
448  }
449  else
450  {
451  keyframes_.insert(keyframes_.begin() + new_frame_pos, k);
452  }
453 }
454 
455 void Motion::loadFrame(int frame) const
456 {
457  YAML::Emitter em;
458  /* clang-format off */
459  em << YAML::BeginMap
460  << YAML::Key << "play_motion"
461  << YAML::Value << YAML::BeginMap
462  << YAML::Key << "motions"
463  << YAML::Value << YAML::BeginMap
464  << YAML::Key << getParamName()
465  << YAML::Value << keyframes_[frame].print(getJoints())
466  << YAML::EndMap
467  << YAML::EndMap
468  << YAML::EndMap;
469  /* clang-format on */
470 
471  // Build file
472  // ROS_INFO_STREAM("YAML: \n" << motion_yaml);
473  loadParams(em, getParamName());
474 }
475 
476 void Motion::loadYAML(double downshift) const
477 {
478  YAML::Emitter em;
479  /* clang-format off */
480  em << YAML::BeginMap
481  << YAML::Key << "play_motion"
482  << YAML::Value << YAML::BeginMap
483  << YAML::Key << "motions"
484  << YAML::Value << YAML::BeginMap
485  << YAML::Key << getParamName()
486  << YAML::Value << print(downshift)
487  << YAML::EndMap
488  << YAML::EndMap
489  << YAML::EndMap;
490  /* clang-format on */
491 
492  // Build file
493  // ROS_INFO_STREAM("YAML: \n" << motion_yaml);
494  loadParams(em, getParamName());
495 }
496 
497 void Motion::extendFrames(const sensor_msgs::JointStateConstPtr &msg)
498 {
499  for (unsigned int i = 0; i < msg->name.size(); ++i)
500  {
501  if (!isJointUsed(msg->name[i]))
502  {
503  for (std::vector<KeyFrame>::iterator it = keyframes_.begin(); it != keyframes_.end(); ++it)
504  {
505  if (std::isnan(it->getJointPosition(msg->name[i])))
506  {
507  it->addPosition(msg->name[i], msg->position[i]);
508  }
509  }
510  }
511  }
512 }
513 
514 void Motion::addJointToGroup(const std::string &group, const std::string &joint)
515 {
516  if (joint_groups_.find(group) == joint_groups_.end())
517  joint_groups_[group] = {};
518 
519  joint_groups_.at(group).push_back(joint);
520 }
521 
522 bool Motion::addGroupToGroup(const std::string &group, const std::string &subgroup)
523 {
524  if (joint_groups_.find(group) == joint_groups_.end())
525  joint_groups_[group] = {};
526 
527  if (joint_groups_.find(subgroup) != joint_groups_.end())
528  {
529  joint_groups_.at(group).insert(joint_groups_.at(group).end(),
530  joint_groups_.at(subgroup).begin(),
531  joint_groups_.at(subgroup).end());
532  return true;
533  }
534  else
535  {
536  // Subgroup is not yet loaded
537  return false;
538  }
539 }
540 
541 PrintMotion Motion::print(double downshift) const
542 {
543  return print("", "", "", downshift);
544 }
545 
547 {
548  keyframes_.clear();
549 }
550 
551 PrintMotion Motion::print(const std::string &name, const std::string &usage,
552  const std::string &description, double downshift) const
553 {
554  PrintMotion pm;
555  pm.joints_ = getJoints();
556 
557  double basetime = 0.0;
558  for (auto frame : keyframes_)
559  {
560  pm.points_.push_back(frame.print(basetime, downshift, pm.joints_));
561  basetime = pm.points_.back().time_from_start_;
562  }
563 
564  if (name != "" || usage != "" || description != "")
565  {
566  pm.meta_.print_ = true;
567  pm.meta_.name_ = name;
568  pm.meta_.usage_ = usage;
569  pm.meta_.description_ = description;
570  }
571  else
572  {
573  pm.meta_.print_ = false;
574  }
575 
576  return pm;
577 }
578 
579 void loadParams(const YAML::Emitter &param, const std::string &name)
580 {
581  // Clean param
582  exec(("rosparam delete /play_motion/motions/" + name).c_str());
583 
584  // Open file
585  std::ofstream ofile("/tmp/" + name + ".yaml");
586  ofile << param.c_str();
587  ROS_INFO_STREAM("File /tmp/" << name << ".yaml written");
588  ofile.close();
589 
590  // Loat to ros
591  exec(("rosparam load /tmp/" + name + ".yaml").c_str());
592  ros::Duration d(1.0);
593  d.sleep();
594 }
595 
596 std::string cleanName(const std::string &name)
597 {
598  return name.substr(0, name.size() - 6);
599 }
600 
601 std::string rosifyName(const std::string &name)
602 {
603  return name + "_joint";
604 }
605 
607 {
608  double val = std::numeric_limits<double>::quiet_NaN();
610  {
611  val = static_cast<double>(value);
612  }
613  else if (value.getType() == XmlRpc::XmlRpcValue::TypeInt)
614  {
615  val = static_cast<int>(value);
616  }
617  else
618  {
619  ROS_ERROR_STREAM("Unknown time type: " << value.getType());
620  }
621 
622  return val;
623 }
624 } // namespace pal
625 
626 namespace YAML
627 {
628 Emitter &operator<<(YAML::Emitter &out, const pal::PrintMotion &m)
629 {
630  out.SetFloatPrecision(5);
631  out.SetDoublePrecision(5);
632 
633 
634  /* clang-format off */
635  out << YAML::BeginMap
636  << YAML::Key << pal::PrintMotion::JOINTS_KEY
637  << YAML::Value << YAML::Flow << m.joints_
638  << YAML::Key << pal::PrintMotion::POINTS_KEY
639  << m.points_
640  << m.meta_
641  << YAML::EndMap;
642  /* clang-format on */
643 
644  return out;
645 }
646 
647 Emitter &operator<<(YAML::Emitter &out, const pal::PrintPoint &k)
648 {
649  out.SetFloatPrecision(5);
650  out.SetDoublePrecision(5);
651 
652  /* clang-format off */
653  out << YAML::BeginMap
654  << YAML::Key << pal::PrintPoint::TIME_KEY
655  << YAML::Value << k.time_from_start_
656  << YAML::Key << pal::PrintPoint::POSITIONS_KEY
657  << YAML::Value << YAML::Flow << k.positions_
658  << YAML::EndMap;
659  /* clang-format on */
660 
661  return out;
662 }
663 
664 Emitter &operator<<(YAML::Emitter &out, const pal::PrintMeta &m)
665 {
666  if (m.print_)
667  {
668  /* clang-format off */
669  out << YAML::Key << pal::PrintMeta::META_KEY << YAML::Value
670  << YAML::BeginMap
671  << YAML::Key << pal::PrintMeta::NAME_KEY
672  << YAML::Value << m.name_
673  << YAML::Key << pal::PrintMeta::USAGE_KEY
674  << YAML::Value << YAML::Flow << m.usage_
675  << YAML::Key << pal::PrintMeta::DESC_KEY
676  << YAML::Value << YAML::Flow << m.description_
677  << YAML::EndMap;
678  /* clang-format on */
679  }
680 
681  return out;
682 }
683 } // namespace YAML
d
void setParamName()
Motion(const std::string &robot_description, const std::string &robot_description_semantic, const std::vector< std::string > &extra_joints)
float time_increment_
Definition: motion_model.h:132
static const std::string POINTS_KEY
Definition: motion_model.h:37
void addPosition(const std::string &name, double position)
static const std::string TIME_KEY
Definition: motion_model.h:16
std::vector< std::string > joints_
Definition: motion_model.h:39
void updateKeyFrame(const sensor_msgs::JointStateConstPtr &msg, int frame)
std::string rosifyName(const std::string &name)
bool addGroupToGroup(const std::string &group, const std::string &subgroup)
void copyFrame(int frame, int new_frame_pos=-1)
std::string tmp_name_
Definition: motion_model.h:297
static const std::string DESC_KEY
Definition: motion_model.h:27
Emitter & operator<<(YAML::Emitter &out, const pal::PrintMotion &m)
std::vector< KeyFrame > keyframes_
Definition: motion_model.h:298
std::vector< std::string > getAvailableJoints()
Definition: motion_model.h:273
double toDouble(XmlRpc::XmlRpcValue &value)
std::vector< std::string > getJoints() const
Definition: motion_model.h:186
void addJointModel(const std::string &joint_name, const JointModel &joint_model)
std::unordered_map< std::string, std::vector< std::string > > joint_groups_
Definition: motion_model.h:299
std::string usage_
Definition: motion_model.h:31
bool isJointUsed(const std::string &joint_name) const
Definition: motion_model.h:230
PrintMeta meta_
Definition: motion_model.h:41
std::string cleanName(const std::string &name)
static const std::string POSITIONS_KEY
Definition: motion_model.h:17
float time_from_start_
Definition: motion_model.h:19
Type const & getType() const
static const std::string META_KEY
Definition: motion_model.h:24
std::vector< JointPosition > joints_
Definition: motion_model.h:131
void addJointToGroup(const std::string &group, const std::string &joint)
std::vector< double > positions_
Definition: motion_model.h:20
void loadYAML(double downshift) const
const std::string & getParamName() const
Definition: motion_model.h:170
#define ROS_INFO(...)
double changeJoint(int frame, const std::string &joint_name, double position)
static const std::string NAME_KEY
Definition: motion_model.h:25
std::unordered_map< std::string, JointModel > joint_models_
Definition: motion_model.h:302
void changeTime(int frame, float time_increment)
PrintPoint print(double basetime, double downshift, const std::vector< std::string > &names) const
void loadFrame(int frame) const
std::unordered_map< std::string, bool > extra_joints_
Definition: motion_model.h:300
#define ROS_DEBUG_STREAM(args)
void addKeyFrame(const sensor_msgs::JointStateConstPtr &msg, float time_increment=DEFAULT_TIME)
void cleanUnused(const std::map< std::string, bool > &used_joints)
double getJointPosition(const std::string &joint) const
void extendFrames(const sensor_msgs::JointStateConstPtr &msg)
void removeKeyFrame(int frame)
static const float DEFAULT_TIME
Definition: motion_model.h:141
std::vector< PrintPoint > points_
Definition: motion_model.h:40
float getTime() const
Definition: motion_model.h:113
#define ROS_INFO_STREAM(args)
void loadParams(const YAML::Emitter &param, const std::string &filename)
std::size_t size() const
Definition: motion_model.h:206
void setMotionGroups(const std::string &robot_description, const std::string &robot_description_semantic)
bool sleep() const
#define ROS_ERROR_STREAM(args)
PrintMotion print(double downshift=1.0) const
static const std::string USAGE_KEY
Definition: motion_model.h:26
std::string name_
Definition: motion_model.h:30
void setTime(float time_increment)
Definition: motion_model.h:117
std::string description_
Definition: motion_model.h:32
static const std::string JOINTS_KEY
Definition: motion_model.h:36
std::string group_used_
Definition: motion_model.h:301
KeyFrame(float time_increment)
void removeAllKeyFrames()


play_motion_builder
Author(s):
autogenerated on Mon Feb 28 2022 23:13:38