Go to QuArK Web Site
Tagging and Gluing
Updated 05 Apr 2018
Upper levels:
QuArK Information Base
3. Advanced customization
3.6. Plug-Ins
3.6.1. Plugin Tutorial

 3.6.1.3. Tagging and Gluing

 [ Prev - Up - Next ] 

Now we get some commands to actually do something. What we want the user to be able to do is first `tag' a selected side, then later select one or more sides, and finally give another command `glue to tagged', which will move the selected sides so that they are coplanar with the tagged one. So we will actually need two (eventually three) commands in all.


 Index


 Tagging

tiglari - 05 Apr 2018   [ Top ] 

For tagging, What we need to do is:

  1. See what's has been selected
  2. check that it is a single side
  3. store it somewhere for future use by the gluing command
  4. draw some indication of the selection on the screen.

The last step involves some subleties, so we'll leave it out for now.

When you start editing a map with QuArK, it creates an instance of the class mapeditor (defined in quarkpy.mapeditor), which then looks after the editing of that map. Since we want to keep track of what's been tagged in this particular editing session, but not outside of it, a good way to store the info of what's been tagged is in some sort of object attached to the mapeditor.

For this we'll use a Python `class' (you might want to look at the Python tutorial on classes), and we'll start by defining it like this:

class Tagging:
    tagged = None
Note that this is a very simple class, without explicit constructors or methods; it's equivalent to a simple record. Python doesn't have a record construct, only classes.

Now the new TagSideClick command will look like this; I've dumped in excessive comments that you'd presumably want to leave out of your version. And absolutely do not worry about how much of it doesn't make sense at first.

def tagSideClick (m):
    editor = mapeditor()
    #
    #  do we have an editor? the function mapeditor() returns the editor
    #   from which the command was invoked.  If we don't, bail.
    #
    if editor is None: return
    #
    #  attach a new tagging object to it (to keep track of what side
    #  is tagged).  `Tagging()' is the `default constructor', automatically
    #  provided to make a new instance of the class, if no explicit
    #  constructor has been defined
    #
    editor.tagging = Tagging()
    #
    # get the editor's selection-list and stick it in a local variable
    #
    tagged = editor.layout.explorer.sellist
    #
    # now check that what's selected is exactly one side
    #
    if (len(tagged) < 1):
        quarkx.msgbox("No selection", MT_ERROR, MB_OK)
    elif (len(tagged) > 1):
        quarkx.msgbox("Only one selection allowed", MT_ERROR, MB_OK)
    elif (tagged[0].type!= ":f"):
        quarkx.msgbox("The selected object is not a face", MT_ERROR, MB_OK)
    #
    # and at last we're ready to rock and roll!
    #
    else:
        #
        #  This actually stashes our tagged side (the first & only
        #  element of a one-element list) in the tagging object
        #  that we've attached to the editor.
        #
        editor.tagging.tagged = tagged[0]
Make sure that this definition replaces your old one, if you put it in ahead of the old one, the old one will be loaded up second and overwrite the new one. You also want to change the command setting up the menu item so that it will be labelled `&Tag side'.

When you test this, what ought to happen is that when you give the Tag side command after selecting a single face, there is no visible effect, but if you've violated any of the conditions, you ought to get an error-notifying message-box. So make a point of testing all the various erroneous conditions! In general, it is very important to test that an operation will make sense before performing it (in jargon, that `it's preconditions are met'), but in fact this is a rather primitive approach, and we will soom move on to menu item enabling and disabling instead.


 Glueing

tiglari - 05 Apr 2018   [ Top ] 

Now on to implement the gluing command! First, add a new command to the commands menu called `&Glue to Tagged', which invokes the function glueSideClick.

Now to define this function. It basically has to do three things:

  • Identify the side that we want to glue (move)
  • Identify the side that we want to glue it to
  • Do the glue

There's another, somewhat philsophical point about UI design, which is that it's a good idea to have some separation between the code implementing the UI and the code implementing the actual operations that the UI is being used to control, since one often wants to re-use the operations code elsewhere in the program. It's often hard to figure out exactly how to split up the function, but it's usually worth trying. So here's the UI portion:

After getting the editor, we get the tagged face and the editor, check that various preconditions are met, and then call the glueing function we're going to define shortly:

def glueSideClick (m):
    editor = mapeditor()
    if editor is None: return
    tagged = editor.tagging.tagged
    #
    # get the selection (which will be a list)
    #
    sides = editor.layout.explorer.sellist
    #
    # check that it meets conditions (this could be done more slickly)
    #
    if len(sides) < 1:
        quarkx.msgbox("Something must be selected for glueing", MT_WARNING, MB_OK)
        return
    if len(sides) > 1:
        quarkx.msgbox("Only one thing may be selected for glueing", MT_WARNING, MB_OK)
        return
    if sides[0].type != ":f":
        quarkx.msgbox("The selection must be a face", MT_WARNING, MB_OK)
    #
    # Now derive the new side
    #
    side = sides[0]
    newside = glueToTagged(side, tagged)
Now we get so something a bit tricky. The glueToTagged function, which we haven't written yet, is supposed to manufactory a copy of the face `side', occupying the same plane as the face `tagged'. But so far this face is just sitting there as the value of the variable `newside', and we need to get it into the map. There is a special mechanism, the `undo' module, which should always be used to make changes to the map, for the reason that the this module keeps track of what's been done and enables it to be undone and redone with the `Undo' menu items.

So here's how to use the undo module:

    # First create an `undo' object with the quarkx.action()
    #  function.  This will keep track of the actions performed.
    #
    undo = quarkx.action()
    #
    # Now substitute the new side for the old one (in the undo object)
    #
    undo.exchange(side, newside)
    #
    # and perform the action for real in the map
    #
    undo.ok(editor.Root, "glue to tagged")
Here we just perform one exchange before the ok, but any number of actions can be performed, and there's more than just exchange, as you can see by consulting the documentation of 'The undo module'.

Next we want to define the function that produces the new face. This involves some map object manipulation and 3D math (see 'Attributes of Internal objects of class "Face"'):

def glueToTagged(side, tagged):
    #
    # Make a copy of the original (so that texture etc. info is preserved)
    #
    new = side.copy()
    #
    # .distortion rotates the face into a new position; we have
    #  to make sure that the normal is going to point outward from
    #  its parent poly.
    #
    if new.normal*tagged.normal < 0:
        new.distortion(-tagged.normal, new.origin)
    else: new.distortion(tagged.normal, new.origin)
    #
    #  Now shift our new side into position:
    #
    new.translate(tagged.origin-new.origin)
    return new
This code works, but it has various problems. To see the first, start a map, and click on the `Glue to Tagged' menu item. Either nothing will happen at all, or else you'll get a console-screen-full of verbiage ending in a complaint about an `attribute error' (you're supposed to get the console error, but currently something involved in delivering that error doesn't always work). This is because the GlueSideClick code refers to the `tagging.tagged'-value of the mapeditor, but in this case it didn't have one, because no `Tag' command had been issued.

Later we'll see how to fix this problem by having the menu item `disabled' until a side is actually tagged, but since the problem of accessing attributes that haven't been defined yet arises elsewere, we'll show how to do it by using a function that contains an `exception handler':

def gettagged(o):
    try:
        return o.tagging.tagged
    except (AttributeError): return None

The `try' command runs the following block of code normally, except that if the kind of error named in the following `except' statement occurs, that execution is aborted, and the code in the except-block is executed instead. So this little function returns None rather than just causing an error, in case whatever is fed to it lacks a `tagging.tagged'-value.

So we can prevent the grotty-looking console outburst by replacing line 4 of GlueSideClick with:

tagged = gettagged(editor)
if tagged is None:
    quarkx.msgbox("Something must be tagged",MT_WARNING,MB_OK)
The real maptagside plugin actually does this, the gettagged function and much else besides is defined in plugins/tagging.py (which should perhaps be relocated to quarkpy, but hasn't been). tagging.py also defines a function tagface(face, editor) for setting the tag.

By now you might be wondering about the faces sometimes being called `sides', this happened because `side' is the term I first used when writing the plugin, and by the time I decided it was a mistake, the bad decision had entrenched itself with too many entanglements.

Returning to the mere functioning of our plugin, another problem is that when we do tag a side, we see no indication of what we have done, in unfavorable contrast to actual QuArK, where the tagged side gets outlined in red, with a red square in the center. And also the interface invites us to perform actions that then produce error messages, such as trying to glue when there's nothing to glue to, rather than indicating at any moment what the sensible things to do are, and also there are no hints or help.

Finally, the whole thing is rather clunky, we have to select faces, and then go to the command menu to tag and glue them, and wouldn't it be better if all this could be done off the right mouse-button menu? In the next three lessons we attend to these matters.


 Troubleshooting

tiglari - 05 Apr 2018   [ Top ] 

When things start getting a bit complicated, bugs get likely. Plugins starting with map- get loaded when you start the map editor, and if they are syntactically ill-formed, the loading process will stop, leaving you with an error message on the `console' (dark screen, red letters). After fixing the mistake, it is often possible to procede just by closing the grey window you also get (failed mapeditor window), and opening the map editor again. Sometimes this doesn't work, and you have to restart QuArK.

QuArK is also supposed to show the console when a runtime error is encountered, but for some reason, this isn't always working now, and command execution just stops. You can add `debug' statements to code, which print things out to the console, and you can also put QuArK into `developer mode' in the options menu. Then `squawk' statements will print out messageboxes with whatever message you want. e.g.

    squawk('newface: '+newface.shortname)

Finally, in developer mode, a `Reload' command will appear on the command menu; this allows you to reload a module. This usually works for plugins (tho not always for quarkpy files), with some glitches, such as duplication, etc., of menu items. But it's often quicker than restarting QuArK from scarch. It can't be made perfect, because Python doesn't have any `unload' command that could reverse the effects of loading a module.

A tested version of what you ought to have now is  maptagside1.py  in plugin_examples.zip



Copyright (c) 2022, GNU General Public License by The QuArK (Quake Army Knife) Community - https://quark.sourceforge.io/

 [ Prev - Top - Next ]