@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
JavaScript
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;
}
}