feat(canvas): add CAD vector semantic context and non-destructive vector crop

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>
This commit is contained in:
2026-07-16 16:42:30 +08:00
parent 29b3bd222c
commit ac6f1d84af
23 changed files with 1061 additions and 88 deletions
+29 -2
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { BlockRecord, CadDocument, Insert, Line, Point, XYZ } from "@node-projects/acad-ts";
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
@@ -105,12 +105,19 @@ test("converts R12 DXF entities whose colors are inherited from their layer", ()
const input = new TextEncoder().encode(r12PointDrawing);
const buffer = input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
const svg = convertCadBufferToSvg(buffer, "dxf");
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(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/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", () => {
@@ -195,3 +202,23 @@ test("excludes invisible CAD entities from output and drawing bounds", () => {
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, /强/);
});