多角形を、直線に沿って滑らずに転がす
はじめに
下図のように、GeoGebraで、多角形を、直線に沿って滑らずに転がす(以下単に「転がす」という)方法を紹介する。
解説
前提オブジェクト
多角形poly1
直線j
数値α(転がす角度を指定するのに用いる)
※さしあたり、-6π<=α<=6πで、スライダーを作っておく。
環境整備
maxLength = Distance[Corner[1], Corner[3]]
maxLengthは、アプレット画面の対角線の長さである。直線jのうち、画面上で見える部分は、この長さを超えることはない。したがって、poly1を転がす範囲は、maxLengthの2倍あれば十分だろう。
vertList = {Vertex[poly1]}
poly1の頂点のリスト。
ステップ① poly1を直線まで持ってくる
poly1は、直線jから離れていることもあれば、直線jと交わっていることもあるだろう。都合良く直線jに接している場合ばかりではないということだ。まずは、poly1を、直線jに接するように平行移動させよう。
vectorList = Zip[Vector[β, ClosestPoint[j, β]], β, vertList]
poly1の各頂点から、直線jに垂線を下ろし、それをベクトルとして取得する(vectorList)。
productList = Zip[γ ⊗ UnitVector[j], Vector[γ], vectorList]
外積を用いて、vectorListの各要素であるベクトルの向きを判別する(productList)。
negativeVectors = RemoveUndefined[Zip[If[δ < 0, Vector[ε], ?], δ, productList, Vector[ε], vectorList]]
positiveVectors = RemoveUndefined[Zip[If[δ ≥ 0, Vector[ε], ?], δ, productList, Vector[ε], vectorList]]
向きに応じてnegativeとpositiveに分ける(negativeVectors、positiveVectors)。
lengthOfNegativeVectors = Zip[Length[η], Vector[η], negativeVectors]
sortedNegativeVectors = Sort[negativeVectors, lengthOfNegativeVectors]
lengthOfPositiveVectors = Zip[Length[ζ], Vector[ζ], positiveVectors]
sortedPositiveVectors = Sort[positiveVectors, lengthOfPositiveVectors]
negativeVectors、positiveVectorsそれぞれ、ベクトルの長さに応じて昇順に並べ替える(sortedNegativeVectors、sortedPositiveVectors)
positiveMaxAndnegativeMax = Flatten[{Last[sortedPositiveVectors], Last[sortedNegativeVectors]}]
negativeVectorsのうち最長のベクトルと、positiveVectorsのうち最長のベクトルを1本ずつ選んで、リストを作る(positiveMaxAndnegativeMax)。
shorterElementOfpositiveMaxAndnegativeMax = First[Sort[positiveMaxAndnegativeMax, Zip[Length[μ], Vector[μ], positiveMaxAndnegativeMax]]]
positiveMaxAndnegativeMaxのうち、長さが短い方だけを残す(shorterElementOfpositiveMaxAndnegativeMax)。
positiveMinAndNegativeMin = RemoveUndefined[Flatten[{First[sortedPositiveVectors], First[sortedNegativeVectors]}]]
negativeVectorsのうち最短のベクトルと、positiveVectorsのうち最短のベクトルを1本ずつ選んで、リストを作る(positiveMinAndNegativeMin)。
vectorToAttach = If[Length[positiveMinAndNegativeMin] ≟ 1, Vector[Element[positiveMinAndNegativeMin, 1]], Vector[Element[shorterElementOfpositiveMaxAndnegativeMax, 1]]]
Length[positiveMinAndNegativeMin]の要素数が1の場合には、poly1が直線jと交わらない。この場合には、positiveMinAndNegativeMinの唯一の要素であるベクトルを返す(vectorToAttach;If条件を満たす場合)。
poly1が直線jと交わる場合には、shorterElementOfpositiveMaxAndnegativeMaxの唯一の要素であるベクトルを返す(vectorToAttach;else)。
poly2 = Translate[poly1, vectorToAttach]
vectorToAttachに従って、poly1を平行移動した多角形を作成する(poly2;上図の赤い多角形)。
ステップ② α=0に対応する初期位置を決める
poly2は、poly1を単に平行移動しただけである。これをα=0の場合(初期位置)としてしまうと、「右にどれだけ転がせば次の頂点が直線とぶつかるか」と、「左にどれだけ転がせば次の頂点が直線とぶつかるか」の両方を測定しなければならず、設計が煩雑になる。そこで、「すでに2頂点が直線上にある状態」を初期位置にする。
conv = Translate[ConvexPolygon[vertList], vectorToAttach]
転がす際の挙動は、poly1の凸包で考えた方が分かりやすい。そこで、poly1の凸包ConvexPolygon[vertList]をvectorToAttachで平行移動した多角形を作成する(conv)。
なお、ここで用いている、ConvexPolygon[<点のリスト>]は、オリジナルツールである。ConvexPolygonツールの詳細は、下記記事を参照。
点がパス上にあるか否かを表す真偽値オブジェクト - うしブログ
convAngle = Zip[Θ + 0, Θ, {Angle[conv]}]
convの内角の大きさ(ラジアン)をリストで取得する(convAngle)。+0をしているのは、角度オブジェクトではなく、数値オブジェクトとして取得したいからである。
convVert = {Vertex[conv]}
convの頂点をリストで取得する(convVert)。
convSides = Sides[conv]
convの辺をリストで取得する(convSides)。なお、Sides[<多角形>]は、オリジナルツールである(下記記事参照)。
多角形オブジェクトを用いて、頂点または辺のリストを返す - うしブログ
Contact = Point[vectorToAttach, 1]
convと直線jとの接点を作成する(Contact)。
convAngle, convVert, convSidesの各要素は、ConvexPolygonツールの仕様上、反時計回りに並ぶ。しかし、どこが始点になるかは、場合によって区々である。
そこで、以降の処理を容易にするため、convAngle, convVert, convSidesを、Contactを始点とする反時計回りに並べ替えておこう。
indexOfContact = IndexOf[Contact, convVert]
Contactの、convVertにおけるIndexを取得する(indexOfContact)。
key = Zip[If[Mod[σ, Length[convVert]] ≟ 0, Length[convVert], Mod[σ, Length[convVert]]], σ, Sequence[indexOfContact + ρ, ρ, 0, Length[convVert] - 1]]
並べ替えの基準となる数値リストを作成する(key)。
sortedAngle = Zip[Element[convAngle, ϕ], ϕ, key]
sortedVert = Zip[Element[convVert, τ], τ, key]
sortedSides = Zip[Element[convSides, φ], φ, key]
keyで並べ替える。
lastVector = Vector[Element[sortedVert, 1], Element[Last[sortedVert], 1]]
sortedVertの最初と最後を結んだベクトルを作成する(lastVector)。
poly3 = Rotate[poly2, If[Angle[lastVector, Vector[UnitVector[j]]] > π, Angle[lastVector, Vector[UnitVector[j]]] - π, Angle[lastVector, Vector[UnitVector[j]]]], Contact]
originalPoint = Rotate[Element[Last[sortedVert], 1], If[Angle[lastVector, Vector[UnitVector[j]]] > π, Angle[lastVector, Vector[UnitVector[j]]] - π, Angle[lastVector, Vector[UnitVector[j]]]], Contact]
originalPositionOfConv = Rotate[conv, If[Angle[lastVector, Vector[UnitVector[j]]] > π, Angle[lastVector, Vector[UnitVector[j]]] - π, Angle[lastVector, Vector[UnitVector[j]]]], Contact]
lastVectorとUnitVector[j]とのなす角に着目して、lastVectorが直線jと重なるように、poly2を(Contactを中心に)回転させる(poly3)。poly3をもって、α=0の場合の位置(初期位置)とする。
convおよび、sortedVertの最後の要素も、同様に回転させる(originalPositionOfConv、originalPoint)。
ステップ③ 転がした場合に各頂点が着地する場所を記録する
α<0の場合を「Rearに転がす場合」、a>0の場合を「Frontに転がす場合」と呼ぶことにしよう。
originalPositionOfConvをRearに転がす場合に、originalPositionOfConvの各頂点が直線jに着地する場所を、点のリスト(Rear)として記録しよう。
times = floor(maxLength / Sum[sortedSides])
maxLengthに応じて、どこまで記録するかを決める(times)。
reverseSortedSidesFull = Sequence[Element[Sort[sortedSides, Zip[-IndexOf[χ, sortedSides], χ, sortedSides]], If[Mod[t, Length[convVert]] ≟ 0, Length[convVert], Mod[t, Length[convVert]]]], t, 1, times Length[convVert]]
sortedSidesを逆順に並べ替え、timesに応じて適切なだけ、要素の列挙を繰り返す(reverseSortedSidesFull)。
reverseSortedSidesFullSum = Sequence[Sum[reverseSortedSidesFull, t], t, 1, Length[reverseSortedSidesFull]]
reverseSortedSidesFullを累積的に積み重ねる(reverseSortedSidesFullSum)。
Rear = Zip[Dilate[originalPoint, ψ / Element[Last[sortedSides], 1], Contact], ψ, reverseSortedSidesFullSum]
reverseSortedSidesFullSumに応じて、originalPointをDilateする(Rear)。
Front = Sort[Translate[Rear, Vector[Element[Last[Rear], 1], Contact]], Zip[-IndexOf[Σ, Translate[Rear, Vector[Element[Last[Rear], 1], Contact]]], Σ, Translate[Rear, Vector[Element[Last[Rear], 1], Contact]]]]
続いて、Frontに転がす場合の着地点を記録する(Front)。Rearと同様に作っても良いのだが、Rearがすでに作られているので、それを流用して作成した。
ステップ④ αの値によって、着地点を特定する
originalPositionOfConvを転がした際に、どの頂点が直線jと接するかは、αによって特定できる(ContactSliding)。
Rearに転がす場合
reverseSortedAngle = Sort[sortedAngle, Zip[-IndexOf[Γ, sortedAngle], Γ, sortedAngle]]
outerReverseSortedAngle = Zip[π - Δ, Δ, reverseSortedAngle]
outerReverseSortedAngleFull = Sequence[Element[outerReverseSortedAngle, If[Mod[t, Length[outerReverseSortedAngle]] ≟ 0, Length[outerReverseSortedAngle], Mod[t, Length[outerReverseSortedAngle]]]], t, 1, Length[Rear]]
outerReverseSortedAngleFullSum = Sequence[Sum[outerReverseSortedAngleFull, t], t, 1, Length[outerReverseSortedAngleFull]]
RearIndexOfα = IndexOf[-α, Sort[Join[{outerReverseSortedAngleFullSum, {-α}}]]]
ContactSlidingRear = Element[Rear, RearIndexOfα]
Frontに転がす場合
outerSortedAngle = Zip[π - Π, Π, sortedAngle]
outerSortedAngleFull = Sequence[Element[outerSortedAngle, If[Mod[t, Length[outerSortedAngle]] ≟ 0, Length[outerSortedAngle], Mod[t, Length[outerSortedAngle]]]], t, 1, Length[Front]]
outerSortedAngleFullSum = Sequence[Sum[outerSortedAngleFull, t], t, 1, Length[outerSortedAngleFull]]
FrontIndexOfα = IndexOf[α, Sort[Join[{outerSortedAngleFullSum, {α}}]]]
ContactSlidingFront = Element[Front, FrontIndexOfα]
両者を統合
ContactSliding = Element[RemoveUndefined[{If[α < 0, ContactSlidingRear, ContactSlidingFront], Contact}], 1]
ステップ⑤ αの値によって、直線jと接する頂点を特定する
originalPositionOfConvの頂点のうち、どの頂点が直線jと接することになるのかは、やはりαによって特定できる(translater)。
Rearに転がす場合
sortedVertOriginalPosition = Zip[Rotate[Φ, If[Angle[lastVector, Vector[UnitVector[j]]] > π, Angle[lastVector, Vector[UnitVector[j]]] - π, Angle[lastVector, Vector[UnitVector[j]]]], Contact], Φ, sortedVert]
reverseSortedVertOriginalPosition = Sort[sortedVertOriginalPosition, Zip[-IndexOf[Ω, sortedVertOriginalPosition], Ω, sortedVertOriginalPosition]]
reverseSortedVertOriginalPositionFull = Sequence[Element[reverseSortedVertOriginalPosition, If[Mod[t, Length[reverseSortedVertOriginalPosition]] ≟ 0, Length[reverseSortedVertOriginalPosition], Mod[t, Length[reverseSortedVertOriginalPosition]]]], t, 1, Length[Rear]]
translaterRear = Element[reverseSortedVertOriginalPositionFull, RearIndexOfα]
Frontに転がす場合
sortedVertOriginalPositionFull = Sequence[Element[sortedVertOriginalPosition, If[Mod[t, Length[sortedVertOriginalPosition]] ≟ 0, Length[sortedVertOriginalPosition], Mod[t, Length[sortedVertOriginalPosition]]]], t, 1, Length[Front]]
translaterFront = Element[sortedVertOriginalPositionFull, FrontIndexOfα]
両者を統合
translater = Element[RemoveUndefined[{If[α < 0, translaterRear, translaterFront], Contact}], 1]
なお、translaterは、αが描画対象外の値をとったときは、Contactを返すようにしている(フェイルセーフ)。
ステップ⑥ poly3をαに従って回転移動させ、ContactSlidingおよびtranslaterに従って平行移動する
回転移動
poly4 = Rotate[poly3, -α, originalPoint]
poly3を、αに従って(originalPointを中心に)回転移動した多角形を作成する(poly4)。
平行移動
poly5 = Translate[poly4, Vector[originalPoint, ContactSliding]]
poly4を、ContactSlidingに従って平行移動する(poly5)。
poly6 = Translate[poly5, Vector[Translate[Rotate[translater, -α, originalPoint], Vector[originalPoint, ContactSliding]], ContactSliding]]
poly5を、translaterに従って平行移動する(poly6)。
poly6が、目的の多角形である。
オリジナルツール「RollPolygonWithoutSlipping」
上記知見を利用して、オリジナルツールを作成した。
RollPolygonWithoutSlipping tool - GeoGebra
構文は
RollPolygonWithoutSlipping[<多角形>, <直線>, <数値>]
である。
多角形を、直線に沿って、指定の角度だけ滑らずに転がした様子を描画する。