Reads an IC layout stream file and places every cell reference and array so the layout can be viewed and measured as drawing objects.
How the precision is kept
Coordinates in the file are 32-bit integers and are read with getInt32, so a 64-bit number holds each one exactly. The file's 8-byte base-16 reals are converted to 64-bit numbers. Placement composes 2 × 3 matrices in database units and multiplies by the user unit only at the end.
Calculations under this policy
3 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
gds.jsCadCraft 0.8.264-bit3 calculationsReviewed
Tests bin/test-cadcraft.mjs
8-byte base-16 real
64-bit
Converts the stream format's excess-64 base-16 real (units, magnification, angle) to a number.
value = sign × (m ÷ 2⁵⁶) × 16^(e − 64), m = the 7 mantissa bytes as one integer
The mantissa is built as m × 256 + byte over 7 bytes, which is exact for the first 6 bytes and rounds once on the 7th when the 56-bit value needs more than 53 bits. The division by 2⁵⁶ (written as 72057594037927936) only changes the exponent, and the power of 16 comes from Math.pow.
gds.js
lines 21 to 26
CadCraft 0.8.2
function real8(dv, p) {
const b0 = dv.getUint8(p), sign = b0 & 0x80 ? -1 : 1, exp = (b0 & 0x7f) - 64;
let m = 0;
for (let i = 1; i < 8; i++) m = m * 256 + dv.getUint8(p + i);
return sign * (m / 72057594037927936) * Math.pow(16, exp); // 2^56
}
Reference transform
64-bit
The matrix that places one cell reference: reflect, magnify, rotate, move, and its product with the parent's matrix.
r = angle × π ÷ 180; M = [mag·cos r, mag·sin r, −f·mag·sin r, f·mag·cos r, x, y], f = −1 when reflected
The angle in degrees is converted with Math.PI and passed to cos and sin; right angles are not special-cased, so cos of 90° evaluates to about 6.1 × 10⁻¹⁷ instead of 0. Matrix products are plain sums of products with no tolerance.
gds.js
lines 82 to 87
CadCraft 0.8.2
const mul = (m, n) => [m[0] * n[0] + m[2] * n[1], m[1] * n[0] + m[3] * n[1], m[0] * n[2] + m[2] * n[3], m[1] * n[2] + m[3] * n[3], m[0] * n[4] + m[2] * n[5] + m[4], m[1] * n[4] + m[3] * n[5] + m[5]];
const place = (x, y, reflect, mag, angle) => {
const r = (angle || 0) * Math.PI / 180, c = Math.cos(r) * mag, s = Math.sin(r) * mag, f = reflect ? -1 : 1;
return [c, s, -s * f, c * f, x, y]; // reflect about the X axis first (y → −y), then magnify, rotate and move
};
const apply = (m, x, y) => [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
Flattened coordinates, array steps and path width
64-bit
Places every element of every referenced cell and array and converts its points and width to user units.
Points are transformed in integer database units and multiplied by the user unit last; a user unit such as 0.001 is not exact in binary, so each coordinate is a rounded product. Array positions are origin + column × step + row × step, with the step from a division by the column or row count. A user unit that is zero or negative is replaced by 0.001.
gds.js
lines 89 to 120
CadCraft 0.8.2
export function flattenGds(gds, topName, { max = 400000, depth = 16 } = {}) {
const items = [];
let count = 0, truncated = false;
const u = gds.lib.userUnit > 0 ? gds.lib.userUnit : 1e-3; // user units per database unit
const walk = (name, m, d) => {
const els = gds.cells.get(name);
if (!els || d > depth || truncated) return;
for (const e of els) {
if (truncated) return;
if (e.k === 'sref' || e.k === 'aref') {
const base = e.pts.length >= 2 ? e.pts : [0, 0];
if (e.k === 'sref') { walk(e.name, mul(m, place(base[0], base[1], e.reflect, e.mag || 1, e.angle)), d + 1); continue; }
const cols = e.cols || 1, rows = e.rows || 1;
if (e.pts.length < 6) continue;
const [x0, y0, xc, yc, xr, yr] = e.pts;
const dxc = (xc - x0) / cols, dyc = (yc - y0) / cols, dxr = (xr - x0) / rows, dyr = (yr - y0) / rows;
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
if (truncated) return;
walk(e.name, mul(m, place(x0 + c * dxc + r * dxr, y0 + c * dyc + r * dyr, e.reflect, e.mag || 1, e.angle)), d + 1);
}
continue;
}
if (++count > max) { truncated = true; return; }
const pts = [];
for (let i = 0; i + 1 < e.pts.length; i += 2) { const [x, y] = apply(m, e.pts[i], e.pts[i + 1]); pts.push(x * u, y * u); }
const k = Math.hypot(m[0], m[1]);
items.push({ k: e.k === 'box' ? 'boundary' : e.k, layer: e.layer, datatype: e.datatype, pts, width: (e.width || 0) * Math.abs(k) * u, pathtype: e.pathtype || 0, string: e.string, h: 1 * Math.abs(k), angle: Math.atan2(m[1], m[0]) });
}
};
walk(topName, [1, 0, 0, 1, 0, 0], 0);
return { items, count: Math.min(count, max), truncated };
}
In detail
Method
Stream records are read as the file header comment describes them: 2-byte length, record type, data type, big-endian integers, 8-byte excess-64 base-16 reals. References are placed by reflecting about the X axis, then magnifying, rotating and moving.
Number types
64-bit numbers; coordinates come from 32-bit integers in the file, reals from a 56-bit base-16 mantissa.
Limits
A 56-bit mantissa has 3 more bits than a 64-bit number keeps, so a real with those bits set is rounded once. Files over 256 MB or 5,000,000 elements are refused; flattening stops at 400,000 items and 16 levels by default.
Approach: 64-bit, direct
Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.