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.
- checksums.yaml +4 -4
- data/README.txt +13 -0
- data/app/assets/javascripts/vendor/dynamicbones.mjs +1053 -0
- data/app/assets/javascripts/vendor/talkinghead.mjs +4825 -0
- data/app/controllers/wco/api_controller.rb +5 -2
- data/app/controllers/wco/application_controller.rb +31 -5
- data/app/controllers/wco/newsproducer_controller.rb +22 -0
- data/app/controllers/wco/newsvideos_controller.rb +1 -33
- data/app/controllers/wco/reports_controller.rb +21 -0
- data/app/helpers/wco/application_helper.rb +1 -1
- data/app/models/wco/ai_writer.rb +2 -1
- data/app/models/wco/leadset.rb +1 -1
- data/app/models/wco/newspartial.rb +10 -6
- data/app/models/wco/newsvideo.rb +46 -9
- data/app/models/wco/setting.rb +24 -0
- data/app/models/wco_email/email_filter.rb +1 -1
- data/app/views/wco/_main_footer.haml +2 -0
- data/app/views/wco/application/settings.haml +19 -0
- data/app/views/wco/newspartials/_show_in_newsvideo.haml +10 -4
- data/app/views/wco/newsproducer/_studio_1.js +279 -0
- data/app/views/wco/newsproducer/canvas.html.erb +263 -0
- data/app/views/wco/newsproducer/canvas_minimal.html.erb +132 -0
- data/app/views/wco/newsproducer/studio_1.html.erb +26 -0
- data/app/views/wco/reports/show.haml +9 -4
- data/config/initializers/assets.rb +2 -0
- data/config/routes.rb +15 -4
- data/lib/shortcuts.rb +4 -3
- metadata +27 -5
- data/config/initializers/ahoy.rb-trash +0 -12
- /data/app/views/wco/reports/{_to_facebook.html → _to_facebook_js.html} +0 -0
- /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 };
|