Files
cadSet/cad-agent-studio/src/viewer-cadjs/lib/gcode/parseGcode.test.js
T
2026-07-27 17:24:04 +08:00

202 lines
6.5 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {
buildGcodePreviewMeshData,
GCODE_PREVIEW_DETAIL_MAX
} from "./buildPreviewMesh.js";
import { parseGcode } from "./parseGcode.js";
test("parseGcode extracts extrusion, travel, layers, and basic print stats", () => {
const data = parseGcode(`
; perimeter preview
G21
G90
M82
G92 E0
G0 X0 Y0 Z0.2 F6000
G1 X10 Y0 E0.4 F1200
G1 X10 Y10 E0.8
G0 X0 Y10 F6000
G92 E0
G1 X0 Y0 E0.4
G1 Z0.4 F600
G1 X5 Y0 E0.8
M104 S210
`, { sourceUrl: "/models/test.gcode", fileRef: "test.gcode" });
assert.equal(data.schemaVersion, 1);
assert.equal(data.sourceUrl, "/models/test.gcode");
assert.equal(data.fileRef, "test.gcode");
assert.equal(data.units, "mm");
assert.deepEqual(data.bounds, {
min: [0, 0, 0],
max: [10, 10, 0.4]
});
assert.equal(data.layers.length, 2);
assert.deepEqual(data.layers.map((layer) => layer.z), [0.2, 0.4]);
assert.equal(data.stats.movementCommands, 7);
assert.equal(data.stats.extrusionMoves, 4);
assert.equal(data.stats.travelMoves, 3);
assert.equal(data.stats.temperatureCommands, 1);
assert.equal(Math.round(data.stats.extrusionMm * 1000) / 1000, 1.6);
assert.equal(data.segments.filter((segment) => segment.kind === "extrusion").length, 4);
assert.equal(data.segments[1].layerIndex, 0);
assert.equal(data.segments.at(-1).layerIndex, 1);
});
test("parseGcode follows relative moves, samples IJ arcs, and reports ignored commands", () => {
const data = parseGcode(`
G91
M83
G1 X2 E0.05
G2 X2 Y0 E0.05 I1 J0
G4 P200
`);
assert.ok(data.stats.movementCommands > 2);
assert.equal(data.stats.extrusionMoves, data.stats.movementCommands);
assert.ok(data.segments.some((segment) => segment.arc));
assert.ok(data.warnings.some((warning) => warning.includes("Relative XYZ positioning")));
assert.ok(data.warnings.some((warning) => /G4 line \d+/.test(warning)));
});
test("parseGcode summarizes slicer feature annotations including supports", () => {
const data = parseGcode(`
G21
G90
M82
G0 X0 Y0 Z0.2
;TYPE:WALL-OUTER
G1 X10 Y0 E0.4
; FEATURE: Support interface
G1 X10 Y10 E0.8
;TYPE:SUPPORT
G1 X0 Y10 E1.2
; FEATURE: Sparse infill
G1 X0 Y0 E1.6
`);
const featuresById = new Map(data.features.map((feature) => [feature.id, feature]));
assert.equal(featuresById.get("outer-wall")?.label, "Outer wall");
assert.equal(featuresById.get("support-interface")?.category, "support");
assert.equal(featuresById.get("support-interface")?.extrusionMoves, 1);
assert.deepEqual(featuresById.get("support-interface")?.rawLabels, ["Support interface"]);
assert.equal(featuresById.get("support")?.extrusionMoves, 1);
assert.equal(featuresById.get("sparse-infill")?.category, "infill");
assert.deepEqual(
data.segments.filter((segment) => segment.kind === "extrusion").map((segment) => segment.featureId),
["outer-wall", "support-interface", "support", "sparse-infill"]
);
});
test("buildGcodePreviewMeshData turns visible G-code paths into layer-colored mesh data", () => {
const data = parseGcode(`
G21
G90
M82
G0 X0 Y0 Z0.2
;TYPE:WALL-OUTER
G1 X4 Y0 E0.2
G0 X4 Y4
G1 X0 Y4 E0.4
G1 Z0.4
G1 X0 Y0 E0.6
`);
const extrusionOnly = buildGcodePreviewMeshData(data, {
showTravel: false,
maxLayer: 0
});
assert.equal(extrusionOnly.sourceFormat, "gcode");
assert.deepEqual(extrusionOnly.metadata.options.layerRange, [0, 0]);
assert.equal(extrusionOnly.metadata.options.showTravel, false);
assert.equal(extrusionOnly.metadata.previewGeometry, "ribbon");
assert.equal(extrusionOnly.metadata.features.some((feature) => feature.id === "outer-wall"), true);
assert.equal(extrusionOnly.has_source_colors, true);
assert.ok(extrusionOnly.vertices.length > 0);
assert.ok(extrusionOnly.indices.length > 0);
assert.equal(extrusionOnly.colors.length, extrusionOnly.vertices.length);
assert.equal(extrusionOnly.normals.length, extrusionOnly.vertices.length);
assert.equal(extrusionOnly.bounds.min[2], 0.2);
assert.ok(extrusionOnly.vertices[2] > extrusionOnly.bounds.min[2]);
assert.deepEqual(extrusionOnly.parts.map((part) => part.label), ["Layer 1"]);
const withTravel = buildGcodePreviewMeshData(data, {
showTravel: true,
maxLayer: 1
});
assert.ok(withTravel.parts.some((part) => part.label === "Travel"));
assert.ok(withTravel.parts.some((part) => part.label === "Layer 2"));
assert.equal(withTravel.metadata.stats.extrusionMoves, 3);
});
test("buildGcodePreviewMeshData renders support paths with a neutral color and z bias", () => {
const data = parseGcode(`
G21
G90
M82
G0 X0 Y0 Z0.2
;TYPE:WALL-OUTER
G1 X4 Y0 E0.2
G1 Z0.4
G1 X4 Y4 E0.4
G1 Z0.6
;TYPE:SUPPORT
G1 X0 Y4 E0.6
`);
const preview = buildGcodePreviewMeshData(data, { detailMode: "full" });
const supportVertexOffset = 2 * 4 * 3;
const supportRgb = Array.from(preview.colors.slice(supportVertexOffset, supportVertexOffset + 3));
assert.ok(supportRgb[0] < 0.75);
assert.ok(supportRgb[1] > 0.45);
assert.ok(supportRgb[2] > 0.55);
assert.ok(preview.vertices[supportVertexOffset + 2] > 0.6);
assert.equal(preview.bounds.max[2], 0.6);
});
test("buildGcodePreviewMeshData adaptively samples whole layers unless full detail is requested", () => {
const moves = Array.from({ length: 8 }, (_, layerIndex) => `
G1 Z${((layerIndex + 1) * 0.2).toFixed(1)}
G1 X${layerIndex * 2 + 1} Y0 E${(layerIndex * 2 + 1) / 10}
G1 X${layerIndex * 2 + 2} Y0 E${(layerIndex * 2 + 2) / 10}
`).join("\n");
const data = parseGcode(`
G21
G90
M82
G0 X0 Y0 Z0.2
${moves}
`);
const adaptive = buildGcodePreviewMeshData(data, {
maxVisibleSegments: 6
});
assert.equal(adaptive.metadata.visibleSegments, 16);
assert.equal(adaptive.metadata.segmentStride, 1);
assert.equal(adaptive.metadata.layerStride, 4);
assert.equal(adaptive.metadata.renderedLayers, 3);
assert.equal(adaptive.metadata.renderedSegments, 6);
assert.equal(adaptive.metadata.decimationMode, "layers");
assert.equal(adaptive.metadata.decimated, true);
assert.deepEqual(adaptive.parts.map((part) => part.label), ["Layer 1", "Layer 5", "Layer 8"]);
const full = buildGcodePreviewMeshData(data, {
detailMode: "full",
maxVisibleSegments: 6
});
assert.equal(full.metadata.visibleSegments, 16);
assert.equal(full.metadata.segmentStride, 1);
assert.equal(full.metadata.layerStride, 1);
assert.equal(full.metadata.renderedSegments, 16);
assert.equal(full.metadata.decimated, false);
const maxAdaptive = buildGcodePreviewMeshData(data, {
detailLevel: 99
});
assert.equal(maxAdaptive.metadata.options.detailLevel, GCODE_PREVIEW_DETAIL_MAX);
});