diff --git a/src/collision/shape/BoxShape.ts b/src/collision/shape/BoxShape.ts index 05773c48..9bcac7ce 100644 --- a/src/collision/shape/BoxShape.ts +++ b/src/collision/shape/BoxShape.ts @@ -33,6 +33,7 @@ import { PolygonShape } from './PolygonShape'; * A rectangle polygon which extend PolygonShape. */ export class BoxShape extends PolygonShape { + // note that box is serialized/deserialized as polygon static TYPE = 'polygon' as const; constructor(hx: number, hy: number, center?: Vec2Value, angle?: number) { diff --git a/src/serializer/__test__/Serialize.test.ts b/src/serializer/__test__/Serialize.test.ts index 1c03f72f..e1d7e86c 100644 --- a/src/serializer/__test__/Serialize.test.ts +++ b/src/serializer/__test__/Serialize.test.ts @@ -1,4 +1,4 @@ -// import { util } from 'util'; +// import util from 'util'; import { describe, it, expect } from 'vitest'; import { Vec2 } from '../../common/Vec2'; @@ -7,24 +7,24 @@ import { BoxShape } from '../../collision/shape/BoxShape'; import { DistanceJoint } from '../../dynamics/joint/DistanceJoint'; import { World } from '../../dynamics/World'; -import { Serializer } from '../../serializer'; +import { Serializer } from '../../serializer/index'; describe('Serializer', function(): void { it('saves and loads to JSON', function(): void { - var world = new World(); + const world = new World(); - var circle = new CircleShape(1); - var box = new BoxShape(1, 1); + const circle = new CircleShape(1); + const box = new BoxShape(1, 1); - var b1 = world.createBody({ + const b1 = world.createBody({ position : new Vec2(0, 0), type : 'dynamic' }); b1.createFixture(circle); - var b2 = world.createBody({ + const b2 = world.createBody({ position : new Vec2(2, 0), type : 'dynamic' }); @@ -37,16 +37,15 @@ describe('Serializer', function(): void { localAnchorB: new Vec2(0, -1) })); - var json = Serializer.toJson(world); - var text = JSON.stringify(json, null, ' '); - // console.log(util.inspect(json, false, null, true)); + const json1 = Serializer.toJson(world); + const text1 = JSON.stringify(json1, null, ' '); - world = Serializer.fromJson(json); - var json2 = Serializer.toJson(world); + const world2 = Serializer.fromJson(json1); + const json2 = Serializer.toJson(world2); - var text2 = JSON.stringify(json, null, ' '); + const text2 = JSON.stringify(json2, null, ' '); - expect(json).to.deep.equal(json2); - expect(text.split('\n')).to.deep.equal(text2.split('\n')); + expect(json1).to.deep.equal(json2); + expect(text1.split('\n')).to.deep.equal(text2.split('\n')); }); }); diff --git a/src/serializer/index.ts b/src/serializer/index.ts index 23e94afa..66fa8187 100644 --- a/src/serializer/index.ts +++ b/src/serializer/index.ts @@ -1,4 +1,3 @@ -// tslint:disable:typedef import { World } from '../dynamics/World'; import { Body } from '../dynamics/Body'; import { Joint } from '../dynamics/Joint'; @@ -7,7 +6,7 @@ import { Shape } from '../collision/Shape'; import { Vec2 } from '../common/Vec2'; import { Vec3 } from '../common/Vec3'; import { ChainShape } from "../collision/shape/ChainShape"; -import { BoxShape } from "../collision/shape/BoxShape"; +// import { BoxShape } from "../collision/shape/BoxShape"; import { EdgeShape } from "../collision/shape/EdgeShape"; import { PolygonShape } from "../collision/shape/PolygonShape"; import { CircleShape } from "../collision/shape/CircleShape"; @@ -25,110 +24,152 @@ import { WheelJoint } from "../dynamics/joint/WheelJoint"; let SID = 0; -export function Serializer(opts?) { - opts = opts || {}; - - const rootClass = opts.rootClass || World; - - const preSerialize = opts.preSerialize || function(obj) { return obj; }; - const postSerialize = opts.postSerialize || function(data, obj) { return data; }; - - const preDeserialize = opts.preDeserialize || function(data) { return data; }; - const postDeserialize = opts.postDeserialize || function(obj, data) { return obj; }; - - // This is used to create ref objects during serialize - const refTypes = { - World, - Body, - Joint, - Fixture, - Shape, - }; - - // This is used by restore to deserialize objects and refs - const restoreTypes = { - Vec2, - Vec3, - ...refTypes - }; +// Classes to be serialized as reference objects +const SERIALIZE_REF_TYPES = { + 'World': World, + 'Body': Body, + 'Joint': Joint, + 'Fixture': Fixture, + 'Shape': Shape, +}; + +// For deserializing reference objects by reference type +const DESERIALIZE_BY_REF_TYPE = { + 'Vec2': Vec2, + 'Vec3': Vec3, + 'World': World, + 'Body': Body, + 'Joint': Joint, + 'Fixture': Fixture, + 'Shape': Shape, +}; + +// For deserializing data objects by type field +const DESERIALIZE_BY_TYPE_FIELD = { + [Body.STATIC]: Body, + [Body.DYNAMIC]: Body, + [Body.KINEMATIC]: Body, + [ChainShape.TYPE]: ChainShape, + // [BoxShape.TYPE]: BoxShape, + [PolygonShape.TYPE]: PolygonShape, + [EdgeShape.TYPE]: EdgeShape, + [CircleShape.TYPE]: CircleShape, + [DistanceJoint.TYPE]: DistanceJoint, + [FrictionJoint.TYPE]: FrictionJoint, + [GearJoint.TYPE]: GearJoint, + [MotorJoint.TYPE]: MotorJoint, + [MouseJoint.TYPE]: MouseJoint, + [PrismaticJoint.TYPE]: PrismaticJoint, + [PulleyJoint.TYPE]: PulleyJoint, + [RevoluteJoint.TYPE]: RevoluteJoint, + [RopeJoint.TYPE]: RopeJoint, + [WeldJoint.TYPE]: WeldJoint, + [WheelJoint.TYPE]: WheelJoint, +} - const CLASS_BY_TYPE_PROP = { - [Body.STATIC]: Body, - [Body.DYNAMIC]: Body, - [Body.KINEMATIC]: Body, - [ChainShape.TYPE]: ChainShape, - [BoxShape.TYPE]: BoxShape, - [EdgeShape.TYPE]: EdgeShape, - [PolygonShape.TYPE]: PolygonShape, - [CircleShape.TYPE]: CircleShape, - [DistanceJoint.TYPE]: DistanceJoint, - [FrictionJoint.TYPE]: FrictionJoint, - [GearJoint.TYPE]: GearJoint, - [MotorJoint.TYPE]: MotorJoint, - [MouseJoint.TYPE]: MouseJoint, - [PrismaticJoint.TYPE]: PrismaticJoint, - [PulleyJoint.TYPE]: PulleyJoint, - [RevoluteJoint.TYPE]: RevoluteJoint, - [RopeJoint.TYPE]: RopeJoint, - [WeldJoint.TYPE]: WeldJoint, - [WheelJoint.TYPE]: WheelJoint, +// dummy types +type DataType = any; +type ObjectType = any; +type ClassName = any; + +type SerializedType = object[]; + +type RefType = { + refIndex: number, + refType: string, +}; + +type SerializerOptions = { + rootClass: ClassName, + preSerialize?: (obj: ObjectType) => DataType, + postSerialize?: (data: DataType, obj: any) => DataType, + preDeserialize?: (data: DataType) => DataType, + postDeserialize?: (obj: ObjectType, data: DataType) => ObjectType, +}; + +const DEFAULT_OPTIONS: SerializerOptions = { + rootClass: World, + preSerialize: function(obj) { return obj; }, + postSerialize: function(data, obj) { return data; }, + preDeserialize: function(data: DataType) { return data; }, + postDeserialize: function(obj, data) { return obj; }, +}; + +type DeserializeChildCallback = (classHint: any, obj: any, context: any) => any; +type ClassDeserializerMethod = (data: any, context: any, deserialize: DeserializeChildCallback) => any; + +export class Serializer { + options: SerializerOptions; + constructor(options: SerializerOptions) { + this.options = { + ...DEFAULT_OPTIONS, + ...options, + }; } - this.toJson = function(root) { + toJson = (root: T): SerializedType => { + const preSerialize = this.options.preSerialize; + const postSerialize = this.options.postSerialize; const json = []; - const queue = [root]; - const refMap = {}; + // Breadth-first ref serialization queue + const refQueue = [root]; + + const refMemoById: Record = {}; - function storeRef(value, typeName) { + function addToRefQueue(value: any, typeName: string) { value.__sid = value.__sid || ++SID; - if (!refMap[value.__sid]) { - queue.push(value); - const index = json.length + queue.length; + if (!refMemoById[value.__sid]) { + refQueue.push(value); + const index = json.length + refQueue.length; const ref = { refIndex: index, refType: typeName }; - refMap[value.__sid] = ref; + refMemoById[value.__sid] = ref; } - return refMap[value.__sid]; + return refMemoById[value.__sid]; } - function serialize(obj) { + function serializeWithHooks(obj: ObjectType) { obj = preSerialize(obj); let data = obj._serialize(); data = postSerialize(data, obj); return data; } - function toJson(value, top?) { + // traverse the object graph + // ref objects are pushed into the queue + // other objects are serialize in-place + function traverse(value: any, noRefType = false) { if (typeof value !== 'object' || value === null) { return value; } + // object with _serialize function if (typeof value._serialize === 'function') { - if (value !== top) { - // tslint:disable-next-line:no-for-in - for (const typeName in refTypes) { - if (value instanceof refTypes[typeName]) { - return storeRef(value, typeName); + if (!noRefType) { + for (const typeName in SERIALIZE_REF_TYPES) { + if (value instanceof SERIALIZE_REF_TYPES[typeName]) { + return addToRefQueue(value, typeName); } } } - value = serialize(value); + // object with _serialize function + value = serializeWithHooks(value); } + // recursive for arrays any objects if (Array.isArray(value)) { const newValue = []; for (let key = 0; key < value.length; key++) { - newValue[key] = toJson(value[key]); + newValue[key] = traverse(value[key]); } value = newValue; } else { const newValue = {}; - // tslint:disable-next-line:no-for-in for (const key in value) { if (value.hasOwnProperty(key)) { - newValue[key] = toJson(value[key]); + newValue[key] = traverse(value[key]); } } value = newValue; @@ -136,65 +177,74 @@ export function Serializer(opts?) { return value; } - while (queue.length) { - const obj = queue.shift(); - const str = toJson(obj, obj); + while (refQueue.length) { + const obj = refQueue.shift(); + const str = traverse(obj, true); json.push(str); } return json; - }; + } - this.fromJson = function(json: object) { - const refMap = {}; + fromJson = (json: SerializedType): T => { + const preDeserialize = this.options.preDeserialize; + const postDeserialize = this.options.postDeserialize; + const rootClass = this.options.rootClass; - function findDeserilizer(data, cls) { - if (!cls || !cls._deserialize) { - cls = CLASS_BY_TYPE_PROP[data.type] - } - return cls && cls._deserialize; - } + const deserializedRefMemoByIndex: Record = {}; - /** - * Deserialize a data object. - */ - function deserialize(cls, data, ctx) { - const deserializer = findDeserilizer(data, cls); + function deserializeWithHooks(classHint: ClassName, data: DataType, context: any): ObjectType { + if (!classHint || !classHint._deserialize) { + classHint = DESERIALIZE_BY_TYPE_FIELD[data.type] + } + const deserializer = classHint && classHint._deserialize; if (!deserializer) { return; } data = preDeserialize(data); - let obj = deserializer(data, ctx, restoreRef); + const classDeserializeFn = classHint._deserialize as ClassDeserializerMethod; + let obj = classDeserializeFn(data, context, deserializeChild); obj = postDeserialize(obj, data); return obj; } /** - * Restore a ref object or deserialize a data object. - * - * This is passed as callback to class deserializers. + * Recursive callback function to deserialize a child data object or reference object. + * + * @param classHint suggested class to deserialize obj to + * @param dataOrRef data or reference object + * @param context for example world when deserializing bodies and joints */ - function restoreRef(cls, ref, ctx) { - if (!ref.refIndex) { - return cls && cls._deserialize && deserialize(cls, ref, ctx); + function deserializeChild(classHint: ClassName, dataOrRef: DataType | RefType, context: any) { + const isRefObject = dataOrRef.refIndex && dataOrRef.refType; + if (!isRefObject) { + return deserializeWithHooks(classHint, dataOrRef, context); } - cls = restoreTypes[ref.refType] || cls; - const index = ref.refIndex; - if (!refMap[index]) { - const data = json[index]; - const obj = deserialize(cls, data, ctx); - refMap[index] = obj; + const ref = dataOrRef as RefType; + if (DESERIALIZE_BY_REF_TYPE[ref.refType]) { + classHint = DESERIALIZE_BY_REF_TYPE[ref.refType]; } - return refMap[index]; + const refIndex = ref.refIndex; + if (!deserializedRefMemoByIndex[refIndex]) { + const data = json[refIndex]; + const obj = deserializeWithHooks(classHint, data, context); + deserializedRefMemoByIndex[refIndex] = obj; + } + return deserializedRefMemoByIndex[refIndex]; } - const root = rootClass._deserialize(json[0], null, restoreRef); + const root = deserializeWithHooks(rootClass, json[0], null); return root; }; + + static toJson: (root: World) => SerializedType; + static fromJson: (json: SerializedType) => World; } -const serializer = new Serializer(); +const worldSerializer = new Serializer({ + rootClass: World, +}); -Serializer.toJson = serializer.toJson; -Serializer.fromJson = serializer.fromJson; +Serializer.fromJson = worldSerializer.fromJson; +Serializer.toJson = worldSerializer.toJson;