diff --git a/toonz/sources/include/tmetaimage.h b/toonz/sources/include/tmetaimage.h
index 5726d1a..9d360c7 100644
--- a/toonz/sources/include/tmetaimage.h
+++ b/toonz/sources/include/tmetaimage.h
@@ -181,8 +181,10 @@ protected:
   virtual void onFixData() { }
 
 public:
-  void setDefaults()
-    { onSetDefaults(); }
+  void setDefaults() {
+    { LockEvents lock(*this); onSetDefaults(); }
+    data().touch();
+  }
   void dataChanged(const TVariant &value)
     { if (m_locks == 0) onDataChanged(value); }
   void fixData()
diff --git a/toonz/sources/tnztools/CMakeLists.txt b/toonz/sources/tnztools/CMakeLists.txt
index a333996..db7abdf 100644
--- a/toonz/sources/tnztools/CMakeLists.txt
+++ b/toonz/sources/tnztools/CMakeLists.txt
@@ -143,6 +143,7 @@ set(SOURCES
     assistants/assistantline.cpp
     assistants/assistantellipse.cpp
     assistants/replicatoraffine.cpp
+    assistants/replicatorgrid.cpp
     assistants/replicatormirror.cpp
     editassistantstool.cpp
 )
diff --git a/toonz/sources/tnztools/assistants/replicatorgrid.cpp b/toonz/sources/tnztools/assistants/replicatorgrid.cpp
new file mode 100644
index 0000000..00e1cef
--- /dev/null
+++ b/toonz/sources/tnztools/assistants/replicatorgrid.cpp
@@ -0,0 +1,267 @@
+
+
+// TnzTools includes
+#include <tools/replicator.h>
+#include <tools/modifiers/modifierclone.h>
+
+
+// TnzCore includes
+#include <tgl.h>
+
+
+//*****************************************************************************************
+//    TReplicatorGrid implementation
+//*****************************************************************************************
+
+class TReplicatorGrid final : public TReplicator {
+  Q_DECLARE_TR_FUNCTIONS(TReplicatorAffine)
+public:
+  const TStringId m_idFixAngle;
+  const TStringId m_idFixSkew;
+  const TStringId m_idMirrorA;
+  const TStringId m_idMirrorB;
+  const TStringId m_idCountA;
+  const TStringId m_idCountAInv;
+  const TStringId m_idCountB;
+  const TStringId m_idCountBInv;
+
+protected:
+  TAssistantPoint &m_center;
+  TAssistantPoint &m_a;
+  TAssistantPoint &m_b;
+
+public:
+  TReplicatorGrid(TMetaObject &object):
+    TReplicator(object),
+    m_idFixAngle("fixAngle"),
+    m_idFixSkew("fixSkew"),
+    m_idMirrorA("mirrorA"),
+    m_idMirrorB("mirrorB"),
+    m_idCountA("countA"),
+    m_idCountAInv("countAInv"),
+    m_idCountB("countB"),
+    m_idCountBInv("countBInv"),
+    m_center( addPoint("center", TAssistantPoint::CircleCross) ),
+    m_a     ( addPoint("a",      TAssistantPoint::CircleFill, TPointD(80,   0)) ),
+    m_b     ( addPoint("b",      TAssistantPoint::Circle,     TPointD( 0, -80)) )
+  {
+    addProperty( new TBoolProperty(m_idFixAngle.str(),  getFixAngle()) );
+    addProperty( new TBoolProperty(m_idFixSkew.str(),   getFixSkew()) );
+    addProperty( new TBoolProperty(m_idMirrorA.str(),   getMirrorA()) );
+    addProperty( new TBoolProperty(m_idMirrorB.str(),   getMirrorB()) );
+    addProperty( createCountProperty(m_idCountA, getCountA(), 1) );
+    addProperty( createCountProperty(m_idCountAInv, getCountAInv(), 0) );
+    addProperty( createCountProperty(m_idCountB, getCountB(), 1) );
+    addProperty( createCountProperty(m_idCountBInv, getCountBInv(), 0) );
+  }
+
+  
+  static QString getLocalName()
+    { return tr("Replicator Grid"); }
+
+    
+  void updateTranslation() const override {
+    TReplicator::updateTranslation();
+    setTranslation(m_idFixAngle, tr("Fix Angle"));
+    setTranslation(m_idFixSkew, tr("Fix Skew"));
+    setTranslation(m_idMirrorA, tr("Mirror A"));
+    setTranslation(m_idMirrorB, tr("Mirror B"));
+    setTranslation(m_idCountA, tr("Count A"));
+    setTranslation(m_idCountAInv, tr("Inv. Count A"));
+    setTranslation(m_idCountB, tr("Count B"));
+    setTranslation(m_idCountBInv, tr("Inv. Count B"));
+  }
+
+  
+  inline bool getFixAngle() const
+    { return data()[m_idFixAngle].getBool(); }
+  inline bool getFixSkew() const
+    { return data()[m_idFixSkew].getBool(); }
+  inline bool getMirrorA() const
+    { return data()[m_idMirrorA].getBool(); }
+  inline bool getMirrorB() const
+    { return data()[m_idMirrorB].getBool(); }
+  inline int getCountA() const
+    { return (int)data()[m_idCountA].getDouble(); }
+  inline int getCountAInv() const
+    { return (int)data()[m_idCountAInv].getDouble(); }
+  inline int getCountB() const
+    { return (int)data()[m_idCountB].getDouble(); }
+  inline int getCountBInv() const
+    { return (int)data()[m_idCountBInv].getDouble(); }
+
+protected:
+  inline void setCountA(int x)
+    { if (getCountA() != (double)x) data()[m_idCountA].setDouble((double)x); }
+  inline void setCountAInv(int x)
+    { if (getCountAInv() != (double)x) data()[m_idCountAInv].setDouble((double)x); }
+  inline void setCountB(int x)
+    { if (getCountB() != (double)x) data()[m_idCountB].setDouble((double)x); }
+  inline void setCountBInv(int x)
+    { if (getCountBInv() != (double)x) data()[m_idCountBInv].setDouble((double)x); }
+
+    
+  void onSetDefaults() override {
+    setCountA(4);
+    setCountB(4);
+    TReplicator::onSetDefaults();
+  }
+
+  
+  void onFixData() override {
+    TReplicator::onFixData();
+    setCountA( std::max(1, std::min(multiplierSoftLimit, getCountA())) );
+    setCountAInv( std::max(0, std::min(multiplierSoftLimit, getCountAInv())) );
+    setCountB( std::max(1, std::min(multiplierSoftLimit, getCountB())) );
+    setCountBInv( std::max(0, std::min(multiplierSoftLimit, getCountBInv())) );
+  }
+
+
+  void onFixPoints() override {
+    TPointD &c = m_center.position;
+    TPointD &a = m_a.position;
+    TPointD &b = m_b.position;
+
+    if (getFixAngle()) {
+      double la = tdistance(a, c);
+      double lb = tdistance(b, c);
+      a = TPointD(a.x < c.x ? -la : la, 0) + c;
+      b = TPointD(0, b.y < c.y ? -lb : lb) + c;
+    } else
+    if (getFixSkew()) {
+      TPointD da = a - c;
+      double la = norm2(da);
+      if (la > TConsts::epsilon*TConsts::epsilon) {
+        TPointD db = b - c;
+        TPointD pa(-da.y, da.x);
+        double l = pa*db/la;
+        b = pa*l + c;
+      }
+    }
+  }
+  
+  
+  void onMovePoint(TAssistantPoint &point, const TPointD &position) override {
+    TPointD pc = m_center.position;
+    point.position = position;
+    if (&point == &m_center) {
+      TPointD d = m_center.position - pc;
+      m_a.position += d;
+      m_b.position += d;
+    } else {
+      onFixPoints();
+    }
+  }
+  
+  
+  void onDataFieldChanged(const TStringId &name, const TVariant &value) override {
+    TReplicator::onDataFieldChanged(name, value);
+    if ( name == m_idFixAngle
+      || name == m_idFixSkew )
+        fixPoints();
+  }
+
+  
+  static bool makeMirror(TAffine &mirror, const TPointD &c, const TPointD &x) {
+    if (!(norm2(x) > TConsts::epsilon*TConsts::epsilon))
+      return false;
+    TPointD y(-x.y, x.x);
+    TAffine t0( x.x,  y.x, c.x,
+                x.y,  y.y, c.y );
+    TAffine t1( x.x, -y.x, c.x,
+                x.y, -y.y, c.y );
+    mirror = t1*t0.inv();
+    return true;
+  }
+  
+  
+public:
+  int getMultipler() const override {
+    return (getCountA() + getCountAInv())
+         * (getCountB() + getCountBInv())
+         * (getMirrorA() ? 2 : 1)
+         * (getMirrorB() ? 2 : 1);    
+  }
+  
+  
+  void getModifiers(
+    const TAffine &toTool,
+    TInputModifier::List &outModifiers ) const override
+  {
+    TPointD c = toTool*m_center.position;
+    TPointD da = toTool*m_a.position - c;
+    TPointD db = toTool*m_b.position - c;
+    
+    TAffine ma, mb;
+    
+    bool mirrorA = getMirrorA() && makeMirror(ma, c, da);
+    bool mirrorB = getMirrorB() && makeMirror(ma, c, db);
+
+    int a1 = getCountA();
+    int b1 = getCountB();
+    int a0 = -getCountAInv();
+    int b0 = -getCountBInv();
+    
+    TModifierClone *modifier = new TModifierClone();
+    for(int ib = b0; ib < b1; ++ib)
+    for(int ia = a0; ia < a1; ++ia) {
+      TPointD o = da*ia + db*ib;
+      if (ia || ib)
+        modifier->transforms.push_back(TTrackTransform(
+          TAffine( 1, 0, o.x,
+                   0, 1, o.y ) ));
+      if (mirrorA)
+        modifier->transforms.push_back(TTrackTransform(
+          TAffine( ma.a11, ma.a12, ma.a13 + o.x,
+                   ma.a21, ma.a22, ma.a23 + o.y ) ));
+      if (mirrorB)
+        modifier->transforms.push_back(TTrackTransform(
+          TAffine( mb.a11, mb.a12, mb.a13 + o.x,
+                   mb.a21, mb.a22, mb.a23 + o.y ) ));
+    }
+    outModifiers.push_back(modifier);
+  }
+
+  
+  void draw(TToolViewer*, bool enabled) const override {
+    double alpha = getDrawingAlpha(enabled);
+    double pixelSize = sqrt(tglGetPixelSize2());
+
+    TPointD c = m_center.position;
+    TPointD a = m_a.position;
+    TPointD b = m_b.position;
+    TPointD da = a - c;
+    TPointD db = b - c;
+
+    int a1 = getCountA();
+    int b1 = getCountB();
+    int a0 = -getCountAInv();
+    int b0 = -getCountBInv();
+    
+    bool mirrorA = getMirrorA();
+    bool mirrorB = getMirrorB();
+    
+    // draw base
+    drawSegment(c, a, pixelSize, alpha);
+    drawSegment(c, b, pixelSize, alpha);
+    
+    // draw clones
+    for(int ib = b0; ib < b1; ++ib)
+    for(int ia = a0; ia < a1; ++ia) {
+      TPointD o = c + da*ia + db*ib;
+      if (ia || ib) {
+        drawSegment(o, o + da*0.2, pixelSize, alpha);
+        drawSegment(o, o + db*0.2, pixelSize, alpha);
+      }
+      if (mirrorA) drawSegment(o, o - da*0.2, pixelSize, alpha);
+      if (mirrorB) drawSegment(o, o - db*0.2, pixelSize, alpha);
+    }
+  }
+};
+
+
+//*****************************************************************************************
+//    Registration
+//*****************************************************************************************
+
+static TAssistantTypeT<TReplicatorGrid> replicatorGrid("replicatorGrid");
diff --git a/toonz/sources/tnztools/replicator.cpp b/toonz/sources/tnztools/replicator.cpp
index 411966d..59d1a4c 100644
--- a/toonz/sources/tnztools/replicator.cpp
+++ b/toonz/sources/tnztools/replicator.cpp
@@ -44,7 +44,7 @@ TIntProperty*
 TReplicator::createCountProperty(const TStringId &id, int def, int min, int max) {
   if (min < 0) min = 0;
   if (def < min) def = min;
-  if (max <= 0) max = multiplierSoftLimit - 1;
+  if (max <= 0) max = multiplierSoftLimit;
   assert(min < max && def < max);
   TIntProperty *property = new TIntProperty(id.str(), min, max, def);
   property->setSpinner();