Sizes a picture placed under a drawing from two picked points and a distance, moves and turns it, places it in sheet views, and works out its size and matrix in a printed PDF.
How the precision is kept
A picture is stored as a center point, a scale in millimeters per pixel and an angle in degrees. Every stored value is rounded to 6 decimals after each change. Corners and matrices are recomputed from those stored values with one cosine and sine.
Open item. Keep the picture's scale in millimeters per pixel at full precision instead of rounding it to 6 decimals.
Calculations under this policy
15 calculations in 3 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.
Applied to every value the picture record keeps: millimeters, millimeters per pixel and degrees alike.
picture.js
line 35
BimCraft 0.8.1
const r6 = (v) => Math.round(v * 1e6) / 1e6;
New picture record
64-bit
The cleaned numbers of a picture, with defaults.
default scale = r6(20000 ÷ width in pixels); angle = r6(((a mod 360) + 360) mod 360)
Pixel sizes are rounded to whole numbers of at least 1. A picture with no scale is made 20 m wide. Position, scale and angle are rounded to 6 decimals.
picture.js
lines 72 to 84
BimCraft 0.8.1
/** A picture record with clean numbers. Defaults: 20 m wide, centred on (0, 0), not scaled yet, fully opaque, unlocked. */
export function makePicture(o = {}) {
const w = Math.max(1, Math.round(num(o.w, 1))), h = Math.max(1, Math.round(num(o.h, 1)));
const mm = num(o.mmPerPx, 0);
return {
name: String(o.name || 'picture').slice(0, 120), mime: String(o.mime || 'image/png'), w, h,
x: r6(num(o.x, 0)), y: r6(num(o.y, 0)), mmPerPx: mm > 0 ? r6(mm) : r6(20000 / w),
angle: r6(((num(o.angle, 0) % 360) + 360) % 360), opacity: Math.min(1, Math.max(LIMITS.minOpacity, num(o.opacity, 1))),
locked: !!o.locked, scaled: !!o.scaled, hidden: !!o.hidden,
...(o.section ? { section: String(o.section).slice(0, 64) } : {}), // the section line (a GlobalId) a vertical-plane picture was put on, if it was put on one
...(o.plane === 'vertical' && Array.isArray(o.a) && Array.isArray(o.b) ? { plane: 'vertical', a: [r6(num(o.a[0], 0)), r6(num(o.a[1], 0))], b: [r6(num(o.b[0], 0)), r6(num(o.b[1], 0))] } : { plane: 'level' }),
};
}
Scale by two points and a distance
64-bit
The new scale and center that make two picked points a given distance apart.
k = mm ÷ |b − a|; scale′ = r6(scale × k); center′ = r6(a + (center − a) × k)
The picked distance is one Math.hypot and the factor one division, guarded by a 1e-6 mm minimum. The new scale is the old rounded scale times k, rounded again to 6 decimals.
picture.js
lines 134 to 142
BimCraft 0.8.1
/** The change that makes two points of the picture `mm` apart: it grows or shrinks about the first, which stays where it is. */
export function scaleByPoints(p, a, b, mm) {
unlocked(p);
const d = Math.hypot(b[0] - a[0], b[1] - a[1]);
if (!(d > 1e-6)) refuse('pic.err.samepoint');
if (!(mm > 0) || !Number.isFinite(mm)) refuse('pic.err.distance');
const k = mm / d;
return { mmPerPx: r6(p.mmPerPx * k), x: r6(a[0] + (p.x - a[0]) * k), y: r6(a[1] + (p.y - a[1]) * k), scaled: true };
}
Corners of a placed picture
64-bit
The four corners of the picture in plan millimeters.
Half sizes are pixel counts times the stored scale. One cosine and sine of the stored angle are used for all four corners, and each coordinate is rounded to 6 decimals.
picture.js
lines 111 to 115
BimCraft 0.8.1
/** Lower-left, lower-right, upper-right, upper-left in plan mm. */
export function corners(p) {
const hw = p.w * p.mmPerPx / 2, hh = p.h * p.mmPerPx / 2, a = p.angle * Math.PI / 180, c = Math.cos(a), s = Math.sin(a);
return [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]].map(([x, y]) => [r6(p.x + x * c - y * s), r6(p.y + x * s + y * c)]);
}
Pixel to plan matrix
64-bit
The transform from picture pixels to plan millimeters.
[k·c, k·s, k·s, −k·c, x + k(−w/2·c − h/2·s), y + k(−w/2·s + h/2·c)]
Built from the stored scale, angle and center in one expression per entry and rounded to 6 decimals per entry.
picture.js
lines 117 to 122
BimCraft 0.8.1
/** Canvas transform [a, b, c, d, e, f]: a picture pixel (x right, y down, origin at its top-left) → plan mm. */
export function matrix(p) {
const k = p.mmPerPx, a = p.angle * Math.PI / 180, c = Math.cos(a), s = Math.sin(a), hw = p.w / 2, hh = p.h / 2;
// pixel (u, v) → centred (u - hw, hh - v) → scaled → turned → moved
return [k * c, k * s, k * s, -k * c, p.x + k * (-hw * c - hh * s), p.y + k * (-hw * s + hh * c)].map(r6);
}
Plan point to picture pixel
64-bit
The pixel of the picture under a plan point.
(u, v) = (R(−θ)·(q − center)) ÷ scale; pixel = (u + w ÷ 2, h ÷ 2 − v)
The center is subtracted before the rotation, so the rotation works on local differences. The result is not rounded. The division by the scale is not guarded here; makePicture keeps the scale positive.
picture.js
lines 124 to 129
BimCraft 0.8.1
/** A plan point in picture pixels (x right, y down). */
export function toPicture(p, q) {
const a = p.angle * Math.PI / 180, c = Math.cos(a), s = Math.sin(a), dx = q[0] - p.x, dy = q[1] - p.y;
const lx = (dx * c + dy * s) / p.mmPerPx, ly = (-dx * s + dy * c) / p.mmPerPx;
return [lx + p.w / 2, p.h / 2 - ly];
}
Picture on a vertical plane
64-bit
Puts a picture on the plane standing over a line, as wide as the line.
k = L ÷ w; center = (L ÷ 2, h·k ÷ 2)
The line length is one Math.hypot and must exceed 1 mm. Scale, center and the line's end points are rounded to 6 decimals.
picture.js
lines 97 to 103
BimCraft 0.8.1
export function onLine(p, a, b) {
unlocked(p);
const L = lineLength(a, b);
if (!(L > 1)) refuse('pic.err.samepoint');
const k = L / p.w;
return { plane: 'vertical', a: [r6(a[0]), r6(a[1])], b: [r6(b[0]), r6(b[1])], x: r6(L / 2), y: r6(p.h * k / 2), mmPerPx: r6(k), angle: 0, scaled: true };
}
Picture width typed
64-bit
The scale that makes the picture a typed width.
scale = r6(mm ÷ w)
One division by the pixel width, rounded to 6 decimals, so the width read back is w × the rounded scale and can differ from the typed width by up to w × 5e-7 mm.
The picture's corners in level coordinates, for a level or a vertical plane.
p = origin + u·û + v·v̂
The plane's along-axis is the line's direction divided by its Math.hypot length. Each coordinate is rounded to 6 decimals, on top of the rounding already in corners.
picture.js
lines 92 to 95
BimCraft 0.8.1
export function corners3d(p) {
const f = frame(p);
return corners(p).map(([u, v]) => [0, 1, 2].map((i) => r6(f.o[i] + u * f.u[i] + v * f.v[i])));
}
Decides whether a standing picture shows in a view and gives its matrix there.
shown when |û·dir| ≤ sin(2°) and, in a section, |(a − o)·dir| ≤ 100 mm; x = a·right ± picture x, y = level difference + picture y
The facing test compares a dot product of unit vectors with sin(tolerance). The picture's along-axis is taken as exactly +1 or −1 times the view's right direction, so a plane up to 2° off square is drawn without the cosine shortening. Each matrix entry is rounded to 6 decimals.
picture-sheet.js
lines 20 to 37
BimCraft 0.8.1
export function viewPlacement(p, { dir, o = null, guid = '', dz = 0, tolDeg = 2, tolMm = 100 } = {}) {
if (!p || p.hidden || p.plane !== 'vertical' || !Array.isArray(p.a) || !Array.isArray(p.b) || !Array.isArray(dir)) return null;
const dx = p.b[0] - p.a[0], dy = p.b[1] - p.a[1], L = Math.hypot(dx, dy);
if (!(L > 1)) return null;
const u = [dx / L, dy / L], right = [dir[1], -dir[0]];
const facing = Math.abs(u[0] * dir[0] + u[1] * dir[1]); // 0 = the plane is square to the view
if (facing > Math.sin(tolDeg * Math.PI / 180)) return null;
const onSection = !!guid && p.section === guid;
if (o && !onSection) {
const off = (p.a[0] - o[0]) * dir[0] + (p.a[1] - o[1]) * dir[1];
if (Math.abs(off) > tolMm) return null;
}
const s = u[0] * right[0] + u[1] * right[1]; // +1 or -1 (to the tolerance)
const sg = s < 0 ? -1 : 1;
const A = p.a[0] * right[0] + p.a[1] * right[1];
const m = matrix(p);
return [sg * m[0], m[1], sg * m[2], m[3], A + sg * m[4], dz + m[5]].map((v) => Math.round(v * 1e6) / 1e6);
}
The pixel size a picture is drawn at on a sheet, limited by dots per inch and total pixels.
paper mm per pixel = scale ÷ ratio; dpi = 25.4 ÷ that; k = min(1, max dpi ÷ dpi, √(max pixels ÷ (w·h)))
25.4 is exact. The pixel counts are rounded to whole numbers of at least 1, so the drawn picture's proportions can differ from the original by up to half a pixel in each direction. The dpi returned is rounded to a whole number and capped at 99999.
picture-pdf.js
lines 57 to 64
BimCraft 0.8.1
/** The pixels to draw a picture at on a sheet at 1:ratio: paper mm per pixel = mmPerPx / ratio, so dpi = 25.4 / that. */
export function printSize({ w, h, mmPerPx, ratio, maxDpi = 200, maxPixels = 16e6 }) {
const paperMm = mmPerPx / ratio, dpi = paperMm > 0 ? 25.4 / paperMm : Infinity;
let k = dpi > maxDpi ? maxDpi / dpi : 1;
if (w * h * k * k > maxPixels) k = Math.sqrt(maxPixels / (w * h));
k = Math.min(1, k);
return { w: Math.max(1, Math.round(w * k)), h: Math.max(1, Math.round(h * k)), k, dpi: Math.round(Math.min(dpi, 99999)) };
}
Matrix for the image's unit square
64-bit
Turns the pixel-to-plan matrix into the one PDF needs for an image drawn on a unit square.
[a·w, b·w, −c·h, −d·h, e + c·h, f + d·h]
Products of the 6-decimal matrix entries and the whole pixel sizes, one addition for each offset.
picture-pdf.js
lines 76 to 79
BimCraft 0.8.1
/** pixel (u, v: right, down) = plan mm through m; the unit square (s, t: right, up) has u = s·w, v = (1 − t)·h. */
export function unitMatrix(m, w, h) {
return [m[0] * w, m[1] * w, -m[2] * h, -m[3] * h, m[4] + m[2] * h, m[5] + m[3] * h];
}
In detail
Method
Scale = true distance ÷ picked distance; similarity transform about the first picked point; rotation about the center.
Number types
64-bit numbers rounded to 6 decimals; PDF numbers written with 6 decimals.
Tolerances
Picked points must be more than 1e-6 mm apart to scale; a vertical picture's line must be longer than 1 mm. A vertical picture shows in a view when its plane is within 2° of square to the view and, in a section, within 100 mm of the cut.
Limits
The scale is kept to 1e-6 mm per pixel, so the far edge of a picture N pixels wide can be up to N × 5e-7 mm from where the unrounded scale would put it. Repeated moves and scalings each round again.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.