package main import ( "fmt" "image" "image/color" "math" ) // EnclosureConfig holds parameters for enclosure generation type EnclosureConfig struct { PCBThickness float64 // mm WallThickness float64 // mm WallHeight float64 // mm (height of walls above PCB) Clearance float64 // mm (gap between PCB and enclosure wall) DPI float64 OutlineBounds *Bounds // gerber coordinate bounds for drill mapping } // Default enclosure values const ( DefaultPCBThickness = 1.6 DefaultEncWallHeight = 10.0 DefaultEncWallThick = 1.5 DefaultClearance = 0.3 ) // EnclosureResult contains the generated meshes type EnclosureResult struct { EnclosureTriangles [][3]Point TrayTriangles [][3]Point } // SideCutout defines a cutout on a side wall face type SideCutout struct { Side int // 1-indexed side number (clockwise from top) X, Y float64 // Position on the face in mm (from left edge, from bottom) Width float64 // Width in mm Height float64 // Height in mm CornerRadius float64 // Corner radius in mm (0 for square) } // GenerateEnclosure creates enclosure + tray meshes from a board outline image and drill holes. // The enclosure walls conform to the actual board outline shape. // courtyardImg is optional — if provided, component courtyard regions are cut from the lid (flood-filled). // soldermaskImg is optional — if provided, soldermask pad openings are also cut from the lid. func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg EnclosureConfig, courtyardImg image.Image, soldermaskImg image.Image, sideCutouts []SideCutout) *EnclosureResult { pixelToMM := 25.4 / cfg.DPI bounds := outlineImg.Bounds() imgW := bounds.Max.X imgH := bounds.Max.Y // Use ComputeWallMask to get the board shape and wall around it // WallThickness for enclosure = clearance + wall thickness totalWallMM := cfg.Clearance + cfg.WallThickness fmt.Printf("Computing board shape (wall=%.1fmm)...\n", totalWallMM) wallMask, boardMask := ComputeWallMask(outlineImg, totalWallMM, pixelToMM) // Also compute a thinner mask for just the clearance zone (inner wall boundary) clearanceMask, _ := ComputeWallMask(outlineImg, cfg.Clearance, pixelToMM) // Determine the actual enclosure boundary = wall | board (expanded by clearance) // wallMask = pixels that are the wall // boardMask = pixels inside the board outline // clearanceMask = pixels in the clearance zone around the board // The enclosure walls are: wallMask pixels that are NOT in the clearance zone // Actually: wallMask gives us everything from board edge out to totalWall distance // clearanceMask gives us board edge out to clearance distance // Real wall = wallMask AND NOT clearanceMask AND NOT boardMask // Dimensions trayFloor := 1.0 // mm pcbT := cfg.PCBThickness totalH := cfg.WallHeight + pcbT + trayFloor // total enclosure height lidThick := cfg.WallThickness // lid thickness at top // Snap-fit dimensions snapHeight := 1.5 snapFromBottom := trayFloor + 0.3 // Tab dimensions tabW := 8.0 tabD := 6.0 tabH := 2.0 // ========================================== // ENCLOSURE (top shell — conforms to board shape) // ========================================== var encTris [][3]Point fmt.Println("Generating edge-cut conforming enclosure...") // Walls: scan through the image and create boxes for wall pixels // A pixel is "wall" if it's in wallMask but NOT in clearanceMask and NOT in boardMask // Actually simpler: wallMask already represents the OUTSIDE ring. // wallMask = pixels outside board but within thickness distance // boardMask = pixels inside the board // So wall pixels are: wallMask[i] && !boardMask[i] // But we also want to separate outer wall from inner clearance: // Outer wall = wallMask && !clearanceMask (the actual solid wall material) // Inner clearance = clearanceMask (air gap between wall and PCB) // For the enclosure walls, we want the OUTER wall portion only // Wall pixels = wallMask[i] && !clearanceMask[i] && !boardMask[i] // For the lid, we want to cover everything within the outer wall boundary // Lid pixels = wallMask[i] || boardMask[i] || clearanceMask[i] // (i.e., the entire footprint of the enclosure) size := imgW * imgH // Pre-compute board bounding box (needed for side cutout detection and removal tabs) minBX, minBY := imgW, imgH maxBX, maxBY := 0, 0 boardCenterX, boardCenterY := 0.0, 0.0 boardCount := 0 for y := 0; y < imgH; y++ { for x := 0; x < imgW; x++ { if boardMask[y*imgW+x] { boardCenterX += float64(x) boardCenterY += float64(y) boardCount++ if x < minBX { minBX = x } if x > maxBX { maxBX = x } if y < minBY { minBY = y } if y > maxBY { maxBY = y } } } } // Build wall-cutout mask from side cutouts // For each side cutout, determine which wall pixels to subtract wallCutoutMask := make([]bool, size) if len(sideCutouts) > 0 && cfg.OutlineBounds != nil { // Board bounding box in pixels for y := 0; y < imgH; y++ { for x := 0; x < imgW; x++ { idx := y*imgW + x if !(wallMask[idx] && !clearanceMask[idx] && !boardMask[idx]) { continue // not a wall pixel } // Determine which side this wall pixel belongs to // Find distance to each side of the board bounding box dTop := math.Abs(float64(y) - float64(minBY)) dBottom := math.Abs(float64(y) - float64(maxBY)) dLeft := math.Abs(float64(x) - float64(minBX)) dRight := math.Abs(float64(x) - float64(maxBX)) sideNum := 0 minDist := dTop sideNum = 1 // top if dRight < minDist { minDist = dRight sideNum = 2 // right } if dBottom < minDist { minDist = dBottom sideNum = 3 // bottom } if dLeft < minDist { sideNum = 4 // left } // Position along the side in mm var posAlongSide float64 var zPos float64 switch sideNum { case 1: // top — position = X distance from left board edge posAlongSide = float64(x-minBX) * pixelToMM zPos = 0 // all Z heights for walls case 2: // right — position = Y distance from top board edge posAlongSide = float64(y-minBY) * pixelToMM zPos = 0 case 3: // bottom — position = X distance from left board edge posAlongSide = float64(x-minBX) * pixelToMM zPos = 0 case 4: // left — position = Y distance from top board edge posAlongSide = float64(y-minBY) * pixelToMM zPos = 0 } _ = zPos // Check all cutouts for this side for _, c := range sideCutouts { if c.Side != sideNum { continue } // Check if this pixel's position falls within the cutout X range if posAlongSide >= c.X && posAlongSide <= c.X+c.Width { wallCutoutMask[idx] = true break } } } } fmt.Printf("Wall cutout mask: applied %d side cutouts\n", len(sideCutouts)) } // Generate walls using RLE for y := 0; y < imgH; y++ { runStart := -1 for x := 0; x <= imgW; x++ { isWallPixel := false if x < imgW { idx := y*imgW + x isWallPixel = wallMask[idx] && !clearanceMask[idx] && !boardMask[idx] } if isWallPixel { if runStart == -1 { runStart = x } } else { if runStart != -1 { bx := float64(runStart) * pixelToMM by := float64(y) * pixelToMM bw := float64(x-runStart) * pixelToMM bh := pixelToMM AddBox(&encTris, bx, by, bw, bh, totalH) runStart = -1 } } } } // Now subtract side cutout regions from the walls // For each cutout, we remove wall material in the Z range [cutout.Y, cutout.Y+cutout.H] // by NOT generating boxes in that region. Since we already generated full-height walls, // we rebuild wall columns where cutouts exist with gaps. if len(sideCutouts) > 0 { var cutoutEncTris [][3]Point for y := 0; y < imgH; y++ { runStart := -1 for x := 0; x <= imgW; x++ { isCutWall := false if x < imgW { idx := y*imgW + x isCutWall = wallCutoutMask[idx] } if isCutWall { if runStart == -1 { runStart = x } } else { if runStart != -1 { // This run of wall pixels has cutouts — find which cutout midX := (runStart + x) / 2 midIdx := y*imgW + midX _ = midIdx // Find the dominant side and cutout for this run dTop := math.Abs(float64(y) - float64(minBY)) dBottom := math.Abs(float64(y) - float64(maxBY)) dLeft := math.Abs(float64(midX) - float64(minBX)) dRight := math.Abs(float64(midX) - float64(maxBX)) sideNum := 1 minDist := dTop if dRight < minDist { minDist = dRight sideNum = 2 } if dBottom < minDist { minDist = dBottom sideNum = 3 } if dLeft < minDist { sideNum = 4 } bx := float64(runStart) * pixelToMM by2 := float64(y) * pixelToMM bw := float64(x-runStart) * pixelToMM bh := pixelToMM // Find the matching cutout for this side for _, c := range sideCutouts { if c.Side != sideNum { continue } // Wall below cutout: from 0 to cutout.Y if c.Y > 0.1 { addBoxAtZ(&cutoutEncTris, bx, by2, 0, bw, bh, c.Y) } // Wall above cutout: from cutout.Y+cutout.H to totalH cutTop := c.Y + c.Height if cutTop < totalH-0.1 { addBoxAtZ(&cutoutEncTris, bx, by2, cutTop, bw, bh, totalH-cutTop) } break } runStart = -1 } } } } // Replace full-height walls with cutout walls // First remove the original full-height boxes for cutout pixels // (They were already added above, so we need to rebuild) // Simpler approach: rebuild encTris without cutout regions, then add partial walls var newEncTris [][3]Point // Re-generate walls, skipping cutout pixels for y := 0; y < imgH; y++ { runStart := -1 for x := 0; x <= imgW; x++ { isWallPixel := false if x < imgW { idx := y*imgW + x isWallPixel = wallMask[idx] && !clearanceMask[idx] && !boardMask[idx] && !wallCutoutMask[idx] } if isWallPixel { if runStart == -1 { runStart = x } } else { if runStart != -1 { bx := float64(runStart) * pixelToMM by2 := float64(y) * pixelToMM bw := float64(x-runStart) * pixelToMM bb := pixelToMM AddBox(&newEncTris, bx, by2, bw, bb, totalH) runStart = -1 } } } } // Add the partial (cut) wall sections newEncTris = append(newEncTris, cutoutEncTris...) encTris = newEncTris } // Lid: cover the entire enclosure footprint at the top // Lid pixels = any pixel in wallMask OR clearanceMask OR boardMask // Subtract courtyard regions (component footprints) from the lid fmt.Println("Generating lid...") // Build courtyard cutout mask using flood-fill courtyardMask := buildCutoutMask(courtyardImg, imgW, imgH, true) // flood-fill closed outlines if courtyardImg != nil { cutoutCount := 0 for _, v := range courtyardMask { if v { cutoutCount++ } } fmt.Printf("Courtyard cutout (flood-filled): %d pixels\n", cutoutCount) } // Build soldermask cutout mask (direct pixel match, no flood-fill) soldermaskMask := buildCutoutMask(soldermaskImg, imgW, imgH, false) if soldermaskImg != nil { cutoutCount := 0 for _, v := range soldermaskMask { if v { cutoutCount++ } } fmt.Printf("Soldermask cutout: %d pixels\n", cutoutCount) } // Combined cutout: union of courtyard (filled) and soldermask combinedCutout := make([]bool, size) for i := 0; i < size; i++ { combinedCutout[i] = courtyardMask[i] || soldermaskMask[i] } for y := 0; y < imgH; y++ { runStart := -1 for x := 0; x <= imgW; x++ { isLidPixel := false if x < imgW { idx := y*imgW + x inFootprint := wallMask[idx] || clearanceMask[idx] || boardMask[idx] // Cut lid where combined cutout exists inside the board area isCutout := combinedCutout[idx] && boardMask[idx] isLidPixel = inFootprint && !isCutout } if isLidPixel { if runStart == -1 { runStart = x } } else { if runStart != -1 { bx := float64(runStart) * pixelToMM by := float64(y) * pixelToMM bw := float64(x-runStart) * pixelToMM bh := pixelToMM addBoxAtZ(&encTris, bx, by, totalH-lidThick, bw, bh, lidThick) runStart = -1 } } } } // Mounting pegs from NPTH holes: cylinders going from lid downward pegMask := make([]bool, size) // true = peg/socket at this pixel (exclude from tray floor) if cfg.OutlineBounds != nil { mountingHoles := 0 for _, h := range drillHoles { if h.Type != DrillTypeMounting { continue } mountingHoles++ // Convert drill mm coordinates to pixel coordinates px := (h.X - cfg.OutlineBounds.MinX) * cfg.DPI / 25.4 py := (h.Y - cfg.OutlineBounds.MinY) * cfg.DPI / 25.4 // Peg radius slightly smaller than hole for press fit pegRadiusMM := (h.Diameter / 2) - 0.15 pegRadiusPx := pegRadiusMM * cfg.DPI / 25.4 // Socket radius slightly larger for easy insertion socketRadiusPx := (h.Diameter/2 + 0.1) * cfg.DPI / 25.4 // Peg height: from bottom (z=0) up to lid pegH := totalH - lidThick // Scan a bounding box around the hole rInt := int(socketRadiusPx) + 2 cx, cy := int(px), int(py) for dy := -rInt; dy <= rInt; dy++ { for dx := -rInt; dx <= rInt; dx++ { ix, iy := cx+dx, cy+dy if ix < 0 || ix >= imgW || iy < 0 || iy >= imgH { continue } dist := math.Sqrt(float64(dx*dx + dy*dy)) // Peg cylinder (in enclosure, from z=0 up to lid) if dist <= pegRadiusPx { bx := float64(ix) * pixelToMM by := float64(iy) * pixelToMM addBoxAtZ(&encTris, bx, by, 0, pixelToMM, pixelToMM, pegH) } // Socket mask (for tray floor removal) if dist <= socketRadiusPx { pegMask[iy*imgW+ix] = true } } } } if mountingHoles > 0 { fmt.Printf("Generated %d mounting pegs\n", mountingHoles) } } // Snap ledges: on the inside of the walls (at the clearance boundary) // These are pixels that are in clearanceMask but adjacent to wallMask fmt.Println("Generating snap ledges...") for y := 1; y < imgH-1; y++ { runStart := -1 for x := 0; x <= imgW; x++ { isSnapPixel := false if x > 0 && x < imgW-1 { idx := y*imgW + x if clearanceMask[idx] && !boardMask[idx] { // Check if adjacent to a wall pixel hasAdjacentWall := false for _, d := range [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} { ni := (y+d[1])*imgW + (x + d[0]) if ni >= 0 && ni < size { if wallMask[ni] && !clearanceMask[ni] && !boardMask[ni] { hasAdjacentWall = true break } } } isSnapPixel = hasAdjacentWall } } if isSnapPixel { if runStart == -1 { runStart = x } } else { if runStart != -1 { bx := float64(runStart) * pixelToMM by := float64(y) * pixelToMM bw := float64(x-runStart) * pixelToMM bh := pixelToMM addBoxAtZ(&encTris, bx, by, snapFromBottom, bw, bh, snapHeight) runStart = -1 } } } } // ========================================== // TRAY (bottom — conforms to board shape) // ========================================== var trayTris [][3]Point fmt.Println("Generating edge-cut conforming tray...") // Tray floor: covers the cavity area (clearanceMask + boardMask) for y := 0; y < imgH; y++ { runStart := -1 for x := 0; x <= imgW; x++ { isTrayPixel := false if x < imgW { idx := y*imgW + x isTrayPixel = (clearanceMask[idx] || boardMask[idx]) && !pegMask[idx] } if isTrayPixel { if runStart == -1 { runStart = x } } else { if runStart != -1 { bx := float64(runStart) * pixelToMM by := float64(y) * pixelToMM bw := float64(x-runStart) * pixelToMM bh := pixelToMM AddBox(&trayTris, bx, by, bw, bh, trayFloor) runStart = -1 } } } } // PCB support rim: inner edge of clearance zone (adjacent to board) fmt.Println("Generating PCB support rim...") rimH := pcbT * 0.5 for y := 1; y < imgH-1; y++ { runStart := -1 for x := 0; x <= imgW; x++ { isRimPixel := false if x > 0 && x < imgW-1 { idx := y*imgW + x if clearanceMask[idx] && !boardMask[idx] { // Adjacent to board? for _, d := range [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} { ni := (y+d[1])*imgW + (x + d[0]) if ni >= 0 && ni < size && boardMask[ni] { isRimPixel = true break } } } } if isRimPixel { if runStart == -1 { runStart = x } } else { if runStart != -1 { bx := float64(runStart) * pixelToMM by := float64(y) * pixelToMM bw := float64(x-runStart) * pixelToMM bh := pixelToMM addBoxAtZ(&trayTris, bx, by, trayFloor, bw, bh, rimH) runStart = -1 } } } } // Snap bumps: on the outer edge of the tray (adjacent to wall) fmt.Println("Generating snap bumps...") snapBumpH := snapHeight + 0.3 for y := 1; y < imgH-1; y++ { runStart := -1 for x := 0; x <= imgW; x++ { isBumpPixel := false if x > 0 && x < imgW-1 { idx := y*imgW + x if clearanceMask[idx] && !boardMask[idx] { // Adjacent to wall? for _, d := range [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} { ni := (y+d[1])*imgW + (x + d[0]) if ni >= 0 && ni < size { if wallMask[ni] && !clearanceMask[ni] && !boardMask[ni] { isBumpPixel = true break } } } } } if isBumpPixel { if runStart == -1 { runStart = x } } else { if runStart != -1 { bx := float64(runStart) * pixelToMM by := float64(y) * pixelToMM bw := float64(x-runStart) * pixelToMM bh := pixelToMM addBoxAtZ(&trayTris, bx, by, snapFromBottom-0.1, bw, bh, snapBumpH) runStart = -1 } } } } // Removal tabs: INTERNAL — thin finger grips that sit inside the tray cavity // User can push on them from below to pop the tray out fmt.Println("Adding internal removal tabs...") // (uses pre-computed board bounding box: minBX, minBY, maxBX, maxBY, boardCenterY) if boardCount > 0 { boardCenterY /= float64(boardCount) tabCenterY := boardCenterY * pixelToMM // Internal tabs: inside the clearance zone, extending inward // Left tab — just inside the left wall leftInner := float64(minBX)*pixelToMM - cfg.Clearance addBoxAtZ(&trayTris, leftInner, tabCenterY-tabW/2, trayFloor, tabD, tabW, tabH) // Right tab — just inside the right wall rightInner := float64(maxBX)*pixelToMM + cfg.Clearance - tabD addBoxAtZ(&trayTris, rightInner, tabCenterY-tabW/2, trayFloor, tabD, tabW, tabH) } // Embossed lip: a raised ridge around the tray perimeter, 0.5mm thick // This lip mates against a recess in the enclosure for a tight snap fit fmt.Println("Adding embossed lip...") lipH := pcbT + 1.5 // extends above tray floor to grip the enclosure opening lipThickPx := int(math.Ceil(0.5 * cfg.DPI / 25.4)) // 0.5mm in pixels if lipThickPx < 1 { lipThickPx = 1 } // Build lip mask from the adjacency rule, then dilate inward by lipThickPx lipCoreMask := make([]bool, size) for y := 1; y < imgH-1; y++ { for x := 1; x < imgW-1; x++ { idx := y*imgW + x if clearanceMask[idx] && !boardMask[idx] { for _, d := range [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} { ni := (y+d[1])*imgW + (x + d[0]) if ni >= 0 && ni < size { if wallMask[ni] && !clearanceMask[ni] && !boardMask[ni] { lipCoreMask[idx] = true break } } } } } } // Dilate lip mask inward by lipThickPx pixels lipMask := make([]bool, size) copy(lipMask, lipCoreMask) for iter := 1; iter < lipThickPx; iter++ { nextMask := make([]bool, size) copy(nextMask, lipMask) for y := 1; y < imgH-1; y++ { for x := 1; x < imgW-1; x++ { idx := y*imgW + x if lipMask[idx] { continue // already in lip } if !clearanceMask[idx] || boardMask[idx] { continue // must be in clearance zone } // Adjacent to existing lip pixel? for _, d := range [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} { ni := (y+d[1])*imgW + (x + d[0]) if ni >= 0 && ni < size && lipMask[ni] { nextMask[idx] = true break } } } } lipMask = nextMask } // Generate lip boxes for y := 0; y < imgH; y++ { runStart := -1 for x := 0; x <= imgW; x++ { isLipPx := false if x < imgW { isLipPx = lipMask[y*imgW+x] } if isLipPx { if runStart == -1 { runStart = x } } else { if runStart != -1 { bx := float64(runStart) * pixelToMM by := float64(y) * pixelToMM bw := float64(x-runStart) * pixelToMM bh := pixelToMM addBoxAtZ(&trayTris, bx, by, trayFloor, bw, bh, lipH) runStart = -1 } } } } // Add matching recess in enclosure for the lip (0.25mm deep groove) // Recess sits at the inner face of the enclosure wall, where the lip enters fmt.Println("Adding lip recess in enclosure...") recessDepth := 0.25 recessH := lipH + 0.5 // slightly taller than lip for easy entry for y := 0; y < imgH; y++ { runStart := -1 for x := 0; x <= imgW; x++ { // Recess = wall pixels adjacent to the lip (inner face of wall) isRecess := false if x > 0 && x < imgW-1 { idx := y*imgW + x if wallMask[idx] && !clearanceMask[idx] && !boardMask[idx] { for _, d := range [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} { ni := (y+d[1])*imgW + (x + d[0]) if ni >= 0 && ni < size && lipMask[ni] { isRecess = true break } } } } if isRecess { if runStart == -1 { runStart = x } } else { if runStart != -1 { // Subtract recess from enclosure wall by NOT generating here // Instead, generate wall with gap at recess height bx := float64(runStart) * pixelToMM by := float64(y) * pixelToMM bw := float64(x-runStart) * pixelToMM bh := pixelToMM // Wall below recess if trayFloor > 0.05 { addBoxAtZ(&encTris, bx, by, 0, bw, bh, trayFloor) } // Thinner wall in recess zone (subtract recessDepth from thickness) // This is handled by just not filling the recess area _ = recessDepth // Wall above recess addBoxAtZ(&encTris, bx, by, trayFloor+recessH, bw, bh, totalH-(trayFloor+recessH)) runStart = -1 } } } } fmt.Printf("Enclosure: %d triangles, Tray: %d triangles\n", len(encTris), len(trayTris)) _ = math.Pi // keep math import for Phase 2 cylindrical pegs return &EnclosureResult{ EnclosureTriangles: encTris, TrayTriangles: trayTris, } } // addBoxAtZ creates a box at a specific Z offset func addBoxAtZ(triangles *[][3]Point, x, y, z, w, h, zHeight float64) { x0, y0 := x, y x1, y1 := x+w, y+h z0, z1 := z, z+zHeight p000 := Point{x0, y0, z0} p100 := Point{x1, y0, z0} p110 := Point{x1, y1, z0} p010 := Point{x0, y1, z0} p001 := Point{x0, y0, z1} p101 := Point{x1, y0, z1} p111 := Point{x1, y1, z1} p011 := Point{x0, y1, z1} addQuad := func(a, b, c, d Point) { *triangles = append(*triangles, [3]Point{a, b, c}) *triangles = append(*triangles, [3]Point{c, d, a}) } addQuad(p000, p010, p110, p100) // Bottom addQuad(p101, p111, p011, p001) // Top addQuad(p000, p100, p101, p001) // Front addQuad(p100, p110, p111, p101) // Right addQuad(p110, p010, p011, p111) // Back addQuad(p010, p000, p001, p011) // Left } // buildCutoutMask creates a boolean mask from an image. // If floodFill is true, it flood-fills from the edges to find closed regions. func buildCutoutMask(img image.Image, w, h int, floodFill bool) []bool { size := w * h mask := make([]bool, size) if img == nil { return mask } // First: build raw pixel mask from the image bounds := img.Bounds() rawPixels := make([]bool, size) for y := 0; y < h && y < bounds.Max.Y; y++ { for x := 0; x < w && x < bounds.Max.X; x++ { r, g, b, _ := img.At(x+bounds.Min.X, y+bounds.Min.Y).RGBA() gray := color.GrayModel.Convert(color.NRGBA{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8), 255}).(color.Gray) if gray.Y > 128 { rawPixels[y*w+x] = true } } } if !floodFill { // Direct mode: raw pixels are the mask return rawPixels } // Flood-fill mode: fill from edges to find exterior, invert to get interiors // Exterior = everything reachable from edges without crossing a white pixel exterior := floodFillExterior(rawPixels, w, h) // Interior = NOT exterior AND NOT raw pixel (the outline itself) // Actually, interior = NOT exterior (includes both outline pixels and filled regions) for i := 0; i < size; i++ { mask[i] = !exterior[i] } return mask } // floodFillExterior marks all pixels reachable from the image edges // without crossing a white (true) pixel as exterior func floodFillExterior(pixels []bool, w, h int) []bool { size := w * h exterior := make([]bool, size) // BFS queue starting from all edge pixels that are not white queue := make([]int, 0, w*2+h*2) for x := 0; x < w; x++ { // Top edge if !pixels[x] { exterior[x] = true queue = append(queue, x) } // Bottom edge idx := (h-1)*w + x if !pixels[idx] { exterior[idx] = true queue = append(queue, idx) } } for y := 0; y < h; y++ { // Left edge idx := y * w if !pixels[idx] { exterior[idx] = true queue = append(queue, idx) } // Right edge idx = y*w + (w - 1) if !pixels[idx] { exterior[idx] = true queue = append(queue, idx) } } // BFS for len(queue) > 0 { cur := queue[0] queue = queue[1:] x := cur % w y := cur / w for _, d := range [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} { nx, ny := x+d[0], y+d[1] if nx >= 0 && nx < w && ny >= 0 && ny < h { ni := ny*w + nx if !exterior[ni] && !pixels[ni] { exterior[ni] = true queue = append(queue, ni) } } } } return exterior }