Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ jobs:
os:
- Ubuntu
kicad-version:
- "7.0"
- "8.0"
- "9.0"
- "10.0"

runs-on: ${{ matrix.os }}-latest
container:
Expand Down
18 changes: 18 additions & 0 deletions DATAFORMAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ pcbdata = {
// Describes all edge cut drawings including ones in footprints.
// See drawing structure description below.
"edges": [drawing1, drawing2, ...],
// Optional physical board region in board coordinates, using the same
// units as edges. Includes cutouts and boundary-crossing drill notches.
// Used for copper/silkscreen clipping, net hit testing and perimeter
// drawing, independently of pad visibility. Fabrication is not clipped.
// Omit this field when valid geometry is unavailable to preserve the
// original rendering. At least one of svgpath or polygons is required.
"board_clip": {
// SVG path 'd' string. Supports native arcs and curves; each contour
// should be closed with Z. Takes precedence over polygons if both exist.
"svgpath": svgpath,
// Closed contours; the last point connects back to the first.
// Even-odd filling applies to both representations: nested contours
// form cutouts, and separate outer contours form board islands.
"polygons": [
[[point1x, point1y], [point2x, point2y], ...],
...
],
},
"drawings": {
// Contains all drawings + reference + value texts on silkscreen
// layer grouped by front and back.
Expand Down
74 changes: 74 additions & 0 deletions InteractiveHtmlBom/ecad/kicad.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,79 @@ def parse_edges(self, pcb):
bbox.Normalize()
return edges, bbox

@staticmethod
def _polygon_operation(operation, *polygons):
# KiCad 8 and earlier require the polygon mode argument.
if hasattr(pcbnew.SHAPE_POLY_SET, "PM_STRICTLY_SIMPLE"):
operation(*polygons, pcbnew.SHAPE_POLY_SET.PM_STRICTLY_SIMPLE)
else:
operation(*polygons)

def _clipping_methods_available(self, obj, *names):
missing = [name for name in names
if not callable(getattr(obj, name, None))]
if missing:
self.logger.warn("Board clipping unavailable: missing API %s" %
", ".join(missing))
return not missing

def parse_board_clip(self):
"""Export the physical board perimeter for clipping and edge drawing."""
if not self._clipping_methods_available(
self.board, "GetBoardPolygonOutlines"):
return {}
if not self._clipping_methods_available(
pcbnew, "SHAPE_POLY_SET", "FromMM"):
return {}
try:
board = pcbnew.SHAPE_POLY_SET()
if not self._clipping_methods_available(
board, "OutlineCount", "Outline", "HoleCount", "Hole",
"Area", "BooleanIntersection", "BooleanSubtract",
"BooleanAdd"):
return {}
if KICAD_VERSION[0] >= 10:
valid = self.board.GetBoardPolygonOutlines(board, False)
else:
valid = self.board.GetBoardPolygonOutlines(board)
Comment thread
qu1ck marked this conversation as resolved.
Resolved
if not valid or board.OutlineCount() == 0:
self.logger.warn("Invalid board outline; copper clipping disabled")
return {}

# 0.01 mm keeps 0.3 mm drills smooth without the excessive vertex
# count of 0.001 mm. Work in KiCad's native integer coordinates.
error = pcbnew.FromMM(0.01)
edge_drills = pcbnew.SHAPE_POLY_SET()
for footprint in self.footprints:
for pad in footprint.Pads():
if not self._pad_is_through_hole(pad):
continue
if not self._clipping_methods_available(
pad, "TransformHoleToPolygon"):
return {}
drill = pcbnew.SHAPE_POLY_SET()
if not pad.TransformHoleToPolygon(drill, 0, error):
continue
inside = pcbnew.SHAPE_POLY_SET()
self._polygon_operation(
inside.BooleanIntersection, drill, board)
if inside.Area() <= 0:
continue
outside = pcbnew.SHAPE_POLY_SET()
self._polygon_operation(
outside.BooleanSubtract, drill, board)
if outside.Area() > 0:
self._polygon_operation(edge_drills.BooleanAdd, drill)

# Union first: overlapping holes must not cancel under even-odd fill.
if edge_drills.OutlineCount():
self._polygon_operation(board.BooleanSubtract, edge_drills)
return {"board_clip": {"polygons": self.parse_poly_set(board)}}
except (AttributeError, TypeError, RuntimeError) as e:
# Optional geometry must not prevent generation on older bindings.
self.logger.warn("Board clipping unavailable: %s" % e)
return {}

def parse_drawings_on_layers(self, drawings, f_layer, b_layer):
front = []
back = []
Expand Down Expand Up @@ -928,6 +1001,7 @@ def parse(self):
"bom": {},
"font_data": self.font_parser.get_parsed_font()
}
pcbdata.update(self.parse_board_clip())
if self.config.include_tracks:
pcbdata["tracks"] = self.parse_tracks(self.board.GetTracks())
if hasattr(self.board, "Zones"):
Expand Down
16 changes: 16 additions & 0 deletions InteractiveHtmlBom/ecad/schema/genericjsonpcbdata_v1.schema
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
"edges": {
"$ref": "#/definitions/DrawingArray"
},
"board_clip": {
"$ref": "#/definitions/ClippingRegion"
},
"drawings": {
"$ref": "#/definitions/LayerDrawings"
},
Expand Down Expand Up @@ -599,6 +602,19 @@
],
"title": "Zone"
},
"ClippingRegion": {
"description": "Board-coordinate contours, filled with the even-odd rule.",
"type": "object",
"additionalProperties": false,
"properties": {
"svgpath": { "type": "string" },
"polygons": { "$ref": "#/definitions/Polygons" }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add option to pass geometry as svgpath. For some CAD packages they might support native arcs in the outline and this is preferable format.

Make any of [svgpath, polygons] required similar to other structs containing svgpath.

},
"anyOf": [
{ "required": [ "svgpath" ] },
{ "required": [ "polygons" ] }
]
},
"Polygons": {
"type": "array",
"items": {
Expand Down
40 changes: 39 additions & 1 deletion InteractiveHtmlBom/web/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,12 @@ function drawPadHole(ctx, pad, padHoleColor) {
ctx.restore();
}

function clipBoard(ctx) {
if (pcbdata.board_clip) {
ctx.clip(getPolygonsPath(pcbdata.board_clip), "evenodd");
}
}

function drawFootprint(ctx, layer, scalefactor, footprint, colors, highlight, outline) {
if (highlight) {
// draw bounding box
Expand All @@ -341,6 +347,8 @@ function drawFootprint(ctx, layer, scalefactor, footprint, colors, highlight, ou
ctx.restore();
}
}
ctx.save();
clipBoard(ctx);
// draw drawings
for (var drawing of footprint.drawings) {
if (drawing.layer == layer) {
Expand All @@ -365,14 +373,32 @@ function drawFootprint(ctx, layer, scalefactor, footprint, colors, highlight, ou
drawPadHole(ctx, pad, colors.padHole);
}
}
ctx.restore();
}

function drawEdgeCuts(canvas, scalefactor) {
var ctx = canvas.getContext("2d");
var edgecolor = getComputedStyle(topmostdiv).getPropertyValue('--pcb-edge-color');
ctx.save();
if (pcbdata.board_clip) {
// The clipped perimeter includes the new edges along castellated drills.
// Use the thinnest geometric Edge.Cuts stroke: older KiCad boards may
// use thick edges for zone clearance. Keep at least one pixel at any zoom.
var width = pcbdata.edges.reduce((width, edge) =>
["segment", "arc", "circle", "curve", "rect", "polygon"].includes(edge.type)
? Math.min(width, edge.width || 0) : width, Infinity);
ctx.lineWidth = Math.max(Number.isFinite(width) ? width : 0, 1 / scalefactor);
ctx.strokeStyle = edgecolor;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke(getPolygonsPath(pcbdata.board_clip));
ctx.restore();
return;
}
for (var edge of pcbdata.edges) {
drawDrawing(ctx, scalefactor, edge, edgecolor);
}
ctx.restore();
}

function drawFootprints(canvas, layer, scalefactor, highlight) {
Expand Down Expand Up @@ -411,6 +437,10 @@ function drawFootprints(canvas, layer, scalefactor, highlight) {

function drawBgLayer(layername, canvas, layer, scalefactor, edgeColor, polygonColor, textColor) {
var ctx = canvas.getContext("2d");
ctx.save();
if (layername === "silkscreen") {
clipBoard(ctx);
}
for (var d of pcbdata.drawings[layername][layer]) {
if (["segment", "arc", "circle", "curve", "rect"].includes(d.type)) {
drawedge(ctx, scalefactor, d, edgeColor);
Expand All @@ -420,6 +450,7 @@ function drawBgLayer(layername, canvas, layer, scalefactor, edgeColor, polygonCo
drawText(ctx, d, textColor);
}
}
ctx.restore();
}

function drawTracks(canvas, layer, defaultColor, highlight) {
Expand Down Expand Up @@ -512,6 +543,9 @@ function clearCanvas(canvas, color = null) {
}

function drawNets(canvas, layer, highlight) {
var ctx = canvas.getContext("2d");
ctx.save();
clipBoard(ctx);
var style = getComputedStyle(topmostdiv);
if (settings.renderZones) {
var zoneColor = style.getPropertyValue(highlight ? '--zone-color-highlight' : '--zone-color');
Expand All @@ -524,7 +558,6 @@ function drawNets(canvas, layer, highlight) {
if (highlight && settings.renderPads) {
var padColor = style.getPropertyValue('--pad-color-highlight');
var padHoleColor = style.getPropertyValue('--pad-hole-color');
var ctx = canvas.getContext("2d");
for (var footprint of pcbdata.footprints) {
// draw pads
var padDrawn = false;
Expand All @@ -543,6 +576,7 @@ function drawNets(canvas, layer, highlight) {
}
}
}
ctx.restore();
}

function drawHighlightsOnLayer(canvasdict, clear = true) {
Expand Down Expand Up @@ -765,6 +799,10 @@ function pointWithinPad(x, y, pad) {
}

function netHitScan(layer, x, y) {
if (pcbdata.board_clip && !emptyContext2d.isPointInPath(
getPolygonsPath(pcbdata.board_clip), x, y, "evenodd")) {
return null;
}
// Check track segments
if (settings.renderTracks && pcbdata.tracks) {
for (var track of pcbdata.tracks[layer]) {
Expand Down
Loading
Loading