SVGHMI: unlink clones (i.e. deep copy elements refered by svg:use) inside widget. svghmi
authorEdouard Tisserant
Wed, 04 Mar 2020 16:46:03 +0100
branchsvghmi
changeset 2854 c7d5f46cc306
parent 2853 6d39beb19f38
child 2855 525211a54b14
SVGHMI: unlink clones (i.e. deep copy elements refered by svg:use) inside widget.

Designer can use Inkscape clones to duplicate similar assets accross widget. Those clones are unliked so that elements can be independently transformed by individual widgets.
svghmi/gen_index_xhtml.xslt
svghmi/gen_index_xhtml.ysl2
--- a/svghmi/gen_index_xhtml.xslt	Wed Mar 04 09:31:53 2020 +0100
+++ b/svghmi/gen_index_xhtml.xslt	Wed Mar 04 16:46:03 2020 +0100
@@ -1,5 +1,5 @@
 <?xml version="1.0"?>
-<xsl:stylesheet xmlns:func="http://exslt.org/functions" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:str="http://exslt.org/strings" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:exsl="http://exslt.org/common" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ns="beremiz" xmlns:cc="http://creativecommons.org/ns#" xmlns:regexp="http://exslt.org/regular-expressions" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:dc="http://purl.org/dc/elements/1.1/" extension-element-prefixes="ns func" version="1.0" exclude-result-prefixes="ns str regexp exsl func">
+<xsl:stylesheet xmlns:func="http://exslt.org/functions" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:str="http://exslt.org/strings" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:exsl="http://exslt.org/common" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ns="beremiz" xmlns:cc="http://creativecommons.org/ns#" xmlns:regexp="http://exslt.org/regular-expressions" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:dc="http://purl.org/dc/elements/1.1/" extension-element-prefixes="ns func exsl regexp str dyn" version="1.0" exclude-result-prefixes="ns str regexp exsl func dyn">
   <xsl:output method="xml" cdata-section-elements="xhtml:script"/>
   <xsl:variable name="geometry" select="ns:GetSVGGeometry()"/>
   <xsl:variable name="hmitree" select="ns:GetHMITree()"/>
@@ -100,9 +100,9 @@
       </xsl:otherwise>
     </xsl:choose>
   </func:function>
-  <xsl:variable name="groups" select="/svg:svg | //svg:g"/>
   <func:function name="func:overlapping_geometry">
     <xsl:param name="elt"/>
+    <xsl:variable name="groups" select="/svg:svg | //svg:g"/>
     <xsl:variable name="g" select="$geometry[@Id = $elt/@id]"/>
     <xsl:variable name="candidates" select="$geometry[@Id != $elt/@id]"/>
     <func:result select="$candidates[(@Id = $groups/@id and (func:intersect($g, .) = 9)) or &#10;                              (not(@Id = $groups/@id) and (func:intersect($g, .) &gt; 0 ))]"/>
@@ -130,7 +130,7 @@
   <func:function name="func:sumarized_elements">
     <xsl:param name="elements"/>
     <xsl:variable name="short_list" select="$elements[not(ancestor::*/@id = $elements/@id)]"/>
-    <xsl:variable name="filled_groups" select="$short_list/parent::svg:*[&#10;            not(descendant::*[&#10;                not(self::svg:g) and &#10;                not(@id = $discardable_elements/@id) and&#10;                not(@id = $short_list/descendant-or-self::*[not(self::svg:g)]/@id)&#10;            ])]"/>
+    <xsl:variable name="filled_groups" select="$short_list/parent::svg:*[&#10;            not(descendant::*[&#10;                not(self::svg:g) and&#10;                not(@id = $discardable_elements/@id) and&#10;                not(@id = $short_list/descendant-or-self::*[not(self::svg:g)]/@id)&#10;            ])]"/>
     <xsl:variable name="groups_to_add" select="$filled_groups[not(ancestor::*/@id = $filled_groups/@id)]"/>
     <func:result select="$groups_to_add | $short_list[not(ancestor::svg:g/@id = $filled_groups/@id)]"/>
   </func:function>
@@ -227,6 +227,84 @@
       <xsl:text>All units must be set to "px" in Inkscape's document properties</xsl:text>
     </xsl:message>
   </xsl:template>
+  <xsl:variable name="to_unlink" select="$hmi_elements[not(@id = $hmi_pages)]//svg:use"/>
+  <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:use">
+    <xsl:choose>
+      <xsl:when test="@id = $to_unlink/@id">
+        <xsl:call-template name="unlink_clone"/>
+      </xsl:when>
+      <xsl:otherwise>
+        <xsl:copy>
+          <xsl:apply-templates mode="inline_svg" select="@* | node()"/>
+        </xsl:copy>
+      </xsl:otherwise>
+    </xsl:choose>
+  </xsl:template>
+  <xsl:variable name="_excluded_use_attrs">
+    <name>
+      <xsl:text>href</xsl:text>
+    </name>
+    <name>
+      <xsl:text>width</xsl:text>
+    </name>
+    <name>
+      <xsl:text>height</xsl:text>
+    </name>
+    <name>
+      <xsl:text>x</xsl:text>
+    </name>
+    <name>
+      <xsl:text>y</xsl:text>
+    </name>
+  </xsl:variable>
+  <xsl:variable name="excluded_use_attrs" select="exsl:node-set($_excluded_use_attrs)"/>
+  <xsl:template xmlns="http://www.w3.org/2000/svg" name="unlink_clone">
+    <g>
+      <xsl:for-each select="@*[not(local-name() = $excluded_use_attrs/name)]">
+        <xsl:attribute name="{name()}">
+          <xsl:value-of select="."/>
+        </xsl:attribute>
+      </xsl:for-each>
+      <xsl:variable name="targetid" select="substring-after(@xlink:href,'#')"/>
+      <xsl:apply-templates mode="unlink_clone" select="//svg:*[@id = $targetid]">
+        <xsl:with-param name="seed" select="@id"/>
+      </xsl:apply-templates>
+    </g>
+  </xsl:template>
+  <xsl:template xmlns="http://www.w3.org/2000/svg" mode="unlink_clone" match="@id">
+    <xsl:param name="seed"/>
+    <xsl:attribute name="id">
+      <xsl:value-of select="$seed"/>
+      <xsl:text>_</xsl:text>
+      <xsl:value-of select="."/>
+    </xsl:attribute>
+  </xsl:template>
+  <xsl:template xmlns="http://www.w3.org/2000/svg" mode="unlink_clone" match="@*">
+    <xsl:copy/>
+  </xsl:template>
+  <xsl:template xmlns="http://www.w3.org/2000/svg" mode="unlink_clone" match="svg:*">
+    <xsl:param name="seed"/>
+    <xsl:choose>
+      <xsl:when test="@id = $hmi_elements/@id">
+        <use>
+          <xsl:attribute name="xlink:href">
+            <xsl:value-of select="concat('#',@id)"/>
+          </xsl:attribute>
+        </use>
+      </xsl:when>
+      <xsl:otherwise>
+        <xsl:copy>
+          <xsl:apply-templates mode="unlink_clone" select="@* | node()">
+            <xsl:with-param name="seed" select="$seed"/>
+          </xsl:apply-templates>
+        </xsl:copy>
+      </xsl:otherwise>
+    </xsl:choose>
+  </xsl:template>
+  <xsl:variable name="result_svg">
+    <xsl:apply-templates mode="inline_svg" select="/"/>
+  </xsl:variable>
+  <xsl:variable name="result_svg_ns" select="exsl:node-set($result_svg)"/>
   <xsl:template match="/">
     <xsl:comment>
       <xsl:text>Made with SVGHMI. https://beremiz.org</xsl:text>
@@ -258,16 +336,21 @@
 </xsl:text>
       </xsl:for-each>
     </xsl:comment>
+    <xsl:comment>
+      <xsl:text>Unlinked :
+</xsl:text>
+      <xsl:for-each select="$to_unlink">
+        <xsl:value-of select="@id"/>
+        <xsl:text>
+</xsl:text>
+      </xsl:for-each>
+    </xsl:comment>
     <html xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/1999/xhtml">
       <head/>
       <body style="margin:0;overflow:hidden;">
-        <xsl:variable name="_result_svg">
-          <xsl:apply-templates mode="inline_svg" select="svg:svg"/>
-        </xsl:variable>
-        <xsl:copy-of select="$_result_svg"/>
-        <xsl:variable name="result_svg" select="exsl:node-set($_result_svg)"/>
+        <xsl:copy-of select="$result_svg"/>
         <script>
-          <xsl:apply-templates mode="scripts" select="svg:svg"/>
+          <xsl:call-template name="scripts"/>
         </script>
       </body>
     </html>
@@ -323,7 +406,7 @@
     </xsl:variable>
     <func:result select="exsl:node-set($ast)"/>
   </func:function>
-  <xsl:template mode="scripts" match="svg:svg">
+  <xsl:template name="scripts">
     <xsl:text>//(function(){
 </xsl:text>
     <xsl:text>
@@ -386,16 +469,16 @@
           </xsl:otherwise>
         </xsl:choose>
       </xsl:for-each>
-      <xsl:text>        ],
-</xsl:text>
-      <xsl:text>        element: id("</xsl:text>
+      <xsl:text>    ],
+</xsl:text>
+      <xsl:text>    element: id("</xsl:text>
       <xsl:value-of select="@id"/>
       <xsl:text>"),
 </xsl:text>
       <xsl:apply-templates mode="widget_defs" select="$widget">
         <xsl:with-param name="hmi_element" select="."/>
       </xsl:apply-templates>
-      <xsl:text>    }</xsl:text>
+      <xsl:text>  }</xsl:text>
       <xsl:if test="position()!=last()">
         <xsl:text>,</xsl:text>
       </xsl:if>
@@ -1142,12 +1225,10 @@
     <xsl:value-of select="$indent"/>
     <xsl:text> </xsl:text>
     <xsl:value-of select="local-name()"/>
-    <xsl:text> </xsl:text>
     <xsl:for-each select="@*">
       <xsl:value-of select="local-name()"/>
       <xsl:text>=</xsl:text>
       <xsl:value-of select="."/>
-      <xsl:text> </xsl:text>
     </xsl:for-each>
     <xsl:text>
 </xsl:text>
@@ -1164,7 +1245,7 @@
     <xsl:variable name="widget_type" select="@type"/>
     <xsl:for-each select="str:split($labels)">
       <xsl:variable name="name" select="."/>
-      <xsl:variable name="elt_id" select="$hmi_element//*[@inkscape:label=$name][1]/@id"/>
+      <xsl:variable name="elt_id" select="$result_svg_ns//*[@id = $hmi_element/@id]//*[@inkscape:label=$name][1]/@id"/>
       <xsl:choose>
         <xsl:when test="not($elt_id)">
           <xsl:if test="$mandatory='yes'">
--- a/svghmi/gen_index_xhtml.ysl2	Wed Mar 04 09:31:53 2020 +0100
+++ b/svghmi/gen_index_xhtml.ysl2	Wed Mar 04 16:46:03 2020 +0100
@@ -15,6 +15,7 @@
 };
 
 in xsl decl svgtmpl(match, xmlns="http://www.w3.org/2000/svg") alias template;
+in xsl decl svgfunc(name, xmlns="http://www.w3.org/2000/svg") alias template;
 
 istylesheet
             /* From Inkscape */
@@ -29,8 +30,8 @@
 
             /* Our namespace to invoke python code */
             xmlns:ns="beremiz"
-            extension-element-prefixes="ns func"
-            exclude-result-prefixes="ns str regexp exsl func" {
+            extension-element-prefixes="ns func exsl regexp str dyn"
+            exclude-result-prefixes="ns str regexp exsl func dyn" {
 
     /* This retrieves geometry obtained through "inkscape -S"
      * already parsed by python and presented as a list of
@@ -330,48 +331,89 @@
         error > All units must be set to "px" in Inkscape's document properties
     }
 
-    
-    //// Commented out before implementing runtime DOM remove/append on page switch - would have side effect
-    ////
-    //// /* clone unlinkink until widget for better perf with webkit */
-    //// svgtmpl "svg:use", mode="inline_svg" 
-    //// {
-    ////     g{
-    ////         attrib "style" > «@style»
-    ////         attrib "transform" > «@transform»
-    ////         /* keep same id and label in case it is a widget */
-    ////         //attrib "inkscape:label","@inkscape:label";
-    ////         attrib "id" > «@id»
-    ////         const "targetid","substring-after(@xlink:href,'#')";
-    ////         apply "//svg:*[@id = $targetid]", mode="unlink_clone";
-    ////     }
-    //// }
-    //// svgtmpl "@*", mode="unlink_clone" xsl:copy;
-    //// svgtmpl "svg:*", mode="unlink_clone" {
-    ////     choose {
-    ////         when "@id = $hmi_elements/@id" {
-    ////             use{
-    ////                 attrib "xlink:href" > «concat('#',@id)»
-    ////             }
-    ////         }
-    ////         otherwise {
-    ////             xsl:copy apply "@* | node()", mode="unlink_clone";
-    ////         }
-    ////     }
-    //// }
-
-    // template "svg:use/@style", mode="inline_svg"{
-    //     attrib "style" > all:initial;
-    //     //«.»
-    // }
-
-    // template "svg:*[concat('#',@id) = //svg:use/@xlink:href]/@style", mode="inline_svg"{
-    //     attrib "style" > all:unset;
-    //     //«.»
-    // }
+
+    //////////////// Clone Unlinking
+
+    // svg:use (inkscape's clones) inside a widgets are
+    // replaced by real elements they refer in order to :
+    //  - allow finding "needle" element in "meter" widget,
+    //    even if "needle" is in a group refered by a svg use.
+    //  - if "needle" is visible through a svg:use for
+    //    each instance of the widget, then needle would show
+    //    the same position in all instances
+    //
+    // For now, clone unlinkink applies to descendants of all widget except HMI:Page
+    // TODO: narrow application of clone unlinking to active elements,
+    //       while keeping static decoration cloned
+    const "to_unlink", "$hmi_elements[not(@id = $hmi_pages)]//svg:use";
+    svgtmpl "svg:use", mode="inline_svg"
+    {
+        choose {
+            when "@id = $to_unlink/@id"
+                call "unlink_clone";
+            otherwise
+                xsl:copy apply "@* | node()", mode="inline_svg";
+        }
+    }
+
+    // to unlink a clone, an group containing a copy of target element is created
+    // that way, style and transforms can be preserved
+    const "_excluded_use_attrs" {
+        name > href
+        name > width
+        name > height
+        name > x
+        name > y
+    }
+    const "excluded_use_attrs","exsl:node-set($_excluded_use_attrs)";
+
+    svgfunc "unlink_clone"{
+        g{
+            // include non excluded attributes
+            foreach "@*[not(local-name() = $excluded_use_attrs/name)]"
+                attrib "{name()}" > «.»
+
+            const "targetid","substring-after(@xlink:href,'#')";
+            apply "//svg:*[@id = $targetid]", mode="unlink_clone"{
+                with "seed","@id";
+            }
+        }
+    }
+
+    // clone unlinking is really similar to deep-copy
+    // all nodes are sytematically copied
+    svgtmpl "@id", mode="unlink_clone" {
+        param "seed";
+        attrib "id" > «$seed»_«.»
+    }
+
+    svgtmpl "@*", mode="unlink_clone" xsl:copy;
+
+    // copying widgets would have unwanted effect
+    // instead widget is refered through a svg:use.
+    svgtmpl "svg:*", mode="unlink_clone" {
+        param "seed";
+        choose {
+            // node recursive copy ends when finding a widget
+            when "@id = $hmi_elements/@id" {
+                // place a clone instead of copying
+                use{
+                    attrib "xlink:href" > «concat('#',@id)»
+                }
+            }
+            otherwise {
+                xsl:copy apply "@* | node()", mode="unlink_clone" {
+                    with "seed","$seed";
+                }
+            }
+        }
+    }
 
     /*const "mark" > =HMI=\n*/
 
+    const "result_svg" apply "/", mode="inline_svg";
+    const "result_svg_ns", "exsl:node-set($result_svg)";
+
     /* copy root node and add geometry as comment for a test */
     template "/" {
         comment > Made with SVGHMI. https://beremiz.org
@@ -397,13 +439,19 @@
                 | «@id»
             }
         }
+        comment {
+            | Unlinked :
+            foreach "$to_unlink"{
+                | «@id»
+            }
+        }
         /**/
         html xmlns="http://www.w3.org/1999/xhtml"
              xmlns:svg="http://www.w3.org/2000/svg"
              xmlns:xlink="http://www.w3.org/1999/xlink" {
             head;
             body style="margin:0;overflow:hidden;" {
-                apply "svg:svg", mode="inline_svg";
+                copy "$result_svg";
                 script{
                     call "scripts";
                 }
@@ -622,7 +670,7 @@
         const "widget_type","@type";
         foreach "str:split($labels)" {
             const "name",".";
-            const "elt_id","$hmi_element//*[@inkscape:label=$name][1]/@id";
+            const "elt_id","$result_svg_ns//*[@id = $hmi_element/@id]//*[@inkscape:label=$name][1]/@id";
             choose {
                 when "not($elt_id)" {
                     if "$mandatory='yes'" {