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 creator.viewport.common.mesheditor; 10 import i18n; 11 import creator.viewport; 12 import creator.viewport.common; 13 import creator.viewport.common.mesh; 14 import creator.viewport.common.spline; 15 import creator.core.input; 16 import creator.core.actionstack; 17 import creator.actions; 18 import creator.ext; 19 import creator.widgets; 20 import creator; 21 import inochi2d; 22 import inochi2d.core.dbg; 23 import bindbc.opengl; 24 import bindbc.imgui; 25 import std.algorithm.mutation; 26 import std.algorithm.searching; 27 28 enum VertexToolMode { 29 Points, 30 Connect, 31 PathDeform, 32 } 33 34 class IncMeshEditor { 35 private: 36 bool deformOnly = false; 37 bool vertexMapDirty = false; 38 39 Drawable target; 40 VertexToolMode toolMode = VertexToolMode.Points; 41 MeshVertex*[] selected; 42 MeshVertex*[] mirrorSelected; 43 MeshVertex*[] newSelected; 44 45 vec2 lastMousePos; 46 vec2 mousePos; 47 48 bool isDragging = false; 49 bool isSelecting = false; 50 bool mutateSelection = false; 51 bool invertSelection = false; 52 MeshVertex* maybeSelectOne; 53 MeshVertex* vtxAtMouse; 54 vec2 selectOrigin; 55 IncMesh previewMesh; 56 57 bool deforming = false; 58 CatmullSpline path; 59 uint pathDragTarget; 60 61 MeshEditorDeformationAction deformAction = null; 62 63 bool isSelected(MeshVertex* vert) { 64 import std.algorithm.searching : canFind; 65 return selected.canFind(vert); 66 } 67 68 void toggleSelect(MeshVertex* vert) { 69 import std.algorithm.searching : countUntil; 70 import std.algorithm.mutation : remove; 71 auto idx = selected.countUntil(vert); 72 if (isSelected(vert)) { 73 selected = selected.remove(idx); 74 } else { 75 selected ~= vert; 76 } 77 updateMirrorSelected(); 78 } 79 80 MeshVertex* selectOne(MeshVertex* vert) { 81 if (selected.length > 0) { 82 auto lastSel = selected[$-1]; 83 84 selected = [vert]; 85 if (deformAction) 86 deformAction.addVertex(vert); 87 return lastSel; 88 } 89 90 selected = [vert]; 91 if (deformAction) 92 deformAction.addVertex(vert); 93 updateMirrorSelected(); 94 return null; 95 } 96 97 void deselectAll() { 98 selected.length = 0; 99 if (deformAction) 100 deformAction.clear(); 101 updateMirrorSelected(); 102 } 103 104 vec2 mirrorH(vec2 point) { 105 return 2 * vec2(mirrorOrigin.x, 0) + vec2(-point.x, point.y); 106 } 107 108 vec2 mirrorV(vec2 point) { 109 return 2 * vec2(0, mirrorOrigin.x) + vec2(point.x, -point.y); 110 } 111 112 vec2 mirrorHV(vec2 point) { 113 return 2 * mirrorOrigin - point; 114 } 115 116 vec2 mirror(uint axis, vec2 point) { 117 switch (axis) { 118 case 0: return point; 119 case 1: return mirrorH(point); 120 case 2: return mirrorV(point); 121 case 3: return mirrorHV(point); 122 default: assert(false, "bad axis"); 123 } 124 } 125 126 vec2 mirrorDelta(uint axis, vec2 point) { 127 switch (axis) { 128 case 0: return point; 129 case 1: return vec2(-point.x, point.y); 130 case 2: return vec2(point.x, -point.y); 131 case 3: return vec2(-point.x, -point.y); 132 default: assert(false, "bad axis"); 133 } 134 } 135 136 MeshVertex *mirrorVertex(uint axis, MeshVertex *vtx) { 137 if (axis == 0) return vtx; 138 MeshVertex *v = mesh.getVertexFromPoint(mirror(axis, vtx.position)); 139 if (v is vtx) return null; 140 return v; 141 } 142 143 void foreachMirror(void delegate(uint axis) func) { 144 if (mirrorHoriz) func(1); 145 if (mirrorVert) func(2); 146 if (mirrorHoriz && mirrorVert) func(3); 147 func(0); 148 } 149 150 void updateMirrorSelected() { 151 mirrorSelected.length = 0; 152 if (!mirrorHoriz && !mirrorVert) return; 153 154 // Avoid duplicate selections... 155 MeshVertex*[] tmpSelected; 156 foreach(v; selected) { 157 if (mirrorSelected.canFind(v)) continue; 158 tmpSelected ~= v; 159 160 foreachMirror((uint axis) { 161 MeshVertex *v2 = mirrorVertex(axis, v); 162 if (v2 is null) return; 163 if (axis != 0) { 164 if (!tmpSelected.canFind(v2) && !mirrorSelected.canFind(v2)) 165 mirrorSelected ~= v2; 166 } 167 }); 168 } 169 if (deformAction) { 170 foreach (v; mirrorSelected) { 171 deformAction.addVertex(v); 172 } 173 } 174 selected = tmpSelected; 175 } 176 177 178 public: 179 IncMesh mesh; 180 bool previewTriangulate = false; 181 bool mirrorHoriz = false; 182 bool mirrorVert = false; 183 vec2 mirrorOrigin = vec2(0, 0); 184 185 this(bool deformOnly) { 186 this.deformOnly = deformOnly; 187 } 188 189 Drawable getTarget() { 190 return target; 191 } 192 193 void setTarget(Drawable target) { 194 this.target = target; 195 mesh = new IncMesh(target.getMesh()); 196 refreshMesh(); 197 } 198 199 ref IncMesh getMesh() { 200 return mesh; 201 } 202 203 VertexToolMode getToolMode() { 204 return toolMode; 205 } 206 207 void setToolMode(VertexToolMode toolMode) { 208 assert(!deformOnly || toolMode != VertexToolMode.Connect); 209 this.toolMode = toolMode; 210 isDragging = false; 211 isSelecting = false; 212 pathDragTarget = -1; 213 deselectAll(); 214 } 215 216 bool previewingTriangulation() { 217 return previewTriangulate && toolMode == VertexToolMode.Points; 218 } 219 220 void resetMesh() { 221 mesh.reset(); 222 } 223 224 void refreshMesh() { 225 mesh.refresh(); 226 if (previewingTriangulation()) { 227 previewMesh = mesh.autoTriangulate(); 228 } else { 229 previewMesh = null; 230 } 231 updateMirrorSelected(); 232 } 233 234 void importMesh(MeshData data) { 235 mesh.import_(data); 236 mesh.refresh(); 237 } 238 239 void applyOffsets(vec2[] offsets) { 240 assert(deformOnly); 241 242 mesh.applyOffsets(offsets); 243 } 244 245 vec2[] getOffsets() { 246 assert(deformOnly); 247 248 return mesh.getOffsets(); 249 } 250 251 void applyToTarget() { 252 // Export mesh 253 MeshData data = mesh.export_(); 254 data.fixWinding(); 255 256 // Fix UVs 257 foreach(i; 0..data.uvs.length) { 258 if (Part part = cast(Part)target) { 259 260 // Texture 0 is always albedo texture 261 auto tex = part.textures[0]; 262 263 // By dividing by width and height we should get the values in UV coordinate space. 264 data.uvs[i].x /= cast(float)tex.width; 265 data.uvs[i].y /= cast(float)tex.height; 266 data.uvs[i] += vec2(0.5, 0.5); 267 } 268 } 269 270 if (data.vertices.length != target.vertices.length) 271 vertexMapDirty = true; 272 273 // Apply the model 274 auto action = new DrawableChangeAction(target.name, target); 275 target.rebuffer(data); 276 277 if (vertexMapDirty) { 278 // Remove incompatible Deforms 279 280 foreach (param; incActivePuppet().parameters) { 281 if (auto group = cast(ExParameterGroup)param) { 282 foreach(x, ref xparam; group.children) { 283 ParameterBinding binding = xparam.getBinding(target, "deform"); 284 if (binding) { 285 xparam.removeBinding(binding); 286 action.addBinding(xparam, binding); 287 } 288 } 289 } else { 290 ParameterBinding binding = param.getBinding(target, "deform"); 291 if (binding) { 292 param.removeBinding(binding); 293 action.addBinding(param, binding); 294 } 295 } 296 } 297 vertexMapDirty = false; 298 } 299 300 action.updateNewState(); 301 incActionPush(action); 302 } 303 304 void applyPreview() { 305 mesh = previewMesh; 306 previewMesh = null; 307 previewTriangulate = false; 308 } 309 310 void pushDeformAction() { 311 if (deformAction && deformAction.dirty) { 312 deformAction.updateNewState(); 313 incActionPush(deformAction); 314 deformAction = null; 315 } 316 } 317 318 MeshEditorDeformationAction getDeformAction(bool reset = false)() { 319 if (reset) 320 pushDeformAction(); 321 if (deformAction is null || !deformAction.isApplyable()) { 322 switch (toolMode) { 323 case VertexToolMode.Points: 324 deformAction = new MeshEditorDeformationAction(target.name); 325 break; 326 case VertexToolMode.PathDeform: 327 deformAction = new MeshEditorPathDeformAction(target.name); 328 break; 329 default: 330 } 331 } else { 332 if (reset) 333 deformAction.clear(); 334 } 335 return deformAction; 336 } 337 alias getCleanDeformAction = getDeformAction!true; 338 339 bool update(ImGuiIO* io, Camera camera) { 340 bool changed = false; 341 342 lastMousePos = mousePos; 343 344 mousePos = incInputGetMousePosition(); 345 if (deformOnly) { 346 vec4 pIn = vec4(-mousePos.x, -mousePos.y, 0, 1); 347 mat4 tr = target.transform.matrix().inverse(); 348 vec4 pOut = tr * pIn; 349 mousePos = vec2(pOut.x, pOut.y); 350 } else { 351 mousePos = -mousePos; 352 } 353 354 vtxAtMouse = mesh.getVertexFromPoint(mousePos); 355 356 if (incInputIsMouseReleased(ImGuiMouseButton.Left)) { 357 isDragging = false; 358 if (isSelecting) { 359 if (mutateSelection) { 360 if (!invertSelection) { 361 foreach(v; newSelected) { 362 auto idx = selected.countUntil(v); 363 if (idx == -1) selected ~= v; 364 } 365 } else { 366 foreach(v; newSelected) { 367 auto idx = selected.countUntil(v); 368 if (idx != -1) selected = selected.remove(idx); 369 } 370 } 371 updateMirrorSelected(); 372 newSelected.length = 0; 373 } else { 374 selected = newSelected; 375 newSelected = []; 376 updateMirrorSelected(); 377 } 378 379 isSelecting = false; 380 } 381 pushDeformAction(); 382 } 383 384 if (igIsMouseClicked(ImGuiMouseButton.Left)) maybeSelectOne = null; 385 386 switch(toolMode) { 387 case VertexToolMode.Points: 388 389 if (deformOnly) { 390 incStatusTooltip(_("Select"), _("Left Mouse")); 391 } else { 392 incStatusTooltip(_("Select"), _("Left Mouse")); 393 incStatusTooltip(_("Create"), _("Ctrl+Left Mouse")); 394 } 395 396 void addOrRemoveVertex(bool selectedOnly) { 397 if (deformOnly) return; 398 // Check if mouse is over a vertex 399 if (vtxAtMouse !is null) { 400 401 // In the case that it is, double clicking would remove an item 402 if (!selectedOnly || isSelected(vtxAtMouse)) { 403 foreachMirror((uint axis) { 404 mesh.removeVertexAt(mirror(axis, mousePos)); 405 }); 406 refreshMesh(); 407 vertexMapDirty = true; 408 changed = true; 409 selected.length = 0; 410 updateMirrorSelected(); 411 maybeSelectOne = null; 412 vtxAtMouse = null; 413 } 414 } else { 415 ulong off = mesh.vertices.length; 416 foreachMirror((uint axis) { 417 mesh.vertices ~= new MeshVertex(mirror(axis, mousePos)); 418 }); 419 refreshMesh(); 420 vertexMapDirty = true; 421 changed = true; 422 selectOne(mesh.vertices[off]); 423 } 424 } 425 426 // Key actions 427 if (!deformOnly && incInputIsKeyPressed(ImGuiKey.Delete)) { 428 foreachMirror((uint axis) { 429 foreach(v; selected) { 430 MeshVertex *v2 = mirrorVertex(axis, v); 431 if (v2 !is null) mesh.remove(v2); 432 } 433 }); 434 selected = []; 435 updateMirrorSelected(); 436 refreshMesh(); 437 vertexMapDirty = true; 438 changed = true; 439 } 440 void shiftSelection(vec2 delta) { 441 float magnitude = 10.0; 442 if (io.KeyAlt) magnitude = 1.0; 443 else if (io.KeyShift) magnitude = 100.0; 444 delta *= magnitude; 445 446 foreachMirror((uint axis) { 447 vec2 mDelta = mirrorDelta(axis, delta); 448 foreach(v; selected) { 449 MeshVertex *v2 = mirrorVertex(axis, v); 450 if (v2 !is null) v2.position += mDelta; 451 } 452 }); 453 refreshMesh(); 454 changed = true; 455 } 456 457 if (incInputIsKeyPressed(ImGuiKey.LeftArrow)) { 458 shiftSelection(vec2(-1, 0)); 459 } else if (incInputIsKeyPressed(ImGuiKey.RightArrow)) { 460 shiftSelection(vec2(1, 0)); 461 } else if (incInputIsKeyPressed(ImGuiKey.DownArrow)) { 462 shiftSelection(vec2(0, 1)); 463 } else if (incInputIsKeyPressed(ImGuiKey.UpArrow)) { 464 shiftSelection(vec2(0, -1)); 465 } 466 467 // Left click selection 468 if (igIsMouseClicked(ImGuiMouseButton.Left)) { 469 if (!deformOnly && io.KeyCtrl && !io.KeyShift) { 470 // Add/remove action 471 addOrRemoveVertex(false); 472 } else { 473 MeshEditorDeformationAction action; 474 // Select / drag start 475 if (deformOnly) { 476 action = getCleanDeformAction(); 477 } 478 479 if (mesh.isPointOverVertex(mousePos)) { 480 if (io.KeyShift) toggleSelect(vtxAtMouse); 481 else if (!isSelected(vtxAtMouse)) selectOne(vtxAtMouse); 482 else maybeSelectOne = vtxAtMouse; 483 } else { 484 selectOrigin = mousePos; 485 isSelecting = true; 486 } 487 } 488 } 489 if (!isDragging && !isSelecting && 490 incInputIsMouseReleased(ImGuiMouseButton.Left) && maybeSelectOne !is null) { 491 selectOne(maybeSelectOne); 492 } 493 494 // Left double click action 495 if (!deformOnly && igIsMouseDoubleClicked(ImGuiMouseButton.Left) && !io.KeyShift && !io.KeyCtrl) { 496 addOrRemoveVertex(true); 497 } 498 499 // Dragging 500 if (incDragStartedInViewport(ImGuiMouseButton.Left) && igIsMouseDown(ImGuiMouseButton.Left) && incInputIsDragRequested(ImGuiMouseButton.Left)) { 501 if (!isSelecting) { 502 isDragging = true; 503 getDeformAction(); 504 } 505 } 506 507 if (isDragging) { 508 foreach(select; selected) { 509 foreachMirror((uint axis) { 510 MeshVertex *v = mirrorVertex(axis, select); 511 if (v is null) return; 512 if (deformAction) { 513 deformAction.addVertex(v); 514 deformAction.markAsDirty(); 515 } 516 v.position += mirror(axis, mousePos - lastMousePos); 517 }); 518 } 519 changed = true; 520 refreshMesh(); 521 } 522 523 break; 524 case VertexToolMode.Connect: 525 assert(!deformOnly); 526 if (selected.length == 0) { 527 incStatusTooltip(_("Select"), _("Left Mouse")); 528 } else{ 529 incStatusTooltip(_("Connect/Disconnect"), _("Left Mouse")); 530 incStatusTooltip(_("Connect Multiple"), _("Shift+Left Mouse")); 531 } 532 533 if (igIsMouseClicked(ImGuiMouseButton.Left)) { 534 if (vtxAtMouse !is null) { 535 auto prev = selectOne(vtxAtMouse); 536 if (prev !is null) { 537 if (prev != selected[$-1]) { 538 539 // Connect or disconnect between previous and this node 540 if (!prev.isConnectedTo(selected[$-1])) { 541 foreachMirror((uint axis) { 542 MeshVertex *mPrev = mirrorVertex(axis, prev); 543 MeshVertex *mSel = mirrorVertex(axis, selected[$-1]); 544 if (mPrev !is null && mSel !is null) mPrev.connect(mSel); 545 }); 546 changed = true; 547 } else { 548 foreachMirror((uint axis) { 549 MeshVertex *mPrev = mirrorVertex(axis, prev); 550 MeshVertex *mSel = mirrorVertex(axis, selected[$-1]); 551 if (mPrev !is null && mSel !is null) mPrev.disconnect(mSel); 552 }); 553 changed = true; 554 } 555 if (!io.KeyShift) deselectAll(); 556 } else { 557 558 // Selecting the same vert twice unselects it 559 deselectAll(); 560 } 561 } 562 563 refreshMesh(); 564 } else { 565 // Clicking outside a vert deselect verts 566 deselectAll(); 567 } 568 } 569 break; 570 case VertexToolMode.PathDeform: 571 if (deforming) { 572 incStatusTooltip(_("Deform"), _("Left Mouse")); 573 incStatusTooltip(_("Switch Mode"), _("TAB")); 574 } else { 575 incStatusTooltip(_("Create/Destroy"), _("Left Mouse (x2)")); 576 incStatusTooltip(_("Switch Mode"), _("TAB")); 577 } 578 579 vtxAtMouse = null; // Do not need this in this mode 580 581 if (incInputIsKeyPressed(ImGuiKey.Tab)) { 582 if (path.target is null) { 583 path.createTarget(mesh); 584 getCleanDeformAction(); 585 } else { 586 if (deformAction !is null) { 587 pushDeformAction(); 588 getCleanDeformAction(); 589 } 590 } 591 deforming = !deforming; 592 if (deforming) { 593 getCleanDeformAction(); 594 path.updateTarget(mesh); 595 } 596 else path.resetTarget(mesh); 597 changed = true; 598 } 599 600 CatmullSpline editPath = path; 601 if (deforming) { 602 if (deformAction is null) 603 getCleanDeformAction(); 604 editPath = path.target; 605 } 606 607 if (igIsMouseDoubleClicked(ImGuiMouseButton.Left) && !deforming) { 608 int idx = path.findPoint(mousePos); 609 if (idx != -1) path.removePoint(idx); 610 else path.addPoint(mousePos); 611 pathDragTarget = -1; 612 path.mapReference(); 613 } else if (igIsMouseClicked(ImGuiMouseButton.Left)) { 614 pathDragTarget = editPath.findPoint(mousePos); 615 } 616 617 if (incDragStartedInViewport(ImGuiMouseButton.Left) && igIsMouseDown(ImGuiMouseButton.Left) && incInputIsDragRequested(ImGuiMouseButton.Left)) { 618 if (pathDragTarget != -1) { 619 isDragging = true; 620 getDeformAction(); 621 } 622 } 623 624 if (isDragging && pathDragTarget != -1) { 625 editPath.points[pathDragTarget].position += mousePos - lastMousePos; 626 editPath.update(); 627 if (deforming) { 628 path.updateTarget(mesh); 629 if (deformAction) 630 deformAction.markAsDirty(); 631 changed = true; 632 } else { 633 path.mapReference(); 634 } 635 } 636 637 if (changed) refreshMesh(); 638 639 break; 640 default: assert(0); 641 } 642 643 if (isSelecting) { 644 newSelected = mesh.getInRect(selectOrigin, mousePos); 645 mutateSelection = io.KeyShift; 646 invertSelection = io.KeyCtrl; 647 } 648 649 if (changed) 650 mesh.changed = true; 651 652 if (mesh.changed) { 653 if (previewingTriangulation()) 654 previewMesh = mesh.autoTriangulate(); 655 mesh.changed = false; 656 } 657 return changed; 658 } 659 660 void draw(Camera camera) { 661 mat4 trans = mat4.identity; 662 if (deformOnly) trans = target.transform.matrix(); 663 664 if (vtxAtMouse !is null && !isSelecting) { 665 MeshVertex*[] one = [vtxAtMouse]; 666 mesh.drawPointSubset(one, vec4(1, 1, 1, 0.3), trans, 15); 667 } 668 669 if (previewMesh) { 670 previewMesh.drawLines(trans, vec4(0.7, 0.7, 0, 1)); 671 mesh.drawPoints(trans); 672 } else { 673 mesh.draw(trans); 674 } 675 676 if (selected.length) { 677 if (isSelecting && !mutateSelection) 678 mesh.drawPointSubset(selected, vec4(0.6, 0, 0, 1), trans); 679 else 680 mesh.drawPointSubset(selected, vec4(1, 0, 0, 1), trans); 681 } 682 683 if (mirrorSelected.length) 684 mesh.drawPointSubset(mirrorSelected, vec4(1, 0, 1, 1), trans); 685 686 if (isSelecting) { 687 vec3[] rectLines = incCreateRectBuffer(selectOrigin, mousePos); 688 inDbgSetBuffer(rectLines); 689 if (!mutateSelection) inDbgDrawLines(vec4(1, 0, 0, 1), trans); 690 else if(invertSelection) inDbgDrawLines(vec4(0, 1, 1, 0.8), trans); 691 else inDbgDrawLines(vec4(0, 1, 0, 0.8), trans); 692 693 if (newSelected.length) { 694 if (mutateSelection && invertSelection) 695 mesh.drawPointSubset(newSelected, vec4(1, 0, 1, 1), trans); 696 else 697 mesh.drawPointSubset(newSelected, vec4(1, 0, 0, 1), trans); 698 } 699 } 700 701 vec2 camSize = camera.getRealSize(); 702 vec2 camPosition = camera.position; 703 vec3[] axisLines; 704 if (mirrorHoriz) { 705 axisLines ~= incCreateLineBuffer( 706 vec2(mirrorOrigin.x, -camSize.y - camPosition.y), 707 vec2(mirrorOrigin.x, camSize.y - camPosition.y) 708 ); 709 } 710 if (mirrorVert) { 711 axisLines ~= incCreateLineBuffer( 712 vec2(-camSize.x - camPosition.x, mirrorOrigin.y), 713 vec2(camSize.x - camPosition.x, mirrorOrigin.y) 714 ); 715 } 716 717 if (axisLines.length > 0) { 718 inDbgSetBuffer(axisLines); 719 inDbgDrawLines(vec4(0.8, 0, 0.8, 1), trans); 720 } 721 722 if (path && path.target && deforming) { 723 path.draw(trans, vec4(0, 0.6, 0.6, 1)); 724 path.target.draw(trans, vec4(0, 1, 0, 1)); 725 } else if (path) { 726 if (path.target) path.target.draw(trans, vec4(0, 0.6, 0, 1)); 727 path.draw(trans, vec4(0, 1, 1, 1)); 728 } 729 } 730 731 void viewportTools() { 732 igSetWindowFontScale(1.30); 733 igPushStyleVar(ImGuiStyleVar.ItemSpacing, ImVec2(1, 1)); 734 igPushStyleVar(ImGuiStyleVar.FramePadding, ImVec2(8, 10)); 735 if (incButtonColored("", ImVec2(0, 0), getToolMode() == VertexToolMode.Points ? ImVec4.init : ImVec4(0.6, 0.6, 0.6, 1))) { 736 setToolMode(VertexToolMode.Points); 737 path = null; 738 refreshMesh(); 739 } 740 incTooltip(_("Vertex Tool")); 741 742 if (!deformOnly) { 743 if (incButtonColored("", ImVec2(0, 0), getToolMode() == VertexToolMode.Connect ? ImVec4.init : ImVec4(0.6, 0.6, 0.6, 1))) { 744 setToolMode(VertexToolMode.Connect); 745 path = null; 746 refreshMesh(); 747 } 748 incTooltip(_("Edge Tool")); 749 } 750 751 if (incButtonColored("", ImVec2(0, 0), getToolMode() == VertexToolMode.PathDeform ? ImVec4.init : ImVec4(0.6, 0.6, 0.6, 1))) { 752 setToolMode(VertexToolMode.PathDeform); 753 path = new CatmullSpline; 754 deforming = false; 755 refreshMesh(); 756 } 757 incTooltip(_("Path Deform Tool")); 758 759 igPopStyleVar(2); 760 igSetWindowFontScale(1); 761 } 762 763 CatmullSpline getPath() { 764 return path; 765 } 766 767 void setPath(CatmullSpline path) { 768 this.path = path; 769 770 } 771 }