Skip to content

Commit 9946140

Browse files
Tato Leviczclaude
andcommitted
refactor: troca Canvas por Shape (GPU) para node rendering
- Shape com PathSvg e OddEvenFill para cutouts vetoriais - Escala perfeitamente com zoom (sem pixelizacao) - antialiasing: true + smooth: true para bordas suaves - Remove Canvas (CPU bitmap) por Shape (GPU tessellation) - Cutouts, header, borda com gaps todos via SVG paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 96bc71b commit 9946140

1 file changed

Lines changed: 151 additions & 131 deletions

File tree

resources/qml/Node.qml

Lines changed: 151 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import QtQuick 2.15
2+
import QtQuick.Shapes 1.15
23

34
Item {
45
id: root
@@ -29,7 +30,7 @@ Item {
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

Comments
 (0)