Skip to content

Instantly share code, notes, and snippets.

@Andrej730
Last active September 7, 2023 09:18
Show Gist options
  • Save Andrej730/5b99ed5dfcb69734bb53005c71f18813 to your computer and use it in GitHub Desktop.
Save Andrej730/5b99ed5dfcb69734bb53005c71f18813 to your computer and use it in GitHub Desktop.
Solvespace Python example and notes

Reference

Notes on py_slvs

  • Methods creating an entity, param, or constraint usually return its handle index (an integer starting from 1).
    • You can get an actual object using .getEntity, .getConstraint, or .getParam methods.
  • solvesys.EntityHandle, solvesys.GroupHandle, solvesys.ParamHandle return the number of handles for the type (or the last handle index).
  • solvesys.Dof refers to the degrees of freedom, which are updated after .solve.
  • Get entity type: solvesys.getEntity(5).type returns a type index that is interpreted using mapping.
  • Each entity/param/constraint has a group id in entity.group.
    • Groups are important as they allow the solver to tweak only a specific group and keep entities of other groups intact.
    • Object id is stored in entity.h.

Additional Notes

  • The "V" suffix in methods like addDistanceV, addNormal3dV etc. implies that you can provide just a float value instead of creating a param for the float value first (params will be created automatically).
  • Changing .co doesn't trigger a solve in CAD Sketcher. On every change, it creates the Solver again; it doesn't keep it running.
  • Constrained points/lines can be accessed from constraint parameters.
  • Entity parameters can only be accessed by solvesys.getEntityParam.
  • There's no way to get entities related to params besides iterating over all entities.
  • There's no way to add a reference constraint like in CAD Sketcher/FreeCAD that doesn't constrain anything. Instead, it's just there to calculate the value. So, those values should be calculated manually.
  • To fix some point, you just need to use a group that's not going to be used in the .solve. In CAD Sketcher, they use group = 1.
  • In CAD Sketcher, to account for the current mouse drag direction during solving, they add a constraint that pins the dragged entity to the line defined by the current mouse movement. This is a clever approach.
    • Examples can be found in each entity's .tweak() method: point_2d.py on GitHub.
    • Getting the tweaking position: tweak.py on GitHub.
    • There's also an option to add a tweak point using the addWhereDragged constraint: solver.py on GitHub. However, the CAD Sketcher developer mentions that this tends to overconstrain the system. Therefore, they use the addPointOnLine constraint for the tweaked point with a very small line made of the tweak point plus a vector perpendicular to the tweak vector.
  • If some constraints fail during the solving process, you can find them in solvesys.Failed. Sometimes, you might need to adjust lines a bit before solving (e.g., SLVS_C_ANGLE will fail on parallel lines).
  • It's possible to create 3d constraint, just need to make sure to either avoid wrklpln argument in the constraint or set it to 0. If you try to constraint a 3d line but you still provide wrklpln argument then "the constraint applies on the projection of the geometry into that wrklpln". You can find constraints supporting 3D in solver system methods that have default value = 0 for wrkpln argument.
from py_slvs import slvs
from mathutils import Vector
# https://gist.github.com/Andrej730/5b99ed5dfcb69734bb53005c71f18813
# mapping can be found here:
# https://github.com/realthunder/solvespace/blob/python/include/slvs.h
type_id_mapping = {
50000: 'SLVS_E_POINT_IN_3D',
50001: 'SLVS_E_POINT_IN_2D',
60000: 'SLVS_E_NORMAL_IN_3D',
60001: 'SLVS_E_NORMAL_IN_2D',
70000: 'SLVS_E_DISTANCE',
80000: 'SLVS_E_WORKPLANE',
80001: 'SLVS_E_LINE_SEGMENT',
80002: 'SLVS_E_CUBIC',
80003: 'SLVS_E_CIRCLE',
80004: 'SLVS_E_ARC_OF_CIRCLE',
90000: 'SLVS_E_TRANSFORM',
100000: 'SLVS_C_POINTS_COINCIDENT',
100001: 'SLVS_C_PT_PT_DISTANCE',
100002: 'SLVS_C_PT_PLANE_DISTANCE',
100003: 'SLVS_C_PT_LINE_DISTANCE',
100004: 'SLVS_C_PT_FACE_DISTANCE',
100005: 'SLVS_C_PT_IN_PLANE',
100006: 'SLVS_C_PT_ON_LINE',
100007: 'SLVS_C_PT_ON_FACE',
100008: 'SLVS_C_EQUAL_LENGTH_LINES',
100009: 'SLVS_C_LENGTH_RATIO',
100010: 'SLVS_C_EQ_LEN_PT_LINE_D',
100011: 'SLVS_C_EQ_PT_LN_DISTANCES',
100012: 'SLVS_C_EQUAL_ANGLE',
100013: 'SLVS_C_EQUAL_LINE_ARC_LEN',
100014: 'SLVS_C_SYMMETRIC',
100015: 'SLVS_C_SYMMETRIC_HORIZ',
100016: 'SLVS_C_SYMMETRIC_VERT',
100017: 'SLVS_C_SYMMETRIC_LINE',
100018: 'SLVS_C_AT_MIDPOINT',
100019: 'SLVS_C_HORIZONTAL',
100020: 'SLVS_C_VERTICAL',
100021: 'SLVS_C_DIAMETER',
100022: 'SLVS_C_PT_ON_CIRCLE',
100023: 'SLVS_C_SAME_ORIENTATION',
100024: 'SLVS_C_ANGLE',
100025: 'SLVS_C_PARALLEL',
100026: 'SLVS_C_PERPENDICULAR',
100027: 'SLVS_C_ARC_LINE_TANGENT',
100028: 'SLVS_C_CUBIC_LINE_TANGENT',
100029: 'SLVS_C_EQUAL_RADIUS',
100030: 'SLVS_C_PROJ_PT_DISTANCE',
100031: 'SLVS_C_WHERE_DRAGGED',
100032: 'SLVS_C_CURVE_CURVE_TANGENT',
100033: 'SLVS_C_LENGTH_DIFFERENCE'
}
solve_status_mapping = {
0: "Successfully solved sketch.",
1: "Cannot solve sketch because of inconsistent constraints, check through the failed constraints and remove the ones that contradict each other.",
2: "Cannot solve sketch, system didn't converge.",
3: "Cannot solve sketch because of too many unknowns.",
4: "Solver failed to initialize.",
5: "Some constraints seem to be redundant, this might cause an error once the constraints are no longer consistent. Check through the marked constraints and only keep what's necessary.",
6: "Cannot solve sketch because of unknown failure."
}
V = lambda *x: Vector([float(i) for i in x])
solvesys = slvs.System()
def get_entity_params(entity):
if isinstance(entity, int):
entity = solvesys.getEntity(entity)
params = []
entity_id = entity.h
param_i = 0
while True:
param_id = solvesys.getEntityParam(entity_id, param_i)
if param_id == 0:
break
params.append(param_id)
param_i += 1
return params
def add_point(pos, workplane=None, fixed=False):
is_2d = bool(workplane)
pos = pos[:2] if is_2d else pos
point_group = group if not fixed else FIXED_GROUP
# `solvesys.addParamV` returns index of registered parameter
# value can be accessed later with `solvesys.getParam(index)`
params = [solvesys.addParamV(i, group=point_group) for i in pos]
# `solvesys.addPoint3d` also returns entity index
# you can get it later with `solvesys.getEntity(index)`
if is_2d:
p = solvesys.addPoint2d(workplane, *params, group=point_group)
else:
p = solvesys.addPoint3d(*params, group=point_group)
return p
def quat_from_vector(vector):
# similarly you can get vector (normal) from quaternion
# Vector((0,0,1)).rotate(quat)
return Vector((0,0,1)).rotation_difference(vector)
def get_param_values(params):
return [solvesys.getParam(p).val for p in params]
def point_coords(point_entity):
return Vector(get_param_values(get_entity_params(point_entity)))
def aligned_distance(e1, e2, value, alignment="VERTICAL"):
"""order of points is not important"""
# in solve space there is no way to add aligned distance from the box
# therefore we do it in 3 constraints
#
# first point represents base for Y / vertical constraint
# second point - for X / horizontal constraint
# but order doesn't matter
p1, p2 = point_coords(e1), point_coords(e2)
between = add_point((p2.x, p1.y), workplane)
solvesys.addPointsHorizontal(between, e2, workplane, group=group) # same x
solvesys.addPointsVertical(between, e1, workplane, group=group) # same y
constrained_point = e1 if alignment == "VERTICAL" else e2
solvesys.addPointsDistance(value, between, constrained_point, wrkpln=workplane, group=group)
# BASIC SETUP, note that base points, normals and workplane must be fixed (assigned to the FIXED_GROUP)
# otherwise solver might change them trying to sovle the sketch
FIXED_GROUP = 1 # index 1 is reserved to keep some entities intact
group = 3
base_point_3d = add_point((0,0,0), fixed=True)
base_normal = V(0,0,1)
base_normal = solvesys.addNormal3dV(*quat_from_vector(base_normal), group=FIXED_GROUP)
workplane = solvesys.addWorkplane(base_point_3d, base_normal, group=FIXED_GROUP)
# there is no base point by default, we need to create it
base_point = add_point(V(0,0), workplane, fixed=True)
def case_1_2d():
# horizontal constraint example
edge = ((0,0,8), (5,0,24))
edge = [e[:2] for e in edge]
edge = [add_point(pos, workplane) for pos in edge]
line = solvesys.addLineSegment(*edge, group=group)
# set new param values, line is not parallel anymore
solvesys.getParam(get_entity_params(edge[0])[1]).val = 5.0
solvesys.getParam(get_entity_params(edge[1])[1]).val = 3.0
# [Vector((0.0, 5.0)), Vector((5.0, 3.0))]
print([point_coords(e) for e in edge])
solvesys.addLineHorizontal(line, wrkpln=workplane, group=group)
retvalue = solvesys.solve(group=group, reportFailed=True, findFreeParams=False)
print(solve_status_mapping[retvalue])
# both y equals 3 now, case solved
# [Vector((0.0, 3.0)), Vector((5.0, 3.0))]
print([point_coords(e) for e in edge])
def case_1_3d():
# horizontal constraint example
edge = ((0,0,8), (5,0,24))
edge = [add_point(pos) for pos in edge]
line = solvesys.addLineSegment(*edge, group=group)
# set new param values, line is not parallel anymore
solvesys.getParam(get_entity_params(edge[0])[1]).val = 5.0
solvesys.getParam(get_entity_params(edge[1])[1]).val = 3.0
# in 3d constraints we need to specify x axis
x_point = add_point((1,0,0), fixed=True)
x_axis = solvesys.addLineSegment(base_point_3d, x_point, group=FIXED_GROUP)
solvesys.addLineHorizontal(line, wrkpln=workplane, group=group)
# solvesys.addParallel(line, x_axis, group=group)
# [Vector((0.0, 5.0)), Vector((5.0, 3.0))]
print([point_coords(e) for e in edge])
retvalue = solvesys.solve(group=group, reportFailed=True, findFreeParams=False)
print(solve_status_mapping[retvalue])
# both y equals 3 now, case solved
# [Vector((0.0, 3.0)), Vector((5.0, 3.0))]
print([point_coords(e) for e in edge])
print(point_coords(x_point))
def case_2_2d():
# distance constraint example
coords = (5,0)
point = add_point(coords, workplane)
print(point_coords(base_point_3d))
print(point_coords(point))
solvesys.addPointsDistance(10, base_point_3d, point, wrkpln=workplane, group=group)
retvalue = solvesys.solve(group=group, reportFailed=True, findFreeParams=False)
print(solve_status_mapping[retvalue])
print(point_coords(base_point_3d))
print(point_coords(point))
def case_2_3d():
# distance constraint example
use_2d = False
coords = (5,0,5)
point = add_point(coords)
print(point_coords(base_point_3d))
print(point_coords(point))
solvesys.addPointsDistance(10, base_point_3d, point, group=group)
retvalue = solvesys.solve(group=group, reportFailed=True, findFreeParams=False)
print(solve_status_mapping[retvalue])
print(point_coords(base_point_3d))
print(point_coords(point))
def case_3():
# reproduced in CAD Sketcher - https://i.imgur.com/QmAqDjO.png
dims = (10, 5)
x_offset = 5
offset = V(0,10) # y offset to avoid failing constraints
profile_1 = [V(dims[0], 0), V(-dims[0], 0)]
profile_2 = [V(dims[1], 0), V(-dims[1], 0)]
profile_2 = [p + offset for p in profile_2]
profile_1 = [add_point(p, workplane) for p in profile_1]
profile_2 = [add_point(p, workplane) for p in profile_2]
base_edge = solvesys.addLineSegment(*profile_1, group=group)
side_edges = [
solvesys.addLineSegment(profile_1[0], profile_2[0], group=group),
solvesys.addLineSegment(profile_1[1], profile_2[1], group=group),
]
theta = 30 # in degrees
supplementary_angle = False
# CONSTRAINTS
# profile 1
solvesys.addPointsDistance(dims[0]*2, *profile_1, wrkpln=workplane, group=group)
solvesys.addPointsHorizontal(*profile_1, group=group, wrkpln=workplane)
solvesys.addMidPoint(base_point, base_edge, group=group, wrkpln=workplane)
# profile 2
solvesys.addPointsDistance(dims[1]*2, *profile_2, wrkpln=workplane, group=group)
solvesys.addPointsHorizontal(*profile_2, group=group, wrkpln=workplane)
aligned_distance(profile_2[1], base_point, x_offset+dims[1], "HORIZONTAL")
solvesys.addAngle(theta, supplementary_angle, *side_edges, group=group)
print([get_param_values(get_entity_params(p)) for p in profile_1])
print([get_param_values(get_entity_params(p)) for p in profile_2])
retvalue = solvesys.solve(group=group, reportFailed=True, findFreeParams=False)
print(solve_status_mapping[retvalue])
print([get_param_values(get_entity_params(p)) for p in profile_1])
print([get_param_values(get_entity_params(p)) for p in profile_2])
case_1_3d()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment