Files
KDL_WORK/kdl-wasm/web/src/olp/grl.ts
2026-06-28 08:20:33 +08:00

174 lines
5.4 KiB
TypeScript

import {
generateGrlProgram,
type GeneratedGrlProgram,
type GeneratedPathPointSpec,
type GrlGeneratorStyle,
type GrlProgramGenerationSpec
} from "../grl/generator/index.js";
import type {
OlpOperation,
OlpPath,
OlpPathPoint,
OlpProgram,
OlpProjectModel,
OlpSpeed,
OlpTarget,
OlpZone
} from "./model.js";
import { validateOlpProject } from "./model.js";
export interface PathOperationGrlOptions {
moduleName?: string;
programId?: string;
style?: GrlGeneratorStyle;
}
export interface PathOperationGrlResult extends GeneratedGrlProgram {
spec: GrlProgramGenerationSpec;
program: OlpProgram;
}
export function pathOperationToGrl(
model: OlpProjectModel,
operationId: string,
options: PathOperationGrlOptions = {}
): PathOperationGrlResult {
const validation = validateOlpProject(model);
const errors = validation.issues.filter((issue) => issue.severity === "error");
if (errors.length > 0) {
throw new Error(`Cannot generate GRL from invalid OLP model: ${errors.map((issue) => issue.code).join(", ")}`);
}
const operation = required(model.operations.find((item) => item.id === operationId), `Operation ${operationId} not found`);
const path = required(model.paths.find((item) => item.id === operation.pathId), `Path ${operation.pathId} not found`);
const targets = collectTargetsForPath(model.targets, path);
const spec = toGenerationSpec(model, operation, path, targets, options.moduleName);
const generated = generateGrlProgram(spec, options.style ?? "expanded");
const program: OlpProgram = {
id: options.programId ?? `${operation.id}_grl`,
name: spec.moduleName,
language: "grl",
entryOperationIds: [operation.id],
source: {
kind: "generated",
text: generated.text,
generatedFrom: {
operationId: operation.id,
pathId: path.id
}
}
};
return {
...generated,
spec,
program
};
}
function toGenerationSpec(
model: OlpProjectModel,
operation: OlpOperation,
path: OlpPath,
targets: OlpTarget[],
moduleName?: string
): GrlProgramGenerationSpec {
return {
moduleName: moduleName ?? sanitizeIdentifier(model.project.name),
speeds: Object.fromEntries(model.speeds.map((speed) => [speed.name, renderSpeed(speed)])),
zones: Object.fromEntries(model.zones.map((zone) => [zone.name, renderZone(zone)])),
targets: targets.map((target) => {
if (target.kind === "joint") {
return {
name: target.name,
kind: "joint" as const,
values: target.joints ?? []
};
}
return {
name: target.name,
kind: "pose" as const,
values: target.pose ?? [0, 0, 0, 0, 0, 0]
};
}),
path: {
name: path.name,
...(path.source ? { source: path.source } : {}),
defaults: {
speed: resolveSpeedName(model, path.defaults?.speedId) ?? "v_linear",
zone: resolveZoneName(model, path.defaults?.zoneId) ?? "z10"
},
points: path.points.map((point) => toGeneratedPoint(model, point))
},
operation: {
name: operation.name,
kind: operation.kind,
path: path.name,
...(operation.startAction ? { startAction: operation.startAction } : {}),
...(operation.endAction ? { endAction: operation.endAction } : {})
}
};
}
function collectTargetsForPath(allTargets: OlpTarget[], path: OlpPath): OlpTarget[] {
const targetIds = new Set(path.points.flatMap((point) => [point.targetId, point.viaTargetId].filter((id): id is string => Boolean(id))));
return allTargets.filter((target) => targetIds.has(target.id));
}
function toGeneratedPoint(model: OlpProjectModel, point: OlpPathPoint): GeneratedPathPointSpec {
const target = required(model.targets.find((item) => item.id === point.targetId), `Target ${point.targetId} not found`);
const via = point.viaTargetId
? required(model.targets.find((item) => item.id === point.viaTargetId), `Via target ${point.viaTargetId} not found`)
: undefined;
return {
id: point.name,
motion: point.motion,
target: target.name,
...(via ? { via: via.name } : {}),
...(point.speedId ? { speed: required(resolveSpeedName(model, point.speedId), `Speed ${point.speedId} not found`) } : {}),
...(point.zoneId ? { zone: required(resolveZoneName(model, point.zoneId), `Zone ${point.zoneId} not found`) } : {})
};
}
function renderSpeed(speed: OlpSpeed): string {
if (speed.kind === "joint_percent") {
return `joint(${speed.value} %)`;
}
return `linear(${speed.value} mm/s)`;
}
function renderZone(zone: OlpZone): string {
if (zone.kind === "fine") {
return "fine";
}
if (zone.kind === "cnt") {
return `z(${zone.value ?? 10})`;
}
return `z(${zone.value ?? 10} mm)`;
}
function resolveSpeedName(model: OlpProjectModel, id?: string): string | undefined {
if (!id) return undefined;
return model.speeds.find((speed) => speed.id === id)?.name;
}
function resolveZoneName(model: OlpProjectModel, id?: string): string | undefined {
if (!id) return undefined;
return model.zones.find((zone) => zone.id === id)?.name;
}
function sanitizeIdentifier(value: string): string {
const sanitized = value.replace(/[^A-Za-z0-9_]/g, "_");
if (/^[A-Za-z_]/.test(sanitized)) {
return sanitized;
}
return `Project_${sanitized}`;
}
function required<T>(value: T | undefined, message: string): T {
if (value === undefined) {
throw new Error(message);
}
return value;
}