001/** 002 * NurbsCurve -- Implementation and interface in common to all NURBS curves. 003 * 004 * Copyright (C) 2009-2025, by Joseph A. Huwaldt. All rights reserved. 005 * 006 * This library is free software; you can redistribute it and/or modify it under the terms 007 * of the GNU Lesser General Public License as published by the Free Software Foundation; 008 * either version 2.1 of the License, or (at your option) any later version. 009 * 010 * This library is distributed in the hope that it will be useful, but WITHOUT ANY 011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 012 * PARTICULAR PURPOSE. See the GNU Library General Public License for more details. 013 * 014 * You should have received a copy of the GNU Lesser General Public License along with 015 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - 016 * Suite 330, Boston, MA 02111-1307, USA. Or visit: http://www.gnu.org/licenses/lgpl.html 017 * 018 * This source file is based on, but heavily modified from, the LGPL jgeom (Geometry 019 * Library for Java) code by Samuel Gerber, Copyright (C) 2005. 020 */ 021package geomss.geom.nurbs; 022 023import geomss.geom.*; 024import jahuwaldt.js.param.Parameter; 025import jahuwaldt.tools.math.BracketRoot1D; 026import jahuwaldt.tools.math.Evaluatable1D; 027import jahuwaldt.tools.math.MathTools; 028import jahuwaldt.tools.math.RootException; 029import java.text.MessageFormat; 030import java.util.ArrayList; 031import java.util.Collections; 032import java.util.List; 033import static java.util.Objects.nonNull; 034import static java.util.Objects.requireNonNull; 035import java.util.logging.Level; 036import java.util.logging.Logger; 037import javax.measure.quantity.Area; 038import javax.measure.quantity.Dimensionless; 039import javax.measure.quantity.Length; 040import javax.measure.unit.Unit; 041import javolution.context.ObjectFactory; 042import javolution.context.StackContext; 043import javolution.lang.MathLib; 044import javolution.text.Text; 045import javolution.text.TextBuilder; 046import javolution.util.FastList; 047import javolution.util.FastMap; 048import javolution.util.FastTable; 049import javolution.xml.XMLFormat; 050import javolution.xml.stream.XMLStreamException; 051import org.jscience.mathematics.number.Float64; 052import org.jscience.mathematics.vector.Float64Vector; 053 054/** 055 * The interface and implementation in common to all NURBS curves. 056 * 057 * <p> Modified by: Joseph A. Huwaldt </p> 058 * 059 * @author Joseph A. Huwaldt, Date: May 28, 2009 060 * @version February 17, 2025 061 */ 062@SuppressWarnings({"serial", "CloneableImplementsClone"}) 063public abstract class NurbsCurve extends AbstractCurve<NurbsCurve> { 064 065 /** 066 * Return a list of control points for this curve. 067 * 068 * @return the ordered control points 069 */ 070 public abstract List<ControlPoint> getControlPoints(); 071 072 /** 073 * Return the knot vector of this curve. 074 * 075 * @return The knot vector. 076 */ 077 public abstract KnotVector getKnotVector(); 078 079 /** 080 * Return the degree of the NURBS curve. 081 * 082 * @return degree of curve 083 */ 084 public abstract int getDegree(); 085 086 /** 087 * Return an immutable version of this NURBS curve. 088 * 089 * @return an immutable version of this curve. 090 */ 091 public abstract BasicNurbsCurve immutable(); 092 093 /** 094 * Return a new curve that is identical to this one, but with the parameterization 095 * reversed. 096 * 097 * @return A new curve that is identical to this one but with the parameterization 098 * reversed. 099 */ 100 @Override 101 public NurbsCurve reverse() { 102 List<ControlPoint> oldCPs = getControlPoints(); 103 104 // Reverse the order of the control points. 105 FastTable<ControlPoint> cps = FastTable.newInstance(); 106 for (int i = oldCPs.size() - 1; i >= 0; --i) 107 cps.add(oldCPs.get(i)); 108 FastTable.recycle((FastTable)oldCPs); 109 110 // Reverse the order of the knot vector. 111 KnotVector kv = getKnotVector().reverse(); 112 113 // Return a new curve. 114 BasicNurbsCurve curve = BasicNurbsCurve.newInstance(cps, kv); 115 copyState(curve); // Copy over the super-class state for this curve to the new one. 116 117 FastTable.recycle(cps); 118 return curve; 119 } 120 121 /** 122 * Create and return a new {@link BasicNurbsCurve NURBS curve} that is geometrically 123 * identical to this one but with a new knot inserted at the specified parametric 124 * location. The parameterization of the new curve is identical to this curve, but the 125 * new curve will have a new knot at the specified location and a new set of control 126 * points. 127 * 128 * @param s The parametric location where the knot is to be inserted. 129 * @param numInsertions The number of times that the knot should be inserted at the 130 * specified position. 131 * @return A new NURBS curve that is identical to this one, but with a new knot 132 * inserted at the specified location. 133 */ 134 public BasicNurbsCurve insertKnot(double s, int numInsertions) { 135 // Algorithm A5.1, pg. 151, Piegl, L., Tiller, W., The Nurbs Book, 2nd Edition, Springer-Verlag, Berlin, 1997. 136 137 validateParameter(s); 138 if (numInsertions < 1) 139 throw new IllegalArgumentException(RESOURCES.getString("numInsertionsLE0Err")); 140 141 StackContext.enter(); 142 try { 143 // Construct the new knot vector by inserting the specified parametric position 144 // into the list of knots. 145 KnotVector SP = getKnotVector(); 146 int numKnots = SP.length(); 147 int p = SP.getDegree(); 148 int order = p + 1; 149 int r = numInsertions; 150 151 // Find where in the knot vector the new knot lies. 152 int k = 0; 153 for (int i = 0; i < numKnots; ++i) { 154 if (SP.getValue(i) > s) { 155 k = i - 1; 156 break; 157 } 158 } 159 160 // Determine the multiplicity of this knot. 161 int sm = 0; 162 if (s <= SP.getValue(k)) 163 sm = SP.findMultiplicity(k); 164 165 if ((r + sm) > order) 166 r = order - sm; 167 168 if (r <= 0) 169 return StackContext.outerCopy((BasicNurbsCurve)this.copyToReal()); 170 171 // Load the new knot vector. 172 FastTable<Float64> SQList = FastTable.newInstance(); 173 for (int i = 0; i <= k; ++i) { 174 SQList.add(SP.get(i)); 175 } 176 177 for (int i = 0; i < r; ++i) { 178 SQList.add(Float64.valueOf(s)); 179 } 180 181 for (int i = k + 1; i < numKnots; ++i) { 182 SQList.add(SP.get(i)); 183 } 184 185 KnotVector SQ = KnotVector.newInstance(p, Float64Vector.valueOf(SQList)); 186 187 // Save unaltered control points. 188 List<ControlPoint> Pw = getControlPoints(); 189 int np = Pw.size(); 190 FastTable<ControlPoint> Qw = FastTable.newInstance(); 191 Qw.setSize(np + r); 192 193 for (int i = 0; i <= k - p; ++i) { 194 Qw.set(i, Pw.get(i)); 195 } 196 197 for (int i = k - sm; i < np; ++i) { 198 Qw.set(i + r, Pw.get(i)); 199 } 200 201 // Temporary storage for altered control points. 202 FastTable<ControlPoint> Rw = FastTable.newInstance(); 203 Rw.setSize(order); 204 for (int i = 0; i <= p - sm; ++i) { 205 Rw.set(i, Pw.get(k - p + i).applyWeight()); // Project them from NURBS to B-Spline. 206 } 207 208 // Insert the knot r times. 209 int L = 0; 210 for (int j = 1; j <= r; ++j) { 211 L = k - p + j; 212 int pmjms = p - j - sm; 213 for (int i = 0; i <= pmjms; ++i) { 214 // Defining new control points in terms of old. 215 // alpha = (s - SP(L+i))/(SP(i+k+1) - SP(L+i)) 216 // Rw(i) = alpha*Rw[i+1] + (1.0 - alpha)*Rw[i] 217 double alpha = (s - SP.getValue(L + i)) / (SP.getValue(i + k + 1) - SP.getValue(L + i)); 218 219 ControlPoint Rwi = Rw.get(i); 220 Rwi = Rwi.times(1.0 - alpha); 221 ControlPoint Rwip1 = Rw.get(i + 1); 222 Rwip1 = Rwip1.times(alpha); 223 Rwip1 = Rwip1.plus(Rwi); 224 225 // Store the new control point. 226 Rw.set(i, Rwip1); 227 } 228 229 // Store the new control points (converting back from B-Spline to NURBS). 230 Qw.set(L, Rw.get(0).getHomogeneous()); 231 if (pmjms > 0) 232 Qw.set(k + r - j - sm, Rw.get(pmjms).getHomogeneous()); 233 234 } 235 236 // Load the remaining control points. 237 for (int i = L + 1; i < k - sm; ++i) { 238 Qw.set(i, Rw.get(i - L).getHomogeneous()); // Converting back from B-Spline to NURBS. 239 } 240 241 // Construct and return a new curve. 242 BasicNurbsCurve curve = BasicNurbsCurve.newInstance(Qw, SQ); 243 return StackContext.outerCopy(curve); 244 245 } finally { 246 StackContext.exit(); 247 } 248 249 } 250 251 /** 252 * Create and return a new {@link BasicNurbsCurve NURBS curve} that is geometrically 253 * identical to this one but with the specified list of knots inserted into it's knot 254 * vector. The parameterization of the new curve is identical to this curve, but the 255 * new curve will have the new knots at the specified locations and a new set of 256 * control points. This is more efficient than repeatedly calling insertKnot() for 257 * multiple knot insertions. 258 * 259 * @param newKnots The parametric locations where the knots are to be inserted. 260 * @return A new NURBS curve that is identical to this one, but with the new knots 261 * inserted at the specified locations. 262 */ 263 public BasicNurbsCurve refineKnotVector(double[] newKnots) { 264 return refineKnotVector(Float64Vector.valueOf(newKnots)); 265 } 266 267 /** 268 * Create and return a new {@link BasicNurbsCurve NURBS curve} that is geometrically 269 * identical to this one but with the specified list of knots inserted into it's knot 270 * vector. The parameterization of the new curve is identical to this curve, but the 271 * new curve will have the new knots at the specified locations and a new set of 272 * control points. This is more efficient than repeatedly calling insertKnot() for 273 * multiple knot insertions. 274 * 275 * @param newKnots The parametric locations where the knots are to be inserted. May 276 * not be null. 277 * @return A new NURBS curve that is identical to this one, but with the new knots 278 * inserted at the specified locations. 279 */ 280 public BasicNurbsCurve refineKnotVector(Float64Vector newKnots) { 281 // Algorithm A5.4, pg. 164, Piegl, L., Tiller, W., The Nurbs Book, 2nd Edition, Springer-Verlag, Berlin, 1997. 282 283 // Check on the validity of the new knots. 284 int r = newKnots.getDimension() - 1; 285 double sOld = 0; 286 for (int i = 0; i <= r; ++i) { 287 double s = newKnots.getValue(i); 288 validateParameter(s); 289 if (s < sOld) 290 throw new IllegalArgumentException(RESOURCES.getString("knotsNotIncreasingErr")); 291 sOld = s; 292 } 293 294 StackContext.enter(); 295 try { 296 // Get the existing knot vector. 297 KnotVector U = getKnotVector(); 298 int p = U.getDegree(); 299 300 // Get the existing control points. 301 List<ControlPoint> Pw = getControlPoints(); 302 int n = Pw.size() - 1; 303 304 int m = n + p + 1; 305 306 // Allocate storage for the new control points. 307 FastTable<ControlPoint> Qw = FastTable.newInstance(); 308 Qw.setSize(n + r + 2); 309 310 // Allocate storage for the new knot vector. 311 FastTable<Float64> UbarList = FastTable.newInstance(); 312 UbarList.setSize(m + r + 2); 313 314 int a = U.findSpan(newKnots.getValue(0)); 315 int b = U.findSpan(newKnots.getValue(r)); 316 ++b; 317 318 // Copy over the unaltered control points. 319 for (int j = 0; j <= a - p; ++j) { 320 Qw.set(j, Pw.get(j)); 321 } 322 for (int j = b - 1; j <= n; ++j) { 323 Qw.set(j + r + 1, Pw.get(j)); 324 } 325 326 // Copy over the unaltered knots. 327 for (int j = 0; j <= a; ++j) { 328 UbarList.set(j, U.get(j)); 329 } 330 for (int j = b + p; j <= m; ++j) { 331 UbarList.set(j + r + 1, U.get(j)); 332 } 333 334 int i = b + p - 1; 335 int k = b + p + r; 336 for (int j = r; j >= 0; --j) { 337 while (newKnots.getValue(j) <= U.getValue(i) && i > a) { 338 Qw.set(k - p - 1, Pw.get(i - p - 1)); 339 UbarList.set(k, U.get(i)); 340 --k; 341 --i; 342 } 343 344 Qw.set(k - p - 1, Qw.get(k - p)); 345 346 for (int l = 1; l <= p; ++l) { 347 int ind = k - p + l; 348 int indm1 = ind - 1; 349 int kpl = k + l; 350 double Ubarkpl = UbarList.get(kpl).doubleValue(); 351 double alpha = Ubarkpl - newKnots.getValue(j); 352 if (MathTools.isApproxZero(alpha)) 353 Qw.set(indm1, Qw.get(ind)); 354 else { 355 alpha /= Ubarkpl - U.getValue(i - p + l); 356 357 // Qw[ind-1] = alpha*Qw[ind-1] + (1.0-alpha)*Qw[ind]; 358 ControlPoint Qwind = Qw.get(ind).applyWeight(); 359 ControlPoint Qwindm1 = Qw.get(indm1).applyWeight(); 360 Qwindm1 = Qwindm1.times(alpha); 361 Qwind = Qwind.times(1.0 - alpha); 362 Qwind = Qwind.plus(Qwindm1); 363 Qw.set(indm1, Qwind.getHomogeneous()); 364 } 365 } 366 367 UbarList.set(k, newKnots.get(j)); 368 --k; 369 } 370 371 // Construct and return a new curve. 372 KnotVector Ubar = KnotVector.newInstance(p, Float64Vector.valueOf(UbarList)); 373 BasicNurbsCurve curve = BasicNurbsCurve.newInstance(Qw, Ubar); 374 return StackContext.outerCopy(curve); 375 376 } finally { 377 StackContext.exit(); 378 } 379 380 } 381 382 /** 383 * Return a {@link BasicNurbsCurve NURBS curve} that is geometrically identical to 384 * this one but with the specified list of knots merged into it's knot vector. The 385 * parameterization of the new curve is identical to this curve, but the new curve 386 * will have any new knots at the specified locations added and a new set of control 387 * points. If the input knot list matches the knot list of this curve, then this curve 388 * is returned. 389 * 390 * @param Um The knot vector to merge with this curve's knot vector. May not be null. 391 * @return A NURBS curve that is identical to this one, but with the merger of the 392 * input knot vector and this curve's knot vector. 393 */ 394 public NurbsCurve mergeKnotVector(Float64Vector Um) { 395 396 StackContext.enter(); 397 try { 398 // Get this curve's knot vector. 399 KnotVector U = getKnotVector(); 400 int Usize = U.length(); 401 int Umsize = Um.getDimension(); 402 403 // Find the knots that need inserting. 404 FastTable<Float64> Iknots = FastTable.newInstance(); 405 406 boolean done = false; 407 int ia = 0, ib = 0; 408 while (!done) { 409 double Umv = Um.getValue(ib); 410 double Uv = U.getValue(ia); 411 if (MathLib.abs(Umv - Uv) <= TOL_S) { // Umv == Uv 412 ++ib; 413 ++ia; 414 } else { 415 Iknots.add(Float64.valueOf(Umv)); 416 ++ib; 417 } 418 done = (ia >= Usize || ib >= Umsize); 419 } 420 421 // Check to see if any knots need inserting. 422 if (Iknots.isEmpty()) { 423 return this; 424 } 425 426 BasicNurbsCurve newCrv = this.refineKnotVector(Float64Vector.valueOf(Iknots)); 427 return StackContext.outerCopy(newCrv); 428 429 } finally { 430 StackContext.exit(); 431 } 432 433 } 434 435 /** 436 * Attempts to remove the knot with the specified index from this NURBS curve the 437 * specified number of times. If the knot can not be removed without changing the 438 * shape of the curve, then a reference to this curve is returned: 439 * <code>if (crv == crv.removeKnot(...)) { no knots removed }</code>. 440 * 441 * @param index The index of the knot to be removed (degree + 1 ≤ index < 442 * knot vector length - degree). 443 * @param numRemovals The number of times that the knot at "index" should be removed 444 * (1 ≤ num ≤ multiplicity of the knot). If numRemovals is > 445 * multiplicity of the knot, then numRemovals is set to the 446 * multiplicity of the knot. 447 * @param tolerance The knot(s) specified will be removed as long as the deviation 448 * of the curve is everywhere less than this tolerance. May not be 449 * null. 450 * @return A new NURBS curve that is identical to this one (to within the specified 451 * tolerance), but with the specified knot removed or a reference to this 452 * curve if the knot could not be removed. 453 */ 454 public NurbsCurve removeKnot(int index, int numRemovals, Parameter<Length> tolerance) { 455 // Algorithm: A5.8, pg. 185, Piegl, L., Tiller, W., The Nurbs Book, 2nd Edition, Springer-Verlag, Berlin, 1997. 456 requireNonNull(tolerance); 457 458 KnotVector Ukv = getKnotVector(); 459 int degree = Ukv.getDegree(); 460 int order = degree + 1; 461 int m = Ukv.length(); 462 463 // Check to make sure the knot to be removed is a legal one. 464 if (index < order || index >= m - degree) 465 throw new IllegalArgumentException( 466 MessageFormat.format(RESOURCES.getString("illegalKnotIndexForRemoval"), 467 index, order, m - degree)); 468 if (numRemovals < 0) 469 throw new IllegalArgumentException(RESOURCES.getString("negNumKnotsRemoveErr")); 470 if (numRemovals == 0) 471 return this; 472 473 StackContext.enter(); 474 try { 475 // Check if this is a multiple knot. 476 // If it is, reset "index" to be the largest index for the multiple knot. 477 // Want: Ukv(index) != Ukv(index+1) 478 while (index < m - degree - 1 && Ukv.getValue(index + 1) == Ukv.getValue(index)) { 479 ++index; 480 } 481 double u = Ukv.getValue(index); 482 483 // Determine multiplicity. 484 int s = Ukv.findMultiplicity(index); 485 if (numRemovals > s) 486 numRemovals = s; 487 488 // Get the original control points. 489 List<ControlPoint> Pw = getControlPoints(); 490 int n = Pw.size(); 491 492 // Determine an appropriate and meaningful tolerance for knot removal. 493 double tol = tolerance.getValue(getUnit()); 494 boolean isRational = false; 495 double wmin = Double.MAX_VALUE; 496 double Pmax = -Double.MAX_VALUE; 497 for (int i = 0; i < n; ++i) { 498 ControlPoint Pwi = Pw.get(i); 499 double w = Pwi.getWeight(); 500 wmin = MathLib.min(wmin, w); 501 if (MathTools.isApproxEqual(w, 1)) 502 isRational = true; 503 Pmax = MathLib.max(Pmax, Pwi.normValue()); 504 } 505 if (isRational) 506 tol = tol * (1 + Pmax) / wmin; 507 508 // Allocate storage for calculated control points. 509 FastTable<ControlPoint> temp = FastTable.newInstance(); 510 temp.setSize(2 * degree + 1); 511 512 // First control point out. 513 int fout = (2 * index - s - degree) / 2; 514 int last = index - s; 515 int first = index - degree; 516 int i, j, t; 517 518 // This loop is Eqn. 5.28 519 for (t = 0; t < numRemovals; ++t) { 520 // Diff. in index between temp and Pw. 521 int off = first - 1; 522 temp.set(0, Pw.get(off).applyWeight()); 523 ControlPoint cp = Pw.get(last + 1).applyWeight(); 524 temp.set(last + 1 - off, cp); 525 i = first; 526 j = last; 527 int ii = 1; 528 int jj = last - off; 529 boolean remflag = false; 530 531 while (j - i > t) { 532 // Compute new control points for one removal step. 533 double alfi = (u - Ukv.getValue(i)) / (Ukv.getValue(i + order + t) - Ukv.getValue(i)); 534 double alfj = (u - Ukv.getValue(j - t)) / (Ukv.getValue(j + order) - Ukv.getValue(j - t)); 535 ControlPoint Pwi = Pw.get(i).applyWeight(); 536 ControlPoint Pwj = Pw.get(j).applyWeight(); 537 538 // temp[ii] = (Pw[i] - (1.0-alfi)*temp[ii-1])/alfi; 539 ControlPoint tmpCP = temp.get(ii - 1); 540 tmpCP = tmpCP.times(1.0 - alfi); 541 Pwi = Pwi.minus(tmpCP); 542 Pwi = Pwi.divide(alfi); 543 temp.set(ii, Pwi); 544 545 // temp[jj] = (Pw[j] - alfj*temp[jj+1])/(1 - alfi); 546 tmpCP = temp.get(jj + 1); 547 tmpCP = tmpCP.times(alfj); 548 Pwj = Pwj.minus(tmpCP); 549 Pwj = Pwj.divide(1.0 - alfj); 550 temp.set(jj, Pwj); 551 552 ++i; 553 ++ii; 554 --j; 555 --jj; 556 } 557 558 // Check if knot removable. 559 if (j - i < t) { 560 if (temp.get(ii - 1).getHomogeneous().distance(temp.get(jj + 1).getHomogeneous()) <= tol) 561 remflag = true; 562 } else { 563 double alfi = (u - Ukv.getValue(i)) / (Ukv.getValue(i + order + t) - Ukv.getValue(i)); 564 // tmp = alfi*temp[ii+t+1] + (1.0-alfi)*temp[ii-1]; 565 ControlPoint tmp2 = temp.get(ii - 1); 566 tmp2 = tmp2.times(1.0 - alfi); 567 ControlPoint tmp = temp.get(ii + t + 1); 568 tmp = tmp.times(alfi); 569 tmp = tmp.plus(tmp2); 570 if (Pw.get(i).distance(tmp.getHomogeneous()) <= tol) 571 remflag = true; 572 } 573 if (!remflag) 574 // Can not remove any more knots. 575 break; // Get out of the for-loop. 576 else { 577 // Successfull removal. Save new control points. 578 i = first; 579 j = last; 580 581 while (j - i > t) { 582 Pw.set(i, temp.get(i - off).getHomogeneous()); 583 Pw.set(j, temp.get(j - off).getHomogeneous()); 584 ++i; 585 --j; 586 } 587 } 588 --first; 589 ++last; 590 } // end of for-loop. 591 592 if (t == 0) { 593 // No knots removed. 594 return this; 595 } 596 597 // Construct a new knot vector by shifting the old knots over. 598 FastTable<Float64> newKV = FastTable.newInstance(); 599 int end = index + 1 - t; 600 for (int k = 0; k < end; ++k) { 601 newKV.add(Ukv.get(k)); 602 } 603 for (int k = index + 1; k < m; ++k) { 604 newKV.add(Ukv.get(k)); 605 } 606 KnotVector kv = KnotVector.newInstance(degree, Float64Vector.valueOf(newKV)); 607 608 // Pj thru Pi will be overwritten 609 j = fout; 610 i = j; 611 for (int k = 1; k < t; ++k) { 612 if ((k % 2) == 1) // k modulo 2 613 ++i; 614 else 615 --j; 616 } 617 618 // Construct a new control point list. 619 FastTable<ControlPoint> newCPs = FastTable.newInstance(); 620 for (int k = 0; k < j; ++k) { 621 newCPs.add(Pw.get(k)); 622 } 623 for (int k = i + 1; k < n; ++k) { 624 newCPs.add(Pw.get(k)); 625 } 626 627 // Construct a new curve. 628 BasicNurbsCurve crv = BasicNurbsCurve.newInstance(newCPs, kv); 629 return StackContext.outerCopy(crv); 630 631 } finally { 632 StackContext.exit(); 633 } 634 635 } 636 637 /** 638 * Return the total arc length of this curve. 639 * 640 * @param eps The desired fractional accuracy on the arc-length. 641 * @return The total arc length of the curve. 642 */ 643 @Override 644 public Parameter<Length> getArcLength(double eps) { 645 646 if (getDegree() == 1) 647 // A polygon is a special case that can be dealt with easily. 648 return getCPPolyLength(); 649 650 // Otherwise, just use the standard approach. 651 return getArcLength(0, 1, eps); 652 } 653 654 /** 655 * Return the length of the control point polygon for this curve. 656 */ 657 private Parameter<Length> getCPPolyLength() { 658 659 StackContext.enter(); 660 try { 661 Parameter<Length> length = Parameter.ZERO_LENGTH.to(getUnit()); 662 List<ControlPoint> Pw = getControlPoints(); 663 664 int size = Pw.size(); 665 Point Pim1 = Pw.get(0).getPoint(); 666 for (int i = 1; i < size; ++i) { 667 Point Pi = Pw.get(i).getPoint(); 668 length = length.plus(Pim1.distance(Pi)); 669 Pim1 = Pi; 670 } 671 672 return StackContext.outerCopy(length); 673 674 } finally { 675 StackContext.exit(); 676 } 677 } 678 679 /** 680 * Return the arc length of a segment of this curve. 681 * 682 * @param s1 The starting parametric distance along the curve to begin the arc-length 683 * calculation from. 684 * @param s2 The ending parametric distance along the curve to end the arc-length 685 * calculation at. 686 * @param eps The desired fractional accuracy on the arc-length. 687 * @return The arc length of the specified segment of the curve. 688 */ 689 @Override 690 public Parameter<Length> getArcLength(double s1, double s2, double eps) { 691 validateParameter(s1); 692 validateParameter(s2); 693 694 // Method: Find arc-lengths of each Bezier segment and add them up. 695 // This avoids problems with discontinuities and improves accuracy. 696 // Adjusts eps value for each segment assuming "s" is proportional to arc-length 697 // which is not exactly true (also applied a factor of 2 conservatism). 698 699 // Create a list of Bezier segment starting parametric positions ( and the last end position, 1). 700 FastTable<Double> bParams = getSegmentParameters(); 701 702 // Find the arc-length of each segment but the last in turn and sum them up. 703 Parameter<Length> L = Parameter.ZERO_LENGTH.to(getUnit()); 704 int nSegs = bParams.size() - 1; 705 double epsf = 2 * (s2 - s1); 706 double sprev = s1; 707 for (int i = 1; i < nSegs; ++i) { 708 double si = bParams.get(i); 709 if (si > s1 && si < s2) { 710 L = L.plus(super.getArcLength(sprev, si, eps / (si - sprev) * 0.5)); 711 sprev = si; 712 epsf = 1; 713 } 714 } 715 716 // Add in the last segment. 717 L = L.plus(super.getArcLength(sprev, s2, eps * epsf / (s2 - sprev) * 0.5)); 718 719 return L; 720 } 721 722 /** 723 * Return a string of points that are gridded onto the curve with the number of points 724 * and spacing chosen to result in straight line segments between the points that have 725 * mid-points that are all within the specified tolerance of this curve. 726 * 727 * @param tol The maximum distance that a straight line between gridded points may 728 * deviate from this curve. May not be null. 729 * @return A string of subrange points gridded onto the curve. 730 */ 731 @Override 732 public PointString<SubrangePoint> gridToTolerance(Parameter<Length> tol) { 733 if (getDegree() > 1) 734 return super.gridToTolerance(tol); 735 736 // A more efficient algorithm is possible for degree=1 (linear) curves. 737 // Since a linear curve consists of straight line segments between knots, 738 // we can start with the knots and then "thin to tolerance" from there. 739 740 if (isDegenerate(requireNonNull(tol))) { 741 // If this curve is a single point, just return that point twice. 742 PointString<SubrangePoint> str = PointString.newInstance(); 743 str.add(getPoint(0)); 744 str.add(getPoint(1)); 745 return str; 746 } 747 748 // Create a list of Bezier segment starting parametric positions ( and the last end position, 1). 749 FastTable<Double> bParams = getSegmentParameters(); 750 751 // Convert the parametric positions into SubrangePoints. 752 PointString<SubrangePoint> pntList = PointString.newInstance(); 753 int numPnts = bParams.size(); 754 for (int i = 0; i < numPnts; ++i) { 755 Double s = bParams.get(i); 756 pntList.add(getPoint(s)); 757 } 758 759 // Loop over the points and remove any that are closer than tol to line segments 760 // between adjacent points. 761 Parameter<Area> tol2 = tol.times(tol); 762 for (int i = numPnts - 2; i >= 1; --i) { 763 SubrangePoint pi = pntList.get(i); 764 double s = pi.getParPosition().getValue(0); 765 766 // Calculate a point, pa, along the line between p(i+1) and p(i-1) 767 // at the position of "s" (or pi). 768 SubrangePoint pip1 = pntList.get(i + 1); 769 SubrangePoint pim1 = pntList.get(i - 1); 770 Point pa = interpSPoint(pim1, pip1, s); 771 772 // Compute the distance between the point at "s" and the line seg point 773 // and compare with the tolerance. 774 boolean distLTtol = pa.distanceSq(pi).isLessThan(tol2); 775 if (distLTtol) { 776 // Remove this point from the list. 777 SubrangePoint pnt = pntList.remove(i); 778 SubrangePoint.recycle(pnt); 779 } 780 } 781 782 return pntList; 783 } 784 785 /** 786 * Guess an initial set of points to use when gridding to tolerance. The initial 787 * points must include the start and end of the curve as well as any discontinuities 788 * in the curve. Other than that, they should be distributed as best as possible to 789 * indicate areas of higher curvature, but should be generated as efficiently as 790 * possible. The guessed points MUST be monotonically increasing from 0 to 1. 791 * 792 * @param tol2 The square of the maximum distance that a straight line 793 * between gridded points may deviate from this curve. 794 * @return A FastList of subrange points to use as a starting point for the grid to 795 * tolerance algorithms. 796 */ 797 @Override 798 protected FastList<SubrangePoint> guessGrid2TolPoints(Parameter<Area> tol2) { 799 if (getPhyDimension() < 2) 800 return super.guessGrid2TolPoints(tol2); 801 802 // Extract the boundaries of Bezier segments and place grid points at those. 803 // Find the control point that is furthest from a straight line from the start to 804 // the end of each Bezier segment and place a grid point that that approximate location. 805 FastTable<Double> parS = FastTable.newInstance(); 806 StackContext.enter(); 807 try { 808 // Create a list of Bezier segment starting parametric positions ( and the last end position, 1). 809 FastTable<Double> bParams = getSegmentParameters(); 810 811 // Create a list of Bezier segments for the curve. 812 GeomList<BasicNurbsCurve> segs = CurveUtils.decomposeToBezier(this); 813 814 // Loop over the segment starting positions. 815 int nSegs = segs.size(); 816 for (int i = 0; i < nSegs; ++i) { 817 // Always add the start point for each segment to the output points. 818 Double s0 = bParams.get(i); 819 parS.add(s0); 820 821 // Get this Bezier segment. 822 BasicNurbsCurve seg = segs.get(i); 823 824 // Get this segment's control points. 825 FastTable<ControlPoint> cps = (FastTable<ControlPoint>)seg.getControlPoints(); 826 827 // Find the control point that is furthest away from a line segment between the 1st and last CP. 828 Point p0 = cps.get(0).getPoint(); 829 Point p1 = cps.getLast().getPoint(); 830 Unit<Length> unit = getUnit(); 831 double Lv = p1.distance(p0).getValue(unit); 832 if (!MathTools.isApproxZero(Lv)) { 833 Vector<Dimensionless> u = p1.minus(p0).toGeomVector().toUnitVector(); 834 double dmax2 = 0; 835 double sdmax = 0.5; 836 int numCP = cps.size() - 1; 837 for (int cpIdx = 1; cpIdx < numCP; ++cpIdx) { 838 Point cpi = cps.get(cpIdx).getPoint(); 839 Point pi = GeomUtil.pointLineClosest(cpi, p0, u); 840 double dist2 = cpi.distanceSqValue(pi); 841 if (dist2 > dmax2) { 842 // Store maximum distance (squared). 843 dmax2 = dist2; 844 // Approx. parametric location of maximum point. 845 sdmax = pi.distance(p0).getValue(unit) / Lv; 846 } 847 } 848 849 // Add the location of the maximum lateral offset to the starting points. 850 if (dmax2 > tol2.getValue(unit.pow(2).asType(Area.class))) { 851 // Convert from segment to full curve parametric position. 852 Double s1 = bParams.get(i + 1); 853 sdmax = segmentPos2Parent(sdmax, s0, s1); 854 parS.add(sdmax); 855 } 856 } 857 } 858 859 // Add the last point to the list. 860 parS.add(bParams.getLast()); 861 862 } finally { 863 StackContext.exit(); 864 } 865 866 // Convert the parametric positions into SubrangePoints. 867 FastList<SubrangePoint> pntList = FastList.newInstance(); 868 int nSegs = parS.size(); 869 for (int i = 0; i < nSegs; ++i) { 870 Double s = parS.get(i); 871 pntList.add(getPoint(s)); 872 } 873 FastTable.recycle(parS); 874 875 return pntList; 876 } 877 878 /** 879 * Return a list of brackets that surround possible closest/farthest points. Each of 880 * the brackets will be refined using a root solver in order to find the actual 881 * closest or farthest points. 882 * 883 * @param point The point to find the closest/farthest point on this curve to/from. 884 * @param closest Set to <code>true</code> to return the closest point or 885 * <code>false</code> to return the farthest point. 886 * @param eval A function that calculates the dot product of (p(s)-q) and ps(s) on 887 * the curve. 888 * @return A list of brackets that each surround a potential closest/farthest point. 889 */ 890 @Override 891 protected List<BracketRoot1D> guessClosestFarthest(GeomPoint point, boolean closest, 892 Evaluatable1D eval) { 893 requireNonNull(point); 894 requireNonNull(eval); 895 896 // Inspired by: Ma, Hewitt, "Point inversion and projection for NURBS curve and surface", CAGD, 2003. 897 // But not quite as complex (or efficient or robust). 898 // Extract the curve degree. 899 int degree = getDegree(); 900 int order = degree + 1; 901 902 // Create a list of Bezier segment starting parametric positions ( and the last end position, 1). 903 FastTable<Double> bParams = getSegmentParameters(); 904 905 // Decompose the curve into Bezier segments. 906 GeomList<BasicNurbsCurve> segs = CurveUtils.decomposeToBezier(this); 907 908 // Create a list of Bezier end parameters that may be closest/farthest points. 909 FastTable<Double> closestEnds = FastTable.newInstance(); 910 911 // Create a list of segments to sub-divide into more segments. 912 FastTable<Integer> segIndexes = FastTable.newInstance(); 913 914 // Loop over all the Bezier segments. 915 int size = segs.size(); 916 for (int i = 0; i < size; ++i) { 917 BasicNurbsCurve seg = segs.get(i); 918 List<ControlPoint> segCP = seg.getControlPoints(); 919 920 // Determine if this segment is a "valid" segment (simple and convex). 921 if (CurveUtils.isSimpleConvexPolygon(segCP)) { 922 // Determine if the end-points are closest to the target point. 923 if (CurveUtils.isBezierEndPointNearest(point, segCP)) { 924 // One of the ends is closest. 925 double s = bParams.get(i); // Start of segment "i". 926 closestEnds.add(s); 927 s = bParams.get(i + 1); // End of segment "i". 928 closestEnds.add(s); 929 930 } else { 931 // An interior point is likely closest. 932 segIndexes.add(i); 933 } 934 935 } else { 936 // Handle invalid segments by just searching for interior brackets. 937 segIndexes.add(i); 938 } 939 940 FastTable.recycle((FastTable)segCP); 941 } 942 GeomList.recycle(segs); 943 944 // Look for brackets in each of the Bezier segments identified. 945 FastTable<BracketRoot1D> brackets = findInteriorBrackets(order + 1, segIndexes, bParams, eval); 946 947 // Finally, look for brackets near the end points of other segments. 948 brackets.addAll(findEndBrackets(closestEnds, point, closest, eval)); 949 950 // Look for and remove overlapping brackets that are bracketing the same root. 951 removeOverlappingBrackets(brackets, eval); 952 953 // Cleanup before leaving. 954 FastTable.recycle(closestEnds); 955 FastTable.recycle(segIndexes); 956 FastTable.recycle(bParams); 957 958 return brackets; 959 } 960 961 /** 962 * Return a list of brackets that surround possible plane intersection points. Each of 963 * the brackets will be refined using a root solver in order to find the actual 964 * intersection points. 965 * 966 * @param plane The plane to find the intersection points on this curve to/from. 967 * @param eval A function that calculates the <code>n dot (p(s) - pp)</code> on the 968 * curve. 969 * @return A list of brackets that each surround a potential intersection point. 970 * Returns an empty list if there are no intersections found. 971 */ 972 @Override 973 protected List<BracketRoot1D> guessIntersect(GeomPlane plane, Evaluatable1D eval) { 974 requireNonNull(plane); 975 requireNonNull(eval); 976 977 // Get the Bezier segment start parameters. 978 FastTable<Double> bParams = getSegmentParameters(); 979 980 // Extract the curve degree. 981 int degree = getDegree(); 982 int numPoints = degree + 3; 983 984 // Find multiple intersection points (roots) by sub-dividing each Bezier segment into pieces and 985 // bracketing any roots that occur in any of those segments. 986 FastTable<BracketRoot1D> outputs = FastTable.newInstance(); 987 try { 988 double s0 = bParams.get(0); 989 double fp = eval.function(s0); 990 int numSegs = bParams.size() - 1; 991 for (int j = 0; j < numSegs; ++j) { 992 // Get the next segment end. 993 double s1 = bParams.get(j + 1); 994 995 // Search for brackets between s0 and s1. 996 double dx = (s1 - s0) / numPoints; 997 if (dx != 0) { 998 double x = s0; 999 double xp = x; 1000 for (int i = 0; i < numPoints; ++i) { 1001 x += dx; 1002 if (x > s1) 1003 x = s1; 1004 double fc = eval.function(x); 1005 if (fc * fp <= 0.0) { 1006 // Create a bracket (while dealing with roundoff) 1007 double xpt = xp - MathTools.epsilon(xp); 1008 if (xpt < 0.0) 1009 xpt = 0.0; 1010 double xt = x + MathTools.epsilon(x); 1011 if (xt > 1.0) 1012 xt = 1.0; 1013 outputs.add(new BracketRoot1D(xpt, xt)); 1014 } 1015 xp = x; 1016 fp = fc; 1017 } 1018 } 1019 1020 // Set the next segment start to this segment end. 1021 s0 = s1; 1022 } 1023 } catch (RootException err) { 1024 Logger.getLogger(NurbsCurve.class.getName()).log(Level.WARNING, 1025 "Failed to bracket plane intersection points.", err); 1026 } 1027 1028 return outputs; 1029 } 1030 1031 /** 1032 * Return a list of brackets that surround possible surface intersection points. Each 1033 * of the brackets will be refined using a root solver in order to find the actual 1034 * intersection points. 1035 * 1036 * @param surface The surface to find the intersection points on this curve to/from. 1037 * @param eval A function that calculates the <code>n dot (p(s) - pp)</code> on the 1038 * curve. 1039 * @return A list of brackets that each surround a potential intersection point. 1040 * Returns an empty list if there are no intersections found. 1041 */ 1042 @Override 1043 protected List<BracketRoot1D> guessIntersect(Surface surface, Evaluatable1D eval) { 1044 1045 // Get the Bezier segment start parameters. 1046 FastTable<Double> bParams = getSegmentParameters(); 1047 int numSegs = bParams.size() - 1; 1048 1049 // Determine the number of points per segment. 1050 int numPoints = getNumPointsPerSegment(); 1051 1052 // Don't allow an excessive number of bracket tests. 1053 if (numPoints * numSegs > 50) { 1054 numPoints = 50 / numSegs; 1055 if (numPoints == 0) 1056 numPoints = 1; 1057 } 1058 1059 // Find multiple intersection points (roots) by sub-dividing curve into pieces and 1060 // bracketing any roots that occur in any of those segments. 1061 FastTable<BracketRoot1D> brackets = FastTable.newInstance(); 1062 try { 1063 double fp = eval.function(bParams.get(0)); 1064 for (int i = 0; i < numSegs; ++i) { 1065 double s0 = bParams.get(i); 1066 double s1 = bParams.get(i + 1); 1067 fp = findNextBrackets(eval, s0, s1, numPoints, fp, brackets); 1068 } 1069 1070 } catch (RootException err) { 1071 Logger.getLogger(NurbsCurve.class.getName()).log(Level.WARNING, 1072 "Failed to bracket surface intersection points.", err); 1073 } 1074 1075 return brackets; 1076 } 1077 1078 /** 1079 * Given a function, <code>func</code>, defined on the interval <code>x1</code> to 1080 * <code>x2</code>, this routine subdivides the interval into <code>n</code> equally 1081 * spaced segments, and searches for zero crossings of the function. Brackets around 1082 * any zero crossings found are placed in the input "brackets" list. The function 1083 * value at x1 is also input to eliminate unnecessary function evaluations when 1084 * calling this function in a loop. 1085 * 1086 * @param func The function that is being search for zero crossings. 1087 * @param x1 The start of the interval to be searched. 1088 * @param x2 The end of the interval to be searched. 1089 * @param n The number segments to divide the interval into. 1090 * @param fx1 The function value at x1. This is used to eliminate a redundant 1091 * function evaluation when this function is called repeatedly in a 1092 * loop. 1093 * @param brackets An existing list that has any new brackets added to it. 1094 * @return The value of the function evaluated at x2. 1095 * @throws RootException if the Evaluatable1D throws a exception. 1096 */ 1097 private static double findNextBrackets(Evaluatable1D func, double x1, double x2, int n, 1098 double fx1, List<BracketRoot1D> brackets) throws RootException { 1099 1100 double dx = (x2 - x1) / n; 1101 if (MathTools.isApproxZero(dx)) 1102 return fx1; 1103 1104 double x = x1; 1105 double fp = fx1; 1106 for (int i = 0; i < n; ++i) { 1107 x += dx; 1108 if (x > x2) 1109 x = x2; 1110 double fc = func.function(x); 1111 if (fc * fp <= 0.0) { 1112 brackets.add(new BracketRoot1D(x - dx, x)); 1113 } 1114 fp = fc; 1115 } 1116 1117 return fp; 1118 } 1119 1120 /** 1121 * Bracket any closest/farthest points interior to the listed segments by 1122 * sub-dividing. 1123 * 1124 * @param numSubSegs The number of segments to divide the Bezier segment into for root 1125 * bracketing. 1126 * @param segIndexes The indexes for the Bezier segments to be searched for 1127 * closest/farthest points. 1128 * @param knotList A list containing the parametric positions of the start of each 1129 * Bezier segment. 1130 * @param eval A function that calculates the dot product of (p(s)-q) and ps(s) 1131 * on the curve. 1132 * @return A list of bracketed closest/farthest points in the segments indicated by 1133 * segIndexes. 1134 */ 1135 private static FastTable<BracketRoot1D> findInteriorBrackets(int numSubSegs, List<Integer> segIndexes, List<Double> knotList, Evaluatable1D eval) { 1136 1137 FastTable<BracketRoot1D> brackets = FastTable.newInstance(); 1138 for (int i : segIndexes) { 1139 double s1 = knotList.get(i); // S for start of segment "i". 1140 double s2 = knotList.get(i + 1); // S for end of segment "i". 1141 try { 1142 brackets.addAll(BracketRoot1D.findBrackets(eval, s1, s2, numSubSegs)); 1143 } catch (RootException err) { 1144 Logger.getLogger(NurbsCurve.class.getName()).log(Level.WARNING, 1145 "Failed to find interior bracket points.", err); 1146 } 1147 } 1148 1149 return brackets; 1150 } 1151 1152 /** 1153 * Bracket any closest/farthest points at the end of the closest/farthest Bezier 1154 * segment end. 1155 * 1156 * @param segEnds A list of ends of Bezier segments to search for the closest/farthest 1157 * point. 1158 * @param point The point to find the closest/farthest point on this curve to/from. 1159 * @param closest Set to <code>true</code> to return the closest point or 1160 * <code>false</code> to return the farthest point. 1161 * @param eval A function that calculates the dot product of (p(s)-q) and ps(s) on 1162 * the curve. 1163 * @return A list of brackets surrounding the closest/farthest end point of the 1164 * supplied Bezier segment ends. 1165 */ 1166 private List<BracketRoot1D> findEndBrackets(List<Double> segEnds, GeomPoint point, boolean closest, 1167 Evaluatable1D eval) { 1168 1169 int size = segEnds.size(); 1170 if (size > 0) { 1171 // Find the closest/farthest segment end-point. The other's don't matter. 1172 double sOpt = segEnds.get(0); 1173 StackContext.enter(); 1174 try { 1175 double dOpt = getRealPoint(sOpt).distanceValue(point); 1176 double oldS = -1; 1177 for (int i = 1; i < size; ++i) { 1178 double s = segEnds.get(i); 1179 if (s != oldS) { 1180 double dist = getRealPoint(s).distanceValue(point); 1181 if (closest) { 1182 if (dist < dOpt) { 1183 dOpt = dist; 1184 sOpt = s; 1185 } 1186 } else { 1187 if (dist > dOpt) { 1188 dOpt = dist; 1189 sOpt = s; 1190 } 1191 } 1192 } 1193 oldS = s; 1194 } 1195 } finally { 1196 StackContext.exit(); 1197 } 1198 1199 // Bracket around the closest end-point. 1200 try { 1201 return bracketNear(eval, sOpt); 1202 } catch (RootException err) { 1203 Logger.getLogger(NurbsCurve.class.getName()).log(Level.WARNING, 1204 "Failed to find end bracket points.", err); 1205 } 1206 } 1207 1208 // Return an empty list (must be ArrayList as that is what "Roots.bracket" returns). 1209 return new ArrayList(); 1210 } 1211 1212 /** 1213 * Remove any overlapping brackets that are covering the same closest/farthest point. 1214 * 1215 * @param brackets A list of root brackets. The list will be modified to delete any 1216 * brackets that are both bracketing the same root. 1217 * @param eval A function that calculates the dot product of (p(s)-q) and ps(s) on 1218 * the curve. 1219 */ 1220 private void removeOverlappingBrackets(List<BracketRoot1D> brackets, Evaluatable1D eval) { 1221 1222 // Look for and remove overlapping brackets that are bracketing the same root. 1223 int size = brackets.size(); 1224 if (size > 1) { 1225 try { 1226 // First sort the brackets into ascending order of the start of each bracket. 1227 Collections.sort(brackets); 1228 1229 --size; 1230 for (int i = 0; i < size; ++i) { 1231 BracketRoot1D b = brackets.get(i); 1232 BracketRoot1D bp1 = brackets.get(i + 1); 1233 double x1 = b.x2; 1234 double x2 = bp1.x1; 1235 if (x1 > x2) { 1236 // We have an overlapping bracket pair. 1237 // Do they bound the same root? 1238 double f1 = eval.function(x1); 1239 double f2 = eval.function(x2); 1240 if (f1 * f2 <= 0) { 1241 // Both bracket the same root. 1242 1243 // Modify the 1st bracket to more closely cover the root. 1244 b.x1 = x2; 1245 b.x2 = x1; 1246 1247 // Delete the 2nd bracket. 1248 brackets.remove(i + 1); 1249 --size; 1250 } 1251 } 1252 } 1253 } catch (RootException err) { 1254 Logger.getLogger(NurbsCurve.class.getName()).log(Level.WARNING, 1255 "Evaluation function failed while removing overlapping brackets.", err); 1256 } 1257 } 1258 } 1259 1260 /** 1261 * Returns transformed version of this element. The returned object implements 1262 * {@link geomss.geom.GeomTransform} and contains this element as a child. 1263 * 1264 * @param transform The transformation to apply to this geometry. May not be null. 1265 * @return A new triangle that is identical to this one with the specified 1266 * transformation applied. 1267 * @throws DimensionException if this point is not 3D. 1268 */ 1269 @Override 1270 public NurbsCurveTrans getTransformed(GTransform transform) { 1271 return NurbsCurveTrans.newInstance(this, requireNonNull(transform)); 1272 } 1273 1274 /** 1275 * Split this {@link NurbsCurve} at the specified parametric position returning a list 1276 * containing two curves (a lower curve with smaller parametric positions than "s" and 1277 * an upper curve with larger parametric positions). 1278 * 1279 * @param s The parametric position where this curve should be split (must not be 0 or 1280 * 1!). 1281 * @return A list containing two curves: 0 == the lower curve, 1 == the upper curve. 1282 */ 1283 @Override 1284 public GeomList<NurbsCurve> splitAt(double s) { 1285 validateParameter(s); 1286 if (parNearEnds(s, TOL_S)) 1287 throw new IllegalArgumentException( 1288 MessageFormat.format(RESOURCES.getString("canNotSplitAtEnds"), "curve")); 1289 1290 GeomList<NurbsCurve> output = GeomList.newInstance(); // Outside of StackContext. 1291 StackContext.enter(); 1292 try { 1293 KnotVector U = getKnotVector(); 1294 int degree = U.getDegree(); 1295 int span = U.findSpan(s); 1296 1297 // Find the multiplicity of the split parameter. 1298 int sIndex; 1299 if (MathLib.abs(s - U.getValue(span)) <= TOL_S) 1300 sIndex = U.findMultiplicity(span); 1301 else 1302 sIndex = 0; 1303 1304 // Number of times to insert the knot required to split the curve. 1305 int t = degree + 1 - sIndex; 1306 1307 // Insert the knot where we are splitting the curve "t" times. 1308 BasicNurbsCurve cTmp = insertKnot(s, t); 1309 KnotVector kvTmp = cTmp.getKnotVector(); 1310 List<ControlPoint> cpTmp = cTmp.getControlPoints(); 1311 1312 span = kvTmp.findSpan(s) - degree; // span is now the beginning of the upper curve. 1313 1314 // Create a knot vector for the lower curve. 1315 FastTable<Float64> kvList = FastTable.newInstance(); 1316 int size = span + degree + 1; 1317 for (int i = 0; i < size; ++i) { 1318 double snew = kvTmp.getValue(i) / s; // Scale to range 0-1. 1319 // Watch for roundoff 1320 if (snew <= TOL_S) 1321 snew = 0; 1322 else if (snew >= 1 - TOL_S) 1323 snew = 1; 1324 kvList.add(Float64.valueOf(snew)); 1325 } 1326 KnotVector kv = KnotVector.newInstance(degree, Float64Vector.valueOf(kvList)); 1327 1328 // Create a ControlPoint list for the lower curve. 1329 FastTable<ControlPoint> cpList = FastTable.newInstance(); 1330 size = span; 1331 for (int i = 0; i < size; ++i) { 1332 cpList.add(cpTmp.get(i)); 1333 } 1334 1335 // Create the lower curve. 1336 BasicNurbsCurve cl = BasicNurbsCurve.newInstance(cpList, kv); 1337 1338 // Create a knot vector for the upper curve. 1339 double oms = 1 - s; 1340 kvList.clear(); 1341 size = kvTmp.length(); 1342 for (int i = span; i < size; ++i) { 1343 double snew = (kvTmp.getValue(i) - s) / oms; // Scale to range 0-1. 1344 // Watch for roundoff. 1345 if (snew <= TOL_S) 1346 snew = 0; 1347 else if (snew >= 1 - TOL_S) 1348 snew = 1; 1349 kvList.add(Float64.valueOf(snew)); 1350 } 1351 kv = KnotVector.newInstance(degree, Float64Vector.valueOf(kvList)); 1352 1353 // Create a ControlPoint list for the upper curve. 1354 cpList.clear(); 1355 size = cpTmp.size(); 1356 for (int i = span; i < size; ++i) { 1357 cpList.add(cpTmp.get(i)); 1358 } 1359 1360 // Create the upper curve. 1361 BasicNurbsCurve cu = BasicNurbsCurve.newInstance(cpList, kv); 1362 1363 // Put in the output list (which is outside the StackContext). 1364 output.add(StackContext.outerCopy(cl)); 1365 output.add(StackContext.outerCopy(cu)); 1366 1367 } finally { 1368 StackContext.exit(); 1369 } 1370 1371 return output; 1372 } 1373 1374 /** 1375 * Return the intersection between a line segment and this curve. 1376 * 1377 * @param line The line segment to intersect. May not be null. 1378 * @param tol Tolerance (in physical space) to refine the point positions to. May not be null. 1379 * @return A list containing two PointString instances each containing zero or more 1380 * subrange points, on this curve and the input line segment respectively, 1381 * made by the intersection of this curve with the specified line segment. If 1382 * no intersections are found a list of two empty PointStrings are returned. 1383 */ 1384 @Override 1385 public GeomList<PointString<SubrangePoint>> intersect(LineSegment line, Parameter<Length> tol) { 1386 requireNonNull(line); 1387 requireNonNull(tol); 1388 1389 // First check to see if the line segment even comes near the curve. 1390 GeomList<PointString<SubrangePoint>> output = GeomList.newInstance(); 1391 output.add(PointString.newInstance()); 1392 output.add(PointString.newInstance()); 1393 if (!GeomUtil.lineSegBoundsIntersect(line.getStart(), line.getEnd(), this)) 1394 return output; 1395 1396 // Check for a degenerate curve case. 1397 if (isDegenerate(tol)) { 1398 // Just see if the start point on this curve intersects that curve (is on that curve). 1399 SubrangePoint p1 = getPoint(0); 1400 Point p1r = p1.copyToReal(); 1401 1402 // Find the closest point on the input curve. 1403 SubrangePoint p2 = line.getClosest(p1r, GTOL); 1404 1405 if (p1r.distance(p2).isLessThan(tol)) { 1406 // Add the points to the lists of intersections being collected. 1407 output.get(0).add(p1); 1408 output.get(1).add(p2); 1409 } 1410 1411 return output; 1412 } 1413 1414 // Create a line segment-curve intersector object. 1415 LineSegNurbsCurveIntersect intersector = LineSegNurbsCurveIntersect.newInstance(this, tol, line, output); 1416 1417 // Intersect the line segment and curve (intersections are added to "output"). 1418 output = intersector.intersect(); 1419 1420 // Clean up. 1421 LineSegNurbsCurveIntersect.recycle(intersector); 1422 1423 return output; 1424 } 1425 1426 /** 1427 * Return a NURBS curve representation of this curve to within the specified 1428 * tolerance. This implementation returns a reference to this curve itself since it is 1429 * already a NURBS curve and the tolerance parameter is ignored. 1430 * 1431 * @param tol Ignored. May pass <code>null</code>. 1432 * @return A reference to this NURBS curve is returned. 1433 */ 1434 @Override 1435 public NurbsCurve toNurbs(Parameter<Length> tol) { 1436 return this; 1437 } 1438 1439 /** 1440 * Return a NURBS curve representation of this curve. This implementation returns a 1441 * reference to this curve itself since it is already a NURBS curve. 1442 * 1443 * @return A reference to this NURBS curve is returned. 1444 */ 1445 public NurbsCurve toNurbs() { 1446 return this; 1447 } 1448 1449 /** 1450 * Return <code>true</code> if this NurbsCurve contains valid and finite numerical 1451 * components. A value of <code>false</code> will be returned if any of the control 1452 * point values or knots are NaN or Inf. 1453 * 1454 * @return true if this curve contains valid and finite values. 1455 */ 1456 @Override 1457 public boolean isValid() { 1458 1459 // Check the knot vector. 1460 if (!getKnotVector().isValid()) 1461 return false; 1462 1463 // Check the control points. 1464 List<ControlPoint> controlPoints = getControlPoints(); 1465 int size = controlPoints.size(); 1466 for (int i = 0; i < size; ++i) { 1467 ControlPoint cp = controlPoints.get(i); 1468 if (!cp.isValid()) 1469 return false; 1470 } 1471 1472 return true; 1473 } 1474 1475 /** 1476 * Return <code>true</code> if this curve is degenerate (i.e.: has length less than 1477 * the specified tolerance). 1478 * 1479 * @param tol The tolerance for determining if this curve is degenerate. May not be null. 1480 * @return true if this curve is degenerate. 1481 * @see #getArcLength 1482 */ 1483 @Override 1484 public boolean isDegenerate(Parameter<Length> tol) { 1485 requireNonNull(tol); 1486 1487 // For a NURBS curve this is much faster than computing 1488 // the length of the curve using getArcLength() and then comparing 1489 // with tol. 1490 Parameter<Length> cLength = getCPPolyLength(); 1491 return cLength.compareTo(tol) <= 0; 1492 } 1493 1494 /** 1495 * Returns <code>true</code> if this curve is a line to within the specified 1496 * tolerance. 1497 * 1498 * @param tol The tolerance for determining if this curve is a line. If null is 1499 * passed, then exact linearity is required. 1500 * @return true if this curve is a line. 1501 */ 1502 @Override 1503 public boolean isLine(Parameter<Length> tol) { 1504 1505 // Make some simple checks on linearity first. 1506 if (getDegree() == 1) { 1507 StackContext.enter(); 1508 try { 1509 1510 // Get the control points. 1511 List<ControlPoint> controlPoints = getControlPoints(); 1512 1513 // First check for an obvious case. 1514 int numCp = controlPoints.size(); 1515 if (numCp <= 2) { 1516 return true; 1517 } 1518 1519 // Are all the weights equal to 1? 1520 boolean isLinear = true; 1521 int size = controlPoints.size(); 1522 for (int i = 0; i < size; ++i) { 1523 ControlPoint cp = controlPoints.get(i); 1524 if (MathTools.isApproxEqual(cp.getWeight(), 1)) { 1525 isLinear = false; 1526 break; 1527 } 1528 } 1529 1530 // If degree == 1, weights == 1, and all the control points are co-linear, 1531 // then the curve is a line. 1532 if (isLinear) { 1533 Point cp0 = controlPoints.get(0).getPoint(); 1534 Point cp1 = controlPoints.get(1).getPoint(); 1535 Vector<Dimensionless> u = cp1.minus(cp0).toGeomVector().toUnitVector(); 1536 for (int i = 2; i < numCp; ++i) { 1537 Point cp = controlPoints.get(i).getPoint(); 1538 Parameter<Length> d = GeomUtil.pointLineDistance(cp, cp0, u); 1539 if (d.isGreaterThan(tol)) { 1540 isLinear = false; 1541 break; 1542 } 1543 } 1544 if (isLinear) 1545 return true; 1546 } 1547 1548 } finally { 1549 StackContext.exit(); 1550 } 1551 } 1552 1553 // If simple checks fail, try more complex tests for linearity. 1554 return super.isLine(tol); 1555 } 1556 1557 /** 1558 * Return <code>true</code> if this curve is planar or <code>false</code> if it is 1559 * not. 1560 * 1561 * @param tol The geometric tolerance to use in determining if the curve is planar. 1562 * May not be null. 1563 * @return true if this curve is planar. 1564 */ 1565 @Override 1566 public boolean isPlanar(Parameter<Length> tol) { 1567 // If the curve is less than 3D, then it must be planar. 1568 int numDims = getPhyDimension(); 1569 if (numDims <= 2) 1570 return true; 1571 1572 // Is this curve degenerate? 1573 if (isDegenerate(requireNonNull(tol))) 1574 return true; 1575 1576 // Is the curve linear? 1577 if (isLine(tol)) 1578 return true; 1579 1580 // The curve is planar if the control points are co-planar. 1581 StackContext.enter(); 1582 try { 1583 1584 // Get the control points. 1585 List<ControlPoint> cps = getControlPoints(); 1586 1587 // If there are only two or three control points, then the curve is planar by definition 1588 // (it is a line or triangle). 1589 int numCP = cps.size(); 1590 if (numCP <= 3) 1591 return true; 1592 1593 // Form a vector with the 1st two control points. 1594 Point p0 = cps.get(0).getPoint(); 1595 Point p1 = cps.get(1).getPoint(); 1596 Vector<Length> v1 = p1.toGeomVector().minus(p0.toGeomVector()); 1597 1598 // Form a vector with the 2nd and 3rd control points. 1599 Point p2 = cps.get(2).getPoint(); 1600 Vector<Length> v2 = p2.toGeomVector().minus(p1.toGeomVector()); 1601 1602 // Cross product of these two vectors defines the plane. 1603 Point ZERO_POINT = Point.newInstance(numDims); 1604 Vector<Dimensionless> nhat = v1.cross(v2).toUnitVector(); 1605 nhat.setOrigin(ZERO_POINT); 1606 1607 // Find deviation from a plane for each point by projecting a vector from p1 to pi 1608 // onto the normal formed by the cross product of the 1st two vectors. 1609 // If the projection is anything other than zero, the points and tangent 1610 // vectors are not in the same plane. 1611 for (int i = 3; i < numCP; ++i) { 1612 Point pi = cps.get(i).getPoint(); 1613 Vector<Length> p1pi = pi.minus(p1).toGeomVector(); 1614 if (!p1pi.mag().isApproxZero()) { 1615 Parameter proj = p1pi.dot(nhat); 1616 if (proj.isLargerThan(tol)) 1617 return false; 1618 } 1619 } 1620 1621 } finally { 1622 StackContext.exit(); 1623 } 1624 1625 return true; 1626 } 1627 1628 /** 1629 * Return a list containing parameters at the start of logical segments of this curve. 1630 * The first element in this list must always be 0.0 and the last element 1.0. These 1631 * should be good places to break the curve up into pieces for analysis, but otherwise 1632 * are arbitrary. 1633 * <p> 1634 * This implementation returns the parameters at the start of each Bezier segment of 1635 * this NURBS curve. 1636 * </p> 1637 * 1638 * @return A list of parameters at the start and end of each Bezier segment. 1639 */ 1640 @Override 1641 protected FastTable<Double> getSegmentParameters() { 1642 return CurveUtils.getBezierStartParameters(this); 1643 } 1644 1645 /** 1646 * Return the number of points per segment that should be used when analyzing curve 1647 * segments by certain methods. 1648 * <p> 1649 * This implementation always returns 2*(p + 1) where p is the degree of the curve. 1650 * </p> 1651 * 1652 * @return The number of points per segment that should be used when analyzing this 1653 * curve. 1654 */ 1655 @Override 1656 protected int getNumPointsPerSegment() { 1657 return 2 * (getDegree() + 1); 1658 } 1659 1660 /** 1661 * Returns the text representation of this geometry element that consists of the name 1662 * followed by the control point values, followed by the knot vector. For example: 1663 * <pre> 1664 * {aCurve = {{{1 ft, 0 ft}, 1.0}, {{0 ft, 1 ft}, 0.25}, {{-1 ft, 0 ft}, 1.0}},{degree=2,{0.0, 0.0, 0.0, 1.0, 1.0, 1.0}}} 1665 * </pre> 1666 * If there is no name, then the output looks like this: 1667 * <pre> 1668 * {{{{1 ft, 0 ft}, 1.0}, {{0 ft, 1 ft}, 0.25}, {{-1 ft, 0 ft}, 1.0}},{degree=2,{0.0, 0.0, 0.0, 1.0, 1.0, 1.0}}} 1669 * </pre> 1670 * 1671 * @return the text representation of this geometry element. 1672 */ 1673 @Override 1674 public Text toText() { 1675 TextBuilder tmp = TextBuilder.newInstance(); 1676 tmp.append('{'); 1677 String nameStr = getName(); 1678 boolean hasName = nonNull(nameStr); 1679 if (hasName) { 1680 tmp.append(nameStr); 1681 tmp.append(" = {"); 1682 } 1683 List<ControlPoint> cpList = getControlPoints(); 1684 int size = cpList.size(); 1685 for (int i = 0; i < size; i++) { 1686 tmp.append(cpList.get(i).toText()); 1687 if (i != size - 1) { 1688 tmp.append(", "); 1689 } 1690 } 1691 tmp.append("},"); 1692 1693 tmp.append(getKnotVector().toText()); 1694 1695 if (hasName) 1696 tmp.append('}'); 1697 tmp.append('}'); 1698 Text txt = tmp.toText(); 1699 TextBuilder.recycle(tmp); 1700 return txt; 1701 } 1702 1703 /** 1704 * A class used to intersect an line segment and this NURBS curve. 1705 */ 1706 private static class LineSegNurbsCurveIntersect { 1707 1708 private NurbsCurve _thisCurve; 1709 private Parameter<Length> _tol; 1710 private LineSegment _line; 1711 private GeomPoint _p0; 1712 private GeomPoint _p1; 1713 private Vector<Length> _Lu; 1714 private GeomList<PointString<SubrangePoint>> _output; 1715 1716 public static LineSegNurbsCurveIntersect newInstance(NurbsCurve thisCurve, Parameter<Length> tol, 1717 LineSegment line, GeomList<PointString<SubrangePoint>> output) { 1718 1719 // Make sure the line and curve have the same dimensions. 1720 int numDims = thisCurve.getPhyDimension(); 1721 if (line.getPhyDimension() > numDims) 1722 throw new IllegalArgumentException(MessageFormat.format( 1723 RESOURCES.getString("sameOrFewerDimErr"), "line", "curve")); 1724 else if (line.getPhyDimension() < numDims) 1725 line = line.toDimension(numDims); 1726 1727 if (thisCurve instanceof GeomTransform) 1728 thisCurve = thisCurve.copyToReal(); 1729 if (line instanceof GeomTransform) 1730 line = line.copyToReal(); 1731 1732 // Create an instance of this class and fill in the class variables. 1733 LineSegNurbsCurveIntersect o = FACTORY.object(); 1734 o._thisCurve = thisCurve; 1735 o._tol = tol; 1736 o._line = line; 1737 o._p0 = line.getStart(); 1738 o._p1 = line.getEnd(); 1739 o._Lu = line.getDerivativeVector().immutable(); 1740 o._output = output; 1741 1742 return o; 1743 } 1744 1745 /** 1746 * Method that actually carries out the intersection adding the intersection 1747 * points to the list of lists provided in the constructor. 1748 * 1749 * @return A list containing two PointString instances each containing zero or 1750 * more subrange points, on this curve and the input line segment 1751 * respectively, made by the intersection of this curve with the specified 1752 * line segment. If no intersections are found a list of two empty 1753 * PointStrings are returned. 1754 */ 1755 public GeomList<PointString<SubrangePoint>> intersect() { 1756 1757 // Decompose the NurbsCurve into a set of Bezier curve segments. 1758 GeomList<BasicNurbsCurve> segs = CurveUtils.decomposeToBezier(_thisCurve); 1759 1760 // Create a list of segment starting par. positions (and the last end position, 1). 1761 FastTable<Double> bParams = _thisCurve.getSegmentParameters(); 1762 1763 // Work with each segment separately. 1764 int numSegs = segs.size(); 1765 for (int i = 0; i < numSegs; ++i) { 1766 NurbsCurve curveSeg = segs.get(i); 1767 if (GeomUtil.lineSegBoundsIntersect(_p0, _p1, curveSeg)) { 1768 double s0 = bParams.get(i); 1769 double s1 = bParams.get(i + 1); 1770 divideAndConquerLineSeg(curveSeg, s0, s1); 1771 } 1772 } 1773 1774 // Clean up. 1775 FastTable.recycle(bParams); 1776 GeomList.recycle(segs); 1777 1778 return _output; 1779 } 1780 1781 /** 1782 * Uses a recursive "Divide and Conquer" approach to intersecting a line segment 1783 * with a curve. On each call, the following happens: 1784 * <pre> 1785 * 1) The curve is checked to see if it is approx. a straight line. 1786 * 1b) If it is, then a line-line intersection is performed, the 1787 * intersection point added to the output list and the method exited. 1788 * 2) The curve is divided into two halves. 1789 * 2b) Each half is tested to see if it's bounding box is intersected by the line. 1790 * If it is, then this method is called with that segment recursively. 1791 * Otherwise, the method is exited. 1792 * </pre> 1793 * A number of class variables are used to pass information to this 1794 * recursion: 1795 * <pre> 1796 * _tol The tolerance to use when determining if the curve is locally linear. 1797 * _line The line segment instance we are intersecting the curve with. 1798 * _p0 A point at the start of the line being intersected with this curve. 1799 * _p1 A point at the end of the line being intersected with this curve. 1800 * _Lu A vector indicating the direction of the line. 1801 * _output A list used to collect the output subrange intersection points. 1802 * </pre> 1803 * 1804 * @param crv The curve segment being checked for intersection with the line. 1805 * @param s0 The parametric position of the start of this curve segment on the 1806 * master curve. 1807 * @param s1 The parametric position of the end of this curve segment on the 1808 * master curve. 1809 */ 1810 private void divideAndConquerLineSeg(NurbsCurve crv, double s0, double s1) { 1811 1812 // Check to see if the input curve is within tolerance of a straight line. 1813 Point p0 = crv.getRealPoint(0); 1814 Point p1 = crv.getRealPoint(1); 1815 Point pAvg = p0.plus(p1).divide(2); 1816 Point pm = crv.getRealPoint(0.5); 1817 if (pAvg.distance(pm).isLessThan(_tol)) { 1818 // The input curve segment is nearly a straight line. 1819 1820 // Intersect the input line and the line approximating the curve. 1821 Vector<Length> crvDir = p1.minus(p0).toGeomVector(); 1822 MutablePoint sLn = MutablePoint.newInstance(2); 1823 int numDims = p0.getPhyDimension(); 1824 Unit<Length> unit = p0.getUnit(); 1825 MutablePoint pi1 = MutablePoint.newInstance(numDims, unit); 1826 MutablePoint pi2 = MutablePoint.newInstance(numDims, unit); 1827 IntersectType type = GeomUtil.lineLineIntersect(_p0, _Lu, p0, crvDir, _tol, sLn, pi1, pi2); 1828 Vector.recycle(crvDir); 1829 1830 // Make sure the intersection points fall on the line segments. 1831 double sl_1 = sLn.getValue(0); 1832 if (sl_1 > 1) { 1833 pi1.set(_line.getEnd()); 1834 sl_1 = 1; 1835 } else if (sl_1 < 0) { 1836 pi1.set(_line.getStart()); 1837 sl_1 = 0; 1838 } 1839 double sl_2 = sLn.getValue(1); 1840 if (sl_2 > 1) { 1841 pi2.set(crv.getRealPoint(1)); 1842 sl_2 = 1; 1843 } else if (sl_2 < 0) { 1844 pi2.set(crv.getRealPoint(0)); 1845 sl_2 = 0; 1846 } 1847 if (pi1.distance(pi2).isLessThan(_tol)) { 1848 // Add an intersection point on each curve to the output lists. 1849 _output.get(1).add(_line.getPoint(sl_1)); 1850 double s = s0 + (s1 - s0) * sl_2; // Convert from segment par. pos. to full curve pos. 1851 s = roundParNearEnds(s); // Deal with roundoff issues. 1852 _output.get(0).add(_thisCurve.getPoint(s)); 1853 } 1854 1855 // Clean up. 1856 MutablePoint.recycle(sLn); 1857 MutablePoint.recycle(pi1); 1858 MutablePoint.recycle(pi2); 1859 1860 } else { 1861 // Sub-divide the curve into two segments. 1862 GeomList<NurbsCurve> segments = crv.splitAt(0.5); 1863 1864 // Check for possible intersections on the left segment. 1865 NurbsCurve seg = segments.get(0); 1866 if (GeomUtil.lineSegBoundsIntersect(_p0, _p1, seg)) { 1867 // May be an intersection. 1868 double sl = s0; 1869 double sh = (s0 + s1) / 2; 1870 1871 // Recurse down to a finer level of detail. 1872 divideAndConquerLineSeg(seg, sl, sh); 1873 } 1874 BasicNurbsCurve.recycle((BasicNurbsCurve)seg); 1875 1876 // Check for possible intersections on the right segment. 1877 seg = segments.get(1); 1878 if (GeomUtil.lineSegBoundsIntersect(_p0, _p1, seg)) { 1879 // May be an intersection. 1880 double sl = (s0 + s1) / 2; 1881 double sh = s1; 1882 1883 // Recurse down to a finer level of detail. 1884 divideAndConquerLineSeg(seg, sl, sh); 1885 } 1886 BasicNurbsCurve.recycle((BasicNurbsCurve)seg); 1887 1888 // Clean up. 1889 GeomList.recycle(segments); 1890 } 1891 1892 } 1893 1894 public static void recycle(LineSegNurbsCurveIntersect instance) { 1895 FACTORY.recycle(instance); 1896 } 1897 1898 private LineSegNurbsCurveIntersect() { } 1899 1900 private static final ObjectFactory<LineSegNurbsCurveIntersect> FACTORY = new ObjectFactory<LineSegNurbsCurveIntersect>() { 1901 @Override 1902 protected LineSegNurbsCurveIntersect create() { 1903 return new LineSegNurbsCurveIntersect(); 1904 } 1905 1906 @Override 1907 protected void cleanup(LineSegNurbsCurveIntersect obj) { 1908 obj._thisCurve = null; 1909 obj._tol = null; 1910 obj._line = null; 1911 obj._p0 = null; 1912 obj._p1 = null; 1913 obj._Lu = null; 1914 obj._output = null; 1915 } 1916 }; 1917 } 1918 1919 /** 1920 * Holds the default XML representation for this object. 1921 */ 1922 @SuppressWarnings("FieldNameHidesFieldInSuperclass") 1923 protected static final XMLFormat<NurbsCurve> XML = new XMLFormat<NurbsCurve>(NurbsCurve.class) { 1924 1925 @Override 1926 public void read(XMLFormat.InputElement xml, NurbsCurve obj) throws XMLStreamException { 1927 String name = xml.getAttribute("name", (String)null); 1928 if (nonNull(name)) 1929 obj.setName(name); 1930 FastMap userData = xml.get("UserData", FastMap.class); 1931 if (nonNull(userData)) { 1932 obj.putAllUserData(userData); 1933 } 1934 } 1935 1936 @Override 1937 public void write(NurbsCurve obj, XMLFormat.OutputElement xml) throws XMLStreamException { 1938 String name = obj.getName(); 1939 if (nonNull(name)) 1940 xml.setAttribute("name", name); 1941 FastMap userData = (FastMap)obj.getAllUserData(); 1942 if (!userData.isEmpty()) 1943 xml.add(userData, "UserData", FastMap.class); 1944 } 1945 }; 1946 1947}