Skip to content

Commit e0d413c

Browse files
committed
Optimize pad rendering and hit detection
1 parent 22e8bbc commit e0d413c

File tree

1 file changed

+88
-81
lines changed

1 file changed

+88
-81
lines changed

InteractiveHtmlBom/web/render.js

Lines changed: 88 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ var renderValues = true;
88
var renderDnpOutline = false;
99
var renderTracks = true;
1010
var renderZones = true;
11+
var emptyContext2d = document.createElement("canvas").getContext("2d");
1112

1213
function deg2rad(deg) {
1314
return deg * Math.PI / 180;
@@ -122,74 +123,79 @@ function drawedge(ctx, scalefactor, edge, color) {
122123
}
123124
}
124125

125-
function drawChamferedRect(ctx, color, size, radius, chamfpos, chamfratio, ctxmethod) {
126+
function getChamferedRectPath(size, radius, chamfpos, chamfratio) {
126127
// chamfpos is a bitmask, left = 1, right = 2, bottom left = 4, bottom right = 8
127-
ctx.beginPath();
128-
ctx.strokeStyle = color;
128+
var path = new Path2D();
129129
var width = size[0];
130130
var height = size[1];
131131
var x = width * -0.5;
132132
var y = height * -0.5;
133133
var chamfOffset = Math.min(width, height) * chamfratio;
134-
ctx.moveTo(x, 0);
134+
path.moveTo(x, 0);
135135
if (chamfpos & 4) {
136-
ctx.lineTo(x, y + height - chamfOffset);
137-
ctx.lineTo(x + chamfOffset, y + height);
138-
ctx.lineTo(0, y + height);
136+
path.lineTo(x, y + height - chamfOffset);
137+
path.lineTo(x + chamfOffset, y + height);
138+
path.lineTo(0, y + height);
139139
} else {
140-
ctx.arcTo(x, y + height, x + width, y + height, radius);
140+
path.arcTo(x, y + height, x + width, y + height, radius);
141141
}
142142
if (chamfpos & 8) {
143-
ctx.lineTo(x + width - chamfOffset, y + height);
144-
ctx.lineTo(x + width, y + height - chamfOffset);
145-
ctx.lineTo(x + width, 0);
143+
path.lineTo(x + width - chamfOffset, y + height);
144+
path.lineTo(x + width, y + height - chamfOffset);
145+
path.lineTo(x + width, 0);
146146
} else {
147-
ctx.arcTo(x + width, y + height, x + width, y, radius);
147+
path.arcTo(x + width, y + height, x + width, y, radius);
148148
}
149149
if (chamfpos & 2) {
150-
ctx.lineTo(x + width, y + chamfOffset);
151-
ctx.lineTo(x + width - chamfOffset, y);
152-
ctx.lineTo(0, y);
150+
path.lineTo(x + width, y + chamfOffset);
151+
path.lineTo(x + width - chamfOffset, y);
152+
path.lineTo(0, y);
153153
} else {
154-
ctx.arcTo(x + width, y, x, y, radius);
154+
path.arcTo(x + width, y, x, y, radius);
155155
}
156156
if (chamfpos & 1) {
157-
ctx.lineTo(x + chamfOffset, y);
158-
ctx.lineTo(x, y + chamfOffset);
159-
ctx.lineTo(x, 0);
157+
path.lineTo(x + chamfOffset, y);
158+
path.lineTo(x, y + chamfOffset);
159+
path.lineTo(x, 0);
160160
} else {
161-
ctx.arcTo(x, y, x, y + height, radius);
161+
path.arcTo(x, y, x, y + height, radius);
162162
}
163-
ctx.closePath();
164-
ctxmethod();
163+
path.closePath();
164+
return path;
165165
}
166166

167-
function drawOblong(ctx, color, size, ctxmethod) {
168-
drawChamferedRect(ctx, color, size, Math.min(size[0], size[1]) / 2, 0, 0, ctxmethod);
167+
function getOblongPath(size) {
168+
return getChamferedRectPath(size, Math.min(size[0], size[1]) / 2, 0, 0);
169169
}
170170

171-
function drawPolygons(ctx, color, polygons, ctxmethod) {
172-
ctx.fillStyle = color;
171+
function getPolygonsPath(polygons) {
172+
var combinedPath = new Path2D();
173173
for (var polygon of polygons) {
174-
ctx.beginPath();
174+
var path = new Path2D();
175175
for (var vertex of polygon) {
176-
ctx.lineTo(...vertex)
176+
path.lineTo(...vertex)
177177
}
178-
ctx.closePath();
179-
ctxmethod();
178+
path.closePath();
179+
combinedPath.addPath(path);
180180
}
181+
return combinedPath;
181182
}
182183

183184
function drawPolygonShape(ctx, shape, color) {
184185
ctx.save();
185-
if (shape.svgpath) {
186-
ctx.fillStyle = color;
187-
ctx.fill(new Path2D(shape.svgpath));
188-
} else {
186+
ctx.fillStyle = color;
187+
if (!shape.path2d) {
188+
if (shape.svgpath) {
189+
shape.path2d = new Path2D(shape.svgpath);
190+
} else {
191+
shape.path2d = getPolygonsPath(shape.polygons);
192+
}
193+
}
194+
if (!shape.svgpath) {
189195
ctx.translate(...shape.pos);
190196
ctx.rotate(deg2rad(-shape.angle));
191-
drawPolygons(ctx, color, shape.polygons, ctx.fill.bind(ctx));
192197
}
198+
ctx.fill(shape.path2d);
193199
ctx.restore();
194200
}
195201

@@ -203,11 +209,32 @@ function drawDrawing(ctx, layer, scalefactor, drawing, color) {
203209
}
204210
}
205211

206-
function drawCircle(ctx, radius, ctxmethod) {
207-
ctx.beginPath();
208-
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
209-
ctx.closePath();
210-
ctxmethod();
212+
function getCirclePath(radius) {
213+
var path = new Path2D();
214+
path.arc(0, 0, radius, 0, 2 * Math.PI);
215+
path.closePath();
216+
return path;
217+
}
218+
219+
function getCachedPadPath(pad) {
220+
if (!pad.path2d) {
221+
// if path2d is not set, build one and cache it on pad object
222+
if (pad.shape == "rect") {
223+
pad.path2d = new Path2D();
224+
pad.path2d.rect(...pad.size.map(c => -c * 0.5), ...pad.size);
225+
} else if (pad.shape == "oval") {
226+
pad.path2d = getOblongPath(pad.size);
227+
} else if (pad.shape == "circle") {
228+
pad.path2d = getCirclePath(pad.size[0] / 2);
229+
} else if (pad.shape == "roundrect") {
230+
pad.path2d = getChamferedRectPath(pad.size, pad.radius, 0, 0);
231+
} else if (pad.shape == "chamfrect") {
232+
pad.path2d = getChamferedRectPath(pad.size, pad.radius, pad.chamfpos, pad.chamfratio)
233+
} else if (pad.shape == "custom") {
234+
pad.path2d = getPolygonsPath(pad.polygons);
235+
}
236+
}
237+
return pad.path2d;
211238
}
212239

213240
function drawPad(ctx, pad, color, outline, hole) {
@@ -219,32 +246,18 @@ function drawPad(ctx, pad, color, outline, hole) {
219246
}
220247
ctx.fillStyle = color;
221248
ctx.strokeStyle = color;
222-
var ctxmethod = outline ? ctx.stroke.bind(ctx) : ctx.fill.bind(ctx);
223-
if (pad.shape == "rect") {
224-
var rect = [...pad.size.map(c => -c * 0.5), ...pad.size];
225-
if (outline) {
226-
ctx.strokeRect(...rect);
227-
} else {
228-
ctx.fillRect(...rect);
229-
}
230-
} else if (pad.shape == "oval") {
231-
drawOblong(ctx, color, pad.size, ctxmethod);
232-
} else if (pad.shape == "circle") {
233-
drawCircle(ctx, pad.size[0] / 2, ctxmethod);
234-
} else if (pad.shape == "roundrect") {
235-
drawChamferedRect(ctx, color, pad.size, pad.radius, 0, 0, ctxmethod);
236-
} else if (pad.shape == "chamfrect") {
237-
drawChamferedRect(ctx, color, pad.size, pad.radius, pad.chamfpos, pad.chamfratio, ctxmethod)
238-
} else if (pad.shape == "custom") {
239-
drawPolygons(ctx, color, pad.polygons, ctxmethod);
249+
var path = getCachedPadPath(pad);
250+
if (outline) {
251+
ctx.stroke(path);
252+
} else {
253+
ctx.fill(path);
240254
}
241255
if (pad.type == "th" && hole) {
242-
ctxmethod = ctx.fill.bind(ctx);
243256
ctx.fillStyle = "#CCCCCC";
244257
if (pad.drillshape == "oblong") {
245-
drawOblong(ctx, "#CCCCCC", pad.drillsize, ctxmethod);
258+
ctx.fill(getOblongPath(pad.drillsize));
246259
} else {
247-
drawCircle(ctx, pad.drillsize[0] / 2, ctxmethod);
260+
ctx.fill(getCirclePath(pad.drillsize[0] / 2));
248261
}
249262
}
250263
ctx.restore();
@@ -345,12 +358,16 @@ function drawTracks(canvas, layer, color, highlight) {
345358
function drawZones(canvas, layer, color, highlight) {
346359
ctx = canvas.getContext("2d");
347360
ctx.strokeStyle = color;
361+
ctx.fillStyle = color;
348362
ctx.lineJoin = "round";
349363
for(var zone of pcbdata.zones[layer]) {
364+
if (!zone.path2d) {
365+
zone.path2d = getPolygonsPath(zone.polygons);
366+
}
350367
if (highlight && highlightedNet != zone.net) continue;
351368
ctx.lineWidth = zone.width;
352-
drawPolygons(ctx, color, zone.polygons, ctx.stroke.bind(ctx));
353-
drawPolygons(ctx, color, zone.polygons, ctx.fill.bind(ctx));
369+
ctx.fill(zone.path2d);
370+
ctx.stroke(zone.path2d);
354371
}
355372
}
356373

@@ -563,35 +580,25 @@ function pointWithinPad(x, y, pad) {
563580
v[0] -= pad.offset[0];
564581
v[1] -= pad.offset[1];
565582
}
566-
if (["rect", "roundrect", "chamfrect"].includes(pad.shape)) {
567-
return -pad.size[0] / 2 <= v[0] && v[0] <= pad.size[0] / 2 &&
568-
-pad.size[1] / 2 <= v[1] && v[1] <= pad.size[1] / 2;
569-
} else if (pad.shape == "oval") {
570-
var d = (pad.size[0] - pad.size[1]) / 2;
571-
if (d > 0) {
572-
return pointWithinDistanceToSegment(v[0], v[1], d, 0, -d, 0, pad.size[1] / 2);
573-
} else {
574-
return pointWithinDistanceToSegment(v[0], v[1], 0, d, 0, -d, pad.size[0] / 2);
575-
}
576-
} else if (pad.shape == "circle") {
577-
return v[0] * v[0] + v[1] * v[1] <= pad.size[0] * pad.size[0] / 4;
578-
}
583+
return emptyContext2d.isPointInPath(getCachedPadPath(pad), ...v);
579584
}
580585

581586
function netHitScan(layer, x, y) {
582587
// Check track segments
583-
if ("tracks" in pcbdata) {
588+
if (renderTracks && pcbdata.tracks) {
584589
for(var track of pcbdata.tracks[layer]) {
585590
if (pointWithinDistanceToSegment(x, y, ...track.start, ...track.end, track.width / 2)) {
586591
return track.net;
587592
}
588593
}
589594
}
590595
// Check pads
591-
for (var mod of pcbdata.modules) {
592-
for(var pad of mod.pads) {
593-
if (pad.layers.includes(layer) && pointWithinPad(x, y, pad)) {
594-
return pad.net;
596+
if (renderPads) {
597+
for (var mod of pcbdata.modules) {
598+
for(var pad of mod.pads) {
599+
if (pad.layers.includes(layer) && pointWithinPad(x, y, pad)) {
600+
return pad.net;
601+
}
595602
}
596603
}
597604
}

0 commit comments

Comments
 (0)