Thursday, October 10, 2019 / JavaScript, functional-programming, XD

再帰を使って木構造をリストに変換する JavaScript

Adobe XD の ドキュメント構造は scenegraph.root をルートノードとした木構造として表現されています。
これらのノードをスクリプトから操作するには、この木構造をたどる必要があります。 いちいちたどるのは面倒なので、木構造からリストに変換することを考えることにします。

Adobe XD Scenenode

traverse

たとえば、以下のように list 変数を traverse 再帰関数に含める形で実装することもできるのですが:

const traverse = (node, list)=>{
    if( node.children.length==0 ){
        return;
    }

    list.push(node);
    node.children.forEach( (childNode)=> {
        traverse(childNode, list);
    });
};

const scenegraph = require("scenegraph"); 

const rootNode = scenegraph.root;
traverse( rootNode, list );

list 変数を使わないで、 const nodeList = toNodeList(scenegraph.root); とするだけでリストに変換できるような toNodeList 関数を書いてみようと思います。

SceneNode クラス

最初に Adobe XD なしでコードがテストできるように、自前の SceneNode クラスをつくります。

class SceneNode {
    constructor(name, depth){
        this.name = name;
        this.depth = depth;
        this.children = [];
    }

    add(childNode){
        this.children.push(childNode);
    }

    toString(){
        const indent='  ';
        return [...Array(this.depth).keys()].reduce( (a,b)=> a + indent, '' ) + this.name
    }
}

build tree

次にテスト用の木構造をつくります:

const rootNode = new SceneNode('root', 0);

const childNode1 = new SceneNode('child1', 1);
const childNode11 = new SceneNode('child11', 2);
const childNode12 = new SceneNode('child12', 2);

const childNode2 = new SceneNode('child2', 1);
const childNode21 = new SceneNode('child21', 2);
const childNode22 = new SceneNode('child22', 2);

const childNode3 = new SceneNode('child3', 1);
const childNode31 = new SceneNode('child31', 2);
const childNode32 = new SceneNode('child32', 2);

rootNode.add(childNode1);

childNode1.add(childNode11);
childNode1.add(childNode12);

rootNode.add(childNode2);
childNode2.add(childNode21);
childNode2.add(childNode22);

rootNode.add(childNode3);
childNode3.add(childNode31);
childNode3.add(childNode32);

test with traverse function

これをまずは、traverse 関数でテストしてみます。

const traverse = (node, list)=>{
    if( node.children.length==0 ){
       return;
    }

    list.push(node);
    node.children.forEach( (childNode)=> {
       traverse(childNode, list);
    });
};

const list = [];
traverse( rootNode, list );
list.forEach( (node)=> {
    console.log(node.toString());
});

結果

root
  child1
    child11
    child12
  child2
    child21
    child22
  child3
    child31
    child32

うまくいきました。

toNodeList function

今度は list 変数を使わない toNodeList 関数を実装します。

const toNodeList = (node)=> {
    if( node.children.length==0 ){
        return [node];
    }

    return node.children.reduce( (a,b)=> a.concat( toNodeList(b) ), [node]);
};

reduce の部分で初期値 [node] を与える場所が最後になっているので、とても読みづらいですね。致し方ない。

これで以下のように使う側はスッキリ書くことができます。

toNodeList(rootNode).forEach( (note)=> { console.log(node.toString()); } );

XD で ImageFill のある SceneNode のみを抽出

以上から、たとえば Adobe XD で scenegraph.root (シーングラフ) から 画像の入った SceneNode だけを抽出するには:

const scenegraph = require("scenegraph"); 
const { ImageFill } = require("scenegraph");

const imageNodeFilter = (node)=> (node.fill && node.fill instanceof ImageFill);          

const rootNode = scenegraph.root;
toNodeList( rootNode ).filter( imageNodeFilter ).forEach( (node)=>{
    console.log(node);
});

これでうまくいくはずです。

まとめ

Adobe XD は InDesign や Photoshop の Extendscript と違い 最新の JavaScript仕様で plugin を書くことができるのでよい。 Extendscript が ES6 で記述できる日は来るのであろうか? それとも、ある日突然使えなくなるとか... ESTK はすでに見捨てられたし。