UI: Scada metadata and configuration improvements.

This commit is contained in:
Igor Kulikov 2024-05-03 19:21:07 +03:00
parent eb50048c50
commit f01f99e738
5 changed files with 675 additions and 171 deletions

View File

@ -15,13 +15,23 @@
///
import { ValueType } from '@shared/models/constants';
import { Box, Element, Runner, Svg, SVG, Timeline } from '@svgdotjs/svg.js';
import { Box, Element, Runner, Svg, SVG, Timeline, Text } from '@svgdotjs/svg.js';
import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models';
import { insertVariable, isDefinedAndNotNull, isNumber, isNumeric, isUndefinedOrNull, mergeDeep } from '@core/utils';
import {
formatValue,
insertVariable,
isDefinedAndNotNull,
isNumber,
isNumeric,
isUndefinedOrNull,
mergeDeep,
parseFunction
} from '@core/utils';
import { BehaviorSubject, forkJoin, Observable } from 'rxjs';
import { map, share } from 'rxjs/operators';
import { ValueAction, ValueGetter } from '@home/components/widget/lib/action/action-widget.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { Font } from '@shared/models/widget-settings.models';
export type ValueMatcherType = 'any' | 'constant' | 'range';
@ -31,29 +41,85 @@ export interface ValueMatcher {
range?: {from: number; to: number};
}
export type ScadaObjectAttributeValueType = 'input' | 'property';
const valueMatches = (matcher: ValueMatcher, val: any): boolean => {
switch (matcher.type) {
case 'any':
return true;
case 'constant':
return matcher.value === val;
case 'range':
if (isDefinedAndNotNull(val) && isNumeric(val)) {
const num = Number(val);
const range = matcher.range;
return ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to));
} else {
return false;
}
}
};
export interface ScadaObjectAttributeValue {
type: ScadaObjectAttributeValueType;
propertyId?: string;
export type ScadaObjectValueType = 'input' | 'constant' | 'property' | 'function' | 'valueFormat';
export interface ScadaObjectValueBase {
type: ScadaObjectValueType;
}
export interface ScadaObjectAttributeState {
export interface ScadaObjectValueConstant extends ScadaObjectValueBase {
constantValue?: any;
}
export interface ScadaObjectValueProperty extends ScadaObjectValueBase {
propertyId?: string;
computedPropertyValue?: any;
}
export interface ScadaObjectValueFunction extends ScadaObjectValueBase {
valueConvertFunction?: string;
valueConverter?: (val: any) => any;
}
export interface ScadaObjectValueFormat extends ScadaObjectValueBase {
units?: ScadaObjectValue;
decimals?: ScadaObjectValue;
computedUnits?: string;
computedDecimals?: number;
}
export type ScadaObjectValue = ScadaObjectValueProperty & ScadaObjectValueConstant & ScadaObjectValueFunction & ScadaObjectValueFormat;
export interface ScadaObjectAttribute {
name: string;
value: ScadaObjectAttributeValue;
value: ScadaObjectValue;
}
export interface ScadaObjectText {
content?: ScadaObjectValue;
font?: ScadaObjectValue;
color?: ScadaObjectValue;
}
export interface ScadaObjectElementState {
tag: string;
show?: ScadaObjectValue;
text?: ScadaObjectText;
attributes?: ScadaObjectAttribute[];
animate?: number;
}
export interface ScadaObjectState {
tag: string;
attributes: ScadaObjectAttributeState[];
animate?: number;
initial?: boolean;
value?: any;
state: ScadaObjectElementState[];
}
export interface ScadaObjectUpdateState {
matcher: ValueMatcher;
state: ScadaObjectState[];
stateId: string;
}
const filterUpdateStates = (states: ScadaObjectUpdateState[], val: any): ScadaObjectUpdateState[] =>
states.filter(s => valueMatches(s.matcher, val));
export enum ScadaObjectBehaviorType {
setValue = 'setValue',
getValue = 'getValue'
@ -77,32 +143,38 @@ export interface ScadaObjectBehaviorSet extends ScadaObjectBehaviorBase {
export type ScadaObjectBehavior = ScadaObjectBehaviorGet | ScadaObjectBehaviorSet;
export type ScadaObjectPropertyType = 'string' | 'number' | 'color';
export type ScadaObjectPropertyType = 'string' | 'number' | 'color' | 'font' | 'units' | 'switch';
export interface ScadaObjectPropertyBase {
id: string;
name: string;
type: ScadaObjectPropertyType;
default: any;
required?: boolean;
subLabel?: string;
divider?: boolean;
fieldSuffix?: string;
disableOnProperty?: string;
}
export interface ScadaObjectNumberProperty extends ScadaObjectPropertyBase {
min?: number;
max?: number;
step?: number;
}
export type ScadaObjectProperty = ScadaObjectPropertyBase & ScadaObjectNumberProperty;
export interface ScadaObjectMetadata {
title: string;
initial: ScadaObjectState[];
states: {[id: string]: ScadaObjectState};
behavior: ScadaObjectBehavior[];
properties: ScadaObjectProperty[];
}
export const emptyMetadata: ScadaObjectMetadata = {
title: '',
initial: [],
states: {},
behavior: [],
properties: []
};
@ -126,6 +198,7 @@ const parseScadaObjectMetadataFromDom = (svgDoc: Document): ScadaObjectMetadata
return emptyMetadata;
}
} catch (_e) {
console.error(_e);
return emptyMetadata;
}
};
@ -201,8 +274,9 @@ export class ScadaObject {
this.metadata = parseScadaObjectMetadataFromDom(doc);
const defaults = defaultScadaObjectSettings(this.metadata);
this.settings = mergeDeep<ScadaObjectSettings>({}, defaults, this.inputSettings || {});
this.prepareMetadata();
this.prepareSvgShape(doc);
this.prepareStates();
this.initStates();
})
);
}
@ -228,17 +302,58 @@ export class ScadaObject {
}
}
private prepareMetadata() {
for (const stateId of Object.keys(this.metadata.states)) {
const state = this.metadata.states[stateId];
for (const elementState of state.state) {
this.prepareValue(elementState.show);
this.prepareValue(elementState.text?.content);
this.prepareValue(elementState.text?.font);
this.prepareValue(elementState.text?.color);
if (elementState.attributes) {
for (const attribute of elementState.attributes) {
this.prepareValue(attribute.value);
}
}
}
}
}
private prepareValue(value: ScadaObjectValue) {
if (value) {
if (value.type === 'function' && value.valueConvertFunction) {
try {
value.valueConverter = parseFunction(this.insertVariables(value.valueConvertFunction), ['value']);
} catch (e) {
value.valueConverter = (v) => v;
}
} else if (value.type === 'property') {
value.computedPropertyValue = this.getPropertyValue(value.propertyId);
} else if (value.type === 'valueFormat') {
if (value.units) {
this.prepareValue(value.units);
}
if (value.decimals) {
this.prepareValue(value.decimals);
}
}
}
}
private insertVariables(content: string): string {
for (const property of this.metadata.properties) {
const value = this.getPropertyValue(property.id);
content = insertVariable(content, property.id, value);
}
return content;
}
private prepareSvgShape(doc: XMLDocument) {
const elements = doc.getElementsByTagName('tb:metadata');
for (let i=0;i<elements.length;i++) {
elements.item(i).remove();
}
let svgContent = doc.documentElement.innerHTML;
for (const property of this.metadata.properties) {
const value = this.settings[property.id] || '';
svgContent = insertVariable(svgContent, property.id, value);
}
this.svgShape = SVG().svg(svgContent);
this.svgShape = SVG().svg(doc.documentElement.innerHTML);
this.svgShape.node.style.overflow = 'visible';
this.svgShape.node.style['user-select'] = 'none';
this.box = this.svgShape.bbox();
@ -251,7 +366,7 @@ export class ScadaObject {
}
}
private prepareStates() {
private initStates() {
for (const behavior of this.metadata.behavior) {
if (behavior.type === ScadaObjectBehaviorType.getValue) {
const getBehavior = behavior as ScadaObjectBehaviorGet;
@ -266,8 +381,9 @@ export class ScadaObject {
this.valueActions.push(valueGetter);
}
}
if (this.metadata.initial) {
this.updateState(this.metadata.initial);
const initialState = Object.values(this.metadata.states).find(s => s.initial);
if (initialState) {
this.updateState(initialState);
}
if (this.valueGetters.length) {
const getValueObservables: Array<Observable<any>> = [];
@ -301,24 +417,62 @@ export class ScadaObject {
private onValue(id: string, value: any) {
const getBehavior = this.metadata.behavior.find(b => b.id === id) as ScadaObjectBehaviorGet;
value = this.normalizeValue(value, getBehavior.valueType);
const updateStates = this.filterUpdateStates(getBehavior.onUpdate, value);
const updateStates = filterUpdateStates(getBehavior.onUpdate, value);
if (this.animationTimeline) {
this.animationTimeline.finish();
}
for (const updateState of updateStates) {
this.updateState(updateState.state, value);
const state = this.metadata.states[updateState.stateId];
this.updateState(state, value);
}
}
private updateState(state: ScadaObjectState[], value?: any) {
for (const stateEntry of state) {
const tag = stateEntry.tag;
private updateState(state: ScadaObjectState, value?: any) {
if (state) {
for (const elementState of state.state) {
const tag = elementState.tag;
const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`);
const attrs = this.computeAttributes(stateEntry.attributes, value);
if (elements.length) {
if (elementState.show) {
const show: boolean = this.computeValue(elementState.show, value);
elements.forEach(e => {
this.setElementAttributes(e, attrs, stateEntry.animate);
if (show) {
e.show();
} else {
e.hide();
}
});
}
if (elementState.attributes) {
const attrs = this.computeAttributes(elementState.attributes, value);
elements.forEach(e => {
this.setElementAttributes(e, attrs, elementState.animate);
});
}
if (elementState.text) {
if (elementState.text.content) {
const text: string = this.computeValue(elementState.text.content, value);
elements.forEach(e => {
this.setElementText(e, text);
});
}
if (elementState.text.font || elementState.text.color) {
let font: Font = this.computeValue(elementState.text.font, value);
if (typeof font !== 'object') {
font = undefined;
}
let color: string = this.computeValue(elementState.text.color, value);
if (typeof color !== 'string') {
color = undefined;
}
elements.forEach(e => {
this.setElementFont(e, font, color);
});
}
}
}
}
}
}
private normalizeValue(value: any, type: ValueType): any {
@ -339,11 +493,11 @@ export class ScadaObject {
}
}
private computeAttributes(attributes: ScadaObjectAttributeState[], value: any): {[attr: string]: any} {
private computeAttributes(attributes: ScadaObjectAttribute[], value: any): {[attr: string]: any} {
const res: {[attr: string]: any} = {};
for (const attribute of attributes) {
const attr = attribute.name;
res[attr] = this.getAttributeValue(attribute, value);
res[attr] = this.computeValue(attribute.value, value);
}
return res;
}
@ -356,6 +510,41 @@ export class ScadaObject {
}
}
private setElementText(element: Element, text: string) {
let textElement: Text;
if (element.type === 'text') {
const children = element.children();
if (children.length && children[0].type === 'tspan') {
textElement = children[0] as Text;
} else {
textElement = element as Text;
}
} else if (element.type === 'tspan') {
textElement = element as Text;
}
if (textElement) {
textElement.text(text);
}
}
private setElementFont(element: Element, font: Font, color: string) {
if (element.type === 'text') {
const textElement = element as Text;
if (font) {
textElement.font({
family: font.family,
size: (isDefinedAndNotNull(font.size) && isDefinedAndNotNull(font.sizeUnit)) ?
font.size + font.sizeUnit : null,
weight: font.weight,
style: font.style
});
}
if (color) {
textElement.fill(color);
}
}
}
private animation(element: Element, duration: number): Runner {
if (!this.animationTimeline) {
this.animationTimeline = new Timeline();
@ -364,35 +553,55 @@ export class ScadaObject {
return element.animate(duration, 0, 'now');
}
private getAttributeValue(attribute: ScadaObjectAttributeState, value: any): any {
if (attribute.value.type === 'input') {
private computeValue(objectValue: ScadaObjectValue, value: any): any {
if (objectValue) {
switch (objectValue.type) {
case 'input':
return value;
} else if (attribute.value.type === 'property') {
const id = attribute.value.propertyId;
return this.settings[id] || '';
case 'constant':
return objectValue.constantValue;
case 'property':
return objectValue.computedPropertyValue;
case 'function':
try {
return objectValue.valueConverter(value);
} catch (_e) {
return value;
}
case 'valueFormat':
let units = '';
let decimals = 0;
if (objectValue.units) {
units = this.computeValue(objectValue.units, value);
}
if (objectValue.decimals) {
decimals = this.computeValue(objectValue.decimals, value);
}
return formatValue(value, decimals, units, false);
}
} else {
return '';
}
}
private filterUpdateStates(states: ScadaObjectUpdateState[], val: any): ScadaObjectUpdateState[] {
return states.filter(s => this.valueMatches(s.matcher, val));
}
private valueMatches(matcher: ValueMatcher, val: any): boolean {
switch (matcher.type) {
case 'any':
return true;
case 'constant':
return matcher.value === val;
case 'range':
if (isDefinedAndNotNull(val) && isNumeric(val)) {
const num = Number(val);
const range = matcher.range;
return ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to));
private getPropertyValue(id: string): any {
const property = this.metadata.properties.find(p => p.id === id);
if (property) {
const value = this.settings[id];
if (isDefinedAndNotNull(value)) {
return value;
} else {
return false;
}
switch (property.type) {
case 'string':
return '';
case 'number':
return 0;
case 'color':
return '#000';
}
}
} else {
return '';
}
}
}

View File

@ -32,17 +32,42 @@
</tb-get-value-action-settings>
</div>
</div>
<div *ngIf="metadata?.properties?.length" class="tb-form-panel">
<div *ngFor="let property of metadata.properties" class="tb-form-row space-between">
<div class="fixed-title-width">{{ property.name }}</div>
<div *ngIf="propertyRows?.length" class="tb-form-panel">
<div *ngFor="let propertyRow of propertyRows" class="tb-form-row space-between">
<mat-slide-toggle *ngIf="propertyRow.switch" class="mat-slide fixed-title-width" formControlName="{{ propertyRow.switch.id }}">
{{ propertyRow.label }}
</mat-slide-toggle>
<div *ngIf="!propertyRow.switch" class="fixed-title-width">{{ propertyRow.label }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<ng-container *ngFor="let property of propertyRow.properties">
<div *ngIf="property.subLabel" class="tb-small-label">{{ property.subLabel }}</div>
<mat-form-field *ngIf="property.type === 'string'" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="{{ property.id }}" [required]="property.required" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix }}</div>
</mat-form-field>
<tb-color-input *ngIf="property.type === 'color'"
[required]="property.required"
asBoxInput
colorClearButton
formControlName="{{ property.id }}">
</tb-color-input>
<mat-form-field *ngIf="property.type === 'number'" appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="{{ property.id }}" [min]="property.min" [max]="property.max" type="number" placeholder="{{ 'widget-config.set' | translate }}">
<input matInput formControlName="{{ property.id }}" [required]="property.required"
[min]="property.min" [max]="property.max" [step]="property.step"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix }}</div>
</mat-form-field>
<tb-font-settings *ngIf="property.type === 'font'"
formControlName="{{ property.id }}"
clearButton
disabledLineHeight>
</tb-font-settings>
<tb-unit-input *ngIf="property.type === 'units'"
[required]="property.required"
formControlName="{{ property.id }}"></tb-unit-input>
<mat-divider *ngIf="property.divider" vertical></mat-divider>
</ng-container>
</div>
</div>
</div>
</ng-container>

View File

@ -14,12 +14,15 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator,
ValidatorFn,
Validators
} from '@angular/forms';
@ -37,6 +40,11 @@ import { ValueType } from '@shared/models/constants';
import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice, widgetType } from '@shared/models/widget.models';
import { isDefinedAndNotNull } from '@core/utils';
import {
ScadaPropertyRow,
toPropertyRows
} from '@home/components/widget/lib/settings/common/scada/scada-object-settings.models';
import { merge, Observable, Subscription } from 'rxjs';
@Component({
selector: 'tb-scada-object-settings',
@ -47,10 +55,15 @@ import { isDefinedAndNotNull } from '@core/utils';
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScadaObjectSettingsComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ScadaObjectSettingsComponent),
multi: true
}
]
})
export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor {
export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator {
ScadaObjectBehaviorType = ScadaObjectBehaviorType;
@ -73,13 +86,18 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
private propagateChange = null;
private validatorTriggers: string[];
private validatorSubscription: Subscription;
public scadaObjectSettingsFormGroup: UntypedFormGroup;
metadata: ScadaObjectMetadata;
propertyRows: ScadaPropertyRow[];
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder,
private http: HttpClient) {
private http: HttpClient,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
@ -114,6 +132,7 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
this.scadaObjectSettingsFormGroup.disable({emitEvent: false});
} else {
this.scadaObjectSettingsFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
@ -122,10 +141,25 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
this.setupValue();
}
validate(c: UntypedFormControl) {
const valid = this.scadaObjectSettingsFormGroup.valid;
return valid ? null : {
scadaObject: {
valid: false,
},
};
}
private loadMetadata() {
if (this.validatorSubscription) {
this.validatorSubscription.unsubscribe();
this.validatorSubscription = null;
}
this.validatorTriggers = [];
this.http.get(this.svgPath, {responseType: 'text'}).subscribe(
(svgContent) => {
this.metadata = parseScadaObjectMetadataFromContent(svgContent);
this.propertyRows = toPropertyRows(this.metadata.properties);
for (const control of Object.keys(this.scadaObjectSettingsFormGroup.controls)) {
this.scadaObjectSettingsFormGroup.removeControl(control, {emitEvent: false});
}
@ -133,7 +167,15 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
this.scadaObjectSettingsFormGroup.addControl(behaviour.id, this.fb.control(null, []), {emitEvent: false});
}
for (const property of this.metadata.properties) {
if (property.disableOnProperty) {
if (!this.validatorTriggers.includes(property.disableOnProperty)) {
this.validatorTriggers.push(property.disableOnProperty);
}
}
const validators: ValidatorFn[] = [];
if (property.required) {
validators.push(Validators.required);
}
if (property.type === 'number') {
if (isDefinedAndNotNull(property.min)) {
validators.push(Validators.min(property.min));
@ -144,11 +186,37 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
}
this.scadaObjectSettingsFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false});
}
if (this.validatorTriggers.length) {
const observables: Observable<any>[] = [];
for (const trigger of this.validatorTriggers) {
observables.push(this.scadaObjectSettingsFormGroup.get(trigger).valueChanges);
}
this.validatorSubscription = merge(...observables).subscribe(() => {
this.updateValidators();
});
}
this.setupValue();
this.cd.markForCheck();
}
);
}
private updateValidators() {
for (const trigger of this.validatorTriggers) {
const value: boolean = this.scadaObjectSettingsFormGroup.get(trigger).value;
this.metadata.properties.filter(p => p.disableOnProperty === trigger).forEach(
(p) => {
const control = this.scadaObjectSettingsFormGroup.get(p.id);
if (value) {
control.enable({emitEvent: false});
} else {
control.disable({emitEvent: false});
}
}
);
}
}
private setupValue() {
if (this.metadata) {
const defaults = defaultScadaObjectSettings(this.metadata);
@ -156,6 +224,7 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
this.scadaObjectSettingsFormGroup.patchValue(
this.modelValue, {emitEvent: false}
);
this.setDisabledState(this.disabled);
}
}

View File

@ -0,0 +1,43 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ScadaObjectProperty } from '@home/components/widget/lib/scada/scada.models';
export interface ScadaPropertyRow {
label: string;
properties: ScadaObjectProperty[];
switch?: ScadaObjectProperty;
}
export const toPropertyRows = (properties: ScadaObjectProperty[]): ScadaPropertyRow[] => {
const result: ScadaPropertyRow[] = [];
for (const property of properties) {
let propertyRow = result.find(r => r.label === property.name);
if (!propertyRow) {
propertyRow = {
label: property.name,
properties: []
};
result.push(propertyRow);
}
if (property.type === 'switch') {
propertyRow.switch = property;
} else {
propertyRow.properties.push(property);
}
}
return result;
};

View File

@ -1,39 +1,160 @@
<svg width="100" height="100" version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg">
<tb:metadata>
{
<tb:metadata>{
"title": "My first SCADA Object",
"initial": [
"states": {
"initialState": {
"initial": true,
"state": [
{
"tag": "RECT",
"tag": "level",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "background"
"propertyId": "levelBackground"
}
}
]
},
{
"tag": "levelValueBackground",
"show": {
"type": "property",
"propertyId": "showValue"
}
},
{
"name": "stroke",
"value": {
"tag": "levelValue",
"show": {
"type": "property",
"propertyId": "strokeColor"
"propertyId": "showValue"
},
"text": {
"font": {
"type": "property",
"propertyId": "valueFont"
},
"color": {
"type": "property",
"propertyId": "valueColor"
}
}
},
{
"name": "stroke-width",
"tag": "minLevel",
"show": {
"type": "property",
"propertyId": "showMinMaxLevel"
},
"text": {
"content": {
"type": "property",
"propertyId": "minLevel"
},
"font": {
"type": "property",
"propertyId": "minMaxLevelFont"
},
"color": {
"type": "property",
"propertyId": "minMaxLevelColor"
}
}
},
{
"tag": "maxLevel",
"show": {
"type": "property",
"propertyId": "showMinMaxLevel"
},
"text": {
"content": {
"type": "property",
"propertyId": "maxLevel"
},
"font": {
"type": "property",
"propertyId": "minMaxLevelFont"
},
"color": {
"type": "property",
"propertyId": "minMaxLevelColor"
}
}
}
]
},
"updateLevelState": {
"state": [
{
"tag": "level",
"animate": 200,
"attributes": [
{
"name": "height",
"value": {
"type": "function",
"valueConvertFunction": "var level = (value - ${minLevel}) / (${maxLevel} - ${minLevel});\nlevel = Math.max(0, Math.min(1, level));\nreturn level*80;"
}
}
]
},
{
"tag": "levelValue",
"text": {
"content": {
"type": "valueFormat",
"units": {
"type": "property",
"propertyId": "valueUnits"
},
"decimals": {
"type": "property",
"propertyId": "valueDecimals"
}
}
}
}
]
},
"disabledState": {
"state": [
{
"tag": "level",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "strokeWidth"
"propertyId": "disabledLevelBackground"
}
}
]
}
],
]
},
"enabledState": {
"state": [
{
"tag": "level",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "levelBackground"
}
}
]
}
]
}
},
"behavior": [
{
"id": "initialState",
"name": "Initial state",
"id": "levelState",
"name": "Level",
"type": "getValue",
"valueType": "DOUBLE",
"defaultValue": 0,
@ -42,26 +163,7 @@
"matcher": {
"type": "any"
},
"state": [
{
"tag": "RECT",
"animate": 200,
"attributes": [
{
"name": "height",
"value": {
"type": "input"
}
},
{
"name": "width",
"value": {
"type": "input"
}
}
]
}
]
"stateId": "updateLevelState"
}
]
},
@ -77,73 +179,129 @@
"type": "constant",
"value": true
},
"state": [
{
"tag": "RECT",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "disabledBackground"
}
}
]
}
]
"stateId": "disabledState"
},
{
"matcher": {
"type": "constant",
"value": false
},
"state": [
{
"tag": "RECT",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "background"
}
}
]
}
]
"stateId": "enabledState"
}
]
}
],
"properties": [
{
"id": "strokeColor",
"name": "Stroke color",
"type": "color",
"default": "#aaa"
"id": "showValue",
"name": "Value",
"type": "switch",
"default": true
},
{
"id": "strokeWidth",
"name": "Stroke width",
"id": "valueUnits",
"name": "Value",
"type": "units",
"default": "",
"disableOnProperty": "showValue"
},
{
"id": "valueDecimals",
"name": "Value",
"type": "number",
"default": 5,
"default": 2,
"min": 0,
"max": 20
"max": 15,
"step": 1,
"fieldSuffix": "decimals",
"disableOnProperty": "showValue"
},
{
"id": "background",
"name": "Background",
"id": "valueFont",
"name": "Value",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showValue"
},
{
"id": "valueColor",
"name": "Value",
"type": "color",
"default": "green"
"default": "#000000",
"disableOnProperty": "showValue"
},
{
"id": "disabledBackground",
"name": "Disabled background",
"id": "minLevel",
"name": "Level range",
"subLabel": "min",
"type": "number",
"required": true,
"default": 0,
"min": 0
},
{
"id": "maxLevel",
"name": "Level range",
"subLabel": "max",
"type": "number",
"required": true,
"default": 100,
"min": 0
},
{
"id": "showMinMaxLevel",
"name": "Min/Max label",
"type": "switch",
"default": true
},
{
"id": "minMaxLevelFont",
"name": "Min/Max label",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showMinMaxLevel"
},
{
"id": "minMaxLevelColor",
"name": "Min/Max label",
"type": "color",
"default": "#666",
"disableOnProperty": "showMinMaxLevel"
},
{
"id": "levelBackground",
"name": "Level background",
"subLabel": "Enabled",
"type": "color",
"default": "#1abb48",
"divider": true
},
{
"id": "disabledLevelBackground",
"name": "Level background",
"subLabel": "Disabled",
"type": "color",
"default": "#ccc"
}
]
}
</tb:metadata>
<rect tb:tag="RECT" width="100" height="100" rx="0" fill="none" stroke="#ccc" stroke-width="2"/>
}</tb:metadata>
<rect width="100" height="100" rx="0" fill="none" stroke="#ccc" stroke-width="2"/>
<rect x="8" y="15" width="20" height="81" rx="2" fill="#ececec" stroke="#000" stroke-width="1.0359"/>
<rect transform="scale(1 -1)" x="8.5" y="-95.5" width="19" height="40" rx="1.5" fill="#1abb48" tb:tag="level"/>
<text x="32" y="95" fill="#666" font-family="Roboto" font-size="6px" tb:tag="minLevel" xml:space="preserve"><tspan x="31.354307" y="96.626251">min</tspan></text>
<rect x="10.941" y="49.544" width="14.155" height="12" rx="2.8668" fill="#fff" fill-opacity=".45148" tb:tag="levelValueBackground"/>
<text x="18" y="56" dominant-baseline="middle" fill="#000000" font-family="Roboto" font-size="6px" text-anchor="middle" tb:tag="levelValue" xml:space="preserve"><tspan>N/A</tspan></text>
<text x="32" y="20" fill="#666" font-family="Roboto" font-size="6px" tb:tag="maxLevel" xml:space="preserve"><tspan x="29.865496" y="18.716127">max</tspan></text>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB