@woosh/meep-engine 2.84.9 → 2.84.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/build/meep.cjs +185 -87
- package/build/meep.min.js +1 -1
- package/build/meep.module.js +185 -87
- package/editor/process/symbolic/makePositionedIconDisplaySymbol.js +2 -4
- package/editor/view/EditorView.js +48 -204
- package/editor/view/ecs/HierarchicalEntityListView.js +191 -0
- package/editor/view/ecs/HierarchicalEntityListView.module.scss +13 -0
- package/editor/view/prepareMeshLibrary.js +178 -0
- package/editor/view/v2/SplitView.js +104 -0
- package/editor/view/v2/ViewManagementSystem.js +0 -0
- package/editor/view/v2/prototypeEditor.js +127 -0
- package/package.json +1 -1
- package/src/core/cache/Cache.d.ts +2 -0
- package/src/core/cache/Cache.js +58 -8
- package/src/core/cache/Cache.spec.js +38 -0
- package/src/core/cache/CacheElement.js +6 -0
- package/src/core/cache/LoadingCache.js +25 -2
- package/src/core/cache/LoadingCache.spec.js +9 -6
- package/src/core/collection/array/arraySetSortingDiff.js +6 -6
- package/src/core/collection/table/RowFirstTable.js +364 -368
- package/src/core/geom/3d/plane/plane3_compute_ray_intersection.js +3 -1
- package/src/core/geom/3d/topology/simplify/prototypeMeshSimplification.js +7 -7
- package/src/engine/animation/curve/ecd_bind_animation_curve.js +9 -0
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometryFlags.js +8 -1
- package/src/engine/graphics/ecs/mesh-v2/aggregate/prototypeSGMesh.js +23 -19
- package/src/engine/graphics/ecs/mesh-v2/sg_hierarchy_compute_bounding_box_via_parent_entity.js +2 -2
- package/src/engine/graphics/ecs/mesh-v2/three_object_to_entity_composition.js +3 -1
- package/src/view/View.js +64 -95
- package/src/view/setElementTransform.js +20 -0
- package/src/view/setElementVisibility.js +15 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { SurfacePoint3 } from "../../src/core/geom/3d/SurfacePoint3.js";
|
|
2
|
+
import Vector2 from "../../src/core/geom/Vector2.js";
|
|
3
|
+
import Vector3 from "../../src/core/geom/Vector3.js";
|
|
4
|
+
import { obtainTerrain } from "../../src/engine/ecs/terrain/util/obtainTerrain.js";
|
|
5
|
+
import { Transform } from "../../src/engine/ecs/transform/Transform.js";
|
|
6
|
+
import Mesh from "../../src/engine/graphics/ecs/mesh/Mesh.js";
|
|
7
|
+
import { MeshEvents } from "../../src/engine/graphics/ecs/mesh/MeshEvents.js";
|
|
8
|
+
import { make_ray_from_viewport_position } from "../../src/engine/graphics/make_ray_from_viewport_position.js";
|
|
9
|
+
import BottomLeftResizeHandleView from "../../src/view/elements/BottomLeftResizeHandleView.js";
|
|
10
|
+
import ComponentAddAction from "../actions/concrete/ComponentAddAction.js";
|
|
11
|
+
import EntityCreateAction from "../actions/concrete/EntityCreateAction.js";
|
|
12
|
+
import SelectionAddAction from "../actions/concrete/SelectionAddAction.js";
|
|
13
|
+
import SelectionClearAction from "../actions/concrete/SelectionClearAction.js";
|
|
14
|
+
import MeshLibraryView from "./library/MeshLibraryView.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* @param {Editor} editor
|
|
19
|
+
*/
|
|
20
|
+
export function prepareMeshLibrary(editor) {
|
|
21
|
+
let resolveEngine;
|
|
22
|
+
|
|
23
|
+
const pEngine = new Promise(function (resolve, reject) {
|
|
24
|
+
resolveEngine = resolve;
|
|
25
|
+
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
* @type {Promise<GraphicsEngine>}
|
|
31
|
+
*/
|
|
32
|
+
const pGraphicsEngine = pEngine.then(function (e) {
|
|
33
|
+
return e.graphics;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const pRenderer = pGraphicsEngine.then(function (graphicsEngine) {
|
|
37
|
+
return graphicsEngine.renderer;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const pAssetManager = pEngine.then(e => e.assetManager);
|
|
41
|
+
|
|
42
|
+
const meshLibraryView = new MeshLibraryView(
|
|
43
|
+
editor.meshLibrary,
|
|
44
|
+
pAssetManager,
|
|
45
|
+
pRenderer
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
function handleDropEvent(event) {
|
|
49
|
+
event.stopPropagation();
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
|
|
52
|
+
const dataText = event.dataTransfer.getData('text/json');
|
|
53
|
+
if (dataText === "") {
|
|
54
|
+
//no data
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const data = JSON.parse(dataText);
|
|
59
|
+
|
|
60
|
+
const type = data.type;
|
|
61
|
+
|
|
62
|
+
if (type !== "Mesh") {
|
|
63
|
+
//wrong type
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const url = data.url;
|
|
68
|
+
|
|
69
|
+
const engine = editor.engine;
|
|
70
|
+
const graphics = engine.graphics;
|
|
71
|
+
|
|
72
|
+
const position = new Vector2(event.clientX, event.clientY);
|
|
73
|
+
|
|
74
|
+
graphics.viewport.positionGlobalToLocal(position, position);
|
|
75
|
+
|
|
76
|
+
const normalizedPosition = new Vector2();
|
|
77
|
+
|
|
78
|
+
//compute world position for drop
|
|
79
|
+
const ray = make_ray_from_viewport_position(engine, position);
|
|
80
|
+
|
|
81
|
+
const source = ray.origin;
|
|
82
|
+
const direction = ray.direction;
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
const entityManager = engine.entityManager;
|
|
86
|
+
const ecd = entityManager.dataset;
|
|
87
|
+
|
|
88
|
+
const terrain = obtainTerrain(ecd);
|
|
89
|
+
|
|
90
|
+
const worldPosition = new Vector3();
|
|
91
|
+
|
|
92
|
+
const mesh = new Mesh();
|
|
93
|
+
|
|
94
|
+
mesh.castShadow = true;
|
|
95
|
+
mesh.receiveShadow = true;
|
|
96
|
+
|
|
97
|
+
const transform = new Transform();
|
|
98
|
+
|
|
99
|
+
const actions = editor.actions;
|
|
100
|
+
actions.mark('New Mesh Placed from Library');
|
|
101
|
+
const entityCreateAction = new EntityCreateAction();
|
|
102
|
+
actions.do(entityCreateAction);
|
|
103
|
+
|
|
104
|
+
const sp3 = new SurfacePoint3();
|
|
105
|
+
|
|
106
|
+
const hit_found = terrain.raycastFirstSync(sp3, source.x, source.y, source.z, direction.x, direction.y, direction.z);
|
|
107
|
+
|
|
108
|
+
function handleMeshSetEvent() {
|
|
109
|
+
const bb = mesh.boundingBox;
|
|
110
|
+
|
|
111
|
+
const c0 = new Vector3(bb.x0, bb.y0, bb.z0);
|
|
112
|
+
const c1 = new Vector3(bb.x1, bb.y1, bb.z1);
|
|
113
|
+
|
|
114
|
+
const diagonal = c0.distanceTo(c1);
|
|
115
|
+
|
|
116
|
+
const offset = direction.clone().multiplyScalar(diagonal);
|
|
117
|
+
|
|
118
|
+
transform.position.add(offset);
|
|
119
|
+
|
|
120
|
+
//remove listener
|
|
121
|
+
ecd.removeEntityEventListener(entityCreateAction.entity, MeshEvents.DataSet, handleMeshSetEvent);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (hit_found) {
|
|
125
|
+
//got a terrain ray hit, set world placement position to that point
|
|
126
|
+
worldPosition.copy(sp3.position);
|
|
127
|
+
} else {
|
|
128
|
+
//set position to the source of the ray pick if there's nothing else available
|
|
129
|
+
worldPosition.copy(source);
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
//wait for mesh to load
|
|
133
|
+
ecd.addEntityEventListener(entityCreateAction.entity, MeshEvents.DataSet, handleMeshSetEvent);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
transform.position.copy(worldPosition);
|
|
137
|
+
|
|
138
|
+
mesh.url = url;
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
actions.doMany([
|
|
142
|
+
new ComponentAddAction(entityCreateAction.entity, transform),
|
|
143
|
+
new ComponentAddAction(entityCreateAction.entity, mesh),
|
|
144
|
+
//automatically select newly placed object
|
|
145
|
+
new SelectionClearAction(),
|
|
146
|
+
new SelectionAddAction([entityCreateAction.entity])
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function handleDragOverEvent(event) {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
meshLibraryView.on.linked.add(function () {
|
|
157
|
+
resolveEngine(editor.engine);
|
|
158
|
+
|
|
159
|
+
const viewport = editor.engine.graphics.viewport;
|
|
160
|
+
|
|
161
|
+
viewport.el.addEventListener('drop', handleDropEvent);
|
|
162
|
+
viewport.el.addEventListener('dragover', handleDragOverEvent);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
meshLibraryView.on.unlinked.add(function () {
|
|
166
|
+
const viewport = editor.engine.graphics.viewport;
|
|
167
|
+
|
|
168
|
+
viewport.el.removeEventListener('drop', handleDropEvent);
|
|
169
|
+
viewport.el.removeEventListener('dragover', handleDragOverEvent);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
meshLibraryView.size.set(400, 400);
|
|
173
|
+
|
|
174
|
+
const resizeHandleView = new BottomLeftResizeHandleView(meshLibraryView);
|
|
175
|
+
meshLibraryView.addChild(resizeHandleView);
|
|
176
|
+
|
|
177
|
+
return meshLibraryView;
|
|
178
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { clamp01 } from "../../../src/core/math/clamp01.js";
|
|
2
|
+
import { CSS_ABSOLUTE_POSITIONING } from "../../../src/view/CSS_ABSOLUTE_POSITIONING.js";
|
|
3
|
+
import EmptyView from "../../../src/view/elements/EmptyView.js";
|
|
4
|
+
|
|
5
|
+
export class SplitView extends EmptyView {
|
|
6
|
+
#direction = 'x';
|
|
7
|
+
#child_a = new EmptyView();
|
|
8
|
+
#child_b = new EmptyView();
|
|
9
|
+
#fraction = 0.5;
|
|
10
|
+
#round_split_to_nearest_integer = true
|
|
11
|
+
|
|
12
|
+
static from({ a, b, type = 'x', fraction = 0.5 }) {
|
|
13
|
+
const r = new SplitView();
|
|
14
|
+
|
|
15
|
+
r.direction = type;
|
|
16
|
+
r.fraction = fraction;
|
|
17
|
+
r.childA = a;
|
|
18
|
+
r.childB = b;
|
|
19
|
+
|
|
20
|
+
return r;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
super();
|
|
25
|
+
|
|
26
|
+
this.addChild(this.#child_a);
|
|
27
|
+
this.addChild(this.#child_b);
|
|
28
|
+
|
|
29
|
+
this.layout();
|
|
30
|
+
|
|
31
|
+
this.bindSignal(this.size.onChanged, this.layout, this);
|
|
32
|
+
this.on.linked.add(this.layout, this);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
set fraction(v) {
|
|
36
|
+
this.#fraction = v;
|
|
37
|
+
this.layout();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
set direction(v) {
|
|
41
|
+
if (!['x', 'y'].includes(v)) {
|
|
42
|
+
throw new Error(`Invalid direction '${v}', valid values are 'x' and 'y'`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.#direction = v;
|
|
46
|
+
|
|
47
|
+
this.layout();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
set childA(v) {
|
|
51
|
+
this.removeChild(this.#child_a);
|
|
52
|
+
|
|
53
|
+
this.#child_a = v;
|
|
54
|
+
this.addChild(v);
|
|
55
|
+
|
|
56
|
+
this.layout();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
set childB(v) {
|
|
60
|
+
this.removeChild(this.#child_b);
|
|
61
|
+
|
|
62
|
+
this.#child_b = v;
|
|
63
|
+
this.addChild(v);
|
|
64
|
+
|
|
65
|
+
this.layout();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
layout() {
|
|
69
|
+
const size = this.size;
|
|
70
|
+
const f = clamp01(this.#fraction);
|
|
71
|
+
|
|
72
|
+
this.#child_a.css(CSS_ABSOLUTE_POSITIONING);
|
|
73
|
+
this.#child_b.css(CSS_ABSOLUTE_POSITIONING);
|
|
74
|
+
|
|
75
|
+
const width = size.x;
|
|
76
|
+
const height = size.y;
|
|
77
|
+
if (this.#direction === 'x') {
|
|
78
|
+
let first = width*f;
|
|
79
|
+
if(this.#round_split_to_nearest_integer){
|
|
80
|
+
first = Math.round(first);
|
|
81
|
+
}
|
|
82
|
+
const second = width - first;
|
|
83
|
+
|
|
84
|
+
this.#child_a.size.set(first, height);
|
|
85
|
+
this.#child_b.size.set(second, height);
|
|
86
|
+
|
|
87
|
+
this.#child_a.position.set(0, 0);
|
|
88
|
+
this.#child_b.position.set(first, 0);
|
|
89
|
+
} else {
|
|
90
|
+
|
|
91
|
+
let first = height*f;
|
|
92
|
+
if(this.#round_split_to_nearest_integer){
|
|
93
|
+
first = Math.round(first);
|
|
94
|
+
}
|
|
95
|
+
const second = width - first;
|
|
96
|
+
|
|
97
|
+
this.#child_a.size.set(width, first);
|
|
98
|
+
this.#child_b.size.set(width, second);
|
|
99
|
+
|
|
100
|
+
this.#child_a.position.set(0, 0);
|
|
101
|
+
this.#child_b.position.set(0, first);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import Name from "../../../../model/game/ecs/component/Name.js";
|
|
2
|
+
import { initializeEditor } from "../../../../model/game/initializeEditor.js";
|
|
3
|
+
import { randomFromArray } from "../../../src/core/math/random/randomFromArray.js";
|
|
4
|
+
import { randomIntegerBetween } from "../../../src/core/math/random/randomIntegerBetween.js";
|
|
5
|
+
import { seededRandom } from "../../../src/core/math/random/seededRandom.js";
|
|
6
|
+
import { TextureAssetLoader } from "../../../src/engine/asset/loaders/texture/TextureAssetLoader.js";
|
|
7
|
+
import { EntityNode } from "../../../src/engine/ecs/parent/EntityNode.js";
|
|
8
|
+
import { Transform } from "../../../src/engine/ecs/transform/Transform.js";
|
|
9
|
+
import { EngineHarness } from "../../../src/engine/EngineHarness.js";
|
|
10
|
+
import { enableEditor } from "../../enableEditor.js";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* @param {function} rng
|
|
16
|
+
*/
|
|
17
|
+
function randomNode(rng = Math.random) {
|
|
18
|
+
const node = new EntityNode();
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
node.entity.add(new Transform());
|
|
22
|
+
const ENGLISH_FIRST_NAMES = [
|
|
23
|
+
"Alex", "Anthony","Adam",
|
|
24
|
+
"Barbara", "Bartholomew","Benjamin",
|
|
25
|
+
"Charles", "Christian", "Cindy", "Catherine",
|
|
26
|
+
"Duncan", "David", "Donna", "Dylan",
|
|
27
|
+
"Ethan", "Edward", "Edgar", "Elizabeth",
|
|
28
|
+
"Fletcher", "Francis", "Fae",
|
|
29
|
+
"George", "Gavin", "Gabriel", "Gideon", "Griffin", "Grace", "Gordon",
|
|
30
|
+
"Hugh", "Harry", "Hilla",
|
|
31
|
+
"Ian", "Inge", "Ivy", "Isabella",
|
|
32
|
+
"John", "Jeff", "Joanna","Jack",
|
|
33
|
+
"Kevin", "Kelsey", "Kara", "Kade", "Kenneth", "Kylie", "Kaleb", "Khloe", "Keith", "Katie",
|
|
34
|
+
"Lewis", "Liam",
|
|
35
|
+
"Mathew", "Mark", "Mona",
|
|
36
|
+
"Nickolas", "Nigel", "Natasha",
|
|
37
|
+
"Otto", "Owen", "Olivia", "Oliver", "Odette", "Ophelia", "Orion",
|
|
38
|
+
"Peter", "Paola",
|
|
39
|
+
"Raphael", "Ralf", "Robert",
|
|
40
|
+
"Steward", "Simon", "Samantha",
|
|
41
|
+
"Trevor", "Tylor", "Terry",
|
|
42
|
+
"Uwe", "Uriel", "Uma", "Ulysses",
|
|
43
|
+
"Vladimir", "Vera",
|
|
44
|
+
"Wolfgang", "William", "Wyatt", "Willow", "Wesley", "Wren",
|
|
45
|
+
"Xylo", "Xavier", "Xander",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const ENGLISH_SURNAMES = [
|
|
49
|
+
"Angel", "Aaron", "Abbott", "Abrams", "Ainsley","Anderson","Allen",
|
|
50
|
+
"Baker", "Bank", "Black", "Bale","Brown","Bennet","Bailey",
|
|
51
|
+
"Carter", "Chandler","Cooper","Clarke","Cook",
|
|
52
|
+
"Drake", "Dalton", "Davison", "Dawson", "Day","Davis",
|
|
53
|
+
"Ellis", "Eastwood", "Erickson", "Eaton", "Ellison","Evans","Edwards",
|
|
54
|
+
"Farley", "Fisc", "Fry", "Falcon","Farmer","Fletcher",
|
|
55
|
+
"Gabin", "Green", "Gray", "Gibson", "Grant", "Gordon", "Gill","Gold","Goldring","Golding",
|
|
56
|
+
"Hall", "Harris", "Hill", "Hughes", "Harrison", "Hunt", "Holmes",
|
|
57
|
+
"Ingram", "Irwin", "Irving", "Ives",
|
|
58
|
+
"Johnson","Jones","Jackson",
|
|
59
|
+
"Kelvin","King",
|
|
60
|
+
"Lewis", "Law","Lee",
|
|
61
|
+
"Morris","Mills","Martin","Moore","Morgan","Mitchel",
|
|
62
|
+
"Phillips","Parker","Price",
|
|
63
|
+
"Robinson","Roberts","Richardson",
|
|
64
|
+
"Smith","Scott",
|
|
65
|
+
"Tailor","Taylor","Thomas","Thompson","Turner",
|
|
66
|
+
"White","Wilson","Williams","Wright","Wood","Ward","Watson",
|
|
67
|
+
"Young",
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const first_name = randomFromArray(rng, ENGLISH_FIRST_NAMES);
|
|
71
|
+
const last_name = randomFromArray(rng, ENGLISH_SURNAMES);
|
|
72
|
+
|
|
73
|
+
node.entity.add(new Name(`${first_name} ${last_name}`))
|
|
74
|
+
|
|
75
|
+
return node;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
*
|
|
80
|
+
* @param {Engine} engine
|
|
81
|
+
* @returns {Promise<void>}
|
|
82
|
+
*/
|
|
83
|
+
async function main(engine) {
|
|
84
|
+
|
|
85
|
+
await EngineHarness.buildBasics({
|
|
86
|
+
engine,
|
|
87
|
+
showFps: false
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
const random = seededRandom();
|
|
92
|
+
|
|
93
|
+
const density = 0.2;
|
|
94
|
+
|
|
95
|
+
const nodes = [];
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < 50; i++) {
|
|
98
|
+
nodes.push(randomNode(random));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
while (nodes.length > 4) {
|
|
102
|
+
const child_index = randomIntegerBetween(random, 0, nodes.length - 1);
|
|
103
|
+
|
|
104
|
+
const [child] = nodes.splice(child_index, 1);
|
|
105
|
+
|
|
106
|
+
const parent = randomFromArray(random, nodes);
|
|
107
|
+
|
|
108
|
+
parent.addChild(child);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
112
|
+
|
|
113
|
+
const node = nodes[i];
|
|
114
|
+
|
|
115
|
+
node.build(engine.entityManager.dataset);
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
enableEditor(engine, initializeEditor).enable();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
new EngineHarness().initialize({
|
|
124
|
+
configuration(config, engine) {
|
|
125
|
+
config.addLoader('texture', new TextureAssetLoader())
|
|
126
|
+
}
|
|
127
|
+
}).then(main)
|
package/package.json
CHANGED
package/src/core/cache/Cache.js
CHANGED
|
@@ -174,14 +174,56 @@ export class Cache {
|
|
|
174
174
|
recomputeWeight() {
|
|
175
175
|
let result = 0;
|
|
176
176
|
|
|
177
|
-
for (let [key,
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
for (let [key, record] of this.data) {
|
|
178
|
+
|
|
179
|
+
const weight = this.computeElementWeight(key, record.value);
|
|
180
|
+
record.weight = weight;
|
|
181
|
+
|
|
182
|
+
result += weight;
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
this.weight = result;
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Useful when working with wrapped value types, where contents can change and affect overall weight
|
|
190
|
+
* Instead of re-inserting element, we can just update weights
|
|
191
|
+
* NOTE: this method may trigger eviction
|
|
192
|
+
* @param {Key} key
|
|
193
|
+
* @returns {boolean} true when weight successfully updated, false if element was not found in cache
|
|
194
|
+
*/
|
|
195
|
+
updateElementWeight(key) {
|
|
196
|
+
const record = this.data.get(key);
|
|
197
|
+
|
|
198
|
+
if (record === undefined) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const old_weight = record.weight;
|
|
203
|
+
|
|
204
|
+
const new_weight = this.computeElementWeight(key, record.value);
|
|
205
|
+
|
|
206
|
+
if (new_weight === old_weight) {
|
|
207
|
+
// we're done, no change
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
record.weight = new_weight;
|
|
212
|
+
|
|
213
|
+
const delta_weight = new_weight - old_weight;
|
|
214
|
+
|
|
215
|
+
this.weight += delta_weight;
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
this.weight > this.maxWeight
|
|
219
|
+
&& new_weight >= this.maxWeight //make it less likely to drop entire cache
|
|
220
|
+
) {
|
|
221
|
+
this.evictUntilWeight(this.maxWeight);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
185
227
|
/**
|
|
186
228
|
* @private
|
|
187
229
|
* @param {Key} key
|
|
@@ -189,7 +231,15 @@ export class Cache {
|
|
|
189
231
|
* @returns {number}
|
|
190
232
|
*/
|
|
191
233
|
computeElementWeight(key, value) {
|
|
192
|
-
|
|
234
|
+
const key_weight = this.keyWeigher(key);
|
|
235
|
+
|
|
236
|
+
assert.notNaN(key_weight, 'key_weight');
|
|
237
|
+
|
|
238
|
+
const value_weight = this.valueWeigher(value);
|
|
239
|
+
|
|
240
|
+
assert.notNaN(value_weight, 'value_weight');
|
|
241
|
+
|
|
242
|
+
return key_weight + value_weight;
|
|
193
243
|
}
|
|
194
244
|
|
|
195
245
|
/**
|
|
@@ -264,6 +314,9 @@ export class Cache {
|
|
|
264
314
|
//compute weight
|
|
265
315
|
const elementWeight = this.computeElementWeight(key, value);
|
|
266
316
|
|
|
317
|
+
element.weight = elementWeight;
|
|
318
|
+
|
|
319
|
+
|
|
267
320
|
/**
|
|
268
321
|
* It's possible that element being added is larger than cache's capacity,
|
|
269
322
|
* in which case entire cache will be evicted, but there still won't be enough space
|
|
@@ -374,14 +427,11 @@ export class Cache {
|
|
|
374
427
|
|
|
375
428
|
const key = element.key;
|
|
376
429
|
|
|
377
|
-
//compute weight
|
|
378
|
-
const elementWeight = this.computeElementWeight(key, value);
|
|
379
|
-
|
|
380
430
|
//remove from cache
|
|
381
431
|
this.data.delete(key);
|
|
382
432
|
|
|
383
433
|
//update weight
|
|
384
|
-
this.weight -=
|
|
434
|
+
this.weight -= element.weight;
|
|
385
435
|
}
|
|
386
436
|
|
|
387
437
|
/**
|
|
@@ -179,6 +179,44 @@ test("recomputeWeight", () => {
|
|
|
179
179
|
expect(cache.weight).toBe(4);
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
+
test("updateElementWeight", () => {
|
|
183
|
+
class Record {
|
|
184
|
+
weight = 0;
|
|
185
|
+
|
|
186
|
+
constructor(v) {
|
|
187
|
+
this.weight = v;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
*
|
|
193
|
+
* @type {Cache<string, Record>}
|
|
194
|
+
*/
|
|
195
|
+
const cache = new Cache({
|
|
196
|
+
valueWeigher: (v) => v.weight,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const record_a = new Record(7);
|
|
200
|
+
const record_b = new Record(11);
|
|
201
|
+
|
|
202
|
+
cache.set("a", record_a);
|
|
203
|
+
cache.set("b", record_b);
|
|
204
|
+
|
|
205
|
+
expect(cache.weight).toBe(18);
|
|
206
|
+
|
|
207
|
+
record_a.weight = 3;
|
|
208
|
+
|
|
209
|
+
cache.updateElementWeight("a");
|
|
210
|
+
|
|
211
|
+
expect(cache.weight).toBe(14);
|
|
212
|
+
|
|
213
|
+
record_b.weight = 13;
|
|
214
|
+
|
|
215
|
+
cache.updateElementWeight("b");
|
|
216
|
+
|
|
217
|
+
expect(cache.weight).toBe(16);
|
|
218
|
+
});
|
|
219
|
+
|
|
182
220
|
test("silentRemove should not notify", () => {
|
|
183
221
|
|
|
184
222
|
const cache = new Cache();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//
|
|
2
2
|
|
|
3
3
|
import { assert } from "../assert.js";
|
|
4
|
+
import { returnZero } from "../function/returnZero.js";
|
|
4
5
|
import { current_time_in_seconds } from "../time/current_time_in_seconds.js";
|
|
5
6
|
import { Cache } from "./Cache.js";
|
|
6
7
|
|
|
@@ -17,6 +18,7 @@ class Record {
|
|
|
17
18
|
this.value = value;
|
|
18
19
|
this.time = time;
|
|
19
20
|
this.failed = false;
|
|
21
|
+
this.weight = 0;
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -27,6 +29,15 @@ class Record {
|
|
|
27
29
|
*/
|
|
28
30
|
const DEFAULT_TIME_TO_LIVE = Infinity;
|
|
29
31
|
|
|
32
|
+
/**
|
|
33
|
+
* @template T
|
|
34
|
+
* @param {Record<T>} record
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
function record_get_value_weight(record) {
|
|
38
|
+
return record.weight;
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
/**
|
|
31
42
|
* Asynchronous cache capable of resolving its own values by keys
|
|
32
43
|
* Modelled on Guava's LoadingCache concept
|
|
@@ -54,6 +65,11 @@ export class LoadingCache {
|
|
|
54
65
|
* @type {boolean}
|
|
55
66
|
*/
|
|
56
67
|
#policyRetryFailed = true;
|
|
68
|
+
/**
|
|
69
|
+
*
|
|
70
|
+
* @type {function(V): number}
|
|
71
|
+
*/
|
|
72
|
+
#value_weigher;
|
|
57
73
|
|
|
58
74
|
/**
|
|
59
75
|
* @see {@link Cache} for more details on what each parameter means
|
|
@@ -71,7 +87,7 @@ export class LoadingCache {
|
|
|
71
87
|
constructor({
|
|
72
88
|
maxWeight,
|
|
73
89
|
keyWeigher,
|
|
74
|
-
valueWeigher,
|
|
90
|
+
valueWeigher = returnZero,
|
|
75
91
|
keyHashFunction,
|
|
76
92
|
keyEqualityFunction,
|
|
77
93
|
capacity,
|
|
@@ -85,7 +101,7 @@ export class LoadingCache {
|
|
|
85
101
|
this.#internal = new Cache({
|
|
86
102
|
maxWeight,
|
|
87
103
|
keyWeigher,
|
|
88
|
-
valueWeigher,
|
|
104
|
+
valueWeigher: record_get_value_weight,
|
|
89
105
|
keyHashFunction,
|
|
90
106
|
keyEqualityFunction,
|
|
91
107
|
capacity,
|
|
@@ -94,6 +110,7 @@ export class LoadingCache {
|
|
|
94
110
|
this.#timeToLive = timeToLive;
|
|
95
111
|
this.#load = load;
|
|
96
112
|
this.#policyRetryFailed = retryFailed;
|
|
113
|
+
this.#value_weigher = valueWeigher;
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
/**
|
|
@@ -132,6 +149,12 @@ export class LoadingCache {
|
|
|
132
149
|
|
|
133
150
|
this.#internal.put(key, record);
|
|
134
151
|
|
|
152
|
+
promise.then((value) => {
|
|
153
|
+
// re-score value based on actual data
|
|
154
|
+
record.weight = this.#value_weigher(value);
|
|
155
|
+
this.#internal.updateElementWeight(key);
|
|
156
|
+
});
|
|
157
|
+
|
|
135
158
|
promise.catch(() => {
|
|
136
159
|
// mark as failure
|
|
137
160
|
record.failed = true;
|