# HG changeset patch # User Edouard Tisserant # Date 1665043366 -7200 # Node ID 921f620577e8e586232ca5a9f142e1aab3c75838 # Parent 1cf21430ed4abd0023ed8b74ad65ea140ca37cc7# Parent f117526d41bae50c47bb782d17396c3111a9ede1 Merged changes from default diff -r 1cf21430ed4a -r 921f620577e8 exemples/svghmi_references/beremiz.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/svghmi_references/beremiz.xml Thu Oct 06 10:02:46 2022 +0200 @@ -0,0 +1,5 @@ + + + + + diff -r 1cf21430ed4a -r 921f620577e8 exemples/svghmi_references/plc.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/svghmi_references/plc.xml Thu Oct 06 10:02:46 2022 +0200 @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LocalVar0 + + + + + + + + + + + LocalVar1 + + + + + + + + + + + + + + + + + + diff -r 1cf21430ed4a -r 921f620577e8 exemples/svghmi_references/svghmi_0@svghmi/baseconfnode.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/svghmi_references/svghmi_0@svghmi/baseconfnode.xml Thu Oct 06 10:02:46 2022 +0200 @@ -0,0 +1,2 @@ + + diff -r 1cf21430ed4a -r 921f620577e8 exemples/svghmi_references/svghmi_0@svghmi/confnode.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/svghmi_references/svghmi_0@svghmi/confnode.xml Thu Oct 06 10:02:46 2022 +0200 @@ -0,0 +1,2 @@ + + diff -r 1cf21430ed4a -r 921f620577e8 exemples/svghmi_references/svghmi_0@svghmi/svghmi.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/svghmi_references/svghmi_0@svghmi/svghmi.svg Thu Oct 06 10:02:46 2022 +0200 @@ -0,0 +1,1647 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + Switch widget + + + + + + + Togglebutton + + + + + + + + + Button + + + + + + + + + PushButton + + + + + + + Notes: - Widget roles are described in objects labels.- Press Ctrl+O to open object properties panel- To see objects in a tree, select Object->Objects in menu- Inkscape's "objects" are SVG elements- Press Ctrl+X to edit SVG elements directly with XML editor + + + + + Page (inkscape) + + + + final position in page + offset positionfor "B" + offset positionfor "C" + HMI:Switch@... (group) |-. "A" (group) | |- reference (rect) | |- ... |-. "B" (group) | |- frame (rect) | |- ... |-. "C" (group) | |- frame (rect) | |- ... + Some widgets like Switch or Button are displaying one of many groups that represent the possible states of the widget. Since all groups need to appear in the same place, they overlap and the drawing becomes hard to understand and maintain.Using specially labelled "reference" and "frame" rectangles, groups can be spread out. Theses rectangles can be used in widget or anywhere in the drawing, and do not appear in final result. + reference + frame + frame + Button widgets + HMI:Switch@... (group) |- reference (rect) |-. "A" (group) | |- ... |-. "B" (group) | |- frame (rect) | |- ... |-. "C" (group) | |- frame (rect) | |- ... + or + + + simple + + + + with widgets + + + user choice : %s + + + selected dialog : %s + + Switch and Assign widgets can be used together to simulate behavior modal dialog or "popup" with user feedback."selection" and "userChoice" local HMI are used to respectivelyselect dialog to be shown and store user choice.Here, "reference" and "frame" rectangles are necessary toto spread out dialogs and page, otherwise overlapping. + + + + + + A SIMPLE MODAL DIALOG + + + OK + + + + Cancel + + + + + X + + + + + + + + + A MODAL DIALOGwith widgets + + + + 0 + + + + 1 + + + + 2 + + + + 3 + + 0 + position + + + + + + + + + + + X + + + + :dialog="None" +:return="Applied" +:plcvar=uservar +@dialog=selection +@return=userChoice +@uservar=.position +@plcvar=/PLCHMIVAR + + Apply + + + + In this example, 3 types of button ar connected to the sameHMI local variable. Here, "reference" and "frame" rectangles are used toseparate active and inactive state of buttons + A + B + C + + + Page (final result) + + + A + + + + B + + + + C + + + + + + + Home + + + + + Swith + + + + + Buttons + + + + + declaration of "position" HMI local variable + + + declaration of 'selection' local variable + + + declaration of 'userChoice' local variable + + + declaration of "range" HMI local variable + + + declaration of "size" HMI local variable + + + declaration of "position" HMI local variable + + + diff -r 1cf21430ed4a -r 921f620577e8 svghmi/analyse_widget.xslt --- a/svghmi/analyse_widget.xslt Wed Oct 05 20:44:01 2022 +0200 +++ b/svghmi/analyse_widget.xslt Thu Oct 06 10:02:46 2022 +0200 @@ -262,6 +262,42 @@ speed + + + + + + + + Arguments are either: + + + + - name=value: setting variable with literal value. + + - name=other_name: copy variable content into another + + + + "active"+"inactive" labeled elements can be provided to show feedback when pressed + + + + Exemples: + + + + HMI:Assign:notify=1@notify=/PLCVAR + + HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + + + + + + Assign variables on click + + diff -r 1cf21430ed4a -r 921f620577e8 svghmi/detachable_pages.ysl2 --- a/svghmi/detachable_pages.ysl2 Wed Oct 05 20:44:01 2022 +0200 +++ b/svghmi/detachable_pages.ysl2 Thu Oct 06 10:02:46 2022 +0200 @@ -68,7 +68,11 @@ param "page"; const "page_overlapping_geometry", "$overlapping_geometry/elt[@id = $page/@id]/*"; const "page_overlapping_elements", "//svg:*[@id = $page_overlapping_geometry/@Id]"; - const "page_sub_elements", "func:refered_elements($page | $page_overlapping_elements)"; + const "page_widgets_elements", """ + $hmi_elements[not(@id=$page/@id) + and descendant-or-self::svg:*/@id = $page_overlapping_elements/@id] + /descendant-or-self::svg:*"""; + const "page_sub_elements", "func:refered_elements($page | $page_overlapping_elements | $page_widgets_elements)"; result "$page_sub_elements"; } @@ -214,6 +218,10 @@ foreach "$detachable_elements"{ | «@id» } + | DISCARDABLES: + foreach "$discardable_elements"{ + | «@id» + } | In Foreach: foreach "$in_forEach_widget_ids"{ | «.» diff -r 1cf21430ed4a -r 921f620577e8 svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Wed Oct 05 20:44:01 2022 +0200 +++ b/svghmi/gen_index_xhtml.xslt Thu Oct 06 10:02:46 2022 +0200 @@ -555,6 +555,23 @@ + + + + + + + + + + + + + + + + + @@ -657,7 +674,8 @@ - + + @@ -890,6 +908,14 @@ + DISCARDABLES: + + + + + + + In Foreach: @@ -945,6 +971,21 @@ + + + + + + + + + + + + + + + @@ -1516,8 +1557,6 @@ var cache = hmitree_types.map(_ignored => undefined); - var updates = new Map(); - function page_local_index(varname, pagename){ @@ -1530,7 +1569,7 @@ new_index = next_available_index++; - hmi_locals[pagename] = {[varname]:new_index} + hmi_locals[pagename] = {[varname]:new_index}; } else { @@ -1556,8 +1595,6 @@ cache[new_index] = defaultval; - updates.set(new_index, defaultval); - if(persistent_locals.has(varname)) persistent_indexes.set(new_index, varname); @@ -2656,6 +2693,199 @@ } + + + + + + + + Arguments are either: + + + + - name=value: setting variable with literal value. + + - name=other_name: copy variable content into another + + + + "active"+"inactive" labeled elements can be provided to show feedback when pressed + + + + Exemples: + + + + HMI:Assign:notify=1@notify=/PLCVAR + + HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + + + + + + Assign variables on click + + + + class + AssignWidget + extends Widget{ + + frequency = 2; + + + + onmouseup(evt) { + + svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + + if(this.enable_state) { + + this.activity_state = false + + this.request_animate(); + + this.assign(); + + } + + } + + + + onmousedown(){ + + if(this.enable_state) { + + svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + + this.activity_state = true; + + this.request_animate(); + + } + + } + + + + } + + + + + + + + + /disabled + + + + + + + activable_sub:{ + + + + + + /active /inactive + + + no + + + + + + }, + + has_activity: + + , + + init: function() { + + this.bound_onmouseup = this.onmouseup.bind(this); + + this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); + + }, + + assignments: {}, + + dispatch: function(value, oldval, varnum) { + + + + + + + + + if(varnum == + + ) this.assignments[" + + "] = value; + + + + + + }, + + assign: function() { + + + + + + + + + + + + + + + + const + + = this.assignments[" + + "]; + + if( + + != undefined) + + this.apply_hmi_value( + + , + + ); + + + + this.apply_hmi_value( + + , + + ); + + + + + }, + + @@ -5923,39 +6153,31 @@ frequency = 2; - - - make_on_click() { - - let that = this; - - const name = this.args[0]; - - return function(evt){ - - /* TODO: in order to allow jumps to page selected through - - for exemple a dropdown, support path pointing to local - - variable whom value would be an HMI_TREE index and then - - jump to a relative page not hard-coded in advance - - */ - - if(that.enable_state) { - - const index = - - (that.is_relative && that.indexes.length > 0) ? - - that.indexes[0] + that.offset : undefined; - - fading_page_switch(name, index); - - that.notify(); - - } + target_page_is_current_page = false; + + button_beeing_pressed = false; + + + + onmouseup(evt) { + + svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + + if(this.enable_state) { + + const index = + + (this.is_relative && this.indexes.length > 0) ? + + this.indexes[0] + this.offset : undefined; + + this.button_beeing_pressed = false; + + this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed; + + fading_page_switch(this.args[0], index); + + this.notify(); } @@ -5963,6 +6185,24 @@ + onmousedown(){ + + if(this.enable_state) { + + svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + + this.button_beeing_pressed = true; + + this.activity_state = true; + + this.request_animate(); + + } + + } + + + notify_page_change(page_name, index) { // called from animate() @@ -5973,7 +6213,9 @@ const ref_name = this.args[0]; - this.activity_state = ((ref_name == undefined || ref_name == page_name) && index == ref_index); + this.target_page_is_current_page = ((ref_name == undefined || ref_name == page_name) && index == ref_index); + + this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed; // Since called from animate, update activity directly @@ -6031,7 +6273,9 @@ init: function() { - this.element.onclick = this.make_on_click(); + this.bound_onmouseup = this.onmouseup.bind(this); + + this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); this.activable = true; @@ -11091,1000 +11335,1064 @@ - // Apply updates recieved through ws.onmessage to subscribed widgets - - function apply_updates() { - - updates.forEach((value, index) => { + // Called on requestAnimationFrame, modifies DOM + + var requestAnimationFrameID = null; + + function animate() { + + 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"; + + } + + } + + + + if(jumps_need_update) update_jumps(); + + + + + + pending_widget_animates.forEach(widget => widget._animate()); + + pending_widget_animates = []; + + rearm = false; + + } while(0); + + + + requestAnimationFrameID = null; + + + + if(rearm) requestHMIAnimation(); + + } + + + + function requestHMIAnimation() { + + if(requestAnimationFrameID == null){ + + requestAnimationFrameID = window.requestAnimationFrame(animate); + + } + + } + + + + // Message reception handler + + // Hash is verified and HMI values updates resulting from binary parsing + + // are stored until browser can compute next frame, DOM is left untouched + + ws.onmessage = function (evt) { + + + + let data = evt.data; + + let dv = new DataView(data); + + let i = 0; + + try { + + for(let hash_int of hmi_hash) { + + if(hash_int != dv.getUint8(i)){ + + throw new Error("Hash doesn't match"); + + }; + + i++; + + }; + + + + while(i < data.byteLength){ + + let index = dv.getUint32(i, true); + + i += 4; + + let iectype = hmitree_types[index]; + + if(iectype != undefined){ + + let dvgetter = dvgetters[iectype]; + + let [value, bytesize] = dvgetter(dv,i); + + dispatch_value(index, value); + + i += bytesize; + + } else { + + throw new Error("Unknown index "+index); + + } + + }; + + + + // register for rendering on next frame, since there are updates + + } catch(err) { + + // 1003 is for "Unsupported Data" + + // ws.close(1003, err.message); + + + + // TODO : remove debug alert ? + + alert("Error : "+err.message+"\nHMI will be reloaded."); + + + + // force reload ignoring cache + + location.reload(true); + + } + + }; + + + + hmi_hash_u8 = new Uint8Array(hmi_hash); + + + + function send_blob(data) { + + if(data.length > 0) { + + ws.send(new Blob([hmi_hash_u8].concat(data))); + + }; + + }; + + + + const typedarray_types = { + + INT: (number) => new Int16Array([number]), + + BOOL: (truth) => new Int16Array([truth]), + + NODE: (truth) => new Int16Array([truth]), + + REAL: (number) => new Float32Array([number]), + + STRING: (str) => { + + // beremiz default string max size is 128 + + str = str.slice(0,128); + + binary = new Uint8Array(str.length + 1); + + binary[0] = str.length; + + for(let i = 0; i < str.length; i++){ + + binary[i+1] = str.charCodeAt(i); + + } + + return binary; + + } + + /* TODO */ + + }; + + + + function send_reset() { + + send_blob(new Uint8Array([1])); /* reset = 1 */ + + }; + + + + var subscriptions = []; + + + + function subscribers(index) { + + let entry = subscriptions[index]; + + let res; + + if(entry == undefined){ + + res = new Set(); + + subscriptions[index] = [res,0]; + + }else{ + + [res, _ign] = entry; + + } + + return res + + } + + + + function get_subscription_period(index) { + + let entry = subscriptions[index]; + + if(entry == undefined) + + return 0; + + let [_ign, period] = entry; + + return period; + + } + + + + function set_subscription_period(index, period) { + + let entry = subscriptions[index]; + + if(entry == undefined){ + + subscriptions[index] = [new Set(), period]; + + } else { + + entry[1] = period; + + } + + } + + + + if(has_watchdog){ + + // artificially subscribe the watchdog widget to "/heartbeat" hmi variable + + // Since dispatch directly calls change_hmi_value, + + // PLC will periodically send variable at given frequency + + subscribers(heartbeat_index).add({ + + /* type: "Watchdog", */ + + frequency: 1, + + indexes: [heartbeat_index], + + new_hmi_value: function(index, value, oldval) { + + apply_hmi_value(heartbeat_index, value+1); + + } + + }); + + } + + + + + + var page_fading = "off"; + + var page_fading_args = "off"; + + function fading_page_switch(...args){ + + if(page_fading == "in_progress") + + page_fading = "forced"; + + else + + page_fading = "pending"; + + page_fading_args = args; + + + + requestHMIAnimation(); + + + + } + + document.body.style.backgroundColor = "black"; + + + + // subscribe to per instance current page hmi variable + + // PLC must prefix page name with "!" for page switch to happen + + subscribers(current_page_var_index).add({ + + frequency: 1, + + indexes: [current_page_var_index], + + new_hmi_value: function(index, value, oldval) { + + if(value.startsWith("!")) + + fading_page_switch(value.slice(1)); + + } + + }); + + + + function svg_text_to_multiline(elt) { + + return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); + + } + + + + function multiline_to_svg_text(elt, str, blank) { + + str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;}); + + } + + + + function switch_langnum(langnum) { + + langnum = Math.max(0, Math.min(langs.length - 1, langnum)); + + + + for (let translation of translations) { + + let [objs, msgs] = translation; + + let msg = msgs[langnum]; + + for (let obj of objs) { + + multiline_to_svg_text(obj, msg); + + obj.setAttribute("lang",langnum); + + } + + } + + return langnum; + + } + + + + // backup original texts + + for (let translation of translations) { + + let [objs, msgs] = translation; + + msgs.unshift(svg_text_to_multiline(objs[0])); + + } + + + + var lang_local_index = hmi_local_index("lang"); + + var langcode_local_index = hmi_local_index("lang_code"); + + var langname_local_index = hmi_local_index("lang_name"); + + subscribers(lang_local_index).add({ + + indexes: [lang_local_index], + + new_hmi_value: function(index, value, oldval) { + + let current_lang = switch_langnum(value); + + let [langname,langcode] = langs[current_lang]; + + apply_hmi_value(langcode_local_index, langcode); + + apply_hmi_value(langname_local_index, langname); + + switch_page(); + + } + + }); + + + + // returns en_US, fr_FR or en_UK depending on selected language + + function get_current_lang_code(){ + + return cache[langcode_local_index]; + + } + + + + function setup_lang(){ + + let current_lang = cache[lang_local_index]; + + let new_lang = switch_langnum(current_lang); + + if(current_lang != new_lang){ + + apply_hmi_value(lang_local_index, new_lang); + + } + + } + + + + setup_lang(); + + + + function update_subscriptions() { + + let delta = []; + + for(let index in subscriptions){ + + let widgets = subscribers(index); + + + + // periods are in ms + + let previous_period = get_subscription_period(index); + + + + // subscribing with a zero period is unsubscribing + + let new_period = 0; + + if(widgets.size > 0) { + + let maxfreq = 0; + + for(let widget of widgets){ + + let wf = widget.frequency; + + if(wf != undefined && maxfreq < wf) + + maxfreq = wf; + + } + + + + if(maxfreq != 0) + + new_period = 1000/maxfreq; + + } + + + + if(previous_period != new_period) { + + set_subscription_period(index, new_period); + + if(index <= last_remote_index){ + + delta.push( + + new Uint8Array([2]), /* subscribe = 2 */ + + new Uint32Array([index]), + + new Uint16Array([new_period])); + + } + + } + + } + + send_blob(delta); + + }; + + + + function send_hmi_value(index, value) { + + if(index > last_remote_index){ dispatch_value(index, value); + + + if(persistent_indexes.has(index)){ + + let varname = persistent_indexes.get(index); + + document.cookie = varname+"="+value+"; max-age=3153600000"; + + } + + + + return; + + } + + + + let iectype = hmitree_types[index]; + + let tobinary = typedarray_types[iectype]; + + send_blob([ + + new Uint8Array([0]), /* setval = 0 */ + + new Uint32Array([index]), + + tobinary(value)]); + + + + // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf + + // cache[index] = value; + + }; + + + + function apply_hmi_value(index, new_val) { + + // Similarly to previous comment, taking decision to update based + + // on cache content is bad and can lead to inconsistency + + /*let old_val = cache[index];*/ + + if(new_val != undefined /*&& old_val != new_val*/) + + send_hmi_value(index, new_val); + + return new_val; + + } + + + + const quotes = {"'":null, '"':null}; + + + + function eval_operation_string(old_val, opstr) { + + let op = opstr[0]; + + let given_val; + + if(opstr.length < 2) + + return undefined; + + if(opstr[1] in quotes){ + + if(opstr.length < 3) + + return undefined; + + if(opstr[opstr.length-1] == opstr[1]){ + + given_val = opstr.slice(2,opstr.length-1); + + } + + } else { + + given_val = Number(opstr.slice(1)); + + } + + let new_val; + + switch(op){ + + case "=": + + new_val = given_val; + + break; + + case "+": + + new_val = old_val + given_val; + + break; + + case "-": + + new_val = old_val - given_val; + + break; + + case "*": + + new_val = old_val * given_val; + + break; + + case "/": + + new_val = old_val / given_val; + + break; + + } + + return new_val; + + } + + + + var current_visible_page; + + var current_subscribed_page; + + var current_page_index; + + var page_node_local_index = hmi_local_index("page_node"); + + var page_switch_in_progress = false; + + + + function toggleFullscreen() { + + let elem = document.documentElement; + + + + if (!document.fullscreenElement) { + + elem.requestFullscreen().catch(err => { + + console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); + }); - updates.clear(); + } else { + + document.exitFullscreen(); + + } } - // Called on requestAnimationFrame, modifies DOM - - var requestAnimationFrameID = null; - - function animate() { - - 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; + function prepare_svg() { + + // prevents context menu from appearing on right click and long touch + + document.body.addEventListener('contextmenu', e => { + + toggleFullscreen(); + + e.preventDefault(); + + }); + + + + for(let eltid in detachable_elements){ + + let [element,parent] = detachable_elements[eltid]; + + parent.removeChild(element); + + } + + }; + + + + function switch_page(page_name, page_index) { + + if(page_switch_in_progress){ + + /* page switch already going */ + + /* TODO LOG ERROR */ + + return false; + + } + + page_switch_in_progress = true; + + + + if(page_name == undefined) + + page_name = current_subscribed_page; + + else if(page_index == undefined){ + + [page_name, page_index] = page_name.split('@') + + } + + + + let old_desc = page_desc[current_subscribed_page]; + + let new_desc = page_desc[page_name]; + + + + if(new_desc == undefined){ + + /* TODO LOG ERROR */ + + return false; + + } + + + + if(page_index == undefined) + + page_index = new_desc.page_index; + + else if(typeof(page_index) == "string") { + + let hmitree_node = hmitree_nodes[page_index]; + + if(hmitree_node !== undefined){ + + let [int_index, hmiclass] = hmitree_node; + + if(hmiclass == new_desc.page_class) + + page_index = int_index; + + else + + page_index = new_desc.page_index; + + } else { + + page_index = new_desc.page_index; } - - - // Do the page swith if pending - - if(page_switch_in_progress){ - - if(current_subscribed_page != current_visible_page){ - - switch_visible_page(current_subscribed_page); + } + + + + if(old_desc){ + + old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); + + } + + const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; + + + + const container_id = page_name + (page_index != undefined ? page_index : ""); + + + + new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); + + + + update_subscriptions(); + + + + current_subscribed_page = page_name; + + current_page_index = page_index; + + let page_node; + + if(page_index != undefined){ + + page_node = hmitree_paths[page_index]; + + }else{ + + page_node = ""; + + } + + apply_hmi_value(page_node_local_index, page_node); + + + + jumps_need_update = true; + + + + requestHMIAnimation(); + + jump_history.push([page_name, page_index]); + + if(jump_history.length > 42) + + jump_history.shift(); + + + + apply_hmi_value(current_page_var_index, page_index == undefined + + ? page_name + + : page_name + "@" + hmitree_paths[page_index]); + + + + return true; + + }; + + + + function switch_visible_page(page_name) { + + + + let old_desc = page_desc[current_visible_page]; + + let new_desc = page_desc[page_name]; + + + + if(old_desc){ + + for(let eltid in old_desc.required_detachables){ + + if(!(eltid in new_desc.required_detachables)){ + + let [element, parent] = old_desc.required_detachables[eltid]; + + parent.removeChild(element); } - - - page_switch_in_progress = false; - - - - if(page_fading == "in_progress"){ - - svg_root.classList.remove("fade-out-page"); - - page_fading = "off"; + } + + for(let eltid in new_desc.required_detachables){ + + if(!(eltid in old_desc.required_detachables)){ + + let [element, parent] = new_desc.required_detachables[eltid]; + + parent.appendChild(element); } } - - - if(jumps_need_update) update_jumps(); - - - - - - pending_widget_animates.forEach(widget => widget._animate()); - - pending_widget_animates = []; - - rearm = false; - - } while(0); - - - - requestAnimationFrameID = null; - - - - if(rearm) requestHMIAnimation(); + }else{ + + for(let eltid in new_desc.required_detachables){ + + let [element, parent] = new_desc.required_detachables[eltid]; + + parent.appendChild(element); + + } + + } + + + + svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); + + current_visible_page = page_name; + + }; + + + + /* From https://jsfiddle.net/ibowankenobi/1mmh7rs6/6/ */ + + function getAbsoluteCTM(element){ + + var height = svg_root.height.baseVal.value, + + width = svg_root.width.baseVal.value, + + viewBoxRect = svg_root.viewBox.baseVal, + + vHeight = viewBoxRect.height, + + vWidth = viewBoxRect.width; + + if(!vWidth || !vHeight){ + + return element.getCTM(); + + } + + var sH = height/vHeight, + + sW = width/vWidth, + + matrix = svg_root.createSVGMatrix(); + + matrix.a = sW; + + matrix.d = sH + + var realCTM = element.getCTM().multiply(matrix.inverse()); + + realCTM.e = realCTM.e/sW + viewBoxRect.x; + + realCTM.f = realCTM.f/sH + viewBoxRect.y; + + return realCTM; } - function requestHMIAnimation() { - - if(requestAnimationFrameID == null){ - - requestAnimationFrameID = window.requestAnimationFrame(animate); - - } + function apply_reference_frames(){ + + const matches = svg_root.querySelectorAll("g[svghmi_x_offset]"); + + matches.forEach((group) => { + + let [x,y] = ["x", "y"].map((axis) => Number(group.getAttribute("svghmi_"+axis+"_offset"))); + + let ctm = getAbsoluteCTM(group); + + // zero translation part of CTM + + // to only apply rotation/skewing to offset vector + + ctm.e = 0; + + ctm.f = 0; + + let invctm = ctm.inverse(); + + let vect = new DOMPoint(x, y); + + let newvect = vect.matrixTransform(invctm); + + let transform = svg_root.createSVGTransform(); + + transform.setTranslate(newvect.x, newvect.y); + + group.transform.baseVal.appendItem(transform); + + ["x", "y"].forEach((axis) => group.removeAttribute("svghmi_"+axis+"_offset")); + + }); } - // Message reception handler - - // Hash is verified and HMI values updates resulting from binary parsing - - // are stored until browser can compute next frame, DOM is left untouched - - ws.onmessage = function (evt) { - - - - let data = evt.data; - - let dv = new DataView(data); - - let i = 0; - - try { - - for(let hash_int of hmi_hash) { - - if(hash_int != dv.getUint8(i)){ - - throw new Error("Hash doesn't match"); - - }; - - i++; - - }; - - - - while(i < data.byteLength){ - - let index = dv.getUint32(i, true); - - i += 4; - - let iectype = hmitree_types[index]; - - if(iectype != undefined){ - - let dvgetter = dvgetters[iectype]; - - let [value, bytesize] = dvgetter(dv,i); - - updates.set(index, value); - - i += bytesize; - - } else { - - throw new Error("Unknown index "+index); - - } - - }; - - - - apply_updates(); - - // register for rendering on next frame, since there are updates - - } catch(err) { - - // 1003 is for "Unsupported Data" - - // ws.close(1003, err.message); - - - - // TODO : remove debug alert ? - - alert("Error : "+err.message+"\nHMI will be reloaded."); - - - - // force reload ignoring cache - - location.reload(true); - - } - - }; - - - - hmi_hash_u8 = new Uint8Array(hmi_hash); - - - - function send_blob(data) { - - if(data.length > 0) { - - ws.send(new Blob([hmi_hash_u8].concat(data))); - - }; - - }; - - - - const typedarray_types = { - - INT: (number) => new Int16Array([number]), - - BOOL: (truth) => new Int16Array([truth]), - - NODE: (truth) => new Int16Array([truth]), - - REAL: (number) => new Float32Array([number]), - - STRING: (str) => { - - // beremiz default string max size is 128 - - str = str.slice(0,128); - - binary = new Uint8Array(str.length + 1); - - binary[0] = str.length; - - for(let i = 0; i < str.length; i++){ - - binary[i+1] = str.charCodeAt(i); - - } - - return binary; - - } - - /* TODO */ - - }; - - - - function send_reset() { - - send_blob(new Uint8Array([1])); /* reset = 1 */ - - }; - - - - var subscriptions = []; - - - - function subscribers(index) { - - let entry = subscriptions[index]; - - let res; - - if(entry == undefined){ - - res = new Set(); - - subscriptions[index] = [res,0]; - - }else{ - - [res, _ign] = entry; - - } - - return res - - } - - - - function get_subscription_period(index) { - - let entry = subscriptions[index]; - - if(entry == undefined) - - return 0; - - let [_ign, period] = entry; - - return period; - - } - - - - function set_subscription_period(index, period) { - - let entry = subscriptions[index]; - - if(entry == undefined){ - - subscriptions[index] = [new Set(), period]; - - } else { - - entry[1] = period; - - } - - } - - - - if(has_watchdog){ - - // artificially subscribe the watchdog widget to "/heartbeat" hmi variable - - // Since dispatch directly calls change_hmi_value, - - // PLC will periodically send variable at given frequency - - subscribers(heartbeat_index).add({ - - /* type: "Watchdog", */ - - frequency: 1, - - indexes: [heartbeat_index], - - new_hmi_value: function(index, value, oldval) { - - apply_hmi_value(heartbeat_index, value+1); - - } - - }); - - } - - - - - - var page_fading = "off"; - - var page_fading_args = "off"; - - function fading_page_switch(...args){ - - if(page_fading == "in_progress") - - page_fading = "forced"; - - else - - page_fading = "pending"; - - page_fading_args = args; - - - - requestHMIAnimation(); - - - - } - - document.body.style.backgroundColor = "black"; - - - - // subscribe to per instance current page hmi variable - - // PLC must prefix page name with "!" for page switch to happen - - subscribers(current_page_var_index).add({ - - frequency: 1, - - indexes: [current_page_var_index], - - new_hmi_value: function(index, value, oldval) { - - if(value.startsWith("!")) - - fading_page_switch(value.slice(1)); - - } - - }); - - - - function svg_text_to_multiline(elt) { - - return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); - - } - - - - function multiline_to_svg_text(elt, str, blank) { - - str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;}); - - } - - - - function switch_langnum(langnum) { - - langnum = Math.max(0, Math.min(langs.length - 1, langnum)); - - - - for (let translation of translations) { - - let [objs, msgs] = translation; - - let msg = msgs[langnum]; - - for (let obj of objs) { - - multiline_to_svg_text(obj, msg); - - obj.setAttribute("lang",langnum); - - } - - } - - return langnum; - - } - - - - // backup original texts - - for (let translation of translations) { - - let [objs, msgs] = translation; - - msgs.unshift(svg_text_to_multiline(objs[0])); - - } - - - - var lang_local_index = hmi_local_index("lang"); - - var langcode_local_index = hmi_local_index("lang_code"); - - var langname_local_index = hmi_local_index("lang_name"); - - subscribers(lang_local_index).add({ - - indexes: [lang_local_index], - - new_hmi_value: function(index, value, oldval) { - - let current_lang = switch_langnum(value); - - let [langname,langcode] = langs[current_lang]; - - apply_hmi_value(langcode_local_index, langcode); - - apply_hmi_value(langname_local_index, langname); - - switch_page(); - - } - - }); - - - - // returns en_US, fr_FR or en_UK depending on selected language - - function get_current_lang_code(){ - - return cache[langcode_local_index]; - - } - - - - function setup_lang(){ - - let current_lang = cache[lang_local_index]; - - let new_lang = switch_langnum(current_lang); - - if(current_lang != new_lang){ - - apply_hmi_value(lang_local_index, new_lang); - - } - - } - - - - setup_lang(); - - - - function update_subscriptions() { - - let delta = []; - - for(let index in subscriptions){ - - let widgets = subscribers(index); - - - - // periods are in ms - - let previous_period = get_subscription_period(index); - - - - // subscribing with a zero period is unsubscribing - - let new_period = 0; - - if(widgets.size > 0) { - - let maxfreq = 0; - - for(let widget of widgets){ - - let wf = widget.frequency; - - if(wf != undefined && maxfreq < wf) - - maxfreq = wf; - - } - - - - if(maxfreq != 0) - - new_period = 1000/maxfreq; - - } - - - - if(previous_period != new_period) { - - set_subscription_period(index, new_period); - - if(index <= last_remote_index){ - - delta.push( - - new Uint8Array([2]), /* subscribe = 2 */ - - new Uint32Array([index]), - - new Uint16Array([new_period])); - - } - - } - - } - - send_blob(delta); - - }; - - - - function send_hmi_value(index, value) { - - if(index > last_remote_index){ - - dispatch_value(index, value); - - - - if(persistent_indexes.has(index)){ - - let varname = persistent_indexes.get(index); - - document.cookie = varname+"="+value+"; max-age=3153600000"; - - } - - - - return; - - } - - - - let iectype = hmitree_types[index]; - - let tobinary = typedarray_types[iectype]; - - send_blob([ - - new Uint8Array([0]), /* setval = 0 */ - - new Uint32Array([index]), - - tobinary(value)]); - - - - // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf - - // cache[index] = value; - - }; - - - - function apply_hmi_value(index, new_val) { - - // Similarly to previous comment, taking decision to update based - - // on cache content is bad and can lead to inconsistency - - /*let old_val = cache[index];*/ - - if(new_val != undefined /*&& old_val != new_val*/) - - send_hmi_value(index, new_val); - - return new_val; - - } - - - - const quotes = {"'":null, '"':null}; - - - - function eval_operation_string(old_val, opstr) { - - let op = opstr[0]; - - let given_val; - - if(opstr.length < 2) - - return undefined; - - if(opstr[1] in quotes){ - - if(opstr.length < 3) - - return undefined; - - if(opstr[opstr.length-1] == opstr[1]){ - - given_val = opstr.slice(2,opstr.length-1); - - } - - } else { - - given_val = Number(opstr.slice(1)); - - } - - let new_val; - - switch(op){ - - case "=": - - new_val = given_val; - - break; - - case "+": - - new_val = old_val + given_val; - - break; - - case "-": - - new_val = old_val - given_val; - - break; - - case "*": - - new_val = old_val * given_val; - - break; - - case "/": - - new_val = old_val / given_val; - - break; - - } - - return new_val; - - } - - - - var current_visible_page; - - var current_subscribed_page; - - var current_page_index; - - var page_node_local_index = hmi_local_index("page_node"); - - var page_switch_in_progress = false; - - - - function toggleFullscreen() { - - let elem = document.documentElement; - - - - if (!document.fullscreenElement) { - - elem.requestFullscreen().catch(err => { - - console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); - - }); - - } else { - - document.exitFullscreen(); - - } - - } - - - - function prepare_svg() { - - // prevents context menu from appearing on right click and long touch - - document.body.addEventListener('contextmenu', e => { - - toggleFullscreen(); - - e.preventDefault(); - - }); - - - - for(let eltid in detachable_elements){ - - let [element,parent] = detachable_elements[eltid]; - - parent.removeChild(element); - - } - - }; - - - - function switch_page(page_name, page_index) { - - if(page_switch_in_progress){ - - /* page switch already going */ - - /* TODO LOG ERROR */ - - return false; - - } - - page_switch_in_progress = true; - - - - if(page_name == undefined) - - page_name = current_subscribed_page; - - else if(page_index == undefined){ - - [page_name, page_index] = page_name.split('@') - - } - - - - let old_desc = page_desc[current_subscribed_page]; - - let new_desc = page_desc[page_name]; - - - - if(new_desc == undefined){ - - /* TODO LOG ERROR */ - - return false; - - } - - - - if(page_index == undefined) - - page_index = new_desc.page_index; - - else if(typeof(page_index) == "string") { - - let hmitree_node = hmitree_nodes[page_index]; - - if(hmitree_node !== undefined){ - - let [int_index, hmiclass] = hmitree_node; - - if(hmiclass == new_desc.page_class) - - page_index = int_index; - - else - - page_index = new_desc.page_index; - - } else { - - page_index = new_desc.page_index; - - } - - } - - - - if(old_desc){ - - old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); - - } - - const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; - - - - const container_id = page_name + (page_index != undefined ? page_index : ""); - - - - new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); - - - - update_subscriptions(); - - - - current_subscribed_page = page_name; - - current_page_index = page_index; - - let page_node; - - if(page_index != undefined){ - - page_node = hmitree_paths[page_index]; - - }else{ - - page_node = ""; - - } - - apply_hmi_value(page_node_local_index, page_node); - - - - jumps_need_update = true; - - - - requestHMIAnimation(); - - jump_history.push([page_name, page_index]); - - if(jump_history.length > 42) - - jump_history.shift(); - - - - apply_hmi_value(current_page_var_index, page_index == undefined - - ? page_name - - : page_name + "@" + hmitree_paths[page_index]); - - - - return true; - - }; - - - - function switch_visible_page(page_name) { - - - - let old_desc = page_desc[current_visible_page]; - - let new_desc = page_desc[page_name]; - - - - if(old_desc){ - - for(let eltid in old_desc.required_detachables){ - - if(!(eltid in new_desc.required_detachables)){ - - let [element, parent] = old_desc.required_detachables[eltid]; - - parent.removeChild(element); - - } - - } - - for(let eltid in new_desc.required_detachables){ - - if(!(eltid in old_desc.required_detachables)){ - - let [element, parent] = new_desc.required_detachables[eltid]; - - parent.appendChild(element); - - } - - } - - }else{ - - for(let eltid in new_desc.required_detachables){ - - let [element, parent] = new_desc.required_detachables[eltid]; - - parent.appendChild(element); - - } - - } - - - - svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); - - current_visible_page = page_name; - - }; - - - // Once connection established ws.onopen = function (evt) { + apply_reference_frames(); + init_widgets(); send_reset(); diff -r 1cf21430ed4a -r 921f620577e8 svghmi/geometry.ysl2 --- a/svghmi/geometry.ysl2 Wed Oct 05 20:44:01 2022 +0200 +++ b/svghmi/geometry.ysl2 Thu Oct 06 10:02:46 2022 +0200 @@ -145,3 +145,16 @@ result """$candidates[(@Id = $groups/@id and (func:intersect($g, .) = 9)) or (not(@Id = $groups/@id) and (func:intersect($g, .) > 0 ))]"""; } + +def "func:offset" { + param "elt1"; + param "elt2"; + const "g1", "$geometry[@Id = $elt1/@id]"; + const "g2", "$geometry[@Id = $elt2/@id]"; + const "result" vector { + attrib "x" value "$g2/@x - $g1/@x"; + attrib "y" value "$g2/@y - $g1/@y"; + } + result "exsl:node-set($result)"; +} + diff -r 1cf21430ed4a -r 921f620577e8 svghmi/inline_svg.ysl2 --- a/svghmi/inline_svg.ysl2 Wed Oct 05 20:44:01 2022 +0200 +++ b/svghmi/inline_svg.ysl2 Thu Oct 06 10:02:46 2022 +0200 @@ -42,6 +42,23 @@ attrib "{name()}" > «substring(., 2)» } +// remove "reference" and "frame" rectangles +svgtmpl "svg:rect[@inkscape:label='reference' or @inkscape:label='frame']", mode="inline_svg" { + // nothing +} + +svgtmpl "svg:g[svg:rect/@inkscape:label='frame']", mode="inline_svg" { + const "reference_rect","(../svg:rect | ../svg:g/svg:rect)[@inkscape:label='reference']"; + const "frame_rect","svg:rect[@inkscape:label='frame']"; + const "offset","func:offset($frame_rect, $reference_rect)"; + + xsl:copy { + attrib "svghmi_x_offset" value "$offset/vector/@x"; + attrib "svghmi_y_offset" value "$offset/vector/@y"; + apply "@* | node()", mode="inline_svg"; + } +} + ////// Clone unlinking // // svg:use (inkscape's clones) inside a widgets are diff -r 1cf21430ed4a -r 921f620577e8 svghmi/svghmi.js --- a/svghmi/svghmi.js Wed Oct 05 20:44:01 2022 +0200 +++ b/svghmi/svghmi.js Thu Oct 06 10:02:46 2022 +0200 @@ -46,14 +46,6 @@ } }; -// Apply updates recieved through ws.onmessage to subscribed widgets -function apply_updates() { - updates.forEach((value, index) => { - dispatch_value(index, value); - }); - updates.clear(); -} - // Called on requestAnimationFrame, modifies DOM var requestAnimationFrameID = null; function animate() { @@ -126,14 +118,13 @@ if(iectype != undefined){ let dvgetter = dvgetters[iectype]; let [value, bytesize] = dvgetter(dv,i); - updates.set(index, value); + dispatch_value(index, value); i += bytesize; } else { throw new Error("Unknown index "+index); } }; - apply_updates(); // register for rendering on next frame, since there are updates } catch(err) { // 1003 is for "Unsupported Data" @@ -541,8 +532,49 @@ current_visible_page = page_name; }; +/* From https://jsfiddle.net/ibowankenobi/1mmh7rs6/6/ */ +function getAbsoluteCTM(element){ + var height = svg_root.height.baseVal.value, + width = svg_root.width.baseVal.value, + viewBoxRect = svg_root.viewBox.baseVal, + vHeight = viewBoxRect.height, + vWidth = viewBoxRect.width; + if(!vWidth || !vHeight){ + return element.getCTM(); + } + var sH = height/vHeight, + sW = width/vWidth, + matrix = svg_root.createSVGMatrix(); + matrix.a = sW; + matrix.d = sH + var realCTM = element.getCTM().multiply(matrix.inverse()); + realCTM.e = realCTM.e/sW + viewBoxRect.x; + realCTM.f = realCTM.f/sH + viewBoxRect.y; + return realCTM; +} + +function apply_reference_frames(){ + const matches = svg_root.querySelectorAll("g[svghmi_x_offset]"); + matches.forEach((group) => { + let [x,y] = ["x", "y"].map((axis) => Number(group.getAttribute("svghmi_"+axis+"_offset"))); + let ctm = getAbsoluteCTM(group); + // zero translation part of CTM + // to only apply rotation/skewing to offset vector + ctm.e = 0; + ctm.f = 0; + let invctm = ctm.inverse(); + let vect = new DOMPoint(x, y); + let newvect = vect.matrixTransform(invctm); + let transform = svg_root.createSVGTransform(); + transform.setTranslate(newvect.x, newvect.y); + group.transform.baseVal.appendItem(transform); + ["x", "y"].forEach((axis) => group.removeAttribute("svghmi_"+axis+"_offset")); + }); +} + // Once connection established ws.onopen = function (evt) { + apply_reference_frames(); init_widgets(); send_reset(); // show main page diff -r 1cf21430ed4a -r 921f620577e8 svghmi/widget_assign.ysl2 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svghmi/widget_assign.ysl2 Thu Oct 06 10:02:46 2022 +0200 @@ -0,0 +1,88 @@ +// widget_assign.ysl2 + +widget_desc("Assign") { + longdesc + || + + Arguments are either: + + - name=value: setting variable with literal value. + - name=other_name: copy variable content into another + + "active"+"inactive" labeled elements can be provided to show feedback when pressed + + Exemples: + + HMI:Assign:notify=1@notify=/PLCVAR + HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + + || + + shortdesc > Assign variables on click + +} + +widget_class("Assign") { +|| + frequency = 2; + + onmouseup(evt) { + svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + if(this.enable_state) { + this.activity_state = false + this.request_animate(); + this.assign(); + } + } + + onmousedown(){ + if(this.enable_state) { + svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + this.activity_state = true; + this.request_animate(); + } + } + +|| +} + +widget_defs("Assign") { + optional_activable(); + + | init: function() { + | this.bound_onmouseup = this.onmouseup.bind(this); + | this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); + | }, + + | assignments: {}, + | dispatch: function(value, oldval, varnum) { + const "widget", "."; + foreach "path" { + const "varid","generate-id()"; + const "varnum","position()-1"; + if "@assign" foreach "$widget/path[@assign]" if "$varid = generate-id()" { + | if(varnum == «$varnum») this.assignments["«@assign»"] = value; + } + } + | }, + | assign: function() { + const "paths","path"; + foreach "arg[contains(@value,'=')]"{ + const "name","substring-before(@value,'=')"; + const "value","substring-after(@value,'=')"; + const "index" foreach "$paths" if "@assign = $name" value "position()-1"; + const "isVarName", "regexp:test($value,'^[a-zA-Z_][a-zA-Z0-9_]+$')"; + choose { + when "$isVarName"{ + | const «$value» = this.assignments["«$value»"]; + | if(«$value» != undefined) + | this.apply_hmi_value(«$index», «$value»); + } + otherwise { + | this.apply_hmi_value(«$index», «$value»); + } + } + } + | }, +} + diff -r 1cf21430ed4a -r 921f620577e8 svghmi/widget_jump.ysl2 --- a/svghmi/widget_jump.ysl2 Wed Oct 05 20:44:01 2022 +0200 +++ b/svghmi/widget_jump.ysl2 Thu Oct 06 10:02:46 2022 +0200 @@ -52,23 +52,28 @@ || activable = false; frequency = 2; + target_page_is_current_page = false; + button_beeing_pressed = false; - make_on_click() { - let that = this; - const name = this.args[0]; - return function(evt){ - /* TODO: in order to allow jumps to page selected through - for exemple a dropdown, support path pointing to local - variable whom value would be an HMI_TREE index and then - jump to a relative page not hard-coded in advance - */ - if(that.enable_state) { - const index = - (that.is_relative && that.indexes.length > 0) ? - that.indexes[0] + that.offset : undefined; - fading_page_switch(name, index); - that.notify(); - } + onmouseup(evt) { + svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + if(this.enable_state) { + const index = + (this.is_relative && this.indexes.length > 0) ? + this.indexes[0] + this.offset : undefined; + this.button_beeing_pressed = false; + this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed; + fading_page_switch(this.args[0], index); + this.notify(); + } + } + + onmousedown(){ + if(this.enable_state) { + svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + this.button_beeing_pressed = true; + this.activity_state = true; + this.request_animate(); } } @@ -77,7 +82,8 @@ if(this.activable) { const ref_index = this.indexes.length > 0 ? this.indexes[0] + this.offset : undefined; const ref_name = this.args[0]; - this.activity_state = ((ref_name == undefined || ref_name == page_name) && index == ref_index); + this.target_page_is_current_page = ((ref_name == undefined || ref_name == page_name) && index == ref_index); + this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed; // Since called from animate, update activity directly if(this.enable_displayed_state && this.has_activity) { this.animate_activity(); @@ -98,7 +104,8 @@ const "jump_disability","$has_activity and $has_disability"; | init: function() { - | this.element.onclick = this.make_on_click(); + | this.bound_onmouseup = this.onmouseup.bind(this); + | this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); if "$has_activity" { | this.activable = true; } diff -r 1cf21430ed4a -r 921f620577e8 svghmi/widgets_common.ysl2 --- a/svghmi/widgets_common.ysl2 Wed Oct 05 20:44:01 2022 +0200 +++ b/svghmi/widgets_common.ysl2 Thu Oct 06 10:02:46 2022 +0200 @@ -189,14 +189,13 @@ ]); var persistent_indexes = new Map(); var cache = hmitree_types.map(_ignored => undefined); - var updates = new Map(); function page_local_index(varname, pagename){ let pagevars = hmi_locals[pagename]; let new_index; if(pagevars == undefined){ new_index = next_available_index++; - hmi_locals[pagename] = {[varname]:new_index} + hmi_locals[pagename] = {[varname]:new_index}; } else { let result = pagevars[varname]; if(result != undefined) { @@ -209,7 +208,6 @@ let defaultval = local_defaults[varname]; if(defaultval != undefined) { cache[new_index] = defaultval; - updates.set(new_index, defaultval); if(persistent_locals.has(varname)) persistent_indexes.set(new_index, varname); }