three-dxf
Version:
A dxf viewer for the browser using three.js.
810 lines (651 loc) • 28.9 kB
JavaScript
import * as THREE from 'three';
import { BufferGeometry, Color, Float32BufferAttribute, Vector3 } from 'three';
import { OrbitControls } from './OrbitControls';
import bSpline from './bspline';
import { Text } from 'troika-three-text'
import { parseDxfMTextContent } from '@dxfom/mtext';
const textControlCharactersRegex = /\\[AXQWOoLIpfH].*;/g;
const curlyBraces = /\\[{}]/g;
// Three.js extension functions. Webpack doesn't seem to like it if we modify the THREE object directly.
var THREEx = { Math: {} };
/**
* Returns the angle in radians of the vector (p1,p2). In other words, imagine
* putting the base of the vector at coordinates (0,0) and finding the angle
* from vector (1,0) to (p1,p2).
* @param {Object} p1 start point of the vector
* @param {Object} p2 end point of the vector
* @return {Number} the angle
*/
THREEx.Math.angle2 = function (p1, p2) {
var v1 = new THREE.Vector2(p1.x, p1.y);
var v2 = new THREE.Vector2(p2.x, p2.y);
v2.sub(v1); // sets v2 to be our chord
v2.normalize();
if (v2.y < 0) return -Math.acos(v2.x);
return Math.acos(v2.x);
};
THREEx.Math.polar = function (point, distance, angle) {
var result = {};
result.x = point.x + distance * Math.cos(angle);
result.y = point.y + distance * Math.sin(angle);
return result;
};
/**
* Calculates points for a curve between two points using a bulge value. Typically used in polylines.
* @param startPoint - the starting point of the curve
* @param endPoint - the ending point of the curve
* @param bulge - a value indicating how much to curve
* @param segments - number of segments between the two given points
*/
function getBulgeCurvePoints(startPoint, endPoint, bulge, segments) {
var vertex, i,
center, p0, p1, angle,
radius, startAngle,
thetaAngle;
var obj = {};
obj.startPoint = p0 = startPoint ? new THREE.Vector2(startPoint.x, startPoint.y) : new THREE.Vector2(0, 0);
obj.endPoint = p1 = endPoint ? new THREE.Vector2(endPoint.x, endPoint.y) : new THREE.Vector2(1, 0);
obj.bulge = bulge = bulge || 1;
angle = 4 * Math.atan(bulge);
radius = p0.distanceTo(p1) / 2 / Math.sin(angle / 2);
center = THREEx.Math.polar(startPoint, radius, THREEx.Math.angle2(p0, p1) + (Math.PI / 2 - angle / 2));
obj.segments = segments = segments || Math.max(Math.abs(Math.ceil(angle / (Math.PI / 18))), 6); // By default want a segment roughly every 10 degrees
startAngle = THREEx.Math.angle2(center, p0);
thetaAngle = angle / segments;
var vertices = [];
vertices.push(new THREE.Vector3(p0.x, p0.y, 0));
for (i = 1; i <= segments - 1; i++) {
vertex = THREEx.Math.polar(center, Math.abs(radius), startAngle + thetaAngle * i);
vertices.push(new THREE.Vector3(vertex.x, vertex.y, 0));
}
return vertices;
};
/**
* Viewer class for a dxf object.
* @param {Object} data - the dxf object
* @param {Object} parent - the parent element to which we attach the rendering canvas
* @param {Number} width - width of the rendering canvas in pixels
* @param {Number} height - height of the rendering canvas in pixels
* @param {Object} font - a font loaded with THREE.FontLoader
* @constructor
*/
export function Viewer(data, parent, width, height, font) {
createLineTypeShaders(data);
var scene = new THREE.Scene();
// Create scene from dxf object (data)
var i, entity, obj, min_x, min_y, min_z, max_x, max_y, max_z;
var dims = {
min: { x: 0, y: 0, z: 0 },
max: { x: 0, y: 0, z: 0 }
};
for (i = 0; i < data.entities.length; i++) {
entity = data.entities[i];
obj = drawEntity(entity, data);
if (obj) {
var bbox = new THREE.Box3().setFromObject(obj);
if (isFinite(bbox.min.x) && (dims.min.x > bbox.min.x)) dims.min.x = bbox.min.x;
if (isFinite(bbox.min.y) && (dims.min.y > bbox.min.y)) dims.min.y = bbox.min.y;
if (isFinite(bbox.min.z) && (dims.min.z > bbox.min.z)) dims.min.z = bbox.min.z;
if (isFinite(bbox.max.x) && (dims.max.x < bbox.max.x)) dims.max.x = bbox.max.x;
if (isFinite(bbox.max.y) && (dims.max.y < bbox.max.y)) dims.max.y = bbox.max.y;
if (isFinite(bbox.max.z) && (dims.max.z < bbox.max.z)) dims.max.z = bbox.max.z;
scene.add(obj);
}
obj = null;
}
width = width || parent.clientWidth;
height = height || parent.clientHeight;
var aspectRatio = width / height;
var upperRightCorner = { x: dims.max.x, y: dims.max.y };
var lowerLeftCorner = { x: dims.min.x, y: dims.min.y };
// Figure out the current viewport extents
var vp_width = upperRightCorner.x - lowerLeftCorner.x;
var vp_height = upperRightCorner.y - lowerLeftCorner.y;
var center = center || {
x: vp_width / 2 + lowerLeftCorner.x,
y: vp_height / 2 + lowerLeftCorner.y
};
// Fit all objects into current ThreeDXF viewer
var extentsAspectRatio = Math.abs(vp_width / vp_height);
if (aspectRatio > extentsAspectRatio) {
vp_width = vp_height * aspectRatio;
} else {
vp_height = vp_width / aspectRatio;
}
var viewPort = {
bottom: -vp_height / 2,
left: -vp_width / 2,
top: vp_height / 2,
right: vp_width / 2,
center: {
x: center.x,
y: center.y
}
};
var camera = new THREE.OrthographicCamera(viewPort.left, viewPort.right, viewPort.top, viewPort.bottom, 1, 19);
camera.position.z = 10;
camera.position.x = viewPort.center.x;
camera.position.y = viewPort.center.y;
var renderer = this.renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
renderer.setClearColor(0xfffffff, 1);
parent.appendChild(renderer.domElement);
parent.style.display = 'block';
//TODO: Need to make this an option somehow so others can roll their own controls.
var controls = new OrbitControls(camera, parent);
controls.target.x = camera.position.x;
controls.target.y = camera.position.y;
controls.target.z = 0;
controls.zoomSpeed = 3;
//Uncomment this to disable rotation (does not make much sense with 2D drawings).
//controls.enableRotate = false;
this.render = function () { renderer.render(scene, camera) };
controls.addEventListener('change', this.render);
this.render();
controls.update();
this.resize = function (width, height) {
var originalWidth = renderer.domElement.width;
var originalHeight = renderer.domElement.height;
var hscale = width / originalWidth;
var vscale = height / originalHeight;
camera.top = (vscale * camera.top);
camera.bottom = (vscale * camera.bottom);
camera.left = (hscale * camera.left);
camera.right = (hscale * camera.right);
// camera.updateProjectionMatrix();
renderer.setSize(width, height);
renderer.setClearColor(0xfffffff, 1);
this.render();
};
function drawEntity(entity, data) {
var mesh;
if (entity.type === 'CIRCLE' || entity.type === 'ARC') {
mesh = drawArc(entity, data);
} else if (entity.type === 'LWPOLYLINE' || entity.type === 'LINE' || entity.type === 'POLYLINE') {
mesh = drawLine(entity, data);
} else if (entity.type === 'TEXT') {
mesh = drawText(entity, data);
} else if (entity.type === 'SOLID') {
mesh = drawSolid(entity, data);
} else if (entity.type === 'POINT') {
mesh = drawPoint(entity, data);
} else if (entity.type === 'INSERT') {
mesh = drawBlock(entity, data);
} else if (entity.type === 'SPLINE') {
mesh = drawSpline(entity, data);
} else if (entity.type === 'MTEXT') {
mesh = drawMtext(entity, data);
} else if (entity.type === 'ELLIPSE') {
mesh = drawEllipse(entity, data);
} else if (entity.type === 'DIMENSION') {
var dimTypeEnum = entity.dimensionType & 7;
if (dimTypeEnum === 0) {
mesh = drawDimension(entity, data);
} else {
console.log("Unsupported Dimension type: " + dimTypeEnum);
}
}
else {
console.log("Unsupported Entity Type: " + entity.type);
}
return mesh;
}
function drawEllipse(entity, data) {
var color = getColor(entity, data);
var xrad = Math.sqrt(Math.pow(entity.majorAxisEndPoint.x, 2) + Math.pow(entity.majorAxisEndPoint.y, 2));
var yrad = xrad * entity.axisRatio;
var rotation = Math.atan2(entity.majorAxisEndPoint.y, entity.majorAxisEndPoint.x);
var curve = new THREE.EllipseCurve(
entity.center.x, entity.center.y,
xrad, yrad,
entity.startAngle, entity.endAngle,
false, // Always counterclockwise
rotation
);
var points = curve.getPoints(50);
var geometry = new THREE.BufferGeometry().setFromPoints(points);
var material = new THREE.LineBasicMaterial({ linewidth: 1, color: color });
// Create the final object to add to the scene
var ellipse = new THREE.Line(geometry, material);
return ellipse;
}
function drawMtext(entity, data) {
var color = getColor(entity, data);
if (!font) { return console.log('font parameter not set. Ignoring text entity.') }
var textAndControlChars = parseDxfMTextContent(entity.text);
//Note: We currently only support a single format applied to all the mtext text
var content = mtextContentAndFormattingToTextAndStyle(textAndControlChars, entity, color);
var txt = createTextForScene(content.text, content.style, entity, color);
if (!txt) return null;
var group = new THREE.Object3D();
group.add(txt);
return group;
}
function mtextContentAndFormattingToTextAndStyle(textAndControlChars, entity, color) {
let activeStyle = {
horizontalAlignment: 'left',
textHeight: entity.height
}
var text = [];
for (let item of textAndControlChars) {
if (typeof item === 'string') {
if (item.startsWith('pxq') && item.endsWith(';')) {
if (item.indexOf('c') !== -1)
activeStyle.horizontalAlignment = 'center';
else if (item.indexOf('l') !== -1)
activeStyle.horizontalAlignment = 'left';
else if (item.indexOf('r') !== -1)
activeStyle.horizontalAlignment = 'right';
else if (item.indexOf('j') !== -1)
activeStyle.horizontalAlignment = 'justify';
} else {
text.push(item);
}
} else if (Array.isArray(item)) {
var nestedFormat = mtextContentAndFormattingToTextAndStyle(item, entity, color);
text.push(nestedFormat.text);
} else if (typeof item === 'object') {
if (item['S'] && item['S'].length === 3) {
text.push(item['S'][0] + '/' + item['S'][2]);
} else {
// not yet supported.
}
}
}
return {
text: text.join(),
style: activeStyle
}
}
function createTextForScene(text, style, entity, color) {
if (!text) return null;
let textEnt = new Text();
textEnt.text = text
.replaceAll('\\P', '\n')
.replaceAll('\\X', '\n');
textEnt.font = font;
textEnt.fontSize = style.textHeight;
textEnt.maxWidth = entity.width;
textEnt.position.x = entity.position.x;
textEnt.position.y = entity.position.y;
textEnt.position.z = entity.position.z;
textEnt.textAlign = style.horizontalAlignment;
textEnt.color = color;
if (entity.rotation) {
textEnt.rotation.z = entity.rotation * Math.PI / 180;
}
if (entity.directionVector) {
var dv = entity.directionVector;
textEnt.rotation.z = new THREE.Vector3(1, 0, 0).angleTo(new THREE.Vector3(dv.x, dv.y, dv.z));
}
switch (entity.attachmentPoint) {
case 1:
// Top Left
textEnt.anchorX = 'left';
textEnt.anchorY = 'top';
break;
case 2:
// Top Center
textEnt.anchorX = 'center';
textEnt.anchorY = 'top';
break;
case 3:
// Top Right
textEnt.anchorX = 'right';
textEnt.anchorY = 'top';
break;
case 4:
// Middle Left
textEnt.anchorX = 'left';
textEnt.anchorY = 'middle';
break;
case 5:
// Middle Center
textEnt.anchorX = 'center';
textEnt.anchorY = 'middle';
break;
case 6:
// Middle Right
textEnt.anchorX = 'right';
textEnt.anchorY = 'middle';
break;
case 7:
// Bottom Left
textEnt.anchorX = 'left';
textEnt.anchorY = 'bottom';
break;
case 8:
// Bottom Center
textEnt.anchorX = 'center';
textEnt.anchorY = 'bottom';
break;
case 9:
// Bottom Right
textEnt.anchorX = 'right';
textEnt.anchorY = 'bottom';
break;
default:
return undefined;
};
textEnt.sync(() => {
if (textEnt.textAlign !== 'left') {
textEnt.geometry.computeBoundingBox();
var textWidth = textEnt.geometry.boundingBox.max.x - textEnt.geometry.boundingBox.min.x;
if (textEnt.textAlign === 'center') textEnt.position.x += (entity.width - textWidth) / 2;
if (textEnt.textAlign === 'right') textEnt.position.x += (entity.width - textWidth);
}
});
return textEnt;
}
function drawSpline(entity, data) {
var color = getColor(entity, data);
var points = getBSplinePolyline(entity.controlPoints, entity.degreeOfSplineCurve, entity.knotValues, 100);
var geometry = new THREE.BufferGeometry().setFromPoints(points);
var material = new THREE.LineBasicMaterial({ linewidth: 1, color: color });
var splineObject = new THREE.Line(geometry, material);
return splineObject;
}
/**
* Interpolate a b-spline. The algorithm examins the knot vector
* to create segments for interpolation. The parameterisation value
* is re-normalised back to [0,1] as that is what the lib expects (
* and t i de-normalised in the b-spline library)
*
* @param controlPoints the control points
* @param degree the b-spline degree
* @param knots the knot vector
* @returns the polyline
*/
function getBSplinePolyline(controlPoints, degree, knots, interpolationsPerSplineSegment, weights) {
const polyline = []
const controlPointsForLib = controlPoints.map(function (p) {
return [p.x, p.y]
})
const segmentTs = [knots[degree]]
const domain = [knots[degree], knots[knots.length - 1 - degree]]
for (let k = degree + 1; k < knots.length - degree; ++k) {
if (segmentTs[segmentTs.length - 1] !== knots[k]) {
segmentTs.push(knots[k])
}
}
interpolationsPerSplineSegment = interpolationsPerSplineSegment || 25
for (let i = 1; i < segmentTs.length; ++i) {
const uMin = segmentTs[i - 1]
const uMax = segmentTs[i]
for (let k = 0; k <= interpolationsPerSplineSegment; ++k) {
const u = k / interpolationsPerSplineSegment * (uMax - uMin) + uMin
// Clamp t to 0, 1 to handle numerical precision issues
let t = (u - domain[0]) / (domain[1] - domain[0])
t = Math.max(t, 0)
t = Math.min(t, 1)
const p = bSpline(t, degree, controlPointsForLib, knots, weights)
polyline.push(new THREE.Vector2(p[0], p[1]));
}
}
return polyline
}
function drawLine(entity, data) {
let points = [];
let color = getColor(entity, data);
var material, lineType, vertex, startPoint, endPoint, bulgeGeometry,
bulge, i, line;
if (!entity.vertices) return console.log('entity missing vertices.');
// create geometry
for (i = 0; i < entity.vertices.length; i++) {
if (entity.vertices[i].bulge) {
bulge = entity.vertices[i].bulge;
startPoint = entity.vertices[i];
endPoint = i + 1 < entity.vertices.length ? entity.vertices[i + 1] : points[0];
let bulgePoints = getBulgeCurvePoints(startPoint, endPoint, bulge);
points.push.apply(points, bulgePoints);
} else {
vertex = entity.vertices[i];
points.push(new THREE.Vector3(vertex.x, vertex.y, 0));
}
}
if (entity.shape) points.push(points[0]);
// set material
if (entity.lineType) {
lineType = data.tables.lineType.lineTypes[entity.lineType];
}
if (lineType && lineType.pattern && lineType.pattern.length !== 0) {
material = new THREE.LineDashedMaterial({ color: color, gapSize: 4, dashSize: 4 });
} else {
material = new THREE.LineBasicMaterial({ linewidth: 1, color: color });
}
var geometry = new BufferGeometry().setFromPoints(points);
line = new THREE.Line(geometry, material);
return line;
}
function drawArc(entity, data) {
var startAngle, endAngle;
if (entity.type === 'CIRCLE') {
startAngle = entity.startAngle || 0;
endAngle = startAngle + 2 * Math.PI;
} else {
startAngle = entity.startAngle;
endAngle = entity.endAngle;
}
var curve = new THREE.ArcCurve(
0, 0,
entity.radius,
startAngle,
endAngle);
var points = curve.getPoints(32);
var geometry = new THREE.BufferGeometry().setFromPoints(points);
var material = new THREE.LineBasicMaterial({ color: getColor(entity, data) });
var arc = new THREE.Line(geometry, material);
arc.position.x = entity.center.x;
arc.position.y = entity.center.y;
arc.position.z = entity.center.z;
return arc;
}
function addTriangleFacingCamera(verts, p0, p1, p2) {
// Calculate which direction the points are facing (clockwise or counter-clockwise)
var vector1 = new Vector3();
var vector2 = new Vector3();
vector1.subVectors(p1, p0);
vector2.subVectors(p2, p0);
vector1.cross(vector2);
var v0 = new Vector3(p0.x, p0.y, p0.z);
var v1 = new Vector3(p1.x, p1.y, p1.z);
var v2 = new Vector3(p2.x, p2.y, p2.z);
// If z < 0 then we must draw these in reverse order
if (vector1.z < 0) {
verts.push(v2, v1, v0);
} else {
verts.push(v0, v1, v2);
}
}
function drawSolid(entity, data) {
var material, verts,
geometry = new THREE.BufferGeometry();
var points = entity.points;
// verts = geometry.vertices;
verts = [];
addTriangleFacingCamera(verts, points[0], points[1], points[2]);
addTriangleFacingCamera(verts, points[1], points[2], points[3]);
material = new THREE.MeshBasicMaterial({ color: getColor(entity, data) });
geometry.setFromPoints(verts);
return new THREE.Mesh(geometry, material);
}
function drawText(entity, data) {
var geometry, material, text;
if (!font)
return console.warn('Text is not supported without a Three.js font loaded with THREE.FontLoader! Load a font of your choice and pass this into the constructor. See the sample for this repository or Three.js examples at http://threejs.org/examples/?q=text#webgl_geometry_text for more details.');
geometry = new THREE.TextGeometry(entity.text, { font: font, height: 0, size: entity.textHeight || 12 });
if (entity.rotation) {
var zRotation = entity.rotation * Math.PI / 180;
geometry.rotateZ(zRotation);
}
material = new THREE.MeshBasicMaterial({ color: getColor(entity, data) });
text = new THREE.Mesh(geometry, material);
text.position.x = entity.startPoint.x;
text.position.y = entity.startPoint.y;
text.position.z = entity.startPoint.z;
return text;
}
function drawPoint(entity, data) {
var geometry, material, point;
geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new Float32BufferAttribute([entity.position.x, entity.position.y, entity.position.z], 3));
var color = getColor(entity, data);
material = new THREE.PointsMaterial({ size: 0.1, color: new Color(color) });
point = new THREE.Points(geometry, material);
scene.add(point);
}
function drawDimension(entity, data) {
var block = data.blocks[entity.block];
if (!block || !block.entities) return null;
var group = new THREE.Object3D();
// if(entity.anchorPoint) {
// group.position.x = entity.anchorPoint.x;
// group.position.y = entity.anchorPoint.y;
// group.position.z = entity.anchorPoint.z;
// }
for (var i = 0; i < block.entities.length; i++) {
var childEntity = drawEntity(block.entities[i], data, group);
if (childEntity) group.add(childEntity);
}
return group;
}
function drawBlock(entity, data) {
var block = data.blocks[entity.name];
if (!block.entities) return null;
var group = new THREE.Object3D()
if (entity.xScale) group.scale.x = entity.xScale;
if (entity.yScale) group.scale.y = entity.yScale;
if (entity.rotation) {
group.rotation.z = entity.rotation * Math.PI / 180;
}
if (entity.position) {
group.position.x = entity.position.x;
group.position.y = entity.position.y;
group.position.z = entity.position.z;
}
for (var i = 0; i < block.entities.length; i++) {
var childEntity = drawEntity(block.entities[i], data, group);
if (childEntity) group.add(childEntity);
}
return group;
}
function getColor(entity, data) {
var color = 0x000000; //default
if (entity.color) color = entity.color;
else if (data.tables && data.tables.layer && data.tables.layer.layers[entity.layer])
color = data.tables.layer.layers[entity.layer].color;
if (color == null || color === 0xffffff) {
color = 0x000000;
}
return color;
}
function createLineTypeShaders(data) {
var ltype, type;
if (!data.tables || !data.tables.lineType) return;
var ltypes = data.tables.lineType.lineTypes;
for (type in ltypes) {
ltype = ltypes[type];
if (!ltype.pattern) continue;
ltype.material = createDashedLineShader(ltype.pattern);
}
}
function createDashedLineShader(pattern) {
var i,
dashedLineShader = {},
totalLength = 0.0;
for (i = 0; i < pattern.length; i++) {
totalLength += Math.abs(pattern[i]);
}
dashedLineShader.uniforms = THREE.UniformsUtils.merge([
THREE.UniformsLib['common'],
THREE.UniformsLib['fog'],
{
'pattern': { type: 'fv1', value: pattern },
'patternLength': { type: 'f', value: totalLength }
}
]);
dashedLineShader.vertexShader = [
'attribute float lineDistance;',
'varying float vLineDistance;',
THREE.ShaderChunk['color_pars_vertex'],
'void main() {',
THREE.ShaderChunk['color_vertex'],
'vLineDistance = lineDistance;',
'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
'}'
].join('\n');
dashedLineShader.fragmentShader = [
'uniform vec3 diffuse;',
'uniform float opacity;',
'uniform float pattern[' + pattern.length + '];',
'uniform float patternLength;',
'varying float vLineDistance;',
THREE.ShaderChunk['color_pars_fragment'],
THREE.ShaderChunk['fog_pars_fragment'],
'void main() {',
'float pos = mod(vLineDistance, patternLength);',
'for ( int i = 0; i < ' + pattern.length + '; i++ ) {',
'pos = pos - abs(pattern[i]);',
'if( pos < 0.0 ) {',
'if( pattern[i] > 0.0 ) {',
'gl_FragColor = vec4(1.0, 0.0, 0.0, opacity );',
'break;',
'}',
'discard;',
'}',
'}',
THREE.ShaderChunk['color_fragment'],
THREE.ShaderChunk['fog_fragment'],
'}'
].join('\n');
return dashedLineShader;
}
function findExtents(scene) {
for (var child of scene.children) {
var minX, maxX, minY, maxY;
if (child.position) {
minX = Math.min(child.position.x, minX);
minY = Math.min(child.position.y, minY);
maxX = Math.max(child.position.x, maxX);
maxY = Math.max(child.position.y, maxY);
}
}
return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY } };
}
}
// Show/Hide helpers from https://plainjs.com/javascript/effects/hide-or-show-an-element-42/
// get the default display style of an element
function defaultDisplay(tag) {
var iframe = document.createElement('iframe');
iframe.setAttribute('frameborder', 0);
iframe.setAttribute('width', 0);
iframe.setAttribute('height', 0);
document.documentElement.appendChild(iframe);
var doc = (iframe.contentWindow || iframe.contentDocument).document;
// IE support
doc.write();
doc.close();
var testEl = doc.createElement(tag);
doc.documentElement.appendChild(testEl);
var display = (window.getComputedStyle ? getComputedStyle(testEl, null) : testEl.currentStyle).display
iframe.parentNode.removeChild(iframe);
return display;
}
// actual show/hide function used by show() and hide() below
function showHide(el, show) {
var value = el.getAttribute('data-olddisplay'),
display = el.style.display,
computedDisplay = (window.getComputedStyle ? getComputedStyle(el, null) : el.currentStyle).display;
if (show) {
if (!value && display === 'none') el.style.display = '';
if (el.style.display === '' && (computedDisplay === 'none')) value = value || defaultDisplay(el.nodeName);
} else {
if (display && display !== 'none' || !(computedDisplay == 'none'))
el.setAttribute('data-olddisplay', (computedDisplay == 'none') ? display : computedDisplay);
}
if (!show || el.style.display === 'none' || el.style.display === '')
el.style.display = show ? value || '' : 'none';
}
// helper functions
function show(el) { showHide(el, true); }
function hide(el) { showHide(el); }