
########################################################
#
#               Side Tagging and Glueing Plugin
#                      v1.6, Aug 13, 1999
#                      works with Quark5.11
#
#
#        by tiglari@hexenworld.com, with lots of advice
#          code snippets and additions 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":          "Aug 13, 1999",
   "author":        "tiglari",
   "author e-mail": "tiglari@hexenworld.com",
   "quark":         "Version 5.11" }

import quarkx
import quarkpy.mapmenus
import quarkpy.mapentities
import quarkpy.qmenu
import quarkpy.mapeditor
import quarkpy.qbaseeditor
import quarkpy.mapcommands
import quarkpy.mapoptions
import quarkpy.qhandles
import quarkpy.mapbtns
from quarkpy.maputils import *


# A lot of recent code is piled up at the front.

#
#  snap object stuff, need to know how to get bbox info for this
#

def objectmenu(o, editor, oldmenu=quarkpy.mapentities.EntityManager.menu.im_func):
  menu = oldmenu(o,editor)
  menu [:0] = [snapobj(editor,o)]
  return menu

#quarkpy.mapentities.EntityManager.menu = objectmenu

def snapobj(editor,o):
  tagged=gettagged(editor)
  item=qmenu.item("Snap to tagged",SnapClick)
  if tagged is None:
    item.state=qmenu.disabled
  else:
    item.o = o   
  return item
  
def SnapClick(m):
   editor=mapeditor()
   if editor is None: return
   (vmin,vmax)=quarkx.boundingboxof([m.o])
   squawk("min: "+`vmin`+"  max: "+`vmax`)
   


#
#  Brush merger: brush with a face kissing the tagged side
#  is merged with that side's brush, if this is workable.   
#  This stuff doesn't work yet, so it's menu item is commented out
#

def closenough(vtx,vtx1):
  if abs(vtx-vtx1) < 1:
    return 1
  else: return 0


def same_vertices(face,tagged):
  "face and tagged must have one vtx set which must be the same"
  facevtx = face.vertices[0]
  tagvtx = tagged.vertices[0]
  length = len(facevtx)
  if length!=len(tagvtx):
    return 0
  for fvtx in facevtx:
    for tvtx in tagvtx:
      if closenough(fvtx,tvtx):
        break
    else: return 0
  return 1
  
def useableby(face, poly):
   newface=face.copy()
#
#  wtf doesn't this work?
#
#  newpoly=poly.copy()
#  newpoly.appenditem(newface)
#  newpoly.rebuildall()
#  squawk(`len(newpoly.faces)`)
   newpoly = quarkx.newobj("test:p")
   for face2 in poly.faces:
     newpoly.appenditem(face2.copy())
   newpoly.appenditem(newface)
   for face2 in newpoly.faces:
#     squawk("checking")
     if newface==face2:
       return 1
   return 0


def mergepoly(editor,o):

  def noncoplanar(poly, face):
    for f in poly.faces:
      if coplanar(f, face):
        return 0
    return 1

  item = qmenu.item("Merge Polys",MergePolyClick,"|This command can merge two brushes which `kiss' at a face, meaning that the faces have the same location, orientation, size and shape, but are oriented in opposite directions.\n\nTo use it, tag one of the kissing faces, then select the brush that contains the other.  If this menu item becomes enabled, the operation is then supposed to be able to combine the two brushes into one.  The selected brush will be `dominant', in that the resulting brush will be in its position of the group structure, and its textures and higher shared faces will be retained where relevant.\n\nIf the operation will change the overall shape, or create an invalid brush, this menu item is supposed to remain disabled.")
  item.state=qmenu.disabled
  tagged=gettagged(editor)
  if tagged is None or len(tagged.faceof)!=1:
    return item
  #
  # find a kissing face, if there is one.
  #
  for face in o.faces:
    if len(face.faceof)!=1 or not coplanar(face,tagged): continue
    if face.normal*tagged.normal>0: continue
    if same_vertices(face,tagged): 
      item.tagged=tagged
      item.face=face
      new = quarkx.newobj(o.name)
      for oldface in o.subitems: # not faces, we don't mess with shared faces
        if oldface==face or oldface.type!=":f": continue
        new.appenditem(oldface.copy())
      #
      # the merged poly will be in o's group, so we need to copy
      # all the faces.  any facees used by the tagged faces's
      # poly that are actually used by o will not be ok.
      #
      for tagface in tagged.faceof[0].faces:
        if noncoplanar(o,tagface): # ys
          new.appenditem(tagface.copy())
          #
          # If it can be added to o and still be used by
          # o, then it changes the shape of o and merger 
          # should not be enabled
          #
          editor.layout.explorer.sellist=[tagface]
#          squawk("testing useability")
          if useableby(tagface, o):  # if it can be added to
#             squawk("passed")
             return item
      item.state=qmenu.normal
      item.result = new
      item.o = o
      item.tagged = tagged
#      squawk("dunda crap")
  return item
  
def MergePolyClick(m):
 editor=mapeditor()
 if editor is None: return
 undo = quarkx.action()
 undo.exchange(m.o, m.result)
 undo.exchange(m.tagged.faceof[0], None)
 editor.ok(undo,"merge polys")


#
# projecting textures onto faces
#

def projectpointtoplane2(point,along,origin,normal):
  "standard vector, normalized vector, origin v of plane, normal v to plane"  
  p1 = origin-point
  p1 = p1*normal
  p2 = along*normal
  p3 = p1/p2
  p4 = p3*along
  return point+p4
  
def projectpointtoplane(p,n1,o2,n2):
  "project point to plane at o2 with normal n2 along normal n1"
  v1 = o2-p
  v2 = v1*n2
  v3 = n1*n2
  v4 = v2/v3
  v5 = v4*n1
  return p + v5

#  return p + (((o2-p)*n2)/n1*n2)*n1

#  return point+((origin-point)*normal/along*normal)*along
   
def projtex(editor,o):
  item=qmenu.item("Project texture",ProjTexClick,"|Projects the texture of the tagged face onto the selected ones, so that a texture can be wrapped without seams over an irregular assortment of faces.\n\nThe standard Quake-engine texture system is like this anyway, with textures being projected from xy, yz or xz planes, whichever the face is nearest to parallel with.\n\nBut this scheme gives you full control of the plane that the texture is projected from.\n\nStill has warts: seem to require that the projecting plane (which can be in a hidden brush) be big enough to `cover' the faces being projected onto.")
  tagged=gettagged(editor)
  if tagged is None or math.fabs(tagged.normal*o.normal)<.001:
    item.state=qmenu.disabled
  else:
    item.o = o
    item.tagged=tagged
  return item
  
def ProjTexClick(m):
  editor = mapeditor()
  if editor is None: return
  undo = quarkx.action()
#  squawk(`m.tagged`)
  newface = projecttexfrom(m.tagged, m.o)
  undo.exchange(m.o, newface)
  editor.ok(undo,"Project Texture")
  


def projecttexfrom(source, goal):
  (o, t1, t2) = source.threepoints(1)
  along = source.normal
  (org, t3, t4) = goal.threepoints(1)
  norm = goal.normal
  (p, pt1, pt2) = (projectpointtoplane(o,along,org,norm),
                   projectpointtoplane(t1,along,org,norm),
                   projectpointtoplane(t2,along,org,norm))
  newface=goal.copy()
  newface.setthreepoints((p,pt1,pt2),1)
  newface.texturename=source.texturename
  if newface.normal*goal.normal<0:
    newface.swapsides()
#    (p1, p2, p3) = newface.threepoints(1)
#    squawk("swapping")
#    newface.setthreepoints((p1,-p2,-p3),1)
#  else: squawk("noswap")
  return newface

#
# experiment
#

def prepareobjecttodrop(editor,obj,oldprep=quarkpy.mapbtns.prepareobjecttodrop):
  objdict=getspecdict("_tag",obj)
  mapdict=getspecdict("_tag",editor.Root)
  if not (objdict=={} or mapdict=={}):
    objkeys=objdict.keys()
    mapkeys=mapdict.keys()
    objkeys.sort()
    mapkeys.sort()
    maxkey=mapkeys[-1]
    if objkeys[-1]>maxkey:
      maxkey=objkeys[-1]
    newkey=maxkey+1
    for tag in objdict.keys():
      if mapdict.has_key(tag):
        for thing in objdict[tag]:
          thing.setint("_tag",newkey)
      newkey=newkey+1
    objkeys = getspecdict("_tag",obj)
  oldprep(editor,obj)
 
quarkpy.mapbtns.prepareobjecttodrop = prepareobjecttodrop 

def newredimage_ok(editor,undo,old,new,undomsg,oldok=quarkpy.qhandles.redimage_ok):
  codrug = codrag(editor,undo,old,new)
  if codrug >= 0:
    oldok(editor,undo,old,new,undomsg)
  if codrug==1 and menmultisellinkdrag.state==qmenu.normal:
    editor.layout.explorer.sellist=new

quarkpy.qhandles.redimage_ok = newredimage_ok


def glueface(undo, face, target, exch=1):
    "moves face to the target position"
    if exch:
      newface = face.copy()
    else:
      newface = face
    if newface.normal*target.normal < 0:
      newface.distortion(-target.normal, face.origin)
    else:
      newface.distortion(target.normal, face.origin)
    newface.translate(newface.normal * \
        (newface.normal*target.origin - newface.dist))
    if exch:
      undo.exchange(face, newface)

def checkglue(dict):
  for key in dict.keys():
      face1 = dict[key][0]
      for face in dict[key][1:]:
        if not coplanar(face,face1):
          return 0
  return 1

def gluefaces(editor,undo,movee,tagdict):
  mapdict = getspecdict("_tag",editor.Root)
  for key in tagdict.keys():
    face1 = tagdict[key][0]
    faces = mapdict[key]
    for face in faces:
      if checktree(movee,face) or coplanar(face,face1):
        continue
      glueface(undo,face,face1)
 
def number(num,sing,pl):
  if (num==1):
    return sing
  else:
    return pl

def codrag(editor,undo,old,new):
  if len(old)>1: return 0
  old = old[0]
  new = new[0]
  type = old.type
  if not (type==":f" or type==":p" or type==":b" or type==":g"):
     return 0
  dragdict=getspecdict("_tag",new)
  if dragdict=={}: return 0
  if not checkglue(dragdict):
    quarkx.msgbox("The dragged "+typenames[type]+" contains linked faces that are not coplanar.\n\nYou should fix this and then use the 'Glue linked faces' speedmenu item to align the linked faces elsewhere in the map",
       MT_WARNING,MB_OK)
    return 0
  if mengluelinkedondrag.state==qmenu.normal:
    size = len(dragdict.keys())
    response = quarkx.msgbox("This object is linked to "+`size`+number(size," other face"," other faces")+".  Do you want to drag them too?",MT_INFORMATION,MB_YES_NO_CANCEL)
    if response == MR_NO:
      return 0
    if response == MR_CANCEL:
      return -1
  gluefaces(editor,undo,old,dragdict)
  return 1

def ToggleCheck(m):
  if m.state == qmenu.checked:
    m.state = qmenu.normal
  else:
    m.state = qmenu.checked

def toggleitem(text,hint):
  item = qmenu.item(text,ToggleCheck,hint)
  return item
  
menlinkonglue=toggleitem("Link on Glue","|When this option is checked, the `Glue-to-tagged' command links the glued side to the tagged one, making it easy to keep then copanar.")
menmultisellinkdrag=toggleitem("MultiSelect on Linked Drag","|When this option is checked, when a face is dragged that is linked to others, they all become the multi-selection after the drag.\n\nWhen it is unchecked, the selection remains unchanged.")
#menmultisellinkdrag.state=qmenu.checked
mengluelinkedondrag=toggleitem("Silent Glue Linked on Drag","|When this option is checked, when something is dragged, faces linked to its faces will be dragged along too.")
#mengluelinkedondrag.state=qmenu.checked

quarkpy.mapoptions.items.append(qmenu.sep)
quarkpy.mapoptions.items.append(menmultisellinkdrag)
quarkpy.mapoptions.items.append(menlinkonglue)
quarkpy.mapoptions.items.append(mengluelinkedondrag)


#
#---------------- An important exception -------------
#

bail = 'bail'

def check_true(whatever):
  "this device is used to exit from complicated sequences of tests"
  "  when one fails"
  if not whatever:
     raise bail, 0
  return whatever

#
# ----------- Tag storing & fetching --------------
#

class Tagging:
  "a place to stick side-tagging stuff, to be attached to editor;"
  "only real purpose is to forestall name-collisions"

#
# In these fetching routines, o is expected to be an editor
#

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 gettaggedlist(o):
  "Returns a list of tagged stuff"
  try:
    return o.tagging.taglist
  except (AttributeError): return None

def anytag(o):
  "Is anything tagged ?"
  return gettagged(o) is not None or gettaggedpt(o) is not None or gettaggedlist(o) is not None


#
# --------------- Some Geometry Utilities ---------
#

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"
  "returns list of (el,ind) tuples, where el is from l1,"
  "and ind is its index"
  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:
    return []
  if pozzies[0]==0 and pozzies[1]>1:
    intx.reverse()
  return intx
    
def abutting_vtx2(l1, l2):
  "gets the 2 vtx of l1 that are on the same line as 2 of l2, which are"
  "supposed to be vertex-cyles of abutting faces"
  "returns [i1, i2] list, index positions in l1, in cycle-order"
  "and ind is its index"
  (len1, len2) = (len(l1), len(l2))
  i1 = 0
  for v1 in l1:
    i = i1
    i1 = cyclenext(i1, len1)
    v1n = l1[i1]
    i2 = 0
    for v2 in l2:
      i2 = cyclenext(i2, len2)
      v2n = l2[i2]
      if colinear([v1, v1n, v2, v2n]):
        return [i, i1]
  return []
    
def colinear(list):
 if len(list) < 3:
   return 1
 line = (list[1]-list[0]).normalized
 v0 = list[0]
 for v in list[2:]:
   if v0 - v:
     if not samelinenorm(line, (v0-v).normalized):
       return 0
 return 1

def samelinenorm(v1, v2):
  "v1 and v2 must be normalized"
  if math.fabs(v1* v2) == 1:
    return 1
  else:
    return 0

def abutting_face_vtx(vtxes1, vtxes2):
  "gets the 2 vertices shared by abutting faces, specified as lists"
  "of vertex-cyles, returns (vi1, vi2, ci), indN the indexes of"
  "vertices and then the cycle, in vtxes1"
  index = 0
  for vtx1 in vtxes1:
    for vtx2 in vtxes2:
      result = abutting_vtx2(vtx1, vtx2)
      if result:
        result.append(index)
        return result
    index = index+1
  return []
      
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 cyclenext(i, len):
  j = i+1
  if j == len:
    return 0
  else:
    return j
    
def perptonormthru(source, dest, normthru):
  "the line from source to dest that is perpendicular to (normalized) normthru"
  diff = source-dest
  dot = diff*normthru
  return diff - dot*normthru
  
def oppositeedge(vtx, i, b):
  "returns (op1, op2, width) tuple, or 0, indexes of opposite vertices"
  "in cycle order, and a vector from first to the opposite edge's line."
  "i is the index of first vertex, b the normalized separation vector"
  "separation vector is second vertex minus first, in cycle-order"
  length = len(vtx)
#  squawk("i: "+`i`+"vtxes: "+`vtx`)
  first = cyclenext(i, length)
  start = vtx[i]
  while (first != i):
    second = cyclenext(first, length)
#    squawk("trying "+`vtx[first]`+":"+`vtx[second]`)
    sep = (vtx[second]-vtx[first]).normalized
#    squawk("normals: "+`b`+":"+`sep`)
    if not (sep + b):      # note that signs are opposite cuz of cycling
      return (first, second, perptonormthru(start,vtx[first],b))
    first = second
  return 0

  
def oppositeedge_vtxes(vtxes, abuttment):
  "does the work of oppositeedge, but taking a list of vertex-cycles"
  "and the output of abutting_face_vtx as inputs"
  cycle = vtxes[abuttment[2]]
  separ = (cycle[abuttment[1]]-cycle[abuttment[0]]).normalized
  return oppositeedge(cycle,abuttment[1],separ)
     

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

#
# ----------- map object utilities -------------------
#

def checktree(root, obj):
  "makes sure obj is still in root, not swapped out"
  while obj is not root:
    t = obj.parent
    if t is None or not (obj in t.subitems):
      return 0
    obj = t
  return 1

#
# ----------- menu-management utilties ----------------
#

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



#
# ---------  misc utilities -----------
#

def squawk(msg):
  quarkx.msgbox(msg, MT_INFORMATION, MB_OK)

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]


#
# -------- map drawing routines
#

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 drawredface(view, cv, face):
    for vtx in face.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)

def coplanar(f1, f2):
  (p1, p2, p3) = f1.threepoints(0)
  (q1, q2, q3) = f2.threepoints(0)
  n = f1.normal
  if n*(q1-p1) == 0:
    return 1
  return 0
  
def drawlinks(editor, view):
  "separated linked faces drawn dotted, in blue if selected otherwise red"
  dict = getspecdict("_tag", editor.Root)
  if dict is None: return
  keys = dict.keys()
#  quarkx.msgbox(`keys`,MT_INFORMATION,MB_OK)
  for key in keys:
    faces = dict[key]
    planes = [faces[0]]
    for face in faces[1:len(faces)]:
      for plane in planes:
        if coplanar(face,plane):
          break;
      else:
        planes.append(face)
    if len(planes)>1:
      cv = view.canvas()
      sellist = editor.layout.explorer.sellist
      if len(sellist) == 1 and sellist[0] in planes:
        cv.pencolor = MapColor("Duplicator")
      else:
        cv.pencolor = MapColor("Tag")
      cv.penstyle = PS_DOT
      #
      #  something's screwy here
      #
      for face in faces:
        drawredface(view,cv,face)
        
def tagfinishdrawing(editor, view, oldmore=quarkpy.qbaseeditor.BaseEditor.finishdrawing):
  "the new finishdrawning routine"
  #
  # first of all call overridden routine
  #
  oldmore(editor, view)

  #
  # first draw in colored dotted lines any sides
  #   have gotten separated.
  #
  drawlinks(editor, view)
  #
  # now on with the real show
  #
  oldmore(editor, view)
  tagged = gettagged(editor)
  if tagged is None:
    tagpt = gettaggedpt(editor)
    if tagpt is None:
      taglist = gettaggedlist(editor)
      if taglist is None:
         return
  cv = view.canvas()
  cv.pencolor = MapColor("Tag")
  if not tagged is None:
    if not checktree(editor.Root,tagged):
      ClearTagClick(None)
      return
    drawredface(view, cv, tagged)
  elif not tagpt is None:
    drawsquare(cv, view.proj(tagpt), 8)
  elif not taglist is None:
    for face in taglist:
      if not checktree(editor.Root,face):
        ClearTagClick(None)
        return
    for face in taglist:
      drawredface(view, cv, face)

quarkpy.qbaseeditor.BaseEditor.finishdrawing = tagfinishdrawing


# -----------------------------------------------------
# ------------- tagging menu items --------------------
# -----------------------------------------------------

#
# ----------- tag & clear side & point  --------------------
#


def TagSideClick (m):
  "tags a side on mouse-click"
  editor = mapeditor()
  if editor is None: return
  editor.tagging = Tagging()
  editor.tagging.tagged = sideof(m, editor)
  editor.invalidateviews()


def TagPointClick (m):
  "tags a single point"
  editor = mapeditor()
  if editor is None: return
  editor.tagging = Tagging()
  editor.tagging.tagpt = m.pos
  mapeditor().invalidateviews()         # redraw the map

def ClearTagClick (m):
  "clears tag on menu-click"
  editor = mapeditor()
  if editor is None: return
  try:
    del editor.tagging
  except AttributeError:
    return
  editor.invalidateviews()


#
# ------------- glueing & aligning sides & points ----------------
#


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))
  undo.exchange(side, new)
  editor.layout.explorer.sellist = []
  editor.ok(undo, ActionString)
  if gluit and menlinkonglue.state==qmenu.checked:
    if quarkx.msgbox("Link glued face?",MT_CONFIRMATION,MB_YES|MB_NO)==MR_YES:
      m.side=new
      LinkFaceClick(m,0)

#
# -------------- Managing Tagged List --------------
#


def removefromtaggedstate(menuitem, editor, o):
  "in charge of disabling remove from tagged menu item"
  tagged = gettagged(editor)
  if not tagged is None:
    if tagged == o:
       return
  else:
    taglist = gettaggedlist(editor)
    if not taglist is None:
      count = taglist.count(o)
      if count:
        return
  menuitem.state = qmenu.disabled

def AddtoTaggedClick(m):
  "expects the selected face to be attached to m as .side"
  editor = mapeditor()
  if editor is None: return
  
  tagged = gettagged(editor)
  if not tagged is None:
    editor.tagging = Tagging()
    editor.tagging.taglist = [tagged, m.side]
  else:
    taglist = gettaggedlist(editor)
    if not taglist is None:
      taglist.append(m.side)
  editor.invalidateviews()
 
def RemovefromTaggedClick(m):
  "expects the selected face to be attached to m as .side"
  "does no checks, expects enabling code to work properly"
  editor = mapeditor()
  if editor is None: return
  
  tagged = gettagged(editor)
  if not tagged is None:
    ClearTagClick(None)
  else:
    taglist = gettaggedlist(editor)
    if not taglist is None:
      taglist.remove(m.side)
  editor.invalidateviews()

def SelectTaggedClick(m):
  "expects .taglist to be attached to m"
  editor = mapeditor()
  if editor is None:
    return
  ClearTagClick(None)
  editor.layout.explorer.sellist = m.taglist


#
# ------ Wrapping texture from one face to another
#  (no fitting, this one also works in lots of cases where it's
#   probably useless, such as faces that don't abutt, but whose
#   planes intersect)
#

def aligntexstate(aligntex, tagged, o):
  "sorts out what kind of abuttment, if any for tagged side and this one"
  if tagged is None or o is tagged:
     aligntex.state = qmenu.disabled
     return
  if coplanar_adjacent_sides(tagged, o):
    aligntex.abuttype = 0
  elif intersecting_sides(tagged, o):
    aligntex.abuttype = 1
  else:
    aligntex.state = qmenu.disabled


def wraptex(orig, side):
    "actually wraps the texture from orig to side"
    "assumes aligntexstate has checked the preconditions"
    newside = side.copy()
#    copytags(side, newside) # transfers specifics beginning with _
    (o, t1, t2) = orig.threepoints(0)
    (r, s1, s2) = side.threepoints(0)
    n = side.normal
    newside.texturename = orig.texturename
    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.setthreepoints((o, t1, t2,), 0)
    newside.distortion(side.normal,p) 
    return newside

def wraptex2(orig, side):
    "actually wraps the texture from orig to side"
    "assumes aligntexstate has checked the preconditions"
    newside = orig.copy()
    copytags(side, newside) # transfers specifics beginning with _
    (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 AlignTexClick(m):
  "wraps texture from tagged to selected side"
  "uses wraptex to do the real work"
  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:
  #
  #   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
  #   done by wraptex), and then swap it in for the selected side
  #  
    newside = wraptex(tagged, side)
  else:
    newside = tagged.copy()
  undo.exchange(side, newside)
  editor.ok(undo, ActionString)
  if checkshifttagged.state == qmenu.checked:
    m.side = newside
    TagSideClick(m)


#
# ---------- wrapping & fitting texture around pillar
#

def pillarwrapdisabler(menuitem, tagged, o):
  "figures out if wrapping can procede from tagged to selected and"
  "so-on around poly, attaching useful info to m on successful return"
  #
  # the faces belong to polys
  #
  selpolys = o.faceof
  tagpolys = tagged.faceof
  #
  # using a defined exception as a bailout go-to
  #
  bail = 'bail'
  try:
    if tagpolys[0].type != ":p" or selpolys[0].type != ":p":
      raise bail, 0
    #
    # and indeed to the same poly
    #
    thepolys = intersection(tagpolys, selpolys)
    if thepolys == []:
      raise bail, 0
    thepoly = thepolys[0]  # I hope this assumption isn't unsound
    #
    # and indeed that these faces furthermore abutt
    #
    ovx = o.verticesof(thepoly)
    tvx = tagged.verticesof(thepoly)
    #
    # 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(ovx, tvx)
    if not shared:
      raise bail, 0
    sep = (shared[1][0]-shared[0][0]).normalized
    result = oppositeedge(ovx, shared[0][1], sep)
    if not result:
      raise bail, 0
    (first, second, gap) = result
    #
    # Now we embark on a voyage of discovery around the poly,
    # looking for a face that shares 2 vertices with our selected
    # face, such that these 2 vertices make an edge paralell to sep,
    # and so on around, till we get back to the originally selected
    # tagged face.  If at any prior time we fail, we bail, but if
    # we emerge successful, we attach to the menu-item a list of faces
    # for wrapping, the length of the path around, and the `gap' vector
    # of the tagged side (for figuring out how to rescale the texture)
    #
    polyfaces = thepoly.faces
#    squawk(`len(polyfaces)`)
    polyfaces.remove(tagged)
    polyfaces.remove(o)
#    squawk(`len(polyfaces)`)
    facelist = [tagged, o]
    circuitlength = abs(gap)
    ovtxes = [ovx[first], ovx[second]]
    def findnextface(polyfaces, thepoly, ovtxes):
        "defined here to reduce variable binding"
        for face in polyfaces:
          vtxes = face.verticesof(thepoly)
          int = abutting_vtx(vtxes, ovtxes)
          if len(int) == 2:
            polyfaces.remove(face)
            return (face, vtxes, int)
        return 0
    while 1:
      facial = findnextface(polyfaces, thepoly, ovtxes)
      #
      #  mebbe we found a next face
      #
      if facial:
         (face, nvtxes, shared) = facial
         result = oppositeedge(nvtxes, shared[0][1], sep)
         if not result:
           raise bail, 0
         (first, second, gap) = result
         facelist.append(face)
         circuitlength = circuitlength + abs(gap)
         ovtxes = [nvtxes[first], nvtxes[second]]     
         continue
      #
      # but if we didn't, mebbe that's cuz we're all the way around!
      #
      else:
        int = abutting_vtx(tvx, ovtxes)
        if len(int) == 2:
          result = oppositeedge(tvx, int[0][1], sep)
          if result:
            (first, second, gap) = result
            circuitlength = circuitlength + abs(gap)
            menuitem.wraplist = facelist
            menuitem.circuit = circuitlength
            menuitem.horiz = gap
#            squawk("faces: "+`len(facelist)`)
            return
      raise bail, 0
  #
  # and the final bailout go-to
  #

  except bail, dummy:
    menuitem.state = qmenu.disabled
    return 0
  
def PillarWrapClick(m):
  "expects m to have fields:"
  "  .wraplist - sequential list of faces for wraparound"
  "  .circuit - total circuitlength"
  "  .horiz - vector from one side of tagged face to the other"
  "all preconditions should be satisfied if menuitem enabled"
  
  (faces, circuit, gap) = (m.wraplist, m.circuit, m.horiz)
  editor = mapeditor()
  firstface = faces[0]
  #
  # First figure out how to scale the texture
  #
  txsrc=editor.TexSource
  tp = firstface.threepoints(2, txsrc)
  gapn = gap.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*gapn), math.fabs((tp[2]-tp[0]).normalized*gapn))
  if proj[0] > proj[1]:
    primaxis = 0
  else:
    primaxis = 1
  projlength = axlengths[primaxis]*proj[primaxis]
  times = circuit/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 = circuit/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 checkaspectratio.state == qmenu.checked:
#    squawk("preserving aspect")
    newaxes[secaxis] = tp[0]+stretched[secaxis]
#  newaxes = (tp[0]+stretched[0], tp[0]+stretched[1])
  newface = firstface.copy()
  newface.setthreepoints((tp[0], newaxes[0], newaxes[1]),2,txsrc)
  ActionString = "pillar wrap"
  undo = quarkx.action()
  #
  # swap in the resized texture  
  #
  undo.exchange(firstface, newface)
  startface = newface
  #
  # and now at last for the big wrap
  #
  faces.remove(firstface)
  for face in faces:
    currface = newface  
    newface = wraptex(currface, face)
    undo.exchange(face, newface)
  editor.ok(undo, ActionString)
  editor.layout.explorer.sellist = [startface]

#
#  --- wrapping and fitting across chain of tagged sides
#

def wraptaggedstate(menuitem, o):
   "figures out if wrap & fit can happen across the tagged list of sides,"
   "starting from the current one, and proceding either from edge, or if"
   "the current one is in a cycle, in the direction the list was formed."
   "attaching useful info to menuitem on successful return, including"
   "list of faces in wrap order, & total length to fit"

   editor = mapeditor()
   if editor is None:
      return
   taglist = gettaggedlist(editor)
#   bail = 'bail'
   try:  # raise the bail exception to quit the sequence tests
     if taglist is None:
        raise bail, 0
     #
     # the current side must be in the tagged list
     #  (actaully, on an end or in a cycle!) 
     #
     taglist = taglist[:]      #make a copy
     if taglist.count(o) == 0:
        raise bail, 0
     pozzie = taglist.index(o)
     #
     #  Figure out which direction to go in building the wrap list.
     #  At an edge, go in the only possible direction.  In the middle,
     #  it's only possible if there's a circuit.  In this case, go in
     #  the direction of the abutting (tagged) face with the higher
     #  index, or with a zero index, or who cares.  If you can't use
     #  everything on the taglist, then the menu-item shouldn't get
     #  enabled.
     #
     #  So first find the face(s) that abutt with o.
     #
     tagind = taglist.index(o) # we'll need this shortly
     taglist.remove(o)
     ovtxes = o.vertices  # this is a vertex list, no info about polys
     faces = []
     abuttments = []
     for face in taglist:
        abuttment = abutting_face_vtx(ovtxes, face.vertices)
#        squawk("abuttment = "+`abuttment`)
        #
        # If successful, abuttment[0] will be a list [v1i, v2i, ci]
        #  the indexes from ovtexes that are shared with face.vertices,
        #  ci the index of vertex-cycle they're from
        #
        if abuttment:
          faces.append(face)
          abuttments.append(abuttment)
     if len(faces) > 2 or len(faces) == 0:
       raise bail, 0  #  wrong number, no proper abuttment
     if len(faces) == 2:
       #
       # Now we pick one, hopefully one that was tagged after o
       #
       (ind1, ind2) = (taglist.index(faces[0]), taglist.index(faces[1]))
#       squawk("index: "+`index`)
       if ind1 >= tagind:
         index = 0
       elif ind2 >= tagind:
         index = 1
       elif ind1 == 0:
         index = 0
       else:
         index = 1
     else:
       index = 0
     nextface = faces[index]
     (vi1, vi2, ci) = abuttments[index]
     #
     # Now we check that both o and nextface have an edge parellel
     #  to their shared edge, and get the distance between these
     #  edges.
     #
#    squawk("banzai")     
     vertical = (ovtxes[ci][vi2]-ovtxes[ci][vi1]).normalized
     (opi1, opi2, width) =  check_true(oppositeedge(ovtxes[ci], vi1, vertical)) 
     #
     # record the `horizontal' vector going from one side of the face
     #  to the other
     #
     horiz = width
     #
     # Now find out how to shift the origin of the texture threepoints
     #
     (p0, p1, p2) = o.threepoints(2)
     texorigshift = perptonormthru(p0, ovtxes[ci][vi1], vertical)
     #
#     squawk("passed 1")
     #
     # annoying to have to do this again, but such is life
     #   or perhaps I'm a moron
     #
     circuit = abs(width)
     current = faces[index]
     nvtxes = current.vertices
     (vi1, vi2, ci) = abutting_face_vtx(nvtxes, ovtxes)
#     squawk("passed 2")
     #
     # Note the sign-switch, cuz oppositeedge is very lazy
     #
     vertical = -vertical
     (opi1, opi2, width) = check_true(oppositeedge(nvtxes[ci], vi1, vertical))
#     squawk("passed 3")
     circuit = circuit + abs(width)   
     taglist.remove(current)
     wraplist = [o, current]
     #
     # Now repeat finding one face that extends the wall we've got
     #   so far. If every face gets used, sucess, otherwise bail.
     #
     while taglist:
#       squawk("taglen: "+`len(taglist)`)
       for face in taglist[:]:
         abuttment = abutting_face_vtx(face.vertices, nvtxes)
         if abuttment:
#           squawk("abutted")
           nvtxes = face.vertices
           (vi1, vi2, ci) = abuttment
           (opi1, opi2, width) = check_true(oppositeedge(nvtxes[ci], vi1, vertical))
#           squawk("checked out")
           circuit = circuit + abs(width)
           taglist.remove(face)
           wraplist.append(face)
           break
       #
       # OK we've looked at every that's left, & none of them
       #  latch onto what we've got, so bail
       #
       else:
         raise bail, 0    
#     squawk("length: "+`len(wraplist)`)
     menuitem.wraplist = wraplist
     menuitem.circuit = circuit
     menuitem.horiz = horiz
     menuitem.texorigshift = texorigshift
   except bail, dummy:
     menuitem.state = qmenu.disabled
     return


def nextface(v1, v2, faces):
       "test routine"
       for face in faces:
         vtxes = face.vertices
         vxi=0
         for vtx in vtxes:
           vi = 0
           for v in vtx:
             if not v-v1:
               vni = cyclenext(vi, len(vtx))
               if not vtx[vni]-v2:
                 return (vi, vni,vxi, face)
             vi = vi+1
           xvi = vxi+1
         squawk("bailing")
         return None

def TaggedWrapClick(m):
  side = m.wraplist[0]
  (p1, p2, p3) = side.threepoints(2)
  shift = m.texorigshift
  side.setthreepoints((p1+shift, p2+shift, p3+shift), 2)
  PillarWrapClick(m)


# -----------------------------------------------------
# --------------  Set up Menus  -----------------------
#
#  (no real organization here yet!)
#
# Right Mouse menus are built `on the click' so that their items can
#  contain info about the object they are clicked over, and also
#  info prepared by their disablers (which do quite a lot of the work
#  for the texture-fitting operations)
#

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 from tagged, around pillar in direction of selected, scaling to eliminate seams.\n\nWon't work if the edges to be wrapped around aren't all paralell, and scales texture minimally to fit.  `preserve aspect ration' option controls whether one or both texture dimensions are scaled.\n\n  Beta Version"

aspecttext = "|If checked, aspect ratio of textures is preserved when texture is scaled wrapping around multiple sides (pillar and multi-wrap).\n\n Click to toggle check."
checkaspectratio = qmenu.item("Preserve aspect ratio", ToggleCheck, aspecttext)

selecttaggedtext = "Tagged items become multi-selection"

shifttagtext = "|If checked, selected side becomes tagged side after `wrap texture from tagged' operation.\n\n  Click to toggle check."
checkshifttagged = qmenu.item("Shift tagged to selected", ToggleCheck, shifttagtext)

def tagpopup(editor, o):
  addtotagged = gluemenuitem("&Add to tagged", AddtoTaggedClick, o, "Adds side to tagged list")
  removefromtagged = gluemenuitem("&Remove from tagged", RemovefromTaggedClick, o, "Takes side off tagged list")
  removefromtaggedstate(removefromtagged, editor,o)
  selecttagged = qmenu.item("&Select tagged list", SelectTaggedClick,selecttaggedtext)
  if gettaggedlist(editor) is None:
    selecttagged.state = qmenu.disabled
  else:
    selecttagged.taglist = gettaggedlist(editor)
  if gettaggedlist(editor) is None and gettagged(editor) is None:
        addtotagged.state = qmenu.disabled
  
  list = [addtotagged,
          removefromtagged,
          selecttagged,
          mencleartag]
  return qmenu.popup("&More Tagging", list, None, "More commands for managing tags")

wrappoptext = "More commands and options"
wraptaggedtext = "|Wraps texture from selected side, which is in a chain of tagged sides, across the rest of the chain, scaling the texture to eliminate seams.\n\nIf side is flanked by two on the tagged list, wrapping goes in direction of whichever was first pushed onto the tagged list.\n\nSomewhat limited, since it requires each side to share two vertices with the next, and tends to fail for complicated multi-brush wraps, probably because tests for vertex-sharing are unduly stringent.  I will be working on making them less restrictive."

def wrappopup(o, tagged):
  editor = mapeditor()
  aligntex = gluemenuitem("Wrap texture &from tagged", AlignTexClick, o, aligntext)
  pillarwrap = gluemenuitem("Wrap texture &around pillar", PillarWrapClick, o, wraptext)
  wraptagged = gluemenuitem("&Fit texture across tagged", TaggedWrapClick, o, wraptaggedtext)
  if tagged != None:
    pillarwrapdisabler(pillarwrap, tagged, o)
  else:
    pillarwrap.state = qmenu.disabled
  aligntexstate(aligntex, tagged, o)
  wraptaggedstate(wraptagged, o)
  list = [aligntex,
          projtex(editor,o),
          pillarwrap,
          wraptagged,
          qmenu.sep,
          checkaspectratio,
          checkshifttagged
         ]
  return qmenu.popup("Texture &Wrapping", list, None, wrappoptext)


def getspecdict(spec, root):
  faces = root.findallsubitems("",":f")
  dict = {}
  for face in faces:
    val = face.getint(spec)
    if val!=0:
#      squawk("key "+`val`)
      if dict.has_key(val):
        dict[val].append(face)
      else:
        dict[val] = [face]
  return dict

def findfreetag(dict):
  keys = dict.keys()
#  squawk(`keys`)
  keys.sort()
  i = 1
  for j in keys:
    if i != j:
      return i;
    i = i+1
  return i;

def LinkFaceClick(m, glue=1):
  editor = mapeditor()
  if editor is None: return
  tagged = gettagged(editor)
  tag = tagged.getint("_tag")
  undo = quarkx.action()
  if tag == 0:
    dict = getspecdict("_tag",editor.Root)
#  squawk(`dict`)
    tag = findfreetag(dict)
#    squawk(`tag`)
    newtagged = tagged.copy()
    newtagged.setint("_tag",tag)
#    squawk("new: "+`newtagged.getint("_tag")`)
    undo.exchange(tagged, newtagged)
    editor.tagging.tagged = newtagged    
  newside = m.side.copy()
  newside.setint("_tag",tag)
  oldtag = m.side.getint("_tag")
  if oldtag:
    if quarkx.msgbox("There are faces already linked to this one.\nDo you want to link them to the tagged face also?",
      MT_INFORMATION, MB_YES | MB_NO) == MR_YES:
      faces = dict[oldtag]
      for face in faces:
        if face == m.side:
          continue
        newface = face.copy()
        newface.setint("_tag",tag)
#        if glue: glueface(undo,newface,tagged,0)
        undo.exchange(face,newface)
#  if glue: glueface(undo, newside, tagged, 0)
  undo.exchange(m.side, newside)
  editor.ok(undo, "Link Sides")
  editor.layout.explorer.sellist = [newside]

def gluelinked(o):
  tag = o.getint("_tag")
  item =  qmenu.item("&Glue linked",GlueLinkedClick,"Glue linked items to this")
  if tag == 0:
    item.state = qmenu.disabled
  else:
     item.tag = tag
     item.object = o
  return item

def GlueLinkedClick(m):
#  squawk(`m.tag`)
  editor = mapeditor()
  dict = getspecdict("_tag",editor.Root)
  faces = dict[m.tag]
  glueto = m.object
  undo = quarkx.action()
  for face in faces:
    if face.getint("_tag") == 0 or face is glueto:
      continue
    glueface(undo, face, glueto)
  editor.ok(undo,"Glue Linked Faces")
  editor.layout.explorer.sellist = [glueto]

def UnlinkFaceClick(m):
  editor = mapeditor()
  if editor is None: return
  undo = quarkx.action()
  dict = getspecdict("_tag",editor.Root)
  cofaces = dict[m.tag]
  if len(cofaces) == 2:
    (face0, face1) = (cofaces[0].copy(), cofaces[1].copy())
    face0["_tag"] = face1["_tag"] = ""
    undo.exchange(cofaces[0], face0)
    undo.exchange(cofaces[1], face1) 
  else:
    face = m.object.copy()
    face["_tag"] = ""
    undo.exchange(m.object, face)
  editor.ok(undo,"Unlink Face")  
  editor.layout.sellist = [m.object]

def UnlinkAllClick(m):
  editor = mapeditor()
  if editor is None: return
  undo = quarkx.action()
  dict = getspecdict("_tag",editor.Root)
  cofaces = dict[m.tag]
  for face in cofaces:
    newface = face.copy()
    newface["_tag"] = ""
    undo.exchange(face, newface)
  editor.ok(undo,"Unlink All")  
  editor.layout.sellist = [m.object]
  
def selectmenuitem(o,text,command,help):
  tag = o.getint("_tag")
  item =  qmenu.item(text,command,help)
  if tag == 0:
    item.state = qmenu.disabled
  else:
     item.tag = tag
     item.object = o
  return item

def SelectClick(m):
  editor = mapeditor()
  if editor is None: return
  dict = getspecdict("_tag",editor.Root)
  cofaces = dict[m.tag]
  list = []
  for face in cofaces:
    list.append(face)
  editor.layout.explorer.sellist = list

def isfaces(list):
  if len(list) >1 and list[0].type == ':f':
    return 1
  else:
    return 0

def LinkSelClick(m):
  editor = mapeditor()
  if editor is None: return
  dict = getspecdict("_tag",editor.Root)
  tag =  findfreetag(dict)
  #
  # disabler is supposed to assure that faces is made of faces
  #
  faces = mapeditor().layout.explorer.sellist
  for face in faces:
    face.setint("_tag",tag)

def breaksharedface(e,o):
  faceitem = qmenu.item("&Break shared face",BreakFaceClick,"|Breaks a shared face into independent faces that are linked. (So that for example they can have different textures, or the same texture differently aligned)")
  faceitem.object = o
  faceitem.editor = e
  faceitem.polys = o.faceof
  if len(faceitem.polys) < 2:
    faceitem.state = qmenu.disabled
  return faceitem
  
def BreakFaceClick(m):
  undo = quarkx.action()
  polys = m.polys
  face = m.object
  editor = m.editor
  dict = getspecdict("_tag",editor.Root)
  tag = findfreetag(dict)
  for poly in polys:
    newpoly = poly.copy()
    newface = face.copy()
    newpoly.appenditem(newface)
    newface.setint("_tag",tag)
    undo.exchange(poly, newpoly)
  undo.exchange(face, None)
  editor.ok(undo, "Break Shared Face")
  editor.layout.explorer.sellist = []


#
#  --------------------  face menu ------------------------
#  stash the old function in the new function's last parameter
#  the disabling + menu logic here has completely lost the plot ...
#

linktext = "|Linking is a device for making it easier to keep certain sets of faces coplanar, such as the components of a chair or a window-frame, or a floor and the bottom of something that's supposed to sit on it.\n\nTo link two faces, tag one, and then select `Link face to tagged' from the Linking submenu for the face you want to link to it.\n\nFurther commands from the Linking submenu then become enabled, for gluing to a side others that are linked to it, unlinking, etc.\n\nSince linked faces are supposed to be coplanar, if two aren't then they are both drawn in dotted red."

def tagmenu(o, editor, oldfacemenu = quarkpy.mapentities.FaceType.menu.im_func):
  "the new right-mouse for sides"
  menu = oldfacemenu(o, editor)
  tagged = gettagged(editor)
  glueitem = gluemenuitem("&Glue to tagged", GlueSideClick, o, gluetext)
  linktotagged = gluemenuitem("&Link face to tagged", LinkFaceClick, o, "|Links face to tagged for the `glue to linked' command.\n\nNormally the `Glue to tagged' command with the `link on glue' option set should be used instead of this command, because this command doesn't move the face to what it gets linked to, and so doesn't test for broken polys.")
#  aligntex = gluemenuitem("&Wrap texture from tagged", AlignTexClick, o, aligntext)
  tagpop = tagpopup(editor, o)
  wrappop = wrappopup(o, tagged)
  linkpopup = qmenu.popup("&Linking",
     [      selectmenuitem(o,"&Glue linked",GlueLinkedClick,"|Glue to this face all faces that are linked to it.\n\n (This one stays still; the others move.)\n\nSince linked sides are supposed to be coplanar, if they aren't they're drawn in dotted red lines."),
      selectmenuitem(o,"&Select linked faces",SelectClick,"Select all faces linked to this face.\n\nSo that you can for example select all the faces linked this one, and drag or shear them as a multiselection, rather than first move one and then glue the others to it."),
      selectmenuitem(o,"&Unlink face",UnlinkFaceClick,"|Unlink this face from the ones it's linked to"),
      selectmenuitem(o,"Unlink &all",UnlinkAllClick,"|Unlink all the faces linked to this from each other"),
      linktotagged
     ], None, linktext)
  if o is tagged:
    glueitem.state = qmenu.disabled
    linktotagged.state = qmenu.disabled
    wrappop.state = qmenu.disabled
    tagpop.state = qmenu.disabled
    if o.getint("_tag") == 0:
      linkpopup.state = qmenu.disabled
  if tagged is None:
      if o.getint("_tag") == 0:
        linkpopup.state = qmenu.disabled
      if gettaggedpt(editor) is None:
        glueitem.state = qmenu.disabled
      else:
        glueitem.hint = gluepttext
      linktotagged.state = qmenu.disabled
      if not gettaggedlist(editor):
        wrappop.state = qmenu.disabled
        tagpop.state = qmenu.disabled
  menu[:0] = [#extendtolinked(editor, o),
              gluemenuitem("Tag &side",TagSideClick,o,tagtext),
#              addtotagged,
              glueitem,
              tagpop,
              linkpopup,
#              aligntex,
              wrappop,
              breaksharedface(editor,o),
              qmenu.sep]
  return menu


#
# now bung in the new one.
#
quarkpy.mapentities.FaceType.menu = tagmenu


#
#  Ditto for the right-mouse-button menu for vertices
#

def disttotagged(editor,pos):
  pt = gettaggedpt(editor)
  item = qmenu.item("Distance to tagged",DistTaggedClick,"|Distance from here to the tagged point")
  if pt is None:
    item.state=qmenu.disabled
  else:
    item.dist=abs(pt-pos)
  return item

def DistTaggedClick(m):
  quarkx.msgbox("Distance = "+`m.dist`+" units",
    MT_INFORMATION,MB_OK)

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 face 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),
               disttotagged(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

#
#  menus for entities containing faces.
#


def doestaggedcutpoly(tagged, o, cutpoly):
  cutter1 = tagged.copy()
  piece1 = o.copy()
  piece1.appenditem(cutter1)
  if piece1.broken:
    cutpoly.state=qmenu.disabled
    return
  piece2 = o.copy()
  cutter2 = tagged.copy()
  cutter2.swapsides()
  piece2.appenditem(cutter2)
  if piece2.broken:
    cutpoly.state=qmenu.disabled
    return
  cutpoly.pieces = (piece1,piece2)
  cutpoly.o = o

def CutPolyClick(m):
  editor = mapeditor()
  if editor is None: return
  newgroup = quarkx.newobj("pieces:g")
  newgroup.appenditem(m.pieces[0])
  newgroup.appenditem(m.pieces[1])
  undo = quarkx.action()
  undo.exchange(m.o, newgroup)
  editor.ok(undo,"Cut poly along tagged")
  

def GluePolyClick(m):
  editor=mapeditor()
  if editor is None: return
  tagdict = m.dict
  if not checkglue(tagdict):
     quarkx.msgbox("The selection contains linked faces that aren't coplanar.  Fix this and then try again",MT_WARNING,MB_OK)
     return
  undo = quarkx.action()
  mapdict = getspecdict("_tag",editor.Root)
  gluefaces(editor,undo,m.o,tagdict)
  editor.ok(undo,"Glue linked faces")    
  editor.layout.explorer.sellist=[m.o]

typenames = {
  ":p" : "poly",
  ":b" : "entity",
  ":g" : "group"
}


def gluepoly(editor, o):
  typename = typenames[o.type]
  item = qmenu.item("Glue linked faces",GluePolyClick,"|Faces that are linked to faces in this "+typename+" get glued to them")
  tagdict = getspecdict("_tag",o)
  if tagdict == {}:
    item.state=qmenu.disabled
    return item
  item.dict = tagdict
  item.o = o
  return item

def tagpolymenu(o, editor, oldmenu=quarkpy.mapentities.PolyhedronType.menu.im_func):
  "the new right-mouse menu for polys"
  menu = oldmenu(o, editor)
  cutpoly = qmenu.item("Cut poly along tagged",CutPolyClick,"|Cuts this poly along the plane of the tagged face.")
  tagged = gettagged(editor)
  if tagged is None:
    cutpoly.state = qmenu.disabled
  else:
    doestaggedcutpoly(tagged,o,cutpoly)
  menu[:0] = [gluepoly(editor,o),
              mergepoly(editor,o), # now in maptagsidemerge
              cutpoly,
              qmenu.sep]
  return menu
  
quarkpy.mapentities.PolyhedronType.menu = tagpolymenu

def taggroupmenu(o, editor,oldmenu=quarkpy.mapentities.GroupType.menu.im_func):
  "the new right-mouse menu for groups"
  menu = oldmenu(o, editor)
  menu[:0] = [gluepoly(editor,o),
              qmenu.sep]
  return menu
  
quarkpy.mapentities.GroupType.menu = taggroupmenu

def tagbrushmenu(o, editor,oldmenu=quarkpy.mapentities.BrushEntityType.menu.im_func):
  "the new right-mouse menu for groups"
  menu = oldmenu(o, editor)
  menu[:0] = [gluepoly(editor,o),
              qmenu.sep]
  return menu
  
quarkpy.mapentities.BrushEntityType.menu = tagbrushmenu



#
#  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 isfaces(selection):
     menlinksel.state = qmenu.normal
  else:
     menlinksel.state = qmenu.disabled
  if isoneface(selection):
    face = selection[0]
    mentagside.state = qmenu.normal
    tag =face.getint("_tag")
#
# This below overloads the menu, I think.
#    if tag==0:
#      mengluelinked.state = qmenu.disabled
#    else:
#      mengluelinked.state = qmenu.normal
#      mengluelinked.object = face
#      mengluelinked.tag = tag
  else:
    face = None
    mentagside.state = qmenu.disabled
#    mengluelinked.state = qmenu.disabled
  tagged = gettagged(editor)
  if tagged is None:
    if gettaggedpt(editor):
      mencleartag.state = qmenu.normal
      menglueside.state = qmenu.normal
    else:
      if not gettaggedlist(editor):
        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)
menlinksel  = qmenu.item("&Link selected",LinkSelClick,"Link the selected faces")
#mengluelinked = qmenu.item("Gl&ue linked",GlueLinkedClick,"Glue linked faces to the selected one")


quarkpy.mapcommands.items.append(qmenu.sep)   # separator
quarkpy.mapcommands.items.append(mentagside)
quarkpy.mapcommands.shortcuts["Ctrl+T"] = mentagside
quarkpy.mapcommands.items.append(mencleartag)
quarkpy.mapcommands.items.append(menglueside)
quarkpy.mapcommands.shortcuts["Ctrl+G"] = menglueside
quarkpy.mapcommands.items.append(menaligntex)
quarkpy.mapcommands.shortcuts["Ctrl+W"] = menaligntex
quarkpy.mapcommands.items.append(menlinksel)
quarkpy.mapcommands.shortcuts["Ctrl+L"] = menlinksel
#
# de trop on the menu, methinks
#quarkpy.mapcommands.items.append(mengluelinked)
#quarkpy.mapcommands.shortcuts["Alt+L"] = mengluelinked


quarkpy.mapcommands.onclick = commandsclick

# Jan 28, 1999 - made menus constant, added flyover help