SKP import: placements, texture coordinates and face loops
Reads component placements, texture coordinates and the direction of face loops from SKP files.
How the precision is kept
Vertex coordinates, plane normals and placement numbers are read from the file as 64-bit doubles and passed on unchanged. The only arithmetic is dividing a placement by its last number, projecting points to texture coordinates and comparing signed loop areas.
Calculations under this policy
6 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.
A component's placement as a 4 × 4 matrix from the file's 13 doubles.
rows of the 3 × 3 become columns; when w ≠ 1 every entry ÷ w
The numbers are copied; only when the thirteenth, w, differs from 1 are the other 15 entries divided by it. A w that is not finite or within 1e-12 of zero is replaced by 1.
skp/legacy.js
lines 980 to 990
ModelCraft 0.8.1
/** SketchUp's 13 doubles → column-major 4×4. The first nine are the 3×3 part ROW by row (x' = m0·x + m1·y +
* m2·z + m9, as OpenSKP reads it; checked on gondola_v20.skp, whose rotated components meet their neighbours'
* vertices only this way round), then the translation, then w. w is 1 in every file we have; we treat it as
* the homogeneous divisor. */
function matrixOf(xf) {
const w = okNum(xf[12]) && Math.abs(xf[12]) > 1e-12 ? xf[12] : 1;
const m = [xf[0], xf[3], xf[6], 0, xf[1], xf[4], xf[7], 0, xf[2], xf[5], xf[8], 0, xf[9], xf[10], xf[11], w];
if (!m.every(okNum)) return null;
if (w !== 1) { for (let i = 0; i < 15; i++) m[i] /= w; m[15] = 1; }
return m;
}
Texture coordinates (classic files)
64-bit
The texture coordinates of each loop point of a textured face.
The 3 × 3 inverse is by cofactors divided by the determinant, skipped when |det| is 1e-12 or less. The plane coordinates are dot products of the point's own coordinates, so they are as large as the point's distance from the origin before the tile size divides them.
skp/legacy.js
lines 992 to 1019
ModelCraft 0.8.1
/** Texture coordinates for a face's loops (OpenSKP's recipe): plane basis xr = Z × n, yr = n × xr; untouched
* textures project straight onto it, positioned ones go through the inverse of their 3×3 mapping. */
function faceUv(loops, n, ftc, tileW, tileH) {
const [nx, ny, nz] = n;
let xr, yr;
const cl = Math.hypot(nx, ny);
if (cl < 1e-9) { xr = [1, 0, 0]; yr = [0, nz >= 0 ? 1 : -1, 0]; }
else { xr = [-ny / cl, nx / cl, 0]; yr = [-nz * xr[1], nz * xr[0], nx * xr[1] - ny * xr[0]]; }
let inv = null;
if (ftc) {
const [a, b, c, d, e, f, g, h, i] = ftc.front;
const det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
if (Number.isFinite(det) && Math.abs(det) > 1e-12) {
const k = 1 / det;
inv = [(e * i - f * h) * k, (c * h - b * i) * k, (b * f - c * e) * k, (f * g - d * i) * k, (a * i - c * g) * k,
(c * d - a * f) * k, (d * h - e * g) * k, (b * g - a * h) * k, (a * e - b * d) * k];
}
}
const tw = okNum(tileW) && tileW > 1e-9 ? tileW : 1, th = okNum(tileH) && tileH > 1e-9 ? tileH : 1;
return loops.map((pts) => pts.map((p) => {
const px = p[0] * xr[0] + p[1] * xr[1] + p[2] * xr[2];
const py = p[0] * yr[0] + p[1] * yr[1] + p[2] * yr[2];
if (!inv) return [px / tw, py / th];
let q = px * inv[2] + py * inv[5] + inv[8];
if (Math.abs(q) < 1e-12) q = 1;
return [(px * inv[0] + py * inv[3] + inv[6]) / q / tw, (px * inv[1] + py * inv[4] + inv[7]) / q / th];
}));
}
Loop normal
64-bit
The Newell normal of a face loop, used to check which way round the loop runs.
A sum over the loop's edges in order. Only the sign of its dot product with the stored plane normal is used, with no tolerance.
skp/legacy.js
lines 1021 to 1029
ModelCraft 0.8.1
/** Newell normal of a loop (for checking orientation against the stored plane). */
function newell(pts) {
let x = 0, y = 0, z = 0;
for (let i = 0; i < pts.length; i++) {
const a = pts[i], b = pts[(i + 1) % pts.length];
x += (a[1] - b[1]) * (a[2] + b[2]); y += (a[2] - b[2]) * (a[0] + b[0]); z += (a[0] - b[0]) * (a[1] + b[1]);
}
return [x, y, z];
}
A component's placement as a 4 × 4 matrix from the file's 13 doubles.
k = 1 ÷ w; every rotation and translation entry × k
The entries are multiplied by the reciprocal of w, so each is rounded twice unless w is a power of two such as the 0.5 the file's header comment mentions. A w that is not finite or within 1e-12 of zero is reported as damage.
The texture coordinates of a point on a textured face.
(p_x, p_y) = (p·x_r, p·y_r); without a placement: uv = (p_x ÷ tile width, p_y ÷ tile height); with one: (p_x, p_y, 1)·M⁻¹ ÷ q ÷ tile size
The same recipe as in legacy.js: cofactor inverse divided by the determinant, ignored when |det| is 1e-12 or less, and a projective divisor below 1e-12 replaced by 1.
skp/vff.js
lines 256 to 281
ModelCraft 0.8.1
/** Texture coordinates of p on a face with normal n (SketchUp's default projection, or the face's placement matrix). */
function faceUv(p, n, place, tw, th) {
// The face's plane axes: x = normalize(Z × n), y = n × x (vertical normals: x = X, y = ±Y).
let xr, yr;
const cl = Math.hypot(n[0], n[1]);
if (cl < 1e-9) { xr = [1, 0, 0]; yr = [0, n[2] >= 0 ? 1 : -1, 0]; } else {
xr = [-n[1] / cl, n[0] / cl, 0];
yr = [n[1] * xr[2] - n[2] * xr[1], n[2] * xr[0] - n[0] * xr[2], n[0] * xr[1] - n[1] * xr[0]];
}
const px = p[0] * xr[0] + p[1] * xr[1] + p[2] * xr[2];
const py = p[0] * yr[0] + p[1] * yr[1] + p[2] * yr[2];
if (!place) return [px / tw, py / th];
// place = 3×3 row-major, texture → face plane, row vectors: [u v 1]·M = [px py 1]; invert it.
const [a, b, c, d, e, f, g, h, i] = place;
const det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
if (!(Math.abs(det) > 1e-12)) return [px / tw, py / th];
const k = 1 / det;
const inv = [(e * i - f * h) * k, (c * h - b * i) * k, (b * f - c * e) * k,
(f * g - d * i) * k, (a * i - c * g) * k, (c * d - a * f) * k,
(d * h - e * g) * k, (b * g - a * h) * k, (a * e - b * d) * k];
const u = px * inv[0] + py * inv[3] + inv[6];
const v = px * inv[1] + py * inv[4] + inv[7];
let q = px * inv[2] + py * inv[5] + inv[8];
if (Math.abs(q) < 1e-12) q = 1;
return [u / q / tw, v / q / th];
}
Outer loop and loop direction
64-bit
Chooses a face's outer loop and turns loops so that the outer runs counter-clockwise about the normal and holes clockwise.
area(loop) = Newell(loop)·unit normal ÷ 2; outer = loop with the largest |area|
The stored normal is used when its three numbers are finite, else the first loop's Newell normal; a zero-length normal is divided by 1. Signs of areas are compared with 0 without tolerance.
skp/vff.js
lines 717 to 725
ModelCraft 0.8.1
const n = f.normal && Number.isFinite(f.normal[0] + f.normal[1] + f.normal[2]) ? f.normal : newell(loops[0]);
const nl = Math.hypot(n[0], n[1], n[2]) || 1;
const unit = [n[0] / nl, n[1] / nl, n[2] / nl];
const area = (l) => { const w = newell(l); return (w[0] * unit[0] + w[1] * unit[1] + w[2] * unit[2]) / 2; };
let outer = 0;
for (let i = 1; i < loops.length; i++) if (Math.abs(area(loops[i])) > Math.abs(area(loops[outer]))) outer = i;
if (outer !== 0) { const [o] = loops.splice(outer, 1); loops.unshift(o); }
if (area(loops[0]) < 0) { for (const l of loops) l.reverse(); flipped++; }
for (let i = 1; i < loops.length; i++) if (area(loops[i]) > 0) loops[i].reverse();
In detail
Method
Placement: 13 doubles (3 × 3 rows, translation, divisor w) to a column-major 4 × 4. Texture coordinates: the face's plane axes x = unit(Z × n), y = n × x, then the inverse of the 3 × 3 placement by cofactors. Loop direction: Newell normal against the stored plane normal.
Number types
64-bit doubles read with getFloat64; plain arrays.
Tolerances
A placement divisor with |w| ≤ 1e-12 is treated as 1 in legacy.js and refused as damaged in vff.js. A normal with a horizontal part below 1e-9 uses fixed axes. A texture placement with |determinant| ≤ 1e-12 is ignored; a projective divisor below 1e-12 is replaced by 1. All absolute.
Limits
The two readers divide by w differently: legacy.js divides each entry, vff.js multiplies by 1 ÷ w, which rounds twice unless w is a power of two.
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.