@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.
Files changed (332) hide show
  1. package/README.md +50 -0
  2. package/dist/scada_symbols/3-phase-voltage-relay-hp.svg +549 -0
  3. package/dist/scada_symbols/apartments-hp.svg +344 -0
  4. package/dist/scada_symbols/battery-hp.svg +473 -0
  5. package/dist/scada_symbols/bottom-flow-meter.svg +933 -0
  6. package/dist/scada_symbols/bottom-light-bulb-hp.svg +346 -0
  7. package/dist/scada_symbols/bottom-right-elbow-connector-hp.svg +233 -0
  8. package/dist/scada_symbols/bottom-right-elbow-pipe.svg +532 -0
  9. package/dist/scada_symbols/bottom-tee-connector-hp.svg +470 -0
  10. package/dist/scada_symbols/bottom-tee-pipe.svg +857 -0
  11. package/dist/scada_symbols/centrifugal-pump.svg +626 -0
  12. package/dist/scada_symbols/conical-tank.svg +1136 -0
  13. package/dist/scada_symbols/consumers-hp.svg +346 -0
  14. package/dist/scada_symbols/control-panel-hp.svg +332 -0
  15. package/dist/scada_symbols/crane-hp.svg +346 -0
  16. package/dist/scada_symbols/cross-connector-hp.svg +586 -0
  17. package/dist/scada_symbols/cross-pipe.svg +1051 -0
  18. package/dist/scada_symbols/curcuit-breaker-hp.svg +444 -0
  19. package/dist/scada_symbols/cylindrical-tank.svg +1332 -0
  20. package/dist/scada_symbols/drawwork-hp.svg +355 -0
  21. package/dist/scada_symbols/drill-hp.svg +354 -0
  22. package/dist/scada_symbols/drilling-line-hp.svg +361 -0
  23. package/dist/scada_symbols/drilling-rig-hp.svg +354 -0
  24. package/dist/scada_symbols/dynamic-horizontal-scale-hp.svg +605 -0
  25. package/dist/scada_symbols/dynamic-vertical-scale-hp.svg +598 -0
  26. package/dist/scada_symbols/electrical-distribution-board-hp.svg +332 -0
  27. package/dist/scada_symbols/electrical-engine-hp.svg +360 -0
  28. package/dist/scada_symbols/elevated-tank.svg +1564 -0
  29. package/dist/scada_symbols/energy-meter-hp.svg +488 -0
  30. package/dist/scada_symbols/extra-long-horizontal-pipe.svg +426 -0
  31. package/dist/scada_symbols/extra-long-vertical-pipe.svg +438 -0
  32. package/dist/scada_symbols/filter-hp.svg +287 -0
  33. package/dist/scada_symbols/four-rate-energy-meter-hp.svg +891 -0
  34. package/dist/scada_symbols/fuel-generator-hp.svg +359 -0
  35. package/dist/scada_symbols/gas-preventer-hp.svg +361 -0
  36. package/dist/scada_symbols/gas-wellhead-hp.svg +363 -0
  37. package/dist/scada_symbols/heat-exchanger-hp.svg +363 -0
  38. package/dist/scada_symbols/heat-pump-hp.svg +532 -0
  39. package/dist/scada_symbols/high-voltage-tower-hp.svg +297 -0
  40. package/dist/scada_symbols/hook-hp.svg +293 -0
  41. package/dist/scada_symbols/horizontal-ball-valve.svg +271 -0
  42. package/dist/scada_symbols/horizontal-broken-pipe.svg +97 -0
  43. package/dist/scada_symbols/horizontal-connector-hp.svg +268 -0
  44. package/dist/scada_symbols/horizontal-curcuit-breaker-hp.svg +439 -0
  45. package/dist/scada_symbols/horizontal-energy-system-controller-hp.svg +378 -0
  46. package/dist/scada_symbols/horizontal-inline-flow-meter.svg +913 -0
  47. package/dist/scada_symbols/horizontal-pipe.svg +422 -0
  48. package/dist/scada_symbols/horizontal-tank-hp.svg +524 -0
  49. package/dist/scada_symbols/horizontal-tank.svg +1343 -0
  50. package/dist/scada_symbols/horizontal-valve-hp.svg +392 -0
  51. package/dist/scada_symbols/horizontal-wheel-valve.svg +300 -0
  52. package/dist/scada_symbols/house-hp.svg +349 -0
  53. package/dist/scada_symbols/index.json +702 -0
  54. package/dist/scada_symbols/industrial-fuel-generator-hp.svg +358 -0
  55. package/dist/scada_symbols/inverter-hp.svg +578 -0
  56. package/dist/scada_symbols/large-conical-tank.svg +1146 -0
  57. package/dist/scada_symbols/large-cylindrical-tank.svg +1323 -0
  58. package/dist/scada_symbols/large-horizontal-separator-connector-hp.svg +342 -0
  59. package/dist/scada_symbols/large-horizontal-separator-hp.svg +350 -0
  60. package/dist/scada_symbols/large-inverter-hp.svg +578 -0
  61. package/dist/scada_symbols/large-stand-cylindrical-tank.svg +1384 -0
  62. package/dist/scada_symbols/large-stand-vertical-tank.svg +1386 -0
  63. package/dist/scada_symbols/large-vertical-separator-connector-hp.svg +350 -0
  64. package/dist/scada_symbols/large-vertical-separator-hp.svg +350 -0
  65. package/dist/scada_symbols/large-vertical-tank.svg +1325 -0
  66. package/dist/scada_symbols/leak-sensor.svg +158 -0
  67. package/dist/scada_symbols/left-analog-water-level-meter.svg +748 -0
  68. package/dist/scada_symbols/left-bottom-elbow-connector-hp.svg +233 -0
  69. package/dist/scada_symbols/left-bottom-elbow-pipe.svg +532 -0
  70. package/dist/scada_symbols/left-drain-pipe.svg +166 -0
  71. package/dist/scada_symbols/left-elbow-drain-pipe.svg +181 -0
  72. package/dist/scada_symbols/left-flow-meter.svg +911 -0
  73. package/dist/scada_symbols/left-heat-pump.svg +939 -0
  74. package/dist/scada_symbols/left-motor-pump.svg +541 -0
  75. package/dist/scada_symbols/left-tee-connector-hp.svg +470 -0
  76. package/dist/scada_symbols/left-tee-pipe.svg +869 -0
  77. package/dist/scada_symbols/left-top-elbow-connector-hp.svg +233 -0
  78. package/dist/scada_symbols/left-top-elbow-pipe.svg +532 -0
  79. package/dist/scada_symbols/long-bottom-filter.svg +88 -0
  80. package/dist/scada_symbols/long-horizontal-broken-pipe.svg +97 -0
  81. package/dist/scada_symbols/long-horizontal-connector-hp.svg +269 -0
  82. package/dist/scada_symbols/long-horizontal-pipe.svg +488 -0
  83. package/dist/scada_symbols/long-top-filter.svg +88 -0
  84. package/dist/scada_symbols/long-vertical-broken-pipe.svg +97 -0
  85. package/dist/scada_symbols/long-vertical-connector-hp.svg +267 -0
  86. package/dist/scada_symbols/long-vertical-pipe.svg +485 -0
  87. package/dist/scada_symbols/low-voltage-tower-hp.svg +278 -0
  88. package/dist/scada_symbols/low-voltage-transformer-tower-hp.svg +334 -0
  89. package/dist/scada_symbols/manufacture-hp.svg +344 -0
  90. package/dist/scada_symbols/meter.svg +751 -0
  91. package/dist/scada_symbols/oil-pump-hp.svg +357 -0
  92. package/dist/scada_symbols/platform-hp.svg +267 -0
  93. package/dist/scada_symbols/pool-hp.svg +523 -0
  94. package/dist/scada_symbols/pool.svg +998 -0
  95. package/dist/scada_symbols/power-socket-hp.svg +371 -0
  96. package/dist/scada_symbols/power-transformer-hp.svg +359 -0
  97. package/dist/scada_symbols/preventer-hp.svg +343 -0
  98. package/dist/scada_symbols/pump-hp.svg +368 -0
  99. package/dist/scada_symbols/right-analog-water-level-meter.svg +748 -0
  100. package/dist/scada_symbols/right-drain-pipe.svg +167 -0
  101. package/dist/scada_symbols/right-elbow-drain-pipe.svg +178 -0
  102. package/dist/scada_symbols/right-flow-meter.svg +911 -0
  103. package/dist/scada_symbols/right-heat-pump.svg +948 -0
  104. package/dist/scada_symbols/right-motor-pump.svg +541 -0
  105. package/dist/scada_symbols/right-tee-connector-hp.svg +470 -0
  106. package/dist/scada_symbols/right-tee-pipe.svg +857 -0
  107. package/dist/scada_symbols/rotor-hp.svg +346 -0
  108. package/dist/scada_symbols/sand-filter-hp.svg +652 -0
  109. package/dist/scada_symbols/sand-filter.svg +704 -0
  110. package/dist/scada_symbols/short-bottom-filter.svg +88 -0
  111. package/dist/scada_symbols/short-left-drain-pipe.svg +160 -0
  112. package/dist/scada_symbols/short-right-drain-pipe.svg +160 -0
  113. package/dist/scada_symbols/short-top-filter.svg +88 -0
  114. package/dist/scada_symbols/short-vertical-tank-hp.svg +522 -0
  115. package/dist/scada_symbols/simple-horizontal-scale-hp.svg +509 -0
  116. package/dist/scada_symbols/simple-vertical-scale-hp.svg +509 -0
  117. package/dist/scada_symbols/single-key-switch-hp.svg +373 -0
  118. package/dist/scada_symbols/small-cylindrical-tank.svg +1306 -0
  119. package/dist/scada_symbols/small-horizontal-separator-connector-hp.svg +342 -0
  120. package/dist/scada_symbols/small-horizontal-separator-hp.svg +342 -0
  121. package/dist/scada_symbols/small-left-meter.svg +725 -0
  122. package/dist/scada_symbols/small-left-motor-pump.svg +443 -0
  123. package/dist/scada_symbols/small-meter.svg +694 -0
  124. package/dist/scada_symbols/small-power-transformer-hp.svg +359 -0
  125. package/dist/scada_symbols/small-right-center.svg +725 -0
  126. package/dist/scada_symbols/small-right-motor-pump.svg +443 -0
  127. package/dist/scada_symbols/small-spherical-tank.svg +1362 -0
  128. package/dist/scada_symbols/small-vertical-separator-connector-hp.svg +342 -0
  129. package/dist/scada_symbols/small-vertical-separator-hp.svg +342 -0
  130. package/dist/scada_symbols/solar-panel-hp.svg +353 -0
  131. package/dist/scada_symbols/spherical-tank.svg +1392 -0
  132. package/dist/scada_symbols/stand-cylindrical-tank.svg +1393 -0
  133. package/dist/scada_symbols/stand-horizontal-tank.svg +1404 -0
  134. package/dist/scada_symbols/stand-solar-panel-hp.svg +357 -0
  135. package/dist/scada_symbols/stand-vertical-short-tank.svg +1366 -0
  136. package/dist/scada_symbols/stand-vertical-tank.svg +1386 -0
  137. package/dist/scada_symbols/three-rate-energy-meter-hp.svg +759 -0
  138. package/dist/scada_symbols/top-flow-meter.svg +927 -0
  139. package/dist/scada_symbols/top-light-bulb-hp.svg +346 -0
  140. package/dist/scada_symbols/top-right-elbow-connector-hp.svg +233 -0
  141. package/dist/scada_symbols/top-right-elbow-pipe.svg +532 -0
  142. package/dist/scada_symbols/top-tee-connector-hp.svg +470 -0
  143. package/dist/scada_symbols/top-tee-pipe.svg +857 -0
  144. package/dist/scada_symbols/turbine-hp.svg +377 -0
  145. package/dist/scada_symbols/two-key-switch-hp.svg +489 -0
  146. package/dist/scada_symbols/two-rate-energy-meter-hp.svg +627 -0
  147. package/dist/scada_symbols/vertical-ball-valve.svg +271 -0
  148. package/dist/scada_symbols/vertical-broken-pipe.svg +97 -0
  149. package/dist/scada_symbols/vertical-connector-hp.svg +269 -0
  150. package/dist/scada_symbols/vertical-energy-system-controller-hp.svg +378 -0
  151. package/dist/scada_symbols/vertical-inline-flow-meter.svg +913 -0
  152. package/dist/scada_symbols/vertical-pipe.svg +422 -0
  153. package/dist/scada_symbols/vertical-short-tank.svg +1305 -0
  154. package/dist/scada_symbols/vertical-tank-hp.svg +524 -0
  155. package/dist/scada_symbols/vertical-tank.svg +1326 -0
  156. package/dist/scada_symbols/vertical-valve-hp.svg +392 -0
  157. package/dist/scada_symbols/vertical-wheel-valve.svg +300 -0
  158. package/dist/scada_symbols/voltage-relay-hp.svg +440 -0
  159. package/dist/scada_symbols/voltage-stabilizer-hp.svg +584 -0
  160. package/dist/scada_symbols/waterstop.svg +276 -0
  161. package/dist/scada_symbols/wind-turbine-cluster-hp.svg +370 -0
  162. package/dist/scada_symbols/wind-turbine-hp.svg +360 -0
  163. package/dist/widget-scada.js +1331 -0
  164. package/dist/widget-scada.js.map +1 -0
  165. package/package.json +61 -0
  166. package/scada_symbols/3-phase-voltage-relay-hp.svg +549 -0
  167. package/scada_symbols/apartments-hp.svg +344 -0
  168. package/scada_symbols/battery-hp.svg +473 -0
  169. package/scada_symbols/bottom-flow-meter.svg +933 -0
  170. package/scada_symbols/bottom-light-bulb-hp.svg +346 -0
  171. package/scada_symbols/bottom-right-elbow-connector-hp.svg +233 -0
  172. package/scada_symbols/bottom-right-elbow-pipe.svg +532 -0
  173. package/scada_symbols/bottom-tee-connector-hp.svg +470 -0
  174. package/scada_symbols/bottom-tee-pipe.svg +857 -0
  175. package/scada_symbols/centrifugal-pump.svg +626 -0
  176. package/scada_symbols/conical-tank.svg +1136 -0
  177. package/scada_symbols/consumers-hp.svg +346 -0
  178. package/scada_symbols/control-panel-hp.svg +332 -0
  179. package/scada_symbols/crane-hp.svg +346 -0
  180. package/scada_symbols/cross-connector-hp.svg +586 -0
  181. package/scada_symbols/cross-pipe.svg +1051 -0
  182. package/scada_symbols/curcuit-breaker-hp.svg +444 -0
  183. package/scada_symbols/cylindrical-tank.svg +1332 -0
  184. package/scada_symbols/drawwork-hp.svg +355 -0
  185. package/scada_symbols/drill-hp.svg +354 -0
  186. package/scada_symbols/drilling-line-hp.svg +361 -0
  187. package/scada_symbols/drilling-rig-hp.svg +354 -0
  188. package/scada_symbols/dynamic-horizontal-scale-hp.svg +605 -0
  189. package/scada_symbols/dynamic-vertical-scale-hp.svg +598 -0
  190. package/scada_symbols/electrical-distribution-board-hp.svg +332 -0
  191. package/scada_symbols/electrical-engine-hp.svg +360 -0
  192. package/scada_symbols/elevated-tank.svg +1564 -0
  193. package/scada_symbols/energy-meter-hp.svg +488 -0
  194. package/scada_symbols/extra-long-horizontal-pipe.svg +426 -0
  195. package/scada_symbols/extra-long-vertical-pipe.svg +438 -0
  196. package/scada_symbols/filter-hp.svg +287 -0
  197. package/scada_symbols/four-rate-energy-meter-hp.svg +891 -0
  198. package/scada_symbols/fuel-generator-hp.svg +359 -0
  199. package/scada_symbols/gas-preventer-hp.svg +361 -0
  200. package/scada_symbols/gas-wellhead-hp.svg +363 -0
  201. package/scada_symbols/heat-exchanger-hp.svg +363 -0
  202. package/scada_symbols/heat-pump-hp.svg +532 -0
  203. package/scada_symbols/high-voltage-tower-hp.svg +297 -0
  204. package/scada_symbols/hook-hp.svg +293 -0
  205. package/scada_symbols/horizontal-ball-valve.svg +271 -0
  206. package/scada_symbols/horizontal-broken-pipe.svg +97 -0
  207. package/scada_symbols/horizontal-connector-hp.svg +268 -0
  208. package/scada_symbols/horizontal-curcuit-breaker-hp.svg +439 -0
  209. package/scada_symbols/horizontal-energy-system-controller-hp.svg +378 -0
  210. package/scada_symbols/horizontal-inline-flow-meter.svg +913 -0
  211. package/scada_symbols/horizontal-pipe.svg +422 -0
  212. package/scada_symbols/horizontal-tank-hp.svg +524 -0
  213. package/scada_symbols/horizontal-tank.svg +1343 -0
  214. package/scada_symbols/horizontal-valve-hp.svg +392 -0
  215. package/scada_symbols/horizontal-wheel-valve.svg +300 -0
  216. package/scada_symbols/house-hp.svg +349 -0
  217. package/scada_symbols/index.json +702 -0
  218. package/scada_symbols/industrial-fuel-generator-hp.svg +358 -0
  219. package/scada_symbols/inverter-hp.svg +578 -0
  220. package/scada_symbols/large-conical-tank.svg +1146 -0
  221. package/scada_symbols/large-cylindrical-tank.svg +1323 -0
  222. package/scada_symbols/large-horizontal-separator-connector-hp.svg +342 -0
  223. package/scada_symbols/large-horizontal-separator-hp.svg +350 -0
  224. package/scada_symbols/large-inverter-hp.svg +578 -0
  225. package/scada_symbols/large-stand-cylindrical-tank.svg +1384 -0
  226. package/scada_symbols/large-stand-vertical-tank.svg +1386 -0
  227. package/scada_symbols/large-vertical-separator-connector-hp.svg +350 -0
  228. package/scada_symbols/large-vertical-separator-hp.svg +350 -0
  229. package/scada_symbols/large-vertical-tank.svg +1325 -0
  230. package/scada_symbols/leak-sensor.svg +158 -0
  231. package/scada_symbols/left-analog-water-level-meter.svg +748 -0
  232. package/scada_symbols/left-bottom-elbow-connector-hp.svg +233 -0
  233. package/scada_symbols/left-bottom-elbow-pipe.svg +532 -0
  234. package/scada_symbols/left-drain-pipe.svg +166 -0
  235. package/scada_symbols/left-elbow-drain-pipe.svg +181 -0
  236. package/scada_symbols/left-flow-meter.svg +911 -0
  237. package/scada_symbols/left-heat-pump.svg +939 -0
  238. package/scada_symbols/left-motor-pump.svg +541 -0
  239. package/scada_symbols/left-tee-connector-hp.svg +470 -0
  240. package/scada_symbols/left-tee-pipe.svg +869 -0
  241. package/scada_symbols/left-top-elbow-connector-hp.svg +233 -0
  242. package/scada_symbols/left-top-elbow-pipe.svg +532 -0
  243. package/scada_symbols/long-bottom-filter.svg +88 -0
  244. package/scada_symbols/long-horizontal-broken-pipe.svg +97 -0
  245. package/scada_symbols/long-horizontal-connector-hp.svg +269 -0
  246. package/scada_symbols/long-horizontal-pipe.svg +488 -0
  247. package/scada_symbols/long-top-filter.svg +88 -0
  248. package/scada_symbols/long-vertical-broken-pipe.svg +97 -0
  249. package/scada_symbols/long-vertical-connector-hp.svg +267 -0
  250. package/scada_symbols/long-vertical-pipe.svg +485 -0
  251. package/scada_symbols/low-voltage-tower-hp.svg +278 -0
  252. package/scada_symbols/low-voltage-transformer-tower-hp.svg +334 -0
  253. package/scada_symbols/manufacture-hp.svg +344 -0
  254. package/scada_symbols/meter.svg +751 -0
  255. package/scada_symbols/oil-pump-hp.svg +357 -0
  256. package/scada_symbols/platform-hp.svg +267 -0
  257. package/scada_symbols/pool-hp.svg +523 -0
  258. package/scada_symbols/pool.svg +998 -0
  259. package/scada_symbols/power-socket-hp.svg +371 -0
  260. package/scada_symbols/power-transformer-hp.svg +359 -0
  261. package/scada_symbols/preventer-hp.svg +343 -0
  262. package/scada_symbols/pump-hp.svg +368 -0
  263. package/scada_symbols/right-analog-water-level-meter.svg +748 -0
  264. package/scada_symbols/right-drain-pipe.svg +167 -0
  265. package/scada_symbols/right-elbow-drain-pipe.svg +178 -0
  266. package/scada_symbols/right-flow-meter.svg +911 -0
  267. package/scada_symbols/right-heat-pump.svg +948 -0
  268. package/scada_symbols/right-motor-pump.svg +541 -0
  269. package/scada_symbols/right-tee-connector-hp.svg +470 -0
  270. package/scada_symbols/right-tee-pipe.svg +857 -0
  271. package/scada_symbols/rotor-hp.svg +346 -0
  272. package/scada_symbols/sand-filter-hp.svg +652 -0
  273. package/scada_symbols/sand-filter.svg +704 -0
  274. package/scada_symbols/short-bottom-filter.svg +88 -0
  275. package/scada_symbols/short-left-drain-pipe.svg +160 -0
  276. package/scada_symbols/short-right-drain-pipe.svg +160 -0
  277. package/scada_symbols/short-top-filter.svg +88 -0
  278. package/scada_symbols/short-vertical-tank-hp.svg +522 -0
  279. package/scada_symbols/simple-horizontal-scale-hp.svg +509 -0
  280. package/scada_symbols/simple-vertical-scale-hp.svg +509 -0
  281. package/scada_symbols/single-key-switch-hp.svg +373 -0
  282. package/scada_symbols/small-cylindrical-tank.svg +1306 -0
  283. package/scada_symbols/small-horizontal-separator-connector-hp.svg +342 -0
  284. package/scada_symbols/small-horizontal-separator-hp.svg +342 -0
  285. package/scada_symbols/small-left-meter.svg +725 -0
  286. package/scada_symbols/small-left-motor-pump.svg +443 -0
  287. package/scada_symbols/small-meter.svg +694 -0
  288. package/scada_symbols/small-power-transformer-hp.svg +359 -0
  289. package/scada_symbols/small-right-center.svg +725 -0
  290. package/scada_symbols/small-right-motor-pump.svg +443 -0
  291. package/scada_symbols/small-spherical-tank.svg +1362 -0
  292. package/scada_symbols/small-vertical-separator-connector-hp.svg +342 -0
  293. package/scada_symbols/small-vertical-separator-hp.svg +342 -0
  294. package/scada_symbols/solar-panel-hp.svg +353 -0
  295. package/scada_symbols/spherical-tank.svg +1392 -0
  296. package/scada_symbols/stand-cylindrical-tank.svg +1393 -0
  297. package/scada_symbols/stand-horizontal-tank.svg +1404 -0
  298. package/scada_symbols/stand-solar-panel-hp.svg +357 -0
  299. package/scada_symbols/stand-vertical-short-tank.svg +1366 -0
  300. package/scada_symbols/stand-vertical-tank.svg +1386 -0
  301. package/scada_symbols/three-rate-energy-meter-hp.svg +759 -0
  302. package/scada_symbols/top-flow-meter.svg +927 -0
  303. package/scada_symbols/top-light-bulb-hp.svg +346 -0
  304. package/scada_symbols/top-right-elbow-connector-hp.svg +233 -0
  305. package/scada_symbols/top-right-elbow-pipe.svg +532 -0
  306. package/scada_symbols/top-tee-connector-hp.svg +470 -0
  307. package/scada_symbols/top-tee-pipe.svg +857 -0
  308. package/scada_symbols/turbine-hp.svg +377 -0
  309. package/scada_symbols/two-key-switch-hp.svg +489 -0
  310. package/scada_symbols/two-rate-energy-meter-hp.svg +627 -0
  311. package/scada_symbols/vertical-ball-valve.svg +271 -0
  312. package/scada_symbols/vertical-broken-pipe.svg +97 -0
  313. package/scada_symbols/vertical-connector-hp.svg +269 -0
  314. package/scada_symbols/vertical-energy-system-controller-hp.svg +378 -0
  315. package/scada_symbols/vertical-inline-flow-meter.svg +913 -0
  316. package/scada_symbols/vertical-pipe.svg +422 -0
  317. package/scada_symbols/vertical-short-tank.svg +1305 -0
  318. package/scada_symbols/vertical-tank-hp.svg +524 -0
  319. package/scada_symbols/vertical-tank.svg +1326 -0
  320. package/scada_symbols/vertical-valve-hp.svg +392 -0
  321. package/scada_symbols/vertical-wheel-valve.svg +300 -0
  322. package/scada_symbols/voltage-relay-hp.svg +440 -0
  323. package/scada_symbols/voltage-stabilizer-hp.svg +584 -0
  324. package/scada_symbols/waterstop.svg +276 -0
  325. package/scada_symbols/wind-turbine-cluster-hp.svg +370 -0
  326. package/scada_symbols/wind-turbine-hp.svg +360 -0
  327. package/src/default-data.json +18 -0
  328. package/src/definition-schema.d.ts +1521 -0
  329. package/src/definition-schema.json +5228 -0
  330. package/src/scada-runtime.ts +1290 -0
  331. package/src/widget-scada.ts +269 -0
  332. 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
+ }