Browse Source

Merge pull request #4 from luna215/canvas

Added UI LineGraph
Alexander Rose 6 years ago
parent
commit
1b6d9ccb3f

+ 5 - 5
src/mol-geo/geometry/direct-volume/direct-volume.ts

@@ -8,9 +8,9 @@ import { ValueCell } from 'mol-util'
 import { Sphere3D, Box3D } from 'mol-math/geometry'
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { DirectVolumeValues } from 'mol-gl/renderable/direct-volume';
-import { Vec3, Mat4 } from 'mol-math/linear-algebra';
+import { Vec3, Mat4, Vec2 } from 'mol-math/linear-algebra';
 import { Box } from '../../primitive/box';
-import { getControlPointsFromString, createTransferFunctionTexture } from './transfer-function';
+import { createTransferFunctionTexture, getControlPointsFromVec2Array } from './transfer-function';
 import { Texture } from 'mol-gl/webgl/texture';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { TransformData } from '../transform-data';
@@ -72,7 +72,7 @@ export namespace DirectVolume {
         ...Geometry.Params,
         isoValue: PD.Numeric(0.22, { min: -1, max: 1, step: 0.01 }),
         renderMode: PD.Select('isosurface', RenderModeOptions),
-        controlPoints: PD.Text('0.19:0.1, 0.2:0.5, 0.21:0.1, 0.4:0.3'),
+        controlPoints: PD.LineGraph([Vec2.create(0.19, 0.1), Vec2.create(0.2, 0.5), Vec2.create(0.21, 0.1), Vec2.create(0.4, 0.3)]),
     }
     export type Params = typeof Params
 
@@ -93,7 +93,7 @@ export namespace DirectVolume {
             transform.aTransform.ref.value, transform.instanceCount.ref.value
         )
 
-        const controlPoints = getControlPointsFromString(props.controlPoints)
+        const controlPoints = getControlPointsFromVec2Array(props.controlPoints)
         const transferTex = createTransferFunctionTexture(controlPoints)
 
         const maxSteps = Math.ceil(Vec3.magnitude(gridDimension.ref.value)) * 2
@@ -130,7 +130,7 @@ export namespace DirectVolume {
         ValueCell.updateIfChanged(values.dUseFog, props.useFog)
         ValueCell.updateIfChanged(values.dRenderMode, props.renderMode)
 
-        const controlPoints = getControlPointsFromString(props.controlPoints)
+        const controlPoints = getControlPointsFromVec2Array(props.controlPoints)
         createTransferFunctionTexture(controlPoints, values.tTransferTex)
     }
 

+ 6 - 0
src/mol-geo/geometry/direct-volume/transfer-function.ts

@@ -9,6 +9,7 @@ import { spline } from 'mol-math/interpolate';
 import { ColorScale } from 'mol-util/color';
 import { ColorMatplotlib } from 'mol-util/color/tables';
 import { ValueCell } from 'mol-util';
+import { Vec2 } from 'mol-math/linear-algebra';
 
 export interface ControlPoint { x: number, alpha: number }
 
@@ -18,6 +19,11 @@ export function getControlPointsFromString(s: string): ControlPoint[] {
         return { x: parseFloat(ps[0]), alpha: parseFloat(ps[1]) }
     })
 }
+
+export function getControlPointsFromVec2Array(array: Vec2[]): ControlPoint[] {
+    return array.map(v => ({ x: v[0], alpha: v[1] }))
+}
+
 // TODO move core function to mol-canvas3d/color
 export function createTransferFunctionTexture(controlPoints: ControlPoint[], texture?: ValueCell<TextureImage<Uint8Array>>): ValueCell<TextureImage<Uint8Array>> {
     const cp = [

+ 64 - 0
src/mol-plugin/skin/base/components/line-graph.scss

@@ -0,0 +1,64 @@
+.msp-canvas {
+    width: 100%;
+    height: 100%;
+    background-color: #f3f2ee;
+    
+    text {
+        -webkit-touch-callout: none; /* iOS Safari */
+        -webkit-user-select: none; /* Safari */
+        -khtml-user-select: none; /* Konqueror HTML */
+        -moz-user-select: none; /* Firefox */
+        -ms-user-select: none; /* Internet Explorer/Edge */
+        user-select: none; /* Non-prefixed version, currently
+                              supported by Chrome and Opera */
+    }
+
+    circle {
+        stroke: black;
+        stroke-width: 10;
+        stroke-opacity: .3;
+
+        &:hover {
+            fill: #ae5d04;
+            stroke: black;
+            stroke-width: 10px;
+        }
+    }
+
+    .info {
+        fill: white;
+        stroke: black;
+        stroke-width: 3;
+    }
+
+    .show {
+        visibility: visible;
+    }
+    .hide {
+        visibility: hidden;
+    }
+
+    .delete-button {
+        rect {
+            fill: #ED4337;
+            stroke: black;
+        }
+        
+        text {
+            stroke: white;
+            fill: white;
+        }
+
+        &:hover {
+            stroke: black;
+            stroke-width: 3;
+            fill: #ff6961;
+        }
+    }
+
+    .infoCircle {
+        &:hover {
+            fill: #4c66b2;
+        }
+    }
+}

+ 2 - 1
src/mol-plugin/skin/base/ui.scss

@@ -40,4 +40,5 @@
 @import 'components/transformer';
 @import 'components/toast';
 @import 'components/help';
-@import 'components/temp';
+@import 'components/temp';
+@import 'components/line-graph.scss';

+ 354 - 0
src/mol-plugin/ui/controls/line-graph/line-graph-component.tsx

@@ -0,0 +1,354 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Paul Luna <paulluna0215@gmail.com>
+ */
+import PointComponent from './point-component';
+
+import * as React from 'react';
+import { Vec2 } from 'mol-math/linear-algebra';
+
+interface LineGraphComponentState {
+    points: Vec2[],
+    selected?: number,
+    copyPoint: any,
+    updatedX: number, 
+    updatedY: number,
+}
+
+export default class LineGraphComponent extends React.Component<any, LineGraphComponentState> {
+    private myRef:any;
+    private height: number;
+    private width: number;
+    private padding: number;
+    
+    constructor(props: any) {
+        super(props);
+        this.myRef = React.createRef();
+        this.state = {
+            points:[
+                Vec2.create(0, 0),
+                Vec2.create(1, 0)
+            ],
+            selected: undefined,
+            copyPoint: undefined,
+            updatedX: 0,
+            updatedY: 0,
+        };
+        this.height = 400;
+        this.width = 600;
+        this.padding = 70;
+        
+        for (const point of this.props.data){
+            this.state.points.push(point);
+        }
+        
+        this.state.points.sort((a, b) => { 
+            if(a[0] === b[0]){
+                if(a[0] === 0){
+                    return a[1]-b[1];
+                }
+                if(a[1] === 1){
+                    return b[1]-a[1];
+                }
+                return a[1]-b[1];
+            }
+            return a[0] - b[0];
+        });
+
+        this.handleDrag = this.handleDrag.bind(this);
+        this.handleDoubleClick = this.handleDoubleClick.bind(this);
+        this.refCallBack = this.refCallBack.bind(this);
+        this.handlePointUpdate = this.handlePointUpdate.bind(this);
+        this.change = this.change.bind(this);
+    }
+
+    public render() {
+        const points = this.renderPoints();
+        const ghostPoint = this.state.copyPoint;
+        return ([
+            <div key="LineGraph">                
+                <svg
+                    className="msp-canvas"
+                    ref={this.refCallBack} 
+                    viewBox={`0 0 ${this.width+this.padding} ${this.height+this.padding}`}
+                    onMouseMove={this.handleDrag} 
+                    onMouseUp={this.handlePointUpdate}
+                    onDoubleClick={this.handleDoubleClick}>  
+            
+                    <g stroke="black" fill="black">
+                        <Poly 
+                            data={this.state.points} 
+                            k={0.5}
+                            height={this.height}
+                            width={this.width}
+                            padding={this.padding}/>
+                        {points}
+                        {ghostPoint}
+                    </g>
+
+                     <defs>
+                        <linearGradient id="Gradient">
+                            <stop offset="0%" stopColor="#d30000"/>
+                            <stop offset="30%" stopColor="#ffff05"/>
+                            <stop offset="50%" stopColor="#05ff05"/>
+                            <stop offset="70%" stopColor="#05ffff"/>
+                            <stop offset="100%" stopColor="#041ae0"/>
+                        </linearGradient>
+                    </defs>
+                    
+                </svg>
+            </div>,
+            <div key="modal" id="modal-root" />
+        ]);
+    }
+
+    private change(points: Vec2[]){
+        let copyPoints = points.slice();
+        copyPoints.shift();
+        copyPoints.pop();
+        this.props.onChange(copyPoints);    
+    }
+
+    private handleMouseDown = (id:number) => (event: any) => {
+        if(id === 0 || id === this.state.points.length-1){
+            return;
+        } 
+        const copyPoint: Vec2 = this.normalizePoint(Vec2.create(this.state.points[id][0], this.state.points[id][1]));
+        this.setState({
+            selected: id,
+            copyPoint: "ready",
+            updatedX: copyPoint[0],
+            updatedY: copyPoint[1],
+        });
+
+        event.preventDefault();
+    }
+
+    private handleDrag(event: any) {
+        if(this.state.copyPoint === undefined){
+            return
+        }
+        const pt = this.myRef.createSVGPoint();
+        let updatedCopyPoint;
+        const padding = this.padding/2;
+        pt.x = event.clientX;
+        pt.y = event.clientY;
+        const svgP = pt.matrixTransform(this.myRef.getScreenCTM().inverse());
+
+        if( svgP.x < (padding) || 
+            svgP.x > (this.width+(padding)) || 
+            svgP.y > (this.height+(padding)) || 
+            svgP.y < (padding)) {
+            return;
+        }
+        updatedCopyPoint = Vec2.create(svgP.x, svgP.y);
+        this.setState({
+            updatedX: updatedCopyPoint[0],
+            updatedY: updatedCopyPoint[1],
+        });
+        const unNormalizePoint = this.unNormalizePoint(updatedCopyPoint);
+        this.setState({
+            copyPoint: <PointComponent 
+                            selected={false}
+                            key="copy" 
+                            x={updatedCopyPoint[0]} 
+                            y={updatedCopyPoint[1]} 
+                            nX={unNormalizePoint[0]} 
+                            nY={unNormalizePoint[1]}
+                            delete={this.deletePoint}
+                            onmouseover={this.props.onHover}/>
+        });
+        this.props.onDrag(unNormalizePoint);
+        event.preventDefault()
+    }
+
+    private handlePointUpdate(event: any) {
+        const selected = this.state.selected;
+        if(selected === undefined || selected === 0 || selected === this.state.points.length-1) {
+            this.setState({
+                selected: undefined,
+                copyPoint: undefined,
+            });
+            return
+        }
+        const updatedPoint = this.unNormalizePoint(Vec2.create(this.state.updatedX, this.state.updatedY));
+        const points = this.state.points.filter((_,i) => i !== this.state.selected);
+        points.push(updatedPoint);;
+        points.sort((a, b) => { 
+            if(a[0] === b[0]){
+                if(a[0] === 0){
+                    return a[1]-b[1];
+                }
+                if(a[1] === 1){
+                    return b[1]-a[1];
+                }
+                return a[1]-b[1];
+            }
+            return a[0] - b[0];
+        });
+        this.setState({
+            points,
+            selected: undefined,
+            copyPoint: undefined,
+        });
+        this.change(points);
+        event.preventDefault();
+    }
+
+    private handleDoubleClick(event: any) {
+        let newPoint;
+        const pt = this.myRef.createSVGPoint();
+        pt.x = event.clientX;
+        pt.y = event.clientY;
+        const svgP = pt.matrixTransform(this.myRef.getScreenCTM().inverse());
+        const points = this.state.points;
+        const padding = this.padding/2; 
+
+        if( svgP.x < (padding) || 
+            svgP.x > (this.width+(padding)) || 
+            svgP.y > (this.height+(padding)) || 
+            svgP.y < (this.padding/2)) {
+            return;
+        }
+        newPoint = this.unNormalizePoint(Vec2.create(svgP.x, svgP.y));
+        points.push(newPoint);
+        points.sort((a, b) => { 
+            if(a[0] === b[0]){
+                if(a[0] === 0){
+                    return a[1]-b[1];
+                }
+                if(a[1] === 1){
+                    return b[1]-a[1];
+                }
+                return a[1]-b[1];
+            }
+            return a[0] - b[0];
+        });
+        this.setState({points})
+        this.change(points);
+        event.preventDefault();
+    }
+    private deletePoint = (i:number) => (event: any) => {
+    if(i===0 || i===this.state.points.length-1){ return};
+        const points = this.state.points.filter((_,j) => j !== i);
+        points.sort((a, b) => { 
+            if(a[0] === b[0]){
+                if(a[0] === 0){
+                    return a[1]-b[1];
+                }
+                if(a[1] === 1){
+                    return b[1]-a[1];
+                }
+                return a[1]-b[1];
+            }
+            return a[0] - b[0];
+        });
+        this.setState({points});
+        this.change(points);
+        event.stopPropagation();
+    }
+
+    private normalizePoint(point: Vec2) {
+        const min = this.padding/2;
+        const maxX = this.width+min;
+        const maxY = this.height+min; 
+        const normalizedX = (point[0]*(maxX-min))+min; 
+        const normalizedY = (point[1]*(maxY-min))+min;
+        const reverseY = (this.height+this.padding)-normalizedY;
+        const newPoint = Vec2.create(normalizedX, reverseY);
+        return newPoint;
+    }
+
+    private unNormalizePoint(point: Vec2) {
+        const min = this.padding/2;
+        const maxX = this.width+min; 
+        const maxY = this.height+min;
+        const unNormalizedX = (point[0]-min)/(maxX-min);
+
+        // we have to take into account that we reversed y when we first normalized it.
+        const unNormalizedY = ((this.height+this.padding)-point[1]-min)/(maxY-min); 
+
+        return Vec2.create(unNormalizedX, unNormalizedY);
+    }
+
+    private refCallBack(element: any) {
+        if(element){
+            this.myRef = element;
+        }
+    }
+
+    private renderPoints() {
+        const points: any[] = [];
+        let point: Vec2;
+        for (let i = 0; i < this.state.points.length; i++){
+            if(i != 0 && i != this.state.points.length-1){
+                point = this.normalizePoint(this.state.points[i]);
+                points.push(<PointComponent
+                        key={i}
+                        id={i}
+                        x={point[0]} 
+                        y={point[1]}
+                        nX={this.state.points[i][0]}
+                        nY={this.state.points[i][1]}
+                        selected={false}
+                        delete={this.deletePoint}
+                        onmouseover={this.props.onHover}
+                        onMouseDown={this.handleMouseDown(i)}
+                    />);
+            }
+        }
+        return points;
+    }
+}
+
+function Poly(props: any) {
+
+    const points: Vec2[] = [];
+    let min:number;
+    let maxX:number;
+    let maxY: number;
+    let normalizedX: number;
+    let normalizedY: number;
+    let reverseY: number;
+    
+    for(const point of props.data){
+        min = parseInt(props.padding, 10)/2;
+        maxX = parseInt(props.width, 10)+min;
+        maxY = parseInt(props.height, 10)+min; 
+        normalizedX = (point[0]*(maxX-min))+min; 
+        normalizedY = (point[1]*(maxY-min))+min;
+        reverseY = (props.height+props.padding)-normalizedY;
+        points.push(Vec2.create(normalizedX, reverseY));
+    }
+
+    if (props.k == null) {props.k = 0.3};
+    const data = points;
+    const size = data.length;
+    const last = size - 2;
+    let path = "M" + [data[0][0], data[0][1]];
+
+    for (let i=0; i<size-1;i++){
+        const x0 = i ? data[i-1][0] : data[0][0];
+        const y0 = i ? data[i-1][1] : data[0][1];
+
+        const x1 = data[i][0];
+        const y1 = data[i][1];
+
+        const x2 = data[i+1][0];
+        const y2 = data[i+1][1];
+
+        const x3 = i !== last ? data[i+2][0] : x2;
+        const y3 = i !== last ? data[i+2][1] : y2; 
+
+        const cp1x = x1 + (x2 - x0)/6 * props.k;
+        const cp1y = y1 + (y2 -y0)/6 * props.k;
+
+        const cp2x = x2 - (x3 -x1)/6 * props.k;
+        const cp2y = y2 - (y3 - y1)/6 * props.k;
+
+        path += "C" + [cp1x, cp1y, cp2x, cp2y, x2, y2];
+    }
+
+    return <path d={path} strokeWidth="5" stroke="#cec9ba" fill="none"/>
+}

+ 47 - 0
src/mol-plugin/ui/controls/line-graph/point-component.tsx

@@ -0,0 +1,47 @@
+
+import * as React from 'react';
+
+import { Vec2 } from 'mol-math/linear-algebra';
+
+export default class PointComponent extends React.Component<any, {show: boolean}> {
+    constructor(props: any){
+        super(props);
+        this.state = {show: false}
+        
+        this.handleHover = this.handleHover.bind(this);
+        this.handleHoverOff = this.handleHoverOff.bind(this);
+        this.deletePoint = this.deletePoint.bind(this);
+    }
+
+    private handleHover() {
+        this.setState({show: true});
+        const point = Vec2.create(this.props.nX, this.props.nY);
+        this.props.onmouseover(point);
+    }
+
+    private handleHoverOff(){
+        this.setState({show: false});
+        this.props.onmouseover(undefined);
+    }
+
+    private deletePoint() {
+        this.props.delete(this.props.id);   
+    }
+
+    public render() {
+        return([
+            <circle 
+                r="10"
+                key={`${this.props.id}circle`}
+                id={`${this.props.id}`}
+                cx={this.props.x} 
+                cy={this.props.y} 
+                onDoubleClick={this.props.delete(this.props.id)}
+                onMouseEnter={this.handleHover} 
+                onMouseLeave={this.handleHoverOff}
+                onMouseDown={this.props.onMouseDown}
+                fill="black"
+            />
+        ]);
+    }
+}

+ 57 - 1
src/mol-plugin/ui/controls/parameters.tsx

@@ -6,12 +6,17 @@
  */
 
 import * as React from 'react'
+
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { camelCaseToWords } from 'mol-util/string';
 import { ColorNames, ColorNamesValueMap } from 'mol-util/color/tables';
 import { Color } from 'mol-util/color';
+import { Vec2 } from 'mol-math/linear-algebra';
+import LineGraphComponent from './line-graph/line-graph-component';
+
 import { Slider, Slider2 } from './slider';
 
+
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
     values: any,
@@ -53,7 +58,7 @@ function controlFor(param: PD.Any): ParamControl | undefined {
         ? BoundedIntervalControl : IntervalControl;
         case 'group': return GroupControl;
         case 'mapped': return MappedControl;
-        case 'line-graph': return void 0;
+        case 'line-graph': return LineGraphControl;
     }
     console.warn(`${(param as any).type} has no associated UI component.`);
     return void 0;
@@ -93,6 +98,57 @@ export class BoolControl extends SimpleParam<PD.Boolean> {
     }
 }
 
+export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGraph>, { isExpanded: boolean, isOverPoint: boolean, message: string }> {
+    state = { 
+        isExpanded: false,
+        isOverPoint: false,
+        message: `${this.props.param.defaultValue.length} points`,
+    }
+
+    onHover = (point?: Vec2) => {
+        this.setState({isOverPoint: !this.state.isOverPoint});
+        if(point){
+            this.setState({message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`});
+            return;
+        }
+        this.setState({message: `${this.props.value.length} points`});
+    }
+
+    onDrag = (point: Vec2) => {
+        this.setState({message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`});
+    }
+
+    onChange = (value: PD.LineGraph['defaultValue'] ) => {
+        this.props.onChange({ name: this.props.name, param: this.props.param, value: value});
+    }
+
+    toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
+        this.setState({ isExpanded: !this.state.isExpanded });
+        e.currentTarget.blur();
+    }
+
+    render() {
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
+        return <>
+            <div className='msp-control-row'>
+                <span>{label}</span>
+                <div>
+                    <button onClick={this.toggleExpanded}>
+                        {`${this.state.message}`}
+                    </button>
+                </div>
+            </div>
+            <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
+                <LineGraphComponent
+                    data={this.props.param.defaultValue} 
+                    onChange={this.onChange} 
+                    onHover={this.onHover}
+                    onDrag={this.onDrag}/>
+            </div>
+        </>;
+    }
+}
+
 export class NumberInputControl extends SimpleParam<PD.Numeric> {
     onChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.update(+e.target.value); }
     renderControl() {

+ 1 - 1
src/mol-plugin/ui/state.tsx

@@ -9,8 +9,8 @@ import * as React from 'react';
 import { PluginComponent } from './base';
 import { shallowEqual } from 'mol-util';
 import { List } from 'immutable';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParameterControls } from './controls/parameters';
+import { ParamDefinition as PD} from 'mol-util/param-definition';
 import { Subject } from 'rxjs';
 
 export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> {