Merge branch 'master' into master.7306.symbolY

# Conflicts:
#	src/Components/Canvas/Symbol.ts
#	src/Components/SVG/Elements/DimensionLayer.tsx
#	src/Components/SVG/Elements/SelectorSymbol/SelectorSymbol.tsx
This commit is contained in:
Carl Fuchs 2023-02-20 10:21:54 +01:00
commit d72c2266f3
52 changed files with 1967 additions and 913 deletions

View file

@ -1,100 +0,0 @@
import * as React from 'react';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { DIMENSION_MARGIN } from '../../../utils/default';
import { GetAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
import { TransformX } from '../../../utils/svg';
import { Dimension } from './Dimension';
interface IDimensionLayerProps {
containers: Map<string, IContainerModel>
roots: IContainerModel | IContainerModel[] | null
scale?: number
}
function GetDimensionsNodes(
containers: Map<string, IContainerModel>,
root: IContainerModel,
scale: number
): React.ReactNode[] {
const it = MakeBFSIterator(root, containers);
const dimensions: React.ReactNode[] = [];
let currentDepth = 0;
let min = Infinity;
let max = -Infinity;
let lastY = 0;
for (const { container, depth } of it) {
if (currentDepth !== depth) {
AddNewDimension(currentDepth, min, max, lastY, scale, '#000000', dimensions);
currentDepth = depth;
min = Infinity;
max = -Infinity;
}
const absoluteX = GetAbsolutePosition(containers, container)[0];
const x = TransformX(absoluteX, container.properties.width, container.properties.positionReference);
lastY = container.properties.y + container.properties.height;
if (x < min) {
min = x;
}
if (x > max) {
max = x;
}
}
AddNewDimension(currentDepth, min, max, lastY, scale, '#000000', dimensions);
return dimensions;
}
/**
* A layer containing all dimension
* @param props
* @returns
*/
export function DepthDimensionLayer(props: IDimensionLayerProps): JSX.Element {
let dimensions: React.ReactNode[] = [];
const scale = props.scale ?? 1;
if (Array.isArray(props.roots)) {
props.roots.forEach(child => {
dimensions.concat(GetDimensionsNodes(props.containers, child, scale));
});
} else if (props.roots !== null) {
dimensions = GetDimensionsNodes(props.containers, props.roots, scale);
}
return (
<g>
{dimensions}
</g>
);
}
function AddNewDimension(currentDepth: number, min: number, max: number, lastY: number, scale: number, color: string, dimensions: React.ReactNode[]): void {
const id = `dim-depth-${currentDepth}`;
const xStart = min;
const xEnd = max;
const y = lastY + (DIMENSION_MARGIN * (currentDepth + 1)) / scale;
const width = xEnd - xStart;
const text = width
.toFixed(0)
.toString();
if (width === 0) {
return;
}
dimensions.push(
<Dimension
key={id}
id={id}
xStart={xStart}
yStart={y}
xEnd={xEnd}
yEnd={y}
text={text}
scale={scale}
color={color}
/>
);
}

View file

@ -1,6 +1,9 @@
import * as React from 'react';
import { type IDimensionOptions } from '../../../Interfaces/IDimensionOptions';
import { NOTCHES_LENGTH } from '../../../utils/default';
export type IDimensionStyle = Omit<IDimensionOptions, 'positions'>;
interface IDimensionProps {
id: string
xStart: number
@ -8,7 +11,7 @@ interface IDimensionProps {
xEnd: number
yEnd: number
text: string
color: string
style: IDimensionStyle
scale?: number
}
@ -28,8 +31,9 @@ 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.color,
strokeWidth: 2 / scale
stroke: props.style.color,
strokeWidth: (props.style.width ?? 2) / scale,
strokeDasharray: props.style.dashArray
};
/// We need to find the points of the notches
@ -79,9 +83,11 @@ export function Dimension(props: IDimensionProps): JSX.Element {
x2={endBottomX}
y2={endBottomY}
style={style}/>
<text textAnchor={'middle'} alignmentBaseline={'central'}
<text
x={textX}
y={textY}
textAnchor={'middle'}
alignmentBaseline={'central'}
style={{
transform: `rotate(${rotation}turn) scale(${1 / scale})`,
transformOrigin: `${textX}px ${textY}px`

View file

@ -1,7 +1,6 @@
import * as React from 'react';
import { Orientation } from '../../../Enums/Orientation';
import { Position } from '../../../Enums/Position';
import { type IContainerModel } from '../../../Interfaces/IContainerModel';
import {
DIMENSION_MARGIN,
SHOW_BORROWER_DIMENSIONS,
@ -11,7 +10,8 @@ import {
} from '../../../utils/default';
import { FindContainerById, MakeRecursionDFSIterator, Pairwise } from '../../../utils/itertools';
import { TransformX, TransformY } from '../../../utils/svg';
import { Dimension } from './Dimension';
import { Dimension, type IDimensionStyle } from './Dimension';
import { type IContainerModel } from '../../../Interfaces/IContainerModel';
import { type ISymbolModel } from '../../../Interfaces/ISymbolModel';
interface IDimensionLayerProps {
@ -252,10 +252,10 @@ function AddHorizontalChildrenDimension(
container: IContainerModel,
currentTransform: [number, number],
dimensions: React.ReactNode[],
scale: number,
color: string
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);
@ -305,7 +305,7 @@ function AddHorizontalChildrenDimension(
yEnd={yDim}
text={textChildren}
scale={scale}
color={color}/>);
style={style}/>);
}
function AddVerticalChildrenDimension(
@ -315,10 +315,10 @@ function AddVerticalChildrenDimension(
container: IContainerModel,
currentTransform: [number, number],
dimensions: React.ReactNode[],
scale: number,
color: string
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);
@ -373,7 +373,7 @@ function AddVerticalChildrenDimension(
yEnd={yChildrenEnd + offset}
text={textChildren}
scale={scale}
color={color}
style={style}
/>);
}
@ -384,9 +384,9 @@ function AddHorizontalBorrowerDimension(
depth: number,
currentTransform: [number, number],
dimensions: React.ReactNode[],
scale: number,
color: string
scale: number
): void {
const style = container.properties.dimensionOptions.dimensionWithMarks;
const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform);
const marks = []; // list of vertical lines for the dimension
for (const {
@ -431,7 +431,7 @@ function AddHorizontalBorrowerDimension(
yEnd={yDim}
text={value.toFixed(0)}
scale={scale}
color={color}/>);
style={style}/>);
count++;
}
}
@ -444,9 +444,9 @@ function AddVerticalBorrowerDimension(
depth: number,
currentTransform: [number, number],
dimensions: React.ReactNode[],
scale: number,
color: string
scale: number
): void {
const style = container.properties.dimensionOptions.dimensionWithMarks;
const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform);
const marks = []; // list of vertical lines for the dimension
for (const {
@ -496,7 +496,7 @@ function AddVerticalBorrowerDimension(
yEnd={next}
text={value.toFixed(0)}
scale={scale}
color={color}/>);
style={style}/>);
count++;
}
}
@ -507,9 +507,9 @@ function AddVerticalSelfDimension(
container: IContainerModel,
currentTransform: [number, number],
dimensions: React.ReactNode[],
scale: number,
color: string
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;
@ -532,7 +532,7 @@ function AddVerticalSelfDimension(
yEnd={yEnd}
text={textVert}
scale={scale}
color={color}/>
style={style}/>
);
}
@ -541,9 +541,9 @@ function AddHorizontalSelfDimension(
container: IContainerModel,
currentTransform: [number, number],
dimensions: React.ReactNode[],
scale: number,
color: string
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];
@ -561,7 +561,7 @@ function AddHorizontalSelfDimension(
yEnd={yDim}
text={text}
scale={scale}
color={color}/>
style={style}/>
);
}
@ -570,9 +570,9 @@ function AddHorizontalSelfMarginsDimension(
container: IContainerModel,
currentTransform: [number, number],
dimensions: React.ReactNode[],
scale: number,
color: string
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}`;
@ -591,7 +591,7 @@ function AddHorizontalSelfMarginsDimension(
yEnd={yDim}
text={text}
scale={scale}
color={color}/>
style={style}/>
);
}
@ -613,7 +613,7 @@ function AddHorizontalSelfMarginsDimension(
yEnd={yDim}
text={text}
scale={scale}
color={color}/>
style={style}/>
);
}
}
@ -624,9 +624,9 @@ function AddVerticalSelfMarginDimension(
container: IContainerModel,
currentTransform: [number, number],
dimensions: React.ReactNode[],
scale: number,
color: string
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}`;
@ -650,7 +650,7 @@ function AddVerticalSelfMarginDimension(
yEnd={yEnd}
text={textVert}
scale={scale}
color={color}/>
style={style}/>
);
}
const bottom = container.properties.margin.bottom;
@ -676,7 +676,44 @@ function AddVerticalSelfMarginDimension(
yEnd={yEnd}
text={textVert}
scale={scale}
color={color}/>
style={style}/>
);
}
}
function AddHorizontalSymbolDimension(
symbol: ISymbolModel,
dimensions: React.ReactNode[],
scale: number,
depth: number
): void {
const width = symbol.x + (symbol.width / 2);
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();
// TODO: Put this in default.ts
const defaultDimensionSymbolStyle: IDimensionStyle = {
color: 'black'
};
dimensions.push(
<Dimension
key={id}
id={id}
xStart={0}
yStart={-offset}
xEnd={width}
yEnd={-offset}
text={text}
scale={scale}
style={defaultDimensionSymbolStyle}/>
);
}

View file

@ -0,0 +1,54 @@
import '../Selector.scss';
import * as React from 'react';
import { SHOW_SELECTOR_TEXT } from '../../../../utils/default';
interface ISelectorProps {
text: string
x: number
y: number
width: number
height: number
scale: number
style?: React.CSSProperties
}
export function Selector({ text, x, y, width, height, scale, style: overrideStyle }: ISelectorProps): JSX.Element {
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',
...overrideStyle
};
return (
<>
<rect
x={x}
y={y}
width={width}
height={height}
style={style}
>
</rect>
{SHOW_SELECTOR_TEXT
? <text
x={xText}
y={yText}
style={{
transform: `scale(${1 / scale}) translateX(-50%)`,
transformBox: 'fill-box'
}}
>
{ text }
</text>
: null}
</>
);
}

View file

@ -1,9 +1,9 @@
import '../Selector.scss';
import * as React from 'react';
import { type IContainerModel } from '../../../../Interfaces/IContainerModel';
import { SHOW_SELECTOR_TEXT } from '../../../../utils/default';
import { GetAbsolutePosition } from '../../../../utils/itertools';
import { RemoveMargin } from '../../../../utils/svg';
import { Selector } from '../Selector/Selector';
interface ISelectorContainerProps {
containers: Map<string, IContainerModel>
@ -33,41 +33,14 @@ export function SelectorContainer(props: ISelectorContainerProps): JSX.Element {
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'
};
return (
<>
<rect
x={x}
y={y}
width={width}
height={height}
style={style}
>
</rect>
{SHOW_SELECTOR_TEXT
? <text
x={xText}
y={yText}
style={{
transform: `scale(${1 / scale}) translateX(-50%)`,
transformBox: 'fill-box'
}}
>
{props.selected.properties.displayedText}
</text>
: null}
</>
<Selector
text={props.selected.properties.displayedText}
x={x}
y={y}
width={width}
height={height}
scale={scale}
/>
);
}

View file

@ -1,7 +1,8 @@
import '../Selector.scss';
import * as React from 'react';
import { SHOW_SELECTOR_TEXT, SYMBOL_MARGIN } from '../../../../utils/default';
import { SYMBOL_MARGIN } from '../../../../utils/default';
import { type ISymbolModel } from '../../../../Interfaces/ISymbolModel';
import { Selector } from '../Selector/Selector';
interface ISelectorSymbolProps {
symbols: Map<string, ISymbolModel>
@ -19,7 +20,7 @@ export function SelectorSymbol(props: ISelectorSymbolProps): JSX.Element {
const scale = (props.scale ?? 1);
const [width, height] = [
props.selected.width,
props.selected.width / scale,
props.selected.height / scale
];
@ -29,45 +30,28 @@ export function SelectorSymbol(props: ISelectorSymbolProps): JSX.Element {
x = -SYMBOL_MARGIN;
y = props.selected.offset;
} else {
x = props.selected.offset;
y = -SYMBOL_MARGIN - height;
[x,y] = [
props.selected.offset + props.selected.width / 2,
-SYMBOL_MARGIN - height]
}
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'
transform: 'translateX(-50%)',
transformBox: 'fill-box'
};
return (
<>
<rect
x={x}
y={y}
width={width}
height={height}
style={style}
>
</rect>
{SHOW_SELECTOR_TEXT
? <text
x={xText}
y={yText}
style={{
transform: `scale(${1 / scale}) translateX(-50%)`,
transformBox: 'fill-box'
}}
>
{props.selected.displayedText}
</text>
: null}
</>
<Selector
text={props.selected.displayedText}
x={x}
y={y}
width={width}
height={height}
scale={scale}
style={style}
/>
);
}

View file

@ -1,14 +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 { type IContainerModel } from '../../Interfaces/IContainerModel';
import { SelectorContainer } from './Elements/SelectorContainer/SelectorContainer';
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer';
import { MAX_FRAMERATE, SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
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
@ -16,19 +15,27 @@ interface ISVGProps {
viewerHeight: number
width: number
height: number
containers: Map<string, IContainerModel>
children: IContainerModel
selectedContainer?: IContainerModel
symbols: Map<string, ISymbolModel>
selectedSymbol?: ISymbolModel
drawParams: DrawParams
selectContainer: (containerId: string) => void
isComponentsOpen: boolean
isSymbolsOpen: boolean
}
export enum SelectorMode {
Nothing,
Containers,
Symbols
}
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>(TOOL_PAN);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const [value, setValue] = React.useState<Value>({} as Value);
@ -54,14 +61,33 @@ export function SVG(props: ISVGProps): JSX.Element {
};
const children: React.ReactNode | React.ReactNode[] = <Container
key={`container-${props.children.properties.id}`}
containers={props.containers}
model={props.children}
key={`container-${mainContainer.properties.id}`}
containers={containers}
model={mainContainer}
depth={0}
scale={scale}
selectContainer={props.selectContainer}
/>;
function Selector(): JSX.Element {
switch (selectorMode) {
case SelectorMode.Containers:
return <SelectorContainer
containers={containers}
scale={scale}
selected={selectedContainer}
/>;
case SelectorMode.Symbols:
return <SelectorSymbol
symbols={symbols}
scale={scale}
selected={selectedSymbol}
/>;
default:
return <></>;
}
}
return (
<div id={ID} className={props.className}>
<ReactSVGPanZoom
@ -86,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={{
@ -94,17 +123,25 @@ export function SVG(props: ISVGProps): JSX.Element {
width: 120,
height: 120
}}
customToolbar={(props: IToolbarProps) => (
<Toolbar
{...props}
SVGAlignX={ALIGN_CENTER}
SVGAlignY={ALIGN_CENTER}
fittingScale={0.8}
/>
)}
>
<svg {...properties}>
{children}
{SHOW_DIMENSIONS_PER_DEPTH
? <DepthDimensionLayer containers={props.containers} scale={scale} roots={props.children} />
: null}
<DimensionLayer containers={props.containers} symbols={props.symbols} scale={scale} root={props.children} />
<SymbolLayer scale={scale} symbols={props.symbols} />
{/* leave this at the end so it can be removed during the svg export */}
{ props.isComponentsOpen ? <SelectorContainer containers={props.containers} scale={scale} selected={props.selectedContainer} /> : null }
{ props.isSymbolsOpen ? <SelectorSymbol symbols={props.symbols} scale={scale} selected={props.selectedSymbol} /> : null }
<DimensionLayer
containers={containers}
symbols={symbols}
scale={scale}
root={mainContainer}
/>
<SymbolLayer scale={scale} symbols={symbols} />
<Selector />
</svg>
</ReactSVGPanZoom>
</div>
@ -112,7 +149,8 @@ export function SVG(props: ISVGProps): JSX.Element {
}
function UseFitOnce(svgViewer: React.RefObject<ReactSVGPanZoom>, 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]);
}

View file

@ -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.

View file

@ -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<IToolbarButtonProps, IToolbarButtonState> {
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 (
<button
onMouseEnter={e => { this.change(e); }}
onMouseLeave={e => { this.change(e); }}
onTouchStart={e => {
this.change(e);
this.props.onClick(e);
}}
onTouchEnd={e => { this.change(e); }}
onTouchCancel={e => { this.change(e); }}
onClick={this.props.onClick}
style={style}
title={this.props.title}
name={this.props.name}
type="button"
>{this.props.children}</button>
);
}
}

View file

@ -0,0 +1,164 @@
import {
ArrowsPointingOutIcon,
CursorArrowRaysIcon,
HandRaisedIcon,
MagnifyingGlassMinusIcon,
MagnifyingGlassPlusIcon
} from '@heroicons/react/24/outline';
import React from 'react';
import { fromObject, 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 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 matrix = transform(
fromObject(fittedValue),
translate(viewerWidth, viewerHeight),
scale(fittingScale, fittingScale),
translate(-viewerWidth, -viewerHeight)
);
fittedValue = set(fittedValue, {
...matrix
});
}
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 (
<div style={style} role="toolbar">
<ToolbarButton
toolbarPosition={position}
active={tool === TOOL_NONE}
activeColor={activeToolColor}
name="unselect-tools"
title="Selection"
onClick={ (event: React.MouseEvent | React.TouchEvent) => { handleChangeTool(event, TOOL_NONE); } }>
<CursorArrowRaysIcon/>
</ToolbarButton>
<ToolbarButton
toolbarPosition={position}
active={tool === TOOL_PAN}
activeColor={activeToolColor}
name="select-tool-pan"
title="Pan"
onClick={ (event: React.MouseEvent | React.TouchEvent) => { handleChangeTool(event, TOOL_PAN); } }>
<HandRaisedIcon/>
</ToolbarButton>
<ToolbarButton
toolbarPosition={position}
active={tool === TOOL_ZOOM_IN}
activeColor={activeToolColor}
name="select-tool-zoom-in"
title="Zoom in"
onClick={ (event: React.MouseEvent | React.TouchEvent) => { handleChangeTool(event, TOOL_ZOOM_IN); } }>
<MagnifyingGlassPlusIcon/>
</ToolbarButton>
<ToolbarButton
toolbarPosition={position}
active={tool === TOOL_ZOOM_OUT}
activeColor={activeToolColor}
name="select-tool-zoom-out"
title="Zoom out"
onClick={ (event: React.MouseEvent | React.TouchEvent) => { handleChangeTool(event, TOOL_ZOOM_OUT); } }>
<MagnifyingGlassMinusIcon/>
</ToolbarButton>
<ToolbarButton
toolbarPosition={position}
active={false}
activeColor={activeToolColor}
name="fit-to-viewer"
title="Fit to viewer"
onClick={ (event: React.MouseEvent | React.TouchEvent) => { handleFit(event); } }>
<ArrowsPointingOutIcon/>
</ToolbarButton>
</div>
);
}