UNPKG

replicad

Version:

The library to build browser based 3D models with code

1,635 lines 613 kB
(function(global, factory) { typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.replicad = {})); })(this, function(exports2) { "use strict"; const OC = { library: null }; const setOC = (oc) => { OC.library = oc; }; const getOC = () => { if (!OC.library) throw new Error("oppencascade has not been loaded"); return OC.library; }; if (!globalThis.FinalizationRegistry) { console.log("Garbage collection will not work"); globalThis.FinalizationRegistry = () => ({ register: () => null, unregister: () => null }); } const deletetableRegistry = new globalThis.FinalizationRegistry( (heldValue) => { try { heldValue.delete(); } catch (e) { console.error(e); } } ); class WrappingObj { constructor(wrapped) { this.oc = getOC(); if (wrapped) { deletetableRegistry.register(this, wrapped, wrapped); } this._wrapped = wrapped; } get wrapped() { if (this._wrapped === null) throw new Error("This object has been deleted"); return this._wrapped; } set wrapped(newWrapped) { if (this._wrapped) { deletetableRegistry.unregister(this._wrapped); this._wrapped.delete(); } deletetableRegistry.register(this, newWrapped, newWrapped); this._wrapped = newWrapped; } delete() { var _a; deletetableRegistry.unregister(this.wrapped); (_a = this.wrapped) == null ? void 0 : _a.delete(); this._wrapped = null; } } const GCWithScope = () => { function gcWithScope(value) { deletetableRegistry.register(gcWithScope, value); return value; } return gcWithScope; }; const GCWithObject = (obj) => { function registerForGC(value) { deletetableRegistry.register(obj, value); return value; } return registerForGC; }; const localGC = (debug) => { const cleaner = /* @__PURE__ */ new Set(); return [ (v) => { cleaner.add(v); return v; }, () => { [...cleaner.values()].forEach((d) => d.delete()); cleaner.clear(); }, debug ? cleaner : void 0 ]; }; const HASH_CODE_MAX = 2147483647; const DEG2RAD = Math.PI / 180; const RAD2DEG = 180 / Math.PI; const round3 = (v) => Math.round(v * 1e3) / 1e3; function isPoint(p) { if (Array.isArray(p)) return p.length === 3 || p.length === 2; else if (p instanceof Vector) return true; else if (p && typeof (p == null ? void 0 : p.XYZ) === "function") return true; return false; } const makeAx3 = (center, dir, xDir) => { const oc = getOC(); const origin = asPnt(center); const direction = asDir(dir); let axis; if (xDir) { const xDirection = asDir(xDir); axis = new oc.gp_Ax3_3(origin, direction, xDirection); xDirection.delete(); } else { axis = new oc.gp_Ax3_4(origin, direction); } origin.delete(); direction.delete(); return axis; }; const makeAx2 = (center, dir, xDir) => { const oc = getOC(); const origin = asPnt(center); const direction = asDir(dir); let axis; if (xDir) { const xDirection = asDir(xDir); axis = new oc.gp_Ax2_2(origin, direction, xDirection); xDirection.delete(); } else { axis = new oc.gp_Ax2_3(origin, direction); } origin.delete(); direction.delete(); return axis; }; const makeAx1 = (center, dir) => { const oc = getOC(); const origin = asPnt(center); const direction = asDir(dir); const axis = new oc.gp_Ax1_2(origin, direction); origin.delete(); direction.delete(); return axis; }; const makeVec = (vector = [0, 0, 0]) => { const oc = getOC(); if (Array.isArray(vector)) { if (vector.length === 3) return new oc.gp_Vec_4(...vector); else if (vector.length === 2) return new oc.gp_Vec_4(...vector, 0); } else if (vector instanceof Vector) { return new oc.gp_Vec_3(vector.wrapped.XYZ()); } else if (vector.XYZ) return new oc.gp_Vec_3(vector.XYZ()); return new oc.gp_Vec_4(0, 0, 0); }; class Vector extends WrappingObj { constructor(vector = [0, 0, 0]) { super(makeVec(vector)); } get repr() { return `x: ${round3(this.x)}, y: ${round3(this.y)}, z: ${round3(this.z)}`; } get x() { return this.wrapped.X(); } get y() { return this.wrapped.Y(); } get z() { return this.wrapped.Z(); } get Length() { return this.wrapped.Magnitude(); } toTuple() { return [this.x, this.y, this.z]; } cross(v) { return new Vector(this.wrapped.Crossed(v.wrapped)); } dot(v) { return this.wrapped.Dot(v.wrapped); } sub(v) { return new Vector(this.wrapped.Subtracted(v.wrapped)); } add(v) { return new Vector(this.wrapped.Added(v.wrapped)); } multiply(scale2) { return new Vector(this.wrapped.Multiplied(scale2)); } normalized() { return new Vector(this.wrapped.Normalized()); } normalize() { this.wrapped.Normalize(); return this; } getCenter() { return this; } getAngle(v) { return this.wrapped.Angle(v.wrapped) * RAD2DEG; } projectToPlane(plane) { const base = plane.origin; const normal = plane.zDir; const v1 = this.sub(base); const v2 = normal.multiply(v1.dot(normal) / normal.Length ** 2); const projection = this.sub(v2); v1.delete(); v2.delete(); return projection; } equals(other) { return this.wrapped.IsEqual(other.wrapped, 1e-5, 1e-5); } toPnt() { return new this.oc.gp_Pnt_2(this.wrapped.XYZ()); } toDir() { return new this.oc.gp_Dir_3(this.wrapped.XYZ()); } rotate(angle, center = [0, 0, 0], direction = [0, 0, 1]) { const ax = makeAx1(center, direction); this.wrapped.Rotate(ax, angle * DEG2RAD); ax.delete(); return this; } } const DIRECTIONS$1 = { X: [1, 0, 0], Y: [0, 1, 0], Z: [0, 0, 1] }; function makeDirection(p) { if (p === "X" || p === "Y" || p === "Z") { return DIRECTIONS$1[p]; } return p; } function asPnt(coords) { const v = new Vector(coords); const pnt2 = v.toPnt(); v.delete(); return pnt2; } function asDir(coords) { const v = new Vector(coords); const dir = v.toDir(); v.delete(); return dir; } class Transformation extends WrappingObj { constructor(transform) { const oc = getOC(); super(transform || new oc.gp_Trsf_1()); } translate(xDistOrVector, yDist = 0, zDist = 0) { const translation = new Vector( typeof xDistOrVector === "number" ? [xDistOrVector, yDist, zDist] : xDistOrVector ); this.wrapped.SetTranslation_1(translation.wrapped); return this; } rotate(angle, position = [0, 0, 0], direction = [0, 0, 1]) { const dir = asDir(direction); const origin = asPnt(position); const axis = new this.oc.gp_Ax1_2(origin, dir); this.wrapped.SetRotation_1(axis, angle * DEG2RAD); axis.delete(); dir.delete(); origin.delete(); return this; } mirror(inputPlane = "YZ", inputOrigin) { const r = GCWithScope(); let origin; let direction; if (typeof inputPlane === "string") { const plane = r(createNamedPlane(inputPlane, inputOrigin)); origin = plane.origin; direction = plane.zDir; } else if (inputPlane instanceof Plane) { origin = inputOrigin || inputPlane.origin; direction = inputPlane.zDir; } else { origin = inputOrigin || [0, 0, 0]; direction = inputPlane; } const mirrorAxis = r(makeAx2(origin, direction)); this.wrapped.SetMirror_3(mirrorAxis); return this; } scale(center, scale2) { const pnt2 = asPnt(center); this.wrapped.SetScale(pnt2, scale2); pnt2.delete(); return this; } coordSystemChange(fromSystem, toSystem) { const r = GCWithScope(); const fromAx = r( fromSystem === "reference" ? new this.oc.gp_Ax3_1() : makeAx3(fromSystem.origin, fromSystem.zDir, fromSystem.xDir) ); const toAx = r( toSystem === "reference" ? new this.oc.gp_Ax3_1() : makeAx3(toSystem.origin, toSystem.zDir, toSystem.xDir) ); this.wrapped.SetTransformation_1(fromAx, toAx); return this; } transformPoint(point) { const pnt2 = asPnt(point); const newPoint = pnt2.Transformed(this.wrapped); pnt2.delete(); return newPoint; } transform(shape) { const transformer = new this.oc.BRepBuilderAPI_Transform_2( shape, this.wrapped, true ); return transformer.ModifiedShape(shape); } } class Plane { constructor(origin, xDirection = null, normal = [0, 0, 1]) { this.oc = getOC(); const zDir = new Vector(normal); if (zDir.Length === 0) { throw new Error("normal should be non null"); } this.zDir = zDir.normalize(); let xDir; if (!xDirection) { const ax3 = makeAx3(origin, zDir); xDir = new Vector(ax3.XDirection()); ax3.delete(); } else { xDir = new Vector(xDirection); } if (xDir.Length === 0) { throw new Error("xDir should be non null"); } this.xDir = xDir.normalize(); this.yDir = this.zDir.cross(this.xDir).normalize(); this.origin = new Vector(origin); } delete() { this.localToGlobal.delete(); this.xDir.delete(); this.yDir.delete(); this.zDir.delete(); this._origin.delete(); } clone() { return new Plane(this.origin, this.xDir, this.zDir); } get origin() { return this._origin; } set origin(newOrigin) { this._origin = newOrigin; this._calcTransforms(); } translateTo(point) { const newPlane = this.clone(); newPlane.origin = new Vector(point); return newPlane; } translate(xDistOrVector, yDist = 0, zDist = 0) { const translation = new Vector( typeof xDistOrVector === "number" ? [xDistOrVector, yDist, zDist] : xDistOrVector ); return this.translateTo(this.origin.add(translation)); } translateX(xDist) { return this.translate(xDist, 0, 0); } translateY(yDist) { return this.translate(0, yDist, 0); } translateZ(zDist) { return this.translate(0, 0, zDist); } pivot(angle, direction = [1, 0, 0]) { const dir = makeDirection(direction); const zDir = new Vector(this.zDir).rotate(angle, [0, 0, 0], dir); const xDir = new Vector(this.xDir).rotate(angle, [0, 0, 0], dir); return new Plane(this.origin, xDir, zDir); } rotate2DAxes(angle) { const xDir = new Vector(this.xDir).rotate(angle, [0, 0, 0], this.zDir); return new Plane(this.origin, xDir, this.zDir); } _calcTransforms() { const globalCoordSystem = new this.oc.gp_Ax3_1(); const localCoordSystem = makeAx3(this.origin, this.zDir, this.xDir); const forwardT = new this.oc.gp_Trsf_1(); forwardT.SetTransformation_1(globalCoordSystem, localCoordSystem); this.globalToLocal = new Transformation(); this.globalToLocal.coordSystemChange("reference", { origin: this.origin, zDir: this.zDir, xDir: this.xDir }); this.localToGlobal = new Transformation(); this.localToGlobal.coordSystemChange( { origin: this.origin, zDir: this.zDir, xDir: this.xDir }, "reference" ); } setOrigin2d(x, y) { this.origin = this.toWorldCoords([x, y]); } toLocalCoords(vec2) { const pnt2 = this.globalToLocal.transformPoint(vec2); const newVec = new Vector(pnt2); pnt2.delete(); return newVec; } toWorldCoords(v) { const pnt2 = this.localToGlobal.transformPoint(v); const newVec = new Vector(pnt2); pnt2.delete(); return newVec; } } const PLANES_CONFIG = { XY: { xDir: [1, 0, 0], normal: [0, 0, 1] }, YZ: { xDir: [0, 1, 0], normal: [1, 0, 0] }, ZX: { xDir: [0, 0, 1], normal: [0, 1, 0] }, XZ: { xDir: [1, 0, 0], normal: [0, -1, 0] }, YX: { xDir: [0, 1, 0], normal: [0, 0, -1] }, ZY: { xDir: [0, 0, 1], normal: [-1, 0, 0] }, front: { xDir: [1, 0, 0], normal: [0, 0, 1] }, back: { xDir: [-1, 0, 0], normal: [0, 0, -1] }, left: { xDir: [0, 0, 1], normal: [-1, 0, 0] }, right: { xDir: [0, 0, -1], normal: [1, 0, 0] }, top: { xDir: [1, 0, 0], normal: [0, 1, 0] }, bottom: { xDir: [1, 0, 0], normal: [0, -1, 0] } }; const createNamedPlane = (plane, sourceOrigin = [0, 0, 0]) => { const config = PLANES_CONFIG[plane]; if (!config) throw new Error(`Could not find plane ${plane}`); let origin; if (typeof sourceOrigin === "number") { origin = config.normal.map((v) => v * sourceOrigin); } else { origin = sourceOrigin; } return new Plane(origin, config.xDir, config.normal); }; let BoundingBox$1 = class BoundingBox extends WrappingObj { constructor(wrapped) { const oc = getOC(); let boundBox = wrapped; if (!boundBox) { boundBox = new oc.Bnd_Box_1(); } super(boundBox); } get repr() { const [min, max] = this.bounds; return `${new Vector(min).repr} - ${new Vector(max).repr}`; } get bounds() { const xMin = { current: 0 }; const yMin = { current: 0 }; const zMin = { current: 0 }; const xMax = { current: 0 }; const yMax = { current: 0 }; const zMax = { current: 0 }; this.wrapped.Get(xMin, yMin, zMin, xMax, yMax, zMax); return [ [xMin.current, yMin.current, zMin.current], [xMax.current, yMax.current, zMax.current] ]; } get center() { const [[xmin, ymin, zmin], [xmax, ymax, zmax]] = this.bounds; return [ xmin + (xmax - xmin) / 2, ymin + (ymax - ymin) / 2, zmin + (zmax - zmin) / 2 ]; } get width() { const [[xmin], [xmax]] = this.bounds; return Math.abs(xmax - xmin); } get height() { const [[, ymin], [, ymax]] = this.bounds; return Math.abs(ymax - ymin); } get depth() { const [[, , zmin], [, , zmax]] = this.bounds; return Math.abs(zmax - zmin); } add(other) { this.wrapped.Add_1(other.wrapped); } isOut(other) { return this.wrapped.IsOut_4(other.wrapped); } }; const makePlaneFromFace = (face, originOnSurface = [0, 0]) => { const originPoint = face.pointOnSurface(...originOnSurface); const normal = face.normalAt(originPoint); const v = new Vector([0, 0, 1]); let xd = v.cross(normal); if (xd.Length < 1e-8) { xd.delete(); xd = new Vector([1, 0, 0]); } v.delete(); return new Plane(originPoint, xd, normal); }; function makePlane(plane = "XY", origin = [0, 0, 0]) { if (plane instanceof Plane) { return plane.clone(); } else { return createNamedPlane(plane, origin); } } function rotate(shape, angle, position = [0, 0, 0], direction = [0, 0, 1]) { const transformation = new Transformation(); transformation.rotate(angle, position, direction); const newShape = transformation.transform(shape); transformation.delete(); return newShape; } function translate(shape, vector) { const transformation = new Transformation(); transformation.translate(vector); const newShape = transformation.transform(shape); transformation.delete(); return newShape; } function mirror(shape, inputPlane, origin) { const transformation = new Transformation(); transformation.mirror(inputPlane, origin); const newShape = transformation.transform(shape); transformation.delete(); return newShape; } function scale(shape, center, scale2) { const transformation = new Transformation(); transformation.scale(center, scale2); const newShape = transformation.transform(shape); transformation.delete(); return newShape; } function isPoint2D(point) { return Array.isArray(point) && point.length === 2; } let CURVE_TYPES_MAP = null; const getCurveTypesMap = (refresh) => { if (CURVE_TYPES_MAP && !refresh) return CURVE_TYPES_MAP; const oc = getOC(); const ga = oc.GeomAbs_CurveType; CURVE_TYPES_MAP = /* @__PURE__ */ new Map([ [ga.GeomAbs_Line, "LINE"], [ga.GeomAbs_Circle, "CIRCLE"], [ga.GeomAbs_Ellipse, "ELLIPSE"], [ga.GeomAbs_Hyperbola, "HYPERBOLA"], [ga.GeomAbs_Parabola, "PARABOLA"], [ga.GeomAbs_BezierCurve, "BEZIER_CURVE"], [ga.GeomAbs_BSplineCurve, "BSPLINE_CURVE"], [ga.GeomAbs_OffsetCurve, "OFFSET_CURVE"], [ga.GeomAbs_OtherCurve, "OTHER_CURVE"] ]); return CURVE_TYPES_MAP; }; const findCurveType = (type) => { let shapeType2 = getCurveTypesMap().get(type); if (!shapeType2) shapeType2 = getCurveTypesMap(true).get(type); if (!shapeType2) throw new Error("unknown type"); return shapeType2; }; function precisionRound(number, precision) { const factor = Math.pow(10, precision); const n = precision < 0 ? number : 0.01 / factor + number; return Math.round(n * factor) / factor; } function range(len) { return Array.from(Array(len).keys()); } function zip(arrays) { const minLength = Math.min(...arrays.map((arr) => arr.length)); return range(minLength).map((i) => arrays.map((arr) => arr[i])); } function round2(v) { return Math.round(v * 100) / 100; } const reprPnt = ([x, y]) => { return `(${round2(x)},${round2(y)})`; }; const asFixed = (p, precision = 1e-9) => { let num = p; if (Math.abs(p) < precision) num = 0; return num.toFixed(-Math.log10(precision)); }; const removeDuplicatePoints = (points, precision = 1e-9) => { return Array.from( new Map( points.map(([p0, p1]) => [ `[${asFixed(p0, precision)},${asFixed(p1, precision)}]`, [p0, p1] ]) ).values() ); }; const pnt = ([x, y]) => { const oc = getOC(); return new oc.gp_Pnt2d_3(x, y); }; const direction2d = ([x, y]) => { const oc = getOC(); return new oc.gp_Dir2d_4(x, y); }; const vec = ([x, y]) => { const oc = getOC(); return new oc.gp_Vec2d_4(x, y); }; const axis2d = (point, direction) => { const oc = getOC(); const [r, gc] = localGC(); const axis = new oc.gp_Ax2d_2(r(pnt(point)), r(direction2d(direction))); gc(); return axis; }; class BoundingBox2d extends WrappingObj { constructor(wrapped) { const oc = getOC(); let boundBox = wrapped; if (!boundBox) { boundBox = new oc.Bnd_Box2d(); } super(boundBox); } get repr() { const [min, max] = this.bounds; return `${reprPnt(min)} - ${reprPnt(max)}`; } get bounds() { const xMin = { current: 0 }; const yMin = { current: 0 }; const xMax = { current: 0 }; const yMax = { current: 0 }; this.wrapped.Get(xMin, yMin, xMax, yMax); return [ [xMin.current, yMin.current], [xMax.current, yMax.current] ]; } get center() { const [[xmin, ymin], [xmax, ymax]] = this.bounds; return [xmin + (xmax - xmin) / 2, ymin + (ymax - ymin) / 2]; } get width() { const [[xmin], [xmax]] = this.bounds; return Math.abs(xmax - xmin); } get height() { const [[, ymin], [, ymax]] = this.bounds; return Math.abs(ymax - ymin); } outsidePoint(paddingPercent = 1) { const [min, max] = this.bounds; const width = max[0] - min[0]; const height = max[1] - min[1]; return [ max[0] + width / 100 * paddingPercent, max[1] + height / 100 * paddingPercent * 0.9 ]; } add(other) { this.wrapped.Add_1(other.wrapped); } isOut(other) { return this.wrapped.IsOut_4(other.wrapped); } containsPoint(other) { const r = GCWithScope(); const point = r(pnt(other)); return !this.wrapped.IsOut_1(point); } } const samePoint$2 = ([x0, y0], [x1, y1], precision = 1e-6) => { return Math.abs(x0 - x1) <= precision && Math.abs(y0 - y1) <= precision; }; const add2d = ([x0, y0], [x1, y1]) => { return [x0 + x1, y0 + y1]; }; const subtract2d = ([x0, y0], [x1, y1]) => { return [x0 - x1, y0 - y1]; }; const scalarMultiply2d = ([x0, y0], scalar) => { return [x0 * scalar, y0 * scalar]; }; const distance2d = ([x0, y0], [x1, y1] = [0, 0]) => { return Math.sqrt((x0 - x1) ** 2 + (y0 - y1) ** 2); }; const squareDistance2d = ([x0, y0], [x1, y1] = [0, 0]) => { return (x0 - x1) ** 2 + (y0 - y1) ** 2; }; function crossProduct2d([x0, y0], [x1, y1]) { return x0 * y1 - y0 * x1; } const angle2d = ([x0, y0], [x1, y1] = [0, 0]) => { return Math.atan2(y1 * x0 - y0 * x1, x0 * x1 + y0 * y1); }; const polarAngle2d = ([x0, y0], [x1, y1] = [0, 0]) => { return Math.atan2(y1 - y0, x1 - x0); }; const normalize2d = ([x0, y0]) => { const l = distance2d([x0, y0]); return [x0 / l, y0 / l]; }; const rotate2d = (point, angle, center = [0, 0]) => { const [px0, py0] = point; const [cx, cy] = center; const px = px0 - cx; const py = py0 - cy; const sinA = Math.sin(angle); const cosA = Math.cos(angle); const xnew = px * cosA - py * sinA; const ynew = px * sinA + py * cosA; return [xnew + cx, ynew + cy]; }; const polarToCartesian = (r, theta) => { const x = Math.cos(theta) * r; const y = Math.sin(theta) * r; return [x, y]; }; const cartesianToPolar = ([x, y]) => { const r = distance2d([x, y]); const theta = Math.atan2(y, x); return [r, theta]; }; const determinant2x2 = (matrix) => { return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]; }; class Curve2D extends WrappingObj { constructor(handle) { const oc = getOC(); const inner = handle.get(); super(new oc.Handle_Geom2d_Curve_2(inner)); this._boundingBox = null; } get boundingBox() { if (this._boundingBox) return this._boundingBox; const oc = getOC(); const boundBox = new oc.Bnd_Box2d(); oc.BndLib_Add2dCurve.Add_3(this.wrapped, 1e-6, boundBox); this._boundingBox = new BoundingBox2d(boundBox); return this._boundingBox; } get repr() { return `${this.geomType} ${reprPnt(this.firstPoint)} - ${reprPnt( this.lastPoint )}`; } get innerCurve() { return this.wrapped.get(); } value(parameter) { const pnt2 = this.innerCurve.Value(parameter); const vec2 = [pnt2.X(), pnt2.Y()]; pnt2.delete(); return vec2; } get firstPoint() { return this.value(this.firstParameter); } get lastPoint() { return this.value(this.lastParameter); } get firstParameter() { return this.innerCurve.FirstParameter(); } get lastParameter() { return this.innerCurve.LastParameter(); } adaptor() { const oc = getOC(); return new oc.Geom2dAdaptor_Curve_2(this.wrapped); } get geomType() { const adaptor = this.adaptor(); const curveType = findCurveType(adaptor.GetType()); adaptor.delete(); return curveType; } clone() { return new Curve2D(this.innerCurve.Copy()); } reverse() { this.innerCurve.Reverse(); } distanceFromPoint(point) { const oc = getOC(); const r = GCWithScope(); const projector = r( new oc.Geom2dAPI_ProjectPointOnCurve_2(r(pnt(point)), this.wrapped) ); let curveToPoint = Infinity; try { curveToPoint = projector.LowerDistance(); } catch (e) { curveToPoint = Infinity; } return Math.min( curveToPoint, distance2d(point, this.firstPoint), distance2d(point, this.lastPoint) ); } distanceFromCurve(curve) { const oc = getOC(); const r = GCWithScope(); let curveDistance = Infinity; const projector = r( new oc.Geom2dAPI_ExtremaCurveCurve( this.wrapped, curve.wrapped, this.firstParameter, this.lastParameter, curve.firstParameter, curve.lastParameter ) ); try { curveDistance = projector.LowerDistance(); } catch (e) { curveDistance = Infinity; } return Math.min( curveDistance, this.distanceFromPoint(curve.firstPoint), this.distanceFromPoint(curve.lastPoint), curve.distanceFromPoint(this.firstPoint), curve.distanceFromPoint(this.lastPoint) ); } distanceFrom(element) { if (isPoint2D(element)) { return this.distanceFromPoint(element); } return this.distanceFromCurve(element); } isOnCurve(point) { return this.distanceFromPoint(point) < 1e-9; } parameter(point, precision = 1e-9) { const oc = getOC(); const r = GCWithScope(); let lowerDistance; let lowerDistanceParameter; try { const projector = r( new oc.Geom2dAPI_ProjectPointOnCurve_2(r(pnt(point)), this.wrapped) ); lowerDistance = projector.LowerDistance(); lowerDistanceParameter = projector.LowerDistanceParameter(); } catch (e) { if (samePoint$2(point, this.firstPoint, precision)) return this.firstParameter; if (samePoint$2(point, this.lastPoint, precision)) return this.lastParameter; throw new Error("Failed to find parameter"); } if (lowerDistance > precision) { throw new Error( `Point ${reprPnt(point)} not on curve ${this.repr}, ${lowerDistance.toFixed(9)}` ); } return lowerDistanceParameter; } tangentAt(index) { const oc = getOC(); const [r, gc] = localGC(); let param; if (Array.isArray(index)) { param = this.parameter(index); } else { const paramLength = this.innerCurve.LastParameter() - this.innerCurve.FirstParameter(); param = paramLength * index + this.innerCurve.FirstParameter(); } const point = r(new oc.gp_Pnt2d_1()); const dir = r(new oc.gp_Vec2d_1()); this.innerCurve.D1(param, point, dir); const tgtVec = [dir.X(), dir.Y()]; gc(); return tgtVec; } splitAt(points, precision = 1e-9) { const oc = getOC(); const r = GCWithScope(); let parameters = points.map((point) => { if (isPoint2D(point)) return this.parameter(point, precision); return point; }); parameters = Array.from( new Map( parameters.map((p) => [precisionRound(p, -Math.log10(precision)), p]) ).values() ).sort((a, b) => a - b); const firstParam = this.firstParameter; const lastParam = this.lastParameter; if (firstParam > lastParam) { parameters.reverse(); } if (Math.abs(parameters[0] - firstParam) < precision * 100) parameters = parameters.slice(1); if (!parameters.length) return [this]; if (Math.abs(parameters[parameters.length - 1] - lastParam) < precision * 100) parameters = parameters.slice(0, -1); if (!parameters.length) return [this]; return zip([ [firstParam, ...parameters], [...parameters, lastParam] ]).map(([first, last]) => { try { if (this.geomType === "BEZIER_CURVE") { const curveCopy = new oc.Geom2d_BezierCurve_1( r(this.adaptor()).Bezier().get().Poles_2() ); curveCopy.Segment(first, last); return new Curve2D(new oc.Handle_Geom2d_Curve_2(curveCopy)); } if (this.geomType === "BSPLINE_CURVE") { const adapted = r(this.adaptor()).BSpline().get(); const curveCopy = new oc.Geom2d_BSplineCurve_1( adapted.Poles_2(), adapted.Knots_2(), adapted.Multiplicities_2(), adapted.Degree(), adapted.IsPeriodic() ); curveCopy.Segment(first, last, precision); return new Curve2D(new oc.Handle_Geom2d_Curve_2(curveCopy)); } const trimmed = new oc.Geom2d_TrimmedCurve( this.wrapped, first, last, true, true ); return new Curve2D(new oc.Handle_Geom2d_Curve_2(trimmed)); } catch (e) { throw new Error("Failed to split the curve"); } }); } } const approximateAsBSpline = (adaptor, tolerance = 1e-4, continuity = "C0", maxSegments = 200) => { const oc = getOC(); const r = GCWithScope(); const continuities = { C0: oc.GeomAbs_Shape.GeomAbs_C0, C1: oc.GeomAbs_Shape.GeomAbs_C1, C2: oc.GeomAbs_Shape.GeomAbs_C2, C3: oc.GeomAbs_Shape.GeomAbs_C3 }; const convert = r( new oc.Geom2dConvert_ApproxCurve_2( adaptor.ShallowCopy(), tolerance, continuities[continuity], maxSegments, 3 ) ); return new Curve2D(convert.Curve()); }; const BSplineToBezier = (adaptor) => { if (findCurveType(adaptor.GetType()) !== "BSPLINE_CURVE") throw new Error("You can only convert a Bspline"); const handle = adaptor.BSpline(); const oc = getOC(); const convert = new oc.Geom2dConvert_BSplineCurveToBezierCurve_1(handle); function* bezierCurves() { const nArcs = convert.NbArcs(); if (!nArcs) return; for (let i = 1; i <= nArcs; i++) { const arc = convert.Arc(i); yield new Curve2D(arc); } } const curves = Array.from(bezierCurves()); convert.delete(); return curves; }; function approximateAsSvgCompatibleCurve(curves, options = { tolerance: 1e-4, continuity: "C0", maxSegments: 300 }) { const r = GCWithScope(); return curves.flatMap((curve) => { const adaptor = r(curve.adaptor()); const curveType = findCurveType(adaptor.GetType()); if (curveType === "ELLIPSE" || curveType === "CIRCLE" && samePoint$2(curve.firstPoint, curve.lastPoint)) { return curve.splitAt([0.5]); } if (["LINE", "ELLIPSE", "CIRCLE"].includes(curveType)) { return curve; } if (curveType === "BEZIER_CURVE") { const b = adaptor.Bezier().get(); const deg = b.Degree(); if ([1, 2, 3].includes(deg)) { return curve; } } if (curveType === "BSPLINE_CURVE") { const c = BSplineToBezier(adaptor); return approximateAsSvgCompatibleCurve(c, options); } const bspline = approximateAsBSpline( adaptor, options.tolerance, options.continuity, options.maxSegments ); return approximateAsSvgCompatibleCurve( BSplineToBezier(r(bspline.adaptor())), options ); }); } function* pointsIteration(intersector) { const nPoints = intersector.NbPoints(); if (!nPoints) return; for (let i = 1; i <= nPoints; i++) { const point = intersector.Point(i); yield [point.X(), point.Y()]; } } function* commonSegmentsIteration(intersector) { const nSegments = intersector.NbSegments(); if (!nSegments) return; const oc = getOC(); for (let i = 1; i <= nSegments; i++) { const h1 = new oc.Handle_Geom2d_Curve_1(); const h2 = new oc.Handle_Geom2d_Curve_1(); try { intersector.Segment(i, h1, h2); } catch (e) { continue; } yield new Curve2D(h1); h2.delete(); } } const intersectCurves = (first, second, precision = 1e-9) => { if (first.boundingBox.isOut(second.boundingBox)) return { intersections: [], commonSegments: [], commonSegmentsPoints: [] }; const oc = getOC(); const intersector = new oc.Geom2dAPI_InterCurveCurve_1(); let intersections; let commonSegments; try { intersector.Init_1(first.wrapped, second.wrapped, precision); intersections = Array.from(pointsIteration(intersector)); commonSegments = Array.from(commonSegmentsIteration(intersector)); } catch (e) { console.error(first, second, e); throw new Error("Intersections failed between curves"); } finally { intersector.delete(); } const segmentsAsPoints = commonSegments.filter((c) => samePoint$2(c.firstPoint, c.lastPoint, precision)).map((c) => c.firstPoint); if (segmentsAsPoints.length) { intersections.push(...segmentsAsPoints); commonSegments = commonSegments.filter( (c) => !samePoint$2(c.firstPoint, c.lastPoint, precision) ); } const commonSegmentsPoints = commonSegments.flatMap((c) => [ c.firstPoint, c.lastPoint ]); return { intersections, commonSegments, commonSegmentsPoints }; }; const selfIntersections = (curve, precision = 1e-9) => { const oc = getOC(); const intersector = new oc.Geom2dAPI_InterCurveCurve_1(); let intersections; try { intersector.Init_1(curve.wrapped, curve.wrapped, precision); intersections = Array.from(pointsIteration(intersector)); } catch (e) { throw new Error("Self intersection failed"); } finally { intersector.delete(); } return intersections; }; const make2dSegmentCurve = (startPoint, endPoint) => { const oc = getOC(); const [r, gc] = localGC(); const segment = r( new oc.GCE2d_MakeSegment_1(r(pnt(startPoint)), r(pnt(endPoint))) ).Value(); const curve = new Curve2D(segment); if (!samePoint$2(curve.firstPoint, startPoint)) { curve.reverse(); } gc(); return curve; }; const make2dThreePointArc = (startPoint, midPoint, endPoint) => { const oc = getOC(); const [r, gc] = localGC(); const segment = r( new oc.GCE2d_MakeArcOfCircle_4( r(pnt(startPoint)), r(pnt(midPoint)), r(pnt(endPoint)) ) ).Value(); gc(); const curve = new Curve2D(segment); if (!samePoint$2(curve.firstPoint, startPoint)) { curve.wrapped.get().SetTrim( curve.lastParameter, curve.firstParameter, true, true ); } return curve; }; const make2dTangentArc = (startPoint, tangent, endPoint) => { const oc = getOC(); const [r, gc] = localGC(); const segment = r( new oc.GCE2d_MakeArcOfCircle_5( r(pnt(startPoint)), r(vec(tangent)), r(pnt(endPoint)) ) ).Value(); gc(); const curve = new Curve2D(segment); if (!samePoint$2(curve.firstPoint, startPoint)) { curve.wrapped.get().SetTrim( curve.lastParameter, curve.firstParameter, true, true ); } return curve; }; const make2dCircle = (radius, center = [0, 0]) => { const oc = getOC(); const [r, gc] = localGC(); const segment = r( new oc.GCE2d_MakeCircle_7(r(pnt(center)), radius, true) ).Value(); gc(); return new Curve2D(segment); }; const make2dEllipse = (majorRadius, minorRadius, xDir = [1, 0], center = [0, 0], direct = true) => { const oc = getOC(); const [r, gc] = localGC(); const ellipse = r( new oc.gp_Elips2d_2( r(axis2d(center, xDir)), majorRadius, minorRadius, direct ) ); const segment = r(new oc.GCE2d_MakeEllipse_1(ellipse)).Value(); gc(); return new Curve2D(segment); }; const make2dEllipseArc = (majorRadius, minorRadius, startAngle, endAngle, center = [0, 0], xDir, direct = true) => { const oc = getOC(); const [r, gc] = localGC(); const ellipse = r( new oc.gp_Elips2d_2(r(axis2d(center, xDir)), majorRadius, minorRadius, true) ); const segment = r( new oc.GCE2d_MakeArcOfEllipse_1(ellipse, startAngle, endAngle, direct) ).Value(); gc(); return new Curve2D(segment); }; const make2dBezierCurve = (startPoint, controls, endPoint) => { const oc = getOC(); const [r, gc] = localGC(); const arrayOfPoints = r( new oc.TColgp_Array1OfPnt2d_2(1, controls.length + 2) ); arrayOfPoints.SetValue(1, r(pnt(startPoint))); controls.forEach((p, i) => { arrayOfPoints.SetValue(i + 2, r(pnt(p))); }); arrayOfPoints.SetValue(controls.length + 2, r(pnt(endPoint))); const bezCurve = new oc.Geom2d_BezierCurve_1(arrayOfPoints); gc(); return new Curve2D(new oc.Handle_Geom2d_Curve_2(bezCurve)); }; function make2dInerpolatedBSplineCurve(points, { tolerance = 1e-3, smoothing = null, degMax = 3, degMin = 1 } = {}) { const r = GCWithScope(); const oc = getOC(); const pnts = r(new oc.TColgp_Array1OfPnt2d_2(1, points.length)); points.forEach((point, index) => { pnts.SetValue(index + 1, r(pnt(point))); }); let splineBuilder; if (smoothing) { splineBuilder = r( new oc.Geom2dAPI_PointsToBSpline_6( pnts, smoothing[0], smoothing[1], smoothing[2], degMax, oc.GeomAbs_Shape.GeomAbs_C2, tolerance ) ); } else { splineBuilder = r( new oc.Geom2dAPI_PointsToBSpline_2( pnts, degMin, degMax, oc.GeomAbs_Shape.GeomAbs_C2, tolerance ) ); } if (!splineBuilder.IsDone()) { throw new Error("B-spline approximation failed"); } return new Curve2D(splineBuilder.Curve()); } const make2dArcFromCenter = (startPoint, endPoint, center) => { const midChord = scalarMultiply2d(add2d(startPoint, endPoint), 0.5); const orientedRadius = distance2d(center, startPoint); const midChordDir = normalize2d(subtract2d(midChord, center)); return make2dThreePointArc( startPoint, add2d(scalarMultiply2d(midChordDir, orientedRadius), center), endPoint ); }; const offsetEndPoints = (firstPoint, lastPoint, offset2) => { const tangent = normalize2d(subtract2d(lastPoint, firstPoint)); const normal = [tangent[1], -tangent[0]]; const offsetVec = [normal[0] * offset2, normal[1] * offset2]; return { firstPoint: add2d(firstPoint, offsetVec), lastPoint: add2d(lastPoint, offsetVec) }; }; const make2dOffset = (curve, offset2) => { const r = GCWithScope(); const curveType = curve.geomType; if (curveType === "CIRCLE") { const circle = r(r(curve.adaptor()).Circle()); const radius = circle.Radius(); const orientationCorrection = circle.IsDirect() ? 1 : -1; const orientedOffset = offset2 * orientationCorrection; const newRadius = radius + orientedOffset; if (newRadius < 1e-10) { const centerPos = r(circle.Location()); const center = [centerPos.X(), centerPos.Y()]; const offsetViaCenter = (point) => { const [x, y] = normalize2d(subtract2d(point, center)); return add2d(point, [orientedOffset * x, orientedOffset * y]); }; return { collapsed: true, firstPoint: offsetViaCenter(curve.firstPoint), lastPoint: offsetViaCenter(curve.lastPoint) }; } const oc2 = getOC(); const newCircle = new oc2.gp_Circ2d_3(circle.Axis(), newRadius); const newInnerCurve = new oc2.Geom2d_Circle_1(newCircle); const newCurve = new oc2.Geom2d_TrimmedCurve( new oc2.Handle_Geom2d_Curve_2(newInnerCurve), curve.firstParameter, curve.lastParameter, true, true ); return new Curve2D(new oc2.Handle_Geom2d_Curve_2(newCurve)); } if (curveType === "LINE") { const { firstPoint, lastPoint } = offsetEndPoints( curve.firstPoint, curve.lastPoint, offset2 ); return make2dSegmentCurve(firstPoint, lastPoint); } const oc = getOC(); const offsetCurve = new Curve2D( new oc.Handle_Geom2d_Curve_2( new oc.Geom2d_OffsetCurve(curve.wrapped, offset2, true) ) ); const approximation = approximateAsBSpline(offsetCurve.adaptor()); const selfIntersects = selfIntersections(approximation); if (selfIntersects.length) { return { collapsed: true, firstPoint: approximation.firstPoint, lastPoint: approximation.lastPoint }; } return approximation; }; function removeCorner(firstCurve, secondCurve, radius) { const sinAngle = crossProduct2d( firstCurve.tangentAt(1), secondCurve.tangentAt(0) ); if (Math.abs(sinAngle) < 1e-10) return null; const orientationCorrection = sinAngle > 0 ? -1 : 1; const offset2 = Math.abs(radius) * orientationCorrection; const firstOffset = make2dOffset(firstCurve, offset2); const secondOffset = make2dOffset(secondCurve, offset2); if (!(firstOffset instanceof Curve2D) || !(secondOffset instanceof Curve2D)) { return null; } let potentialCenter; try { const { intersections } = intersectCurves(firstOffset, secondOffset, 1e-9); potentialCenter = intersections.at(-1); } catch (e) { return null; } if (!isPoint2D(potentialCenter)) { return null; } const center = potentialCenter; const splitForFillet = (curve, offsetCurve) => { const [x, y] = offsetCurve.tangentAt(center); const normal = normalize2d([-y, x]); const splitPoint = add2d(center, scalarMultiply2d(normal, offset2)); const splitParam = curve.parameter(splitPoint, 1e-6); return curve.splitAt([splitParam]); }; const [first] = splitForFillet(firstCurve, firstOffset); const [, second] = splitForFillet(secondCurve, secondOffset); return { first, second, center }; } function filletCurves(firstCurve, secondCurve, radius) { const cornerRemoved = removeCorner(firstCurve, secondCurve, radius); if (!cornerRemoved) { console.warn( "Cannot fillet between curves", firstCurve.repr, secondCurve.repr ); return [firstCurve, secondCurve]; } const { first, second, center } = cornerRemoved; return [ first, make2dArcFromCenter(first.lastPoint, second.firstPoint, center), second ]; } function chamferCurves(firstCurve, secondCurve, radius) { const cornerRemoved = removeCorner(firstCurve, secondCurve, radius); if (!cornerRemoved) { console.warn( "Cannot chamfer between curves", firstCurve.repr, secondCurve.repr ); return [firstCurve, secondCurve]; } const { first, second } = cornerRemoved; return [ first, make2dSegmentCurve(first.lastPoint, second.firstPoint), second ]; } class FlatQueue { constructor() { this.ids = []; this.values = []; this.length = 0; } clear() { this.length = 0; } push(id, value) { let pos = this.length++; while (pos > 0) { const parent = pos - 1 >> 1; const parentValue = this.values[parent]; if (value >= parentValue) break; this.ids[pos] = this.ids[parent]; this.values[pos] = parentValue; pos = parent; } this.ids[pos] = id; this.values[pos] = value; } pop() { if (this.length === 0) return void 0; const top = this.ids[0]; this.length--; if (this.length > 0) { const id = this.ids[0] = this.ids[this.length]; const value = this.values[0] = this.values[this.length]; const halfLength = this.length >> 1; let pos = 0; while (pos < halfLength) { let left = (pos << 1) + 1; const right = left + 1; let bestIndex = this.ids[left]; let bestValue = this.values[left]; const rightValue = this.values[right]; if (right < this.length && rightValue < bestValue) { left = right; bestIndex = this.ids[right]; bestValue = rightValue; } if (bestValue >= value) break; this.ids[pos] = bestIndex; this.values[pos] = bestValue; pos = left; } this.ids[pos] = id; this.values[pos] = value; } return top; } peek() { if (this.length === 0) return void 0; return this.ids[0]; } peekValue() { if (this.length === 0) return void 0; return this.values[0]; } shrink() { this.ids.length = this.values.length = this.length; } } const ARRAY_TYPES = [ Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array ]; const VERSION = 3; class Flatbush { static from(data) { if (!(data instanceof ArrayBuffer)) { throw new Error("Data must be an instance of ArrayBuffer."); } const [magic, versionAndType] = new Uint8Array(data, 0, 2); if (magic !== 251) { throw new Error("Data does not appear to be in a Flatbush format."); } if (versionAndType >> 4 !== VERSION) { throw new Error(`Got v${versionAndType >> 4} data when expected v${VERSION}.`); } const [nodeSize] = new Uint16Array(data, 2, 1); const [numItems] = new Uint32Array(data, 4, 1); return new Flatbush(numItems, nodeSize, ARRAY_TYPES[versionAndType & 15], data); } constructor(numItems, nodeSize = 16, ArrayType = Float64Array, data) { if (numItems === void 0) throw new Error("Missing required argument: numItems."); if (isNaN(numItems) || numItems <= 0) throw new Error(`Unpexpected numItems value: ${numItems}.`); this.numItems = +numItems; this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535); let n = numItems; let numNodes = n; this._levelBounds = [n * 4]; do { n = Math.ceil(n / this.nodeSize); numNodes += n; this._levelBounds.push(numNodes * 4); } while (n !== 1); this.ArrayType = ArrayType || Float64Array; this.IndexArrayType = numNodes < 16384 ? Uint16Array : Uint32Array; const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType); const nodesByteSize = numNodes * 4 * this.ArrayType.BYTES_PER_ELEMENT; if (arrayTypeIndex < 0) { throw new Error(`Unexpected typed array class: ${ArrayType}.`); } if (data && data instanceof ArrayBuffer) { this.data = data; this._boxes = new this.ArrayType(this.data, 8, numNodes * 4); this._indices = new this.IndexArrayType(this.data, 8 + nodesByteSize, numNodes); this._pos = numNodes * 4; this.minX = this._boxes[this._pos - 4]; this.minY = this._boxes[this._pos - 3]; this.maxX = this._boxes[this._pos - 2]; this.maxY = this._boxes[this._pos - 1]; } else { this.data = new ArrayBuffer(8 + nodesByteSize + numNodes * this.IndexArrayType.BYTES_PER_ELEMENT); this._boxes = new this.ArrayType(this.data, 8, numNodes * 4); this._indices = new this.IndexArrayType(this.data, 8 + nodesByteSize, numNodes); this._pos = 0; this.minX = Infinity; this.minY = Infinity; this.maxX = -Infinity; this.maxY = -Infinity; new Uint8Array(this.data, 0, 2).set([251, (VERSION << 4) + arrayTypeIndex]); new Uint16Array(this.data, 2, 1)[0] = nodeSize; new Uint32Array(this.data, 4, 1)[0] = numItems; } this._queue = new FlatQueue(); } add(minX, minY, maxX, maxY) { const index = this._pos >> 2; this._indices[index] = index; this._boxes[this._pos++] = minX; this._boxes[this._pos++] = minY; this._boxes[this._pos++] = maxX; this._boxes[this._pos++] = maxY; if (minX < this.minX) this.minX = minX; if (minY < this.minY) this.minY = minY; if (maxX > this.maxX) this.maxX = maxX; if (maxY > this.maxY) this.maxY = maxY; return index; } finish() { if (this._pos >> 2 !== this.numItems) { throw new Error(`Added ${this._pos >> 2} items when expected ${this.numItems}.`); } if (this.numItems <= this.nodeSize) { this._boxes[this._pos++] = this.minX; this._boxes[this._pos++] = this.minY; this._boxes[this._pos++] = this.maxX; this._boxes[this._pos++] = this.maxY; return; } const width = this.maxX - this.minX || 1; const height = this.maxY - this.minY || 1; const hilbertValues = new Uint32Array(this.numItems); const hilbertMax = (1 << 16) - 1; for (let i = 0; i < this.numItems; i++) { let pos = 4 * i; const minX = this._boxes[pos++]; const minY = this._boxes[pos++]; const maxX = this._boxes[pos++]; const maxY = this._boxes[pos++]; const x = Math.floor(hilbertMax * ((minX + maxX) / 2 - this.minX) / width); const y = Math.floor(hilbertMax * ((minY + maxY) / 2 - this.mi