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 clr
clr.AddReference('IronPython.Wpf')
import wpf
from System import Math, Action, Func, MidpointRounding
from System.Text import Encoding
from System.Collections.Generic import List
from System.Windows import Visibility
from System.Windows.Controls import StackPanel
from System.Windows.Input import Keyboard, AccessKeyManager
from System import Type, Tuple, Array, Double
from System.IO import StreamReader, MemoryStream
from System.Windows.Threading import Dispatcher
clr.AddReference("System.Drawing")
from System.Drawing import Bitmap
clr.AddReference("System.Windows.Forms")
from System.Windows.Forms import Control, Keys
try:
from Trimble.Vce.ViewModel.CommandLine.CommandLineCommands import OffsetLineCalculator
except:
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, ModelEvents
from Trimble.Vce.Core.Components import WorldView, Project, Layer, TextStyle
from Trimble.Vce.SurveyCAD import ProjectLineStyle
from Trimble.DiskIO import SearchPath, OptionsManager
from Trimble.Vce.Alignment.Linestring import Linestring,ElementFactory, IStraightSegment, IXYZLocation,ITangentArcSegment, ArcTangentType
from 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 Client
clr.AddReference("Trimble.Vce.UI.BaseCommands")
from Trimble.Vce.UI.BaseCommands import ViewHelper
clr.AddReference("Trimble.Vce.UI.UIManager")
from Trimble.Vce.UI.UIManager import UIEvents, TrimbleOffice
clr.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 up
try:
from Trimble.Sdk.UI import InputMethod, MousePosition, CursorStyle, UIEventArgs
from Trimble.Sdk.Interfaces.UI import ControlBoolean
except:
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, Line
from Trimble.Vce.Geometry import Point3D, Line2D, Side, Vector3D, Intersections
from Trimble.Vce.ForeignCad import TextEntity, Leader, MText, Point as CadPoint, TextUtilities, AttachmentPoint
from System.Text.RegularExpressions import Regex, Match, RegexOptions
from System import Decimal
from operator import itemgetter
from Trimble.Vce.Interfaces.SnapIn import IPolyseg
#from Trimble.Vce.UI.ConstructionCommands import EditTextCmd
from Trimble.Vce.Interfaces.Units import LinearType
from Trimble.Vce.UI.UIManager import TrimbleOffice, UIEvents
from Trimble.Vce.UI.Controls import VceMessageBox
from Trimble.Vce.UI.Controls.Wpf import OffsetEdit, SelectEntity, VerticalAngleEdit, TemplateSlope, DistanceEdit, ElevationEdit
from System import String
from System.Windows.Documents import Hyperlink
from System.Diagnostics import Process
from System.IO import FileStream, FileMode, FileAccess, Path
from System.Windows.Markup import XamlReader
from System.Windows.Controls import Label
from System.Windows.Input import Key
import time
import threading
from Trimble.Vce.Geometry.PolySeg.Node import Node
from Trimble.Vce.Alignment.Linestring.Linestring import ExtendType
from Trimble.Vce.Interfaces.SnapIn import ISnapIn
import os
import sys
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, parent_dir) # Add the parent directory to sys.path
from 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 UInt32
from Trimble.Vce.Core.Components.TextStyle import TextHeightMode, TextJustification
from Trimble.Vce.Interfaces.SnapIn import IPolyseg, IViewMember, ISettableName, IName, IViewSite, ISnapInHelpers
import wpf
from System.Collections.ObjectModel import ObservableCollection
from System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgs
from System.Windows.Controls import StackPanel, Button
from System.Windows.Media.Imaging import BitmapImage
from System.Windows import Application, Window
from System.IO import StreamReader
from System import Array, Exception
# Data Model for grid view
from System.Windows.Controls import DataGrid, DataGridTextColumn, DataGridTemplateColumn, Button
from System.Windows import DataTemplate, Style
from System.Windows.Controls.Primitives import ButtonBase
from System.Windows import FrameworkElementFactory
from System.Windows.Data import Binding
from System.Windows import RoutedEventArgs # Correct import for RoutedEventArgs
from System.Collections.ObjectModel import ObservableCollection
from System.Windows import Setter
from System.Windows import DependencyProperty
from System.Windows import RoutedEventHandler
from System.Windows.Controls import DataGridLength
from System import Uri, UriKind
from System.Windows.Controls import Image
from System.Windows import UIElement
from System.Windows.Media import Stretch
from System.Windows import Thickness
from System.Windows.Data import RelativeSource
from System.Windows import HorizontalAlignment, VerticalAlignment
from Trimble.Vce.Geometry.PolySeg import PolySeg
from 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 check
class 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
------------------------------