@salusoft89/planegcs
Version:
A wasm build and a simple wrapper of the FreeCAD PlaneGCS (2D geometric constraint solver)
552 lines • 26.7 kB
JavaScript
// This library provides WebAssembly bindings for the FreeCAD's geometric solver library planegcs.
// Copyright (C) 2023 Miroslav Šerý, Salusoft89 <miroslav.sery@salusoft89.cz>
import { constraint_param_index } from "../planegcs_dist/constraint_param_index.js";
import { SketchIndex } from "./sketch_index.js";
import { emsc_vec_to_arr } from "./emsc_vectors.js";
import { is_sketch_constraint, is_sketch_geometry } from "./sketch_primitive.js";
import { Algorithm, Constraint_Alignment } from "../planegcs_dist/enums.js";
import get_property_offset, { property_offsets } from "./geom_params.js";
export class GcsWrapper {
get debug_mode() {
return this.gcs.get_debug_mode();
}
set debug_mode(mode) {
this.gcs.set_debug_mode(mode);
}
get equal_optimization() {
return this.enable_equal_optimization;
}
set equal_optimization(val) {
this.enable_equal_optimization = val;
}
constructor(gcs) {
this.p_param_index = new Map();
this.sketch_index = new SketchIndex();
// sketch param name -> index in gcs params
this.sketch_param_index = new Map();
// nondriving constraint id -> list of its properties pushed as p-params (in order)
this.nondriving_constraint_params_order = new Map();
this.enable_equal_optimization = false;
this.gcs = gcs;
}
destroy_gcs_module() {
// only call before the deleting of this object
this.gcs.clear_data();
this.gcs.delete();
}
clear_data() {
this.nondriving_constraint_params_order.clear();
this.gcs.clear_data();
this.p_param_index.clear();
this.sketch_param_index.clear();
this.sketch_index.clear();
}
// ------ Sketch -> GCS ------- (when building up a sketch)
push_primitive(o) {
switch (o.type) {
case 'point':
this.push_point(o);
break;
case 'line':
this.push_line(o);
break;
case 'circle':
this.push_circle(o);
break;
case 'arc':
this.push_arc(o);
break;
case 'ellipse':
this.push_ellipse(o);
break;
case 'arc_of_ellipse':
this.push_arc_of_ellipse(o);
break;
case 'hyperbola':
this.push_hyperbola(o);
break;
case 'arc_of_hyperbola':
this.push_arc_of_hyperbola(o);
break;
case 'parabola':
this.push_parabola(o);
break;
case 'arc_of_parabola':
this.push_arc_of_parabola(o);
break;
default:
this.push_constraint(o);
}
if (this.sketch_index.has(o.id)) {
throw new Error(`object with id ${o.id} already exists`);
}
this.sketch_index.set_primitive(o);
}
push_primitives_and_params(objects) {
for (const o of objects) {
if (o.type === 'param') {
this.push_sketch_param(o.name, o.value);
}
else {
this.push_primitive(o);
}
}
}
solve(algorithm = Algorithm.DogLeg) {
return this.gcs.solve_system(algorithm);
}
apply_solution() {
this.gcs.apply_solution();
for (const obj of this.sketch_index.get_primitives()) {
this.pull_primitive(obj);
}
}
set_convergence_threshold(threshold) {
this.gcs.set_covergence_threshold(threshold);
}
get_convergence_threshold() {
return this.gcs.get_convergence_threshold();
}
set_max_iterations(n) {
this.gcs.set_max_iterations(n);
}
get_max_iterations() {
return this.gcs.get_max_iterations();
}
get_gcs_params() {
return emsc_vec_to_arr(this.gcs.get_p_params());
}
get_gcs_conflicting_constraints() {
return emsc_vec_to_arr(this.gcs.get_conflicting()).map((i) => this.sketch_index.get_id_by_index(i));
}
get_gcs_redundant_constraints() {
return emsc_vec_to_arr(this.gcs.get_redundant()).map((i) => this.sketch_index.get_id_by_index(i));
}
get_gcs_partially_redundant_constraints() {
return emsc_vec_to_arr(this.gcs.get_partially_redundant()).map((i) => this.sketch_index.get_id_by_index(i));
}
has_gcs_conflicting_constraints() {
return this.gcs.has_conflicting();
}
has_gcs_redundant_constraints() {
return this.gcs.has_redundant();
}
has_gcs_partially_redundant_constraints() {
return this.gcs.has_partially_redundant();
}
push_sketch_param(name, value, fixed = true) {
const pos = this.gcs.params_size();
this.gcs.push_p_param(value, fixed);
this.sketch_param_index.set(name, pos);
return pos;
}
set_sketch_param(name, value) {
const pos = this.sketch_param_index.get(name);
if (pos === undefined) {
throw new Error(`sketch param ${name} not found`);
}
this.gcs.set_p_param(pos, value, true);
}
get_sketch_param_value(name) {
const pos = this.sketch_param_index.get(name);
return pos === undefined ? undefined : this.gcs.get_p_param(pos);
}
get_sketch_param_values() {
const result = new Map();
for (const [name, pos] of this.sketch_param_index) {
result.set(name, this.gcs.get_p_param(pos));
}
return result;
}
push_p_params(id, values, fixed = false) {
const pos = this.gcs.params_size();
for (const value of values) {
this.gcs.push_p_param(value, fixed);
}
if (!this.p_param_index.has(id)) {
this.p_param_index.set(id, pos);
}
return pos;
}
push_point(p) {
if (this.p_param_index.has(p.id)) {
return;
}
this.push_p_params(p.id, [p.x, p.y], p.fixed);
}
push_line(l) {
const p1 = this.sketch_index.get_sketch_point(l.p1_id);
const p2 = this.sketch_index.get_sketch_point(l.p2_id);
this.push_point(p1);
this.push_point(p2);
}
push_circle(c) {
const p = this.sketch_index.get_sketch_point(c.c_id);
this.push_point(p);
this.push_p_params(c.id, [c.radius], false);
}
push_arc(a) {
const center = this.sketch_index.get_sketch_point(a.c_id);
this.push_point(center);
const start = this.sketch_index.get_sketch_point(a.start_id);
this.push_point(start);
const end = this.sketch_index.get_sketch_point(a.end_id);
this.push_point(end);
this.push_p_params(a.id, [a.start_angle, a.end_angle, a.radius], false);
}
push_ellipse(e) {
const center = this.sketch_index.get_sketch_point(e.c_id);
this.push_point(center);
const focus1 = this.sketch_index.get_sketch_point(e.focus1_id);
this.push_point(focus1);
this.push_p_params(e.id, [e.radmin], false);
}
push_hyperbola(h) {
const center = this.sketch_index.get_sketch_point(h.c_id);
this.push_point(center);
const focus1 = this.sketch_index.get_sketch_point(h.focus1_id);
this.push_point(focus1);
this.push_p_params(h.id, [h.radmin], false);
}
push_arc_of_hyperbola(ah) {
const center = this.sketch_index.get_sketch_point(ah.c_id);
this.push_point(center);
const focus1 = this.sketch_index.get_sketch_point(ah.focus1_id);
this.push_point(focus1);
const start = this.sketch_index.get_sketch_point(ah.start_id);
this.push_point(start);
const end = this.sketch_index.get_sketch_point(ah.end_id);
this.push_point(end);
this.push_p_params(ah.id, [ah.start_angle, ah.end_angle, ah.radmin], false);
}
push_parabola(p) {
const vertex = this.sketch_index.get_sketch_point(p.vertex_id);
this.push_point(vertex);
const focus1 = this.sketch_index.get_sketch_point(p.focus1_id);
this.push_point(focus1);
}
push_arc_of_parabola(ap) {
const vertex = this.sketch_index.get_sketch_point(ap.vertex_id);
this.push_point(vertex);
const focus1 = this.sketch_index.get_sketch_point(ap.focus1_id);
this.push_point(focus1);
const start = this.sketch_index.get_sketch_point(ap.start_id);
this.push_point(start);
const end = this.sketch_index.get_sketch_point(ap.end_id);
this.push_point(end);
this.push_p_params(ap.id, [ap.start_angle, ap.end_angle], false);
}
push_arc_of_ellipse(ae) {
const center = this.sketch_index.get_sketch_point(ae.c_id);
this.push_point(center);
const focus1 = this.sketch_index.get_sketch_point(ae.focus1_id);
this.push_point(focus1);
const start = this.sketch_index.get_sketch_point(ae.start_id);
this.push_point(start);
const end = this.sketch_index.get_sketch_point(ae.end_id);
this.push_point(end);
this.push_p_params(ae.id, [ae.start_angle, ae.end_angle, ae.radmin], false);
}
sketch_primitive_to_gcs(o) {
switch (o.type) {
case 'point': {
const p_i = this.get_primitive_addr(o.id);
return this.gcs.make_point(p_i + property_offsets.point.x, p_i + property_offsets.point.y);
}
case 'line': {
const p1_i = this.get_primitive_addr(o.p1_id);
const p2_i = this.get_primitive_addr(o.p2_id);
return this.gcs.make_line(p1_i + property_offsets.point.x, p1_i + property_offsets.point.y, p2_i + property_offsets.point.x, p2_i + property_offsets.point.y);
}
case 'circle': {
const cp_i = this.get_primitive_addr(o.c_id);
const circle_i = this.get_primitive_addr(o.id);
return this.gcs.make_circle(cp_i + property_offsets.point.x, cp_i + property_offsets.point.y, circle_i + property_offsets.circle.radius);
}
case 'arc': {
const c_i = this.get_primitive_addr(o.c_id);
const start_i = this.get_primitive_addr(o.start_id);
const end_i = this.get_primitive_addr(o.end_id);
const a_i = this.get_primitive_addr(o.id);
return this.gcs.make_arc(c_i + property_offsets.point.x, c_i + property_offsets.point.y, start_i + property_offsets.point.x, start_i + property_offsets.point.y, end_i + property_offsets.point.x, end_i + property_offsets.point.y, a_i + property_offsets.arc.start_angle, a_i + property_offsets.arc.end_angle, a_i + property_offsets.arc.radius);
}
case 'ellipse': {
const c_i = this.get_primitive_addr(o.c_id);
const focus1_i = this.get_primitive_addr(o.focus1_id);
const radmin_i = this.get_primitive_addr(o.id);
return this.gcs.make_ellipse(c_i + property_offsets.point.x, c_i + property_offsets.point.y, focus1_i + property_offsets.point.x, focus1_i + property_offsets.point.y, radmin_i + property_offsets.ellipse.radmin);
}
case 'arc_of_ellipse': {
const c_i = this.get_primitive_addr(o.c_id);
const focus1_i = this.get_primitive_addr(o.focus1_id);
const start_i = this.get_primitive_addr(o.start_id);
const end_i = this.get_primitive_addr(o.end_id);
const a_i = this.get_primitive_addr(o.id);
return this.gcs.make_arc_of_ellipse(c_i + property_offsets.point.x, c_i + property_offsets.point.y, focus1_i + property_offsets.point.x, focus1_i + property_offsets.point.y, start_i + property_offsets.point.x, start_i + property_offsets.point.y, end_i + property_offsets.point.x, end_i + property_offsets.point.y, a_i + property_offsets.arc_of_ellipse.start_angle, a_i + property_offsets.arc_of_ellipse.end_angle, a_i + property_offsets.arc_of_ellipse.radmin);
}
case 'hyperbola': {
const c_i = this.get_primitive_addr(o.c_id);
const focus1_i = this.get_primitive_addr(o.focus1_id);
const radmin_i = this.get_primitive_addr(o.id + property_offsets.hyperbola.radmin);
return this.gcs.make_hyperbola(c_i + property_offsets.point.x, c_i + property_offsets.point.y, focus1_i + property_offsets.point.x, focus1_i + property_offsets.point.y, radmin_i + property_offsets.hyperbola.radmin);
}
case 'arc_of_hyperbola': {
const c_i = this.get_primitive_addr(o.c_id);
const focus1_i = this.get_primitive_addr(o.focus1_id);
const start_i = this.get_primitive_addr(o.start_id);
const end_i = this.get_primitive_addr(o.end_id);
const a_i = this.get_primitive_addr(o.id);
return this.gcs.make_arc_of_hyperbola(c_i + property_offsets.point.x, c_i + property_offsets.point.y, focus1_i + property_offsets.point.x, focus1_i + property_offsets.point.y, start_i + property_offsets.point.x, start_i + property_offsets.point.y, end_i + property_offsets.point.x, end_i + property_offsets.point.y, a_i + property_offsets.arc_of_hyperbola.start_angle, a_i + property_offsets.arc_of_hyperbola.end_angle, a_i + property_offsets.arc_of_hyperbola.radmin);
}
case 'parabola': {
const vertex_i = this.get_primitive_addr(o.vertex_id);
const focus1_i = this.get_primitive_addr(o.focus1_id);
return this.gcs.make_parabola(vertex_i + property_offsets.point.x, vertex_i + property_offsets.point.y, focus1_i + property_offsets.point.x, focus1_i + property_offsets.point.y);
}
case 'arc_of_parabola': {
const vertex_i = this.get_primitive_addr(o.vertex_id);
const focus1_i = this.get_primitive_addr(o.focus1_id);
const start_i = this.get_primitive_addr(o.start_id);
const end_i = this.get_primitive_addr(o.end_id);
const a_i = this.get_primitive_addr(o.id);
return this.gcs.make_arc_of_parabola(vertex_i + property_offsets.point.x, vertex_i + property_offsets.point.y, focus1_i + property_offsets.point.x, focus1_i + property_offsets.point.y, start_i + property_offsets.point.x, start_i + property_offsets.point.y, end_i + property_offsets.point.x, end_i + property_offsets.point.y, a_i + property_offsets.arc_of_parabola.start_angle, a_i + property_offsets.arc_of_parabola.end_angle);
}
default:
throw new Error(`not-implemented object type: ${o.type}`);
}
}
push_constraint(c) {
var _a, _b, _c, _d;
const add_constraint_args = [];
const deletable = [];
const constraint_params = constraint_param_index[c.type];
if (constraint_params === undefined) {
throw new Error(`unknown constraint type: ${c.type}`);
}
let numeric_tag_id = -1;
if (!c.temporary) {
numeric_tag_id = this.sketch_index.counter() + 1;
}
for (const parameter of Object.keys(constraint_params)) {
const type = constraint_params[parameter];
if (type === undefined) {
throw new Error(`unknown parameter type: ${type} in constraint ${c.type}`);
}
// parameters with default values
if (parameter === 'tagId') {
add_constraint_args.push(numeric_tag_id);
continue;
}
if (parameter === 'driving') {
// add the driving? value (true by default)
add_constraint_args.push((_a = c.driving) !== null && _a !== void 0 ? _a : true);
continue;
}
if (parameter === 'internalalignment' && c.type === 'equal') {
add_constraint_args.push((_b = c.internalalignment) !== null && _b !== void 0 ? _b : Constraint_Alignment.NoInternalAlignment);
continue;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const val = c[parameter];
const is_fixed = ((_c = c.driving) !== null && _c !== void 0 ? _c : true);
if (type === 'object_param_or_number') { // or string
// properties of constraints that can be either:
// 1. number => push a new p-param
// 2. string => reference a sketch parameter
// 3. { o_id: '1', prop: 'x' } => reference a property of a sketch primitive
if (typeof val === 'number') {
if (!c.driving) {
// register a parameter of a non-driving constraint, that can be later updated
// by the pull_constraint function
const list = this.nondriving_constraint_params_order.get(c.id);
if (list === undefined) {
this.nondriving_constraint_params_order.set(c.id, [parameter]);
}
else {
list.push(parameter);
}
}
const pos = this.push_p_params(c.id, [val], is_fixed);
add_constraint_args.push(pos);
}
else if (typeof val === 'string') {
// this is a sketch param
const param_addr = this.sketch_param_index.get(val);
if (param_addr === undefined) {
throw new Error(`couldn't parse object param: ${parameter} in constraint ${c.type}: unknown param ${val}`);
}
add_constraint_args.push(param_addr);
}
else if (typeof val === 'boolean') {
add_constraint_args.push(val);
}
else if (val !== undefined) {
const ref_primitive = this.sketch_index.get_primitive_or_fail(val.o_id);
if (!is_sketch_geometry(ref_primitive)) {
throw new Error(`Primitive #${val.o_id} (${ref_primitive.type}) is not supported to be referenced from a constraint.`);
}
const param_addr = this.get_primitive_addr(val.o_id) + get_property_offset(ref_primitive.type, val.prop);
add_constraint_args.push(param_addr);
}
}
else if (type === 'object_id' && typeof val === 'string') {
const obj = this.sketch_index.get_primitive_or_fail(val);
const gcs_obj = this.sketch_primitive_to_gcs(obj);
add_constraint_args.push(gcs_obj);
deletable.push(gcs_obj);
}
else if (type === 'primitive_type' && (typeof val === 'number' || typeof val === 'boolean')) {
add_constraint_args.push(val);
}
else {
throw new Error(`unhandled parameter ${parameter} type: ${type}`);
}
}
const c_name = c.type;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.gcs[`add_constraint_${c_name}`](...add_constraint_args, (_d = c.scale) !== null && _d !== void 0 ? _d : 1);
// if something is set to be equal, then optimize this process by setting the parameter to the value directly
if (this.enable_equal_optimization && c_name === 'equal') {
const [param_1_addr, param_2_addr] = [add_constraint_args[0], add_constraint_args[1]];
if (typeof param_1_addr === 'number' && typeof param_2_addr === 'number') {
if (this.gcs.get_is_fixed(param_1_addr) && !this.gcs.get_is_fixed(param_2_addr)) {
this.gcs.set_p_param(param_2_addr, this.gcs.get_p_param(param_1_addr), false);
}
else if (this.gcs.get_is_fixed(param_2_addr) && !this.gcs.get_is_fixed(param_1_addr)) {
this.gcs.set_p_param(param_1_addr, this.gcs.get_p_param(param_2_addr), false);
}
}
}
// wasm-allocated objects must be manually deleted
for (const geom_shape of deletable) {
geom_shape.delete();
}
}
// delete_constraint_by_id(id: oid): boolean {
// if (id !== '-1') {
// const item = this.sketch_index.get_primitive(id);
// if (item !== undefined && !is_sketch_geometry(item)) {
// throw new Error(`object #${id} (${item.type}) is not a constraint (delete_constraint_by_id)`);
// }
// }
// this.gcs.clear_by_id(id);
// return this.sketch_index.delete_primitive(id);
// }
get_primitive_addr(id) {
const addr = this.p_param_index.get(id);
if (addr === undefined) {
throw new Error(`sketch object ${id} not found in p-params index`);
}
return addr;
}
// ------- GCS -> Sketch ------- (when retrieving a solution)
pull_primitive(p) {
if (this.p_param_index.has(p.id)) {
if (p.type === 'point') {
this.pull_point(p);
}
else if (p.type === 'line') {
this.pull_line(p);
}
else if (p.type === 'arc') {
this.pull_arc(p);
}
else if (p.type === 'circle') {
this.pull_circle(p);
}
else if (p.type === 'ellipse') {
this.pull_ellipse(p);
}
else if (p.type === 'arc_of_ellipse') {
this.pull_arc_of_ellipse(p);
}
else if (p.type === 'hyperbola') {
this.pull_hyperbola(p);
}
else if (p.type === 'arc_of_hyperbola') {
this.pull_arc_of_hyperbola(p);
}
else if (p.type === 'parabola') {
this.pull_parabola(p);
}
else if (p.type === 'arc_of_parabola') {
this.pull_arc_of_parabola(p);
}
else if (is_sketch_constraint(p)) {
this.pull_constraint(p);
}
else {
// console.log(`${p.type}`);
// todo: is this else branch necessary?
this.sketch_index.set_primitive(p);
}
}
else {
// console.log(`object ${o.type} #${o.id} not found in params when retrieving solution`);
this.sketch_index.set_primitive(p);
}
}
pull_point(p) {
const point_addr = this.get_primitive_addr(p.id);
const point = Object.assign(Object.assign({}, p), { x: this.gcs.get_p_param(point_addr + property_offsets.point.x), y: this.gcs.get_p_param(point_addr + property_offsets.point.y) });
this.sketch_index.set_primitive(point);
}
pull_line(l) {
this.sketch_index.set_primitive(l);
}
pull_arc(a) {
const addr = this.get_primitive_addr(a.id);
this.sketch_index.set_primitive(Object.assign(Object.assign({}, a), { start_angle: this.gcs.get_p_param(addr + property_offsets.arc.start_angle), end_angle: this.gcs.get_p_param(addr + property_offsets.arc.end_angle), radius: this.gcs.get_p_param(addr + property_offsets.arc.radius) }));
}
pull_circle(c) {
const addr = this.get_primitive_addr(c.id);
this.sketch_index.set_primitive(Object.assign(Object.assign({}, c), { radius: this.gcs.get_p_param(addr + property_offsets.circle.radius) }));
}
pull_ellipse(e) {
const addr = this.get_primitive_addr(e.id);
this.sketch_index.set_primitive(Object.assign(Object.assign({}, e), { radmin: this.gcs.get_p_param(addr + property_offsets.ellipse.radmin) }));
}
pull_arc_of_ellipse(ae) {
const addr = this.get_primitive_addr(ae.id);
this.sketch_index.set_primitive(Object.assign(Object.assign({}, ae), { start_angle: this.gcs.get_p_param(addr + property_offsets.arc_of_ellipse.start_angle), end_angle: this.gcs.get_p_param(addr + property_offsets.arc_of_ellipse.end_angle), radmin: this.gcs.get_p_param(addr + property_offsets.arc_of_ellipse.radmin) }));
}
pull_hyperbola(h) {
const addr = this.get_primitive_addr(h.id);
this.sketch_index.set_primitive(Object.assign(Object.assign({}, h), { radmin: this.gcs.get_p_param(addr + property_offsets.hyperbola.radmin) }));
}
pull_arc_of_hyperbola(ah) {
const addr = this.get_primitive_addr(ah.id);
this.sketch_index.set_primitive(Object.assign(Object.assign({}, ah), { start_angle: this.gcs.get_p_param(addr + property_offsets.arc_of_hyperbola.start_angle), end_angle: this.gcs.get_p_param(addr + property_offsets.arc_of_hyperbola.end_angle), radmin: this.gcs.get_p_param(addr + property_offsets.arc_of_hyperbola.radmin) }));
}
pull_parabola(p) {
this.sketch_index.set_primitive(p);
}
pull_arc_of_parabola(ap) {
const addr = this.get_primitive_addr(ap.id);
this.sketch_index.set_primitive(Object.assign(Object.assign({}, ap), { start_angle: this.gcs.get_p_param(addr + property_offsets.arc_of_parabola.start_angle), end_angle: this.gcs.get_p_param(addr + property_offsets.arc_of_parabola.end_angle) }));
}
pull_constraint(c) {
// We don't need to pull driving constraints
if (c.driving !== false) {
return;
}
const constraint_addr = this.get_primitive_addr(c.id);
const offsets = this.nondriving_constraint_params_order.get(c.id);
if (!offsets) {
console.warn(`No offsets for constraint type ${c.type}`);
return;
}
const constraint_copy = Object.assign({}, c);
// Helper function to update a property of any constraint type (Equal, L2L, etc.)
// preventing Typescript to complain about the type of the property (unable to assign a number to never)
function update_property(obj, key, value) {
obj[key] = value;
}
for (const [offset, constraint_property_name] of offsets.entries()) {
const param = this.gcs.get_p_param(constraint_addr + offset);
update_property(constraint_copy, constraint_property_name, param);
}
this.sketch_index.set_primitive(constraint_copy);
}
}
//# sourceMappingURL=gcs_wrapper.js.map