AI Prompt: Plate-Level Preview Cache — Dynamic Assembly from Cached Base Plates¶
Purpose: Replace the full-preview-per-dimension cache (16,471 files, ~32 GB) with a plate-level cache of only 200 base plates (~41 MB), assembled on the fly with dynamically generated border geometry to produce previews for any dimension at any resolution Estimated Effort: 12–16 hours Prerequisites: Working
@forma3d/gridflock-corelibrary, existinggeneratePreviewStl()function, existingcombineStlBuffers(), existingcalculatePlateSet()Research: This prompt, Section 1 (Findings & Calculations) Output: A plate-level caching system that eliminates the need for pre-populating 16,471+ full preview STLs, supports any input resolution (1mm, 0.5cm, or continuous), and produces previews in under 1 second instead of 12–30 seconds Status: ✅ DONE
1. Findings & Calculations¶
Background¶
The current preview cache stores one fully combined STL per dimension pair. For 0.5 cm resolution (100–1000 mm, step 5 mm), this requires 16,471 files at ~32 GB of disk space. The initial idea was to support 1 mm input resolution (step 1 mm) for better drawer fit precision, but that would require 406,351 full preview files at ~853 GB — clearly impractical.
An alternative idea emerged: cache individual plates and assemble previews on the fly. This was investigated by enumerating every plate across all preview combinations at both resolutions.
Key calculation: what makes a plate unique?¶
Each preview consists of multiple plates. A plate's STL geometry is determined by:
- Grid size
[cols, rows]— 1 to 6 in each dimension (constrained by printer bed) - Connector edges
[N, E, S, W]— 4 booleans indicating which edges connect to neighbors - Border dimensions
{left, right, front, back}— solid border widths for exact drawer fit
For the preview, connector type (intersection-puzzle), magnets (disabled), and printer profile (bambu-a1, 256×256 mm bed) are fixed constants.
The analysis¶
A script was written to enumerate all plates across all preview combinations at both resolutions. For each combination, calculatePlateSet() was run to determine the plates, and each plate was assigned a signature key of gridCols×gridRows-NESW-borderL,borderR,borderF,borderB. Unique signatures were collected.
Results¶
| Metric | 0.5 cm steps (5 mm) | 1 mm steps |
|---|---|---|
| Total preview combos (W×H) | 16,471 | 406,351 |
| Total plate instances across all previews | 132,456 | 3,270,902 |
| Unique plate geometries (incl. border) | 20,576 | 147,500 |
| Unique base plates (grid size + connectors, ignoring border) | 200 | 200 |
| Reuse ratio (instances ÷ unique) | 6.4× | 22.2× |
Critical insight: the border is what explodes the count¶
- There are only 200 unique base plate geometries (grid size + connector edge pattern). This number is identical for both resolutions.
- The border creates 12,014 unique combinations, inflating 200 base plates to 147,500 unique plate geometries at 1 mm resolution.
- Border values range from 0.5 mm to 20.5 mm in 0.5 mm steps (41 non-zero values per side).
- Interior plates always have zero border on all sides (only outer/edge plates have non-zero border).
Why the base plate count is resolution-independent¶
The border remainder is computed as (drawerMm mod 42) / 2. Since the Gridfinity grid unit is 42 mm and drawer dimensions are always integers (in mm), the set of possible remainders is always {0, 1, 2, ..., 41}, and the set of possible border values is always {0, 0.5, 1.0, ..., 20.5} — regardless of whether the input step is 1 mm or 5 mm. What changes between resolutions is only which (gridCount, remainder) pairs occur, but the grid segmentation algorithm (distributeIdeal for X, distributeIncremental with stagger for Y) produces the same set of grid segment sizes (1–6) regardless.
The proposed architecture¶
Instead of caching full assembled previews or individual plates with border baked in:
| Approach | Cache files | Cache size | Supports 1 mm? | Cold preview time |
|---|---|---|---|---|
| Full preview cache (current) | 16,471 | ~32 GB | No (~853 GB needed) | None (pre-populated) |
| Individual plates with border | 147,500 | ~29 GB | Yes but huge | Assembly only |
| Base plates + dynamic border | 200 | ~41 MB | Yes, any resolution | < 1 second |
The approach:
1. Pre-generate and cache 200 base plates (zero border, all grid size + connector edge combinations) — ~41 MB total
2. At preview time: look up cached base plates, generate simple border rectangles as STL on the fly, combine everything with combineStlBuffers()
3. The border geometry is trivial: a flat rectangular extrusion (12 triangles per border strip, generated in microseconds)
Why this is fast enough¶
calculatePlateSet(): pure math, runs in < 1 ms- Reading 200 cached plates from disk (or memory): negligible — most previews use 2–16 plates
- Border STL generation: 12 triangles per border strip, no CSG operations — microseconds
combineStlBuffers(): binary buffer concatenation with vertex offset — milliseconds- Total estimated time: 10–100 ms vs current 12–30 seconds for cold cache misses
🎯 Mission¶
Replace the current full-preview-per-dimension caching system with a plate-level cache that:
- Pre-generates only 200 base plate STLs (grid size × connector edge combinations, zero border)
- At preview request time, assembles a complete preview by:
- Looking up the required base plates from the cache
- Generating border geometry on the fly as simple rectangular STL
- Combining everything with offsets using the existing
combineStlBuffers() - Supports any input resolution (1 mm, 0.5 cm, or continuous) with the same 200-file cache
- Produces preview response times under 1 second for any dimension pair
📐 Architecture¶
Current flow (full preview cache)¶
Client: POST /api/v1/gridflock/preview { widthMm, heightMm }
│
▼
GridflockPreviewService.generatePreview()
├── Cache lookup: preview-{w}x{h}.stl
│ ├── HIT → return cached full preview STL
│ └── MISS ↓
├── generatePreviewStl(w, h) ← 12–30 seconds
│ ├── calculatePlateSet()
│ ├── generatePlatesParallel() ← heavy JSCAD CSG
│ └── combineStlBuffers()
└── Save to disk, return STL
New flow (plate-level cache + dynamic assembly)¶
Client: POST /api/v1/gridflock/preview { widthMm, heightMm }
│
▼
GridflockPreviewService.generatePreview()
├── calculatePlateSet(w, h) ← < 1 ms (pure math)
│
├── For each plate in the set:
│ ├── Compute base plate key: {cols}x{rows}-{NESW}
│ ├── Load base plate STL from plate cache ← < 1 ms (disk/memory)
│ ├── Generate border STL strips on the fly ← < 1 ms (trivial geometry)
│ └── Collect StlPlacement with offsets
│
└── combineStlBuffers(placements) ← < 10 ms (buffer concat)
│
▼
Binary STL response ← Total: 10–100 ms
Cache structure¶
/data/gridflock/plate-cache/
├── plate-1x1-0000.stl # 1×1 grid, no connectors
├── plate-1x1-0100.stl # 1×1 grid, east connector only
├── plate-1x1-0110.stl # 1×1 grid, east + south connectors
├── ...
├── plate-6x6-1111.stl # 6×6 grid, all 4 connectors
└── 200 files total (~41 MB)
Naming convention: plate-{cols}x{rows}-{N}{E}{S}{W}.stl
- {cols} and {rows}: grid size (1–6)
- {N}{E}{S}{W}: connector edges as 0/1 (e.g., 1010 = north + south connectors)
📋 Implementation¶
Phase 1: Base Plate Cache Key & Generator (3–4 hours)¶
Priority: P0 | Impact: Critical | Dependencies: None
Create the system for generating and caching the 200 base plates.
1. Define the base plate cache key function¶
In libs/gridflock-core/src/lib/preview-generator.ts, add a function that computes the cache key for a base plate:
export function basePlateCacheKey(
gridSize: [number, number],
connectorEdges: [boolean, boolean, boolean, boolean],
): string {
const connStr = connectorEdges.map((c) => (c ? '1' : '0')).join('');
return `plate-${gridSize[0]}x${gridSize[1]}-${connStr}.stl`;
}
This key is border-independent: it identifies a plate solely by grid size and connector pattern. All 200 unique keys can be enumerated from the range of valid grid sizes (1–6 × 1–6) and valid connector edge patterns.
2. Create a base plate generation function¶
Create a new function generateBasePlateStl() in libs/gridflock-core/src/lib/preview-generator.ts that generates a single plate STL with zero border:
export function generateBasePlateStl(
gridSize: [number, number],
connectorEdges: [boolean, boolean, boolean, boolean],
options: PlateGenerationOptions,
): Buffer {
const result = generateGridFlockPlate({
gridSize,
connectorType: options.connectorType,
magnets: options.magnets,
edgePuzzle: options.edgePuzzle,
plateNumber: false,
connectorEdges,
border: NO_BORDER,
});
return result.stlBuffer;
}
Note: plateNumber is set to false for all cached base plates. Plate numbering is a per-assembly concern and would need to be handled differently (see Phase 4 nice-to-haves).
3. Create a function to enumerate all 200 base plate keys¶
export function enumerateAllBasePlateKeys(): Array<{
key: string;
gridSize: [number, number];
connectorEdges: [boolean, boolean, boolean, boolean];
}> {
const results: Array<{...}> = [];
// Enumerate all valid (gridSize, connectorEdges) combinations
// that actually appear across the full dimension range
for (let w = 100; w <= 1000; w += 1) {
for (let h = w; h <= 1000; h += 1) {
const plateSet = calculatePlateSet({
targetWidthMm: w, targetDepthMm: h,
printerProfile: PRINTER_PROFILES['bambu-a1'],
...DEFAULT_PREVIEW_OPTIONS,
});
for (const plate of plateSet.plates) {
const key = basePlateCacheKey(plate.gridSize, plate.connectorEdges);
if (!seen.has(key)) {
seen.add(key);
results.push({ key, gridSize: plate.gridSize, connectorEdges: plate.connectorEdges });
}
}
}
}
return results;
}
Important: This enumeration approach is slow (406k iterations). A smarter approach: since the plate segmentation is deterministic and only depends on totalGridX, totalGridY, and effectiveMaxGrid, enumerate the 22 possible total grid counts per axis (2–23) instead. For each count, run the distribution algorithms to discover all plate grid sizes and connector patterns. This produces the same 200 keys in milliseconds.
The recommended approach:
export function enumerateAllBasePlateKeys(): Array<{
key: string;
gridSize: [number, number];
connectorEdges: [boolean, boolean, boolean, boolean];
}> {
const seen = new Set<string>();
const results: Array<{...}> = [];
// totalGridX and totalGridY range from 2 (floor(100/42)) to 23 (floor(1000/42))
// effectiveMaxGrid can be 5 or 6 depending on border size
for (const effectiveMax of [5, 6]) {
for (let totalX = 2; totalX <= 23; totalX++) {
for (let totalY = totalX; totalY <= 23; totalY++) {
// Simulate calculatePlateSet logic with this (totalX, totalY, effectiveMax)
// Collect unique (gridSize, connectorEdges) tuples
// ... (replicate the distribution + stagger logic)
}
}
}
return results;
}
Either approach is acceptable — the important thing is that the result is exactly 200 unique keys matching the analysis.
4. Export new functions from gridflock-core¶
Add to libs/gridflock-core/src/index.ts:
export {
generatePreviewStl,
generateBasePlateStl,
basePlateCacheKey,
enumerateAllBasePlateKeys,
type PreviewStlOptions,
} from './lib/preview-generator';
Phase 2: Border STL Generator (2–3 hours)¶
Priority: P0 | Impact: Critical | Dependencies: None (parallel with Phase 1)
Create a lightweight function that generates a border strip as a binary STL buffer. This replaces the JSCAD CSG border that is currently baked into each plate's base geometry.
1. Create generateBorderStl() in gridflock-core¶
Create libs/gridflock-core/src/lib/border-generator.ts:
The border is a simple rectangular solid (box/cuboid) that sits adjacent to the base plate. It has the same height as the plate's base (from -extraHeight to PROFILE_HEIGHT_MM) and the width/depth specified by the border dimension.
For a plate with border {left: 10, right: 0, front: 8, back: 0}, the border consists of up to 4 rectangular strips placed around the plate. Each strip is a simple box — no grid cells, no connectors, no magnets. Just a solid extrusion.
Binary STL for a box is fixed-size: 12 triangles (2 per face × 6 faces) = 12 × 50 bytes + 84 byte header = 684 bytes. This is trivially fast to generate — no CSG library needed.
export function generateBorderStripStl(
width: number,
depth: number,
height: number,
offsetX: number,
offsetY: number,
offsetZ: number,
): Buffer {
// Generate a binary STL cuboid directly (no JSCAD dependency)
// 12 triangles for a box: 2 per face × 6 faces
// Each triangle: 12 bytes normal + 36 bytes vertices + 2 bytes attribute = 50 bytes
// Total: 80 (header) + 4 (count) + 12 * 50 = 684 bytes
const HEADER_SIZE = 80;
const TRIANGLE_COUNT = 12;
const buf = Buffer.alloc(HEADER_SIZE + 4 + TRIANGLE_COUNT * 50);
buf.write('GridFlock Border Strip', 0, 'ascii');
buf.writeUInt32LE(TRIANGLE_COUNT, HEADER_SIZE);
// Define 8 vertices of the box
const x0 = offsetX;
const x1 = offsetX + width;
const y0 = offsetY;
const y1 = offsetY + depth;
const z0 = offsetZ;
const z1 = offsetZ + height;
// Write 12 triangles (2 per face)
// ... (standard box triangulation)
}
The key insight: this function generates raw binary STL without any dependency on JSCAD. It's pure buffer manipulation — microseconds of execution time.
2. Create generatePlateBorderStls() helper¶
Given a PlateSpec (from calculatePlateSet), generate the border strip STL buffers for all non-zero border sides:
export interface BorderStlPlacement {
stlBuffer: Buffer;
offsetX: number;
offsetY: number;
}
export function generatePlateBorderStls(
plate: PlateSpec,
extraHeight: number,
): BorderStlPlacement[] {
const placements: BorderStlPlacement[] = [];
const totalHeight = PROFILE_HEIGHT_MM + extraHeight;
const zBottom = -extraHeight;
const gridWidth = plate.gridSize[0] * GRID_UNIT_MM;
const gridDepth = plate.gridSize[1] * GRID_UNIT_MM;
if (plate.border.left > 0) {
placements.push({
stlBuffer: generateBorderStripStl(
plate.border.left, gridDepth + plate.border.front + plate.border.back,
totalHeight, -plate.border.left, -plate.border.front, zBottom,
),
offsetX: 0,
offsetY: 0,
});
}
// ... right, front, back border strips
// Handle corner overlaps (L-shaped borders at corners)
return placements;
}
Corner handling: Where two border strips meet at a corner, they can either overlap (creating internal faces — acceptable for preview) or be carefully sized to avoid overlap. For a preview, slight overlaps are invisible and acceptable. The simplest approach is:
- Left/right strips span the full depth (including front/back border)
- Front/back strips span only the grid width (between left and right borders)
This avoids overlap entirely with no visible seams.
3. Export from gridflock-core¶
Add to libs/gridflock-core/src/index.ts:
export {
generateBorderStripStl,
generatePlateBorderStls,
type BorderStlPlacement,
} from './lib/border-generator';
4. Verify visual quality¶
The border strips must align seamlessly with the base plate's outer edge. Since both the base plate (with NO_BORDER) and the border strips share the same Z range and are placed edge-to-edge, there should be no visible gap. However, the base plate has rounded corners (cornerRadius: 4mm) which means the border strip's inner edge (which is a straight line) will overlap slightly with the rounded corner area. For a preview visualization, this is invisible.
If visual perfection is required, the border strip could be generated with a matching corner cutout, but this adds complexity for negligible visual benefit in a 3D preview.
Phase 3: Preview Assembly Service (4–5 hours)¶
Priority: P0 | Impact: Critical | Dependencies: Phase 1, Phase 2
Modify the preview service to assemble previews from cached base plates + dynamic borders.
1. Create PlateCache service¶
Create a new service PlateCache in apps/gridflock-service/src/gridflock/plate-cache.service.ts:
@Injectable()
export class PlateCacheService {
private readonly cache = new Map<string, Buffer>();
private readonly cacheDir: string;
constructor() {
this.cacheDir = process.env['PLATE_CACHE_PATH'] || '/data/gridflock/plate-cache';
}
async initialize(): Promise<void> {
// Load all 200 base plate STLs into memory (~41 MB total)
// This is small enough to keep entirely in memory
const entries = await readdir(this.cacheDir);
for (const filename of entries) {
if (filename.startsWith('plate-') && filename.endsWith('.stl')) {
const buffer = await readFile(join(this.cacheDir, filename));
this.cache.set(filename, buffer);
}
}
}
getPlate(key: string): Buffer | null {
return this.cache.get(key) ?? null;
}
isFullyPopulated(): boolean {
return this.cache.size >= 200;
}
}
The cache is loaded into memory at startup. At ~41 MB for 200 files, this is negligible. Disk reads are eliminated entirely after initialization.
2. Modify GridflockPreviewService.generatePreview()¶
Replace the current flow (cache lookup → miss → generate full preview) with the new assembly flow:
async generatePreview(dto: PreviewGridDto): Promise<Buffer> {
const w = Math.max(dto.widthMm, dto.heightMm);
const h = Math.min(dto.widthMm, dto.heightMm);
// Step 1: Calculate plate set (pure math, < 1 ms)
const plateSet = calculatePlateSet({
targetWidthMm: w,
targetDepthMm: h,
printerProfile: PRINTER_PROFILES['bambu-a1'],
...DEFAULT_PREVIEW_OPTIONS,
});
// Step 2: Assemble preview from cached plates + dynamic borders
const placements: StlPlacement[] = [];
const xOffsets = computeUniformOffsets(plateSet.plates, 0);
const yOffsetsByColumn = computeOffsetsPerColumn(plateSet.plates);
for (const plate of plateSet.plates) {
const [px, py] = plate.position;
const baseOffsetX = xOffsets[px] ?? 0;
const colOffsets = yOffsetsByColumn.get(px) ?? [0];
const baseOffsetY = colOffsets[py] ?? 0;
// Look up the base plate (no border) from cache
const baseKey = basePlateCacheKey(plate.gridSize, plate.connectorEdges);
const basePlateStl = this.plateCacheService.getPlate(baseKey);
if (!basePlateStl) {
// Fallback: generate the full plate with border (slow path)
this.logger.warn(`Base plate cache miss: ${baseKey}, generating on the fly`);
// ... fallback to existing generation
}
// Add the base plate
placements.push({
stlBuffer: basePlateStl,
offsetX: baseOffsetX,
offsetY: baseOffsetY,
});
// Generate border strips on the fly and add them
const borderPlacements = generatePlateBorderStls(plate, extraHeight);
for (const bp of borderPlacements) {
placements.push({
stlBuffer: bp.stlBuffer,
offsetX: baseOffsetX + bp.offsetX,
offsetY: baseOffsetY + bp.offsetY,
});
}
}
// Step 3: Combine all STL buffers (fast buffer concat, < 10 ms)
return combineStlBuffers(placements);
}
3. Remove the full-preview disk cache dependency¶
The current full-preview cache (preview-{w}x{h}.stl files) is no longer needed for preview generation. However, keep the existing cache-reading code as a backward-compatible fast path: if a full preview file exists on disk (from the previously pre-populated cache), serve it directly. This ensures zero regression during the transition.
async generatePreview(dto: PreviewGridDto): Promise<Buffer> {
const w = Math.max(dto.widthMm, dto.heightMm);
const h = Math.min(dto.widthMm, dto.heightMm);
// Fast path: check if a full pre-generated preview exists (legacy cache)
const legacyFilename = `preview-${w}x${h}.stl`;
const legacyCached = await this.readFromDisk(legacyFilename);
if (legacyCached) {
this.logger.log(`Legacy preview cache hit: ${legacyFilename}`);
return legacyCached;
}
// New path: assemble from plate-level cache
return this.assemblePreviewFromPlateCache(w, h);
}
Over time, the legacy full-preview cache can be removed entirely.
4. Add optional result caching¶
Even though assembly is fast (~10–100 ms), optionally cache the assembled result in memory (LRU cache) for repeat requests to the same dimension pair:
private readonly assembledCache = new LRUCache<string, Buffer>({
maxSize: 100 * 1024 * 1024, // 100 MB max
sizeCalculation: (buf) => buf.length,
});
This is optional — the assembly is fast enough that even without caching, the response time is excellent.
5. Register services and update module¶
Update apps/gridflock-service/src/gridflock/gridflock.module.ts:
- Add PlateCacheService to providers
- Initialize the plate cache on module startup (OnModuleInit)
Phase 4: Base Plate Population Script (2–3 hours)¶
Priority: P0 | Impact: Critical | Dependencies: Phase 1
Create a script to pre-generate all 200 base plates.
1. Create scripts/populate-plate-cache.ts¶
This script is dramatically simpler than the full preview population script because there are only 200 files to generate.
import {
generateBasePlateStl,
enumerateAllBasePlateKeys,
DEFAULT_PREVIEW_OPTIONS,
} from '@forma3d/gridflock-core';
async function main() {
const outDir = parseArgs().out ?? './plate-cache-output';
const plates = enumerateAllBasePlateKeys();
console.log(`Generating ${plates.length} base plates...`);
for (const { key, gridSize, connectorEdges } of plates) {
const outPath = join(outDir, key);
if (existsSync(outPath)) { skipped++; continue; }
const stl = generateBasePlateStl(gridSize, connectorEdges, DEFAULT_PREVIEW_OPTIONS);
writeFileSync(outPath, stl);
generated++;
}
console.log(`Done! Generated ${generated}, skipped ${skipped}.`);
}
Expected execution time: 2–5 minutes (200 plates, ~1–2 seconds each). Compare to 10–69 hours for the full preview population.
2. Update the upload script¶
Modify scripts/upload-preview-cache.sh to also support uploading the plate cache:
# Upload plate cache (200 files, ~41 MB)
rsync -avz --progress \
"$LOCAL_DIR/" \
"$REMOTE_HOST:/data/gridflock/plate-cache/"
Or create a separate scripts/upload-plate-cache.sh for clarity.
Phase 5: Shopify UI — 1 mm Resolution Validation (2–3 hours)¶
Priority: P1 | Impact: High | Dependencies: None (parallel with Phases 1–4)
Add input validation to the Shopify configurator. Since the plate-level cache supports any resolution with the same 200 cached base plates, we can offer full 1 mm (0.1 cm) precision — giving customers an exact-fit baseplate for their drawer. The only constraints are the min/max range and sub-millimeter values.
1. Validation rules¶
| Rule | Behavior | Message |
|---|---|---|
| Value < 10 cm | Clamp to 10 cm | Red error: "Minimum drawer size is 10 cm" |
| Value > 100 cm | Clamp to 100 cm | Red error: "Maximum drawer size is 100 cm" |
| Value has sub-millimeter precision (e.g., 10.15 cm) | Round to nearest 0.1 cm | Green info: "Rounded to 10.2 cm (1 mm resolution)" |
| Value in range and on 0.1 cm increment | Accept as-is | No message |
Rounding behavior: Round down (floor) to the nearest 0.1 cm (= 1 mm). Rounding down ensures the baseplate is never larger than the drawer — a baseplate that is 0.9 mm too small has a negligible gap, but a baseplate that is 0.1 mm too large won't fit at all. Example: 10.17 → 10.1, 10.99 → 10.9, 45.67 → 45.6.
function snapToMm(value) {
return Math.floor(value * 10) / 10;
}
2. Modify configurator.liquid¶
In deployment/shopify-theme/sections/configurator.liquid, update the dimension inputs:
- Change
min="10"tomin="10"(already correct) - Change
max="200"tomax="100" - Change
step="0.5"tostep="0.1"(1 mm precision) - Add
inputevent listener for validation and feedback
3. Add validation feedback elements¶
Add message containers below each dimension input:
<div class="dimension-feedback" data-feedback-for="drawer-width" style="display:none;">
<span class="feedback-text"></span>
</div>
Styling:
- Red message (error): color: #dc2626; font-size: 0.875rem;
- Green message (info): color: #16a34a; font-size: 0.875rem;
4. Add validation JavaScript¶
In the configurator JavaScript (either inline in configurator.liquid or in configurator-3d.js):
function validateDimension(inputEl, feedbackEl) {
const raw = parseFloat(inputEl.value);
const feedback = feedbackEl.querySelector('.feedback-text');
if (isNaN(raw)) return;
if (raw < 10) {
inputEl.value = '10.0';
feedback.textContent = 'Minimum drawer size is 10 cm';
feedback.style.color = '#dc2626';
feedbackEl.style.display = 'block';
return;
}
if (raw > 100) {
inputEl.value = '100.0';
feedback.textContent = 'Maximum drawer size is 100 cm';
feedback.style.color = '#dc2626';
feedbackEl.style.display = 'block';
return;
}
const snapped = Math.floor(raw * 10) / 10;
if (Math.abs(snapped - raw) > 0.001) {
inputEl.value = snapped.toFixed(1);
feedback.textContent = `Rounded down to ${snapped.toFixed(1)} cm (1 mm resolution)`;
feedback.style.color = '#16a34a';
feedbackEl.style.display = 'block';
return;
}
feedbackEl.style.display = 'none';
}
5. Update backend validation to match¶
Update the backend DTOs and services to reflect the new 10–100 cm range:
apps/order-service/src/storefront/dto/grid-checkout.dto.ts: Change@Max(1200)to@Max(1000)for width, keep or adjust heightapps/gridflock-service/src/gridflock/dto/preview-grid.dto.ts: Change@Max(2000)to@Max(1000)SystemConfigkeygridflock.max_dimension_mm: Default to 1000
✅ Validation Checklist¶
Phase 1: Base Plate Cache Key & Generator¶
-
basePlateCacheKey()produces deterministic keys in formatplate-{cols}x{rows}-{NESW}.stl -
generateBasePlateStl()generates a plate withNO_BORDERandplateNumber: false -
enumerateAllBasePlateKeys()returns exactly 200 unique keys - All new functions exported from
@forma3d/gridflock-core - Unit tests for key generation and enumeration
Phase 2: Border STL Generator¶
-
generateBorderStripStl()produces valid binary STL (correct header, triangle count, vertex positions) - Border strip has correct dimensions (width, depth, height match inputs)
-
generatePlateBorderStls()generates the correct number of strips per plate (0–4 depending on border) - No JSCAD dependency — pure binary STL buffer generation
- Border strips align seamlessly with base plate edges (no visible gaps)
- Unit tests validate STL format and dimensions
- Corner handling avoids overlapping geometry
Phase 3: Preview Assembly Service¶
-
PlateCacheServiceloads all 200 base plates into memory on startup -
PlateCacheService.getPlate()returns the correct buffer for a given key -
generatePreview()assembles a complete preview from cached plates + dynamic borders - Assembled preview is visually identical to the current full-preview output (within border rendering differences)
- Preview response time is under 1 second for any dimension pair
- Legacy full-preview cache files still work as a fast path
- Fallback to full generation works when plate cache is empty/missing
- Assembly handles all edge cases: single-plate previews, max-dimension previews, non-zero borders on all sides
Phase 4: Population Script¶
-
scripts/populate-plate-cache.tsgenerates all 200 base plates - Script completes in under 5 minutes
- Script is resumable (skips existing files)
- Generated files match
generateBasePlateStl()output exactly - Upload script transfers files to the staging server
Phase 5: Shopify UI Validation¶
- Dimension inputs constrained to 10–100 cm range with
step="0.1" - Values < 10 cm → clamped to 10 cm with red error message
- Values > 100 cm → clamped to 100 cm with red error message
- Sub-millimeter values (e.g., 10.17 cm) → rounded down to 10.1 cm with green info message
- Valid 0.1 cm values → no message shown
- Preview updates correctly after validation/rounding
- Backend DTOs updated to match new range limits
Build & Lint¶
-
pnpm nx run-many -t build --allpasses -
pnpm nx run-many -t lint --allpasses - No
any,ts-ignore, oreslint-disableintroduced - Existing preview functionality is not broken (backward compatible)
🚫 Constraints and Rules¶
MUST DO¶
- Generate exactly 200 base plates (verify count matches analysis)
- Generate border STL as raw binary buffer — no JSCAD dependency for border generation
- Keep the legacy full-preview cache as a fast path (backward compatibility)
- Load all 200 base plates into memory at startup (~41 MB — negligible)
- Handle the case where the plate cache is not populated (fallback to full generation)
- Maintain visual quality: assembled preview must look the same as a full preview to the end user
- Use existing
combineStlBuffers()for assembly — no new STL combination logic - Use existing
calculatePlateSet()for plate layout — no new layout logic - Validate the exact count of 200 unique base plates before shipping
- Round dimensions down (floor) to the nearest 0.1 cm (1 mm) in the Shopify UI — ensures the baseplate is never larger than the drawer
MUST NOT¶
- Remove the existing
generatePreviewStl()function (it's still needed as fallback and for the production pipeline) - Break the existing preview endpoint API contract
- Introduce JSCAD dependencies in the border generator (must be pure binary STL)
- Cache assembled previews to disk (the whole point is to avoid large disk caches)
- Use
any,ts-ignore,eslint-disable, orconsole.login production code - Change the existing
calculatePlateSet()algorithm (it determines plate layout — must stay unchanged) - Modify grid cell geometry, connector geometry, or magnet geometry
SHOULD DO (Nice to Have)¶
- Add an in-memory LRU cache for assembled previews (avoids repeated assembly for the same dimensions)
- Add a health check endpoint that reports plate cache status (
{ loaded: 200, totalSizeBytes: ... }) - Add timing metrics to the preview endpoint (plate lookup time, border generation time, assembly time)
- Consider generating plate numbering as a separate STL layer (currently omitted from base plates)
- Add a
--dry-runoption to the population script - Add a
--verifyoption that checks all 200 files exist and are valid STL
📚 Key References¶
Codebase¶
- Preview generator:
libs/gridflock-core/src/lib/preview-generator.ts—generatePreviewStl(), offset helpers - Plate set calculator:
libs/gridflock-core/src/lib/plate-set-calculator.ts—calculatePlateSet() - STL combiner:
libs/gridflock-core/src/lib/stl-combiner.ts—combineStlBuffers() - Plate generator:
libs/gridflock-core/src/lib/generator.ts—generateGridFlockPlate(),generatePlateStl() - Base plate geometry:
libs/gridflock-core/src/lib/geometry/base-plate.ts—createBasePlate()(border handling) - Defaults:
libs/gridflock-core/src/lib/defaults.ts—NO_BORDER,DEFAULT_MAGNET_PARAMS - Printer profiles:
libs/gridflock-core/src/lib/printer-profiles.ts—PRINTER_PROFILES['bambu-a1'] - Types:
libs/gridflock-core/src/lib/types.ts—PlateSpec,BorderDimensions,GRID_UNIT_MM - Core exports:
libs/gridflock-core/src/index.ts - Preview service:
apps/gridflock-service/src/gridflock/gridflock-preview.service.ts - Preview DTO:
apps/gridflock-service/src/gridflock/dto/preview-grid.dto.ts - Shopify configurator:
deployment/shopify-theme/sections/configurator.liquid - Existing population script:
scripts/populate-preview-cache.ts - Upload script:
scripts/upload-preview-cache.sh - Prepopulation research:
docs/03-architecture/research/stl-cache-prepopulation-research.md
Key Constants¶
| Constant | Value | Source |
|---|---|---|
GRID_UNIT_MM |
42 | types.ts |
PROFILE_HEIGHT_MM |
4.65 | types.ts |
| bambu-a1 bed size | 256 × 256 mm | printer-profiles.ts |
| bambu-a1 max grid | 6 × 6 | printer-profiles.ts |
PLATE_GAP_MM |
10 | preview-generator.ts |
| Total grid range | 2–23 per axis | floor(100/42) to floor(1000/42) |
| Unique base plates | 200 | Analysis (this document) |
| Unique border values | 0.5 to 20.5 mm in 0.5 mm steps | (remainder / 2) where remainder = 1–41 |
Combinatorics Summary¶
| Resolution | Preview combos | Plate instances | Unique plates (with border) | Unique base plates (no border) | Cache size |
|---|---|---|---|---|---|
| 0.5 cm (5 mm) | 16,471 | 132,456 | 20,576 | 200 | ~41 MB |
| 1 mm | 406,351 | 3,270,902 | 147,500 | 200 | ~41 MB |
END OF PROMPT
This prompt implements a plate-level caching system that replaces the current full-preview-per-dimension cache (16,471 files, ~32 GB) with only 200 cached base plates (~41 MB). At preview time, base plates are looked up from an in-memory cache, border geometry is generated on the fly as trivial rectangular STL (no JSCAD), and everything is combined using the existing combineStlBuffers(). This supports any input resolution with the same 200-file cache and reduces preview generation time from 12–30 seconds to under 1 second. The Shopify UI is updated to support 1 mm (0.1 cm) input precision with user-friendly validation feedback for min/max range and sub-millimeter rounding.