import argparse import json import sys from pathlib import Path from . import __version__ from .kernel import build, operator_catalog, load_plugin, validate_bundle from .project import Project, read_json, write_json from .examples import demo_project, small_project def main(argv=None): p=argparse.ArgumentParser(prog='spatial-lab',description='Mathematical geometry workbench; author through commands, inspect in a browser, transfer exact meshes to Blender.') p.add_argument('--version',action='version',version=__version__) p.add_argument('--plugin',action='append',default=[],help='Load a trusted local Python operator module (repeatable)') s=p.add_subparsers(dest='command',required=True) q=s.add_parser('init',help='Create an editable project');q.add_argument('project');q.add_argument('--example',choices=['atrium','simple'],default='atrium') q=s.add_parser('build',help='Evaluate and export the technical bundle');q.add_argument('project');q.add_argument('--out',required=True) q=s.add_parser('inspect',help='Show evaluated nodes and mesh checks');q.add_argument('project');q.add_argument('--node') q=s.add_parser('param',help='Change a global parameter transactionally');q.add_argument('project');q.add_argument('name');q.add_argument('value',help='JSON value, e.g. 12 or "2*pi"') q=s.add_parser('patch',help='Apply an atomic list of graph changes');q.add_argument('project');q.add_argument('changes',help='JSON file containing an array of changes') q=s.add_parser('node',help='Add or replace a node by stable ID');q.add_argument('project');q.add_argument('spec',help='JSON file containing one node') q=s.add_parser('remove',help='Remove a node');q.add_argument('project');q.add_argument('node_id');q.add_argument('--cascade',action='store_true') for name in ['undo','redo']: q=s.add_parser(name);q.add_argument('project') s.add_parser('ops',help='List installed operators and their versions') q=s.add_parser('check',help='Check bundle integrity');q.add_argument('bundle') q=s.add_parser('serve',help='Run the read-only, live geometry viewer');q.add_argument('project');q.add_argument('--port',type=int,default=8767) a=p.parse_args(argv) try: for plugin in a.plugin:load_plugin(plugin) if a.command=='ops':result=operator_catalog() elif a.command=='init': if Path(a.project).exists():raise ValueError('Project already exists; choose another path') project=demo_project() if a.example=='atrium' else small_project();b=build(project);write_json(a.project,project);result={'project':str(Path(a.project).resolve()),**b['stats']} elif a.command=='build': b=Project(a.project).build();write_json(a.out,b);result={'bundle':str(Path(a.out).resolve()),'hash':b['bundle_hash'],**b['stats'],'warnings':b['validation']['warnings']} elif a.command=='inspect': b=Project(a.project).build() if a.node: result=next((n for n in b['nodes'] if n['id']==a.node),None) if result is None:raise ValueError('No such node: '+a.node) else:result={'name':b['name'],'parameters':b['recipe'].get('parameters',{}),'nodes':b['nodes'],'stats':b['stats'],'bounds':b['bounds'],'validation':b['validation']} elif a.command=='check': b=read_json(a.bundle);validate_bundle(b);result={'valid':True,'hash':b['bundle_hash'],'stats':b['stats']} elif a.command=='serve': from .server import serve serve(a.project,a.port);return 0 else: project=Project(a.project) if a.command in ['undo','redo']:b=project.travel(a.command) else: if a.command=='patch':changes=read_json(a.changes) elif a.command=='node':changes=[{'action':'upsert_node','node':read_json(a.spec)}] elif a.command=='remove':changes=[{'action':'remove_node','id':a.node_id,'cascade':a.cascade}] elif a.command=='param':changes=[{'action':'set_parameter','name':a.name,'value':json.loads(a.value)}] b=project.apply(changes) result={'action':a.command,'project_hash':b['project_hash'],**b['stats'],'warnings':b['validation']['warnings']} print(json.dumps(result,indent=2,allow_nan=False));return 0 except (ValueError,OSError,KeyError,TypeError) as e: print(json.dumps({'error':str(e)}),file=sys.stderr);return 2 if __name__=='__main__':raise SystemExit(main())