소스 검색

Merge remote-tracking branch 'ulm/small-refactor' into small-refactor

Joeri Exelmans 2 년 전
부모
커밋
c44e59a64e

+ 57 - 62
src/frontend/rountangleEditor/RountangleComponent.tsx

@@ -7,6 +7,7 @@ import {PrimitiveValue} from "../../onion/types";
 export interface RountangleProps extends Rountangle {
     id:       PrimitiveValue;
     dispatch: (action: RountangleAction) => void;
+    zoom:     number;
 }
 
 export interface RountangleState {
@@ -19,8 +20,6 @@ export interface RountangleState {
 }
 
 export class RountangleComponent extends React.Component<RountangleProps, RountangleState> {
-    private readonly rountangleRef: React.RefObject<HTMLDivElement>
-
     shouldComponentUpdate(nextProps: Readonly<RountangleProps>, nextState: Readonly<RountangleState>, nextContext: any): boolean {
         return this.props.posX       !== nextProps.posX
             || this.props.posY       !== nextProps.posY
@@ -35,15 +34,6 @@ export class RountangleComponent extends React.Component<RountangleProps, Rounta
             || this.state.resizeHeight    !== nextState.resizeHeight;
     }
 
-    componentDidMount() {
-        // IMPORTANT: notice the `passive: false` option
-        this.rountangleRef.current?.addEventListener('wheel', this.onWheel, { passive: false });
-    }
-
-    componentWillUnmount() {
-        this.rountangleRef.current?.removeEventListener('wheel', this.onWheel);
-    }
-
     constructor(props: RountangleProps) {
         super(props);
         this.state = {
@@ -54,11 +44,9 @@ export class RountangleComponent extends React.Component<RountangleProps, Rounta
             resizeWidth: 0,
             resizeHeight: 0
         }
-
-        this.rountangleRef = React.createRef<HTMLDivElement>();
     }
 
-    onPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
+    onPointerDown = (event: React.PointerEvent<SVGRectElement>) => {
         // only left mouse button
         if (event.button !== 0) return;
 
@@ -69,19 +57,22 @@ export class RountangleComponent extends React.Component<RountangleProps, Rounta
         event.preventDefault();
     }
 
-    onPointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
+    onPointerMove = (event: React.PointerEvent<SVGRectElement>) => {
         if (!this.state.dragging) return;
-        this.setState({
-            moved: true,
-            movementsX: this.state.movementsX + event.movementX,
-            movementsY: this.state.movementsY + event.movementY
-        });
+
+        if (event.movementX !== 0 || event.movementY !== 0) {
+            this.setState({
+                moved: true,
+                movementsX: this.state.movementsX + event.movementX * this.props.zoom,
+                movementsY: this.state.movementsY + event.movementY * this.props.zoom
+            });
+        }
 
         event.stopPropagation();
         event.preventDefault();
     }
 
-    onPointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
+    onPointerUp = (event: React.PointerEvent<SVGRectElement>) => {
         event.currentTarget.releasePointerCapture(event.pointerId);
         event.stopPropagation();
         event.preventDefault();
@@ -107,17 +98,6 @@ export class RountangleComponent extends React.Component<RountangleProps, Rounta
         }
     }
 
-    onWheel = (event: WheelEvent) => {
-        event.preventDefault();
-        event.stopPropagation();
-
-        this.props.dispatch({
-            tag: 'changeRountangleZ',
-            id: this.props.id,
-            newPosZ: Math.max(this.props.posZ + event.deltaY, 10)
-        });
-    }
-
     onDoubleClick = (event: React.MouseEvent) => {
         const newRountangleName = prompt('Rename', this.props.name);
         if (newRountangleName && newRountangleName !== this.props.name) {
@@ -130,10 +110,12 @@ export class RountangleComponent extends React.Component<RountangleProps, Rounta
     }
 
     onResize = (deltaHeight: number, deltaWidth: number) => {
-        this.setState({
-            resizeWidth:  this.state.resizeWidth  + deltaHeight,
-            resizeHeight: this.state.resizeHeight + deltaWidth
-        })
+        if (deltaHeight !== 0 || deltaWidth !== 0) {
+            this.setState({
+                resizeWidth:  this.state.resizeWidth  + deltaHeight * this.props.zoom,
+                resizeHeight: this.state.resizeHeight + deltaWidth * this.props.zoom
+            });
+        }
     }
 
     onResizeComplete = () => {
@@ -152,32 +134,45 @@ export class RountangleComponent extends React.Component<RountangleProps, Rounta
 
     render() {
         return(
-          <div
-              ref={this.rountangleRef}
-              className={`re-rountangle ${this.state.dragging ? 'dragging' : ''}`}
-              style={{
-                  left:   this.props.posX + this.state.movementsX,
-                  top:    this.props.posY + this.state.movementsY,
-                  zIndex: this.props.posZ,
-                  width:  this.props.width + this.state.resizeWidth,
-                  height: this.props.height + this.state.resizeHeight
-              }}
-              onPointerDown={this.onPointerDown}
-              onPointerMove={this.onPointerMove}
-              onPointerUp={this.onPointerUp}
-              onDoubleClick={this.onDoubleClick}
-          >
-              <div title={this.props.id.toString()} className={'re-rountangle-name'}>
-                  {this.props.name}
-              </div>
-              <RountangleResizeHandleComponent
-                  id={this.props.id}
-                  height={this.props.height + this.state.resizeHeight}
-                  width={this.props.width + this.state.resizeWidth}
-                  onResize={this.onResize}
-                  onResizeComplete={this.onResizeComplete}
-              />
-          </div>
+            <g
+                className={`re-rountangle-wrapper ${this.state.dragging ? 'dragging' : ''}`}
+            >
+                <rect
+                    rx={5}
+                    ry={5}
+                    x={this.props.posX + this.state.movementsX}
+                    y={this.props.posY + this.state.movementsY}
+                    width={this.props.width + this.state.resizeWidth}
+                    height={this.props.height + this.state.resizeHeight}
+                    className={'re-rountangle'}
+                />
+                <foreignObject
+                    x={this.props.posX + this.state.movementsX}
+                    y={this.props.posY + this.state.movementsY}
+                    width={this.props.width + this.state.resizeWidth}
+                    height={this.props.height + this.state.resizeHeight}
+                    onPointerDown={this.onPointerDown}
+                    onPointerMove={this.onPointerMove}
+                    onPointerUp={this.onPointerUp}
+                    onDoubleClick={this.onDoubleClick}
+                >
+                    <div
+                        className={'re-rountangle-name'}
+                        title={this.props.id.toString()}
+                    >
+                        <span>{this.props.name}</span>
+                    </div>
+                </foreignObject>
+                <RountangleResizeHandleComponent
+                    id={this.props.id}
+                    x={this.props.posX + this.state.movementsX}
+                    y={this.props.posY + this.state.movementsY}
+                    height={this.props.height + this.state.resizeHeight}
+                    width={this.props.width + this.state.resizeWidth}
+                    onResize={this.onResize}
+                    onResizeComplete={this.onResizeComplete}
+                />
+            </g>
         );
     }
 }

+ 22 - 20
src/frontend/rountangleEditor/RountangleEditor.css

@@ -1,37 +1,39 @@
 .re-background {
-    /*background: #eee;*/
-    /*height: 100px;*/
     position: relative;
     font-family: sans-serif;
 }
+.re-background.dragging {
+    cursor: move;
+}
+
+.re-rountangle-wrapper {
+    filter: drop-shadow(.1rem .1rem .1em deeppink);
+}
 .re-rountangle {
-    position: absolute;
-    border: 3px solid black;
-    border-radius: 5px;
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-    text-align: center;
+    stroke: black;
+    stroke-width: 3px;
     overflow: hidden;
-    filter: drop-shadow(.1rem .1rem .1em deeppink);
     font-weight: bold;
+    fill: transparent;
 }
-.re-rountangle:hover {
+.re-rountangle-wrapper:hover {
     cursor: grab;
 }
-.re-rountangle:hover.dragging {
+.re-rountangle-wrapper:hover.dragging {
     cursor: grabbing;
 }
 .re-rountangle-name {
-
+    font-weight: 600;
+    text-overflow: ellipsis;
+    text-align: center;
+    height: 100%;
+    max-height: 100%;
+    display: flex;
+    justify-content: center;
+    flex-direction: column;
+    pointer-events: none;
 }
 .re-rountangle-resize-handle {
-    width: 20px;
-    height: 20px;
-    position: absolute;
-    bottom: -10px;
-    right: -10px;
-    background: deeppink;
+    fill: deeppink;
     cursor: nwse-resize;
-    rotate: 45deg;
 }

+ 120 - 18
src/frontend/rountangleEditor/RountangleEditor.tsx

@@ -82,18 +82,48 @@ export interface RountangleEditorProps {
   onUserEdit?: (deltas: PrimitiveDelta[], description: string) => void;
 }
 
-export class RountangleEditor extends React.Component<RountangleEditorProps, {}> {
-    readonly canvasRef: React.RefObject<HTMLDivElement>
+interface RountangleEditorState {
+    baseWidth:     number;
+    currentWidth:  number;
+    baseHeight:    number;
+    currentHeight: number;
+    translateX:    number;
+    translateY:    number;
+    zoom:          number;
+    dragging:      boolean;
+    moved:         boolean;
+}
+
+export class RountangleEditor extends React.Component<RountangleEditorProps, RountangleEditorState> {
+    readonly canvasRef: React.RefObject<SVGSVGElement>
     private onDispatchListeners: Set<(action: RountangleAction) => void>;
 
     constructor(props) {
         super(props);
-        this.canvasRef = React.createRef<HTMLDivElement>();
+        this.canvasRef = React.createRef<SVGSVGElement>();
         this.onDispatchListeners = new Set();
+        this.state = {
+            baseWidth: 350,
+            baseHeight: 350,
+            currentHeight: 350,
+            currentWidth: 350,
+            translateX: 0,
+            translateY: 0,
+            zoom: 1.0,
+            dragging: false,
+            moved: false
+        }
     }
 
     componentDidMount() {
         this.addOnDispatchListener(_ => this.forceUpdate());
+        // IMPORTANT: notice the `passive: false` option
+        this.canvasRef.current?.addEventListener('wheel', this.onWheel, { passive: false });
+
+    }
+
+    componentWillUnmount() {
+        this.canvasRef.current?.removeEventListener('wheel', this.onWheel);
     }
 
 
@@ -175,31 +205,102 @@ export class RountangleEditor extends React.Component<RountangleEditorProps, {}>
         this.onDispatchListeners.add(newListener);
     }
 
-    onPointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
+    onPointerDown = (event: React.PointerEvent<SVGSVGElement>) => {
+        // only left mouse button
+        if (event.button !== 0) return;
         event.stopPropagation();
         event.preventDefault();
+        event.currentTarget.setPointerCapture(event.pointerId);
+
+        if (!event.altKey) {
+            this.setState({dragging: true, moved: false});
+        }
+    }
+
+    onPointerMove = (event: React.PointerEvent<SVGSVGElement>) => {
+        if (!this.state.dragging) return;
+
+        if (event.movementY !== 0 ||event.movementX !== 0) {
+            this.setState({
+                translateX: this.state.translateX - event.movementX * this.state.zoom,
+                translateY: this.state.translateY - event.movementY * this.state.zoom,
+                moved:      true
+            });
+        }
+
+        event.stopPropagation();
+        event.preventDefault();
+    }
+
+    private clickToSVGPos = (x:number, y: number): DOMPoint | undefined => {
+        // point transformation adapted from https://stackoverflow.com/a/70595400
+        if (!this.canvasRef.current) return undefined;
+
+        const screenCTM = this.canvasRef.current.getScreenCTM();
+        if (screenCTM === null) return undefined;
+
+        const refPoint = new DOMPoint(x, y);
+        return refPoint.matrixTransform(screenCTM.inverse());
+    }
+
+    onPointerUp = (event: React.PointerEvent<SVGSVGElement>) => {
+        event.stopPropagation();
+        event.preventDefault();
+        event.currentTarget.releasePointerCapture(event.pointerId);
+
+        this.setState({
+            dragging: false,
+            moved:    false
+        });
+
         // add new state on left mouse button and ALT-Key pressed
         if (event.button === 0 && event.altKey) {
-            const newRountangleName = prompt('Name', 'New Rountangle');
-            if (newRountangleName && this.canvasRef.current) {
-                this.dispatch({
-                    tag: 'createRountangle',
-                    id: this.props.generateUUID().value.toString(),
-                    posX: event.pageX - this.canvasRef.current.offsetLeft,
-                    posY: event.pageY - this.canvasRef.current.offsetTop,
-                    posZ: 10,
-                    width: 100,
-                    height: 66,
-                    name: newRountangleName});
+            const cursorPoint = this.clickToSVGPos(event.clientX, event.clientY);
+
+            if (cursorPoint) {
+                const newRountangleName = prompt('Name', 'New Rountangle');
+                if (newRountangleName) {
+                    this.dispatch({
+                        tag: 'createRountangle',
+                        id: this.props.generateUUID().value.toString(),
+                        posX: cursorPoint.x,
+                        posY: cursorPoint.y,
+                        posZ: 10,
+                        width: 100,
+                        height: 66,
+                        name: newRountangleName});
+                }
             }
         }
     }
 
+    onWheel = (event: WheelEvent) => {
+        event.preventDefault();
+        event.stopPropagation();
+        if (event.deltaY === 0) return;
+        if(!this.canvasRef.current) return;
+
+        const newZoom = event.deltaY > 0 ? Math.min(this.state.zoom * 1.1, 3.0) : Math.max(this.state.zoom * 0.9, 0.1);
+
+        this.setState({
+            zoom: newZoom,
+            translateX: this.state.translateX + (this.state.baseWidth * this.state.zoom - this.state.baseWidth * newZoom)/2,
+            translateY: this.state.translateY + (this.state.baseHeight * this.state.zoom - this.state.baseHeight * newZoom)/2,
+            currentWidth: this.state.baseWidth * newZoom,
+            currentHeight: this.state.baseHeight * newZoom
+        })
+    }
+
     render() {
         return(
-            <div
-                   className="re-background canvas"
+            <svg
+                   className={`re-background canvas ${this.state.dragging ? 'dragging' : ''}`}
+                   onPointerDown={this.onPointerDown}
+                   onPointerMove={this.onPointerMove}
                    onPointerUp={this.onPointerUp}
+                   width='350px'
+                   height='100%'
+                   viewBox={`${this.state.translateX} ${this.state.translateY} ${this.state.currentWidth} ${this.state.currentHeight}`}
                    ref={this.canvasRef}
                >
                    {
@@ -217,11 +318,12 @@ export class RountangleEditor extends React.Component<RountangleEditorProps, {}>
                                 posZ={rountangle.posZ}
                                 width={rountangle.width}
                                 height={rountangle.height}
+                                zoom={this.state.zoom}
                                 dispatch={this.dispatch}
                             />
                        })
                    }
-            </div>
+            </svg>
         )
     }
 }

+ 19 - 7
src/frontend/rountangleEditor/RountangleResizeHandleComponent.tsx

@@ -4,6 +4,8 @@ import {PrimitiveValue} from "../../onion/types";
 
 interface RountangleResizeHandleProps {
     id:               PrimitiveValue;
+    x:                number;
+    y:                number;
     width:            number;
     height:           number;
     onResize:         (deltaX: number, deltaY: number) => void;
@@ -18,7 +20,9 @@ export interface RountangleResizeHandleState {
 export class RountangleResizeHandleComponent extends React.Component<RountangleResizeHandleProps, RountangleResizeHandleState> {
     shouldComponentUpdate(nextProps: Readonly<RountangleResizeHandleProps>, nextState: Readonly<RountangleState>, nextContext: any): boolean {
         return this.props.width !== nextProps.width
-            || this.props.height !== nextProps.height;
+            || this.props.height !== nextProps.height
+            || this.props.x !== nextProps.x
+            || this.props.y !== nextProps.y
     }
 
     constructor(props: RountangleResizeHandleProps) {
@@ -29,7 +33,7 @@ export class RountangleResizeHandleComponent extends React.Component<RountangleR
         }
     }
 
-    onPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
+    onPointerDown = (event: React.PointerEvent<SVGSVGElement>) => {
         // only left mouse button
         if (event.button !== 0) return;
 
@@ -44,7 +48,7 @@ export class RountangleResizeHandleComponent extends React.Component<RountangleR
         event.preventDefault();
     }
 
-    onPointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
+    onPointerMove = (event: React.PointerEvent<SVGSVGElement>) => {
         if (!this.state.dragging) return;
 
         this.props.onResize(event.movementX, event.movementY);
@@ -53,7 +57,7 @@ export class RountangleResizeHandleComponent extends React.Component<RountangleR
         event.preventDefault();
     }
 
-    onPointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
+    onPointerUp = (event: React.PointerEvent<SVGSVGElement>) => {
         event.currentTarget.releasePointerCapture(event.pointerId);
         event.stopPropagation();
         event.preventDefault();
@@ -68,11 +72,19 @@ export class RountangleResizeHandleComponent extends React.Component<RountangleR
     }
 
     render() {
-        return <div
-            className={'re-rountangle-resize-handle'}
+        return <svg
+            x={this.props.x + this.props.width - 16.5}
+            y={this.props.y + this.props.height - 16.5}
+            width={15}
+            height={15}
+            className="re-rountangle-resize-handle"
             onPointerDown={this.onPointerDown}
             onPointerMove={this.onPointerMove}
             onPointerUp={this.onPointerUp}
-        />
+        >
+            <polygon
+                points={"15,0 15,15, 0,15"}
+            />
+        </svg>
     }
 }