# HG changeset patch # User Edouard Tisserant <edouard.tisserant@gmail.com> # Date 1658091215 -7200 # Node ID f5850ce25cafa95272b7b567d81811ca13ec736b # Parent ffe4e46a31631c7594292070b93aa13f1c1c6b49# Parent 59158e360b8c1335c6eb4f6539cc9d9f103dc299 Merge from default branch + fix traffic light example SVG missing background + update generated XSLT diff -r ffe4e46a3163 -r f5850ce25caf exemples/svghmi_traffic_light/svghmi_0@svghmi/svghmi.svg --- a/exemples/svghmi_traffic_light/svghmi_0@svghmi/svghmi.svg Sun Jul 17 22:46:26 2022 +0200 +++ b/exemples/svghmi_traffic_light/svghmi_0@svghmi/svghmi.svg Sun Jul 17 22:53:35 2022 +0200 @@ -1251,14 +1251,14 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1.979899" - inkscape:cx="205.65994" - inkscape:cy="103.00174" + inkscape:cx="52.116754" + inkscape:cy="96.940825" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" units="px" - inkscape:window-width="1600" - inkscape:window-height="836" + inkscape:window-width="3840" + inkscape:window-height="2096" inkscape:window-x="0" inkscape:window-y="27" inkscape:window-maximized="1" @@ -1274,7 +1274,7 @@ <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> + <dc:title /> </cc:Work> </rdf:RDF> </metadata> @@ -1283,6 +1283,14 @@ inkscape:groupmode="layer" id="layer1" transform="translate(37.474617,-760.93329)"> + <rect + style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none" + id="rect250" + width="320" + height="240" + x="-37.474617" + y="760.93329" + inkscape:label="HMI:Page:Home" /> <path style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#282828;fill-opacity:1;stroke:none;stroke-width:2.04116011;marker:none;enable-background:accumulate" d="m 114.28125,14.28125 v 130 h 18.9375 v 93.5625 h 5.71875 V 176.4375 h 8.90625 v 15.71875 h 36.4375 v -32.5 h -36.4375 v 12.125 h -8.90625 v -27.5 h 21.78125 v -130 z" @@ -1529,13 +1537,5 @@ x="62.818459" y="812.17749" style="font-size:6.17188501px;line-height:1.25;font-family:sans-serif">ON</tspan></text> - <rect - style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none" - id="rect250" - width="320" - height="240" - x="-37.474617" - y="760.93329" - inkscape:label="HMI:Page:Home" /> </g> </svg> diff -r ffe4e46a3163 -r f5850ce25caf svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Sun Jul 17 22:46:26 2022 +0200 +++ b/svghmi/gen_index_xhtml.xslt Sun Jul 17 22:53:35 2022 +0200 @@ -2059,6 +2059,10 @@ </xsl:text> <xsl:text>} </xsl:text> + <xsl:message terminate="no"> + <xsl:value-of select="@type"/> + <xsl:text> widget is used in SVG but widget type is not declared</xsl:text> + </xsl:message> </xsl:template> <xsl:variable name="included_ids" select="$parsed_widgets/widget[not(@type = $excluded_types) and not(@id = $discardable_elements/@id)]/@id"/> <xsl:variable name="hmi_widgets" select="$hmi_elements[@id = $included_ids]"/> @@ -2080,6 +2084,8 @@ </xsl:text> <xsl:text> </xsl:text> + <xsl:text> +</xsl:text> </xsl:template> <xsl:template name="defs_by_labels"> <xsl:param name="labels" select="''"/> @@ -4880,6 +4886,7 @@ </xsl:variable> <xsl:variable name="have_edit" select="string-length($edit_elt)>0"/> <xsl:value-of select="$edit_elt"/> + <xsl:variable name="action_elements" select="$hmi_element/*[regexp:test(@inkscape:label,'^[=+\-].+')]"/> <xsl:if test="$have_value"> <xsl:text> frequency: 5, </xsl:text> @@ -4920,6 +4927,14 @@ <xsl:text> }, </xsl:text> </xsl:if> + <xsl:for-each select="$action_elements"> + <xsl:text> action_elt_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:text>: id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>"), +</xsl:text> + </xsl:for-each> <xsl:text> init: function() { </xsl:text> <xsl:if test="$have_edit"> @@ -4936,10 +4951,10 @@ <xsl:text> this.animate(); </xsl:text> </xsl:if> - <xsl:for-each select="$hmi_element/*[regexp:test(@inkscape:label,'^[=+\-].+')]"> - <xsl:text> id("</xsl:text> - <xsl:value-of select="@id"/> - <xsl:text>").onclick = () => this.on_op_click("</xsl:text> + <xsl:for-each select="$action_elements"> + <xsl:text> this.action_elt_</xsl:text> + <xsl:value-of select="position()"/> + <xsl:text>.onclick = () => this.on_op_click("</xsl:text> <xsl:value-of select="func:escape_quotes(@inkscape:label)"/> <xsl:text>"); </xsl:text> @@ -5696,7 +5711,7 @@ </xsl:text> <xsl:text>.fade-out-page { </xsl:text> - <xsl:text> animation: fadeOut 0.6s both; + <xsl:text> animation: cubic-bezier(0, 0.8, 0.6, 1) fadeOut 0.6s both; </xsl:text> <xsl:text>} </xsl:text> @@ -6105,6 +6120,14 @@ <xsl:text> }, </xsl:text> </xsl:template> + <xsl:template match="widget[@type='List']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>ListWidget</xsl:text> + <xsl:text> extends Widget{ +</xsl:text> + <xsl:text>} +</xsl:text> + </xsl:template> <xsl:template match="widget[@type='ListSwitch']" mode="widget_desc"> <type> <xsl:value-of select="@type"/> @@ -7867,6 +7890,14 @@ <xsl:text> ].reverse(), </xsl:text> </xsl:template> + <xsl:template match="widget[@type='TextList']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>TextListWidget</xsl:text> + <xsl:text> extends Widget{ +</xsl:text> + <xsl:text>} +</xsl:text> + </xsl:template> <xsl:template match="widget[@type='TextStyleList']" mode="widget_desc"> <type> <xsl:value-of select="@type"/> @@ -7908,6 +7939,14 @@ <xsl:text> }, </xsl:text> </xsl:template> + <xsl:template match="widget[@type='TextStyleList']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>TextStyleListWidget</xsl:text> + <xsl:text> extends Widget{ +</xsl:text> + <xsl:text>} +</xsl:text> + </xsl:template> <xsl:template match="widget[@type='ToggleButton']" mode="widget_desc"> <type> <xsl:value-of select="@type"/> @@ -10499,49 +10538,309 @@ </xsl:text> <xsl:text>function animate() { </xsl:text> - <xsl:text> // Do the page swith if any one pending -</xsl:text> - <xsl:text> if(current_subscribed_page != current_visible_page){ -</xsl:text> - <xsl:text> switch_visible_page(current_subscribed_page); + <xsl:text> let rearm = true; +</xsl:text> + <xsl:text> do{ +</xsl:text> + <xsl:text> if(page_fading == "pending" || page_fading == "forced"){ +</xsl:text> + <xsl:text> if(page_fading == "pending") +</xsl:text> + <xsl:text> svg_root.classList.add("fade-out-page"); +</xsl:text> + <xsl:text> page_fading = "in_progress"; +</xsl:text> + <xsl:text> if(page_fading_args.length) +</xsl:text> + <xsl:text> setTimeout(function(){ +</xsl:text> + <xsl:text> switch_page(...page_fading_args); +</xsl:text> + <xsl:text> },1); +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // Do the page swith if pending +</xsl:text> + <xsl:text> if(page_switch_in_progress){ +</xsl:text> + <xsl:text> if(current_subscribed_page != current_visible_page){ +</xsl:text> + <xsl:text> switch_visible_page(current_subscribed_page); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> page_switch_in_progress = false; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(page_fading == "in_progress"){ +</xsl:text> + <xsl:text> svg_root.classList.remove("fade-out-page"); +</xsl:text> + <xsl:text> page_fading = "off"; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> while(widget = need_cache_apply.pop()){ +</xsl:text> + <xsl:text> widget.apply_cache(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(jumps_need_update) update_jumps(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> apply_updates(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> pending_widget_animates.forEach(widget => widget._animate()); +</xsl:text> + <xsl:text> pending_widget_animates = []; +</xsl:text> + <xsl:text> rearm = false; +</xsl:text> + <xsl:text> } while(0); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> requestAnimationFrameID = null; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(rearm) requestHMIAnimation(); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function requestHMIAnimation() { +</xsl:text> + <xsl:text> if(requestAnimationFrameID == null){ +</xsl:text> + <xsl:text> requestAnimationFrameID = window.requestAnimationFrame(animate); </xsl:text> <xsl:text> } </xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> while(widget = need_cache_apply.pop()){ -</xsl:text> - <xsl:text> widget.apply_cache(); + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// Message reception handler +</xsl:text> + <xsl:text>// Hash is verified and HMI values updates resulting from binary parsing +</xsl:text> + <xsl:text>// are stored until browser can compute next frame, DOM is left untouched +</xsl:text> + <xsl:text>ws.onmessage = function (evt) { +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> let data = evt.data; +</xsl:text> + <xsl:text> let dv = new DataView(data); +</xsl:text> + <xsl:text> let i = 0; +</xsl:text> + <xsl:text> try { +</xsl:text> + <xsl:text> for(let hash_int of hmi_hash) { +</xsl:text> + <xsl:text> if(hash_int != dv.getUint8(i)){ +</xsl:text> + <xsl:text> throw new Error("Hash doesn't match"); +</xsl:text> + <xsl:text> }; +</xsl:text> + <xsl:text> i++; +</xsl:text> + <xsl:text> }; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> while(i < data.byteLength){ +</xsl:text> + <xsl:text> let index = dv.getUint32(i, true); +</xsl:text> + <xsl:text> i += 4; +</xsl:text> + <xsl:text> let iectype = hmitree_types[index]; +</xsl:text> + <xsl:text> if(iectype != undefined){ +</xsl:text> + <xsl:text> let dvgetter = dvgetters[iectype]; +</xsl:text> + <xsl:text> let [value, bytesize] = dvgetter(dv,i); +</xsl:text> + <xsl:text> updates.set(index, value); +</xsl:text> + <xsl:text> i += bytesize; +</xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> throw new Error("Unknown index "+index); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> }; +</xsl:text> + <xsl:text> // register for rendering on next frame, since there are updates +</xsl:text> + <xsl:text> requestHMIAnimation(); +</xsl:text> + <xsl:text> } catch(err) { +</xsl:text> + <xsl:text> // 1003 is for "Unsupported Data" +</xsl:text> + <xsl:text> // ws.close(1003, err.message); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // TODO : remove debug alert ? +</xsl:text> + <xsl:text> alert("Error : "+err.message+"\nHMI will be reloaded."); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // force reload ignoring cache +</xsl:text> + <xsl:text> location.reload(true); </xsl:text> <xsl:text> } </xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(jumps_need_update) update_jumps(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> apply_updates(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> pending_widget_animates.forEach(widget => widget._animate()); -</xsl:text> - <xsl:text> pending_widget_animates = []; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> requestAnimationFrameID = null; + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>hmi_hash_u8 = new Uint8Array(hmi_hash); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function send_blob(data) { +</xsl:text> + <xsl:text> if(data.length > 0) { +</xsl:text> + <xsl:text> ws.send(new Blob([hmi_hash_u8].concat(data))); +</xsl:text> + <xsl:text> }; +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>const typedarray_types = { +</xsl:text> + <xsl:text> INT: (number) => new Int16Array([number]), +</xsl:text> + <xsl:text> BOOL: (truth) => new Int16Array([truth]), +</xsl:text> + <xsl:text> NODE: (truth) => new Int16Array([truth]), +</xsl:text> + <xsl:text> REAL: (number) => new Float32Array([number]), +</xsl:text> + <xsl:text> STRING: (str) => { +</xsl:text> + <xsl:text> // beremiz default string max size is 128 +</xsl:text> + <xsl:text> str = str.slice(0,128); +</xsl:text> + <xsl:text> binary = new Uint8Array(str.length + 1); +</xsl:text> + <xsl:text> binary[0] = str.length; +</xsl:text> + <xsl:text> for(let i = 0; i < str.length; i++){ +</xsl:text> + <xsl:text> binary[i+1] = str.charCodeAt(i); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> return binary; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> /* TODO */ +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function send_reset() { +</xsl:text> + <xsl:text> send_blob(new Uint8Array([1])); /* reset = 1 */ +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var subscriptions = []; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function subscribers(index) { +</xsl:text> + <xsl:text> let entry = subscriptions[index]; +</xsl:text> + <xsl:text> let res; +</xsl:text> + <xsl:text> if(entry == undefined){ +</xsl:text> + <xsl:text> res = new Set(); +</xsl:text> + <xsl:text> subscriptions[index] = [res,0]; +</xsl:text> + <xsl:text> }else{ +</xsl:text> + <xsl:text> [res, _ign] = entry; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> return res </xsl:text> <xsl:text>} </xsl:text> <xsl:text> </xsl:text> - <xsl:text>function requestHMIAnimation() { -</xsl:text> - <xsl:text> if(requestAnimationFrameID == null){ -</xsl:text> - <xsl:text> requestAnimationFrameID = window.requestAnimationFrame(animate); + <xsl:text>function get_subscription_period(index) { +</xsl:text> + <xsl:text> let entry = subscriptions[index]; +</xsl:text> + <xsl:text> if(entry == undefined) +</xsl:text> + <xsl:text> return 0; +</xsl:text> + <xsl:text> let [_ign, period] = entry; +</xsl:text> + <xsl:text> return period; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function set_subscription_period(index, period) { +</xsl:text> + <xsl:text> let entry = subscriptions[index]; +</xsl:text> + <xsl:text> if(entry == undefined){ +</xsl:text> + <xsl:text> subscriptions[index] = [new Set(), period]; +</xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> entry[1] = period; </xsl:text> <xsl:text> } </xsl:text> @@ -10549,996 +10848,810 @@ </xsl:text> <xsl:text> </xsl:text> - <xsl:text>// Message reception handler -</xsl:text> - <xsl:text>// Hash is verified and HMI values updates resulting from binary parsing -</xsl:text> - <xsl:text>// are stored until browser can compute next frame, DOM is left untouched -</xsl:text> - <xsl:text>ws.onmessage = function (evt) { -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> let data = evt.data; -</xsl:text> - <xsl:text> let dv = new DataView(data); -</xsl:text> - <xsl:text> let i = 0; -</xsl:text> - <xsl:text> try { -</xsl:text> - <xsl:text> for(let hash_int of hmi_hash) { -</xsl:text> - <xsl:text> if(hash_int != dv.getUint8(i)){ -</xsl:text> - <xsl:text> throw new Error("Hash doesn't match"); -</xsl:text> - <xsl:text> }; -</xsl:text> - <xsl:text> i++; -</xsl:text> - <xsl:text> }; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> while(i < data.byteLength){ -</xsl:text> - <xsl:text> let index = dv.getUint32(i, true); -</xsl:text> - <xsl:text> i += 4; -</xsl:text> - <xsl:text> let iectype = hmitree_types[index]; -</xsl:text> - <xsl:text> if(iectype != undefined){ -</xsl:text> - <xsl:text> let dvgetter = dvgetters[iectype]; -</xsl:text> - <xsl:text> let [value, bytesize] = dvgetter(dv,i); -</xsl:text> - <xsl:text> updates.set(index, value); -</xsl:text> - <xsl:text> i += bytesize; -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> throw new Error("Unknown index "+index); + <xsl:text>if(has_watchdog){ +</xsl:text> + <xsl:text> // artificially subscribe the watchdog widget to "/heartbeat" hmi variable +</xsl:text> + <xsl:text> // Since dispatch directly calls change_hmi_value, +</xsl:text> + <xsl:text> // PLC will periodically send variable at given frequency +</xsl:text> + <xsl:text> subscribers(heartbeat_index).add({ +</xsl:text> + <xsl:text> /* type: "Watchdog", */ +</xsl:text> + <xsl:text> frequency: 1, +</xsl:text> + <xsl:text> indexes: [heartbeat_index], +</xsl:text> + <xsl:text> new_hmi_value: function(index, value, oldval) { +</xsl:text> + <xsl:text> apply_hmi_value(heartbeat_index, value+1); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> }); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var page_fading = "off"; +</xsl:text> + <xsl:text>var page_fading_args = "off"; +</xsl:text> + <xsl:text>function fading_page_switch(...args){ +</xsl:text> + <xsl:text> if(page_fading == "in_progress") +</xsl:text> + <xsl:text> page_fading = "forced"; +</xsl:text> + <xsl:text> else +</xsl:text> + <xsl:text> page_fading = "pending"; +</xsl:text> + <xsl:text> page_fading_args = args; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> requestHMIAnimation(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text>document.body.style.backgroundColor = "black"; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// subscribe to per instance current page hmi variable +</xsl:text> + <xsl:text>// PLC must prefix page name with "!" for page switch to happen +</xsl:text> + <xsl:text>subscribers(current_page_var_index).add({ +</xsl:text> + <xsl:text> frequency: 1, +</xsl:text> + <xsl:text> indexes: [current_page_var_index], +</xsl:text> + <xsl:text> new_hmi_value: function(index, value, oldval) { +</xsl:text> + <xsl:text> if(value.startsWith("!")) +</xsl:text> + <xsl:text> fading_page_switch(value.slice(1)); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>}); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function svg_text_to_multiline(elt) { +</xsl:text> + <xsl:text> return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function multiline_to_svg_text(elt, str, blank) { +</xsl:text> + <xsl:text> str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;}); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function switch_langnum(langnum) { +</xsl:text> + <xsl:text> langnum = Math.max(0, Math.min(langs.length - 1, langnum)); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> for (let translation of translations) { +</xsl:text> + <xsl:text> let [objs, msgs] = translation; +</xsl:text> + <xsl:text> let msg = msgs[langnum]; +</xsl:text> + <xsl:text> for (let obj of objs) { +</xsl:text> + <xsl:text> multiline_to_svg_text(obj, msg); +</xsl:text> + <xsl:text> obj.setAttribute("lang",langnum); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> return langnum; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// backup original texts +</xsl:text> + <xsl:text>for (let translation of translations) { +</xsl:text> + <xsl:text> let [objs, msgs] = translation; +</xsl:text> + <xsl:text> msgs.unshift(svg_text_to_multiline(objs[0])); +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var lang_local_index = hmi_local_index("lang"); +</xsl:text> + <xsl:text>var langcode_local_index = hmi_local_index("lang_code"); +</xsl:text> + <xsl:text>var langname_local_index = hmi_local_index("lang_name"); +</xsl:text> + <xsl:text>subscribers(lang_local_index).add({ +</xsl:text> + <xsl:text> indexes: [lang_local_index], +</xsl:text> + <xsl:text> new_hmi_value: function(index, value, oldval) { +</xsl:text> + <xsl:text> let current_lang = switch_langnum(value); +</xsl:text> + <xsl:text> let [langname,langcode] = langs[current_lang]; +</xsl:text> + <xsl:text> apply_hmi_value(langcode_local_index, langcode); +</xsl:text> + <xsl:text> apply_hmi_value(langname_local_index, langname); +</xsl:text> + <xsl:text> switch_page(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>}); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>// returns en_US, fr_FR or en_UK depending on selected language +</xsl:text> + <xsl:text>function get_current_lang_code(){ +</xsl:text> + <xsl:text> return cache[langcode_local_index]; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function setup_lang(){ +</xsl:text> + <xsl:text> let current_lang = cache[lang_local_index]; +</xsl:text> + <xsl:text> let new_lang = switch_langnum(current_lang); +</xsl:text> + <xsl:text> if(current_lang != new_lang){ +</xsl:text> + <xsl:text> apply_hmi_value(lang_local_index, new_lang); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>setup_lang(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function update_subscriptions() { +</xsl:text> + <xsl:text> let delta = []; +</xsl:text> + <xsl:text> for(let index in subscriptions){ +</xsl:text> + <xsl:text> let widgets = subscribers(index); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // periods are in ms +</xsl:text> + <xsl:text> let previous_period = get_subscription_period(index); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // subscribing with a zero period is unsubscribing +</xsl:text> + <xsl:text> let new_period = 0; +</xsl:text> + <xsl:text> if(widgets.size > 0) { +</xsl:text> + <xsl:text> let maxfreq = 0; +</xsl:text> + <xsl:text> for(let widget of widgets){ +</xsl:text> + <xsl:text> let wf = widget.frequency; +</xsl:text> + <xsl:text> if(wf != undefined && maxfreq < wf) +</xsl:text> + <xsl:text> maxfreq = wf; </xsl:text> <xsl:text> } </xsl:text> - <xsl:text> }; -</xsl:text> - <xsl:text> // register for rendering on next frame, since there are updates + <xsl:text> +</xsl:text> + <xsl:text> if(maxfreq != 0) +</xsl:text> + <xsl:text> new_period = 1000/maxfreq; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(previous_period != new_period) { +</xsl:text> + <xsl:text> set_subscription_period(index, new_period); +</xsl:text> + <xsl:text> if(index <= last_remote_index){ +</xsl:text> + <xsl:text> delta.push( +</xsl:text> + <xsl:text> new Uint8Array([2]), /* subscribe = 2 */ +</xsl:text> + <xsl:text> new Uint32Array([index]), +</xsl:text> + <xsl:text> new Uint16Array([new_period])); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> send_blob(delta); +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function send_hmi_value(index, value) { +</xsl:text> + <xsl:text> if(index > last_remote_index){ +</xsl:text> + <xsl:text> updates.set(index, value); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(persistent_indexes.has(index)){ +</xsl:text> + <xsl:text> let varname = persistent_indexes.get(index); +</xsl:text> + <xsl:text> document.cookie = varname+"="+value+"; max-age=3153600000"; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> </xsl:text> <xsl:text> requestHMIAnimation(); </xsl:text> - <xsl:text> } catch(err) { -</xsl:text> - <xsl:text> // 1003 is for "Unsupported Data" -</xsl:text> - <xsl:text> // ws.close(1003, err.message); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> // TODO : remove debug alert ? -</xsl:text> - <xsl:text> alert("Error : "+err.message+"\nHMI will be reloaded."); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> // force reload ignoring cache -</xsl:text> - <xsl:text> location.reload(true); + <xsl:text> return; </xsl:text> <xsl:text> } </xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> let iectype = hmitree_types[index]; +</xsl:text> + <xsl:text> let tobinary = typedarray_types[iectype]; +</xsl:text> + <xsl:text> send_blob([ +</xsl:text> + <xsl:text> new Uint8Array([0]), /* setval = 0 */ +</xsl:text> + <xsl:text> new Uint32Array([index]), +</xsl:text> + <xsl:text> tobinary(value)]); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf +</xsl:text> + <xsl:text> // cache[index] = value; +</xsl:text> <xsl:text>}; </xsl:text> <xsl:text> </xsl:text> - <xsl:text>hmi_hash_u8 = new Uint8Array(hmi_hash); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function send_blob(data) { -</xsl:text> - <xsl:text> if(data.length > 0) { -</xsl:text> - <xsl:text> ws.send(new Blob([hmi_hash_u8].concat(data))); -</xsl:text> - <xsl:text> }; + <xsl:text>function apply_hmi_value(index, new_val) { +</xsl:text> + <xsl:text> // Similarly to previous comment, taking decision to update based +</xsl:text> + <xsl:text> // on cache content is bad and can lead to inconsistency +</xsl:text> + <xsl:text> /*let old_val = cache[index];*/ +</xsl:text> + <xsl:text> if(new_val != undefined /*&& old_val != new_val*/) +</xsl:text> + <xsl:text> send_hmi_value(index, new_val); +</xsl:text> + <xsl:text> return new_val; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>const quotes = {"'":null, '"':null}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function eval_operation_string(old_val, opstr) { +</xsl:text> + <xsl:text> let op = opstr[0]; +</xsl:text> + <xsl:text> let given_val; +</xsl:text> + <xsl:text> if(opstr.length < 2) +</xsl:text> + <xsl:text> return undefined; +</xsl:text> + <xsl:text> if(opstr[1] in quotes){ +</xsl:text> + <xsl:text> if(opstr.length < 3) +</xsl:text> + <xsl:text> return undefined; +</xsl:text> + <xsl:text> if(opstr[opstr.length-1] == opstr[1]){ +</xsl:text> + <xsl:text> given_val = opstr.slice(2,opstr.length-1); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> given_val = Number(opstr.slice(1)); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> let new_val; +</xsl:text> + <xsl:text> switch(op){ +</xsl:text> + <xsl:text> case "=": +</xsl:text> + <xsl:text> new_val = given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> case "+": +</xsl:text> + <xsl:text> new_val = old_val + given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> case "-": +</xsl:text> + <xsl:text> new_val = old_val - given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> case "*": +</xsl:text> + <xsl:text> new_val = old_val * given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> case "/": +</xsl:text> + <xsl:text> new_val = old_val / given_val; +</xsl:text> + <xsl:text> break; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> return new_val; +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>var current_visible_page; +</xsl:text> + <xsl:text>var current_subscribed_page; +</xsl:text> + <xsl:text>var current_page_index; +</xsl:text> + <xsl:text>var page_node_local_index = hmi_local_index("page_node"); +</xsl:text> + <xsl:text>var page_switch_in_progress = false; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function toggleFullscreen() { +</xsl:text> + <xsl:text> let elem = document.documentElement; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if (!document.fullscreenElement) { +</xsl:text> + <xsl:text> elem.requestFullscreen().catch(err => { +</xsl:text> + <xsl:text> console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); +</xsl:text> + <xsl:text> }); +</xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> document.exitFullscreen(); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text>} +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function prepare_svg() { +</xsl:text> + <xsl:text> // prevents context menu from appearing on right click and long touch +</xsl:text> + <xsl:text> document.body.addEventListener('contextmenu', e => { +</xsl:text> + <xsl:text> toggleFullscreen(); +</xsl:text> + <xsl:text> e.preventDefault(); +</xsl:text> + <xsl:text> }); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> for(let eltid in detachable_elements){ +</xsl:text> + <xsl:text> let [element,parent] = detachable_elements[eltid]; +</xsl:text> + <xsl:text> parent.removeChild(element); +</xsl:text> + <xsl:text> } </xsl:text> <xsl:text>}; </xsl:text> <xsl:text> </xsl:text> - <xsl:text>const typedarray_types = { -</xsl:text> - <xsl:text> INT: (number) => new Int16Array([number]), -</xsl:text> - <xsl:text> BOOL: (truth) => new Int16Array([truth]), -</xsl:text> - <xsl:text> NODE: (truth) => new Int16Array([truth]), -</xsl:text> - <xsl:text> REAL: (number) => new Float32Array([number]), -</xsl:text> - <xsl:text> STRING: (str) => { -</xsl:text> - <xsl:text> // beremiz default string max size is 128 -</xsl:text> - <xsl:text> str = str.slice(0,128); -</xsl:text> - <xsl:text> binary = new Uint8Array(str.length + 1); -</xsl:text> - <xsl:text> binary[0] = str.length; -</xsl:text> - <xsl:text> for(let i = 0; i < str.length; i++){ -</xsl:text> - <xsl:text> binary[i+1] = str.charCodeAt(i); + <xsl:text>function switch_page(page_name, page_index) { +</xsl:text> + <xsl:text> if(page_switch_in_progress){ +</xsl:text> + <xsl:text> /* page switch already going */ +</xsl:text> + <xsl:text> /* TODO LOG ERROR */ +</xsl:text> + <xsl:text> return false; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> page_switch_in_progress = true; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(page_name == undefined) +</xsl:text> + <xsl:text> page_name = current_subscribed_page; +</xsl:text> + <xsl:text> else if(page_index == undefined){ +</xsl:text> + <xsl:text> [page_name, page_index] = page_name.split('@') +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> let old_desc = page_desc[current_subscribed_page]; +</xsl:text> + <xsl:text> let new_desc = page_desc[page_name]; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(new_desc == undefined){ +</xsl:text> + <xsl:text> /* TODO LOG ERROR */ +</xsl:text> + <xsl:text> return false; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(page_index == undefined) +</xsl:text> + <xsl:text> page_index = new_desc.page_index; +</xsl:text> + <xsl:text> else if(typeof(page_index) == "string") { +</xsl:text> + <xsl:text> let hmitree_node = hmitree_nodes[page_index]; +</xsl:text> + <xsl:text> if(hmitree_node !== undefined){ +</xsl:text> + <xsl:text> let [int_index, hmiclass] = hmitree_node; +</xsl:text> + <xsl:text> if(hmiclass == new_desc.page_class) +</xsl:text> + <xsl:text> page_index = int_index; +</xsl:text> + <xsl:text> else +</xsl:text> + <xsl:text> page_index = new_desc.page_index; +</xsl:text> + <xsl:text> } else { +</xsl:text> + <xsl:text> page_index = new_desc.page_index; </xsl:text> <xsl:text> } </xsl:text> - <xsl:text> return binary; -</xsl:text> <xsl:text> } </xsl:text> - <xsl:text> /* TODO */ + <xsl:text> +</xsl:text> + <xsl:text> if(old_desc){ +</xsl:text> + <xsl:text> old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> const container_id = page_name + (page_index != undefined ? page_index : ""); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> update_subscriptions(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> current_subscribed_page = page_name; +</xsl:text> + <xsl:text> current_page_index = page_index; +</xsl:text> + <xsl:text> let page_node; +</xsl:text> + <xsl:text> if(page_index != undefined){ +</xsl:text> + <xsl:text> page_node = hmitree_paths[page_index]; +</xsl:text> + <xsl:text> }else{ +</xsl:text> + <xsl:text> page_node = ""; +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> apply_hmi_value(page_node_local_index, page_node); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> jumps_need_update = true; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> requestHMIAnimation(); +</xsl:text> + <xsl:text> jump_history.push([page_name, page_index]); +</xsl:text> + <xsl:text> if(jump_history.length > 42) +</xsl:text> + <xsl:text> jump_history.shift(); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> apply_hmi_value(current_page_var_index, page_index == undefined +</xsl:text> + <xsl:text> ? page_name +</xsl:text> + <xsl:text> : page_name + "@" + hmitree_paths[page_index]); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> return true; </xsl:text> <xsl:text>}; </xsl:text> <xsl:text> </xsl:text> - <xsl:text>function send_reset() { -</xsl:text> - <xsl:text> send_blob(new Uint8Array([1])); /* reset = 1 */ + <xsl:text>function switch_visible_page(page_name) { +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> let old_desc = page_desc[current_visible_page]; +</xsl:text> + <xsl:text> let new_desc = page_desc[page_name]; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> if(old_desc){ +</xsl:text> + <xsl:text> for(let eltid in old_desc.required_detachables){ +</xsl:text> + <xsl:text> if(!(eltid in new_desc.required_detachables)){ +</xsl:text> + <xsl:text> let [element, parent] = old_desc.required_detachables[eltid]; +</xsl:text> + <xsl:text> parent.removeChild(element); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> for(let eltid in new_desc.required_detachables){ +</xsl:text> + <xsl:text> if(!(eltid in old_desc.required_detachables)){ +</xsl:text> + <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; +</xsl:text> + <xsl:text> parent.appendChild(element); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> }else{ +</xsl:text> + <xsl:text> for(let eltid in new_desc.required_detachables){ +</xsl:text> + <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; +</xsl:text> + <xsl:text> parent.appendChild(element); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); +</xsl:text> + <xsl:text> current_visible_page = page_name; </xsl:text> <xsl:text>}; </xsl:text> <xsl:text> </xsl:text> - <xsl:text>var subscriptions = []; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function subscribers(index) { -</xsl:text> - <xsl:text> let entry = subscriptions[index]; -</xsl:text> - <xsl:text> let res; -</xsl:text> - <xsl:text> if(entry == undefined){ -</xsl:text> - <xsl:text> res = new Set(); -</xsl:text> - <xsl:text> subscriptions[index] = [res,0]; -</xsl:text> - <xsl:text> }else{ -</xsl:text> - <xsl:text> [res, _ign] = entry; + <xsl:text>// Once connection established +</xsl:text> + <xsl:text>ws.onopen = function (evt) { +</xsl:text> + <xsl:text> init_widgets(); +</xsl:text> + <xsl:text> send_reset(); +</xsl:text> + <xsl:text> // show main page +</xsl:text> + <xsl:text> prepare_svg(); +</xsl:text> + <xsl:text> switch_page(default_page); +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>ws.onclose = function (evt) { +</xsl:text> + <xsl:text> // TODO : add visible notification while waiting for reload +</xsl:text> + <xsl:text> console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in 10s."); +</xsl:text> + <xsl:text> // TODO : re-enable auto reload when not in debug +</xsl:text> + <xsl:text> //window.setTimeout(() => location.reload(true), 10000); +</xsl:text> + <xsl:text> alert("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+"."); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>}; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>const xmlns = "http://www.w3.org/2000/svg"; +</xsl:text> + <xsl:text>var edit_callback; +</xsl:text> + <xsl:text>const localtypes = {"PAGE_LOCAL":null, "HMI_LOCAL":null} +</xsl:text> + <xsl:text>function edit_value(path, valuetype, callback, initial) { +</xsl:text> + <xsl:text> if(valuetype in localtypes){ +</xsl:text> + <xsl:text> valuetype = (typeof initial) == "number" ? "HMI_REAL" : "HMI_STRING"; </xsl:text> <xsl:text> } </xsl:text> - <xsl:text> return res -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function get_subscription_period(index) { -</xsl:text> - <xsl:text> let entry = subscriptions[index]; -</xsl:text> - <xsl:text> if(entry == undefined) -</xsl:text> - <xsl:text> return 0; -</xsl:text> - <xsl:text> let [_ign, period] = entry; -</xsl:text> - <xsl:text> return period; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function set_subscription_period(index, period) { -</xsl:text> - <xsl:text> let entry = subscriptions[index]; -</xsl:text> - <xsl:text> if(entry == undefined){ -</xsl:text> - <xsl:text> subscriptions[index] = [new Set(), period]; -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> entry[1] = period; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>if(has_watchdog){ -</xsl:text> - <xsl:text> // artificially subscribe the watchdog widget to "/heartbeat" hmi variable -</xsl:text> - <xsl:text> // Since dispatch directly calls change_hmi_value, -</xsl:text> - <xsl:text> // PLC will periodically send variable at given frequency -</xsl:text> - <xsl:text> subscribers(heartbeat_index).add({ -</xsl:text> - <xsl:text> /* type: "Watchdog", */ -</xsl:text> - <xsl:text> frequency: 1, -</xsl:text> - <xsl:text> indexes: [heartbeat_index], -</xsl:text> - <xsl:text> new_hmi_value: function(index, value, oldval) { -</xsl:text> - <xsl:text> apply_hmi_value(heartbeat_index, value+1); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> }); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>var page_fading_in_progress = false; -</xsl:text> - <xsl:text>function fading_page_switch(...args){ -</xsl:text> - <xsl:text> svg_root.classList.add("fade-out-page"); -</xsl:text> - <xsl:text> page_fading_in_progress = true; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> setTimeout(function(){ -</xsl:text> - <xsl:text> switch_page(...args); -</xsl:text> - <xsl:text> },1); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text>document.body.style.backgroundColor = "black"; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>// subscribe to per instance current page hmi variable -</xsl:text> - <xsl:text>// PLC must prefix page name with "!" for page switch to happen -</xsl:text> - <xsl:text>subscribers(current_page_var_index).add({ -</xsl:text> - <xsl:text> frequency: 1, -</xsl:text> - <xsl:text> indexes: [current_page_var_index], -</xsl:text> - <xsl:text> new_hmi_value: function(index, value, oldval) { -</xsl:text> - <xsl:text> if(value.startsWith("!")) -</xsl:text> - <xsl:text> fading_page_switch(value.slice(1)); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>}); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function svg_text_to_multiline(elt) { -</xsl:text> - <xsl:text> return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function multiline_to_svg_text(elt, str, blank) { -</xsl:text> - <xsl:text> str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;}); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function switch_langnum(langnum) { -</xsl:text> - <xsl:text> langnum = Math.max(0, Math.min(langs.length - 1, langnum)); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> for (let translation of translations) { -</xsl:text> - <xsl:text> let [objs, msgs] = translation; -</xsl:text> - <xsl:text> let msg = msgs[langnum]; -</xsl:text> - <xsl:text> for (let obj of objs) { -</xsl:text> - <xsl:text> multiline_to_svg_text(obj, msg); -</xsl:text> - <xsl:text> obj.setAttribute("lang",langnum); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> return langnum; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>// backup original texts -</xsl:text> - <xsl:text>for (let translation of translations) { -</xsl:text> - <xsl:text> let [objs, msgs] = translation; -</xsl:text> - <xsl:text> msgs.unshift(svg_text_to_multiline(objs[0])); -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>var lang_local_index = hmi_local_index("lang"); -</xsl:text> - <xsl:text>var langcode_local_index = hmi_local_index("lang_code"); -</xsl:text> - <xsl:text>var langname_local_index = hmi_local_index("lang_name"); -</xsl:text> - <xsl:text>subscribers(lang_local_index).add({ -</xsl:text> - <xsl:text> indexes: [lang_local_index], -</xsl:text> - <xsl:text> new_hmi_value: function(index, value, oldval) { -</xsl:text> - <xsl:text> let current_lang = switch_langnum(value); -</xsl:text> - <xsl:text> let [langname,langcode] = langs[current_lang]; -</xsl:text> - <xsl:text> apply_hmi_value(langcode_local_index, langcode); -</xsl:text> - <xsl:text> apply_hmi_value(langname_local_index, langname); -</xsl:text> - <xsl:text> switch_page(); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>}); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>// returns en_US, fr_FR or en_UK depending on selected language -</xsl:text> - <xsl:text>function get_current_lang_code(){ -</xsl:text> - <xsl:text> return cache[langcode_local_index]; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function setup_lang(){ -</xsl:text> - <xsl:text> let current_lang = cache[lang_local_index]; -</xsl:text> - <xsl:text> let new_lang = switch_langnum(current_lang); -</xsl:text> - <xsl:text> if(current_lang != new_lang){ -</xsl:text> - <xsl:text> apply_hmi_value(lang_local_index, new_lang); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>setup_lang(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function update_subscriptions() { -</xsl:text> - <xsl:text> let delta = []; -</xsl:text> - <xsl:text> for(let index in subscriptions){ -</xsl:text> - <xsl:text> let widgets = subscribers(index); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> // periods are in ms -</xsl:text> - <xsl:text> let previous_period = get_subscription_period(index); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> // subscribing with a zero period is unsubscribing -</xsl:text> - <xsl:text> let new_period = 0; -</xsl:text> - <xsl:text> if(widgets.size > 0) { -</xsl:text> - <xsl:text> let maxfreq = 0; -</xsl:text> - <xsl:text> for(let widget of widgets){ -</xsl:text> - <xsl:text> let wf = widget.frequency; -</xsl:text> - <xsl:text> if(wf != undefined && maxfreq < wf) -</xsl:text> - <xsl:text> maxfreq = wf; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(maxfreq != 0) -</xsl:text> - <xsl:text> new_period = 1000/maxfreq; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(previous_period != new_period) { -</xsl:text> - <xsl:text> set_subscription_period(index, new_period); -</xsl:text> - <xsl:text> if(index <= last_remote_index){ -</xsl:text> - <xsl:text> delta.push( -</xsl:text> - <xsl:text> new Uint8Array([2]), /* subscribe = 2 */ -</xsl:text> - <xsl:text> new Uint32Array([index]), -</xsl:text> - <xsl:text> new Uint16Array([new_period])); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> send_blob(delta); + <xsl:text> let [keypadid, xcoord, ycoord] = keypads[valuetype]; +</xsl:text> + <xsl:text> edit_callback = callback; +</xsl:text> + <xsl:text> let widget = hmi_widgets[keypadid]; +</xsl:text> + <xsl:text> widget.start_edit(path, valuetype, callback, initial); </xsl:text> <xsl:text>}; </xsl:text> <xsl:text> </xsl:text> - <xsl:text>function send_hmi_value(index, value) { -</xsl:text> - <xsl:text> if(index > last_remote_index){ -</xsl:text> - <xsl:text> updates.set(index, value); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(persistent_indexes.has(index)){ -</xsl:text> - <xsl:text> let varname = persistent_indexes.get(index); -</xsl:text> - <xsl:text> document.cookie = varname+"="+value+"; max-age=3153600000"; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> requestHMIAnimation(); -</xsl:text> - <xsl:text> return; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> let iectype = hmitree_types[index]; -</xsl:text> - <xsl:text> let tobinary = typedarray_types[iectype]; -</xsl:text> - <xsl:text> send_blob([ -</xsl:text> - <xsl:text> new Uint8Array([0]), /* setval = 0 */ -</xsl:text> - <xsl:text> new Uint32Array([index]), -</xsl:text> - <xsl:text> tobinary(value)]); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf -</xsl:text> - <xsl:text> // cache[index] = value; + <xsl:text>var current_modal; /* TODO stack ?*/ +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text>function show_modal() { +</xsl:text> + <xsl:text> let [element, parent] = detachable_elements[this.element.id]; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> tmpgrp = document.createElementNS(xmlns,"g"); +</xsl:text> + <xsl:text> tmpgrpattr = document.createAttribute("transform"); +</xsl:text> + <xsl:text> let [xcoord,ycoord] = this.coordinates; +</xsl:text> + <xsl:text> let [xdest,ydest] = page_desc[current_visible_page].bbox; +</xsl:text> + <xsl:text> tmpgrpattr.value = "translate("+String(xdest-xcoord)+","+String(ydest-ycoord)+")"; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> tmpgrp.setAttributeNode(tmpgrpattr); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> tmpgrp.appendChild(element); +</xsl:text> + <xsl:text> parent.appendChild(tmpgrp); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> current_modal = [this.element.id, tmpgrp]; </xsl:text> <xsl:text>}; </xsl:text> <xsl:text> </xsl:text> - <xsl:text>function apply_hmi_value(index, new_val) { -</xsl:text> - <xsl:text> // Similarly to previous comment, taking decision to update based -</xsl:text> - <xsl:text> // on cache content is bad and can lead to inconsistency -</xsl:text> - <xsl:text> /*let old_val = cache[index];*/ -</xsl:text> - <xsl:text> if(new_val != undefined /*&& old_val != new_val*/) -</xsl:text> - <xsl:text> send_hmi_value(index, new_val); -</xsl:text> - <xsl:text> return new_val; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>const quotes = {"'":null, '"':null}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function eval_operation_string(old_val, opstr) { -</xsl:text> - <xsl:text> let op = opstr[0]; -</xsl:text> - <xsl:text> let given_val; -</xsl:text> - <xsl:text> if(opstr.length < 2) -</xsl:text> - <xsl:text> return undefined; -</xsl:text> - <xsl:text> if(opstr[1] in quotes){ -</xsl:text> - <xsl:text> if(opstr.length < 3) -</xsl:text> - <xsl:text> return undefined; -</xsl:text> - <xsl:text> if(opstr[opstr.length-1] == opstr[1]){ -</xsl:text> - <xsl:text> given_val = opstr.slice(2,opstr.length-1); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> given_val = Number(opstr.slice(1)); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> let new_val; -</xsl:text> - <xsl:text> switch(op){ -</xsl:text> - <xsl:text> case "=": -</xsl:text> - <xsl:text> new_val = given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> case "+": -</xsl:text> - <xsl:text> new_val = old_val + given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> case "-": -</xsl:text> - <xsl:text> new_val = old_val - given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> case "*": -</xsl:text> - <xsl:text> new_val = old_val * given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> case "/": -</xsl:text> - <xsl:text> new_val = old_val / given_val; -</xsl:text> - <xsl:text> break; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> return new_val; -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>var current_visible_page; -</xsl:text> - <xsl:text>var current_subscribed_page; -</xsl:text> - <xsl:text>var current_page_index; -</xsl:text> - <xsl:text>var page_node_local_index = hmi_local_index("page_node"); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function toggleFullscreen() { -</xsl:text> - <xsl:text> let elem = document.documentElement; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if (!document.fullscreenElement) { -</xsl:text> - <xsl:text> elem.requestFullscreen().catch(err => { -</xsl:text> - <xsl:text> console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); -</xsl:text> - <xsl:text> }); -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> document.exitFullscreen(); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text>} -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function prepare_svg() { -</xsl:text> - <xsl:text> // prevents context menu from appearing on right click and long touch -</xsl:text> - <xsl:text> document.body.addEventListener('contextmenu', e => { -</xsl:text> - <xsl:text> toggleFullscreen(); -</xsl:text> - <xsl:text> e.preventDefault(); -</xsl:text> - <xsl:text> }); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> for(let eltid in detachable_elements){ -</xsl:text> - <xsl:text> let [element,parent] = detachable_elements[eltid]; -</xsl:text> - <xsl:text> parent.removeChild(element); -</xsl:text> - <xsl:text> } + <xsl:text>function end_modal() { +</xsl:text> + <xsl:text> let [eltid, tmpgrp] = current_modal; +</xsl:text> + <xsl:text> let [element, parent] = detachable_elements[this.element.id]; +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> parent.removeChild(tmpgrp); +</xsl:text> + <xsl:text> +</xsl:text> + <xsl:text> current_modal = undefined; </xsl:text> <xsl:text>}; </xsl:text> <xsl:text> </xsl:text> - <xsl:text>function switch_page(page_name, page_index) { -</xsl:text> - <xsl:text> if(current_subscribed_page != current_visible_page){ -</xsl:text> - <xsl:text> /* page switch already going */ -</xsl:text> - <xsl:text> /* TODO LOG ERROR */ -</xsl:text> - <xsl:text> return false; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(page_name == undefined) -</xsl:text> - <xsl:text> page_name = current_subscribed_page; -</xsl:text> - <xsl:text> else if(page_index == undefined){ -</xsl:text> - <xsl:text> [page_name, page_index] = page_name.split('@') -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> let old_desc = page_desc[current_subscribed_page]; -</xsl:text> - <xsl:text> let new_desc = page_desc[page_name]; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(new_desc == undefined){ -</xsl:text> - <xsl:text> /* TODO LOG ERROR */ -</xsl:text> - <xsl:text> return false; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(page_index == undefined) -</xsl:text> - <xsl:text> page_index = new_desc.page_index; -</xsl:text> - <xsl:text> else if(typeof(page_index) == "string") { -</xsl:text> - <xsl:text> let hmitree_node = hmitree_nodes[page_index]; -</xsl:text> - <xsl:text> if(hmitree_node !== undefined){ -</xsl:text> - <xsl:text> let [int_index, hmiclass] = hmitree_node; -</xsl:text> - <xsl:text> if(hmiclass == new_desc.page_class) -</xsl:text> - <xsl:text> page_index = int_index; -</xsl:text> - <xsl:text> else -</xsl:text> - <xsl:text> page_index = new_desc.page_index; -</xsl:text> - <xsl:text> } else { -</xsl:text> - <xsl:text> page_index = new_desc.page_index; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(old_desc){ -</xsl:text> - <xsl:text> old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> const container_id = page_name + (page_index != undefined ? page_index : ""); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> update_subscriptions(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> current_subscribed_page = page_name; -</xsl:text> - <xsl:text> current_page_index = page_index; -</xsl:text> - <xsl:text> let page_node; -</xsl:text> - <xsl:text> if(page_index != undefined){ -</xsl:text> - <xsl:text> page_node = hmitree_paths[page_index]; -</xsl:text> - <xsl:text> }else{ -</xsl:text> - <xsl:text> page_node = ""; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> apply_hmi_value(page_node_local_index, page_node); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> jumps_need_update = true; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> requestHMIAnimation(); -</xsl:text> - <xsl:text> jump_history.push([page_name, page_index]); -</xsl:text> - <xsl:text> if(jump_history.length > 42) -</xsl:text> - <xsl:text> jump_history.shift(); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> apply_hmi_value(current_page_var_index, page_index == undefined -</xsl:text> - <xsl:text> ? page_name -</xsl:text> - <xsl:text> : page_name + "@" + hmitree_paths[page_index]); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> return true; -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function switch_visible_page(page_name) { -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> let old_desc = page_desc[current_visible_page]; -</xsl:text> - <xsl:text> let new_desc = page_desc[page_name]; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> if(old_desc){ -</xsl:text> - <xsl:text> for(let eltid in old_desc.required_detachables){ -</xsl:text> - <xsl:text> if(!(eltid in new_desc.required_detachables)){ -</xsl:text> - <xsl:text> let [element, parent] = old_desc.required_detachables[eltid]; -</xsl:text> - <xsl:text> parent.removeChild(element); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> for(let eltid in new_desc.required_detachables){ -</xsl:text> - <xsl:text> if(!(eltid in old_desc.required_detachables)){ -</xsl:text> - <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; -</xsl:text> - <xsl:text> parent.appendChild(element); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> }else{ -</xsl:text> - <xsl:text> for(let eltid in new_desc.required_detachables){ -</xsl:text> - <xsl:text> let [element, parent] = new_desc.required_detachables[eltid]; -</xsl:text> - <xsl:text> parent.appendChild(element); -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); -</xsl:text> - <xsl:text> if(page_fading_in_progress) -</xsl:text> - <xsl:text> svg_root.classList.remove("fade-out-page"); -</xsl:text> - <xsl:text> page_fading_in_progress = false; -</xsl:text> - <xsl:text> current_visible_page = page_name; -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>// Once connection established -</xsl:text> - <xsl:text>ws.onopen = function (evt) { -</xsl:text> - <xsl:text> init_widgets(); -</xsl:text> - <xsl:text> send_reset(); -</xsl:text> - <xsl:text> // show main page -</xsl:text> - <xsl:text> prepare_svg(); -</xsl:text> - <xsl:text> switch_page(default_page); -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>ws.onclose = function (evt) { -</xsl:text> - <xsl:text> // TODO : add visible notification while waiting for reload -</xsl:text> - <xsl:text> console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in 10s."); -</xsl:text> - <xsl:text> // TODO : re-enable auto reload when not in debug -</xsl:text> - <xsl:text> //window.setTimeout(() => location.reload(true), 10000); -</xsl:text> - <xsl:text> alert("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+"."); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>const xmlns = "http://www.w3.org/2000/svg"; -</xsl:text> - <xsl:text>var edit_callback; -</xsl:text> - <xsl:text>const localtypes = {"PAGE_LOCAL":null, "HMI_LOCAL":null} -</xsl:text> - <xsl:text>function edit_value(path, valuetype, callback, initial) { -</xsl:text> - <xsl:text> if(valuetype in localtypes){ -</xsl:text> - <xsl:text> valuetype = (typeof initial) == "number" ? "HMI_REAL" : "HMI_STRING"; -</xsl:text> - <xsl:text> } -</xsl:text> - <xsl:text> let [keypadid, xcoord, ycoord] = keypads[valuetype]; -</xsl:text> - <xsl:text> edit_callback = callback; -</xsl:text> - <xsl:text> let widget = hmi_widgets[keypadid]; -</xsl:text> - <xsl:text> widget.start_edit(path, valuetype, callback, initial); -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>var current_modal; /* TODO stack ?*/ -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function show_modal() { -</xsl:text> - <xsl:text> let [element, parent] = detachable_elements[this.element.id]; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> tmpgrp = document.createElementNS(xmlns,"g"); -</xsl:text> - <xsl:text> tmpgrpattr = document.createAttribute("transform"); -</xsl:text> - <xsl:text> let [xcoord,ycoord] = this.coordinates; -</xsl:text> - <xsl:text> let [xdest,ydest] = page_desc[current_visible_page].bbox; -</xsl:text> - <xsl:text> tmpgrpattr.value = "translate("+String(xdest-xcoord)+","+String(ydest-ycoord)+")"; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> tmpgrp.setAttributeNode(tmpgrpattr); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> tmpgrp.appendChild(element); -</xsl:text> - <xsl:text> parent.appendChild(tmpgrp); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> current_modal = [this.element.id, tmpgrp]; -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text>function end_modal() { -</xsl:text> - <xsl:text> let [eltid, tmpgrp] = current_modal; -</xsl:text> - <xsl:text> let [element, parent] = detachable_elements[this.element.id]; -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> parent.removeChild(tmpgrp); -</xsl:text> - <xsl:text> -</xsl:text> - <xsl:text> current_modal = undefined; -</xsl:text> - <xsl:text>}; -</xsl:text> - <xsl:text> -</xsl:text> + <xsl:text> +// +// +// Declarations from SVG scripts (inkscape document properties) +// +// +</xsl:text> + <xsl:for-each select="/svg:svg/svg:script"> + <xsl:text> +</xsl:text> + <xsl:text>/* </xsl:text> + <xsl:value-of select="@id"/> + <xsl:text> */ +</xsl:text> + <xsl:value-of select="text()"/> + <xsl:text> +</xsl:text> + </xsl:for-each> </script> </body> </html> diff -r ffe4e46a3163 -r f5850ce25caf svghmi/gen_index_xhtml.ysl2 --- a/svghmi/gen_index_xhtml.ysl2 Sun Jul 17 22:46:26 2022 +0200 +++ b/svghmi/gen_index_xhtml.ysl2 Sun Jul 17 22:53:35 2022 +0200 @@ -102,6 +102,12 @@ include text svghmi.js + | \n//\n//\n// Declarations from SVG scripts (inkscape document properties) \n//\n// + foreach "/svg:svg/svg:script" { + | + | /* «@id» */ + | «text()» + } } } } diff -r ffe4e46a3163 -r f5850ce25caf svghmi/svghmi.js --- a/svghmi/svghmi.js Sun Jul 17 22:46:26 2022 +0200 +++ b/svghmi/svghmi.js Sun Jul 17 22:53:35 2022 +0200 @@ -59,32 +59,49 @@ // Called on requestAnimationFrame, modifies DOM var requestAnimationFrameID = null; function animate() { - // Do the page swith if any one pending - if(page_switch_in_progress){ - if(current_subscribed_page != current_visible_page){ - switch_visible_page(current_subscribed_page); - } - - page_switch_in_progress = false; - - if(page_fading_in_progress){ - svg_root.classList.remove("fade-out-page"); - page_fading_in_progress = false; - } - } - - while(widget = need_cache_apply.pop()){ - widget.apply_cache(); - } - - if(jumps_need_update) update_jumps(); - - apply_updates(); - - pending_widget_animates.forEach(widget => widget._animate()); - pending_widget_animates = []; + let rearm = true; + do{ + if(page_fading == "pending" || page_fading == "forced"){ + if(page_fading == "pending") + svg_root.classList.add("fade-out-page"); + page_fading = "in_progress"; + if(page_fading_args.length) + setTimeout(function(){ + switch_page(...page_fading_args); + },1); + break; + } + + // Do the page swith if pending + if(page_switch_in_progress){ + if(current_subscribed_page != current_visible_page){ + switch_visible_page(current_subscribed_page); + } + + page_switch_in_progress = false; + + if(page_fading == "in_progress"){ + svg_root.classList.remove("fade-out-page"); + page_fading = "off"; + } + } + + while(widget = need_cache_apply.pop()){ + widget.apply_cache(); + } + + if(jumps_need_update) update_jumps(); + + apply_updates(); + + pending_widget_animates.forEach(widget => widget._animate()); + pending_widget_animates = []; + rearm = false; + } while(0); requestAnimationFrameID = null; + + if(rearm) requestHMIAnimation(); } function requestHMIAnimation() { @@ -212,14 +229,17 @@ } -var page_fading_in_progress = false; +var page_fading = "off"; +var page_fading_args = "off"; function fading_page_switch(...args){ - svg_root.classList.add("fade-out-page"); - page_fading_in_progress = true; - - setTimeout(function(){ - switch_page(...args); - },1); + if(page_fading == "in_progress") + page_fading = "forced"; + else + page_fading = "pending"; + page_fading_args = args; + + requestHMIAnimation(); + } document.body.style.backgroundColor = "black"; diff -r ffe4e46a3163 -r f5850ce25caf svghmi/widget_jump.ysl2 --- a/svghmi/widget_jump.ysl2 Sun Jul 17 22:46:26 2022 +0200 +++ b/svghmi/widget_jump.ysl2 Sun Jul 17 22:53:35 2022 +0200 @@ -145,7 +145,7 @@ emit "cssdefs:jump" || .fade-out-page { - animation: fadeOut 0.6s both; + animation: cubic-bezier(0, 0.8, 0.6, 1) fadeOut 0.6s both; } @keyframes fadeOut { diff -r ffe4e46a3163 -r f5850ce25caf svghmi/widget_list.ysl2 --- a/svghmi/widget_list.ysl2 Sun Jul 17 22:46:26 2022 +0200 +++ b/svghmi/widget_list.ysl2 Sun Jul 17 22:53:35 2022 +0200 @@ -27,3 +27,4 @@ | }, } +widget_class("List"); diff -r ffe4e46a3163 -r f5850ce25caf svghmi/widget_textlist.ysl2 --- a/svghmi/widget_textlist.ysl2 Sun Jul 17 22:46:26 2022 +0200 +++ b/svghmi/widget_textlist.ysl2 Sun Jul 17 22:53:35 2022 +0200 @@ -26,3 +26,5 @@ // could find a proper way in xpath to reverse() | ].reverse(), } + +widget_class("TextList"); diff -r ffe4e46a3163 -r f5850ce25caf svghmi/widget_textstylelist.ysl2 --- a/svghmi/widget_textstylelist.ysl2 Sun Jul 17 22:46:26 2022 +0200 +++ b/svghmi/widget_textstylelist.ysl2 Sun Jul 17 22:53:35 2022 +0200 @@ -26,3 +26,4 @@ | }, } +widget_class("TextStyleList"); diff -r ffe4e46a3163 -r f5850ce25caf svghmi/widgets_common.ysl2 --- a/svghmi/widgets_common.ysl2 Sun Jul 17 22:46:26 2022 +0200 +++ b/svghmi/widgets_common.ysl2 Sun Jul 17 22:53:35 2022 +0200 @@ -477,14 +477,17 @@ generate-id() = generate-id(key('TypesKey', @type)) and not(@type = $excluded_types)]"""; apply "$used_widget_types", mode="widget_class"; -} - -template "widget", mode="widget_class" -|| -class «@type»Widget extends Widget{ - /* empty class, as «@type» widget didn't provide any */ -} -|| + +} + +template "widget", mode="widget_class" { + || + class «@type»Widget extends Widget{ + /* empty class, as «@type» widget didn't provide any */ + } + || + warning > «@type» widget is used in SVG but widget type is not declared +} const "included_ids","$parsed_widgets/widget[not(@type = $excluded_types) and not(@id = $discardable_elements/@id)]/@id"; const "hmi_widgets","$hmi_elements[@id = $included_ids]"; @@ -494,6 +497,7 @@ | var hmi_widgets = { apply "$hmi_widgets", mode="hmi_widgets"; | } + | } function "defs_by_labels" { diff -r ffe4e46a3163 -r f5850ce25caf tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg --- a/tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg Sun Jul 17 22:46:26 2022 +0200 +++ b/tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg Sun Jul 17 22:53:35 2022 +0200 @@ -18,6 +18,14 @@ viewBox="0 0 1280 720" height="720" width="1280"> + <script + id="script1244"> +hmi_widgets["g443-3"].off_action = function(){ + console.log("Hello from Inkscape"); + PushButtonWidget.prototype.off_action.call(this); + fading_page_switch(); + console.log("Bye from Inkscape"); +}</script> <metadata id="metadata4542"> <rdf:RDF> @@ -125,12 +133,12 @@ inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:document-units="px" - inkscape:current-layer="g5053" + inkscape:current-layer="hmi0" showgrid="false" units="px" - inkscape:zoom="0.80184804" - inkscape:cx="784.66046" - inkscape:cy="-449.34319" + inkscape:zoom="0.14174805" + inkscape:cx="-1530.0784" + inkscape:cy="-1404.9832" inkscape:window-width="1600" inkscape:window-height="836" inkscape:window-x="0" @@ -8399,4 +8407,47 @@ sodipodi:role="line">Freq4</tspan></text> </g> </g> + <g + transform="matrix(0.57180538,0,0,0.57180538,-77.992226,121.53383)" + id="g443-3" + inkscape:label="HMI:PushButton@/SELECTION" + style="stroke-width:1"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:#ff6600;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect5492-6" + width="245.44583" + height="95.723877" + x="971.96545" + y="594.82263" + ry="23.930969" + inkscape:label="inactive" + rx="23.930969" /> + <rect + rx="23.930969" + inkscape:label="active" + ry="23.930969" + y="594.82263" + x="971.96545" + height="95.723877" + width="245.44583" + id="rect433-7" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#fdfdfd;fill-opacity:1;fill-rule:nonzero;stroke:#ffd0b2;stroke-width:28.60938263;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <g + style="stroke-width:1" + inkscape:label="text" + id="g952-5"> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;display:inline;fill:#ff6600;fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + x="1090.7626" + y="656.98151" + id="text950-3" + inkscape:label="setting_jmp"><tspan + sodipodi:role="line" + id="tspan948-5" + x="1090.7626" + y="656.98151" + style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px">up</tspan></text> + </g> + </g> </svg>