mirror of
https://github.com/Doodle3D/Doodle3D-Core.git
synced 2025-04-20 17:56:24 +02:00
258 lines
9.8 KiB
JavaScript
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;
|
|
}
|
|
}
|