Keeps the drafting lines and hatch regions drawn in a view, checks them, moves them and works out the strokes of each hatch pattern inside its region.
How the precision is kept
Points are plan millimeters in 64-bit. Reading multiplies each stored coordinate by the file's length unit; writing goes through the model writer, which rounds coordinates to 6 decimals of the file's unit. Hatch strokes are clipped to the region with a parametric line intersection and an even-odd test at the middle of each piece.
Calculations under this policy
11 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.
The signed area of a closed outline, used to refuse a hatch with no area.
area = ½ Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ)
The shoelace sum on the coordinates as given, not moved to a local origin. Far from the plan origin the products are large and nearly cancel; the result is only compared with 100 mm².
drafting.js
line 54
BimCraft 0.8.1
const area2 = (p) => { let a = 0; for (let i = 0; i < p.length; i++) { const q = p[i], r = p[(i + 1) % p.length]; a += q[0] * r[1] - r[0] * q[1]; } return a / 2; };
Points taken from a drawing gesture
64-bit
The points of a new line or region, without repeats.
A point is dropped when it lies within 0.5 mm of the one kept before it (Math.hypot of the differences, absolute). Points that are not finite numbers are dropped. The coordinates themselves are kept unrounded.
drafting.js
lines 55 to 59
BimCraft 0.8.1
const clean = (pts) => {
const out = [];
for (const q of pts || []) { if (!Array.isArray(q) || !Number.isFinite(+q[0]) || !Number.isFinite(+q[1])) continue; const p = [+q[0], +q[1]]; const l = out[out.length - 1]; if (!l || Math.hypot(p[0] - l[0], p[1] - l[1]) > 0.5) out.push(p); }
return out;
};
Points read from the file
64-bit
A drafting item's points in millimeters, from the IFC polyline that stores them.
mm = stored coordinate × file length unit in mm
Each coordinate is multiplied once by the file's unit. A polyline whose last point is within 0.01 mm of its first is read as closed and the repeated point is removed.
drafting.js
lines 72 to 89
BimCraft 0.8.1
const curve = (h) => {
const pl = h ? w.api.GetLine(w.mid, h, false) : null, pts = [];
for (const ph of pl?.Points || []) { const p = w.api.GetLine(w.mid, ph.value, false); const c = (p?.Coordinates || []).map((v) => Number(val(v)) * w.unitMm); pts.push([c[0], c[1]]); }
return pts;
};
const sh = a.Representation?.value ? w.api.GetLine(w.mid, a.Representation.value, false) : null;
for (const h of sh?.Representations || []) {
const r = w.api.GetLine(w.mid, h.value, false);
for (const it of r?.Items || []) {
const item = w.api.GetLine(w.mid, it.value, false);
if (item?.OuterBoundary) { d.pts = curve(item.OuterBoundary.value); d.holes = (item.InnerBoundaries || []).map((x) => curve(x.value)); }
else for (const ch of item?.Elements || []) d.pts = curve(ch.value);
}
}
const P = d.pts;
if (P.length > 2 && Math.hypot(P[0][0] - P[P.length - 1][0], P[0][1] - P[P.length - 1][1]) < 0.01) { P.pop(); d.closed = true; }
if (d.kind === 'hatch') { d.closed = true; d.holes = d.holes.map((h) => (h.length > 2 && Math.hypot(h[0][0] - h[h.length - 1][0], h[0][1] - h[h.length - 1][1]) < 0.01 ? h.slice(0, -1) : h)); }
if (P.length >= 2) out.push(d);
Hatch spacing and angle written to the file
64-bit
The IFC fill style of a pattern: one hatching per line family with its spacing and angle.
spacing = gap × scale in file units; angle = degrees × π ÷ 180 (× 180 ÷ π when the file's angles are degrees)
The spacing is rounded to 6 decimals of the file's length unit and the angle to 9 decimals (round from model-write.js). The style is shared by a name that carries the scale rounded to 3 decimals, so two scales that agree to 3 decimals share one style.
drafting.js
lines 101 to 115
BimCraft 0.8.1
/** The fill area style of a pattern at a scale, shared by name (found in the file, else made). */
function fillStyle(w, pattern, scale) {
const name = 'CadCraft drafting: ' + pattern + ' x' + round(scale, 3); // i18n-ignore: a style name kept in the IFC (plain ASCII)
for (const id of ids(w.api, w.mid, 'IFCFILLAREASTYLE')) { const l = w.api.GetLine(w.mid, id, false); if (l && val(l.Name) === name) return id; }
const P = PATTERNS.find((p) => p.key === pattern) || PATTERNS[1];
const black = () => w.make('IFCCOLOURRGB', null, 0, 0, 0);
const styles = [];
if (P.solid) styles.push(w.H(w.make('IFCCOLOURRGB', null, 0.35, 0.35, 0.35)));
for (const f of P.families) {
const cs = w.v4 ? w.make('IFCCURVESTYLE', null, null, null, w.H(black()), null) : w.make('IFCCURVESTYLE', null, null, null, w.H(black()));
styles.push(w.H(w.make('IFCFILLAREASTYLEHATCHING', w.H(cs), w.T('IFCPOSITIVELENGTHMEASURE', round(w.u(f.gap * scale))), null, null, round(f.angle * Math.PI / 180 * angleK(w), 9))));
}
if (!styles.length) styles.push(w.H(black())); // insulation: drawn by BimCraft; other programs get a plain black fill style
return w.v4 ? w.make('IFCFILLAREASTYLE', w.L(name), styles, null) : w.make('IFCFILLAREASTYLE', w.L(name), styles);
}
Is the item big enough
64-bit
Refuses a hatch with fewer than 3 corners or under 100 mm², and a line under 10 mm.
L = Σ √(Δx² + Δy²); refuse when L < 10 mm or |area| < 100 mm²
The length is a sum of Math.hypot over the segments in order. Both limits are absolute values in millimeters.
drafting.js
lines 126 to 134
BimCraft 0.8.1
function check(d) {
if (d.kind === 'hatch') {
if (d.pts.length < 3 || Math.abs(area2(d.pts)) < 100) refuse('model.err.dr.area');
if (!PATTERN_KEYS.includes(d.pattern)) refuse('model.err.dr.pattern');
} else {
let L = 0; for (let i = 1; i < d.pts.length; i++) L += Math.hypot(d.pts[i][0] - d.pts[i - 1][0], d.pts[i][1] - d.pts[i - 1][1]);
if (d.pts.length < 2 || L < 10) refuse('model.err.dr.short');
}
}
Move drafting items
64-bit
Every point of the picked items moved by one offset.
(x, y) → (x + dx, y + dy)
The points are read from the file, the offset is added in 64-bit and the shape is written again, so each move passes through the writer's rounding once.
drafting.js
lines 174 to 182
BimCraft 0.8.1
export function moveDrafting(guids, dx, dy) {
return op((w) => {
if (!Number.isFinite(dx) || !Number.isFinite(dy)) refuse('model.err.length');
const list = findAll(w, guids);
const mv = (pts) => pts.map(([x, y]) => [x + dx, y + dy]);
for (const d of list) writeShape(w, d.id, { ...d, pts: mv(d.pts), holes: d.holes.map(mv) });
return { id: list[0].id, ids: list.map((d) => d.id) };
});
}
Is a point inside the region
64-bit
Whether a point lies inside the outline and outside its holes.
even-odd count of edges crossed by a ray toward +x
A plain ray-crossing test with no tolerance. An edge is tested only when its ends lie on opposite sides of the ray, so yj − yi is never zero in the division. A point exactly on an edge can fall either way.
drafting.js
lines 197 to 205
BimCraft 0.8.1
// ------------------------------------------------------------------------------------------------------- the patterns
function inRings(x, y, rings) {
let inside = false;
for (const p of rings) for (let i = 0, j = p.length - 1; i < p.length; j = i++) {
const [xi, yi] = p[i], [xj, yj] = p[j];
if ((yi > y) !== (yj > y) && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
}
return inside;
}
Clip a stroke to the region
64-bit
The pieces of a straight stroke that lie inside the region.
t = ((c − a) × e) ÷ (d × e), u = ((c − a) × d) ÷ (d × e); keep 0 < t < 1, 0 ≤ u ≤ 1
The divisor is the cross product of the stroke and the edge; below 1e-12 in absolute value the two are treated as parallel. That limit is not scaled to the lengths of the stroke and the edge. Pieces shorter than 1e-9 of the stroke are skipped and the middle of each piece decides inside or outside.
drafting.js
lines 206 to 227
BimCraft 0.8.1
export function clipSegment(rings, a, b) {
const dx = b[0] - a[0], dy = b[1] - a[1], ts = [0, 1];
for (const p of rings) for (let i = 0; i < p.length; i++) {
const c = p[i], d = p[(i + 1) % p.length], ex = d[0] - c[0], ey = d[1] - c[1];
const den = dx * ey - dy * ex;
if (Math.abs(den) < 1e-12) continue;
const t = ((c[0] - a[0]) * ey - (c[1] - a[1]) * ex) / den, u = ((c[0] - a[0]) * dy - (c[1] - a[1]) * dx) / den;
if (t > 0 && t < 1 && u >= 0 && u <= 1) ts.push(t);
}
ts.sort((m, n) => m - n);
const out = [];
for (let i = 0; i + 1 < ts.length; i++) {
const t0 = ts[i], t1 = ts[i + 1];
if (t1 - t0 < 1e-9) continue;
const tm = (t0 + t1) / 2;
if (!inRings(a[0] + dx * tm, a[1] + dy * tm, rings)) continue;
const last = out[out.length - 1];
const p0 = [a[0] + dx * t0, a[1] + dy * t0], p1 = [a[0] + dx * t1, a[1] + dy * t1];
if (last && Math.hypot(last[1][0] - p0[0], last[1][1] - p0[1]) < 1e-6) last[1] = p1; else out.push([p0, p1]);
}
return out;
}
Strokes of a hatch pattern
64-bit
The lines, dots or zigzags of a pattern inside a region, at its scale.
line k of a family: n · p = ⌊(n_min − offset) ÷ gap⌋ × gap + k × gap, gap = pattern gap × scale
The first line is placed at a whole multiple of the gap from the plan's origin; the following lines are reached by adding the gap repeatedly. The angle is degrees × π ÷ 180 with Math.PI. When the estimate of strokes passes 30,000 the scale is doubled until it does not, so a very fine pattern is drawn coarser than stored.
drafting.js
lines 228 to 272
BimCraft 0.8.1
/** A pattern's strokes inside a region. scale multiplies its spacing; a scale too fine for the region is made coarser. */
export function hatchSegments(outer, holes = [], pattern = 'diagonal', scale = 1) {
const P = PATTERNS.find((p) => p.key === pattern);
const out = [];
if (!P || P.solid || !outer || outer.length < 3) return out;
const rings = [outer, ...(holes || [])];
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
for (const [x, y] of outer) { x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); }
const diag = Math.hypot(x1 - x0, y1 - y0) || 1;
let k = scaleOf(scale);
// how many strokes it would take; too many → coarser (the drawing stays readable and the page responsive)
const est = (kk) => (P.families.reduce((n, f) => n + diag / (f.gap * kk) * (1 + (f.group?.length || 0)), 0) + (P.dots ? (x1 - x0) * (y1 - y0) / (P.dots * kk * 2.5) ** 2 : 0) + (P.zigzag ? (x1 - x0) * (y1 - y0) / (P.zigzag * kk) ** 2 * 2 : 0));
while (est(k) > MAX_STROKES) k *= 2;
const push = (a, b) => { for (const [p, q] of clipSegment(rings, a, b)) out.push(p[0], p[1], q[0], q[1]); };
for (const f of P.families) {
const r = f.angle * Math.PI / 180, d = [Math.cos(r), Math.sin(r)], n = [-d[1], d[0]], gap = f.gap * k;
const corners = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
const ns = corners.map((c) => c[0] * n[0] + c[1] * n[1]), ds = corners.map((c) => c[0] * d[0] + c[1] * d[1]);
const nmin = Math.min(...ns), nmax = Math.max(...ns), dmin = Math.min(...ds) - 1, dmax = Math.max(...ds) + 1;
const offs = [0, ...(f.group || []).map((o) => o * k)];
for (let c = Math.floor((nmin - offs[offs.length - 1]) / gap) * gap; c <= nmax; c += gap) for (const o of offs) {
const m = c + o;
if (m < nmin || m > nmax) continue;
push([n[0] * m + d[0] * dmin, n[1] * m + d[1] * dmin], [n[0] * m + d[0] * dmax, n[1] * m + d[1] * dmax]);
}
}
if (P.dots) {
// concrete: dots (very short strokes) on a staggered grid, nudged by a fixed hash so they do not read as a grid
const s = P.dots * k * 2.5, dot = P.dots * k * 0.08;
for (let gy = Math.floor(y0 / s); gy * s <= y1; gy++) for (let gx = Math.floor(x0 / s) - 1; gx * s <= x1; gx++) {
const h = Math.abs(Math.sin(gx * 12.9898 + gy * 78.233) * 43758.5453) % 1, h2 = Math.abs(Math.sin(gx * 39.346 + gy * 11.135) * 24634.6345) % 1;
const x = gx * s + (gy % 2 ? s / 2 : 0) + (h - 0.5) * s * 0.6, y = gy * s + (h2 - 0.5) * s * 0.6;
if (inRings(x, y, rings)) out.push(x - dot / 2, y, x + dot / 2, y);
}
}
if (P.zigzag) {
// insulation: rows of zigzag, each row as high as one zag
const h = P.zigzag * k, p = h * 0.6;
for (let ry = Math.floor(y0 / h) * h; ry < y1; ry += h) for (let x = Math.floor(x0 / p) * p; x < x1; x += p) {
push([x, ry], [x + p / 2, ry + h]);
push([x + p / 2, ry + h], [x + p, ry]);
}
}
return out;
}
Pattern scale
64-bit
The scale of a hatch pattern as it is kept.
scale = min(50, max(0.1, v)), or 1 when v is not a positive number
The value is clamped, not rounded, here. It is rounded to 3 decimals when written to the CadCraft_Drafting property set.
The next point of a drafting line from typed coordinates, a typed offset, a typed length and angle, or a typed length along the pointer's direction.
polar: (x + L·cos(a·π ÷ 180), y + L·sin(a·π ÷ 180)); length only: last + L × (to − last) ÷ |to − last|
The typed numbers are used as parsed, in 64-bit. The angle is turned to radians with Math.PI, so a typed 90° gives a cosine of about 6e-17, not 0. The direction for a bare length is the unit vector to the pointer, with the divisor replaced by 1 when it is zero.
Shoelace area for the region check; segment and edge intersection by the two-line parametric solve; even-odd ray crossing for inside tests. Pattern lines are laid on a grid anchored at the plan's origin so neighboring regions line up.
Number types
64-bit numbers in arrays of [x, y].
Tolerances
Points closer than 0.5 mm to the one before are dropped. On reading, a polyline whose ends are within 0.01 mm is closed; on writing, a closed item's last point within 0.5 mm of its first is dropped. A line under 10 mm and a hatch under 100 mm² are refused. Clipping: a divisor below 1e-12 (absolute) is parallel, pieces shorter than 1e-9 of the stroke are skipped, pieces within 1e-6 mm are joined. Pattern scale is held between 0.1 and 50.
Limits
The stored scale is rounded to 3 decimals and the hatch angle written to the file to 9 decimals. A pattern that would need more than 30,000 strokes is drawn at twice the spacing, repeatedly, until it fits: the drawn spacing is then not the stored one.
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.