UI: Hint for dynamic settings and refactoring flow animation

This commit is contained in:
Artem Dzhereleiko 2025-05-01 08:47:12 +03:00
parent d6fb28ebd7
commit 3066d17294
19 changed files with 357 additions and 82 deletions

View File

@ -3,7 +3,7 @@
"description": "Bottom right elbow connector", "description": "Bottom right elbow connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 1, "widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n", "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 200,100 L 125,100 Q 100,100 100,125 L 100, 200';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -140,13 +140,14 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -155,21 +156,23 @@
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -182,6 +185,7 @@
"type": "number", "type": "number",
"default": 4, "default": 4,
"subLabel": "Width", "subLabel": "Width",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"min": 1, "min": 1,
"step": 1, "step": 1,
@ -200,6 +204,7 @@
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -215,6 +220,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -250,5 +256,5 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M200 100H132C115 100 100 115 100 132V200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/> <path d="M 100,200 L 100,125 Q 100,100 125,100 L 200, 100" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -3,7 +3,7 @@
"description": "Bottom tee connector", "description": "Bottom tee connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 1, "widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}", "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst leftLineReversed = \"M 100,100 H 0\";\nconst rightLine = \"M100 100H200\";\nconst rightLineReversed = \"M 200,100 H 100\";\nconst bottomLine = \"M 100,200 V 103\";\nconst bottomLineReversed = \"M 100,103 V 200\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}\n",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -377,13 +377,14 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -392,21 +393,23 @@
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -419,6 +422,7 @@
"type": "number", "type": "number",
"default": 4, "default": 4,
"subLabel": "Width", "subLabel": "Width",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"min": 1, "min": 1,
"step": 1, "step": 1,
@ -437,6 +441,7 @@
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -452,6 +457,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -3,7 +3,7 @@
"description": "Cross connector", "description": "Cross connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 1, "widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}", "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst leftLineReversed = \"M 100,100 H 0\";\nconst topLine = \"M100 97L100 0\";\nconst topLineReversed = \"M 100,0 V 97\";\nconst rightLine = \"M100 100H200\";\nconst rightLineReversed = \"M 200,100 H 100\";\nconst bottomLine = \"M 100,200 V 103\";\nconst bottomLineReversed = \"M 100,103 V 200\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -493,13 +493,14 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -508,21 +509,23 @@
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -535,6 +538,7 @@
"type": "number", "type": "number",
"default": 4, "default": 4,
"subLabel": "Width", "subLabel": "Width",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"min": 1, "min": 1,
"step": 1, "step": 1,
@ -553,6 +557,7 @@
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -568,6 +573,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -3,7 +3,7 @@
"description": "Horizontal connector with an optional directional arrow to visually indicate flow.", "description": "Horizontal connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 1, "widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n", "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 200,100 H 0';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n\n",
"tags": [ "tags": [
{ {
"tag": "arrow", "tag": "arrow",
@ -176,13 +176,14 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -191,21 +192,23 @@
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -229,11 +232,14 @@
"name": "{i18n:scada.symbol.flow}", "name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "color", "type": "color",
"default": "#C8DFF7" "default": "#C8DFF7",
"disabled": false,
"visible": true
}, },
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -249,6 +255,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@ -3,7 +3,7 @@
"description": "Left bottom elbow connector", "description": "Left bottom elbow connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 1, "widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n", "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,200 L 100,125 Q 100,100 75,100 L 0, 100';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -140,13 +140,14 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -155,21 +156,24 @@
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": "mainLine",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -182,6 +186,7 @@
"type": "number", "type": "number",
"default": 4, "default": 4,
"subLabel": "Width", "subLabel": "Width",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"min": 1, "min": 1,
"step": 1, "step": 1,
@ -193,11 +198,14 @@
"name": "{i18n:scada.symbol.flow}", "name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "color", "type": "color",
"default": "#C8DFF7" "default": "#C8DFF7",
"disabled": false,
"visible": true
}, },
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -213,6 +221,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -248,5 +257,5 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M100 200L100 131C100 113.879 86.1208 100 69 100L0 100" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/> <path d="M 0,100 L 75,100 Q 100,100 100,125 L 100, 200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -3,7 +3,7 @@
"description": "Left tee connector", "description": "Left tee connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 1, "widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H97\";\nconst topLine = \"M100 100L100 0\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}", "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H97\";\nconst leftLineReversed = \"M 97,100 H 0\";\nconst topLine = \"M100 100L100 0\";\nconst topLineReversed = \"M 100,0 V 100\";\nconst bottomLine = \"M 100,200 V 100\";\nconst bottomLineReversed = \"M 100,100 V 200\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -377,13 +377,14 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -392,21 +393,23 @@
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -419,6 +422,7 @@
"type": "number", "type": "number",
"default": 4, "default": 4,
"subLabel": "Width", "subLabel": "Width",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"min": 1, "min": 1,
"step": 1, "step": 1,
@ -437,6 +441,7 @@
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -452,6 +457,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -487,5 +493,5 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M100 0V200" stroke="#1A1A1A" stroke-width="6" id="path6" tb:tag="line"/><path d="M100 113V87C100 87 100 100 86 100C100 100 100 113 100 113Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="2" id="path8" tb:tag="line-color"/><path d="M0 100H85C93.2843 100 100 93.2843 100 85V0" stroke="#1A1A1A" stroke-width="6" id="path4" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 106.716 100 115V200" stroke="#1A1A1A" stroke-width="6" id="path2" tb:tag="line"/><g tb:tag="animationGroup"><g tb:tag="leftLine"/><g tb:tag="topLine"/><g tb:tag="bottomLine"/></g> <path d="M100 0V200" stroke="#1A1A1A" stroke-width="6" id="path6" tb:tag="line"/><path d="M100 113V87C100 87 100 100 86 100C100 100 100 113 100 113Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="2" id="path8" tb:tag="line-color"/><path d="M0 100H85C93.2843 100 100 93.2843 100 85V0" stroke="#1A1A1A" stroke-width="6" id="path4" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 106.716 100 115V200" stroke="#1A1A1A" stroke-width="6" id="path2" tb:tag="line"/><g tb:tag="animationGroup"><g tb:tag="leftLine"/><g tb:tag="topLine"> </g><g tb:tag="bottomLine"> </g></g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -3,7 +3,7 @@
"description": "Left top elbow connector", "description": "Left top elbow connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 1, "widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n", "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,0 L 100,75 Q 100,100 75,100 L 0, 100';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -140,13 +140,14 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -155,21 +156,23 @@
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -182,6 +185,7 @@
"type": "number", "type": "number",
"default": 4, "default": 4,
"subLabel": "Width", "subLabel": "Width",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"min": 1, "min": 1,
"step": 1, "step": 1,
@ -200,6 +204,7 @@
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -215,6 +220,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -250,5 +256,5 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M0 100H69C86.1208 100 100 86.1208 100 69V0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/> <path d="M 0,100 L 75,100 Q 100,100 100,75 L 100, 0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -1,10 +1,9 @@
<svg width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"><tb:metadata xmlns=""><![CDATA[{
<tb:metadata><![CDATA[{
"title": "HP Long horizontal connector", "title": "HP Long horizontal connector",
"description": "Long horizontal connector with an optional directional arrow to visually indicate flow.", "description": "Long horizontal connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 2, "widgetSizeX": 2,
"widgetSizeY": 1, "widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n", "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 400,100 H 0';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [ "tags": [
{ {
"tag": "arrow", "tag": "arrow",
@ -177,27 +176,30 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -206,7 +208,7 @@
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -219,6 +221,7 @@
"type": "number", "type": "number",
"default": 4, "default": 4,
"subLabel": "Width", "subLabel": "Width",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"min": 1, "min": 1,
"step": 1, "step": 1,
@ -230,11 +233,14 @@
"name": "{i18n:scada.symbol.flow}", "name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "color", "type": "color",
"default": "#C8DFF7" "default": "#C8DFF7",
"disabled": false,
"visible": true
}, },
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -250,6 +256,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -285,5 +292,5 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="m0 100h400" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m229 100-58 29v-58z" fill="#1a1a1a" tb:tag="arrow"/><g tb:tag="animationGroup"/> <path d="M0 100H400" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m229 100-58 29v-58z" fill="#1a1a1a" tb:tag="arrow"/><g tb:tag="animationGroup"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@ -3,7 +3,7 @@
"description": "Long vertical connector with an optional directional arrow to visually indicate flow.", "description": "Long vertical connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 2, "widgetSizeY": 2,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n", "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,0 V 400';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n\n",
"tags": [ "tags": [
{ {
"tag": "arrow", "tag": "arrow",
@ -176,13 +176,14 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -191,21 +192,23 @@
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -218,6 +221,7 @@
"type": "number", "type": "number",
"default": 4, "default": 4,
"subLabel": "Width", "subLabel": "Width",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"min": 1, "min": 1,
"step": 1, "step": 1,
@ -229,11 +233,14 @@
"name": "{i18n:scada.symbol.flow}", "name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "color", "type": "color",
"default": "#C8DFF7" "default": "#C8DFF7",
"disabled": false,
"visible": true
}, },
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -249,6 +256,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -284,5 +292,5 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="m100 400v-400" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m100 171 29 58h-58l29-58z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/> <path d="M 100,400 V 0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m100 171 29 58h-58l29-58z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@ -3,7 +3,7 @@
"description": "Right tee connector", "description": "Right tee connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 1, "widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst topLine = \"M100 100L100 0\";\nconst rightLine = \"M103 100H200\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}", "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst topLine = \"M100 100L100 0\";\nconst topLineReversed = \"M 100,0 V 100\";\nconst rightLine = \"M103 100H200\";\nconst rightLineReversed = \"M 200,100 H 103\";\nconst bottomLine = \"M 100,200 V 100\";\nconst bottomLineReversed = \"M 100,100 V 200\";\n\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -377,13 +377,14 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -392,21 +393,23 @@
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -419,6 +422,7 @@
"type": "number", "type": "number",
"default": 4, "default": 4,
"subLabel": "Width", "subLabel": "Width",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"min": 1, "min": 1,
"step": 1, "step": 1,
@ -437,6 +441,7 @@
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -452,6 +457,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -3,7 +3,7 @@
"description": "Top right elbow connector", "description": "Top right elbow connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 1, "widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n", "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 200,100 L 125,100 Q 100,100 100,75 L 100, 0';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -140,13 +140,14 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -155,21 +156,23 @@
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -182,6 +185,7 @@
"type": "number", "type": "number",
"default": 4, "default": 4,
"subLabel": "Width", "subLabel": "Width",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"min": 1, "min": 1,
"step": 1, "step": 1,
@ -200,6 +204,7 @@
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -215,6 +220,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -250,5 +256,5 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M100 0V69C100 86.1208 113.879 100 131 100H200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/> <path d="M 100,0 L 100,75 Q 100,100 125,100 L 200, 100" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -3,7 +3,7 @@
"description": "Top tee connector", "description": "Top tee connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 1, "widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}", "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst leftLineReversed = \"M 100,100 H 0\";\nconst topLine = \"M100 97L100 0\";\nconst topLineReversed = \"M 100,0 V 97\";\nconst rightLine = \"M100 100H200\";\nconst rightLineReversed = \"M 200,100 H 100\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -377,13 +377,14 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -392,21 +393,23 @@
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -419,6 +422,7 @@
"type": "number", "type": "number",
"default": 4, "default": 4,
"subLabel": "Width", "subLabel": "Width",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"min": 1, "min": 1,
"step": 1, "step": 1,
@ -437,6 +441,7 @@
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -452,6 +457,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -3,7 +3,7 @@
"description": "Vertical connector with an optional directional arrow to visually indicate flow.", "description": "Vertical connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 1, "widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n", "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,0 V 200';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [ "tags": [
{ {
"tag": "arrow", "tag": "arrow",
@ -176,13 +176,14 @@
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
@ -191,21 +192,23 @@
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}", "name": "{i18n:scada.symbol.main-line}",
"type": "number", "type": "number",
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return !model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false, "disabled": false,
"visible": true "visible": false
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.main-line}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"disabled": false, "disabled": false,
@ -218,6 +221,7 @@
"type": "number", "type": "number",
"default": 4, "default": 4,
"subLabel": "Width", "subLabel": "Width",
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"min": 1, "min": 1,
"step": 1, "step": 1,
@ -229,11 +233,14 @@
"name": "{i18n:scada.symbol.flow}", "name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "color", "type": "color",
"default": "#C8DFF7" "default": "#C8DFF7",
"disabled": false,
"visible": true
}, },
{ {
"id": "flowStyleDash", "id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -249,6 +256,7 @@
{ {
"id": "flowStyleGap", "id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}", "name": "{i18n:scada.symbol.flow-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.animation}", "group": "{i18n:scada.symbol.animation}",
"type": "number", "type": "number",
"default": 10, "default": 10,
@ -284,5 +292,5 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M100 200L100 0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="M100 71L129 129H71L100 71Z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/> <path d="M 100,200 V 0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="M100 71L129 129H71L100 71Z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@ -83,6 +83,10 @@ export interface ScadaSymbolApi {
cssAnimation: (element: Element) => ScadaSymbolAnimation | undefined; cssAnimation: (element: Element) => ScadaSymbolAnimation | undefined;
resetCssAnimation: (element: Element) => void; resetCssAnimation: (element: Element) => void;
finishCssAnimation: (element: Element) => void; finishCssAnimation: (element: Element) => void;
connectorAnimation:(element: Element) => ConnectorScadaSymbolAnimation | undefined;
connectorAnimate:(element: Element) => ConnectorScadaSymbolAnimation;
resetConnectorAnimation: (element: Element) => void;
finishConnectorAnimation: (element: Element) => void;
disable: (element: Element | Element[]) => void; disable: (element: Element | Element[]) => void;
enable: (element: Element | Element[]) => void; enable: (element: Element | Element[]) => void;
callAction: (event: Event, behaviorId: string, value?: any, observer?: Partial<Observer<void>>) => void; callAction: (event: Event, behaviorId: string, value?: any, observer?: Partial<Observer<void>>) => void;
@ -186,6 +190,8 @@ const tbNamespaceRegex = /<svg.*(xmlns:tb="https:\/\/thingsboard.io\/svg").*>/gm
const tbTagRegex = /tb:tag="([^"]*)"/gms; const tbTagRegex = /tb:tag="([^"]*)"/gms;
let syncTime = Date.now();
const generateElementId = () => { const generateElementId = () => {
const id = guid(); const id = guid();
const firstChar = id.charAt(0); const firstChar = id.charAt(0);
@ -485,6 +491,7 @@ export class ScadaSymbolObject {
private settings: ScadaSymbolObjectSettings; private settings: ScadaSymbolObjectSettings;
private context: ScadaSymbolContext; private context: ScadaSymbolContext;
private cssAnimations: CssScadaSymbolAnimations; private cssAnimations: CssScadaSymbolAnimations;
private connectorAnimations: ScadaSymbolFlowConnectorAnimations;
private svgShape: Svg; private svgShape: Svg;
private box: Box; private box: Box;
@ -604,6 +611,7 @@ export class ScadaSymbolObject {
private init() { private init() {
this.cssAnimations = new CssScadaSymbolAnimations(this.svgShape, this.raf); this.cssAnimations = new CssScadaSymbolAnimations(this.svgShape, this.raf);
this.connectorAnimations = new ScadaSymbolFlowConnectorAnimations();
this.context = { this.context = {
api: { api: {
generateElementId: () => generateElementId(), generateElementId: () => generateElementId(),
@ -615,6 +623,10 @@ export class ScadaSymbolObject {
cssAnimation: this.cssAnimation.bind(this), cssAnimation: this.cssAnimation.bind(this),
resetCssAnimation: this.resetCssAnimation.bind(this), resetCssAnimation: this.resetCssAnimation.bind(this),
finishCssAnimation: this.finishCssAnimation.bind(this), finishCssAnimation: this.finishCssAnimation.bind(this),
connectorAnimation: this.connectorAnimation.bind(this),
connectorAnimate: this.connectorAnimate.bind(this),
resetConnectorAnimation: this.resetConnectorAnimation.bind(this),
finishConnectorAnimation: this.finishConnectorAnimation.bind(this),
disable: this.disableElement.bind(this), disable: this.disableElement.bind(this),
enable: this.enableElement.bind(this), enable: this.enableElement.bind(this),
callAction: this.callAction.bind(this), callAction: this.callAction.bind(this),
@ -959,6 +971,22 @@ export class ScadaSymbolObject {
this.cssAnimations.finishAnimation(element); this.cssAnimations.finishAnimation(element);
} }
private connectorAnimate(element: Element, path: string, reversedPath: string): ConnectorScadaSymbolAnimation {
return this.connectorAnimations.animate(element, path, reversedPath);
}
private connectorAnimation(element: Element): ConnectorScadaSymbolAnimation | undefined {
return this.connectorAnimations.animation(element);
}
private resetConnectorAnimation(element: Element) {
this.connectorAnimations.resetAnimation(element);
}
private finishConnectorAnimation(element: Element) {
this.connectorAnimations.finishAnimation(element);
}
private disableElement(e: Element | Element[]) { private disableElement(e: Element | Element[]) {
this.elements(e).forEach(element => { this.elements(e).forEach(element => {
element.attr({'pointer-events': 'none'}); element.attr({'pointer-events': 'none'});
@ -1108,6 +1136,20 @@ interface ScadaSymbolAnimation {
} }
const scadaSymbolConnectorFlowAnimationId = 'scadaSymbolConnectorFlowAnimation';
type StrokeLineCap = 'butt' | 'round '| 'square';
interface ConnectorScadaSymbolAnimation {
play(): void;
stop(): void;
finish(): void;
flowAppearance(width: number, color: string, lineCap: StrokeLineCap, dashWidth: number, dashGap: number): ConnectorScadaSymbolAnimation;
duration(speed: number): ConnectorScadaSymbolAnimation;
direction(direction: boolean): ConnectorScadaSymbolAnimation;
}
class CssScadaSymbolAnimations { class CssScadaSymbolAnimations {
constructor(private svgShape: Svg, constructor(private svgShape: Svg,
private raf: RafService) {} private raf: RafService) {}
@ -1159,6 +1201,135 @@ class CssScadaSymbolAnimations {
} }
} }
class ScadaSymbolFlowConnectorAnimations {
constructor() {}
public animate(element: Element, path = '', reversedPath = ''): ConnectorScadaSymbolAnimation {
this.checkOldAnimation(element);
return this.setupAnimation(element, this.createAnimation(element, path, reversedPath));
}
public animation(element: Element): ConnectorScadaSymbolAnimation | undefined {
return element.remember(scadaSymbolConnectorFlowAnimationId);
}
public resetAnimation(element: Element) {
const animation: ConnectorScadaSymbolAnimation = element.remember(scadaSymbolConnectorFlowAnimationId);
if (animation) {
animation.stop();
element.remember(scadaSymbolConnectorFlowAnimationId, null);
}
}
public finishAnimation(element: Element) {
const animation: ConnectorScadaSymbolAnimation = element.remember(scadaSymbolConnectorFlowAnimationId);
if (animation) {
animation.finish();
element.remember(scadaSymbolConnectorFlowAnimationId, null);
}
}
private setupAnimation(element: Element, animation: ConnectorScadaSymbolAnimation): ConnectorScadaSymbolAnimation {
element.remember(scadaSymbolConnectorFlowAnimationId, animation);
return animation;
}
private checkOldAnimation(element: Element) {
const previousAnimation: ConnectorScadaSymbolAnimation = element.remember(scadaSymbolConnectorFlowAnimationId);
if (previousAnimation) {
previousAnimation.finish();
}
}
private createAnimation(element: Element, path: string, reversedPath: string): ConnectorScadaSymbolAnimation {
return new FlowConnectorAnimation(element, path, reversedPath);
}
}
class FlowConnectorAnimation implements ConnectorScadaSymbolAnimation {
private readonly _path: string;
private readonly _reversedPath: string;
private readonly _animation: Element;
private _duration: number = 1;
private _lineColor: string = '#C8DFF7';
private _lineWidth: number = 4;
private _strokeLineCap: StrokeLineCap = 'butt';
private _dashWidth: number = 10;
private _dashGap: number = 10;
private _direction: boolean = true;
constructor(private element: Element,
path: string,
pathReversed: string) {
this._path = path;
this._reversedPath = pathReversed;
const dashArray = `${this._dashWidth} ${this._dashGap}`;
const values = `${this._dashWidth + this._dashGap};0`;
this._animation = SVG(
`<path d="${this._path}" stroke-dasharray="${dashArray}" stroke-linecap="${this._strokeLineCap}" fill="none" stroke="${this._lineColor}" stroke-width="${this._lineWidth}">` +
`<animate attributeName="stroke-dashoffset" values="${values}" dur="${this._duration}s" begin="indefinite" calcMode="linear" repeatCount="indefinite"></animate></path>`
);
}
public play() {
if (!this.element.node.childElementCount) {
this.element.add(this._animation);
}
if (!syncTime) {
syncTime = Date.now();
}
const animateElement = this.element.node.getElementsByTagName('animate')[0];
const offset = ((Date.now() - syncTime) % 1000) * -1;
(animateElement as SVGAnimationElement).beginElementAt(offset);
}
public stop() {
const animateElement = this.element.node.getElementsByTagName('animate')[0];
(animateElement as SVGAnimationElement)?.endElement();
}
public finish() {
this.element.findOne('path')?.remove();
}
public flowAppearance(width: number, color: string, linecap: StrokeLineCap, dashWidth: number, dashGap: number): this {
const totalLength = (this._animation.node as SVGPathElement).getTotalLength();
let offset = 0;
if ((totalLength % 100) !== 0) {
const clientWidth = totalLength < 100 ? 100 : this.element.node.ownerSVGElement.clientWidth;
const clientWidthDash = clientWidth / (dashWidth + dashGap);
const totalLengthDash = totalLength / clientWidthDash;
offset = ((dashWidth + dashGap) - totalLengthDash) / 2;
}
this._lineColor = color;
this._lineWidth = width;
this._strokeLineCap = linecap;
this._dashWidth = dashWidth - offset;
this._dashGap = dashGap - offset;
const dashArray = `${this._dashWidth}${this._dashGap ? ` ${this._dashGap}` : ''}`;
const values = `${this._dashWidth + (this._dashGap || this._dashWidth)};0`;
this._animation.stroke({width, color, linecap, dasharray: dashArray});
this._animation.findOne('animate').attr('values', values);
return this;
}
public duration(speed: number): this {
this._duration = speed;
this._animation.findOne('animate').attr('dur', `${speed}s`);
return this;
}
public direction(direction: boolean): this {
this._direction = direction;
this._animation.attr('d', direction ? this._path : this._reversedPath);
return this;
}
}
interface ScadaSymbolAnimationKeyframe { interface ScadaSymbolAnimationKeyframe {
stop: string; stop: string;
style: any; style: any;

View File

@ -30,6 +30,12 @@
<input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}"> <input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.hint</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="hint" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.group-title</div> <div class="fixed-title-width" translate>dynamic-form.property.group-title</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">

View File

@ -104,6 +104,7 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
{ {
id: [this.property.id, [Validators.required]], id: [this.property.id, [Validators.required]],
name: [this.property.name, [Validators.required]], name: [this.property.name, [Validators.required]],
hint: [this.property.hint, []],
group: [this.property.group, []], group: [this.property.group, []],
type: [this.property.type, [Validators.required]], type: [this.property.type, [Validators.required]],
arrayItemType: [this.property.arrayItemType, [Validators.required]], arrayItemType: [this.property.arrayItemType, [Validators.required]],

View File

@ -91,9 +91,15 @@
<div [formGroup]="propertiesFormGroup" class="tb-form-row space-between overflow-auto" [class]="propertyRow.rowClass"> <div [formGroup]="propertiesFormGroup" class="tb-form-row space-between overflow-auto" [class]="propertyRow.rowClass">
<mat-slide-toggle *ngIf="propertyRow.switch && propertyRow.switch.visible" <mat-slide-toggle *ngIf="propertyRow.switch && propertyRow.switch.visible"
class="mat-slide fixed-title-width margin" formControlName="{{ propertyRow.switch.id }}"> class="mat-slide fixed-title-width margin" formControlName="{{ propertyRow.switch.id }}">
{{ propertyRow.label | customTranslate }} <div tb-hint-tooltip-icon="{{ propertyRow.hint | customTranslate }}">
{{ propertyRow.label | customTranslate }}
</div>
</mat-slide-toggle> </mat-slide-toggle>
<div *ngIf="!propertyRow.switch" class="fixed-title-width fixed-title-height">{{ propertyRow.label | customTranslate }}</div> <div *ngIf="!propertyRow.switch" class="fixed-title-width fixed-title-height">
<div tb-hint-tooltip-icon="{{ propertyRow.hint | customTranslate }}">
{{ propertyRow.label | customTranslate }}
</div>
</div>
<div *ngIf="propertyRow.properties.length" class="tb-flex" [class]="propertyRow.propertiesRowClass"> <div *ngIf="propertyRow.properties.length" class="tb-flex" [class]="propertyRow.propertiesRowClass">
<ng-container *ngFor="let property of propertyRow.properties"> <ng-container *ngFor="let property of propertyRow.properties">
<ng-container *ngIf="property.visible"> <ng-container *ngIf="property.visible">

View File

@ -87,6 +87,7 @@ export type PropertyConditionFunction = (property: FormProperty, model: any) =>
export interface FormPropertyBase { export interface FormPropertyBase {
id: string; id: string;
name: string; name: string;
hint?: string;
group?: string; group?: string;
type: FormPropertyType; type: FormPropertyType;
default: any; default: any;
@ -237,6 +238,7 @@ export interface FormPropertyContainerBase {
} }
export interface FormPropertyRow extends FormPropertyContainerBase { export interface FormPropertyRow extends FormPropertyContainerBase {
hint?: string;
properties?: FormProperty[]; properties?: FormProperty[];
switch?: FormProperty; switch?: FormProperty;
rowClass?: string; rowClass?: string;
@ -362,6 +364,7 @@ const toPropertyContainers = (properties: FormProperty[],
if (!propertyRow) { if (!propertyRow) {
propertyRow = { propertyRow = {
label: property.name, label: property.name,
hint: property.hint,
type: FormPropertyContainerType.row, type: FormPropertyContainerType.row,
properties: [], properties: [],
rowClass: property.rowClass, rowClass: property.rowClass,

View File

@ -3446,6 +3446,7 @@
"flow-animation-hint": "Indicates whether animation is present in connector.", "flow-animation-hint": "Indicates whether animation is present in connector.",
"flow": "Flow", "flow": "Flow",
"flow-style": "Flow style", "flow-style": "Flow style",
"flow-style-hint": "Set the Dash and Gap values so that their sum is divisible by 100 without a remainder for perfect animation synchronization.",
"flow-dash-cap": "Flow dash cap", "flow-dash-cap": "Flow dash cap",
"dash-cap-butt": "Butt", "dash-cap-butt": "Butt",
"dash-cap-round": "Round", "dash-cap-round": "Round",