Doodle3D-Core/src/utils/subtractShapeFromState.js

166 lines
5.8 KiB
JavaScript

import update from 'react-addons-update';
import { Matrix } from '@doodle3d/cal';
import ClipperShape from '@doodle3d/clipper-js';
import { recursiveClone } from './clone.js';
import { addObject, removeObject } from '../reducer/objectReducers.js';
import { SHAPE_TYPE_PROPERTIES } from '../constants/shapeTypeProperties.js';
import { shapeToPoints } from '../shape/shapeToPoints.js';
import { applyMatrixOnShape, pathToVectorPath } from './vectorUtils.js';
import { findEndPointIndexesOfPaths, mergePaths } from '../reducer/pointsReducers.js';
import R from 'ramda';
// import createDebug from 'debug';
// const debug = createDebug('d3d:util:substractShapeFromState');
function getShapePaths(shapeData, matrix) {
matrix = shapeData.transform.multiplyMatrix(matrix);
const shapes = shapeToPoints(shapeData)
.reduce((a, { points, holes }) => a.concat([points, ...holes]), []);
// collect all paths (transform to Scene Space)
return applyMatrixOnShape(shapes, matrix);
}
// Does the last point of the outline equal the last point?
// TODO move to pointsReducer
function isClosed(paths) {
const outline = paths[0];
const firstPoint = outline[0];
const lastPoint = outline[outline.length - 1];
return firstPoint.equals(lastPoint);
}
export default function subtractShapeFromState(state, differenceShape, tool, options = {}) {
const { matrix = new Matrix(), skipCompoundPath = false, scale, skip } = options;
const objectsToAdd = [];
if (scale !== undefined) differenceShape.scaleUp(scale);
for (const id of [...state.spaces[state.activeSpace].objectIds]) {
const shapeData = state.objectsById[id];
if (skip && skip.includes(id)) continue;
// if shape doens't interact with tool go to next shape
if (!SHAPE_TYPE_PROPERTIES[shapeData.type].tools[tool]) continue;
const paths = getShapePaths(shapeData, matrix);
if (skipCompoundPath && shapeData.fill) continue;
let shape = new ClipperShape(paths, shapeData.fill, true, false, false);
if (scale !== undefined) shape = shape.scaleUp(scale);
shape = shape.round().removeDuplicates();
// shape touched differenceShape ?
// TODO optimize: can we find a cheaper check?
const hasCollsion = shape.intersect(differenceShape).paths.length > 0;
if (!hasCollsion) continue;
const resultShape = shape.difference(differenceShape);
// Nothing left?
if (resultShape.paths.length === 0) {
state = removeObject(state, shapeData.UID);
continue;
}
// Seperate shapes, group hole shapes with their outline (shape they're a hole in).
// Returns two dimentional array, with per shape an array of paths.
// The first item of each path is the outline, the others are holes, if there are any.
let resultShapes = resultShape
.separateShapes()
.map(resultPaths => { // go through all created shapes (1 shape can be a combination of outline+holes)
if (scale !== undefined) resultPaths.scaleDown(scale);
resultPaths = resultPaths
.removeDuplicates()
.mapToLower()
.filter(path => path.length > 1)
.map(pathToVectorPath);
// Clipper never adds a line from the last point to first point,
// so we add these lines for all paths of this shape manually.
// But only when this was a filled shape.
if (shapeData.fill) {
resultPaths.forEach(path => path.push(path[0].clone()));
}
return resultPaths;
})
.filter(resultPaths => resultPaths.length > 0);
if (resultShapes.length === 0) {
state = removeObject(state, shapeData.UID);
continue;
}
if (!shapeData.fill && isClosed(paths)) {
resultShapes = mergeShapes(resultShapes);
}
// go through all (merged) shapes
for (let i = 0; i < resultShapes.length; i ++) {
const [points, ...holes] = resultShapes[i];
// try updating shape with first path.
// only possible if compount path or free hand shape
if (i === 0 && ['COMPOUND_PATH', 'FREE_HAND', 'POLYGON'].includes(shapeData.type)) {
state = update(state, {
objectsById: {
[shapeData.UID]: {
points: { $set: points },
holes: { $set: holes },
transform: { $set: matrix.inverseMatrix() }
}
}
});
} else {
// if first shape but not a compount path or free hand shape,
// we can't update so we have to remove
if (i === 0) {
state = removeObject(state, shapeData.UID);
}
objectsToAdd.push({
...recursiveClone(shapeData),
type: shapeData.fill ? 'COMPOUND_PATH' : 'FREE_HAND',
transform: matrix.inverseMatrix(),
points,
holes
});
}
}
}
for (let i = 0; i < objectsToAdd.length; i ++) {
const object = objectsToAdd[i];
state = addObject(state, object);
}
return state;
}
const getOutline = R.head();
function mergeShapes(shapes) {
// debug('mergeShapes');
// get outlines from shapes
const paths = R.map(getOutline, shapes);
// debug(' paths: ', toString(paths));
const match = findEndPointIndexesOfPaths(paths);
// debug(' match: ', toString(match));
if (match) {
const [pathAIndex, pathBIndex, matchingIndexes] = match;
const mergedPath = mergePaths(paths[pathAIndex], paths[pathBIndex], matchingIndexes); // merge path
// debug(' mergedPath: ', toString(mergedPath));
const mergedShape = [mergedPath]; // turn path into shape
// update shapes
const newShapes = R.pipe(
R.update(pathAIndex, mergedShape), // update one shape with merged shape
R.remove(pathBIndex, 1) // remove other path
)(shapes);
// if merge was found we start over
return mergeShapes(newShapes);
} else { // no merge, just return same shapes
return shapes;
}
}