Merged PR 200: Implement the Canvas API rather than using SVG
This commit is contained in:
parent
af1b32c8d6
commit
04d79688cb
6 changed files with 712 additions and 11 deletions
146
src/Components/Canvas/Canvas.tsx
Normal file
146
src/Components/Canvas/Canvas.tsx
Normal file
|
@ -0,0 +1,146 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { IPoint } from '../../Interfaces/IPoint';
|
||||
import { BAR_WIDTH } from '../Bar/Bar';
|
||||
|
||||
interface ICanvasProps {
|
||||
width: number
|
||||
height: number
|
||||
draw: (context: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint) => void
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
};
|
||||
|
||||
function UseCanvas(draw: (context: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint) => void): React.RefObject<HTMLCanvasElement> {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const frameCount = useRef(0);
|
||||
const translatePos = useRef({
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
|
||||
const startDragOffset = useRef({
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
|
||||
const scale = useRef(1.0);
|
||||
const scaleMultiplier = useRef(0.8);
|
||||
const dragging = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
let animationFrameId = 0;
|
||||
if (canvas === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
// add event listeners to handle screen drag
|
||||
function MouseDown(evt: MouseEvent): void {
|
||||
dragging.current = true;
|
||||
startDragOffset.current.x = evt.clientX - translatePos.current.x;
|
||||
startDragOffset.current.y = evt.clientY - translatePos.current.y;
|
||||
}
|
||||
|
||||
function MouseUp(evt: MouseEvent): void {
|
||||
dragging.current = false;
|
||||
}
|
||||
|
||||
function MouseMove(evt: MouseEvent): void {
|
||||
if (!dragging.current || context === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
translatePos.current.x = evt.clientX - startDragOffset.current.x;
|
||||
translatePos.current.y = evt.clientY - startDragOffset.current.y;
|
||||
frameCount.current++;
|
||||
draw(context, frameCount.current, scale.current, translatePos.current);
|
||||
}
|
||||
function HandleScroll(evt: WheelEvent): void {
|
||||
if (context === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.deltaY >= 0) {
|
||||
scale.current *= scaleMultiplier.current;
|
||||
} else {
|
||||
scale.current /= scaleMultiplier.current;
|
||||
}
|
||||
|
||||
draw(context, frameCount.current, scale.current, translatePos.current);
|
||||
evt.preventDefault();
|
||||
}
|
||||
canvas.addEventListener('mousedown', MouseDown);
|
||||
window.addEventListener('mouseup', MouseUp);
|
||||
canvas.addEventListener('mousemove', MouseMove);
|
||||
canvas.addEventListener('wheel', HandleScroll);
|
||||
|
||||
function Render(): void {
|
||||
if (context === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
frameCount.current++;
|
||||
draw(context, frameCount.current, scale.current, translatePos.current);
|
||||
animationFrameId = window.requestAnimationFrame(Render);
|
||||
}
|
||||
|
||||
Render();
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(animationFrameId);
|
||||
canvas.removeEventListener('mousedown', MouseDown);
|
||||
window.removeEventListener('mouseup', MouseUp);
|
||||
canvas.removeEventListener('mousemove', MouseMove);
|
||||
canvas.removeEventListener('wheel', HandleScroll);
|
||||
};
|
||||
}, [draw]);
|
||||
|
||||
return canvasRef;
|
||||
}
|
||||
|
||||
function UseSVGAutoResizer(
|
||||
setViewer: React.Dispatch<React.SetStateAction<Viewer>>
|
||||
): void {
|
||||
React.useEffect(() => {
|
||||
function OnResize(): void {
|
||||
setViewer({
|
||||
viewerWidth: window.innerWidth - BAR_WIDTH,
|
||||
viewerHeight: window.innerHeight
|
||||
});
|
||||
}
|
||||
window.addEventListener('resize', OnResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', OnResize);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
interface Viewer {
|
||||
viewerWidth: number
|
||||
viewerHeight: number
|
||||
}
|
||||
|
||||
export function Canvas({ width, height, draw, style, className }: ICanvasProps): JSX.Element {
|
||||
const canvasRef = UseCanvas(draw);
|
||||
|
||||
const [{ viewerWidth, viewerHeight }, setViewer] = React.useState<Viewer>({
|
||||
viewerWidth: width,
|
||||
viewerHeight: height
|
||||
});
|
||||
|
||||
UseSVGAutoResizer(setViewer);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={style}
|
||||
width={viewerWidth}
|
||||
height={viewerHeight}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
82
src/Components/Canvas/Dimension.ts
Normal file
82
src/Components/Canvas/Dimension.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { NOTCHES_LENGTH } from '../../utils/default';
|
||||
|
||||
interface IDimensionProps {
|
||||
id: string
|
||||
xStart: number
|
||||
yStart: number
|
||||
xEnd: number
|
||||
yEnd: number
|
||||
text: string
|
||||
strokeWidth: number
|
||||
scale?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 2D Parametric function. Returns a new coordinate from the origin coordinate
|
||||
* See for more details https://en.wikipedia.org/wiki/Parametric_equation.
|
||||
* TL;DR a parametric function is a function with a parameter
|
||||
* @param x0 Origin coordinate
|
||||
* @param t The parameter
|
||||
* @param vx Transform vector
|
||||
* @returns Returns a new coordinate from the origin coordinate
|
||||
*/
|
||||
function ApplyParametric(x0: number, t: number, vx: number): number {
|
||||
return x0 + t * vx;
|
||||
}
|
||||
|
||||
export function RenderDimension(ctx: CanvasRenderingContext2D, props: IDimensionProps): void {
|
||||
const scale = props.scale ?? 1;
|
||||
const strokeStyle = 'black';
|
||||
const lineWidth = 2 / scale;
|
||||
|
||||
/// We need to find the points of the notches
|
||||
// Get the vector of the line
|
||||
const [deltaX, deltaY] = [(props.xEnd - props.xStart), (props.yEnd - props.yStart)];
|
||||
const rotation = (Math.atan2(deltaY, deltaX) / (2 * Math.PI));
|
||||
|
||||
// Get the unit vector
|
||||
const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
const [unitX, unitY] = [deltaX / norm, deltaY / norm];
|
||||
|
||||
// Get the perpandicular vector
|
||||
const [perpVecX, perpVecY] = [unitY, -unitX];
|
||||
|
||||
// Use the parametric function to get the coordinates (x = x0 + t * v.x)
|
||||
const notchesLength = NOTCHES_LENGTH / scale;
|
||||
const startTopX = ApplyParametric(props.xStart, notchesLength, perpVecX);
|
||||
const startTopY = ApplyParametric(props.yStart, notchesLength, perpVecY);
|
||||
const startBottomX = ApplyParametric(props.xStart, -notchesLength, perpVecX);
|
||||
const startBottomY = ApplyParametric(props.yStart, -notchesLength, perpVecY);
|
||||
|
||||
const endTopX = ApplyParametric(props.xEnd, notchesLength, perpVecX);
|
||||
const endTopY = ApplyParametric(props.yEnd, notchesLength, perpVecY);
|
||||
const endBottomX = ApplyParametric(props.xEnd, -notchesLength, perpVecX);
|
||||
const endBottomY = ApplyParametric(props.yEnd, -notchesLength, perpVecY);
|
||||
|
||||
ctx.save();
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = strokeStyle;
|
||||
ctx.fillStyle = strokeStyle;
|
||||
ctx.moveTo(startTopX, startTopY);
|
||||
ctx.lineTo(startBottomX, startBottomY);
|
||||
ctx.stroke();
|
||||
ctx.moveTo(props.xStart, props.yStart);
|
||||
ctx.lineTo(props.xEnd, props.yEnd);
|
||||
ctx.stroke();
|
||||
ctx.moveTo(endTopX, endTopY);
|
||||
ctx.lineTo(endBottomX, endBottomY);
|
||||
ctx.stroke();
|
||||
const textX = (props.xStart + props.xEnd) / 2;
|
||||
const textY = (props.yStart + props.yEnd) / 2;
|
||||
ctx.font = `${16 / scale}px Verdana`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.textBaseline = 'bottom';
|
||||
ctx.translate(textX, textY);
|
||||
ctx.rotate(rotation * Math.PI * 2);
|
||||
ctx.fillText(props.text, 0, 0);
|
||||
|
||||
ctx.restore();
|
||||
}
|
345
src/Components/Canvas/DimensionLayer.ts
Normal file
345
src/Components/Canvas/DimensionLayer.ts
Normal file
|
@ -0,0 +1,345 @@
|
|||
import { Orientation } from '../../Enums/Orientation';
|
||||
import { Position } from '../../Enums/Position';
|
||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { SHOW_SELF_DIMENSIONS, SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS } from '../../utils/default';
|
||||
import { MakeRecursionDFSIterator, Pairwise } from '../../utils/itertools';
|
||||
import { TransformX, TransformY } from '../../utils/svg';
|
||||
import { RenderDimension } from './Dimension';
|
||||
|
||||
const MODULE_STROKE_WIDTH = 1;
|
||||
|
||||
export function AddDimensions(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
container: IContainerModel,
|
||||
dimMapped: number[],
|
||||
currentTransform: [number, number],
|
||||
scale: number,
|
||||
depth: number
|
||||
): void {
|
||||
ctx.beginPath();
|
||||
if (SHOW_SELF_DIMENSIONS && container.properties.showSelfDimensions.length > 0) {
|
||||
ActionByPosition(
|
||||
ctx,
|
||||
dimMapped,
|
||||
container.properties.showSelfDimensions,
|
||||
AddHorizontalSelfDimension,
|
||||
AddVerticalSelfDimension,
|
||||
[container,
|
||||
currentTransform,
|
||||
scale]
|
||||
);
|
||||
}
|
||||
|
||||
if (SHOW_BORROWER_DIMENSIONS && container.properties.showDimensionWithMarks.length > 0) {
|
||||
ActionByPosition(
|
||||
ctx,
|
||||
dimMapped,
|
||||
container.properties.showDimensionWithMarks,
|
||||
AddHorizontalBorrowerDimension,
|
||||
AddVerticalBorrowerDimension,
|
||||
[container,
|
||||
depth,
|
||||
currentTransform,
|
||||
scale]
|
||||
);
|
||||
}
|
||||
|
||||
if (SHOW_CHILDREN_DIMENSIONS && container.properties.showChildrenDimensions.length > 0 && container.children.length >= 2) {
|
||||
ActionByPosition(
|
||||
ctx,
|
||||
dimMapped,
|
||||
container.properties.showChildrenDimensions,
|
||||
AddHorizontalChildrenDimension,
|
||||
AddVerticalChildrenDimension,
|
||||
[container,
|
||||
currentTransform,
|
||||
scale]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction that call another function given the positions
|
||||
* @param dimMapped Position mapped depending on the Position enum in order:
|
||||
* [0:left, 1:bottom, 2:up, 3:right]
|
||||
* @param positions List of positions
|
||||
* @param horizontalAction Action called when a left or right position is present
|
||||
* @param verticalAction Action called when a down or up position is present
|
||||
* @param params Params for the actions
|
||||
* (the two actions must have the same number of params, and in the same order)
|
||||
*/
|
||||
function ActionByPosition(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
dimMapped: number[],
|
||||
positions: Position[],
|
||||
horizontalAction: (ctx: CanvasRenderingContext2D, dim: number, ...params: any[]) => void,
|
||||
verticalAction: (ctx: CanvasRenderingContext2D, dim: number, ...params: any[]) => void,
|
||||
params: any[]
|
||||
): void {
|
||||
positions.forEach((position: Position) => {
|
||||
const dim = dimMapped[position];
|
||||
switch (position) {
|
||||
case Position.Left:
|
||||
case Position.Right:
|
||||
verticalAction(ctx, dim, ...params);
|
||||
break;
|
||||
case Position.Down:
|
||||
case Position.Up:
|
||||
horizontalAction(ctx, dim, ...params);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function AddHorizontalChildrenDimension(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
yDim: number,
|
||||
container: IContainerModel,
|
||||
currentTransform: [number, number],
|
||||
scale: number
|
||||
): void {
|
||||
const childrenId = `dim-y${yDim.toFixed(0)}-children-${container.properties.id}`;
|
||||
|
||||
const lastChild = container.children[container.children.length - 1];
|
||||
let xChildrenStart = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference);
|
||||
let xChildrenEnd = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference);
|
||||
|
||||
// Find the min and max
|
||||
for (let i = container.children.length - 2; i >= 0; i--) {
|
||||
const child = container.children[i];
|
||||
const left = TransformX(child.properties.x, child.properties.width, child.properties.positionReference);
|
||||
if (left < xChildrenStart) {
|
||||
xChildrenStart = left;
|
||||
}
|
||||
const right = TransformX(child.properties.x, child.properties.width, child.properties.positionReference);
|
||||
if (right > xChildrenEnd) {
|
||||
xChildrenEnd = right;
|
||||
}
|
||||
}
|
||||
|
||||
if (xChildrenStart === xChildrenEnd) {
|
||||
// do not show an empty dimension
|
||||
return;
|
||||
}
|
||||
|
||||
const textChildren = (xChildrenEnd - xChildrenStart)
|
||||
.toFixed(2)
|
||||
.toString();
|
||||
|
||||
const offset = currentTransform[0] + container.properties.x;
|
||||
RenderDimension(ctx, {
|
||||
id: childrenId,
|
||||
xStart: xChildrenStart + offset,
|
||||
xEnd: xChildrenEnd + offset,
|
||||
yStart: yDim,
|
||||
yEnd: yDim,
|
||||
strokeWidth: MODULE_STROKE_WIDTH,
|
||||
text: textChildren,
|
||||
scale
|
||||
});
|
||||
}
|
||||
|
||||
function AddVerticalChildrenDimension(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
xDim: number,
|
||||
container: IContainerModel,
|
||||
currentTransform: [number, number],
|
||||
scale: number
|
||||
): void {
|
||||
const childrenId = `dim-x${xDim.toFixed(0)}-children-${container.properties.id}`;
|
||||
|
||||
const lastChild = container.children[container.children.length - 1];
|
||||
let yChildrenStart = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference);
|
||||
let yChildrenEnd = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference);
|
||||
|
||||
// Find the min and max
|
||||
for (let i = container.children.length - 2; i >= 0; i--) {
|
||||
const child = container.children[i];
|
||||
const top = TransformY(child.properties.y, child.properties.height, child.properties.positionReference);
|
||||
if (top < yChildrenStart) {
|
||||
yChildrenStart = top;
|
||||
}
|
||||
const bottom = TransformY(child.properties.y, child.properties.height, child.properties.positionReference);
|
||||
if (bottom > yChildrenEnd) {
|
||||
yChildrenEnd = bottom;
|
||||
}
|
||||
}
|
||||
|
||||
if (yChildrenStart === yChildrenEnd) {
|
||||
// do not show an empty dimension
|
||||
return;
|
||||
}
|
||||
|
||||
const textChildren = (yChildrenEnd - yChildrenStart)
|
||||
.toFixed(2)
|
||||
.toString();
|
||||
|
||||
const offset = currentTransform[0] + container.properties.x;
|
||||
|
||||
RenderDimension(ctx, {
|
||||
id: childrenId,
|
||||
xStart: xDim,
|
||||
xEnd: xDim,
|
||||
yStart: yChildrenStart + offset,
|
||||
yEnd: yChildrenEnd + offset,
|
||||
strokeWidth: MODULE_STROKE_WIDTH,
|
||||
text: textChildren,
|
||||
scale
|
||||
});
|
||||
}
|
||||
|
||||
function AddHorizontalBorrowerDimension(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
yDim: number,
|
||||
container: IContainerModel,
|
||||
depth: number,
|
||||
currentTransform: [number, number],
|
||||
scale: number
|
||||
): void {
|
||||
const it = MakeRecursionDFSIterator(container, depth, currentTransform);
|
||||
const marks = []; // list of vertical lines for the dimension
|
||||
for (const {
|
||||
container: childContainer, currentTransform: childCurrentTransform
|
||||
} of it) {
|
||||
const isHidden = !childContainer.properties.markPosition.includes(Orientation.Horizontal);
|
||||
if (isHidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const x = TransformX(
|
||||
childContainer.properties.x,
|
||||
childContainer.properties.width,
|
||||
childContainer.properties.positionReference
|
||||
);
|
||||
|
||||
const restoredX = x + childCurrentTransform[0];
|
||||
|
||||
marks.push(
|
||||
restoredX
|
||||
);
|
||||
}
|
||||
|
||||
const restoredX = container.properties.x + currentTransform[0];
|
||||
marks.push(restoredX);
|
||||
marks.push(restoredX + container.properties.width);
|
||||
marks.sort((a, b) => a - b);
|
||||
let count = 0;
|
||||
for (const { cur, next } of Pairwise(marks)) {
|
||||
const id = `dim-y${yDim.toFixed(0)}-borrow-${container.properties.id}-{${count}}`;
|
||||
RenderDimension(ctx, {
|
||||
id,
|
||||
xStart: cur,
|
||||
xEnd: next,
|
||||
yStart: yDim,
|
||||
yEnd: yDim,
|
||||
strokeWidth: MODULE_STROKE_WIDTH,
|
||||
text: (next - cur).toFixed(0).toString(),
|
||||
scale
|
||||
});
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
function AddVerticalBorrowerDimension(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
xDim: number,
|
||||
container: IContainerModel,
|
||||
depth: number,
|
||||
currentTransform: [number, number],
|
||||
scale: number
|
||||
): void {
|
||||
const it = MakeRecursionDFSIterator(container, depth, currentTransform);
|
||||
const marks = []; // list of vertical lines for the dimension
|
||||
for (const {
|
||||
container: childContainer, currentTransform: childCurrentTransform
|
||||
} of it) {
|
||||
const isHidden = !childContainer.properties.markPosition.includes(Orientation.Vertical);
|
||||
if (isHidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const y = TransformY(
|
||||
childContainer.properties.y,
|
||||
childContainer.properties.height,
|
||||
childContainer.properties.positionReference
|
||||
);
|
||||
|
||||
const restoredy = y + childCurrentTransform[1];
|
||||
|
||||
marks.push(
|
||||
restoredy
|
||||
);
|
||||
}
|
||||
|
||||
const restoredY = container.properties.y + currentTransform[1];
|
||||
marks.push(restoredY);
|
||||
marks.push(restoredY + container.properties.height);
|
||||
marks.sort((a, b) => a - b);
|
||||
let count = 0;
|
||||
for (const { cur, next } of Pairwise(marks)) {
|
||||
const id = `dim-x${xDim.toFixed(0)}-borrow-${container.properties.id}-{${count}}`;
|
||||
RenderDimension(ctx, {
|
||||
id,
|
||||
xStart: xDim,
|
||||
xEnd: xDim,
|
||||
yStart: cur,
|
||||
yEnd: next,
|
||||
strokeWidth: MODULE_STROKE_WIDTH,
|
||||
text: (next - cur).toFixed(0).toString(),
|
||||
scale
|
||||
});
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
function AddVerticalSelfDimension(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
xDim: number,
|
||||
container: IContainerModel,
|
||||
currentTransform: [number, number],
|
||||
scale: number
|
||||
): void {
|
||||
const height = container.properties.height;
|
||||
const idVert = `dim-x${xDim.toFixed(0)}-${container.properties.id}`;
|
||||
const yStart = container.properties.y + currentTransform[1];
|
||||
const yEnd = yStart + height;
|
||||
const textVert = height
|
||||
.toFixed(0)
|
||||
.toString();
|
||||
RenderDimension(ctx, {
|
||||
id: idVert,
|
||||
xStart: xDim,
|
||||
yStart,
|
||||
xEnd: xDim,
|
||||
yEnd,
|
||||
strokeWidth: MODULE_STROKE_WIDTH,
|
||||
text: textVert,
|
||||
scale
|
||||
});
|
||||
}
|
||||
|
||||
function AddHorizontalSelfDimension(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
yDim: number,
|
||||
container: IContainerModel,
|
||||
currentTransform: [number, number],
|
||||
scale: number
|
||||
): void {
|
||||
const width = container.properties.width;
|
||||
const id = `dim-y${yDim.toFixed(0)}-${container.properties.id}`;
|
||||
const xStart = container.properties.x + currentTransform[0];
|
||||
const xEnd = xStart + width;
|
||||
const text = width
|
||||
.toFixed(0)
|
||||
.toString();
|
||||
RenderDimension(ctx, {
|
||||
id,
|
||||
xStart,
|
||||
yStart: yDim,
|
||||
xEnd,
|
||||
yEnd: yDim,
|
||||
strokeWidth: MODULE_STROKE_WIDTH,
|
||||
text,
|
||||
scale
|
||||
});
|
||||
}
|
57
src/Components/Canvas/Selector.ts
Normal file
57
src/Components/Canvas/Selector.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { SHOW_SELECTOR_TEXT } from '../../utils/default';
|
||||
import { GetAbsolutePosition } from '../../utils/itertools';
|
||||
import { RemoveMargin } from '../../utils/svg';
|
||||
|
||||
interface ISelectorProps {
|
||||
selected?: IContainerModel
|
||||
scale?: number
|
||||
}
|
||||
|
||||
export function RenderSelector(ctx: CanvasRenderingContext2D, frameCount: number, props: ISelectorProps): void {
|
||||
if (props.selected === undefined || props.selected === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scale = (props.scale ?? 1);
|
||||
let [x, y] = GetAbsolutePosition(props.selected);
|
||||
let [width, height] = [
|
||||
props.selected.properties.width,
|
||||
props.selected.properties.height
|
||||
];
|
||||
|
||||
({ x, y, width, height } = RemoveMargin(x, y, width, height,
|
||||
props.selected.properties.margin.left,
|
||||
props.selected.properties.margin.bottom,
|
||||
props.selected.properties.margin.top,
|
||||
props.selected.properties.margin.right
|
||||
));
|
||||
|
||||
const xText = x + width / 2;
|
||||
const yText = y + height / 2;
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
stroke: '#3B82F6',
|
||||
strokeWidth: 4 / scale,
|
||||
fillOpacity: 0,
|
||||
transitionProperty: 'all',
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionDuration: '150ms',
|
||||
animation: 'fadein 750ms ease-in alternate infinite'
|
||||
};
|
||||
|
||||
ctx.strokeStyle = '#3B82F6';
|
||||
ctx.lineWidth = 4 / scale;
|
||||
ctx.globalAlpha = 0.25 * (Math.sin(frameCount * 0.0125) ** 2);
|
||||
ctx.strokeRect(x, y, width, height);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = 'black';
|
||||
|
||||
if (SHOW_SELECTOR_TEXT) {
|
||||
ctx.font = `${16 / scale}px Verdana`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(props.selected.properties.displayedText, xText, yText);
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
}
|
|
@ -9,12 +9,17 @@ import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save';
|
|||
import { OnKey } from './Actions/Shortcuts';
|
||||
import { events as EVENTS } from '../../Events/EditorEvents';
|
||||
import { IEditorState } from '../../Interfaces/IEditorState';
|
||||
import { DISABLE_API, MAX_HISTORY } from '../../utils/default';
|
||||
import { DIMENSION_MARGIN, DISABLE_API, MAX_HISTORY, USE_EXPERIMENTAL_CANVAS_API } from '../../utils/default';
|
||||
import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './Actions/SymbolOperations';
|
||||
import { FindContainerById } from '../../utils/itertools';
|
||||
import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools';
|
||||
import { IMenuAction, Menu } from '../Menu/Menu';
|
||||
import { GetAction } from './Actions/ContextMenuActions';
|
||||
import { AddContainerToSelectedContainer, AddContainer } from './Actions/AddContainer';
|
||||
import { Canvas } from '../Canvas/Canvas';
|
||||
import { BAR_WIDTH } from '../Bar/Bar';
|
||||
import { IPoint } from '../../Interfaces/IPoint';
|
||||
import { AddDimensions } from '../Canvas/DimensionLayer';
|
||||
import { RenderSelector } from '../Canvas/Selector';
|
||||
|
||||
interface IEditorProps {
|
||||
root: Element | Document
|
||||
|
@ -228,6 +233,55 @@ export function Editor(props: IEditorProps): JSX.Element {
|
|||
const configuration = props.configuration;
|
||||
const current = GetCurrentHistoryState(history, historyCurrentStep);
|
||||
const selected = FindContainerById(current.mainContainer, current.selectedContainerId);
|
||||
|
||||
function Draw(ctx: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint): void {
|
||||
const topDim = current.mainContainer.properties.y;
|
||||
const leftDim = current.mainContainer.properties.x;
|
||||
const rightDim = current.mainContainer.properties.x + current.mainContainer.properties.width;
|
||||
const bottomDim = current.mainContainer.properties.y + current.mainContainer.properties.height;
|
||||
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
ctx.save();
|
||||
ctx.translate(translatePos.x, translatePos.y);
|
||||
ctx.scale(scale, scale);
|
||||
ctx.fillStyle = '#000000';
|
||||
const it = MakeRecursionDFSIterator(current.mainContainer, 0, [0, 0]);
|
||||
for (const { container, depth, currentTransform } of it) {
|
||||
const [x, y] = [
|
||||
container.properties.x + currentTransform[0],
|
||||
container.properties.y + currentTransform[1]
|
||||
];
|
||||
|
||||
// Draw container
|
||||
ctx.strokeStyle = container.properties.style?.stroke ?? '#000000';
|
||||
ctx.fillStyle = container.properties.style?.fill ?? '#000000';
|
||||
ctx.lineWidth = Number(container.properties.style?.strokeWidth ?? 1);
|
||||
ctx.globalAlpha = Number(container.properties.style?.fillOpacity ?? 1);
|
||||
ctx.fillRect(x, y, container.properties.width, container.properties.height);
|
||||
ctx.globalAlpha = Number(container.properties.style?.strokeOpacity ?? 1);
|
||||
ctx.strokeRect(x, y, container.properties.width, container.properties.height);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.strokeStyle = '#000000';
|
||||
|
||||
// Draw dimensions
|
||||
const containerLeftDim = leftDim - (DIMENSION_MARGIN * (depth + 1)) / scale;
|
||||
const containerTopDim = topDim - (DIMENSION_MARGIN * (depth + 1)) / scale;
|
||||
const containerBottomDim = bottomDim + (DIMENSION_MARGIN * (depth + 1)) / scale;
|
||||
const containerRightDim = rightDim + (DIMENSION_MARGIN * (depth + 1)) / scale;
|
||||
const dimMapped = [containerLeftDim, containerBottomDim, containerTopDim, containerRightDim];
|
||||
AddDimensions(ctx, container, dimMapped, currentTransform, scale, depth);
|
||||
|
||||
// Draw selector
|
||||
RenderSelector(ctx, frameCount, {
|
||||
scale,
|
||||
selected
|
||||
});
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={editorRef} className="Editor font-sans h-full">
|
||||
<UI
|
||||
|
@ -310,8 +364,17 @@ export function Editor(props: IEditorProps): JSX.Element {
|
|||
configuration
|
||||
)}
|
||||
saveEditorAsSVG={() => SaveEditorAsSVG()}
|
||||
loadState={(move) => setHistoryCurrentStep(move)} />
|
||||
<SVG
|
||||
loadState={(move) => setHistoryCurrentStep(move)}
|
||||
/>
|
||||
{
|
||||
USE_EXPERIMENTAL_CANVAS_API
|
||||
? <Canvas
|
||||
draw={Draw}
|
||||
className='ml-16'
|
||||
width={window.innerWidth - BAR_WIDTH}
|
||||
height={window.innerHeight}
|
||||
/>
|
||||
: <SVG
|
||||
width={current.mainContainer?.properties.width}
|
||||
height={current.mainContainer?.properties.height}
|
||||
selected={selected}
|
||||
|
@ -319,6 +382,8 @@ export function Editor(props: IEditorProps): JSX.Element {
|
|||
>
|
||||
{current.mainContainer}
|
||||
</SVG>
|
||||
}
|
||||
|
||||
<Menu
|
||||
getListener={() => editorRef.current}
|
||||
actions={menuActions}
|
||||
|
|
|
@ -17,6 +17,12 @@ export const FAST_BOOT = false;
|
|||
/** Disable any call to the API (default = false) */
|
||||
export const DISABLE_API = false;
|
||||
|
||||
/**
|
||||
* Replace the SVG viewer by a canvas
|
||||
* EXPERIMENTAL: svg export wont work and it won't be possible to insert a custom svg)
|
||||
*/
|
||||
export const USE_EXPERIMENTAL_CANVAS_API = false;
|
||||
|
||||
/** Enable keyboard shortcuts (default = true) */
|
||||
export const ENABLE_SHORTCUTS = true;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue