diff --git a/indra/llcommon/indra_constants.h b/indra/llcommon/indra_constants.h
index 99ad5f24731f8a52f49e3f8d71eb60a7d7b016fe..fda84aa5a8c4cf4b4de411b66d3270dc0d8f10e2 100644
--- a/indra/llcommon/indra_constants.h
+++ b/indra/llcommon/indra_constants.h
@@ -154,7 +154,6 @@ const U8 SIM_ACCESS_MAX 	= SIM_ACCESS_ADULT;
 
 // attachment constants
 const S32 MAX_AGENT_ATTACHMENTS = 38;
-const S32 MAX_AGENT_ANIMATED_OBJECT_ATTACHMENTS = 1;
 const U8  ATTACHMENT_ADD = 0x80;
 
 // god levels
diff --git a/indra/newview/llmeshrepository.cpp b/indra/newview/llmeshrepository.cpp
index c2a0393170e02c9705bf17bf3bef45431c6ac3c5..92c2be4a8bfadd7beca82979b3d119d49a954350 100644
--- a/indra/newview/llmeshrepository.cpp
+++ b/indra/newview/llmeshrepository.cpp
@@ -4021,6 +4021,33 @@ void LLMeshRepository::uploadError(LLSD& args)
 	mUploadErrorQ.push(args);
 }
 
+F32 LLMeshRepository::getEstTrianglesHigh(LLUUID mesh_id)
+{
+    F32 triangles_high = 0.f;
+    if (mThread && mesh_id.notNull())
+    {
+        LLMutexLock lock(mThread->mHeaderMutex);
+        LLMeshRepoThread::mesh_header_map::iterator iter = mThread->mMeshHeader.find(mesh_id);
+        if (iter != mThread->mMeshHeader.end() && mThread->mMeshHeaderSize[mesh_id] > 0)
+        {
+            LLSD& header = iter->second;
+            if (header.has("404")
+                || !header.has("lowest_lod")
+                || (header.has("version") && header["version"].asInteger() > MAX_MESH_VERSION))
+            {
+                return 0.f;
+            }
+
+            S32 bytes_high = header["high_lod"]["size"].asInteger();
+            F32 METADATA_DISCOUNT = (F32) gSavedSettings.getU32("MeshMetaDataDiscount");  //discount 128 bytes to cover the cost of LLSD tags and compression domain overhead
+            F32 MINIMUM_SIZE = (F32) gSavedSettings.getU32("MeshMinimumByteSize"); //make sure nothing is "free"
+            F32 bytes_per_triangle = (F32) gSavedSettings.getU32("MeshBytesPerTriangle");
+            triangles_high = llmax((F32) bytes_high-METADATA_DISCOUNT, MINIMUM_SIZE)/bytes_per_triangle;
+        }
+    }
+    return triangles_high;
+}
+
 F32 LLMeshRepository::getStreamingCost(LLUUID mesh_id, F32 radius, S32* bytes, S32* bytes_visible, S32 lod, F32 *unscaled_value)
 {
     if (mThread && mesh_id.notNull())
diff --git a/indra/newview/llmeshrepository.h b/indra/newview/llmeshrepository.h
index 23af837f6fdda3b4a2951c70814245785bced769..28f037b85e803634dd07e5ef16163304dd7f68cb 100644
--- a/indra/newview/llmeshrepository.h
+++ b/indra/newview/llmeshrepository.h
@@ -472,6 +472,7 @@ class LLMeshRepository
 	
 	static LLDeadmanTimer sQuiescentTimer;		// Time-to-complete-mesh-downloads after significant events
 
+    F32 getEstTrianglesHigh(LLUUID mesh_id);
 	F32 getStreamingCost(LLUUID mesh_id, F32 radius, S32* bytes = NULL, S32* visible_bytes = NULL, S32 detail = -1, F32 *unscaled_value = NULL);
 	static F32 getStreamingCost(LLSD& header, F32 radius, S32* bytes = NULL, S32* visible_bytes = NULL, S32 detail = -1, F32 *unscaled_value = NULL);
 
diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp
index 3e8d8883e03a623e816e94b0c9fe7a5ce9963f23..d3f240ac91f14d01e2b045facfb23ea4ac767871 100644
--- a/indra/newview/llselectmgr.cpp
+++ b/indra/newview/llselectmgr.cpp
@@ -663,6 +663,10 @@ void LLSelectMgr::confirmUnlinkObjects(const LLSD& notification, const LLSD& res
 // otherwise. this allows the handle_link method to more finely check
 // the selection and give an error message when the uer has a
 // reasonable expectation for the link to work, but it will fail.
+//
+// AXON - additional check that if the selection includes at least one
+// animated object, the total mesh triangle count cannot exceed the
+// designated limit.
 bool LLSelectMgr::enableLinkObjects()
 {
 	bool new_value = false;
@@ -687,6 +691,10 @@ bool LLSelectMgr::enableLinkObjects()
 			new_value = LLSelectMgr::getInstance()->getSelection()->applyToRootObjects(&func, firstonly);
 		}
 	}
+    if (!LLSelectMgr::getInstance()->getSelection()->checkAnimatedObjectEstTris())
+    {
+        new_value = false;
+    }
 	return new_value;
 }
 
@@ -7421,6 +7429,27 @@ bool LLObjectSelection::applyToObjects(LLSelectedObjectFunctor* func)
 	return result;
 }
 
+bool LLObjectSelection::checkAnimatedObjectEstTris()
+{
+    F32 est_tris = 0;
+    F32 max_tris = 0;
+    S32 anim_count = 0;
+	for (root_iterator iter = root_begin(); iter != root_end(); )
+	{
+		root_iterator nextiter = iter++;
+		LLViewerObject* object = (*nextiter)->getObject();
+		if (!object)
+			continue;
+        if (object->isAnimatedObject())
+        {
+            anim_count++;
+        }
+        est_tris += object->recursiveGetEstTrianglesHigh();
+        max_tris = llmax((F32)max_tris,(F32)object->getAnimatedObjectMaxTris());
+	}
+	return anim_count==0 || est_tris <= max_tris;
+}
+
 bool LLObjectSelection::applyToRootObjects(LLSelectedObjectFunctor* func, bool firstonly)
 {
 	bool result = firstonly ? false : true;
diff --git a/indra/newview/llselectmgr.h b/indra/newview/llselectmgr.h
index fc4b920c5104a374ac35668af54f9a67dc6ab4b6..4e79cb003dfd09db740e1c1b51ac74555616e77d 100644
--- a/indra/newview/llselectmgr.h
+++ b/indra/newview/llselectmgr.h
@@ -339,6 +339,9 @@ class LLObjectSelection : public LLRefCount
 	// returns TRUE is any node is currenly worn as an attachment
 	BOOL isAttachment();
 
+    // AXON validate a potential link against limits
+    bool checkAnimatedObjectEstTris();
+    
 	// Apply functors to various subsets of the selected objects
 	// If firstonly is FALSE, returns the AND of all apply() calls.
 	// Else returns TRUE immediately if any apply() call succeeds (i.e. OR with early exit)
diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp
index 34e7bc0fad2250085394142761e6e2acbba4ef11..0140a63e73dc04ee5db9053927135f8cad569f7b 100644
--- a/indra/newview/llviewerobject.cpp
+++ b/indra/newview/llviewerobject.cpp
@@ -3580,6 +3580,38 @@ F32 LLViewerObject::getLinksetPhysicsCost()
 	return mLinksetPhysicsCost;
 }
 
+F32 LLViewerObject::recursiveGetEstTrianglesHigh() const
+{
+    F32 est_tris = getEstTrianglesHigh();
+    for (child_list_t::const_iterator iter = mChildList.begin();
+         iter != mChildList.end(); iter++)
+    {
+        const LLViewerObject* child = *iter;
+        est_tris += child->recursiveGetEstTrianglesHigh();
+    }
+    return est_tris;
+}
+
+S32 LLViewerObject::getAnimatedObjectMaxTris() const
+{
+    S32 max_tris = 0;
+    LLSD features;
+    if (getRegion())
+    {
+        getRegion()->getSimulatorFeatures(features);
+        if (features.has("AnimatedObjects"))
+        {
+            max_tris = features["AnimatedObjects"]["AnimatedObjectMaxTris"].asInteger();
+        }
+    }
+    return max_tris;
+}
+
+F32 LLViewerObject::getEstTrianglesHigh() const
+{
+    return 0.f;
+}
+
 F32 LLViewerObject::getStreamingCost(S32* bytes, S32* visible_bytes, F32* unscaled_value) const
 {
 	return 0.f;
@@ -3698,7 +3730,6 @@ void LLViewerObject::boostTexturePriority(BOOL boost_children /* = TRUE */)
 	}
 }
 
-
 void LLViewerObject::setLineWidthForWindowSize(S32 window_width)
 {
 	if (window_width < 700)
diff --git a/indra/newview/llviewerobject.h b/indra/newview/llviewerobject.h
index c79ff7bb74048f403986bbaea6268ada28501c2c..a1a7eed002e765c3dabc1e55eed96301ded50220 100644
--- a/indra/newview/llviewerobject.h
+++ b/indra/newview/llviewerobject.h
@@ -357,6 +357,9 @@ class LLViewerObject
 	
 	virtual void setScale(const LLVector3 &scale, BOOL damped = FALSE);
 
+    S32 getAnimatedObjectMaxTris() const;
+    F32 recursiveGetEstTrianglesHigh() const;
+    virtual F32 getEstTrianglesHigh() const;
 	virtual F32 getStreamingCost(S32* bytes = NULL, S32* visible_bytes = NULL, F32* unscaled_value = NULL) const;
 	virtual U32 getTriangleCount(S32* vcount = NULL) const;
 	virtual U32 getHighLODTriangleCount();
diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp
index 5e0c5d68581138a3bd14991bfb2aa95f4b61a3f7..ac5d1b335c08b14937ef8ba1a6f8df616d1b58b8 100644
--- a/indra/newview/llvoavatar.cpp
+++ b/indra/newview/llvoavatar.cpp
@@ -6602,13 +6602,32 @@ U32 LLVOAvatar::getNumAnimatedObjectAttachments() const
 	return num_attachments;
 }
 
+//-----------------------------------------------------------------------------
+// getMaxAnimatedObjectAttachments()
+// Gets from simulator feature if available, otherwise 0.
+//-----------------------------------------------------------------------------
+S32 LLVOAvatar::getMaxAnimatedObjectAttachments() const
+{
+    S32 max_attach = 0;
+    LLSD features;
+    if (getRegion())
+    {
+        getRegion()->getSimulatorFeatures(features);
+        if (features.has("AnimatedObjects"))
+        {
+            max_attach = features["AnimatedObjects"]["MaxAgentAnimatedObjectAttachments"].asInteger();
+        }
+    }
+    return max_attach;
+}
+
 //-----------------------------------------------------------------------------
 // canAttachMoreAnimatedObjects()
 // Returns true if we can attach <n> more animated objects.
 //-----------------------------------------------------------------------------
 BOOL LLVOAvatar::canAttachMoreAnimatedObjects(U32 n) const
 {
-	return (getNumAnimatedObjectAttachments() + n) <= MAX_AGENT_ANIMATED_OBJECT_ATTACHMENTS;
+	return (getNumAnimatedObjectAttachments() + n) <= getMaxAnimatedObjectAttachments();
 }
 
 //-----------------------------------------------------------------------------
diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h
index 1a87a62946e5d78a91bc45f2e0ecc51ab4ca209f..5a40a45eaef6592cb8adfafa6264789d17ea062f 100644
--- a/indra/newview/llvoavatar.h
+++ b/indra/newview/llvoavatar.h
@@ -778,6 +778,7 @@ class LLVOAvatar :
 	LLBBox 				getHUDBBox() const;
 	void 				resetHUDAttachments();
 	BOOL				canAttachMoreObjects(U32 n=1) const;
+    S32					getMaxAnimatedObjectAttachments() const;
     BOOL				canAttachMoreAnimatedObjects(U32 n=1) const;
 protected:
 	U32					getNumAttachments() const; // O(N), not O(1)
diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp
index 403bff5a9e0a7a9544bbf412f28c1d6effa7b782..07032ca0aef0485fd7d900fc485365022442c380 100644
--- a/indra/newview/llvovolume.cpp
+++ b/indra/newview/llvovolume.cpp
@@ -3349,22 +3349,25 @@ bool LLVOVolume::canBeAnimatedObject() const
     {
         return false;
     }
-// AXON remove this check if animated object attachments are allowed
-#if 0
-    if (isAttachment())
-    {
-        return false;
-    }
-#endif
 	if (!getVolume())
 	{
 		return false;
 	}
+    if (!isRootEdit())
+    {
+        return false;
+    }
 	const LLMeshSkinInfo* skin = gMeshRepo.getSkinInfo(getVolume()->getParams().getSculptID(), this);
     if (!skin)
     {
         return false;
     }
+    F32 est_tris = recursiveGetEstTrianglesHigh();
+    if (est_tris > getAnimatedObjectMaxTris())
+    {
+        LL_INFOS() << "est_tris " << est_tris << " exceeds limit " << getAnimatedObjectMaxTris() << LL_ENDL;
+        return false;
+    }
     return true;
 }
 
@@ -3833,6 +3836,15 @@ U32 LLVOVolume::getRenderCost(texture_cost_t &textures) const
 	return (U32)shame;
 }
 
+F32 LLVOVolume::getEstTrianglesHigh() const
+{
+	if (isMesh())
+	{
+		return gMeshRepo.getEstTrianglesHigh(getVolume()->getParams().getSculptID());
+	}
+    return 0.f;
+}
+
 F32 LLVOVolume::getStreamingCost(S32* bytes, S32* visible_bytes, F32* unscaled_value) const
 {
 	F32 radius = getScale().length()*0.5f;
diff --git a/indra/newview/llvovolume.h b/indra/newview/llvovolume.h
index c972d7770e172b92554f43487d648409a14212b6..5891b36da87a7bafee0af3f2292449c16d3c6aab 100644
--- a/indra/newview/llvovolume.h
+++ b/indra/newview/llvovolume.h
@@ -134,6 +134,7 @@ class LLVOVolume : public LLViewerObject
 	/*virtual*/	const LLMatrix4	getRenderMatrix() const;
 				typedef std::map<LLUUID, S32> texture_cost_t;
 				U32 	getRenderCost(texture_cost_t &textures) const;
+    /*virtual*/	F32		getEstTrianglesHigh() const;
 				F32		getStreamingCost(S32* bytes, S32* visible_bytes, F32* unscaled_value) const;
 	/*virtual*/	F32		getStreamingCost(S32* bytes = NULL, S32* visible_bytes = NULL) { return getStreamingCost(bytes, visible_bytes, NULL); }
 
diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml
index 50013656ca60d0cb0e5346607d98c97ef06fe89f..0223d20f0fd00622ea65d92b0165dffbaa8ff91c 100644
--- a/indra/newview/skins/default/xui/en/notifications.xml
+++ b/indra/newview/skins/default/xui/en/notifications.xml
@@ -10620,6 +10620,29 @@ You are not allowed to change this shape.
 Operation would cause the number of attached animated objects to exceed the limit.
   </notification>
 
+  <notification
+   icon="alertmodal.tga"
+   name="NoPermsLinkAnimatedObjectTooLarge"
+   type="notify">
+   <tag>fail</tag>
+Can't link these objects because the resulting animated object would exceed the size limit.
+  </notification>
+
+  <notification
+   icon="alertmodal.tga"
+   name="NoPermsSetFlagAnimatedObjectTooLarge"
+   type="notify">
+   <tag>fail</tag>
+Can't make this object into an animated object because it would exceed the size limit.
+  </notification>
+
+  <notification
+   icon="alertmodal.tga"
+   name="ErrorNoMeshData"
+   type="notify">
+   <tag>fail</tag>
+Server error: cannot complete this operation because mesh data is not loaded.
+  </notification>
 
   <notification
    icon="alertmodal.tga"