Polyline Path Tool

公開 更新

環境:Houdini 20.0.751

ポリライン上に沿ってポリラインを生成するツール。

元々存在するポリラインを「親ポリライン」とし、そこに沿って作られるポリラインを「生成ポリライン」とする。

Parameterの構成

Folder Type: Multiparm Block (tabs) Name: prims Label: Primitives

Folder Type: Multiparm Block (list) Name: points_# Label: Points

Name: pt_#_# Label: Prim # Point # Type: Float

Viewer Stateのコード

#
# Polyline Path Tool 20250319
#
import hou
import viewerstate.utils as su

import math
from enum import Enum

# Enumでモード切り替え
class Mode(Enum):
    SELECT = 0
    EDIT = 1

class State(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer
        
        # UIのテンプレート
        HUD_TEMPLATE = {
            "title": "Polyline Path", "desc": "tool", "icon": "SOP_subnet",
            "rows": [
                {"id": "mode", "label": "Mode", "value": "SELECT"},
                {"id": "mode_g", "type": "choicegraph", "count": 2, "value": 0},
                {"id": "switch_act", "label": "Switch Mode", "key": "Ctrl"},
                {"id": "add_act", "label": "Select / Add Prim", "key": "LMB"},
                {"id": "del_act", "label": "Delete Prim", "key": "Del"},
                {"id": "focus_act", "label": "Camera Focus", "key": "F"},
            ]
        }
        
        # テンプレートを使用してHUDを設定します。通常では、これはステートが有効な間に一回実行します。
        self.scene_viewer.hudInfo(hud_template=HUD_TEMPLATE)
        
        self.mode = Mode.SELECT # モード
        
        self.origin = None
        self.direction = None
        self.viewport = None

        self.selected_point = -1    # 選択ポイントのローカルインデックス
        self.selected_gen_prim = -1     # 選択中のプリミティブ番号
        self.selected_src_prim = -1     # 選択プリミティブの親のインデックス
        
        self.intersect_gen_prim = -1    # 交差中のプリミティブ番号(ポイントもプリミティブとして扱う)
        self.intersect_src_prim = -1

        self.node = None
        self.drawable_geometry = None       # 入力されたポリラインのジオメトリ(表示用)
        self.src_curve_geometry = None     # 入力されたポリラインのジオメトリ
        self.gen_curve_geometry = None     # 生成されたポリライン
        self.src_curve_bsphere = None     # 入力されたポリラインのバウンディングスフィア
        self.gen_curve_bsphere = None     # 生成されたポリラインのバウンディングスフィア

        # 入力用のフラグ
        self.mouse_pressed = False
        self.old_mouse_pressed = False
        self.drag = False   # ドラッグ中のフラグ
        self.dragIndex = -1  # ドラッグ中のポイントインデックス
        
        # ポイントとエッジの判定距離(ピクセル)
        self.point_range = 16.0
        self.edge_range = 8.0
        self.edge_selecting_range = 96.0

        # 表示ポイント(編集中:オレンジ:大)
        edit_point = hou.GeometryDrawable(self.scene_viewer, hou.drawableGeometryType.Point, "point",
            params = {
                "num_rings": 2,
                "radius": 3,
                "color1": (1.0,0.6,0.1,1.0),
                "style" : hou.drawableGeometryPointStyle.LinearSquare}
        )
        self.guideEditPoint = hou.GeometryDrawableGroup("poly_edit_point")
        self.guideEditPoint.addDrawable(edit_point)

        # 選択中ポイント(選択中:オレンジ)
        selected_point = hou.GeometryDrawable(self.scene_viewer, hou.drawableGeometryType.Point, "point",
            params = {
                "num_rings": 2,
                "radius": 5,
                "color1": (1.0,0.6,0.1,1.0),
                "style" : hou.drawableGeometryPointStyle.LinearSquare}
        )
        self.guideSelectedPoint = hou.GeometryDrawableGroup("poly_selected_point")
        self.guideSelectedPoint.addDrawable(selected_point)
        
        # エッジ(選択中:実線:オレンジ)
        curve_line = hou.GeometryDrawable(self.scene_viewer, hou.drawableGeometryType.Line, "line",
            params = {
                "color1": (1.0,0.6,0.1,1.0),
                "style": hou.drawableGeometryLineStyle.Plain,
                "line_width": 1 }
        )
        self.guideCurveLine = hou.GeometryDrawableGroup("poly_curve_line")
        self.guideCurveLine.addDrawable(curve_line)

        # エッジ(選択済み:実線:オレンジ)
        selected_curve_line = hou.GeometryDrawable(self.scene_viewer, hou.drawableGeometryType.Line, "line",
            params = {
                "color1": (1.0,0.6,0.1,1.0),
                "style": hou.drawableGeometryLineStyle.Plain,
                "line_width": 3 }
        )
        self.guideSelectedCurveLine = hou.GeometryDrawableGroup("poly_selected_curve_line")
        self.guideSelectedCurveLine.addDrawable(selected_curve_line)

        # エッジ(点線)
        selecting_line = hou.GeometryDrawable(self.scene_viewer, hou.drawableGeometryType.Line, "line",
            params = {
                "color1": (1.0,0.7,0.1,1.0),
                "style": hou.drawableGeometryLineStyle.Dash1,
                "line_width": 3 }
        )
        self.guideSelectingLine = hou.GeometryDrawableGroup("poly_selecting_line")
        self.guideSelectingLine.addDrawable(selecting_line)

        # エッジ(選択中:実線:オレンジ)
        selected_line = hou.GeometryDrawable(self.scene_viewer, hou.drawableGeometryType.Line, "line",
            params = {
                "color1": (1.0,0.6,0.1,1.0),
                "style": hou.drawableGeometryLineStyle.Plain,
                "line_width": 3 }
        )
        self.guideSelectedLine = hou.GeometryDrawableGroup("poly_selectied")
        self.guideSelectedLine.addDrawable(selected_line)

    #
    # ユーザーがStateに入ると呼び出される
    #
    def onEnter(self, kwargs):
        self.node = kwargs["node"]
        
        # ガイドのジオメトリ
        hda_node = self.scene_viewer.currentNode()
        hda_node.cook(force=True) # 強制クック
        if hda_node:
            hda_node.node('geo_src_curve').cook(force=True)
            self.src_curve_geometry = hda_node.node('geo_src_curve').geometry()
            hda_node.node('geo_new_curve').cook(force=True)
            self.gen_curve_geometry = hda_node.node('geo_new_curve').geometry()
            hda_node.node('src_bsphere').cook(force=True)
            self.src_curve_bsphere = hda_node.node('src_bsphere').geometry()
            hda_node.node('new_bsphere').cook(force=True)
            self.gen_curve_bsphere = hda_node.node('new_bsphere').geometry()
            
            # オーバーレイで描画する入力ポリライン
            colli = hda_node.node('geo_src_curve').geometry()
            self.drawable_geometry = hou.SimpleDrawable(self.scene_viewer, colli, "drawable_geometry")
            
            self.drawable_geometry.setDisplayMode(hou.drawableDisplayMode.WireframeMode)
            #self.drawable_geometry.setDisplayMode(hou.drawableDisplayMode.CurrentViewportMode)
            self.drawable_geometry.enable(True)
            self.drawable_geometry.show(True)

        self.modeChange()

        self.node.parm("onEnter").set(1)
            
    # Stateが中断された時に呼び出される
    def onInterrupt(self,kwargs):
        #self.drawable_geometry.show(False)
        #self.guideSelectedPoint.show(False)
        #self.guideEditPoint.show(False)
        #self.guideSelectingLine.show(False)
        #self.guideSelectedLine.show(False)
        pass
        
    # Stateの中断が再開された時に呼び出される
    def onResume(self, kwargs):
        self.drawable_geometry.show(True)

    def onExit(self, kwargs):
        self.node.parm("onEnter").set(0)

    # マウス左ボタンが押された時に呼ばれる処理
    def start(self):
        if not self.mouse_pressed:
            self.scene_viewer.beginStateUndo("Mouse Pressed")
        self.mouse_pressed = True

    # マウス左ボタンが放された時に呼ばれる処理
    def finish(self):
        # 前フレームで押下されていたら
        if self.mouse_pressed:
            self.scene_viewer.endStateUndo()
        self.mouse_pressed = False

    # 選択済みポイントDrawableの更新処理
    def updateGuideEditPoint(self):
        # UIのジオメトリを描画する
        poly_geo = hou.Geometry()

        if self.selected_src_prim != -1:
            poly = poly_geo.createPolygon()

            multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)
            list_pos = self.getPointPositionsFromMultiparm(multiparm_prim_index)
            for i in range(len(list_pos)):
                point = poly_geo.createPoint()
                point.setPosition(list_pos[i])
                poly.addVertex(point)

        self.guideEditPoint.setGeometry(poly_geo)  # update the drawable

    # 選択中ポイントDrawableの更新処理
    def updateGuideSelectedPoint(self):
        # UIのジオメトリを描画する
        poly_geo = hou.Geometry()
        #self.log("self.selected_point:" + str(self.selected_point))
        if self.selected_point != -1 and self.selected_src_prim != -1:
            poly = poly_geo.createPolygon()

            list_pos = self.getPointPositionsFromMultiparm(self.selected_src_prim)
            
            point = poly_geo.createPoint()
            point.setPosition(list_pos[self.selected_point])
            poly.addVertex(point)

        self.guideSelectedPoint.setGeometry(poly_geo)  # update the drawable

    # 生成ラインDrawableの更新処理
    def updateGuideCurveLine(self):
        poly_geo = hou.Geometry()
        
        poly_prims = self.gen_curve_geometry.prims()
        for i in range(len(poly_prims)):
            poly_points = self.gen_curve_geometry.prim(i).points()
            poly = poly_geo.createPolygon(False) # hou.Polygonクラスが生成される
            for i in range(len(poly_points)):
                point = poly_geo.createPoint()
                point.setPosition(poly_points[i].position())
                poly.addVertex(point)
        # update the drawable
        self.guideCurveLine.setGeometry(poly_geo)
        
    # 選択済みの生成ラインDrawableの更新処理
    def updateGuideSelectedCurveLine(self):
        poly_geo = hou.Geometry()
        
        if self.selected_gen_prim != -1:
            #poly_prims = self.gen_curve_geometry.prims()
            poly_points = self.gen_curve_geometry.prim(self.selected_gen_prim).points()
            poly = poly_geo.createPolygon(False) # hou.Polygonクラスが生成される
            for i in range(len(poly_points)):
                point = poly_geo.createPoint()
                point.setPosition(poly_points[i].position())
                poly.addVertex(point)
        # update the drawable
        self.guideSelectedCurveLine.setGeometry(poly_geo)

    # 選択中ラインDrawableの更新処理
    def updateGuideSelectingLine(self):
        poly_geo = hou.Geometry()
        if self.intersect_prim != -1:
            poly_points = self.src_curve_geometry.prim(self.intersect_prim).points()
            # Construct a new geometry
            poly = poly_geo.createPolygon(False) # hou.Polygonクラスが生成される
    
            for i in range(len(poly_points)):
                point = poly_geo.createPoint()
                point.setPosition(poly_points[i].position())
                poly.addVertex(point)
        # update the drawable
        self.guideSelectingLine.setGeometry(poly_geo)

    # 選択済みラインDrawableの更新処理
    def updateGuideSelectedLine(self):
        poly_geo = hou.Geometry()
        if self.selected_src_prim != -1:
            # 入力されたポリラインからポイント情報を取得する
            poly_points = self.src_curve_geometry.prim(self.selected_src_prim).points()
            poly = poly_geo.createPolygon(False) # hou.Polygonクラスが生成される
            for i in range(len(poly_points)):
                point = poly_geo.createPoint()
                point.setPosition(poly_points[i].position())
                poly.addVertex(point)
                
        self.guideSelectedLine.setGeometry(poly_geo)
    
    #
    # Multiparmのポイントを描画する
    #
    def getPointPositionsFromMultiparm(self, selected_src_prim):
        list_position = []
        multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)
        multiparm_curve = self.node.parm("curves_" + str(multiparm_prim_index))
        for i in range(multiparm_curve.evalAsInt()):
            multiparm_point = self.node.parm("points_%d_%d" % (multiparm_prim_index, i))
            for j in range(multiparm_point.evalAsInt()):
                param_u = self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, i, j)).eval()
                pos = self.calcPosFromParametricUV(self.src_curve_geometry.prim(self.selected_src_prim), param_u)
                list_position.append(pos)

        return list_position

    # IDから親ポリラインのインデックス番号を返す関数
    def getParentPrimIndexFromId(self, id):
        prim_index = -1
        for i in range(len(self.src_curve_geometry.prims())):
            uuid = self.src_curve_geometry.prim(i).attribValue("id")
            if id == uuid:
                prim_index = i
                break

        return prim_index

    # 親ポリラインのインデックスからIDを返す関数
    def getIdFromParentPrimIndex(self, src_prim_id):
        return self.src_curve_geometry.prim(src_prim_id).attribValue("id")

    # IDからMultiparmのインデックス番号を返す
    def getMultiparmIndexFromId(self, id):
        index = -1
        for i in range(self.node.parm("prims").evalAsInt()):
            parent_id = self.node.parm("parent_id_%d" % i).eval()
            if id == parent_id:
                index = i
                break

        return index

    # PrimitiveのインデックスからMultiparmのインデックス番号を返す
    def getMultiparmIndexFromPrim(self, prim_index):
        id = self.src_curve_geometry.prim(prim_index).attribValue("id")
        multiparm_index = self.getMultiparmIndexFromId(id)
        return multiparm_index

    #
    # マウスポインタとポイントが交差していたらインデックス番号を、していなかったら-1を返す
    #
    def isIntersectPoints(self, origin, viewport, positions, radius):
        closeIndex = -1
        closeDist = 1000000
        for i in range(len(positions)):
            A = origin
            P = positions[i]
            dist = (viewport.mapToScreen(P) - viewport.mapToScreen(A)).lengthSquared()

            if dist < pow(radius, 2) and dist < closeDist:
                closeDist = dist
                closeIndex = i
                
        return closeIndex

    #
    # 入力はhou.Primクラスのリスト。交差していたらインデックス番号を、していなかったら-1を返す
    #
    def isIntersectPolylines(self, origin, direction, viewport, prims, bspheres, radius):
        # エッジとマウスポインタの交差判定
        closeIndex = -1
        closeDist = 1000000
        
        # 球と直線の交差判定
        for i in range(len(prims)):
            
            b_radius = bspheres[i].attribValue("radius")
            center = hou.Vector3(bspheres[i].attribValue("center"))
            p0 = origin - center
            p1 = (origin + direction) - center
            v = (p1 - p0).normalized()

            a = v.x() * v.x() + v.y() * v.y() + v.z() * v.z()
            b = 2 * (p0.x() * v.x() + p0.y() * v.y() + p0.z() * v.z())
            c = p0.x() * p0.x() + p0.y() * p0.y() + p0.z() * p0.z() - b_radius * b_radius
            
            # 条件をクリアしたら直線と線分の交差判定
            if b * b - 4 * a * c >= 0:
                list_points = prims[i].points()  # プリミティブからポイントを取得

                # エッジごとの判定処理
                # マウスポインタからの線分と各エッジを最短で結ぶ直線と交点を計算している
                A = origin
                B = origin + direction
                ab = (B-A).normalized()
                
                for j in range(len(list_points) - 1):
                    C = list_points[j].position()
                    D = list_points[j+1].position()
                    cd = (D-C).normalized()
                    ac = (C-A)
                    #d0 = (ab.dot(ac)-cd.dot(ac)*ab.dot(cd))/(1-(ab.dot(cd)*ab.dot(cd)))
                    d1 = (ab.dot(ac)*ab.dot(cd)-cd.dot(ac))/(1-(ab.dot(cd)*ab.dot(cd)))
                    #c0 = A + ab * d0
                    c0 = A
                    c1 = hou.Vector3()
                    
                    if d1 >= 0 and pow(d1, 2) <= (D-C).lengthSquared(): # 線分の範囲内
                        c1 = C + cd * d1
                    elif d1 < 0:    # 始点より前
                        c1 = C
                    elif pow(d1, 2) > (D-C).lengthSquared():    # 終点より後
                        c1 = D
                        
                    dist = (viewport.mapToScreen(c1) - viewport.mapToScreen(c0)).lengthSquared()
                    
                    if dist < pow(radius, 2) and dist < closeDist:
                        closeIndex = i
                        break
                else:
                    continue
                break

        return closeIndex

    #
    # マウスポインタとポリラインの交差判定からパラメトリックUVとワールド座標を求める
    # 線分AB:マウスからの光線、線分CD:ポリラインのひと区間
    # 
    def calcParametricPos(self, origin, direction, viewport, prim, radius):
        isIntersect = False    
    
        closeDist = 1000000
        closePos = hou.Vector3()
        
        # 入力ポリラインからポイントを取得
        list_points = prim.points()
        
        perimeter = 0   # Polylineの全長
        pos_coord = 0   # Polylineでの位置

        # エッジごとの判定処理
        # マウスポインタからの線分と各エッジを最短で結ぶ直線と交点を計算している
        A = origin
        B = origin + direction
        ab = (B-A).normalized()

        mouse_screen_pos = viewport.mapToScreen(A)
        
        for j in range(len(list_points) - 1):
            C = list_points[j].position()
            D = list_points[j+1].position()
            cd = (D-C).normalized()
            ac = C-A
            
            #d0 = (ab.dot(ac)-cd.dot(ac)*ab.dot(cd))/(1-(ab.dot(cd)*ab.dot(cd)))
            d1 = (ab.dot(ac)*ab.dot(cd)-cd.dot(ac))/(1-(ab.dot(cd)*ab.dot(cd)))

            # 線分の範囲内か判別
            c1 = hou.Vector3()  # 線分上のポイント
            # 線分の範囲内
            if d1 >= 0 and pow(d1, 2) <= (D-C).lengthSquared():
                c1 = C + cd * d1
            # 始点より前
            elif d1 < 0:
                c1 = C
            # 終点より後
            elif pow(d1, 2) > (D-C).lengthSquared():
                c1 = D
                
            dist = (viewport.mapToScreen(c1) - mouse_screen_pos).lengthSquared()
            if dist < closeDist and dist < pow(radius, 2):
                closeDist = dist
                closePos = c1
                pos_coord = perimeter + d1
                isIntersect = True

            perimeter += (D-C).length()

        parametric_u = pos_coord / perimeter
        parametric_u = max(0.0, min(parametric_u, 1.0))

        return isIntersect, parametric_u, closePos

    #
    # パラメトリック座標からワールド座標を計算する
    #
    def calcPosFromParametricUV(self, prim, param_u):
        # 入力ポリラインからポイントを取得
        list_points = prim.points()
        # ポリラインの全長を計算
        perimeter = 0
        for j in range(len(list_points)-1):
            p0 = list_points[j].position()
            p1 = list_points[j+1].position()
            dist = (p1-p0).length()
            perimeter += dist

        pos = perimeter * param_u   # 位置を計算

        result = hou.Vector3()
        sum = 0
        for j in range(len(list_points)-1):
            p0 = list_points[j].position()
            p1 = list_points[j+1].position()
            dist = (p1-p0).length()

            if sum + dist >= pos:
                rest = pos - sum
                result = p0 + (p1 - p0).normalized() * rest
                break
            sum += dist

        return result

    #
    # パラメトリックUVの前後を反転する
    # 
    def swapParametricUV(self):

        multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)
        curve_index = int(self.selected_point / 2)
        point_index = int(self.selected_point % 2)
        multiparm_points = self.node.parm("points_%d_%d" % (multiparm_prim_index, curve_index))
        
        value = []
        for i in range(multiparm_points.evalAsInt()):
            value.append(self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, curve_index, i)).evalAsFloat())

        if value[0] > value[1]:
            value.sort()
            for i in range(multiparm_points.evalAsInt()):
                self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, curve_index, i)).set(value[i])
            # 選択ポイントの入れ替え
            if point_index%2 == 0:
                self.selected_point += 1
            else:
                self.selected_point -= 1
        
    #
    # マウスイベント
    #
    def onMouseEvent(self, kwargs):

        ui_event = kwargs["ui_event"]
        device = ui_event.device()
        self.origin, self.direction = ui_event.ray()
        self.viewport = self.scene_viewer.curViewport()
        
        #
        # Mode == SELECT
        #
        if self.mode == Mode.SELECT:

            # エッジとマウスポインタの交差判定
            intersect_prim = self.isIntersectPolylines(self.origin, self.direction, self.viewport, self.src_curve_geometry.prims(), self.src_curve_bsphere.prims(), self.edge_range)

            # 左ボタンクリック
            if device.isLeftButton():
                # ポリラインと交差してない場合
                if intersect_prim == -1:
                    # 選択しているポリラインがあれば選択解除する
                    if self.selected_src_prim != -1:
                        self.selected_src_prim = -1
                # ポリラインと交差している
                else:
                    # 同じインデックスなら選択解除する
                    if intersect_prim == self.selected_src_prim:
                        self.selected_src_prim = -1
                    else:
                        self.selected_src_prim = intersect_prim
                        # 選択しているMultiparmを先頭にする(なければ生成する)
                        multiparm = self.node.parm("prims")
                        id = self.getIdFromParentPrimIndex(self.selected_src_prim)
                        index = -1
                        for i in range(multiparm.evalAsInt()):
                            parent_id = self.node.parm("parent_id_%d" % i).eval()
                            if id == parent_id:
                                index = i
                                break
                        if index == -1:
                            # 新しくMultiparm項目をつくる
                            index = multiparm.evalAsInt()
                            multiparm.insertMultiParmInstance(index)
                            self.node.parm("parent_id_%d" % index).set(id) # uuidをセット
                            hou.ui.setMultiParmTabInEditors(multiparm, index)
                        else:
                            hou.ui.setMultiParmTabInEditors(multiparm, index)

                        # 現在選択してるタブのインデックスを記録する
                        parm_editor = hou.ui.paneTabOfType(hou.paneTabType.Parm)
                        selected_tab_index = parm_editor.multiParmTab('prims')
                        self.node.parm("tab_index").set(selected_tab_index)

            # Drawablesジオメトリの更新
            if intersect_prim != -1:
                self.intersect_prim = intersect_prim
                self.updateGuideSelectingLine()
                self.guideSelectingLine.show(True)
            else:
                self.intersect_prim = -1
                self.guideSelectingLine.show(False)

            self.updateGuideSelectedLine()
            self.guideSelectedLine.show(True)

        #
        # Mode == EDIT
        #
        elif self.mode == Mode.EDIT:
            # 左ボタンクリック
            if device.isLeftButton():
                # ドラッグ中
                if self.drag == True:
                    # スナップをいれる
                    snapFlag = False
                    
                    isSnapPoint = self.node.parm("snap").eval()
                    if isSnapPoint:
                        # Multiparmからポイントリストをつくる
                        parm_pos = self.getPointPositionsFromMultiparm(self.selected_src_prim)
                        for i in range(len(parm_pos)):
                            if i != self.selected_point: # ドラッグしているポイント番号は対象外に
                                closeIndex = self.isIntersectPoints(self.origin, self.viewport, [parm_pos[i]], self.point_range)
                                if closeIndex != -1:
                                    multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)
                                    curve_index = int(i / 2)
                                    point_index = int(i % 2)
                                    param_u = self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, curve_index, point_index)).eval()
                                    snapFlag = True
                                    break
                    
                    if snapFlag == True:
                        multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)
                        curve_index = int(self.dragIndex / 2)
                        point_index = int(self.dragIndex % 2)
                        self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, curve_index, point_index)).set(param_u) # 座標をセット
                    else:
                        # カーブとの交差を計算し、座標を取得する
                        isIntersect, param_u, pos = self.calcParametricPos(self.origin, self.direction, self.viewport, self.src_curve_geometry.prim(self.selected_src_prim), self.edge_selecting_range)
                        
                        if isIntersect:
                            multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)
                            curve_index = int(self.dragIndex / 2)
                            point_index = int(self.dragIndex % 2)
                            self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, curve_index, point_index)).set(param_u) # 座標をセット

                # ドラッグしてない場合
                else:
                    # マウスポインタと生成カーブの交差判定
                    intersect_curve = self.isIntersectPolylines(self.origin, self.direction, self.viewport, self.gen_curve_geometry.prims(), self.gen_curve_bsphere.prims(), self.edge_range)
                    self.log("intersect_curve:" + str(intersect_curve))
                    # 生成カーブと交差していたら
                    if intersect_curve != -1:
                        #
                        if self.selected_gen_prim == intersect_curve:
                            self.selected_gen_prim = -1
                        else:
                            self.selected_gen_prim = intersect_curve
                            self.selected_point = -1    # 選択ポイントは解除する
                            multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)
                            multiparm_curves = self.node.parm("curves_" + str(multiparm_prim_index))
                            hou.ui.setMultiParmTabInEditors(multiparm_curves, intersect_curve)
                    else:
                        self.selected_gen_prim = -1

                    # マウスポインタと生成ポイントの交差判定
                    list_pos = self.getPointPositionsFromMultiparm(self.selected_src_prim)
                    intersect_point = self.isIntersectPoints(self.origin, self.viewport, list_pos, self.point_range)

                    # マウスポインタがポイントと交差していたらドラッグ状態へ遷移
                    if intersect_point != -1:
                        self.start()
                        #self.log(intersect_point)
                        self.dragIndex = intersect_point
                        self.drag = True
                        self.selected_point = intersect_point
                        curve_index = int(intersect_point/2)
                        #point_index = int(intersect_point%2)

                        self.selected_gen_prim = curve_index

                        multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)
                        multiparm_curves = self.node.parm("curves_" + str(multiparm_prim_index))
                        hou.ui.setMultiParmTabInEditors(multiparm_curves, curve_index)

                    else:
                        # 
                        if intersect_curve == -1:
                            self.intersect_src_prim = self.isIntersectPolylines(self.origin, self.direction, self.viewport, self.src_curve_geometry.prims(), self.src_curve_bsphere.prims(), self.edge_range)
                            # 親のポリラインとの交差
                            if self.intersect_src_prim == self.selected_src_prim:
                                # 新しくポイントを生成し、ポリラインを生成する
                                isIntersect, parametric_u, pos = self.calcParametricPos(self.origin, self.direction, self.viewport, self.src_curve_geometry.prim(self.intersect_src_prim), self.edge_range)
                                
                                multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)
                                multiparm_curves = self.node.parm("curves_%d" % (multiparm_prim_index))
                                curve_index = multiparm_curves.evalAsInt()
                                multiparm_curves.insertMultiParmInstance(curve_index)
                                #self.log("multiparm_prim_index:" + str(multiparm_prim_index))
                                #self.log("curve_index:" + str(curve_index))
                                multiparm_points = self.node.parm("points_%d_%d" % (multiparm_prim_index, curve_index))
                                multiparm_points.insertMultiParmInstance(0)
                                self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, curve_index, 0)).set(parametric_u-0.01) # 座標をセット
                                multiparm_points.insertMultiParmInstance(1)
                                self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, curve_index, 1)).set(parametric_u+0.01)

                                self.selected_point = -1
                                self.selected_gen_prim = -1
                            # 親のポリラインと交差していない
                            else:
                                # SELECTモードへ
                                #self.mode = Mode.SELECT
                                #self.modeChange()
                                #return
                                self.selected_point = -1
                                self.selected_gen_prim = -1
                            
            else:
                self.drag = False
                self.finish()
                if self.mouse_pressed == False and self.mouse_pressed != self.old_mouse_pressed:
                    if self.selected_point != -1:
                        self.swapParametricUV() # 順序反転

            # Drawablesジオメトリの更新
            self.updateGuideEditPoint()
            self.guideEditPoint.show(True)
            self.updateGuideSelectedPoint()
            self.guideSelectedPoint.show(True)
            self.updateGuideCurveLine()
            self.guideCurveLine.show(True)
            self.updateGuideSelectedCurveLine()
            self.guideSelectedCurveLine.show(True)
            
        self.old_mouse_pressed = self.mouse_pressed

        return True
        
    #
    # モード切替時
    #
    def modeChange(self):
        self.selected_point = -1
        self.intersect_gen_prim = -1

        if self.mode == Mode.SELECT:
            self.updateGuideSelectedLine()
            self.guideSelectedLine.show(True)
            self.guideCurveLine.show(False)
            self.guideEditPoint.show(False)
            self.guideSelectedPoint.show(False)

            HUD_TEMPLATE = {
                "title": "Polyline Path", "desc": "tool", "icon": "SOP_subnet",
                "rows": [
                    {"id": "mode", "label": "Mode", "value": "SELECT"},
                    {"id": "mode_g", "type": "choicegraph", "count": 2, "value": 0},
                    {"id": "switch_act", "label": "Switch Mode", "key": "Ctrl"},
                    {"id": "add_act", "label": "Select Prim", "key": "LMB"},
                    {"id": "focus_act", "label": "Camera Focus", "key": "F"},
                ]
            }
            self.scene_viewer.hudInfo(hud_template=HUD_TEMPLATE)

        elif self.mode == Mode.EDIT:
            self.updateGuideEditPoint()
            self.guideEditPoint.show(True)
            
            self.guideSelectingLine.show(False)
            self.guideSelectedLine.show(False)
            
            HUD_TEMPLATE = {
                "title": "Polyline Path", "desc": "tool", "icon": "SOP_subnet",
                "rows": [
                    {"id": "mode", "label": "Mode", "value": "EDIT"},
                    {"id": "mode_g", "type": "choicegraph", "count": 2, "value": 1},
                    {"id": "switch_act", "label": "Switch Mode", "key": "Ctrl"},
                    {"id": "add_act", "label": "Add Point", "key": "LMB"},
                    {"id": "del_act", "label": "Delete Point", "key": "Del"},
                    {"id": "max_act", "label": "Maximize Length", "key": "A"},
                    #{"id": "split_act", "label": "Split Prim", "key": "V"},
                    {"id": "focus_act", "label": "Camera Focus", "key": "F"},
                ]
            }
            self.scene_viewer.hudInfo(hud_template=HUD_TEMPLATE)
            
        self.log(self.mode.name)

    #  
    # キーボードイベント
    #
    def onKeyEvent(self,kwargs):
        ui_event = kwargs["ui_event"]
        self.key_pressed = ui_event.device().keyString()

        #Delキーが押されたら削除する
        if self.key_pressed in ("Del"):
            if self.mode == Mode.SELECT:
                if self.selected_src_prim != -1:
                    multiparm_prim = self.node.parm("prims")
                    multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)

                    multiparm_prim.removeMultiParmInstance(multiparm_prim_index)
                    self.selected_src_prim = -1
                    self.selected_point = -1

                self.updateGuideSelectedLine()
                self.guideSelectedLine.show(True)
                self.updateGuideSelectedPoint()
                self.guideSelectedPoint.show(True)

            # EDIT Mode
            elif self.mode == Mode.EDIT:
                # 選択ポイントのカーブを削除
                if self.selected_src_prim != -1:
                    multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)
                    multiparm_curves = self.node.parm("curves_%d" % multiparm_prim_index)
                    if self.selected_point != -1:
                        curve_index = int(self.selected_point / 2)
                        multiparm_curves.removeMultiParmInstance(curve_index)
                        self.selected_point = -1
                    elif self.selected_gen_prim != -1:
                        multiparm_curves.removeMultiParmInstance(self.selected_gen_prim)
                        self.selected_gen_prim = -1

                self.updateGuideSelectedPoint()
                self.guideSelectedPoint.show(True)
                self.updateGuideEditPoint()
                self.guideEditPoint.show(True)
                self.updateGuideCurveLine()
                self.guideCurveLine.show(True)
                self.updateGuideSelectedCurveLine()
                self.guideSelectedCurveLine.show(True)

            return True

        '''
        # ポリラインの分割処理
        if self.key_pressed in ("v"):
            if self.mode == Mode.SELECT:
                # 選択ポリラインとマウスポインタから、分割する座標(パラメトリック座標)を取得する
                if self.selected_gen_prim != -1:
                    intersect_src_prim = self.isIntersectPolylines(self.origin, self.direction, self.viewport, self.gen_curve_geometry.prims(), self.gen_curve_bsphere.prims(), self.edge_range)
                    
                    # 選択ポリラインとマウスポインタの交差からパラメトリック座標を取得する
                    if intersect_src_prim == self.selected_gen_prim:
                        # 親のポリラインを探す
                        id = self.gen_curve_geometry.prim(self.selected_gen_prim).attribValue("parent_id")
                        parent_index = self.getParentPrimIndexFromId(id)
                        isIntersect, param_u, pos = self.calcParametricPos(self.origin, self.direction, self.viewport, self.src_curve_geometry.prim(parent_index), self.edge_range)
                        
                        param_end = self.node.parm("pt_%d_%d" % (self.selected_gen_prim, 1)).evalAsFloat()
                        self.node.parm("pt_%d_%d" % (self.selected_gen_prim, 1)).set(param_u) # 座標をセット

                        # 新規ポイント(プリミティブ)を生成する
                        multiparm = self.node.parm("prims")
                        index = multiparm.evalAsInt()
                        multiparm.insertMultiParmInstance(index)

                        multiparm_points = self.node.parm("points_" + str(index))
                        multiparm_points.insertMultiParmInstance(0)
                        multiparm_points.insertMultiParmInstance(1)
                        
                        self.node.parm("parent_id_%d" % index).set(id)          # 親のUUIDを設定
                        self.node.parm("pt_%d_%d" % (index, 0)).set(param_u)    # 座標をセット
                        self.node.parm("pt_%d_%d" % (index, 1)).set(param_end)  # 座標をセット
  
                        self.updateGuideSelectedLine()
                        self.guideSelectedLine.show(True)
                        self.updateGuideSelectedPoint()
                        self.guideSelectedPoint.show(True)

            return True
        '''
        # 最大の長さのカーブに変更する
        if self.key_pressed in ("a"):
            if self.mode == Mode.EDIT:
                if self.selected_point != -1:
                    list_pos = []
                    multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)
                    curve_index = int(self.selected_point / 2)

                    multiparm_curve = self.node.parm("curves_" + str(multiparm_prim_index))
                    for i in range(multiparm_curve.evalAsInt()):
                        if i != curve_index:
                            multiparm_point = self.node.parm("points_%d_%d" % (multiparm_prim_index, i))
                            for j in range(multiparm_point.evalAsInt()):
                                param_u = self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, i, j)).eval()
                                list_pos.append(param_u)

                    list_pos.append(0.0)
                    list_pos.append(1.0)
                    list_pos.sort()

                    # 始点
                    param_u = self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, curve_index, 0)).eval()
                    for i in range(len(list_pos)-1):
                        if(list_pos[i] < param_u and param_u < list_pos[i+1]):
                            self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, curve_index, 0)).set(list_pos[i])
                            break

                    # 終点
                    param_u = self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, curve_index, 1)).eval()
                    for i in range(len(list_pos)-1):
                        if(list_pos[i] < param_u and param_u < list_pos[i+1]):
                            self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, curve_index, 1)).set(list_pos[i+1])
                            break

                    self.updateGuideEditPoint()
                    self.guideEditPoint.show(True)
                    self.updateGuideSelectedPoint()
                    self.guideSelectedPoint.show(True)
                    self.updateGuideCurveLine()
                    self.guideCurveLine.show(True)

            return True

        '''
        # オーバーラップの解消
        if self.key_pressed in ("c"):
            if self.mode == Mode.SELECT:
                if self.selected_gen_prim != -1:
                    if self.isPrimPolyline(self.selected_gen_prim) == 1:
                        list_indices = []
                        selectedParentPrim = self.getParentPrimIndexFromSelectedGenPrim()
                        multiparm = self.node.parm("prims")
                        for i in range(multiparm.evalAsInt()):
                            if i != self.selected_gen_prim:
                                id = self.node.parm("parent_id_%d" % i).eval()
                                parent = self.getParentPrimIndexFromId(id)
                                if parent == selectedParentPrim:
                                    list_indices.append(i)

                        # 始点
                        pos = self.node.parm("pt_%d_%d" % (self.selected_gen_prim, 0)).eval()
                        farPos = pos
                        for i in range(len(list_indices)):
                            start = self.node.parm("pt_%d_%d" % (list_indices[i], 0)).eval()
                            end = self.node.parm("pt_%d_%d" % (list_indices[i], 1)).eval()
                            if start < pos and pos < end:
                                if end > farPos:
                                    farPos = end
                        self.node.parm("pt_%d_%d" % (self.selected_gen_prim, 0)).set(farPos)

                        # 終点
                        pos = self.node.parm("pt_%d_%d" % (self.selected_gen_prim, 1)).eval()
                        nearPos = pos
                        for i in range(len(list_indices)):
                            start = self.node.parm("pt_%d_%d" % (list_indices[i], 0)).eval()
                            end = self.node.parm("pt_%d_%d" % (list_indices[i], 1)).eval()
                            if start < pos and pos < end:
                                if start < nearPos:
                                    nearPos = start
                        self.node.parm("pt_%d_%d" % (self.selected_gen_prim, 1)).set(nearPos)

                        self.updateGuideSelectedLine()
                        self.guideSelectedLine.show(True)
                        self.updateGuideSelectedPoint()
                        self.guideSelectedPoint.show(True)

            return True
        '''
        # カメラのピボットを選択しているポイント座標にする
        if self.key_pressed in ("f"):
            #self.log(self.selected_point)
            center = hou.Vector3()
            flag = False
            if self.mode == Mode.SELECT:
                if self.selected_src_prim != -1:
                    points = self.src_curve_geometry.prim(self.selected_src_prim).points()
                    center = sum((p.position() for p in points), hou.Vector3()) / len(points)
                    flag = True

            elif self.mode == Mode.EDIT:
                multiparm_prim_index = self.getMultiparmIndexFromPrim(self.selected_src_prim)
                if self.selected_point != -1:
                    curve_index = int(self.selected_point / 2)
                    point_index = int(self.selected_point % 2)
                    param_u = self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, curve_index, point_index)).eval()
                    center = self.calcPosFromParametricUV(self.src_curve_geometry.prim(self.selected_src_prim), param_u)
                    flag = True

                elif self.selected_gen_prim != -1:
                    param_u = self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, self.selected_gen_prim, 0)).eval()
                    pos0 = self.calcPosFromParametricUV(self.src_curve_geometry.prim(self.selected_src_prim), param_u)
                    param_u = self.node.parm("pt_%d_%d_%d" % (multiparm_prim_index, self.selected_gen_prim, 1)).eval()
                    pos1 = self.calcPosFromParametricUV(self.src_curve_geometry.prim(self.selected_src_prim), param_u)
                    center = (pos0 + pos1)/2
                    flag = True

            if flag:
                viewTransform = self.viewport.viewTransform()
                tuples = viewTransform.asTuple()
                worldCamPos = hou.Vector3(tuples[12], tuples[13], tuples[14])
                
                # カメラのピボットを変更する
                cam = self.viewport.defaultCamera() # hou.GeometryViewportCamera
                camPivotPos = hou.Vector3(cam.pivot())
                dist = (worldCamPos - camPivotPos).length()

                cam.setPivot(center)
                cam.setTranslation(center + hou.Vector3(0, 0, dist))

            return True

        # デバッグ用
        if self.key_pressed in ("z"):
            return True

        self.key_pressed = None
        return False

    #
    # 修飾キーのイベント
    #
    def onKeyTransitEvent(self, kwargs):
        ui_event = kwargs["ui_event"]
        if ui_event.device().isCtrlKey() == 1:
            if self.mode == Mode.SELECT:
                if self.selected_src_prim != -1:
                    self.mode = Mode.EDIT
                    self.modeChange()

            elif self.mode == Mode.EDIT:
                self.mode = Mode.SELECT
                self.modeChange()
            
        return False

    def onDraw( self, kwargs ):
        """ This callback is used for rendering the drawables """
        handle = kwargs["draw_handle"]
        self.guideSelectedPoint.draw(handle)
        self.guideEditPoint.draw(handle)
        self.guideSelectingLine.draw(handle)
        self.guideSelectedLine.draw(handle)
        self.guideCurveLine.draw(handle)
        self.guideSelectedCurveLine.draw(handle)

def createViewerStateTemplate():
    """ Mandatory entry point to create and return the viewer state 
        template to register. """

    state_typename = kwargs["type"].definition().sections()["DefaultState"].contents()
    state_label = "ViewerStatePolylinePathTool::1.0"
    state_cat = hou.sopNodeTypeCategory()

    template = hou.ViewerStateTemplate(state_typename, state_label, state_cat)
    template.bindFactory(State)
    template.bindIcon(kwargs["type"].icon())

    return template

関数

getIdFromParentPrimIndex()

getIdFromParentPrimIndex(int index) 返り値:string

ポリラインからIDを取得する。親のポリライン対象。

getParentPrimIndexFromId()

getParentPrimIndexFromId(string id) 返り値:int

IDから一致するプリミティブのインデックスを返す。親のポリライン対象。

getPointPositionsFromMultiparm()

getPointPositionsFromMultiparm(int index) 返り値:hou.Vector3[]

Multiparmの番号を引数に入れ、ポイント座標2つ(または1つ)を取得する。

calcPosFromParametricUV()

calcPosFromParametricUV(int prim_index; float param_u) 返り値:hou.Vector3

ポリライン上のパラメトリック座標からワールド座標を計算して返す。

calcParametricPos()

calcParametricPos(hou.Vector3 origin; hou.Vector3 direction; hou.GeometryViewport viewport; int prim; float radius) 返り値:bool isIntersect, float parametric_u, hou.Vector3 closePos

ビューポート上のマウスポインタ位置から近くのポリライン上の座標を返す。

isIntersectPoints()

isIntersectPoints(hou.Vector3 origin; hou.GeometryViewport viewport; hou.Vector3[] positions; float radius) 返り値:int

マウスポインタとポイントが交差しているかチェックする。交差していたらインデックスを返す。なければ-1が返る。

isIntersectPolylines()

isIntersectPolylines(hou.Vector3 origin; hou.Vector3 direction; hou.GeometryViewport viewport; hou.Prim[] prims; hou.Prim[] bspheres; float radius) 返り値:int

交差するポリラインのインデックスを返す。

Detail WrangleをHDA内に配置する

パラメトリックUVからポリラインを生成している。

//
// Multiparmのパラメータからプリミティブを生成する
// Run Over: Detail
//
int numprims = `chs("../prims")`;
for(int i = 0; i < numprims; i++)
{
    int prim = addprim(0, "polyline");
}

wrangle1というノード名にしておき、Pythonコードから参照するようになっている。ここで生成されたポリラインがHDAの結果の出力となる。

//
// パラメータからポリラインを生成する
// Run Over: Primitives
//

// idからPrimのインデックスを探す
string id = chs(sprintf("../parent_id_%g", @primnum));

int parent_index = -1;
for(int j = 0; j < nprimitives(1); j++)
{
    string uuid = prim(1, "id", j);
    if(uuid == id)
    {
        parent_index = j;
        break;
    }
}

if(parent_index != -1)
{
    
    int points = chi(sprintf("../points_%g", @primnum));
    
    if(points == 2)
    {
        float start = chf(sprintf("../pt_%g_%g", @primnum, 0));
        float end = chf(sprintf("../pt_%g_%g", @primnum, 1));
        
        setprimattrib(0, "parent_id", @primnum, id);
        setprimattrib(0, "u_start", @primnum, start);
        setprimattrib(0, "u_end", @primnum, end);
        
        /*
        // 順序入れ替え
        if(start > end)
        {
            float temp = start;
            start = end;
            end = temp;
        }*/
        
        int pts[] = primpoints(1, parent_index);
        
        float curveLength = primintrinsic(1, "measuredperimeter", parent_index);
        float sum = 0;
        
        float startPos = curveLength * start;
        float endPos = curveLength * end;
        
        int flagStart = 0;  // 通過したらフラグを立てる
        int flagEnd = 0;
        
        for(int j = 0; j < len(pts)-1; j++)
        {
            vector p0 = point(1, "P", pts[j]);
            vector p1 = point(1, "P", pts[j+1]);
            float length = length(p1-p0);
            
            // 最初のポイントのエリアを発見
            if(sum + length > startPos && flagStart == 0)
            {
                float rest = startPos - sum;
                vector newPos = p0 + normalize(p1 - p0) * rest;
                int pt = addpoint(0, newPos);
                addvertex(0, @primnum, pt);
                flagStart = 1;
            }
            
            // 終点ポイントのエリアを発見
            if(sum + length > endPos && flagEnd == 0)
            {
                float rest = endPos - sum;
                vector newPos = p0 + normalize(p1 - p0) * rest;
                int pt = addpoint(0, newPos);
                addvertex(0, @primnum, pt);
                flagEnd = 1;
                break;
            }
            // 一番最後まで到達した場合
            else if(flagStart == 1)
            {
                int pt = addpoint(0, p1);
                addvertex(0, @primnum, pt);
            }
    
            // 何もなければ加算
            sum += length;
        }
    }
}
else
{
    removeprim(0, @primnum, 1);
}

メモ

IDを介して親のポリラインと生成ポリライン、マルチパラメータ(UI)の相互のやりとりがあるためややこしくなっている。

生成ポリラインから親のポリラインのインデックスを探す

id = self.gen_curve_geometry.prim(self.selected_gen_prim).attribValue("parent_id")
parent_index = self.getParentPrimIndexFromId(id)

wrangleで生成したポリラインに割り振ったIDから探している。

← 記事一覧へ