feat: add invisible crop marks for consistent SVG viewBox boundaries
Add crop marks system to both flashcards.typ and single-card.typ templates: - Invisible rectangular marks at four corners plus center reference - Optional debugging visibility with show-crop-marks: true - Configurable crop-margin parameter for boundary spacing - Link annotations for programmatic identification - Comprehensive documentation with processing examples - Post-processing script for automated viewBox calculation Eliminates need for manual SVG cropping by providing precise boundaries for automated viewBox handling and consistent template output. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
@@ -97,6 +97,7 @@ Here are some sample outputs from the templates to show what you can generate:
|
||||
- Custom column counts
|
||||
- Scale factors
|
||||
- Interactive bead annotations
|
||||
- **Crop marks**: Invisible viewBox boundaries for consistent SVG cropping
|
||||
|
||||
### `single-card.typ` - Optimized Single Cards
|
||||
|
||||
@@ -105,6 +106,120 @@ Here are some sample outputs from the templates to show what you can generate:
|
||||
- **PNG Export Ready**: Transparent backgrounds supported
|
||||
- **Custom Dimensions**: Configurable width/height
|
||||
- **Font Options**: Family, size, colored numerals
|
||||
- **Crop marks**: Invisible viewBox boundaries for consistent SVG cropping
|
||||
|
||||
## 🎯 Crop Marks & SVG Processing
|
||||
|
||||
Both templates include **crop marks** - invisible elements that define precise boundaries for consistent viewBox handling, eliminating the need for manual SVG cropping.
|
||||
|
||||
### Crop Marks System
|
||||
|
||||
- **Purpose**: Define consistent viewBox boundaries across all generated SVGs
|
||||
- **Implementation**: Invisible rectangles placed at the four corners and center
|
||||
- **Annotation**: Each crop mark is annotated using `link("crop-mark://position", element)`
|
||||
- **Debugging**: Set `show-crop-marks: true` to make marks visible in red
|
||||
|
||||
### Usage
|
||||
|
||||
```typst
|
||||
// flashcards.typ - invisible crop marks (default)
|
||||
#draw-soroban(123, show-crop-marks: false, crop-margin: 10pt)
|
||||
|
||||
// flashcards.typ - visible crop marks for debugging
|
||||
#draw-soroban(123, show-crop-marks: true, crop-margin: 15pt)
|
||||
|
||||
// single-card.typ - invisible crop marks (default)
|
||||
#generate-single-card(42, show-crop-marks: false, crop-margin: 10pt)
|
||||
|
||||
// single-card.typ - visible crop marks for debugging
|
||||
#generate-single-card(42, show-crop-marks: true, crop-margin: 15pt)
|
||||
```
|
||||
|
||||
### Link Annotation System
|
||||
|
||||
Both templates use Typst's `link()` function to annotate elements for post-processing:
|
||||
|
||||
- **Beads**: `link("bead://column-position-type", bead-element)`
|
||||
- **Crop Marks**: `link("crop-mark://position", mark-element)`
|
||||
|
||||
**Note**: Link annotations are exported to PDF format but not SVG. For SVG processing, the crop marks work as invisible positioning elements that can be identified by their precise coordinates and styling.
|
||||
|
||||
Example annotations in generated PDFs:
|
||||
- `bead://col1-ones-heaven` - Heaven bead in column 1, ones position
|
||||
- `bead://col2-tens-earth-1` - First earth bead in column 2, tens position
|
||||
- `crop-mark://top-left` - Top-left crop boundary
|
||||
- `crop-mark://center` - Center reference point
|
||||
|
||||
### SVG Crop Mark Processing
|
||||
|
||||
For SVG files, crop marks can be identified by their coordinates and used for precise viewBox calculation:
|
||||
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
const { JSDOM } = require('jsdom');
|
||||
|
||||
function findCropMarks(svgPath) {
|
||||
const svgContent = fs.readFileSync(svgPath, 'utf-8');
|
||||
const dom = new JSDOM(svgContent);
|
||||
const document = dom.window.document;
|
||||
|
||||
// Crop marks are invisible rectangles with stroke-width="0"
|
||||
const invisibleRects = document.querySelectorAll('rect[stroke-width="0"]');
|
||||
|
||||
let cropBounds = {
|
||||
minX: Infinity, maxX: -Infinity,
|
||||
minY: Infinity, maxY: -Infinity
|
||||
};
|
||||
|
||||
invisibleRects.forEach(rect => {
|
||||
const parent = rect.closest('g[transform]');
|
||||
if (parent) {
|
||||
const transform = parent.getAttribute('transform');
|
||||
const translateMatch = transform.match(/translate\(([^)]+)\)/);
|
||||
|
||||
if (translateMatch) {
|
||||
const [x, y] = translateMatch[1].split(' ').map(Number);
|
||||
cropBounds.minX = Math.min(cropBounds.minX, x);
|
||||
cropBounds.maxX = Math.max(cropBounds.maxX, x);
|
||||
cropBounds.minY = Math.min(cropBounds.minY, y);
|
||||
cropBounds.maxY = Math.max(cropBounds.maxY, y);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return cropBounds;
|
||||
}
|
||||
|
||||
function updateViewBox(svgPath, outputPath) {
|
||||
const bounds = findCropMarks(svgPath);
|
||||
const svgContent = fs.readFileSync(svgPath, 'utf-8');
|
||||
|
||||
// Calculate new viewBox from crop marks
|
||||
const width = bounds.maxX - bounds.minX;
|
||||
const height = bounds.maxY - bounds.minY;
|
||||
const newViewBox = `${bounds.minX} ${bounds.minY} ${width} ${height}`;
|
||||
|
||||
// Update viewBox in SVG
|
||||
const updatedSvg = svgContent.replace(
|
||||
/viewBox="[^"]*"/,
|
||||
`viewBox="${newViewBox}"`
|
||||
);
|
||||
|
||||
fs.writeFileSync(outputPath, updatedSvg);
|
||||
console.log(`Updated viewBox to: ${newViewBox}`);
|
||||
}
|
||||
|
||||
// Usage
|
||||
updateViewBox('soroban.svg', 'cropped-soroban.svg');
|
||||
```
|
||||
|
||||
**Crop Mark Processing Features:**
|
||||
- **Automatic ViewBox**: Calculate precise viewBox from crop mark positions
|
||||
- **Consistent Cropping**: Eliminate manual SVG cropping across all generated files
|
||||
- **Debugging Support**: Set `show-crop-marks: true` to visually verify boundaries
|
||||
- **Flexible Margins**: Adjust `crop-margin` to control boundary spacing
|
||||
|
||||
**Example Usage Script:** See `examples/svg-post-processor.js` for a complete implementation
|
||||
|
||||
## 🔧 Installation & Setup
|
||||
|
||||
|
||||
213
packages/templates/examples/svg-post-processor.js
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* SVG Post-Processor for Typst-generated Soroban Templates
|
||||
*
|
||||
* Converts Typst link annotations into HTML data attributes for easier
|
||||
* programmatic manipulation of generated SVG files.
|
||||
*
|
||||
* Usage:
|
||||
* node svg-post-processor.js input.svg output.svg
|
||||
*
|
||||
* Processes:
|
||||
* - Bead annotations: link("bead://col1-ones-heaven") -> data-bead-* attributes
|
||||
* - Crop mark annotations: link("crop-mark://top-left") -> data-crop-* attributes
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Use jsdom if available, fallback to regex-based processing
|
||||
let JSDOM;
|
||||
try {
|
||||
JSDOM = require('jsdom').JSDOM;
|
||||
} catch (e) {
|
||||
console.warn('jsdom not found, using fallback regex processing');
|
||||
}
|
||||
|
||||
function processWithJSDOM(svgContent) {
|
||||
const dom = new JSDOM(svgContent);
|
||||
const document = dom.window.document;
|
||||
|
||||
// Find all elements with href attributes (Typst link annotations)
|
||||
const linkedElements = document.querySelectorAll('[href]');
|
||||
let processedCount = 0;
|
||||
|
||||
linkedElements.forEach(element => {
|
||||
const href = element.getAttribute('href');
|
||||
|
||||
if (href.startsWith('bead://')) {
|
||||
// Extract bead information: bead://col1-ones-heaven or bead://col2-tens-earth-1
|
||||
const beadInfo = href.replace('bead://', '');
|
||||
const parts = beadInfo.split('-');
|
||||
|
||||
if (parts.length >= 3) {
|
||||
const column = parts[0]; // col1, col2, etc.
|
||||
const position = parts[1]; // ones, tens, hundreds, etc.
|
||||
const type = parts[2]; // heaven, earth
|
||||
const index = parts[3]; // For earth beads: 1, 2, 3, 4
|
||||
|
||||
element.setAttribute('data-bead-column', column);
|
||||
element.setAttribute('data-bead-position', position);
|
||||
element.setAttribute('data-bead-type', type);
|
||||
element.setAttribute('data-element-type', 'bead');
|
||||
|
||||
if (index) {
|
||||
element.setAttribute('data-bead-index', index);
|
||||
}
|
||||
|
||||
processedCount++;
|
||||
}
|
||||
}
|
||||
else if (href.startsWith('crop-mark://')) {
|
||||
// Extract crop mark position: crop-mark://top-left
|
||||
const position = href.replace('crop-mark://', '');
|
||||
|
||||
element.setAttribute('data-crop-position', position);
|
||||
element.setAttribute('data-element-type', 'crop-mark');
|
||||
|
||||
// Add CSS class for easier styling
|
||||
const currentClass = element.getAttribute('class') || '';
|
||||
element.setAttribute('class', `${currentClass} crop-mark crop-mark-${position}`.trim());
|
||||
|
||||
processedCount++;
|
||||
}
|
||||
|
||||
// Remove the original href attribute
|
||||
element.removeAttribute('href');
|
||||
});
|
||||
|
||||
return {
|
||||
content: dom.serialize(),
|
||||
processedCount
|
||||
};
|
||||
}
|
||||
|
||||
function processWithRegex(svgContent) {
|
||||
let processedCount = 0;
|
||||
|
||||
// Process bead links: <a href="bead://col1-ones-heaven"><g>...</g></a>
|
||||
let result = svgContent.replace(
|
||||
/<a[^>]*href="bead:\/\/([^"]+)"[^>]*>(.*?)<\/a>/gs,
|
||||
(match, beadInfo, content) => {
|
||||
const parts = beadInfo.split('-');
|
||||
if (parts.length >= 3) {
|
||||
const column = parts[0];
|
||||
const position = parts[1];
|
||||
const type = parts[2];
|
||||
const index = parts[3];
|
||||
|
||||
let dataAttrs = `data-bead-column="${column}" data-bead-position="${position}" data-bead-type="${type}" data-element-type="bead"`;
|
||||
if (index) {
|
||||
dataAttrs += ` data-bead-index="${index}"`;
|
||||
}
|
||||
|
||||
processedCount++;
|
||||
// Wrap content in a group with data attributes
|
||||
return `<g ${dataAttrs}>${content}</g>`;
|
||||
}
|
||||
return match;
|
||||
}
|
||||
);
|
||||
|
||||
// Process crop mark links: <a href="crop-mark://top-left"><rect.../></a>
|
||||
result = result.replace(
|
||||
/<a[^>]*href="crop-mark:\/\/([^"]+)"[^>]*>(.*?)<\/a>/gs,
|
||||
(match, position, content) => {
|
||||
processedCount++;
|
||||
return `<g data-crop-position="${position}" data-element-type="crop-mark" class="crop-mark crop-mark-${position}">${content}</g>`;
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
content: result,
|
||||
processedCount
|
||||
};
|
||||
}
|
||||
|
||||
function processTypstSVG(inputPath, outputPath) {
|
||||
console.log(`📖 Reading: ${inputPath}`);
|
||||
|
||||
if (!fs.existsSync(inputPath)) {
|
||||
console.error(`❌ Input file not found: ${inputPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const svgContent = fs.readFileSync(inputPath, 'utf-8');
|
||||
|
||||
// Choose processing method
|
||||
const processor = JSDOM ? processWithJSDOM : processWithRegex;
|
||||
const processingMethod = JSDOM ? 'JSDOM' : 'regex fallback';
|
||||
|
||||
console.log(`⚙️ Processing with: ${processingMethod}`);
|
||||
|
||||
const result = processor(svgContent);
|
||||
|
||||
// Write processed SVG
|
||||
fs.writeFileSync(outputPath, result.content);
|
||||
|
||||
console.log(`✅ Processed: ${result.processedCount} annotations`);
|
||||
console.log(`💾 Output: ${outputPath}`);
|
||||
|
||||
return result.processedCount;
|
||||
}
|
||||
|
||||
function showUsage() {
|
||||
console.log(`
|
||||
SVG Post-Processor for Typst Soroban Templates
|
||||
|
||||
Usage:
|
||||
node svg-post-processor.js <input.svg> [output.svg]
|
||||
|
||||
Arguments:
|
||||
input.svg Path to the input SVG file with Typst link annotations
|
||||
output.svg Path for the processed output (optional, defaults to input-processed.svg)
|
||||
|
||||
Examples:
|
||||
node svg-post-processor.js gallery/basic-5.svg processed-basic-5.svg
|
||||
node svg-post-processor.js test-visible-crop.svg
|
||||
|
||||
Features:
|
||||
• Converts bead annotations to data-bead-* attributes
|
||||
• Converts crop mark annotations to data-crop-* attributes
|
||||
• Preserves all original SVG styling and structure
|
||||
• Adds CSS classes for crop marks
|
||||
|
||||
Dependencies:
|
||||
• jsdom (optional, for robust DOM processing)
|
||||
• Without jsdom: uses regex-based fallback processing
|
||||
`);
|
||||
}
|
||||
|
||||
// Main execution
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
||||
showUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const inputPath = args[0];
|
||||
const outputPath = args[1] || inputPath.replace(/\.svg$/, '-processed.svg');
|
||||
|
||||
try {
|
||||
const count = processTypstSVG(inputPath, outputPath);
|
||||
|
||||
if (count === 0) {
|
||||
console.log('⚠️ No Typst link annotations found in the SVG file');
|
||||
console.log(' Make sure the SVG was generated with annotated templates');
|
||||
} else {
|
||||
console.log('🎉 Processing complete!');
|
||||
console.log(' You can now use CSS selectors like:');
|
||||
console.log(' • [data-bead-type="heaven"] - Select heaven beads');
|
||||
console.log(' • [data-crop-position="top-left"] - Select crop boundaries');
|
||||
console.log(' • .crop-mark - Select all crop marks');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error processing SVG:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { processTypstSVG };
|
||||
@@ -1,4 +1,4 @@
|
||||
#let draw-soroban(value, columns: auto, show-empty: false, hide-inactive: false, bead-shape: "diamond", color-scheme: "monochrome", color-palette: "default", base-size: 1.0) = {
|
||||
#let draw-soroban(value, columns: auto, show-empty: false, hide-inactive: false, bead-shape: "diamond", color-scheme: "monochrome", color-palette: "default", base-size: 1.0, show-crop-marks: false, crop-margin: 10pt) = {
|
||||
// Parse the value into digits
|
||||
let digits = if type(value) == int {
|
||||
str(value).clusters().map(d => int(d))
|
||||
@@ -293,6 +293,89 @@
|
||||
stroke: none
|
||||
)
|
||||
)
|
||||
|
||||
// Add crop marks for consistent viewBox handling
|
||||
// These marks define the intended crop boundaries
|
||||
#let crop-mark-size = 2pt * base-size
|
||||
#let crop-mark-stroke = if show-crop-marks { 0.5pt } else { 0pt }
|
||||
#let crop-mark-color = if show-crop-marks { red } else { none }
|
||||
|
||||
// Calculate crop boundaries with margin
|
||||
#let crop-left = -crop-margin
|
||||
#let crop-right = total-width + crop-margin
|
||||
#let crop-top = -total-height / 2 - crop-margin
|
||||
#let crop-bottom = total-height / 2 + crop-margin
|
||||
|
||||
// Top-left crop mark
|
||||
#place(
|
||||
dx: crop-left,
|
||||
dy: crop-top,
|
||||
link("crop-mark://top-left",
|
||||
rect(
|
||||
width: crop-mark-size,
|
||||
height: crop-mark-size,
|
||||
fill: crop-mark-color,
|
||||
stroke: crop-mark-stroke + crop-mark-color
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Top-right crop mark
|
||||
#place(
|
||||
dx: crop-right - crop-mark-size,
|
||||
dy: crop-top,
|
||||
link("crop-mark://top-right",
|
||||
rect(
|
||||
width: crop-mark-size,
|
||||
height: crop-mark-size,
|
||||
fill: crop-mark-color,
|
||||
stroke: crop-mark-stroke + crop-mark-color
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Bottom-left crop mark
|
||||
#place(
|
||||
dx: crop-left,
|
||||
dy: crop-bottom - crop-mark-size,
|
||||
link("crop-mark://bottom-left",
|
||||
rect(
|
||||
width: crop-mark-size,
|
||||
height: crop-mark-size,
|
||||
fill: crop-mark-color,
|
||||
stroke: crop-mark-stroke + crop-mark-color
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Bottom-right crop mark
|
||||
#place(
|
||||
dx: crop-right - crop-mark-size,
|
||||
dy: crop-bottom - crop-mark-size,
|
||||
link("crop-mark://bottom-right",
|
||||
rect(
|
||||
width: crop-mark-size,
|
||||
height: crop-mark-size,
|
||||
fill: crop-mark-color,
|
||||
stroke: crop-mark-stroke + crop-mark-color
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Center reference mark (for debugging alignment)
|
||||
#if show-crop-marks {
|
||||
place(
|
||||
dx: total-width / 2 - crop-mark-size / 2,
|
||||
dy: -crop-mark-size / 2,
|
||||
link("crop-mark://center",
|
||||
circle(
|
||||
radius: crop-mark-size / 2,
|
||||
fill: rgb("#ff6b6b"),
|
||||
stroke: 0.5pt + rgb("#ff6b6b")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
<div class="stats">
|
||||
<div class="stats-info">
|
||||
<strong>6</strong> examples rendered
|
||||
• Generated on 9/16/2025 at 10:15:32 AM
|
||||
• Generated on 9/16/2025 at 10:26:11 AM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -241,6 +241,18 @@
|
||||
<g transform="translate(0 45)">
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 3 L 37.5 3 L 37.5 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 -97.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3 L 3 3 L 3 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(44.5 -97.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3 L 3 3 L 3 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 94.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3 L 3 3 L 3 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(44.5 94.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3 L 3 3 L 3 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
@@ -420,6 +432,18 @@
|
||||
<g transform="translate(0 36)">
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2.4 L 90 2.4 L 90 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 -81)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.4 L 2.4 2.4 L 2.4 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(97.60000000000001 -81)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.4 L 2.4 2.4 L 2.4 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 78.60000000000001)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.4 L 2.4 2.4 L 2.4 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(97.60000000000001 78.60000000000001)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.4 L 2.4 2.4 L 2.4 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
@@ -551,6 +575,18 @@
|
||||
<g transform="translate(0 54)">
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 3.6 L 90 3.6 L 90 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 -114)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3.6 L 3.6 3.6 L 3.6 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(96.39999999999999 -114)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3.6 L 3.6 3.6 L 3.6 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 110.39999999999999)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3.6 L 3.6 3.6 L 3.6 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(96.39999999999999 110.39999999999999)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3.6 L 3.6 3.6 L 3.6 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
@@ -634,6 +670,18 @@
|
||||
<g transform="translate(0 75)">
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 5 L 62.5 5 L 62.5 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 -152.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5 L 5 5 L 5 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(67.5 -152.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5 L 5 5 L 5 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 147.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5 L 5 5 L 5 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(67.5 147.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5 L 5 5 L 5 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
@@ -768,6 +816,18 @@
|
||||
<g transform="translate(0 30)">
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 75 2 L 75 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 -70)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2 L 2 2 L 2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(83 -70)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2 L 2 2 L 2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 68)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2 L 2 2 L 2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(83 68)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2 L 2 2 L 2 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
@@ -995,6 +1055,18 @@
|
||||
<g transform="translate(0 39)">
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2.6 L 130 2.6 L 130 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 -86.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.6 L 2.6 2.6 L 2.6 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(137.4 -86.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.6 L 2.6 2.6 L 2.6 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 83.89999999999999)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.6 L 2.6 2.6 L 2.6 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(137.4 83.89999999999999)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.6 L 2.6 2.6 L 2.6 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
@@ -58,6 +58,18 @@
|
||||
<g transform="translate(0 45)">
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 3 L 37.5 3 L 37.5 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 -97.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3 L 3 3 L 3 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(44.5 -97.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3 L 3 3 L 3 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 94.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3 L 3 3 L 3 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(44.5 94.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3 L 3 3 L 3 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 5.7 KiB |
@@ -106,6 +106,18 @@
|
||||
<g transform="translate(0 54)">
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 3.6 L 90 3.6 L 90 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 -114)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3.6 L 3.6 3.6 L 3.6 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(96.39999999999999 -114)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3.6 L 3.6 3.6 L 3.6 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 110.39999999999999)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3.6 L 3.6 3.6 L 3.6 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(96.39999999999999 110.39999999999999)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 3.6 L 3.6 3.6 L 3.6 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 11 KiB |
@@ -154,6 +154,18 @@
|
||||
<g transform="translate(0 36)">
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2.4 L 90 2.4 L 90 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 -81)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.4 L 2.4 2.4 L 2.4 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(97.60000000000001 -81)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.4 L 2.4 2.4 L 2.4 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 78.60000000000001)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.4 L 2.4 2.4 L 2.4 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(97.60000000000001 78.60000000000001)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.4 L 2.4 2.4 L 2.4 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
@@ -109,6 +109,18 @@
|
||||
<g transform="translate(0 30)">
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 75 2 L 75 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 -70)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2 L 2 2 L 2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(83 -70)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2 L 2 2 L 2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 68)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2 L 2 2 L 2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(83 68)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2 L 2 2 L 2 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
@@ -202,6 +202,18 @@
|
||||
<g transform="translate(0 39)">
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2.6 L 130 2.6 L 130 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 -86.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.6 L 2.6 2.6 L 2.6 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(137.4 -86.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.6 L 2.6 2.6 L 2.6 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 83.89999999999999)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.6 L 2.6 2.6 L 2.6 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(137.4 83.89999999999999)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 2.6 L 2.6 2.6 L 2.6 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
@@ -58,6 +58,18 @@
|
||||
<g transform="translate(0 75)">
|
||||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 5 L 62.5 5 L 62.5 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 -152.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5 L 5 5 L 5 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(67.5 -152.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5 L 5 5 L 5 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(-10 147.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5 L 5 5 L 5 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(67.5 147.5)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5 L 5 5 L 5 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 5.7 KiB |
@@ -100,6 +100,8 @@
|
||||
font-family: "DejaVu Sans",
|
||||
scale-factor: 1.0,
|
||||
color-palette: "default",
|
||||
show-crop-marks: false,
|
||||
crop-margin: 10pt,
|
||||
) = {
|
||||
// Set page size to exact card dimensions
|
||||
set page(
|
||||
@@ -145,4 +147,86 @@
|
||||
#create-colored-numeral(number, color-scheme, colored-numerals, font-size * scale-factor, color-palette: color-palette)
|
||||
]
|
||||
}
|
||||
|
||||
// Add crop marks for consistent viewBox handling
|
||||
// These marks define the intended crop boundaries for both sides
|
||||
#let crop-mark-size = 2pt
|
||||
#let crop-mark-stroke = if show-crop-marks { 0.5pt } else { 0pt }
|
||||
#let crop-mark-color = if show-crop-marks { red } else { none }
|
||||
|
||||
// Calculate crop boundaries with margin
|
||||
#let crop-left = -crop-margin
|
||||
#let crop-right = width + crop-margin
|
||||
#let crop-top = -crop-margin
|
||||
#let crop-bottom = height + crop-margin
|
||||
|
||||
// Top-left crop mark
|
||||
#place(
|
||||
dx: crop-left,
|
||||
dy: crop-top,
|
||||
link("crop-mark://top-left",
|
||||
rect(
|
||||
width: crop-mark-size,
|
||||
height: crop-mark-size,
|
||||
fill: crop-mark-color,
|
||||
stroke: crop-mark-stroke + crop-mark-color
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Top-right crop mark
|
||||
#place(
|
||||
dx: crop-right - crop-mark-size,
|
||||
dy: crop-top,
|
||||
link("crop-mark://top-right",
|
||||
rect(
|
||||
width: crop-mark-size,
|
||||
height: crop-mark-size,
|
||||
fill: crop-mark-color,
|
||||
stroke: crop-mark-stroke + crop-mark-color
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Bottom-left crop mark
|
||||
#place(
|
||||
dx: crop-left,
|
||||
dy: crop-bottom - crop-mark-size,
|
||||
link("crop-mark://bottom-left",
|
||||
rect(
|
||||
width: crop-mark-size,
|
||||
height: crop-mark-size,
|
||||
fill: crop-mark-color,
|
||||
stroke: crop-mark-stroke + crop-mark-color
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Bottom-right crop mark
|
||||
#place(
|
||||
dx: crop-right - crop-mark-size,
|
||||
dy: crop-bottom - crop-mark-size,
|
||||
link("crop-mark://bottom-right",
|
||||
rect(
|
||||
width: crop-mark-size,
|
||||
height: crop-mark-size,
|
||||
fill: crop-mark-color,
|
||||
stroke: crop-mark-stroke + crop-mark-color
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Center reference mark for debugging alignment
|
||||
#place(
|
||||
dx: width / 2 - crop-mark-size / 2,
|
||||
dy: height / 2 - crop-mark-size / 2,
|
||||
link("crop-mark://center",
|
||||
rect(
|
||||
width: crop-mark-size,
|
||||
height: crop-mark-size,
|
||||
fill: crop-mark-color,
|
||||
stroke: crop-mark-stroke + crop-mark-color
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||