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
5 changes: 5 additions & 0 deletions .changeset/fix-oval-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'basekit': patch
---

Keep saved five-pocket magnet layouts in a cross on elongated bases and matching holders.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Make a round, square, or hex token for objectives, Oath of Moment, or anything e
- Printable 15, 20, 30, and 35mm flying stems with adjustable peg or ball-joint connections.
- Round, square, and hex tokens with wrapped text and traced image silhouettes.
- Shared magnet dimensions across bases, holders, movement trays, and spray trays.
- Balanced or five-pocket cross magnet arrangements for bases and holders.
- Balanced or five-pocket cross magnet arrangements for bases and holders. Saved cross layouts keep one centre pocket and four outer pockets on both axes, including elongated footprints.
- Exact size labels, filenames, dimensions, and high-quality exports.
- Browser-saved settings with shared base and holder preferences, and a confirmed reset for each generator or for the shared settings.
- Copyable links that open the current generator with the same settings.
Expand Down
39 changes: 39 additions & 0 deletions src/geometry/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,45 @@ describe('buildBase', () => {
expect(positions.filter(({ x, y }) => Math.hypot(x, y) > 1e-6)).toHaveLength(4)
})

it.each([
[83, 52.5],
[52.5, 83],
])('keeps five-pocket crosses on both axes of an elongated %s × %s well', (halfWidth, halfLength) => {
expect(magnetPositions(5, halfWidth, halfLength, 4.9, { layout: 'five-cross', ellipticalRow: true })).toEqual([
{ x: 0, y: 0, r: 0 },
{ x: 0, y: 26.25, r: 26.25 },
{ x: -26.25, y: 0, r: 26.25 },
{ x: 0, y: -26.25, r: 26.25 },
{ x: 26.25, y: 0, r: 26.25 },
])
})

it('drills the legacy 170 × 109 oval cross on both axes', () => {
const base = presetFor({ shape: 'oval', width: 170, length: 109, label: '170×109', use: '' }, 8, 1)
const config = {
...base,
wallThickness: 2,
magnets: { ...base.magnets, layout: 'five-cross' as const, count: 5, diameter: 6 },
ribs: { ...base.ribs, count: 4 },
segments: exportSegmentsFor(170),
}
const { mesh } = build(config)
const centers = [
[0, 0],
[0, 26.25],
[-26.25, 0],
[0, -26.25],
[26.25, 0],
]
for (const [x, y] of centers) {
const pocket = pocketAt(mesh, x, y, 3.1)
expect(pocket.maxRadius).toBeCloseTo(3.1, 1)
expect(pocket.minZ).toBeCloseTo(config.height - config.magnets.thickness - config.magnets.depthClearance, 5)
expect(pocket.maxZ).toBeCloseTo(config.height, 5)
}
expect(duplicateVertices(mesh)).toBe(0)
})

it.for([RECT_SIZES[0], RECT_SIZES[1], ROUND_SIZES[0], ROUND_SIZES[1]])('still fits a size label on a cramped $label base', (size) => {
// The well of a small base is mostly boss and ribs, and the label used to be
// dropped silently when the first direction tried had no room.
Expand Down
12 changes: 9 additions & 3 deletions src/geometry/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,20 @@ export function magnetPositions(
options: MagnetPositionOptions = {},
): Circle[] {
if (count <= 0) return []
const short = Math.min(halfWidth, halfLength)
const radius = Math.max(Math.min(short * 0.5, short - clear), 0)
if (options.layout === 'five-cross') {
return [{ x: 0, y: 0, r: 0 }, ...magnetPositions(4, halfWidth, halfLength, clear, { ...options, layout: 'balanced' })]
return [
{ x: 0, y: 0, r: 0 },
{ x: 0, y: radius, r: radius },
{ x: -radius, y: 0, r: radius },
{ x: 0, y: -radius, r: radius },
{ x: radius, y: 0, r: radius },
]
}
if (count === 1) return [{ x: 0, y: 0, r: 0 }]

const long = Math.max(halfWidth, halfLength)
const short = Math.min(halfWidth, halfLength)
if (!magnetsRing(halfWidth, halfLength)) {
const ellipseReach = long * Math.sqrt(Math.max(0, 1 - (clear / short) ** 2)) - clear
const reach = Math.max(options.ellipticalRow ? ellipseReach : long - clear, 0)
Expand All @@ -65,7 +72,6 @@ export function magnetPositions(
})
}

const radius = Math.max(Math.min(short * 0.5, short - clear), 0)
return Array.from({ length: count }, (_, i) => {
const a = Math.PI / 2 + (2 * Math.PI * i) / count
return { x: radius * Math.cos(a), y: radius * Math.sin(a), r: radius }
Expand Down
8 changes: 7 additions & 1 deletion src/geometry/holder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,13 @@ describe('holderLayout', () => {
}
const oval = holderGroup('oval', 1, { shape: 'oval', width: 60, length: 35 })

expect(holderSlotMagnetCenters(oval, settings)).toHaveLength(5)
expect(holderSlotMagnetCenters(oval, settings)).toEqual([
{ x: 0, y: 0 },
{ x: 0, y: 7.75 },
{ x: -7.75, y: 0 },
{ x: 0, y: -7.75 },
{ x: 7.75, y: 0 },
])
})

it('fits forty 32mm models within a 7×5 box without false overflow', () => {
Expand Down
Loading