diff --git a/LICENSE b/LICENSE
index d3f3c206..87a328f2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c)2025 Bit By Bit Developers
+Copyright (c) 2025 Bit By Bit Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/languages/ar.json b/languages/ar.json
index 25b4cc6b..2d145c7b 100644
--- a/languages/ar.json
+++ b/languages/ar.json
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "دبوس مع تسمية",
"pinWithLabel": "دبوس مع تسمية",
"bitbybit.occt.dimensions.pinWithLabel_description": "يُنشئ دبوسًا مع تسمية. يمكن استخدامه لشرح أشياء حول النماذج أو تحديد أشياء مهمة في المشهد ثلاثي الأبعاد.",
- "offsetFromStart": "إزاحة عن البداية"
+ "offsetFromStart": "إزاحة عن البداية",
+ "bitbybit.vector.lengthSq": "الطول التربيعي",
+ "lengthSq": "الطول التربيعي",
+ "bitbybit.vector.lengthSq_description": "يحسب الطول التربيعي للمتجه",
+ "bitbybit.point.twoPointsAlmostEqual": "نقطتان متساويتان",
+ "twoPointsAlmostEqual": "نقطتان متساويتان",
+ "bitbybit.point.twoPointsAlmostEqual_description": "يتحقق مما إذا كانت نقطتان متساويتين تقريبًا",
+ "bitbybit.line.lineToSegment": "خط إلى قطعة مستقيمة",
+ "lineToSegment": "خط إلى قطعة مستقيمة",
+ "bitbybit.line.lineToSegment_description": "تحويل الخط إلى قطعة مستقيمة",
+ "bitbybit.line.linesToSegments": "خطوط إلى قطع مستقيمة",
+ "linesToSegments": "خطوط إلى قطع مستقيمة",
+ "bitbybit.line.linesToSegments_description": "يحول الخطوط إلى قطع مستقيمة",
+ "bitbybit.line.segmentToLine": "قطعة مستقيمة إلى خط",
+ "segmentToLine": "قطعة مستقيمة إلى خط",
+ "bitbybit.line.segmentToLine_description": "يحول القطعة المستقيمة إلى خط",
+ "segment": "قطعة مستقيمة",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "قطع مستقيمة إلى خطوط",
+ "segmentsToLines": "قطع مستقيمة إلى خطوط",
+ "bitbybit.line.segmentsToLines_description": "يحول القطع المستقيمة إلى خطوط",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "فرز القطع المستقيمة إلى خطوط متعددة",
+ "sortSegmentsIntoPolylines": "فرز القطع المستقيمة إلى خطوط متعددة",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "إنشاء الخطوط المتعددة من قطع مستقيمة قد تكون متصلة ولكنها مبعثرة عشوائيًا",
+ "sort": "فرز",
+ "bitbybit.mesh.signedDistanceToPlane": "المسافة الموجهة إلى المستوى",
+ "signedDistanceToPlane": "المسافة الموجهة إلى المستوى",
+ "bitbybit.mesh.signedDistanceToPlane_description": "يحسب المسافة الموجهة من نقطة إلى مستوى.",
+ "plane": "مستوى",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "حساب مستوى المثلث",
+ "calculateTrianglePlane": "حساب مستوى المثلث",
+ "bitbybit.mesh.calculateTrianglePlane_description": "يحسب مستوى المثلث من المثلث.",
+ "triangle": "مثلث",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "تقاطع مثلث مع مثلث",
+ "triangleTriangleIntersection": "تقاطع مثلث مع مثلث",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "يحسب تقاطع مثلثين.",
+ "triangle1": "مثلث 1",
+ "triangle2": "مثلث 2",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "قطع تقاطع شبكتين",
+ "meshMeshIntersectionSegments": "قطع تقاطع شبكتين",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "يحسب قطع التقاطع لشبكتين.",
+ "mesh1": "شبكة 1",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "شبكة 2",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "خطوط تقاطع شبكتين المتعددة",
+ "meshMeshIntersectionPolylines": "خطوط تقاطع شبكتين المتعددة",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "يحسب خطوط التقاطع المتعددة لشبكتين.",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "وجوه الشكل إلى نقاط المضلع",
+ "shapeFacesToPolygonPoints": "وجوه الشكل إلى نقاط المضلع",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "ينشئ نقاط المضلع من وجوه الشكل",
+ "reversedPoints": "نقاط معكوسة",
+ "bitbybit.occt.shapeToMesh": "شكل إلى شبكة",
+ "shapeToMesh": "شكل إلى شبكة",
+ "bitbybit.occt.shapeToMesh_description": "ينشئ شبكة من الشكل",
+ "bitbybit.occt.shapesToMeshes": "أشكال إلى شبكات",
+ "shapesToMeshes": "أشكال إلى شبكات",
+ "bitbybit.occt.shapesToMeshes_description": "ينشئ شبكة من الشكل",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "من نقاط المضلع",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "إنشاء متعدد الطيات (Manifold) من مجموعة من نقاط المضلع التي تصف المثلثات.",
+ "traingle": "مثلث"
}
\ No newline at end of file
diff --git a/languages/bn.json b/languages/bn.json
index d3ab46f2..22c83342 100644
--- a/languages/bn.json
+++ b/languages/bn.json
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "লেবেল সহ পিন",
"pinWithLabel": "লেবেল সহ পিন",
"bitbybit.occt.dimensions.pinWithLabel_description": "লেবেল সহ পিন তৈরি করে। এটি মডেল সম্পর্কে জিনিস ব্যাখ্যা করতে বা 3D দৃশ্যে গুরুত্বপূর্ণ জিনিস চিহ্নিত করতে ব্যবহার করা যেতে পারে।",
- "offsetFromStart": "শুরু থেকে অফসেট"
+ "offsetFromStart": "শুরু থেকে অফসেট",
+ "bitbybit.vector.lengthSq": "দৈর্ঘ্য বর্গ",
+ "lengthSq": "দৈর্ঘ্য বর্গ",
+ "bitbybit.vector.lengthSq_description": "ভেক্টরের দৈর্ঘ্য বর্গ গণনা করে",
+ "bitbybit.point.twoPointsAlmostEqual": "দুটি বিন্দু সমান",
+ "twoPointsAlmostEqual": "দুটি বিন্দু সমান",
+ "bitbybit.point.twoPointsAlmostEqual_description": "দুটি বিন্দু প্রায় সমান কিনা তা পরীক্ষা করে",
+ "bitbybit.line.lineToSegment": "রেখা থেকে সেগমেন্ট",
+ "lineToSegment": "রেখা থেকে সেগমেন্ট",
+ "bitbybit.line.lineToSegment_description": "রেখাকে সেগমেন্টে রূপান্তর করুন",
+ "bitbybit.line.linesToSegments": "রেখাগুলি থেকে সেগমেন্ট",
+ "linesToSegments": "রেখাগুলি থেকে সেগমেন্ট",
+ "bitbybit.line.linesToSegments_description": "রেখাগুলিকে সেগমেন্টে রূপান্তর করে",
+ "bitbybit.line.segmentToLine": "সেগমেন্ট থেকে রেখা",
+ "segmentToLine": "সেগমেন্ট থেকে রেখা",
+ "bitbybit.line.segmentToLine_description": "সেগমেন্টকে রেখাতে রূপান্তর করে",
+ "segment": "সেগমেন্ট",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "সেগমেন্টগুলি থেকে রেখা",
+ "segmentsToLines": "সেগমেন্টগুলি থেকে রেখা",
+ "bitbybit.line.segmentsToLines_description": "সেগমেন্টগুলিকে রেখাতে রূপান্তর করে",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "সেগমেন্টগুলিকে পলিলাইনে সাজান",
+ "sortSegmentsIntoPolylines": "সেগমেন্টগুলিকে পলিলাইনে সাজান",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "সম্ভাব্যভাবে সংযুক্ত কিন্তু এলোমেলোভাবে মিশ্রিত সেগমেন্টগুলি থেকে পলিলাইন তৈরি করুন",
+ "sort": "সাজান",
+ "bitbybit.mesh.signedDistanceToPlane": "সমতলে স্বাক্ষরিত দূরত্ব",
+ "signedDistanceToPlane": "সমতলে স্বাক্ষরিত দূরত্ব",
+ "bitbybit.mesh.signedDistanceToPlane_description": "একটি বিন্দু থেকে একটি সমতলের স্বাক্ষরিত দূরত্ব গণনা করে।",
+ "plane": "সমতল",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "ত্রিভুজ সমতল গণনা করুন",
+ "calculateTrianglePlane": "ত্রিভুজ সমতল গণনা করুন",
+ "bitbybit.mesh.calculateTrianglePlane_description": "ত্রিভুজ থেকে ত্রিভুজ সমতল গণনা করে।",
+ "triangle": "ত্রিভুজ",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "ত্রিভুজ ত্রিভুজ ছেদ",
+ "triangleTriangleIntersection": "ত্রিভুজ ত্রিভুজ ছেদ",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "দুটি ত্রিভুজের ছেদ গণনা করে।",
+ "triangle1": "ত্রিভুজ ১",
+ "triangle2": "ত্রিভুজ ২",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "মেশ মেশ ছেদ সেগমেন্ট",
+ "meshMeshIntersectionSegments": "মেশ মেশ ছেদ সেগমেন্ট",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "দুটি মেশের ছেদ সেগমেন্ট গণনা করে।",
+ "mesh1": "মেশ ১",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "মেশ ২",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "মেশ মেশ ছেদ পলিলাইন",
+ "meshMeshIntersectionPolylines": "মেশ মেশ ছেদ পলিলাইন",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "দুটি মেশের ছেদ পলিলাইন গণনা করে।",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "আকৃতির মুখ থেকে বহুভুজ বিন্দু",
+ "shapeFacesToPolygonPoints": "আকৃতির মুখ থেকে বহুভুজ বিন্দু",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "আকৃতির মুখ থেকে বহুভুজ বিন্দু তৈরি করে",
+ "reversedPoints": "বিপরীত বিন্দু",
+ "bitbybit.occt.shapeToMesh": "আকৃতি থেকে মেশ",
+ "shapeToMesh": "আকৃতি থেকে মেশ",
+ "bitbybit.occt.shapeToMesh_description": "আকৃতি থেকে মেশ তৈরি করে",
+ "bitbybit.occt.shapesToMeshes": "আকৃতিগুলি থেকে মেশ",
+ "shapesToMeshes": "আকৃতিগুলি থেকে মেশ",
+ "bitbybit.occt.shapesToMeshes_description": "আকৃতি থেকে মেশ তৈরি করে",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "বহুভুজ বিন্দু থেকে",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "ত্রিভুজ বর্ণনাকারী বহুভুজ বিন্দুর একটি সেট থেকে একটি ম্যানিফোল্ড তৈরি করুন।",
+ "traingle": "ত্রিভুজ"
}
\ No newline at end of file
diff --git a/languages/de.json b/languages/de.json
index c1847b34..511b1ced 100644
--- a/languages/de.json
+++ b/languages/de.json
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "Pin mit Beschriftung",
"pinWithLabel": "Pin mit Beschriftung",
"bitbybit.occt.dimensions.pinWithLabel_description": "Erstellt einen Pin mit Beschriftung. Dieser kann verwendet werden, um Sachverhalte an den Modellen zu erklären oder wichtige Punkte in der 3D-Szene zu markieren.",
- "offsetFromStart": "Versatz vom Startpunkt"
+ "offsetFromStart": "Versatz vom Startpunkt",
+ "bitbybit.vector.lengthSq": "Länge zum Quadrat",
+ "lengthSq": "Länge zum Quadrat",
+ "bitbybit.vector.lengthSq_description": "Berechnet die quadrierte Länge des Vektors",
+ "bitbybit.point.twoPointsAlmostEqual": "zwei Punkte gleich",
+ "twoPointsAlmostEqual": "zwei Punkte gleich",
+ "bitbybit.point.twoPointsAlmostEqual_description": "Prüft, ob zwei Punkte fast gleich sind",
+ "bitbybit.line.lineToSegment": "Linie zu Segment",
+ "lineToSegment": "Linie zu Segment",
+ "bitbybit.line.lineToSegment_description": "Konvertiert die Linie in ein Segment",
+ "bitbybit.line.linesToSegments": "Linien zu Segmenten",
+ "linesToSegments": "Linien zu Segmenten",
+ "bitbybit.line.linesToSegments_description": "Konvertiert die Linien in Segmente",
+ "bitbybit.line.segmentToLine": "Segment zu Linie",
+ "segmentToLine": "Segment zu Linie",
+ "bitbybit.line.segmentToLine_description": "Konvertiert das Segment in eine Linie",
+ "segment": "Segment",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "Segmente zu Linien",
+ "segmentsToLines": "Segmente zu Linien",
+ "bitbybit.line.segmentsToLines_description": "Konvertiert die Segmente in Linien",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "Segmente zu Polylinien sortieren",
+ "sortSegmentsIntoPolylines": "Segmente zu Polylinien sortieren",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "Erstellt die Polylinien aus Segmenten, die potenziell verbunden, aber zufällig gemischt sind",
+ "sort": "sortieren",
+ "bitbybit.mesh.signedDistanceToPlane": "vorzeichenbehafteter Abstand zur Ebene",
+ "signedDistanceToPlane": "vorzeichenbehafteter Abstand zur Ebene",
+ "bitbybit.mesh.signedDistanceToPlane_description": "Berechnet den vorzeichenbehafteten Abstand von einem Punkt zu einer Ebene.",
+ "plane": "Ebene",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "Dreiecksebene berechnen",
+ "calculateTrianglePlane": "Dreiecksebene berechnen",
+ "bitbybit.mesh.calculateTrianglePlane_description": "Berechnet die Dreiecksebene aus dem Dreieck.",
+ "triangle": "Dreieck",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "Dreieck-Dreieck-Schnittpunkt",
+ "triangleTriangleIntersection": "Dreieck-Dreieck-Schnittpunkt",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "Berechnet den Schnittpunkt zweier Dreiecke.",
+ "triangle1": "Dreieck 1",
+ "triangle2": "Dreieck 2",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "Netz-Netz-Schnittsegmente",
+ "meshMeshIntersectionSegments": "Netz-Netz-Schnittsegmente",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "Berechnet die Schnittsegmente zweier Netze.",
+ "mesh1": "Netz 1",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "Netz 2",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "Netz-Netz-Schnittpolylinien",
+ "meshMeshIntersectionPolylines": "Netz-Netz-Schnittpolylinien",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "Berechnet die Schnittpolylinien zweier Netze.",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "Formflächen zu Polygonpunkten",
+ "shapeFacesToPolygonPoints": "Formflächen zu Polygonpunkten",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "Erstellt Polygonpunkte aus den Formflächen",
+ "reversedPoints": "umgekehrte Punkte",
+ "bitbybit.occt.shapeToMesh": "Form zu Netz",
+ "shapeToMesh": "Form zu Netz",
+ "bitbybit.occt.shapeToMesh_description": "Erstellt ein Netz aus der Form",
+ "bitbybit.occt.shapesToMeshes": "Formen zu Netzen",
+ "shapesToMeshes": "Formen zu Netzen",
+ "bitbybit.occt.shapesToMeshes_description": "Erstellt ein Netz aus der Form",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "aus Polygonpunkten",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "Erstellen Sie einen Manifold aus einer Reihe von Polygonpunkten, die Dreiecke beschreiben.",
+ "traingle": "Dreieck"
}
\ No newline at end of file
diff --git a/languages/en.json b/languages/en.json
index ff27b35d..243d3ef7 100644
--- a/languages/en.json
+++ b/languages/en.json
@@ -1800,7 +1800,7 @@
"bitbybit.line.getEndPoint_description": "gets line end point",
"bitbybit.line.length": "length",
"bitbybit.line.length_description": "gets line length",
- "bitbybit.line.reverse": "reverse",
+ "bitbybit.line.reverse": "reverse line",
"reverse": "reverse",
"bitbybit.line.reverse_description": "reverses line endpoints",
"bitbybit.line.transformLine": "transform line",
@@ -1809,7 +1809,7 @@
"bitbybit.line.transformsForLines": "transforms for lines",
"transformsForLines": "transforms for lines",
"bitbybit.line.transformsForLines_description": "transforms multiple lines",
- "bitbybit.line.create": "create",
+ "bitbybit.line.create": "line",
"bitbybit.line.create_description": "creates line",
"bitbybit.line.createAsync": "async",
"createAsync": "async",
@@ -1840,12 +1840,12 @@
"bitbybit.polyline.getPoints": "get points",
"getPoints": "get points",
"bitbybit.polyline.getPoints_description": "gets polyline points",
- "bitbybit.polyline.reverse": "reverse",
+ "bitbybit.polyline.reverse": "reverse polyline",
"bitbybit.polyline.reverse_description": "reverses polyline points",
"bitbybit.polyline.transformPolyline": "transform polyline",
"transformPolyline": "transform polyline",
"bitbybit.polyline.transformPolyline_description": "transforms polyline",
- "bitbybit.polyline.create": "create",
+ "bitbybit.polyline.create": "polyline",
"bitbybit.polyline.create_description": "creates polyline",
"isClosed": "is closed",
"string | number[]": "string or number array",
@@ -2251,9 +2251,9 @@
"bitbybit.occt.shapes.wire.createRectangleWire": "rectangle wire",
"createRectangleWire": "rectangle wire",
"bitbybit.occt.shapes.wire.createRectangleWire_description": "creates opencascade rectangle wire",
- "bitbybit.occt.shapes.wire.createLPolygonWire": "l polygon wire",
- "createLPolygonWire": "l polygon wire",
- "bitbybit.occt.shapes.wire.createLPolygonWire_description": "creates opencascade l polygon wire",
+ "bitbybit.occt.shapes.wire.createLPolygonWire": "L polygon wire",
+ "createLPolygonWire": "L polygon wire",
+ "bitbybit.occt.shapes.wire.createLPolygonWire_description": "creates opencascade L polygon wire",
"widthFirst": "width first",
"lengthFirst": "length first",
"widthSecond": "width second",
@@ -3152,7 +3152,7 @@
"bitbybit.things.kidsCorner.birdhouses.chirpyChalet.create_description": "creates chirpy chalet birdhouse",
"roofAngle": "roof angle",
"bitbybit.things.threeDPrinting.vases.serenitySwirl.create": "serenity swirl",
- "threeDPrinting": "three d printing",
+ "threeDPrinting": "3D printing",
"vases": "vases",
"serenitySwirl": "serenity swirl",
"bitbybit.things.threeDPrinting.vases.serenitySwirl.create_description": "creates serenity swirl vase",
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "pin with label",
"pinWithLabel": "pin with label",
"bitbybit.occt.dimensions.pinWithLabel_description": "Creates pin label. It can be used to explain things about the models or mark important things in the 3D scene.",
- "offsetFromStart": "offset from start"
+ "offsetFromStart": "offset from start",
+ "bitbybit.vector.lengthSq": "length squared",
+ "lengthSq": "length squared",
+ "bitbybit.vector.lengthSq_description": "Computes the squared length of the vector",
+ "bitbybit.point.twoPointsAlmostEqual": "two points equal",
+ "twoPointsAlmostEqual": "two points equal",
+ "bitbybit.point.twoPointsAlmostEqual_description": "Checks if two points are almost equal",
+ "bitbybit.line.lineToSegment": "line to segment",
+ "lineToSegment": "line to segment",
+ "bitbybit.line.lineToSegment_description": "Convert the line to segment",
+ "bitbybit.line.linesToSegments": "lines to segments",
+ "linesToSegments": "lines to segments",
+ "bitbybit.line.linesToSegments_description": "Converts the lines to segments",
+ "bitbybit.line.segmentToLine": "segment to line",
+ "segmentToLine": "segment to line",
+ "bitbybit.line.segmentToLine_description": "Converts the segment to line",
+ "segment": "segment",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "segments to lines",
+ "segmentsToLines": "segments to lines",
+ "bitbybit.line.segmentsToLines_description": "Converts the segments to lines",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "sort segments to polylines",
+ "sortSegmentsIntoPolylines": "sort segments to polylines",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "Create the polylines from segments that are potentially connected but scrambled randomly",
+ "sort": "sort",
+ "bitbybit.mesh.signedDistanceToPlane": "signed distance to plane",
+ "signedDistanceToPlane": "signed distance to plane",
+ "bitbybit.mesh.signedDistanceToPlane_description": "Computes the signed distance from a point to a plane.",
+ "plane": "plane",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "calculate triangle plane",
+ "calculateTrianglePlane": "calculate triangle plane",
+ "bitbybit.mesh.calculateTrianglePlane_description": "Calculates the triangle plane from triangle.",
+ "triangle": "triangle",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "triangle triangle intersection",
+ "triangleTriangleIntersection": "triangle triangle intersection",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "Calculates the intersection of two triangles.",
+ "triangle1": "triangle 1",
+ "triangle2": "triangle 2",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "mesh mesh intersection segments",
+ "meshMeshIntersectionSegments": "mesh mesh intersection segments",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "Computes the intersection segments of two meshes.",
+ "mesh1": "mesh 1",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "mesh 2",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "mesh mesh intersection polylines",
+ "meshMeshIntersectionPolylines": "mesh mesh intersection polylines",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "Computes the intersection polylines of two meshes.",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "shape faces to polygon points",
+ "shapeFacesToPolygonPoints": "shape faces to polygon points",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "Creates polygon points from the shape faces",
+ "reversedPoints": "reversed points",
+ "bitbybit.occt.shapeToMesh": "shape to mesh",
+ "shapeToMesh": "shape to mesh",
+ "bitbybit.occt.shapeToMesh_description": "Creates mesh from the shape",
+ "bitbybit.occt.shapesToMeshes": "shapes to meshes",
+ "shapesToMeshes": "shapes to meshes",
+ "bitbybit.occt.shapesToMeshes_description": "Creates mesh from the shape",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "from polygon points",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "Create a Manifold from a set of polygon points describing triangles.",
+ "traingle": "traingle"
}
\ No newline at end of file
diff --git a/languages/es.json b/languages/es.json
index c7aec429..171383c8 100644
--- a/languages/es.json
+++ b/languages/es.json
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "pin con etiqueta",
"pinWithLabel": "pin con etiqueta",
"bitbybit.occt.dimensions.pinWithLabel_description": "Crea un pin con etiqueta. Se puede usar para explicar cosas sobre los modelos o marcar elementos importantes en la escena 3D.",
- "offsetFromStart": "desplazamiento desde el inicio"
+ "offsetFromStart": "desplazamiento desde el inicio",
+ "bitbybit.vector.lengthSq": "longitud al cuadrado",
+ "lengthSq": "longitud al cuadrado",
+ "bitbybit.vector.lengthSq_description": "Calcula la longitud al cuadrado del vector",
+ "bitbybit.point.twoPointsAlmostEqual": "dos puntos iguales",
+ "twoPointsAlmostEqual": "dos puntos iguales",
+ "bitbybit.point.twoPointsAlmostEqual_description": "Comprueba si dos puntos son casi iguales",
+ "bitbybit.line.lineToSegment": "línea a segmento",
+ "lineToSegment": "línea a segmento",
+ "bitbybit.line.lineToSegment_description": "Convierte la línea a segmento",
+ "bitbybit.line.linesToSegments": "líneas a segmentos",
+ "linesToSegments": "líneas a segmentos",
+ "bitbybit.line.linesToSegments_description": "Convierte las líneas a segmentos",
+ "bitbybit.line.segmentToLine": "segmento a línea",
+ "segmentToLine": "segmento a línea",
+ "bitbybit.line.segmentToLine_description": "Convierte el segmento a línea",
+ "segment": "segmento",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "segmentos a líneas",
+ "segmentsToLines": "segmentos a líneas",
+ "bitbybit.line.segmentsToLines_description": "Convierte los segmentos a líneas",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "ordenar segmentos en polilíneas",
+ "sortSegmentsIntoPolylines": "ordenar segmentos en polilíneas",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "Crea las polilíneas a partir de segmentos que están potencialmente conectados pero mezclados aleatoriamente",
+ "sort": "ordenar",
+ "bitbybit.mesh.signedDistanceToPlane": "distancia con signo al plano",
+ "signedDistanceToPlane": "distancia con signo al plano",
+ "bitbybit.mesh.signedDistanceToPlane_description": "Calcula la distancia con signo desde un punto a un plano.",
+ "plane": "plano",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "calcular plano del triángulo",
+ "calculateTrianglePlane": "calcular plano del triángulo",
+ "bitbybit.mesh.calculateTrianglePlane_description": "Calcula el plano del triángulo a partir del triángulo.",
+ "triangle": "triángulo",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "intersección triángulo-triángulo",
+ "triangleTriangleIntersection": "intersección triángulo-triángulo",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "Calcula la intersección de dos triángulos.",
+ "triangle1": "triángulo 1",
+ "triangle2": "triángulo 2",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "segmentos de intersección malla-malla",
+ "meshMeshIntersectionSegments": "segmentos de intersección malla-malla",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "Calcula los segmentos de intersección de dos mallas.",
+ "mesh1": "malla 1",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "malla 2",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "polilíneas de intersección malla-malla",
+ "meshMeshIntersectionPolylines": "polilíneas de intersección malla-malla",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "Calcula las polilíneas de intersección de dos mallas.",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "caras de forma a puntos de polígono",
+ "shapeFacesToPolygonPoints": "caras de forma a puntos de polígono",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "Crea puntos de polígono a partir de las caras de la forma",
+ "reversedPoints": "puntos invertidos",
+ "bitbybit.occt.shapeToMesh": "forma a malla",
+ "shapeToMesh": "forma a malla",
+ "bitbybit.occt.shapeToMesh_description": "Crea una malla a partir de la forma",
+ "bitbybit.occt.shapesToMeshes": "formas a mallas",
+ "shapesToMeshes": "formas a mallas",
+ "bitbybit.occt.shapesToMeshes_description": "Crea una malla a partir de la forma",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "desde puntos de polígono",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "Crear un Manifold a partir de un conjunto de puntos de polígono que describen triángulos.",
+ "traingle": "triángulo"
}
\ No newline at end of file
diff --git a/languages/fr.json b/languages/fr.json
index c29563d5..b9e8f5cc 100644
--- a/languages/fr.json
+++ b/languages/fr.json
@@ -2251,9 +2251,9 @@
"bitbybit.occt.shapes.wire.createRectangleWire": "fil rectangle",
"createRectangleWire": "fil rectangle",
"bitbybit.occt.shapes.wire.createRectangleWire_description": "crée un fil rectangle opencascade",
- "bitbybit.occt.shapes.wire.createLPolygonWire": "fil polygone l",
- "createLPolygonWire": "fil polygone l",
- "bitbybit.occt.shapes.wire.createLPolygonWire_description": "crée un fil polygone l opencascade",
+ "bitbybit.occt.shapes.wire.createLPolygonWire": "fil polygone L",
+ "createLPolygonWire": "fil polygone L",
+ "bitbybit.occt.shapes.wire.createLPolygonWire_description": "crée un fil polygone L opencascade",
"widthFirst": "largeur première",
"lengthFirst": "longueur première",
"widthSecond": "largeur seconde",
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "repère avec étiquette",
"pinWithLabel": "repère avec étiquette",
"bitbybit.occt.dimensions.pinWithLabel_description": "Crée un repère avec étiquette. Il peut être utilisé pour expliquer des choses sur les modèles ou marquer des éléments importants dans la scène 3D.",
- "offsetFromStart": "décalage par rapport au début"
+ "offsetFromStart": "décalage par rapport au début",
+ "bitbybit.vector.lengthSq": "longueur au carré",
+ "lengthSq": "longueur au carré",
+ "bitbybit.vector.lengthSq_description": "Calcule la longueur au carré du vecteur",
+ "bitbybit.point.twoPointsAlmostEqual": "deux points égaux",
+ "twoPointsAlmostEqual": "deux points égaux",
+ "bitbybit.point.twoPointsAlmostEqual_description": "Vérifie si deux points sont presque égaux",
+ "bitbybit.line.lineToSegment": "ligne vers segment",
+ "lineToSegment": "ligne vers segment",
+ "bitbybit.line.lineToSegment_description": "Convertit la ligne en segment",
+ "bitbybit.line.linesToSegments": "lignes vers segments",
+ "linesToSegments": "lignes vers segments",
+ "bitbybit.line.linesToSegments_description": "Convertit les lignes en segments",
+ "bitbybit.line.segmentToLine": "segment vers ligne",
+ "segmentToLine": "segment vers ligne",
+ "bitbybit.line.segmentToLine_description": "Convertit le segment en ligne",
+ "segment": "segment",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "segments vers lignes",
+ "segmentsToLines": "segments vers lignes",
+ "bitbybit.line.segmentsToLines_description": "Convertit les segments en lignes",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "trier les segments en polylignes",
+ "sortSegmentsIntoPolylines": "trier les segments en polylignes",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "Crée les polylignes à partir de segments potentiellement connectés mais mélangés aléatoirement",
+ "sort": "trier",
+ "bitbybit.mesh.signedDistanceToPlane": "distance signée au plan",
+ "signedDistanceToPlane": "distance signée au plan",
+ "bitbybit.mesh.signedDistanceToPlane_description": "Calcule la distance signée d'un point à un plan.",
+ "plane": "plan",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "calculer le plan du triangle",
+ "calculateTrianglePlane": "calculer le plan du triangle",
+ "bitbybit.mesh.calculateTrianglePlane_description": "Calcule le plan du triangle à partir du triangle.",
+ "triangle": "triangle",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "intersection triangle-triangle",
+ "triangleTriangleIntersection": "intersection triangle-triangle",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "Calcule l'intersection de deux triangles.",
+ "triangle1": "triangle 1",
+ "triangle2": "triangle 2",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "segments d'intersection maillage-maillage",
+ "meshMeshIntersectionSegments": "segments d'intersection maillage-maillage",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "Calcule les segments d'intersection de deux maillages.",
+ "mesh1": "maillage 1",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "maillage 2",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "polylignes d'intersection maillage-maillage",
+ "meshMeshIntersectionPolylines": "polylignes d'intersection maillage-maillage",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "Calcule les polylignes d'intersection de deux maillages.",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "faces de forme vers points de polygone",
+ "shapeFacesToPolygonPoints": "faces de forme vers points de polygone",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "Crée des points de polygone à partir des faces de la forme",
+ "reversedPoints": "points inversés",
+ "bitbybit.occt.shapeToMesh": "forme vers maillage",
+ "shapeToMesh": "forme vers maillage",
+ "bitbybit.occt.shapeToMesh_description": "Crée un maillage à partir de la forme",
+ "bitbybit.occt.shapesToMeshes": "formes vers maillages",
+ "shapesToMeshes": "formes vers maillages",
+ "bitbybit.occt.shapesToMeshes_description": "Crée un maillage à partir de la forme",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "à partir de points de polygone",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "Créer une variété (Manifold) à partir d'un ensemble de points de polygone décrivant des triangles.",
+ "traingle": "triangle"
}
diff --git a/languages/hi.json b/languages/hi.json
index b733ffbc..947937df 100644
--- a/languages/hi.json
+++ b/languages/hi.json
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "लेबल वाला पिन",
"pinWithLabel": "लेबल वाला पिन",
"bitbybit.occt.dimensions.pinWithLabel_description": "लेबल वाला पिन बनाता है। इसका उपयोग मॉडल के बारे में चीजों को समझाने या 3D दृश्य में महत्वपूर्ण चीजों को चिह्नित करने के लिए किया जा सकता है।",
- "offsetFromStart": "शुरुआत से ऑफ़सेट"
+ "offsetFromStart": "शुरुआत से ऑफ़सेट",
+ "bitbybit.vector.lengthSq": "लंबाई वर्ग",
+ "lengthSq": "लंबाई वर्ग",
+ "bitbybit.vector.lengthSq_description": "वेक्टर की लंबाई का वर्ग परिकलित करता है",
+ "bitbybit.point.twoPointsAlmostEqual": "दो बिंदु समान",
+ "twoPointsAlmostEqual": "दो बिंदु समान",
+ "bitbybit.point.twoPointsAlmostEqual_description": "जांचता है कि क्या दो बिंदु लगभग समान हैं",
+ "bitbybit.line.lineToSegment": "रेखा से खंड",
+ "lineToSegment": "रेखा से खंड",
+ "bitbybit.line.lineToSegment_description": "रेखा को खंड में बदलें",
+ "bitbybit.line.linesToSegments": "रेखाओं से खंड",
+ "linesToSegments": "रेखाओं से खंड",
+ "bitbybit.line.linesToSegments_description": "रेखाओं को खंडों में बदलता है",
+ "bitbybit.line.segmentToLine": "खंड से रेखा",
+ "segmentToLine": "खंड से रेखा",
+ "bitbybit.line.segmentToLine_description": "खंड को रेखा में बदलता है",
+ "segment": "खंड",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "खंडों से रेखाएँ",
+ "segmentsToLines": "खंडों से रेखाएँ",
+ "bitbybit.line.segmentsToLines_description": "खंडों को रेखाओं में बदलता है",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "खंडों को पॉलीलाइन में क्रमबद्ध करें",
+ "sortSegmentsIntoPolylines": "खंडों को पॉलीलाइन में क्रमबद्ध करें",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "संभावित रूप से जुड़े हुए लेकिन यादृच्छिक रूप से बिखरे हुए खंडों से पॉलीलाइन बनाएं",
+ "sort": "क्रमबद्ध करें",
+ "bitbybit.mesh.signedDistanceToPlane": "तल से सांकेतिक दूरी",
+ "signedDistanceToPlane": "तल से सांकेतिक दूरी",
+ "bitbybit.mesh.signedDistanceToPlane_description": "एक बिंदु से एक तल तक सांकेतिक दूरी की गणना करता है।",
+ "plane": "तल",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "त्रिभुज तल की गणना करें",
+ "calculateTrianglePlane": "त्रिभुज तल की गणना करें",
+ "bitbybit.mesh.calculateTrianglePlane_description": "त्रिभुज से त्रिभुज तल की गणना करता है।",
+ "triangle": "त्रिभुज",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "त्रिभुज-त्रिभुज प्रतिच्छेदन",
+ "triangleTriangleIntersection": "त्रिभुज-त्रिभुज प्रतिच्छेदन",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "दो त्रिभुजों के प्रतिच्छेदन की गणना करता है।",
+ "triangle1": "त्रिभुज 1",
+ "triangle2": "त्रिभुज 2",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "मेश-मेश प्रतिच्छेदन खंड",
+ "meshMeshIntersectionSegments": "मेश-मेश प्रतिच्छेदन खंड",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "दो मेश के प्रतिच्छेदन खंडों की गणना करता है।",
+ "mesh1": "मेश 1",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "मेश 2",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "मेश-मेश प्रतिच्छेदन पॉलीलाइन",
+ "meshMeshIntersectionPolylines": "मेश-मेश प्रतिच्छेदन पॉलीलाइन",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "दो मेश की प्रतिच्छेदन पॉलीलाइन की गणना करता है।",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "आकृति फलक से बहुभुज बिंदु",
+ "shapeFacesToPolygonPoints": "आकृति फलक से बहुभुज बिंदु",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "आकृति फलकों से बहुभुज बिंदु बनाता है",
+ "reversedPoints": "उल्टे बिंदु",
+ "bitbybit.occt.shapeToMesh": "आकृति से मेश",
+ "shapeToMesh": "आकृति से मेश",
+ "bitbybit.occt.shapeToMesh_description": "आकृति से मेश बनाता है",
+ "bitbybit.occt.shapesToMeshes": "आकृतियों से मेश",
+ "shapesToMeshes": "आकृतियों से मेश",
+ "bitbybit.occt.shapesToMeshes_description": "आकृति से मेश बनाता है",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "बहुभुज बिंदुओं से",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "त्रिभुजों का वर्णन करने वाले बहुभुज बिंदुओं के एक सेट से एक मैनिफोल्ड (Manifold) बनाएं।",
+ "traingle": "त्रिभुज"
}
\ No newline at end of file
diff --git a/languages/id.json b/languages/id.json
index 2da8cbdf..b980c16c 100644
--- a/languages/id.json
+++ b/languages/id.json
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "pin dengan label",
"pinWithLabel": "pin dengan label",
"bitbybit.occt.dimensions.pinWithLabel_description": "Membuat pin dengan label. Ini bisa digunakan untuk menjelaskan hal-hal tentang model atau menandai bagian penting di scene 3D.",
- "offsetFromStart": "pergeseran dari awal"
+ "offsetFromStart": "pergeseran dari awal",
+ "bitbybit.vector.lengthSq": "kuadrat panjang",
+ "lengthSq": "kuadrat panjang",
+ "bitbybit.vector.lengthSq_description": "Menghitung kuadrat panjang vektor",
+ "bitbybit.point.twoPointsAlmostEqual": "dua titik sama",
+ "twoPointsAlmostEqual": "dua titik sama",
+ "bitbybit.point.twoPointsAlmostEqual_description": "Memeriksa apakah dua titik hampir sama",
+ "bitbybit.line.lineToSegment": "garis ke segmen",
+ "lineToSegment": "garis ke segmen",
+ "bitbybit.line.lineToSegment_description": "Konversi garis ke segmen",
+ "bitbybit.line.linesToSegments": "garis-garis ke segmen",
+ "linesToSegments": "garis-garis ke segmen",
+ "bitbybit.line.linesToSegments_description": "Mengonversi garis-garis ke segmen",
+ "bitbybit.line.segmentToLine": "segmen ke garis",
+ "segmentToLine": "segmen ke garis",
+ "bitbybit.line.segmentToLine_description": "Mengonversi segmen ke garis",
+ "segment": "segmen",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "segmen-segmen ke garis",
+ "segmentsToLines": "segmen-segmen ke garis",
+ "bitbybit.line.segmentsToLines_description": "Mengonversi segmen-segmen ke garis",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "urutkan segmen menjadi poliline",
+ "sortSegmentsIntoPolylines": "urutkan segmen menjadi poliline",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "Buat poliline dari segmen yang berpotensi terhubung tetapi diacak secara acak",
+ "sort": "urutkan",
+ "bitbybit.mesh.signedDistanceToPlane": "jarak bertanda ke bidang",
+ "signedDistanceToPlane": "jarak bertanda ke bidang",
+ "bitbybit.mesh.signedDistanceToPlane_description": "Menghitung jarak bertanda dari suatu titik ke bidang.",
+ "plane": "bidang",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "hitung bidang segitiga",
+ "calculateTrianglePlane": "hitung bidang segitiga",
+ "bitbybit.mesh.calculateTrianglePlane_description": "Menghitung bidang segitiga dari segitiga.",
+ "triangle": "segitiga",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "perpotongan segitiga-segitiga",
+ "triangleTriangleIntersection": "perpotongan segitiga-segitiga",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "Menghitung perpotongan dua segitiga.",
+ "triangle1": "segitiga 1",
+ "triangle2": "segitiga 2",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "segmen perpotongan mesh-mesh",
+ "meshMeshIntersectionSegments": "segmen perpotongan mesh-mesh",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "Menghitung segmen perpotongan dari dua mesh.",
+ "mesh1": "mesh 1",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "mesh 2",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "poliline perpotongan mesh-mesh",
+ "meshMeshIntersectionPolylines": "poliline perpotongan mesh-mesh",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "Menghitung poliline perpotongan dari dua mesh.",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "permukaan bentuk ke titik poligon",
+ "shapeFacesToPolygonPoints": "permukaan bentuk ke titik poligon",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "Membuat titik poligon dari permukaan bentuk",
+ "reversedPoints": "titik terbalik",
+ "bitbybit.occt.shapeToMesh": "bentuk ke mesh",
+ "shapeToMesh": "bentuk ke mesh",
+ "bitbybit.occt.shapeToMesh_description": "Membuat mesh dari bentuk",
+ "bitbybit.occt.shapesToMeshes": "bentuk-bentuk ke mesh",
+ "shapesToMeshes": "bentuk-bentuk ke mesh",
+ "bitbybit.occt.shapesToMeshes_description": "Membuat mesh dari bentuk",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "dari titik poligon",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "Buat Manifold dari sekumpulan titik poligon yang mendeskripsikan segitiga.",
+ "traingle": "segitiga"
}
\ No newline at end of file
diff --git a/languages/lt.json b/languages/lt.json
index 94aca8c6..ac03bebd 100644
--- a/languages/lt.json
+++ b/languages/lt.json
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "smeigtukas su užrašu",
"pinWithLabel": "smeigtukas su užrašu",
"bitbybit.occt.dimensions.pinWithLabel_description": "Sukuria smeigtuką su užrašu. Jį galima naudoti paaiškinti informaciją apie modelius arba pažymėti svarbius dalykus 3D scenoje.",
- "offsetFromStart": "poslinkis nuo pradžios"
+ "offsetFromStart": "poslinkis nuo pradžios",
+ "bitbybit.vector.lengthSq": "ilgis kvadratu",
+ "lengthSq": "ilgis kvadratu",
+ "bitbybit.vector.lengthSq_description": "Apskaičiuoja vektoriaus ilgį kvadratu",
+ "bitbybit.point.twoPointsAlmostEqual": "du taškai lygūs",
+ "twoPointsAlmostEqual": "du taškai lygūs",
+ "bitbybit.point.twoPointsAlmostEqual_description": "Tikrina, ar du taškai yra beveik lygūs",
+ "bitbybit.line.lineToSegment": "tiesė į atkarpą",
+ "lineToSegment": "tiesė į atkarpą",
+ "bitbybit.line.lineToSegment_description": "Konvertuoja tiesę į atkarpą",
+ "bitbybit.line.linesToSegments": "tiesės į atkarpas",
+ "linesToSegments": "tiesės į atkarpas",
+ "bitbybit.line.linesToSegments_description": "Konvertuoja tieses į atkarpas",
+ "bitbybit.line.segmentToLine": "atkarpa į tiesę",
+ "segmentToLine": "atkarpa į tiesę",
+ "bitbybit.line.segmentToLine_description": "Konvertuoja atkarpą į tiesę",
+ "segment": "atkarpa",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "atkarpos į tieses",
+ "segmentsToLines": "atkarpos į tieses",
+ "bitbybit.line.segmentsToLines_description": "Konvertuoja atkarpas į tieses",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "atkarpų rūšiavimas į polilinijas",
+ "sortSegmentsIntoPolylines": "atkarpų rūšiavimas į polilinijas",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "Sukuria polilinijas iš atkarpų, kurios potencialiai yra sujungtos, bet atsitiktinai sumaišytos",
+ "sort": "rūšiavimas",
+ "bitbybit.mesh.signedDistanceToPlane": "ženklinis atstumas iki plokštumos",
+ "signedDistanceToPlane": "ženklinis atstumas iki plokštumos",
+ "bitbybit.mesh.signedDistanceToPlane_description": "Apskaičiuoja ženklinį atstumą nuo taško iki plokštumos.",
+ "plane": "plokštuma",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "trikampio plokštumos apskaičiavimas",
+ "calculateTrianglePlane": "trikampio plokštumos apskaičiavimas",
+ "bitbybit.mesh.calculateTrianglePlane_description": "Apskaičiuoja trikampio plokštumą iš trikampio.",
+ "triangle": "trikampis",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "trikampių sankirta",
+ "triangleTriangleIntersection": "trikampių sankirta",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "Apskaičiuoja dviejų trikampių sankirtą.",
+ "triangle1": "trikampis 1",
+ "triangle2": "trikampis 2",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "tinklų sankirtos atkarpos",
+ "meshMeshIntersectionSegments": "tinklų sankirtos atkarpos",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "Apskaičiuoja dviejų tinklų sankirtos atkarpas.",
+ "mesh1": "tinklas 1",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "tinklas 2",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "tinklų sankirtos polilinijos",
+ "meshMeshIntersectionPolylines": "tinklų sankirtos polilinijos",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "Apskaičiuoja dviejų tinklų sankirtos polilinijas.",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "formos sienelės į daugiakampio taškus",
+ "shapeFacesToPolygonPoints": "formos sienelės į daugiakampio taškus",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "Sukuria daugiakampio taškus iš formos sienelių",
+ "reversedPoints": "apversti taškai",
+ "bitbybit.occt.shapeToMesh": "forma į tinklą",
+ "shapeToMesh": "forma į tinklą",
+ "bitbybit.occt.shapeToMesh_description": "Sukuria tinklą iš formos",
+ "bitbybit.occt.shapesToMeshes": "formos į tinklus",
+ "shapesToMeshes": "formos į tinklus",
+ "bitbybit.occt.shapesToMeshes_description": "Sukuria tinklą iš formos",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "iš daugiakampio taškų",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "Sukurti kolektorių iš daugiakampio taškų aibės, aprašančios trikampius.",
+ "traingle": "trikampis"
}
\ No newline at end of file
diff --git a/languages/pt.json b/languages/pt.json
index fc849ed5..d0b07c8c 100644
--- a/languages/pt.json
+++ b/languages/pt.json
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "pino com rótulo",
"pinWithLabel": "pino com rótulo",
"bitbybit.occt.dimensions.pinWithLabel_description": "Cria um pino com rótulo. Pode ser usado para explicar coisas sobre os modelos ou marcar itens importantes na cena 3D.",
- "offsetFromStart": "deslocamento do início"
+ "offsetFromStart": "deslocamento do início",
+ "bitbybit.vector.lengthSq": "comprimento ao quadrado",
+ "lengthSq": "comprimento ao quadrado",
+ "bitbybit.vector.lengthSq_description": "Calcula o comprimento ao quadrado do vetor",
+ "bitbybit.point.twoPointsAlmostEqual": "dois pontos iguais",
+ "twoPointsAlmostEqual": "dois pontos iguais",
+ "bitbybit.point.twoPointsAlmostEqual_description": "Verifica se dois pontos são quase iguais",
+ "bitbybit.line.lineToSegment": "linha para segmento",
+ "lineToSegment": "linha para segmento",
+ "bitbybit.line.lineToSegment_description": "Converte a linha para segmento",
+ "bitbybit.line.linesToSegments": "linhas para segmentos",
+ "linesToSegments": "linhas para segmentos",
+ "bitbybit.line.linesToSegments_description": "Converte as linhas para segmentos",
+ "bitbybit.line.segmentToLine": "segmento para linha",
+ "segmentToLine": "segmento para linha",
+ "bitbybit.line.segmentToLine_description": "Converte o segmento para linha",
+ "segment": "segmento",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "segmentos para linhas",
+ "segmentsToLines": "segmentos para linhas",
+ "bitbybit.line.segmentsToLines_description": "Converte os segmentos para linhas",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "ordenar segmentos em polilinhas",
+ "sortSegmentsIntoPolylines": "ordenar segmentos em polilinhas",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "Cria as polilinhas a partir de segmentos que estão potencialmente conectados, mas misturados aleatoriamente",
+ "sort": "ordenar",
+ "bitbybit.mesh.signedDistanceToPlane": "distância com sinal ao plano",
+ "signedDistanceToPlane": "distância com sinal ao plano",
+ "bitbybit.mesh.signedDistanceToPlane_description": "Calcula a distância com sinal de um ponto a um plano.",
+ "plane": "plano",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "calcular plano do triângulo",
+ "calculateTrianglePlane": "calcular plano do triângulo",
+ "bitbybit.mesh.calculateTrianglePlane_description": "Calcula o plano do triângulo a partir do triângulo.",
+ "triangle": "triângulo",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "interseção triângulo-triângulo",
+ "triangleTriangleIntersection": "interseção triângulo-triângulo",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "Calcula a interseção de dois triângulos.",
+ "triangle1": "triângulo 1",
+ "triangle2": "triângulo 2",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "segmentos de interseção malha-malha",
+ "meshMeshIntersectionSegments": "segmentos de interseção malha-malha",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "Calcula os segmentos de interseção de duas malhas.",
+ "mesh1": "malha 1",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "malha 2",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "polilinhas de interseção malha-malha",
+ "meshMeshIntersectionPolylines": "polilinhas de interseção malha-malha",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "Calcula as polilinhas de interseção de duas malhas.",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "faces da forma para pontos do polígono",
+ "shapeFacesToPolygonPoints": "faces da forma para pontos do polígono",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "Cria pontos do polígono a partir das faces da forma",
+ "reversedPoints": "pontos invertidos",
+ "bitbybit.occt.shapeToMesh": "forma para malha",
+ "shapeToMesh": "forma para malha",
+ "bitbybit.occt.shapeToMesh_description": "Cria malha a partir da forma",
+ "bitbybit.occt.shapesToMeshes": "formas para malhas",
+ "shapesToMeshes": "formas para malhas",
+ "bitbybit.occt.shapesToMeshes_description": "Cria malha a partir da forma",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "a partir de pontos do polígono",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "Cria um Manifold a partir de um conjunto de pontos do polígono que descrevem triângulos.",
+ "traingle": "triângulo"
}
\ No newline at end of file
diff --git a/languages/ru.json b/languages/ru.json
index ddc91212..e94ae36c 100644
--- a/languages/ru.json
+++ b/languages/ru.json
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "маркер с меткой",
"pinWithLabel": "маркер с меткой",
"bitbybit.occt.dimensions.pinWithLabel_description": "Создает маркер с меткой. Его можно использовать для пояснения информации о моделях или отметки важных элементов в 3D-сцене.",
- "offsetFromStart": "смещение от начала"
+ "offsetFromStart": "смещение от начала",
+ "bitbybit.vector.lengthSq": "длина в квадрате",
+ "lengthSq": "длина в квадрате",
+ "bitbybit.vector.lengthSq_description": "Вычисляет квадрат длины вектора",
+ "bitbybit.point.twoPointsAlmostEqual": "две точки равны",
+ "twoPointsAlmostEqual": "две точки равны",
+ "bitbybit.point.twoPointsAlmostEqual_description": "Проверяет, почти ли равны две точки",
+ "bitbybit.line.lineToSegment": "линия в отрезок",
+ "lineToSegment": "линия в отрезок",
+ "bitbybit.line.lineToSegment_description": "Преобразовать линию в отрезок",
+ "bitbybit.line.linesToSegments": "линии в отрезки",
+ "linesToSegments": "линии в отрезки",
+ "bitbybit.line.linesToSegments_description": "Преобразует линии в отрезки",
+ "bitbybit.line.segmentToLine": "отрезок в линию",
+ "segmentToLine": "отрезок в линию",
+ "bitbybit.line.segmentToLine_description": "Преобразует отрезок в линию",
+ "segment": "отрезок",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "отрезки в линии",
+ "segmentsToLines": "отрезки в линии",
+ "bitbybit.line.segmentsToLines_description": "Преобразует отрезки в линии",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "сортировать отрезки в полилинии",
+ "sortSegmentsIntoPolylines": "сортировать отрезки в полилинии",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "Создать полилинии из сегментов, которые потенциально связаны, но случайно перемешаны",
+ "sort": "сортировать",
+ "bitbybit.mesh.signedDistanceToPlane": "знаковое расстояние до плоскости",
+ "signedDistanceToPlane": "знаковое расстояние до плоскости",
+ "bitbybit.mesh.signedDistanceToPlane_description": "Вычисляет знаковое расстояние от точки до плоскости.",
+ "plane": "плоскость",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "вычислить плоскость треугольника",
+ "calculateTrianglePlane": "вычислить плоскость треугольника",
+ "bitbybit.mesh.calculateTrianglePlane_description": "Вычисляет плоскость треугольника из треугольника.",
+ "triangle": "треугольник",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "пересечение треугольников",
+ "triangleTriangleIntersection": "пересечение треугольников",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "Вычисляет пересечение двух треугольников.",
+ "triangle1": "треугольник 1",
+ "triangle2": "треугольник 2",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "отрезки пересечения сеток",
+ "meshMeshIntersectionSegments": "отрезки пересечения сеток",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "Вычисляет отрезки пересечения двух сеток.",
+ "mesh1": "сетка 1",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "сетка 2",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "полилинии пересечения сеток",
+ "meshMeshIntersectionPolylines": "полилинии пересечения сеток",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "Вычисляет полилинии пересечения двух сеток.",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "грани формы в точки полигона",
+ "shapeFacesToPolygonPoints": "грани формы в точки полигона",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "Создает точки полигона из граней формы",
+ "reversedPoints": "обратные точки",
+ "bitbybit.occt.shapeToMesh": "форма в сетку",
+ "shapeToMesh": "форма в сетку",
+ "bitbybit.occt.shapeToMesh_description": "Создает сетку из формы",
+ "bitbybit.occt.shapesToMeshes": "формы в сетки",
+ "shapesToMeshes": "формы в сетки",
+ "bitbybit.occt.shapesToMeshes_description": "Создает сетку из формы",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "из точек полигона",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "Создать многообразие (Manifold) из набора точек полигона, описывающих треугольники.",
+ "traingle": "треугольник"
}
\ No newline at end of file
diff --git a/languages/uk.json b/languages/uk.json
index e421de57..b1684379 100644
--- a/languages/uk.json
+++ b/languages/uk.json
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "шпилька з міткою",
"pinWithLabel": "шпилька з міткою",
"bitbybit.occt.dimensions.pinWithLabel_description": "Створює шпильку з міткою. Її можна використовувати для пояснення елементів моделей або позначення важливих об'єктів у 3D-сцені.",
- "offsetFromStart": "зсув від початку"
+ "offsetFromStart": "зсув від початку",
+ "bitbybit.vector.lengthSq": "довжина у квадраті",
+ "lengthSq": "довжина у квадраті",
+ "bitbybit.vector.lengthSq_description": "Обчислює квадрат довжини вектора",
+ "bitbybit.point.twoPointsAlmostEqual": "дві точки рівні",
+ "twoPointsAlmostEqual": "дві точки рівні",
+ "bitbybit.point.twoPointsAlmostEqual_description": "Перевіряє, чи дві точки майже рівні",
+ "bitbybit.line.lineToSegment": "лінія у відрізок",
+ "lineToSegment": "лінія у відрізок",
+ "bitbybit.line.lineToSegment_description": "Перетворити лінію на відрізок",
+ "bitbybit.line.linesToSegments": "лінії у відрізки",
+ "linesToSegments": "лінії у відрізки",
+ "bitbybit.line.linesToSegments_description": "Перетворює лінії на відрізки",
+ "bitbybit.line.segmentToLine": "відрізок у лінію",
+ "segmentToLine": "відрізок у лінію",
+ "bitbybit.line.segmentToLine_description": "Перетворює відрізок на лінію",
+ "segment": "відрізок",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "відрізки у лінії",
+ "segmentsToLines": "відрізки у лінії",
+ "bitbybit.line.segmentsToLines_description": "Перетворює відрізки на лінії",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "сортувати відрізки в полілінії",
+ "sortSegmentsIntoPolylines": "сортувати відрізки в полілінії",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "Створити полілінії з відрізків, які потенційно з'єднані, але перемішані випадковим чином",
+ "sort": "сортувати",
+ "bitbybit.mesh.signedDistanceToPlane": "знакова відстань до площини",
+ "signedDistanceToPlane": "знакова відстань до площини",
+ "bitbybit.mesh.signedDistanceToPlane_description": "Обчислює знакову відстань від точки до площини.",
+ "plane": "площина",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "обчислити площину трикутника",
+ "calculateTrianglePlane": "обчислити площину трикутника",
+ "bitbybit.mesh.calculateTrianglePlane_description": "Обчислює площину трикутника з трикутника.",
+ "triangle": "трикутник",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "перетин трикутників",
+ "triangleTriangleIntersection": "перетин трикутників",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "Обчислює перетин двох трикутників.",
+ "triangle1": "трикутник 1",
+ "triangle2": "трикутник 2",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "відрізки перетину сіток",
+ "meshMeshIntersectionSegments": "відрізки перетину сіток",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "Обчислює відрізки перетину двох сіток.",
+ "mesh1": "сітка 1",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "сітка 2",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "полілінії перетину сіток",
+ "meshMeshIntersectionPolylines": "полілінії перетину сіток",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "Обчислює полілінії перетину двох сіток.",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "грані форми в точки полігону",
+ "shapeFacesToPolygonPoints": "грані форми в точки полігону",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "Створює точки полігону з граней форми",
+ "reversedPoints": "зворотні точки",
+ "bitbybit.occt.shapeToMesh": "форма в сітку",
+ "shapeToMesh": "форма в сітку",
+ "bitbybit.occt.shapeToMesh_description": "Створює сітку з форми",
+ "bitbybit.occt.shapesToMeshes": "форми в сітки",
+ "shapesToMeshes": "форми в сітки",
+ "bitbybit.occt.shapesToMeshes_description": "Створює сітку з форми",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "з точок полігону",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "Створити многовид (Manifold) із набору точок полігону, що описують трикутники.",
+ "traingle": "трикутник"
}
\ No newline at end of file
diff --git a/languages/zh-hans.json b/languages/zh-hans.json
index b7b221fa..d2c9d0ab 100644
--- a/languages/zh-hans.json
+++ b/languages/zh-hans.json
@@ -5130,5 +5130,67 @@
"bitbybit.occt.dimensions.pinWithLabel": "带标签的图钉",
"pinWithLabel": "带标签的图钉",
"bitbybit.occt.dimensions.pinWithLabel_description": "创建带标签的图钉。可用于解释模型相关信息或在 3D 场景中标记重要内容。",
- "offsetFromStart": "距起点偏移"
+ "offsetFromStart": "距起点偏移",
+ "bitbybit.vector.lengthSq": "长度平方",
+ "lengthSq": "长度平方",
+ "bitbybit.vector.lengthSq_description": "计算向量的长度平方",
+ "bitbybit.point.twoPointsAlmostEqual": "两点相等",
+ "twoPointsAlmostEqual": "两点相等",
+ "bitbybit.point.twoPointsAlmostEqual_description": "检查两个点是否几乎相等",
+ "bitbybit.line.lineToSegment": "直线到线段",
+ "lineToSegment": "直线到线段",
+ "bitbybit.line.lineToSegment_description": "将直线转换为线段",
+ "bitbybit.line.linesToSegments": "多条直线到线段",
+ "linesToSegments": "多条直线到线段",
+ "bitbybit.line.linesToSegments_description": "将多条直线转换为线段",
+ "bitbybit.line.segmentToLine": "线段到直线",
+ "segmentToLine": "线段到直线",
+ "bitbybit.line.segmentToLine_description": "将线段转换为直线",
+ "segment": "线段",
+ "Base.Segment3": "Base.Segment3",
+ "bitbybit.line.segmentsToLines": "多条线段到直线",
+ "segmentsToLines": "多条线段到直线",
+ "bitbybit.line.segmentsToLines_description": "将多条线段转换为直线",
+ "Base.Segment3[]": "Base.Segment3[]",
+ "bitbybit.polyline.sortSegmentsIntoPolylines": "将线段排序为多段线",
+ "sortSegmentsIntoPolylines": "将线段排序为多段线",
+ "bitbybit.polyline.sortSegmentsIntoPolylines_description": "从可能连接但随机打乱的线段创建多段线",
+ "sort": "排序",
+ "bitbybit.mesh.signedDistanceToPlane": "到平面的有符号距离",
+ "signedDistanceToPlane": "到平面的有符号距离",
+ "bitbybit.mesh.signedDistanceToPlane_description": "计算点到平面的有符号距离。",
+ "plane": "平面",
+ "Base.TrianglePlane3": "Base.TrianglePlane3",
+ "bitbybit.mesh.calculateTrianglePlane": "计算三角形平面",
+ "calculateTrianglePlane": "计算三角形平面",
+ "bitbybit.mesh.calculateTrianglePlane_description": "从三角形计算其所在的平面。",
+ "triangle": "三角形",
+ "Base.Triangle3": "Base.Triangle3",
+ "bitbybit.mesh.triangleTriangleIntersection": "三角形与三角形相交",
+ "triangleTriangleIntersection": "三角形与三角形相交",
+ "bitbybit.mesh.triangleTriangleIntersection_description": "计算两个三角形的交集。",
+ "triangle1": "三角形 1",
+ "triangle2": "三角形 2",
+ "bitbybit.mesh.meshMeshIntersectionSegments": "网格与网格相交线段",
+ "meshMeshIntersectionSegments": "网格与网格相交线段",
+ "bitbybit.mesh.meshMeshIntersectionSegments_description": "计算两个网格的相交线段。",
+ "mesh1": "网格 1",
+ "Base.Mesh3": "Base.Mesh3",
+ "mesh2": "网格 2",
+ "bitbybit.mesh.meshMeshIntersectionPolylines": "网格与网格相交多段线",
+ "meshMeshIntersectionPolylines": "网格与网格相交多段线",
+ "bitbybit.mesh.meshMeshIntersectionPolylines_description": "计算两个网格的相交多段线。",
+ "bitbybit.occt.shapeFacesToPolygonPoints": "形状面转换为多边形点",
+ "shapeFacesToPolygonPoints": "形状面转换为多边形点",
+ "bitbybit.occt.shapeFacesToPolygonPoints_description": "从形状面创建多边形点",
+ "reversedPoints": "反转点",
+ "bitbybit.occt.shapeToMesh": "形状到网格",
+ "shapeToMesh": "形状到网格",
+ "bitbybit.occt.shapeToMesh_description": "从形状创建网格",
+ "bitbybit.occt.shapesToMeshes": "多个形状到网格",
+ "shapesToMeshes": "多个形状到网格",
+ "bitbybit.occt.shapesToMeshes_description": "从形状创建网格",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints": "从多边形点",
+ "bitbybit.manifold.manifold.shapes.fromPolygonPoints_description": "从描述三角形的一组多边形点创建流形(Manifold)。",
+ "traingle": "三角形"
}
\ No newline at end of file
diff --git a/packages/dev/babylonjs/LICENSE b/packages/dev/babylonjs/LICENSE
index d3f3c206..87a328f2 100644
--- a/packages/dev/babylonjs/LICENSE
+++ b/packages/dev/babylonjs/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c)2025 Bit By Bit Developers
+Copyright (c) 2025 Bit By Bit Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/dev/babylonjs/README.md b/packages/dev/babylonjs/README.md
index f3cce9cd..0afdb132 100644
--- a/packages/dev/babylonjs/README.md
+++ b/packages/dev/babylonjs/README.md
@@ -1,14 +1,14 @@
## Bit By Bit Developers library for BABYLONJS game engine
+This project exposes core 3D algorithms of Bitbybit platform through BABYLONJS game engine. Code is open-sourced under MIT license. This library was previously intertwined in core package and is now separated.
+
+
+
Visit [bitbybit.dev](https://bitbybit.dev) to use our full cloud platform.
Best way to support us - [Silver or Gold plan subscription](https://bitbybit.dev/auth/pick-plan)
Buy unique products from our [Crafts shop](https://crafts.bitbybit.dev) all designed with Bitbybit algorithms
Check out [3D Bits app for Shopify](https://apps.shopify.com/3d-bits-1) also used in our Crafts shop
-
-
-This project exposes 3D algorithms of Bit By Bit Developers platform through BABYLONJS game engine. Code is open-sourced under MIT license. This library was previously intertwined in core package and is now separated.
-
## Github
https://github.com/bitbybit-dev/bitbybit
## NPM
diff --git a/packages/dev/babylonjs/lib/api/bitbybit-base.ts b/packages/dev/babylonjs/lib/api/bitbybit-base.ts
index ea7b7864..f8e8046a 100644
--- a/packages/dev/babylonjs/lib/api/bitbybit-base.ts
+++ b/packages/dev/babylonjs/lib/api/bitbybit-base.ts
@@ -2,8 +2,6 @@ import { OCCT as BaseOCCT, OCCTWorkerManager } from "@bitbybit-dev/occt-worker";
import { JSONPath } from "jsonpath-plus";
import { Babylon } from "./bitbybit/babylon/babylon";
import {
- Line,
- Polyline,
Verb,
Tag,
Time,
@@ -14,6 +12,8 @@ import {
import {
Vector,
Point,
+ Line,
+ Polyline,
TextBitByBit,
Color,
MathBitByBit,
@@ -22,6 +22,7 @@ import {
Logic,
Transforms,
Dates,
+ MeshBitByBit,
} from "@bitbybit-dev/base";
import {
JSCAD
@@ -60,6 +61,7 @@ export class BitByBitBase {
public dates: Dates;
public tag: Tag;
public time: Time;
+ public mesh: MeshBitByBit;
public occt: OCCTW & BaseOCCT;
public asset: Asset;
public color: Color;
@@ -85,10 +87,10 @@ export class BitByBitBase {
this.context);
this.color = new Color(this.math);
- this.line = new Line(this.context, geometryHelper);
this.transforms = new Transforms(this.vector, this.math);
this.point = new Point(geometryHelper, this.transforms, this.vector);
- this.polyline = new Polyline(this.context, geometryHelper);
+ this.line = new Line(this.point, geometryHelper);
+ this.polyline = new Polyline(this.vector, this.point, geometryHelper);
this.verb = new Verb(this.context, geometryHelper, this.math);
this.time = new Time(this.context);
this.occt = new OCCTW(this.context, this.occtWorkerManager);
@@ -98,6 +100,7 @@ export class BitByBitBase {
this.text = new TextBitByBit(this.point);
this.dates = new Dates();
this.lists = new Lists();
+ this.mesh = new MeshBitByBit(this.vector, this.polyline);
}
init(scene: BABYLON.Scene, occt?: Worker, jscad?: Worker, manifold?: Worker, havokPlugin?: BABYLON.HavokPlugin) {
@@ -114,7 +117,7 @@ export class BitByBitBase {
if (jscad) {
this.jscadWorkerManager.setJscadWorker(jscad);
}
- if(manifold){
+ if (manifold) {
this.manifoldWorkerManager.setManifoldWorker(manifold);
}
}
diff --git a/packages/dev/babylonjs/lib/api/inputs/base-inputs.ts b/packages/dev/babylonjs/lib/api/inputs/base-inputs.ts
index af8a7d05..dcce866f 100644
--- a/packages/dev/babylonjs/lib/api/inputs/base-inputs.ts
+++ b/packages/dev/babylonjs/lib/api/inputs/base-inputs.ts
@@ -22,6 +22,12 @@ export namespace Base {
export type Vector3 = [number, number, number];
export type Axis3 = {origin: Base.Point3, direction: Base.Vector3};
export type Axis2 = {origin: Base.Point2, direction: Base.Vector2};
+ export type Segment2 = [Point2, Point2];
+ export type Segment3 = [Point3, Point3];
+ // Triangle plane is efficient defininition described by a normal vector and d value (N dot X = d)
+ export type TrianglePlane3 = { normal: Vector3; d: number; }
+ export type Triangle3 = [Base.Point3, Base.Point3, Base.Point3];
+ export type Mesh3 = Triangle3[];
export type Plane3 = { origin: Base.Point3, normal: Base.Vector3, direction: Base.Vector3 };
export type BoundingBox = { min: Base.Point3, max: Base.Point3, center?: Base.Point3, width?: number, height?: number, length?: number };
export type Line2 = { start: Base.Point2, end: Base.Point2 };
diff --git a/packages/dev/base/LICENSE b/packages/dev/base/LICENSE
index d3f3c206..87a328f2 100644
--- a/packages/dev/base/LICENSE
+++ b/packages/dev/base/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c)2025 Bit By Bit Developers
+Copyright (c) 2025 Bit By Bit Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/dev/base/lib/api/inputs/base-inputs.ts b/packages/dev/base/lib/api/inputs/base-inputs.ts
index aeda5802..02cbc7cf 100644
--- a/packages/dev/base/lib/api/inputs/base-inputs.ts
+++ b/packages/dev/base/lib/api/inputs/base-inputs.ts
@@ -7,8 +7,14 @@ export namespace Base {
export type Vector2 = [number, number];
export type Point3 = [number, number, number];
export type Vector3 = [number, number, number];
- export type Axis3 = {origin: Base.Point3, direction: Base.Vector3};
- export type Axis2 = {origin: Base.Point2, direction: Base.Vector2};
+ export type Axis3 = { origin: Base.Point3, direction: Base.Vector3 };
+ export type Axis2 = { origin: Base.Point2, direction: Base.Vector2 };
+ export type Segment2 = [Point2, Point2];
+ export type Segment3 = [Point3, Point3];
+ // Triangle plane is efficient defininition described by a normal vector and d value (N dot X = d)
+ export type TrianglePlane3 = { normal: Vector3; d: number; }
+ export type Triangle3 = [Base.Point3, Base.Point3, Base.Point3];
+ export type Mesh3 = Triangle3[];
export type Plane3 = { origin: Base.Point3, normal: Base.Vector3, direction: Base.Vector3 };
export type BoundingBox = { min: Base.Point3, max: Base.Point3, center?: Base.Point3, width?: number, height?: number, length?: number };
export type Line2 = { start: Base.Point2, end: Base.Point2 };
diff --git a/packages/dev/base/lib/api/inputs/index.ts b/packages/dev/base/lib/api/inputs/index.ts
index 6296629d..0618c99f 100644
--- a/packages/dev/base/lib/api/inputs/index.ts
+++ b/packages/dev/base/lib/api/inputs/index.ts
@@ -8,3 +8,6 @@ export * from "./vector-inputs";
export * from "./transforms-inputs";
export * from "./base-inputs";
export * from "./dates-inputs";
+export * from "./line-inputs";
+export * from "./polyline-inputs";
+export * from "./mesh-inputs";
diff --git a/packages/dev/base/lib/api/inputs/inputs.ts b/packages/dev/base/lib/api/inputs/inputs.ts
index e0dc78b1..e8af5496 100644
--- a/packages/dev/base/lib/api/inputs/inputs.ts
+++ b/packages/dev/base/lib/api/inputs/inputs.ts
@@ -8,3 +8,7 @@ export * from "./text-inputs";
export * from "./vector-inputs";
export * from "./transforms-inputs";
export * from "./dates-inputs";
+export * from "./line-inputs";
+export * from "./polyline-inputs";
+export * from "./mesh-inputs";
+
diff --git a/packages/dev/core/lib/api/inputs/line-inputs.ts b/packages/dev/base/lib/api/inputs/line-inputs.ts
similarity index 69%
rename from packages/dev/core/lib/api/inputs/line-inputs.ts
rename to packages/dev/base/lib/api/inputs/line-inputs.ts
index 238cdcfb..f82a0a2f 100644
--- a/packages/dev/core/lib/api/inputs/line-inputs.ts
+++ b/packages/dev/base/lib/api/inputs/line-inputs.ts
@@ -13,12 +13,14 @@ export namespace Line {
}
/**
* Start point
+ * @default undefined
*/
- start: Base.Point3;
+ start?: Base.Point3;
/**
* End point
+ * @default undefined
*/
- end: Base.Point3;
+ end?: Base.Point3;
}
export class LineStartEndPointsDto {
/**
@@ -30,10 +32,12 @@ export namespace Line {
}
/**
* Start points
+ * @default undefined
*/
startPoints: Base.Point3[];
/**
* End points
+ * @default undefined
*/
endPoints: Base.Point3[];
}
@@ -51,26 +55,38 @@ export namespace Line {
}
/**
* Line
+ * @default undefined
*/
- line: LinePointsDto;
+ line?: LinePointsDto;
/**
* Value between 0 and 1
+ * @default 1
+ * @minimum 0
+ * @maximum 1
+ * @step 0.1
*/
- opacity = 1;
+ opacity? = 1;
/**
* Hex colour string
+ * @default #444444
*/
- colours: string | string[] = "#444444";
+ colours?: string | string[] = "#444444";
/**
* Width of the line
+ * @default 3
+ * @minimum 0
+ * @maximum Infinity
+ * @step 0.1
*/
- size = 3;
+ size? = 3;
/**
* Indicates wether the position of this line will change in time
+ * @default false
*/
- updatable = false;
+ updatable? = false;
/**
* Line mesh variable in case it already exists and needs updating
+ * @default undefined
*/
lineMesh?: T;
}
@@ -89,26 +105,38 @@ export namespace Line {
}
/**
* Lines
+ * @default undefined
*/
- lines: LinePointsDto[];
+ lines?: LinePointsDto[];
/**
* Value between 0 and 1
+ * @default 1
+ * @minimum 0
+ * @maximum 1
+ * @step 0.1
*/
- opacity = 1;
+ opacity? = 1;
/**
* Hex colour string
+ * @default #444444
*/
- colours: string | string[] = "#444444";
+ colours?: string | string[] = "#444444";
/**
* Width of the line
+ * @default 3
+ * @minimum 0
+ * @maximum Infinity
+ * @step 0.1
*/
- size = 3;
+ size? = 3;
/**
* Indicates wether the position of these lines will change in time
+ * @default false
*/
- updatable = false;
+ updatable? = false;
/**
* Line mesh variable in case it already exists and needs updating
+ * @default undefined
*/
linesMesh?: T;
}
@@ -118,8 +146,9 @@ export namespace Line {
}
/**
* Points
+ * @default undefined
*/
- points: Base.Point3[];
+ points?: Base.Point3[];
}
export class LineDto {
constructor(line?: LinePointsDto) {
@@ -127,8 +156,29 @@ export namespace Line {
}
/**
* Line to convert
+ * @default undefined
*/
- line: LinePointsDto;
+ line?: LinePointsDto;
+ }
+ export class SegmentDto {
+ constructor(segment?: Base.Segment3) {
+ if (segment !== undefined) { this.segment = segment; }
+ }
+ /**
+ * Segment
+ * @default undefined
+ */
+ segment?: Base.Segment3;
+ }
+ export class SegmentsDto {
+ constructor(segments?: Base.Segment3[]) {
+ if (segments !== undefined) { this.segments = segments; }
+ }
+ /**
+ * Segments
+ * @default undefined
+ */
+ segments?: Base.Segment3[];
}
export class LinesDto {
constructor(lines?: LinePointsDto[]) {
@@ -136,8 +186,9 @@ export namespace Line {
}
/**
* Lines to convert
+ * @default undefined
*/
- lines: LinePointsDto[];
+ lines?: LinePointsDto[];
}
export class PointOnLineDto {
constructor(line?: LinePointsDto, param?: number) {
@@ -146,12 +197,17 @@ export namespace Line {
}
/**
* Line to get point on
+ * @default undefined
*/
- line: LinePointsDto;
+ line?: LinePointsDto;
/**
* Param to use for point on line
+ * @default 0.5
+ * @minimum -Infinity
+ * @maximum Infinity
+ * @step 0.1
*/
- param: number;
+ param? = 0.5;
}
export class TransformLineDto {
constructor(line?: LinePointsDto, transformation?: Base.TransformMatrixes) {
@@ -160,12 +216,14 @@ export namespace Line {
}
/**
* Line to transform
+ * @default undefined
*/
- line: LinePointsDto;
+ line?: LinePointsDto;
/**
* Transformation matrix or a list of transformation matrixes
+ * @default undefined
*/
- transformation: Base.TransformMatrixes;
+ transformation?: Base.TransformMatrixes;
}
export class TransformsLinesDto {
constructor(lines?: LinePointsDto[], transformation?: Base.TransformMatrixes[]) {
@@ -174,12 +232,14 @@ export namespace Line {
}
/**
* Lines to transform
+ * @default undefined
*/
- lines: LinePointsDto[];
+ lines?: LinePointsDto[];
/**
* Transformations matrix or a list of transformations matrixes
+ * @default undefined
*/
- transformation: Base.TransformMatrixes[];
+ transformation?: Base.TransformMatrixes[];
}
export class TransformLinesDto {
constructor(lines?: LinePointsDto[], transformation?: Base.TransformMatrixes) {
@@ -188,11 +248,13 @@ export namespace Line {
}
/**
* Lines to transform
+ * @default undefined
*/
- lines: LinePointsDto[];
+ lines?: LinePointsDto[];
/**
* Transformation matrix or a list of transformation matrixes
+ * @default undefined
*/
- transformation: Base.TransformMatrixes;
+ transformation?: Base.TransformMatrixes;
}
}
diff --git a/packages/dev/base/lib/api/inputs/mesh-inputs.ts b/packages/dev/base/lib/api/inputs/mesh-inputs.ts
new file mode 100644
index 00000000..f8b66929
--- /dev/null
+++ b/packages/dev/base/lib/api/inputs/mesh-inputs.ts
@@ -0,0 +1,103 @@
+/* eslint-disable @typescript-eslint/no-namespace */
+import { Base } from "./base-inputs";
+
+
+// tslint:disable-next-line: no-namespace
+export namespace Mesh {
+ export class SignedDistanceFromPlaneToPointDto {
+ constructor(point?: Base.Point3, plane?: Base.TrianglePlane3) {
+ if (point !== undefined) { this.point = point; }
+ if (plane !== undefined) { this.plane = plane; }
+ }
+ /**
+ * Point from which to find the distance
+ * @default undefined
+ */
+ point?: Base.Point3;
+ /**
+ * Triangle plane to which the distance is calculated
+ * @default undefined
+ */
+ plane?: Base.TrianglePlane3;
+ }
+
+ export class TriangleDto {
+ constructor(triangle?: Base.Triangle3) {
+ if (triangle !== undefined) { this.triangle = triangle; }
+ }
+ /**
+ * Triangle to be used
+ * @default undefined
+ */
+ triangle?: Base.Triangle3;
+ }
+ export class TriangleToleranceDto {
+ constructor(triangle?: Base.Triangle3) {
+ if (triangle !== undefined) { this.triangle = triangle; }
+ }
+ /**
+ * Triangle to be used
+ * @default undefined
+ */
+ triangle?: Base.Triangle3;
+ /**
+ * Tolerance for the calculation
+ * @default 1e-7
+ * @minimum -Infinity
+ * @maximum Infinity
+ * @step 1e-7
+ */
+ tolerance? = 1e-7;
+ }
+
+ export class TriangleTriangleToleranceDto {
+ constructor(triangle1?: Base.Triangle3, triangle2?: Base.Triangle3, tolerance?: number) {
+ if (triangle1 !== undefined) { this.triangle1 = triangle1; }
+ if (triangle2 !== undefined) { this.triangle2 = triangle2; }
+ if (tolerance !== undefined) { this.tolerance = tolerance; }
+ }
+ /**
+ * First triangle
+ * @default undefined
+ */
+ triangle1?: Base.Triangle3;
+ /**
+ * Second triangle
+ * @default undefined
+ */
+ triangle2?: Base.Triangle3;
+ /**
+ * Tolerance for the calculation
+ * @default 1e-7
+ * @minimum -Infinity
+ * @maximum Infinity
+ * @step 1e-7
+ */
+ tolerance? = 1e-7;
+ }
+ export class MeshMeshToleranceDto {
+ constructor(mesh1?: Base.Mesh3, mesh2?: Base.Mesh3, tolerance?: number) {
+ if (mesh1 !== undefined) { this.mesh1 = mesh1; }
+ if (mesh2 !== undefined) { this.mesh2 = mesh2; }
+ if (tolerance !== undefined) { this.tolerance = tolerance; }
+ }
+ /**
+ * First mesh
+ * @default undefined
+ */
+ mesh1?: Base.Mesh3;
+ /**
+ * Second mesh
+ * @default undefined
+ */
+ mesh2?: Base.Mesh3;
+ /**
+ * Tolerance for the calculation
+ * @default 1e-7
+ * @minimum -Infinity
+ * @maximum Infinity
+ * @step 1e-7
+ */
+ tolerance? = 1e-7;
+ }
+}
diff --git a/packages/dev/base/lib/api/inputs/point-inputs.ts b/packages/dev/base/lib/api/inputs/point-inputs.ts
index 69987dff..3d934e5d 100644
--- a/packages/dev/base/lib/api/inputs/point-inputs.ts
+++ b/packages/dev/base/lib/api/inputs/point-inputs.ts
@@ -419,6 +419,31 @@ export namespace Point {
*/
point: Base.Point3;
}
+ export class TwoPointsToleranceDto {
+ constructor(point1?: Base.Point3, point2?: Base.Point3, tolerance?: number) {
+ if (point1 !== undefined) { this.point1 = point1; }
+ if (point2 !== undefined) { this.point2 = point2; }
+ if (tolerance !== undefined) { this.tolerance = tolerance; }
+ }
+ /**
+ * First point to compare
+ * @default undefined
+ */
+ point1?: Base.Point3;
+ /**
+ * Second point to compare
+ * @default undefined
+ */
+ point2?: Base.Point3;
+ /**
+ * Tolerance for the calculation
+ * @default 1e-7
+ * @minimum -Infinity
+ * @maximum Infinity
+ * @step 1e-7
+ */
+ tolerance? = 1e-7;
+ }
export class StartEndPointsDto {
constructor(startPoint?: Base.Point3, endPoint?: Base.Point3) {
if (startPoint !== undefined) { this.startPoint = startPoint; }
diff --git a/packages/dev/core/lib/api/inputs/polyline-inputs.ts b/packages/dev/base/lib/api/inputs/polyline-inputs.ts
similarity index 61%
rename from packages/dev/core/lib/api/inputs/polyline-inputs.ts
rename to packages/dev/base/lib/api/inputs/polyline-inputs.ts
index 29dd8332..c4c58221 100644
--- a/packages/dev/core/lib/api/inputs/polyline-inputs.ts
+++ b/packages/dev/base/lib/api/inputs/polyline-inputs.ts
@@ -2,6 +2,25 @@
import { Base } from "./base-inputs";
export namespace Polyline {
+ export class PolylineCreateDto {
+ /**
+ * Provide options without default values
+ */
+ constructor(points?: Base.Point3[], isClosed?: boolean) {
+ if (points !== undefined) { this.points = points; }
+ if (isClosed !== undefined) { this.isClosed = isClosed; }
+ }
+ /**
+ * Points of the polyline
+ * @default undefined
+ */
+ points?: Base.Point3[];
+ /**
+ * Can contain is closed information
+ * @default false
+ */
+ isClosed? = false;
+ }
export class PolylinePropertiesDto {
/**
* Provide options without default values
@@ -12,14 +31,17 @@ export namespace Polyline {
}
/**
* Points of the polyline
+ * @default undefined
*/
- points: Base.Point3[];
+ points?: Base.Point3[];
/**
* Can contain is closed information
+ * @default false
*/
isClosed? = false;
/**
- * Can contain color information
+ * Optional polyline color
+ * @default #444444
*/
color?: string | number[];
}
@@ -29,8 +51,9 @@ export namespace Polyline {
}
/**
* Polyline with points
+ * @default undefined
*/
- polyline: PolylinePropertiesDto;
+ polyline?: PolylinePropertiesDto;
}
export class PolylinesDto {
constructor(polylines?: PolylinePropertiesDto[]) {
@@ -38,8 +61,9 @@ export namespace Polyline {
}
/**
* Polylines array
+ * @default undefined
*/
- polylines: PolylinePropertiesDto[];
+ polylines?: PolylinePropertiesDto[];
}
export class TransformPolylineDto {
constructor(polyline?: PolylinePropertiesDto, transformation?: Base.TransformMatrixes) {
@@ -48,12 +72,14 @@ export namespace Polyline {
}
/**
* Polyline to transform
+ * @default undefined
*/
- polyline: PolylinePropertiesDto;
+ polyline?: PolylinePropertiesDto;
/**
* Transformation matrix or a list of transformation matrixes
+ * @default undefined
*/
- transformation: Base.TransformMatrixes;
+ transformation?: Base.TransformMatrixes;
}
export class DrawPolylineDto {
/**
@@ -69,26 +95,38 @@ export namespace Polyline {
}
/**
* Polyline
+ * @default undefined
*/
- polyline: PolylinePropertiesDto;
+ polyline?: PolylinePropertiesDto;
/**
* Value between 0 and 1
+ * @default 1
+ * @minimum 0
+ * @maximum 1
+ * @step 0.1
*/
- opacity = 1;
+ opacity? = 1;
/**
* Hex colour string
+ * @default #444444
*/
- colours: string | string[] = "#444444";
+ colours?: string | string[] = "#444444";
/**
* Width of the polyline
+ * @default 3
+ * @minimum 0
+ * @maximum Infinity
+ * @step 0.1
*/
- size = 3;
+ size? = 3;
/**
* Indicates wether the position of this polyline will change in time
+ * @default false
*/
- updatable = false;
+ updatable? = false;
/**
* Line mesh variable in case it already exists and needs updating
+ * @default undefined
*/
polylineMesh?: T;
}
@@ -106,27 +144,57 @@ export namespace Polyline {
}
/**
* Polylines
+ * @default undefined
*/
- polylines: PolylinePropertiesDto[];
+ polylines?: PolylinePropertiesDto[];
/**
* Value between 0 and 1
+ * @default 1
+ * @minimum 0
+ * @maximum 1
+ * @step 0.1
*/
- opacity = 1;
+ opacity? = 1;
/**
* Hex colour string
+ * @default #444444
*/
- colours: string | string[] = "#444444";
+ colours?: string | string[] = "#444444";
/**
* Width of the polyline
+ * @default 3
+ * @minimum 0
+ * @maximum Infinity
+ * @step 0.1
*/
- size = 3;
+ size? = 3;
/**
* Indicates wether the position of this polyline will change in time
+ * @default false
*/
- updatable = false;
+ updatable? = false;
/**
* Polyline mesh variable in case it already exists and needs updating
+ * @default undefined
*/
polylinesMesh?: T;
}
+ export class SegmentsToleranceDto {
+ constructor(segments?: Base.Segment3[]) {
+ if (segments !== undefined) { this.segments = segments; }
+ }
+ /**
+ * Segments array
+ * @default undefined
+ */
+ segments?: Base.Segment3[];
+ /**
+ * Tolerance for the calculation
+ * @default 1e-5
+ * @minimum -Infinity
+ * @maximum Infinity
+ * @step 1e-5
+ */
+ tolerance? = 1e-5;
+ }
}
diff --git a/packages/dev/base/lib/api/inputs/text-inputs.ts b/packages/dev/base/lib/api/inputs/text-inputs.ts
index 6f35f40d..515d32c6 100644
--- a/packages/dev/base/lib/api/inputs/text-inputs.ts
+++ b/packages/dev/base/lib/api/inputs/text-inputs.ts
@@ -227,7 +227,7 @@ export namespace Text {
* Will center text on 0, 0, 0
* @default false
*/
- centerOnOrigin = false;
+ centerOnOrigin? = false;
}
}
diff --git a/packages/dev/base/lib/api/inputs/vector-inputs.ts b/packages/dev/base/lib/api/inputs/vector-inputs.ts
index bf2d500c..8207783d 100644
--- a/packages/dev/base/lib/api/inputs/vector-inputs.ts
+++ b/packages/dev/base/lib/api/inputs/vector-inputs.ts
@@ -106,6 +106,16 @@ export namespace Vector {
*/
vector: number[];
}
+ export class Vector3Dto {
+ constructor(vector?: Base.Vector3) {
+ if (vector !== undefined) { this.vector = vector; }
+ }
+ /**
+ * Vector array of 3 numbers
+ * @default undefined
+ */
+ vector: Base.Vector3;
+ }
export class RangeMaxDto {
constructor(max?: number) {
if (max !== undefined) { this.max = max; }
diff --git a/packages/dev/base/lib/api/services/dates.test.ts b/packages/dev/base/lib/api/services/dates.test.ts
new file mode 100644
index 00000000..25bfb25f
--- /dev/null
+++ b/packages/dev/base/lib/api/services/dates.test.ts
@@ -0,0 +1,412 @@
+import { Dates } from "./dates"; // Adjust path as needed
+
+let dates: Dates;
+
+beforeAll(() => {
+ dates = new Dates();
+});
+
+// Define some fixed dates for consistent testing
+// Note: Month is 0-indexed (0 = January, 11 = December)
+// Use specific UTC values to avoid local timezone ambiguities where possible
+const testTimestamp = 1678881600000; // Represents: Wed Mar 15 2023 12:00:00 GMT+0000 (UTC)
+const testDateUTC = new Date(testTimestamp); // March 15, 2023 12:00:00 UTC
+
+// A date specified with local components - its UTC representation depends on test environment timezone
+const localYear = 2024;
+const localMonth = 0; // January
+const localDay = 1;
+const localHours = 10;
+const localMinutes = 30;
+const localSeconds = 15;
+const localMilliseconds = 500;
+const testDateLocal = new Date(localYear, localMonth, localDay, localHours, localMinutes, localSeconds, localMilliseconds);
+// Note: Weekday depends on the date and timezone, e.g., Jan 1, 2024 was a Monday (1)
+
+describe("Dates Class Unit Tests", () => {
+
+ // --- Convert Methods ---
+ describe("Convert Methods", () => {
+ it("toDateString should return date part as string", () => {
+ // Format is implementation-dependent, e.g., "Wed Mar 15 2023"
+ const result = dates.toDateString({ date: testDateUTC });
+ // Check for presence of key components
+ expect(result).toMatch(/Mar/); // Contains month abbreviation
+ expect(result).toMatch(/15/); // Contains day
+ expect(result).toMatch(/2023/); // Contains year
+ expect(result).toMatch(/Wed/); // Contains weekday abbreviation
+ });
+
+ it("toISOString should return date in ISO 8601 format", () => {
+ const result = dates.toISOString({ date: testDateUTC });
+ expect(result).toBe("2023-03-15T12:00:00.000Z");
+ });
+
+ it("toJSON should return date in ISO 8601 format", () => {
+ // toJSON typically delegates to toISOString
+ const result = dates.toJSON({ date: testDateUTC });
+ expect(result).toBe("2023-03-15T12:00:00.000Z");
+ });
+
+ it("toString should return implementation-dependent string with timezone", () => {
+ const result = dates.toString({ date: testDateLocal });
+ // Example: "Mon Jan 01 2024 10:30:15 GMT+XXXX (Your Timezone Name)"
+ expect(result).toContain("Jan");
+ expect(result).toContain("01");
+ expect(result).toContain("2024");
+ expect(result).toContain("10:30:15");
+ expect(result).toContain("GMT"); // Should indicate timezone offset
+ });
+
+ it("toTimeString should return time part as string with timezone", () => {
+ const result = dates.toTimeString({ date: testDateLocal });
+ // Example: "10:30:15 GMT+XXXX (Your Timezone Name)"
+ expect(result).toContain("10:30:15");
+ expect(result).toContain("GMT");
+ });
+
+ it("toUTCString should return date string in UTC format", () => {
+ const result = dates.toUTCString({ date: testDateUTC });
+ // Standard format: "Wed, 15 Mar 2023 12:00:00 GMT"
+ expect(result).toBe("Wed, 15 Mar 2023 12:00:00 GMT");
+ });
+ });
+
+ // --- Create Methods ---
+ describe("Create Methods", () => {
+ // For testing 'now', we use fake timers
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it("now should return the current date and time", () => {
+ const specificTime = 1700000000000; // An arbitrary timestamp
+ jest.setSystemTime(specificTime);
+ const result = dates.now();
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getTime()).toBe(specificTime);
+ });
+
+ it("createDate should create a date using local time components", () => {
+ const result = dates.createDate({
+ year: localYear, month: localMonth, day: localDay,
+ hours: localHours, minutes: localMinutes, seconds: localSeconds, milliseconds: localMilliseconds
+ });
+ expect(result).toEqual(testDateLocal); // Compare with the pre-constructed local date
+ // Verify local components directly
+ expect(result.getFullYear()).toBe(localYear);
+ expect(result.getMonth()).toBe(localMonth);
+ expect(result.getDate()).toBe(localDay);
+ expect(result.getHours()).toBe(localHours);
+ // ... and so on
+ });
+
+ it("createDate should handle month overflow", () => {
+ // Month 12 should become January of the next year
+ const result = dates.createDate({ year: 2023, month: 12, day: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 });
+ expect(result.getFullYear()).toBe(2024);
+ expect(result.getMonth()).toBe(0); // January
+ expect(result.getDate()).toBe(1);
+ });
+
+ it("createDateUTC should create a date using UTC components", () => {
+ const result = dates.createDateUTC({
+ year: 2023, month: 2, day: 15, // March 15th
+ hours: 12, minutes: 0, seconds: 0, milliseconds: 0
+ });
+ // Compare its time value to the known UTC date
+ expect(result.getTime()).toBe(testDateUTC.getTime());
+ // Verify UTC components directly
+ expect(result.getUTCFullYear()).toBe(2023);
+ expect(result.getUTCMonth()).toBe(2); // March
+ expect(result.getUTCDate()).toBe(15);
+ expect(result.getUTCHours()).toBe(12);
+ });
+
+ it("createFromUnixTimeStamp should create a date from milliseconds since epoch", () => {
+ const timestamp = 0; // Epoch
+ const result = dates.createFromUnixTimeStamp({ unixTimeStamp: timestamp });
+ expect(result.getTime()).toBe(timestamp);
+ expect(result.toUTCString()).toBe("Thu, 01 Jan 1970 00:00:00 GMT");
+
+ const result2 = dates.createFromUnixTimeStamp({ unixTimeStamp: testTimestamp });
+ expect(result2.getTime()).toBe(testTimestamp);
+ });
+ });
+
+ // --- Parse Methods ---
+ describe("Parse Methods", () => {
+ it("parseDate should parse a valid ISO date string", () => {
+ const dateStr = "2023-03-15T12:00:00.000Z";
+ const result = dates.parseDate({ dateString: dateStr });
+ expect(result).toBe(testTimestamp);
+ });
+
+ it("parseDate should parse other common date string formats (may be implementation-dependent)", () => {
+ const dateStr1 = "15 Mar 2023 12:00:00 GMT";
+ const result1 = dates.parseDate({ dateString: dateStr1 });
+ expect(result1).toBe(testTimestamp);
+
+ // Parsing non-standard formats without explicit timezone might vary!
+ // const dateStr2 = "01/01/2024"; // Interpretation depends on locale/implementation
+ // const result2 = dates.parseDate({ dateString: dateStr2 });
+ // // Need to know expected timestamp for this format in the test environment
+ // expect(result2).toBe(/* expected timestamp for Jan 1 2024 local time */);
+ });
+
+ it("parseDate should return NaN for invalid date string", () => {
+ const dateStr = "invalid date string";
+ const result = dates.parseDate({ dateString: dateStr });
+ expect(result).toBeNaN();
+ });
+ });
+
+ // --- Get Methods (Local Time) ---
+ describe("Get Methods (Local)", () => {
+ const input = { date: testDateLocal };
+
+ it("getDayOfMonth should return the day of the month", () => {
+ expect(dates.getDayOfMonth(input)).toBe(localDay); // 1
+ });
+ it("getWeekday should return the day of the week (0=Sun, 6=Sat)", () => {
+ expect(dates.getWeekday(input)).toBe(testDateLocal.getDay()); // e.g., 1 for Monday Jan 1 2024
+ });
+ it("getYear should return the full year", () => {
+ expect(dates.getYear(input)).toBe(localYear); // 2024
+ });
+ it("getMonth should return the month (0-indexed)", () => {
+ expect(dates.getMonth(input)).toBe(localMonth); // 0
+ });
+ it("getHours should return the hours (0-23)", () => {
+ expect(dates.getHours(input)).toBe(localHours); // 10
+ });
+ it("getMinutes should return the minutes (0-59)", () => {
+ expect(dates.getMinutes(input)).toBe(localMinutes); // 30
+ });
+ it("getSeconds should return the seconds (0-59)", () => {
+ expect(dates.getSeconds(input)).toBe(localSeconds); // 15
+ });
+ it("getMilliseconds should return the milliseconds (0-999)", () => {
+ expect(dates.getMilliseconds(input)).toBe(localMilliseconds); // 500
+ });
+ it("getTime should return milliseconds since epoch", () => {
+ expect(dates.getTime(input)).toBe(testDateLocal.getTime());
+ });
+ });
+
+ // --- Get Methods (UTC Time) ---
+ describe("Get Methods (UTC)", () => {
+ const input = { date: testDateUTC }; // Use the UTC-defined date
+
+ it("getUTCYear should return the UTC year", () => {
+ expect(dates.getUTCYear(input)).toBe(2023);
+ });
+ it("getUTCMonth should return the UTC month (0-indexed)", () => {
+ expect(dates.getUTCMonth(input)).toBe(2); // March
+ });
+ it("getUTCDay should return the UTC day of the month", () => {
+ expect(dates.getUTCDay(input)).toBe(15);
+ });
+ // Note: getUTCDay() is day of *month* in JS, not day of week.
+ // There isn't a direct getUTCWeekday equivalent, but getDay() often works as intended
+ // if the Date object itself holds the correct UTC time value.
+
+ it("getUTCHours should return the UTC hours", () => {
+ expect(dates.getUTCHours(input)).toBe(12);
+ });
+ it("getUTCMinutes should return the UTC minutes", () => {
+ expect(dates.getUTCMinutes(input)).toBe(0);
+ });
+ it("getUTCSeconds should return the UTC seconds", () => {
+ expect(dates.getUTCSeconds(input)).toBe(0);
+ });
+ it("getUTCMilliseconds should return the UTC milliseconds", () => {
+ expect(dates.getUTCMilliseconds(input)).toBe(0);
+ });
+ });
+ // --- Set Methods (UTC Time) ---
+ describe("Set Methods (UTC)", () => {
+ let originalDate: Date;
+ const originalTimestamp = testDateUTC.getTime(); // Store the initial timestamp
+
+ beforeEach(() => {
+ originalDate = new Date(originalTimestamp); // Use the fixed timestamp
+ });
+
+ // Helper remains the same - ensures the original date object passed in wasn't modified
+ const expectOriginalUnchanged = () => {
+ expect(originalDate.getTime()).toBe(originalTimestamp);
+ };
+
+ it("setUTCYear should set the UTC year and return a new date instance", () => {
+ const newYear = 2022;
+ const result = dates.setUTCYear({ date: originalDate, year: newYear });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getUTCFullYear()).toBe(newYear);
+ expect(result).not.toBe(originalDate); // Check if it's a new object instance
+ // Timestamp MUST change when year changes
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setUTCMonth should set the UTC month and return a new date instance", () => {
+ const newMonth = 11; // December
+ const result = dates.setUTCMonth({ date: originalDate, month: newMonth });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getUTCMonth()).toBe(newMonth);
+ expect(result).not.toBe(originalDate);
+ // Timestamp MUST change when month changes (unless it coincidentally lands on same time)
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setUTCDay should set the UTC day and return a new date instance", () => {
+ const newDay = 25;
+ const result = dates.setUTCDay({ date: originalDate, day: newDay });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getUTCDate()).toBe(newDay);
+ expect(result).not.toBe(originalDate);
+ // Timestamp MUST change when day changes
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setUTCHours should set the UTC hours and return a new date instance", () => {
+ const newHours = 0; // Original was 12
+ const result = dates.setUTCHours({ date: originalDate, hours: newHours });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getUTCHours()).toBe(newHours);
+ expect(result).not.toBe(originalDate);
+ // Timestamp MUST change when hours change
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setUTCMinutes should set the UTC minutes and return a new date instance", () => {
+ const newMinutes = 30; // Original was 0
+ const result = dates.setUTCMinutes({ date: originalDate, minutes: newMinutes });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getUTCMinutes()).toBe(newMinutes);
+ expect(result).not.toBe(originalDate);
+ // Timestamp MUST change when minutes change
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setUTCSeconds should set the UTC seconds and return a new date instance", () => {
+ const newSeconds = 45; // Original was 0
+ const result = dates.setUTCSeconds({ date: originalDate, seconds: newSeconds });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getUTCSeconds()).toBe(newSeconds);
+ expect(result).not.toBe(originalDate);
+ // Timestamp MUST change when seconds change
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setUTCMilliseconds should set the UTC milliseconds and return a new date instance", () => {
+ const newMs = 123; // Original was 0
+ const result = dates.setUTCMilliseconds({ date: originalDate, milliseconds: newMs });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getUTCMilliseconds()).toBe(newMs);
+ expect(result).not.toBe(originalDate);
+ // Timestamp MUST change when ms change
+ expect(result.getTime()).not.toBe(originalTimestamp); // This specific check confirms timestamp changed
+ expectOriginalUnchanged();
+ });
+ });
+
+ // Apply the same logic fix to the Set Methods (Local) block
+ describe("Set Methods (Local)", () => {
+ let originalDate: Date;
+ const originalTimestamp = testDateLocal.getTime(); // Store the initial timestamp
+
+ beforeEach(() => {
+ originalDate = new Date(originalTimestamp);
+ });
+ const expectOriginalUnchanged = () => {
+ expect(originalDate.getTime()).toBe(originalTimestamp);
+ };
+
+ it("setYear should set the year and return a new date instance", () => {
+ const newYear = 2025;
+ const result = dates.setYear({ date: originalDate, year: newYear });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getFullYear()).toBe(newYear);
+ expect(result).not.toBe(originalDate);
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setMonth should set the month and return a new date instance", () => {
+ const newMonth = 5; // June
+ const result = dates.setMonth({ date: originalDate, month: newMonth });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getMonth()).toBe(newMonth);
+ expect(result).not.toBe(originalDate);
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setMonth should handle overflow and return a new date instance", () => {
+ const newMonth = 12; // Should become Jan of next year
+ const result = dates.setMonth({ date: originalDate, month: newMonth });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getMonth()).toBe(0);
+ expect(result.getFullYear()).toBe(originalDate.getFullYear() + 1);
+ expect(result).not.toBe(originalDate);
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setDayOfMonth should set the day and return a new date instance", () => {
+ const newDay = 15;
+ const result = dates.setDayOfMonth({ date: originalDate, day: newDay });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getDate()).toBe(newDay);
+ expect(result).not.toBe(originalDate);
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setHours should set the hours and return a new date instance", () => {
+ const newHours = 23; // Original was 10
+ const result = dates.setHours({ date: originalDate, hours: newHours });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getHours()).toBe(newHours);
+ expect(result).not.toBe(originalDate);
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setMinutes should set the minutes and return a new date instance", () => {
+ const newMinutes = 59; // Original was 30
+ const result = dates.setMinutes({ date: originalDate, minutes: newMinutes });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getMinutes()).toBe(newMinutes);
+ expect(result).not.toBe(originalDate);
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setSeconds should set the seconds and return a new date instance", () => {
+ const newSeconds = 1; // Original was 15
+ const result = dates.setSeconds({ date: originalDate, seconds: newSeconds });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getSeconds()).toBe(newSeconds);
+ expect(result).not.toBe(originalDate);
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setMilliseconds should set the milliseconds and return a new date instance", () => {
+ const newMs = 999; // Original was 500
+ const result = dates.setMilliseconds({ date: originalDate, milliseconds: newMs });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getMilliseconds()).toBe(newMs);
+ expect(result).not.toBe(originalDate);
+ // Check the timestamp DID change
+ expect(result.getTime()).not.toBe(originalTimestamp);
+ expectOriginalUnchanged();
+ });
+ it("setTime should set the time value and return a new date instance", () => {
+ const newTime = testTimestamp; // Use the other test date's timestamp
+ const result = dates.setTime({ date: originalDate, time: newTime });
+ expect(result).toBeInstanceOf(Date);
+ expect(result.getTime()).toBe(newTime);
+ expect(result).not.toBe(originalDate); // Different instance
+ expectOriginalUnchanged();
+ });
+ });
+
+}); // End describe('Dates Class Unit Tests')
\ No newline at end of file
diff --git a/packages/dev/base/lib/api/services/dates.ts b/packages/dev/base/lib/api/services/dates.ts
index 6e366aba..1a44c0a8 100644
--- a/packages/dev/base/lib/api/services/dates.ts
+++ b/packages/dev/base/lib/api/services/dates.ts
@@ -321,7 +321,9 @@ export class Dates {
* @drawable false
* */
setYear(inputs: Inputs.Dates.DateYearDto): Date {
- return new Date(inputs.date.setFullYear(inputs.year));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setFullYear(inputs.year);
+ return dateCopy;
}
/**
@@ -333,7 +335,9 @@ export class Dates {
* @drawable false
* */
setMonth(inputs: Inputs.Dates.DateMonthDto): Date {
- return new Date(inputs.date.setMonth(inputs.month));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setMonth(inputs.month);
+ return dateCopy;
}
/**
@@ -345,7 +349,9 @@ export class Dates {
* @drawable false
*/
setDayOfMonth(inputs: Inputs.Dates.DateDayDto): Date {
- return new Date(inputs.date.setDate(inputs.day));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setDate(inputs.day);
+ return dateCopy;
}
/**
@@ -357,7 +363,9 @@ export class Dates {
* @drawable false
* */
setHours(inputs: Inputs.Dates.DateHoursDto): Date {
- return new Date(inputs.date.setHours(inputs.hours));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setHours(inputs.hours);
+ return dateCopy;
}
/**
@@ -369,7 +377,9 @@ export class Dates {
* @drawable false
* */
setMinutes(inputs: Inputs.Dates.DateMinutesDto): Date {
- return new Date(inputs.date.setMinutes(inputs.minutes));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setMinutes(inputs.minutes);
+ return dateCopy;
}
/**
@@ -381,7 +391,9 @@ export class Dates {
* @drawable false
*/
setSeconds(inputs: Inputs.Dates.DateSecondsDto): Date {
- return new Date(inputs.date.setSeconds(inputs.seconds));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setSeconds(inputs.seconds);
+ return dateCopy;
}
/**
@@ -393,7 +405,9 @@ export class Dates {
* @drawable false
*/
setMilliseconds(inputs: Inputs.Dates.DateMillisecondsDto): Date {
- return new Date(inputs.date.setMilliseconds(inputs.milliseconds));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setMilliseconds(inputs.milliseconds);
+ return dateCopy;
}
/**
@@ -405,7 +419,9 @@ export class Dates {
* @drawable false
*/
setTime(inputs: Inputs.Dates.DateTimeDto): Date {
- return new Date(inputs.date.setTime(inputs.time));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setTime(inputs.time);
+ return dateCopy;
}
@@ -418,7 +434,9 @@ export class Dates {
* @drawable false
* */
setUTCYear(inputs: Inputs.Dates.DateYearDto): Date {
- return new Date(inputs.date.setUTCFullYear(inputs.year));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setUTCFullYear(inputs.year);
+ return dateCopy;
}
/**
@@ -430,7 +448,9 @@ export class Dates {
* @drawable false
* */
setUTCMonth(inputs: Inputs.Dates.DateMonthDto): Date {
- return new Date(inputs.date.setUTCMonth(inputs.month));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setUTCMonth(inputs.month);
+ return dateCopy;
}
/**
@@ -442,7 +462,9 @@ export class Dates {
* @drawable false
*/
setUTCDay(inputs: Inputs.Dates.DateDayDto): Date {
- return new Date(inputs.date.setUTCDate(inputs.day));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setUTCDate(inputs.day);
+ return dateCopy;
}
/**
@@ -454,7 +476,9 @@ export class Dates {
* @drawable false
* */
setUTCHours(inputs: Inputs.Dates.DateHoursDto): Date {
- return new Date(inputs.date.setUTCHours(inputs.hours));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setUTCHours(inputs.hours);
+ return dateCopy;
}
/**
@@ -466,7 +490,9 @@ export class Dates {
* @drawable false
* */
setUTCMinutes(inputs: Inputs.Dates.DateMinutesDto): Date {
- return new Date(inputs.date.setUTCMinutes(inputs.minutes));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setUTCMinutes(inputs.minutes);
+ return dateCopy;
}
/**
@@ -478,7 +504,9 @@ export class Dates {
* @drawable false
*/
setUTCSeconds(inputs: Inputs.Dates.DateSecondsDto): Date {
- return new Date(inputs.date.setUTCSeconds(inputs.seconds));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setUTCSeconds(inputs.seconds);
+ return dateCopy;
}
/**
@@ -490,7 +518,9 @@ export class Dates {
* @drawable false
*/
setUTCMilliseconds(inputs: Inputs.Dates.DateMillisecondsDto): Date {
- return new Date(inputs.date.setUTCMilliseconds(inputs.milliseconds));
+ const dateCopy = new Date(inputs.date.getTime());
+ dateCopy.setUTCMilliseconds(inputs.milliseconds);
+ return dateCopy;
}
}
diff --git a/packages/dev/base/lib/api/services/index.ts b/packages/dev/base/lib/api/services/index.ts
index c30b794d..dd65589a 100644
--- a/packages/dev/base/lib/api/services/index.ts
+++ b/packages/dev/base/lib/api/services/index.ts
@@ -7,4 +7,7 @@ export * from "./text";
export * from "./dates";
export * from "./vector";
export * from "./transforms";
-export * from "./geometry-helper";
\ No newline at end of file
+export * from "./geometry-helper";
+export * from "./line";
+export * from "./polyline";
+export * from "./mesh";
\ No newline at end of file
diff --git a/packages/dev/base/lib/api/services/line.test.ts b/packages/dev/base/lib/api/services/line.test.ts
new file mode 100644
index 00000000..7ac7fcf6
--- /dev/null
+++ b/packages/dev/base/lib/api/services/line.test.ts
@@ -0,0 +1,334 @@
+import { GeometryHelper } from "./geometry-helper";
+import { MathBitByBit } from "./math";
+import { Point } from "./point";
+import { Line } from "./line";
+import { Transforms } from "./transforms";
+import { Vector } from "./vector";
+import * as Inputs from "../inputs";
+
+describe("Line unit tests", () => {
+
+ let geometryHelper: GeometryHelper;
+ let math: MathBitByBit;
+ let vector: Vector;
+ let point: Point;
+ let line: Line;
+ let transforms: Transforms;
+
+
+ // Precision for floating point comparisons
+ const TOLERANCE = 1e-7;
+
+ // Helper to compare points/vectors with tolerance
+ const expectPointCloseTo = (
+ received: Inputs.Base.Point3 | Inputs.Base.Vector3 | undefined,
+ expected: Inputs.Base.Point3 | Inputs.Base.Vector3
+ ) => {
+ expect(received).toBeDefined();
+ if (!received) return; // Guard for TS
+ expect(received.length).toEqual(expected.length);
+ expect(received[0]).toBeCloseTo(expected[0], TOLERANCE);
+ expect(received[1]).toBeCloseTo(expected[1], TOLERANCE);
+ if (expected.length > 2 && received.length > 2) {
+ expect(received[2]).toBeCloseTo(expected[2], TOLERANCE);
+ }
+ };
+
+ // Helper to compare lines with tolerance
+ const expectLineCloseTo = (
+ received: Inputs.Base.Line3 | undefined,
+ expected: Inputs.Base.Line3
+ ) => {
+ expect(received).toBeDefined();
+ if (!received) return;
+ expectPointCloseTo(received.start, expected.start);
+ expectPointCloseTo(received.end, expected.end);
+ };
+
+ // Helper to compare arrays of lines with tolerance
+ const expectLinesCloseTo = (
+ received: Inputs.Base.Line3[],
+ expected: Inputs.Base.Line3[]
+ ) => {
+ expect(received.length).toEqual(expected.length);
+ received.forEach((l, i) => expectLineCloseTo(l, expected[i]));
+ };
+
+
+ beforeAll(() => {
+ geometryHelper = new GeometryHelper();
+ math = new MathBitByBit();
+ vector = new Vector(math, geometryHelper);
+ transforms = new Transforms(vector, math);
+ point = new Point(geometryHelper, transforms, vector);
+ line = new Line(point, geometryHelper);
+ });
+
+
+ describe("Line Class Unit Tests (Integration)", () => {
+
+ const sampleLine: Inputs.Base.Line3 = { start: [1, 2, 3], end: [4, 6, 8] };
+ const sampleLineZeroLength: Inputs.Base.Line3 = { start: [1, 1, 1], end: [1, 1, 1] };
+
+ describe("getStartPoint", () => {
+ it("should return the start point of the line", () => {
+ const result = line.getStartPoint({ line: sampleLine });
+ expect(result).toEqual(sampleLine.start);
+ });
+ });
+
+ describe("getEndPoint", () => {
+ it("should return the end point of the line", () => {
+ const result = line.getEndPoint({ line: sampleLine });
+ expect(result).toEqual(sampleLine.end);
+ });
+ });
+
+ describe("length", () => {
+ it("should calculate the length of the line", () => {
+ // start: [1, 2, 3], end: [4, 6, 8] -> dx=3, dy=4, dz=5
+ // length = sqrt(3^2 + 4^2 + 5^2) = sqrt(9 + 16 + 25) = sqrt(50)
+ const expectedLength = Math.sqrt(50);
+ const result = line.length({ line: sampleLine });
+ expect(result).toBeCloseTo(expectedLength, TOLERANCE);
+ });
+
+ it("should return 0 for a zero-length line", () => {
+ const result = line.length({ line: sampleLineZeroLength });
+ expect(result).toBeCloseTo(0, TOLERANCE);
+ });
+ });
+
+ describe("reverse", () => {
+ it("should swap the start and end points", () => {
+ const result = line.reverse({ line: sampleLine });
+ const expectedReversedLine: Inputs.Base.Line3 = { start: sampleLine.end, end: sampleLine.start };
+ expect(result).toEqual(expectedReversedLine);
+ // Ensure it returns a new object
+ expect(result).not.toBe(sampleLine);
+ });
+ });
+
+ describe("transformLine", () => {
+ it("should transform both start and end points of the line", () => {
+ const inputLine: Inputs.Base.Line3 = { start: [0, 0, 0], end: [1, 0, 0] };
+ const transformation = transforms.translationXYZ({ translation: [10, 5, -2] });
+ const result = line.transformLine({ line: inputLine, transformation });
+ const expectedLine: Inputs.Base.Line3 = { start: [10, 5, -2], end: [11, 5, -2] };
+ expectLineCloseTo(result, expectedLine);
+ });
+
+ it("should rotate the line", () => {
+ const inputLine: Inputs.Base.Line3 = { start: [1, 0, 0], end: [2, 0, 5] };
+ const transformation = transforms.rotationCenterAxis({ center: [0, 0, 0], axis: [0, 0, 1], angle: 90 });
+ const result = line.transformLine({ line: inputLine, transformation });
+ const expectedLine: Inputs.Base.Line3 = { start: [0, 1, 0], end: [0, 2, 5] };
+ expectLineCloseTo(result, expectedLine);
+ });
+
+ it("should return a new object", () => {
+ const inputLine: Inputs.Base.Line3 = { start: [0, 0, 0], end: [1, 0, 0] };
+ const transformation = [transforms.identity()];
+ const result = line.transformLine({ line: inputLine, transformation });
+ expect(result).not.toBe(inputLine);
+ expect(result.start).not.toBe(inputLine.start); // transformControlPoints likely creates new points/arrays
+ expect(result.end).not.toBe(inputLine.end);
+ });
+ });
+
+ describe("transformsForLines", () => {
+ it("should apply individual transforms to corresponding lines", () => {
+ const inputLines: Inputs.Base.Line3[] = [
+ { start: [0, 0, 0], end: [1, 0, 0] },
+ { start: [5, 5, 5], end: [5, 6, 5] }
+ ];
+ const transformations = [
+ transforms.translationXYZ({ translation: [0, 10, 0] }), // Translate line 1
+ transforms.rotationCenterAxis({ center: [5, 5, 5], axis: [1, 0, 0], angle: 90 }) // Rotate line 2 around its start
+ ];
+ const result = line.transformsForLines({ lines: inputLines, transformation: transformations });
+ const expectedLines: Inputs.Base.Line3[] = [
+ { start: [0, 10, 0], end: [1, 10, 0] },
+ { start: [5, 5, 5], end: [5, 5, 6] } // Rotation around X maps Y=1 -> Z=1
+ ];
+ expectLinesCloseTo(result, expectedLines);
+ });
+
+ it("should handle empty input arrays", () => {
+ const result = line.transformsForLines({ lines: [], transformation: [] });
+ expect(result).toEqual([]);
+ });
+
+ // Note: This method does not check for mismatched array lengths, unlike Point.transformsForPoints
+ // Adding such a check might be a good idea in the Line class itself.
+ // Testing the current behavior (likely error or incorrect result) is less useful than testing intended logic.
+ });
+
+ describe("create", () => {
+ it("should create a line object from start and end points", () => {
+ const startP: Inputs.Base.Point3 = [9, 8, 7];
+ const endP: Inputs.Base.Point3 = [6, 5, 4];
+ const result = line.create({ start: startP, end: endP });
+ const expectedLine: Inputs.Base.Line3 = { start: startP, end: endP };
+ expect(result).toEqual(expectedLine);
+ });
+ });
+
+ describe("getPointOnLine", () => {
+ const testLine: Inputs.Base.Line3 = { start: [0, 0, 0], end: [10, 20, -30] };
+
+ it("should return the start point when param is 0", () => {
+ const result = line.getPointOnLine({ line: testLine, param: 0 });
+ expectPointCloseTo(result, testLine.start);
+ });
+
+ it("should return the end point when param is 1", () => {
+ const result = line.getPointOnLine({ line: testLine, param: 1 });
+ expectPointCloseTo(result, testLine.end);
+ });
+
+ it("should return the midpoint when param is 0.5", () => {
+ const result = line.getPointOnLine({ line: testLine, param: 0.5 });
+ const expectedMidpoint: Inputs.Base.Point3 = [5, 10, -15];
+ expectPointCloseTo(result, expectedMidpoint);
+ });
+
+ it("should extrapolate backward when param is < 0", () => {
+ const result = line.getPointOnLine({ line: testLine, param: -0.5 });
+ // Direction = [10, 20, -30]. Start + (-0.5)*Dir = [0,0,0] + [-5, -10, 15]
+ const expectedPoint: Inputs.Base.Point3 = [-5, -10, 15];
+ expectPointCloseTo(result, expectedPoint);
+ });
+
+ it("should extrapolate forward when param is > 1", () => {
+ const result = line.getPointOnLine({ line: testLine, param: 1.2 });
+ // Start + (1.2)*Dir = [0,0,0] + [12, 24, -36]
+ const expectedPoint: Inputs.Base.Point3 = [12, 24, -36];
+ expectPointCloseTo(result, expectedPoint);
+ });
+ });
+
+ describe("linesBetweenPoints", () => {
+ it("should create lines connecting consecutive points", () => {
+ const points: Inputs.Base.Point3[] = [[0, 0, 0], [1, 1, 1], [2, 0, 0]];
+ const result = line.linesBetweenPoints({ points });
+ const expectedLines: Inputs.Base.Line3[] = [
+ { start: [0, 0, 0], end: [1, 1, 1] },
+ { start: [1, 1, 1], end: [2, 0, 0] }
+ ];
+ expect(result).toEqual(expectedLines);
+ });
+
+ it("should create one line for two points", () => {
+ const points: Inputs.Base.Point3[] = [[5, 0, 0], [0, 5, 0]];
+ const result = line.linesBetweenPoints({ points });
+ const expectedLines: Inputs.Base.Line3[] = [
+ { start: [5, 0, 0], end: [0, 5, 0] }
+ ];
+ expect(result).toEqual(expectedLines);
+ });
+
+ it("should return an empty array for one point", () => {
+ const points: Inputs.Base.Point3[] = [[1, 2, 3]];
+ const result = line.linesBetweenPoints({ points });
+ expect(result).toEqual([]);
+ });
+
+ it("should return an empty array for zero points", () => {
+ const points: Inputs.Base.Point3[] = [];
+ const result = line.linesBetweenPoints({ points });
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe("linesBetweenStartAndEndPoints", () => {
+ it("should create lines between corresponding start and end points", () => {
+ const starts: Inputs.Base.Point3[] = [[0, 0, 0], [1, 1, 1]];
+ const ends: Inputs.Base.Point3[] = [[10, 0, 0], [11, 1, 1]];
+ const result = line.linesBetweenStartAndEndPoints({ startPoints: starts, endPoints: ends });
+ const expectedLines: Inputs.Base.Line3[] = [
+ { start: [0, 0, 0], end: [10, 0, 0] },
+ { start: [1, 1, 1], end: [11, 1, 1] }
+ ];
+ expect(result).toEqual(expectedLines);
+ });
+
+ it("should filter out zero-length lines", () => {
+ const starts: Inputs.Base.Point3[] = [[0, 0, 0], [5, 5, 5], [1, 2, 3]];
+ const ends: Inputs.Base.Point3[] = [[10, 0, 0], [5, 5, 5], [4, 5, 6]]; // Middle line is zero-length
+ const result = line.linesBetweenStartAndEndPoints({ startPoints: starts, endPoints: ends });
+ const expectedLines: Inputs.Base.Line3[] = [
+ { start: [0, 0, 0], end: [10, 0, 0] },
+ { start: [1, 2, 3], end: [4, 5, 6] }
+ ];
+ expect(result).toEqual(expectedLines);
+ });
+
+ it("should return an empty array for empty input lists", () => {
+ const result = line.linesBetweenStartAndEndPoints({ startPoints: [], endPoints: [] });
+ expect(result).toEqual([]);
+ });
+
+ // Note: Like transformsForLines, assumes lists are the same length. Mismatched lengths aren't explicitly handled.
+ });
+
+ describe("lineToSegment", () => {
+ it("should convert a line object to a segment array", () => {
+ const result = line.lineToSegment({ line: sampleLine });
+ const expectedSegment: Inputs.Base.Segment3 = [sampleLine.start, sampleLine.end];
+ expect(result).toEqual(expectedSegment);
+ });
+ });
+
+ describe("linesToSegments", () => {
+ it("should convert multiple line objects to segment arrays", () => {
+ const lines: Inputs.Base.Line3[] = [
+ { start: [0, 0, 0], end: [1, 1, 1] },
+ { start: [2, 2, 2], end: [3, 3, 3] }
+ ];
+ const result = line.linesToSegments({ lines });
+ const expectedSegments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 1, 1]],
+ [[2, 2, 2], [3, 3, 3]]
+ ];
+ expect(result).toEqual(expectedSegments);
+ });
+
+ it("should return an empty array for empty lines input", () => {
+ const result = line.linesToSegments({ lines: [] });
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe("segmentToLine", () => {
+ it("should convert a segment array to a line object", () => {
+ const segment: Inputs.Base.Segment3 = [[10, 9, 8], [7, 6, 5]];
+ const result = line.segmentToLine({ segment });
+ const expectedLine: Inputs.Base.Line3 = { start: segment[0], end: segment[1] };
+ expect(result).toEqual(expectedLine);
+ });
+ });
+
+ describe("segmentsToLines", () => {
+ it("should convert multiple segment arrays to line objects", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 1, 1]],
+ [[2, 2, 2], [3, 3, 3]]
+ ];
+ const result = line.segmentsToLines({ segments });
+ const expectedLines: Inputs.Base.Line3[] = [
+ { start: [0, 0, 0], end: [1, 1, 1] },
+ { start: [2, 2, 2], end: [3, 3, 3] }
+ ];
+ expect(result).toEqual(expectedLines);
+ });
+
+ it("should return an empty array for empty segments input", () => {
+ const result = line.segmentsToLines({ segments: [] });
+ expect(result).toEqual([]);
+ });
+ });
+
+ });
+});
+
diff --git a/packages/dev/core/lib/api/bitbybit/line.ts b/packages/dev/base/lib/api/services/line.ts
similarity index 51%
rename from packages/dev/core/lib/api/bitbybit/line.ts
rename to packages/dev/base/lib/api/services/line.ts
index 1db899af..96ed1eaf 100644
--- a/packages/dev/core/lib/api/bitbybit/line.ts
+++ b/packages/dev/base/lib/api/services/line.ts
@@ -1,40 +1,22 @@
-import { ContextBase } from "../context";
-import { GeometryHelper } from "@bitbybit-dev/base";
+import { GeometryHelper } from "./geometry-helper";
import * as Inputs from "../inputs";
+import { Point } from "./point";
/**
- * Contains various methods for lines. Line in bitbybit is a simple object that has star and end point properties.
+ * Contains various methods for lines and segments. Line in bitbybit is a simple object that has start and end point properties.
* { start: [ x, y, z ], end: [ x, y, z ] }
*/
-
export class Line {
- constructor(private readonly context: ContextBase, private readonly geometryHelper: GeometryHelper) { }
-
- /**
- * Converts a line to a NURBS line curve
- * Returns the verbnurbs Line object
- * @param inputs Line to be transformed to curve
- * @returns Verb nurbs curve
- */
- convertToNurbsCurve(inputs: Inputs.Line.LineDto): any {
- return new this.context.verb.geom.Line(inputs.line.start, inputs.line.end);
- }
-
- /**
- * Converts lines to a NURBS curves
- * Returns array of the verbnurbs Line objects
- * @param inputs Lines to be transformed to curves
- * @returns Verb nurbs curves
- */
- convertLinesToNurbsCurves(inputs: Inputs.Line.LinesDto): any[] {
- return inputs.lines.map(line => new this.context.verb.geom.Line(line.start, line.end));
- }
+ constructor(private readonly point: Point, private readonly geometryHelper: GeometryHelper) { }
/**
* Gets the start point of the line
- * @param inputs Line to be queried
- * @returns Start point
+ * @param inputs a line
+ * @returns start point
+ * @group get
+ * @shortname line start point
+ * @drawable true
*/
getStartPoint(inputs: Inputs.Line.LineDto): Inputs.Base.Point3 {
return inputs.line.start;
@@ -42,8 +24,11 @@ export class Line {
/**
* Gets the end point of the line
- * @param inputs Line to be queried
- * @returns End point
+ * @param inputs a line
+ * @returns end point
+ * @group get
+ * @shortname line end point
+ * @drawable true
*/
getEndPoint(inputs: Inputs.Line.LineDto): Inputs.Base.Point3 {
return inputs.line.end;
@@ -51,17 +36,23 @@ export class Line {
/**
* Gets the length of the line
- * @param inputs Line to be queried
- * @returns Length of the line
+ * @param inputs a line
+ * @returns line length
+ * @group get
+ * @shortname line length
+ * @drawable false
*/
length(inputs: Inputs.Line.LineDto): number {
- return this.context.verb.core.Vec.dist(inputs.line.start, inputs.line.end);
+ return this.point.distance({ startPoint: inputs.line.start, endPoint: inputs.line.end });
}
/**
* Reverse the endpoints of the line
- * @param inputs Line to be reversed
- * @returns Reversed line
+ * @param inputs a line
+ * @returns reversed line
+ * @group operations
+ * @shortname reversed line
+ * @drawable true
*/
reverse(inputs: Inputs.Line.LineDto): Inputs.Base.Line3 {
return { start: inputs.line.end, end: inputs.line.start };
@@ -69,8 +60,11 @@ export class Line {
/**
* Transform the line
- * @param inputs Line to be transformed
- * @returns Transformed line
+ * @param inputs a line
+ * @returns transformed line
+ * @group transforms
+ * @shortname transform line
+ * @drawable true
*/
transformLine(inputs: Inputs.Line.TransformLineDto): Inputs.Base.Line3 {
const transformation = inputs.transformation;
@@ -84,8 +78,11 @@ export class Line {
/**
* Transforms the lines with multiple transform for each line
- * @param inputs Lines to be transformed and transformations
- * @returns Transformed lines
+ * @param inputs lines
+ * @returns transformed lines
+ * @group transforms
+ * @shortname transform lines
+ * @drawable true
*/
transformsForLines(inputs: Inputs.Line.TransformsLinesDto): Inputs.Base.Line3[] {
return inputs.lines.map((line, index) => {
@@ -101,8 +98,11 @@ export class Line {
/**
* Create the line
- * @param inputs Endpoints of the line
- * @returns Line
+ * @param inputs start and end points of the line
+ * @returns line
+ * @group create
+ * @shortname line
+ * @drawable true
*/
create(inputs: Inputs.Line.LinePointsDto): Inputs.Base.Line3 {
return {
@@ -111,22 +111,13 @@ export class Line {
};
}
- /**
- * Create the line from possibly async inputs of points
- * @param inputs Endpoints of the line
- * @returns Line
- */
- createAsync(inputs: Inputs.Line.LinePointsDto): Promise {
- return Promise.resolve({
- start: inputs.start,
- end: inputs.end,
- });
- }
-
/**
* Gets the point on the line segment at a given param
- * @param inputs Line and parameter
- * @returns Point on line
+ * @param inputs line
+ * @returns point on line
+ * @group get
+ * @shortname point on line
+ * @drawable true
*/
getPointOnLine(inputs: Inputs.Line.PointOnLineDto): Inputs.Base.Point3 {
// Calculate direction vector of line segment
@@ -142,9 +133,12 @@ export class Line {
}
/**
- * Create the line segments between all of the points in a list
- * @param inputs Lines in a list
- * @returns Lines
+ * Create the lines segments between all of the points in a list
+ * @param inputs points
+ * @returns lines
+ * @group create
+ * @shortname lines between points
+ * @drawable true
*/
linesBetweenPoints(inputs: Inputs.Line.PointsLinesDto): Inputs.Base.Line3[] {
const lines = [];
@@ -157,23 +151,66 @@ export class Line {
}
/**
- * Create the lines between two lists of start and end points of equal length
- * @param inputs Two lists of start and end points
- * @returns Lines
+ * Create the lines between start and end points
+ * @param inputs start points and end points
+ * @returns lines
+ * @group create
+ * @shortname start and end points to lines
+ * @drawable true
*/
linesBetweenStartAndEndPoints(inputs: Inputs.Line.LineStartEndPointsDto): Inputs.Base.Line3[] {
return inputs.startPoints
.map((s, index) => ({ start: s, end: inputs.endPoints[index] }))
- .filter(line => this.context.verb.core.Vec.dist(line.start, line.end) !== 0);
+ .filter(line => this.point.distance({ startPoint: line.start, endPoint: line.end }) !== 0);
+ }
+
+ /**
+ * Convert the line to segment
+ * @param inputs line
+ * @returns segment
+ * @group convert
+ * @shortname line to segment
+ * @drawable false
+ */
+ lineToSegment(inputs: Inputs.Line.LineDto): Inputs.Base.Segment3 {
+ return [inputs.line.start, inputs.line.end];
+ }
+
+ /**
+ * Converts the lines to segments
+ * @param inputs lines
+ * @returns segments
+ * @group convert
+ * @shortname lines to segments
+ * @drawable false
+ */
+ linesToSegments(inputs: Inputs.Line.LinesDto): Inputs.Base.Segment3[] {
+ return inputs.lines.map(line => [line.start, line.end]);
+ }
+
+ /**
+ * Converts the segment to line
+ * @param inputs segment
+ * @returns line
+ * @group convert
+ * @shortname segment to line
+ * @drawable true
+ */
+ segmentToLine(inputs: Inputs.Line.SegmentDto): Inputs.Base.Line3 {
+ return { start: inputs.segment[0], end: inputs.segment[1] };
}
/**
- * Create the lines between two lists of start and end points of equal length with potential async inputs
- * @param inputs Two lists of start and end points
- * @returns Lines
+ * Converts the segments to lines
+ * @param inputs segments
+ * @returns lines
+ * @group convert
+ * @shortname segments to lines
+ * @drawable true
*/
- linesBetweenStartAndEndPointsAsync(inputs: Inputs.Line.LineStartEndPointsDto): Promise {
- return Promise.resolve(this.linesBetweenStartAndEndPoints(inputs));
+ segmentsToLines(inputs: Inputs.Line.SegmentsDto): Inputs.Base.Line3[] {
+ return inputs.segments.map(segment => ({ start: segment[0], end: segment[1] }));
}
+
}
diff --git a/packages/dev/base/lib/api/services/mesh.test.ts b/packages/dev/base/lib/api/services/mesh.test.ts
new file mode 100644
index 00000000..428425f6
--- /dev/null
+++ b/packages/dev/base/lib/api/services/mesh.test.ts
@@ -0,0 +1,311 @@
+import { GeometryHelper } from "./geometry-helper";
+import { MathBitByBit } from "./math";
+import { Point } from "./point";
+import { Polyline } from "./polyline";
+import { Transforms } from "./transforms";
+import { Vector } from "./vector";
+import * as Inputs from "../inputs";
+import { MeshBitByBit } from "./mesh";
+
+describe("Mesh unit tests", () => {
+ let geometryHelper = new GeometryHelper();
+ let math = new MathBitByBit();
+ let vector = new Vector(math, geometryHelper);
+ let transforms = new Transforms(vector, math);
+ let point = new Point(geometryHelper, transforms, vector);
+ let polyline = new Polyline(vector, point, geometryHelper);
+ let meshBitByBit = new MeshBitByBit(vector, polyline);
+
+ const TOLERANCE = 1e-7;
+
+ const expectPointCloseTo = (
+ received: Inputs.Base.Point3 | Inputs.Base.Vector3 | undefined,
+ expected: Inputs.Base.Point3 | Inputs.Base.Vector3,
+ precision = TOLERANCE
+ ) => {
+ expect(received).toBeDefined();
+ if (!received) return;
+ expect(received.length).toEqual(expected.length);
+ expect(received[0]).toBeCloseTo(expected[0], precision);
+ expect(received[1]).toBeCloseTo(expected[1], precision);
+ if (expected.length > 2 && received.length > 2) {
+ expect(received[2]).toBeCloseTo(expected[2], precision);
+ }
+ };
+ const expectSegmentCloseTo = (
+ received: Inputs.Base.Segment3 | undefined,
+ expected: Inputs.Base.Segment3,
+ precision = TOLERANCE
+ ) => {
+ expect(received).toBeDefined();
+ if (!received) return;
+ expect(received).toHaveLength(2);
+ const order1Matches = Math.abs(vector.dist({ first: received[0], second: expected[0] })) < precision &&
+ Math.abs(vector.dist({ first: received[1], second: expected[1] })) < precision;
+ const order2Matches = Math.abs(vector.dist({ first: received[0], second: expected[1] })) < precision &&
+ Math.abs(vector.dist({ first: received[1], second: expected[0] })) < precision;
+ expect(order1Matches || order2Matches).toBe(true);
+ };
+
+ const expectPlaneCloseTo = (
+ received: Inputs.Base.TrianglePlane3 | undefined,
+ expected: Inputs.Base.TrianglePlane3,
+ precision = TOLERANCE
+ ) => {
+ expect(received).toBeDefined();
+ if (!received) return;
+ const normalDir1 = vector.sub({ first: received.normal, second: expected.normal });
+ const normalDir2 = vector.add({ first: received.normal, second: expected.normal });
+ const dir1Match = vector.lengthSq({ vector: normalDir1 as Inputs.Base.Vector3 }) < precision * precision;
+ const dir2Match = vector.lengthSq({ vector: normalDir2 as Inputs.Base.Vector3 }) < precision * precision;
+ expect(dir1Match || dir2Match).toBe(true);
+ expect(received.d).toBeCloseTo(dir1Match ? expected.d : -expected.d, precision);
+ };
+
+ beforeAll(() => {
+ geometryHelper = new GeometryHelper();
+ math = new MathBitByBit();
+ vector = new Vector(math, geometryHelper);
+ transforms = new Transforms(vector, math);
+ point = new Point(geometryHelper, transforms, vector);
+ polyline = new Polyline(vector, point, geometryHelper);
+ meshBitByBit = new MeshBitByBit(vector, polyline);
+ });
+
+ // Simple plane definitions
+ const xyPlane: Inputs.Base.TrianglePlane3 = { normal: [0, 0, 1], d: 0 }; // Z=0 plane
+ const xyPlaneOffset: Inputs.Base.TrianglePlane3 = { normal: [0, 0, 1], d: 5 }; // Z=5 plane
+ const slantedPlane: Inputs.Base.TrianglePlane3 = { normal: vector.normalized({ vector: [1, 1, 1] }) as Inputs.Base.Vector3, d: 0 }; // X+Y+Z=0 plane through origin
+
+ // Simple triangles
+ const triXY1: Inputs.Base.Triangle3 = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]; // On XY plane, normal ~[0,0,1]
+ const triXY2: Inputs.Base.Triangle3 = [[2, 0, 0], [3, 0, 0], [2, 1, 0]]; // On XY plane, normal ~[0,0,1]
+ const triXYOffset: Inputs.Base.Triangle3 = [[0, 0, 5], [1, 0, 5], [0, 1, 5]]; // On Z=5 plane, normal ~[0,0,1]
+ const triXZ: Inputs.Base.Triangle3 = [[0, 0, 0], [1, 0, 0], [0, 0, 1]]; // On XZ plane, normal ~[0,-1,0]
+ const triSlanted: Inputs.Base.Triangle3 = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; // On X+Y+Z=1 plane
+ const triDegenerateCollinear: Inputs.Base.Triangle3 = [[0, 0, 0], [1, 1, 1], [2, 2, 2]];
+ const triDegenerateCoincident: Inputs.Base.Triangle3 = [[0, 0, 0], [0, 0, 0], [1, 1, 1]];
+
+ describe("signedDistanceToPlane", () => {
+ it("should return 0 for a point on the plane", () => {
+ const p: Inputs.Base.Point3 = [10, 20, 0];
+ const dist = meshBitByBit.signedDistanceToPlane({ point: p, plane: xyPlane });
+ expect(dist).toBeCloseTo(0, TOLERANCE);
+ });
+
+ it("should return positive distance for point on normal side", () => {
+ const p: Inputs.Base.Point3 = [5, 5, 3];
+ const dist = meshBitByBit.signedDistanceToPlane({ point: p, plane: xyPlane });
+ expect(dist).toBeCloseTo(3, TOLERANCE);
+ });
+
+ it("should return negative distance for point on opposite side", () => {
+ const p: Inputs.Base.Point3 = [5, 5, -2];
+ const dist = meshBitByBit.signedDistanceToPlane({ point: p, plane: xyPlane });
+ expect(dist).toBeCloseTo(-2, TOLERANCE);
+ });
+
+ it("should work for offset planes", () => {
+ const p: Inputs.Base.Point3 = [1, 1, 7];
+ const dist = meshBitByBit.signedDistanceToPlane({ point: p, plane: xyPlaneOffset });
+ expect(dist).toBeCloseTo(2, TOLERANCE);
+ });
+
+ it("should work for slanted planes", () => {
+ const p: Inputs.Base.Point3 = [1, 1, 1];
+ const dist = meshBitByBit.signedDistanceToPlane({ point: p, plane: slantedPlane });
+ expect(dist).toBeCloseTo(Math.sqrt(3), TOLERANCE);
+ });
+ });
+
+ describe("calculateTrianglePlane", () => {
+ it("should calculate plane for simple XY triangle", () => {
+ const plane = meshBitByBit.calculateTrianglePlane({ triangle: triXY1 });
+ expectPlaneCloseTo(plane, xyPlane);
+ });
+
+ it("should calculate plane for offset XY triangle", () => {
+ const plane = meshBitByBit.calculateTrianglePlane({ triangle: triXYOffset });
+ expectPlaneCloseTo(plane, xyPlaneOffset);
+ });
+
+ it("should calculate plane for XZ triangle", () => {
+ const plane = meshBitByBit.calculateTrianglePlane({ triangle: triXZ });
+ const expectedPlane: Inputs.Base.TrianglePlane3 = { normal: [0, -1, 0], d: 0 };
+ expectPlaneCloseTo(plane, expectedPlane);
+ });
+
+ it("should calculate plane for slanted triangle", () => {
+ const plane = meshBitByBit.calculateTrianglePlane({ triangle: triSlanted });
+ const expectedNormal = vector.normalized({ vector: [1, 1, 1] }) as Inputs.Base.Vector3;
+ const expectedD = vector.dot({ first: expectedNormal, second: [1, 0, 0] });
+ const expectedPlane: Inputs.Base.TrianglePlane3 = { normal: expectedNormal, d: expectedD };
+ expectPlaneCloseTo(plane, expectedPlane);
+ });
+
+ it("should return undefined for degenerate collinear triangle", () => {
+ const plane = meshBitByBit.calculateTrianglePlane({ triangle: triDegenerateCollinear });
+ expect(plane).toBeUndefined();
+ });
+
+ it("should return undefined for degenerate coincident triangle", () => {
+ const plane = meshBitByBit.calculateTrianglePlane({ triangle: triDegenerateCoincident });
+ expect(plane).toBeUndefined();
+ });
+
+ it("should return undefined for triangle degenerate within tolerance", () => {
+ const tolerance = 1e-7;
+ const smallDist = tolerance * 0.1; // Make it smaller than tolerance
+ const triDegenWithinTol: Inputs.Base.Triangle3 = [[0,0,0], [1,0,0], [2, smallDist, 0]];
+ const planeDegen = meshBitByBit.calculateTrianglePlane({ triangle: triDegenWithinTol, tolerance: tolerance });
+ expect(planeDegen).toBeUndefined(); // Expect undefined because it IS degenerate within tolerance
+ });
+
+ it("should calculate plane for triangle near-degenerate but outside tolerance", () => {
+ const tolerance = 1e-7;
+ const deviation = tolerance;
+ const triBarelyValid: Inputs.Base.Triangle3 = [[0,0,0], [1,0,0], [2, deviation, 0]];
+ const plane = meshBitByBit.calculateTrianglePlane({ triangle: triBarelyValid, tolerance: tolerance });
+
+ expect(plane).toBeDefined();
+ if (plane) {
+ const normalMagnitude = Math.sign(plane.normal[2]);
+ expectPointCloseTo(plane.normal, [0, 0, 1 * normalMagnitude], TOLERANCE);
+ expect(plane.d).toBeCloseTo(0, TOLERANCE);
+ }
+ });
+
+ it("should return undefined for the second degenerate case within tolerance", () => {
+ const degenTol = 1e-3;
+ const triDegenWithinTol2: Inputs.Base.Triangle3 = [[0,0,0], [1,0,0], [2, degenTol * 0.1, 0]];
+ const planeDegen = meshBitByBit.calculateTrianglePlane({ triangle: triDegenWithinTol2, tolerance: degenTol });
+ expect(planeDegen).toBeUndefined();
+ });
+ });
+
+ describe("triangleTriangleIntersection", () => {
+ it("should return undefined for far-apart triangles", () => {
+ const triFar: Inputs.Base.Triangle3 = [[10, 10, 10], [11, 10, 10], [10, 11, 10]];
+ const result = meshBitByBit.triangleTriangleIntersection({ triangle1: triXY1, triangle2: triFar });
+ expect(result).toBeUndefined();
+ });
+
+ it("should return undefined for parallel, non-coplanar triangles", () => {
+ const result = meshBitByBit.triangleTriangleIntersection({ triangle1: triXY1, triangle2: triXYOffset });
+ expect(result).toBeUndefined();
+ });
+
+ it("should return undefined if one triangle is fully \"above\" the others plane", () => {
+ const triPlaneSep: Inputs.Base.Triangle3 = [[-1, 0, 0], [-1, 1, 0], [-1, 0, 1]];
+ const triPositiveX: Inputs.Base.Triangle3 = [[1, 0, 0], [1, 1, 0], [1, 0, 1]];
+ const result = meshBitByBit.triangleTriangleIntersection({ triangle1: triPlaneSep, triangle2: triPositiveX });
+ expect(result).toBeUndefined();
+ });
+
+ it("should return undefined for coplanar triangles (explicitly not handled)", () => {
+ const result = meshBitByBit.triangleTriangleIntersection({ triangle1: triXY1, triangle2: triXY2 });
+ expect(result).toBeUndefined();
+ });
+
+ it("should return undefined for degenerate input triangles", () => {
+ const result1 = meshBitByBit.triangleTriangleIntersection({ triangle1: triDegenerateCollinear, triangle2: triXY1 });
+ const result2 = meshBitByBit.triangleTriangleIntersection({ triangle1: triXY1, triangle2: triDegenerateCoincident });
+ expect(result1).toBeUndefined();
+ expect(result2).toBeUndefined();
+ });
+
+ it("should return undefined for triangles touching at an edge", () => {
+ const triTouchingEdge: Inputs.Base.Triangle3 = [[1, 0, 0], [1, 1, 0], [0, 0, 0]];
+ const result = meshBitByBit.triangleTriangleIntersection({ triangle1: triXY1, triangle2: triTouchingEdge });
+ expect(result).toBeUndefined();
+ });
+
+ it("should return undefined for triangles touching at a vertex", () => {
+ const triTouchingVertex: Inputs.Base.Triangle3 = [[0, 0, 0], [-1, 0, 0], [0, -1, 0]];
+ const result = meshBitByBit.triangleTriangleIntersection({ triangle1: triXY1, triangle2: triTouchingVertex });
+ expect(result).toBeUndefined();
+ });
+
+ it("should return correct segment for simple orthogonal intersection (XY and XZ)", () => {
+ const expectedSegment: Inputs.Base.Segment3 = [[0, 0, 0], [1, 0, 0]];
+ const result = meshBitByBit.triangleTriangleIntersection({ triangle1: triXY1, triangle2: triXZ });
+ expectSegmentCloseTo(result, expectedSegment);
+ });
+
+ it("should return correct segment for shifted orthogonal intersection", () => {
+ const triXZ_Shifted: Inputs.Base.Triangle3 = [[0.5, 0, 0], [1.5, 0, 0], [0.5, 0, 1]]; // Y=0 plane, X from 0.5 to 1.5
+ const expectedSegment: Inputs.Base.Segment3 = [[0.5, 0, 0], [1.0, 0, 0]];
+ const result = meshBitByBit.triangleTriangleIntersection({ triangle1: triXY1, triangle2: triXZ_Shifted });
+ expectSegmentCloseTo(result, expectedSegment);
+ });
+
+ it("should return correct segment for piercing intersection", () => {
+ const triPiercing: Inputs.Base.Triangle3 = [[1, -1, -1], [1, 1, 1], [1, 3, -1]]; // On X=1 plane
+ const expectedSegment: Inputs.Base.Segment3 = [[1, 0, 0], [1, 1, 0]];
+ const result = meshBitByBit.triangleTriangleIntersection({ triangle1: [[0, 0, 0], [2, 0, 0], [0, 2, 0]], triangle2: triPiercing });
+ expectSegmentCloseTo(result, expectedSegment);
+ });
+
+ });
+
+ // Cube 1 centered at origin, side 2 (-1 to 1)
+ const cube1Tris: Inputs.Base.Triangle3[] = [
+ [[1, -1, -1], [1, 1, -1], [1, 1, 1]], [[1, -1, -1], [1, 1, 1], [1, -1, 1]],
+ [[-1, -1, -1], [-1, -1, 1], [-1, 1, 1]], [[-1, -1, -1], [-1, 1, 1], [-1, 1, -1]],
+ [[-1, -1, 1], [1, -1, 1], [1, 1, 1]], [[-1, -1, 1], [1, 1, 1], [-1, 1, 1]],
+ [[-1, -1, -1], [-1, 1, -1], [1, 1, -1]], [[-1, -1, -1], [1, 1, -1], [1, -1, -1]],
+ [[-1, 1, -1], [-1, 1, 1], [1, 1, 1]], [[-1, 1, -1], [1, 1, 1], [1, 1, -1]],
+ [[-1, -1, -1], [1, -1, -1], [1, -1, 1]], [[-1, -1, -1], [1, -1, 1], [-1, -1, 1]],
+ ];
+ // Cube 2 centered at (1.5, 0, 0), side 2 (0.5 to 2.5) - Intersects cube1
+ const cube2Tris: Inputs.Base.Triangle3[] = cube1Tris.map(tri => tri.map(p => [p[0] + 1.5, p[1], p[2]]) as Inputs.Base.Triangle3);
+ // Non-intersecting cube
+ const cube3Tris: Inputs.Base.Triangle3[] = cube1Tris.map(tri => tri.map(p => [p[0] + 5, p[1], p[2]]) as Inputs.Base.Triangle3);
+
+ describe("meshMeshIntersectionSegments", () => {
+
+
+ it("should return an empty array for non-intersecting meshes", () => {
+ const result = meshBitByBit.meshMeshIntersectionSegments({ mesh1: cube1Tris, mesh2: cube3Tris, tolerance: TOLERANCE });
+ expect(result).toEqual([]);
+ });
+
+ it("should return intersection segments for intersecting meshes", () => {
+ const result = meshBitByBit.meshMeshIntersectionSegments({ mesh1: cube1Tris, mesh2: cube2Tris });
+ expect(result.length).toBe(24);
+ });
+
+ it("should handle empty mesh inputs", () => {
+ const result1 = meshBitByBit.meshMeshIntersectionSegments({ mesh1: [], mesh2: cube2Tris });
+ const result2 = meshBitByBit.meshMeshIntersectionSegments({ mesh1: cube1Tris, mesh2: [] });
+ const result3 = meshBitByBit.meshMeshIntersectionSegments({ mesh1: [], mesh2: [] });
+ expect(result1).toEqual([]);
+ expect(result2).toEqual([]);
+ expect(result3).toEqual([]);
+ });
+ });
+ describe("meshMeshIntersectionPolylines", () => {
+ const intersectionTolerance = 1e-6;
+
+ it("should return an empty array for non-intersecting meshes", () => {
+ const result = meshBitByBit.meshMeshIntersectionPolylines({ mesh1: cube1Tris, mesh2: cube3Tris, tolerance: intersectionTolerance });
+ expect(result).toEqual([]);
+ });
+
+ it("should return intersection polylines for intersecting meshes", () => {
+ const result = meshBitByBit.meshMeshIntersectionPolylines({ mesh1: cube1Tris, mesh2: cube2Tris });
+ expect(result.length).toBe(4);
+ });
+
+ it("should handle empty mesh inputs", () => {
+ const result1 = meshBitByBit.meshMeshIntersectionPolylines({ mesh1: [], mesh2: cube2Tris, tolerance: intersectionTolerance });
+ const result2 = meshBitByBit.meshMeshIntersectionPolylines({ mesh1: cube1Tris, mesh2: [], tolerance: intersectionTolerance });
+ const result3 = meshBitByBit.meshMeshIntersectionPolylines({ mesh1: [], mesh2: [], tolerance: intersectionTolerance });
+ expect(result1).toEqual([]);
+ expect(result2).toEqual([]);
+ expect(result3).toEqual([]);
+ });
+
+ });
+});
+
diff --git a/packages/dev/base/lib/api/services/mesh.ts b/packages/dev/base/lib/api/services/mesh.ts
new file mode 100644
index 00000000..9155cf58
--- /dev/null
+++ b/packages/dev/base/lib/api/services/mesh.ts
@@ -0,0 +1,260 @@
+import * as Inputs from "../inputs";
+import { Polyline } from "./polyline";
+import { Vector } from "./vector";
+
+/**
+ * Contains various mesh helper methods that are not necessarily present in higher level CAD kernels that bitbybit is using.
+ */
+export class MeshBitByBit {
+ constructor(private readonly vector: Vector, private readonly polyline: Polyline) { }
+
+ /**
+ * Computes the signed distance from a point to a plane.
+ * @param inputs a point and a plane
+ * @returns signed distance
+ * @group base
+ * @shortname signed dist to plane
+ * @drawable false
+ */
+ signedDistanceToPlane(inputs: Inputs.Mesh.SignedDistanceFromPlaneToPointDto): number {
+ return this.vector.dot({ first: inputs.plane.normal, second: inputs.point }) - inputs.plane.d;
+ }
+
+ /**
+ * Calculates the triangle plane from triangle.
+ * @param inputs triangle and tolerance
+ * @returns triangle plane
+ * @group traingle
+ * @shortname triangle plane
+ * @drawable false
+ */
+ calculateTrianglePlane(inputs: Inputs.Mesh.TriangleToleranceDto): Inputs.Base.TrianglePlane3 | undefined {
+ const EPSILON_SQ = (inputs.tolerance || 1e-7) ** 2;
+
+ const edge1 = this.vector.sub({ first: inputs.triangle[1], second: inputs.triangle[0] });
+ const edge2 = this.vector.sub({ first: inputs.triangle[2], second: inputs.triangle[0] });
+ const normal = this.vector.cross({ first: edge1, second: edge2 });
+
+ if (this.vector.lengthSq({ vector: normal as Inputs.Base.Vector3 }) < EPSILON_SQ) {
+ return undefined; // Degenerate triangle
+ }
+
+ // Defensive copy if normalize modifies in-place, otherwise remove .slice()
+ const normalizedNormal = this.vector.normalized({ vector: normal }) as Inputs.Base.Vector3;
+ const d = this.vector.dot({ first: normalizedNormal, second: inputs.triangle[0] });
+ return { normal: normalizedNormal, d: d };
+ }
+
+ /**
+ * Calculates the intersection of two triangles.
+ * @param inputs first triangle, second triangle, and tolerance
+ * @returns intersection segment or undefined if no intersection
+ * @group traingle
+ * @shortname triangle-triangle int
+ * @drawable false
+ */
+ triangleTriangleIntersection(inputs: Inputs.Mesh.TriangleTriangleToleranceDto): Inputs.Base.Segment3 | undefined {
+ const t1 = inputs.triangle1;
+ const t2 = inputs.triangle2;
+ const EPSILON = inputs.tolerance || 1e-7;
+ const p1 = t1[0], p2 = t1[1], p3 = t1[2];
+ const q1 = t2[0], q2 = t2[1], q3 = t2[2];
+
+ const plane1 = this.calculateTrianglePlane({ triangle: t1, tolerance: EPSILON });
+ const plane2 = this.calculateTrianglePlane({ triangle: t2, tolerance: EPSILON });
+
+ if (!plane1 || !plane2) return undefined;
+
+ const distQ_Plane1 = [
+ this.signedDistanceToPlane({ point: q1, plane: plane1 }),
+ this.signedDistanceToPlane({ point: q2, plane: plane1 }),
+ this.signedDistanceToPlane({ point: q3, plane: plane1 }),
+ ];
+
+ if ((distQ_Plane1[0] > EPSILON && distQ_Plane1[1] > EPSILON && distQ_Plane1[2] > EPSILON) ||
+ (distQ_Plane1[0] < -EPSILON && distQ_Plane1[1] < -EPSILON && distQ_Plane1[2] < -EPSILON)) {
+ return undefined;
+ }
+
+ const distP_Plane2 = [
+ this.signedDistanceToPlane({ point: p1, plane: plane2 }),
+ this.signedDistanceToPlane({ point: p2, plane: plane2 }),
+ this.signedDistanceToPlane({ point: p3, plane: plane2 }),
+ ];
+
+ if ((distP_Plane2[0] > EPSILON && distP_Plane2[1] > EPSILON && distP_Plane2[2] > EPSILON) ||
+ (distP_Plane2[0] < -EPSILON && distP_Plane2[1] < -EPSILON && distP_Plane2[2] < -EPSILON)) {
+ return undefined;
+ }
+
+ const allDistPZero = distP_Plane2.every(d => Math.abs(d) < EPSILON);
+ const allDistQZero = distQ_Plane1.every(d => Math.abs(d) < EPSILON);
+
+ if (allDistPZero && allDistQZero) {
+ // console.warn("Coplanar case detected, not handled.");
+ return undefined; // Explicitly not handling coplanar intersection areas
+ }
+
+ const lineDir = this.vector.cross({ first: plane1.normal, second: plane2.normal }) as Inputs.Base.Vector3;
+ const det = this.vector.dot({ first: lineDir, second: lineDir }); // det = |lineDir|^2
+
+ if (det < EPSILON * EPSILON) {
+ // console.warn("Planes are parallel or near parallel.");
+ return undefined; // Planes parallel, no line intersection (coplanar case handled above)
+ }
+
+ // --- Calculate Interval Projections ---
+
+ // Store the 3D points that define the intervals on the line
+ const t1_intersection_points_3d: Inputs.Base.Point3[] = [];
+ const t2_intersection_points_3d: Inputs.Base.Point3[] = [];
+
+ const edges1: Inputs.Base.Segment3[] = [[p1, p2], [p2, p3], [p3, p1]];
+ const dists1 = distP_Plane2;
+ for (let i = 0; i < 3; ++i) {
+ const u = edges1[i][0];
+ const v = edges1[i][1];
+ const du = dists1[i];
+ const dv = dists1[(i + 1) % 3];
+
+ if (Math.abs(du) < EPSILON) t1_intersection_points_3d.push(u); // Start vertex is on plane2
+ // Removed the redundant check for dv here, handled by next edge start
+
+ if ((du * dv) < 0 && Math.abs(du - dv) > EPSILON) { // Edge crosses plane2
+ const t = du / (du - dv);
+ t1_intersection_points_3d.push(this.computeIntersectionPoint(u, v, t));
+ }
+ }
+
+ const edges2: Inputs.Base.Segment3[] = [[q1, q2], [q2, q3], [q3, q1]];
+ const dists2 = distQ_Plane1;
+ for (let i = 0; i < 3; ++i) {
+ const u = edges2[i][0];
+ const v = edges2[i][1];
+ const du = dists2[i];
+ const dv = dists2[(i + 1) % 3];
+
+ if (Math.abs(du) < EPSILON) t2_intersection_points_3d.push(u); // Start vertex is on plane1
+ // Removed redundant check for dv
+
+ if ((du * dv) < 0 && Math.abs(du - dv) > EPSILON) { // Edge crosses plane1
+ const t = du / (du - dv);
+ t2_intersection_points_3d.push(this.computeIntersectionPoint(u, v, t));
+ }
+ }
+
+ // We expect exactly two points for each triangle in the standard piercing case.
+ // Handle potential duplicates or edge cases if more points are generated (e.g., edge lies on plane)
+ // A simple check for the common case:
+ if (t1_intersection_points_3d.length < 2 || t2_intersection_points_3d.length < 2) {
+ // This can happen if triangles touch at a vertex or edge without crossing planes,
+ // or due to numerical precision near edges/vertices.
+ // console.log("Intersection appears to be edge/vertex contact or numerical issue.");
+ return undefined; // Treat touch as no intersection segment for now
+ }
+
+ // Calculate a robust origin ON the intersection line
+ const n1 = plane1.normal;
+ const n2 = plane2.normal;
+ const d1 = plane1.d;
+ const d2 = plane2.d;
+ // Point P = ( (d1 * N2 - d2 * N1) x D ) / (D dot D)
+ const term1 = this.vector.mul({ vector: n2, scalar: d1 });
+ const term2 = this.vector.mul({ vector: n1, scalar: d2 });
+ const termSub = this.vector.sub({ first: term1, second: term2 });
+ const crossTerm = this.vector.cross({ first: termSub, second: lineDir });
+ const lineOrigin = this.vector.mul({ vector: crossTerm, scalar: 1.0 / det }) as Inputs.Base.Point3;
+
+
+ // Project the 3D intersection points onto the lineDir, relative to lineOrigin
+ // param = dot(point - lineOrigin, lineDir)
+ const t1_params = t1_intersection_points_3d.map(p =>
+ this.vector.dot({ first: this.vector.sub({ first: p, second: lineOrigin }), second: lineDir })
+ );
+ const t2_params = t2_intersection_points_3d.map(p =>
+ this.vector.dot({ first: this.vector.sub({ first: p, second: lineOrigin }), second: lineDir })
+ );
+
+ // Find the intervals
+ const t1Interval = [Math.min(...t1_params), Math.max(...t1_params)];
+ const t2Interval = [Math.min(...t2_params), Math.max(...t2_params)];
+
+ // Find the overlap of the two intervals
+ const intersectionMinParam = Math.max(t1Interval[0], t2Interval[0]);
+ const intersectionMaxParam = Math.min(t1Interval[1], t2Interval[1]);
+
+ // Check if the overlap is valid
+ if (intersectionMinParam < intersectionMaxParam - (EPSILON * det)) { // Let's use scaled epsilon for robustness against small det values.
+ // Convert the final parameters back to 3D points using the lineOrigin
+ // P = lineOrigin + dir * (param / det)
+ const point1 = this.vector.add({ first: lineOrigin, second: this.vector.mul({ vector: lineDir, scalar: intersectionMinParam / det }) }) as Inputs.Base.Point3;
+ const point2 = this.vector.add({ first: lineOrigin, second: this.vector.mul({ vector: lineDir, scalar: intersectionMaxParam / det }) }) as Inputs.Base.Point3;
+
+ // Check if the resulting segment has non-zero length
+ const segVec = this.vector.sub({ first: point1, second: point2 });
+ if (this.vector.lengthSq({ vector: segVec as Inputs.Base.Vector3 }) > EPSILON * EPSILON) {
+ return [point1, point2];
+ } else {
+ return undefined; // Degenerate segment
+ }
+ } else {
+ return undefined; // Intervals do not overlap
+ }
+ }
+
+ /**
+ * Computes the intersection segments of two meshes.
+ * @param inputs first mesh, second mesh, and tolerance
+ * @returns array of intersection segments
+ * @group mesh
+ * @shortname mesh-mesh int segments
+ * @drawable false
+ */
+ meshMeshIntersectionSegments(inputs: Inputs.Mesh.MeshMeshToleranceDto): Inputs.Base.Segment3[] {
+ const mesh1 = inputs.mesh1;
+ const mesh2 = inputs.mesh2;
+ const intersectionSegments: Inputs.Base.Segment3[] = [];
+
+ for (let i = 0; i < mesh1.length; ++i) {
+ for (let j = 0; j < mesh2.length; ++j) {
+ const triangle1 = mesh1[i];
+ const triangle2 = mesh2[j];
+
+ const segment = this.triangleTriangleIntersection({ triangle1, triangle2, tolerance: inputs.tolerance });
+
+ if (segment) {
+ intersectionSegments.push(segment);
+ }
+ }
+ }
+
+ return intersectionSegments;
+ }
+
+ /**
+ * Computes the intersection polylines of two meshes.
+ * @param inputs first mesh, second mesh, and tolerance
+ * @returns array of intersection polylines
+ * @group mesh
+ * @shortname mesh-mesh int polylines
+ * @drawable true
+ */
+ meshMeshIntersectionPolylines(inputs: Inputs.Mesh.MeshMeshToleranceDto): Inputs.Base.Polyline3[] {
+ const segments = this.meshMeshIntersectionSegments(inputs);
+ return this.polyline.sortSegmentsIntoPolylines({ segments, tolerance: inputs.tolerance });
+ }
+
+ private computeIntersectionPoint(u: Inputs.Base.Point3, v: Inputs.Base.Point3, t: number) {
+ return this.vector.add(
+ {
+ first: u,
+ second: this.vector.mul({
+ vector: this.vector.sub({
+ first: v,
+ second: u
+ }),
+ scalar: t
+ })
+ }) as Inputs.Base.Point3;
+ }
+}
diff --git a/packages/dev/base/lib/api/services/point.test.ts b/packages/dev/base/lib/api/services/point.test.ts
new file mode 100644
index 00000000..339a1482
--- /dev/null
+++ b/packages/dev/base/lib/api/services/point.test.ts
@@ -0,0 +1,617 @@
+import { GeometryHelper } from "./geometry-helper";
+import { MathBitByBit } from "./math";
+import { Point } from "./point";
+import { Transforms } from "./transforms";
+import { Vector } from "./vector";
+import * as Inputs from "../inputs";
+
+describe("Point unit tests", () => {
+ let geometryHelper: GeometryHelper;
+ let math: MathBitByBit;
+ let vector: Vector;
+ let point: Point;
+ let transforms: Transforms;
+
+ beforeAll(() => {
+ geometryHelper = new GeometryHelper();
+ math = new MathBitByBit();
+ vector = new Vector(math, geometryHelper);
+ transforms = new Transforms(vector, math);
+ point = new Point(geometryHelper, transforms, vector);
+ });
+
+ const TOLERANCE = 1e-7;
+
+ describe("Point Class Unit Tests (Integration)", () => {
+
+ describe("transformPoint", () => {
+ it("should translate a point correctly", () => {
+ const p: Inputs.Base.Point3 = [1, 2, 3];
+ const translationVec: Inputs.Base.Vector3 = [10, -5, 2];
+ const transformation = transforms.translationXYZ({ translation: translationVec });
+ const result = point.transformPoint({ point: p, transformation });
+ expectPointCloseTo(result, [11, -3, 5]);
+ });
+
+ it("should rotate a point around Z axis", () => {
+ const p: Inputs.Base.Point3 = [1, 0, 5];
+ const transformation = transforms.rotationCenterAxis({
+ center: [0, 0, 0],
+ axis: [0, 0, 1],
+ angle: 90
+ });
+ const result = point.transformPoint({ point: p, transformation });
+ expectPointCloseTo(result, [0, 1, 5]);
+ });
+
+ it("should scale a point relative to origin", () => {
+ const p: Inputs.Base.Point3 = [2, 3, 4];
+ const transformation = transforms.scaleCenterXYZ({
+ center: [0, 0, 0],
+ scaleXyz: [2, 0.5, 1]
+ });
+ const result = point.transformPoint({ point: p, transformation });
+ expectPointCloseTo(result, [4, 1.5, 4]);
+ });
+ });
+
+ describe("transformPoints", () => {
+ it("should translate multiple points correctly", () => {
+ const pts: Inputs.Base.Point3[] = [[1, 2, 3], [10, 10, 10]];
+ const translationVec: Inputs.Base.Vector3 = [5, -5, 0];
+ const transformation = transforms.translationXYZ({ translation: translationVec });
+ const result = point.transformPoints({ points: pts, transformation });
+ expectPointsCloseTo(result, [[6, -3, 3], [15, 5, 10]]);
+ });
+
+ it("should rotate multiple points", () => {
+ const pts: Inputs.Base.Point3[] = [[1, 0, 0], [0, 1, 5]];
+ const transformation = transforms.rotationCenterAxis({
+ center: [0, 0, 0],
+ axis: [0, 0, 1],
+ angle: -90
+ });
+ const result = point.transformPoints({ points: pts, transformation });
+ expectPointsCloseTo(result, [[0, -1, 0], [1, 0, 5]]);
+ });
+
+ it("should handle empty points array", () => {
+ const pts: Inputs.Base.Point3[] = [];
+ const transformation = transforms.identity();
+ const result = point.transformPoints({ points: pts, transformation: [transformation] });
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe("transformsForPoints", () => {
+ it("should apply individual transforms to corresponding points", () => {
+ const pts: Inputs.Base.Point3[] = [[1, 0, 0], [5, 5, 5]];
+ const transformations = [
+ transforms.rotationCenterAxis({ center: [0, 0, 0], axis: [0, 0, 1], angle: 90 }),
+ transforms.translationXYZ({ translation: [1, 1, 1] })
+ ];
+ const result = point.transformsForPoints({ points: pts, transformation: transformations });
+ expectPointsCloseTo(result, [[0, 1, 0], [6, 6, 6]]);
+ });
+
+ it("should throw an error if points and transformations lengths differ", () => {
+ const pts: Inputs.Base.Point3[] = [[1, 1, 1]];
+ const transformations = [[transforms.identity()], [transforms.identity()]];
+ expect(() => {
+ point.transformsForPoints({ points: pts, transformation: transformations });
+ }).toThrow("You must provide equal nr of points and transformations");
+ });
+
+ it("should handle empty arrays", () => {
+ const pts: Inputs.Base.Point3[] = [];
+ const transformations: any[] = [];
+ const result = point.transformsForPoints({ points: pts, transformation: transformations });
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe("translatePoints", () => {
+ it("should translate points by a single vector", () => {
+ const pts: Inputs.Base.Point3[] = [[0, 0, 0], [1, -1, 2]];
+ const translation: Inputs.Base.Vector3 = [10, 20, 30];
+ const result = point.translatePoints({ points: pts, translation });
+ expectPointsCloseTo(result, [[10, 20, 30], [11, 19, 32]]);
+ });
+ });
+
+ describe("translatePointsWithVectors", () => {
+ it("should apply individual translation vectors to corresponding points", () => {
+ const pts: Inputs.Base.Point3[] = [[1, 1, 1], [5, 5, 5]];
+ const translations: Inputs.Base.Vector3[] = [[10, 0, 0], [0, 20, 0]];
+ const result = point.translatePointsWithVectors({ points: pts, translations });
+ expectPointsCloseTo(result, [[11, 1, 1], [5, 25, 5]]);
+ });
+
+ it("should throw an error if points and translations lengths differ", () => {
+ const pts: Inputs.Base.Point3[] = [[1, 1, 1]];
+ const translations: Inputs.Base.Vector3[] = [[10, 0, 0], [0, 20, 0]];
+ expect(() => {
+ point.translatePointsWithVectors({ points: pts, translations });
+ }).toThrow("You must provide equal nr of points and translations");
+ });
+ });
+
+ describe("translateXYZPoints", () => {
+ it("should translate points by individual x, y, z values", () => {
+ const pts: Inputs.Base.Point3[] = [[0, 0, 0], [1, -1, 2]];
+ const x = -1, y = 2, z = -3;
+ const result = point.translateXYZPoints({ points: pts, x, y, z });
+ expectPointsCloseTo(result, [[-1, 2, -3], [0, 1, -1]]);
+ });
+ });
+
+ describe("scalePointsCenterXYZ", () => {
+ it("should scale points relative to a center", () => {
+ const pts: Inputs.Base.Point3[] = [[2, 2, 2], [0, 0, 0]];
+ const center: Inputs.Base.Point3 = [1, 1, 1]; // Center of scaling
+ const scaleXyz: Inputs.Base.Vector3 = [2, 3, 0.5];
+ const result = point.scalePointsCenterXYZ({ points: pts, center, scaleXyz });
+ expectPointsCloseTo(result, [[3, 4, 1.5], [-1, -2, 0.5]]);
+ });
+ });
+
+ describe("rotatePointsCenterAxis", () => {
+ it("should rotate points around a center and axis", () => {
+ const pts: Inputs.Base.Point3[] = [[2, 1, 5], [1, 1, 0]]; // P1 is offset from center
+ const center: Inputs.Base.Point3 = [1, 1, 5]; // Center of rotation
+ const axis: Inputs.Base.Vector3 = [0, 0, 1]; // Z-axis
+ const angle = 90;
+ const result = point.rotatePointsCenterAxis({ points: pts, center, axis, angle });
+ expectPointsCloseTo(result, [[1, 2, 5], [1, 1, 0]]);
+ });
+ });
+
+ describe("boundingBoxOfPoints", () => {
+ // These tests don't rely on external dependencies, same as before
+ it("should calculate the correct bounding box for multiple points", () => {
+ const points: Inputs.Base.Point3[] = [[1, 2, 3], [4, -1, 6], [0, 5, -2]];
+ const expectedBBox: Inputs.Base.BoundingBox = {
+ min: [0, -1, -2],
+ max: [4, 5, 6],
+ center: [2, 2, 2],
+ width: 4,
+ height: 6,
+ length: 8,
+ };
+ const result = point.boundingBoxOfPoints({ points });
+ expect(result.min).toEqual(expectedBBox.min);
+ expect(result.max).toEqual(expectedBBox.max);
+ expectPointCloseTo(result.center, expectedBBox.center);
+ expect(result.width).toBeCloseTo(expectedBBox.width);
+ expect(result.height).toBeCloseTo(expectedBBox.height);
+ expect(result.length).toBeCloseTo(expectedBBox.length);
+ });
+
+ it("should return a zero-dimension bounding box for a single point", () => {
+ const points: Inputs.Base.Point3[] = [[5, 5, 5]];
+ const expectedBBox: Inputs.Base.BoundingBox = {
+ min: [5, 5, 5],
+ max: [5, 5, 5],
+ center: [5, 5, 5],
+ width: 0,
+ height: 0,
+ length: 0,
+ };
+ const result = point.boundingBoxOfPoints({ points });
+ expect(result).toEqual(expectedBBox);
+ });
+
+ it("should handle empty points array", () => {
+ const points: Inputs.Base.Point3[] = [];
+ const result = point.boundingBoxOfPoints({ points });
+ expect(result.min).toEqual([Infinity, Infinity, Infinity]);
+ expect(result.max).toEqual([-Infinity, -Infinity, -Infinity]);
+ expect(result.center).toEqual([NaN, NaN, NaN]);
+ expect(result.width).toEqual(-Infinity); // max - min = -Inf - Inf = -Inf
+ expect(result.height).toEqual(-Infinity);
+ expect(result.length).toEqual(-Infinity);
+ });
+ });
+
+ describe("closestPointFromPoints methods", () => {
+ // These tests rely only on distance, which is internal or uses basic math
+ const sourcePoint: Inputs.Base.Point3 = [0, 0, 0];
+ const targetPoints: Inputs.Base.Point3[] = [
+ [10, 0, 0], // dist 10
+ [0, 5, 0], // dist 5 - closest
+ [0, 0, -7], // dist 7
+ [3, 4, 0], // dist 5 - first one at index 1 (-> 2) should be picked
+ ];
+ const expectedClosestPoint: Inputs.Base.Point3 = [0, 5, 0];
+ const expectedClosestIndex = 2; // 1-based index
+ const expectedClosestDistance = 5;
+
+ it("closestPointFromPointsDistance should return the minimum distance", () => {
+ const distance = point.closestPointFromPointsDistance({ point: sourcePoint, points: targetPoints });
+ expect(distance).toBeCloseTo(expectedClosestDistance);
+ });
+
+ it("closestPointFromPointsIndex should return the 1-based index of the closest point", () => {
+ const index = point.closestPointFromPointsIndex({ point: sourcePoint, points: targetPoints });
+ expect(index).toBe(expectedClosestIndex);
+ });
+
+ it("closestPointFromPoints should return the coordinates of the closest point", () => {
+ const closest = point.closestPointFromPoints({ point: sourcePoint, points: targetPoints });
+ expect(closest).toEqual(expectedClosestPoint);
+ });
+
+ it("should handle empty target points list (check internal method behavior)", () => {
+ const testFuncDistance = () => point.closestPointFromPointsDistance({ point: sourcePoint, points: [] });
+ const testFuncIndex = () => point.closestPointFromPointsIndex({ point: sourcePoint, points: [] });
+ const testFuncPoint = () => point.closestPointFromPoints({ point: sourcePoint, points: [] });
+ expect(testFuncDistance()).toBe(Number.MAX_SAFE_INTEGER);
+ expect(testFuncIndex()).toBeNaN();
+ expect(testFuncPoint()).toBeUndefined();
+ });
+ });
+
+ describe("distance", () => {
+ it("should calculate the distance between two points", () => {
+ const p1: Inputs.Base.Point3 = [1, 2, 3];
+ const p2: Inputs.Base.Point3 = [4, -2, 15]; // dx=3, dy=-4, dz=12
+ const dist = point.distance({ startPoint: p1, endPoint: p2 });
+ expect(dist).toBeCloseTo(13);
+ });
+
+ it("should return 0 for coincident points", () => {
+ const p1: Inputs.Base.Point3 = [5, 5, 5];
+ const dist = point.distance({ startPoint: p1, endPoint: p1 });
+ expect(dist).toBeCloseTo(0);
+ });
+ });
+
+ describe("distancesToPoints", () => {
+ it("should calculate distances from one start point to multiple end points", () => {
+ const start: Inputs.Base.Point3 = [0, 0, 0];
+ const ends: Inputs.Base.Point3[] = [
+ [3, 4, 0], // dist 5
+ [0, 0, 1], // dist 1
+ [1, 1, 1] // dist sqrt(3) ~ 1.732
+ ];
+ const distances = point.distancesToPoints({ startPoint: start, endPoints: ends });
+ expect(distances.length).toBe(3);
+ expect(distances[0]).toBeCloseTo(5);
+ expect(distances[1]).toBeCloseTo(1);
+ expect(distances[2]).toBeCloseTo(Math.sqrt(3));
+ });
+
+ it("should return an empty array if end points list is empty", () => {
+ const start: Inputs.Base.Point3 = [0, 0, 0];
+ const ends: Inputs.Base.Point3[] = [];
+ const distances = point.distancesToPoints({ startPoint: start, endPoints: ends });
+ expect(distances).toEqual([]);
+ });
+ });
+
+ describe("multiplyPoint", () => {
+ it("should create an array with the specified number of identical points", () => {
+ const p: Inputs.Base.Point3 = [10, 20, 30];
+ const amount = 3;
+ const result = point.multiplyPoint({ point: p, amountOfPoints: amount });
+ expect(result).toHaveLength(amount);
+ expect(result).toEqual([
+ [10, 20, 30],
+ [10, 20, 30],
+ [10, 20, 30]
+ ]);
+ });
+
+ it("should return an empty array if amount is 0", () => {
+ const p: Inputs.Base.Point3 = [10, 20, 30];
+ const result = point.multiplyPoint({ point: p, amountOfPoints: 0 });
+ expect(result).toEqual([]);
+ });
+
+ it("should return an empty array if amount is negative (or handle as error)", () => {
+ const p: Inputs.Base.Point3 = [10, 20, 30];
+ const result = point.multiplyPoint({ point: p, amountOfPoints: -1 });
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe("getX, getY, getZ", () => {
+ const p: Inputs.Base.Point3 = [-1.5, 0, 99.9];
+ it("getX should return the first element", () => {
+ expect(point.getX({ point: p })).toBe(-1.5);
+ });
+ it("getY should return the second element", () => {
+ expect(point.getY({ point: p })).toBe(0);
+ });
+ it("getZ should return the third element", () => {
+ expect(point.getZ({ point: p })).toBe(99.9);
+ });
+ });
+
+ describe("averagePoint", () => {
+ it("should calculate the average of multiple points", () => {
+ const pts: Inputs.Base.Point3[] = [[1, 1, 1], [3, 5, -1], [5, 0, 6]];
+ // AvgX = (1+3+5)/3 = 9/3 = 3
+ // AvgY = (1+5+0)/3 = 6/3 = 2
+ // AvgZ = (1-1+6)/3 = 6/3 = 2
+ const result = point.averagePoint({ points: pts });
+ expectPointCloseTo(result, [3, 2, 2]);
+ });
+
+ it("should return the point itself if only one point is provided", () => {
+ const pts: Inputs.Base.Point3[] = [[10, 20, 30]];
+ const result = point.averagePoint({ points: pts });
+ expectPointCloseTo(result, [10, 20, 30]);
+ });
+
+ it("should return NaN components if points array is empty", () => {
+ const pts: Inputs.Base.Point3[] = [];
+ const result = point.averagePoint({ points: pts });
+ expect(result[0]).toBeNaN();
+ expect(result[1]).toBeNaN();
+ expect(result[2]).toBeNaN();
+ });
+ });
+
+ describe("pointXYZ", () => {
+ it("should create a 3D point array from x, y, z", () => {
+ const result = point.pointXYZ({ x: 1.1, y: -2.2, z: 3.3 });
+ expect(result).toEqual([1.1, -2.2, 3.3]);
+ });
+ });
+
+ describe("pointXY", () => {
+ it("should create a 2D point array from x, y", () => {
+ const result = point.pointXY({ x: 5, y: 10 });
+ expect(result).toEqual([5, 10]);
+ });
+ });
+
+ describe("spiral", () => {
+ it("should generate the specified number of points", () => {
+ const result = point.spiral({
+ phi: 1.618,
+ widening: 9,
+ radius: 10,
+ factor: 1,
+ numberPoints: 20
+ });
+ expect(result.length).toBe(20);
+ });
+
+ it("should generate points with Z=0", () => {
+ const result = point.spiral({ phi: 1.618, widening: 9, radius: 5, factor: 2, numberPoints: 5 });
+ expect(result.length).toBeGreaterThan(0);
+ result.forEach(p => {
+ expect(p[0]).not.toBeNaN();
+ expect(p[1]).not.toBeNaN();
+ expect(p[2]).toBe(0);
+ });
+ });
+
+ it("should handle step leading to division by zero in log (i=0)", () => {
+ const result = point.spiral({ phi: 1.618, widening: 9, radius: 1, factor: 1, numberPoints: 2 });
+ expect(result.length).toBe(2);
+ expect(result[0]).toEqual([0, 0, 0]);
+ expect(result[1][0]).not.toBeNaN();
+ expect(result[1][1]).not.toBeNaN();
+ });
+ });
+
+ describe("hexGrid", () => {
+ it("should generate the correct number of points", () => {
+ const nrX = 3, nrY = 4;
+ const result = point.hexGrid({ radiusHexagon: 1, nrHexagonsX: nrX, nrHexagonsY: nrY, orientOnCenter: false, pointsOnGround: false });
+ expect(result.length).toBe(nrX * nrY);
+ });
+
+ it("should generate points on XY plane by default", () => {
+ const result = point.hexGrid({ radiusHexagon: 1, nrHexagonsX: 2, nrHexagonsY: 2, orientOnCenter: false, pointsOnGround: false });
+ expect(result.length).toBe(4);
+ result.forEach(p => expect(p[2]).toBe(0)); // Z should be 0
+ });
+
+ it("should place points on XZ plane if pointsOnGround is true", () => {
+ const result = point.hexGrid({ radiusHexagon: 1, nrHexagonsX: 2, nrHexagonsY: 2, pointsOnGround: true, orientOnCenter: false });
+ expect(result.length).toBe(4);
+ result.forEach(p => expect(p[1]).toBe(0));
+ expect(result[0][2]).toBe(0);
+ if (result.length > 1) {
+ expect(result[1][2]).not.toBe(0);
+ }
+ });
+
+ it("should center the grid if orientOnCenter is true", () => {
+ const radius = 2;
+ const nrX = 3, nrY = 3;
+ const resultNoCenter = point.hexGrid({ radiusHexagon: radius, nrHexagonsX: nrX, nrHexagonsY: nrY, orientOnCenter: false, pointsOnGround: false });
+ const resultCenter = point.hexGrid({ radiusHexagon: radius, nrHexagonsX: nrX, nrHexagonsY: nrY, orientOnCenter: true, pointsOnGround: false });
+
+ let avgX = 0, avgY = 0, avgZ = 0;
+ resultCenter.forEach(p => { avgX += p[0]; avgY += p[1]; avgZ += p[2]; });
+ avgX /= resultCenter.length;
+ avgY /= resultCenter.length;
+ avgZ /= resultCenter.length;
+
+ expect(avgX).toBeCloseTo(0.577);
+ expect(avgY).toBeCloseTo(0);
+ expect(avgZ).toBeCloseTo(0);
+
+ expect(resultCenter).not.toEqual(resultNoCenter);
+ });
+ });
+
+ describe("removeConsecutiveDuplicates", () => {
+ it("should remove consecutive duplicate points within tolerance", () => {
+ const pts: Inputs.Base.Point3[] = [[0, 0, 0], [0, 0, 1e-9], [1, 1, 1], [1, 1, 1 + 1e-4], [2, 2, 2]];
+ const tolerance = 1e-5;
+ const result = point.removeConsecutiveDuplicates({ points: pts, tolerance, checkFirstAndLast: false });
+ expectPointsCloseTo(result, [[0, 0, 0], [1, 1, 1], [1, 1, 1 + 1e-4], [2, 2, 2]]);
+ });
+
+ it("should remove consecutive duplicate points with default tolerance", () => {
+ const pts: Inputs.Base.Point3[] = [[0, 0, 0], [0, 0, 1e-8], [1, 1, 1], [1, 1, 1 + 1e-3], [2, 2, 2]];
+ const result = point.removeConsecutiveDuplicates({ points: pts, tolerance: undefined, checkFirstAndLast: false });
+ expectPointsCloseTo(result, [[0, 0, 0], [1, 1, 1], [1, 1, 1 + 1e-3], [2, 2, 2]]);
+ });
+
+ it("should keep all points if no consecutive duplicates exist", () => {
+ const pts: Inputs.Base.Point3[] = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]];
+ const result = point.removeConsecutiveDuplicates({ points: pts, tolerance: 1e-5, checkFirstAndLast: false });
+ expectPointsCloseTo(result, pts);
+ });
+
+ it("should handle checkFirstAndLast correctly for open polyline", () => {
+ const pts: Inputs.Base.Point3[] = [[0, 0, 0], [1, 1, 1], [2, 2, 2], [0, 0, 1e-9]];
+ const result = point.removeConsecutiveDuplicates({ points: pts, checkFirstAndLast: true, tolerance: 1e-5 });
+ expectPointsCloseTo(result, [[0, 0, 0], [1, 1, 1], [2, 2, 2]]);
+ });
+
+ it("should handle checkFirstAndLast correctly for closed polyline (already duplicated)", () => {
+ const pts: Inputs.Base.Point3[] = [[0, 0, 0], [1, 1, 1], [2, 2, 2], [0, 0, 0]];
+ const result = point.removeConsecutiveDuplicates({ points: pts, checkFirstAndLast: true, tolerance: 1e-5 });
+ expectPointsCloseTo(result, [[0, 0, 0], [1, 1, 1], [2, 2, 2]]);
+ });
+
+ it("should handle checkFirstAndLast=false", () => {
+ const pts: Inputs.Base.Point3[] = [[0, 0, 0], [1, 1, 1], [2, 2, 2], [0, 0, 1e-9]]; // Last point close to first
+ const result = point.removeConsecutiveDuplicates({ points: pts, checkFirstAndLast: false, tolerance: 1e-5 });
+ expectPointsCloseTo(result, [[0, 0, 0], [1, 1, 1], [2, 2, 2], [0, 0, 1e-9]]);
+ });
+
+ it("should handle single point array", () => {
+ const pts: Inputs.Base.Point3[] = [[1, 2, 3]];
+ const result = point.removeConsecutiveDuplicates({ points: pts, tolerance: 1e-5, checkFirstAndLast: false });
+ expectPointsCloseTo(result, pts);
+ });
+
+ it("should handle empty point array", () => {
+ const pts: Inputs.Base.Point3[] = [];
+ const result = point.removeConsecutiveDuplicates({ points: pts, tolerance: 1e-5, checkFirstAndLast: false });
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe("normalFromThreePoints", () => {
+ it("should calculate the normal vector for non-collinear points (XY plane)", () => {
+ const p1: Inputs.Base.Point3 = [0, 0, 0];
+ const p2: Inputs.Base.Point3 = [1, 0, 0];
+ const p3: Inputs.Base.Point3 = [0, 1, 0];
+ const normal = point.normalFromThreePoints({ point1: p1, point2: p2, point3: p3, reverseNormal: false });
+ expectPointCloseTo(normal, [0, 0, 1]);
+ });
+
+ it("should calculate the normal vector for non-collinear points (off-axis)", () => {
+ const p1: Inputs.Base.Point3 = [1, 1, 1];
+ const p2: Inputs.Base.Point3 = [2, 1, 1]; // P2-P1 = [1,0,0]
+ const p3: Inputs.Base.Point3 = [1, 2, 1]; // P3-P1 = [0,1,0]
+ const normal = point.normalFromThreePoints({ point1: p1, point2: p2, point3: p3, reverseNormal: false });
+ expectPointCloseTo(normal, [0, 0, 1]);
+ });
+
+ it("should calculate the normal vector for points forming XZ plane", () => {
+ const p1: Inputs.Base.Point3 = [0, 0, 0];
+ const p2: Inputs.Base.Point3 = [1, 0, 0]; // V1=[1,0,0]
+ const p3: Inputs.Base.Point3 = [0, 0, 1]; // V2=[0,0,1]
+ const normal = point.normalFromThreePoints({ point1: p1, point2: p2, point3: p3, reverseNormal: false });
+ expectPointCloseTo(normal, [0, -1, 0]);
+ });
+
+ it("should reverse the normal if reverseNormal is true", () => {
+ const p1: Inputs.Base.Point3 = [0, 0, 0];
+ const p2: Inputs.Base.Point3 = [1, 0, 0];
+ const p3: Inputs.Base.Point3 = [0, 1, 0];
+ const normal = point.normalFromThreePoints({ point1: p1, point2: p2, point3: p3, reverseNormal: true });
+ expectPointCloseTo(normal, [0, 0, -1]);
+ });
+
+ it("should return undefined for collinear points", () => {
+ const p1: Inputs.Base.Point3 = [0, 0, 0];
+ const p2: Inputs.Base.Point3 = [1, 1, 1];
+ const p3: Inputs.Base.Point3 = [2, 2, 2]; // p3 = p1 + 2*(p2-p1)
+ // V1 = [1,1,1], V2 = [2,2,2]
+ // Cross = [1*2-1*2, 1*2-1*2, 1*2-1*2] = [0,0,0]
+ // Should log warning and return undefined
+ const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
+ const normal = point.normalFromThreePoints({ point1: p1, point2: p2, point3: p3, reverseNormal: false });
+ expect(normal).toBeUndefined();
+ expect(consoleWarnSpy).toHaveBeenCalledWith("Points are collinear or coincident; cannot calculate a unique normal.");
+ consoleWarnSpy.mockRestore();
+ });
+
+ it("should return undefined for coincident points", () => {
+ const p1: Inputs.Base.Point3 = [1, 1, 1];
+ const p2: Inputs.Base.Point3 = [1, 1, 1];
+ const p3: Inputs.Base.Point3 = [2, 3, 4];
+ const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
+ const normal = point.normalFromThreePoints({ point1: p1, point2: p2, point3: p3, reverseNormal: false });
+ expect(normal).toBeUndefined();
+ expect(consoleWarnSpy).toHaveBeenCalledWith("Points are collinear or coincident; cannot calculate a unique normal.");
+ consoleWarnSpy.mockRestore();
+ });
+
+ it("should throw error for invalid point formats", () => {
+ const p1: any = [0, 0];
+ const p2: Inputs.Base.Point3 = [1, 1, 1];
+ const p3: Inputs.Base.Point3 = [2, 2, 2];
+ expect(() => point.normalFromThreePoints({ point1: p1, point2: p2, point3: p3, reverseNormal: false })).toThrow("All points must be arrays of 3 numbers [x, y, z]");
+ expect(() => point.normalFromThreePoints({ point1: null as any, point2: p2, point3: p3, reverseNormal: false })).toThrow("All points must be arrays of 3 numbers [x, y, z]");
+ });
+ });
+
+ describe("twoPointsAlmostEqual", () => {
+ it("should return true for points within tolerance", () => {
+ const p1: Inputs.Base.Point3 = [1, 1, 1];
+ const p2: Inputs.Base.Point3 = [1, 1, 1 + 1e-7];
+ const tolerance = 1e-5;
+ expect(point.twoPointsAlmostEqual({ point1: p1, point2: p2, tolerance })).toBe(true);
+ });
+
+ it("should return true for identical points", () => {
+ const p1: Inputs.Base.Point3 = [1, 1, 1];
+ const tolerance = 1e-5;
+ expect(point.twoPointsAlmostEqual({ point1: p1, point2: p1, tolerance })).toBe(true);
+ });
+
+ it("should return false for points outside tolerance", () => {
+ const p1: Inputs.Base.Point3 = [1, 1, 1];
+ const p2: Inputs.Base.Point3 = [1, 1, 1 + 1e-4];
+ const tolerance = 1e-5;
+ expect(point.twoPointsAlmostEqual({ point1: p1, point2: p2, tolerance })).toBe(false);
+ });
+
+ it("should return false for points equal to tolerance distance", () => {
+ // distance < tolerance, so if distance === tolerance, should be false
+ const p1: Inputs.Base.Point3 = [1, 1, 1];
+ const p2: Inputs.Base.Point3 = [1, 1, 1 + 1e-5];
+ const tolerance = 1e-5;
+ expect(point.twoPointsAlmostEqual({ point1: p1, point2: p2, tolerance })).toBe(false);
+ });
+ });
+
+ });
+
+ // Helper to compare points/vectors with tolerance
+ const expectPointCloseTo = (
+ received: Inputs.Base.Point3 | Inputs.Base.Vector3 | undefined,
+ expected: Inputs.Base.Point3 | Inputs.Base.Vector3
+ ) => {
+ expect(received).toBeDefined();
+ if (!received) return; // Guard for TS
+ expect(received.length).toEqual(expected.length);
+ expect(received[0]).toBeCloseTo(expected[0], TOLERANCE);
+ expect(received[1]).toBeCloseTo(expected[1], TOLERANCE);
+ if (expected.length > 2 && received.length > 2) {
+ expect(received[2]).toBeCloseTo(expected[2], TOLERANCE);
+ }
+ };
+
+ // Helper to compare arrays of points/vectors with tolerance
+ const expectPointsCloseTo = (
+ received: Inputs.Base.Point3[] | Inputs.Base.Vector3[],
+ expected: Inputs.Base.Point3[] | Inputs.Base.Vector3[]
+ ) => {
+ expect(received.length).toEqual(expected.length);
+ received.forEach((p, i) => expectPointCloseTo(p, expected[i]));
+ };
+});
diff --git a/packages/dev/base/lib/api/services/point.ts b/packages/dev/base/lib/api/services/point.ts
index bd295115..f4b51a41 100644
--- a/packages/dev/base/lib/api/services/point.ts
+++ b/packages/dev/base/lib/api/services/point.ts
@@ -472,4 +472,19 @@ export class Point {
return { index: closestPointIndex + 1, distance, point };
}
+ /**
+ * Checks if two points are almost equal
+ * @param inputs Two points and the tolerance
+ * @returns true if the points are almost equal
+ * @group measure
+ * @shortname two points almost equal
+ * @drawable false
+ */
+ twoPointsAlmostEqual(inputs: Inputs.Point.TwoPointsToleranceDto): boolean {
+ const p1 = inputs.point1;
+ const p2 = inputs.point2;
+ const dist = this.distance({ startPoint: p1, endPoint: p2 });
+ return dist < inputs.tolerance;
+ }
+
}
diff --git a/packages/dev/base/lib/api/services/polyline.test.ts b/packages/dev/base/lib/api/services/polyline.test.ts
new file mode 100644
index 00000000..ad4cdbc8
--- /dev/null
+++ b/packages/dev/base/lib/api/services/polyline.test.ts
@@ -0,0 +1,539 @@
+import { GeometryHelper } from "./geometry-helper";
+import { MathBitByBit } from "./math";
+import { Point } from "./point";
+import { Polyline } from "./polyline";
+import { Transforms } from "./transforms";
+import { Vector } from "./vector";
+import * as Inputs from "../inputs";
+
+describe("Polyline unit tests", () => {
+ let geometryHelper: GeometryHelper;
+ let math: MathBitByBit;
+ let vector: Vector;
+ let point: Point;
+ let polyline: Polyline;
+ let transforms: Transforms;
+
+ beforeAll(() => {
+ geometryHelper = new GeometryHelper();
+ math = new MathBitByBit();
+ vector = new Vector(math, geometryHelper);
+ transforms = new Transforms(vector, math);
+ point = new Point(geometryHelper, transforms, vector);
+ polyline = new Polyline(vector, point, geometryHelper);
+ });
+
+ const TOLERANCE = 1e-7;
+
+ const expectPointCloseTo = (
+ received: Inputs.Base.Point3 | Inputs.Base.Vector3 | undefined,
+ expected: Inputs.Base.Point3 | Inputs.Base.Vector3
+ ) => {
+ expect(received).toBeDefined();
+ if (!received) return; // Guard for TS
+ expect(received.length).toEqual(expected.length);
+ expect(received[0]).toBeCloseTo(expected[0], TOLERANCE);
+ expect(received[1]).toBeCloseTo(expected[1], TOLERANCE);
+ if (expected.length > 2 && received.length > 2) {
+ expect(received[2]).toBeCloseTo(expected[2], TOLERANCE);
+ }
+ };
+
+ const expectPointsCloseTo = (
+ received: Inputs.Base.Point3[] | Inputs.Base.Vector3[],
+ expected: Inputs.Base.Point3[] | Inputs.Base.Vector3[]
+ ) => {
+ expect(received.length).toEqual(expected.length);
+ received.forEach((p, i) => expectPointCloseTo(p, expected[i]));
+ };
+
+ it("should create polyline", () => {
+ const p = polyline.create({
+ points: [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0]],
+ isClosed: true
+ });
+ expect(p).toBeDefined();
+ expect(p.points.length).toEqual(4);
+ });
+
+
+ describe("length", () => {
+ it("should calculate the length of a simple open polyline", () => {
+ const p: Inputs.Base.Polyline3 = { points: [[0, 0, 0], [3, 0, 0], [3, 4, 0]] }; // Length 3 + 4 = 7
+ const result = polyline.length({ polyline: p });
+ expect(result).toBeCloseTo(7, TOLERANCE);
+ });
+
+ it("should calculate the length of a closed polyline (sum of segments)", () => {
+ // Note: Implementation sums segment lengths, doesn't automatically add closing segment length
+ const p: Inputs.Base.Polyline3 = {
+ points: [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 0]], // Explicitly closed square
+ isClosed: true
+ }; // Length 1 + 1 + 1 + 1 = 4
+ const result = polyline.length({ polyline: p });
+ expect(result).toBeCloseTo(4, TOLERANCE);
+ });
+
+ it("should calculate length correctly even if isClosed=true but points dont form loop", () => {
+ // The isClosed flag doesn't affect the length calculation based on the code
+ const p: Inputs.Base.Polyline3 = {
+ points: [[0, 0, 0], [3, 0, 0], [3, 4, 0]], // Same as open L-shape
+ isClosed: true
+ }; // Length 3 + 4 = 7
+ const result = polyline.length({ polyline: p });
+ expect(result).toBeCloseTo(7, TOLERANCE);
+ });
+
+ it("should return 0 for a polyline with a single point", () => {
+ const p: Inputs.Base.Polyline3 = { points: [[1, 2, 3]] };
+ const result = polyline.length({ polyline: p });
+ expect(result).toBeCloseTo(0, TOLERANCE);
+ });
+
+ it("should return 0 for a polyline with two identical points", () => {
+ const p: Inputs.Base.Polyline3 = { points: [[1, 2, 3], [1, 2, 3]] };
+ const result = polyline.length({ polyline: p });
+ expect(result).toBeCloseTo(0, TOLERANCE);
+ });
+
+ it("should return 0 for a polyline with no points", () => {
+ const p: Inputs.Base.Polyline3 = { points: [] };
+ const result = polyline.length({ polyline: p });
+ expect(result).toBeCloseTo(0, TOLERANCE);
+ });
+ });
+
+ describe("countPoints", () => {
+ it("should return the correct number of points", () => {
+ const p: Inputs.Base.Polyline3 = { points: [[0, 0, 0], [1, 1, 1], [2, 2, 2]] };
+ expect(polyline.countPoints({ polyline: p })).toBe(3);
+ });
+
+ it("should return 1 for a single-point polyline", () => {
+ const p: Inputs.Base.Polyline3 = { points: [[1, 2, 3]] };
+ expect(polyline.countPoints({ polyline: p })).toBe(1);
+ });
+
+ it("should return 0 for an empty polyline", () => {
+ const p: Inputs.Base.Polyline3 = { points: [] };
+ expect(polyline.countPoints({ polyline: p })).toBe(0);
+ });
+ });
+
+ describe("getPoints", () => {
+ it("should return the points array", () => {
+ const points: Inputs.Base.Point3[] = [[0, 0, 0], [1, 1, 1]];
+ const p: Inputs.Base.Polyline3 = { points: points };
+ const result = polyline.getPoints({ polyline: p });
+ expect(result).toEqual(points);
+ expect(result).toBe(points);
+ });
+
+ it("should return an empty array for an empty polyline", () => {
+ const p: Inputs.Base.Polyline3 = { points: [] };
+ const result = polyline.getPoints({ polyline: p });
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe("reverse", () => {
+ it("should reverse the order of points in the polyline", () => {
+ const initialPoints: Inputs.Base.Point3[] = [[0, 0, 0], [1, 1, 1], [2, 2, 2]];
+ const p: Inputs.Base.Polyline3 = { points: [...initialPoints] }; // Pass a copy
+ const result = polyline.reverse({ polyline: p });
+ const expectedPoints: Inputs.Base.Point3[] = [[2, 2, 2], [1, 1, 1], [0, 0, 0]];
+ expect(result.points).toEqual(expectedPoints);
+ expect(p.points).toEqual(expectedPoints);
+ });
+
+ it("should handle a polyline with two points", () => {
+ const initialPoints: Inputs.Base.Point3[] = [[0, 0, 0], [1, 1, 1]];
+ const p: Inputs.Base.Polyline3 = { points: [...initialPoints] };
+ const result = polyline.reverse({ polyline: p });
+ expect(result.points).toEqual([[1, 1, 1], [0, 0, 0]]);
+ });
+
+ it("should handle a polyline with a single point", () => {
+ const initialPoints: Inputs.Base.Point3[] = [[1, 2, 3]];
+ const p: Inputs.Base.Polyline3 = { points: [...initialPoints] };
+ const result = polyline.reverse({ polyline: p });
+ expect(result.points).toEqual([[1, 2, 3]]);
+ });
+
+ it("should handle an empty polyline", () => {
+ const p: Inputs.Base.Polyline3 = { points: [] };
+ const result = polyline.reverse({ polyline: p });
+ expect(result.points).toEqual([]);
+ });
+ });
+
+ describe("transformPolyline", () => {
+ it("should translate all points in the polyline", () => {
+ const p: Inputs.Base.Polyline3 = { points: [[0, 0, 0], [1, 1, 0]] };
+ const translationVec: Inputs.Base.Vector3 = [10, -5, 2];
+ const transformation = transforms.translationXYZ({ translation: translationVec });
+ const result = polyline.transformPolyline({ polyline: p, transformation });
+ const expectedPoints: Inputs.Base.Point3[] = [[10, -5, 2], [11, -4, 2]];
+ expectPointsCloseTo(result.points, expectedPoints);
+ });
+
+ it("should rotate all points in the polyline", () => {
+ const p: Inputs.Base.Polyline3 = { points: [[1, 0, 0], [2, 0, 5]] };
+ const transformation = transforms.rotationCenterAxis({
+ center: [0, 0, 0], axis: [0, 0, 1], angle: 90
+ });
+ const result = polyline.transformPolyline({ polyline: p, transformation });
+ const expectedPoints: Inputs.Base.Point3[] = [[0, 1, 0], [0, 2, 5]]; // Rotated points
+ expectPointsCloseTo(result.points, expectedPoints);
+ });
+
+ it("should handle an empty polyline", () => {
+ const p: Inputs.Base.Polyline3 = { points: [] };
+ const transformation = transforms.translationXYZ({ translation: [1, 1, 1] });
+ const result = polyline.transformPolyline({ polyline: p, transformation });
+ expect(result.points).toEqual([]);
+ });
+
+ it("should return a new object for the polyline properties", () => {
+ const points: Inputs.Base.Point3[] = [[0, 0, 0]];
+ const p: Inputs.Base.Polyline3 = { points };
+ const transformation = [transforms.identity()];
+ const result = polyline.transformPolyline({ polyline: p, transformation });
+ expect(result).toBeDefined();
+ expect(result.points).toBeDefined();
+ expect(result).not.toBe(p);
+ });
+ });
+
+
+ describe("sort segments into polylines", () => {
+
+ it("should return an empty array for empty input", () => {
+ const result = polyline.sortSegmentsIntoPolylines({ segments: [] });
+ expect(result).toEqual([]);
+ });
+
+ it("should return an empty array for undefined input", () => {
+ const result = polyline.sortSegmentsIntoPolylines({ segments: undefined as any });
+ expect(result).toEqual([]);
+ });
+
+ it("should handle a single segment", () => {
+ const segments: Inputs.Base.Segment3[] = [[[0, 0, 0], [1, 1, 1]]];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 0, 0], [1, 1, 1]]);
+ expect(result[0].isClosed).toBe(false);
+ });
+
+ it("should handle two unconnected segments", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 0, 0]],
+ [[2, 2, 0], [3, 2, 0]],
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ const sortedResult = sortPolylinesForComparison(result);
+
+ expect(sortedResult).toHaveLength(2);
+ expect(sortedResult[0].points).toEqual([[0, 0, 0], [1, 0, 0]]);
+ expect(sortedResult[0].isClosed).toBe(false);
+ expect(sortedResult[1].points).toEqual([[2, 2, 0], [3, 2, 0]]);
+ expect(sortedResult[1].isClosed).toBe(false);
+ });
+
+ it("should connect two segments in order", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 0, 0]],
+ [[1, 0, 0], [1, 1, 0]],
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 0, 0], [1, 0, 0], [1, 1, 0]]);
+ expect(result[0].isClosed).toBe(false);
+ });
+
+ it("should connect multiple segments in order", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 0, 0]],
+ [[1, 0, 0], [1, 1, 0]],
+ [[1, 1, 0], [0, 1, 0]],
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]);
+ expect(result[0].isClosed).toBe(false);
+ });
+
+ it("should connect multiple segments in scrambled order", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[1, 0, 0], [1, 1, 0]], // Middle
+ [[1, 1, 0], [0, 1, 0]], // End
+ [[0, 0, 0], [1, 0, 0]], // Start
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]); // Check the most likely order based on implementation finding index 0 first
+ expect(result[0].isClosed).toBe(false);
+ });
+
+ it("should connect multiple segments in scrambled order starting from the middle", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[2, 2, 0], [3, 2, 0]], // segment 2 (tail)
+ [[0, 2, 0], [1, 2, 0]], // segment 0 (head)
+ [[1, 2, 0], [2, 2, 0]], // segment 1 (middle, processed first)
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 2, 0], [1, 2, 0], [2, 2, 0], [3, 2, 0]]);
+ expect(result[0].isClosed).toBe(false);
+ });
+
+
+ it("should create a closed polyline from ordered segments", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 0, 0]],
+ [[1, 0, 0], [1, 1, 0]],
+ [[1, 1, 0], [0, 1, 0]],
+ [[0, 1, 0], [0, 0, 0]], // Closing segment
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]); // Closed loop, last point removed
+ expect(result[0].isClosed).toBe(true);
+ });
+
+ it("should create a closed polyline from scrambled segments", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[1, 1, 0], [0, 1, 0]],
+ [[0, 1, 0], [0, 0, 0]],
+ [[0, 0, 0], [1, 0, 0]],
+ [[1, 0, 0], [1, 1, 0]],
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[1, 1, 0], [0, 1, 0], [0, 0, 0], [1, 0, 0]]);
+ expect(result[0].isClosed).toBe(true);
+ });
+
+ it("should create a closed polyline detected during backward pass", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[1, 0, 0], [0, 0, 0]],
+ [[0, 1, 0], [1, 1, 0]],
+ [[1, 1, 0], [1, 0, 0]],
+ [[0, 0, 0], [0, 1, 0]],
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[1, 0, 0], [0, 0, 0], [0, 1, 0], [1, 1, 0]]);
+ expect(result[0].isClosed).toBe(true);
+ });
+
+
+ it("should handle segments connecting within tolerance", () => {
+ const tolerance = 0.1;
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 0, 0]],
+ [[1.05, 0, 0], [1, 1, 0]], // Connects within tolerance
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments, tolerance });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 0, 0], [1, 0, 0], [1, 1, 0]]);
+ expect(result[0].isClosed).toBe(false);
+ });
+
+ it("should NOT connect segments outside tolerance", () => {
+ const tolerance = 0.01;
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 0, 0]],
+ [[1.05, 0, 0], [1, 1, 0]], // Does NOT connect within tolerance
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments, tolerance });
+ const sortedResult = sortPolylinesForComparison(result);
+
+ expect(sortedResult).toHaveLength(2);
+ expect(sortedResult[0].points).toEqual([[0, 0, 0], [1, 0, 0]]);
+ expect(sortedResult[0].isClosed).toBe(false);
+ expect(sortedResult[1].points).toEqual([[1.05, 0, 0], [1, 1, 0]]);
+ expect(sortedResult[1].isClosed).toBe(false);
+ });
+
+ it("should ignore degenerate segments", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 0, 0]],
+ [[1, 0, 0], [1, 0, 0]], // Degenerate
+ [[1, 0, 0], [1, 1, 0]],
+ [[2, 2, 2], [2, 2, 2 + 1e-9]] // Degenerate within default tolerance
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 0, 0], [1, 0, 0], [1, 1, 0]]);
+ expect(result[0].isClosed).toBe(false);
+ });
+
+ it("should ignore degenerate segments with custom tolerance", () => {
+ const tolerance = 0.1;
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 0, 0]],
+ [[1, 0, 0], [1.05, 0, 0]], // Degenerate within custom tolerance
+ [[1, 0, 0], [1, 1, 0]],
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments, tolerance });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 0, 0], [1, 0, 0], [1, 1, 0]]);
+ expect(result[0].isClosed).toBe(false);
+ });
+
+ it("should handle multiple distinct polylines", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ // Polyline 1 (Open)
+ [[0, 0, 0], [1, 0, 0]],
+ [[1, 0, 0], [1, 1, 0]],
+ // Polyline 2 (Closed)
+ [[5, 5, 5], [6, 5, 5]],
+ [[6, 6, 5], [5, 6, 5]],
+ [[6, 5, 5], [6, 6, 5]],
+ [[5, 6, 5], [5, 5, 5]],
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ const sortedResult = sortPolylinesForComparison(result);
+
+ expect(sortedResult).toHaveLength(2);
+
+ expect(sortedResult[0].points).toEqual([[0, 0, 0], [1, 0, 0], [1, 1, 0]]);
+ expect(sortedResult[0].isClosed).toBe(false);
+
+ expect(sortedResult[1].points).toEqual([[5, 5, 5], [6, 5, 5], [6, 6, 5], [5, 6, 5]]);
+ expect(sortedResult[1].isClosed).toBe(true);
+ });
+
+ it("should chain through junctions using greedy approach", () => { // Rename for clarity
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 1, 1]], // Seg 0: A -> J
+ [[2, 2, 2], [1, 1, 1]], // Seg 1: B -> J
+ [[1, 1, 1], [0, 0, 2]], // Seg 2: J -> C
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ const sortedResult = sortPolylinesForComparison(result);
+
+ expect(sortedResult).toHaveLength(2);
+ expect(sortedResult).toEqual(expect.arrayContaining([
+ expect.objectContaining({
+ points: expect.arrayContaining([[0, 0, 0], [1, 1, 1], [2, 2, 2]]),
+ isClosed: false
+ }),
+ expect.objectContaining({ points: [[1, 1, 1], [0, 0, 2]], isClosed: false })
+ ]));
+ });
+
+ it("should handle reversed segment forming a 2-point closed loop", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 1, 1]],
+ [[1, 1, 1], [0, 0, 0]], // Reversed
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 0, 0], [1, 1, 1]]);
+ expect(result[0].isClosed).toBe(true);
+ });
+
+ it("should handle duplicate segments correctly", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 0, 0]],
+ [[1, 0, 0], [1, 1, 0]],
+ [[0, 0, 0], [1, 0, 0]], // Duplicate of the first segment
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[1, 0, 0], [0, 0, 0], [1, 0, 0], [1, 1, 0]]);
+ expect(result[0].isClosed).toBe(false);
+ });
+
+ it("should handle segments forming a minimal closed triangle", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 0, 0]],
+ [[1, 0, 0], [0, 1, 0]],
+ [[0, 1, 0], [0, 0, 0]],
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 0, 0], [1, 0, 0], [0, 1, 0]]);
+ expect(result[0].isClosed).toBe(true);
+ });
+
+ it("should handle points close to grid boundaries correctly", () => {
+ const tolerance = 0.1;
+
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [0.99, 0, 0]], // Seg 0
+ [[1.08, 0, 0], [2, 0, 0]], // Seg 1
+ [[1.21, 0, 0], [3, 0, 0]], // Seg 2 (unconnected)
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments, tolerance });
+ const sortedResult = sortPolylinesForComparison(result);
+ expect(sortedResult).toHaveLength(2);
+ expect(sortedResult[0].points).toEqual([[0, 0, 0], [0.99, 0, 0], [2, 0, 0]]); // Points from original segments
+ expect(sortedResult[0].isClosed).toBe(false);
+ expect(sortedResult[1].points).toEqual([[1.21, 0, 0], [3, 0, 0]]);
+ expect(sortedResult[1].isClosed).toBe(false);
+ });
+
+ it("should connect two segments meeting end-to-end (reversed second segment)", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[0, 0, 0], [1, 0, 0]], // A -> B
+ [[2, 0, 0], [1, 0, 0]], // C -> B (Reversed connection)
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 0, 0], [1, 0, 0], [2, 0, 0]]);
+ expect(result[0].isClosed).toBe(false);
+ });
+
+ it("should connect multiple segments with mixed directions", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[1, 0, 0], [2, 0, 0]], // B -> C
+ [[0, 0, 0], [1, 0, 0]], // A -> B
+ [[3, 0, 0], [2, 0, 0]], // D -> C (Reversed)
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[0, 0, 0], [1, 0, 0], [2, 0, 0], [3, 0, 0]]);
+ expect(result[0].isClosed).toBe(false);
+ });
+
+ it("should form a closed loop with mixed directions", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[1, 1, 0], [0, 1, 0]], // C -> B
+ [[0, 0, 0], [1, 0, 0]], // A -> D
+ [[1, 0, 0], [1, 1, 0]], // D -> C
+ [[0, 1, 0], [0, 0, 0]], // B -> A (Closes loop)
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[1, 1, 0], [0, 1, 0], [0, 0, 0], [1, 0, 0]]);
+ expect(result[0].isClosed).toBe(true);
+ });
+
+ it("should connect segments meeting start-to-start", () => {
+ const segments: Inputs.Base.Segment3[] = [
+ [[1, 0, 0], [0, 0, 0]], // B -> A
+ [[1, 0, 0], [2, 0, 0]], // B -> C
+ ];
+ const result = polyline.sortSegmentsIntoPolylines({ segments });
+ expect(result).toHaveLength(1);
+ expect(result[0].points).toEqual([[2, 0, 0], [1, 0, 0], [0, 0, 0]]); // Order depends on chaining direction
+ expect(result[0].isClosed).toBe(false);
+ });
+ });
+
+ const sortPolylinesForComparison = (polylines: Inputs.Base.Polyline3[]): Inputs.Base.Polyline3[] => {
+ return polylines.sort((a, b) => {
+ const pA = a.points[0];
+ const pB = b.points[0];
+ if (pA[0] !== pB[0]) return pA[0] - pB[0];
+ if (pA[1] !== pB[1]) return pA[1] - pB[1];
+ return pA[2] - pB[2];
+ });
+ };
+
+});
+
diff --git a/packages/dev/base/lib/api/services/polyline.ts b/packages/dev/base/lib/api/services/polyline.ts
new file mode 100644
index 00000000..7e353133
--- /dev/null
+++ b/packages/dev/base/lib/api/services/polyline.ts
@@ -0,0 +1,295 @@
+import { GeometryHelper } from "./geometry-helper";
+import * as Inputs from "../inputs";
+import { Point } from "./point";
+import { Vector } from "./vector";
+
+/**
+ * Contains various methods for polyline. Polyline in bitbybit is a simple object that has points property containing an array of points.
+ * { points: number[][] }
+ */
+export class Polyline {
+
+ constructor(private readonly vector: Vector, private readonly point: Point, private readonly geometryHelper: GeometryHelper) { }
+
+ /**
+ * Gets the length of the polyline
+ * @param inputs a polyline
+ * @returns length
+ * @group get
+ * @shortname polyline length
+ * @drawable false
+ */
+ length(inputs: Inputs.Polyline.PolylineDto): number {
+ let distanceOfPolyline = 0;
+ for (let i = 1; i < inputs.polyline.points.length; i++) {
+ const previousPoint = inputs.polyline.points[i - 1];
+ const currentPoint = inputs.polyline.points[i];
+ distanceOfPolyline += this.point.distance({ startPoint: previousPoint, endPoint: currentPoint });
+ }
+ return distanceOfPolyline;
+ }
+
+ /**
+ * Gets the number of points in the polyline
+ * @param inputs a polyline
+ * @returns nr of points
+ * @group get
+ * @shortname nr polyline points
+ * @drawable false
+ */
+ countPoints(inputs: Inputs.Polyline.PolylineDto): number {
+ return inputs.polyline.points.length;
+ }
+
+ /**
+ * Gets the points of the polyline
+ * @param inputs a polyline
+ * @returns points
+ * @group get
+ * @shortname points
+ * @drawable true
+ */
+ getPoints(inputs: Inputs.Polyline.PolylineDto): Inputs.Base.Point3[] {
+ return inputs.polyline.points;
+ }
+
+ /**
+ * Reverse the points of the polyline
+ * @param inputs a polyline
+ * @returns reversed polyline
+ * @group convert
+ * @shortname reverse polyline
+ * @drawable true
+ */
+ reverse(inputs: Inputs.Polyline.PolylineDto): Inputs.Polyline.PolylinePropertiesDto {
+ return { points: inputs.polyline.points.reverse() };
+ }
+
+ /**
+ * Transform the polyline
+ * @param inputs a polyline
+ * @returns transformed polyline
+ * @group transforms
+ * @shortname transform polyline
+ * @drawable true
+ */
+ transformPolyline(inputs: Inputs.Polyline.TransformPolylineDto): Inputs.Polyline.PolylinePropertiesDto {
+ const transformation = inputs.transformation;
+ let transformedControlPoints = inputs.polyline.points;
+ transformedControlPoints = this.geometryHelper.transformControlPoints(transformation, transformedControlPoints);
+ return { points: transformedControlPoints };
+ }
+
+ /**
+ * Create the polyline
+ * @param inputs points and info if its closed
+ * @returns polyline
+ * @group create
+ * @shortname polyline
+ * @drawable true
+ */
+ create(inputs: Inputs.Polyline.PolylineCreateDto): Inputs.Polyline.PolylinePropertiesDto {
+ return {
+ points: inputs.points,
+ };
+ }
+
+ /**
+ * Create the polylines from segments that are potentially connected but scrambled randomly
+ * @param inputs segments
+ * @returns polylines
+ * @group sort
+ * @shortname segments to polylines
+ * @drawable true
+ */
+ sortSegmentsIntoPolylines(inputs: Inputs.Polyline.SegmentsToleranceDto): Inputs.Base.Polyline3[] {
+ const tolerance = inputs.tolerance ?? 1e-5; // Default tolerance
+ const segments = inputs.segments;
+ if (!segments || segments.length === 0) {
+ return [];
+ }
+
+ const toleranceSq = tolerance * tolerance;
+ const numSegments = segments.length;
+ const used = new Array(numSegments).fill(false);
+ const results: Inputs.Base.Polyline3[] = [];
+
+ // --- Spatial Hash Map ---
+ interface EndpointInfo {
+ segmentIndex: number;
+ endpointIndex: 0 | 1;
+ coords: Inputs.Base.Point3;
+ }
+ const endpointMap = new Map();
+ const invTolerance = 1.0 / tolerance;
+
+ const getGridKey = (p: Inputs.Base.Point3): string => {
+ const ix = Math.round(p[0] * invTolerance);
+ const iy = Math.round(p[1] * invTolerance);
+ const iz = Math.round(p[2] * invTolerance);
+ return `${ix},${iy},${iz}`;
+ };
+
+ // 1. Build the spatial map
+ for (let i = 0; i < numSegments; i++) {
+ const segment = segments[i];
+ if (this.point.twoPointsAlmostEqual({ point1: segment[0], point2: segment[1], tolerance: tolerance })) {
+ used[i] = true; // Mark degenerate as used
+ continue;
+ }
+
+ const key0 = getGridKey(segment[0]);
+ const key1 = getGridKey(segment[1]);
+ const info0: EndpointInfo = { segmentIndex: i, endpointIndex: 0, coords: segment[0] };
+ const info1: EndpointInfo = { segmentIndex: i, endpointIndex: 1, coords: segment[1] };
+
+ if (!endpointMap.has(key0)) endpointMap.set(key0, []);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ endpointMap.get(key0)!.push(info0);
+
+ if (key1 !== key0) {
+ if (!endpointMap.has(key1)) endpointMap.set(key1, []);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ endpointMap.get(key1)!.push(info1);
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ endpointMap.get(key0)!.push(info1); // Add both endpoints if same key
+ }
+ }
+
+ // --- Helper to find connecting segment ---
+ const findConnection = (
+ pointToMatch: Inputs.Base.Point3
+ ): EndpointInfo | undefined => {
+ const searchKeys: string[] = [];
+ const px = Math.round(pointToMatch[0] * invTolerance);
+ const py = Math.round(pointToMatch[1] * invTolerance);
+ const pz = Math.round(pointToMatch[2] * invTolerance);
+
+ for (let dx = -1; dx <= 1; dx++) {
+ for (let dy = -1; dy <= 1; dy++) {
+ for (let dz = -1; dz <= 1; dz++) {
+ searchKeys.push(`${px + dx},${py + dy},${pz + dz}`);
+ }
+ }
+ }
+
+ let bestMatch: EndpointInfo | undefined = undefined;
+ let minDistanceSq = toleranceSq;
+
+ for (const searchKey of searchKeys) {
+ const candidates = endpointMap.get(searchKey);
+ if (!candidates) continue;
+
+ for (const candidate of candidates) {
+ // Only consider segments not already used in *any* polyline
+ if (!used[candidate.segmentIndex]) {
+ const diffVector = this.vector.sub({ first: candidate.coords, second: pointToMatch });
+ const distSq = this.vector.lengthSq({ vector: diffVector as Inputs.Base.Vector3});
+
+ if (distSq < minDistanceSq) {
+ // Check with precise method if it's a potential best match
+ if (this.point.twoPointsAlmostEqual({point1: candidate.coords, point2: pointToMatch, tolerance: tolerance})){
+ bestMatch = candidate;
+ minDistanceSq = distSq; // Update min distance found
+ }
+ }
+ }
+ }
+ }
+ // No need for final check here, already done inside the loop
+ if(bestMatch && !used[bestMatch.segmentIndex]) { // Double check used status
+ return bestMatch;
+ }
+ return undefined;
+ };
+
+
+ // 2. Iterate and chain segments
+ for (let i = 0; i < numSegments; i++) {
+ if (used[i]) continue; // Skip if already part of a polyline
+
+ // Start a new polyline
+ used[i] = true; // Mark the starting segment as used
+ const startSegment = segments[i];
+ const currentPoints: Inputs.Base.Point3[] = [startSegment[0], startSegment[1]];
+ let currentHead = startSegment[0];
+ let currentTail = startSegment[1];
+ let isClosed = false;
+ let iterations = 0;
+
+ // Extend forward (tail)
+ while (iterations++ < numSegments) {
+ const nextMatch = findConnection(currentTail);
+ if (!nextMatch) break; // No unused segment connects to the tail
+
+ // We found a potential next segment
+ const nextSegment = segments[nextMatch.segmentIndex];
+ const pointToAdd = (nextMatch.endpointIndex === 0) ? nextSegment[1] : nextSegment[0];
+
+ // Check for closure *before* adding the point
+ if (this.point.twoPointsAlmostEqual({ point1: pointToAdd, point2: currentHead, tolerance: tolerance })) {
+ isClosed = true;
+ // Mark the closing segment as used
+ used[nextMatch.segmentIndex] = true;
+ break; // Closed loop found
+ }
+
+ // Not closing, so add the point and mark the segment used
+ used[nextMatch.segmentIndex] = true;
+ currentPoints.push(pointToAdd);
+ currentTail = pointToAdd;
+ }
+
+ // Extend backward (head) - only if not already closed
+ iterations = 0;
+ if (!isClosed) {
+ while (iterations++ < numSegments) {
+ const prevMatch = findConnection(currentHead);
+ if (!prevMatch) break; // No unused segment connects to the head
+
+ const prevSegment = segments[prevMatch.segmentIndex];
+ const pointToAdd = (prevMatch.endpointIndex === 0) ? prevSegment[1] : prevSegment[0];
+
+ // Check for closure against the current tail *before* adding
+ if (this.point.twoPointsAlmostEqual({ point1: pointToAdd, point2: currentTail, tolerance: tolerance })) {
+ isClosed = true;
+ // Mark the closing segment as used
+ used[prevMatch.segmentIndex] = true;
+ break; // Closed loop found
+ }
+
+ // Not closing, add point to beginning and mark segment used
+ used[prevMatch.segmentIndex] = true;
+ currentPoints.unshift(pointToAdd);
+ currentHead = pointToAdd;
+ }
+ }
+
+ // Final closure check (might be redundant now, but harmless)
+ // This catches cases like A->B, B->A which form a 2-point closed loop
+ if (!isClosed && currentPoints.length >= 2) {
+ isClosed = this.point.twoPointsAlmostEqual({ point1: currentHead, point2: currentTail, tolerance: tolerance });
+ }
+
+ // Remove duplicate point for closed loops with more than 2 points
+ if (isClosed && currentPoints.length > 2) {
+ // Check if the first and last points are indeed the ones needing merging
+ if (this.point.twoPointsAlmostEqual({ point1: currentPoints[currentPoints.length - 1], point2: currentPoints[0], tolerance: tolerance })) {
+ currentPoints.pop();
+ }
+ }
+
+ // Add the completed polyline (even if it's just the starting segment)
+ results.push({
+ points: currentPoints,
+ isClosed: isClosed,
+ });
+ }
+
+ return results;
+ }
+
+
+}
+
diff --git a/packages/dev/base/lib/api/services/text.test.ts b/packages/dev/base/lib/api/services/text.test.ts
index 34239254..3691d034 100644
--- a/packages/dev/base/lib/api/services/text.test.ts
+++ b/packages/dev/base/lib/api/services/text.test.ts
@@ -4,10 +4,75 @@ import { Point } from "./point";
import { TextBitByBit } from "./text";
import { Transforms } from "./transforms";
import { Vector } from "./vector";
+import * as Inputs from "../inputs";
+
describe("Text unit tests", () => {
let text: TextBitByBit;
+ // Mock Font Data Structure (simplified)
+ // Uses character code as key.
+ // First element is width, then pairs of [x, y] relative coords. `undefined` signifies path break.
+ const mockFont = {
+ // Height of the font design coordinate space
+ height: 100, // Example height
+ // Glyph for 'A' (char code 65) - simple triangle
+ 65: [
+ 50, // width
+ 0, 0, // path 1 start
+ 25, 80,
+ 50, 0,
+ 0, 0, // close path 1
+ ],
+ // Glyph for 'B' (char code 66) - two paths (e.g., two vertical lines)
+ 66: [
+ 40, // width
+ 0, 0, // path 1
+ 0, 80,
+ undefined, // path break
+ 30, 0, // path 2
+ 30, 80,
+ ],
+ // Glyph for ' ' (space) (char code 32) - just width
+ 32: [
+ 20, // width (no geometry needed, but width matters)
+ ],
+ // Glyph for '?' (fallback, char code 63) - simple square
+ 63: [
+ 45, // width
+ 0, 0,
+ 45, 0,
+ 45, 70,
+ 0, 70,
+ 0, 0,
+ ]
+ };
+
+ const expectPointCloseTo = (
+ received: Inputs.Base.Point3 | Inputs.Base.Vector3 | undefined,
+ expected: Inputs.Base.Point3 | Inputs.Base.Vector3
+ ) => {
+ expect(received).toBeDefined();
+ if (!received) return; // Guard for TS
+ expect(received.length).toEqual(expected.length);
+ expect(received[0]).toBeCloseTo(expected[0], TOLERANCE);
+ expect(received[1]).toBeCloseTo(expected[1], TOLERANCE);
+ if (expected.length > 2 && received.length > 2) {
+ expect(received[2]).toBeCloseTo(expected[2], TOLERANCE);
+ }
+ };
+
+ const expectPointsCloseTo = (
+ received: Inputs.Base.Point3[] | Inputs.Base.Vector3[],
+ expected: Inputs.Base.Point3[] | Inputs.Base.Vector3[]
+ ) => {
+ expect(received.length).toEqual(expected.length);
+ received.forEach((p, i) => expectPointCloseTo(p, expected[i]));
+ };
+
+ const TOLERANCE = 1e-7;
+
+
beforeAll(async () => {
const geometryHelper = new GeometryHelper();
const math = new MathBitByBit();
@@ -61,5 +126,242 @@ describe("Text unit tests", () => {
const result = text.format({ text: "Hello World, Matas", values: ["dada"] });
expect(result).toEqual("Hello World, Matas");
});
+
+
+ describe("vectorChar", () => {
+
+ it("should create vector data for a basic character (A)", () => {
+ const char = "A";
+ const code = char.charCodeAt(0);
+ const targetHeight = 20;
+ const glyphWidth = mockFont[code][0];
+ const fontDesignHeight = mockFont.height;
+ const ratio = targetHeight / fontDesignHeight;
+ const expectedWidth = glyphWidth * ratio;
+
+ const result = text.vectorChar({
+ char: char,
+ height: targetHeight,
+ font: mockFont
+ } as jest.Mocked);
+
+ expect(result.width).toBeCloseTo(expectedWidth, TOLERANCE);
+ expect(result.height).toBeCloseTo(targetHeight, TOLERANCE);
+ expect(result.paths).toHaveLength(1);
+
+ const expectedPath: Inputs.Base.Point3[] = [
+ [0 * ratio + 0, 0, 0 * ratio + 0],
+ [25 * ratio + 0, 0, 80 * ratio + 0],
+ [50 * ratio + 0, 0, 0 * ratio + 0],
+ [0 * ratio + 0, 0, 0 * ratio + 0],
+ ];
+ expectPointsCloseTo(result.paths[0], expectedPath);
+ });
+
+ it("should apply xOffset and yOffset", () => {
+ const char = "A";
+ const targetHeight = 10;
+ const xOff = 5;
+ const yOff = -2;
+ const ratio = 0.1;
+
+ const result = text.vectorChar({
+ char: char, height: targetHeight, font: mockFont, xOffset: xOff, yOffset: yOff
+ } as jest.Mocked);
+
+ const expectedFirstPoint: Inputs.Base.Point3 = [
+ 0 * ratio + xOff, 0, 0 * ratio + yOff
+ ];
+ expectPointCloseTo(result.paths[0][0], expectedFirstPoint);
+ });
+
+ it("should handle extrudeOffset", () => {
+ const char = "A";
+ const targetHeight = 20;
+ const extrudeOff = 4;
+ const fontDesignHeight = mockFont.height;
+ const ratio = (targetHeight - extrudeOff) / fontDesignHeight;
+ const extrudeYOff = extrudeOff / 2;
+ const glyphWidth = mockFont[char.charCodeAt(0)][0];
+ const expectedWidth = glyphWidth * ratio;
+
+ const result = text.vectorChar({
+ char: char, height: targetHeight, font: mockFont, extrudeOffset: extrudeOff
+ } as jest.Mocked);
+
+ expect(result.width).toBeCloseTo(expectedWidth, TOLERANCE);
+ expect(result.height).toBeCloseTo(targetHeight, TOLERANCE);
+
+ const expectedFirstPointY = 0 * ratio + 0 + extrudeYOff;
+ const expectedSecondPointY = 80 * ratio + 0 + extrudeYOff;
+
+ expect(result.paths[0][0][2]).toBeCloseTo(expectedFirstPointY, TOLERANCE);
+ expect(result.paths[0][1][2]).toBeCloseTo(expectedSecondPointY, TOLERANCE);
+ });
+
+ it("should create multiple paths for characters with breaks (B)", () => {
+ const char = "B";
+ const targetHeight = 10;
+ const result = text.vectorChar({ char: char, height: targetHeight, font: mockFont } as jest.Mocked);
+
+ expect(result.paths).toHaveLength(2);
+ const expectedPath1: Inputs.Base.Point3[] = [[0, 0, 0], [0, 0, 8]];
+ const expectedPath2: Inputs.Base.Point3[] = [[3, 0, 0], [3, 0, 8]];
+
+ expectPointsCloseTo(result.paths[0], expectedPath1);
+ expectPointsCloseTo(result.paths[1], expectedPath2);
+ });
+
+ it("should use fallback character (?) for unknown characters", () => {
+ const char = "Z";
+ const fallbackCode = 63; // '?'
+ const targetHeight = 10;
+ const fallbackGlyphWidth = mockFont[fallbackCode][0]; // 45
+ const expectedWidth = fallbackGlyphWidth * (targetHeight / mockFont.height); // 45 * 0.1 = 4.5
+
+ const result = text.vectorChar({ char: char, height: targetHeight, font: mockFont } as jest.Mocked);
+
+ expect(result.width).toBeCloseTo(expectedWidth, TOLERANCE);
+ expect(result.paths).toHaveLength(1);
+
+ const expectedPathFallback: Inputs.Base.Point3[] = [
+ [0 * 0.1, 0, 0 * 0.1],
+ [45 * 0.1, 0, 0 * 0.1],
+ [45 * 0.1, 0, 70 * 0.1],
+ [0 * 0.1, 0, 70 * 0.1],
+ [0 * 0.1, 0, 0 * 0.1],
+ ];
+ expectPointsCloseTo(result.paths[0], expectedPathFallback);
+ });
+
+ it("should handle space character (width only)", () => {
+ const char = " ";
+ const targetHeight = 10;
+ const spaceGlyphWidth = mockFont[char.charCodeAt(0)][0]; // 20
+ const expectedWidth = spaceGlyphWidth * (targetHeight / mockFont.height); // 20 * 0.1 = 2
+
+ const result = text.vectorChar({ char: char, height: targetHeight, font: mockFont } as jest.Mocked);
+
+ expect(result.width).toBeCloseTo(expectedWidth, TOLERANCE);
+ expect(result.height).toBeCloseTo(targetHeight, TOLERANCE);
+ expect(result.paths).toEqual([]);
+ });
+
+ it("should use fallback for empty string input", () => {
+ const char = "";
+ const fallbackCode = 63;
+ const targetHeight = 10;
+ const fallbackGlyphWidth = mockFont[fallbackCode][0];
+ const expectedWidth = fallbackGlyphWidth * (targetHeight / mockFont.height);
+
+ const result = text.vectorChar({ char: char, height: targetHeight, font: mockFont } as jest.Mocked);
+ expect(result.width).toBeCloseTo(expectedWidth, TOLERANCE);
+ });
+
+ });
+
+ describe("vectorText", () => {
+
+ it("should create data for a single character text", () => {
+ const result = text.vectorText({ text: "A", font: mockFont, height: 10 } as jest.Mocked);
+ expect(result).toHaveLength(1);
+ expect(result[0].chars).toHaveLength(1);
+ expect(result[0].chars[0].paths).toHaveLength(3);
+ });
+
+ it("should create data for a simple text string (\"AB\")", () => {
+ const height = 10;
+ const result = text.vectorText({ text: "AB", font: mockFont, height: height } as jest.Mocked);
+ expect(result[0].chars[0].paths).toEqual([[[4.285714285714286, 0, 10], [0.47619047619047616, 0, 0]], [[4.285714285714286, 0, 10], [8.095238095238095, 0, 0]], [[1.9047619047619047, 0, 3.333333333333333], [6.666666666666666, 0, 3.333333333333333]]]);
+ });
+
+ it("should handle spaces correctly (\"A B\")", () => {
+ const height = 10;
+
+ const result = text.vectorText({ text: "A B", font: mockFont, height: height } as jest.Mocked);
+ expect(result[0].chars[0].paths).toEqual([[[4.285714285714286, 0, 10], [0.47619047619047616, 0, 0]], [[4.285714285714286, 0, 10], [8.095238095238095, 0, 0]], [[1.9047619047619047, 0, 3.333333333333333], [6.666666666666666, 0, 3.333333333333333]]]);
+ });
+
+ it("should handle newline characters (\"A\\nB\")", () => {
+ const height = 10;
+ const lineSpacing = 1.5;
+
+ const result = text.vectorText({ text: "A\nB", font: mockFont, height: height, lineSpacing: lineSpacing } as jest.Mocked);
+ expect(result).toHaveLength(2);
+
+ expect(result[0].chars[0].paths).toEqual([[[4.285714285714286, 0, 10], [0.47619047619047616, 0, 0]], [[4.285714285714286, 0, 10], [8.095238095238095, 0, 0]], [[1.9047619047619047, 0, 3.333333333333333], [6.666666666666666, 0, 3.333333333333333]]]);
+ });
+
+ it("should apply letterSpacing", () => {
+ const height = 10;
+ const letterSpacing = 0.5;
+
+ const result = text.vectorText({ text: "AB", font: mockFont, height: height, letterSpacing: letterSpacing } as jest.Mocked);
+ expect(result[0].chars[0].paths).toEqual([
+ [[4.285714285714286, 0, 10], [0.47619047619047616, 0, 0]],
+ [[4.285714285714286, 0, 10], [8.095238095238095, 0, 0]],
+ [
+ [1.9047619047619047, 0, 3.333333333333333],
+ [6.666666666666666, 0, 3.333333333333333]
+ ]
+ ]);
+ });
+
+ it("should align text center", () => {
+ const txt = "A\nAB";
+ const height = 10;
+
+ const result = text.vectorText({ text: txt, font: mockFont, height: height, align: Inputs.Base.horizontalAlignEnum.center } as jest.Mocked);
+ expect(result).toHaveLength(2);
+
+ expect(result[0].chars[0].paths).toEqual([[[14.285714285714285, 0, 10], [10.476190476190476, 0, 0]], [[14.285714285714285, 0, 10], [18.095238095238095, 0, 0]], [[11.904761904761905, 0, 3.333333333333333], [16.666666666666664, 0, 3.333333333333333]]]);
+ });
+
+ it("should align text right", () => {
+ const txt = "A\nAB";
+ const height = 10;
+
+ const result = text.vectorText({ text: txt, font: mockFont, height: height, align: Inputs.Base.horizontalAlignEnum.right } as jest.Mocked);
+ expect(result).toHaveLength(2);
+ expect(result[0].chars[0].paths).toEqual([
+ [[24.285714285714285, 0, 10], [20.476190476190474, 0, 0]],
+ [[24.285714285714285, 0, 10], [28.095238095238095, 0, 0]],
+ [
+ [21.904761904761905, 0, 3.333333333333333],
+ [26.666666666666664, 0, 3.333333333333333]
+ ]
+ ]);
+ });
+
+ it("should center the entire text block if centerOnOrigin is true", () => {
+ const txt = "A";
+ const height = 10;
+
+ const result = text.vectorText({ text: txt, font: mockFont, height: height, centerOnOrigin: true } as jest.Mocked);
+ expect(result).toHaveLength(1);
+ expect(result[0].chars).toHaveLength(1);
+ expect(result[0].chars[0].paths).toEqual([
+ [[0, 0, 5], [-3.8095238095238093, 0, -5]],
+ [[0, 0, 5], [3.8095238095238093, 0, -5]],
+ [
+ [-2.380952380952381, 0, -1.666666666666667],
+ [2.3809523809523805, 0, -1.666666666666667]
+ ]
+ ]);
+
+ });
+
+ it("should return empty array for empty text", () => {
+ const result = text.vectorText({ text: "", font: mockFont } as jest.Mocked);
+ expect(result).toEqual([]);
+ });
+
+ it("should throw error for non-string input", () => {
+ expect(() => {
+ text.vectorText({ text: 123 as any, font: mockFont } as jest.Mocked);
+ }).toThrow("text must be a string");
+ });
+
+ });
});
diff --git a/packages/dev/base/lib/api/services/transforms.test.ts b/packages/dev/base/lib/api/services/transforms.test.ts
new file mode 100644
index 00000000..360eb12c
--- /dev/null
+++ b/packages/dev/base/lib/api/services/transforms.test.ts
@@ -0,0 +1,328 @@
+import { GeometryHelper } from "./geometry-helper";
+import { MathBitByBit } from "./math";
+import { Transforms } from "./transforms";
+import { Vector } from "./vector";
+import * as Inputs from "../inputs";
+
+describe("Transforms unit tests", () => {
+ let geometryHelper: GeometryHelper;
+ let math: MathBitByBit;
+ let vector: Vector;
+ let transforms: Transforms;
+
+ // Precision for floating point comparisons
+ const TOLERANCE = 1e-7;
+
+ // Helper to compare two 4x4 matrices (16-element arrays) with tolerance
+ const expectMatrixCloseTo = (received: Inputs.Base.TransformMatrix | undefined, expected: Inputs.Base.TransformMatrix) => {
+ expect(received).toBeDefined();
+ if (!received) return;
+ expect(received).toHaveLength(16);
+ expect(expected).toHaveLength(16);
+ for (let i = 0; i < 16; i++) {
+ expect(received[i]).toBeCloseTo(expected[i], TOLERANCE);
+ }
+ };
+
+ // Helper to compare arrays of matrices (like those returned by center-based operations)
+ const expectMatrixesCloseTo = (received: Inputs.Base.TransformMatrixes | undefined, expected: Inputs.Base.TransformMatrixes) => {
+ expect(received).toBeDefined();
+ if (!received) return;
+ expect(received.length).toEqual(expected.length);
+ received.forEach((matrix, i) => expectMatrixCloseTo(matrix, expected[i]));
+ };
+
+ beforeAll(() => {
+ geometryHelper = new GeometryHelper();
+ math = new MathBitByBit();
+ vector = new Vector(math, geometryHelper);
+ transforms = new Transforms(vector, math);
+ });
+
+ describe("Transforms Class Unit Tests (Integration)", () => {
+
+ const centerPoint: Inputs.Base.Point3 = [10, 20, 30];
+
+ const identityMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] as Inputs.Base.TransformMatrix;
+ const translationMatrix = (x, y, z) => [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1] as Inputs.Base.TransformMatrix;
+ const scalingMatrix = (x, y, z) => [x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1] as Inputs.Base.TransformMatrix;
+ const rotationXMatrix = (angleRad) => {
+ const c = Math.cos(angleRad);
+ const s = Math.sin(angleRad);
+ return [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1] as Inputs.Base.TransformMatrix;
+ };
+ const rotationYMatrix = (angleRad) => {
+ const c = Math.cos(angleRad);
+ const s = Math.sin(angleRad);
+ return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1] as Inputs.Base.TransformMatrix;
+ };
+ const rotationZMatrix = (angleRad) => {
+ const c = Math.cos(angleRad);
+ const s = Math.sin(angleRad);
+ return [c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] as Inputs.Base.TransformMatrix;
+ };
+
+ describe("identity", () => {
+ it("should return the identity matrix", () => {
+ const result = transforms.identity();
+ expectMatrixCloseTo(result, identityMatrix);
+ });
+ });
+
+ describe("translationXYZ", () => {
+ it("should create a single translation matrix", () => {
+ const translationVec: Inputs.Base.Vector3 = [5, -10, 15];
+ const result = transforms.translationXYZ({ translation: translationVec });
+ expect(result).toBeInstanceOf(Array);
+ expect(result).toHaveLength(1);
+ expectMatrixCloseTo(result[0], translationMatrix(5, -10, 15));
+ });
+ });
+
+ describe("translationsXYZ", () => {
+ it("should create multiple translation matrices", () => {
+ const translations: Inputs.Base.Vector3[] = [[1, 2, 3], [4, 5, 6]];
+ const result = transforms.translationsXYZ({ translations });
+ expect(result).toBeInstanceOf(Array);
+ expect(result).toHaveLength(2);
+ expect(result[0]).toBeInstanceOf(Array);
+ expect(result[0]).toHaveLength(1);
+ expect(result[1]).toBeInstanceOf(Array);
+ expect(result[1]).toHaveLength(1);
+ expectMatrixCloseTo(result[0][0], translationMatrix(1, 2, 3));
+ expectMatrixCloseTo(result[1][0], translationMatrix(4, 5, 6));
+ });
+
+ it("should return an empty array for empty input", () => {
+ const result = transforms.translationsXYZ({ translations: [] });
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe("scaleXYZ", () => {
+ it("should create a single scaling matrix", () => {
+ const scaleVec: Inputs.Base.Vector3 = [2, 0.5, -1];
+ const result = transforms.scaleXYZ({ scaleXyz: scaleVec });
+ expect(result).toHaveLength(1);
+ expectMatrixCloseTo(result[0], scalingMatrix(2, 0.5, -1));
+ });
+ });
+
+ describe("uniformScale", () => {
+ it("should create a single uniform scaling matrix", () => {
+ const scaleFactor = 3.5;
+ const result = transforms.uniformScale({ scale: scaleFactor });
+ expect(result).toHaveLength(1);
+ expectMatrixCloseTo(result[0], scalingMatrix(3.5, 3.5, 3.5));
+ });
+ });
+
+ describe("scaleCenterXYZ", () => {
+ it("should create translate-scale-translate matrices", () => {
+ const scaleVec: Inputs.Base.Vector3 = [2, 3, 4];
+ const result = transforms.scaleCenterXYZ({ center: centerPoint, scaleXyz: scaleVec });
+
+ expect(result).toHaveLength(3);
+ const expected: Inputs.Base.TransformMatrixes = [
+ translationMatrix(-centerPoint[0], -centerPoint[1], -centerPoint[2]),
+ scalingMatrix(scaleVec[0], scaleVec[1], scaleVec[2]),
+ translationMatrix(centerPoint[0], centerPoint[1], centerPoint[2]),
+ ];
+ expectMatrixesCloseTo(result, expected);
+ });
+ });
+
+ describe("uniformScaleFromCenter", () => {
+ it("should create translate-uniform_scale-translate matrices", () => {
+ const scaleFactor = 5;
+ const result = transforms.uniformScaleFromCenter({ center: centerPoint, scale: scaleFactor });
+
+ expect(result).toHaveLength(3);
+ const expected: Inputs.Base.TransformMatrixes = [
+ translationMatrix(-centerPoint[0], -centerPoint[1], -centerPoint[2]),
+ scalingMatrix(scaleFactor, scaleFactor, scaleFactor),
+ translationMatrix(centerPoint[0], centerPoint[1], centerPoint[2]),
+ ];
+ expectMatrixesCloseTo(result, expected);
+ });
+ });
+
+ describe("rotationCenterX", () => {
+ it("should create translate-rotateX-translate matrices", () => {
+ const angleDeg = 90;
+ const angleRad = math.degToRad({ number: angleDeg });
+ const result = transforms.rotationCenterX({ center: centerPoint, angle: angleDeg });
+
+ expect(result).toHaveLength(3);
+ const expected: Inputs.Base.TransformMatrixes = [
+ translationMatrix(-centerPoint[0], -centerPoint[1], -centerPoint[2]),
+ rotationXMatrix(angleRad),
+ translationMatrix(centerPoint[0], centerPoint[1], centerPoint[2]),
+ ];
+ expectMatrixesCloseTo(result, expected);
+ });
+ });
+
+ describe("rotationCenterY", () => {
+ it("should create translate-rotateY-translate matrices", () => {
+ const angleDeg = -45;
+ const angleRad = math.degToRad({ number: angleDeg });
+ const result = transforms.rotationCenterY({ center: centerPoint, angle: angleDeg });
+
+ expect(result).toHaveLength(3);
+ const expected: Inputs.Base.TransformMatrixes = [
+ translationMatrix(-centerPoint[0], -centerPoint[1], -centerPoint[2]),
+ rotationYMatrix(angleRad),
+ translationMatrix(centerPoint[0], centerPoint[1], centerPoint[2]),
+ ];
+ expectMatrixesCloseTo(result, expected);
+ });
+ });
+
+ describe("rotationCenterZ", () => {
+ it("should create translate-rotateZ-translate matrices", () => {
+ const angleDeg = 180;
+ const angleRad = math.degToRad({ number: angleDeg });
+ const result = transforms.rotationCenterZ({ center: centerPoint, angle: angleDeg });
+
+ expect(result).toHaveLength(3);
+ const expected: Inputs.Base.TransformMatrixes = [
+ translationMatrix(-centerPoint[0], -centerPoint[1], -centerPoint[2]),
+ rotationZMatrix(angleRad),
+ translationMatrix(centerPoint[0], centerPoint[1], centerPoint[2]),
+ ];
+ expectMatrixesCloseTo(result, expected);
+ });
+ });
+
+ describe("rotationCenterAxis", () => {
+ it("should create translate-rotateAxis-translate matrices", () => {
+ const angleDeg = 90;
+ const angleRad = math.degToRad({ number: angleDeg });
+ const axis: Inputs.Base.Vector3 = [0, 1, 0];
+
+ const result = transforms.rotationCenterAxis({ center: centerPoint, axis: axis, angle: angleDeg });
+ expect(result).toHaveLength(3);
+
+ const expectedMiddleMatrix = rotationYMatrix(angleRad);
+
+ const expected: Inputs.Base.TransformMatrixes = [
+ translationMatrix(-centerPoint[0], -centerPoint[1], -centerPoint[2]),
+ expectedMiddleMatrix,
+ translationMatrix(centerPoint[0], centerPoint[1], centerPoint[2]),
+ ];
+ expectMatrixesCloseTo(result, expected);
+ });
+
+ it("should handle non-unit axis vector by normalizing it", () => {
+ const angleDeg = 180;
+ const angleRad = math.degToRad({ number: angleDeg });
+ const nonUnitAxis: Inputs.Base.Vector3 = [2, 0, 0];
+
+ const result = transforms.rotationCenterAxis({ center: centerPoint, axis: nonUnitAxis, angle: angleDeg });
+ expect(result).toHaveLength(3);
+
+ const expectedMiddleMatrix = rotationXMatrix(angleRad);
+
+ const expected: Inputs.Base.TransformMatrixes = [
+ translationMatrix(-centerPoint[0], -centerPoint[1], -centerPoint[2]),
+ expectedMiddleMatrix,
+ translationMatrix(centerPoint[0], centerPoint[1], centerPoint[2]),
+ ];
+ expectMatrixesCloseTo(result, expected);
+ });
+
+ });
+
+ describe("rotationCenterYawPitchRoll", () => {
+ it("should create translate-rotateYPR-translate matrices for pure Yaw (Y rot)", () => {
+ const yaw = 90, pitch = 0, roll = 0;
+ const result = transforms.rotationCenterYawPitchRoll({ center: centerPoint, yaw, pitch, roll });
+ expect(result).toHaveLength(3);
+ expect(result).toEqual([
+ [
+ 1, 0, 0, 0, 0, 1,
+ 0, 0, 0, 0, 1, 0,
+ -10, -20, -30, 1
+ ],
+ [
+ 2.220446049250313e-16,
+ 0,
+ -1,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ 1,
+ 0,
+ 2.220446049250313e-16,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ [
+ 1, 0, 0, 0, 0, 1,
+ 0, 0, 0, 0, 1, 0,
+ 10, 20, 30, 1
+ ]
+ ]);
+ });
+
+ // Add tests for pure Pitch (X rot) and pure Roll (Z rot) if desired
+ it("should create translate-rotateYPR-translate matrices for pure Pitch (X rot)", () => {
+ const yaw = 0, pitch = 90, roll = 0;
+ const angleRadX = math.degToRad({ number: pitch }); // Pitch corresponds to X
+
+ const result = transforms.rotationCenterYawPitchRoll({ center: centerPoint, yaw, pitch, roll });
+ expect(result).toHaveLength(3);
+
+ const expectedMiddleMatrix = rotationXMatrix(angleRadX);
+
+ const expected: Inputs.Base.TransformMatrixes = [
+ translationMatrix(-centerPoint[0], -centerPoint[1], -centerPoint[2]),
+ expectedMiddleMatrix,
+ translationMatrix(centerPoint[0], centerPoint[1], centerPoint[2]),
+ ];
+ expectMatrixesCloseTo(result, expected);
+ });
+
+ it("should create translate-rotateYPR-translate matrices for pure Roll (Z rot)", () => {
+ const yaw = 0, pitch = 0, roll = 90;
+ const angleRadZ = math.degToRad({ number: roll }); // Roll corresponds to Z
+
+ const result = transforms.rotationCenterYawPitchRoll({ center: centerPoint, yaw, pitch, roll });
+ expect(result).toHaveLength(3);
+
+ const expectedMiddleMatrix = rotationZMatrix(angleRadZ);
+
+ const expected: Inputs.Base.TransformMatrixes = [
+ translationMatrix(-centerPoint[0], -centerPoint[1], -centerPoint[2]),
+ expectedMiddleMatrix,
+ translationMatrix(centerPoint[0], centerPoint[1], centerPoint[2]),
+ ];
+ expectMatrixesCloseTo(result, expected);
+ });
+
+
+ it("should handle combined rotations", () => {
+ // Calculating the combined matrix manually is complex.
+ // We'll check the structure and the translation components.
+ const yaw = 45, pitch = 30, roll = 60;
+ const result = transforms.rotationCenterYawPitchRoll({ center: centerPoint, yaw, pitch, roll });
+ expect(result).toHaveLength(3);
+ expectMatrixCloseTo(result[0], translationMatrix(-centerPoint[0], -centerPoint[1], -centerPoint[2]));
+ expectMatrixCloseTo(result[2], translationMatrix(centerPoint[0], centerPoint[1], centerPoint[2]));
+ // Check the middle matrix is not identity (it should be a rotation)
+ expect(result[1]).not.toEqual(identityMatrix);
+ // TODO: For a more robust test, apply the resulting transform to a known point
+ // and verify its final position after YPR rotation. This tests the effect.
+ });
+ });
+
+
+ });
+
+});
\ No newline at end of file
diff --git a/packages/dev/base/lib/api/services/transforms.ts b/packages/dev/base/lib/api/services/transforms.ts
index b5116e0d..eee396c3 100644
--- a/packages/dev/base/lib/api/services/transforms.ts
+++ b/packages/dev/base/lib/api/services/transforms.ts
@@ -179,6 +179,17 @@ export class Transforms {
return inputs.translations.map(translation => [this.translation(translation[0], translation[1], translation[2])]) as Base.TransformMatrixes[];
}
+ /**
+ * Creates the identity transformation
+ * @returns transformation
+ * @group identity
+ * @shortname identity
+ * @drawable false
+ */
+ identity(): Base.TransformMatrix {
+ return [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0];
+ }
+
private translation(x: number, y: number, z: number) {
return [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, x, y, z, 1.0];
}
@@ -187,9 +198,6 @@ export class Transforms {
return [x, 0.0, 0.0, 0.0, 0.0, y, 0.0, 0.0, 0.0, 0.0, z, 0.0, 0.0, 0.0, 0.0, 1.0];
}
- private identity() {
- return [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0];
- }
private rotationAxis(axis: Base.Vector3, angle: number) {
const s = Math.sin(-angle);
diff --git a/packages/dev/base/lib/api/services/vector.ts b/packages/dev/base/lib/api/services/vector.ts
index 0016711c..1c4434d0 100644
--- a/packages/dev/base/lib/api/services/vector.ts
+++ b/packages/dev/base/lib/api/services/vector.ts
@@ -516,4 +516,17 @@ export class Vector {
sum(inputs: Inputs.Vector.VectorDto): number {
return inputs.vector.reduce((a, b) => a + b, 0);
}
+
+ /**
+ * Computes the squared length of the vector
+ * @param inputs Vector to compute the length
+ * @returns Number that is squared length of the vector
+ * @group base
+ * @shortname length squared
+ * @drawable false
+ */
+ lengthSq(inputs: Inputs.Vector.Vector3Dto): number {
+ const v = inputs.vector;
+ return v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
+ }
}
diff --git a/packages/dev/base/package.json b/packages/dev/base/package.json
index 6adf1198..6c82d9f1 100644
--- a/packages/dev/base/package.json
+++ b/packages/dev/base/package.json
@@ -86,7 +86,7 @@
"node_modules/(?!@bitbybit-dev)/"
],
"collectCoverageFrom": [
- "lib/api/**/*"
+ "lib/api/services/**/*"
]
}
}
\ No newline at end of file
diff --git a/packages/dev/core/LICENSE b/packages/dev/core/LICENSE
index d3f3c206..87a328f2 100644
--- a/packages/dev/core/LICENSE
+++ b/packages/dev/core/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c)2025 Bit By Bit Developers
+Copyright (c) 2025 Bit By Bit Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/dev/core/lib/api/bitbybit/index.ts b/packages/dev/core/lib/api/bitbybit/index.ts
index ded7a256..4e10ffc0 100644
--- a/packages/dev/core/lib/api/bitbybit/index.ts
+++ b/packages/dev/core/lib/api/bitbybit/index.ts
@@ -3,7 +3,5 @@ export * from "./verb";
export * from "./asset";
export * from "./base-types";
export * from "./json";
-export * from "./line";
-export * from "./polyline";
export * from "./tag";
export * from "./time";
\ No newline at end of file
diff --git a/packages/dev/core/lib/api/bitbybit/polyline.ts b/packages/dev/core/lib/api/bitbybit/polyline.ts
deleted file mode 100644
index 4f50e19d..00000000
--- a/packages/dev/core/lib/api/bitbybit/polyline.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { ContextBase } from "../context";
-import { GeometryHelper } from "@bitbybit-dev/base";
-import * as Inputs from "../inputs/inputs";
-
-/**
- * Contains various methods for polyline. Polyline in bitbybit is a simple object that has points property containing an array of points.
- * { points: number[][] }
- */
-
-export class Polyline {
-
- constructor(private readonly context: ContextBase, private readonly geometryHelper: GeometryHelper) { }
-
- /**
- * Converts a polyline to a NURBS curve
- * Returns the verbnurbs NurbsCurve object
- * @param inputs Polyline to be transformed to curve
- * @returns Verb nurbs curve
- */
- convertToNurbsCurve(inputs: Inputs.Polyline.PolylineDto): any {
- return this.context.verb.geom.NurbsCurve.byPoints(inputs.polyline.points, 1);
- }
-
- /**
- * Gets the length of the polyline
- * @param inputs Polyline to be queried
- * @returns Length of the polyline
- */
- length(inputs: Inputs.Polyline.PolylineDto): number {
- let distanceOfPolyline = 0;
- for (let i = 1; i < inputs.polyline.points.length; i++) {
- const previousPoint = inputs.polyline.points[i - 1];
- const currentPoint = inputs.polyline.points[i];
- distanceOfPolyline += this.context.verb.core.Vec.dist(previousPoint, currentPoint);
- }
- return distanceOfPolyline;
- }
-
- /**
- * Gets the number of points in the polyline
- * @param inputs Polyline to be queried
- * @returns Number of points in polyline
- */
- countPoints(inputs: Inputs.Polyline.PolylineDto): number {
- return inputs.polyline.points.length;
- }
-
- /**
- * Gets the points of the polyline
- * @param inputs Polyline to be queried
- * @returns Points of the polyline
- */
- getPoints(inputs: Inputs.Polyline.PolylineDto): number[][] {
- return inputs.polyline.points;
- }
-
- /**
- * Reverse the points of the polyline
- * @param inputs Polyline to be reversed
- * @returns Reversed polyline
- */
- reverse(inputs: Inputs.Polyline.PolylineDto): Inputs.Polyline.PolylinePropertiesDto {
- return { points: inputs.polyline.points.reverse() };
- }
-
- /**
- * Transform the polyline
- * @param inputs Polyline to be transformed
- * @returns Transformed polyline
- */
- transformPolyline(inputs: Inputs.Polyline.TransformPolylineDto): Inputs.Polyline.PolylinePropertiesDto {
- const transformation = inputs.transformation;
- let transformedControlPoints = inputs.polyline.points;
- transformedControlPoints = this.geometryHelper.transformControlPoints(transformation, transformedControlPoints);
- return { points: transformedControlPoints };
- }
-
- /**
- * Create the polyline
- * @param inputs Points of the polyline
- * @returns Polyline
- */
- create(inputs: Inputs.Polyline.PolylinePropertiesDto): Inputs.Polyline.PolylinePropertiesDto {
- return {
- points: inputs.points,
- };
- }
-
-}
-
diff --git a/packages/dev/core/lib/api/inputs/base-inputs.ts b/packages/dev/core/lib/api/inputs/base-inputs.ts
index 431fb88f..9a41567f 100644
--- a/packages/dev/core/lib/api/inputs/base-inputs.ts
+++ b/packages/dev/core/lib/api/inputs/base-inputs.ts
@@ -21,6 +21,12 @@ export namespace Base {
export type Vector3 = [number, number, number];
export type Axis3 = {origin: Base.Point3, direction: Base.Vector3};
export type Axis2 = {origin: Base.Point2, direction: Base.Vector2};
+ export type Segment2 = [Point2, Point2];
+ export type Segment3 = [Point3, Point3];
+ // Triangle plane is efficient defininition described by a normal vector and d value (N dot X = d)
+ export type TrianglePlane3 = { normal: Vector3; d: number; }
+ export type Triangle3 = [Base.Point3, Base.Point3, Base.Point3];
+ export type Mesh3 = Triangle3[];
export type Plane3 = { origin: Base.Point3, normal: Base.Vector3, direction: Base.Vector3 };
export type BoundingBox = { min: Base.Point3, max: Base.Point3, center?: Base.Point3, width?: number, height?: number, length?: number };
export type Line2 = { start: Base.Point2, end: Base.Point2 };
diff --git a/packages/dev/core/lib/api/inputs/index.ts b/packages/dev/core/lib/api/inputs/index.ts
index 7588ab4b..826157ec 100644
--- a/packages/dev/core/lib/api/inputs/index.ts
+++ b/packages/dev/core/lib/api/inputs/index.ts
@@ -1,7 +1,5 @@
export * from "./asset-inputs";
export * from "./json-inputs";
-export * from "./line-inputs";
-export * from "./polyline-inputs";
export * from "./tag-inputs";
export * from "./time-inputs";
export * from "./verb-inputs";
diff --git a/packages/dev/core/lib/api/inputs/inputs.ts b/packages/dev/core/lib/api/inputs/inputs.ts
index 32f1256e..f23fafcb 100644
--- a/packages/dev/core/lib/api/inputs/inputs.ts
+++ b/packages/dev/core/lib/api/inputs/inputs.ts
@@ -1,7 +1,5 @@
export * from "./asset-inputs";
export * from "./json-inputs";
-export * from "./line-inputs";
-export * from "./polyline-inputs";
export * from "./tag-inputs";
export * from "./time-inputs";
export * from "./verb-inputs";
diff --git a/packages/dev/jscad-worker/LICENSE b/packages/dev/jscad-worker/LICENSE
index d3f3c206..87a328f2 100644
--- a/packages/dev/jscad-worker/LICENSE
+++ b/packages/dev/jscad-worker/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c)2025 Bit By Bit Developers
+Copyright (c) 2025 Bit By Bit Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/dev/jscad/LICENSE b/packages/dev/jscad/LICENSE
index d3f3c206..87a328f2 100644
--- a/packages/dev/jscad/LICENSE
+++ b/packages/dev/jscad/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c)2025 Bit By Bit Developers
+Copyright (c) 2025 Bit By Bit Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/dev/jscad/lib/api/inputs/base-inputs.ts b/packages/dev/jscad/lib/api/inputs/base-inputs.ts
index 5f06f5c6..e0c863b6 100644
--- a/packages/dev/jscad/lib/api/inputs/base-inputs.ts
+++ b/packages/dev/jscad/lib/api/inputs/base-inputs.ts
@@ -8,6 +8,12 @@ export namespace Base {
export type Vector3 = [number, number, number];
export type Axis3 = {origin: Base.Point3, direction: Base.Vector3};
export type Axis2 = {origin: Base.Point2, direction: Base.Vector2};
+ export type Segment2 = [Point2, Point2];
+ export type Segment3 = [Point3, Point3];
+ // Triangle plane is efficient defininition described by a normal vector and d value (N dot X = d)
+ export type TrianglePlane3 = { normal: Vector3; d: number; }
+ export type Triangle3 = [Base.Point3, Base.Point3, Base.Point3];
+ export type Mesh3 = Triangle3[];
export type Plane3 = { origin: Base.Point3, normal: Base.Vector3, direction: Base.Vector3 };
export type BoundingBox = { min: Base.Point3, max: Base.Point3, center?: Base.Point3, width?: number, height?: number, length?: number };
export type Line2 = { start: Base.Point2, end: Base.Point2 };
diff --git a/packages/dev/manifold-worker/LICENSE b/packages/dev/manifold-worker/LICENSE
index d3f3c206..87a328f2 100644
--- a/packages/dev/manifold-worker/LICENSE
+++ b/packages/dev/manifold-worker/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c)2025 Bit By Bit Developers
+Copyright (c) 2025 Bit By Bit Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/dev/manifold-worker/lib/api/manifold/shapes.ts b/packages/dev/manifold-worker/lib/api/manifold/shapes.ts
index 6267d2e3..47bd43f7 100644
--- a/packages/dev/manifold-worker/lib/api/manifold/shapes.ts
+++ b/packages/dev/manifold-worker/lib/api/manifold/shapes.ts
@@ -32,6 +32,18 @@ export class ManifoldShapes {
return this.manifoldWorkerManager.genericCallToWorkerPromise("manifold.shapes.manifoldFromMesh", inputs);
}
+ /**
+ * Create a Manifold from a set of polygon points describing triangles.
+ * @param inputs Polygon points
+ * @returns Manifold
+ * @group create
+ * @shortname from polygon points
+ * @drawable true
+ */
+ fromPolygonPoints(inputs: Inputs.Manifold.FromPolygonPointsDto): Promise {
+ return this.manifoldWorkerManager.genericCallToWorkerPromise("manifold.shapes.fromPolygonPoints", inputs);
+ }
+
/**
* Create a 3D cube shape
* @param inputs Cube parameters
diff --git a/packages/dev/manifold/LICENSE b/packages/dev/manifold/LICENSE
index d3f3c206..87a328f2 100644
--- a/packages/dev/manifold/LICENSE
+++ b/packages/dev/manifold/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c)2025 Bit By Bit Developers
+Copyright (c) 2025 Bit By Bit Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/dev/manifold/lib/api/inputs/base-inputs.ts b/packages/dev/manifold/lib/api/inputs/base-inputs.ts
index 5f06f5c6..e0c863b6 100644
--- a/packages/dev/manifold/lib/api/inputs/base-inputs.ts
+++ b/packages/dev/manifold/lib/api/inputs/base-inputs.ts
@@ -8,6 +8,12 @@ export namespace Base {
export type Vector3 = [number, number, number];
export type Axis3 = {origin: Base.Point3, direction: Base.Vector3};
export type Axis2 = {origin: Base.Point2, direction: Base.Vector2};
+ export type Segment2 = [Point2, Point2];
+ export type Segment3 = [Point3, Point3];
+ // Triangle plane is efficient defininition described by a normal vector and d value (N dot X = d)
+ export type TrianglePlane3 = { normal: Vector3; d: number; }
+ export type Triangle3 = [Base.Point3, Base.Point3, Base.Point3];
+ export type Mesh3 = Triangle3[];
export type Plane3 = { origin: Base.Point3, normal: Base.Vector3, direction: Base.Vector3 };
export type BoundingBox = { min: Base.Point3, max: Base.Point3, center?: Base.Point3, width?: number, height?: number, length?: number };
export type Line2 = { start: Base.Point2, end: Base.Point2 };
diff --git a/packages/dev/manifold/lib/api/inputs/manifold-inputs.ts b/packages/dev/manifold/lib/api/inputs/manifold-inputs.ts
index 627f37df..a0c28392 100644
--- a/packages/dev/manifold/lib/api/inputs/manifold-inputs.ts
+++ b/packages/dev/manifold/lib/api/inputs/manifold-inputs.ts
@@ -163,6 +163,15 @@ export namespace Manifold {
*/
mesh: DecomposedManifoldMeshDto;
}
+ export class FromPolygonPointsDto {
+ constructor(polygonPoints?: Base.Point3[][]) {
+ if (polygonPoints !== undefined) { this.polygonPoints = polygonPoints; }
+ }
+ /**
+ * Points describing polygons
+ */
+ polygonPoints?: Base.Point3[][];
+ }
export class CubeDto {
constructor(center?: boolean, size?: number) {
if (center !== undefined) { this.center = center; }
@@ -498,7 +507,7 @@ export namespace Manifold {
*/
number = 1;
}
- export class ManifoldSmoothByNormalsDto{
+ export class ManifoldSmoothByNormalsDto {
constructor(manifold?: T, normalIdx?: number) {
if (manifold !== undefined) { this.manifold = manifold; }
if (normalIdx !== undefined) { this.normalIdx = normalIdx; }
diff --git a/packages/dev/manifold/lib/api/services/manifold/manifold-shapes.ts b/packages/dev/manifold/lib/api/services/manifold/manifold-shapes.ts
index 122588d2..e36cadb2 100644
--- a/packages/dev/manifold/lib/api/services/manifold/manifold-shapes.ts
+++ b/packages/dev/manifold/lib/api/services/manifold/manifold-shapes.ts
@@ -14,6 +14,77 @@ export class ManifoldShapes {
return new Manifold(inputs.mesh as Manifold3D.Mesh);
}
+ fromPolygonPoints(inputs: Inputs.Manifold.FromPolygonPointsDto): Manifold3D.Manifold {
+ const { Manifold } = this.manifold;
+ const polygons = inputs.polygonPoints;
+ // Map to store unique vertices and their assigned index.
+ // Key: string representation "x,y,z"
+ // Value: index in the unique vertex list
+ const vertexMap = new Map();
+
+ const uniqueVertexCoords: number[] = [];
+ const triangleIndices: number[] = [];
+
+ let vertexIndexCounter = 0;
+
+ // --- Iterate, Deduplicate, and Store Unique Vertices/Indices ---
+ for (const triangle of polygons) {
+ if (!triangle || triangle.length !== 3) {
+ console.warn(`Skipping invalid polygon data (expected 3 vertices): ${JSON.stringify(triangle)}`);
+ continue; // Skip malformed triangles
+ }
+
+ for (const point of triangle) {
+ if (!point || point.length !== 3 || point.some(isNaN)) {
+ // Handle potential malformed point data more robustly
+ console.warn(`Skipping invalid point data in triangle: ${JSON.stringify(point)}`);
+ // Let's throw an error for clearer failure:
+ throw new Error(`Invalid point data encountered: ${JSON.stringify(point)} in triangle ${JSON.stringify(triangle)}`);
+ }
+
+ // Create a unique key for the vertex based on its coordinates
+ // Using string concatenation is simple for exact matches.
+ // Be aware of potential floating-point precision issues if vertices
+ // might be *very* slightly different but should be treated as the same.
+ const vertexKey = `${point[0]},${point[1]},${point[2]}`;
+
+ let index: number;
+
+ // Check if this vertex has already been seen
+ if (vertexMap.has(vertexKey)) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ index = vertexMap.get(vertexKey)!;
+ } else {
+ // It's a new unique vertex
+ index = vertexIndexCounter;
+ vertexMap.set(vertexKey, index);
+ uniqueVertexCoords.push(point[0], point[1], point[2]);
+ vertexIndexCounter++;
+ }
+
+ // Add the index (either existing or new) to the triangle indices list
+ triangleIndices.push(index);
+ }
+ }
+
+ // --- Create TypedArrays ---
+
+ // Number of properties per vertex (x, y, z)
+ // If we had normals, UVs etc., this would be higher, we're not dealing with that here when making meshes from simple polygon points.
+ const numProp = 3;
+
+ const vertProperties = Float32Array.from(uniqueVertexCoords);
+ const triVerts = Uint32Array.from(triangleIndices);
+
+ // --- Populate the DTO ---
+ const meshDto = new Inputs.Manifold.DecomposedManifoldMeshDto();
+ meshDto.numProp = numProp;
+ meshDto.vertProperties = vertProperties;
+ meshDto.triVerts = triVerts;
+
+ return new Manifold(meshDto as Manifold3D.Mesh);
+ }
+
cube(inputs: Inputs.Manifold.CubeDto): Manifold3D.Manifold {
const { Manifold } = this.manifold;
const { cube } = Manifold;
diff --git a/packages/dev/occt-worker/LICENSE b/packages/dev/occt-worker/LICENSE
index d3f3c206..87a328f2 100644
--- a/packages/dev/occt-worker/LICENSE
+++ b/packages/dev/occt-worker/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c)2025 Bit By Bit Developers
+Copyright (c) 2025 Bit By Bit Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/dev/occt-worker/lib/api/occt/occt.ts b/packages/dev/occt-worker/lib/api/occt/occt.ts
index 55685aef..2b65f2de 100644
--- a/packages/dev/occt-worker/lib/api/occt/occt.ts
+++ b/packages/dev/occt-worker/lib/api/occt/occt.ts
@@ -39,13 +39,23 @@ export class OCCT {
this.io = new OCCTIO(occWorkerManager);
}
+ /**
+ * Creates polygon points from the shape faces
+ * @param inputs shape
+ * @group convert
+ * @shortname faces to polygon points
+ * @drawable false
+ */
+ async shapeFacesToPolygonPoints(inputs: Inputs.OCCT.ShapeFacesToPolygonPointsDto): Promise {
+ return await this.occWorkerManager.genericCallToWorkerPromise("shapeFacesToPolygonPoints", inputs);
+ }
+
/**
* Creates mesh from the shape
* @param inputs shape
- * @group drawing
+ * @group convert
* @shortname shape to mesh
* @drawable false
- * @ignore true
*/
async shapeToMesh(inputs: Inputs.OCCT.ShapeToMeshDto): Promise {
return await this.occWorkerManager.genericCallToWorkerPromise("shapeToMesh", inputs);
@@ -54,10 +64,9 @@ export class OCCT {
/**
* Creates mesh from the shape
* @param inputs shape
- * @group drawing
+ * @group convert
* @shortname shape to mesh
* @drawable false
- * @ignore true
*/
async shapesToMeshes(inputs: Inputs.OCCT.ShapesToMeshesDto): Promise {
return await this.occWorkerManager.genericCallToWorkerPromise("shapesToMeshes", inputs);
diff --git a/packages/dev/occt/LICENSE b/packages/dev/occt/LICENSE
index d3f3c206..87a328f2 100644
--- a/packages/dev/occt/LICENSE
+++ b/packages/dev/occt/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c)2025 Bit By Bit Developers
+Copyright (c) 2025 Bit By Bit Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/dev/occt/lib/api/inputs/occ-inputs.ts b/packages/dev/occt/lib/api/inputs/occ-inputs.ts
index 0bcdd075..97f1ad5d 100644
--- a/packages/dev/occt/lib/api/inputs/occ-inputs.ts
+++ b/packages/dev/occt/lib/api/inputs/occ-inputs.ts
@@ -4981,6 +4981,37 @@ export namespace OCCT {
*/
adjustYtoZ = false;
}
+ export class ShapeFacesToPolygonPointsDto {
+ constructor(shape?: T, precision?: number, adjustYtoZ?: boolean, reversedPoints?: boolean) {
+ if (shape !== undefined) { this.shape = shape; }
+ if (precision !== undefined) { this.precision = precision; }
+ if (adjustYtoZ !== undefined) { this.adjustYtoZ = adjustYtoZ; }
+ if (reversedPoints !== undefined) { this.reversedPoints = reversedPoints; }
+ }
+ /**
+ * Shape to save
+ * @default undefined
+ */
+ shape: T;
+ /**
+ * Precision of the mesh
+ * @default 0.01
+ * @minimum 0
+ * @maximum Infinity
+ * @step 0.001
+ */
+ precision = 0.01;
+ /**
+ * Adjust Y (up) coordinate system to Z (up) coordinate system
+ * @default false
+ */
+ adjustYtoZ = false;
+ /**
+ * Reverse the order of the points describing the polygon because some CAD kernels use the opposite order
+ * @default false
+ */
+ reversedPoints = false;
+ }
export class ShapesToMeshesDto {
constructor(shapes?: T[], precision?: number, adjustYtoZ?: boolean) {
if (shapes !== undefined) { this.shapes = shapes; }
diff --git a/packages/dev/occt/lib/occ-service.ts b/packages/dev/occt/lib/occ-service.ts
index de0994df..b5ed2483 100644
--- a/packages/dev/occt/lib/occ-service.ts
+++ b/packages/dev/occt/lib/occ-service.ts
@@ -42,11 +42,36 @@ export class OCCTService {
this.io = new OCCTIO(occ, och);
}
- shapesToMeshes(inputs: { shapes: TopoDS_Shape[], precision: number, adjustYtoZ: boolean }): Inputs.OCCT.DecomposedMeshDto[] {
- return inputs.shapes.map(shape => this.shapeToMesh({shape, precision: inputs.precision, adjustYtoZ: inputs.adjustYtoZ}));
+ shapeFacesToPolygonPoints(inputs: Inputs.OCCT.ShapeFacesToPolygonPointsDto): Inputs.Base.Point3[][] {
+ const def = this.shapeToMesh(inputs);
+ const res = [];
+ def.faceList.forEach(face => {
+ const vertices = face.vertex_coord;
+ const indices = face.tri_indexes;
+ for (let i = 0; i < indices.length; i += 3) {
+ const p1 = indices[i];
+ const p2 = indices[i + 1];
+ const p3 = indices[i + 2];
+ let pts =[
+ [vertices[p1 * 3], vertices[p1 * 3 + 1], vertices[p1 * 3 + 2]],
+ [vertices[p2 * 3], vertices[p2 * 3 + 1], vertices[p2 * 3 + 2]],
+ [vertices[p3 * 3], vertices[p3 * 3 + 1], vertices[p3 * 3 + 2]],
+ ];
+ if(inputs.reversedPoints){
+ pts = pts.reverse();
+ }
+ res.push(pts);
+ }
+ });
+
+ return res;
+ }
+
+ shapesToMeshes(inputs: Inputs.OCCT.ShapesToMeshesDto): Inputs.OCCT.DecomposedMeshDto[] {
+ return inputs.shapes.map(shape => this.shapeToMesh({ shape, precision: inputs.precision, adjustYtoZ: inputs.adjustYtoZ }));
}
- shapeToMesh(inputs: { shape: TopoDS_Shape, precision: number, adjustYtoZ: boolean }): Inputs.OCCT.DecomposedMeshDto {
+ shapeToMesh(inputs: Inputs.OCCT.ShapeToMeshDto): Inputs.OCCT.DecomposedMeshDto {
const faceList: Inputs.OCCT.DecomposedFaceDto[] = [];
const edgeList: Inputs.OCCT.DecomposedEdgeDto[] = [];
diff --git a/packages/dev/threejs/LICENSE b/packages/dev/threejs/LICENSE
index d3f3c206..87a328f2 100644
--- a/packages/dev/threejs/LICENSE
+++ b/packages/dev/threejs/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c)2025 Bit By Bit Developers
+Copyright (c) 2025 Bit By Bit Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/dev/threejs/lib/api/bitbybit-base.test.ts b/packages/dev/threejs/lib/api/bitbybit-base.test.ts
deleted file mode 100644
index b12310d6..00000000
--- a/packages/dev/threejs/lib/api/bitbybit-base.test.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { BitByBitBase } from "./bitbybit-base";
-
-describe("Bitbybit base unit tests", () => {
-
- it("should create BitByBitBase", () => {
- const bitbybit = new BitByBitBase();
- expect(bitbybit).toBeDefined();
- });
-
- it("should init bitbybitbase", () => {
- const bitbybit = new BitByBitBase();
-
- bitbybit.init({ mockScene: true } as jest.Mock, { mockWorker: true } as jest.Mock, { mockWorker: true } as jest.Mock);
- expect(bitbybit.context).toBeDefined();
- expect(bitbybit.context.scene).toBeDefined();
- expect(bitbybit.occtWorkerManager["occWorker"]).toBeDefined();
- expect(bitbybit.jscadWorkerManager["jscadWorker"]).toBeDefined();
-
- });
-
- it("should init bitbybitbase without occt and jscad workers", () => {
- const bitbybit = new BitByBitBase();
-
- bitbybit.init({ mockScene: true } as jest.Mock,undefined, undefined);
- expect(bitbybit.context).toBeDefined();
- expect(bitbybit.context.scene).toBeDefined();
- expect(bitbybit.occtWorkerManager["occWorker"]).not.toBeDefined();
- expect(bitbybit.jscadWorkerManager["jscadWorker"]).not.toBeDefined();
-
- });
-});
diff --git a/packages/dev/threejs/lib/api/bitbybit-base.ts b/packages/dev/threejs/lib/api/bitbybit-base.ts
index bf786b81..56a34418 100644
--- a/packages/dev/threejs/lib/api/bitbybit-base.ts
+++ b/packages/dev/threejs/lib/api/bitbybit-base.ts
@@ -1,8 +1,6 @@
import { OCCT as BaseOCCT, OCCTWorkerManager } from "@bitbybit-dev/occt-worker";
import { JSONPath } from "jsonpath-plus";
import {
- Line,
- Polyline,
Verb,
Tag,
Time,
@@ -14,9 +12,11 @@ import { JSCAD } from "@bitbybit-dev/jscad-worker";
import { ManifoldBitByBit } from "@bitbybit-dev/manifold-worker";
import {
Vector,
- Point, TextBitByBit, Color,
+ Point,
+ Line,
+ Polyline, TextBitByBit, Color,
MathBitByBit, GeometryHelper,
- Lists, Logic, Transforms, Dates
+ Lists, Logic, Transforms, Dates, MeshBitByBit
} from "@bitbybit-dev/base";
import { Draw } from "./bitbybit/draw";
import { Context } from "./context";
@@ -53,6 +53,7 @@ export class BitByBitBase {
public tag: Tag;
public time: Time;
public occt: OCCTW & BaseOCCT;
+ public mesh: MeshBitByBit;
public asset: Asset;
public color: Color;
@@ -72,10 +73,10 @@ export class BitByBitBase {
this.tag = new Tag(this.context);
this.draw = new Draw(drawHelper, this.context, this.tag);
this.color = new Color(this.math);
- this.line = new Line(this.context, geometryHelper);
+ this.line = new Line(this.point, geometryHelper);
this.transforms = new Transforms(this.vector, this.math);
- this.point = new Point(geometryHelper, this.transforms, this.vector);
- this.polyline = new Polyline(this.context, geometryHelper);
+ this.point = new Point(geometryHelper, this.transforms, this.vector);
+ this.polyline = new Polyline(this.vector, this.point, geometryHelper);
this.verb = new Verb(this.context, geometryHelper, this.math);
this.time = new Time(this.context);
this.occt = new OCCTW(this.context, this.occtWorkerManager);
@@ -85,6 +86,7 @@ export class BitByBitBase {
this.text = new TextBitByBit(this.point);
this.dates = new Dates();
this.lists = new Lists();
+ this.mesh = new MeshBitByBit(this.vector, this.polyline);
}
init(scene: THREEJS.Scene, occt?: Worker, jscad?: Worker, manifold?: Worker) {
diff --git a/packages/dev/threejs/lib/api/inputs/base-inputs.ts b/packages/dev/threejs/lib/api/inputs/base-inputs.ts
index b53373ab..bcd500a2 100644
--- a/packages/dev/threejs/lib/api/inputs/base-inputs.ts
+++ b/packages/dev/threejs/lib/api/inputs/base-inputs.ts
@@ -9,6 +9,12 @@ export namespace Base {
export type Vector3 = [number, number, number];
export type Axis3 = {origin: Base.Point3, direction: Base.Vector3};
export type Axis2 = {origin: Base.Point2, direction: Base.Vector2};
+ export type Segment2 = [Point2, Point2];
+ export type Segment3 = [Point3, Point3];
+ // Triangle plane is efficient defininition described by a normal vector and d value (N dot X = d)
+ export type TrianglePlane3 = { normal: Vector3; d: number; }
+ export type Triangle3 = [Base.Point3, Base.Point3, Base.Point3];
+ export type Mesh3 = Triangle3[];
export type Plane3 = { origin: Base.Point3, normal: Base.Vector3, direction: Base.Vector3 };
export type BoundingBox = { min: Base.Point3, max: Base.Point3, center?: Base.Point3, width?: number, height?: number, length?: number };
export type Line2 = { start: Base.Point2, end: Base.Point2 };