Splitting and Trimming Ink

Splitting and Trimming Ink

We ve covered most editing functions of ink strokes, including creation, deletion, drawing attributes, and transformation. One area in the Tablet PC Platform remains for which digital ink can be modified. In this section, we ll discuss the methods by which ink strokes are split into multiple pieces and clipped or trimmed outside a rectangle.

Splitting Strokes

It is sometimes desirable to split an ink stroke into two or more separate pieces. For example, to implement point-erase we could compute the rectangle intersections of the eraser on the ink stroke and chop out the segments inside the rectangle, or we might want to support the user s ability to select only a portion of a stroke and perform operations such as moving. In this case, we would want to split the ink strokes at the selection boundary before performing any operation.

The Tablet PC Platform provides a method by which an ink stroke can be split into two pieces. It is accomplished with the Stroke class s Split method:

Stroke Stroke.Split(float index)

This method accepts a floating-point index that defines the point along a Stroke object for which the split will occur, and the newly created Stroke object that begins at the split point is returned. The Stroke object for which the method is called on is truncated up to the split point.

The (X,Y) point along the Stroke at which the split occurs is inclusive to both strokes. In other words, the last point in the ink stroke that is split will always equal the first point of the newly created ink stroke.

NOTE
The floating-point index used to specify the split point is based on the ink stroke s polyline representation. If an ink stroke employs curve fitting, a split operation might result in the ink appearing to jump very slightly while the curve fitting is updated to two separate strokes.

Clipping/Trimming Strokes

Clipping ink strokes is similar to splitting them except that any ink outside the clipping area is deleted instead of preserved. Ink strokes can be clipped at the level of Ink object, Strokes collection, and Stroke object:

void Ink.Clip(Rectangle rectangle) void Strokes.Clip(Rectangle rectangle) void Stroke.Clip(Rectangle rectangle)

The rectangle s edge is inclusive of ink strokes and is specified in ink coordinates. In other words, any ink clipped by the rectangle will have at least one point on the rectangle s edge. Similar to the Stroke class s Split method, the Clip methods use the polyline representation of ink strokes to compute the split point thus, the same issue mentioned for curve fitting will apply.

Sample Application StrokeChopper

The StrokeChopper sample application demonstrates splitting and trimming ink strokes by implementing erasing and clipping tools. The eraser can perform stroke-erase, cusp-erase, and point-erase, while the clipping tool allows the user to draw out a rectangle and have ink clipped either inside or outside it, depending on the user s choice.

The stroke-erase and point-erase functionality behave in the same manner as InkOverlay s InkOverlayEditingMode.Delete mode does, but cusp-erase is functionality we haven t seen before. Cusp-based erase is kind of a hybrid of point-level and stroke-level erase when the eraser targets a Stroke object, the ink segment defined by the cusps adjacent to the hit point is removed.

StrokeChopper.cs

//////////////////////////////////////////////////////////////////// // // StrokeChopper.cs // // (c) 2002 Microsoft Press // by Rob Jarrett // // This program demonstrates an implementation of point-level erase // as well as clipping strokes inside or outside of a rectangle. // //////////////////////////////////////////////////////////////////// using System; using System.Drawing; using System.Windows.Forms; using Microsoft.Ink; using MSPress.BuildingTabletApps; public class frmMain : Form { // User interface private InkInputPanel pnlInput; private Button btnFormat; private ComboBox cbxEditMode; private ComboBox cbxEraseMode; private CheckBox cbInside; private InkCollector inkCollector; // Custom editing mode support private enum StrokeChopperEditingMode { Ink, Erase, Clip }; private enum StrokeChopperEraseMode { Stroke, Cusp, Point }; private StrokeChopperEditingMode modeEditing; private StrokeChopperEraseMode modeErase; private bool fClipInside; private Rectangle rcClip; // Entry point of the program [STAThread] static void Main() { Application.Run(new frmMain()); } // Main form setup public frmMain() { SuspendLayout(); // Create and place all of our controls pnlInput = new InkInputPanel(); pnlInput.BackColor = Color.White; pnlInput.BorderStyle = BorderStyle.Fixed3D; pnlInput.Location = new Point(8, 8); pnlInput.Size = new Size(352, 192); btnFormat = new Button(); btnFormat.Location = new Point(8, 204); btnFormat.Size = new Size(50, 20); btnFormat.Text = "Format"; btnFormat.Click += new System.EventHandler(btnFormat_Click); cbxEditMode = new ComboBox(); cbxEditMode.DropDownStyle = ComboBoxStyle.DropDownList; cbxEditMode.Location = new Point(66, 204); cbxEditMode.Size = new Size(72, 20); cbxEditMode.SelectedIndexChanged += new System.EventHandler(cbxEditMode_SelIndexChg); cbxEraseMode = new ComboBox(); cbxEraseMode.DropDownStyle = ComboBoxStyle.DropDownList; cbxEraseMode.Location = new Point(146, 204); cbxEraseMode.Size = new Size(72, 20); cbxEraseMode.SelectedIndexChanged += new System.EventHandler(cbxEraseMode_SelIndexChg); cbInside = new CheckBox(); cbInside.Location = new Point(146, 204); cbInside.Size = new Size(96, 20); cbInside.Text = "Clip Inside"; cbInside.Click += new System.EventHandler(cbInside_Click); // Configure the form itself ClientSize = new Size(368, 236); Controls.AddRange(new Control[] { pnlInput, btnFormat, cbxEditMode, cbxEraseMode, cbInside}); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; Text = "StrokeChopper"; ResumeLayout(false); // Fill up the editing mode combobox cbxEditMode.Items.Add(StrokeChopperEditingMode.Ink); cbxEditMode.Items.Add(StrokeChopperEditingMode.Erase); cbxEditMode.Items.Add(StrokeChopperEditingMode.Clip); // Fill up the erase mode combobox cbxEraseMode.Items.Add(StrokeChopperEraseMode.Stroke); cbxEraseMode.Items.Add(StrokeChopperEraseMode.Cusp); cbxEraseMode.Items.Add(StrokeChopperEraseMode.Point); // Hook up to mouse events to perform custom mode behaviors pnlInput.MouseDown += new MouseEventHandler(pnlInput_MouseDown); pnlInput.MouseMove += new MouseEventHandler(pnlInput_MouseMove); pnlInput.MouseUp += new MouseEventHandler(pnlInput_MouseUp); // Create a new InkCollector, using pnlInput for the collection // area inkCollector = new InkCollector(pnlInput.Handle); // Set up the initial mode settings modeEditing = StrokeChopperEditingMode.Ink; modeErase = StrokeChopperEraseMode.Stroke; fClipInside = true; // Update the UI with the mode settings cbxEditMode.SelectedItem = modeEditing; cbxEraseMode.SelectedItem = modeErase; cbInside.Checked = fClipInside; } // Handle the click of the format button private void btnFormat_Click(object sender, EventArgs e) { FormatInkDlg dlgFormat = new FormatInkDlg(); dlgFormat.DrawingAttributes = inkCollector.DefaultDrawingAttributes; if (dlgFormat.ShowDialog(this) == DialogResult.OK) { inkCollector.DefaultDrawingAttributes = dlgFormat.DrawingAttributes; } } // Handle the selection change of the editing mode combobox private void cbxEditMode_SelIndexChg(object sender, EventArgs e) { modeEditing = (StrokeChopperEditingMode)cbxEditMode.SelectedItem; switch (modeEditing) { case StrokeChopperEditingMode.Ink: inkCollector.Enabled = true; break; case StrokeChopperEditingMode.Erase: inkCollector.Enabled = false; pnlInput.Cursor = System.Windows.Forms.Cursors.Hand; break; case StrokeChopperEditingMode.Clip: inkCollector.Enabled = false; pnlInput.Cursor = System.Windows.Forms.Cursors.Cross; break; } // Update the UI as a result of the mode selection cbxEraseMode.Visible = (modeEditing == StrokeChopperEditingMode.Erase); cbInside.Visible = (modeEditing == StrokeChopperEditingMode.Clip); } // Handle the selection change of the erase mode combobox private void cbxEraseMode_SelIndexChg(object sender, EventArgs e) { modeErase = (StrokeChopperEraseMode)cbxEraseMode.SelectedItem; } // Handle the click of the clip inside checkbox private void cbInside_Click(object sender, EventArgs e) { fClipInside = cbInside.Checked; } // Handle mouse down in the input panel private void pnlInput_MouseDown(object sender, MouseEventArgs e) { if (modeEditing == StrokeChopperEditingMode.Clip) { // Start up clip tracking UI StartClip(e); } } // Handle mouse move in the input panel private void pnlInput_MouseMove(object sender, MouseEventArgs e) { if (modeEditing == StrokeChopperEditingMode.Erase && e.Button == MouseButtons.Left) { // Perform erase mode ContinueErase(e); } else if (modeEditing == StrokeChopperEditingMode.Clip && e.Button == MouseButtons.Left) { // Update clip tracking UI ContinueClip(e); } } // Handle mouse up in the input panel private void pnlInput_MouseUp(object sender, MouseEventArgs e) { if (modeEditing == StrokeChopperEditingMode.Clip) { // Perform ink clip EndClip(e); } } // Perform erasing - dispatch to correct helper function private void ContinueErase(MouseEventArgs e) { Point ptHit = new Point(e.X, e.Y); switch (modeErase) { case StrokeChopperEraseMode.Stroke: { StrokeErase(ptHit); break; } case StrokeChopperEraseMode.Cusp: { CuspErase(ptHit); break; } case StrokeChopperEraseMode.Point: { PointErase(ptHit, 250); break; } } } // Perform stroke-level erase private void StrokeErase(Point ptHit) { // Use a 2-pixel radius for the hit-area Point ptRadius = new Point(2, 2); // Convert hit point and radius to ink space Graphics g = pnlInput.CreateGraphics(); inkCollector.Renderer.PixelToInkSpace(g, ref ptHit); inkCollector.Renderer.PixelToInkSpace(g, ref ptRadius); g.Dispose(); // See what strokes are hit by the cursor Strokes strksHit = inkCollector.Ink.HitTest(ptHit, ptRadius.X); // Any strokes touched by the cursor are deleted foreach (Stroke s in strksHit) { DeleteStroke(s); } } // Perform cusp-level erase private void CuspErase(Point ptHit) { // Use a 2-pixel radius for the hit-area Point ptRadius = new Point(2, 2); // Convert hit point and radius to ink space Graphics g = pnlInput.CreateGraphics(); inkCollector.Renderer.PixelToInkSpace(g, ref ptHit); inkCollector.Renderer.PixelToInkSpace(g, ref ptRadius); g.Dispose(); // See what strokes are hit by the cursor Strokes strksHit = inkCollector.Ink.HitTest(ptHit, ptRadius.X); if (strksHit.Count > 0) { // Any strokes touched by the cursor will have the cusp // closest to the hit point removed and deleted foreach (Stroke s in strksHit) { int[] arrCusps = s.PolylineCusps; if (arrCusps.Length == 2) { // The stroke has no cusps so we'll erase the whole // thing DeleteStroke(s); } else { // Find out where along the stroke was hit float fHit = s.NearestPoint(ptHit); for (int i = 1; i < arrCusps.Length; i++) { if (arrCusps[i] >= fHit) { // Split off the 3rd segment if (i < arrCusps.Length-1) { s.Split(arrCusps[i]); } // Split off the preceding segment if (i > 1) { // There is a segment before so split // off the end of it DeleteStroke(s.Split(arrCusps[i-1])); } else { // No preceding segment to split // so just erase the stroke DeleteStroke(s); } break; } } } } } } // Perform point-level erase private void PointErase(Point ptHit, int nEraserSize) { // Convert hit point to ink space Graphics g = pnlInput.CreateGraphics(); inkCollector.Renderer.PixelToInkSpace(g, ref ptHit); g.Dispose(); // Compute the rectangle to use for the eraser ptHit.Offset(-nEraserSize/2, -nEraserSize/2); Rectangle rcErase = new Rectangle( ptHit, new Size(nEraserSize, nEraserSize)); // See if the eraser hits any strokes in the InkCollector foreach (Stroke s in inkCollector.Ink.Strokes) { StrokeIntersection[] si = s.GetRectangleIntersections(rcErase); // Walk through the hit results for (int i = si.Length-1; i >= 0; i--) { if (si[i].BeginIndex == si[i].EndIndex) { if (si[i].BeginIndex == -1f) { // The entire stroke is contained within the // rectangle, so delete the whole thing DeleteStroke(s); break; } else { // Only one point is intersecting the rectangle // so we don't need to do anything continue; } } else if (si[i].BeginIndex > 0f && si[i].EndIndex > 0f) { // A segment of the stroke segment is within the // rectangle, so split it into three pieces, // throwing away the middle one s.Split(si[i].EndIndex); DeleteStroke(s.Split(si[i].BeginIndex)); } else if ( (si[i].BeginIndex == 0f si[i].BeginIndex == -1f) && si[i].EndIndex > 0f) { // The stroke starts inside the rectangle and ends // outside of it - split it into two and throw away // the first half (i.e. what's inside the rect) s.Split(si[i].EndIndex); DeleteStroke(s); } else if (si[i].BeginIndex > 0f && (si[i].EndIndex == s.GetPoints().Length-1 si[i].EndIndex == -1f)) { // The stroke starts outside the rectangle and ends // inside of it - split it into two parts and throw // away the second half (i.e. what's inside the // rect) DeleteStroke(s.Split(si[i].BeginIndex)); } } } } // Delete a stroke and invalidate the leftover area private void DeleteStroke(Stroke s) { Rectangle rcInvalid = inkCollector.Renderer.Measure(s); inkCollector.Ink.DeleteStroke(s); Graphics g = pnlInput.CreateGraphics(); RendererEx.InkSpaceToPixel( inkCollector.Renderer, g, ref rcInvalid); g.Dispose(); rcInvalid.Inflate(1, 1); pnlInput.Invalidate(rcInvalid); } // Start up clipping UI private void StartClip(MouseEventArgs e) { // Initialize tracking rectangle rcClip = new Rectangle(new Point(e.X, e.Y), new Size(0, 0)); } // Continue the clipping UI private void ContinueClip(MouseEventArgs e) { // Erase the previous tracking rectangle Rectangle rcDraw = rcClip; rcDraw.Location = pnlInput.PointToScreen(rcClip.Location); ControlPaint.DrawReversibleFrame( rcDraw, BackColor, FrameStyle.Dashed); // Update rectangle with new mouse location, constraining to // the input panel's bounds rcClip.Width = e.X - rcClip.X; if (rcClip.Left + rcClip.Width < 0) { rcClip.Width = -rcClip.X; } else if (rcClip.Right > pnlInput.ClientSize.Width) { rcClip.Width = pnlInput.ClientSize.Width - rcClip.X; } rcClip.Height = e.Y - rcClip.Y; if (rcClip.Top + rcClip.Height < 0) { rcClip.Height = -rcClip.Y; } else if (rcClip.Bottom > pnlInput.ClientSize.Height) { rcClip.Height = pnlInput.ClientSize.Height - rcClip.Y; } // Draw the new tracking rectangle rcDraw = rcClip; rcDraw.Location = pnlInput.PointToScreen(rcClip.Location); ControlPaint.DrawReversibleFrame( rcDraw, BackColor, FrameStyle.Dashed); } // End the clipping UI and perform the clip operation private void EndClip(MouseEventArgs e) { // Erase the previous tracking rectangle Rectangle rcDraw = rcClip; rcDraw.Location = pnlInput.PointToScreen(rcClip.Location); ControlPaint.DrawReversibleFrame( rcDraw, BackColor, FrameStyle.Dashed); // Compute the well-formed rectangle to use for clipping Point ptTopLeft = new Point(System.Math.Min(rcClip.X, rcClip.X + rcClip.Width), System.Math.Min(rcClip.Y, rcClip.Y + rcClip.Height)); Point ptBottomRight = new Point(System.Math.Max(rcClip.X, rcClip.X + rcClip.Width), System.Math.Max(rcClip.Y, rcClip.Y + rcClip.Height)); Graphics g = pnlInput.CreateGraphics(); inkCollector.Renderer.PixelToInkSpace(g, ref ptTopLeft); inkCollector.Renderer.PixelToInkSpace(g, ref ptBottomRight); g.Dispose(); Rectangle rcClipInk = new Rectangle(ptTopLeft, new Size(ptBottomRight.X - ptTopLeft.X, ptBottomRight.Y - ptTopLeft.Y)); // Perform the clip operation if (fClipInside) { inkCollector.Ink.Clip(rcClipInk); } else { inkCollector.Ink.ExtractStrokes(rcClipInk, ExtractFlags.RemoveFromOriginal); } // Update the display pnlInput.Invalidate(); } }

The application provides three modes: Ink, Erase, and Clip. The Erase and Clip modes are custom modes that use mouse messages to trigger their functionality, although we just as easily could have used tablet events.

While in Erase mode, mouse movement with the left button down will call either the StrokeErase, CuspErase, or PointErase method to perform the chosen operation. These methods are fairly self-contained, each performing its respective erasing functionality.

The StrokeErase method uses the Ink class s HitTest method to compute the ink strokes within 2 pixels of the erase point; the strokes are then deleted. CuspErase also uses the Ink class s HitTest method to compute the strokes within 2 pixels of the erase point, and then it uses the Stroke class s PolylineCusps property and NearestPoint method to determine the segment of the ink strokes to delete. The PointErase method calls GetRectangleIntersections on every Stroke object to determine the segments of any ink strokes that must be deleted.

NOTE
The PointErase method leverages the Stroke class s GetRectangleIntersections to compute the segments of ink that need to be deleted. I first thought it would be nice to call Ink.ExtractStrokes(rcErase, ExtractFlags.RemoveFromOriginal), but unfortunately the initial version 1 release of the Tablet PC Platform has a bug causing that version of ExtractStrokes to be overzealous in the extraction. If any edge of the extraction rectangle falls exactly on the first point of a stroke, the entire stroke will be extracted. I therefore found it unusable for point-erase. The rest of the ExtractStrokes functionality works wonderfully, though!

The clipping operation s functionality is found in the StartClip, ContinueClip, and EndClip methods. StartClip and ContinueClip perform the rectangle UI animation, and EndClip cleans up the rectangle UI and also performs the clipping operation:

// Perform the clip operation if (fClipInside) { inkCollector.Ink.Clip(rcClipInk); } else { inkCollector.Ink.ExtractStrokes(rcClipInk, ExtractFlags.RemoveFromOriginal); }

Notice how the ExtractStrokes method is used without significant problems. The key difference here with respect to point-erase are the conditions under which the method is called. The user will rarely draw out a rectangle whose edge falls exactly on the first point of a stroke; with point-erase, however, it is very easy to expose the bug discussed in the preceding note because the mouse is dragged along pixel by pixel.

NOTE
It would be a great exercise to write a bug-free version of ExtractStrokes based on the PointErase method.



Building Tablet PC Applications
Building Tablet PC Applications (Pro-Developer)
ISBN: 0735617236
EAN: 2147483647
Year: 2001
Pages: 73

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net