From 04ed30af3d5ce5a4c501a19467bcd883cee3ec58 Mon Sep 17 00:00:00 2001 From: LGLabGreg Date: Sat, 9 May 2026 08:47:21 +0100 Subject: [PATCH 1/5] feat: data modules circuit-board --- .changeset/silver-traces-turn.md | 5 + apps/docs/public/llms-full.txt | 2 +- .../src/app/data-modules-settings/page.tsx | 1 + .../docs/src/components/demo/data-modules.tsx | 1 + .../src/components/data-modules.test.tsx | 56 ++++++++++- .../src/components/data-modules.tsx | 57 ++++++++++- packages/react-qr-code/src/constants.ts | 2 + packages/react-qr-code/src/types/lib.ts | 1 + .../src/utils/data-modules.test.ts | 94 ++++++++++++++++++- .../react-qr-code/src/utils/data-modules.ts | 51 ++++++++++ 10 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 .changeset/silver-traces-turn.md diff --git a/.changeset/silver-traces-turn.md b/.changeset/silver-traces-turn.md new file mode 100644 index 0000000..370c298 --- /dev/null +++ b/.changeset/silver-traces-turn.md @@ -0,0 +1,5 @@ +--- +'@lglab/react-qr-code': minor +--- + +Add circuit-board data module style. diff --git a/apps/docs/public/llms-full.txt b/apps/docs/public/llms-full.txt index 03abc2d..666cead 100644 --- a/apps/docs/public/llms-full.txt +++ b/apps/docs/public/llms-full.txt @@ -39,7 +39,7 @@ The main component exported by the library. | `randomSize` | `boolean` | `false` | If true, modules will have slightly varied sizes. | **Available Styles (`DataModulesStyle`):** -`'square'`, `'square-sm'`, `'pinched-square'`, `'rounded'`, `'leaf'`, `'vertical-line'`, `'horizontal-line'`, `'circle'`, `'diamond'`, `'star'`, `'heart'`, `'hashtag'` +`'square'`, `'square-sm'`, `'pinched-square'`, `'rounded'`, `'leaf'`, `'vertical-line'`, `'horizontal-line'`, `'circuit-board'`, `'circle'`, `'diamond'`, `'star'`, `'heart'`, `'hashtag'` ### FinderPatternOuterSettings diff --git a/apps/docs/src/app/data-modules-settings/page.tsx b/apps/docs/src/app/data-modules-settings/page.tsx index 4965e93..b2a59aa 100644 --- a/apps/docs/src/app/data-modules-settings/page.tsx +++ b/apps/docs/src/app/data-modules-settings/page.tsx @@ -31,6 +31,7 @@ const props: Prop[] = [ 'leaf', 'vertical-line', 'horizontal-line', + 'circuit-board', 'circle', 'diamond', 'star', diff --git a/apps/docs/src/components/demo/data-modules.tsx b/apps/docs/src/components/demo/data-modules.tsx index 6693d57..728b001 100644 --- a/apps/docs/src/components/demo/data-modules.tsx +++ b/apps/docs/src/components/demo/data-modules.tsx @@ -18,6 +18,7 @@ const styles: DataModulesStyle[] = [ 'leaf', 'vertical-line', 'horizontal-line', + 'circuit-board', 'circle', 'diamond', 'star', diff --git a/packages/react-qr-code/src/components/data-modules.test.tsx b/packages/react-qr-code/src/components/data-modules.test.tsx index 912832f..dc9a3f4 100644 --- a/packages/react-qr-code/src/components/data-modules.test.tsx +++ b/packages/react-qr-code/src/components/data-modules.test.tsx @@ -1,6 +1,7 @@ -import { render } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { describe, expect, it, vi } from 'vitest' +import { CIRCUIT_BOARD_LINE_WIDTH, CIRCUIT_BOARD_PAD_RADIUS } from '../constants' import { dataModulesHorizontalLineNeighbours, dataModulesLeafNeighbours, @@ -53,6 +54,59 @@ describe('DataModules', () => { }) }) + it('renders circuit-board as traces with pads', () => { + const modules = Array.from({ length: 10 }, () => Array(10).fill(false)) + modules[8][8] = true + modules[8][9] = true + modules[9][8] = true + + render( + , + ) + + const group = screen.getByTestId('data-modules') + const paths = group.querySelectorAll('path') + + expect(group.tagName.toLowerCase()).toBe('g') + expect(group).toHaveAttribute('fill', '#ffdd99') + expect(group).toHaveAttribute('stroke', '#ffdd99') + expect(group).toHaveAttribute('stroke-width', CIRCUIT_BOARD_LINE_WIDTH.toString()) + expect(paths).toHaveLength(2) + expect(paths[0]).toHaveAttribute('fill', 'none') + expect(paths[0].getAttribute('d')).toContain('M10.5,10.5L11.5,10.5') + expect(paths[0].getAttribute('d')).toContain('M10.5,10.5L10.5,11.5') + expect(paths[1]).toHaveAttribute('stroke', 'none') + expect(paths[1].getAttribute('d')).not.toContain( + dataModulesUtils.circuitBoardPad(10.5, 10.5, CIRCUIT_BOARD_PAD_RADIUS), + ) + }) + + it('renders standalone circuit-board modules as squares', () => { + const modules = Array.from({ length: 10 }, () => Array(10).fill(false)) + modules[8][8] = true + + render( + , + ) + + const group = screen.getByTestId('data-modules') + const paths = group.querySelectorAll('path') + + expect(paths).toHaveLength(1) + expect(paths[0]).toHaveAttribute('stroke', 'none') + expect(paths[0]).toHaveAttribute('d', 'M10,10h1v1h-1Z') + }) + it.each(dataModulesRoundedNeighbours)( `with neighbours %j calls the correct shape function %s for style rounded`, (neighbours, method) => { diff --git a/packages/react-qr-code/src/components/data-modules.tsx b/packages/react-qr-code/src/components/data-modules.tsx index 72a2149..cf761e6 100644 --- a/packages/react-qr-code/src/components/data-modules.tsx +++ b/packages/react-qr-code/src/components/data-modules.tsx @@ -1,15 +1,23 @@ import { type ReactNode, useCallback, useMemo } from 'react' -import { DEFAULT_NUM_STAR_POINTS } from '../constants' +import { + CIRCUIT_BOARD_LINE_WIDTH, + CIRCUIT_BOARD_PAD_RADIUS, + DEFAULT_NUM_STAR_POINTS, +} from '../constants' import type { DataModulesProps } from '../types/utils' import { bottomRounded, circle, + circuitBoardPad, + circuitBoardShouldDrawPad, dataModuleCanBeRandomSize, diamond, + getRenderableDataModuleNeighbours, getScaleFactor, leaf, leftRounded, + line, rightRounded, square, topRounded, @@ -39,6 +47,8 @@ export const DataModules = ({ ) const ops: Array = [] + const circuitBoardTraceOps: Array = [] + const circuitBoardPadOps: Array = [] const numCells = modules.length const isRandom = dataModuleCanBeRandomSize(style) && randomSize @@ -64,7 +74,24 @@ export const DataModules = ({ const yPos = y + margin + posOffset if (cell) { - if (style === 'square' || style === 'square-sm') { + if (style === 'circuit-board') { + const cx = x + margin + 0.5 + const cy = y + margin + 0.5 + const neighbours = getRenderableDataModuleNeighbours(x, y, modules, numCells) + const { right, bottom, count } = neighbours + + if (right) { + circuitBoardTraceOps.push(line(cx, cy, cx + 1, cy)) + } + if (bottom) { + circuitBoardTraceOps.push(line(cx, cy, cx, cy + 1)) + } + if (count === 0) { + circuitBoardPadOps.push(square(x + margin, y + margin, 1)) + } else if (circuitBoardShouldDrawPad({ ...neighbours, count })) { + circuitBoardPadOps.push(circuitBoardPad(cx, cy, CIRCUIT_BOARD_PAD_RADIUS)) + } + } else if (style === 'square' || style === 'square-sm') { ops.push(square(xPos, yPos, size)) } else if (style === 'pinched-square') { ops.push(pinchedSquare(xPos, yPos, size, 0.25)) @@ -149,9 +176,33 @@ export const DataModules = ({ } }) }) + + const paint = gradient ? `url(#${gradientId})` : color + + if (style === 'circuit-board') { + return ( + + {circuitBoardTraceOps.length > 0 && ( + + )} + {circuitBoardPadOps.length > 0 && ( + + )} + + ) + } + return ( { it('returns 0.75 for square-sm style', () => { @@ -20,6 +27,65 @@ describe('getScaleFactor', () => { }) }) +describe('dataModuleCanBeRandomSize', () => { + it('does not allow random size for circuit-board modules', () => { + expect(dataModuleCanBeRandomSize('circuit-board')).toBe(false) + }) +}) + +describe('circuitBoardShouldDrawPad', () => { + it('draws pads only for endpoints', () => { + expect( + circuitBoardShouldDrawPad({ + left: true, + right: false, + top: false, + bottom: false, + count: 1, + }), + ).toBe(true) + }) + + it('does not draw pads for isolated, turn, junction, or straight-through modules', () => { + expect( + circuitBoardShouldDrawPad({ + left: false, + right: false, + top: false, + bottom: false, + count: 0, + }), + ).toBe(false) + expect( + circuitBoardShouldDrawPad({ + left: true, + right: false, + top: true, + bottom: false, + count: 2, + }), + ).toBe(false) + expect( + circuitBoardShouldDrawPad({ + left: true, + right: true, + top: true, + bottom: false, + count: 3, + }), + ).toBe(false) + expect( + circuitBoardShouldDrawPad({ + left: false, + right: false, + top: true, + bottom: true, + count: 2, + }), + ).toBe(false) + }) +}) + describe('getModuleNeighbours', () => { it('should return correct neighbours for a center cell', () => { const modules: Modules = [ @@ -162,3 +228,29 @@ describe('getModuleNeighbours', () => { }) }) }) + +describe('isRenderableDataModule', () => { + it('returns false for finder pattern modules', () => { + const modules: Modules = Array.from({ length: 21 }, () => Array(21).fill(false)) + modules[0][0] = true + + expect(isRenderableDataModule({ x: 0, y: 0, modules, numCells: 21 })).toBe(false) + }) +}) + +describe('getRenderableDataModuleNeighbours', () => { + it('ignores neighbouring finder pattern modules', () => { + const modules: Modules = Array.from({ length: 21 }, () => Array(21).fill(false)) + modules[3][6] = true + modules[3][7] = true + modules[3][8] = true + + expect(getRenderableDataModuleNeighbours(7, 3, modules, 21)).toEqual({ + left: false, + right: true, + top: false, + bottom: false, + count: 1, + }) + }) +}) diff --git a/packages/react-qr-code/src/utils/data-modules.ts b/packages/react-qr-code/src/utils/data-modules.ts index 652bedd..5b4115d 100644 --- a/packages/react-qr-code/src/utils/data-modules.ts +++ b/packages/react-qr-code/src/utils/data-modules.ts @@ -1,5 +1,7 @@ import type { DataModulesStyle, Modules } from '../types/lib' import type { DataModulesNeighbours } from '../types/utils' +import { isFinderPatternInnerModule } from './finder-patterns-inner' +import { isFinderPatternOuterModule } from './finder-patterns-outer' export const dataModuleCanBeRandomSize = (style: DataModulesStyle): boolean => style === 'square' || @@ -37,6 +39,47 @@ export const getModuleNeighbours = ( } } +export const isRenderableDataModule = ({ + x, + y, + modules, + numCells, +}: { + x: number + y: number + modules: Modules + numCells: number +}) => { + return ( + y >= 0 && + y < modules.length && + x >= 0 && + x < modules[y].length && + modules[y][x] && + !isFinderPatternOuterModule({ x, y, numCells }) && + !isFinderPatternInnerModule({ x, y, numCells }) + ) +} + +export const getRenderableDataModuleNeighbours = ( + x: number, + y: number, + modules: Modules, + numCells: number, +): DataModulesNeighbours => { + const sides = { + left: isRenderableDataModule({ x: x - 1, y, modules, numCells }), + right: isRenderableDataModule({ x: x + 1, y, modules, numCells }), + top: isRenderableDataModule({ x, y: y - 1, modules, numCells }), + bottom: isRenderableDataModule({ x, y: y + 1, modules, numCells }), + } + + return { + ...sides, + count: Object.values(sides).filter(Boolean).length, + } +} + export const square = (x: number, y: number, size: number) => `M${x},${y}h${size}v${size}h-${size}Z` @@ -46,6 +89,14 @@ export const circle = (x: number, y: number, size: number) => export const diamond = (x: number, y: number, size: number) => `M${x},${y + size / 2}l${size / 2},-${size / 2}l${size / 2},${size / 2}l-${size / 2},${size / 2}Z` +export const line = (x1: number, y1: number, x2: number, y2: number) => + `M${x1},${y1}L${x2},${y2}` + +export const circuitBoardPad = (cx: number, cy: number, radius: number) => + circle(cx - radius, cy - radius, radius * 2) + +export const circuitBoardShouldDrawPad = ({ count }: DataModulesNeighbours) => count === 1 + export const topRightRounded = (x: number, y: number) => `M ${x} ${y} v 1 From d5089642121401bd8c01f95267f8388931834ae7 Mon Sep 17 00:00:00 2001 From: LGLabGreg Date: Sat, 9 May 2026 09:01:42 +0100 Subject: [PATCH 2/5] fix: coerce demo number inputs to numbers --- apps/docs/src/components/demo/main-settings.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/docs/src/components/demo/main-settings.tsx b/apps/docs/src/components/demo/main-settings.tsx index 141d507..67464a0 100644 --- a/apps/docs/src/components/demo/main-settings.tsx +++ b/apps/docs/src/components/demo/main-settings.tsx @@ -21,9 +21,10 @@ const errorCorrectionLevels: ErrorCorrectionLevel[] = ['L', 'M', 'Q', 'H'] export const MainSettings = ({ qrProps, setQrProps }: MainSettingsProps) => { const onInputChange = (e: React.ChangeEvent, key: string) => { + const { value, type } = e.target setQrProps((prevProps) => ({ ...prevProps, - [key]: e.target.value, + [key]: type === 'number' && value !== '' ? Number(value) : value, })) } const onValueChange = (value: string, key: string) => { From 48ceee74528af1c802ddfcfae7eea03d22b2671f Mon Sep 17 00:00:00 2001 From: LGLabGreg Date: Sat, 9 May 2026 14:47:12 +0100 Subject: [PATCH 3/5] test: fix corner --- .../src/components/data-modules.test.tsx | 2 +- .../react-qr-code/src/components/data-modules.tsx | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/react-qr-code/src/components/data-modules.test.tsx b/packages/react-qr-code/src/components/data-modules.test.tsx index dc9a3f4..f59547a 100644 --- a/packages/react-qr-code/src/components/data-modules.test.tsx +++ b/packages/react-qr-code/src/components/data-modules.test.tsx @@ -104,7 +104,7 @@ describe('DataModules', () => { expect(paths).toHaveLength(1) expect(paths[0]).toHaveAttribute('stroke', 'none') - expect(paths[0]).toHaveAttribute('d', 'M10,10h1v1h-1Z') + expect(paths[0]).toHaveAttribute('d', 'M10.125,10.125h0.75v0.75h-0.75Z') }) it.each(dataModulesRoundedNeighbours)( diff --git a/packages/react-qr-code/src/components/data-modules.tsx b/packages/react-qr-code/src/components/data-modules.tsx index cf761e6..269a784 100644 --- a/packages/react-qr-code/src/components/data-modules.tsx +++ b/packages/react-qr-code/src/components/data-modules.tsx @@ -87,7 +87,15 @@ export const DataModules = ({ circuitBoardTraceOps.push(line(cx, cy, cx, cy + 1)) } if (count === 0) { - circuitBoardPadOps.push(square(x + margin, y + margin, 1)) + const isolatedSize = 0.75 + const isolatedOffset = (1 - isolatedSize) / 2 + circuitBoardPadOps.push( + square( + x + margin + isolatedOffset, + y + margin + isolatedOffset, + isolatedSize, + ), + ) } else if (circuitBoardShouldDrawPad({ ...neighbours, count })) { circuitBoardPadOps.push(circuitBoardPad(cx, cy, CIRCUIT_BOARD_PAD_RADIUS)) } @@ -185,8 +193,8 @@ export const DataModules = ({ fill={paint} stroke={paint} strokeWidth={CIRCUIT_BOARD_LINE_WIDTH} - strokeLinecap='round' - strokeLinejoin='round' + strokeLinecap='square' + strokeLinejoin='miter' shapeRendering='geometricPrecision' data-testid='data-modules' > From 61b19ead1f5f128f7575e5941c4df93f19e54292 Mon Sep 17 00:00:00 2001 From: LGLabGreg Date: Sat, 9 May 2026 15:12:06 +0100 Subject: [PATCH 4/5] feat: microchip finder inner and circuit-board polish Add microchip finder pattern inner style, square corners and smaller isolated cell for circuit-board, alphabetised demo style lists. --- .changeset/silver-traces-turn.md | 2 +- .../src/app/data-modules-settings/page.tsx | 20 +++++++------- .../finder-pattern-inner-settings/page.tsx | 27 ++++++++++--------- .../finder-pattern-outer-settings/page.tsx | 20 +++++++------- .../docs/src/components/demo/data-modules.tsx | 20 +++++++------- .../components/demo/finder-pattern-inner.tsx | 27 ++++++++++--------- .../components/demo/finder-pattern-outer.tsx | 20 +++++++------- .../src/components/data-modules.test.tsx | 9 ++++++- .../components/finder-patter-inner.test.tsx | 16 +++++++++++ .../src/components/finder-patterns-inner.tsx | 9 ++++++- packages/react-qr-code/src/types/lib.ts | 1 + packages/react-qr-code/src/utils/svg.ts | 23 ++++++++++++++++ 12 files changed, 125 insertions(+), 69 deletions(-) diff --git a/.changeset/silver-traces-turn.md b/.changeset/silver-traces-turn.md index 370c298..53ca28b 100644 --- a/.changeset/silver-traces-turn.md +++ b/.changeset/silver-traces-turn.md @@ -2,4 +2,4 @@ '@lglab/react-qr-code': minor --- -Add circuit-board data module style. +Add circuit-board data module style and microchip finder pattern inner style. diff --git a/apps/docs/src/app/data-modules-settings/page.tsx b/apps/docs/src/app/data-modules-settings/page.tsx index b2a59aa..66cb122 100644 --- a/apps/docs/src/app/data-modules-settings/page.tsx +++ b/apps/docs/src/app/data-modules-settings/page.tsx @@ -24,19 +24,19 @@ const props: Prop[] = [ description: 'The style of the modules', defaultValue: 'square', possibleValues: [ - 'square', - 'square-sm', - 'pinched-square', - 'rounded', - 'leaf', - 'vertical-line', - 'horizontal-line', - 'circuit-board', 'circle', + 'circuit-board', 'diamond', - 'star', - 'heart', 'hashtag', + 'heart', + 'horizontal-line', + 'leaf', + 'pinched-square', + 'rounded', + 'square', + 'square-sm', + 'star', + 'vertical-line', ], }, { diff --git a/apps/docs/src/app/finder-pattern-inner-settings/page.tsx b/apps/docs/src/app/finder-pattern-inner-settings/page.tsx index ffc7264..2d0b55a 100644 --- a/apps/docs/src/app/finder-pattern-inner-settings/page.tsx +++ b/apps/docs/src/app/finder-pattern-inner-settings/page.tsx @@ -24,25 +24,26 @@ const props: Prop[] = [ description: 'The style of the finder patterns inner part', defaultValue: 'square', possibleValues: [ - 'square', - 'pinched-square', - 'rounded-sm', - 'rounded', - 'rounded-lg', 'circle', - 'inpoint-sm', + 'diamond', + 'hashtag', + 'heart', 'inpoint', 'inpoint-lg', - 'outpoint-sm', - 'outpoint', - 'outpoint-lg', - 'leaf-sm', + 'inpoint-sm', 'leaf', 'leaf-lg', - 'diamond', + 'leaf-sm', + 'microchip', + 'outpoint', + 'outpoint-lg', + 'outpoint-sm', + 'pinched-square', + 'rounded', + 'rounded-lg', + 'rounded-sm', + 'square', 'star', - 'heart', - 'hashtag', ], }, ] diff --git a/apps/docs/src/app/finder-pattern-outer-settings/page.tsx b/apps/docs/src/app/finder-pattern-outer-settings/page.tsx index 9914c68..a0ff018 100644 --- a/apps/docs/src/app/finder-pattern-outer-settings/page.tsx +++ b/apps/docs/src/app/finder-pattern-outer-settings/page.tsx @@ -24,21 +24,21 @@ const props: Prop[] = [ description: 'The style of the finder patterns outer part', defaultValue: 'square', possibleValues: [ - 'square', - 'pinched-square', - 'rounded-sm', - 'rounded', - 'rounded-lg', 'circle', - 'inpoint-sm', 'inpoint', 'inpoint-lg', - 'outpoint-sm', - 'outpoint', - 'outpoint-lg', - 'leaf-sm', + 'inpoint-sm', 'leaf', 'leaf-lg', + 'leaf-sm', + 'outpoint', + 'outpoint-lg', + 'outpoint-sm', + 'pinched-square', + 'rounded', + 'rounded-lg', + 'rounded-sm', + 'square', ], }, ] diff --git a/apps/docs/src/components/demo/data-modules.tsx b/apps/docs/src/components/demo/data-modules.tsx index 728b001..0c66a0c 100644 --- a/apps/docs/src/components/demo/data-modules.tsx +++ b/apps/docs/src/components/demo/data-modules.tsx @@ -11,19 +11,19 @@ interface DataModulesProps { } const styles: DataModulesStyle[] = [ - 'square', - 'square-sm', - 'pinched-square', - 'rounded', - 'leaf', - 'vertical-line', - 'horizontal-line', - 'circuit-board', 'circle', + 'circuit-board', 'diamond', - 'star', - 'heart', 'hashtag', + 'heart', + 'horizontal-line', + 'leaf', + 'pinched-square', + 'rounded', + 'square', + 'square-sm', + 'star', + 'vertical-line', ] export const DataModules = ({ qrProps, setQrProps }: DataModulesProps) => { diff --git a/apps/docs/src/components/demo/finder-pattern-inner.tsx b/apps/docs/src/components/demo/finder-pattern-inner.tsx index 907c50e..8bf5867 100644 --- a/apps/docs/src/components/demo/finder-pattern-inner.tsx +++ b/apps/docs/src/components/demo/finder-pattern-inner.tsx @@ -11,25 +11,26 @@ interface FinderPatternInnerProps { } const styles: FinderPatternInnerStyle[] = [ - 'square', - 'pinched-square', - 'rounded-sm', - 'rounded', - 'rounded-lg', 'circle', - 'inpoint-sm', + 'diamond', + 'hashtag', + 'heart', 'inpoint', 'inpoint-lg', - 'outpoint-sm', - 'outpoint', - 'outpoint-lg', - 'leaf-sm', + 'inpoint-sm', 'leaf', 'leaf-lg', - 'diamond', + 'leaf-sm', + 'microchip', + 'outpoint', + 'outpoint-lg', + 'outpoint-sm', + 'pinched-square', + 'rounded', + 'rounded-lg', + 'rounded-sm', + 'square', 'star', - 'heart', - 'hashtag', ] export const FinderPatternInner = ({ qrProps, setQrProps }: FinderPatternInnerProps) => { diff --git a/apps/docs/src/components/demo/finder-pattern-outer.tsx b/apps/docs/src/components/demo/finder-pattern-outer.tsx index 879856c..38b750a 100644 --- a/apps/docs/src/components/demo/finder-pattern-outer.tsx +++ b/apps/docs/src/components/demo/finder-pattern-outer.tsx @@ -11,21 +11,21 @@ interface DataModulesProps { } const styles: FinderPatternOuterStyle[] = [ - 'square', - 'pinched-square', - 'rounded-sm', - 'rounded', - 'rounded-lg', 'circle', - 'inpoint-sm', 'inpoint', 'inpoint-lg', - 'outpoint-sm', - 'outpoint', - 'outpoint-lg', - 'leaf-sm', + 'inpoint-sm', 'leaf', 'leaf-lg', + 'leaf-sm', + 'outpoint', + 'outpoint-lg', + 'outpoint-sm', + 'pinched-square', + 'rounded', + 'rounded-lg', + 'rounded-sm', + 'square', ] export const FinderPatternOuter = ({ qrProps, setQrProps }: DataModulesProps) => { diff --git a/packages/react-qr-code/src/components/data-modules.test.tsx b/packages/react-qr-code/src/components/data-modules.test.tsx index f59547a..169a471 100644 --- a/packages/react-qr-code/src/components/data-modules.test.tsx +++ b/packages/react-qr-code/src/components/data-modules.test.tsx @@ -81,7 +81,14 @@ describe('DataModules', () => { expect(paths[0].getAttribute('d')).toContain('M10.5,10.5L11.5,10.5') expect(paths[0].getAttribute('d')).toContain('M10.5,10.5L10.5,11.5') expect(paths[1]).toHaveAttribute('stroke', 'none') - expect(paths[1].getAttribute('d')).not.toContain( + const padPath = paths[1].getAttribute('d') ?? '' + expect(padPath).toContain( + dataModulesUtils.circuitBoardPad(11.5, 10.5, CIRCUIT_BOARD_PAD_RADIUS), + ) + expect(padPath).toContain( + dataModulesUtils.circuitBoardPad(10.5, 11.5, CIRCUIT_BOARD_PAD_RADIUS), + ) + expect(padPath).not.toContain( dataModulesUtils.circuitBoardPad(10.5, 10.5, CIRCUIT_BOARD_PAD_RADIUS), ) }) diff --git a/packages/react-qr-code/src/components/finder-patter-inner.test.tsx b/packages/react-qr-code/src/components/finder-patter-inner.test.tsx index dc2ed9a..1041161 100644 --- a/packages/react-qr-code/src/components/finder-patter-inner.test.tsx +++ b/packages/react-qr-code/src/components/finder-patter-inner.test.tsx @@ -132,6 +132,22 @@ describe('DataModules', () => { }) }) + it('renders correctly with style microchip', () => { + const spy = vi.spyOn(svgUtils, 'microchip') + render( + , + ) + const paths = screen.getAllByTestId('finder-patterns-inner') + expect(paths).toHaveLength(3) + paths.forEach((path) => { + expect(spy).toHaveBeenCalled() + expect(path.getAttribute('fill')).toBe('#ff0000') + }) + }) + it('renders correctly with style hashtag', () => { const spy = vi.spyOn(svgUtils, 'hashtag') render( diff --git a/packages/react-qr-code/src/components/finder-patterns-inner.tsx b/packages/react-qr-code/src/components/finder-patterns-inner.tsx index 6f2be28..0825c38 100644 --- a/packages/react-qr-code/src/components/finder-patterns-inner.tsx +++ b/packages/react-qr-code/src/components/finder-patterns-inner.tsx @@ -13,7 +13,7 @@ import { finderPatternsInnerLeaf, } from '../utils/finder-patterns-inner' import { sanitizeFinderPatternInnerSettings } from '../utils/settings' -import { hashtag, heart, pinchedSquare, star } from '../utils/svg' +import { hashtag, heart, microchip, pinchedSquare, star } from '../utils/svg' const testProps = { 'data-testid': 'finder-patterns-inner', @@ -164,6 +164,13 @@ export const FinderPatternsInner = ({ }) } + if (style === 'microchip') { + return coordinates.map(({ x, y }) => { + const path = microchip(x, y, FINDER_PATTERN_INNER_SIZE) + return + }) + } + if (style === 'hashtag') { return coordinates.map(({ x, y }) => { const path = hashtag(x - 0.25, y - 0.25, 3.5) diff --git a/packages/react-qr-code/src/types/lib.ts b/packages/react-qr-code/src/types/lib.ts index 5c0d3a9..389e466 100644 --- a/packages/react-qr-code/src/types/lib.ts +++ b/packages/react-qr-code/src/types/lib.ts @@ -91,6 +91,7 @@ export type FinderPatternInnerStyle = | 'star' | 'heart' | 'hashtag' + | 'microchip' export interface FinderPatternInnerSettings { color?: string diff --git a/packages/react-qr-code/src/utils/svg.ts b/packages/react-qr-code/src/utils/svg.ts index 1c24509..a9f49a3 100644 --- a/packages/react-qr-code/src/utils/svg.ts +++ b/packages/react-qr-code/src/utils/svg.ts @@ -49,6 +49,29 @@ export const pinchedSquare = ( `Q ${x + size / 2} ${y + controlOffset}, ${x} ${y}` + 'Z' +const MICROCHIP_LEG_HEIGHT_RATIO = 0.15 +const MICROCHIP_LEG_WIDTH_RATIO = 0.1 +const MICROCHIP_LEG_SPAN_RATIO = 0.7 +const MICROCHIP_NUM_LEGS = 4 + +export const microchip = (x: number, y: number, size: number) => { + const legH = size * MICROCHIP_LEG_HEIGHT_RATIO + const legW = size * MICROCHIP_LEG_WIDTH_RATIO + const bodyH = size - legH * 2 + const body = `M${x},${y + legH}h${size}v${bodyH}h${-size}Z` + const legSpan = size * MICROCHIP_LEG_SPAN_RATIO + const legStart = x + (size - legSpan) / 2 + const legStep = (legSpan - legW) / (MICROCHIP_NUM_LEGS - 1) + const legs = Array.from({ length: MICROCHIP_NUM_LEGS }, (_, i) => { + const lx = legStart + legStep * i + return ( + `M${lx},${y}h${legW}v${legH}h${-legW}Z` + + `M${lx},${y + size - legH}h${legW}v${legH}h${-legW}Z` + ) + }).join('') + return body + legs +} + export const hashtag = (x: number, y: number, size: number) => { const eigth = size / 8 return `M ${x + size} ${y + eigth * 3} From d25c3f68295c523427a478e6c144dbfb2829ead1 Mon Sep 17 00:00:00 2001 From: LGLabGreg Date: Sat, 9 May 2026 15:37:50 +0100 Subject: [PATCH 5/5] refactor: render circuit-board as a single path Collapse the -with-two-paths circuit-board renderer into the single filled shared by every other style. Traces become thin filled rectangles extended traceHalf past both endpoints so junction cells fill cleanly under nonzero fill rule. Pad circle is wound clockwise to match. --- .../src/components/data-modules.test.tsx | 99 +++++++++++++++---- .../src/components/data-modules.tsx | 42 +++----- .../react-qr-code/src/utils/data-modules.ts | 15 +-- 3 files changed, 101 insertions(+), 55 deletions(-) diff --git a/packages/react-qr-code/src/components/data-modules.test.tsx b/packages/react-qr-code/src/components/data-modules.test.tsx index 169a471..ca7e6ad 100644 --- a/packages/react-qr-code/src/components/data-modules.test.tsx +++ b/packages/react-qr-code/src/components/data-modules.test.tsx @@ -69,26 +69,37 @@ describe('DataModules', () => { />, ) - const group = screen.getByTestId('data-modules') - const paths = group.querySelectorAll('path') - - expect(group.tagName.toLowerCase()).toBe('g') - expect(group).toHaveAttribute('fill', '#ffdd99') - expect(group).toHaveAttribute('stroke', '#ffdd99') - expect(group).toHaveAttribute('stroke-width', CIRCUIT_BOARD_LINE_WIDTH.toString()) - expect(paths).toHaveLength(2) - expect(paths[0]).toHaveAttribute('fill', 'none') - expect(paths[0].getAttribute('d')).toContain('M10.5,10.5L11.5,10.5') - expect(paths[0].getAttribute('d')).toContain('M10.5,10.5L10.5,11.5') - expect(paths[1]).toHaveAttribute('stroke', 'none') - const padPath = paths[1].getAttribute('d') ?? '' - expect(padPath).toContain( + const path = screen.getByTestId('data-modules') + + expect(path.tagName.toLowerCase()).toBe('path') + expect(path).toHaveAttribute('fill', '#ffdd99') + expect(path).not.toHaveAttribute('stroke') + const d = path.getAttribute('d') ?? '' + const traceHalf = CIRCUIT_BOARD_LINE_WIDTH / 2 + const traceLength = 1 + CIRCUIT_BOARD_LINE_WIDTH + expect(d).toContain( + dataModulesUtils.rect( + 10.5 - traceHalf, + 10.5 - traceHalf, + traceLength, + CIRCUIT_BOARD_LINE_WIDTH, + ), + ) + expect(d).toContain( + dataModulesUtils.rect( + 10.5 - traceHalf, + 10.5 - traceHalf, + CIRCUIT_BOARD_LINE_WIDTH, + traceLength, + ), + ) + expect(d).toContain( dataModulesUtils.circuitBoardPad(11.5, 10.5, CIRCUIT_BOARD_PAD_RADIUS), ) - expect(padPath).toContain( + expect(d).toContain( dataModulesUtils.circuitBoardPad(10.5, 11.5, CIRCUIT_BOARD_PAD_RADIUS), ) - expect(padPath).not.toContain( + expect(d).not.toContain( dataModulesUtils.circuitBoardPad(10.5, 10.5, CIRCUIT_BOARD_PAD_RADIUS), ) }) @@ -106,12 +117,58 @@ describe('DataModules', () => { />, ) - const group = screen.getByTestId('data-modules') - const paths = group.querySelectorAll('path') + const path = screen.getByTestId('data-modules') + + expect(path.tagName.toLowerCase()).toBe('path') + expect(path).toHaveAttribute('d', 'M10.125,10.125h0.75v0.75h-0.75Z') + }) + + it('fully covers the cell centre at circuit-board junctions', () => { + // Junction cell at (8,8) with neighbours at (7,8), (9,8), (8,7), (8,9): + // it has top+left+right+bottom = count 4 but draws no traces itself + // (only right/bottom). The incoming traces from each neighbour must + // collectively fill the cell centre or a white notch appears. + const modules = Array.from({ length: 12 }, () => Array(12).fill(false)) + modules[7][8] = true + modules[8][7] = true + modules[8][8] = true + modules[8][9] = true + modules[9][8] = true + + render( + , + ) - expect(paths).toHaveLength(1) - expect(paths[0]).toHaveAttribute('stroke', 'none') - expect(paths[0]).toHaveAttribute('d', 'M10.125,10.125h0.75v0.75h-0.75Z') + const d = screen.getByTestId('data-modules').getAttribute('d') ?? '' + const traceHalf = CIRCUIT_BOARD_LINE_WIDTH / 2 + const traceLength = 1 + CIRCUIT_BOARD_LINE_WIDTH + + // Incoming horizontal trace from cell (8,7), centre (9.5, 10.5): + // must extend past its own end (10.5) by traceHalf so the junction + // cell's centre is covered from the left. + expect(d).toContain( + dataModulesUtils.rect( + 9.5 - traceHalf, + 10.5 - traceHalf, + traceLength, + CIRCUIT_BOARD_LINE_WIDTH, + ), + ) + // Incoming vertical trace from cell (7,8), centre (10.5, 9.5): + // covers the junction from above. + expect(d).toContain( + dataModulesUtils.rect( + 10.5 - traceHalf, + 9.5 - traceHalf, + CIRCUIT_BOARD_LINE_WIDTH, + traceLength, + ), + ) }) it.each(dataModulesRoundedNeighbours)( diff --git a/packages/react-qr-code/src/components/data-modules.tsx b/packages/react-qr-code/src/components/data-modules.tsx index 269a784..10b715d 100644 --- a/packages/react-qr-code/src/components/data-modules.tsx +++ b/packages/react-qr-code/src/components/data-modules.tsx @@ -17,7 +17,7 @@ import { getScaleFactor, leaf, leftRounded, - line, + rect, rightRounded, square, topRounded, @@ -47,8 +47,6 @@ export const DataModules = ({ ) const ops: Array = [] - const circuitBoardTraceOps: Array = [] - const circuitBoardPadOps: Array = [] const numCells = modules.length const isRandom = dataModuleCanBeRandomSize(style) && randomSize @@ -77,19 +75,28 @@ export const DataModules = ({ if (style === 'circuit-board') { const cx = x + margin + 0.5 const cy = y + margin + 0.5 + const traceHalf = CIRCUIT_BOARD_LINE_WIDTH / 2 + // Traces extend traceHalf past both endpoints so that adjacent + // traces fully cover the cell-center square at every junction + // (preventing white notches at L/T/+ bends under nonzero fill). + const traceLength = 1 + CIRCUIT_BOARD_LINE_WIDTH const neighbours = getRenderableDataModuleNeighbours(x, y, modules, numCells) const { right, bottom, count } = neighbours if (right) { - circuitBoardTraceOps.push(line(cx, cy, cx + 1, cy)) + ops.push( + rect(cx - traceHalf, cy - traceHalf, traceLength, CIRCUIT_BOARD_LINE_WIDTH), + ) } if (bottom) { - circuitBoardTraceOps.push(line(cx, cy, cx, cy + 1)) + ops.push( + rect(cx - traceHalf, cy - traceHalf, CIRCUIT_BOARD_LINE_WIDTH, traceLength), + ) } if (count === 0) { const isolatedSize = 0.75 const isolatedOffset = (1 - isolatedSize) / 2 - circuitBoardPadOps.push( + ops.push( square( x + margin + isolatedOffset, y + margin + isolatedOffset, @@ -97,7 +104,7 @@ export const DataModules = ({ ), ) } else if (circuitBoardShouldDrawPad({ ...neighbours, count })) { - circuitBoardPadOps.push(circuitBoardPad(cx, cy, CIRCUIT_BOARD_PAD_RADIUS)) + ops.push(circuitBoardPad(cx, cy, CIRCUIT_BOARD_PAD_RADIUS)) } } else if (style === 'square' || style === 'square-sm') { ops.push(square(xPos, yPos, size)) @@ -187,27 +194,6 @@ export const DataModules = ({ const paint = gradient ? `url(#${gradientId})` : color - if (style === 'circuit-board') { - return ( - - {circuitBoardTraceOps.length > 0 && ( - - )} - {circuitBoardPadOps.length > 0 && ( - - )} - - ) - } - return ( - `M${x},${y}h${size}v${size}h-${size}Z` +export const rect = (x: number, y: number, width: number, height: number) => + `M${x},${y}h${width}v${height}h${-width}Z` + +export const square = (x: number, y: number, size: number) => rect(x, y, size, size) export const circle = (x: number, y: number, size: number) => `M${x},${y + size / 2}a${size / 2},${size / 2} 0 1,0 ${size},0a${size / 2},${size / 2} 0 1,0 -${size},0Z` @@ -89,11 +91,12 @@ export const circle = (x: number, y: number, size: number) => export const diamond = (x: number, y: number, size: number) => `M${x},${y + size / 2}l${size / 2},-${size / 2}l${size / 2},${size / 2}l-${size / 2},${size / 2}Z` -export const line = (x1: number, y1: number, x2: number, y2: number) => - `M${x1},${y1}L${x2},${y2}` - +// Wound clockwise (sweep-flag 1) so the pad fills correctly when combined +// in a single path with clockwise-wound trace rects under nonzero fill. +// Switching to circle() (counter-clockwise) would XOR the overlap and +// produce donut-shaped pads. export const circuitBoardPad = (cx: number, cy: number, radius: number) => - circle(cx - radius, cy - radius, radius * 2) + `M${cx - radius},${cy}a${radius},${radius} 0 1,1 ${radius * 2},0a${radius},${radius} 0 1,1 ${-radius * 2},0Z` export const circuitBoardShouldDrawPad = ({ count }: DataModulesNeighbours) => count === 1