fullcontrol-js
Version:
FullControl TypeScript rewrite - API parity mirror of Python library (G-code generation & geometry)
1 lines • 175 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/core/base-model.ts","../src/util/format.ts","../src/models/point.ts","../src/models/vector.ts","../src/models/extrusion.ts","../src/models/printer.ts","../src/models/auxiliary.ts","../src/models/commands.ts","../src/models/controls.ts","../src/models/annotations.ts","../src/util/extra.ts","../src/util/check.ts","../src/geometry/polar.ts","../src/geometry/midpoint.ts","../src/geometry/measure.ts","../src/geometry/move.ts","../src/geometry/move_polar.ts","../src/geometry/reflect.ts","../src/geometry/reflect_polar.ts","../src/geometry/segmentation.ts","../src/geometry/ramping.ts","../src/geometry/arcs.ts","../src/geometry/shapes.ts","../src/geometry/waves.ts","../src/geometry/travel_to.ts","../src/gcode/primer/index.ts","../src/devices/community/singletool/generic.ts","../src/devices/community/singletool/base_settings.ts","../src/pipeline/state.ts","../src/pipeline/gcode.ts","../src/pipeline/visualize.ts","../src/pipeline/transform.ts","../src/devices/community/singletool/custom.ts","../src/devices/community/singletool/prusa_mk4.ts","../src/devices/community/singletool/prusa_i3.ts","../src/devices/community/singletool/prusa_mini.ts","../src/devices/community/singletool/ender_3.ts","../src/devices/community/singletool/ender_5_plus.ts","../src/devices/community/singletool/voron_zero.ts","../src/devices/community/singletool/bambulab_x1.ts","../src/devices/community/singletool/cr_10.ts","../src/devices/community/singletool/wasp2040clay.ts","../src/devices/community/singletool/toolchanger_T0.ts","../src/devices/community/singletool/toolchanger_T1.ts","../src/devices/community/singletool/toolchanger_T2.ts","../src/devices/community/singletool/toolchanger_T3.ts","../src/devices/community/singletool/raise3d_pro2_nozzle1.ts","../src/devices/community/singletool/ultimaker2plus.ts","../src/devices/community/singletool/import_printer.ts"],"sourcesContent":["// Public API exports will be aggregated here.\r\n// Incrementally populated as modules are ported.\r\n\r\nexport * from './models/point.js'\r\nexport * from './models/vector.js'\r\nexport * from './models/extrusion.js'\r\nexport * from './models/printer.js'\r\nexport * from './models/auxiliary.js'\r\nexport * from './models/commands.js'\r\nexport * from './models/controls.js'\r\nexport * from './models/annotations.js'\r\n// geometry\r\nexport * from './geometry/polar.js'\r\nexport * from './geometry/midpoint.js'\r\nexport * from './geometry/measure.js'\r\nexport * from './geometry/move.js'\r\nexport * from './geometry/move_polar.js'\r\nexport * from './geometry/reflect.js'\r\nexport * from './geometry/reflect_polar.js'\r\nexport * from './geometry/segmentation.js'\r\nexport * from './geometry/arcs.js'\r\nexport * from './geometry/ramping.js'\r\nexport * from './geometry/shapes.js'\r\nexport * from './geometry/waves.js'\r\nexport * from './geometry/travel_to.js'\r\n\r\n// utility functions\r\nexport * from './util/extra.js'\r\nexport * from './util/check.js'\r\n\r\n// pipeline / transform\r\nexport * from './pipeline/state.js'\r\nexport * from './pipeline/gcode.js'\r\nexport * from './pipeline/visualize.js'\r\nexport * from './pipeline/transform.js'\r\n\r\n// devices loader\r\nexport { import_printer } from './devices/community/singletool/import_printer.js'\r\n\r\n\r\n","// Lightweight analogue of Python BaseModelPlus (pydantic)\r\nexport interface InitObject<T> { [key: string]: any }\r\n\r\nexport abstract class BaseModelPlus {\r\n // Accept partial init object; assign only known keys if enforceKeys provided\r\n constructor(init?: InitObject<any>, enforceKeys?: readonly string[]) {\r\n if (init) {\r\n for (const k of Object.keys(init)) {\r\n if (!enforceKeys || enforceKeys.includes(k)) {\r\n (this as any)[k] = init[k]\r\n } else {\r\n // mimic Python strictness with an error\r\n throw new Error(`attribute \"${k}\" not allowed for class ${this.constructor.name}`)\r\n }\r\n }\r\n }\r\n }\r\n\r\n updateFrom(source: any) {\r\n if (!source) return\r\n for (const [k, v] of Object.entries(source)) {\r\n if (v === undefined) continue\r\n // Mirror Python behavior: attributes can appear dynamically\r\n ;(this as any)[k] = v\r\n }\r\n }\r\n\r\n // Python parity alias\r\n update_from(source: any) { this.updateFrom(source) }\r\n\r\n copy<T extends this>(): T {\r\n const clone = Object.create(this.constructor.prototype)\r\n for (const [k, v] of Object.entries(this)) (clone as any)[k] = structuredClone(v)\r\n return clone\r\n }\r\n}\r\n","/**\r\n * Formatting utilities to match Python's gcode output formatting\r\n */\r\n\r\n/**\r\n * Format a number with at most 6 significant figures (matching Python's .6 format)\r\n * Used for StationaryExtrusion E values\r\n */\r\nexport function formatPrecision6(val: number): string {\r\n return val.toPrecision(6).replace(/\\.?0+$/, '')\r\n}\r\n\r\n/**\r\n * Format coordinate values (X, Y, Z) - up to 6 decimal places, strip trailing zeros\r\n * Matches Python's .6f format with rstrip('0').rstrip('.')\r\n */\r\nexport function formatCoordinate(val: number): string {\r\n return val.toFixed(6).replace(/0+$/, '').replace(/\\.$/, '')\r\n}\r\n\r\n/**\r\n * Format extrusion values (E) - up to 6 decimal places, strip trailing zeros\r\n * Matches Python's .6f format with rstrip('0').rstrip('.')\r\n */\r\nexport function formatExtrusion(val: number): string {\r\n return val.toFixed(6).replace(/0+$/, '').replace(/\\.$/, '')\r\n}\r\n\r\n/**\r\n * Format feedrate values (F) - strip unnecessary decimals\r\n */\r\nexport function formatFeedrate(val: number): string {\r\n return val.toFixed(1).replace(/\\.0+$/, '').replace(/\\.$/, '')\r\n}\r\n","import { BaseModelPlus } from '../core/base-model.js'\r\nimport { formatCoordinate } from '../util/format.js'\r\n\r\nexport class Point extends BaseModelPlus {\r\n x?: number\r\n y?: number\r\n z?: number\r\n color?: [number, number, number] // optional for parity with visualization\r\n extrude?: boolean // whether this move should extrude\r\n speed?: number // optional per-move speed override\r\n static readonly typeName = 'Point'\r\n constructor(init?: Partial<Point>) { super(init) }\r\n toJSON() { return { x: this.x, y: this.y, z: this.z, color: this.color, extrude: this.extrude, speed: this.speed } }\r\n static fromJSON(data: any) { return new Point(data) }\r\n\r\n XYZ_gcode(prev: Point) {\r\n let s = ''\r\n if (this.x != null && this.x !== prev.x) s += `X${formatCoordinate(this.x)} `\r\n if (this.y != null && this.y !== prev.y) s += `Y${formatCoordinate(this.y)} `\r\n if (this.z != null && this.z !== prev.z) s += `Z${formatCoordinate(this.z)} `\r\n return s === '' ? undefined : s\r\n }\r\n\r\n gcode(state: any) { // state typed loosely to avoid circular import\r\n const XYZ = this.XYZ_gcode(state.point)\r\n if (XYZ == null) return undefined\r\n const isFirst = state._first_movement_done !== true\r\n const extruding = !!state.extruder?.on && !isFirst\r\n // First movement always G0 travel (Python basic_line ordering)\r\n const G = isFirst ? 'G0 ' : ((extruding || state.extruder?.travel_format === 'G1_E0') ? 'G1 ' : 'G0 ')\r\n const F = state.printer?.f_gcode(state) || ''\r\n let E = ''\r\n if (!isFirst && state.extruder) {\r\n if (extruding) E = state.extruder.e_gcode(this, state)\r\n else if (state.extruder?.travel_format === 'G1_E0') E = state.extruder.e_gcode(this, state)\r\n }\r\n const line = `${G}${F}${XYZ}${E}`.trim()\r\n if (state.printer) state.printer.speed_changed = false\r\n state.point.updateFrom(this)\r\n if (isFirst) state._first_movement_done = true\r\n return line\r\n }\r\n}\r\n","export class Vector {\r\n x?: number\r\n y?: number\r\n z?: number\r\n constructor(init?: Partial<Vector>) { Object.assign(this, init) }\r\n static readonly typeName = 'Vector'\r\n toJSON() { return { x: this.x, y: this.y, z: this.z } }\r\n static fromJSON(data: any) { return new Vector(data) }\r\n}\r\n","import { BaseModelPlus } from '../core/base-model.js'\r\nimport { Point } from './point.js'\r\nimport { formatPrecision6, formatExtrusion, formatCoordinate } from '../util/format.js'\r\n\r\nexport class ExtrusionGeometry extends BaseModelPlus {\r\n area_model?: 'rectangle' | 'stadium' | 'circle' | 'manual'\r\n width?: number\r\n height?: number\r\n diameter?: number\r\n area?: number\r\n static readonly typeName = 'ExtrusionGeometry'\r\n constructor(init?: Partial<ExtrusionGeometry>) { super(init) }\r\n update_area() {\r\n if (this.area_model === 'rectangle' && this.width != null && this.height != null) this.area = this.width * this.height\r\n else if (this.area_model === 'stadium' && this.width != null && this.height != null)\r\n this.area = ((this.width - this.height) * this.height) + (Math.PI * (this.height / 2) ** 2)\r\n else if (this.area_model === 'circle' && this.diameter != null)\r\n this.area = Math.PI * (this.diameter / 2) ** 2\r\n }\r\n toJSON() { return { area_model: this.area_model, width: this.width, height: this.height, diameter: this.diameter, area: this.area } }\r\n static fromJSON(d: any) { return new ExtrusionGeometry(d) }\r\n gcode(state: any) {\r\n state.extrusion_geometry.updateFrom(this)\r\n if (this.width != null || this.height != null || this.diameter != null || this.area_model != null) {\r\n try { state.extrusion_geometry.update_area() } catch {}\r\n }\r\n return undefined\r\n }\r\n}\r\n\r\nexport class StationaryExtrusion extends BaseModelPlus {\r\n volume!: number\r\n speed!: number\r\n static readonly typeName = 'StationaryExtrusion'\r\n constructor(init?: Partial<StationaryExtrusion>) { super(init) }\r\n toJSON() { return { volume: this.volume, speed: this.speed } }\r\n static fromJSON(d: any) { return new StationaryExtrusion(d) }\r\n gcode(state: any) {\r\n if (state.printer) state.printer.speed_changed = true\r\n const eVal = state.extruder.get_and_update_volume(this.volume) * state.extruder.volume_to_e\r\n // Python state after primer typically has extruder.on True; ensure subsequent Z-only move is treated as printing\r\n if (state.extruder && state.extruder.on !== true) state.extruder.on = true\r\n // Python uses .6 format (6 significant figures) for StationaryExtrusion E values\r\n return `G1 F${this.speed} E${formatPrecision6(eVal)}`\r\n }\r\n}\r\n\r\nexport class Extruder extends BaseModelPlus {\r\n on?: boolean\r\n // gcode related\r\n units?: 'mm' | 'mm3'\r\n dia_feed?: number\r\n relative_gcode?: boolean\r\n volume_to_e?: number\r\n total_volume?: number\r\n total_volume_ref?: number\r\n travel_format?: 'G1_E0' | 'none'\r\n retraction_length?: number\r\n retraction_speed?: number\r\n static readonly typeName = 'Extruder'\r\n constructor(init?: Partial<Extruder>) { super(init) }\r\n update_e_ratio() {\r\n if (this.units === 'mm3') this.volume_to_e = 1\r\n else if (this.units === 'mm' && this.dia_feed != null) this.volume_to_e = 1 / (Math.PI * (this.dia_feed / 2) ** 2)\r\n }\r\n get_and_update_volume(volume: number) {\r\n if (this.total_volume == null) this.total_volume = 0\r\n if (this.total_volume_ref == null) this.total_volume_ref = 0\r\n this.total_volume += volume\r\n // Reduce floating drift to align with Python double rounding when formatted to 6 decimals\r\n this.total_volume = Math.round(this.total_volume * 1e12) / 1e12\r\n const ret = this.total_volume - this.total_volume_ref\r\n if (this.relative_gcode) this.total_volume_ref = this.total_volume\r\n return ret\r\n }\r\n toJSON() { return { on: this.on, units: this.units, dia_feed: this.dia_feed, relative_gcode: this.relative_gcode } }\r\n static fromJSON(d: any) { return new Extruder(d) }\r\n e_gcode(point1: Point, state: any) {\r\n const distance_forgiving = (p1: Point, p2: Point) => {\r\n const dx = (p1.x == null || p2.x == null) ? 0 : p1.x - p2.x\r\n const dy = (p1.y == null || p2.y == null) ? 0 : p1.y - p2.y\r\n const dz = (p1.z == null || p2.z == null) ? 0 : p1.z - p2.z\r\n return Math.sqrt(dx*dx + dy*dy + dz*dz)\r\n }\r\n if (this.on) {\r\n const length = distance_forgiving(point1, state.point)\r\n if (length === 0) return ''\r\n const area = state.extrusion_geometry?.area || 0\r\n const ratio = this.volume_to_e || 1\r\n const val = this.get_and_update_volume(length * area) * ratio\r\n return `E${formatExtrusion(val)}`\r\n } else {\r\n if (this.travel_format === 'G1_E0') {\r\n const ratio = this.volume_to_e || 1\r\n const val = this.get_and_update_volume(0) * ratio\r\n return `E${formatExtrusion(val)}`\r\n }\r\n return ''\r\n }\r\n }\r\n gcode(state: any) {\r\n state.extruder.updateFrom(this)\r\n if (this.on != null && state.printer) state.printer.speed_changed = true\r\n if (this.units != null || this.dia_feed != null) state.extruder.update_e_ratio()\r\n if (this.relative_gcode != null) {\r\n // Python emits M83/M82 whenever relative_gcode attribute is set (even if same value)\r\n state.extruder.total_volume_ref = state.extruder.total_volume\r\n return state.extruder.relative_gcode ? 'M83 ; relative extrusion' : 'M82 ; absolute extrusion\\nG92 E0 ; reset extrusion position to zero'\r\n }\r\n return undefined\r\n }\r\n}\r\n\r\nexport class Retraction extends BaseModelPlus {\r\n length?: number // override extruder default\r\n speed?: number // mm/min feedrate for retraction move\r\n static readonly typeName = 'Retraction'\r\n constructor(init?: Partial<Retraction>) { super(init) }\r\n toJSON() { return { length: this.length, speed: this.speed } }\r\n static fromJSON(d: any) { return new Retraction(d) }\r\n gcode(state: any) {\r\n // Firmware based retraction preferred if printer.command_list has 'retract'\r\n const cmdMap = state.printer?.command_list as Record<string,string> | undefined\r\n if (cmdMap && cmdMap.retract) return undefined // handled as PrinterCommand in pipeline if user inserted one\r\n // Otherwise emulate retraction as a negative stationary extrusion (relative extrusion or absolute E delta)\r\n const len = this.length ?? state.extruder.retraction_length\r\n if (len == null || len === 0) return undefined\r\n const speed = this.speed ?? state.extruder.retraction_speed ?? 1800\r\n // Mark feedrate change\r\n if (state.printer) state.printer.speed_changed = true\r\n // Convert a linear retraction length (in mm of filament) into volume -> E using volume_to_e inverse\r\n // Python uses StationaryExtrusion(volume=neg, speed) where negative volume indicates retraction\r\n // Here we bypass volume and directly compute E delta in filament units (assuming units='mm')\r\n const ratio = state.extruder.volume_to_e || 1\r\n // If units=mm3 then len is linear filament; must convert to volume: pi*(dia/2)^2 * len so that * volume_to_e yields linear again.\r\n let eDelta: number\r\n if (state.extruder.units === 'mm3') {\r\n if (state.extruder.dia_feed) {\r\n const area = Math.PI * (state.extruder.dia_feed/2)**2\r\n eDelta = len * area * ratio\r\n } else {\r\n return undefined // cannot compute\r\n }\r\n } else {\r\n eDelta = len * ratio\r\n }\r\n // Update extruder volume bookkeeping as negative extrusion\r\n state.extruder.get_and_update_volume(-(eDelta / ratio))\r\n const eVal = state.extruder.total_volume - state.extruder.total_volume_ref\r\n if (state.extruder.relative_gcode) {\r\n // relative: E value is negative delta\r\n const rel = -eDelta\r\n return `G1 F${speed} E${formatExtrusion(rel)}`\r\n } else {\r\n // absolute: current E after negative move\r\n return `G1 F${speed} E${formatExtrusion(eVal*ratio)}`\r\n }\r\n }\r\n}\r\n\r\nexport class Unretraction extends BaseModelPlus {\r\n length?: number\r\n speed?: number\r\n static readonly typeName = 'Unretraction'\r\n constructor(init?: Partial<Unretraction>) { super(init) }\r\n toJSON() { return { length: this.length, speed: this.speed } }\r\n static fromJSON(d: any) { return new Unretraction(d) }\r\n gcode(state: any) {\r\n const cmdMap = state.printer?.command_list as Record<string,string> | undefined\r\n if (cmdMap && cmdMap.unretract) return undefined\r\n const len = this.length ?? state.extruder.retraction_length\r\n if (len == null || len === 0) return undefined\r\n const speed = this.speed ?? state.extruder.retraction_speed ?? 1800\r\n if (state.printer) state.printer.speed_changed = true\r\n const ratio = state.extruder.volume_to_e || 1\r\n let eDelta: number\r\n if (state.extruder.units === 'mm3') {\r\n if (state.extruder.dia_feed) {\r\n const area = Math.PI * (state.extruder.dia_feed/2)**2\r\n eDelta = len * area * ratio\r\n } else return undefined\r\n } else {\r\n eDelta = len * ratio\r\n }\r\n // Positive extrusion\r\n state.extruder.get_and_update_volume(eDelta / ratio)\r\n const eVal = state.extruder.total_volume - state.extruder.total_volume_ref\r\n if (state.extruder.relative_gcode) {\r\n return `G1 F${speed} E${formatExtrusion(eDelta)}`\r\n } else {\r\n return `G1 F${speed} E${formatExtrusion(eVal*ratio)}`\r\n }\r\n }\r\n}\r\n","import { BaseModelPlus } from '../core/base-model.js'\r\nimport { formatFeedrate } from '../util/format.js'\r\n\r\nexport class Printer extends BaseModelPlus {\r\n print_speed?: number\r\n travel_speed?: number\r\n command_list?: Record<string, string>\r\n new_command?: Record<string, string>\r\n speed_changed?: boolean\r\n static readonly typeName = 'Printer'\r\n constructor(init?: Partial<Printer>) { super(init) }\r\n f_gcode(state: any) {\r\n if (this.speed_changed) {\r\n const speed = state.extruder?.on ? this.print_speed : this.travel_speed\r\n if (speed != null) return `F${formatFeedrate(speed)} `\r\n }\r\n return ''\r\n }\r\n gcode(state: any) {\r\n state.printer.update_from(this)\r\n if (this.print_speed != null || this.travel_speed != null) state.printer.speed_changed = true\r\n if (this.new_command) state.printer.command_list = { ...(state.printer.command_list||{}), ...this.new_command }\r\n return undefined\r\n }\r\n toJSON() { return { print_speed: this.print_speed, travel_speed: this.travel_speed } }\r\n static fromJSON(d: any) { return new Printer(d) }\r\n}\r\n","import { BaseModelPlus } from '../core/base-model.js'\r\nimport { Printer } from './printer.js'\r\n\r\nexport class Buildplate extends BaseModelPlus {\r\n static readonly typeName = 'Buildplate'\r\n temp?: number\r\n wait?: boolean\r\n constructor(init?: Partial<Buildplate>) { super(init) }\r\n gcode() {\r\n if (this.temp == null) return ''\r\n const code = this.wait ? 'M190' : 'M140'\r\n return `${code} S${this.temp}`\r\n }\r\n}\r\n\r\nexport class Hotend extends BaseModelPlus {\r\n static readonly typeName = 'Hotend'\r\n temp?: number\r\n wait?: boolean\r\n tool?: number\r\n constructor(init?: Partial<Hotend>) { super(init) }\r\n gcode() {\r\n if (this.temp == null) return ''\r\n const code = this.wait ? 'M109' : 'M104'\r\n const tool = this.tool != null ? ` T${this.tool}` : ''\r\n return `${code}${tool} S${this.temp}`\r\n }\r\n}\r\n\r\nexport class Fan extends BaseModelPlus {\r\n static readonly typeName = 'Fan'\r\n speed_percent: number = 0\r\n part_fan_index?: number // future multi-fan\r\n constructor(init?: Partial<Fan>) { super(init) }\r\n gcode() {\r\n const s = Math.round(Math.max(0, Math.min(100, this.speed_percent)) * 255 / 100)\r\n return this.speed_percent > 0 ? `M106 S${s}` : 'M107'\r\n }\r\n}\r\n","import { BaseModelPlus } from '../core/base-model.js'\r\nimport { Printer } from './printer.js'\r\n\r\nexport interface GcodeStateLike { printer: Printer; gcode: string[] }\r\n\r\nexport class PrinterCommand extends BaseModelPlus {\r\n id?: string\r\n static readonly typeName = 'PrinterCommand'\r\n constructor(init?: Partial<PrinterCommand>) { super(init) }\r\n gcode(state: GcodeStateLike) {\r\n if (this.id && state.printer.command_list) return state.printer.command_list[this.id]\r\n }\r\n}\r\n\r\nexport class ManualGcode extends BaseModelPlus {\r\n text?: string\r\n static readonly typeName = 'ManualGcode'\r\n constructor(init?: Partial<ManualGcode>) { super(init) }\r\n gcode() { return this.text }\r\n}\r\n\r\nexport class GcodeComment extends BaseModelPlus {\r\n text?: string\r\n end_of_previous_line_text?: string\r\n static readonly typeName = 'GcodeComment'\r\n constructor(init?: Partial<GcodeComment>) { super(init) }\r\n gcode(state: GcodeStateLike) {\r\n if (this.end_of_previous_line_text && state.gcode.length > 0)\r\n state.gcode[state.gcode.length - 1] += ' ; ' + this.end_of_previous_line_text\r\n if (this.text) return '; ' + this.text\r\n }\r\n}\r\n","import { BaseModelPlus } from '../core/base-model.js'\r\n\r\nexport class GcodeControls extends BaseModelPlus {\r\n printer_name?: string\r\n initialization_data?: Record<string, any>\r\n save_as?: string\r\n include_date?: boolean = true\r\n show_banner: boolean = true\r\n show_tips: boolean = true\r\n silent: boolean = false\r\n static readonly typeName = 'GcodeControls'\r\n constructor(init?: Partial<GcodeControls>) { super(init) }\r\n initialize() {\r\n if (!this.printer_name) {\r\n this.printer_name = 'generic'\r\n console.warn(\"warning: printer is not set - defaulting to 'generic'\")\r\n }\r\n }\r\n}\r\n\r\nexport class PlotControls extends BaseModelPlus {\r\n color_type: string = 'z_gradient'\r\n line_width?: number\r\n style?: 'tube' | 'line'\r\n tube_type: 'flow' | 'cylinders' = 'flow'\r\n tube_sides: number = 4\r\n zoom: number = 1\r\n hide_annotations = false\r\n hide_travel = false\r\n hide_axes = false\r\n neat_for_publishing = false\r\n raw_data = false\r\n printer_name: string = 'generic'\r\n initialization_data?: Record<string, any>\r\n static readonly typeName = 'PlotControls'\r\n constructor(init?: Partial<PlotControls>) { super(init) }\r\n initialize() {\r\n if (!this.raw_data) {\r\n if (!this.style) {\r\n this.style = 'tube'\r\n console.warn(\"warning: plot style is not set - defaulting to 'tube'\")\r\n }\r\n if (this.style === 'tube' && this.line_width != null) {\r\n console.warn('warning: line_width set but style=tube; it is ignored for extruding lines')\r\n }\r\n if (this.line_width == null) this.line_width = 2\r\n }\r\n }\r\n}\r\n","import { BaseModelPlus } from '../core/base-model.js'\r\nimport { Point } from './point.js'\r\n\r\nexport class PlotAnnotation extends BaseModelPlus {\r\n point?: Point\r\n label?: string\r\n static readonly typeName = 'PlotAnnotation'\r\n constructor(init?: Partial<PlotAnnotation>) { super(init) }\r\n visualize(state: any, plot_data: any, _plot_controls: any) {\r\n if (!this.point) this.point = new Point({ x: state.point.x, y: state.point.y, z: state.point.z })\r\n if (plot_data?.add_annotation) plot_data.add_annotation(this)\r\n }\r\n}\r\n","import { Point } from '../models/point.js'\r\nimport { Vector } from '../models/vector.js'\r\nimport { Extruder, ExtrusionGeometry, StationaryExtrusion, Retraction, Unretraction } from '../models/extrusion.js'\r\nimport { Printer } from '../models/printer.js'\r\nimport { Fan, Hotend, Buildplate } from '../models/auxiliary.js'\r\nimport { PrinterCommand, ManualGcode, GcodeComment } from '../models/commands.js'\r\nimport { GcodeControls, PlotControls } from '../models/controls.js'\r\nimport { PlotAnnotation } from '../models/annotations.js'\r\n\r\nexport function flatten<T>(steps: (T | T[])[]): T[] {\r\n return steps.flatMap(s => Array.isArray(s) ? s : [s])\r\n}\r\n\r\nexport function linspace(start: number, end: number, number_of_points: number): number[] {\r\n if (number_of_points < 2) return [start]\r\n const out: number[] = []\r\n const step = (end - start) / (number_of_points - 1)\r\n for (let i = 0; i < number_of_points; i++) out.push(start + step * i)\r\n return out\r\n}\r\n\r\nexport function points_only(steps: any[], track_xyz = true): Point[] {\r\n const pts: Point[] = steps.filter(s => s instanceof Point)\r\n if (!track_xyz) return pts\r\n for (let i = 0; i < pts.length - 1; i++) {\r\n const next = pts[i + 1].copy<Point>()\r\n const cur = pts[i]\r\n if (cur.x != null && next.x == null) next.x = cur.x\r\n if (cur.y != null && next.y == null) next.y = cur.y\r\n if (cur.z != null && next.z == null) next.z = cur.z\r\n pts[i + 1] = next\r\n }\r\n while (pts.length && (pts[0].x == null || pts[0].y == null || pts[0].z == null)) pts.shift()\r\n return pts\r\n}\r\n\r\nexport function relative_point(reference: Point | any[], x_offset: number, y_offset: number, z_offset: number): Point {\r\n let pt: Point | undefined\r\n if (reference instanceof Point) pt = reference\r\n else if (Array.isArray(reference)) {\r\n for (let i = reference.length - 1; i >= 0; i--) if (reference[i] instanceof Point) { pt = reference[i]; break }\r\n }\r\n if (!pt) throw new Error('The reference object must be a Point or list containing at least one point')\r\n if (pt.x == null || pt.y == null || pt.z == null) throw new Error(`The reference point must have all x,y,z defined (x=${pt.x}, y=${pt.y}, z=${pt.z})`)\r\n return new Point({ x: pt.x + x_offset, y: pt.y + y_offset, z: pt.z + z_offset })\r\n}\r\n\r\nexport function first_point(steps: any[], fully_defined = true): Point {\r\n for (const s of steps) if (s instanceof Point) { if (!fully_defined || (s.x != null && s.y != null && s.z != null)) return s }\r\n throw new Error(fully_defined ? 'No point found in steps with all of x y z defined' : 'No point found in steps')\r\n}\r\n\r\nexport function last_point(steps: any[], fully_defined = true): Point {\r\n for (let i = steps.length - 1; i >= 0; i--) {\r\n const s = steps[i]\r\n if (s instanceof Point) { if (!fully_defined || (s.x != null && s.y != null && s.z != null)) return s }\r\n }\r\n throw new Error(fully_defined ? 'No point found in steps with all of x y z defined' : 'No point found in steps')\r\n}\r\n\r\nexport function export_design(steps: any[], filename?: string): string {\r\n const serialized = steps.map(s => ({ type: (s.constructor as any).typeName || s.constructor.name, data: { ...s } }))\r\n const json = JSON.stringify(serialized, null, 2)\r\n if (filename) {\r\n // browser-safe: user handles download; node: write file lazily (dynamic import to avoid fs in browser bundles)\r\n if (typeof window === 'undefined') {\r\n import('node:fs').then(fs => fs.writeFileSync(filename + '.json', json))\r\n }\r\n }\r\n return json\r\n}\r\n\r\nexport function import_design(registry: Record<string, any>, jsonOrFilename: string): any[] {\r\n let jsonStr = jsonOrFilename\r\n if (jsonOrFilename.endsWith('.json')) {\r\n if (typeof window !== 'undefined') throw new Error('File system import not available in browser context')\r\n const fs = require('node:fs')\r\n jsonStr = fs.readFileSync(jsonOrFilename, 'utf-8')\r\n }\r\n const arr = JSON.parse(jsonStr)\r\n return arr.map((o: any) => {\r\n const cls = registry[o.type]\r\n if (!cls) throw new Error(`Unknown design type '${o.type}'`)\r\n return cls.fromJSON ? cls.fromJSON(o.data) : new cls(o.data)\r\n })\r\n}\r\n\r\nexport function build_default_registry(): Record<string, any> {\r\n const reg: Record<string, any> = {}\r\n const add = (cls: any) => { if (cls && (cls as any).typeName) reg[(cls as any).typeName] = cls }\r\n // Core models\r\n ;[\r\n Point, Vector,\r\n Extruder, ExtrusionGeometry, StationaryExtrusion, Retraction, Unretraction,\r\n Printer,\r\n Fan, Hotend, Buildplate,\r\n PrinterCommand, ManualGcode, GcodeComment,\r\n GcodeControls, PlotControls,\r\n PlotAnnotation\r\n ].forEach(add)\r\n return reg\r\n}\r\n","import { Point } from '../models/point.js'\r\nimport { flatten, first_point } from './extra.js'\r\n\r\nexport function check(steps: any[]) {\r\n if (!Array.isArray(steps)) { console.warn('design must be a 1D list of instances'); return }\r\n const types = new Set(steps.map(s => Array.isArray(s) ? 'list' : s?.constructor?.name))\r\n let results = ''\r\n if (types.has('list')) {\r\n results += 'warning - list contains nested lists; use flatten() to convert to 1D\\n'\r\n }\r\n results += 'step types ' + JSON.stringify([...types])\r\n console.log('check results:\\n' + results)\r\n}\r\n\r\nexport function fix(steps: any[], result_type: 'gcode' | 'plot', controls: any) {\r\n const hasNested = steps.some(s => Array.isArray(s))\r\n if (hasNested) {\r\n console.warn('warning - design includes nested lists; flattening automatically')\r\n steps = flatten(steps)\r\n }\r\n const p0 = first_point(steps, false)\r\n if (p0.x == null || p0.y == null || p0.z == null) {\r\n console.warn(`warning - first point should define x,y,z; filling missing with 0`)\r\n p0.x = p0.x ?? 0; p0.y = p0.y ?? 0; p0.z = p0.z ?? 0\r\n }\r\n if (result_type === 'plot' && controls?.color_type === 'manual' && (p0 as any).color == null) {\r\n throw new Error('for PlotControls(color_type=\\'manual\\') first point must have a color')\r\n }\r\n return steps\r\n}\r\n\r\nexport function check_points(geometry: Point | any[], checkType: string) {\r\n if (checkType === 'polar_xy') {\r\n const checkPoint = (pt: Point) => { if (pt.x == null || pt.y == null) throw new Error('polar transformations require points with x and y defined') }\r\n if (geometry instanceof Point) checkPoint(geometry)\r\n else for (const g of geometry) if (g instanceof Point) checkPoint(g)\r\n }\r\n}\r\n","import { Point } from '../models/point.js'\r\nimport { Vector } from '../models/vector.js'\r\nimport { check_points } from '../util/check.js'\r\n\r\nexport interface PolarPoint { radius: number; angle: number }\r\n\r\nexport function polar_to_point(centre: Point, radius: number, angle: number): Point {\r\n return new Point({ x: (centre.x ?? 0) + radius * Math.cos(angle), y: (centre.y ?? 0) + radius * Math.sin(angle), z: centre.z })\r\n}\r\n\r\nexport function point_to_polar(target_point: Point, origin_point: Point): PolarPoint {\r\n check_points([target_point, origin_point], 'polar_xy')\r\n const r = Math.hypot((target_point.x! - origin_point.x!), (target_point.y! - origin_point.y!))\r\n const angle = Math.atan2((target_point.y! - origin_point.y!), (target_point.x! - origin_point.x!))\r\n return { radius: r, angle: ((angle % (2*Math.PI)) + 2*Math.PI) % (2*Math.PI) }\r\n}\r\n\r\nexport function polar_to_vector(length: number, angle: number): Vector {\r\n return new Vector({ x: length * Math.cos(angle), y: length * Math.sin(angle) })\r\n}\r\n","import { Point } from '../models/point.js'\r\n\r\nexport function midpoint(p1: Point, p2: Point): Point {\r\n return new Point({ x: avg(p1.x, p2.x), y: avg(p1.y, p2.y), z: avg(p1.z, p2.z) })\r\n}\r\nfunction avg(a?: number, b?: number) { return (a!=null && b!=null) ? (a + b)/2 : undefined }\r\n\r\nexport function interpolated_point(p1: Point, p2: Point, f: number): Point {\r\n const interp = (a?: number, b?: number) => (a!=null || b!=null) ? ((a ?? b!) + f * ((b ?? a!) - (a ?? b!))) : undefined\r\n return new Point({ x: interp(p1.x, p2.x), y: interp(p1.y, p2.y), z: interp(p1.z, p2.z) })\r\n}\r\n\r\nexport function centreXY_3pt(p1: Point, p2: Point, p3: Point): Point {\r\n const D = 2 * (p1.x! * (p2.y! - p3.y!) + p2.x! * (p3.y! - p1.y!) + p3.x! * (p1.y! - p2.y!))\r\n if (D === 0) throw new Error('The points are collinear, no unique circle')\r\n const x_centre = ((p1.x!**2 + p1.y!**2) * (p2.y! - p3.y!) + (p2.x!**2 + p2.y!**2) * (p3.y! - p1.y!) + (p3.x!**2 + p3.y!**2) * (p1.y! - p2.y!)) / D\r\n const y_centre = ((p1.x!**2 + p1.y!**2) * (p3.x! - p2.x!) + (p2.x!**2 + p2.y!**2) * (p1.x! - p3.x!) + (p3.x!**2 + p3.y!**2) * (p2.x! - p1.x!)) / D\r\n return new Point({ x: x_centre, y: y_centre, z: p1.z })\r\n}\r\n","import { Point } from '../models/point.js'\r\nimport { point_to_polar } from './polar.js'\r\n\r\nexport function distance(p1: Point, p2: Point): number {\r\n return Math.sqrt(((p1.x??0)-(p2.x??0))**2 + ((p1.y??0)-(p2.y??0))**2 + ((p1.z??0)-(p2.z??0))**2)\r\n}\r\n\r\nexport function angleXY_between_3_points(start: Point, mid: Point, end: Point): number {\r\n return point_to_polar(end, mid).angle - point_to_polar(start, mid).angle\r\n}\r\n\r\nexport function path_length(points: Point[]): number { let len = 0; for (let i=0;i<points.length-1;i++) len += distance(points[i], points[i+1]); return len }\r\n","import { Point } from '../models/point.js'\r\nimport { Vector } from '../models/vector.js'\r\n\r\nexport function move(geometry: Point | any[], vector: Vector, copy=false, copy_quantity=2): Point | any[] {\r\n return copy ? copy_geometry(geometry, vector, copy_quantity) : move_geometry(geometry, vector)\r\n}\r\n\r\nexport function move_geometry(geometry: Point | any[], vector: Vector): Point | any[] {\r\n const move_point = (p: Point): Point => {\r\n const n = p.copy<Point>()\r\n if (n.x != null && vector.x != null) n.x += vector.x\r\n if (n.y != null && vector.y != null) n.y += vector.y\r\n if (n.z != null && vector.z != null) n.z += vector.z\r\n return n\r\n }\r\n if (geometry instanceof Point) return move_point(geometry)\r\n return geometry.map(e => e instanceof Point ? move_point(e) : e)\r\n}\r\n\r\nexport function copy_geometry(geometry: Point | any[], vector: Vector, quantity: number): any[] {\r\n const out: any[] = []\r\n for (let i=0;i<quantity;i++) {\r\n const v = new Vector({ x: vector.x!=null? vector.x*i: undefined, y: vector.y!=null? vector.y*i: undefined, z: vector.z!=null? vector.z*i: undefined })\r\n const g = move_geometry(geometry, v)\r\n if (g instanceof Point) out.push(g)\r\n else out.push(...g)\r\n }\r\n return out\r\n}\r\n","import { Point } from '../models/point.js'\r\nimport { point_to_polar, polar_to_point } from './polar.js'\r\nimport { check_points } from '../util/check.js'\r\n\r\nexport function move_polar(geometry: Point | any[], centre: Point, radius: number, angle: number, copy=false, copy_quantity=2): Point | any[] {\r\n check_points(geometry, 'polar_xy')\r\n return copy ? copy_geometry_polar(geometry, centre, radius, angle, copy_quantity) : move_geometry_polar(geometry, centre, radius, angle)\r\n}\r\n\r\nexport function move_geometry_polar(geometry: Point | any[], centre: Point, radius: number, angle: number): Point | any[] {\r\n const move_point = (p: Point): Point => {\r\n const pol = point_to_polar(p, centre)\r\n const np = polar_to_point(centre, pol.radius + radius, pol.angle + angle)\r\n const clone = p.copy<Point>()\r\n clone.x = np.x; clone.y = np.y\r\n return clone\r\n }\r\n if (geometry instanceof Point) return move_point(geometry)\r\n return geometry.map(e=> e instanceof Point ? move_point(e) : e)\r\n}\r\n\r\nexport function copy_geometry_polar(geometry: Point | any[], centre: Point, radius: number, angle: number, quantity: number): any[] {\r\n const out: any[] = []\r\n for (let i=0;i<quantity;i++) {\r\n const rnow = radius * i\r\n const anow = angle * i\r\n const g = move_geometry_polar(geometry, centre, rnow, anow)\r\n if (g instanceof Point) out.push(g)\r\n else out.push(...g)\r\n }\r\n return out\r\n}\r\n","import { Point } from '../models/point.js'\r\n\r\nexport function reflectXY_mc(p: Point, m_reflect: number, c_reflect: number): Point {\r\n const m_reflect_normal = -1 / m_reflect\r\n const c_reflect_normal = (p.y ?? 0) - (m_reflect_normal * (p.x ?? 0))\r\n const x = (c_reflect_normal - c_reflect) / (m_reflect - m_reflect_normal)\r\n const y = (c_reflect_normal - ((m_reflect_normal / m_reflect) * c_reflect)) / (1 - (m_reflect_normal / m_reflect))\r\n return new Point({ x: (p.x ?? 0) + 2 * (x - (p.x ?? 0)), y: (p.y ?? 0) + 2 * (y - (p.y ?? 0)), z: p.z })\r\n}\r\n\r\nexport function reflectXY(p: Point, p1: Point, p2: Point): Point {\r\n if (p2.x === p1.x) return new Point({ x: (p.x ?? 0) + 2 * (p1.x! - (p.x ?? 0)), y: p.y, z: p.z })\r\n if (p2.y === p1.y) return new Point({ x: p.x, y: (p.y ?? 0) + 2 * (p1.y! - (p.y ?? 0)), z: p.z })\r\n const m = (p2.y! - p1.y!) / (p2.x! - p1.x!)\r\n const c = p1.y! - (m * p1.x!)\r\n return reflectXY_mc(p, m, c)\r\n}\r\n","import { Point } from '../models/point.js'\r\nimport { reflectXY } from './reflect.js'\r\nimport { polar_to_point } from './polar.js'\r\n\r\nexport function reflectXYpolar(p: Point, preflect: Point, angle_reflect: number): Point {\r\n return reflectXY(p, preflect, polar_to_point(preflect, 1, angle_reflect))\r\n}\r\n","import { Point } from '../models/point.js'\r\nimport { linspace } from '../util/extra.js'\r\nimport { interpolated_point } from './midpoint.js'\r\nimport { distance } from './measure.js'\r\n\r\nexport function segmented_line(p1: Point, p2: Point, segments: number): Point[] {\r\n const xs = linspace(p1.x!, p2.x!, segments+1)\r\n const ys = linspace(p1.y!, p2.y!, segments+1)\r\n const zs = linspace(p1.z!, p2.z!, segments+1)\r\n return xs.map((_,i)=> new Point({ x: xs[i], y: ys[i], z: zs[i] }))\r\n}\r\n\r\nexport function segmented_path(points: Point[], segments: number): Point[] {\r\n const lengths = points.slice(0,-1).map((_,i)=> distance(points[i], points[i+1]))\r\n const cumulative = [0]\r\n for (const l of lengths) cumulative.push(cumulative[cumulative.length-1]+l)\r\n const seg_length = cumulative[cumulative.length-1]/segments\r\n const out: Point[] = [points[0]]\r\n let path_section_now = 1\r\n for (let s=1; s<segments; s++) {\r\n const target = seg_length*s\r\n while (target > cumulative[path_section_now]) path_section_now++\r\n const interpolation_length = target - cumulative[path_section_now-1]\r\n const fraction = interpolation_length / distance(points[path_section_now-1], points[path_section_now])\r\n out.push(interpolated_point(points[path_section_now-1], points[path_section_now], fraction))\r\n }\r\n out.push(points[points.length-1])\r\n return out\r\n}\r\n","import { Point } from '../models/point.js'\r\nimport { Vector } from '../models/vector.js'\r\nimport { linspace } from '../util/extra.js'\r\nimport { move } from './move.js'\r\nimport { move_polar } from './move_polar.js'\r\n\r\nexport function ramp_xyz(list: Point[], x_change=0, y_change=0, z_change=0): Point[] {\r\n const xs = linspace(0,x_change,list.length)\r\n const ys = linspace(0,y_change,list.length)\r\n const zs = linspace(0,z_change,list.length)\r\n for (let i=0;i<list.length;i++) list[i] = move(list[i], new Vector({ x: xs[i], y: ys[i], z: zs[i] })) as Point\r\n return list\r\n}\r\n\r\nexport function ramp_polar(list: Point[], centre: Point, radius_change=0, angle_change=0): Point[] {\r\n const rs = linspace(0,radius_change,list.length)\r\n const as = linspace(0,angle_change,list.length)\r\n for (let i=0;i<list.length;i++) list[i] = move_polar(list[i], centre, rs[i], as[i]) as Point\r\n return list\r\n}\r\n","import { Point } from '../models/point.js'\r\nimport { linspace } from '../util/extra.js'\r\nimport { polar_to_point, point_to_polar } from './polar.js'\r\nimport { ramp_xyz, ramp_polar } from './ramping.js'\r\nimport { centreXY_3pt } from './midpoint.js'\r\n\r\nexport function arcXY(centre: Point, radius: number, start_angle: number, arc_angle: number, segments=100): Point[] {\r\n return linspace(start_angle, start_angle+arc_angle, segments+1).map(a=> polar_to_point(centre, radius, a))\r\n}\r\n\r\nexport function variable_arcXY(centre: Point, start_radius: number, start_angle: number, arc_angle: number, segments=100, radius_change=0, z_change=0): Point[] {\r\n let arc = arcXY(centre, start_radius, start_angle, arc_angle, segments)\r\n arc = ramp_xyz(arc, 0, 0, z_change)\r\n return ramp_polar(arc, centre, radius_change, 0)\r\n}\r\n\r\nexport function elliptical_arcXY(centre: Point, a: number, b: number, start_angle: number, arc_angle: number, segments=100): Point[] {\r\n const t = linspace(start_angle, start_angle+arc_angle, segments+1)\r\n return t.map(tt=> new Point({ x: a*Math.cos(tt)+centre.x!, y: b*Math.sin(tt)+centre.y!, z: centre.z }))\r\n}\r\n\r\nexport function arcXY_3pt(p1: Point, p2: Point, p3: Point, segments=100): Point[] {\r\n const centre = centreXY_3pt(p1, p2, p3)\r\n const radius = Math.hypot(p1.x!-centre.x!, p1.y!-centre.y!)\r\n const start_angle = Math.atan2(p1.y!-centre.y!, p1.x!-centre.x!)\r\n const mid_angle = Math.atan2(p2.y!-centre.y!, p2.x!-centre.x!)\r\n const end_angle = Math.atan2(p3.y!-centre.y!, p3.x!-centre.x!)\r\n const twoPi = Math.PI*2\r\n const norm = (a: number) => { while(a<0) a+=twoPi; while(a>=twoPi) a-=twoPi; return a }\r\n const sa = norm(start_angle), ma = norm(mid_angle), ea = norm(end_angle)\r\n const ccw = (ma > sa && ma < ea) || (sa > ea && (ma > sa || ma < ea))\r\n const arc_angle = ccw ? (ea - sa) : -(twoPi - (ea - sa))\r\n return arcXY(centre, radius, sa, arc_angle, segments)\r\n}\r\n","import { Point } from '../models/point.js'\r\nimport { arcXY, variable_arcXY, elliptical_arcXY } from './arcs.js'\r\nimport { centreXY_3pt } from './midpoint.js'\r\n\r\nexport function rectangleXY(start: Point, x_size: number, y_size: number, cw=false): Point[] {\r\n const p1 = new Point({ x: start.x! + x_size * (cw?0:1), y: start.y! + y_size * (cw?1:0), z: start.z })\r\n const p2 = new Point({ x: start.x! + x_size, y: start.y! + y_size, z: start.z })\r\n const p3 = new Point({ x: start.x! + x_size * (cw?1:0), y: start.y! + y_size * (cw?0:1), z: start.z })\r\n return [start.copy<Point>(), p1, p2, p3, start.copy<Point>()]\r\n}\r\n\r\nexport function circleXY(centre: Point, radius: number, start_angle: number, segments=100, cw=false): Point[] {\r\n return arcXY(centre, radius, start_angle, Math.PI*2 * (1 - (2*Number(cw))), segments)\r\n}\r\n\r\nexport function circleXY_3pt(p1: Point, p2: Point, p3: Point, start_angle?: number, start_at_first_point?: boolean, segments=100, cw=false): Point[] {\r\n const centre = centreXY_3pt(p1,p2,p3)\r\n const radius = Math.hypot(p1.x!-centre.x!, p1.y!-centre.y!)\r\n if (start_angle!=null && start_at_first_point!=null) throw new Error('start_angle and start_at_first_point cannot both be set')\r\n if (start_angle==null) {\r\n if (start_at_first_point==null) throw new Error('neither start_angle nor start_at_first_point set')\r\n start_angle = Math.atan2(p1.y!-centre.y!, p1.x!-centre.x!)\r\n }\r\n return arcXY(centre, radius, start_angle, Math.PI*2 * (1 - (2*Number(cw))), segments)\r\n}\r\n\r\nexport function ellipseXY(centre: Point, a: number, b: number, start_angle: number, segments=100, cw=false): Point[] {\r\n return elliptical_arcXY(centre, a, b, start_angle, Math.PI*2 * (1 - (2*Number(cw))), segments)\r\n}\r\n\r\nexport function polygonXY(centre: Point, enclosing_radius: number, start_angle: number, sides: number, cw=false): Point[] {\r\n return arcXY(centre, enclosing_radius, start_angle, Math.PI*2 * (1 - (2*Number(cw))), sides)\r\n}\r\n\r\nexport function spiralXY(centre: Point, start_radius: number, end_radius: number, start_angle: number, n_turns: number, segments: number, cw=false): Point[] {\r\n return variable_arcXY(centre, start_radius, start_angle, n_turns*Math.PI*2 * (1 - (2*Number(cw))), segments, end_radius-start_radius, 0)\r\n}\r\n\r\nexport function helixZ(centre: Point, start_radius: number, end_radius: number, start_angle: number, n_turns: number, pitch_z: number, segments: number, cw=false): Point[] {\r\n return variable_arcXY(centre, start_radius, start_angle, n_turns*Math.PI*2 * (1 - (2*Number(cw))), segments, end_radius-start_radius, pitch_z*n_turns)\r\n}\r\n","import { Point } from '../models/point.js'\r\nimport { Vector } from '../models/vector.js'\r\nimport { polar_to_point } from './polar.js'\r\nimport { move, move_geometry } from './move.js'\r\nimport { move_polar } from './move_polar.js'\r\n\r\nexport function squarewaveXYpolar(start: Point, direction_polar: number, amplitude: number, line_spacing: number, periods: number, extra_half_period=false, extra_end_line=false): Point[] {\r\n const steps: Point[] = [start.copy<Point>()]\r\n for (let i=0;i<periods;i++) {\r\n steps.push(polar_to_point(steps[steps.length-1], amplitude, direction_polar + Math.PI/2))\r\n steps.push(polar_to_point(steps[steps.length-1], line_spacing, direction_polar))\r\n steps.push(polar_to_point(steps[steps.length-1], amplitude, direction_polar - Math.PI/2))\r\n if (i !== periods-1) steps.push(polar_to_point(steps[steps.length-1], line_spacing, direction_polar))\r\n }\r\n if (extra_half_period) {\r\n steps.push(polar_to_point(steps[steps.length-1], line_spacing, direction_polar))\r\n steps.push(polar_to_point(steps[steps.length-1], amplitude, direction_polar + Math.PI/2))\r\n }\r\n if (extra_end_line) steps.push(polar_to_point(steps[steps.length-1], line_spacing, direction_polar))\r\n return steps\r\n}\r\n\r\nexport function squarewaveXY(start: Point, direction_vector: Vector, amplitude: number, line_spacing: number, periods: number, extra_half_period=false, extra_end_line=false): Point[] {\r\n const vx = direction_vector.x ?? 0, vy = direction_vector.y ?? 0\r\n const direction_polar = Math.atan2(vy, vx)\r\n return squarewaveXYpolar(start, direction_polar, amplitude, line_spacing, periods, extra_half_period, extra_end_line)\r\n}\r\n\r\nexport function trianglewaveXYpolar(start: Point, direction_polar: number, amplitude: number, tip_separation: number, periods: number, extra_half_period=false): Point[] {\r\n const steps: Point[] = [start.copy<Point>()]\r\n for (let i=0;i<periods;i++) {\r\n let temp = polar_to_point(steps[steps.length-1], amplitude, direction_polar + Math.PI/2)\r\n steps.push(polar_to_point(temp, tip_separation/2, direction_polar))\r\n temp = polar_to_point(steps[steps.length-1], amplitude, direction_polar - Math.PI/2)\r\n steps.push(polar_to_point(temp, tip_separation/2, direction_polar))\r\n }\r\n if (extra_half_period) {\r\n const temp = polar_to_point(steps[steps.length-1], amplitude, direction_polar + Math.PI/2)\r\n steps.push(polar_to_point(temp, tip_separation/2, direction_polar))\r\n }\r\n return steps\r\n}\r\n\r\nexport function sinewaveXYpolar(start: Point, direction_polar: number, amplitude: number, period_length: number, periods: number, segments_per_period=16, extra_half_period=false, phase_shift=0): Point[] {\r\n const steps: Point[] = []\r\n const totalSegments = periods*segments_per_period + (extra_half_period? Math.floor(0.5*segments_per_period):0)\r\n for (let i=0;i<= totalSegments; i++) {\r\n const axis_distance = i * period_length / segments_per_period\r\n const amp_now = amplitude * (0.5 - 0.5 * Math.cos(((i/segments_per_period)*Math.PI*2) + phase_shift))\r\n steps.push(move(start, new Vector({ x: axis_distance, y: amp_now, z: 0 })) as Point)\r\n }\r\n return move_polar(steps, start, 0, direction_polar) as Point[]\r\n}\r\n","import { Point } from '../models/point.js'\r\nimport { Extruder } from '../models/extrusion.js'\r\nimport { first_point } from '../util/extra.js'\r\n\r\n// travel_to: returns [Extruder(off), point, Extruder(on)] matching Python behavior.\r\n// Accepts a Point or a geometry (list) from which the first Point is extracted.\r\nexport function travel_to(geometry: Point | any[]): any[] {\r\n let point: Point\r\n if (geometry instanceof Point) point = geometry\r\n else if (Array.isArray(geometry)) point = first_point(geometry)\r\n else throw new Error('travel_to expects a Point or array of steps containing at least one Point')\r\n return [new Extruder({ on: false }), point, new Extruder({ on: true })]\r\n}\r\n","import { Point } from '../../models/point.js'\r\nimport { Extruder } from '../../models/extrusion.js'\r\nimport { ManualGcode } from '../../models/commands.js'\r\n\r\n// Utility to deep copy a point (minimal for our usage)\r\nfunction clonePoint(p: Point) { return new Point({ x: p.x, y: p.y, z: p.z, extrude: (p as any).extrude }) }\r\n\r\nexport type PrimerName = 'travel' | 'front_lines_then_y' | 'front_lines_then_x' | 'front_lines_then_xy' | 'x' | 'y' | 'no_primer'\r\n\r\nexport function buildPrimer(name: PrimerName, endPoint: Point, options?: { enablePrimer?: boolean }): any[] {\r\n if (!options?.enablePrimer || name === 'no_primer') return []\r\n switch (name) {\r\n case 'travel':\r\n return [ new Extruder({ on:false }), clonePoint(endPoint), new Extruder({ on:true }) ]\r\n case 'front_lines_then_y':\r\n return frontLinesThen('y', endPoint)\r\n case 'front_lines_then_x':\r\n return frontLinesThen('x', endPoint)\r\n case 'front_lines_then_xy':\r\n return frontLinesThen('xy', endPoint)\r\n case 'x':\r\n return axisPrime('x', endPoint)\r\n case 'y':\r\n return axisPrime('y', endPoint)\r\n default:\r\n return []\r\n }\r\n}\r\n\r\nfunction addBoxSeq(endPoint: Point) {\r\n return [ new Point({ x:110 }), new Point({ y:14 }), new Point({ x:10 }), new Point({ y:16 }) ]\r\n}\r\n\r\nfunction frontLinesThen(mode: 'x'|'y'|'xy', endPoint: Point) {\r\n const seq: any[] = []\r\n seq.push(new ManualGcode({ text: ';-----\\n; START OF PRIMER PROCEDURE\\n;-----' }))\r\n seq.push(new Extruder({ on:false }))\r\n seq.push(new Point({ x:10, y:12, z:endPoint.z }))\r\n seq.push(new Extruder({ on:true }))\r\n seq.push(...addBoxSeq(endPoint))\r\n if (mode === 'y') {\r\n seq.push(new Point({ x:endPoint.x }))\r\n seq.push(new Point({ y:endPoint.y }))\r\n } else if (mode === 'x') {\r\n seq.push(new Point({ y:endPoint.y }))\r\n seq.push(new Point({ x:endPoint.x }))\r\n } else {\r\n seq.push(new Point