mirror of
https://github.com/Doodle3D/Doodle3D-Slicer.git
synced 2024-11-22 05:37:55 +01:00
337 lines
9.7 KiB
JavaScript
337 lines
9.7 KiB
JavaScript
|
import earcut from 'earcut';
|
||
|
import { add, divide, distanceTo, normalize, subtract, normal, dot } from './src/sliceActions/helpers/vector2.js';
|
||
|
|
||
|
function lineIntersection(a1, a2, b1, b2) {
|
||
|
// source: http://mathworld.wolfram.com/Line-LineIntersection.html
|
||
|
const intersection = {
|
||
|
x: ((a1.x * a2.y - a1.y * a2.x) * (b1.x - b2.x) - (a1.x - a2.x) * (b1.x * b2.y - b1.y * b2.x)) / ((a1.x - a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x - b2.x)),
|
||
|
y: ((a1.x * a2.y - a1.y * a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x * b2.y - b1.y * b2.x)) / ((a1.x - a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x - b2.x))
|
||
|
};
|
||
|
|
||
|
const intersectionA = subtract(intersection, a1);
|
||
|
const directionA = subtract(a2, a1);
|
||
|
const normalA = normalize(directionA);
|
||
|
const distanceA = dot(normalA, intersectionA);
|
||
|
if (distanceA < 0 || distanceA > dot(normalA, directionA)) return false;
|
||
|
|
||
|
const intersectionB = subtract(intersection, b1);
|
||
|
const directionB = subtract(b2, b1);
|
||
|
const normalB = normalize(directionB);
|
||
|
const distanceB = dot(normalB, intersectionB);
|
||
|
if (distanceB < 0 || distanceB > dot(normalB, directionB)) return false;
|
||
|
|
||
|
return intersection;
|
||
|
}
|
||
|
|
||
|
function pointIsInsideConvex(point, convex, vertices) {
|
||
|
for (let i = 0; i < convex.length; i ++) {
|
||
|
const vertexA = vertices[convex[i]];
|
||
|
const vertexB = vertices[convex[(i + 1) % convex.length]];
|
||
|
|
||
|
const n = normalize(normal(subtract(vertexB, vertexA)));
|
||
|
const p = subtract(point, vertexA);
|
||
|
|
||
|
if (dot(p, n) < 0) return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
function decompose(polygon) {
|
||
|
const vertices = polygon.reduce((points, path) => {
|
||
|
points.push(...path);
|
||
|
return points;
|
||
|
}, []);
|
||
|
const flatVertices = vertices.reduce((points, { x, y }) => {
|
||
|
points.push(x, y);
|
||
|
return points;
|
||
|
}, []);
|
||
|
let offset = 0;
|
||
|
const holes = polygon
|
||
|
.map(path => offset += path.length)
|
||
|
.slice(0, -1);
|
||
|
|
||
|
const flatTrainglesIndexed = earcut(flatVertices, holes);
|
||
|
const convexPolygons = [];
|
||
|
for (let i = 0; i < flatTrainglesIndexed.length; i += 3) {
|
||
|
const face = [
|
||
|
flatTrainglesIndexed[i],
|
||
|
flatTrainglesIndexed[i + 1],
|
||
|
flatTrainglesIndexed[i + 2]
|
||
|
];
|
||
|
const center = divide(face.reduce((total, point) => {
|
||
|
if (!total) {
|
||
|
return vertices[point];
|
||
|
} else {
|
||
|
return add(total, vertices[point]);
|
||
|
}
|
||
|
}, null), face.length);
|
||
|
convexPolygons.push({
|
||
|
center,
|
||
|
face,
|
||
|
connects: []
|
||
|
});
|
||
|
}
|
||
|
|
||
|
for (let i = 0; i < convexPolygons.length; i ++) {
|
||
|
for (let j = i + 1; j < convexPolygons.length; j ++) {
|
||
|
const triangleIndexedA = convexPolygons[i];
|
||
|
const triangleIndexedB = convexPolygons[j];
|
||
|
|
||
|
const overlap = [];
|
||
|
triangleIndexedA.face.map(index => {
|
||
|
if (triangleIndexedB.face.includes(index)) overlap.push(index);
|
||
|
});
|
||
|
|
||
|
if (overlap.length === 2) {
|
||
|
const distance = distanceTo(convexPolygons[i].center, convexPolygons[j].center);
|
||
|
triangleIndexedA.connects.push({ to: j, edge: overlap, distance });
|
||
|
triangleIndexedB.connects.push({ to: i, edge: overlap, distance });
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return { vertices, convexPolygons };
|
||
|
}
|
||
|
|
||
|
function findClosestPath(convexPolygons, start, end, visited = [], path = []) {
|
||
|
if (start === end) return [];
|
||
|
|
||
|
visited = [...visited, start];
|
||
|
|
||
|
const { connects } = convexPolygons[start];
|
||
|
|
||
|
const finish = connects.find(({ to }) => to === end);
|
||
|
if (finish) return [...path, finish];
|
||
|
|
||
|
const posibilities = [];
|
||
|
for (const connect of connects) {
|
||
|
if (visited.includes(connect.to)) continue;
|
||
|
|
||
|
const posibility = findClosestPath(convexPolygons, connect.to, end, visited, [...path, connect]);
|
||
|
if (posibility) posibilities.push(posibility);
|
||
|
}
|
||
|
|
||
|
if (posibilities.length === 0) {
|
||
|
return null;
|
||
|
} else if (posibilities.length === 1) {
|
||
|
return posibilities[0];
|
||
|
} else if (posibilities.length > 1) {
|
||
|
const distanceMap = new WeakMap();
|
||
|
for (const posibility of posibilities) {
|
||
|
const distance = posibility.reduce((totalDistance, connect) => totalDistance + connect.distance, 0);
|
||
|
distanceMap.set(posibility, distance);
|
||
|
}
|
||
|
|
||
|
return posibilities.sort((a, b) => distanceMap.get(a) - distanceMap.get(b))[0];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// const parse = string => parseFloat(string);
|
||
|
// function findClosestPath(map, start, end) {
|
||
|
// // dijkstra's algorithm
|
||
|
// const costs = { [start]: 0 };
|
||
|
// const open = { [0]: [start] };
|
||
|
// const predecessors = {};
|
||
|
//
|
||
|
// while (open) {
|
||
|
// const keys = Object.keys(open).map(parse);
|
||
|
// if (keys.length === 0) break;
|
||
|
// keys.sort();
|
||
|
//
|
||
|
// const [key] = keys;
|
||
|
// const bucket = open[key];
|
||
|
// const node = bucket.shift();
|
||
|
// const currentCost = key;
|
||
|
// const { connects } = map[node];
|
||
|
//
|
||
|
// if (!bucket.length) delete open[key];
|
||
|
//
|
||
|
// for (const { distance, to } of connects) {
|
||
|
// const totalCost = distance + currentCost;
|
||
|
// const vertexCost = costs[to];
|
||
|
//
|
||
|
// if ((typeof vertexCost === 'undefined') || (vertexCost > totalCost)) {
|
||
|
// costs[to] = totalCost;
|
||
|
//
|
||
|
// if (!open[totalCost]) open[totalCost] = [];
|
||
|
// open[totalCost].push(to);
|
||
|
//
|
||
|
// predecessors[to] = node;
|
||
|
// }
|
||
|
// }
|
||
|
// }
|
||
|
//
|
||
|
// if (typeof costs[end] === 'undefined') return null;
|
||
|
//
|
||
|
// const nodes = [];
|
||
|
// let node = end;
|
||
|
// while (typeof node !== 'undefined') {
|
||
|
// nodes.push(node);
|
||
|
// node = predecessors[node];
|
||
|
// }
|
||
|
// nodes.reverse();
|
||
|
//
|
||
|
// const path = [];
|
||
|
// for (let i = 1; i < nodes.length; i ++) {
|
||
|
// const from = nodes[i - 1];
|
||
|
// const to = nodes[i];
|
||
|
//
|
||
|
// const connection = map[from].connects.find(connect => connect.to === to);
|
||
|
// path.push(connection);
|
||
|
// }
|
||
|
//
|
||
|
// return path;
|
||
|
// }
|
||
|
|
||
|
function containLineInPath(path, start, end, vertices) {
|
||
|
const line = [start];
|
||
|
|
||
|
for (const { edge: [indexA, indexB] } of path) {
|
||
|
const vertexA = vertices[indexA];
|
||
|
const vertexB = vertices[indexB];
|
||
|
|
||
|
const intersection = lineIntersection(start, end, vertexA, vertexB);
|
||
|
if (!intersection) {
|
||
|
const lastPoint = line[line.length - 1];
|
||
|
const distanceA = distanceTo(lastPoint, vertexA) + distanceTo(vertexA, end);
|
||
|
const distanceB = distanceTo(lastPoint, vertexB) + distanceTo(vertexB, end);
|
||
|
|
||
|
line.push(distanceA < distanceB ? vertexA : vertexB);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
line.push(end);
|
||
|
return line;
|
||
|
}
|
||
|
|
||
|
const canvas = document.createElement('canvas');
|
||
|
document.body.appendChild(canvas);
|
||
|
canvas.width = 610;
|
||
|
canvas.height = 610;
|
||
|
const context = canvas.getContext('2d');
|
||
|
context.lineJoin = 'bevel';
|
||
|
|
||
|
function circle(radius = 10, x = 0, y = 0, clockWise = true, segments = 40) {
|
||
|
const shape = [];
|
||
|
|
||
|
for (let rad = 0; rad < Math.PI * 2; rad += Math.PI * 2 / segments) {
|
||
|
if (clockWise) {
|
||
|
shape.push({ x: Math.cos(rad) * radius + x, y: Math.sin(rad) * radius + y });
|
||
|
} else {
|
||
|
shape.push({ x: Math.cos(rad) * radius + x, y: -Math.sin(rad) * radius + y });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return shape;
|
||
|
}
|
||
|
|
||
|
const START = { x: 300, y: 300 };
|
||
|
const END = { x: 300, y: 20 };
|
||
|
// const CONCAVE_POLYGON = [[
|
||
|
// { x: 10, y: 10 },
|
||
|
// { x: 600, y: 10 },
|
||
|
// { x: 500, y: 200 },
|
||
|
// { x: 600, y: 600 },
|
||
|
// { x: 10, y: 600 }
|
||
|
// ], [
|
||
|
// { x: 160, y: 120 },
|
||
|
// { x: 120, y: 400 },
|
||
|
// { x: 400, y: 400 }
|
||
|
// ], circle(50, 300, 100, false)];
|
||
|
const CONCAVE_POLYGON = [circle(300, 305, 305, true, 100), circle(50, 300, 100, false)];
|
||
|
|
||
|
canvas.onmousedown = (event) => {
|
||
|
START.x = event.offsetX;
|
||
|
START.y = event.offsetY;
|
||
|
compute();
|
||
|
};
|
||
|
canvas.onmousemove = (event) => {
|
||
|
END.x = event.offsetX;
|
||
|
END.y = event.offsetY;
|
||
|
compute();
|
||
|
};
|
||
|
compute();
|
||
|
|
||
|
function compute() {
|
||
|
const { convexPolygons, vertices } = decompose(CONCAVE_POLYGON);
|
||
|
const startPolygon = convexPolygons.findIndex(({ face }) => pointIsInsideConvex(START, face, vertices));
|
||
|
const endPolygon = convexPolygons.findIndex(({ face }) => pointIsInsideConvex(END, face, vertices));
|
||
|
if (startPolygon === -1 || endPolygon === -1) return;
|
||
|
|
||
|
const path = findClosestPath(convexPolygons, startPolygon, endPolygon);
|
||
|
if (!path) return;
|
||
|
const line = containLineInPath(path, START, END, vertices);
|
||
|
|
||
|
// draw
|
||
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
|
||
|
context.beginPath();
|
||
|
for (const shape of CONCAVE_POLYGON) {
|
||
|
let first = true;
|
||
|
for (const { x, y } of shape) {
|
||
|
if (first) {
|
||
|
context.moveTo(x, y);
|
||
|
} else {
|
||
|
context.lineTo(x, y);
|
||
|
}
|
||
|
first = false;
|
||
|
}
|
||
|
}
|
||
|
context.closePath();
|
||
|
context.fillStyle = 'lightgray';
|
||
|
context.fill();
|
||
|
|
||
|
// context.fillStyle = 'black';
|
||
|
// context.strokeStyle = 'black';
|
||
|
// context.textAlign = 'center';
|
||
|
// context.textBaseline = 'middle';
|
||
|
// context.lineWidth = 1;
|
||
|
// context.font = '14px arial';
|
||
|
// for (let i = 0; i < convexPolygons.length; i ++) {
|
||
|
// const { face, center } = convexPolygons[i];
|
||
|
//
|
||
|
// context.beginPath();
|
||
|
// for (const index of face) {
|
||
|
// const vertex = vertices[index];
|
||
|
// context.lineTo(vertex.x, vertex.y);
|
||
|
// }
|
||
|
// context.closePath();
|
||
|
// context.stroke();
|
||
|
//
|
||
|
// context.fillText(i, center.x, center.y);
|
||
|
// }
|
||
|
|
||
|
// if (path) {
|
||
|
// context.beginPath();
|
||
|
// for (const { edge: [indexA, indexB] } of path) {
|
||
|
// const pointA = vertices[indexA];
|
||
|
// const pointB = vertices[indexB];
|
||
|
// context.moveTo(pointA.x, pointA.y);
|
||
|
// context.lineTo(pointB.x, pointB.y);
|
||
|
// }
|
||
|
// context.strokeStyle = 'blue';
|
||
|
// context.lineWidth = 3;
|
||
|
// context.stroke();
|
||
|
// }
|
||
|
|
||
|
if (line) {
|
||
|
context.beginPath();
|
||
|
for (const point of line) {
|
||
|
context.lineTo(point.x, point.y);
|
||
|
}
|
||
|
context.strokeStyle = 'green';
|
||
|
context.lineWidth = 2;
|
||
|
context.stroke();
|
||
|
}
|
||
|
|
||
|
context.beginPath();
|
||
|
context.arc(START.x, START.y, 3, 0, Math.PI * 2);
|
||
|
context.fillStyle = 'blue';
|
||
|
context.fill();
|
||
|
|
||
|
context.beginPath();
|
||
|
context.arc(END.x, END.y, 3, 0, Math.PI * 2);
|
||
|
context.fillStyle = 'red';
|
||
|
context.fill();
|
||
|
}
|