1 // Copyright 2000-2005 the Contributors, as shown in the revision logs.
2 // Licensed under the GNU General Public License version 2 ("the License").
3 // You may not use this file except in compliance with the License.
5 package org.ibex.graphics;
7 import org.ibex.util.*;
9 /** an abstract path; may contain splines and arcs */
12 public static final float PX_PER_INCH = 72;
13 public static final float INCHES_PER_CM = (float)0.3937;
14 public static final float INCHES_PER_MM = INCHES_PER_CM / 10;
15 private static final int DEFAULT_PATHLEN = 1000;
16 private static final int NUMSTEPS = 10;
17 private static final float PI = (float)Math.PI;
19 boolean closed = false;
22 protected void add(Curve c) {
23 if (head==null) { tail=head=c; return; }
29 public void addTo(Mesh m, boolean evenOdd) {
30 for(Curve c = head; c != null; c = c.next) c.addTo(m);
34 abstract class Curve {
37 float c1x, c1y, c2x, c2y;
39 public abstract void addTo(Mesh ret);
42 class Line extends Curve {
43 public void addTo(Mesh ret) {
50 class Move extends Curve {
51 public void addTo(Mesh ret) {
53 if (next==null) return;
60 class Arc extends Curve {
61 public void addTo(Mesh ret) {
62 System.out.println("ARC!");
66 float fa = ((int)c2y) >> 1;
67 float fs = ((int)c2y) & 1;
73 // F.6.5: given x1,y1,x2,y2,fa,fs, compute cx,cy,theta1,dtheta
74 float x1_ = (float)Math.cos(phi) * (x1 - x2) / 2 + (float)Math.sin(phi) * (y1 - y2) / 2;
75 float y1_ = -1 * (float)Math.sin(phi) * (x1 - x2) / 2 + (float)Math.cos(phi) * (y1 - y2) / 2;
76 float tmp = (float)Math.sqrt((rx * rx * ry * ry - rx * rx * y1_ * y1_ - ry * ry * x1_ * x1_) /
77 (rx * rx * y1_ * y1_ + ry * ry * x1_ * x1_));
78 float cx_ = (fa == fs ? -1 : 1) * tmp * (rx * y1_ / ry);
79 float cy_ = (fa == fs ? -1 : 1) * -1 * tmp * (ry * x1_ / rx);
80 float cx = (float)Math.cos(phi) * cx_ - (float)Math.sin(phi) * cy_ + (x1 + x2) / 2;
81 float cy = (float)Math.sin(phi) * cx_ + (float)Math.cos(phi) * cy_ + (y1 + y2) / 2;
83 // F.6.4 Conversion from center to endpoint parameterization
84 float ux = 1, uy = 0, vx = (x1_ - cx_) / rx, vy = (y1_ - cy_) / ry;
85 float det = ux * vy - uy * vx;
86 float theta1 = (det < 0 ? -1 : 1) *
87 (float)Math.acos((ux * vx + uy * vy) /
88 ((float)Math.sqrt(ux * ux + uy * uy) * (float)Math.sqrt(vx * vx + vy * vy)));
89 ux = (x1_ - cx_) / rx; uy = (y1_ - cy_) / ry;
90 vx = (-1 * x1_ - cx_) / rx; vy = (-1 * y1_ - cy_) / ry;
91 det = ux * vy - uy * vx;
92 float dtheta = (det < 0 ? -1 : 1) *
93 (float)Math.acos((ux * vx + uy * vy) /
94 ((float)Math.sqrt(ux * ux + uy * uy) * (float)Math.sqrt(vx * vx + vy * vy)));
95 dtheta = dtheta % (float)(2 * Math.PI);
97 if (fs == 0 && dtheta > 0) theta1 -= 2 * PI;
98 if (fs == 1 && dtheta < 0) theta1 += 2 * PI;
100 if (fa == 1 && dtheta < 0) dtheta = 2 * PI + dtheta;
101 else if (fa == 1 && dtheta > 0) dtheta = -1 * (2 * PI - dtheta);
103 // FIXME: integrate F.6.6
104 // FIXME: isn't quite ending where it should...
106 // F.6.3: Parameterization alternatives
107 float theta = theta1;
108 for(int j=0; j<NUMSTEPS; j++) {
109 float rasterx = rx * (float)Math.cos(theta) * (float)Math.cos(phi) -
110 ry * (float)Math.sin(theta) * (float)Math.sin(phi) + cx;
111 float rastery = rx * (float)Math.cos(theta) * (float)Math.sin(phi) +
112 ry * (float)Math.cos(phi) * (float)Math.sin(theta) + cy;
113 ret.add(rasterx, rastery);
114 theta += dtheta / NUMSTEPS;
119 class Bezier extends Curve {
121 public void addTo(Mesh ret) {
122 float ax = next.x - 3 * c2x + 3 * c1x - x;
123 float bx = 3 * c2x - 6 * c1x + 3 * x;
124 float cx = 3 * c1x - 3 * x;
126 float ay = next.y - 3 * c2y + 3 * c1y - y;
127 float by = 3 * c2y - 6 * c1y + 3 * y;
128 float cy = 3 * c1y - 3 * y;
135 float steps = (float)Math.sqrt( (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0) );
137 //for(float t=0; t<1; t += 0.5) {
138 for(float t=0; t<1; t += 1/(steps/20)) {
139 float rx = ax * t * t * t + bx * t * t + cx * t + dx;
140 float ry = ay * t * t * t + by * t * t + cy * t + dy;
146 class QuadBezier extends Curve {
147 float cx, cy, cx2, cy2;
148 public void addTo(Mesh ret) {
149 throw new Error("doesn't work yet");
151 float bx = next.x - 2 * c1x + x;
152 float cx = 2 * c1x - 2 * x;
154 float by = next.y - 2 * c1y + y;
155 float cy = 2 * c1y - 2 * y;
158 float x0 = a.multiply_px(x, y);
159 float y0 = a.multiply_py(x, y);
160 float x1 = a.multiply_px(next.x, next.y);
161 float y1 = a.multiply_py(next.x, next.y);
162 float steps = (float)Math.sqrt( (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0) );
164 for(float t=0; t<1; t += 1 / (steps/20)) {
165 float rx = bx * t * t + cx * t + dx;
166 float ry = by * t * t + cy * t + dy;
167 ret.add(a.multiply_px(rx, ry), a.multiply_py(rx, ry));
173 public Path(String s) {
175 Tokenizer t = new Tokenizer(s);
176 char last_command = 'M';
177 boolean first = true;
178 while(t.hasMoreTokens()) {
179 char command = t.parseCommand();
180 if (first && command == 'm') command = 'M';
181 if (first && command != 'M') throw new RuntimeException("the first command of a path must be 'M'");
183 boolean relative = Character.toLowerCase(command) == command;
184 command = Character.toLowerCase(command);
185 parseSingleCommandAndArguments(t, command, relative);
186 last_command = command;
190 //#repeat width/height multiply_px/multiply_py horizontalBounds/verticalBounds
191 public long horizontalBounds(Affine a) {
193 float min = Float.MAX_VALUE;
194 float max = Float.MIN_VALUE;
195 for(Curve c = head; c != null; c = c.next) {
196 min = Math.min(min, a.multiply_px(c.x, c.y));
197 max = Math.max(max, a.multiply_px(c.x, c.y));
199 return Encode.twoFloatsToLong(max, min);
203 public long transform(Affine a, boolean forReal) { return transform(a, forReal, true); }
204 public long transform(Affine a, boolean forReal, boolean widthheight) {
205 float minx = Integer.MAX_VALUE; float miny = Integer.MAX_VALUE;
206 float maxx = Integer.MIN_VALUE; float maxy = Integer.MIN_VALUE;
207 for(Curve c = head; c != null; c = c.next) {
208 if (c instanceof Arc) { /* FIXME!!! WRONG!!!! */ continue; }
209 float x = a.multiply_px(c.x, c.y); if (x>maxx) maxx = x; if (x<minx) minx = x;
210 float y = a.multiply_py(c.x, c.y); if (y>maxy) maxy = y; if (y<miny) miny = y;
211 float c1x = a.multiply_px(c.c1x, c.c1y); if (c1x>maxx) maxx = c1x; if (c1x<minx) minx = c1x;
212 float c1y = a.multiply_py(c.c1x, c.c1y); if (c1y>maxy) maxy = c1y; if (c1y<miny) miny = c1y;
213 float c2x = a.multiply_px(c.c2x, c.c2y); if (c2x>maxx) maxx = c2x; if (c2x<minx) minx = c2x;
214 float c2y = a.multiply_py(c.c2x, c.c2y); if (c2y>maxy) maxy = c2y; if (c2y<miny) miny = c2y;
217 c.c1x = c1x; c.c1y = c1y;
218 c.c2x = c2x; c.c2y = c2y;
221 if (widthheight) return ((((long)Float.floatToIntBits(maxx - minx)) << 32) | ((long)Float.floatToIntBits(maxy - miny)));
222 else return ((((long)Float.floatToIntBits(minx)) << 32) | ((long)Float.floatToIntBits(miny)));
225 public void alignToOrigin() {
226 float minx = Integer.MAX_VALUE; float miny = Integer.MAX_VALUE;
227 for(Curve c = head; c != null; c = c.next) { if (c.x < minx) minx = c.x; if (c.y < miny) miny = c.y; }
228 for(Curve c = head; c != null; c = c.next) {
229 c.x -= minx; c.y -= miny;
230 if (c instanceof Arc) continue;
231 c.c1x -= minx; c.c2x -= minx; c.c1y -= miny; c.c2y -= miny;
235 public static class Tokenizer {
236 // FIXME: check array bounds exception for improperly terminated string
239 char lastCommand = 'M';
240 public Tokenizer(String s) { this.s = s; }
242 public static Path parse(String s) {
243 if (s == null) return null;
244 Tokenizer t = new Tokenizer(s);
245 Path ret = new Path(s);
247 char last_command = 'M';
248 boolean first = true;
249 while(t.hasMoreTokens()) {
250 char command = t.parseCommand();
251 if (first && command != 'M') throw new RuntimeException("the first command of a path must be 'M'");
253 boolean relative = Character.toLowerCase(command) == command;
254 command = Character.toLowerCase(command);
255 ret.parseSingleCommandAndArguments(t, command, relative);
256 last_command = command;
260 private void consumeWhitespace() {
261 while(i < s.length() && (Character.isWhitespace(s.charAt(i)))) i++;
262 if (i < s.length() && s.charAt(i) == ',') i++;
263 while(i < s.length() && (Character.isWhitespace(s.charAt(i)))) i++;
265 public boolean hasMoreTokens() { consumeWhitespace(); return i < s.length(); }
266 public char parseCommand() {
268 char c = s.charAt(i);
269 if (!Character.isLetter(c)) return lastCommand;
271 return lastCommand = c;
273 public float parseFloat() {
276 float multiplier = 1;
277 for(; i < s.length(); i++) {
278 char c = s.charAt(i);
279 if (Character.isWhitespace(c) || c == ',' || (c == '-' && i != start)) break;
280 if (!((c >= '0' && c <= '9') || c == '.' || c == 'e' || c == 'E' || c == '-')) {
281 if (c == '%') { // FIXME
282 } else if (s.regionMatches(i, "pt", 0, i+2)) { // FIXME
283 } else if (s.regionMatches(i, "em", 0, i+2)) { // FIXME
284 } else if (s.regionMatches(i, "pc", 0, i+2)) { // FIXME
285 } else if (s.regionMatches(i, "ex", 0, i+2)) { // FIXME
286 } else if (s.regionMatches(i, "mm", 0, i+2)) { i += 2; multiplier = INCHES_PER_MM * PX_PER_INCH; break;
287 } else if (s.regionMatches(i, "cm", 0, i+2)) { i += 2; multiplier = INCHES_PER_CM * PX_PER_INCH; break;
288 } else if (s.regionMatches(i, "in", 0, i+2)) { i += 2; multiplier = PX_PER_INCH; break;
289 } else if (s.regionMatches(i, "px", 0, i+2)) { i += 2; break;
290 } else if (Character.isLetter(c)) break;
291 throw new RuntimeException("didn't expect character \"" + c + "\" in a numeric constant");
294 //if (start == i) throw new RuntimeException("FIXME");
295 if (start == i) return (float)0.0;
297 return Float.parseFloat(s.substring(start, i)) * multiplier;
298 } catch (NumberFormatException nfe) {
299 Log.warn(Path.class, "offending string was \"" + s.substring(start, i) + "\"");
305 protected void parseSingleCommandAndArguments(Tokenizer t, char command, boolean relative) {
306 if (tail==null && command!='m') throw new RuntimeException("first command MUST be an 'm', not a " + command);
310 for(c = tail.prev; c != null && !(c instanceof Move); c = c.prev);
311 Line ret = new Line();
315 Move mov = new Move();
320 // FIXME: actually, we should search back to the last 'z' or 'm', not just 'm'
325 // feature: collapse consecutive movetos
326 Move ret = new Move();
327 ret.x = t.parseFloat() + (relative ? tail.y : 0);
328 ret.y = t.parseFloat() + (relative ? tail.y : 0);
333 case 'l': case 'h': case 'v': {
334 float first = t.parseFloat(), second;
335 if (command == 'h') second = relative ? 0 : tail.y;
336 else if (command == 'v') { second = first; first = relative ? 0 : tail.x; }
337 else second = t.parseFloat();
338 Line ret = new Line();
339 ret.x = first + (relative ? tail.x : 0);
340 ret.y = second + (relative ? tail.y : 0);
347 ret.c1x = t.parseFloat() + (relative ? tail.x : 0);
348 ret.c1y = t.parseFloat() + (relative ? tail.y : 0);
349 ret.c2x = (t.parseFloat() / 360) * 2 * PI;
350 ret.c2y = t.parseFloat();
351 ret.x = t.parseFloat() + (relative ? tail.x : 0);
352 ret.y = t.parseFloat() + (relative ? tail.y : 0);
357 case 's': case 'c': {
358 Bezier ret = new Bezier();
359 if (command == 'c') {
360 tail.c1x = t.parseFloat() + (relative ? tail.x : 0);
361 tail.c1y = t.parseFloat() + (relative ? tail.y : 0);
362 } else if (head != null && tail instanceof Bezier) {
363 tail.c1x = 2 * tail.x-((Bezier)tail).c2x;
364 tail.c1y = 2 * tail.y-((Bezier)tail).c2x;
369 tail.c2x = t.parseFloat() + (relative ? tail.x : 0);
370 tail.c2y = t.parseFloat() + (relative ? tail.y : 0);
371 ret.x = t.parseFloat() + (relative ? tail.x : 0);
372 ret.y = t.parseFloat() + (relative ? tail.y : 0);
377 case 't': case 'q': {
378 QuadBezier ret = new QuadBezier();
379 if (command == 'q') {
380 tail.c1x = t.parseFloat() + (relative ? tail.x : 0);
381 tail.c1y = t.parseFloat() + (relative ? tail.y : 0);
382 } else if (head != null && tail instanceof QuadBezier) {
383 tail.c1x = 2 * tail.x-((QuadBezier)tail).c1x;
384 tail.c1y = 2 * tail.y-((QuadBezier)tail).c1y;
389 ret.x = t.parseFloat() + (relative ? tail.x : 0);
390 ret.y = t.parseFloat() + (relative ? tail.y : 0);