Doodle3D-Core/src/reducer/d2/tools/penReducer.js
2017-11-14 16:09:12 +01:00

258 lines
9.8 KiB
JavaScript

import update from 'react-addons-update';
import * as actions from '../../../actions/index.js';
import createDebug from 'debug';
import { removeObject, addObjectActive2D, setActive2D } from '../../../reducer/objectReducers.js';
import { SNAPPING_DISTANCE, LINE_WIDTH } from '../../../constants/d2Constants.js';
import { SHAPE_TYPE_PROPERTIES } from '../../../constants/shapeTypeProperties.js';
import { getSnappingPoints } from '../../../utils/objectSelectors.js';
import { fitPath, segmentBezierPath } from '../../../utils/curveUtils.js';
import * as tools from '../../../constants/d2Tools.js';
const debug = createDebug('d3d:reducer:pen');
const LINE_DIAMETER = LINE_WIDTH / 2.0;
const FREE_HAND = 'FREE_HAND';
const POLYGON = 'POLYGON';
const BRUSH = 'BRUSH';
export default function penReducer(state, action) {
if (action.log !== false && action.type) debug(action.type);
const screenPosition = action.position;
const activeShape = state.d2.activeShape;
const activeTool = state.d2.tool;
let screenMatrixZoom;
let screenMatrixZoomInverse;
if (action.screenMatrixZoom) {
screenMatrixZoom = action.screenMatrixZoom;
screenMatrixZoomInverse = action.screenMatrixZoom.inverseMatrix();
}
switch (action.type) {
case actions.D2_DRAG_START: {
const preDrags = action.preDrags.map((preDrag) => preDrag.applyMatrix(screenMatrixZoomInverse));
switch (activeTool) {
case tools.POLYGON: {
return addObjectActive2D(state, {
type: POLYGON,
// draw straight line between drag start and current position
points: [preDrags[0], preDrags[preDrags.length - 1]]
});
}
case tools.BRUSH: {
return addObjectActive2D(state, {
type: BRUSH,
strokeWidth: state.d2.brush.size,
points: preDrags
});
}
case tools.FREE_HAND: {
return addObjectActive2D(state, {
type: FREE_HAND,
points: preDrags
});
}
default:
return state;
}
}
case actions.D2_DRAG: {
if (activeShape) {
const shapeData = state.objectsById[activeShape];
const points = shapeData.points;
const matrix = shapeData.transform.multiplyMatrix(screenMatrixZoom).inverseMatrix();
const position = screenPosition.applyMatrix(matrix);
switch (activeTool) {
case tools.POLYGON: {
return update(state, {
objectsById: {
[activeShape]: {
points: { [points.length - 1]: { $set: position } }
}
}
}); // update last point to curren mouse position
}
case tools.BRUSH:
case tools.FREE_HAND: {
state = update(state, {
objectsById: {
[activeShape]: {
points: { $push: [position] }
}
}
}); // add current mouse position to points array
}
default:
return state;
}
}
return state;
}
case actions.D2_DRAG_END: {
if (activeShape) {
state = setActive2D(state, null);
const currentShapeData = state.objectsById[activeShape];
const shapeProps = SHAPE_TYPE_PROPERTIES[currentShapeData.type];
if (!shapeProps.snapping) return state;
let currentPoints = currentShapeData.points;
// smooth
if (activeTool === tools.FREE_HAND) {
// convert to screen space
currentPoints = currentPoints.map(point => point.applyMatrix(screenMatrixZoom));
// smooth path
currentPoints = segmentBezierPath(fitPath(currentPoints));
// convert back to object space
currentPoints = currentPoints.map(point => point.applyMatrix(screenMatrixZoomInverse));
}
const currentStartPoint = currentPoints[0].applyMatrix(screenMatrixZoom);
const currentEndPoint = currentPoints[currentPoints.length - 1].applyMatrix(screenMatrixZoom);
// create list of possible snapping connections
// sorts on distance to connection and filters out connections that exceed the snapping threshold
const snappingDistance = SNAPPING_DISTANCE + LINE_DIAMETER * screenMatrixZoom.sx;
const snappingPoints = getSnappingPoints(state, screenMatrixZoom)
.reduce((points, { shapeData, endPoint, startPoint }) => {
if (shapeData.UID === activeShape) {
if (currentPoints.length >= 3) {
points.push({ shapeData, hit: 'self', distance: endPoint.distanceTo(startPoint) });
}
} else {
points.push(
{ shapeData, hit: 'start-start', distance: currentStartPoint.distanceTo(startPoint) },
{ shapeData, hit: 'start-end', distance: currentStartPoint.distanceTo(endPoint) },
{ shapeData, hit: 'end-start', distance: currentEndPoint.distanceTo(startPoint) },
{ shapeData, hit: 'end-end', distance: currentEndPoint.distanceTo(endPoint) }
);
}
return points;
}, [])
.filter(({ distance }) => distance < snappingDistance)
.sort((a, b) => a.distance - b.distance);
// the active shape's start and end points can only be connected to one other shape,
// this variable can be used to check if the start or end point is already been snapped to
// when the point has a connection it stores the shape UID of the connected shape
let currentEndPointHit = false;
let currentStartPointHit = false;
for (const { hit, shapeData } of snappingPoints) {
let newPoints;
if (hit !== 'self') {
const newMatrix = shapeData.transform.multiplyMatrix(currentShapeData.transform.inverseMatrix());
newPoints = shapeData.points.map(point => point.applyMatrix(newMatrix));
}
if (hit === 'start-start' || hit === 'end-end') newPoints.reverse();
switch (hit) {
case 'self':
// can only connect to self if end point AND start point aren't connected yet
if (!currentEndPointHit && !currentStartPointHit) {
currentEndPointHit = shapeData.UID;
currentStartPointHit = shapeData.UID;
currentPoints = update(currentPoints, {
[currentPoints.length - 1]: { $set: currentPoints[0].clone() }
});
}
break;
case 'start-start':
case 'start-end':
// can only connect start point if start point isn't connected yet
if (!currentStartPointHit) {
currentStartPointHit = shapeData.UID;
if (currentEndPointHit === shapeData.UID) {
// if the end point is connected to the same shape it has already been added to
// the end of the shape and does not have to be added to the start of the shape
if (activeTool === tools.FREE_HAND) {
currentPoints = update(currentPoints, {
$push: [currentPoints[0].clone()]
});
} else if (activeTool === tools.POLYGON) {
currentPoints = update(currentPoints, {
[currentPoints.length - 1]: { $set: currentPoints[0].clone() }
});
}
} else {
// add the shape to the start of the active shape
if (activeTool === tools.FREE_HAND) {
currentPoints = update(currentPoints, {
$splice: [[0, 0, ...newPoints]]
});
} else if (activeTool === tools.POLYGON) {
currentPoints = update(currentPoints, {
$splice: [[0, 1, ...newPoints]]
});
}
}
if (state.objectsById[shapeData.UID]) state = removeObject(state, shapeData.UID);
}
break;
case 'end-start':
case 'end-end':
if (!currentEndPointHit) {
currentEndPointHit = shapeData.UID;
if (currentStartPointHit === shapeData.UID) {
// if the start point is connected to the same shape it has already been added to
// the start of the shape and does not have to be added to the end of the shape
if (activeTool === tools.FREE_HAND) {
currentPoints = update(currentPoints, {
$push: [currentPoints[0].clone()]
});
} else if (activeTool === tools.POLYGON) {
currentPoints = update(currentPoints, {
[currentPoints.length - 1]: { $set: currentPoints[0].clone() }
});
}
} else {
// add the shape to the end of the active shape
if (activeTool === tools.FREE_HAND) {
currentPoints = update(currentPoints, {
$push: newPoints
});
} else if (activeTool === tools.POLYGON) {
currentPoints = update(currentPoints, {
$splice: [[currentPoints.length - 1, 1, ...newPoints]]
});
}
}
if (state.objectsById[shapeData.UID]) state = removeObject(state, shapeData.UID);
}
break;
default:
break;
}
}
state = update(state, {
objectsById: {
[activeShape]: {
points: { $set: currentPoints }
}
}
});
}
return state;
}
default:
return state;
}
}