diff --git a/modules/math/equality.ts b/modules/math/equality.ts index 2e9c87c63..31de65460 100644 --- a/modules/math/equality.ts +++ b/modules/math/equality.ts @@ -1,4 +1,9 @@ -import {distanceSquared3, distanceSquaredAB3, distanceSquaredANegB3} from "math/distance"; +import { + distanceAB, + distanceSquared3, + distanceSquaredAB3, + distanceSquaredANegB3 +} from "math/distance"; export const TOLERANCE = 1E-6; export const TOLERANCE_SQ = TOLERANCE * TOLERANCE; @@ -7,6 +12,10 @@ export function areEqual(v1, v2, tolerance) { return Math.abs(v1 - v2) < tolerance; } +export function arePointsEqual(v1, v2, toleranceSQ) { + return areEqual(distanceAB(v1, v2), 0, toleranceSQ); +} + export function areVectorsEqual(v1, v2, toleranceSQ) { return areEqual(distanceSquaredAB3(v1, v2), 0, toleranceSQ); } diff --git a/modules/math/optim/dogleg.js b/modules/math/optim/dogleg.js index 2f74c0f9a..583641714 100644 --- a/modules/math/optim/dogleg.js +++ b/modules/math/optim/dogleg.js @@ -413,4 +413,4 @@ var cg = function(A, x, b, tol, maxIt) { var dogleg = {DEBUG_HANDLER : function() {}}; //backward compatibility -export {dog_leg, dogleg} \ No newline at end of file +export {dog_leg, dogleg, lu_solve} \ No newline at end of file diff --git a/modules/scene/sceneSetup.ts b/modules/scene/sceneSetup.ts index 63af60b69..d2ca26ab3 100644 --- a/modules/scene/sceneSetup.ts +++ b/modules/scene/sceneSetup.ts @@ -86,7 +86,7 @@ export default class SceneSetUp { this.createOrthographicCamera(); this.createPerspectiveCamera(); - this.camera = this.oCamera; + this.camera = this.pCamera; this.light = new DirectionalLight( 0xffffff ); this.light.position.set( 10, 10, 10 ); diff --git a/modules/voxels/octree.js b/modules/voxels/octree.js index 3d721c526..7712a81e7 100644 --- a/modules/voxels/octree.js +++ b/modules/voxels/octree.js @@ -5,6 +5,7 @@ export class Node { constructor() { this.nodes = null; this.tag = 0; + this.normal = null; } @@ -13,15 +14,81 @@ export class Node { } breakDown() { + if (this.nodes) { + console.error("attempt of breaking down not a leaf node") + this.makeLeaf(); + } this.nodes = [new Node(), new Node(), new Node(), new Node(), new Node(), new Node(), new Node(), new Node()]; + this.nodes.forEach(n => n.tag = this.tag); + } + + makeLeaf() { + if (this.nodes) { + this.nodes.forEach(n => n.dispose()); + this.nodes = null; + } } + + dispose() {} + } -const directors = [ +export const directors = [ [0,0,0], [1,0,0], [0,1,0], [1,1,0], [0,0,1], [1,0,1], [0,1,1], [1,1,1] ]; +export class NDTree { + + constructor(size) { + this.root = new Node(); + this.size = size; + if (this.size % 2 !== 0) { + throw 'size of nd tree must be power of two' + } + this.dimension = 3; + this.directors = directors; + this.nodesCount = Math.pow(2, this.dimension); + } + + traverse(handler) { + traverseOctree(this.root, this.size, handler); + } + + defragment() { + + function defrg(node, x,y,z, size) { + + if (node.leaf) { + return; + } + + const subSize = size / 2; + + let allChildrenLeafsSameKind = true; + + for (let i = 0; i < 8; i ++) { + const subNode = node.nodes[i]; + if (subNode) { + const [dx, dy, dz] = directors[i]; + defrg(subNode, x + dx*subSize, y + dy*subSize, z + dz*subSize, subSize) + if (!subNode.leaf || subNode.tag !== node.tag) { + allChildrenLeafsSameKind = false; + } + } + } + + if (allChildrenLeafsSameKind) { + node.makeLeaf(); + } + + } + + defrg(this.root, 0,0,0, this.size); + } + +} + export function traverseOctree(root, baseSize, handler) { const stack = []; @@ -32,7 +99,7 @@ export function traverseOctree(root, baseSize, handler) { const [node, [x,y,z], size] = stack.pop(); if (node.leaf) { - handler(x, y, z, size, node.tag); + handler(x, y, z, size, node.tag, node); continue; } const subSize = size / 2; @@ -45,12 +112,40 @@ export function traverseOctree(root, baseSize, handler) { stack.push([subNode, subLocation, subSize]); } } - } +} + +export function generateVoxelShape(root, baseSize, classify) { + const stack = []; + + stack.push([root, [0,0,0], baseSize]); + + while (stack.length !== 0) { + + const [node, [x,y,z], size] = stack.pop(); + + node.size = size; // todo remove, debug + node.xyz = [x,y,z]; // todo remove, debug + node.tag = classify(x, y, z, size); + + if (size === 1 || node.tag !== 'edge') { + continue; + } + node.breakDown(); + + const subSize = size / 2; + + for (let i = 0; i < 8; i ++) { + const [dx, dy, dz] = directors[i]; + const subLocation = [x + dx*subSize, y + dy*subSize, z + dz*subSize]; + const subNode = node.nodes[i]; + stack.push([subNode, subLocation, subSize]); + } + } } -export function pushVoxel(root, baseSize, [vx, vy, vz], tag) { +export function pushVoxel(root, baseSize, [vx, vy, vz], tag, normal, semantic) { const stack = []; stack.push([root, [0,0,0], baseSize]); @@ -61,6 +156,7 @@ export function pushVoxel(root, baseSize, [vx, vy, vz], tag) { if (size === 1 && x === vx && y === vy && z === vz) { node.tag = tag; + node.normal = normal; return; } if (size === 1) { @@ -110,8 +206,8 @@ export function createOctreeFromSurface(origin, sceneSize, treeSize, surface, ta const voxel = vec.sub(pMin, origin); vec._div(voxel, resolution); vec.scalarOperand(voxel, voxel, v => Math.floor(v)); - - pushVoxel(root, treeSize, voxel, tag); + const normal = surface.normal(uMin, vMin); + pushVoxel(root, treeSize, voxel, tag, normal); } else { const uMid = uMin + (uMax - uMin) / 2; const vMid = vMin + (vMax - vMin) / 2; diff --git a/modules/voxels/vixelViz.ts b/modules/voxels/vixelViz.ts new file mode 100644 index 000000000..8b63790e2 --- /dev/null +++ b/modules/voxels/vixelViz.ts @@ -0,0 +1,54 @@ +import {BoxGeometry, Color, Group, Mesh, MeshPhongMaterial} from "three"; + +const geometry = new BoxGeometry( 1, 1, 1 ); + +export class Cube extends Group { + + material: MeshPhongMaterial; + + constructor(size=1, colorTag) { + + super(); + + let material; + if (colorTag) { + material = MaterialTable[colorTag]; + } else { + material = MaterialRandomTable[Math.round(Math.random() * 100000) % MaterialRandomTable.length] + } + + const mesh = new Mesh(geometry, material); + mesh.position.x = 0.5*size; + mesh.position.y = 0.5*size; + mesh.position.z = 0.5*size; + mesh.scale.x = size + mesh.scale.y = size + mesh.scale.z = size + + this.add(mesh) + + } +} + +const randomInt = (min, max) => { + return Math.floor(Math.random() * (max - min + 1)) + min; +}; + +const niceColor = () => { + const h = randomInt(0, 360); + const s = randomInt(42, 98); + const l = randomInt(40, 90); + return `hsl(${h},${s}%,${l}%)`; +}; + +const MaterialRandomTable = []; + +for (let i = 0; i < 1000; i ++) { + MaterialRandomTable.push(new MeshPhongMaterial( { color: new Color(niceColor())} )); +} + +const MaterialTable = { + 'inside': new MeshPhongMaterial( { color: 'white'} ), + 'edge': new MeshPhongMaterial( { color: 0x999999} ) +}; + diff --git a/modules/voxels/voxelBool.ts b/modules/voxels/voxelBool.ts new file mode 100644 index 000000000..611fdb64c --- /dev/null +++ b/modules/voxels/voxelBool.ts @@ -0,0 +1,139 @@ +import {directors, NDTree} from "voxels/octree"; + +export function ndTreeSubtract(a: NDTree, b: NDTree) { + mergeNDTrees(a, b, 'subtract') +} + + +function mergeNDTrees(aTree: NDTree, bTree: NDTree, boolSemantic) { + + const stack = []; + + if (aTree.size !== bTree.size) { + throw 'unsupported'; + } + + stack.push([ + aTree.root, + bTree.root, + [0,0,0], + aTree.size + ]); + + let counter = 0; + while (stack.length !== 0) { + counter ++; + const [a, b, [x, y, z], size] = stack.pop(); + + if (a.leaf && b.leaf) { + if (boolSemantic === 'subtract') { + if (b.tag === 'inside' || b.tag === 'edge') { + a.tag = 'outside'; + } + //TBD... + } + continue; + } + + if (a.leaf) { + a.breakDown(); + } + + const subSize = size / 2; + + for (let i = 0; i < aTree.nodesCount; i ++) { + const [dx, dy, dz] = aTree.directors[i]; + const subLocation = [x + dx*subSize, y + dy*subSize, z + dz*subSize]; + const subNode1 = a.nodes[i]; + const subNode2 = b.leaf ? b : b.nodes[i]; + stack.push([subNode1, subNode2, subLocation, subSize]); + } + } + console.log("!!!! =",counter) + +} + +export function ndTreeTransformAndSubtract(a: NDTree, b: NDTree, transformer) { + b.traverse((xo, yo, zo, size, tag, node) => { + + const coord = transformer([xo, yo, zo]); + + if (tag !== 'outside') { + + insertNode(a.root, a.size, coord, size, tag, 'subtract'); + + } + + }); + + +} + +function insertNode(targetNode, targetSize, [vx, vy, vz], insertSize, tag, boolSemantic) { + if (vx > targetSize || vy > targetSize || vz > targetSize) { + return; + } + + const stack = []; + + stack.push([targetNode, [0,0,0], targetSize]); + + while (stack.length !== 0) { + + const [node, [x,y,z], size] = stack.pop(); + + + const nodeInside = isInside(x, y, z, size, vx, vy, vz, insertSize); + if (nodeInside) { + if (boolSemantic === 'subtract') { + if (node.tag === 'inside' || node.tag === 'edge') { + node.tag = 'outside'; + node.makeLeaf(); + } + //TBD... + } + continue; + } + + if (size === 1) { + continue; + } + + if (node.leaf) { + node.breakDown(); + } + + const subSize = size / 2; + + for (let i = 0; i < 8; i ++) { + const [dx, dy, dz] = directors[i]; + const subLocation = [x + dx*subSize, y + dy*subSize, z + dz*subSize]; + const [sx1, sy1, sz1] = subLocation; + if (nodeInside || overlaps(vx, vy, vz, insertSize, sx1, sy1, sz1, subSize)) { + const subNode = node.nodes[i]; + stack.push([subNode, subLocation, subSize]); + } + } + } +} + +function isInside(tx, ty, tz, tsize, rx, ry, rz, rsize) { + + return isPtInside(tx, ty, tz, rx, ry, rz, rsize) && isPtInside(tx+tsize, ty+tsize,tz+tsize, rx, ry, rz, rsize) + +} +function overlaps(tx, ty, tz, tsize, rx, ry, rz, rsize) { + return overlap1d(tx, tx + tsize, rx, rx + rsize) + * overlap1d(ty, ty + tsize, ry, ry + rsize) + * overlap1d(tz, tz + tsize, rz, rz + rsize) > 0; +} + +function isPtInside(x, y, z, rx, ry, rz, size) { + + return (x >= rx) && (y >= ry) && (z >= rz) && (x <= rx + size) && (y <= ry+size) && (z <= rz+size); + +} + +function overlap1d(min1, max1, min2, max2) { + return Math.max(0, Math.min(max1, max2) - Math.max(min1, min2)) +} diff --git a/modules/voxels/voxelPrimitives.ts b/modules/voxels/voxelPrimitives.ts new file mode 100644 index 000000000..a1ad70b5b --- /dev/null +++ b/modules/voxels/voxelPrimitives.ts @@ -0,0 +1,41 @@ +import {directors, generateVoxelShape, NDTree, Node} from "voxels/octree"; +import {Vec3} from "math/vec"; +import {sq} from "math/commons"; + + +export function renderVoxelSphere(radius: number, [a,b,c]: Vec3, ndTree: NDTree) { + + const rr = radius*radius + + generateVoxelShape( + ndTree.root, + ndTree.size, + (x, y, z, size) => { + let insides = 0; + let outsides = 0; + iterateCubeVertices(x, y, z, size, (x, y, z) => { + + if ((sq(x-a) + sq(y-b) + sq(z-c)) <= rr) { + insides ++; + } else { + outsides ++; + } + }); + + if (insides !== 0 && outsides !== 0) { + return 'edge'; + } else if (insides !== 0) { + return 'inside'; + } else { + return 'outside'; + } + + } + ) +} + +function iterateCubeVertices(x, y, z, size, cb) { + directors.forEach(([dx, dy, dz]) => { + cb(x + size*dx, y + size*dy, z + size*dz); + }); +} \ No newline at end of file diff --git a/web/app/cad/sandbox.ts b/web/app/cad/sandbox.ts index 213be2f72..568befe6a 100644 --- a/web/app/cad/sandbox.ts +++ b/web/app/cad/sandbox.ts @@ -4,7 +4,7 @@ import BrepCurve from 'geom/curves/brepCurve'; import NurbsCurve from "geom/curves/nurbsCurve"; import {surfaceIntersect} from 'geom/intersection/surfaceSurface'; import NurbsSurface from 'geom/surfaces/nurbsSurface'; -import {createOctreeFromSurface, traverseOctree} from "voxels/octree"; +import {createOctreeFromSurface, NDTree, traverseOctree} from "voxels/octree"; import {Matrix3x4} from 'math/matrix'; import {AXIS, ORIGIN} from "math/vector"; import {BrepInputData, CubeExample} from "engine/data/brepInputData"; @@ -18,6 +18,20 @@ import {DefeatureFaceWizard} from "./craft/defeature/DefeatureFaceWizard"; import {defeatureByEdge, defeatureByVertex} from "brep/operations/directMod/defeaturing"; import {BooleanType} from "engine/api"; import {MBrepShell} from './model/mshell'; +import * as vec from "math/vec"; +import { + BoxGeometry, + BufferAttribute, + BufferGeometry, + DoubleSide, Group, + Mesh, + MeshBasicMaterial, + MeshPhongMaterial +} from "three"; +import {pseudoFrenetFrame} from "geom/curves/frenetFrame"; +import {renderVoxelSphere} from "voxels/voxelPrimitives"; +import {Cube} from "voxels/vixelViz"; +import {ndTreeSubtract, ndTreeTransformAndSubtract} from "voxels/voxelBool"; // @ts-ignore @@ -391,7 +405,84 @@ export function runSandbox(ctx: ApplicationContext) { // // } - function voxelTest(size = 8) { + function voxelTest2(size = 512) { + + size= 128; + + const work = new NDTree(size); + const tool = new NDTree(size); + + renderVoxelSphere(32, [16,16,16], work); + renderVoxelSphere(16, [0,0,0], tool); + + // ndTreeSubtract(work, tool); + + + + + + + + let oldNodes = new Set(); + + let delta = -5 + function simulate() { + + for (let i = 0; i < 100; i ++) { + ndTreeTransformAndSubtract(work, tool, (pt) => pt.map(s => s + delta) ); + delta ++; + + } + work.defragment(); + + + let curNodes = new Set(); + + oldNodes.forEach(n => { + ctx.cadScene.auxGroup.remove(n.visual); + }); + + oldNodes.clear(); + + work.traverse((x, y, z, size, tag, node) => { + + // if (size === 1 ) { + if (tag !== 'outside') { + // if (tag === 'edge' ) { + // console.log(size) + // console.log(node.xyz) + // console.log([x, y, z]) + + + const cube = new Cube(size, tag); + cube.position.set(x, y, z); + ctx.cadScene.auxGroup.add(cube); + ctx.cadScene.auxGroup.scale.set(10, 10, 10); + node.visual = cube; + + oldNodes.add(node); + } + + }); + ctx.viewer.requestRender(); + // setTimeout(() => requestAnimationFrame(simulate), 100); + } + + simulate() + + + console.log("voxel count", ctx.cadScene.auxGroup.children.length); + + // geometry.setAttribute( 'position', new BufferAttribute( new Float32Array(vertices), 3 ) ); + // geometry.setAttribute( 'normal', new BufferAttribute( new Float32Array(normals), 3 ) ); + + + + console.log("done") + + } + + function voxelTest(size = 512) { const degree = 3 , knots = [0, 0, 0, 0, 0.333, 0.666, 1, 1, 1, 1] @@ -405,19 +496,41 @@ export function runSandbox(ctx: ApplicationContext) { let srf = verb.geom.NurbsSurface.byKnotsControlPointsWeights( degree, degree, knots, knots, pts ); srf = srf.transform(new Matrix3x4().scale(10,10,10).toArray()); srf = new NurbsSurface(srf); - __DEBUG__.AddParametricSurface(srf); + // __DEBUG__.AddParametricSurface(srf); const origin = [0,-500,-250]; const treeSize = size; const sceneSize = 512; const r = sceneSize / treeSize; const octree = createOctreeFromSurface(origin, sceneSize, treeSize, srf, 1); - traverseOctree(octree, treeSize, (x, y, z, size, tag) => { + + const geometry = new BufferGeometry(); + + const vertices = [] + const normals = [] + + traverseOctree(octree, treeSize, (x, y, z, size, tag, normal) => { if (size === 1 && tag === 1) { - // const base = [x, y, z]; - // vec._mul(base, r); - // vec._add(base, origin); + const base = [x, y, z]; + vec._mul(base, r); + vec._add(base, origin); + + + const [T, N, B] = pseudoFrenetFrame(normal); + let n = vec.add(base, vec.mul(N, r)); + let b = vec.add(base, vec.mul(B, r)); + vertices.push(...base); + vertices.push(...n); + vertices.push(...b); + + vertices.push(...b); + vertices.push(...n); + vertices.push(...vec._add(vec.add(vec.mul(N, r), vec.mul(B, r)), base)); + + normals.push(...normal); + // __DEBUG__.AddNormal3(base, normal) + // __DEBUG__.AddPolyLine3([ // vec.add(base, [0, r, 0]), // vec.add(base, [0, r, r]), @@ -428,8 +541,18 @@ export function runSandbox(ctx: ApplicationContext) { // vec.add(base, [r, 0, r]), // vec.add(base, [0, 0, 0]), // ], 0xff0000); + } }); + + geometry.setAttribute( 'position', new BufferAttribute( new Float32Array(vertices), 3 ) ); + // geometry.setAttribute( 'normal', new BufferAttribute( new Float32Array(normals), 3 ) ); + + const material = new MeshBasicMaterial( { color: 0xffffff, side: DoubleSide } ); + const mesh = new Mesh( geometry, material ); + + ctx.cadScene.auxGroup.add(mesh); + console.log("done") } @@ -706,7 +829,9 @@ export function runSandbox(ctx: ApplicationContext) { //testRemoveVertex(); // testRemoveEdge(); // testOJS(); - setTimeout(testOCCT, 500); + // setTimeout(testOCCT, 500); + // voxelTest() + voxelTest2(16) } }); diff --git a/web/app/sketcher/actions/objectToolActions.js b/web/app/sketcher/actions/objectToolActions.js index 6de304369..a2b388103 100644 --- a/web/app/sketcher/actions/objectToolActions.js +++ b/web/app/sketcher/actions/objectToolActions.js @@ -11,6 +11,7 @@ import { RectangleToolIcon } from "../icons/tools/ToolIcons"; import {AddSegmentTool} from "../tools/segment"; +import {BSplineTool} from "../tools/b-spline"; import {BezierCurveTool} from "../tools/bezier-curve"; import {EllipseTool} from "../tools/ellipse"; import {AddPointTool} from "../tools/point"; @@ -130,6 +131,19 @@ export default [ }, + { + id: 'BSplineTool', + shortName: 'BSpline', + kind: 'Tool', + description: 'Add a b spline curve', + icon: BezierToolIcon, // need a new icon + + invoke: (ctx) => { + ctx.viewer.toolManager.takeControl(new BSplineTool(ctx.viewer)); + } + + }, + { id: 'RectangleTool', shortName: 'Rectangle', diff --git a/web/app/sketcher/shapes/b-spline.ts b/web/app/sketcher/shapes/b-spline.ts new file mode 100644 index 000000000..d42d06c0e --- /dev/null +++ b/web/app/sketcher/shapes/b-spline.ts @@ -0,0 +1,853 @@ +import { EndPoint } from "./point"; +import { Segment } from "./segment"; +import Vector from "math/vector"; +import { SketchObject } from "./sketch-object"; +import { Layer, Viewer } from "../viewer2d"; +import { TOLERANCE, areEqual, arePointsEqual } from "math/equality"; +import { lu_solve } from "math/optim/dogleg"; +import { isPointInsidePolygon, polygonOffset, ConvexHull2D } from "geom/euclidean"; + +type IPolynomialFunc = (t: number) => number; +type IPoint = { x: number; y: number; z?: number }; +export const getDividedValue = (numerator: number, denominator: number) => { + if (denominator === 0) { + return 0; + } else { + return numerator / denominator; + } +}; + +export class BSplinePolynomial { + /** * B-spline polynomial with variable coefficients */ + readonly order: number; + kValues: number[]; + maxIndex: number; + private cache: Map = new Map(); + private polynomialArray: BSplinePolynomial[] = []; + /** + * @param kValues Polynomial value boundary points Node vector + * @param order The degree of the polynomial (for example, 3rd order is 2nd order, 5th order is 4th order) degree = order - 1 degree is the degree of the B-spline curve + */ + constructor(kValues: number[], order: number) { + this.order = order; + this.kValues = kValues; + this.maxIndex = kValues.length - this.order; + this.polynomialArray = []; + for (let i = 0; i < this.order; i += 1) { + this.polynomialArray.push(new BSplinePolynomial(this.kValues, i)); + } + } + + updateKValues(kValues: number[]) { + this.kValues = kValues; + this.maxIndex = kValues.length - this.order; + + this.cache.clear(); + + if (this.polynomialArray.length === 0) { + for (let i = 0; i < this.order; i += 1) { + this.polynomialArray.push(new BSplinePolynomial(this.kValues, i)); + } + } else { + for (let i = 0; i < this.order; i += 1) { + this.polynomialArray[i].updateKValues(this.kValues); + } + } + } + + get(index: number): IPolynomialFunc { + if (index > this.maxIndex) { + return () => 0; + } + const cacheFunc = this.cache.get(index); + if (cacheFunc) { + return cacheFunc; + } + return this.getPolynomialFunc(index); + } + + /** + * Get the polynomial evaluation function, that is, the vector polynomial variable coefficient, + * and return a function that accepts the t parameter + */ + private getPolynomialFunc(index: number): IPolynomialFunc { + const tList = this.kValues; + const { order } = this; + const polynomialIndexSubtractOne = this.polynomialArray[order - 1]; + let func: IPolynomialFunc; + if (order === 1) { + func = (t: number) => { + if (t >= this.kValues[index] && t < this.kValues[index + 1]) { + return 1; + } else { + return 0; + } + }; + } else { + const k1 = tList[index + order - 1] - tList[index]; + const k2 = tList[index + order] - tList[index + 1]; + func = (t: number) => + getDividedValue(t - tList[index], k1) * polynomialIndexSubtractOne.get(index)(t) + + getDividedValue(tList[index + order] - t, k2) * polynomialIndexSubtractOne.get(index + 1)(t); + } + this.cache.set(index, func); + return func; + } +} + +// B-spline curve interpolation drawing method and control point drawing method +class BSplineInterpolation { + // Interpolation only supports cubic B-spline + interpolation: boolean; + degree: number; + kSolver: CentripetalParameterMethod; + cSolver: CPointsCalculator; + cPoints: EndPoint[]; + kValues: number[]; + fPoints: EndPoint[]; + constructor(degree: number, interpolation: boolean) { + this.degree = degree; + this.interpolation = interpolation; + this.init(); + } + + init() { + this.fPoints = []; + this.cPoints = []; + this.kValues = []; + if (this.interpolation) { + this.kSolver = new CentripetalParameterMethod(); + this.cSolver = new CPointsCalculator(); + } else { + this.kSolver = null; + this.cSolver = null; + } + } + + update(fPoints: EndPoint[]) { + if (!this.interpolation) { + return; + } + this.fPoints = fPoints; + if (this.fPoints.length < 3) { + this.interpolation = false; + } + } + + solve() { + // Solve the nodes and control points according to the interpolation points fPoints + if (!this.interpolation) { + return; + } + this.kValues = this.kSolver.calculate(this.fPoints, this.degree); + this.cSolver.setup(this.kValues, this.fPoints, this.degree); + const cPointsCoordinates = this.cSolver.calculate(); + this.cPoints.length = cPointsCoordinates.length; + cPointsCoordinates.forEach((item, index) => { + this.cPoints[index] = new EndPoint(item.x, item.y); + }); + } +} + +class BSplineControlVertices { + // B-spline curve control point drawing method, support drawing spline curves of different degrees + CVModel: boolean; + maxDegree: number; + degree: number; + cPoints: EndPoint[]; + kValues: number[]; + fPoints: EndPoint[]; + constructor(degree: number, CVModel: boolean) { + this.maxDegree = degree; + this.CVModel = CVModel; + this.fPoints = []; + this.cPoints = []; + this.kValues = []; + } + + update(cPoints: EndPoint[], degree: number) { + if (!this.CVModel) { + return; + } + this.cPoints = cPoints; + if (this.cPoints.length < 3) { + this.CVModel = false; + } + this.maxDegree = degree; + if (this.cPoints.length < this.maxDegree + 1) { + this.degree = this.cPoints.length - 1; + } else { + this.degree = this.maxDegree; + } + } + + solve() { + // Solve the nodes, the number of which meets the control point and order requirements, + // and fill 0 and 1 at both ends to make the spline curve clamped and evenly segmented in the middle. + this.fPoints = [this.cPoints[0], this.cPoints[this.cPoints.length - 1]]; + this.kValues = [...new Array(this.degree + 1).fill(0.0)]; + for (let i = 0; i < this.cPoints.length - this.degree - 1; ++i) { + this.kValues.push((i + 1) / (this.cPoints.length - this.degree)); + } + this.kValues.push(...new Array(this.degree + 1).fill(1.0)); + } +} + +export interface IBSplineOpts { + degree: number; + cPoints: IPoint[]; + fPoints: IPoint[]; + kValues: number[]; +} + +export class BSpline extends SketchObject { + ctx: CanvasRenderingContext2D | undefined; + + scale: number; + + degree: number; + + closed: boolean; + + order: number; + + cPoints: EndPoint[]; // Spline control points + + kValues: number[]; // Spline Nodes + + knots: number[]; // Curve nodes after deduplication + + fPoints: EndPoint[]; // Fitting points for easy curve adjustment + + a: EndPoint; // Start point of the curve + + b: EndPoint; // End point of the curve + + numberOfKnots: number; + + numberOfControlPoints: number; + + numberOfFitPoints: number; + + bSplinePolynomial: BSplinePolynomial; + + derivativePolynomial: BSplinePolynomial; + + bSplineInterpolation: BSplineInterpolation; + + bSplineControlVertices: BSplineControlVertices; + + hull: Vector[]; // Curved polygonal bounding box + + // discretePoints: EndPoint[]; // Fixed curve discrete points for distance calculation + discretePointsWithScale: { [key: number]: EndPoint[] }; // Record discrete points at different ratios to save computing resources + + step: number; // 曲线离散步长 + + dragging: boolean = false; + + constructor( + opts: IBSplineOpts, + interpolation: boolean = false, // If true, the interpolation method is manually drawn + CVModel: boolean = false, // If true, the CV method is used for manual drawing. If both are false, the data is read and drawn. + id?: string, + ctx?: CanvasRenderingContext2D, + scale?: number, + ) { + super(id); + this.ctx = ctx; + this.scale = scale || 1; + this.degree = opts.degree; + this.order = this.degree + 1; + this.closed = false; + this.numberOfControlPoints = opts.cPoints.length; + this.numberOfKnots = opts.kValues.length; + this.numberOfFitPoints = opts.fPoints.length; + if (arePointsEqual(opts.cPoints[0], opts.cPoints[this.numberOfControlPoints - 1], TOLERANCE)) { + this.closed = true; + } + this.cPoints = []; + for (const [i, point] of opts.cPoints.entries()) { + const cPointId = "spline${this.id}_cPoint${i}"; + const cPoint = new EndPoint(point.x, point.y, cPointId); + this.addChild(cPoint); + this.cPoints.push(cPoint); + cPoint.visible = false; + } + this.kValues = opts.kValues; + this.updateKnots(); + this.fPoints = []; + if (opts.fPoints.length) { + for (const [i, point] of opts.fPoints.entries()) { + const fPointId = "spline${this.id}_fPoint${i}"; + const fPoint = new EndPoint(point.x, point.y, fPointId); + this.addChild(fPoint); + this.fPoints.push(fPoint); + } + this.a = this.fPoints[0]; + this.b = this.fPoints[this.numberOfFitPoints - 1]; + } else { + this.a = this.cPoints[0]; + this.b = this.cPoints[this.numberOfControlPoints - 1]; + this.a.visible = true; + this.b.visible = true; + } + if (this.degree >= this.numberOfControlPoints) { + throw new Error( + "the degree(${this.degree}) should be smaller than the length of control point(${this.numberOfControlPoints}).", + ); + } + if (this.degree < 1) { + throw new Error("degree cannot be less than 1."); + } + if (this.numberOfKnots !== this.numberOfControlPoints + this.order) { + throw new Error( + "the array length of parameter t (${this.numberOfKnots}) must be equal to the sum of the length of cPoints (${this.numberOfControlPoints}) and the degree (${this.degree}). and 1", + ); + } + this.bSplinePolynomial = new BSplinePolynomial(this.kValues, this.order); + const newKValues = this.kValues.slice(1, this.kValues.length - 1); + this.derivativePolynomial = new BSplinePolynomial(newKValues, this.order - 1); + this.bSplineInterpolation = new BSplineInterpolation(this.degree, interpolation); + this.bSplineControlVertices = new BSplineControlVertices(this.degree, CVModel); + if (interpolation) { + this.bSplineInterpolation.update(this.fPoints); + } + if (CVModel) { + this.bSplineControlVertices.update(this.cPoints, this.degree); + } + this.step = 0.1; + this.discretePointsWithScale = { + 1: this.transToEndPoints(this.getDiscretePoints(1)), + }; + } + + updateKnots() { + this.knots = Array.from(new Set(this.kValues)).sort((a, b) => a - b); + } + + getPoint(t: number) { + let x = 0; + let y = 0; + for (let index = 0; index < this.numberOfControlPoints; ++index) { + const ratio = this.bSplinePolynomial.get(index)(t); + x += ratio * this.cPoints[index].x; + y += ratio * this.cPoints[index].y; + } + return { x, y }; + } + + basisFunction(i, p, u, knots) { + if (p === 0) { + return knots[i] <= u && u < knots[i + 1] ? 1.0 : 0.0; + } + const left = (u - knots[i]) / (knots[i + p] - knots[i]) || 0; + const right = (knots[i + p + 1] - u) / (knots[i + p + 1] - knots[i + 1]) || 0; + return left * this.basisFunction(i, p - 1, u, knots) + right * this.basisFunction(i + 1, p - 1, u, knots); + } + + derivativeBSpline(t: number) { + const n = this.cPoints.length - 1; + let dx = 0, + dy = 0; + for (let i = 0; i < n; i++) { + const denom = this.kValues[i + this.degree + 1] - this.kValues[i + 1]; + if (denom === 0) continue; + const coeff = this.degree / denom; + const diffX = this.cPoints[i + 1].x - this.cPoints[i].x; + const diffY = this.cPoints[i + 1].y - this.cPoints[i].y; + const Ni = this.derivativePolynomial.get(i)(t); + dx += coeff * diffX * Ni; + dy += coeff * diffY * Ni; + } + return { x: dx, y: dy }; + } + + addChildPoint(point: EndPoint): void { + point.id = this.id; + this.addChild(point); + } + + removeChildPoint(point: EndPoint) { + this.children.forEach((item, index) => { + if (item === point) { + this.children.splice(index, 1); + } + }); + } + + setChildPoint(points: EndPoint[]) { + this.children = points; + points.forEach((item) => { + item.parent = this; + }); + } + + updatePoint(index: number, point: EndPoint) { + if (index < 0 || index > this.cPoints.length - 1) { + throw new Error("parameter index error."); + } + if (typeof this.cPoints[index] !== "undefined") { + this.removeChildPoint(this.cPoints[index]); + } + this.addChildPoint(point); + this.cPoints[index] = point; + } + + addCPoint(point: EndPoint) { + this.addChildPoint(point); + this.cPoints.push(point); + point.visible = false; + this.numberOfControlPoints += 1; + } + + setCPoints(points: EndPoint[]) { + this.cPoints.length = points.length; + this.numberOfControlPoints = this.cPoints.length; + points.forEach((item, index) => { + this.updatePoint(index, item); + item.visible = false; + }); + } + + resetCPoints(points: EndPoint[], visible: boolean) { + this.cPoints = points; + this.numberOfControlPoints = this.cPoints.length; + this.cPoints.forEach((item) => { + item.visible = visible; + }); + } + + addFPoint(point: EndPoint) { + if (point.id !== this.fPoints[this.fPoints.length - 1].id) { + this.addChild(point); + this.fPoints.push(point); + this.numberOfFitPoints = this.fPoints.length; + } + } + + removeFPoint() { + const point = this.fPoints.pop(); + this.removeChildPoint(point); + } + + setFPoint(points: EndPoint[]) { + this.fPoints.length = points.length; + this.numberOfFitPoints = this.fPoints.length; + for (let i = 0; i < points.length; ++i) { + if (this.fPoints[i] !== points[i]) { + this.removeChildPoint(this.fPoints[i]); + this.fPoints[i] = points[i]; + this.addChild(this.fPoints[i]); + this.fPoints[i].visible = true; + } + } + } + + resetFPoints(points: EndPoint[], visible: boolean) { + this.fPoints = points; + this.numberOfFitPoints = this.fPoints.length; + this.fPoints.forEach((item) => { + item.visible = visible; + }); + } + + setPointA(point: EndPoint) { + this.a = point; + } + + setPointB(point: EndPoint) { + this.b = point; + } + + setKValues(kValues: number[]) { + this.kValues = kValues; + this.updateKnots(); + this.bSplinePolynomial.updateKValues(this.kValues); + const newKValues = this.kValues.slice(1, this.kValues.length - 1); + this.derivativePolynomial.updateKValues(newKValues); + this.numberOfKnots = this.kValues.length; + } + + interpolate(fPoints: EndPoint[]) { + if (fPoints.length > 2) { + this.bSplineInterpolation.update(fPoints); + } + } + + update() { + this.bSplineInterpolation.solve(); + this.resetFPoints(this.bSplineInterpolation.fPoints, true); + this.setKValues(this.bSplineInterpolation.kValues); + this.resetCPoints(this.bSplineInterpolation.cPoints, false); + this.setPointA(this.fPoints[0]); + this.setPointB(this.fPoints[this.fPoints.length - 1]); + this.setChildPoint([...this.fPoints, ...this.cPoints]); + } + + cvReset(cPoints: EndPoint[], degree: number) { + if (cPoints.length > 2) { + this.bSplineControlVertices.update(cPoints, degree); + this.degree = degree; + this.order = this.degree + 1; + } + } + + cvUpdate() { + this.bSplineControlVertices.solve(); + this.resetFPoints(this.bSplineControlVertices.fPoints, false); + this.setKValues(this.bSplineControlVertices.kValues); + this.resetCPoints(this.bSplineControlVertices.cPoints, true); + this.setPointA(this.cPoints[0]); + this.setPointB(this.cPoints[this.cPoints.length - 1]); + this.setChildPoint([...this.cPoints]); + } + + getDiscretePoints(scale: number) { + const ratio = 1 / scale; + const discretePoints: IPoint[] = []; + for (let index = 0; index < this.knots.length - 1; ++index) { + if (index === 0) { + discretePoints.push(this.a); + } else { + const fPoint = this.getPoint(this.knots[index]); + if (this.bSplineInterpolation.interpolation && this.knots.length === this.fPoints.length) { + discretePoints.push(...this.transToIPoints([this.fPoints[index]])); + } else { + discretePoints.push(fPoint); + } + } + + if (this.knots[index + 1] - this.knots[index] < this.step * ratio) { + continue; + } + for (let k = this.knots[index] + this.step * ratio; k <= this.knots[index + 1]; k += this.step * ratio) { + const p = this.getPoint(k); + discretePoints.push(p); + } + } + discretePoints.push(this.b); + return discretePoints; + } + + visitParams(callback) { + for (const point of this.cPoints) { + point.visitParams(callback); + } + } + + normalDistance(aim: Vector, scale: number) { + // Get the vertices of the convex polygon surrounded by control points in sequence + const boundaryPoints = [...this.cPoints]; // Deep copy avoids ConvexHull2D function sorting affecting this.cPoints + const hullPoints = ConvexHull2D(boundaryPoints); + + // Get the point vector after the convex polygon is expanded + // (the center point position of the convex polygon quadrilateral bounding box remains unchanged) + this.hull = polygonOffset(hullPoints, 1 + 0.3 / scale); + if (isPointInsidePolygon(aim, this.hull)) { + const discreteScale = this.getDiscreteScale(scale); + return this.closestNormalDistance(aim, this.discretePointsWithScale[discreteScale]); + } + return -1; + } + + closestNormalDistance(aim: Vector, segments: EndPoint[]) { + let hero = -1; + for (let p = segments.length - 1, q = 0; q < segments.length; p = q++) { + const dist = Math.min(Segment.calcNormalDistance(aim, segments[p], segments[q])); + if (dist !== -1) { + hero = hero === -1 ? dist : Math.min(dist, hero); + } + } + return hero; + } + + transToEndPoints(points: IPoint[]) { + const endPoints = []; + for (const point of points) { + endPoints.push(new EndPoint(point.x, point.y)); + } + return endPoints; + } + + transToIPoints(points: EndPoint[]) { + const IPoints = []; + for (const point of points) { + IPoints.push({ x: point.x, y: point.y, z: 0.0 }); + } + return IPoints; + } + + bsplineToBezierSegments() { + // Each interval is a Bézier + const bezierSegments = []; + if (!this.closed && this.bSplineInterpolation) { + for (let i = 0; i < this.fPoints.length - 1; i++) { + const a = { x: this.fPoints[i].x, y: this.fPoints[i].y }; + const b = { x: this.fPoints[i + 1].x, y: this.fPoints[i + 1].y }; + const derivative1 = this.derivativeBSpline(this.knots[i]); + const derivative2 = this.derivativeBSpline(this.knots[i + 1]); + let cp1X; + let cp1Y; + let cp2X; + let cp2Y; + if (areEqual(derivative1.x, 0, TOLERANCE)) { + cp1X = this.fPoints[i].x; + if (areEqual(this.cPoints[i + 2].x, this.cPoints[i + 1].x, TOLERANCE)) { + cp1Y = this.fPoints[i].y; + } else { + cp1Y = + ((this.cPoints[i + 2].y - this.cPoints[i + 1].y) / (this.cPoints[i + 2].x - this.cPoints[i + 1].x)) * + (cp1X - this.cPoints[i + 1].x) + + this.cPoints[i + 1].y; + } + } else { + const k = derivative1.y / derivative1.x; + if (areEqual(this.cPoints[i + 2].x, this.cPoints[i + 1].x, TOLERANCE)) { + cp1X = this.cPoints[i + 1].x; + cp1Y = this.fPoints[i].y + k * (cp1X - this.fPoints[i].x); + } else { + const k1 = + (this.cPoints[i + 2].y - this.cPoints[i + 1].y) / (this.cPoints[i + 2].x - this.cPoints[i + 1].x); + cp1X = areEqual(k, k1, TOLERANCE) + ? this.fPoints[i].x + : (this.fPoints[i].y - this.cPoints[i + 1].y + k1 * this.cPoints[i + 1].x - k * this.fPoints[i].x) / + (k1 - k); + cp1Y = + ((this.cPoints[i + 2].y - this.cPoints[i + 1].y) / (this.cPoints[i + 2].x - this.cPoints[i + 1].x)) * + (cp1X - this.cPoints[i + 1].x) + + this.cPoints[i + 1].y; + } + } + if (areEqual(derivative2.x, 0, TOLERANCE)) { + cp2X = this.fPoints[i + 1].x; + if (areEqual(this.cPoints[i + 2].x, this.cPoints[i + 1].x, TOLERANCE)) { + cp2Y = this.fPoints[i + 1].y; + } else { + cp2Y = + ((this.cPoints[i + 2].y - this.cPoints[i + 1].y) / (this.cPoints[i + 2].x - this.cPoints[i + 1].x)) * + (cp2X - this.cPoints[i + 1].x) + + this.cPoints[i + 1].y; + } + } else { + const k = derivative2.y / derivative2.x; + if (areEqual(this.cPoints[i + 2].x, this.cPoints[i + 1].x, TOLERANCE)) { + cp2X = this.cPoints[i + 1].x; + cp2Y = this.fPoints[i + 1].y + k * (cp2X - this.fPoints[i + 1].x); + } else { + const k1 = + (this.cPoints[i + 2].y - this.cPoints[i + 1].y) / (this.cPoints[i + 2].x - this.cPoints[i + 1].x); + cp2X = areEqual(k, k1, TOLERANCE) + ? this.fPoints[i + 1].x + : (this.fPoints[i + 1].y - + this.cPoints[i + 1].y + + k1 * this.cPoints[i + 1].x - + k * this.fPoints[i + 1].x) / + (k1 - k); + cp2Y = + ((this.cPoints[i + 2].y - this.cPoints[i + 1].y) / (this.cPoints[i + 2].x - this.cPoints[i + 1].x)) * + (cp2X - this.cPoints[i + 1].x) + + this.cPoints[i + 1].y; + } + } + const cp1 = { x: cp1X, y: cp1Y }; + const cp2 = { x: cp2X, y: cp2Y }; + bezierSegments.push({ a, b, cp1, cp2 }); + } + } + return bezierSegments; + } + + /** + * Draw B-spline curves (converted to Bézier segments) + */ + drawBSplineBezier(ctx: CanvasRenderingContext2D) { + const segments = this.bsplineToBezierSegments(); + + ctx.beginPath(); + + for (const seg of segments) { + ctx.moveTo(seg.a.x, seg.a.y); + ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.b.x, seg.b.y); + } + ctx.stroke(); + } + + drawBSplineLine(ctx: CanvasRenderingContext2D, scale: number) { + const discretePoints = this.getDiscretePointsWithScale(scale); + const len = discretePoints.length; + if (len === 0) { + return; + } + ctx.beginPath(); + ctx.moveTo(discretePoints[0].x, discretePoints[0].y); + for (const point of discretePoints) { + ctx.lineTo(point.x, point.y); + } + ctx.stroke(); + } + + drawImpl(ctx: CanvasRenderingContext2D, scale: number, viewer: Viewer) { + // This function will be called multiple times to draw the image. + const discreteScale = this.getDiscreteScale(scale); + if (this.bSplineInterpolation.interpolation && this.dragging !== true) { + this.update(); + } else if (this.bSplineControlVertices.CVModel && this.dragging !== true) { + this.cvUpdate(); + } else { + if (!this.discretePointsWithScale[discreteScale]) { + this.discretePointsWithScale[discreteScale] = this.transToEndPoints(this.getDiscretePoints(discreteScale)); + } + } + // this.drawBSplineLine(ctx, discreteScale); + this.drawBSplineBezier(ctx); + } + + getDiscreteScale(scale: number) { + let discreteScale = Math.ceil(Math.log2(scale)); + if (discreteScale < -3) { + discreteScale = 0.125; + } else if (discreteScale <= 1) { + discreteScale = 2 ** discreteScale; + } else { + discreteScale = 2; + } + return discreteScale; + } + + getDiscretePointsWithScale(scale: number) { + const discreteScale = this.getDiscreteScale(scale); + this.discretePointsWithScale[discreteScale] = this.transToEndPoints(this.getDiscretePoints(discreteScale)); + return this.discretePointsWithScale[discreteScale]; + } + + write() { + return { + degree: this.degree, + cPoints: this.transToIPoints(this.cPoints), + fPoints: this.transToIPoints(this.fPoints), + kValues: this.kValues, + }; + } + + static read(id: string, bSplineData: IBSplineOpts) { + return new BSpline(bSplineData, false, false, id); + } + + drag(x, y, dx, dy) { + this.dragging = true; + this.translate(dx, dy); + } + + stabilize(viewer: Viewer) { + this.children.forEach((c) => c.stabilize(viewer)); + } +} + +interface KnotsCalculator { + calculate(modelPoints: EndPoint[], degree: number): number[]; +} + +export class CentripetalParameterMethod implements KnotsCalculator { + calculate(modelPoints: EndPoint[], degree: number) { + const n = modelPoints.length; + const accumulatedLengths = [0.0]; + const knotValues = new Array(degree).fill(0.0); + for (let i = 0; i < n - 1; ++i) { + const lineLength = Math.sqrt( + (modelPoints[i + 1].x - modelPoints[i].x) ** 2 + (modelPoints[i + 1].y - modelPoints[i].y) ** 2, + ); + accumulatedLengths.push(accumulatedLengths[accumulatedLengths.length - 1] + Math.sqrt(lineLength)); + } + for (let i = 0; i < n; ++i) { + knotValues.push(accumulatedLengths[i] / accumulatedLengths[n - 1]); + } + knotValues.push(...new Array(degree).fill(1.0)); + return knotValues; + } +} + +export class CPointsCalculator { + cPoints: Array<{ x: number; y: number; z: 0 }>; + knotValues: number[]; + degree: number; + modelPoints: EndPoint[]; + + constructor() { + this.cPoints = []; + this.knotValues = []; + this.degree = 3; + this.modelPoints = []; + } + + setup(knotValues: number[], modelPoints: EndPoint[], degree: number) { + this.knotValues = knotValues; + this.degree = degree; + this.modelPoints = modelPoints; + const x = this.modelPoints.length; + if (x < this.degree) { + throw new Error("too less points !"); + } + } + + calculate() { + const n = this.modelPoints.length + this.degree - 1; + const matrixN = new Array(n); + const polynomial = new BSplinePolynomial(this.knotValues, this.degree + 1); + const start = new BesselTangentMethod(); + start.calculate(this.modelPoints[0], this.modelPoints[1], this.modelPoints[2]); + const end = new BesselTangentMethod(); + end.calculate( + this.modelPoints[this.modelPoints.length - 3], + this.modelPoints[this.modelPoints.length - 2], + this.modelPoints[this.modelPoints.length - 1], + ); + const { startTangent } = start; + const { endTangent } = end; + const matrixP = new Array(n); + const matrixFX = [(startTangent.x * (this.knotValues[this.degree + 1] - this.knotValues[1])) / this.degree]; + const matrixFY = [(startTangent.y * (this.knotValues[this.degree + 1] - this.knotValues[1])) / this.degree]; + matrixN[0] = [-1, 1, ...new Array(n - 2).fill(0)]; + for (let i = 1; i < this.modelPoints.length; ++i) { + matrixN[i] = new Array(n); + matrixFX.push(this.modelPoints[i - 1].x); + matrixFY.push(this.modelPoints[i - 1].y); + for (let j = 0; j < n; ++j) { + matrixN[i][j] = polynomial.get(j)(this.knotValues[i - 1 + this.degree]); + } + } + matrixN[this.modelPoints.length] = [...new Array(n - 1).fill(0), 1]; + matrixFX.push(this.modelPoints[this.modelPoints.length - 1].x); + matrixFY.push(this.modelPoints[this.modelPoints.length - 1].y); + matrixFX.push((endTangent.x * (this.knotValues[this.degree + n - 1] - this.knotValues[n - 1])) / this.degree); + matrixFY.push((endTangent.y * (this.knotValues[this.degree + n - 1] - this.knotValues[n - 1])) / this.degree); + matrixN[n - 1] = [...new Array(n - 2).fill(0), -1, 1]; + const matrixPX = lu_solve(matrixN, matrixFX, false); + const matrixPY = lu_solve(matrixN, matrixFY, false); + this.cPoints = []; + for (let i = 0; i < n; ++i) { + this.cPoints[i] = { x: matrixPX[i], y: matrixPY[i], z: 0 }; + } + return this.cPoints; + } +} + +class BesselTangentMethod { + startTangent: EndPoint; + middleTangent: EndPoint; + endTangent: EndPoint; + + calculate(pointA: EndPoint, pointB: EndPoint, pointC: EndPoint) { + const distanceAB = Math.sqrt((pointB.x - pointA.x) ** 2 + (pointB.y - pointA.y) ** 2); + const distanceBC = Math.sqrt((pointC.x - pointB.x) ** 2 + (pointC.y - pointB.y) ** 2); + const sum = distanceAB + distanceBC; + const deltaAB = new EndPoint((pointB.x - pointA.x) / distanceAB, (pointB.y - pointA.y) / distanceAB); + const deltaBC = new EndPoint((pointC.x - pointB.x) / distanceBC, (pointC.y - pointB.y) / distanceBC); + this.middleTangent = new EndPoint( + (distanceAB / sum) * deltaAB.x + (distanceBC / sum) * deltaBC.x, + (distanceAB / sum) * deltaAB.y + (distanceBC / sum) * deltaBC.y, + ); + this.startTangent = new EndPoint(2 * deltaAB.x - this.middleTangent.x, 2 * deltaAB.y - this.middleTangent.y); + this.endTangent = new EndPoint(2 * deltaBC.x - this.middleTangent.x, 2 * deltaBC.y - this.middleTangent.y); + } +} diff --git a/web/app/sketcher/tools/b-spline.js b/web/app/sketcher/tools/b-spline.js new file mode 100644 index 000000000..3b6496d61 --- /dev/null +++ b/web/app/sketcher/tools/b-spline.js @@ -0,0 +1,85 @@ +import { Tool } from "./tool"; +import { Segment } from "../shapes/segment"; +import { EndPoint } from "../shapes/point"; +import { IBSplineOpts, CentripetalParameterMethod, CPointsCalculator, BSpline } from "../shapes/b-spline"; +import Vector from "math/vector"; +import { TOLERANCE, arePointsEqual } from "math/equality"; + +export class BSplineTool extends Tool { + constructor(viewer) { + super("basic spline curve", viewer); + this.init(); + this._v = new Vector(); + } + + init() { + this.degree = 3; + this.fPoints = []; + this.curve = null; + this.otherCurveEndPoint = null; + } + + restart() { + this.init(); + this.sendHint("specify first point"); + } + + cleanup(e) { + this.viewer.cleanSnap(); + } + + mouseup(e) { + const p = this.viewer.screenToModel(e); + const length = this.fPoints.length; + if (length && arePointsEqual(this.fPoints[length - 1], p)) { + return; + } + const point = new EndPoint(p.x, p.y); + this.fPoints.push(point); + if (this.fPoints.length < 2) { + this.curve = new Segment(this.fPoints[0].x, this.fPoints[0].y, point.x, point.y); + this.viewer.activeLayer.add(this.curve); + } else if (this.fPoints.length == 2) { + const opts = { + degree: this.degree, + cPoints: [this.fPoints[0], this.fPoints[0], this.fPoints[1], this.fPoints[1], this.fPoints[1]], + fPoints: [...this.fPoints, new EndPoint(p.x + 0.1, p.y + 0.1)], + kValues: [0, 0, 0, 1, 1, 1, 0, 0, 0], + }; + this.viewer.activeLayer.remove(this.curve); + this.curve = new BSpline(opts, true, false); + this.curve.update(); + this.viewer.activeLayer.add(this.curve); + } else { + this.curve.removeFPoint(); + this.curve.addFPoint(point); + this.curve.update(); + } + if (this.curve !== null) { + this.curve.stabilize(this.viewer); + } + this.viewer.refresh(); + } + + mousemove(e) { + if (this.curve == null) { + return; + } + const p = this.viewer.screenToModel(e); + if (this.fPoints.length < 2) { + this.curve.b.x = p.x; + this.curve.b.y = p.y; + } else { + if (this.curve.fPoints.length == this.fPoints.length) { + const point = new EndPoint(p.x, p.y); + this.curve.addFPoint(point); + } else { + this.curve.fPoints[this.fPoints.length].x = p.x; + this.curve.fPoints[this.fPoints.length].y = p.y; + } + this.curve.update(); + } + + this.viewer.refresh(); + } +}