
########################################################
#
#               Side Tagging and Glueing Plugin
#                      v1.5a, Mar 22, 1999
#                      works with Quark5.7
#
#
#        by tiglari@hexenworld.com, with lots of advice
#              and code snippets from Armin Rigo
#     
#
#   Possible extensions:
#    - button & floating toolbar as well as menu commands
#    - tracking of tagged sides that move
#   I'm not sure how useful either of these would really be, and
#   think there's other stuff that's a higher priority now.
#
#   You may freely distribute modified & extended versions of
#   this plugin as long as you give due credit to tiglari &
#   Armin Rigo. (It's free software, just like Quark itself.)
#
#   Please notify bugs & improvements to tiglari@hexenworld.com
#  
#
##########################################################

Info = {
   "plug-in":       "Side Tag & Glue",
   "desc":          "Side tagging and gluing to tagged side",
   "date":          "1999",
   "author":        "tiglari",
   "author e-mail": "tiglari@hexenworld.com",
   "quark":         "Version 5.7" }

import quarkx
import quarkpy.mapmenus
import quarkpy.mapentities
import quarkpy.qmenu
import quarkpy.mapeditor
import quarkpy.mapcommands
from quarkpy.maputils import *


#
# utilities
#
def gettagged(o):
  " safe fetch of tagging.tagged attribute"
  try:
    return o.tagging.tagged
  except (AttributeError): return None

def gettaggedpt(o):
  "Returns the tagged point."
  try:
    return o.tagging.tagpt
  except (AttributeError): return None

def anytag(o):
  "Is anything tagged ?"
  return gettagged(o) is not None or gettaggedpt(o) is not None


#
#  Now the right-mouse button menu for sides.
#

tagtext = "|`Tags' a side for reference in later operations of positioning and alignment.\n\nThe tagged side then appears in red."
gluetext = "Moves & aligns this side to the tagged one"
gluepttext = "Moves this side to the tagged point"
aligntext = "|Copies the texture from the tagged face to this one, wrapping around a shared edge with proper alignment.\n\nThis is only really supposed work when the faces abutt at an edge, although it sometimes works more generally."
wraptext = "|Wraps texture on this face around the prism capped by the tagged face, with minimal distortion.\n\nWrapping is clockwise around tagged side, viewed from outside.\n\n  Alpha Version"

def aligntexstate(aligntex, tagged, o):
  "sorts out what kind of abuttment, if any for tagged side and this one"
  if coplanar_adjacent_sides(tagged, o):
    aligntex.abuttype = 0
  elif intersecting_sides(tagged, o):
    aligntex.abuttype = 1
  else:
    aligntex.state = qmenu.disabled

def wraptexstate(wraptex, tagged, o):
  "figures out if this side abuts tagged in same poly"
  "attaches useful stuff to m on successful return"
  #
  # the faces belong to polys
  # 
  selpolys = o.faceof
  tagpolys = tagged.faceof
  if tagpolys[0].type != ":p" or selpolys[0].type != ":p":
     wraptex.state = qmenu.disabled
     return 0
  #
  # and indeed to the same poly
  #
  thepolys = intersection(tagpolys, selpolys)
  if thepolys == []:
    wraptex.state = qmenu.disabled
    return 0
  thepoly = thepolys[0]
  #
  # and indeed that these faces furthermore abutt
  #
  ovx = o.verticesof(thepoly)
  tvx = tagged.verticesof(thepoly)
#  shared = intersection_vect(tvx, ovx)
  #
  # the order to abutting vtx matters, since returned list of tuples
  #  provides index in first arg as second member of the tuples
  #
  shared = abutting_vtx(tvx, ovx)
  if shared == []:
    wraptex.state = qmenu.disabled
    return 0
#  squawk('shared len = ' + `len(shared)`)
  wraptex.shared = shared
  wraptex.taggedvx = tvx
  wraptex.tagged = tagged
  wraptex.thepoly = thepoly
  wraptex.selface = o
  return 1
  
def intersection(l1, l2):
  "but not for points/vectors"
  return filter(lambda el, list=l2: list.count(el)>0, l1)

def abutting_vtx(l1, l2):
  "gets the two vtx shared between l1 & l2, which are"
  "supposed to be vertex-cyles of abutting faces"
  "checks for truth of various assumptions"
  "returns (vtx,ind) pozzie, where ind is the index in l1"
  intx = []
  pozzies = []
  i = -1
  for el1 in l1:
    i = i+1
    for el2 in l2 :
      if not (el1-el2):
        pozzies.append(i)
        intx.append((el1,i))
        break
  if len(intx) != 2:
    squawk("Somethings wrong here, #intersection = "+`len(intx)`)
    return []
  if pozzies[0]==0 and pozzies[1]>1:
    intx.reverse()
  return intx
    

def intersection_vect(l1, l2):
  "for points/vectors only"
  "note that the points come out in the same order they have in l1"
  shared = []
  for el1 in l1:
    for el2 in l2 :
      if not (el1-el2):
        shared.append(el1)
        break
  return shared
       
def gluemenuitem(String, ClickFunction,o, helptext=''):
  "make a menu-item with a side attached"
  item = qmenu.item(String, ClickFunction, helptext)
  item.side = o
  return item

# I think the right mouse menu needs to be built on the click
#  in order to have the clicked-on side attached to it,
#  is this right?        -- Yes.

#
#  stash the old function in the new function's last parameter
#
def tagmenu(o, editor, oldfacemenu = quarkpy.mapentities.FaceType.menu.im_func):
  "the new right-mouse for sides"
  menu = oldfacemenu(o, editor)
  tagged = gettagged(editor)
  if o is tagged:
    menu[:0] = [qmenu.item("Clear Tag",ClearTagClick),   # already tagged, keep only this command
                qmenu.sep]
  else:
    glueitem = gluemenuitem("Glue to tagged", GlueSideClick, o, gluetext)
    aligntex = gluemenuitem("Wrap texture from tagged", AlignTexClick, o, aligntext)
    wraptex = gluemenuitem("Wrap texture around prism", WrapTexClick, o, wraptext)
    if tagged is None:
      if gettaggedpt(editor) is None:
        glueitem.state = qmenu.disabled
      else:
        glueitem.hint = gluepttext
      aligntex.state = qmenu.disabled
      wraptex.state = qmenu.disabled
    else:
      aligntexstate(aligntex, tagged, o)
      #
      # attach vertices shared between this and tagged to
      #  help things go faster if this item is used
      #
      wraptexstate(wraptex, tagged, o)
    menu[:0] = [gluemenuitem("Tag side",TagSideClick,o,tagtext),
                glueitem,
                aligntex,
                wraptex,
                qmenu.sep]
  return menu


def WrapTexClick(m):
  if len (m.shared) != 2:
     squawk('len m.shared ! = '+`len(m.shared)`+', me bail!')
     return
  editor = mapeditor()
  tvx  = m.taggedvx      # cycle of faces from tagged side
  faces = m.thepoly.faces
  ind1 = m.shared[0][1]
  ind2 = m.shared[1][1]
#  squawk(`ind1` + ':' + `ind2`)
  selface = m.selface
  poly = m.thepoly
  polyfaces = poly.faces
  polyfaces.remove(selface)
  polyfaces.remove(m.tagged)
  vtxlength = len(tvx)
  vtxstart = ind1
  vtxcurr = ind2
  vtg = m.shared[1][0]-m.shared[0][0]
  wraplength = abs(vtg)
  wrapfaces = []
  #
  # get distance for wraparound, and list of faces to wrap
  #  to, in correct order (wrapfaces)
  #
  while vtxcurr != vtxstart:
    vtxnext = vtxcurr+1
    if vtxnext == vtxlength:
      vtxnext = 0
    vtx1 = tvx[vtxcurr]
    vtx2 = tvx[vtxnext]
    for face in polyfaces:
      vtxes = face.verticesof(poly)
      int = intersection_vect([vtx1, vtx2], vtxes)
      if len(int) == 2:
         polyfaces.remove(face)
         wrapfaces.append(face)
         break
    wraplength = wraplength + abs(vtx2-vtx1)
#    squawk("wrap length: "+`len(wrapfaces)`)
    #
    # get ready for next iteration, wrapping around end
    #
    vtxcurr = vtxnext
  #
  # Now figure out the distortion factors
  #
  txsrc=editor.TexSource
  tp = selface.threepoints(2, txsrc)
  vtgn = vtg.normalized
  # projection of texture scale vectors onto prism cap
  axlengths = (abs(tp[1]-tp[0]), abs(tp[2]-tp[0]))
  normal = ((tp[1]-tp[0]).normalized, (tp[2]-tp[0]).normalized)
  proj = (math.fabs((tp[1]-tp[0]).normalized*vtgn), math.fabs((tp[2]-tp[0]).normalized*vtgn))
#  squawk("tex dist = (" + `dist[0]`+":"+`dist[1]`+")")
  aspect = quarkx.msgbox("preserve aspect ratio?",
    MT_CONFIRMATION, MB_YES | MB_NO)
#  squawk("aspect = "+`aspect`)
  if proj[0] > proj[1]:
    primaxis = 0
  else:
    primaxis = 1
  projlength = axlengths[primaxis]*proj[primaxis]
  times = wraplength/projlength
  repeat = math.floor(times)
  if times - repeat > .5:
    repeat = repeat + 1
#  squawk("t="+`times`+"; r="+`repeat`)
  #
  # replength is the desired length of projection of the
  #  chosen texture axis onto the capping face
  #
  replength = wraplength/repeat
  stretch = replength/axlengths[primaxis]
#  squawk("stretch = "+`stretch`)
  stretched = ((tp[1]-tp[0])*stretch, (tp[2]-tp[0])*stretch)
  newaxes = [tp[1], tp[2]]
  secaxis = 1 - primaxis
  newaxes[primaxis] = tp[0]+stretched[primaxis]
  if aspect == MR_YES:
#    squawk("preserving aspect")
    newaxes[secaxis] = tp[0]+stretched[secaxis]
#  newaxes = (tp[0]+stretched[0], tp[0]+stretched[1])
  newface = selface.copy()
  newface.setthreepoints((tp[0], newaxes[0], newaxes[1]),2,txsrc)
  ActionString = "prism wrap"
  undo = quarkx.action()
  #
  # swap in the resized texture  
  #
  undo.exchange(selface, newface)
  startface = newface
  #
  # and now at last for the big wrap
  #
  for face in wrapfaces:
    currface = newface  
    newface = wraptex(currface, face)
    undo.exchange(face, newface)
  editor.ok(undo, ActionString)
  editor.layout.explorer.sellist = [startface]

def index_vect(list, vect):
  "finds the index-position of a vector on a list of vectors"
  ind = 0
  for el  in list:
    if not (el-vect):
      return ind;
    ind = ind + 1
   

def coplanar_adjacent_sides(side1,side2):
  list = [side1]
  quarkx.extendcoplanar(list,[side2])
  if len(list) == 2:
    return 1
  else: return 0

def intersecting_sides(side1, side2):
  if math.fabs(side1.normal*side2.normal) < .99999:
     return 1
  else: return 0


#
# now bung in the new one.
#
quarkpy.mapentities.FaceType.menu = tagmenu


#
#  Ditto for the right-mouse-button menu for vertices
#

verttext = "|To use this, you need to have one side tagged and another selected.\n\nThe selected side will then be aligned parallel to the tagged side, rotating around this vertex as a fulcrum."
def tagvertmenu(self, editor, view, oldvertmenu = quarkpy.maphandles.VertexHandle.menu.im_func):
  menu = oldvertmenu(self,editor,view)
  face = None
  tagged = gettagged(editor)
  if not tagged is None:
    selection = editor.layout.explorer.sellist
    if isoneface(selection) and not selection[0] is tagged:
      face = selection[0]
  item = gluemenuitem("&Align selection to tagged", GlueSideClick, face, verttext)
  item.fulcrum = self.pos
  if face is None:
    item.state = qmenu.disabled
  menu[:0] = [item]
  return menu

quarkpy.maphandles.VertexHandle.menu = tagvertmenu


#
#  Ditto for all handles that have a position
#

def tagpointitem(editor, origin):
  oldtag = gettaggedpt(editor)
  if oldtag is not None and not (origin-oldtag):
    tagv = qmenu.item("Clear tag", ClearTagClick)
  else:
    tagv = qmenu.item("&Tag point", TagPointClick, "|`Tags' the point below the mouse for reference in later operations of positioning and alignment.\n\nThe tagged point then appears in red.")
    tagv.pos = origin
  return tagv


def originmenu(self, editor, view, oldoriginmenu = quarkpy.qhandles.GenericHandle.OriginItems.im_func):
  menu = oldoriginmenu(self, editor, view)
  if isinstance(self, quarkpy.maphandles.FaceHandle):
    return menu        # nothing to do for faces

  if len(menu)==0 or menu[0] is not qmenu.sep:
    menu[:0] = [qmenu.sep]  # inserts a separator if necessary

  if view is not None:   # Point gluing for everything

    def GluePointClick(m, self=self, editor=editor, view=view):
      tagpt = gettaggedpt(editor)
      if tagpt is not None:
        self.Action(editor, self.pos, tagpt, MB_NOGRID, view)
      else:
        tagged = gettagged(editor)
        if tagged is not None:
          p = self.pos
          p = p - tagged.normal * (p*tagged.normal-tagged.dist)
          self.Action(editor, self.pos, p, MB_NOGRID, view)

    gluev = qmenu.item("&Glue to tagged", GluePointClick, "|Glue this point to the tagged point, or if a side is tagged, move this point into the plane of this side.")
    if not anytag(editor):
      gluev.state = qmenu.disabled
    menu[1:1] = [gluev]

  menu[1:1] = [tagpointitem(editor, self.pos)]
  return menu


quarkpy.qhandles.GenericHandle.OriginItems = originmenu


#
#  Ditto for the menu that appears when we click on the background
#

def backmenu(editor, view=None, origin=None, oldbackmenu = quarkpy.mapmenus.BackgroundMenu):
  menu = oldbackmenu(editor, view, origin)
  if origin is not None:
    item = tagpointitem(editor, editor.aligntogrid(origin))
    for test in menu:
      if hasattr(test, "origin"):
        i = menu.index(test)+1
        break
    else:
      i = 0
    menu[i:i] = [item]
  return menu


quarkpy.mapmenus.BackgroundMenu = backmenu


#
#  Now for the actual side-tagging machinery
#

class Tagging:
  "a place to stick side-tagging stuff"
  tagged = None
  tagpt = None
  oldfinishdrawing = None  # where we will stash the original


def drawsquare(cv, o, side):
  "function to draw a square around o"
  if o.visible:
    dl = side/2
    cv.brushstyle = BS_CLEAR
    cv.rectangle(o.x+dl, o.y+dl, o.x-dl, o.y-dl)


def checktree(root, obj):
  while obj is not root:
    t = obj.parent
    if t is None or not (obj in t.subitems):
      return 0
    obj = t
  return 1

    
def tagfinishdrawing(editor, view):
  "the new finishdrawning routine"
  Tagging.oldfinishdrawing(editor, view)
  tagged = gettagged(editor)
  if tagged is None:
    tagpt = gettaggedpt(editor)
    if tagpt is None:
      return
  elif not checktree(editor.Root,tagged):
    #
    # clear tag if face no longer in map
    #
    ClearTagClick(None)
    return
  cv = view.canvas()
  cv.pencolor = MapColor("Tag")
  if tagged is None:
    #
    # Point tagged
    #
    drawsquare(cv, view.proj(tagpt), 8)
  else:
    #
    # Face tagged
    #
    for vtx in editor.tagging.tagged.vertices: # is a list of lists
      sum = quarkx.vect(0, 0, 0)
      p2 = view.proj(vtx[-1])  # the last one
      for v in vtx:
        p1 = p2
        p2 = view.proj(v)
        sum = sum + p2
        cv.line(p1,p2)
      drawsquare(cv, sum/len(vtx), 8)

#
#  Menu item commands
#

#def isoneface(selections, msgboxes=0):
#    if selections is None: return 0
#    if msgboxes == 0:
#      if len(selections) == 1 and selections[0].type == ":f":
#        return 1
#      else: return 0
#    elif (len(selections) < 1):
#      quarkx.msgbox("No selection", MT_ERROR, MB_OK)
#    elif (len(selections) > 1):
#      quarkx.msgbox("Only one selection allowed", MT_ERROR, MB_OK)
#    elif (selections[0].type!= ":f"):
#      quarkx.msgbox("The selected object is not a face", MT_ERROR, MB_OK)
#    else:
#      return 1
#    return 0

def isoneface(selections):
  return len(selections) == 1 and selections[0].type == ':f'


def sideof (m, editor):
  try:
    return m.side
  except (AttributeError) :
    tagged = editor.layout.explorer.sellist
    if not isoneface(tagged):
      return None
    #editor.visualselection()       #clears handle on tagged side(s)
    return tagged[0]


def TagSideClick (m):
  "tags a side on mouse-click, also replaces finishdrawing"
  editor = mapeditor()
  if editor is None: return
  editor.tagging = Tagging()
  editor.tagging.tagged = sideof(m, editor)

  if Tagging.oldfinishdrawing is None:  # we haven't done this yet
    Tagging.oldfinishdrawing = quarkpy.mapeditor.MapEditor.finishdrawing
    quarkpy.mapeditor.MapEditor.finishdrawing = tagfinishdrawing
  mapeditor().invalidateviews()         # redraw the map

def TagPointClick (m):
  "tags a single point and replaces finishdrawing"
  editor = mapeditor()
  if editor is None: return
  editor.tagging = Tagging()
  editor.tagging.tagpt = m.pos

  if Tagging.oldfinishdrawing is None:  # we haven't done this yet
    Tagging.oldfinishdrawing = quarkpy.mapeditor.MapEditor.finishdrawing
    quarkpy.mapeditor.MapEditor.finishdrawing = tagfinishdrawing
  mapeditor().invalidateviews()         # redraw the map

# this doesn't work, gives NameError on mapeditor, why?
#    -- two reasons : 1st, it's "MapEditor". 2nd, more important : the module
#       quarkpy.mapeditor is NOT initialized yet when this plug-in loads...
#quarkpy.mapeditor.Mapeditor.stupid = 1

def ClearTagClick (m):
  "clears tag on menu-click"
  editor = mapeditor()
  if editor is None: return
  try:
    del editor.tagging
  except AttributeError:
    return
  editor.invalidateviews()

def AlignTexClick(m):
  "wraps texture from tagged to selected side"
  editor = mapeditor()
  if editor is None: return
  side = sideof(m, editor)
  tagged = gettagged(editor)
  editor.invalidateviews()
  ActionString = "wrap texture from tagged"
  undo = quarkx.action()
  if m.abuttype == 1:
#    squawk("intersection")
#
#   first, we find a texture axis on the tagged side that intersects
#   the selected plane, and then compute the point of intersection,
#   then rotate a copy of the tagged side around the point, (all this
#   now done by wraptex, and then swap it in for the selected side
#  
    newside = wraptex(tagged, side)
  undo.exchange(side, newside)
  editor.ok(undo, ActionString)

def wraptex(orig, side):
    newside = orig.copy()
    (o, t1, t2) = orig.threepoints(0)
    (r, s1, s2) = side.threepoints(0)
    n = side.normal
    if n*(t1-o) != 0:
#      quarkx.msgbox("t1 is OK",MT_INFORMATION, MB_OK)
      t = t1
    else:
#      quarkx.msgbox("t2 is hopefully OK",MT_INFORMATION, MB_OK)
      t = t2
    l = -(n*(o-r))/(n*(t-o))
    p = l*(t-o)+o
    if n*(p-r) > .000001:
      squawk("Sorry, something's not right here, I can't do this")
      return
    newside.distortion(side.normal,p) 
    return newside

def squawk(msg):
  quarkx.msgbox(msg, MT_INFORMATION, MB_OK)

  
def GlueSideClick(m):
  "glues selection or current side handle to tagged side or point"
  editor = mapeditor()
  if editor is None: return
  try:
    sides = [m.side]
  except (AttributeError):
    sides = editor.layout.explorer.sellist
  if (len(sides) < 1):
    quarkx.msgbox("Nothing to do", MT_WARNING, MB_OK)
    return
  for side in sides:
    if (side.type != ":f"):
      quarkx.msgbox("Some selected object is not a face", MT_ERROR, MB_OK)
      return
  tagged = gettagged(editor)
  if tagged is None:
    tagpt = gettaggedpt(editor)
    if tagpt is None:
      return
  else:
    tagpt = tagged.origin
  editor.invalidateviews()
  gluit = 1
  ActionString = "glue to tagged"
  undo = quarkx.action()
  for side in sides:
    fulcrum = side.origin
    try:
      fulcrum = m.fulcrum
      gluit = 0
      ActionString = "Align to tagged"
    except (AttributeError) : pass
    new=side.copy()
    if tagged is not None:
      #
      # if necessary (it usually is), flip normal of new side
      #
      if new.normal*tagged.normal < 0:
        new.distortion(-tagged.normal, fulcrum)
      else: new.distortion(tagged.normal,side.origin)
    if gluit:
      #
      # force the translation vector to be parallel to the normal vector to avoid texture translation
      #
      new.translate(new.normal * (new.normal*tagpt - new.dist))
    #
    # only do swap if it won't break any polys
    #
    if checkpolys(side, new):
       undo.exchange(side, new)
    else:
      quarkx.msgbox("That operation would trash a polyhedron", MT_ERROR, MB_OK)
      return
  editor.layout.explorer.sellist = []
  editor.ok(undo, ActionString)


#
#  maybe a candidate for quarkx?
#
def checkpolys(old, new):
  "checks that swapping new for old breaks none of old's polys"
  polys = old.faceof
  for poly in polys:
    clone = quarkx.newobj("poly:p")
    for face in poly.faces:
      if face is old:
        clone.appenditem(new.copy())
      else:
        clone.appenditem(face.copy())
    if clone.broken:
      return 0
  return 1



#
#  Set up command menus.  Maybe junk these for buttons, or only
#   use right-mouse click?
#

def commandsclick(menu, oldcommand=quarkpy.mapcommands.onclick):
  oldcommand(menu)
  editor = mapeditor()
  if editor is None: return
  selection = editor.layout.explorer.sellist
  if isoneface(selection):
    face = selection[0]
    mentagside.state = qmenu.normal
  else:
    face = None
    mentagside.state = qmenu.disabled
  tagged = gettagged(editor)
  if tagged is None:
    if gettaggedpt(editor):
      mencleartag.state = qmenu.normal
      menglueside.state = qmenu.normal
    else:
      mencleartag.state = qmenu.disabled
      menglueside.state = qmenu.disabled
    menaligntex.state = qmenu.disabled
  else:
    mencleartag.state = qmenu.normal
    menglueside.state = len(selection)==0 and qmenu.disabled
    if face is None or tagged is face:
      menaligntex.state = qmenu.disabled
    else:
      menaligntex.state = qmenu.normal
      aligntexstate(menaligntex, tagged, face)


mentagside  = qmenu.item("&Tag Side", TagSideClick, tagtext)
mencleartag = qmenu.item("&Clear Tag", ClearTagClick, "Clears tag")
menglueside = qmenu.item("&Glue to Tagged", GlueSideClick, "Moves & aligns sel. side to tagged side")
menaligntex = qmenu.item("&Wrap texture from tagged", AlignTexClick, aligntext)

quarkpy.mapcommands.items.append(qmenu.sep)   # separator
quarkpy.mapcommands.items.append(mentagside)
quarkpy.mapcommands.items.append(mencleartag)
quarkpy.mapcommands.items.append(menglueside)
quarkpy.mapcommands.items.append(menaligntex)

quarkpy.mapcommands.onclick = commandsclick

# Jan 28, 1999 - made menus constant, added flyover help