UNPKG

@bitbybit-dev/occt

Version:

Bit By Bit Developers CAD algorithms using OpenCascade Technology kernel. Run in Node and in Browser.

537 lines (536 loc) 23.4 kB
import * as Inputs from "../../api/inputs/inputs"; export class DimensionsService { constructor(base, transformsService, converterService, entitiesService, edgesService, wiresService) { this.base = base; this.transformsService = transformsService; this.converterService = converterService; this.entitiesService = entitiesService; this.edgesService = edgesService; this.wiresService = wiresService; } /** * Evaluates a mathematical expression or template string with a given value * @param expression The expression to evaluate (can contain 'val' placeholder) * @param value The numeric value to substitute for 'val' * @param decimalPlaces Number of decimal places to format the result * @param removeTrailingZeros Whether to remove trailing zeros from the result * @returns The evaluated expression as a formatted string */ evaluateExpression(expression, value, decimalPlaces, removeTrailingZeros = false) { try { // Replace 'val' with the actual value in the expression const evaluatedExpression = expression.replace(/val/g, value.toString()); // Simple math expression evaluation (supports +, -, *, /, parentheses) // Only allow safe mathematical operations const safeExpression = evaluatedExpression.replace(/[^0-9+\-*/.() ]/g, ""); if (safeExpression !== evaluatedExpression) { // If expression contains non-math characters, treat it as a template // For template strings, we still want to format numbers with decimal places const formattedValue = removeTrailingZeros ? this.base.math.roundAndRemoveTrailingZeros({ number: value, decimalPlaces }).toString() : value.toFixed(decimalPlaces); return expression.replace(/val/g, formattedValue); } // Evaluate mathematical expression and apply decimal places const result = Function("\"use strict\"; return (" + safeExpression + ")")(); return removeTrailingZeros ? this.base.math.roundAndRemoveTrailingZeros({ number: result, decimalPlaces }).toString() : result.toFixed(decimalPlaces); } catch (error) { // If evaluation fails, return the original value formatted return removeTrailingZeros ? this.base.math.roundAndRemoveTrailingZeros({ number: value, decimalPlaces }).toString() : value.toFixed(decimalPlaces); } } /** * Formats dimension label text with optional expression evaluation * @param value The numeric value to display * @param labelOverwrite Optional expression to evaluate instead of raw value * @param decimalPlaces Number of decimal places for formatting * @param labelSuffix Suffix to append to the text * @param removeTrailingZeros Whether to remove trailing zeros from the result * @returns Formatted dimension label text */ formatDimensionLabel(value, labelOverwrite, decimalPlaces, labelSuffix, removeTrailingZeros = false) { let result; if (labelOverwrite) { result = this.evaluateExpression(labelOverwrite, value, decimalPlaces, removeTrailingZeros); } else { if (removeTrailingZeros) { result = this.base.math.roundAndRemoveTrailingZeros({ number: value, decimalPlaces }).toString(); } else { result = value.toFixed(decimalPlaces); } } return result + " " + labelSuffix; } createArrow(inputs) { const shapesToDelete = []; // Normalize the direction vector (should point away from tip when not flipped) const dir = this.base.vector.normalized({ vector: inputs.direction }); // Determine arrow direction based on flip const arrowDir = inputs.flipped ? dir : this.base.vector.mul({ vector: dir, scalar: -1 }); // Calculate the perpendicular vector in the plane const perpendicular = this.base.vector.cross({ first: arrowDir, second: inputs.normal }); const perpNorm = this.base.vector.normalized({ vector: perpendicular }); // Calculate half angle in radians const halfAngleRad = (inputs.angle / 2) * Math.PI / 180; // Calculate arrow line endpoints const baseLength = inputs.size * Math.cos(halfAngleRad); const sideOffset = inputs.size * Math.sin(halfAngleRad); // Base point for both arrow lines const baseVec = this.base.vector.mul({ vector: arrowDir, scalar: baseLength }); const basePoint = this.base.point.translatePoints({ points: [inputs.tipPoint], translation: baseVec })[0]; // Calculate the two arrow line endpoints const sideVec1 = this.base.vector.mul({ vector: perpNorm, scalar: sideOffset }); const sideVec2 = this.base.vector.mul({ vector: perpNorm, scalar: -sideOffset }); const endPoint1 = this.base.point.translatePoints({ points: [basePoint], translation: sideVec1 })[0]; const endPoint2 = this.base.point.translatePoints({ points: [basePoint], translation: sideVec2 })[0]; // Create the two arrow lines const line1 = this.wiresService.createLineWireWithExtensions({ start: inputs.tipPoint, end: endPoint1, extensionStart: 0, extensionEnd: 0 }); const line2 = this.wiresService.createLineWireWithExtensions({ start: inputs.tipPoint, end: endPoint2, extensionStart: 0, extensionEnd: 0 }); const result = this.converterService.makeCompound({ shapes: [line1, line2] }); // Cleanup shapesToDelete.forEach(shape => shape.delete()); return result; } simpleLinearLengthDimension(inputs) { const shapesToDelete = []; const lineBetweenPoints = this.wiresService.createLineWireWithExtensions({ start: inputs.start, end: inputs.end, extensionStart: inputs.crossingSize, extensionEnd: inputs.crossingSize, }); shapesToDelete.push(lineBetweenPoints); const translatedLine = this.transformsService.translate({ shape: lineBetweenPoints, translation: inputs.direction, }); const translatedPts = this.base.point.translatePoints({ points: [inputs.start, inputs.end], translation: inputs.direction, }); const translatedStartPt = translatedPts[0]; const translatedEndPt = translatedPts[1]; const startLineToTranslatedPoint = this.wiresService.createLineWireWithExtensions({ start: inputs.start, end: translatedStartPt, extensionStart: -inputs.offsetFromPoints, extensionEnd: inputs.crossingSize, }); const endLineToTranslatedPoint = this.wiresService.createLineWireWithExtensions({ start: inputs.end, end: translatedEndPt, extensionStart: -inputs.offsetFromPoints, extensionEnd: inputs.crossingSize, }); const midPt = this.wiresService.midPointOnWire({ shape: translatedLine }); const length = this.base.point.distance({ startPoint: inputs.start, endPoint: inputs.end, }); const labelText = this.formatDimensionLabel(length, inputs.labelOverwrite, inputs.decimalPlaces, inputs.labelSuffix, inputs.removeTrailingZeros); const txtOpt = new Inputs.OCCT.TextWiresDto(); txtOpt.text = labelText; txtOpt.xOffset = 0; txtOpt.yOffset = 0; txtOpt.height = inputs.labelSize; txtOpt.centerOnOrigin = true; const txt = this.wiresService.textWiresWithData(txtOpt); // get the up vector for the dimension plane const normalThreePoints = this.base.point.normalFromThreePoints({ point1: inputs.start, point2: inputs.end, point3: midPt, reverseNormal: true, }); const dirStartEnd = this.base.vector.sub({ first: inputs.end, second: inputs.start, }); let currentShape = this.transformsService.rotate({ shape: txt.compound, angle: -90 + (inputs.labelRotation || 0), axis: [0, 1, 0], }); shapesToDelete.push(...txt.shapes.map((s) => s.shape)); let previousShape = currentShape; // Apply horizontal flip if requested if (inputs.labelFlipHorizontal) { currentShape = this.transformsService.scale3d({ shape: currentShape, scale: [-1, 1, 1], center: [0, 0, 0] }); shapesToDelete.push(previousShape); previousShape = currentShape; } // Apply vertical flip if requested if (inputs.labelFlipVertical) { currentShape = this.transformsService.scale3d({ shape: currentShape, scale: [1, 1, -1], center: [0, 0, 0] }); shapesToDelete.push(previousShape); previousShape = currentShape; } const alignedLabelTxtToDir = this.transformsService.alignNormAndAxis({ shape: currentShape, fromOrigin: [0, 0, 0], fromNorm: [0, 1, 0], fromAx: [0, 0, 1], toOrigin: [0, 0, 0], toNorm: normalThreePoints, toAx: dirStartEnd, }); shapesToDelete.push(previousShape); const normDir = this.base.vector.normalized({ vector: inputs.direction }); const offsetLabelVec = this.base.vector.mul({ vector: normDir, scalar: inputs.labelOffset }); const addToDir = this.base.vector.add({ first: midPt, second: offsetLabelVec, }); const labelTransformed = this.transformsService.translate({ shape: alignedLabelTxtToDir, translation: addToDir }); shapesToDelete.push(alignedLabelTxtToDir); const shapesToInclude = [translatedLine, startLineToTranslatedPoint, endLineToTranslatedPoint, labelTransformed]; // Add arrows if enabled if (inputs.endType === Inputs.OCCT.dimensionEndTypeEnum.arrow) { // Arrow at start point - points outward by default const startArrow = this.createArrow({ tipPoint: translatedStartPt, direction: dirStartEnd, normal: normalThreePoints, size: inputs.arrowSize, angle: inputs.arrowAngle, flipped: !inputs.arrowsFlipped }); shapesToInclude.push(startArrow); // Arrow at end point (direction is reversed) - points outward by default const endArrowDir = this.base.vector.mul({ vector: dirStartEnd, scalar: -1 }); const endArrow = this.createArrow({ tipPoint: translatedEndPt, direction: endArrowDir, normal: normalThreePoints, size: inputs.arrowSize, angle: inputs.arrowAngle, flipped: !inputs.arrowsFlipped }); shapesToInclude.push(endArrow); } const res = this.converterService.makeCompound({ shapes: shapesToInclude }); // delete shapes shapesToDelete.forEach((shape) => { shape.delete(); }); return res; } simpleAngularDimension(inputs) { const shapesToDelete = []; const normDir1 = this.base.vector.normalized({ vector: inputs.direction1 }); const endVec = this.base.vector.mul({ vector: normDir1, scalar: inputs.radius }); const endPt = this.base.point.translatePoints({ points: [endVec], translation: inputs.center, })[0]; const line1WithExt = this.wiresService.createLineWireWithExtensions({ start: inputs.center, end: endPt, extensionStart: -inputs.offsetFromCenter, extensionEnd: inputs.extraSize, }); const normDir2 = this.base.vector.normalized({ vector: inputs.direction2 }); const endVec2 = this.base.vector.mul({ vector: normDir2, scalar: inputs.radius }); const endPt2 = this.base.point.translatePoints({ points: [endVec2], translation: inputs.center, })[0]; const line2WithExt = this.wiresService.createLineWireWithExtensions({ start: inputs.center, end: endPt2, extensionStart: -inputs.offsetFromCenter, extensionEnd: inputs.extraSize, }); const normalThreePoints = this.base.point.normalFromThreePoints({ point1: inputs.center, point2: endPt, point3: endPt2, reverseNormal: true, }); const normalThreePointsRev = this.base.point.normalFromThreePoints({ point1: inputs.center, point2: endPt, point3: endPt2, reverseNormal: false, }); const circ = this.entitiesService.createCircle(inputs.radius, inputs.center, normalThreePointsRev, Inputs.OCCT.typeSpecificityEnum.edge); shapesToDelete.push(circ); const arc = this.edgesService.arcFromCircleAndTwoPoints({ circle: circ, start: endPt, end: endPt2, sense: false, }); shapesToDelete.push(arc); const wireArc = this.wiresService.createWireFromEdge({ shape: arc }); const midPt = this.wiresService.midPointOnWire({ shape: wireArc }); let angle = this.base.vector.angleBetween({ first: inputs.direction1, second: inputs.direction2, }); if (inputs.radians) { angle = this.base.math.degToRad({ number: angle }); } const labelText = this.formatDimensionLabel(angle, inputs.labelOverwrite, inputs.decimalPlaces, inputs.labelSuffix, inputs.removeTrailingZeros); const txtOpt = new Inputs.OCCT.TextWiresDto(); txtOpt.text = labelText; txtOpt.xOffset = 0; txtOpt.yOffset = 0; txtOpt.height = inputs.labelSize; txtOpt.centerOnOrigin = true; const txt = this.wiresService.textWiresWithData(txtOpt); const vectorToMid = this.base.vector.sub({ first: midPt, second: inputs.center, }); const normVecToMid = this.base.vector.normalized({ vector: vectorToMid }); let currentShape = this.transformsService.rotate({ shape: txt.compound, angle: inputs.labelRotation || 0, axis: [0, 1, 0], }); shapesToDelete.push(...txt.shapes.map((s) => s.shape)); let previousShape = currentShape; // Apply horizontal flip if requested if (inputs.labelFlipHorizontal) { currentShape = this.transformsService.scale3d({ shape: currentShape, scale: [-1, 1, 1], center: [0, 0, 0] }); shapesToDelete.push(previousShape); previousShape = currentShape; } // Apply vertical flip if requested if (inputs.labelFlipVertical) { currentShape = this.transformsService.scale3d({ shape: currentShape, scale: [1, 1, -1], center: [0, 0, 0] }); shapesToDelete.push(previousShape); previousShape = currentShape; } const alignedLabelTxtToDir = this.transformsService.alignNormAndAxis({ shape: currentShape, fromOrigin: [0, 0, 0], fromNorm: [0, 1, 0], fromAx: [0, 0, 1], toOrigin: [0, 0, 0], toNorm: normalThreePoints, toAx: normVecToMid, }); shapesToDelete.push(previousShape); const offsetLabelVec = this.base.vector.mul({ vector: normVecToMid, scalar: inputs.labelOffset }); const addToDir = this.base.vector.add({ first: midPt, second: offsetLabelVec, }); const labelTransformed = this.transformsService.translate({ shape: alignedLabelTxtToDir, translation: addToDir }); shapesToDelete.push(alignedLabelTxtToDir); const shapesToInclude = [line1WithExt, line2WithExt, wireArc, labelTransformed]; // Add arrows if enabled if (inputs.endType === Inputs.OCCT.dimensionEndTypeEnum.arrow) { // Get points on the arc at the start and end for arrow placement const arcStartPoint = this.edgesService.pointOnEdgeAtParam({ shape: arc, param: 0 }); const arcEndPoint = this.edgesService.pointOnEdgeAtParam({ shape: arc, param: 1 }); // Get tangent directions at arc endpoints for arrow placement const arcStartTangent = this.edgesService.tangentOnEdgeAtParam({ shape: arc, param: 0 }); const arcEndTangent = this.edgesService.tangentOnEdgeAtParam({ shape: arc, param: 1 }); // Arrow at first direction point (start of arc) - points outward by default const startArrow = this.createArrow({ tipPoint: arcStartPoint, direction: arcStartTangent, normal: normalThreePoints, size: inputs.arrowSize, angle: inputs.arrowAngle, flipped: !inputs.arrowsFlipped }); shapesToInclude.push(startArrow); // Reverse the end tangent so both arrows point outward along the arc by default const reversedEndTangent = this.base.vector.mul({ vector: arcEndTangent, scalar: -1 }); // Arrow at second direction point (end of arc) - points outward by default const endArrow = this.createArrow({ tipPoint: arcEndPoint, direction: reversedEndTangent, normal: normalThreePoints, size: inputs.arrowSize, angle: inputs.arrowAngle, flipped: !inputs.arrowsFlipped }); shapesToInclude.push(endArrow); } const res = this.converterService.makeCompound({ shapes: shapesToInclude }); // delete shapes shapesToDelete.forEach((shape) => { shape.delete(); }); return res; } pinWithLabel(inputs) { const pinLine = this.wiresService.createLineWireWithExtensions({ start: inputs.startPoint, end: inputs.endPoint, extensionStart: -inputs.offsetFromStart, extensionEnd: 0, }); const txtOpt = new Inputs.OCCT.TextWiresDto(); txtOpt.text = inputs.label; txtOpt.xOffset = 0; txtOpt.yOffset = 0; txtOpt.height = inputs.labelSize; txtOpt.centerOnOrigin = true; const text = this.wiresService.textWiresWithData(txtOpt); const textWidth = text.data.width; const dirNorm = this.base.vector.normalized({ vector: inputs.direction }); const offsetLabelVec = this.base.vector.mul({ vector: dirNorm, scalar: textWidth / 2 + inputs.labelOffset }); // const translateTxtVec = this.vector.add({ first: inputs.direction, second: offsetLabelVec }) as Inputs.Base.Vector3; const endPtLabelLine = this.base.point.translatePoints({ points: [inputs.endPoint], translation: inputs.direction, })[0]; const lineBeneathLabel = this.wiresService.createLineWireWithExtensions({ start: inputs.endPoint, end: endPtLabelLine, extensionStart: 0, extensionEnd: 0, }); const normalThreePoints = this.base.point.normalFromThreePoints({ point1: inputs.startPoint, point2: inputs.endPoint, point3: endPtLabelLine, reverseNormal: false, }); let currentShape = this.transformsService.rotate({ shape: text.compound, angle: -90 + (inputs.labelRotation || 0), axis: [0, 1, 0], }); const shapesToDelete = text.shapes.map((s) => s.shape); let previousShape = currentShape; // Apply horizontal flip if requested if (inputs.labelFlipHorizontal) { currentShape = this.transformsService.scale3d({ shape: currentShape, scale: [-1, 1, 1], center: [0, 0, 0] }); shapesToDelete.push(previousShape); previousShape = currentShape; } // Apply vertical flip if requested if (inputs.labelFlipVertical) { currentShape = this.transformsService.scale3d({ shape: currentShape, scale: [1, 1, -1], center: [0, 0, 0] }); shapesToDelete.push(previousShape); previousShape = currentShape; } const alignedLabelTxtToDir = this.transformsService.alignNormAndAxis({ shape: currentShape, fromOrigin: [0, 0, 0], fromNorm: [0, 1, 0], fromAx: [0, 0, 1], toOrigin: [0, 0, 0], toNorm: normalThreePoints, toAx: dirNorm, }); shapesToDelete.push(previousShape); const addToDir = this.base.vector.add({ first: endPtLabelLine, second: offsetLabelVec, }); const labelTransformed = this.transformsService.translate({ shape: alignedLabelTxtToDir, translation: addToDir, }); shapesToDelete.push(alignedLabelTxtToDir); const shapesToInclude = [pinLine, labelTransformed, lineBeneathLabel]; // Add arrow if enabled if (inputs.endType === Inputs.OCCT.dimensionEndTypeEnum.arrow) { // Calculate the direction from start to end for the pin const pinDirection = this.base.vector.sub({ first: inputs.endPoint, second: inputs.startPoint, }); // Arrow at the start point (default orientation is flipped, then apply user flip) const arrow = this.createArrow({ tipPoint: inputs.startPoint, direction: pinDirection, normal: normalThreePoints, size: inputs.arrowSize, angle: inputs.arrowAngle, flipped: !inputs.arrowsFlipped }); shapesToInclude.push(arrow); } const res = this.converterService.makeCompound({ shapes: shapesToInclude }); // delete shapes shapesToDelete.forEach((shape) => { shape.delete(); }); return res; } }