dxf: split into layer-key and block-partition workflows
This commit is contained in:
300
dxf/block_partition_report.mjs
Normal file
300
dxf/block_partition_report.mjs
Normal file
@@ -0,0 +1,300 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {
|
||||
boundsFromPoints,
|
||||
computeEntityListBounds,
|
||||
first,
|
||||
num,
|
||||
parseDxfText,
|
||||
readDxfText,
|
||||
transformPoint,
|
||||
} from "./lib/dxf_basic_parser.mjs";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = {};
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const token = argv[i];
|
||||
if (!token.startsWith("--")) continue;
|
||||
const key = token.slice(2);
|
||||
const value = argv[i + 1];
|
||||
if (!value || value.startsWith("--")) continue;
|
||||
out[key] = value;
|
||||
i += 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function toFixedNumber(value, fraction = 3) {
|
||||
return Number(Number(value).toFixed(fraction));
|
||||
}
|
||||
|
||||
function computeZoneKey(centerX, centerY, bounds, split) {
|
||||
const width = Math.max(1, bounds.maxX - bounds.minX);
|
||||
const height = Math.max(1, bounds.maxY - bounds.minY);
|
||||
const col = Math.min(split - 1, Math.max(0, Math.floor(((centerX - bounds.minX) / width) * split)));
|
||||
const row = Math.min(split - 1, Math.max(0, Math.floor(((centerY - bounds.minY) / height) * split)));
|
||||
return `Z-R${row + 1}C${col + 1}`;
|
||||
}
|
||||
|
||||
function buildBlockPartitionData(dxfPath, split) {
|
||||
const { blocks, entities, headerBounds } = parseDxfText(readDxfText(dxfPath));
|
||||
const drawingWidth = Math.max(1, headerBounds.maxX - headerBounds.minX);
|
||||
const drawingHeight = Math.max(1, headerBounds.maxY - headerBounds.minY);
|
||||
const drawingArea = drawingWidth * drawingHeight;
|
||||
|
||||
const blockDefs = new Map();
|
||||
for (const block of blocks) {
|
||||
const name = first(block, 2) ?? "*anonymous*";
|
||||
const basePoint = { x: num(block, 10), y: num(block, 20) };
|
||||
const localBounds = computeEntityListBounds(block.entities);
|
||||
blockDefs.set(name, {
|
||||
name,
|
||||
basePoint,
|
||||
localBounds,
|
||||
entityCount: block.entities.length,
|
||||
});
|
||||
}
|
||||
|
||||
const partitions = [];
|
||||
for (const entity of entities) {
|
||||
if (entity.type !== "INSERT") continue;
|
||||
const blockName = first(entity, 2) ?? "";
|
||||
const blockLayer = first(entity, 8) ?? "0";
|
||||
const blockDef = blockDefs.get(blockName);
|
||||
if (!blockDef || !blockDef.localBounds) continue;
|
||||
|
||||
const insert = {
|
||||
x: num(entity, 10),
|
||||
y: num(entity, 20),
|
||||
rotation: num(entity, 50),
|
||||
xScale: num(entity, 41, 1),
|
||||
yScale: num(entity, 42, 1),
|
||||
};
|
||||
|
||||
const b = blockDef.localBounds;
|
||||
const corners = [
|
||||
{ x: b.minX, y: b.minY },
|
||||
{ x: b.maxX, y: b.minY },
|
||||
{ x: b.maxX, y: b.maxY },
|
||||
{ x: b.minX, y: b.maxY },
|
||||
].map((point) => transformPoint(point, insert, blockDef.basePoint));
|
||||
|
||||
const worldBounds = boundsFromPoints(corners);
|
||||
if (!worldBounds) continue;
|
||||
const area = Math.max(1, worldBounds.width * worldBounds.height);
|
||||
const oversized =
|
||||
area > drawingArea * 0.6 ||
|
||||
worldBounds.width > drawingWidth * 0.9 ||
|
||||
worldBounds.height > drawingHeight * 0.9;
|
||||
if (oversized) continue;
|
||||
|
||||
const zoneKey = computeZoneKey(worldBounds.cx, worldBounds.cy, headerBounds, split);
|
||||
partitions.push({
|
||||
id: `P-${String(partitions.length + 1).padStart(4, "0")}`,
|
||||
zoneKey,
|
||||
blockName,
|
||||
layer: blockLayer,
|
||||
handle: first(entity, 5) ?? "",
|
||||
rotation: toFixedNumber(insert.rotation, 2),
|
||||
scale: {
|
||||
x: toFixedNumber(insert.xScale, 4),
|
||||
y: toFixedNumber(insert.yScale, 4),
|
||||
},
|
||||
center: {
|
||||
x: toFixedNumber(worldBounds.cx),
|
||||
y: toFixedNumber(worldBounds.cy),
|
||||
},
|
||||
bounds: {
|
||||
minX: toFixedNumber(worldBounds.minX),
|
||||
minY: toFixedNumber(worldBounds.minY),
|
||||
maxX: toFixedNumber(worldBounds.maxX),
|
||||
maxY: toFixedNumber(worldBounds.maxY),
|
||||
width: toFixedNumber(worldBounds.width),
|
||||
height: toFixedNumber(worldBounds.height),
|
||||
},
|
||||
blockEntityCount: blockDef.entityCount,
|
||||
});
|
||||
}
|
||||
|
||||
partitions.sort((a, b) => {
|
||||
const dy = b.center.y - a.center.y;
|
||||
if (Math.abs(dy) > 0.0001) return dy;
|
||||
return a.center.x - b.center.x;
|
||||
});
|
||||
|
||||
const byBlock = {};
|
||||
const byZone = {};
|
||||
for (const item of partitions) {
|
||||
byBlock[item.blockName] = (byBlock[item.blockName] ?? 0) + 1;
|
||||
byZone[item.zoneKey] = (byZone[item.zoneKey] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
meta: {
|
||||
source: path.basename(dxfPath),
|
||||
generatedAt: new Date().toISOString(),
|
||||
split,
|
||||
partitionCount: partitions.length,
|
||||
blockDefinitionCount: blockDefs.size,
|
||||
headerBounds,
|
||||
},
|
||||
byBlock,
|
||||
byZone,
|
||||
partitions,
|
||||
};
|
||||
}
|
||||
|
||||
function buildHtml(payload) {
|
||||
return `<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>DXF block partition report</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f2f4f8;
|
||||
--card: #ffffff;
|
||||
--line: #d8dee8;
|
||||
--ink: #152230;
|
||||
--muted: #6b7a8e;
|
||||
--accent: #1861bf;
|
||||
--accent-soft: rgba(24, 97, 191, 0.12);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: "Segoe UI", "Pretendard", sans-serif; color: var(--ink); background: linear-gradient(180deg, #f8fafc 0%, var(--bg) 100%); }
|
||||
.layout { display: grid; grid-template-columns: 1.8fr 1fr; gap: 14px; padding: 14px; min-height: 100vh; }
|
||||
.panel { background: var(--card); border: 1px solid var(--line); border-radius: 14px; overflow: hidden; box-shadow: 0 10px 24px rgba(28, 39, 58, 0.06); }
|
||||
.header { padding: 12px 14px; border-bottom: 1px solid var(--line); display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
|
||||
.header strong { font-size: 14px; }
|
||||
.header span { font-size: 12px; color: var(--muted); }
|
||||
.canvas-wrap { padding: 12px; }
|
||||
svg { width: 100%; height: 78vh; border: 1px solid var(--line); border-radius: 10px; background: #f9fbfd; }
|
||||
.list { max-height: 84vh; overflow: auto; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid #ecf0f6; white-space: nowrap; }
|
||||
th { position: sticky; top: 0; background: #f8fafc; z-index: 1; font-weight: 600; }
|
||||
tr:hover td { background: var(--accent-soft); }
|
||||
.tag { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; border: 1px solid #cadaf4; color: #124c95; background: #edf4ff; }
|
||||
@media (max-width: 980px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
svg { height: 54vh; }
|
||||
.list { max-height: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
<section class="panel">
|
||||
<div class="header">
|
||||
<strong>Block partition map</strong>
|
||||
<span id="meta"></span>
|
||||
</div>
|
||||
<div class="canvas-wrap">
|
||||
<svg id="map"></svg>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<div class="header">
|
||||
<strong>Partition list</strong>
|
||||
<span>${payload.partitions.length} items</span>
|
||||
</div>
|
||||
<div class="list">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Zone</th>
|
||||
<th>Block</th>
|
||||
<th>Layer</th>
|
||||
<th>Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<script>
|
||||
const DATA = ${JSON.stringify(payload)};
|
||||
const map = document.getElementById("map");
|
||||
const rows = document.getElementById("rows");
|
||||
const meta = document.getElementById("meta");
|
||||
|
||||
const W = 1600;
|
||||
const H = 980;
|
||||
map.setAttribute("viewBox", \`0 0 \${W} \${H}\`);
|
||||
meta.textContent = \`\${DATA.meta.source} | partitions: \${DATA.meta.partitionCount}\`;
|
||||
|
||||
const bounds = DATA.meta.headerBounds;
|
||||
const worldW = Math.max(1, bounds.maxX - bounds.minX);
|
||||
const worldH = Math.max(1, bounds.maxY - bounds.minY);
|
||||
const pad = 30;
|
||||
const drawW = W - pad * 2;
|
||||
const drawH = H - pad * 2;
|
||||
|
||||
function tx(x) { return pad + ((x - bounds.minX) / worldW) * drawW; }
|
||||
function ty(y) { return pad + ((bounds.maxY - y) / worldH) * drawH; }
|
||||
|
||||
const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
bg.setAttribute("x", pad);
|
||||
bg.setAttribute("y", pad);
|
||||
bg.setAttribute("width", drawW);
|
||||
bg.setAttribute("height", drawH);
|
||||
bg.setAttribute("fill", "#ffffff");
|
||||
bg.setAttribute("stroke", "#dde5ef");
|
||||
map.appendChild(bg);
|
||||
|
||||
DATA.partitions.forEach((item, index) => {
|
||||
const x = tx(item.bounds.minX);
|
||||
const y = ty(item.bounds.maxY);
|
||||
const width = Math.max(2, tx(item.bounds.maxX) - tx(item.bounds.minX));
|
||||
const height = Math.max(2, ty(item.bounds.minY) - ty(item.bounds.maxY));
|
||||
|
||||
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
rect.setAttribute("x", x);
|
||||
rect.setAttribute("y", y);
|
||||
rect.setAttribute("width", width);
|
||||
rect.setAttribute("height", height);
|
||||
rect.setAttribute("fill", "rgba(24,97,191,0.12)");
|
||||
rect.setAttribute("stroke", "rgba(24,97,191,0.8)");
|
||||
rect.setAttribute("stroke-width", "1.1");
|
||||
map.appendChild(rect);
|
||||
|
||||
if (index % 4 === 0) {
|
||||
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||
label.setAttribute("x", x + 3);
|
||||
label.setAttribute("y", Math.max(16, y + 12));
|
||||
label.setAttribute("fill", "#0e3d7b");
|
||||
label.setAttribute("font-size", "11");
|
||||
label.textContent = item.id;
|
||||
map.appendChild(label);
|
||||
}
|
||||
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = \`
|
||||
<td>\${item.id}</td>
|
||||
<td><span class="tag">\${item.zoneKey}</span></td>
|
||||
<td>\${item.blockName}</td>
|
||||
<td>\${item.layer}</td>
|
||||
<td>\${item.bounds.width} x \${item.bounds.height}</td>
|
||||
\`;
|
||||
rows.appendChild(tr);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
||||
const dxfPath = path.resolve(args.input ?? path.join(scriptDir, "center.dxf"));
|
||||
const outputJsonPath = path.resolve(args.json ?? path.join(scriptDir, "block_partition_report.json"));
|
||||
const outputHtmlPath = path.resolve(args.html ?? path.join(scriptDir, "block_partition_report.html"));
|
||||
const split = Math.max(2, Number(args.split ?? 4) || 4);
|
||||
|
||||
const report = buildBlockPartitionData(dxfPath, split);
|
||||
fs.writeFileSync(outputJsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
fs.writeFileSync(outputHtmlPath, buildHtml(report), "utf8");
|
||||
console.log(`saved: ${outputJsonPath}`);
|
||||
console.log(`saved: ${outputHtmlPath}`);
|
||||
Reference in New Issue
Block a user