TouchDesigner Pythonでoperatorをコントロールする

[TouchDesigner Advent Calendar 2017](https://qiita.com/advent-calendar/2017/touchdesigner) 9日目の記事です。よろしくお願いします。

## はじめに
TouchDesigner（以下Touch）で作業していると、人それぞれここはPython（C++かも）でやったほうがいいなと思う箇所が出て来ると思います。俺の場合はDesigner Mode（新規ファイルを作ると出ている画面）でoperatorのサイズを揃えたり等間隔で置いたりといった調整が手動でやりにくいと感じ、そのストレス軽減が最初の目的でした。また、Touchをちょっと触ってみたくらいの時に市原湖畔美術館でのCarsten Nicolaiの展示で作品の転換時に画面が見えて、あ、これTouchDesignerなんだ〜かっこいい〜と思ったのと、ライブパフォーマンスの際にDesigner Modeを見せちゃう人とかもいると知ったことも色々調べるきっかけとなりました。

この記事での座標、サイズ指定などは全てDesigner Mode上のものですので、Perform Modeでの表示には影響しません。そんなわけで以下ご紹介する内容は地味だったり無意味だったりしますが、TouchでのPythonの使い方を理解する一助となると思います。

Touchでの基本的なPythonの使い方は同一Advent Calendarの[TouchDesignerでのPython入門（基礎編）](https://qiita.com/sanokazuya0306/items/d733b17854a566801d00)、[TouchDesignerでのPython入門（応用編）](https://qiita.com/sanokazuya0306/items/3dfd2cf851b59cb9b89f)で丁寧に書かれているのでそちらを参考にしてください。俺もPythonが超得意ってわけではないのでもっと効率的な書き方などありましたらツッコミ頂けるとうれしいです。

## サンプルファイル
githubにアップしましたので必要に応じて参照してください。
https://github.com/chimanaco/touchdesigner-advent-calendar-2017

## 対象となるoperatorの指定
まず、対象となるoperatorを指定するには以下のものがあります。

#### 名前で指定
以下のコードでは`moviefilein1`というファイル名のoperatorが対象となります。

```python
o = op('moviefilein1')
// your code
```

#### 同一階層のoperator全部
以下のコードでは実行するスクリプトと同一階層にある全てのoperatorが対象となります。

```python
c = parent().children
for o in c:
    // your code
```

#### 名前に文字列を含むoperatorを指定
以下のコードでは`moviefilein`という文字列を含むoperatorが対象となります。

```python
c = parent().children
for o in c:
  if "moviefilein" in o.name:
    // your code
```

#### familyで指定
以下のコードでは`TOP family`のoperatorが対象となります。

```python
c = parent().children
for o in c:
  if(o.family == 'TOP'):
      // your code
```
#### labelで指定
以下のコードでは`Movie File In`のoperatorが対象となります。

```python
c = parent().children
for o in c:
  if(o.label == 'Movie File In'):
      // your code
```

#### typeで指定
以下のコードでは`moviefilein type`のoperatorが対象となります。`label`との使い分けがよく分かっていません。

```python
c = parent().children
for o in c:
  if(o.type == 'moviefilein'):
      // your code
```

## 位置の指定
座標を指定して配置します。
以下のコードでは`moviefilein1` operatorの座標を取得し、左揃えで`moviefilein2` operatorの位置を指定します。

```python
marginY = 20

o1 = op('moviefilein1')
x1 = o1.nodeX
y1 = o1.nodeY

o2 = op('moviefilein2')
o2.nodeX = x1
o2.nodeY = y1 - o1.nodeHeight - marginY
```

![Position](https://media.giphy.com/media/xT0xeu37HmzIt35GPm/giphy.gif)

ちなみに `Edit` -> `Preferences` -> `Network` の下の方の設定でグリッドにスナップができるようになることに最近気づきました。

## サイズのリセット
拡大縮小したり他所からコピペしたoperatorを手動でうまくサイズ調整することができなかったため、リセットボタンを作りました。
例えば`moviefilein1`というoperatorは以下のコードでリセットできます。ドラッグでリサイズする時にもグリッドが出てくれるとありがたいんですが出ないのかしら…。

```python
op('moviefilein1').resetNodeSize()
```

![Reset](https://media.giphy.com/media/xT0xei33AneMVRHmV2/giphy.gif)

## サイズを指定する
設定する値の単位はピクセル？`Help`→`Python Example`→`ui_examples`を見てみると`1 pixel = 1 tile unit`とあるんですが`1 tile`って何でしょうね。
以下のコードでは`moviefilein1` operatorの幅を`500`、高さを`50`に指定します。

```python
o = op('moviefilein1')
o.nodeWidth = 500
o.nodeHeight = 50
```
![Resize](https://media.giphy.com/media/3o6fJgd0m8NSQ3nU0E/giphy.gif)

テストする時には、いじっては戻しいじっては戻しを繰り返したいのでサイズ変更ボタンとリセットボタンを並べて置いています。Python実行だとUndo(`Ctrl+z`)が効かないので。

## 複数operatorのサイズを更新する
以下のコードでは幅`120`の`16`個の`ConstantTop`のoperatorを作成、横並びに配置します。

```python
w = 120
for i in range(0, 16):
    o = parent().create(constantTOP)

    # size
    o.nodeWidth = w

    # position
    o.nodeX = xPos * i    
```

以下のコードでは複数の`ConstantTop`のoperatorの高さを`Position`という名前の`Table`から取得して更新に使用しています。`Position`の値を定期的に変更するとアニメーションになります。この場合作成したoperatorを絞り込む条件として`o.label=='Constant'`と`o.family=='TOP'`を指定していますが、同一階層上に既存の`ConstantTop` operatorがあった場合それらにも影響してしまいますので条件に注意してください。

```python
c = parent().children
index = 0
for o in c:
  if o.label=='Constant' and o.family=='TOP':
      # size
      cell = op('Position')[index, 0].val
      height = int(float(cell))
      o.nodeHeight = height
      index += 1
```

![ResizeMulti](https://media.giphy.com/media/3o6fJbJotrFXYF7maA/giphy.gif)

サンプルでは`noise`で高さをコントロールしていますが、サウンドリアクティブにしたりセンサーから取得した値を使用したりするともっと楽しくなると思います。

# operatorを形に沿って配置する

Pythonで座標を計算して図形を描く場合と、SOPの頂点座標を使用する2パターンやってみました。

## Pythonで座標を計算する

以下のコードではoperatorを螺線形に配置します。

```python
radius = 0

for i in range(0, 200):
    radius += i * 0.1
    addOpSpiral(i, total, radius)
```

```python
def addOpSpiral(index, total, radius):
    angleSpeed = 5

    o = parent().create(constantTOP)

    # size
    setOpSize(o, 120, 67.5)

    # positon
    angle = toRadians(index * 360 / total * angleSpeed)
    x = math.cos(angle) * radius
    y = math.sin(angle) * radius
    setOpPosition(o, x, y)
    return

# Common utilities
def setOpSize(op, w, h):
    op.nodeWidth = w
    op.nodeHeight = h
    return

def setOpPosition(op, x, y):
    op.nodeX = x
    op.nodeY = y
    return

def toRadians(angle):
    return angle * (math.pi / 180)
```

![Spriral](https://media.giphy.com/media/3o6fIQ7ACqiL6FZHb2/giphy.gif)

ここでは螺旋形に配置していますが、興味ある方は好きな形に配置してみてください。

## SOPの頂点座標を使用する

`FontSOP`→`ResampleSOP`→`SOPtoCHOP`→`MathCHOP`→`ChoptoDAT`という流れでテキストの頂点データを`Position`と名付けた`TableDAT`にインプットし、以下のコードでoperatorの座標に指定します。

```python
c = parent().children
index = 0
for o in c:
  if o.label=='Constant' and o.family=='TOP':
      # position
      x = op('Position')[index, 0].val
      y = op('Position')[index, 1].val
      setOpPosition(o, x, y)
```

![TextSOP](https://media.giphy.com/media/3o6fJ6ItGG1gDc5fY4/giphy.gif)

サンプルでは`Sequence BlendSOP`を使用して元となるテキストを切り替えています。

## ネストされたoperatorを追加する
Replicatorを使うのがTouchらしいやり方な気がしますが、Pythonでも同じようなことができます。operatorを追加していくだけですが、connectメソッドでoperator同士をつなげられるところが好きです。
以下のコードは`containerCOMP`を追加、その中に`constant`と`out`の二つの`TOP`を追加し`、outTOP`からの出力を予め配置しておいた`switch1`につなげています。

```python
w = 240
h = 135
yPos = -(h + 10)
total = 5

for i in range(0, total):
	# add container
	c = parent().create(containerCOMP)

	# size
	c.nodeWidth = w
	c.nodeHeight = h

	# position
	c.nodeY = yPos * i

	# add op to container
	addOpToContainer(c, i, total)

	# connect
	c.outputConnectors[0].connect(op('switch1'))
```

```python
def addOpToContainer(op, index, total):
	# add
	t1 = op.create(constantTOP)

	t2 = op.create(outTOP)
	t2.nodeX = 240

	# connect
	t1.outputConnectors[0].connect(t2)
	return
```
![Container](https://media.giphy.com/media/3o6fITQzFLj2uiEkeI/giphy.gif)

## Designer Modeでズームする
こちらは実際にoperatorを操作するわけではないのですが、関連ということで。
`pane`の`zoom`の数値を指定することでDesigner Modeのズーム具合を指定できます。
以下のコードは画面の中心（座標0,0）にある幅`240`、高さ`130`のoperatorを中心にズームする場合です。

```python
p = ui.panes.current # 現在のpaneを指定
p.zoom = 1  #8くらいでフルスクリーン
p.x = 120 # ターゲットにしたいoperatorのwidth/2
p.y = 67.5 # ターゲットにしたいoperatorのheight/2
```

![Zoom](https://media.giphy.com/media/xT0xesQaJW2K2WiZA4/giphy.gif)

`zoom`の値を定期的に変更することでアニメーションになります。サンプルは拡大のみですが、一覧表示時、縮小時などにアニメーションをつけたり画面を覆うタイミングでPerform Modeに切り替えたりするなどして作り込むとプレゼンテーションに使えたり、Carsten Nicolaiの展示のような動き（一覧を表示→次の作品にフォーカス→次の作品をフルスクリーンで表示、を繰り返す）も作れます。サンプルのようにズームさせていると各operatorに触りにくいので停止用ショートカットを設定するとか、MIDIコントローラーに停止ボタンを設定しておくなどしたほうが好いかもしれないです。

## うまくいかなかったこと

```
op.activeViewer = 1
```
という設定があってoperatorのviewerを全域使うためにこれを使用しているのですが、 operatorを作ったすぐのタイミングでは設定できないことが多かったので、アニメーションとして定期的に呼ぶメソッドに設定を入れています。operatorがcookされる前に設定しようとしているからダメだとかかしら。

## おまけ
俺得のoperatorのresize、reset、locate、destroyができるtoxを作りました。
https://github.com/chimanaco/touchdesigner-tox にあるOperator_adjust.tox というやつです。確認メッセージは未実装でボタンを押したら即実行されてundoできないので、試される方おられたら現状destroyは注意が必要です。

## 最後に
お読み頂きありがとうございました。他にもデフォルトのoperatorのサイズがCOMP > TOP > CHOP = SOP = MAT = DAT となっているのとかも気になるところですが、Designer Modeをライブコーディングで使ってみたいのでもっと楽しく魅せる編集画面を目指します（その割にサンプルファイル美しくないですごめんなさい）。

明日は[@ToyoshiMorioka](https://qiita.com/ToyoshiMorioka)さんの登場です！
引き続きこの後も盛り沢山なTouchDesigner Advent Calendar 2017を楽しんでいきましょう！