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. + +Picture showing bitbybit.dev platform + 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 -Picture showing bitbybit.dev platform - -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 };