Turns drawing objects into the entities a DWG file holds: dimensions with their defining points, hatches with pattern lines, text, viewports, sheets, lineweights and extents.
How the precision is kept
Coordinates, radii, bulges, angles and scales are handed to the DWG writer as the 64-bit numbers the drawing holds. Two kinds of value are rounded here on the way: lineweights to whole hundredths of a millimeter (the format's unit) and the oblique angle of multi-line text to 6 decimals of a degree inside its format code. Pattern lines are scaled, turned and moved in 64-bit with 25.4 as the inch factor.
Open item. Write the oblique angle of multi-line text with full precision instead of 6 decimals of a degree.
Calculations under this policy
13 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.
Converts a lineweight in millimeters to the whole number the file stores.
Math.round(w × 100) for w ≥ 0; negative codes kept
The value kept in the file is rounded to 0.01 mm because the format stores a whole number. A lineweight that is not a whole number of hundredths comes back changed.
dwg-save.js
lines 93 to 94
CadCraft 0.8.2
// linetypes and lineweights (0.4 C8): DWG keeps lineweights in 1/100 mm (-1 by layer, -2 by block, -3 default)
const lw100 = (w, absent) => (w === undefined || w === null ? absent : w < 0 ? w : Math.round(w * 100));
Spline: fit points or control points
64-bit
Decides whether a spline's fit points are written with it, by checking that they still give its control points.
The control points are recomputed from the fit points and compared with a tolerance scaled to the largest coordinate. Control points, knots and weights are written as they are either way; only the fit points and a flag depend on the test.
Moves baseline-anchored text to a top anchor one text height up, and writes the oblique angle as a format code.
anchor = (x, y) + R(rot) × (0, h); code = \Q followed by degrees to 6 decimals
The anchor shift is one rotation of (0, h) in 64-bit. The oblique angle is converted to degrees and rounded with toFixed(6) before it is written into the text, so it reads back within 5e-7 degrees and not to 15 digits.
dwg-save.js
lines 274 to 277
CadCraft 0.8.2
let va = e.va || 'baseline', at = { x: e.x, y: e.y };
if (!(va in ROW)) { const d = mApply(mRotate(e.rot || 0), { x: 0, y: e.h }); at = { x: e.x + d.x, y: e.y + d.y }; va = 'top'; }
let body = rich ? e.mt : s.split('\n').map((l) => l.replace(/\\/g, '\\\\').replace(/\{/g, '\\{').replace(/\}/g, '\\}')).join('\\P');
if (e.obl && !(rich && /^\\Q/.test(e.mt))) body = '\\Q' + +(e.obl * 180 / Math.PI).toFixed(6) + ';' + body; // MTEXT has no oblique field: its format code (P.7)
Hatch boundary rings
64-bit
Writes a hatch's rings as closed polylines, or a 3 or 4 cornered solid fill as a SOLID.
Ring coordinates are copied; every vertex is written with bulge 0. The repeated closing point is dropped first (openRing, 1e-12 absolute). A SOLID's corners are written in the order 1, 2, 4, 3 the format expects.
dwg-save.js
lines 292 to 308
CadCraft 0.8.2
/** Solid fill → HATCH SOLID (a 3- or 4-cornered single ring → SOLID); a pattern → HATCH with the pattern's own lines. */
function hatch(e, base) {
const rings = (e.loops || []).map((l) => { const pts = []; for (let k = 0; k + 1 < l.length; k += 2) pts.push({ x: l[k], y: l[k + 1] }); return openRing(pts); }).filter((r) => r.length >= 3);
if (!rings.length) return [];
const z = Number.isFinite(e.z) ? e.z : 0;
if (e.solid && rings.length === 1 && rings[0].length <= 4 && !e.wipe) {
const [a, b, c, d = c] = rings[0];
return [{ ...base, type: 'SOLID', points: [P(a.x, a.y, z), P(b.x, b.y, z), P(d.x, d.y, z), P(c.x, c.y, z)], ...(z ? { elevation: z } : {}) }]; // SOLID corners run 1, 2, 4, 3
}
const loops = rings.map((r, i) => ({ flags: i ? 0 : 1, polyline: { closed: true, vertices: r.map((p) => ({ x: p.x, y: p.y, bulge: 0 })) } }));
const more = { ...(z ? { elevation: z } : {}), style: ISLAND[e.island] ?? 0 }; // island detection (P.7): normal 0, outer 1, ignore 2
if (e.solid) return [{ ...base, type: 'HATCH', pattern: 'SOLID', solid: true, loops, ...more }];
const lines = patternLines(e);
if (!lines) note('hatchUnknown');
return [{ ...base, type: 'HATCH', pattern: String(e.pattern || 'USER').toUpperCase().slice(0, 31), solid: false, patternType: PATTERNS[String(e.pattern || '').toUpperCase()] ? 1 : 2,
angle: e.pangle || 0, scale: e.pscale > 0 ? e.pscale : 1, loops, lines: lines || [], ...more }];
}
Hatch pattern lines for the file
64-bit
The pattern's line families in drawing units: angle in radians, base point and offset in world axes.
k = scale ÷ (1 metric, 25.4 otherwise); base = R(turn) × (x, y) × k + origin; offset = (dx cos a − dy sin a, dx sin a + dy cos a)
25.4 is exact. The family angle is kept in degrees, has the pattern's turn added as turn × 180 ÷ π, and is converted back with × π ÷ 180, two rounded conversions. The pattern origin is added to every base point.
dwg-save.js
lines 309 to 325
CadCraft 0.8.2
/** The pattern's line families in drawing units, as DWG keeps them (angle in radians, base, offset in world axes). */
function patternLines(e) {
let fams = null;
if (Array.isArray(e.patlines) && e.patlines.length) fams = e.patlines;
else {
const def = PATTERNS[String(e.pattern || '').toUpperCase()];
if (!def) return null;
const k = (e.pscale > 0 ? e.pscale : 1) / (hatchIsMetric(e, doc) ? 1 : 25.4), turn = e.pangle || 0;
const c = Math.cos(turn), s = Math.sin(turn);
fams = def.lines.map((f) => ({ angle: f.angle + turn * 180 / Math.PI, x: (f.x * c - f.y * s) * k, y: (f.x * s + f.y * c) * k, dx: f.dx * k, dy: f.dy * k, dashes: f.dashes.map((d) => d * k) }));
}
const ox = Number.isFinite(e.ox) ? e.ox : 0, oy = Number.isFinite(e.oy) ? e.oy : 0; // the pattern's origin (P.7) moves every line, as hatch.js draws it
return fams.map((f) => {
const a = f.angle * Math.PI / 180, ca = Math.cos(a), sa = Math.sin(a);
return { angle: a, base: { x: f.x + ox, y: f.y + oy }, offset: { x: f.dx * ca - f.dy * sa, y: f.dx * sa + f.dy * ca }, dashes: (f.dashes || []).slice() };
});
}
Dimension points and measurement
64-bit
The defining points, text point and measurement written for a dimension.
measurement = dimMeasure; linear: defPoint = end of the dimension line on the second extension line; radial: near = c + r × (cos a, sin a), far = c − r × (cos a, sin a)
The measurement is the full 64-bit value from dims.js, not the rounded text. For radius and diameter the point on the curve is recomputed from r and the angle to the text point, so it lies on the circle to rounding but is not the point that was picked.
dwg-save.js
lines 327 to 362
CadCraft 0.8.2
/** A dimension → DIMENSION (its points in DXF meaning) + the unnamed block that draws it. */
function dimension(e, base) {
const own = e.block && doc.blocks.get(e.block);
const name = '*D' + ++dimN;
blocks.push({ name, base: P(0, 0), entities: (own ? own.entities : dimGeometry(e)).flatMap(conv) });
note('dim');
const st = { ...DIM_DEFAULTS, ...(e.st || {}) };
const d = { ...base, type: 'DIMENSION', dimType: DIM_TYPE[e.kind] ?? 0, block: name, style: dimStyleName(e.dimstyle), textMid: P(...xy(dimTextPoint(e))),
text: e.text || '', measurement: dimMeasure(e), attach: 5, ...(e.trot ? { textRotation: e.trot } : {}) };
d.flags = d.dimType | 32 | 128; // 32: its block is its own; 128: the text sits where it was put
switch (e.kind) {
case 'linear': case 'aligned': {
const { q2, r } = dimLinePoints(e); // where the dimension line meets the second extension line (slanted ones included)
Object.assign(d, { defPoint: P(q2.x, q2.y), xline1: P(e.p1.x, e.p1.y), xline2: P(e.p2.x, e.p2.y), angle: e.kind === 'linear' ? r : 0, ...(Number.isFinite(e.obl) && e.obl ? { obliqueAngle: e.obl } : {}) });
break;
}
case 'radius': case 'diameter': {
const r = Math.hypot(e.p1.x - e.c.x, e.p1.y - e.c.y);
const dl = Math.hypot(e.loc.x - e.c.x, e.loc.y - e.c.y);
const a = dl > 1e-12 ? Math.atan2(e.loc.y - e.c.y, e.loc.x - e.c.x) : Math.atan2(e.p1.y - e.c.y, e.p1.x - e.c.x);
const near = P(e.c.x + r * Math.cos(a), e.c.y + r * Math.sin(a)), far = P(e.c.x - r * Math.cos(a), e.c.y - r * Math.sin(a));
Object.assign(d, { defPoint: e.kind === 'radius' ? P(e.c.x, e.c.y) : far, center: near, leaderLen: 0 });
break;
}
case 'angular':
Object.assign(d, { defPoint: P(e.loc.x, e.loc.y), xline1: P(e.p1.x, e.p1.y), xline2: P(e.p2.x, e.p2.y), center: P(e.c.x, e.c.y) });
break;
default: note('other'); return [{ ...base, type: 'INSERT', block: name, insert: P(0, 0) }];
}
// this dimension's own settings, as AutoCAD keeps per-dimension overrides (ACAD "DSTYLE" xdata: code, value, …), every
// variable one for one (dwg-props.js); what no variable holds exactly (arrowhead names, text style, true colours) in CADCRAFT
d.xdata = { ACAD: dstyleItems(st) };
const cc = ccDimItems(st);
if (cc.length) d.xdata.CADCRAFT = cc;
return [d];
}
Viewport view height
64-bit
The model height a sheet viewport shows, and its view center.
view height = h ÷ s (s taken as 1 when not above 0)
One 64-bit division of the viewport's paper height by its scale. The reader divides the other way (h ÷ view height), so a scale goes out and comes back through two divisions and can differ in its last digit.
dwg-save.js
lines 387 to 397
CadCraft 0.8.2
/** A sheet's viewport → VIEWPORT (paper units; the model view at its centre and scale). */
function viewport(e, base, id = 0) {
// status (DXF 90): 0x8060 as AutoCAD writes it; + 16384 display locked, + 131072 viewport off (P.7)
const status = 0x8060 | (e.locked ? 16384 : 0) | (e.on === false ? 131072 : 0);
const frz = Array.isArray(e.frz) ? e.frz.filter((n) => typeof n === 'string' && n) : []; // layers frozen in it (VP.1): names, cc-dwg finds the records
// its layer overrides (VP.3) go into the layers' XRECORDs, which name this viewport by handle
const lov = !modelOnly && e.lov && typeof e.lov === 'object' && Object.keys(e.lov).length ? e.lov : null;
const vh = lov ? 'CC' + (0x7C000000 + vpOverrides.length).toString(16).toUpperCase() : null;
if (lov) vpOverrides.push({ h: vh, lov });
return { ...base, ...(vh ? { handle: vh } : {}), ...(Number.isFinite(e.tw) && e.tw ? { twist: e.tw } : {}), type: 'VIEWPORT', center: P(e.x, e.y), width: e.w, height: e.h, viewCenter: dcsOf(e), viewTarget: P(0, 0), viewHeight: e.h / (e.s > 0 ? e.s : 1), id, status, ...(frz.length ? { frozenLayers: frz } : {}) };
}
Sheet paper size and limits
64-bit
The paper size in millimeters, the sheet's limits and the sheet viewport's center.
paper mm = w × 25.4 (inch sheets) or w; limits = (x₀, y₀) to (x₀ + w, y₀ + h); center = (x₀ + w ÷ 2, y₀ + h ÷ 2)
25.4 is exact. The paper's corner and size are also written unchanged as 64-bit values in the sheet's own record, so they do not depend on the millimeter conversion.
The smallest and largest x and y of everything in model space, written to the file's header.
min and max over the bounds of every object
Comparisons only; the result is one of the objects' own bound values. The bounds of each object come from geom.js.
dwg-save.js
lines 478 to 482
CadCraft 0.8.2
const ext = emptyBounds();
for (const e of doc.model.values()) {
const b = entityBounds(e, doc.blocks);
if (validBounds(b)) { ext.minX = Math.min(ext.minX, b.minX); ext.minY = Math.min(ext.minY, b.minY); ext.maxX = Math.max(ext.maxX, b.maxX); ext.maxY = Math.max(ext.maxY, b.maxY); }
}
Closing point of a ring
64-bit
Drops a ring's last point when it repeats the first.
|first − last| < 1e-12
The test is absolute, 1e-12 drawing units. At coordinates above about 8200 neighboring 64-bit numbers are farther apart than that, so there only an exactly repeated point is dropped.
dwg-save.js
lines 595 to 600
CadCraft 0.8.2
/** A closed ring's distinct corners (the repeated closing point dropped). */
function openRing(pts) {
const out = pts.slice();
if (out.length > 1 && Math.hypot(out[0].x - out[out.length - 1].x, out[0].y - out[out.length - 1].y) < 1e-12) out.pop();
return out;
}
Construction line direction
64-bit
The unit direction written for an infinite line or ray.
direction = (dx, dy) ÷ √(dx² + dy²)
Normalized with Math.hypot at save time. A zero direction writes nothing.
The origin and the x and y axis vectors of a user coordinate system.
x axis = (cos θ, sin θ); y axis = (−sin θ, cos θ)
The drawing keeps a UCS as an origin and an angle; the file keeps axis vectors, so the angle goes through cosine and sine and comes back through atan2 when read.
dwg-save.js
lines 491 to 492
CadCraft 0.8.2
// the current UCS and the named ones (DR.8; ucs.js { x, y, ang }): $UCSORG / $UCSXDIR / $UCSYDIR and the UCS table
const axes = (u) => ({ origin: P(u.x, u.y), xAxis: P(Math.cos(u.ang), Math.sin(u.ang)), yAxis: P(-Math.sin(u.ang), Math.cos(u.ang)) });
Linetype pattern length
64-bit
The total length of one repeat of a linetype's dashes and gaps.
length = Σ |elementᵢ|
The absolute values are added in pattern order as 64-bit numbers.
Direct copy of doubles; dimension points recomputed from the dimension's defining points; pattern line families scaled by pattern scale ÷ (1 or 25.4) and rotated.
Number types
64-bit numbers; lineweights as whole numbers of 1/100 mm.
Tolerances
A spline is written with fit data only when recomputed control points match within 1e-7 × max(1, largest |x| + |y|); a ring's repeated closing point is dropped within 1e-12 (absolute); a radial dimension's direction falls back to the picked point when the text point is within 1e-12 of the center.
Limits
Hatch boundaries are written as polygons with no bulges, as the drawing holds them.
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.