diff --git a/src/bezierTool.js b/src/bezierTool.js index 12ae620..6c65ad5 100644 --- a/src/bezierTool.js +++ b/src/bezierTool.js @@ -37,6 +37,50 @@ Vと違って両側の制御点の選択点との距離を保ちつつ、 後ろの制御点の位置を前の制御点から選択点に伸びる半直線上に配置する */ +/* +function setup() { + createCanvas(400, 400); + path0 = new Path2D("M 100 100 L 200 100 L 200 200"); + path1 = new Path2D("M 100 100 L 200 100 L 200 200 Z"); + + background(220); + + stroke(0); + drawingContext.stroke(path1); +} +参考:SVGパス:https://developer.mozilla.org/ja/docs/Web/SVG/Tutorial/Paths +というわけでいくつか改善点 +まずBはやめてCにしましょう +QCやBCはQIやBIにする。IはintermediateのIです。 +で、Zですが、これ閉路、なんですね。 +つまり基本的に用意しない... +今のシステムだとおしりの点を普通に登録してるので要らないですね +Mで新しいパスが始まるので +Mでマルチパスを切り替えればいいわけです。Z要らないと。 +それにCやQで終わる場合にそこと頭をLで結ぶメリットがほぼ無いです。要らないです。 +要は"M a b L c d Z"って"M a b L c d L a b"と同じ、勝手にLを作ってしまうので。 +不要ですね +strokeで扱いたい場合に意図しない挙動をしても困るので...やめますかね。 +そうなるとベジエ扱ってるコード全部直す羽目になる... +加えてHとかVもあるとか。あと小文字で差分?でもとりあえず要らないか + +セーブとロードからZを排除するの簡単そう +よかった~機能してないや +... +回転体に使うパスとか普通に閉じてないからZあるとまずいのよ +やめましょ + +2024-05-16 +Zをロードとセーブから排除 + +パースの方いじらないとやばいんだけど、簡単ですね +Mのところで +subData.length>0だったらresultに追加すればいいだけ +ただ文字データがZを使ってて解釈違いになるとまずいので +Zのところに「subDataの頭とLでつなぐ」って書いておけばいい +それと最後...出力の直前でsubDataをresultに追加すればOKですね +*/ + let paths = []; let pathId = -1; let pointId = -1; @@ -55,15 +99,10 @@ let scalingVelocity = 0; // TwitterBird(スケール1でロード、フリップ無し) const loadingData = "M -141.000 -122.000 Q -84.500 -54.000 2.000 -50.000 C -3.333 -139.667 79.333 -154.333 124.000 -115.000 Q 145.500 -116.000 167.000 -129.000 Q 158.500 -104.000 142.000 -91.000 Q 163.000 -93.500 179.000 -104.000 Q 165.500 -79.000 146.000 -70.000 C 132.000 140.333 -53.000 181.667 -163.000 115.000 Q -106.500 111.500 -68.000 86.000 Q -116.000 68.000 -130.000 35.000 Q -112.000 38.000 -96.000 35.000 Q -153.500 11.500 -153.000 -38.000 Q -139.500 -28.000 -125.000 -29.000 Q -165.500 -74.500 -141.000 -122.000 Z"; -// 肉球(スケール300でロード、フリップ無し) -const loadingData2 = "M 0.063 0.298 C 0.109 0.311 0.150 0.329 0.200 0.364 C 0.225 0.376 0.250 0.398 0.275 0.417 C 0.287 0.439 0.313 0.454 0.341 0.489 C 0.360 0.505 0.380 0.522 0.400 0.539 C 0.425 0.573 0.450 0.604 0.478 0.636 C 0.506 0.661 0.539 0.694 0.566 0.742 C 0.588 0.792 0.556 0.820 0.503 0.836 C 0.463 0.851 0.394 0.864 0.325 0.886 C 0.247 0.898 0.139 0.917 0.056 0.932 C -0.014 0.938 -0.083 0.943 -0.153 0.948 C -0.212 0.954 -0.287 0.911 -0.313 0.867 C -0.317 0.829 -0.334 0.798 -0.294 0.745 C -0.272 0.698 -0.241 0.679 -0.225 0.654 C -0.200 0.616 -0.175 0.577 -0.159 0.542 C -0.125 0.495 -0.104 0.447 -0.072 0.398 C -0.044 0.365 -0.006 0.328 0.047 0.301 Z M -0.469 0.066 C -0.434 0.100 -0.388 0.128 -0.350 0.163 C -0.329 0.188 -0.306 0.212 -0.300 0.237 C -0.283 0.277 -0.279 0.317 -0.275 0.356 C -0.287 0.392 -0.300 0.427 -0.325 0.450 C -0.346 0.471 -0.384 0.478 -0.412 0.487 C -0.452 0.487 -0.492 0.487 -0.531 0.487 C -0.575 0.478 -0.613 0.447 -0.641 0.425 C -0.662 0.397 -0.662 0.366 -0.675 0.328 C -0.678 0.303 -0.700 0.256 -0.697 0.216 C -0.694 0.188 -0.681 0.128 -0.662 0.097 C -0.647 0.078 -0.637 0.069 -0.616 0.053 C -0.588 0.041 -0.537 0.041 -0.487 0.056 Z M 0.700 -0.159 C 0.719 -0.141 0.738 -0.122 0.756 -0.103 C 0.775 -0.078 0.787 -0.013 0.787 0.034 C 0.787 0.059 0.784 0.116 0.772 0.169 C 0.766 0.200 0.756 0.237 0.731 0.275 C 0.713 0.309 0.688 0.331 0.637 0.350 C 0.600 0.356 0.566 0.353 0.525 0.341 C 0.500 0.331 0.469 0.316 0.441 0.294 C 0.426 0.270 0.411 0.246 0.400 0.216 C 0.394 0.190 0.391 0.157 0.388 0.125 C 0.388 0.100 0.402 0.063 0.409 0.031 C 0.424 0.000 0.439 -0.031 0.453 -0.063 C 0.477 -0.087 0.497 -0.122 0.525 -0.138 C 0.534 -0.153 0.563 -0.169 0.631 -0.178 C 0.600 -0.175 0.659 -0.172 0.681 -0.169 Z M -0.269 -0.694 C -0.248 -0.692 -0.227 -0.680 -0.197 -0.662 C -0.181 -0.647 -0.163 -0.631 -0.141 -0.606 C -0.125 -0.581 -0.100 -0.550 -0.097 -0.522 C -0.087 -0.482 -0.084 -0.441 -0.081 -0.403 C -0.080 -0.348 -0.079 -0.293 -0.078 -0.237 C -0.078 -0.178 -0.084 -0.131 -0.100 -0.075 C -0.113 -0.034 -0.150 -0.009 -0.191 0.006 C -0.223 0.018 -0.261 0.020 -0.300 0.022 C -0.347 0.025 -0.388 0.013 -0.425 -0.019 C -0.453 -0.050 -0.487 -0.087 -0.503 -0.134 C -0.512 -0.156 -0.516 -0.206 -0.519 -0.266 C -0.522 -0.306 -0.516 -0.347 -0.512 -0.384 C -0.503 -0.431 -0.491 -0.469 -0.478 -0.509 C -0.463 -0.544 -0.444 -0.575 -0.419 -0.603 C -0.397 -0.622 -0.375 -0.647 -0.353 -0.659 C -0.330 -0.672 -0.307 -0.684 -0.287 -0.694 Z M 0.269 -0.759 C 0.293 -0.752 0.317 -0.745 0.341 -0.738 C 0.360 -0.720 0.380 -0.702 0.397 -0.681 C 0.428 -0.641 0.441 -0.609 0.450 -0.572 C 0.454 -0.525 0.458 -0.478 0.463 -0.431 C 0.463 -0.378 0.463 -0.322 0.459 -0.284 C 0.456 -0.234 0.447 -0.197 0.441 -0.172 C 0.431 -0.141 0.425 -0.109 0.403 -0.078 C 0.384 -0.053 0.350 -0.031 0.313 -0.009 C 0.284 0.000 0.247 0.009 0.203 0.003 C 0.169 -0.009 0.128 -0.034 0.100 -0.059 C 0.075 -0.094 0.063 -0.109 0.050 -0.134 C 0.045 -0.169 0.040 -0.203 0.034 -0.237 C 0.033 -0.274 0.025 -0.303 0.028 -0.334 C 0.028 -0.384 0.041 -0.425 0.044 -0.469 C 0.053 -0.500 0.069 -0.541 0.078 -0.572 C 0.097 -0.609 0.116 -0.644 0.138 -0.684 C 0.159 -0.709 0.175 -0.734 0.194 -0.753 C 0.215 -0.761 0.229 -0.764 0.244 -0.762 Z M -0.694 -0.056 C -0.703 -0.072 -0.722 -0.081 -0.750 -0.097 C -0.762 -0.103 -0.787 -0.097 -0.800 -0.084 C -0.803 -0.066 -0.800 -0.047 -0.794 -0.034 C -0.784 -0.025 -0.772 -0.016 -0.750 -0.003 C -0.744 -0.008 -0.731 0.000 -0.713 -0.006 C -0.688 -0.022 -0.688 -0.037 -0.684 -0.044 Z M -0.309 -0.753 C -0.331 -0.759 -0.362 -0.759 -0.375 -0.784 C -0.376 -0.800 -0.377 -0.822 -0.378 -0.844 C -0.372 -0.860 -0.366 -0.877 -0.356 -0.884 C -0.342 -0.899 -0.324 -0.904 -0.309 -0.897 C -0.297 -0.887 -0.287 -0.878 -0.278 -0.863 C -0.269 -0.838 -0.275 -0.803 -0.278 -0.791 C -0.278 -0.778 -0.297 -0.759 -0.297 -0.762 Z M 0.250 -0.972 C 0.272 -0.969 0.297 -0.947 0.303 -0.919 C 0.306 -0.897 0.303 -0.887 0.291 -0.856 C 0.306 -0.872 0.269 -0.841 0.263 -0.838 C 0.241 -0.838 0.225 -0.847 0.212 -0.866 C 0.200 -0.872 0.203 -0.897 0.206 -0.922 C 0.211 -0.942 0.223 -0.955 0.237 -0.969 Z M 0.778 -0.328 C 0.800 -0.313 0.809 -0.294 0.803 -0.281 C 0.803 -0.263 0.803 -0.247 0.794 -0.228 C 0.791 -0.216 0.778 -0.200 0.750 -0.197 C 0.725 -0.200 0.715 -0.214 0.703 -0.225 C 0.702 -0.245 0.707 -0.268 0.719 -0.284 C 0.727 -0.303 0.742 -0.316 0.762 -0.334 Z"; +// Zは廃止されました! +const testPath = "M -0.438 -0.406 C -0.501 -0.616 -0.702 -0.606 -0.800 -0.519 Q -0.898 -0.431 -0.731 -0.275 Q -0.622 -0.084 -0.313 -0.213 Q -0.003 -0.341 0.319 -0.103 Q 0.634 0.009 0.444 0.216 C 0.253 0.422 0.259 0.561 0.084 0.631 C -0.091 0.701 -0.309 0.647 -0.491 0.609 C -0.672 0.572 -0.788 0.413 -0.741 0.225"; -// これを回転させる(フリップあり) -/* -const utuwa = "M 0.000 -0.388 Q 0.162 -0.386 0.355 -0.335 Q 0.549 -0.283 0.645 -0.003 Q 0.735 0.277 0.756 0.372 Q 0.764 0.411 0.773 0.421 Q 0.784 0.430 0.795 0.419 Q 0.802 0.409 0.797 0.362 Q 0.748 0.180 0.682 -0.025 Q 0.603 -0.218 0.549 -0.271 Q 0.460 -0.352 0.342 -0.384 Q 0.320 -0.393 0.321 -0.425 Q 0.321 -0.464 0.320 -0.523 Q 0.292 -0.544 0.279 -0.489 Q 0.276 -0.449 0.278 -0.406 Q 0.150 -0.447 0.001 -0.444 Z"; -*/ -// ツボの右半分 -const tubo = "M 0.001 -0.713 Q 0.323 -0.745 0.504 -0.583 Q 0.610 -0.483 0.630 -0.356 Q 0.651 -0.230 0.557 -0.103 Q 0.476 0.005 0.352 0.044 Q 0.231 0.086 0.166 0.194 Q 0.089 0.309 0.158 0.430 Q 0.187 0.477 0.232 0.502 Q 0.294 0.542 0.304 0.639 Q 0.317 0.708 0.362 0.697 Q 0.409 0.686 0.402 0.625 Q 0.397 0.553 0.335 0.497 Q 0.277 0.455 0.235 0.395 Q 0.194 0.338 0.257 0.213 Q 0.302 0.152 0.393 0.123 Q 0.499 0.084 0.598 -0.005 Q 0.712 -0.124 0.726 -0.267 Q 0.728 -0.352 0.683 -0.472 Q 0.642 -0.571 0.569 -0.657 Q 0.497 -0.740 0.406 -0.771 Q 0.295 -0.813 0.186 -0.813 L -0.004 -0.813 Z"; +const toonkigou = "M -0.045 -0.352 Q -0.125 -0.573 -0.045 -0.787 Q 0.004 -0.901 0.079 -0.909 Q 0.148 -0.895 0.175 -0.829 Q 0.226 -0.699 0.193 -0.541 Q 0.159 -0.405 0.069 -0.285 L 0.017 -0.222 L 0.057 -0.038 Q 0.154 -0.046 0.220 -0.000 Q 0.291 0.050 0.321 0.118 Q 0.355 0.231 0.300 0.337 Q 0.261 0.422 0.166 0.472 L 0.205 0.644 Q 0.226 0.763 0.167 0.848 Q 0.116 0.913 0.007 0.919 Q -0.113 0.920 -0.166 0.804 Q -0.209 0.705 -0.129 0.648 Q -0.048 0.606 0.015 0.682 Q 0.060 0.756 -0.002 0.822 Q -0.036 0.855 -0.089 0.852 C -0.062 0.893 0.027 0.893 0.085 0.870 Q 0.154 0.831 0.166 0.735 Q 0.175 0.673 0.155 0.604 L 0.130 0.483 Q 0.010 0.511 -0.087 0.470 Q -0.243 0.405 -0.305 0.273 Q -0.368 0.129 -0.303 -0.032 Q -0.270 -0.118 -0.178 -0.221 Q -0.122 -0.283 -0.045 -0.352 M -0.015 -0.379 Q 0.103 -0.466 0.140 -0.609 Q 0.172 -0.709 0.129 -0.774 Q 0.061 -0.786 0.008 -0.704 Q -0.055 -0.595 -0.036 -0.471 L -0.015 -0.379 Z M 0.161 0.426 Q 0.263 0.372 0.256 0.228 Q 0.217 0.087 0.082 0.087 L 0.161 0.426 M 0.121 0.444 L 0.045 0.088 Q -0.052 0.115 -0.059 0.233 Q -0.069 0.313 0.021 0.372 Q -0.122 0.326 -0.142 0.222 Q -0.154 0.099 -0.062 0.021 Q -0.021 -0.013 0.020 -0.027 L -0.013 -0.184 L -0.181 -0.005 Q -0.320 0.178 -0.161 0.368 Q -0.034 0.488 0.121 0.444"; const config = { outputScale:1, @@ -77,9 +116,9 @@ const config = { function createGUI(){ const gui = new lil.GUI(); - gui.add(config, "outputScale", 1, 320, 1); + gui.add(config, "outputScale", 1, 320, 0.01); gui.add(config, "output_yFlip"); - gui.add(config, "inputScale",1,320,1); + gui.add(config, "inputScale",1,320,0.01); gui.add(config, "input_yFlip"); gui.add(config, "fixCoord_x", 0, 640, 1); gui.add(config, "fixCoord_y", 0, 640, 1); @@ -89,11 +128,10 @@ function createGUI(){ /* let dlImg, img; function preload(){ - dlImg = loadImage("toon.png"); + dlImg = loadImage("tatiwaki.png"); } */ - function setup() { createCanvas(640, 640); createGUI(); @@ -185,9 +223,9 @@ function draw() { case "M": fill("gray"); break; case "L": fill("forestgreen"); break; case "Q": fill("red"); break; - case "QC": fill("orange"); break; - case "B": fill("blue"); break; - case "BC": fill("skyblue"); break; + case "QI": fill("orange"); break; + case "C": fill("blue"); break; + case "CI": fill("skyblue"); break; } } else { fill(255); } @@ -217,7 +255,7 @@ function keyTyped(){ // すっからかんでエンターの場合 if (keyIsDown(13) && pathId < 0) { - loadPathData(tubo); + loadPathData(toonkigou); pathId = 0; updateCurveLayer(); return; @@ -294,19 +332,19 @@ function mouseWheel(e){ scalingVelocity -= e.delta * 0.0005; } -// 選択点がQ/Bで、次の点もQ/Bの場合に、 +// 選択点がQ/Cで、次の点もQ/Bの場合に、 // その点の前後の点を見て、後の点を前の点と選択点に関して対称な位置に移す function alignControlPoint(){ if (pointId < 0) return; const path = paths[pathId]; const p = path[pointId]; - if (p.type !== 'Q' && p.type !== 'B') return; + if (p.type !== 'Q' && p.type !== 'C') return; // 次の点を見つけるの難しいね... // 単純に次の点を見て、Lなら無視、QCかBCなら...でOK. // というかQCかBCでいいだろ。それで判断しよう。 const nextPointType = path[pointId+1].type; - if (nextPointType !== 'QC' && nextPointType !== 'BC') return; - const q = (nextPointType === 'QC' ? path[pointId+2] : path[pointId+3]); + if (nextPointType !== 'QI' && nextPointType !== 'CI') return; + const q = (nextPointType === 'QI' ? path[pointId+2] : path[pointId+3]); const prevCP = path[pointId-1]; const nextCP = path[pointId+1]; nextCP.x = 2*p.x - prevCP.x; @@ -356,33 +394,33 @@ function mouseClicked(){ if (!keyIsDown(16)) { const lastPoint = currentPath[currentPath.length-1]; - const middlePoint = {x:(lastPoint.x + mPos.x) * 0.5, y:(lastPoint.y + mPos.y) * 0.5, type:"QC"}; + const middlePoint = {x:(lastPoint.x + mPos.x) * 0.5, y:(lastPoint.y + mPos.y) * 0.5, type:"QI"}; currentPath.push(middlePoint); currentPath.push({x:mPos.x, y:mPos.y, type:"Q"}); }else if(insertData.insertable){ const lastPoint = currentPath[insertData.prevId]; - const middlePoint = {x:(lastPoint.x + mPos.x) * 0.5, y:(lastPoint.y + mPos.y) * 0.5, type:"QC"}; + const middlePoint = {x:(lastPoint.x + mPos.x) * 0.5, y:(lastPoint.y + mPos.y) * 0.5, type:"QI"}; currentPath.splice(insertData.prevId+1, 0, {x:mPos.x, y:mPos.y, type:"Q"}); currentPath.splice(insertData.prevId+1, 0, middlePoint); } updateCurveLayer(); return; } - // Bキーで制御点とともに3次ベジエ点を追加 + // Cキーで制御点とともに3次ベジエ点を追加 - if (keyIsDown(66)){ + if (keyIsDown(67)){ if (!keyIsDown(16)){ const lastPoint = currentPath[currentPath.length-1]; - const firstPoint = {x:(lastPoint.x*2.0 + mPos.x) / 3.0, y:(lastPoint.y*2.0 + mPos.y) / 3.0, type:"BC"}; - const secondPoint = {x:(lastPoint.x + mPos.x*2.0) / 3.0, y:(lastPoint.y + mPos.y*2.0) / 3.0, type:"BC"}; + const firstPoint = {x:(lastPoint.x*2.0 + mPos.x) / 3.0, y:(lastPoint.y*2.0 + mPos.y) / 3.0, type:"CI"}; + const secondPoint = {x:(lastPoint.x + mPos.x*2.0) / 3.0, y:(lastPoint.y + mPos.y*2.0) / 3.0, type:"CI"}; currentPath.push(firstPoint, secondPoint); - currentPath.push({x:mPos.x, y:mPos.y, type:"B"}); + currentPath.push({x:mPos.x, y:mPos.y, type:"C"}); }else if(insertData.insertable){ const lastPoint = currentPath[insertData.prevId]; - const firstPoint = {x:(lastPoint.x*2.0 + mPos.x) / 3.0, y:(lastPoint.y*2.0 + mPos.y) / 3.0, type:"BC"}; - const secondPoint = {x:(lastPoint.x + mPos.x*2.0) / 3.0, y:(lastPoint.y + mPos.y*2.0) / 3.0, type:"BC"}; - currentPath.splice(insertData.prevId+1, 0, {x:mPos.x, y:mPos.y, type:"B"}); + const firstPoint = {x:(lastPoint.x*2.0 + mPos.x) / 3.0, y:(lastPoint.y*2.0 + mPos.y) / 3.0, type:"CI"}; + const secondPoint = {x:(lastPoint.x + mPos.x*2.0) / 3.0, y:(lastPoint.y + mPos.y*2.0) / 3.0, type:"CI"}; + currentPath.splice(insertData.prevId+1, 0, {x:mPos.x, y:mPos.y, type:"C"}); currentPath.splice(insertData.prevId+1, 0, secondPoint); currentPath.splice(insertData.prevId+1, 0, firstPoint); } @@ -401,7 +439,7 @@ function mouseClicked(){ currentPath.splice(pointId-1, 2); pointId = -1; break; - case "B": + case "C": currentPath.splice(pointId-2, 3); pointId = -1; break; @@ -411,6 +449,12 @@ function mouseClicked(){ if (paths.length === 0){ //paths.push([]); pathId = -1; + } else if (pathId === paths.length) { + // 最後尾のパスを消した場合のバグを修正 + // たとえばpathIdが1でもともと長さ4の場合減って3になっても + // 0,1,2の1になるけど3だと0,1,2のどれでもなくエラーになる + // うっかりしてたわ + pathId = paths.length-1; } pointId = -1; break; @@ -449,7 +493,7 @@ function updateCurveLayer(){ curveLayer.stroke(config.strokeColor); addBezier2(curveLayer, prevQ, cp, p); break; - case "B": + case "C": const prevB = path[i-3]; const cp0 = path[i-2]; const cp1 = path[i-1]; @@ -477,7 +521,7 @@ function updateSeparateLayer(){ const keyPoints = []; for(let i=0; i0) result.push(subData.slice()); + subData.length = 0; + subData.push(createVector(Number(cmdData[i+1]), Number(cmdData[i+2])).mult(parseScale)); + i+=2; break; + case "L": + const p = subData[subData.length-1]; + const q = createVector(Number(cmdData[i+1]), Number(cmdData[i+2])).mult(parseScale); + const lineLength = q.dist(p); + for(let lengthSum=0; lengthSum= 1; i--){ + const p = points[i]; + const q = points[i-1]; + if (p.dist(q) < threshold){ + //console.log("merge"); + points.splice(i,1); + } + } + if (closed) { + // 頭に戻る場合はそれも排除する + if (points[0].dist(points[points.length-1]) < threshold) { + points.pop(); + } + } + } + + /* + mergePointsAll(contours, options={}) + contours: p5のベクトル列の配列 + options: mergePointsと同じ。同じ内容がすべての点列に適用される。 + なので、closedとそれ以外が混じっているなら個別に対処する必要がある。 + そこは柔軟性を持たせたいところだけどね。めんどくさいわね。optionsとしてoptionsの配列を許せばいけるか? + ただほとんどの場合は閉路とそれ以外が混じることはほぼ無いからな。 + */ + function mergePointsAll(contours, options = {}){ + for(let contour of contours) { + mergePoints(contour, options); + } + } + + /* + evenlySpacing + points:p5のベクトル列 + options: + minLength: 最小長さ。これより長い間隔がある場合、点を挿入する + closed: 閉じている場合は頭も考慮する + 等間隔にする処理 + まあ、ほぼ、ね。 + 広い場合は点を挿入する...まあ線形補間だが。 + んでlerpとか使ってなるたけ同じ間隔になるように塩梅する処理 + textDataって割とそこら辺雑だからな + 見た目的にそこまで変化はないけど + まあそれなりに有用です + */ + // 等間隔化にもclosed optionを導入したいな + function evenlySpacing(points, options = {}){ + const {minLength = 1, closed = false} = options; + // minLengthより長い長さがある場合に、点を挿入する + let newPoints = []; + newPoints.push(points[0]); + + for(let i=1; i minLengthかどうか見るとか。<とか。 + let localSum = 0; + const result = [newPoints[0]]; + const lastPoint = createVector(); + lastPoint.set(result[0]); + for(let i=1; i minLength) { + // たとえば2.1と1の場合は3分割するが1.9と1の場合は2分割する + const m = floor(distance/minLength) + 1; + for(let k=1; k rank[bRoot]){ + parent[bRoot] = aRoot; + }else if(rank[bRoot] > rank[aRoot]){ + parent[aRoot] = bRoot; + }else if(aRoot != bRoot){ + parent[bRoot] = aRoot; + rank[aRoot] = rank[aRoot] + 1; + } + } + for(let i = 0; i < 2; i++){ + for(let q of query){ + Union(q[0], q[1]); + } + } + let uf = []; + for(let i = 0; i < n; i++){ + uf.push({id:i, pt:parent[i]}); + } + uf.sort((x, y) => { + if(x.pt < y.pt){ return -1; } + if(x.pt > y.pt){ return 1; } + return 0; + }); + uf[0].lv = 0; + let count = 1; + for(let i = 1; i < n; i++){ + if(uf[i].pt == uf[i-1].pt){ + uf[i].lv = uf[i-1].lv; + }else{ + uf[i].lv = uf[i-1].lv + 1; + count++; + } + } + uf.sort((x, y) => { + if(x.id < y.id){ return -1; } + if(x.id > y.id){ return 1; } + return 0; + }); + // 代表系の集合もあると便利だと思う。 + const represents = new Array(count); + const members = new Array(count); + for(let i=0; ic, b--->cの外積を取る操作。 + そんだけ。 + */ + function getDet2(a,b,c){ + return (c.x-a.x) * (c.y-b.y) - (c.x-b.x) * (c.y-a.y); + } + + /* + getIntersections(a,b,c,d,threshold=1e-9) + a,b,c,d: ベクトル + イメージ的にはa----bとc----dが線分で、これらの交わり具合を調べるための関数。 + 割と複雑なことをしているのだ + threshold: 一致判定の閾値。default:1e-9 + 戻り値:{flag, intersections} + flagがdisjointの場合は交点無し + flagがoverlapの場合というのは重なるケースである + この場合たとえばa----bの上にcやdが来る場合それを入れたりする + c----dの上にaやbを乗せたりするわけ + flagがsingleなのが通常の交叉の場合 + intersectionsの中にオブジェクトが入ってる + pは座標、typeのinsert_abやcdはa--bの上かc--dの上かという話 + んでたとえばratioが0.3ならa---bの上に0.3:0.7で内分した位置にその点が置かれる + まあそんな感じです + overlapはもともと無視していたんですがとある事情により追加されました + */ + function getIntersection(a,b,c,d,threshold = 1e-9){ + // a,b,c,dは2次元ベクトル + // a-b と c-dが交わるかどうか調べる + const abc = getDet2(a,b,c); + const abd = getDet2(a,b,d); + const cda = getDet2(c,d,a); + const cdb = getDet2(c,d,b); + if (abc<0&&abd<0) return {flag:"disjoint"}; + if (abc>0&&abd>0) return {flag:"disjoint"}; + if (cda<0&&cdb<0) return {flag:"disjoint"}; + if (cda>0&&cdb>0) return {flag:"disjoint"}; + + const abr = Math.abs(abc) + Math.abs(abd); + const cdr = Math.abs(cda) + Math.abs(cdb); + + // 4点が一直線上にあるとみなせる場合は + // abとcdのうち距離が大きい方を取り + // それにより4点が同一直線上にあるとみなし + // 内積を使って大小判定を実行する + + if (abr < threshold || cdr < threshold){ + // abかcdが0とみなせる場合は処理しない + const distAB = a.dist(b); + const distCD = c.dist(d); + if(distAB < threshold || distCD < threshold) return {flag:"disjoint"}; + const intersections = []; + const abab = distAB*distAB; + const ab_c = ((b.x-a.x)*(c.x-a.x)+(b.y-a.y)*(c.y-a.y))/abab; + const ab_d = ((b.x-a.x)*(d.x-a.x)+(b.y-a.y)*(d.y-a.y))/abab; + if(ab_c >= 0 && ab_c <= 1) { + intersections.push({type:"insert_ab", p:c.copy(), ratio:ab_c}); + } + if(ab_d >= 0 && ab_d <= 1) { + intersections.push({type:"insert_ab", p:d.copy(), ratio:ab_d}); + } + const cdcd = distCD*distCD; + const cd_a = ((d.x-c.x)*(a.x-c.x)+(d.y-c.y)*(a.y-c.y))/cdcd; + const cd_b = ((d.x-c.x)*(b.x-c.x)+(d.y-c.y)*(b.y-c.y))/cdcd; + if(cd_a >= 0 && cd_a <= 1) { + intersections.push({type:"insert_cd", p:a.copy(), ratio:cd_a}); + } + if(cd_b >= 0 && cd_b <= 1) { + intersections.push({type:"insert_cd", p:b.copy(), ratio:cd_b}); + } + return {flag:"overlap", intersections}; + } + + // a---cp---bの比率 + // c---cp---dの比率 + const ratioA = Math.abs(cda)/cdr; + const ratioC = Math.abs(abc)/abr; + const intersection = p5.Vector.lerp(a, b, ratioA); // 共通にする + + // pには交差点を入れる + return { + flag:"single", intersections:[ + {type:"insert_ab", p:intersection, ratio:ratioA}, + {type:"insert_cd", p:intersection, ratio:ratioC} + ] + } + } + + /* + createDisjointPaths(contours, mergeThreshold=1e-9) + contours: p5のベクトル列の配列。すべて閉路であることが前提。交叉については自由。自己交叉もOK. + mergeThreshold: 点をまとめる際のマージの基準 default:1e-9 + 入力はいわゆる2次元のベクトル列です。 + 3次元では使えないです。まあp5の_triangulateに比べたら劣化してるかもしれないけど + どうでもいいですね。あれ使いづらいので。 + 出力: + {islands, cycles, vertices:mergedVertices, edges:mergedEdges}; + islandsはislandの配列 + islandは互いに分かれた最小単位のサイクルで + verticesとedgesという形で頂点列と辺列が格納されている + cyclesは互いにdisjointなサイクルの配列 + つまりisland同士が接している場合にそれらをつなげるわけ + そのうえで + 互いに一切接触しないサイクルの配列 + 同じくverticesとedgesからなる + verticesとedgesはデータ配列 + indexの参照先 + verticesのpにはベクトルが入っている + edgesのindicesには両端のverticeのindexが入っている + 以上 + ですからここからcyclesToCyclesに持っていく場合 + cyclesの中身をindexからベクトルに翻訳する必要があるわけ + 後は同じ。 + + そういうわけなので + ここからcyclesToCyclesに持っていくなら + cyclesにおいてそのindexのverticeのpを取り出す必要があるのよね + */ + + function createDisjointPaths(contours, mergeThreshold = 1e-9){ + + // cpCheckArrayをcontourの数だけ用意する + const cpCheckArrays = new Array(contours.length); + for(let i=0; imのときだけサーチする... + + // 平面走査 + // バグってますね。原因不明.... + // direction考慮したら直った + + // わかんない + // わかるのはlibtessが速いってことだけ + // テッセレーションに特化してる分余計なことしてないから + // 余計なことしなければ速いんだろうけど... + // まあパス分割とかしなくても三角形に分けるの可能だからな + // ... + // OpenProcessingに落とすともう7秒とかかかる + // うんざりする + // やめよう + + // 調べたんですが + // 平面走査のところだけ単独で調べると + // 半分になってますね(おおよそね) + // 何回調べてもおよそ半分 + // ここについては改善されたようです + // たとえば得られたcrossPointsってxの小さい順に得られるので + // それ使ってマージを楽にやれば + // いい感じになるんじゃないかと。??しらんけど。 + + // 他のパートも軒並み遅い + // というか「交叉検出」「マージング」「それ以降の処理」の負荷が + // おおよそとんとんなので + // 交叉検出だけ速くても仕方ないんですよね + // おそらくlibtessは交叉検出から一気にテッセレーションにもっていってるんで + // 速いんでしょう + // やり方は不明... + + const contourEdges = []; + let eid = 0; + for(let i=0; i { + if(ev0.x > ev1.x) return -1; + if(ev0.x < ev1.x) return 1; + if(ev0.inout > ev1.inout) return -1; + if(ev0.inout < ev1.inout) return 1; + return 0; + }); + const currentEdges = []; + + while(events.length > 0){ + const ev = events.pop(); + const e1 = ev.target; + if(ev.inout === 1){ + for(let m=currentEdges.length-1; m>=0; m--){ + const e0 = currentEdges[m]; + if(e0.eid === e1.eid){ + currentEdges.splice(m,1); + break; + } + } + continue; + } + for(let k=0; k0){ + // cpaをratioの小さい順にsortしてpoints[i]のあとに放り込んでいく + cpa.sort((u, v) => { + if (u.ratio < v.ratio) return -1; + if (u.ratio > v.ratio) return 1; + return 0; + }); + for(let k=0; k=0; i--){ + const e0 = mergedEdges[i]; + if(e0.double) continue; + for(let k=i-1; k>=0; k--){ + const e1 = mergedEdges[k]; + if(e1.double) continue; + if(e0.indices[0] === e1.indices[0] && e0.indices[1] === e1.indices[1]){ + e0.double = true; + e1.double = true; + break; + } + } + } + for(let i=mergedEdges.length-1; i>=0; i--){ + if(mergedEdges[i].double){ + mergedEdges.splice(i,1); + } + } + // このとき辺が出ていない頂点が発生する可能性があるのでそれを排除する + // そのときindexが変わってしまうので + // やめよう + // overlap属性をoffにすれば問題ない + // 無視されるので + + // たとえばここでmergedVerticesを走査して + // 事前にmergedEdgesにoverlap:falseとかフラグを付けておいて + // verticeごとにindicesの配列が + // 上見ればわかるけどこれ全部[a,b](a { + if (e0.xMin < e1.xMin) return -1; + if (e0.xMin > e1.xMin) return 1; + return 0; + }); + // 頂点に接続辺のindexを集める + for(let k=0; k { + const e0 = mergedEdges[i0]; + const e1 = mergedEdges[i1]; + const p0 = mergedVertices[e0.indices[0] + e0.indices[1] - p.index]; + const p1 = mergedVertices[e1.indices[0] + e1.indices[1] - p.index]; + const angle0 = Math.atan2(p0.p.y - p.p.y, p0.p.x - p.p.x); + const angle1 = Math.atan2(p1.p.y - p.p.y, p1.p.x - p.p.x); + if (angle0 > angle1) return -1; + if (angle0 < angle1) return 1; + return 0; + }); + } + // mergedUnion.vertices = mergedVertices; + // mergedUnion.edges = mergedEdges; + // mergedUnions.push(mergedUnion); + //} + // OK + // あとはmergedUnionごとにislandsを作るんですが + // このとき最終的に単純閉曲線にすることを考えると + // 辺に向きつけが設定されないと不便なので + // islandは頂点の列と辺の列を両方用意した方がいいと思う + // んで辺たちはバラバラになりますから + // 頂点の列を見ながら + // 向きがそのようになるようにindicesを必要に応じてsortする必要があるね + // 0番->1番となるようにね + + //for(const mergedUnion of mergedUnions){ + // const {vertices:mergedVertices, edges:mergedEdges} = mergedUnion; + const islands = []; + // 最初のindexと次のindex + let startIndex = 0; + let nextIndex = 0; + let arrivedIndex; + let islandVertices = []; + let islandEdges = []; + + while(1){ + + if(nextIndex === startIndex) { + + // 最初にislandVerticesとislandEdgesのlengthを見て + // 正ならislandsにぶち込む + // コピーをぶち込んで初期化 + if(islandVertices.length > 0){ + // dirtyFlagはサイクル分解用 + const island = { + vertices:islandVertices.slice(), edges:islandEdges.slice(), + dirtyFlag:false + }; + islands.push(island); + islandVertices.length = 0; + islandEdges.length = 0; + } + + // 次のループの準備 + // 最初にdirtyFlagがfalseであるようなedgeを取るが、 + // このedgeが採用されるとは限らないことに注意する。 + let startEdgeIndex = -1; + for(let k=0; k=0; i--){ + // 接続辺のindexをサーチしていく + const index = mergedVertices[startIndex].indices[i]; + if(!mergedEdges[index].dirtyFlag){ + arrivedIndex = index; + nextIndex = mergedEdges[index].indices[0] + mergedEdges[index].indices[1] - startIndex; + mergedEdges[index].dirtyFlag = true; + islandVertices.push(startIndex); + islandEdges.push(index); + break; + } + } + } + } + + const p = mergedVertices[nextIndex]; + // p.indicesの元をサーチしていく。 + // 最初にarrivedEdge.indexになるところを特定 + let searchStartIndex; + for(let i=0; i=0; i--){ + const v = overlappedVertices[i]; + v.overlap = false; + const neighbors = v.indices; // 偶数個 + const someIndex = mergedEdges[neighbors[0]].islandId; // どれかひとつの + for(let i=1; i= 0){ + const subMountains = []; + for(let i=cycleObjects.length-1; i>=0; i--){ + const co = cycleObjects[i]; + if(co.parents.length === indicator){ + subMountains.push([co]); + } + } + for(const mountain of subMountains){ + for(const childIndex of mountain[0].children){ + const child = cycleObjects[childIndex] + if(child.parents.length === indicator+1){ + mountain.push(child); + } + } + mountain.sort((c0, c1) => { + if(c0.parents.length > c1.parents.length) return -1; + if(c0.parents.length < c1.parents.length) return 1; + if(c0.xMaxValue < c1.xMaxValue) return -1; + if(c0.xMaxValue > c1.xMaxValue) return 1; + return 0; + }); + } + mountains.push(...subMountains); + indicator -= 2; + } + // これでいける? + + const resultCycles = []; + const subCycleArrays = []; + + for(const mountain of mountains){ + const initialCycle = mountain.pop(); + let resultCycle = initialCycle.indices.slice(); + // subCycleの配列. clockwiseなども考慮する。側面の構成などに使う。 + const subCycleArray = [initialCycle.indices.slice()]; + if(mountain.length === 0) { + resultCycles.push(resultCycle); + // 一つの場合はここで入れないといけないんだ + subCycleArrays.push(subCycleArray); + continue; + } + // 事前に一番最初の段階でのxMaxを取っておいてそれを使わないといけないんだ + // それがずっと通用しつづける + // なぜなら内部だから + + // これを先に採用しておき、右に線を伸ばす際のガイドとする + const outerXMax = getXMax(cycles[initialCycle.cycleIndex]).value; + + while(mountain.length > 0){ + const target = mountain.pop(); + const L0 = target.indices.length; + const L1 = resultCycle.length; + // targetとresultCycleをつなげる + // まずtargetのxMaxIndexからx最大点を取得してその点から + // resultCycleのxMaxを半分とする対蹠点への線分を作り + // 線分交叉で得られるresultCycleの方の線分との交点を取る + // single/overlapで得られる交点をかき集める + // 一番近いものを取りそれに対応する辺を用意する + // 辺の両端との距離が1e-10未満であればそのまま採用する + // そうでない場合は時計回りですから + // 大きい方を取る + // 交点、大きい方、targetの最大点で三角形を作る + // 三角形の内部にresultCycleの点がなければ大きい方を採用する + // あればそのうちでatan2の絶対値が最小のものを採用する + // atan2は最大点から交点に向かうやつとその点に向かうやつで取る + // 採用した点がresultCycleの何番目か記録しておく + // targetのindicesを用意する + // resultCyclesを、採用点から採用点まで時計回りに進み、 + // そこで最大点に行き、反時計回りにtargetのindicesを進み、最大点に戻る。 + // そういうサイクルとしてnewCycleを作ってそれで置き換える + // 次のループに進む。 + const {xMaxIndex, xMaxValue} = target; + + const xMaxPoint = allVertices[target.indices[xMaxIndex]]; + + const horizon = xMaxPoint.y; + // outerXMaxは先に計算しておく + const outerPoint = createVector(outerXMax + 100, horizon); + // (xMaxPoint, outerPoint)で線分 + // これとresultCycleで線分交叉やって交わる辺を探す + let crossPoints = []; + for(let i=0; i { + if(cp0.ratio < cp1.ratio) return -1; + if(cp0.ratio > cp1.ratio) return 1; + return 0; + }); + const cp = crossPoints[0]; // ratio最小 + const left = allVertices[resultCycle[cp.index]]; + const right = allVertices[resultCycle[(cp.index+1)%L1]]; + let cutIndex; + // leftに近ければleft,rightに近ければright. + // どっちも違うならxMaxPoint, cp.p, rightで三角形を作り + // 若干面倒な処理をする + // rightか、または内部の点のうちxMaxPoint --> cp.pとの成す角が + // 最小の点を取る + if(cp.p.dist(left) < 1e-10){ + cutIndex = cp.index; + } else if(cp.p.dist(right) < 1e-10){ + cutIndex = (cp.index+1)%resultCycle.length; + } else { + const candidate = []; + for(let k=0; k { + if(cd0.angle < cd1.angle) return -1; + if(cd0.angle > cd1.angle) return 1; + return 0; + }); + // 0番を取る + cutIndex = candidate[0].index; + } + } + // cutIndexが決まったので統合に入る + const newCycle = [resultCycle[cutIndex]]; + for(let k=1; k<=L1; k++){ + newCycle.push(resultCycle[(cutIndex+k) % L1]); + } + + const subCycle = []; + //newCycle.push(target.indices[xMaxIndex]); + subCycle.push(target.indices[xMaxIndex]); + // parentsの数が深さで、これが奇数の場合逆にし、偶数の場合順方向。 + const clockwiseAdding = ( + target.parents.length % 2 === 1 ? false : true + ); + // 残りの点を追加 + for(let k=1; k<=L0; k++){ + if(clockwiseAdding){ + //newCycle.push(target.indices[(xMaxIndex+k) % L0]); + subCycle.push(target.indices[(xMaxIndex+k) % L0]); + } else { + //newCycle.push(target.indices[(xMaxIndex-k+L0) % L0]); + subCycle.push(target.indices[(xMaxIndex-k+L0) % L0]); + } + } + newCycle.push(...subCycle); + resultCycle = newCycle; + subCycle.pop(); // 重複排除 + subCycleArray.push(subCycle); + } + // resultCyclesにresultCycleを放り込む + resultCycles.push(resultCycle); + subCycleArrays.push(subCycleArray); + // 次のmountainへ進む... + } + // allVerticesとresultCyclesを + // {vertices:allVertices, cycles:resultCycles} + // として出力する + return { + vertices:allVertices, cycles:resultCycles, subCycleArrays:subCycleArrays + }; + // この形式のcycleであればearcutが適用でき、テッセレーションが可能。 + } + + // subroutines for cyclesToCycles() (getIntersections()は流用) + + function getXMax(contour){ + // x座標が最大となるindexを返すだけ。入力は閉路。何でもOK. + let xMax = -Infinity; + let xMaxIndex = -1; + for(let i=0; ibとa-->cのなす角。2次元専用。 + // a-->bをa-->cにする向きが正の時、正の値。 + // 3次元には使えないのであしからず。 + const cv = (b.x-a.x)*(c.y-a.y) - (b.y-a.y)*(c.x-a.x); + const dv = (b.x-a.x)*(c.x-a.x) + (b.y-a.y)*(c.y-a.y); + return Math.atan2(cv, dv); + } + + function isClockwise(cycle){ + // 時計回りの時trueを返す + // ベクトルの角度の変化を全部足すだけ + // 前提として3点以上...だがまあ問題ないだろ + let angleSum = 0; + if(cycle.length < 3) return true; + for(let i=0; i 0) return true; + return false; + } + + + function insideCycle(p, cycle){ + // pがcycleの内部に含まれているときtrueを返す + // cycleはベクトル列を想定 + // cycleは単純閉曲線。向きはどっちでもOK + // 角度を全部足してPI/2より大きければOK + let angleSum = 0; + for(let i=0; i 6) return true; + return false; + } + + /* + executeEarcut(vertices, indices=[], threshold=1e-9) + vertices: p5のベクトル列。単純閉曲線でなければならない。 + 穴が開いていてもいいが、深さが2以上だと確実にバグる。まあ当然だが。 + indices: 重複頂点を扱う場合はインデックス配列で指定することもできるわけで、 + これが[]でない場合はそれを採用する。[]の場合、verticesを順繰りに番号付けすることになる。 + まああまり使わないけどな...基本indices指定。つまりcyclesToCyclesで得られるそれ、をそのまま使うことを想定してる。 + threshold: 相等判定のしきいち。default:1e-9 + 出力:{vertices, faces} + facesにはindexが3つずつ順繰りに入ってる。3つずつ取り出してverticesから参照して三角形を復元する。 + すべて正の向きなので心配ないです。 + */ + function executeEarcut(vertices, indices = [], threshold = 1e-9){ + const cycle = []; + if(indices.length === 0){ + for(let i=0; i= 3){ + if(cycle.length === 3){ + // 最後の三角形をぶちこんで終了 + result.push(...earcutTriangle(vertices, cycle)); + break; + } + // 4以上の場合は帰納法による + // 下準備の過程で点を排除する場合には点の数を減らしてcontinue; + // 角度の総和と角度の列を用意 + const angleArray = []; + let angleSum = 0; + let signSum = 0; // 符号の和の絶対値をpoints.lengthと比較する + // 角度を計算する + // 辺のなす角を記録していく。つぶれてないかどうかのチェックもここでやる。 + let crushedTriangleId = -1; + for(let i=0; iqをq->rに重ねる際の角度の変化を記録していく + const cp = (q.x-p.x) * (r.y-q.y) - (q.y-p.y) * (r.x-q.x); + // 三角形がつぶれてる場合は中間点を排除する + // 直進あるいは出戻り + if (Math.abs(cp) < threshold) { + crushedTriangleId = (i+1)%cycle.length; + break; + } + const ip = (q.x-p.x) * (r.x-q.x) + (q.y-p.y) * (r.y-q.y); + const angle = Math.atan2(cp, ip); + angleArray.push(angle); + angleSum += angle; + signSum += Math.sign(angle); // 1か-1を加えていく. 凸判定に使う。 + } + // つぶれてる場合はidを排除してcontinue; + if (crushedTriangleId >= 0) { + cycle.splice(crushedTriangleId, 1); + continue; + } + // 凸の場合はそのまま答えが出る。判定は符号の和で調べられる。 + // つまりこれのためで、つぶれてるようなのがあるとこれの判定ができないんよ + // 結果が簡単に出るのでありがたい。第二引数で符号。数字そのままでOK. + if(Math.abs(signSum) === cycle.length){ + result.push(...earcutConvexPolygon(cycle, signSum)); + // 処理を終了する + break; + } + // つぶれなく、凸でもないケース。 + // 向き付け。1か-1. + // orientationを掛けて正なら第一の条件を満たす + const orientation = Math.sign(angleSum); + // 確実に存在する。信じなさい。 + let cuttingEarId = -1; + for(let i=0; i 0) { + result.push(cycle[i], cycle[(i+1)%cycle.length], cycle[(i+2)%cycle.length]); + } else { + result.push(cycle[i], cycle[(i+2)%cycle.length], cycle[(i+1)%cycle.length]); + } + cuttingEarId = (i+1)%cycle.length; + // ひとつでもカット出来たら終了 + break; + } + // もちろんあるが、万が一の場合は抜けてしまおう + if(cuttingEarId < 0){ + console.log("failure"); + break; + } + cycle.splice(cuttingEarId, 1); + } + + // おわったので出力 + return {vertices:vertices, faces:result}; + } + + // subroutines for executeEarcut + // 三角形の面積がめちゃ小さい場合にfalse、そういう閾値 + function insideTriangle(p0,p1,p2,p, threshold = 1e-9){ + // pがp0,p1,p2の三角形の内部にあるかどうか調べるだけ + // ベクトルを使った方が分かりやすいけどね。 + const a11 = Math.pow(p1.x-p0.x,2) + Math.pow(p1.y-p0.y,2); + const a12 = (p1.x-p0.x)*(p2.x-p0.x) + (p1.y-p0.y)*(p2.y-p0.y); + const a22 = Math.pow(p2.x-p0.x,2) + Math.pow(p2.y-p0.y,2); + const qDotU = (p1.x-p0.x)*(p.x-p0.x) + (p1.y-p0.y)*(p.y-p0.y); + const qDotV = (p2.x-p0.x)*(p.x-p0.x) + (p2.y-p0.y)*(p.y-p0.y); + const d = a11*a22-a12*a12; + if (abs(d) < threshold) return false; + const a = a22*qDotU - a12*qDotV; + const b = -a12*qDotU + a11*qDotV; + if(a>0 && b>0 && a+b0の場合、p,q,rで時計回り + // cp<0の場合、p,r,qで時計回り + if(cp>0) return indices.slice(); + return [indices[0], indices[2], indices[1]]; // 逆! + } + + // 凸ケース。いきなり終了できる。 + // 0,1,2,3,4,...だとして + // orientationが1なら0,1,2,0,2,3,0,3,4,... + // -1なら0,2,1,0,3,2,0,4,3,...そんな感じ + // vertices要らんわな。indexと向き付けだけでいい。 + // 0,1,2じゃないわ!!!indices[0],[1],[2]だわ!! + function earcutConvexPolygon(indices, orientation = 1){ + const result = []; + for(let i=1; i 0) { + result.push(indices[0], indices[i], indices[i+1]); + } else { + result.push(indices[0], indices[i+1], indices[i]); + } + } + return result; + } + + // p5.Geometry関連 + // 有用なメソッドも沢山出来たんですが + // まああんま使いたくないので + // 独自にいろいろ用意しましょ + // 使いづらいんだよな + + /* + createPlaneMeshFromCycles(options={}) + options: + result: cyclesToCyclesの結果が入る。 + planeHeight: 高さ。まあ要らんだろうが。 + cyclesToCyclesで出したサイクル群からメッシュを生成する。平面です。 + geomを出力させる形になってるのは柔軟性のため。だって色とか付けたいでしょ? + 変形もしたいでしょう。その際に不便なんだよな。 + */ + // 面だけ + function createPlaneMeshFromCycles(options = {}){ + const {result, planeHeight = 0} = options; + const {vertices, cycles, subCycleArrays} = result; + const geom = new p5.Geometry(); + + // 面だけ。シンプル。 + + const vn = vertices.length; + + for(let i=0; i