Upstream Snapshot bb87928bce Import upstream snapshot 80e710700aac9573a2230f74f7ce9e094833a0bc
Upstream: https://github.com/Rhoban/onshape-to-robot
Upstream-Commit: 80e710700aac9573a2230f74f7ce9e094833a0bc
Upstream-Branch: master
2026-08-28 15:42:31 +08:00

209 lines
7.6 KiB
Python

from __future__ import annotations
import numpy as np
import re
import os
import commentjson as json
class Config:
def __init__(self, robot_path: str, safe: bool = False):
self.safe: bool = safe
self.config_file: str = robot_path
if os.path.isdir(robot_path):
self.config_file += os.path.sep + "config.json"
# Loading JSON configuration
if not os.path.exists(self.config_file):
raise Exception(f"ERROR: The file {self.config_file} can't be found")
with open(self.config_file, "r", encoding="utf8") as stream:
self.config: dict = json.load(stream)
# Loaded processors
self.processors: list = []
self.read_configuration()
# Output directory, making it if it doesn't exists
self.output_directory: str = os.path.dirname(os.path.abspath(self.config_file))
if self.robot_name is None:
self.robot_name = os.path.dirname(os.path.abspath(self.config_file)).split(
"/"
)[-1]
try:
os.makedirs(self.output_directory)
except OSError:
pass
def to_camel_case(self, snake_str: str) -> str:
"""
Converts a string to camel case
"""
components = snake_str.split("_")
return components[0] + "".join(x.title() for x in components[1:])
def get(self, name: str, default=None, required: bool = True, values_list=None):
"""
Gets an entry from the configuration
Args:
name (str): entry name
default: default fallback value if the entry is not present. Defaults to None.
required (bool, optional): whether the configuration entry is required. Defaults to False.
values_list: list of allowed values. Defaults to None.
"""
camel_name = self.to_camel_case(name)
if name in self.config or camel_name in self.config:
if name in self.config:
value = self.config[name]
else:
value = self.config[camel_name]
if values_list is not None and value not in values_list:
raise Exception(
f"Value for {name} should be onf of: {','.join(values_list)}"
)
return value
elif required and default is None:
raise Exception(f"ERROR: missing required key {name} in config")
return default
def printable_version(self) -> str:
if self.url is not None:
return self.url
else:
version = f"document_id: {self.document_id}"
if self.version_id:
version += f" / version_id: {self.version_id}"
elif self.workspace_id:
version += f" / workspace_id: {self.workspace_id}"
return version
def parse_url(self):
pattern = "https://(.*)/(.*)/([wv])/(.*)/e/(.*)"
match = re.match(pattern, self.url)
if match is None:
raise Exception(f"Invalid URL: {self.url}")
match_groups = match.groups()
self.document_id = match_groups[1]
if match_groups[2] == "w":
self.workspace_id = match_groups[3]
elif match_groups[2] == "v":
self.version_id = match_groups[3]
self.element_id = match_groups[4]
def asset_path(self, asset_name: str) -> str:
return f"{self.output_directory}/{self.assets_directory}/{asset_name}"
def read_configuration(self):
"""
Load and check configuration entries
"""
# Robot name
self.robot_name: str = self.get("robot_name", None, required=False)
self.output_filename: str = self.get("output_filename", "robot")
# Securing filename
self.output_filename = "".join(
c for c in self.output_filename if c.isalnum() or c in ("_", "-")
).rstrip()
self.assets_directory: str = self.get("assets_directory", "assets")
# Main settings
self.document_id: str = self.get("document_id", required=False)
self.version_id: str | None = self.get("version_id", required=False)
self.workspace_id: str | None = self.get("workspace_id", required=False)
self.element_id: str | None = self.get("element_id", required=False)
if self.version_id and self.workspace_id:
raise Exception("You can't specify workspace_id and version_id")
self.url: str = self.get("url", None, required=False)
if self.url is not None:
self.parse_url()
if self.url is None and self.document_id is None:
raise Exception("You need to specify either a url or a document_id")
self.draw_frames: bool = self.get("draw_frames", False)
self.assembly_name: str = self.get("assembly_name", required=False)
self.output_format: str = self.get("output_format")
self.configuration: str | dict = self.get("configuration", "default")
self.ignore_limits: bool = self.get("ignore_limits", False)
if isinstance(self.configuration, dict):
self.configuration = ";".join(
[f"{k}={v}" for k, v in self.configuration.items()]
)
# Joint specs
self.joint_properties: dict = self.get("joint_properties", {})
self.geom_properties: dict = self.get("geom_properties", {})
self.no_dynamics: bool = self.get("no_dynamics", False)
# Ignore / whitelists
self.ignore: list[str] = self.get("ignore", {})
if isinstance(self.ignore, list):
self.ignore = {entry: "all" for entry in self.ignore}
# Color override
self.color: str | None = self.get("color", required=False)
# Post-import commands
self.post_import_commands: list[str] = self.get("post_import_commands", [])
# Whether to include configuration suffix in part names
self.include_configuration_suffix: bool = self.get(
"include_configuration_suffix", True
)
# Number of decimals to keep for small numbers
self.round_decimals = self.get("round_decimals", 12)
# Loading processors
from . import processors
loaded_modules = {}
processors_list: list[str] | None = self.get("processors", None, required=False)
if processors_list is None or self.safe:
self.processors = [
processor(self)
for processor in processors.default_processors
if (processor.is_safe or not self.safe)
]
else:
for entry in processors_list:
parts = entry.split(":")
if len(parts) == 1:
processor = eval(f"processors.{entry}")
else:
module, cls = parts
if module not in loaded_modules:
loaded_modules[module] = __import__(module, fromlist=[cls])
processor = getattr(loaded_modules[module], cls)
if processor is None:
raise Exception(f"ERROR: Processor {entry} not found")
self.processors.append(processor(self))
def round(self, object: float | list | tuple | np.ndarray):
"""
Round the given number or list of numbers using the configuration decimals
"""
if isinstance(object, float):
return round(object, self.round_decimals)
elif isinstance(object, np.ndarray):
return object.round(self.round_decimals)
else:
original_type = type(object)
return original_type(np.array(object).round(self.round_decimals))