From f689147378113f5056460ea33caecd878425628c Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Wed, 23 Nov 2022 13:41:18 -0500 Subject: [PATCH] add urdf schema --- docs/conf.py | 1 + docs/requirements.txt | 3 +- examples/section.ipynb | 2 +- trimesh/constants.py | 9 +- trimesh/exchange/load.py | 21 +- trimesh/exchange/urdf.py | 28 ++- trimesh/path/exchange/load.py | 15 +- trimesh/resources/__init__.py | 21 +- trimesh/resources/schema/README.md | 4 +- trimesh/resources/schema/urdf.xsd | 324 +++++++++++++++++++++++++++++ trimesh/scene/transforms.py | 2 +- trimesh/util.py | 5 +- 12 files changed, 392 insertions(+), 43 deletions(-) create mode 100644 trimesh/resources/schema/urdf.xsd diff --git a/docs/conf.py b/docs/conf.py index fca0b9cf5..ed52b15ec 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -80,6 +80,7 @@ def abspath(rel): # The theme to use for HTML and HTML Help pages html_theme = 'sphinx_rtd_theme' # html_theme = 'insegel' +# html_theme = 'furo' # options for rtd-theme html_theme_options = { diff --git a/docs/requirements.txt b/docs/requirements.txt index eac806ae7..0ecb694e1 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -8,4 +8,5 @@ pyopenssl==22.1.0 autodocsumm==0.2.9 jinja2==3.1.2 matplotlib==3.6.2 -nbconvert==7.2.4 +nbconvert==7.2.5 + diff --git a/examples/section.ipynb b/examples/section.ipynb index c89a88b7c..c1981c0d9 100644 --- a/examples/section.ipynb +++ b/examples/section.ipynb @@ -130,7 +130,7 @@ "source": [ "# we can plot the intersection (red) and our original geometry(black and green)\n", "ax = plt.gca()\n", - "for h in hits:\n", + "for h in hits.geoms:\n", " ax.plot(*h.xy, color='r')\n", "slice_2D.show()" ] diff --git a/trimesh/constants.py b/trimesh/constants.py index 215faddfa..375289fb2 100644 --- a/trimesh/constants.py +++ b/trimesh/constants.py @@ -1,13 +1,6 @@ import numpy as np -from .util import log, PY3 - -if PY3: - # will be the highest granularity clock available - from time import perf_counter as now -else: - # perf_counter not available on python 2 - from time import time as now +from .util import log, now class ToleranceMesh(object): diff --git a/trimesh/exchange/load.py b/trimesh/exchange/load.py index 481ad837a..4eb6c3d1a 100644 --- a/trimesh/exchange/load.py +++ b/trimesh/exchange/load.py @@ -9,10 +9,9 @@ from ..parent import Geometry from ..points import PointCloud from ..scene.scene import Scene, append_scenes -from ..constants import log_time, log +from ..util import log, now from . import misc - from .xyz import _xyz_loaders from .ply import _ply_loaders from .stl import _stl_loaders @@ -173,7 +172,6 @@ def load(file_obj, return loaded -@log_time def load_mesh(file_obj, file_type=None, resolver=None, @@ -209,10 +207,12 @@ def load_mesh(file_obj, try: # make sure we keep passed kwargs to loader # but also make sure loader keys override passed keys - results = mesh_loaders[file_type](file_obj, - file_type=file_type, - resolver=resolver, - **kwargs) + loader = mesh_loaders[file_type] + tic = now() + results = loader(file_obj, + file_type=file_type, + resolver=resolver, + **kwargs) if not isinstance(results, list): results = [results] @@ -223,10 +223,9 @@ def load_mesh(file_obj, loaded[-1].metadata.update(metadata) if len(loaded) == 1: loaded = loaded[0] - # show the repr for loaded - log.debug('loaded {} using {}'.format( - str(loaded), - mesh_loaders[file_type].__name__)) + # show the repr for loaded, loader used, and time + log.debug('loaded {} using `{}` in {:0.4f}s'.format( + str(loaded), loader.__name__, now() - tic)) finally: # if we failed to load close file if opened: diff --git a/trimesh/exchange/urdf.py b/trimesh/exchange/urdf.py index c05141d9b..a05fd5042 100644 --- a/trimesh/exchange/urdf.py +++ b/trimesh/exchange/urdf.py @@ -2,7 +2,7 @@ import numpy as np -from ..constants import log +from ..constants import log, tol from ..decomposition import convex_decomposition from ..version import __version__ as trimesh_version @@ -13,25 +13,28 @@ def export_urdf(mesh, color=[0.75, 0.75, 0.75], **kwargs): """ - Convert a Trimesh object into a URDF package for physics simulation. - This breaks the mesh into convex pieces and writes them to the same - directory as the .urdf file. + Convert a Trimesh object into a URDF package for physics + simulation. This breaks the mesh into convex pieces and + writes them to the same directory as the .urdf file. Parameters --------- - mesh : Trimesh object + mesh : trimesh.Trimesh + Input geometry directory : str - The directory path for the URDF package + The directory path for the URDF package Returns --------- - mesh : Trimesh object - Multi-body mesh containing convex decomposition + mesh : Trimesh + Multi-body mesh containing convex decomposition """ import lxml.etree as et # TODO: fix circular import from .export import export_mesh + from ..resources import get + # Extract the save directory and the file name fullpath = os.path.abspath(directory) name = os.path.basename(fullpath) @@ -158,6 +161,13 @@ def export_urdf(mesh, description.text = name tree = et.ElementTree(root) - tree.write(os.path.join(fullpath, 'model.config')) + if tol.strict: + schema = et.XMLSchema(file=get( + 'schema/urdf.xsd', as_stream=True)) + if not schema.validate(tree): + # actual error isn't raised by validate + raise ValueError(schema.error_log) + + tree.write(os.path.join(fullpath, 'model.config')) return np.sum(convex_pieces) diff --git a/trimesh/path/exchange/load.py b/trimesh/path/exchange/load.py index 49f253d8f..d2d0aec88 100644 --- a/trimesh/path/exchange/load.py +++ b/trimesh/path/exchange/load.py @@ -31,6 +31,10 @@ def load_path(file_obj, file_type=None, **kwargs): path : Path, Path2D, Path3D file_object Data as a native trimesh Path file_object """ + # avoid a circular import + from ...exchange.load import load_kwargs + # record how long we took + tic = util.now() if isinstance(file_obj, Path): # we have been passed a Path file_object so @@ -46,7 +50,8 @@ def load_path(file_obj, file_type=None, **kwargs): # get the file type from the extension file_type = os.path.splitext(file_obj)[-1][1:].lower() # call the loader - kwargs.update(path_loaders[file_type](f, file_type=file_type)) + kwargs.update(path_loaders[file_type]( + f, file_type=file_type)) elif util.is_instance_named(file_obj, ['Polygon', 'MultiPolygon']): # convert from shapely polygons to Path2D kwargs.update(misc.polygon_to_path(file_obj)) @@ -55,7 +60,6 @@ def load_path(file_obj, file_type=None, **kwargs): kwargs.update(misc.linestrings_to_path(file_obj)) elif isinstance(file_obj, dict): # load as kwargs - from ...exchange.load import load_kwargs return load_kwargs(file_obj) elif util.is_sequence(file_obj): # load as lines in space @@ -63,8 +67,11 @@ def load_path(file_obj, file_type=None, **kwargs): else: raise ValueError('Not a supported object type!') - from ...exchange.load import load_kwargs - return load_kwargs(kwargs) + result = load_kwargs(kwargs) + util.log.debug('loaded {} in {:0.4f}s'.format( + str(result), util.now() - tic)) + + return result def path_formats(): diff --git a/trimesh/resources/__init__.py b/trimesh/resources/__init__.py index bcf33dedd..0d8ea9c3d 100644 --- a/trimesh/resources/__init__.py +++ b/trimesh/resources/__init__.py @@ -1,7 +1,7 @@ import os import json -from ..util import decode_text +from ..util import decode_text, wrap_as_stream # find the current absolute path to this directory _pwd = os.path.expanduser(os.path.abspath( @@ -11,7 +11,7 @@ _cache = {} -def get(name, decode=True, decode_json=False): +def get(name, decode=True, decode_json=False, as_stream=False): """ Get a resource from the `trimesh/resources` folder. @@ -23,6 +23,8 @@ def get(name, decode=True, decode_json=False): Whether or not to decode result as UTF-8 decode_json : bool Run `json.loads` on resource if True. + as_stream : bool + Return as a file-like object Returns ------------- @@ -30,10 +32,15 @@ def get(name, decode=True, decode_json=False): File data """ # key by name and decode - cache_key = (name, bool(decode), bool(decode_json)) - if cache_key in _cache: - # return cached resource - return _cache[cache_key] + cache_key = (name, + bool(decode), + bool(decode_json), + bool(as_stream)) + cached = _cache.get(cache_key) + if hasattr(cached, 'seek'): + cached.seek(0) + if cached is not None: + return cached # get the resource using relative names with open(os.path.join(_pwd, name), 'rb') as f: @@ -46,6 +53,8 @@ def get(name, decode=True, decode_json=False): if decode_json: resource = json.loads(resource) + elif as_stream: + resource = wrap_as_stream(resource) # store for later access _cache[cache_key] = resource diff --git a/trimesh/resources/schema/README.md b/trimesh/resources/schema/README.md index f44b9e9e2..13c3ff542 100644 --- a/trimesh/resources/schema/README.md +++ b/trimesh/resources/schema/README.md @@ -1,3 +1,5 @@ # trimesh/resources/schemas -Contains [JSON schema](https://json-schema.org/) for exports. The goal is if there is a JSON export format (like the header of a GLTF file) or a `to_dict` method to have a well-defined schema we can validate in unit tests. \ No newline at end of file +Contain schemas for formats when available. They are currently mostly [JSON schema](https://json-schema.org/) although if formats have an XSD, DTD, or other schema format we are happy to include it here. + +The `primitive` schema directory is a [JSON schema](https://json-schema.org/) for `trimesh` exports. The goal is if we implement a `to_dict` method to have a well-defined schema we can validate in unit tests. diff --git a/trimesh/resources/schema/urdf.xsd b/trimesh/resources/schema/urdf.xsd new file mode 100644 index 000000000..ea21e2f13 --- /dev/null +++ b/trimesh/resources/schema/urdf.xsd @@ -0,0 +1,324 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/trimesh/scene/transforms.py b/trimesh/scene/transforms.py index e132763c9..d9c52a934 100644 --- a/trimesh/scene/transforms.py +++ b/trimesh/scene/transforms.py @@ -675,7 +675,7 @@ def shortest_path(self, u, v): if len(common) == 0: raise ValueError('No path from {}->{}!'.format(u, v)) elif len(common) > 1: - # get the first occuring common element in "forward" + # get the first occurring common element in "forward" link = next(f for f in forward if f in common) assert link in common else: diff --git a/trimesh/util.py b/trimesh/util.py index 81a377616..3296c234b 100644 --- a/trimesh/util.py +++ b/trimesh/util.py @@ -33,12 +33,14 @@ # a flag we can check elsewhere for Python 3 PY3 = sys.version_info.major >= 3 + if PY3: # for type checking basestring = str # Python 3 from io import BytesIO, StringIO from shutil import which # noqa + from time import perf_counter as now # noqa else: # Python 2 from StringIO import StringIO @@ -47,6 +49,7 @@ StringIO.__enter__ = lambda a: a StringIO.__exit__ = lambda a, b, c, d: a.close() BytesIO = StringIO + from time import time as now # noqa try: @@ -2309,7 +2312,7 @@ def decode_text(text, initial='utf-8'): # detect different file encodings import chardet # try to detect the encoding of the file - # only look at the first 1000 charecters otherwise + # only look at the first 1000 characters otherwise # for big files chardet looks at everything and is slow detect = chardet.detect(text[:1000]) # warn on files that aren't UTF-8