Compare commits
13 Commits
b2506063ca
...
3aacc77ccd
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3aacc77ccd | ||
![]() |
098e18ea76 | ||
![]() |
86063aae5a | ||
![]() |
f82e336849 | ||
![]() |
e128f6f7da | ||
![]() |
a8ab286133 | ||
![]() |
08a07ea5a1 | ||
![]() |
428a18fbf3 | ||
![]() |
00ea2f69e0 | ||
![]() |
e5ac510839 | ||
![]() |
aaa6ac7cc3 | ||
![]() |
4461121a6c | ||
![]() |
ff2a3fffb3 |
|
@ -14,12 +14,12 @@
|
|||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"exports": "./dist/index.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.umd.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"name": "ecstc",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
104
src/codecs.js
104
src/codecs.js
|
@ -1,4 +1,57 @@
|
|||
function registerCodecs(Codecs) {
|
||||
|
||||
class SparseArray {
|
||||
|
||||
constructor(blueprint) {
|
||||
this.$$isSparseCodec = new Codecs.bool();
|
||||
this.$$arrayCodec = new Codecs.array(blueprint);
|
||||
this.$$sparseCodec = new Codecs.map({key: {type: 'varuint'}, value: blueprint.element});
|
||||
}
|
||||
|
||||
decode(view, target) {
|
||||
const isSparse = this.$$isSparseCodec.decode(view, target);
|
||||
if (isSparse) {
|
||||
return Object.fromEntries(this.$$sparseCodec.decode(view, target));
|
||||
}
|
||||
else {
|
||||
return this.$$arrayCodec.decode(view, target);
|
||||
}
|
||||
}
|
||||
|
||||
encode(value, view, byteOffset) {
|
||||
let written = 0;
|
||||
if (Array.isArray(value)) {
|
||||
written += this.$$isSparseCodec.encode(false, view, byteOffset + written);
|
||||
written += this.$$arrayCodec.encode(value, view, byteOffset + written);
|
||||
}
|
||||
else {
|
||||
written += this.$$isSparseCodec.encode(true, view, byteOffset + written);
|
||||
const map = [];
|
||||
for (const key in value) {
|
||||
map.push([parseInt(key), value[key]]);
|
||||
}
|
||||
written += this.$$sparseCodec.encode(map, view, byteOffset + written);
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
size(value) {
|
||||
let size = this.$$isSparseCodec.size();
|
||||
if (Array.isArray(value)) {
|
||||
size += this.$$arrayCodec.size(value);
|
||||
}
|
||||
else {
|
||||
const map = [];
|
||||
for (const key in value) {
|
||||
map.push([parseInt(key), value[key]]);
|
||||
}
|
||||
size += this.$$sparseCodec.size(map);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MaybeDeletion extends Codecs.object {
|
||||
|
||||
constructor(blueprint) {
|
||||
|
@ -33,14 +86,17 @@ function registerCodecs(Codecs) {
|
|||
|
||||
}
|
||||
|
||||
function convertPropertiesToOptional(blueprint) {
|
||||
if ('properties' in blueprint) {
|
||||
const newBlueprint = {properties: {}, type: blueprint.type}
|
||||
function convertProperties(blueprint) {
|
||||
if ('element' in blueprint) {
|
||||
return {element: convertProperties(blueprint.element), type: 'ecstc-sparse-array'};
|
||||
}
|
||||
else if ('properties' in blueprint) {
|
||||
const newBlueprint = {properties: {}, type: blueprint.type};
|
||||
for (const key in blueprint.properties) {
|
||||
newBlueprint.properties[key] = {
|
||||
...convertPropertiesToOptional(blueprint.properties[key]),
|
||||
...convertProperties(blueprint.properties[key]),
|
||||
optional: true,
|
||||
}
|
||||
};
|
||||
}
|
||||
return newBlueprint;
|
||||
}
|
||||
|
@ -52,7 +108,32 @@ function registerCodecs(Codecs) {
|
|||
class Component extends MaybeDeletion {
|
||||
|
||||
constructor(blueprint) {
|
||||
super(convertPropertiesToOptional(blueprint));
|
||||
super(convertProperties(blueprint));
|
||||
}
|
||||
|
||||
decode(view, target) {
|
||||
const isDeletion = this.$$isDeletion.decode(view, target);
|
||||
if (isDeletion) {
|
||||
return false;
|
||||
}
|
||||
return super.decode(view, target);
|
||||
}
|
||||
|
||||
encode(value, view, byteOffset) {
|
||||
let written = 0;
|
||||
written += this.$$isDeletion.encode(false === value, view, byteOffset);
|
||||
if (value) {
|
||||
written += super.encode(value, view, byteOffset + written);
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
size(value) {
|
||||
let size = this.$$isDeletion.size();
|
||||
if (value) {
|
||||
size += super.size(value);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -60,14 +141,14 @@ function registerCodecs(Codecs) {
|
|||
class Entity extends MaybeDeletion {
|
||||
|
||||
constructor(blueprint) {
|
||||
const {ecs} = blueprint;
|
||||
const {Components} = blueprint;
|
||||
const properties = {};
|
||||
for (const componentName in ecs.Components) {
|
||||
const Component = ecs.Components[componentName];
|
||||
for (const componentName in Components) {
|
||||
const Component = Components[componentName];
|
||||
properties[componentName] = {
|
||||
optional: true,
|
||||
type: 'ecstc-component',
|
||||
properties: Component.constructor.properties,
|
||||
properties: Component.properties,
|
||||
};
|
||||
}
|
||||
super({properties});
|
||||
|
@ -80,7 +161,7 @@ function registerCodecs(Codecs) {
|
|||
constructor(blueprint) {
|
||||
super({
|
||||
key: {type: 'varuint'},
|
||||
value: {type: 'ecstc-entity', ecs: blueprint.ecs},
|
||||
value: {type: 'ecstc-entity', Components: blueprint.Components},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -89,6 +170,7 @@ function registerCodecs(Codecs) {
|
|||
Codecs['ecstc-ecs'] = Ecs;
|
||||
Codecs['ecstc-entity'] = Entity;
|
||||
Codecs['ecstc-component'] = Component;
|
||||
Codecs['ecstc-sparse-array'] = SparseArray;
|
||||
}
|
||||
|
||||
export default registerCodecs;
|
||||
|
|
|
@ -30,10 +30,16 @@ class Component {
|
|||
|
||||
instance() {
|
||||
const Component = this;
|
||||
const properties = {};
|
||||
const {constructor} = this;
|
||||
const Instance = class {
|
||||
entityId;
|
||||
constructor() {
|
||||
for (const key in constructor.properties) {
|
||||
const propertyBlueprint = constructor.properties[key];
|
||||
const Property = Properties[propertyBlueprint.type];
|
||||
Property.define(this, key, propertyBlueprint);
|
||||
}
|
||||
}
|
||||
destroy() {
|
||||
this.set(undefined);
|
||||
}
|
||||
|
@ -44,16 +50,24 @@ class Component {
|
|||
}
|
||||
set(entityId, values = {}) {
|
||||
this.entityId = entityId;
|
||||
for (const key in properties) {
|
||||
for (const key in constructor.properties) {
|
||||
if (key in values) {
|
||||
this[key] = values[key];
|
||||
continue;
|
||||
}
|
||||
const propertyBlueprint = constructor.properties[key];
|
||||
if ('defaultValue' in propertyBlueprint) {
|
||||
this[key] = propertyBlueprint.defaultValue;
|
||||
continue;
|
||||
}
|
||||
const Property = Properties[propertyBlueprint.type];
|
||||
this[key] = Property.defaultValue(values[key], propertyBlueprint);
|
||||
this[key] = Property.defaultValue();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
toJSON() {
|
||||
const json = {};
|
||||
for (const key in properties) {
|
||||
for (const key in constructor.properties) {
|
||||
if ('object' === typeof this[key] && this[key].toJSON) {
|
||||
json[key] = this[key].toJSON();
|
||||
}
|
||||
|
@ -64,12 +78,6 @@ class Component {
|
|||
return json;
|
||||
}
|
||||
};
|
||||
for (const key in constructor.properties) {
|
||||
const propertyBlueprint = constructor.properties[key];
|
||||
const Property = Properties[propertyBlueprint.type];
|
||||
properties[key] = Property.definitions(propertyBlueprint, key);
|
||||
}
|
||||
Object.defineProperties(Instance.prototype, properties);
|
||||
return Instance;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,10 +22,6 @@ test('component', () => {
|
|||
change = {[entityId]: {[componentName]: change}};
|
||||
switch (changeIndex++) {
|
||||
case 0: {
|
||||
expect(change).to.deep.equal({1: {Number: {number: 128}}});
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
expect(change).to.deep.equal({1: {Number: {number: 129}}});
|
||||
break;
|
||||
}
|
||||
|
@ -38,7 +34,7 @@ test('component', () => {
|
|||
expect(instance.toJSON()).to.deep.equal({number: 128});
|
||||
instance.number = 129;
|
||||
expect(instance.toJSON()).to.deep.equal({number: 129});
|
||||
expect(changeIndex).to.equal(2);
|
||||
expect(changeIndex).to.equal(1);
|
||||
});
|
||||
|
||||
test('empty', () => {
|
||||
|
|
20
src/ecs.js
20
src/ecs.js
|
@ -8,6 +8,7 @@ class Ecs {
|
|||
changes = [];
|
||||
Components = {};
|
||||
entities = new Map();
|
||||
globals = {};
|
||||
Systems = {};
|
||||
tracking = true;
|
||||
|
||||
|
@ -57,7 +58,15 @@ class Ecs {
|
|||
continue;
|
||||
}
|
||||
for (const key in values) {
|
||||
entity[componentName][key] = values[key];
|
||||
if (
|
||||
'object' === typeof entity[componentName][key]
|
||||
&& 'merge' in entity[componentName][key]
|
||||
) {
|
||||
entity[componentName][key].merge(values[key]);
|
||||
}
|
||||
else {
|
||||
entity[componentName][key] = values[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,14 +79,15 @@ class Ecs {
|
|||
|
||||
createSpecific(entityId, components) {
|
||||
const entity = new Entity(entityId);
|
||||
let added = false;
|
||||
let addedAnyComponents = false;
|
||||
for (const componentName in components) {
|
||||
if (this.Components[componentName]) {
|
||||
added = true;
|
||||
entity.addComponent(this.Components[componentName], components[componentName]);
|
||||
}
|
||||
this.markChange(entityId, componentName, {});
|
||||
addedAnyComponents = true;
|
||||
}
|
||||
if (!added) {
|
||||
if (!addedAnyComponents) {
|
||||
this.markChange(entityId);
|
||||
}
|
||||
this.entities.set(entityId, entity);
|
||||
|
@ -163,7 +173,7 @@ class Ecs {
|
|||
if (!l[key]) {
|
||||
l[key] = [];
|
||||
}
|
||||
for (const j of value) {
|
||||
for (const j in value) {
|
||||
l[key][j] = value[j];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,13 @@ import {Codecs, Schema} from 'crunches';
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import registerCodecs from './codecs.js';
|
||||
import Component from './component.js';
|
||||
import Ecs from './ecs.js';
|
||||
import System from './system.js';
|
||||
import {fakeEnvironment} from './test-helper.js';
|
||||
|
||||
const {Components} = fakeEnvironment();
|
||||
const {A, D} = Components;
|
||||
const {A, D, E} = Components;
|
||||
|
||||
test('smoke', () => {
|
||||
expect(() => new Ecs({Components: {}, Systems: {}})).not.toThrowError();
|
||||
|
@ -30,7 +31,7 @@ test('components', () => {
|
|||
test('diffs', () => {
|
||||
const ecs = new Ecs({Components: {A}, Systems: {}});
|
||||
const entity = ecs.create({A: {}});
|
||||
expect(ecs.diff()).to.deep.equal(new Map([[entity.id, {A: {a: 64}}]]));
|
||||
expect(ecs.diff()).to.deep.equal(new Map([[entity.id, {A: {}}]]));
|
||||
// memoized
|
||||
expect(ecs.diff()).to.equal(ecs.diff());
|
||||
entity.A.a = 32;
|
||||
|
@ -47,6 +48,17 @@ test('diffs', () => {
|
|||
expect(ecs.diff()).to.deep.equal(new Map([[1, false]]));
|
||||
});
|
||||
|
||||
test('nested diffs', () => {
|
||||
const ecs = new Ecs({Components: {E}, Systems: {}});
|
||||
const entity = ecs.create({E: {}});
|
||||
expect(ecs.diff()).to.deep.equal(new Map([[entity.id, {E: {}}]]));
|
||||
// memoized
|
||||
expect(ecs.diff()).to.equal(ecs.diff());
|
||||
entity.E.e.f = 32;
|
||||
expect(ecs.changes.length).to.equal(2);
|
||||
expect(ecs.diff()).to.deep.equal(new Map([[entity.id, {E: {e: {f: 32}}}]]));
|
||||
});
|
||||
|
||||
test('defers destruction', () => {
|
||||
const ecs = new Ecs({Components: {A}, Systems: {}});
|
||||
const entity = ecs.create({A: {}});
|
||||
|
@ -91,11 +103,12 @@ test('indexing', () => {
|
|||
|
||||
test('serialization', async () => {
|
||||
registerCodecs(Codecs);
|
||||
const ecs = new Ecs({Components: {D}, Systems: {}});
|
||||
const Components = {D};
|
||||
const ecs = new Ecs({Components, Systems: {}});
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
ecs.create((i & 1) ? {} : {D: {d: 1 / (i + 1)}});
|
||||
}
|
||||
const schema = new Schema({type: 'ecstc-ecs', ecs});
|
||||
const schema = new Schema({type: 'ecstc-ecs', Components});
|
||||
let view;
|
||||
view = schema.encode(ecs.diff());
|
||||
expect(schema.decode(view)).to.deep.equal(ecs.diff());
|
||||
|
@ -107,7 +120,7 @@ test('serialization', async () => {
|
|||
});
|
||||
|
||||
test('apply', () => {
|
||||
const ecs = new Ecs({Components: {A}, Systems: {}});
|
||||
const ecs = new Ecs({Components: {A, D, E}, Systems: {}});
|
||||
ecs.apply(new Map([[32, {}]]))
|
||||
const entity = ecs.entities.get(32);
|
||||
expect(entity).not.to.be.undefined;
|
||||
|
@ -122,4 +135,55 @@ test('apply', () => {
|
|||
ecs.apply(new Map([[32, false]]))
|
||||
expect(ecs.entities.get(32)).to.be.undefined;
|
||||
expect(ecs.diff()).to.deep.equal(new Map());
|
||||
const d = ecs.create({D: {d: 32, e: 33}});
|
||||
ecs.apply(new Map([[d.id, {D: {e: 34}}]]))
|
||||
expect(d.D.d).to.equal(32);
|
||||
expect(d.D.e).to.equal(34);
|
||||
const e = ecs.create({E: {e: {f: 4}}});
|
||||
ecs.apply(new Map([[e.id, {E: {e: {g: 5}}}]]))
|
||||
expect(e.E.e.f).to.equal(4);
|
||||
expect(e.E.e.g).to.equal(5);
|
||||
});
|
||||
|
||||
test('sparse array', async () => {
|
||||
registerCodecs(Codecs);
|
||||
class TileLayers extends Component {
|
||||
static componentName = 'TileLayers';
|
||||
static properties = {
|
||||
layers: {
|
||||
type: 'array',
|
||||
element: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: {
|
||||
type: 'array',
|
||||
element: {
|
||||
type: 'uint16',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const data = Array(60 * 60).fill(162);
|
||||
const Components = {D, TileLayers};
|
||||
const ecs = new Ecs({Components, Systems: {}});
|
||||
const entity = ecs.create({
|
||||
TileLayers: {
|
||||
layers: [
|
||||
{
|
||||
data,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
ecs.setClean();
|
||||
entity.TileLayers.layers.at(0).data.setAt(1, 2);
|
||||
const schema = new Schema({type: 'ecstc-ecs', Components});
|
||||
let view;
|
||||
view = schema.encode(ecs.diff());
|
||||
entity.TileLayers.layers.at(0).data.setAt(1, 0);
|
||||
ecs.apply(schema.decode(view));
|
||||
expect(entity.TileLayers.layers.at(0).data.at(1)).to.equal(2);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {definitions, Properties} from './properties.js';
|
||||
import {define, Properties} from './properties.js';
|
||||
|
||||
const properties = import.meta.glob(
|
||||
['./properties/*.js', '!./properties/*.test.js'],
|
||||
|
@ -21,6 +21,8 @@ for (const numberType of [
|
|||
|
||||
delete Properties.number;
|
||||
|
||||
export {definitions, Properties};
|
||||
|
||||
export {default as registerCodecs} from './codecs.js';
|
||||
export {default as Component} from './component.js';
|
||||
export {default as Ecs} from './ecs.js';
|
||||
export {default as System} from './system.js';
|
||||
export {define, Properties};
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
export const Properties = {};
|
||||
|
||||
export function definitions(blueprint, key) {
|
||||
const privateKey = `$$${key ?? 'value'}`;
|
||||
return {
|
||||
get: function() {
|
||||
return this[privateKey];
|
||||
},
|
||||
set: function(value) {
|
||||
if (this[privateKey] !== value) {
|
||||
this[privateKey] = value;
|
||||
if (this.markChange) {
|
||||
if (key) {
|
||||
this.markChange({[key]: value});
|
||||
export function define(receiver, key, blueprint) {
|
||||
const privateKey = `$$${key}`;
|
||||
receiver[privateKey] = blueprint.defaultValue ?? this.defaultValue();
|
||||
if (receiver.markChange) {
|
||||
receiver.markChange({[key]: receiver[privateKey]});
|
||||
}
|
||||
Object.defineProperties(
|
||||
receiver,
|
||||
{
|
||||
[key]: {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
return receiver[privateKey];
|
||||
},
|
||||
set: (value) => {
|
||||
if (receiver[privateKey] !== value) {
|
||||
receiver[privateKey] = value;
|
||||
if (receiver.markChange) {
|
||||
receiver.markChange({[key]: value});
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.markChange(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,93 +1,163 @@
|
|||
import {Properties} from '../properties.js';
|
||||
|
||||
const ArrayProperty = {
|
||||
defaultValue(value = [], blueprint) {
|
||||
const defaultValue = [];
|
||||
const elementBlueprint = blueprint.element;
|
||||
const ElementClass = Properties[elementBlueprint.type];
|
||||
for (const key in value) {
|
||||
defaultValue[key] = ElementClass.defaultValue(value[key] ?? blueprint.defaultValue?.[key], elementBlueprint);
|
||||
}
|
||||
return defaultValue;
|
||||
defaultValue() {
|
||||
return [];
|
||||
},
|
||||
definitions(blueprint, key) {
|
||||
define(receiver, key, blueprint) {
|
||||
let isPrimitiveType = false;
|
||||
switch (blueprint.element.type) {
|
||||
case 'bool':
|
||||
case 'int8': case 'uint8':
|
||||
case 'int16': case 'uint16':
|
||||
case 'int32': case 'uint32':
|
||||
case 'float32': case 'float64':
|
||||
case 'string': {
|
||||
isPrimitiveType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const privateKey = `$$${key}`;
|
||||
const privateArrayKey = `$$array$$${key}`;
|
||||
return {
|
||||
get: function() {
|
||||
return this[privateKey];
|
||||
let coalescing = true;
|
||||
let coalesced;
|
||||
const flushCoalesced = () => {
|
||||
if (coalesced) {
|
||||
if (receiver.markChange) {
|
||||
receiver.markChange({[key]: coalesced});
|
||||
}
|
||||
coalesced = undefined;
|
||||
}
|
||||
coalescing = false;
|
||||
}
|
||||
receiver[privateKey] = [];
|
||||
const toJSON = () => {
|
||||
return receiver[privateKey].map((element) => {
|
||||
return 'object' === typeof element ? element.toJSON() : element;
|
||||
});
|
||||
};
|
||||
if (receiver.markChange) {
|
||||
receiver[privateKey].markChange = (change) => {
|
||||
if (!coalescing) {
|
||||
if (receiver.markChange) {
|
||||
receiver.markChange({[key]: change});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!coalesced) {
|
||||
coalesced = {};
|
||||
}
|
||||
coalesced = {
|
||||
...coalesced,
|
||||
...change,
|
||||
};
|
||||
};
|
||||
}
|
||||
const api = {
|
||||
[Symbol.iterator]: () => {
|
||||
if (isPrimitiveType) {
|
||||
return receiver[privateKey].values();
|
||||
}
|
||||
const protocol = toJSON().values();
|
||||
return {
|
||||
next: () => {
|
||||
let result = protocol.next();
|
||||
if (result.done) {
|
||||
return {done: true};
|
||||
}
|
||||
return {done: false, value: result.value};
|
||||
},
|
||||
};
|
||||
},
|
||||
set: function(value) {
|
||||
if (this[privateArrayKey] !== value) {
|
||||
const elementBlueprint = blueprint.element;
|
||||
const ElementClass = Properties[elementBlueprint.type];
|
||||
this[privateArrayKey] = value;
|
||||
this[privateKey] = Object.defineProperties({}, {
|
||||
[Symbol.iterator]: {
|
||||
value: () => {
|
||||
const protocol = this[privateArrayKey].values();
|
||||
return {
|
||||
next: () => {
|
||||
let result = protocol.next();
|
||||
if (result.done) {
|
||||
return {done: true};
|
||||
}
|
||||
return {done: false, value: result.value?.proxy};
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
at: {
|
||||
value: (index) => this[privateArrayKey].at(index).proxy,
|
||||
},
|
||||
setAt: {
|
||||
value: (index, value) => {
|
||||
let suppress = true;
|
||||
this[privateArrayKey][index] = Object.defineProperties(
|
||||
{
|
||||
markChange: (change) => {
|
||||
if (!suppress) {
|
||||
if (this.markChange) {
|
||||
if (key) {
|
||||
this.markChange({[key]: {[index]: change}});
|
||||
}
|
||||
else {
|
||||
this.markChange({[index]: change});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
proxy: ElementClass.definitions(
|
||||
elementBlueprint,
|
||||
),
|
||||
},
|
||||
);
|
||||
this[privateArrayKey][index].proxy = value;
|
||||
if (this.markChange) {
|
||||
if (key) {
|
||||
this.markChange({[key]: {[index]: value}});
|
||||
}
|
||||
else {
|
||||
this.markChange({[index]: value});
|
||||
}
|
||||
}
|
||||
suppress = false;
|
||||
},
|
||||
},
|
||||
toJSON: {
|
||||
value: () => {
|
||||
return Array.from(this[privateKey]);
|
||||
},
|
||||
},
|
||||
});
|
||||
for (const key in value) {
|
||||
this[privateKey].setAt(key, ElementClass.defaultValue(value?.[key] ?? blueprint.defaultValue?.[key], elementBlueprint));
|
||||
at: (index) => {
|
||||
return receiver[privateKey][index];
|
||||
},
|
||||
get length() {
|
||||
return receiver[privateKey].length;
|
||||
},
|
||||
merge(value) {
|
||||
coalescing = true;
|
||||
if (isPrimitiveType) {
|
||||
const change = {};
|
||||
for (const elementKey in value) {
|
||||
receiver[privateKey][elementKey] = value[elementKey];
|
||||
change[elementKey] = value[elementKey];
|
||||
}
|
||||
if (receiver.markChange) {
|
||||
receiver[privateKey].markChange(change);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (const elementKey in value) {
|
||||
receiver[key].setAt(elementKey, value[elementKey]);
|
||||
}
|
||||
}
|
||||
flushCoalesced();
|
||||
},
|
||||
setAt: (index, value) => {
|
||||
const elementBlueprint = blueprint.element;
|
||||
let wasCoalescing = coalescing;
|
||||
coalescing = true;
|
||||
if (isPrimitiveType) {
|
||||
receiver[privateKey][index] = value;
|
||||
if (receiver.markChange) {
|
||||
receiver[privateKey].markChange({[index]: value});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!(index in receiver[privateKey])) {
|
||||
Properties[elementBlueprint.type].define(
|
||||
receiver[privateKey],
|
||||
index,
|
||||
elementBlueprint,
|
||||
);
|
||||
}
|
||||
receiver[privateKey][index] = value;
|
||||
}
|
||||
if (!wasCoalescing) {
|
||||
flushCoalesced();
|
||||
}
|
||||
},
|
||||
toJSON,
|
||||
};
|
||||
Object.defineProperties(
|
||||
receiver,
|
||||
{
|
||||
[key]: {
|
||||
configurable: true,
|
||||
get() {
|
||||
return api;
|
||||
},
|
||||
set(value) {
|
||||
this[privateKey].length = 0;
|
||||
// fast paths
|
||||
if (isPrimitiveType) {
|
||||
coalescing = false;
|
||||
const change = {};
|
||||
for (const elementKey in value) {
|
||||
this[privateKey][elementKey] = value[elementKey];
|
||||
change[elementKey] = value[elementKey];
|
||||
}
|
||||
if (receiver.markChange) {
|
||||
receiver[privateKey].markChange(change);
|
||||
}
|
||||
return;
|
||||
}
|
||||
coalescing = true;
|
||||
for (const elementKey in value) {
|
||||
this[key].setAt(elementKey, value[elementKey]);
|
||||
}
|
||||
flushCoalesced();
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
if ('defaultValue' in blueprint) {
|
||||
receiver[key] = blueprint.defaultValue;
|
||||
}
|
||||
else {
|
||||
flushCoalesced();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {expect, test} from 'vitest';
|
||||
import {expect, test, vi} from 'vitest';
|
||||
|
||||
import {Properties} from '../properties.js';
|
||||
import ArrayProperty from './array.js';
|
||||
|
@ -7,54 +7,139 @@ import NumberProperty from './number.js';
|
|||
|
||||
Properties.array = ArrayProperty;
|
||||
Properties.object = ObjectProperty;
|
||||
Properties.uint16 = NumberProperty;
|
||||
Properties.uint8 = NumberProperty;
|
||||
|
||||
test('array', () => {
|
||||
let changeIndex = 0;
|
||||
const receiver = Object.defineProperties(
|
||||
{
|
||||
markChange: (change) => {
|
||||
switch (changeIndex++) {
|
||||
case 0: {
|
||||
expect(change).to.deep.equal({layers: {0: {data: []}}});
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
expect(change).to.deep.equal({layers: {0: {data: {10: 1}}}});
|
||||
break;
|
||||
}
|
||||
}
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
ArrayProperty.define(receiver, 'property', {
|
||||
element: {type: 'uint8'},
|
||||
});
|
||||
receiver.property.setAt(0, 6);
|
||||
expect(markChange).toHaveBeenCalledWith({property: {0: 6}});
|
||||
});
|
||||
|
||||
test('defaults primitive', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
const defaultValue = [1, 2, 3];
|
||||
ArrayProperty.define(receiver, 'property', {
|
||||
defaultValue,
|
||||
element: {type: 'uint8'},
|
||||
});
|
||||
expect(markChange).toHaveBeenCalledWith({property: {0: 1, 1: 2, 2: 3}});
|
||||
receiver.property.setAt(3, 6);
|
||||
expect(markChange).toHaveBeenCalledWith({property: {3: 6}});
|
||||
});
|
||||
|
||||
test('defaults nesting', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
const defaultValue = [{x: 1}];
|
||||
ArrayProperty.define(receiver, 'property', {
|
||||
defaultValue,
|
||||
element: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
x: {type: 'uint8'},
|
||||
},
|
||||
},
|
||||
{
|
||||
$$: ObjectProperty.definitions(
|
||||
{
|
||||
properties: {
|
||||
layers: {
|
||||
element: {
|
||||
properties: {
|
||||
data: {
|
||||
element: {type: 'uint16'},
|
||||
type: 'array',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
type: 'array',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
receiver.$$ = {layers: [{data: []}]};
|
||||
const object = receiver.$$;
|
||||
object.layers.at(0).data.setAt(10, 1);
|
||||
expect(changeIndex).to.equal(2);
|
||||
const data = [];
|
||||
for (const i of object.layers.at(0).data) {
|
||||
data.push(i);
|
||||
}
|
||||
expect(data).to.deep.equal(object.layers.at(0).data.toJSON());
|
||||
});
|
||||
expect(markChange).toHaveBeenCalledWith({property: {0: {x: 1}}});
|
||||
receiver.property.at(0).x = 123;
|
||||
expect(markChange).toHaveBeenCalledWith({property: {0: {x: 123}}});
|
||||
});
|
||||
|
||||
test('toJSON primitve', () => {
|
||||
const receiver = {};
|
||||
ArrayProperty.define(receiver, 'property', {
|
||||
defaultValue: [1, 2, 3],
|
||||
element: {type: 'uint8'},
|
||||
});
|
||||
expect(receiver.property.toJSON()).to.deep.equal([1, 2, 3])
|
||||
});
|
||||
|
||||
test('toJSON nesting', () => {
|
||||
const receiver = {};
|
||||
const defaultValue = [{x: 1}];
|
||||
ArrayProperty.define(receiver, 'property', {
|
||||
defaultValue,
|
||||
element: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
x: {type: 'uint8'},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(receiver.property.toJSON()).to.deep.equal(defaultValue);
|
||||
});
|
||||
|
||||
test('iterate primitive', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const defaultValue = [1, 2, 3];
|
||||
ArrayProperty.define(receiver, 'property', {
|
||||
defaultValue,
|
||||
element: {type: 'uint8'},
|
||||
});
|
||||
const i = [];
|
||||
for (const element of receiver.property) {
|
||||
i.push(element);
|
||||
}
|
||||
expect(i).to.deep.equal(defaultValue);
|
||||
});
|
||||
|
||||
test('iterate nesting', () => {
|
||||
const receiver = {};
|
||||
const defaultValue = [{x: 1}];
|
||||
ArrayProperty.define(receiver, 'property', {
|
||||
defaultValue,
|
||||
element: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
x: {type: 'uint8'},
|
||||
},
|
||||
},
|
||||
});
|
||||
const i = [];
|
||||
for (const element of receiver.property) {
|
||||
i.push(element);
|
||||
}
|
||||
expect(i).to.deep.equal(defaultValue);
|
||||
});
|
||||
|
||||
test('merge', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
const defaultValue = [1, 2, 3];
|
||||
ArrayProperty.define(receiver, 'property', {
|
||||
defaultValue,
|
||||
element: {type: 'uint8'},
|
||||
});
|
||||
expect(markChange).toHaveBeenCalledWith({property: {0: 1, 1: 2, 2: 3}});
|
||||
receiver.property.merge({1: 5, 2: 10});
|
||||
expect(markChange).toHaveBeenCalledWith({property: {1: 5, 2: 10}});
|
||||
expect(receiver.property.toJSON()).deep.equal([1, 5, 10]);
|
||||
});
|
||||
|
||||
test('length', () => {
|
||||
const receiver = {};
|
||||
ArrayProperty.define(receiver, 'property', {
|
||||
defaultValue: [1, 2, 3],
|
||||
element: {type: 'uint8'},
|
||||
});
|
||||
expect(receiver.property.length).to.equal(3);
|
||||
receiver.property.setAt(2, 3);
|
||||
expect(receiver.property.length).to.equal(3);
|
||||
receiver.property.setAt(3, 4);
|
||||
expect(receiver.property.length).to.equal(4);
|
||||
});
|
||||
|
||||
test('full set', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
ArrayProperty.define(receiver, 'property', {
|
||||
element: {type: 'uint8'},
|
||||
});
|
||||
receiver.property = [1, 2, 3];
|
||||
expect(markChange).toHaveBeenCalledWith({property: {0: 1, 1: 2, 2: 3}});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {definitions} from '../properties.js'
|
||||
import {define} from '../properties.js'
|
||||
|
||||
const BoolProperty = {
|
||||
defaultValue(value, blueprint) {
|
||||
return value ?? blueprint.defaultValue ?? false;
|
||||
defaultValue() {
|
||||
return false;
|
||||
},
|
||||
definitions,
|
||||
define,
|
||||
};
|
||||
|
||||
export default BoolProperty;
|
||||
|
|
|
@ -1,109 +1,164 @@
|
|||
import {Properties} from '../properties.js';
|
||||
|
||||
const MapProperty = {
|
||||
defaultValue(value = [], blueprint) {
|
||||
const defaultValue = new Map();
|
||||
const elementValueBlueprint = blueprint.value;
|
||||
const ElementValueClass = Properties[elementValueBlueprint.type];
|
||||
for (const [key, mapValue] in value) {
|
||||
defaultValue.set(
|
||||
key,
|
||||
ElementValueClass.defaultValue(
|
||||
mapValue ?? blueprint.defaultValue?.[key],
|
||||
elementValueBlueprint,
|
||||
),
|
||||
);
|
||||
}
|
||||
return defaultValue;
|
||||
defaultValue() {
|
||||
return new Map();
|
||||
},
|
||||
definitions(blueprint, key) {
|
||||
define(receiver, key, blueprint) {
|
||||
const {defaultValue} = blueprint;
|
||||
const privateKey = `$$${key}`;
|
||||
const privateMapKey = `$$map$$${key}`;
|
||||
return {
|
||||
get: function() {
|
||||
return this[privateKey];
|
||||
let fakeKeyCaret = 0;
|
||||
const fakeKeys = new Map();
|
||||
const reversedFakeKeys = new Map();
|
||||
let coalescing = true;
|
||||
let coalesced;
|
||||
const flushCoalesced = () => {
|
||||
if (coalesced) {
|
||||
if (receiver.markChange) {
|
||||
receiver.markChange({[key]: coalesced});
|
||||
}
|
||||
coalesced = undefined;
|
||||
}
|
||||
coalescing = false;
|
||||
}
|
||||
receiver[privateKey] = {};
|
||||
const api = {
|
||||
[Symbol.iterator]: () => {
|
||||
const protocol = fakeKeys.entries();
|
||||
return {
|
||||
next: () => {
|
||||
let result = protocol.next();
|
||||
if (result.done) {
|
||||
return {done: true};
|
||||
}
|
||||
const [key, fakeKey] = result.value;
|
||||
return {done: false, value: [key, receiver[privateKey][fakeKey]]};
|
||||
},
|
||||
};
|
||||
},
|
||||
set: function(value) {
|
||||
if (this[privateMapKey] !== value) {
|
||||
const elementValueBlueprint = blueprint.value;
|
||||
const ElementValueClass = Properties[elementValueBlueprint.type];
|
||||
this[privateMapKey] = new Map(value);
|
||||
this[privateKey] = Object.defineProperties({}, {
|
||||
[Symbol.iterator]: {
|
||||
value: () => {
|
||||
const protocol = this[privateMapKey].entries();
|
||||
return {
|
||||
next: () => {
|
||||
let result = protocol.next();
|
||||
if (result.done) {
|
||||
return {done: true};
|
||||
}
|
||||
const [key, mapValue] = result.value;
|
||||
return {done: false, value: [key, mapValue?.proxy]};
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
get: {
|
||||
value: (index) => this[privateMapKey].get(index).proxy,
|
||||
},
|
||||
set: {
|
||||
value: (mapKey, value) => {
|
||||
let suppress = true;
|
||||
this[privateMapKey].set(
|
||||
mapKey,
|
||||
Object.defineProperties(
|
||||
{
|
||||
markChange: (change) => {
|
||||
if (!suppress) {
|
||||
if (this.markChange) {
|
||||
if (key) {
|
||||
this.markChange({[key]: change});
|
||||
}
|
||||
else {
|
||||
this.markChange(change);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
proxy: ElementValueClass.definitions(
|
||||
elementValueBlueprint,
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
this[privateMapKey].get(mapKey).proxy = value;
|
||||
if (this.markChange) {
|
||||
if (key) {
|
||||
this.markChange({[key]: {[mapKey]: value}});
|
||||
}
|
||||
else {
|
||||
this.markChange({[mapKey]: value});
|
||||
}
|
||||
}
|
||||
suppress = false;
|
||||
},
|
||||
},
|
||||
toJSON: {
|
||||
value: () => {
|
||||
return Array.from(this[privateKey]);
|
||||
},
|
||||
},
|
||||
});
|
||||
for (const [key, mapValue] of value) {
|
||||
this[privateKey].set(
|
||||
key,
|
||||
ElementValueClass.defaultValue(
|
||||
mapValue ?? blueprint.defaultValue?.[key],
|
||||
elementValueBlueprint,
|
||||
),
|
||||
);
|
||||
}
|
||||
clear() {
|
||||
let wasCoalescing = coalescing;
|
||||
coalescing = true;
|
||||
for (const [key] of fakeKeys) {
|
||||
this.delete(key);
|
||||
}
|
||||
if (!wasCoalescing) {
|
||||
flushCoalesced();
|
||||
}
|
||||
},
|
||||
delete(mapKey) {
|
||||
const fakeKey = fakeKeys.get(mapKey);
|
||||
delete receiver[privateKey][fakeKey];
|
||||
if (receiver[privateKey].markChange) {
|
||||
receiver[privateKey].markChange({[fakeKey]: undefined});
|
||||
}
|
||||
fakeKeys.delete(mapKey);
|
||||
reversedFakeKeys.delete(`${fakeKey}`);
|
||||
},
|
||||
get(key) {
|
||||
return receiver[privateKey][fakeKeys.get(key)];
|
||||
},
|
||||
has(key) {
|
||||
return fakeKeys.has(key);
|
||||
},
|
||||
merge(value) {
|
||||
coalescing = true;
|
||||
for (const propertyKey in value) {
|
||||
const parsedKey = !isNaN(propertyKey) ? parseFloat(propertyKey) : propertyKey;
|
||||
if (undefined === value[propertyKey]) {
|
||||
this.delete(parsedKey);
|
||||
}
|
||||
else {
|
||||
this.set(parsedKey, value[propertyKey]);
|
||||
}
|
||||
}
|
||||
flushCoalesced();
|
||||
},
|
||||
set(key, value) {
|
||||
let wasCoalescing = coalescing;
|
||||
coalescing = true;
|
||||
if (!fakeKeys.has(key)) {
|
||||
fakeKeys.set(key, fakeKeyCaret);
|
||||
reversedFakeKeys.set(`${fakeKeyCaret}`, key);
|
||||
fakeKeyCaret += 1;
|
||||
}
|
||||
const fakeKey = fakeKeys.get(key);
|
||||
if (!(fakeKeys.get(key) in receiver[privateKey])) {
|
||||
Properties[blueprint.value.type].define(
|
||||
receiver[privateKey],
|
||||
fakeKey,
|
||||
blueprint.value,
|
||||
);
|
||||
}
|
||||
receiver[privateKey][fakeKey] = value;
|
||||
if (!wasCoalescing) {
|
||||
flushCoalesced();
|
||||
}
|
||||
},
|
||||
get size() {
|
||||
return fakeKeys.size;
|
||||
},
|
||||
toJSON() {
|
||||
const json = [];
|
||||
for (const [fakeKey, key] of reversedFakeKeys) {
|
||||
let value;
|
||||
if ('object' === typeof receiver[privateKey][fakeKey]) {
|
||||
value = receiver[privateKey][fakeKey].toJSON();
|
||||
}
|
||||
else {
|
||||
value = receiver[privateKey][fakeKey];
|
||||
}
|
||||
json.push([key, value]);
|
||||
}
|
||||
return json;
|
||||
},
|
||||
};
|
||||
if (receiver.markChange) {
|
||||
receiver[privateKey].markChange = (change) => {
|
||||
const translatedChange = {};
|
||||
for (const key in change) {
|
||||
translatedChange[reversedFakeKeys.get(key)] = change[key];
|
||||
}
|
||||
if (!coalescing) {
|
||||
if (receiver.markChange) {
|
||||
receiver.markChange({[key]: translatedChange});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!coalesced) {
|
||||
coalesced = {};
|
||||
}
|
||||
coalesced = {
|
||||
...coalesced,
|
||||
...translatedChange,
|
||||
};
|
||||
};
|
||||
}
|
||||
Object.defineProperties(
|
||||
receiver,
|
||||
{
|
||||
[key]: {
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return api;
|
||||
},
|
||||
set: function(iterable) {
|
||||
coalescing = true;
|
||||
api.clear();
|
||||
for (const [key, value] of iterable) {
|
||||
api.set(key, value);
|
||||
}
|
||||
flushCoalesced();
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
if (defaultValue) {
|
||||
receiver[key] = defaultValue;
|
||||
}
|
||||
else {
|
||||
flushCoalesced();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,46 +1,122 @@
|
|||
import {expect, test} from 'vitest';
|
||||
import {expect, test, vi} from 'vitest';
|
||||
|
||||
import {Properties} from '../properties.js';
|
||||
import MapProperty from './map.js';
|
||||
import NumberProperty from './number.js';
|
||||
import ObjectProperty from './object.js';
|
||||
|
||||
Properties.map = MapProperty;
|
||||
Properties.object = ObjectProperty;
|
||||
Properties.uint16 = NumberProperty;
|
||||
|
||||
test('map', () => {
|
||||
let changeIndex = 0;
|
||||
const receiver = Object.defineProperties(
|
||||
{
|
||||
markChange: (change) => {
|
||||
switch (changeIndex++) {
|
||||
case 0: {
|
||||
expect(change).to.deep.equal({0: 1});
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
expect(change).to.deep.equal({1: 2});
|
||||
break;
|
||||
}
|
||||
}
|
||||
test('map api', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
MapProperty.define(receiver, 'property', {
|
||||
key: {type: 'uint16'},
|
||||
value: {type: 'uint16'},
|
||||
});
|
||||
expect(receiver.property.has(1)).to.be.false;
|
||||
expect(receiver.property.size).to.equal(0);
|
||||
receiver.property.set(1, 2);
|
||||
expect(markChange).toHaveBeenCalledWith({property: {1: 2}});
|
||||
expect(receiver.property.size).to.equal(1);
|
||||
receiver.property.set(2, 3);
|
||||
expect(markChange).toHaveBeenCalledWith({property: {2: 3}});
|
||||
expect(receiver.property.size).to.equal(2);
|
||||
receiver.property.set(3, 4);
|
||||
expect(markChange).toHaveBeenCalledWith({property: {3: 4}});
|
||||
expect(receiver.property.size).to.equal(3);
|
||||
expect(receiver.property.has(1)).to.be.true;
|
||||
receiver.property.delete(1);
|
||||
expect(markChange).toHaveBeenCalledWith({property: {1: undefined}});
|
||||
expect(receiver.property.size).to.equal(2);
|
||||
expect(receiver.property.has(1)).to.be.false;
|
||||
receiver.property.clear();
|
||||
expect(markChange).toHaveBeenCalledWith({property: {2: undefined, 3: undefined}});
|
||||
expect(receiver.property.size).to.equal(0);
|
||||
});
|
||||
|
||||
test('iterate', () => {
|
||||
const receiver = {};
|
||||
const defaultValue = [[1, 2], [3, 4]];
|
||||
MapProperty.define(receiver, 'property', {
|
||||
defaultValue,
|
||||
key: {type: 'uint16'},
|
||||
value: {type: 'uint16'},
|
||||
});
|
||||
const i = [];
|
||||
for (const tuple of receiver.property) {
|
||||
i.push(tuple);
|
||||
}
|
||||
expect(i).to.deep.equal(defaultValue);
|
||||
});
|
||||
|
||||
test('primitive map default', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
const defaultValue = new Map([[1, 2], [3, 4]]);
|
||||
MapProperty.define(receiver, 'property', {
|
||||
defaultValue,
|
||||
key: {type: 'uint16'},
|
||||
value: {type: 'uint16'},
|
||||
});
|
||||
expect(markChange).toHaveBeenCalledWith({property: {1: 2, 3: 4}});
|
||||
});
|
||||
|
||||
test('iterable default', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
const defaultValue = [[1, 2], [3, 4]];
|
||||
MapProperty.define(receiver, 'property', {
|
||||
defaultValue,
|
||||
key: {type: 'uint16'},
|
||||
value: {type: 'uint16'},
|
||||
});
|
||||
expect(markChange).toHaveBeenCalledWith({property: {1: 2, 3: 4}});
|
||||
});
|
||||
|
||||
test('nested default', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
const defaultValue = new Map([[1, {x: 2}], [2, {x: 3}]])
|
||||
MapProperty.define(receiver, 'property', {
|
||||
defaultValue,
|
||||
key: {type: 'uint16'},
|
||||
value: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
x: {type: 'uint16'},
|
||||
},
|
||||
},
|
||||
{
|
||||
$$: MapProperty.definitions(
|
||||
{
|
||||
key: {type: 'uint16'},
|
||||
value: {type: 'uint16'},
|
||||
type: 'map',
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
receiver.$$ = new Map([[0, 1]]);
|
||||
const map = receiver.$$;
|
||||
map.set(1, 2);
|
||||
expect(changeIndex).to.equal(2);
|
||||
const data = [];
|
||||
for (const i of map) {
|
||||
data.push(i);
|
||||
}
|
||||
expect(data).to.deep.equal(map.toJSON());
|
||||
});
|
||||
expect(markChange).toHaveBeenCalledWith({property: {1: {x: 2}, 2: {x: 3}}});
|
||||
receiver.property.get(1).x = 123;
|
||||
expect(markChange).toHaveBeenCalledWith({property: {1: {x: 123}}});
|
||||
});
|
||||
|
||||
test('merge', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const defaultValue = new Map([[1, 2], [3, 4]]);
|
||||
MapProperty.define(receiver, 'property', {
|
||||
defaultValue,
|
||||
key: {type: 'uint16'},
|
||||
value: {type: 'uint16'},
|
||||
});
|
||||
receiver.property.merge({5: 6, 7: 8});
|
||||
expect(receiver.property.toJSON()).to.deep.equal([[1, 2], [3, 4], [5, 6], [7, 8]])
|
||||
receiver.property.merge({1: undefined, 3: undefined});
|
||||
expect(receiver.property.toJSON()).to.deep.equal([[5, 6], [7, 8]])
|
||||
});
|
||||
|
||||
test('full set', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const defaultValue = new Map([[1, 2], [3, 4]]);
|
||||
MapProperty.define(receiver, 'property', {
|
||||
defaultValue,
|
||||
key: {type: 'uint16'},
|
||||
value: {type: 'uint16'},
|
||||
});
|
||||
receiver.property = new Map([[5, 6], [7, 8]])
|
||||
expect(receiver.property.toJSON()).to.deep.equal([[5, 6], [7, 8]])
|
||||
});
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {definitions} from '../properties.js'
|
||||
import {define} from '../properties.js'
|
||||
|
||||
const NumberProperty = {
|
||||
defaultValue(value, blueprint) {
|
||||
return value ?? blueprint.defaultValue ?? 0;
|
||||
defaultValue() {
|
||||
return 0;
|
||||
},
|
||||
definitions,
|
||||
define,
|
||||
};
|
||||
|
||||
export default NumberProperty;
|
||||
|
|
|
@ -1,99 +1,101 @@
|
|||
import {Properties} from '../properties.js';
|
||||
|
||||
const ObjectProperty = {
|
||||
defaultValue(value, blueprint) {
|
||||
const defaultValue = {};
|
||||
for (const key in blueprint.properties) {
|
||||
const propertyBlueprint = blueprint.properties[key];
|
||||
const PropertyClass = Properties[propertyBlueprint.type];
|
||||
defaultValue[key] = PropertyClass.defaultValue(
|
||||
value?.[key] ?? blueprint.defaultValue?.[key],
|
||||
defaultValue() {
|
||||
return {};
|
||||
},
|
||||
define(receiver, key, blueprint) {
|
||||
const {defaultValue} = blueprint;
|
||||
const privateKey = `$$${key}`;
|
||||
let coalescing = true;
|
||||
let coalesced;
|
||||
const flushCoalesced = () => {
|
||||
if (coalesced) {
|
||||
if (receiver.markChange) {
|
||||
receiver.markChange({[key]: coalesced});
|
||||
}
|
||||
coalesced = undefined;
|
||||
}
|
||||
coalescing = false;
|
||||
}
|
||||
receiver[privateKey] = {
|
||||
merge(value) {
|
||||
coalescing = true;
|
||||
for (const propertyKey in value) {
|
||||
receiver[key][propertyKey] = value[propertyKey]
|
||||
}
|
||||
flushCoalesced();
|
||||
},
|
||||
toJSON() {
|
||||
const json = {};
|
||||
for (const key in blueprint.properties) {
|
||||
if ('object' === typeof this[key]) {
|
||||
json[key] = this[key].toJSON();
|
||||
}
|
||||
else {
|
||||
json[key] = this[key];
|
||||
}
|
||||
}
|
||||
return json;
|
||||
},
|
||||
};
|
||||
if (receiver.markChange) {
|
||||
receiver[privateKey].markChange = (change) => {
|
||||
if (!coalescing) {
|
||||
if (receiver.markChange) {
|
||||
receiver.markChange({[key]: change});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!coalesced) {
|
||||
coalesced = {};
|
||||
}
|
||||
coalesced = {
|
||||
...coalesced,
|
||||
...change,
|
||||
};
|
||||
};
|
||||
}
|
||||
for (const propertyKey in blueprint.properties) {
|
||||
const propertyBlueprint = blueprint.properties[propertyKey];
|
||||
Properties[propertyBlueprint.type].define(
|
||||
receiver[privateKey],
|
||||
propertyKey,
|
||||
propertyBlueprint,
|
||||
);
|
||||
}
|
||||
return defaultValue;
|
||||
},
|
||||
definitions(blueprint, key) {
|
||||
const privateKey = `$$${key}`;
|
||||
return {
|
||||
get: function() {
|
||||
return this[privateKey];
|
||||
},
|
||||
set: function(value) {
|
||||
if (this[privateKey] !== value) {
|
||||
let coalescing = true;
|
||||
let coalesced;
|
||||
const properties = {};
|
||||
for (const propertyKey in blueprint.properties) {
|
||||
const propertyBlueprint = blueprint.properties[propertyKey];
|
||||
properties[propertyKey] = Properties[propertyBlueprint.type].definitions(
|
||||
propertyBlueprint,
|
||||
propertyKey,
|
||||
);
|
||||
}
|
||||
this[privateKey] = Object.defineProperties(
|
||||
(this.markChange && {
|
||||
markChange: (change) => {
|
||||
if (!coalescing) {
|
||||
if (this.markChange) {
|
||||
if (key) {
|
||||
this.markChange({[key]: change});
|
||||
}
|
||||
else {
|
||||
this.markChange(change);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!coalesced) {
|
||||
coalesced = {};
|
||||
}
|
||||
coalesced = {
|
||||
...coalesced,
|
||||
...change,
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
...properties,
|
||||
toJSON: {
|
||||
value: () => {
|
||||
const json = {};
|
||||
for (const key in properties) {
|
||||
if ('object' === typeof this[privateKey][key]) {
|
||||
json[key] = this[privateKey][key].toJSON();
|
||||
}
|
||||
else {
|
||||
json[key] = this[privateKey][key];
|
||||
}
|
||||
}
|
||||
return json;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
for (const key in properties) {
|
||||
const propertyBlueprint = blueprint.properties[key];
|
||||
this[privateKey][key] = Properties[propertyBlueprint.type].defaultValue(
|
||||
value?.[key] ?? blueprint.defaultValue?.[key],
|
||||
propertyBlueprint,
|
||||
);
|
||||
}
|
||||
if (coalesced) {
|
||||
if (this.markChange) {
|
||||
if (key) {
|
||||
this.markChange({[key]: coalesced});
|
||||
Object.defineProperties(
|
||||
receiver,
|
||||
{
|
||||
[key]: {
|
||||
get: () => {
|
||||
return receiver[privateKey];
|
||||
},
|
||||
set: (value) => {
|
||||
coalescing = true;
|
||||
for (const propertyKey in blueprint.properties) {
|
||||
const propertyBlueprint = blueprint.properties[propertyKey];
|
||||
if (propertyKey in value) {
|
||||
receiver[key][propertyKey] = value[propertyKey];
|
||||
}
|
||||
else if ('defaultValue' in propertyBlueprint) {
|
||||
receiver[key][propertyKey] = propertyBlueprint.defaultValue;
|
||||
}
|
||||
else {
|
||||
this.markChange(coalesced);
|
||||
receiver[key][propertyKey] = Properties[propertyBlueprint.type].defaultValue();
|
||||
}
|
||||
}
|
||||
coalesced = undefined;
|
||||
}
|
||||
coalescing = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
flushCoalesced();
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
if (defaultValue) {
|
||||
receiver[key] = defaultValue;
|
||||
}
|
||||
else {
|
||||
flushCoalesced();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {expect, test} from 'vitest';
|
||||
import {expect, test, vi} from 'vitest';
|
||||
|
||||
import {Properties} from '../properties.js';
|
||||
import ObjectProperty from './object.js';
|
||||
|
@ -7,63 +7,91 @@ import StringProperty from './string.js';
|
|||
Properties.object = ObjectProperty;
|
||||
Properties.string = StringProperty;
|
||||
|
||||
test('object', () => {
|
||||
const defaultValue = {x: {y: '2', z: 'asd'}};
|
||||
let changeIndex = 0;
|
||||
const receiver = Object.defineProperties(
|
||||
{
|
||||
markChange(change) {
|
||||
switch (changeIndex++) {
|
||||
case 0: {
|
||||
expect(change).to.deep.equal({x: {y: '2', z: 'asd'}});
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
expect(change).to.deep.equal({x: {y: '123'}});
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
expect(change).to.deep.equal({x: {y: '', z: 'zed'}});
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
expect(change).to.deep.equal({x: {y: '12'}});
|
||||
break;
|
||||
}
|
||||
}
|
||||
test('property defaults', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
ObjectProperty.define(receiver, 'property', {
|
||||
properties: {
|
||||
x: {defaultValue: 'zed', type: 'string'},
|
||||
y: {
|
||||
properties: {
|
||||
z: {defaultValue: 'sdf', type: 'string'},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
$$: ObjectProperty.definitions(
|
||||
{
|
||||
defaultValue,
|
||||
properties: {
|
||||
x: {
|
||||
properties: {
|
||||
y: {type: 'string'},
|
||||
z: {defaultValue: 'zed', type: 'string'},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
receiver.$$ = {};
|
||||
const object = receiver.$$;
|
||||
expect(object.x.y).to.equal(defaultValue.x.y);
|
||||
expect(object.x.z).to.equal(defaultValue.x.z);
|
||||
object.x.y = '123';
|
||||
expect(object.x.y).to.equal('123');
|
||||
expect(object.x.z).to.equal(defaultValue.x.z);
|
||||
object.x = {};
|
||||
expect(object.x.y).to.equal('');
|
||||
expect(object.x.z).to.equal('zed');
|
||||
object.x.y = '12';
|
||||
expect(object.x.y).to.equal('12');
|
||||
expect(object.x.z).to.equal('zed');
|
||||
expect(changeIndex).to.equal(4);
|
||||
expect(object.toJSON()).to.deep.equal({x: {y: '12', z: 'zed'}});
|
||||
});
|
||||
expect(markChange).toHaveBeenCalledWith({property: {x: 'zed', y: {z: 'sdf'}}});
|
||||
expect(receiver.property.x).to.equal('zed');
|
||||
receiver.property.y.z = 'asd';
|
||||
expect(markChange).toHaveBeenCalledWith({property: {y: {z: 'asd'}}});
|
||||
});
|
||||
|
||||
test('blueprint defaults', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
ObjectProperty.define(receiver, 'property', {
|
||||
defaultValue: {x: 'foo'},
|
||||
properties: {
|
||||
x: {defaultValue: 'zed', type: 'string'},
|
||||
y: {defaultValue: 'moo', type: 'string'},
|
||||
},
|
||||
});
|
||||
expect(markChange).toHaveBeenCalledWith({property: {x: 'foo', y: 'moo'}});
|
||||
expect(receiver.property.x).to.equal('foo');
|
||||
});
|
||||
|
||||
test('set', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
ObjectProperty.define(receiver, 'property', {
|
||||
properties: {
|
||||
x: {defaultValue: 'zed', type: 'string'},
|
||||
y: {type: 'string'},
|
||||
},
|
||||
});
|
||||
expect(markChange).toHaveBeenCalledWith({property: {x: 'zed', y: ''}});
|
||||
receiver.property.x = 'nope';
|
||||
expect(markChange).toHaveBeenCalledWith({property: {x: 'nope'}});
|
||||
receiver.property = {y: 'foo'};
|
||||
expect(markChange).toHaveBeenCalledWith({property: {x: 'zed', y: 'foo'}});
|
||||
expect(receiver.property.x).to.equal('zed');
|
||||
expect(receiver.property.y).to.equal('foo');
|
||||
});
|
||||
|
||||
test('merge', () => {
|
||||
const receiver = {markChange: () => {}};
|
||||
const markChange = vi.spyOn(receiver, 'markChange');
|
||||
ObjectProperty.define(receiver, 'property', {
|
||||
properties: {
|
||||
x: {defaultValue: 'zed', type: 'string'},
|
||||
y: {type: 'string'},
|
||||
z: {type: 'string'},
|
||||
},
|
||||
});
|
||||
expect(markChange).toHaveBeenCalledWith({property: {x: 'zed', y: '', z: ''}});
|
||||
receiver.property.x = 'nope';
|
||||
expect(markChange).toHaveBeenCalledWith({property: {x: 'nope'}});
|
||||
receiver.property.merge({y: 'foo', z: 'bar'});
|
||||
expect(markChange).toHaveBeenCalledWith({property: {y: 'foo', z: 'bar'}});
|
||||
expect(receiver.property.x).to.equal('nope');
|
||||
expect(receiver.property.y).to.equal('foo');
|
||||
expect(receiver.property.z).to.equal('bar');
|
||||
});
|
||||
|
||||
test('toJSON', () => {
|
||||
const receiver = {};
|
||||
ObjectProperty.define(receiver, 'property', {
|
||||
properties: {
|
||||
x: {
|
||||
properties: {
|
||||
y: {type: 'string'},
|
||||
z: {defaultValue: 'zed', type: 'string'},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
});
|
||||
receiver.property.x.y = 'asd';
|
||||
expect(receiver.property.toJSON()).to.deep.equal({x: {y: 'asd', z: 'zed'}});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {definitions} from '../properties.js'
|
||||
import {define} from '../properties.js'
|
||||
|
||||
const StringProperty = {
|
||||
defaultValue(value, blueprint) {
|
||||
return value ?? blueprint.defaultValue ?? '';
|
||||
defaultValue() {
|
||||
return '';
|
||||
},
|
||||
definitions,
|
||||
define,
|
||||
};
|
||||
|
||||
export default StringProperty;
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import ArrayProperty from './properties/array.js';
|
||||
import NumberProperty from './properties/number.js';
|
||||
import ObjectProperty from './properties/object.js';
|
||||
import StringProperty from './properties/string.js';
|
||||
import Entity from './entity.js';
|
||||
import {Properties} from './properties.js';
|
||||
import Component from './component.js';
|
||||
|
||||
Properties.array = ArrayProperty;
|
||||
Properties.object = ObjectProperty;
|
||||
Properties.float32 = NumberProperty;
|
||||
Properties.float64 = NumberProperty;
|
||||
Properties.uint16 = NumberProperty;
|
||||
Properties.int32 = NumberProperty;
|
||||
Properties.string = StringProperty;
|
||||
|
||||
export function wrapComponents(Components) {
|
||||
return Components
|
||||
|
@ -28,12 +33,14 @@ export function fakeEnvironment() {
|
|||
['B', {b: {type: 'int32', defaultValue: 32}}],
|
||||
['C', {c: {type: 'int32'}}],
|
||||
['D', {d: {type: 'float64'}, e: {type: 'float64'}}],
|
||||
['E', {e: {type: 'object', properties: {f: {type: 'int32'}, g: {type: 'int32'}}}}],
|
||||
]);
|
||||
const fakeEcs = {markChange() {}};
|
||||
const A = new Components.A(fakeEcs);
|
||||
const B = new Components.B(fakeEcs);
|
||||
const C = new Components.C(fakeEcs);
|
||||
const D = new Components.D(fakeEcs);
|
||||
const E = new Components.E(fakeEcs);
|
||||
const one = new Entity(1);
|
||||
one.addComponent(B);
|
||||
const two = new Entity(2);
|
||||
|
@ -42,6 +49,6 @@ export function fakeEnvironment() {
|
|||
two.addComponent(C);
|
||||
const three = new Entity(3);
|
||||
three.addComponent(A);
|
||||
return {A, B, C, D, Components, one, two, three};
|
||||
return {A, B, C, D, E, Components, one, two, three};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user