mirror of
https://github.com/Doodle3D/Doodle3D-Core.git
synced 2024-09-29 01:08:38 +02:00
338 lines
11 KiB
JavaScript
338 lines
11 KiB
JavaScript
|
import { Vector, Matrix, Image } from 'cal';
|
||
|
import { SELECTION_VIEW_MIN_AXIS_SCALE, SELECTION_VIEW_MIN_SCALE } from '../../constants/d2Constants';
|
||
|
import dottedLineUrl from '../../../img/2d/dotLine01.png';
|
||
|
import rotateHandleUrl from '../../../img/2d/rotateHandle.png';
|
||
|
import corner01Url from '../../../img/2d/corner01.png';
|
||
|
import corner02Url from '../../../img/2d/corner02.png';
|
||
|
import corner03Url from '../../../img/2d/corner03.png';
|
||
|
import corner04Url from '../../../img/2d/corner04.png';
|
||
|
import { PIXEL_RATIO } from '../../constants/general';
|
||
|
import { getSelectedObjectsSelector, getBoundingBox } from '../../utils/selectionUtils';
|
||
|
import * as actions from '../../actions/index.js';
|
||
|
import BaseTool from './BaseTool.js';
|
||
|
import * as humanReadable from '../../utils/humanReadable.js';
|
||
|
import { dimensionsText } from '../texts.js';
|
||
|
import { isNegative } from '../../utils/matrixUtils';
|
||
|
// import createDebug from 'debug';
|
||
|
// const debug = createDebug('d3d:2d:selection');
|
||
|
|
||
|
const dottedLine = new Image(dottedLineUrl, 0, 14).load(() => URL.revokeObjectURL(dottedLineUrl));
|
||
|
const rotateHandle = new Image(rotateHandleUrl, 27, 30).load(() => URL.revokeObjectURL(rotateHandleUrl));
|
||
|
const cornerImages = [
|
||
|
new Image(corner01Url, 35, 35).load(() => URL.revokeObjectURL(corner01Url)),
|
||
|
new Image(corner02Url, 35, 35).load(() => URL.revokeObjectURL(corner02Url)),
|
||
|
new Image(corner03Url, 35, 35).load(() => URL.revokeObjectURL(corner03Url)),
|
||
|
new Image(corner04Url, 35, 35).load(() => URL.revokeObjectURL(corner04Url))
|
||
|
];
|
||
|
|
||
|
export default class TransformTool extends BaseTool {
|
||
|
constructor(dispatch, sceneSpaceContainer) {
|
||
|
super(dispatch, sceneSpaceContainer);
|
||
|
|
||
|
this.active = true;
|
||
|
this.visible = true;
|
||
|
|
||
|
this.transform = new Matrix();
|
||
|
this.position = new Vector();
|
||
|
|
||
|
this.enableHitDetection = true;
|
||
|
}
|
||
|
|
||
|
_findHit(position) {
|
||
|
if (this.numObjects === 0) return 'dragselect';
|
||
|
|
||
|
const thresholdDistance = 20.0 * PIXEL_RATIO;
|
||
|
const pixelRatioNormalizer = new Matrix();
|
||
|
pixelRatioNormalizer.scale = 1 / PIXEL_RATIO;
|
||
|
const screenMatrixZoom = this.parent.getScreenMatrix().multiplyMatrix(pixelRatioNormalizer);
|
||
|
|
||
|
const {
|
||
|
rotatePos, corners, cornerNames, sides, sideNames, points
|
||
|
} = getHandlePositions(this.boundingBox, this.transform, screenMatrixZoom);
|
||
|
|
||
|
if (rotatePos.distanceTo(position) < thresholdDistance) return 'rotate';
|
||
|
|
||
|
for (let i = 0; i < corners.length; i ++) {
|
||
|
const point = corners[i];
|
||
|
if (point.distanceTo(position) < thresholdDistance) return cornerNames[i];
|
||
|
}
|
||
|
|
||
|
for (let i = 0; i < sides.length; i ++) {
|
||
|
const point = sides[i];
|
||
|
if (point.distanceTo(position) < thresholdDistance) return sideNames[i];
|
||
|
}
|
||
|
|
||
|
// if point is inside bounding box => translate
|
||
|
if (pointInsideConvexPolygon(points, position)) return 'translate';
|
||
|
|
||
|
// if position is outside any select handle => dragselect
|
||
|
return 'dragselect';
|
||
|
}
|
||
|
|
||
|
dragStart(event) {
|
||
|
const { position, intersections } = event;
|
||
|
let handle = this._findHit(position, intersections);
|
||
|
|
||
|
if (!handle) {
|
||
|
super.dragStart(event);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (handle === 'dragselect') {
|
||
|
const [uid] = intersections;
|
||
|
if (uid !== undefined) {
|
||
|
this.dispatch(actions.deselectAll());
|
||
|
this.dispatch(actions.select(uid));
|
||
|
handle = 'translate';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this._dispatch(actions.transformStart, handle, position);
|
||
|
|
||
|
this.position.copy(position);
|
||
|
}
|
||
|
|
||
|
drag(event) {
|
||
|
if (!this._active) {
|
||
|
super.drag(event);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const { position } = event;
|
||
|
const delta = position.subtract(this.position);
|
||
|
this.position.copy(position);
|
||
|
|
||
|
this._dispatch(actions.transform, delta, position);
|
||
|
}
|
||
|
|
||
|
dragEnd(event) {
|
||
|
if (!this._active) {
|
||
|
super.dragEnd(event);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this._dispatch(actions.transformEnd);
|
||
|
}
|
||
|
|
||
|
multitouchStart(event) {
|
||
|
if (this.numObjects === 0) {
|
||
|
super.multitouchStart(event);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const screenMatrixZoom = this.parent.getScreenMatrix();
|
||
|
const { points } = getHandlePositions(this.boundingBox, this.transform, screenMatrixZoom);
|
||
|
if (!event.positions.some(position => pointInsideConvexPolygon(points, position))) {
|
||
|
super.multitouchStart(event);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this._dispatch(actions.multitouchTransformStart);
|
||
|
}
|
||
|
|
||
|
multitouch(event) {
|
||
|
if (!this._active) {
|
||
|
super.multitouch(event);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const { positions, previousPositions } = event;
|
||
|
this._dispatch(actions.multitouchTransform, positions, previousPositions);
|
||
|
}
|
||
|
|
||
|
multitouchEnd(event) {
|
||
|
if (!this._active) {
|
||
|
super.dragEnd(event);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this._dispatch(actions.multitouchTransformEnd);
|
||
|
}
|
||
|
|
||
|
tap(event) {
|
||
|
const { intersections } = event;
|
||
|
if (intersections.length === 0) {
|
||
|
this.dispatch(actions.deselectAll());
|
||
|
} else {
|
||
|
const [id] = intersections;
|
||
|
this.dispatch(actions.toggleSelect(id));
|
||
|
}
|
||
|
|
||
|
super.tap(event);
|
||
|
}
|
||
|
|
||
|
update(state) {
|
||
|
super.update(state);
|
||
|
|
||
|
let needRender = false;
|
||
|
if (state === this._state) return needRender;
|
||
|
|
||
|
this._active = state.d2.transform.active;
|
||
|
|
||
|
const { selection, objectsById, d2: transform } = state;
|
||
|
|
||
|
if (!this._state || selection !== this._state.selection) {
|
||
|
const selectedShapeDatas = getSelectedObjectsSelector(selection.objects, objectsById);
|
||
|
const boundingBox = getBoundingBox(selectedShapeDatas, selection.transform);
|
||
|
|
||
|
this.numObjects = selection.objects.length;
|
||
|
this.transform = selection.transform;
|
||
|
this.boundingBox = boundingBox;
|
||
|
|
||
|
needRender = true;
|
||
|
}
|
||
|
|
||
|
if (!this._state || transform !== this._state.d2.transform) needRender = true;
|
||
|
|
||
|
this._state = state;
|
||
|
return needRender;
|
||
|
}
|
||
|
|
||
|
draw(context, matrix) {
|
||
|
if (this.numObjects !== 0) {
|
||
|
context.globalAlpha = 1.0;
|
||
|
const scale = 0.5 * PIXEL_RATIO;
|
||
|
|
||
|
const {
|
||
|
rotatePos, corners, sides, points, width, height, widthPosition, heightPosition
|
||
|
} = getHandlePositions(this.boundingBox, this.transform, matrix);
|
||
|
const { rotation } = this.transform;
|
||
|
|
||
|
const rotateAngle = isNegative(this.transform) ? Math.PI + rotation : rotation;
|
||
|
rotateHandle.drawAngleScale(context, 0, rotatePos.x, rotatePos.y, rotateAngle, scale, scale);
|
||
|
|
||
|
for (let i = 0; i < points.length; i ++) {
|
||
|
const pointA = points[i];
|
||
|
const pointB = points[(i + 1) % points.length];
|
||
|
|
||
|
const angle = pointB.subtract(pointA).angle();
|
||
|
|
||
|
const sx = pointA.distanceTo(pointB) * 2 / PIXEL_RATIO;
|
||
|
const sy = dottedLine.height;
|
||
|
|
||
|
dottedLine.drawCropAngleScale(context, 0, pointA.x, pointA.y, sx, sy, angle, scale, scale);
|
||
|
}
|
||
|
|
||
|
for (let i = 0; i < corners.length; i ++) {
|
||
|
const image = cornerImages[i];
|
||
|
const point = corners[i];
|
||
|
|
||
|
image.drawAngleScale(context, 0, point.x, point.y, rotation, scale, scale);
|
||
|
}
|
||
|
|
||
|
for (let i = 0; i < sides.length; i ++) {
|
||
|
const image = cornerImages[i];
|
||
|
const point = sides[i];
|
||
|
|
||
|
image.drawAngleScale(context, 0, point.x, point.y, rotation, scale, scale);
|
||
|
}
|
||
|
|
||
|
const textWidth = humanReadable.distance(width);
|
||
|
dimensionsText.drawText(context, textWidth, widthPosition.x, widthPosition.y);
|
||
|
const textHeight = humanReadable.distance(height);
|
||
|
dimensionsText.drawText(context, textHeight, heightPosition.x, heightPosition.y);
|
||
|
}
|
||
|
|
||
|
if (this._state && this._state.d2.transform.handle === 'dragselect') {
|
||
|
let { start, end } = this._state.d2.transform.dragSelect;
|
||
|
start = start.scale(PIXEL_RATIO);
|
||
|
end = end.scale(PIXEL_RATIO);
|
||
|
|
||
|
const width = end.x - start.x;
|
||
|
const height = end.y - start.y;
|
||
|
|
||
|
context.strokeStyle = '#72bcd4';
|
||
|
context.lineWidth = 1.0 * PIXEL_RATIO;
|
||
|
context.strokeRect(start.x, start.y, width, height);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const CORNERS = ['lefttop', 'leftbottom', 'rightbottom', 'righttop'];
|
||
|
const SIDES = ['left', 'bottom', 'right', 'top'];
|
||
|
|
||
|
function getHandlePositions(boundingBox, transform, screenMatrixZoom) {
|
||
|
const matrix = transform.multiplyMatrix(screenMatrixZoom);
|
||
|
const { min, max } = boundingBox;
|
||
|
|
||
|
const points = [
|
||
|
new Vector(min.x, min.z),
|
||
|
new Vector(min.x, max.z),
|
||
|
new Vector(max.x, max.z),
|
||
|
new Vector(max.x, min.z)
|
||
|
].map(point => point.applyMatrix(matrix));
|
||
|
if (isNegative(transform)) points.reverse();
|
||
|
|
||
|
const distanceVertical = points[0].distanceTo(points[1]);
|
||
|
const distanceHorizontal = points[1].distanceTo(points[2]);
|
||
|
|
||
|
const minAxisScale = PIXEL_RATIO * SELECTION_VIEW_MIN_AXIS_SCALE;
|
||
|
const minViewScale = PIXEL_RATIO * SELECTION_VIEW_MIN_SCALE;
|
||
|
|
||
|
const drawScaleY = distanceVertical > minAxisScale;
|
||
|
const drawScaleX = distanceHorizontal > minAxisScale;
|
||
|
const drawMinScale = distanceVertical < minViewScale && distanceHorizontal < minViewScale;
|
||
|
|
||
|
const corners = [];
|
||
|
const cornerNames = [];
|
||
|
for (let i = 0; i < points.length; i ++) {
|
||
|
if (drawMinScale && i !== 2) continue;
|
||
|
|
||
|
const point = points[i];
|
||
|
corners.push(point);
|
||
|
|
||
|
const cornerIndex = isNegative(transform) ? (3 - i) : i;
|
||
|
cornerNames.push(`scale-${CORNERS[cornerIndex]}`);
|
||
|
}
|
||
|
|
||
|
const sides = [];
|
||
|
const sideNames = [];
|
||
|
for (let i = 0; i < points.length; i ++) {
|
||
|
const pointA = points[i];
|
||
|
|
||
|
const isHorizontal = (i + 1) % 2 === 0;
|
||
|
const isVertical = i % 2 === 0;
|
||
|
if (!(drawScaleX && isHorizontal) && !(drawScaleY && isVertical)) continue;
|
||
|
|
||
|
const pointB = points[(i + 1) % points.length];
|
||
|
sides.push(pointA.add(pointB).scale(0.5));
|
||
|
|
||
|
const cornerIndex = isNegative(transform) ? (6 - i) % 4 : i;
|
||
|
sideNames.push(`scale-${SIDES[cornerIndex]}`);
|
||
|
}
|
||
|
|
||
|
const center = points[0].add(points[3]).scale(0.5);
|
||
|
const normal = points[3].subtract(points[0]).normal().normalize();
|
||
|
|
||
|
const rotatePos = center.add(normal.scale(30 * PIXEL_RATIO));
|
||
|
|
||
|
const width = (max.x - min.x) * Math.abs(transform.sx);
|
||
|
const height = (max.z - min.z) * Math.abs(transform.sy);
|
||
|
|
||
|
const widthPosition = calculateCenterLineOffset(points[1], points[2], -25, -20);
|
||
|
const heightPosition = calculateCenterLineOffset(points[2], points[3], -25, -20);
|
||
|
|
||
|
return {
|
||
|
rotatePos, corners, cornerNames, sides, sideNames, points, width, height, widthPosition, heightPosition
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function calculateCenterLineOffset(a, b, minOffset, additionalOffset) {
|
||
|
const normal = b.subtract(a).normal().normalize();
|
||
|
const offset = minOffset + Math.abs(normal.dot(new Vector(1, 0))) * additionalOffset;
|
||
|
return a.add(b).scale(0.5).add(normal.scale(offset));
|
||
|
}
|
||
|
|
||
|
function pointInsideConvexPolygon(boundingBoxPoints, point) {
|
||
|
for (let i = 1; i <= boundingBoxPoints.length; i ++) {
|
||
|
const pointA = boundingBoxPoints[i - 1];
|
||
|
const pointB = boundingBoxPoints[i % boundingBoxPoints.length];
|
||
|
|
||
|
const normal = pointB.subtract(pointA).normal().normalize();
|
||
|
|
||
|
if (point.subtract(pointA).dot(normal) < 0) return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|