This commit is contained in:
zhengxuhan 2025-08-19 13:19:44 +08:00 committed by GitHub
commit eed90fdc8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1434 additions and 18 deletions

View file

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

View file

@ -413,4 +413,4 @@ var cg = function(A, x, b, tol, maxIt) {
var dogleg = {DEBUG_HANDLER : function() {}}; //backward compatibility
export {dog_leg, dogleg}
export {dog_leg, dogleg, lu_solve}

View file

@ -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 );

View file

@ -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 pushVoxel(root, baseSize, [vx, vy, vz], tag) {
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, 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;

View file

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

139
modules/voxels/voxelBool.ts Normal file
View file

@ -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))
}

View file

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

View file

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

View file

@ -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',

View file

@ -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<number, IPolynomialFunc> = 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);
}
}

View file

@ -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();
}
}