forked from 1b-agent/opencadstudio-upstream
186 lines
6.5 KiB
Python
186 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate a Python facade over a caller-supplied document transport.
|
|
|
|
Usage: python generate.py ../../src/gen/binding_schema.json > ocs_doc.py
|
|
The transport converts these Python DTOs to/from the Rust wire representation.
|
|
"""
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
HEADER = '''"""Generated document facade. See bindings/README.md for the transport contract."""
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
@dataclass
|
|
class DocApiEnvelope:
|
|
version: int
|
|
body: Any
|
|
|
|
@dataclass
|
|
class Receipt:
|
|
outcome: Any
|
|
query_results: list
|
|
new_revision: int
|
|
|
|
@dataclass
|
|
class Entity:
|
|
id: int
|
|
_doc: "Document"
|
|
|
|
def bounds(self):
|
|
return self._doc._apply_query({"GetBounds": {"id": self.id}})
|
|
|
|
def transform(self, placement):
|
|
self._doc._apply_op({"Transform": {"id": self.id, "placement": placement}})
|
|
|
|
def delete(self):
|
|
self._doc._apply_op({"Delete": {"id": self.id}})
|
|
|
|
def layer(self):
|
|
return self._doc._apply_query({"GetEntityLayer": {"id": self.id}})
|
|
|
|
def set_layer(self, layer):
|
|
self._doc._apply_op({"SetEntityLayer": {"id": self.id, "layer": layer}})
|
|
'''
|
|
|
|
DOCUMENT = '''
|
|
@dataclass
|
|
class Document:
|
|
_transport: Any
|
|
_tab: int
|
|
|
|
def _id(self, value):
|
|
if isinstance(value, Entity):
|
|
if value._doc._transport is not self._transport or value._doc._tab != self._tab:
|
|
raise ValueError("handle belongs to another document")
|
|
return value.id
|
|
return int(value)
|
|
|
|
def _apply_op(self, op, want=None):
|
|
receipt = self._transport.apply(DocApiEnvelope(ENVELOPE_VERSION, ("Op", op)))
|
|
if want is None:
|
|
return None
|
|
if want.startswith("Vec<"):
|
|
handle = globals()[want[4:-1]]
|
|
return [handle(i, self) for i in receipt.outcome["NewIds"]]
|
|
return globals()[want](receipt.outcome["NewId"], self)
|
|
|
|
def _apply_query(self, query):
|
|
receipt = self._transport.apply(DocApiEnvelope(ENVELOPE_VERSION, ("Queries", [query])))
|
|
if len(receipt.query_results) != 1:
|
|
raise ValueError("expected one query result")
|
|
result = receipt.query_results[0]
|
|
if not isinstance(result, dict) or len(result) != 1:
|
|
raise ValueError("invalid query result")
|
|
return next(iter(result.values()))
|
|
|
|
def revision(self):
|
|
return self._apply_query("GetGeometryRevision")
|
|
|
|
def block_entities(self, block_name):
|
|
return self._apply_query({"GetBlockEntities": {"block_name": block_name}})
|
|
|
|
def solids(self): return Solids(self)
|
|
def curves(self): return Curves(self)
|
|
def entities(self): return Entities(self)
|
|
|
|
def layers(self):
|
|
return self._apply_query({"ListLayers": {}})
|
|
|
|
def create_layer(self, info):
|
|
self._apply_op({"CreateLayer": info})
|
|
|
|
def update_layer(self, name, info):
|
|
self._apply_op({"UpdateLayer": {"name": name, "info": info}})
|
|
|
|
def delete_layer(self, name):
|
|
self._apply_op({"DeleteLayer": {"name": name}})
|
|
|
|
@dataclass
|
|
class DocApi:
|
|
_transport: Any
|
|
active_tab: int = 0
|
|
|
|
def document(self, tab):
|
|
if tab != self.active_tab:
|
|
raise ValueError("transport is bound to another tab")
|
|
return Document(self._transport, tab)
|
|
'''
|
|
|
|
|
|
def render_method(method, constructor=False):
|
|
args = method.get("args", [])
|
|
signature = "".join(", " + a["name"] for a in args)
|
|
variant = method.get("op") or method["query"]
|
|
fields = {}
|
|
for arg in args:
|
|
name, ty = arg["name"], arg["ty"]
|
|
if ty in ("EntityRef", "Solid", "ObjectId"):
|
|
fields[name] = f"self._doc._id({name})"
|
|
elif ty == "&[EntityRef]":
|
|
fields[name] = f"[self._doc._id(p) for p in {name}]"
|
|
else:
|
|
fields[name] = name
|
|
fixed = dict(method.get("fixed") or {})
|
|
if variant in ("SolidBoolean", "GetIntersects"):
|
|
fields = {"a": "self.id", "b": fields.pop("other"), **fields}
|
|
fields.pop("other", None)
|
|
elif not constructor:
|
|
fields = {"id": "self.id", **fields}
|
|
if variant == "Revolve":
|
|
fields["axis"] = "(pivot, axis)"
|
|
fields.pop("pivot")
|
|
nested = fixed.pop("primitive", fixed.pop("curve", None))
|
|
fields.update({key: repr(value) for key, value in fixed.items()})
|
|
payload = "{" + ", ".join(repr(k) + ": " + v for k, v in fields.items()) + "}"
|
|
if variant == "CreateMany":
|
|
payload = "[{'Curve': {'Point': {'position': p}}} for p in positions]"
|
|
elif nested:
|
|
payload = "{" + repr(nested) + ": " + payload + "}"
|
|
message = "{" + repr(variant) + ": " + payload + "}"
|
|
if method.get("kind") == "query":
|
|
call = f"self._doc._apply_query({message})"
|
|
else:
|
|
want = method.get("returns")
|
|
if want == "()":
|
|
want = None
|
|
call = f"self._doc._apply_op({message}, {want!r})"
|
|
return f" def {method['name']}(self{signature}):\n return {call}\n"
|
|
|
|
|
|
def render(schema):
|
|
out = [HEADER, f"ENVELOPE_VERSION = {schema['envelope_version']}\n"]
|
|
handles = {name: {} for name in schema["object_model"]["handles"]}
|
|
collections = {"solids": {}, "curves": {}, "entities": {}}
|
|
for family in schema["families"]:
|
|
handle = family.get("handle") or family["name"]
|
|
for method in family.get("methods", []):
|
|
existing = handles[handle].get(method["name"])
|
|
if existing is not None and existing != method:
|
|
raise ValueError(f"conflicting method: {handle}.{method['name']}")
|
|
handles[handle][method["name"]] = method
|
|
for constructor in family.get("constructors", []):
|
|
collections[family["collection"]][constructor["name"]] = constructor
|
|
# Families sharing Entity contribute to one class, preserving all methods.
|
|
entity_methods = "\n".join(render_method(m) for m in handles.pop("Entity").values())
|
|
out[0] += entity_methods
|
|
for handle, methods in handles.items():
|
|
out.append(f"\nclass {handle}(Entity):\n" + (
|
|
"\n".join(render_method(m) for m in methods.values()) or " pass\n"))
|
|
for collection, constructors in collections.items():
|
|
out.append(f"\nclass {collection.title()}:\n def __init__(self, doc):\n self._doc = doc\n")
|
|
out.extend(render_method(c, constructor=True) for c in constructors.values())
|
|
out.append(DOCUMENT)
|
|
return "\n".join(out)
|
|
|
|
|
|
def main():
|
|
default = Path(__file__).resolve().parents[2] / "src/gen/binding_schema.json"
|
|
path = Path(sys.argv[1]) if len(sys.argv) > 1 else default
|
|
sys.stdout.write(render(json.loads(path.read_text(encoding="utf-8"))))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|