WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,543 @@
|
||||
// Beam Analysis drawings for the geometry entries in docs/ba-web-spec.md
|
||||
// section 4 -- registered on `window.ToolDrawings` so PA's app.js redrawOne
|
||||
// hook (tools/pa_web/static/app.js:697-706) calls us instead of PA's own
|
||||
// renderers for beam blocks. Drawn with the same pen as PA: the primitives
|
||||
// on `window.Draw` exported by tools/pa_web/static/draw.js (svgRoot, label,
|
||||
// dimH, dimV, ftIn, inch, el, clear, safeNum, clamp, mergeRanges, estWidth,
|
||||
// _stagger).
|
||||
//
|
||||
// Everything works off real-unit numbers (ft, in) straight out of the
|
||||
// geometry dict -- no pixels arrive from the server, all layout and scaling
|
||||
// happens in here. Plain ES2017, no modules, no dependencies beyond
|
||||
// window.Draw and window.Units (both loaded first). Never throws: any field
|
||||
// may be absent or non-numeric and that load (or its part of the drawing) is
|
||||
// simply skipped.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// draw.js exports its primitives on window.Draw (draw.js:730) and app.js
|
||||
// reaches them as D.* (app.js:11). Same here: alias the handful this file
|
||||
// uses into local names so the drawing code below reads like draw.js's
|
||||
// own renderers instead of wearing a `Draw.` prefix on every call.
|
||||
var D = window.Draw;
|
||||
var el = D.el, label = D.label, dimH = D.dimH, dimV = D.dimV;
|
||||
var svgRoot = D.svgRoot, safeNum = D.safeNum, estWidth = D.estWidth;
|
||||
var stagger = D._stagger;
|
||||
var ftIn = D.ftIn, inch = D.inch;
|
||||
|
||||
// ------------------------------------------------------------- geometry
|
||||
|
||||
// Which shape a block's geometry entry carries is decided by the KEYS on
|
||||
// the entry, never by page id: `extents.elevation` has the beam outline
|
||||
// plus supports only; `loads.elevation` adds point/line/moments;
|
||||
// `reinforcement.elevation` has the outline plus longitudinal bars and
|
||||
// stirrup zones; `reinforcement.section` is the cross-section.
|
||||
function isElevation(geom) {
|
||||
return geom && (geom.length_ft !== undefined || geom.supports !== undefined);
|
||||
}
|
||||
function isSection(geom) {
|
||||
return geom && (geom.width_in !== undefined || geom.type !== undefined);
|
||||
}
|
||||
function hasBars(geom) { return !!(geom && (geom.bars || geom.transverse)); }
|
||||
function hasLoads(geom) { return !!(geom && (geom.point || geom.line || geom.moments)); }
|
||||
|
||||
// ---------------------------------------------------------- beam outline
|
||||
|
||||
// Shared by every elevation: the beam drawn as a horizontal rectangle
|
||||
// (length_ft wide, height_in tall) with the height exaggerated the way PA's
|
||||
// section drawing exaggerates a thin strip, and the support pads under it.
|
||||
//
|
||||
// `topReserve` is the band above the member already claimed by the load
|
||||
// lanes (a small baseline when there are none); the member fills what is
|
||||
// left between that band and the overall-length dimension row at the
|
||||
// bottom, so load glyphs never collide with the outline.
|
||||
function drawBeam(geom, svg, W, H, topReserve) {
|
||||
var lengthFt = safeNum(geom.length_ft);
|
||||
if (lengthFt === null || lengthFt <= 0) lengthFt = null;
|
||||
var heightIn = safeNum(geom.height_in);
|
||||
if (heightIn === null || heightIn <= 0) heightIn = null;
|
||||
|
||||
var marginL = 60, marginR = 30;
|
||||
var drawW = W - marginL - marginR;
|
||||
var scale = drawW / (lengthFt || 1);
|
||||
var dimY = H - 14; // overall-length dimension row (draw.js dimH tick ~6)
|
||||
var padH = 12; // bearing-pad projection below the member
|
||||
|
||||
// Member height: exaggerated up to the space available between the load
|
||||
// lanes and the pads, never beyond.
|
||||
var exaggerate = 1, rectH = 0;
|
||||
if (heightIn !== null) {
|
||||
var trueH = (heightIn / 12) * scale;
|
||||
var avail = Math.max(20, dimY - topReserve - padH - 10);
|
||||
exaggerate = Math.max(1, Math.min(8, avail / (trueH || 1)));
|
||||
rectH = Math.min(avail, trueH * exaggerate);
|
||||
}
|
||||
var rectY = topReserve + 4;
|
||||
var rectX = marginL;
|
||||
|
||||
function xAt(ft) { return rectX + ft * scale; }
|
||||
|
||||
var out = {
|
||||
xAt: xAt, scale: scale, exaggerate: exaggerate,
|
||||
rectX: rectX, rectY: rectY, rectW: drawW, rectH: rectH,
|
||||
marginL: marginL, marginR: marginR, drawW: drawW,
|
||||
dimY: dimY, padH: padH, lengthFt: lengthFt, heightIn: heightIn,
|
||||
topReserve: topReserve
|
||||
};
|
||||
|
||||
// Beam outline -- only when there is a length to place it by.
|
||||
if (lengthFt !== null) {
|
||||
el('rect', {
|
||||
x: rectX, y: rectY, width: drawW, height: rectH,
|
||||
fill: '#f4f6fb', stroke: '#1b1e23', 'stroke-width': 1.5
|
||||
}, svg);
|
||||
}
|
||||
|
||||
// Supports: bearing pads the width of their own length_ft, drawn as a
|
||||
// pad projecting below the beam, with a filled triangle glyph centred on
|
||||
// the pad centreline (the bearing point). Pads that would run past the
|
||||
// beam end are clipped to the beam face.
|
||||
(geom.supports || []).forEach(function (s) {
|
||||
if (!s || typeof s !== 'object') return;
|
||||
var x = safeNum(s.x_ft);
|
||||
if (x === null) return;
|
||||
var len = safeNum(s.length_ft);
|
||||
if (len === null || len < 0) len = 0;
|
||||
var kind = s.kind;
|
||||
var tri = (kind || '').toLowerCase() === 'pin' ? '#1b1e23' : '#475569';
|
||||
var cx = xAt(x);
|
||||
var padL = xAt(Math.max(0, x - len / 2));
|
||||
var padR = xAt(Math.min(lengthFt || 0, x + len / 2));
|
||||
if (padR - padL < 2) { padL = cx - 3; padR = cx + 3; }
|
||||
var padTop = rectY + rectH;
|
||||
var padBot = Math.min(dimY - 8, padTop + padH);
|
||||
if (padBot > padTop) {
|
||||
el('rect', {
|
||||
x: padL, y: padTop, width: padR - padL, height: padBot - padTop,
|
||||
fill: '#e2e8f0', stroke: '#1b1e23', 'stroke-width': 1.2
|
||||
}, svg);
|
||||
}
|
||||
// Triangle glyph at the centreline, apex at the beam underside (the
|
||||
// support reaction point), base at the pad bottom. With no member
|
||||
// height the apex lands on the drawn member line itself.
|
||||
var apexY = rectH ? padTop : rectY;
|
||||
el('polygon', {
|
||||
points: [cx - 6, padBot, cx + 6, padBot, cx, apexY].join(' '),
|
||||
fill: tri, stroke: tri
|
||||
}, svg);
|
||||
});
|
||||
|
||||
// Overall length dimension, below the pads, in feet-inches.
|
||||
if (lengthFt !== null) {
|
||||
dimH(svg, rectX, rectX + drawW, dimY, ftIn(lengthFt));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- load
|
||||
//
|
||||
// Loads are drawn ABOVE the beam in lanes: line loads at the top (their
|
||||
// band + hatch is the tallest glyph), moments below them, and point-load
|
||||
// arrows immediately above the member so their arrowheads land on the beam
|
||||
// top without crossing another lane.
|
||||
|
||||
// Measure the lanes first (so the beam can be pushed below them), draw
|
||||
// them after. Returns null when the geometry carries no loads; otherwise
|
||||
// { topReserve, lines, points, moments } with every y absolute in canvas.
|
||||
function loadLanes(geom, xAt) {
|
||||
var points = [], lines = [], moments = [];
|
||||
(geom.line || []).forEach(function (ln) {
|
||||
if (!ln || typeof ln !== 'object') return;
|
||||
var x0 = safeNum(ln.x0_ft), x1 = safeNum(ln.x1_ft);
|
||||
if (x0 === null || x1 === null) return;
|
||||
if (x1 < x0) { var t = x0; x0 = x1; x1 = t; }
|
||||
var px0 = xAt(x0), px1 = xAt(x1);
|
||||
if (px1 - px0 >= 2) lines.push({ px0: px0, px1: px1 });
|
||||
});
|
||||
(geom.point || []).forEach(function (p) {
|
||||
if (!p || typeof p !== 'object') return;
|
||||
var x = safeNum(p.x_ft);
|
||||
if (x !== null) points.push({ px: xAt(x), id: p.id });
|
||||
});
|
||||
(geom.moments || []).forEach(function (m) {
|
||||
if (!m || typeof m !== 'object') return;
|
||||
var x = safeNum(m.x_ft);
|
||||
if (x !== null) moments.push({ px: xAt(x), id: m.id });
|
||||
});
|
||||
if (!points.length && !lines.length && !moments.length) return null;
|
||||
|
||||
// Line loads: one 26px lane each (label above a band + hatch).
|
||||
var lane = 0;
|
||||
lines.forEach(function (ln) {
|
||||
ln.y = 40 + lane * 26; // label row of this lane
|
||||
lane++;
|
||||
});
|
||||
var laneH = lane * 26;
|
||||
|
||||
// Moment arcs sit below the line lanes, above the point-load arrows.
|
||||
var momentH = moments.length ? 44 : 0;
|
||||
moments.forEach(function (m, i) {
|
||||
m.y = 40 + laneH + 12 + (moments.length > 1 ? i * 24 : 0); // arc centre row
|
||||
});
|
||||
|
||||
// Point-load arrows are drawn lowest so their tips reach the beam top;
|
||||
// their labels are staggered so close loads never print on top of each
|
||||
// other. The reserve must hold the deepest stagger level.
|
||||
var pItems = points.map(function (p) {
|
||||
var w = estWidth(p.id || '?', 10);
|
||||
return { x0: p.px - w / 2, x1: p.px + w / 2 };
|
||||
});
|
||||
var pLevels = stagger(pItems, 6);
|
||||
var maxLvl = 0;
|
||||
pLevels.forEach(function (l) { if (l > maxLvl) maxLvl = l; });
|
||||
var pointH = points.length ? 36 + maxLvl * 16 : 0;
|
||||
|
||||
var topReserve = 40 + laneH + momentH + pointH;
|
||||
points.forEach(function (p, i) {
|
||||
p.y = topReserve - pointH + 8 + pLevels[i] * 16; // arrow tail row
|
||||
});
|
||||
|
||||
return {
|
||||
topReserve: topReserve,
|
||||
lines: lines, points: points, moments: moments
|
||||
};
|
||||
}
|
||||
|
||||
// One glyph per load, labelled with the id ONLY -- the case values are too
|
||||
// dense to print on an elevation (the tables carry them). `lanes` holds the
|
||||
// measured positions; `out` is the beam layout whose top the arrows land
|
||||
// on.
|
||||
function drawLoads(geom, lanes, svg, out) {
|
||||
var beamTop = out.rectY;
|
||||
|
||||
// Line loads: a band along their own extent with hatch below.
|
||||
lanes.lines.forEach(function (ln) {
|
||||
var by = ln.y + 13; // band line sits under the label row
|
||||
el('line', { x1: ln.px0, y1: by, x2: ln.px1, y2: by, stroke: '#0ea5e9', 'stroke-width': 4 }, svg);
|
||||
var ticks = Math.max(2, Math.round((ln.px1 - ln.px0) / 18));
|
||||
for (var i = 0; i <= ticks; i++) {
|
||||
var tx = ln.px0 + (ln.px1 - ln.px0) * (i / ticks);
|
||||
el('line', { x1: tx, y1: by, x2: tx, y2: by + 8, stroke: '#0ea5e9', 'stroke-width': 1 }, svg);
|
||||
}
|
||||
});
|
||||
|
||||
// Line-load ids: draw after the bands so label backgrounds sit on top.
|
||||
var li = 0;
|
||||
(geom.line || []).forEach(function (ln) {
|
||||
if (!ln || typeof ln !== 'object') return;
|
||||
var x0 = safeNum(ln.x0_ft), x1 = safeNum(ln.x1_ft);
|
||||
if (x0 === null || x1 === null) return;
|
||||
if (x1 < x0) { var t = x0; x0 = x1; x1 = t; }
|
||||
if (lanes.lines[li]) {
|
||||
label(svg, (lanes.lines[li].px0 + lanes.lines[li].px1) / 2,
|
||||
lanes.lines[li].y - 2, ln.id || '?', { size: 10, fill: '#0ea5e9' });
|
||||
}
|
||||
li++;
|
||||
});
|
||||
|
||||
// Point loads: a down arrow whose tip reaches the beam top.
|
||||
lanes.points.forEach(function (p, i) {
|
||||
var tipY = beamTop - 2;
|
||||
el('line', { x1: p.px, y1: p.y, x2: p.px, y2: tipY - 10, stroke: '#2f6fed', 'stroke-width': 2 }, svg);
|
||||
el('polygon', {
|
||||
points: [p.px - 4, tipY - 12, p.px + 4, tipY - 12, p.px, tipY - 2].join(' '),
|
||||
fill: '#2f6fed'
|
||||
}, svg);
|
||||
var pid = (geom.point[i] && geom.point[i].id) || '?';
|
||||
label(svg, p.px, p.y - 4, pid, { size: 10, fill: '#2f6fed' });
|
||||
});
|
||||
|
||||
// Moments: a curved-arrow (rotation) glyph with the id below it.
|
||||
lanes.moments.forEach(function (m, i) {
|
||||
var cy = m.y;
|
||||
var r = 9;
|
||||
var x0 = m.px + r, y0 = cy;
|
||||
var x1 = m.px + r * Math.cos(-2.4), y1 = cy + r * Math.sin(-2.4);
|
||||
el('path', {
|
||||
d: 'M ' + x0 + ' ' + y0 + ' A ' + r + ' ' + r + ' 0 1 0 ' + x1 + ' ' + y1,
|
||||
fill: 'none', stroke: '#d33', 'stroke-width': 1.8
|
||||
}, svg);
|
||||
var tipX = m.px + r * Math.cos(-0.4), tipY = cy + r * Math.sin(-0.4);
|
||||
var ang = -0.4 - Math.PI / 2;
|
||||
el('polygon', {
|
||||
points: [tipX, tipY,
|
||||
tipX + 6 * Math.cos(ang + 0.5), tipY + 6 * Math.sin(ang + 0.5),
|
||||
tipX + 6 * Math.cos(ang - 0.5), tipY + 6 * Math.sin(ang - 0.5)].join(' '),
|
||||
fill: '#d33'
|
||||
}, svg);
|
||||
var mid = (geom.moments[i] && geom.moments[i].id) || '?';
|
||||
label(svg, m.px, cy + r + 16, mid, { size: 10, fill: '#d33' });
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- bar
|
||||
|
||||
// Longitudinal bar rows on the reinforcement elevation: one horizontal line
|
||||
// at the bar's y_in (measured up from the beam soffit), with a hook tick at
|
||||
// the end whose condition is 'hook'. Straight ends get no glyph. A bar can
|
||||
// only be placed once the outline has a height -- with no height entered
|
||||
// there is nowhere to put it, and it is skipped (geometry problems name it).
|
||||
function drawBars(geom, svg, out) {
|
||||
if (!out.heightIn) return;
|
||||
var hPx = out.rectH / out.heightIn; // pixels per inch of depth (already exaggerated)
|
||||
(geom.bars || []).forEach(function (b) {
|
||||
if (!b || typeof b !== 'object') return;
|
||||
var x0 = safeNum(b.x0_ft), x1 = safeNum(b.x1_ft);
|
||||
if (x0 === null || x1 === null) return;
|
||||
if (x1 < x0) { var t = x0; x0 = x1; x1 = t; }
|
||||
var yIn = safeNum(b.y_in);
|
||||
if (yIn === null) return;
|
||||
if (yIn < 0) yIn = 0;
|
||||
if (yIn > out.heightIn) yIn = out.heightIn;
|
||||
// y_in from the BOTTOM of the section; SVG y runs down from the top of
|
||||
// the outline.
|
||||
var y = out.rectY + out.rectH - yIn * hPx;
|
||||
var px0 = out.xAt(x0), px1 = out.xAt(x1);
|
||||
el('line', { x1: px0, y1: y, x2: px1, y2: y, stroke: '#1b1e23', 'stroke-width': 1.6 }, svg);
|
||||
// Hook: a short perpendicular tick turned toward the member interior
|
||||
// (up for a bar in the bottom half, down for one in the top half).
|
||||
var hookUp = yIn < out.heightIn / 2;
|
||||
if ((b.start_cond || '').toLowerCase() === 'hook') {
|
||||
var hy = hookUp ? y - 6 : y + 6;
|
||||
el('line', { x1: px0, y1: y, x2: px0, y2: hy, stroke: '#1b1e23', 'stroke-width': 1.6 }, svg);
|
||||
}
|
||||
if ((b.end_cond || '').toLowerCase() === 'hook') {
|
||||
var hy2 = hookUp ? y - 6 : y + 6;
|
||||
el('line', { x1: px1, y1: y, x2: px1, y2: hy2, stroke: '#1b1e23', 'stroke-width': 1.6 }, svg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Transverse ("stirrup") zones on the reinforcement elevation: vertical
|
||||
// ticks over x0..x1 at the true spacing_in pitch -- the first tick at x0
|
||||
// (the face of the first stirrup) and one every pitch after it, ticks that
|
||||
// would land past x1 dropped. Cap at 60 ticks per zone: a 4" pitch over a
|
||||
// 40' span would otherwise try to draw 120 hairlines the browser then has
|
||||
// to rasterise. The band spans the member depth inside a small inset so it
|
||||
// reads as ties around the bars. `legs > 2` adds a second hairline.
|
||||
function drawTransverse(geom, svg, out) {
|
||||
(geom.transverse || []).forEach(function (z) {
|
||||
if (!z || typeof z !== 'object') return;
|
||||
var x0 = safeNum(z.x0_ft), x1 = safeNum(z.x1_ft);
|
||||
if (x0 === null || x1 === null) return;
|
||||
if (x1 < x0) { var t = x0; x0 = x1; x1 = t; }
|
||||
var spacingIn = safeNum(z.spacing_in);
|
||||
if (spacingIn === null || spacingIn <= 0) return;
|
||||
var yTop = out.rectY, yBot = out.rectY + out.rectH;
|
||||
if (out.heightIn) {
|
||||
// Stirrups wrap the steel inside the cover: draw the band inset from
|
||||
// the outline faces so it reads as ties around the bars, not as a
|
||||
// solid fill of the whole depth.
|
||||
var inset = Math.max(1.5, (Math.min(2, out.heightIn / 4) / out.heightIn) * out.rectH);
|
||||
yTop += inset;
|
||||
yBot -= inset;
|
||||
}
|
||||
if (yBot - yTop < 2) return;
|
||||
var legs = Math.max(1, Math.round(safeNum(z.legs) || 1));
|
||||
var px0 = out.xAt(x0), px1 = out.xAt(x1);
|
||||
if (px1 - px0 < 2) return;
|
||||
// Step from x0 at spacing_in (converted to feet); each tick is one
|
||||
// pitch on. The cap keeps an extreme zone from overdrawing.
|
||||
var count = Math.min(60, Math.floor((x1 - x0) / (spacingIn / 12)) + 1);
|
||||
for (var i = 0; i < count; i++) {
|
||||
var tx = px0 + i * (spacingIn / 12) * out.scale;
|
||||
if (tx > px1 + 0.5) break;
|
||||
el('line', { x1: tx, y1: yTop, x2: tx, y2: yBot, stroke: '#7c3aed', 'stroke-width': 0.9 }, svg);
|
||||
if (legs > 2) {
|
||||
el('line', { x1: tx + 2.5, y1: yTop, x2: tx + 2.5, y2: yBot, stroke: '#c4b5fd', 'stroke-width': 0.6 }, svg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- hooks
|
||||
|
||||
// `elevation(inst, state, host, geom)`: the beam outline + supports +
|
||||
// overall length (extents.elevation); load arrows/bands/moments when the
|
||||
// geometry carries loads (loads.elevation); longitudinal bars and stirrup
|
||||
// zones when it carries reinforcement (reinforcement.elevation). One glyph
|
||||
// per load or bar, ids only -- never the case values. Which variant is
|
||||
// drawn is decided by which keys the geometry entry carries, not page id.
|
||||
function elevation(inst, state, host, geom) {
|
||||
geom = geom || {};
|
||||
var aspect = (inst && inst.block && inst.block.aspect) || 0.24;
|
||||
var root = svgRoot(host, aspect, 1000);
|
||||
var svg = root.svg, W = root.W, H = root.H;
|
||||
|
||||
if (isSection(geom) && !isElevation(geom)) {
|
||||
// Defensive: a section entry mis-routed to the elevation fn.
|
||||
label(svg, W / 2, H / 2, '(cross-section data -- use the section drawing)', { size: 12, fill: '#6b7280' });
|
||||
return;
|
||||
}
|
||||
|
||||
var lengthFt = safeNum(geom.length_ft);
|
||||
if (lengthFt === null || lengthFt <= 0) lengthFt = null;
|
||||
if (lengthFt === null && !hasLoads(geom) && !hasBars(geom)) {
|
||||
label(svg, W / 2, H / 2, 'Enter a beam length to see the elevation.', { size: 12, fill: '#6b7280' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Reserve the load lanes ABOVE the member before the beam is laid out,
|
||||
// so the outline never draws over a load glyph. Loads only render for
|
||||
// the loads.elevation entry (the reinforcement elevation carries bars).
|
||||
var scale = (W - 90) / (lengthFt || 1);
|
||||
var lanes = (hasLoads(geom) && !hasBars(geom))
|
||||
? loadLanes(geom, function (ft) { return 60 + ft * scale; })
|
||||
: null;
|
||||
var topReserve = lanes ? lanes.topReserve : 18;
|
||||
|
||||
var out = drawBeam(geom, svg, W, H, topReserve);
|
||||
|
||||
if (hasBars(geom)) {
|
||||
// reinforcement.elevation: bars + stirrup zones ON the outline.
|
||||
drawBars(geom, svg, out);
|
||||
drawTransverse(geom, svg, out);
|
||||
} else if (lanes) {
|
||||
// loads.elevation: one glyph per load above the outline.
|
||||
drawLoads(geom, lanes, svg, out);
|
||||
}
|
||||
|
||||
if (out.heightIn !== null && out.exaggerate > 1.1) {
|
||||
label(svg, out.marginL, 12, '(depth exaggerated ' + out.exaggerate.toFixed(1) + '\u00d7)', { anchor: 'start', size: 10, fill: '#6b7280' });
|
||||
}
|
||||
}
|
||||
|
||||
// `section(inst, state, host, geom)`: the cross-section (rectangular / L /
|
||||
// inverted_t), the bars as filled circles at their (x_in, y_in) -- y from
|
||||
// the soffit, converted to SVG's top-down y -- strands as smaller OPEN
|
||||
// circles, width/height dimensions in inches, and a `_stagger`ed label per
|
||||
// distinct bar size.
|
||||
function section(inst, state, host, geom) {
|
||||
geom = geom || {};
|
||||
var aspect = (inst && inst.block && inst.block.aspect) || 0.28;
|
||||
var root = svgRoot(host, aspect, 1000);
|
||||
var svg = root.svg, W = root.W, H = root.H;
|
||||
|
||||
var widthIn = safeNum(geom.width_in);
|
||||
var heightIn = safeNum(geom.height_in);
|
||||
if (widthIn === null || widthIn <= 0 || heightIn === null || heightIn <= 0) {
|
||||
label(svg, W / 2, H / 2, 'Enter the section width and height to see the section.', { size: 12, fill: '#6b7280' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Margins follow renderReinforcementSection: width dimension below the
|
||||
// section, height dimension to its left, labels in inches.
|
||||
var marginL = 70, marginR = 30, marginTop = 40, marginBottom = 56;
|
||||
var drawW = W - marginL - marginR;
|
||||
var drawH = H - marginTop - marginBottom;
|
||||
var scale = Math.min(drawW / widthIn, drawH / heightIn);
|
||||
var rectX = marginL + (drawW - widthIn * scale) / 2;
|
||||
var rectY = marginTop + (drawH - heightIn * scale) / 2;
|
||||
|
||||
function xAt(xIn) { return rectX + xIn * scale; }
|
||||
// y_in measured from the bottom of the section; SVG y runs down.
|
||||
function yAt(yIn) { return rectY + (heightIn - yIn) * scale; }
|
||||
|
||||
// The section outline mirrors `model.Section.rects`
|
||||
// (beamcalc/src/beamcalc/model.py:170-217) exactly -- that is the
|
||||
// canonical shape the engine analyses, so the picture and the numbers
|
||||
// cannot drift. Rects are (x0, y0, w, h) with y measured UP from the
|
||||
// bottom, x from the left. `height_in` is the OVERALL height
|
||||
// (stem_h + flange_t for L / inverted_t -- model.py:196,209); the stem
|
||||
// rectangle is drawn at FULL height and the ledge at the bottom.
|
||||
function addOutline(xIn, yBottomIn, wIn, hIn) {
|
||||
el('rect', {
|
||||
x: xAt(xIn), y: yAt(yBottomIn + hIn), width: Math.max(1, wIn * scale), height: Math.max(1, hIn * scale),
|
||||
fill: '#f4f6fb', stroke: '#1b1e23', 'stroke-width': 1.2
|
||||
}, svg);
|
||||
}
|
||||
|
||||
var type = String(geom.type || 'rectangular').toLowerCase().replace(/[\s_]+/g, '_');
|
||||
if (type === 'rect' || type.indexOf('rect') === 0) type = 'rectangular';
|
||||
var stemW = safeNum(geom.stem_width_in);
|
||||
var flangeW = safeNum(geom.flange_width_in);
|
||||
var flangeT = safeNum(geom.flange_thickness_in);
|
||||
var side = String(geom.side || 'left').toLowerCase();
|
||||
|
||||
if (type === 'inverted_t' || type === 'inverted_tee' || type === 'it') {
|
||||
// model.py:205-217 -- a full-height centred stem over a bottom flange
|
||||
// that runs out both sides: (flange_w, 0, stem_w, h) plus the two
|
||||
// side ledges (0, 0, flange_w, flange_t) and
|
||||
// (flange_w + stem_w, 0, flange_w, flange_t).
|
||||
if (flangeT !== null) {
|
||||
addOutline(0, 0, widthIn, flangeT);
|
||||
}
|
||||
if (stemW !== null && flangeW !== null) {
|
||||
addOutline(flangeW, 0, stemW, heightIn);
|
||||
}
|
||||
} else if (type === 'l') {
|
||||
// model.py:190-203 -- the stem occupies the full height; the ledge is
|
||||
// at the BOTTOM on one side. side=left: stem starts at x=flange_w and
|
||||
// the ledge fills the left; side=right is the mirror image.
|
||||
if (stemW !== null && flangeW !== null) {
|
||||
var stemX = side === 'right' ? 0 : flangeW;
|
||||
var ledgeX = side === 'right' ? stemW : 0;
|
||||
addOutline(stemX, 0, stemW, heightIn);
|
||||
if (flangeT !== null) addOutline(ledgeX, 0, flangeW, flangeT);
|
||||
} else if (flangeT !== null && flangeW !== null) {
|
||||
// No stem dims (malformed): still show the ledge rather than a blank.
|
||||
addOutline(side === 'right' ? Math.max(0, widthIn - flangeW) : 0, 0, flangeW, flangeT);
|
||||
}
|
||||
} else {
|
||||
// rectangular -- and anything unrecognised falls back to a plain box
|
||||
// rather than drawing nothing.
|
||||
addOutline(0, 0, widthIn, heightIn);
|
||||
}
|
||||
|
||||
// Bars: filled circles, diameter scaled to the section (capped so a #11
|
||||
// bar does not cover its neighbour), at their own x_in / y_in.
|
||||
(geom.bars || []).forEach(function (b) {
|
||||
if (!b || typeof b !== 'object') return;
|
||||
var xIn = safeNum(b.x_in), yIn = safeNum(b.y_in);
|
||||
if (xIn === null || yIn === null) return;
|
||||
var diaIn = safeNum(b.dia_in);
|
||||
var r = (diaIn !== null && diaIn > 0) ? Math.max(2, Math.min(6, diaIn * scale * 0.5)) : 3;
|
||||
el('circle', { cx: xAt(xIn), cy: yAt(yIn), r: r, fill: '#1b1e23' }, svg);
|
||||
});
|
||||
|
||||
// Strands: smaller OPEN circles (the PA convention for prestressing
|
||||
// steel in section).
|
||||
(geom.strands || []).forEach(function (s) {
|
||||
if (!s || typeof s !== 'object') return;
|
||||
var xIn = safeNum(s.x_in), yIn = safeNum(s.y_in);
|
||||
if (xIn === null || yIn === null) return;
|
||||
el('circle', { cx: xAt(xIn), cy: yAt(yIn), r: 3, fill: '#fff', stroke: '#2f6fed', 'stroke-width': 1.4 }, svg);
|
||||
});
|
||||
|
||||
dimH(svg, rectX, rectX + widthIn * scale, rectY + heightIn * scale + 16, inch(widthIn), { below: true });
|
||||
dimV(svg, rectY, rectY + heightIn * scale, rectX - 14, inch(heightIn), { left: true });
|
||||
|
||||
// One `_stagger`ed label per distinct bar size, spread across the width
|
||||
// just above the outline -- a legend that never prints over the concrete.
|
||||
// `_stagger` levels push a label down only when it would otherwise
|
||||
// overlap its neighbour's text.
|
||||
var sizes = [];
|
||||
(geom.bars || []).forEach(function (b) {
|
||||
if (!b || typeof b !== 'object') return;
|
||||
var size = b.size;
|
||||
if (!size || sizes.indexOf(size) !== -1) return;
|
||||
sizes.push(size);
|
||||
});
|
||||
if (sizes.length) {
|
||||
var gap = 14;
|
||||
var items = sizes.map(function (s, i) {
|
||||
var w = estWidth(s, 11) + gap;
|
||||
// Real x centres: spread the labels evenly across the section width.
|
||||
var cx = rectX + widthIn * scale * (i + 0.5) / sizes.length;
|
||||
return { x0: cx - w / 2, x1: cx + w / 2 };
|
||||
});
|
||||
var levels = stagger(items, 6);
|
||||
var lvlY = rectY - 10;
|
||||
sizes.forEach(function (s, i) {
|
||||
var cx = rectX + widthIn * scale * (i + 0.5) / sizes.length;
|
||||
label(svg, cx, lvlY + levels[i] * 13, s, { size: 11, fill: '#1b1e23' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.ToolDrawings = {
|
||||
elevation: elevation,
|
||||
section: section
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user