--- 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 @@
<xsl:text>speed</xsl:text>
+ <xsl:template match="widget[@type='Assign']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Arguments are either: + <xsl:text>- name=value: setting variable with literal value. + <xsl:text>- name=other_name: copy variable content into another + <xsl:text>"active"+"inactive" labeled elements can be provided to show feedback when pressed + <xsl:text>HMI:Assign:notify=1@notify=/PLCVAR + <xsl:text>HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + <xsl:text>Assign variables on click</xsl:text> <xsl:template match="widget[@type='Back']" mode="widget_desc">
<xsl:value-of select="@type"/>
--- 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 @@
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"{
+ foreach "$discardable_elements"{ foreach "$in_forEach_widget_ids"{
--- 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 @@
<xsl:variable name="candidates" select="$geometry[@Id != $elt/@id]"/>
<func:result select="$candidates[(@Id = $groups/@id and (func:intersect($g, .) = 9)) or (not(@Id = $groups/@id) and (func:intersect($g, .) > 0 ))]"/>
+ <func:function name="func:offset"> + <xsl:param name="elt1"/> + <xsl:param name="elt2"/> + <xsl:variable name="g1" select="$geometry[@Id = $elt1/@id]"/> + <xsl:variable name="g2" select="$geometry[@Id = $elt2/@id]"/> + <xsl:variable name="result"> + <xsl:attribute name="x"> + <xsl:value-of select="$g2/@x - $g1/@x"/> + <xsl:attribute name="y"> + <xsl:value-of select="$g2/@y - $g1/@y"/> + <func:result select="exsl:node-set($result)"/> <xsl:variable name="hmi_lists_descs" select="$parsed_widgets/widget[@type = 'List']"/>
<xsl:variable name="hmi_lists" select="$hmi_elements[@id = $hmi_lists_descs/@id]"/>
<xsl:variable name="hmi_textlists_descs" select="$parsed_widgets/widget[@type = 'TextList']"/>
@@ -657,7 +674,8 @@
<xsl:variable name="page_overlapping_geometry" select="$overlapping_geometry/elt[@id = $page/@id]/*"/>
<xsl:variable name="page_overlapping_elements" select="//svg:*[@id = $page_overlapping_geometry/@Id]"/>
- <xsl:variable name="page_sub_elements" select="func:refered_elements($page | $page_overlapping_elements)"/>
+ <xsl:variable name="page_widgets_elements" select=" $hmi_elements[not(@id=$page/@id) and descendant-or-self::svg:*/@id = $page_overlapping_elements/@id] /descendant-or-self::svg:*"/> + <xsl:variable name="page_sub_elements" select="func:refered_elements($page | $page_overlapping_elements | $page_widgets_elements)"/> <func:result select="$page_sub_elements"/>
<func:function name="func:required_elements">
@@ -890,6 +908,14 @@
+ <xsl:text>DISCARDABLES: + <xsl:for-each select="$discardable_elements"> + <xsl:value-of select="@id"/> <xsl:for-each select="$in_forEach_widget_ids">
@@ -945,6 +971,21 @@
<xsl:value-of select="substring(., 2)"/>
+ <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:rect[@inkscape:label='reference' or @inkscape:label='frame']"/> + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:g[svg:rect/@inkscape:label='frame']"> + <xsl:variable name="reference_rect" select="(../svg:rect | ../svg:g/svg:rect)[@inkscape:label='reference']"/> + <xsl:variable name="frame_rect" select="svg:rect[@inkscape:label='frame']"/> + <xsl:variable name="offset" select="func:offset($frame_rect, $reference_rect)"/> + <xsl:attribute name="svghmi_x_offset"> + <xsl:value-of select="$offset/vector/@x"/> + <xsl:attribute name="svghmi_y_offset"> + <xsl:value-of select="$offset/vector/@y"/> + <xsl:apply-templates mode="inline_svg" select="@* | node()"/> <xsl:variable name="targets_not_to_unlink" select="$hmi_lists/descendant-or-self::svg:*"/>
<xsl:variable name="to_unlink" select="$hmi_widgets/descendant-or-self::svg:use"/>
<func:function name="func:is_unlinkable">
@@ -1516,8 +1557,6 @@
<xsl:text>var cache = hmitree_types.map(_ignored => undefined);
- <xsl:text>var updates = new Map();
<xsl:text>function page_local_index(varname, pagename){
@@ -1530,7 +1569,7 @@
<xsl:text> new_index = next_available_index++;
- <xsl:text> hmi_locals[pagename] = {[varname]:new_index}
+ <xsl:text> hmi_locals[pagename] = {[varname]:new_index}; @@ -1556,8 +1595,6 @@
<xsl:text> cache[new_index] = defaultval;
- <xsl:text> updates.set(new_index, defaultval);
<xsl:text> if(persistent_locals.has(varname))
<xsl:text> persistent_indexes.set(new_index, varname);
@@ -2656,6 +2693,199 @@
+ <xsl:template match="widget[@type='Assign']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Arguments are either: + <xsl:text>- name=value: setting variable with literal value. + <xsl:text>- name=other_name: copy variable content into another + <xsl:text>"active"+"inactive" labeled elements can be provided to show feedback when pressed + <xsl:text>HMI:Assign:notify=1@notify=/PLCVAR + <xsl:text>HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + <xsl:text>Assign variables on click</xsl:text> + <xsl:template match="widget[@type='Assign']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>AssignWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 2; + <xsl:text> onmouseup(evt) { + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + <xsl:text> if(this.enable_state) { + <xsl:text> this.activity_state = false + <xsl:text> this.request_animate(); + <xsl:text> this.assign(); + <xsl:text> onmousedown(){ + <xsl:text> if(this.enable_state) { + <xsl:text> svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + <xsl:text> this.activity_state = true; + <xsl:text> this.request_animate(); + <xsl:template match="widget[@type='Assign']" mode="widget_defs"> + <xsl:param name="hmi_element"/> + <xsl:variable name="disability"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/disabled</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> activable_sub:{ + <xsl:variable name="activity"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/active /inactive</xsl:text> + <xsl:with-param name="mandatory"> + <xsl:text>no</xsl:text> + <xsl:value-of select="$activity"/> + <xsl:variable name="has_activity" select="string-length($activity)>0"/> + <xsl:text> has_activity: </xsl:text> + <xsl:value-of select="$has_activity"/> + <xsl:text> init: function() { + <xsl:text> this.bound_onmouseup = this.onmouseup.bind(this); + <xsl:text> this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); + <xsl:text> assignments: {}, + <xsl:text> dispatch: function(value, oldval, varnum) { + <xsl:variable name="widget" select="."/> + <xsl:for-each select="path"> + <xsl:variable name="varid" select="generate-id()"/> + <xsl:variable name="varnum" select="position()-1"/> + <xsl:if test="@assign"> + <xsl:for-each select="$widget/path[@assign]"> + <xsl:if test="$varid = generate-id()"> + <xsl:text> if(varnum == </xsl:text> + <xsl:value-of select="$varnum"/> + <xsl:text>) this.assignments["</xsl:text> + <xsl:value-of select="@assign"/> + <xsl:text> assign: function() { + <xsl:variable name="paths" select="path"/> + <xsl:for-each select="arg[contains(@value,'=')]"> + <xsl:variable name="name" select="substring-before(@value,'=')"/> + <xsl:variable name="value" select="substring-after(@value,'=')"/> + <xsl:variable name="index"> + <xsl:for-each select="$paths"> + <xsl:if test="@assign = $name"> + <xsl:value-of select="position()-1"/> + <xsl:variable name="isVarName" select="regexp:test($value,'^[a-zA-Z_][a-zA-Z0-9_]+$')"/> + <xsl:when test="$isVarName"> + <xsl:text> const </xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> = this.assignments["</xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> if(</xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> != undefined) + <xsl:text> this.apply_hmi_value(</xsl:text> + <xsl:value-of select="$index"/> + <xsl:text>, </xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> this.apply_hmi_value(</xsl:text> + <xsl:value-of select="$index"/> + <xsl:text>, </xsl:text> + <xsl:value-of select="$value"/> <xsl:template match="widget[@type='Back']" mode="widget_desc">
<xsl:value-of select="@type"/>
@@ -5923,39 +6153,49 @@
<xsl:text> frequency = 2;
- <xsl:text> make_on_click() {
- <xsl:text> let that = this;
- <xsl:text> const name = this.args[0];
- <xsl:text> return function(evt){
- <xsl:text> /* TODO: in order to allow jumps to page selected through
- <xsl:text> for exemple a dropdown, support path pointing to local
- <xsl:text> variable whom value would be an HMI_TREE index and then
- <xsl:text> jump to a relative page not hard-coded in advance
- <xsl:text> if(that.enable_state) {
- <xsl:text> const index =
- <xsl:text> (that.is_relative && that.indexes.length > 0) ?
- <xsl:text> that.indexes[0] + that.offset : undefined;
- <xsl:text> fading_page_switch(name, index);
- <xsl:text> that.notify();
+ <xsl:text> target_page_is_current_page = false; + <xsl:text> button_beeing_pressed = false; + <xsl:text> onmouseup(evt) { + <xsl:text> svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + <xsl:text> if(this.enable_state) { + <xsl:text> const index = + <xsl:text> (this.is_relative && this.indexes.length > 0) ? + <xsl:text> this.indexes[0] + this.offset : undefined; + <xsl:text> this.button_beeing_pressed = false; + <xsl:text> this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed; + <xsl:text> fading_page_switch(this.args[0], index); + <xsl:text> this.notify(); + <xsl:text> onmousedown(){ + <xsl:text> if(this.enable_state) { + <xsl:text> svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + <xsl:text> this.button_beeing_pressed = true; + <xsl:text> this.activity_state = true; + <xsl:text> this.request_animate(); @@ -5973,7 +6213,9 @@
<xsl:text> const ref_name = this.args[0];
- <xsl:text> this.activity_state = ((ref_name == undefined || ref_name == page_name) && index == ref_index);
+ <xsl:text> this.target_page_is_current_page = ((ref_name == undefined || ref_name == page_name) && index == ref_index); + <xsl:text> this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed; <xsl:text> // Since called from animate, update activity directly
@@ -6031,7 +6273,9 @@
<xsl:variable name="jump_disability" select="$has_activity and $has_disability"/>
<xsl:text> init: function() {
- <xsl:text> this.element.onclick = this.make_on_click();
+ <xsl:text> this.bound_onmouseup = this.onmouseup.bind(this); + <xsl:text> this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); <xsl:if test="$has_activity">
<xsl:text> this.activable = true;
@@ -11091,22 +11335,6 @@
- <xsl:text>// Apply updates recieved through ws.onmessage to subscribed widgets
- <xsl:text>function apply_updates() {
- <xsl:text> updates.forEach((value, index) => {
- <xsl:text> dispatch_value(index, value);
- <xsl:text> updates.clear();
<xsl:text>// Called on requestAnimationFrame, modifies DOM
<xsl:text>var requestAnimationFrameID = null;
@@ -11251,7 +11479,7 @@
<xsl:text> let [value, bytesize] = dvgetter(dv,i);
- <xsl:text> updates.set(index, value);
+ <xsl:text> dispatch_value(index, value); <xsl:text> i += bytesize;
@@ -11265,8 +11493,6 @@
- <xsl:text> apply_updates();
<xsl:text> // register for rendering on next frame, since there are updates
<xsl:text> } catch(err) {
@@ -12081,10 +12307,92 @@
+ <xsl:text>/* From https://jsfiddle.net/ibowankenobi/1mmh7rs6/6/ */ + <xsl:text>function getAbsoluteCTM(element){ + <xsl:text> var height = svg_root.height.baseVal.value, + <xsl:text> width = svg_root.width.baseVal.value, + <xsl:text> viewBoxRect = svg_root.viewBox.baseVal, + <xsl:text> vHeight = viewBoxRect.height, + <xsl:text> vWidth = viewBoxRect.width; + <xsl:text> if(!vWidth || !vHeight){ + <xsl:text> return element.getCTM(); + <xsl:text> var sH = height/vHeight, + <xsl:text> sW = width/vWidth, + <xsl:text> matrix = svg_root.createSVGMatrix(); + <xsl:text> matrix.a = sW; + <xsl:text> matrix.d = sH + <xsl:text> var realCTM = element.getCTM().multiply(matrix.inverse()); + <xsl:text> realCTM.e = realCTM.e/sW + viewBoxRect.x; + <xsl:text> realCTM.f = realCTM.f/sH + viewBoxRect.y; + <xsl:text> return realCTM; + <xsl:text>function apply_reference_frames(){ + <xsl:text> const matches = svg_root.querySelectorAll("g[svghmi_x_offset]"); + <xsl:text> matches.forEach((group) => { + <xsl:text> let [x,y] = ["x", "y"].map((axis) => Number(group.getAttribute("svghmi_"+axis+"_offset"))); + <xsl:text> let ctm = getAbsoluteCTM(group); + <xsl:text> // zero translation part of CTM + <xsl:text> // to only apply rotation/skewing to offset vector + <xsl:text> let invctm = ctm.inverse(); + <xsl:text> let vect = new DOMPoint(x, y); + <xsl:text> let newvect = vect.matrixTransform(invctm); + <xsl:text> let transform = svg_root.createSVGTransform(); + <xsl:text> transform.setTranslate(newvect.x, newvect.y); + <xsl:text> group.transform.baseVal.appendItem(transform); + <xsl:text> ["x", "y"].forEach((axis) => group.removeAttribute("svghmi_"+axis+"_offset")); <xsl:text>// Once connection established
<xsl:text>ws.onopen = function (evt) {
+ <xsl:text> apply_reference_frames(); <xsl:text> init_widgets();
--- 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 ))]""";
+ 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)"; --- 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);
// Called on requestAnimationFrame, modifies DOM
var requestAnimationFrameID = null;
@@ -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); throw new Error("Unknown index "+index);
// register for rendering on next frame, since there are updates
// 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, + matrix = svg_root.createSVGMatrix(); + var realCTM = element.getCTM().multiply(matrix.inverse()); + realCTM.e = realCTM.e/sW + viewBoxRect.x; + realCTM.f = realCTM.f/sH + viewBoxRect.y; +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 + 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(); --- 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 @@
+ target_page_is_current_page = false; + button_beeing_pressed = false;
- const name = this.args[0];
- /* 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) {
- (that.is_relative && that.indexes.length > 0) ?
- that.indexes[0] + that.offset : undefined;
- fading_page_switch(name, index);
+ svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + if(this.enable_state) { + (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); + 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 @@
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) {
@@ -98,7 +104,8 @@
const "jump_disability","$has_activity and $has_disability";
- | this.element.onclick = this.make_on_click();
+ | this.bound_onmouseup = this.onmouseup.bind(this); + | this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); --- 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];
if(pagevars == undefined){
new_index = next_available_index++;
- hmi_locals[pagename] = {[varname]:new_index}
+ hmi_locals[pagename] = {[varname]:new_index}; 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);