David Catuhe 4 gadi atpakaļ
vecāks
revīzija
25052fa00e

+ 53 - 0
guiEditor/src/sharedUiComponents/colorPicker/colorComponentEntry.tsx

@@ -0,0 +1,53 @@
+import * as React from "react";
+
+export interface IColorComponentEntryProps {
+    value: number,
+    label: string,
+    max?: number,
+    min?: number,
+    onChange: (value: number) => void
+}
+
+export class ColorComponentEntry extends React.Component<IColorComponentEntryProps> {
+    constructor(props: IColorComponentEntryProps) {
+        super(props);
+    }
+
+    updateValue(valueString: string) {
+        if (/[^0-9\.\-]/g.test(valueString)) {
+            return;
+        }
+
+        let valueAsNumber = parseInt(valueString);
+
+        if (isNaN(valueAsNumber)) {
+            return;
+        }
+        if(this.props.max != undefined && (valueAsNumber > this.props.max)) {
+            valueAsNumber = this.props.max;
+        }
+        if(this.props.min != undefined && (valueAsNumber < this.props.min)) {
+            valueAsNumber = this.props.min;
+        }
+
+        this.props.onChange(valueAsNumber);
+    }
+
+    public render() {
+        return (
+            <div className="color-picker-component">
+                <div className="color-picker-component-value">
+                    <input type="number" step={1} className="numeric-input"
+                        value={this.props.value} 
+                        onChange={(evt) => this.updateValue(evt.target.value)} />
+                </div>                        
+                <div className="color-picker-component-label">
+                    {
+                        this.props.label
+                    }
+                </div>
+            </div>
+        )
+    }
+
+}

+ 181 - 0
guiEditor/src/sharedUiComponents/colorPicker/colorPicker.scss

@@ -0,0 +1,181 @@
+.color-picker-container {
+    width: 320px;
+    height: 300px;
+    background-color: white;
+    display: grid;    
+    grid-template-columns: 100%;
+    grid-template-rows: 50% 50px 60px 40px 1fr;
+    font-family: "acumin-pro-condensed";
+    font-weight: normal;   
+    font-size: 14px;
+    
+    .color-picker-saturation {
+        grid-row: 1;
+        grid-column: 1;
+        display: grid;
+        grid-template-columns: 100%;
+        grid-template-rows: 100%;
+        position: relative;
+        cursor: pointer;
+    
+        .color-picker-saturation-white {
+            grid-row: 1;
+            grid-column: 1;
+
+            background: -webkit-linear-gradient(to right, #fff, rgba(255,255,255,0));
+            background: linear-gradient(to right, #fff, rgba(255,255,255,0));
+        }
+
+        .color-picker-saturation-black {
+            grid-row: 1;
+            grid-column: 1;
+
+            background: -webkit-linear-gradient(to top, #000, rgba(0,0,0,0));
+            background: linear-gradient(to top, #000, rgba(0,0,0,0));
+        }
+
+        .color-picker-saturation-cursor {
+            pointer-events: none;
+            width: 4px;
+            height: 4px;
+            box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px 1px rgba(0,0,0,.3), 0 0 1px 2px rgba(0,0,0,.4);
+            border-radius: 50%;
+            transform: translate(-2px, -2px);
+            position: absolute;
+        }
+    }
+
+    .color-picker-hue {
+        grid-row: 2;
+        grid-column: 1;
+        display: grid;
+        margin: 10px;
+        grid-template-columns: 24% 76%;
+        grid-template-rows: 100%;
+
+        .color-picker-hue-color {
+            grid-row: 1;
+            grid-column: 1;
+            align-self: center;
+            justify-self: center;
+            width: 30px;
+            height: 30px;
+            border-radius: 15px;
+            border: 1px solid black;
+        }
+
+        .color-picker-hue-slider {
+            grid-row: 1;
+            grid-column: 2;
+            align-self: center;
+            height: 16px;
+            position: relative;
+            cursor: pointer;
+            
+            background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0
+                    33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
+            background: -webkit-linear-gradient(to right, #f00 0%, #ff0
+                17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);                
+
+            .color-picker-hue-cursor {
+                pointer-events: none;
+                width: 8px;
+                height: 18px;
+                transform: translate(-4px, -2px);
+                background-color: rgb(248, 248, 248);
+                box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);
+                position: absolute;
+            }
+        }
+    }
+
+    .color-picker-component {
+        display: grid;
+        margin: 5px;
+        grid-template-columns: 100%;
+        grid-template-rows: 50% 50%;
+
+        .color-picker-component-value {
+            justify-self: center;
+            align-self: center;
+            grid-row: 1;
+            grid-column: 1;
+            margin-bottom: 4px;
+
+            input {
+                width: 50px;
+            }
+        }
+
+        .color-picker-component-label {
+            justify-self: center;
+            align-self: center;
+            grid-row: 2;
+            grid-column: 1;
+            color:black;
+        }
+    }
+
+    .color-picker-rgb {
+        grid-row: 3;
+        grid-column: 1;
+        display: grid;
+        margin: 10px;
+        grid-template-columns: 20% 6.66% 20% 6.66% 20% 6.66% 20%;
+        grid-template-rows: 100%;
+    }
+
+    .red {
+        grid-row: 1;
+        grid-column: 1;
+    }
+
+    .green {
+        grid-row: 1;
+        grid-column: 3;
+    }
+
+    .blue {
+        grid-row: 1;
+        grid-column: 5;
+    }
+
+    .alpha {
+        grid-row: 1;
+        grid-column: 7;
+
+        &.grayed {
+            opacity: 0.5;
+        }
+    }
+
+    .color-picker-hex {
+        grid-row: 4;
+        grid-column: 1;
+        display: grid;       
+        grid-template-columns: 20% 80%;
+        grid-template-rows: 100%;
+
+        .color-picker-hex-label {
+            justify-self: center;
+            align-self: center;
+            grid-row: 1;
+            grid-column: 1;
+            margin-left: 10px;
+            color:black;
+        }
+
+        .color-picker-hex-value {
+            justify-self: left;
+            align-self: center;
+            grid-row: 1;
+            grid-column: 2;
+            margin-left: 10px;
+            margin-right: 10px;
+
+            input {
+                width: 70px;
+            }
+        }
+    }
+}

+ 220 - 0
guiEditor/src/sharedUiComponents/colorPicker/colorPicker.tsx

@@ -0,0 +1,220 @@
+import * as React from "react";
+import { Color3, Color4 } from "babylonjs/Maths/math.color";
+import { ColorComponentEntry } from './colorComponentEntry';
+import { HexColor } from './hexColor';
+
+require("./colorPicker.scss");
+
+/**
+ * Interface used to specify creation options for color picker
+ */
+export interface IColorPickerProps {
+    color: Color3 | Color4,
+    debugMode?: boolean,
+    onColorChanged?: (color: Color3 | Color4) => void
+}
+
+/**
+ * Interface used to specify creation options for color picker
+ */
+export interface IColorPickerState {
+    color: Color3;
+    alpha: number;
+}
+
+/**
+ * Class used to create a color picker
+ */
+export class ColorPicker extends React.Component<IColorPickerProps, IColorPickerState> {
+    private _saturationRef: React.RefObject<HTMLDivElement>;
+    private _hueRef: React.RefObject<HTMLDivElement>;
+    private _isSaturationPointerDown: boolean;
+    private _isHuePointerDown: boolean;
+
+    constructor(props: IColorPickerProps) {
+        super(props);
+        if (this.props.color instanceof Color4) {
+            this.state = {color: new Color3(this.props.color.r, this.props.color.g, this.props.color.b), alpha: this.props.color.a};
+        } else {
+            this.state = {color : this.props.color.clone(), alpha: 1};
+        }
+        this._saturationRef = React.createRef();
+        this._hueRef = React.createRef();
+    }
+
+    onSaturationPointerDown(evt: React.PointerEvent<HTMLDivElement>) {
+        this._evaluateSaturation(evt);
+        this._isSaturationPointerDown = true;
+
+        evt.currentTarget.setPointerCapture(evt.pointerId);
+    }
+
+    onSaturationPointerUp(evt: React.PointerEvent<HTMLDivElement>) {
+        this._isSaturationPointerDown = false;
+        evt.currentTarget.releasePointerCapture(evt.pointerId);
+    }
+
+    onSaturationPointerMove(evt: React.PointerEvent<HTMLDivElement>) {
+        if (!this._isSaturationPointerDown) {
+            return;
+        }
+        this._evaluateSaturation(evt);
+    }
+
+    onHuePointerDown(evt: React.PointerEvent<HTMLDivElement>) {
+        this._evaluateHue(evt);
+        this._isHuePointerDown = true;
+
+        evt.currentTarget.setPointerCapture(evt.pointerId);
+    }
+    
+    onHuePointerUp(evt: React.PointerEvent<HTMLDivElement>) {
+        this._isHuePointerDown = false;
+        evt.currentTarget.releasePointerCapture(evt.pointerId);
+    }
+
+    onHuePointerMove(evt: React.PointerEvent<HTMLDivElement>) {
+        if (!this._isHuePointerDown) {
+            return;
+        }
+        this._evaluateHue(evt);
+    }
+
+    private _evaluateSaturation(evt: React.PointerEvent<HTMLDivElement>) {
+        let left = evt.nativeEvent.offsetX;
+        let top = evt.nativeEvent.offsetY;
+      
+        const saturation =  Math.min(1, Math.max(0.0001, left / this._saturationRef.current!.clientWidth));
+        const value = Math.min(1, Math.max(0.0001, 1 - (top / this._saturationRef.current!.clientHeight)));
+
+        if (this.props.debugMode) {
+            console.log("Saturation: " + saturation);
+            console.log("Value: " + value);
+        }
+
+        let hsv = this.state.color.toHSV();
+        Color3.HSVtoRGBToRef(hsv.r, saturation, value, this.state.color);
+        this.setState({color: this.state.color});
+    }
+
+    private _evaluateHue(evt: React.PointerEvent<HTMLDivElement>) {
+        let left = evt.nativeEvent.offsetX;
+      
+        const hue = 360 * Math.min(0.9999, Math.max(0.0001, left / this._hueRef.current!.clientWidth));
+
+        if (this.props.debugMode) {
+            console.log("Hue: " + hue);
+        }
+
+        let hsv = this.state.color.toHSV();
+        Color3.HSVtoRGBToRef(hue, Math.max(hsv.g, 0.0001), Math.max(hsv.b, 0.0001), this.state.color);
+        this.setState({color: this.state.color});
+    }
+
+    componentDidUpdate() {
+        this.raiseOnColorChanged();
+    }
+
+    raiseOnColorChanged() {
+        if (!this.props.onColorChanged) {
+            return;
+        }
+
+        if (this.props.color instanceof Color4) {
+            let newColor4 = Color4.FromColor3(this.state.color, this.state.alpha);
+
+            this.props.onColorChanged(newColor4);
+
+            return;
+        }
+
+        this.props.onColorChanged(this.state.color.clone());
+    } 
+
+    public render() {
+        let colorHex = this.state.color.toHexString();
+        let hsv = this.state.color.toHSV();
+        let colorRef = new Color3();
+        Color3.HSVtoRGBToRef(hsv.r, 1, 1, colorRef)
+        let colorHexRef = colorRef.toHexString();
+        let hasAlpha = this.props.color instanceof Color4;
+
+        return (
+            <div className="color-picker-container">
+                <div className="color-picker-saturation"  
+                    onPointerMove={e => this.onSaturationPointerMove(e)}               
+                    onPointerDown={e => this.onSaturationPointerDown(e)}
+                    onPointerUp={e => this.onSaturationPointerUp(e)}
+                    ref={this._saturationRef}
+                    style={{
+                        background: colorHexRef
+                    }}>
+                    <div className="color-picker-saturation-white">
+                    </div>
+                    <div className="color-picker-saturation-black">
+                    </div>
+                    <div className="color-picker-saturation-cursor" style={{
+                        top: `${ -(hsv.b * 100) + 100 }%`,
+                        left: `${ hsv.g * 100 }%`,
+                    }}>
+                    </div>
+                </div>
+                <div className="color-picker-hue">
+                    <div className="color-picker-hue-color" style={{
+                        background: colorHex
+                    }}>
+                    </div>
+                    <div className="color-picker-hue-slider"                    
+                        ref={this._hueRef}
+                        onPointerMove={e => this.onHuePointerMove(e)}               
+                        onPointerDown={e => this.onHuePointerDown(e)}
+                        onPointerUp={e => this.onHuePointerUp(e)}
+                    >                    
+                        <div className="color-picker-hue-cursor" style={{
+                            left: `${ (hsv.r / 360.0) * 100 }%`,
+                            border: `1px solid ` + colorHexRef
+                        }}>                    
+                        </div>
+                    </div>
+                </div>
+                <div className="color-picker-rgb">
+                    <div className="red">
+                        <ColorComponentEntry label="R" min={0} max={255} value={this.state.color.r * 255 | 0} onChange={value => {
+                            this.state.color.r = value / 255.0;
+                            this.forceUpdate();
+                        }}/>
+                    </div>   
+                    <div className="green">
+                        <ColorComponentEntry label="G" min={0} max={255}  value={this.state.color.g * 255 | 0} onChange={value => {
+                            this.state.color.g = value / 255.0;
+                            this.forceUpdate();
+                        }}/>
+                    </div>  
+                    <div className="blue">
+                        <ColorComponentEntry label="B" min={0} max={255}  value={this.state.color.b * 255 | 0} onChange={value => {
+                            this.state.color.b = value / 255.0;
+                            this.forceUpdate();
+                        }}/>
+                    </div>        
+                    <div className={"alpha" + (hasAlpha ? "" : " grayed")}>
+                        <ColorComponentEntry label="A" min={0} max={255} value={this.state.alpha * 255 | 0} onChange={value => {
+                                this.setState({alpha: value / 255.0});
+                                this.forceUpdate();
+                        }}/>
+                    </div>   
+                </div>  
+                <div className="color-picker-hex">
+                    <div className="color-picker-hex-label">
+                        Hex
+                    </div>
+                    <div className="color-picker-hex-value">     
+                        <HexColor expectedLength={6} value={colorHex} onChange={value => {
+                            this.setState({color: Color3.FromHexString(value)});
+                        }}/>            
+                    </div>
+                </div>
+            </div>
+        );
+    }
+}
+

+ 45 - 0
guiEditor/src/sharedUiComponents/colorPicker/hexColor.tsx

@@ -0,0 +1,45 @@
+import * as React from "react";
+
+export interface IHexColorProps {
+    value: string,
+    expectedLength: number,
+    onChange: (value: string) => void
+}
+
+export class HexColor extends React.Component<IHexColorProps, {hex: string}> {
+    constructor(props: IHexColorProps) {
+        super(props);
+
+        this.state = {hex: this.props.value.replace("#", "")}
+    }
+
+    shouldComponentUpdate(nextProps: IHexColorProps, nextState: {hex: string}) {
+        if (nextProps.value!== this.props.value) {
+            nextState.hex = nextProps.value.replace("#", "");
+        }
+
+        return true;
+    }
+
+    updateHexValue(valueString: string) {
+        if (valueString != "" && /^[0-9A-Fa-f]+$/g.test(valueString) == false) {
+            return;
+        }
+    
+        this.setState({hex: valueString});
+
+        if(valueString.length !== this.props.expectedLength) {
+            return;
+        }
+       
+        this.props.onChange("#" + valueString);
+    }
+
+    public render() {
+        return (
+            <input type="string" className="hex-input" value={this.state.hex} 
+                onChange={evt => this.updateHexValue(evt.target.value)}/>   
+        )
+    }
+
+}

+ 31 - 0
guiEditor/src/sharedUiComponents/lines/booleanLineComponent.tsx

@@ -0,0 +1,31 @@
+import * as React from "react";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faCheck, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
+
+export interface IBooleanLineComponentProps {
+    label: string;
+    value: boolean;
+}
+
+export class BooleanLineComponent extends React.Component<IBooleanLineComponentProps> {
+    constructor(props: IBooleanLineComponentProps) {
+        super(props);
+    }
+
+    render() {
+
+        const check = this.props.value ? <FontAwesomeIcon icon={faCheck} /> : <FontAwesomeIcon icon={faTimesCircle} />
+        const className = this.props.value ? "value check" : "value uncheck";
+
+        return (
+            <div className="textLine">
+                <div className="label" title={this.props.label}>
+                    {this.props.label}
+                </div>
+                <div className={className}>
+                    {check}
+                </div>
+            </div>
+        );
+    }
+}

+ 21 - 0
guiEditor/src/sharedUiComponents/lines/buttonLineComponent.tsx

@@ -0,0 +1,21 @@
+import * as React from "react";
+
+export interface IButtonLineComponentProps {
+    label: string;
+    onClick: () => void;
+}
+
+export class ButtonLineComponent extends React.Component<IButtonLineComponentProps> {
+    constructor(props: IButtonLineComponentProps) {
+        super(props);
+    }
+
+    render() {
+
+        return (
+            <div className="buttonLine">
+                <button onClick={() => this.props.onClick()}>{this.props.label}</button>
+            </div>
+        );
+    }
+}

+ 39 - 0
guiEditor/src/sharedUiComponents/lines/fileButtonLineComponent.tsx

@@ -0,0 +1,39 @@
+import * as React from "react";
+
+interface IFileButtonLineComponentProps {
+    label: string;
+    onClick: (file: File) => void;
+    accept: string;
+}
+
+export class FileButtonLineComponent extends React.Component<IFileButtonLineComponentProps> {
+    private static _IDGenerator = 0;
+    private _id = FileButtonLineComponent._IDGenerator++;
+    private uploadInputRef: React.RefObject<HTMLInputElement>;
+
+
+    constructor(props: IFileButtonLineComponentProps) {
+        super(props);
+        this.uploadInputRef = React.createRef();
+    }
+
+    onChange(evt: any) {
+        var files: File[] = evt.target.files;
+        if (files && files.length) {
+            this.props.onClick(files[0]);
+        }
+
+        evt.target.value = "";
+    }
+
+    render() {
+        return (
+            <div className="buttonLine">
+                <label htmlFor={"file-upload" + this._id} className="file-upload">
+                    {this.props.label}
+                </label>
+                <input ref={this.uploadInputRef} id={"file-upload" + this._id} type="file" accept={this.props.accept} onChange={evt => this.onChange(evt)} />
+            </div>
+        );
+    }
+}

+ 39 - 0
guiEditor/src/sharedUiComponents/lines/fileMultipleButtonLineComponent.tsx

@@ -0,0 +1,39 @@
+import * as React from "react";
+
+interface IFileMultipleButtonLineComponentProps {
+    label: string;
+    onClick: (event: any) => void;
+    accept: string;
+}
+
+export class FileMultipleButtonLineComponent extends React.Component<IFileMultipleButtonLineComponentProps> {
+    private static _IDGenerator = 0;
+    private _id = FileMultipleButtonLineComponent._IDGenerator++;
+    private uploadInputRef: React.RefObject<HTMLInputElement>;
+
+
+    constructor(props: IFileMultipleButtonLineComponentProps) {
+        super(props);
+        this.uploadInputRef = React.createRef();
+    }
+
+    onChange(evt: any) {
+        var files: File[] = evt.target.files;
+        if (files && files.length) {
+            this.props.onClick(evt);
+        }
+
+        evt.target.value = "";
+    }
+
+    render() {
+        return (
+            <div className="buttonLine">
+                <label htmlFor={"file-upload" + this._id} className="file-upload">
+                    {this.props.label}
+                </label>
+                <input ref={this.uploadInputRef} id={"file-upload" + this._id} type="file" accept={this.props.accept} onChange={evt => this.onChange(evt)} multiple />
+            </div>
+        );
+    }
+}

+ 27 - 0
guiEditor/src/sharedUiComponents/lines/iconButtonLineComponent.tsx

@@ -0,0 +1,27 @@
+import * as React from 'react';
+
+export interface IIconButtonLineComponentProps {
+  icon: string;
+  onClick: () => void;
+  tooltip: string;
+  active?: boolean;
+}
+
+export class IconButtonLineComponent extends React.Component<
+  IIconButtonLineComponentProps
+> {
+  constructor(props: IIconButtonLineComponentProps) {
+    super(props);
+  }
+
+  render() {
+    return (
+      <div
+        style={{ backgroundColor: this.props.active ? '#111111' : '' }}
+        title={this.props.tooltip}
+        className={`icon ${this.props.icon}`}
+        onClick={() => this.props.onClick()}
+      />
+    );
+  }
+}

+ 51 - 0
guiEditor/src/sharedUiComponents/lines/indentedTextLineComponent.tsx

@@ -0,0 +1,51 @@
+import * as React from "react";
+
+interface IIndentedTextLineComponentProps {
+    value?: string;
+    color?: string;
+    underline?: boolean;
+    onLink?: () => void;
+    url?: string;
+    additionalClass?: string;
+}
+
+export class IndentedTextLineComponent extends React.Component<IIndentedTextLineComponentProps> {
+    constructor(props: IIndentedTextLineComponentProps) {
+        super(props);
+    }
+
+    onLink() {
+        if (this.props.url) {
+            window.open(this.props.url, '_blank');
+            return;
+        }
+        if (!this.props.onLink) {
+            return;
+        }
+
+        this.props.onLink();
+    }
+
+    renderContent() {
+        if (this.props.onLink || this.props.url) {
+            return (
+                <div className="link-value" title={this.props.value} onClick={() => this.onLink()}>
+                    {this.props.url ? "doc" : (this.props.value || "no name")}
+                </div>
+            )
+        }
+        return (
+            <div className="value" title={this.props.value} style={{ color: this.props.color ? this.props.color : "" }}>
+                {this.props.value || "no name"}
+            </div>
+        )
+    }
+
+    render() {
+        return (
+            <div className={"indented " + (this.props.underline ? "textLine underline" : "textLine" + (this.props.additionalClass ? " " + this.props.additionalClass : ""))}>
+                {this.renderContent()}
+            </div>
+        );
+    }
+}

+ 47 - 0
guiEditor/src/sharedUiComponents/lines/linkButtonComponent.tsx

@@ -0,0 +1,47 @@
+import * as React from "react";
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+interface ILinkButtonComponentProps {
+    label: string;
+    buttonLabel: string;
+    url?: string;
+    onClick: () => void;
+    icon?: IconProp;
+    onIconClick?: () => void;
+}
+
+export class LinkButtonComponent extends React.Component<ILinkButtonComponentProps> {
+    constructor(props: ILinkButtonComponentProps) {
+        super(props);
+    }
+
+    onLink() {
+        if (this.props.url) {
+            window.open(this.props.url, '_blank');
+        }
+    }
+
+    render() {
+        return (
+            <div className={"linkButtonLine"}>
+                <div className="link" title={this.props.label} onClick={() => this.onLink()}>
+                    {this.props.label}
+                </div>
+                <div className="link-button">
+                    <button onClick={() => this.props.onClick()}>{this.props.buttonLabel}</button>
+                </div> 
+                {
+                    this.props.icon &&
+                    <div className="link-icon hoverIcon" onClick={() => {
+                        if (this.props.onIconClick) {
+                            this.props.onIconClick();
+                        }
+                    }}>
+                        <FontAwesomeIcon icon={this.props.icon} />
+                    </div> 
+                }
+            </div>
+        );
+    }
+}

+ 38 - 0
guiEditor/src/sharedUiComponents/lines/messageLineComponent.tsx

@@ -0,0 +1,38 @@
+import * as React from "react";
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+interface IMessageLineComponentProps {
+    text: string;
+    color?: string;
+    icon?: IconProp;
+}
+
+export class MessageLineComponent extends React.Component<IMessageLineComponentProps> {
+    constructor(props: IMessageLineComponentProps) {
+        super(props);
+    }
+
+    render() {
+        if (this.props.icon) {
+            return (
+                <div className="iconMessageLine">
+                    <div className="icon" style={{ color: this.props.color ? this.props.color : "" }}>
+                        <FontAwesomeIcon icon={this.props.icon}/>
+                    </div>
+                    <div className="value" title={this.props.text}>
+                        {this.props.text}
+                    </div>
+                </div>
+            );
+        }
+
+        return (
+            <div className="messageLine">
+                <div className="value" title={this.props.text} style={{ color: this.props.color ? this.props.color : "" }}>
+                    {this.props.text}
+                </div>
+            </div>
+        );
+    }
+}

+ 80 - 0
guiEditor/src/sharedUiComponents/lines/numericInputComponent.tsx

@@ -0,0 +1,80 @@
+import * as React from "react";
+
+interface INumericInputComponentProps {
+    label: string;
+    value: number;
+    step?: number;
+    onChange: (value: number) => void;
+    precision?: number;
+}
+
+export class NumericInputComponent extends React.Component<INumericInputComponentProps, { value: string }> {
+
+    static defaultProps = {
+        step: 1,
+    };
+
+    private _localChange = false;
+    constructor(props: INumericInputComponentProps) {
+        super(props);
+
+        this.state = { value: this.props.value.toFixed(this.props.precision !== undefined ? this.props.precision : 3) }
+    }
+
+    shouldComponentUpdate(nextProps: INumericInputComponentProps, nextState: { value: string }) {
+        if (this._localChange) {
+            return true;
+        }
+
+        if (nextProps.value.toString() !== nextState.value) {
+            nextState.value = nextProps.value.toFixed(this.props.precision !== undefined ? this.props.precision : 3);
+            return true;
+        }
+        return false;
+    }
+
+    updateValue(evt: any) {
+        let value = evt.target.value;
+
+        if (/[^0-9\.\-]/g.test(value)) {
+            return;
+        }
+
+        let valueAsNumber = parseFloat(value);
+
+        this._localChange = true;
+        this.setState({ value: value });
+
+        if (isNaN(valueAsNumber)) {
+            return;
+        }
+
+        this.props.onChange(valueAsNumber);
+    }
+
+    onBlur() {
+        this._localChange = false;
+        let valueAsNumber = parseFloat(this.state.value);
+
+        if (isNaN(valueAsNumber)) {
+            this.props.onChange(this.props.value);
+            return;
+        }
+
+        this.props.onChange(valueAsNumber);
+    }
+
+    render() {
+        return (
+            <div className="numeric">
+                {
+                    this.props.label &&
+                    <div className="numeric-label" title={this.props.label}>
+                        {`${this.props.label}: `}
+                    </div>
+                }
+                <input type="number" step={this.props.step} className="numeric-input" value={this.state.value} onChange={evt => this.updateValue(evt)} onBlur={() => this.onBlur()}/>
+            </div>
+        )
+    }
+}

+ 52 - 0
guiEditor/src/sharedUiComponents/lines/radioLineComponent.tsx

@@ -0,0 +1,52 @@
+import * as React from "react";
+import { Nullable } from "babylonjs/types";
+import { Observer, Observable } from "babylonjs/Misc/observable";
+
+interface IRadioButtonLineComponentProps {
+    onSelectionChangedObservable: Observable<RadioButtonLineComponent>;
+    label: string;
+    isSelected: () => boolean;
+    onSelect: () => void;
+}
+
+export class RadioButtonLineComponent extends React.Component<IRadioButtonLineComponentProps, { isSelected: boolean }> {
+    private _onSelectionChangedObserver: Nullable<Observer<RadioButtonLineComponent>>;
+
+    constructor(props: IRadioButtonLineComponentProps) {
+        super(props);
+
+        this.state = { isSelected: this.props.isSelected() };
+    }
+
+    componentDidMount() {
+        this._onSelectionChangedObserver = this.props.onSelectionChangedObservable.add((value) => {
+            this.setState({ isSelected: value === this });
+        });
+    }
+
+    componentWillUnmount() {
+        if (this._onSelectionChangedObserver) {
+            this.props.onSelectionChangedObservable.remove(this._onSelectionChangedObserver);
+            this._onSelectionChangedObserver = null;
+        }
+    }
+
+    onChange() {
+        this.props.onSelect();
+        this.props.onSelectionChangedObservable.notifyObservers(this);
+    }
+
+    render() {
+        return (
+            <div className="radioLine">
+                <div className="label" title={this.props.label}>
+                    {this.props.label}
+                </div>
+                <div className="radioContainer">
+                    <input id={this.props.label} className="radio" type="radio" checked={this.state.isSelected} onChange={() => this.onChange()} />
+                    <label htmlFor={this.props.label} className="labelForRadio" />
+                </div>
+            </div>
+        );
+    }
+}

+ 60 - 0
guiEditor/src/sharedUiComponents/lines/textLineComponent.tsx

@@ -0,0 +1,60 @@
+import * as React from "react";
+
+interface ITextLineComponentProps {
+    label?: string;
+    value?: string;
+    color?: string;
+    underline?: boolean;
+    onLink?: () => void;
+    url?: string;
+    ignoreValue?: boolean;
+    additionalClass?: string;
+}
+
+export class TextLineComponent extends React.Component<ITextLineComponentProps> {
+    constructor(props: ITextLineComponentProps) {
+        super(props);
+    }
+
+    onLink() {
+        if (this.props.url) {
+            window.open(this.props.url, '_blank');
+            return;
+        }
+        if (!this.props.onLink) {
+            return;
+        }
+
+        this.props.onLink();
+    }
+
+    renderContent() {
+        if (this.props.ignoreValue) {
+            return null;
+        }
+
+        if (this.props.onLink || this.props.url) {
+            return (
+                <div className="link-value" title={this.props.value} onClick={() => this.onLink()}>
+                    {this.props.url ? "doc" : (this.props.value || "no name")}
+                </div>
+            )
+        }
+        return (
+            <div className="value" title={this.props.value} style={{ color: this.props.color ? this.props.color : "" }}>
+                {this.props.value || "no name"}
+            </div>
+        )
+    }
+
+    render() {
+        return (
+            <div className={this.props.underline ? "textLine underline" : "textLine" + (this.props.additionalClass ? " " + this.props.additionalClass : "")}>
+                <div className="label"  title={this.props.label ?? ""}>
+                    {this.props.label ?? ""}
+                </div>
+                {this.renderContent()}
+            </div>
+        );
+    }
+}

+ 31 - 0
guiEditor/src/sharedUiComponents/lines/valueLineComponent.tsx

@@ -0,0 +1,31 @@
+import * as React from "react";
+
+interface IValueLineComponentProps {
+    label: string;
+    value: number;
+    color?: string;
+    fractionDigits?: number;
+    units?: string;
+}
+
+export class ValueLineComponent extends React.Component<IValueLineComponentProps> {
+    constructor(props: IValueLineComponentProps) {
+        super(props);
+    }
+
+    render() {
+        const digits = this.props.fractionDigits !== undefined ? this.props.fractionDigits : 2;
+        const value = this.props.value.toFixed(digits) + (this.props.units ? " " + this.props.units : "");
+
+        return (
+            <div className="textLine">
+                <div className="label" title={this.props.label}>
+                    {this.props.label}
+                </div>
+                <div className="value" style={{ color: this.props.color ? this.props.color : "" }}>
+                    {value}
+                </div>
+            </div>
+        );
+    }
+}