ac6f1d84af
Extend CAD conversion to emit bounded semantic metadata (units, layers, blocks, entity types, labels, crop-indexed bounds) alongside the derived SVG, tag imported drawings as cad-vector nodes, and carry semanticContext through canvas nodes, snapshot normalization, and reference images. Agent Chat forwards this metadata as hidden cad-context reference parts. Add non-destructive vector cropping: cropping a vector/CAD node keeps the source SVG and spawns a transparent WebP sibling with crop-scoped semantic context, wired through the vectorized image toolbar and the workspace crop tool. Include node --test coverage for CAD semantics and vector crop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
225 lines
5.5 KiB
JavaScript
225 lines
5.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { BlockRecord, CadDocument, Insert, Line, Point, TextEntity, XYZ } from "@node-projects/acad-ts";
|
|
import { convertCadBufferToSvg, convertCadDocumentToSvg } from "../src/ui/lib/cadConversion.ts";
|
|
|
|
const r12PointDrawing = `0
|
|
SECTION
|
|
2
|
|
HEADER
|
|
9
|
|
$ACADVER
|
|
1
|
|
AC1009
|
|
0
|
|
ENDSEC
|
|
0
|
|
SECTION
|
|
2
|
|
TABLES
|
|
0
|
|
TABLE
|
|
2
|
|
LTYPE
|
|
70
|
|
1
|
|
0
|
|
LTYPE
|
|
2
|
|
CONTINUOUS
|
|
70
|
|
0
|
|
3
|
|
Solid line
|
|
72
|
|
65
|
|
73
|
|
0
|
|
40
|
|
0.0
|
|
0
|
|
ENDTAB
|
|
0
|
|
TABLE
|
|
2
|
|
LAYER
|
|
70
|
|
1
|
|
0
|
|
LAYER
|
|
2
|
|
0
|
|
70
|
|
0
|
|
62
|
|
7
|
|
6
|
|
CONTINUOUS
|
|
0
|
|
ENDTAB
|
|
0
|
|
ENDSEC
|
|
0
|
|
SECTION
|
|
2
|
|
BLOCKS
|
|
0
|
|
ENDSEC
|
|
0
|
|
SECTION
|
|
2
|
|
ENTITIES
|
|
0
|
|
POINT
|
|
8
|
|
0
|
|
10
|
|
12.5
|
|
20
|
|
8.25
|
|
30
|
|
0.0
|
|
0
|
|
TEXT
|
|
8
|
|
0
|
|
10
|
|
18.0
|
|
20
|
|
14.0
|
|
30
|
|
0.0
|
|
40
|
|
2.5
|
|
1
|
|
R12 label
|
|
0
|
|
ENDSEC
|
|
0
|
|
EOF
|
|
`;
|
|
|
|
test("converts R12 DXF entities whose colors are inherited from their layer", () => {
|
|
Object.defineProperty(Point, "name", { configurable: true, value: "a" });
|
|
const input = new TextEncoder().encode(r12PointDrawing);
|
|
const buffer = input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
|
|
|
|
const svg = convertCadBufferToSvg(buffer, "dxf", "R12 sample.dxf");
|
|
|
|
assert.equal(Point.name, "Point");
|
|
assert.match(svg, /<circle\b/);
|
|
assert.match(svg, /<text\b/);
|
|
assert.match(svg, /fill="rgb\(255,255,255\)"/);
|
|
const metadataSource = svg.match(/<metadata id="cad-semantic-context">([\s\S]*?)<\/metadata>/)?.[1] || "";
|
|
const metadata = JSON.parse(metadataSource.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&"));
|
|
assert.equal(metadata.sourceName, "R12 sample.dxf");
|
|
assert.equal(metadata.format, "DXF");
|
|
assert.equal(metadata.modelEntityCount, 2);
|
|
assert.ok(metadata.entityTypes.some((item) => item.name.includes("TEXT")));
|
|
assert.ok(metadata.texts.some((item) => item.text === "R12 label"));
|
|
});
|
|
|
|
test("emits repeated block inserts as reusable SVG references with full transforms", () => {
|
|
const document = new CadDocument();
|
|
const block = new BlockRecord("REPEATED_PART");
|
|
block.blockEntity.basePoint = new XYZ(2, 3, 0);
|
|
block.entities.add(new Line(new XYZ(2, 3, 0), new XYZ(12, 3, 0)));
|
|
document.blockRecords.add(block);
|
|
|
|
for (let index = 0; index < 200; index += 1) {
|
|
const insert = new Insert();
|
|
insert.block = block;
|
|
insert.insertPoint = new XYZ(index * 20, 10, 0);
|
|
insert.xScale = 2;
|
|
insert.yScale = 3;
|
|
insert.rotation = Math.PI / 2;
|
|
document.modelSpace.entities.add(insert);
|
|
}
|
|
|
|
const svg = convertCadDocumentToSvg(document);
|
|
|
|
assert.equal((svg.match(/<line\b/g) ?? []).length, 1);
|
|
assert.equal((svg.match(/<use\b/g) ?? []).length, 200);
|
|
assert.equal((svg.match(/id="cad-block-/g) ?? []).length, 1);
|
|
assert.match(svg, /transform="matrix\(0 2 -3 0 9 6\)"/);
|
|
assert.ok(svg.length < 50_000, `expected reusable output, received ${svg.length} bytes`);
|
|
});
|
|
|
|
test("expands MINSERT rows and columns without duplicating block geometry", () => {
|
|
const document = new CadDocument();
|
|
const block = new BlockRecord("ARRAY_PART");
|
|
block.entities.add(new Line(new XYZ(0, 0, 0), new XYZ(1, 0, 0)));
|
|
document.blockRecords.add(block);
|
|
const insert = new Insert();
|
|
insert.block = block;
|
|
insert.columnCount = 3;
|
|
insert.columnSpacing = 10;
|
|
insert.rowCount = 2;
|
|
insert.rowSpacing = 5;
|
|
document.modelSpace.entities.add(insert);
|
|
|
|
const svg = convertCadDocumentToSvg(document);
|
|
|
|
assert.equal((svg.match(/<line\b/g) ?? []).length, 1);
|
|
assert.equal((svg.match(/<use\b/g) ?? []).length, 6);
|
|
assert.match(svg, /matrix\(1 0 0 1 20 5\)/);
|
|
});
|
|
|
|
test("cuts cyclic nested block references instead of recursively expanding them", () => {
|
|
const document = new CadDocument();
|
|
const first = new BlockRecord("CYCLE_A");
|
|
const second = new BlockRecord("CYCLE_B");
|
|
document.blockRecords.add(first);
|
|
document.blockRecords.add(second);
|
|
first.entities.add(new Line(new XYZ(0, 0, 0), new XYZ(10, 0, 0)));
|
|
const firstToSecond = new Insert();
|
|
firstToSecond.block = second;
|
|
first.entities.add(firstToSecond);
|
|
const secondToFirst = new Insert();
|
|
secondToFirst.block = first;
|
|
second.entities.add(secondToFirst);
|
|
const rootInsert = new Insert();
|
|
rootInsert.block = first;
|
|
document.modelSpace.entities.add(rootInsert);
|
|
|
|
const svg = convertCadDocumentToSvg(document);
|
|
|
|
assert.equal((svg.match(/id="cad-block-/g) ?? []).length, 2);
|
|
assert.equal((svg.match(/<use\b/g) ?? []).length, 2);
|
|
assert.ok(svg.length < 10_000);
|
|
});
|
|
|
|
test("excludes invisible CAD entities from output and drawing bounds", () => {
|
|
const document = new CadDocument();
|
|
document.modelSpace.entities.add(new Line(new XYZ(0, 0, 0), new XYZ(100, 50, 0)));
|
|
const invisibleOutlier = new Line(new XYZ(500_000, 500_000, 0), new XYZ(501_000, 501_000, 0));
|
|
invisibleOutlier.isInvisible = true;
|
|
document.modelSpace.entities.add(invisibleOutlier);
|
|
|
|
const svg = convertCadDocumentToSvg(document);
|
|
|
|
assert.equal((svg.match(/<line\b/g) ?? []).length, 1);
|
|
assert.match(svg, /viewBox="0 0 100 50"/);
|
|
});
|
|
|
|
test("repairs common GBK text decoded as Latin-1 before SVG and semantic output", () => {
|
|
const document = new CadDocument();
|
|
document.modelSpace.entities.add(new Line(new XYZ(0, 0, 0), new XYZ(100, 50, 0)));
|
|
const text = new TextEntity();
|
|
text.value = "¿î·âÑùÖ½°å";
|
|
text.insertPoint = new XYZ(0, 0, 0);
|
|
text.height = 12;
|
|
document.modelSpace.entities.add(text);
|
|
const shortText = new TextEntity();
|
|
shortText.value = "Ç¿";
|
|
shortText.insertPoint = new XYZ(20, 20, 0);
|
|
shortText.height = 12;
|
|
document.modelSpace.entities.add(shortText);
|
|
|
|
const svg = convertCadDocumentToSvg(document, { format: "dxf", sourceName: "Chinese labels.dxf" });
|
|
|
|
assert.match(svg, /款封样纸板/);
|
|
assert.match(svg, /强/);
|
|
});
|