mirror of
https://github.com/Doodle3D/Doodle3D-Core.git
synced 2024-12-22 19:13:49 +01:00
add actions and reducer
This commit is contained in:
parent
ed5c3979d3
commit
2be3bc6362
33
package-lock.json
generated
33
package-lock.json
generated
@ -4408,6 +4408,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ramda": {
|
||||||
|
"version": "0.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.25.0.tgz",
|
||||||
|
"integrity": "sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ=="
|
||||||
|
},
|
||||||
"randomatic": {
|
"randomatic": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
|
||||||
@ -4487,6 +4492,15 @@
|
|||||||
"prop-types": "15.6.0"
|
"prop-types": "15.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-addons-update": {
|
||||||
|
"version": "15.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-addons-update/-/react-addons-update-15.6.2.tgz",
|
||||||
|
"integrity": "sha1-5TdTxbNIh5dFEMiC1/sHWFHV5QQ=",
|
||||||
|
"requires": {
|
||||||
|
"fbjs": "0.8.16",
|
||||||
|
"object-assign": "4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-jss": {
|
"react-jss": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-jss/-/react-jss-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-jss/-/react-jss-8.0.0.tgz",
|
||||||
@ -4547,6 +4561,17 @@
|
|||||||
"set-immediate-shim": "1.0.1"
|
"set-immediate-shim": "1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"redux": {
|
||||||
|
"version": "3.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
|
||||||
|
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
|
||||||
|
"requires": {
|
||||||
|
"lodash": "4.17.4",
|
||||||
|
"lodash-es": "4.17.4",
|
||||||
|
"loose-envify": "1.3.1",
|
||||||
|
"symbol-observable": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"redux-form": {
|
"redux-form": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/redux-form/-/redux-form-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/redux-form/-/redux-form-7.1.2.tgz",
|
||||||
@ -4562,6 +4587,14 @@
|
|||||||
"prop-types": "15.6.0"
|
"prop-types": "15.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"redux-undo": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-undo/-/redux-undo-0.6.1.tgz",
|
||||||
|
"integrity": "sha1-/0B3Sbj0aL6tY25BOBnpLAmC7so=",
|
||||||
|
"requires": {
|
||||||
|
"redux": "3.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"regenerate": {
|
"regenerate": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz",
|
||||||
|
@ -28,12 +28,15 @@
|
|||||||
"pouchdb": "^6.3.4",
|
"pouchdb": "^6.3.4",
|
||||||
"proptypes": "^1.1.0",
|
"proptypes": "^1.1.0",
|
||||||
"raf": "^3.4.0",
|
"raf": "^3.4.0",
|
||||||
|
"ramda": "^0.25.0",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"react": "^16.0.0",
|
"react": "^16.0.0",
|
||||||
|
"react-addons-update": "^15.6.2",
|
||||||
"react-jss": "^8.0.0",
|
"react-jss": "^8.0.0",
|
||||||
"react-redux": "^5.0.6",
|
"react-redux": "^5.0.6",
|
||||||
"react-resize-detector": "^1.1.0",
|
"react-resize-detector": "^1.1.0",
|
||||||
"redux-form": "^7.1.2",
|
"redux-form": "^7.1.2",
|
||||||
|
"redux-undo": "^0.6.1",
|
||||||
"regenerator-runtime": "^0.11.0",
|
"regenerator-runtime": "^0.11.0",
|
||||||
"semver": "^5.4.1",
|
"semver": "^5.4.1",
|
||||||
"shortid": "^2.2.8",
|
"shortid": "^2.2.8",
|
||||||
|
@ -3,5 +3,7 @@ import * as utils from './utils/index.js';
|
|||||||
import * as d3 from './d3/index.js';
|
import * as d3 from './d3/index.js';
|
||||||
import * as components from './components/index.js';
|
import * as components from './components/index.js';
|
||||||
import * as constants from './constants/index.js';
|
import * as constants from './constants/index.js';
|
||||||
|
import * as actions from './actions/index.js';
|
||||||
|
import * as reducer from './reducer/index.js';
|
||||||
|
|
||||||
export { shape, utils, d3, components, constants };
|
export { shape, utils, d3, components, constants, actions, reducer };
|
||||||
|
15
src/reducer/d2/addImageReducer.js
Normal file
15
src/reducer/d2/addImageReducer.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { CANVAS_SIZE, INITIAL_IMAGE_SCALE } from '../../constants/d2Constants.js';
|
||||||
|
import { Matrix } from 'cal';
|
||||||
|
import { addObject } from '../objectReducers.js';
|
||||||
|
|
||||||
|
const IMAGE_SIZE = CANVAS_SIZE * 2 * INITIAL_IMAGE_SCALE;
|
||||||
|
|
||||||
|
export default function addImageReducer(state, action) {
|
||||||
|
const { payload: imageData } = action;
|
||||||
|
|
||||||
|
const scale = Math.min(IMAGE_SIZE / imageData.width, IMAGE_SIZE / imageData.height);
|
||||||
|
const transform = new Matrix();
|
||||||
|
transform.scale = scale;
|
||||||
|
|
||||||
|
return addObject(state, { type: 'IMAGE_GUIDE', imageData, transform });
|
||||||
|
}
|
15
src/reducer/d2/constrainMatrix.js
Normal file
15
src/reducer/d2/constrainMatrix.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Vector, Utils as CALUtils } from 'cal';
|
||||||
|
import { MIN_ZOOM, MAX_ZOOM, CANVAS_SIZE } from '../../constants/d2Constants.js';
|
||||||
|
|
||||||
|
export default function constrainMatrix(matrix) {
|
||||||
|
const scale = matrix.sx;
|
||||||
|
const scaleClamped = CALUtils.MathExtended.clamb(scale, MIN_ZOOM, MAX_ZOOM);
|
||||||
|
matrix.scale = scaleClamped;
|
||||||
|
|
||||||
|
const pan = new Vector().copy(matrix).scale(scaleClamped / scale);
|
||||||
|
const maxTranslate = (CANVAS_SIZE / MIN_ZOOM - CANVAS_SIZE / scaleClamped) * scaleClamped;
|
||||||
|
|
||||||
|
matrix.x = CALUtils.MathExtended.clamb(pan.x, -maxTranslate, maxTranslate);
|
||||||
|
matrix.y = CALUtils.MathExtended.clamb(pan.y, -maxTranslate, maxTranslate);
|
||||||
|
matrix.rotation = 0;
|
||||||
|
}
|
18
src/reducer/d2/panReducer.js
Normal file
18
src/reducer/d2/panReducer.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import constrainMatrix from './constrainMatrix.js';
|
||||||
|
|
||||||
|
export default function d2PanReducer(state, action) {
|
||||||
|
let { canvasMatrix } = state.d2;
|
||||||
|
const matrix = action.screenMatrixContainer.normalize().inverseMatrix();
|
||||||
|
const delta = action.position.subtract(action.previousPosition).applyMatrix(matrix);
|
||||||
|
|
||||||
|
canvasMatrix = canvasMatrix.translate(delta.x, delta.y);
|
||||||
|
|
||||||
|
constrainMatrix(canvasMatrix);
|
||||||
|
|
||||||
|
return update(state, {
|
||||||
|
d2: {
|
||||||
|
canvasMatrix: { $set: canvasMatrix }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
41
src/reducer/d2/pinchZoomReducer.js
Normal file
41
src/reducer/d2/pinchZoomReducer.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import { Matrix } from 'cal';
|
||||||
|
import constrainMatrix from './constrainMatrix.js';
|
||||||
|
import { calculateGestureMatrix } from '../../utils/matrixUtils.js';
|
||||||
|
import * as actions from '../../actions/index.js';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
const debug = createDebug('d3d:reducer:d2:pinchZoom');
|
||||||
|
|
||||||
|
export default function pinchZoomReducer(state, action) {
|
||||||
|
if (action.log !== false) debug(action.type);
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.CLEAR:
|
||||||
|
return update(state, {
|
||||||
|
d2: { canvasMatrix: { $set: new Matrix() } }
|
||||||
|
});
|
||||||
|
|
||||||
|
case actions.D2_MULTITOUCH:
|
||||||
|
return multitouch(state, action);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function multitouch(state, { positions, previousPositions, screenMatrixContainer }) {
|
||||||
|
const gestureMatrix = calculateGestureMatrix(positions, previousPositions, screenMatrixContainer, {
|
||||||
|
rotate: false, scale: true, pan: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const { canvasMatrix } = state.d2;
|
||||||
|
const matrix = canvasMatrix.multiplyMatrix(gestureMatrix);
|
||||||
|
|
||||||
|
constrainMatrix(matrix);
|
||||||
|
|
||||||
|
return update(state, {
|
||||||
|
d2: {
|
||||||
|
canvasMatrix: { $set: matrix }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
85
src/reducer/d2/toolReducer.js
Normal file
85
src/reducer/d2/toolReducer.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import circleReducer from './tools/shapes/circleReducer.js';
|
||||||
|
import circleSegmentReducer from './tools/shapes/circleSegmentReducer.js';
|
||||||
|
import rectReducer from './tools/shapes/rectReducer.js';
|
||||||
|
import polyPointReducer from './tools/shapes/polyPointReducer.js';
|
||||||
|
import heartReducer from './tools/shapes/heartReducer.js';
|
||||||
|
import starReducer from './tools/shapes/starReducer.js';
|
||||||
|
import triangleReducer from './tools/shapes/triangleReducer.js';
|
||||||
|
import bucketReducer from './tools/bucketReducer.js';
|
||||||
|
import penReducer from './tools/penReducer.js';
|
||||||
|
import textReducer from './tools/textReducer.js';
|
||||||
|
import photoGuideReducer from './tools/photoGuideReducer.js';
|
||||||
|
import { transformReducer } from './tools/transformReducer.js';
|
||||||
|
import eraserReducer from './tools/eraserReducer.js';
|
||||||
|
import * as actions from '../../actions/index.js';
|
||||||
|
import * as tools from '../../constants/d2Tools.js';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
const debug = createDebug('d3d:reducer:d2:tool');
|
||||||
|
|
||||||
|
const reducers = {
|
||||||
|
[tools.CIRCLE]: circleReducer,
|
||||||
|
[tools.CIRCLE_SEGMENT]: circleSegmentReducer,
|
||||||
|
[tools.FREE_HAND]: penReducer,
|
||||||
|
[tools.RECT]: rectReducer,
|
||||||
|
[tools.STAR]: starReducer,
|
||||||
|
[tools.TRIANGLE]: triangleReducer,
|
||||||
|
[tools.POLY_POINT]: polyPointReducer,
|
||||||
|
[tools.HEART]: heartReducer,
|
||||||
|
[tools.TRANSFORM]: transformReducer,
|
||||||
|
[tools.ERASER]: eraserReducer,
|
||||||
|
[tools.POLYGON]: penReducer,
|
||||||
|
[tools.PHOTO_GUIDE]: photoGuideReducer,
|
||||||
|
[tools.BUCKET]: bucketReducer,
|
||||||
|
[tools.TEXT]: textReducer,
|
||||||
|
[tools.BRUSH]: penReducer
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function toolReducer(state, action) {
|
||||||
|
// if (action.log !== false) debug(action.type);
|
||||||
|
|
||||||
|
// change 2D tool after explicit tool change action or on some selection
|
||||||
|
if (action.type === actions.D2_CHANGE_TOOL) {
|
||||||
|
state = updateTool(state, action.tool);
|
||||||
|
}
|
||||||
|
if (action.category === actions.CAT_SELECTION) {
|
||||||
|
state = updateTool(state, tools.TRANSFORM);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.D2_DRAG_START:
|
||||||
|
state = update(state, {
|
||||||
|
d2: { dragging: { $set: true } }
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case actions.D2_DRAG_END:
|
||||||
|
state = update(state, {
|
||||||
|
d2: { dragging: { $set: false } }
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = state.d2.tool;
|
||||||
|
const reducer = reducers[tool];
|
||||||
|
if (reducer) {
|
||||||
|
return reducer(state, action);
|
||||||
|
} else {
|
||||||
|
if (action.log !== false) debug('Unkown 2D tool: ', tool);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTool(state, newTool) {
|
||||||
|
if (newTool === state.d2.tool) {
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
debug('d2 tool: ', newTool);
|
||||||
|
return update(state, {
|
||||||
|
d2: { tool: { $set: newTool } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
130
src/reducer/d2/tools/bucketReducer.js
Normal file
130
src/reducer/d2/tools/bucketReducer.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import * as actions from '../../../actions/index.js';
|
||||||
|
import { SHAPE_TYPE_PROPERTIES } from '../../../constants/shapeTypeProperties.js';
|
||||||
|
import { LINE_WIDTH, CLIPPER_PRECISION } from '../../../constants/d2Constants.js';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
import { shapeToPoints, applyMatrixOnShape, pathToVectorPath } from '../../../utils/shapeDataUtils.js';
|
||||||
|
import { addObject } from '../../../reducers/objectReducers.js';
|
||||||
|
import fillPath from 'fill-path';
|
||||||
|
import ClipperShape from 'clipper-js';
|
||||||
|
import subtractShapeFromState from '../../../utils/subtractShapeFromState.js';
|
||||||
|
import update from 'react-addons-update';
|
||||||
|
import { get as getConfig } from '../../../services/config.js';
|
||||||
|
import { getColor, getFirst, filterType, getObjectsFromIds } from '../../../utils/objectSelectors.js';
|
||||||
|
|
||||||
|
const debug = createDebug('d3d:reducer:bucket');
|
||||||
|
|
||||||
|
const MITER_LIMIT = 30.0;
|
||||||
|
|
||||||
|
export default function bucketReducer(state, action) {
|
||||||
|
if (action.log !== false) debug(action.type);
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.D2_TAP:
|
||||||
|
const { experimentalColorPicker } = getConfig();
|
||||||
|
|
||||||
|
let color = state.context.color;
|
||||||
|
if (experimentalColorPicker) {
|
||||||
|
const imageColor = getColor(
|
||||||
|
getFirst(
|
||||||
|
filterType(
|
||||||
|
getObjectsFromIds(state, action.objects),
|
||||||
|
'IMAGE_GUIDE'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
action.position,
|
||||||
|
action.screenMatrixZoom
|
||||||
|
);
|
||||||
|
if (imageColor !== null) color = imageColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if clicked on a filled shape change shape color
|
||||||
|
const filledPathIndex = action.objects.findIndex(id => (
|
||||||
|
state.objectsById[id].fill &&
|
||||||
|
SHAPE_TYPE_PROPERTIES[state.objectsById[id].type].tools[state.d2.tool]
|
||||||
|
));
|
||||||
|
if (filledPathIndex !== -1) {
|
||||||
|
const id = action.objects[filledPathIndex];
|
||||||
|
return update(state, {
|
||||||
|
objectsById: { [id]: { color: { $set: color } } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise fill paths
|
||||||
|
const {
|
||||||
|
screenMatrixContainer,
|
||||||
|
screenMatrixZoom
|
||||||
|
} = action;
|
||||||
|
|
||||||
|
// convert mouse position to container space
|
||||||
|
// discard screen matrix zoom and apply screen matrix container
|
||||||
|
const matrix = screenMatrixZoom.inverseMatrix().multiplyMatrix(screenMatrixContainer);
|
||||||
|
const position = action.position.applyMatrix(matrix);
|
||||||
|
|
||||||
|
const paths = getPaths(state, screenMatrixContainer);
|
||||||
|
|
||||||
|
const result = fillPath(paths, position, {
|
||||||
|
lineWidth: LINE_WIDTH,
|
||||||
|
miterLimit: MITER_LIMIT,
|
||||||
|
fillOffset: 'outside'
|
||||||
|
});
|
||||||
|
// TODO
|
||||||
|
// reduce number of points of result, sometimes result has 900+ points
|
||||||
|
|
||||||
|
if (result.length === 0) return state;
|
||||||
|
|
||||||
|
const fillPaths = result
|
||||||
|
.map(pathToVectorPath)
|
||||||
|
.map(path => {
|
||||||
|
path.push(path[0].clone());
|
||||||
|
return path;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fillShape = new ClipperShape(fillPaths, true, true, true)
|
||||||
|
.offset(LINE_WIDTH / 2.0, {
|
||||||
|
joinType: 'jtMiter',
|
||||||
|
endType: 'etClosedPolygon',
|
||||||
|
miterLimit: MITER_LIMIT
|
||||||
|
});
|
||||||
|
|
||||||
|
state = subtractShapeFromState(state, fillShape, state.d2.tool, {
|
||||||
|
matrix: screenMatrixContainer,
|
||||||
|
skipCompoundPath: true,
|
||||||
|
scale: CLIPPER_PRECISION
|
||||||
|
});
|
||||||
|
|
||||||
|
return addCompoundPathToState(state, fillPaths, screenMatrixContainer.inverseMatrix(), color);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPaths(state, screenMatrixContainer) {
|
||||||
|
const paths = [];
|
||||||
|
|
||||||
|
for (const id of state.spaces[state.activeSpace].objectIds) {
|
||||||
|
const shapeData = state.objectsById[id];
|
||||||
|
|
||||||
|
if (!SHAPE_TYPE_PROPERTIES[shapeData.type].tools[state.d2.tool]) continue;
|
||||||
|
|
||||||
|
const shapes = shapeToPoints(shapeData);
|
||||||
|
for (let i = 0; i < shapes.length; i ++) {
|
||||||
|
const { points, holes } = shapes[i];
|
||||||
|
const matrix = shapeData.transform.multiplyMatrix(screenMatrixContainer);
|
||||||
|
const shape = applyMatrixOnShape([points, ...holes], matrix);
|
||||||
|
|
||||||
|
paths.push(...shape);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCompoundPathToState(state, paths, transform, color) {
|
||||||
|
const [points, ...holes] = paths;
|
||||||
|
return addObject(state, {
|
||||||
|
type: 'COMPOUND_PATH',
|
||||||
|
transform,
|
||||||
|
points,
|
||||||
|
holes,
|
||||||
|
color
|
||||||
|
});
|
||||||
|
}
|
35
src/reducer/d2/tools/eraserReducer.js
Normal file
35
src/reducer/d2/tools/eraserReducer.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import * as actions from '../../../actions/index';
|
||||||
|
import ClipperShape from 'clipper-js';
|
||||||
|
import * as d2Tools from '../../../constants/d2Tools';
|
||||||
|
import subtractShapeFromState from '../../../utils/subtractShapeFromState';
|
||||||
|
import { CLIPPER_PRECISION } from '../../../constants/d2Constants.js';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:reducer:eraser');
|
||||||
|
|
||||||
|
export default function eraserReducer(state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.D2_TAP:
|
||||||
|
return handleEraser(state, [action.position], action.screenMatrixZoom);
|
||||||
|
|
||||||
|
case actions.D2_DRAG_START:
|
||||||
|
return handleEraser(state, action.preDrags, action.screenMatrixZoom);
|
||||||
|
|
||||||
|
case actions.D2_DRAG:
|
||||||
|
return handleEraser(state, [action.previousPosition, action.position], action.screenMatrixZoom);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEraser(state, path, screenMatrixZoom) {
|
||||||
|
// create eraser line from prev mouse position to new mouse position
|
||||||
|
const eraserShape = new ClipperShape([path], false, true, true).offset(state.d2.eraser.size, {
|
||||||
|
jointType: 'jtRound',
|
||||||
|
endType: 'etOpenRound',
|
||||||
|
miterLimit: 2.0,
|
||||||
|
roundPrecision: 0.25
|
||||||
|
});
|
||||||
|
|
||||||
|
return subtractShapeFromState(state, eraserShape, d2Tools.ERASER, { matrix: screenMatrixZoom, scale: CLIPPER_PRECISION });
|
||||||
|
}
|
257
src/reducer/d2/tools/penReducer.js
Normal file
257
src/reducer/d2/tools/penReducer.js
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as actions from '../../../actions/index.js';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
import { removeObject, addObjectActive2D, setActive2D } from '../../../reducers/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;
|
||||||
|
}
|
||||||
|
}
|
62
src/reducer/d2/tools/photoGuideReducer.js
Normal file
62
src/reducer/d2/tools/photoGuideReducer.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as actions from '../../../actions/index.js';
|
||||||
|
import { addObject } from '../../../reducers/objectReducers.js';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
const debug = createDebug('d3d:reducer:photoGuide');
|
||||||
|
|
||||||
|
export default function photoGuideReducer(state, action) {
|
||||||
|
if (action.log !== false) debug(action.type);
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.TRACE_DRAG: {
|
||||||
|
return update(state, {
|
||||||
|
d2: {
|
||||||
|
activeShape: { $set: action.id },
|
||||||
|
trace: {
|
||||||
|
active: { $set: true },
|
||||||
|
start: { $set: action.start.clone() },
|
||||||
|
position: { $set: action.position.clone() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case `${actions.FLOOD_FILL}_FULFILLED`: {
|
||||||
|
return update(state, {
|
||||||
|
d2: {
|
||||||
|
trace: {
|
||||||
|
floodFillData: { $set: action.payload }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case `${actions.TRACE_FLOOD_FILL}_FULFILLED`: {
|
||||||
|
const paths = action.payload;
|
||||||
|
|
||||||
|
for (const points of paths) {
|
||||||
|
state = addObject(state, {
|
||||||
|
type: 'FREE_HAND',
|
||||||
|
points
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.TRACE_DRAG_END: {
|
||||||
|
state = update(state, {
|
||||||
|
d2: {
|
||||||
|
activeShape: { $set: null },
|
||||||
|
trace: {
|
||||||
|
active: { $set: false },
|
||||||
|
floodFillData: { $set: { edge: [], fill: [] } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
75
src/reducer/d2/tools/shapes/circleReducer.js
Normal file
75
src/reducer/d2/tools/shapes/circleReducer.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import { Matrix, Vector } from 'cal';
|
||||||
|
import * as actions from '../../../../actions/index.js';
|
||||||
|
import { addObjectActive2D, addObject, setActive2D } from '../../../objectReducers.js';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
const debug = createDebug('d3d:reducer:circle');
|
||||||
|
|
||||||
|
const DEFAULT_RADIUS = 25;
|
||||||
|
const DEFAULT_SEGMENT = Math.PI * 0.5; // for segmented circle
|
||||||
|
const MAX_SEGMENT = Math.PI * 2;
|
||||||
|
|
||||||
|
export default function circleReducer(state, action, segmented = false) {
|
||||||
|
if (action.log !== false) debug(action.type);
|
||||||
|
const { position, screenMatrixZoom } = action;
|
||||||
|
let scenePosition;
|
||||||
|
if (position !== undefined && screenMatrixZoom !== undefined) {
|
||||||
|
scenePosition = position.applyMatrix(screenMatrixZoom.inverseMatrix());
|
||||||
|
}
|
||||||
|
const activeShape = state.d2.activeShape;
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.D2_DRAG_START: {
|
||||||
|
return addObjectActive2D(state, {
|
||||||
|
type: (segmented ? 'CIRCLE_SEGMENT' : 'CIRCLE'),
|
||||||
|
transform: new Matrix({ x: scenePosition.x, y: scenePosition.y })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case actions.D2_DRAG: {
|
||||||
|
if (activeShape) {
|
||||||
|
const beginPos = new Vector().copy(state.objectsById[activeShape].transform);
|
||||||
|
const delta = scenePosition.subtract(beginPos);
|
||||||
|
let segment;
|
||||||
|
if (segmented) {
|
||||||
|
segment = (delta.angle() + Math.PI * 2.5) % MAX_SEGMENT;
|
||||||
|
} else {
|
||||||
|
segment = MAX_SEGMENT;
|
||||||
|
}
|
||||||
|
const radius = delta.length();
|
||||||
|
state = updateCircle(state, activeShape, radius, segment);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
case actions.D2_DRAG_END: {
|
||||||
|
if (activeShape) {
|
||||||
|
state = setActive2D(state, null);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
case actions.D2_TAP: {
|
||||||
|
const segment = segmented ? DEFAULT_SEGMENT : MAX_SEGMENT;
|
||||||
|
|
||||||
|
return addObject(state, {
|
||||||
|
type: (segmented ? 'CIRCLE_SEGMENT' : 'CIRCLE'),
|
||||||
|
transform: new Matrix({ x: scenePosition.x, y: scenePosition.y }),
|
||||||
|
circle: {
|
||||||
|
radius: DEFAULT_RADIUS,
|
||||||
|
segment
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateCircle(state, id, radius, segment) {
|
||||||
|
return update(state, {
|
||||||
|
objectsById: {
|
||||||
|
[id]: {
|
||||||
|
circle: {
|
||||||
|
radius: { $set: radius },
|
||||||
|
segment: { $set: segment }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
8
src/reducer/d2/tools/shapes/circleSegmentReducer.js
Normal file
8
src/reducer/d2/tools/shapes/circleSegmentReducer.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import circleReducer from './circleReducer.js';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
const debug = createDebug('d3d:reducer:circleSegment');
|
||||||
|
|
||||||
|
export default function circleSegmentReducer(state, action) {
|
||||||
|
if (action.log !== false) debug(action.type);
|
||||||
|
return circleReducer(state, action, true);
|
||||||
|
}
|
71
src/reducer/d2/tools/shapes/heartReducer.js
Normal file
71
src/reducer/d2/tools/shapes/heartReducer.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as actions from '../../../../actions/index.js';
|
||||||
|
import { addObjectActive2D, addObject, setActive2D } from '../../../objectReducers.js';
|
||||||
|
import { Matrix, Vector } from 'cal';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:reducer:star');
|
||||||
|
|
||||||
|
const DEFAULT_WIDTH = 30.0;
|
||||||
|
const DEFAULT_HEIGHT = 30.0;
|
||||||
|
|
||||||
|
export default function starReducer(state, action) {
|
||||||
|
// if (action.log !== false) debug(action.type);
|
||||||
|
const { position, screenMatrixZoom } = action;
|
||||||
|
let scenePosition;
|
||||||
|
if (position !== undefined && screenMatrixZoom !== undefined) {
|
||||||
|
scenePosition = position.applyMatrix(screenMatrixZoom.inverseMatrix());
|
||||||
|
}
|
||||||
|
const activeShape = state.d2.activeShape;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.D2_DRAG_START:
|
||||||
|
return addObjectActive2D(state, {
|
||||||
|
type: 'HEART',
|
||||||
|
transform: new Matrix({ x: scenePosition.x, y: scenePosition.y }),
|
||||||
|
heart: {
|
||||||
|
width: 1.0,
|
||||||
|
height: 1.0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
case actions.D2_DRAG:
|
||||||
|
if (activeShape) {
|
||||||
|
const beginPos = new Vector().copy(state.objectsById[activeShape].transform);
|
||||||
|
const delta = scenePosition.subtract(beginPos);
|
||||||
|
|
||||||
|
const width = Math.abs(delta.x);
|
||||||
|
const height = Math.abs(delta.y);
|
||||||
|
|
||||||
|
return update(state, {
|
||||||
|
objectsById: {
|
||||||
|
[activeShape]: {
|
||||||
|
heart: {
|
||||||
|
width: { $set: width },
|
||||||
|
height: { $set: height }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.D2_DRAG_END:
|
||||||
|
if (activeShape) {
|
||||||
|
state = setActive2D(state, null);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.D2_TAP:
|
||||||
|
return addObject(state, {
|
||||||
|
type: 'HEART',
|
||||||
|
transform: new Matrix({ x: scenePosition.x, y: scenePosition.y }),
|
||||||
|
heart: {
|
||||||
|
width: DEFAULT_WIDTH,
|
||||||
|
height: DEFAULT_HEIGHT
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
69
src/reducer/d2/tools/shapes/polyPointReducer.js
Normal file
69
src/reducer/d2/tools/shapes/polyPointReducer.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as actions from '../../../../actions/index.js';
|
||||||
|
import { addObjectActive2D, addObject, setActive2D } from '../../../objectReducers.js';
|
||||||
|
import { Matrix, Vector } from 'cal';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:reducer:star');
|
||||||
|
|
||||||
|
const DEFAULT_RADIUS = 25;
|
||||||
|
|
||||||
|
export default function starReducer(state, action) {
|
||||||
|
// if (action.log !== false) debug(action.type);
|
||||||
|
const { position, screenMatrixZoom } = action;
|
||||||
|
let scenePosition;
|
||||||
|
if (position !== undefined && screenMatrixZoom !== undefined) {
|
||||||
|
scenePosition = position.applyMatrix(screenMatrixZoom.inverseMatrix());
|
||||||
|
}
|
||||||
|
const activeShape = state.d2.activeShape;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.D2_DRAG_START:
|
||||||
|
return addObjectActive2D(state, {
|
||||||
|
type: 'POLY_POINTS',
|
||||||
|
transform: new Matrix({ x: scenePosition.x, y: scenePosition.y }),
|
||||||
|
polyPoints: {
|
||||||
|
numPoints: 6,
|
||||||
|
radius: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
case actions.D2_DRAG:
|
||||||
|
if (activeShape) {
|
||||||
|
const beginPos = new Vector().copy(state.objectsById[activeShape].transform);
|
||||||
|
const delta = scenePosition.subtract(beginPos);
|
||||||
|
|
||||||
|
const numPoints = Math.min(15, Math.max(3, Math.round(delta.y / 10) + 6));
|
||||||
|
const radius = Math.abs(delta.x);
|
||||||
|
return update(state, {
|
||||||
|
objectsById: {
|
||||||
|
[activeShape]: {
|
||||||
|
polyPoints: {
|
||||||
|
numPoints: { $set: numPoints },
|
||||||
|
radius: { $set: radius }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.D2_DRAG_END:
|
||||||
|
if (activeShape) {
|
||||||
|
state = setActive2D(state, null);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.D2_TAP:
|
||||||
|
return addObject(state, {
|
||||||
|
type: 'POLY_POINTS',
|
||||||
|
transform: new Matrix({ x: scenePosition.x, y: scenePosition.y }),
|
||||||
|
polyPoints: {
|
||||||
|
numPoints: 6,
|
||||||
|
radius: DEFAULT_RADIUS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
64
src/reducer/d2/tools/shapes/rectReducer.js
Normal file
64
src/reducer/d2/tools/shapes/rectReducer.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as actions from '../../../../actions/index.js';
|
||||||
|
import { addObjectActive2D, addObject, setActive2D } from '../../../objectReducers.js';
|
||||||
|
import { Matrix, Vector } from 'cal';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
const debug = createDebug('d3d:reducer:rect');
|
||||||
|
|
||||||
|
const DEFAULT_WIDTH = 25;
|
||||||
|
const DEFAULT_HEIGHT = 25;
|
||||||
|
|
||||||
|
export default function rectReducer(state, action) {
|
||||||
|
if (action.log !== false) debug(action.type);
|
||||||
|
const { position, screenMatrixZoom } = action;
|
||||||
|
let scenePosition;
|
||||||
|
if (position !== undefined && screenMatrixZoom !== undefined) {
|
||||||
|
scenePosition = position.applyMatrix(screenMatrixZoom.inverseMatrix());
|
||||||
|
}
|
||||||
|
const activeShape = state.d2.activeShape;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.D2_DRAG_START:
|
||||||
|
return addObjectActive2D(state, {
|
||||||
|
type: 'RECT',
|
||||||
|
transform: new Matrix({ x: scenePosition.x, y: scenePosition.y })
|
||||||
|
});
|
||||||
|
|
||||||
|
case actions.D2_DRAG:
|
||||||
|
const beginPos = new Vector().copy(state.objectsById[activeShape].transform);
|
||||||
|
const delta = scenePosition.subtract(beginPos);
|
||||||
|
|
||||||
|
const width = delta.x;
|
||||||
|
const height = delta.y;
|
||||||
|
return updateRect(state, activeShape, width, height);
|
||||||
|
|
||||||
|
case actions.D2_DRAG_END:
|
||||||
|
return setActive2D(state, null);
|
||||||
|
|
||||||
|
case actions.D2_TAP:
|
||||||
|
return addObject(state, {
|
||||||
|
type: 'RECT',
|
||||||
|
transform: new Matrix({ x: scenePosition.x, y: scenePosition.y }),
|
||||||
|
rectSize: {
|
||||||
|
x: DEFAULT_WIDTH,
|
||||||
|
y: DEFAULT_HEIGHT
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRect(state, id, width, height) {
|
||||||
|
return update(state, {
|
||||||
|
objectsById: {
|
||||||
|
[id]: {
|
||||||
|
rectSize: {
|
||||||
|
x: { $set: width },
|
||||||
|
y: { $set: height }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
60
src/reducer/d2/tools/shapes/skewRectReducer.js
Normal file
60
src/reducer/d2/tools/shapes/skewRectReducer.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as actions from '../../../../actions/index.js';
|
||||||
|
import { removeObject, addObjectActive2D, setActive2D } from '../../../objectReducers.js';
|
||||||
|
import { Vector, Matrix } from 'cal';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
const debug = createDebug('d3d:reducer:skewRect');
|
||||||
|
|
||||||
|
export default function skewRectReducer(state, action) {
|
||||||
|
if (action.log !== false) debug(action.type);
|
||||||
|
const mouse = action.mouse;
|
||||||
|
const activeShape = state.d2.activeShape;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case actions.D2_MOUSE_DOWN:
|
||||||
|
return addObjectActive2D(state, {
|
||||||
|
type: 'SKEW_RECT',
|
||||||
|
transform: new Matrix({ x: mouse.position.x, y: mouse.position.y }),
|
||||||
|
points: [
|
||||||
|
new Vector(100, 0),
|
||||||
|
new Vector(-100, 0),
|
||||||
|
new Vector(-100, 0),
|
||||||
|
new Vector(100, 0)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
case actions.D2_MOUSE_MOVE:
|
||||||
|
if (action.mouse.down && activeShape) {
|
||||||
|
const delta = mouse.position.subtract(mouse.start);
|
||||||
|
|
||||||
|
state = update(state, {
|
||||||
|
objectsById: {
|
||||||
|
[activeShape]: {
|
||||||
|
points: {
|
||||||
|
[2]: { $set: new Vector(delta.x - 100, delta.y) },
|
||||||
|
[3]: { $set: new Vector(delta.x + 100, delta.y) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.D2_MOUSE_UP:
|
||||||
|
if (activeShape) {
|
||||||
|
state = setActive2D(state, null);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.D2_MOUSE_CLICK:
|
||||||
|
if (activeShape) {
|
||||||
|
state = removeObject(state, activeShape);
|
||||||
|
state = setActive2D(state, null);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
75
src/reducer/d2/tools/shapes/starReducer.js
Normal file
75
src/reducer/d2/tools/shapes/starReducer.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as actions from '../../../../actions/index.js';
|
||||||
|
import { addObjectActive2D, addObject, setActive2D } from '../../../../objectReducers.js';
|
||||||
|
import { Matrix, Vector } from 'cal';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:reducer:star');
|
||||||
|
|
||||||
|
const DEFAULT_INNER_RADIUS = 10;
|
||||||
|
const DEFAULT_OUTER_RADIUS = 25;
|
||||||
|
|
||||||
|
export default function starReducer(state, action) {
|
||||||
|
// if (action.log !== false) debug(action.type);
|
||||||
|
const { position, screenMatrixZoom } = action;
|
||||||
|
let scenePosition;
|
||||||
|
if (position !== undefined && screenMatrixZoom !== undefined) {
|
||||||
|
scenePosition = position.applyMatrix(screenMatrixZoom.inverseMatrix());
|
||||||
|
}
|
||||||
|
const activeShape = state.d2.activeShape;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.D2_DRAG_START:
|
||||||
|
return addObjectActive2D(state, {
|
||||||
|
type: 'STAR',
|
||||||
|
transform: new Matrix({ x: scenePosition.x, y: scenePosition.y }),
|
||||||
|
star: {
|
||||||
|
rays: 5,
|
||||||
|
innerRadius: 0,
|
||||||
|
outerRadius: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
case actions.D2_DRAG:
|
||||||
|
if (activeShape) {
|
||||||
|
const beginPos = new Vector().copy(state.objectsById[activeShape].transform);
|
||||||
|
const delta = scenePosition.subtract(beginPos);
|
||||||
|
|
||||||
|
const innerRadius = Math.abs(delta.y);
|
||||||
|
const outerRadius = Math.abs(delta.x);
|
||||||
|
state = updateStar(state, activeShape, innerRadius, outerRadius);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.D2_DRAG_END:
|
||||||
|
if (activeShape) {
|
||||||
|
state = setActive2D(state, null);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.D2_TAP:
|
||||||
|
return addObject(state, {
|
||||||
|
type: 'STAR',
|
||||||
|
transform: new Matrix({ x: scenePosition.x, y: scenePosition.y }),
|
||||||
|
star: {
|
||||||
|
rays: 5,
|
||||||
|
innerRadius: DEFAULT_INNER_RADIUS,
|
||||||
|
outerRadius: DEFAULT_OUTER_RADIUS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateStar(state, id, innerRadius, outerRadius) {
|
||||||
|
return update(state, {
|
||||||
|
objectsById: {
|
||||||
|
[id]: {
|
||||||
|
star: {
|
||||||
|
innerRadius: { $set: innerRadius },
|
||||||
|
outerRadius: { $set: outerRadius }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
70
src/reducer/d2/tools/shapes/triangleReducer.js
Normal file
70
src/reducer/d2/tools/shapes/triangleReducer.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as actions from '../../../../actions/index.js';
|
||||||
|
import { addObjectActive2D, addObject, setActive2D } from '../../../objectReducers.js';
|
||||||
|
import { Matrix, Vector } from 'cal';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
const debug = createDebug('d3d:reducer:triangle');
|
||||||
|
|
||||||
|
const DEFAULT_WIDTH = 25;
|
||||||
|
const DEFAULT_HEIGHT = 25;
|
||||||
|
|
||||||
|
export default function triangleReducer(state, action) {
|
||||||
|
if (action.log !== false) debug(action.type);
|
||||||
|
const { position, screenMatrixZoom } = action;
|
||||||
|
let scenePosition;
|
||||||
|
if (position !== undefined && screenMatrixZoom !== undefined) {
|
||||||
|
scenePosition = position.applyMatrix(screenMatrixZoom.inverseMatrix());
|
||||||
|
}
|
||||||
|
const activeShape = state.d2.activeShape;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.D2_DRAG_START:
|
||||||
|
return addObjectActive2D(state, {
|
||||||
|
type: 'TRIANGLE',
|
||||||
|
transform: new Matrix({ x: scenePosition.x, y: scenePosition.y })
|
||||||
|
});
|
||||||
|
|
||||||
|
case actions.D2_DRAG:
|
||||||
|
if (activeShape) {
|
||||||
|
const beginPos = new Vector().copy(state.objectsById[activeShape].transform);
|
||||||
|
const delta = scenePosition.subtract(beginPos);
|
||||||
|
|
||||||
|
const width = delta.x * 2;
|
||||||
|
const height = delta.y;
|
||||||
|
state = updateTriangle(state, activeShape, width, height);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.D2_DRAG_END:
|
||||||
|
if (activeShape) {
|
||||||
|
state = setActive2D(state, null);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.D2_TAP:
|
||||||
|
return addObject(state, {
|
||||||
|
type: 'TRIANGLE',
|
||||||
|
transform: new Matrix({ x: scenePosition.x, y: scenePosition.y }),
|
||||||
|
triangleSize: {
|
||||||
|
x: DEFAULT_WIDTH,
|
||||||
|
y: DEFAULT_HEIGHT
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTriangle(state, id, width, height) {
|
||||||
|
return update(state, {
|
||||||
|
objectsById: {
|
||||||
|
[id]: {
|
||||||
|
triangleSize: {
|
||||||
|
x: { $set: width },
|
||||||
|
y: { $set: height }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
58
src/reducer/d2/tools/textReducer.js
Normal file
58
src/reducer/d2/tools/textReducer.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Matrix, Vector } from 'cal';
|
||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as actions from '../../../actions/index.js';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
import { addObjectActive2D, setActive2D, removeObject } from '../../../reducers/objectReducers.js';
|
||||||
|
const debug = createDebug('d3d:reducer:text');
|
||||||
|
|
||||||
|
export default function textReducer(state, action) {
|
||||||
|
if (action.log !== false) debug(action.type);
|
||||||
|
|
||||||
|
const activeShape = state.d2.activeShape;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.D2_TEXT_INIT: {
|
||||||
|
const { position, textId, screenMatrixZoom } = action;
|
||||||
|
const screenPosition = (position && screenMatrixZoom) ?
|
||||||
|
position.applyMatrix(screenMatrixZoom.inverseMatrix()) :
|
||||||
|
new Vector();
|
||||||
|
|
||||||
|
if (textId) {
|
||||||
|
return setActive2D(state, textId);
|
||||||
|
} else {
|
||||||
|
return addObjectActive2D(state, {
|
||||||
|
transform: new Matrix({ x: screenPosition.x, y: screenPosition.y }),
|
||||||
|
type: 'TEXT'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
case actions.D2_TEXT_INPUT_CHANGE: {
|
||||||
|
const { text, family, style, weight, fill } = action;
|
||||||
|
return update(state, {
|
||||||
|
objectsById: {
|
||||||
|
[activeShape]: {
|
||||||
|
text: {
|
||||||
|
text: { $set: text },
|
||||||
|
family: { $set: family },
|
||||||
|
style: { $set: style },
|
||||||
|
weight: { $set: weight }
|
||||||
|
},
|
||||||
|
fill: { $set: fill }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case actions.D2_TEXT_ADD: {
|
||||||
|
if (activeShape && state.objectsById[activeShape].text.text.length === 0) {
|
||||||
|
return setActive2D(removeObject(state, activeShape), null);
|
||||||
|
} else {
|
||||||
|
return setActive2D(state, null);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
383
src/reducer/d2/tools/transformReducer.js
Normal file
383
src/reducer/d2/tools/transformReducer.js
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import { Matrix, Vector } from 'cal';
|
||||||
|
import { CANVAS_SIZE } from '../../../constants/d2Constants.js';
|
||||||
|
import * as actions from '../../../actions/index.js';
|
||||||
|
import { calculateGestureMatrix } from '../../../utils/matrixUtils.js';
|
||||||
|
import * as selectionUtils from '../../../utils/selectionUtils.js';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:reducer:transform');
|
||||||
|
|
||||||
|
const CANVAS_WIDTH = CANVAS_SIZE * 2;
|
||||||
|
const CANVAS_HEIGHT = CANVAS_SIZE * 2;
|
||||||
|
|
||||||
|
export function transformReducer(state, action) {
|
||||||
|
// if (action.log !== false) {
|
||||||
|
// debug(action.type);
|
||||||
|
// }
|
||||||
|
|
||||||
|
switch (action.category) {
|
||||||
|
case actions.CAT_SELECTION:
|
||||||
|
state = updateInitTransform(state);
|
||||||
|
state = updateTransforms(state);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.TRANSFORM_START:
|
||||||
|
case actions.TRANSFORM_END: {
|
||||||
|
const start = action.type === actions.TRANSFORM_START;
|
||||||
|
state = update(state, {
|
||||||
|
d2: {
|
||||||
|
transform: {
|
||||||
|
active: { $set: start },
|
||||||
|
handle: { $set: start ? action.handle : '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (action.handle === 'dragselect' && start) {
|
||||||
|
state = update(state, {
|
||||||
|
d2: {
|
||||||
|
transform: {
|
||||||
|
dragSelect: {
|
||||||
|
end: { $set: action.position.clone() },
|
||||||
|
start: { $set: action.position.clone() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.TRANSFORM: {
|
||||||
|
switch (state.d2.transform.handle) {
|
||||||
|
case 'translate':
|
||||||
|
return translate(state, action);
|
||||||
|
|
||||||
|
case 'rotate':
|
||||||
|
return rotate(state, action);
|
||||||
|
|
||||||
|
case 'scale-lefttop':
|
||||||
|
case 'scale-leftbottom':
|
||||||
|
case 'scale-rightbottom':
|
||||||
|
case 'scale-righttop':
|
||||||
|
case 'scale-left':
|
||||||
|
case 'scale-bottom':
|
||||||
|
case 'scale-right':
|
||||||
|
case 'scale-top':
|
||||||
|
return scale(state, action);
|
||||||
|
|
||||||
|
case 'dragselect':
|
||||||
|
return dragSelect(state, action);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.MULTITOUCH_TRANSFORM_START:
|
||||||
|
case actions.MULTITOUCH_TRANSFORM_END: {
|
||||||
|
const start = action.type === actions.MULTITOUCH_TRANSFORM_START;
|
||||||
|
state = update(state, {
|
||||||
|
d2: {
|
||||||
|
transform: {
|
||||||
|
active: { $set: start }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.MULTITOUCH_TRANSFORM:
|
||||||
|
return multitouch(state, action);
|
||||||
|
|
||||||
|
case actions.MOVE_SELECTION:
|
||||||
|
return moveSelection(state, action.deltaX, action.deltaY);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function translate(state, { delta, screenMatrixZoom }) {
|
||||||
|
delta = delta.applyMatrix(screenMatrixZoom.normalize().inverseMatrix());
|
||||||
|
|
||||||
|
return moveSelection(state, delta.x, delta.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotate(state, { delta, position, screenMatrixZoom }) {
|
||||||
|
const transform = state.selection.transform;
|
||||||
|
|
||||||
|
let { center } = getBoundingBox(state, false);
|
||||||
|
center = center.applyMatrix(transform);
|
||||||
|
const centerScreen = center.applyMatrix(screenMatrixZoom);
|
||||||
|
|
||||||
|
const current = position;
|
||||||
|
const start = position.subtract(delta);
|
||||||
|
|
||||||
|
const oldAngle = start.subtract(centerScreen).angle();
|
||||||
|
const newAngle = current.subtract(centerScreen).angle();
|
||||||
|
const angle = newAngle - oldAngle;
|
||||||
|
|
||||||
|
const rotation = new Matrix().rotateAroundAbsolute(angle, center);
|
||||||
|
|
||||||
|
return containSelectedObjects(updateTransforms(update(state, {
|
||||||
|
selection: {
|
||||||
|
transform: { $set: transform.multiplyMatrix(rotation) }
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
function scale(state, { position, screenMatrixZoom }) {
|
||||||
|
// let {min, max} = state.selection.boundingBox;
|
||||||
|
let { min, max } = getBoundingBox(state, false);
|
||||||
|
|
||||||
|
min = new Vector(min.x, min.z);
|
||||||
|
max = new Vector(max.x, max.z);
|
||||||
|
|
||||||
|
let move; // position of corner we are transforming
|
||||||
|
let scaleAround; // position of corner to use as reference point
|
||||||
|
switch (state.d2.transform.handle) {
|
||||||
|
case 'scale-lefttop':
|
||||||
|
scaleAround = new Vector(max.x, max.y);
|
||||||
|
move = new Vector(min.x, min.y);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'scale-leftbottom':
|
||||||
|
scaleAround = new Vector(max.x, min.y);
|
||||||
|
move = new Vector(min.x, max.y);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'scale-rightbottom':
|
||||||
|
scaleAround = new Vector(min.x, min.y);
|
||||||
|
move = new Vector(max.x, max.y);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'scale-righttop':
|
||||||
|
scaleAround = new Vector(min.x, max.y);
|
||||||
|
move = new Vector(max.x, min.y);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'scale-top':
|
||||||
|
scaleAround = new Vector((min.x + max.x) / 2, max.y);
|
||||||
|
move = new Vector((min.x + max.x) / 2, min.y);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'scale-bottom':
|
||||||
|
scaleAround = new Vector((min.x + max.x) / 2, min.y);
|
||||||
|
move = new Vector((min.x + max.x) / 2, max.y);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'scale-left':
|
||||||
|
scaleAround = new Vector(max.x, (min.y + max.y) / 2);
|
||||||
|
move = new Vector(min.x, (min.y + max.y) / 2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'scale-right':
|
||||||
|
scaleAround = new Vector(min.x, (min.y + max.y) / 2);
|
||||||
|
move = new Vector(max.x, (min.y + max.y) / 2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw Error('Unknown corner');
|
||||||
|
}
|
||||||
|
position = new Vector(position.x, position.y);
|
||||||
|
// transform from container space to object space
|
||||||
|
const matrix = state.selection.transform.multiplyMatrix(screenMatrixZoom).inverseMatrix();
|
||||||
|
const mousePos = position.applyMatrix(matrix);
|
||||||
|
|
||||||
|
const currentSize = move.subtract(scaleAround);
|
||||||
|
const newSize = mousePos.subtract(scaleAround);
|
||||||
|
|
||||||
|
let sx;
|
||||||
|
let sy;
|
||||||
|
switch (state.d2.transform.handle) {
|
||||||
|
case 'scale-lefttop':
|
||||||
|
case 'scale-leftbottom':
|
||||||
|
case 'scale-rightbottom':
|
||||||
|
case 'scale-righttop':
|
||||||
|
const ratioX = currentSize.x === 0 ? 1 : (newSize.x / currentSize.x);
|
||||||
|
const ratioY = currentSize.y === 0 ? 1 : (newSize.y / currentSize.y);
|
||||||
|
|
||||||
|
sx = sy = (Math.abs(ratioX) > Math.abs(ratioY)) ? ratioX : ratioY;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'scale-top':
|
||||||
|
case 'scale-bottom':
|
||||||
|
sx = 1;
|
||||||
|
sy = currentSize.y === 0 ? 1 : (newSize.y / currentSize.y);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'scale-left':
|
||||||
|
case 'scale-right':
|
||||||
|
sx = currentSize.x === 0 ? 1 : (newSize.x / currentSize.x);
|
||||||
|
sy = 1;
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw Error('Unknown corner');
|
||||||
|
}
|
||||||
|
|
||||||
|
const transform = state.selection.transform;
|
||||||
|
const newScale = new Matrix().scaleAroundAbsolute(sx, sy, scaleAround);
|
||||||
|
|
||||||
|
const newState = updateTransforms(update(state, {
|
||||||
|
selection: {
|
||||||
|
transform: { $set: newScale.multiplyMatrix(transform) }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const boundingBox = getBoundingBox(newState, true);
|
||||||
|
|
||||||
|
// TODO: move to constants
|
||||||
|
const minX = boundingBox.min.x < -CANVAS_SIZE;
|
||||||
|
const minY = boundingBox.min.z < -CANVAS_SIZE;
|
||||||
|
const maxX = boundingBox.max.x > CANVAS_SIZE;
|
||||||
|
const maxY = boundingBox.max.z > CANVAS_SIZE;
|
||||||
|
|
||||||
|
return (minX || minY || maxX || maxY) ? state : newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragSelect(state, { position }) {
|
||||||
|
return update(state, {
|
||||||
|
d2: {
|
||||||
|
transform: {
|
||||||
|
dragSelect: {
|
||||||
|
end: { $set: position.clone() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function multitouch(state, { positions, previousPositions, screenMatrixZoom }) {
|
||||||
|
const gestureMatrix = calculateGestureMatrix(positions, previousPositions, screenMatrixZoom, {
|
||||||
|
rotate: true, scale: true, pan: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const transform = state.selection.transform.multiplyMatrix(gestureMatrix);
|
||||||
|
|
||||||
|
return containSelectedObjects(updateTransforms(update(state, {
|
||||||
|
selection: {
|
||||||
|
transform: { $set: transform }
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveSelection(state, deltaX, deltaY) {
|
||||||
|
const transform = state.selection.transform.translate(deltaX, deltaY);
|
||||||
|
|
||||||
|
return containSelectedObjects(updateTransforms(update(state, {
|
||||||
|
selection: {
|
||||||
|
transform: { $set: transform }
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
function containSelectedObjects(state) {
|
||||||
|
const boundingBox = getBoundingBox(state, true);
|
||||||
|
let transform = new Matrix(state.selection.transform);
|
||||||
|
|
||||||
|
let minX = boundingBox.min.x;
|
||||||
|
let minY = boundingBox.min.z;
|
||||||
|
let maxX = boundingBox.max.x;
|
||||||
|
let maxY = boundingBox.max.z;
|
||||||
|
|
||||||
|
const width = maxX - minX;
|
||||||
|
const height = maxY - minY;
|
||||||
|
|
||||||
|
let change = false;
|
||||||
|
if (width > CANVAS_WIDTH || height > CANVAS_HEIGHT) {
|
||||||
|
change = true;
|
||||||
|
|
||||||
|
const constainFactor = Math.min(CANVAS_WIDTH / width, CANVAS_HEIGHT / height);
|
||||||
|
const center = new Vector((maxX + minX) / 2, (maxY + minY) / 2);
|
||||||
|
const newScale = new Matrix().scaleAroundAbsolute(constainFactor, constainFactor, center);
|
||||||
|
|
||||||
|
transform = transform.multiplyMatrix(newScale);
|
||||||
|
|
||||||
|
minX = center.x + (minX - center.x) * constainFactor;
|
||||||
|
maxX = center.x + (maxX - center.x) * constainFactor;
|
||||||
|
maxY = center.y + (maxY - center.y) * constainFactor;
|
||||||
|
minY = center.y + (minY - center.y) * constainFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minX < -CANVAS_SIZE) {
|
||||||
|
change = true;
|
||||||
|
transform.x -= minX + CANVAS_SIZE;
|
||||||
|
} else if (maxX > CANVAS_SIZE) {
|
||||||
|
change = true;
|
||||||
|
transform.x -= maxX - CANVAS_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minY < -CANVAS_SIZE) {
|
||||||
|
change = true;
|
||||||
|
transform.y -= minY + CANVAS_SIZE;
|
||||||
|
} else if (maxY > CANVAS_SIZE) {
|
||||||
|
change = true;
|
||||||
|
transform.y -= maxY - CANVAS_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change) {
|
||||||
|
return updateTransforms(update(state, {
|
||||||
|
selection: { transform: { $set: transform } }
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateInitTransform(state) {
|
||||||
|
// Clone object's transform into object's initialTransform
|
||||||
|
// Store new Matrix as selection transform
|
||||||
|
state = update(state, {
|
||||||
|
selection: {
|
||||||
|
objects: {
|
||||||
|
$set: state.selection.objects.map(({ id }) => {
|
||||||
|
const shapeData = state.objectsById[id];
|
||||||
|
const initialTransform = new Matrix(shapeData.transform);
|
||||||
|
|
||||||
|
return { id, initialTransform };
|
||||||
|
})
|
||||||
|
},
|
||||||
|
transform: { $set: new Matrix() }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update each object's transform with multiplication of object's initial transform with selection transform
|
||||||
|
function updateTransforms(state) {
|
||||||
|
const matrix = state.selection.transform;
|
||||||
|
|
||||||
|
for (const { initialTransform, id } of state.selection.objects) {
|
||||||
|
const transform = initialTransform.multiplyMatrix(matrix);
|
||||||
|
|
||||||
|
state = update(state, {
|
||||||
|
objectsById: {
|
||||||
|
[id]: { transform: { $set: transform } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBoundingBox(state, axisAligned) {
|
||||||
|
const selection = state.selection;
|
||||||
|
const selectedShapeDatas = selectionUtils.getSelectedObjectsSelector(selection.objects, state.objectsById);
|
||||||
|
if (axisAligned) {
|
||||||
|
return selectionUtils.getBoundingBox(selectedShapeDatas);
|
||||||
|
} else {
|
||||||
|
return selectionUtils.getBoundingBox(selectedShapeDatas, selection.transform);
|
||||||
|
}
|
||||||
|
}
|
25
src/reducer/d2/wheelZoomReducer.js
Normal file
25
src/reducer/d2/wheelZoomReducer.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import { Matrix } from 'cal';
|
||||||
|
import constrainMatrix from './constrainMatrix.js';
|
||||||
|
import { MAX_ZOOM } from '../../constants/d2Constants.js';
|
||||||
|
|
||||||
|
export default function d2WheelZoomReducer(state, action) {
|
||||||
|
const { position, wheelDelta, screenMatrixContainer } = action;
|
||||||
|
const { canvasMatrix } = state.d2;
|
||||||
|
|
||||||
|
const targetScale = 1 + wheelDelta / -500;
|
||||||
|
|
||||||
|
if (canvasMatrix.sx === MAX_ZOOM && targetScale > 1) return state;
|
||||||
|
const rotateAround = position.applyMatrix(screenMatrixContainer.inverseMatrix());
|
||||||
|
const scaleMatrix = new Matrix().scaleAroundAbsolute(targetScale, targetScale, rotateAround);
|
||||||
|
|
||||||
|
const matrix = canvasMatrix.multiplyMatrix(scaleMatrix);
|
||||||
|
|
||||||
|
constrainMatrix(matrix);
|
||||||
|
|
||||||
|
return update(state, {
|
||||||
|
d2: {
|
||||||
|
canvasMatrix: { $set: matrix }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
58
src/reducer/d3/toolReducer.js
vendored
Normal file
58
src/reducer/d3/toolReducer.js
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import * as actions from '../../actions/index.js';
|
||||||
|
import * as tools from '../../constants/d3Tools.js';
|
||||||
|
import heightReducer from './tools/heightReducer.js';
|
||||||
|
import twistReducer from './tools/twistReducer.js';
|
||||||
|
import sculptReducer from './tools/sculptReducer.js';
|
||||||
|
import stampReducer from './tools/stampReducer.js';
|
||||||
|
import { cameraReducer } from './tools/cameraReducer.js';
|
||||||
|
import update from 'react-addons-update';
|
||||||
|
|
||||||
|
import createDebug from 'debug';
|
||||||
|
const debug = createDebug('d3d:reducer:d3:tool');
|
||||||
|
|
||||||
|
const reducers = {
|
||||||
|
[tools.HEIGHT]: heightReducer,
|
||||||
|
[tools.TWIST]: twistReducer,
|
||||||
|
[tools.SCULPT]: sculptReducer,
|
||||||
|
[tools.STAMP]: stampReducer
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function toolReducer(state, action) {
|
||||||
|
// if (action.log !== false) debug(action.type);
|
||||||
|
|
||||||
|
state = update(state, {
|
||||||
|
d3: { camera: { $set: cameraReducer(state.d3.camera, action) } }
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.sketcher.D3_DRAG_START:
|
||||||
|
state = update(state, {
|
||||||
|
d3: { dragging: { $set: true } }
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case actions.sketcher.D3_DRAG_END:
|
||||||
|
state = update(state, {
|
||||||
|
d3: { dragging: { $set: false } }
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === actions.sketcher.D3_CHANGE_TOOL && action.tool !== state.d3.tool) {
|
||||||
|
state = update(state, {
|
||||||
|
d3: { tool: { $set: action.tool } }
|
||||||
|
});
|
||||||
|
debug('d3 tool: ', action.tool);
|
||||||
|
}
|
||||||
|
const tool = state.d3.tool;
|
||||||
|
const reducer = reducers[tool];
|
||||||
|
if (reducer) {
|
||||||
|
return reducer(state, action);
|
||||||
|
} else {
|
||||||
|
debug('Unkown 3D tool: ', tool);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
165
src/reducer/d3/tools/cameraReducer.js
vendored
Normal file
165
src/reducer/d3/tools/cameraReducer.js
vendored
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as actions from '../../../actions/index.js';
|
||||||
|
import { MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM, MAX_CAMERA_PAN } from '../../../constants/d3Constants.js';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:reducer:camera');
|
||||||
|
|
||||||
|
const CAMERA_STATES = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2 };
|
||||||
|
const PAN_SPEED = 0.001;
|
||||||
|
const ZOOM_SPEED = 0.001;
|
||||||
|
const ROTATION_SPEED = 0.005;
|
||||||
|
|
||||||
|
export const defaultCamera = {
|
||||||
|
object: new THREE.PerspectiveCamera(),
|
||||||
|
center: new THREE.Vector3(0, 0, 0),
|
||||||
|
state: CAMERA_STATES.NONE
|
||||||
|
};
|
||||||
|
defaultCamera.object.position.set(0, 125, 250);
|
||||||
|
defaultCamera.object.lookAt(defaultCamera.center);
|
||||||
|
defaultCamera.object.updateMatrix();
|
||||||
|
|
||||||
|
export function cameraReducer(state, action) {
|
||||||
|
// if (action.log !== false) debug(action.type);
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.D3_MOUSE_WHEEL: {
|
||||||
|
const delta = new THREE.Vector3(0, 0, action.wheelDelta);
|
||||||
|
return zoom(state, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.D3_DRAG_START: {
|
||||||
|
return update(state, {
|
||||||
|
state: { $set: CAMERA_STATES.ROTATE }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.D3_SECOND_DRAG_START: {
|
||||||
|
return update(state, {
|
||||||
|
state: { $set: CAMERA_STATES.PAN }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.D3_DRAG:
|
||||||
|
case actions.D3_SECOND_DRAG: {
|
||||||
|
const movement = action.position.subtract(action.previousPosition);
|
||||||
|
|
||||||
|
switch (state.state) {
|
||||||
|
case CAMERA_STATES.ROTATE: {
|
||||||
|
const delta = new THREE.Vector3(-movement.x * ROTATION_SPEED, -movement.y * ROTATION_SPEED, 0);
|
||||||
|
return rotate(state, delta);
|
||||||
|
}
|
||||||
|
case CAMERA_STATES.PAN: {
|
||||||
|
const delta = new THREE.Vector3(-movement.x, movement.y, 0);
|
||||||
|
return pan(state, delta);
|
||||||
|
}
|
||||||
|
case CAMERA_STATES.ZOOM: {
|
||||||
|
const delta = new THREE.Vector3(0, 0, movement.y);
|
||||||
|
return zoom(state, delta);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.D3_DRAG_END:
|
||||||
|
case actions.D3_SECOND_DRAG_END:
|
||||||
|
return update(state, {
|
||||||
|
state: { $set: CAMERA_STATES.NONE }
|
||||||
|
});
|
||||||
|
|
||||||
|
case actions.D3_MULTITOUCH: {
|
||||||
|
const distance = action.positions[0].distanceTo(action.positions[1]);
|
||||||
|
const prevDistance = action.previousPositions[0].distanceTo(action.previousPositions[1]);
|
||||||
|
|
||||||
|
const zoomDelta = new THREE.Vector3(0, 0, prevDistance - distance);
|
||||||
|
state = zoom(state, zoomDelta);
|
||||||
|
|
||||||
|
const offset0 = new THREE.Vector3(
|
||||||
|
-(action.positions[0].x - action.previousPositions[0].x),
|
||||||
|
action.positions[0].y - action.previousPositions[0].y,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const offset1 = new THREE.Vector3(
|
||||||
|
-(action.positions[1].x - action.previousPositions[1].x),
|
||||||
|
action.positions[1].y - action.previousPositions[1].y,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const panDelta = offset0.add(offset1).multiplyScalar(0.5);
|
||||||
|
|
||||||
|
return pan(state, panDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.CLEAR:
|
||||||
|
return defaultCamera;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotate(state, delta) {
|
||||||
|
let { center, object } = state;
|
||||||
|
object = new THREE.PerspectiveCamera().copy(state.object);
|
||||||
|
center = new THREE.Vector3().copy(center);
|
||||||
|
|
||||||
|
const vector = new THREE.Vector3().copy(object.position).sub(center);
|
||||||
|
const spherical = new THREE.Spherical().setFromVector3(vector);
|
||||||
|
spherical.theta += delta.x;
|
||||||
|
spherical.phi += delta.y;
|
||||||
|
spherical.makeSafe();
|
||||||
|
vector.setFromSpherical(spherical);
|
||||||
|
|
||||||
|
object.position.copy(center).add(vector);
|
||||||
|
object.lookAt(center);
|
||||||
|
|
||||||
|
object.updateMatrix();
|
||||||
|
|
||||||
|
return update(state, {
|
||||||
|
object: { $set: object }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pan(state, delta) {
|
||||||
|
let { center, object } = state;
|
||||||
|
object = new THREE.PerspectiveCamera().copy(state.object);
|
||||||
|
center = new THREE.Vector3().copy(center);
|
||||||
|
|
||||||
|
const distance = object.position.distanceTo(center);
|
||||||
|
|
||||||
|
delta.multiplyScalar(distance * PAN_SPEED);
|
||||||
|
delta.applyMatrix3(new THREE.Matrix3().getNormalMatrix(object.matrix));
|
||||||
|
delta.add(center).clampLength(0.0, MAX_CAMERA_PAN).sub(center);
|
||||||
|
|
||||||
|
object.position.add(delta);
|
||||||
|
center.add(delta);
|
||||||
|
|
||||||
|
object.updateMatrix();
|
||||||
|
|
||||||
|
return update(state, {
|
||||||
|
object: { $set: object },
|
||||||
|
center: { $set: center }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoom(state, delta) {
|
||||||
|
let { center, object } = state;
|
||||||
|
object = new THREE.PerspectiveCamera().copy(state.object);
|
||||||
|
center = new THREE.Vector3().copy(center);
|
||||||
|
|
||||||
|
const distance = object.position.distanceTo(center);
|
||||||
|
delta.multiplyScalar(distance * ZOOM_SPEED);
|
||||||
|
|
||||||
|
if (delta.length() > distance) return state;
|
||||||
|
|
||||||
|
delta.applyMatrix3(new THREE.Matrix3().getNormalMatrix(object.matrix));
|
||||||
|
object.position.add(delta);
|
||||||
|
object.position.sub(center).clampLength(MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM).add(center);
|
||||||
|
|
||||||
|
object.updateMatrix();
|
||||||
|
|
||||||
|
return update(state, {
|
||||||
|
object: { $set: object }
|
||||||
|
});
|
||||||
|
}
|
100
src/reducer/d3/tools/heightReducer.js
vendored
Normal file
100
src/reducer/d3/tools/heightReducer.js
vendored
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import { Utils } from 'cal';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { SHAPE_TYPE_PROPERTIES } from '../../../constatants/shapeTypeProperties.js';
|
||||||
|
import * as d3Tools from '../../../constatants/d3Tools.js';
|
||||||
|
import { getSelectedObjectsSelector, getBoundingBox } from '../../../utils/selectionUtils.js';
|
||||||
|
import * as actions from '../../../actions/index.js';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:reducer:height');
|
||||||
|
|
||||||
|
const { MathExtended } = Utils;
|
||||||
|
|
||||||
|
const MIN_HEIGHT = 1;
|
||||||
|
const MAX_HEIGHT = 600;
|
||||||
|
const CHANGE_FACTOR = 0.5;
|
||||||
|
|
||||||
|
export default function heightReducer(state, action) {
|
||||||
|
// if (action.log !== false) debug(action.type);
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.HEIGHT_START:
|
||||||
|
case actions.HEIGHT_END: {
|
||||||
|
state = update(state, {
|
||||||
|
d3: {
|
||||||
|
height: {
|
||||||
|
active: { $set: action.type === actions.HEIGHT_START },
|
||||||
|
handle: { $set: action.type === actions.HEIGHT_START ? action.handle : '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.HEIGHT: {
|
||||||
|
const cameraAngle = new THREE.Vector3()
|
||||||
|
.set(action.delta.x, -action.delta.y, 0)
|
||||||
|
.applyMatrix3(new THREE.Matrix3().getNormalMatrix(state.d3.camera.object.matrix));
|
||||||
|
|
||||||
|
const sceneUp = new THREE.Vector3(0, 1, 0)
|
||||||
|
.applyMatrix4(new THREE.Matrix4().extractRotation(state.spaces[state.activeSpace].matrix));
|
||||||
|
|
||||||
|
let delta = cameraAngle.dot(sceneUp) * CHANGE_FACTOR;
|
||||||
|
|
||||||
|
const selectedShapeDatas = getSelectedObjectsSelector(state.selection.objects, state.objectsById);
|
||||||
|
const { min, max } = getBoundingBox(selectedShapeDatas, state.selection.transform);
|
||||||
|
const totalHeight = max.y - min.y;
|
||||||
|
|
||||||
|
state = update(state, {
|
||||||
|
objectsById: state.selection.objects
|
||||||
|
.map(({ id }) => state.objectsById[id])
|
||||||
|
.filter(shapeData => SHAPE_TYPE_PROPERTIES[shapeData.type].tools[d3Tools.HEIGHT])
|
||||||
|
.reduce((updateObject, shapeData) => {
|
||||||
|
let height;
|
||||||
|
let z;
|
||||||
|
switch (state.d3.height.handle) {
|
||||||
|
case 'top': {
|
||||||
|
delta -= Math.max(max.y + delta - MAX_HEIGHT, 0);
|
||||||
|
const scale = (totalHeight + delta) / totalHeight;
|
||||||
|
|
||||||
|
height = shapeData.height * scale;
|
||||||
|
z = (shapeData.z - min.y) * scale + min.y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'bottom': {
|
||||||
|
delta -= Math.min(min.y + delta, 0);
|
||||||
|
const scale = (totalHeight - delta) / totalHeight;
|
||||||
|
|
||||||
|
height = shapeData.height * scale;
|
||||||
|
z = (shapeData.z - min.y) * scale + min.y + delta;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'translate':
|
||||||
|
height = shapeData.height;
|
||||||
|
z = shapeData.z + delta;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return updateObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
height = MathExtended.clamb(height, MIN_HEIGHT, MAX_HEIGHT - MIN_HEIGHT);
|
||||||
|
z = MathExtended.clamb(z, 0, MAX_HEIGHT - height);
|
||||||
|
|
||||||
|
updateObject[shapeData.UID] = {
|
||||||
|
height: { $set: height },
|
||||||
|
z: { $set: z }
|
||||||
|
};
|
||||||
|
return updateObject;
|
||||||
|
}, {})
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
338
src/reducer/d3/tools/sculptReducer.js
vendored
Normal file
338
src/reducer/d3/tools/sculptReducer.js
vendored
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as actions from '../../../actions/index.js';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:reducer:sculpt');
|
||||||
|
import { SHAPE_TYPE_PROPERTIES } from '../../../constants/shapeTypeProperties.js';
|
||||||
|
import * as d3Tools from '../../../constants/d3Tools';
|
||||||
|
|
||||||
|
// state.d3.sculpt.handles: [
|
||||||
|
// {
|
||||||
|
// pos: number // position of handle
|
||||||
|
// scale: number // scale of handle
|
||||||
|
// ids: [ // list of sculpt steps this handle influences
|
||||||
|
// {
|
||||||
|
// id: number // object id
|
||||||
|
// index: number // sculpt index
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
|
||||||
|
const CHANGE_FACTOR = 0.005; // 0.5;
|
||||||
|
const HANDLES_MERGE_DISTANCE = 2.0;
|
||||||
|
|
||||||
|
export function init(state) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function sculptReducer(state, action) {
|
||||||
|
// if (action.log !== false) debug(action.type);
|
||||||
|
|
||||||
|
switch (action.category) {
|
||||||
|
case actions.CAT_SELECTION:
|
||||||
|
return initSculpt(state);
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case actions.D3_CHANGE_TOOL:
|
||||||
|
return initSculpt(state);
|
||||||
|
|
||||||
|
case actions.ADD_SCULPT_HANDLE: {
|
||||||
|
const pos = action.height;
|
||||||
|
const { handles } = state.d3.sculpt;
|
||||||
|
|
||||||
|
// find index of new handle based on position
|
||||||
|
const index = handles.findIndex(handle => action.height < handle.pos);
|
||||||
|
if (index === -1) return state;
|
||||||
|
|
||||||
|
// interpolate scale based on handles above and below new handle
|
||||||
|
const alpha = (action.height - handles[index - 1].pos) / (handles[index].pos - handles[index - 1].pos);
|
||||||
|
const scale = handles[index].scale * alpha + handles[index - 1].scale * (1 - alpha);
|
||||||
|
|
||||||
|
state = update(state, {
|
||||||
|
d3: {
|
||||||
|
sculpt: {
|
||||||
|
handles: {
|
||||||
|
$splice: [[index, 0, { pos, scale, ids: [] }]]
|
||||||
|
},
|
||||||
|
activeHandle: { $set: action.start ? index : null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
state = addHandles(state, index, pos);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.REMOVE_SCULPT_HANDLE: {
|
||||||
|
const { handles } = state.d3.sculpt;
|
||||||
|
const removedHandleIndex = action.heightIndex;
|
||||||
|
|
||||||
|
// you can't remove top and bottom handles
|
||||||
|
if (removedHandleIndex === 0 || removedHandleIndex === handles.length - 1) return state;
|
||||||
|
|
||||||
|
const removedHandle = handles[removedHandleIndex];
|
||||||
|
// retrieve sculpt steps handled by this handle which can be removed
|
||||||
|
// removable sculpt steps are steps that aren't the top and bottom steps of objects
|
||||||
|
const removableSculptSteps = removedHandle.ids
|
||||||
|
.filter(sculptStep => isRemovableSculptStep(state, sculptStep));
|
||||||
|
// retrieve sculpt steps handled by this handle which can't be removed
|
||||||
|
const nonRemovableSculptSteps = removedHandle.ids
|
||||||
|
.filter(sculptStep => !isRemovableSculptStep(state, sculptStep));
|
||||||
|
|
||||||
|
// update handles
|
||||||
|
// if all sculpt handles controlled by this handle can be removed, remove handle
|
||||||
|
if (nonRemovableSculptSteps.length === 0) {
|
||||||
|
state = updateHandles(state, {
|
||||||
|
$splice: [[removedHandleIndex, 1]]
|
||||||
|
});
|
||||||
|
// else remove just the objects of which the sculpt steps can be removed
|
||||||
|
} else {
|
||||||
|
state = updateHandles(state, {
|
||||||
|
[removedHandleIndex]: {
|
||||||
|
ids: {
|
||||||
|
$set: nonRemovableSculptSteps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert sculpt steps of removed handle into
|
||||||
|
// objects collection with per object it's removed indexes
|
||||||
|
const objects = sculptStepsToObjects(removableSculptSteps);
|
||||||
|
|
||||||
|
// Remove sculpt steps from objects
|
||||||
|
state = update(state, {
|
||||||
|
objectsById: Object.entries(objects)
|
||||||
|
.reduce((updateObject, [id, indexes]) => {
|
||||||
|
updateObject[id] = {
|
||||||
|
sculpt: {
|
||||||
|
$splice: [[indexes[0], indexes.length]]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return updateObject;
|
||||||
|
}, {})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update sculpt step indexes in handles
|
||||||
|
state = updateHandles(state, state.d3.sculpt.handles.reduce((updateObject, handle, handleIndex) => {
|
||||||
|
// go through handle's sculpt steps
|
||||||
|
for (const key in handle.ids) {
|
||||||
|
const { id, index } = handle.ids[key];
|
||||||
|
const removedIndexes = objects[id];
|
||||||
|
// if a sculpt step index is higher than a removed step
|
||||||
|
if (removedIndexes && index > removedIndexes[0]) {
|
||||||
|
// add update object for handle if needed
|
||||||
|
if (!updateObject[handleIndex]) updateObject[handleIndex] = { ids: {} };
|
||||||
|
// update index of handle's sculpt step
|
||||||
|
updateObject[handleIndex].ids[key] = { index: { $set: index - removedIndexes.length } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updateObject;
|
||||||
|
}, {}));
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.SCULPT_START: {
|
||||||
|
const { pos } = state.d3.sculpt.handles[action.heightIndex];
|
||||||
|
const index = action.heightIndex;
|
||||||
|
|
||||||
|
state = update(state, {
|
||||||
|
d3: {
|
||||||
|
sculpt: {
|
||||||
|
activeHandle: { $set: index }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
state = addHandles(state, index, pos);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.SCULPT_END: {
|
||||||
|
return update(state, {
|
||||||
|
d3: {
|
||||||
|
sculpt: {
|
||||||
|
activeHandle: { $set: null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.SCULPT: {
|
||||||
|
const delta = action.delta * CHANGE_FACTOR;
|
||||||
|
const heightIndex = state.d3.sculpt.activeHandle;
|
||||||
|
const selectionScale = Math.max(0.1, state.d3.sculpt.handles[heightIndex].scale + delta);
|
||||||
|
|
||||||
|
state = update(state, {
|
||||||
|
d3: { sculpt: { handles: { [heightIndex]: { scale: { $set: selectionScale } } } } }
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const { id, index } of state.d3.sculpt.handles[heightIndex].ids) {
|
||||||
|
const shapeData = state.objectsById[id];
|
||||||
|
if (!SHAPE_TYPE_PROPERTIES[shapeData.type].tools[d3Tools.SCULPT]) continue;
|
||||||
|
|
||||||
|
const shapeScale = Math.max(0.1, shapeData.sculpt[index].scale + delta);
|
||||||
|
state = update(state, {
|
||||||
|
objectsById: { [id]: { sculpt: { [index]: { scale: { $set: shapeScale } } } } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through sculpt steps and group / nest them per object
|
||||||
|
// sort the indexes per objects
|
||||||
|
function sculptStepsToObjects(sculptSteps) {
|
||||||
|
const objects = sculptSteps.reduce((result, { id, index }) => {
|
||||||
|
if (result[id] === undefined) result[id] = [];
|
||||||
|
result[id].push(index);
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
for (const id in objects) {
|
||||||
|
objects[id].sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRemovableSculptStep(state, sculptStep) {
|
||||||
|
const { id, index } = sculptStep;
|
||||||
|
const { sculpt } = state.objectsById[id];
|
||||||
|
|
||||||
|
return index !== 0 && index !== sculpt.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHandles(state, handlesUpdates) {
|
||||||
|
return update(state, {
|
||||||
|
d3: {
|
||||||
|
sculpt: {
|
||||||
|
handles: handlesUpdates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSculpt(state) {
|
||||||
|
const sculpt = getInitialSculpt(state);
|
||||||
|
return update(state, {
|
||||||
|
d3: {
|
||||||
|
sculpt: {
|
||||||
|
handles: { $set: sculpt }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialSculpt(state) {
|
||||||
|
// determine initial sculpt
|
||||||
|
return state.selection.objects
|
||||||
|
.map(({ id }) => state.objectsById[id])
|
||||||
|
// get sculpt steps per object
|
||||||
|
.map(({ sculpt, UID, z, height }) => {
|
||||||
|
return sculpt.map(({ pos, scale }) => ({
|
||||||
|
pos: pos * height + z,
|
||||||
|
scale,
|
||||||
|
id: UID
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
// convert to handles
|
||||||
|
.reduce((handles, sculpt) => {
|
||||||
|
for (let index = 0; index < sculpt.length; index ++) {
|
||||||
|
const { pos, scale, id } = sculpt[index];
|
||||||
|
// if there is already a handle nearby
|
||||||
|
const nearbyHandle = handles.find(handle => Math.abs(handle.pos - pos) < HANDLES_MERGE_DISTANCE);
|
||||||
|
if (nearbyHandle) {
|
||||||
|
// have the same handle also influence this object
|
||||||
|
nearbyHandle.ids.push({ id, index });
|
||||||
|
} else {
|
||||||
|
// otherwise create new handle
|
||||||
|
handles.push({
|
||||||
|
pos,
|
||||||
|
scale: state.selection.objects.length === 1 ? scale : 1.0,
|
||||||
|
ids: [{ id, index }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handles;
|
||||||
|
}, [])
|
||||||
|
// sort on y position
|
||||||
|
.sort((a, b) => a.pos - b.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add sculpt steps if needed and id added update existing sculpt steps
|
||||||
|
function addHandles(state, addedHandleIndex, pos) {
|
||||||
|
const { handles } = state.d3.sculpt;
|
||||||
|
|
||||||
|
// id's of objects that are already handled by handle
|
||||||
|
const objectIds = handles[addedHandleIndex].ids.map(({ id }) => id);
|
||||||
|
for (const { id } of state.selection.objects) {
|
||||||
|
// if selected object is already handled by handle skip
|
||||||
|
if (objectIds.includes(id)) continue;
|
||||||
|
|
||||||
|
// calculate relative y position (0 - 1) within object
|
||||||
|
const { z, height, sculpt } = state.objectsById[id];
|
||||||
|
const handlePos = (pos - z) / height;
|
||||||
|
|
||||||
|
// if handle position is above of below an object skip
|
||||||
|
if (handlePos <= 0.0 || handlePos >= 1.0) continue;
|
||||||
|
|
||||||
|
// find index of new sculpt step based on position
|
||||||
|
const index = sculpt.findIndex(handle => handlePos < handle.pos);
|
||||||
|
|
||||||
|
// interpolate scale based on sculpt steps above and below
|
||||||
|
const alpha = (handlePos - sculpt[index - 1].pos) / (sculpt[index].pos - sculpt[index - 1].pos);
|
||||||
|
const scale = sculpt[index].scale * alpha + sculpt[index - 1].scale * (1 - alpha);
|
||||||
|
|
||||||
|
// add handle
|
||||||
|
state = updateHandles(state, {
|
||||||
|
// add object to handle
|
||||||
|
[addedHandleIndex]: {
|
||||||
|
ids: { $push: [{ id, index }] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// add sculpt step to object
|
||||||
|
state = update(state, {
|
||||||
|
objectsById: {
|
||||||
|
[id]: {
|
||||||
|
sculpt: {
|
||||||
|
$splice: [[index, 0, {
|
||||||
|
pos: handlePos,
|
||||||
|
scale
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// per handle: update sculpt step indexes by incrementing all higher indexes
|
||||||
|
state = updateHandles(state, state.d3.sculpt.handles.reduce((updateObject, handle, handleIndex) => {
|
||||||
|
// skip current and lower handles
|
||||||
|
if (handleIndex <= addedHandleIndex) return updateObject;
|
||||||
|
for (const key in handle.ids) {
|
||||||
|
const { index, id } = handle.ids[key];
|
||||||
|
|
||||||
|
// skip sculpt steps for objects that are already handled by current handle
|
||||||
|
if (objectIds.includes(id)) continue;
|
||||||
|
|
||||||
|
// add update object for handle if needed
|
||||||
|
if (!updateObject[handleIndex]) updateObject[handleIndex] = { ids: {} };
|
||||||
|
// update index of handle's sculpt step
|
||||||
|
updateObject[handleIndex].ids[key] = { index: { $set: index + 1 } };
|
||||||
|
}
|
||||||
|
return updateObject;
|
||||||
|
}, {}));
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
54
src/reducer/d3/tools/stampReducer.js
vendored
Normal file
54
src/reducer/d3/tools/stampReducer.js
vendored
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import * as actions from '../../actions/index.js';
|
||||||
|
import { addObject, addSpaceActive } from '../../objectReducers.js';
|
||||||
|
import { recursiveClone } from '../../utils/clone.js';
|
||||||
|
import { getBoundingBox, getSelectedObjectsSelector } from '../../utils/selectionUtils.js';
|
||||||
|
import { updateInitTransform } from '../../d2/tools/transformReducer.js';
|
||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:reducer:height');
|
||||||
|
|
||||||
|
const ROTATION_MATRIX = new THREE.Matrix4().makeRotationX(Math.PI / 2);
|
||||||
|
|
||||||
|
export default function heightReducer(state, action) {
|
||||||
|
// if (action.log !== false) debug(action.type);
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.STAMP: {
|
||||||
|
const { point: position, face: { normal }, object } = action.hit;
|
||||||
|
// apply world rotation on normal so 'space transformations' are applied on the normal
|
||||||
|
normal.applyEuler(object.getWorldRotation());
|
||||||
|
|
||||||
|
// flip normal when clicking on backside of a face
|
||||||
|
const incidence = new THREE.Vector3().subVectors(state.d3.camera.object.position, position).normalize();
|
||||||
|
if (normal.dot(incidence) > 0) normal.multiplyScalar(-1);
|
||||||
|
|
||||||
|
// calculate space transformation based on collision click and face normal
|
||||||
|
const matrix = new THREE.Matrix4();
|
||||||
|
matrix.lookAt(new THREE.Vector3(0, 0, 0), normal, new THREE.Vector3(0, 1, 0));
|
||||||
|
matrix.multiply(ROTATION_MATRIX);
|
||||||
|
matrix.setPosition(position);
|
||||||
|
|
||||||
|
state = addSpaceActive(state, matrix);
|
||||||
|
|
||||||
|
const selectedObjects = state.selection.objects;
|
||||||
|
const boundingbox = getBoundingBox(getSelectedObjectsSelector(selectedObjects, state.objectsById));
|
||||||
|
|
||||||
|
for (const { id } of selectedObjects) {
|
||||||
|
const shapeData = recursiveClone(state.objectsById[id]);
|
||||||
|
shapeData.transform = shapeData.transform.translate(-boundingbox.center.x, -boundingbox.center.y);
|
||||||
|
delete shapeData.space;
|
||||||
|
state = addObject(state, shapeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateInitTransform(update(state, {
|
||||||
|
selection: {
|
||||||
|
objects: { $set: state.spaces[state.activeSpace].objectIds.map(id => ({ id })) }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
94
src/reducer/d3/tools/twistReducer.js
vendored
Normal file
94
src/reducer/d3/tools/twistReducer.js
vendored
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import { Utils } from 'cal';
|
||||||
|
import * as actions from '../../../actions/index.js';
|
||||||
|
import * as d3Tools from '../../../constants/d3Tools';
|
||||||
|
import { SHAPE_TYPE_PROPERTIES } from '../../../constants/shapeTypeProperties.js';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:reducer:twist');
|
||||||
|
|
||||||
|
const CHANGE_FACTOR = 0.002;
|
||||||
|
const maxTwist = 1;
|
||||||
|
|
||||||
|
export default function twistReducer(state, action) {
|
||||||
|
// if (action.log !== false) debug(action.type);
|
||||||
|
|
||||||
|
switch (action.category) {
|
||||||
|
case actions.CAT_SELECTION:
|
||||||
|
return initTwist(state);
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.D3_CHANGE_TOOL:
|
||||||
|
return initTwist(state);
|
||||||
|
|
||||||
|
case actions.TWIST_START:
|
||||||
|
case actions.TWIST_END:
|
||||||
|
state = update(state, {
|
||||||
|
d3: {
|
||||||
|
twist: {
|
||||||
|
active: { $set: action.type === actions.TWIST_START }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return state;
|
||||||
|
|
||||||
|
|
||||||
|
case actions.TWIST:
|
||||||
|
const delta = action.delta.x * 1 * CHANGE_FACTOR;
|
||||||
|
|
||||||
|
state = update(state, {
|
||||||
|
d3: {
|
||||||
|
twist: {
|
||||||
|
rotation: { $set: state.d3.twist.rotation + delta }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const selectionData of state.selection.objects) {
|
||||||
|
const shapeData = state.objectsById[selectionData.id];
|
||||||
|
if (!SHAPE_TYPE_PROPERTIES[shapeData.type].tools[d3Tools.TWIST]) continue;
|
||||||
|
const twist = Utils.MathExtended.clamb(shapeData.twist + delta, -maxTwist, maxTwist);
|
||||||
|
// debug(UID, ': twist: ', shapeData.twist, '>', twist);
|
||||||
|
|
||||||
|
state = update(state, {
|
||||||
|
objectsById: {
|
||||||
|
[shapeData.UID]: { twist: { $set: twist } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTwist(state) {
|
||||||
|
const twist = getInitialTwist(state);
|
||||||
|
return update(state, {
|
||||||
|
d3: {
|
||||||
|
twist: {
|
||||||
|
rotation: { $set: twist },
|
||||||
|
active: { $set: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialTwist(state) {
|
||||||
|
const selectedObjects = state.selection.objects.map(({ id }) => state.objectsById[id]);
|
||||||
|
let twist;
|
||||||
|
if (selectedObjects.length === 0) {
|
||||||
|
twist = 0;
|
||||||
|
} else if (selectedObjects.length === 1) {
|
||||||
|
// one shape selected: use shape's twist
|
||||||
|
const [selectedObject] = selectedObjects;
|
||||||
|
twist = selectedObject.twist;
|
||||||
|
} else {
|
||||||
|
twist = 0;
|
||||||
|
}
|
||||||
|
return twist;
|
||||||
|
}
|
267
src/reducer/index.js
Normal file
267
src/reducer/index.js
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import undoable from 'redux-undo';
|
||||||
|
import undoFilter from '../utils/undoFilter.js';
|
||||||
|
import * as actions from '../actions/index.js';
|
||||||
|
import * as d2Tools from '../constants/d2Tools.js';
|
||||||
|
import * as d3Tools from '../constants/d3Tools.js';
|
||||||
|
import * as contextTools from '../constants/contextTools.js';
|
||||||
|
import { ERASER_SIZES, BRUSH_SIZES } from '../constants/d2Constants.js';
|
||||||
|
import update from 'react-addons-update';
|
||||||
|
import { defaultCamera, cameraReducer } from './d3/tools/cameraReducer.js';
|
||||||
|
import d2AddImageReducer from './d2/addImageReducer.js';
|
||||||
|
import d2ToolReducer from './d2/toolReducer.js';
|
||||||
|
import d3ToolReducer from './d3/toolReducer.js';
|
||||||
|
import d2PinchZoomReducer from './d2/pinchZoomReducer.js';
|
||||||
|
import d2WheelZoomReducer from './d2/wheelZoomReducer.js';
|
||||||
|
import d2PanZoomReducer from './d2/panReducer.js';
|
||||||
|
import selectionReducer from './selectionReducer.js';
|
||||||
|
import selectionOperationReducer from './selectionOperationReducer.js';
|
||||||
|
import contextReducer from './contextReducer.js';
|
||||||
|
import { Matrix, Vector } from 'cal';
|
||||||
|
import {
|
||||||
|
setActiveSpace, addSpaceActive, setActive2D, removeAllObjects, getActive2D, addObject
|
||||||
|
} from './objectReducers.js';
|
||||||
|
import menusReducer from './menusReducer.js';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:reducer:sketcher');
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
spaces: {
|
||||||
|
world: {
|
||||||
|
matrix: new THREE.Matrix4(),
|
||||||
|
objectIds: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
objectsById: {},
|
||||||
|
activeSpace: 'world',
|
||||||
|
objectIdCounter: 0,
|
||||||
|
context: {
|
||||||
|
color: 0x96cbef
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
transform: new Matrix(),
|
||||||
|
// array of objects (one per selected object),
|
||||||
|
// containing the object id and it's initialTransform (pre selection transform)
|
||||||
|
objects: []
|
||||||
|
},
|
||||||
|
d2: {
|
||||||
|
brush: {
|
||||||
|
size: BRUSH_SIZES[contextTools.BRUSH_SIZE_MEDIUM]
|
||||||
|
},
|
||||||
|
eraser: {
|
||||||
|
active: false,
|
||||||
|
size: ERASER_SIZES[contextTools.ERASER_SIZE_MEDIUM]
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
active: false,
|
||||||
|
handle: '',
|
||||||
|
dragSelect: {
|
||||||
|
start: new Vector(),
|
||||||
|
end: new Vector()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trace: {
|
||||||
|
start: new Vector(),
|
||||||
|
position: new Vector(),
|
||||||
|
active: false,
|
||||||
|
floodFillData: {
|
||||||
|
edge: [],
|
||||||
|
fill: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tool: d2Tools.FREE_HAND,
|
||||||
|
toolbar: {
|
||||||
|
open: []
|
||||||
|
},
|
||||||
|
canvasMatrix: new Matrix(),
|
||||||
|
dragging: false
|
||||||
|
},
|
||||||
|
d3: {
|
||||||
|
height: {
|
||||||
|
handle: '',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
twist: {
|
||||||
|
rotation: 0,
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
sculpt: {
|
||||||
|
handles: [],
|
||||||
|
activeHandle: null
|
||||||
|
},
|
||||||
|
tool: d3Tools.HEIGHT,
|
||||||
|
toolbar: {
|
||||||
|
open: []
|
||||||
|
},
|
||||||
|
camera: defaultCamera,
|
||||||
|
dragging: false
|
||||||
|
},
|
||||||
|
menus: menusReducer(undefined, {})
|
||||||
|
};
|
||||||
|
|
||||||
|
function sketcherReducer(state = initialState, action) {
|
||||||
|
// if (action.log !== false) debug(action.type);
|
||||||
|
|
||||||
|
switch (action.category) {
|
||||||
|
case actions.CAT_SELECTION:
|
||||||
|
const preSelectionState = state;
|
||||||
|
state = selectionReducer(state, action);
|
||||||
|
// if selection changed
|
||||||
|
if (state.selection.objects !== preSelectionState.selection.objects) {
|
||||||
|
state = d2ToolReducer(state, action);
|
||||||
|
state = d3ToolReducer(state, action);
|
||||||
|
state = updateMenus(state, action);
|
||||||
|
state = contextReducer(state, action);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.ADD_OBJECT:
|
||||||
|
return addObject(state, action.objectData);
|
||||||
|
|
||||||
|
case actions.D2_DRAG_START:
|
||||||
|
case actions.D2_DRAG:
|
||||||
|
case actions.D2_DRAG_END:
|
||||||
|
case actions.D2_TAP:
|
||||||
|
case actions.TRACE_DRAG:
|
||||||
|
case actions.TRACE_DRAG_END:
|
||||||
|
case actions.TRACE_TAP:
|
||||||
|
case `${actions.FLOOD_FILL}_PENDING`:
|
||||||
|
case `${actions.FLOOD_FILL}_FULFILLED`:
|
||||||
|
case `${actions.FLOOD_FILL}_REJECTED`:
|
||||||
|
case `${actions.TRACE_FLOOD_FILL}_PENDING`:
|
||||||
|
case `${actions.TRACE_FLOOD_FILL}_FULFILLED`:
|
||||||
|
case `${actions.TRACE_FLOOD_FILL}_REJECTED`:
|
||||||
|
case actions.TRANSFORM_START:
|
||||||
|
case actions.TRANSFORM:
|
||||||
|
case actions.TRANSFORM_END:
|
||||||
|
case actions.MULTITOUCH_TRANSFORM_START:
|
||||||
|
case actions.MULTITOUCH_TRANSFORM:
|
||||||
|
case actions.MULTITOUCH_TRANSFORM_END:
|
||||||
|
case actions.D2_TEXT_INIT:
|
||||||
|
case actions.D2_TEXT_INPUT_CHANGE:
|
||||||
|
case actions.D2_TEXT_ADD:
|
||||||
|
case actions.MOVE_SELECTION:
|
||||||
|
return d2ToolReducer(state, action);
|
||||||
|
|
||||||
|
case `${actions.ADD_IMAGE}_FULFILLED`:
|
||||||
|
return d2AddImageReducer(state, action);
|
||||||
|
|
||||||
|
case actions.D2_MOUSE_WHEEL:
|
||||||
|
return d2WheelZoomReducer(state, action);
|
||||||
|
|
||||||
|
case actions.D2_SECOND_DRAG:
|
||||||
|
return d2PanZoomReducer(state, action);
|
||||||
|
|
||||||
|
case actions.D2_MULTITOUCH_START:
|
||||||
|
case actions.D2_MULTITOUCH:
|
||||||
|
case actions.D2_MULTITOUCH_END:
|
||||||
|
state = d2ToolReducer(state, action);
|
||||||
|
state = d2PinchZoomReducer(state, action);
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.D3_DRAG_START:
|
||||||
|
case actions.D3_DRAG:
|
||||||
|
case actions.D3_DRAG_END:
|
||||||
|
case actions.D3_SECOND_DRAG_START:
|
||||||
|
case actions.D3_SECOND_DRAG:
|
||||||
|
case actions.D3_SECOND_DRAG_END:
|
||||||
|
case actions.D3_TAP:
|
||||||
|
case actions.D3_MOUSE_WHEEL:
|
||||||
|
case actions.D3_MULTITOUCH_START:
|
||||||
|
case actions.D3_MULTITOUCH:
|
||||||
|
case actions.D3_MULTITOUCH_END:
|
||||||
|
case actions.HEIGHT_START:
|
||||||
|
case actions.HEIGHT:
|
||||||
|
case actions.HEIGHT_END:
|
||||||
|
case actions.TWIST_START:
|
||||||
|
case actions.TWIST:
|
||||||
|
case actions.TWIST_END:
|
||||||
|
case actions.SCULPT_START:
|
||||||
|
case actions.SCULPT:
|
||||||
|
case actions.SCULPT_END:
|
||||||
|
case actions.ADD_SCULPT_HANDLE:
|
||||||
|
case actions.REMOVE_SCULPT_HANDLE:
|
||||||
|
case actions.STAMP:
|
||||||
|
return d3ToolReducer(state, action);
|
||||||
|
|
||||||
|
case actions.D2_CHANGE_TOOL:
|
||||||
|
state = setActive2D(state, null);
|
||||||
|
state = selectionReducer(state, action);
|
||||||
|
state = d2ToolReducer(state, action); // switch and initialize tool
|
||||||
|
state = updateMenus(state, action);
|
||||||
|
state = contextReducer(state, action);
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.D3_CHANGE_TOOL:
|
||||||
|
state = d3ToolReducer(state, action); // switch and initialize tool
|
||||||
|
state = updateMenus(state, action);
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.CONTEXT_CHANGE_TOOL:
|
||||||
|
state = contextReducer(state, action);
|
||||||
|
state = updateMenus(state, action);
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.CLEAR:
|
||||||
|
state = setActive2D(state, null);
|
||||||
|
state = removeAllObjects(state);
|
||||||
|
state = selectionReducer(state, action); // deselect all
|
||||||
|
state = d2PinchZoomReducer(state, action); // reset zoom
|
||||||
|
state = update(state, {
|
||||||
|
d3: { camera: { $set: cameraReducer(state.d3.camera, action) } }
|
||||||
|
});
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.DUPLICATE_SELECTION:
|
||||||
|
case actions.DELETE_SELECTION:
|
||||||
|
case actions.UNION:
|
||||||
|
case actions.INTERSECT:
|
||||||
|
return selectionOperationReducer(state, action);
|
||||||
|
|
||||||
|
case actions.MENU_OPEN:
|
||||||
|
case actions.MENU_CLOSE:
|
||||||
|
return updateMenus(state, action);
|
||||||
|
|
||||||
|
case actions.UPDATE_MATRIX:
|
||||||
|
return update(state, {
|
||||||
|
objectsById: { [action.id]: { transform: { $set: action.transform } } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO these actions should reset
|
||||||
|
// actions.user.USER_LOGOUT_FULFILLED
|
||||||
|
// actions.files.OPEN_SKETCH:
|
||||||
|
// actions.files.FILES_LOAD_FULFILLED:
|
||||||
|
case actions.CLEAR:
|
||||||
|
return initialState;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default undoable(sketcherReducer, {
|
||||||
|
filter: undoFilter,
|
||||||
|
initTypes: []
|
||||||
|
// debug: true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function updateMenus(state, action) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
menus: menusReducer(state.menus, action)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getD2ActiveShape(state) {
|
||||||
|
return getActive2D(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEmpty(state) {
|
||||||
|
return Object.keys(state.sketcher.present.objectsById).length === 0;
|
||||||
|
}
|
100
src/reducer/objectReducer.js
Normal file
100
src/reducer/objectReducer.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import shortid from 'shortid';
|
||||||
|
import { SHAPE_TYPE_PROPERTIES } from '../constants/shapeTypeProperties.js';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
const debug = createDebug('d3d:reducer:object');
|
||||||
|
|
||||||
|
function generateUID(state) {
|
||||||
|
return `S${state.objectIdCounter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addObject(state, object, UID = generateUID(state)) {
|
||||||
|
const { defaultProperties } = SHAPE_TYPE_PROPERTIES[object.type];
|
||||||
|
|
||||||
|
object = {
|
||||||
|
...defaultProperties,
|
||||||
|
color: state.context.color,
|
||||||
|
space: state.activeSpace,
|
||||||
|
...object,
|
||||||
|
UID
|
||||||
|
};
|
||||||
|
|
||||||
|
debug('addObject: ', UID);
|
||||||
|
state = update(state, {
|
||||||
|
spaces: { [object.space]: { objectIds: { $push: [UID] } } },
|
||||||
|
objectsById: { [UID]: { $set: object } },
|
||||||
|
objectIdCounter: { $set: state.objectIdCounter + 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
export function removeObject(state, UID) {
|
||||||
|
debug('removeObject: ', UID);
|
||||||
|
|
||||||
|
const object = state.objectsById[UID];
|
||||||
|
const { space } = object;
|
||||||
|
|
||||||
|
const filteredObjectIds = state.spaces[space].objectIds;
|
||||||
|
|
||||||
|
const filteredObjectsById = { ...state.objectsById };
|
||||||
|
delete filteredObjectsById[UID];
|
||||||
|
|
||||||
|
return update(state, {
|
||||||
|
spaces: { [space]: { objectIds: { $splice: [[filteredObjectIds.indexOf(UID), 1]] } } },
|
||||||
|
objectsById: { $set: filteredObjectsById }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function removeAllObjects(state) {
|
||||||
|
debug('removeAllObjects: ');
|
||||||
|
|
||||||
|
return update(state, {
|
||||||
|
objectsById: { $set: {} },
|
||||||
|
activeSpace: { $set: 'world' },
|
||||||
|
spaces: { $set: {
|
||||||
|
world: {
|
||||||
|
matrix: new THREE.Matrix4(),
|
||||||
|
objectIds: []
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function setActive2D(state, UID) {
|
||||||
|
debug('activeShape: ', UID);
|
||||||
|
return update(state, {
|
||||||
|
d2: { activeShape: { $set: UID } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function addObjectActive2D(state, object) {
|
||||||
|
const UID = generateUID(state);
|
||||||
|
state = addObject(state, object, UID);
|
||||||
|
return setActive2D(state, UID);
|
||||||
|
}
|
||||||
|
export function removeObjectActive2D(state, UID) {
|
||||||
|
const newState = removeObject(state, UID);
|
||||||
|
return setActive2D(newState, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActive2D(state) {
|
||||||
|
return state.d2.activeShape ? state.objectsById[state.d2.activeShape] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSpaceActive(state, matrix, space = shortid.generate()) {
|
||||||
|
state = addSpace(state, matrix, space);
|
||||||
|
return setActiveSpace(state, space);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setActiveSpace(state, space) {
|
||||||
|
debug('activeSpace: ', space);
|
||||||
|
return update(state, { activeSpace: { $set: space } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSpace(state, matrix, space = shortid.generate()) {
|
||||||
|
debug('addSpace: ', space);
|
||||||
|
return update(state, {
|
||||||
|
spaces: { [space]: { $set: {
|
||||||
|
objectIds: [],
|
||||||
|
matrix
|
||||||
|
} } }
|
||||||
|
});
|
||||||
|
}
|
139
src/reducer/selectionOperationReducer.js
Normal file
139
src/reducer/selectionOperationReducer.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import ClipperShape from 'clipper-js';
|
||||||
|
import { Matrix } from 'cal';
|
||||||
|
import { addObject, removeObject } from '../objectReducers.js';
|
||||||
|
import { recursiveClone } from '../utils/clone.js';
|
||||||
|
import { shapeToPoints } from '../shape/shapeToPoints.js';
|
||||||
|
import { applyMatrixOnShape, pathToVectorPath } from '../utils/vectorUtils.js';
|
||||||
|
import subtractShapeFromState from '../utils/subtractShapeFromState.js';
|
||||||
|
import * as d2Tools from '../constants/d2Tools';
|
||||||
|
import { CLIPPER_PRECISION } from '../constants/d2Constants.js';
|
||||||
|
|
||||||
|
const LINE_WIDTH = 0.5;
|
||||||
|
|
||||||
|
export default function (state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.DELETE_SELECTION:
|
||||||
|
for (const { id } of state.selection.objects) {
|
||||||
|
state = removeObject(state, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
state = update(state, {
|
||||||
|
selection: {
|
||||||
|
objects: { $set: [] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.DUPLICATE_SELECTION:
|
||||||
|
for (const { id } of state.selection.objects) {
|
||||||
|
const shapeData = state.objectsById[id];
|
||||||
|
state = addObject(state, recursiveClone(shapeData));
|
||||||
|
}
|
||||||
|
|
||||||
|
// force update selection so alpha is redrawn
|
||||||
|
state = update(state, {
|
||||||
|
selection: {
|
||||||
|
objects: { $set: [...state.selection.objects] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case actions.UNION: {
|
||||||
|
let unionShape = new ClipperShape([], true);
|
||||||
|
|
||||||
|
const shapeDataDictation = state.objectsById[state.selection.objects[0].id];
|
||||||
|
|
||||||
|
for (const { id } of state.selection.objects) {
|
||||||
|
const shapeData = state.objectsById[id];
|
||||||
|
|
||||||
|
if (!shapeData.fill) continue;
|
||||||
|
|
||||||
|
state = removeObject(state, id);
|
||||||
|
|
||||||
|
const shapes = applyMatrixOnShape(
|
||||||
|
shapeToPoints(shapeData).reduce((a, { points, holes }) => a.concat([points, ...holes]), []),
|
||||||
|
shapeData.transform
|
||||||
|
);
|
||||||
|
|
||||||
|
const shape = new ClipperShape(shapes, shapeData.fill, true, false, false)
|
||||||
|
.scaleUp(CLIPPER_PRECISION)
|
||||||
|
.round();
|
||||||
|
unionShape = unionShape.union(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unionShapes = unionShape.scaleDown(CLIPPER_PRECISION).seperateShapes().map(shape => {
|
||||||
|
shape = shape
|
||||||
|
.removeDuplicates()
|
||||||
|
.mapToLower()
|
||||||
|
.map(pathToVectorPath);
|
||||||
|
shape.forEach(path => path.push(path[0].clone()));
|
||||||
|
return shape;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const shape of unionShapes) {
|
||||||
|
const [points, ...holes] = shape;
|
||||||
|
|
||||||
|
state = addObject(state, {
|
||||||
|
...recursiveClone(shapeDataDictation),
|
||||||
|
type: 'COMPOUND_PATH',
|
||||||
|
transform: new Matrix(),
|
||||||
|
points,
|
||||||
|
holes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
state = update(state, {
|
||||||
|
selection: {
|
||||||
|
objects: { $set: [] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.INTERSECT: {
|
||||||
|
let unionShape = new ClipperShape([], true);
|
||||||
|
|
||||||
|
for (const { id } of state.selection.objects) {
|
||||||
|
const shapeData = state.objectsById[id];
|
||||||
|
|
||||||
|
const shapes = applyMatrixOnShape(
|
||||||
|
shapeToPoints(shapeData).reduce((a, { points, holes }) => a.concat([points, ...holes]), []),
|
||||||
|
shapeData.transform
|
||||||
|
);
|
||||||
|
|
||||||
|
let shape = new ClipperShape(shapes, shapeData.fill, true, false, false)
|
||||||
|
.scaleUp(CLIPPER_PRECISION)
|
||||||
|
.round();
|
||||||
|
if (!shapeData.fill) {
|
||||||
|
shape = shape
|
||||||
|
.offset(LINE_WIDTH * CLIPPER_PRECISION, { jointType: 'jtSquare', endType: 'etOpenButt' })
|
||||||
|
.simplify('pftNonZero');
|
||||||
|
}
|
||||||
|
unionShape = unionShape.union(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
unionShape = unionShape.scaleDown(CLIPPER_PRECISION);
|
||||||
|
|
||||||
|
state = subtractShapeFromState(state, unionShape, d2Tools.ERASER, {
|
||||||
|
scale: CLIPPER_PRECISION,
|
||||||
|
skip: state.selection.objects.map(({ id }) => id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// force update selection so alpha is redrawn
|
||||||
|
state = update(state, {
|
||||||
|
selection: {
|
||||||
|
objects: { $set: [...state.selection.objects] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
136
src/reducer/selectionReducer.js
Normal file
136
src/reducer/selectionReducer.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import * as actions from '../actions/index.js';
|
||||||
|
import { Vector } from 'cal';
|
||||||
|
import { shapeToPoints } from '../utils/shapeDataUtils.js';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
const debug = createDebug('d3d:reducer:selection');
|
||||||
|
|
||||||
|
export default function selectionReducer(state, action) {
|
||||||
|
// if (action.log !== false) debug(action.type);
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.sketcher.SELECT: {
|
||||||
|
const { shapeID } = action;
|
||||||
|
return selectObject(state, shapeID);
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.sketcher.BED_SELECT: {
|
||||||
|
return update(state, {
|
||||||
|
activeSpace: { $set: 'world' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.sketcher.DESELECT: {
|
||||||
|
const { shapeID } = action;
|
||||||
|
return deselectObject(state, shapeID);
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.sketcher.TOGGLE_SELECT: {
|
||||||
|
const { shapeID } = action;
|
||||||
|
return toggleSelectObject(state, shapeID);
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.sketcher.SELECT_ALL: {
|
||||||
|
return state.spaces.world.objectIds.reduce((_state, id) => selectObject(_state, id), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
case actions.sketcher.D2_CHANGE_TOOL:
|
||||||
|
case actions.sketcher.DESELECT_ALL:
|
||||||
|
case actions.sketcher.CLEAR:
|
||||||
|
case actions.files.OPEN_SKETCH:
|
||||||
|
case actions.files.FILES_LOAD_FULFILLED:
|
||||||
|
return deselectAll(state);
|
||||||
|
|
||||||
|
case actions.sketcher.DRAG_SELECT:
|
||||||
|
const { start, end } = state.d2.transform.dragSelect;
|
||||||
|
const matrix = action.screenMatrixZoom.inverseMatrix();
|
||||||
|
|
||||||
|
const min = new Vector(Math.min(start.x, end.x), Math.min(start.y, end.y)).applyMatrix(matrix);
|
||||||
|
const max = new Vector(Math.max(start.x, end.x), Math.max(start.y, end.y)).applyMatrix(matrix);
|
||||||
|
|
||||||
|
return addSelectFromBoundingBox(state, min, max);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSelectFromBoundingBox(state, min, max) {
|
||||||
|
for (const id of state.spaces[state.activeSpace].objectIds) {
|
||||||
|
if (isSelected(state, id)) continue;
|
||||||
|
|
||||||
|
const shapeData = state.objectsById[id];
|
||||||
|
const compoundPaths = shapeToPoints(shapeData);
|
||||||
|
const points = compoundPaths.reduce((a, { points: b }) => a.concat(b), []);
|
||||||
|
|
||||||
|
for (let point of points) {
|
||||||
|
point = point.applyMatrix(shapeData.transform);
|
||||||
|
|
||||||
|
if (point.x > min.x && point.y > min.y && point.x < max.x && point.y < max.y) {
|
||||||
|
state = selectObject(state, id);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected(state, shapeUID) {
|
||||||
|
return state.selection.objects
|
||||||
|
.map(({ id }) => id)
|
||||||
|
.indexOf(shapeUID) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectObject(state, id) {
|
||||||
|
debug('select: ', id);
|
||||||
|
if (isSelected(state, id)) return state;
|
||||||
|
|
||||||
|
const { space } = state.objectsById[id];
|
||||||
|
|
||||||
|
const objects = state.selection.objects
|
||||||
|
.filter(object => state.objectsById[object.id].space === space);
|
||||||
|
objects.push({ id });
|
||||||
|
|
||||||
|
return update(state, {
|
||||||
|
activeSpace: { $set: space },
|
||||||
|
selection: {
|
||||||
|
objects: { $set: objects }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselectObject(state, id) {
|
||||||
|
debug('deselect: ', id);
|
||||||
|
if (!isSelected(state, id)) return state;
|
||||||
|
|
||||||
|
const index = state.selection.objects.map((object) => object.id).indexOf(id);
|
||||||
|
return update(state, {
|
||||||
|
selection: {
|
||||||
|
objects: {
|
||||||
|
$splice: [[index, 1]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectObject(state, id) {
|
||||||
|
if (isSelected(state, id)) {
|
||||||
|
return deselectObject(state, id);
|
||||||
|
} else {
|
||||||
|
return selectObject(state, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselectAll(state) {
|
||||||
|
debug('deselect all');
|
||||||
|
if (state.selection.objects.length === 0) {
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
return update(state, {
|
||||||
|
selection: {
|
||||||
|
objects: { $set: [] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
17
src/utils/clone.js
Normal file
17
src/utils/clone.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export function recursiveClone(object) {
|
||||||
|
if (object instanceof Image || object instanceof HTMLCanvasElement) {
|
||||||
|
return object;
|
||||||
|
} else if (object.clone instanceof Function) {
|
||||||
|
return object.clone();
|
||||||
|
} else if (object instanceof Array) {
|
||||||
|
return object.map(recursiveClone);
|
||||||
|
} else if (typeof object === 'object') {
|
||||||
|
const clone = {};
|
||||||
|
for (const key in object) {
|
||||||
|
clone[key] = recursiveClone(object[key]);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
} else {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
50
src/utils/matrixUtils.js
Normal file
50
src/utils/matrixUtils.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Matrix, Vector } from 'cal';
|
||||||
|
|
||||||
|
export function calculateGestureMatrix(positions, previousPositions, screenMatrix, { rotate, scale, pan }) {
|
||||||
|
const matrix = screenMatrix.inverseMatrix();
|
||||||
|
const screenCenter = positions[0].add(positions[1]).scale(0.5).applyMatrix(matrix);
|
||||||
|
|
||||||
|
let gestureMatrix = new Matrix();
|
||||||
|
if (pan) {
|
||||||
|
const previousCenter = previousPositions[0].add(previousPositions[1]).scale(0.5);
|
||||||
|
const center = positions[0].add(positions[1]).scale(0.5);
|
||||||
|
const gesturePan = center.subtract(previousCenter).applyMatrix(matrix.normalize());
|
||||||
|
gestureMatrix = gestureMatrix.translate(gesturePan.x, gesturePan.y);
|
||||||
|
}
|
||||||
|
if (scale) {
|
||||||
|
const previousLength = previousPositions[0].distanceTo(previousPositions[1]);
|
||||||
|
const length = positions[0].distanceTo(positions[1]);
|
||||||
|
const gestureScale = length / previousLength;
|
||||||
|
gestureMatrix = gestureMatrix.scaleAroundAbsolute(gestureScale, gestureScale, screenCenter);
|
||||||
|
}
|
||||||
|
if (rotate) {
|
||||||
|
const previousAngle = previousPositions[1].subtract(previousPositions[0]).angle();
|
||||||
|
const angle = positions[1].subtract(positions[0]).angle();
|
||||||
|
const gestureRotation = angle - previousAngle;
|
||||||
|
gestureMatrix = gestureMatrix.rotateAroundAbsolute(gestureRotation, screenCenter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return gestureMatrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decomposeMatrix(matrix) {
|
||||||
|
const { sx, sy, x, y, rotation } = matrix;
|
||||||
|
return { position: new Vector(x, y), scale: new Vector(sx, sy), rotation };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculatePointInImage(point, shapeData, screenMatrix) {
|
||||||
|
const { width, height } = shapeData.imageData;
|
||||||
|
const centerOffset = new Vector(width / 2, height / 2);
|
||||||
|
const matrix = shapeData.transform.multiplyMatrix(screenMatrix).inverseMatrix();
|
||||||
|
|
||||||
|
return point
|
||||||
|
.applyMatrix(matrix)
|
||||||
|
.add(centerOffset)
|
||||||
|
.round();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNegative(transform) {
|
||||||
|
const negativeX = transform.sx > 0;
|
||||||
|
const negativeY = transform.sy > 0;
|
||||||
|
return !((negativeX && negativeY) || (!negativeX && !negativeY));
|
||||||
|
}
|
75
src/utils/selectionUtils.js
Normal file
75
src/utils/selectionUtils.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import { shapeToPoints, getPointsBounds } from './shapeDataUtils.js';
|
||||||
|
import { Vector } from 'cal';
|
||||||
|
import arrayMemoizer from './arrayMemoizer.js';
|
||||||
|
import memoize from 'memoizee';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:util:selection');
|
||||||
|
|
||||||
|
// Memoized selector that returns the same array of shapeSata's when
|
||||||
|
// - the selection array didn't change
|
||||||
|
// - the objects in the resulting array didn't change
|
||||||
|
// enables memoization of utils that use this array
|
||||||
|
export const getSelectedObjectsSelector = arrayMemoizer(getSelectedObjects);
|
||||||
|
// export const getSelectedObjectsSelector = getSelectedObjects;
|
||||||
|
function getSelectedObjects(selectedObjects, objectsById) {
|
||||||
|
return selectedObjects.map(({ id }) => objectsById[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get the boundingbox of (multiple) shape data's
|
||||||
|
* This generally can by two types of boundingboxes.
|
||||||
|
* 1) The boundingbox of the shapes without the selection transformations (rotation, scale etc).
|
||||||
|
* Requires a selectionTransform matrix.
|
||||||
|
* This is used to display the transformations that are applied to a selection,
|
||||||
|
* showing a for example rotated boundingbox.
|
||||||
|
* Views like the 2D SelectionView will place it over the selection
|
||||||
|
* by applying the selection transformations (translating it to Objects Container Space).
|
||||||
|
* 2) The boundingbox of the shapes with the selection transformations.
|
||||||
|
* This is used to get the axis-aligned bounding box of shapes, this is
|
||||||
|
* usually used to contain the shapes within the drawing area.
|
||||||
|
*/
|
||||||
|
export const getBoundingBox = memoize(getBoundingBoxRaw, { max: 1 });
|
||||||
|
// export const getBoundingBox = getBoundingBoxRaw;
|
||||||
|
function getBoundingBoxRaw(shapeDatas, selectionTransform) {
|
||||||
|
if (selectionTransform !== undefined) {
|
||||||
|
// To show a rectangle / box around the selection that has the
|
||||||
|
// selection transforms (rotation, scale) of the selection we first have to
|
||||||
|
// remove the selection transforms so we can get the boundingbox of
|
||||||
|
// all the selected shapes as if they where not transformed yet.
|
||||||
|
// We do this by multiplying it by the inverse of the selection transforms.
|
||||||
|
// In the case of one shape the selection transforms equals it's own
|
||||||
|
// transform, but in the case of multiple shapes this only contains the
|
||||||
|
// transformations since the last selection change.
|
||||||
|
const selectionTransformInverse = selectionTransform.inverseMatrix();
|
||||||
|
shapeDatas = shapeDatas.map(shapeData => ({
|
||||||
|
...shapeData,
|
||||||
|
transform: shapeData.transform.multiplyMatrix(selectionTransformInverse)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let minX = Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
let minZ = Infinity;
|
||||||
|
let maxZ = -Infinity;
|
||||||
|
for (const shapeData of shapeDatas) {
|
||||||
|
const compoundPath = shapeToPoints(shapeData);
|
||||||
|
const { min, max } = getPointsBounds(compoundPath, shapeData.transform);
|
||||||
|
minX = (min.x < minX) ? min.x : minX;
|
||||||
|
minY = (min.y < minY) ? min.y : minY;
|
||||||
|
maxX = (max.x > maxX) ? max.x : maxX;
|
||||||
|
maxY = (max.y > maxY) ? max.y : maxY;
|
||||||
|
|
||||||
|
const { z, height } = shapeData;
|
||||||
|
minZ = (z < minZ) ? z : minZ;
|
||||||
|
maxZ = (z + height > maxZ) ? z + height : maxZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = new THREE.Vector3(minX, minZ, minY);
|
||||||
|
const max = new THREE.Vector3(maxX, maxZ, maxY);
|
||||||
|
const center = new Vector(minX, minY).add(new Vector(maxX, maxY)).scale(0.5);
|
||||||
|
|
||||||
|
return { min, max, center };
|
||||||
|
}
|
165
src/utils/subtractShapeFromState.js
Normal file
165
src/utils/subtractShapeFromState.js
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import update from 'react-addons-update';
|
||||||
|
import { Matrix } from 'cal';
|
||||||
|
import ClipperShape from 'clipper-js';
|
||||||
|
import { recursiveClone } from './clone.js';
|
||||||
|
import { addObject, removeObject } from '../reducers/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 '../reducers/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
|
||||||
|
.seperateShapes()
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
103
src/utils/traceUtils.js
Normal file
103
src/utils/traceUtils.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { pathToVectorPath, applyMatrixOnPath } from 'src/js/utils/shapeDataUtils.js';
|
||||||
|
import { Matrix } from 'cal';
|
||||||
|
import TraceWorker from 'workers/worker.js';
|
||||||
|
import { getPixel } from './colorUtils.js';
|
||||||
|
import memoize from 'memoizee';
|
||||||
|
|
||||||
|
const TRACE_WORKER = new TraceWorker();
|
||||||
|
|
||||||
|
export const prepareImageData = memoize(prepareImageDataRaw, { max: 1 });
|
||||||
|
function prepareImageDataRaw(canvas, x, y) {
|
||||||
|
const { width, height } = canvas;
|
||||||
|
const imageData = canvas.getContext('2d').getImageData(0, 0, width, height);
|
||||||
|
|
||||||
|
const compairValue = getPixel(imageData, y * width + x);
|
||||||
|
|
||||||
|
const data = new Uint8Array(width * height);
|
||||||
|
for (let i = 0; i < data.length; i ++) {
|
||||||
|
const pixel = getPixel(imageData, i);
|
||||||
|
const r = Math.abs(pixel.r - compairValue.r);
|
||||||
|
const g = Math.abs(pixel.g - compairValue.g);
|
||||||
|
const b = Math.abs(pixel.b - compairValue.b);
|
||||||
|
|
||||||
|
const value = Math.max(r, g, b);
|
||||||
|
|
||||||
|
data[i] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { width, height, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
const FLOOD_FILL_MULTIPLIER = 0.4;
|
||||||
|
export function calculateTolerance(start, current) {
|
||||||
|
const tolerance = start.distanceTo(current) * FLOOD_FILL_MULTIPLIER;
|
||||||
|
|
||||||
|
return tolerance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function floodFill(tolerance, image, start) {
|
||||||
|
const imageData = prepareImageData(image, start.x, start.y);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const callback = ({ data }) => {
|
||||||
|
switch (data.msg) {
|
||||||
|
case 'FLOOD_FILL':
|
||||||
|
TRACE_WORKER.removeEventListener('message', callback);
|
||||||
|
|
||||||
|
if (data.status === 'OK') {
|
||||||
|
resolve(data.traceData);
|
||||||
|
} else {
|
||||||
|
reject(data.error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
TRACE_WORKER.addEventListener('message', callback);
|
||||||
|
|
||||||
|
TRACE_WORKER.postMessage({
|
||||||
|
msg: 'FLOOD_FILL',
|
||||||
|
tolerance, imageData, start
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function traceFloodFill(traceData, shapeData) {
|
||||||
|
const { fill } = traceData;
|
||||||
|
const { width, height } = shapeData.imageData;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const callback = ({ data }) => {
|
||||||
|
switch (data.msg) {
|
||||||
|
case 'TRACE':
|
||||||
|
TRACE_WORKER.removeEventListener('message', callback);
|
||||||
|
|
||||||
|
const transform = new Matrix()
|
||||||
|
.translate(width / -2, height / -2)
|
||||||
|
.multiplyMatrix(shapeData.transform);
|
||||||
|
|
||||||
|
const paths = data.paths
|
||||||
|
.map(pathToVectorPath)
|
||||||
|
.map(path => applyMatrixOnPath(path, transform));
|
||||||
|
|
||||||
|
if (data.status === 'OK') {
|
||||||
|
resolve(paths);
|
||||||
|
} else {
|
||||||
|
reject(data.error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
TRACE_WORKER.addEventListener('message', callback);
|
||||||
|
|
||||||
|
TRACE_WORKER.postMessage({
|
||||||
|
msg: 'TRACE',
|
||||||
|
fill, width, height
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
26
src/utils/tweenUtils.js
Normal file
26
src/utils/tweenUtils.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import raf from 'raf';
|
||||||
|
|
||||||
|
export function tween(duration, callback) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let elapsedTime = 0;
|
||||||
|
let lastTime = performance.now();
|
||||||
|
|
||||||
|
const step = () => {
|
||||||
|
const currentTime = performance.now();
|
||||||
|
const deltaTime = currentTime - lastTime;
|
||||||
|
lastTime = currentTime;
|
||||||
|
|
||||||
|
elapsedTime += deltaTime;
|
||||||
|
const progress = elapsedTime / duration;
|
||||||
|
|
||||||
|
if (progress >= 1.0) {
|
||||||
|
callback(1.0);
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
callback(progress);
|
||||||
|
raf(step);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
step(lastTime);
|
||||||
|
});
|
||||||
|
}
|
52
src/utils/undoFilter.js
Normal file
52
src/utils/undoFilter.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import * as actions from '../actions/index.js';
|
||||||
|
import * as d2Tools from 'src/js/constants/d2Tools.js';
|
||||||
|
import * as contextTools from 'src/js/constants/contextTools.js';
|
||||||
|
// import createDebug from 'debug';
|
||||||
|
// const debug = createDebug('d3d:util:undoFilter');
|
||||||
|
|
||||||
|
const INCLUDE = [
|
||||||
|
`${actions.TRACE_FLOOD_FILL}_FULFILLED`,
|
||||||
|
`${actions.ADD_IMAGE}_FULFILLED`,
|
||||||
|
actions.TOGGLE_SELECT,
|
||||||
|
actions.TRANSFORM_END,
|
||||||
|
actions.MULTITOUCH_TRANSFORM_END,
|
||||||
|
actions.SELECT_ALL,
|
||||||
|
actions.DESELECT_ALL,
|
||||||
|
actions.STAMP,
|
||||||
|
actions.SELECT,
|
||||||
|
actions.D2_TEXT_ADD,
|
||||||
|
actions.CLEAR,
|
||||||
|
actions.TWIST_END,
|
||||||
|
actions.SCULPT_END,
|
||||||
|
actions.ADD_SCULPT_HANDLE,
|
||||||
|
actions.REMOVE_SCULPT_HANDLE,
|
||||||
|
actions.HEIGHT_END,
|
||||||
|
actions.DELETE_SELECTION,
|
||||||
|
actions.DUPLICATE_SELECTION,
|
||||||
|
actions.UNION,
|
||||||
|
actions.INTERSECT
|
||||||
|
];
|
||||||
|
|
||||||
|
const ACTION_INCLUDES = {
|
||||||
|
[actions.D2_DRAG_END]: [...d2Tools.SHAPE_TOOLS, ...d2Tools.PEN_TOOLS, d2Tools.ERASER],
|
||||||
|
[actions.D2_TAP]: [...d2Tools.SHAPE_TOOLS, d2Tools.BUCKET, d2Tools.ERASER]
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONTEXT_TOOL_CHANGES = [
|
||||||
|
contextTools.ALIGN_LEFT,
|
||||||
|
contextTools.ALIGN_HORIZONTAL,
|
||||||
|
contextTools.ALIGN_RIGHT,
|
||||||
|
contextTools.ALIGN_TOP,
|
||||||
|
contextTools.ALIGN_VERTICAL,
|
||||||
|
contextTools.ALIGN_BOTTOM
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function undoFilter(action, currentState) {
|
||||||
|
if (INCLUDE.indexOf(action.type) !== -1) return true;
|
||||||
|
if (ACTION_INCLUDES[action.type] && ACTION_INCLUDES[action.type].indexOf(currentState.d2.tool) !== -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (action.type === actions.CONTEXT_CHANGE_TOOL && CONTEXT_TOOL_CHANGES.includes(action.tool)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user