Browse Source

*** empty log message ***

David Rose 25 years ago
parent
commit
43f23a2a55
39 changed files with 4873 additions and 8 deletions
  1. 3 0
      dtool/Config.Irix.pp
  2. 3 0
      dtool/Config.Linux.pp
  3. 3 0
      dtool/Config.Win32.pp
  4. 3 0
      dtool/LocalSetup.pp
  5. 4 0
      dtool/pptempl/Depends.pp
  6. 47 0
      dtool/src/dtoolutil/filename.cxx
  7. 2 0
      dtool/src/dtoolutil/filename.h
  8. 20 0
      pandatool/src/egg-palettize/Sources.pp
  9. 1191 0
      pandatool/src/egg-palettize/attribFile.cxx
  10. 132 0
      pandatool/src/egg-palettize/attribFile.h
  11. 14 0
      pandatool/src/egg-palettize/config_egg_palettize.cxx
  12. 592 0
      pandatool/src/egg-palettize/eggPalettize.cxx
  13. 71 0
      pandatool/src/egg-palettize/eggPalettize.h
  14. 14 0
      pandatool/src/egg-palettize/imageFile.cxx
  15. 28 0
      pandatool/src/egg-palettize/imageFile.h
  16. 544 0
      pandatool/src/egg-palettize/palette.cxx
  17. 90 0
      pandatool/src/egg-palettize/palette.h
  18. 347 0
      pandatool/src/egg-palettize/sourceEgg.cxx
  19. 75 0
      pandatool/src/egg-palettize/sourceEgg.h
  20. 80 0
      pandatool/src/egg-palettize/string_utils.cxx
  21. 20 0
      pandatool/src/egg-palettize/string_utils.h
  22. 586 0
      pandatool/src/egg-palettize/texture.cxx
  23. 131 0
      pandatool/src/egg-palettize/texture.h
  24. 362 0
      pandatool/src/egg-palettize/userAttribLine.cxx
  25. 86 0
      pandatool/src/egg-palettize/userAttribLine.h
  26. 5 3
      pandatool/src/eggbase/Sources.pp
  27. 1 1
      pandatool/src/eggbase/eggBase.cxx
  28. 2 0
      pandatool/src/eggbase/eggFilter.h
  29. 104 0
      pandatool/src/eggbase/eggMultiBase.cxx
  30. 45 0
      pandatool/src/eggbase/eggMultiBase.h
  31. 160 0
      pandatool/src/eggbase/eggMultiFilter.cxx
  32. 40 0
      pandatool/src/eggbase/eggMultiFilter.h
  33. 1 1
      pandatool/src/eggprogs/eggTrans.cxx
  34. 7 1
      pandatool/src/flt/fltHeader.cxx
  35. 1 0
      pandatool/src/flt/fltHeader.h
  36. 6 0
      pandatool/src/fltprogs/fltCopy.cxx
  37. 3 2
      pandatool/src/fltprogs/fltCopy.h
  38. 49 0
      pandatool/src/progbase/programBase.cxx
  39. 1 0
      pandatool/src/progbase/programBase.h

+ 3 - 0
dtool/Config.Irix.pp

@@ -81,6 +81,9 @@
 // Do we have <unistd.h>?
 #define HAVE_UNISTD_H 1
 
+// Do we have <utime.h>?
+#define HAVE_UTIME_H 1
+
 // Do we have <sys/soundcard.h> (and presumably a Linux-style audio
 // interface)?
 #define HAVE_SYS_SOUNDCARD_H

+ 3 - 0
dtool/Config.Linux.pp

@@ -81,6 +81,9 @@
 // Do we have <unistd.h>?
 #define HAVE_UNISTD_H 1
 
+// Do we have <utime.h>?
+#define HAVE_UTIME_H 1
+
 // Do we have <sys/soundcard.h> (and presumably a Linux-style audio
 // interface)?
 #define HAVE_SYS_SOUNDCARD_H 1

+ 3 - 0
dtool/Config.Win32.pp

@@ -81,6 +81,9 @@
 // Do we have <unistd.h>?
 #define HAVE_UNISTD_H
 
+// Do we have <utime.h>?
+#define HAVE_UTIME_H
+
 // Do we have <sys/soundcard.h> (and presumably a Linux-style audio
 // interface)?
 #define HAVE_SYS_SOUNDCARD_H

+ 3 - 0
dtool/LocalSetup.pp

@@ -130,6 +130,9 @@ $[cdefine HAVE_SYS_TYPES]
 /* Define if you have the <unistd.h> header file.  */
 $[cdefine HAVE_UNISTD_H]
 
+/* Define if you have the <utime.h> header file.  */
+$[cdefine HAVE_UTIME_H]
+
 /* Do we have <sys/soundcard.h> (and presumably a Linux-style audio
    interface)? */
 $[cdefine HAVE_SYS_SOUNDCARD_H]

+ 4 - 0
dtool/pptempl/Depends.pp

@@ -31,6 +31,10 @@ Warning: Lib(s) $[nonexisting], referenced in $[DIRNAME]/$[TARGET], not found.
     #set DEPENDABLE_HEADERS $[DEPENDABLE_HEADERS] $[filter %.h %.I,$[SOURCES]]
   #end metalib_target static_lib_target ss_lib_target lib_target noinst_lib_target bin_target noinst_bin_target
 
+  #forscopes test_bin_target
+    #set DEPENDABLE_HEADERS $[DEPENDABLE_HEADERS] $[filter %.h %.I,$[SOURCES]]
+  #end test_bin_target
+
   // Allow the user to define additional EXTRA_DEPENDS targets in each
   // Sources.pp.
   #define DEPEND_DIRS \

+ 47 - 0
dtool/src/dtoolutil/filename.cxx

@@ -18,6 +18,14 @@
 #include <stdio.h>  // For rename()
 #include <sys/stat.h>
 
+#ifdef HAVE_UTIME_H
+#include <utime.h>
+
+// We assume we have these too.
+#include <errno.h>
+#include <fcntl.h>
+#endif
+
 
 #if defined(WIN32)
 /* begin Win32-specific code */
@@ -787,6 +795,45 @@ open_read_write(fstream &stream) const {
   return (!stream.fail());
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: Filename::touch
+//       Access: Public
+//  Description: Updates the modification time of the file to the
+//               current time.  If the file does not already exist, it
+//               will be created.  Returns true if successful, false
+//               if there is an error.
+////////////////////////////////////////////////////////////////////
+bool Filename::
+touch() const {
+#ifdef HAVE_UTIME_H
+  // Most Unix systems can do this explicitly.
+  string filename = to_os_specific();
+  int result = utime(filename.c_str(), NULL);
+  if (result < 0) {
+    if (errno == ENOENT) {
+      // So the file doesn't already exist; create it.
+      int fd = creat(filename.c_str(), 0666);
+      if (fd < 0) {
+	perror(filename.c_str());
+	return false;
+      }
+      close(fd);
+      return true;
+    }
+    perror(filename.c_str());
+    return false;
+  }
+  return true;
+#else
+  // Other systems may not have an explicit control over the
+  // modification time.  For these systems, we'll just temporary open
+  // the file in append mode, then close it again (it gets closed when
+  // the ofstream goes out of scope).
+  ofstream file;
+  return open_append(file);
+#endif
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: Filename::unlink
 //       Access: Public

+ 2 - 0
dtool/src/dtoolutil/filename.h

@@ -130,6 +130,8 @@ public:
   bool open_append(ofstream &stream) const;
   bool open_read_write(fstream &stream) const;
 
+  bool touch() const;
+
   bool unlink() const;
   bool rename_to(const Filename &other) const;
 

+ 20 - 0
pandatool/src/egg-palettize/Sources.pp

@@ -0,0 +1,20 @@
+#begin bin_target
+  #define TARGET egg-palettize
+  #define LOCAL_LIBS \
+    eggbase progbase
+  #define OTHER_LIBS \
+    egg:c linmath:c putil:c express:c pnmimagetypes:c \
+    pandaegg:m panda:m pandaexpress:m \
+    dtoolutil:c dconfig:c dtool:m pystub
+
+  #define SOURCES \
+    attribFile.cxx attribFile.h config_egg_palettize.cxx \
+    eggPalettize.cxx eggPalettize.h \
+    imageFile.cxx imageFile.h palette.cxx palette.h sourceEgg.cxx \
+    sourceEgg.h string_utils.cxx string_utils.h texture.cxx texture.h \
+    userAttribLine.cxx userAttribLine.h
+
+  #define INSTALL_HEADERS \
+
+#end bin_target
+

+ 1191 - 0
pandatool/src/egg-palettize/attribFile.cxx

@@ -0,0 +1,1191 @@
+// Filename: attribFile.cxx
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "attribFile.h"
+#include "userAttribLine.h"
+#include "eggPalettize.h"
+#include "string_utils.h"
+#include "texture.h"
+#include "palette.h"
+#include "sourceEgg.h"
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdio.h>
+#include <map>
+#include <set>
+#include <fcntl.h>
+
+AttribFile::
+AttribFile(const Filename &filename) {
+  _name = filename.get_basename_wo_extension();
+  _txa_filename = filename;
+  _txa_filename.set_extension("txa");
+  _pi_filename = filename;
+  _pi_filename.set_extension("pi");
+  _txa_fd = -1;
+
+  _optimal = false;
+  _txa_needs_rewrite = false;
+
+  _palette_prefix = _name + "-palette.";
+  _pal_xsize = 512;
+  _pal_ysize = 512;
+  _default_margin = 2;
+  _force_power_2 = false;
+  _aggressively_clean_mapdir = false;
+}
+
+string AttribFile::
+get_name() const {
+  return _name;
+}
+
+bool AttribFile::
+grab_lock() {
+  if (!_txa_filename.exists()) {
+    nout << "Attributes file " << _txa_filename << " does not exist.\n";
+  }
+
+  _txa_fd = open(_txa_filename.c_str(), O_RDWR | O_CREAT, 0666);
+  if (_txa_fd < 0) {
+    perror(_txa_filename.c_str());
+    return false;
+  }
+
+  struct flock fl;
+  fl.l_type = F_WRLCK;
+  fl.l_whence = SEEK_SET;
+  fl.l_start = 0;
+  fl.l_len = 0;
+
+  if (fcntl(_txa_fd, F_SETLK, &fl) < 0) {
+    nout << "Waiting for lock on " << _txa_filename << "\n";
+    while (fcntl(_txa_fd, F_SETLKW, &fl) < 0) {
+      if (errno != EINTR) {
+	perror(_txa_filename.c_str());
+	return false;
+      }
+    }
+  }
+
+  _txa_fstrm.attach(_txa_fd);
+
+  return true;
+}
+
+bool AttribFile::
+release_lock() {
+  // Closing the fstream will close the fd, and thus release all the
+  // file locks.
+  _txa_fstrm.close();
+  _txa_fd = -1;
+
+  return true;
+}
+
+bool AttribFile::
+read() {
+  bool okflag = true;
+
+  okflag = read_txa(_txa_fstrm);
+
+  {
+    if (!_pi_filename.exists()) {
+      nout << "Palette information file " << _pi_filename << " does not exist.\n";
+    } else {
+      ifstream infile(_pi_filename.c_str());
+      if (!infile) {
+	nout << "Palette information file " << _pi_filename << " exists, but cannot be read.\n";
+	return false;
+      }
+
+      okflag = read_pi(infile);
+    }
+  }
+
+  return okflag;
+}
+
+bool AttribFile::
+write() {
+  bool okflag = true;
+
+  if (_txa_needs_rewrite) {
+    // Rewind and truncate the file for writing.
+    _txa_fstrm.clear();
+    _txa_fstrm.seekp(0, ios::beg);
+    ftruncate(_txa_fd, 0);
+
+    okflag = write_txa(_txa_fstrm) && okflag;
+    _txa_fstrm << flush;
+  }
+
+  {
+    ofstream outfile(_pi_filename.c_str(), ios::out, 0666);
+    if (!outfile) {
+      nout << "Unable to write file " << _pi_filename << "\n";
+      return false;
+    }
+    
+    okflag = write_pi(outfile) && okflag;
+  }
+
+  return okflag;
+}
+
+void AttribFile::
+update_params(EggPalettize *prog) {
+  if (prog->_got_map_dirname) {
+    _map_dirname = prog->_map_dirname;
+  }
+  if (prog->_got_palette_size) {
+    _pal_xsize = prog->_pal_size[0];
+    _pal_ysize = prog->_pal_size[1];
+  }
+  if (prog->_got_default_margin) {
+    _default_margin = prog->_default_margin;
+  }
+  if (prog->_got_force_power_2) {
+    _force_power_2 = prog->_force_power_2;
+  }
+  if (prog->_got_aggressively_clean_mapdir) {
+    _aggressively_clean_mapdir = prog->_aggressively_clean_mapdir;
+  }
+}
+
+void AttribFile::
+get_req_sizes() {
+  Textures::iterator ti;
+  for (ti = _textures.begin(); ti != _textures.end(); ++ti) {
+    Texture *tex = (*ti).second;
+    tex->clear_req();
+
+    int margin = _default_margin;
+    UserLines::const_iterator ui;
+
+    bool matched = false;
+    for (ui = _user_lines.begin(); 
+	 ui != _user_lines.end() && !matched;
+	 ++ui) {
+      matched = (*ui)->match_texture(tex, margin);
+    }
+
+    if (matched) {
+      tex->set_matched_anything(true);
+    }
+  }
+}
+
+// Update the unused flags on all textures to accurately reflect
+// those that are unused by any egg files.  Omit unused textures
+// from the palettizing set.
+void AttribFile::
+update_texture_flags() {
+  // First, clear all the flags.
+  Textures::iterator ti;
+  for (ti = _textures.begin(); ti != _textures.end(); ++ti) {
+    Texture *tex = (*ti).second;
+    tex->set_unused(true);
+    tex->set_uses_alpha(false);
+  }
+
+  // Then, for each egg file, mark all the textures it's known to be
+  // using, and update the repeat and alpha flags.
+  Eggs::const_iterator ei;
+  for (ei = _eggs.begin(); ei != _eggs.end(); ++ei) {
+    SourceEgg *egg = (*ei).second;
+    egg->mark_texture_flags();
+  }
+
+  // Now go back through and omit any unused textures.  This is also a
+  // fine time to mark the textures' original packing state, so we can
+  // check later to see if they've been repacked elsewhere.
+  for (ti = _textures.begin(); ti != _textures.end(); ++ti) {
+    Texture *tex = (*ti).second;
+    tex->record_orig_state();
+
+    if (tex->unused()) {
+      tex->set_omit(Texture::OR_unused);
+    }
+  }
+}
+
+// Clear out all the old packing order and start again from the top.
+// This should get as nearly optimal a packing as this poor little
+// algorithm can manage.
+void AttribFile::
+repack_all_textures() {
+  // First, delete all the existing palettes.
+  Palettes::iterator pi;
+  for (pi = _palettes.begin(); pi != _palettes.end(); ++pi) {
+    // Remove the old palette file?
+    if (_aggressively_clean_mapdir && 
+	!(*pi)->get_filename().empty()) {
+      if (access((*pi)->get_filename().c_str(), F_OK) == 0) {
+	nout << "Deleting " << (*pi)->get_filename() << "\n";
+	unlink((*pi)->get_filename().c_str());
+      }
+    }
+
+    delete (*pi);
+  }
+  _palettes.clear();
+
+  // Reorder the textures in descending order by height and width for
+  // optimal packing.
+  vector<Texture *> textures;
+  get_eligible_textures(textures);
+  
+  // Now pack all the textures.  This will create new palettes.
+  vector<Texture *>::iterator ti;
+  for (ti = textures.begin(); ti != textures.end(); ++ti) {
+    pack_texture(*ti);
+  }
+
+  _optimal = true;
+}
+
+// Add new textures into the palettes without disturbing whatever was
+// already there.  This won't generate an optimal palette, but it
+// won't require rebuilding every egg file that already uses this
+// palette.
+void AttribFile::
+repack_some_textures() {
+  bool empty_before = _palettes.empty();
+  bool any_added = false;
+
+  // Reorder the textures in descending order by height and width for
+  // optimal packing.
+  vector<Texture *> textures;
+  get_eligible_textures(textures);
+  
+  // Now pack whatever textures are currently unpacked.
+  vector<Texture *>::iterator ti;
+  for (ti = textures.begin(); ti != textures.end(); ++ti) {
+    Texture *tex = (*ti);
+    if (!tex->is_packed()) {
+      if (pack_texture(tex)) {
+	any_added = true;
+      }
+    }
+  }
+
+  _optimal = (empty_before || !any_added);
+}
+
+void AttribFile::
+optimal_resize() {
+  Palettes::iterator pi;
+  for (pi = _palettes.begin(); pi != _palettes.end(); ++pi) {
+    (*pi)->optimal_resize();
+  }
+}
+
+void AttribFile::
+finalize_palettes() {
+  Palettes::iterator pi;
+  for (pi = _palettes.begin(); pi != _palettes.end(); ++pi) {
+    (*pi)->finalize_palette();
+  }
+}
+
+void AttribFile::
+remove_unused_lines() {
+  UserLines::iterator read, write;
+
+  read = _user_lines.begin();
+  write = _user_lines.begin();
+  while (read != _user_lines.end()) {
+    if ((*read)->was_used()) {
+      (*write++) = (*read++);
+    } else {
+      delete (*read);
+      _txa_needs_rewrite = true;
+      read++;
+    }
+  }
+  _user_lines.erase(write, _user_lines.end());
+}
+
+// Checks that each texture that wants packing has been packed, that
+// no textures that don't need packing have been packed, and that all
+// textures are packed at their correct sizes.  Returns true if no
+// changes need to be made, false otherwise.
+bool AttribFile::
+check_packing(bool force_optimal) {
+  bool all_ok = true;
+
+  Textures::iterator ti;
+  for (ti = _textures.begin(); ti != _textures.end(); ++ti) {
+    Texture *texture = (*ti).second;
+
+    if (texture->get_omit() == Texture::OR_none) {
+      // Here's a texture that thinks it wants to be packed.  Does it?
+      int xsize, ysize;
+      if (!texture->get_req(xsize, ysize)) {
+	// If we don't know the texture's size, we can't place it.
+	nout << "Warning!  Can't determine size of " << texture->get_name()
+	     << "\n";
+	texture->set_omit(Texture::OR_unknown);
+
+      } else if ((xsize > _pal_xsize || ysize > _pal_ysize) ||
+		 (xsize == _pal_xsize && ysize == _pal_ysize)) {
+	// If the texture is too big for the palette (or exactly fills the
+	// palette), we can't place it.
+	texture->set_omit(Texture::OR_size);
+
+      } else {
+	// Ok, this texture really does want to be packed.  Is it?
+	int px, py, m;
+	if (texture->get_packed_size(px, py, m)) {
+	  // The texture is packed.  Does it have the right size?
+	  if (px != xsize || py != ysize) {
+	    // Oops, we'll have to repack it.
+	    unpack_texture(texture);
+	    _optimal = false;
+	    all_ok = false;
+	  }
+	  if (m != texture->get_margin()) {
+	    // The margin has changed, although not the size.  We
+	    // won't have to repack it, but we do need to update it.
+	    texture->set_changed(true);
+	  }
+	} else {
+	  // The texture isn't packed.  Need to pack it.
+	  all_ok = false;
+	}
+      }
+    }
+
+    if (texture->get_omit() != Texture::OR_none) {
+      // Here's a texture that doesn't want to be packed.  Is it?
+      if (unpack_texture(texture)) {
+	// It was!  Not any more.
+	_optimal = false;
+	all_ok = false;
+      }
+    }
+  }
+
+  if (force_optimal && !_optimal) {
+    // If the user wants to insist on an optimal packing, we'll have
+    // to give it to him.
+    all_ok = false;
+  }
+
+  return all_ok;
+}
+
+
+bool AttribFile::
+pack_texture(Texture *texture) {
+  // Now try to place it in each of our existing palettes.
+  Palettes::iterator pi;
+  for (pi = _palettes.begin(); pi != _palettes.end(); ++pi) {
+    if ((*pi)->pack_texture(texture)) {
+      return true;
+    }
+  }
+
+  // It didn't place anywhere; create a new palette for it.
+  Palette *palette = 
+    new Palette(_palettes.size() + 1, _pal_xsize, _pal_ysize, 0, this);
+  if (!palette->pack_texture(texture)) {
+    // Hmm, it didn't fit on an empty palette.  Must be too big.
+    texture->set_omit(Texture::OR_size);
+    delete palette;
+    return false;
+  }
+  _palettes.push_back(palette);
+
+  return true;
+}
+
+bool AttribFile::
+unpack_texture(Texture *texture) {
+  if (texture->is_packed()) {
+    bool unpacked = texture->get_palette()->unpack_texture(texture);
+    assert(unpacked);
+    return true;
+  }
+
+  // It wasn't packed.
+  return false;
+}
+
+// Updates the timestamp on each egg file that will need to be
+// rebuilt, so that a future make process will pick it up.  This is
+// only necessary to update egg files that may not have been included
+// on the command line, and which we don't have direct access to.
+void AttribFile::
+touch_dirty_egg_files(bool force_redo_all,
+		      bool eggs_include_images) {
+  Eggs::iterator ei;
+  for (ei = _eggs.begin(); ei != _eggs.end(); ++ei) {
+    SourceEgg *egg = (*ei).second;
+
+    if (egg->needs_rebuild(force_redo_all, eggs_include_images)) {
+      Filename filename = egg->get_egg_filename();
+      filename.set_extension("pt");
+      nout << "Touching " << filename << "\n";
+      if (!filename.touch()) {
+	nout << "unable to touch " << filename << "\n";
+      }
+    }
+  }
+}
+
+
+Texture *AttribFile::
+get_texture(const string &name) {
+  Textures::iterator ti;
+  ti = _textures.find(name);
+  if (ti != _textures.end()) {
+    return (*ti).second;
+  }
+
+  Texture *texture = new Texture(this, name);
+  _textures[name] = texture;
+  return texture;
+}
+
+void AttribFile::
+get_eligible_textures(vector<Texture *> &textures) {
+  // First, copy the texture pointers into this map structure to sort
+  // them in descending order by size.  This is a 2-d map such that
+  // each map[ysize][xsize] is a set of texture pointers.
+  typedef map<int, map<int, set<Texture *> > > TexBySize;
+  TexBySize tex_by_size;
+  int num_textures = 0;
+
+  Textures::iterator ti;
+  for (ti = _textures.begin(); ti != _textures.end(); ++ti) {
+    Texture *texture = (*ti).second;
+
+    if (texture->get_omit() == Texture::OR_none) {
+      int xsize, ysize;
+      if (texture->get_req(xsize, ysize)) {
+	tex_by_size[-ysize][-xsize].insert(texture);
+	num_textures++;
+      }
+    }
+  }
+
+  // Now walk through this map and get out our textures, nicely sorted
+  // in descending order by height and width.
+  textures.clear();
+  textures.reserve(num_textures);
+
+  TexBySize::const_iterator t1;
+  for (t1 = tex_by_size.begin(); t1 != tex_by_size.end(); ++t1) {
+    map<int, set<Texture *> >::const_iterator t2;
+    for (t2 = (*t1).second.begin(); t2 != (*t1).second.end(); ++t2) {
+      set<Texture *>::const_iterator t3;
+      for (t3 = (*t2).second.begin(); t3 != (*t2).second.end(); ++t3) {
+	textures.push_back(*t3);
+      }
+    }
+  }
+}
+
+SourceEgg *AttribFile::
+get_egg(Filename name) {
+  Eggs::iterator ei;
+  ei = _eggs.find(name);
+  if (ei != _eggs.end()) {
+    return (*ei).second;
+  }
+
+  SourceEgg *egg = new SourceEgg();
+  egg->resolve_egg_filename(name);
+  egg->set_egg_filename(name);
+  _eggs[name] = egg;
+  return egg;
+}
+
+bool AttribFile::
+generate_palette_images() {
+  bool okflag = true;
+
+  Palettes::iterator pi;
+  for (pi = _palettes.begin(); pi != _palettes.end(); ++pi) {
+    Palette *palette = (*pi);
+    if (palette->new_palette()) {
+      // If the palette is a new palette, we'll have to generate a new
+      // image from scratch.
+      okflag = palette->generate_image() && okflag;
+    } else {
+      // Otherwise, we can probably get by with just updating
+      // whichever images, if any, have changed.
+      okflag = palette->refresh_image() && okflag;
+    }
+  }
+
+  return okflag;
+}
+
+bool AttribFile::
+transfer_unplaced_images(bool force_redo_all) {
+  bool okflag = true;
+
+  Textures::iterator ti;
+  for (ti = _textures.begin(); ti != _textures.end(); ++ti) {
+    Texture *texture = (*ti).second;
+
+    if (texture->get_omit() != Texture::OR_none &&
+	texture->get_omit() != Texture::OR_unused) {
+      // Here's a texture that needs to be moved to our mapdir.  But
+      // maybe it's already there and hasn't changed recently.
+      if (force_redo_all || texture->needs_refresh()) {
+	// Nope, needs to be updated.
+	okflag = texture->transfer() && okflag;
+      }
+    } else {
+      if (_aggressively_clean_mapdir) {
+	if (access(texture->get_filename().c_str(), F_OK) == 0) {
+	  nout << "Deleting " << texture->get_filename() << "\n";
+	  unlink(texture->get_filename().c_str());
+	}
+      }
+    }
+  }
+
+  return okflag;
+}
+
+
+void AttribFile::
+check_dup_textures(map<string, Texture *> &textures,
+		   map<string, int> &dup_textures) const {
+  Textures::const_iterator ti;
+  for (ti = _textures.begin(); ti != _textures.end(); ++ti) {
+    Texture *texture = (*ti).second;
+    string name = texture->get_name();
+      
+    map<string, Texture *>::iterator mi = textures.find(name);
+    if (mi == textures.end()) {
+      // This texture hasn't been used yet.
+      textures[name] = texture;
+      
+    } else {
+      // This texture has already been used in another palette.  The
+      // smaller of the two is considered wasted space.
+      Texture *other = (*mi).second;
+      
+      if (!other->is_really_packed() && !texture->is_really_packed()) {
+	// No, neither one is packed, so it's not wasted space.
+      
+      } else {
+	int txsize, tysize;
+	int oxsize, oysize;
+	int wasted_size = 0;
+      
+	if (other->is_really_packed() != texture->is_really_packed()) {
+	  // If one texture is packed and the other isn't, the packed
+	  // one is considered wasted space.
+	  if (other->is_really_packed()) {
+	    if (other->get_req(oxsize, oysize)) {
+	      wasted_size = oxsize * oysize;
+	    }
+	    (*mi).second = texture;
+	  } else {
+	    if (texture->get_req(txsize, tysize)) {
+	      wasted_size = txsize * tysize;
+	    }
+	  }
+
+	} else {
+	  // Both textures are packed.  The smaller one is considered
+	  // wasted space.
+	  assert(other->is_really_packed() && texture->is_really_packed());
+
+	  if (texture->get_req(txsize, tysize) && 
+	      other->get_req(oxsize, oysize)) {
+	    if (txsize * tysize <= oxsize * oysize) {
+	      wasted_size = txsize * tysize;
+	    } else {
+	      wasted_size = oxsize * oysize;
+	      (*mi).second = texture;
+	    }
+	  }
+	}
+	
+	// Now update the wasted space total for this texture.
+	map<string, int>::iterator di = dup_textures.find(name);
+	if (di != dup_textures.end()) {
+	  (*di).second += wasted_size;
+	} else {
+	  dup_textures[name] = wasted_size;
+	}
+      }
+    }
+  }
+}
+
+void AttribFile::
+collect_statistics(int &num_textures, int &num_placed, int &num_palettes,
+		   int &orig_size, int &resized_size, 
+		   int &palette_size, int &unplaced_size) const {
+  num_textures = _textures.size();
+  num_palettes = _palettes.size();
+  num_placed = 0;
+  orig_size = 0;
+  resized_size = 0;
+  palette_size = 0;
+  unplaced_size = 0;
+
+  Textures::const_iterator ti;
+  for (ti = _textures.begin(); ti != _textures.end(); ++ti) {
+    Texture *texture = (*ti).second;
+
+    int xsize, ysize;
+    int rxsize, rysize;
+    int rsize = 0;
+    if (texture->get_size(xsize, ysize) && 
+	texture->get_last_req(rxsize, rysize)) {
+      orig_size += xsize * ysize;
+      resized_size += rxsize * rysize;
+      rsize = rxsize * rysize;
+    }
+    
+    if (texture->is_really_packed()) {
+      num_placed++;
+    } else {
+      unplaced_size += rsize;
+    }
+  }
+
+  Palettes::const_iterator pi;
+  for (pi = _palettes.begin(); pi != _palettes.end(); ++pi) {
+    Palette *palette = (*pi);
+    if (palette->get_num_textures() > 1) {
+      int xsize, ysize;
+      palette->get_size(xsize, ysize);
+      palette_size += xsize * ysize;
+    }
+  }    
+}  
+
+
+bool AttribFile::
+read_txa(istream &infile) {
+  string line;
+
+  getline(infile, line);
+  int line_num = 1;
+
+  while (!infile.eof()) {
+    UserAttribLine *ul = new UserAttribLine(line, this);
+    if (!ul->is_valid()) {
+      nout << "Error at line " << line_num << " of " << _txa_filename << "\n";
+      return false;
+    }
+    if (ul->is_old_style()) {
+      _txa_needs_rewrite = true;
+    }
+    _user_lines.push_back(ul);
+
+    getline(infile, line);
+    line_num++;
+  }
+  return true;
+}
+
+bool AttribFile::
+read_pi(istream &infile) {
+  string line;
+
+  getline(infile, line);
+  int line_num = 1;
+
+  while (!infile.eof()) {
+    // First, strip off the comment.
+    if (!line.empty()) {
+      if (line[0] == '#') {
+	line = "";
+      } else {
+	size_t pos = line.find(" #");
+	if (pos != string::npos) {
+	  line = line.substr(0, pos - 1);
+	}
+      }
+    }
+
+    vector<string> words = extract_words(line);
+    bool okflag = true;
+
+    if (words.empty()) {
+      getline(infile, line);
+      line_num++;
+
+    } else if (words[0] == "params") {
+      okflag = parse_params(words, infile, line, line_num);
+
+    } else if (words[0] == "packing") {
+      okflag = parse_packing(words, infile, line, line_num);
+
+    } else if (words[0] == "textures") {
+      okflag = parse_texture(words, infile, line, line_num);
+
+    } else if (words[0] == "pathnames") {
+      okflag = parse_pathname(words, infile, line, line_num);
+
+    } else if (words[0] == "egg") {
+      okflag = parse_egg(words, infile, line, line_num);
+
+    } else if (words[0] == "palette") {
+      okflag = parse_palette(words, infile, line, line_num);
+
+    } else if (words[0] == "unplaced") {
+      okflag = parse_unplaced(words, infile, line, line_num);
+
+    } else if (words[0] == "surprises") {
+      okflag = parse_surprises(words, infile, line, line_num);
+
+    } else {
+      nout << "Invalid keyword: " << words[0] << "\n";
+      okflag = false;
+    }
+
+    if (!okflag) {
+      nout << "Error at line " << line_num << " of " << _pi_filename << "\n";
+      return false;
+    }
+  }
+
+  return true;
+}
+
+bool AttribFile::
+write_txa(ostream &out) const {
+  UserLines::const_iterator ui;
+
+  for (ui = _user_lines.begin(); ui != _user_lines.end(); ++ui) {
+    (*ui)->write(out);
+  }
+
+  if (!out) {
+    nout << "I/O error when writing to " << _txa_filename << "\n";
+    return false;
+  }
+  return true;
+}
+
+bool AttribFile::
+write_pi(ostream &out) const {
+  out << 
+    "# This file was generated by egg-palettize.  Edit it at your own peril.\n";
+
+  out << "\nparams\n"
+      << "  map_directory " << _map_dirname << "\n"
+      << "  pal_xsize " << _pal_xsize << "\n"
+      << "  pal_ysize " << _pal_ysize << "\n"
+      << "  default_margin " << _default_margin << "\n"
+      << "  force_power_2 " << _force_power_2 << "\n"
+      << "  aggressively_clean_mapdir " << _aggressively_clean_mapdir << "\n";
+
+  if (_optimal) {
+    out << "\npacking is optimal\n";
+    // Well, as nearly as this program can do it, anyway.
+  } else {
+    out << "\npacking is suboptimal\n";
+  }
+
+  out << "\npathnames\n";
+  Textures::const_iterator ti;
+  for (ti = _textures.begin(); ti != _textures.end(); ++ti) {
+    (*ti).second->write_pathname(out);
+  }
+
+  Eggs::const_iterator ei;
+  for (ei = _eggs.begin(); ei != _eggs.end(); ++ei) {
+    out << "\n";
+    (*ei).second->write_pi(out);
+  }
+
+  Palettes::const_iterator pi;
+  for (pi = _palettes.begin(); pi != _palettes.end(); ++pi) {
+    out << "\n";
+    (*pi)->write(out);
+  }
+
+  out << "\n";
+  for (ti = _textures.begin(); ti != _textures.end(); ++ti) {
+    (*ti).second->write_unplaced(out);
+  }
+
+  // Sort textures in descending order by scale percent.
+  typedef multimap<double, Texture *> SortTextures;
+  SortTextures sort_textures;
+  for (ti = _textures.begin(); ti != _textures.end(); ++ti) {
+    Texture *texture = (*ti).second;
+    sort_textures.insert(SortTextures::value_type(-texture->get_scale_pct(),
+						  texture));
+  }
+
+  bool any_surprises = false;
+
+  out << "\ntextures\n";
+  SortTextures::const_iterator sti;
+  for (sti = sort_textures.begin(); sti != sort_textures.end(); ++sti) {
+    Texture *texture = (*sti).second;
+    texture->write_size(out);
+    any_surprises = any_surprises || !texture->matched_anything();
+  }
+
+  if (any_surprises) {
+    // Some textures didn't match any commands; they're "surprise"
+    // textures.
+    out << "\nsurprises\n";
+    for (ti = _textures.begin(); ti != _textures.end(); ++ti) {
+      Texture *texture = (*ti).second;
+      if (!texture->matched_anything()) {
+	out << "  " << texture->get_name() << "\n";
+      }
+    }
+  }
+
+  if (!out) {
+    nout << "I/O error when writing to " << _pi_filename << "\n";
+    return false;
+  }
+
+  return true;
+}
+
+bool AttribFile::
+parse_params(const vector<string> &words, istream &infile, 
+	     string &line, int &line_num) {
+  if (words.size() != 1) {
+    nout << "Unexpected keywords on line.\n";
+    return false;
+  }
+
+  getline(infile, line);
+  line = trim_right(line);
+  line_num++;
+  while (!infile.eof() && !line.empty() && isspace(line[0])) {
+    string param, value;
+    extract_param_value(line, param, value);
+
+    if (param == "map_directory") {
+      _map_dirname = value;
+
+      // These are all deprecated.
+    } else if (param == "converted_directory") {
+    } else if (param == "convert_extension") {
+    } else if (param == "convert_command") {
+    } else if (param == "palette_prefix") {
+    } else if (param == "map_prefix") {
+
+    } else if (param == "pal_xsize") {
+      _pal_xsize = atoi(value.c_str());
+    } else if (param == "pal_ysize") {
+      _pal_ysize = atoi(value.c_str());
+    } else if (param == "default_margin") {
+      _default_margin = atoi(value.c_str());
+    } else if (param == "force_power_2") {
+      _force_power_2 = atoi(value.c_str());
+    } else if (param == "aggressively_clean_mapdir") {
+      _aggressively_clean_mapdir = atoi(value.c_str());
+    } else {
+      nout << "Unexpected keyword: " << param << "\n";
+      return false;
+    }
+
+    getline(infile, line);
+    line = trim_right(line);
+    line_num++;
+  }
+
+  return true;
+}
+
+bool AttribFile::
+parse_packing(const vector<string> &words, istream &infile, 
+	      string &line, int &line_num) {
+  if (words.size() != 3 || words[1] != "is" ||
+      (words[2] != "optimal" && words[2] != "suboptimal")) {
+    nout << "Expected 'packing is {optimal|suboptimal}'\n";
+    return false;
+  }
+
+  _optimal = (words[2] == "optimal");
+
+  getline(infile, line);
+  line_num++;
+  return true;
+}
+
+
+bool AttribFile::
+parse_texture(const vector<string> &words, istream &infile, 
+	      string &line, int &line_num) {
+  if (words.size() != 1) {
+    nout << "Unexpected words on line.\n";
+    return false;
+  }
+  
+  getline(infile, line);
+  line = trim_right(line);
+  line_num++;
+  while (!infile.eof() && !line.empty() && isspace(line[0])) {
+    vector<string> twords = extract_words(line);
+    if (twords.size() < 1) {
+      nout << "Expected texture name and additional parameters.\n";
+      return false;
+    }
+    Texture *texture = get_texture(twords[0]);
+
+    int kw = 1;
+    while (kw < (int)twords.size()) {
+      if (kw + 3 <= (int)twords.size() && twords[kw] == "orig") {
+	texture->set_size(atoi(twords[kw + 1].c_str()),
+			  atoi(twords[kw + 2].c_str()));
+	kw += 3;
+
+      } else if (kw + 3 <= (int)twords.size() && twords[kw] == "new") {
+	texture->set_last_req(atoi(twords[kw + 1].c_str()),
+			      atoi(twords[kw + 2].c_str()));
+	kw += 3;
+
+      } else if (twords[kw].find('%') != string::npos) {
+	// Ignore scale percentage.
+	kw++;
+
+      } else {
+	nout << "Unexpected keyword: " << twords[kw] << "\n";
+      }
+    }
+
+    getline(infile, line);
+    line = trim_right(line);
+    line_num++;
+  }
+
+  return true;
+}
+
+bool AttribFile::
+parse_pathname(const vector<string> &words, istream &infile, 
+	       string &line, int &line_num) {
+  if (words.size() != 1) {
+    nout << "Unexpected words on line.\n";
+    return false;
+  }
+  
+  getline(infile, line);
+  line = trim_right(line);
+  line_num++;
+  Texture *texture = NULL;
+
+  while (!infile.eof() && !line.empty() && isspace(line[0])) {
+    vector<string> twords = extract_words(line);
+    if (twords.size() == 1) {
+      // Only one word on the line means it's an alternate filename
+      // for the previous texture.
+      if (texture == NULL) {
+	nout << "Expected texture name and pathname.\n";
+	return false;
+      }
+      texture->add_filename(twords[0]);
+
+    } else if (twords.size() == 2) {
+      // Two words on the line means it's a texture name and filename.
+      texture = get_texture(twords[0]);
+      texture->add_filename(twords[1]);
+
+    } else {
+      // Anything else is a mistake.
+      nout << "Expected texture name and pathname.\n";
+      return false;
+    }
+
+    getline(infile, line);
+    line = trim_right(line);
+    line_num++;
+  }
+
+  return true;
+}
+
+bool AttribFile::
+parse_egg(const vector<string> &words, istream &infile, 
+	  string &line, int &line_num) {
+  if (words.size() != 2) {
+    nout << "Egg filename expected.\n";
+    return false;
+  }
+  
+  SourceEgg *egg = get_egg(words[1]);
+
+  getline(infile, line);
+  line = trim_right(line);
+  line_num++;
+  while (!infile.eof() && !line.empty() && isspace(line[0])) {
+    vector<string> twords = extract_words(line);
+    if (twords.size() < 1) {
+      nout << "Expected texture name\n";
+      return false;
+    }
+
+    string name = twords[0];
+    bool repeats = false;
+    bool alpha = false;
+
+    int kw = 1;
+    while (kw < (int)twords.size()) {
+      if (twords[kw] == "repeats") {
+	repeats = true;
+	kw++;
+
+      } else if (twords[kw] == "alpha") {
+	alpha = true;
+	kw++;
+
+      } else {
+	nout << "Unexpected keyword " << twords[kw] << "\n";
+	return false;
+      }
+    }
+
+    egg->add_texture(get_texture(name), repeats, alpha);
+
+    getline(infile, line);
+    line = trim_right(line);
+    line_num++;
+  }
+
+  return true;
+}
+  
+
+
+bool AttribFile::
+parse_palette(const vector<string> &words, istream &infile, 
+	      string &line, int &line_num) {
+  if (words.size() != 6) {
+    nout << "Palette filename, size, and number of components expected.\n";
+    return false;
+  }
+
+  string filename = words[1];
+  if (words[2] != "size") {
+    nout << "Expected keyword 'size'\n";
+    return false;
+  }
+  int xsize = atoi(words[3].c_str());
+  int ysize = atoi(words[4].c_str());
+  int components = atoi(words[5].c_str());
+
+  Palette *palette = new Palette(filename, xsize, ysize, components, this);
+  _palettes.push_back(palette);
+
+  getline(infile, line);
+  line = trim_right(line);
+  line_num++;
+  while (!infile.eof() && !line.empty() && isspace(line[0])) {
+    vector<string> twords = extract_words(line);
+    if (twords.size() != 9) {
+      nout << "Expected texture placement line.\n";
+      return false;
+    }
+
+    Texture *texture = get_texture(twords[0]);
+    
+    if (twords[1] != "at") {
+      nout << "Expected keyword 'at'\n";
+      return false;
+    }
+    int left = atoi(twords[2].c_str());
+    int top = atoi(twords[3].c_str());
+    
+    if (twords[4] != "size") {
+      nout << "Expected keyword 'size'\n";
+      return false;
+    }
+    int xsize = atoi(twords[5].c_str());
+    int ysize = atoi(twords[6].c_str());
+    
+    if (twords[7] != "margin") {
+      nout << "Expected keyword 'margin'\n";
+      return false;
+    }
+    int margin = atoi(twords[8].c_str());
+
+    palette->place_texture_at(texture, left, top, xsize, ysize, margin);
+
+    getline(infile, line);
+    line = trim_right(line);
+    line_num++;
+  }
+
+  return true;
+}
+
+
+  
+bool AttribFile::
+parse_unplaced(const vector<string> &words, istream &infile, 
+	       string &line, int &line_num) {
+  if (words.size() != 4) {
+    nout << "Unplaced texture description expected.\n";
+    return false;
+  }
+
+  Texture *texture = get_texture(words[1]);
+
+  if (words[2] != "because") {
+    nout << "Expected keyword 'because'\n";
+    return false;
+  }
+  
+  if (words[3] == "size") {
+    texture->set_omit(Texture::OR_size);
+  } else if (words[3] == "repeats") {
+    texture->set_omit(Texture::OR_repeats);
+  } else if (words[3] == "omitted") {
+    texture->set_omit(Texture::OR_omitted);
+  } else if (words[3] == "unused") {
+    texture->set_omit(Texture::OR_unused);
+  } else if (words[3] == "unknown") {
+    texture->set_omit(Texture::OR_unknown);
+  } else if (words[3] == "solitary") {
+    texture->set_omit(Texture::OR_solitary);
+  } else if (words[3] == "cmdline") {
+    texture->set_omit(Texture::OR_cmdline);
+  } else {
+    nout << "Unknown keyword " << words[3] << "\n";
+    return false;
+  }
+
+  getline(infile, line);
+  line_num++;
+  return true;
+}
+
+bool AttribFile::
+parse_surprises(const vector<string> &words, istream &infile, 
+		string &line, int &line_num) {
+  if (words.size() != 1) {
+    nout << "Unexpected words on line.\n";
+    return false;
+  }
+
+  // This is just the list of surprise textures from last time.  Its
+  // only purpose is to inform the user; we can completely ignore it.
+  
+  getline(infile, line);
+  line = trim_right(line);
+  line_num++;
+  while (!infile.eof() && !line.empty() && isspace(line[0])) {
+    getline(infile, line);
+    line = trim_right(line);
+    line_num++;
+  }
+
+  return true;
+}

+ 132 - 0
pandatool/src/egg-palettize/attribFile.h

@@ -0,0 +1,132 @@
+// Filename: attribFile.h
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef ATTRIBFILE_H
+#define ATTRIBFILE_H
+
+#include <pandatoolbase.h>
+
+#include <filename.h>
+
+#include <map>
+#include <vector>
+
+class UserAttribLine;
+class Texture;
+class Palette;
+class SourceEgg;
+class EggPalettize;
+
+////////////////////////////////////////////////////////////////////
+// 	 Class : AttribFile
+// Description : 
+////////////////////////////////////////////////////////////////////
+class AttribFile {
+public:
+  AttribFile(const Filename &filename);
+
+  string get_name() const;
+
+  bool grab_lock();
+  bool release_lock();
+
+  bool read();
+  bool write();
+
+  void update_params(EggPalettize *prog);
+
+  void get_req_sizes();
+  void update_texture_flags();
+
+  void repack_all_textures();
+  void repack_some_textures();
+  void optimal_resize();
+  void finalize_palettes();
+  void remove_unused_lines();
+  bool check_packing(bool force_optimal);
+  bool pack_texture(Texture *texture);
+  bool unpack_texture(Texture *texture);
+
+  void touch_dirty_egg_files(bool force_redo_all,
+			     bool eggs_include_images);
+
+  Texture *get_texture(const string &name);
+  void get_eligible_textures(vector<Texture *> &textures);
+  SourceEgg *get_egg(Filename name);
+
+  bool generate_palette_images();
+  bool transfer_unplaced_images(bool force_redo_all);
+
+  void check_dup_textures(map<string, Texture *> &textures,
+			  map<string, int> &dup_textures) const;
+
+  void collect_statistics(int &num_textures, int &num_placed,
+			  int &num_palettes,
+			  int &orig_size, int &resized_size, 
+			  int &palette_size, int &unplaced_size) const;
+
+private:
+  typedef vector<UserAttribLine *> UserLines;
+  UserLines _user_lines;
+
+  typedef map<string, SourceEgg *> Eggs;
+  Eggs _eggs;
+
+  typedef vector<Palette *> Palettes;
+  Palettes _palettes;
+
+  typedef map<string, Texture *> Textures;
+  Textures _textures;
+
+  string get_pi_filename(const string &txa_filename) const;
+
+  bool read_txa(istream &outfile);
+  bool read_pi(istream &outfile);
+  bool write_txa(ostream &outfile) const;
+  bool write_pi(ostream &outfile) const;
+
+  bool parse_params(const vector<string> &words, istream &infile, 
+		       string &line, int &line_num);
+  bool parse_packing(const vector<string> &words, istream &infile, 
+			string &line, int &line_num);
+  bool parse_texture(const vector<string> &words, istream &infile, 
+			string &line, int &line_num);
+  bool parse_pathname(const vector<string> &words, istream &infile, 
+			 string &line, int &line_num);
+  bool parse_egg(const vector<string> &words, istream &infile, 
+		    string &line, int &line_num);
+  bool parse_palette(const vector<string> &words, istream &infile, 
+			string &line, int &line_num);
+  bool parse_unplaced(const vector<string> &words, istream &infile, 
+			 string &line, int &line_num);
+  bool parse_surprises(const vector<string> &words, istream &infile, 
+			  string &line, int &line_num);
+
+  bool _optimal;
+  bool _txa_needs_rewrite;
+
+  string _name;
+  Filename _txa_filename;
+  Filename _pi_filename;
+
+
+public:
+  // These parameter values come from the command line, or from the
+  // .pi file if omitted from the command line.  These are the
+  // parameter values that specifically refer to textures and
+  // palettes, and thus should be stored in the .pi file for future
+  // reference.
+  Filename _map_dirname;
+  string _palette_prefix;
+  int _pal_xsize, _pal_ysize;
+  int _default_margin;
+  bool _force_power_2;
+  bool _aggressively_clean_mapdir;
+
+  int _txa_fd;
+  fstream _txa_fstrm;
+};
+
+#endif

+ 14 - 0
pandatool/src/egg-palettize/config_egg_palettize.cxx

@@ -0,0 +1,14 @@
+// Filename: config_egg_palettize.cxx
+// Created by:  drose (02Nov00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include <dconfig.h>
+
+#include "sourceEgg.h"
+
+Configure(config_egg_palettize);
+
+ConfigureFn(config_egg_palettize) {
+  SourceEgg::init_type();
+}

+ 592 - 0
pandatool/src/egg-palettize/eggPalettize.cxx

@@ -0,0 +1,592 @@
+// Filename: eggPalettize.cxx
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "eggPalettize.h"
+#include "attribFile.h"
+#include "texture.h"
+#include "string_utils.h"
+#include "sourceEgg.h"
+
+#include <pnmImage.h>
+#include <stdio.h>
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggPalettize::Constructor
+//       Access: Public
+//  Description: 
+////////////////////////////////////////////////////////////////////
+EggPalettize::
+EggPalettize() : EggMultiFilter(true) {
+  set_program_description
+    ("egg-palettize attempts to pack several texture maps from various models "
+     "together into one or more palette images, for improved rendering performance "
+     "and ease of texture management.  It can also resize textures on the fly, "
+     "whether or not they are actually placed on a palette.\n\n"
+     
+     "egg-palettize reads and writes an AttributesFile, which contains instructions "
+     "from the user about resizing particular textures, as well as the complete "
+     "information necessary to reconstruct the palettization from past runs, "
+     "including references to other egg files that may share this palette.  This "
+     "is designed to allow multiple egg files to use the same palette, without "
+     "having to process them all at once.\n\n"
+     
+     "Note that it is not even necessary to specify any egg files at all on the "
+     "command line; egg-palettize can be run on an existing AttributesFiles by "
+     "itself to freshen up a palette when necessary.");
+
+
+  clear_runlines();
+  add_runline("[opts] attribfile.txa file.egg [file.egg ...]");
+
+  add_option
+    ("s", "", 0, 
+     "Do not process anything, but report statistics on all palette "
+     "information files read.",
+     &EggPalettize::dispatch_none, &_statistics_only);
+  redescribe_option
+    ("d",
+     "The directory in which to write the palettized egg files.  This is "
+     "only necessary if more than one egg file is processed at the same "
+     "time; if it is included, each egg file will be processed and written "
+     "into the indicated directory.");
+  add_option
+    ("dm", "dirname", 0, 
+     "The directory in which to place all maps: generated palettes, "
+     "as well as images which were not placed on palettes "
+     "(but may have been resized).  It is often best if this is a "
+     "fully-qualified directory name rather than a relative directory name, "
+     "particularly if -d is used to write the egg files to a directory "
+     "different than the current directory, as the same name is written "
+     "into the egg files.",
+     &EggPalettize::dispatch_filename, &_got_map_dirname, &_map_dirname);
+  add_option
+    ("f", "", 0, 
+     "Force an optimal packing.  By default, textures are added to "
+     "existing palettes without disturbing them, which can lead to "
+     "suboptimal packing.  Including this switch forces the palettes "
+     "to be rebuilt if necessary to optimize the packing, but this "
+     "may invalidate other egg files which share this palette.",
+     &EggPalettize::dispatch_none, &_force_optimal);
+  add_option
+    ("F", "", 0, 
+     "Force a redo of everything.  This is useful in case something "
+     "has gotten out of sync and the old palettes are just bad.",
+     &EggPalettize::dispatch_none, &_force_redo_all);
+  add_option
+    ("R", "", 0, 
+     "Resize mostly-empty palettes to their minimal size.",
+     &EggPalettize::dispatch_none, &_optimal_resize);
+  add_option
+    ("t", "", 0, 
+     "Touch any additional egg files that share this palette and will "
+     "need to be refreshed, but were not included on the command "
+     "line.  Presumably a future make pass will cause them to be run "
+     "through egg-palettize again.",
+     &EggPalettize::dispatch_none, &_touch_eggs);
+  add_option
+    ("T", "", 0, 
+     "When touching egg files, consider an egg file to be invalidated "
+     "if textures have changed in any way, rather than simply moving "
+     "around within their palettes.  You should use this switch "
+     "if the texture images themselves are to be stored within bam files "
+     "generated from the eggs, or some such nonsense.",
+     &EggPalettize::dispatch_none, &_eggs_include_images);
+  add_option
+    ("C", "", 0, 
+     "Aggressively keep the map directory clean by deleting unused "
+     "textures from previous passes.",
+     &EggPalettize::dispatch_none, &_got_aggressively_clean_mapdir);
+  add_option
+    ("r", "", 0, 
+     "Respect any repeat/clamp flags given in the egg files.  The "
+     "default is to override a repeat flag if a texture's UV's don't "
+     "exceed the range [0..1].",
+     &EggPalettize::dispatch_none, &_dont_override_repeats);
+  add_option
+    ("z", "fuzz", 0, 
+     "The fuzz factor when applying the above repeat test.  UV's are "
+     "considered to be in the range [0..1] if they are actually in "
+     "the range [0-fuzz .. 1+fuzz].  The default is 0.01.",
+     &EggPalettize::dispatch_double, NULL, &_fuzz_factor);
+  add_option
+    ("m", "margin", 0, 
+     "Specify the default margin size.",
+     &EggPalettize::dispatch_int, &_got_default_margin, &_default_margin);
+  add_option
+    ("P", "x,y", 0, 
+     "Specify the default palette size.",
+     &EggPalettize::dispatch_int_pair, &_got_palette_size, _pal_size);
+  add_option
+    ("2", "", 0, 
+     "Force textures that have been left out of the palette to a size "
+     "which is an even power of 2.  They will be scaled down to "
+     "achieve this.",
+     &EggPalettize::dispatch_none, &_got_force_power_2);
+  add_option
+    ("x", "", 0, 
+     "Don't palettize anything, but do resize textures.",
+     &EggPalettize::dispatch_none, &_dont_palettize);
+  add_option
+    ("k", "", 0, 
+     "Kill lines from the attributes file that aren't used on any "
+     "texture.",
+     &EggPalettize::dispatch_none, &_remove_unused_lines);
+  add_option
+    ("H", "", 0, 
+     "Describe the syntax of the attributes file.",
+     &EggPalettize::dispatch_none, &_describe_input_file);
+  
+  _fuzz_factor = 0.01;
+  _aggressively_clean_mapdir = false;
+  _force_power_2 = false;
+}
+
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggPalettize::handle_args
+//       Access: Protected, Virtual
+//  Description: Does something with the additional arguments on the
+//               command line (after all the -options have been
+//               parsed).  Returns true if the arguments are good,
+//               false otherwise.
+////////////////////////////////////////////////////////////////////
+bool EggPalettize::
+handle_args(ProgramBase::Args &args) {
+  if (_describe_input_file) {
+    describe_input_file();
+    exit(1);
+  }
+
+
+  Args egg_names;
+  Args txa_names;
+
+  // First, collect all the filenames.
+  Args::iterator ai;
+  for (ai = args.begin(); ai != args.end(); ++ai) {
+    Filename filename = (*ai);
+    string ext = filename.get_extension();
+
+    if (ext == "egg") {
+      egg_names.push_back(filename);
+    } else if (ext == "txa" || ext == "pi") {
+      txa_names.push_back(filename);
+    } else {
+      nout << "Don't know what kind of file " << filename << " is.\n";
+      return false;
+    }
+  }
+
+  // Now sanity check them.  Either we have zero egg files, and one or
+  // more txa files, or we have some egg files and exactly one txa
+  // file.
+  if (egg_names.empty()) {
+    if (txa_names.empty()) {
+      nout << "No files specified.\n";
+      return false;
+    }
+
+    for (ai = txa_names.begin(); ai != txa_names.end(); ++ai) {
+      AttribFile *af = new AttribFile(*ai);
+      _attrib_files.push_back(af);
+    }
+
+  } else {
+    if (txa_names.empty()) {
+      nout << "Must include an attribs.txa file.\n";
+      return false;
+    }
+    if (txa_names.size() != 1) {
+      nout << "Can only read one attribs.txa file when processing egg files.\n";
+      return false;
+    }
+
+    AttribFile *af = new AttribFile(txa_names[0]);
+    _attrib_files.push_back(af);
+  }
+
+  return EggMultiFilter::handle_args(egg_names);
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggPalettize::read_egg
+//       Access: Protected, Virtual
+//  Description: Allocates and returns a new EggData structure that
+//               represents the indicated egg file.  If the egg file
+//               cannot be read for some reason, returns NULL. 
+//
+//               This can be overridden by derived classes to control
+//               how the egg files are read, or to extend the
+//               information stored with each egg structure, by
+//               deriving from EggData.
+////////////////////////////////////////////////////////////////////
+EggData *EggPalettize::
+read_egg(const Filename &filename) {
+  // We should only call this function if we have exactly one .txa file.
+  nassertr(_attrib_files.size() == 1, (EggData *)NULL);
+
+  SourceEgg *egg = _attrib_files[0]->get_egg(filename);
+  if (egg->read(filename)) {
+    return egg;
+  }
+
+  // Failure reading.
+  delete egg;
+  return (EggData *)NULL;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggPalettize::describe_input_file
+//       Access: Public
+//  Description: 
+////////////////////////////////////////////////////////////////////
+void EggPalettize::
+describe_input_file() {
+  nout <<
+    "An attributes file consists mostly of lines describing desired sizes of "
+    "texture maps.  The format resembles, but is not identical to, that of "
+    "the qtess input file.  Examples:\n\n"
+
+    "  texturename.rgb : 64 64\n"
+    "  texture-a.rgb texture-b.rgb : 32 16 2\n"
+    "  *.rgb : 50%\n"
+    "  eyelids.rgb : 16 16 omit\n\n"
+
+    "In general, each line consists of one or more filenames (and can "
+    "contain shell globbing characters like '*' or '?'), and a colon "
+    "followed by a size request.  For each texture appearing in an egg "
+    "file, the input list is scanned from the beginning and the first "
+    "line that matches the filename defines the size of the texture.\n\n"
+
+    "A size request may be either a pair of numbers, giving a specific x y "
+    "size of the texture, or it may be a scale factor in the form of a "
+    "percentage.  It may also include an additional number, giving a margin "
+    "for this particular texture (otherwise the default margin is "
+    "applied).  Finally, the keyword 'omit' may be included along with the "
+    "size to specify that the texture should not be placed in a palette.\n\n"
+
+    "There are some other special lines that may appear in this second, "
+    "along with the resize commands.  They begin with a colon to "
+    "distinguish them from the resize commands.  They are:\n\n"
+
+    ":palette xsize ysize\n\n"
+
+    "This specifies the size of the palette file(s) to be created.  It "
+    "overrides the -s command-line option.\n\n"
+
+    ":margin msize\n\n"
+
+    "This specifies the size of the default margin for all subsequent "
+    "resize commands.  This may appear several times in a given file.\n\n"
+
+    "Comments may appear freely throughout the file, and are set off by a "
+    "hash mark (#).\n";
+}
+
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggPalettize::format_space
+//       Access: Public
+//  Description:
+////////////////////////////////////////////////////////////////////
+string EggPalettize::
+format_space(int size_pixels, bool verbose) {
+  int size_bytes = size_pixels * 4;
+  int size_k = (size_bytes + 512) / 1024;
+  int mm_size_k = (size_bytes * 4 / 3 + 512) / 1024;
+  char str[128];
+  if (verbose) {
+    sprintf(str, "%dk, w/mm %dk", size_k, mm_size_k);
+  } else {
+    sprintf(str, "%dk, %dk", size_k, mm_size_k);
+  }
+  assert(strlen(str) < 128);
+
+  return str;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggPalettize::report_statistics
+//       Access: Public
+//  Description:
+////////////////////////////////////////////////////////////////////
+void EggPalettize::
+report_statistics() {
+  // Look for textures in common.
+  map<string, Texture *> textures;
+  map<string, int> dup_textures;
+
+  AttribFiles::iterator afi;
+  for (afi = _attrib_files.begin(); afi != _attrib_files.end(); ++afi) {
+    AttribFile &af = *(*afi);
+    af.check_dup_textures(textures, dup_textures);
+  }
+
+  if (dup_textures.empty()) {
+    cout << "\nEach texture appears in only one palette group.\n";
+  } else {
+    cout << "\nThe following textures appear in more than one palette group:\n";
+    int net_wasted_size = 0;
+    map<string, int>::const_iterator di;
+    for (di = dup_textures.begin(); di != dup_textures.end(); ++di) {
+      cout << "  " << (*di).first << " (" 
+	   << format_space((*di).second) << ")\n";
+      net_wasted_size += (*di).second;
+    }
+    cout << "Total wasted memory from common textures is "
+	 << format_space(net_wasted_size, true) << "\n";
+  }
+
+  int net_palette_size = 0;
+  int net_num_palettes = 0;
+
+  for (afi = _attrib_files.begin(); afi != _attrib_files.end(); ++afi) {
+    AttribFile &af = *(*afi);
+    int num_textures, num_placed, num_palettes;
+    int orig_size, resized_size, palette_size, unplaced_size;
+    af.collect_statistics(num_textures, num_placed, num_palettes,
+			  orig_size, resized_size, 
+			  palette_size, unplaced_size);
+    cout << "\nPalette group " << af.get_name() << ":\n"
+	 << "   " << num_textures << " textures, " 
+	 << num_placed << " of which are packed onto " 
+	 << num_palettes << " palettes (" << num_textures - num_placed
+	 << " unplaced)"
+	 << "\n   Original texture memory: " 
+	 << format_space(orig_size, true)
+	 << "\n   After resizing:          " 
+	 << format_space(resized_size, true)
+	 << "\n   After palettizing:       " 
+	 << format_space(palette_size + unplaced_size, true)
+	 << "\n";
+
+    net_palette_size += palette_size;
+    net_num_palettes += num_palettes;
+  }
+
+  int net_num_textures = textures.size();
+  int net_num_placed = 0;
+  int net_orig_size = 0;
+  int net_resized_size = 0;
+  int net_unplaced_size = 0;
+  typedef map<Texture::OmitReason, pair<int, int> > UnplacedReasons;
+  UnplacedReasons unplaced_reasons;
+
+  map<string, Texture *>::iterator ti;
+  for (ti = textures.begin(); ti != textures.end(); ++ti) {
+    Texture *texture = (*ti).second;
+
+    int xsize, ysize;
+    int rxsize, rysize;
+    int rsize = 0;
+    if (texture->get_size(xsize, ysize) && 
+	texture->get_last_req(rxsize, rysize)) {
+      net_orig_size += xsize * ysize;
+      net_resized_size += rxsize * rysize;
+      rsize = rxsize * rysize;
+    }
+
+
+    if (texture->is_really_packed()) {
+      net_num_placed++;
+
+    } else {
+      net_unplaced_size += rsize;
+
+      // Here's an unplaced texture; add its size to the unplaced
+      // reasons table.
+      UnplacedReasons::iterator uri = 
+	unplaced_reasons.find(texture->get_omit());
+      if (uri == unplaced_reasons.end()) {
+	unplaced_reasons.insert
+	  (UnplacedReasons::value_type(texture->get_omit(), 
+				       pair<int, int>(1, rsize)));
+      } else {
+	(*uri).second.first++;
+	(*uri).second.second += rsize;
+      }
+    }
+  }
+
+  cout << "\nOverall:\n"
+       << "   " << net_num_textures << " textures, " 
+       << net_num_placed << " of which are packed onto " 
+       << net_num_palettes << " palettes (" 
+       << net_num_textures - net_num_placed << " unplaced)"
+       << "\n   Original texture memory: "
+       << format_space(net_orig_size, true)
+       << "\n   After resizing:          " 
+       << format_space(net_resized_size, true)
+       << "\n   After palettizing:       " 
+       << format_space(net_palette_size + net_unplaced_size, true)
+       << "\n\n";
+
+  UnplacedReasons::iterator uri;
+  for (uri = unplaced_reasons.begin(); 
+       uri != unplaced_reasons.end();
+       ++uri) {
+    Texture::OmitReason reason = (*uri).first;
+    int count = (*uri).second.first;
+    int size = (*uri).second.second;
+    cout << count << " textures (" << format_space(size)
+	 << ") unplaced because ";
+    switch (reason) {
+    case Texture::OR_none:
+      cout << "of no reason--textures should have been placed\n";
+      break;
+      
+    case Texture::OR_size:
+      cout << "size was too large for palette\n";
+      break;
+      
+    case Texture::OR_repeats:
+      cout << "repeating\n";
+      break;
+      
+    case Texture::OR_omitted:
+      cout << "explicitly omitted\n";
+      break;
+      
+    case Texture::OR_unused:
+      cout << "unused by any egg file\n";
+      break;
+      
+    case Texture::OR_unknown:
+      cout << "texture file is missing\n";
+      break;
+      
+    case Texture::OR_cmdline:
+      cout << "-x was given on command line\n";
+      break;
+      
+    case Texture::OR_solitary:
+      cout << "texture was alone on a palette\n";
+      break;
+      
+    default:
+      cout << "unknown reason\n";
+    }
+  }
+
+  cout << "\n";
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggPalettize::run
+//       Access: Public
+//  Description:
+////////////////////////////////////////////////////////////////////
+void EggPalettize::
+run() {
+  _force_power_2 = _got_force_power_2;
+  _aggressively_clean_mapdir = _got_aggressively_clean_mapdir;
+
+  bool okflag = true;
+
+  // We'll repeat the processing steps for each attrib file.  If we
+  // have multiple attrib files, then we have no egg files, so all of
+  // the egg loops below fall out.  On the other hand, if we do have
+  // egg files, then we have only one attrib file, so this outer loop
+  // falls out.
+
+  AttribFiles::iterator afi;
+  for (afi = _attrib_files.begin(); afi != _attrib_files.end(); ++afi) {
+    AttribFile &af = *(*afi);
+
+    if (!af.grab_lock()) {
+      // Failing to grab the write lock on the attribute file is a
+      // fatal error.
+      exit(1);
+    }
+
+    if (_statistics_only) {
+      nout << "Reading " << af.get_name() << "\n";
+      okflag = af.read();
+
+    } else {
+      nout << "Processing " << af.get_name() << "\n";
+
+      if (!af.read()) {
+	// Failing to read the attribute file is a fatal error.
+	exit(1);
+      }
+      
+      af.update_params(this);
+      
+      // Get all the texture references out of the egg files.
+      Eggs::iterator ei;
+      for (ei = _eggs.begin(); ei != _eggs.end(); ++ei) {
+	SourceEgg *egg = DCAST(SourceEgg, *ei);
+	egg->get_textures(af, this);
+      }
+      
+      // Apply the user's requested size changes onto the textures.
+      af.get_req_sizes();
+      af.update_texture_flags();
+      
+      if (!af.check_packing(_force_optimal) || _force_redo_all) {
+	// We need some more palettization work here.
+	if (_force_redo_all || _force_optimal) {
+	  af.repack_all_textures();
+	} else {
+	  af.repack_some_textures();
+	}
+	
+      } else {
+	nout << "No changes to palette arrangement are required.\n";
+      }
+      
+      af.finalize_palettes();
+      
+      if (_optimal_resize) {
+	af.optimal_resize();
+      }
+      
+      if (_remove_unused_lines) {
+	af.remove_unused_lines();
+      }
+      
+      if (!af.write()) {
+	// Failing to rewrite the attribute file is a fatal error.
+	exit(1);
+      }
+      
+      // And rebuild whatever images are necessary.
+      okflag = af.generate_palette_images() && okflag;
+      okflag = af.transfer_unplaced_images(_force_redo_all) && okflag;
+      
+      // Now apply the palettization effects to the egg files.
+      for (ei = _eggs.begin(); ei != _eggs.end(); ++ei) {
+	SourceEgg *egg = DCAST(SourceEgg, *ei);
+	egg->update_trefs();
+      }
+      
+      // Write out the egg files.
+      write_eggs();
+      
+      if (_touch_eggs) {
+	af.touch_dirty_egg_files(_force_redo_all, _eggs_include_images);
+      }
+    }
+
+    okflag = af.release_lock() && okflag;
+  }
+
+  if (_statistics_only) {
+    report_statistics();
+  }
+
+  if (!okflag) {
+    exit(1);
+  }
+}
+
+int 
+main(int argc, char *argv[]) {
+  EggPalettize prog;
+  prog.parse_command_line(argc, argv);
+  prog.run();
+  return 0;
+}

+ 71 - 0
pandatool/src/egg-palettize/eggPalettize.h

@@ -0,0 +1,71 @@
+// Filename: eggPalettize.h
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef EGGPALETTIZE_H
+#define EGGPALETTIZE_H
+
+#include <pandatoolbase.h>
+
+#include "attribFile.h"
+#include <eggMultiFilter.h>
+
+#include <vector>
+
+class Texture;
+
+////////////////////////////////////////////////////////////////////
+// 	 Class : EggPalettize
+// Description : This is the heart of the program.
+////////////////////////////////////////////////////////////////////
+class EggPalettize : public EggMultiFilter {
+public:
+  EggPalettize();
+
+  virtual bool handle_args(Args &args);
+  virtual EggData *read_egg(const Filename &filename);
+
+  void describe_input_file();
+
+  string format_space(int size_pixels, bool verbose = false);
+  void report_statistics();
+  void run();
+
+  typedef vector<AttribFile *> AttribFiles;
+  AttribFiles _attrib_files;
+
+  // The following parameter values specifically relate to textures
+  // and palettes.  These values are store in the .pi file for future
+  // reference.
+  Filename _map_dirname;
+  bool _got_map_dirname;
+  int _pal_size[2];
+  bool _got_palette_size;
+  int _default_margin;
+  bool _got_default_margin;
+  bool _force_power_2;
+  bool _got_force_power_2;
+  bool _aggressively_clean_mapdir;
+  bool _got_aggressively_clean_mapdir;
+
+  // The following values relate specifically to egg files.  They're
+  // not saved for future sessions.
+  double _fuzz_factor;
+  bool _dont_override_repeats;
+
+  // The following values control behavior specific to this session.
+  // They're not saved either.
+  bool _statistics_only;
+  bool _force_optimal;
+  bool _force_redo_all;
+  bool _optimal_resize;
+  bool _touch_eggs;
+  bool _eggs_include_images;
+  bool _dont_palettize;
+  bool _remove_unused_lines;
+
+  bool _describe_input_file;
+};
+
+#endif

+ 14 - 0
pandatool/src/egg-palettize/imageFile.cxx

@@ -0,0 +1,14 @@
+// Filename: imageFile.cxx
+// Created by:  drose (07Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "imageFile.h"
+
+ImageFile::
+ImageFile() {
+}
+
+ImageFile::
+~ImageFile() {
+}

+ 28 - 0
pandatool/src/egg-palettize/imageFile.h

@@ -0,0 +1,28 @@
+// Filename: imageFile.h
+// Created by:  drose (07Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef IMAGEFILE_H
+#define IMAGEFILE_H
+
+#include <pandatoolbase.h>
+
+#include <filename.h>
+
+////////////////////////////////////////////////////////////////////
+// 	 Class : ImageFile
+// Description : This is the base class for both Palette and Texture.
+////////////////////////////////////////////////////////////////////
+class ImageFile {
+public:
+  ImageFile();
+  virtual ~ImageFile();
+
+  virtual Filename get_filename() const=0;
+  virtual Filename get_basename() const=0;
+
+};
+
+#endif
+

+ 544 - 0
pandatool/src/egg-palettize/palette.cxx

@@ -0,0 +1,544 @@
+// Filename: palette.cxx
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "palette.h"
+#include "texture.h"
+#include "attribFile.h"
+#include "string_utils.h"
+
+#include <notify.h>
+#include <pnmImage.h>
+
+bool Palette::TexturePlacement::
+intersects(int hleft, int htop, int xsize, int ysize) const {
+  int hright = hleft + xsize;
+  int hbot = htop + ysize;
+
+  int mright = _left + _xsize;
+  int mbot = _top + _ysize;
+
+  return 
+    !(hleft >= mright || hright <= _left ||
+      htop >= mbot || hbot <= _top);
+}
+
+PNMImage *Palette::TexturePlacement::
+resize_image(PNMImage *source) const {
+  // Compute need_*size to account for the (interior) margins.
+  // However, if the size of the texture if very small, we want to
+  // make exterior margins, so we won't reduce the size past a certain
+  // point.
+  int need_xsize = min(_xsize, max(_xsize - 2 * _margin, 8));
+  int need_ysize = min(_ysize, max(_ysize - 2 * _margin, 8));
+
+  if (source->get_x_size() == need_xsize && source->get_y_size() == need_ysize) {
+    // The image is already the right size.
+    return source;
+  }
+
+  PNMImage *new_image =
+    new PNMImage(need_xsize, need_ysize, source->get_color_type());
+  new_image->gaussian_filter_from(0.5, *source);
+  delete source;
+  return new_image;
+}
+
+PNMImage *Palette::TexturePlacement::
+add_margins(PNMImage *source) const {
+  if (_margin == 0) {
+    // No margins to add.
+    return source;
+  }
+
+  int orig_xsize = source->get_x_size();
+  int orig_ysize = source->get_y_size();
+
+  int need_xsize = orig_xsize + _margin * 2;
+  int need_ysize = orig_ysize + _margin * 2;
+
+  PNMImage *new_image =
+    new PNMImage(need_xsize, need_ysize, source->get_color_type());
+  new_image->copy_sub_image(*source, _margin, _margin);
+  
+  // Now extend the margin out to the sides.
+  for (int i = 0; i < _margin; i++) {
+    for (int x = 0; x < orig_xsize; x++) {
+      // top edge
+      new_image->set_xel(x + _margin, i, source->get_xel(x, 0));
+      // bottom edge
+      new_image->set_xel(x + _margin, need_ysize - 1 - i, 
+			 source->get_xel(x, orig_ysize - 1));
+    }
+    for (int y = 0; y < orig_ysize; y++) {
+      // left edge
+      new_image->set_xel(i, y + _margin, source->get_xel(0, y));
+      // right edge
+      new_image->set_xel(need_xsize - 1 - i, y + _margin, 
+			 source->get_xel(orig_xsize - 1, y));
+    }
+
+    for (int j = 0; j < _margin; j++) {
+      // top-left corner
+      new_image->set_xel(i, j, source->get_xel(0, 0)); 
+      // top-right corner
+      new_image->set_xel(need_xsize - 1 - i, j, 
+			 source->get_xel(orig_xsize - 1, 0));
+      // bottom-left corner
+      new_image->set_xel(i, need_ysize - 1 - j, 
+			 source->get_xel(0, orig_ysize - 1)); 
+      // bottom-right corner
+      new_image->set_xel(need_xsize - 1 - i, need_ysize - 1 - j,
+			 source->get_xel(orig_xsize - 1, orig_ysize - 1));
+    }
+  }
+
+  if (source->has_alpha()) {
+    // Now do the same thing in the alpha channel.
+    for (int i = 0; i < _margin; i++) {
+      for (int x = 0; x < orig_xsize; x++) {
+	// top edge
+	new_image->set_alpha(x + _margin, i, source->get_alpha(x, 0));
+	// bottom edge
+	new_image->set_alpha(x + _margin, need_ysize - 1 - i, 
+			     source->get_alpha(x, orig_ysize - 1));
+      }
+      for (int y = 0; y < orig_ysize; y++) {
+	// left edge
+	new_image->set_alpha(i, y + _margin, source->get_alpha(0, y));
+	// right edge
+	new_image->set_alpha(need_xsize - 1 - i, y + _margin, 
+			     source->get_alpha(orig_xsize - 1, y));
+      }
+      
+      for (int j = 0; j < _margin; j++) {
+	// top-left corner
+	new_image->set_alpha(i, j, source->get_alpha(0, 0)); 
+	// top-right corner
+	new_image->set_alpha(need_xsize - 1 - i, j, 
+			     source->get_alpha(orig_xsize - 1, 0));
+	// bottom-left corner
+	new_image->set_alpha(i, need_ysize - 1 - j, 
+			     source->get_alpha(0, orig_ysize - 1)); 
+	// bottom-right corner
+	new_image->set_alpha(need_xsize - 1 - i, need_ysize - 1 - j,
+			     source->get_alpha(orig_xsize - 1, orig_ysize - 1));
+      }
+    }
+  }
+
+  delete source;
+  return new_image;
+}
+  
+
+
+Palette::
+Palette(const Filename &filename, int xsize, int ysize, int components,
+	AttribFile *af) :
+  _filename(filename),
+  _xsize(xsize),
+  _ysize(ysize),
+  _components(components),
+  _attrib_file(af)
+{
+  _index = -1;
+  _palette_changed = false;
+  _new_palette = false;
+}
+
+Palette::
+Palette(int index, int xsize, int ysize, int components, AttribFile *af) :
+  _index(index),
+  _xsize(xsize),
+  _ysize(ysize),
+  _components(components),
+  _attrib_file(af)
+{
+  _palette_changed = false;
+  _new_palette = true;
+}
+
+Palette::
+~Palette() {
+  // Unmark any textures we've had packed.
+  TexPlace::iterator ti;
+  for (ti = _texplace.begin(); ti != _texplace.end(); ++ti) {
+    (*ti)._texture->mark_unpacked();
+  }
+}
+
+Filename Palette::
+get_filename() const {
+  return _filename;
+}
+
+Filename Palette::
+get_basename() const {
+  return _basename;
+}
+
+bool Palette::
+changed() const {
+  return _palette_changed;
+}
+
+bool Palette::
+new_palette() const {
+  return _new_palette;
+}
+
+int Palette::
+get_num_textures() const {
+  return _texplace.size();
+}
+
+bool Palette::
+check_uses_alpha() const {
+  // Returns true if any texture in the palette uses alpha.
+  TexPlace::const_iterator ti;
+  for (ti = _texplace.begin(); ti != _texplace.end(); ++ti) {
+    if ((*ti)._texture->uses_alpha()) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+void Palette::
+get_size(int &xsize, int &ysize) const {
+  xsize = _xsize;
+  ysize = _ysize;
+}
+  
+
+void Palette::
+place_texture_at(Texture *texture, int left, int top,
+		 int xsize, int ysize, int margin) {
+  TexturePlacement tp;
+  tp._texture = texture;
+  tp._left = left;
+  tp._top = top;
+  tp._xsize = xsize;
+  tp._ysize = ysize;
+  tp._margin = margin;
+  _texplace.push_back(tp);
+
+  texture->mark_pack_location(this, left, top, xsize, ysize, margin);
+}
+
+bool Palette::
+pack_texture(Texture *texture) {
+  int xsize, ysize;
+  if (!texture->get_req(xsize, ysize)) {
+    return false;
+  }
+  
+  int left, top;
+  if (find_home(left, top, xsize, ysize)) {
+    place_texture_at(texture, left, top, xsize, ysize, texture->get_margin());
+    _palette_changed = true;
+    return true;
+  }
+  return false;
+}
+
+bool Palette::
+unpack_texture(Texture *texture) {
+  TexPlace::iterator ti;
+  for (ti = _texplace.begin(); ti != _texplace.end(); ++ti) {
+    if ((*ti)._texture == texture) {
+      _texplace.erase(ti);
+      texture->mark_unpacked();
+      return true;
+    }
+  }
+  return false;
+}
+
+// Attempt to shrink the palette down to the smallest possible (power
+// of 2) size that includes all its textures.
+void Palette::
+optimal_resize() {
+  if (_texplace.size() < 2) {
+    // If we don't have at least two textures, there's no point in
+    // shrinking the palette because we won't be using it anyway.
+    // Might as well keep it full-sized so we can better pack future
+    // textures.
+    return;
+  }
+
+  int max_y = get_max_y();
+  assert(max_y > 0);
+  bool resized = false;
+
+  while (max_y <= _ysize / 2) {
+    // If at least half the palette is empty, we can try resizing.
+    if (max_y <= _ysize / 4) {
+      // Wow, three-quarters of the palette is empty.
+      Palette *np = try_resize(_xsize / 2, _ysize / 2);
+      if (np != NULL) {
+	// Excellent!  We just reduced the palette size by half in
+	// both dimensions.
+	_texplace = np->_texplace;
+	_xsize = _xsize / 2;
+	_ysize = _ysize / 2;
+	_palette_changed = true;
+	delete np;
+      } else {
+	// Well, we couldn't reduce in both dimensions, but we can
+	// always reduce twice in the y dimension.
+	_ysize = _ysize / 4;
+      }
+    } else {
+      _ysize = _ysize / 2;
+    }
+    resized = true;
+    max_y = get_max_y();
+  }
+
+  assert(get_max_y() <= _ysize);
+
+  if (resized) {
+    nout << "Resizing " << get_basename() << " to " << _xsize << " " 
+	 << _ysize << "\n";
+    // If we've resized the palette, all the egg files that reference
+    // these textures will need to be rebuilt.
+    TexPlace::iterator ti;
+    for (ti = _texplace.begin(); ti != _texplace.end(); ++ti) {
+      (*ti)._texture->set_changed(true);
+    }
+
+    // And we'll have to mark the palette as a new image.
+    _new_palette = true;
+  }
+}
+
+
+void Palette::
+finalize_palette() {
+  if (_filename.empty()) {
+    // Generate a filename based on the index number.
+    char index_str[128];
+    sprintf(index_str, "%03d", _index);
+    _basename = _attrib_file->_palette_prefix + index_str + ".rgb";
+    _filename = _basename;
+    _filename.set_dirname(_attrib_file->_map_dirname);
+  } else {
+    _basename = _filename.get_basename();
+  }
+
+  _components = check_uses_alpha() ? 4 : 3;
+
+  if (_texplace.size() == 1) {
+    // If we packed exactly one texture, never mind.
+    Texture *texture = (*_texplace.begin())._texture;
+
+    // This is a little odd: we mark the texture as being omitted, but
+    // we don't actually unpack it.  That way it will still be
+    // recorded as belonging to this palette (for future
+    // palettizations), but it will also be copied to the map
+    // directory, and any egg files that reference it will use the
+    // texture and not the palette.
+    texture->set_omit(Texture::OR_solitary);
+  }
+}
+
+void Palette::
+write(ostream &out) const {
+  out << "palette " << _filename
+      << " size " << _xsize << " " << _ysize << " " << _components
+      << "\n";
+  TexPlace::const_iterator ti;
+  for (ti = _texplace.begin(); ti != _texplace.end(); ++ti) {
+    out << "  " << (*ti)._texture->get_name()
+	<< " at " << (*ti)._left << " " << (*ti)._top
+	<< " size " << (*ti)._xsize << " " << (*ti)._ysize
+	<< " margin " << (*ti)._margin
+	<< "\n";
+  }
+}
+
+bool Palette::
+generate_image() {
+  bool okflag = true;
+
+  PNMImage palette(_xsize, _ysize, _components);
+
+  nout << "Generating " << _filename << "\n";
+
+  TexPlace::const_iterator ti;
+  for (ti = _texplace.begin(); ti != _texplace.end(); ++ti) {
+    Texture *texture = (*ti)._texture;
+    nout << "  " << texture->get_name() << "\n";
+    okflag = copy_texture_image(palette, *ti) && okflag;
+  }
+
+  nout << "Writing " << _filename << "\n";
+  if (!palette.write(_filename)) {
+    nout << "Error in writing.\n";
+    okflag = false;
+  }
+
+  return okflag;
+}
+
+bool Palette::
+refresh_image() {
+  if (!_filename.exists()) {
+    nout << "Palette image " << _filename << " does not exist, rebuilding.\n";
+    return generate_image();
+  }
+
+  bool okflag = true;
+
+  PNMImage palette;
+  if (!palette.read(_filename)) {
+    nout << "Unable to read old palette image " << _filename 
+	 << ", rebuilding.\n";
+    return generate_image();
+  }
+
+  bool any_changed = false;
+
+  if (_components == 4 && !palette.has_alpha()) {
+    palette.add_alpha();
+    palette.alpha_fill(1.0);
+    any_changed = true;
+
+  } else if (_components == 3 && palette.has_alpha()) {
+    palette.remove_alpha();
+    any_changed = true;
+  }
+
+  TexPlace::const_iterator ti;
+  for (ti = _texplace.begin(); ti != _texplace.end(); ++ti) {
+    Texture *texture = (*ti)._texture;
+    if (texture->needs_refresh()) {
+      if (!any_changed) {
+	nout << "Refreshing " << _filename << "\n";
+	any_changed = true;
+      }
+
+      nout << "  " << texture->get_name() << "\n";
+      okflag = copy_texture_image(palette, *ti) && okflag;
+    }
+  }
+
+  if (any_changed) {
+    nout << "Writing " << _filename << "\n";
+    if (!palette.write(_filename)) {
+      nout << "Error in writing.\n";
+      okflag = false;
+    }
+  }
+
+  return okflag;
+}
+
+
+Palette *Palette::
+try_resize(int new_xsize, int new_ysize) const {
+  Palette *np =
+    new Palette(_index, new_xsize, new_ysize,
+		_components, _attrib_file);
+
+  bool okflag = true;
+  TexPlace::const_iterator ti;
+  for (ti = _texplace.begin(); 
+       ti != _texplace.end() && okflag;
+       ++ti) {
+    okflag = np->pack_texture((*ti)._texture);
+  }
+
+  if (okflag) {
+    return np;
+  } else {
+    delete np;
+    return NULL;
+  }
+}
+
+int Palette::
+get_max_y() const {
+  int max_y = 0;
+
+  TexPlace::const_iterator ti;
+  for (ti = _texplace.begin(); ti != _texplace.end(); ++ti) {
+    max_y = max(max_y, (*ti)._top + (*ti)._ysize);
+  }
+
+  return max_y;
+}
+
+bool Palette::
+find_home(int &left, int &top, int xsize, int ysize) const {
+  top = 0;
+  while (top + ysize <= _ysize) {
+    int next_y = _ysize;
+    // Scan along the row at 'top'.
+    left = 0;
+    while (left + xsize <= _xsize) {
+      int next_x = left;
+      // Consider the spot at left, top.
+
+      // Can we place it here?
+      bool place_ok = true;
+      TexPlace::const_iterator ti;
+      for (ti = _texplace.begin(); 
+	   ti != _texplace.end() && place_ok;
+	   ++ti) {
+	if ((*ti).intersects(left, top, xsize, ysize)) {
+	  // Nope.
+	  place_ok = false;
+	  next_x = (*ti)._left + (*ti)._xsize;
+	  next_y = min(next_y, (*ti)._top + (*ti)._ysize);
+	}
+      }
+      
+      if (place_ok) {
+	// Hooray!
+	return true;
+      }
+
+      assert(next_x > left);
+      left = next_x;
+    }
+
+    assert(next_y > top);
+    top = next_y;
+  }
+
+  // Nope, wouldn't fit anywhere.
+  return false;
+}
+
+bool Palette::
+copy_texture_image(PNMImage &palette, const TexturePlacement &tp) {
+  bool okflag = true;
+  PNMImage *image = tp._texture->read_image();
+  if (image == NULL) {
+    nout << "  *** Unable to read " << tp._texture->get_name() << "\n";
+    okflag = false;
+    
+    // Create a solid red texture for images we can't read.
+    image = new PNMImage(tp._xsize, tp._ysize);
+    image->fill(1.0, 0.0, 0.0);
+    
+  } else {
+    image = tp.add_margins(tp.resize_image(image));
+  }
+  
+  if (_components == 4 && !image->has_alpha()) {
+    // We need to add an alpha channel for the image if the
+    // palette has an alpha channel.
+    image->add_alpha();
+    image->alpha_fill(1.0);
+  }
+  palette.copy_sub_image(*image, tp._left, tp._top);
+  delete image;
+
+  return okflag;
+}

+ 90 - 0
pandatool/src/egg-palettize/palette.h

@@ -0,0 +1,90 @@
+// Filename: palette.h
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef PALETTE_H
+#define PALETTE_H
+
+#include <pandatoolbase.h>
+
+#include "imageFile.h"
+
+#include <filename.h>
+
+#include <vector>
+
+class Texture;
+class PNMImage;
+class AttribFile;
+
+////////////////////////////////////////////////////////////////////
+// 	 Class : Palette
+// Description : 
+////////////////////////////////////////////////////////////////////
+class Palette : public ImageFile {
+public:
+  Palette(const Filename &filename, int xsize, int ysize, int components,
+	  AttribFile *af);
+  Palette(int index, int xsize, int ysize, int components,
+	  AttribFile *af);
+  ~Palette();
+
+  virtual Filename get_filename() const;
+  virtual Filename get_basename() const;
+
+  bool changed() const;
+  bool new_palette() const;
+  int get_num_textures() const;
+
+  bool check_uses_alpha() const;
+
+  void get_size(int &xsize, int &ysize) const;
+
+  void place_texture_at(Texture *texture, int left, int top,
+			int xsize, int ysize, int margin);
+
+  bool pack_texture(Texture *texture);
+  bool unpack_texture(Texture *texture);
+
+  void optimal_resize();
+
+  void finalize_palette();
+  
+  void write(ostream &out) const;
+
+  bool generate_image();
+  bool refresh_image();
+
+private:  
+  class TexturePlacement {
+  public:
+    bool intersects(int left, int top, int xsize, int ysize) const;
+    PNMImage *resize_image(PNMImage *source) const;
+    PNMImage *add_margins(PNMImage *source) const;
+
+    Texture *_texture;
+    int _left, _top;
+    int _xsize, _ysize, _margin;
+  };
+
+  Palette *try_resize(int new_xsize, int new_ysize) const;
+  int get_max_y() const;
+  bool find_home(int &left, int &top, int xsize, int ysize) const;
+  bool copy_texture_image(PNMImage &palette, const TexturePlacement &tp);
+
+  
+  typedef vector<TexturePlacement> TexPlace;
+  TexPlace _texplace;
+
+  Filename _filename;
+  Filename _basename;
+  int _index;
+  int _xsize, _ysize, _components;
+  bool _palette_changed;
+  bool _new_palette;
+  
+  AttribFile *_attrib_file;
+};
+
+#endif

+ 347 - 0
pandatool/src/egg-palettize/sourceEgg.cxx

@@ -0,0 +1,347 @@
+// Filename: sourceEgg.cxx
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "sourceEgg.h"
+#include "texture.h"
+#include "eggPalettize.h"
+#include "string_utils.h"
+#include "palette.h"
+
+#include <eggAttributes.h>
+#include <eggVertex.h>
+#include <eggNurbsSurface.h>
+#include <eggPrimitive.h>
+#include <eggTextureCollection.h>
+
+TypeHandle SourceEgg::_type_handle;
+
+SourceEgg::TextureRef::
+TextureRef(Texture *texture, bool repeats, bool alpha) :
+  _texture(texture),
+  _repeats(repeats),
+  _alpha(alpha) 
+{
+  _eggtex = NULL;
+}
+
+SourceEgg::
+SourceEgg() {
+}
+
+SourceEgg::TextureRef &SourceEgg::
+add_texture(Texture *texture, bool repeats, bool alpha) {
+  _texrefs.push_back(TextureRef(texture, repeats, alpha));
+  return _texrefs.back();
+}
+
+void SourceEgg::
+get_textures(AttribFile &af, EggPalettize *prog) {
+  _texrefs.clear();
+
+  EggTextureCollection tc;
+  tc.find_used_textures(this);
+
+  EggTextureCollection::iterator ti;
+  for (ti = tc.begin(); ti != tc.end(); ++ti) {
+    EggTexture *eggtex = (*ti);
+    string name = eggtex->get_basename();
+    
+    Texture *texture = af.get_texture(name);
+    texture->add_filename(*eggtex);
+
+    if (prog->_dont_palettize) {
+      // If the user specified -x, it means to omit all textures
+      // processed in this run, forever.
+      texture->set_omit(Texture::OR_cmdline);
+    } else {
+      // Or until we next see it without -x.
+      if (texture->get_omit() == Texture::OR_cmdline) {
+	texture->set_omit(Texture::OR_none);
+      }
+    }
+
+    bool repeats = 
+      eggtex->get_wrap_mode() == EggTexture::WM_repeat ||
+      eggtex->get_wrap_u() == EggTexture::WM_repeat ||
+      eggtex->get_wrap_v() == EggTexture::WM_repeat;
+
+    bool repeat_unspecified = 
+      eggtex->get_wrap_mode() == EggTexture::WM_unspecified &&
+      eggtex->get_wrap_u() == EggTexture::WM_unspecified &&
+      eggtex->get_wrap_v() == EggTexture::WM_unspecified;
+
+    bool alpha = true; //eggtex->uses_alpha();
+
+    // Check the range of UV's actually used within the egg file.
+    bool any_uvs = false;
+    TexCoordd min_uv, max_uv;
+    get_uv_range(this, eggtex, any_uvs, min_uv, max_uv);
+
+    // Now we need to apply the texture matrix, if there is one, to
+    // our bounding box UV's.
+    
+    if (eggtex->has_transform()) {
+      // Transforming a bounding box by a matrix requires transforming
+      // all four corners.
+
+      TexCoordd a(min_uv[0], min_uv[1]);
+      TexCoordd b(min_uv[0], max_uv[1]);
+      TexCoordd c(max_uv[0], max_uv[1]);
+      TexCoordd d(max_uv[0], min_uv[1]);
+
+      LMatrix3d transform = eggtex->get_transform();
+
+      a = a * transform;
+      b = b * transform;
+      c = c * transform;
+      d = d * transform;
+
+      // Now boil down these four corners into a new bounding box.
+
+      min_uv.set(min(min(a[0], b[0]), min(c[0], d[0])),
+		 min(min(a[1], b[1]), min(c[1], d[1])));
+      max_uv.set(max(max(a[0], b[0]), max(c[0], d[0])),
+		 max(max(a[1], b[1]), max(c[1], d[1])));
+    }
+
+    bool truly_repeats =
+      (max_uv[0] > 1.0 + prog->_fuzz_factor || 
+       min_uv[0] < 0.0 - prog->_fuzz_factor ||
+       max_uv[1] > 1.0 + prog->_fuzz_factor || 
+       min_uv[1] < 0.0 - prog->_fuzz_factor);
+    
+    if (repeat_unspecified) {
+      // If the egg file didn't give us any advice regarding
+      // repeating, we can go entirely based on what we saw in the
+      // UV's.
+      repeats = truly_repeats;
+
+    } else {
+      if (repeats && !truly_repeats) {
+	// The egg file specified that the texture repeats, but we
+	// discovered that it doesn't really.  Quietly mark it so.
+	if (!prog->_dont_override_repeats) {
+	  repeats = false;
+	}
+
+      } else if (!repeats && truly_repeats) {
+	// The egg file specified that the texture doesn't repeat, but
+	// its UV's were outside the normal range!  That's almost
+	// certainly a modeling error.
+	
+	// We won't trouble the user with this kind of spammy message,
+	// though.
+	/*
+	nout << "Warning: texture " << texture->get_name() << " (tref " 
+	     << eggtex->name << ") marked clamped, but UV's range from ("
+	     << min_uv << ") to (" << max_uv << ")\n";
+	     */
+
+      } else if (repeats && truly_repeats) {
+	// Well, it really does repeat.  Or does it?
+	if (fabs(max_uv[0] - min_uv[0]) <= 1.0 + prog->_fuzz_factor &&
+	    fabs(max_uv[1] - min_uv[1]) <= 1.0 + prog->_fuzz_factor) {
+	  // It really shouldn't!  The UV's fit totally within a unit
+	  // square, just not the square (0,0) - (1,1).  This is a
+	  // modeling problem; inform the user.
+	  nout << "Warning: texture " << texture->get_name()
+	       << " cannot be clamped because UV's range from ("
+	       << min_uv << ") to (" << max_uv << ")\n";
+	}
+      }
+    }
+    
+    TextureRef &texref = add_texture(texture, repeats, alpha);
+    texref._eggtex = eggtex;
+  }
+}
+
+// Updates each Texture with the flags stored in the various egg
+// files.  Also marks textures as used.
+void SourceEgg::
+mark_texture_flags() {
+  TexRefs::iterator ti;
+  for (ti = _texrefs.begin(); ti != _texrefs.end(); ++ti) {
+    Texture *texture = (*ti)._texture;
+    texture->set_unused(false);
+    if ((*ti)._alpha) {
+      texture->set_uses_alpha(true);
+    }
+    if ((*ti)._repeats) {
+      texture->set_omit(Texture::OR_repeats);
+    }
+  }
+}
+
+// Updates the egg file to point to the new palettes.
+void SourceEgg::
+update_trefs() {
+  TexRefs::iterator ti;
+  for (ti = _texrefs.begin(); ti != _texrefs.end(); ++ti) {
+    Texture *texture = (*ti)._texture;
+    EggTexture *eggtex = (*ti)._eggtex;
+
+    if (eggtex != NULL) {
+      // Make the alpha mode explicit if it isn't already.
+
+      /*
+      if (eggtex->get_alpha_mode == EggTexture::AM_unspecified) {
+	eggtex->set_alpha = eggtex->UsesAlpha() ? 
+	  EggTexture::AM_on : EggTexture::AM_off;
+      }
+      */
+
+      if (!texture->is_packed() || 
+	  texture->get_omit() != Texture::OR_none) {
+	// This texture wasn't palettized, so just rename the
+	// reference to the new one.
+	eggtex->set_fullpath(texture->get_filename());
+
+      } else {
+	// This texture was palettized, so redirect the tref to point
+	// within the palette.
+	Palette *palette = texture->get_palette();
+	
+	eggtex->set_fullpath(palette->get_filename());
+	
+	// Set the texture attributes to be uniform across all palettes.
+	eggtex->set_minfilter(EggTexture::FT_mipmap_trilinear);
+	eggtex->set_magfilter(EggTexture::FT_bilinear);
+	eggtex->set_format(EggTexture::F_rgba8);
+	eggtex->set_wrap_mode(EggTexture::WM_clamp);
+	eggtex->set_wrap_u(EggTexture::WM_clamp);
+	eggtex->set_wrap_v(EggTexture::WM_clamp);
+	
+	// Build a matrix that will scale the UV's to their new place
+	// on the palette.
+	int left, top, xsize, ysize, margin;
+	texture->get_packed_location(left, top);
+	texture->get_packed_size(xsize, ysize, margin);
+	
+	// Shrink the box to be within the margins.
+	top += margin;
+	left += margin;
+	xsize -= margin*2;
+	ysize -= margin*2;
+	
+	// Now determine the relative size and position within the
+	// palette.
+	int bottom = top + ysize;
+	int palx, paly;
+	palette->get_size(palx, paly);
+	LVecBase2d t((double)left / (double)palx,
+		     (double)(paly - bottom) / (double)paly);
+	
+	LVecBase2d s((double)xsize / (double)palx, 
+		     (double)ysize / (double)paly);
+	
+	LMatrix3d texmat
+	  (s[0],  0.0,  0.0,
+	    0.0, s[1],  0.0,
+	   t[0], t[1],  1.0);
+	
+	// Do we already have a texture matrix?  If so, compose them.
+	if (eggtex->has_transform()) {
+	  eggtex->set_transform(eggtex->get_transform() * texmat);
+	} else {
+	  // Otherwise, just store it.
+	  eggtex->set_transform(texmat);
+	}
+      }
+    }
+  }
+}
+
+// Returns true if any of the textures referenced by the egg file have
+// been adjusted this pass, implying that the egg file will have to be
+// re-run through egg-palettize, and/or re-pfb'ed.
+bool SourceEgg::
+needs_rebuild(bool force_redo_all,
+	      bool eggs_include_images) const {
+  //  if (!_wrote_egg) {
+    TexRefs::const_iterator ti;
+    for (ti = _texrefs.begin(); ti != _texrefs.end(); ++ti) {
+      bool dirty = 
+	eggs_include_images ? 
+	(*ti)._texture->needs_refresh() :
+	(*ti)._texture->packing_changed();
+      if (force_redo_all || dirty) {
+	return true;
+      }
+    }
+    //  }
+
+  return false;
+}
+
+void SourceEgg::
+write_pi(ostream &out) const {
+  out << "egg " << get_egg_filename() << "\n";
+  TexRefs::const_iterator ti;
+  for (ti = _texrefs.begin(); ti != _texrefs.end(); ++ti) {
+    out << "  " << (*ti)._texture->get_name();
+    if ((*ti)._repeats) {
+      out << " repeats";
+    }
+    if ((*ti)._alpha) {
+      out << " alpha";
+    }
+    out << "\n";
+  }
+}
+
+void SourceEgg::
+get_uv_range(EggGroupNode *group, EggTexture *tref,
+	     bool &any_uvs, TexCoordd &min_uv, TexCoordd &max_uv) {
+  EggGroupNode::iterator ci;
+
+  for (ci = group->begin(); ci != group->end(); ci++) {
+    EggNode *child = (*ci);
+    if (child->is_of_type(EggNurbsSurface::get_class_type())) {
+      EggNurbsSurface *nurbs = DCAST(EggNurbsSurface, child);
+      if (nurbs->has_texture() && nurbs->get_texture() == tref) {
+	// Here's a NURBS surface that references the texture.  Unlike
+	// other kinds of geometries, NURBS don't store UV's; they're
+	// implicit in the surface.  NURBS UV's will always run in the
+	// range (0,0) - (1,1).
+	if (any_uvs) {
+	  min_uv.set(min(min_uv[0], 0.0), min(min_uv[1], 0.0));
+	  max_uv.set(max(max_uv[0], 1.0), max(max_uv[1], 1.0));
+	} else {
+	  min_uv.set(0.0, 0.0);
+	  max_uv.set(1.0, 1.0);
+	  any_uvs = true;
+	}
+      }
+
+    } else if (child->is_of_type(EggPrimitive::get_class_type())) {
+      EggPrimitive *geom = DCAST(EggPrimitive, child);
+      if (geom->has_texture() && geom->get_texture() == tref) {
+	// Here's a piece of geometry that references this texture.
+	// Walk through its vertices at get its UV's.
+	EggPrimitive::iterator pi;
+	for (pi = geom->begin(); pi != geom->end(); ++pi) {
+	  EggVertex *vtx = (*pi);
+	  if (vtx->has_uv()) {
+	    const TexCoordd &uv = vtx->get_uv();
+	    if (any_uvs) {
+	      min_uv.set(min(min_uv[0], uv[0]), min(min_uv[1], uv[1]));
+	      max_uv.set(max(max_uv[0], uv[0]), max(max_uv[1], uv[1]));
+	    } else {
+	      // The first UV.
+	      min_uv = uv;
+	      max_uv = uv;
+	      any_uvs = true;
+	    }
+	  }
+	}
+      }
+
+    } else if (child->is_of_type(EggGroupNode::get_class_type())) {
+      EggGroupNode *cg = DCAST(EggGroupNode, child);
+      get_uv_range(cg, tref, any_uvs, min_uv, max_uv);
+    }
+  }
+}	

+ 75 - 0
pandatool/src/egg-palettize/sourceEgg.h

@@ -0,0 +1,75 @@
+// Filename: sourceEgg.h
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef SOURCEGG_H
+#define SOURCEGG_H
+
+#include <pandatoolbase.h>
+
+#include <eggData.h>
+#include <luse.h>
+
+
+class Texture;
+class AttribFile;
+class EggPalettize;
+class EggTexture;
+class EggGroup;
+    
+class SourceEgg : public EggData {
+public:
+  class TextureRef;
+
+  SourceEgg();
+
+  TextureRef &add_texture(Texture *texture, bool repeats, bool alpha);
+  void get_textures(AttribFile &af, EggPalettize *prog);
+
+  void mark_texture_flags();
+  void update_trefs();
+
+  bool needs_rebuild(bool force_redo_all, 
+			bool eggs_include_images) const;
+
+  void write_pi(ostream &out) const;
+
+
+  class TextureRef {
+  public:
+    TextureRef(Texture *texture, bool repeats, bool alpha);
+
+    Texture *_texture;
+    bool _repeats;
+    bool _alpha;
+
+    EggTexture *_eggtex;
+  };
+
+private:  
+  void get_uv_range(EggGroupNode *group, EggTexture *tref,
+		    bool &any_uvs, TexCoordd &min_uv, TexCoordd &max_uv);
+
+  typedef vector<TextureRef> TexRefs;
+  TexRefs _texrefs;
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    EggData::init_type();
+    register_type(_type_handle, "SourceEgg",
+                  EggData::get_class_type());
+  }
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+ 
+private:
+  static TypeHandle _type_handle;
+};
+
+#endif

+ 80 - 0
pandatool/src/egg-palettize/string_utils.cxx

@@ -0,0 +1,80 @@
+// Filename: string_utils.cxx
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "string_utils.h"
+
+
+string 
+trim_left(const string &str) {
+  size_t begin = 0;
+  while (begin < str.size() && isspace(str[begin])) {
+    begin++;
+  }
+
+  return str.substr(begin);
+}
+
+string 
+trim_right(const string &str) {
+  size_t begin = 0;
+  size_t end = str.size();
+  while (end > begin && isspace(str[end - 1])) {
+    end--;
+  }
+
+  return str.substr(begin, end - begin);
+}
+
+vector<string>
+extract_words(const string &str) {
+  vector<string> result;
+
+  size_t pos = 0;
+  while (pos < str.length() && isspace(str[pos])) {
+    pos++;
+  }
+  while (pos < str.length()) {
+    size_t word_start = pos;
+    while (pos < str.length() && !isspace(str[pos])) {
+      pos++;
+    }
+    result.push_back(str.substr(word_start, pos - word_start));
+
+    while (pos < str.length() && isspace(str[pos])) {
+      pos++;
+    }
+  }
+
+  return result;
+}
+
+// Extracts the first word of the string into param, and the remainder
+// of the line into value.
+void 
+extract_param_value(const string &str, string &param, string &value) {
+  size_t i = 0;
+
+  // First, skip all whitespace at the beginning.
+  while (i < str.length() && isspace(str[i])) {
+    i++;
+  }
+
+  size_t start = i;
+
+  // Now skip to the end of the whitespace.
+  while (i < str.length() && !isspace(str[i])) {
+    i++;
+  }
+
+  size_t end = i;
+
+  param = str.substr(start, end - start);
+
+  // Skip a little bit further to the start of the value.
+  while (i < str.length() && isspace(str[i])) {
+    i++;
+  }
+  value = trim_right(str.substr(i));
+}

+ 20 - 0
pandatool/src/egg-palettize/string_utils.h

@@ -0,0 +1,20 @@
+// Filename: string_utils.h
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef STRING_UTILS_H
+#define STRING_UTILS_H
+
+#include <pandatoolbase.h>
+
+#include <vector>
+
+string trim_left(const string &str);
+string trim_right(const string &str);
+
+vector<string> extract_words(const string &str);
+void extract_param_value(const string &str, string &param, string &value);
+
+#endif
+

+ 586 - 0
pandatool/src/egg-palettize/texture.cxx

@@ -0,0 +1,586 @@
+// Filename: texture.cxx
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "texture.h"
+#include "palette.h"
+#include "attribFile.h"
+
+#include <pnmImage.h>
+#include <pnmReader.h>
+
+
+Texture::
+Texture(AttribFile *af, const Filename &name) : 
+  _name(name),
+  _attrib_file(af)
+{
+  _got_filename = false;
+  _file_exists = false;
+  _texture_changed = false;
+  _unused = true;
+  _matched_anything = false;
+  _got_size = false;
+  _got_req = false;
+  _got_last_req = false;
+  _margin = _attrib_file->_default_margin;
+  _read_header = false;
+  _omit = OR_none;
+  _uses_alpha = false;
+  _is_packed = false;
+  _palette = NULL;
+}
+
+Filename Texture::
+get_name() const {
+  return _name;
+}
+  
+void Texture::
+add_filename(const Filename &filename) {
+  if (!filename.exists()) {
+    // Store the filename, even though it doesn't exist.
+    if (!_got_filename) {
+      _got_filename = true;
+      _filename = filename;
+      _file_exists = false;
+    }
+
+  } else {
+    bool inserted = _filenames.insert(filename).second;
+
+    if (!_got_filename || !_file_exists) {
+      // This was the first filename we encountered; no sweat.
+      _got_filename = true;
+      _file_exists = true;
+      _filename = filename;
+      
+      check_size();
+
+    } else if (inserted) {
+      // We had been using a different filename previously.  Now we've
+      // got a new one.  Maybe this one is better?
+      
+      // First, read the headers.  We'll consider the larger image to
+      // be the better choice.
+      if (!_got_size) {
+	read_header();
+      }
+
+      int oxsize, oysize;
+      bool got_other_size =
+	read_image_header(filename, oxsize, oysize);
+
+      if (got_other_size) {
+	if (!_got_size || oxsize * oysize > _xsize * _ysize) {
+	  _filename = filename;
+	  _xsize = oxsize;
+	  _ysize = oysize;
+	  _got_size = true;
+	  _texture_changed = true;
+	  
+	} else if (oxsize * oysize == _xsize * _ysize) {
+	  // If the sizes are the same, we'll consider the newer image
+	  // to be the better choice.
+	  if (filename.compare_timestamps(_filename, false, false) > 0) {
+	    _filename = filename;
+	    _texture_changed = true;
+	  }
+	}
+      }
+    }
+  }
+}
+
+Filename Texture::
+get_filename() const {
+  Filename filename = _name;
+  filename.set_dirname(_attrib_file->_map_dirname);
+  return filename;
+}
+
+Filename Texture::
+get_basename() const {
+  return _name;
+}
+
+bool Texture::
+get_size(int &xsize, int &ysize) {
+  if (!_got_size) {
+    read_header();
+  }
+
+  if (!_got_size) {
+    return false;
+  }
+
+  xsize = _xsize;
+  ysize = _ysize;
+  return true;
+}
+
+void Texture::
+set_size(int xsize, int ysize) {
+  // If we've already read the file header, don't let anyone tell us
+  // differently.
+  if (!_read_header) {
+    _xsize = xsize;
+    _ysize = ysize;
+    _got_size = true;
+  }
+}
+
+bool Texture::
+get_req(int &xsize, int &ysize) {
+  if (!_got_req) {
+    return get_size(xsize, ysize);
+  }
+  xsize = _req_xsize;
+  ysize = _req_ysize;
+  return true;
+}
+
+bool Texture::
+get_last_req(int &xsize, int &ysize) {
+  if (!_got_last_req) {
+    return get_size(xsize, ysize);
+  }
+  xsize = _last_req_xsize;
+  ysize = _last_req_ysize;
+  return true;
+}
+
+void Texture::
+set_last_req(int xsize, int ysize) {
+  _last_req_xsize = xsize;
+  _last_req_ysize = ysize;
+  _got_last_req = true;
+}
+
+void Texture::
+reset_req(int xsize, int ysize) {
+  if (_got_last_req && 
+      (_last_req_xsize != xsize || _last_req_ysize != ysize)) {
+    // We've changed the requested size from the last time we ran.
+    // That constitutes a change to the texture.
+    _texture_changed = true;
+  }
+
+  _req_xsize = xsize;
+  _req_ysize = ysize;
+  _got_req = true;
+}
+
+void Texture::
+scale_req(double scale_pct) {
+  if (!_got_size) {
+    read_header();
+  }
+  if (_got_size) {
+    reset_req(_xsize * scale_pct / 100.0,
+	      _ysize * scale_pct / 100.0);
+  }
+}
+
+void Texture::
+clear_req() {
+  _got_req = false;
+  _margin = _attrib_file->_default_margin;
+
+  if (_omit != OR_cmdline) {
+    // If we previously omitted this texture from the command line,
+    // preserve that property.
+    _omit = OR_none;
+  }
+}
+
+double Texture::
+get_scale_pct() {
+  if (!_got_size) {
+    read_header();
+  }
+  if (_got_size && _got_req) {
+    return 
+      100.0 * (((double)_req_xsize / (double)_xsize) +
+	       ((double)_req_ysize / (double)_ysize)) / 2.0;
+  } else {
+    return 100.0;
+  }
+}
+
+int Texture::
+get_margin() const {
+  return _margin;
+}
+
+void Texture::
+set_margin(int margin) {
+  _margin = margin;
+}
+
+Texture::OmitReason Texture::
+get_omit() const {
+  return _omit;
+}
+
+void Texture::
+set_omit(OmitReason omit) {
+  _omit = omit;
+}
+
+bool Texture::
+needs_refresh() {
+  if (!_texture_changed) {
+    // We consider the texture to be out-of-date if it's moved around
+    // in the palette.
+    _texture_changed = packing_changed();
+  }
+
+  if (!_texture_changed && _file_exists) {
+    // Compare the texture's timestamp to that of its palette (or
+    // resized copy).  If it's newer, it's changed and must be
+    // replaced.
+
+    Filename target_filename;
+    if (is_packed() && _omit == OR_none) {
+      // Compare to the palette file.
+      target_filename = _palette->get_filename();
+      if (_palette->new_palette()) {
+	// It's a brand new palette; don't even bother comparing
+	// timestamps.
+	_texture_changed = true;
+      }
+
+    } else {
+      // Compare to the resized file.
+      target_filename = get_filename();
+    }
+
+    if (!_texture_changed) {
+      _texture_changed = 
+	(target_filename.compare_timestamps(_filename, true, false) < 0);
+    }
+  }
+
+  return _texture_changed;
+}
+
+void Texture::
+set_changed(bool changed) {
+  _texture_changed = changed;
+}
+
+bool Texture::
+unused() const {
+  return _unused;
+}
+
+void Texture::
+set_unused(bool unused) {
+  _unused = unused;
+}
+
+bool Texture::
+matched_anything() const {
+  return _matched_anything;
+}
+
+void Texture::
+set_matched_anything(bool matched_anything) {
+  _matched_anything = matched_anything;
+}
+
+bool Texture::
+uses_alpha() const {
+  return _uses_alpha;
+}
+
+void Texture::
+set_uses_alpha(bool uses_alpha) {
+  _uses_alpha = uses_alpha;
+}
+
+void Texture::
+mark_pack_location(Palette *palette, int left, int top,
+		   int xsize, int ysize, int margin) {
+  _is_packed = true;
+  _palette = palette;
+  _pleft = left;
+  _ptop = top;
+  _pxsize = xsize;
+  _pysize = ysize;
+  _pmargin = margin;
+}
+
+void Texture::
+mark_unpacked() {
+  _is_packed = false;
+}
+
+bool Texture::
+is_packed() const {
+  return _is_packed;
+}
+
+// Returns the same thing as is_packed(), except it doesn't consider a
+// texture that has been left alone on a palette to be packed.
+bool Texture::
+is_really_packed() const {
+  return _is_packed && _omit != OR_solitary;
+}
+
+Palette *Texture::
+get_palette() const {
+  return _is_packed ? _palette : (Palette *)NULL;
+}
+
+bool Texture::
+get_packed_location(int &left, int &top) const {
+  left = _pleft;
+  top = _ptop;
+  return _is_packed;
+}
+
+bool Texture::
+get_packed_size(int &xsize, int &ysize, int &margin) const {
+  xsize = _pxsize;
+  ysize = _pysize;
+  margin = _pmargin;
+  return _is_packed;
+}
+
+void Texture::
+record_orig_state() {
+  // Records the current packing state, storing it aside as the state
+  // at load time.  Later, when the packing state may have changed,
+  // packing_changed() will return true if it has or false if it
+  // has not.
+  _orig_is_packed = _is_packed;
+  if (_is_packed) {
+    _orig_palette_name = _palette->get_filename();
+    _opleft = _pleft;
+    _optop = _ptop;
+    _opxsize = _pxsize;
+    _opysize = _pysize;
+    _opmargin = _pmargin;
+  }
+}
+
+bool Texture::
+packing_changed() const {
+  if (_orig_is_packed != _is_packed) {
+    return true;
+  }
+  if (_is_packed) {
+    return _orig_palette_name != _palette->get_filename() ||
+      _opleft != _pleft ||
+      _optop != _ptop ||
+      _opxsize != _pxsize ||
+      _opysize != _pysize ||
+      _opmargin != _pmargin;
+  }
+  return false;
+}
+
+void Texture::
+write_size(ostream &out) {
+  if (_omit != OR_unused) {
+    if (!_got_size) {
+      read_header();
+    }
+    out << "  " << _name;
+    if (_got_size) {
+      out << " orig " << _xsize << " " << _ysize;
+    }
+    if (_got_req) {
+      out << " new " << _req_xsize << " " << _req_ysize;
+    }
+    out << " " << get_scale_pct() << "%\n";
+  }
+}
+
+void Texture::
+write_pathname(ostream &out) const {
+  if (_got_filename && _omit != OR_unused) {
+    if (!_file_exists) {
+      nout << "Warning: texture " << _filename << " does not exist.\n";
+    }
+
+    out << "  " << _name << " " << _filename << "\n";
+
+    // Also write out all the alternate filenames.
+    Filenames::const_iterator fi;
+    for (fi = _filenames.begin(); fi != _filenames.end(); ++fi) {
+      if ((*fi) != _filename) {
+	// Indent by the same number of spaces to line up the filenames.
+	for (int i = 0; i < (int)_name.length() + 3; i++) {
+	  out << ' ';
+	}
+	out << (*fi) << "\n";
+      }
+    }
+  }
+}
+
+void Texture::
+write_unplaced(ostream &out) const {
+  if (_omit != OR_none && _omit != OR_unused) {
+    out << "unplaced " << get_name() << " because ";
+    switch (_omit) {
+    case OR_size:
+      out << "size";
+      break;
+    case OR_repeats:
+      out << "repeats";
+      break;
+    case OR_omitted:
+      out << "omitted";
+      break;
+    case OR_unused:
+      out << "unused";
+      break;
+    case OR_unknown:
+      out << "unknown";
+      break;
+    case OR_solitary:
+      out << "solitary";
+      break;
+    case OR_cmdline:
+      out << "cmdline";
+      break;
+    default:
+      nout << "Invalid type: " << (int)_omit << "\n";
+      abort();
+    }
+    out << "\n";
+  }
+}
+
+bool Texture::
+transfer() {
+  bool okflag = true;
+
+  Filename new_filename = get_filename();
+  if (new_filename == _filename) {
+    nout << "*** Texture " << _name << " is already in the map directory!\n"
+	 << "    Cannot modify texture in place!\n";
+    return false;
+  }
+
+  int nx, ny;
+  if (!get_req(nx, ny)) {
+    nout << "Unknown size for image " << _name << "\n";
+    nx = 16;
+    ny = 16;
+  }
+
+  if (_attrib_file->_force_power_2) {
+    int newx = to_power_2(nx);
+    int newy = to_power_2(ny);
+    if (newx != nx || newy != ny) {
+      nx = newx;
+      ny = newy;
+    }
+  }
+ 
+  PNMImage *image = read_image();
+  if (image == NULL) {
+    nout << "*** Unable to read " << _name << "\n";
+    okflag = false;
+
+    // Create a solid red texture for images we can't read.
+    image = new PNMImage(nx, ny);
+    image->fill(1.0, 0.0, 0.0);
+
+  } else {
+    // Should we scale it?
+    if (nx != image->get_x_size() && ny != image->get_y_size()) {
+      nout << "Resizing " << new_filename << " to " 
+	   << nx << " " << ny << "\n";
+      PNMImage *new_image =
+	new PNMImage(nx, ny, image->get_color_type());
+      new_image->gaussian_filter_from(0.5, *image);
+      delete image;
+      image = new_image;
+      
+    } else {
+      nout << "Copying " << new_filename
+	   << " (size " << nx << " " << ny << ")\n";
+    }
+  }
+    
+  if (!image->write(new_filename)) {
+    nout << "Error in writing.\n";
+    okflag = false;
+  }
+  delete image;
+
+  return okflag;
+}
+
+PNMImage *Texture::
+read_image() {
+  if (!_got_filename || !_file_exists) {
+    return NULL;
+  }
+
+  PNMImage *image = new PNMImage;
+  if (image->read(_filename)) {
+    return image;
+  }
+
+  // Hmm, it wasn't able to read the image successfully.  Oh well.
+  delete image;
+  return NULL;
+}
+
+void Texture::
+check_size() {
+  // Make sure the file has the size it claims to have.
+  if (_got_size) {
+    int old_xsize = _xsize;
+    int old_ysize = _ysize;
+    read_header();
+    if (_xsize != old_xsize && _ysize != old_ysize) {
+      nout << "Source texture " << _name << " has changed size, from "
+	   << old_xsize << " " << old_ysize << " to "
+	   << _xsize << " " << _ysize << "\n";
+      _texture_changed = true;
+    }
+  }
+}
+
+void Texture::
+read_header() {
+  // Open the file and read its header to determine its size.
+  if (_got_filename && _file_exists) {
+    if (!_read_header) {
+      _read_header = true;
+      _got_size = read_image_header(_filename, _xsize, _ysize);
+    }
+    _read_header = true;
+  }
+}
+
+bool Texture::
+read_image_header(const Filename &filename, int &xsize, int &ysize) {
+  PNMImageHeader header;
+  if (!header.read_header(filename)) {
+    nout << "Warning: cannot read texture " << filename << "\n";
+    return false;
+  }
+
+  xsize = header.get_x_size();
+  ysize = header.get_y_size();
+  return true;
+}
+
+int Texture::
+to_power_2(int value) {
+  int x = 1;
+  while ((x << 1) <= value) {
+    x = (x << 1);
+  }
+  return x;
+}

+ 131 - 0
pandatool/src/egg-palettize/texture.h

@@ -0,0 +1,131 @@
+// Filename: texture.h
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef TEXTURE_H
+#define TEXTURE_H
+
+#include <pandatoolbase.h>
+
+#include "imageFile.h"
+
+#include <set>
+
+class Palette;
+class PNMImage;
+class AttribFile;
+
+////////////////////////////////////////////////////////////////////
+// 	 Class : Texture
+// Description : 
+////////////////////////////////////////////////////////////////////
+class Texture : public ImageFile {
+public:
+  enum OmitReason {
+    OR_none,
+    OR_size, OR_repeats, OR_omitted, OR_unused, OR_unknown,
+    OR_cmdline, OR_solitary
+  };
+
+  Texture(AttribFile *af, const Filename &name);
+
+  Filename get_name() const;
+  
+  void add_filename(const Filename &filename);
+
+  virtual Filename get_filename() const;
+  virtual Filename get_basename() const;
+
+  bool get_size(int &xsize, int &ysize);
+  void set_size(int xsize, int ysize);
+
+  bool get_req(int &xsize, int &ysize);
+  bool get_last_req(int &xsize, int &ysize);
+  void set_last_req(int xsize, int ysize);
+  void reset_req(int xsize, int ysize);
+  void scale_req(double scale_pct);
+  void clear_req();
+  double get_scale_pct();
+
+  int get_margin() const;
+  void set_margin(int margin);
+
+  OmitReason get_omit() const;
+  void set_omit(OmitReason omit);
+
+  bool needs_refresh();
+  void set_changed(bool changed);
+
+  bool unused() const;
+  void set_unused(bool unused);
+
+  bool matched_anything() const;
+  void set_matched_anything(bool matched_anything);
+
+  bool uses_alpha() const;
+  void set_uses_alpha(bool uses_alpha);
+
+  void mark_pack_location(Palette *palette, int left, int top,
+			  int xsize, int ysize, int margin);
+  void mark_unpacked();
+  bool is_packed() const;
+  bool is_really_packed() const;
+  Palette *get_palette() const;
+  bool get_packed_location(int &left, int &top) const;
+  bool get_packed_size(int &xsize, int &ysize, int &margin) const;
+  void record_orig_state();
+  bool packing_changed() const;
+
+  void write_size(ostream &out);
+  void write_pathname(ostream &out) const;
+  void write_unplaced(ostream &out) const;
+
+  bool transfer();
+
+  PNMImage *read_image();
+
+private:  
+  void check_size();
+  void read_header();
+  bool read_image_header(const Filename &filename, int &xsize, int &ysize);
+  static int to_power_2(int value);
+
+  Filename _name;
+
+  typedef set<Filename> Filenames;
+  Filenames _filenames;
+
+  bool _got_filename;
+  Filename _filename;
+  bool _file_exists;
+  bool _texture_changed;
+  bool _unused;
+  bool _matched_anything;
+  bool _uses_alpha;
+
+  bool _got_size;
+  int _xsize, _ysize;
+
+  bool _got_req;
+  int _req_xsize, _req_ysize;
+  bool _got_last_req;
+  int _last_req_xsize, _last_req_ysize;
+  int _margin;
+  OmitReason _omit;
+
+  bool _is_packed;
+  Palette *_palette;
+  int _pleft, _ptop, _pxsize, _pysize, _pmargin;
+
+  bool _orig_is_packed;
+  Filename _orig_palette_name;
+  int _opleft, _optop, _opxsize, _opysize, _opmargin;
+
+  bool _read_header;
+
+  AttribFile *_attrib_file;
+};
+
+
+#endif

+ 362 - 0
pandatool/src/egg-palettize/userAttribLine.cxx

@@ -0,0 +1,362 @@
+// Filename: userAttribLine.cxx
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "userAttribLine.h"
+#include "string_utils.h"
+#include "texture.h"
+#include "attribFile.h"
+
+#include <notify.h>
+
+#include <ctype.h>
+#include <fnmatch.h>
+
+UserAttribLine::TextureName::
+TextureName(const string &pattern) : _pattern(pattern) {
+}
+
+UserAttribLine::
+UserAttribLine(const string &cline, AttribFile *af) : _attrib_file(af) {
+  _is_old_style = false;
+
+  // By default, all lines are marked 'was_used'.  That means they'll
+  // be preserved across a -k on the command line.  Lines that name
+  // textures will have _was_used set false until a texture actually
+  // matches that line.
+  _was_used = true;
+
+  string line = cline;
+
+  // First, strip off the comment.
+  if (!line.empty()) {
+    if (line[0] == '#') {
+      _comment = line;
+      line = "";
+    } else {
+      size_t pos = line.find(" #");
+      if (pos != string::npos) {
+	while (pos > 0 && isspace(line[pos])) {
+	  pos--;
+	}
+	_comment = line.substr(pos + 1);
+	line = line.substr(0, pos);
+      }
+    }
+  }
+
+  // Now, analyze the line.
+  _line_type = LT_invalid;
+  _xsize = 0;
+  _ysize = 0;
+  _scale_pct = 0.0;
+  _msize = -1;
+  _omit = false;
+
+  bool is_valid = true;
+  if (line.empty()) {
+    _line_type = LT_comment;
+
+  } else if (line[0] == ':') {
+    is_valid = keyword_line(line);
+
+  } else {
+    is_valid = texture_line(line);
+  }
+
+  if (!is_valid) {
+    _line_type = LT_invalid;
+  }
+}
+
+UserAttribLine::
+~UserAttribLine() {
+}
+
+bool UserAttribLine::
+is_valid() const {
+  return _line_type != LT_invalid;
+}
+
+bool UserAttribLine::
+is_old_style() const {
+  return _is_old_style;
+}
+
+bool UserAttribLine::
+was_used() const {
+  return _was_used;
+}
+
+void UserAttribLine::
+write(ostream &out) const {
+  switch (_line_type) {
+  case LT_invalid:
+    out << "*** invalid line ***\n";
+    break;
+
+  case LT_comment:
+    break;
+
+  case LT_margin:
+    out << ":margin " << _msize;
+    break;
+
+  case LT_palette:
+    out << ":palette " << _xsize << " " << _ysize;
+    break;
+
+  case LT_size:
+    list_textures(out) << " : " << _xsize << " " << _ysize;
+    if (_msize > 0) {
+      out << " " << _msize;
+    }
+    if (_omit) {
+      out << " omit";
+    }
+    break;
+
+  case LT_scale:
+    list_textures(out) << " : " << _scale_pct << "%";
+    if (_msize > 0) {
+      out << " " << _msize;
+    }
+    if (_omit) {
+      out << " omit";
+    }
+    break;
+
+  case LT_name:
+    list_textures(out) << " :";
+    if (_omit) {
+      out << " omit";
+    }
+    break;
+
+  default:
+    nout << "Unexpected type: " << (int)_line_type << "\n";
+    abort();
+  };
+
+  out << _comment << "\n";
+}
+
+bool UserAttribLine::
+match_texture(Texture *texture, int &margin) {
+  // See if the texture name matches any of the filename patterns on
+  // this line.
+  bool matched_any = false;
+  TextureNames::const_iterator tni;
+  for (tni = _texture_names.begin();
+       tni != _texture_names.end() && !matched_any;
+       ++tni) {
+    if (fnmatch((*tni)._pattern.c_str(), texture->get_name().c_str(), 0) == 0) {
+      matched_any = true;
+    }
+  }
+
+  if (matched_any) {
+    _was_used = true;
+
+    // It does!  So do the right thing with this line.
+    switch (_line_type) {
+    case LT_invalid:
+    case LT_comment:
+    case LT_palette:
+      return false;
+      
+    case LT_margin:
+      margin = _msize;
+      return false;
+      
+    case LT_size:
+      texture->reset_req(_xsize, _ysize);
+      texture->set_margin(_msize < 0 ? margin : _msize);
+      if (_omit) {
+	texture->set_omit(Texture::OR_omitted);
+      }
+      return true;
+      
+    case LT_scale:
+      texture->scale_req(_scale_pct);
+      texture->set_margin(_msize < 0 ? margin : _msize);
+      if (_omit) {
+	texture->set_omit(Texture::OR_omitted);
+      }
+      return true;
+      
+    case LT_name:
+      if (_omit) {
+	texture->set_omit(Texture::OR_omitted);
+      }
+      return true;
+      
+    default:
+      nout << "Unexpected type: " << (int)_line_type << "\n";
+      abort();
+    }
+  }
+
+  return false;
+}
+
+ostream &UserAttribLine::
+list_textures(ostream &out) const {
+  if (!_texture_names.empty()) {
+    out << _texture_names[0]._pattern;
+    for (int i = 1; i < (int)_texture_names.size(); i++) {
+      out << " " << _texture_names[i]._pattern;
+    }
+  }
+  return out;
+}
+
+bool UserAttribLine::
+keyword_line(const string &line) {
+  vector<string> words = extract_words(line);
+  assert(!words.empty());
+
+  if (words[0] == ":margin") {
+    _line_type = LT_margin;
+    if (words.size() != 2) {
+      nout << "Expected margin size.\n";
+      return false;
+    }
+
+    _msize = atoi(words[1].c_str());
+
+  } else if (words[0] == ":palette") {
+    _line_type = LT_palette;
+    if (words.size() != 3) {
+      nout << "Expected xsize ysize of palette.\n";
+      return false;
+    }
+    _xsize = atoi(words[1].c_str());
+    _ysize = atoi(words[2].c_str());
+    _attrib_file->_pal_xsize = _xsize;
+    _attrib_file->_pal_ysize = _ysize;
+
+  } else {
+    nout << "Unknown keyword: " << words[0] << "\n";
+    return false;
+  }
+
+  return true;
+}
+
+bool UserAttribLine::
+texture_line(const string &line) {
+  _was_used = false;
+
+  // Scan for a colon followed by a space.
+
+  size_t colon = line.find(": ");
+  if (colon == string::npos) {
+    return old_style_line(line);
+  }
+
+  // Split the line into two parts at the colon.
+  vector<string> names = extract_words(line.substr(0, colon));
+  vector<string> params = extract_words(line.substr(colon + 2));
+
+  vector<string>::const_iterator ni;
+  for (ni = names.begin(); ni != names.end(); ++ni) {
+    _texture_names.push_back(TextureName(*ni));
+  }
+
+  if (!params.empty() && params[params.size() - 1] == "omit") {
+    // If the last word is "omit", set the omit flag and remove the
+    // word.
+    _omit = true;
+    params.pop_back();
+  }
+
+  if (params.empty()) {
+    _line_type = LT_name;
+    return true;
+  }
+
+  // Is it a percentage?
+  if (!params[0].empty() && params[0][params[0].size() - 1] == '%') {
+    _line_type = LT_scale;
+    _scale_pct = atof(params[0].c_str());
+    if (_scale_pct <= 0.0) {
+      nout << "Invalid scale percentage for ";
+      copy(names.begin(), names.end(), ostream_iterator<string>(nout, " "));
+      nout << ": " << _scale_pct << "%\n";
+      return false;
+    }
+    return true;
+  }
+
+  // It must be xsize ysize [margin]
+  if (params.size() == 2) {
+    _line_type = LT_size;
+    _xsize = atoi(params[0].c_str());
+    _ysize = atoi(params[1].c_str());
+    if (_xsize <= 0 || _ysize <= 0) {
+      nout << "Invalid texture size for ";
+      copy(names.begin(), names.end(), ostream_iterator<string>(nout, " "));
+      nout << ": " << _xsize << " " << _ysize << "\n";
+      return false;
+    }
+    return true;
+  }
+
+  if (params.size() == 3) {
+    _line_type = LT_size;
+    _xsize = atoi(params[0].c_str());
+    _ysize = atoi(params[1].c_str());
+    _msize = atoi(params[2].c_str());
+    if (_xsize <= 0 || _ysize <= 0) {
+      nout << "Invalid texture size for ";
+      copy(names.begin(), names.end(), ostream_iterator<string>(nout, " "));
+      nout << ": " << _xsize << " " << _ysize << "\n";
+      return false;
+    }
+    return true;
+  }
+
+  nout << "Expected xsize ysize [msize]\n";
+  return false;
+}
+
+
+bool UserAttribLine::
+old_style_line(const string &line) {
+  vector<string> words = extract_words(line);
+  assert(!words.empty());
+
+  if (words.size() != 3 && words.size() != 4) {
+    nout << "Colon omitted.\n";
+    return false;
+  }
+
+  _is_old_style = true;
+  _line_type = LT_size;
+  _texture_names.push_back(TextureName(words[0]));
+  _xsize = atoi(words[1].c_str());
+  _ysize = atoi(words[2].c_str());
+  if (words.size() > 3) {
+    _msize = atoi(words[3].c_str());
+
+    if (_msize < 0) {
+      _msize = -1;
+      _omit = true;
+      
+    } else if (_msize == _attrib_file->_default_margin) {
+      _msize = -1;
+    }
+  } else {
+    _msize = -1;
+  }
+
+  if (_xsize <= 0 || _ysize <= 0) {
+    nout << "Invalid texture size for " << words[0] << ": "
+	 << _xsize << " " << _ysize << "\n";
+    return false;
+  }
+
+  return true;
+}

+ 86 - 0
pandatool/src/egg-palettize/userAttribLine.h

@@ -0,0 +1,86 @@
+// Filename: userAttribLine.h
+// Created by:  drose (02Sep99)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef USERATTRIBLINE_H
+#define USERATTRIBLINE_H
+
+#include <pandatoolbase.h>
+
+#include <vector>
+
+class AttribFile;
+class Texture;
+
+////////////////////////////////////////////////////////////////////
+// 	 Class : UserAttribLine
+// Description : A single entry in the user part (the beginning) of
+//               the attrib file, this defines how the user would like
+//               some particular texture to be scaled.
+////////////////////////////////////////////////////////////////////
+
+//
+// This might be a line of any of the following forms:
+//
+//   (blank line)
+//   # Comment
+//   :margin msize
+//   :palette xsize ysize 
+//   texturename xsize ysize msize
+//   texturename [texturename ...] : xsize ysize [msize] [omit]
+//   texturename [texturename ...] : scale% [msize] [omit]
+//   texturename [texturename ...] : [omit]
+//
+
+class UserAttribLine {
+public:
+  UserAttribLine(const string &line, AttribFile *af);
+  ~UserAttribLine();
+
+  bool is_valid() const;
+  bool is_old_style() const;
+  bool was_used() const;
+
+  void write(ostream &out) const;
+
+  bool match_texture(Texture *texture, int &margin);
+
+private:
+  enum LineType {
+    LT_invalid,
+    LT_comment, 
+    LT_margin, LT_palette,
+    LT_size, LT_scale, LT_name
+  };
+  class TextureName {
+  public:
+    TextureName(const string &pattern);
+    TextureName(const TextureName &copy) : 
+      _pattern(copy._pattern) { }
+
+    string _pattern;
+  };
+
+  typedef vector<TextureName> TextureNames;
+  TextureNames _texture_names;
+
+  ostream &list_textures(ostream &out) const;
+  bool keyword_line(const string &line);
+  bool texture_line(const string &line);
+  bool old_style_line(const string &line);
+
+  string _comment;
+  LineType _line_type;
+  int _xsize, _ysize;
+  double _scale_pct;
+  int _msize;
+  bool _omit;
+  bool _is_old_style;
+  bool _was_used;
+  
+  AttribFile *_attrib_file;
+};
+
+
+#endif

+ 5 - 3
pandatool/src/eggbase/Sources.pp

@@ -7,13 +7,15 @@
 
   #define SOURCES \
     eggBase.cxx eggBase.h eggConverter.cxx eggConverter.h eggFilter.cxx \
-    eggFilter.h eggReader.cxx eggReader.h eggToSomething.cxx \
+    eggFilter.h eggMultiBase.cxx eggMultiBase.h \
+    eggMultiFilter.cxx eggMultiFilter.h \
+    eggReader.cxx eggReader.h eggToSomething.cxx \
     eggToSomething.h eggWriter.cxx eggWriter.h somethingToEgg.cxx \
     somethingToEgg.h
 
   #define INSTALL_HEADERS \
-    eggBase.h eggConverter.h eggFilter.h eggReader.h eggToSomething.h \
-    eggWriter.h somethingToEgg.h
+    eggBase.h eggConverter.h eggFilter.h eggMultiBase.h eggMultiFilter.h \
+    eggReader.h eggToSomething.h eggWriter.h somethingToEgg.h
 
 #end ss_lib_target
 

+ 1 - 1
pandatool/src/eggbase/eggBase.cxx

@@ -89,5 +89,5 @@ append_command_comment(EggData &data) {
     }
   }
 
-  data.insert(_data.begin(), new EggComment("", comment));
+  data.insert(data.begin(), new EggComment("", comment));
 }

+ 2 - 0
pandatool/src/eggbase/eggFilter.h

@@ -20,6 +20,8 @@
 class EggFilter : public EggReader, public EggWriter {
 public:
   EggFilter(bool allow_last_param = false, bool allow_stdout = true);
+
+protected:
   virtual bool handle_args(Args &args);
 };
 

+ 104 - 0
pandatool/src/eggbase/eggMultiBase.cxx

@@ -0,0 +1,104 @@
+// Filename: eggMultiBase.cxx
+// Created by:  drose (02Nov00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "eggMultiBase.h"
+
+#include <eggData.h>
+#include <eggComment.h>
+#include <filename.h>
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggMultiBase::Constructor
+//       Access: Public
+//  Description: 
+////////////////////////////////////////////////////////////////////
+EggMultiBase::
+EggMultiBase() {
+  add_option
+    ("cs", "coordinate-system", 80, 
+     "Specify the coordinate system to operate in.  This may be one of "
+     "'y-up', 'z-up', 'y-up-left', or 'z-up-left'.",
+     &EggMultiBase::dispatch_coordinate_system, 
+     &_got_coordinate_system, &_coordinate_system);
+}
+
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggMultiBase::append_command_comment
+//       Access: Protected
+//  Description: Inserts a comment into the beginning of the indicated
+//               egg file corresponding to the command line that
+//               invoked this program.
+//
+//               Normally this function is called automatically when
+//               appropriate by EggMultiFilter, and it's not necessary
+//               to call it explicitly.
+////////////////////////////////////////////////////////////////////
+void EggMultiBase::
+append_command_comment(EggData &data) {
+  string comment;
+
+  comment = _program_name;
+  Args::const_iterator ai;
+  for (ai = _program_args.begin(); ai != _program_args.end(); ++ai) {
+    const string &arg = (*ai);
+
+    // First, check to see if the string is shell-acceptable.
+    bool legal = true;
+    string::const_iterator si;
+    for (si = arg.begin(); legal && si != arg.end(); ++si) {
+      switch (*si) {
+      case ' ':
+      case '\n':
+      case '\t':
+      case '*':
+      case '?':
+      case '\\':
+      case '(':
+      case ')':
+      case '|':
+      case '&':
+      case '<':
+      case '>':
+      case '"':
+      case ';':
+      case '$':
+	legal = false;
+      }
+    }
+
+    if (legal) {
+      comment += " " + arg;
+    } else {
+      comment += " '" + arg + "'";
+    }
+  }
+
+  data.insert(data.begin(), new EggComment("", comment));
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggMultiBase::read_egg
+//       Access: Protected, Virtual
+//  Description: Allocates and returns a new EggData structure that
+//               represents the indicated egg file.  If the egg file
+//               cannot be read for some reason, returns NULL. 
+//
+//               This can be overridden by derived classes to control
+//               how the egg files are read, or to extend the
+//               information stored with each egg structure, by
+//               deriving from EggData.
+////////////////////////////////////////////////////////////////////
+EggData *EggMultiBase::
+read_egg(const Filename &filename) {
+  EggData *data = new EggData;
+  if (data->read(filename)) {
+    return data;
+  }
+
+  // Failure reading.
+  delete data;
+  return (EggData *)NULL;
+}

+ 45 - 0
pandatool/src/eggbase/eggMultiBase.h

@@ -0,0 +1,45 @@
+// Filename: eggMultiBase.h
+// Created by:  drose (02Nov00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef EGGMULTIBASE_H
+#define EGGMULTIBASE_H
+
+#include <pandatoolbase.h>
+
+#include <programBase.h>
+#include <coordinateSystem.h>
+
+class Filename;
+class EggData;
+
+////////////////////////////////////////////////////////////////////
+// 	 Class : EggMultiBase
+// Description : This specialization of ProgramBase is intended for
+//               programs that read and/or write multiple egg files.
+//
+//               See also EggMultiFilter, for a class that also knows
+//               how to read a bunch of egg files in and write them
+//               out again.
+////////////////////////////////////////////////////////////////////
+class EggMultiBase : public ProgramBase {
+public:
+  EggMultiBase();
+
+protected:
+  void append_command_comment(EggData &_data);
+
+  virtual EggData *read_egg(const Filename &filename);
+
+protected:
+  bool _got_coordinate_system;
+  CoordinateSystem _coordinate_system;
+
+  typedef vector<EggData *> Eggs;
+  Eggs _eggs;
+};
+
+#endif
+
+

+ 160 - 0
pandatool/src/eggbase/eggMultiFilter.cxx

@@ -0,0 +1,160 @@
+// Filename: eggMultiFilter.cxx
+// Created by:  drose (02Nov00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "eggMultiFilter.h"
+
+#include <notify.h>
+#include <eggData.h>
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggMultiFilter::Constructor
+//       Access: Public
+//  Description: 
+////////////////////////////////////////////////////////////////////
+EggMultiFilter::
+EggMultiFilter(bool allow_empty) : _allow_empty(allow_empty) {
+  clear_runlines();
+  add_runline("-o output.egg [opts] input.egg");
+  add_runline("-d dirname [opts] file.egg [file.egg ...]");
+  add_runline("-inplace [opts] file.egg [file.egg ...]");
+
+  add_option
+    ("o", "filename", 50,
+     "Specify the filename to which the resulting egg file will be written.  "
+     "This is only valid when there is only one input egg file on the command "
+     "line.  If you want to process multiple files simultaneously, you must "
+     "use either -d or -inplace.",
+     &EggMultiFilter::dispatch_filename, &_got_output_filename, &_output_filename);
+
+  add_option
+    ("d", "dirname", 50, 
+     "Specify the name of the directory in which to write the resulting egg "
+     "files.  If you are processing only one egg file, this may be omitted "
+     "in lieu of the -o option.  If you are processing multiple egg files, "
+     "this may be omitted only if you specify -inplace instead.",
+     &EggMultiFilter::dispatch_filename, &_got_output_dirname, &_output_dirname);
+
+  add_option
+    ("inplace", "", 50, 
+     "If this option is given, the input files will be rewritten in place with "
+     "the results.  This obviates the need to specify -d for an output "
+     "directory; however, it's risky because the original input "
+     "files are lost.",
+     &EggMultiFilter::dispatch_none, &_inplace);
+}
+
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggMultiFilter::handle_args
+//       Access: Protected, Virtual
+//  Description: Does something with the additional arguments on the
+//               command line (after all the -options have been
+//               parsed).  Returns true if the arguments are good,
+//               false otherwise.
+////////////////////////////////////////////////////////////////////
+bool EggMultiFilter::
+handle_args(ProgramBase::Args &args) {
+  if (args.empty()) {
+    if (!_allow_empty) {
+      nout << "You must specify the egg file(s) to read on the command line.\n";
+      return false;
+    }
+  } else {
+    // These only apply if we have specified any egg files.
+
+    if (_got_output_filename && args.size() == 1) {
+      if (_got_output_dirname) {
+	nout << "Cannot specify both -o and -d.\n";
+	return false;
+      } else if (_inplace) {
+	nout << "Cannot specify both -o and -inplace.\n";
+	return false;
+      }
+
+    } else {
+      if (_got_output_filename) {
+	nout << "Cannot use -o when multiple egg files are specified.\n";
+	return false;
+      }
+
+      if (_got_output_dirname && _inplace) {
+	nout << "Cannot specify both -inplace and -d.\n";
+	return false;
+	
+      } else if (!_got_output_dirname && !_inplace) {
+	nout << "You must specify either -inplace or -d.\n";
+	return false;
+      }
+    }
+  }
+
+
+  Args::const_iterator ai;
+  for (ai = args.begin(); ai != args.end(); ++ai) {
+    EggData *data = read_egg(*ai);
+    if (data == (EggData *)NULL) {
+      // Rather than returning false, we simply exit here, so the
+      // ProgramBase won't try to tell the user how to run the program
+      // just because we got a bad egg file.
+      exit(1);
+    }
+
+    _eggs.push_back(data);
+  }
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggMultiFilter::post_command_line
+//       Access: Protected, Virtual
+//  Description: 
+////////////////////////////////////////////////////////////////////
+bool EggMultiFilter::
+post_command_line() {
+  Eggs::iterator ei;
+  for (ei = _eggs.begin(); ei != _eggs.end(); ++ei) {
+    EggData *data = (*ei);
+    if (_got_coordinate_system) {
+      data->set_coordinate_system(_coordinate_system);
+    }
+    append_command_comment(*data);
+  }
+  
+  return EggMultiBase::post_command_line();
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: EggMultiFilter::write_eggs
+//       Access: Protected
+//  Description: Writes out all of the egg files in the _eggs vector,
+//               to the output directory if one is specified, or over
+//               the input files if -inplace was specified.
+////////////////////////////////////////////////////////////////////
+void EggMultiFilter::
+write_eggs() {
+  Eggs::iterator ei;
+  for (ei = _eggs.begin(); ei != _eggs.end(); ++ei) {
+    EggData *data = (*ei);
+    Filename filename = data->get_egg_filename();
+
+    if (_got_output_filename) {
+      nassertv(!_inplace && !_got_output_dirname && _eggs.size() == 1);
+      filename = _output_filename;
+
+    } else if (_got_output_dirname) {
+      nassertv(!_inplace);
+      filename.set_dirname(_output_dirname);
+
+    } else {
+      nassertv(_inplace);
+    }
+
+    if (!data->write_egg(filename)) {
+      // Error writing an egg file; abort.
+      exit(1);
+    }
+  }
+}

+ 40 - 0
pandatool/src/eggbase/eggMultiFilter.h

@@ -0,0 +1,40 @@
+// Filename: eggMultiFilter.h
+// Created by:  drose (02Nov00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef EGGMULTIFILTER_H
+#define EGGMULTIFILTER_H
+
+#include <pandatoolbase.h>
+
+#include "eggMultiBase.h"
+
+////////////////////////////////////////////////////////////////////
+// 	 Class : EggMultiFilter
+// Description : This is a base class for a program that reads in a
+//               number of egg files, operates on them, and writes
+//               them out again (presumably to a different directory).
+////////////////////////////////////////////////////////////////////
+class EggMultiFilter : public EggMultiBase {
+public:
+  EggMultiFilter(bool allow_empty = false);
+
+protected:
+  virtual bool handle_args(Args &args);
+  virtual bool post_command_line();
+
+  void write_eggs();
+
+protected:
+  bool _allow_empty;
+  bool _got_output_filename;
+  Filename _output_filename;
+  bool _got_output_dirname;
+  Filename _output_dirname;
+  bool _inplace;
+};
+
+#endif
+
+

+ 1 - 1
pandatool/src/eggprogs/eggTrans.cxx

@@ -13,7 +13,7 @@
 EggTrans::
 EggTrans() {
   set_program_description
-    ("This program reads an egg file and writes an essentially equivalent "
+    ("egg-trans reads an egg file and writes an essentially equivalent "
      "egg file to the standard output, or to the file specified with -o.  "
      "Some simple operations on the egg file are supported."); 
 }

+ 7 - 1
pandatool/src/flt/fltHeader.cxx

@@ -56,6 +56,7 @@ FltHeader() : FltBeadID(this) {
 
   _vertex_lookups_stale = false;
   _current_vertex_offset = 0;
+  _got_color_palette = false;
   _got_eyepoint_trackplane_palette = false;
 
   _auto_attr_update = AU_if_missing;
@@ -1264,6 +1265,11 @@ extract_color_palette(FltRecordReader &reader) {
   nassertr(reader.get_opcode() == FO_color_palette, false);
   DatagramIterator &iterator = reader.get_iterator();
 
+  if (_got_color_palette) {
+    nout << "Warning: multiple color palettes found.\n";
+  }
+  _got_color_palette = true;
+
   static const int expected_color_entries = 1024;
 
   iterator.skip_bytes(128);
@@ -1559,7 +1565,7 @@ write_eyepoint_palette(FltRecordWriter &writer) const {
     return FE_ok;
   }
 
-  writer.set_opcode(FO_color_palette);
+  writer.set_opcode(FO_eyepoint_palette);
   Datagram &datagram = writer.update_datagram();
   datagram.pad_bytes(4);
 

+ 1 - 0
pandatool/src/flt/fltHeader.h

@@ -236,6 +236,7 @@ private:
 
 
   // Support for the color palette.
+  bool _got_color_palette;
   typedef vector<FltPackedColor> Colors;
   typedef map<int, string> ColorNames;
   Colors _colors;

+ 6 - 0
pandatool/src/fltprogs/fltCopy.cxx

@@ -141,6 +141,11 @@ copy_flt_file(const Filename &source, const Filename &dest,
     }
   }
 
+  // Remove all the textures from the palette, and then add back only
+  // those we found in use.  This way we don't copy a file that
+  // references bogus textures.
+  header->clear_textures();
+
   Textures::const_iterator ti;
   for (ti = textures.begin(); ti != textures.end(); ++ti) {
     FltTexture *tex = (*ti);
@@ -164,6 +169,7 @@ copy_flt_file(const Filename &source, const Filename &dest,
       // filename, relative to the flt file.
       tex->_filename = dir->get_rel_to(texture_dir) + "/" + 
 	texture_filename.get_basename();
+      header->add_texture(tex);
     }
   }
 

+ 3 - 2
pandatool/src/fltprogs/fltCopy.h

@@ -11,6 +11,7 @@
 #include "cvsCopy.h"
 
 #include <dSearchPath.h>
+#include <pointerTo.h>
 
 #include <set>
 
@@ -54,8 +55,8 @@ private:
 		    bool new_file);
 
 
-  typedef set<FltExternalReference *> Refs;
-  typedef set<FltTexture *> Textures;
+  typedef set<PT(FltExternalReference)> Refs;
+  typedef set<PT(FltTexture)> Textures;
 
   void scan_flt(FltRecord *record, Refs &refs, Textures &textures);
 

+ 49 - 0
pandatool/src/progbase/programBase.cxx

@@ -567,6 +567,55 @@ dispatch_int(const string &opt, const string &arg, void *var) {
   return true;
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: ProgramBase::dispatch_int_pair
+//       Access: Protected
+//  Description: Standard dispatch function for an option that takes
+//               a pair of integer parameters.  The data pointer is to
+//               an array of two integers.
+////////////////////////////////////////////////////////////////////
+bool ProgramBase::
+dispatch_int_pair(const string &opt, const string &arg, void *var) {
+  if (arg.empty()) {
+    nout << "-" << opt
+	 << " requires an pair of integers separated by a comma.\n";
+    return false;
+  }
+
+  size_t comma = arg.find(',');
+  if (comma == string::npos) {
+    nout << "-" << opt
+	 << " requires an pair of integers separated by a comma.\n";
+    return false;
+  }
+
+  string first = arg.substr(0, comma);
+  string second = arg.substr(comma + 1);
+
+  int *ip = (int *)var;
+  char *endptr;
+
+  const char *first_str = first.c_str();
+  ip[0] = strtol(first_str, &endptr, 0);
+
+  if (*endptr != '\0') {
+    nout << "Invalid integer parameter for -" << opt << ": " 
+	 << first << "\n";
+    return false;
+  }
+
+  const char *second_str = second.c_str();
+  ip[1] = strtol(second_str, &endptr, 0);
+
+  if (*endptr != '\0') {
+    nout << "Invalid integer parameter for -" << opt << ": " 
+	 << second << "\n";
+    return false;
+  }
+
+  return true;
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: ProgramBase::dispatch_double
 //       Access: Protected

+ 1 - 0
pandatool/src/progbase/programBase.h

@@ -61,6 +61,7 @@ protected:
   bool dispatch_none(const string &opt, const string &arg, void *);
   bool dispatch_count(const string &opt, const string &arg, void *var);
   bool dispatch_int(const string &opt, const string &arg, void *var);
+  bool dispatch_int_pair(const string &opt, const string &arg, void *var);
   bool dispatch_double(const string &opt, const string &arg, void *var);
   bool dispatch_string(const string &opt, const string &arg, void *var);
   bool dispatch_filename(const string &opt, const string &arg, void *var);