Files
soroban-abacus-flashcards/packages/templates/extract-viewbox.js
Thomas Hallock bda5bc6c0e fix: prevent database imports from being bundled into client code
**Problem:**
- player-ownership.ts imported drizzle-orm and @/db at top level
- When RoomMemoryPairsProvider imported client-safe utilities, Webpack bundled ALL imports including database code
- This caused hydration error: "The 'original' argument must be of type Function"
- Node.js util.promisify was being called in browser context

**Solution:**
1. Created player-ownership.client.ts with ONLY client-safe utilities
   - No database imports
   - Safe to import from 'use client' components
   - Contains: buildPlayerOwnershipFromRoomData(), buildPlayerMetadata(), helper functions

2. Updated player-ownership.ts to re-export client utilities and add server-only functions
   - Re-exports everything from .client.ts
   - Adds buildPlayerOwnershipMap() (async, database-backed)
   - Safe to import from server components/API routes

3. Updated RoomMemoryPairsProvider to import from .client.ts

**Result:**
- No more hydration errors on /arcade/room
- Client bundle doesn't include database code
- Server code can still use both client and server utilities

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 11:40:46 -05:00

235 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// Extract viewBox from crop marks with proper transform accumulation
// This correctly walks up the SVG hierarchy to calculate final coordinates
const fs = require("fs");
function parseSVGWithTransforms(svgContent) {
// Parse SVG structure to find crop marks in both CLI and Node.js formats
const cropMarks = [];
// Method 1: Look for crop-mark:// links (Node.js format)
const linkMatches = svgContent.matchAll(
/xlink:href="crop-mark:\/\/(left|right|top|bottom)"/g,
);
const transformRegex = /transform="translate\(([^)]+)\)"/g;
// Convert content to searchable text for easier parsing
const searchableContent = svgContent.replace(/\n/g, " ").replace(/\s+/g, " ");
for (const linkMatch of linkMatches) {
const direction = linkMatch[1];
const beforeLink = searchableContent.substring(0, linkMatch.index);
// Find all transforms before this link
let totalX = 0,
totalY = 0;
let transformMatch;
transformRegex.lastIndex = 0; // Reset regex
while ((transformMatch = transformRegex.exec(beforeLink)) !== null) {
const coords = transformMatch[1].split(/[,\s]+/).map(Number);
totalX += coords[0] || 0;
totalY += coords[1] || 0;
}
cropMarks.push({
type: "crop-mark",
direction,
finalTransform: { x: totalX, y: totalY },
});
}
// Method 2: Fallback to CLI format (multi-line with path elements)
if (cropMarks.length === 0) {
const lines = svgContent.split("\n");
const stack = []; // Track parent transforms
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const indent = line.match(/^(\s*)/)[1].length;
// Track opening/closing tags to maintain hierarchy
if (line.includes("<g ") && line.includes("transform=")) {
const transformMatch = line.match(/transform="translate\(([^)]+)\)"/);
if (transformMatch) {
const coords = transformMatch[1].split(/[,\s]+/).map(Number);
const transform = { x: coords[0] || 0, y: coords[1] || 0 };
// Maintain stack based on indentation level
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
stack.pop();
}
// Calculate accumulated transform
const accumulated = stack.reduce(
(acc, parent) => ({
x: acc.x + parent.transform.x,
y: acc.y + parent.transform.y,
}),
{ x: 0, y: 0 },
);
const finalTransform = {
x: accumulated.x + transform.x,
y: accumulated.y + transform.y,
};
stack.push({ indent, transform, finalTransform });
// Check if this contains a tiny crop mark point (not the dashed lines)
if (
i + 1 < lines.length &&
lines[i + 1].includes('fill="#ff4136"') &&
lines[i + 1].includes("0.1 0.1")
) {
cropMarks.push({
type: "crop-mark",
finalTransform,
line: i,
});
}
}
}
}
}
return cropMarks;
}
function extractViewBoxFromCropMarks(svgPath) {
console.log(
`📐 Analyzing crop marks with transform accumulation in ${svgPath}...`,
);
if (!fs.existsSync(svgPath)) {
throw new Error(`SVG file not found: ${svgPath}`);
}
const svgContent = fs.readFileSync(svgPath, "utf8");
// Use proper SVG parsing with transform accumulation
const elements = parseSVGWithTransforms(svgContent);
const cropMarkElements = elements.filter((e) => e.type === "crop-mark");
if (cropMarkElements.length === 0) {
console.log(" ⚠️ No crop marks found in this SVG");
return null;
}
// Extract positions from accumulated transforms
const cropMarks = {};
cropMarkElements.forEach((element, index) => {
let markType;
if (element.direction) {
// Node.js format: has explicit direction
markType = element.direction;
} else {
// CLI format: use order-based mapping
const markTypes = ["left", "right", "top", "bottom"];
markType = markTypes[index];
}
if (markType) {
const { x, y } = element.finalTransform;
cropMarks[markType] = { x, y };
console.log(` ✅ Found ${markType} at (${x}, ${y}) [accumulated]`);
}
});
// Calculate viewBox from accumulated positions
const positions = Object.values(cropMarks);
const minX = Math.min(...positions.map((p) => p.x));
const maxX = Math.max(...positions.map((p) => p.x));
const minY = Math.min(...positions.map((p) => p.y));
const maxY = Math.max(...positions.map((p) => p.y));
const width = maxX - minX;
const height = maxY - minY;
const viewBox = `${minX} ${minY} ${width} ${height}`;
console.log(`📏 Calculated viewBox: "${viewBox}"`);
console.log(` 📊 Dimensions: ${width} × ${height}`);
console.log(` 📍 Origin: (${minX}, ${minY})`);
return {
viewBox,
width,
height,
minX,
minY,
maxX,
maxY,
cropMarks,
};
}
function updateSVGViewBox(inputPath, outputPath = null) {
const result = extractViewBoxFromCropMarks(inputPath);
if (!result) {
console.log("❌ Cannot update viewBox - no crop marks found");
return false;
}
const svgContent = fs.readFileSync(inputPath, "utf8");
// Update the viewBox attribute and SVG dimensions to match aspect ratio
let updatedSVG = svgContent.replace(
/viewBox="[^"]*"/,
`viewBox="${result.viewBox}"`,
);
// Update width and height to match the viewBox dimensions for correct aspect ratio
updatedSVG = updatedSVG.replace(/width="[^"]*"/, `width="${result.width}pt"`);
updatedSVG = updatedSVG.replace(
/height="[^"]*"/,
`height="${result.height}pt"`,
);
const output =
outputPath || inputPath.replace(".svg", "-cropped-correct.svg");
fs.writeFileSync(output, updatedSVG);
console.log(`✅ Updated SVG saved to ${output}`);
return true;
}
// CLI usage
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log(
"Usage: node extract-viewbox-correct.js <svg-file> [output-file]",
);
console.log("");
console.log("Examples:");
console.log(
" node extract-viewbox-correct.js gallery/debug-crop-marks-89.svg",
);
console.log(
" node extract-viewbox-correct.js gallery/crop-single-1.svg cropped.svg",
);
process.exit(1);
}
const [inputFile, outputFile] = args;
try {
if (outputFile) {
updateSVGViewBox(inputFile, outputFile);
} else {
extractViewBoxFromCropMarks(inputFile);
}
} catch (error) {
console.error("❌ Error:", error.message);
process.exit(1);
}
}
module.exports = { extractViewBoxFromCropMarks, updateSVGViewBox };