Skip to content

Commit 1d89804

Browse files
committed
Added GeomEncoder for JSON deprecating LocationEncoder
1 parent 1c9cd01 commit 1d89804

File tree

4 files changed

+172
-4
lines changed

4 files changed

+172
-4
lines changed

src/build123d/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
"Compound",
151151
"Location",
152152
"LocationEncoder",
153+
"GeomEncoder",
153154
"Joint",
154155
"RigidJoint",
155156
"RevoluteJoint",

src/build123d/geometry.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@
3939
import json
4040
import logging
4141
import numpy as np
42+
import warnings
4243

44+
from collections.abc import Iterable, Sequence
4345
from math import degrees, pi, radians, isclose
4446
from typing import Any, overload, TypeAlias, TYPE_CHECKING
4547

46-
from collections.abc import Iterable, Sequence
47-
4848
import OCP.TopAbs as TopAbs_ShapeEnum
4949

5050
from OCP.Bnd import Bnd_Box, Bnd_OBB
@@ -1252,6 +1252,68 @@ def __repr__(self) -> str:
12521252
return f"Color{str(tuple(self))}"
12531253

12541254

1255+
class GeomEncoder(json.JSONEncoder):
1256+
"""
1257+
A JSON encoder for build123d geometry objects.
1258+
1259+
This class extends ``json.JSONEncoder`` to provide custom serialization for
1260+
geometry objects such as Axis, Color, Location, Plane, and Vector. It converts
1261+
each geometry object into a dictionary containing exactly one key that identifies
1262+
the geometry type (e.g. ``"Axis"``, ``"Vector"``, etc.), paired with a tuple or
1263+
list that represents the underlying data. Any other object types are handled by
1264+
the standard encoder.
1265+
1266+
The inverse decoding is performed by the ``geometry_hook`` static method, which
1267+
expects the dictionary to have precisely one key from the known geometry types.
1268+
It then uses a class registry (``CLASS_REGISTRY``) to look up and instantiate
1269+
the appropriate class with the provided values.
1270+
1271+
**Usage Example**::
1272+
1273+
import json
1274+
1275+
# Suppose we have some geometry objects:
1276+
axis = Axis(position=(0, 0, 0), direction=(1, 0, 0))
1277+
vector = Vector(0.0, 1.0, 2.0)
1278+
1279+
data = {
1280+
"my_axis": axis,
1281+
"my_vector": vector
1282+
}
1283+
1284+
# Encode them to JSON:
1285+
encoded_data = json.dumps(data, cls=GeomEncoder, indent=4)
1286+
1287+
# Decode them back:
1288+
decoded_data = json.loads(encoded_data, object_hook=GeomEncoder.geometry_hook)
1289+
1290+
"""
1291+
1292+
def default(self, obj):
1293+
"""Return a JSON-serializable representation of a known geometry object."""
1294+
if isinstance(obj, Axis):
1295+
return {"Axis": (tuple(obj.position), tuple(obj.direction))}
1296+
elif isinstance(obj, Color):
1297+
return {"Color": obj.to_tuple()}
1298+
if isinstance(obj, Location):
1299+
return {"Location": obj.to_tuple()}
1300+
elif isinstance(obj, Plane):
1301+
return {"Plane": (tuple(obj.origin), tuple(obj.x_dir), tuple(obj.z_dir))}
1302+
elif isinstance(obj, Vector):
1303+
return {"Vector": tuple(obj)}
1304+
else:
1305+
# Let the base class default method raise the TypeError
1306+
return super().default(obj)
1307+
1308+
@staticmethod
1309+
def geometry_hook(json_dict):
1310+
"""Convert dictionaries back into geometry objects for decoding."""
1311+
if len(json_dict.items()) != 1:
1312+
raise ValueError(f"Invalid geometry json object {json_dict}")
1313+
for key, value in json_dict.items():
1314+
return CLASS_REGISTRY[key](*value)
1315+
1316+
12551317
class Location:
12561318
"""Location in 3D space. Depending on usage can be absolute or relative.
12571319
@@ -1714,6 +1776,7 @@ class LocationEncoder(json.JSONEncoder):
17141776

17151777
def default(self, o: Location) -> dict:
17161778
"""Return a serializable object"""
1779+
warnings.warn("Use GeomEncoder instead", DeprecationWarning, stacklevel=2)
17171780
if not isinstance(o, Location):
17181781
raise TypeError("Only applies to Location objects")
17191782
return {"Location": o.to_tuple()}
@@ -1725,6 +1788,7 @@ def location_hook(obj) -> dict:
17251788
Example:
17261789
read_json = json.load(infile, object_hook=LocationEncoder.location_hook)
17271790
"""
1791+
warnings.warn("Use GeomEncoder instead", DeprecationWarning, stacklevel=2)
17281792
if "Location" in obj:
17291793
obj = Location(*[[float(f) for f in v] for v in obj["Location"]])
17301794
return obj
@@ -2890,6 +2954,15 @@ def intersect(self, *args, **kwargs):
28902954
return shape.intersect(self)
28912955

28922956

2957+
CLASS_REGISTRY = {
2958+
"Axis": Axis,
2959+
"Color": Color,
2960+
"Location": Location,
2961+
"Plane": Plane,
2962+
"Vector": Vector,
2963+
}
2964+
2965+
28932966
def to_align_offset(
28942967
min_point: VectorLike,
28952968
max_point: VectorLike,

tests/test_direct_api/test_json.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""
2+
build123d tests
3+
4+
name: test_json.py
5+
by: Gumyr
6+
date: February 24, 2025
7+
8+
desc:
9+
This python module contains tests for the build123d project.
10+
11+
license:
12+
13+
Copyright 2025 Gumyr
14+
15+
Licensed under the Apache License, Version 2.0 (the "License");
16+
you may not use this file except in compliance with the License.
17+
You may obtain a copy of the License at
18+
19+
http://www.apache.org/licenses/LICENSE-2.0
20+
21+
Unless required by applicable law or agreed to in writing, software
22+
distributed on an "AS IS" BASIS,
23+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24+
See the License for the specific language governing permissions and
25+
limitations under the License.
26+
27+
"""
28+
29+
import json
30+
import os
31+
import unittest
32+
from build123d.geometry import (
33+
Axis,
34+
Color,
35+
GeomEncoder,
36+
Location,
37+
LocationEncoder,
38+
Matrix,
39+
Plane,
40+
Rotation,
41+
Vector,
42+
)
43+
44+
45+
class TestGeomEncode(unittest.TestCase):
46+
47+
def test_as_json(self):
48+
49+
a_json = json.dumps(Axis.Y, cls=GeomEncoder)
50+
axis = json.loads(a_json, object_hook=GeomEncoder.geometry_hook)
51+
self.assertEqual(Axis.Y, axis)
52+
53+
c_json = json.dumps(Color("red"), cls=GeomEncoder)
54+
color = json.loads(c_json, object_hook=GeomEncoder.geometry_hook)
55+
self.assertEqual(Color("red").to_tuple(), color.to_tuple())
56+
57+
loc = Location((0, 1, 2), (4, 8, 16))
58+
l_json = json.dumps(loc, cls=GeomEncoder)
59+
loc_json = json.loads(l_json, object_hook=GeomEncoder.geometry_hook)
60+
self.assertAlmostEqual(loc.position, loc_json.position, 5)
61+
self.assertAlmostEqual(loc.orientation, loc_json.orientation, 5)
62+
63+
with self.assertWarnsRegex(DeprecationWarning, "Use GeomEncoder instead"):
64+
loc_legacy = json.loads(l_json, object_hook=LocationEncoder.location_hook)
65+
self.assertAlmostEqual(loc.position, loc_legacy.position, 5)
66+
self.assertAlmostEqual(loc.orientation, loc_legacy.orientation, 5)
67+
68+
p_json = json.dumps(Plane.XZ, cls=GeomEncoder)
69+
plane = json.loads(p_json, object_hook=GeomEncoder.geometry_hook)
70+
self.assertEqual(Plane.XZ, plane)
71+
72+
rot = Rotation((0, 1, 4))
73+
r_json = json.dumps(rot, cls=GeomEncoder)
74+
rotation = json.loads(r_json, object_hook=GeomEncoder.geometry_hook)
75+
self.assertAlmostEqual(rot.position, rotation.position, 5)
76+
self.assertAlmostEqual(rot.orientation, rotation.orientation, 5)
77+
78+
v_json = json.dumps(Vector(1, 2, 4), cls=GeomEncoder)
79+
vector = json.loads(v_json, object_hook=GeomEncoder.geometry_hook)
80+
self.assertEqual(Vector(1, 2, 4), vector)
81+
82+
def test_as_json_error(self):
83+
with self.assertRaises(TypeError):
84+
json.dumps(Matrix(), cls=GeomEncoder)
85+
86+
v_json = '{"Vector": [1.0, 2.0, 4.0], "Color": [0, 0, 0, 0]}'
87+
with self.assertRaises(ValueError):
88+
json.loads(v_json, object_hook=GeomEncoder.geometry_hook)
89+
90+
91+
if __name__ == "__main__":
92+
unittest.main()

tests/test_direct_api/test_location.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,15 +290,17 @@ def test_as_json(self):
290290
}
291291

292292
# Serializing json with custom Location encoder
293-
json_object = json.dumps(data_dict, indent=4, cls=LocationEncoder)
293+
with self.assertWarnsRegex(DeprecationWarning, "Use GeomEncoder instead"):
294+
json_object = json.dumps(data_dict, indent=4, cls=LocationEncoder)
294295

295296
# Writing to sample.json
296297
with open("sample.json", "w") as outfile:
297298
outfile.write(json_object)
298299

299300
# Reading from sample.json
300301
with open("sample.json") as infile:
301-
read_json = json.load(infile, object_hook=LocationEncoder.location_hook)
302+
with self.assertWarnsRegex(DeprecationWarning, "Use GeomEncoder instead"):
303+
read_json = json.load(infile, object_hook=LocationEncoder.location_hook)
302304

303305
# Validate locations
304306
for key, value in read_json.items():

0 commit comments

Comments
 (0)