brep-io-kernel 1.0.0-ci.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +32 -0
- package/README.md +157 -0
- package/dist-kernel/brep-kernel.js +74699 -0
- package/package.json +58 -0
- package/src/BREP/AssemblyComponent.js +42 -0
- package/src/BREP/BREP.js +43 -0
- package/src/BREP/BetterSolid.js +805 -0
- package/src/BREP/Edge.js +103 -0
- package/src/BREP/Extrude.js +403 -0
- package/src/BREP/Face.js +187 -0
- package/src/BREP/MeshRepairer.js +634 -0
- package/src/BREP/OffsetShellSolid.js +614 -0
- package/src/BREP/PointCloudWrap.js +302 -0
- package/src/BREP/Revolve.js +345 -0
- package/src/BREP/SolidMethods/authoring.js +112 -0
- package/src/BREP/SolidMethods/booleanOps.js +230 -0
- package/src/BREP/SolidMethods/chamfer.js +122 -0
- package/src/BREP/SolidMethods/edgeResolution.js +25 -0
- package/src/BREP/SolidMethods/fillet.js +792 -0
- package/src/BREP/SolidMethods/index.js +72 -0
- package/src/BREP/SolidMethods/io.js +105 -0
- package/src/BREP/SolidMethods/lifecycle.js +103 -0
- package/src/BREP/SolidMethods/manifoldOps.js +375 -0
- package/src/BREP/SolidMethods/meshCleanup.js +2512 -0
- package/src/BREP/SolidMethods/meshQueries.js +264 -0
- package/src/BREP/SolidMethods/metadata.js +106 -0
- package/src/BREP/SolidMethods/metrics.js +51 -0
- package/src/BREP/SolidMethods/transforms.js +361 -0
- package/src/BREP/SolidMethods/visualize.js +508 -0
- package/src/BREP/SolidShared.js +26 -0
- package/src/BREP/Sweep.js +1596 -0
- package/src/BREP/Tube.js +857 -0
- package/src/BREP/Vertex.js +43 -0
- package/src/BREP/applyBooleanOperation.js +704 -0
- package/src/BREP/boundsUtils.js +48 -0
- package/src/BREP/chamfer.js +551 -0
- package/src/BREP/edgePolylineUtils.js +85 -0
- package/src/BREP/fillets/common.js +388 -0
- package/src/BREP/fillets/fillet.js +1422 -0
- package/src/BREP/fillets/filletGeometry.js +15 -0
- package/src/BREP/fillets/inset.js +389 -0
- package/src/BREP/fillets/offsetHelper.js +143 -0
- package/src/BREP/fillets/outset.js +88 -0
- package/src/BREP/helix.js +193 -0
- package/src/BREP/meshToBrep.js +234 -0
- package/src/BREP/primitives.js +279 -0
- package/src/BREP/setupManifold.js +71 -0
- package/src/BREP/threadGeometry.js +1120 -0
- package/src/BREP/triangleUtils.js +8 -0
- package/src/BREP/triangulate.js +608 -0
- package/src/FeatureRegistry.js +183 -0
- package/src/PartHistory.js +1132 -0
- package/src/UI/AccordionWidget.js +292 -0
- package/src/UI/CADmaterials.js +850 -0
- package/src/UI/EnvMonacoEditor.js +522 -0
- package/src/UI/FloatingWindow.js +396 -0
- package/src/UI/HistoryWidget.js +457 -0
- package/src/UI/MainToolbar.js +131 -0
- package/src/UI/ModelLibraryView.js +194 -0
- package/src/UI/OrthoCameraIdle.js +206 -0
- package/src/UI/PluginsWidget.js +280 -0
- package/src/UI/SceneListing.js +606 -0
- package/src/UI/SelectionFilter.js +629 -0
- package/src/UI/ViewCube.js +389 -0
- package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +329 -0
- package/src/UI/assembly/AssemblyConstraintControlsWidget.js +282 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.css +292 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.js +1373 -0
- package/src/UI/assembly/constraintFaceUtils.js +115 -0
- package/src/UI/assembly/constraintHighlightUtils.js +70 -0
- package/src/UI/assembly/constraintLabelUtils.js +31 -0
- package/src/UI/assembly/constraintPointUtils.js +64 -0
- package/src/UI/assembly/constraintSelectionUtils.js +185 -0
- package/src/UI/assembly/constraintStatusUtils.js +142 -0
- package/src/UI/componentSelectorModal.js +240 -0
- package/src/UI/controls/CombinedTransformControls.js +386 -0
- package/src/UI/dialogs.js +351 -0
- package/src/UI/expressionsManager.js +100 -0
- package/src/UI/featureDialogWidgets/booleanField.js +25 -0
- package/src/UI/featureDialogWidgets/booleanOperationField.js +97 -0
- package/src/UI/featureDialogWidgets/buttonField.js +45 -0
- package/src/UI/featureDialogWidgets/componentSelectorField.js +102 -0
- package/src/UI/featureDialogWidgets/defaultField.js +23 -0
- package/src/UI/featureDialogWidgets/fileField.js +66 -0
- package/src/UI/featureDialogWidgets/index.js +34 -0
- package/src/UI/featureDialogWidgets/numberField.js +165 -0
- package/src/UI/featureDialogWidgets/optionsField.js +33 -0
- package/src/UI/featureDialogWidgets/referenceSelectionField.js +208 -0
- package/src/UI/featureDialogWidgets/stringField.js +24 -0
- package/src/UI/featureDialogWidgets/textareaField.js +28 -0
- package/src/UI/featureDialogWidgets/threadDesignationField.js +160 -0
- package/src/UI/featureDialogWidgets/transformField.js +252 -0
- package/src/UI/featureDialogWidgets/utils.js +43 -0
- package/src/UI/featureDialogWidgets/vec3Field.js +133 -0
- package/src/UI/featureDialogs.js +1414 -0
- package/src/UI/fileManagerWidget.js +615 -0
- package/src/UI/history/HistoryCollectionWidget.js +1294 -0
- package/src/UI/history/historyCollectionWidget.css.js +257 -0
- package/src/UI/history/historyDisplayInfo.js +133 -0
- package/src/UI/mobile.js +28 -0
- package/src/UI/objectDump.js +442 -0
- package/src/UI/pmi/AnnotationCollectionWidget.js +120 -0
- package/src/UI/pmi/AnnotationHistory.js +353 -0
- package/src/UI/pmi/AnnotationRegistry.js +90 -0
- package/src/UI/pmi/BaseAnnotation.js +269 -0
- package/src/UI/pmi/LabelOverlay.css +102 -0
- package/src/UI/pmi/LabelOverlay.js +191 -0
- package/src/UI/pmi/PMIMode.js +1550 -0
- package/src/UI/pmi/PMIViewsWidget.js +1098 -0
- package/src/UI/pmi/annUtils.js +729 -0
- package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +647 -0
- package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +507 -0
- package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +462 -0
- package/src/UI/pmi/dimensions/LeaderAnnotation.js +403 -0
- package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +532 -0
- package/src/UI/pmi/dimensions/NoteAnnotation.js +110 -0
- package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +659 -0
- package/src/UI/pmi/pmiStyle.js +44 -0
- package/src/UI/sketcher/SketchMode3D.js +4095 -0
- package/src/UI/sketcher/dimensions.js +674 -0
- package/src/UI/sketcher/glyphs.js +236 -0
- package/src/UI/sketcher/highlights.js +60 -0
- package/src/UI/toolbarButtons/aboutButton.js +5 -0
- package/src/UI/toolbarButtons/exportButton.js +609 -0
- package/src/UI/toolbarButtons/flatPatternButton.js +307 -0
- package/src/UI/toolbarButtons/importButton.js +160 -0
- package/src/UI/toolbarButtons/inspectorToggleButton.js +12 -0
- package/src/UI/toolbarButtons/metadataButton.js +1063 -0
- package/src/UI/toolbarButtons/orientToFaceButton.js +114 -0
- package/src/UI/toolbarButtons/registerDefaultButtons.js +46 -0
- package/src/UI/toolbarButtons/saveButton.js +99 -0
- package/src/UI/toolbarButtons/scriptRunnerButton.js +302 -0
- package/src/UI/toolbarButtons/testsButton.js +26 -0
- package/src/UI/toolbarButtons/undoRedoButtons.js +25 -0
- package/src/UI/toolbarButtons/wireframeToggleButton.js +5 -0
- package/src/UI/toolbarButtons/zoomToFitButton.js +5 -0
- package/src/UI/triangleDebuggerWindow.js +945 -0
- package/src/UI/viewer.js +4228 -0
- package/src/assemblyConstraints/AssemblyConstraintHistory.js +1576 -0
- package/src/assemblyConstraints/AssemblyConstraintRegistry.js +120 -0
- package/src/assemblyConstraints/BaseAssemblyConstraint.js +66 -0
- package/src/assemblyConstraints/constraintExpressionUtils.js +35 -0
- package/src/assemblyConstraints/constraintUtils/parallelAlignment.js +676 -0
- package/src/assemblyConstraints/constraints/AngleConstraint.js +485 -0
- package/src/assemblyConstraints/constraints/CoincidentConstraint.js +194 -0
- package/src/assemblyConstraints/constraints/DistanceConstraint.js +616 -0
- package/src/assemblyConstraints/constraints/FixedConstraint.js +78 -0
- package/src/assemblyConstraints/constraints/ParallelConstraint.js +252 -0
- package/src/assemblyConstraints/constraints/TouchAlignConstraint.js +961 -0
- package/src/core/entities/HistoryCollectionBase.js +72 -0
- package/src/core/entities/ListEntityBase.js +109 -0
- package/src/core/entities/schemaProcesser.js +121 -0
- package/src/exporters/sheetMetalFlatPattern.js +659 -0
- package/src/exporters/sheetMetalUnfold.js +862 -0
- package/src/exporters/step.js +1135 -0
- package/src/exporters/threeMF.js +575 -0
- package/src/features/assemblyComponent/AssemblyComponentFeature.js +780 -0
- package/src/features/boolean/BooleanFeature.js +94 -0
- package/src/features/chamfer/ChamferFeature.js +116 -0
- package/src/features/datium/DatiumFeature.js +80 -0
- package/src/features/edgeFeatureUtils.js +41 -0
- package/src/features/extrude/ExtrudeFeature.js +143 -0
- package/src/features/fillet/FilletFeature.js +197 -0
- package/src/features/helix/HelixFeature.js +405 -0
- package/src/features/hole/HoleFeature.js +1050 -0
- package/src/features/hole/screwClearance.js +86 -0
- package/src/features/hole/threadDesignationCatalog.js +149 -0
- package/src/features/imageHeightSolid/ImageHeightmapSolidFeature.js +463 -0
- package/src/features/imageToFace/ImageToFaceFeature.js +727 -0
- package/src/features/imageToFace/imageEditor.js +1270 -0
- package/src/features/imageToFace/traceUtils.js +971 -0
- package/src/features/import3dModel/Import3dModelFeature.js +151 -0
- package/src/features/loft/LoftFeature.js +605 -0
- package/src/features/mirror/MirrorFeature.js +151 -0
- package/src/features/offsetFace/OffsetFaceFeature.js +370 -0
- package/src/features/offsetShell/OffsetShellFeature.js +89 -0
- package/src/features/overlapCleanup/OverlapCleanupFeature.js +85 -0
- package/src/features/pattern/PatternFeature.js +275 -0
- package/src/features/patternLinear/PatternLinearFeature.js +120 -0
- package/src/features/patternRadial/PatternRadialFeature.js +186 -0
- package/src/features/plane/PlaneFeature.js +154 -0
- package/src/features/primitiveCone/primitiveConeFeature.js +99 -0
- package/src/features/primitiveCube/primitiveCubeFeature.js +70 -0
- package/src/features/primitiveCylinder/primitiveCylinderFeature.js +91 -0
- package/src/features/primitivePyramid/primitivePyramidFeature.js +72 -0
- package/src/features/primitiveSphere/primitiveSphereFeature.js +62 -0
- package/src/features/primitiveTorus/primitiveTorusFeature.js +109 -0
- package/src/features/remesh/RemeshFeature.js +97 -0
- package/src/features/revolve/RevolveFeature.js +111 -0
- package/src/features/selectionUtils.js +118 -0
- package/src/features/sheetMetal/SheetMetalContourFlangeFeature.js +1656 -0
- package/src/features/sheetMetal/SheetMetalCutoutFeature.js +1056 -0
- package/src/features/sheetMetal/SheetMetalFlangeFeature.js +1568 -0
- package/src/features/sheetMetal/SheetMetalHemFeature.js +43 -0
- package/src/features/sheetMetal/SheetMetalObject.js +141 -0
- package/src/features/sheetMetal/SheetMetalTabFeature.js +176 -0
- package/src/features/sheetMetal/UNFOLD_NEUTRAL_REQUIREMENTS.md +153 -0
- package/src/features/sheetMetal/contour-flange-rebuild-spec.md +261 -0
- package/src/features/sheetMetal/profileUtils.js +25 -0
- package/src/features/sheetMetal/sheetMetalCleanup.js +9 -0
- package/src/features/sheetMetal/sheetMetalFaceTypes.js +146 -0
- package/src/features/sheetMetal/sheetMetalMetadata.js +165 -0
- package/src/features/sheetMetal/sheetMetalPipeline.js +169 -0
- package/src/features/sheetMetal/sheetMetalProfileUtils.js +216 -0
- package/src/features/sheetMetal/sheetMetalTabUtils.js +29 -0
- package/src/features/sheetMetal/sheetMetalTree.js +210 -0
- package/src/features/sketch/SketchFeature.js +955 -0
- package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +800 -0
- package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +704 -0
- package/src/features/sketch/sketchSolver2D/mathHelpersMod.js +307 -0
- package/src/features/spline/SplineEditorSession.js +988 -0
- package/src/features/spline/SplineFeature.js +1388 -0
- package/src/features/spline/splineUtils.js +218 -0
- package/src/features/sweep/SweepFeature.js +110 -0
- package/src/features/transform/TransformFeature.js +152 -0
- package/src/features/tube/TubeFeature.js +635 -0
- package/src/fs.proxy.js +625 -0
- package/src/idbStorage.js +254 -0
- package/src/index.js +12 -0
- package/src/main.js +15 -0
- package/src/metadataManager.js +64 -0
- package/src/path.proxy.js +277 -0
- package/src/plugins/ghLoader.worker.js +151 -0
- package/src/plugins/pluginManager.js +286 -0
- package/src/pmi/PMIViewsManager.js +134 -0
- package/src/services/componentLibrary.js +198 -0
- package/src/tests/ConsoleCapture.js +189 -0
- package/src/tests/S7-diagnostics-2025-12-23T18-37-23-570Z.json +630 -0
- package/src/tests/browserTests.js +597 -0
- package/src/tests/debugBoolean.js +225 -0
- package/src/tests/partFiles/badBoolean.json +957 -0
- package/src/tests/partFiles/extrudeTest.json +88 -0
- package/src/tests/partFiles/filletFail.json +58 -0
- package/src/tests/partFiles/import_TEst.part.part.json +646 -0
- package/src/tests/partFiles/sheetMetalHem.BREP.json +734 -0
- package/src/tests/test_boolean_subtract.js +27 -0
- package/src/tests/test_chamfer.js +17 -0
- package/src/tests/test_extrudeFace.js +24 -0
- package/src/tests/test_fillet.js +17 -0
- package/src/tests/test_fillet_nonClosed.js +45 -0
- package/src/tests/test_filletsMoreDifficult.js +46 -0
- package/src/tests/test_history_features_basic.js +149 -0
- package/src/tests/test_hole.js +282 -0
- package/src/tests/test_mirror.js +16 -0
- package/src/tests/test_offsetShellGrouping.js +85 -0
- package/src/tests/test_plane.js +4 -0
- package/src/tests/test_primitiveCone.js +11 -0
- package/src/tests/test_primitiveCube.js +7 -0
- package/src/tests/test_primitiveCylinder.js +8 -0
- package/src/tests/test_primitivePyramid.js +9 -0
- package/src/tests/test_primitiveSphere.js +17 -0
- package/src/tests/test_primitiveTorus.js +21 -0
- package/src/tests/test_pushFace.js +126 -0
- package/src/tests/test_sheetMetalContourFlange.js +125 -0
- package/src/tests/test_sheetMetal_features.js +80 -0
- package/src/tests/test_sketch_openLoop.js +45 -0
- package/src/tests/test_solidMetrics.js +58 -0
- package/src/tests/test_stlLoader.js +1889 -0
- package/src/tests/test_sweepFace.js +55 -0
- package/src/tests/test_tube.js +45 -0
- package/src/tests/test_tube_closedLoop.js +67 -0
- package/src/tests/tests.js +493 -0
- package/src/tools/assemblyConstraintDialogCapturePage.js +56 -0
- package/src/tools/dialogCapturePageFactory.js +227 -0
- package/src/tools/featureDialogCapturePage.js +47 -0
- package/src/tools/pmiAnnotationDialogCapturePage.js +60 -0
- package/src/utils/axisHelpers.js +99 -0
- package/src/utils/deepClone.js +69 -0
- package/src/utils/geometryTolerance.js +37 -0
- package/src/utils/normalizeTypeString.js +8 -0
- package/src/utils/xformMath.js +51 -0
|
@@ -0,0 +1,1422 @@
|
|
|
1
|
+
import { Solid } from "../BetterSolid.js";
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
import {
|
|
4
|
+
getScaleAdaptiveTolerance,
|
|
5
|
+
getDistanceTolerance,
|
|
6
|
+
getAngleTolerance,
|
|
7
|
+
trimFilletCaches,
|
|
8
|
+
getCachedFaceDataForTris,
|
|
9
|
+
averageFaceNormalObjectSpace,
|
|
10
|
+
localFaceNormalAtPoint,
|
|
11
|
+
projectPointOntoFaceTriangles,
|
|
12
|
+
batchProjectPointsOntoFace,
|
|
13
|
+
clamp,
|
|
14
|
+
isFiniteVec3,
|
|
15
|
+
} from './inset.js';
|
|
16
|
+
import {
|
|
17
|
+
solveCenterFromOffsetPlanesAnchored,
|
|
18
|
+
} from './outset.js';
|
|
19
|
+
import { Tube } from "../Tube.js";
|
|
20
|
+
import { computeFaceAreaFromTriangles } from "./filletGeometry.js";
|
|
21
|
+
|
|
22
|
+
export { clearFilletCaches, trimFilletCaches } from './inset.js';
|
|
23
|
+
export { fixTJunctionsAndPatchHoles } from './outset.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compute the fillet centerline polyline for an input edge without building the fillet solid.
|
|
27
|
+
*
|
|
28
|
+
* Returns polylines for:
|
|
29
|
+
* - points: locus of arc centers (centerline)
|
|
30
|
+
* - tangentA: tangency curve on face A (cylinder-face A intersection)
|
|
31
|
+
* - tangentB: tangency curve on face B (cylinder-face B intersection)
|
|
32
|
+
* All points are returned as objects {x,y,z} for readability.
|
|
33
|
+
* Downstream consumers that require array triples are still supported
|
|
34
|
+
* via Solid.addAuxEdge, which now accepts both objects and [x,y,z] arrays.
|
|
35
|
+
*
|
|
36
|
+
* @param {any} edgeObj Edge object (expects `.faces[0/1]`, `.userData.polylineLocal`, and `.parent` solid)
|
|
37
|
+
* @param {number} radius Fillet radius (> 0)
|
|
38
|
+
* @param {'INSET'|'OUTSET'} sideMode Preferred side relative to outward normals (default 'INSET')
|
|
39
|
+
* @returns {{ points: {x:number,y:number,z:number}[], tangentA?: {x:number,y:number,z:number}[], tangentB?: {x:number,y:number,z:number}[], edge?: {x:number,y:number,z:number}[], closedLoop: boolean }}
|
|
40
|
+
*/
|
|
41
|
+
export function computeFilletCenterline(edgeObj, radius = 1, sideMode = 'INSET') {
|
|
42
|
+
const out = { points: [], tangentA: [], tangentB: [], edge: [], closedLoop: false };
|
|
43
|
+
try {
|
|
44
|
+
if (!edgeObj || !Number.isFinite(radius) || radius <= 0) return out;
|
|
45
|
+
const solid = edgeObj.parentSolid || edgeObj.parent;
|
|
46
|
+
if (!solid) return out;
|
|
47
|
+
const faceA = edgeObj.faces?.[0] || null;
|
|
48
|
+
const faceB = edgeObj.faces?.[1] || null;
|
|
49
|
+
const faceNameA = faceA?.name || edgeObj?.userData?.faceA || null;
|
|
50
|
+
const faceNameB = faceB?.name || edgeObj?.userData?.faceB || null;
|
|
51
|
+
const segmentFacePairs = Array.isArray(edgeObj?.userData?.segmentFacePairs) ? edgeObj.userData.segmentFacePairs : null;
|
|
52
|
+
const useSegmentPairs = Array.isArray(segmentFacePairs) && segmentFacePairs.length > 0;
|
|
53
|
+
if (!useSegmentPairs && (!faceNameA || !faceNameB)) return out;
|
|
54
|
+
|
|
55
|
+
const polyLocal = edgeObj.userData?.polylineLocal;
|
|
56
|
+
if (!Array.isArray(polyLocal) || polyLocal.length < 2) return out;
|
|
57
|
+
|
|
58
|
+
// Tolerances (scale-adaptive to radius)
|
|
59
|
+
const eps = getScaleAdaptiveTolerance(radius, 1e-12);
|
|
60
|
+
const distTol = getDistanceTolerance(radius);
|
|
61
|
+
const angleTol = getAngleTolerance();
|
|
62
|
+
const vecLengthTol = getScaleAdaptiveTolerance(radius, 1e-14);
|
|
63
|
+
|
|
64
|
+
// Average outward normals per face (object space)
|
|
65
|
+
let nAavg = null;
|
|
66
|
+
let nBavg = null;
|
|
67
|
+
let trisA = null;
|
|
68
|
+
let trisB = null;
|
|
69
|
+
let faceKeyA = null;
|
|
70
|
+
let faceKeyB = null;
|
|
71
|
+
let faceDataA = null;
|
|
72
|
+
let faceDataB = null;
|
|
73
|
+
|
|
74
|
+
// Create unique cache keys that include solid identity and geometry hash to prevent cross-contamination
|
|
75
|
+
const solidId = solid.uuid || solid.name || solid.constructor.name;
|
|
76
|
+
if (!useSegmentPairs) {
|
|
77
|
+
nAavg = averageFaceNormalObjectSpace(solid, faceNameA);
|
|
78
|
+
nBavg = averageFaceNormalObjectSpace(solid, faceNameB);
|
|
79
|
+
if (!isFiniteVec3(nAavg) || !isFiniteVec3(nBavg)) return out;
|
|
80
|
+
|
|
81
|
+
// Fetch triangles and cached data for both faces once
|
|
82
|
+
trisA = solid.getFace(faceNameA);
|
|
83
|
+
trisB = solid.getFace(faceNameB);
|
|
84
|
+
if (!Array.isArray(trisA) || !trisA.length || !Array.isArray(trisB) || !trisB.length) return out;
|
|
85
|
+
|
|
86
|
+
const geometryHashA = trisA.length > 0 ? `${trisA.length}_${trisA[0].p1?.[0]?.toFixed(3) || 0}` : '0';
|
|
87
|
+
const geometryHashB = trisB.length > 0 ? `${trisB.length}_${trisB[0].p1?.[0]?.toFixed(3) || 0}` : '0';
|
|
88
|
+
faceKeyA = `${solidId}:${faceNameA}:${geometryHashA}`;
|
|
89
|
+
faceKeyB = `${solidId}:${faceNameB}:${geometryHashB}`;
|
|
90
|
+
faceDataA = getCachedFaceDataForTris(trisA, faceKeyA);
|
|
91
|
+
faceDataB = getCachedFaceDataForTris(trisB, faceKeyB);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Robust closed-loop detection (prefer flags, else compare endpoints)
|
|
95
|
+
let isClosed = !!(edgeObj.closedLoop || edgeObj.userData?.closedLoop);
|
|
96
|
+
if (!isClosed && polyLocal.length > 2) {
|
|
97
|
+
const a = polyLocal[0];
|
|
98
|
+
const b = polyLocal[polyLocal.length - 1];
|
|
99
|
+
if (a && b) {
|
|
100
|
+
const dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2];
|
|
101
|
+
const d2 = dx * dx + dy * dy + dz * dz;
|
|
102
|
+
const eps2 = distTol * distTol;
|
|
103
|
+
if (d2 <= eps2) isClosed = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
out.closedLoop = isClosed;
|
|
107
|
+
|
|
108
|
+
// Build sampling points: original vertices + midpoints (wrap for closed)
|
|
109
|
+
let samples;
|
|
110
|
+
let sampleSegmentIdx = null;
|
|
111
|
+
{
|
|
112
|
+
const src = polyLocal.slice();
|
|
113
|
+
if (isClosed && src.length > 2) {
|
|
114
|
+
const a = src[0], b = src[src.length - 1];
|
|
115
|
+
if (a && b && a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) src.pop();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const outPts = [];
|
|
119
|
+
const segIdxs = [];
|
|
120
|
+
const segCount = useSegmentPairs
|
|
121
|
+
? Math.max(1, segmentFacePairs.length)
|
|
122
|
+
: Math.max(1, (isClosed ? src.length : (src.length - 1)));
|
|
123
|
+
for (let i = 0; i < src.length; i++) {
|
|
124
|
+
const a = src[i];
|
|
125
|
+
const segIdxVertex = isClosed
|
|
126
|
+
? ((i - 1 + segCount) % segCount)
|
|
127
|
+
: Math.max(0, Math.min(i - 1, segCount - 1));
|
|
128
|
+
const segIdxMid = isClosed ? (i % segCount) : Math.min(i, segCount - 1);
|
|
129
|
+
outPts.push(new THREE.Vector3(a[0], a[1], a[2]));
|
|
130
|
+
segIdxs.push(segIdxVertex);
|
|
131
|
+
const j = i + 1;
|
|
132
|
+
if (isClosed) {
|
|
133
|
+
const b = src[(i + 1) % src.length];
|
|
134
|
+
outPts.push(new THREE.Vector3(0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]), 0.5 * (a[2] + b[2])));
|
|
135
|
+
segIdxs.push(segIdxMid);
|
|
136
|
+
} else if (j < src.length) {
|
|
137
|
+
const b = src[j];
|
|
138
|
+
outPts.push(new THREE.Vector3(0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]), 0.5 * (a[2] + b[2])));
|
|
139
|
+
segIdxs.push(segIdxMid);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
samples = outPts;
|
|
143
|
+
if (useSegmentPairs) sampleSegmentIdx = segIdxs;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Project samples to both faces and compute local normals
|
|
147
|
+
const sampleCount = samples.length;
|
|
148
|
+
let qAList = null;
|
|
149
|
+
let qBList = null;
|
|
150
|
+
let normalsA = null;
|
|
151
|
+
let normalsB = null;
|
|
152
|
+
let getFaceEntry = null;
|
|
153
|
+
if (!useSegmentPairs) {
|
|
154
|
+
qAList = batchProjectPointsOntoFace(trisA, samples, faceDataA, faceKeyA);
|
|
155
|
+
qBList = batchProjectPointsOntoFace(trisB, samples, faceDataB, faceKeyB);
|
|
156
|
+
normalsA = new Array(sampleCount);
|
|
157
|
+
normalsB = new Array(sampleCount);
|
|
158
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
159
|
+
normalsA[i] = localFaceNormalAtPoint(solid, faceNameA, qAList[i], faceDataA, faceKeyA) || nAavg;
|
|
160
|
+
normalsB[i] = localFaceNormalAtPoint(solid, faceNameB, qBList[i], faceDataB, faceKeyB) || nBavg;
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
const faceCache = new Map();
|
|
164
|
+
getFaceEntry = (faceName) => {
|
|
165
|
+
if (!faceName) return null;
|
|
166
|
+
if (faceCache.has(faceName)) return faceCache.get(faceName);
|
|
167
|
+
const tris = solid.getFace(faceName);
|
|
168
|
+
if (!Array.isArray(tris) || !tris.length) return null;
|
|
169
|
+
const geometryHash = tris.length > 0 ? `${tris.length}_${tris[0].p1?.[0]?.toFixed(3) || 0}` : '0';
|
|
170
|
+
const faceKey = `${solidId}:${faceName}:${geometryHash}`;
|
|
171
|
+
const data = getCachedFaceDataForTris(tris, faceKey);
|
|
172
|
+
const avg = averageFaceNormalObjectSpace(solid, faceName);
|
|
173
|
+
if (!isFiniteVec3(avg)) return null;
|
|
174
|
+
const entry = { tris, data, key: faceKey, avg };
|
|
175
|
+
faceCache.set(faceName, entry);
|
|
176
|
+
return entry;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Scratch vectors
|
|
181
|
+
const tangent = new THREE.Vector3();
|
|
182
|
+
const tempU = new THREE.Vector3();
|
|
183
|
+
const tempV = new THREE.Vector3();
|
|
184
|
+
const fallbackDir = new THREE.Vector3();
|
|
185
|
+
const bisector3 = new THREE.Vector3();
|
|
186
|
+
const avgNormalScratch = new THREE.Vector3();
|
|
187
|
+
|
|
188
|
+
const rEff = Math.max(eps, radius);
|
|
189
|
+
let centers = [];
|
|
190
|
+
let tanA = [];
|
|
191
|
+
let tanB = [];
|
|
192
|
+
let edgePts = [];
|
|
193
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
194
|
+
const p = samples[i];
|
|
195
|
+
const pPrev = isClosed ? samples[(i - 1 + sampleCount) % sampleCount] : samples[Math.max(0, i - 1)];
|
|
196
|
+
const pNext = isClosed ? samples[(i + 1) % sampleCount] : samples[Math.min(sampleCount - 1, i + 1)];
|
|
197
|
+
|
|
198
|
+
tangent.copy(pNext).sub(pPrev);
|
|
199
|
+
|
|
200
|
+
if (tangent.lengthSq() < vecLengthTol) continue;
|
|
201
|
+
tangent.normalize();
|
|
202
|
+
|
|
203
|
+
let qA = null;
|
|
204
|
+
let qB = null;
|
|
205
|
+
let nA = null;
|
|
206
|
+
let nB = null;
|
|
207
|
+
let faceNameAUse = faceNameA;
|
|
208
|
+
let faceNameBUse = faceNameB;
|
|
209
|
+
let faceDataAUse = faceDataA;
|
|
210
|
+
let faceDataBUse = faceDataB;
|
|
211
|
+
let trisAUse = trisA;
|
|
212
|
+
let trisBUse = trisB;
|
|
213
|
+
let faceKeyAUse = faceKeyA;
|
|
214
|
+
let faceKeyBUse = faceKeyB;
|
|
215
|
+
let nAavgUse = nAavg;
|
|
216
|
+
let nBavgUse = nBavg;
|
|
217
|
+
let allowRefine = true;
|
|
218
|
+
if (useSegmentPairs && typeof getFaceEntry === 'function') {
|
|
219
|
+
const segIdx = Array.isArray(sampleSegmentIdx) ? sampleSegmentIdx[i] : 0;
|
|
220
|
+
const pair = segmentFacePairs[segIdx] || segmentFacePairs[segmentFacePairs.length - 1];
|
|
221
|
+
if (pair && typeof pair === 'object' && !Array.isArray(pair) && pair.base && pair.sideA && pair.sideB) {
|
|
222
|
+
const baseName = pair.base;
|
|
223
|
+
const sideAName = pair.sideA;
|
|
224
|
+
const sideBName = pair.sideB;
|
|
225
|
+
const tBlend = Number.isFinite(pair.t) ? Math.max(0, Math.min(1, Number(pair.t))) : 0.5;
|
|
226
|
+
const entryBase = getFaceEntry(baseName);
|
|
227
|
+
const entrySideA = getFaceEntry(sideAName);
|
|
228
|
+
const entrySideB = getFaceEntry(sideBName);
|
|
229
|
+
if (!entryBase || !entrySideA || !entrySideB) continue;
|
|
230
|
+
faceNameAUse = baseName;
|
|
231
|
+
faceDataAUse = entryBase.data;
|
|
232
|
+
trisAUse = entryBase.tris;
|
|
233
|
+
faceKeyAUse = entryBase.key;
|
|
234
|
+
nAavgUse = entryBase.avg;
|
|
235
|
+
|
|
236
|
+
const qBase = projectPointOntoFaceTriangles(trisAUse, p, faceDataAUse, faceKeyAUse);
|
|
237
|
+
nA = localFaceNormalAtPoint(solid, baseName, qBase, faceDataAUse, faceKeyAUse) || nAavgUse;
|
|
238
|
+
qA = qBase;
|
|
239
|
+
|
|
240
|
+
const qSideA = projectPointOntoFaceTriangles(entrySideA.tris, p, entrySideA.data, entrySideA.key);
|
|
241
|
+
const qSideB = projectPointOntoFaceTriangles(entrySideB.tris, p, entrySideB.data, entrySideB.key);
|
|
242
|
+
const nSideA = localFaceNormalAtPoint(solid, sideAName, qSideA, entrySideA.data, entrySideA.key) || entrySideA.avg;
|
|
243
|
+
const nSideB = localFaceNormalAtPoint(solid, sideBName, qSideB, entrySideB.data, entrySideB.key) || entrySideB.avg;
|
|
244
|
+
const blend = nSideA.clone().multiplyScalar(1 - tBlend).addScaledVector(nSideB, tBlend);
|
|
245
|
+
nB = (blend.lengthSq() > 0) ? blend.normalize() : nSideA.clone();
|
|
246
|
+
qB = qSideA.clone().lerp(qSideB, tBlend);
|
|
247
|
+
faceNameBUse = sideAName;
|
|
248
|
+
faceDataBUse = entrySideA.data;
|
|
249
|
+
trisBUse = entrySideA.tris;
|
|
250
|
+
faceKeyBUse = entrySideA.key;
|
|
251
|
+
nBavgUse = entrySideA.avg;
|
|
252
|
+
allowRefine = false;
|
|
253
|
+
} else {
|
|
254
|
+
const segA = Array.isArray(pair) ? pair[0] : (pair?.faceA || pair?.a || null);
|
|
255
|
+
const segB = Array.isArray(pair) ? pair[1] : (pair?.faceB || pair?.b || null);
|
|
256
|
+
if (!segA || !segB) continue;
|
|
257
|
+
faceNameAUse = segA;
|
|
258
|
+
faceNameBUse = segB;
|
|
259
|
+
const entryA = getFaceEntry(faceNameAUse);
|
|
260
|
+
const entryB = getFaceEntry(faceNameBUse);
|
|
261
|
+
if (!entryA || !entryB) continue;
|
|
262
|
+
faceDataAUse = entryA.data;
|
|
263
|
+
faceDataBUse = entryB.data;
|
|
264
|
+
trisAUse = entryA.tris;
|
|
265
|
+
trisBUse = entryB.tris;
|
|
266
|
+
faceKeyAUse = entryA.key;
|
|
267
|
+
faceKeyBUse = entryB.key;
|
|
268
|
+
nAavgUse = entryA.avg;
|
|
269
|
+
nBavgUse = entryB.avg;
|
|
270
|
+
qA = projectPointOntoFaceTriangles(trisAUse, p, faceDataAUse, faceKeyAUse);
|
|
271
|
+
qB = projectPointOntoFaceTriangles(trisBUse, p, faceDataBUse, faceKeyBUse);
|
|
272
|
+
nA = localFaceNormalAtPoint(solid, faceNameAUse, qA, faceDataAUse, faceKeyAUse) || nAavgUse;
|
|
273
|
+
nB = localFaceNormalAtPoint(solid, faceNameBUse, qB, faceDataBUse, faceKeyBUse) || nBavgUse;
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
qA = qAList[i];
|
|
277
|
+
qB = qBList[i];
|
|
278
|
+
nA = normalsA[i] || nAavgUse;
|
|
279
|
+
nB = normalsB[i] || nBavgUse;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const vA3 = tempU.copy(nA).cross(tangent);
|
|
283
|
+
const vB3 = tempV.copy(nB).cross(tangent);
|
|
284
|
+
if (vA3.lengthSq() < eps || vB3.lengthSq() < eps) continue;
|
|
285
|
+
vA3.normalize(); vB3.normalize();
|
|
286
|
+
|
|
287
|
+
const u = vA3.clone();
|
|
288
|
+
const v = new THREE.Vector3().crossVectors(tangent, u).normalize();
|
|
289
|
+
const d0_2 = new THREE.Vector2(1, 0);
|
|
290
|
+
const d1_2 = new THREE.Vector2(vB3.dot(u), vB3.dot(v));
|
|
291
|
+
d1_2.normalize();
|
|
292
|
+
const dot2 = clamp(d0_2.x * d1_2.x + d0_2.y * d1_2.y, -1, 1);
|
|
293
|
+
const angAbs = Math.acos(dot2);
|
|
294
|
+
const sinHalf = Math.sin(0.5 * angAbs);
|
|
295
|
+
if (Math.abs(sinHalf) < angleTol) continue;
|
|
296
|
+
const expectDist = rEff / Math.abs(sinHalf);
|
|
297
|
+
|
|
298
|
+
// 2D inward normals in section plane for fallback
|
|
299
|
+
const inA3 = tangent.clone().cross(vA3).negate();
|
|
300
|
+
const inB3 = tangent.clone().cross(vB3).negate();
|
|
301
|
+
const n0_2 = new THREE.Vector2(inA3.dot(u), inA3.dot(v)).normalize();
|
|
302
|
+
const n1_2 = new THREE.Vector2(inB3.dot(u), inB3.dot(v)).normalize();
|
|
303
|
+
let bis2 = new THREE.Vector2(n0_2.x + n1_2.x, n0_2.y + n1_2.y);
|
|
304
|
+
const lenBis2 = bis2.length();
|
|
305
|
+
if (lenBis2 > 1e-9) bis2.multiplyScalar(1 / lenBis2); else bis2.set(0, 0);
|
|
306
|
+
|
|
307
|
+
// Solve with anchored offset planes in 3D
|
|
308
|
+
const C_in = solveCenterFromOffsetPlanesAnchored(p, tangent, nA, qA, -1, nB, qB, -1, rEff);
|
|
309
|
+
const C_out = solveCenterFromOffsetPlanesAnchored(p, tangent, nA, qA, +1, nB, qB, +1, rEff);
|
|
310
|
+
let pick = (String(sideMode).toUpperCase() === 'OUTSET') ? 'out' : 'in';
|
|
311
|
+
let center = (pick === 'in') ? (C_in || C_out) : (C_out || C_in);
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
// Initial tangency points from center (used to refine/fallback)
|
|
316
|
+
const sA = (pick === 'in') ? -1 : +1;
|
|
317
|
+
const sB = sA;
|
|
318
|
+
let tA = center ? center.clone().addScaledVector(nA, -sA * rEff) : p.clone();
|
|
319
|
+
let tB = center ? center.clone().addScaledVector(nB, -sB * rEff) : p.clone();
|
|
320
|
+
|
|
321
|
+
// Fallback if intersection failed
|
|
322
|
+
if (!center) {
|
|
323
|
+
if (bis2.lengthSq() > eps) {
|
|
324
|
+
const dir3 = fallbackDir.set(0, 0, 0).addScaledVector(u, bis2.x).addScaledVector(v, bis2.y);
|
|
325
|
+
if (pick === 'out') dir3.negate();
|
|
326
|
+
dir3.normalize();
|
|
327
|
+
center = p.clone().addScaledVector(dir3, expectDist);
|
|
328
|
+
} else {
|
|
329
|
+
const avgN = avgNormalScratch.copy(nA).add(nB);
|
|
330
|
+
if (avgN.lengthSq() > eps) {
|
|
331
|
+
avgN.normalize();
|
|
332
|
+
const sign = (pick === 'in') ? -1 : 1;
|
|
333
|
+
center = p.clone().addScaledVector(avgN, sign * expectDist);
|
|
334
|
+
} else {
|
|
335
|
+
// give up on this sample
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Optional refinement: if initial p->center distance far from expected, recompute
|
|
342
|
+
const initialDist = center.distanceTo(p);
|
|
343
|
+
const needsRefinement = Math.abs(initialDist - expectDist) > 0.1 * rEff;
|
|
344
|
+
if (needsRefinement && allowRefine) {
|
|
345
|
+
try {
|
|
346
|
+
const qA1 = projectPointOntoFaceTriangles(trisAUse, tA, faceDataAUse);
|
|
347
|
+
const qB1 = projectPointOntoFaceTriangles(trisBUse, tB, faceDataBUse);
|
|
348
|
+
const nA1 = localFaceNormalAtPoint(solid, faceNameAUse, qA1, faceDataAUse, faceKeyAUse) || nAavgUse;
|
|
349
|
+
const nB1 = localFaceNormalAtPoint(solid, faceNameBUse, qB1, faceDataBUse, faceKeyBUse) || nBavgUse;
|
|
350
|
+
const C_ref = solveCenterFromOffsetPlanesAnchored(p, tangent, nA1, qA1, sA, nB1, qB1, sB, rEff);
|
|
351
|
+
if (C_ref) {
|
|
352
|
+
center = C_ref;
|
|
353
|
+
// Update normals used at tangency too
|
|
354
|
+
nA = nA1;
|
|
355
|
+
nB = nB1;
|
|
356
|
+
tA = center.clone().addScaledVector(nA, -sA * rEff);
|
|
357
|
+
tB = center.clone().addScaledVector(nB, -sB * rEff);
|
|
358
|
+
}
|
|
359
|
+
} catch { /* ignore */ }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Safety cap: if center is unreasonably far, snap to 2D bisector expectation
|
|
363
|
+
{
|
|
364
|
+
const pToC = center.distanceTo(p);
|
|
365
|
+
const hardCap = 6 * rEff;
|
|
366
|
+
const factor = 3.0;
|
|
367
|
+
if (!Number.isFinite(pToC) || pToC > hardCap || pToC > factor * expectDist) {
|
|
368
|
+
let dir2 = new THREE.Vector2(bis2.x, bis2.y);
|
|
369
|
+
if (String(sideMode).toUpperCase() === 'OUTSET') dir2.multiplyScalar(-1);
|
|
370
|
+
if (dir2.lengthSq() > 1e-16) {
|
|
371
|
+
dir2.normalize();
|
|
372
|
+
const dir3 = bisector3.set(0, 0, 0).addScaledVector(u, dir2.x).addScaledVector(v, dir2.y).normalize();
|
|
373
|
+
// Clamp the bisector distance so acute/near-parallel face
|
|
374
|
+
// configurations do not explode the centerline far from the edge.
|
|
375
|
+
const safeDist = Math.min(expectDist, hardCap);
|
|
376
|
+
center = p.clone().addScaledVector(dir3, safeDist);
|
|
377
|
+
// Recompute tangency points using latest normals
|
|
378
|
+
tA = center.clone().addScaledVector(nA, -sA * rEff);
|
|
379
|
+
tB = center.clone().addScaledVector(nB, -sB * rEff);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
centers.push({ x: center.x, y: center.y, z: center.z });
|
|
385
|
+
tanA.push({ x: tA.x, y: tA.y, z: tA.z });
|
|
386
|
+
tanB.push({ x: tB.x, y: tB.y, z: tB.z });
|
|
387
|
+
edgePts.push({ x: p.x, y: p.y, z: p.z });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// For closed loops, explicitly duplicate the start point at the end
|
|
391
|
+
// so the centerline is a closed polyline (last point equals first point).
|
|
392
|
+
if (isClosed && centers.length >= 2) {
|
|
393
|
+
const firstCenter = centers[0];
|
|
394
|
+
const lastCenter = centers[centers.length - 1];
|
|
395
|
+
|
|
396
|
+
const exactlyEqual = (a, b) => a.x === b.x && a.y === b.y && a.z === b.z;
|
|
397
|
+
|
|
398
|
+
if (!exactlyEqual(firstCenter, lastCenter)) {
|
|
399
|
+
// Always append an explicit duplicate of the first point
|
|
400
|
+
centers.push({ x: firstCenter.x, y: firstCenter.y, z: firstCenter.z });
|
|
401
|
+
|
|
402
|
+
// Mirror closure on tangency curves and sampled edge points to keep arrays aligned
|
|
403
|
+
if (tanA.length > 0) {
|
|
404
|
+
const a0 = tanA[0];
|
|
405
|
+
tanA.push({ x: a0.x, y: a0.y, z: a0.z });
|
|
406
|
+
}
|
|
407
|
+
if (tanB.length > 0) {
|
|
408
|
+
const b0 = tanB[0];
|
|
409
|
+
tanB.push({ x: b0.x, y: b0.y, z: b0.z });
|
|
410
|
+
}
|
|
411
|
+
if (edgePts.length > 0) {
|
|
412
|
+
const e0 = edgePts[0];
|
|
413
|
+
edgePts.push({ x: e0.x, y: e0.y, z: e0.z });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
out.points = centers;
|
|
418
|
+
out.tangentA = tanA;
|
|
419
|
+
out.tangentB = tanB;
|
|
420
|
+
out.edge = edgePts;
|
|
421
|
+
fixPolylineWinding(centers, tanA, tanB);
|
|
422
|
+
return out;
|
|
423
|
+
} catch (e) {
|
|
424
|
+
console.warn('[computeFilletCenterline] failed:', e?.message || e);
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Fix polyline winding order to ensure consistent triangle orientation.
|
|
431
|
+
* Checks all three polylines (centerline, tangentA, tangentB) for consistent winding.
|
|
432
|
+
*
|
|
433
|
+
* @param {Array} centerline - Array of center points {x, y, z}
|
|
434
|
+
* @param {Array} tangentA - Array of tangent A points {x, y, z}
|
|
435
|
+
* @param {Array} tangentB - Array of tangent B points {x, y, z}
|
|
436
|
+
* @returns {Object} - {centerlineReversed: boolean, tangentAReversed: boolean, tangentBReversed: boolean}
|
|
437
|
+
*/
|
|
438
|
+
// Decide which polylines to reverse so that point i across
|
|
439
|
+
// centerline/tangentA/tangentB correspond to a consistent cross‑section.
|
|
440
|
+
// Uses an objective based on how close the tangent points are to the fillet
|
|
441
|
+
// radius from the centerline at sampled indices (quarter/half/three‑quarter).
|
|
442
|
+
// Falls back to direction/cross heuristics when radius is unavailable.
|
|
443
|
+
function fixPolylineWinding(centerline, tangentA, tangentB, expectedRadius = null) {
|
|
444
|
+
try {
|
|
445
|
+
// Fast-path: if any array is too small or lengths differ, do nothing
|
|
446
|
+
if (!Array.isArray(centerline) || !Array.isArray(tangentA) || !Array.isArray(tangentB)) {
|
|
447
|
+
return { centerlineReversed: false, tangentAReversed: false, tangentBReversed: false };
|
|
448
|
+
}
|
|
449
|
+
const isValidPoint = (p) => !!p && isFinite(p.x) && isFinite(p.y) && isFinite(p.z);
|
|
450
|
+
const n = Math.min(centerline.length, tangentA.length, tangentB.length);
|
|
451
|
+
if (n < 3) {
|
|
452
|
+
return { centerlineReversed: false, tangentAReversed: false, tangentBReversed: false };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// If we have a target radius, use it to search over combinations of
|
|
456
|
+
// {reverse centerline, reverse A, reverse B} that best satisfy
|
|
457
|
+
// dist(center[i], tangentX[i]) ≈ radius at a few sample locations.
|
|
458
|
+
if (Number.isFinite(expectedRadius) && expectedRadius > 0) {
|
|
459
|
+
const dist = (p, q) => {
|
|
460
|
+
const dx = (q.x - p.x), dy = (q.y - p.y), dz = (q.z - p.z);
|
|
461
|
+
return Math.hypot(dx, dy, dz);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// Choose robust sample indices near 1/4, 1/2, 3/4 along the polyline
|
|
465
|
+
const idxs = [];
|
|
466
|
+
const idxFromT = (t) => Math.max(0, Math.min(n - 1, Math.round(t * (n - 1))));
|
|
467
|
+
const pushUnique = (i) => { if (!idxs.includes(i)) idxs.push(i); };
|
|
468
|
+
pushUnique(idxFromT(0.25));
|
|
469
|
+
pushUnique(idxFromT(0.5));
|
|
470
|
+
pushUnique(idxFromT(0.75));
|
|
471
|
+
|
|
472
|
+
const combos = [
|
|
473
|
+
[false, false, false],
|
|
474
|
+
[false, true, false],
|
|
475
|
+
[false, false, true],
|
|
476
|
+
[true, false, false],
|
|
477
|
+
[true, true, false],
|
|
478
|
+
[true, false, true],
|
|
479
|
+
[false, true, true],
|
|
480
|
+
[true, true, true]
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
let best = { cost: Infinity, rc: false, ra: false, rb: false };
|
|
484
|
+
for (const [rc, ra, rb] of combos) {
|
|
485
|
+
let cost = 0;
|
|
486
|
+
for (const i of idxs) {
|
|
487
|
+
const ci = centerline[rc ? (n - 1 - i) : i];
|
|
488
|
+
const ai = tangentA[ra ? (n - 1 - i) : i];
|
|
489
|
+
const bi = tangentB[rb ? (n - 1 - i) : i];
|
|
490
|
+
const dA = dist(ci, ai);
|
|
491
|
+
const dB = dist(ci, bi);
|
|
492
|
+
// Sum absolute deviations from expected radius
|
|
493
|
+
cost += Math.abs(dA - expectedRadius) + Math.abs(dB - expectedRadius);
|
|
494
|
+
}
|
|
495
|
+
if (cost < best.cost) best = { cost, rc, ra, rb };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (best.cost < Infinity) {
|
|
499
|
+
return {
|
|
500
|
+
centerlineReversed: best.rc,
|
|
501
|
+
tangentAReversed: best.ra,
|
|
502
|
+
tangentBReversed: best.rb
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Primary heuristic: align the progression direction of tangents to the centerline.
|
|
508
|
+
// We compare average segment directions (normalized sum) and flip if the dot is negative.
|
|
509
|
+
const avgDir = (pts) => {
|
|
510
|
+
let sx = 0, sy = 0, sz = 0;
|
|
511
|
+
for (let i = 0; i < n - 1; i++) {
|
|
512
|
+
const a = pts[i], b = pts[i + 1];
|
|
513
|
+
sx += (b.x - a.x); sy += (b.y - a.y); sz += (b.z - a.z);
|
|
514
|
+
}
|
|
515
|
+
const len = Math.hypot(sx, sy, sz) || 1;
|
|
516
|
+
return { x: sx / len, y: sy / len, z: sz / len };
|
|
517
|
+
};
|
|
518
|
+
const cDir = avgDir(centerline);
|
|
519
|
+
const aDir = avgDir(tangentA);
|
|
520
|
+
const bDir = avgDir(tangentB);
|
|
521
|
+
|
|
522
|
+
const dot = (u, v) => (u.x * v.x + u.y * v.y + u.z * v.z);
|
|
523
|
+
let centerlineReversed = false;
|
|
524
|
+
let tangentAReversed = false;
|
|
525
|
+
let tangentBReversed = false;
|
|
526
|
+
|
|
527
|
+
// If a tangent flows opposite the centerline, flip it.
|
|
528
|
+
if (dot(cDir, aDir) < 0) tangentAReversed = true;
|
|
529
|
+
if (dot(cDir, bDir) < 0) tangentBReversed = true;
|
|
530
|
+
|
|
531
|
+
// If both tangents are flipped by the above, it may be easier to flip the centerline
|
|
532
|
+
// instead to keep A/B in their original indexing. Choose the minimal total reversals.
|
|
533
|
+
if (tangentAReversed && tangentBReversed) {
|
|
534
|
+
centerlineReversed = true;
|
|
535
|
+
tangentAReversed = false;
|
|
536
|
+
tangentBReversed = false;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Secondary heuristic (legacy): examine relative cross-product signs to detect
|
|
540
|
+
// inconsistent relationships. This complements the direction-alignment above
|
|
541
|
+
// and only proposes additional flips if still inconsistent.
|
|
542
|
+
// Sample several points along the polylines to determine consistent orientation
|
|
543
|
+
const sampleCount = Math.min(8, Math.floor(centerline.length / 3));
|
|
544
|
+
const sampleIndices = [];
|
|
545
|
+
for (let i = 1; i < sampleCount - 1; i++) {
|
|
546
|
+
const idx = Math.floor(i * (centerline.length - 2) / (sampleCount - 1));
|
|
547
|
+
if (idx + 1 < centerline.length) {
|
|
548
|
+
sampleIndices.push(idx);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
let centerlineToTangentA_CrossProducts = [];
|
|
553
|
+
let centerlineToTangentB_CrossProducts = [];
|
|
554
|
+
let tangentAToTangentB_CrossProducts = [];
|
|
555
|
+
|
|
556
|
+
// Analyze the relationship between each pair of polylines
|
|
557
|
+
for (const idx of sampleIndices) {
|
|
558
|
+
if (idx + 1 >= centerline.length) continue;
|
|
559
|
+
|
|
560
|
+
const c1 = centerline[idx];
|
|
561
|
+
const c2 = centerline[idx + 1];
|
|
562
|
+
const tA1 = tangentA[idx];
|
|
563
|
+
const tA2 = tangentA[idx + 1];
|
|
564
|
+
const tB1 = tangentB[idx];
|
|
565
|
+
const tB2 = tangentB[idx + 1];
|
|
566
|
+
|
|
567
|
+
// Validate all points are finite
|
|
568
|
+
if (!isValidPoint(c1) || !isValidPoint(c2) ||
|
|
569
|
+
!isValidPoint(tA1) || !isValidPoint(tA2) ||
|
|
570
|
+
!isValidPoint(tB1) || !isValidPoint(tB2)) {
|
|
571
|
+
continue; // Skip this sample if any point is invalid
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Vector along centerline
|
|
575
|
+
const centerVec = { x: c2.x - c1.x, y: c2.y - c1.y, z: c2.z - c1.z };
|
|
576
|
+
|
|
577
|
+
// Vector along tangent A
|
|
578
|
+
const tangentAVec = { x: tA2.x - tA1.x, y: tA2.y - tA1.y, z: tA2.z - tA1.z };
|
|
579
|
+
|
|
580
|
+
// Vector from centerline to tangent A
|
|
581
|
+
const centerToTangentA = { x: tA1.x - c1.x, y: tA1.y - c1.y, z: tA1.z - c1.z };
|
|
582
|
+
|
|
583
|
+
// Vector from centerline to tangent B
|
|
584
|
+
const centerToTangentB = { x: tB1.x - c1.x, y: tB1.y - c1.y, z: tB1.z - c1.z };
|
|
585
|
+
|
|
586
|
+
// Vector from tangent A to tangent B
|
|
587
|
+
const tangentAToTangentB = { x: tB1.x - tA1.x, y: tB1.y - tA1.y, z: tB1.z - tA1.z };
|
|
588
|
+
|
|
589
|
+
// Calculate cross products to determine relative orientations
|
|
590
|
+
// We'll use the dot product of cross products with a consistent reference vector
|
|
591
|
+
|
|
592
|
+
// Cross product: centerline direction × (center to tangentA)
|
|
593
|
+
const cross1 = {
|
|
594
|
+
x: centerVec.y * centerToTangentA.z - centerVec.z * centerToTangentA.y,
|
|
595
|
+
y: centerVec.z * centerToTangentA.x - centerVec.x * centerToTangentA.z,
|
|
596
|
+
z: centerVec.x * centerToTangentA.y - centerVec.y * centerToTangentA.x
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// Cross product: centerline direction × (center to tangentB)
|
|
600
|
+
const cross2 = {
|
|
601
|
+
x: centerVec.y * centerToTangentB.z - centerVec.z * centerToTangentB.y,
|
|
602
|
+
y: centerVec.z * centerToTangentB.x - centerVec.x * centerToTangentB.z,
|
|
603
|
+
z: centerVec.x * centerToTangentB.y - centerVec.y * centerToTangentB.x
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// Cross product: tangentA direction × (tangentA to tangentB)
|
|
607
|
+
const cross3 = {
|
|
608
|
+
x: tangentAVec.y * tangentAToTangentB.z - tangentAVec.z * tangentAToTangentB.y,
|
|
609
|
+
y: tangentAVec.z * tangentAToTangentB.x - tangentAVec.x * tangentAToTangentB.z,
|
|
610
|
+
z: tangentAVec.x * tangentAToTangentB.y - tangentAVec.y * tangentAToTangentB.x
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// Use the magnitude of the Z component as a simple 2D projection heuristic
|
|
614
|
+
centerlineToTangentA_CrossProducts.push(cross1.z);
|
|
615
|
+
centerlineToTangentB_CrossProducts.push(cross2.z);
|
|
616
|
+
tangentAToTangentB_CrossProducts.push(cross3.z);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Analyze the consistency of cross products
|
|
620
|
+
const validCenterToA = centerlineToTangentA_CrossProducts.filter(x => isFinite(x));
|
|
621
|
+
const validCenterToB = centerlineToTangentB_CrossProducts.filter(x => isFinite(x));
|
|
622
|
+
const validAToB = tangentAToTangentB_CrossProducts.filter(x => isFinite(x));
|
|
623
|
+
|
|
624
|
+
const avgCenterToA = validCenterToA.length > 0 ?
|
|
625
|
+
validCenterToA.reduce((a, b) => a + Math.sign(b), 0) / validCenterToA.length : 0;
|
|
626
|
+
const avgCenterToB = validCenterToB.length > 0 ?
|
|
627
|
+
validCenterToB.reduce((a, b) => a + Math.sign(b), 0) / validCenterToB.length : 0;
|
|
628
|
+
const avgAToB = validAToB.length > 0 ?
|
|
629
|
+
validAToB.reduce((a, b) => a + Math.sign(b), 0) / validAToB.length : 0;
|
|
630
|
+
|
|
631
|
+
// For a proper fillet, we expect:
|
|
632
|
+
// 1. Centerline and tangents should have consistent progression direction
|
|
633
|
+
// 2. Tangent A and B should generally go in opposite directions relative to each other
|
|
634
|
+
// 3. All three should form a consistent right-handed coordinate system
|
|
635
|
+
|
|
636
|
+
const centerRelationshipInconsistent = (avgCenterToA > 0) !== (avgCenterToB > 0);
|
|
637
|
+
const tangentsGoSameDirection = avgAToB > 0.5; // Strong positive correlation means same direction
|
|
638
|
+
if (centerRelationshipInconsistent && !(centerlineReversed || tangentAReversed || tangentBReversed)) {
|
|
639
|
+
// If centerline relationships are inconsistent AND tangents go in same direction,
|
|
640
|
+
// this suggests the centerline itself might need reversal
|
|
641
|
+
if (tangentsGoSameDirection) {
|
|
642
|
+
centerlineReversed = true;
|
|
643
|
+
} else {
|
|
644
|
+
// Heuristic: reverse the tangent with stronger inconsistency
|
|
645
|
+
if (Math.abs(avgCenterToB) > Math.abs(avgCenterToA)) {
|
|
646
|
+
tangentBReversed = true;
|
|
647
|
+
} else {
|
|
648
|
+
tangentAReversed = true;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
} else if (tangentsGoSameDirection && !(centerlineReversed || tangentAReversed || tangentBReversed)) {
|
|
652
|
+
// Even if center relationships are consistent, if tangents go in same direction,
|
|
653
|
+
// we likely need to reverse one tangent
|
|
654
|
+
tangentBReversed = true;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
centerlineReversed,
|
|
659
|
+
tangentAReversed,
|
|
660
|
+
tangentBReversed
|
|
661
|
+
};
|
|
662
|
+
} catch (error) {
|
|
663
|
+
console.warn('Winding order analysis failed:', error?.message || error);
|
|
664
|
+
return { centerlineReversed: false, tangentAReversed: false, tangentBReversed: false };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Convenience: compute and attach the fillet centerline as an auxiliary edge on a Solid.
|
|
670
|
+
*
|
|
671
|
+
* @param {any} solid Target solid to receive the aux edge (overlay)
|
|
672
|
+
* @param {any} edgeObj Edge to analyze (must belong to `solid`)
|
|
673
|
+
* @param {number} radius Fillet radius (>0)
|
|
674
|
+
* @param {'INSET'|'OUTSET'} sideMode Side preference
|
|
675
|
+
* @param {string} name Edge name (default 'FILLET_CENTERLINE')
|
|
676
|
+
* @param {object} [options] Additional aux edge options
|
|
677
|
+
* @param {boolean} [options.closedLoop=false] Render as closed loop when visualized
|
|
678
|
+
* @param {boolean} [options.polylineWorld=false] Whether points are already in world space
|
|
679
|
+
* @param {'OVERLAY'|'BASE'|string} [options.materialKey='OVERLAY'] Visualization material tag
|
|
680
|
+
* @returns {{ points: {x:number,y:number,z:number}[], closedLoop: boolean } | null}
|
|
681
|
+
*/
|
|
682
|
+
export function attachFilletCenterlineAuxEdge(solid, edgeObj, radius = 1, sideMode = 'INSET', name = 'FILLET_CENTERLINE', options = {}) {
|
|
683
|
+
try {
|
|
684
|
+
if (!solid || !edgeObj) return null;
|
|
685
|
+
const res = computeFilletCenterline(edgeObj, radius, sideMode);
|
|
686
|
+
if (res && Array.isArray(res.points) && res.points.length >= 2) {
|
|
687
|
+
const opts = { materialKey: 'OVERLAY', closedLoop: !!res.closedLoop, ...(options || {}) };
|
|
688
|
+
solid.addAuxEdge(name, res.points, opts);
|
|
689
|
+
return res;
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
} catch (e) {
|
|
693
|
+
console.warn('[attachFilletCenterlineAuxEdge] failed:', e?.message || e);
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
// Functional API: builds fillet tube and wedge and returns them.
|
|
700
|
+
export function filletSolid({ edgeToFillet, radius = 1, sideMode = 'INSET', debug = false, name = 'fillet', inflate = 0.1, resolution = 32, showTangentOverlays = false } = {}) {
|
|
701
|
+
try {
|
|
702
|
+
// Validate inputs
|
|
703
|
+
if (!edgeToFillet) {
|
|
704
|
+
throw new Error('filletSolid: edgeToFillet is required');
|
|
705
|
+
}
|
|
706
|
+
if (!Number.isFinite(radius) || radius <= 0) {
|
|
707
|
+
throw new Error(`filletSolid: radius must be a positive number, got ${radius}`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const side = String(sideMode).toUpperCase();
|
|
711
|
+
const tubeResolution = (Number.isFinite(Number(resolution)) && Number(resolution) > 0)
|
|
712
|
+
? Math.max(8, Math.floor(Number(resolution)))
|
|
713
|
+
: 32;
|
|
714
|
+
const logDebug = (...args) => { if (debug) console.log(...args); };
|
|
715
|
+
logDebug(`🔧 Starting fillet operation: edge=${edgeToFillet?.name || 'unnamed'}, radius=${radius}, side=${side}`);
|
|
716
|
+
|
|
717
|
+
const res = computeFilletCenterline(edgeToFillet, radius, side);
|
|
718
|
+
logDebug('The fillet centerline result is:', res);
|
|
719
|
+
|
|
720
|
+
if (!res) {
|
|
721
|
+
throw new Error('computeFilletCenterline returned null/undefined');
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const centerline = Array.isArray(res?.points) ? res.points : [];
|
|
725
|
+
let tangentA = Array.isArray(res?.tangentA) ? res.tangentA : [];
|
|
726
|
+
let tangentB = Array.isArray(res?.tangentB) ? res.tangentB : [];
|
|
727
|
+
let edgePts = Array.isArray(res?.edge) ? res.edge : [];
|
|
728
|
+
const closedLoop = !!res?.closedLoop;
|
|
729
|
+
|
|
730
|
+
if (debug) {
|
|
731
|
+
try { logDebug('filletSolid: centerline/tangent edges computed'); } catch { }
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Clone into plain objects
|
|
735
|
+
const centerlineCopy = centerline.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
|
|
736
|
+
let tangentACopy = tangentA.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
|
|
737
|
+
let tangentBCopy = tangentB.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
|
|
738
|
+
const tangentASnap = tangentACopy.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
|
|
739
|
+
const tangentBSnap = tangentBCopy.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
|
|
740
|
+
let edgeCopy = edgePts.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
|
|
741
|
+
// Working copy of the original edge points used for wedge construction.
|
|
742
|
+
// Kept separate from `edgeCopy` so we can apply small insets/offsets without
|
|
743
|
+
// disturbing other consumers that rely on the original edge sampling.
|
|
744
|
+
let edgeWedgeCopy = edgeCopy.map(pt => ({ x: pt.x, y: pt.y, z: pt.z }));
|
|
745
|
+
|
|
746
|
+
// Visualize original centerline in yellow before any manipulation
|
|
747
|
+
if (debug && centerlineCopy.length >= 2) {
|
|
748
|
+
console.log('🟡 ORIGINAL CENTERLINE (Yellow):');
|
|
749
|
+
const originalVisualization = new Solid();
|
|
750
|
+
originalVisualization.name = `${name}_ORIGINAL_CENTERLINE`;
|
|
751
|
+
|
|
752
|
+
// Add centerline as line segments
|
|
753
|
+
for (let i = 0; i < centerlineCopy.length - 1; i++) {
|
|
754
|
+
const p1 = centerlineCopy[i];
|
|
755
|
+
const p2 = centerlineCopy[i + 1];
|
|
756
|
+
console.log(` Segment ${i}: (${p1.x.toFixed(3)}, ${p1.y.toFixed(3)}, ${p1.z.toFixed(3)}) → (${p2.x.toFixed(3)}, ${p2.y.toFixed(3)}, ${p2.z.toFixed(3)})`);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Convert to array format for addAuxEdge
|
|
760
|
+
const originalCenterlineArray = centerlineCopy.map(pt => [pt.x, pt.y, pt.z]);
|
|
761
|
+
originalVisualization.addAuxEdge('ORIGINAL_CENTERLINE', originalCenterlineArray, {
|
|
762
|
+
materialKey: 'YELLOW',
|
|
763
|
+
closedLoop: closedLoop,
|
|
764
|
+
lineWidth: 3.0
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
try {
|
|
768
|
+
originalVisualization.visualize();
|
|
769
|
+
console.log('🟡 Original centerline visualization created (Yellow)');
|
|
770
|
+
} catch (vizError) {
|
|
771
|
+
console.warn('Failed to visualize original centerline:', vizError?.message || vizError);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
logDebug('Checking all polyline winding orders...');
|
|
776
|
+
if (centerlineCopy.length >= 2) {
|
|
777
|
+
const c1 = centerlineCopy[0];
|
|
778
|
+
const c2 = centerlineCopy[1];
|
|
779
|
+
const cLast = centerlineCopy[centerlineCopy.length - 1];
|
|
780
|
+
logDebug(`Centerline: start=(${c1.x.toFixed(3)}, ${c1.y.toFixed(3)}, ${c1.z.toFixed(3)}) → (${c2.x.toFixed(3)}, ${c2.y.toFixed(3)}, ${c2.z.toFixed(3)}) ... end=(${cLast.x.toFixed(3)}, ${cLast.y.toFixed(3)}, ${cLast.z.toFixed(3)})`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Apply a small offset to the tangent curves relative to the centerline.
|
|
784
|
+
// Keep OUTSET behavior unchanged: move tangents slightly toward the centerline;
|
|
785
|
+
// INSET moves them outward. Closed loops skip inflation to avoid self‑intersection.
|
|
786
|
+
{
|
|
787
|
+
// Respect the sign of `inflate` so callers can shrink the tool for
|
|
788
|
+
// OUTSET (negative) while expanding for INSET (positive).
|
|
789
|
+
const offsetDistance = Number.isFinite(inflate) ? Number(inflate) : 0;
|
|
790
|
+
const n = Math.min(centerlineCopy.length, tangentACopy.length, tangentBCopy.length);
|
|
791
|
+
for (let i = 0; i < n; i++) {
|
|
792
|
+
const c = centerlineCopy[i];
|
|
793
|
+
const ta = tangentACopy[i];
|
|
794
|
+
const tb = tangentBCopy[i];
|
|
795
|
+
if (c && ta) {
|
|
796
|
+
const dax = ta.x - c.x, day = ta.y - c.y, daz = ta.z - c.z;
|
|
797
|
+
const daL = Math.hypot(dax, day, daz);
|
|
798
|
+
if (daL > 1e-12) {
|
|
799
|
+
ta.x += (dax / daL) * offsetDistance;
|
|
800
|
+
ta.y += (day / daL) * offsetDistance;
|
|
801
|
+
ta.z += (daz / daL) * offsetDistance;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (c && tb) {
|
|
805
|
+
const dbx = tb.x - c.x, dby = tb.y - c.y, dbz = tb.z - c.z;
|
|
806
|
+
const dbL = Math.hypot(dbx, dby, dbz);
|
|
807
|
+
if (dbL > 1e-12) {
|
|
808
|
+
tb.x += (dbx / dbL) * offsetDistance;
|
|
809
|
+
tb.y += (dby / dbL) * offsetDistance;
|
|
810
|
+
tb.z += (dbz / dbL) * offsetDistance;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
try { if (offsetDistance) logDebug(`Applied tangent offsetDistance=${offsetDistance} to ${n} samples`); } catch { }
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Push wedge edge points slightly relative to the centerline to ensure
|
|
818
|
+
// the wedge doesn't extend beyond the original geometry. For OUTSET this
|
|
819
|
+
// nudge is inward (toward the centerline). For INSET it must be the
|
|
820
|
+
// opposite direction (away from the centerline) to build the correct wedge.
|
|
821
|
+
// Slightly offset edge points to guarantee robust boolean overlap.
|
|
822
|
+
// Use a small radius-scaled inward nudge for OUTSET, capped to avoid
|
|
823
|
+
// large displacements on big models.
|
|
824
|
+
const outsetInsetMagnitude = Math.max(1e-4, Math.min(0.05, Math.abs(radius) * 0.05));
|
|
825
|
+
const wedgeInsetMagnitude = closedLoop ? 0 : ((side === 'INSET') ? Math.abs(inflate) : outsetInsetMagnitude);
|
|
826
|
+
for (let i = 0; i < edgeWedgeCopy.length; i++) {
|
|
827
|
+
const edgeWedgePt = edgeWedgeCopy[i];
|
|
828
|
+
const centerPt = centerlineCopy[i] || centerlineCopy[centerlineCopy.length - 1]; // Fallback to last point
|
|
829
|
+
|
|
830
|
+
if (edgeWedgePt && centerPt) {
|
|
831
|
+
try {
|
|
832
|
+
const origWedgeEdge = { ...edgeWedgePt };
|
|
833
|
+
|
|
834
|
+
// Calculate direction from edge point toward the centerline (inward direction)
|
|
835
|
+
const inwardDir = {
|
|
836
|
+
x: centerPt.x - edgeWedgePt.x,
|
|
837
|
+
y: centerPt.y - edgeWedgePt.y,
|
|
838
|
+
z: centerPt.z - edgeWedgePt.z
|
|
839
|
+
};
|
|
840
|
+
const inwardLength = Math.sqrt(inwardDir.x * inwardDir.x + inwardDir.y * inwardDir.y + inwardDir.z * inwardDir.z);
|
|
841
|
+
|
|
842
|
+
if (inwardLength > 1e-12) {
|
|
843
|
+
// Normalize and apply inset
|
|
844
|
+
const normalizedInward = {
|
|
845
|
+
x: inwardDir.x / inwardLength,
|
|
846
|
+
y: inwardDir.y / inwardLength,
|
|
847
|
+
z: inwardDir.z / inwardLength
|
|
848
|
+
};
|
|
849
|
+
// Determine direction: OUTSET -> inward, INSET -> outward (opposite)
|
|
850
|
+
const dirSign = (side === 'INSET') ? -1 : 1;
|
|
851
|
+
const step = dirSign * wedgeInsetMagnitude;
|
|
852
|
+
// Apply
|
|
853
|
+
edgeWedgePt.x += normalizedInward.x * step;
|
|
854
|
+
edgeWedgePt.y += normalizedInward.y * step;
|
|
855
|
+
edgeWedgePt.z += normalizedInward.z * step;
|
|
856
|
+
|
|
857
|
+
// Validate the result
|
|
858
|
+
if (!isFiniteVec3(edgeWedgePt)) {
|
|
859
|
+
console.warn(`Invalid wedge edge point after inset at index ${i}, reverting to original`);
|
|
860
|
+
Object.assign(edgeWedgePt, origWedgeEdge);
|
|
861
|
+
}
|
|
862
|
+
} else {
|
|
863
|
+
console.warn(`Edge point ${i} is too close to centerline, skipping wedge inset`);
|
|
864
|
+
}
|
|
865
|
+
} catch (insetError) {
|
|
866
|
+
console.warn(`Wedge edge inset failed at index ${i}: ${insetError?.message || insetError}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (wedgeInsetMagnitude) logDebug(`Applied wedge inset of ${wedgeInsetMagnitude} units (${side === 'INSET' ? 'outward' : 'inward'}) to ${edgeWedgeCopy.length} edge points`);
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
// Do not reorder edge points. Centerline/tangent/edge points are produced in
|
|
875
|
+
// lockstep elsewhere; reindexing the edge points breaks correspondence and
|
|
876
|
+
// can create long crossing triangles. If orientation issues arise, reverse
|
|
877
|
+
// the entire polylines together rather than reordering indices.
|
|
878
|
+
|
|
879
|
+
// Visualize manipulated centerline after all processing
|
|
880
|
+
if (debug && centerlineCopy.length >= 2) {
|
|
881
|
+
console.log('🔵 MANIPULATED CENTERLINE (Blue):');
|
|
882
|
+
const manipulatedVisualization = new Solid();
|
|
883
|
+
manipulatedVisualization.name = `${name}_MANIPULATED_CENTERLINE`;
|
|
884
|
+
|
|
885
|
+
// Add manipulated centerline as line segments
|
|
886
|
+
for (let i = 0; i < centerlineCopy.length - 1; i++) {
|
|
887
|
+
const p1 = centerlineCopy[i];
|
|
888
|
+
const p2 = centerlineCopy[i + 1];
|
|
889
|
+
console.log(` Segment ${i}: (${p1.x.toFixed(3)}, ${p1.y.toFixed(3)}, ${p1.z.toFixed(3)}) → (${p2.x.toFixed(3)}, ${p2.y.toFixed(3)}, ${p2.z.toFixed(3)})`);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Convert to array format for addAuxEdge
|
|
893
|
+
const manipulatedCenterlineArray = centerlineCopy.map(pt => [pt.x, pt.y, pt.z]);
|
|
894
|
+
manipulatedVisualization.addAuxEdge('MANIPULATED_CENTERLINE', manipulatedCenterlineArray, {
|
|
895
|
+
materialKey: 'BLUE',
|
|
896
|
+
closedLoop: closedLoop,
|
|
897
|
+
lineWidth: 3.0
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
try {
|
|
901
|
+
manipulatedVisualization.visualize();
|
|
902
|
+
console.log('🔵 Manipulated centerline visualization created (Blue)');
|
|
903
|
+
} catch (vizError) {
|
|
904
|
+
console.warn('Failed to visualize manipulated centerline:', vizError?.message || vizError);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
logDebug('centerlines all generated fine');
|
|
909
|
+
|
|
910
|
+
// Validate spacing/variation for the path we will actually use for the tube
|
|
911
|
+
const tubePathOriginal = Array.isArray(centerline) ? centerline : [];
|
|
912
|
+
if (tubePathOriginal.length < 2) {
|
|
913
|
+
console.error('Insufficient centerline points for tube generation');
|
|
914
|
+
// Return debug information even on centerline failure
|
|
915
|
+
return {
|
|
916
|
+
tube: null,
|
|
917
|
+
wedge: null,
|
|
918
|
+
finalSolid: null,
|
|
919
|
+
centerline: centerlineCopy || [],
|
|
920
|
+
tangentA: tangentACopy || [],
|
|
921
|
+
tangentB: tangentBCopy || [],
|
|
922
|
+
tangentASeam: tangentASnap || [],
|
|
923
|
+
tangentBSeam: tangentBSnap || [],
|
|
924
|
+
error: 'Insufficient centerline points for tube generation'
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
{
|
|
928
|
+
const firstPt = tubePathOriginal[0];
|
|
929
|
+
const hasVariation = tubePathOriginal.some(pt =>
|
|
930
|
+
Math.abs(pt.x - firstPt.x) > 1e-6 ||
|
|
931
|
+
Math.abs(pt.y - firstPt.y) > 1e-6 ||
|
|
932
|
+
Math.abs(pt.z - firstPt.z) > 1e-6
|
|
933
|
+
);
|
|
934
|
+
if (!hasVariation) {
|
|
935
|
+
console.error('Degenerate centerline: all points are identical');
|
|
936
|
+
// Return debug information even on centerline failure
|
|
937
|
+
return {
|
|
938
|
+
tube: null,
|
|
939
|
+
wedge: null,
|
|
940
|
+
finalSolid: null,
|
|
941
|
+
centerline: centerlineCopy || [],
|
|
942
|
+
tangentA: tangentACopy || [],
|
|
943
|
+
tangentB: tangentBCopy || [],
|
|
944
|
+
tangentASeam: tangentASnap || [],
|
|
945
|
+
tangentBSeam: tangentBSnap || [],
|
|
946
|
+
error: 'Degenerate centerline: all points are identical'
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const minSpacing = radius * 0.01;
|
|
950
|
+
for (let i = 1; i < tubePathOriginal.length; i++) {
|
|
951
|
+
const curr = tubePathOriginal[i];
|
|
952
|
+
const prev = tubePathOriginal[i - 1];
|
|
953
|
+
const distance = Math.hypot(curr.x - prev.x, curr.y - prev.y, curr.z - prev.z);
|
|
954
|
+
if (distance < minSpacing) {
|
|
955
|
+
console.warn(`Centerline points ${i - 1} and ${i} are too close (distance: ${distance}), this may cause tube generation issues`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Build tube from the ORIGINAL centerline (not the modified copy)
|
|
961
|
+
let filletTube = null;
|
|
962
|
+
try {
|
|
963
|
+
// Tube expects [x,y,z] arrays; convert original {x,y,z} objects
|
|
964
|
+
let tubePoints = tubePathOriginal.map(p => [p.x, p.y, p.z]);
|
|
965
|
+
|
|
966
|
+
if (closedLoop) {
|
|
967
|
+
logDebug('Closed loop detected: preparing tube centerline...');
|
|
968
|
+
// For closed loops: ensure the tube polyline has the same point at start and end
|
|
969
|
+
if (tubePoints.length >= 2) {
|
|
970
|
+
const firstPt = tubePoints[0];
|
|
971
|
+
const lastPt = tubePoints[tubePoints.length - 1];
|
|
972
|
+
|
|
973
|
+
// Check if first and last points are different
|
|
974
|
+
const dx = firstPt[0] - lastPt[0];
|
|
975
|
+
const dy = firstPt[1] - lastPt[1];
|
|
976
|
+
const dz = firstPt[2] - lastPt[2];
|
|
977
|
+
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
978
|
+
|
|
979
|
+
// Add the first point at the end to close the loop
|
|
980
|
+
tubePoints.push([firstPt[0], firstPt[1], firstPt[2]]);
|
|
981
|
+
logDebug('Closed loop: Added first point at end for tube generation');
|
|
982
|
+
|
|
983
|
+
}
|
|
984
|
+
} else {
|
|
985
|
+
logDebug('Non-closed loop detected: preparing tube centerline...');
|
|
986
|
+
// For non-closed loops: extend the start and end segments of the centerline polyline for tube only
|
|
987
|
+
if (tubePoints.length >= 2) {
|
|
988
|
+
logDebug('Non-closed loop: Extending tube centerline segments...');
|
|
989
|
+
const extensionDistance = 0.1;
|
|
990
|
+
|
|
991
|
+
// Extend first segment backwards
|
|
992
|
+
const p0 = tubePoints[0];
|
|
993
|
+
const p1 = tubePoints[1];
|
|
994
|
+
const dir0 = [p0[0] - p1[0], p0[1] - p1[1], p0[2] - p1[2]];
|
|
995
|
+
const len0 = Math.sqrt(dir0[0] * dir0[0] + dir0[1] * dir0[1] + dir0[2] * dir0[2]);
|
|
996
|
+
|
|
997
|
+
if (len0 > 1e-12) {
|
|
998
|
+
const norm0 = [dir0[0] / len0, dir0[1] / len0, dir0[2] / len0];
|
|
999
|
+
const extendedStart = [
|
|
1000
|
+
p0[0] + norm0[0] * extensionDistance,
|
|
1001
|
+
p0[1] + norm0[1] * extensionDistance,
|
|
1002
|
+
p0[2] + norm0[2] * extensionDistance
|
|
1003
|
+
];
|
|
1004
|
+
tubePoints[0] = extendedStart;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Extend last segment forwards
|
|
1008
|
+
const lastIdx = tubePoints.length - 1;
|
|
1009
|
+
const pLast = tubePoints[lastIdx];
|
|
1010
|
+
const pPrev = tubePoints[lastIdx - 1];
|
|
1011
|
+
const dirLast = [pLast[0] - pPrev[0], pLast[1] - pPrev[1], pLast[2] - pPrev[2]];
|
|
1012
|
+
const lenLast = Math.sqrt(dirLast[0] * dirLast[0] + dirLast[1] * dirLast[1] + dirLast[2] * dirLast[2]);
|
|
1013
|
+
|
|
1014
|
+
if (lenLast > 1e-12) {
|
|
1015
|
+
const normLast = [dirLast[0] / lenLast, dirLast[1] / lenLast, dirLast[2] / lenLast];
|
|
1016
|
+
const extendedEnd = [
|
|
1017
|
+
pLast[0] + normLast[0] * extensionDistance,
|
|
1018
|
+
pLast[1] + normLast[1] * extensionDistance,
|
|
1019
|
+
pLast[2] + normLast[2] * extensionDistance
|
|
1020
|
+
];
|
|
1021
|
+
tubePoints[lastIdx] = extendedEnd;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
logDebug(`Extended tube centerline by ${extensionDistance} units at both ends`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const inflatedTubeRadius = radius ;
|
|
1029
|
+
filletTube = new Tube({
|
|
1030
|
+
points: tubePoints,
|
|
1031
|
+
radius: inflatedTubeRadius,
|
|
1032
|
+
innerRadius: 0,
|
|
1033
|
+
resolution: tubeResolution,
|
|
1034
|
+
name: `${name}_TUBE`,
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
// Store PMI metadata on the outer pipe face so downstream annotations
|
|
1038
|
+
// can recover the user radius instead of the inflated geometry value.
|
|
1039
|
+
try {
|
|
1040
|
+
const faceTag = `${name}_TUBE_Outer`;
|
|
1041
|
+
const overrideMeta = {
|
|
1042
|
+
type: 'pipe',
|
|
1043
|
+
source: 'FilletFeature',
|
|
1044
|
+
featureID: name,
|
|
1045
|
+
inflatedRadius: inflatedTubeRadius,
|
|
1046
|
+
pmiRadiusOverride: radius,
|
|
1047
|
+
radiusOverride: radius,
|
|
1048
|
+
};
|
|
1049
|
+
if (edgeToFillet?.name) overrideMeta.edgeReference = edgeToFillet.name;
|
|
1050
|
+
filletTube.setFaceMetadata(faceTag, overrideMeta);
|
|
1051
|
+
|
|
1052
|
+
if (showTangentOverlays) {
|
|
1053
|
+
const auxOpts = { materialKey: 'OVERLAY', closedLoop: !!closedLoop };
|
|
1054
|
+
if (Array.isArray(tangentASnap) && tangentASnap.length >= 2) {
|
|
1055
|
+
filletTube.addAuxEdge(`${name}_TANGENT_A_PATH`, tangentASnap, auxOpts);
|
|
1056
|
+
}
|
|
1057
|
+
if (Array.isArray(tangentBSnap) && tangentBSnap.length >= 2) {
|
|
1058
|
+
filletTube.addAuxEdge(`${name}_TANGENT_B_PATH`, tangentBSnap, auxOpts);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Capture tube cap area + round face label for post-boolean retagging (non-closed only).
|
|
1063
|
+
if (!closedLoop) {
|
|
1064
|
+
const roundFaceName = faceTag;
|
|
1065
|
+
const markTubeCap = (capName) => {
|
|
1066
|
+
const tris = filletTube.getFace(capName);
|
|
1067
|
+
const area = computeFaceAreaFromTriangles(tris);
|
|
1068
|
+
if (area > 0) {
|
|
1069
|
+
filletTube.setFaceMetadata(capName, {
|
|
1070
|
+
filletSourceArea: area,
|
|
1071
|
+
filletRoundFace: roundFaceName,
|
|
1072
|
+
filletEndCap: true,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
markTubeCap(`${name}_TUBE_CapStart`);
|
|
1077
|
+
markTubeCap(`${name}_TUBE_CapEnd`);
|
|
1078
|
+
}
|
|
1079
|
+
} catch {
|
|
1080
|
+
// Best-effort – lack of metadata should not abort fillet creation.
|
|
1081
|
+
}
|
|
1082
|
+
} catch (tubeError) {
|
|
1083
|
+
console.error('Tube creation failed:', tubeError?.message || tubeError);
|
|
1084
|
+
|
|
1085
|
+
// Return debug information even on tube failure
|
|
1086
|
+
const debugWedge = new Solid();
|
|
1087
|
+
debugWedge.name = `${name}_FAILED_TUBE_DEBUG`;
|
|
1088
|
+
return {
|
|
1089
|
+
tube: null,
|
|
1090
|
+
wedge: debugWedge,
|
|
1091
|
+
finalSolid: null,
|
|
1092
|
+
centerline: centerlineCopy,
|
|
1093
|
+
tangentA: tangentACopy,
|
|
1094
|
+
tangentB: tangentBCopy,
|
|
1095
|
+
tangentASeam: tangentASnap || [],
|
|
1096
|
+
tangentBSeam: tangentBSnap || [],
|
|
1097
|
+
error: `Tube generation failed: ${tubeError?.message || tubeError}`
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
// Build wedge solid from triangles between centerline and tangency edges
|
|
1103
|
+
logDebug('Creating wedge solid...');
|
|
1104
|
+
const wedgeSolid = new Solid();
|
|
1105
|
+
wedgeSolid.name = `${name}_WEDGE`;
|
|
1106
|
+
|
|
1107
|
+
if (closedLoop) {
|
|
1108
|
+
// CLOSED LOOP PATH - preserve existing logic exactly
|
|
1109
|
+
try {
|
|
1110
|
+
const minTriangleArea = radius * radius * 1e-8;
|
|
1111
|
+
let validTriangles = 0;
|
|
1112
|
+
let skippedTriangles = 0;
|
|
1113
|
+
for (let i = 0; i < centerlineCopy.length - 1; i++) {
|
|
1114
|
+
const c1 = centerlineCopy[i];
|
|
1115
|
+
const c2 = centerlineCopy[i + 1];
|
|
1116
|
+
const tA1 = tangentACopy[i];
|
|
1117
|
+
const tA2 = tangentACopy[i + 1];
|
|
1118
|
+
const tB1 = tangentBCopy[i];
|
|
1119
|
+
const tB2 = tangentBCopy[i + 1];
|
|
1120
|
+
|
|
1121
|
+
const isValidTriangle = (p1, p2, p3) => {
|
|
1122
|
+
const v1 = { x: p2.x - p1.x, y: p2.y - p1.y, z: p2.z - p1.z };
|
|
1123
|
+
const v2 = { x: p3.x - p1.x, y: p3.y - p1.y, z: p3.z - p1.z };
|
|
1124
|
+
const cross = {
|
|
1125
|
+
x: v1.y * v2.z - v1.z * v2.y,
|
|
1126
|
+
y: v1.z * v2.x - v1.x * v2.z,
|
|
1127
|
+
z: v1.x * v2.y - v1.y * v2.x
|
|
1128
|
+
};
|
|
1129
|
+
const area = 0.5 * Math.sqrt(cross.x * cross.x + cross.y * cross.y + cross.z * cross.z);
|
|
1130
|
+
return area > minTriangleArea;
|
|
1131
|
+
};
|
|
1132
|
+
const isValidPoint = (p) => isFinite(p.x) && isFinite(p.y) && isFinite(p.z);
|
|
1133
|
+
const addTriangleWithValidation = (groupName, p1, p2, p3) => {
|
|
1134
|
+
if (!isValidPoint(p1) || !isValidPoint(p2) || !isValidPoint(p3)) {
|
|
1135
|
+
console.warn(`Invalid points detected - p1:(${p1.x},${p1.y},${p1.z}) p2:(${p2.x},${p2.y},${p2.z}) p3:(${p3.x},${p3.y},${p3.z})`);
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
wedgeSolid.addTriangle(groupName, [p1.x, p1.y, p1.z], [p2.x, p2.y, p2.z], [p3.x, p3.y, p3.z]);
|
|
1139
|
+
return true;
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
// Tangent A side
|
|
1143
|
+
if (isValidTriangle(c1, tA1, c2) && addTriangleWithValidation(`${name}_WEDGE_A`, c1, tA1, c2)) validTriangles++; else skippedTriangles++;
|
|
1144
|
+
if (isValidTriangle(c2, tA1, tA2) && addTriangleWithValidation(`${name}_WEDGE_A`, c2, tA1, tA2)) validTriangles++; else skippedTriangles++;
|
|
1145
|
+
// Tangent B side
|
|
1146
|
+
if (isValidTriangle(c1, c2, tB1) && addTriangleWithValidation(`${name}_WEDGE_B`, c1, c2, tB1)) validTriangles++; else skippedTriangles++;
|
|
1147
|
+
if (isValidTriangle(c2, tB2, tB1) && addTriangleWithValidation(`${name}_WEDGE_B`, c2, tB2, tB1)) validTriangles++; else skippedTriangles++;
|
|
1148
|
+
|
|
1149
|
+
// Side walls on original faces - use inset wedge edge points
|
|
1150
|
+
const e1 = edgeWedgeCopy[i];
|
|
1151
|
+
const e2 = edgeWedgeCopy[i + 1];
|
|
1152
|
+
if (e1 && e2) {
|
|
1153
|
+
if (isValidTriangle(e1, tA1, e2) && addTriangleWithValidation(`${name}_SIDE_A`, e1, tA1, e2)) validTriangles++; else skippedTriangles++;
|
|
1154
|
+
if (isValidTriangle(e2, tA1, tA2) && addTriangleWithValidation(`${name}_SIDE_A`, e2, tA1, tA2)) validTriangles++; else skippedTriangles++;
|
|
1155
|
+
if (isValidTriangle(e1, e2, tB1) && addTriangleWithValidation(`${name}_SIDE_B`, e1, e2, tB1)) validTriangles++; else skippedTriangles++;
|
|
1156
|
+
if (isValidTriangle(e2, tB2, tB1) && addTriangleWithValidation(`${name}_SIDE_B`, e2, tB2, tB1)) validTriangles++; else skippedTriangles++;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
logDebug(`Wedge triangles added successfully (closed loop): ${validTriangles} valid, ${skippedTriangles} skipped`);
|
|
1160
|
+
if (validTriangles === 0) {
|
|
1161
|
+
console.error('No valid triangles could be created for wedge solid - all were degenerate');
|
|
1162
|
+
// Return debug information even on wedge failure
|
|
1163
|
+
return {
|
|
1164
|
+
tube: filletTube,
|
|
1165
|
+
wedge: wedgeSolid,
|
|
1166
|
+
finalSolid: null,
|
|
1167
|
+
centerline: centerlineCopy,
|
|
1168
|
+
tangentA: tangentACopy,
|
|
1169
|
+
tangentB: tangentBCopy,
|
|
1170
|
+
tangentASeam: tangentASnap || [],
|
|
1171
|
+
tangentBSeam: tangentBSnap || [],
|
|
1172
|
+
error: 'No valid triangles could be created for wedge solid - all were degenerate'
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
} catch (wedgeError) {
|
|
1176
|
+
console.error('Failed to create wedge triangles (closed loop):', wedgeError?.message || wedgeError);
|
|
1177
|
+
// Return debug information even on wedge error
|
|
1178
|
+
return {
|
|
1179
|
+
tube: filletTube,
|
|
1180
|
+
wedge: wedgeSolid,
|
|
1181
|
+
finalSolid: null,
|
|
1182
|
+
centerline: centerlineCopy,
|
|
1183
|
+
tangentA: tangentACopy,
|
|
1184
|
+
tangentB: tangentBCopy,
|
|
1185
|
+
tangentASeam: tangentASnap || [],
|
|
1186
|
+
tangentBSeam: tangentBSnap || [],
|
|
1187
|
+
error: `Wedge triangle creation failed: ${wedgeError?.message || wedgeError}`
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
} else {
|
|
1191
|
+
// NON-CLOSED LOOP PATH - specialized handling for open edges
|
|
1192
|
+
try {
|
|
1193
|
+
logDebug('Creating wedge solid for non-closed loop...');
|
|
1194
|
+
const minTriangleArea = radius * radius * 1e-8;
|
|
1195
|
+
let validTriangles = 0;
|
|
1196
|
+
let skippedTriangles = 0;
|
|
1197
|
+
|
|
1198
|
+
const isValidTriangle = (p1, p2, p3) => {
|
|
1199
|
+
const v1 = { x: p2.x - p1.x, y: p2.y - p1.y, z: p2.z - p1.z };
|
|
1200
|
+
const v2 = { x: p3.x - p1.x, y: p3.y - p1.y, z: p3.z - p1.z };
|
|
1201
|
+
const cross = {
|
|
1202
|
+
x: v1.y * v2.z - v1.z * v2.y,
|
|
1203
|
+
y: v1.z * v2.x - v1.x * v2.z,
|
|
1204
|
+
z: v1.x * v2.y - v1.y * v2.x
|
|
1205
|
+
};
|
|
1206
|
+
const area = 0.5 * Math.sqrt(cross.x * cross.x + cross.y * cross.y + cross.z * cross.z);
|
|
1207
|
+
return area > minTriangleArea;
|
|
1208
|
+
};
|
|
1209
|
+
const isValidPoint = (p) => isFinite(p.x) && isFinite(p.y) && isFinite(p.z);
|
|
1210
|
+
const addTriangleWithValidation = (groupName, p1, p2, p3) => {
|
|
1211
|
+
if (!isValidPoint(p1) || !isValidPoint(p2) || !isValidPoint(p3)) {
|
|
1212
|
+
console.warn(`Invalid points detected - p1:(${p1.x},${p1.y},${p1.z}) p2:(${p2.x},${p2.y},${p2.z}) p3:(${p3.x},${p3.y},${p3.z})`);
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
wedgeSolid.addTriangle(groupName, [p1.x, p1.y, p1.z], [p2.x, p2.y, p2.z], [p3.x, p3.y, p3.z]);
|
|
1216
|
+
return true;
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
// Create triangular strip along the fillet path
|
|
1220
|
+
// For open edges, we create a proper triangulated surface between centerline and tangent lines
|
|
1221
|
+
for (let i = 0; i < centerlineCopy.length - 1; i++) {
|
|
1222
|
+
const c1 = centerlineCopy[i];
|
|
1223
|
+
const c2 = centerlineCopy[i + 1];
|
|
1224
|
+
const tA1 = tangentACopy[i];
|
|
1225
|
+
const tA2 = tangentACopy[i + 1];
|
|
1226
|
+
const tB1 = tangentBCopy[i];
|
|
1227
|
+
const tB2 = tangentBCopy[i + 1];
|
|
1228
|
+
const e1 = edgeWedgeCopy[i];
|
|
1229
|
+
const e2 = edgeWedgeCopy[i + 1];
|
|
1230
|
+
|
|
1231
|
+
// Create triangulated surfaces between each pair of curves
|
|
1232
|
+
// Surface between centerline and tangent A
|
|
1233
|
+
if (isValidTriangle(c1, c2, tA1) && addTriangleWithValidation(`${name}_SURFACE_CA`, c1, c2, tA1)) validTriangles++; else skippedTriangles++;
|
|
1234
|
+
if (isValidTriangle(c2, tA2, tA1) && addTriangleWithValidation(`${name}_SURFACE_CA`, c2, tA2, tA1)) validTriangles++; else skippedTriangles++;
|
|
1235
|
+
|
|
1236
|
+
// Surface between centerline and tangent B
|
|
1237
|
+
if (isValidTriangle(c1, tB1, c2) && addTriangleWithValidation(`${name}_SURFACE_CB`, c1, tB1, c2)) validTriangles++; else skippedTriangles++;
|
|
1238
|
+
if (isValidTriangle(c2, tB1, tB2) && addTriangleWithValidation(`${name}_SURFACE_CB`, c2, tB1, tB2)) validTriangles++; else skippedTriangles++;
|
|
1239
|
+
|
|
1240
|
+
// Surface between tangent A and edge (original face A)
|
|
1241
|
+
if (e1 && e2) {
|
|
1242
|
+
if (isValidTriangle(tA1, tA2, e1) && addTriangleWithValidation(`${name}_FACE_A`, tA1, tA2, e1)) validTriangles++; else skippedTriangles++;
|
|
1243
|
+
if (isValidTriangle(tA2, e2, e1) && addTriangleWithValidation(`${name}_FACE_A`, tA2, e2, e1)) validTriangles++; else skippedTriangles++;
|
|
1244
|
+
|
|
1245
|
+
// Surface between tangent B and edge (original face B)
|
|
1246
|
+
if (isValidTriangle(tB1, e1, tB2) && addTriangleWithValidation(`${name}_FACE_B`, tB1, e1, tB2)) validTriangles++; else skippedTriangles++;
|
|
1247
|
+
if (isValidTriangle(tB2, e1, e2) && addTriangleWithValidation(`${name}_FACE_B`, tB2, e1, e2)) validTriangles++; else skippedTriangles++;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Add end caps for open edges to create a closed solid
|
|
1252
|
+
if (centerlineCopy.length >= 2) {
|
|
1253
|
+
logDebug('Adding end caps for non-closed loop...');
|
|
1254
|
+
|
|
1255
|
+
// First end cap
|
|
1256
|
+
const firstC = centerlineCopy[0];
|
|
1257
|
+
const firstTA = tangentACopy[0];
|
|
1258
|
+
const firstTB = tangentBCopy[0];
|
|
1259
|
+
const firstE = edgeWedgeCopy[0];
|
|
1260
|
+
|
|
1261
|
+
if (firstE && isValidPoint(firstC) && isValidPoint(firstTA) && isValidPoint(firstTB) && isValidPoint(firstE)) {
|
|
1262
|
+
let endCapFirstC = firstC;
|
|
1263
|
+
let endCapFirstTA = firstTA;
|
|
1264
|
+
let endCapFirstTB = firstTB;
|
|
1265
|
+
let endCapFirstE = firstE;
|
|
1266
|
+
|
|
1267
|
+
// Create triangular fan from centerline to form end cap
|
|
1268
|
+
if (isValidTriangle(endCapFirstC, endCapFirstTB, endCapFirstTA) && addTriangleWithValidation(`${name}_END_CAP_1`, endCapFirstC, endCapFirstTB, endCapFirstTA)) validTriangles++; else skippedTriangles++;
|
|
1269
|
+
if (isValidTriangle(endCapFirstTA, endCapFirstTB, endCapFirstE) && addTriangleWithValidation(`${name}_END_CAP_1`, endCapFirstTA, endCapFirstTB, endCapFirstE)) validTriangles++; else skippedTriangles++;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Last end cap
|
|
1273
|
+
const lastIndex = centerlineCopy.length - 1;
|
|
1274
|
+
const lastC = centerlineCopy[lastIndex];
|
|
1275
|
+
const lastTA = tangentACopy[lastIndex];
|
|
1276
|
+
const lastTB = tangentBCopy[lastIndex];
|
|
1277
|
+
const lastE = edgeWedgeCopy[lastIndex];
|
|
1278
|
+
|
|
1279
|
+
if (lastE && isValidPoint(lastC) && isValidPoint(lastTA) && isValidPoint(lastTB) && isValidPoint(lastE)) {
|
|
1280
|
+
let endCapLastC = lastC;
|
|
1281
|
+
let endCapLastTA = lastTA;
|
|
1282
|
+
let endCapLastTB = lastTB;
|
|
1283
|
+
let endCapLastE = lastE;
|
|
1284
|
+
|
|
1285
|
+
// Create triangular fan from centerline to form end cap (reversed winding for proper normal)
|
|
1286
|
+
if (isValidTriangle(endCapLastC, endCapLastTA, endCapLastTB) && addTriangleWithValidation(`${name}_END_CAP_2`, endCapLastC, endCapLastTA, endCapLastTB)) validTriangles++; else skippedTriangles++;
|
|
1287
|
+
if (isValidTriangle(endCapLastTA, endCapLastE, endCapLastTB) && addTriangleWithValidation(`${name}_END_CAP_2`, endCapLastTA, endCapLastE, endCapLastTB)) validTriangles++; else skippedTriangles++;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
logDebug(`Wedge triangles added successfully (non-closed loop): ${validTriangles} valid, ${skippedTriangles} skipped`);
|
|
1292
|
+
if (validTriangles === 0) {
|
|
1293
|
+
console.error('No valid triangles could be created for non-closed wedge solid - all were degenerate');
|
|
1294
|
+
// Return debug information even on wedge failure
|
|
1295
|
+
return {
|
|
1296
|
+
tube: filletTube,
|
|
1297
|
+
wedge: wedgeSolid,
|
|
1298
|
+
finalSolid: null,
|
|
1299
|
+
centerline: centerlineCopy,
|
|
1300
|
+
tangentA: tangentACopy,
|
|
1301
|
+
tangentB: tangentBCopy,
|
|
1302
|
+
tangentASeam: tangentASnap || [],
|
|
1303
|
+
tangentBSeam: tangentBSnap || [],
|
|
1304
|
+
error: 'No valid triangles could be created for non-closed wedge solid - all were degenerate'
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
} catch (wedgeError) {
|
|
1308
|
+
console.error('Failed to create wedge triangles (non-closed loop):', wedgeError?.message || wedgeError);
|
|
1309
|
+
// Return debug information even on wedge error
|
|
1310
|
+
return {
|
|
1311
|
+
tube: filletTube,
|
|
1312
|
+
wedge: wedgeSolid,
|
|
1313
|
+
finalSolid: null,
|
|
1314
|
+
centerline: centerlineCopy,
|
|
1315
|
+
tangentA: tangentACopy,
|
|
1316
|
+
tangentB: tangentBCopy,
|
|
1317
|
+
tangentASeam: tangentASnap || [],
|
|
1318
|
+
tangentBSeam: tangentBSnap || [],
|
|
1319
|
+
error: `Non-closed wedge triangle creation failed: ${wedgeError?.message || wedgeError}`
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Triangle winding fix for all cases
|
|
1325
|
+
try {
|
|
1326
|
+
wedgeSolid.fixTriangleWindingsByAdjacency();
|
|
1327
|
+
} catch (windingError) {
|
|
1328
|
+
console.warn('Triangle winding fix failed:', windingError?.message || windingError);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (debug) {
|
|
1332
|
+
console.log('Debug mode: wedge solid stored');
|
|
1333
|
+
}
|
|
1334
|
+
logDebug('Wedge solid creation completed');
|
|
1335
|
+
const triangleCount = wedgeSolid._triVerts ? wedgeSolid._triVerts.length / 3 : 0;
|
|
1336
|
+
logDebug('Wedge solid created with', triangleCount, 'triangles (raw count)');
|
|
1337
|
+
try { wedgeSolid.visualize(); } catch { }
|
|
1338
|
+
|
|
1339
|
+
wedgeSolid.pushFace(`${name}_FACE_A`, 0.0001);
|
|
1340
|
+
wedgeSolid.pushFace(`${name}_FACE_B`, 0.0001);
|
|
1341
|
+
|
|
1342
|
+
// Apply end cap offset for INSET fillets using pushFace method
|
|
1343
|
+
if (side === 'INSET' && !closedLoop) {
|
|
1344
|
+
logDebug('Applying end cap offset to INSET fillet using pushFace...');
|
|
1345
|
+
try {
|
|
1346
|
+
// Push both end caps outward by 0.001
|
|
1347
|
+
wedgeSolid.pushFace(`${name}_END_CAP_1`, 0.0001);
|
|
1348
|
+
wedgeSolid.pushFace(`${name}_END_CAP_2`, 0.0001);
|
|
1349
|
+
wedgeSolid.visualize();
|
|
1350
|
+
logDebug('End cap offset applied successfully');
|
|
1351
|
+
} catch (pushError) {
|
|
1352
|
+
console.warn('Failed to apply end cap offset:', pushError?.message || pushError);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Record areas and target round-face label for post-boolean relabeling.
|
|
1357
|
+
const roundFaceName = `${name}_TUBE_Outer`;
|
|
1358
|
+
const markFace = (faceName, isEndCap = false) => {
|
|
1359
|
+
const tris = wedgeSolid.getFace(faceName);
|
|
1360
|
+
const area = computeFaceAreaFromTriangles(tris);
|
|
1361
|
+
if (area > 0) {
|
|
1362
|
+
wedgeSolid.setFaceMetadata(faceName, {
|
|
1363
|
+
filletSourceArea: area,
|
|
1364
|
+
filletRoundFace: roundFaceName,
|
|
1365
|
+
filletEndCap: !!isEndCap,
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
if (!closedLoop) {
|
|
1370
|
+
markFace(`${name}_END_CAP_1`, true);
|
|
1371
|
+
markFace(`${name}_END_CAP_2`, true);
|
|
1372
|
+
}
|
|
1373
|
+
markFace(`${name}_WEDGE_A`, false);
|
|
1374
|
+
markFace(`${name}_WEDGE_B`, false);
|
|
1375
|
+
|
|
1376
|
+
try {
|
|
1377
|
+
const finalSolid = wedgeSolid.subtract(filletTube);
|
|
1378
|
+
finalSolid.name = `${name}_FINAL_FILLET`;
|
|
1379
|
+
try { finalSolid.visualize(); } catch { }
|
|
1380
|
+
logDebug('Final fillet solid created by subtracting tube from wedge', finalSolid);
|
|
1381
|
+
|
|
1382
|
+
return {
|
|
1383
|
+
tube: filletTube,
|
|
1384
|
+
wedge: wedgeSolid,
|
|
1385
|
+
finalSolid,
|
|
1386
|
+
centerline: centerlineCopy,
|
|
1387
|
+
tangentA: tangentACopy,
|
|
1388
|
+
tangentB: tangentBCopy,
|
|
1389
|
+
tangentASeam: tangentASnap || [],
|
|
1390
|
+
tangentBSeam: tangentBSnap || [],
|
|
1391
|
+
};
|
|
1392
|
+
} catch (booleanError) {
|
|
1393
|
+
console.error('Boolean operation failed:', booleanError?.message || booleanError);
|
|
1394
|
+
// Return debug information even on boolean failure
|
|
1395
|
+
return {
|
|
1396
|
+
tube: filletTube,
|
|
1397
|
+
wedge: wedgeSolid,
|
|
1398
|
+
finalSolid: null,
|
|
1399
|
+
centerline: centerlineCopy,
|
|
1400
|
+
tangentA: tangentACopy,
|
|
1401
|
+
tangentB: tangentBCopy,
|
|
1402
|
+
tangentASeam: tangentASnap || [],
|
|
1403
|
+
tangentBSeam: tangentBSnap || [],
|
|
1404
|
+
error: `Boolean operation failed: ${booleanError?.message || booleanError}`
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
} catch (globalError) {
|
|
1408
|
+
console.error('Fillet operation failed completely:', globalError?.message || globalError);
|
|
1409
|
+
// Return minimal debug information even on complete failure
|
|
1410
|
+
return {
|
|
1411
|
+
tube: null,
|
|
1412
|
+
wedge: null,
|
|
1413
|
+
finalSolid: null,
|
|
1414
|
+
centerline: [],
|
|
1415
|
+
tangentA: [],
|
|
1416
|
+
tangentB: [],
|
|
1417
|
+
tangentASeam: [],
|
|
1418
|
+
tangentBSeam: [],
|
|
1419
|
+
error: `Fillet operation failed: ${globalError?.message || globalError}`
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
}
|