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); } }