wco_models 3.1.0.302 → 3.1.0.304

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.txt +13 -0
  3. data/app/assets/javascripts/vendor/dynamicbones.mjs +1053 -0
  4. data/app/assets/javascripts/vendor/talkinghead.mjs +4825 -0
  5. data/app/controllers/wco/api_controller.rb +5 -2
  6. data/app/controllers/wco/application_controller.rb +31 -5
  7. data/app/controllers/wco/newsproducer_controller.rb +22 -0
  8. data/app/controllers/wco/newsvideos_controller.rb +1 -33
  9. data/app/controllers/wco/reports_controller.rb +21 -0
  10. data/app/helpers/wco/application_helper.rb +1 -1
  11. data/app/models/wco/ai_writer.rb +2 -1
  12. data/app/models/wco/leadset.rb +1 -1
  13. data/app/models/wco/newspartial.rb +10 -6
  14. data/app/models/wco/newsvideo.rb +46 -9
  15. data/app/models/wco/setting.rb +24 -0
  16. data/app/models/wco_email/email_filter.rb +1 -1
  17. data/app/views/wco/_main_footer.haml +2 -0
  18. data/app/views/wco/application/settings.haml +19 -0
  19. data/app/views/wco/newspartials/_show_in_newsvideo.haml +10 -4
  20. data/app/views/wco/newsproducer/_studio_1.js +279 -0
  21. data/app/views/wco/newsproducer/canvas.html.erb +263 -0
  22. data/app/views/wco/newsproducer/canvas_minimal.html.erb +132 -0
  23. data/app/views/wco/newsproducer/studio_1.html.erb +26 -0
  24. data/app/views/wco/reports/show.haml +9 -4
  25. data/config/initializers/assets.rb +2 -0
  26. data/config/routes.rb +15 -4
  27. data/lib/shortcuts.rb +4 -3
  28. metadata +27 -5
  29. data/config/initializers/ahoy.rb-trash +0 -12
  30. /data/app/views/wco/reports/{_to_facebook.html → _to_facebook_js.html} +0 -0
  31. /data/lib/{20260402_tmp.rb → done/20260402_migrate_email_filters.rb} +0 -0
@@ -0,0 +1,1053 @@
1
+ /**
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2024 Mika Suominen
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import * as THREE from 'three';
26
+
27
+ // Temporary variables
28
+ let tmp,tmp2,tmp3;
29
+ const arr = [0,0,0,0];
30
+ const v = new THREE.Vector3();
31
+ const v2 = new THREE.Vector3();
32
+ const w = new THREE.Vector3();
33
+ const w2 = new THREE.Vector3();
34
+ const p = new THREE.Plane();
35
+ const r = new THREE.Ray();
36
+ const e = new THREE.Euler();
37
+ const q = new THREE.Quaternion();
38
+ const q2 = new THREE.Quaternion();
39
+ const m = new THREE.Matrix4();
40
+ const minv = new THREE.Matrix4();
41
+
42
+ // Axis
43
+ const origin = new THREE.Vector3();
44
+ const forward = new THREE.Vector3(0, 0, 1);
45
+ const axisx = new THREE.Vector3(1, 0, 0);
46
+ const axisy = new THREE.Vector3(0, 1, 0);
47
+ const axisz = new THREE.Vector3(0, 0, 1);
48
+
49
+
50
+ class DynamicBones {
51
+
52
+ constructor( opt = null ) {
53
+ this.opt = Object.assign({
54
+ warmupMs: 2000,
55
+ sensitivityFactor: 1, // Sensitivity to external forces
56
+ movementFactor: 1, // Scale movement
57
+ isExcludes: true, // Use excludes
58
+ isPivots: true, // Use pivots
59
+ isLimits: true, // Use limits
60
+ helperBoneColor1: 0xff0000,
61
+ helperBoneColor2: 0xf7c5cc,
62
+ helperLinkColor1: 0xff0000,
63
+ helperLinkColor2: 0x0000ff,
64
+ helperExcludesColor: 0xaaaaff
65
+ }, opt || {});
66
+
67
+ this.scene = null;
68
+ this.armature = null;
69
+ this.config = [];
70
+ this.data = []; // Dynamic bones data
71
+ this.dict = {}; // Dictionary from bone name to data object
72
+ this.objectsUpdate = []; // Matrices to be update in order
73
+ this.helpers = {
74
+ isActive: false,
75
+ isShowAll: false,
76
+ points: { bones: [], pivots: [], object: null },
77
+ lines: { bones: [], object: null },
78
+ excludes: { bones: [], deltaLocals: [], radii: [], objects: [] }
79
+ };
80
+ this.running = false;
81
+ this.timerMs = 0; // Warm-up timer
82
+
83
+ }
84
+
85
+ /**
86
+ * Get option value.
87
+ *
88
+ * @param {string} key Option name
89
+ * @return {any} Option value
90
+ */
91
+ getOptionValue(key) {
92
+ return this.opt[key];
93
+ }
94
+
95
+ /**
96
+ * Set option value.
97
+ *
98
+ * @param {string} key Option name
99
+ * @param {any} val Option value
100
+ */
101
+ setOptionValue(key,val) {
102
+ this.opt[key] = val;
103
+ if ( this.helpers.isActive ) {
104
+ this.showHelpers();
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Get dynamic bone names.
110
+ *
111
+ * @return {string[]} Array of bone names
112
+ */
113
+ getBoneNames() {
114
+ return this.data.map( x => x.name );
115
+ }
116
+
117
+ /**
118
+ * Get property value for a dynamic bone.
119
+ *
120
+ * @param {string} name Name of the bone
121
+ * @param {string} key Property name
122
+ * @return {any} Property value
123
+ */
124
+ getValue(name, key) {
125
+ if ( this.scene === null ) {
126
+ throw new Error("Dynamic bones has not been setup yet.");
127
+ }
128
+ if ( !this.dict.hasOwnProperty(name) ) {
129
+ throw new Error("Dynamic bone '"+name+"' not found.");
130
+ }
131
+ const d = this.dict[name];
132
+ let val;
133
+ if ( key === "type" ) {
134
+ val = d.type;
135
+ } else if ( key === "stiffness" ) {
136
+ val = d.k.every( x => x === d.k[0] ) ? d.k[0] : [...d.k];
137
+ } else if ( key === "damping" ) {
138
+ val = d.c.every( x => x === d.c[0] ) ? d.c[0] : [...d.c];
139
+ } else if ( key === "external" ) {
140
+ val = ( d.ext < 1.0 ) ? d.ext : null;
141
+ } else if ( key === "limits" ) {
142
+ val = d.limits?.map( x => ( x === null ) ? null : [...x] );
143
+ } else if ( key === "deltaLocal" ) {
144
+ val = d.dl ? [...d.dl] : null;
145
+ } else if ( key === "excludes" ) {
146
+ val = d.excludes ? [...d.excludes.map( x => {
147
+ const o = { bone: x.bone.name.slice(), radius: x.radius };
148
+ if ( x.deltaLocal ) {
149
+ o.deltaLocal = [...x.deltaLocal];
150
+ }
151
+ return o;
152
+ })] : null;
153
+ } else if ( key === "deltaWorld" ) {
154
+ val = d.dw ? [...d.dw] : null;
155
+ } else if ( key === "pivot" ) {
156
+ val = d.pivot;
157
+ } else if ( key === "helper" ) {
158
+ val = d.helper;
159
+ } else {
160
+ throw new Error("Unsupported property '"+key+"'.");
161
+ }
162
+ return val;
163
+ }
164
+
165
+ /**
166
+ * Set property value for a dynamic bone.
167
+ *
168
+ * @param {string} name Name of the bone
169
+ * @param {string} key Property name
170
+ * @param {any} val Property value
171
+ */
172
+ setValue(name, key, val) {
173
+ if ( this.scene === null ) {
174
+ throw new Error("Dynamic bones has not been setup yet.");
175
+ }
176
+ if ( !this.dict.hasOwnProperty(name) ) {
177
+ throw new Error("Dynamic bone '"+name+"' not found.");
178
+ }
179
+ const d = this.dict[name];
180
+ if ( key === "type" ) {
181
+ if ( !val ) throw new Error("Parameter 'type' not set.");
182
+ if ( typeof val !== "string" ) throw new Error( "Type must be a string." );
183
+ switch(val) {
184
+ case "point":
185
+ d.isPoint = true; d.isX = true; d.isY = true; d.isZ = true; d.isT = false;
186
+ break;
187
+ case "link":
188
+ d.isPoint = false; d.isX = true; d.isY = false; d.isZ = true; d.isT = false;
189
+ break;
190
+ case "mix1":
191
+ d.isPoint = false; d.isX = true; d.isY = true; d.isZ = true; d.isT = false;
192
+ break;
193
+ case "mix2":
194
+ d.isPoint = false; d.isX = true; d.isY = false; d.isZ = true; d.isT = true;
195
+ break;
196
+ case "full":
197
+ d.isPoint = false; d.isX = true; d.isY = true; d.isZ = true; d.isT = true;
198
+ break;
199
+ default:
200
+ throw new Error( "Unknown type'" + val + "'." );
201
+ }
202
+ d.type = val.slice();
203
+ } else if ( key === "stiffness" ) {
204
+ if ( !val ) throw new Error( "Parameter 'stiffness' not set." );
205
+ if ( !Number.isNaN(val) && val >= 0 ) {
206
+ d.k = Array(4).fill(val);
207
+ } else if ( Array.isArray(val) && val.length === 4 && val.every( x => x >= 0 ) ) {
208
+ d.k = [...val];
209
+ } else {
210
+ throw new Error( "Stiffness must be a non-negative number or an array of four non-negative numbers." );
211
+ }
212
+ } else if ( key === "damping" ) {
213
+ if ( !val ) throw new Error( "Parameter 'damping' not set." );
214
+ if ( !Number.isNaN(val) && val >= 0 ) {
215
+ d.c = Array(4).fill(val);
216
+ } else if ( Array.isArray(val) && val.length === 4 && val.every( x => x >= 0 ) ) {
217
+ d.c = [...val];
218
+ } else {
219
+ throw new Error( "Damping must be a non-negative number or an array of four non-negative numbers." );
220
+ }
221
+ } else if ( key === "external" ) {
222
+ if ( val === null || val === undefined ) {
223
+ d.ext = 1.0;
224
+ } else if ( !Number.isNaN(val) && val >=0 && val <= 1 ) {
225
+ d.ext = val;
226
+ } else {
227
+ throw new Error( "External (if set) must be a number between [0,1]." );
228
+ }
229
+ } else if ( key === "limits" ) {
230
+
231
+ if ( val === null || val === undefined ) {
232
+ d.limits = null;
233
+ } else {
234
+ if ( !Array.isArray(val) || val.length !== 4 ) throw new Error( "Limits (if set) must null, or an array of four arrays." );
235
+ if ( !val.every( x => x === null || (Array.isArray(x) && x.length === 2 && ( x[0] === null || !Number.isNaN(x[0]) ) && ( x[1] === null || !Number.isNaN(x) ) ) ) ) throw new Error( "Limit values must be null or numbers." );
236
+ d.limits = [
237
+ val[0] ? [...val[0]] : null,
238
+ val[1] ? [...val[1]] : null,
239
+ val[2] ? [...val[2]] : null,
240
+ val[3] ? [...val[3]] : null
241
+ ];
242
+ }
243
+
244
+ } else if ( key === "excludes" ) {
245
+
246
+ if ( val === null || val === undefined ) {
247
+ d.excludes = null;
248
+ } else {
249
+ if ( !Array.isArray(val) ) throw new Error( "Excludes (if set) must null, or an array." );
250
+ d.excludes = [];
251
+ val.forEach( (x,i) => {
252
+ if ( !x.bone ) throw new Error("Bone not specified in #" + i + " exclude." );
253
+ if ( typeof x.bone !== "string" || x.bone.length === 0 ) throw new Error("Bone name must be a non-empty string in #" + i + " exclude." );
254
+ const bone = this.armature.getObjectByName( x.bone );
255
+ if ( !bone ) throw new Error("Bone '" + x.bone + "' not found in #" + i + " exclude." );
256
+ if ( Number.isNaN(x.radius) && x.radius >= 0 ) throw new Error("Radius must be a non-negative number in #" + i + " exclude." );
257
+
258
+ const o = {
259
+ bone: bone, // Bone object
260
+ radius: x.radius, // Radius
261
+ radiusSq: x.radius * x.radius, // Radius squared
262
+ deltaLocal: null
263
+ };
264
+
265
+ if ( x.deltaLocal ) {
266
+ if ( !Array.isArray(x.deltaLocal) || x.deltaLocal.length !== 3 || x.deltaLocal.some( y => Number.isNaN(y) ) ) throw new Error("deltaLocal must be an array of three numbers in #" + i + " exclude." );
267
+ o.deltaLocal = [...x.deltaLocal];
268
+ }
269
+
270
+ d.excludes.push(o);
271
+ });
272
+ }
273
+
274
+ this.showHelpers();
275
+
276
+ } else if ( key === "helper" ) {
277
+
278
+ if ( val === null || val === undefined ) {
279
+ d.helper = null;
280
+ } else {
281
+ if ( val !== false && val !== true ) throw new Error( "Helper, if set, must be false or true." );
282
+ d.helper = val;
283
+ }
284
+
285
+ this.showHelpers();
286
+
287
+ } else if ( key === "pivot" ) {
288
+
289
+ if ( val === null || val === undefined ) {
290
+ d.pivot = null;
291
+ } else {
292
+ if ( val !== false && val !== true ) throw new Error( "Pivot, if set, must be false or true." );
293
+ if ( val === true && d.type === 0 ) throw new Error( "Point type bone can't be a pivot." );
294
+ d.pivot = val;
295
+ }
296
+
297
+ } else if ( key === "deltaLocal" ) {
298
+
299
+ if ( val === null || val === undefined ) {
300
+ d.dl = null;
301
+ } else {
302
+ if ( !Array.isArray(val) || val.length !== 3 ) throw new Error( "deltaLocal, is set, must be an array of three numbers." );
303
+ if ( !val.every( x => !Number.isNaN(x) ) ) throw new Error( "deltaLocal values must be numbers." );
304
+ d.dl = [...val];
305
+ }
306
+
307
+ } else if ( key === "deltaWorld" ) {
308
+
309
+ if ( val === null || val === undefined ) {
310
+ d.dw = null;
311
+ } else {
312
+ if ( !Array.isArray(val) || val.length !== 3 ) throw new Error( "deltaWorld, is set, must be an array of three values." );
313
+ if ( !val.every( x => !Number.isNaN(x) ) ) throw new Error( "deltaWorld values must be numbers." );
314
+ d.dw = [...val];
315
+ }
316
+
317
+ } else {
318
+ throw new Error("Unsupported property "+key);
319
+ }
320
+ }
321
+
322
+
323
+ /**
324
+ * Get current config.
325
+ *
326
+ * @return {Object[]} Config.
327
+ */
328
+ getConfig() {
329
+ return this.data.map( d => {
330
+
331
+ const o = { bone: d.name.slice() };
332
+ ["type","stiffness","damping","external","deltaLocal",
333
+ "deltaWorld","limits","excludes","pivot","helper"].forEach( x => {
334
+ tmp = this.getValue(d.name,x);
335
+ if ( tmp ) o[x] = tmp;
336
+ });
337
+ return o;
338
+ });
339
+ }
340
+
341
+ /**
342
+ * Sort bones so that they get updated in optimal order.
343
+ */
344
+ sortBones() {
345
+
346
+ if ( this.scene === null ) {
347
+ throw new Error("Dynamic bones has not been setup yet.");
348
+ }
349
+
350
+ // Order for all bones
351
+ let i = 0;
352
+ const objectsOrder = new WeakMap();
353
+ this.armature.traverse( x => {
354
+ if ( !objectsOrder.has(x) ) {
355
+ objectsOrder.set(x,i);
356
+ i++;
357
+ }
358
+ });
359
+
360
+ // Order data
361
+ this.data.sort( (a,b) => objectsOrder.get(a.bone) - objectsOrder.get(b.bone) );
362
+
363
+ // Find children
364
+ this.data.forEach( x => {
365
+ tmp = this.dict[x.boneParent.name];
366
+ if ( tmp ) {
367
+ if ( !tmp.children ) tmp.children = [];
368
+ tmp.children.push(x);
369
+ }
370
+ });
371
+
372
+ // Get all dynamic bones, excluded bones, and their ancestry, only unique
373
+ this.objectsUpdate = [];
374
+ const objectsSet = new WeakSet();
375
+ const getParents = (x) => {
376
+ return (x.parent?.isBone) ? [x,...getParents( x.parent )] : [x];
377
+ };
378
+ const addObject = (x) => {
379
+ const o = getParents(x);
380
+ o.forEach( y => {
381
+ if ( !objectsSet.has(y) ) {
382
+ this.objectsUpdate.push(y);
383
+ objectsSet.add(y);
384
+ }
385
+ });
386
+ };
387
+ this.data.forEach( x => {
388
+ addObject(x.bone);
389
+ if ( x.excludes ) {
390
+ x.excludes.forEach( y => {
391
+ addObject(y.bone);
392
+ });
393
+ }
394
+ });
395
+
396
+ // Sort in optimal order
397
+ this.objectsUpdate.sort( (a,b) => objectsOrder.get(a) - objectsOrder.get(b) );
398
+
399
+ }
400
+
401
+
402
+ /**
403
+ * Setup dynamic bones.
404
+ *
405
+ * @param {Scene} scene Scene, if helper is used
406
+ * @param {Object3D} armature Armature object
407
+ * @param {Object[]} config Array of configuration objects
408
+ */
409
+ setup(scene, armature, config) {
410
+
411
+ // Remove previous setup
412
+ this.dispose();
413
+
414
+ // Helper function to check logical statement and throw exception if false.
415
+ const check = (test, error) => {
416
+ if ( !test ) {
417
+ this.dispose();
418
+ throw new Error(error);
419
+ }
420
+ }
421
+
422
+ // Parameters
423
+ check( scene?.isScene, "First parameter must be Scene.");
424
+ this.scene = scene;
425
+ check( armature?.isObject3D, "Second parameter must be the armature Object3D.");
426
+ this.armature = armature;
427
+ check( Array.isArray(config), "Third parameter must be an array of bone configs.");
428
+ this.config = config;
429
+
430
+
431
+ // Configuration
432
+ this.config.forEach( (item,i) => {
433
+
434
+ // Item id
435
+ const id = "Config item #" + i + ": ";
436
+
437
+ // Bone, must be specified for each item
438
+ check( item.bone, id + "Bone not specified." );
439
+ const name = item.bone;
440
+ check( typeof name === "string" && name.length > 0, id + "Bone name must be a non-empty string." );
441
+ const bone = this.armature.getObjectByName( name );
442
+ check( bone, id + "Bone '" + name + "' not found." );
443
+ check( bone.parent?.isBone, id + "Bone must have a parent bone." );
444
+ check( this.data.every( x => x.bone !== bone ), id + "Bone '" + name + "' already exists." );
445
+ bone.updateMatrixWorld(true);
446
+
447
+ // Data object
448
+ const o = {
449
+ name: name, // Bone name
450
+ bone: bone, // Bone object
451
+ boneParent: bone.parent, /// Bone's parent object
452
+ vBasis: bone.position.clone(), // Original local position
453
+ vWorld: bone.parent.getWorldPosition(v).clone(), // World position, parent
454
+ qBasis: bone.parent.quaternion.clone(), // Original quaternion, parent
455
+ l: bone.position.length(), // Bone length
456
+ p: [0,0,0,0], // Relative position [m]
457
+ v: [0,0,0,0], // Velocity [m/s]
458
+ a: [0,0,0,0], // Acceleration [m/s]
459
+ ev: [0,0,0,0], // External velocity [m/s]
460
+ ea: [0,0,0,0] // External acceleration [m/s^2]
461
+ };
462
+
463
+ // Set pivot/gravity baseline
464
+ o.boneParent.matrixWorld.decompose( v, q, w ); // World quaternion q
465
+ v.copy(forward).applyQuaternion(q).setY(0).normalize(); // Project to XZ-plane
466
+ q.premultiply( q2.setFromUnitVectors(forward,v).invert() ).normalize();
467
+ o.qWorldInverseYaw = q.clone().normalize(); // Only the yaw rotation
468
+
469
+ // Add to data and dictionary
470
+ this.data.push(o);
471
+ this.dict[name] = o;
472
+
473
+ // Set dynamic bone properties
474
+ try {
475
+ this.setValue(name, "type", item.type);
476
+ this.setValue(name, "stiffness", item.stiffness);
477
+ this.setValue(name, "damping", item.damping);
478
+ this.setValue(name, "external", item.external);
479
+ this.setValue(name, "limits", item.limits);
480
+ this.setValue(name, "excludes", item.excludes);
481
+ this.setValue(name, "deltaLocal", item.deltaLocal);
482
+ this.setValue(name, "deltaWorld", item.deltaWorld);
483
+ this.setValue(name, "pivot", item.pivot);
484
+ this.setValue(name, "helper", item.helper);
485
+ } catch(error) {
486
+ check( false, id + error );
487
+ }
488
+
489
+ });
490
+
491
+ // Sort bones
492
+ this.sortBones();
493
+
494
+ // We are OK to go!
495
+ this.start();
496
+
497
+ }
498
+
499
+
500
+ /**
501
+ * Animate dynamic bones.
502
+ * @param {number} dt Delta time in ms.
503
+ */
504
+ update(dt) {
505
+
506
+ // Are we running?
507
+ if ( !this.running ) return;
508
+
509
+ let i,j,l,k,d;
510
+
511
+ // Timing
512
+ this.timerMs += dt; // Warmup timer
513
+ if ( dt > 1000) this.timerMs = 0; // Odd update, so we warmup
514
+ dt /= 1000; // delta time to seconds [s]
515
+
516
+ // Update all bone matrices in optimal order, only once
517
+ for( i=0, l=this.objectsUpdate.length; i<l; i++ ) {
518
+ d = this.objectsUpdate[i];
519
+ d.updateMatrix();
520
+ if ( d.parent === null ) {
521
+ d.matrixWorld.copy( d.matrix );
522
+ } else {
523
+ d.matrixWorld.multiplyMatrices( d.parent.matrixWorld, d.matrix );
524
+ }
525
+ d.matrixWorldNeedsUpdate = false;
526
+ }
527
+
528
+ // Data
529
+ for( i=0, l=this.data.length; i<l; i++ ) {
530
+ d = this.data[i];
531
+
532
+ // Get parent's world displacement and update world position
533
+ v.copy(d.vWorld); // Previous position
534
+ m.copy( d.boneParent.matrixWorld );
535
+ minv.copy(m).invert();
536
+ d.vWorld.setFromMatrixPosition( m ); // Update position
537
+ v.applyMatrix4( minv ); // World to local
538
+
539
+ if ( v.length() > 0.5 ) {
540
+ // Not realistic update, so limit
541
+ console.info("Info: Unrealistic jump of " + v.length().toFixed(2) + " meters.");
542
+ v.setLength(0.5);
543
+ }
544
+
545
+ // External effect, parent
546
+ v.applyQuaternion(d.bone.quaternion);
547
+ arr[0] = v.x;
548
+ arr[1] = v.y;
549
+ arr[2] = -v.z;
550
+ arr[3] = v.length() / 3; // TODO: Hack, fix this in later versions
551
+
552
+ // External effect, children
553
+ if ( d.children ) {
554
+ for( j=0, k=d.children.length; j<k; j++ ) {
555
+ tmp = d.children[j];
556
+ arr[0] -= tmp.v[0] * dt / 3;
557
+ arr[1] -= tmp.v[1] * dt / 3;
558
+ arr[2] += tmp.v[2] * dt / 3;
559
+ arr[3] -= tmp.v[3] * dt / 3;
560
+ }
561
+ }
562
+
563
+ // External effect, scale
564
+ tmp = this.opt.sensitivityFactor;
565
+ arr[0] *= d.ext * tmp;
566
+ arr[1] *= d.ext * tmp;
567
+ arr[2] *= d.ext * tmp;
568
+ arr[3] *= d.ext * tmp;
569
+
570
+
571
+ if ( d.isX ) {
572
+
573
+ // External force/velocity due to drag
574
+ tmp = arr[0] / dt;
575
+ d.ea[0] = (tmp - d.ev[0]) / dt; // External acceleration
576
+ d.ev[0] = tmp; // External velocity
577
+
578
+ // VELOCITY VERLET INTEGRATION
579
+ //
580
+ // 1. Total acceleration at step n:
581
+ // a_n = - k/m * x_n - c/m * v_n - a_ext
582
+ // (m = 1 kg)
583
+ // 2. Update position
584
+ // x_n+1 = x_n + v_n * dt + 1/2 * a_n * d_t^2
585
+ // (When parent moves, child moves, so we compensate with arr)
586
+ // 3. Predict new velocity:
587
+ // v_n+1 = v_n + 1/2 * a_n * dt
588
+ // 4. New acceleration at time n+1:
589
+ // a_n+1 = - k/m * x_n+1 - c/m * v_n+1 - a_ext
590
+ // (m = 1 kg)
591
+ // 5. Corrected velocity:
592
+ // v_n+1 = v_n + 1/2 * (a_n + a_n+1) * dt
593
+ //
594
+ d.a[0] = - d.k[0] * d.p[0] - d.c[0] * d.v[0] - d.ea[0];
595
+ d.p[0] += d.v[0] * dt + d.a[0] * dt * dt / 2 + arr[0];
596
+ tmp = d.v[0] + d.a[0] * dt / 2;
597
+ tmp = - d.k[0] * d.p[0] - d.c[0] * tmp - d.ea[0];
598
+ d.v[0] = d.v[0] + (tmp + d.a[0]) * dt / 2; // Iterated velocity
599
+ }
600
+
601
+ if ( d.isY ) {
602
+ tmp = arr[1] / dt;
603
+ d.ea[1] = (tmp - d.ev[1]) / dt; // External acceleration
604
+ d.ev[1] = tmp; // External velocity
605
+
606
+ d.a[1] = - d.k[1] * d.p[1] - d.c[1] * d.v[1] - d.ea[1];
607
+ d.p[1] += d.v[1] * dt + d.a[1] * dt * dt / 2 + arr[1];
608
+ tmp = d.v[1] + d.a[1] * dt / 2;
609
+ tmp = - d.k[1] * d.p[1] - d.c[1] * tmp - d.ea[1];
610
+ d.v[1] = d.v[1] + (tmp + d.a[1]) * dt / 2; // Iterated velocity
611
+ }
612
+
613
+ if ( d.isZ ) {
614
+ tmp = arr[2] / dt;
615
+ d.ea[2] = (tmp - d.ev[2]) / dt; // External acceleration
616
+ d.ev[2] = tmp; // External velocity
617
+
618
+ d.a[2] = - d.k[2] * d.p[2] - d.c[2] * d.v[2] - d.ea[2];
619
+ d.p[2] += d.v[2] * dt + d.a[2] * dt * dt / 2 + arr[2];
620
+ tmp = d.v[2] + d.a[2] * dt / 2;
621
+ tmp = - d.k[2] * d.p[2] - d.c[2] * tmp - d.ea[2];
622
+ d.v[2] = d.v[2] + (tmp + d.a[2]) * dt / 2; // Iterated velocity
623
+ }
624
+
625
+ if ( d.isT ) {
626
+ tmp = arr[3] / dt;
627
+ d.ea[3] = (tmp - d.ev[3]) / dt; // External acceleration
628
+ d.ev[3] = tmp; // External velocity
629
+
630
+ d.a[3] = - d.k[3] * d.p[3] - d.c[3] * d.v[3] - d.ea[3];
631
+ d.p[3] += d.v[3] * dt + d.a[3] * dt * dt / 2 + arr[3];
632
+ tmp = d.v[3] + d.a[3] * dt / 2;
633
+ tmp = - d.k[3] * d.p[3] - d.c[3] * tmp - d.ea[3];
634
+ d.v[3] = d.v[3] + (tmp + d.a[3]) * dt / 2; // Iterated velocity
635
+ }
636
+
637
+ // Warmup
638
+ if ( this.timerMs < this.opt.warmupMs ) {
639
+ d.v[0] *= 0.0001; d.p[0] *= 0.0001;
640
+ d.v[1] *= 0.0001; d.p[1] *= 0.0001;
641
+ d.v[2] *= 0.0001; d.p[2] *= 0.0001;
642
+ d.v[3] *= 0.0001; d.p[3] *= 0.0001;
643
+ }
644
+
645
+ // Positions relative to basis
646
+ arr[0] = d.p[0];
647
+ arr[1] = d.p[1];
648
+ arr[2] = d.p[2];
649
+ arr[3] = d.p[3];
650
+
651
+ // Scale movement
652
+ tmp = this.opt.movementFactor;
653
+ arr[0] *= tmp;
654
+ arr[1] *= tmp;
655
+ arr[2] *= tmp;
656
+ arr[3] *= tmp;
657
+
658
+ // Delta local
659
+ if ( d.dl ) {
660
+ tmp = d.dl;
661
+ arr[0] += tmp[0];
662
+ arr[1] += tmp[1];
663
+ arr[2] += tmp[2];
664
+ }
665
+
666
+ // Delta world
667
+ if ( d.dw ) {
668
+ tmp = d.dw;
669
+ v.set(
670
+ d.vBasis.x + arr[0],
671
+ d.vBasis.y + arr[1],
672
+ d.vBasis.z + arr[2]
673
+ );
674
+ v.applyMatrix4(m); // local to world
675
+ v.x += tmp[0];
676
+ v.y += tmp[1];
677
+ v.z += tmp[2];
678
+ v.applyMatrix4(minv); // world to local
679
+ arr[0] += v.x - d.vBasis.x;
680
+ arr[1] += v.y - d.vBasis.y;
681
+ arr[2] += v.z - d.vBasis.z;
682
+ }
683
+
684
+ // Limits
685
+ if ( d.limits && this.opt.isLimits ) {
686
+ tmp = d.limits;
687
+ if ( tmp[0] ) {
688
+ if ( tmp[0][0] !== null && arr[0] < tmp[0][0] ) arr[0] = tmp[0][0];
689
+ if ( tmp[0][1] !== null && arr[0] > tmp[0][1] ) arr[0] = tmp[0][1];
690
+ }
691
+ if ( tmp[1] ) {
692
+ if ( tmp[1][0] !== null && arr[1] < tmp[1][0] ) arr[1] = tmp[1][0];
693
+ if ( tmp[1][1] !== null && arr[1] > tmp[1][1] ) arr[1] = tmp[1][1];
694
+ }
695
+ if ( tmp[2] ) {
696
+ if ( tmp[2][0] !== null && arr[2] < tmp[2][0] ) arr[2] = tmp[2][0];
697
+ if ( tmp[2][1] !== null && arr[2] > tmp[2][1] ) arr[2] = tmp[2][1];
698
+ }
699
+ if ( tmp[3] ) {
700
+ if ( tmp[3][0] !== null && arr[3] < tmp[3][0] ) arr[3] = tmp[3][0];
701
+ if ( tmp[3][1] !== null && arr[3] > tmp[3][1] ) arr[3] = tmp[3][1];
702
+ }
703
+ }
704
+
705
+ // Apply move
706
+ if ( d.isPoint ) {
707
+
708
+ // Point: set position
709
+ d.bone.position.set(
710
+ d.vBasis.x + arr[0],
711
+ d.vBasis.y + arr[1],
712
+ d.vBasis.z - arr[2]
713
+ );
714
+
715
+ } else {
716
+
717
+ // Baseline orientation, either original or free hanging
718
+ d.boneParent.quaternion.copy(d.qBasis);
719
+ if ( d.pivot && this.opt.isPivots ) {
720
+ d.boneParent.updateWorldMatrix(false,false);
721
+ d.boneParent.matrixWorld.decompose( v, q, w );
722
+ v.copy(forward).applyQuaternion(q).setY(0).normalize();
723
+ q.premultiply( q2.setFromUnitVectors(forward,v).invert() ).normalize();
724
+ d.boneParent.quaternion.multiply(q.invert());
725
+ d.boneParent.quaternion.multiply(d.qWorldInverseYaw);
726
+ }
727
+
728
+ if ( d.isZ ) {
729
+ tmp = Math.atan( arr[0] / d.l );
730
+ q.setFromAxisAngle(axisz, -tmp);
731
+ d.boneParent.quaternion.multiply(q);
732
+ }
733
+
734
+ if ( d.isY ) {
735
+ tmp = d.l / 3;
736
+ tmp = tmp * Math.tanh( arr[1] / tmp );
737
+ d.bone.position.setLength( d.l + tmp );
738
+ }
739
+
740
+ if ( d.isX ) {
741
+ tmp = Math.atan( arr[2] / d.l );
742
+ q.setFromAxisAngle(axisx, -tmp);
743
+ d.boneParent.quaternion.multiply(q);
744
+ }
745
+
746
+ if ( d.isT ) {
747
+ tmp = 1.5 * Math.tanh( arr[3] * 1.5 );
748
+ q.setFromAxisAngle(axisy, -tmp);
749
+ d.boneParent.quaternion.multiply(q);
750
+ }
751
+
752
+ // Update world
753
+ d.boneParent.updateWorldMatrix(false,true);
754
+
755
+ // Excluded zones
756
+ if ( d.excludes && this.opt.isExcludes ) {
757
+ for( j=0, k=d.excludes.length; j<k; j++ ) {
758
+ tmp = d.excludes[j];
759
+
760
+ // Zone and the bone
761
+ w.set(0,0,0);
762
+ if ( tmp.deltaLocal ) {
763
+ w.x += tmp.deltaLocal[0];
764
+ w.y += tmp.deltaLocal[1];
765
+ w.z += tmp.deltaLocal[2];
766
+ }
767
+ w.applyMatrix4(tmp.bone.matrixWorld);
768
+ minv.copy(d.boneParent.matrixWorld).invert();
769
+ w.applyMatrix4(minv);
770
+ v.copy(d.bone.position);
771
+
772
+ // Continue, if the bone is not inside the zone OR
773
+ // the spheres do not intersect (e.g. one inside the other)
774
+ if ( v.distanceToSquared(w) >= tmp.radiusSq ) continue;
775
+ tmp3 = v.length();
776
+ tmp2 = w.length();
777
+ if ( tmp2 > tmp.radius + tmp3 ) continue;
778
+ if ( tmp2 < Math.abs( tmp.radius - tmp3 ) ) continue;
779
+
780
+ // Intersection circle
781
+ tmp2 = (tmp2*tmp2 + tmp3*tmp3 - tmp.radiusSq) / (2 * tmp2);
782
+ w.normalize(); // Normal vector
783
+ w2.copy(w).multiplyScalar(tmp2); // Center
784
+ tmp2 = Math.sqrt(tmp3*tmp3 - tmp2 * tmp2); // Radius
785
+
786
+ // Project the bone on the circle
787
+ v.subVectors(v,w2).projectOnPlane(w).normalize().multiplyScalar(tmp2);
788
+
789
+ // Direction vector the defines the correct half of the intersection circle
790
+ v2.subVectors(d.vBasis,w2).projectOnPlane(w).normalize();
791
+
792
+ // Check that the point is on the right half, if not, project
793
+ tmp3 = v2.dot(v);
794
+ if ( tmp3 < 0 ) {
795
+ tmp3 = Math.sqrt(tmp2 * tmp2 - tmp3 * tmp3); // Projection factor
796
+ v2.multiplyScalar(tmp3);
797
+ v.add(v2);
798
+ }
799
+
800
+ // Rotate
801
+ v.add(w2).normalize();
802
+ w.copy(d.bone.position).normalize();
803
+ q.setFromUnitVectors(w,v);
804
+ d.boneParent.quaternion.premultiply(q);
805
+ d.boneParent.updateWorldMatrix(false,true);
806
+
807
+ }
808
+ }
809
+
810
+ }
811
+
812
+ }
813
+
814
+ // Update helper
815
+ if ( this.helpers.isActive ) {
816
+ this.updateHelpers();
817
+ }
818
+
819
+ }
820
+
821
+ /**
822
+ * Add dynamic bone helpers to the scene.
823
+ *
824
+ * @param {boolean} all Show all or just flagged, if not set, use previous mode
825
+ * @param {boolean} [keepSetting=false] If true, keep the previos all setting
826
+ */
827
+ showHelpers(all) {
828
+
829
+ // Remove previous helpers, if any
830
+ this.hideHelpers();
831
+ this.helpers.isShowAll = (all === undefined) ? this.helpers.isShowAll : (all === true);
832
+
833
+ // Find out the bones with helper set to true
834
+ tmp = this.helpers;
835
+ this.data.forEach( d => {
836
+ if ( this.helpers.isShowAll || d.helper === true ) {
837
+ tmp.points.bones.push( d.bone );
838
+ tmp.points.pivots.push( d.pivot );
839
+ if ( d.type !== 0 ) {
840
+ tmp.lines.bones.push( d.bone );
841
+ }
842
+ if ( d.excludes ) {
843
+ d.excludes.forEach( x => {
844
+ let found = false;
845
+ for( let i=0; i<tmp.excludes.bones.length; i++ ) {
846
+ if ( tmp.excludes.bones[i] !== x.bone ) continue;
847
+ if ( tmp.excludes.radii[i] !== x.radius ) continue;
848
+ if ( tmp.excludes.deltaLocals[i] === null && x.deltaLocal !== null ) continue;
849
+ if ( tmp.excludes.deltaLocals[i] !== null && x.deltaLocal === null ) continue;
850
+ if ( tmp.excludes.deltaLocals[i] !== null && tmp.excludes.deltaLocals[i].some( (y,j) => y !== x.deltaLocal[j] ) ) continue;
851
+ found = true;
852
+ break;
853
+ }
854
+ if ( !found ) {
855
+ tmp.excludes.bones.push( x.bone );
856
+ tmp.excludes.radii.push( x.radius );
857
+ tmp.excludes.deltaLocals.push( x.deltaLocal ? [...x.deltaLocal] : null );
858
+ tmp.excludes.objects.push( null );
859
+ }
860
+ });
861
+ }
862
+ }
863
+ });
864
+
865
+ // Create constraint helpers
866
+ tmp = this.helpers.excludes;
867
+ if ( this.opt.isExcludes && tmp.bones.length ) {
868
+ tmp.bones.forEach( (x,i) => {
869
+ const geom = new THREE.SphereGeometry( tmp.radii[i], 6, 6 );
870
+ const material = new THREE.MeshBasicMaterial( {
871
+ depthTest: false, depthWrite: false, toneMapped: false,
872
+ transparent: true, wireframe: true,
873
+ color: this.opt.helperExcludesColor
874
+ });
875
+ tmp.objects[i] = new THREE.Mesh( geom, material );
876
+ tmp.objects[i].renderOrder = 997;
877
+ x.add(tmp.objects[i]);
878
+ if ( tmp.deltaLocals[i] ) {
879
+ tmp.objects[i].position.set(
880
+ tmp.deltaLocals[i][0],
881
+ tmp.deltaLocals[i][1],
882
+ tmp.deltaLocals[i][2]
883
+ );
884
+ }
885
+ });
886
+ }
887
+
888
+ // Create points helpers
889
+ tmp = this.helpers.points;
890
+ if ( tmp.bones.length ) {
891
+ this.helpers.isActive = true;
892
+ const geom = new THREE.BufferGeometry();
893
+ const vertices = tmp.bones.map( x => [0,0,0] ).flat();
894
+ geom.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
895
+ const c1 = new THREE.Color( this.opt.helperBoneColor1 );
896
+ const c2 = new THREE.Color( this.opt.helperBoneColor2 );
897
+ const colors = tmp.pivots.map( x => (x && this.opt.isPivots) ? [c2.r,c2.g,c2.b] : [c1.r,c1.g,c1.b] ).flat();
898
+ geom.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
899
+ const material = new THREE.PointsMaterial( {
900
+ depthTest: false, depthWrite: false, toneMapped: false,
901
+ transparent: true, size: 0.2, vertexColors: true
902
+ });
903
+ tmp.object = new THREE.Points( geom, material );
904
+ tmp.object.renderOrder = 998;
905
+ tmp.object.matrix = this.armature.matrixWorld;
906
+ tmp.object.matrixAutoUpdate = false;
907
+ this.scene.add(tmp.object);
908
+ }
909
+
910
+ // Create lines helper
911
+ tmp = this.helpers.lines;
912
+ if ( tmp.bones.length ) {
913
+ const geom = new THREE.BufferGeometry();
914
+ const vertices = tmp.bones.map( x => [0,0,0,0,0,0] ).flat();
915
+ geom.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
916
+ const c1 = new THREE.Color( this.opt.helperLinkColor1 );
917
+ const c2 = new THREE.Color( this.opt.helperLinkColor2 );
918
+ const colors = tmp.bones.map( x => [c1.r,c1.g,c1.b,c2.r,c2.g,c2.b] ).flat();
919
+ geom.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
920
+ const material = new THREE.LineBasicMaterial( {
921
+ vertexColors: true, depthTest: false, depthWrite: false,
922
+ toneMapped: false, transparent: true
923
+ });
924
+ tmp.object = new THREE.LineSegments( geom, material );
925
+ tmp.object.renderOrder = 999;
926
+ tmp.object.matrix = this.armature.matrixWorld;
927
+ tmp.object.matrixAutoUpdate = false;
928
+ this.scene.add(tmp.object);
929
+ }
930
+
931
+ }
932
+
933
+ /**
934
+ * Update the positions of dynamic bone helpers.
935
+ */
936
+ updateHelpers() {
937
+
938
+ // Update points
939
+ tmp = this.helpers.points;
940
+ if ( tmp.bones.length ) {
941
+ minv.copy( this.armature.matrixWorld ).invert();
942
+ const pos = tmp.object.geometry.getAttribute('position');
943
+ for( let i=0, l=tmp.bones.length; i<l; i++ ) {
944
+ m.multiplyMatrices( minv, tmp.bones[i].matrixWorld );
945
+ v.setFromMatrixPosition( m );
946
+ pos.setXYZ( i, v.x, v.y, v.z );
947
+ }
948
+ pos.needsUpdate = true;
949
+ tmp.object.updateMatrixWorld();
950
+ }
951
+
952
+ // Update lines
953
+ tmp = this.helpers.lines;
954
+ if ( tmp.bones.length ) {
955
+ minv.copy( this.armature.matrixWorld ).invert();
956
+ const pos = tmp.object.geometry.getAttribute('position');
957
+ for ( let i=0, j=0, l=tmp.bones.length; i < l; i++, j+=2 ) {
958
+ m.multiplyMatrices( minv, tmp.bones[i].matrixWorld );
959
+ v.setFromMatrixPosition( m );
960
+ pos.setXYZ( j, v.x, v.y, v.z );
961
+ m.multiplyMatrices( minv, tmp.bones[i].parent.matrixWorld );
962
+ v.setFromMatrixPosition( m );
963
+ pos.setXYZ( j + 1, v.x, v.y, v.z );
964
+ }
965
+ pos.needsUpdate = true;
966
+ tmp.object.updateMatrixWorld();
967
+ }
968
+
969
+ }
970
+
971
+ /**
972
+ * Remove dynamic bone helpers.
973
+ */
974
+ hideHelpers() {
975
+
976
+ // Hide points and lines
977
+ [ this.helpers.points, this.helpers.lines ].forEach( x => {
978
+ x.bones = [];
979
+ if ( x.object ) {
980
+ this.scene.remove( x.object );
981
+ x.object.geometry.dispose();
982
+ x.object.material.dispose();
983
+ x.object = null
984
+ }
985
+ });
986
+
987
+ // Hide contraints
988
+ tmp = this.helpers.excludes;
989
+ tmp.objects.forEach( (y,i) => {
990
+ if ( y ) {
991
+ tmp.bones[i].remove( y );
992
+ y.geometry.dispose();
993
+ y.material.dispose();
994
+ }
995
+ });
996
+ tmp.bones = [];
997
+ tmp.deltaLocals = [];
998
+ tmp.radii = [];
999
+ tmp.objects = [];
1000
+
1001
+ // De-activate
1002
+ this.helpers.isActive = false;
1003
+
1004
+ }
1005
+
1006
+ /**
1007
+ * Start dynamic bones.
1008
+ */
1009
+ start() {
1010
+ if ( this.data.length ) {
1011
+ this.running = true;
1012
+ this.timerMs = 0;
1013
+ this.showHelpers();
1014
+ }
1015
+ }
1016
+
1017
+ /**
1018
+ * Stop dynamic bones and reset positions and rotations.
1019
+ */
1020
+ stop() {
1021
+
1022
+ this.running = false;
1023
+ this.hideHelpers();
1024
+
1025
+ // Reset positions and parent rotations
1026
+ for( let i=0, l=this.data.length; i<l; i++ ) {
1027
+ const d = this.data[i];
1028
+ d.bone.position.copy( d.vBasis );
1029
+ d.boneParent.quaternion.copy( d.qBasis );
1030
+ }
1031
+
1032
+ }
1033
+
1034
+ /**
1035
+ * Reset local position and rotations and dispose the dynamic bones.
1036
+ */
1037
+ dispose() {
1038
+
1039
+ this.stop();
1040
+
1041
+ this.scene = null;
1042
+ this.armature = null;
1043
+ this.config = [];
1044
+ this.data = [];
1045
+ this.dict = {};
1046
+ this.objectsUpdate = [];
1047
+ this.timerMs = 0;
1048
+
1049
+ }
1050
+
1051
+ }
1052
+
1053
+ export { DynamicBones };