The following is really stretching accuracy limits. I can't see a geospatial use case where this becomes critical.
Here is probably some inaccuracy of the Sin/Cos computation for the rotation kicking in.
For geospatial use I'd probably just declare every deviation after the 10th decimal as unimportant.
Original Message:
Sent: 01-16-2025 20:55
From: Vitalii Vovk
Subject: Precision Challenges with Perpendicular Dot Product (perpDot) in TBC
My source data comes from simply drawing two lines (each with only two points: start and stop) in TBC at 90 degrees to each other.
If I convert those lines into vectors, the dot product of the vectors should theoretically be zero. However, when the lines are based around large coordinates, such as 2,000,000 and 6,000,000 (plus some 15-17 decimal points ), I was never able to achieve a dot product result of exactly zero.
I understand the floating-point precision issues and I tried everything I could think of to maintain consistent precision in the calculations. At that point, I wanted to determine when the dot product would be zero and when it wouldn't.
I did have some success figuring out overlapping segments in about 80% of cases, but the solution was not 100% reliable, which is frustrating. The reliability also seemed to depend on the length of the vectors, especially in cases where one vector was very large (e.g., 1 million meters) and the other was very small (e.g., less than 1 cm) (I did tey to normalize vectors as well). I had better success finding the intersection point between two segments as you have a little more room for accuracy ther
I concluded (though I might be wrong) that the error might originate in the coordinates within TBC itself. For instance, if a coordinate like 656389634.3456353534534553
is being used, it's likely not accurate beyond the 6th or 7th decimal place when using a float type.
I'm curious: if a dot product method exists in the TBC SDK, what approach do developers use to ensure high accuracy in such calculations when precision is must?
------------------------------
Vitalii Vovk
Original Message:
Sent: 01-13-2025 19:58
From: Ronny Schneider
Subject: Precision Challenges with Perpendicular Dot Product (perpDot) in TBC
Where exactly do those CAD line segments originate from? Another CAD package, survey data, or based on some coordinates and then rotated/offset/shifted all the time?
Arguing about the 7th or 8th decimal is completely dependent on the sources origin/accuracy.
You say that the point information you use to define a Vector3D might be incorrect? How do you come to this conclusion? Did you compare the Point3D information, that you've retrieved from a polyseg/segment???, and that doesn't match the data you see in another package.
Please upload some sample data?
------------------------------
Ronny Schneider
Original Message:
Sent: 12-17-2024 09:08
From: Vitalii Vovk
Subject: Precision Challenges with Perpendicular Dot Product (perpDot) in TBC
I was analyzing CAD line segments and noticed that the input points used to build vectors in TBC might be inherently incorrect, probably due to floating-point precision issues. I was wondering how much I can trust the precision of the input from the Segment Points in TBC.
You can tell a lot of things by utilizing the perpDot Product for segments.
The approach that I took on overlapping segments:
I built vectors that correspond to the given segments.
If segments are parallel, the perpDot of their vectors should be 0.
Now we test if the segments are on the same line by comparing the vector of Segment A to a new vector built from any point of Segment A to any point of Segment B. If they are on the same line, the perpDot will be 0.
You have to choose a vector and point to which all results will be referenced. I chose Segment A and its start point.
Now you can build additional vectors from the start point of Segment A: one to the start point of Segment B and another to the end point of Segment B.
By finding out the scalar between the vector of Segment A and, for example, the vector from the start point of Segment A to the start point of Segment B (simply dividing one of the coordinates by the other), you will get the parametric value for the start point of Segment B in relation to the start point of Segment A. If it is between 0 and 1, the start point of Segment B is within Segment A. If it is 0, it is the same as the start point of Segment A. If it equals 1, it is the same point as the end of Segment A. If it is less than 0, it is before the start point of Segment A. If it is more than 1, it is past the limits of Segment A in the direction of the vector from Segment A.
------------------------------
Vitalii Vovk
Original Message:
Sent: 12-17-2024 02:24
From: Ronny Schneider
Subject: Precision Challenges with Perpendicular Dot Product (perpDot) in TBC
Hi Vitalii,
even without looking at your code I can see that there will always be an issue to make a 100 % accurate statement whether an extremely short segment is parallel to a very long one.
The problem I see is the source of the coordinates for those segments. If I assume that they come from some survey activity the best you can hope for is probably 1/10 mm.
That means the definition of your segments will be accurate to 1/10 over 1 inch and 1/10 over 1000 foot. The spatial definition of the longer segment will be much more accurate than the shorter one.
I'd probably compute the offset of the shorter segment nodes to the longer segment. And when the difference of the perpendicular offset is smaller than my expected coordinate precision I'd declare the segments parallel.
I haven't tested your code or run some experiments on my own yet.
Do you see inconsistent offset results if you declare a long segment (i.e. 0 to 100000) and a short one (i.e. +0.0001 to +0.0002)? Do you get as answer that the short segment may be on the left side of the line?
------------------------------
Ronny Schneider
Original Message:
Sent: 12-16-2024 12:02
From: Vitalii Vovk
Subject: Precision Challenges with Perpendicular Dot Product (perpDot) in TBC
For the past couple of weeks, I've been exploring the dot product and perpendicular dot product (perpDot) practical features . The perpDot product, in particular, has some unique applications, such as:
- Determining if two lines are parallel.
- Identifying which side of the line a point lies on.
- Finding the intersection point between two line segments.
While figuring out the math behind these operations was relatively straightforward, I've hit a wall regarding achieving consistent precision for the perpDot calculations. This issue becomes especially noticeable when comparing small segments (less than 0.1') with large segments (more than 1000').
I have a questions to the developers and others experienced with TBC's coding:
What is the best approach to calculate the perpDot product when precision is critical?
- Should I rely on rounding or truncating numbers to improve precision?
- Are there any built-in TBC tools, settings, or practices that could help ensure accuracy in such calculations?
I've tried different ways to make the perpDot calculation more accurate and consistent:
- Using native TBC tools to calculate perpDot.
- Truncating input values (e.g., limiting decimal places for point coordinates).
- Rounding numbers to different levels of precision.
These attempts have improved the results to some extent, but the precision still isn't ideal for my needs.
Any pointers and help will be appreciated. Also, I share some of the code that was given the most desirable results among all of the other options (code might not be efficient or redundant in some places)
import clrclr.AddReference('IronPython.Wpf')import wpffrom System import Math, Action, Func, MidpointRoundingfrom System.Text import Encodingfrom System.Collections.Generic import Listfrom System.Windows import Visibilityfrom System.Windows.Controls import StackPanelfrom System.Windows.Input import Keyboard, AccessKeyManagerfrom System import Type, Tuple, Array, Doublefrom System.IO import StreamReader, MemoryStreamfrom System.Windows.Threading import Dispatcherclr.AddReference("System.Drawing")from System.Drawing import Bitmapclr.AddReference("System.Windows.Forms")from System.Windows.Forms import Control, Keys try: from Trimble.Vce.ViewModel.CommandLine.CommandLineCommands import OffsetLineCalculatorexcept: clr.AddReference("Trimble.Sdk.UI.Commands") from Trimble.Sdk.UI.Commands.CommandLine import OffsetLineCalculator try: clr.AddReference("Trimble.Sdk") # In version 5.50, model assemblies are "pre-referenced"except: clr.AddReference("Trimble.Vce.Core") clr.AddReference("Trimble.Vce.Alignment") clr.AddReference("Trimble.Vce.ForeignCad") clr.AddReference("Trimble.Vce.Data.Construction") clr.AddReference("Trimble.Vce.Geometry") clr.AddReference("Trimble.Vce.Units") clr.AddReference("Trimble.Vce.Interfaces") clr.AddReference("Trimble.DiskIO") clr.AddReference("Trimble.Vce.Gem") from Trimble.Vce.Core import TransactMethodCall, ModelEventsfrom Trimble.Vce.Core.Components import WorldView, Project, Layer, TextStylefrom Trimble.Vce.SurveyCAD import ProjectLineStylefrom Trimble.DiskIO import SearchPath, OptionsManager from Trimble.Vce.Alignment.Linestring import Linestring,ElementFactory, IStraightSegment, IXYZLocation,ITangentArcSegment, ArcTangentTypefrom Trimble.Vce.Alignment.Parameters import Bearing, Elevation from Trimble.Vce.Data.Construction.Settings import ConstructionCommandsSettings from Trimble.Vce.Geometry import Point3D, Side, Vector3D from Trimble.Vce.Interfaces import Clientclr.AddReference("Trimble.Vce.UI.BaseCommands")from Trimble.Vce.UI.BaseCommands import ViewHelperclr.AddReference("Trimble.Vce.UI.UIManager")from Trimble.Vce.UI.UIManager import UIEvents, TrimbleOfficeclr.AddReference("Trimble.Vce.UI.Controls")# later version of TBC moved these to a different assembly. If not found in new location, look at old #This part of the script was replaced to be able to start it in TBC 5.9 and uptry: from Trimble.Sdk.UI import InputMethod, MousePosition, CursorStyle, UIEventArgs from Trimble.Sdk.Interfaces.UI import ControlBooleanexcept: try: from Trimble.Sdk.Interfaces.UI import InputMethod, MousePosition, CursorStyle, ControlBoolean, UIEventArgs except: try: from Trimble.Vce.UI.UIManager import UIEventArgs,TrimbleOffice # version 5.40 from Trimble.CustomControl.Interfaces.Enums import InputMethod clr.AddReference("Trimble.Vce.ViewModel") from Trimble.CustomControl.Interfaces import MousePosition, ControlBoolean from Trimble.CustomControl.Interfaces.Enums import CursorStyle except: from Trimble.Vce.UI.Controls import MousePosition, CursorStyle, ControlBoolean,VceMessageBox # my added imports from Trimble.Vce.Geometry.PolySeg.Segment import Segment, Linefrom Trimble.Vce.Geometry import Point3D, Line2D, Side, Vector3D, Intersectionsfrom Trimble.Vce.ForeignCad import TextEntity, Leader, MText, Point as CadPoint, TextUtilities, AttachmentPointfrom System.Text.RegularExpressions import Regex, Match, RegexOptionsfrom System import Decimalfrom operator import itemgetterfrom Trimble.Vce.Interfaces.SnapIn import IPolyseg#from Trimble.Vce.UI.ConstructionCommands import EditTextCmdfrom Trimble.Vce.Interfaces.Units import LinearTypefrom Trimble.Vce.UI.UIManager import TrimbleOffice, UIEventsfrom Trimble.Vce.UI.Controls import VceMessageBox from Trimble.Vce.UI.Controls.Wpf import OffsetEdit, SelectEntity, VerticalAngleEdit, TemplateSlope, DistanceEdit, ElevationEditfrom System import Stringfrom System.Windows.Documents import Hyperlinkfrom System.Diagnostics import Process from System.IO import FileStream, FileMode, FileAccess, Pathfrom System.Windows.Markup import XamlReaderfrom System.Windows.Controls import Labelfrom System.Windows.Input import Keyimport timeimport threadingfrom Trimble.Vce.Geometry.PolySeg.Node import Node from Trimble.Vce.Alignment.Linestring.Linestring import ExtendType from Trimble.Vce.Interfaces.SnapIn import ISnapIn import osimport sysparent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))sys.path.insert(0, parent_dir) # Add the parent directory to sys.pathfrom TeichertUtilities import VectorsAngle, JoinPadLines, Units, Utilities, Validator from Trimble.Vce.Coordinates import Point as CoordPoint from Trimble.Vce.Core.Components import WorldView from System import UInt32from Trimble.Vce.Core.Components.TextStyle import TextHeightMode, TextJustificationfrom Trimble.Vce.Interfaces.SnapIn import IPolyseg, IViewMember, ISettableName, IName, IViewSite, ISnapInHelpers import wpffrom System.Collections.ObjectModel import ObservableCollectionfrom System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgsfrom System.Windows.Controls import StackPanel, Buttonfrom System.Windows.Media.Imaging import BitmapImagefrom System.Windows import Application, Windowfrom System.IO import StreamReaderfrom System import Array, Exception # Data Model for grid viewfrom System.Windows.Controls import DataGrid, DataGridTextColumn, DataGridTemplateColumn, Buttonfrom System.Windows import DataTemplate, Stylefrom System.Windows.Controls.Primitives import ButtonBasefrom System.Windows import FrameworkElementFactoryfrom System.Windows.Data import Bindingfrom System.Windows import RoutedEventArgs # Correct import for RoutedEventArgsfrom System.Collections.ObjectModel import ObservableCollectionfrom System.Windows import Setterfrom System.Windows import DependencyProperty from System.Windows import RoutedEventHandlerfrom System.Windows.Controls import DataGridLength from System import Uri, UriKindfrom System.Windows.Controls import Imagefrom System.Windows import UIElementfrom System.Windows.Media import Stretch from System.Windows import Thicknessfrom System.Windows.Data import RelativeSourcefrom System.Windows import HorizontalAlignment, VerticalAlignment from Trimble.Vce.Geometry.PolySeg import PolySegfrom decimal import Decimal as pDecimal, getcontext #important to change name to Decimal not get confused with .net decimals # addin this vector that will be inherited by custom vector class for the purpose of the instance checkclass CustomVector: pass class SegmentsSettings: def __init__(self, **kwargs): # Validate and initialize attributes self._container = kwargs.get("container", None) self._segment = kwargs.get("segment", None) self._segmentA = kwargs.get("segmentA", None) self._segmentB = kwargs.get("segmentB", None) self._point = kwargs.get("point", None) # Validate container at initialization if self._container is not None and not isinstance(self._container, WorldView): raise TypeError("Passed container is empty or incorrect") @property def container(self): return self._container @container.setter def container(self, value): # Validate container type if value is None or not isinstance(value, WorldView): raise TypeError("Passed container is empty or incorrect") self._container = value @property def vPoly(self): return self._vPoly @vPoly.setter def vPoly(self, value ): test = clr.GetClrType(PolySeg) if not isinstance(value, PolySeg): TypeError("object should be type of vertical PolySeg") self._vPoly = value @property def point(self): return self._point @point.setter def point(self, value): if value is not None and not isinstance(value, Point3D): raise TypeError("point must be type of Point3D") self._point = value @property def segment(self): return self._segment @segment.setter def segment(self, value): # Validate type using isinstance if value is not None and not isinstance(value, Segment): raise TypeError("segment must be of type Segment.") self._segment = value @property def segmentA(self): return self._segmentA @segmentA.setter def segmentA(self, value): if value is not None and not isinstance(value, Segment): raise TypeError("segmentA must be of type Segment.") self._segmentA = value @property def segmentB(self): return self._segmentB @segmentB.setter def segmentB(self, value): if value is not None and not isinstance(value, Segment): raise TypeError("segmentB must be of type Segment.") self._segmentB = value class CustomSegment(SegmentsSettings): def __init__(self, **kwargs): super(CustomSegment, self).__init__(**kwargs) def overlappedSegments(self, segA, segB): """ This method calculates the overlap properties between two segments (Segment A and Segment B). Segment A is considered the primary segment, and all calculations are based on its start point. **Key Outputs:** - `overlap`: Boolean, True if any part of Segment B overlaps with Segment A. - `tB0`, `tB1`: t-values of Segment B's start and end points projected onto Segment A AKA ParametricLocation. - `p_B0_overlap`: Boolean, True if Segment B's start point is within the bounds of Segment A. - `p_B1_overlap`: Boolean, True if Segment B's end point is within the bounds of Segment A. - `stationB0`: The station on Segment A corresponding to Segment B's start point if overlapping. - `stationB1`: The station on Segment A corresponding to Segment B's end point if overlapping. - `segBinSegA`: Boolean, True if Segment B is fully inside or equal to Segment A. - `segAinSegB`: Boolean, True if Segment A is fully inside of Segment B. **Notes:** - If Segment A is fully inside Segment B, `stationB0`, `stationB1`, `p_B0_overlap`, and `p_B1_overlap` will remain None. - Calculations use canonical line equations {x=mt+x0; y=mt+y0} or the equivalent gA(s) = A0 + t * (A1 - A0). - Parallelism is determined using `PerpDot` (Perpendicular Dot Product). If segments are parallel, they are checked for collinearity. """ if segA is None or segB is None: raise ValueError("segmentA and segmentB should be instances of Segment in overlappedSegments method.") # Assign segments segA = self.segment = segA segB = self.segment = segB if segA.IsZeroLength2D(): raise ValueError("Segment A Length 2D is 0") if segB.IsZeroLength2D(): raise ValueError("Segment A Length 2D is 0") #!!!!! need to check if segments are not 0 length !!!!!!!!!! # Initialize 2D vectors for calculations (to ensure compatibility) # Direction vector for Segment A and B vA = self.__calculateVectorPrecise(segA.BeginPoint, segA.EndPoint, imitateVector = True) #vAt = segA.EndPoint - segA.BeginPoint # for testing purposes only vB = self.__calculateVectorPrecise(segB.BeginPoint, segB.EndPoint, imitateVector = True) #vBt = segB.EndPoint - segB.BeginPoint # for testing purposes only #Vector from start of Segment A to Start of Segment B vU_A0_B0 = self.__calculateVectorPrecise(segA.BeginPoint, segB.BeginPoint, imitateVector = True ) #Vector from start of Segment A to End of Segment B vU_A0_B1 = self.__calculateVectorPrecise(segA.BeginPoint, segB.EndPoint, imitateVector = True ) # Initialize return values overlap = False # Flag indicating if any overlap exists tB0 = None # t-value for Segment B's start point on Segment A tB1 = None # t-value for Segment B's end point on Segment A p_B0_overlap = False # True if Segment B's start is within Segment A p_B1_overlap = False # True if Segment B's end is within Segment A stationB0 = None # Station of Segment B's start point on Segment A stationB1 = None # Station of Segment B's end point on Segment A segAinSegB = False # True if Segment A is fully inside Segment B segBinSegA = False # True if Segment B is fully inside Segment A # Default return values defaultValues = (overlap, tB0, tB1, p_B0_overlap, p_B1_overlap, stationB0, stationB1, segBinSegA, segAinSegB) # Check if the segments are collinear (parallel) if not self.__calculatePerpDotPrecise(vA, vB, normalize = True, autothreshHold=True) == 0: # is 15 16 digits prcision for doubles in c# ? #if not self.simpleCollinear(vA, vB) : # is 15 16 digits prcision for doubles in c# ? return defaultValues # Segments are not parallel # Check if the segments are collinear, if true it also means they are located on the same line if not self.__calculatePerpDotPrecise(vA, vU_A0_B0, normalize=True, autothreshHold=True) == 0: #if not self.simpleCollinear(vA, vU_A0_B0): return defaultValues # Segments do not coincide on the same line # Calculate t-values for Segment B's start and end points projected onto Segment A tB0 = self.__parametricValuePrecise(vU_A0_B0,vA, roundValueDigits = 9) tB1 = self.__parametricValuePrecise(vU_A0_B1,vA , roundValueDigits = 9) # Check if Segment B's start point is on Segment A if 0 <= tB0 <= 1: p_B0_overlap = True # Overlap exists stationB0 = float(pDecimal(str(segA.BeginStation)) + (pDecimal(str(segA.EndStation)) * pDecimal(str(tB0)))) # the same as segA.Station(tB0) # Check if Segment B's end point is on Segment A if 0 <= tB1 <= 1: p_B1_overlap = True # Overlap exists try: stationB1 = float(pDecimal(str(segA.BeginStation)) + pDecimal(str(segA.EndStation)) * pDecimal(str(tB1))) # the same as segA.Station(tB1) except Exception as err: # Handle errors and display an error message. VceMessageBox.Show(TrimbleOffice.TheOffice.MainWindow.Form, "Error: {}".format(err)) # Determine overlap and containment relationships if p_B0_overlap and p_B1_overlap: # Segment B is fully within Segment A if (tB0 in {0.0, 1.0}) and (tB1 in {0.0, 1.0}): # checking if segment end points are indentical segAinSegB = True segBinSegA = True overlap = True elif not p_B0_overlap and not p_B1_overlap: if tB0 is not None and tB1 is not None: # Segment A is fully within Segment B if tB0 * tB1 < 0: #Only if to ends with oposite sign it meats the segment A is fully inside of the segment B # NEEDDS TO BE FIXED ONE SIDE CAN EQUALS 0 segAinSegB = True overlap = True # clear Parametric value for Segment A because end points are outside tB0 = None tB1 = None else: # needs to be fixed added if 0 and t should give you if (tB0 in {0.0, 1.0}) or (tB1 in {0.0, 1.0}): if tB0 == 0.0 and (tB0 + tB1) > 0: segAinSegB = True elif tB1 == 0.0 and (tB0 + tB1) > 0: segAinSegB = True elif tB0 == 1.0 and (tB0 * tB1) < 0: segAinSegB = True elif tB1 == 1.0 and (tB0 * tB1) < 0: segAinSegB = True # Partial overlap: At least one endpoint of Segment B overlaps with Segment A overlap = True tB0 = tB0 if p_B0_overlap else None tB1 = tB1 if p_B1_overlap else None # Update return values if overlap exists if overlap: defaultValues = (overlap, tB0, tB1, p_B0_overlap, p_B1_overlap, stationB0, stationB1, segBinSegA, segAinSegB) return defaultValues def simpleCollinear(self, vA, vB, truncateValue=6): # Case 1: Both vectors are vertical if vA.X == 0 and vB.X == 0: return True # Case 2: Both vectors are horizontal (slope = 0) if vA.Y == 0 and vB.Y == 0: return True # Case 3: Neither vector is vertical, compare tangents if vA.X != 0 and vB.X != 0: getcontext().prec = 200 tanA = Math.Atan(pDecimal(str(vA.Y)) / pDecimal(str(vA.X))) # Tangent for vA tanB = Math.Atan(pDecimal(str(vB.Y)) / pDecimal(str(vB.X))) # Tangent for vB # Truncate values to the specified precision tanA = self.truncate_to(tanA, digits=truncateValue) tanB = self.truncate_to(tanB, digits=truncateValue) # Compare tangents for collinearity if tanA == tanB: return True # Case 4: Non-matching cases return False def __calculateVectorPrecise(self, StartV, EndV, imitateVector = False): """ Parameters: StartV, EndV: Objects with attributes X, Y, and Z (representing 3D points). Returns: VectorImitation: A new object representing the vector difference. """ getcontext().prec = 200 # Set precision for Decimal calculations. class VectorImitationPrecise(CustomVector): def __init__(self, X, Y, Z, length2D=None): """ Initializes the VectorImitationPrecise instance. Parameters: X, Y, Z (Decimal or compatible): Components of the vector. length2D (Decimal, optional): Precomputed 2D length. If None, it is calculated. """ if not all(isinstance(value, (Decimal, int, float, pDecimal)) for value in [X, Y, Z] if value is not None): raise ValueError("X, Y, and Z must be Decimal, int, or float.") self.X = pDecimal(X) self.Y = pDecimal(Y) self.Z = pDecimal(Z) if Z is not None else None self.Length2D = self.calcLength2D() if length2D is None else length2D self.Length = self.calcLength() self.Angle = Math.Atan2(self.Y, self.X) self.Azimuth = self.azimuth() def azimuth(self): if self.X != 0: angle = abs(Math.Atan(self.Y/self.X)) if self.X >= 0 and self.Y >= 0: #first quadrant return (Math.PI/2) - angle elif self.X < 0 and self.Y >= 0: # second quadrant return (3*Math.PI/2) + angle elif self.X < 0 and self.Y < 0: #third quadrant return (3*Math.PI/2) - angle elif self.X >= 0 and self.Y < 0: return (Math.PI/2) + angle def calcLength(self): """ Calculates the 3D length of the vector with high precision. Returns: Decimal: The 3D length of the vector """ if self.Z is None: return self.sqrt_precise(self.sum_of_squares(self.X, self.Y)) return self.sqrt_precise(self.sum_of_squares(self.X, self.Y, self.Z)) def Clone(self): """ Creates a copy of the current instance. Returns: VectorImitationPrecise: A new instance with the same attribute values. """ return VectorImitationPrecise(self.X, self.Y, self.Z, length2D=self.Length2D) def calcLength2D(self): """ Calculates the 2D length of the vector (ignoring Z) with high precision. Returns: Decimal: The 2D length of the vector. """ return self.sqrt_precise(self.sum_of_squares(self.X, self.Y)) def sum_of_squares(self, *values): """ Computes the sum of squares for multiple components with high precision. Parameters: components (Decimal): Components for which to calculate the sum of squares. Returns: Decimal: The sum of squares of the components. """ result = sum(value * value for value in values) return result def sqrt_precise(self, value, precision=200): if value < 0: raise ValueError("Cannot compute the square root of a negative number.") if value == 0: return pDecimal(0) getcontext().prec = precision guess = value / 2 epsilon = pDecimal('1e-{0}'.format(precision // 2)) while True: next_guess = (guess + value / guess) / 2 if abs(next_guess - guess) < epsilon: return next_guess guess = next_guess def __add__(self, object): """ Adds a Point3D object to the current Point3D instance. Parameters: object (Point3D): The Point3D object to add. Returns: Point3D: A new Point3D object representing the sum of the two points. """ if isinstance(object, clr.GetClrType(Point3D)): getcontext().prec = 200 x_Value = self.X + pDecimal(str(object.X)) y_Value = self.Y + pDecimal(str(object.Y)) z_Value = ( self.Z + pDecimal(str(object.Z)) if not Double.IsNaN(object.Z) and self.Z is not None else Double.NaN ) return Point3D(x_Value, y_Value, z_Value) def __mul__(self, value): """ Multiplies the current Point3D instance by a scalar value. Parameters: value (Double or Decimal): The scalar value to multiply by. Returns: VectorImitation: A new VectorImitation object representing the scaled point. """ if isinstance(value, (Double, Decimal)): getcontext().prec = 200 scalar = pDecimal(str(value)) x_Value = scalar * self.X y_Value = scalar * self.Y z_Value = scalar * self.Z if self.Z is not None else None return VectorImitationPrecise (x_Value, y_Value, z_Value) else: raise TypeError("Unsupported operand for ImitateVectorPrecise") def __rmul__(self, scalar): """ Multiplies the vector by a scalar value (scalar * vector). Parameters: scalar (float or Decimal): The scalar value. Returns: VectorImitationPrecise: A new scaled vector. """ return self.__mul__(scalar) try: # Calculate precise differences for X, Y, and Z components. x_diff = pDecimal(str(EndV.X)) - pDecimal(str(StartV.X)) y_diff = pDecimal(str(EndV.Y)) - pDecimal(str(StartV.Y)) if Double.IsNaN(StartV.Z) or Double.IsNaN(EndV.Z): z_diff = None else: z_diff = pDecimal(str(EndV.Z)) - pDecimal(str(StartV.Z)) #test = float(VectorImitationPrecise(pDecimal(54.706244987), pDecimal(84.603718944), None).length2D) # Create and return the result vector. if imitateVector: return VectorImitationPrecise(x_diff, y_diff, z_diff) return Vector3D(float(x_diff), float(y_diff), Double.NaN if z_diff is None else float(z_diff)) except Exception as err: # Handle errors and display an error message. VceMessageBox.Show(TrimbleOffice.TheOffice.MainWindow.Form, "Error: {}".format(err)) return None def __calculatePerpDotPrecise(self, vA, vB, autothreshHold=False, normalize = False): """ Calculates the perpendicular dot product (PerpDot) of two vectors with high precision. Parameters: vA (Vector3D): The first vector. vB (Vector3D): The second vector. precision (int): The precision to use for the pDecimal calculations. Returns: pDecimal: The PerpDot product of the vectors with high precision. """ getcontext().prec = 200 if normalize: vA = self.__normalizedPrecice(vA) vB = self.__normalizedPrecice(vB) if autothreshHold: minL = vA.Length2D if vA.Length2D < vB.Length2D else vB.Length2D treshHold = self.__setThreshHold(minL) perpDot = (pDecimal(str(vA.X)) * pDecimal(str(vB.Y))) - (pDecimal(str(vA.Y)) * pDecimal(str(vB.X))) if autothreshHold: if abs(perpDot) <= treshHold: return pDecimal(0) return perpDot def __setThreshHold(self, value): """ Determines the appropriate threshold based on the input value. Iterates through thresholds in ascending order of keys. Parameters: value (float): The input value to determine the threshold. Returns: float: The appropriate threshold for the given value. """ thresholds = { 0.005: 1E-5, 0.01: 1E-6, 0.1: 1E-7, 1: 1E-8, 10: 1E-9, 100: 1E-10, 10000: 1E-11, 100000: 1E-12, 1000000: 1E-13, } # Iterate over keys in ascending order for key in sorted(thresholds.keys()): if value <= key: return thresholds[key] return 1E-14 def __normalizedPrecice(self, v, tresHold=pDecimal("1E-8")): """ Normalizes the vector `v` with high precision, setting its 2D length to 1. Parameters: v (Vector2D): The vector to normalize. tresHold (Decimal): The threshold below which the vector is considered too small to normalize. Returns: Vector2D: The normalized vector with its length set to 1. """ v = v.Clone() # Check if the vector is effectively zero if isinstance(v, CustomVector): if pDecimal(str(v.Length2D)) < tresHold: v.Length2D = pDecimal(0) v.X = pDecimal(0) v.Y = pDecimal(0) if v.Z: v.Z = pDecimal(0) return v # Set precision for Decimal calculations getcontext().prec = 200 # Normalize the vector v.X = pDecimal(str(v.X)) / pDecimal(str(v.Length2D)) v.Y = pDecimal(str(v.Y)) / pDecimal(str(v.Length2D)) v.Length2D = pDecimal(1) # Set normalized length only works for imitate vector return v else: return v.Normalized() def __parametricValuePrecise(self, firstV, secondV, precision=200, roundValue=True, roundValueDigits = 15, returnFloat=True): """ Calculates precise parametric values using high precision with the Decimal module. Parameters: firstV (Vector3D): The vector representing the numerator. secondV (Vector3D): The vector representing the denominator. Returns: float or Decimal: The parametric value, rounded to the specified precision. """ test1 = float(firstV.X) test2 = float(secondV.X) result = test1/test2 # Set the precision for Decimal calculations getcontext().prec = precision try: # Calculate parametric value using X components t = pDecimal(str(firstV.X)) / pDecimal(str(secondV.X)) except (ZeroDivisionError, InvalidOperation): try: # Fallback to Y components if X components fail t = pDecimal(str(firstV.Y)) / pDecimal(str(secondV.Y)) except (ZeroDivisionError, InvalidOperation): # Should not occur if the vectors are valid raise ZeroDivisionError("Division by zero encountered for both X and Y components of the second vector.") # Round the result to the specified number of decimal places if roundValue: rounding_format = '1e-' + str(roundValueDigits) t = t.quantize(pDecimal(rounding_format)) if returnFloat: t = float(t) # Return as float or Decimal based on user preference return t def pointSide(self, segment, point, thresHold = float("1E-7")): """ it returns the point is to the segment """ seg = self.segment = segment point = self.point = point v = seg.EndPoint - seg.BeginPoint vP = point - seg.EndPoint vt = self.__calculateVectorPrecise(seg.BeginPoint, seg.EndPoint) vPt = self.__calculateVectorPrecise(seg.EndPoint, point) perpDot = Math.Round(Vector3D.PerpDot(v, vP), 10) test = self.__calculatePerpDotPrecise(vt, vPt) test = float(test) # if may not catch if point directly on the line if abs(perpDot) < thresHold: return Side.On else: return Side.Right if perpDot < 0 else Side.Left def segmentInersection(self, segA, segB, elevationMode = 1): """ Base for finding intersection is canonical formula for a line in a form {x= mt + x0 y= nt + y0} that can be writen as in a matrix form A = at + A0 if two line have the solution that is the point of itersection as+A0 = bt+B0 and multiply one of the vectors on perpDot of itself that would give us 0 **Key Outputs:** - point that represent point of intesection - s parametric value (scalar for vector a to point of intersection) for segemenA - t parametric value for sefment B **Notes:** - if parametic valuw is from 0 to 1 it means point of intersection on that segment or both of the segments if both parametric values are in the range from 0 to 1 - This method will work well horizontal nodes. If line has horizontal Nodes and VPI you woudl need to convert VPI to horizontal nodes to make this method give correct ele vation **Elevation Mdodes** - 1 - return intersection point with grade projection based on Segment A - 2 - return intersection point with grade projection based on Segment B - 3 - return intersection point with grade that would mean elevation between segment A and segment B - 4 - returns minimul elevation between segment A and Segment B - 5 - returns maximum elevation between Segment A and Segment B """ segA = self.segment = segA #run segment throught the setter segB = self.segment = segB #run segment throught the setter vA = self.__calculateVectorPrecise(segA.BeginPoint, segA.EndPoint) # create a vector based on segment vB = self.__calculateVectorPrecise(segB.BeginPoint, segB.EndPoint) # create a vector based on segment isParallel = self.simpleCollinear(vA, vB, truncateValue=6) # make sure this segmens are not parallel if isParallel: return False vU_A0_B0 = self.__calculateVectorPrecise(segA.BeginPoint, segB.BeginPoint) #addtiional vector to make formula work vU_B0_A0 = self.__calculateVectorPrecise(segB.BeginPoint, segA.BeginPoint) #computation for segment A s = self. __calculatePerpDotPrecise(vB,vU_A0_B0 ) / self. __calculatePerpDotPrecise(vB,vA ) # parametric Value for segment A s = float(s) # have to flip in order to use my custom __add__ (it meters on which side '+' is ) #it works faster if you dont use imitateVectorPrecice (CustomVector) if isinstance(vA, CustomVector): pA = (s * vA) + segA.BeginPoint #point with elevation based on Segment A else: pA = segA.BeginPoint + (s * vA) #computation for serment B t = self. __calculatePerpDotPrecise(vA,vU_B0_A0 ) / self. __calculatePerpDotPrecise(vA, vB ) # parametric Value parametrivale for Segment B t = float(t) # have to flip in order to use my custom __add__ (it meters on which side '+' is ) #it works faster if you dont use imitateVectorPrecice (CustomVector) if isinstance(vB, CustomVector): pB = (t * vB) + segB.BeginPoint #point with elevation based on Segment B else: pB = segB.BeginPoint + (t * vB) point = None # Adjust resulted point for the selected elevation mode if elevationMode == 1: point = pA elif elevationMode == 2: point = pB elif elevationMode == 3: meanEl = (pA.Z + pB.Z)/2 point = Point3D(pA.X, pA.Y, meanEl ) elif elevationMode == 4: minEl = pA.Z if pA.Z < pB.Z else pB.Z point = Point3D(pA.X, pA.Y,minEl ) elif elevationMode == 5: maxEl = pA.Z if pA.Z > pB.Z else pB.Z point = Point3D(pA.X, pA.Y, maxEl) roundedPoint = self.__roundPointValues(point) #round up poitn x,y, and z value return (roundedPoint, s, t ) def __roundPointValues(self, point, digits=6): if isinstance(point, Point3D): x = Math.Round(point.X, digits) y = Math.Round(point.Y, digits) z = Math.Round(point.Z, digits) if point.Z is not Double.NaN else Double.NaN return Point3D(x,y,z) return None def truncate_to(self, value, digits=15): """ Truncate a floating-point number to the specified number of decimal places without rounding. Parameters: value (float): The number to be truncated. digits (int): The number of decimal places to keep. Returns: float: The truncated number. """ value_str = "%.20f" % value # Use classic string formatting for high precision if '.' in value_str: integer_part, decimal_part = value_str.split('.') truncated_str = integer_part + '.' + decimal_part[:digits] return float(truncated_str) return float(value_str) def truncate_entity(self, entity, digits=15): """ Truncate all components of a Vector3D or Point3D to a fixed number of decimal places. Parameters: entity (Vector3D or Point3D): The vector or point to be truncated. digits (int): The number of decimal places to keep. type (str): The type of object to return ("Vector" or "Point"). Returns: Vector3D or Point3D: A new object with truncated components. """ truncated_x = self.truncate_to(entity.X, digits) truncated_y = self.truncate_to(entity.Y, digits) truncated_z = self.truncate_to(entity.Z, digits) if isinstance(entity, clr.GetClrType(Vector3D)): return Vector3D(truncated_x, truncated_y, truncated_z) elif isinstance(entity, clr.GetClrType(Point3D)): return Point3D(truncated_x, truncated_y, truncated_z) else: raise ValueError("Invalid type specified. Use 'Vector' or 'Point'.")
------------------------------
Vitalii Vovk
------------------------------