指定の線形状に沿うように選択形状を整列 (スクリプト)

指定の線形状に沿うように、選択形状を整列します。

標準の機能では存在しないため、スクリプトで実現します。


import numpy
import math

scene = xshade.scene()

#---------------------------------------------------------.
# cPosがp1 - p2の直線に下す垂線情報を計算.
# @param[in]  p1      直線の始点.
# @param[in]  p2      直線の終点.
# @param[in]  cPos    調査点.
# @param[out] retData 垂線情報が返る.
#                     retData = {'position': 0.0, 'distance': 0.0}
#                          'position' : p1-p2を0.0-1.0としたときの位置.
#                          'distance' : p1-p2に下したcPosからの垂線距離.
#---------------------------------------------------------.
def calcPerpendicular (p1, p2, cPos, retData):
  retData['position'] = -1.0
  retData['distance'] = -1.0

  fMin = 1e-7
  vDir = numpy.array([p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]], dtype='float64')
  len1 = numpy.linalg.norm(vDir)
  if len1 < fMin:
    return False
  
  vDir = vDir / len1

  vDir2 = numpy.array([cPos[0] - p1[0], cPos[1] - p1[1], cPos[2] - p1[2]], dtype='float64')
  len2  = numpy.linalg.norm(vDir2)
  if len2 < fMin:
    retData['position'] = 0.0
    retData['distance'] = 0.0
    return True

  vDir2 = vDir2 / len2

  angleV = numpy.dot(vDir, vDir2)
  if math.fabs(angleV) > 1.0 - fMin:
    aPos = len2 / len1
    if angleV < 0.0:
      retData['position'] = -aPos
      retData['distance'] = 0.0
      return True

  len3 = angleV * len2
  retData['position'] = len3 / len1
  retData['distance'] = numpy.linalg.norm((vDir * len3 + p1) - cPos)
  return True

#---------------------------------------.
# ゼロチェック.
#---------------------------------------.
def isZero (v):
  minV = 1e-5
  if v > -minV and v < minV:
    return True
  return False

#---------------------------------------.
# ベジェ上の位置を計算.
#---------------------------------------.
def getBezierPoint (v1Pos, v1Out, v2In, v2Pos, fPos):
  fMin = 1e-6

  rPos = [0.0, 0.0, 0.0]
  cPos = []
  cPos.append([v1Pos[0], v1Pos[1], v1Pos[2]])
  cPos.append([v1Out[0], v1Out[1], v1Out[2]])
  cPos.append([v2In[0],  v2In[1],  v2In[2]])
  cPos.append([v2Pos[0], v2Pos[1], v2Pos[2]])

  fPos2 = float(fPos)
  fPos2 = max(0.0, fPos2)
  fPos2 = min(1.0, fPos2)
  
  if isZero(fPos2):
    rPos = cPos[0]
    return rPos

  if isZero(fPos2 - 1.0):
    rPos = cPos[3]
    return rPos

  # ベジェ曲線の計算.
  t   = fPos2
  t2  = 1.0 - t
  t2d = t2 * t2
  td  = t * t
  b1  = t2d * t2
  b2  = 3.0 * t * t2d
  b3  = 3.0 * td * t2
  b4  = t * td

  for i in range(3):
    rPos[i] = b1 * cPos[0][i] + b2 * cPos[1][i] + b3 * cPos[2][i] + b4 * cPos[3][i]

  return rPos

#---------------------------------------.
# 線形状を直線の集まりに分解.
# @param[in] shape       対象形状.
# @param[in] lineDivCou  ラインの全体の分割数.
# @return ワールド座標でのポイントの配列.
#---------------------------------------.
def getLinePoints (shape, lineDivCou):
  vList = []
  if shape.type != 4:  # 線形状でない場合.
    return vList
  
  lwMat = numpy.matrix(shape.local_to_world_matrix)
  vCou = shape.total_number_of_control_points

  divCou = lineDivCou / vCou
  if divCou < 4:
    divCou = 4
  divD = 1.0 / float(divCou)

  # ベジェをポイントで保持.
  for i in range(vCou):
    if shape.closed == False and (i + 1 >= vCou):
      break
    p1 = shape.control_point(i)
    p2 = shape.control_point((i + 1) % vCou)

    dPos = 0.0
    for j in range(divCou + 1):
      p = getBezierPoint(p1.position, p1.out_handle, p2.in_handle, p2.position, dPos)
      if (i == 0) or (i != 0 and j > 0):
        # pをワールド座標に変換.
        v = numpy.array([p[0], p[1], p[2], 1.0])
        v = numpy.dot(v, lwMat)
        p = [v[0,0], v[0,1], v[0,2]]

        vList.append(p)
      dPos += divD

  return vList

#--------------------------------------------------------------.
# 指定の形状の中心座標をワールド座標で取得.
# @param[in] shape  対象形状.
#--------------------------------------------------------------.
def getShapeWorldCenterPos (shape):
  cPos = shape.center_point  # ローカル座標での中心座標.
  lwMat = numpy.matrix(shape.local_to_world_matrix)
  v = numpy.array([cPos[0], cPos[1], cPos[2], 1.0])
  v = numpy.dot(v, lwMat)
  cPos = [v[0,0], v[0,1], v[0,2]]
  return cPos

#--------------------------------------------------------------.
# 指定の形状のワールド座標の中心を変更.
# @param[in] shape    対象形状.
# @param[in] orgWPos  元のワールド座標での中心.
# @param[in] wPos     新しいワールド座標での中心.
#--------------------------------------------------------------.
def setShapeWorldCenterPos (shape, orgWPos, wPos):
  # ワールド座標位置をローカル座標に変換.
  wlMat = numpy.matrix(shape.world_to_local_matrix)
  v = numpy.array([orgWPos[0], orgWPos[1], orgWPos[2], 1.0])
  v = numpy.dot(v, wlMat)
  orgCPos = [v[0,0], v[0,1], v[0,2]]

  v = numpy.array([wPos[0], wPos[1], wPos[2], 1.0])
  v = numpy.dot(v, wlMat)
  cPos = [v[0,0], v[0,1], v[0,2]]

  # 座標の変更は、形状の移動で行う.
  dV = [cPos[0] - orgCPos[0], cPos[1] - orgCPos[1], cPos[2] - orgCPos[2]]
  shape.move_object(((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (dV[0], dV[1], dV[2], 1)))

# --------------------------------------------.
# 指定の形状を、vListの線形状に近づかせる.
# @param[in] vList      線形状を分割したポイント配列.
# @param[in] shape      対象の形状.
# @param[in] distV      選択形状を記憶された線形状に近づかせる距離 (ミリメートル).
# @param[in] fMargin    範囲0.0-1.0を少しはみ出させるマージン.
# @return 処理に成功すればTrueが返る.
# --------------------------------------------.
def moveToLine (vList, shape, distV, fMargin):
  # 形状のワールド座標での中心座標.
  centerWPos = getShapeWorldCenterPos(shape)

  # 垂線計算で使用する連想配列.
  pDat = {
    'position': 0.0,
    'distance' : 0.0
  }

  # ライン上の垂線位置を計算し、一番垂線までの距離が短いところを採用.
  minDist = -1.0
  minPos  = [0.0, 0.0, 0.0]
  for i in range(len(vList) - 1):
    p1 = vList[i]
    p2 = vList[i + 1]
    if calcPerpendicular(p1, p2, centerWPos, pDat) == False:
      continue
        
    # p1-p2を0.0-1.0としたときの位置.
    pos  = pDat['position']

    # 垂線の足からcenterPosまでの距離.
    dist = pDat['distance']

    if pos < -fMargin or pos > 1.0 + fMargin:
      continue

    if minDist < 0.0 or dist < minDist:
      minDist = dist

      # 垂線の足を計算.
      np1 = numpy.array([p1[0], p1[1], p1[2]], dtype='float64')
      np2 = numpy.array([p2[0], p2[1], p2[2]], dtype='float64')
      minPos  = (np2 - np1) * pos + np1

  if minDist >= 0.0:
    cPos = numpy.array([centerWPos[0], centerWPos[1], centerWPos[2]], dtype='float64')
    vDir = cPos - minPos

    # 垂線の足(minPos)からdistVだけ離れた位置を計算.
    lenV = numpy.linalg.norm(vDir)
    if lenV != 0.0:
       vDir /= lenV

    if lenV > distV:
      newPos = vDir * distV + minPos

      # 中心位置を更新.
      setShapeWorldCenterPos(shape, centerWPos, [newPos[0], newPos[1], newPos[2]])

    return True

  return False

# ---------------------------------------------------------------------.

# 範囲0.0-1.0を少しはみ出させるマージン.
fMargin = 0.5

if scene.number_of_memorized_shapes == 0 or scene.memorized_shape().type != 4:
  xshade.show_message_box('線形状を「記憶」してください。', False)
  
else:
  # 記憶された形状を取得.
  memShape = scene.memorized_shape()

  if memShape.type == 4:   # 線形状の場合.

    # ダイアログボックスの作成と表示.
    dlg = xshade.create_dialog_with_uuid('5ac13729-57cb-4777-ba0b-644d3f1daca0')
    div_id = dlg.append_int('線形状の分割数')
    dist_id = dlg.append_float('線形状から離す距離', 'mm')
    dlg.set_value(div_id, 50)
    dlg.set_value(dist_id, 10.0)
    dlg.set_default_value(div_id, 50)
    dlg.set_default_value(dist_id, 10.0)
    dlg.append_default_button()

    if dlg.ask("選択形状を記憶した線形状に整列"):
      lineDivCou = max(10, dlg.get_value(div_id))
      distV      = max(0.0, dlg.get_value(dist_id))

      # 選択形状を取得し、線形状以外のものを格納.
      aShapes = []
      for shape in scene.active_shapes:
        if shape.type != 4:
          aShapes.append(shape)

      for loop in range(3):
        # 線形状からポイントの配列を取得.
        vList = getLinePoints(memShape, lineDivCou)

        updateF = False
        for i in range(len(aShapes)):
          if aShapes[i] == None:
            continue

          # 指定の形状を、memShape(vListがポイントの配列)の線形状に近づかせる.
          if moveToLine(vList, aShapes[i], distV, fMargin):
            aShapes[i] = None
            updateF = True

        if updateF == False:
          break

        # 線分上に垂線が存在しない場合は、ラインの分割を粗くして再度行う.
        lineDivCou /= 2

使い方は、
線形状を選択しツールパラメータの「記憶」ボタンを押して形状を記憶します。
線形状に寄せる形状をブラウザで選択し、上記スクリプトを実行します。

実行すると、ダイアログボックスにパラメータが表示されます。

「線形状の分割数」は記憶した線形状を内部的に何分割するか整数で指定し、
「線形状から離す距離」は線形状からどれだけ離すかを指定します。
ダイアログボックスでOKボタンを押して確定すると、
選択形状が記憶した線形状に沿って垂線が「線形状から離す距離」の距離だけ離れた位置に移動します。
なお、このスクリプトはUNDO/REDOには対応していません。

以下のように曲線に対しても使用できます。

Translate »