Skip to content

feat: add knockout silkscreen text support#705

Open
MekonMAC wants to merge 1 commit intotscircuit:mainfrom
MekonMAC:feat/knockout-silkscreen-text
Open

feat: add knockout silkscreen text support#705
MekonMAC wants to merge 1 commit intotscircuit:mainfrom
MekonMAC:feat/knockout-silkscreen-text

Conversation

@MekonMAC
Copy link

/claim #770

Summary

Adds knockout silkscreen text rendering support for 3d-viewer, completing the final item on the issue #770 checklist.

Changes

  • src/utils/silkscreen-texture.ts: Added knockout rendering logic

    • Checks is_knockout and knockout_padding properties from circuit-json
    • Draws filled rectangle as background
    • Uses canvas globalCompositeOperation='destination-out' to cut text from rectangle
    • Supports custom padding (default 0.2mm on all sides)
    • Supports rotation and anchor alignment
    • Works on both top and bottom layers
  • stories/KnockoutSilkscreenText.stories.tsx: Added Storybook story demonstrating:

    • Regular (non-knockout) text
    • Knockout text with default padding
    • Knockout text with custom padding
    • Knockout text with rotation
    • Knockout text on bottom layer

Context

Previous PRs merged:

  • circuit-to-svg#506 — SVG knockout rendering (merged Feb 6)
  • core#1886 — map knockout props (merged Feb 7)
  • circuit-to-canvas#159 — canvas knockout rendering (merged Feb 8)

This PR completes the 3d-viewer implementation, covering the last remaining item in the issue checklist.

Testing

  • TypeScript compiles without errors
  • Added Storybook story for visual verification

Closes tscircuit/tscircuit#770

@vercel
Copy link

vercel bot commented Feb 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
3d-viewer Ready Ready Preview, Comment Feb 22, 2026 10:48am

Request Review

…approach

- Implement knockout silkscreen text rendering in silkscreen-drawing.ts
- Use destination-out composite operation to cut out knockout text areas
- Add SilkscreenTextKnockout story to demonstrate the feature
- Follows same pattern as copper text knockout from PR tscircuit#705

Closes tscircuit/tscircuit#770
Comment on lines +89 to +125
// Handle knockout silkscreen text using destination-out approach
const knockoutTexts = elements.filter(
(element): element is PcbSilkscreenText =>
element.type === "pcb_silkscreen_text" && element.is_knockout === true,
)
if (knockoutTexts.length === 0) return

const maskCanvas = document.createElement("canvas")
maskCanvas.width = ctx.canvas.width
maskCanvas.height = ctx.canvas.height
const maskCtx = maskCanvas.getContext("2d")
if (!maskCtx) return

const knockoutCutoutDrawer = new CircuitToCanvasDrawer(maskCtx)
knockoutCutoutDrawer.configure({
colorOverrides: {
silkscreen: {
top: "rgb(255,255,255)",
bottom: "rgb(255,255,255)",
},
},
})
setDrawerBounds(knockoutCutoutDrawer, bounds)
knockoutCutoutDrawer.drawElements(
knockoutTexts.map((text) => ({
...text,
is_knockout: false,
})),
{
layers: [renderLayer],
},
)

ctx.save()
ctx.globalCompositeOperation = "destination-out"
ctx.drawImage(maskCanvas, 0, 0)
ctx.restore()
Copy link
Contributor

Choose a reason for hiding this comment

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

The implementation does not handle the knockout_padding property that is described in the PR description and used in the test story. The description claims "Supports custom padding (default 0.2mm on all sides)" and the story file defines knockout_padding objects (lines 56-61 in the story), but the code never reads or applies these padding values.

Additionally, the description claims the code "Draws filled rectangle as background" before cutting out text, but the implementation only performs a destination-out operation on the mask canvas without drawing any background rectangles. This approach will cut the text out of whatever is already rendered on that layer, rather than creating a filled rectangle with knockout text as described.

To fix this, the code should:

// For each knockout text, draw a background rectangle first
for (const text of knockoutTexts) {
  // Calculate text bounds with padding
  const padding = text.knockout_padding || { 
    left: 0.2, right: 0.2, top: 0.2, bottom: 0.2 
  }
  // Draw filled rectangle with padding
  // Then cut text from it using destination-out
}
Suggested change
// Handle knockout silkscreen text using destination-out approach
const knockoutTexts = elements.filter(
(element): element is PcbSilkscreenText =>
element.type === "pcb_silkscreen_text" && element.is_knockout === true,
)
if (knockoutTexts.length === 0) return
const maskCanvas = document.createElement("canvas")
maskCanvas.width = ctx.canvas.width
maskCanvas.height = ctx.canvas.height
const maskCtx = maskCanvas.getContext("2d")
if (!maskCtx) return
const knockoutCutoutDrawer = new CircuitToCanvasDrawer(maskCtx)
knockoutCutoutDrawer.configure({
colorOverrides: {
silkscreen: {
top: "rgb(255,255,255)",
bottom: "rgb(255,255,255)",
},
},
})
setDrawerBounds(knockoutCutoutDrawer, bounds)
knockoutCutoutDrawer.drawElements(
knockoutTexts.map((text) => ({
...text,
is_knockout: false,
})),
{
layers: [renderLayer],
},
)
ctx.save()
ctx.globalCompositeOperation = "destination-out"
ctx.drawImage(maskCanvas, 0, 0)
ctx.restore()
// Handle knockout silkscreen text using destination-out approach
const knockoutTexts = elements.filter(
(element): element is PcbSilkscreenText =>
element.type === "pcb_silkscreen_text" && element.is_knockout === true,
)
if (knockoutTexts.length === 0) return
for (const text of knockoutTexts) {
// Get padding values with defaults
const padding = text.knockout_padding || {
left: 0.2, right: 0.2, top: 0.2, bottom: 0.2
}
// Create mask canvas for this text
const maskCanvas = document.createElement("canvas")
maskCanvas.width = ctx.canvas.width
maskCanvas.height = ctx.canvas.height
const maskCtx = maskCanvas.getContext("2d")
if (!maskCtx) continue
// Calculate text bounds (simplified - would need proper text measurement)
const textBounds = {
x: text.x - padding.left,
y: text.y - padding.top,
width: (text.text?.length || 0) * (text.size || 1) * 0.6 + padding.left + padding.right,
height: (text.size || 1) + padding.top + padding.bottom
}
// Draw filled rectangle background
maskCtx.fillStyle = "rgb(255,255,255)"
maskCtx.fillRect(textBounds.x, textBounds.y, textBounds.width, textBounds.height)
// Cut out the text using destination-out
const textCutoutDrawer = new CircuitToCanvasDrawer(maskCtx)
textCutoutDrawer.configure({
colorOverrides: {
silkscreen: {
top: "rgb(255,255,255)",
bottom: "rgb(255,255,255)",
},
},
})
setDrawerBounds(textCutoutDrawer, bounds)
maskCtx.save()
maskCtx.globalCompositeOperation = "destination-out"
textCutoutDrawer.drawElements(
[{
...text,
is_knockout: false,
}],
{
layers: [renderLayer],
},
)
maskCtx.restore()
// Apply the mask to the main canvas
ctx.drawImage(maskCanvas, 0, 0)
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support "knockout" silkscreen text

1 participant