11import QtQuick 2.15
2+ import QtQuick.Shapes 1.15
23
34Item {
45 id: root
2930 property color portColumnColor: style ? style .darkenColor (nodeColor, 0.10 ) : nodeColor
3031
3132 property real headerHeight: style ? style .nodeHeaderHeight : 35
32- property real portSz: style ? style .portSize : 15
33+ property real portSz: style ? style .portSize : 10
3334 property real portSpacing: style ? style .nodePortSpacing : 10
3435 property real nodeRadius: style ? style .nodeRadius : 5
3536
@@ -45,150 +46,169 @@ Item {
4546
4647 Component .onCompleted : completed = true
4748
48- // Trigger canvas repaint on relevant changes
49- onNodeColorChanged: nodeCanvas .requestPaint ()
50- onInPortsChanged: nodeCanvas .requestPaint ()
51- onOutPortsChanged: nodeCanvas .requestPaint ()
52- onWidthChanged: nodeCanvas .requestPaint ()
53- onHeightChanged: nodeCanvas .requestPaint ()
54- onSelectedChanged: nodeCanvas .requestPaint ()
49+ // Helper: build SVG path string for a rounded rect
50+ function svgRoundRect (x , y , w , h , r ) {
51+ return " M " + (x+ r) + " " + y
52+ + " L " + (x+ w- r) + " " + y
53+ + " A " + r + " " + r + " 0 0 1 " + (x+ w) + " " + (y+ r)
54+ + " L " + (x+ w) + " " + (y+ h- r)
55+ + " A " + r + " " + r + " 0 0 1 " + (x+ w- r) + " " + (y+ h)
56+ + " L " + (x+ r) + " " + (y+ h)
57+ + " A " + r + " " + r + " 0 0 1 " + x + " " + (y+ h- r)
58+ + " L " + x + " " + (y+ r)
59+ + " A " + r + " " + r + " 0 0 1 " + (x+ r) + " " + y
60+ + " Z"
61+ }
62+
63+ // Helper: SVG circle path (for cutouts)
64+ function svgCircle (cx , cy , r ) {
65+ return " M " + (cx+ r) + " " + cy
66+ + " A " + r + " " + r + " 0 1 0 " + (cx- r) + " " + cy
67+ + " A " + r + " " + r + " 0 1 0 " + (cx+ r) + " " + cy + " Z"
68+ }
69+
70+ // Build the full SVG path for background with cutouts
71+ function buildCutoutPath () {
72+ var w = root .width
73+ var h = root .height
74+ var r = nodeRadius
75+ var cutR = portSz * 0.75
76+
77+ var path = svgRoundRect (0 , 0 , w, h, r)
78+
79+ for (var i = 0 ; i < inPorts; i++ ) {
80+ var iy = headerHeight + 5 + i * (portSz + portSpacing) + portSz / 2
81+ path += svgCircle (0 , iy, cutR)
82+ }
83+ for (var j = 0 ; j < outPorts; j++ ) {
84+ var oy = headerHeight + 5 + j * (portSz + portSpacing) + portSz / 2
85+ path += svgCircle (w, oy, cutR)
86+ }
87+ return path
88+ }
89+
90+ // Build border path with gaps at port positions
91+ function buildBorderPath () {
92+ var w = root .width
93+ var h = root .height
94+ var r = nodeRadius
95+ var hh = headerHeight
96+ var cutR = portSz * 0.75
97+
98+ // Top edge
99+ var path = " M " + r + " 0"
100+ + " L " + (w- r) + " 0"
101+ + " A " + r + " " + r + " 0 0 1 " + w + " " + r
102+
103+ // Right edge with gaps
104+ var lastRY = r
105+ for (var ro = 0 ; ro < outPorts; ro++ ) {
106+ var roy = hh + 5 + ro * (portSz + portSpacing) + portSz / 2
107+ path += " L " + w + " " + (roy - cutR)
108+ path += " M " + w + " " + (roy + cutR)
109+ lastRY = roy + cutR
110+ }
111+
112+ // Continue right to bottom
113+ path += " L " + w + " " + (h- r)
114+ + " A " + r + " " + r + " 0 0 1 " + (w- r) + " " + h
115+
116+ // Bottom edge
117+ path += " L " + r + " " + h
118+ + " A " + r + " " + r + " 0 0 1 0 " + (h- r)
119+
120+ // Left edge with gaps (bottom to top)
121+ for (var ri = inPorts - 1 ; ri >= 0 ; ri-- ) {
122+ var riy = hh + 5 + ri * (portSz + portSpacing) + portSz / 2
123+ path += " L 0 " + (riy + cutR)
124+ path += " M 0 " + (riy - cutR)
125+ }
126+
127+ // Continue left to top
128+ path += " L 0 " + r
129+ + " A " + r + " " + r + " 0 0 1 " + r + " 0"
130+
131+ return path
132+ }
55133
56134 // =================================================================
57- // Canvas: draws body with real transparent cutouts at port positions
135+ // Shape: GPU-rendered body with transparent cutouts
58136 // =================================================================
59- Canvas {
60- id: nodeCanvas
137+ Shape {
61138 anchors .fill : parent
139+ antialiasing: true
140+ smooth: true
141+
142+ // Port column background with cutouts (evenodd makes holes)
143+ ShapePath {
144+ fillColor: root .portColumnColor
145+ strokeColor: " transparent"
146+ fillRule: ShapePath .OddEvenFill
147+ PathSvg { path: buildCutoutPath () }
148+ }
149+ }
62150
63- onPaint: {
64- var ctx = getContext (" 2d" )
65- ctx .reset ()
66- ctx .clearRect (0 , 0 , width, height)
67-
68- var w = root .width
69- var h = root .height
70- var r = nodeRadius
71- var hh = headerHeight
72- var cutR = portSz * 0.75
73- var colW = portSz + 4 // port column width
74-
75- // --- Background with port cutouts (evenodd) ---
76- ctx .beginPath ()
77-
78- // Outer rounded rectangle
79- ctx .moveTo (r, 0 )
80- ctx .lineTo (w - r, 0 )
81- ctx .arcTo (w, 0 , w, r, r)
82- ctx .lineTo (w, h - r)
83- ctx .arcTo (w, h, w - r, h, r)
84- ctx .lineTo (r, h)
85- ctx .arcTo (0 , h, 0 , h - r, r)
86- ctx .lineTo (0 , r)
87- ctx .arcTo (0 , 0 , r, 0 , r)
88- ctx .closePath ()
89-
90- // Input port cutouts
91- for (var i = 0 ; i < inPorts; i++ ) {
92- var iy = hh + 5 + i * (portSz + portSpacing) + portSz / 2
93- ctx .moveTo (cutR, iy)
94- ctx .arc (0 , iy, cutR, 0 , 2 * Math .PI , false )
95- }
96-
97- // Output port cutouts
98- for (var j = 0 ; j < outPorts; j++ ) {
99- var oy = hh + 5 + j * (portSz + portSpacing) + portSz / 2
100- ctx .moveTo (w + cutR, oy)
101- ctx .arc (w, oy, cutR, 0 , 2 * Math .PI , false )
102- }
103-
104- // Fill port column color (darker, behind everything)
105- ctx .fillStyle = root .portColumnColor .toString ()
106- ctx .fill (" evenodd" )
107-
108- // --- Body center (main color, between port columns) ---
109- ctx .beginPath ()
110- var bodyLeft = inPorts > 0 ? colW : 0
111- var bodyRight = outPorts > 0 ? w - colW : w
112- ctx .rect (bodyLeft, hh, bodyRight - bodyLeft, h - hh)
113- ctx .fillStyle = root .bodyColor .toString ()
114- ctx .fill ()
115-
116- // --- Header fill ---
117- ctx .beginPath ()
118- ctx .moveTo (r, 0 )
119- ctx .lineTo (w - r, 0 )
120- ctx .arcTo (w, 0 , w, r, r)
121- ctx .lineTo (w, hh)
122- ctx .lineTo (0 , hh)
123- ctx .lineTo (0 , r)
124- ctx .arcTo (0 , 0 , r, 0 , r)
125- ctx .closePath ()
126- ctx .fillStyle = root .headerColor .toString ()
127- ctx .fill ()
128-
129- // --- Divider line ---
130- ctx .beginPath ()
131- ctx .moveTo (0 , hh)
132- ctx .lineTo (w, hh)
133- ctx .strokeStyle = " rgba(0,0,0,0.3)"
134- ctx .lineWidth = 1
135- ctx .stroke ()
136-
137- // --- Node border (with same cutouts) ---
138- ctx .beginPath ()
139-
140- // Top edge
141- ctx .moveTo (r, 0 )
142- ctx .lineTo (w - r, 0 )
143- ctx .arcTo (w, 0 , w, r, r)
144-
145- // Right edge with output port gaps
146- if (outPorts > 0 ) {
147- for (var ro = 0 ; ro < outPorts; ro++ ) {
148- var roy = hh + 5 + ro * (portSz + portSpacing) + portSz / 2
149- // Line to top of cutout
150- ctx .lineTo (w, roy - cutR)
151- // Arc around the cutout (go around the outside)
152- ctx .moveTo (w, roy + cutR)
153- }
154- }
151+ // Body center (between port columns)
152+ Rectangle {
153+ x: inPorts > 0 ? portSz + 4 : 0
154+ y: headerHeight
155+ width: root .width - (inPorts > 0 ? portSz + 4 : 0 ) - (outPorts > 0 ? portSz + 4 : 0 )
156+ height: root .height - headerHeight
157+ color: bodyColor
158+ }
155159
156- // Continue right edge to bottom
157- ctx .lineTo (w, h - r)
158- ctx .arcTo (w, h, w - r, h, r)
159-
160- // Bottom edge
161- ctx .lineTo (r, h)
162- ctx .arcTo (0 , h, 0 , h - r, r)
163-
164- // Left edge with input port gaps
165- if (inPorts > 0 ) {
166- // Go from bottom up
167- var lastY = h - r
168- for (var ri = inPorts - 1 ; ri >= 0 ; ri-- ) {
169- var riy = hh + 5 + ri * (portSz + portSpacing) + portSz / 2
170- ctx .lineTo (0 , riy + cutR)
171- ctx .moveTo (0 , riy - cutR)
160+ // Header
161+ Shape {
162+ width: root .width
163+ height: headerHeight
164+ antialiasing: true
165+ smooth: true
166+
167+ ShapePath {
168+ fillColor: root .headerColor
169+ strokeColor: " transparent"
170+ PathSvg {
171+ path: {
172+ var w = root .width
173+ var r = nodeRadius
174+ var hh = headerHeight
175+ return " M " + r + " 0"
176+ + " L " + (w- r) + " 0"
177+ + " A " + r + " " + r + " 0 0 1 " + w + " " + r
178+ + " L " + w + " " + hh
179+ + " L 0 " + hh
180+ + " L 0 " + r
181+ + " A " + r + " " + r + " 0 0 1 " + r + " 0 Z"
172182 }
173183 }
184+ }
185+ }
174186
175- // Continue left edge to top
176- ctx .lineTo (0 , r)
177- ctx .arcTo (0 , 0 , r, 0 , r)
187+ // Divider line
188+ Rectangle {
189+ x: 0
190+ y: headerHeight
191+ width: root .width
192+ height: 1
193+ color: Qt .rgba (0 , 0 , 0 , 0.3 )
194+ }
178195
179- if (root .selected ) {
180- ctx .strokeStyle = style ? style .nodeSelectedBorder .toString () : " #4a9eff"
181- ctx .lineWidth = 2
182- } else {
183- ctx .strokeStyle = " rgba(0,0,0,0.5)"
184- ctx .lineWidth = 0.5
185- }
186- ctx .stroke ()
196+ // Border with gaps
197+ Shape {
198+ anchors .fill : parent
199+ antialiasing: true
200+ smooth: true
201+
202+ ShapePath {
203+ fillColor: " transparent"
204+ strokeColor: root .selected ? (style ? style .nodeSelectedBorder : " #4a9eff" ) : Qt .rgba (0 , 0 , 0 , 0.5 )
205+ strokeWidth: root .selected ? 2 : 0.5
206+ PathSvg { path: buildBorderPath () }
187207 }
188208 }
189209
190210 // =================================================================
191- // Header icon + caption (on top of canvas)
211+ // Header icon + caption
192212 // =================================================================
193213 Image {
194214 id: iconImg
0 commit comments