1 //= require "path.class"
  2 
  3 (function(global){
  4   
  5   "use strict";
  6   
  7   var fabric = global.fabric || (global.fabric = { }),
  8       extend = fabric.util.object.extend,
  9       invoke = fabric.util.array.invoke,
 10       parentSet = fabric.Object.prototype.set,
 11       parentToObject = fabric.Object.prototype.toObject,
 12       camelize = fabric.util.string.camelize,
 13       capitalize = fabric.util.string.capitalize;
 14   
 15   if (fabric.PathGroup) {
 16     fabric.warn('fabric.PathGroup is already defined');
 17     return;
 18   }
 19   
 20   /** 
 21    * @class PathGroup
 22    * @extends fabric.Path
 23    */
 24   fabric.PathGroup = fabric.util.createClass(fabric.Path, /** @scope fabric.PathGroup.prototype */ {
 25     
 26     /**
 27      * @property
 28      * @type String
 29      */
 30     type: 'path-group',
 31     
 32     /**
 33      * @property
 34      * @type Boolean
 35      */
 36     forceFillOverwrite: false,
 37     
 38     /**
 39      * Constructor
 40      * @method initialize
 41      * @param {Array} paths
 42      * @param {Object} [options] Options object
 43      * @return {fabric.PathGroup} thisArg
 44      */
 45     initialize: function(paths, options) {
 46       
 47       options = options || { };
 48       
 49       this.originalState = { };
 50       this.paths = paths;
 51       
 52       this.setOptions(options);
 53       this._initProperties();
 54       
 55       this.setCoords();
 56       
 57       if (options.sourcePath) {
 58         this.setSourcePath(options.sourcePath);
 59       }
 60     },
 61     
 62     /**
 63      * @private
 64      * @method _initProperties
 65      */
 66     _initProperties: function() {
 67       this.stateProperties.forEach(function(prop) {
 68         if (prop === 'fill') {
 69           this.set(prop, this.options[prop]);
 70         }
 71         else if (prop === 'angle') {
 72           this.setAngle(this.options[prop]);
 73         }
 74         else {
 75           this[prop] = this.options[prop];
 76         }
 77       }, this);
 78     },
 79     
 80     /**
 81      * Renders this group on a specified context
 82      * @method render
 83      * @param {CanvasRenderingContext2D} ctx Context to render this instance on
 84      */
 85     render: function(ctx) {
 86       if (this.stub) {
 87         // fast-path, rendering image stub
 88         ctx.save();
 89         
 90         this.transform(ctx);
 91         this.stub.render(ctx, false /* no transform */);
 92         if (this.active) {
 93           this.drawBorders(ctx);
 94           this.drawCorners(ctx);
 95         }
 96         ctx.restore();
 97       }
 98       else {
 99         ctx.save();
100         
101         var m = this.transformMatrix;
102         if (m) {
103           ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
104         }
105         
106         this.transform(ctx);
107         for (var i = 0, l = this.paths.length; i < l; ++i) {
108           this.paths[i].render(ctx, true);
109         }
110         if (this.active) {
111           this.drawBorders(ctx);
112           this.hideCorners || this.drawCorners(ctx);
113         }
114         ctx.restore();
115       }
116     },
117     
118     /**
119      * Sets certain property to a certain value
120      * @method set
121      * @param {String} prop
122      * @param {Any} value
123      * @return {fabric.PathGroup} thisArg
124      */
125     set: function(prop, value) {
126       if ((prop === 'fill' || prop === 'overlayFill') && this.isSameColor()) {
127         this[prop] = value;
128         var i = this.paths.length;
129         while (i--) {
130           this.paths[i].set(prop, value);
131         }
132       }
133       else {
134         // skipping parent "class" - fabric.Path
135         parentSet.call(this, prop, value);
136       }
137       return this;
138     },
139     
140     /**
141      * Returns object representation of this path group
142      * @method toObject
143      * @return {Object} object representation of an instance
144      */
145     toObject: function() {
146       return extend(parentToObject.call(this), {
147         paths: invoke(this.getObjects(), 'clone'),
148         sourcePath: this.sourcePath
149       });
150     },
151     
152     /**
153      * Returns dataless object representation of this path group
154      * @method toDatalessObject
155      * @return {Object} dataless object representation of an instance
156      */
157     toDatalessObject: function() {
158       var o = this.toObject();
159       if (this.sourcePath) {
160         o.paths = this.sourcePath;
161       }
162       return o;
163     },
164     
165      /**
166       * Returns a string representation of this path group
167       * @method toString
168       * @return {String} string representation of an object
169       */
170     toString: function() {
171       return '#<fabric.PathGroup (' + this.complexity() + 
172         '): { top: ' + this.top + ', left: ' + this.left + ' }>';
173     },
174     
175     /**
176      * Returns true if all paths in this group are of same color
177      * @method isSameColor
178      * @return {Boolean} true if all paths are of the same color (`fill`)
179      */
180     isSameColor: function() {
181       var firstPathFill = this.getObjects()[0].get('fill');
182       return this.getObjects().every(function(path) {
183         return path.get('fill') === firstPathFill;
184       });
185     },
186     
187     /**
188       * Returns number representation of object's complexity
189       * @method complexity
190       * @return {Number} complexity
191       */
192     complexity: function() {
193       return this.paths.reduce(function(total, path) {
194         return total + ((path && path.complexity) ? path.complexity() : 0);
195       }, 0);
196     },
197     
198     /**
199       * Makes path group grayscale
200       * @method toGrayscale
201       * @return {fabric.PathGroup} thisArg
202       */
203     toGrayscale: function() {
204       var i = this.paths.length;
205       while (i--) {
206         this.paths[i].toGrayscale();
207       }
208       return this;
209     },
210     
211     /**
212      * Returns all paths in this path group
213      * @method getObjects
214      * @return {Array} array of path objects included in this path group
215      */
216     getObjects: function() {
217       return this.paths;
218     }
219   });
220   
221   /**
222    * @private
223    * @method instantiatePaths
224    */
225   function instantiatePaths(paths) {
226     for (var i = 0, len = paths.length; i < len; i++) {
227       if (!(paths[i] instanceof fabric.Object)) {
228         var klassName = camelize(capitalize(paths[i].type));
229         paths[i] = fabric[klassName].fromObject(paths[i]);
230       }
231     }
232     return paths;
233   }
234   
235   /**
236    * Creates fabric.Triangle instance from an object representation
237    * @static
238    * @method fabric.PathGroup.fromObject
239    * @param {Object} object
240    * @return {fabric.PathGroup}
241    */
242   fabric.PathGroup.fromObject = function(object) {
243     var paths = instantiatePaths(object.paths);
244     return new fabric.PathGroup(paths, object);
245   };
246 })(this);