symcad.core.GraphAPI
The GraphAPI module provides methods and functionality for interacting with the graph-based
data representation of a symcad.core.Assembly in the SymCAD library.
All methods within this module are for internal use only and should not be directly accessed
by any external module. Interacting with graph-based Assembly representations should be
done via the symcad.core.Assembly.Assembly.save() and symcad.core.Assembly.Assembly.load()
methods.
The SymCAD Graph Schema for an Assembly is JSON-based, as follows:
{
"name": str,
"parts": [
{
"name": str,
"type": str,
"geometry": {
"property1": str | float,
},
"material_density": float,
"static_origin": null | {
"x": str | float,
"y": str | float,
"z": str | float
},
"static_placement": null | {
"x": str | float,
"y": str | float,
"z": str | float
},
"attachment_points": [
{
"name": str,
"x": float,
"y": float,
"z": float
}
],
"connection_ports": [
{
"name": str,
"x": float,
"y": float,
"z": float
}
],
"orientation": {
"roll": str | float,
"pitch": str | float,
"yaw": str | float
},
"is_exposed": true | false
}
],
"attachments": [
{
"source_node": str,
"source_attachment": str,
"destination_node": str,
"destination_attachment": str
}
],
"connections": [
{
"source_node": str,
"source_connection": str,
"destination_node": str,
"destination_connection": str
}
]
}
1#!/usr/bin/env python3 2# Copyright (C) 2022, Will Hedgecock 3# 4# This program is free software: you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation, either version 3 of the License, or 7# (at your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17""" 18The GraphAPI module provides methods and functionality for interacting with the graph-based 19data representation of a `symcad.core.Assembly` in the SymCAD library. 20 21All methods within this module are for internal use only and should not be directly accessed 22by any external module. Interacting with graph-based `Assembly` representations should be 23done via the `symcad.core.Assembly.Assembly.save()` and `symcad.core.Assembly.Assembly.load()` 24methods. 25 26The SymCAD Graph Schema for an `Assembly` is JSON-based, as follows: 27 28```python 29{ 30 "name": str, 31 "parts": [ 32 { 33 "name": str, 34 "type": str, 35 "geometry": { 36 "property1": str | float, 37 }, 38 "material_density": float, 39 "static_origin": null | { 40 "x": str | float, 41 "y": str | float, 42 "z": str | float 43 }, 44 "static_placement": null | { 45 "x": str | float, 46 "y": str | float, 47 "z": str | float 48 }, 49 "attachment_points": [ 50 { 51 "name": str, 52 "x": float, 53 "y": float, 54 "z": float 55 } 56 ], 57 "connection_ports": [ 58 { 59 "name": str, 60 "x": float, 61 "y": float, 62 "z": float 63 } 64 ], 65 "orientation": { 66 "roll": str | float, 67 "pitch": str | float, 68 "yaw": str | float 69 }, 70 "is_exposed": true | false 71 } 72 ], 73 "attachments": [ 74 { 75 "source_node": str, 76 "source_attachment": str, 77 "destination_node": str, 78 "destination_attachment": str 79 } 80 ], 81 "connections": [ 82 { 83 "source_node": str, 84 "source_connection": str, 85 "destination_node": str, 86 "destination_connection": str 87 } 88 ] 89} 90``` 91""" 92 93from __future__ import annotations 94from .Coordinate import Coordinate 95from .Geometry import Geometry 96from .Assembly import Assembly 97from typing import Any 98import json, math, sympy 99 100 101def _isfloat(num: Any) -> bool: 102 """Private helper function to test if a value is float-convertible.""" 103 try: 104 float(num) 105 return True 106 except TypeError: 107 return False 108 109 110def export_to_json(assembly: Assembly) -> str: 111 """Returns a string-based JSON representation of the specified `Assembly`.""" 112 113 # Iterate through all SymCAD parts in the assembly 114 json_dict = { 'name': assembly.name, 'parts': [], 'attachments': [], 'connections': [] } 115 for part in assembly.parts: 116 117 # Create static origin and placement structures 118 if part.static_origin is not None: 119 static_origin = { 120 'x': str(part.static_origin.x) 121 if not _isfloat(part.static_origin.x) else 122 float(part.static_origin.x), 123 'y': str(part.static_origin.y) 124 if not _isfloat(part.static_origin.y) else 125 float(part.static_origin.y), 126 'z': str(part.static_origin.z) 127 if not _isfloat(part.static_origin.z) else 128 float(part.static_origin.z) 129 } 130 else: 131 static_origin = None 132 if part.static_placement is not None: 133 static_placement = { 134 'x': str(part.static_placement.x) 135 if not _isfloat(part.static_placement.x) else 136 float(part.static_placement.x), 137 'y': str(part.static_placement.y) 138 if not _isfloat(part.static_placement.y) else 139 float(part.static_placement.y), 140 'z': str(part.static_placement.z) 141 if not _isfloat(part.static_placement.z) else 142 float(part.static_placement.z) 143 } 144 else: 145 static_placement = None 146 147 # Create a JSON structure for the current part 148 component = { 149 'name': part.name, 150 'type': '.'.join(str(part.__class__).split('\'')[1].split('.')[2:-1]), 151 'geometry': {k: str(part.geometry.__dict__[k]) 152 if not _isfloat(part.geometry.__dict__[k]) else 153 part.geometry.__dict__[k] 154 for k in set(list(part.geometry.__dict__.keys())) - {'name'}}, 155 'material_density': part.material_density, 156 'static_origin': static_origin, 157 'static_placement': static_placement, 158 'attachment_points': [pt.__dict__ for pt in part.attachment_points], 159 'connection_ports': [pt.__dict__ for pt in part.connection_ports], 160 'orientation': { 161 'roll': str(part.orientation.roll) 162 if not _isfloat(part.orientation.roll) else 163 math.degrees(part.orientation.roll), 164 'pitch': str(part.orientation.pitch) 165 if not _isfloat(part.orientation.pitch) else 166 math.degrees(part.orientation.pitch), 167 'yaw': str(part.orientation.yaw) 168 if not _isfloat(part.orientation.yaw) else 169 math.degrees(part.orientation.yaw) 170 }, 171 'is_exposed': part.is_exposed 172 } 173 174 # Create an attachment structure for each attachment in the current part 175 attachments = [] 176 for local_attachment, remote_attachment in part.attachments.items(): 177 attachments.append({ 178 'source_node': part.name, 179 'source_attachment': local_attachment, 180 'destination_node': remote_attachment.split('#')[0], 181 'destination_attachment': remote_attachment.split('#')[1] 182 }) 183 184 # Create a connection structure for each connection in the current part 185 connections = [] 186 for local_connection, remote_connection in part.connections.items(): 187 connections.append({ 188 'source_node': part.name, 189 'source_connection': local_connection, 190 'destination_node': remote_connection.split('#')[0], 191 'destination_connection': remote_connection.split('#')[1] 192 }) 193 194 # Append the part component and its attachments to the JSON structure 195 json_dict['parts'].append(component) 196 for new_attach in attachments: 197 already_exists = False 198 for attach2 in json_dict['attachments']: 199 if new_attach['source_node'] == attach2['destination_node'] and \ 200 new_attach['source_attachment'] == attach2['destination_attachment'] and \ 201 new_attach['destination_node'] == attach2['source_node'] and \ 202 new_attach['destination_attachment'] == attach2['source_attachment']: 203 already_exists = True 204 if not already_exists: 205 json_dict['attachments'].append(new_attach) 206 207 # Append the part connections to the JSON structure 208 for new_connect in connections: 209 already_exists = False 210 for connect2 in json_dict['connections']: 211 if new_connect['source_node'] == connect2['destination_node'] and \ 212 new_connect['source_connection'] == connect2['destination_connection'] and \ 213 new_connect['destination_node'] == connect2['source_node'] and \ 214 new_connect['destination_connection'] == connect2['source_connection']: 215 already_exists = True 216 if not already_exists: 217 json_dict['connections'].append(new_connect) 218 219 # Return a string representation of the complete JSON structure 220 return json.dumps(json_dict, indent=3) 221 222 223def import_from_json(json_str: str) -> Assembly: 224 """Returns a new `Assembly` parsed from its JSON string representation in `json_str`.""" 225 226 # Parse the JSON string as an actual JSON dictionary 227 json_dict = json.loads(json_str) 228 assembly = Assembly(json_dict['name']) 229 230 # Iterate through all parts in the JSON structure 231 for part in json_dict['parts']: 232 233 # Convert the JSON part into its SymCAD representation 234 part_class = getattr(__import__('symcad'), 'parts') 235 for comp in part['type'].split('.'): 236 part_class = getattr(part_class, comp) 237 shape = part_class(part['name']) 238 geometry = Geometry(part['name']) 239 for property, value in part['geometry'].items(): 240 setattr(geometry, property, sympy.Symbol(value) if isinstance(value, str) else value) 241 setattr(shape, 'geometry', geometry) 242 setattr(shape, 'material_density', part['material_density']) 243 if part['static_origin'] is not None: 244 setattr(shape, 'static_origin', 245 Coordinate(part['name'] + '_origin', 246 x=sympy.Symbol(part['static_origin']['x']) 247 if isinstance(part['static_origin']['x'], str) else 248 part['static_origin']['x'], 249 y=sympy.Symbol(part['static_origin']['y']) 250 if isinstance(part['static_origin']['y'], str) else 251 part['static_origin']['y'], 252 z=sympy.Symbol(part['static_origin']['z']) 253 if isinstance(part['static_origin']['z'], str) else 254 part['static_origin']['z'])) 255 if part['static_placement'] is not None: 256 setattr(shape, 'static_placement', 257 Coordinate(part['name'] + '_placement', 258 x=sympy.Symbol(part['static_placement']['x']) 259 if isinstance(part['static_placement']['x'], str) else 260 part['static_placement']['x'], 261 y=sympy.Symbol(part['static_placement']['y']) 262 if isinstance(part['static_placement']['y'], str) else 263 part['static_placement']['y'], 264 z=sympy.Symbol(part['static_placement']['z']) 265 if isinstance(part['static_placement']['z'], str) else 266 part['static_placement']['z'])) 267 attachment_points = [] 268 for attachment_point in part['attachment_points']: 269 attachment_points.append(Coordinate(attachment_point['name'], 270 x=attachment_point['x'], 271 y=attachment_point['y'], 272 z=attachment_point['z'])) 273 connection_ports = [] 274 for connection_port in part['connection_ports']: 275 connection_ports.append(Coordinate(connection_port['name'], 276 x=connection_port['x'], 277 y=connection_port['y'], 278 z=connection_port['z'])) 279 setattr(shape, 'attachment_points', attachment_points) 280 setattr(shape, 'connection_ports', connection_ports) 281 setattr(getattr(shape, 'orientation'), 'roll', 282 sympy.Symbol(part['orientation']['roll']) 283 if isinstance(part['orientation']['roll'], str) else 284 math.radians(part['orientation']['roll'])) 285 setattr(getattr(shape, 'orientation'), 'pitch', 286 sympy.Symbol(part['orientation']['pitch']) 287 if isinstance(part['orientation']['pitch'], str) else 288 math.radians(part['orientation']['pitch'])) 289 setattr(getattr(shape, 'orientation'), 'yaw', 290 sympy.Symbol(part['orientation']['yaw']) 291 if isinstance(part['orientation']['yaw'], str) else 292 math.radians(part['orientation']['yaw'])) 293 setattr(shape, 'is_exposed', part['is_exposed']) 294 assembly.add_part(shape) 295 296 # Make all necessary part attachments 297 for attachment in json_dict['attachments']: 298 for part in assembly.parts: 299 if part.name == attachment['source_node']: 300 source = part 301 elif part.name == attachment['destination_node']: 302 dest = part 303 source.attach(attachment['source_attachment'], dest, attachment['destination_attachment']) 304 305 # Make all necessary part connections 306 for connection in json_dict['connections']: 307 for part in assembly.parts: 308 if part.name == connection['source_node']: 309 source = part 310 elif part.name == connection['destination_node']: 311 dest = part 312 source.connect(connection['source_connection'], dest, connection['destination_connection']) 313 314 return assembly
111def export_to_json(assembly: Assembly) -> str: 112 """Returns a string-based JSON representation of the specified `Assembly`.""" 113 114 # Iterate through all SymCAD parts in the assembly 115 json_dict = { 'name': assembly.name, 'parts': [], 'attachments': [], 'connections': [] } 116 for part in assembly.parts: 117 118 # Create static origin and placement structures 119 if part.static_origin is not None: 120 static_origin = { 121 'x': str(part.static_origin.x) 122 if not _isfloat(part.static_origin.x) else 123 float(part.static_origin.x), 124 'y': str(part.static_origin.y) 125 if not _isfloat(part.static_origin.y) else 126 float(part.static_origin.y), 127 'z': str(part.static_origin.z) 128 if not _isfloat(part.static_origin.z) else 129 float(part.static_origin.z) 130 } 131 else: 132 static_origin = None 133 if part.static_placement is not None: 134 static_placement = { 135 'x': str(part.static_placement.x) 136 if not _isfloat(part.static_placement.x) else 137 float(part.static_placement.x), 138 'y': str(part.static_placement.y) 139 if not _isfloat(part.static_placement.y) else 140 float(part.static_placement.y), 141 'z': str(part.static_placement.z) 142 if not _isfloat(part.static_placement.z) else 143 float(part.static_placement.z) 144 } 145 else: 146 static_placement = None 147 148 # Create a JSON structure for the current part 149 component = { 150 'name': part.name, 151 'type': '.'.join(str(part.__class__).split('\'')[1].split('.')[2:-1]), 152 'geometry': {k: str(part.geometry.__dict__[k]) 153 if not _isfloat(part.geometry.__dict__[k]) else 154 part.geometry.__dict__[k] 155 for k in set(list(part.geometry.__dict__.keys())) - {'name'}}, 156 'material_density': part.material_density, 157 'static_origin': static_origin, 158 'static_placement': static_placement, 159 'attachment_points': [pt.__dict__ for pt in part.attachment_points], 160 'connection_ports': [pt.__dict__ for pt in part.connection_ports], 161 'orientation': { 162 'roll': str(part.orientation.roll) 163 if not _isfloat(part.orientation.roll) else 164 math.degrees(part.orientation.roll), 165 'pitch': str(part.orientation.pitch) 166 if not _isfloat(part.orientation.pitch) else 167 math.degrees(part.orientation.pitch), 168 'yaw': str(part.orientation.yaw) 169 if not _isfloat(part.orientation.yaw) else 170 math.degrees(part.orientation.yaw) 171 }, 172 'is_exposed': part.is_exposed 173 } 174 175 # Create an attachment structure for each attachment in the current part 176 attachments = [] 177 for local_attachment, remote_attachment in part.attachments.items(): 178 attachments.append({ 179 'source_node': part.name, 180 'source_attachment': local_attachment, 181 'destination_node': remote_attachment.split('#')[0], 182 'destination_attachment': remote_attachment.split('#')[1] 183 }) 184 185 # Create a connection structure for each connection in the current part 186 connections = [] 187 for local_connection, remote_connection in part.connections.items(): 188 connections.append({ 189 'source_node': part.name, 190 'source_connection': local_connection, 191 'destination_node': remote_connection.split('#')[0], 192 'destination_connection': remote_connection.split('#')[1] 193 }) 194 195 # Append the part component and its attachments to the JSON structure 196 json_dict['parts'].append(component) 197 for new_attach in attachments: 198 already_exists = False 199 for attach2 in json_dict['attachments']: 200 if new_attach['source_node'] == attach2['destination_node'] and \ 201 new_attach['source_attachment'] == attach2['destination_attachment'] and \ 202 new_attach['destination_node'] == attach2['source_node'] and \ 203 new_attach['destination_attachment'] == attach2['source_attachment']: 204 already_exists = True 205 if not already_exists: 206 json_dict['attachments'].append(new_attach) 207 208 # Append the part connections to the JSON structure 209 for new_connect in connections: 210 already_exists = False 211 for connect2 in json_dict['connections']: 212 if new_connect['source_node'] == connect2['destination_node'] and \ 213 new_connect['source_connection'] == connect2['destination_connection'] and \ 214 new_connect['destination_node'] == connect2['source_node'] and \ 215 new_connect['destination_connection'] == connect2['source_connection']: 216 already_exists = True 217 if not already_exists: 218 json_dict['connections'].append(new_connect) 219 220 # Return a string representation of the complete JSON structure 221 return json.dumps(json_dict, indent=3)
Returns a string-based JSON representation of the specified Assembly.
224def import_from_json(json_str: str) -> Assembly: 225 """Returns a new `Assembly` parsed from its JSON string representation in `json_str`.""" 226 227 # Parse the JSON string as an actual JSON dictionary 228 json_dict = json.loads(json_str) 229 assembly = Assembly(json_dict['name']) 230 231 # Iterate through all parts in the JSON structure 232 for part in json_dict['parts']: 233 234 # Convert the JSON part into its SymCAD representation 235 part_class = getattr(__import__('symcad'), 'parts') 236 for comp in part['type'].split('.'): 237 part_class = getattr(part_class, comp) 238 shape = part_class(part['name']) 239 geometry = Geometry(part['name']) 240 for property, value in part['geometry'].items(): 241 setattr(geometry, property, sympy.Symbol(value) if isinstance(value, str) else value) 242 setattr(shape, 'geometry', geometry) 243 setattr(shape, 'material_density', part['material_density']) 244 if part['static_origin'] is not None: 245 setattr(shape, 'static_origin', 246 Coordinate(part['name'] + '_origin', 247 x=sympy.Symbol(part['static_origin']['x']) 248 if isinstance(part['static_origin']['x'], str) else 249 part['static_origin']['x'], 250 y=sympy.Symbol(part['static_origin']['y']) 251 if isinstance(part['static_origin']['y'], str) else 252 part['static_origin']['y'], 253 z=sympy.Symbol(part['static_origin']['z']) 254 if isinstance(part['static_origin']['z'], str) else 255 part['static_origin']['z'])) 256 if part['static_placement'] is not None: 257 setattr(shape, 'static_placement', 258 Coordinate(part['name'] + '_placement', 259 x=sympy.Symbol(part['static_placement']['x']) 260 if isinstance(part['static_placement']['x'], str) else 261 part['static_placement']['x'], 262 y=sympy.Symbol(part['static_placement']['y']) 263 if isinstance(part['static_placement']['y'], str) else 264 part['static_placement']['y'], 265 z=sympy.Symbol(part['static_placement']['z']) 266 if isinstance(part['static_placement']['z'], str) else 267 part['static_placement']['z'])) 268 attachment_points = [] 269 for attachment_point in part['attachment_points']: 270 attachment_points.append(Coordinate(attachment_point['name'], 271 x=attachment_point['x'], 272 y=attachment_point['y'], 273 z=attachment_point['z'])) 274 connection_ports = [] 275 for connection_port in part['connection_ports']: 276 connection_ports.append(Coordinate(connection_port['name'], 277 x=connection_port['x'], 278 y=connection_port['y'], 279 z=connection_port['z'])) 280 setattr(shape, 'attachment_points', attachment_points) 281 setattr(shape, 'connection_ports', connection_ports) 282 setattr(getattr(shape, 'orientation'), 'roll', 283 sympy.Symbol(part['orientation']['roll']) 284 if isinstance(part['orientation']['roll'], str) else 285 math.radians(part['orientation']['roll'])) 286 setattr(getattr(shape, 'orientation'), 'pitch', 287 sympy.Symbol(part['orientation']['pitch']) 288 if isinstance(part['orientation']['pitch'], str) else 289 math.radians(part['orientation']['pitch'])) 290 setattr(getattr(shape, 'orientation'), 'yaw', 291 sympy.Symbol(part['orientation']['yaw']) 292 if isinstance(part['orientation']['yaw'], str) else 293 math.radians(part['orientation']['yaw'])) 294 setattr(shape, 'is_exposed', part['is_exposed']) 295 assembly.add_part(shape) 296 297 # Make all necessary part attachments 298 for attachment in json_dict['attachments']: 299 for part in assembly.parts: 300 if part.name == attachment['source_node']: 301 source = part 302 elif part.name == attachment['destination_node']: 303 dest = part 304 source.attach(attachment['source_attachment'], dest, attachment['destination_attachment']) 305 306 # Make all necessary part connections 307 for connection in json_dict['connections']: 308 for part in assembly.parts: 309 if part.name == connection['source_node']: 310 source = part 311 elif part.name == connection['destination_node']: 312 dest = part 313 source.connect(connection['source_connection'], dest, connection['destination_connection']) 314 315 return assembly
Returns a new Assembly parsed from its JSON string representation in json_str.