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(value: T | undefined, message: string): T { if (value === undefined) { throw new Error(message); } return value; }