Places a PDF page at its true size in the drawing's units, and turns a page's lines, arcs, circles, fills and text into drawing objects.
How the precision is kept
The true-size scale is points to drawing units from the exact definitions (72 points per inch, 25.4 mm per inch) in 64-bit. Import maps page points through a 64-bit placement matrix, but the page points themselves arrive from pdf.js as 32-bit numbers (see the page vectors policy). Curves are either recognized as circular arcs within a relative tolerance or flattened to 10 straight pieces. Stroke widths are rounded to two decimals before they are stored.
Open item. Keep the imported stroke width unrounded, and take page coordinates in 64-bit rather than from pdf.js's 32-bit path buffer.
Calculations under this policy
13 calculations in 2 source files. 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.
Each factor is one or two 64-bit divisions or multiplications of the defining constants (72 points per inch, 25.4 mm per inch); no factor is typed as a rounded decimal. The unit is recognized by matching the drawing's unit name, and a drawing with no unit, or a name that matches nothing, is read as millimeters.
pdfmodel.js
lines 87 to 101
CadCraft 0.8.2
/** Drawing units per PDF point for a drawing's units (a point is 1/72 inch): the true size of the page. */
export function unitScale(doc) {
const u = String(doc.meta?.units || '').toLowerCase();
const inch = 1 / 72;
if (/inch/.test(u)) return inch;
if (/feet|foot/.test(u)) return inch / 12;
if (/yard/.test(u)) return inch / 36;
if (/mile/.test(u)) return inch / 63360;
if (/micro/.test(u)) return inch * 1e6;
if (/mil/.test(u) && !/millim/.test(u)) return inch * 1000;
if (/centim/.test(u)) return 2.54 / 72;
if (/kilom/.test(u)) return 0.0254 / 72 / 1000;
if (/meter/.test(u) && !/millim|centim|kilom/.test(u)) return 0.0254 / 72;
return 25.4 / 72; // millimetres, or unitless: read as mm
}
Underlay scale and rotation for saving
64-bit
The insertion point, x and y scale and rotation of an underlay as a DWG stores them.
Scales are column lengths by hypot and the rotation is one atan2 of the reference's matrix; nothing is rounded. Fade and contrast fall back to 0 and 50 when they are not finite numbers.
pdfmodel.js
lines 58 to 75
CadCraft 0.8.2
/**
* A reference to an underlay block → what a DWG's PDFUNDERLAY holds, in the reference's space:
* { file, page, insert, scale {x, y}, rotation, clip? (page points; two corners or a polygon), fade, contrast, mono, on }, or
* null when the block is not an underlay.
*/
export function pdfPlacement(ref, block) {
const fr = pdfFrame(block);
if (!fr || block.entities.some((e) => e !== fr && !e.pdfName)) return null;
const p = fr.pdf, m = insertMatrix(ref, block);
const at = mApply(m, { x: 0, y: 0 });
const sx = Math.hypot(m[0], m[1]), sy = Math.hypot(m[2], m[3]) * Math.sign(m[0] * m[3] - m[1] * m[2] || 1);
return {
file: p.file, page: p.page, insert: at, scale: { x: sx, y: sy }, rotation: Math.atan2(m[1], m[0]),
...(p.clipped ? { clip: fr.pts.map((q) => ({ x: q.x, y: q.y })) } : {}),
fade: Number.isFinite(p.fade) ? p.fade : 0, contrast: Number.isFinite(p.contrast) ? p.contrast : 50, mono: !!p.mono, on: !p.off, size: { w: p.w, h: p.h },
...(p.layers && Object.keys(p.layers).length ? { layers: p.layers } : {}),
};
}
Underlay read from a file
64-bit
Builds the underlay block and its placing reference from a file's PDF underlay record.
Insertion, scale and rotation are copied unchanged. When the file gives no page size the frame uses PAGE_GUESS, A4 written to four decimals of a point (595.2756 × 841.8898), until the PDF itself is given. Fade and contrast are rounded to whole numbers and held to 0–80 and 0–100.
pdfunder.jsCadCraft 0.8.2 32-bit in part10 calculationsReviewed
Tests bin/test-cadcraft.mjs
Scale typed when attaching
64-bit
The scale an attached page gets: its true size, or the number typed.
The prompt shows the true-size scale to 6 significant digits. Pressing Enter, or typing exactly that 6-digit number, keeps the unrounded 64-bit scale; any other positive number is used as typed.
Decides whether one cubic curve of the page is an arc of a circle, and if so gives its center, radius and angles.
circle through B(0), B(½), B(1); arc when | |B(t) − c| − r | ≤ 0.002·r at t = 0.25, 0.75, 0.1, 0.9
The tolerance is relative: 0.002 × the radius, checked at four parameters. The arc that is kept is the fitted circle, so it can differ from the PDF's curve by up to that amount at the tested points. The control points it reads were 32-bit when pdf.js delivered the path.
pdfunder.js
lines 215 to 227
CadCraft 0.8.2
/** Is this one cubic an arc of a circle (within tolerance)? → { cx, cy, r, a0, a1 (CCW radians) } | null */
export function cubicArc(p0, c1, c2, p3, tol = 0.002) {
const at = (t) => { const u = 1 - t; return [u * u * u * p0[0] + 3 * u * u * t * c1[0] + 3 * u * t * t * c2[0] + t * t * t * p3[0], u * u * u * p0[1] + 3 * u * u * t * c1[1] + 3 * u * t * t * c2[1] + t * t * t * p3[1]]; };
const m = at(0.5);
const circ = circleThrough(p0, m, p3);
if (!circ) return null;
for (const t0 of [0.25, 0.75, 0.1, 0.9]) { const q = at(t0); if (Math.abs(Math.hypot(q[0] - circ.cx, q[1] - circ.cy) - circ.r) > tol * circ.r) return null; }
// direction: the angle from p0 through m to p3 along the shorter way
const a0 = Math.atan2(p0[1] - circ.cy, p0[0] - circ.cx), am = Math.atan2(m[1] - circ.cy, m[0] - circ.cx), a1 = Math.atan2(p3[1] - circ.cy, p3[0] - circ.cx);
const norm = (a) => ((a % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
const ccw = norm(am - a0) < norm(a1 - a0);
return ccw ? { ...circ, a0: norm(a0), a1: norm(a1) } : { ...circ, a0: norm(a1), a1: norm(a0) };
}
Circle through three points
64-bit
Center and radius of the circle through three points.
d = 2(aₓ(b_y − c_y) + bₓ(c_y − a_y) + cₓ(a_y − b_y)); center from |a|², |b|², |c|² ÷ d
Uses absolute coordinates and their squares, so three points close together and far from the origin lose digits; here the coordinates are page points measured from the page corner. Points are called collinear when |d| < 1e-12, an absolute value that does not scale with the size of the points, and a radius of 1e-9 or less is refused.
The vertices of each imported path in drawing coordinates, with curves replaced by straight pieces.
B(t) at t = 0.1 … 1.0, then (x, y) → placement matrix
Each cubic is replaced by 10 straight pieces at equal parameter steps: the vertices lie on the curve and the polyline cuts across between them. Points are evaluated in page points, then mapped by the 64-bit placement matrix; the page points were 32-bit on arrival from pdf.js.
pdfunder.js
lines 257 to 273
CadCraft 0.8.2
// the outline as drawing points, curves flattened
const pts = [];
let cursor = null;
for (const o of ops) {
if (o[0] === 'M' || o[0] === 'L') { const q = P(o[1], o[2]); pts.push({ x: q.x, y: q.y, b: 0 }); cursor = [o[1], o[2]]; }
else {
const p0 = cursor;
for (let s = 1; s <= 10; s++) {
const t0 = s / 10, u = 1 - t0;
const x = u * u * u * p0[0] + 3 * u * u * t0 * o[1] + 3 * u * t0 * t0 * o[3] + t0 * t0 * t0 * o[5];
const y = u * u * u * p0[1] + 3 * u * u * t0 * o[2] + 3 * u * t0 * t0 * o[4] + t0 * t0 * t0 * o[6];
const q = P(x, y); pts.push({ x: q.x, y: q.y, b: 0 });
}
cursor = [o[5], o[6]];
}
}
if (pts.length < 2) continue;
Stroke width kept on imported objects
64-bit
The line width stored on each imported line, arc and polyline.
lw = round(width × scale × 100) ÷ 100
The width (PDF line width × the size of the content matrix × the import scale) is rounded to two decimals of a drawing unit before it is stored. A width below 0.005 drawing units is therefore stored as 0.
Turns a path made only of circular curves into a circle or an arc object.
same circle when |rᵢ − r₀| < 1e-3·r₀ and |cᵢ − c₀| < 1e-3·r₀; circle when Σ sweep = 2π within 1e-3
A closed path of three or more matching arcs, or arcs whose sweeps add to a full turn within 1e-3 rad, becomes a circle with the first arc's center and radius; the other arcs' fits are not averaged in. A single arc keeps its own fitted center, radius and angles, wrapped to [0, 2π). Center and radius come from 32-bit path points.
h = PDF height × scale ÷ FONT_EM; angle = (PDF angle + placement rotation) mod 2π
The position goes through the placement matrix in 64-bit. The height is the PDF text height times the scale, divided by FONT_EM, a constant of geom.js.
Rescales an imported page so that two picked points are a stated distance apart.
f = real ÷ |p₂ − p₁|
One hypot and one division, skipped unless both distances are above zero. The factor is applied to every imported object about the insertion point.
pdfunder.js
lines 345 to 349
CadCraft 0.8.2
const cur = Math.hypot(p2.x - p1.x, p2.y - p1.y);
if (typeof real === 'number' && real > 0 && cur > 0) {
const f = real / cur;
c.doc.change('PDFIMPORT', () => { for (const id of built.ids) { const e = c.doc.get(id); if (e) c.doc.update(id, scaleEntity(e, at, f)); } });
s *= f;
Scaling an object about a point
64-bit
Moves and resizes one imported object by the factor f about the insertion point.
x′ = aₓ + (x − aₓ)·f; r′ = r·f; h′ = h·f
Each coordinate takes one subtraction, one multiplication and one addition in 64-bit; radii and text heights are multiplied directly. Bulges, angles and stroke widths are left as they were.
Unit factors by division of exact constants; circle through three points by the circumcenter determinant; cubic Bézier evaluation; uniform scaling about a point.
Number types
64-bit numbers; page geometry read from pdf.js starts as 32-bit.
Tolerances
A cubic is an arc when four test points lie within 0.002 × r of the circle through its ends and midpoint. Arcs of one path share a circle within 1e-3 × r (radius and center); a full circle when sweeps add to 2π within 1e-3 rad. Collinear test |d| < 1e-12 (absolute, squared page points); radius ≤ 1e-9 refused.
Limits
Curves that are not circular become 10-piece polylines. The page size of an underlay read from a DWG is an A4 guess until the PDF file is given. Stroke width is kept to 0.01 drawing units.
Approach: 32-bit step, flagged
One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.