373 lines
22 KiB
JavaScript
373 lines
22 KiB
JavaScript
import { fnv1a64 } from "./replay.mjs";
|
|
import { exportNativeCheckpoint, restoreNativeCheckpoint } from "./native-checkpoint.mjs";
|
|
|
|
const SCHEMA = "wasm_program_runtime.v1";
|
|
const SEGMENT_BYTES = 216;
|
|
const POSE_BYTES = 72;
|
|
const AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
|
|
const DEFAULT_RAPID_VELOCITY = 1000;
|
|
const PROGRAM_ERROR_REASONS = Object.freeze({
|
|
[-1100]: "wasm_program_invalid_argument", [-1101]: "wasm_program_range_error", [-1102]: "wasm_program_invalid_state",
|
|
[-1103]: "wasm_program_control_error", [-1104]: "wasm_program_interpreter_error", [-1105]: "wasm_program_motion_error",
|
|
});
|
|
|
|
function programError(errorPrefix, result) {
|
|
const reason = PROGRAM_ERROR_REASONS[result] ?? `wasm_${errorPrefix}_${result}`;
|
|
return { status: "error", code: `wasm_${errorPrefix}_${result}`, result, reason, message_key: reason, source: "linuxcnc-wasm-sim_program" };
|
|
}
|
|
|
|
function modalCode(prefix, value, fallback) { return `${prefix}${Number.isFinite(Number(value)) ? Number(value) : fallback}`; }
|
|
|
|
function activeGcodes(state) {
|
|
const motion = { 0: "G0", 1: "G1", 2: "G2", 3: "G3" }[state.motion_mode] ?? "G1";
|
|
const spindle = { 3: "M3", 4: "M4", 5: "M5" }[state.spindle_mode] ?? `M${state.spindle_mode ?? 5}`;
|
|
const feed = Number(state.feed ?? 0);
|
|
const speed = Number(state.speed ?? 0);
|
|
return [state.units === 20 ? "G20" : "G21", state.distance_mode === 91 ? "G91" : "G90", modalCode("G", state.plane, 17), motion, spindle, `F${Number.isFinite(feed) ? feed : 0}`, `S${Number.isFinite(speed) ? speed : 0}`].join(" ");
|
|
}
|
|
|
|
function call(core, name, returnType, argTypes, args) {
|
|
const fn = core?.[`_${name}`] ?? core?.[name];
|
|
if (typeof fn === "function") return fn(...args);
|
|
if (typeof core?.ccall === "function") return core.ccall(name, returnType, argTypes, args);
|
|
throw new Error(`missing core export: ${name}`);
|
|
}
|
|
|
|
function pose(core, pointer) {
|
|
const values = [...core.HEAPF64.subarray(pointer / 8, pointer / 8 + 9)];
|
|
return Object.fromEntries(AXES.map((axis, index) => [axis, values[index]]));
|
|
}
|
|
|
|
function segment(core, pointer) {
|
|
const start = pose(core, pointer);
|
|
const end = pose(core, pointer + 72);
|
|
const cart = (offset) => ({ x: core.HEAPF64[pointer / 8 + offset / 8], y: core.HEAPF64[pointer / 8 + offset / 8 + 1], z: core.HEAPF64[pointer / 8 + offset / 8 + 2] });
|
|
const ints = new DataView(core.HEAPU8.buffer, pointer, SEGMENT_BYTES);
|
|
return { start, end, center: cart(144), normal: cart(168), velocity: core.HEAPF64[pointer / 8 + 24], acceleration: core.HEAPF64[pointer / 8 + 25], motion_type: ints.getInt32(208, true), turn: ints.getInt32(212, true) };
|
|
}
|
|
|
|
function sampleSegment(core, segmentPointer, posePointer, value) {
|
|
const turn = Math.max(1, Math.abs(Number(value.turn ?? 1)));
|
|
const count = value.motion_type === 2 ? Math.max(32, Math.ceil(turn * 32)) : 1;
|
|
const samples = [];
|
|
for (let index = 0; index <= count; index += 1) {
|
|
const result = call(core, "sim_posemath_segment_point", "number", ["number", "number", "number"], [segmentPointer, index / count, posePointer]);
|
|
if (result !== 0) throw new Error(`wasm_program_segment_sample_${result}`);
|
|
samples.push(pose(core, posePointer));
|
|
}
|
|
return samples;
|
|
}
|
|
|
|
function readString(core, pointer, capacity) {
|
|
const bytes = core.HEAPU8.subarray(pointer, pointer + capacity);
|
|
const end = bytes.indexOf(0);
|
|
return new TextDecoder().decode(bytes.subarray(0, end < 0 ? bytes.length : end));
|
|
}
|
|
|
|
export class WasmProgramRuntime {
|
|
constructor(core) {
|
|
this.core = core;
|
|
this.available = ["sim_program_size", "sim_program_init", "sim_program_step", "sim_program_canonical", "sim_program_add_subroutine", "sim_program_set_rapid_velocity"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
|
this.pointer = 0;
|
|
this.motionPointer = 0;
|
|
this.samplePointer = 0;
|
|
this.samplePosePointer = 0;
|
|
this.source = "";
|
|
this.hashedSource = null;
|
|
this.cachedSourceHash = null;
|
|
this.path = null;
|
|
this.subroutines = [];
|
|
this.executionSource = "";
|
|
this.executionLines = [];
|
|
this.executionLineMetadata = [];
|
|
this.rapidVelocity = 35;
|
|
this.synchronizedPosition = Object.fromEntries(AXES.map((axis) => [axis, 0]));
|
|
if (!this.available || typeof core?._malloc !== "function") return;
|
|
this.pointer = core._malloc(call(core, "sim_program_size", "number", [], []));
|
|
this.motionPointer = core._malloc(SEGMENT_BYTES);
|
|
this.samplePointer = core._malloc(SEGMENT_BYTES);
|
|
this.samplePosePointer = core._malloc(POSE_BYTES);
|
|
if (!this.pointer || !this.motionPointer || !this.samplePointer || !this.samplePosePointer) throw new Error("wasm_program_allocation_failed");
|
|
const result = call(core, "sim_program_init", "number", ["number"], [this.pointer]);
|
|
if (result !== 0) throw new Error(`wasm_program_init_${result}`);
|
|
}
|
|
|
|
invoke(name, args = [], errorPrefix = name) {
|
|
if (!this.available) return { status: "blocked", code: "wasm_program_unavailable" };
|
|
const result = call(this.core, name, "number", ["number", ...args.map(() => "number")], [this.pointer, ...args]);
|
|
return result === 0 ? { status: "verified", command: name } : { status: "error", code: `wasm_${errorPrefix}_${result}`, result, reason: PROGRAM_ERROR_REASONS[result] ?? `wasm_${errorPrefix}_${result}`, message_key: PROGRAM_ERROR_REASONS[result] ?? `wasm_${errorPrefix}_${result}`, source: "linuxcnc-wasm-sim_program" };
|
|
}
|
|
|
|
get sourceHash() {
|
|
// Source text is immutable between file replacements. Only its identity
|
|
// needs checking on a tick; the canonical WASM state is still read fresh.
|
|
if (this.hashedSource !== this.source) {
|
|
this.hashedSource = this.source;
|
|
this.cachedSourceHash = this.source ? fnv1a64(this.source) : null;
|
|
}
|
|
return this.cachedSourceHash;
|
|
}
|
|
|
|
load(source, path = null) {
|
|
if (typeof source !== "string" || source.length === 0) return { status: "error", code: "program_source_invalid" };
|
|
const bytes = new TextEncoder().encode(source);
|
|
const pointer = this.core._malloc(bytes.byteLength);
|
|
this.core.HEAPU8.set(bytes, pointer);
|
|
try {
|
|
const result = call(this.core, "sim_program_load", "number", ["number", "number", "number"], [this.pointer, pointer, bytes.byteLength]);
|
|
if (result !== 0) return programError("program_load", result);
|
|
const velocityResult = call(this.core, "sim_program_set_rapid_velocity", "number", ["number", "number"], [this.pointer, this.rapidVelocity]);
|
|
if (velocityResult !== 0) return { status: "error", code: `wasm_program_rapid_velocity_${velocityResult}` };
|
|
this.source = source;
|
|
this.path = path;
|
|
this.subroutines = [];
|
|
this.executionSource = source;
|
|
this.executionLines = source.split(/\r?\n/);
|
|
this.executionLineMetadata = this.executionLines.map((_, index) => ({ path: this.path, line: index + 1 }));
|
|
while (this.executionLines.length && this.executionLines.at(-1) === "") {
|
|
this.executionLines.pop();
|
|
this.executionLineMetadata.pop();
|
|
}
|
|
return { status: "verified", program: this.snapshot() };
|
|
} finally { this.core._free(pointer); }
|
|
}
|
|
|
|
requiresSubroutines(source = this.source) {
|
|
if (typeof source !== "string" || source.length === 0) return { status: "error", code: "program_source_invalid" };
|
|
const bytes = new TextEncoder().encode(source);
|
|
const pointer = this.core._malloc(bytes.byteLength);
|
|
this.core.HEAPU8.set(bytes, pointer);
|
|
try {
|
|
const result = call(this.core, "sim_program_requires_subroutines", "number", ["number", "number"], [pointer, bytes.byteLength]);
|
|
if (result < 0) return { status: "error", code: `wasm_program_requires_subroutines_${result}` };
|
|
return { status: "verified", required: result === 1 };
|
|
} finally { this.core._free(pointer); }
|
|
}
|
|
|
|
preview() {
|
|
if (!this.source) return { status: "error", code: "program_preview_source_missing" };
|
|
const temporary = new WasmProgramRuntime(this.core);
|
|
const segments = [];
|
|
try {
|
|
const loaded = temporary.load(this.source, this.path);
|
|
if (loaded.status !== "verified") return loaded;
|
|
const synchronized = temporary.synchronizePosition(this.synchronizedPosition);
|
|
if (synchronized.status !== "verified") return synchronized;
|
|
const state = this.snapshot();
|
|
const initialized = temporary.initializeCoordinates(state.coordinate_context, state);
|
|
if (initialized.status !== "verified") return initialized;
|
|
temporary.setRapidVelocity(this.rapidVelocity);
|
|
for (const subroutine of this.subroutines) {
|
|
const added = temporary.addSubroutine(subroutine.name, subroutine.source, subroutine.path);
|
|
if (added.status !== "verified") return added;
|
|
}
|
|
const started = temporary.start();
|
|
if (started.status !== "verified") return started;
|
|
const limit = Math.min(100000, Math.max(1, temporary.snapshot().line_count * 8 + 1));
|
|
let kinematicsMode = 0;
|
|
for (let index = 0; index < limit; index += 1) {
|
|
const step = temporary.step();
|
|
if (step.status !== "verified") return { status: "error", code: step.code, line: temporary.snapshot().current_line, segments };
|
|
if (step.has_control && [428, 429, 430].includes(step.control_code)) kinematicsMode = { 428: 1, 429: 0, 430: 2 }[step.control_code];
|
|
if (step.motion) segments.push({ id: segments.length + 1, kinematics_mode: kinematicsMode, ...step.motion });
|
|
if ([4, 5, 6].includes(step.program.status)) break;
|
|
}
|
|
return { status: "verified", schema: "wasm-canonical-preview.v1", source: "linuxcnc-wasm-sim_program", path: this.path, source_hash: this.sourceHash, line_count: this.snapshot().line_count, segments, preview_hash: fnv1a64(segments) };
|
|
} finally { temporary.destroy(); }
|
|
}
|
|
|
|
start() { return this.invoke("sim_program_start", [], "program_start"); }
|
|
initializeCoordinates(context, state = {}) {
|
|
if (!context) return { status: "verified" };
|
|
const previous = this.snapshot().coordinate_context;
|
|
const blocks = ["G21"];
|
|
for (const [index, offsets] of context.offsets.entries()) {
|
|
if (offsets.some((value, axis) => value !== (previous?.offsets?.[index]?.[axis] ?? 0))) blocks.push(`G10 L2 P${index + 1} ${AXES.map((axis, a) => `${axis.toUpperCase()}${offsets[a]}`).join(" ")}`);
|
|
}
|
|
const system = context.system <= 6 ? `G${53 + context.system}` : `G59.${context.system - 6}`;
|
|
blocks.push(`${system} G${state.units === 20 ? 20 : 21} G${state.distance_mode === 91 ? 91 : 90}`);
|
|
for (const block of blocks) {
|
|
const result = this.executeBlock(block);
|
|
if (result.status !== "verified") return result;
|
|
}
|
|
return { status: "verified" };
|
|
}
|
|
synchronizePosition(value) {
|
|
const position = Object.fromEntries(AXES.map((axis) => [axis, Number(value?.[axis] ?? 0)]));
|
|
if (Object.values(position).some((coordinate) => !Number.isFinite(coordinate))) return { status: "error", code: "program_position_nonfinite" };
|
|
this.core.HEAPF64.set(AXES.map((axis) => position[axis]), this.samplePosePointer / 8);
|
|
const result = call(this.core, "sim_program_set_position", "number", ["number", "number"], [this.pointer, this.samplePosePointer]);
|
|
if (result !== 0) return programError("program_set_position", result);
|
|
this.synchronizedPosition = position;
|
|
return { status: "verified", command: "sim_program_set_position", position: { ...position } };
|
|
}
|
|
setRapidVelocity(velocity) {
|
|
if (!Number.isFinite(Number(velocity)) || Number(velocity) <= 0) return { status: "error", code: "program_rapid_velocity_invalid" };
|
|
this.rapidVelocity = Number(velocity);
|
|
const result = call(this.core, "sim_program_set_rapid_velocity", "number", ["number", "number"], [this.pointer, this.rapidVelocity]);
|
|
if (result !== 0) throw new Error(`wasm_program_set_rapid_velocity_${result}`);
|
|
return { status: "verified", command: "sim_program_set_rapid_velocity" };
|
|
}
|
|
addSubroutine(name, source, path = null) {
|
|
if (typeof source !== "string" || source.length === 0) return { status: "error", code: "program_subroutine_invalid" };
|
|
const bytes = new TextEncoder().encode(source);
|
|
const pointer = this.core._malloc(bytes.byteLength);
|
|
this.core.HEAPU8.set(bytes, pointer);
|
|
try {
|
|
const code = call(this.core, "sim_program_add_subroutine", "number", ["number", "number", "number"], [this.pointer, pointer, bytes.byteLength]);
|
|
if (code !== 0) return { status: "error", code: `wasm_program_add_subroutine_${code}`, result: code };
|
|
if (this.executionSource && !this.executionSource.endsWith("\n")) this.executionSource += "\n";
|
|
if (this.executionLines.length && this.executionLines.at(-1) !== "") {
|
|
this.executionLines.push("");
|
|
this.executionLineMetadata.push({ path: null, line: 0 });
|
|
}
|
|
this.executionSource += source;
|
|
if (this.executionLines.length && this.executionLines.at(-1) === "") {
|
|
this.executionLines.pop();
|
|
this.executionLineMetadata.pop();
|
|
}
|
|
this.executionLines.push(...source.split(/\r?\n/));
|
|
this.executionLineMetadata.push(...source.split(/\r?\n/).map((_, index) => ({ path, line: index + 1 })));
|
|
this.subroutines.push({ name: String(name ?? "subroutine"), path, source });
|
|
return { status: "verified", command: "sim_program_add_subroutine", name: String(name ?? "subroutine") };
|
|
} finally { this.core._free(pointer); }
|
|
}
|
|
pause() { return this.invoke("sim_program_pause", [], "program_pause"); }
|
|
resume() { return this.invoke("sim_program_resume", [], "program_resume"); }
|
|
abort() { return this.invoke("sim_program_abort", [], "program_abort"); }
|
|
seek(line) { return this.invoke("sim_program_seek", [line], "program_seek"); }
|
|
|
|
restoreState(value) {
|
|
if (!value || ![SCHEMA, "sim_program.v1"].includes(value.schema) || !Number.isSafeInteger(value.current_line) || value.current_line < 0 || !this.source && !value.native_state) throw new Error("wasm_program_restore_schema");
|
|
if (value.source_hash && value.source_hash !== this.sourceHash) throw new Error("wasm_program_restore_source_hash");
|
|
if (value.native_state) {
|
|
if (value.execution_source_hash !== fnv1a64(this.executionSource)) throw new Error("wasm_program_restore_execution_source_hash");
|
|
restoreNativeCheckpoint(this.core, "sim_program", value.native_state, this.pointer);
|
|
this.synchronizedPosition = structuredClone(value.synchronized_position);
|
|
this.rapidVelocity = value.rapid_velocity;
|
|
return this.snapshot();
|
|
}
|
|
const subroutines = [...this.subroutines];
|
|
const loaded = this.load(this.source, this.path);
|
|
if (loaded.status !== "verified") throw new Error(loaded.code);
|
|
const initialized = this.initializeCoordinates(value.coordinate_context, value);
|
|
if (initialized.status !== "verified") throw new Error(initialized.code);
|
|
const synchronized = this.synchronizePosition(Object.fromEntries(AXES.map((axis) => [axis, Number(value[axis] ?? 0)])));
|
|
if (synchronized.status !== "verified") throw new Error(synchronized.code);
|
|
for (const subroutine of subroutines) {
|
|
const added = this.addSubroutine(subroutine.name, subroutine.source);
|
|
if (added.status !== "verified") throw new Error(added.code);
|
|
}
|
|
if (value.current_line > 0) {
|
|
const seek = this.seek(value.current_line);
|
|
if (seek.status !== "verified") throw new Error(seek.code);
|
|
}
|
|
const status = value.program_status ?? ({ 0: "idle", 1: "loaded", 2: "running", 3: "paused", 4: "completed", 5: "aborted", 6: "fault" }[value.status] ?? "loaded");
|
|
if (status === "running") {
|
|
const resumed = this.resume();
|
|
if (resumed.status !== "verified") {
|
|
const started = this.start();
|
|
if (started.status !== "verified") throw new Error(started.code);
|
|
}
|
|
} else if (status === "completed") {
|
|
this.start();
|
|
for (let index = 0; index < this.snapshot().line_count + 1 && this.snapshot().status === 2; index += 1) this.step();
|
|
} else if (status === "paused") {
|
|
const from = Number(value.pause_from ?? 2);
|
|
if (from !== 1) {
|
|
this.start();
|
|
if (from === 4) for (let index = 0; index < this.snapshot().line_count + 1 && this.snapshot().status === 2; index += 1) this.step();
|
|
}
|
|
const paused = this.pause();
|
|
if (paused.status !== "verified") throw new Error(paused.code);
|
|
} else if (status === "aborted") {
|
|
const aborted = this.abort();
|
|
if (aborted.status !== "verified") throw new Error(aborted.code);
|
|
}
|
|
if (value.has_motion === 0) this.clearMotion();
|
|
return this.snapshot();
|
|
}
|
|
|
|
executeBlock(block) {
|
|
const bytes = new TextEncoder().encode(String(block ?? ""));
|
|
const pointer = this.core._malloc(bytes.byteLength);
|
|
this.core.HEAPU8.set(bytes, pointer);
|
|
try {
|
|
const result = call(this.core, "sim_program_execute_block", "number", ["number", "number", "number"], [this.pointer, pointer, bytes.byteLength]);
|
|
if (result !== 0) return programError("program_execute", result);
|
|
const program = this.snapshot();
|
|
return { status: "verified", program, motion: this.readMotion(program) };
|
|
} finally { this.core._free(pointer); }
|
|
}
|
|
|
|
readMotion(state = this.snapshot()) {
|
|
if (!state.has_motion) return null;
|
|
const result = call(this.core, "sim_program_read_motion", "number", ["number", "number"], [this.pointer, this.motionPointer]);
|
|
if (result !== 0) return null;
|
|
const value = segment(this.core, this.motionPointer);
|
|
// LinuxCNC does not enqueue a trajectory segment when a linear/rapid
|
|
// block resolves to the current synchronized pose. This occurs naturally
|
|
// when AUTO_RUN reopens a completed/aborted file and its first positioning
|
|
// block names the position where the previous run stopped.
|
|
const linearNoop = value.motion_type === 1 && AXES.every((axis) => Math.abs(Number(value.start?.[axis] ?? 0) - Number(value.end?.[axis] ?? 0)) <= 1e-12);
|
|
if (linearNoop) {
|
|
call(this.core, "sim_program_clear_motion", "number", ["number"], [this.pointer]);
|
|
return null;
|
|
}
|
|
// LinuxCNC rapid blocks do not carry an F word. The trajectory queue
|
|
// requires a positive velocity, so use the simulation's configured rapid
|
|
// rate for that WASM-produced linear segment.
|
|
if (state.motion_mode === 0 && value.motion_type === 1 && value.velocity <= 0) value.velocity = DEFAULT_RAPID_VELOCITY;
|
|
value.samples = sampleSegment(this.core, this.motionPointer, this.samplePosePointer, value);
|
|
value.samples_source = "linuxcnc-wasm-sim_posemath_segment_point";
|
|
value.canonical_motion_mode = state.motion_mode;
|
|
value.kind = value.motion_type === 2 && Math.abs(Number(value.end?.z ?? 0) - Number(value.start?.z ?? 0)) > 1e-12 ? "helix" : value.motion_type === 2 ? "arc" : state.motion_mode === 0 ? "rapid" : value.motion_type === 1 ? "feed" : "traverse";
|
|
if (value.kind === "helix") {
|
|
value.helix = true;
|
|
value.radius = Math.hypot(Number(value.start?.x ?? 0) - Number(value.center?.x ?? 0), Number(value.start?.y ?? 0) - Number(value.center?.y ?? 0));
|
|
value.turns = Math.abs(Number(value.turn ?? 1));
|
|
value.axial_delta = Number(value.end?.z ?? 0) - Number(value.start?.z ?? 0);
|
|
}
|
|
call(this.core, "sim_program_clear_motion", "number", ["number"], [this.pointer]);
|
|
return { ...value, source_line: state.current_line };
|
|
}
|
|
|
|
clearMotion() { return this.invoke("sim_program_clear_motion", [], "program_clear_motion"); }
|
|
|
|
step() {
|
|
const result = this.invoke("sim_program_step", [], "program_step");
|
|
if (result.status !== "verified") return result;
|
|
const state = this.snapshot();
|
|
return { status: "verified", program_status: state.status, line: state.current_line, control_code: state.control_code ?? 0, has_control: state.has_control === 1, program: state, motion: this.readMotion(state) };
|
|
}
|
|
|
|
checkpoint() {
|
|
return { ...this.snapshot(), subroutine_sources: structuredClone(this.subroutines), synchronized_position: structuredClone(this.synchronizedPosition), execution_source_hash: fnv1a64(this.executionSource), native_state: exportNativeCheckpoint(this.core, "sim_program", this.pointer) };
|
|
}
|
|
|
|
snapshot() {
|
|
if (!this.available) return { schema: SCHEMA, status: "blocked", code: "wasm_program_unavailable" };
|
|
const required = call(this.core, "sim_program_canonical", "number", ["number", "number", "number"], [this.pointer, 0, 0]);
|
|
const buffer = this.core._malloc(required);
|
|
try {
|
|
call(this.core, "sim_program_canonical", "number", ["number", "number", "number"], [this.pointer, buffer, required]);
|
|
const state = JSON.parse(readString(this.core, buffer, required));
|
|
const hash = call(this.core, "sim_program_state_hash", "number", ["number"], [this.pointer]);
|
|
const programStatus = { 0: "idle", 1: "loaded", 2: "running", 3: "paused", 4: "completed", 5: "aborted", 6: "fault" }[state.status] ?? "idle";
|
|
const motionControl = { 0: "continuous", 1: "exact_path", 2: "exact_stop" }[state.motion_control_mode] ?? "continuous";
|
|
const executionLineText = state.current_line > 0 ? (this.executionLines[state.current_line - 1] ?? "").trim() : "";
|
|
const executionMeta = state.current_line > 0 ? (this.executionLineMetadata[state.current_line - 1] ?? { path: this.path, line: state.current_line }) : { path: this.path, line: 0 };
|
|
return { schema: SCHEMA, source: "linuxcnc-wasm-sim_program", ...state, motion_control: { mode: motionControl, path_tolerance: Number(state.path_tolerance ?? 0), naive_cam_tolerance: Number(state.naive_cam_tolerance ?? 0) }, active_gcodes: activeGcodes(state), loaded: state.status !== 0, line: state.current_line, currentLine: state.current_line, execution_line: state.current_line, execution_line_text: executionLineText, execution_source_file: executionMeta.path, execution_source_line: executionMeta.line, program_status: programStatus, path: this.path, subroutines: this.subroutines.map((subroutine) => subroutine.name), state_hash: `fnv1a64:${BigInt.asUintN(64, BigInt(hash >>> 0)).toString(16).padStart(16, "0")}`, source_hash: this.sourceHash };
|
|
} finally { this.core._free(buffer); }
|
|
}
|
|
|
|
destroy() {
|
|
if (this.pointer && (typeof this.core?._sim_program_destroy === "function" || typeof this.core?.sim_program_destroy === "function")) call(this.core, "sim_program_destroy", "number", ["number"], [this.pointer]);
|
|
for (const pointer of [this.pointer, this.motionPointer, this.samplePointer, this.samplePosePointer]) if (pointer && typeof this.core?._free === "function") this.core._free(pointer);
|
|
this.pointer = this.motionPointer = this.samplePointer = this.samplePosePointer = 0;
|
|
this.executionSource = "";
|
|
this.executionLines = [];
|
|
this.executionLineMetadata = [];
|
|
}
|
|
}
|