Doodle3D-Core/src/actions/index.js

509 lines
18 KiB
JavaScript

import { ActionCreators as undo } from 'redux-undo';
import * as notification from 'react-notification-system-redux';
import * as selectionUtils from '../utils/selectionUtils.js';
import { calculatePointInImage, decomposeMatrix } from '../utils/matrixUtils.js';
import { loadImage, prepareImage } from '../utils/imageUtils.js';
import * as traceUtils from '../utils/traceUtils.js';
import { createThrottle } from '../utils/async.js';
import { tween } from '../utils/tweenUtils.js';
import { DEFAULT_TRACE_TOLERANCE, MAX_TRACE_TOLERANCE } from '../constants/d2Constants.js';
import * as d2Tools from '../constants/d2Tools.js';
import { Matrix } from '@doodle3d/cal';
// import createDebug from 'debug';
// const debug = createDebug('d3d:actions');
export { undo };
export const ADD_OBJECT = 'ADD_OBJECT';
export const D2_DRAG_START = 'D2_DRAG_START';
export const D2_DRAG = 'D2_DRAG';
export const D2_DRAG_END = 'D2_DRAG_END';
export const D2_SECOND_DRAG_START = 'D2_SECOND_DRAG_START';
export const D2_SECOND_DRAG = 'D2_SECOND_DRAG';
export const D2_SECOND_DRAG_END = 'D2_SECOND_DRAG_END';
export const D2_TAP = 'D2_TAP';
export const D2_MOUSE_WHEEL = 'D2_MOUSE_WHEEL';
export const D2_MULTITOUCH_START = 'D2_MULTITOUCH_START';
export const D2_MULTITOUCH = 'D2_MULTITOUCH';
export const D2_MULTITOUCH_END = 'D2_MULTITOUCH_END';
export const D3_DRAG_START = 'D3_DRAG_START';
export const D3_DRAG = 'D3_DRAG';
export const D3_DRAG_END = 'D3_DRAG_END';
export const D3_SECOND_DRAG_START = 'D3_SECOND_DRAG_START';
export const D3_SECOND_DRAG = 'D3_SECOND_DRAG';
export const D3_SECOND_DRAG_END = 'D3_SECOND_DRAG_END';
export const D3_TAP = 'D3_TAP';
export const D3_MOUSE_WHEEL = 'D3_MOUSE_WHEEL';
export const D3_MULTITOUCH_START = 'D3_MULTITOUCH_START';
export const D3_MULTITOUCH = 'D3_MULTITOUCH';
export const D3_MULTITOUCH_END = 'D3_MULTITOUCH_END';
export const D2_CHANGE_TOOL = 'D2_CHANGE_TOOL';
export const D3_CHANGE_TOOL = 'D3_CHANGE_TOOL';
export const CONTEXT_CHANGE_TOOL = 'CONTEXT_CHANGE_TOOL';
export const HEIGHT_START = 'HEIGHT_START';
export const HEIGHT = 'HEIGHT';
export const HEIGHT_END = 'HEIGHT_END';
export const TWIST_START = 'TWIST_START';
export const TWIST = 'TWIST';
export const TWIST_END = 'TWIST_END';
export const SCULPT_START = 'SCULPT_START';
export const SCULPT = 'SCULPT';
export const SCULPT_END = 'SCULPT_END';
export const ADD_SCULPT_HANDLE = 'ADD_SCULPT_HANDLE';
export const REMOVE_SCULPT_HANDLE = 'REMOVE_SCULPT_HANDLE';
export const STAMP = 'STAMP';
export const BED_SELECT = 'BED_SELECT';
export const CLEAR = 'CLEAR';
export const TRANSFORM_START = 'TRANSFORM_START';
export const TRANSFORM = 'TRANSFORM';
export const TRANSFORM_END = 'TRANSFORM_END';
export const DRAG_SELECT = 'DRAG_SELECT';
export const MULTITOUCH_TRANSFORM_START = 'MULTITOUCH_TRANSFORM_START';
export const MULTITOUCH_TRANSFORM = 'MULTITOUCH_TRANSFORM';
export const MULTITOUCH_TRANSFORM_END = 'MULTITOUCH_TRANSFORM_END';
export const SELECT = 'SELECT';
export const UPDATE_MATRIX = 'UPDATE_MATRIX';
export const DESELECT = 'DESELECT';
export const TOGGLE_SELECT = 'TOGGLE_SELECT';
export const DESELECT_ALL = 'DESELECT_ALL';
export const SELECT_ALL = 'SELECT_ALL';
export const DELETE_SELECTION = 'DELETE_SELECTION';
export const DUPLICATE_SELECTION = 'DUPLICATE_SELECTION';
export const ALIGN = 'ALIGN';
export const ADD_IMAGE = 'ADD_IMAGE';
export const D2_TEXT_INIT = 'D2_TEXT_INIT';
export const D2_TEXT_INPUT_CHANGE = 'D2_TEXT_INPUT_CHANGE';
export const UNION = 'UNION';
export const INTERSECT = 'INTERSECT';
export const MOVE_SELECTION = 'MOVE_SELECTION';
export const TRACE_DRAG = 'TRACE_DRAG';
export const TRACE_DRAG_END = 'TRACE_DRAG_END';
export const TRACE_TAP = 'TRACE_TAP';
export const FLOOD_FILL = 'FLOOD_FILL';
export const TRACE_FLOOD_FILL = 'TRACE_FLOOD_FILL';
export const MENU_OPEN = 'MENU_OPEN';
export const MENU_CLOSE = 'MENU_CLOSE';
export const OPEN_SKETCH = 'OPEN_SKETCH';
export const SET_PREVENT_SCROLL = 'SET_PREVENT_SCROLL';
export const SET_DISABLE_SCROLL = 'SET_DISABLE_SCROLL';
// CATEGORIES
// actions that influence selected objects
export const CAT_SELECTION = 'SELECTION';
export function addObject(objectData) {
return { type: 'ADD_OBJECT', objectData };
}
export function d2DragStart(position, preDrags, objects, screenMatrixContainer, screenMatrixZoom) {
return { type: D2_DRAG_START, position, preDrags, objects, screenMatrixContainer, screenMatrixZoom };
}
export function d2Drag(position, previousPosition, screenMatrixContainer, screenMatrixZoom) {
return { type: D2_DRAG, position, previousPosition, screenMatrixContainer, screenMatrixZoom, log: false };
}
export function d2DragEnd(position, screenMatrixContainer, screenMatrixZoom) {
return { type: D2_DRAG_END, position, screenMatrixContainer, screenMatrixZoom };
}
export function d2SecondDragStart(position, preDrags, objects, screenMatrixContainer, screenMatrixZoom) {
return { type: D2_SECOND_DRAG_START, position, preDrags, objects, screenMatrixContainer, screenMatrixZoom };
}
export function d2SecondDrag(position, previousPosition, screenMatrixContainer, screenMatrixZoom) {
return {
type: D2_SECOND_DRAG,
log: false,
position, previousPosition, screenMatrixContainer, screenMatrixZoom };
}
export function d2SecondDragEnd(position, screenMatrixContainer, screenMatrixZoom) {
return { type: D2_SECOND_DRAG_END, position, screenMatrixContainer, screenMatrixZoom };
}
export function d2Tap(position, objects, screenMatrixContainer, screenMatrixZoom) {
return { type: D2_TAP, position, objects, screenMatrixContainer, screenMatrixZoom };
}
export function d2MultitouchStart(positions, preDrags, screenMatrixContainer, screenMatrixZoom) {
return { type: D2_MULTITOUCH_START, positions, preDrags, screenMatrixContainer, screenMatrixZoom };
}
export function d2Multitouch(positions, previousPositions, screenMatrixContainer, screenMatrixZoom) {
return {
log: false,
type: D2_MULTITOUCH,
positions, previousPositions, screenMatrixContainer, screenMatrixZoom
};
}
export function d2MultitouchEnd(positions, screenMatrixContainer, screenMatrixZoom) {
return { type: D2_MULTITOUCH_END, positions, screenMatrixContainer, screenMatrixZoom };
}
export function d2MouseWheel(position, wheelDelta, screenMatrixContainer, screenMatrixZoom) {
return { type: D2_MOUSE_WHEEL, position, wheelDelta, screenMatrixContainer, screenMatrixZoom, log: false };
}
export function d3DragStart(position, preDrags, objects) {
return { type: D3_DRAG_START, position, preDrags, objects };
}
export function d3Drag(position, previousPosition) {
return { type: D3_DRAG, position, previousPosition, log: false };
}
export function d3DragEnd(position) {
return { type: D3_DRAG_END, position };
}
export function d3SecondDragStart(position, preDrags, objects) {
return { type: D3_SECOND_DRAG_START, position, preDrags, objects };
}
export function d3SecondDrag(position, previousPosition) {
return { type: D3_SECOND_DRAG, log: false, position, previousPosition };
}
export function d3SecondDragEnd(position) {
return { type: D3_SECOND_DRAG_END, position };
}
export function d3Tap(position, objects) {
return { type: D3_TAP, position, objects };
}
export function d3MultitouchStart(positions, preDrags) {
return { type: D3_MULTITOUCH_START, positions, preDrags };
}
export function d3Multitouch(positions, previousPositions) {
return { log: false, type: D3_MULTITOUCH, positions, previousPositions };
}
export function d3MultitouchEnd(positions) {
return { type: D3_MULTITOUCH_END, positions };
}
export function d3MouseWheel(position, wheelDelta) {
return { type: D3_MOUSE_WHEEL, position, wheelDelta, log: false };
}
export function d2ChangeTool(tool) {
return (dispatch, getState) => {
dispatch({ type: D2_CHANGE_TOOL, tool });
const state = getState();
switch (tool) {
case d2Tools.PHOTO_GUIDE:
const hasImage = Object
.values(state.sketcher.present.objectsById)
.some(object => object.type === 'IMAGE_GUIDE');
if (!hasImage) dispatch(importImage());
break;
case d2Tools.TEXT:
const hasText = Object
.values(state.sketcher.present.objectsById)
.some(object => object.type === 'TEXT');
if (!hasText) {
dispatch(d2textInit());
}
break;
default:
break;
}
};
}
export function transformStart(handle, position, screenMatrixContainer, screenMatrixZoom) {
return { type: TRANSFORM_START, handle, position, screenMatrixContainer, screenMatrixZoom };
}
export function transform(delta, position, screenMatrixContainer, screenMatrixZoom) {
return { type: TRANSFORM, delta, position, screenMatrixContainer, screenMatrixZoom, log: false };
}
export function transformEnd(screenMatrixContainer, screenMatrixZoom) {
return (dispatch, getState) => {
const state = getState();
if (state.sketcher.present.d2.transform.handle === 'dragselect') {
dispatch({ type: DRAG_SELECT, screenMatrixContainer, screenMatrixZoom, category: CAT_SELECTION });
}
dispatch({ type: TRANSFORM_END, screenMatrixContainer, screenMatrixZoom });
};
}
export function multitouchTransformStart(screenMatrixContainer, screenMatrixZoom) {
return { type: MULTITOUCH_TRANSFORM_START, screenMatrixContainer, screenMatrixZoom };
}
export function multitouchTransform(positions, previousPositions, screenMatrixContainer, screenMatrixZoom) {
return {
type: MULTITOUCH_TRANSFORM, positions, previousPositions,
screenMatrixContainer, screenMatrixZoom, log: false
};
}
export function multitouchTransformEnd(screenMatrixContainer, screenMatrixZoom) {
return { type: MULTITOUCH_TRANSFORM_END, screenMatrixContainer, screenMatrixZoom };
}
export function d3ChangeTool(tool) {
return { type: D3_CHANGE_TOOL, tool };
}
export function contextChangeTool(tool) {
return { type: CONTEXT_CHANGE_TOOL, tool };
}
export function changeHeightStart(handle) {
return { type: HEIGHT_START, handle };
}
export function changeHeight(delta) {
return { type: HEIGHT, delta, log: false };
}
export function changeHeightEnd() {
return { type: HEIGHT_END };
}
export function twistStart() {
return { type: TWIST_START };
}
export function twist(delta) {
return { type: TWIST, delta, log: false };
}
export function twistEnd() {
return { type: TWIST_END };
}
export function sculptStart(heightIndex) {
return { type: SCULPT_START, heightIndex };
}
export function sculpt(delta) {
return { type: SCULPT, delta, log: false };
}
export function sculptEnd() {
return { type: SCULPT_END };
}
export function addSculptHandle(height, start) {
return { type: ADD_SCULPT_HANDLE, height, start };
}
export function removeSculptHandle(heightIndex) {
return { type: REMOVE_SCULPT_HANDLE, heightIndex };
}
export function stamp(hit) {
return { type: STAMP, hit };
}
export function clear() {
return { type: CLEAR };
}
export function select(shapeID) {
return { category: CAT_SELECTION, type: SELECT, shapeID };
}
export function deselect(shapeID) {
return { category: CAT_SELECTION, type: DESELECT, shapeID };
}
export function toggleSelect(shapeID) {
return { category: CAT_SELECTION, type: TOGGLE_SELECT, shapeID };
}
export function deselectAll() {
return { category: CAT_SELECTION, type: DESELECT_ALL };
}
export function selectAll() {
return { category: CAT_SELECTION, type: SELECT_ALL };
}
export function bedSelect() {
return { category: CAT_SELECTION, type: BED_SELECT };
}
export function deleteSelection() {
return { type: DELETE_SELECTION };
}
export function duplicateSelection() {
return (dispatch, getState) => {
// store current object ids so we can determine what objects are added by duplicate selection
const initialObjectIds = Object.keys(getState().sketcher.present.objectsById);
dispatch({ type: DUPLICATE_SELECTION });
const { selection, objectsById } = getState().sketcher.present;
// calculate object ids added by duplicate selection
const newIds = Object
.keys(objectsById)
.filter(id => !initialObjectIds.includes(id));
if (newIds.length === 0) return;
const selectedShapeDatas = selectionUtils.getSelectedObjectsSelector(selection.objects, objectsById);
const { center } = selectionUtils.getBoundingBox(selectedShapeDatas);
const ease = t => (Math.sin(((t + 0.5) * 2 * Math.PI) + (0.5 * Math.PI)) / 2) + 0.5;
newIds
.map(id => objectsById[id])
.forEach(shapeData => {
tween(300, t => {
t = ease(t);
const scale = t * 0.3 + 1.0;
const scaleMatrix = new Matrix().scaleAroundRelative(scale, scale, center);
const stepTransform = shapeData.transform.multiplyMatrix(scaleMatrix);
dispatch({ type: UPDATE_MATRIX, transform: stepTransform, id: shapeData.UID, log: false });
});
});
};
}
export function tweenShape(id, duration, initialTransform, targetTransform, ease) {
return dispatch => {
const {
rotation: targetRotation,
position: targetPosition,
scale: targetScale
} = decomposeMatrix(targetTransform);
const {
rotation: initialRotation,
position: initialPosition,
scale: initialScale
} = decomposeMatrix(initialTransform);
return tween(duration, t => {
t = ease ? ease(t) : t;
const it = 1 - t;
const rotation = targetRotation * t + initialRotation * it;
const position = targetPosition.scale(t).add(initialPosition.scale(it));
const scale = targetScale.scale(t).add(initialScale.scale(it));
dispatch({
type: UPDATE_MATRIX,
transform: new Matrix({
x: position.x, y: position.y,
sx: scale.x, sy: scale.y,
rotation
}),
id,
log: false
});
});
};
}
export function union() {
return { type: UNION };
}
export function intersect() {
return { type: INTERSECT };
}
export function moveSelection(deltaX, deltaY) {
return { type: MOVE_SELECTION, deltaX, deltaY };
}
export function addImage(file) {
return (dispatch) => {
const url = URL.createObjectURL(file);
return dispatch({
type: ADD_IMAGE,
payload: loadImage(url).then(prepareImage)
}).then(() => {
URL.revokeObjectURL(url);
dispatch({ type: D2_CHANGE_TOOL, tool: d2Tools.PHOTO_GUIDE });
}).catch(error => {
URL.revokeObjectURL(url);
dispatch(notification.error({
position: 'tc',
title: 'Error loading image, please try again with another image'
}));
throw error; // rethrow for other listeners
});
};
}
export function d2textInit(position, textId, screenMatrixContainer, screenMatrixZoom) {
return (dispatch) => {
dispatch({ type: D2_TEXT_INIT, position, textId, screenMatrixContainer, screenMatrixZoom });
};
}
export function d2textInputChange(text) {
return { type: D2_TEXT_INPUT_CHANGE, text };
}
const traceDragThrottle = createThrottle();
export function traceDrag(position, start, id, screenMatrixContainer, screenMatrixZoom) {
return (dispatch, getState) => {
dispatch({ type: TRACE_DRAG, position, start, id, screenMatrixContainer, screenMatrixZoom });
const state = getState();
traceDragThrottle(() => {
const { trace, activeShape } = state.sketcher.present.d2;
const shapeData = state.sketcher.present.objectsById[activeShape];
const tolerance = traceUtils.calculateTolerance(trace.start, trace.position);
const traceStart = calculatePointInImage(trace.start, shapeData, screenMatrixZoom);
if (tolerance > MAX_TRACE_TOLERANCE) return null;
return dispatch(floodFill(tolerance, shapeData, traceStart)).catch(() => {
// Ignore floodfill errors so throttle function doesn't get stuck
});
});
};
}
// export function traceTap(position, objects, screenMatrixContainer, screenMatrixZoom) {
// return async (dispatch, getState) => {
// dispatch({ type: TRACE_TAP });
//
// const state = getState();
// const id = objects.find(_id => state.sketcher.present.objectsById[_id].type === 'IMAGE_GUIDE');
//
// if (id) {
// const shapeData = state.sketcher.present.objectsById[id];
// const traceStart = calculatePointInImage(position, shapeData, screenMatrixZoom);
//
// const { value: traceData } = await dispatch(floodFill(DEFAULT_TRACE_TOLERANCE, shapeData, traceStart));
//
// return dispatch(traceFloodFill(traceData, shapeData));
// } else {
// return dispatch(importImage());
// }
// };
// }
export function importImage() {
return dispatch => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = () => {
if (!input.files) return;
const [file] = Array.from(input.files);
if (!file) return;
dispatch(addImage(file)).then(() => {
delete window.importImageInput;
});
};
input.click();
// Fixes import from camera work, see #935
window.importImageInput = input;
};
}
export function traceDragEnd() {
return (dispatch, getState) => {
const state = getState();
dispatch({ type: TRACE_DRAG_END });
const id = state.sketcher.present.d2.activeShape;
if (id) {
const traceData = state.sketcher.present.d2.trace.floodFillData;
const shapeData = state.sketcher.present.objectsById[id];
return dispatch(traceFloodFill(traceData, shapeData));
}
};
}
export function floodFill(tolerance, shapeData, start) {
return {
type: FLOOD_FILL,
payload: traceUtils.floodFill(tolerance, shapeData.imageData, start)
};
}
export function traceFloodFill(traceData, shapeData) {
return {
type: TRACE_FLOOD_FILL,
payload: traceUtils.traceFloodFill(traceData, shapeData)
};
}
export function menuOpen(menuValue) {
return { type: MENU_OPEN, menuValue };
}
export function menuClose(menuValue) {
return { type: MENU_CLOSE, menuValue };
}
export function openSketch(data) {
return { type: OPEN_SKETCH, data };
}
export function setPreventScroll(preventScroll) {
return { type: SET_PREVENT_SCROLL, preventScroll };
}
export function setDisableScroll(disableScroll) {
return { type: SET_DISABLE_SCROLL, disableScroll };
}