diff --git a/package.json b/package.json index ff96249..8804ff9 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "version": "v1.0.0", "type": "module", + "postinstall": "npx patch-package", "scripts": { "d": "mprocs", "dev": "vite", @@ -22,10 +23,11 @@ "interweave": "^13.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-svg-pan-zoom": "^3.11.0", + "react-svg-pan-zoom": "^3.12.1", "react-window": "^1.8.8", "sweetalert2": "^11.7.1", - "sweetalert2-react-content": "^5.0.7" + "sweetalert2-react-content": "^5.0.7", + "transformation-matrix": "^2.14.0" }, "devDependencies": { "@testing-library/dom": "^8.20.0", @@ -60,5 +62,10 @@ "typescript": "^4.9.5", "vite": "^4.1.1", "vitest": "^0.28.4" + }, + "pnpm": { + "patchedDependencies": { + "@types/react-svg-pan-zoom@3.3.5": "patches/@types__react-svg-pan-zoom@3.3.5.patch" + } } } diff --git a/patches/@types+react-svg-pan-zoom+3.3.5.patch b/patches/@types+react-svg-pan-zoom+3.3.5.patch new file mode 100644 index 0000000..b7fc8cc --- /dev/null +++ b/patches/@types+react-svg-pan-zoom+3.3.5.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/@types/react-svg-pan-zoom/index.d.ts b/node_modules/@types/react-svg-pan-zoom/index.d.ts +index a57d545..83ace9f 100644 +--- a/node_modules/@types/react-svg-pan-zoom/index.d.ts ++++ b/node_modules/@types/react-svg-pan-zoom/index.d.ts +@@ -256,7 +256,11 @@ export function zoom(value: Value, SVGPointX: number, SVGPointY: number, scaleFa + export function fitSelection( + value: Value, selectionSVGPointX: number, selectionSVGPointY: number, selectionWidth: number, selectionHeight: number): Value; + +-export function fitToViewer(value: Value): Value; ++export function fitToViewer( ++ value: Value, ++ SVGAlignX?: typeof ALIGN_CENTER | typeof ALIGN_LEFT | typeof ALIGN_RIGHT | undefined, ++ SVGAlignY?: typeof ALIGN_CENTER | typeof ALIGN_TOP | typeof ALIGN_BOTTOM | undefined ++): Value; + + export function zoomOnViewerCenter(value: Value, scaleFactor: number): Value; + diff --git a/patches/@types__react-svg-pan-zoom@3.3.5.patch b/patches/@types__react-svg-pan-zoom@3.3.5.patch new file mode 100644 index 0000000..506b048 --- /dev/null +++ b/patches/@types__react-svg-pan-zoom@3.3.5.patch @@ -0,0 +1,17 @@ +diff --git a/index.d.ts b/index.d.ts +index a57d545d33b2798024b9762d3d3513e58a38e19d..83ace9fc85b7354e128948402a50e00083eacd8c 100644 +--- a/index.d.ts ++++ b/index.d.ts +@@ -256,7 +256,11 @@ export function zoom(value: Value, SVGPointX: number, SVGPointY: number, scaleFa + export function fitSelection( + value: Value, selectionSVGPointX: number, selectionSVGPointY: number, selectionWidth: number, selectionHeight: number): Value; + +-export function fitToViewer(value: Value): Value; ++export function fitToViewer( ++ value: Value, ++ SVGAlignX?: typeof ALIGN_CENTER | typeof ALIGN_LEFT | typeof ALIGN_RIGHT | undefined, ++ SVGAlignY?: typeof ALIGN_CENTER | typeof ALIGN_TOP | typeof ALIGN_BOTTOM | undefined ++): Value; + + export function zoomOnViewerCenter(value: Value, scaleFactor: number): Value; + \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 212f797..8607471 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,10 @@ lockfileVersion: 5.4 +patchedDependencies: + '@types/react-svg-pan-zoom@3.3.5': + hash: kv3ctd73j5hnzcxdc2ceiq5wuy + path: patches/@types__react-svg-pan-zoom@3.3.5.patch + specifiers: '@heroicons/react': ^2.0.14 '@react-hook/size': ^2.1.2 @@ -33,12 +38,13 @@ specifiers: postcss: ^8.4.21 react: ^18.2.0 react-dom: ^18.2.0 - react-svg-pan-zoom: ^3.11.0 + react-svg-pan-zoom: ^3.12.1 react-window: ^1.8.8 sass: ^1.58.0 sweetalert2: ^11.7.1 sweetalert2-react-content: ^5.0.7 tailwindcss: ^3.2.4 + transformation-matrix: ^2.14.0 typescript: ^4.9.5 vite: ^4.1.1 vitest: ^0.28.4 @@ -49,10 +55,11 @@ dependencies: interweave: 13.0.0_react@18.2.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 - react-svg-pan-zoom: 3.11.0_react@18.2.0 + react-svg-pan-zoom: 3.12.1 react-window: 1.8.8_biqbaboplfbrettd7655fr4n2y sweetalert2: 11.7.1 sweetalert2-react-content: 5.0.7_5cbezu6w3tvev2ldv5vdmnpfca + transformation-matrix: 2.14.0 devDependencies: '@testing-library/dom': 8.20.0 @@ -61,7 +68,7 @@ devDependencies: '@testing-library/user-event': 14.4.3_yxlyej73nftwmh2fiao7paxmlm '@types/react': 18.0.27 '@types/react-dom': 18.0.10 - '@types/react-svg-pan-zoom': 3.3.5 + '@types/react-svg-pan-zoom': 3.3.5_kv3ctd73j5hnzcxdc2ceiq5wuy '@types/react-window': 1.8.5 '@typescript-eslint/eslint-plugin': 5.51.0_b635kmla6dsb4frxfihkw4m47e '@typescript-eslint/parser': 5.51.0_4vsywjlpuriuw3tl5oq6zy5a64 @@ -927,11 +934,12 @@ packages: '@types/react': 18.0.27 dev: true - /@types/react-svg-pan-zoom/3.3.5: + /@types/react-svg-pan-zoom/3.3.5_kv3ctd73j5hnzcxdc2ceiq5wuy: resolution: {integrity: sha512-W8GRFCDy7raSDr5OXGjSyvX5KmdWlIQfv0NLa1jfAYVUO4ClVbgorWeAAom7nY3Pl+4h9blXE1Bnu2CW1iMEvQ==} dependencies: '@types/react': 18.0.27 dev: true + patched: true /@types/react-window/1.8.5: resolution: {integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==} @@ -3453,13 +3461,10 @@ packages: engines: {node: '>=0.10.0'} dev: true - /react-svg-pan-zoom/3.11.0_react@18.2.0: - resolution: {integrity: sha512-xK2tpfp4YksHOfyMZH5zXP52ARLSBgkoJgWNJmJ1B+6O1tkuf23TQp7Q4m9GG5IRSK5KWO0JEGEWlNYG9+iiug==} - peerDependencies: - react: '>=17.0.0' + /react-svg-pan-zoom/3.12.1: + resolution: {integrity: sha512-ug1LHCN5qed56C64xFypr/ClajuMFkig1OKvwJrIgGeSyHOjWM7XGgSgeP3IfHAkNw8QEc6a31ggZRpTijWYRw==} dependencies: prop-types: 15.8.1 - react: 18.2.0 transformation-matrix: 2.14.0 dev: false diff --git a/src/Components/Canvas/Canvas.tsx b/src/Components/Canvas/Canvas.tsx index 708b5cd..d7321b2 100644 --- a/src/Components/Canvas/Canvas.tsx +++ b/src/Components/Canvas/Canvas.tsx @@ -1,16 +1,85 @@ import React, { useEffect, useRef } from 'react'; -import { IPoint } from '../../Interfaces/IPoint'; +import { type IPoint } from '../../Interfaces/IPoint'; import { BAR_WIDTH } from '../Bar/Bar'; +import { SelectorMode } from '../SVG/SVG'; +import { type DrawParams } from '../Viewer/Viewer'; +import { RenderContainers, RenderSymbols } from './Renderer'; +import { RenderContainerSelector } from './SelectorContainer'; +import { RenderSymbolSelector } from './SelectorSymbol'; interface ICanvasProps { + className?: string width: number height: number - draw: (context: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint) => void - className?: string style?: React.CSSProperties + drawParams: DrawParams }; -function UseCanvas(draw: (context: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint) => void): React.RefObject { +function Draw( + ctx: CanvasRenderingContext2D, + frameCount: number, + scale: number, + translatePos: IPoint, + { + mainContainer, + selectorMode, + selectedContainer, + selectedSymbol, + containers, + symbols + }: DrawParams +): void { + if (mainContainer === undefined) { + return; + } + + const topDim = mainContainer.properties.y; + const leftDim = mainContainer.properties.x; + const rightDim = mainContainer.properties.x + mainContainer.properties.width; + const bottomDim = mainContainer.properties.y + mainContainer.properties.height; + + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + ctx.save(); + ctx.setTransform(scale, 0, 0, scale, translatePos.x, translatePos.y); + ctx.fillStyle = '#000000'; + + // Draw containers and symbol dimensions + RenderContainers( + ctx, + mainContainer, + containers, + leftDim, bottomDim, topDim, rightDim, scale); + + // Draw symbols and symbol dimensions + RenderSymbols(ctx, symbols, scale); + + // Draw selector + switch (selectorMode) { + case SelectorMode.Containers: + RenderContainerSelector(ctx, frameCount, { + containers, + scale, + selected: selectedContainer + }); + break; + case SelectorMode.Symbols: + RenderSymbolSelector(ctx, frameCount, { + symbols, + scale, + selected: selectedSymbol + }); + break; + } + + ctx.restore(); +} + +function UseCanvas( + draw: (context: CanvasRenderingContext2D, + frameCount: number, + scale: number, + translatePos: IPoint) => void +): React.RefObject { const canvasRef = useRef(null); const frameCount = useRef(0); const translatePos = useRef({ @@ -123,8 +192,21 @@ interface Viewer { viewerHeight: number } -export function Canvas({ width, height, draw, style, className }: ICanvasProps): JSX.Element { - const canvasRef = UseCanvas(draw); +export function Canvas({ + className, + width, + height, + style, + drawParams +}: ICanvasProps): JSX.Element { + const canvasRef = UseCanvas(( + ...CanvasProps + ) => { + Draw( + ...CanvasProps, + drawParams + ); + }); const [{ viewerWidth, viewerHeight }, setViewer] = React.useState({ viewerWidth: width, diff --git a/src/Components/Canvas/Container.ts b/src/Components/Canvas/Container.ts new file mode 100644 index 0000000..4914b4b --- /dev/null +++ b/src/Components/Canvas/Container.ts @@ -0,0 +1,18 @@ +import { type IContainerModel } from '../../Interfaces/IContainerModel'; + +export function RenderContainer( + ctx: CanvasRenderingContext2D, + container: IContainerModel, + x: number, + y: number +): void { + ctx.save(); + 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.restore(); +} diff --git a/src/Components/Canvas/Dimension.ts b/src/Components/Canvas/Dimension.ts index e95fc92..639bc2d 100644 --- a/src/Components/Canvas/Dimension.ts +++ b/src/Components/Canvas/Dimension.ts @@ -1,4 +1,5 @@ import { NOTCHES_LENGTH } from '../../utils/default'; +import { IDimensionStyle } from '../SVG/Elements/Dimension'; interface IDimensionProps { id: string @@ -7,7 +8,7 @@ interface IDimensionProps { xEnd: number yEnd: number text: string - strokeWidth: number + style: IDimensionStyle scale?: number } @@ -26,8 +27,11 @@ function ApplyParametric(x0: number, t: number, vx: number): number { export function RenderDimension(ctx: CanvasRenderingContext2D, props: IDimensionProps): void { const scale = props.scale ?? 1; - const strokeStyle = 'black'; - const lineWidth = 2 / scale; + const strokeStyle = props.style.color ?? 'black'; + const lineWidth = (props.style.width ?? 2) / scale; + const dashArray: number[] = props.style.dashArray?.split(' ') + .flatMap(array => array.split(',')) + .map(stringValue => parseInt(stringValue)) ?? []; /// We need to find the points of the notches // Get the vector of the line @@ -59,6 +63,7 @@ export function RenderDimension(ctx: CanvasRenderingContext2D, props: IDimension ctx.lineWidth = lineWidth; ctx.strokeStyle = strokeStyle; ctx.fillStyle = strokeStyle; + ctx.setLineDash(dashArray); ctx.moveTo(startTopX, startTopY); ctx.lineTo(startBottomX, startBottomY); ctx.stroke(); @@ -68,6 +73,7 @@ export function RenderDimension(ctx: CanvasRenderingContext2D, props: IDimension ctx.moveTo(endTopX, endTopY); ctx.lineTo(endBottomX, endBottomY); ctx.stroke(); + ctx.setLineDash([]); const textX = (props.xStart + props.xEnd) / 2; const textY = (props.yStart + props.yEnd) / 2; ctx.font = `${16 / scale}px Verdana`; diff --git a/src/Components/Canvas/DimensionLayer.ts b/src/Components/Canvas/DimensionLayer.ts index 1b40961..061a4e0 100644 --- a/src/Components/Canvas/DimensionLayer.ts +++ b/src/Components/Canvas/DimensionLayer.ts @@ -1,14 +1,19 @@ 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 { type IContainerModel } from '../../Interfaces/IContainerModel'; +import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { + SHOW_SELF_DIMENSIONS, + SHOW_BORROWER_DIMENSIONS, + SHOW_CHILDREN_DIMENSIONS, + DIMENSION_MARGIN, + SHOW_SELF_MARGINS_DIMENSIONS, DEFAULT_DIMENSION_SYMBOL_STYLE +} from '../../utils/default'; import { FindContainerById, MakeRecursionDFSIterator, Pairwise } from '../../utils/itertools'; import { TransformX, TransformY } from '../../utils/svg'; import { RenderDimension } from './Dimension'; -const MODULE_STROKE_WIDTH = 1; - -export function AddDimensions( +export function AddContainerDimensions( ctx: CanvasRenderingContext2D, containers: Map, container: IContainerModel, @@ -18,7 +23,8 @@ export function AddDimensions( depth: number ): void { ctx.beginPath(); - if (SHOW_SELF_DIMENSIONS && container.properties.dimensionOptions.selfDimensions.positions.length > 0) { + if (SHOW_SELF_DIMENSIONS && + container.properties.dimensionOptions.selfDimensions.positions.length > 0) { ActionByPosition( ctx, dimMapped, @@ -32,7 +38,24 @@ export function AddDimensions( ); } - if (SHOW_BORROWER_DIMENSIONS && container.properties.dimensionOptions.dimensionWithMarks.positions.length > 0) { + if (SHOW_SELF_MARGINS_DIMENSIONS && + container.properties.dimensionOptions.selfMarginsDimensions.positions.length > 0) { + ActionByPosition( + ctx, + dimMapped, + container.properties.dimensionOptions.selfMarginsDimensions.positions, + AddHorizontalSelfMarginsDimension, + AddVerticalSelfMarginDimension, + [ + container, + currentTransform, + scale + ] + ); + } + + if (SHOW_BORROWER_DIMENSIONS && + container.properties.dimensionOptions.dimensionWithMarks.positions.length > 0) { ActionByPosition( ctx, dimMapped, @@ -48,7 +71,9 @@ export function AddDimensions( ); } - if (SHOW_CHILDREN_DIMENSIONS && container.properties.dimensionOptions.childrenDimensions.positions.length > 0 && container.children.length >= 2) { + if (SHOW_CHILDREN_DIMENSIONS && + container.properties.dimensionOptions.childrenDimensions.positions.length > 0 && + container.children.length >= 2) { ActionByPosition( ctx, dimMapped, @@ -64,8 +89,33 @@ export function AddDimensions( } } +export function AddSymbolDimensions( + ctx: CanvasRenderingContext2D, + symbol: ISymbolModel, + scale: number, + depth: number +): void { + if (symbol.isVertical) { + AddVerticalSymbolDimension( + ctx, + symbol, + scale, + depth + ); + return; + } + + AddHorizontalSymbolDimension( + ctx, + symbol, + scale, + depth + ); +} + /** * Fonction that call another function given the positions + * @param ctx * @param dimMapped Position mapped depending on the Position enum in order: * [0:left, 1:bottom, 2:up, 3:right] * @param positions List of positions @@ -108,6 +158,7 @@ function AddHorizontalChildrenDimension( scale: number ): void { const childrenId = `dim-y${yDim.toFixed(0)}-children-${container.properties.id}`; + const style = container.properties.dimensionOptions.childrenDimensions; const lastChildId = container.children[container.children.length - 1]; const lastChild = FindContainerById(containers, lastChildId); @@ -115,8 +166,14 @@ function AddHorizontalChildrenDimension( if (lastChild === undefined) { return; } - let xChildrenStart = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference); - let xChildrenEnd = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference); + 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--) { @@ -152,9 +209,9 @@ function AddHorizontalChildrenDimension( xEnd: xChildrenEnd + offset, yStart: yDim, yEnd: yDim, - strokeWidth: MODULE_STROKE_WIDTH, text: textChildren, - scale + scale, + style }); } @@ -168,6 +225,7 @@ function AddVerticalChildrenDimension( scale: number ): void { const childrenId = `dim-x${xDim.toFixed(0)}-children-${container.properties.id}`; + const style = container.properties.dimensionOptions.childrenDimensions; const lastChildId = container.children[container.children.length - 1]; const lastChild = FindContainerById(containers, lastChildId); @@ -176,7 +234,10 @@ function AddVerticalChildrenDimension( return; } - let yChildrenStart = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference); + let yChildrenStart = TransformY( + lastChild.properties.y, + lastChild.properties.height, + lastChild.properties.positionReference); let yChildrenEnd = yChildrenStart; // Find the min and max @@ -218,9 +279,9 @@ function AddVerticalChildrenDimension( xEnd: xDim, yStart: yChildrenStart + offset, yEnd: yChildrenEnd + offset, - strokeWidth: MODULE_STROKE_WIDTH, text: textChildren, - scale + scale, + style }); } @@ -234,6 +295,7 @@ function AddHorizontalBorrowerDimension( scale: number ): void { const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); + const style = container.properties.dimensionOptions.dimensionWithMarks; const marks = []; // list of vertical lines for the dimension for (const { container: childContainer, currentTransform: childCurrentTransform @@ -274,9 +336,9 @@ function AddHorizontalBorrowerDimension( xEnd: next, yStart: yDim, yEnd: yDim, - strokeWidth: MODULE_STROKE_WIDTH, text: value.toFixed(0), - scale + scale, + style }); count++; } @@ -293,6 +355,7 @@ function AddVerticalBorrowerDimension( scale: number ): void { const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); + const style = container.properties.dimensionOptions.dimensionWithMarks; const marks = []; // list of vertical lines for the dimension for (const { container: childContainer, currentTransform: childCurrentTransform @@ -338,9 +401,9 @@ function AddVerticalBorrowerDimension( xEnd: xDim, yStart: cur, yEnd: next, - strokeWidth: MODULE_STROKE_WIDTH, text: value.toFixed(0), - scale + scale, + style }); count++; } @@ -354,6 +417,7 @@ function AddVerticalSelfDimension( currentTransform: [number, number], scale: number ): void { + const style = container.properties.dimensionOptions.selfDimensions; const height = container.properties.height; const idVert = `dim-x${xDim.toFixed(0)}-${container.properties.id}`; let yStart = container.properties.y + currentTransform[1] + height; @@ -372,9 +436,9 @@ function AddVerticalSelfDimension( xEnd: xDim, yStart, yEnd, - strokeWidth: MODULE_STROKE_WIDTH, text: textVert, - scale + scale, + style }); } @@ -385,6 +449,7 @@ function AddHorizontalSelfDimension( currentTransform: [number, number], scale: number ): void { + const style = container.properties.dimensionOptions.selfDimensions; const width = container.properties.width; const id = `dim-y${yDim.toFixed(0)}-${container.properties.id}`; const xStart = container.properties.x + currentTransform[0]; @@ -398,8 +463,180 @@ function AddHorizontalSelfDimension( yStart: yDim, xEnd, yEnd: yDim, - strokeWidth: MODULE_STROKE_WIDTH, text, - scale + scale, + style + }); +} + +function AddHorizontalSelfMarginsDimension( + ctx: CanvasRenderingContext2D, + yDim: number, + container: IContainerModel, + currentTransform: [number, number], + dimensions: React.ReactNode[], + scale: number +): void { + const style = container.properties.dimensionOptions.selfMarginsDimensions; + const left = container.properties.margin.left; + if (left != null) { + const id = `dim-y-margin-left${yDim.toFixed(0)}-${container.properties.id}`; + const xStart = container.properties.x + currentTransform[0] - left; + const xEnd = xStart + left; + const text = left + .toFixed(0) + .toString(); + RenderDimension(ctx, { + id, + xStart, + yStart: yDim, + xEnd, + yEnd: yDim, + text, + scale, + style + }); + } + + const right = container.properties.margin.right; + if (right != null) { + const id = `dim-y-margin-right${yDim.toFixed(0)}-${container.properties.id}`; + const xStart = container.properties.x + container.properties.width + currentTransform[0]; + const xEnd = xStart + right; + const text = right + .toFixed(0) + .toString(); + + RenderDimension(ctx, { + id, + xStart, + yStart: yDim, + xEnd, + yEnd: yDim, + text, + scale, + style + }); + } +} + +function AddVerticalSelfMarginDimension( + ctx: CanvasRenderingContext2D, + xDim: number, + isRight: boolean, + container: IContainerModel, + currentTransform: [number, number], + scale: number +): void { + const style = container.properties.dimensionOptions.selfMarginsDimensions; + const top = container.properties.margin.top; + if (top != null) { + const idVert = `dim-x-margin-top${xDim.toFixed(0)}-${container.properties.id}`; + let yStart = container.properties.y + currentTransform[1]; + let yEnd = yStart - top; + const textVert = top + .toFixed(0) + .toString(); + + if (isRight) { + [yStart, yEnd] = [yEnd, yStart]; + } + + RenderDimension(ctx, { + id: idVert, + xStart: xDim, + yStart, + xEnd: xDim, + yEnd, + text: textVert, + scale, + style + }); + } + const bottom = container.properties.margin.bottom; + if (bottom != null) { + const idVert = `dim-x-margin-bottom${xDim.toFixed(0)}-${container.properties.id}`; + let yStart = container.properties.y + container.properties.height + bottom + currentTransform[1]; + let yEnd = yStart - bottom; + const textVert = bottom + .toFixed(0) + .toString(); + + if (isRight) { + [yStart, yEnd] = [yEnd, yStart]; + } + + RenderDimension(ctx, { + id: idVert, + xStart: xDim, + yStart, + xEnd: xDim, + yEnd, + text: textVert, + scale, + style + }); + } +} + +function AddHorizontalSymbolDimension( + ctx: CanvasRenderingContext2D, + symbol: ISymbolModel, + scale: number, + depth: number +): void { + const width = TransformX(symbol.offset, symbol.width, symbol.config.PositionReference); + + if (width == null || width <= 0) { + return; + } + + const id = `dim-y-margin-left${symbol.width.toFixed(0)}-${symbol.id}`; + + const offset = (DIMENSION_MARGIN * (depth + 1)) / scale; + const text = width + .toFixed(0) + .toString(); + + RenderDimension(ctx, { + id, + xStart: 0, + yStart: -offset, + xEnd: width, + yEnd: -offset, + text, + scale, + style: DEFAULT_DIMENSION_SYMBOL_STYLE + }); +} + +function AddVerticalSymbolDimension( + ctx: CanvasRenderingContext2D, + symbol: ISymbolModel, + scale: number, + depth: number +): void { + const height = TransformY(symbol.offset, symbol.height, symbol.config.PositionReference); + + if (height == null || height <= 0) { + return; + } + + const id = `dim-y-margin-left${symbol.width.toFixed(0)}-${symbol.id}`; + + const offset = (DIMENSION_MARGIN * (depth + 1)) / scale; + const text = height + .toFixed(0) + .toString(); + + RenderDimension(ctx, { + id, + xStart: -offset, + yStart: height, + xEnd: -offset, + yEnd: 0, + text, + scale, + style: DEFAULT_DIMENSION_SYMBOL_STYLE }); } diff --git a/src/Components/Canvas/Renderer.ts b/src/Components/Canvas/Renderer.ts new file mode 100644 index 0000000..73abdac --- /dev/null +++ b/src/Components/Canvas/Renderer.ts @@ -0,0 +1,103 @@ +import { type IContainerModel } from '../../Interfaces/IContainerModel'; +import { type IHistoryState } from '../../Interfaces/IHistoryState'; +import { ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { DIMENSION_MARGIN } from '../../utils/default'; +import { MakeRecursionDFSIterator } from '../../utils/itertools'; +import { RenderContainer } from './Container'; +import { AddContainerDimensions, AddSymbolDimensions } from './DimensionLayer'; +import { RenderSymbol } from './Symbol'; + +export function RenderContainers( + ctx: CanvasRenderingContext2D, + root: IContainerModel, + containers: Map, + leftDim: number, + bottomDim: number, + topDim: number, + rightDim: number, + scale: number +): void { + const it = MakeRecursionDFSIterator(root, containers, 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 + RenderContainer(ctx, container, x, y); + + // Draw dimensions + RenderContainerDimensions( + ctx, + leftDim, + bottomDim, + topDim, + rightDim, + depth, + scale, + containers, + container, + currentTransform + ); + } +} + +export function RenderContainerDimensions( + ctx: CanvasRenderingContext2D, + leftDim: number, + bottomDim: number, + topDim: number, + rightDim: number, + depth: number, + scale: number, + containers: Map, + container: IContainerModel, + currentTransform: [number, number] +): void { + ctx.save(); + const depthOffset = (DIMENSION_MARGIN * (depth + 1)) / scale; + const containerLeftDim = leftDim - depthOffset; + const containerTopDim = topDim - depthOffset; + const containerBottomDim = bottomDim + depthOffset; + const containerRightDim = rightDim + depthOffset; + const dimMapped = [containerLeftDim, containerBottomDim, containerTopDim, containerRightDim]; + AddContainerDimensions(ctx, containers, container, dimMapped, currentTransform, scale, depth); + ctx.restore(); +} + +export function RenderSymbols( + ctx: CanvasRenderingContext2D, + symbols: Map, + scale: number +): void { + symbols.forEach((symbol: ISymbolModel) => { + RenderSymbol(ctx, symbol); + + if (!symbol.showDimension) { + return; + } + + // TODO: Implement DimensionManager + AddSymbolDimensions(ctx, symbol, scale, 0); + }); +} + +export function RenderSymbolDimensions( + ctx: CanvasRenderingContext2D, + depth: number, + scale: number, + containers: Map, + container: IContainerModel, + currentTransform: [number, number] +): void { + ctx.save(); + const depthOffset = (DIMENSION_MARGIN * (depth + 1)) / scale; + const containerLeftDim = -depthOffset; + const containerTopDim = -depthOffset; + const containerBottomDim = depthOffset; + const containerRightDim = depthOffset; + const dimMapped = [containerLeftDim, containerBottomDim, containerTopDim, containerRightDim]; + AddContainerDimensions(ctx, containers, container, dimMapped, currentTransform, scale, depth); + ctx.restore(); +} diff --git a/src/Components/Canvas/Selector.ts b/src/Components/Canvas/Selector.ts index c9ea814..1dc1654 100644 --- a/src/Components/Canvas/Selector.ts +++ b/src/Components/Canvas/Selector.ts @@ -1,33 +1,26 @@ -import { IContainerModel } from '../../Interfaces/IContainerModel'; import { SHOW_SELECTOR_TEXT } from '../../utils/default'; -import { GetAbsolutePosition } from '../../utils/itertools'; -import { RemoveMargin } from '../../utils/svg'; interface ISelectorProps { - containers: Map - selected?: IContainerModel - scale?: number + text: string + x: number + y: number + width: number + height: number + 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.containers, 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 - )); - +export function RenderSelector( + ctx: CanvasRenderingContext2D, + frameCount: number, + { + text, + x, + y, + width, + height, + scale + }: ISelectorProps +): void { const xText = x + width / 2; const yText = y + height / 2; @@ -42,7 +35,7 @@ export function RenderSelector(ctx: CanvasRenderingContext2D, frameCount: number if (SHOW_SELECTOR_TEXT) { ctx.font = `${16 / scale}px Verdana`; ctx.textAlign = 'center'; - ctx.fillText(props.selected.properties.displayedText, xText, yText); + ctx.fillText(text, xText, yText); ctx.textAlign = 'left'; } } diff --git a/src/Components/Canvas/SelectorContainer.ts b/src/Components/Canvas/SelectorContainer.ts new file mode 100644 index 0000000..c151f6f --- /dev/null +++ b/src/Components/Canvas/SelectorContainer.ts @@ -0,0 +1,48 @@ +import { type IContainerModel } from '../../Interfaces/IContainerModel'; +import { GetAbsolutePosition } from '../../utils/itertools'; +import { RemoveMargin } from '../../utils/svg'; +import { RenderSelector } from './Selector'; + +interface ISelectorProps { + containers: Map + selected?: IContainerModel + scale?: number +} + +export function RenderContainerSelector( + 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.containers, 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 text = props.selected.properties.displayedText; + RenderSelector( + ctx, + frameCount, + { + text, + x, + y, + width, + height, + scale + } + ); +} diff --git a/src/Components/Canvas/SelectorSymbol.ts b/src/Components/Canvas/SelectorSymbol.ts new file mode 100644 index 0000000..69f49f5 --- /dev/null +++ b/src/Components/Canvas/SelectorSymbol.ts @@ -0,0 +1,47 @@ +import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { SYMBOL_MARGIN } from '../../utils/default'; +import { RenderSelector } from './Selector'; + +interface ISelectorProps { + symbols: Map + selected?: ISymbolModel +} + +export function RenderSymbolSelector( + ctx: CanvasRenderingContext2D, + frameCount: number, + props: ISelectorProps +): void { + if (props.selected === undefined || props.selected === null) { + return; + } + + const [width, height] = [ + props.selected.width, + props.selected.height + ]; + + let x, y: number; + + if (props.selected.isVertical) { + x = -SYMBOL_MARGIN - props.selected.width; + y = props.selected.offset; + } else { + x = props.selected.offset; + y = -SYMBOL_MARGIN - props.selected.height; + } + + const text = props.selected.displayedText; + RenderSelector( + ctx, + frameCount, + { + text, + x, + y, + width, + height, + scale: 1 + } + ); +} diff --git a/src/Components/Canvas/Symbol.ts b/src/Components/Canvas/Symbol.ts index 96ca161..842d433 100644 --- a/src/Components/Canvas/Symbol.ts +++ b/src/Components/Canvas/Symbol.ts @@ -1,12 +1,11 @@ -import { ISymbolModel } from '../../Interfaces/ISymbolModel'; -import { DIMENSION_MARGIN } from '../../utils/default'; +import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { SYMBOL_MARGIN } from '../../utils/default'; const IMAGE_CACHE = new Map(); export function RenderSymbol( - symbol: ISymbolModel, ctx: CanvasRenderingContext2D, - scale: number): void { + symbol: ISymbolModel): void { const href = symbol.config.Image.Base64Image ?? symbol.config.Image.Url; if (href === undefined) { @@ -19,27 +18,38 @@ export function RenderSymbol( newImage.src = href; IMAGE_CACHE.set(href, newImage); newImage.onload = () => { - DrawImage(ctx, scale, newImage, symbol); + DrawImage(ctx, newImage, symbol); }; return; } - DrawImage(ctx, scale, image, symbol); + DrawImage(ctx, image, symbol); } + function DrawImage( ctx: CanvasRenderingContext2D, - scale: number, image: HTMLImageElement, symbol: ISymbolModel ): void { + let x, y: number; + if (symbol.isVertical) { + x = -SYMBOL_MARGIN - symbol.width; + y = symbol.offset; + } else { + x = symbol.offset; + y = -SYMBOL_MARGIN - symbol.height; + } + ctx.save(); ctx.fillStyle = '#000000'; + const width = symbol.width; + const height = symbol.height; ctx.drawImage( image, - symbol.x, - -DIMENSION_MARGIN, - symbol.width, - symbol.height + x, + y, + width, + height ); ctx.restore(); } diff --git a/src/Components/ContainerProperties/ContainerForm.tsx b/src/Components/ContainerProperties/ContainerForm.tsx index 8b7a505..9a19743 100644 --- a/src/Components/ContainerProperties/ContainerForm.tsx +++ b/src/Components/ContainerProperties/ContainerForm.tsx @@ -5,7 +5,8 @@ import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; import { SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS, - SHOW_SELF_DIMENSIONS, SHOW_SELF_MARGINS_DIMENSIONS + SHOW_SELF_DIMENSIONS, + SHOW_SELF_MARGINS_DIMENSIONS } from '../../utils/default'; import { ApplyWidthMargin, @@ -27,8 +28,12 @@ import { OrientationSelector } from '../RadioGroupButtons/OrientationSelector'; import { OrientationCheckboxes } from '../CheckboxGroupButtons/OrientationCheckboxes'; import { PositionCheckboxes } from '../CheckboxGroupButtons/PositionCheckboxes'; import { Category } from '../Category/Category'; +import { Orientation } from '../../Enums/Orientation'; +import { FindContainerById } from '../../utils/itertools'; +import { type IContainerModel } from '../../Interfaces/IContainerModel'; interface IContainerFormProps { + containers: Map properties: IContainerProperties symbols: Map onChange: (key: string, value: string | number | boolean | number[], type?: PropertyType) => void @@ -36,6 +41,8 @@ interface IContainerFormProps { export function ContainerForm(props: IContainerFormProps): JSX.Element { const categoryHeight = 'h-11'; + const parent = FindContainerById(props.containers, props.properties.parentId); + const isVertical = parent?.properties.orientation === Orientation.Vertical; return (
{ props.onChange('width', ApplyWidthMargin(Number(value), props.properties.margin.left, props.properties.margin.right)); }} + value={(RemoveWidthMargin(props.properties.width, + props.properties.margin.left, + props.properties.margin.right)).toString()} + onChange={(value) => { + props.onChange('width', ApplyWidthMargin(Number(value), + props.properties.margin.left, + props.properties.margin.right)); + }} isDisabled={props.properties.isFlex}/> { props.onChange('height', ApplyWidthMargin(Number(value), props.properties.margin.top, props.properties.margin.bottom)); }} + value={(RemoveWidthMargin(props.properties.height, + props.properties.margin.top, + props.properties.margin.bottom)).toString()} + onChange={(value) => { + props.onChange('height', ApplyWidthMargin(Number(value), + props.properties.margin.top, + props.properties.margin.bottom)); + }} isDisabled={props.properties.isFlex} /> ({ + inputs={[...props.symbols.values()].filter(symbol => (symbol.isVertical === isVertical)).map(symbol => ({ key: symbol.id, text: symbol.id, value: symbol.id @@ -366,7 +385,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element { labelClassName='' inputClassName='' type='color' - value={props.properties.dimensionOptions.selfDimensions.color} + value={props.properties.dimensionOptions.selfDimensions.color ?? '#000000'} onChange={(e) => { props.onChange('color', e.target.value, PropertyType.SelfDimension); }}/> { props.onChange('color', e.target.value, PropertyType.SelfMarginDimension); }}/> { props.onChange('color', e.target.value, PropertyType.ChildrenDimensions); }}/> { props.onChange('color', e.target.value, PropertyType.DimensionWithMarks); }}/>
- { props.onChange('stroke', value, PropertyType.Style); }} - /> + type='color' + value={props.properties.style.stroke ?? '#000000'} + onChange={(e) => { props.onChange('stroke', e.target.value, PropertyType.Style); }}/> { props.onChange('strokeWidth', Number(value), PropertyType.Style); }} /> - { props.onChange('fill', value, PropertyType.Style); }} - /> + type='color' + value={props.properties.style.fill ?? '#000000'} + onChange={(e) => { props.onChange('fill', e.target.value, PropertyType.Style); }}/> properties?: IContainerProperties symbols: Map onChange: (key: string, value: string | number | boolean | number[], type?: PropertyType) => void @@ -18,6 +20,7 @@ export function ContainerProperties(props: IPropertiesProps): JSX.Element { return (
diff --git a/src/Components/Editor/Actions/SymbolOperations.ts b/src/Components/Editor/Actions/SymbolOperations.ts index 7464193..abff45f 100644 --- a/src/Components/Editor/Actions/SymbolOperations.ts +++ b/src/Components/Editor/Actions/SymbolOperations.ts @@ -1,10 +1,10 @@ -import { IConfiguration } from '../../../Interfaces/IConfiguration'; -import { IContainerModel } from '../../../Interfaces/IContainerModel'; -import { IHistoryState } from '../../../Interfaces/IHistoryState'; -import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; +import { type IConfiguration } from '../../../Interfaces/IConfiguration'; +import { type IContainerModel } from '../../../Interfaces/IContainerModel'; +import { type IHistoryState } from '../../../Interfaces/IHistoryState'; +import { type ISymbolModel } from '../../../Interfaces/ISymbolModel'; import { GetDefaultSymbolModel } from '../../../utils/default'; import { FindContainerById } from '../../../utils/itertools'; -import { RestoreX } from '../../../utils/svg'; +import {RestoreX, RestoreY} from '../../../utils/svg'; import { ApplyBehaviors, ApplyBehaviorsOnSiblingsChildren } from '../Behaviors/Behaviors'; import { GetCurrentHistory, GetCurrentHistoryState, UpdateCounters } from '../Editor'; import { AddContainers } from './AddContainer'; @@ -32,7 +32,12 @@ export function AddSymbol( const newSymbols = structuredClone(current.symbols); const newSymbol: ISymbolModel = GetDefaultSymbolModel(name, typeCounters, type, symbolConfig); const containers = structuredClone(current.containers); - newSymbol.x = RestoreX(newSymbol.x, newSymbol.width, newSymbol.config.PositionReference); + + if (newSymbol.isVertical) { + newSymbol.offset = RestoreY(newSymbol.offset, newSymbol.height, newSymbol.config.PositionReference); + } else { + newSymbol.offset = RestoreX(newSymbol.offset, newSymbol.width, newSymbol.config.PositionReference); + } newSymbols.set(newSymbol.id, newSymbol); diff --git a/src/Components/Editor/Behaviors/SymbolBehaviors.ts b/src/Components/Editor/Behaviors/SymbolBehaviors.ts index 5efde19..97c872d 100644 --- a/src/Components/Editor/Behaviors/SymbolBehaviors.ts +++ b/src/Components/Editor/Behaviors/SymbolBehaviors.ts @@ -1,16 +1,39 @@ -import { IContainerModel } from '../../../Interfaces/IContainerModel'; -import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; +import { type IContainerModel } from '../../../Interfaces/IContainerModel'; +import { type ISymbolModel } from '../../../Interfaces/ISymbolModel'; import { ApplyParentTransform, FindContainerById } from '../../../utils/itertools'; -import { RestoreX, TransformX } from '../../../utils/svg'; +import { RestoreX, RestoreY, TransformX, TransformY } from '../../../utils/svg'; -export function ApplySymbol(containers: Map, container: IContainerModel, symbol: ISymbolModel): IContainerModel { - container.properties.x = TransformX(symbol.x, symbol.width, symbol.config.PositionReference); - container.properties.x = RestoreX(container.properties.x, container.properties.width, container.properties.positionReference); - const parent = FindContainerById(containers, container.properties.parentId); - let x = 0; - if (parent !== undefined && parent !== null) { - ([x] = ApplyParentTransform(containers, parent, container.properties.x, 0)); +export function ApplySymbol(containers: Map, + container: IContainerModel, + symbol: ISymbolModel): IContainerModel { + if (symbol.isVertical) { + container.properties.y = TransformY(symbol.offset, + symbol.height, + symbol.config.PositionReference); + container.properties.y = RestoreY(container.properties.y, + container.properties.height, + container.properties.positionReference); + const parent = FindContainerById(containers, container.properties.parentId); + let y = 0; + if (parent !== undefined && parent !== null) { + ([,y] = ApplyParentTransform(containers, parent, 0, container.properties.y)); + } + container.properties.y = y; + return container; + } else { + container.properties.x = TransformX( + symbol.offset, + symbol.width, + symbol.config.PositionReference); + container.properties.x = RestoreX(container.properties.x, + container.properties.width, + container.properties.positionReference); + const parent = FindContainerById(containers, container.properties.parentId); + let x = 0; + if (parent !== undefined && parent !== null) { + ([x] = ApplyParentTransform(containers, parent, container.properties.x, 0)); + } + container.properties.x = x; + return container; } - container.properties.x = x; - return container; } diff --git a/src/Components/ElementsList/ElementsSideBar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx similarity index 78% rename from src/Components/ElementsList/ElementsSideBar.tsx rename to src/Components/ElementsSidebar/ElementsSidebar.tsx index 78d9b6b..c583413 100644 --- a/src/Components/ElementsList/ElementsSideBar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,21 +1,22 @@ import * as React from 'react'; -import { useState } from 'react'; import useSize from '@react-hook/size'; import { FixedSizeList as List } from 'react-window'; import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; import { ContainerProperties } from '../ContainerProperties/ContainerProperties'; -import { IContainerModel } from '../../Interfaces/IContainerModel'; +import { type IContainerModel } from '../../Interfaces/IContainerModel'; import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools'; -import { ISymbolModel } from '../../Interfaces/ISymbolModel'; -import { PropertyType } from '../../Enums/PropertyType'; +import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { type PropertyType } from '../../Enums/PropertyType'; import { ToggleSideBar } from '../Sidebar/ToggleSideBar/ToggleSideBar'; import { Text } from '../Text/Text'; +import { ExtendedSidebar } from '../UI/UI'; -interface IElementsSideBarProps { +interface IElementsSidebarProps { containers: Map mainContainer: IContainerModel symbols: Map selectedContainer: IContainerModel | undefined + selectedExtendedSidebar: ExtendedSidebar onPropertyChange: ( key: string, value: string | number | boolean | number[], @@ -23,8 +24,7 @@ interface IElementsSideBarProps { ) => void selectContainer: (containerId: string) => void addContainer: (index: number, type: string, parent: string) => void - isExpanded: boolean - onExpandChange: () => void + onExpandChange: (value: ExtendedSidebar) => void } function RemoveBorderClasses(target: HTMLButtonElement, exception: string = ''): void { @@ -124,11 +124,10 @@ function HandleOnDrop( } } -export function ElementsSideBar(props: IElementsSideBarProps): JSX.Element { +export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element { // States const divRef = React.useRef(null); const [,height] = useSize(divRef); - const [showProperties, setShowProperties] = useState(props.isExpanded); // Render const it = MakeRecursionDFSIterator(props.mainContainer, props.containers, 0, [0, 0], true); @@ -167,18 +166,28 @@ export function ElementsSideBar(props: IElementsSideBarProps): JSX.Element { return (
- {showProperties && -
- -
+ {props.selectedExtendedSidebar === ExtendedSidebar.Property && +
+ +
}
- { setShowProperties(newValue); props.onExpandChange(); }} /> + { + const newValue = props.selectedExtendedSidebar !== ExtendedSidebar.Property + ? ExtendedSidebar.Property + : ExtendedSidebar.None; + props.onExpandChange(newValue); + }} + />
selectContainer(container.properties.id)} - onDrop={(event) => HandleOnDrop(event, containers, mainContainer, addContainer)} - onDragOver={(event) => HandleDragOver(event, mainContainer)} - onDragLeave={(event) => HandleDragLeave(event)} + onClick={() => { selectContainer(container.properties.id); }} + onDrop={(event) => { HandleOnDrop(event, containers, mainContainer, addContainer); }} + onDragOver={(event) => { HandleDragOver(event, mainContainer); }} + onDragLeave={(event) => { HandleDragLeave(event); }} > {verticalBars} {text} diff --git a/src/Components/SVG/Elements/Dimension.tsx b/src/Components/SVG/Elements/Dimension.tsx index 30469be..8e98bc1 100644 --- a/src/Components/SVG/Elements/Dimension.tsx +++ b/src/Components/SVG/Elements/Dimension.tsx @@ -31,7 +31,7 @@ function ApplyParametric(x0: number, t: number, vx: number): number { export function Dimension(props: IDimensionProps): JSX.Element { const scale = props.scale ?? 1; const style: React.CSSProperties = { - stroke: props.style.color, + stroke: props.style.color ?? '#000000', strokeWidth: (props.style.width ?? 2) / scale, strokeDasharray: props.style.dashArray }; diff --git a/src/Components/SVG/Elements/DimensionLayer.tsx b/src/Components/SVG/Elements/DimensionLayer.tsx index 3a2b679..35ca740 100644 --- a/src/Components/SVG/Elements/DimensionLayer.tsx +++ b/src/Components/SVG/Elements/DimensionLayer.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Orientation } from '../../../Enums/Orientation'; import { Position } from '../../../Enums/Position'; import { + DEFAULT_DIMENSION_SYMBOL_STYLE, DIMENSION_MARGIN, SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS, @@ -10,7 +11,7 @@ import { } from '../../../utils/default'; import { FindContainerById, MakeRecursionDFSIterator, Pairwise } from '../../../utils/itertools'; import { TransformX, TransformY } from '../../../utils/svg'; -import { Dimension, type IDimensionStyle } from './Dimension'; +import { Dimension } from './Dimension'; import { type IContainerModel } from '../../../Interfaces/IContainerModel'; import { type ISymbolModel } from '../../../Interfaces/ISymbolModel'; @@ -41,14 +42,12 @@ function ActionByPosition( positions.forEach((position: Position) => { const dim = dimMapped[position]; switch (position) { + case Position.Right: case Position.Left: - case Position.Right: { - const isRight = position === Position.Right; - verticalAction(dim, isRight, ...params); + verticalAction(dim, false, ...params); break; - } - case Position.Down: case Position.Up: + case Position.Down: horizontalAction(dim, ...params); break; } @@ -94,7 +93,8 @@ function Dimensions({ containers, symbols, root, scale }: IDimensionLayerProps): ); } - if (SHOW_SELF_MARGINS_DIMENSIONS && container.properties.dimensionOptions.selfMarginsDimensions.positions.length > 0) { + if (SHOW_SELF_MARGINS_DIMENSIONS && + container.properties.dimensionOptions.selfMarginsDimensions.positions.length > 0) { ActionByPosition( dimMapped, container.properties.dimensionOptions.selfMarginsDimensions.positions, @@ -127,7 +127,9 @@ function Dimensions({ containers, symbols, root, scale }: IDimensionLayerProps): ); } - if (SHOW_CHILDREN_DIMENSIONS && container.properties.dimensionOptions.childrenDimensions.positions.length > 0 && container.children.length >= 2) { + if (SHOW_CHILDREN_DIMENSIONS && + container.properties.dimensionOptions.childrenDimensions.positions.length > 0 && + container.children.length >= 2) { ActionByPosition( dimMapped, container.properties.dimensionOptions.childrenDimensions.positions, @@ -144,29 +146,26 @@ function Dimensions({ containers, symbols, root, scale }: IDimensionLayerProps): } } - let startDepthSymbols: number = 0; - for (const symbol of symbols) { - if (symbol[1].showDimension) { - startDepthSymbols++; - AddHorizontalSymbolDimension( - symbol[1], - dimensions, - scale, - startDepthSymbols - ); + // TODO: Implement DimensionManager + symbols.forEach((symbol) => { + if (symbol.showDimension) { + if (symbol.isVertical) { + AddVerticalSymbolDimension(symbol, dimensions, scale, 0); + } else { + AddHorizontalSymbolDimension(symbol, dimensions, scale, 0); + } } - } + }); return dimensions; } -function AddHorizontalSymbolDimension( - symbol: ISymbolModel, +function AddHorizontalSymbolDimension(symbol: ISymbolModel, dimensions: React.ReactNode[], scale: number, depth: number ): void { - const width = symbol.x + (symbol.width / 2); + const width = TransformX(symbol.offset, symbol.width, symbol.config.PositionReference); if (width != null && width > 0) { const id = `dim-y-margin-left${symbol.width.toFixed(0)}-${symbol.id}`; @@ -174,11 +173,6 @@ function AddHorizontalSymbolDimension( const text = width .toFixed(0) .toString(); - - // TODO: Put this in default.ts - const defaultDimensionSymbolStyle: IDimensionStyle = { - color: 'black' - }; dimensions.push( + style={DEFAULT_DIMENSION_SYMBOL_STYLE}/> + ); + } +} + +function AddVerticalSymbolDimension(symbol: ISymbolModel, + dimensions: React.ReactNode[], + scale: number, + depth: number +): void { + const height = TransformY(symbol.offset, symbol.height, symbol.config.PositionReference); + if (height != null && height > 0) { + const id = `dim-x-margin-left${symbol.height.toFixed(0)}-${symbol.id}`; + + const offset = (DIMENSION_MARGIN * (depth + 1)) / scale; + const text = height + .toFixed(0) + .toString(); + dimensions.push( + ); } } @@ -227,8 +249,12 @@ function AddHorizontalChildrenDimension( return; } - let xChildrenStart = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference); - let xChildrenEnd = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference); + 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--) { @@ -290,8 +316,12 @@ function AddVerticalChildrenDimension( return; } - let yChildrenStart = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference); - let yChildrenEnd = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference); + 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--) { @@ -537,7 +567,7 @@ function AddHorizontalSelfMarginsDimension( ): void { const style = container.properties.dimensionOptions.selfMarginsDimensions; const left = container.properties.margin.left; - if (left != null) { + if (left != null && left > 0) { const id = `dim-y-margin-left${yDim.toFixed(0)}-${container.properties.id}`; const xStart = container.properties.x + currentTransform[0] - left; const xEnd = xStart + left; @@ -559,7 +589,7 @@ function AddHorizontalSelfMarginsDimension( } const right = container.properties.margin.right; - if (right != null) { + if (right != null && right > 0) { const id = `dim-y-margin-right${yDim.toFixed(0)}-${container.properties.id}`; const xStart = container.properties.x + container.properties.width + currentTransform[0]; const xEnd = xStart + right; @@ -591,7 +621,7 @@ function AddVerticalSelfMarginDimension( ): void { const style = container.properties.dimensionOptions.selfMarginsDimensions; const top = container.properties.margin.top; - if (top != null) { + if (top != null && top > 0) { const idVert = `dim-x-margin-top${xDim.toFixed(0)}-${container.properties.id}`; let yStart = container.properties.y + currentTransform[1]; let yEnd = yStart - top; @@ -617,7 +647,7 @@ function AddVerticalSelfMarginDimension( ); } const bottom = container.properties.margin.bottom; - if (bottom != null) { + if (bottom != null && bottom > 0) { const idVert = `dim-x-margin-bottom${xDim.toFixed(0)}-${container.properties.id}`; let yStart = container.properties.y + container.properties.height + bottom + currentTransform[1]; let yEnd = yStart - bottom; diff --git a/src/Components/SVG/Elements/SelectorSymbol/SelectorSymbol.tsx b/src/Components/SVG/Elements/SelectorSymbol/SelectorSymbol.tsx index 01133fb..22f4800 100644 --- a/src/Components/SVG/Elements/SelectorSymbol/SelectorSymbol.tsx +++ b/src/Components/SVG/Elements/SelectorSymbol/SelectorSymbol.tsx @@ -1,13 +1,12 @@ import '../Selector.scss'; import * as React from 'react'; -import { SYMBOL_MARGIN } from '../../../../utils/default'; +import { DIMENSION_MARGIN, SYMBOL_MARGIN } from '../../../../utils/default'; import { type ISymbolModel } from '../../../../Interfaces/ISymbolModel'; import { Selector } from '../Selector/Selector'; interface ISelectorSymbolProps { symbols: Map selected?: ISymbolModel - scale?: number } export function SelectorSymbol(props: ISelectorSymbolProps): JSX.Element { @@ -18,30 +17,27 @@ export function SelectorSymbol(props: ISelectorSymbolProps): JSX.Element { ); } - const scale = (props.scale ?? 1); - const [width, height] = [ - props.selected.width / scale, - props.selected.height / scale - ]; + let x, y: number; - const [x, y] = [ - props.selected.x + props.selected.width / 2, - -SYMBOL_MARGIN - height]; + const scaledHeight = props.selected.height; + const scaledWidth = props.selected.width; - const style: React.CSSProperties = { - transform: 'translateX(-50%)', - transformBox: 'fill-box' - }; + if (props.selected.isVertical) { + x = -SYMBOL_MARGIN - props.selected.width; + y = props.selected.offset; + } else { + x = props.selected.offset; + y = -SYMBOL_MARGIN - props.selected.height; + } return ( ); } diff --git a/src/Components/SVG/Elements/Symbol.tsx b/src/Components/SVG/Elements/Symbol.tsx index 09a19f9..c3a6eb8 100644 --- a/src/Components/SVG/Elements/Symbol.tsx +++ b/src/Components/SVG/Elements/Symbol.tsx @@ -1,7 +1,7 @@ import { Interweave } from 'interweave'; import * as React from 'react'; -import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; -import { DIMENSION_MARGIN, SYMBOL_MARGIN } from '../../../utils/default'; +import { type ISymbolModel } from '../../../Interfaces/ISymbolModel'; +import { SYMBOL_MARGIN } from '../../../utils/default'; interface ISymbolProps { model: ISymbolModel @@ -12,11 +12,22 @@ export function Symbol(props: ISymbolProps): JSX.Element { const href = props.model.config.Image.Base64Image ?? props.model.config.Image.Url; const hasSVG = props.model.config.Image.Svg !== undefined && props.model.config.Image.Svg !== null; + + let x, y: number; + + if (props.model.isVertical) { + x = -SYMBOL_MARGIN - props.model.width; + y = props.model.offset; + } else { + x = props.model.offset; + y = -SYMBOL_MARGIN - props.model.height; + } + if (hasSVG) { return ( + x={x} + y={y} + height={props.model.height} + width={props.model.width} /> ); } diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index 4b49213..2a99c74 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; -import { ReactSVGPanZoom, type Tool, TOOL_PAN, type Value } from 'react-svg-pan-zoom'; +import { ReactSVGPanZoom, type Tool, TOOL_PAN, type Value, ALIGN_CENTER } from 'react-svg-pan-zoom'; import { Container } from './Elements/Container'; -import { IContainerModel } from '../../Interfaces/IContainerModel'; import { SelectorContainer } from './Elements/SelectorContainer/SelectorContainer'; import { MAX_FRAMERATE } from '../../utils/default'; import { SymbolLayer } from './Elements/SymbolLayer'; -import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; import { DimensionLayer } from './Elements/DimensionLayer'; import { SelectorSymbol } from './Elements/SelectorSymbol/SelectorSymbol'; +import { type IToolbarProps, Toolbar } from './SVGReactPanZoom/ui-toolbar/toolbar'; +import { type DrawParams } from '../Viewer/Viewer'; interface ISVGProps { className?: string @@ -15,12 +15,7 @@ interface ISVGProps { viewerHeight: number width: number height: number - containers: Map - children: IContainerModel - selectedContainer?: IContainerModel - symbols: Map - selectedSymbol?: ISymbolModel - selectorMode: SelectorMode + drawParams: DrawParams selectContainer: (containerId: string) => void } @@ -33,6 +28,14 @@ export enum SelectorMode { export const ID = 'svg'; export function SVG(props: ISVGProps): JSX.Element { + const { + mainContainer, + selectorMode, + selectedContainer, + selectedSymbol, + containers, + symbols + } = props.drawParams; const [tool, setTool] = React.useState(TOOL_PAN); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const [value, setValue] = React.useState({} as Value); @@ -58,27 +61,27 @@ export function SVG(props: ISVGProps): JSX.Element { }; const children: React.ReactNode | React.ReactNode[] = ; function Selector(): JSX.Element { - switch (props.selectorMode) { + switch (selectorMode) { case SelectorMode.Containers: return ; case SelectorMode.Symbols: return ; default: return <>; @@ -109,6 +112,9 @@ export function SVG(props: ISVGProps): JSX.Element { const value = event as Value; setScale(value.a); }} + onDoubleClick={() => { + svgViewer?.current?.setPointOnViewerCenter(props.width / 2, props.height / 2, 0.8); + }} background={'#ffffff'} defaultTool='pan' miniatureProps={{ @@ -117,11 +123,24 @@ export function SVG(props: ISVGProps): JSX.Element { width: 120, height: 120 }} + customToolbar={(props: IToolbarProps) => ( + + )} > {children} - - + + @@ -130,7 +149,8 @@ export function SVG(props: ISVGProps): JSX.Element { } function UseFitOnce(svgViewer: React.RefObject, width: number, height: number): void { - React.useEffect(() => { - svgViewer?.current?.fitToViewer(); + React.useCallback(() => { + // TODO: Fix this + svgViewer?.current?.setPointOnViewerCenter(width / 2, height / 2, 0.8); }, [svgViewer, width, height]); } diff --git a/src/Components/SVG/SVGReactPanZoom/LICENSE b/src/Components/SVG/SVGReactPanZoom/LICENSE new file mode 100644 index 0000000..2f083c0 --- /dev/null +++ b/src/Components/SVG/SVGReactPanZoom/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 https://github.com/chrvadala + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/Components/SVG/SVGReactPanZoom/ui-toolbar/toolbar-button.tsx b/src/Components/SVG/SVGReactPanZoom/ui-toolbar/toolbar-button.tsx new file mode 100644 index 0000000..91fed61 --- /dev/null +++ b/src/Components/SVG/SVGReactPanZoom/ui-toolbar/toolbar-button.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { POSITION_TOP, POSITION_BOTTOM } from 'react-svg-pan-zoom'; + +interface IToolbarButtonProps { + title: string + name: string + toolbarPosition: string + activeColor: string + onClick: (event: React.MouseEvent | React.TouchEvent) => void + active: boolean + children: JSX.Element | JSX.Element[] +} + +interface IToolbarButtonState { + hover: boolean +} + +export class ToolbarButton extends React.Component { + public state: IToolbarButtonState; + + constructor(props: IToolbarButtonProps) { + super(props); + this.state = { hover: false }; + } + + change(event: (React.MouseEvent | React.TouchEvent)): void { + event.preventDefault(); + event.stopPropagation(); + + switch (event.type) { + case 'mouseenter': + case 'touchstart': + this.setState({ hover: true }); + break; + case 'mouseleave': + case 'touchend': + case 'touchcancel': + this.setState({ hover: false }); + break; + default: + // noop + } + } + + render(): JSX.Element { + const style = { + display: 'block', + width: '24px', + height: '24px', + margin: [POSITION_TOP, POSITION_BOTTOM].includes(this.props.toolbarPosition) ? '2px 1px' : '1px 2px', + color: this.props.active || this.state.hover ? this.props.activeColor : '#FFF', + transition: 'color 200ms ease', + background: 'none', + padding: '0px', + border: '0px', + outline: '0px', + cursor: 'pointer' + }; + + return ( + + ); + } +} diff --git a/src/Components/SVG/SVGReactPanZoom/ui-toolbar/toolbar.tsx b/src/Components/SVG/SVGReactPanZoom/ui-toolbar/toolbar.tsx new file mode 100644 index 0000000..8d11038 --- /dev/null +++ b/src/Components/SVG/SVGReactPanZoom/ui-toolbar/toolbar.tsx @@ -0,0 +1,188 @@ +import { + ArrowsPointingOutIcon, + CursorArrowRaysIcon, + HandRaisedIcon, + MagnifyingGlassMinusIcon, + MagnifyingGlassPlusIcon +} from '@heroicons/react/24/outline'; +import React from 'react'; +import { applyToPoint, fromObject, inverse, scale, transform, translate } from 'transformation-matrix'; + +import { + fitToViewer, + POSITION_TOP, + POSITION_BOTTOM, + POSITION_LEFT, + POSITION_RIGHT, + TOOL_NONE, + TOOL_PAN, + TOOL_ZOOM_IN, + TOOL_ZOOM_OUT, ALIGN_LEFT, ALIGN_TOP, + type Value, + type Tool, + type ALIGN_BOTTOM, + type ALIGN_CENTER, + type ALIGN_RIGHT, + type ToolbarPosition +} from 'react-svg-pan-zoom'; +import { ToolbarButton } from './toolbar-button'; + +export interface IToolbarProps { + tool: Tool + value: Value + onChangeValue: (value: Value) => void + onChangeTool: (tool: Tool) => void + activeToolColor?: string + position?: ToolbarPosition | undefined + SVGAlignX?: typeof ALIGN_CENTER | typeof ALIGN_LEFT | typeof ALIGN_RIGHT | undefined + SVGAlignY?: typeof ALIGN_CENTER | typeof ALIGN_TOP | typeof ALIGN_BOTTOM | undefined + fittingScale?: number | undefined +} + +/** + * Change value + * @param value + * @param patch + * @param action + * @returns {Object} + */ +function set(value: Value, patch: object, action = null): Value { + value = Object.assign({}, value, patch, { lastAction: action }); + return Object.freeze(value); +} + +/** + * Export x,y coords relative to SVG + * @param value + * @param viewerX + * @param viewerY + * @returns {*|{x, y}|{x: number, y: number}} + */ +function getSVGPoint(value: Value, viewerX: number, viewerY: number): PointObjectNotation { + const matrix = fromObject(value); + + const inverseMatrix = inverse(matrix); + return applyToPoint(inverseMatrix, { x: viewerX, y: viewerY }); +} + +export function zoom( + value: Value, + SVGPointX: number, + SVGPointY: number, + scaleFactor: number +): Value { + const matrix = transform( + fromObject(value), + translate(SVGPointX, SVGPointY), + scale(scaleFactor, scaleFactor), + translate(-SVGPointX, -SVGPointY) + ); + + return set(value, { + ...matrix + }); +} + +export function Toolbar({ + tool, + value, + onChangeValue, + onChangeTool, + activeToolColor = '#1CA6FC', + position = POSITION_RIGHT, + SVGAlignX = ALIGN_LEFT, + SVGAlignY = ALIGN_TOP, + fittingScale = undefined +}: IToolbarProps): JSX.Element { + function handleChangeTool(event: React.MouseEvent | React.TouchEvent, tool: Tool): void { + onChangeTool(tool); + event.stopPropagation(); + event.preventDefault(); + }; + + function handleFit(event: React.MouseEvent | React.TouchEvent): void { + let fittedValue: Value = fitToViewer(value, SVGAlignX, SVGAlignY); + if (fittingScale !== undefined) { + const { viewerWidth, viewerHeight } = fittedValue; + const SVGPoint = getSVGPoint(value, viewerWidth / 2, viewerHeight / 2); + fittedValue = zoom(fittedValue, SVGPoint.x, SVGPoint.y, fittingScale); + } + + onChangeValue(fittedValue); + event.stopPropagation(); + event.preventDefault(); + }; + + const isHorizontal = [POSITION_TOP, POSITION_BOTTOM].includes(position); + + const style: React.CSSProperties = { + // position + position: 'absolute', + transform: [POSITION_TOP, POSITION_BOTTOM].includes(position) ? 'translate(-50%, 0px)' : 'none', + top: [POSITION_LEFT, POSITION_RIGHT, POSITION_TOP].includes(position) ? '5px' : 'unset', + left: [POSITION_TOP, POSITION_BOTTOM].includes(position) ? '50%' : (POSITION_LEFT === position ? '5px' : 'unset'), + right: [POSITION_RIGHT].includes(position) ? '5px' : 'unset', + bottom: [POSITION_BOTTOM].includes(position) ? '5px' : 'unset', + + // inner styling + backgroundColor: 'rgba(19, 20, 22, 0.90)', + borderRadius: '2px', + display: 'flex', + flexDirection: isHorizontal ? 'row' : 'column', + padding: isHorizontal ? '1px 2px' : '2px 1px' + }; + + return ( +
+ { handleChangeTool(event, TOOL_NONE); } }> + + + + { handleChangeTool(event, TOOL_PAN); } }> + + + + { handleChangeTool(event, TOOL_ZOOM_IN); } }> + + + + { handleChangeTool(event, TOOL_ZOOM_OUT); } }> + + + + { handleFit(event); } }> + + +
+ ); +} diff --git a/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.tsx b/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.tsx index 61a35d9..7d7a082 100644 --- a/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.tsx +++ b/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.tsx @@ -4,16 +4,16 @@ import './ToggleSideBar.scss'; interface IToggleSidebarProps { title: string checked: boolean - onChange: (newValue: boolean) => void + onClick: () => void } -export function ToggleSideBar({ title, checked, onChange }: IToggleSidebarProps): JSX.Element { +export function ToggleSideBar({ title, checked, onClick }: IToggleSidebarProps): JSX.Element { return (