The Diagram DSL builds physics and chemistry figures from relations, not coordinates. Declare objects with their physical parameters, then constrain them — the engine solves placement, rotation, and scaling. Open the DSL panel in the editor (toolbar ▸ DSL) to render scenes live and insert them as a single group.
A constraint a.anchor = b.anchor moves element a so its anchor
coincides with the reference — the right-hand element stays put.
a.prop = expr assigns scalars (numbers, arithmetic on
el.anchor.x/.y); a.anchor.x = b.anchor.x aligns one axis
without contact. Line, rope and vector endpoints stretch instead of translate.
Constraints run in multiple passes — order doesn't matter — and dependents re-route.
canvas 800x500 grid 8x8 incline ramp1 x=100 y_base=380 angle=30 run=260 block m1 on_incline=ramp1 at=0.55 w=70 h=46 pulley p1 p1.handle.endpoint = ramp1.tip # strut pivots at the tip rope r1 from=m1.top to=m2.top through=p1 vector N on=m1 preset=normal len=56
canvas WxH [grid CxR]
Canvas size + optional grid for cell:/region: anchors
WxH | pixel size, e.g. 800x500 |
grid=CxR | columns x rows for canvas.cell:c:r anchors |
cell:c:r | centre of grid cell (c,r) |
cell_tl:c:r | top-left corner of grid cell |
region:name | top_left/top/top_right/left/center/right/bottom_left/bottom/bottom_right |
surface id x=.. y=.. [w=400] [h=14]
Horizontal surface / tabletop / floor with hatch below
x,y | top-left corner |
w | width |
h | thickness |
a, b | top-left and top-right of the top edge |
top/bottom/left/right | edge midpoints |
edge_left / edge_right | ends of the top edge |
t:frac | fraction along the top edge |
v:i, seg:i:t | vertex / edge fraction (polygon family) |
incline id x=.. y_base=.. angle=.. [run=260]
Ramp = three constituent lines — inclined_line, horizontal_line, vertical_line — each with full line-family anchors. Rises to the right at `angle` degrees
x,y_base | bottom-left corner |
angle | slope angle (deg) |
run | horizontal run (px) |
arc=0 | hide the automatic angle arc at the slope corner |
apex / tip | top of the slope |
base_l / base_r | left and right base vertices |
inclined_line | the slope line — sub-refs: .t:0.3 / .t1_10 / .t1:10 |
horizontal_line / base_line | the bottom line |
vertical_line | the back edge |
<line>_middle / <line>_k_n_point | edge divisions on any line |
slope:t / vertical:t / base:t | fractions along each line |
normal / slope_dir | outward normal / up-slope unit direction |
orientation | constraint: =vertical|horizontal|deg sets angle |
block id [on|on_incline|hang|x+y] [w=60] [h=45] [label=..]
Rectangular body; place on a surface, incline, pulley, rope, or free
on=el | rest on element (surface id) |
on_incline=el | rest on incline; at=0..1 along slope |
hang=el [drop=90] [side=right] | hang below a pulley via rope |
hang_pulley=el [gap=3] | directly under a pulley |
x,y | free placement (top-left) |
w,h | size |
label | centred text |
center | centre of mass |
contact | contact-face midpoint |
up_face / down_face | face centres on incline |
top/bottom/left/right[:t] | edge midpoints / fractions (rotated frame) |
tl/tr/bl/br | corners (rotated frame) |
left_edge_middle / top_edge_3_10 | edge family anchors |
width / height / *_size | scalar props |
pulley id [mount=.. of=el] [r=16] | pulley id
Circle pulley; mounted on edge/tip/block or free-standing
mount | edge-right | edge-left | tip | block | above |
of=el | element to mount on |
r | radius |
x,y | free centre (mount=above) |
gap | block-mount drop |
center | hub centre |
mount | mounting point |
tangent_h / tangent_v / tangent_v_l | named tangents |
top / bottom | rim extremes |
rim:deg | rim point at angle (0=right, +down) |
clock:N | clock position 1..12 |
Noclock / Ndeg | same, named |
arc:t / circumference:t | point at fraction of the rim |
radius / diameter | scalar props |
handle / strut / rod | mounting rod from centre — p1.handle.endpoint=ramp1.tip pivots at the tip; p1.handle.slope=210, .len=40 aim it; the pulley rides the far end |
rope id from=a.b to=c.d through=pulley1[.over],pulley2[.under]
Rope routed through tangent points around pulleys (polyline → line family)
from / to | {el.anchor} | {point:x,y} — endpoints |
through | pulley list, optional .over/.under wrap |
pulley=el | legacy single hop |
orientation=vertical | straight drop from the side tangent |
start / end | endpoints |
left_endpoint / right_endpoint | endpoints by x (connectable) |
t:frac | arc-length fraction along the whole rope |
seg:i:t | fraction along i-th straight leg |
tangent:i | tangent point on i-th pulley |
length | total straight-leg length (scalar) |
a / b / mid | line family |
line id [from=a.b] [to=c.d] [arrow=1] [sw=2] | line a, b
Straight segment; endpoints stretchable via constraints
from / to | point refs (el.anchor or x,y) |
x1,y1,x2,y2 or x,y | literal endpoints |
arrow=1 | arrowhead at the far end |
orientation | vertical | horizontal |
a / b / mid | endpoints + midpoint |
start / end | same as a/b |
left_endpoint / right_endpoint | by x — connectable |
t:frac | fraction along the line |
length | scalar |
vector id on=el [preset=..|angle=..|toward=el|along_rope=el] [len=60] [label=..]
Force/velocity arrow anchored to a body
on=el | body it attaches to (anchor=.. picks the point) |
preset | gravity | normal | friction_up | friction_down | along_up | along_down | up |
angle=deg | explicit direction (0=right,-90=up) |
toward=el | point at another element |
along_rope=el | tension direction from a rope |
from/to | explicit endpoints |
len | length |
label | tip label |
origin / tail / start | tail point |
tip / end | arrowhead tip |
mid | midpoint |
t:frac | fraction along the arrow |
left_endpoint / right_endpoint / length | line family + scalar |
label id pos=a.b|target=el text=.. [size=14]
Text annotation, free or attached to an element
pos | point ref |
target=el | centre under element bbox |
text | content |
size | font size |
center | text centre |
top/bottom/… | bbox names |
angle_arc id at=a.b from_deg=.. to_deg=.. [r=24] [label=..]
Angle marker between two ray directions at a vertex
at | vertex point ref |
from_deg / to_deg | ray directions (deg) |
r | arc radius |
label | e.g. "30°" |
center / c | vertex |
radius | scalar |
lens id cx=.. cy=.. [f=110] [u=180] [h_obj=70] [lens_h=170] [convex=1]
Closed-form thin-lens ray diagram (image position computed)
cx,cy | lens centre on axis |
f | focal length (px); convex=0 → diverging |
u | object distance |
h_obj | object height |
lens_h | lens body height |
ray_end / axis_end | right extent |
oc / pole | optical centre |
F_left / F_right / 2F_* | focal points |
obj_top / img_top | arrow tips |
top_edge / bottom_edge | lens body ends |
axis:t / lens:t / rayN:t | parametric line points |
mirror id cx=.. cy=.. [R=150] [u=240] [h_obj=80] [half_deg=40] [convex=1]
Closed-form mirror ray diagram — concave (default) or convex=1
cx,cy | pole (vertex) |
R | radius of curvature (F = R/2) |
u | object distance |
h_obj | object height |
half_deg | arc half-angle |
convex=1 | convex mirror — C/F virtual behind, diverging rays, upright image |
pole / C / F | vertex / centre of curvature / focus |
obj_top / img_top | arrow tips |
axis:t / rim:deg / rayN:t | parametric points |
prism id cx=.. base_y=.. [A=60] [mu=1.5] [i=45] [size=200]
Closed-form refraction through a triangular prism (Snell)
A | apex angle (deg) |
mu | refractive index |
i | incidence angle (deg) |
size | triangle size (px) |
cx,base_y | centre x / base line y |
apex / base_l / base_r | triangle vertices (A/B/C) |
Q / R | incidence / emergence points |
V | deviation vertex |
n1 / n2 | face normals |
AB:t / AC:t / BC:t / incident / inside / emergent | parametric |
mol id mol=FORMULA [pos=..] [r=46] [style=ballandstick|stick|spacefill]
Molecule placeholder — sphere glyph in the SVG; the real 3D structure shows in the 3D viewer (3D toggle in the DSL panel). FORMULA must be in the built molecule library (CH4, H2O, NaCl, H2SO4, KMnO4…)
mol / formula | molecule key from the library |
pos / center | placement |
r | placeholder radius |
style | 3Dmol style: ballandstick | stick | spacefill |
center / top / bottom / left / right | sphere anchors — connectable |
rim:deg | rim point at angle |
radius | scalar |
lewis id mol=FORMULA [pos=..] [size=90]
Lewis structure — 2D dot-and-bond diagram (lone pairs, bond order, formal charges) rendered from the molecule library. For bonding pedagogy; use mol= for the 3D view
mol / formula | molecule key from the library |
pos / center | placement |
size | half-extent (px) |
center / top / bottom / left / right | bbox anchors |
spring id [from=a.b] [to=c.d] [coils=7] [amp=10] [length=..]
Coil spring between two points — full line-family element
from / to | point refs — ends stretch via constraints |
coils | number of coil turns |
amp | half-width of the coil (px) |
length | rest length on the free end |
orientation | vertical | horizontal |
sw | stroke width |
a / b / mid | endpoints + midpoint |
t:frac | fraction along the axis |
left_endpoint / right_endpoint / length | line family + scalar |
wave id x=.. y=.. [w=320] [amp=30] [cycles=2]
Sine wave polyline with phase-aware anchors
x,y | left endpoint on axis |
w | width |
amp | amplitude |
cycles | periods |
start / end | first / last point |
crest:i / trough:i / node:i | i-th crest, trough, crossing |
phase:deg | point at phase angle |
t:frac | fraction along x extent |
Idioms for positioning — pins, alignment, gaps, rotations, stretching:
a.anchor = b.anchor # moves a so its anchor lands on b's anchor # the rhs never moves — pin the element that should TRAVEL m1.top = s.top # block m1 lands its top edge on surface s # to move b instead, swap sides: b.top = a.top
# two pins on the SAME edge family rigid-seat the element: m1.bottom_edge_1_10 = ramp1.inclined_line_3_10 m1.bottom_edge_8_10 = ramp1.inclined_line_4_10 # the solver picks rotation <=90deg — body stays outside the ramp # prefer on_incline= when available; 2 pins = manual seat
m2.center.x = m1.center.x # same x — stacks vertically m2.top.y = m1.bottom.y # level the edges — no contact p1.center.x = ceiling.bottom.x # centre under a point
m1.right+20 = m2.left # 20px gap — offset follows the # anchor direction: right->+x, left->-x, top->-y, bottom->+y m1.top+15 = s.top # 15px below the surface line
label l pos=m1.top+(0,-10) text="m1" # 10px above the anchor # works in from/to/pins: v1.origin = m1.center+(30,-40)
# line/rope/vector endpoints move instead of translating: l.left = m1.center # left end sticks to the block centre l.right = p1.9oclock # right end on the pulley rim frame.slope = 30 # direction pinned; endpoint sets extent frame.length = 200 # free end sized
m1.rot = 15 # rotate about own centre (deg) ramp2.orientation = 40 # incline angle via constraint l.slope = -30 # line direction, clockwise-positive l.orientation = vertical # snap to axis
incline r2 angle=40 mirror=h # flips the SHAPE; anchors follow # mirror changes geometry only — normals/angles/signs stay consistent
p1.radius = m1.width / 2 # reads assigned value — order matters: m1.width = 60 # assign plain numbers FIRST m2.center.x = ramp1.apex.x - m2.width/2 - 10 # coords inside arithmetic # stable props only: width/height/radius/angle/slope/length
rope r1 from=m1.top to=m2.top through=p1 # routes over the rim — never pin both ends to rim points
p1.handle.endpoint = ramp1.tip # rod pivots at the tip p1.handle.slope = 270 # direction (270 = straight up) p1.handle.len = 25 # rod length; pulley rides the far end # also strut/rod/arm/stem/leg/mount; X.<part> is a line element
vector N on=m1 preset=normal len=56 # normal on incline vector W on=m1 preset=gravity len=70 # straight down vector Fr on=m1 preset=friction_up len=44 # along slope up vector T on=m2 along_rope=r1 len=64 # tension from a rope vector F on=m1 anchor=m1.left preset=up # anchor picks the origin
m1.center = canvas.cell:2:2 # grid cell centre (0-indexed) label t pos=canvas.region:bottom text="FBD" # named canvas regions
surface ceiling x=150 y=50 w=200 h=16 pulley p1 mount=strut of=ceiling anchor=ceiling.bottom angle=90 len=70 # rod from ceiling.bottom straight down 70px, pulley at its end
rope r1 from=m1.top to=m2.top through=p1 m1.center.x = p1.9oclock.x # hang left mass off the left tangent m2.center.x = p1.3oclock.x # right mass off the right tangent # through=p1 routes the rope over the top — no manual endpoint wiring
block m1 on_incline=ramp1 at=0.55 w=70 h=46 vector N on=m1 preset=normal len=56 vector W on=m1 preset=gravity len=70 vector Fr on=m1 preset=friction_up len=44
pulley p1 p1.handle.endpoint = ramp1.tip # strut pivots at the tip p1.handle.slope = 210 # rod direction (deg, clockwise) p1.handle.len = 40 # the pulley rides the rod's far end
incline ramp1 x=100 y_base=380 angle=30 run=240 incline ramp2 x=360 y_base=380 angle=40 run=200 mirror=h ramp1.tip = ramp2.tip # shared apex — run AUTOSCALES so the base # stays grounded; pin the unmirrored ramp's tip, never the reverse pulley p1 p1.handle.endpoint = ramp1.tip p1.handle.slope = 270 # straight up block m1 on_incline=ramp1 at=0.5 block m2 on_incline=ramp2 at=0.5 rope r1 from=m1.top to=m2.top through=p1