├── .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 | 
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 | 
13 |
14 |
15 | ### [Reconstruction Dataset](docs/reconstruction.md)
16 | Sequential construction sequence information from a subset of simple 'sketch and extrude' designs.
17 |
18 | 
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 | 
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 | 
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 | 
15 |
16 | ### [Reconstruction Dataset](reconstruction.md)
17 | The Reconstruction Dataset contains construction sequence information from a subset of simple 'sketch and extrude' designs.
18 | 
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 | 
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 | 
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 | 
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 | 
16 |
17 |
18 | The most frequent construction sequence combinations are shown below. S indicates a _sketch_ and E indicates an _extrude_ operation.
19 |
20 | 
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 | 
28 |
29 | The graph below illustrates the distribution of curve count per design, as another measure of design complexity.
30 |
31 | 
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 | 
41 |
42 | ### Dimensions & Constraints
43 | Shown below are the distribution of dimension and constraint types in the dataset.
44 |
45 | 
46 |
47 | 
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 | 
54 |
55 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
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 | 
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 | 
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 |
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 |
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
--------------------------------------------------------------------------------