@@ -71,6 +71,24 @@ def convert(part):
7171 return [convert (c ) for c in re .split ("([0-9]+)" , text )]
7272
7373
74+ def canonicalize_json (obj : Any ) -> Any :
75+ """
76+ Recursively canonicalize a JSON-serializable object for deterministic output.
77+
78+ - Dicts are converted to sorted dicts (by key)
79+ - Lists are sorted by their JSON string representation
80+ - Primitives are returned as-is
81+ """
82+ if isinstance (obj , dict ):
83+ return {k : canonicalize_json (v ) for k , v in sorted (obj .items ())}
84+ elif isinstance (obj , list ):
85+ canonicalized = [canonicalize_json (item ) for item in obj ]
86+ # Sort by JSON representation for stable ordering
87+ return sorted (canonicalized , key = lambda x : json .dumps (x , sort_keys = True ))
88+ else :
89+ return obj
90+
91+
7492# Read PYTHONPATH environment variable and add all folders to the search path
7593python_path = os .environ .get ("PYTHONPATH" , "" )
7694path_separator = (
@@ -3327,36 +3345,33 @@ def _get_footprint_data(self, fp: pcbnew.FOOTPRINT) -> dict:
33273345 }
33283346 for pad in fp .Pads ()
33293347 ],
3330- "graphical_items" : sorted (
3331- [
3332- {
3333- "type" : item .GetClass (),
3334- "layer" : item .GetLayerName (),
3335- "position" : {
3336- "x" : item .GetPosition ().x ,
3337- "y" : item .GetPosition ().y ,
3338- },
3339- "start" : (
3340- {"x" : item .GetStart ().x , "y" : item .GetStart ().y }
3341- if hasattr (item , "GetStart" )
3342- else None
3343- ),
3344- "end" : (
3345- {"x" : item .GetEnd ().x , "y" : item .GetEnd ().y }
3346- if hasattr (item , "GetEnd" )
3347- else None
3348- ),
3349- "angle" : (
3350- item .GetAngle () if hasattr (item , "GetAngle" ) else None
3351- ),
3352- "text" : item .GetText () if hasattr (item , "GetText" ) else None ,
3353- "shape" : item .GetShape () if hasattr (item , "GetShape" ) else None ,
3354- "width" : item .GetWidth () if hasattr (item , "GetWidth" ) else None ,
3355- }
3356- for item in fp .GraphicalItems ()
3357- ],
3358- key = lambda g : (g ["position" ]["x" ], g ["position" ]["y" ]),
3359- ),
3348+ "graphical_items" : [
3349+ {
3350+ "type" : item .GetClass (),
3351+ "layer" : item .GetLayerName (),
3352+ "position" : {
3353+ "x" : item .GetPosition ().x ,
3354+ "y" : item .GetPosition ().y ,
3355+ },
3356+ "start" : (
3357+ {"x" : item .GetStart ().x , "y" : item .GetStart ().y }
3358+ if hasattr (item , "GetStart" )
3359+ else None
3360+ ),
3361+ "end" : (
3362+ {"x" : item .GetEnd ().x , "y" : item .GetEnd ().y }
3363+ if hasattr (item , "GetEnd" )
3364+ else None
3365+ ),
3366+ "angle" : (
3367+ item .GetAngle () if hasattr (item , "GetAngle" ) else None
3368+ ),
3369+ "text" : item .GetText () if hasattr (item , "GetText" ) else None ,
3370+ "shape" : item .GetShape () if hasattr (item , "GetShape" ) else None ,
3371+ "width" : item .GetWidth () if hasattr (item , "GetWidth" ) else None ,
3372+ }
3373+ for item in fp .GraphicalItems ()
3374+ ],
33603375 }
33613376
33623377 def _get_group_data (self , group : pcbnew .PCB_GROUP ) -> dict :
@@ -3499,37 +3514,16 @@ def _export_layout_snapshot(self):
34993514 self .board .Groups (), key = lambda g : g .GetName () or ""
35003515 )
35013516 ],
3502- "zones" : [
3503- self ._get_zone_data (zone )
3504- for zone in sorted (
3505- self .board .Zones (), key = lambda z : z .GetZoneName () or ""
3506- )
3507- ],
3508- "tracks" : [
3509- self ._get_track_data (track )
3510- for track in sorted (
3511- tracks , key = lambda t : (t .GetNetname () or "" , t .GetLayerName () or "" )
3512- )
3513- ],
3514- "vias" : [
3515- self ._get_via_data (via )
3516- for via in sorted (
3517- vias ,
3518- key = lambda v : (
3519- v .GetNetname () or "" ,
3520- v .GetPosition ().x ,
3521- v .GetPosition ().y ,
3522- ),
3523- )
3524- ],
3517+ "zones" : [self ._get_zone_data (zone ) for zone in self .board .Zones ()],
3518+ "tracks" : [self ._get_track_data (track ) for track in tracks ],
3519+ "vias" : [self ._get_via_data (via ) for via in vias ],
35253520 }
35263521
35273522 with self .snapshot_path .open ("w" , encoding = "utf-8" ) as f :
35283523 json .dump (
3529- snapshot ,
3524+ canonicalize_json ( snapshot ) ,
35303525 f ,
35313526 indent = 2 ,
3532- sort_keys = True , # Ensure all dictionaries are sorted by key
35333527 ensure_ascii = False ,
35343528 )
35353529
0 commit comments