@record-evolution/widget-scada 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/scada_symbols/3-phase-voltage-relay-hp.svg +549 -0
- package/dist/scada_symbols/apartments-hp.svg +344 -0
- package/dist/scada_symbols/battery-hp.svg +473 -0
- package/dist/scada_symbols/bottom-flow-meter.svg +933 -0
- package/dist/scada_symbols/bottom-light-bulb-hp.svg +346 -0
- package/dist/scada_symbols/bottom-right-elbow-connector-hp.svg +233 -0
- package/dist/scada_symbols/bottom-right-elbow-pipe.svg +532 -0
- package/dist/scada_symbols/bottom-tee-connector-hp.svg +470 -0
- package/dist/scada_symbols/bottom-tee-pipe.svg +857 -0
- package/dist/scada_symbols/centrifugal-pump.svg +626 -0
- package/dist/scada_symbols/conical-tank.svg +1136 -0
- package/dist/scada_symbols/consumers-hp.svg +346 -0
- package/dist/scada_symbols/control-panel-hp.svg +332 -0
- package/dist/scada_symbols/crane-hp.svg +346 -0
- package/dist/scada_symbols/cross-connector-hp.svg +586 -0
- package/dist/scada_symbols/cross-pipe.svg +1051 -0
- package/dist/scada_symbols/curcuit-breaker-hp.svg +444 -0
- package/dist/scada_symbols/cylindrical-tank.svg +1332 -0
- package/dist/scada_symbols/drawwork-hp.svg +355 -0
- package/dist/scada_symbols/drill-hp.svg +354 -0
- package/dist/scada_symbols/drilling-line-hp.svg +361 -0
- package/dist/scada_symbols/drilling-rig-hp.svg +354 -0
- package/dist/scada_symbols/dynamic-horizontal-scale-hp.svg +605 -0
- package/dist/scada_symbols/dynamic-vertical-scale-hp.svg +598 -0
- package/dist/scada_symbols/electrical-distribution-board-hp.svg +332 -0
- package/dist/scada_symbols/electrical-engine-hp.svg +360 -0
- package/dist/scada_symbols/elevated-tank.svg +1564 -0
- package/dist/scada_symbols/energy-meter-hp.svg +488 -0
- package/dist/scada_symbols/extra-long-horizontal-pipe.svg +426 -0
- package/dist/scada_symbols/extra-long-vertical-pipe.svg +438 -0
- package/dist/scada_symbols/filter-hp.svg +287 -0
- package/dist/scada_symbols/four-rate-energy-meter-hp.svg +891 -0
- package/dist/scada_symbols/fuel-generator-hp.svg +359 -0
- package/dist/scada_symbols/gas-preventer-hp.svg +361 -0
- package/dist/scada_symbols/gas-wellhead-hp.svg +363 -0
- package/dist/scada_symbols/heat-exchanger-hp.svg +363 -0
- package/dist/scada_symbols/heat-pump-hp.svg +532 -0
- package/dist/scada_symbols/high-voltage-tower-hp.svg +297 -0
- package/dist/scada_symbols/hook-hp.svg +293 -0
- package/dist/scada_symbols/horizontal-ball-valve.svg +271 -0
- package/dist/scada_symbols/horizontal-broken-pipe.svg +97 -0
- package/dist/scada_symbols/horizontal-connector-hp.svg +268 -0
- package/dist/scada_symbols/horizontal-curcuit-breaker-hp.svg +439 -0
- package/dist/scada_symbols/horizontal-energy-system-controller-hp.svg +378 -0
- package/dist/scada_symbols/horizontal-inline-flow-meter.svg +913 -0
- package/dist/scada_symbols/horizontal-pipe.svg +422 -0
- package/dist/scada_symbols/horizontal-tank-hp.svg +524 -0
- package/dist/scada_symbols/horizontal-tank.svg +1343 -0
- package/dist/scada_symbols/horizontal-valve-hp.svg +392 -0
- package/dist/scada_symbols/horizontal-wheel-valve.svg +300 -0
- package/dist/scada_symbols/house-hp.svg +349 -0
- package/dist/scada_symbols/index.json +702 -0
- package/dist/scada_symbols/industrial-fuel-generator-hp.svg +358 -0
- package/dist/scada_symbols/inverter-hp.svg +578 -0
- package/dist/scada_symbols/large-conical-tank.svg +1146 -0
- package/dist/scada_symbols/large-cylindrical-tank.svg +1323 -0
- package/dist/scada_symbols/large-horizontal-separator-connector-hp.svg +342 -0
- package/dist/scada_symbols/large-horizontal-separator-hp.svg +350 -0
- package/dist/scada_symbols/large-inverter-hp.svg +578 -0
- package/dist/scada_symbols/large-stand-cylindrical-tank.svg +1384 -0
- package/dist/scada_symbols/large-stand-vertical-tank.svg +1386 -0
- package/dist/scada_symbols/large-vertical-separator-connector-hp.svg +350 -0
- package/dist/scada_symbols/large-vertical-separator-hp.svg +350 -0
- package/dist/scada_symbols/large-vertical-tank.svg +1325 -0
- package/dist/scada_symbols/leak-sensor.svg +158 -0
- package/dist/scada_symbols/left-analog-water-level-meter.svg +748 -0
- package/dist/scada_symbols/left-bottom-elbow-connector-hp.svg +233 -0
- package/dist/scada_symbols/left-bottom-elbow-pipe.svg +532 -0
- package/dist/scada_symbols/left-drain-pipe.svg +166 -0
- package/dist/scada_symbols/left-elbow-drain-pipe.svg +181 -0
- package/dist/scada_symbols/left-flow-meter.svg +911 -0
- package/dist/scada_symbols/left-heat-pump.svg +939 -0
- package/dist/scada_symbols/left-motor-pump.svg +541 -0
- package/dist/scada_symbols/left-tee-connector-hp.svg +470 -0
- package/dist/scada_symbols/left-tee-pipe.svg +869 -0
- package/dist/scada_symbols/left-top-elbow-connector-hp.svg +233 -0
- package/dist/scada_symbols/left-top-elbow-pipe.svg +532 -0
- package/dist/scada_symbols/long-bottom-filter.svg +88 -0
- package/dist/scada_symbols/long-horizontal-broken-pipe.svg +97 -0
- package/dist/scada_symbols/long-horizontal-connector-hp.svg +269 -0
- package/dist/scada_symbols/long-horizontal-pipe.svg +488 -0
- package/dist/scada_symbols/long-top-filter.svg +88 -0
- package/dist/scada_symbols/long-vertical-broken-pipe.svg +97 -0
- package/dist/scada_symbols/long-vertical-connector-hp.svg +267 -0
- package/dist/scada_symbols/long-vertical-pipe.svg +485 -0
- package/dist/scada_symbols/low-voltage-tower-hp.svg +278 -0
- package/dist/scada_symbols/low-voltage-transformer-tower-hp.svg +334 -0
- package/dist/scada_symbols/manufacture-hp.svg +344 -0
- package/dist/scada_symbols/meter.svg +751 -0
- package/dist/scada_symbols/oil-pump-hp.svg +357 -0
- package/dist/scada_symbols/platform-hp.svg +267 -0
- package/dist/scada_symbols/pool-hp.svg +523 -0
- package/dist/scada_symbols/pool.svg +998 -0
- package/dist/scada_symbols/power-socket-hp.svg +371 -0
- package/dist/scada_symbols/power-transformer-hp.svg +359 -0
- package/dist/scada_symbols/preventer-hp.svg +343 -0
- package/dist/scada_symbols/pump-hp.svg +368 -0
- package/dist/scada_symbols/right-analog-water-level-meter.svg +748 -0
- package/dist/scada_symbols/right-drain-pipe.svg +167 -0
- package/dist/scada_symbols/right-elbow-drain-pipe.svg +178 -0
- package/dist/scada_symbols/right-flow-meter.svg +911 -0
- package/dist/scada_symbols/right-heat-pump.svg +948 -0
- package/dist/scada_symbols/right-motor-pump.svg +541 -0
- package/dist/scada_symbols/right-tee-connector-hp.svg +470 -0
- package/dist/scada_symbols/right-tee-pipe.svg +857 -0
- package/dist/scada_symbols/rotor-hp.svg +346 -0
- package/dist/scada_symbols/sand-filter-hp.svg +652 -0
- package/dist/scada_symbols/sand-filter.svg +704 -0
- package/dist/scada_symbols/short-bottom-filter.svg +88 -0
- package/dist/scada_symbols/short-left-drain-pipe.svg +160 -0
- package/dist/scada_symbols/short-right-drain-pipe.svg +160 -0
- package/dist/scada_symbols/short-top-filter.svg +88 -0
- package/dist/scada_symbols/short-vertical-tank-hp.svg +522 -0
- package/dist/scada_symbols/simple-horizontal-scale-hp.svg +509 -0
- package/dist/scada_symbols/simple-vertical-scale-hp.svg +509 -0
- package/dist/scada_symbols/single-key-switch-hp.svg +373 -0
- package/dist/scada_symbols/small-cylindrical-tank.svg +1306 -0
- package/dist/scada_symbols/small-horizontal-separator-connector-hp.svg +342 -0
- package/dist/scada_symbols/small-horizontal-separator-hp.svg +342 -0
- package/dist/scada_symbols/small-left-meter.svg +725 -0
- package/dist/scada_symbols/small-left-motor-pump.svg +443 -0
- package/dist/scada_symbols/small-meter.svg +694 -0
- package/dist/scada_symbols/small-power-transformer-hp.svg +359 -0
- package/dist/scada_symbols/small-right-center.svg +725 -0
- package/dist/scada_symbols/small-right-motor-pump.svg +443 -0
- package/dist/scada_symbols/small-spherical-tank.svg +1362 -0
- package/dist/scada_symbols/small-vertical-separator-connector-hp.svg +342 -0
- package/dist/scada_symbols/small-vertical-separator-hp.svg +342 -0
- package/dist/scada_symbols/solar-panel-hp.svg +353 -0
- package/dist/scada_symbols/spherical-tank.svg +1392 -0
- package/dist/scada_symbols/stand-cylindrical-tank.svg +1393 -0
- package/dist/scada_symbols/stand-horizontal-tank.svg +1404 -0
- package/dist/scada_symbols/stand-solar-panel-hp.svg +357 -0
- package/dist/scada_symbols/stand-vertical-short-tank.svg +1366 -0
- package/dist/scada_symbols/stand-vertical-tank.svg +1386 -0
- package/dist/scada_symbols/three-rate-energy-meter-hp.svg +759 -0
- package/dist/scada_symbols/top-flow-meter.svg +927 -0
- package/dist/scada_symbols/top-light-bulb-hp.svg +346 -0
- package/dist/scada_symbols/top-right-elbow-connector-hp.svg +233 -0
- package/dist/scada_symbols/top-right-elbow-pipe.svg +532 -0
- package/dist/scada_symbols/top-tee-connector-hp.svg +470 -0
- package/dist/scada_symbols/top-tee-pipe.svg +857 -0
- package/dist/scada_symbols/turbine-hp.svg +377 -0
- package/dist/scada_symbols/two-key-switch-hp.svg +489 -0
- package/dist/scada_symbols/two-rate-energy-meter-hp.svg +627 -0
- package/dist/scada_symbols/vertical-ball-valve.svg +271 -0
- package/dist/scada_symbols/vertical-broken-pipe.svg +97 -0
- package/dist/scada_symbols/vertical-connector-hp.svg +269 -0
- package/dist/scada_symbols/vertical-energy-system-controller-hp.svg +378 -0
- package/dist/scada_symbols/vertical-inline-flow-meter.svg +913 -0
- package/dist/scada_symbols/vertical-pipe.svg +422 -0
- package/dist/scada_symbols/vertical-short-tank.svg +1305 -0
- package/dist/scada_symbols/vertical-tank-hp.svg +524 -0
- package/dist/scada_symbols/vertical-tank.svg +1326 -0
- package/dist/scada_symbols/vertical-valve-hp.svg +392 -0
- package/dist/scada_symbols/vertical-wheel-valve.svg +300 -0
- package/dist/scada_symbols/voltage-relay-hp.svg +440 -0
- package/dist/scada_symbols/voltage-stabilizer-hp.svg +584 -0
- package/dist/scada_symbols/waterstop.svg +276 -0
- package/dist/scada_symbols/wind-turbine-cluster-hp.svg +370 -0
- package/dist/scada_symbols/wind-turbine-hp.svg +360 -0
- package/dist/widget-scada.js +1331 -0
- package/dist/widget-scada.js.map +1 -0
- package/package.json +61 -0
- package/scada_symbols/3-phase-voltage-relay-hp.svg +549 -0
- package/scada_symbols/apartments-hp.svg +344 -0
- package/scada_symbols/battery-hp.svg +473 -0
- package/scada_symbols/bottom-flow-meter.svg +933 -0
- package/scada_symbols/bottom-light-bulb-hp.svg +346 -0
- package/scada_symbols/bottom-right-elbow-connector-hp.svg +233 -0
- package/scada_symbols/bottom-right-elbow-pipe.svg +532 -0
- package/scada_symbols/bottom-tee-connector-hp.svg +470 -0
- package/scada_symbols/bottom-tee-pipe.svg +857 -0
- package/scada_symbols/centrifugal-pump.svg +626 -0
- package/scada_symbols/conical-tank.svg +1136 -0
- package/scada_symbols/consumers-hp.svg +346 -0
- package/scada_symbols/control-panel-hp.svg +332 -0
- package/scada_symbols/crane-hp.svg +346 -0
- package/scada_symbols/cross-connector-hp.svg +586 -0
- package/scada_symbols/cross-pipe.svg +1051 -0
- package/scada_symbols/curcuit-breaker-hp.svg +444 -0
- package/scada_symbols/cylindrical-tank.svg +1332 -0
- package/scada_symbols/drawwork-hp.svg +355 -0
- package/scada_symbols/drill-hp.svg +354 -0
- package/scada_symbols/drilling-line-hp.svg +361 -0
- package/scada_symbols/drilling-rig-hp.svg +354 -0
- package/scada_symbols/dynamic-horizontal-scale-hp.svg +605 -0
- package/scada_symbols/dynamic-vertical-scale-hp.svg +598 -0
- package/scada_symbols/electrical-distribution-board-hp.svg +332 -0
- package/scada_symbols/electrical-engine-hp.svg +360 -0
- package/scada_symbols/elevated-tank.svg +1564 -0
- package/scada_symbols/energy-meter-hp.svg +488 -0
- package/scada_symbols/extra-long-horizontal-pipe.svg +426 -0
- package/scada_symbols/extra-long-vertical-pipe.svg +438 -0
- package/scada_symbols/filter-hp.svg +287 -0
- package/scada_symbols/four-rate-energy-meter-hp.svg +891 -0
- package/scada_symbols/fuel-generator-hp.svg +359 -0
- package/scada_symbols/gas-preventer-hp.svg +361 -0
- package/scada_symbols/gas-wellhead-hp.svg +363 -0
- package/scada_symbols/heat-exchanger-hp.svg +363 -0
- package/scada_symbols/heat-pump-hp.svg +532 -0
- package/scada_symbols/high-voltage-tower-hp.svg +297 -0
- package/scada_symbols/hook-hp.svg +293 -0
- package/scada_symbols/horizontal-ball-valve.svg +271 -0
- package/scada_symbols/horizontal-broken-pipe.svg +97 -0
- package/scada_symbols/horizontal-connector-hp.svg +268 -0
- package/scada_symbols/horizontal-curcuit-breaker-hp.svg +439 -0
- package/scada_symbols/horizontal-energy-system-controller-hp.svg +378 -0
- package/scada_symbols/horizontal-inline-flow-meter.svg +913 -0
- package/scada_symbols/horizontal-pipe.svg +422 -0
- package/scada_symbols/horizontal-tank-hp.svg +524 -0
- package/scada_symbols/horizontal-tank.svg +1343 -0
- package/scada_symbols/horizontal-valve-hp.svg +392 -0
- package/scada_symbols/horizontal-wheel-valve.svg +300 -0
- package/scada_symbols/house-hp.svg +349 -0
- package/scada_symbols/index.json +702 -0
- package/scada_symbols/industrial-fuel-generator-hp.svg +358 -0
- package/scada_symbols/inverter-hp.svg +578 -0
- package/scada_symbols/large-conical-tank.svg +1146 -0
- package/scada_symbols/large-cylindrical-tank.svg +1323 -0
- package/scada_symbols/large-horizontal-separator-connector-hp.svg +342 -0
- package/scada_symbols/large-horizontal-separator-hp.svg +350 -0
- package/scada_symbols/large-inverter-hp.svg +578 -0
- package/scada_symbols/large-stand-cylindrical-tank.svg +1384 -0
- package/scada_symbols/large-stand-vertical-tank.svg +1386 -0
- package/scada_symbols/large-vertical-separator-connector-hp.svg +350 -0
- package/scada_symbols/large-vertical-separator-hp.svg +350 -0
- package/scada_symbols/large-vertical-tank.svg +1325 -0
- package/scada_symbols/leak-sensor.svg +158 -0
- package/scada_symbols/left-analog-water-level-meter.svg +748 -0
- package/scada_symbols/left-bottom-elbow-connector-hp.svg +233 -0
- package/scada_symbols/left-bottom-elbow-pipe.svg +532 -0
- package/scada_symbols/left-drain-pipe.svg +166 -0
- package/scada_symbols/left-elbow-drain-pipe.svg +181 -0
- package/scada_symbols/left-flow-meter.svg +911 -0
- package/scada_symbols/left-heat-pump.svg +939 -0
- package/scada_symbols/left-motor-pump.svg +541 -0
- package/scada_symbols/left-tee-connector-hp.svg +470 -0
- package/scada_symbols/left-tee-pipe.svg +869 -0
- package/scada_symbols/left-top-elbow-connector-hp.svg +233 -0
- package/scada_symbols/left-top-elbow-pipe.svg +532 -0
- package/scada_symbols/long-bottom-filter.svg +88 -0
- package/scada_symbols/long-horizontal-broken-pipe.svg +97 -0
- package/scada_symbols/long-horizontal-connector-hp.svg +269 -0
- package/scada_symbols/long-horizontal-pipe.svg +488 -0
- package/scada_symbols/long-top-filter.svg +88 -0
- package/scada_symbols/long-vertical-broken-pipe.svg +97 -0
- package/scada_symbols/long-vertical-connector-hp.svg +267 -0
- package/scada_symbols/long-vertical-pipe.svg +485 -0
- package/scada_symbols/low-voltage-tower-hp.svg +278 -0
- package/scada_symbols/low-voltage-transformer-tower-hp.svg +334 -0
- package/scada_symbols/manufacture-hp.svg +344 -0
- package/scada_symbols/meter.svg +751 -0
- package/scada_symbols/oil-pump-hp.svg +357 -0
- package/scada_symbols/platform-hp.svg +267 -0
- package/scada_symbols/pool-hp.svg +523 -0
- package/scada_symbols/pool.svg +998 -0
- package/scada_symbols/power-socket-hp.svg +371 -0
- package/scada_symbols/power-transformer-hp.svg +359 -0
- package/scada_symbols/preventer-hp.svg +343 -0
- package/scada_symbols/pump-hp.svg +368 -0
- package/scada_symbols/right-analog-water-level-meter.svg +748 -0
- package/scada_symbols/right-drain-pipe.svg +167 -0
- package/scada_symbols/right-elbow-drain-pipe.svg +178 -0
- package/scada_symbols/right-flow-meter.svg +911 -0
- package/scada_symbols/right-heat-pump.svg +948 -0
- package/scada_symbols/right-motor-pump.svg +541 -0
- package/scada_symbols/right-tee-connector-hp.svg +470 -0
- package/scada_symbols/right-tee-pipe.svg +857 -0
- package/scada_symbols/rotor-hp.svg +346 -0
- package/scada_symbols/sand-filter-hp.svg +652 -0
- package/scada_symbols/sand-filter.svg +704 -0
- package/scada_symbols/short-bottom-filter.svg +88 -0
- package/scada_symbols/short-left-drain-pipe.svg +160 -0
- package/scada_symbols/short-right-drain-pipe.svg +160 -0
- package/scada_symbols/short-top-filter.svg +88 -0
- package/scada_symbols/short-vertical-tank-hp.svg +522 -0
- package/scada_symbols/simple-horizontal-scale-hp.svg +509 -0
- package/scada_symbols/simple-vertical-scale-hp.svg +509 -0
- package/scada_symbols/single-key-switch-hp.svg +373 -0
- package/scada_symbols/small-cylindrical-tank.svg +1306 -0
- package/scada_symbols/small-horizontal-separator-connector-hp.svg +342 -0
- package/scada_symbols/small-horizontal-separator-hp.svg +342 -0
- package/scada_symbols/small-left-meter.svg +725 -0
- package/scada_symbols/small-left-motor-pump.svg +443 -0
- package/scada_symbols/small-meter.svg +694 -0
- package/scada_symbols/small-power-transformer-hp.svg +359 -0
- package/scada_symbols/small-right-center.svg +725 -0
- package/scada_symbols/small-right-motor-pump.svg +443 -0
- package/scada_symbols/small-spherical-tank.svg +1362 -0
- package/scada_symbols/small-vertical-separator-connector-hp.svg +342 -0
- package/scada_symbols/small-vertical-separator-hp.svg +342 -0
- package/scada_symbols/solar-panel-hp.svg +353 -0
- package/scada_symbols/spherical-tank.svg +1392 -0
- package/scada_symbols/stand-cylindrical-tank.svg +1393 -0
- package/scada_symbols/stand-horizontal-tank.svg +1404 -0
- package/scada_symbols/stand-solar-panel-hp.svg +357 -0
- package/scada_symbols/stand-vertical-short-tank.svg +1366 -0
- package/scada_symbols/stand-vertical-tank.svg +1386 -0
- package/scada_symbols/three-rate-energy-meter-hp.svg +759 -0
- package/scada_symbols/top-flow-meter.svg +927 -0
- package/scada_symbols/top-light-bulb-hp.svg +346 -0
- package/scada_symbols/top-right-elbow-connector-hp.svg +233 -0
- package/scada_symbols/top-right-elbow-pipe.svg +532 -0
- package/scada_symbols/top-tee-connector-hp.svg +470 -0
- package/scada_symbols/top-tee-pipe.svg +857 -0
- package/scada_symbols/turbine-hp.svg +377 -0
- package/scada_symbols/two-key-switch-hp.svg +489 -0
- package/scada_symbols/two-rate-energy-meter-hp.svg +627 -0
- package/scada_symbols/vertical-ball-valve.svg +271 -0
- package/scada_symbols/vertical-broken-pipe.svg +97 -0
- package/scada_symbols/vertical-connector-hp.svg +269 -0
- package/scada_symbols/vertical-energy-system-controller-hp.svg +378 -0
- package/scada_symbols/vertical-inline-flow-meter.svg +913 -0
- package/scada_symbols/vertical-pipe.svg +422 -0
- package/scada_symbols/vertical-short-tank.svg +1305 -0
- package/scada_symbols/vertical-tank-hp.svg +524 -0
- package/scada_symbols/vertical-tank.svg +1326 -0
- package/scada_symbols/vertical-valve-hp.svg +392 -0
- package/scada_symbols/vertical-wheel-valve.svg +300 -0
- package/scada_symbols/voltage-relay-hp.svg +440 -0
- package/scada_symbols/voltage-stabilizer-hp.svg +584 -0
- package/scada_symbols/waterstop.svg +276 -0
- package/scada_symbols/wind-turbine-cluster-hp.svg +370 -0
- package/scada_symbols/wind-turbine-hp.svg +360 -0
- package/src/default-data.json +18 -0
- package/src/definition-schema.d.ts +1521 -0
- package/src/definition-schema.json +5228 -0
- package/src/scada-runtime.ts +1290 -0
- package/src/widget-scada.ts +269 -0
- package/thumbnail.png +0 -0
|
@@ -0,0 +1,1290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCADA Runtime Engine
|
|
3
|
+
*
|
|
4
|
+
* Implements a ThingsBoard-compatible SVG runtime that executes
|
|
5
|
+
* stateRenderFunction / actionFunction JS strings from SCADA SVG metadata.
|
|
6
|
+
*
|
|
7
|
+
* Provides: ScadaElement (SVG.js-like wrapper), ctx API (animations,
|
|
8
|
+
* text/value formatting, color processors, actions), and the execution engine.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Types
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export interface SymbolMetadata {
|
|
16
|
+
title: string
|
|
17
|
+
description: string
|
|
18
|
+
stateRenderFunction?: string | null
|
|
19
|
+
tags: TagDef[]
|
|
20
|
+
behavior: BehaviorDef[]
|
|
21
|
+
properties: PropertyDef[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TagDef {
|
|
25
|
+
tag: string
|
|
26
|
+
stateRenderFunction: string | null
|
|
27
|
+
actions: Record<string, { actionFunction: string }> | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BehaviorDef {
|
|
31
|
+
id: string
|
|
32
|
+
name: string
|
|
33
|
+
type: 'value' | 'action' | 'widgetAction'
|
|
34
|
+
valueType: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PropertyDef {
|
|
38
|
+
id: string
|
|
39
|
+
name: string
|
|
40
|
+
type: string
|
|
41
|
+
default: any
|
|
42
|
+
disableOnProperty?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FontDef {
|
|
46
|
+
size: number
|
|
47
|
+
sizeUnit: string
|
|
48
|
+
family: string
|
|
49
|
+
weight: string
|
|
50
|
+
style: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ColorSettings {
|
|
54
|
+
type: 'constant' | 'gradient' | 'range' | 'function'
|
|
55
|
+
color: string
|
|
56
|
+
gradient?: {
|
|
57
|
+
advancedMode: boolean
|
|
58
|
+
gradient: string[]
|
|
59
|
+
minValue: number
|
|
60
|
+
maxValue: number
|
|
61
|
+
}
|
|
62
|
+
rangeList?: {
|
|
63
|
+
advancedMode: boolean
|
|
64
|
+
range: Array<{ from: number; to: number; color: string }>
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Per-element state storage (WeakMap so GC can reclaim)
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
const elementMemory = new WeakMap<Element, Map<string, any>>()
|
|
73
|
+
|
|
74
|
+
function getMemory(el: Element): Map<string, any> {
|
|
75
|
+
let m = elementMemory.get(el)
|
|
76
|
+
if (!m) {
|
|
77
|
+
m = new Map()
|
|
78
|
+
elementMemory.set(el, m)
|
|
79
|
+
}
|
|
80
|
+
return m
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Animation tracking
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
const elementAnimations = new WeakMap<Element, AnimationRunner>()
|
|
88
|
+
const connectorAnimations = new WeakMap<Element, ConnectorAnimationRunner>()
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Connector flow animation — animates dashes along an SVG path to simulate
|
|
92
|
+
* fluid flow in pipe/connector symbols.
|
|
93
|
+
*/
|
|
94
|
+
class ConnectorAnimationRunner {
|
|
95
|
+
private container: Element
|
|
96
|
+
private forwardPath: string
|
|
97
|
+
private reversedPath: string
|
|
98
|
+
private pathEl: SVGPathElement | null = null
|
|
99
|
+
private lineWidth: number = 2
|
|
100
|
+
private lineColor: string = '#fff'
|
|
101
|
+
private dashCap: string = 'round'
|
|
102
|
+
private dashWidth: number = 10
|
|
103
|
+
private dashGap: number = 10
|
|
104
|
+
private dur: number = 1 // seconds
|
|
105
|
+
private dir: string = 'forward'
|
|
106
|
+
private rafId: number = 0
|
|
107
|
+
private rafActive: boolean = false
|
|
108
|
+
private rafLastTime: number = 0
|
|
109
|
+
private offset: number = 0
|
|
110
|
+
|
|
111
|
+
constructor(container: Element, forwardPath: string, reversedPath: string) {
|
|
112
|
+
this.container = container
|
|
113
|
+
this.forwardPath = forwardPath
|
|
114
|
+
this.reversedPath = reversedPath
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
flowAppearance(
|
|
118
|
+
lineWidth: number,
|
|
119
|
+
lineColor: string,
|
|
120
|
+
dashCap: string,
|
|
121
|
+
dashWidth: number,
|
|
122
|
+
dashGap: number
|
|
123
|
+
): ConnectorAnimationRunner {
|
|
124
|
+
this.lineWidth = lineWidth ?? 2
|
|
125
|
+
this.lineColor = lineColor ?? '#fff'
|
|
126
|
+
this.dashCap = dashCap ?? 'round'
|
|
127
|
+
this.dashWidth = dashWidth ?? 10
|
|
128
|
+
this.dashGap = dashGap ?? 10
|
|
129
|
+
this.updatePathStyle()
|
|
130
|
+
return this
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
duration(d: number): ConnectorAnimationRunner {
|
|
134
|
+
this.dur = typeof d === 'number' && isFinite(d) && d > 0 ? d : 1
|
|
135
|
+
return this
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
direction(dir: string): ConnectorAnimationRunner {
|
|
139
|
+
this.dir = dir
|
|
140
|
+
this.updatePathD()
|
|
141
|
+
return this
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
play(): ConnectorAnimationRunner {
|
|
145
|
+
this.ensurePath()
|
|
146
|
+
if (!this.rafActive) {
|
|
147
|
+
this.rafActive = true
|
|
148
|
+
this.rafLastTime = 0
|
|
149
|
+
this.rafId = requestAnimationFrame(this.tick)
|
|
150
|
+
}
|
|
151
|
+
return this
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
finish(): void {
|
|
155
|
+
this.cancelRaf()
|
|
156
|
+
if (this.pathEl && this.pathEl.parentNode) {
|
|
157
|
+
this.pathEl.parentNode.removeChild(this.pathEl)
|
|
158
|
+
}
|
|
159
|
+
this.pathEl = null
|
|
160
|
+
connectorAnimations.delete(this.container)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private tick = (now: number): void => {
|
|
164
|
+
if (!this.rafActive) return
|
|
165
|
+
if (!this.rafLastTime) this.rafLastTime = now
|
|
166
|
+
const dt = (now - this.rafLastTime) / 1000
|
|
167
|
+
this.rafLastTime = now
|
|
168
|
+
|
|
169
|
+
const cycleLength = this.dashWidth + this.dashGap
|
|
170
|
+
// Speed: traverse one dash+gap cycle per `this.dur` seconds
|
|
171
|
+
this.offset += (cycleLength / this.dur) * dt
|
|
172
|
+
if (this.offset > cycleLength) this.offset -= cycleLength
|
|
173
|
+
|
|
174
|
+
if (this.pathEl) {
|
|
175
|
+
this.pathEl.style.strokeDashoffset = String(-this.offset)
|
|
176
|
+
}
|
|
177
|
+
this.rafId = requestAnimationFrame(this.tick)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private ensurePath(): void {
|
|
181
|
+
if (this.pathEl) return
|
|
182
|
+
const ns = 'http://www.w3.org/2000/svg'
|
|
183
|
+
this.pathEl = document.createElementNS(ns, 'path')
|
|
184
|
+
this.updatePathD()
|
|
185
|
+
this.updatePathStyle()
|
|
186
|
+
this.container.appendChild(this.pathEl)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private updatePathD(): void {
|
|
190
|
+
if (!this.pathEl) return
|
|
191
|
+
const d = this.dir === 'reverse' ? this.reversedPath : this.forwardPath
|
|
192
|
+
this.pathEl.setAttribute('d', d)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private updatePathStyle(): void {
|
|
196
|
+
if (!this.pathEl) return
|
|
197
|
+
this.pathEl.setAttribute('fill', 'none')
|
|
198
|
+
this.pathEl.setAttribute('stroke', this.lineColor)
|
|
199
|
+
this.pathEl.setAttribute('stroke-width', String(this.lineWidth))
|
|
200
|
+
this.pathEl.setAttribute('stroke-linecap', this.dashCap)
|
|
201
|
+
this.pathEl.style.strokeDasharray = `${this.dashWidth} ${this.dashGap}`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private cancelRaf(): void {
|
|
205
|
+
if (this.rafId) {
|
|
206
|
+
cancelAnimationFrame(this.rafId)
|
|
207
|
+
this.rafId = 0
|
|
208
|
+
}
|
|
209
|
+
this.rafActive = false
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Material Symbols font loader (injected once into document)
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
let materialFontLoaded = false
|
|
217
|
+
function ensureMaterialSymbolsFont(): void {
|
|
218
|
+
if (materialFontLoaded) return
|
|
219
|
+
materialFontLoaded = true
|
|
220
|
+
const link = document.createElement('link')
|
|
221
|
+
link.rel = 'stylesheet'
|
|
222
|
+
link.href =
|
|
223
|
+
'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200'
|
|
224
|
+
document.head.appendChild(link)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
class AnimationRunner {
|
|
228
|
+
private el: Element
|
|
229
|
+
private duration: number
|
|
230
|
+
private anim: Animation | null = null
|
|
231
|
+
private keyframes: Keyframe[] = []
|
|
232
|
+
private loopCount: number = 1
|
|
233
|
+
private loopBounce: boolean = false
|
|
234
|
+
private playbackSpeed: number = 1
|
|
235
|
+
private easingType: string = 'ease'
|
|
236
|
+
|
|
237
|
+
// rAF-based animation for SVG transforms (pattern flow & rotation)
|
|
238
|
+
private rafId: number = 0
|
|
239
|
+
private rafActive: boolean = false
|
|
240
|
+
private rafPaused: boolean = false
|
|
241
|
+
private rafLastTime: number = 0
|
|
242
|
+
private rafElapsed: number = 0
|
|
243
|
+
private rafTick: ((now: number) => void) | null = null
|
|
244
|
+
private rafPatternEl: Element | null = null
|
|
245
|
+
private rafOriginalTransform: string = ''
|
|
246
|
+
|
|
247
|
+
constructor(el: Element, duration: number) {
|
|
248
|
+
this.el = el
|
|
249
|
+
this.duration = duration
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Animate element by a relative offset (dx, dy). Used by pipe flow animations. */
|
|
253
|
+
relative(dx: number, dy: number): AnimationRunner {
|
|
254
|
+
const safeDx = typeof dx === 'number' && isFinite(dx) ? dx : 0
|
|
255
|
+
const safeDy = typeof dy === 'number' && isFinite(dy) ? dy : 0
|
|
256
|
+
|
|
257
|
+
// For elements inside SVG <pattern>, CSS transforms shift the entire
|
|
258
|
+
// tile (including pipe borders). Use patternTransform via rAF instead.
|
|
259
|
+
const parent = this.el.parentElement
|
|
260
|
+
if (parent?.nodeName?.toLowerCase() === 'pattern') {
|
|
261
|
+
this.cancelRaf()
|
|
262
|
+
this.rafPatternEl = parent
|
|
263
|
+
this.rafOriginalTransform = parent.getAttribute('patternTransform') || ''
|
|
264
|
+
this.rafActive = true
|
|
265
|
+
this.rafPaused = false
|
|
266
|
+
this.rafElapsed = 0
|
|
267
|
+
this.rafLastTime = 0
|
|
268
|
+
|
|
269
|
+
const baseTransform = this.rafOriginalTransform ? `${this.rafOriginalTransform} ` : ''
|
|
270
|
+
this.rafTick = (now: number) => {
|
|
271
|
+
if (!this.rafActive || this.rafPaused) return
|
|
272
|
+
if (!this.rafLastTime) this.rafLastTime = now
|
|
273
|
+
this.rafElapsed += (now - this.rafLastTime) * this.playbackSpeed
|
|
274
|
+
this.rafLastTime = now
|
|
275
|
+
|
|
276
|
+
const progress = (this.rafElapsed % this.duration) / this.duration
|
|
277
|
+
this.rafPatternEl!.setAttribute(
|
|
278
|
+
'patternTransform',
|
|
279
|
+
`${baseTransform}translate(${safeDx * progress}, ${safeDy * progress})`
|
|
280
|
+
)
|
|
281
|
+
this.rafId = requestAnimationFrame(this.rafTick!)
|
|
282
|
+
}
|
|
283
|
+
this.rafId = requestAnimationFrame(this.rafTick)
|
|
284
|
+
} else {
|
|
285
|
+
// Non-pattern element: use CSS keyframe animation
|
|
286
|
+
this.keyframes = [
|
|
287
|
+
{ transform: 'translate(0, 0)' },
|
|
288
|
+
{ transform: `translate(${safeDx}px, ${safeDy}px)` }
|
|
289
|
+
]
|
|
290
|
+
this.easingType = 'linear'
|
|
291
|
+
this.commit()
|
|
292
|
+
}
|
|
293
|
+
return this
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
attr(attrs: Record<string, any>): AnimationRunner {
|
|
297
|
+
// SVG geometric attributes (height, width, x, y, cx, cy, r, rx, ry)
|
|
298
|
+
// cannot be animated via Web Animations API keyframes reliably.
|
|
299
|
+
// Set them directly with a CSS transition for smooth animation.
|
|
300
|
+
// CSS-animatable properties (opacity, fill, stroke, etc.) use keyframes.
|
|
301
|
+
const SVG_GEOM = new Set(['height', 'width', 'x', 'y', 'cx', 'cy', 'r', 'rx', 'ry'])
|
|
302
|
+
const startFrame: Keyframe = {}
|
|
303
|
+
const endFrame: Keyframe = {}
|
|
304
|
+
let hasKeyframes = false
|
|
305
|
+
|
|
306
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
307
|
+
const safeVal = typeof val === 'number' && !isFinite(val) ? 0 : val
|
|
308
|
+
if (SVG_GEOM.has(key)) {
|
|
309
|
+
;(this.el as SVGElement).style.transition = `all ${this.duration}ms ease`
|
|
310
|
+
this.el.setAttribute(key, String(safeVal))
|
|
311
|
+
} else {
|
|
312
|
+
const computed = getComputedStyle(this.el as HTMLElement)
|
|
313
|
+
const current = computed.getPropertyValue(key) || this.el.getAttribute(key) || ''
|
|
314
|
+
startFrame[key] = current
|
|
315
|
+
endFrame[key] = safeVal
|
|
316
|
+
hasKeyframes = true
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (hasKeyframes) {
|
|
320
|
+
this.keyframes = [startFrame, endFrame]
|
|
321
|
+
this.easingType = 'linear'
|
|
322
|
+
}
|
|
323
|
+
return this
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
transform(t: Record<string, any>): AnimationRunner {
|
|
327
|
+
const parts: string[] = []
|
|
328
|
+
const n = (v: any) => (typeof v === 'number' && !isFinite(v) ? 0 : (v ?? 0))
|
|
329
|
+
if (t.translateX !== undefined || t.translateY !== undefined) {
|
|
330
|
+
parts.push(`translate(${n(t.translateX)}, ${n(t.translateY)})`)
|
|
331
|
+
}
|
|
332
|
+
if (t.rotate !== undefined) {
|
|
333
|
+
// Default unspecified origin axes to element bbox center (SVG.js compat)
|
|
334
|
+
const bbox = (this.el as SVGGraphicsElement).getBBox?.()
|
|
335
|
+
const ox = t.originX !== undefined ? n(t.originX) : bbox ? bbox.x + bbox.width / 2 : 0
|
|
336
|
+
const oy = t.originY !== undefined ? n(t.originY) : bbox ? bbox.y + bbox.height / 2 : 0
|
|
337
|
+
parts.push(`rotate(${n(t.rotate)}, ${ox}, ${oy})`)
|
|
338
|
+
}
|
|
339
|
+
if (t.scale !== undefined) {
|
|
340
|
+
parts.push(`scale(${n(t.scale)})`)
|
|
341
|
+
}
|
|
342
|
+
if (parts.length > 0) {
|
|
343
|
+
// For pattern elements, set patternTransform; otherwise set transform attribute
|
|
344
|
+
const tagName = this.el.nodeName?.toLowerCase()
|
|
345
|
+
if (tagName === 'pattern') {
|
|
346
|
+
this.el.setAttribute('patternTransform', parts.join(' '))
|
|
347
|
+
} else {
|
|
348
|
+
this.el.setAttribute('transform', parts.join(' '))
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return this
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
rotate(deg: number): AnimationRunner {
|
|
355
|
+
// Use rAF-based animation that composes rotation into the existing
|
|
356
|
+
// SVG transform attribute. CSS animations would override the SVG
|
|
357
|
+
// transform (e.g. translate) causing the element to shift position.
|
|
358
|
+
const existingTransform = this.el.getAttribute('transform') || ''
|
|
359
|
+
this.cancelRaf()
|
|
360
|
+
this.rafOriginalTransform = existingTransform
|
|
361
|
+
|
|
362
|
+
// Calculate rotation center from the element's bounding box
|
|
363
|
+
const bbox = (this.el as SVGGraphicsElement).getBBox?.()
|
|
364
|
+
const cx = bbox ? bbox.x + bbox.width / 2 : 0
|
|
365
|
+
const cy = bbox ? bbox.y + bbox.height / 2 : 0
|
|
366
|
+
|
|
367
|
+
this.rafActive = true
|
|
368
|
+
this.rafPaused = false
|
|
369
|
+
this.rafElapsed = 0
|
|
370
|
+
this.rafLastTime = 0
|
|
371
|
+
|
|
372
|
+
this.rafTick = (now: number) => {
|
|
373
|
+
if (!this.rafActive || this.rafPaused) return
|
|
374
|
+
if (!this.rafLastTime) this.rafLastTime = now
|
|
375
|
+
this.rafElapsed += (now - this.rafLastTime) * this.playbackSpeed
|
|
376
|
+
this.rafLastTime = now
|
|
377
|
+
|
|
378
|
+
const progress = (this.rafElapsed % this.duration) / this.duration
|
|
379
|
+
const currentDeg = deg * progress
|
|
380
|
+
this.el.setAttribute('transform', `${existingTransform} rotate(${currentDeg}, ${cx}, ${cy})`)
|
|
381
|
+
this.rafId = requestAnimationFrame(this.rafTick!)
|
|
382
|
+
}
|
|
383
|
+
this.rafId = requestAnimationFrame(this.rafTick)
|
|
384
|
+
return this
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
ease(easing: string): AnimationRunner {
|
|
388
|
+
this.easingType = easing
|
|
389
|
+
return this
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
loop(count?: number, bounce?: boolean): AnimationRunner {
|
|
393
|
+
this.loopCount = count === 0 ? Infinity : (count ?? Infinity)
|
|
394
|
+
this.loopBounce = bounce ?? false
|
|
395
|
+
// In rAF mode the animation already loops; only commit for CSS animations
|
|
396
|
+
if (!this.rafActive) this.commit()
|
|
397
|
+
return this
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
speed(s: number): AnimationRunner {
|
|
401
|
+
this.playbackSpeed = s
|
|
402
|
+
if (this.anim) this.anim.playbackRate = s
|
|
403
|
+
// rAF mode reads playbackSpeed each frame automatically
|
|
404
|
+
return this
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
play(): AnimationRunner {
|
|
408
|
+
if (this.rafActive && this.rafPaused) {
|
|
409
|
+
this.rafPaused = false
|
|
410
|
+
this.rafLastTime = 0
|
|
411
|
+
if (this.rafTick) this.rafId = requestAnimationFrame(this.rafTick)
|
|
412
|
+
}
|
|
413
|
+
if (this.anim) this.anim.play()
|
|
414
|
+
return this
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
pause(): AnimationRunner {
|
|
418
|
+
if (this.rafActive) {
|
|
419
|
+
this.rafPaused = true
|
|
420
|
+
if (this.rafId) {
|
|
421
|
+
cancelAnimationFrame(this.rafId)
|
|
422
|
+
this.rafId = 0
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (this.anim) this.anim.pause()
|
|
426
|
+
return this
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
cancel(): void {
|
|
430
|
+
if (this.anim) {
|
|
431
|
+
this.anim.cancel()
|
|
432
|
+
this.anim = null
|
|
433
|
+
}
|
|
434
|
+
this.cancelRaf()
|
|
435
|
+
;(this.el as SVGElement).style.transition = ''
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
finish(): void {
|
|
439
|
+
if (this.anim) {
|
|
440
|
+
this.anim.finish()
|
|
441
|
+
this.anim = null
|
|
442
|
+
}
|
|
443
|
+
this.cancelRaf()
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private cancelRaf() {
|
|
447
|
+
if (this.rafId) {
|
|
448
|
+
cancelAnimationFrame(this.rafId)
|
|
449
|
+
this.rafId = 0
|
|
450
|
+
}
|
|
451
|
+
this.rafActive = false
|
|
452
|
+
this.rafTick = null
|
|
453
|
+
if (this.rafPatternEl) {
|
|
454
|
+
if (this.rafOriginalTransform) {
|
|
455
|
+
this.rafPatternEl.setAttribute('patternTransform', this.rafOriginalTransform)
|
|
456
|
+
} else {
|
|
457
|
+
this.rafPatternEl.removeAttribute('patternTransform')
|
|
458
|
+
}
|
|
459
|
+
this.rafPatternEl = null
|
|
460
|
+
this.rafOriginalTransform = ''
|
|
461
|
+
} else if (this.rafOriginalTransform) {
|
|
462
|
+
// Restore original SVG transform for rotation animations
|
|
463
|
+
this.el.setAttribute('transform', this.rafOriginalTransform)
|
|
464
|
+
this.rafOriginalTransform = ''
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private commit() {
|
|
469
|
+
if (this.keyframes.length === 0) return
|
|
470
|
+
|
|
471
|
+
// Cancel previous animation if any
|
|
472
|
+
if (this.anim) this.anim.cancel()
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
this.anim = this.el.animate(this.keyframes, {
|
|
476
|
+
duration: this.duration,
|
|
477
|
+
iterations: this.loopCount,
|
|
478
|
+
direction: this.loopBounce ? 'alternate' : 'normal',
|
|
479
|
+
fill: 'forwards',
|
|
480
|
+
easing: this.easingType
|
|
481
|
+
})
|
|
482
|
+
this.anim.playbackRate = this.playbackSpeed
|
|
483
|
+
} catch {
|
|
484
|
+
// Some SVG elements don't support Web Animations
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
// ScadaElement — SVG.js-compatible wrapper for SVGElement
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
export class ScadaElement {
|
|
494
|
+
private el: Element
|
|
495
|
+
|
|
496
|
+
constructor(el: Element) {
|
|
497
|
+
this.el = el
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Sanitize a value for SVG attributes: coerce NaN/Infinity to 0 */
|
|
501
|
+
private safe(v: any): any {
|
|
502
|
+
return typeof v === 'number' && !isFinite(v) ? 0 : v
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Get or set attributes. With object: set multiple. With string: get one. */
|
|
506
|
+
attr(attrsOrKey?: Record<string, any> | string, val?: any): any {
|
|
507
|
+
if (typeof attrsOrKey === 'string') {
|
|
508
|
+
if (val !== undefined) {
|
|
509
|
+
this.el.setAttribute(attrsOrKey, String(this.safe(val)))
|
|
510
|
+
return this
|
|
511
|
+
}
|
|
512
|
+
return this.el.getAttribute(attrsOrKey)
|
|
513
|
+
}
|
|
514
|
+
if (attrsOrKey) {
|
|
515
|
+
for (const [k, v] of Object.entries(attrsOrKey)) {
|
|
516
|
+
this.el.setAttribute(k, String(this.safe(v)))
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return this
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
fill(color: string): ScadaElement {
|
|
523
|
+
this.el.setAttribute('fill', color)
|
|
524
|
+
return this
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
stroke(opts: Record<string, any> | string): ScadaElement {
|
|
528
|
+
if (typeof opts === 'string') {
|
|
529
|
+
this.el.setAttribute('stroke', opts)
|
|
530
|
+
} else {
|
|
531
|
+
if (opts.color) this.el.setAttribute('stroke', opts.color)
|
|
532
|
+
if (opts.width !== undefined) this.el.setAttribute('stroke-width', String(this.safe(opts.width)))
|
|
533
|
+
if (opts.dasharray) this.el.setAttribute('stroke-dasharray', opts.dasharray)
|
|
534
|
+
}
|
|
535
|
+
return this
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
show(): ScadaElement {
|
|
539
|
+
;(this.el as SVGElement).style.display = ''
|
|
540
|
+
;(this.el as SVGElement).style.visibility = 'visible'
|
|
541
|
+
return this
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
hide(): ScadaElement {
|
|
545
|
+
;(this.el as SVGElement).style.display = 'none'
|
|
546
|
+
return this
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
transform(t: Record<string, any>): ScadaElement {
|
|
550
|
+
const n = (v: any) => (typeof v === 'number' && !isFinite(v) ? 0 : (v ?? 0))
|
|
551
|
+
const parts: string[] = []
|
|
552
|
+
if (t.translateX !== undefined || t.translateY !== undefined) {
|
|
553
|
+
parts.push(`translate(${n(t.translateX)}, ${n(t.translateY)})`)
|
|
554
|
+
}
|
|
555
|
+
if (t.rotate !== undefined) {
|
|
556
|
+
// Default unspecified origin axes to element bbox center (SVG.js compat)
|
|
557
|
+
const bbox = (this.el as SVGGraphicsElement).getBBox?.()
|
|
558
|
+
const ox = t.originX !== undefined ? n(t.originX) : bbox ? bbox.x + bbox.width / 2 : 0
|
|
559
|
+
const oy = t.originY !== undefined ? n(t.originY) : bbox ? bbox.y + bbox.height / 2 : 0
|
|
560
|
+
parts.push(`rotate(${n(t.rotate)}, ${ox}, ${oy})`)
|
|
561
|
+
}
|
|
562
|
+
if (t.scale !== undefined) {
|
|
563
|
+
parts.push(`scale(${n(t.scale)})`)
|
|
564
|
+
}
|
|
565
|
+
if (parts.length > 0) {
|
|
566
|
+
const tagName = this.el.nodeName?.toLowerCase()
|
|
567
|
+
if (tagName === 'pattern') {
|
|
568
|
+
this.el.setAttribute('patternTransform', parts.join(' '))
|
|
569
|
+
} else {
|
|
570
|
+
this.el.setAttribute('transform', parts.join(' '))
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return this
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Store or retrieve per-element state */
|
|
577
|
+
remember(key: string, value?: any): any {
|
|
578
|
+
const mem = getMemory(this.el)
|
|
579
|
+
if (value !== undefined) {
|
|
580
|
+
mem.set(key, value)
|
|
581
|
+
return this
|
|
582
|
+
}
|
|
583
|
+
return mem.get(key)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Query children */
|
|
587
|
+
find(selector: string): ScadaElement[] {
|
|
588
|
+
return Array.from(this.el.querySelectorAll(selector)).map((e) => new ScadaElement(e))
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Query first matching child */
|
|
592
|
+
findOne(selector: string): ScadaElement | null {
|
|
593
|
+
const el = this.el.querySelector(selector)
|
|
594
|
+
return el ? new ScadaElement(el) : null
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** First child element */
|
|
598
|
+
first(): ScadaElement {
|
|
599
|
+
const fc = this.el.firstElementChild
|
|
600
|
+
return fc ? new ScadaElement(fc) : this
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Remove all children */
|
|
604
|
+
clear(): ScadaElement {
|
|
605
|
+
while (this.el.firstChild) this.el.removeChild(this.el.firstChild)
|
|
606
|
+
return this
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Append child (accepts ScadaElement or raw Element) */
|
|
610
|
+
add(child: ScadaElement | Element): ScadaElement {
|
|
611
|
+
const node = child instanceof ScadaElement ? child.native : child
|
|
612
|
+
this.el.appendChild(node)
|
|
613
|
+
return this
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Get referenced element from an attribute (e.g., fill url(#id)) */
|
|
617
|
+
reference(attrName: string): ScadaElement {
|
|
618
|
+
const val = this.el.getAttribute(attrName)
|
|
619
|
+
if (val) {
|
|
620
|
+
const idMatch = val.match(/url\(#([^)]+)\)/)
|
|
621
|
+
if (idMatch) {
|
|
622
|
+
// Use getRootNode() so IDs inside shadow DOM (Lit) are found
|
|
623
|
+
const root = this.el.getRootNode() as Document | ShadowRoot
|
|
624
|
+
const ref = root.getElementById(idMatch[1])
|
|
625
|
+
if (ref) return new ScadaElement(ref)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return this
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** ForEach for SVG.js compatibility when used on collections */
|
|
632
|
+
forEach(fn: (el: ScadaElement, i: number) => void): void {
|
|
633
|
+
fn(this, 0)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Get bounding box width */
|
|
637
|
+
width(): number {
|
|
638
|
+
return (this.el as SVGGraphicsElement).getBBox?.()?.width ?? 0
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Get bounding box height */
|
|
642
|
+
height(): number {
|
|
643
|
+
return (this.el as SVGGraphicsElement).getBBox?.()?.height ?? 0
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** Get child elements as ScadaElement array */
|
|
647
|
+
children(): ScadaElement[] {
|
|
648
|
+
return Array.from(this.el.children).map((e) => new ScadaElement(e))
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Get underlying DOM element */
|
|
652
|
+
get native(): Element {
|
|
653
|
+
return this.el
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
// Color Processor — handles color_settings type properties
|
|
659
|
+
// ---------------------------------------------------------------------------
|
|
660
|
+
|
|
661
|
+
export class ColorProcessor {
|
|
662
|
+
private settings: ColorSettings
|
|
663
|
+
color: string
|
|
664
|
+
|
|
665
|
+
constructor(settings: ColorSettings | string) {
|
|
666
|
+
if (typeof settings === 'string') {
|
|
667
|
+
this.settings = { type: 'constant', color: settings }
|
|
668
|
+
this.color = settings
|
|
669
|
+
} else {
|
|
670
|
+
this.settings = settings
|
|
671
|
+
this.color = settings.color ?? '#000000'
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
update(value: number): void {
|
|
676
|
+
switch (this.settings.type) {
|
|
677
|
+
case 'constant':
|
|
678
|
+
// color stays the same
|
|
679
|
+
break
|
|
680
|
+
case 'gradient': {
|
|
681
|
+
const g = this.settings.gradient
|
|
682
|
+
if (g && g.gradient.length >= 2) {
|
|
683
|
+
const min = g.minValue ?? 0
|
|
684
|
+
const max = g.maxValue ?? 100
|
|
685
|
+
const ratio = Math.max(0, Math.min(1, (value - min) / (max - min || 1)))
|
|
686
|
+
this.color = interpolateColor(g.gradient[0], g.gradient[g.gradient.length - 1], ratio)
|
|
687
|
+
}
|
|
688
|
+
break
|
|
689
|
+
}
|
|
690
|
+
case 'range': {
|
|
691
|
+
const rl = this.settings.rangeList
|
|
692
|
+
if (rl?.range) {
|
|
693
|
+
for (const r of rl.range) {
|
|
694
|
+
if (value >= r.from && value <= r.to) {
|
|
695
|
+
this.color = r.color
|
|
696
|
+
return
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
break
|
|
701
|
+
}
|
|
702
|
+
// 'function' mode: skip — requires tinycolor2 + eval
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function interpolateColor(c1: string, c2: string, ratio: number): string {
|
|
708
|
+
const parse = (c: string) => {
|
|
709
|
+
// Handle rgba(r,g,b,a) or hex
|
|
710
|
+
const rgbaMatch = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
|
|
711
|
+
if (rgbaMatch) return [+rgbaMatch[1], +rgbaMatch[2], +rgbaMatch[3]]
|
|
712
|
+
// hex
|
|
713
|
+
const hex = c.replace('#', '')
|
|
714
|
+
const h =
|
|
715
|
+
hex.length === 3
|
|
716
|
+
? [
|
|
717
|
+
parseInt(hex[0] + hex[0], 16),
|
|
718
|
+
parseInt(hex[1] + hex[1], 16),
|
|
719
|
+
parseInt(hex[2] + hex[2], 16)
|
|
720
|
+
]
|
|
721
|
+
: [
|
|
722
|
+
parseInt(hex.slice(0, 2), 16),
|
|
723
|
+
parseInt(hex.slice(2, 4), 16),
|
|
724
|
+
parseInt(hex.slice(4, 6), 16)
|
|
725
|
+
]
|
|
726
|
+
return h
|
|
727
|
+
}
|
|
728
|
+
const [r1, g1, b1] = parse(c1)
|
|
729
|
+
const [r2, g2, b2] = parse(c2)
|
|
730
|
+
const r = Math.round(r1 + (r2 - r1) * ratio)
|
|
731
|
+
const g = Math.round(g1 + (g2 - g1) * ratio)
|
|
732
|
+
const b = Math.round(b1 + (b2 - b1) * ratio)
|
|
733
|
+
return `rgb(${r},${g},${b})`
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// ---------------------------------------------------------------------------
|
|
737
|
+
// SVG Factory — ctx.svg.line(), ctx.svg.text()
|
|
738
|
+
// ---------------------------------------------------------------------------
|
|
739
|
+
|
|
740
|
+
function createSvgFactory(svgEl: SVGSVGElement) {
|
|
741
|
+
const ns = 'http://www.w3.org/2000/svg'
|
|
742
|
+
|
|
743
|
+
return {
|
|
744
|
+
line(x1: number, y1: number, x2: number, y2: number): ScadaElement {
|
|
745
|
+
const line = document.createElementNS(ns, 'line')
|
|
746
|
+
line.setAttribute('x1', String(x1))
|
|
747
|
+
line.setAttribute('y1', String(y1))
|
|
748
|
+
line.setAttribute('x2', String(x2))
|
|
749
|
+
line.setAttribute('y2', String(y2))
|
|
750
|
+
return new ScadaElement(line)
|
|
751
|
+
},
|
|
752
|
+
|
|
753
|
+
text(content: string): ScadaElement {
|
|
754
|
+
const text = document.createElementNS(ns, 'text')
|
|
755
|
+
const tspan = document.createElementNS(ns, 'tspan')
|
|
756
|
+
tspan.textContent = content
|
|
757
|
+
text.appendChild(tspan)
|
|
758
|
+
return new ScadaElement(text)
|
|
759
|
+
},
|
|
760
|
+
|
|
761
|
+
rect(x?: number, y?: number, w?: number, h?: number): ScadaElement {
|
|
762
|
+
const rect = document.createElementNS(ns, 'rect')
|
|
763
|
+
if (x !== undefined) rect.setAttribute('x', String(x))
|
|
764
|
+
if (y !== undefined) rect.setAttribute('y', String(y))
|
|
765
|
+
if (w !== undefined) rect.setAttribute('width', String(w))
|
|
766
|
+
if (h !== undefined) rect.setAttribute('height', String(h))
|
|
767
|
+
return new ScadaElement(rect)
|
|
768
|
+
},
|
|
769
|
+
|
|
770
|
+
circle(cx?: number, cy?: number, r?: number): ScadaElement {
|
|
771
|
+
const circle = document.createElementNS(ns, 'circle')
|
|
772
|
+
if (cx !== undefined) circle.setAttribute('cx', String(cx))
|
|
773
|
+
if (cy !== undefined) circle.setAttribute('cy', String(cy))
|
|
774
|
+
if (r !== undefined) circle.setAttribute('r', String(r))
|
|
775
|
+
return new ScadaElement(circle)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// ---------------------------------------------------------------------------
|
|
781
|
+
// Unit helpers
|
|
782
|
+
// ---------------------------------------------------------------------------
|
|
783
|
+
|
|
784
|
+
const UNIT_SYMBOLS: Record<string, string> = {
|
|
785
|
+
celsius: '°C',
|
|
786
|
+
fahrenheit: '°F',
|
|
787
|
+
kelvin: 'K',
|
|
788
|
+
liters: 'L',
|
|
789
|
+
gallons: 'gal',
|
|
790
|
+
cubicMeters: 'm³',
|
|
791
|
+
cubicFeet: 'ft³',
|
|
792
|
+
milliliters: 'mL',
|
|
793
|
+
barrels: 'bbl',
|
|
794
|
+
watts: 'W',
|
|
795
|
+
kilowatts: 'kW',
|
|
796
|
+
megawatts: 'MW',
|
|
797
|
+
horsepower: 'hp',
|
|
798
|
+
wattHours: 'Wh',
|
|
799
|
+
kilowattHours: 'kWh',
|
|
800
|
+
megawattHours: 'MWh',
|
|
801
|
+
volts: 'V',
|
|
802
|
+
millivolts: 'mV',
|
|
803
|
+
kilovolts: 'kV',
|
|
804
|
+
amperes: 'A',
|
|
805
|
+
milliamperes: 'mA',
|
|
806
|
+
hertz: 'Hz',
|
|
807
|
+
kilohertz: 'kHz',
|
|
808
|
+
psi: 'psi',
|
|
809
|
+
bar: 'bar',
|
|
810
|
+
pascal: 'Pa',
|
|
811
|
+
kilopascal: 'kPa',
|
|
812
|
+
atm: 'atm',
|
|
813
|
+
meters: 'm',
|
|
814
|
+
centimeters: 'cm',
|
|
815
|
+
millimeters: 'mm',
|
|
816
|
+
kilometers: 'km',
|
|
817
|
+
feet: 'ft',
|
|
818
|
+
inches: 'in',
|
|
819
|
+
yards: 'yd',
|
|
820
|
+
metersPerSecond: 'm/s',
|
|
821
|
+
kilometersPerHour: 'km/h',
|
|
822
|
+
milesPerHour: 'mph',
|
|
823
|
+
kilograms: 'kg',
|
|
824
|
+
grams: 'g',
|
|
825
|
+
pounds: 'lb',
|
|
826
|
+
tonnes: 't',
|
|
827
|
+
percent: '%',
|
|
828
|
+
rpm: 'rpm',
|
|
829
|
+
decibels: 'dB',
|
|
830
|
+
lux: 'lx',
|
|
831
|
+
lumens: 'lm',
|
|
832
|
+
ohms: 'Ω',
|
|
833
|
+
kilohms: 'kΩ'
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function unitSymbol(units: string): string {
|
|
837
|
+
return UNIT_SYMBOLS[units] ?? units ?? ''
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function formatValue(
|
|
841
|
+
value: any,
|
|
842
|
+
optsOrDecimals?: number | { units?: string; decimals?: number; ignoreUnitSymbol?: boolean },
|
|
843
|
+
units?: string,
|
|
844
|
+
ignoreUnitSymbol?: boolean
|
|
845
|
+
): string {
|
|
846
|
+
if (value == null || value === '') return ''
|
|
847
|
+
const num = Number(value)
|
|
848
|
+
if (isNaN(num)) return String(value)
|
|
849
|
+
|
|
850
|
+
let decimals = 0
|
|
851
|
+
let unitStr = ''
|
|
852
|
+
let ignoreSymbol = false
|
|
853
|
+
|
|
854
|
+
if (typeof optsOrDecimals === 'object' && optsOrDecimals !== null) {
|
|
855
|
+
decimals = optsOrDecimals.decimals ?? 0
|
|
856
|
+
unitStr = optsOrDecimals.units ?? ''
|
|
857
|
+
ignoreSymbol = optsOrDecimals.ignoreUnitSymbol ?? false
|
|
858
|
+
} else {
|
|
859
|
+
decimals = optsOrDecimals ?? 0
|
|
860
|
+
unitStr = units ?? ''
|
|
861
|
+
ignoreSymbol = ignoreUnitSymbol === true
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
let result = num.toFixed(decimals)
|
|
865
|
+
if (!ignoreSymbol && unitStr) {
|
|
866
|
+
result += ' ' + unitSymbol(unitStr)
|
|
867
|
+
}
|
|
868
|
+
return result
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function convertUnitValue(value: number, _units: string): number {
|
|
872
|
+
// In our context, values are already in the target unit — pass through
|
|
873
|
+
return value
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ---------------------------------------------------------------------------
|
|
877
|
+
// API Object builder — creates ctx.api for stateRenderFunction execution
|
|
878
|
+
// ---------------------------------------------------------------------------
|
|
879
|
+
|
|
880
|
+
export interface ScadaCallbacks {
|
|
881
|
+
onAction: (event: Event | null, actionName: string, args?: any, opts?: any) => void
|
|
882
|
+
onSetValue: (key: string, value: any) => void
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function createApi(callbacks: ScadaCallbacks) {
|
|
886
|
+
return {
|
|
887
|
+
cssAnimate(element: ScadaElement | any, duration: number): AnimationRunner {
|
|
888
|
+
const el = element instanceof ScadaElement ? element.native : element
|
|
889
|
+
// Cancel existing
|
|
890
|
+
const existing = elementAnimations.get(el as Element)
|
|
891
|
+
if (existing) existing.cancel()
|
|
892
|
+
const runner = new AnimationRunner(el as Element, duration)
|
|
893
|
+
elementAnimations.set(el as Element, runner)
|
|
894
|
+
return runner
|
|
895
|
+
},
|
|
896
|
+
|
|
897
|
+
cssAnimation(element: ScadaElement | any): AnimationRunner | null {
|
|
898
|
+
const el = element instanceof ScadaElement ? element.native : element
|
|
899
|
+
return elementAnimations.get(el as Element) ?? null
|
|
900
|
+
},
|
|
901
|
+
|
|
902
|
+
resetCssAnimation(element: ScadaElement | any): void {
|
|
903
|
+
const el = element instanceof ScadaElement ? element.native : element
|
|
904
|
+
const runner = elementAnimations.get(el as Element)
|
|
905
|
+
if (runner) {
|
|
906
|
+
runner.cancel()
|
|
907
|
+
elementAnimations.delete(el as Element)
|
|
908
|
+
}
|
|
909
|
+
},
|
|
910
|
+
|
|
911
|
+
finishCssAnimation(element: ScadaElement | any): void {
|
|
912
|
+
const el = element instanceof ScadaElement ? element.native : element
|
|
913
|
+
const runner = elementAnimations.get(el as Element)
|
|
914
|
+
if (runner) {
|
|
915
|
+
runner.finish()
|
|
916
|
+
elementAnimations.delete(el as Element)
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
|
|
920
|
+
font(
|
|
921
|
+
element: ScadaElement | ScadaElement[] | any,
|
|
922
|
+
fontDef: FontDef | any,
|
|
923
|
+
color?: string | any
|
|
924
|
+
): void {
|
|
925
|
+
// Handle arrays (e.g. from element.find())
|
|
926
|
+
if (Array.isArray(element)) {
|
|
927
|
+
for (const item of element) this.font(item, fontDef, color)
|
|
928
|
+
return
|
|
929
|
+
}
|
|
930
|
+
const el = element instanceof ScadaElement ? element.native : (element as Element)
|
|
931
|
+
if (fontDef && typeof fontDef === 'object' && fontDef.size) {
|
|
932
|
+
const family = fontDef.family || 'Roboto'
|
|
933
|
+
// Ensure a generic fallback so missing fonts don't fall through to serif
|
|
934
|
+
const hasFallback = /\b(sans-serif|serif|monospace|cursive|fantasy)\b/.test(family)
|
|
935
|
+
;(el as SVGElement).style.fontSize = fontDef.size + (fontDef.sizeUnit || 'px')
|
|
936
|
+
;(el as SVGElement).style.fontFamily = hasFallback ? family : family + ', sans-serif'
|
|
937
|
+
;(el as SVGElement).style.fontWeight = fontDef.weight || '400'
|
|
938
|
+
;(el as SVGElement).style.fontStyle = fontDef.style || 'normal'
|
|
939
|
+
}
|
|
940
|
+
if (color) {
|
|
941
|
+
const c = color instanceof ColorProcessor ? color.color : String(color)
|
|
942
|
+
el.setAttribute('fill', c)
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
|
|
946
|
+
text(element: ScadaElement | any, text: string): void {
|
|
947
|
+
const el = element instanceof ScadaElement ? element.native : (element as Element)
|
|
948
|
+
// SVG text elements use tspan children or direct textContent
|
|
949
|
+
const tspan = el.querySelector('tspan')
|
|
950
|
+
if (tspan) {
|
|
951
|
+
tspan.textContent = text
|
|
952
|
+
} else {
|
|
953
|
+
el.textContent = text
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
|
|
957
|
+
formatValue,
|
|
958
|
+
unitSymbol,
|
|
959
|
+
convertUnitValue,
|
|
960
|
+
|
|
961
|
+
callAction(event: Event | null, actionName: string, args?: any, opts?: any): void {
|
|
962
|
+
// Execute the opts.next callback if provided (for toggle patterns)
|
|
963
|
+
if (opts?.next) {
|
|
964
|
+
try {
|
|
965
|
+
opts.next()
|
|
966
|
+
} catch {
|
|
967
|
+
/* skip */
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
callbacks.onAction(event, actionName, args, opts)
|
|
971
|
+
},
|
|
972
|
+
|
|
973
|
+
setValue(key: string, value: any): void {
|
|
974
|
+
callbacks.onSetValue(key, value)
|
|
975
|
+
},
|
|
976
|
+
|
|
977
|
+
disable(element: ScadaElement | ScadaElement[] | any): void {
|
|
978
|
+
const items = Array.isArray(element) ? element : [element]
|
|
979
|
+
for (const item of items) {
|
|
980
|
+
const el = item instanceof ScadaElement ? item.native : (item as Element)
|
|
981
|
+
;(el as SVGElement).style.pointerEvents = 'none'
|
|
982
|
+
;(el as SVGElement).style.opacity = '0.5'
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
|
|
986
|
+
enable(element: ScadaElement | ScadaElement[] | any): void {
|
|
987
|
+
const items = Array.isArray(element) ? element : [element]
|
|
988
|
+
for (const item of items) {
|
|
989
|
+
const el = item instanceof ScadaElement ? item.native : (item as Element)
|
|
990
|
+
;(el as SVGElement).style.pointerEvents = ''
|
|
991
|
+
;(el as SVGElement).style.opacity = ''
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
|
|
995
|
+
icon(
|
|
996
|
+
element: ScadaElement | any,
|
|
997
|
+
iconName: string,
|
|
998
|
+
size?: number,
|
|
999
|
+
color?: string | ColorProcessor | any,
|
|
1000
|
+
_useSymbols?: boolean
|
|
1001
|
+
): void {
|
|
1002
|
+
const el = element instanceof ScadaElement ? element.native : (element as Element)
|
|
1003
|
+
ensureMaterialSymbolsFont()
|
|
1004
|
+
const fontSize = size ?? 14
|
|
1005
|
+
const fill =
|
|
1006
|
+
color instanceof ColorProcessor ? color.color : typeof color === 'string' ? color : '#000000'
|
|
1007
|
+
|
|
1008
|
+
// Reuse existing <text> if already injected, otherwise replace content
|
|
1009
|
+
let textEl = el.querySelector('text.__scada-icon') as SVGTextElement | null
|
|
1010
|
+
if (!textEl) {
|
|
1011
|
+
// Clear original SVG path content
|
|
1012
|
+
el.textContent = ''
|
|
1013
|
+
const NS = 'http://www.w3.org/2000/svg'
|
|
1014
|
+
textEl = document.createElementNS(NS, 'text') as SVGTextElement
|
|
1015
|
+
textEl.classList.add('__scada-icon')
|
|
1016
|
+
textEl.setAttribute('dominant-baseline', 'central')
|
|
1017
|
+
textEl.setAttribute('text-anchor', 'middle')
|
|
1018
|
+
el.appendChild(textEl)
|
|
1019
|
+
}
|
|
1020
|
+
textEl.setAttribute('font-family', 'Material Symbols Outlined')
|
|
1021
|
+
textEl.setAttribute('font-size', String(fontSize))
|
|
1022
|
+
textEl.setAttribute('fill', fill)
|
|
1023
|
+
textEl.textContent = iconName
|
|
1024
|
+
},
|
|
1025
|
+
|
|
1026
|
+
transform(element: ScadaElement | any, t: Record<string, any>): void {
|
|
1027
|
+
if (element instanceof ScadaElement) {
|
|
1028
|
+
element.transform(t)
|
|
1029
|
+
} else {
|
|
1030
|
+
new ScadaElement(element as Element).transform(t)
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
|
|
1034
|
+
connectorAnimate(
|
|
1035
|
+
element: ScadaElement | any,
|
|
1036
|
+
forwardPath: string,
|
|
1037
|
+
reversedPath: string
|
|
1038
|
+
): ConnectorAnimationRunner {
|
|
1039
|
+
const el = element instanceof ScadaElement ? element.native : (element as Element)
|
|
1040
|
+
// Cancel existing connector animation on this element
|
|
1041
|
+
const existing = connectorAnimations.get(el)
|
|
1042
|
+
if (existing) existing.finish()
|
|
1043
|
+
const runner = new ConnectorAnimationRunner(el, forwardPath, reversedPath)
|
|
1044
|
+
connectorAnimations.set(el, runner)
|
|
1045
|
+
return runner
|
|
1046
|
+
},
|
|
1047
|
+
|
|
1048
|
+
connectorAnimation(element: ScadaElement | any): ConnectorAnimationRunner | null {
|
|
1049
|
+
const el = element instanceof ScadaElement ? element.native : (element as Element)
|
|
1050
|
+
return connectorAnimations.get(el) ?? null
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// ---------------------------------------------------------------------------
|
|
1056
|
+
// Context builder + execution engine
|
|
1057
|
+
// ---------------------------------------------------------------------------
|
|
1058
|
+
|
|
1059
|
+
export interface ScadaContext {
|
|
1060
|
+
values: Record<string, any>
|
|
1061
|
+
properties: Record<string, any>
|
|
1062
|
+
tags: Record<string, ScadaElement[]>
|
|
1063
|
+
svg: ReturnType<typeof createSvgFactory>
|
|
1064
|
+
api: ReturnType<typeof createApi>
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Build the full ctx object for stateRenderFunction execution
|
|
1069
|
+
*/
|
|
1070
|
+
export function buildContext(
|
|
1071
|
+
svgEl: SVGSVGElement,
|
|
1072
|
+
metadata: SymbolMetadata,
|
|
1073
|
+
values: Record<string, any>,
|
|
1074
|
+
rawProperties: Record<string, any>,
|
|
1075
|
+
callbacks: ScadaCallbacks
|
|
1076
|
+
): ScadaContext {
|
|
1077
|
+
// Wrap color_settings properties in ColorProcessor
|
|
1078
|
+
const properties: Record<string, any> = {}
|
|
1079
|
+
for (const [key, val] of Object.entries(rawProperties)) {
|
|
1080
|
+
if (
|
|
1081
|
+
val &&
|
|
1082
|
+
typeof val === 'object' &&
|
|
1083
|
+
val.type &&
|
|
1084
|
+
(val.type === 'constant' ||
|
|
1085
|
+
val.type === 'gradient' ||
|
|
1086
|
+
val.type === 'range' ||
|
|
1087
|
+
val.type === 'function') &&
|
|
1088
|
+
'color' in val
|
|
1089
|
+
) {
|
|
1090
|
+
properties[key] = new ColorProcessor(val as ColorSettings)
|
|
1091
|
+
} else {
|
|
1092
|
+
properties[key] = val
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Merge metadata property defaults (inputData wins)
|
|
1097
|
+
for (const propDef of metadata.properties) {
|
|
1098
|
+
if (properties[propDef.id] === undefined && propDef.default !== undefined) {
|
|
1099
|
+
const def = propDef.default
|
|
1100
|
+
if (propDef.type === 'color_settings' && def && typeof def === 'object') {
|
|
1101
|
+
properties[propDef.id] = new ColorProcessor(def as ColorSettings)
|
|
1102
|
+
} else {
|
|
1103
|
+
properties[propDef.id] = def
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Merge metadata behavior defaults for values
|
|
1109
|
+
for (const beh of metadata.behavior) {
|
|
1110
|
+
if (beh.type === 'value' && values[beh.id] === undefined) {
|
|
1111
|
+
// Set sensible defaults for unbound values
|
|
1112
|
+
if (beh.valueType === 'BOOLEAN') values[beh.id] = false
|
|
1113
|
+
else if (beh.valueType === 'DOUBLE' || beh.valueType === 'INTEGER') values[beh.id] = 0
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const api = createApi(callbacks)
|
|
1118
|
+
const svgFactory = createSvgFactory(svgEl)
|
|
1119
|
+
|
|
1120
|
+
// Build tags map — lazy proxy that resolves on access
|
|
1121
|
+
const tagsCache: Record<string, ScadaElement[]> = {}
|
|
1122
|
+
const tags = new Proxy(tagsCache, {
|
|
1123
|
+
get(_target, prop: string) {
|
|
1124
|
+
if (!(prop in tagsCache)) {
|
|
1125
|
+
const els = svgEl.querySelectorAll(`[tb\\:tag="${prop}"]`)
|
|
1126
|
+
tagsCache[prop] = Array.from(els).map((e) => new ScadaElement(e))
|
|
1127
|
+
}
|
|
1128
|
+
return tagsCache[prop]
|
|
1129
|
+
}
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
return { values, properties, tags, svg: svgFactory, api }
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Execute a stateRenderFunction for a given element and context
|
|
1137
|
+
*/
|
|
1138
|
+
export function executeStateRender(fnString: string, element: ScadaElement, ctx: ScadaContext): void {
|
|
1139
|
+
try {
|
|
1140
|
+
// The stateRenderFunction has access to: element, ctx
|
|
1141
|
+
// It may also define local functions (e.g., drawMinorTicks)
|
|
1142
|
+
const fn = new Function('element', 'ctx', 'console', fnString)
|
|
1143
|
+
fn(element, ctx, console)
|
|
1144
|
+
} catch (e) {
|
|
1145
|
+
console.warn('[scada-runtime] stateRenderFunction error:', e)
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Execute an actionFunction for a given element and context
|
|
1151
|
+
*/
|
|
1152
|
+
export function executeAction(
|
|
1153
|
+
fnString: string,
|
|
1154
|
+
element: ScadaElement,
|
|
1155
|
+
ctx: ScadaContext,
|
|
1156
|
+
event: Event
|
|
1157
|
+
): void {
|
|
1158
|
+
try {
|
|
1159
|
+
const fn = new Function('element', 'ctx', 'event', fnString)
|
|
1160
|
+
fn(element, ctx, event)
|
|
1161
|
+
} catch (e) {
|
|
1162
|
+
if (import.meta.env?.DEV) {
|
|
1163
|
+
console.warn('[scada-runtime] actionFunction error:', e)
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Run all stateRenderFunctions for all tags in the metadata
|
|
1170
|
+
*/
|
|
1171
|
+
export function renderAllTags(svgEl: SVGSVGElement, metadata: SymbolMetadata, ctx: ScadaContext): void {
|
|
1172
|
+
// Execute top-level stateRenderFunction (e.g. pipe fluid flow setup).
|
|
1173
|
+
// Run twice: ThingsBoard stateRenderFunctions capture a `cssAnimation()`
|
|
1174
|
+
// reference before `animateFlow()` creates the animation, then call
|
|
1175
|
+
// `speed()` on the captured (null) ref. The second pass sees the animation
|
|
1176
|
+
// created by the first pass and applies speed/play/pause correctly.
|
|
1177
|
+
if (metadata.stateRenderFunction) {
|
|
1178
|
+
try {
|
|
1179
|
+
const fn = new Function('ctx', 'svg', 'console', metadata.stateRenderFunction)
|
|
1180
|
+
fn(ctx, ctx.svg, console)
|
|
1181
|
+
fn(ctx, ctx.svg, console)
|
|
1182
|
+
} catch (e) {
|
|
1183
|
+
console.warn('[scada-runtime] top-level stateRenderFunction error:', e)
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
for (const tagDef of metadata.tags) {
|
|
1188
|
+
const elements = svgEl.querySelectorAll(`[tb\\:tag="${tagDef.tag}"]`)
|
|
1189
|
+
if (!elements.length) continue
|
|
1190
|
+
|
|
1191
|
+
if (tagDef.stateRenderFunction) {
|
|
1192
|
+
for (const el of elements) {
|
|
1193
|
+
const wrapped = new ScadaElement(el)
|
|
1194
|
+
// Save original SVG display on first render, restore before each re-render.
|
|
1195
|
+
// Many ThingsBoard stateRenderFunctions only call show() conditionally
|
|
1196
|
+
// without a corresponding hide(), relying on the SVG's initial hidden state.
|
|
1197
|
+
const mem = getMemory(el)
|
|
1198
|
+
if (!mem.has('__origDisplay')) {
|
|
1199
|
+
mem.set('__origDisplay', (el as SVGElement).style.display)
|
|
1200
|
+
mem.set('__origVisibility', (el as SVGElement).style.visibility)
|
|
1201
|
+
mem.set('__origTransform', el.getAttribute('transform') ?? '')
|
|
1202
|
+
} else {
|
|
1203
|
+
;(el as SVGElement).style.display = mem.get('__origDisplay')
|
|
1204
|
+
;(el as SVGElement).style.visibility = mem.get('__origVisibility')
|
|
1205
|
+
const origT = mem.get('__origTransform')
|
|
1206
|
+
if (origT) {
|
|
1207
|
+
el.setAttribute('transform', origT)
|
|
1208
|
+
} else {
|
|
1209
|
+
el.removeAttribute('transform')
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
// Clear application-level cached state so that changes in
|
|
1213
|
+
// properties/values trigger a full redraw. Internal keys
|
|
1214
|
+
// prefixed with '__' are preserved.
|
|
1215
|
+
for (const key of mem.keys()) {
|
|
1216
|
+
if (!key.startsWith('__')) mem.delete(key)
|
|
1217
|
+
}
|
|
1218
|
+
executeStateRender(tagDef.stateRenderFunction, wrapped, ctx)
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Attach click event listeners for all action tags
|
|
1226
|
+
*/
|
|
1227
|
+
export function attachActions(
|
|
1228
|
+
svgEl: SVGSVGElement,
|
|
1229
|
+
metadata: SymbolMetadata,
|
|
1230
|
+
ctx: ScadaContext
|
|
1231
|
+
): Array<() => void> {
|
|
1232
|
+
const cleanups: Array<() => void> = []
|
|
1233
|
+
|
|
1234
|
+
for (const tagDef of metadata.tags) {
|
|
1235
|
+
if (!tagDef.actions) continue
|
|
1236
|
+
|
|
1237
|
+
const elements = svgEl.querySelectorAll(`[tb\\:tag="${tagDef.tag}"]`)
|
|
1238
|
+
for (const el of elements) {
|
|
1239
|
+
;(el as SVGElement).style.cursor = 'pointer'
|
|
1240
|
+
|
|
1241
|
+
for (const [actionName, actionDef] of Object.entries(tagDef.actions)) {
|
|
1242
|
+
if (actionName === 'click' && actionDef.actionFunction) {
|
|
1243
|
+
const handler = (event: Event) => {
|
|
1244
|
+
const wrapped = new ScadaElement(el)
|
|
1245
|
+
executeAction(actionDef.actionFunction, wrapped, ctx, event)
|
|
1246
|
+
}
|
|
1247
|
+
el.addEventListener('click', handler)
|
|
1248
|
+
cleanups.push(() => el.removeEventListener('click', handler))
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
return cleanups
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* Extract defaults from SVG metadata, structured to match the inputData shape.
|
|
1259
|
+
* Properties with type color/color_settings/font go into `styling`,
|
|
1260
|
+
* everything else is top-level. Behavior values go into `bindings`.
|
|
1261
|
+
*/
|
|
1262
|
+
export function getSymbolDefaults(metadata: SymbolMetadata): Record<string, any> {
|
|
1263
|
+
const defaults: Record<string, any> = {}
|
|
1264
|
+
const styling: Record<string, any> = {}
|
|
1265
|
+
const bindings: Record<string, any> = {}
|
|
1266
|
+
|
|
1267
|
+
const STYLING_TYPES = new Set(['color', 'color_settings', 'font'])
|
|
1268
|
+
|
|
1269
|
+
for (const prop of metadata.properties) {
|
|
1270
|
+
if (prop.default === undefined) continue
|
|
1271
|
+
if (STYLING_TYPES.has(prop.type)) {
|
|
1272
|
+
styling[prop.id] = prop.default
|
|
1273
|
+
} else {
|
|
1274
|
+
defaults[prop.id] = prop.default
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
for (const beh of metadata.behavior) {
|
|
1279
|
+
if (beh.type === 'value') {
|
|
1280
|
+
if (beh.valueType === 'BOOLEAN') bindings[beh.id] = false
|
|
1281
|
+
else if (beh.valueType === 'DOUBLE' || beh.valueType === 'INTEGER') bindings[beh.id] = 0
|
|
1282
|
+
else bindings[beh.id] = ''
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (Object.keys(styling).length > 0) defaults.styling = styling
|
|
1287
|
+
if (Object.keys(bindings).length > 0) defaults.bindings = bindings
|
|
1288
|
+
|
|
1289
|
+
return defaults
|
|
1290
|
+
}
|