brep-io-kernel 1.0.0-ci.9
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 +154 -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,151 @@
|
|
|
1
|
+
import { BREP } from "../../BREP/BREP.js";
|
|
2
|
+
const THREE = BREP.THREE;
|
|
3
|
+
// no direct BREP usage here
|
|
4
|
+
|
|
5
|
+
const inputParamsSchema = {
|
|
6
|
+
id: {
|
|
7
|
+
type: "string",
|
|
8
|
+
default_value: null,
|
|
9
|
+
hint: "unique identifier for the mirror feature",
|
|
10
|
+
},
|
|
11
|
+
solids: {
|
|
12
|
+
type: "reference_selection",
|
|
13
|
+
selectionFilter: ["SOLID"],
|
|
14
|
+
multiple: true,
|
|
15
|
+
default_value: [],
|
|
16
|
+
hint: "Select one or more solids to mirror",
|
|
17
|
+
},
|
|
18
|
+
mirrorPlane: {
|
|
19
|
+
type: "reference_selection",
|
|
20
|
+
// Allow mirroring about either a face or a datum plane
|
|
21
|
+
selectionFilter: ["FACE", "PLANE"],
|
|
22
|
+
multiple: false,
|
|
23
|
+
default_value: null,
|
|
24
|
+
hint: "Select the plane or face to mirror about",
|
|
25
|
+
},
|
|
26
|
+
offsetDistance: {
|
|
27
|
+
type: "number",
|
|
28
|
+
default_value: 0,
|
|
29
|
+
hint: "Offset distance for the mirror",
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export class MirrorFeature {
|
|
34
|
+
static shortName = "M";
|
|
35
|
+
static longName = "Mirror";
|
|
36
|
+
|
|
37
|
+
static inputParamsSchema = inputParamsSchema;
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
this.inputParams = {};
|
|
41
|
+
this.persistentData = {};
|
|
42
|
+
}
|
|
43
|
+
async run(partHistory) {
|
|
44
|
+
const scene = partHistory.scene;
|
|
45
|
+
const featureID = this.inputParams.featureID || 'MIRROR';
|
|
46
|
+
|
|
47
|
+
// Resolve targets as objects
|
|
48
|
+
const solidObjs = Array.isArray(this.inputParams.solids) ? this.inputParams.solids.filter(Boolean) : [];
|
|
49
|
+
if (!solidObjs.length) return { added: [], removed: [] };
|
|
50
|
+
|
|
51
|
+
// Resolve mirror reference (face or plane mesh) as object
|
|
52
|
+
const refObj = Array.isArray(this.inputParams.mirrorPlane)
|
|
53
|
+
? (this.inputParams.mirrorPlane[0] || null)
|
|
54
|
+
: (this.inputParams.mirrorPlane || null);
|
|
55
|
+
if (!refObj) return { added: [], removed: [] };
|
|
56
|
+
|
|
57
|
+
// Compute plane origin and normal
|
|
58
|
+
const plane = this.#computeMirrorPlane(refObj, Number(this.inputParams.offsetDistance) || 0);
|
|
59
|
+
if (!plane) return { added: [], removed: [] };
|
|
60
|
+
|
|
61
|
+
const added = [];
|
|
62
|
+
for (const src of solidObjs) {
|
|
63
|
+
if (!src || src.type !== 'SOLID') continue;
|
|
64
|
+
const mirrored = src.mirrorAcrossPlane(plane.point, plane.normal);
|
|
65
|
+
// mutate face names so they are distinct for this feature
|
|
66
|
+
try {
|
|
67
|
+
const idToFaceName = mirrored._idToFaceName instanceof Map ? mirrored._idToFaceName : new Map();
|
|
68
|
+
const mutatedIdToFace = new Map();
|
|
69
|
+
const mutatedFaceToId = new Map();
|
|
70
|
+
for (const [fid, fname] of idToFaceName.entries()) {
|
|
71
|
+
const base = String(fname ?? 'Face');
|
|
72
|
+
const feat = String(featureID ?? 'MIRROR');
|
|
73
|
+
const newName = `${base}::${feat}`;
|
|
74
|
+
mutatedIdToFace.set(fid, newName);
|
|
75
|
+
mutatedFaceToId.set(newName, fid);
|
|
76
|
+
}
|
|
77
|
+
mirrored._idToFaceName = mutatedIdToFace;
|
|
78
|
+
mirrored._faceNameToID = mutatedFaceToId;
|
|
79
|
+
} catch (_) { }
|
|
80
|
+
mirrored.name = `${featureID}:${src.name}:M`;
|
|
81
|
+
// Build face/edge meshes for interaction/visibility
|
|
82
|
+
mirrored.visualize();
|
|
83
|
+
added.push(mirrored);
|
|
84
|
+
}
|
|
85
|
+
return { added, removed: [] };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Given a reference object (FACE or a plane Mesh), compute the mirror plane.
|
|
90
|
+
* Returns { point: THREE.Vector3, normal: THREE.Vector3 }
|
|
91
|
+
*/
|
|
92
|
+
#computeMirrorPlane(refObj, offset) {
|
|
93
|
+
const n = new THREE.Vector3();
|
|
94
|
+
const p = new THREE.Vector3();
|
|
95
|
+
|
|
96
|
+
// If it's a FACE from our BREP visualization
|
|
97
|
+
if (refObj.type === 'FACE' && refObj.geometry) {
|
|
98
|
+
// Average normal (area-weighted) and centroid (area-weighted)
|
|
99
|
+
const pos = refObj.geometry.getAttribute('position');
|
|
100
|
+
if (!pos || pos.count < 3) return null;
|
|
101
|
+
|
|
102
|
+
const a = new THREE.Vector3();
|
|
103
|
+
const b = new THREE.Vector3();
|
|
104
|
+
const c = new THREE.Vector3();
|
|
105
|
+
const ab = new THREE.Vector3();
|
|
106
|
+
const ac = new THREE.Vector3();
|
|
107
|
+
const centroid = new THREE.Vector3();
|
|
108
|
+
let areaSum = 0;
|
|
109
|
+
const toWorld = (out, i) => out.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(refObj.matrixWorld);
|
|
110
|
+
|
|
111
|
+
const triCount = (pos.count / 3) | 0;
|
|
112
|
+
const nAccum = new THREE.Vector3();
|
|
113
|
+
for (let t = 0; t < triCount; t++) {
|
|
114
|
+
const i0 = 3 * t + 0;
|
|
115
|
+
const i1 = 3 * t + 1;
|
|
116
|
+
const i2 = 3 * t + 2;
|
|
117
|
+
toWorld(a, i0); toWorld(b, i1); toWorld(c, i2);
|
|
118
|
+
ab.subVectors(b, a);
|
|
119
|
+
ac.subVectors(c, a);
|
|
120
|
+
const cross = new THREE.Vector3().crossVectors(ac, ab); // area-weighted normal (2*area)
|
|
121
|
+
const triArea = 0.5 * cross.length();
|
|
122
|
+
if (triArea > 0) {
|
|
123
|
+
// centroid of triangle
|
|
124
|
+
centroid.copy(a).add(b).add(c).multiplyScalar(1 / 3);
|
|
125
|
+
p.addScaledVector(centroid, triArea);
|
|
126
|
+
nAccum.add(cross);
|
|
127
|
+
areaSum += triArea;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (areaSum <= 0 || nAccum.lengthSq() === 0) return null;
|
|
131
|
+
p.multiplyScalar(1 / areaSum);
|
|
132
|
+
n.copy(nAccum.normalize());
|
|
133
|
+
} else {
|
|
134
|
+
// Try to interpret as a plane-like Mesh: use its world position and local +Z as normal
|
|
135
|
+
// This matches PlaneGeometry default (XY plane, +Z normal) with applied rotations.
|
|
136
|
+
try {
|
|
137
|
+
const worldQ = new THREE.Quaternion();
|
|
138
|
+
refObj.getWorldQuaternion(worldQ);
|
|
139
|
+
n.set(0, 0, 1).applyQuaternion(worldQ).normalize();
|
|
140
|
+
refObj.getWorldPosition(p);
|
|
141
|
+
} catch (_) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (offset) p.addScaledVector(n, offset);
|
|
147
|
+
return { point: p, normal: n };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// mirror implementation lives in Solid.mirrorAcrossPlane()
|
|
151
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { BREP } from "../../BREP/BREP.js";
|
|
2
|
+
import { LineGeometry } from "three/examples/jsm/Addons.js";
|
|
3
|
+
import { resolveSelectionObject } from "../selectionUtils.js";
|
|
4
|
+
|
|
5
|
+
const THREE = BREP.THREE;
|
|
6
|
+
|
|
7
|
+
const inputParamsSchema = {
|
|
8
|
+
id: {
|
|
9
|
+
type: "string",
|
|
10
|
+
default_value: null,
|
|
11
|
+
hint: "Optional identifier used for naming the offset faces",
|
|
12
|
+
},
|
|
13
|
+
faces: {
|
|
14
|
+
type: "reference_selection",
|
|
15
|
+
selectionFilter: ["FACE"],
|
|
16
|
+
multiple: true,
|
|
17
|
+
default_value: [],
|
|
18
|
+
hint: "Select one or more faces to offset",
|
|
19
|
+
},
|
|
20
|
+
distance: {
|
|
21
|
+
type: "number",
|
|
22
|
+
default_value: 1,
|
|
23
|
+
hint: "Offset distance along the face normal (positive or negative)",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const sanitizeLabel = (value) => {
|
|
28
|
+
const raw = value == null ? "" : String(value);
|
|
29
|
+
const trimmed = raw.trim();
|
|
30
|
+
if (!trimmed) return "";
|
|
31
|
+
return trimmed.replace(/[:\[\]]+/g, "_").replace(/\s+/g, "_").replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const uniqueName = (base, used) => {
|
|
35
|
+
let name = base;
|
|
36
|
+
let idx = 1;
|
|
37
|
+
while (used.has(name)) {
|
|
38
|
+
idx += 1;
|
|
39
|
+
name = `${base}_${idx}`;
|
|
40
|
+
}
|
|
41
|
+
used.add(name);
|
|
42
|
+
return name;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const getFaceNormalWorld = (faceObj) => {
|
|
46
|
+
let n = null;
|
|
47
|
+
if (faceObj && typeof faceObj.getAverageNormal === "function") {
|
|
48
|
+
try { n = faceObj.getAverageNormal().clone(); } catch { n = null; }
|
|
49
|
+
}
|
|
50
|
+
if (!n || n.lengthSq() < 1e-12) {
|
|
51
|
+
try {
|
|
52
|
+
const q = new THREE.Quaternion();
|
|
53
|
+
faceObj.getWorldQuaternion(q);
|
|
54
|
+
n = new THREE.Vector3(0, 0, 1).applyQuaternion(q);
|
|
55
|
+
} catch { n = new THREE.Vector3(0, 0, 1); }
|
|
56
|
+
}
|
|
57
|
+
if (n.lengthSq() < 1e-12) n.set(0, 0, 1);
|
|
58
|
+
n.normalize();
|
|
59
|
+
return n;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getGeometryCenter = (geom) => {
|
|
63
|
+
if (!geom) return new THREE.Vector3();
|
|
64
|
+
try {
|
|
65
|
+
geom.computeBoundingBox();
|
|
66
|
+
if (geom.boundingBox && !geom.boundingBox.isEmpty()) {
|
|
67
|
+
return geom.boundingBox.getCenter(new THREE.Vector3());
|
|
68
|
+
}
|
|
69
|
+
} catch { }
|
|
70
|
+
const pos = geom.getAttribute("position");
|
|
71
|
+
if (!pos || pos.itemSize !== 3 || pos.count === 0) return new THREE.Vector3();
|
|
72
|
+
const acc = new THREE.Vector3();
|
|
73
|
+
for (let i = 0; i < pos.count; i++) {
|
|
74
|
+
acc.x += pos.getX(i);
|
|
75
|
+
acc.y += pos.getY(i);
|
|
76
|
+
acc.z += pos.getZ(i);
|
|
77
|
+
}
|
|
78
|
+
return acc.multiplyScalar(1 / pos.count);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const buildSketchBasis = (origin, normal) => {
|
|
82
|
+
const z = normal.clone().normalize();
|
|
83
|
+
const worldUp = new THREE.Vector3(0, 1, 0);
|
|
84
|
+
const refUp = Math.abs(z.dot(worldUp)) > 0.9 ? new THREE.Vector3(1, 0, 0) : worldUp;
|
|
85
|
+
const x = new THREE.Vector3().crossVectors(refUp, z).normalize();
|
|
86
|
+
const y = new THREE.Vector3().crossVectors(z, x).normalize();
|
|
87
|
+
return {
|
|
88
|
+
origin: [origin.x, origin.y, origin.z],
|
|
89
|
+
x: [x.x, x.y, x.z],
|
|
90
|
+
y: [y.x, y.y, y.z],
|
|
91
|
+
z: [z.x, z.y, z.z],
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const computeBoundaryLoopsFromFace = (faceObj) => {
|
|
96
|
+
const loops = [];
|
|
97
|
+
const geom = faceObj?.geometry;
|
|
98
|
+
if (!geom) return loops;
|
|
99
|
+
const pos = geom.getAttribute("position");
|
|
100
|
+
if (!pos || pos.itemSize !== 3) return loops;
|
|
101
|
+
const idx = geom.getIndex();
|
|
102
|
+
|
|
103
|
+
const world = new Array(pos.count);
|
|
104
|
+
const v = new THREE.Vector3();
|
|
105
|
+
for (let i = 0; i < pos.count; i++) {
|
|
106
|
+
v.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(faceObj.matrixWorld);
|
|
107
|
+
world[i] = [v.x, v.y, v.z];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const keyOf = (p) => `${p[0].toFixed(7)},${p[1].toFixed(7)},${p[2].toFixed(7)}`;
|
|
111
|
+
const canonMap = new Map();
|
|
112
|
+
const canonPts = [];
|
|
113
|
+
const origToCanon = new Array(world.length);
|
|
114
|
+
for (let i = 0; i < world.length; i++) {
|
|
115
|
+
const k = keyOf(world[i]);
|
|
116
|
+
let ci = canonMap.get(k);
|
|
117
|
+
if (ci === undefined) {
|
|
118
|
+
ci = canonPts.length;
|
|
119
|
+
canonMap.set(k, ci);
|
|
120
|
+
canonPts.push(world[i]);
|
|
121
|
+
}
|
|
122
|
+
origToCanon[i] = ci;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const edgeCount = new Map();
|
|
126
|
+
const inc = (a, b) => {
|
|
127
|
+
const A = origToCanon[a] >>> 0;
|
|
128
|
+
const B = origToCanon[b] >>> 0;
|
|
129
|
+
const i = Math.min(A, B), j = Math.max(A, B);
|
|
130
|
+
const k = `${i},${j}`;
|
|
131
|
+
edgeCount.set(k, (edgeCount.get(k) || 0) + 1);
|
|
132
|
+
};
|
|
133
|
+
if (idx) {
|
|
134
|
+
for (let t = 0; t < idx.count; t += 3) {
|
|
135
|
+
inc(idx.getX(t + 0) >>> 0, idx.getX(t + 1) >>> 0);
|
|
136
|
+
inc(idx.getX(t + 1) >>> 0, idx.getX(t + 2) >>> 0);
|
|
137
|
+
inc(idx.getX(t + 2) >>> 0, idx.getX(t + 0) >>> 0);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
const triCount = (pos.count / 3) | 0;
|
|
141
|
+
for (let t = 0; t < triCount; t++) {
|
|
142
|
+
const i0 = 3 * t + 0;
|
|
143
|
+
const i1 = 3 * t + 1;
|
|
144
|
+
const i2 = 3 * t + 2;
|
|
145
|
+
inc(i0, i1);
|
|
146
|
+
inc(i1, i2);
|
|
147
|
+
inc(i2, i0);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const adj = new Map();
|
|
152
|
+
const addAdj = (a, b) => {
|
|
153
|
+
let s = adj.get(a);
|
|
154
|
+
if (!s) { s = new Set(); adj.set(a, s); }
|
|
155
|
+
s.add(b);
|
|
156
|
+
};
|
|
157
|
+
for (const [k, c] of edgeCount.entries()) {
|
|
158
|
+
if (c !== 1) continue;
|
|
159
|
+
const parts = k.split(",");
|
|
160
|
+
const i = Number(parts[0]);
|
|
161
|
+
const j = Number(parts[1]);
|
|
162
|
+
addAdj(i, j);
|
|
163
|
+
addAdj(j, i);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const visited = new Set();
|
|
167
|
+
const edgeKey = (a, b) => {
|
|
168
|
+
const i = Math.min(a, b), j = Math.max(a, b);
|
|
169
|
+
return `${i},${j}`;
|
|
170
|
+
};
|
|
171
|
+
for (const [a, neighbors] of adj.entries()) {
|
|
172
|
+
for (const b of neighbors) {
|
|
173
|
+
const k = edgeKey(a, b);
|
|
174
|
+
if (visited.has(k)) continue;
|
|
175
|
+
const ring = [a, b];
|
|
176
|
+
visited.add(k);
|
|
177
|
+
let prev = a, cur = b, guard = 0;
|
|
178
|
+
while (guard++ < 100000) {
|
|
179
|
+
const nset = adj.get(cur) || new Set();
|
|
180
|
+
let next = null;
|
|
181
|
+
for (const n of nset) {
|
|
182
|
+
if (n === prev) continue;
|
|
183
|
+
const kk = edgeKey(cur, n);
|
|
184
|
+
if (!visited.has(kk)) { next = n; break; }
|
|
185
|
+
}
|
|
186
|
+
if (next == null) break;
|
|
187
|
+
visited.add(edgeKey(cur, next));
|
|
188
|
+
ring.push(next);
|
|
189
|
+
prev = cur;
|
|
190
|
+
cur = next;
|
|
191
|
+
if (cur === ring[0]) break;
|
|
192
|
+
}
|
|
193
|
+
if (ring.length >= 3) {
|
|
194
|
+
const pts = [];
|
|
195
|
+
for (let i = 0; i < ring.length; i++) {
|
|
196
|
+
const p = canonPts[ring[i]];
|
|
197
|
+
if (pts.length) {
|
|
198
|
+
const q = pts[pts.length - 1];
|
|
199
|
+
if (q[0] === p[0] && q[1] === p[1] && q[2] === p[2]) continue;
|
|
200
|
+
}
|
|
201
|
+
pts.push([p[0], p[1], p[2]]);
|
|
202
|
+
}
|
|
203
|
+
if (pts.length >= 3) loops.push({ pts, isHole: false });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (loops.length) {
|
|
209
|
+
let n = null;
|
|
210
|
+
try { n = faceObj.getAverageNormal().clone(); } catch { n = null; }
|
|
211
|
+
if (!n || n.lengthSq() < 1e-12) n = new THREE.Vector3(0, 0, 1);
|
|
212
|
+
n.normalize();
|
|
213
|
+
let ux = new THREE.Vector3(1, 0, 0);
|
|
214
|
+
if (Math.abs(n.dot(ux)) > 0.99) ux.set(0, 1, 0);
|
|
215
|
+
const U = new THREE.Vector3().crossVectors(n, ux).normalize();
|
|
216
|
+
const V = new THREE.Vector3().crossVectors(n, U).normalize();
|
|
217
|
+
const area2 = (arr) => {
|
|
218
|
+
let a = 0;
|
|
219
|
+
for (let i = 0; i < arr.length; i++) {
|
|
220
|
+
const p = arr[i];
|
|
221
|
+
const q = arr[(i + 1) % arr.length];
|
|
222
|
+
a += (p.x * q.y - q.x * p.y);
|
|
223
|
+
}
|
|
224
|
+
return 0.5 * a;
|
|
225
|
+
};
|
|
226
|
+
const loopAreas = loops.map((loop) => {
|
|
227
|
+
const v2 = loop.pts.map((P) => {
|
|
228
|
+
const vec = new THREE.Vector3(P[0], P[1], P[2]);
|
|
229
|
+
return new THREE.Vector2(vec.dot(U), vec.dot(V));
|
|
230
|
+
});
|
|
231
|
+
return area2(v2);
|
|
232
|
+
});
|
|
233
|
+
let outerIdx = 0, outerAbs = 0;
|
|
234
|
+
for (let i = 0; i < loopAreas.length; i++) {
|
|
235
|
+
const ab = Math.abs(loopAreas[i]);
|
|
236
|
+
if (ab > outerAbs) { outerAbs = ab; outerIdx = i; }
|
|
237
|
+
}
|
|
238
|
+
const outerSign = Math.sign(loopAreas[outerIdx] || 1);
|
|
239
|
+
for (let i = 0; i < loops.length; i++) {
|
|
240
|
+
const sign = Math.sign(loopAreas[i] || 0);
|
|
241
|
+
loops[i].isHole = (sign !== outerSign);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return loops;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const buildEdgesFromLoops = (loops, groupName) => {
|
|
249
|
+
const edges = [];
|
|
250
|
+
let edgeIdx = 0;
|
|
251
|
+
for (const loop of loops) {
|
|
252
|
+
const pts = Array.isArray(loop?.pts) ? loop.pts : loop;
|
|
253
|
+
if (!Array.isArray(pts) || pts.length < 2) continue;
|
|
254
|
+
const poly = pts.map((p) => [p[0], p[1], p[2]]);
|
|
255
|
+
const positions = [];
|
|
256
|
+
for (const p of poly) positions.push(p[0], p[1], p[2]);
|
|
257
|
+
if (poly.length >= 2) {
|
|
258
|
+
const first = poly[0];
|
|
259
|
+
const last = poly[poly.length - 1];
|
|
260
|
+
if (!(first[0] === last[0] && first[1] === last[1] && first[2] === last[2])) {
|
|
261
|
+
positions.push(first[0], first[1], first[2]);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const lg = new LineGeometry();
|
|
265
|
+
lg.setPositions(positions);
|
|
266
|
+
try { lg.computeBoundingSphere(); } catch { }
|
|
267
|
+
const edge = new BREP.Edge(lg);
|
|
268
|
+
edge.name = `${groupName}:L${edgeIdx++}`;
|
|
269
|
+
edge.closedLoop = true;
|
|
270
|
+
edge.userData = {
|
|
271
|
+
polylineLocal: poly,
|
|
272
|
+
polylineWorld: true,
|
|
273
|
+
isHole: !!loop?.isHole,
|
|
274
|
+
};
|
|
275
|
+
edges.push(edge);
|
|
276
|
+
}
|
|
277
|
+
return edges;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export class OffsetFaceFeature {
|
|
281
|
+
static shortName = "O.F";
|
|
282
|
+
static longName = "Offset Face";
|
|
283
|
+
static inputParamsSchema = inputParamsSchema;
|
|
284
|
+
|
|
285
|
+
constructor() {
|
|
286
|
+
this.inputParams = {};
|
|
287
|
+
this.persistentData = {};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async run(partHistory) {
|
|
291
|
+
const faceEntries = Array.isArray(this.inputParams.faces) ? this.inputParams.faces.filter(Boolean) : [];
|
|
292
|
+
if (!faceEntries.length) {
|
|
293
|
+
console.warn("[OffsetFaceFeature] No faces selected.");
|
|
294
|
+
return { added: [], removed: [] };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const dist = Number(this.inputParams.distance);
|
|
298
|
+
if (!Number.isFinite(dist)) {
|
|
299
|
+
console.warn("[OffsetFaceFeature] Distance must be a finite number.");
|
|
300
|
+
return { added: [], removed: [] };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const featureIdRaw = this.inputParams.featureID || OffsetFaceFeature.shortName || "OffsetFace";
|
|
304
|
+
const featureId = String(featureIdRaw).trim() || "OffsetFace";
|
|
305
|
+
const added = [];
|
|
306
|
+
const usedNames = new Set();
|
|
307
|
+
|
|
308
|
+
let idx = 0;
|
|
309
|
+
for (const entry of faceEntries) {
|
|
310
|
+
const faceObj = resolveSelectionObject(entry, partHistory);
|
|
311
|
+
if (!faceObj || faceObj.type !== "FACE" || !faceObj.geometry) continue;
|
|
312
|
+
try { faceObj.updateMatrixWorld(true); } catch { }
|
|
313
|
+
|
|
314
|
+
const normal = getFaceNormalWorld(faceObj);
|
|
315
|
+
const offsetVec = normal.clone().multiplyScalar(dist);
|
|
316
|
+
|
|
317
|
+
const geom = faceObj.geometry.clone();
|
|
318
|
+
geom.applyMatrix4(faceObj.matrixWorld);
|
|
319
|
+
if (offsetVec.lengthSq() > 0) {
|
|
320
|
+
geom.applyMatrix4(new THREE.Matrix4().makeTranslation(offsetVec.x, offsetVec.y, offsetVec.z));
|
|
321
|
+
}
|
|
322
|
+
geom.computeVertexNormals();
|
|
323
|
+
geom.computeBoundingBox();
|
|
324
|
+
geom.computeBoundingSphere();
|
|
325
|
+
|
|
326
|
+
const sourceFaceName = String(faceObj.userData?.faceName || faceObj.name || `FACE_${idx + 1}`);
|
|
327
|
+
const safeLabel = sanitizeLabel(sourceFaceName) || `FACE_${idx + 1}`;
|
|
328
|
+
const baseName = `${featureId}:${safeLabel}`;
|
|
329
|
+
const groupName = uniqueName(baseName, usedNames);
|
|
330
|
+
|
|
331
|
+
const group = new THREE.Group();
|
|
332
|
+
group.type = "SKETCH";
|
|
333
|
+
group.name = groupName;
|
|
334
|
+
group.renderOrder = 1;
|
|
335
|
+
group.userData = group.userData || {};
|
|
336
|
+
|
|
337
|
+
const offsetFace = new BREP.Face(geom);
|
|
338
|
+
offsetFace.name = `${groupName}:PROFILE`;
|
|
339
|
+
offsetFace.userData.faceName = offsetFace.name;
|
|
340
|
+
offsetFace.userData.sourceFaceName = sourceFaceName;
|
|
341
|
+
offsetFace.userData.offsetDistance = dist;
|
|
342
|
+
|
|
343
|
+
group.userData.sourceFaceName = sourceFaceName;
|
|
344
|
+
group.userData.offsetDistance = dist;
|
|
345
|
+
group.userData.sketchBasis = buildSketchBasis(getGeometryCenter(geom), normal);
|
|
346
|
+
|
|
347
|
+
try { offsetFace.updateMatrixWorld(true); } catch { }
|
|
348
|
+
const loops = computeBoundaryLoopsFromFace(offsetFace);
|
|
349
|
+
if (loops.length) offsetFace.userData.boundaryLoopsWorld = loops;
|
|
350
|
+
|
|
351
|
+
const edges = buildEdgesFromLoops(loops, groupName);
|
|
352
|
+
for (const edge of edges) {
|
|
353
|
+
edge.faces.push(offsetFace);
|
|
354
|
+
group.add(edge);
|
|
355
|
+
}
|
|
356
|
+
offsetFace.edges = edges;
|
|
357
|
+
group.add(offsetFace);
|
|
358
|
+
|
|
359
|
+
added.push(group);
|
|
360
|
+
idx += 1;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!added.length) {
|
|
364
|
+
console.warn("[OffsetFaceFeature] No valid faces resolved.");
|
|
365
|
+
return { added: [], removed: [] };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return { added, removed: [] };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { OffsetShellSolid } from '../../BREP/OffsetShellSolid.js';
|
|
2
|
+
|
|
3
|
+
const inputParamsSchema = {
|
|
4
|
+
id: {
|
|
5
|
+
type: 'string',
|
|
6
|
+
default_value: null,
|
|
7
|
+
hint: 'Optional identifier used when naming the generated solid and faces',
|
|
8
|
+
},
|
|
9
|
+
distance: {
|
|
10
|
+
type: 'number',
|
|
11
|
+
default_value: 1,
|
|
12
|
+
hint: 'Positive grows the shell, negative shrinks it',
|
|
13
|
+
},
|
|
14
|
+
faces: {
|
|
15
|
+
type: 'reference_selection',
|
|
16
|
+
selectionFilter: ['FACE'],
|
|
17
|
+
multiple: true,
|
|
18
|
+
default_value: [],
|
|
19
|
+
hint: 'Pick one or more faces on the solid to shell (used to find the solid)',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class OffsetShellFeature {
|
|
24
|
+
static shortName = 'O.S';
|
|
25
|
+
static longName = 'Offset Shell';
|
|
26
|
+
static inputParamsSchema = inputParamsSchema;
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
this.inputParams = {};
|
|
30
|
+
this.persistentData = {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async run(partHistory) {
|
|
34
|
+
const faceEntries = Array.isArray(this.inputParams.faces) ? this.inputParams.faces.filter(Boolean) : [];
|
|
35
|
+
if (!faceEntries.length) {
|
|
36
|
+
console.warn('[OffsetShellFeature] No faces selected.');
|
|
37
|
+
return { added: [], removed: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const solids = new Set();
|
|
41
|
+
for (const entry of faceEntries) {
|
|
42
|
+
if (!entry || entry.type !== 'FACE') continue;
|
|
43
|
+
const solid = entry.parentSolid || (entry.parent && entry.parent.type === 'SOLID' ? entry.parent : null);
|
|
44
|
+
if (solid) solids.add(solid);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!solids.size) {
|
|
48
|
+
console.warn('[OffsetShellFeature] Selected faces are not attached to a solid.');
|
|
49
|
+
return { added: [], removed: [] };
|
|
50
|
+
}
|
|
51
|
+
if (solids.size > 1) {
|
|
52
|
+
console.warn('[OffsetShellFeature] Faces from multiple solids selected; aborting offset shell.');
|
|
53
|
+
return { added: [], removed: [] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const targetSolid = solids.values().next().value;
|
|
57
|
+
|
|
58
|
+
const dist = Number(this.inputParams.distance);
|
|
59
|
+
if (!Number.isFinite(dist) || dist === 0) {
|
|
60
|
+
console.warn('[OffsetShellFeature] Distance must be a non-zero finite number.');
|
|
61
|
+
return { added: [], removed: [] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const fallbackId = OffsetShellFeature.shortName || OffsetShellFeature.longName || 'OffsetShell';
|
|
65
|
+
const featureId = (this.inputParams.featureID || fallbackId).trim();
|
|
66
|
+
const newSolidName = `${targetSolid.name || 'Solid'}_${featureId}`;
|
|
67
|
+
|
|
68
|
+
let resultSolid = null;
|
|
69
|
+
try {
|
|
70
|
+
resultSolid = OffsetShellSolid.generate(targetSolid, dist, {
|
|
71
|
+
featureId,
|
|
72
|
+
newSolidName,
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('[OffsetShellFeature] Solid.offsetShell failed:', err);
|
|
76
|
+
return { added: [], removed: [] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!resultSolid) {
|
|
80
|
+
console.warn('[OffsetShellFeature] offsetShell returned no result.');
|
|
81
|
+
return { added: [], removed: [] };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try { resultSolid.name = newSolidName; } catch {}
|
|
85
|
+
try { resultSolid.visualize(); } catch {}
|
|
86
|
+
|
|
87
|
+
return { added: [resultSolid], removed: [] };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { BREP } from "../../BREP/BREP.js";
|
|
2
|
+
|
|
3
|
+
const THREE = BREP.THREE;
|
|
4
|
+
|
|
5
|
+
const inputParamsSchema = {
|
|
6
|
+
id: {
|
|
7
|
+
type: "string",
|
|
8
|
+
default_value: null,
|
|
9
|
+
hint: "unique identifier for the overlap cleanup feature",
|
|
10
|
+
},
|
|
11
|
+
targetSolid: {
|
|
12
|
+
type: "reference_selection",
|
|
13
|
+
selectionFilter: ["SOLID"],
|
|
14
|
+
multiple: false,
|
|
15
|
+
default_value: null,
|
|
16
|
+
hint: "Select a solid to clean up by overlap intersection",
|
|
17
|
+
},
|
|
18
|
+
distance: {
|
|
19
|
+
type: "number",
|
|
20
|
+
default_value: 0.0001,
|
|
21
|
+
step: 0.0001,
|
|
22
|
+
hint: "Translation distance for each axis-shifted copy",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class OverlapCleanupFeature {
|
|
27
|
+
static shortName = "OVL";
|
|
28
|
+
static longName = "Overlap Cleanup";
|
|
29
|
+
static inputParamsSchema = inputParamsSchema;
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
this.inputParams = {};
|
|
33
|
+
this.persistentData = {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async run(partHistory) {
|
|
37
|
+
const scene = partHistory?.scene;
|
|
38
|
+
const targetEntry = Array.isArray(this.inputParams.targetSolid)
|
|
39
|
+
? (this.inputParams.targetSolid[0] || null)
|
|
40
|
+
: (this.inputParams.targetSolid || null);
|
|
41
|
+
const target = (targetEntry && typeof targetEntry === "object")
|
|
42
|
+
? targetEntry
|
|
43
|
+
: (targetEntry ? await scene?.getObjectByName(String(targetEntry)) : null);
|
|
44
|
+
|
|
45
|
+
if (!target || target.type !== "SOLID") return { added: [], removed: [] };
|
|
46
|
+
|
|
47
|
+
const distanceRaw = Number(this.inputParams.distance);
|
|
48
|
+
const distance = Number.isFinite(distanceRaw) ? distanceRaw : 0.0001;
|
|
49
|
+
const featureID = this.inputParams.featureID || this.inputParams.id || null;
|
|
50
|
+
|
|
51
|
+
const base = target.clone();
|
|
52
|
+
const copies = [];
|
|
53
|
+
const shifts = [
|
|
54
|
+
[distance, 0, 0],
|
|
55
|
+
[0, distance, 0],
|
|
56
|
+
[0, 0, distance],
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
for (const [dx, dy, dz] of shifts) {
|
|
60
|
+
const copy = target.clone();
|
|
61
|
+
const t = new THREE.Matrix4().makeTranslation(dx, dy, dz);
|
|
62
|
+
copy.bakeTransform(t);
|
|
63
|
+
copies.push(copy);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const effects = await BREP.applyBooleanOperation(
|
|
67
|
+
partHistory || {},
|
|
68
|
+
base,
|
|
69
|
+
{ operation: "INTERSECT", targets: copies },
|
|
70
|
+
featureID,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const added = Array.isArray(effects.added) ? effects.added : [];
|
|
74
|
+
if (added.length > 0) {
|
|
75
|
+
const result = added[0];
|
|
76
|
+
const baseName = featureID || target.name || "Solid";
|
|
77
|
+
try { result.name = `${baseName}_Overlap`; } catch (_) {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const removed = Array.isArray(effects.removed) ? effects.removed.slice() : [];
|
|
81
|
+
removed.push(target);
|
|
82
|
+
try { for (const obj of removed) { if (obj) obj.__removeFlag = true; } } catch {}
|
|
83
|
+
return { added, removed };
|
|
84
|
+
}
|
|
85
|
+
}
|