|
| 1 | +/** |
| 2 | + * Validates multilayer DXF export naming/content against the flow+control demo JSON. |
| 3 | + * Run: node scripts/validate-multilayer-dxf-export.js |
| 4 | + */ |
| 5 | + |
| 6 | +const fs = require("fs"); |
| 7 | +const path = require("path"); |
| 8 | + |
| 9 | +const DEMO = path.join( |
| 10 | + __dirname, |
| 11 | + "../../Neptune_2026/Microfluidics-Benchmarks/Results/Quick_Examples/flow_and_control_demo/flow_and_control_demo_fromLFR_PR.json" |
| 12 | +); |
| 13 | + |
| 14 | +const UM_TO_MM = 0.001; |
| 15 | + |
| 16 | +function dxfPair(code, value) { |
| 17 | + return `${code}\n${value}\n`; |
| 18 | +} |
| 19 | + |
| 20 | +function writeHeader() { |
| 21 | + let out = "0\nSECTION\n2\nHEADER\n"; |
| 22 | + out += dxfPair(9, "$ACADVER"); |
| 23 | + out += dxfPair(1, "AC1015"); |
| 24 | + out += dxfPair(9, "$INSUNITS"); |
| 25 | + out += dxfPair(70, 4); |
| 26 | + out += "0\nENDSEC\n"; |
| 27 | + return out; |
| 28 | +} |
| 29 | + |
| 30 | +function wrapEntities(entities) { |
| 31 | + return writeHeader() + "0\nSECTION\n2\nENTITIES\n" + entities + "0\nENDSEC\n0\nEOF\n"; |
| 32 | +} |
| 33 | + |
| 34 | +function writeLine(a, b, layer, z = 0) { |
| 35 | + let out = "0\nLINE\n"; |
| 36 | + out += dxfPair(8, layer); |
| 37 | + out += dxfPair(10, a.x); |
| 38 | + out += dxfPair(20, a.y); |
| 39 | + out += dxfPair(30, z); |
| 40 | + out += dxfPair(11, b.x); |
| 41 | + out += dxfPair(21, b.y); |
| 42 | + out += dxfPair(31, z); |
| 43 | + return out; |
| 44 | +} |
| 45 | + |
| 46 | +function writeCircle(center, radius, layer) { |
| 47 | + let out = "0\nCIRCLE\n"; |
| 48 | + out += dxfPair(8, layer); |
| 49 | + out += dxfPair(10, center.x); |
| 50 | + out += dxfPair(20, center.y); |
| 51 | + out += dxfPair(30, 0); |
| 52 | + out += dxfPair(40, radius); |
| 53 | + return out; |
| 54 | +} |
| 55 | + |
| 56 | +function canvasUmToDxfMm(xUm, yUm, deviceHeightUm) { |
| 57 | + return { x: xUm * UM_TO_MM, y: (deviceHeightUm - yUm) * UM_TO_MM }; |
| 58 | +} |
| 59 | + |
| 60 | +function isMultilayerBiochip(layers) { |
| 61 | + let count = 0; |
| 62 | + for (const layer of layers) { |
| 63 | + if (layer.type === "FLOW" || layer.type === "CONTROL") { |
| 64 | + count += 1; |
| 65 | + if (count > 1) return true; |
| 66 | + } |
| 67 | + } |
| 68 | + return false; |
| 69 | +} |
| 70 | + |
| 71 | +function buildMockDevice(json) { |
| 72 | + const heightUm = json.params["y-span"]; |
| 73 | + const layers = json.layers.map(l => ({ |
| 74 | + id: l.id, |
| 75 | + name: l.name, |
| 76 | + type: l.type, |
| 77 | + features: [] |
| 78 | + })); |
| 79 | + const byId = Object.fromEntries(layers.map(l => [l.id, l])); |
| 80 | + |
| 81 | + for (const comp of json.components || []) { |
| 82 | + const layer = byId[comp.layers[0]]; |
| 83 | + if (!layer) continue; |
| 84 | + const entity = (comp.entity || "").toUpperCase(); |
| 85 | + if (entity === "PORT") { |
| 86 | + layer.features.push({ |
| 87 | + type: "Port", |
| 88 | + position: comp.params.position, |
| 89 | + portRadius: comp.params.portRadius |
| 90 | + }); |
| 91 | + } else if (entity === "VALVE3D" || entity === "VALVE" || entity === "CIRCLE VALVE") { |
| 92 | + layer.features.push({ |
| 93 | + type: entity === "VALVE3D" ? "Valve3D_control" : "CircleValve", |
| 94 | + position: comp.params.position, |
| 95 | + valveRadius: comp.params.valveRadius || comp.params.portRadius || 400 |
| 96 | + }); |
| 97 | + } |
| 98 | + // Mixer etc. are complex; DXF export currently focuses on ports/connections/valves. |
| 99 | + } |
| 100 | + |
| 101 | + for (const conn of json.connections || []) { |
| 102 | + const layer = byId[conn.layer]; |
| 103 | + if (!layer) continue; |
| 104 | + layer.features.push({ |
| 105 | + type: "Connection", |
| 106 | + segments: conn.params.segments, |
| 107 | + height: conn.params.height || 250 |
| 108 | + }); |
| 109 | + } |
| 110 | + |
| 111 | + return { name: json.name, heightUm, layers }; |
| 112 | +} |
| 113 | + |
| 114 | +function exportLayer(device, layer) { |
| 115 | + let entities = ""; |
| 116 | + const layerName = layer.name || layer.type; |
| 117 | + for (const feature of layer.features) { |
| 118 | + if (feature.type === "Port" || feature.type === "Valve3D_control" || feature.type === "CircleValve") { |
| 119 | + const radius = feature.portRadius || feature.valveRadius; |
| 120 | + if (!feature.position || !radius) continue; |
| 121 | + const pt = canvasUmToDxfMm(feature.position[0], feature.position[1], device.heightUm); |
| 122 | + const suffix = feature.type === "Port" ? "_ports" : "_valves"; |
| 123 | + entities += writeCircle(pt, radius * UM_TO_MM, layerName + suffix); |
| 124 | + } else if (feature.type === "Connection") { |
| 125 | + for (const seg of feature.segments || []) { |
| 126 | + const a = canvasUmToDxfMm(seg[0][0], seg[0][1], device.heightUm); |
| 127 | + const b = canvasUmToDxfMm(seg[1][0], seg[1][1], device.heightUm); |
| 128 | + entities += writeLine(a, b, layerName + "_channels", (feature.height || 250) * UM_TO_MM); |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | + return wrapEntities(entities); |
| 133 | +} |
| 134 | + |
| 135 | +function generateFiles(device) { |
| 136 | + const exportLayers = device.layers.filter(l => l.type === "FLOW" || l.type === "CONTROL"); |
| 137 | + if (exportLayers.length <= 1) { |
| 138 | + return [{ filename: `${device.name}.dxf`, content: exportLayer(device, exportLayers[0]) }]; |
| 139 | + } |
| 140 | + const files = []; |
| 141 | + let flowIndex = 0; |
| 142 | + let controlIndex = 0; |
| 143 | + for (const layer of exportLayers) { |
| 144 | + if (layer.type === "FLOW") flowIndex += 1; |
| 145 | + else controlIndex += 1; |
| 146 | + const suffix = layer.type === "CONTROL" ? `_ctrl${controlIndex}` : `_flow${flowIndex}`; |
| 147 | + files.push({ |
| 148 | + filename: `${device.name}${suffix}.dxf`, |
| 149 | + content: exportLayer(device, layer) |
| 150 | + }); |
| 151 | + } |
| 152 | + return files; |
| 153 | +} |
| 154 | + |
| 155 | +function countEntities(dxfText, type) { |
| 156 | + const re = new RegExp(`^0\\n${type}\\n`, "gm"); |
| 157 | + return (dxfText.match(re) || []).length; |
| 158 | +} |
| 159 | + |
| 160 | +function main() { |
| 161 | + const json = JSON.parse(fs.readFileSync(DEMO, "utf8")); |
| 162 | + const device = buildMockDevice(json); |
| 163 | + const multilayer = isMultilayerBiochip(device.layers); |
| 164 | + const files = generateFiles(device); |
| 165 | + |
| 166 | + console.log("Device:", device.name); |
| 167 | + console.log("Layers:", device.layers.map(l => `${l.name}(${l.type}) features=${l.features.length}`).join(", ")); |
| 168 | + console.log("isMultilayerBiochip:", multilayer); |
| 169 | + console.log("Expected GCode blocked:", multilayer === true); |
| 170 | + console.log("DXF files:"); |
| 171 | + for (const f of files) { |
| 172 | + const lines = countEntities(f.content, "LINE"); |
| 173 | + const circles = countEntities(f.content, "CIRCLE"); |
| 174 | + console.log(` ${f.filename}: LINE=${lines}, CIRCLE=${circles}, bytes=${f.content.length}`); |
| 175 | + } |
| 176 | + |
| 177 | + const names = files.map(f => f.filename); |
| 178 | + const expect = [`${device.name}_flow1.dxf`, `${device.name}_ctrl1.dxf`]; |
| 179 | + const okNames = expect.every(n => names.includes(n)) && names.length === 2; |
| 180 | + const flow = files.find(f => f.filename.endsWith("_flow1.dxf")); |
| 181 | + const ctrl = files.find(f => f.filename.endsWith("_ctrl1.dxf")); |
| 182 | + const flowOk = flow && countEntities(flow.content, "LINE") > 0 && countEntities(flow.content, "CIRCLE") > 0; |
| 183 | + const ctrlOk = ctrl && countEntities(ctrl.content, "LINE") > 0 && countEntities(ctrl.content, "CIRCLE") > 0; |
| 184 | + |
| 185 | + if (!okNames || !flowOk || !ctrlOk || !multilayer) { |
| 186 | + console.error("FAIL"); |
| 187 | + process.exit(1); |
| 188 | + } |
| 189 | + console.log("PASS: multilayer DXF naming and non-empty flow/control geometry"); |
| 190 | +} |
| 191 | + |
| 192 | +main(); |
0 commit comments