1 /* 2 Copyright © 2022, Inochi2D Project 3 Distributed under the 2-Clause BSD License, see LICENSE file. 4 5 Authors: 6 - Luna Nielsen 7 - Asahi Lina 8 */ 9 module inochi2d.core.param; 10 import inochi2d.fmt.serialize; 11 import inochi2d.math.serialization; 12 import inochi2d.core; 13 import inochi2d.math; 14 import std.exception; 15 import std.array; 16 import std.algorithm.mutation; 17 import std.stdio; 18 19 public import inochi2d.core.param.binding; 20 21 /** 22 A parameter 23 */ 24 class Parameter { 25 public: 26 /** 27 Unique ID of parameter 28 */ 29 uint uuid; 30 31 /** 32 Name of the parameter 33 */ 34 string name; 35 36 /** 37 Optimized indexable name generated at runtime 38 39 DO NOT SERIALIZE THIS. 40 */ 41 string indexableName; 42 43 /** 44 Whether this parameter updates the model 45 */ 46 bool active = true; 47 48 /** 49 Automator calculated offset to apply 50 */ 51 vec2 offset = vec2(0); 52 53 /** 54 The current parameter value 55 */ 56 vec2 value = vec2(0); 57 58 /** 59 The default value 60 */ 61 vec2 defaults = vec2(0); 62 63 /** 64 Whether the parameter is 2D 65 */ 66 bool isVec2; 67 68 /** 69 The parameter's minimum bounds 70 */ 71 vec2 min = vec2(0, 0); 72 73 /** 74 The parameter's maximum bounds 75 */ 76 vec2 max = vec2(1, 1); 77 78 /** 79 Position of the keypoints along each axis 80 */ 81 float[][2] axisPoints = [[0, 1], [0, 1]]; 82 83 /** 84 Binding to targets 85 */ 86 ParameterBinding[] bindings; 87 88 /** 89 Gets the value normalized to the internal range (0.0->1.0) 90 */ 91 vec2 normalizedValue() { 92 return this.mapValue(value); 93 } 94 95 /** 96 Sets the value normalized up from the internal range (0.0->1.0) 97 to the user defined range. 98 */ 99 void normalizedValue(vec2 value) { 100 this.value = vec2( 101 value.x * (max.x-min.x) + min.x, 102 value.y * (max.y-min.y) + min.y 103 ); 104 } 105 106 /** 107 For serialization 108 */ 109 this() { } 110 111 /** 112 Unload UUID on clear 113 */ 114 ~this() { 115 inUnloadUUID(this.uuid); 116 } 117 118 /** 119 Create new parameter 120 */ 121 this(string name, bool isVec2) { 122 this.uuid = inCreateUUID(); 123 this.name = name; 124 this.isVec2 = isVec2; 125 if (!isVec2) 126 axisPoints[1] = [0]; 127 128 this.makeIndexable(); 129 } 130 131 /** 132 Clone this parameter 133 */ 134 Parameter dup() { 135 Parameter newParam = new Parameter(name ~ " (Copy)", isVec2); 136 137 newParam.min = min; 138 newParam.max = max; 139 newParam.axisPoints = axisPoints.dup; 140 141 foreach(binding; bindings) { 142 ParameterBinding newBinding = newParam.createBinding( 143 binding.getNode(), 144 binding.getName(), 145 false 146 ); 147 newBinding.interpolateMode = binding.interpolateMode; 148 foreach(x; 0..axisPointCount(0)) { 149 foreach(y; 0..axisPointCount(1)) { 150 binding.copyKeypointToBinding(vec2u(x, y), newBinding, vec2u(x, y)); 151 } 152 } 153 newParam.addBinding(newBinding); 154 } 155 156 return newParam; 157 } 158 159 /** 160 Serializes a parameter 161 */ 162 void serialize(S)(ref S serializer) { 163 auto state = serializer.objectBegin; 164 serializer.putKey("uuid"); 165 serializer.putValue(uuid); 166 serializer.putKey("name"); 167 serializer.putValue(name); 168 serializer.putKey("is_vec2"); 169 serializer.putValue(isVec2); 170 serializer.putKey("min"); 171 min.serialize(serializer); 172 serializer.putKey("max"); 173 max.serialize(serializer); 174 serializer.putKey("defaults"); 175 defaults.serialize(serializer); 176 serializer.putKey("axis_points"); 177 serializer.serializeValue(axisPoints); 178 serializer.putKey("bindings"); 179 auto arrstate = serializer.arrayBegin(); 180 foreach(binding; bindings) { 181 serializer.elemBegin(); 182 binding.serializeSelf(serializer); 183 } 184 serializer.arrayEnd(arrstate); 185 serializer.objectEnd(state); 186 } 187 188 /** 189 Deserializes a parameter 190 */ 191 SerdeException deserializeFromFghj(Fghj data) { 192 data["uuid"].deserializeValue(this.uuid); 193 data["name"].deserializeValue(this.name); 194 if (!data["is_vec2"].isEmpty) data["is_vec2"].deserializeValue(this.isVec2); 195 if (!data["min"].isEmpty) min.deserialize(data["min"]); 196 if (!data["max"].isEmpty) max.deserialize(data["max"]); 197 if (!data["axis_points"].isEmpty) data["axis_points"].deserializeValue(this.axisPoints); 198 if (!data["defaults"].isEmpty) defaults.deserialize(data["defaults"]); 199 200 if (!data["bindings"].isEmpty) { 201 foreach(Fghj child; data["bindings"].byElement) { 202 203 // Skip empty children 204 if (child["param_name"].isEmpty) continue; 205 206 string paramName; 207 child["param_name"].deserializeValue(paramName); 208 209 if (paramName == "deform") { 210 auto binding = new DeformationParameterBinding(this); 211 binding.deserializeFromFghj(child); 212 bindings ~= binding; 213 } else { 214 auto binding = new ValueParameterBinding(this); 215 binding.deserializeFromFghj(child); 216 bindings ~= binding; 217 } 218 } 219 } 220 221 return null; 222 } 223 224 /** 225 Finalize loading of parameter 226 */ 227 void finalize(Puppet puppet) { 228 this.makeIndexable(); 229 this.value = defaults; 230 231 ParameterBinding[] validBindingList; 232 foreach(i, binding; bindings) { 233 if (puppet.find!Node(binding.getNodeUUID())) { 234 binding.finalize(puppet); 235 validBindingList ~= binding; 236 } 237 } 238 239 bindings = validBindingList; 240 } 241 242 void findOffset(vec2 offset, out vec2u index, out vec2 outOffset) { 243 void interpAxis(uint axis, float val, out uint index, out float offset) { 244 float[] pos = axisPoints[axis]; 245 246 foreach(i; 0..pos.length - 1) { 247 if (pos[i + 1] > val || i == (pos.length - 2)) { 248 index = cast(uint)i; 249 offset = (val - pos[i]) / (pos[i + 1] - pos[i]); 250 return; 251 } 252 } 253 } 254 255 interpAxis(0, offset.x, index.x, outOffset.x); 256 if (isVec2) interpAxis(1, offset.y, index.y, outOffset.y); 257 } 258 259 void preUpdate() { 260 offset = vec2(0); 261 } 262 263 void update() { 264 vec2u index; 265 vec2 offset_; 266 267 if (!active) 268 return; 269 270 findOffset(this.mapValue(value + offset), index, offset_); 271 foreach(binding; bindings) { 272 binding.apply(index, offset_); 273 } 274 } 275 276 /** 277 Get number of points for an axis 278 */ 279 uint axisPointCount(uint axis = 0) { 280 return cast(uint)axisPoints[axis].length; 281 } 282 283 /** 284 Move an axis point to a new offset 285 */ 286 void moveAxisPoint(uint axis, uint oldidx, float newoff) { 287 assert(oldidx > 0 && oldidx < this.axisPointCount(axis)-1, "invalid point index"); 288 assert(newoff > 0 && newoff < 1, "offset out of bounds"); 289 if (isVec2) 290 assert(axis <= 1, "bad axis"); 291 else 292 assert(axis == 0, "bad axis"); 293 294 // Find the index at which to insert 295 uint index; 296 for(index = 1; index < axisPoints[axis].length; index++) { 297 if (axisPoints[axis][index+1] > newoff) 298 break; 299 } 300 301 if (oldidx != index) { 302 // BUG: Apparently deleting the oldindex and replacing it with newindex causes a crash. 303 304 // Insert it into the new position in the list 305 auto swap = axisPoints[oldidx]; 306 axisPoints[axis] = axisPoints[axis].remove(oldidx); 307 axisPoints[axis].insertInPlace(index, swap); 308 debug writeln("after move ", this.axisPointCount(0)); 309 } 310 311 // Tell all bindings to reinterpolate 312 foreach(binding; bindings) { 313 binding.moveKeypoints(axis, oldidx, index); 314 } 315 } 316 317 /** 318 Add a new axis point at the given offset 319 */ 320 void insertAxisPoint(uint axis, float off) { 321 assert(off > 0 && off < 1, "offset out of bounds"); 322 if (isVec2) 323 assert(axis <= 1, "bad axis"); 324 else 325 assert(axis == 0, "bad axis"); 326 327 // Find the index at which to insert 328 uint index; 329 for(index = 1; index < axisPoints[axis].length; index++) { 330 if (axisPoints[axis][index] > off) 331 break; 332 } 333 334 // Insert it into the position list 335 axisPoints[axis].insertInPlace(index, off); 336 337 // Tell all bindings to insert space into their arrays 338 foreach(binding; bindings) { 339 binding.insertKeypoints(axis, index); 340 } 341 } 342 343 /** 344 Delete a specified axis point by index 345 */ 346 void deleteAxisPoint(uint axis, uint index) { 347 if (isVec2) 348 assert(axis <= 1, "bad axis"); 349 else 350 assert(axis == 0, "bad axis"); 351 352 assert(index > 0, "cannot delete axis point at 0"); 353 assert(index < (axisPoints[axis].length - 1), "cannot delete axis point at 1"); 354 355 // Remove the keypoint 356 axisPoints[axis] = axisPoints[axis].remove(index); 357 358 // Tell all bindings to remove it from their arrays 359 foreach(binding; bindings) { 360 binding.deleteKeypoints(axis, index); 361 } 362 } 363 364 /** 365 Flip the mapping across an axis 366 */ 367 void reverseAxis(uint axis) { 368 axisPoints[axis].reverse(); 369 foreach(ref i; axisPoints[axis]) { 370 i = 1 - i; 371 } 372 foreach(binding; bindings) { 373 binding.reverseAxis(axis); 374 } 375 } 376 377 /** 378 Get the offset (0..1) of a specified keypoint index 379 */ 380 vec2 getKeypointOffset(vec2u index) { 381 return vec2(axisPoints[0][index.x], axisPoints[1][index.y]); 382 } 383 384 /** 385 Get the value at a specified keypoint index 386 */ 387 vec2 getKeypointValue(vec2u index) { 388 return unmapValue(getKeypointOffset(index)); 389 } 390 391 /** 392 Maps an input value to an offset (0.0->1.0) 393 */ 394 vec2 mapValue(vec2 value) { 395 vec2 range = max - min; 396 vec2 tmp = (value - min); 397 vec2 off = vec2(tmp.x / range.x, tmp.y / range.y); 398 399 vec2 clamped = vec2( 400 clamp(off.x, 0, 1), 401 clamp(off.y, 0, 1), 402 ); 403 return clamped; 404 } 405 406 /** 407 Maps an offset (0.0->1.0) to a value 408 */ 409 vec2 unmapValue(vec2 offset) { 410 vec2 range = max - min; 411 return vec2(range.x * offset.x, range.y * offset.y) + min; 412 } 413 414 /** 415 Maps an input value to an offset (0.0->1.0) for an axis 416 */ 417 float mapAxis(uint axis, float value) { 418 vec2 input = min; 419 if (axis == 0) input.x = value; 420 else input.y = value; 421 vec2 output = mapValue(input); 422 if (axis == 0) return output.x; 423 else return output.y; 424 } 425 426 /** 427 Maps an internal value (0.0->1.0) to the input range for an axis 428 */ 429 float unmapAxis(uint axis, float offset) { 430 vec2 input = min; 431 if (axis == 0) input.x = offset; 432 else input.y = offset; 433 vec2 output = unmapValue(input); 434 if (axis == 0) return output.x; 435 else return output.y; 436 } 437 438 /** 439 Gets the axis point closest to a given offset 440 */ 441 uint getClosestAxisPointIndex(uint axis, float offset) { 442 uint closestPoint = 0; 443 float closestDist = float.infinity; 444 445 foreach(i, pointVal; axisPoints[axis]) { 446 float dist = abs(pointVal - offset); 447 if (dist < closestDist) { 448 closestDist = dist; 449 closestPoint = cast(uint)i; 450 } 451 } 452 453 return closestPoint; 454 } 455 456 /** 457 Find the keypoint closest to the current value 458 */ 459 vec2u findClosestKeypoint() { 460 return findClosestKeypoint(value); 461 } 462 463 /** 464 Find the keypoint closest to a value 465 */ 466 vec2u findClosestKeypoint(vec2 value) { 467 vec2 mapped = mapValue(value); 468 uint x = getClosestAxisPointIndex(0, mapped.x); 469 uint y = getClosestAxisPointIndex(1, mapped.y); 470 471 return vec2u(x, y); 472 } 473 474 /** 475 Find the keypoint closest to the current value 476 */ 477 vec2 getClosestKeypointValue() { 478 return getKeypointValue(findClosestKeypoint()); 479 } 480 481 /** 482 Find the keypoint closest to a value 483 */ 484 vec2 getClosestKeypointValue(vec2 value) { 485 return getKeypointValue(findClosestKeypoint(value)); 486 } 487 488 /** 489 Find a binding by node ref and name 490 */ 491 ParameterBinding getBinding(Node n, string bindingName) { 492 foreach(ref binding; bindings) { 493 if (binding.getNode() != n) continue; 494 if (binding.getName == bindingName) return binding; 495 } 496 return null; 497 } 498 499 /** 500 Check if a binding exists for a given node and name 501 */ 502 bool hasBinding(Node n, string bindingName) { 503 foreach(ref binding; bindings) { 504 if (binding.getNode() != n) continue; 505 if (binding.getName == bindingName) return true; 506 } 507 return false; 508 } 509 510 /** 511 Create a new binding (without adding it) for a given node and name 512 */ 513 ParameterBinding createBinding(Node n, string bindingName, bool setZero = true) { 514 ParameterBinding b; 515 if (bindingName == "deform") { 516 b = new DeformationParameterBinding(this, n, bindingName); 517 } else { 518 b = new ValueParameterBinding(this, n, bindingName); 519 } 520 521 if (setZero) { 522 vec2u zeroIndex = findClosestKeypoint(vec2(0, 0)); 523 vec2 zero = getKeypointValue(zeroIndex); 524 if (abs(zero.x) < 0.001 && abs(zero.y) < 0.001) b.reset(zeroIndex); 525 } 526 527 return b; 528 } 529 530 /** 531 Find a binding if it exists, or create and add a new one, and return it 532 */ 533 ParameterBinding getOrAddBinding(Node n, string bindingName, bool setZero = true) { 534 ParameterBinding binding = getBinding(n, bindingName); 535 if (binding is null) { 536 binding = createBinding(n, bindingName, setZero); 537 addBinding(binding); 538 } 539 return binding; 540 } 541 542 /** 543 Add a new binding (must not exist) 544 */ 545 void addBinding(ParameterBinding binding) { 546 assert(!hasBinding(binding.getNode, binding.getName)); 547 bindings ~= binding; 548 } 549 550 /** 551 Remove an existing binding by ref 552 */ 553 void removeBinding(ParameterBinding binding) { 554 import std.algorithm.searching : countUntil; 555 import std.algorithm.mutation : remove; 556 ptrdiff_t idx = bindings.countUntil(binding); 557 if (idx >= 0) { 558 bindings = bindings.remove(idx); 559 } 560 } 561 562 void makeIndexable() { 563 import std.uni : toLower; 564 indexableName = name.toLower; 565 } 566 }