diff --git a/src/document/Document.js b/src/document/Document.js index 74285f2b..287a220b 100644 --- a/src/document/Document.js +++ b/src/document/Document.js @@ -37,6 +37,8 @@ var Document = this.Document = Base.extend({ this.symbols = []; this.views = [new DocumentView(this)]; this.activeView = this.views[0]; + this._selectedItems = {}; + this._selectedItemCount = 0; }, getCurrentStyle: function() { @@ -56,41 +58,53 @@ var Document = this.Document = Base.extend({ return false; }, - getSelectionContext: function(param) { - var context = this._selectionContext; - if (!context) { - var canvas = CanvasProvider.getCanvas(this.size); - context = this._selectionContext = canvas.getContext('2d'); - context.strokeWidth = 1; + getSelectedItems: function() { + // TODO: return groups if their children are all selected, + // and filter out their children from the list. + var items = []; + Base.each(this._selectedItems, function(item) { + items.push(item); + }); + return items; + }, + + // TODO: implement setSelectedItems? + + _selectItem: function(item, select) { + if (select) { + this._selectedItemCount++; + this._selectedItems[item.getId()] = item; + } else { + this._selectedItemCount--; + delete this._selectedItems[item.getId()]; } - context.strokeStyle = context.fillStyle = param.layerColor; - return context; }, draw: function() { if (this.canvas) { + var context = this.context; // Initial tests conclude that clearing the canvas using clearRect // is always faster than setting canvas.width = canvas.width // http://jsperf.com/clearrect-vs-setting-width/7 - this.context.clearRect(0, 0, + context.clearRect(0, 0, this.size.width + 1, this.size.height + 1); - this.context.save(); + context.save(); var param = { offset: new Point(0, 0) }; - for (var i = 0, l = this.layers.length; i < l; i++) { - // TODO: use Layer#color: - param.layerColor = '#4f7aff'; - Item.draw(this.layers[i], this.context, param); - } - this.context.restore(); - - // If, during drawing, one of the paths was selected, there will - // be a selectionContext which needs to be composited onto the - // canvas: - if (this._selectionContext) { - var canvas = this._selectionContext.canvas; - this.context.drawImage(canvas, 0, 0); - CanvasProvider.returnCanvas(canvas); - this._selectionContext = null; + for (var i = 0, l = this.layers.length; i < l; i++) + Item.draw(this.layers[i], context, param); + context.restore(); + + // Draw the selection of the selected items in the document: + if (this._selectedItemCount > 0) { + context.save(); + context.strokeWidth = 1; + // Todo: use Layer#color + context.strokeStyle = context.fillStyle = '#4f7aff'; + param = { selection: true }; + Base.each(this._selectedItems, function(item) { + item.draw(context, param); + }); + context.restore(); } } }, diff --git a/src/item/Item.js b/src/item/Item.js index f1b069db..b2169462 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -19,7 +19,7 @@ var Item = this.Item = Base.extend({ initialize: function() { paper.document.activeLayer.appendTop(this); - this.setStyle(this.document.getCurrentStyle()); + this.setStyle(this._document.getCurrentStyle()); }, /** @@ -60,20 +60,39 @@ var Item = this.Item = Base.extend({ child.setSelected(selected); } } else { - this._selected = selected; + if (selected != this._selected) { + // TODO: when an item is removed or moved to another + // document, it needs to be removed from _selectedItems + this._selected = selected; + this._document._selectItem(this, selected); + } } }, - getSelected: function() { - if (this._children) { - for (var i = 0, l = this._children.length; i < l; i++) { - var child = this._children[i]; - if (child.getSelected()) + isSelected: function() { + if (this.children) { + for (var i = 0, l = this.children.length; i < l; i++) { + if (this.children[i].isSelected()) return true; } } else { return !!this._selected; } + return false; + }, + + getDocument: function() { + return this._document; + }, + + setDocument: function(document) { + if (document != this._document) { + this._document = document; + if (this.children) { + for (var i = 0, l = this.children.length; i < l; i++) + this.children[i].setDocument(document); + } + } }, // TODO: isFullySelected / setFullySelected @@ -252,6 +271,8 @@ var Item = this.Item = Base.extend({ * Removes the item. */ remove: function() { + if(this.isSelected()) + this.setSelected(false); return this.removeFromParent(); }, @@ -749,7 +770,7 @@ var Item = this.Item = Base.extend({ item.removeFromParent(); this.children.splice(top ? this.children.length : 0, 0, item); item.parent = this; - item.document = this.document; + item.setDocument(this._document); return true; } return false; @@ -763,7 +784,7 @@ var Item = this.Item = Base.extend({ item.parent.children.splice(item.getIndex() + (above ? 1 : -1), 0, this); this.parent = item.parent; - this.document = item.document; + this.setDocument(item._document); return true; } return false; diff --git a/src/item/Layer.js b/src/item/Layer.js index 1958d0bb..6a41c2a3 100644 --- a/src/item/Layer.js +++ b/src/item/Layer.js @@ -19,13 +19,13 @@ var Layer = this.Layer = Group.extend({ initialize: function() { this.children = []; - this.document = paper.document; - this.document.layers.push(this); + this._document = paper.document; + this._document.layers.push(this); this.activate(); }, getIndex: function() { - return this.parent ? this.base() : this.document.layers.indexOf(this); + return this.parent ? this.base() : this._document.layers.indexOf(this); }, /** @@ -34,7 +34,7 @@ var Layer = this.Layer = Group.extend({ */ removeFromParent: function() { if (!this.parent) { - return !!this.document.layers.splice(this.getIndex(), 1).length; + return !!this._document.layers.splice(this.getIndex(), 1).length; } else { return this.base(); } @@ -42,16 +42,16 @@ var Layer = this.Layer = Group.extend({ getNextSibling: function() { return this.parent ? this.base() - : this.document.layers[this.getIndex() + 1] || null; + : this._document.layers[this.getIndex() + 1] || null; }, getPreviousSibling: function() { return this.parent ? this.base() - : this.document.layers[this.getIndex() - 1] || null; + : this._document.layers[this.getIndex() - 1] || null; }, activate: function() { - this.document.activeLayer = this; + this._document.activeLayer = this; } }, new function () { function move(above) { @@ -59,9 +59,9 @@ var Layer = this.Layer = Group.extend({ // if the item is a layer and contained within Document#layers if (item instanceof Layer && !item.parent) { this.removeFromParent(); - item.document.layers.splice(item.getIndex() + (above ? 1 : -1), + item._document.layers.splice(item.getIndex() + (above ? 1 : -1), 0, this); - this.document = item.document; + this.setDocument(item._document); return true; } else { return this.base(item); diff --git a/src/item/PlacedSymbol.js b/src/item/PlacedSymbol.js index ba85be27..2dbbf029 100644 --- a/src/item/PlacedSymbol.js +++ b/src/item/PlacedSymbol.js @@ -53,13 +53,14 @@ var PlacedSymbol = this.PlacedSymbol = Item.extend({ }, draw: function(ctx, param) { - ctx.save(); - this.matrix.applyToContext(ctx); - Item.draw(this.symbol.getDefinition(), ctx, param); - ctx.restore(); - if (this.getSelected()) { + if (param.selection) { Item.drawSelectedBounds(this.symbol._definition.getStrokeBounds(), - this.document.getSelectionContext(param), this.matrix); + ctx, this.matrix); + } else { + ctx.save(); + this.matrix.applyToContext(ctx); + Item.draw(this.symbol.getDefinition(), ctx, param); + ctx.restore(); } } diff --git a/src/item/Raster.js b/src/item/Raster.js index c3c35da0..c7b44aa9 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -202,15 +202,15 @@ var Raster = this.Raster = Item.extend({ }, draw: function(ctx, param) { - ctx.save(); - this.matrix.applyToContext(ctx); - ctx.drawImage(this._canvas || this._image, - -this._size.width / 2, -this._size.height / 2); - ctx.restore(); - if (this.getSelected()) { + if (param.selection) { var bounds = new Rectangle(this._size).setCenter(0, 0); - Item.drawSelectedBounds(bounds, - this.document.getSelectionContext(param), this.matrix); + Item.drawSelectedBounds(bounds, ctx, this.matrix); + } else { + ctx.save(); + this.matrix.applyToContext(ctx); + ctx.drawImage(this._canvas || this._image, + -this._size.width / 2, -this._size.height / 2); + ctx.restore(); } } }, new function() { diff --git a/src/load.js b/src/load.js index 7b9f27c5..7593512e 100644 --- a/src/load.js +++ b/src/load.js @@ -44,6 +44,7 @@ var sources = [ 'src/path/Segment.js', 'src/path/SegmentPoint.js', + 'src/path/SelectionState.js', 'src/path/Curve.js', 'src/path/CurveLocation.js', 'src/path/PathItem.js', diff --git a/src/paper.js b/src/paper.js index 09ac6147..c16ae7cf 100644 --- a/src/paper.js +++ b/src/paper.js @@ -92,6 +92,7 @@ Base.inject({ //#include "path/Segment.js" //#include "path/SegmentPoint.js" +//#include "path/SelectionState.js" //#include "path/Curve.js" //#include "path/CurveLocation.js" //#include "path/PathItem.js" diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 6454cbcc..872fec2d 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -19,9 +19,8 @@ var CompoundPath = this.CompoundPath = PathItem.extend({ this.base(); this.children = []; if (items) { - for (var i = 0, l = items.length; i < l; i++) { + for (var i = 0, l = items.length; i < l; i++) this.appendTop(items[i]); - } } }, @@ -29,11 +28,11 @@ var CompoundPath = this.CompoundPath = PathItem.extend({ // code (from a utility script?) getBounds: function() { if (this.children.length) { - var rect = this.children[0].getBounds(); - var x1 = rect.x; - var y1 = rect.y; - var x2 = rect.x + rect.width; - var y2 = rect.y + rect.height; + var rect = this.children[0].getBounds(), + x1 = rect.x, + y1 = rect.y, + x2 = rect.x + rect.width, + y2 = rect.y + rect.height; for (var i = 1, l = this.children.length; i < l; i++) { var rect2 = this.children[i].getBounds(); x1 = Math.min(rect2.x, x1); @@ -64,9 +63,8 @@ var CompoundPath = this.CompoundPath = PathItem.extend({ }, smooth: function() { - for (var i = 0, l = this.children.length; i < l; i++) { + for (var i = 0, l = this.children.length; i < l; i++) this.children[i].smooth(); - } }, moveTo: function() { @@ -79,9 +77,8 @@ var CompoundPath = this.CompoundPath = PathItem.extend({ var firstChild = this.children[0]; ctx.beginPath(); param.compound = true; - for (var i = 0, l = this.children.length; i < l; i++) { + for (var i = 0, l = this.children.length; i < l; i++) Item.draw(this.children[i], ctx, param); - } firstChild.setContextStyles(ctx); var fillColor = firstChild.getFillColor(), strokeColor = firstChild.getStrokeColor(); @@ -106,15 +103,14 @@ var CompoundPath = this.CompoundPath = PathItem.extend({ var fields = { moveBy: function() { - var point = arguments.length ? Point.read(arguments) : new Point(); - var path = getCurrentPath(this); - var current = path.segments[path.segments.length - 1].point; + var point = arguments.length ? Point.read(arguments) : new Point(), + path = getCurrentPath(this), + current = path.segments[path.segments.length - 1]._point; this.moveTo(current.add(point)); }, closePath: function() { - var path = getCurrentPath(this); - path.closed = true; + getCurrentPath(this).closed = true; } }; diff --git a/src/path/Curve.js b/src/path/Curve.js index 1ee7d559..40d2a4b0 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -140,6 +140,17 @@ var Curve = this.Curve = Base.extend({ return curves && (curves[this._index1 - 1] || this._path.closed && curves[curves.length - 1]) || null; }, + + // TODO: port back to Scriptographer? + setSelected: function(selected) { + this.getHandle1().setSelected(selected); + this.getHandle2().setSelected(selected); + }, + + // TODO: port back to Scriptographer? + isSelected: function() { + return this.getHandle1().isSelected() && this.getHandle2.isSelected(); + }, getCurveValues: function() { var p1 = this._segment1._point, diff --git a/src/path/Path.js b/src/path/Path.js index 952a40ee..33d64adb 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -20,6 +20,7 @@ var Path = this.Path = PathItem.extend({ initialize: function(segments) { this.base(); this.closed = false; + this._selectedSegmentCount = 0; // Support both passing of segments as array or arguments // If it is an array, it can also be a description of a point, so // check its first entry for object as well @@ -128,6 +129,29 @@ var Path = this.Path = PathItem.extend({ } }, + isSelected: function() { + return this._selectedSegmentCount > 0; + }, + + setSelected: function(selected) { + var wasSelected = this.isSelected(), + length = this._segments.length; + if (!wasSelected != !selected && length) + this._document._selectItem(this, selected); + this._selectedSegmentCount = selected ? length : 0; + for (var i = 0; i < length; i++) + this._segments[i]._selectionState = selected + ? SelectionState.POINT : null; + }, + + isFullySelected: function() { + return this._selectedSegmentCount == this._segments.length; + }, + + setFullySelected: function(selected) { + this.setSelected(selected); + }, + // TODO: pointsToCurves([tolerance[, threshold[, cornerRadius[, scale]]]]) // TODO: curvesToPoints([maxPointDistance[, flatness]]) // TODO: reduceSegments([flatness]) @@ -142,22 +166,22 @@ var Path = this.Path = PathItem.extend({ join: function(path) { if (path != null) { - var segments = path.segments; - var last1 = this.getLastSegment(); - var last2 = path.getLastSegment(); - if (last1.getPoint().equals(last2.getPoint())) + var segments = path.segments, + last1 = this.getLastSegment(), + last2 = path.getLastSegment(); + if (last1._point.equals(last2._point)) path.reverse(); var first2 = path.getFirstSegment(); - if (last1.getPoint().equals(first2.getPoint())) { - last1.setHandleOut(first2.getHandleOut()); + if (last1._point.equals(first2._point)) { + last1.setHandleOut(first2._handleOut); for (var i = 1, l = segments.length; i < l; i++) this._add(segments[i]); } else { var first1 = this.getFirstSegment(); - if (first1.getPoint().equals(first2.getPoint())) + if (first1._point.equals(first2._point)) path.reverse(); - if (first1.getPoint().equals(last2.getPoint())) { - first1.setHandleIn(last2.getHandleIn()); + if (first1._point.equals(last2._point)) { + first1.setHandleIn(last2._handleIn); // Prepend all segments from path except last one for (var i = 0, l = segments.length - 1; i < l; i++) this._add(segments[i], 0); @@ -170,8 +194,8 @@ var Path = this.Path = PathItem.extend({ // Close if they touch in both places var first1 = this.getFirstSegment(); last1 = this.getLastSegment(); - if (last1.getPoint().equals(first1.getPoint())) { - first1.setHandleIn(last1.getHandleIn()); + if (last1._point.equals(first1._point)) { + first1.setHandleIn(last1._handleIn); last1.remove(); this.closed = true; } @@ -182,13 +206,13 @@ var Path = this.Path = PathItem.extend({ // todo: getLocation(point, precision) getLocation: function(length) { - var curves = this.getCurves(); - var currentLength = 0; + var curves = this.getCurves(), + currentLength = 0; for (var i = 0, l = curves.length; i < l; i++) { - var startLength = currentLength; - var curve = curves[i]; + var startLength = currentLength, + curve = curves[i]; currentLength += curve.getLength(); - if(currentLength >= length) { + if (currentLength >= length) { // found the segment within which the length lies var t = curve.getParameter(length - startLength); return new CurveLocation(curve, t); @@ -206,12 +230,12 @@ var Path = this.Path = PathItem.extend({ getLength: function(/* location */) { var location; - if(arguments.length) + if (arguments.length) location = arguments[0]; - var curves = this.getCurves(); - var index = location - ? location.getIndex() - : curves.length; + var curves = this.getCurves(), + index = location + ? location.getIndex() + : curves.length; if (index != -1) { var length = 0; for (var i = 0; i < index; i++) @@ -254,26 +278,28 @@ var Path = this.Path = PathItem.extend({ function drawHandles(ctx, segments) { for (var i = 0, l = segments.length; i < l; i++) { var segment = segments[i], - handleIn = segment.handleIn, - handleOut = segment.handleOut, - point = segment.point, - rounded = point.round(); + point = segment._point, + pointSelected = segment._selectionState == SelectionState.POINT; // TODO: draw handles depending on selection state of // segment.point and neighbouring segments. - drawHandle(ctx, point, handleIn); - drawHandle(ctx, point, handleOut); + if (pointSelected || segment.isSelected(segment._handleIn)) + drawHandle(ctx, point, segment._handleIn); + if (pointSelected || segment.isSelected(segment._handleOut)) + drawHandle(ctx, point, segment._handleOut); // Draw a rectangle at segment.point: ctx.save(); ctx.beginPath(); - ctx.rect(rounded.x - 2, rounded.y - 2, 4, 4); + ctx.rect(point._x - 2, point._y - 2, 4, 4); ctx.fill(); // TODO: Only draw white rectangle if point.isSelected() // is false: - ctx.beginPath(); - ctx.rect(rounded.x - 1, rounded.y - 1, 2, 2); - ctx.fillStyle = '#ffffff'; - ctx.fill(); - ctx.restore(); + if (!pointSelected) { + ctx.beginPath(); + ctx.rect(point._x - 1, point._y - 1, 2, 2); + ctx.fillStyle = '#ffffff'; + ctx.fill(); + ctx.restore(); + } } } @@ -281,12 +307,11 @@ var Path = this.Path = PathItem.extend({ if (!handle.isZero()) { handle = handle.add(point); ctx.beginPath(); - ctx.moveTo(point.x, point.y); + ctx.moveTo(point._x, point._y); ctx.lineTo(handle.x, handle.y); ctx.stroke(); ctx.beginPath(); - var rounded = handle.round(); - ctx.rect(rounded.x - 1, rounded.y - 1, 2, 2); + ctx.rect(handle.x - 1, handle.y - 1, 2, 2); ctx.stroke(); } } @@ -301,8 +326,8 @@ var Path = this.Path = PathItem.extend({ for (var i = 0; i < length; i++) { var segment = segments[i], point = segment._point, - x = point.x, - y = point.y, + x = point._x, + y = point._y, handleIn = segment._handleIn; if (i == 0) { ctx.moveTo(x, y); @@ -312,34 +337,34 @@ var Path = this.Path = PathItem.extend({ } else { ctx.bezierCurveTo( outX, outY, - handleIn.x + x, handleIn.y + y, + handleIn._x + x, handleIn._y + y, x, y ); } } handleOut = segment._handleOut; - outX = handleOut.x + x; - outY = handleOut.y + y; + outX = handleOut._x + x; + outY = handleOut._y + y; } if (this.closed && length > 1) { var segment = segments[0], point = segment._point, - x = point.x, - y = point.y, + x = point._x, + y = point._y, handleIn = segment._handleIn; - ctx.bezierCurveTo(outX, outY, handleIn.x + x, handleIn.y + y, x, y); + ctx.bezierCurveTo(outX, outY, handleIn._x + x, handleIn._y + y, x, y); ctx.closePath(); } - // If the path is part of a compound path or doesn't have a fill or - // stroke, there is no need to continue. - var fillColor = this.getFillColor(), - strokeColor = this.getStrokeColor(); - // If we are drawing onto the selection canvas, stroke the - // path and draw its handles. + // If we are drawing the selection of a path, stroke it and draw + // its handles: if (param.selection) { ctx.stroke(); - drawHandles(ctx, this.segments); + drawHandles(ctx, this._segments); } else { + // If the path is part of a compound path or doesn't have a fill or + // stroke, there is no need to continue. + var fillColor = this.getFillColor(), + strokeColor = this.getStrokeColor(); if (!param.compound && (fillColor || strokeColor)) { this.setContextStyles(ctx); ctx.save(); @@ -358,14 +383,6 @@ var Path = this.Path = PathItem.extend({ } ctx.restore(); } - // If the path is selected, draw it again on the separate - // selection canvas, which will be composited onto the canvas - // after drawing of the document is complete. - if (this.getSelected()) { - param.selection = true; - this.draw(this.document.getSelectionContext(param), param); - param.selection = false; - } } } }; @@ -400,6 +417,7 @@ var Path = this.Path = PathItem.extend({ this[name] = value; }, []); } else { + this.setSelected(false); this._segments.length = 0; } for(var i = 0; i < length; i++) { @@ -417,10 +435,10 @@ var Path = this.Path = PathItem.extend({ * @return Solution vector. */ function getFirstControlPoints(rhs) { - var n = rhs.length; - var x = []; // Solution vector. - var tmp = []; // Temporary workspace. - var b = 2; + var n = rhs.length, + x = [], // Solution vector. + tmp = [], // Temporary workspace. + b = 2; x[0] = rhs[0] / b; // Decomposition and forward substitution. for (var i = 1; i < n; i++) { @@ -446,8 +464,6 @@ var Path = this.Path = PathItem.extend({ beans: true, smooth: function() { - var segments = this._segments; - // This code is based on the work by Oleg V. Polikarpotchkin, // http://ov-p.spaces.live.com/blog/cns!39D56F0C7A08D703!147.entry // It was extended to support closed paths by averaging overlapping @@ -455,13 +471,15 @@ var Path = this.Path = PathItem.extend({ // Polikarpotchkin's closed curve solution, but reuses the same // algorithm as for open paths, and is probably executing faster as // well, so it is preferred. - var size = segments.length; + var segments = this._segments, + size = segments.length, + n = size, + // Add overlapping ends for averaging handles in closed paths + overlap; + if (size <= 2) return; - var n = size; - // Add overlapping ends for averaging handles in closed paths - var overlap; if (this.closed) { // Overlap up to 4 points since averaging beziers affect the 4 // neighboring points @@ -489,17 +507,17 @@ var Path = this.Path = PathItem.extend({ // Set right hand side X values for (var i = 1; i < n - 1; i++) - rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x; - rhs[0] = knots[0].x + 2 * knots[1].x; - rhs[n - 1] = 3 * knots[n - 1].x; + rhs[i] = 4 * knots[i]._x + 2 * knots[i + 1]._x; + rhs[0] = knots[0]._x + 2 * knots[1]._x; + rhs[n - 1] = 3 * knots[n - 1]._x; // Get first control points X-values var x = getFirstControlPoints(rhs); // Set right hand side Y values for (var i = 1; i < n - 1; i++) - rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y; - rhs[0] = knots[0].y + 2 * knots[1].y; - rhs[n - 1] = 3 * knots[n - 1].y; + rhs[i] = 4 * knots[i]._y + 2 * knots[i + 1]._y; + rhs[0] = knots[0]._y + 2 * knots[1]._y; + rhs[n - 1] = 3 * knots[n - 1]._y; // Get first control points Y-values var y = getFirstControlPoints(rhs); @@ -530,12 +548,12 @@ var Path = this.Path = PathItem.extend({ new Point(x[i], y[i]).subtract(segment._point)); if (i < n - 1) handleIn = new Point( - 2 * knots[i + 1].x - x[i + 1], - 2 * knots[i + 1].y - y[i + 1]); + 2 * knots[i + 1]._x - x[i + 1], + 2 * knots[i + 1]._y - y[i + 1]); else handleIn = new Point( - (knots[n].x + x[n - 1]) / 2, - (knots[n].y + y[n - 1]) / 2); + (knots[n]._x + x[n - 1]) / 2, + (knots[n]._y + y[n - 1]) / 2); } } if (this.closed && handleIn) { @@ -591,8 +609,8 @@ var Path = this.Path = PathItem.extend({ var current = getCurrentSegment(this); // Convert to relative values: current.setHandleOut(new Point( - handle1.x - current._point.x, - handle1.y - current._point.y)); + handle1.x - current._point._x, + handle1.y - current._point._y)); // And add the new segment, with handleIn set to c2 this._add(new Segment(to, handle2.subtract(to), new Point())); }, @@ -626,8 +644,8 @@ var Path = this.Path = PathItem.extend({ var current = getCurrentSegment(this)._point; // handle = (through - (1 - t)^2 * current - t^2 * to) / // (2 * (1 - t) * t) - var t1 = 1 - t; - var handle = through.subtract(current.multiply(t1 * t1)).subtract( + var t1 = 1 - t, + handle = through.subtract(current.multiply(t1 * t1)).subtract( to.multiply(t * t)).divide(2 * t * t1); if (handle.isNaN()) throw new Error( @@ -653,8 +671,8 @@ var Path = this.Path = PathItem.extend({ : middle.add(-step.y, step.x); } - var x1 = current._point.x, x2 = through.x, x3 = to.x, - y1 = current._point.y, y2 = through.y, y3 = to.y, + var x1 = current._point._x, x2 = through.x, x3 = to.x, + y1 = current._point._y, y2 = through.y, y3 = to.y, f = x3 * x3 - x3 * x2 - x1 * x3 + x1 * x2 + y3 * y3 - y3 * y2 - y1 * y3 + y1 * y2, diff --git a/src/path/Segment.js b/src/path/Segment.js index 35e27cec..440f6020 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -125,12 +125,111 @@ var Segment = this.Segment = Base.extend({ return this._path && this._path._segments[this.getIndex() - 1] || null; }, - // TODO: - // isSelected: function() { - // - // } - // - // setSelected: function(pt, selected) + isSelected: function(/* point */) { + var point = arguments.length ? arguments[0] : this._point; + var state = this._selectionState; + if (point == this._point) { + return state == SelectionState.POINT; + } else if (point == this._handleIn) { + return (state & SelectionState.HANDLE_IN) + == SelectionState.HANDLE_IN; + } else if (point == this._handleOut) { + return (state & SelectionState.HANDLE_OUT) + == SelectionState.HANDLE_OUT; + } + return false; + }, + + // Todo: port setSelected(selected) back to Scriptographer + setSelected: function(/* pt, selected */) { + var pt, selected; + if (arguments.length == 2) { + // setSelected(pt, selected) + pt = arguments[0]; + selected = arguments[1]; + } else { + // setSelected(selected) + pt = this._point; + selected = arguments[0]; + } + if (!this._path) + return; + var wasSelected = !!this._selectionState; + var state = this._selectionState, + pointSelected = state == SelectionState.POINT, + handleInSelected = (state & SelectionState.HANDLE_IN) + == SelectionState.HANDLE_IN, + handleOutSelected = (state & SelectionState.HANDLE_OUT) + == SelectionState.HANDLE_OUT, + previous = this.getPrevious(), + next = this.getNext(), + closed = this._path.closed, + segments = this._path._segments, + length = segments.length; + if (length > 1 && closed) { + if (previous == null) + previous = segments[length - 1]; + if (next == null) + next = segments[0]; + } + if (pt == this._point) { + if (pointSelected != selected) { + if (selected) { + handleInSelected = handleOutSelected = false; + } else { + // When deselecting a point, the handles get selected + // instead depending on the selection state of their + // neighbors. + handleInSelected = previous != null + && (previous._point.isSelected() + || previous._handleOut.isSelected()); + handleOutSelected = next != null + && (next._point.isSelected() + || next._handleOut.isSelected()); + } + pointSelected = selected; + } + } else if (pt == this._handleIn) { + if (handleInSelected != selected) { + // When selecting handles, the point get deselected. + if (selected) + pointSelected = false; + handleInSelected = selected; + } + } else if (pt == this._handleOut) { + if (handleOutSelected != selected) { + // When selecting handles, the point get deselected. + if (selected) + pointSelected = false; + handleOutSelected = selected; + } + } + this._selectionState = pointSelected + ? SelectionState.POINT + : handleInSelected + ? handleOutSelected + ? SelectionState.HANDLE_BOTH + : SelectionState.HANDLE_IN + : handleOutSelected + ? SelectionState.HANDLE_OUT + : null; + // If the selection state of the segment has changed, we need to let + // it's path know and possibly add or remove it from + // document._selectedItems + if (wasSelected == !this._selectionState) { + var path = this._path, + selectedItems = path._document._selectedItems; + if (!this._selectionState) { + path._selectedSegmentCount--; + if (path._selectedSegmentCount == 0) + path._document._selectItem(path, false); + } else { + path._selectedSegmentCount++; + if (path._selectedSegmentCount == 1) + path._document._selectItem(path, true); + } + } + }, reverse: function() { return new Segment(this._point, this._handleOut, this._handleIn); @@ -141,8 +240,12 @@ var Segment = this.Segment = Base.extend({ }, remove: function() { - if (this._path && this._path._segments) - return !!this._path._segments.splice(this.getIndex(), 1).length; + if (this._path) { + this._path._segments.splice(this.getIndex(), 1); + if (this.isSelected()) + this._path._selectedSegmentCount--; + return true; + } return false; }, diff --git a/src/path/SegmentPoint.js b/src/path/SegmentPoint.js index c3659939..43ca132e 100644 --- a/src/path/SegmentPoint.js +++ b/src/path/SegmentPoint.js @@ -29,7 +29,15 @@ var SegmentPoint = Point.extend({ this._y = y; // this._segment._markDirty(DirtyFlags.BOUNDS); }, - + + setSelected: function(selected) { + this._segment.setSelected(this, selected); + }, + + isSelected: function() { + return this._segment.isSelected(this); + }, + statics: { create: function(segment, arg1, arg2) { var point; diff --git a/src/path/SelectionState.js b/src/path/SelectionState.js new file mode 100644 index 00000000..f5c76857 --- /dev/null +++ b/src/path/SelectionState.js @@ -0,0 +1,22 @@ +/* + * Paper.js + * + * This file is part of Paper.js, a JavaScript Vector Graphics Library, + * based on Scriptographer.org and designed to be largely API compatible. + * http://paperjs.org/ + * http://scriptographer.org/ + * + * Distributed under the MIT license. See LICENSE file for details. + * + * Copyright (c) 2011, Juerg Lehni & Jonathan Puckey + * http://lehni.org/ & http://jonathanpuckey.com/ + * + * All rights reserved. + */ + +var SelectionState = { + POINT: 1, + HANDLE_IN: 2, + HANDLE_OUT: 4, + HANDLE_BOTH: 6 +}; diff --git a/test/tests/Path.js b/test/tests/Path.js index 1a21dec0..6fe0e4ac 100644 --- a/test/tests/Path.js +++ b/test/tests/Path.js @@ -75,4 +75,17 @@ test('path.remove()', function() { path.remove(); equals(doc.activeLayer.children.length, 0); +}); + + +test('Is the path deselected after setting a new list of segments?', function() { + var doc = new Document(); + var path = new Path([0, 0]); + path.selected = true; + equals(path.selected, true); + equals(doc.selectedItems.length, 1); + + path.segments = [[0, 10]]; + equals(path.selected, false); + equals(doc.selectedItems.length, 0); }); \ No newline at end of file diff --git a/test/tests/Segment.js b/test/tests/Segment.js index 2c87ef98..b4124911 100644 --- a/test/tests/Segment.js +++ b/test/tests/Segment.js @@ -48,4 +48,14 @@ test('segment.remove()', function() { var path = new Path([10, 10], [5, 5], [10, 10]); path.segments[1].remove(); equals(path.segments.length, 2); +}); + +test('segment.selected', function() { + var doc = new Document(); + var path = new Path([10, 20], [50, 100]); + path.segments[0].point.selected = true; + equals(path.segments[0].point.selected, true); + + path.segments[0].point.selected = false; + equals(path.segments[0].point.selected, false); }); \ No newline at end of file diff --git a/test/tests/item.js b/test/tests/item.js index 351618eb..f7191e4f 100644 --- a/test/tests/item.js +++ b/test/tests/item.js @@ -147,4 +147,39 @@ test('reverseChildren()', function() { equals(doc.activeLayer.firstChild == path, false); equals(doc.activeLayer.firstChild == thirdPath, true); equals(doc.activeLayer.lastChild == path, true); -}) \ No newline at end of file +}); + +test('Check item#document when moving items across documents', function() { + var doc1 = new Document(); + var path = new Path(); + var group = new Group(); + group.appendTop(new Path()); + + equals(path.document == doc1, true); + var doc2 = new Document(); + doc2.activeLayer.appendTop(path); + equals(path.document == doc2, true); + + doc2.activeLayer.appendTop(group); + equals(group.children[0].document == doc2, true); +}); + +test('group.selected', function() { + var doc = new Document(); + var path = new Path([0, 0]); + var path2 = new Path([0, 0]); + var group = new Group([path, path2]); + path.selected = true; + equals(group.selected, true); + + path.selected = false; + equals(group.selected, false); + + group.selected = true; + equals(path.selected, true); + equals(path2.selected, true); + + group.selected = false; + equals(path.selected, false); + equals(path2.selected, false); +}); \ No newline at end of file