├── .gitignore ├── LICENSE.md ├── README.md ├── docs ├── README.md ├── assembly.md ├── assembly_joint.md ├── images │ ├── assembly_contacts.jpg │ ├── assembly_graph.jpg │ ├── assembly_joint_graph.png │ ├── assembly_joint_labels.jpg │ ├── assembly_joint_mosaic.jpg │ ├── assembly_joint_motion_types.jpg │ ├── assembly_joint_set.jpg │ ├── assembly_mosaic.jpg │ ├── fusion_gallery_mosaic.jpg │ ├── reconstruction_extrude_operations.png │ ├── reconstruction_extrude_types.png │ ├── reconstruction_mosaic.jpg │ ├── reconstruction_overview_extrude.png │ ├── reconstruction_overview_sequence.png │ ├── reconstruction_overview_sketch.png │ ├── reconstruction_sketch_entities.png │ ├── reconstruction_teaser.jpg │ ├── reconstruction_timeline_icons.png │ ├── segmentation_example.jpg │ ├── segmentation_extended_dataset_stats.jpg │ ├── segmentation_features_removed.jpg │ ├── segmentation_mosaic.jpg │ ├── segmentation_start_end_side_faces.jpg │ └── segmentation_timeline.png ├── reconstruction.md ├── reconstruction_stats.md ├── segmentation.md └── tags.json └── tools ├── README.md ├── assembly2cad ├── README.md ├── assembly2cad.manifest └── assembly2cad.py ├── assembly_download ├── README.md └── assembly_download.py ├── assembly_graph ├── README.md ├── assembly2graph.py ├── assembly_graph.py └── assembly_viewer.ipynb ├── common ├── __init__.py ├── assembly_importer.py ├── deserialize.py ├── exceptions.py ├── exporter.py ├── face_reconstructor.py ├── fusion_360_server.manifest ├── geometry.py ├── joint_importer.py ├── launcher.py ├── logger.py ├── match.py ├── name.py ├── regraph.py ├── serialize.py ├── sketch_extrude_importer.py ├── test │ ├── common_test_base.py │ ├── common_test_harness.manifest │ ├── common_test_harness.py │ └── test_geometry.py └── view_control.py ├── fusion360gym ├── README.md ├── __init__.py ├── client │ ├── __init__.py │ ├── fusion360gym_client.py │ └── gym_env.py ├── examples │ ├── README.md │ ├── client_example.py │ ├── face_extrusion_example.py │ ├── sketch_extrusion_example.py │ └── sketch_extrusion_point_example.py ├── server │ ├── .gitignore │ ├── __init__.py │ ├── command_base.py │ ├── command_export.py │ ├── command_face_extrusion.py │ ├── command_reconstruct.py │ ├── command_runner.py │ ├── command_sketch_extrusion.py │ ├── design_state.py │ ├── fusion360gym_server.manifest │ ├── fusion360gym_server.py │ └── launch.py └── test │ ├── .gitignore │ ├── README.md │ ├── common_test.py │ ├── test_detach_util.py │ ├── test_fusion360gym_export.py │ ├── test_fusion360gym_face_extrusion.py │ ├── test_fusion360gym_randomized_reconstruction.py │ ├── test_fusion360gym_reconstruct.py │ ├── test_fusion360gym_server.py │ └── test_fusion360gym_sketch_extrusion.py ├── joint2cad ├── README.md ├── joint2cad.manifest └── joint2cad.py ├── reconverter ├── .gitignore ├── README.md ├── reconverter.manifest └── reconverter.py ├── regraph ├── README.md ├── launch.py ├── regraph_exporter.manifest ├── regraph_exporter.py └── regraph_viewer.ipynb ├── regraphnet ├── README.md ├── ckpt │ ├── model_gat.ckpt │ ├── model_gcn.ckpt │ ├── model_gcn_aug.ckpt │ ├── model_gcn_semisyn.ckpt │ ├── model_gcn_syn.ckpt │ ├── model_gin.ckpt │ ├── model_mlp.ckpt │ └── model_mlp_aug.ckpt ├── data │ ├── 31962_e5291336_0054_0000.json │ ├── 31962_e5291336_0054_0001.json │ ├── 31962_e5291336_0054_0002.json │ └── 31962_e5291336_0054_sequence.json └── src │ ├── inference.py │ ├── inference_torch_geometric.py │ ├── inference_vanilla.py │ ├── train.py │ ├── train_torch_geometric.py │ └── train_vanilla.py ├── search ├── .gitignore ├── README.md ├── agent.py ├── agent_random.py ├── agent_supervised.py ├── evaluation │ └── evaluation.ipynb ├── log.py ├── main.py ├── repl_env.py ├── search.py ├── search_beam.py ├── search_best.py └── search_random.py ├── segmentation_viewer ├── README.md ├── segmentation_viewer.py └── segmentation_viewer_demo.ipynb ├── sketch2image ├── README.md ├── sketch2image.py └── sketch_plotter.py └── testdata ├── .gitignore ├── Box.smt ├── Boxes.smt ├── Couch.json ├── Couch.smt ├── Couch.step ├── Hexagon.json ├── SingleSketchExtrude.json ├── SingleSketchExtrude_Invalid.json ├── assembly_examples └── belt_clamp │ ├── 8b4e1828-b296-11eb-9d3c-f21898acd3b7.smt │ ├── 8b4e5752-b296-11eb-9d3c-f21898acd3b7.smt │ └── assembly.json ├── common ├── BooleanAdjacent.f3d ├── BooleanExactOverlap.f3d ├── BooleanIntersectContained.f3d ├── BooleanIntersectDouble.f3d ├── BooleanIntersectMultiOverlap.f3d ├── BooleanIntersectMultiTool.f3d ├── BooleanIntersectOverlap.f3d ├── BooleanIntersectOverlapSelfIntersect.f3d ├── BooleanIntersectSeparate.f3d ├── BooleanOverlap.f3d ├── BooleanOverlap3Way.f3d ├── BooleanOverlapDouble.f3d └── BooleanSeparate.f3d ├── joint_examples ├── 145132_7aea3b66_0024_1.smt ├── 145132_7aea3b66_0024_2.smt └── joint_set_00119.json └── segmentation_examples ├── 102673_56775b8e_5.obj └── 102673_56775b8e_5.seg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode/ 3 | __pycache__/ 4 | .ipynb_checkpoints/ 5 | .env -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | # Fusion 360 Gallery Dataset License 3 | 4 | The dataset to which this license is attached is data from the Fusion 360 Gallery Dataset (the "Dataset") provided by Autodesk, Inc. (“Autodesk”). You may only access and/or use the Dataset subject to the following terms and conditions (this “License”): 5 | 6 | 1. You may access, use, reproduce and modify the Dataset, in each case, only for non-commercial research purposes. 7 | 8 | 2. You may not redistribute or make available to others the Dataset in its entirety; however you may direct others to https://github.com/AutodeskAILab/Fusion360GalleryDataset to obtain the Dataset. 9 | 10 | 3. You may redistribute or make available to others portions of the Dataset or modifications of the Dataset (your “Modified Set”) only if you adhere to the following requirements: 11 | 12 | 3.1. You do not explicitly or implicitly represent that that your Modified Set is the Dataset itself. One way to satisfy this requirement is, in connection with such redistribution, to prominently indicate that your Modified Set is a portion or modification of the Dataset and to provide the attribution in Item 5 below. 13 | 14 | 3.2. You may not allow others to access, use, reproduce or modify the Modified Set except for non-commercial research purposes. 15 | 16 | 3.3. If you allow others to redistribute or make available the Modified Set (or their modifications to the Modified Set), you must require them to: 17 | (i) do so only for non-commercial research purposes; 18 | (ii) restrict their recipients to non-commercial research purposes through terms at least as restrictive as provided in these Items 3.2 and 3.3; 19 | (iii) include terms at least as protective of Autodesk as provided in Items 7 and 8; and 20 | (iv) retain any notices or attributions associated with the Dataset that were provided by you (and to impose this requirement on any downstream recipients, if any). 21 | 22 | 4. You will comply with applicable data security and privacy laws. You shall not attempt to re-associate any model in the Dataset with the creator of the model or any identifiable individual. 23 | 24 | 5. At your discretion as subject to general principles of academic attribution, if your use of the Dataset was a substantial contributor to your research, provide attribution to the “Fusion 360 Gallery Dataset” and the relevant citations provided on the Dataset website. 25 | 26 | 6. This License does not grant you permission to use any Autodesk trade names, trademarks, service marks, or product names, except as required for reasonable and customary use in describing the origin of the Dataset and satisfying the request for attribution in Item 5 above. 27 | 28 | 7. Autodesk makes no representations or warranties regarding the Dataset, including but not limited to warranties of non-infringement, merchantability or fitness for a particular purpose. 29 | 30 | 8. You accept full responsibility for your use of the Dataset and shall defend and indemnify Autodesk, Inc. including its employees, officers and agents, against any and all claims arising from your use of the Dataset, including but not limited to your use of any copies of copyrighted images that you may create from the Dataset. 31 | 32 | 9. Autodesk reserves the right to terminate this license at any time and may cease access to the Dataset at any time in its sole discretion. 33 | 34 | 10. All rights and licenses to the Dataset not explicitly provided hereunder are reserved for Autodesk. 35 | 36 | 11. If you are employed by a for-profit, commercial entity, your employer shall also be bound by this License, and you hereby represent that you are fully authorized to enter into this License on behalf of such employer. 37 | 38 | 12. The laws of the State of California shall apply to all disputes under this License. 39 | 40 | _Updated 11/2021_ 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fusion 360 Gallery Dataset 2 | ![Fusion 360 Gallery Dataset](docs/images/fusion_gallery_mosaic.jpg) 3 | 4 | The *Fusion 360 Gallery Dataset* contains rich 2D and 3D geometry data derived from parametric CAD models. The dataset is produced from designs submitted by users of the CAD package [Autodesk Fusion 360](https://www.autodesk.com/products/fusion-360/overview) to the [Autodesk Online Gallery](https://gallery.autodesk.com/fusion360). The dataset provides valuable data for learning how people design, including sequential CAD design data, designs segmented by modeling operation, and assemblies containing hierarchy and joint connectivity information. 5 | 6 | ## Datasets 7 | From the approximately 20,000 designs available we derive several datasets focused on specific areas of research. Currently the following data subsets are available, with more to be released on an ongoing basis. 8 | 9 | ### [Assembly Dataset](docs/assembly.md) - NEW! 10 | Multi-part CAD assemblies containing rich information on joints, contact surfaces, holes, and the underlying assembly graph structure. 11 | 12 | ![Fusion 360 Gallery Assembly Dataset](docs/images/assembly_mosaic.jpg) 13 | 14 | 15 | ### [Reconstruction Dataset](docs/reconstruction.md) 16 | Sequential construction sequence information from a subset of simple 'sketch and extrude' designs. 17 | 18 | ![Fusion 360 Gallery Reconstruction Dataset](docs/images/reconstruction_teaser.jpg) 19 | 20 | ### [Segmentation Dataset](docs/segmentation.md) 21 | 22 | A segmentation of 3D models based on the modeling operation used to create each face, e.g. Extrude, Fillet, Chamfer etc... 23 | 24 | ![Fusion 360 Gallery Segmentation Dataset](docs/images/segmentation_example.jpg) 25 | 26 | 27 | ## Publications 28 | Please cite the relevant paper below if you use the Fusion 360 Gallery dataset in your research. 29 | 30 | ### Assembly Dataset 31 | [JoinABLe: Learning Bottom-up Assembly of Parametric CAD Joints](https://arxiv.org/abs/2111.12772) 32 | 33 | ``` 34 | @article{willis2021joinable, 35 | title={JoinABLe: Learning Bottom-up Assembly of Parametric CAD Joints}, 36 | author={Willis, Karl DD and Jayaraman, Pradeep Kumar and Chu, Hang and Tian, Yunsheng and Li, Yifei and Grandi, Daniele and Sanghi, Aditya and Tran, Linh and Lambourne, Joseph G and Solar-Lezama, Armando and Matusik, Wojciech}, 37 | journal={arXiv preprint arXiv:2111.12772}, 38 | year={2021} 39 | } 40 | ``` 41 | 42 | ### Reconstruction Dataset 43 | [Fusion 360 Gallery: A Dataset and Environment for Programmatic CAD Construction from Human Design Sequences](https://arxiv.org/abs/2010.02392) 44 | ``` 45 | @article{willis2020fusion, 46 | title={Fusion 360 Gallery: A Dataset and Environment for Programmatic CAD Construction from Human Design Sequences}, 47 | author={Karl D. D. Willis and Yewen Pu and Jieliang Luo and Hang Chu and Tao Du and Joseph G. Lambourne and Armando Solar-Lezama and Wojciech Matusik}, 48 | journal={ACM Transactions on Graphics (TOG)}, 49 | volume={40}, 50 | number={4}, 51 | year={2021}, 52 | publisher={ACM New York, NY, USA} 53 | } 54 | ``` 55 | 56 | ### Segmentation Dataset 57 | [BRepNet: A Topological Message Passing System for Solid Models](https://arxiv.org/abs/2104.00706) 58 | ``` 59 | @inproceedings{lambourne2021brepnet, 60 | author = {Lambourne, Joseph G. and Willis, Karl D.D. and Jayaraman, Pradeep Kumar and Sanghi, Aditya and Meltzer, Peter and Shayani, Hooman}, 61 | title = {BRepNet: A Topological Message Passing System for Solid Models}, 62 | booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)}, 63 | month = {June}, 64 | year = {2021}, 65 | pages = {12773-12782} 66 | } 67 | ``` 68 | 69 | ## Download 70 | 71 | | Dataset | Designs | Documentation | Download | Paper | Code | 72 | | - | - | - | - | - | - | 73 | | Assembly | 8,251 assemblies / 154,468 parts | [Documentation](docs/assembly.md) | [Instructions](tools/assembly_download) | [Paper](https://arxiv.org/abs/2111.12772) | [Code](tools) | 74 | | Assembly - Joint | 32,148 joints / 23,029 parts | [Documentation](docs/assembly_joint.md) | [j1.0.0 - 2.8 GB](https://fusion-360-gallery-dataset.s3.us-west-2.amazonaws.com/assembly/j1.0.0/j1.0.0.7z) | [Paper](https://arxiv.org/abs/2111.12772) | [Code](https://github.com/AutodeskAILab/JoinABLe) | 75 | | Reconstruction | 8,625 sequences | [Documentation](docs/reconstruction.md) | [r1.0.1 - 2.0 GB](https://fusion-360-gallery-dataset.s3.us-west-2.amazonaws.com/reconstruction/r1.0.1/r1.0.1.zip) | [Paper](https://arxiv.org/abs/2010.02392) | [Code](tools) | 76 | | Segmentation | 35,680 parts | [Documentation](docs/segmentation.md) | [s2.0.1 - 3.1 GB](https://fusion-360-gallery-dataset.s3.us-west-2.amazonaws.com/segmentation/s2.0.1/s2.0.1.zip) | [Paper](https://arxiv.org/abs/2104.00706) | [Code](https://github.com/AutodeskAILab/BRepNet) 77 | 78 | ### Additional Downloads 79 | - **Reconstruction Dataset Extrude Volumes** [(r1.0.1 - 152 MB)](https://fusion-360-gallery-dataset.s3.us-west-2.amazonaws.com/reconstruction/r1.0.1/r1.0.1_extrude_tools.zip): The extrude volumes for each extrude operation in the design timeline. 80 | - **Reconstruction Dataset Face Extrusion Sequences** [(r1.0.1 - 41MB)](https://fusion-360-gallery-dataset.s3.us-west-2.amazonaws.com/reconstruction/r1.0.1/r1.0.1_regraph_05.zip): The pre-processed face extrusion sequences used to train our [reconstruction network](tools/regraphnet). 81 | - **Segmentation Extended STEP Dataset** [(s2.0.1 - 483 MB)](https://fusion-360-gallery-dataset.s3.us-west-2.amazonaws.com/segmentation/s2.0.1/s2.0.1_extended_step.zip): An extended collection of 42,912 STEP files with associated segmentation information. This include all STEP data from s2.0.0 along with additional files for which triangle meshes with close to 2500 edges could not be created. 82 | 83 | ## Tools 84 | As part of the dataset we provide various tools for working with the data. These tools leverage the [Fusion 360 API](http://help.autodesk.com/view/fusion360/ENU/?guid=GUID-7B5A90C8-E94C-48DA-B16B-430729B734DC) to perform operations such as geometry reconstruction, traversing B-Rep data structures, and conversion to other formats. More information can be found in the [tools directory](tools). 85 | 86 | 87 | ## License 88 | Please refer to the [dataset license](LICENSE.md). 89 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Fusion 360 Gallery Dataset Documentation 2 | Here you will find documentation of the data released as part of the Fusion 360 Gallery Dataset. Each dataset is extracted from designs created in [Fusion 360](https://www.autodesk.com/products/fusion-360/overview) and then posted to the [Autodesk Online Gallery](https://gallery.autodesk.com/) by users. 3 | 4 | 5 | ## Datasets 6 | We derive several datasets focused on specific areas of research. The 3D model content between data subsets will overlap as they are drawn from the same source, but will likely be formatted differently. We provide the following datasets. 7 | 8 | ### [Assembly Dataset](assembly.md) 9 | The Assembly Dataset contains multi-part CAD assemblies with rich information on joints, contact surfaces, holes, and the underlying assembly graph structure. 10 | ![Assembly Dataset](images/assembly_mosaic.jpg) 11 | 12 | ### [Assembly Dataset - Joint Data](assembly_joint.md) 13 | The Assembly Dataset joint data contains pairs of parts extracted from CAD assemblies, with one or more joints defined between them. 14 | ![Assembly Dataset - Joint Data](images/assembly_joint_mosaic.jpg) 15 | 16 | ### [Reconstruction Dataset](reconstruction.md) 17 | The Reconstruction Dataset contains construction sequence information from a subset of simple 'sketch and extrude' designs. 18 | ![Reconstruction Dataset](images/reconstruction_mosaic.jpg) 19 | 20 | ### [Segmentation Dataset](segmentation.md) 21 | The Segmentation Dataset contains segmented 3D models based on the modeling operation used to create each face, e.g. Extrude, Fillet, Chamfer etc... 22 | ![Segmentation Dataset](images/segmentation_mosaic.jpg) 23 | 24 | 25 | ## Tools 26 | We provide [tools](../tools) to work with the data using Fusion 360. Full documentation of how to use these tools and write your own is provided in the [tools](../tools) directory. 27 | -------------------------------------------------------------------------------- /docs/images/assembly_contacts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/assembly_contacts.jpg -------------------------------------------------------------------------------- /docs/images/assembly_graph.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/assembly_graph.jpg -------------------------------------------------------------------------------- /docs/images/assembly_joint_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/assembly_joint_graph.png -------------------------------------------------------------------------------- /docs/images/assembly_joint_labels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/assembly_joint_labels.jpg -------------------------------------------------------------------------------- /docs/images/assembly_joint_mosaic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/assembly_joint_mosaic.jpg -------------------------------------------------------------------------------- /docs/images/assembly_joint_motion_types.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/assembly_joint_motion_types.jpg -------------------------------------------------------------------------------- /docs/images/assembly_joint_set.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/assembly_joint_set.jpg -------------------------------------------------------------------------------- /docs/images/assembly_mosaic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/assembly_mosaic.jpg -------------------------------------------------------------------------------- /docs/images/fusion_gallery_mosaic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/fusion_gallery_mosaic.jpg -------------------------------------------------------------------------------- /docs/images/reconstruction_extrude_operations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/reconstruction_extrude_operations.png -------------------------------------------------------------------------------- /docs/images/reconstruction_extrude_types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/reconstruction_extrude_types.png -------------------------------------------------------------------------------- /docs/images/reconstruction_mosaic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/reconstruction_mosaic.jpg -------------------------------------------------------------------------------- /docs/images/reconstruction_overview_extrude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/reconstruction_overview_extrude.png -------------------------------------------------------------------------------- /docs/images/reconstruction_overview_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/reconstruction_overview_sequence.png -------------------------------------------------------------------------------- /docs/images/reconstruction_overview_sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/reconstruction_overview_sketch.png -------------------------------------------------------------------------------- /docs/images/reconstruction_sketch_entities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/reconstruction_sketch_entities.png -------------------------------------------------------------------------------- /docs/images/reconstruction_teaser.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/reconstruction_teaser.jpg -------------------------------------------------------------------------------- /docs/images/reconstruction_timeline_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/reconstruction_timeline_icons.png -------------------------------------------------------------------------------- /docs/images/segmentation_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/segmentation_example.jpg -------------------------------------------------------------------------------- /docs/images/segmentation_extended_dataset_stats.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/segmentation_extended_dataset_stats.jpg -------------------------------------------------------------------------------- /docs/images/segmentation_features_removed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/segmentation_features_removed.jpg -------------------------------------------------------------------------------- /docs/images/segmentation_mosaic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/segmentation_mosaic.jpg -------------------------------------------------------------------------------- /docs/images/segmentation_start_end_side_faces.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/segmentation_start_end_side_faces.jpg -------------------------------------------------------------------------------- /docs/images/segmentation_timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/docs/images/segmentation_timeline.png -------------------------------------------------------------------------------- /docs/reconstruction_stats.md: -------------------------------------------------------------------------------- 1 | # Reconstruction Dataset Statistics 2 | 3 | ## Design Complexity 4 | A key goal of the reconstruction dataset is to provide a suitably scoped baseline for learning-based approaches to CAD reconstruction. Restricting the modeling operations to _sketch_ and _extrude_ vastly narrows the design space and enables simpler approaches for reconstruction. Each design represents a component in Fusion 360 that can have multiple geometric bodies. The vast majority of designs have a single body. 5 | 6 | ![Body Count Per Design](https://i.gyazo.com/01db2404d160935e2020d88383ba51f8.png) 7 | 8 | The number of B-Rep faces in each design gives a good indication of the complexity of the dataset. Below we show the number of faces per design as a distribution, with the peak being between 5-10 faces per design. As we do not filter any of the designs based on complexity, this distribution reflects real designs where simple washers and flat plates are common components in mechanical assemblies. 9 | 10 | ![Face Count Per Design](https://i.gyazo.com/124d7bcb0a9760d00404bfeb2c1128d5.png) 11 | 12 | ## Construction Sequence 13 | The construction sequence is the series of _sketch_ and _extrude_ operations that are executed to produce the final geometry. Each construction sequence must have at least one _sketch_ and one _extrude_ step, for a minimum of two steps. The average number of steps is 4.74, the median 4, the mode 2, and the maximum 61. Below we illustrate the distribution of construction sequence length. 14 | 15 | ![Construction Sequence Length](https://i.gyazo.com/db0be05dbe0c2abf64c45f8ddb40b41c.png) 16 | 17 | 18 | The most frequent construction sequence combinations are shown below. S indicates a _sketch_ and E indicates an _extrude_ operation. 19 | 20 | ![Construction Sequence Frequency](https://i.gyazo.com/3c2353fb3ebdffb307c0981eb8358725.png) 21 | 22 | ## Sketch 23 | ### Curves 24 | Each sketch is made up on different types of curves, such as lines, arcs, and circles. It is notable that mechanical CAD sketches rely heavily on lines, circles, and arcs rather than spline curves. 25 | Below we show the overall distribution of different curve types in the reconstruction dataset. 26 | 27 | ![Curve Type Distribution](https://i.gyazo.com/f27035588f435e18a58a4ae5c1ce0bde.png) 28 | 29 | The graph below illustrates the distribution of curve count per design, as another measure of design complexity. 30 | 31 | ![Curve Count Per Design](https://i.gyazo.com/437eccd587ef9892c851b3fb62eb40d2.png) 32 | 33 | Below we show the frequency that different curve combinations are used together in a design. 34 | Each curve type is abbreviated as follows: 35 | - C: `SketchCircle` 36 | - A: `SketchArc` 37 | - L: `SketchLine` 38 | - S: `SketchFittedSpline` 39 | 40 | ![Curve Type Combination Frequency](https://i.gyazo.com/893bed76b7e957aaea5f2b8f0f177cd5.png) 41 | 42 | ### Dimensions & Constraints 43 | Shown below are the distribution of dimension and constraint types in the dataset. 44 | 45 | ![Dimension Types](https://i.gyazo.com/44298f03a6028a61d0937f9aa6e030c5.png) 46 | 47 | ![Constraint Types](https://i.gyazo.com/097fcf0fb82804389a29cd3b49cb4fc9.png) 48 | 49 | ## Extrude 50 | 51 | Illustrated below is the distribution of different extrude types and operations. Note that tapers can be applied in addition to any extrude type, so the overall frequency of each is shown rather than a relative percentage. 52 | 53 | ![Extrude Type Distribution](https://i.gyazo.com/8b4688545edba7dee16e5cb382b38efb.png) 54 | 55 | ![Extrude Operation Distribution](https://i.gyazo.com/e3b1e28bdddcc27191cf0ebaa45b415f.png) 56 | -------------------------------------------------------------------------------- /docs/tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "industry": [ 3 | "Media & Entertainment", 4 | "Civil Infrastructure", 5 | "Architecture, Engineering & Construction", 6 | "Other Industries", 7 | "Product Design & Manufacturing" 8 | ], 9 | "category": [ 10 | "3D Art + Illustration", 11 | "Game Asset Creation", 12 | "Utilities & Telecom", 13 | "Construction Verification", 14 | "Science", 15 | "Story Development", 16 | "Design", 17 | "Tools", 18 | "General", 19 | "Nature", 20 | "Sport", 21 | "Fantasy", 22 | "Land Development", 23 | "HVAC", 24 | "Automotive", 25 | "Virtual Reality", 26 | "Bridges", 27 | "Factory Layout", 28 | "Games/Film", 29 | "Product Design", 30 | "Digital Visualization", 31 | "Jewelry", 32 | "Concept Art", 33 | "Art", 34 | "Robotics", 35 | "Cartoon", 36 | "Advertising", 37 | "Furniture + Household", 38 | "Rapid Energy Modeling", 39 | "Infrastructure Visualization", 40 | "Game", 41 | "Conceptual Modeling", 42 | "Architecture", 43 | "Mechanical Engineering", 44 | "Aerospace", 45 | "City Model", 46 | "Energy + Power", 47 | "Toys", 48 | "Wood Working", 49 | "Film/TV/Post", 50 | "Character", 51 | "Fashion", 52 | "Archeology", 53 | "Engineering", 54 | "Museum", 55 | "Electronics", 56 | "Interior Design", 57 | "Heritage", 58 | "Infrastructure Design", 59 | "Props/Items", 60 | "Roads & Highways", 61 | "Building Renovation", 62 | "Oil & Gas", 63 | "Machine design", 64 | "Medical", 65 | "SCI-FI", 66 | "Industrial Asset Creation", 67 | "Marine", 68 | "Rail", 69 | "Miscellaneous", 70 | "Environments", 71 | "Gameplay", 72 | "Packaging", 73 | "Drainage", 74 | "Tourism", 75 | "Inspection", 76 | "Water & Wastewater", 77 | "Abstract", 78 | "Level Design", 79 | "Reality" 80 | ], 81 | "products": [ 82 | "3ds Max", 83 | "3ds Max Design", 84 | "Alias", 85 | "Alias AutoStudio", 86 | "Alias Design", 87 | "Alias Surface", 88 | "AutoCAD", 89 | "AutoCAD Architecture", 90 | "AutoCAD LT", 91 | "AutoCAD Mechanical", 92 | "AutoCAD Plant 3D", 93 | "Autodesk® Rendering", 94 | "Fusion 360", 95 | "InfraWorks 360", 96 | "Inventor", 97 | "Inventor LT", 98 | "Inventor Professional", 99 | "Inventor Publisher", 100 | "Maya", 101 | "Not specified", 102 | "ReCap 360", 103 | "ReCap 360 Ultimate", 104 | "ReMake", 105 | "Revit", 106 | "Revit Architecture", 107 | "Revit LT" 108 | ] 109 | } -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Fusion 360 Gallery Dataset Tools 2 | Here we provide various tools for working with the Fusion 360 Gallery Dataset, including the CAD reconstruction code used in [our paper](https://arxiv.org/abs/2010.02392). Several tools leverage the [Fusion 360 API](http://help.autodesk.com/view/fusion360/ENU/?guid=GUID-7B5A90C8-E94C-48DA-B16B-430729B734DC) to perform geometry operations and require Fusion 360 to be installed. 3 | 4 | ## Getting Started 5 | Below are some general instructions for getting started setup with Fusion 360. Please refer to the readme provided along with each tool for specific instructions. 6 | 7 | ### Install Fusion 360 8 | The first step is to install Fusion 360 and setup up an account. As Fusion 360 stores data in the cloud, an account is required to login and use the application. Fusion 360 is available on Windows and Mac and is free for students and educators. [Follow these instructions](https://www.autodesk.com/products/fusion-360/students-teachers-educators) to create a free educational license. 9 | 10 | ### Running 11 | To run a script/add-in in Fusion 360: 12 | 13 | 1. Open Fusion 360 14 | 2. Go to Tools tab > Add-ins > Scripts and Add-ins 15 | 3. In the popup, select the Add-in panel, click the green '+' icon and select the appropriate directory in this repo 16 | 4. Click 'Run' 17 | 18 | ![Running Fusion 360 Scripts/Add-ins](https://help.autodesk.com/sfdcarticles/img/0EM3g0000004S86) 19 | 20 | 21 | ### Debugging 22 | To debug any of tools that use Fusion 360 you need to install [Visual Studio Code](https://code.visualstudio.com/), a free open source editor. For a general overview of how to debug scripts in Fusion 360 from Visual Studio Code, check out [this post](https://modthemachine.typepad.com/my_weblog/2019/09/debug-fusion-360-add-ins.html) and refer to the readme provided along with each tool. 23 | 24 | 25 | ## Tools 26 | - [`Fusion 360 Gym`](fusion360gym): A 'gym' environment for training ML models to design using Fusion 360. 27 | - [`Reconverter`](reconverter): Demonstrates how to batch convert the raw data structure provided with the reconstruction dataset into other representations using Fusion 360. 28 | - [`Regraph`](regraph): Demonstrates how to create a B-Rep graph data structure from data provided with the reconstruction dataset using Fusion 360. 29 | - [`RegraphNet`](regraphnet): A neural network for predicting CAD reconstruction actions. This network takes the output from [`Regraph`](regraph) and is the underlying network used with neurally guided search in [our paper](https://arxiv.org/abs/2010.02392). 30 | - [`Segmentation Viewer`](segmentation_viewer): Viewer for the segmentation dataset to visualize the 3D models with different colors based on the modeling operation. 31 | - [`Search`](search): A framework for running neurally guided search to recover a construction sequence from B-Rep input. We use this code in [our paper](https://arxiv.org/abs/2010.02392). 32 | - [`sketch2image`](sketch2image): Convert sketches provided in json format to images using matplotlib. 33 | - [`Assembly Download`](assembly_download): Download and uncompress the Assembly Dataset. 34 | - [`Assembly Graph`](assembly_graph): Generate a graph representation from an assembly. 35 | - [`Assembly2CAD`](assembly2cad): Build a Fusion 360 CAD model from an assembly data sample. 36 | - [`Joint2CAD`](joint2cad): Build a Fusion 360 CAD model from a joint data sample. 37 | -------------------------------------------------------------------------------- /tools/assembly2cad/README.md: -------------------------------------------------------------------------------- 1 | # Assembly2CAD 2 | 3 | ![Assembly2CAD](https://i.gyazo.com/a43a60bbe9f8a9906da4ea713c2a0728.gif) 4 | 5 | [Assembly2CAD](assembly2cad.py) demonstrates how to build a Fusion 360 CAD model from the assembly data provided with the [Assembly Dataset](../../docs/assembly.md). The resulting CAD model has a complete assembly tree and fully specified parametric joints. 6 | 7 | 8 | ## Running 9 | [Assembly2CAD](assembly2cad.py) runs in Fusion 360 as a script with the following steps. 10 | 1. Follow the [general instructions here](../) to get setup with Fusion 360. 11 | 2. Optionally change the `assembly_file` in [`assembly2cad.py`](assembly2cad.py) to point towards an `assembly.json` provided with the [Assembly Dataset](../../docs/assembly.md). 12 | 3. Optionally change the `png_file` and `f3d_file` in [`assembly2cad.py`](assembly2cad.py) to your preferred name for each file that is exported. 13 | 4. Run the [`assembly2cad.py`](assembly2cad.py) script from within Fusion 360. When the script has finished running the design will be open in Fusion 360. 14 | 5. Check the contents of `assembly2cad/` directory to find the .f3d that was exported. 15 | 16 | ## How it Works 17 | If you look into the code you will notice that the hard work is performed by [`assembly_importer.py`](../common/assembly_importer.py) and does the following: 18 | 1. Opens and reads `assembly.json`. 19 | 2. Gets a list of all .smt files are in the directory where `assembly.json` is located. 20 | 3. Looks into the `root` data of `assembly.json` and creates brep bodies from the smt files at the root level. 21 | 4. Looks into `tree` and `occurrences` data of the `assembly.json` and creates components/occurrences by importing the appropriate .smt files. 22 | 5. After the assembly tree is built it creates joints if specified in `assembly.json`. 23 | 24 | -------------------------------------------------------------------------------- /tools/assembly2cad/assembly2cad.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "autodeskProduct": "Fusion360", 3 | "type": "script", 4 | "author": "", 5 | "description": { 6 | "": "" 7 | }, 8 | "supportedOS": "windows|mac", 9 | "editEnabled": true 10 | } -------------------------------------------------------------------------------- /tools/assembly2cad/assembly2cad.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Construct a Fusion 360 CAD model from an assembly 4 | provided with the Fusion 360 Gallery Assembly Dataset 5 | 6 | """ 7 | 8 | 9 | import os 10 | import sys 11 | from pathlib import Path 12 | import adsk.core 13 | import traceback 14 | 15 | # Add the common folder to sys.path 16 | COMMON_DIR = os.path.join(os.path.dirname(__file__), "..", "common") 17 | if COMMON_DIR not in sys.path: 18 | sys.path.append(COMMON_DIR) 19 | 20 | from assembly_importer import AssemblyImporter 21 | import exporter 22 | 23 | 24 | def run(context): 25 | ui = None 26 | try: 27 | app = adsk.core.Application.get() 28 | ui = app.userInterface 29 | 30 | current_dir = Path(__file__).resolve().parent 31 | data_dir = current_dir.parent / "testdata/assembly_examples/belt_clamp" 32 | assembly_file = data_dir / "assembly.json" 33 | 34 | assembly_importer = AssemblyImporter(assembly_file) 35 | assembly_importer.reconstruct() 36 | 37 | png_file = current_dir / f"{assembly_file.stem}.png" 38 | exporter.export_png_from_component(png_file, app.activeProduct.rootComponent) 39 | 40 | f3d_file = current_dir / f"{assembly_file.stem}.f3d" 41 | exporter.export_f3d(f3d_file) 42 | 43 | if ui: 44 | if f3d_file.exists(): 45 | ui.messageBox(f"Exported to: {f3d_file}") 46 | else: 47 | ui.messageBox(f"Failed to export: {f3d_file}") 48 | except: 49 | if ui: 50 | ui.messageBox(f"Failed to export: {traceback.format_exc()}") 51 | -------------------------------------------------------------------------------- /tools/assembly_download/README.md: -------------------------------------------------------------------------------- 1 | # Assembly Dataset Download 2 | The Assembly Dataset is provided as a series of 7z archives. 3 | Each archive contains approximately 750 assemblies as well as the training split and license information. 4 | The size of the entire dataset is 146.53 GB and 18.8 GB when compressed. 5 | We provide a script to download and extract the files below. 6 | 7 | ## Download 8 | Below are the links to directly download each of the archive files. Each archive can be extracted independently if only a portion of the full dataset is required. 9 | 10 | - [a1.0.0_00.7z](https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_00.7z) (2.3 GB) 11 | - [a1.0.0_01.7z](https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_01.7z) (2.1 GB) 12 | - [a1.0.0_02.7z](https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_02.7z) (2.0 GB) 13 | - [a1.0.0_03.7z](https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_03.7z) (1.8 GB) 14 | - [a1.0.0_04.7z](https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_04.7z) (1.1 GB) 15 | - [a1.0.0_05.7z](https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_05.7z) (1.9 GB) 16 | - [a1.0.0_06.7z](https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_06.7z) (1.5 GB) 17 | - [a1.0.0_07.7z](https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_07.7z) (1.9 GB) 18 | - [a1.0.0_08.7z](https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_08.7z) (1.4 GB) 19 | - [a1.0.0_09.7z](https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_09.7z) (1.2 GB) 20 | - [a1.0.0_10.7z](https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_10.7z) (1.4 GB) 21 | 22 | 23 | ## Extraction 24 | To extract each archive requires a tool that supports the 7z compression format. 25 | 26 | ### Linux 27 | Distributions of Ubuntu come with `p7zip` installed. However, with older versions (e.g. 16.02) extraction times can be excessively slow. If you experience extraction times of longer than 10 mins per archive, we suggest using the latest version provided on the [7-Zip](https://www.7-zip.org) website. The following commands can be used to download and install the latest version if not already available in your package manager: 28 | 29 | ``` 30 | curl https://www.7-zip.org/a/7z2106-linux-x64.tar.xz -o 7z2106-linux-x64.tar.xz 31 | sudo apt install xz-utils 32 | tar -xf 7z2106-linux-x64.tar.xz 33 | ``` 34 | and then extract the archives: 35 | ``` 36 | 7zz x a1.0.0_00.7z 37 | ``` 38 | 39 | ### Mac OS 40 | On recent versions of Mac OS, 7z is supported natively. 41 | 42 | ### Windows 43 | Windows users can download and install [7-Zip](https://www.7-zip.org), which offers both a GUI and command line interface. 44 | 45 | ## Download and Extraction Script 46 | We provide the python script [assembly_download.py](assembly_download.py) to download and extract all archive files. 47 | 48 | ### Installation 49 | The script calls [7-Zip](https://www.7-zip.org) directly so if you encounter problems, ensure the path is set correctly in the `get_7z_path()` function or `7z` is present in your linux path. To run the script you will need the following python libraries that can be installed using `pip`: 50 | 51 | - `requests` 52 | - `tqdm` 53 | 54 | 55 | ### Running 56 | The script can be run by passing in the output directory where the files will be extracted to. 57 | 58 | ``` 59 | python assembly_download.py --output path/to/files 60 | ``` 61 | Additionally the following optional arguments can be passed: 62 | - `--limit`: Limit the number of archive files to download. 63 | - `--threads`: Number of threads to use for downloading in parallel [default: 4]. 64 | - `--download_only`: Download without extracting files. 65 | 66 | The script will not re-download and overwrite archive files that have already been downloaded. -------------------------------------------------------------------------------- /tools/assembly_download/assembly_download.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Download and extract the Fusion 360 Gallery Assembly Dataset 4 | 5 | """ 6 | 7 | import argparse 8 | import sys 9 | import itertools 10 | import requests 11 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 12 | import subprocess 13 | from pathlib import Path 14 | from multiprocessing.pool import ThreadPool 15 | from multiprocessing import Pool 16 | from tqdm import tqdm 17 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 18 | 19 | 20 | def download_file(input): 21 | """Download a file and save it locally""" 22 | url, output_dir, position = input 23 | url_file = Path(url).name 24 | local_file = output_dir / url_file 25 | if not local_file.exists(): 26 | tqdm.write(f"Downloading {url_file} to {local_file}") 27 | r = requests.get(url, stream=True, verify=False) 28 | if r.status_code == 200: 29 | total_length = int(r.headers.get("content-length")) 30 | pbar = tqdm(total=total_length, position=position) 31 | with open(local_file, "wb") as f: 32 | for chunk in r: 33 | pbar.update(len(chunk)) 34 | f.write(chunk) 35 | pbar.close() 36 | tqdm.write(f"Finished downloading {local_file.name}") 37 | else: 38 | tqdm.write(f"Skipping download of {local_file.name} as local file already exists") 39 | return local_file 40 | 41 | 42 | def download_files(output_dir, limit, threads): 43 | """Download the assembly archive files""" 44 | assembly_urls = [ 45 | "https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_00.7z", 46 | "https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_01.7z", 47 | "https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_02.7z", 48 | "https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_03.7z", 49 | "https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_04.7z", 50 | "https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_05.7z", 51 | "https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_06.7z", 52 | "https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_07.7z", 53 | "https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_08.7z", 54 | "https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_09.7z", 55 | "https://fusion-360-gallery-dataset.s3-us-west-2.amazonaws.com/assembly/a1.0.0/a1.0.0_10.7z", 56 | ] 57 | if limit is not None: 58 | assembly_urls = assembly_urls[:limit] 59 | 60 | local_files = [] 61 | iter_data = zip( 62 | assembly_urls, 63 | itertools.repeat(output_dir), 64 | itertools.count(), 65 | ) 66 | 67 | results = ThreadPool(threads).imap(download_file, iter_data) 68 | local_files = [] 69 | for local_file in tqdm(results, total=len(assembly_urls)): 70 | local_files.append(local_file) 71 | 72 | # Serial Implementation 73 | # for index, url in enumerate(assembly_urls): 74 | # local_file = download_file((url, output_dir, index)) 75 | # local_files.append(local_file) 76 | return local_files 77 | 78 | 79 | def get_7z_path(): 80 | """Get the path to the 7-zip application""" 81 | # Edit the below paths to point to your install of 7-Zip 82 | if sys.platform == "darwin": 83 | zip_path = Path("/Applications/7z/7zz") 84 | assert zip_path.exists(), f"Could not find 7-Zip executable: {zip_path}" 85 | zip_path = str(zip_path.resolve()) 86 | elif sys.platform == "win32": 87 | zip_path = Path("C:/Program Files/7-Zip/7z.exe") 88 | assert zip_path.exists(), f"Could not find 7-Zip executable: {zip_path}" 89 | zip_path = str(zip_path.resolve()) 90 | elif sys.platform.startswith("linux"): 91 | # In linux the 7z executable is in the path 92 | zip_path = "7z" 93 | return zip_path 94 | 95 | 96 | def extract_file(zip_path, local_file, assembly_dir): 97 | """Extract a single archive""" 98 | tqdm.write(f"Extracting {local_file.name}...") 99 | args = [ 100 | zip_path, 101 | "x", 102 | str(local_file.resolve()), 103 | "-aos" # Skip extracting of existing files 104 | ] 105 | p = subprocess.run(args, cwd=str(assembly_dir)) 106 | return p.returncode == 0 107 | 108 | 109 | def extract_files(zip_path, local_files, output_dir): 110 | """Extract all files""" 111 | # Make a sub directory for the assembly files 112 | assembly_dir = output_dir / "assembly" 113 | if not assembly_dir.exists(): 114 | assembly_dir.mkdir(parents=True) 115 | results = [] 116 | for local_file in tqdm(local_files): 117 | result = extract_file(zip_path, local_file, assembly_dir) 118 | results.append(result) 119 | return results 120 | 121 | 122 | def main(output_dir, limit, threads, download_only): 123 | if not download_only: 124 | # Check we have a good path first 125 | zip_path = get_7z_path() 126 | 127 | # Download all the files, skipping those that have already been downloaded 128 | local_files = download_files(output_dir, limit, threads) 129 | 130 | if not download_only: 131 | # Extract all the files 132 | results = extract_files(zip_path, local_files, output_dir) 133 | tqdm.write(f"Extracted {sum(results)}/{len(results)} archives") 134 | 135 | 136 | if __name__ == "__main__": 137 | parser = argparse.ArgumentParser() 138 | parser.add_argument( 139 | "--output", type=str, help="Output folder to save compressed files." 140 | ) 141 | parser.add_argument( 142 | "--limit", type=int, help="Limit the number of archive files to download." 143 | ) 144 | parser.add_argument( 145 | "--threads", type=int, default=4, help="Number of threads to use for downloading in parallel [default: 4]." 146 | ) 147 | parser.add_argument("--download_only", action="store_true", help="Download without extracting files.") 148 | args = parser.parse_args() 149 | 150 | output_dir = None 151 | if args.output is not None: 152 | # Prep the output directory 153 | output_dir = Path(args.output) 154 | if not output_dir.exists(): 155 | output_dir.mkdir(parents=True) 156 | 157 | main(output_dir, args.limit, args.threads, args.download_only) 158 | -------------------------------------------------------------------------------- /tools/assembly_graph/README.md: -------------------------------------------------------------------------------- 1 | # AssemblyGraph 2 | Generate a graph representation from an assembly. 3 | 4 | ## Setup 5 | Install requirements: 6 | - `numpy` 7 | - `networkx` 8 | - `meshplot` 9 | - `trimesh` 10 | - `tqdm` 11 | 12 | ## [Assembly Viewer](assembly_viewer.ipynb) 13 | Example notebook showing how to use the [`AssemblyGraph`](assembly_graph.py) class to generate a NetworkX graph and visualize it as both a graph and a 3D model. 14 | 15 | ![Assembly Viewer](https://i.gyazo.com/ef0ab11a58d10da2b25828dceed1f6f1.gif) 16 | 17 | ## [Assembly2Graph](assembly2graph.py) 18 | Utility script to convert a folder of assemblies into a NetworkX node-link graph JSON file format. 19 | 20 | ``` 21 | python assembly2graph.py --input path/to/a1.0.0/assembly --output path/to/graphs 22 | ``` 23 | 24 | -------------------------------------------------------------------------------- /tools/assembly_graph/assembly2graph.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Convert assemblies into a graph representation 4 | 5 | """ 6 | 7 | import sys 8 | import time 9 | import argparse 10 | from pathlib import Path 11 | from tqdm import tqdm 12 | 13 | from assembly_graph import AssemblyGraph 14 | 15 | 16 | def get_input_files(input): 17 | """Get the input files to process""" 18 | input_path = Path(input) 19 | if not input_path.exists(): 20 | sys.exit("Input folder/file does not exist") 21 | if input_path.is_dir(): 22 | assembly_files = [f for f in input_path.glob("**/assembly.json")] 23 | if len(assembly_files) == 0: 24 | sys.exit("Input folder/file does not contain assembly.json files") 25 | return assembly_files 26 | elif input_path.name == "assembly.json": 27 | return [input_path] 28 | else: 29 | sys.exit("Input folder/file invalid") 30 | 31 | 32 | def assembly2graph(args): 33 | """Convert assemblies to graph format""" 34 | input_files = get_input_files(args.input) 35 | if args.limit is not None: 36 | input_files = input_files[:args.limit] 37 | output_dir = Path(args.output) 38 | if not output_dir.exists(): 39 | output_dir.mkdir(parents=True) 40 | tqdm.write(f"Converting {len(input_files)} assemblies...") 41 | start_time = time.time() 42 | for input_file in tqdm(input_files): 43 | ag = AssemblyGraph(input_file) 44 | json_file = output_dir / f"{input_file.parent.stem}_graph.json" 45 | ag.export_graph_json(json_file) 46 | print(f"Time taken: {time.time() - start_time}") 47 | 48 | 49 | if __name__ == "__main__": 50 | parser = argparse.ArgumentParser() 51 | parser.add_argument( 52 | "--input", type=str, required=True, help="Input folder/file with assembly data." 53 | ) 54 | parser.add_argument( 55 | "--output", type=str, default="data", help="Output folder to save graphs." 56 | ) 57 | parser.add_argument( 58 | "--limit", type=int, help="Limit the number assembly files to convert." 59 | ) 60 | args = parser.parse_args() 61 | assembly2graph(args) 62 | -------------------------------------------------------------------------------- /tools/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/common/__init__.py -------------------------------------------------------------------------------- /tools/common/deserialize.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Deserialize dictionary data from json to Fusion 360 entities 4 | 5 | """ 6 | 7 | import adsk.core 8 | import adsk.fusion 9 | 10 | 11 | def point2d(point_data): 12 | return adsk.core.Point2D.create( 13 | point_data["x"], 14 | point_data["y"] 15 | ) 16 | 17 | 18 | def point3d(point_data): 19 | return adsk.core.Point3D.create( 20 | point_data["x"], 21 | point_data["y"], 22 | point_data["z"] 23 | ) 24 | 25 | 26 | def point3d_list(point_list, xform=None): 27 | points = [] 28 | for point_data in point_list: 29 | point = point3d(point_data) 30 | if xform is not None: 31 | point.transformBy(xform) 32 | points.append(point) 33 | return points 34 | 35 | 36 | def vector3d(vector_data): 37 | return adsk.core.Vector3D.create( 38 | vector_data["x"], 39 | vector_data["y"], 40 | vector_data["z"] 41 | ) 42 | 43 | 44 | def line2d(start_point_data, end_point_data): 45 | start_point = point2d(start_point_data) 46 | end_point = point2d(end_point_data) 47 | return adsk.core.Line2D.create(start_point, end_point) 48 | 49 | 50 | def plane(plane_data): 51 | origin = point3d(plane_data["origin"]) 52 | normal = vector3d(plane_data["normal"]) 53 | u_direction = vector3d(plane_data["u_direction"]) 54 | v_direction = vector3d(plane_data["v_direction"]) 55 | plane = adsk.core.Plane.create(origin, normal) 56 | plane.setUVDirections(u_direction, v_direction) 57 | return plane 58 | 59 | 60 | def matrix3d(matrix_data): 61 | matrix = adsk.core.Matrix3D.create() 62 | origin = point3d(matrix_data["origin"]) 63 | x_axis = vector3d(matrix_data["x_axis"]) 64 | y_axis = vector3d(matrix_data["y_axis"]) 65 | z_axis = vector3d(matrix_data["z_axis"]) 66 | matrix.setWithCoordinateSystem(origin, x_axis, y_axis, z_axis) 67 | return matrix 68 | 69 | 70 | def feature_operations(operation_data): 71 | if operation_data == "JoinFeatureOperation": 72 | return adsk.fusion.FeatureOperations.JoinFeatureOperation 73 | if operation_data == "CutFeatureOperation": 74 | return adsk.fusion.FeatureOperations.CutFeatureOperation 75 | if operation_data == "IntersectFeatureOperation": 76 | return adsk.fusion.FeatureOperations.IntersectFeatureOperation 77 | if operation_data == "NewBodyFeatureOperation": 78 | return adsk.fusion.FeatureOperations.NewBodyFeatureOperation 79 | if operation_data == "NewComponentFeatureOperation": 80 | return adsk.fusion.FeatureOperations.NewComponentFeatureOperation 81 | return None 82 | 83 | 84 | def construction_plane(name): 85 | """Return a construction plane given a name""" 86 | app = adsk.core.Application.get() 87 | design = adsk.fusion.Design.cast(app.activeProduct) 88 | construction_planes = { 89 | "xy": design.rootComponent.xYConstructionPlane, 90 | "xz": design.rootComponent.xZConstructionPlane, 91 | "yz": design.rootComponent.yZConstructionPlane 92 | } 93 | name_lower = name.lower() 94 | if name_lower in construction_planes: 95 | return construction_planes[name_lower] 96 | return None 97 | 98 | 99 | def face_by_point3d(point3d_data): 100 | """Find a face with given serialized point3d that sits on that face""" 101 | point_on_face = point3d(point3d_data) 102 | app = adsk.core.Application.get() 103 | design = adsk.fusion.Design.cast(app.activeProduct) 104 | for component in design.allComponents: 105 | try: 106 | entities = component.findBRepUsingPoint( 107 | point_on_face, 108 | adsk.fusion.BRepEntityTypes.BRepFaceEntityType, 109 | 0.01, # -1.0 is the default tolerance 110 | False 111 | ) 112 | if entities is None or len(entities) == 0: 113 | continue 114 | else: 115 | # Return the first face 116 | # although there could be multiple matches 117 | return entities[0] 118 | except Exception as ex: 119 | print("Exception finding BRepFace", ex) 120 | # Ignore and keep looking 121 | pass 122 | return None 123 | 124 | 125 | def view_orientation(name): 126 | """Return a camera view orientation given a name""" 127 | view_orientations = { 128 | "ArbitraryViewOrientation": adsk.core.ViewOrientations.ArbitraryViewOrientation, 129 | "BackViewOrientation": adsk.core.ViewOrientations.BackViewOrientation, 130 | "BottomViewOrientation": adsk.core.ViewOrientations.BottomViewOrientation, 131 | "FrontViewOrientation": adsk.core.ViewOrientations.FrontViewOrientation, 132 | "IsoBottomLeftViewOrientation": adsk.core.ViewOrientations.IsoBottomLeftViewOrientation, 133 | "IsoBottomRightViewOrientation": adsk.core.ViewOrientations.IsoBottomRightViewOrientation, 134 | "IsoTopLeftViewOrientation": adsk.core.ViewOrientations.IsoTopLeftViewOrientation, 135 | "IsoTopRightViewOrientation": adsk.core.ViewOrientations.IsoTopRightViewOrientation, 136 | "LeftViewOrientation": adsk.core.ViewOrientations.LeftViewOrientation, 137 | "RightViewOrientation": adsk.core.ViewOrientations.RightViewOrientation, 138 | "TopViewOrientation": adsk.core.ViewOrientations.TopViewOrientation, 139 | } 140 | name_lower = name.lower() 141 | if name_lower in view_orientations: 142 | return view_orientations[name_lower] 143 | return None 144 | 145 | 146 | def get_key_point_type(key_point_str): 147 | """Return Key Point Type used in a Joint""" 148 | if key_point_str == "CenterKeyPoint": 149 | return adsk.fusion.JointKeyPointTypes.CenterKeyPoint 150 | elif key_point_str == "EndKeyPoint": 151 | return adsk.fusion.JointKeyPointTypes.EndKeyPoint 152 | elif key_point_str == "MiddleKeyPoint": 153 | return adsk.fusion.JointKeyPointTypes.MiddleKeyPoint 154 | elif key_point_str == "StartKeyPoint": 155 | return adsk.fusion.JointKeyPointTypes.StartKeyPoint 156 | else: 157 | raise Exception(f"Unknown keyPointType type: {key_point_str}") 158 | 159 | 160 | def get_rotation_axis(rotation_axis): 161 | """Receives a string and Return Joint direction type""" 162 | if rotation_axis == "XAxisJointDirection": 163 | return adsk.fusion.JointDirections.XAxisJointDirection 164 | elif rotation_axis == "YAxisJointDirection": 165 | return adsk.fusion.JointDirections.YAxisJointDirection 166 | elif rotation_axis == "ZAxisJointDirection": 167 | return adsk.fusion.JointDirections.ZAxisJointDirection 168 | elif rotation_axis == "CustomJointDirection": 169 | return adsk.fusion.JointDirections.CustomJointDirection 170 | else: 171 | raise Exception(f"Unknown JointDirections type: {rotation_axis}") -------------------------------------------------------------------------------- /tools/common/exceptions.py: -------------------------------------------------------------------------------- 1 | class UnsupportedException(Exception): 2 | """Raised when the an unsupported feature is used""" 3 | pass 4 | -------------------------------------------------------------------------------- /tools/common/face_reconstructor.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Face Reconstructor 4 | Reconstruct via face extrusion to match a target design 5 | 6 | """ 7 | 8 | import adsk.core 9 | import adsk.fusion 10 | 11 | import name 12 | import deserialize 13 | 14 | 15 | class FaceReconstructor(): 16 | 17 | def __init__(self, target, reconstruction, use_temp_id=True): 18 | self.target = target 19 | self.reconstruction = reconstruction 20 | self.use_temp_id = use_temp_id 21 | self.app = adsk.core.Application.get() 22 | self.design = adsk.fusion.Design.cast(self.app.activeProduct) 23 | # Populate the cache with a map from uuids to face indices 24 | self.target_uuid_to_face_map = self.get_target_uuid_to_face_map() 25 | 26 | def set_reconstruction_component(self, reconstruction): 27 | """Set the reconstruction component""" 28 | self.reconstruction = reconstruction 29 | 30 | def reconstruct(self, graph_data): 31 | """Reconstruct from the sequence of faces""" 32 | self.sequence = graph_data["sequences"][0] 33 | for seq in self.sequence["sequence"]: 34 | self.add_extrude_from_uuid( 35 | seq["start_face"], 36 | seq["end_face"], 37 | seq["operation"] 38 | ) 39 | 40 | def get_face_from_uuid(self, face_uuid): 41 | """Get a face from an index in the sequence""" 42 | if face_uuid not in self.target_uuid_to_face_map: 43 | return None 44 | uuid_data = self.target_uuid_to_face_map[face_uuid] 45 | # We get the face by following the entity token 46 | face_token = uuid_data["face_token"] 47 | entities = self.design.findEntityByToken(face_token) 48 | if entities is None: 49 | return None 50 | return entities[0] 51 | # body_index = uuid_data["body_index"] 52 | # face_index = uuid_data["face_index"] 53 | # body = self.target.bRepBodies[body_index] 54 | # face = body.faces[face_index] 55 | # return face 56 | 57 | def get_target_uuid_to_face_map(self): 58 | """As we have to find faces multiple times we first 59 | make a map between uuids and face indices""" 60 | target_uuid_to_face_map = {} 61 | for body_index, body in enumerate(self.target.bRepBodies): 62 | for face_index, face in enumerate(body.faces): 63 | face_uuid = self.get_regraph_uuid(face) 64 | assert face_uuid is not None 65 | target_uuid_to_face_map[face_uuid] = { 66 | "body_index": body_index, 67 | "face_index": face_index, 68 | "face_token": face.entityToken 69 | } 70 | return target_uuid_to_face_map 71 | 72 | def add_extrude_from_uuid(self, start_face_uuid, end_face_uuid, operation): 73 | """Create an extrude from a start face uuid to an end face uuid""" 74 | # Start and end face have to reference the occurrence 75 | # in order to perform extrude operations between components 76 | start_face = self.get_face_from_uuid(start_face_uuid) 77 | end_face = self.get_face_from_uuid(end_face_uuid) 78 | operation = deserialize.feature_operations(operation) 79 | return self.add_extrude(start_face, end_face, operation) 80 | 81 | def add_extrude(self, start_face, end_face, operation): 82 | """Create an extrude from a start face to an end face""" 83 | # If there are no bodies to cut or intersect, do nothing 84 | if ((operation == adsk.fusion.FeatureOperations.CutFeatureOperation or 85 | operation == adsk.fusion.FeatureOperations.IntersectFeatureOperation) and 86 | self.reconstruction.bRepBodies.count == 0): 87 | return None 88 | # We generate the extrude bodies in the reconstruction component 89 | extrudes = self.reconstruction.component.features.extrudeFeatures 90 | extrude_input = extrudes.createInput(start_face, operation) 91 | extent = adsk.fusion.ToEntityExtentDefinition.create(end_face, False) 92 | extrude_input.setOneSideExtent(extent, adsk.fusion.ExtentDirections.PositiveExtentDirection) 93 | extrude_input.creationOccurrence = self.reconstruction 94 | tools = [] 95 | for body in self.reconstruction.bRepBodies: 96 | tools.append(body) 97 | extrude_input.participantBodies = tools 98 | extrude = extrudes.add(extrude_input) 99 | return extrude 100 | 101 | def get_regraph_uuid(self, entity): 102 | """Get a uuid or a tempid depending on a flag""" 103 | is_face = isinstance(entity, adsk.fusion.BRepFace) 104 | is_edge = isinstance(entity, adsk.fusion.BRepEdge) 105 | if self.use_temp_id and (is_face or is_edge): 106 | return str(entity.tempId) 107 | else: 108 | return name.get_uuid(entity) 109 | -------------------------------------------------------------------------------- /tools/common/fusion_360_server.manifest: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "autodeskProduct": "Fusion360", 4 | "type": "addin", 5 | "id": "", 6 | "author": "", 7 | "description": { 8 | "": "" 9 | }, 10 | "version": "", 11 | "runOnStartup": true, 12 | "supportedOS": "windows|mac", 13 | "editEnabled": true 14 | } -------------------------------------------------------------------------------- /tools/common/launcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Find and launch Fusion 360 4 | 5 | """ 6 | 7 | import os 8 | import sys 9 | from pathlib import Path 10 | import subprocess 11 | import importlib 12 | 13 | 14 | class Launcher(): 15 | 16 | def __init__(self): 17 | self.fusion_app = self.find_fusion() 18 | if self.fusion_app is None: 19 | print("Error: Fusion 360 could not be found") 20 | elif not self.fusion_app.exists(): 21 | print(f"Error: Fusion 360 does not exist at {self.fusion_app}") 22 | else: 23 | print(f"Fusion 360 found at {self.fusion_app}") 24 | 25 | def launch(self): 26 | """Opens a new instance of Fusion 360""" 27 | if self.fusion_app is None: 28 | print("Error: Fusion 360 could not be found") 29 | return None 30 | elif not self.fusion_app.exists(): 31 | print(f"Error: Fusion 360 does not exist at {self.fusion_app}") 32 | return None 33 | else: 34 | fusion_path = str(self.fusion_app.resolve()) 35 | args = [] 36 | if sys.platform == "darwin": 37 | # -W is to wait for the app to finish 38 | # -n is to open a new app 39 | args = ["open", "-W", "-n", fusion_path] 40 | elif sys.platform == "win32": 41 | args = [fusion_path] 42 | 43 | print(f"Fusion launching from {fusion_path}") 44 | # Turn off output from Fusion 45 | return subprocess.Popen( 46 | args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 47 | 48 | def find_fusion(self): 49 | """Find the Fusion app""" 50 | if sys.platform == "darwin": 51 | return self.find_fusion_mac() 52 | elif sys.platform == "win32": 53 | return self.find_fusion_windows() 54 | 55 | def find_fusion_mac(self): 56 | """Find the Fusion app on mac""" 57 | # Shortcut location that links to the latest version 58 | user_path = Path(os.path.expanduser("~")) 59 | fusion_app = user_path / "Library/Application Support/Autodesk/webdeploy/production/Autodesk Fusion 360.app" 60 | return fusion_app 61 | 62 | def find_fusion_windows(self): 63 | """Find the Fusion app 64 | by looking in a windows FusionLauncher.exe.ini file""" 65 | fusion_launcher = self.find_fusion_launcher() 66 | if fusion_launcher is None: 67 | return None 68 | # FusionLauncher.exe.ini looks like this (encoding is UTF-16): 69 | # [Launcher] 70 | # stream = production 71 | # auid = AutodeskInc.Fusion360 72 | # cmd = ""C:\path\to\Fusion360.exe"" 73 | # global = False 74 | with open(fusion_launcher, "r", encoding="utf16") as f: 75 | lines = f.readlines() 76 | lines = [x.strip() for x in lines] 77 | 78 | for line in lines: 79 | if line.startswith("cmd") and "Fusion360.exe" in line: 80 | pieces = line.split("\"") 81 | for piece in pieces: 82 | if "Fusion360.exe" in piece: 83 | return Path(piece) 84 | return None 85 | 86 | def find_fusion_launcher(self): 87 | """Find the FusionLauncher.exe.ini file on windows""" 88 | user_dir = Path(os.environ["LOCALAPPDATA"]) 89 | production_dir = user_dir / "Autodesk/webdeploy/production/" 90 | production_contents = Path(production_dir).iterdir() 91 | for item in production_contents: 92 | if item.is_dir(): 93 | fusion_launcher = item / "FusionLauncher.exe.ini" 94 | if fusion_launcher.exists(): 95 | return fusion_launcher 96 | return None 97 | -------------------------------------------------------------------------------- /tools/common/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Logger utility class to output log info to the Fusion TextCommands window 4 | 5 | """ 6 | 7 | import adsk.core 8 | import adsk.fusion 9 | import time 10 | 11 | 12 | class Logger: 13 | 14 | def __init__(self): 15 | app = adsk.core.Application.get() 16 | ui = app.userInterface 17 | self.text_palette = ui.palettes.itemById('TextCommands') 18 | 19 | # Make sure the palette is visible. 20 | if not self.text_palette.isVisible: 21 | self.text_palette.isVisible = True 22 | 23 | def log(self, txt_str=""): 24 | print(txt_str) 25 | self.text_palette.writeText(txt_str) 26 | adsk.doEvents() 27 | 28 | def log_time(self, txt_str=""): 29 | time_stamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 30 | time_txt_str = f"{time_stamp} {txt_str}" 31 | print(time_txt_str) 32 | self.text_palette.writeText(time_txt_str) 33 | adsk.doEvents() 34 | -------------------------------------------------------------------------------- /tools/common/match.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Match Fusion 360 entities to ids 4 | 5 | """ 6 | 7 | 8 | import adsk.core 9 | import adsk.fusion 10 | 11 | import deserialize 12 | import name 13 | 14 | 15 | def sketch_by_name(sketch_name, sketches=None): 16 | """Return a sketch with a given name""" 17 | app = adsk.core.Application.get() 18 | design = adsk.fusion.Design.cast(app.activeProduct) 19 | if sketches is None: 20 | sketches = design.rootComponent.sketches 21 | return sketches.itemByName(sketch_name) 22 | 23 | 24 | def sketch_by_id(sketch_id, sketches=None): 25 | """Return a sketch with a given sketch id""" 26 | app = adsk.core.Application.get() 27 | design = adsk.fusion.Design.cast(app.activeProduct) 28 | if sketches is None: 29 | sketches = design.rootComponent.sketches 30 | for sketch in sketches: 31 | uuid = name.get_uuid(sketch) 32 | if uuid is not None and uuid == sketch_id: 33 | return sketch 34 | return None 35 | 36 | 37 | def sketch_profile_by_id(sketch_profile_id, sketches=None): 38 | """Return a sketch profile with a given id""" 39 | app = adsk.core.Application.get() 40 | design = adsk.fusion.Design.cast(app.activeProduct) 41 | if sketches is None: 42 | sketches = design.rootComponent.sketches 43 | for sketch in sketches: 44 | for profile in sketch.profiles: 45 | uuid = name.get_profile_uuid(profile) 46 | if uuid is not None and uuid == sketch_profile_id: 47 | return profile 48 | return None 49 | 50 | 51 | def sketch_profiles_by_curve_id(sketch_curve_id, sketches=None): 52 | """Return the sketch profiles that contain the given curve id""" 53 | app = adsk.core.Application.get() 54 | design = adsk.fusion.Design.cast(app.activeProduct) 55 | matches = [] 56 | if sketches is None: 57 | sketches = design.rootComponent.sketches 58 | for sketch in sketches: 59 | for profile in sketch.profiles: 60 | for loop in profile.profileLoops: 61 | for curve in loop.profileCurves: 62 | sketch_ent = curve.sketchEntity 63 | curve_uuid = name.get_uuid(sketch_ent) 64 | if curve_uuid is not None and curve_uuid == sketch_curve_id: 65 | matches.append(profile) 66 | return matches 67 | 68 | 69 | def sketch_plane(sketch_plane_data): 70 | """ 71 | Return the sketch plane to create a sketch 72 | Can be passed either of: 73 | - Construction plane axes: XY, XZ, YZ 74 | - BRep temp id 75 | - Point3d on the BRep face 76 | """ 77 | app = adsk.core.Application.get() 78 | design = adsk.fusion.Design.cast(app.activeProduct) 79 | # String for brepface or construction plane 80 | if isinstance(sketch_plane_data, str): 81 | # Look for construction plane first 82 | construction_plane = deserialize.construction_plane(sketch_plane_data) 83 | if construction_plane is not None: 84 | return construction_plane 85 | elif isinstance(sketch_plane_data, dict): 86 | point_on_face = deserialize.point3d(sketch_plane_data) 87 | brep_face = face_by_point3d(point_on_face) 88 | if brep_face is not None: 89 | return brep_face 90 | elif isinstance(sketch_plane_data, int): 91 | # Now lets see if it is a brep tempid 92 | brep_face = face_by_id(sketch_plane_data) 93 | if brep_face is not None: 94 | return brep_face 95 | return None 96 | 97 | 98 | def face_by_id(temp_id): 99 | """Find a face with a given id""" 100 | app = adsk.core.Application.get() 101 | design = adsk.fusion.Design.cast(app.activeProduct) 102 | for component in design.allComponents: 103 | for body in component.bRepBodies: 104 | try: 105 | entities = body.findByTempId(temp_id) 106 | if entities is None or len(entities) == 0: 107 | continue 108 | else: 109 | return entities[0] 110 | except: 111 | # Ignore and keep looking 112 | pass 113 | return None 114 | 115 | 116 | def face_by_point3d(point3d): 117 | """Find a face with given point3d that sits on that face""" 118 | app = adsk.core.Application.get() 119 | design = adsk.fusion.Design.cast(app.activeProduct) 120 | for component in design.allComponents: 121 | try: 122 | entities = component.findBRepUsingPoint( 123 | point3d, 124 | adsk.fusion.BRepEntityTypes.BRepFaceEntityType, 125 | 0.01, # -1.0 is the default tolerance 126 | False 127 | ) 128 | if entities is None or len(entities) == 0: 129 | continue 130 | else: 131 | return entities[0] 132 | except: 133 | # Ignore and keep looking 134 | pass 135 | return None 136 | -------------------------------------------------------------------------------- /tools/common/name.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Give and get names for Fusion 360 entities 4 | 5 | """ 6 | 7 | 8 | import adsk.core 9 | import adsk.fusion 10 | import uuid 11 | import json 12 | import math 13 | 14 | 15 | def get_uuid(entity, group_name="Dataset"): 16 | if isinstance(entity, adsk.fusion.Profile): 17 | return get_profile_uuid(entity) 18 | elif isinstance(entity, adsk.fusion.BRepFace): 19 | return get_brep_face_uuid(entity, group_name) 20 | else: 21 | uuid_att = entity.attributes.itemByName(group_name, "uuid") 22 | if uuid_att is not None: 23 | return uuid_att.value 24 | else: 25 | # Return None to allow for workarounds 26 | return None 27 | 28 | 29 | def get_brep_face_uuid(entity, group_name): 30 | """Handle the special case of split brep faces with the same uuid""" 31 | uuid_att = entity.attributes.itemByName(group_name, "uuid") 32 | return get_brep_face_uuid_from_attribute(entity, uuid_att) 33 | 34 | 35 | def get_brep_face_uuid_from_attribute(entity, uuid_att): 36 | """Handle the special case of split brep faces with the same uuid""" 37 | if uuid_att is None: 38 | return None 39 | # First check if this Face was previously split 40 | if (uuid_att.otherParents is not None and 41 | uuid_att.otherParents.count > 0): 42 | # Now we know we have a split face 43 | # because it has another parent 44 | # Next lets see if this face is the original face 45 | # or if was newly created from the split 46 | for parent in uuid_att.otherParents: 47 | if isinstance(parent, adsk.fusion.BRepFace): 48 | is_original = entity.tempId == parent.tempId 49 | # The original face keeps its uuid 50 | if is_original: 51 | return uuid_att.value 52 | # Now we know we are the newly created split face 53 | # so we have to make a uuid 54 | # Due to a bug in Fusion we can't assign a new id 55 | # as an attribute on the split face, so we append the 56 | # number of parents at the end of the uuid 57 | uuid_concat = f"{uuid_att.value}_{uuid_att.otherParents.count}" 58 | return str(uuid.uuid3(uuid.NAMESPACE_URL, uuid_concat)) 59 | else: 60 | # The face was not split, so we are good to go 61 | return uuid_att.value 62 | 63 | 64 | def get_profile_uuid(profile): 65 | """Sketch profiles don"t support attributes 66 | so we cook up a UUID from the curves UUIDs""" 67 | profile_curves = [] 68 | for loop in profile.profileLoops: 69 | for curve in loop.profileCurves: 70 | sketch_ent = curve.sketchEntity 71 | profile_curves.append(get_uuid(sketch_ent)) 72 | # Concat all the uuids from the curves 73 | curve_uuids = "_".join(profile_curves) 74 | # Generate a UUID by hashing the curve_uuids 75 | return str(uuid.uuid3(uuid.NAMESPACE_URL, curve_uuids)) 76 | 77 | 78 | def set_uuid(entity, group_name="Dataset"): 79 | """Set a uuid of an entity 80 | Returns the new or existing uuid of the entity""" 81 | if isinstance(entity, adsk.fusion.BRepFace): 82 | return set_brep_face_uuid(entity, group_name) 83 | uuid_att = entity.attributes.itemByName(group_name, "uuid") 84 | if uuid_att is None: 85 | unique_id = uuid.uuid1() 86 | entity.attributes.add(group_name, "uuid", str(unique_id)) 87 | return str(unique_id) 88 | else: 89 | return uuid_att.value 90 | 91 | 92 | def set_brep_face_uuid(entity, group_name="Dataset"): 93 | """Handle the special case of split brep faces with a parent""" 94 | uuid_att = entity.attributes.itemByName(group_name, "uuid") 95 | entity_uuid = get_brep_face_uuid_from_attribute(entity, uuid_att) 96 | # uuid will always be returned if this is a split face 97 | # as a special version of the parent uuid is returned 98 | if entity_uuid is not None: 99 | # We already have a uuid, so use it 100 | return entity_uuid 101 | # Add a uuid directly to the face 102 | unique_id = uuid.uuid1() 103 | entity.attributes.add(group_name, "uuid", str(unique_id)) 104 | return str(unique_id) 105 | 106 | 107 | def reset_uuid(entity, group_name="Dataset"): 108 | """Reset a uuid of an entity 109 | Returns the reset uuid of the entity""" 110 | unique_id = uuid.uuid1() 111 | entity.attributes.add(group_name, "uuid", str(unique_id)) 112 | return str(unique_id) 113 | 114 | 115 | def set_custom_uuid(entity, custom_uuid, group_name="Dataset"): 116 | entity.attributes.add(group_name, "uuid", custom_uuid) 117 | 118 | 119 | def set_uuids_for_collection(entities, group_name="Dataset"): 120 | for ent in entities: 121 | # Strange -- We sometimes get an None entity in the contraints array 122 | # when we have a SketchFixedSpline in the sketch. We guard against 123 | # that crashing the threads here 124 | if ent is not None: 125 | set_uuid(ent, group_name) 126 | 127 | 128 | def get_uuids_for_collection(entities, group_name="Dataset"): 129 | """Return a list of uuids from a collection""" 130 | uuids = [] 131 | for ent in entities: 132 | # Strange -- We sometimes get an None entity in the contraints array 133 | # when we have a SketchFixedSpline in the sketch. We guard against 134 | # that crashing the threads here 135 | if ent is not None: 136 | uuid = get_uuid(ent) 137 | uuids.append(uuid) 138 | return uuids 139 | 140 | 141 | def set_uuids_for_sketch(sketch, group_name="Dataset"): 142 | # Work around to ensure the profiles are populated 143 | # on a newly opened design 144 | sketch.isComputeDeferred = True 145 | sketch.isVisible = False 146 | sketch.isVisible = True 147 | sketch.isComputeDeferred = False 148 | # We are only interested points and curves 149 | set_uuids_for_collection(sketch.sketchCurves) 150 | set_uuids_for_collection(sketch.sketchPoints) 151 | 152 | 153 | def get_temp_ids_from_collection(collection): 154 | """From a collection, make a set of the tempids""" 155 | id_set = set() 156 | for entity in collection: 157 | if entity is not None: 158 | temp_id = entity.tempId 159 | id_set.add(temp_id) 160 | return id_set 161 | -------------------------------------------------------------------------------- /tools/common/test/common_test_base.py: -------------------------------------------------------------------------------- 1 | 2 | import adsk.core 3 | import adsk.fusion 4 | import unittest 5 | import os 6 | import sys 7 | from pathlib import Path 8 | 9 | 10 | class CommonTestBase(unittest.TestCase): 11 | 12 | def __init__(self): 13 | unittest.TestCase.__init__(self) 14 | self.app = adsk.core.Application.get() 15 | self.design = adsk.fusion.Design.cast(self.app.activeProduct) 16 | self.current_dir = Path(os.path.join(os.path.dirname(__file__))) 17 | self.testdata_dir = self.current_dir.parent.parent / "testdata" 18 | 19 | def clear(self): 20 | """Clear everything by closing all documents""" 21 | for doc in self.app.documents: 22 | # Save without closing 23 | doc.close(False) 24 | self.design = adsk.fusion.Design.cast(self.app.activeProduct) 25 | -------------------------------------------------------------------------------- /tools/common/test/common_test_harness.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "autodeskProduct": "Fusion360", 3 | "type": "script", 4 | "author": "", 5 | "description": { 6 | "": "" 7 | }, 8 | "supportedOS": "windows|mac", 9 | "editEnabled": true 10 | } -------------------------------------------------------------------------------- /tools/common/test/common_test_harness.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Common Test Harness 4 | 5 | """ 6 | import adsk.core 7 | import adsk.fusion 8 | import traceback 9 | 10 | from .test_geometry import TestGeometry 11 | 12 | 13 | def run(context): 14 | 15 | try: 16 | 17 | test_geometry = TestGeometry() 18 | test_geometry.run() 19 | 20 | except: 21 | print(traceback.format_exc()) 22 | -------------------------------------------------------------------------------- /tools/common/view_control.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | import adsk.fusion 3 | 4 | 5 | def disable_grid_display(): 6 | """Disable display of grid - useful to do before exporting a thumbnail""" 7 | app = adsk.core.Application.get() 8 | ui = app.userInterface 9 | cmd_def = ui.commandDefinitions.itemById('ViewLayoutGridCommand') 10 | list_control_def = adsk.core.ListControlDefinition.cast( 11 | cmd_def.controlDefinition) 12 | layout_grid_item = list_control_def.listItems.item(0) 13 | layout_grid_item.isSelected = False 14 | 15 | 16 | def orient_camera(offset, 17 | up_vector=adsk.core.Vector3D.create(0, 0, 1), 18 | target=adsk.core.Point3D.create(0, 0, 0), 19 | fit=True): 20 | """Orient the camera to look at a given target""" 21 | app = adsk.core.Application.get() 22 | design = adsk.fusion.Design.cast(app.activeProduct) 23 | 24 | # Get the existing camera and modify it 25 | camera = app.activeViewport.camera 26 | camera.isSmoothTransition = False 27 | 28 | # We will fit to the contents of the screen 29 | # So we just need to point the camera in the right direction 30 | camera.target = target 31 | camera.upVector = up_vector 32 | camera.eye = adsk.core.Point3D.create( 33 | target.x + offset.x, 34 | target.y + offset.y, 35 | target.z + offset.z 36 | ) 37 | 38 | camera.isFitView = True 39 | app.activeViewport.camera = camera # Update the viewport 40 | 41 | if(fit): 42 | # Set this once to fit to the camera view 43 | # But fit() needs to also be called below 44 | # Call fit to the screen after we have changed to top view 45 | app.activeViewport.fit() 46 | 47 | 48 | def set_geometry_visible(bodies=True, sketches=True, profiles=True): 49 | """Toggle the visibility of geometry""" 50 | app = adsk.core.Application.get() 51 | design = adsk.fusion.Design.cast(app.activeProduct) 52 | 53 | for component in design.allComponents: 54 | for body in component.bRepBodies: 55 | body.isVisible = bodies 56 | for sketch in component.sketches: 57 | sketch.isVisible = sketches 58 | sketch.areProfilesShown = profiles 59 | adsk.doEvents() 60 | -------------------------------------------------------------------------------- /tools/fusion360gym/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/fusion360gym/__init__.py -------------------------------------------------------------------------------- /tools/fusion360gym/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/fusion360gym/client/__init__.py -------------------------------------------------------------------------------- /tools/fusion360gym/client/gym_env.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Abstract Fusion 360 Gym Environment 4 | for launching and interacting with the gym 5 | 6 | """ 7 | import sys 8 | import os 9 | import json 10 | import time 11 | from pathlib import Path 12 | from requests.exceptions import ConnectionError 13 | import psutil 14 | 15 | from fusion360gym_client import Fusion360GymClient 16 | 17 | # Add the common folder to sys.path 18 | COMMON_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "common") 19 | if COMMON_DIR not in sys.path: 20 | sys.path.append(COMMON_DIR) 21 | 22 | from launcher import Launcher 23 | 24 | 25 | class GymEnv(): 26 | 27 | def __init__(self, host="127.0.0.1", port=8080, launch_gym=False): 28 | self.host = host 29 | self.port = port 30 | self.client = Fusion360GymClient(f"http://{self.host}:{self.port}") 31 | # Fusion subprocess 32 | self.p = None 33 | if launch_gym: 34 | self.launch_gym() 35 | 36 | def launch_gym(self): 37 | """Launch the Fusion 360 Gym on the given host/port""" 38 | print("Launching Gym...") 39 | if self.p is not None: 40 | # Give a second for Fusion to crash 41 | time.sleep(2) 42 | return_code = self.p.poll() 43 | print(f"Poll response is: {return_code}") 44 | # Kill the process if it is active 45 | if return_code is None: 46 | self.p.kill() 47 | self.p = None 48 | self.__write_launch_file() 49 | return self.__launch_gym() 50 | 51 | def kill_gym(self, including_parent=True): 52 | """Kill this instance of the Fusion 360 Gym""" 53 | print("Killing Gym...") 54 | if self.p is not None: 55 | try: 56 | parent = psutil.Process(self.p.pid) 57 | children = parent.children(recursive=True) 58 | for child in children: 59 | child.kill() 60 | gone, still_alive = psutil.wait_procs(children, timeout=5) 61 | if including_parent: 62 | parent.kill() 63 | parent.wait(5) 64 | except: 65 | print("Warning: Failed to kill Gym process tree") 66 | else: 67 | print("Warning: Gym process is None") 68 | 69 | def __write_launch_file(self): 70 | """Write the launch file that the gym reads to connect""" 71 | current_dir = Path(__file__).resolve().parent 72 | gym_server_dir = current_dir.parent / "server" 73 | launch_json_file = gym_server_dir / "launch.json" 74 | launch_data = {} 75 | if launch_json_file.exists(): 76 | with open(launch_json_file, "r") as f: 77 | launch_data = json.load(f) 78 | url = f"http://{self.host}:{self.port}" 79 | launch_data[url] = { 80 | "host": self.host, 81 | "port": self.port, 82 | "connected": False 83 | } 84 | print(f"Writing Launch file for: {url}") 85 | with open(launch_json_file, "w") as f: 86 | json.dump(launch_data, f, indent=4) 87 | 88 | def __launch_gym(self): 89 | """Launch the Fusion 360 Gym on the given host/port""" 90 | launcher = Launcher() 91 | self.p = launcher.launch() 92 | # We wait for Fusion to start responding to pings 93 | result = self.__wait_for_fusion() 94 | if result is False: 95 | # Fusion is awake but not responding so restart 96 | self.p.kill() 97 | self.p = None 98 | self.__launch_gym() 99 | else: 100 | return result 101 | 102 | def __wait_for_fusion(self): 103 | """Wait until Fusion has launched""" 104 | if self.p is None: 105 | print("Fusion 360 process is None") 106 | return 107 | print("Waiting for Fusion to launch...") 108 | attempts = 0 109 | max_attempts = 60 110 | time.sleep(1) 111 | while attempts < max_attempts: 112 | print(f"Attempting to ping Fusion") 113 | try: 114 | r = self.client.ping() 115 | if r is not None and r.status_code == 200: 116 | print("Ping response received") 117 | r.close() 118 | return True 119 | else: 120 | print("No ping response received") 121 | r.close() 122 | except ConnectionError as ex: 123 | print(f"Ping raised {type(ex).__name__}") 124 | attempts += 1 125 | time.sleep(5) 126 | return False 127 | 128 | def check_response(self, call, r): 129 | """Check the response is valid and raise exceptions if not""" 130 | if r is None: 131 | raise Exception(f"[{call}] response is None") 132 | if r.status_code != 200: 133 | response_data = r.json() 134 | raise Exception(f"[{call}] {r.status_code}: {response_data['message']}") 135 | -------------------------------------------------------------------------------- /tools/fusion360gym/examples/README.md: -------------------------------------------------------------------------------- 1 | # Fusion 360 Gym Examples 2 | This directory contains a number of examples that demonstrate how to work with the Fusion 360 Gym. 3 | 4 | ## Setup 5 | All examples require Fusion 360 to be open with the Fusion 360 Gym sever running. Follow the instructions in the [main readme](../README.md#running) for full setup details. 6 | 7 | ## Running 8 | For a simple example of how to interact with the server check out [client_example.py](client_example.py). 9 | ``` 10 | cd /path/to/fusion360gym/examples 11 | python client_example.py 12 | ``` 13 | This script will output various files to the `tools/test_data/output` directory and you will see Fusion 360 update as it processes requests. -------------------------------------------------------------------------------- /tools/fusion360gym/examples/client_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Simple example showing usage of the Fusion 360 Gym Client 4 | 5 | """ 6 | 7 | 8 | from pathlib import Path 9 | import sys 10 | import os 11 | import json 12 | 13 | # Add the client folder to sys.path 14 | CLIENT_DIR = os.path.join(os.path.dirname(__file__), "..", "client") 15 | if CLIENT_DIR not in sys.path: 16 | sys.path.append(CLIENT_DIR) 17 | 18 | from fusion360gym_client import Fusion360GymClient 19 | 20 | # Before running ensure the Fusion360GymServer is running 21 | # and configured with the same host name and port number 22 | HOST_NAME = "127.0.0.1" 23 | PORT_NUMBER = 8080 24 | 25 | 26 | def main(): 27 | current_dir = Path(__file__).resolve().parent 28 | data_dir = current_dir.parent.parent / "testdata" 29 | output_dir = data_dir / "output" 30 | if not output_dir.exists(): 31 | output_dir.mkdir() 32 | 33 | # SETUP 34 | # Create the client class to interact with the server 35 | client = Fusion360GymClient(f"http://{HOST_NAME}:{PORT_NUMBER}") 36 | # Clear to force close all documents in Fusion 37 | # Do this before a new reconstruction 38 | r = client.clear() 39 | # Example of how we read the response data 40 | response_data = r.json() 41 | print(f"[{r.status_code}] Response: {response_data['message']}") 42 | 43 | # RECONSTRUCT 44 | # The json file with our design 45 | box_design_json_file = data_dir / "SingleSketchExtrude.json" 46 | # First clear to start fresh 47 | r = client.clear() 48 | r = client.reconstruct(box_design_json_file) 49 | 50 | # MESH 51 | # Create an stl file in the data directory 52 | stl_file = output_dir / "test.stl" 53 | r = client.mesh(stl_file) 54 | 55 | # BREP 56 | # Create a 'step' file 57 | # Or change the file extension to 'smt' to save in that format 58 | step_file = output_dir / "test.step" 59 | r = client.brep(step_file) 60 | 61 | # SKETCHES 62 | # Create a directory for the sketches to go in 63 | sketch_dir = output_dir / "sketches" 64 | if not sketch_dir.exists(): 65 | sketch_dir.mkdir() 66 | # By default sketches are saves as png images 67 | r = client.sketches(sketch_dir) 68 | # Or we can export vector data as .dxf 69 | r = client.sketches(sketch_dir, ".dxf") 70 | 71 | # OTHER 72 | # Ping: check if the server is responding 73 | r = client.ping() 74 | # Refresh: force the ui to refresh 75 | r = client.refresh() 76 | # Detach: stop the server so Fusion becomes responsive again 77 | # r = client.detach() 78 | 79 | # DEBUGGING 80 | # # Uncomment to send an invalid file and show some error info 81 | # invalid_json_file = data_dir / "SingleSketchExtrude_Invalid.json" 82 | # r = client.reconstruct(invalid_json_file) 83 | # response_data = r.json() 84 | # # This will spew out the errors we caught on the server and the stack trace 85 | # print(f"[{r.status_code}] Response message: {response_data['message']}") 86 | 87 | print(f"Done! Check this folder for exported files: {output_dir}") 88 | 89 | 90 | if __name__ == "__main__": 91 | main() 92 | -------------------------------------------------------------------------------- /tools/fusion360gym/examples/face_extrusion_example.py: -------------------------------------------------------------------------------- 1 | """" 2 | 3 | Fusion 360 Gym face extrusion based reconstruction 4 | 5 | """ 6 | 7 | from pathlib import Path 8 | import sys 9 | import os 10 | import json 11 | import random 12 | 13 | # Add the client folder to sys.path 14 | CLIENT_DIR = os.path.join(os.path.dirname(__file__), "..", "client") 15 | if CLIENT_DIR not in sys.path: 16 | sys.path.append(CLIENT_DIR) 17 | 18 | from fusion360gym_client import Fusion360GymClient 19 | 20 | 21 | # Before running ensure the Fusion360Server is running 22 | # and configured with the same host name and port number 23 | HOST_NAME = "127.0.0.1" 24 | PORT_NUMBER = 8080 25 | 26 | 27 | def main(): 28 | # SETUP 29 | # Create the client class to interact with the server 30 | client = Fusion360GymClient(f"http://{HOST_NAME}:{PORT_NUMBER}") 31 | # Clear to force close all documents in Fusion 32 | r = client.clear() 33 | 34 | # Get our target design file 35 | current_dir = Path(__file__).resolve().parent 36 | data_dir = current_dir.parent.parent / "testdata" 37 | couch_design_smt_file = data_dir / "Couch.smt" 38 | # Set the target 39 | r = client.set_target(couch_design_smt_file) 40 | # The face adjacency graph is returned 41 | # which we use to pick nodes for face extrusion 42 | response_json = r.json() 43 | graph = response_json["data"]["graph"] 44 | nodes = graph["nodes"] 45 | # A series of actions to extrude from node to node in the graph 46 | r = client.add_extrudes_by_target_face([ 47 | { 48 | "start_face": nodes[0]["id"], 49 | "end_face": nodes[9]["id"], 50 | "operation": "NewBodyFeatureOperation" 51 | }, 52 | { 53 | "start_face": nodes[1]["id"], 54 | "end_face": nodes[3]["id"], 55 | "operation": "JoinFeatureOperation" 56 | } 57 | ]) 58 | response_json = r.json() 59 | response_data = response_json["data"] 60 | iou = response_data["iou"] 61 | print(f"Finished creating design using face extrusion with IoU value of {iou}") 62 | 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /tools/fusion360gym/examples/sketch_extrusion_example.py: -------------------------------------------------------------------------------- 1 | """" 2 | 3 | Fusion 360 Gym sketch extrusion based construction using lines 4 | 5 | """ 6 | 7 | from pathlib import Path 8 | import sys 9 | import os 10 | import json 11 | 12 | # Add the client folder to sys.path 13 | CLIENT_DIR = os.path.join(os.path.dirname(__file__), "..", "client") 14 | if CLIENT_DIR not in sys.path: 15 | sys.path.append(CLIENT_DIR) 16 | 17 | from fusion360gym_client import Fusion360GymClient 18 | 19 | 20 | # Before running ensure the Fusion360Server is running 21 | # and configured with the same host name and port number 22 | HOST_NAME = "127.0.0.1" 23 | PORT_NUMBER = 8080 24 | 25 | 26 | def main(): 27 | # SETUP 28 | # Create the client class to interact with the server 29 | client = Fusion360GymClient(f"http://{HOST_NAME}:{PORT_NUMBER}") 30 | # Clear to force close all documents in Fusion 31 | # Do this before a new reconstruction 32 | r = client.clear() 33 | 34 | current_dir = Path(__file__).resolve().parent 35 | data_dir = current_dir.parent.parent / "testdata" 36 | output_dir = data_dir / "output" 37 | if not output_dir.exists(): 38 | output_dir.mkdir() 39 | 40 | # Send a list of commands directly to the server to run in sequence 41 | # We need to load the json as a dict to reconstruct 42 | # NOTE: this will not work for all files 43 | # only ones with single profiles (or the first profile used) in a sketch 44 | couch_design_json_file = data_dir / "Couch.json" 45 | with open(couch_design_json_file) as file_handle: 46 | couch_design_json_data = json.load(file_handle) 47 | 48 | timeline = couch_design_json_data["timeline"] 49 | entities = couch_design_json_data["entities"] 50 | # Pull out just the profiles that are used for extrude operations 51 | profiles_used = get_extrude_profiles(timeline, entities) 52 | 53 | sketches = {} 54 | for timeline_object in timeline: 55 | entity_key = timeline_object["entity"] 56 | entity = entities[entity_key] 57 | if entity["type"] == "Sketch": 58 | sketches[entity_key] = add_sketch(client, entity, entity_key, profiles_used) 59 | elif entity["type"] == "ExtrudeFeature": 60 | add_extrude_feature(client, entity, entity_key, sketches) 61 | print("Finished creating design using add_line and add_extrude") 62 | 63 | 64 | def get_extrude_profiles(timeline, entities): 65 | """Get the profiles used with extrude operations""" 66 | profiles = set() 67 | for timeline_object in timeline: 68 | entity_key = timeline_object["entity"] 69 | entity = entities[entity_key] 70 | if entity["type"] == "ExtrudeFeature": 71 | for profile in entity["profiles"]: 72 | profiles.add(profile["profile"]) 73 | return profiles 74 | 75 | 76 | def add_sketch(client, sketch, sketch_id, profiles_used): 77 | """Add a sketch to the design""" 78 | # First we need a plane to sketch on 79 | ref_plane = sketch["reference_plane"] 80 | ref_plane_type = ref_plane["type"] 81 | # Default to making a sketch on the XY plane 82 | sketch_plane = "XY" 83 | if ref_plane_type == "ConstructionPlane": 84 | # Use the name as a reference to the plane axis 85 | sketch_plane = ref_plane["name"] 86 | elif ref_plane_type == "BRepFace": 87 | # Identify the face by a point that sits on it 88 | sketch_plane = ref_plane["point_on_face"] 89 | r = client.add_sketch(sketch_plane) 90 | # Get the sketch name back 91 | response_json = r.json() 92 | sketch_name = response_json["data"]["sketch_name"] 93 | profile_ids = add_profiles(client, sketch_name, sketch, profiles_used) 94 | return { 95 | "sketch_name": sketch_name, 96 | "profile_ids": profile_ids 97 | } 98 | 99 | 100 | def add_profiles(client, sketch_name, sketch, profiles_used): 101 | """Add the sketch profiles to the design""" 102 | profiles = sketch["profiles"] 103 | original_curves = sketch["curves"] 104 | transform = sketch["transform"] 105 | profile_ids = {} 106 | response_json = None 107 | for original_profile_id, profile in profiles.items(): 108 | # Check if this profile is used 109 | if original_profile_id in profiles_used: 110 | for loop in profile["loops"]: 111 | for curve in loop["profile_curves"]: 112 | if curve["type"] != "Line3D": 113 | print(f"Warning: Unsupported curve type - {curve['type']}") 114 | continue 115 | # Skip over curves that are construction geometry 116 | curve_id = curve["curve"] 117 | curve_construction_geom = original_curves[curve_id]["construction_geom"] 118 | if curve_construction_geom: 119 | continue 120 | # We have to send the sketch transform here 121 | # due to the way Fusion saves out data from designs 122 | r = client.add_line(sketch_name, curve["start_point"], curve["end_point"], transform) 123 | response_json = r.json() 124 | # Look at the response and add profiles to the lookup dict 125 | # mapping between the original uuids and the profiles 126 | response_data = response_json["data"] 127 | for re_profile in response_data["profiles"]: 128 | profile_ids[original_profile_id] = re_profile 129 | # Note we make a silly assumption that its always the first 130 | # profile we use, so we return early here 131 | # when there could be many profiles in a sketch 132 | return profile_ids 133 | 134 | 135 | def add_extrude_feature(client, extrude_feature, extrude_feature_id, sketches): 136 | """Add an extrude feature to the design""" 137 | # We only handle a single profile 138 | original_profile_id = extrude_feature["profiles"][0]["profile"] 139 | original_sketch_id = extrude_feature["profiles"][0]["sketch"] 140 | # Use the original ids to find the new ids 141 | sketch_name = sketches[original_sketch_id]["sketch_name"] 142 | profile_id = sketches[original_sketch_id]["profile_ids"][original_profile_id] 143 | # Pull out the other extrude parameters 144 | distance = extrude_feature["extent_one"]["distance"]["value"] 145 | operation = extrude_feature["operation"] 146 | # Add the extrude 147 | r = client.add_extrude(sketch_name, profile_id, distance, operation) 148 | response_json = r.json() 149 | response_data = response_json["data"] 150 | # response_data contains a lot of information about: 151 | # - face adjacency graph: response_data["graph"] 152 | # - extrude faces: response_data["extrude"] 153 | # - bounding box: response_data["bounding_box"] 154 | 155 | 156 | if __name__ == "__main__": 157 | main() 158 | -------------------------------------------------------------------------------- /tools/fusion360gym/examples/sketch_extrusion_point_example.py: -------------------------------------------------------------------------------- 1 | """" 2 | 3 | Fusion 360 Gym sketch extrusion based construction using points 4 | 5 | """ 6 | 7 | from pathlib import Path 8 | import sys 9 | import os 10 | import json 11 | import random 12 | 13 | # Add the client folder to sys.path 14 | CLIENT_DIR = os.path.join(os.path.dirname(__file__), "..", "client") 15 | if CLIENT_DIR not in sys.path: 16 | sys.path.append(CLIENT_DIR) 17 | 18 | from fusion360gym_client import Fusion360GymClient 19 | 20 | 21 | # Before running ensure the Fusion360Server is running 22 | # and configured with the same host name and port number 23 | HOST_NAME = "127.0.0.1" 24 | PORT_NUMBER = 8080 25 | 26 | 27 | def main(): 28 | # Create the client class to interact with the server 29 | client = Fusion360GymClient(f"http://{HOST_NAME}:{PORT_NUMBER}") 30 | # Clear to force close all documents in Fusion 31 | r = client.clear() 32 | 33 | # Create an empty sketch on a random plane 34 | planes = ["XY", "XZ", "YZ"] 35 | random_plane = random.choice(planes) 36 | r = client.add_sketch(random_plane) 37 | # Get the unique name of the sketch created 38 | response_json = r.json() 39 | sketch_name = response_json["data"]["sketch_name"] 40 | # Add four lines to the sketch to make a square 41 | pts = [ 42 | {"x": -5, "y": -5}, 43 | {"x": 5, "y": -5}, 44 | {"x": 5, "y": 5}, 45 | {"x": -5, "y": 5} 46 | ] 47 | for pt in pts: 48 | client.add_point(sketch_name, pt) 49 | r = client.close_profile(sketch_name) 50 | 51 | # Pull out the first profile id 52 | response_json = r.json() 53 | response_data = response_json["data"] 54 | profile_id = next(iter(response_data["profiles"])) 55 | random_distance = random.randrange(5, 10) 56 | # Extrude by a random distance to make a new body 57 | r = client.add_extrude(sketch_name, profile_id, random_distance, "NewBodyFeatureOperation") 58 | 59 | # Pick a random face for the next sketch 60 | response_json = r.json() 61 | response_data = response_json["data"] 62 | faces = response_data["extrude"]["faces"] 63 | random_face = random.choice(faces) 64 | # Create a second sketch on a random face 65 | r = client.add_sketch(random_face["face_id"]) 66 | response_json = r.json() 67 | sketch_name = response_json["data"]["sketch_name"] 68 | # Draw the second smaller square 69 | pts = [ 70 | {"x": 2, "y": 2}, 71 | {"x": 3, "y": 2}, 72 | {"x": 3, "y": 3}, 73 | {"x": 2, "y": 3} 74 | ] 75 | for pt in pts: 76 | r = client.add_point(sketch_name, pt) 77 | r = client.close_profile(sketch_name) 78 | 79 | # Pull out the first profile id 80 | response_json = r.json() 81 | response_data = response_json["data"] 82 | profile_id = next(iter(response_data["profiles"])) 83 | # Extrude by a given distance, adding to the existing body 84 | random_distance = random.randrange(1, 5) 85 | # operations = ["JoinFeatureOperation", "CutFeatureOperation", "IntersectFeatureOperation", "NewBodyFeatureOperation"] 86 | r = client.add_extrude(sketch_name, profile_id, random_distance, "JoinFeatureOperation") 87 | 88 | # Its also possible to do different extrude operations here, for example a cut with a negative extrude value 89 | # random_distance = random.randrange(-5, -1) 90 | # r = client.add_extrude(sketch_name, profile_id, random_distance, "CutFeatureOperation") 91 | 92 | print("Finished creating design using add_point and add_extrude") 93 | 94 | 95 | if __name__ == "__main__": 96 | main() 97 | -------------------------------------------------------------------------------- /tools/fusion360gym/server/.gitignore: -------------------------------------------------------------------------------- 1 | launch.json -------------------------------------------------------------------------------- /tools/fusion360gym/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/fusion360gym/server/__init__.py -------------------------------------------------------------------------------- /tools/fusion360gym/server/command_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Base Command Class 4 | 5 | """ 6 | 7 | import adsk.core 8 | import os 9 | import sys 10 | from pathlib import Path 11 | import tempfile 12 | import importlib 13 | 14 | # Add the common folder to sys.path 15 | COMMON_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "common")) 16 | if COMMON_DIR not in sys.path: 17 | sys.path.append(COMMON_DIR) 18 | 19 | import serialize 20 | import geometry 21 | import regraph 22 | importlib.reload(serialize) 23 | importlib.reload(geometry) 24 | importlib.reload(regraph) 25 | from regraph import Regraph 26 | 27 | 28 | class CommandBase(): 29 | 30 | def __init__(self, runner, design_state): 31 | self.runner = runner 32 | self.design_state = design_state 33 | self.logger = None 34 | self.app = adsk.core.Application.get() 35 | self.design = adsk.fusion.Design.cast(self.app.activeProduct) 36 | self.state = {} 37 | 38 | def set_logger(self, logger): 39 | self.logger = logger 40 | 41 | def clear(self): 42 | """Clear the state""" 43 | self.state = {} 44 | self.design = adsk.fusion.Design.cast(self.app.activeProduct) 45 | 46 | def get_temp_file(self, file, dest_dir=None): 47 | """Return a file with a given name in a temp directory""" 48 | if dest_dir is None: 49 | dest_dir = Path(tempfile.mkdtemp()) 50 | # Make the dir if we need to 51 | if not dest_dir.exists(): 52 | dest_dir.mkdir(parents=True) 53 | 54 | temp_file = dest_dir / file 55 | return temp_file 56 | 57 | def check_file(self, data, valid_formats): 58 | """Check that the data has a valid file value""" 59 | if data is None or "file" not in data: 60 | return None, "file not specified" 61 | data_file = Path(data["file"]) 62 | if data_file.suffix not in valid_formats: 63 | return None, "invalid file extension specified" 64 | return data_file, None 65 | 66 | def return_extrude_data(self, extrude): 67 | """Return data from an extrude operation""" 68 | regraph = Regraph( 69 | reconstruction=self.design_state.reconstruction, 70 | logger=self.logger, 71 | mode="PerFace", 72 | use_temp_id=True, 73 | include_labels=False 74 | ) 75 | return_data = {} 76 | # Info on the extrude 77 | return_data["extrude"] = serialize.extrude_feature_brep(extrude) 78 | # Generate the graph from the reconstruction component 79 | return_data["graph"] = regraph.generate_from_bodies( 80 | self.design_state.reconstruction.bRepBodies 81 | ) 82 | # Calculate the IoU 83 | if self.design_state.target is not None: 84 | return_data["iou"] = geometry.intersection_over_union( 85 | self.design_state.target, 86 | self.design_state.reconstruction 87 | ) 88 | if return_data["iou"] is None: 89 | self.logger.log("Warning! IoU calculation returned None") 90 | # Bounding box of the reconstruction component 91 | bbox = geometry.get_bounding_box(self.design_state.reconstruction) 92 | return_data["bounding_box"] = serialize.bounding_box3d(bbox) 93 | return self.runner.return_success(return_data) 94 | -------------------------------------------------------------------------------- /tools/fusion360gym/server/command_runner.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Run incoming commands from the client 4 | 5 | """ 6 | 7 | import adsk.core 8 | import adsk.fusion 9 | import traceback 10 | import tempfile 11 | import shutil 12 | import os 13 | from zipfile import ZipFile 14 | from pathlib import Path 15 | 16 | from .command_export import CommandExport 17 | from .command_sketch_extrusion import CommandSketchExtrusion 18 | from .command_face_extrusion import CommandFaceExtrusion 19 | from .command_reconstruct import CommandReconstruct 20 | from .design_state import DesignState 21 | 22 | 23 | class CommandRunner(): 24 | 25 | def __init__(self): 26 | self.logger = None 27 | self.app = adsk.core.Application.get() 28 | self.last_command = "" 29 | self.design_state = DesignState(self) 30 | self.export = CommandExport(self, self.design_state) 31 | self.sketch_extrusion = CommandSketchExtrusion(self, self.design_state) 32 | self.face_extrusion = CommandFaceExtrusion(self, self.design_state) 33 | self.reconstruct = CommandReconstruct(self, self.design_state) 34 | self.command_objects = [ 35 | self.export, 36 | self.sketch_extrusion, 37 | self.face_extrusion, 38 | self.reconstruct 39 | ] 40 | self.design_state.set_command_objects(self.command_objects) 41 | 42 | def set_logger(self, logger): 43 | """Set the logger in all command objects""" 44 | self.logger = logger 45 | self.design_state.set_logger(logger) 46 | for obj in self.command_objects: 47 | obj.set_logger(logger) 48 | 49 | def run_command(self, command, data=None): 50 | """Run a command and route it to the right method""" 51 | try: 52 | self.last_command = command 53 | result = None 54 | if command == "ping": 55 | result = self.ping() 56 | elif command == "refresh": 57 | result = self.design_state.refresh() 58 | elif command == "reconstruct": 59 | result = self.reconstruct.reconstruct(data) 60 | elif command == "reconstruct_sketch": 61 | result = self.reconstruct.reconstruct_sketch(data) 62 | elif command == "reconstruct_profile": 63 | result = self.reconstruct.reconstruct_profile(data) 64 | elif command == "reconstruct_curve": 65 | result = self.reconstruct.reconstruct_curve(data) 66 | elif command == "reconstruct_curves": 67 | result = self.reconstruct.reconstruct_curves(data) 68 | elif command == "clear": 69 | result = self.design_state.clear() 70 | elif command == "mesh": 71 | result = self.export.mesh(data) 72 | elif command == "brep": 73 | result = self.export.brep(data) 74 | elif command == "sketches": 75 | result = self.export.sketches(data) 76 | elif command == "screenshot": 77 | result = self.export.screenshot(data) 78 | elif command == "graph": 79 | result = self.export.graph(data) 80 | elif command == "add_sketch": 81 | result = self.sketch_extrusion.add_sketch(data) 82 | elif command == "add_point": 83 | result = self.sketch_extrusion.add_point(data) 84 | elif command == "add_line": 85 | result = self.sketch_extrusion.add_line(data) 86 | elif command == "add_arc": 87 | result = self.sketch_extrusion.add_arc(data) 88 | elif command == "add_circle": 89 | result = self.sketch_extrusion.add_circle(data) 90 | elif command == "close_profile": 91 | result = self.sketch_extrusion.close_profile(data) 92 | elif command == "add_extrude": 93 | result = self.sketch_extrusion.add_extrude(data) 94 | elif command == "set_target": 95 | result = self.face_extrusion.set_target(data) 96 | elif command == "revert_to_target": 97 | result = self.face_extrusion.revert_to_target() 98 | elif command == "add_extrude_by_target_face": 99 | result = self.face_extrusion.add_extrude_by_target_face(data) 100 | elif command == "add_extrudes_by_target_face": 101 | result = self.face_extrusion.add_extrudes_by_target_face(data) 102 | else: 103 | return self.return_failure("Unknown command") 104 | return result 105 | except Exception as ex: 106 | return self.return_exception(ex) 107 | finally: 108 | # Update the UI 109 | adsk.doEvents() 110 | 111 | def ping(self): 112 | """Ping for debugging""" 113 | return self.return_success() 114 | 115 | def return_success(self, data=None): 116 | message = f"Success processing {self.last_command} command" 117 | return 200, message, data 118 | 119 | def return_failure(self, reason): 120 | message = f"Failed processing {self.last_command} command due to {reason}" 121 | return 500, message, None 122 | 123 | def return_exception(self, ex): 124 | message = f"""Error processing {self.last_command} command\n 125 | Exception of type {type(ex)} with args: {ex.args}\n 126 | {traceback.format_exc()}""" 127 | return 500, message, None 128 | -------------------------------------------------------------------------------- /tools/fusion360gym/server/design_state.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Design State for the Fusion 360 Gym 4 | 5 | """ 6 | 7 | import adsk.core 8 | import adsk.fusion 9 | 10 | 11 | class DesignState(): 12 | 13 | def __init__(self, runner): 14 | self.runner = runner 15 | self.app = adsk.core.Application.get() 16 | self.design = adsk.fusion.Design.cast(self.app.activeProduct) 17 | self.logger = None 18 | self.command_objects = None 19 | 20 | # Setup the target and reconstruction design components 21 | # the target design gets set later if required 22 | self.target = None 23 | # The reconstruction design we always setup 24 | self.reconstruction = None 25 | self.setup_reconstruction() 26 | 27 | def set_logger(self, logger): 28 | self.logger = logger 29 | 30 | def set_command_objects(self, command_objects): 31 | """Set a reference to the command objects""" 32 | self.command_objects = command_objects 33 | 34 | def refresh(self): 35 | """Refresh the active viewport""" 36 | self.app.activeViewport.refresh() 37 | return self.runner.return_success() 38 | 39 | def clear(self): 40 | """Clear (i.e. close) all open designs in Fusion""" 41 | for doc in self.app.documents: 42 | # Save without closing 43 | doc.close(False) 44 | if self.command_objects is not None: 45 | # Reset state in all command objects 46 | for obj in self.command_objects: 47 | obj.clear() 48 | self.target = None 49 | self.design = adsk.fusion.Design.cast(self.app.activeProduct) 50 | self.setup_reconstruction() 51 | return self.runner.return_success() 52 | 53 | def setup_reconstruction(self): 54 | """Create the reconstruction component""" 55 | self.reconstruction = self.design.rootComponent.occurrences.addNewComponent( 56 | adsk.core.Matrix3D.create() 57 | ) 58 | self.reconstruction.activate() 59 | name = f"Reconstruction_{self.reconstruction.component.name}" 60 | self.reconstruction.component.name = name 61 | adsk.doEvents() 62 | 63 | def clear_reconstruction(self): 64 | """Clear the reconstruction to an empty component""" 65 | self.reconstruction.deleteMe() 66 | self.setup_reconstruction() 67 | 68 | def set_target(self, file): 69 | """Set the target component from a smt or step file""" 70 | if file.suffix == ".step" or file.suffix == ".stp": 71 | import_options = self.app.importManager.createSTEPImportOptions( 72 | str(file.resolve()) 73 | ) 74 | else: 75 | import_options = self.app.importManager.createSMTImportOptions( 76 | str(file.resolve()) 77 | ) 78 | import_options.isViewFit = False 79 | imported_designs = self.app.importManager.importToTarget2( 80 | import_options, 81 | self.design.rootComponent 82 | ) 83 | self.target = imported_designs[0] 84 | name = f"Target_{self.target.component.name}" 85 | self.target.component.name = name 86 | adsk.doEvents() 87 | -------------------------------------------------------------------------------- /tools/fusion360gym/server/fusion360gym_server.manifest: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "autodeskProduct": "Fusion360", 4 | "type": "addin", 5 | "id": "", 6 | "author": "", 7 | "description": { 8 | "": "" 9 | }, 10 | "version": "", 11 | "runOnStartup": true, 12 | "supportedOS": "windows|mac", 13 | "editEnabled": true 14 | } -------------------------------------------------------------------------------- /tools/fusion360gym/server/launch.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Launcher for multiple Fusion 360 server instances 4 | 5 | """ 6 | import os 7 | import sys 8 | from pathlib import Path 9 | import subprocess 10 | import argparse 11 | import json 12 | import time 13 | import importlib 14 | 15 | # Add the client folder to sys.path 16 | CLIENT_DIR = os.path.join(os.path.dirname(__file__), "..", "client") 17 | if CLIENT_DIR not in sys.path: 18 | sys.path.append(CLIENT_DIR) 19 | from fusion360gym_client import Fusion360GymClient 20 | 21 | COMMON_DIR = os.path.abspath(os.path.join( 22 | os.path.dirname(__file__), "..", "..", "common")) 23 | if COMMON_DIR not in sys.path: 24 | sys.path.append(COMMON_DIR) 25 | from launcher import Launcher 26 | 27 | LAUNCH_JSON_FILE = Path("launch.json") 28 | DEFAULT_HOST = "127.0.0.1" 29 | DEFAULT_PORT = 8080 30 | 31 | parser = argparse.ArgumentParser() 32 | parser.add_argument("--detach", dest="detach", default=False, action="store_true", help="Detach the launched Fusion 360 instances [default: False]") 33 | parser.add_argument("--ping", dest="ping", default=False, action="store_true", help="Ping the launched Fusion 360 instances [default: False]") 34 | parser.add_argument("--host", type=str, default=DEFAULT_HOST, help="Host name as an IP address [default: 127.0.0.1]") 35 | parser.add_argument("--start_port", type=int, default=DEFAULT_PORT, help="The starting port for the first Fusion 360 instance [default: 8080]") 36 | parser.add_argument("--instances", type=int, default=1, help="The number of Fusion 360 instances to start [default: 1]") 37 | args = parser.parse_args() 38 | 39 | 40 | def create_launch_json(host, start_port, instances): 41 | """Launch instruction file to be read by the server on startup""" 42 | launch_data = {} 43 | for instance in range(instances): 44 | port = start_port + instance 45 | url = f"http://{host}:{port}" 46 | launch_data[url] = { 47 | "host": host, 48 | "port": port, 49 | "connected": False 50 | } 51 | with open(LAUNCH_JSON_FILE, "w") as file_handle: 52 | json.dump(launch_data, file_handle, indent=4) 53 | 54 | 55 | def launch_instances(host, start_port, instances): 56 | """Launch multiple instances of Fusion 360""" 57 | launcher = Launcher() 58 | for port in range(start_port, start_port + instances): 59 | print(f"Launching Fusion 360 instance: {host}:{port}") 60 | launcher.launch() 61 | time.sleep(5) 62 | 63 | 64 | def detach_endpoint(endpoint): 65 | """Detach an endpoint""" 66 | try: 67 | client = Fusion360GymClient(endpoint) 68 | print(f"Detaching {endpoint}...") 69 | client.detach() 70 | except Exception as ex: 71 | print(f"Error detaching server {endpoint}: {ex}") 72 | 73 | 74 | def detach(): 75 | """Detach the launched servers to make the Fusion UI responsive""" 76 | if LAUNCH_JSON_FILE.exists(): 77 | with open(LAUNCH_JSON_FILE) as file_handle: 78 | launch_data = json.load(file_handle) 79 | for endpoint, server in launch_data.items(): 80 | if server["connected"]: 81 | detach_endpoint(endpoint) 82 | else: 83 | url = f"http://{args.host}:{args.start_port}" 84 | detach_endpoint(url) 85 | 86 | 87 | def ping_endpoint(endpoint): 88 | """Ping an endpoint""" 89 | try: 90 | client = Fusion360GymClient(endpoint) 91 | r = client.ping() 92 | print(f"Ping response from {endpoint}: {r.status_code}") 93 | except Exception as ex: 94 | print(f"Error pinging server {endpoint}: {ex}") 95 | 96 | 97 | def ping(): 98 | """Ping the launched servers to see if they respond""" 99 | if LAUNCH_JSON_FILE.exists(): 100 | with open(LAUNCH_JSON_FILE) as file_handle: 101 | launch_data = json.load(file_handle) 102 | for endpoint, server in launch_data.items(): 103 | ping_endpoint(endpoint) 104 | else: 105 | ping_endpoint(f"http://{DEFAULT_HOST}:{DEFAULT_PORT}") 106 | 107 | 108 | if __name__ == "__main__": 109 | if args.ping: 110 | ping() 111 | elif args.detach: 112 | detach() 113 | else: 114 | create_launch_json(args.host, args.start_port, args.instances) 115 | launch_instances(args.host, args.start_port, args.instances) 116 | -------------------------------------------------------------------------------- /tools/fusion360gym/test/.gitignore: -------------------------------------------------------------------------------- 1 | test_config.json -------------------------------------------------------------------------------- /tools/fusion360gym/test/README.md: -------------------------------------------------------------------------------- 1 | # Fusion 360 Gym Tests 2 | Tests use the [Python Unit testing framework](https://docs.python.org/3/library/unittest.html). 3 | 4 | 5 | ## Visual Studio Code Setup 6 | Tests can be setup and run from [Visual Studio Code](https://code.visualstudio.com/docs/python/testing) by adding the following to the `settings.json` file in the `.vscode/` folder at the root of the repo. 7 | 8 | ```json 9 | { 10 | "python.testing.unittestArgs": [ 11 | "-v", 12 | "-s", 13 | "./tools/fusion360gym/test", 14 | "-p", 15 | "test_*.py" 16 | ], 17 | "python.testing.pytestEnabled": false, 18 | "python.testing.nosetestsEnabled": false, 19 | "python.testing.unittestEnabled": true, 20 | "python.testing.autoTestDiscoverOnSaveEnabled": false 21 | } 22 | ``` 23 | 24 | 25 | ## Test Config File 26 | To run all tests requires a `test_config.json` file, in the test directory, to store the path to the dataset, required by some tests. The contents of that file are as follows: 27 | 28 | ```json 29 | { 30 | "dataset_dir": "/path/to/reconstruction/dataset" 31 | } 32 | ``` -------------------------------------------------------------------------------- /tools/fusion360gym/test/test_detach_util.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Test utility for detaching the server 4 | 5 | """ 6 | import unittest 7 | import requests 8 | import sys 9 | import os 10 | import importlib 11 | 12 | # Add the client folder to sys.path 13 | CLIENT_DIR = os.path.join(os.path.dirname(__file__), "..", "client") 14 | if CLIENT_DIR not in sys.path: 15 | sys.path.append(CLIENT_DIR) 16 | from fusion360gym_client import Fusion360GymClient 17 | 18 | HOST_NAME = "127.0.0.1" 19 | PORT_NUMBER = 8080 20 | 21 | 22 | class TestDetachUtil(unittest.TestCase): 23 | 24 | @classmethod 25 | def setUpClass(cls): 26 | cls.client = Fusion360GymClient(f"http://{HOST_NAME}:{PORT_NUMBER}") 27 | 28 | @unittest.skip("Skipping detach") 29 | def test_detach(self): 30 | r = self.client.detach() 31 | self.assertEqual(r.status_code, 200) 32 | 33 | 34 | if __name__ == "__main__": 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /tools/fusion360gym/test/test_fusion360gym_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Test basic functionality of the Fusion 360 Server 4 | Including reconstruction 5 | 6 | """ 7 | import unittest 8 | import requests 9 | from pathlib import Path 10 | import sys 11 | import os 12 | import numpy 13 | from stl import mesh 14 | import importlib 15 | import json 16 | import shutil 17 | 18 | # Add the client folder to sys.path 19 | CLIENT_DIR = os.path.join(os.path.dirname(__file__), "..", "client") 20 | if CLIENT_DIR not in sys.path: 21 | sys.path.append(CLIENT_DIR) 22 | 23 | from fusion360gym_client import Fusion360GymClient 24 | 25 | HOST_NAME = "127.0.0.1" 26 | PORT_NUMBER = 8080 27 | 28 | 29 | class TestFusion360Server(unittest.TestCase): 30 | 31 | @classmethod 32 | def setUpClass(cls): 33 | cls.client = Fusion360GymClient(f"http://{HOST_NAME}:{PORT_NUMBER}") 34 | 35 | def test_ping(self): 36 | r = self.client.ping() 37 | self.assertEqual(r.status_code, 200, msg="ping status code") 38 | 39 | def test_refresh(self): 40 | r = self.client.refresh() 41 | self.assertEqual(r.status_code, 200, msg="refresh status code") 42 | 43 | def test_clear(self): 44 | r = self.client.clear() 45 | self.assertEqual(r.status_code, 200, msg="clear status code") 46 | 47 | # @classmethod 48 | # def tearDownClass(cls): 49 | # cls.client.detach() 50 | 51 | 52 | if __name__ == "__main__": 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /tools/joint2cad/README.md: -------------------------------------------------------------------------------- 1 | # Joint2CAD 2 | 3 | ![Joint2CAD](https://i.gyazo.com/d6dfaf36990a4014b5860456abab3494.gif) 4 | 5 | [Joint2CAD](joint2cad.py) demonstrates how to build a Fusion 360 CAD model from the joint data provided with the [Assembly Dataset](../../docs/assembly_joint.md). The resulting CAD model has a fully specified parametric joint. 6 | 7 | 8 | ## Running 9 | [Joint2CAD](joint2cad.py) runs in Fusion 360 as a script with the following steps. 10 | 1. Follow the [general instructions here](../) to get setup with Fusion 360. 11 | 2. Optionally change the `joint_file` in [`joint2cad.py`](joint2cad.py) to point towards a `joint_set_xxxxx.json` provided with the [Assembly Dataset](../../docs/assembly.md) joint data. 12 | 3. Optionally change the `png_file` and `f3d_file` in [`joint2cad.py`](joint2cad.py) to your preferred name for each file that is exported. 13 | 4. Run the [`joint2cad.py`](joint2cad.py) script from within Fusion 360. When the script has finished running the design will be open in Fusion 360. 14 | 5. Check the contents of `joint2cad/` directory to find the .f3d that was exported. 15 | 16 | ## How it Works 17 | If you look into the code you will notice that the hard work is performed by [`joint_importer.py`](../common/joint_importer.py) and does the following: 18 | 1. Opens and reads `joint_set_xxxxx.json`. 19 | 2. Imports the .smt files for each of the parts and creates a component for each. 20 | 3. Creates a joint as specified in the `joint_set_xxxxx.json`. 21 | 22 | -------------------------------------------------------------------------------- /tools/joint2cad/joint2cad.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "autodeskProduct": "Fusion360", 3 | "type": "script", 4 | "author": "", 5 | "description": { 6 | "": "" 7 | }, 8 | "supportedOS": "windows|mac", 9 | "editEnabled": true 10 | } -------------------------------------------------------------------------------- /tools/joint2cad/joint2cad.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Construct a Fusion 360 CAD model from a joint set file 4 | provided with the Fusion 360 Gallery Assembly Dataset joint data 5 | 6 | """ 7 | 8 | 9 | import os 10 | import sys 11 | from pathlib import Path 12 | import adsk.core 13 | import traceback 14 | 15 | # Add the common folder to sys.path 16 | COMMON_DIR = os.path.join(os.path.dirname(__file__), "..", "common") 17 | if COMMON_DIR not in sys.path: 18 | sys.path.append(COMMON_DIR) 19 | 20 | from joint_importer import JointImporter 21 | import exporter 22 | 23 | 24 | def run(context): 25 | ui = None 26 | try: 27 | app = adsk.core.Application.get() 28 | # Turn on component color cycling first 29 | ui = app.userInterface 30 | ui.commandDefinitions.itemById("ViewColorCyclingOnCmd").execute() 31 | adsk.doEvents() 32 | 33 | current_dir = Path(__file__).resolve().parent 34 | data_dir = current_dir.parent / "testdata/joint_examples" 35 | joint_file = data_dir / "joint_set_00119.json" 36 | 37 | joint_importer = JointImporter(joint_file) 38 | joint_importer.reconstruct(joint_index=0) 39 | 40 | png_file = current_dir / f"{joint_file.stem}.png" 41 | exporter.export_png_from_component(png_file, app.activeProduct.rootComponent) 42 | 43 | f3d_file = current_dir / f"{joint_file.stem}.f3d" 44 | exporter.export_f3d(f3d_file) 45 | 46 | if ui: 47 | if f3d_file.exists(): 48 | ui.messageBox(f"Exported to: {f3d_file}") 49 | else: 50 | ui.messageBox(f"Failed to export: {f3d_file}") 51 | 52 | except: 53 | if ui: 54 | ui.messageBox(f"Failed to export: {traceback.format_exc()}") 55 | 56 | -------------------------------------------------------------------------------- /tools/reconverter/.gitignore: -------------------------------------------------------------------------------- 1 | output/ -------------------------------------------------------------------------------- /tools/reconverter/README.md: -------------------------------------------------------------------------------- 1 | # Reconstruction Converter 2 | ![Reconverter Output](https://i.gyazo.com/8639956e2a5bb551a823f8fcad4c7049.gif) 3 | 4 | The Reconstruction Converter (aka Reconverter) demonstrates how to batch convert the raw data structure provided with the [Reconstruction Dataset](../../docs/reconstruction.md) into other representations: 5 | - Images for each curve drawn 6 | - Mesh files after each extrude operation 7 | - B-Rep files of the final design 8 | 9 | Reconverter uses common modules from the Fusion 360 Gym directly to allow lower level control. 10 | 11 | ## Running 12 | Reconverter runs in Fusion 360 as a script with the following steps. 13 | 1. Follow the [general instructions here](../) to get setup with Fusion 360. 14 | 2. Optionally change the `data_dir` in [`reconverter.py`](reconverter.py) to point towards a folder of json data from the reconstruction dataset. By default it reconstructs the json files found in [this folder](../testdata). 15 | 3. Run the [`reconverter.py`](reconverter.py) script from within Fusion 360 16 | 4. Check the contents of the `../testdata/output` folder for the exported files 17 | -------------------------------------------------------------------------------- /tools/reconverter/reconverter.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "autodeskProduct": "Fusion360", 3 | "type": "script", 4 | "author": "", 5 | "description": { 6 | "": "" 7 | }, 8 | "supportedOS": "windows|mac", 9 | "editEnabled": true 10 | } -------------------------------------------------------------------------------- /tools/reconverter/reconverter.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | import adsk.fusion 3 | import traceback 4 | import json 5 | import os 6 | import sys 7 | import time 8 | from pathlib import Path 9 | import importlib 10 | 11 | 12 | # Add the common folder to sys.path 13 | COMMON_DIR = os.path.abspath(os.path.join( 14 | os.path.dirname(__file__), "..", "common")) 15 | if COMMON_DIR not in sys.path: 16 | sys.path.append(COMMON_DIR) 17 | 18 | import exporter 19 | importlib.reload(exporter) 20 | import view_control 21 | from logger import Logger 22 | from sketch_extrude_importer import SketchExtrudeImporter 23 | 24 | 25 | class Reconverter(): 26 | """Reconstruction Converter 27 | Takes a reconstruction json file and converts it 28 | to different formats""" 29 | 30 | def __init__(self, json_file): 31 | self.json_file = json_file 32 | # Export data to this directory 33 | self.output_dir = json_file.parent / "output" 34 | if not self.output_dir.exists(): 35 | self.output_dir.mkdir(parents=True) 36 | # References to the Fusion design 37 | self.app = adsk.core.Application.get() 38 | self.design = adsk.fusion.Design.cast(self.app.activeProduct) 39 | # Counter for the number of design actions that have taken place 40 | self.inc_action_index = 0 41 | # Size of the images to export 42 | self.width = 1024 43 | self.height = 1024 44 | 45 | def reconstruct(self): 46 | """Reconstruct the design from the json file""" 47 | self.home_camera = self.app.activeViewport.camera 48 | self.home_camera.isSmoothTransition = False 49 | self.home_camera.isFitView = True 50 | importer = SketchExtrudeImporter(self.json_file) 51 | importer.reconstruct(self.inc_export) 52 | 53 | 54 | def inc_export(self, data): 55 | """Callback function called whenever a the design changes 56 | i.e. when a curve is added or an extrude 57 | This enables us to save out incremental data""" 58 | if "curve" in data: 59 | self.inc_export_curve(data) 60 | elif "sketch" in data: 61 | # No new geometry is added 62 | pass 63 | elif "extrude" in data: 64 | self.inc_export_extrude(data) 65 | self.inc_action_index += 1 66 | 67 | def inc_export_curve(self, data): 68 | """Save out incremental sketch data as reconstruction takes place""" 69 | png_file = f"{self.json_file.stem}_{self.inc_action_index:04}.png" 70 | png_file_path = self.output_dir / png_file 71 | # Show all geometry 72 | view_control.set_geometry_visible(True, True, True) 73 | exporter.export_png_from_sketch( 74 | png_file_path, 75 | data["sketch"], # Reference to the sketch object that was updated 76 | reset_camera=True, # Zoom to fit the sketch 77 | width=self.width, 78 | height=self.height 79 | ) 80 | 81 | def inc_export_extrude(self, data): 82 | """Save out incremental extrude data as reconstruction takes place""" 83 | png_file = f"{self.json_file.stem}_{self.inc_action_index:04}.png" 84 | png_file_path = self.output_dir / png_file 85 | # Show bodies, sketches, and hide profiles 86 | view_control.set_geometry_visible(True, True, False) 87 | # Restore the home camera 88 | self.app.activeViewport.camera = self.home_camera 89 | # save view of bodies enabled, sketches turned off 90 | exporter.export_png_from_component( 91 | png_file_path, 92 | self.design.rootComponent, 93 | reset_camera=False, 94 | width=self.width, 95 | height=self.height 96 | ) 97 | # Save out just obj file geometry at each extrude 98 | obj_file = f"{self.json_file.stem}_{self.inc_action_index:04}.obj" 99 | obj_file_path = self.output_dir / obj_file 100 | exporter.export_obj_from_component(obj_file_path, self.design.rootComponent) 101 | 102 | def export(self): 103 | """Export the final design in a different format""" 104 | # Meshes 105 | stl_file = self.output_dir / f"{self.json_file.stem}.stl" 106 | exporter.export_stl_from_component(stl_file, self.design.rootComponent) 107 | obj_file = self.output_dir / f"{self.json_file.stem}.obj" 108 | exporter.export_obj_from_component(obj_file, self.design.rootComponent) 109 | # B-Reps 110 | step_file = self.output_dir / f"{self.json_file.stem}.step" 111 | exporter.export_step_from_component( 112 | step_file, self.design.rootComponent) 113 | smt_file = self.output_dir / f"{self.json_file.stem}.smt" 114 | exporter.export_smt_from_component(smt_file, self.design.rootComponent) 115 | # Image 116 | png_file = self.output_dir / f"{self.json_file.stem}.png" 117 | # Hide sketches 118 | view_control.set_geometry_visible(True, False, False) 119 | exporter.export_png_from_component( 120 | png_file, 121 | self.design.rootComponent, 122 | reset_camera=False, 123 | width=1024, 124 | height=1024 125 | ) 126 | 127 | 128 | def run(context): 129 | try: 130 | app = adsk.core.Application.get() 131 | # Logger to print to the text commands window in Fusion 132 | logger = Logger() 133 | # Fusion requires an absolute path 134 | current_dir = Path(__file__).resolve().parent 135 | data_dir = current_dir.parent / "testdata" 136 | 137 | # Get all the files in the data folder 138 | json_files = [ 139 | data_dir / "Couch.json", 140 | # data_dir / "Hexagon.json" 141 | ] 142 | 143 | json_count = len(json_files) 144 | for i, json_file in enumerate(json_files, start=1): 145 | try: 146 | logger.log(f"[{i}/{json_count}] Reconstructing {json_file}") 147 | reconverter = Reconverter(json_file) 148 | reconverter.reconstruct() 149 | # At this point the final design 150 | # should be available in Fusion 151 | reconverter.export() 152 | except Exception as ex: 153 | logger.log(f"Error reconstructing: {ex}") 154 | finally: 155 | # Close the document 156 | # Fusion automatically opens a new window 157 | # after the last one is closed 158 | app.activeDocument.close(False) 159 | 160 | except: 161 | print(traceback.format_exc()) 162 | -------------------------------------------------------------------------------- /tools/regraph/README.md: -------------------------------------------------------------------------------- 1 | # Regraph: Reconstruction Graph Exporter 2 | 'Regraph' demonstrates how to batch convert the raw data structure provided with the [Reconstruction Dataset](../../docs/reconstruction.md) into graphs representing the B-Rep topology with features on faces and edges. 3 | 4 | Regraph 5 | 6 | ## Running 7 | Regraph runs in Fusion 360 as a script with the following steps. 8 | 1. Follow the [general instructions here](../) to get setup with Fusion 360. 9 | 2. Optionally change the `data_dir` in [`regraph_exporter.py`](regraph_exporter.py) to point towards a folder of json data from the reconstruction dataset. By default it reconstructs the json files found in [this folder](../testdata). 10 | 3. Run the [`regraph_exporter.py`](regraph_exporter.py) addin from within Fusion 360. 11 | 4. Check the contents of the `output` folder for the exported files. 12 | 13 | To regenerate the data, delete the output folder and rerun. 14 | 15 | ## Preprocessed Data 16 | We also provide the [pre-processed data](https://fusion-360-gallery-dataset.s3.us-west-2.amazonaws.com/reconstruction/r1.0.1/r1.0.1_regraph_05.zip) used for training in the paper. For details on how to use this data for training, see the [regraphnet documentation](../regraphnet). 17 | 18 | ## Output Format 19 | Data is exported in json that can be read using networkx. See [regraph_viewer.ipynb](regraph_viewer.ipynb) for an example of how to load the data into networkx. From there it can be [loaded into pytorch geometric for example](https://pytorch-geometric.readthedocs.io/en/latest/modules/utils.html#torch_geometric.utils.from_networkx). 20 | 21 | ## PerExtrude Mode 22 | When `mode` is set to `PerExtrude`, a graph is created for each extrude operation in the timeline. 23 | 24 | ### Face Features 25 | The following features are given for each face: 26 | - `surface_type`: The type of surface, see [API reference](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/SurfaceTypes.htm). 27 | - `reversed`: If the normal of this face is reversed with respect to the surface geometry associated with this face, see [API Reference](http://help.autodesk.com/view/fusion360/ENU/?guid=GUID-54B1FCE4-25BB-4C37-BF2A-A984739B13E1). 28 | - `area`: The area of the face. 29 | - `normal_*`: The normal vector of the face. 30 | - `max_tangent_*`: The output directions of maximum curvature at a point at or near the center of the face. 31 | - `max_curvature`: The output magnitude of the maximum curvature at a point at or near the center of the face. 32 | - `min_curvature`: The output magnitude of the maximum curvature at a point at or near the center of the face 33 | 34 | ### Edge Features 35 | The following features are given for each edge: 36 | - `curve_type`: The type of curve, see [API reference](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/Curve3DTypes.htm). 37 | - `length`: The length of the curve. 38 | - `convexity`: The convexity of the edge in relation to the two faces it connects. Can be one of: `Convex`, `Concave`, or `Smooth`. 39 | - `perpendicular`: True/False flag indicating if the edge connects two perpendicular faces. 40 | - `direction_*`: The output direction of the curvature at a point at or near the center of the edge. 41 | - `curvature`: The output magnitude of the curvature at a point at or near the center of the edge. 42 | 43 | 44 | ## PerFace Mode 45 | When `mode` is set to `PerFace`, a target graph is created for the full design, along with a `*_sequence.json` file that includes the steps to press/pull and extrude the faces to make the final design. 46 | 47 | ### Sequence Data 48 | The following data is provided for each step in the `sequence` list: 49 | - `start_face`: The id of the start face used at this step. 50 | - `end_face`: The id of the end face used at this step. 51 | - `operation`: The type of extrude operation used at this step. This will be one of either `JoinFeatureOperation`, `CutFeatureOperation`, `IntersectFeatureOperation`, or `NewBodyFeatureOperation`. See [`FeatureOperations` documentation](http://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/FeatureOperations.htm). 52 | - `faces`: The ids of the faces that are explained at this step. 53 | - `edges`: The ids of the edges that are explained at this step. 54 | 55 | Additionally a `bounding_box` is provided that in the `properties` data structure that can be used to normalize any geometry in model space. 56 | 57 | ### Face Features 58 | Currently the following features are given for each edge (see [UV-Net](https://arxiv.org/abs/2006.10211)): 59 | - `surface_type`: The type of surface for the face. This will be one of either `PlaneSurfaceType`, `CylinderSurfaceType`, `ConeSurfaceType`, `SphereSurfaceType`, `TorusSurfaceType`, `EllipticalCylinderSurfaceType`, `EllipticalConeSurfaceType`, or `NurbsSurfaceType`. See [`SurfaceTypes` documentation](http://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/SurfaceTypes.htm). 60 | - `points`: Points sampled on the face in model space. The order is by xyz point in row first order `u0_v0, u0_v1, u0_v2... uN_vN`. 61 | - `normals`: Normals at the corresponding point location. 62 | - `trimming_mask`: Binary value indicating if the corresponding point is inside/outside the trimming boundary. 63 | 64 | ## Face Labels 65 | The following labels are given for each face: 66 | - `operation_label`: The type of extrude operation. Can be one of: `CutFeatureOperation`, `IntersectFeatureOperation`, `JoinFeatureOperation`. 67 | - `location_in_feature_label`: The location of the face in the extrude feature. Can be one of `StartFace`, `SideFace`, or `EndFace`. 68 | - `timeline_index_label`: An integer value indicating the position in the timeline when the face was created. -------------------------------------------------------------------------------- /tools/regraph/launch.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Autmatically run regraph_exporter.py 4 | and handle relaunching Fusion 360 if necessary 5 | 6 | Requires regraph_exporter to be set to Run On Startup 7 | inside of Fusion 360 8 | 9 | """ 10 | 11 | import os 12 | import sys 13 | from pathlib import Path 14 | import subprocess 15 | import json 16 | import time 17 | 18 | # Add the common folder to sys.path 19 | COMMON_DIR = os.path.abspath(os.path.join( 20 | os.path.dirname(__file__), "..", "common")) 21 | if COMMON_DIR not in sys.path: 22 | sys.path.append(COMMON_DIR) 23 | 24 | from launcher import Launcher 25 | 26 | 27 | def launch_loop(launcher, results_file): 28 | """Launch Fusion again and again in a loop""" 29 | p = launcher.launch() 30 | killing = False 31 | 32 | while p.poll() is None: 33 | if time_out_reached(results_file): 34 | if not killing: 35 | p.kill() 36 | killing = True 37 | else: 38 | time.sleep(1) 39 | 40 | if killing: 41 | # Update the file to avoid infinite loop 42 | results_file.touch() 43 | print(f"Fusion killed after timeout, relaunching...") 44 | else: 45 | print(f"Fusion crashed with code {p.returncode}, relaunching...") 46 | launch_loop(launcher, results_file) 47 | 48 | 49 | def time_out_reached(results_file): 50 | """Check for a timeout by 51 | checking the results file to see if it has been updated""" 52 | if not results_file.exists(): 53 | return False 54 | start_time = os.path.getmtime(results_file) 55 | time_elapsed = time.time() - start_time 56 | print(f"Time processing current file: {time_elapsed}\r", end="") 57 | # Wait for this amount of time before killing 58 | time_out_limit = 15 * 60 59 | return time_elapsed > time_out_limit 60 | 61 | 62 | if __name__ == "__main__": 63 | current_dir = Path(__file__).resolve().parent 64 | data_dir = current_dir.parent / "testdata" 65 | output_dir = data_dir / "output" 66 | # We use the timestamp of this file to check we haven't timed out 67 | # This file is updated at regular intervals when all is working 68 | results_file = output_dir / "regraph_results.json" 69 | if results_file.exists(): 70 | # Touch the file to start the timer 71 | results_file.touch() 72 | 73 | launcher = Launcher() 74 | launch_loop(launcher, results_file) 75 | -------------------------------------------------------------------------------- /tools/regraph/regraph_exporter.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "autodeskProduct": "Fusion360", 3 | "type": "addin", 4 | "author": "", 5 | "description": { 6 | "": "" 7 | }, 8 | "supportedOS": "windows|mac", 9 | "editEnabled": true 10 | } -------------------------------------------------------------------------------- /tools/regraphnet/README.md: -------------------------------------------------------------------------------- 1 | # Reconstruction Neural Network Agent 2 | Message passing neural network to estimate next command probabilities for recovering a construction sequence from B-Rep input. 3 | 4 | ![Network Architecture](https://i.gyazo.com/7f30e61ce2ecc86d1016da0565badc68.png) 5 | 6 | ## Publication 7 | For further details on the method, please refer to [our paper](https://arxiv.org/abs/2010.02392). 8 | ``` 9 | @article{willis2020fusion, 10 | title={Fusion 360 Gallery: A Dataset and Environment for Programmatic CAD Construction from Human Design Sequences}, 11 | author={Karl D. D. Willis and Yewen Pu and Jieliang Luo and Hang Chu and Tao Du and Joseph G. Lambourne and Armando Solar-Lezama and Wojciech Matusik}, 12 | journal={ACM Transactions on Graphics (TOG)}, 13 | volume={40}, 14 | number={4}, 15 | year={2021}, 16 | publisher={ACM New York, NY, USA} 17 | } 18 | ``` 19 | 20 | ## Setup 21 | 1. Install requirements: 22 | - `pytorch` tested with 1.7.0, gpu not required 23 | - `torch_geometric` tested with 1.6.1 24 | - `numpy` tested with 1.18.1 25 | - `scipy` tested with 1.4.1 26 | 2. Prepare data using [Regraph](../regraph) or [download the pre-processed data](https://fusion-360-gallery-dataset.s3.us-west-2.amazonaws.com/reconstruction/r1.0.1/r1.0.1_regraph_05.zip) used for training in the paper. 27 | 28 | ## Training 29 | We provide the pre-trained models used in the paper in the [ckpt directory](ckpt). To train a model run [`train.py`](./src/train.py) from the `src` directory as follows: 30 | ``` 31 | python train.py --dataset /path/to/regraph/data/ --split /path/to/train_test.json 32 | ``` 33 | This will launch training using the specified dataset and split file. The split file is the `train_test.json` file provided with the reconstruction dataset. 34 | 35 | ### Training Arguments 36 | The full list of training arguments is as follows: 37 | - `--no_cuda`: Train on CPU [default: `False`] 38 | - `--dataset`: Folder name of the dataset created with [Regraph](../regraph) 39 | - `--split`: Train/test split file, as provided with the reconstruction dataset 40 | - `--mpn`: Message passing network to use, can be `gcn`, `mlp`, `gat`, or `gin` [default: `gcn`] 41 | - `--augment`: Directory for augmentation data 42 | - `--only_augment`: Train using only the augmented data [default: `False`] 43 | - `--exp_name`: Name of the experiment used for the checkpoint and log files. 44 | - `--epochs`, `lr`, `weight_decay`, `hidden`, `dropout`, `seed`: Specify training hyper-parameters. 45 | 46 | ### Training Results Log 47 | Training results are stored by default in the `log` directory as JSON files. Each JSON file contains a list of steps in the following structure: 48 | 49 | ```js 50 | [ 51 | { 52 | "train/test": 'Train', 53 | "epoch": 0, 54 | "loss": x, 55 | "start_acc": x%, 56 | "end_acc": x%, 57 | "operation_acc": x%, 58 | "overall_acc": x% 59 | }, 60 | ... 61 | ] 62 | ``` 63 | - `train/test`: Data split for current log entry 64 | - `epoch`, `loss`: Training epoch and loss 65 | - `start_acc`, `end_acc`, `operation_acc`: Current accuracy logs for the three separate outputs 66 | - `overall_acc`: Accuracy for three outputs all being correct 67 | 68 | 69 | 70 | ## Inference 71 | We provide an [inference example](src/inference.py) that can be run as follows from the `src` directory: 72 | ``` 73 | python inference.py 74 | ``` 75 | This example loads the example sequence provided in the `data` directory and performs inference. 76 | 77 | ### Inference Arguments 78 | The full list of inference arguments is as follows: 79 | - `--no_cuda`: Train on CPU [default: `False`] 80 | - `--dataset`: Folder name of the dataset created with [Regraph](../regraph) [default: `data`] 81 | - `--mpn`: Message passing network to use, can be `gcn`, `mlp`, `gat`, or `gin` [default: `gcn`] 82 | - `--augment`: Use the checkpoint trained with augmented data [default: `False`] 83 | - `--only_augment`: Use the checkpoint trained on just the augmented data [default: `False`] 84 | -------------------------------------------------------------------------------- /tools/regraphnet/ckpt/model_gat.ckpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/regraphnet/ckpt/model_gat.ckpt -------------------------------------------------------------------------------- /tools/regraphnet/ckpt/model_gcn.ckpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/regraphnet/ckpt/model_gcn.ckpt -------------------------------------------------------------------------------- /tools/regraphnet/ckpt/model_gcn_aug.ckpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/regraphnet/ckpt/model_gcn_aug.ckpt -------------------------------------------------------------------------------- /tools/regraphnet/ckpt/model_gcn_semisyn.ckpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/regraphnet/ckpt/model_gcn_semisyn.ckpt -------------------------------------------------------------------------------- /tools/regraphnet/ckpt/model_gcn_syn.ckpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/regraphnet/ckpt/model_gcn_syn.ckpt -------------------------------------------------------------------------------- /tools/regraphnet/ckpt/model_gin.ckpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/regraphnet/ckpt/model_gin.ckpt -------------------------------------------------------------------------------- /tools/regraphnet/ckpt/model_mlp.ckpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/regraphnet/ckpt/model_mlp.ckpt -------------------------------------------------------------------------------- /tools/regraphnet/ckpt/model_mlp_aug.ckpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/regraphnet/ckpt/model_mlp_aug.ckpt -------------------------------------------------------------------------------- /tools/regraphnet/data/31962_e5291336_0054_sequence.json: -------------------------------------------------------------------------------- 1 | { 2 | "sequence": [ 3 | { 4 | "start_face": "073c66a2-f2ef-11ea-930c-066234a457d1", 5 | "end_face": "073c4d94-f2ef-11ea-825b-066234a457d1", 6 | "operation": "NewBodyFeatureOperation", 7 | "graph": "31962_e5291336_0054_0000.json" 8 | }, 9 | { 10 | "start_face": "073cea70-f2ef-11ea-9796-066234a457d1", 11 | "end_face": "073c4d94-f2ef-11ea-825b-066234a457d1", 12 | "operation": "JoinFeatureOperation", 13 | "graph": "31962_e5291336_0054_0001.json" 14 | }, 15 | { 16 | "start_face": "073d74d8-f2ef-11ea-947a-066234a457d1", 17 | "end_face": "073c66a2-f2ef-11ea-930c-066234a457d1", 18 | "operation": "CutFeatureOperation", 19 | "graph": "31962_e5291336_0054_0002.json" 20 | } 21 | ], 22 | "properties": { 23 | "bounding_box": { 24 | "type": "BoundingBox3D", 25 | "max_point": { 26 | "type": "Point3D", 27 | "x": 0.2999999999999998, 28 | "y": -7.333841812473256, 29 | "z": 22.516315822017432 30 | }, 31 | "min_point": { 32 | "type": "Point3D", 33 | "x": -2.0, 34 | "y": -7.883841812473253, 35 | "z": 21.966315822017435 36 | } 37 | }, 38 | "extrude_count": 3, 39 | "body_count": 1, 40 | "timeline_count": 6 41 | } 42 | } -------------------------------------------------------------------------------- /tools/regraphnet/src/inference.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | 4 | if __name__=="__main__": 5 | # args 6 | parser=argparse.ArgumentParser() 7 | parser.add_argument('--no_cuda',action='store_true',default=True,help='Disables CUDA training.') 8 | parser.add_argument('--dataset',type=str,default='data',help='Dataset name.') 9 | parser.add_argument('--mpn',type=str,default='gcn',choices=['gcn','mlp','gat','gin'],help='Message passing network to use, can be gcn or mlp or gat or gin [default: gcn]') 10 | parser.add_argument('--augment',dest='augment',default=False,action='store_true',help='Use the checkpoint trained with additional augmented data') 11 | parser.add_argument('--only_augment',dest='only_augment',default=False,action='store_true',help='Use the checkpoint trained on only augmented data') 12 | args=parser.parse_args() 13 | 14 | checkpoint_name=f'model_{args.mpn}' 15 | if args.augment: 16 | checkpoint_name+='_aug' 17 | elif args.only_augment: 18 | checkpoint_name+='_syn' 19 | checkpoint_file=f'../ckpt/{checkpoint_name}.ckpt' 20 | if not os.path.isfile(checkpoint_file): 21 | print(f'Checkpoint file not found: {checkpoint_file}') 22 | exit() 23 | else: 24 | print(f'Using checkpoint file: {checkpoint_file}') 25 | 26 | if args.mpn in ['gcn','mlp']: 27 | from inference_vanilla import * 28 | args.cuda=not args.no_cuda and torch.cuda.is_available() 29 | # load model 30 | model=NodePointer(nfeat=708,nhid=256,Use_GCN=(args.mpn=='gcn')) 31 | if args.cuda: 32 | model.load_state_dict(torch.load(checkpoint_file)) 33 | model.cuda() 34 | else: 35 | model.load_state_dict( 36 | torch.load(checkpoint_file, map_location=torch.device("cpu")) 37 | ) 38 | # inference 39 | t1=time.time() 40 | for seq in ['31962_e5291336_0054']: 41 | # load _sequence.json, as an example 42 | path_seq='../%s/%s_sequence.json'%(args.dataset,seq) 43 | if not os.path.isfile(path_seq): 44 | continue 45 | with open(path_seq) as json_data: 46 | data_seq=json.load(json_data) 47 | path_tar='../%s/%s'%(args.dataset,data_seq['sequence'][-1]['graph']) 48 | bbox=data_seq['properties']['bounding_box'] 49 | for t in range(len(data_seq['sequence'])): 50 | # load and format graph data from json files 51 | if t==0: 52 | path_cur=None 53 | else: 54 | path_cur='../%s/%s_%04d.json'%(args.dataset,seq,t-1) 55 | graph_pair_formatted,node_names,operation_names=load_graph_pair(path_tar,path_cur,bbox) 56 | if args.cuda: 57 | for j in range(4): 58 | graph_pair_formatted[j]=graph_pair_formatted[j].cuda() 59 | # inference 60 | actions_sorted,probs_sorted=inference(graph_pair_formatted,model,node_names,operation_names,use_gpu=args.cuda) 61 | print(actions_sorted[:10]) 62 | print(probs_sorted[:10]) 63 | t2=time.time() 64 | print('%.5f seconds.'%(t2-t1)) 65 | else: 66 | from inference_torch_geometric import * 67 | args.cuda=not args.no_cuda and torch.cuda.is_available() 68 | # load model 69 | model=NodePointer(nfeat=708,nhid=256,MPN_type=args.mpn) 70 | model_parameters=filter(lambda p: p.requires_grad, model.parameters()) 71 | params=sum([np.prod(p.size()) for p in model_parameters]) 72 | print('Number params: ',params) 73 | if args.cuda: 74 | model.load_state_dict(torch.load(checkpoint_file)) 75 | model.cuda() 76 | else: 77 | model.load_state_dict( 78 | torch.load(checkpoint_file, map_location=torch.device("cpu")) 79 | ) 80 | # inference 81 | t1=time.time() 82 | for seq in ['31962_e5291336_0054']: 83 | # load _sequence.json, as an example 84 | path_seq='../%s/%s_sequence.json'%(args.dataset,seq) 85 | if not os.path.isfile(path_seq): 86 | continue 87 | with open(path_seq) as json_data: 88 | data_seq=json.load(json_data) 89 | path_tar='../%s/%s'%(args.dataset,data_seq['sequence'][-1]['graph']) 90 | bbox=data_seq['properties']['bounding_box'] 91 | for t in range(len(data_seq['sequence'])): 92 | # load and format graph data from json files 93 | if t==0: 94 | path_cur=None 95 | else: 96 | path_cur='../%s/%s_%04d.json'%(args.dataset,seq,t-1) 97 | graph_pair_formatted,node_names,operation_names=load_graph_pair(path_tar,path_cur,bbox) 98 | if args.cuda: 99 | for j in range(4): 100 | graph_pair_formatted[j]=graph_pair_formatted[j].cuda() 101 | # inference 102 | actions_sorted,probs_sorted=inference(graph_pair_formatted,model,node_names,operation_names,use_gpu=args.cuda) 103 | print(actions_sorted[:10]) 104 | print(probs_sorted[:10]) 105 | t2=time.time() 106 | print('%.5f seconds.'%(t2-t1)) 107 | -------------------------------------------------------------------------------- /tools/regraphnet/src/inference_torch_geometric.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from __future__ import print_function 3 | 4 | import os 5 | import json 6 | import time 7 | import argparse 8 | import numpy as np 9 | 10 | import torch 11 | import torch.nn as nn 12 | import torch.nn.functional as F 13 | 14 | from train_torch_geometric import * 15 | 16 | def load_graph_pair(path_tar,path_cur,bbox): 17 | action_type_dict={'CutFeatureOperation':1,'IntersectFeatureOperation':2,'JoinFeatureOperation':0,'NewBodyFeatureOperation':3,'NewComponentFeatureOperation':4} 18 | operation_names=['JoinFeatureOperation','CutFeatureOperation','IntersectFeatureOperation','NewBodyFeatureOperation','NewComponentFeatureOperation'] 19 | with open(path_tar) as json_data: 20 | data_tar=json.load(json_data) 21 | edges_idx_tar,features_tar=format_graph_data(data_tar,bbox) 22 | if not path_cur: 23 | edges_idx_cur,features_cur=torch.zeros((0)),torch.zeros((0)) 24 | else: 25 | with open(path_cur) as json_data: 26 | data_cur=json.load(json_data) 27 | edges_idx_cur,features_cur=format_graph_data(data_cur,bbox) 28 | graph_pair_formatted=[edges_idx_tar,features_tar,edges_idx_cur,features_cur] 29 | node_names=[x['id'] for x in data_tar['nodes']] 30 | return graph_pair_formatted,node_names,operation_names 31 | 32 | def inference(graph_pair_formatted,model,node_names,operation_names,use_gpu=False): 33 | model.eval() 34 | num_nodes=graph_pair_formatted[1].size()[0] 35 | output_end_conditioned=np.zeros((num_nodes,num_nodes)) 36 | with torch.no_grad(): 37 | graph_pair_formatted.append(0) 38 | output_start,_,output_op=model(graph_pair_formatted,use_gpu=use_gpu) 39 | output_start=F.softmax(output_start.view(1,-1),dim=1) 40 | output_op=F.softmax(output_op,dim=1) 41 | for i in range(num_nodes): 42 | graph_pair_formatted[4]=i 43 | _,output_end,_=model(graph_pair_formatted,use_gpu=use_gpu) 44 | output_end=F.softmax(output_end.view(1,-1),dim=1) 45 | if use_gpu: 46 | output_end_conditioned[i,:]=output_end.data.cpu().numpy() 47 | else: 48 | output_end_conditioned[i,:]=output_end.data.numpy() 49 | if use_gpu: 50 | ps=[output_start.data.cpu().numpy()[0,:],output_end_conditioned,output_op.data.cpu().numpy()[0,:]] 51 | else: 52 | ps=[output_start.data.numpy()[0,:],output_end_conditioned,output_op.data.numpy()[0,:]] 53 | # enumerate all actions 54 | actions,probs=[],[] 55 | for i in range(len(node_names)): 56 | for j in range(len(node_names)): 57 | for k in range(len(operation_names)): 58 | actions.append([node_names[i],node_names[j],operation_names[k]]) 59 | probs.append(ps[0][i]*ps[1][i,j]*ps[2][k]) 60 | actions_sorted,probs_sorted=[],[] 61 | idx=np.argsort(-np.array(probs)) 62 | for i in range(len(probs)): 63 | actions_sorted.append(actions[idx[i]]) 64 | probs_sorted.append(probs[idx[i]]) 65 | return actions_sorted,probs_sorted 66 | 67 | if __name__=="__main__": 68 | # args 69 | parser=argparse.ArgumentParser() 70 | parser.add_argument('--no-cuda',action='store_true',default=True,help='Disables CUDA training.') 71 | parser.add_argument('--dataset',type=str,default='data',help='Dataset name.') 72 | parser.add_argument('--mpn',type=str,default='gat',choices=['gat','gin'],help='Message passing network to use, can be gat or gin [default: gat]') 73 | args=parser.parse_args() 74 | args.cuda=not args.no_cuda and torch.cuda.is_available() 75 | # load model 76 | model=NodePointer(nfeat=708,nhid=256,MPN_type=args.mpn) 77 | model_parameters=filter(lambda p: p.requires_grad, model.parameters()) 78 | params=sum([np.prod(p.size()) for p in model_parameters]) 79 | print('Number params: ',params) 80 | checkpoint_file='../ckpt/model_%s.ckpt'%(args.mpn) 81 | if args.cuda: 82 | model.load_state_dict(torch.load(checkpoint_file)) 83 | model.cuda() 84 | else: 85 | model.load_state_dict( 86 | torch.load(checkpoint_file, map_location=torch.device("cpu")) 87 | ) 88 | # inference 89 | t1=time.time() 90 | for seq in ['31962_e5291336_0054']: 91 | # load _sequence.json, as an example 92 | path_seq='../%s/%s_sequence.json'%(args.dataset,seq) 93 | if not os.path.isfile(path_seq): 94 | continue 95 | with open(path_seq) as json_data: 96 | data_seq=json.load(json_data) 97 | path_tar='../%s/%s'%(args.dataset,data_seq['sequence'][-1]['graph']) 98 | bbox=data_seq['properties']['bounding_box'] 99 | for t in range(len(data_seq['sequence'])): 100 | # load and format graph data from json files 101 | if t==0: 102 | path_cur=None 103 | else: 104 | path_cur='../%s/%s_%04d.json'%(args.dataset,seq,t-1) 105 | graph_pair_formatted,node_names,operation_names=load_graph_pair(path_tar,path_cur,bbox) 106 | if args.cuda: 107 | for j in range(4): 108 | graph_pair_formatted[j]=graph_pair_formatted[j].cuda() 109 | # inference 110 | actions_sorted,probs_sorted=inference(graph_pair_formatted,model,node_names,operation_names,use_gpu=args.cuda) 111 | print(actions_sorted[:10]) 112 | print(probs_sorted[:10]) 113 | t2=time.time() 114 | print('%.5f seconds.'%(t2-t1)) 115 | -------------------------------------------------------------------------------- /tools/regraphnet/src/inference_vanilla.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from __future__ import print_function 3 | 4 | import os 5 | import json 6 | import time 7 | import argparse 8 | import numpy as np 9 | 10 | import torch 11 | import torch.nn as nn 12 | import torch.nn.functional as F 13 | 14 | from train_vanilla import * 15 | 16 | def load_graph_pair(path_tar,path_cur,bbox): 17 | action_type_dict={'CutFeatureOperation':1,'IntersectFeatureOperation':2,'JoinFeatureOperation':0,'NewBodyFeatureOperation':3,'NewComponentFeatureOperation':4} 18 | operation_names=['JoinFeatureOperation','CutFeatureOperation','IntersectFeatureOperation','NewBodyFeatureOperation','NewComponentFeatureOperation'] 19 | with open(path_tar) as json_data: 20 | data_tar=json.load(json_data) 21 | adj_tar,features_tar=format_graph_data(data_tar,bbox) 22 | if not path_cur: 23 | adj_cur,features_cur=torch.zeros((0)),torch.zeros((0)) 24 | else: 25 | with open(path_cur) as json_data: 26 | data_cur=json.load(json_data) 27 | adj_cur,features_cur=format_graph_data(data_cur,bbox) 28 | graph_pair_formatted=[adj_tar,features_tar,adj_cur,features_cur] 29 | node_names=[x['id'] for x in data_tar['nodes']] 30 | return graph_pair_formatted,node_names,operation_names 31 | 32 | def inference(graph_pair_formatted,model,node_names,operation_names,use_gpu=False): 33 | model.eval() 34 | num_nodes=graph_pair_formatted[1].size()[0] 35 | output_end_conditioned=np.zeros((num_nodes,num_nodes)) 36 | with torch.no_grad(): 37 | graph_pair_formatted.append(0) 38 | output_start,_,output_op=model(graph_pair_formatted,use_gpu=use_gpu) 39 | output_start=F.softmax(output_start.view(1,-1),dim=1) 40 | output_op=F.softmax(output_op,dim=1) 41 | for i in range(num_nodes): 42 | graph_pair_formatted[4]=i 43 | _,output_end,_=model(graph_pair_formatted,use_gpu=use_gpu) 44 | output_end=F.softmax(output_end.view(1,-1),dim=1) 45 | if use_gpu: 46 | output_end_conditioned[i,:]=output_end.data.cpu().numpy() 47 | else: 48 | output_end_conditioned[i,:]=output_end.data.numpy() 49 | if use_gpu: 50 | ps=[output_start.data.cpu().numpy()[0,:],output_end_conditioned,output_op.data.cpu().numpy()[0,:]] 51 | else: 52 | ps=[output_start.data.numpy()[0,:],output_end_conditioned,output_op.data.numpy()[0,:]] 53 | # enumerate all actions 54 | actions,probs=[],[] 55 | for i in range(len(node_names)): 56 | for j in range(len(node_names)): 57 | for k in range(len(operation_names)): 58 | actions.append([node_names[i],node_names[j],operation_names[k]]) 59 | probs.append(ps[0][i]*ps[1][i,j]*ps[2][k]) 60 | actions_sorted,probs_sorted=[],[] 61 | idx=np.argsort(-np.array(probs)) 62 | for i in range(len(probs)): 63 | actions_sorted.append(actions[idx[i]]) 64 | probs_sorted.append(probs[idx[i]]) 65 | return actions_sorted,probs_sorted 66 | 67 | if __name__=="__main__": 68 | # args 69 | parser=argparse.ArgumentParser() 70 | parser.add_argument('--no-cuda',action='store_true',default=True,help='Disables CUDA training.') 71 | parser.add_argument('--dataset',type=str,default='data',help='Dataset name.') 72 | parser.add_argument('--mpn',type=str,default='gcn',choices=['gcn','mlp'],help='Message passing network to use, can be gcn or mlp [default: gcn]') 73 | args=parser.parse_args() 74 | args.cuda=not args.no_cuda and torch.cuda.is_available() 75 | # load model 76 | model=NodePointer(nfeat=708,nhid=256,Use_GCN=(args.mpn=='gcn')) 77 | checkpoint_file='../ckpt/model_%s.ckpt'%(args.mpn) 78 | if args.cuda: 79 | model.load_state_dict(torch.load(checkpoint_file)) 80 | model.cuda() 81 | else: 82 | model.load_state_dict( 83 | torch.load(checkpoint_file, map_location=torch.device("cpu")) 84 | ) 85 | # inference 86 | t1=time.time() 87 | for seq in ['31962_e5291336_0054']: 88 | # load _sequence.json, as an example 89 | path_seq='../%s/%s_sequence.json'%(args.dataset,seq) 90 | if not os.path.isfile(path_seq): 91 | continue 92 | with open(path_seq) as json_data: 93 | data_seq=json.load(json_data) 94 | path_tar='../%s/%s'%(args.dataset,data_seq['sequence'][-1]['graph']) 95 | bbox=data_seq['properties']['bounding_box'] 96 | for t in range(len(data_seq['sequence'])): 97 | # load and format graph data from json files 98 | if t==0: 99 | path_cur=None 100 | else: 101 | path_cur='../%s/%s_%04d.json'%(args.dataset,seq,t-1) 102 | graph_pair_formatted,node_names,operation_names=load_graph_pair(path_tar,path_cur,bbox) 103 | if args.cuda: 104 | for j in range(4): 105 | graph_pair_formatted[j]=graph_pair_formatted[j].cuda() 106 | # inference 107 | actions_sorted,probs_sorted=inference(graph_pair_formatted,model,node_names,operation_names,use_gpu=args.cuda) 108 | print(actions_sorted[:10]) 109 | print(probs_sorted[:10]) 110 | t2=time.time() 111 | print('%.5f seconds.'%(t2-t1)) 112 | -------------------------------------------------------------------------------- /tools/regraphnet/src/train.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | if __name__=="__main__": 4 | # args 5 | parser=argparse.ArgumentParser() 6 | parser.add_argument('--no_cuda',action='store_true',default=False,help='Disables CUDA training.') 7 | parser.add_argument('--dataset',type=str,default='RegraphPerFace_05',help='Dataset name.') 8 | parser.add_argument('--split',type=str,default='train_test',help='Split name.') 9 | parser.add_argument('--seed',type=int,default=42,help='Random seed.') 10 | parser.add_argument('--epochs',type=int,default=100,help='Number of epochs to train.') 11 | parser.add_argument('--lr',type=float,default=0.0001,help='Initial learning rate.') 12 | parser.add_argument('--weight_decay',type=float,default=5e-4,help='Weight decay (L2 loss on parameters).') 13 | parser.add_argument('--hidden',type=int,default=256,help='Number of hidden units.') 14 | parser.add_argument('--dropout',type=float,default=0.1,help='Dropout rate.') 15 | parser.add_argument('--mpn',type=str,default='gcn',choices=['gcn','mlp','gat','gin'],help='Message passing network to use, can be gcn or mlp or gat or gin [default: gcn]') 16 | parser.add_argument('--augment',type=str,help='Directory for augmentation data.') 17 | parser.add_argument('--only_augment',dest='only_augment',default=False,action='store_true',help='Train with only augmented data') 18 | parser.add_argument('--exp_name',type=str,help='Name of the experiment. Used for the checkpoint and log files.') 19 | args=parser.parse_args() 20 | 21 | if args.mpn in ['gcn','mlp']: 22 | from train_vanilla import * 23 | else: 24 | from train_torch_geometric import * 25 | args.cuda=not args.no_cuda and torch.cuda.is_available() 26 | # seed 27 | np.random.seed(args.seed) 28 | torch.manual_seed(args.seed) 29 | if args.cuda: 30 | torch.cuda.manual_seed(args.seed) 31 | # data and model 32 | graph_pairs_formatted=load_dataset(args) 33 | if args.mpn in ['gcn','mlp']: 34 | model=NodePointer(nfeat=graph_pairs_formatted[0][1].size()[1],nhid=args.hidden,dropout=args.dropout,Use_GCN=(args.mpn=='gcn')) 35 | else: 36 | model=NodePointer(nfeat=graph_pairs_formatted[0][1].size()[1],nhid=args.hidden,dropout=args.dropout,MPN_type=args.mpn) 37 | optimizer=optim.Adam(model.parameters(),lr=args.lr) 38 | scheduler=ReduceLROnPlateau(optimizer,'min') 39 | # cuda 40 | if args.cuda: 41 | model.cuda() 42 | for i in range(len(graph_pairs_formatted)): 43 | for j in range(7): 44 | graph_pairs_formatted[i][j]=graph_pairs_formatted[i][j].cuda() 45 | # train and test 46 | train_test(graph_pairs_formatted,model,optimizer,scheduler,args) 47 | -------------------------------------------------------------------------------- /tools/search/.gitignore: -------------------------------------------------------------------------------- 1 | log/ -------------------------------------------------------------------------------- /tools/search/README.md: -------------------------------------------------------------------------------- 1 | # Reconstruction with Neurally Guided Search 2 | A framework for running neurally guided search to recover a construction sequence from B-Rep input. 3 | 4 | ![Random Reconstruction](https://i.gyazo.com/702ad3f8f443c44be4ad85383f7fa719.gif) 5 | 6 | ## Publication 7 | For further details on the method, please refer to [our paper](https://arxiv.org/abs/2010.02392). 8 | ``` 9 | @article{willis2020fusion, 10 | title={Fusion 360 Gallery: A Dataset and Environment for Programmatic CAD Construction from Human Design Sequences}, 11 | author={Karl D. D. Willis and Yewen Pu and Jieliang Luo and Hang Chu and Tao Du and Joseph G. Lambourne and Armando Solar-Lezama and Wojciech Matusik}, 12 | journal={ACM Transactions on Graphics (TOG)}, 13 | volume={40}, 14 | number={4}, 15 | year={2021}, 16 | publisher={ACM New York, NY, USA} 17 | } 18 | ``` 19 | ## Training 20 | We provide pretrained checkpoints, so training is not necessary to run search. For those interested in training the network please refer to the [`regraphnet`](../regraphnet) documentation. 21 | 22 | ## Setup 23 | 1. Fusion 360: Follow the [instructions here](../#install-fusion-360) to install Fusion 360. 24 | 2. Fusion 360 Gym: Follow the [instructions here](../fusion360gym#running) to install and run the Fusion 360 Gym, our add-in that runs inside of Fusion 360. 25 | 3. Install requirements: 26 | - `pytorch` tested with 1.4.0, gpu not required 27 | - `numpy` tested with 1.18.1 28 | - `psutil` tested with 5.7.0 29 | - `requests` tested with 2.23.0 30 | 31 | 32 | ## Running 33 | The code runs as a client that interacts with the Fusion 360 Gym, which must be running inside of Fusion 360 in the background. 34 | 1. Launch by running [`main.py`](main.py), for example from the `search` directory run: 35 | ``` 36 | python main.py --input ../testdata/Couch.smt 37 | ``` 38 | This will perform random search with the a random agent by default to reconstruct the Couch.smt geometry. You will see the design inside of Fusion 360 update as the agent attempts to reconstruct the original geometry. 39 | 40 | Specifying a type of agent and a search strategy is done as follows: 41 | ``` 42 | python main.py --input ../testdata/Couch.smt --agent mpn --search best 43 | ``` 44 | 45 | ### Arguments 46 | The full list of arguments is as follows: 47 | - `--input`: File or folder of target .smt B-Rep files to reconstruct, if this is a folder all .smt files will be run 48 | - `--split` (optional): Train/test split file, as provided with the reconstruction dataset, to run only test files from the input folder 49 | - `--output`(optional): Folder to save the output logs to [default: log] 50 | - `--screenshot`(optional): Save screenshots during reconstruction [default: False] 51 | - `--launch_gym` (optional): Launch the Fusion 360 Gym automatically, requires the gym to be set to 'run on startup' within Fusion 360. Enabling this will also handle automatic restarting of Fusion if it crashes [default: False] 52 | - `--agent`(optional): Agent to use, can be rand, mpn, or mlp [default: rand] 53 | - `--search`(optional): Search to use, can be rand, beam or best [default: rand] 54 | - `--budget`(optional): The number of steps to search [default: 100] 55 | - `--synthetic_data`: Type of synthetic data to use, can be aug, semisyn, or syn. 56 | 57 | 58 | ## Results Log 59 | Results are stored by default in the `log` directory as JSON files. Each JSON file contains a list of steps in the following structure: 60 | 61 | ```js 62 | [ 63 | { 64 | "rollout_attempt": 5, 65 | "rollout_step": 0, 66 | "rollout_length": 7, 67 | "used_budget": 35, 68 | "budget": 100, 69 | "start_face": "7", 70 | "end_face": "4", 71 | "operation": "NewBodyFeatureOperation", 72 | "current_iou": null, 73 | "max_iou": 0.6121965660153939, 74 | "time": 1602089959.857739 75 | }, 76 | ... 77 | ] 78 | ``` 79 | - `rollout_attempt`: The current rollout attempt number (rand, beam search only) 80 | - `rollout_step`: The current step in the current rollout attempt (rand, beam search only) 81 | - `rollout_length`: The number of steps in a rollout attempt (rand, beam search only) 82 | - `used_budget`: The number of the steps used in the budget so far 83 | - `budget`: The total budget 84 | - `start_face`: The id of the start face, used by the action (rand search only) 85 | - `end_face`: The id of the end face, used by the action (rand search only) 86 | - `operation`: The operation used by the action (rand search only) 87 | - `prefix`: A list containing a sequence of actions with `start_face`, `end_face`, and `operation` (beam, best search only) 88 | - `current_iou`: The IoU at this step, null if an invalid action was specified 89 | - `max_iou`: The maximum IoU value seen so far 90 | - `time`: The epoch unix time stamp 91 | 92 | 93 | A log of the files that have been processed is stored at `log/search_results.json`. To rerun a given file, delete it from the list (or remove the file) or else it will be skipped during processing. 94 | 95 | 96 | -------------------------------------------------------------------------------- /tools/search/agent.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Agent: 4 | 5 | def __init__(self): 6 | self.operations = [ 7 | "JoinFeatureOperation", 8 | "CutFeatureOperation", 9 | "IntersectFeatureOperation", 10 | "NewBodyFeatureOperation", 11 | "NewComponentFeatureOperation" 12 | ] 13 | 14 | def set_target(self, target_graph, bounding_box): 15 | """Set the target graph and bounding box""" 16 | self.target_graph = target_graph 17 | self.bounding_box = bounding_box 18 | 19 | def get_actions_probabilities(self, current_graph, target_graph): 20 | """Given the current graph, and the target graph, give two lists: 21 | 1) a list of all possible actions, 22 | where each action is a triple (start_face, end_face, operation) 23 | 2) the associated probability for each action""" 24 | pass 25 | -------------------------------------------------------------------------------- /tools/search/agent_random.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | 4 | from agent import Agent 5 | 6 | 7 | class AgentRandom(Agent): 8 | 9 | def __init__(self): 10 | super().__init__() 11 | 12 | def set_target(self, target_graph, bounding_box): 13 | """Set the target graph""" 14 | super().set_target(target_graph, bounding_box) 15 | # Store a list of the faces we can choose from 16 | # These will get filtered for something sensible during search 17 | self.target_faces = [] 18 | for node in self.target_graph["nodes"]: 19 | self.target_faces.append(node["id"]) 20 | assert len(self.target_faces) >= 2 21 | 22 | def get_actions_probabilities(self, current_graph, target_graph): 23 | super().get_actions_probabilities(current_graph, target_graph) 24 | list_actions = [] 25 | list_probabilities = [] 26 | for t1 in self.target_faces: 27 | prob_t1 = 1 / len(self.target_faces) 28 | for t2 in self.target_faces: 29 | if t1 != t2: 30 | prob_t2 = 1 / (len(self.target_faces) - 1) 31 | for op in self.operations: 32 | prob_op = 1 / len(self.operations) 33 | action = { 34 | "start_face": t1, 35 | "end_face": t2, 36 | "operation": op 37 | } 38 | action_prob = prob_t1 * prob_t2 * prob_op 39 | if math.isnan(action_prob): 40 | action_prob = 0.0 41 | 42 | list_actions.append(action) 43 | list_probabilities.append(action_prob) 44 | 45 | return np.array(list_actions), np.array(list_probabilities) 46 | -------------------------------------------------------------------------------- /tools/search/agent_supervised.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import numpy as np 4 | import math 5 | from pathlib import Path 6 | import torch 7 | import torch.nn as nn 8 | import torch.nn.functional as F 9 | 10 | from agent import Agent 11 | 12 | # Add the network folder to sys.path 13 | REGRAPHNET_DIR = os.path.join(os.path.dirname(__file__), "..", "regraphnet") 14 | REGRAPHNET_SRC_DIR = os.path.join(REGRAPHNET_DIR, "src") 15 | if REGRAPHNET_SRC_DIR not in sys.path: 16 | sys.path.append(REGRAPHNET_SRC_DIR) 17 | 18 | import train_vanilla 19 | import train_torch_geometric 20 | 21 | 22 | class AgentSupervised(Agent): 23 | 24 | def __init__(self, agent, syn_data): 25 | super().__init__() 26 | if agent in ["gcn", "mlp"]: 27 | self.model = train_vanilla.NodePointer( 28 | nfeat=708, 29 | nhid=256, 30 | Use_GCN=(agent == "gcn") 31 | ) 32 | self.train_ref = train_vanilla 33 | else: 34 | self.model = train_torch_geometric.NodePointer( 35 | nfeat=708, 36 | nhid=256, 37 | MPN_type=agent 38 | ) 39 | self.train_ref = train_torch_geometric 40 | regraphnet_dir = Path(REGRAPHNET_DIR) 41 | checkpoint_name = f"model_{agent}" 42 | if syn_data is not None: 43 | checkpoint_name += f"_{syn_data}" 44 | checkpoint_file = regraphnet_dir / f"ckpt/{checkpoint_name}.ckpt" 45 | if not checkpoint_file.exists(): 46 | print(f"Error: Checkpoint {checkpoint_file.name} does not exist") 47 | exit() 48 | 49 | print("-------------------------") 50 | print(f"Using {checkpoint_file.name}") 51 | 52 | # Using CUDA is slower, so we use cpu 53 | # Specify cpu to map to 54 | self.model.load_state_dict( 55 | torch.load(checkpoint_file, map_location=torch.device("cpu")) 56 | ) 57 | 58 | def get_actions_probabilities(self, current_graph, target_graph): 59 | super().get_actions_probabilities(current_graph, target_graph) 60 | graph_pair_formatted, node_names = self.load_graph_pair( 61 | target_graph, 62 | current_graph 63 | ) 64 | actions_sorted, probs_sorted = self.inference( 65 | graph_pair_formatted, 66 | node_names 67 | ) 68 | return np.array(actions_sorted), np.array(probs_sorted) 69 | 70 | def load_graph_pair(self, data_tar, data_cur): 71 | adj_tar, features_tar = self.train_ref.format_graph_data( 72 | data_tar, 73 | self.bounding_box 74 | ) 75 | # If the current graph is empty 76 | if len(data_cur["nodes"]) == 0: 77 | adj_cur, features_cur = torch.zeros((0)), torch.zeros((0)) 78 | else: 79 | adj_cur, features_cur = self.train_ref.format_graph_data( 80 | data_cur, self.bounding_box 81 | ) 82 | graph_pair_formatted = [adj_tar, features_tar, adj_cur, features_cur] 83 | node_names = [x["id"] for x in data_tar["nodes"]] 84 | return graph_pair_formatted, node_names 85 | 86 | def inference(self, graph_pair_formatted, node_names): 87 | self.model.eval() 88 | num_nodes = graph_pair_formatted[1].size()[0] 89 | output_end_conditioned = np.zeros((num_nodes, num_nodes)) 90 | with torch.no_grad(): 91 | graph_pair_formatted.append(0) 92 | output_start, _, output_op = self.model( 93 | graph_pair_formatted, 94 | use_gpu=False 95 | ) 96 | output_start = F.softmax(output_start.view(1, -1), dim=1) 97 | output_op = F.softmax(output_op, dim=1) 98 | for i in range(num_nodes): 99 | graph_pair_formatted[4] = i 100 | _, output_end, _ = self.model( 101 | graph_pair_formatted, 102 | use_gpu=False 103 | ) 104 | output_end = F.softmax(output_end.view(1, -1), dim=1) 105 | output_end_conditioned[i, :] = output_end.data.numpy() 106 | ps = [ 107 | output_start.data.numpy()[0, :], 108 | output_end_conditioned, 109 | output_op.data.numpy()[0, :] 110 | ] 111 | # enumerate all actions 112 | actions, probs = [], [] 113 | for i in range(len(node_names)): 114 | for j in range(len(node_names)): 115 | for k in range(len(self.operations)): 116 | actions.append({ 117 | "start_face": node_names[i], 118 | "end_face": node_names[j], 119 | "operation": self.operations[k] 120 | }) 121 | probs.append(ps[0][i]*ps[1][i, j]*ps[2][k]) 122 | return actions, probs 123 | -------------------------------------------------------------------------------- /tools/search/log.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | from pathlib import Path 4 | 5 | 6 | class Log: 7 | 8 | def __init__(self, env, log_dir): 9 | self.env = env 10 | self.current_dir = Path(__file__).resolve().parent 11 | if log_dir is not None: 12 | self.log_dir = log_dir 13 | else: 14 | self.log_dir = Path(__file__).resolve().parent / "log" 15 | if not self.log_dir.exists(): 16 | self.log_dir.mkdir() 17 | self.log_data = [] 18 | 19 | def set_target(self, target_file): 20 | """Set the target file so the log can be named after it""" 21 | self.target_file = target_file 22 | self.log_data = [] 23 | # Create a log folder for this file 24 | # time_stamp = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) 25 | self.log_file_dir = self.log_dir / self.target_file.stem 26 | if not self.log_file_dir.exists(): 27 | self.log_file_dir.mkdir() 28 | self.log_file = self.log_file_dir / f"{self.target_file.stem}_log.json" 29 | 30 | def log(self, data, screenshot=False): 31 | """Log data to the log array""" 32 | if screenshot: 33 | if isinstance(data, dict): 34 | file_name = f"Screenshot_{data['used_budget']:04}.png" 35 | file = self.log_file_dir / file_name 36 | else: 37 | time_stamp = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) 38 | file = self.log_file_dir / f"Screenshot_{time_stamp}.png" 39 | result = self.env.screenshot(file) 40 | if isinstance(data, dict): 41 | data["screenshot"] = file.name 42 | if isinstance(data, dict): 43 | data["time"] = time.time() 44 | self.log_data.append(data) 45 | self.save() 46 | 47 | def save(self): 48 | """Save out a log of the search sequence""" 49 | if (self.log_data is not None and 50 | len(self.log_data) > 0 and 51 | self.log_file is not None): 52 | with open(self.log_file, "w", encoding="utf8") as f: 53 | json.dump(self.log_data, f, indent=4) 54 | -------------------------------------------------------------------------------- /tools/search/repl_env.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import os 4 | 5 | 6 | # Add the client folder to sys.path 7 | CLIENT_DIR = os.path.join(os.path.dirname(__file__), "..", "fusion360gym", "client") 8 | if CLIENT_DIR not in sys.path: 9 | sys.path.append(CLIENT_DIR) 10 | 11 | from gym_env import GymEnv 12 | 13 | 14 | class ReplEnv(GymEnv): 15 | 16 | def set_target(self, target_file): 17 | """Setup search and connect to the Fusion Gym""" 18 | # Set the target 19 | r = self.client.set_target(target_file) 20 | self.check_response("set_target", r) 21 | response_json = r.json() 22 | if "data" not in response_json or "graph" not in response_json["data"]: 23 | raise Exception("[set_target] response graph missing") 24 | return (response_json["data"]["graph"], 25 | response_json["data"]["bounding_box"]) 26 | 27 | def revert_to_target(self): 28 | """Revert to the target to start the search again""" 29 | r = self.client.revert_to_target() 30 | self.check_response("revert_to_target", r) 31 | response_json = r.json() 32 | if "data" not in response_json or "graph" not in response_json["data"]: 33 | raise Exception("[revert_to_target] response graph missing") 34 | return response_json["data"]["graph"] 35 | 36 | def get_empty_graph(self): 37 | """Get an empty graph to kick things off""" 38 | return { 39 | "directed": False, 40 | "multigraph": False, 41 | "graph": {}, 42 | "nodes": [], 43 | "links": [] 44 | } 45 | 46 | def extrude(self, start_face, end_face, operation): 47 | """Extrude wrapper around the gym client""" 48 | is_invalid = False 49 | return_graph = None 50 | return_iou = None 51 | r = self.client.add_extrude_by_target_face( 52 | start_face, end_face, operation) 53 | if r is not None and r.status_code == 200: 54 | response_json = r.json() 55 | if ("data" in response_json and 56 | "graph" in response_json["data"] and 57 | "iou" in response_json["data"]): 58 | return_graph = response_json["data"]["graph"] 59 | return_iou = response_json["data"]["iou"] 60 | return return_graph, return_iou 61 | 62 | def extrudes(self, actions, revert=False): 63 | """Extrudes wrapper around the gym client""" 64 | if len(actions) == 0: 65 | return None, None 66 | is_invalid = False 67 | return_graph = None 68 | return_iou = None 69 | r = self.client.add_extrudes_by_target_face(actions, revert) 70 | if r is not None and r.status_code == 200: 71 | response_json = r.json() 72 | if ("data" in response_json and 73 | "graph" in response_json["data"] and 74 | "iou" in response_json["data"]): 75 | return_graph = response_json["data"]["graph"] 76 | return_iou = response_json["data"]["iou"] 77 | return return_graph, return_iou 78 | 79 | def screenshot(self, file): 80 | """Save out a screenshot""" 81 | r = self.client.screenshot(file) 82 | return r is not None and r.status_code == 200 83 | -------------------------------------------------------------------------------- /tools/search/search.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | from pathlib import Path 4 | 5 | from log import Log 6 | 7 | 8 | class Search: 9 | 10 | def __init__(self, env, log_dir=None): 11 | self.env = env 12 | self.log = Log(env, log_dir) 13 | 14 | def set_target(self, target_file): 15 | """Set the target we are searching for""" 16 | assert target_file.exists() 17 | self.target_file = target_file 18 | self.log.set_target(target_file) 19 | self.target_graph, self.target_bounding_box = self.env.set_target( 20 | self.target_file 21 | ) 22 | # Make a set of the valid nodes that are planar 23 | # We use this for filtering later on 24 | nodes = self.target_graph["nodes"] 25 | self.valid_nodes = set() 26 | for node in nodes: 27 | if node["surface_type"] == "PlaneSurfaceType": 28 | self.valid_nodes.add(node["id"]) 29 | return self.target_graph, self.target_bounding_box 30 | 31 | def search(self, agent, budget, score_function=None, screenshot=False): 32 | """Given a particular agent, a search budget 33 | (measured in number of repl invocations, specifically, 34 | the number of "extrude" function calls), 35 | and a particular scoring function (iou or complete reconstruction) 36 | search for up to each repl invocation, 37 | the best score obtained from the set of explored programs in the search""" 38 | assert self.target_graph is not None 39 | 40 | def filter_bad_actions(self, current_graph, actions, action_probabilities): 41 | """Filter out some actions we clearly don't want to take""" 42 | assert self.target_graph is not None 43 | epsilon = 0.00000000001 44 | # Flag for if the current graph is empty 45 | is_current_graph_empty = len(current_graph["nodes"]) == 0 46 | # Adjust the probabilities of bad actions 47 | for index, action in enumerate(actions): 48 | # We only want faces that are planar 49 | if action["start_face"] not in self.valid_nodes: 50 | action_probabilities[index] = epsilon 51 | elif action["end_face"] not in self.valid_nodes: 52 | action_probabilities[index] = epsilon 53 | # If the current graph is empty, we want a new body operation 54 | elif is_current_graph_empty and action["operation"] != "NewBodyFeatureOperation": 55 | action_probabilities[index] = epsilon 56 | # This operation is not valid for the reconstruction task 57 | elif action["operation"] == "NewComponentFeatureOperation": 58 | action_probabilities[index] = epsilon 59 | # Hack to avoid divide by zero 60 | if action_probabilities[index] < epsilon: 61 | action_probabilities[index] = epsilon 62 | 63 | action_probabilities = action_probabilities / sum(action_probabilities) 64 | return action_probabilities 65 | -------------------------------------------------------------------------------- /tools/search/search_best.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import random 4 | import math 5 | import functools 6 | from pathlib import Path 7 | import numpy as np 8 | from queue import PriorityQueue 9 | 10 | 11 | from search import Search 12 | 13 | 14 | class SearchBest(Search): 15 | 16 | def __init__(self, env, log_dir=None): 17 | super().__init__(env, log_dir) 18 | 19 | def search(self, agent, budget, score_function=None, screenshot=False): 20 | super().search(agent, budget, score_function, screenshot) 21 | # the length of rollout is the same as the number of planar faces as a maximum 22 | rollout_length = 0 23 | for node in self.target_graph["nodes"]: 24 | if node["surface_type"] == "PlaneSurfaceType": 25 | rollout_length += 1 26 | if rollout_length < 2: 27 | # There exist some designs with no planar faces that we can't handle 28 | # We need at least 2 faces 29 | raise Exception("Not enough valid planar faces in target") 30 | elif rollout_length > 2: 31 | rollout_length = math.ceil(rollout_length / 2) 32 | 33 | used_budget = 0 34 | max_score = 0 35 | max_scores = [] 36 | 37 | # We begin each rollout an empty graph 38 | cur_graph = self.env.get_empty_graph() 39 | # like beam search, we keep track of prefixes, but instead of a beam we keep a "fringe" 40 | # we implement this with a priority queue, the queue ordered by min first max last 41 | # so we'll use _negative_ log likelihood and go after the "smallest" nll instead of the max like we do in beam 42 | # each entry in the fringe is a custom PriorityAction (neg_likelihood, (prefix)) 43 | # where prefix is a tuple that contains the actions, that are dicts 44 | # for example an element of the queue is something like : (10, (a1, a4, a10)) 45 | fringe = PriorityQueue() 46 | fringe.put(PriorityAction(0, ())) 47 | 48 | # while there is item in the fridge and we still have budget 49 | while fringe.qsize() > 0 and used_budget < budget: 50 | priority_action = fringe.get() 51 | # nll is something like 10, prefix is something like (a1, a4, a10) 52 | nll = priority_action.nll 53 | prefix = priority_action.prefix 54 | new_graph, cur_iou = self.env.extrudes(list(prefix), revert=True) 55 | if len(prefix) > 0: 56 | used_budget += 1 57 | take_screenshot = screenshot 58 | if cur_iou is not None: 59 | max_score = max(max_score, cur_iou) 60 | else: 61 | # We only want to take screenshots when something changes 62 | take_screenshot = False 63 | if new_graph is not None: 64 | cur_graph = new_graph 65 | 66 | log_data = { 67 | # "rollout_attempt": rollout_attempt, 68 | # "rollout_step": i, 69 | # "rollout_length": rollout_length, 70 | "used_budget": used_budget, 71 | "budget": budget, 72 | "current_iou": cur_iou, 73 | "max_iou": max_score, 74 | "prefix": list(prefix) 75 | } 76 | self.log.log(log_data, take_screenshot) 77 | max_scores.append(max_score) 78 | # Stop early if we find a solution 79 | if math.isclose(max_score, 1, abs_tol=0.00001): 80 | return max_scores 81 | # Stop if the rollout hits the budget 82 | if used_budget >= budget: 83 | break 84 | # If there was an invalid operation 85 | # continue without adding it to the search space 86 | if (new_graph is None or cur_iou is None) and len(prefix) > 0: 87 | continue 88 | 89 | # extend the current prefix by 1 step forward 90 | actions, action_probabilities = agent.get_actions_probabilities(cur_graph, self.target_graph) 91 | # Filter for clearly bad actions 92 | action_probabilities = self.filter_bad_actions(cur_graph, actions, action_probabilities) 93 | # Convert probability to logpr so they can be added rather than multiplied for numerical stability 94 | action_logprs = np.log(action_probabilities) 95 | # add to the candidates back to fringe 96 | for (a, a_logpr) in zip(actions, action_logprs): 97 | child_prefix = prefix + (a,) 98 | child_nll = nll - a_logpr 99 | # do not add a prefix that's longer than rollout length 100 | if len(child_prefix) < rollout_length: 101 | fringe.put(PriorityAction(child_nll, child_prefix)) 102 | 103 | print(f"[{used_budget}/{budget}] Score: {max_score}") 104 | return max_scores 105 | 106 | 107 | @functools.total_ordering 108 | class PriorityAction(): 109 | 110 | def __init__(self, nll, prefix): 111 | self.nll = nll 112 | self.prefix = prefix 113 | self.prefix_str = str(prefix) 114 | 115 | def __gt__(self, other): 116 | if self.nll == other.nll: 117 | return self.prefix_str > other.prefix_str 118 | else: 119 | return self.nll > other.nll 120 | 121 | def __eq__(self, other): 122 | return self.nll == other.nll and self.prefix_str == other.prefix_str 123 | -------------------------------------------------------------------------------- /tools/search/search_random.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import random 4 | import math 5 | from pathlib import Path 6 | import numpy as np 7 | 8 | 9 | from search import Search 10 | 11 | 12 | class SearchRandom(Search): 13 | 14 | def __init__(self, env, log_dir=None): 15 | super().__init__(env, log_dir) 16 | self.log_probs = False 17 | 18 | def search(self, agent, budget, score_function=None, screenshot=False): 19 | super().search(agent, budget, score_function, screenshot) 20 | # the length of rollout is the same as the number of planar faces as a maximum 21 | rollout_length = 0 22 | for node in self.target_graph["nodes"]: 23 | if node["surface_type"] == "PlaneSurfaceType": 24 | rollout_length += 1 25 | if rollout_length < 2: 26 | # There exist some designs with no planar faces that we can't handle 27 | # We need at least 2 faces 28 | raise Exception("Not enough valid planar faces in target") 29 | elif rollout_length > 2: 30 | rollout_length = math.ceil(rollout_length / 2) 31 | 32 | rollout_attempt = 0 33 | used_budget = 0 34 | max_score = 0 35 | max_scores = [] 36 | 37 | while used_budget < budget: 38 | # We begin each rollout an empty graph 39 | cur_graph = self.env.get_empty_graph() 40 | for i in range(rollout_length): 41 | actions, action_probabilities = agent.get_actions_probabilities(cur_graph, self.target_graph) 42 | # Filter for clearly bad actions 43 | action_probabilities = self.filter_bad_actions(cur_graph, actions, action_probabilities) 44 | action = np.random.choice(actions, 1, p=action_probabilities)[0] 45 | new_graph, cur_iou = self.env.extrude(action["start_face"], action["end_face"], action["operation"]) 46 | take_screenshot = screenshot 47 | if cur_iou is not None: 48 | max_score = max(max_score, cur_iou) 49 | else: 50 | # We only want to take screenshots when something changes 51 | take_screenshot = False 52 | if new_graph is not None: 53 | cur_graph = new_graph 54 | 55 | log_data = { 56 | "rollout_attempt": rollout_attempt, 57 | "rollout_step": i, 58 | "rollout_length": rollout_length, 59 | "used_budget": used_budget, 60 | "budget": budget, 61 | "start_face": action["start_face"], 62 | "end_face": action["end_face"], 63 | "operation": action["operation"], 64 | "current_iou": cur_iou, 65 | "max_iou": max_score 66 | } 67 | if self.log_probs: 68 | probs = np.sort(action_probabilities).tolist() 69 | log_data["probabilities"] = probs 70 | 71 | self.log.log(log_data, take_screenshot) 72 | max_scores.append(max_score) 73 | # Stop early if we find a solution 74 | if math.isclose(max_score, 1, abs_tol=0.00001): 75 | return max_scores 76 | used_budget += 1 77 | # Stop if the rollout hits the budget 78 | if used_budget >= budget: 79 | break 80 | print(f"[{used_budget}/{budget}] Score: {max_score}") 81 | # Revert to the target and remove all reconstruction 82 | self.env.revert_to_target() 83 | rollout_attempt += 1 84 | return max_scores 85 | -------------------------------------------------------------------------------- /tools/segmentation_viewer/README.md: -------------------------------------------------------------------------------- 1 | # Segmentation Viewer 2 | 3 | The easiest way to view the segmentation dataset is by visualizing the `.obj` files with the triangles colored according to the segment index values in the `.seg` files. A very small example utility to do this is provided in [segmentation_viewer.py](tools/segmentation_viewer/segmentation_viewer.py). 4 | 5 | Example Segmentation 6 | 7 | ## Setup 8 | Install requirements: 9 | - `numpy` 10 | - `meshplot` 11 | - `igl` 12 | 13 | ## Notebook 14 | An example of using the segmentation viewer is included in this [jupyter notebook](segmentation_viewer_demo.ipynb). 15 | 16 | ## Extracting html files for every example in the dataset 17 | Alternatively you may find it useful to extract an html view for each file in the dataset. The mesh and associated segmentation are then shown in threejs. To extract the html data run the segmentation viewer as follows. 18 | 19 | ``` 20 | python -m tools.segmentation_viewer.segmentation_viewer \ 21 | --meshes_folder s1.0.0/meshes \ 22 | --output_folder /path/to/save/visualization_data 23 | ``` 24 | 25 | ## Arguments 26 | 27 | - `--meshes_folder`: Path to the folder containing the obj meshes in the dataset. 28 | 29 | - `--output_folder`: The path to the folder where you want the visualization data to be saved 30 | -------------------------------------------------------------------------------- /tools/segmentation_viewer/segmentation_viewer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Segmentation Viewer 3 | 4 | This class allows you to view examples from the Fusion Gallery segmentation dataset. 5 | Additionally you can generate an html view for all the files. 6 | """ 7 | 8 | import argparse 9 | from pathlib import Path 10 | import numpy as np 11 | import igl 12 | import meshplot as mp 13 | import math 14 | 15 | class SegmentationViewer: 16 | def __init__(self, meshes_folder): 17 | self.meshes_folder = Path(meshes_folder) 18 | assert self.meshes_folder.exists(), "The meshes folder does not exist" 19 | 20 | bit8_colors = np.array([ 21 | [235, 85, 79], # ExtrudeSide 22 | [220, 198, 73], # ExtrudeEnd 23 | [113, 227, 76], # CutSide 24 | [0, 226, 124], # CutEnd 25 | [23, 213, 221], # Fillet 26 | [92, 99, 222], # Chamfer 27 | [176, 57, 223], # RevolveSide 28 | [238, 61, 178] # RevolveEnd 29 | ] 30 | ) 31 | self.color_map = bit8_colors / 255.0 32 | 33 | def obj_pathname(self, file_stem): 34 | obj_pathname = self.meshes_folder / (file_stem + ".obj") 35 | return obj_pathname 36 | 37 | def seg_pathname(self, file_stem): 38 | seg_pathname = self.meshes_folder / (file_stem + ".seg") 39 | return seg_pathname 40 | 41 | def load_mesh(self, obj_file): 42 | v, f = igl.read_triangle_mesh(str(obj_file)) 43 | return v, f 44 | 45 | def load_data(self, file_stem): 46 | obj_pathname = self.obj_pathname(file_stem) 47 | if not obj_pathname.exists(): 48 | print(f"Waring! -- The file {obj_pathname} does not exist") 49 | return None, None, None 50 | v, f = self.load_mesh(obj_pathname) 51 | 52 | seg_pathname = self.seg_pathname(file_stem) 53 | if not seg_pathname.exists(): 54 | print(f"Warning! -- The file {seg_pathname} does not exist") 55 | return None, None, None 56 | tris_to_segments = np.loadtxt(seg_pathname, dtype=np.uint64) 57 | assert f.shape[0] == tris_to_segments.size, "Expect a segment index for every facet" 58 | facet_colors = self.color_map[tris_to_segments] 59 | return v, f, facet_colors 60 | 61 | def view_segmentation(self, file_stem): 62 | v, f, facet_colors = self.load_data(file_stem) 63 | if v is None: 64 | print(f"The data for {file_stem} could not be loaded") 65 | return 66 | p = mp.plot(v, f, c=facet_colors) 67 | 68 | def save_html(self, file_stem, output_folder): 69 | v, f, facet_colors = self.load_data(file_stem) 70 | if v is None: 71 | print(f"The data for {file_stem} could not be loaded. Skipping") 72 | return 73 | output_pathname = output_folder / (file_stem + ".html") 74 | mp.website() 75 | p = mp.plot(v, f, c=facet_colors) 76 | p.save(str(output_pathname)) 77 | 78 | 79 | def create_html(meshes_folder, output_folder): 80 | viewer = SegmentationViewer(meshes_folder) 81 | obj_files = [ f for f in meshes_folder.glob("**/*.obj")] 82 | for file in obj_files: 83 | viewer.save_html(file.stem, output_folder) 84 | 85 | 86 | if __name__ == '__main__': 87 | parser = argparse.ArgumentParser() 88 | parser.add_argument("--meshes_folder", type=str, required=True, help="Path segmentation meshes folder") 89 | parser.add_argument("--output_folder", type=str, required=True, help="The folder where you would like to create images") 90 | args = parser.parse_args() 91 | 92 | meshes_folder = Path(args.meshes_folder) 93 | if not meshes_folder.exists(): 94 | print(f"The folder {meshes_folder} was not found") 95 | 96 | output_folder = Path(args.output_folder) 97 | if not output_folder.exists(): 98 | output_folder.mkdir() 99 | if not output_folder.exists(): 100 | print(f"Failed to create the output folder {output_folder}") 101 | 102 | # Now create the images for all the files 103 | create_html(meshes_folder, output_folder) 104 | 105 | print("Completed segmentation_viewer.py") 106 | -------------------------------------------------------------------------------- /tools/segmentation_viewer/segmentation_viewer_demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "c5fba256", 6 | "metadata": {}, 7 | "source": [ 8 | "# Fusion 360 Gallery Segmentation Dataset Viewer\n", 9 | "The easiest way to view segmentation data from the Fusion 360 Gallery segmentation dataset is by visualizing the mesh data and associated segmentation. This can easily be done using python based mesh visualization tools. Here we give a simple example which uses the [meshplot]( https://github.com/skoch9/meshplot) library internally." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "id": "a05de486", 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from pathlib import Path\n", 20 | "from segmentation_viewer import SegmentationViewer" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "id": "635ad0c0", 26 | "metadata": {}, 27 | "source": [ 28 | "You will want to point the segmentation viewer at the `meshes` folder in the Fusion 360 Gallery segmentation dataset." 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 2, 34 | "id": "db96d88b", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "mesh_folder = Path(\"../testdata/segmentation_examples\")\n", 39 | "# You may want to sawp this for you download of the segmentation dataset \n", 40 | "# mesh_folder = Path(\"/data_drive/SegmentationDataset/s2.0.0/meshes\")\n", 41 | "viewer = SegmentationViewer(mesh_folder)" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "id": "81abe5c1", 47 | "metadata": {}, 48 | "source": [ 49 | "You can then find the filenames of the files in the dataset. Here we search for the .`obj` files and extract the file stems. The viewer requires just the file stem as it needs to locate both the `.obj` file and the corresponding `.seg` segmentation file." 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 3, 55 | "id": "1cbe27dc", 56 | "metadata": {}, 57 | "outputs": [ 58 | { 59 | "name": "stdout", 60 | "output_type": "stream", 61 | "text": [ 62 | "Number of files in the dataset 1\n" 63 | ] 64 | } 65 | ], 66 | "source": [ 67 | "dataset_file_stems = [ f.stem for f in mesh_folder.glob(\"**/*.obj\")]\n", 68 | "print(f\"Number of files in the dataset {len(dataset_file_stems)}\")" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "id": "6054a5fc", 74 | "metadata": {}, 75 | "source": [ 76 | "You can then choose which file to view based on it's index in the `dataset_file_stems`. Alternatively if you want to view a specific file then you can simply use the filestem in the `view_segmentation()` function." 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 4, 82 | "id": "864d5004", 83 | "metadata": {}, 84 | "outputs": [ 85 | { 86 | "name": "stdout", 87 | "output_type": "stream", 88 | "text": [ 89 | "Viewing file 102673_56775b8e_5\n" 90 | ] 91 | }, 92 | { 93 | "data": { 94 | "application/vnd.jupyter.widget-view+json": { 95 | "model_id": "9f57c01976634fe09b01a48b5e4d6e32", 96 | "version_major": 2, 97 | "version_minor": 0 98 | }, 99 | "text/plain": [ 100 | "Renderer(camera=PerspectiveCamera(children=(DirectionalLight(color='white', intensity=0.6, position=(2.9802322…" 101 | ] 102 | }, 103 | "metadata": {}, 104 | "output_type": "display_data" 105 | } 106 | ], 107 | "source": [ 108 | "file_to_view = 0\n", 109 | "file_stem = dataset_file_stems[file_to_view]\n", 110 | "print(f\"Viewing file {file_stem}\")\n", 111 | "viewer.view_segmentation(file_stem)" 112 | ] 113 | } 114 | ], 115 | "metadata": { 116 | "kernelspec": { 117 | "display_name": "Python 3", 118 | "language": "python", 119 | "name": "python3" 120 | }, 121 | "language_info": { 122 | "codemirror_mode": { 123 | "name": "ipython", 124 | "version": 3 125 | }, 126 | "file_extension": ".py", 127 | "mimetype": "text/x-python", 128 | "name": "python", 129 | "nbconvert_exporter": "python", 130 | "pygments_lexer": "ipython3", 131 | "version": "3.7.10" 132 | } 133 | }, 134 | "nbformat": 4, 135 | "nbformat_minor": 5 136 | } 137 | -------------------------------------------------------------------------------- /tools/sketch2image/README.md: -------------------------------------------------------------------------------- 1 | # Sketch2image 2 | This python utility code creates images from the json sketch data provided with the [Reconstruction Dataset](../../docs/reconstruction.md). 3 | 4 | Sketch2image 5 | 6 | ## [sketch_plotter.py](sketch_plotter.py) 7 | The SketchPlotter class is a reusable utility for plotting sketch data using matplotlib. 8 | ``` 9 | SketchPlotter(sketch, title=None, opts=None) 10 | ``` 11 | - `title`: A title for the image 12 | - `opts`: 13 | - `opts.draw_annotation`: Draw annotation like the sketch points 14 | - `opts.draw_grid`: Draw a background grid 15 | - `opts.linewidth`: Linewidth for the sketch curves 16 | 17 | ## [sketch2image.py](sketch2image.py) 18 | A utility to create sketch images for every reconstruction json file in a folder 19 | ``` 20 | python sketch2image.py --input_folder /path/to/json_files/ --output_folder /path/to/put/images 21 | ``` -------------------------------------------------------------------------------- /tools/sketch2image/sketch2image.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | import argparse 5 | from sketch_plotter import SketchPlotter 6 | 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument("--input_folder", type=str, help="The input directory containing the json files") 9 | parser.add_argument("--output_folder", type=str, help="The output folder for the images") 10 | parser.add_argument("--linewidth", type=int, default=1,help="The linewidth to draw the geometry") 11 | parser.add_argument("--show_title", type=int, default=1, help="Add a title to the image") 12 | parser.add_argument("--draw_annotation", type=int, default=0, help="Draw additional annotation") 13 | parser.add_argument("--draw_grid", type=int, default=0, help="Draw a grid with the image") 14 | args = parser.parse_args() 15 | 16 | if args.input_folder is None: 17 | print("Please specify input folder with the --input_folder argument") 18 | exit() 19 | 20 | if args.output_folder is None: 21 | print("Please specify output folder with the --output_folder argument") 22 | exit() 23 | 24 | 25 | def read_json(pathname): 26 | """Read json from a file""" 27 | with open(pathname) as data_file: 28 | try: 29 | json_data = json.load(data_file) 30 | except: 31 | print(f"Error reading file {pathname}") 32 | return None 33 | return json_data 34 | 35 | def check_valid_sketch(sketch_data): 36 | if sketch_data is None: 37 | return False 38 | if not "points" in sketch_data: 39 | return False 40 | if not "curves" in sketch_data: 41 | return False 42 | return True 43 | 44 | def get_short_name(filename): 45 | name = filename.stem 46 | # We expect something like 47 | # ReconstructionExtractor_Z0HexagonCutJoin_3797e54d_Untitled 48 | names = name.split("_") 49 | if len(names) == 4: 50 | name = names[1] + " " + names[2] 51 | return name 52 | 53 | def image_pathname(file, sketch_name, output_path): 54 | filename = file.stem + "_"+sketch_name 55 | return (output_path / filename).with_suffix(".png") 56 | 57 | def image_exists(file, sketch_name, output_path): 58 | return image_pathname(file, sketch_name, output_path).exists() 59 | 60 | def create_sketch_image(sketch, file, output_path, opts): 61 | if check_valid_sketch(sketch): 62 | sketch_name = sketch["name"] 63 | if image_exists(file, sketch_name, output_path): 64 | print(f"Image for {file} already exists. Skiping") 65 | return 66 | if opts.show_title: 67 | title = get_short_name(file) + " " + sketch_name 68 | else: 69 | title = None 70 | sp = SketchPlotter(sketch, title, opts) 71 | sp.create_drawing() 72 | save_path = image_pathname(file, sketch_name, output_path) 73 | sp.save_image(save_path) 74 | sp.close_figure() 75 | 76 | def create_sketch_images(json_pathname, output_path, opts): 77 | data = read_json(json_pathname) 78 | if not "entities" in data: 79 | return 80 | for entity in data["entities"].values(): 81 | if not "type" in entity: 82 | continue 83 | if entity["type"] == "Sketch": 84 | create_sketch_image(entity, json_pathname, output_path, opts) 85 | 86 | 87 | input_path = Path(args.input_folder) 88 | output_path = Path(args.output_folder) 89 | if not output_path.exists(): 90 | output_path.mkdir() 91 | 92 | files = [f for f in input_path.glob("**/*.json")] 93 | for file in files: 94 | try: 95 | create_sketch_images(file, output_path, args) 96 | except Exception as ex: 97 | print(f"Exception processing sketch {file}.") 98 | print(f"{str(ex)}") 99 | 100 | print("") 101 | print("") 102 | print("Completed sketch2image.py") 103 | -------------------------------------------------------------------------------- /tools/testdata/.gitignore: -------------------------------------------------------------------------------- 1 | output/ -------------------------------------------------------------------------------- /tools/testdata/Box.smt: -------------------------------------------------------------------------------- 1 | 22600 0 2 4 2 | 30 Autodesk Translation Framework 21 ASM 226.3.0.65535 OSX 24 Wed Aug 26 15:45:59 2020 3 | 10 9.999999999999999547e-07 1.000000000000000036e-10 4 | asmheader $-1 -1 @13 226.3.0.65535 # 5 | body $2 -1 $-1 $3 $-1 $4 # 6 | rgb_color-st-attrib $-1 -1 $5 $-1 $1 0.627450980392156854 0.627450980392156854 0.627450980392156854 1 # 7 | lump $-1 -1 $-1 $-1 $6 $1 # 8 | transform $-1 -1 1 0 0 0 1 0 0 0 1 0 0 0 1 no_rotate no_reflect no_shear # 9 | ATTRIB_CUSTOM-attrib $-1 -1 $-1 $2 $1 @20 Timestamp_attrib_def 1 1598475643613824 # 10 | shell $-1 -1 $-1 $-1 $-1 $7 $-1 $3 # 11 | face $8 -1 $-1 $9 $10 $6 $-1 $11 forward single # 12 | DXID-attrib $-1 -1 $-1 $-1 $7 @10 ASM ENTITY @1 0 # 13 | face $12 -1 $-1 $13 $14 $6 $-1 $15 forward single # 14 | loop $-1 -1 $-1 $-1 $16 $7 # 15 | plane-surface $-1 -1 $-1 0 0 0 1 0 -0 -0 1 -0 forward_v I I I I # 16 | DXID-attrib $-1 -1 $-1 $-1 $9 @10 ASM ENTITY @1 1 # 17 | face $17 -1 $-1 $18 $19 $6 $-1 $20 forward single # 18 | loop $-1 -1 $-1 $-1 $21 $9 # 19 | plane-surface $-1 -1 $-1 -1.000000014901161194 0 0 0 -1 0 1 -0 -0 forward_v I I I I # 20 | coedge $22 -1 $-1 $23 $24 $25 $26 reversed $10 0 $-1 # 21 | DXID-attrib $-1 -1 $-1 $-1 $13 @10 ASM ENTITY @1 2 # 22 | face $27 -1 $-1 $28 $29 $6 $-1 $30 forward single # 23 | loop $-1 -1 $-1 $-1 $31 $13 # 24 | plane-surface $-1 -1 $-1 -1.000000014901161194 1.000000014901161194 0 -1 0 0 -0 -1 -0 forward_v I I I I # 25 | coedge $32 -1 $-1 $33 $34 $35 $36 reversed $14 0 $-1 # 26 | ATTRIB_CUSTOM-attrib $-1 -1 $-1 $-1 $16 @17 sketch_attrib_def 1 1 3 @13 108 0 1 0 1 1 # 27 | coedge $-1 -1 $-1 $37 $16 $38 $39 forward $10 0 $-1 # 28 | coedge $-1 -1 $-1 $16 $37 $33 $40 reversed $10 0 $-1 # 29 | coedge $41 -1 $-1 $35 $42 $16 $26 forward $43 0 $-1 # 30 | edge $-1 -1 $-1 $44 0 $45 1.000000014901161194 $16 $46 forward @7 unknown # 31 | DXID-attrib $-1 -1 $-1 $-1 $18 @10 ASM ENTITY @1 3 # 32 | face $47 -1 $-1 $48 $49 $6 $-1 $50 forward single # 33 | loop $-1 -1 $-1 $-1 $51 $18 # 34 | plane-surface $-1 -1 $-1 0 1.000000014901161194 0 0 1 0 -1 -0 -0 forward_v I I I I # 35 | coedge $52 -1 $-1 $53 $54 $55 $56 reversed $19 0 $-1 # 36 | ATTRIB_CUSTOM-attrib $-1 -1 $-1 $-1 $21 @17 sketch_attrib_def 1 1 3 @13 105 0 1 0 1 1 # 37 | coedge $-1 -1 $-1 $57 $21 $24 $40 forward $14 0 $-1 # 38 | coedge $-1 -1 $-1 $21 $57 $53 $58 reversed $14 0 $-1 # 39 | coedge $59 -1 $-1 $55 $25 $21 $36 forward $43 0 $-1 # 40 | edge $-1 -1 $-1 $45 0 $60 1.000000014901161194 $21 $61 forward @7 unknown # 41 | coedge $-1 -1 $-1 $24 $23 $62 $63 reversed $10 0 $-1 # 42 | coedge $-1 -1 $-1 $51 $64 $23 $39 reversed $29 0 $-1 # 43 | edge $-1 -1 $-1 $44 0 $65 1 $38 $66 forward @7 unknown # 44 | edge $-1 -1 $-1 $45 0 $67 1 $33 $68 forward @7 unknown # 45 | ATTRIB_CUSTOM-attrib $-1 -1 $-1 $-1 $25 @17 sketch_attrib_def 1 1 3 @13 108 0 1 0 1 1 # 46 | coedge $69 -1 $-1 $25 $55 $51 $70 forward $43 0 $-1 # 47 | loop $-1 -1 $-1 $-1 $42 $48 # 48 | vertex $-1 -1 $-1 $70 1 $71 # 49 | vertex $-1 -1 $-1 $36 0 $72 # 50 | straight-curve $-1 -1 $-1 0 1.000000014901161194 0 0 -1 0 F 0 F 1.000000014901161194 # 51 | DXID-attrib $-1 -1 $-1 $-1 $28 @10 ASM ENTITY @1 4 # 52 | face $73 -1 $-1 $-1 $43 $6 $-1 $74 reversed single # 53 | loop $-1 -1 $-1 $-1 $75 $28 # 54 | plane-surface $-1 -1 $-1 -0.5000000074505805969 0.5000000074505805969 1 0 0 1 1 0 0 forward_v I I I I # 55 | coedge $76 -1 $-1 $77 $38 $42 $70 reversed $29 0 $-1 # 56 | ATTRIB_CUSTOM-attrib $-1 -1 $-1 $-1 $31 @17 sketch_attrib_def 1 1 3 @13 106 0 1 0 1 1 # 57 | coedge $-1 -1 $-1 $78 $31 $34 $58 forward $19 0 $-1 # 58 | coedge $-1 -1 $-1 $31 $78 $77 $79 reversed $19 0 $-1 # 59 | coedge $80 -1 $-1 $42 $35 $31 $56 forward $43 0 $-1 # 60 | edge $-1 -1 $-1 $60 0 $81 1.000000014901161194 $31 $82 forward @7 unknown # 61 | coedge $-1 -1 $-1 $34 $33 $83 $84 reversed $14 0 $-1 # 62 | edge $-1 -1 $-1 $60 0 $85 1 $53 $86 forward @7 unknown # 63 | ATTRIB_CUSTOM-attrib $-1 -1 $-1 $-1 $35 @17 sketch_attrib_def 1 1 3 @13 105 0 1 0 1 1 # 64 | vertex $-1 -1 $-1 $56 0 $87 # 65 | straight-curve $-1 -1 $-1 0 0 0 -1 0 0 F 0 F 1.000000014901161194 # 66 | coedge $-1 -1 $-1 $75 $83 $37 $63 forward $49 0 $-1 # 67 | edge $-1 -1 $-1 $67 -1.000000014901161194 $65 -0 $37 $88 forward @7 unknown # 68 | coedge $-1 -1 $-1 $38 $77 $75 $89 reversed $29 0 $-1 # 69 | vertex $-1 -1 $-1 $39 1 $90 # 70 | straight-curve $-1 -1 $-1 0 1.000000014901161194 0 0 0 1 I I # 71 | vertex $-1 -1 $-1 $40 1 $91 # 72 | straight-curve $-1 -1 $-1 0 0 0 0 0 1 I I # 73 | ATTRIB_CUSTOM-attrib $-1 -1 $-1 $-1 $42 @17 sketch_attrib_def 1 1 3 @13 107 0 1 0 1 1 # 74 | edge $-1 -1 $-1 $81 0 $44 1.000000014901161194 $51 $92 forward @7 unknown # 75 | point $-1 -1 $-1 0 1.000000014901161194 0 # 76 | point $-1 -1 $-1 0 0 0 # 77 | DXID-attrib $-1 -1 $-1 $-1 $48 @10 ASM ENTITY @1 5 # 78 | plane-surface $-1 -1 $-1 -0.5000000074505805969 0.5000000074505805969 0 0 -0 1 1 0 -0 forward_v I I I I # 79 | coedge $-1 -1 $-1 $93 $62 $64 $89 forward $49 0 $-1 # 80 | ATTRIB_CUSTOM-attrib $-1 -1 $-1 $-1 $51 @17 sketch_attrib_def 1 1 3 @13 107 0 1 0 1 1 # 81 | coedge $-1 -1 $-1 $64 $51 $54 $79 forward $29 0 $-1 # 82 | coedge $-1 -1 $-1 $54 $53 $93 $94 reversed $19 0 $-1 # 83 | edge $-1 -1 $-1 $81 0 $95 1 $77 $96 forward @7 unknown # 84 | ATTRIB_CUSTOM-attrib $-1 -1 $-1 $-1 $55 @17 sketch_attrib_def 1 1 3 @13 106 0 1 0 1 1 # 85 | vertex $-1 -1 $-1 $70 0 $97 # 86 | straight-curve $-1 -1 $-1 -1.000000014901161194 0 0 0 1 0 F 0 F 1.000000014901161194 # 87 | coedge $-1 -1 $-1 $62 $93 $57 $84 forward $49 0 $-1 # 88 | edge $-1 -1 $-1 $85 -1.000000014901161194 $67 -0 $57 $98 forward @7 unknown # 89 | vertex $-1 -1 $-1 $58 1 $99 # 90 | straight-curve $-1 -1 $-1 -1.000000014901161194 0 0 0 0 1 I I # 91 | point $-1 -1 $-1 -1.000000014901161194 0 0 # 92 | straight-curve $-1 -1 $-1 0 1.000000014901161194 1 0 1 0 I I # 93 | edge $-1 -1 $-1 $65 -1.000000014901161194 $95 -0 $64 $100 forward @7 unknown # 94 | point $-1 -1 $-1 0 1.000000014901161194 1 # 95 | point $-1 -1 $-1 0 0 1 # 96 | straight-curve $-1 -1 $-1 -1.000000014901161194 1.000000014901161194 0 1 0 0 F 0 F 1.000000014901161194 # 97 | coedge $-1 -1 $-1 $83 $75 $78 $94 forward $49 0 $-1 # 98 | edge $-1 -1 $-1 $95 -1.000000014901161194 $85 -0 $78 $101 forward @7 unknown # 99 | vertex $-1 -1 $-1 $79 1 $102 # 100 | straight-curve $-1 -1 $-1 -1.000000014901161194 1.000000014901161194 0 0 0 1 I I # 101 | point $-1 -1 $-1 -1.000000014901161194 1.000000014901161194 0 # 102 | straight-curve $-1 -1 $-1 0 0 1 1 0 0 I I # 103 | point $-1 -1 $-1 -1.000000014901161194 0 1 # 104 | straight-curve $-1 -1 $-1 -1.000000014901161194 1.000000014901161194 1 -1 -0 -0 I I # 105 | straight-curve $-1 -1 $-1 -1.000000014901161194 0 1 -0 -1 -0 I I # 106 | point $-1 -1 $-1 -1.000000014901161194 1.000000014901161194 1 # 107 | End-of-ASM-data -------------------------------------------------------------------------------- /tools/testdata/common/BooleanAdjacent.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanAdjacent.f3d -------------------------------------------------------------------------------- /tools/testdata/common/BooleanExactOverlap.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanExactOverlap.f3d -------------------------------------------------------------------------------- /tools/testdata/common/BooleanIntersectContained.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanIntersectContained.f3d -------------------------------------------------------------------------------- /tools/testdata/common/BooleanIntersectDouble.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanIntersectDouble.f3d -------------------------------------------------------------------------------- /tools/testdata/common/BooleanIntersectMultiOverlap.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanIntersectMultiOverlap.f3d -------------------------------------------------------------------------------- /tools/testdata/common/BooleanIntersectMultiTool.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanIntersectMultiTool.f3d -------------------------------------------------------------------------------- /tools/testdata/common/BooleanIntersectOverlap.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanIntersectOverlap.f3d -------------------------------------------------------------------------------- /tools/testdata/common/BooleanIntersectOverlapSelfIntersect.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanIntersectOverlapSelfIntersect.f3d -------------------------------------------------------------------------------- /tools/testdata/common/BooleanIntersectSeparate.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanIntersectSeparate.f3d -------------------------------------------------------------------------------- /tools/testdata/common/BooleanOverlap.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanOverlap.f3d -------------------------------------------------------------------------------- /tools/testdata/common/BooleanOverlap3Way.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanOverlap3Way.f3d -------------------------------------------------------------------------------- /tools/testdata/common/BooleanOverlapDouble.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanOverlapDouble.f3d -------------------------------------------------------------------------------- /tools/testdata/common/BooleanSeparate.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Fusion360GalleryDataset/1084b881f3bb710267801d812d6e9286b8667059/tools/testdata/common/BooleanSeparate.f3d --------------------------------------------------------------------------------