Source code for klampt.io.html

"""An exporter that converts scenes / animations to shareable HTML files.
"""

from klampt import *
from klampt.model import trajectory
from klampt import robotsim
import json

_title_id = '__TITLE__'
_scene_id = '__SCENE_JSON__'
_path_id = '__PATH_JSON__'
_rpc_id = '__RPC_JSON__'
_compressed_id = '__COMPRESSED__'
_dt_id = '__TIMESTEP__'
_frontend_load_id = '__KLAMPT_FRONTEND_LOAD__'

[docs]def make_fixed_precision(obj,digits): if isinstance(obj,float): return round(obj,digits) elif isinstance(obj,list): for i in range(len(obj)): obj[i] = make_fixed_precision(obj[i],digits) elif isinstance(obj,tuple): return [make_fixed_precision(val,digits) for val in obj] elif isinstance(obj,dict): for i,v in obj.items(): obj[i] = make_fixed_precision(v,digits) return obj
[docs]def read_resource_contents(package,filename): try: import importlib.resources ref = importlib.resources.files(package) / filename return ref.read_text() except (ImportError,AttributeError): #Python 3.9 introduces the files() object and raises an exception on Python 3.8 try: import importlib_resources ref = importlib_resources.files(package) / filename return ref.read_text() except ImportError: import pkg_resources fn = pkg_resources.resource_filename(package,filename) with open(fn,'r') as f: result = ''.join(f.readlines()) return result
[docs]class HTMLSharePath: """An exporter that converts scenes / animations to shareable HTML files. Examples:: sharer = HTMLSharePath("mypath.html",name="My spiffy path") sharer.start(sim) #can accept a sim or a world while [simulation is running]: #do whatever control you wish to do here sim.simulate(...) sharer.animate() sharer.end() #this saves to the filename given in the constructor """ def __init__(self,filename=None,name="Klamp't Three.js app",boilerplate='auto',libraries='static'): """ Args: filename (str, optional): the HTML file to generate. If None, then the end() method returns the HTML string. name (str): the title of the HTML page boilerplate (str): the location of the boilerplate HTML file. If 'auto', it's automatically found in the ``klampt/data`` folder. libraries (str): either 'static' or 'dynamic'. In the latter case, the html file loads the libraries from the Klamp't website dynamically. This reduces the size of the HTML file by about 600kb, but the viewer needs an internet connection """ self.name = name if boilerplate == 'auto': boilerplate = 'data/share_path_boilerplate.html' self.boilerplate_file = read_resource_contents('klampt',boilerplate) if libraries == 'static': load_script_filename = 'data/klampt_frontend_load_static.js' else: if libraries != 'dynamic': raise ValueError("The libraries argument must either be 'static' or 'dynamic'") load_script_filename = 'data/klampt_frontend_load_dynamic.js' self.klampt_load_script = read_resource_contents('klampt',load_script_filename) if any(v not in self.boilerplate_file for v in [_scene_id,_path_id,_rpc_id,_compressed_id,_dt_id,_frontend_load_id]): raise RuntimeError("Boilerplate file does not contain the right tags") self.fn = filename self.scene = 'null' self.transforms = {} self.rpc = [] self.dt = 0 self.last_t = 0
[docs] def start(self,world): """Begins the path saving with the given WorldModel or Simulator""" if isinstance(world,Simulator): self.sim = world self.world = world.world self.last_t = world.getTime() else: self.sim = None self.world = world if self.world is not None: self.scene = robotsim.ThreeJSGetScene(self.world)
[docs] def animate(self,time=None,rpc=None): """Updates the path from the world. If the world wasn't a simulator, the time argument needs to be provided. If you want to include extra things, provide them in the rpc argument (as a list of KlamptFrontend rpc calls) """ if self.sim is not None and time is None: time = self.sim.getTime() self.sim.updateWorld() if time is None: raise ValueError("Time needs to be provided") dt = time - self.last_t if self.dt == 0: self.dt = dt if self.dt == 0: return if abs(dt - self.dt) <= 1e-6: dt = self.dt numadd = 0 while dt >= self.dt: numadd += 1 if self.world is not None: transforms = json.loads(robotsim.ThreeJSGetTransforms(self.world)) else: transforms = {'object':[]} for update in transforms['object']: n = update['name'] mat = make_fixed_precision(update['matrix'],4) matpath = self.transforms.setdefault(n,[]) assert len(matpath) == len(self.rpc) lastmat = None for m in matpath[::-1]: if m != None: lastmat = m break if lastmat != mat: matpath.append(mat) else: matpath.append(None) if numadd == 1: if rpc is not None: assert isinstance(rpc,(list,tuple)),"rpc argument must be a list or a tuple" self.rpc.append(rpc) else: self.rpc.append(None) else: self.rpc.append(None) dt -= self.dt self.last_t += self.dt if numadd > 1: print("HTMLSharePath: Note, uneven time spacing, duplicating frame",numadd,"times")
[docs] def end(self,rpc=None): if len(self.rpc)==0: self.rpc = [rpc] elif rpc is not None: self.rpc[-1] += rpc data = self.boilerplate_file.replace(_title_id,self.name) data = data.replace(_scene_id,self.scene) data = data.replace(_path_id,json.dumps(self.transforms)) data = data.replace(_rpc_id,json.dumps(self.rpc)) data = data.replace(_compressed_id,'true') data = data.replace(_dt_id,str(self.dt)) data = data.replace(_frontend_load_id,self.klampt_load_script) if self.fn is None: return data else: print("Path with",len(self.rpc),"frames saved to",self.fn) f = open(self.fn,'w') f.write(data) f.close()
if __name__ == '__main__': import sys import os from klampt import trajectory world = WorldModel() if len(sys.argv) == 1: world.readFile("../../data/athlete_plane.xml") q = world.robot(0).getConfig() q[2] = 2 world.robot(0).setConfig(q) sim = Simulator(world) share = HTMLSharePath(name="Klamp't simulation path") share.start(sim) for i in range(100): sim.simulate(0.02) share.animate() share.end() else: assert len(sys.argv) == 3,"Usage: sharepath.py world.xml robot_path" world.readFile(sys.argv[1]) traj = trajectory.RobotTrajectory(world.robot(0)) traj.load(sys.argv[2]) world.robot(0).setConfig(traj.milestones[0]) dt = 0.02 excess = 1.0 share = HTMLSharePath(name="Klamp't path "+os.path.split(sys.argv[2])[1]) share.start(world) share.dt = dt t = traj.times[0] while t < traj.times[-1] + excess: world.robot(0).setConfig(traj.eval(t)) share.animate(t) t += dt share.end()