@judah-silva/rnacanvas 0.0.1
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 +10 -0
- package/dist/index.d.mts +528 -0
- package/dist/index.d.ts +528 -0
- package/dist/index.js +1466 -0
- package/dist/index.mjs +1408 -0
- package/judah-silva-rnacanvas-0.0.1.tgz +0 -0
- package/package.json +29 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1408 @@
|
|
|
1
|
+
// src/3D/Group.ts
|
|
2
|
+
import { TransformNode } from "@babylonjs/core";
|
|
3
|
+
var Group = class {
|
|
4
|
+
_node;
|
|
5
|
+
_children;
|
|
6
|
+
constructor(name, scene) {
|
|
7
|
+
this._node = new TransformNode(name, scene);
|
|
8
|
+
this._children = /* @__PURE__ */ new Set();
|
|
9
|
+
this._node.rotationQuaternion = this._node.rotation.toQuaternion();
|
|
10
|
+
}
|
|
11
|
+
setParent(parent) {
|
|
12
|
+
this._node.parent = parent ? parent.node : null;
|
|
13
|
+
}
|
|
14
|
+
removeChild(child) {
|
|
15
|
+
if (!this._children.has(child)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
this._children.delete(child);
|
|
19
|
+
child.setParent(null);
|
|
20
|
+
}
|
|
21
|
+
get node() {
|
|
22
|
+
return this._node;
|
|
23
|
+
}
|
|
24
|
+
get children() {
|
|
25
|
+
return this._children;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/3D/Motif.ts
|
|
30
|
+
import { NullEngine, Quaternion as Quaternion3, Scene as Scene2, Space, Vector3 as Vector32 } from "@babylonjs/core";
|
|
31
|
+
|
|
32
|
+
// src/Math/Vec3.ts
|
|
33
|
+
import { Quaternion, Vector3 } from "@babylonjs/core";
|
|
34
|
+
var Vec3 = class _Vec3 {
|
|
35
|
+
_vector3;
|
|
36
|
+
static Zero = new _Vec3(0, 0, 0);
|
|
37
|
+
constructor(x, y, z) {
|
|
38
|
+
this._vector3 = new Vector3(x, y, z);
|
|
39
|
+
}
|
|
40
|
+
clone() {
|
|
41
|
+
return new _Vec3(this.x, this.y, this.z);
|
|
42
|
+
}
|
|
43
|
+
normalize() {
|
|
44
|
+
this._vector3.normalize();
|
|
45
|
+
}
|
|
46
|
+
applyAxisAngle(axis, angle) {
|
|
47
|
+
const rotationQuat = Quaternion.RotationAxis(axis._vector3, angle);
|
|
48
|
+
this._vector3.rotateByQuaternionAroundPointToRef(rotationQuat, Vector3.Zero(), this._vector3);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
applyQuaternion(quat) {
|
|
52
|
+
this._vector3.applyRotationQuaternionInPlace(quat.quaternion);
|
|
53
|
+
}
|
|
54
|
+
multiplyScalar(scalar) {
|
|
55
|
+
this._vector3.scaleInPlace(scalar);
|
|
56
|
+
}
|
|
57
|
+
add(vec) {
|
|
58
|
+
this._vector3.addInPlace(vec._vector3);
|
|
59
|
+
}
|
|
60
|
+
length() {
|
|
61
|
+
return this._vector3.length();
|
|
62
|
+
}
|
|
63
|
+
equals(other) {
|
|
64
|
+
return this.x === other.x && this.y === other.y && this.z === other.z;
|
|
65
|
+
}
|
|
66
|
+
get x() {
|
|
67
|
+
return this._vector3.x;
|
|
68
|
+
}
|
|
69
|
+
get y() {
|
|
70
|
+
return this._vector3.y;
|
|
71
|
+
}
|
|
72
|
+
get z() {
|
|
73
|
+
return this._vector3.z;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/Math/Quat.ts
|
|
78
|
+
import { Quaternion as Quaternion2 } from "@babylonjs/core";
|
|
79
|
+
var Quat = class {
|
|
80
|
+
_quaternion;
|
|
81
|
+
constructor() {
|
|
82
|
+
this._quaternion = new Quaternion2();
|
|
83
|
+
}
|
|
84
|
+
rotateByQuaternion(quaternion) {
|
|
85
|
+
quaternion.quaternion.multiplyToRef(this._quaternion, this._quaternion);
|
|
86
|
+
}
|
|
87
|
+
setToQuaternion(quaternion) {
|
|
88
|
+
if (quaternion === null) {
|
|
89
|
+
throw new Error("Cannot set to null quaternion");
|
|
90
|
+
}
|
|
91
|
+
this._quaternion = quaternion;
|
|
92
|
+
}
|
|
93
|
+
setFromMatrix(matrix) {
|
|
94
|
+
this._quaternion = Quaternion2.FromRotationMatrix(matrix.matrix);
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
setFromEuler(eulerAngle) {
|
|
98
|
+
this._quaternion = Quaternion2.FromEulerAngles(eulerAngle.x, eulerAngle.y, eulerAngle.z);
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
get quaternion() {
|
|
102
|
+
return this._quaternion;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/Math/Matrix4.ts
|
|
107
|
+
import { Matrix } from "@babylonjs/core";
|
|
108
|
+
var Matrix4 = class {
|
|
109
|
+
_matrix;
|
|
110
|
+
constructor() {
|
|
111
|
+
this._matrix = Matrix.Identity();
|
|
112
|
+
}
|
|
113
|
+
fromArray(array) {
|
|
114
|
+
let matArray = array;
|
|
115
|
+
if (array.length === 9) {
|
|
116
|
+
matArray = [
|
|
117
|
+
array[0],
|
|
118
|
+
array[1],
|
|
119
|
+
array[2],
|
|
120
|
+
0,
|
|
121
|
+
array[3],
|
|
122
|
+
array[4],
|
|
123
|
+
array[5],
|
|
124
|
+
0,
|
|
125
|
+
array[6],
|
|
126
|
+
array[7],
|
|
127
|
+
array[8],
|
|
128
|
+
1
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
this._matrix = Matrix.FromArray(matArray);
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
get matrix() {
|
|
135
|
+
return this._matrix;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// src/3D/Motif.ts
|
|
140
|
+
var Motif = class extends Group {
|
|
141
|
+
userData;
|
|
142
|
+
_quat;
|
|
143
|
+
constructor(name) {
|
|
144
|
+
const tempEngine = new NullEngine();
|
|
145
|
+
const tempScene = new Scene2(tempEngine);
|
|
146
|
+
super(name, tempScene);
|
|
147
|
+
this._quat = new Quat();
|
|
148
|
+
this.userData = {
|
|
149
|
+
atomInfo: [],
|
|
150
|
+
fileName: ""
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
addChild(child) {
|
|
154
|
+
if (this._children.has(child)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
child.setParent(this);
|
|
158
|
+
this._children.add(child);
|
|
159
|
+
}
|
|
160
|
+
setPosition(x, y, z) {
|
|
161
|
+
this._node.setAbsolutePosition(new Vector32(x, y, z));
|
|
162
|
+
}
|
|
163
|
+
translate(x, y, z) {
|
|
164
|
+
const newX = this.position.x + x;
|
|
165
|
+
const newY = this.position.y + y;
|
|
166
|
+
const newZ = this.position.z + z;
|
|
167
|
+
this.setPosition(newX, newY, newZ);
|
|
168
|
+
}
|
|
169
|
+
rotate(axis, angle) {
|
|
170
|
+
this._node.rotate(new Vector32(
|
|
171
|
+
axis.x,
|
|
172
|
+
axis.y,
|
|
173
|
+
axis.z
|
|
174
|
+
), angle, Space.WORLD);
|
|
175
|
+
this._nanCheck();
|
|
176
|
+
}
|
|
177
|
+
rotateByQuaternion(quat) {
|
|
178
|
+
if (this._node.rotationQuaternion === null) {
|
|
179
|
+
this._node.rotationQuaternion = this._node.rotation.toQuaternion();
|
|
180
|
+
}
|
|
181
|
+
quat.quaternion.multiplyToRef(this._node.rotationQuaternion, this._node.rotationQuaternion);
|
|
182
|
+
this._nanCheck();
|
|
183
|
+
}
|
|
184
|
+
setQuaternion(quat) {
|
|
185
|
+
if (this._node.rotationQuaternion === null) {
|
|
186
|
+
this._node.rotationQuaternion = this._node.rotation.toQuaternion();
|
|
187
|
+
}
|
|
188
|
+
this._quat.setToQuaternion(quat.quaternion);
|
|
189
|
+
}
|
|
190
|
+
multiplyScalar(scalar) {
|
|
191
|
+
this._node.scaling = new Vector32(
|
|
192
|
+
this._node.scaling.x * scalar,
|
|
193
|
+
this._node.scaling.y * scalar,
|
|
194
|
+
this._node.scaling.z * scalar
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
_nanCheck() {
|
|
198
|
+
if (Number.isNaN(this._quat.quaternion.w) || Number.isNaN(this._quat.quaternion.x) || Number.isNaN(this._quat.quaternion.y) || Number.isNaN(this._quat.quaternion.z)) {
|
|
199
|
+
this._quat.setToQuaternion(Quaternion3.Identity());
|
|
200
|
+
throw new Error(`Quaternion is NaN for motif ${this._node.name}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
get uuid() {
|
|
204
|
+
return this._node.uniqueId.toString();
|
|
205
|
+
}
|
|
206
|
+
get quat() {
|
|
207
|
+
this._quat.setToQuaternion(this._node.rotationQuaternion);
|
|
208
|
+
return this._quat;
|
|
209
|
+
}
|
|
210
|
+
get position() {
|
|
211
|
+
return new Vec3(
|
|
212
|
+
this._node.absolutePosition.x,
|
|
213
|
+
this._node.absolutePosition.y,
|
|
214
|
+
this._node.absolutePosition.z
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/3D/Residue.ts
|
|
220
|
+
import { NullEngine as NullEngine2, Scene as Scene3 } from "@babylonjs/core";
|
|
221
|
+
var Residue = class extends Group {
|
|
222
|
+
constructor(name) {
|
|
223
|
+
const tempEngine = new NullEngine2();
|
|
224
|
+
const tempScene = new Scene3(tempEngine);
|
|
225
|
+
super(name, tempScene);
|
|
226
|
+
}
|
|
227
|
+
addChild(child) {
|
|
228
|
+
if (this._children.has(child)) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
child.setParent(this);
|
|
232
|
+
this._children.add(child);
|
|
233
|
+
}
|
|
234
|
+
// Temporary function to find if Residue contains mesh with uuid
|
|
235
|
+
hasMesh(uuid) {
|
|
236
|
+
let found = false;
|
|
237
|
+
this._children.forEach((child) => {
|
|
238
|
+
if (child.uuid === uuid) {
|
|
239
|
+
found = true;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return found;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// src/3D/MeshObject.ts
|
|
247
|
+
import { Color3, Mesh, StandardMaterial, VertexData } from "@babylonjs/core";
|
|
248
|
+
var MeshObject = class {
|
|
249
|
+
_mesh;
|
|
250
|
+
userData;
|
|
251
|
+
constructor(name) {
|
|
252
|
+
this._mesh = new Mesh(name);
|
|
253
|
+
this.userData = {};
|
|
254
|
+
}
|
|
255
|
+
setParent(parent) {
|
|
256
|
+
if (parent === null) {
|
|
257
|
+
this._mesh.parent = null;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this._mesh.parent = parent.node;
|
|
261
|
+
}
|
|
262
|
+
applyVertexData(positions, indices) {
|
|
263
|
+
const normals = [];
|
|
264
|
+
VertexData.ComputeNormals(positions, indices, normals);
|
|
265
|
+
const vertexData = new VertexData();
|
|
266
|
+
vertexData.positions = positions;
|
|
267
|
+
vertexData.indices = indices;
|
|
268
|
+
vertexData.normals = normals;
|
|
269
|
+
vertexData.applyToMesh(this._mesh);
|
|
270
|
+
}
|
|
271
|
+
applyHighlight() {
|
|
272
|
+
const mat = this._mesh.material;
|
|
273
|
+
mat.emissiveColor = mat.diffuseColor.scale(0.75);
|
|
274
|
+
}
|
|
275
|
+
resetHighlight() {
|
|
276
|
+
const mat = this._mesh.material;
|
|
277
|
+
mat.emissiveColor = Color3.Black();
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Cretes a material with the given color and sets it to the mesh
|
|
281
|
+
* @param color String in the format of #RRGGBB
|
|
282
|
+
*/
|
|
283
|
+
createAndSetMaterial(color) {
|
|
284
|
+
const mat = new StandardMaterial("mat");
|
|
285
|
+
const color3 = Color3.FromHexString(`#${color.replace(/^0x/, "")}`);
|
|
286
|
+
mat.diffuseColor = color3;
|
|
287
|
+
mat.specularColor = color3;
|
|
288
|
+
this._mesh.material = mat;
|
|
289
|
+
}
|
|
290
|
+
setNewMesh(mesh) {
|
|
291
|
+
this._mesh = mesh;
|
|
292
|
+
return this;
|
|
293
|
+
}
|
|
294
|
+
get mesh() {
|
|
295
|
+
return this._mesh;
|
|
296
|
+
}
|
|
297
|
+
get uuid() {
|
|
298
|
+
return this._mesh.uniqueId.toString();
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/3D/RenderScene.ts
|
|
303
|
+
import { Camera, Color4, Engine, Geometry, HemisphericLight, Material, Mesh as Mesh2, Scene as Scene5, UniversalCamera, Vector3 as Vector33 } from "@babylonjs/core";
|
|
304
|
+
|
|
305
|
+
// src/Events/EventTypes.ts
|
|
306
|
+
var Events;
|
|
307
|
+
((Events2) => {
|
|
308
|
+
let EventType;
|
|
309
|
+
((EventType2) => {
|
|
310
|
+
EventType2["POINTER_DOWN"] = "pointerDown";
|
|
311
|
+
EventType2["POINTER_UP"] = "pointerUp";
|
|
312
|
+
EventType2["POINTER_MOVE"] = "pointerMove";
|
|
313
|
+
EventType2["POINTER_WHEEL"] = "pointerWheel";
|
|
314
|
+
EventType2["KEY_DOWN"] = "keyDown";
|
|
315
|
+
EventType2["KEY_UP"] = "keyUp";
|
|
316
|
+
EventType2["TOUCH_START"] = "touchStart";
|
|
317
|
+
EventType2["TOUCH_END"] = "touchEnd";
|
|
318
|
+
EventType2["TOUCH_MOVE"] = "touchMove";
|
|
319
|
+
EventType2["PINCH_START"] = "pinchStart";
|
|
320
|
+
EventType2["PINCH"] = "pinch";
|
|
321
|
+
EventType2["PINCH_END"] = "pinchEnd";
|
|
322
|
+
EventType2["OBJECT_SELECTED"] = "objectSelected";
|
|
323
|
+
EventType2["OBJECT_DESELECTED"] = "objectDeselected";
|
|
324
|
+
EventType2["RESIZE"] = "resize";
|
|
325
|
+
EventType2["RENDER"] = "render";
|
|
326
|
+
EventType2["ENGINE_STARTED"] = "engineStarted";
|
|
327
|
+
EventType2["ENGINE_STOPPED"] = "engineStopped";
|
|
328
|
+
})(EventType = Events2.EventType || (Events2.EventType = {}));
|
|
329
|
+
})(Events || (Events = {}));
|
|
330
|
+
|
|
331
|
+
// src/Events/EventManager.ts
|
|
332
|
+
import { KeyboardEventTypes, Observable, PointerEventTypes } from "@babylonjs/core";
|
|
333
|
+
var EventManager = class {
|
|
334
|
+
// Custom observables for each event type
|
|
335
|
+
_eventObservables = /* @__PURE__ */ new Map();
|
|
336
|
+
// Babylon.js observer references for cleanup
|
|
337
|
+
_pointerObserver = null;
|
|
338
|
+
_keyboardObserver = null;
|
|
339
|
+
_renderObserver = null;
|
|
340
|
+
_activeKeys = /* @__PURE__ */ new Set();
|
|
341
|
+
// Event state variables
|
|
342
|
+
_pendingDeselect = false;
|
|
343
|
+
// private _listeningForEvents: boolean = true;
|
|
344
|
+
constructor() {
|
|
345
|
+
this._initializeObservables();
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Dispose of the event listeners and triggers
|
|
349
|
+
*/
|
|
350
|
+
dispose() {
|
|
351
|
+
this._eventObservables.forEach((observable) => observable.clear());
|
|
352
|
+
this._eventObservables.clear();
|
|
353
|
+
this._pointerObserver?.remove();
|
|
354
|
+
this._pointerObserver = null;
|
|
355
|
+
this._keyboardObserver?.remove();
|
|
356
|
+
this._keyboardObserver = null;
|
|
357
|
+
this._renderObserver?.remove();
|
|
358
|
+
this._renderObserver = null;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Register an event of a certain type
|
|
362
|
+
* @param eventType String event type. Can be custom or standard.
|
|
363
|
+
* @param callback Callback function to run when the event is triggered
|
|
364
|
+
* @returns Babylon Observer instance for later removal
|
|
365
|
+
*/
|
|
366
|
+
on(eventType, callback) {
|
|
367
|
+
if (!this._eventObservables.has(eventType)) {
|
|
368
|
+
this._eventObservables.set(eventType, new Observable());
|
|
369
|
+
}
|
|
370
|
+
return this._eventObservables.get(eventType).add(callback);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Remove a registered event observer
|
|
374
|
+
* @param observer Babylon Observer with the registered callback
|
|
375
|
+
*/
|
|
376
|
+
off(observer) {
|
|
377
|
+
observer.remove();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Emit a custom event, triggering listeners registered with that event type.
|
|
381
|
+
* @param eventType A string event type. Can be custom or standard.
|
|
382
|
+
* @param eventData Event info object partial.
|
|
383
|
+
*/
|
|
384
|
+
emit(eventType, eventData) {
|
|
385
|
+
const event = {
|
|
386
|
+
type: eventType,
|
|
387
|
+
canceled: false,
|
|
388
|
+
timestamp: performance.now(),
|
|
389
|
+
...eventData
|
|
390
|
+
};
|
|
391
|
+
try {
|
|
392
|
+
this.notifyObservers(eventType, event);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
throw new Error(`Error emitting event ${eventType}:`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Method to notify listeners of a type of event
|
|
399
|
+
* @param eventType A string event type. Can be custom or standard.
|
|
400
|
+
* @param event Event info object
|
|
401
|
+
*/
|
|
402
|
+
notifyObservers(eventType, event) {
|
|
403
|
+
if (this._eventObservables.has(eventType)) {
|
|
404
|
+
this._eventObservables.get(eventType).notifyObservers(event);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Method to set up pointer and keyboard events on the scene.
|
|
409
|
+
* Comes preset with POINTER_DOWN, POINTER_UP, POINTER_MOVE, KEYBOARD_DOWN, and KEYBOARD_UP event triggering.
|
|
410
|
+
* @param scene A RenderScene object
|
|
411
|
+
*/
|
|
412
|
+
setupEventHandling(scene) {
|
|
413
|
+
if (scene.isRunning) {
|
|
414
|
+
throw new Error("Cannot setup event handling on running scene.\n");
|
|
415
|
+
}
|
|
416
|
+
const babylonScene = scene.scene;
|
|
417
|
+
this._pointerObserver = babylonScene.onPointerObservable.add((pointerInfo) => {
|
|
418
|
+
const event = pointerInfo.event;
|
|
419
|
+
const pointerEvent = {
|
|
420
|
+
type: Events.EventType.POINTER_DOWN,
|
|
421
|
+
// will be overwritten below
|
|
422
|
+
position: { x: event.clientX, y: event.clientY },
|
|
423
|
+
button: event.button,
|
|
424
|
+
buttons: event.buttons,
|
|
425
|
+
ctrlKey: event.ctrlKey,
|
|
426
|
+
altKey: event.altKey,
|
|
427
|
+
shiftKey: event.shiftKey,
|
|
428
|
+
metaKey: event.metaKey,
|
|
429
|
+
originalEvent: event,
|
|
430
|
+
canceled: false,
|
|
431
|
+
timestamp: performance.now()
|
|
432
|
+
};
|
|
433
|
+
let motifNode = null;
|
|
434
|
+
let pickedMesh = null;
|
|
435
|
+
if (pointerInfo.pickInfo?.hit && pointerInfo.pickInfo?.pickedMesh) {
|
|
436
|
+
pickedMesh = pointerInfo.pickInfo.pickedMesh;
|
|
437
|
+
motifNode = pointerInfo.pickInfo.pickedMesh.parent?.parent;
|
|
438
|
+
}
|
|
439
|
+
let motifObj = null;
|
|
440
|
+
let residue = null;
|
|
441
|
+
if (motifNode && pickedMesh) {
|
|
442
|
+
motifObj = scene.children.get(motifNode.uniqueId.toString());
|
|
443
|
+
residue = this._getResidueFromMotifNode(
|
|
444
|
+
motifNode,
|
|
445
|
+
pickedMesh?.uniqueId.toString(),
|
|
446
|
+
scene.children
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
if (motifNode && residue) {
|
|
450
|
+
pointerEvent.pickedResidue = residue;
|
|
451
|
+
}
|
|
452
|
+
switch (pointerInfo.type) {
|
|
453
|
+
case PointerEventTypes.POINTERDOWN:
|
|
454
|
+
pointerEvent.type = Events.EventType.POINTER_DOWN;
|
|
455
|
+
this.notifyObservers(Events.EventType.POINTER_DOWN, pointerEvent);
|
|
456
|
+
if (motifObj && residue) {
|
|
457
|
+
const selectionEvent = {
|
|
458
|
+
type: Events.EventType.OBJECT_SELECTED,
|
|
459
|
+
residue,
|
|
460
|
+
motif: motifObj,
|
|
461
|
+
multiSelect: event.ctrlKey,
|
|
462
|
+
canceled: false,
|
|
463
|
+
timestamp: performance.now()
|
|
464
|
+
};
|
|
465
|
+
this.notifyObservers(Events.EventType.OBJECT_SELECTED, selectionEvent);
|
|
466
|
+
} else {
|
|
467
|
+
this._pendingDeselect = true;
|
|
468
|
+
}
|
|
469
|
+
break;
|
|
470
|
+
case PointerEventTypes.POINTERUP:
|
|
471
|
+
pointerEvent.type = Events.EventType.POINTER_UP;
|
|
472
|
+
this.notifyObservers(Events.EventType.POINTER_UP, pointerEvent);
|
|
473
|
+
if (this._pendingDeselect) {
|
|
474
|
+
const deselectionEvent = {
|
|
475
|
+
type: Events.EventType.OBJECT_DESELECTED,
|
|
476
|
+
canceled: false,
|
|
477
|
+
timestamp: performance.now()
|
|
478
|
+
};
|
|
479
|
+
this.notifyObservers(Events.EventType.OBJECT_DESELECTED, deselectionEvent);
|
|
480
|
+
this._pendingDeselect = false;
|
|
481
|
+
}
|
|
482
|
+
break;
|
|
483
|
+
case PointerEventTypes.POINTERMOVE:
|
|
484
|
+
pointerEvent.type = Events.EventType.POINTER_MOVE;
|
|
485
|
+
pointerEvent.deltaX = event.movementX;
|
|
486
|
+
pointerEvent.deltaY = event.movementY;
|
|
487
|
+
this.notifyObservers(Events.EventType.POINTER_MOVE, pointerEvent);
|
|
488
|
+
this._pendingDeselect = false;
|
|
489
|
+
break;
|
|
490
|
+
case PointerEventTypes.POINTERWHEEL:
|
|
491
|
+
pointerEvent.type = Events.EventType.POINTER_WHEEL;
|
|
492
|
+
this.notifyObservers(Events.EventType.POINTER_WHEEL, pointerEvent);
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
this._keyboardObserver = babylonScene.onKeyboardObservable.add((keyboardInfo) => {
|
|
497
|
+
const event = keyboardInfo.event;
|
|
498
|
+
const keyboardEvent = {
|
|
499
|
+
type: keyboardInfo.type === KeyboardEventTypes.KEYDOWN ? Events.EventType.KEY_DOWN : Events.EventType.KEY_UP,
|
|
500
|
+
key: event.key,
|
|
501
|
+
code: event.code,
|
|
502
|
+
ctrlKey: event.ctrlKey,
|
|
503
|
+
altKey: event.altKey,
|
|
504
|
+
shiftKey: event.shiftKey,
|
|
505
|
+
metaKey: event.metaKey,
|
|
506
|
+
repeat: event.repeat,
|
|
507
|
+
rotationAxis: new Vec3(0, 0, 0),
|
|
508
|
+
translationDirection: new Vec3(0, 0, 0),
|
|
509
|
+
originalEvent: event,
|
|
510
|
+
canceled: false,
|
|
511
|
+
timestamp: performance.now()
|
|
512
|
+
};
|
|
513
|
+
if (keyboardInfo.type === KeyboardEventTypes.KEYDOWN) {
|
|
514
|
+
this._activeKeys.add(event.key);
|
|
515
|
+
} else if (keyboardInfo.type === KeyboardEventTypes.KEYUP) {
|
|
516
|
+
this._activeKeys.delete(event.key);
|
|
517
|
+
}
|
|
518
|
+
if (this._activeKeys.has("w")) keyboardEvent.rotationAxis.add(new Vec3(-1, 0, 0));
|
|
519
|
+
if (this._activeKeys.has("a")) keyboardEvent.rotationAxis.add(new Vec3(0, 1, 0));
|
|
520
|
+
if (this._activeKeys.has("s")) keyboardEvent.rotationAxis.add(new Vec3(1, 0, 0));
|
|
521
|
+
if (this._activeKeys.has("d")) keyboardEvent.rotationAxis.add(new Vec3(0, -1, 0));
|
|
522
|
+
if (this._activeKeys.has("q")) keyboardEvent.rotationAxis.add(new Vec3(0, 0, -1));
|
|
523
|
+
if (this._activeKeys.has("e")) keyboardEvent.rotationAxis.add(new Vec3(0, 0, 1));
|
|
524
|
+
if (this._activeKeys.has("W")) keyboardEvent.translationDirection.add(new Vec3(0, 1, 0));
|
|
525
|
+
if (this._activeKeys.has("A")) keyboardEvent.translationDirection.add(new Vec3(1, 0, 0));
|
|
526
|
+
if (this._activeKeys.has("S")) keyboardEvent.translationDirection.add(new Vec3(0, -1, 0));
|
|
527
|
+
if (this._activeKeys.has("D")) keyboardEvent.translationDirection.add(new Vec3(-1, 0, 0));
|
|
528
|
+
if (keyboardInfo.type === KeyboardEventTypes.KEYDOWN) {
|
|
529
|
+
this.notifyObservers(Events.EventType.KEY_DOWN, keyboardEvent);
|
|
530
|
+
} else if (keyboardInfo.type === KeyboardEventTypes.KEYUP) {
|
|
531
|
+
this.notifyObservers(Events.EventType.KEY_UP, keyboardEvent);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Initialize observables for all standard event types
|
|
537
|
+
*/
|
|
538
|
+
_initializeObservables() {
|
|
539
|
+
Object.values(Events.EventType).forEach((eventType) => {
|
|
540
|
+
this._eventObservables.set(eventType, new Observable());
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Helper function to get the residue that contains a picked mesh using the transform node of a motif
|
|
545
|
+
* @param node {TransformNode} Motif's TransformNode
|
|
546
|
+
* @param meshUUID Stringified uniqueId of the picked mesh
|
|
547
|
+
* @param sceneChildren Map of Motifs that exist in the scene
|
|
548
|
+
* @returns The Residue that contains the picked mesh, or null if it does not exist
|
|
549
|
+
*/
|
|
550
|
+
_getResidueFromMotifNode(node, meshUUID, sceneChildren) {
|
|
551
|
+
const motifUUID = node.uniqueId.toString();
|
|
552
|
+
if (!sceneChildren.has(motifUUID)) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
const motif = sceneChildren.get(motifUUID);
|
|
556
|
+
let residue = null;
|
|
557
|
+
motif.children.forEach((child) => {
|
|
558
|
+
if (child.hasMesh(meshUUID)) residue = child;
|
|
559
|
+
});
|
|
560
|
+
return residue;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// src/3D/RenderScene.ts
|
|
565
|
+
var RenderScene = class {
|
|
566
|
+
_canvas;
|
|
567
|
+
_scene;
|
|
568
|
+
_engine;
|
|
569
|
+
_camera;
|
|
570
|
+
// For access to motif objects in the scene
|
|
571
|
+
_children = /* @__PURE__ */ new Map();
|
|
572
|
+
// Event variables
|
|
573
|
+
_eventManager;
|
|
574
|
+
// State variables
|
|
575
|
+
_isDisposed = false;
|
|
576
|
+
isRunning = false;
|
|
577
|
+
constructor(canvas, hexColor, cameraPositionZ, renderWidth, renderHeight) {
|
|
578
|
+
this._canvas = canvas;
|
|
579
|
+
this._engine = new Engine(this._canvas, true);
|
|
580
|
+
this._engine.setSize(renderWidth, renderHeight);
|
|
581
|
+
this._scene = new Scene5(this._engine);
|
|
582
|
+
this._scene.clearColor = Color4.FromHexString(`${hexColor.replace(/^0x/, "")}`);
|
|
583
|
+
this._camera = new UniversalCamera("camera", new Vector33(0, 0, cameraPositionZ));
|
|
584
|
+
this._camera.setTarget(new Vector33(0, 0, 0));
|
|
585
|
+
this._camera.mode = Camera.ORTHOGRAPHIC_CAMERA;
|
|
586
|
+
this._camera.orthoTop = this._engine.getRenderHeight() / 2;
|
|
587
|
+
this._camera.orthoBottom = this._engine.getRenderHeight() / -2;
|
|
588
|
+
this._camera.orthoLeft = this._engine.getRenderWidth() / -2;
|
|
589
|
+
this._camera.orthoRight = this._engine.getRenderWidth() / 2;
|
|
590
|
+
this._camera.minZ = 10;
|
|
591
|
+
const light = new HemisphericLight("light", Vector33.Up(), this._scene);
|
|
592
|
+
light.intensity = 1.25;
|
|
593
|
+
this._eventManager = new EventManager();
|
|
594
|
+
window.addEventListener("resize", this._handleResize.bind(this));
|
|
595
|
+
}
|
|
596
|
+
// Start the render loop
|
|
597
|
+
start() {
|
|
598
|
+
if (this._isDisposed) {
|
|
599
|
+
throw new Error("Cannot start a disposed RenderScene.");
|
|
600
|
+
}
|
|
601
|
+
if (this.isRunning) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
this._eventManager.setupEventHandling(this);
|
|
605
|
+
this._engine.runRenderLoop(() => {
|
|
606
|
+
this._scene.render();
|
|
607
|
+
this._eventManager.notifyObservers(Events.EventType.RENDER, {
|
|
608
|
+
type: Events.EventType.RENDER,
|
|
609
|
+
canceled: false,
|
|
610
|
+
timestamp: performance.now()
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
this._eventManager.notifyObservers(Events.EventType.ENGINE_STARTED, {
|
|
614
|
+
type: Events.EventType.ENGINE_STARTED,
|
|
615
|
+
canceled: false,
|
|
616
|
+
timestamp: performance.now()
|
|
617
|
+
});
|
|
618
|
+
this.isRunning = true;
|
|
619
|
+
}
|
|
620
|
+
// Stop the render loop without disposing resources
|
|
621
|
+
stop() {
|
|
622
|
+
if (this.isRunning) {
|
|
623
|
+
this._engine.stopRenderLoop();
|
|
624
|
+
this._eventManager.notifyObservers(Events.EventType.ENGINE_STOPPED, {
|
|
625
|
+
type: Events.EventType.ENGINE_STOPPED,
|
|
626
|
+
canceled: false,
|
|
627
|
+
timestamp: performance.now()
|
|
628
|
+
});
|
|
629
|
+
this.isRunning = false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// Stop and dispose of all resources
|
|
633
|
+
dispose() {
|
|
634
|
+
this.stop();
|
|
635
|
+
window.removeEventListener("resize", this._handleResize);
|
|
636
|
+
this._eventManager.dispose();
|
|
637
|
+
this._scene.dispose();
|
|
638
|
+
this._engine.dispose();
|
|
639
|
+
this.isRunning = false;
|
|
640
|
+
this._isDisposed = true;
|
|
641
|
+
}
|
|
642
|
+
add(motif) {
|
|
643
|
+
if (this._children.has(motif.uuid)) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
this._reattachToScene(motif.node, motif);
|
|
647
|
+
this._children.set(motif.uuid, motif);
|
|
648
|
+
this._scene.addTransformNode(motif.node);
|
|
649
|
+
}
|
|
650
|
+
remove(motif) {
|
|
651
|
+
if (!this._children.has(motif.uuid)) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
this._children.delete(motif.uuid);
|
|
655
|
+
this._scene.removeTransformNode(motif.node);
|
|
656
|
+
}
|
|
657
|
+
setBackgroundColor(hexColor) {
|
|
658
|
+
this._scene.clearColor = Color4.FromHexString(`${hexColor.replace(/^0x/, "")}`);
|
|
659
|
+
}
|
|
660
|
+
_reattachToScene(node, currObj) {
|
|
661
|
+
node._scene = this._scene;
|
|
662
|
+
if (node instanceof Mesh2 && currObj instanceof MeshObject) {
|
|
663
|
+
const meshSerialized = node.serialize();
|
|
664
|
+
const verticeData = node.geometry?.serializeVerticeData();
|
|
665
|
+
const materialData = node.material?.serialize();
|
|
666
|
+
const mesh = Mesh2.Parse(meshSerialized, this._scene, "");
|
|
667
|
+
const geo = Geometry.Parse(verticeData, this._scene, "");
|
|
668
|
+
const mat = Material.Parse(materialData, this._scene, "");
|
|
669
|
+
geo?.applyToMesh(mesh);
|
|
670
|
+
mesh.material = mat;
|
|
671
|
+
currObj.setNewMesh(mesh);
|
|
672
|
+
node.dispose();
|
|
673
|
+
}
|
|
674
|
+
if (!(currObj instanceof MeshObject)) {
|
|
675
|
+
currObj.children.forEach((childObj) => {
|
|
676
|
+
if (childObj instanceof Residue) {
|
|
677
|
+
this._reattachToScene(childObj.node, childObj);
|
|
678
|
+
} else if (currObj instanceof Residue) {
|
|
679
|
+
this._reattachToScene(childObj.mesh, childObj);
|
|
680
|
+
childObj.setParent(currObj);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
_handleResize = () => {
|
|
686
|
+
this._engine.setSize(window.innerWidth, window.innerHeight);
|
|
687
|
+
this._camera.orthoTop = this._engine.getRenderHeight() / 2;
|
|
688
|
+
this._camera.orthoBottom = this._engine.getRenderHeight() / -2;
|
|
689
|
+
this._camera.orthoLeft = this._engine.getRenderWidth() / -2;
|
|
690
|
+
this._camera.orthoRight = this._engine.getRenderWidth() / 2;
|
|
691
|
+
this._eventManager.notifyObservers(Events.EventType.RESIZE, {
|
|
692
|
+
type: Events.EventType.RESIZE,
|
|
693
|
+
canceled: false,
|
|
694
|
+
timestamp: performance.now()
|
|
695
|
+
});
|
|
696
|
+
};
|
|
697
|
+
/**
|
|
698
|
+
* Returns the Babylon.js Scene object.
|
|
699
|
+
*/
|
|
700
|
+
get scene() {
|
|
701
|
+
return this._scene;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Returns the engine.
|
|
705
|
+
*/
|
|
706
|
+
get engine() {
|
|
707
|
+
return this._engine;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Returns the camera.
|
|
711
|
+
*/
|
|
712
|
+
get camera() {
|
|
713
|
+
return this._camera;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Returns the event manager
|
|
717
|
+
*/
|
|
718
|
+
get eventManager() {
|
|
719
|
+
return this._eventManager;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Returns the set of children.
|
|
723
|
+
*/
|
|
724
|
+
get children() {
|
|
725
|
+
return this._children;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Returns the current render width
|
|
729
|
+
*/
|
|
730
|
+
get renderWidth() {
|
|
731
|
+
return this._engine.getRenderWidth();
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Returns the current render height
|
|
735
|
+
*/
|
|
736
|
+
get renderHeight() {
|
|
737
|
+
return this._engine.getRenderHeight();
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
// src/3D/utils/GetAtomInfo.ts
|
|
742
|
+
async function parseAtomCoords(fileName) {
|
|
743
|
+
const response = await fetch(`/${fileName}`);
|
|
744
|
+
const jsonData = await response.json();
|
|
745
|
+
const coordinates = [];
|
|
746
|
+
for (const [key] of Object.entries(jsonData)) {
|
|
747
|
+
const atomMap = jsonData[key][0];
|
|
748
|
+
if (atomMap[`"C1'"`]) {
|
|
749
|
+
coordinates.push(
|
|
750
|
+
new Vec3(
|
|
751
|
+
parseFloat(atomMap[`"C1'"`][0]),
|
|
752
|
+
parseFloat(atomMap[`"C1'"`][1]),
|
|
753
|
+
parseFloat(atomMap[`"C1'"`][2])
|
|
754
|
+
)
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return coordinates;
|
|
759
|
+
}
|
|
760
|
+
async function getAtomCoords(fileName) {
|
|
761
|
+
const coordinates = await parseAtomCoords(fileName);
|
|
762
|
+
return coordinates;
|
|
763
|
+
}
|
|
764
|
+
async function updateAllMotifs(motifMeshArray) {
|
|
765
|
+
const fileNames = [];
|
|
766
|
+
for (const motif of motifMeshArray) {
|
|
767
|
+
fileNames.push(motif.userData.fileName);
|
|
768
|
+
}
|
|
769
|
+
const atomInfoLists = await Promise.all(fileNames.map((o) => getAtomCoords(o)));
|
|
770
|
+
for (let i = 0; i < motifMeshArray.length; i += 1) {
|
|
771
|
+
motifMeshArray[i].userData.atomInfo = atomInfoLists[i];
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/3D/utils/GetPoints.ts
|
|
776
|
+
function getPoints(nucleotideData) {
|
|
777
|
+
const vertices = [];
|
|
778
|
+
const indices = [];
|
|
779
|
+
for (let i = 1; i < nucleotideData.length; i += 2) {
|
|
780
|
+
vertices.push(nucleotideData[i]);
|
|
781
|
+
indices.push(nucleotideData[i + 1]);
|
|
782
|
+
}
|
|
783
|
+
return { vertices, indices };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/3D/utils/GetMotif.ts
|
|
787
|
+
async function getMotif(motifJSONFileName, motifColorHex = "0xcc2900") {
|
|
788
|
+
const motif = new Motif(`${motifJSONFileName}_motif`);
|
|
789
|
+
const motifJSONFileData = await fetch(`/${motifJSONFileName}`);
|
|
790
|
+
const jsonObject = await motifJSONFileData.json();
|
|
791
|
+
for (const [key] of Object.entries(jsonObject)) {
|
|
792
|
+
const { vertices, indices } = getPoints(jsonObject[key]);
|
|
793
|
+
const residue = new Residue("residue");
|
|
794
|
+
const backboneMesh = new MeshObject(`backbone_${key}`);
|
|
795
|
+
backboneMesh.applyVertexData(vertices[0], indices[0]);
|
|
796
|
+
backboneMesh.createAndSetMaterial(motifColorHex);
|
|
797
|
+
const ringMesh = new MeshObject(`ring_${key}`);
|
|
798
|
+
ringMesh.applyVertexData(vertices[1], indices[1]);
|
|
799
|
+
ringMesh.createAndSetMaterial(motifColorHex);
|
|
800
|
+
residue.addChild(backboneMesh);
|
|
801
|
+
residue.addChild(ringMesh);
|
|
802
|
+
motif.addChild(residue);
|
|
803
|
+
}
|
|
804
|
+
motif.userData.fileName = motifJSONFileName;
|
|
805
|
+
return motif;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/Canvas/Canvas.tsx
|
|
809
|
+
import React, { useRef, useEffect, useState } from "react";
|
|
810
|
+
|
|
811
|
+
// src/Canvas/CanvasDataManager.ts
|
|
812
|
+
var CanvasAttributeTypes = /* @__PURE__ */ ((CanvasAttributeTypes2) => {
|
|
813
|
+
CanvasAttributeTypes2["SELECTED_MOTIFS"] = "selectedMotifs";
|
|
814
|
+
CanvasAttributeTypes2["LOCKED_MOTIF_IDS"] = "lockedMotifIds";
|
|
815
|
+
CanvasAttributeTypes2["HLOCKED_MOTIF_IDS"] = "hardLockedMotifIds";
|
|
816
|
+
CanvasAttributeTypes2["SCORE_RMSD"] = "scoreRMSD";
|
|
817
|
+
CanvasAttributeTypes2["KABSCH_RMSD"] = "kabschRMSD";
|
|
818
|
+
return CanvasAttributeTypes2;
|
|
819
|
+
})(CanvasAttributeTypes || {});
|
|
820
|
+
var CanvasDataManager = class {
|
|
821
|
+
static _selectedMotifIds = /* @__PURE__ */ new Set();
|
|
822
|
+
static _lockedMotifIds = [];
|
|
823
|
+
static _hardLockedMotifIds = [];
|
|
824
|
+
static _scoreRMSD = [];
|
|
825
|
+
static _kabschRMSD = [];
|
|
826
|
+
static _listeners = /* @__PURE__ */ new Map();
|
|
827
|
+
// private static _backgroundColor: string = '#040a20';
|
|
828
|
+
static get selectedMotifIds() {
|
|
829
|
+
return this._selectedMotifIds;
|
|
830
|
+
}
|
|
831
|
+
static setSelectedMotifIds(selectedMotifIds) {
|
|
832
|
+
this._selectedMotifIds = selectedMotifIds;
|
|
833
|
+
this._listeners.get("selectedMotifs" /* SELECTED_MOTIFS */)?.forEach((fn) => fn());
|
|
834
|
+
}
|
|
835
|
+
static get lockedMotifIds() {
|
|
836
|
+
return this._lockedMotifIds;
|
|
837
|
+
}
|
|
838
|
+
static setLockedMotifIds(lockedMotifIds) {
|
|
839
|
+
this._lockedMotifIds = lockedMotifIds;
|
|
840
|
+
this._listeners.get("lockedMotifIds" /* LOCKED_MOTIF_IDS */)?.forEach((fn) => fn());
|
|
841
|
+
}
|
|
842
|
+
static get hardLockedMotifIds() {
|
|
843
|
+
return this._hardLockedMotifIds;
|
|
844
|
+
}
|
|
845
|
+
static setHardLockedMotifIds(hardLockedMotifIds) {
|
|
846
|
+
this._hardLockedMotifIds = hardLockedMotifIds;
|
|
847
|
+
this._listeners.get("hardLockedMotifIds" /* HLOCKED_MOTIF_IDS */)?.forEach((fn) => fn());
|
|
848
|
+
}
|
|
849
|
+
static get scoreRMSD() {
|
|
850
|
+
return this._scoreRMSD;
|
|
851
|
+
}
|
|
852
|
+
static setScoreRMSD(scoreRMSD) {
|
|
853
|
+
this._scoreRMSD = scoreRMSD;
|
|
854
|
+
this._listeners.get("scoreRMSD" /* SCORE_RMSD */)?.forEach((fn) => fn());
|
|
855
|
+
}
|
|
856
|
+
static get kabschRMSD() {
|
|
857
|
+
return this._kabschRMSD;
|
|
858
|
+
}
|
|
859
|
+
static setKabschRMSD(kabschRMSD) {
|
|
860
|
+
this._kabschRMSD = kabschRMSD;
|
|
861
|
+
this._listeners.get("kabschRMSD" /* KABSCH_RMSD */)?.forEach((fn) => fn());
|
|
862
|
+
}
|
|
863
|
+
static subscribe(canvasAttributeType, callback) {
|
|
864
|
+
if (!this._listeners.has(canvasAttributeType)) {
|
|
865
|
+
this._listeners.set(canvasAttributeType, /* @__PURE__ */ new Set());
|
|
866
|
+
}
|
|
867
|
+
this._listeners.get(canvasAttributeType).add(callback);
|
|
868
|
+
return () => this._listeners.get(canvasAttributeType).delete(callback);
|
|
869
|
+
}
|
|
870
|
+
// static get backgroundColor(): string {
|
|
871
|
+
// return this._backgroundColor;
|
|
872
|
+
// }
|
|
873
|
+
// static setBackgroundColor(backgroundColor: string): void {
|
|
874
|
+
// this._backgroundColor = backgroundColor;
|
|
875
|
+
// }
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// src/Kabsch/Kabsch.ts
|
|
879
|
+
import numeric from "numeric";
|
|
880
|
+
|
|
881
|
+
// src/RMSD/RotateAtoms.ts
|
|
882
|
+
function rotateAllPoints(atomCoords, quat) {
|
|
883
|
+
const newCoordinates = [];
|
|
884
|
+
for (let i = 0; i < atomCoords.length; i += 1) {
|
|
885
|
+
newCoordinates.push(atomCoords[i].clone());
|
|
886
|
+
newCoordinates[i].applyQuaternion(quat);
|
|
887
|
+
}
|
|
888
|
+
return newCoordinates;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/RMSD/RMSDSlidingWindow.ts
|
|
892
|
+
function getRMSD(a, b) {
|
|
893
|
+
let newCoords1;
|
|
894
|
+
let newCoords2;
|
|
895
|
+
if (!(Array.isArray(a) || Array.isArray(b))) {
|
|
896
|
+
if (a instanceof Motif && b instanceof Motif) {
|
|
897
|
+
newCoords1 = rotateAllPoints(a.userData.atomInfo, a.quat);
|
|
898
|
+
newCoords2 = rotateAllPoints(b.userData.atomInfo, b.quat);
|
|
899
|
+
} else {
|
|
900
|
+
return -1;
|
|
901
|
+
}
|
|
902
|
+
} else if (a.every((item) => item instanceof Vec3)) {
|
|
903
|
+
newCoords1 = a;
|
|
904
|
+
newCoords2 = b;
|
|
905
|
+
} else {
|
|
906
|
+
return -1;
|
|
907
|
+
}
|
|
908
|
+
const squaredDistances = newCoords1.reduce((sum, coord1, index) => {
|
|
909
|
+
const coord2 = newCoords2[index];
|
|
910
|
+
const distanceSquared = (coord1.x - coord2.x) ** 2 + (coord1.y - coord2.y) ** 2 + (coord1.z - coord2.z) ** 2;
|
|
911
|
+
return sum + distanceSquared;
|
|
912
|
+
}, 0);
|
|
913
|
+
const minLength = newCoords1.length;
|
|
914
|
+
const meanSquaredDistance = squaredDistances / minLength;
|
|
915
|
+
return Math.sqrt(meanSquaredDistance);
|
|
916
|
+
}
|
|
917
|
+
function calculateRMSDSlide(coordinates1, coordinates2) {
|
|
918
|
+
try {
|
|
919
|
+
const minLength = Math.min(coordinates1.length, coordinates2.length);
|
|
920
|
+
const smallObj = minLength === coordinates1.length ? coordinates1 : coordinates2;
|
|
921
|
+
const largeObj = minLength === coordinates1.length ? coordinates2 : coordinates1;
|
|
922
|
+
const iterations = largeObj.length - smallObj.length + 1;
|
|
923
|
+
const trimmedCoordinates1 = smallObj.slice(0, minLength);
|
|
924
|
+
let minRMSD = 1e3;
|
|
925
|
+
for (let startPosition = 0; startPosition < iterations; startPosition += 1) {
|
|
926
|
+
const trimmedCoordinates2 = largeObj.slice(startPosition, startPosition + minLength);
|
|
927
|
+
const rmsd = getRMSD(trimmedCoordinates1, trimmedCoordinates2);
|
|
928
|
+
if (rmsd < minRMSD) {
|
|
929
|
+
minRMSD = rmsd;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return minRMSD;
|
|
933
|
+
} catch (error) {
|
|
934
|
+
throw new Error("Error in RMSD calculation");
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/RMSD/GetRMSD.ts
|
|
939
|
+
function calculateRMSD(selectedMotifMeshArray, motifMeshArray) {
|
|
940
|
+
const idToIdx = {};
|
|
941
|
+
const rotatedCoordinates = [];
|
|
942
|
+
for (let i = 0; i < motifMeshArray.length; i += 1) {
|
|
943
|
+
rotatedCoordinates.push(rotateAllPoints(
|
|
944
|
+
motifMeshArray[i].userData.atomInfo,
|
|
945
|
+
motifMeshArray[i].quat
|
|
946
|
+
));
|
|
947
|
+
idToIdx[motifMeshArray[i].uuid] = i;
|
|
948
|
+
}
|
|
949
|
+
const scores = [];
|
|
950
|
+
for (let i = 0; i < selectedMotifMeshArray.length; i += 1) {
|
|
951
|
+
const currScores = [];
|
|
952
|
+
for (let j = 0; j < motifMeshArray.length; j += 1) {
|
|
953
|
+
if (selectedMotifMeshArray[i].uuid !== motifMeshArray[j].uuid) {
|
|
954
|
+
const score = calculateRMSDSlide(
|
|
955
|
+
rotatedCoordinates[idToIdx[selectedMotifMeshArray[i].uuid]],
|
|
956
|
+
rotatedCoordinates[idToIdx[motifMeshArray[j].uuid]]
|
|
957
|
+
);
|
|
958
|
+
currScores.push({
|
|
959
|
+
score,
|
|
960
|
+
selId: selectedMotifMeshArray[i].uuid,
|
|
961
|
+
refId: motifMeshArray[j].uuid
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (currScores.length !== 0) {
|
|
966
|
+
scores.push(currScores);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return scores;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/Kabsch/Kabsch.ts
|
|
973
|
+
function dot(matrix1, matrix2) {
|
|
974
|
+
const result = [];
|
|
975
|
+
for (let i = 0; i < matrix1.length; i += 1) {
|
|
976
|
+
result[i] = [];
|
|
977
|
+
for (let j = 0; j < 3; j += 1) {
|
|
978
|
+
result[i][j] = 0;
|
|
979
|
+
for (let k = 0; k < matrix1[0].length; k += 1) {
|
|
980
|
+
result[i][j] += matrix1[i][k] * matrix2[k][j];
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return result;
|
|
985
|
+
}
|
|
986
|
+
function calculateKabsch(coordinates1, coordinates2) {
|
|
987
|
+
try {
|
|
988
|
+
const convertedCoordinates1 = [];
|
|
989
|
+
for (let i = 0; i < coordinates1.length; i += 1) {
|
|
990
|
+
convertedCoordinates1.push([coordinates1[i].x, coordinates1[i].y, coordinates1[i].z]);
|
|
991
|
+
}
|
|
992
|
+
const convertedCoordinates2 = [];
|
|
993
|
+
for (let i = 0; i < coordinates2.length; i += 1) {
|
|
994
|
+
convertedCoordinates2.push([coordinates2[i].x, coordinates2[i].y, coordinates2[i].z]);
|
|
995
|
+
}
|
|
996
|
+
const covarianceMatrix = dot(numeric.transpose(convertedCoordinates1), convertedCoordinates2);
|
|
997
|
+
const { U, V } = numeric.svd(covarianceMatrix);
|
|
998
|
+
let rotationMatrix = numeric.dot(V, numeric.transpose(U));
|
|
999
|
+
if (numeric.det(rotationMatrix) < 0) {
|
|
1000
|
+
V[2] = V[2].map((val) => -val);
|
|
1001
|
+
rotationMatrix = dot(V, numeric.transpose(U));
|
|
1002
|
+
}
|
|
1003
|
+
return rotationMatrix;
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
throw new Error("error");
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function getKabschCoords(coordinates2, mat) {
|
|
1009
|
+
const flatMatrix = mat.flat();
|
|
1010
|
+
const newMat4 = new Matrix4().fromArray(flatMatrix);
|
|
1011
|
+
const quat = new Quat().setFromMatrix(newMat4);
|
|
1012
|
+
return rotateAllPoints(coordinates2, quat);
|
|
1013
|
+
}
|
|
1014
|
+
function updateCoords(motif1, motif2) {
|
|
1015
|
+
const rotatedCoords = [];
|
|
1016
|
+
rotatedCoords.push(rotateAllPoints(motif1.userData.atomInfo, motif1.quat));
|
|
1017
|
+
rotatedCoords.push(rotateAllPoints(motif2.userData.atomInfo, motif2.quat));
|
|
1018
|
+
return rotatedCoords;
|
|
1019
|
+
}
|
|
1020
|
+
function kabschSlidingWindow(motif1, motif2) {
|
|
1021
|
+
try {
|
|
1022
|
+
const rotatedCoords = updateCoords(motif1, motif2);
|
|
1023
|
+
const coordinates1 = rotatedCoords[0].slice();
|
|
1024
|
+
const coordinates2 = rotatedCoords[1].slice();
|
|
1025
|
+
const minLength = Math.min(coordinates1.length, coordinates2.length);
|
|
1026
|
+
const smallObj = minLength === coordinates1.length ? coordinates1 : coordinates2;
|
|
1027
|
+
const largeObj = minLength === coordinates1.length ? coordinates2 : coordinates1;
|
|
1028
|
+
const iterations = largeObj.length - smallObj.length + 1;
|
|
1029
|
+
const trimmedCoordinates1 = smallObj.slice(0, minLength);
|
|
1030
|
+
let minRMSD = 1e3;
|
|
1031
|
+
let bestRotation = [];
|
|
1032
|
+
for (let startPosition = 0; startPosition < iterations; startPosition += 1) {
|
|
1033
|
+
const trimmedCoordinates2 = largeObj.slice(startPosition, startPosition + minLength);
|
|
1034
|
+
const optimalRotation = calculateKabsch(trimmedCoordinates1, trimmedCoordinates2);
|
|
1035
|
+
const kabschPoints = getKabschCoords(trimmedCoordinates2, optimalRotation);
|
|
1036
|
+
const rmsd = getRMSD(trimmedCoordinates1, kabschPoints);
|
|
1037
|
+
if (rmsd < minRMSD) {
|
|
1038
|
+
minRMSD = rmsd;
|
|
1039
|
+
bestRotation = optimalRotation;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return { matrix: bestRotation, rmsd: minRMSD };
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
throw new Error(`Error in Kabsch calculation between motifs ${motif1.uuid} and ${motif2.uuid}.`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function calculateAllKabschRMSD(motifMeshArray) {
|
|
1048
|
+
const res = [];
|
|
1049
|
+
for (let i = 0; i < motifMeshArray.length; i += 1) {
|
|
1050
|
+
res.push([]);
|
|
1051
|
+
for (let j = 0; j < motifMeshArray.length; j += 1) {
|
|
1052
|
+
res[i].push(0);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
for (let i = 0; i < motifMeshArray.length - 1; i += 1) {
|
|
1056
|
+
for (let j = i + 1; j < motifMeshArray.length; j += 1) {
|
|
1057
|
+
const { rmsd } = kabschSlidingWindow(motifMeshArray[i], motifMeshArray[j]);
|
|
1058
|
+
res[i][j] = rmsd;
|
|
1059
|
+
res[j][i] = rmsd;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return res;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/Canvas/Canvas.tsx
|
|
1066
|
+
function Canvas({
|
|
1067
|
+
rendererHeight = 500,
|
|
1068
|
+
rendererWidth = 500,
|
|
1069
|
+
rendererBackgroundColor = "#040a20",
|
|
1070
|
+
rendererSizeIsWindow = false,
|
|
1071
|
+
cameraPositionZ = 1e3,
|
|
1072
|
+
motifProps,
|
|
1073
|
+
customEventProps
|
|
1074
|
+
}) {
|
|
1075
|
+
const canvasRef = useRef(null);
|
|
1076
|
+
const scene = useRef(null);
|
|
1077
|
+
const motifs = [];
|
|
1078
|
+
let hardLockedMotifIds = [];
|
|
1079
|
+
motifProps.forEach((motifProp) => {
|
|
1080
|
+
motifs.push(motifProp.motif);
|
|
1081
|
+
if (motifProp.locked) hardLockedMotifIds.push(motifProp.motif.uuid);
|
|
1082
|
+
});
|
|
1083
|
+
CanvasDataManager.setHardLockedMotifIds(hardLockedMotifIds);
|
|
1084
|
+
const selectedMotifMeshState = useRef(/* @__PURE__ */ new Set());
|
|
1085
|
+
const lockedMotifIdState = useRef([]);
|
|
1086
|
+
const [cursorStyle, setCursorStyle] = useState("auto");
|
|
1087
|
+
const [selectedMotifIds, setSelectedmotifIds] = useState(/* @__PURE__ */ new Set());
|
|
1088
|
+
const [scoreRMSD, setScoreRMSD] = useState([]);
|
|
1089
|
+
const [kabschRMSD, setKabschRMSD] = useState([]);
|
|
1090
|
+
const [lockedMotifIds, setLockedMotifIds] = useState([]);
|
|
1091
|
+
const addMotif = (motif) => {
|
|
1092
|
+
if (selectedMotifIds.has(motif.uuid)) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const newSet = /* @__PURE__ */ new Set();
|
|
1096
|
+
for (let i = 0; i < motifs.length; i += 1) {
|
|
1097
|
+
if (selectedMotifMeshState.current.has(motifs[i]) || motifs[i].uuid === motif.uuid) {
|
|
1098
|
+
newSet.add(motifs[i].uuid);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
setSelectedmotifIds(newSet);
|
|
1102
|
+
selectedMotifMeshState.current.add(motif);
|
|
1103
|
+
};
|
|
1104
|
+
const removeMotif = (motif) => {
|
|
1105
|
+
if (!selectedMotifIds.has(motif.uuid)) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
selectedMotifMeshState.current.delete(motif);
|
|
1109
|
+
setSelectedmotifIds((prevState) => {
|
|
1110
|
+
const newState = prevState;
|
|
1111
|
+
newState.delete(motif.uuid);
|
|
1112
|
+
return newState;
|
|
1113
|
+
});
|
|
1114
|
+
};
|
|
1115
|
+
function updateGlow() {
|
|
1116
|
+
motifs.forEach((motif) => {
|
|
1117
|
+
motif.children.forEach((residue) => {
|
|
1118
|
+
residue.children.forEach((childMesh) => {
|
|
1119
|
+
if (selectedMotifIds.has(motif.uuid)) {
|
|
1120
|
+
childMesh.applyHighlight();
|
|
1121
|
+
} else {
|
|
1122
|
+
childMesh.resetHighlight();
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
function onSelectMotif(event) {
|
|
1129
|
+
if (event.type !== Events.EventType.OBJECT_SELECTED) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const { motif } = event;
|
|
1133
|
+
if (!motif || selectedMotifMeshState.current.has(motif) || lockedMotifIdState.current.includes(motif.uuid) || hardLockedMotifIds.includes(motif.uuid)) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (event.multiSelect && motif) {
|
|
1137
|
+
addMotif(motif);
|
|
1138
|
+
} else if (motif) {
|
|
1139
|
+
setSelectedmotifIds(/* @__PURE__ */ new Set([motif.uuid]));
|
|
1140
|
+
selectedMotifMeshState.current = /* @__PURE__ */ new Set([motif]);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
function onDeselectMotif(event) {
|
|
1144
|
+
if (event.type !== Events.EventType.OBJECT_DESELECTED) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (selectedMotifMeshState.current.size === 0) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
setSelectedmotifIds(/* @__PURE__ */ new Set());
|
|
1151
|
+
selectedMotifMeshState.current.clear();
|
|
1152
|
+
}
|
|
1153
|
+
function onMouseMove(event) {
|
|
1154
|
+
if (event.deltaX === 0 && event.deltaY === 0) {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (event.buttons === 2 && selectedMotifMeshState.current.size > 0) {
|
|
1158
|
+
if (scene.current) {
|
|
1159
|
+
const { renderWidth, renderHeight } = scene.current;
|
|
1160
|
+
const rawDeltaX = event.deltaX;
|
|
1161
|
+
const rawDeltaY = event.deltaY;
|
|
1162
|
+
const deltaX = rawDeltaX / renderWidth * canvasRef.current.width;
|
|
1163
|
+
const deltaY = rawDeltaY / renderHeight * canvasRef.current.height;
|
|
1164
|
+
selectedMotifMeshState.current.forEach((element) => {
|
|
1165
|
+
if (!lockedMotifIdState.current.includes(element.uuid) && !hardLockedMotifIds.includes(element.uuid)) {
|
|
1166
|
+
element.translate(-deltaX, -deltaY, 0);
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
} else if (event.buttons === 1 && selectedMotifMeshState.current.size > 0) {
|
|
1171
|
+
const { deltaX, deltaY } = event;
|
|
1172
|
+
const directionVec = new Vec3(-deltaX, -deltaY, 0);
|
|
1173
|
+
const axisVec = directionVec.clone().applyAxisAngle(new Vec3(0, 0, 1), Math.PI / 2);
|
|
1174
|
+
axisVec.normalize();
|
|
1175
|
+
if (scene.current) {
|
|
1176
|
+
const angle = directionVec.length() / scene.current.renderWidth * (3 * Math.PI);
|
|
1177
|
+
selectedMotifMeshState.current.forEach((element) => {
|
|
1178
|
+
if (!lockedMotifIdState.current.includes(element.uuid) && !hardLockedMotifIds.includes(element.uuid)) {
|
|
1179
|
+
element.rotate(axisVec, angle);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
setScoreRMSD(calculateRMSD(Array.from(selectedMotifMeshState.current), motifs));
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
function onMouseScroll(event) {
|
|
1187
|
+
if (!event.originalEvent || !(event.originalEvent instanceof WheelEvent)) {
|
|
1188
|
+
throw new Error("Tried to trigger Wheel event, but the event info was not present or not of the correct type");
|
|
1189
|
+
}
|
|
1190
|
+
event.originalEvent.preventDefault();
|
|
1191
|
+
if (selectedMotifMeshState.current.size > 0) {
|
|
1192
|
+
const zoomSpeed = 0.1;
|
|
1193
|
+
const zoomDirection = event.originalEvent.deltaY > 0 ? -1 : 1;
|
|
1194
|
+
selectedMotifMeshState.current.forEach((element) => {
|
|
1195
|
+
if (!lockedMotifIdState.current.includes(element.uuid) && !hardLockedMotifIds.includes(element.uuid)) {
|
|
1196
|
+
const scaleFactor = 1 + zoomDirection * zoomSpeed;
|
|
1197
|
+
element.multiplyScalar(scaleFactor);
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function onMouseUp(event) {
|
|
1203
|
+
if (event.type !== Events.EventType.POINTER_UP) {
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
setCursorStyle("auto");
|
|
1207
|
+
}
|
|
1208
|
+
function onMouseDown(event) {
|
|
1209
|
+
if (event.button === 2 && selectedMotifMeshState.current.size > 0) {
|
|
1210
|
+
setCursorStyle("move");
|
|
1211
|
+
} else if (event.button === 1 && event.ctrlKey) {
|
|
1212
|
+
setCursorStyle("crosshair");
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
function onKeyboardRotate(event) {
|
|
1216
|
+
if (event.rotationAxis.equals(Vec3.Zero)) {
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const angle = event.rotationAxis.length() / 500 * (6 * Math.PI);
|
|
1220
|
+
selectedMotifMeshState.current.forEach((element) => {
|
|
1221
|
+
if (!lockedMotifIdState.current.includes(element.uuid) && !hardLockedMotifIds.includes(element.uuid)) {
|
|
1222
|
+
element.rotate(event.rotationAxis, angle);
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
setScoreRMSD(calculateRMSD(Array.from(selectedMotifMeshState.current), motifs));
|
|
1226
|
+
}
|
|
1227
|
+
function onKeyboardTranslate(event) {
|
|
1228
|
+
if (event.translationDirection.equals(Vec3.Zero)) {
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
event.translationDirection.multiplyScalar(0.5);
|
|
1232
|
+
selectedMotifMeshState.current.forEach((element) => {
|
|
1233
|
+
if (!lockedMotifIdState.current.includes(element.uuid) && !hardLockedMotifIds.includes(element.uuid)) {
|
|
1234
|
+
element.translate(
|
|
1235
|
+
event.translationDirection.x,
|
|
1236
|
+
event.translationDirection.y,
|
|
1237
|
+
event.translationDirection.z
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
function onKeyboardSelect(event) {
|
|
1243
|
+
if (!event.rotationAxis.equals(Vec3.Zero) || !event.translationDirection.equals(Vec3.Zero)) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (!/^[0-9]$/.test(event.key)) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const motif = motifs[Number(event.key) - 1];
|
|
1250
|
+
if (selectedMotifMeshState.current.has(motif)) {
|
|
1251
|
+
removeMotif(motif);
|
|
1252
|
+
} else {
|
|
1253
|
+
addMotif(motif);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
const calculatePositions = (numMotifs) => {
|
|
1257
|
+
if (!canvasRef.current) {
|
|
1258
|
+
return [];
|
|
1259
|
+
}
|
|
1260
|
+
const totalWidth = canvasRef.current.width;
|
|
1261
|
+
const subdividedWidth = totalWidth / numMotifs;
|
|
1262
|
+
const halfWidth = subdividedWidth / 2;
|
|
1263
|
+
const totalHeight = canvasRef.current.height;
|
|
1264
|
+
const subdividedHeight = totalHeight * (numMotifs > 3 ? 1 : 0) / 5;
|
|
1265
|
+
const positions = [];
|
|
1266
|
+
for (let i = 0, x = totalWidth / 2; i < numMotifs; i += 1, x -= subdividedWidth) {
|
|
1267
|
+
positions.push(new Vec3(x - halfWidth, subdividedHeight * (i % 2 ? 1 : -1), -100));
|
|
1268
|
+
}
|
|
1269
|
+
return positions;
|
|
1270
|
+
};
|
|
1271
|
+
useEffect(() => {
|
|
1272
|
+
const unsubscribe = CanvasDataManager.subscribe("selectedMotifs" /* SELECTED_MOTIFS */, () => {
|
|
1273
|
+
setSelectedmotifIds(CanvasDataManager.selectedMotifIds);
|
|
1274
|
+
});
|
|
1275
|
+
return () => unsubscribe();
|
|
1276
|
+
}, []);
|
|
1277
|
+
useEffect(() => {
|
|
1278
|
+
if (CanvasDataManager.selectedMotifIds !== selectedMotifIds) {
|
|
1279
|
+
CanvasDataManager.setSelectedMotifIds(selectedMotifIds);
|
|
1280
|
+
}
|
|
1281
|
+
selectedMotifMeshState.current.clear();
|
|
1282
|
+
motifs.forEach((motif) => {
|
|
1283
|
+
if (selectedMotifIds.has(motif.uuid)) {
|
|
1284
|
+
selectedMotifMeshState.current.add(motif);
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
updateGlow();
|
|
1288
|
+
setScoreRMSD(calculateRMSD(Array.from(selectedMotifMeshState.current), motifs));
|
|
1289
|
+
}, [selectedMotifIds]);
|
|
1290
|
+
useEffect(() => {
|
|
1291
|
+
const unsubscribe = CanvasDataManager.subscribe("lockedMotifIds" /* LOCKED_MOTIF_IDS */, () => {
|
|
1292
|
+
setLockedMotifIds(CanvasDataManager.lockedMotifIds);
|
|
1293
|
+
});
|
|
1294
|
+
return () => unsubscribe();
|
|
1295
|
+
}, []);
|
|
1296
|
+
useEffect(() => {
|
|
1297
|
+
lockedMotifIdState.current = lockedMotifIds;
|
|
1298
|
+
if (CanvasDataManager.lockedMotifIds !== lockedMotifIds) {
|
|
1299
|
+
CanvasDataManager.setLockedMotifIds(lockedMotifIds);
|
|
1300
|
+
}
|
|
1301
|
+
}, [lockedMotifIds]);
|
|
1302
|
+
useEffect(() => {
|
|
1303
|
+
hardLockedMotifIds = CanvasDataManager.hardLockedMotifIds;
|
|
1304
|
+
}, [CanvasDataManager.hardLockedMotifIds]);
|
|
1305
|
+
useEffect(() => {
|
|
1306
|
+
const unsubscribe = CanvasDataManager.subscribe("scoreRMSD" /* SCORE_RMSD */, () => {
|
|
1307
|
+
setScoreRMSD(CanvasDataManager.scoreRMSD);
|
|
1308
|
+
});
|
|
1309
|
+
return () => unsubscribe();
|
|
1310
|
+
}, []);
|
|
1311
|
+
useEffect(() => {
|
|
1312
|
+
if (CanvasDataManager.scoreRMSD !== scoreRMSD) {
|
|
1313
|
+
CanvasDataManager.setScoreRMSD(scoreRMSD);
|
|
1314
|
+
}
|
|
1315
|
+
}, [scoreRMSD]);
|
|
1316
|
+
useEffect(() => {
|
|
1317
|
+
const unsubscribe = CanvasDataManager.subscribe("kabschRMSD" /* KABSCH_RMSD */, () => {
|
|
1318
|
+
setKabschRMSD(CanvasDataManager.kabschRMSD);
|
|
1319
|
+
});
|
|
1320
|
+
return () => unsubscribe();
|
|
1321
|
+
}, []);
|
|
1322
|
+
useEffect(() => {
|
|
1323
|
+
if (CanvasDataManager.kabschRMSD !== kabschRMSD) {
|
|
1324
|
+
CanvasDataManager.setKabschRMSD(kabschRMSD);
|
|
1325
|
+
}
|
|
1326
|
+
}, [kabschRMSD]);
|
|
1327
|
+
useEffect(() => {
|
|
1328
|
+
document.body.style.cursor = cursorStyle;
|
|
1329
|
+
}, [cursorStyle]);
|
|
1330
|
+
useEffect(() => {
|
|
1331
|
+
scene.current?.setBackgroundColor(rendererBackgroundColor);
|
|
1332
|
+
}, [rendererBackgroundColor]);
|
|
1333
|
+
useEffect(() => {
|
|
1334
|
+
if (!canvasRef.current) return;
|
|
1335
|
+
if (!scene.current) {
|
|
1336
|
+
scene.current = new RenderScene(
|
|
1337
|
+
canvasRef.current,
|
|
1338
|
+
rendererBackgroundColor,
|
|
1339
|
+
cameraPositionZ,
|
|
1340
|
+
rendererSizeIsWindow ? window.innerWidth : rendererWidth,
|
|
1341
|
+
rendererSizeIsWindow ? window.innerHeight : rendererHeight
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
const positions = calculatePositions(motifs.length);
|
|
1345
|
+
if (motifs.length > 0) {
|
|
1346
|
+
updateAllMotifs(motifs).then(() => {
|
|
1347
|
+
setKabschRMSD(calculateAllKabschRMSD(motifs));
|
|
1348
|
+
});
|
|
1349
|
+
if (scene.current.children.size !== motifs.length) {
|
|
1350
|
+
motifs.forEach((motifMesh, index) => {
|
|
1351
|
+
if (motifProps[index].position) positions[index] = motifProps[index].position;
|
|
1352
|
+
motifMesh.setPosition(positions[index].x, positions[index].y, positions[index].z);
|
|
1353
|
+
if (motifProps[index].rotation) motifMesh.setQuaternion(motifProps[index].rotation);
|
|
1354
|
+
motifMesh.multiplyScalar(canvasRef.current.width / 250);
|
|
1355
|
+
scene.current?.add(motifMesh);
|
|
1356
|
+
});
|
|
1357
|
+
const eventManager = scene.current?.eventManager;
|
|
1358
|
+
eventManager.on(Events.EventType.OBJECT_SELECTED, onSelectMotif);
|
|
1359
|
+
eventManager.on(Events.EventType.OBJECT_DESELECTED, onDeselectMotif);
|
|
1360
|
+
eventManager.on(Events.EventType.POINTER_MOVE, onMouseMove);
|
|
1361
|
+
eventManager.on(Events.EventType.POINTER_WHEEL, onMouseScroll);
|
|
1362
|
+
eventManager.on(Events.EventType.POINTER_DOWN, onMouseDown);
|
|
1363
|
+
eventManager.on(Events.EventType.POINTER_UP, onMouseUp);
|
|
1364
|
+
eventManager.on(Events.EventType.KEY_DOWN, onKeyboardRotate);
|
|
1365
|
+
eventManager.on(Events.EventType.KEY_DOWN, onKeyboardTranslate);
|
|
1366
|
+
eventManager.on(Events.EventType.KEY_DOWN, onKeyboardSelect);
|
|
1367
|
+
if (customEventProps) {
|
|
1368
|
+
customEventProps.forEach((customEventProp) => {
|
|
1369
|
+
eventManager.on(
|
|
1370
|
+
customEventProp.eventType,
|
|
1371
|
+
customEventProp.callback
|
|
1372
|
+
);
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
} else {
|
|
1376
|
+
motifs.forEach((motifMesh) => {
|
|
1377
|
+
scene.current?.add(motifMesh);
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
scene.current?.start();
|
|
1382
|
+
}, [rendererWidth, rendererHeight, rendererSizeIsWindow, motifProps]);
|
|
1383
|
+
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("canvas", { ref: canvasRef }));
|
|
1384
|
+
}
|
|
1385
|
+
export {
|
|
1386
|
+
Canvas,
|
|
1387
|
+
CanvasAttributeTypes,
|
|
1388
|
+
CanvasDataManager,
|
|
1389
|
+
EventManager,
|
|
1390
|
+
Events,
|
|
1391
|
+
Group,
|
|
1392
|
+
Matrix4,
|
|
1393
|
+
MeshObject,
|
|
1394
|
+
Motif,
|
|
1395
|
+
Quat,
|
|
1396
|
+
RenderScene,
|
|
1397
|
+
Residue,
|
|
1398
|
+
Vec3,
|
|
1399
|
+
calculateAllKabschRMSD,
|
|
1400
|
+
calculateRMSD,
|
|
1401
|
+
calculateRMSDSlide,
|
|
1402
|
+
getMotif,
|
|
1403
|
+
getPoints,
|
|
1404
|
+
getRMSD,
|
|
1405
|
+
kabschSlidingWindow,
|
|
1406
|
+
rotateAllPoints,
|
|
1407
|
+
updateAllMotifs
|
|
1408
|
+
};
|