{"id":50395,"date":"2026-10-09T14:15:29","date_gmt":"2026-10-09T14:15:29","guid":{"rendered":"https:\/\/hollandmechanics.com\/?page_id=50395"},"modified":"2026-10-09T15:03:31","modified_gmt":"2026-10-09T15:03:31","slug":"wheel-lacing-calculator","status":"publish","type":"page","link":"https:\/\/hollandmechanics.com\/en\/wheel-lacing-calculator\/","title":{"rendered":"Wheel Lacing Calculator"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"50395\" class=\"elementor elementor-50395\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-c4245f7 e-flex e-con-boxed e-con e-parent\" data-id=\"c4245f7\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;background_background&quot;:&quot;classic&quot;,&quot;background_motion_fx_motion_fx_scrolling&quot;:&quot;yes&quot;,&quot;background_motion_fx_translateY_effect&quot;:&quot;yes&quot;,&quot;background_motion_fx_translateY_speed&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:0.8,&quot;sizes&quot;:[]},&quot;background_motion_fx_translateY_affectedRange&quot;:{&quot;unit&quot;:&quot;%&quot;,&quot;size&quot;:&quot;&quot;,&quot;sizes&quot;:{&quot;start&quot;:0,&quot;end&quot;:100}},&quot;background_motion_fx_devices&quot;:[&quot;widescreen&quot;,&quot;desktop&quot;,&quot;laptop&quot;,&quot;tablet_extra&quot;,&quot;tablet&quot;,&quot;mobile_extra&quot;,&quot;mobile&quot;]}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-6604eca e-con-full e-flex e-con e-child\" data-id=\"6604eca\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-8f3d620 elementor-widget elementor-widget-spacer\" data-id=\"8f3d620\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-ff9f79f elementor-widget__width-inherit elementor-widget-laptop__width-initial elementor-widget elementor-widget-heading\" data-id=\"ff9f79f\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t\t<h1 class=\"elementor-heading-title elementor-size-default\">Wheel lacing Calculator<\/h1>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-d3f3ce5 e-flex e-con-boxed e-con e-parent\" data-id=\"d3f3ce5\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-d610ccc e-con-full e-flex e-con e-child\" data-id=\"d610ccc\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t<div class=\"elementor-element elementor-element-fca3a7d e-flex e-con-boxed e-con e-child\" data-id=\"fca3a7d\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-51bd97c elementor-widget__width-inherit elementor-widget elementor-widget-html\" data-id=\"51bd97c\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Lacing Screwdriver Explainer<\/title>\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;700&family=Montserrat:wght@700;800&display=swap\" rel=\"stylesheet\">\n<style>\n  \/* Holland Mechanics house tokens *\/\n  :root{\n    --hm-blue:#005389; --label-blue:#1F5F7C; --sub-blue:#317A9E; --accent:#B3E9FF; --red:#D63031;\n    --tint:#F3F8FD; --border:#DFE3E7; --hair:#E6EEF3; --body:#53585B; --muted:#787979; --white:#FFFFFF;\n    --ok:#119915; --warn:#C26A00;\n    --ink-2:#53585B; --line:#DFE3E7;\n  }\n  *{box-sizing:border-box}\n  html,body{margin:0;background:var(--white);color:var(--body);font:400 15px\/1.5 Inter, Arial, sans-serif}\n  .lx{max-width:1180px;margin:0 auto;padding:26px 16px}\n  .lx-head{margin-bottom:26px}\n  .mark{display:flex;align-items:center;gap:10px;margin-bottom:14px}\n  .mark svg{width:26px;height:26px}\n  .mark i{display:block;width:44px;height:2px;background:var(--red)}\n  .eyebrow{margin:0 0 6px;font:700 12px\/1.3 Inter, Arial, sans-serif;letter-spacing:.12em;text-transform:uppercase;color:var(--label-blue)}\n  h2{margin:0 0 14px;max-width:640px;font:800 clamp(22px,3vw,32px)\/1.15 Montserrat, Arial, sans-serif;text-transform:uppercase;color:var(--hm-blue)}\n  .lead{margin:0;max-width:720px;font-size:16px}\n  .rule{height:2px;background:var(--hair);margin:26px 0 0}\n\n  .controls{display:flex;flex-wrap:wrap;gap:14px 26px;align-items:flex-end;padding:14px;border:2px solid var(--border);border-radius:8px;background:var(--tint)}\n  .ctl{display:flex;flex-direction:column;gap:6px}\n  .ctl>span{font:700 11px\/1 Inter, Arial, sans-serif;letter-spacing:.1em;text-transform:uppercase;color:var(--label-blue)}\n  .seg{display:inline-flex;border:2px solid var(--border);border-radius:8px;background:var(--white);overflow:hidden}\n  .seg button{appearance:none;border:0;background:transparent;padding:8px 14px;font:700 13px\/1.2 Inter, Arial, sans-serif;color:var(--hm-blue);cursor:pointer}\n  .seg button+button{border-left:2px solid var(--border)}\n  .seg button[aria-pressed=\"true\"]{background:var(--hm-blue);color:var(--white)}\n  .seg button:focus-visible,.play:focus-visible{outline:3px solid var(--accent);outline-offset:-3px}\n  .ctl.range input{width:170px;margin:6px 0;accent-color:var(--hm-blue)}\n  .ctl output{margin-left:8px;color:var(--hm-blue);letter-spacing:0;text-transform:none;font-size:13px}\n  .transport{margin-left:auto;flex-direction:row;align-items:center;gap:8px}\n  .play{appearance:none;border:2px solid var(--hm-blue);border-radius:8px;background:var(--hm-blue);color:var(--white);padding:8px 16px;font:700 13px\/1.2 Inter, Arial, sans-serif;cursor:pointer;min-width:84px}\n  .play.ghost{background:var(--white);color:var(--hm-blue);min-width:0}\n\n  .stage{margin-top:14px;border:2px solid var(--border);border-radius:8px;background:#EEF2F8;padding:8px}\n  .view{position:relative}\n  .view+.view{margin-top:6px;padding-top:6px;border-top:2px solid var(--white)}\n  .view-label{position:absolute;right:10px;top:8px;font:700 11px\/1.2 Inter, Arial, sans-serif;letter-spacing:.1em;text-transform:uppercase;color:var(--label-blue)}\n  .view svg{display:block;width:100%;height:auto}\n  .drill{display:flex;gap:0}\n  .drill svg{min-width:0}\n  #drillA{flex:325 1 0} #drillB{flex:490 1 0}\n  .specs{display:flex;flex-wrap:wrap;gap:6px 8px;margin:4px 6px 6px}\n  .spec{background:var(--white);border:2px solid var(--border);border-radius:8px;padding:4px 10px;font-size:13px;color:var(--body)}\n  .spec b{color:var(--label-blue);font-weight:700;margin-right:6px}\n\n  .info{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-top:14px}\n  .card{border:2px solid var(--border);border-radius:8px;padding:14px;background:var(--white)}\n  .card b{display:block;font:700 11px\/1.3 Inter, Arial, sans-serif;letter-spacing:.1em;text-transform:uppercase;color:var(--label-blue);margin-bottom:6px}\n  .card .v{font:800 18px\/1.25 Montserrat, Arial, sans-serif;color:var(--hm-blue)}\n  .card .v.ok{color:var(--ok)} .card .v.warn{color:var(--warn)} .card .v.bad{color:var(--red)}\n  .card p{margin:6px 0 0;font-size:14px}\n  .explain{margin:14px 0 0;font-size:16px;max-width:900px}\n  .status{margin-top:14px;font-size:14px;color:var(--muted)}\n  .status strong{color:var(--hm-blue)}\n  .cmp-title{margin:26px 0 10px;font:800 18px\/1.2 Montserrat, Arial, sans-serif;text-transform:uppercase;color:var(--hm-blue)}\n  .tbl-wrap{overflow-x:auto;border:2px solid var(--border);border-radius:8px}\n  .cmp{width:100%;border-collapse:collapse;font-size:14px;min-width:760px}\n  .cmp th{text-align:left;padding:10px 12px;font:700 11px\/1.3 Inter, Arial, sans-serif;letter-spacing:.08em;text-transform:uppercase;color:var(--label-blue);background:var(--tint);border-bottom:2px solid var(--border)}\n  .cmp td{padding:10px 12px;border-top:1px solid var(--hair);vertical-align:top}\n  .cmp tr.sel td{background:var(--tint)} .cmp tr.sel td:first-child{box-shadow:inset 4px 0 0 var(--hm-blue)}\n  .cmp tbody tr{cursor:pointer} .cmp tbody tr:hover td{background:#F8FBFE}\n  .cmp td b{display:block;color:var(--hm-blue);font-weight:700}\n  .cmp small{display:block;color:var(--muted);font-size:12px}\n  .st{display:inline-flex;align-items:center;gap:6px;font-weight:700}\n  .st i{width:9px;height:9px;border-radius:50%;flex:none}\n  .st.ok{color:var(--ok)} .st.ok i{background:var(--ok)} .st.opt{color:var(--warn)} .st.opt i{background:var(--warn)} .st.no{color:var(--red)} .st.no i{background:var(--red)}\n  .note{margin:8px 0 0;font-size:12px;color:var(--muted)}\n  .warn-line{color:var(--red);font-weight:700}\n  .legend{display:flex;flex-wrap:wrap;gap:6px 18px;margin-top:14px;font-size:13px;color:var(--muted)}\n  .legend span{display:inline-flex;align-items:center;gap:6px}\n  .legend i{display:inline-block;width:16px;height:4px;border-radius:2px}\n  @media (max-width:760px){ .info{grid-template-columns:1fr} .transport{margin-left:0} .seg button{padding:8px 10px} .view-label{position:static;display:block;margin:2px 4px 6px} .drill{flex-direction:column} #drillA,#drillB{flex:none;width:100%} }\n  body.bare .lx-head{display:none}\n<\/style>\n<\/head>\n<body>\n<svg id=\"defs\" width=\"0\" height=\"0\" style=\"position:absolute\" aria-hidden=\"true\"><\/svg>\n<section class=\"lx\" aria-label=\"Lacing machine: straight or angled screwdriver\">\n  <header class=\"lx-head\">\n    <div class=\"mark\"><svg viewBox=\"0 0 26 26\" aria-hidden=\"true\"><circle cx=\"13\" cy=\"13\" r=\"11\" fill=\"none\" stroke=\"#1F5F7C\" stroke-width=\"2.4\"\/><circle cx=\"13\" cy=\"13\" r=\"2.6\" fill=\"#1F5F7C\"\/><path d=\"M13 2v22M2 13h22M5.2 5.2l15.6 15.6M20.8 5.2 5.2 20.8\" stroke=\"#1F5F7C\" stroke-width=\"1.2\"\/><\/svg><i><\/i><\/div>\n    <p class=\"eyebrow\">Wheel lacing<\/p>\n    <h2>Straight or angled screwdriver<\/h2>\n    <p class=\"lead\">Rims are drilled either radially or along each spoke line. See how the screwdriver of the ECL, ISL, Inline Lacer and CT Twin Lacer meets the nipple in each case, and which machine fits your wheel.<\/p>\n    <div class=\"rule\"><\/div>\n  <\/header>\n\n  <div class=\"controls\" role=\"group\" aria-label=\"Animation settings\">\n    <div class=\"ctl\"><span id=\"l-rim\">Rim drilling<\/span><div class=\"seg\" data-key=\"drill\" aria-labelledby=\"l-rim\"><button data-v=\"straight\">Straight-drilled<\/button><button data-v=\"angle\">Angle-drilled<\/button><\/div><\/div>\n    <div class=\"ctl\"><span id=\"l-mac\">Machine<\/span><div class=\"seg\" data-key=\"mach\" aria-labelledby=\"l-mac\"><button data-v=\"ecl\">ECL<\/button><button data-v=\"isl\">ISL<\/button><button data-v=\"inline\">Inline Lacer<\/button><button data-v=\"ct\">CT Twin Lacer<\/button><\/div><\/div>\n    <div class=\"ctl\"><span id=\"l-size\">Wheel size (inch)<\/span><div class=\"seg\" data-key=\"bsd\" aria-labelledby=\"l-size\"><button data-v=\"305\">16<\/button><button data-v=\"406\">20<\/button><button data-v=\"507\">24<\/button><button data-v=\"559\">26<\/button><button data-v=\"584\">27.5<\/button><button data-v=\"622\">28 \/ 29<\/button><button data-v=\"686\">32<\/button><\/div><\/div>\n    <div class=\"ctl\"><span id=\"l-hub\">Hub<\/span><div class=\"seg\" data-key=\"hub\" aria-labelledby=\"l-hub\"><button data-v=\"std\">Standard<\/button><button data-v=\"igh\">Enviolo-type<\/button><button data-v=\"motor\">E-bike motor<\/button><\/div><\/div>\n    <div class=\"ctl range\"><span><label for=\"r-depth\">Rim depth<\/label><output id=\"o-depth\"><\/output><\/span><input type=\"range\" id=\"r-depth\" min=\"20\" max=\"90\" step=\"1\"><\/div>\n    <div class=\"ctl range\"><span><label for=\"r-width\">Rim width<\/label><output id=\"o-width\"><\/output><\/span><input type=\"range\" id=\"r-width\" min=\"20\" max=\"100\" step=\"1\"><\/div>\n    <div class=\"ctl range\" title=\"Flange size: diameter of the NDS flange (spoke hole circle)\"><span><label for=\"r-dL\">Flange \u00d8 NDS<\/label><output id=\"o-dL\"><\/output><\/span><input type=\"range\" id=\"r-dL\" min=\"30\" max=\"200\" step=\"1\"><\/div>\n    <div class=\"ctl range\" title=\"Flange size: diameter of the DS flange (spoke hole circle)\"><span><label for=\"r-dR\">Flange \u00d8 DS<\/label><output id=\"o-dR\"><\/output><\/span><input type=\"range\" id=\"r-dR\" min=\"30\" max=\"200\" step=\"1\"><\/div>\n    <div class=\"ctl range\" title=\"Hub width over the locknuts (OLD): the flanges move outwards with it\"><span><label for=\"r-old\">Hub width (OLD)<\/label><output id=\"o-old\"><\/output><\/span><input type=\"range\" id=\"r-old\" min=\"100\" max=\"200\" step=\"1\"><\/div>\n    <div class=\"ctl transport\"><button class=\"play\" id=\"play\">Pause<\/button><button class=\"play ghost\" id=\"restart\" aria-label=\"Restart\">\u27f2<\/button><div class=\"seg\" data-key=\"speed\" aria-label=\"Speed\"><button data-v=\"1\">1\u00d7<\/button><button data-v=\"2\">2\u00d7<\/button><button data-v=\"4\">4\u00d7<\/button><\/div><\/div>\n  <\/div>\n\n  <div class=\"stage\">\n    <div class=\"view\"><span class=\"view-label\">Top view<\/span><svg id=\"side\" viewBox=\"-250 0 810 560\" role=\"img\" aria-label=\"Lacing machine, top view: the wheel lies flat\"><\/svg><\/div>\n    <div class=\"view\"><span class=\"view-label\">Side view \u00b7 sideways angles drawn \u00d72<\/span><svg id=\"dish\" viewBox=\"-250 0 810 176\" role=\"img\" aria-label=\"Lacing machine, side view: the wheel edge-on\"><\/svg><\/div>\n    <div class=\"view\"><span class=\"view-label\">Rim drilling<\/span><div class=\"drill\"><svg id=\"drillA\" viewBox=\"0 0 325 262\" role=\"img\" aria-label=\"Rim drilling: section through a hole\"><\/svg><svg id=\"drillB\" viewBox=\"320 0 490 262\" role=\"img\" aria-label=\"Rim drilling: the spoke bed seen from the hub\"><\/svg><\/div>\n      <div class=\"specs\" id=\"specs\"><\/div><\/div>\n  <\/div>\n\n  <p class=\"explain\" id=\"explain\"><\/p>\n  <div class=\"info\">\n    <div class=\"card\"><b>Screwdriver on the nipple<\/b><div class=\"v\" id=\"c-drv\"><\/div><p id=\"c-drv-t\"><\/p><\/div>\n    <div class=\"card\"><b>Spoke at the nipple<\/b><div class=\"v\" id=\"c-bend\"><\/div><p id=\"c-bend-t\"><\/p><\/div>\n    <div class=\"card\"><b>Machine<\/b><div class=\"v\" id=\"c-mac\"><\/div><p id=\"c-mac-t\"><\/p><\/div>\n  <\/div>\n  <div class=\"status\" id=\"status\"><\/div>\n  <h3 class=\"cmp-title\">Which machine fits this wheel?<\/h3>\n  <div class=\"tbl-wrap\"><table class=\"cmp\" id=\"cmp\"><\/table><\/div>\n  <p class=\"note\">Wheel and rim ranges as published on the Holland Mechanics product pages. Options are available on request; ask us about your wheel.<\/p>\n  <div class=\"legend\">\n    <span><i style=\"background:#084490\"><\/i>NDS leading<\/span><span><i style=\"background:#4F8FE6\"><\/i>NDS trailing<\/span>\n    <span><i style=\"background:#D9530A\"><\/i>DS leading<\/span><span><i style=\"background:#F5A23F\"><\/i>DS trailing<\/span>\n    <span><i style=\"background:#084591;height:2px\"><\/i>hole and nipple axis<\/span>\n    <span><i style=\"background:#D63031\"><\/i>screwdriver off the nipple axis<\/span><span><i style=\"background:#C26A00\"><\/i>spoke bend at the nipple<\/span>\n  <\/div>\n<\/section>\n\n<script>\n\/* Lacing machine explainer. The wheel (32-hole road rear, 3-cross) and its lacing order come from the\n   Holland Mechanics Wheel Line Simulator; the animation is the simulator's 2D lacing view. *\/\nconst D = {\"rimDepth\":24,\"wheel\":{\"count\":32,\"erd\":601,\"rimWidth\":22,\"cross\":3},\"nipple\":{\"type\":\"round\",\"len\":12,\"head\":2.2,\"add\":2,\"threadStart\":3.5,\"mat\":\"brass\"},\"spokes\":[{\"side\":\"L\",\"group\":\"Sa\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":-1.5708},\"rim\":{\"x\":0,\"r\":300.5,\"a\":-0.3927},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sb\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":-1.1781},\"rim\":{\"x\":0,\"r\":300.5,\"a\":-2.35619},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sa\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":-0.7854},\"rim\":{\"x\":0,\"r\":300.5,\"a\":0.3927},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sb\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":-0.3927},\"rim\":{\"x\":0,\"r\":300.5,\"a\":-1.5708},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sa\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":0},\"rim\":{\"x\":0,\"r\":300.5,\"a\":1.1781},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sb\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":0.3927},\"rim\":{\"x\":0,\"r\":300.5,\"a\":-0.7854},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sa\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":0.7854},\"rim\":{\"x\":0,\"r\":300.5,\"a\":1.9635},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sb\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":1.1781},\"rim\":{\"x\":0,\"r\":300.5,\"a\":0},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sa\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":1.5708},\"rim\":{\"x\":0,\"r\":300.5,\"a\":2.74889},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sb\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":1.9635},\"rim\":{\"x\":0,\"r\":300.5,\"a\":0.7854},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sa\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":2.35619},\"rim\":{\"x\":0,\"r\":300.5,\"a\":3.53429},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sb\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":2.74889},\"rim\":{\"x\":0,\"r\":300.5,\"a\":1.5708},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sa\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":3.14159},\"rim\":{\"x\":0,\"r\":300.5,\"a\":4.31969},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sb\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":3.53429},\"rim\":{\"x\":0,\"r\":300.5,\"a\":2.35619},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sa\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":3.92699},\"rim\":{\"x\":0,\"r\":300.5,\"a\":5.10509},\"brace\":6.82049},{\"side\":\"L\",\"group\":\"Sb\",\"hub\":{\"x\":-35,\"r\":22.5,\"a\":4.31969},\"rim\":{\"x\":0,\"r\":300.5,\"a\":3.14159},\"brace\":6.82049},{\"side\":\"R\",\"group\":\"Sd\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":-1.37445},\"rim\":{\"x\":0,\"r\":300.5,\"a\":-0.19635},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sc\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":-0.98175},\"rim\":{\"x\":0,\"r\":300.5,\"a\":-2.15984},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sd\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":-0.58905},\"rim\":{\"x\":0,\"r\":300.5,\"a\":0.58905},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sc\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":-0.19635},\"rim\":{\"x\":0,\"r\":300.5,\"a\":-1.37445},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sd\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":0.19635},\"rim\":{\"x\":0,\"r\":300.5,\"a\":1.37445},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sc\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":0.58905},\"rim\":{\"x\":0,\"r\":300.5,\"a\":-0.58905},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sd\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":0.98175},\"rim\":{\"x\":0,\"r\":300.5,\"a\":2.15984},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sc\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":1.37445},\"rim\":{\"x\":0,\"r\":300.5,\"a\":0.19635},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sd\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":1.76715},\"rim\":{\"x\":0,\"r\":300.5,\"a\":2.94524},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sc\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":2.15984},\"rim\":{\"x\":0,\"r\":300.5,\"a\":0.98175},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sd\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":2.55254},\"rim\":{\"x\":0,\"r\":300.5,\"a\":3.73064},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sc\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":2.94524},\"rim\":{\"x\":0,\"r\":300.5,\"a\":1.76715},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sd\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":3.33794},\"rim\":{\"x\":0,\"r\":300.5,\"a\":4.51604},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sc\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":3.73064},\"rim\":{\"x\":0,\"r\":300.5,\"a\":2.55254},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sd\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":4.12334},\"rim\":{\"x\":0,\"r\":300.5,\"a\":5.30144},\"brace\":3.71492},{\"side\":\"R\",\"group\":\"Sc\",\"hub\":{\"x\":19,\"r\":22.5,\"a\":4.51604},\"rim\":{\"x\":0,\"r\":300.5,\"a\":3.33794},\"brace\":3.71492}],\"order\":[14,0,2,4,6,8,10,12,5,7,9,11,13,15,1,3,30,16,18,20,22,24,26,28,19,21,23,25,27,29,31,17],\"valveA\":4.81056,\"pulses\":{\"Sa\":110,\"Sb\":110,\"Sd\":120,\"Sc\":120},\"taut\":{\"Sa\":104.31153,\"Sb\":104.31153,\"Sd\":110.52208,\"Sc\":110.52208},\"mmPulse\":0.0756};\n\nconst TAU=2*Math.PI, norm=a=>((a%TAU)+TAU)%TAU, f=v=>v.toFixed(1);\nconst clamp=x=>Math.max(0,Math.min(1,x)), ease=x=>1-Math.pow(1-clamp(x),3);\nconst angDiff=(a,b)=>Math.atan2(Math.sin(a-b),Math.cos(a-b));\nconst GC={Sa:'#084490',Sb:'#4F8FE6',Sd:'#D9530A',Sc:'#F5A23F',NDS:'#084490',DS:'#D9530A'};\nconst HOLE='#084591', AMBER='#C26A00', RED='#D63031', GREEN='#119915';\nconst halo='paint-order=\"stroke\" stroke=\"#fff\" stroke-width=\"3\" stroke-linejoin=\"round\"';\nconst N=D.spokes.length, nip=D.nipple, mm=D.mmPulse;\n\/\/ the wheel: 32 holes, bead seat (wheel size) chosen in the page, the simulator's lacing pattern; rim depth, rim width and hub are chosen in the page\n\/\/ (hub values are typical, for illustration)\n\/\/ fcL \/ fcR: hub centre to flange at the hub's own OLD; a wider OLD moves both flanges out by half the difference (the dish stays)\nconst HUBS={std:{name:'Standard hub', d:45, old:130, fcL:35, fcR:19, cross:3, shell:.45},\n  igh:{name:'Enviolo-type hub', d:95, old:135, fcL:27, fcR:27, cross:3, shell:.78},\n  motor:{name:'E-bike motor hub', d:160, old:142, fcL:30, fcR:30, cross:2, shell:.86}};\nconst BASE=D.spokes.map(s=>({side:s.side, group:s.group, rimA:s.rim.a, lead:Math.sign(angDiff(s.hub.a,s.rim.a))}));\nlet S, R, wlFs, WH;\nconst SIZE=560, PAD=250, RATE=160, INSERT=0.5, INDEX=0.35;\nconst c=SIZE\/2;\nconst rFl=s=>s.side==='L'?wlFs.rFlL:wlFs.rFlR;\n\/\/ metal finishes, shared by all three drawings\nfunction setDefs(){ const g=(id,stops,v=true)=>`<linearGradient id=\"${id}\" x1=\"0\" y1=\"0\" x2=\"${v?0:1}\" y2=\"${v?1:0}\">${stops.map(([o,c])=>`<stop offset=\"${o}\" stop-color=\"${c}\"\/>`).join('')}<\/linearGradient>`;\n  const f0=wlFs.rimIn\/wlFs.rimOut, Dp=opt.depth, m=mmFromBed=>(f0+(1-f0)*mmFromBed\/Dp).toFixed(4);   \/\/ stop at a distance (mm) out from the spoke bed\n  document.getElementById('defs').innerHTML=`<defs>\n    ${g('gSteel',[[0,'#f8fafc'],[.35,'#d4dae1'],[.7,'#8e99a5'],[1,'#c0c8d0']])}\n    ${g('gBody',[[0,'#eef2f5'],[.2,'#ffffff'],[.6,'#b4bec8'],[1,'#6e7a87']])}\n    ${g('gDark',[[0,'#7d8995'],[.45,'#4c5661'],[1,'#2a3139']])}\n    ${g('gBlue',[[0,'#3b80b8'],[.5,'#0b4f86'],[1,'#062f52']])}\n    ${g('gNip',[[0,'#f4f6f8'],[.4,'#d9dee4'],[.75,'#9aa4ae'],[1,'#c7ced5']])}\n    ${g('gNipH',[[0,'#f4f6f8'],[.4,'#d9dee4'],[.75,'#9aa4ae'],[1,'#c7ced5']],false)}\n    ${g('gSteelH',[[0,'#f8fafc'],[.35,'#d4dae1'],[.7,'#8e99a5'],[1,'#c0c8d0']],false)}\n    ${g('gSpoke',[[0,'#e9edf1'],[.5,'#b7c0c9'],[1,'#7c8794']])}\n    ${g('gSpokeH',[[0,'#e9edf1'],[.5,'#b7c0c9'],[1,'#7c8794']],false)}\n    ${g('gRail',[[0,'#e6ebf0'],[.5,'#c3cbd3'],[1,'#98a3ae']])}\n    ${g('gRailV',[[0,'#e6ebf0'],[.5,'#c3cbd3'],[1,'#98a3ae']],false)}\n    ${g('gDarkV',[[0,'#7d8995'],[.45,'#4c5661'],[1,'#2a3139']],false)}\n    ${g('gRimBar',[[0,'#2a3037'],[.1,'#aab3bc'],[.2,'#3a424b'],[.5,'#59626d'],[.8,'#3a424b'],[.9,'#aab3bc'],[1,'#2a3037']])}\n    <radialGradient id=\"gRim\" gradientUnits=\"userSpaceOnUse\" cx=\"${c}\" cy=\"${c}\" r=\"${wlFs.rimOut.toFixed(2)}\">\n      <stop offset=\"${m(0)}\" stop-color=\"#15191e\"\/><stop offset=\"${m(1.5)}\" stop-color=\"#2c333b\"\/><stop offset=\"${m(Dp*.42)}\" stop-color=\"#525b66\"\/>\n      <stop offset=\"${m(Dp-10)}\" stop-color=\"#2a3037\"\/><stop offset=\"${m(Dp-9)}\" stop-color=\"#9aa3ad\"\/><stop offset=\"${m(Dp-6)}\" stop-color=\"#dde2e7\"\/>\n      <stop offset=\"${m(Dp-3)}\" stop-color=\"#a5aeb7\"\/><stop offset=\"${m(Dp-2.4)}\" stop-color=\"#2f363e\"\/><stop offset=\"1\" stop-color=\"#4a525b\"\/><\/radialGradient>\n    <linearGradient id=\"gRimLight\" gradientUnits=\"userSpaceOnUse\" x1=\"${f(c-wlFs.rimOut)}\" y1=\"${f(c-wlFs.rimOut)}\" x2=\"${f(c+wlFs.rimOut)}\" y2=\"${f(c+wlFs.rimOut)}\">\n      <stop offset=\"0\" stop-color=\"#fff\" stop-opacity=\".22\"\/><stop offset=\".5\" stop-color=\"#fff\" stop-opacity=\"0\"\/><stop offset=\"1\" stop-color=\"#000\" stop-opacity=\".2\"\/><\/linearGradient>\n    <radialGradient id=\"gHub\" cx=\".4\" cy=\".35\" r=\".75\"><stop offset=\"0\" stop-color=\"#f4f6f8\"\/><stop offset=\".65\" stop-color=\"#c4ccd4\"\/><stop offset=\"1\" stop-color=\"#7f8a96\"\/><\/radialGradient>\n    <radialGradient id=\"gShell\" cx=\".4\" cy=\".35\" r=\".8\"><stop offset=\"0\" stop-color=\"#8a95a1\"\/><stop offset=\"1\" stop-color=\"#2e353d\"\/><\/radialGradient>\n    <pattern id=\"gAlu\" width=\"5\" height=\"5\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate(45)\"><rect width=\"5\" height=\"5\" fill=\"#bcc4cc\"\/><line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"5\" stroke=\"#8a95a0\" stroke-width=\"1.2\"\/><\/pattern>\n    <clipPath id=\"insetClip\"><rect x=\"-229\" y=\"11\" width=\"210\" height=\"146\" rx=\"11\"\/><\/clipPath>\n    <clipPath id=\"stripClip\"><rect x=\"330\" y=\"82\" width=\"470\" height=\"88\" rx=\"8\"\/><\/clipPath><\/defs>`; }\n\n\/\/ a screwdriver unit in its own frame: the bit tip at the origin, the rim towards +x\nfunction driverG({on, spin, led, name, ext=0}){\n  const ph=(spin\/360)*2.6;\n  const knurl=Array.from({length:9},(_,k)=>{ const x=-48+k*2.6+ph; return x>-47&&x<-27?`<line x1=\"${f(x)}\" y1=\"-7\" x2=\"${f(x)}\" y2=\"7\" stroke=\"#59636e\" stroke-width=\".8\" opacity=\".55\"\/>`:''; }).join('');\n  const fins=Array.from({length:9},(_,k)=>`<rect x=\"${-180+k*6}\" y=\"-22\" width=\"2.4\" height=\"44\" rx=\"1\" fill=\"#000\" opacity=\".08\"\/>`).join('');\n  return `<rect x=\"${f(-27-ext)}\" y=\"-3\" width=\"${f(21+ext)}\" height=\"6\" fill=\"url(#gSteel)\" stroke=\"#8792a0\" stroke-width=\".4\"\/><g transform=\"translate(${f(-ext)} 0)\"><path d=\"M-205 5 C-226 7 -232 21 -254 27\" fill=\"none\" stroke=\"#23282e\" stroke-width=\"5\" stroke-linecap=\"round\"\/>\n    <rect x=\"-207\" y=\"-17\" width=\"19\" height=\"34\" rx=\"6\" fill=\"url(#gDark)\"\/>\n    <rect x=\"-192\" y=\"-24\" width=\"114\" height=\"48\" rx=\"11\" fill=\"url(#gBody)\" stroke=\"#5d6874\" stroke-width=\".8\"\/>${fins}\n    <rect x=\"-122\" y=\"-24\" width=\"24\" height=\"48\" fill=\"url(#gBlue)\"\/><circle cx=\"-110\" cy=\"-13\" r=\"3.2\" fill=\"${led}\" stroke=\"#fff\" stroke-width=\".8\"\/>\n    <rect x=\"-80\" y=\"-15\" width=\"24\" height=\"30\" rx=\"4\" fill=\"url(#gDark)\"\/>\n    <rect x=\"-58\" y=\"-10\" width=\"11\" height=\"20\" rx=\"2\" fill=\"url(#gSteel)\" stroke=\"#6e7a87\" stroke-width=\".6\"\/>\n    <rect x=\"-48\" y=\"-8\" width=\"22\" height=\"16\" rx=\"3\" fill=\"url(#gSteel)\" stroke=\"#6e7a87\" stroke-width=\".6\"\/>${knurl}\n    <\/g><rect x=\"-7\" y=\"-4.5\" width=\"7\" height=\"9\" rx=\"1.5\" fill=\"url(#gSteel)\" stroke=\"${on?'#005389':'#5e6e7e'}\" stroke-width=\"${on?1.3:.6}\"\/>\n    ${name?`<text x=\"${f(-135-ext)}\" y=\"-31\" font-size=\"10\" font-weight=\"700\" fill=\"#084591\" text-anchor=\"middle\">${name}<\/text>`:''}`;\n}\n\/\/ a linear guide along the screwdriver axis, from its rear end, in the same frame turned by deg\nconst guideG=(x,y,deg,len)=>`<g transform=\"translate(${f(x)} ${f(y)}) rotate(${f(deg)})\"><rect x=\"0\" y=\"-6\" width=\"${len}\" height=\"12\" rx=\"2\" fill=\"url(#gRail)\" stroke=\"#8d98a3\" stroke-width=\".6\"\/><line x1=\"6\" y1=\"0\" x2=\"${len-6}\" y2=\"0\" stroke=\"#7c8794\" stroke-width=\"1\"\/><\/g>`;\nconst pulsesOf=i=>D.pulses[S[i].group];\n\nconst opt={drill:'straight', mach:'ecl', speed:2, bsd:622, hub:'std', depth:24, width:22, dL:45, dR:45, old:130};   \/\/ flange diameters and hub width over the locknuts, mm\nfunction applyWheel(){\n  const hub=HUBS[opt.hub], Dp=opt.depth, Wd=opt.width, step=2*TAU\/N;\n  const erd=opt.bsd+27-2*Dp, off=Wd>30?(Wd-30)*0.18:0;     \/\/ a deeper rim has a smaller ERD; wide (fat) rims drill their holes off-centre, towards the spoke's flange\n  R=erd\/2; const fcL=Math.max(8,hub.fcL+(opt.old-hub.old)\/2), fcR=Math.max(8,hub.fcR+(opt.old-hub.old)\/2);\n  S=BASE.map(b=>{ const hubA=b.rimA+b.lead*hub.cross*step, hx=b.side==='L'?-fcL:fcR, rx=b.side==='L'?-off:off, hr=(b.side==='L'?opt.dL:opt.dR)\/2;\n    const inPlane=Math.max(5,Math.sqrt(R*R+hr*hr-2*R*hr*Math.cos(hubA-b.rimA)));\n    return {side:b.side, group:b.group, hub:{x:hx, r:hr, a:hubA}, rim:{x:rx, r:R, a:b.rimA}, brace:Math.atan(Math.abs(hx-rx)\/inPlane)*180\/Math.PI}; });\n  const sc=(SIZE\/2-16)\/(R-4+Dp), fl=r=>Math.max(r*sc, Math.min(r*sc*1.6, 30));   \/\/ small flanges drawn up to 1.6\u00d7 oversize, big hubs to scale\n  wlFs={c, sc, rIn:R*sc, rimIn:(R-4)*sc, rimOut:(R-4+Dp)*sc, rFlL:fl(opt.dL\/2), rFlR:fl(opt.dR\/2)};\n  WH={erd, off, hub, fcL, fcR};\n  setDefs();\n}\n\/\/ how much longer the bit is than on a 24 mm rim, in drawing units: the screwdriver stays outside the rim\nconst reach=()=>Math.max(0,(opt.depth-24)*wlFs.sc);\n\/\/ the bit has to pass the access hole in the tyre bed (illustrative: \u00d84 mm bit, \u00d89 mm access hole)\nconst BIT=4, ACCESS=9, CLEAR=(ACCESS-BIT)\/2;\nconst reachMM=()=>opt.depth-2-nip.head-6.9;          \/\/ from the nipple head to the middle of the tyre bed, mm\n\/\/ ECL \/ ISL: straight engagement, one head. Inline Lacer: one head that tilts onto every hole, in the wheel plane and sideways.\n\/\/ CT Twin Lacer: two heads; their angle in the wheel plane is set once per wheel, sideways they do not tilt (they only move up\n\/\/ and down, about 1 cm, for offset-drilled holes)\nconst MACH={ecl:{heads:1, name:'STRAIGHT SCREWDRIVER'}, isl:{heads:1, name:'STRAIGHT SCREWDRIVER'}, inline:{heads:1, name:'ANGLED SCREWDRIVER'}, ct:{heads:2, name:'CT SCREWDRIVER'}};\nconst straightM=m=>m==='ecl'||m==='isl';\n\/\/ wheel and rim ranges from the product pages on hollandmechanics.com (rim width \u00d7 depth in mm; options: Fat up to 100 mm wide,\n\/\/ High up to 60 \/ 90 mm deep). The Inline Lacer's screwdriver moves up to 15\u00b0 radially and 25\u00b0 axially.\nconst SPEC={\n  ecl:{name:'ECL', full:'Easy Cam Lacer (ECL)', inch:[16,29], w:35, h:25, cap:'40\u201360', drv:'straight screwdriver'},\n  isl:{name:'ISL', full:'Intelligent Lacing (ISL)', inch:[16,32], w:50, h:32, optW:100, optH:[60,90], cap:'45\u201365', drv:'straight screwdriver'},\n  inline:{name:'Inline Lacer', full:'Inline Lacer (IL)', inch:[16,29], w:50, h:40, optW:100, optH:[60,90], cap:'45\u201365', drv:'screwdriver tilts 15\u00b0 radial \u00b7 25\u00b0 axial', tilt:[15,25]},\n  ct:{name:'CT Twin Lacer', full:'CT Twin Lacer', inch:[20,29], w:30, h:35, cap:'70\u2013100', drv:'2 screwdrivers, set per wheel in the wheel plane'}};\nconst SIZES={305:['16\"',16], 406:['20\"',20], 507:['24\"',24], 559:['26\"',26], 584:['27.5\"',27.5], 622:['28\"\/29\"',29], 686:['32\"',32]};\n\n\/* ---- geometry: true angles from the wheel model; drawn angles follow the drawing (hub flanges drawn 1.6\u00d7 oversize) ---- *\/\nconst spokeTilt=i=>{ const s=S[i], a=s.rim.a; const hx=s.hub.r*Math.cos(s.hub.a)-R*Math.cos(a), hy=s.hub.r*Math.sin(s.hub.a)-R*Math.sin(a); return angDiff(Math.atan2(hy,hx), a+Math.PI); };\nconst drawnTilt=i=>{ const s=S[i], a=s.rim.a, rf=rFl(s); const hx=rf*Math.cos(s.hub.a)-wlFs.rIn*Math.cos(a), hy=rf*Math.sin(s.hub.a)-wlFs.rIn*Math.sin(a); return angDiff(Math.atan2(hy,hx), a+Math.PI); };\nconst comb=(t,b)=>Math.atan(Math.hypot(Math.tan(t),Math.tan(b*Math.PI\/180)))*180\/Math.PI;\nconst spokeAngle=i=>comb(spokeTilt(i),S[i].brace);                 \/\/ the spoke against the radial line, deg\nconst holeTilt=i=>opt.drill==='angle'?spokeTilt(i):0;\nconst holeAngle=i=>opt.drill==='angle'?spokeAngle(i):0;             \/\/ the hole (and nipple) against the radial line, deg\nconst drawnHole=i=>opt.drill==='angle'?drawnTilt(i):0;\nconst holeBrace=i=>opt.drill==='angle'?S[i].brace:0;                 \/\/ deg, towards the spoke's flange\nconst mean=a=>a.reduce((x,y)=>x+y,0)\/Math.max(1,a.length);\nconst headSpokes=h=>plan.cycles.map(cy=>cy.sp[h]).filter(i=>i!=null);\n\/\/ the screwdriver of head h on spoke i: its angle in the wheel plane (drawn and true, rad) and sideways (deg)\nconst TILT=SPEC.inline.tilt, lim=(a,maxDeg)=>Math.max(-maxDeg*Math.PI\/180,Math.min(maxDeg*Math.PI\/180,a));\nconst drvTiltDrawn=(i,h)=>opt.mach==='inline'?(holeTilt(i)===0?0:drawnHole(i)*lim(holeTilt(i),TILT[0])\/holeTilt(i)):opt.mach==='ct'?mean(headSpokes(h).map(drawnHole)):0;\nconst drvTiltTrue=(i,h)=>opt.mach==='inline'?lim(holeTilt(i),TILT[0]):opt.mach==='ct'?mean(headSpokes(h).map(holeTilt)):0;\nconst drvBrace=i=>opt.mach==='inline'?Math.min(holeBrace(i),TILT[1]):0;\n\/\/ the angle between screwdriver and nipple axis, deg\nconst missAt=(i,h)=>{ const inP=Math.abs(holeTilt(i)-drvTiltTrue(i,h)), side=Math.abs(holeBrace(i)-drvBrace(i))*Math.PI\/180; return reachMM()*Math.hypot(Math.tan(inP),Math.tan(side)); };\nconst maxMiss=()=>Math.max(...plan.off.map((_,h)=>Math.max(...headSpokes(h).map(i=>missAt(i,h)))));\nconst offAngle=(i,h)=>{ const v=(t,b)=>{ const x=[1,Math.tan(t),Math.tan(b*Math.PI\/180)], n=Math.hypot(...x); return x.map(q=>q\/n); };\n  const a=v(holeTilt(i),holeBrace(i)), b=v(drvTiltTrue(i,h),drvBrace(i)); return Math.acos(Math.min(1,a[0]*b[0]+a[1]*b[1]+a[2]*b[2]))*180\/Math.PI; };\n\n\/* ---- the lacing plan: one spoke per index (single head) or a leading and a trailing spoke two holes apart (CT Twin) ---- *\/\nlet plan;\nfunction buildPlan(){\n  const step=2*TAU\/N; let cycles;\n  if(MACH[opt.mach].heads===1) cycles=D.order.map(i=>({sp:[i], mid:S[i].rim.a}));\n  else {\n    const used=new Set(); cycles=[];\n    for(const i of D.order){ const g=S[i].group; if(used.has(i)||(g!=='Sa'&&g!=='Sd')) continue;\n      const pg=g==='Sa'?'Sb':'Sc'; let best=-1, bd=1e9;\n      S.forEach((s,j)=>{ if(s.group!==pg||used.has(j)) return; const d=Math.abs(angDiff(s.rim.a,S[i].rim.a)-step); if(d<bd){ bd=d; best=j; } });\n      if(best<0||bd>0.05) continue; used.add(i); used.add(best);\n      const mid=S[i].rim.a+angDiff(S[best].rim.a,S[i].rim.a)\/2;\n      const pair=[i,best].sort((a,b)=>angDiff(S[a].rim.a,mid)-angDiff(S[b].rim.a,mid));   \/\/ head 1 takes the hole before the middle\n      cycles.push({sp:pair, mid}); }\n    D.order.forEach(i=>{ if(!used.has(i)) cycles.push({sp:[i], mid:S[i].rim.a}); });\n  }\n  let t0=0;\n  cycles.forEach(cy=>{ cy.dur=INDEX+INSERT+Math.max(...cy.sp.map(i=>pulsesOf(i)))\/RATE; cy.start=t0; t0+=cy.dur; cy.rot=norm(Math.PI-norm(cy.mid)); });\n  const off=MACH[opt.mach].heads===1?[0]:[-step\/2, step\/2];      \/\/ where the heads sit, relative to 9 o'clock\n  plan={cycles, off, total:t0+1.5, end:t0};\n}\n\nfunction state(t){\n  const {cycles}=plan; const frac=new Float64Array(N), pul=new Float64Array(N); let k0=-1;\n  cycles.forEach((cy,k)=>{ const tt=t-cy.start; if(tt<=0) return; if(tt<cy.dur) k0=k; const tIn=tt-INDEX;\n    cy.sp.forEach(i=>{ frac[i]=clamp(tIn\/INSERT); pul[i]=Math.round(clamp((tIn-INSERT)\/(pulsesOf(i)\/RATE))*pulsesOf(i)); }); });\n  const done=t>=plan.end; let rot=cycles[0].rot, idxP=1;\n  if(k0>=0){ const cy=cycles[k0], r0=k0>0?cycles[k0-1].rot:cycles[0].rot; let d=norm(cy.rot-r0); if(d>Math.PI) d-=TAU; idxP=clamp((t-cy.start)\/INDEX); rot=r0+d*(1-Math.pow(1-idxP,2)); }\n  else if(done) rot=cycles[cycles.length-1].rot;\n  const heads=plan.off.map((_,h)=>{ if(k0<0) return null; const cy=cycles[k0], i=cy.sp[h]; if(i==null) return null;\n    const thr=clamp((t-cy.start-INDEX-INSERT)\/(pulsesOf(i)\/RATE)); let adv=0, spinning=false;\n    if(frac[i]>=1){ if(thr<0.12) adv=ease(thr\/0.12); else if(thr>0.9) adv=1-ease((thr-0.9)\/0.1); else { adv=1; spinning=true; } }\n    const prev=k0>0?cycles[k0-1].sp[h]:null;\n    return {i, thr, adv, spinning, engaging:frac[i]>=1, prev:prev==null?null:prev}; });\n  const loaded=cycles.reduce((n,cy)=>n+(t>=cy.start+cy.dur?cy.sp.length:0),0), steps=cycles.filter(cy=>t>cy.start).length;\n  return {frac, pul, k0, done, rot, idxP, heads, loaded, steps};\n}\n\n\/* ---- top view (the wheel lies flat in the machine): the wheel turns, the screwdriver(s) stay at the left ---- *\/\nfunction wheelSVG(st){\n  const P=(r,a)=>[c+r*Math.cos(a), c+r*Math.sin(a)];\n  const active=new Set(st.heads.filter(Boolean).map(h=>h.i));\n  const colorOf=i=>{ const g=S[i].group, col=GC[g]; if(st.done||active.has(i)||st.pul[i]>D.taut[g]) return col; const n=parseInt(col.slice(1),16); return `rgba(${(n>>16)&255},${(n>>8)&255},${n&255},0.42)`; };\n  let back='', front='', nips='', eyelets='', holesL='', holesR='';\n  S.forEach((s,i)=>{ const [ex0,ey0]=P(wlFs.rimIn+2.6,s.rim.a); eyelets+=`<circle cx=\"${f(ex0)}\" cy=\"${f(ey0)}\" r=\"1.9\" fill=\"#0b0e11\" stroke=\"#a9b2bb\" stroke-width=\".7\"\/>`;\n    const [hx0,hy0]=P(rFl(s)-2.2,s.hub.a), hole=`<circle cx=\"${f(hx0)}\" cy=\"${f(hy0)}\" r=\"1.2\" fill=\"#2e353d\"\/>`; if(s.side==='L') holesL+=hole; else holesR+=hole;\n    const fr=st.frac[i]; if(fr<=0) return;\n    const [hx,hy]=P(rFl(s),s.hub.a), [fx,fy]=P(wlFs.rIn,s.rim.a), ex=hx+(fx-hx)*fr, ey=hy+(fy-hy)*fr;\n    if(st.pul[i]>0){ const [n1x,n1y]=P(wlFs.rimIn+1,s.rim.a), [n2x,n2y]=P(wlFs.rimIn-5,s.rim.a);\n      nips+=`<line x1=\"${f(n1x)}\" y1=\"${f(n1y)}\" x2=\"${f(n2x)}\" y2=\"${f(n2y)}\" stroke=\"#6f7a86\" stroke-width=\"3.4\" stroke-linecap=\"round\"\/><line x1=\"${f(n1x)}\" y1=\"${f(n1y)}\" x2=\"${f(n2x)}\" y2=\"${f(n2y)}\" stroke=\"#e3e8ec\" stroke-width=\"1.8\" stroke-linecap=\"round\"\/>`; }\n    const w=active.has(i)?2.6:1.4, col=colorOf(i);\n    const ln=`<line x1=\"${f(hx)}\" y1=\"${f(hy)}\" x2=\"${f(ex)}\" y2=\"${f(ey)}\" stroke=\"#fff\" stroke-width=\"${(w+1.4).toFixed(1)}\" stroke-opacity=\".9\" stroke-linecap=\"round\"\/><line x1=\"${f(hx)}\" y1=\"${f(hy)}\" x2=\"${f(ex)}\" y2=\"${f(ey)}\" stroke=\"${col}\" stroke-width=\"${w}\" stroke-linecap=\"round\"\/>`;\n    if(s.side==='R') back+=ln; else front+=ln; });\n  const [v1x,v1y]=P(wlFs.rimIn-.5,D.valveA), [v2x,v2y]=P(wlFs.rimIn-8,D.valveA);\n  const ring=(r1,r2)=>`M${f(c-r2)} ${c}a${f(r2)} ${f(r2)} 0 1 0 ${f(2*r2)} 0a${f(r2)} ${f(r2)} 0 1 0 ${f(-2*r2)} 0Z M${f(c-r1)} ${c}a${f(r1)} ${f(r1)} 0 1 0 ${f(2*r1)} 0a${f(r1)} ${f(r1)} 0 1 0 ${f(-2*r1)} 0Z`;\n  const flange=(r,holes)=>`<circle cx=\"${c}\" cy=\"${c}\" r=\"${f(r)}\" fill=\"url(#gHub)\" stroke=\"#6e7a87\" stroke-width=\".8\"\/>${holes}`;\n  \/\/ anodised rim with a machined brake track, eyelets on every hole, aluminium hub flanges; the light stays fixed\n  return `<g transform=\"rotate(${(st.rot*180\/Math.PI).toFixed(2)} ${c} ${c})\">${back}${flange(wlFs.rFlR,holesR)}${front}${flange(wlFs.rFlL,holesL)}\n    <circle cx=\"${c}\" cy=\"${c}\" r=\"${f(wlFs.rFlL*WH.hub.shell)}\" fill=\"url(#gShell)\"\/><circle cx=\"${c}\" cy=\"${c}\" r=\"3\" fill=\"#dfe4e9\" stroke=\"#6f7a86\" stroke-width=\".6\"\/>\n    <path fill-rule=\"evenodd\" d=\"${ring(wlFs.rimIn,wlFs.rimOut)}\" fill=\"url(#gRim)\" stroke=\"#111418\" stroke-width=\".8\"\/>${eyelets}${nips}\n    <line x1=\"${f(v1x)}\" y1=\"${f(v1y)}\" x2=\"${f(v2x)}\" y2=\"${f(v2y)}\" stroke=\"#d5dbe1\" stroke-width=\"3\" stroke-linecap=\"round\"\/><\/g>\n    <path fill-rule=\"evenodd\" d=\"${ring(wlFs.rimIn,wlFs.rimOut)}\" fill=\"url(#gRimLight)\"\/>`;\n}\n\n\/\/ one screwdriver head, drawn as if its hole were at 9 o'clock, then turned to where the head sits\nfunction headSVG(st, h){\n  const hd=st.heads[h], off=plan.off[h], twin=plan.off.length>1;\n  const sp=hd?S[hd.i]:null, engaging=!!hd&&hd.engaging&&!st.done;\n  const prevT=hd&&hd.prev!=null?drvTiltDrawn(hd.prev,h):0, ang=hd?prevT+(drvTiltDrawn(hd.i,h)-prevT)*(1-Math.pow(1-st.idxP,3)):0;\n  const vTilt=hd?drawnTilt(hd.i):0, vHole=hd?drawnHole(hd.i):0, tTilt=hd?spokeTilt(hd.i):0, tOff=hd?Math.abs(holeTilt(hd.i)-drvTiltTrue(hd.i,h)):0;\n  const px=wlFs.c-wlFs.rIn, py=wlFs.c, ux=Math.cos(ang), uy=Math.sin(ang), ext=reach();\n  const pulsesNow=hd?st.pul[hd.i]:0, on=engaging&&hd.spinning;\n  const retract=-46*(1-(hd?hd.adv:0)), creep=hd&&hd.spinning?hd.thr*10:0;\n  const P=(d,s=0)=>[px+ux*d-uy*s, py+uy*d+ux*s];\n  const bitTip=retract+creep-2;\n  const spin=(pulsesNow*60)%360;\n  const [tx,ty]=P(bitTip), [gx,gy]=P(-250-ext), angD=ang*180\/Math.PI;\n  const led=on?'#119915':(engaging?'#d68910':'#8a9bb0');\n  const arcs=on?[1,2,3].map(k=>{ const r=14+k*7, a0=(spin+k*110)*Math.PI\/180; return `<path d=\"M${f(tx+r*Math.cos(a0))} ${f(ty+r*Math.sin(a0))} A${r} ${r} 0 0 1 ${f(tx+r*Math.cos(a0+0.9))} ${f(ty+r*Math.sin(a0+0.9))}\" fill=\"none\" stroke=\"#084591\" stroke-opacity=\"${(0.55-k*0.12).toFixed(2)}\" stroke-width=\"2\" stroke-linecap=\"round\"\/>`; }).join(''):'';\n  \/\/ mount: the angled screwdriver rides an X-Y stage whose Y carriage follows the rear of the driver (it tilts about the nipple\n  \/\/ seat); the straight one sits on a plate. A single head has its own column, the twin heads share one (drawn once).\n  const rail=twin?'':(!straightM(opt.mach)\n    ?`<rect x=\"${f(px-250-ext)}\" y=\"${f(py-120)}\" width=\"18\" height=\"240\" rx=\"4\" fill=\"url(#gRailV)\" stroke=\"#8d98a3\"\/>`\n    :`<rect x=\"${f(px-250-ext)}\" y=\"${f(py-120)}\" width=\"18\" height=\"240\" rx=\"4\" fill=\"url(#gDarkV)\"\/>`);\n  const mount=!straightM(opt.mach)\n    ?`<rect x=\"${f(px-250-ext)}\" y=\"${f(P(-241-ext)[1]-16)}\" width=\"34\" height=\"32\" rx=\"5\" fill=\"url(#gDark)\"\/>`\n    :`<rect x=\"${f(px-240-ext)}\" y=\"${f(py-10)}\" width=\"22\" height=\"20\" rx=\"4\" fill=\"url(#gDark)\"\/>`;\n  const name=MACH[opt.mach].name+(twin?` ${h+1}`:'');\n  const unit=guideG(gx,gy,angD,190)+`<g transform=\"translate(${f(tx)} ${f(ty)}) rotate(${f(angD)})\">${driverG({on,spin,led,name,ext})}<\/g>${arcs}`;\n  \/\/ wedges on the hub side, where the screwdriver does not cover them\n  const arc=(a0,a1,rad,col)=>{ const A=[px+rad*Math.cos(a0), py+rad*Math.sin(a0)], B=[px+rad*Math.cos(a1), py+rad*Math.sin(a1)];\n    return `<path d=\"M${f(px)} ${f(py)} L${f(A[0])} ${f(A[1])} A${rad} ${rad} 0 0 ${a1>a0?1:0} ${f(B[0])} ${f(B[1])} Z\" fill=\"${col}\" fill-opacity=\".18\" stroke=\"${col}\" stroke-width=\"1\"\/>`; };\n  const holeAxis=hd?`<line x1=\"${f(px-120*Math.cos(vHole))}\" y1=\"${f(py-120*Math.sin(vHole))}\" x2=\"${f(px+150*Math.cos(vHole))}\" y2=\"${f(py+150*Math.sin(vHole))}\" stroke=\"${HOLE}\" stroke-width=\"1.4\" stroke-dasharray=\"7 3 2 3\"\/>`:'';\n  const offW=hd&&engaging&&tOff>0.001;\n  const holeW=hd&&opt.drill==='angle'&&Math.abs(vHole)>0.002;\n  const bendW=hd&&opt.drill==='straight'&&Math.abs(vTilt)>0.002;\n  const inLine=hd&&engaging&&opt.drill==='angle'&&!offW&&!straightM(opt.mach);\n  const dg=a=>(Math.abs(a)*180\/Math.PI).toFixed(1);\n  \/\/ angles in the wheel plane, on the hub side: the hole against the radial line (blue), the screwdriver against the hole (red),\n  \/\/ the spoke bending into a radial hole (amber); the notes stack on the side the wedges open to\n  let marks='', notes=[];\n  if(holeW||offW) marks+=`<line x1=\"${f(px)}\" y1=\"${f(py)}\" x2=\"${f(px+150)}\" y2=\"${f(py)}\" stroke=\"#9aa5b0\" stroke-width=\"1.2\" stroke-dasharray=\"4 4\"\/>`;\n  if(holeW){ marks+=arc(0,vHole,64,HOLE); notes.push([`hole drilled ${dg(holeTilt(hd.i))}\u00b0`,'along the spoke, in the wheel plane',HOLE]); }\n  if(offW){ marks+=`<line x1=\"${f(px)}\" y1=\"${f(py)}\" x2=\"${f(P(150)[0])}\" y2=\"${f(P(150)[1])}\" stroke=\"#7f93a8\" stroke-width=\"1.4\" stroke-dasharray=\"5 4\"\/>`+arc(ang,vHole,96,RED);\n    notes.push([`screwdriver ${dg(tOff)}\u00b0 off`,'the nipple axis',RED]); }\n  if(inLine) notes.push([opt.mach==='ct'?`screwdriver set to ${dg(drvTiltTrue(hd.i,h))}\u00b0`:'screwdriver tilted onto the hole','in line with the nipple',GREEN]);\n  if(bendW){ marks+=arc(0,vTilt,64,AMBER); notes.push([`spoke bends ${dg(tTilt)}\u00b0`,'into the nipple, in the wheel plane',AMBER]); }\n  const dn=(holeW?vHole:bendW?vTilt:vHole-ang)>0, n=notes.length*2;\n  const labels=notes.flatMap(([b,sm,col],q)=>[[b,col,1],[sm,col,0]]).map(([t2,col,b],k)=>[px+30, dn?py+40+13*k:py-17-13*(n-1-k), t2, col, b]);\n  \/\/ labels stay level: their anchor turns with the head, the text does not\n  const co=Math.cos(off), si=Math.sin(off), rp=(x,y)=>[c+(x-c)*co-(y-c)*si, c+(x-c)*si+(y-c)*co];\n  const txt=labels.map(([x,y,s,col,b])=>{ const [X,Y]=rp(x,y); return `<text x=\"${f(X)}\" y=\"${f(Y)}\" font-size=\"${b?10.5:9}\" font-weight=\"${b?700:400}\" fill=\"${col}\" ${halo}>${s}<\/text>`; }).join('');\n  return `<g transform=\"rotate(${(off*180\/Math.PI).toFixed(3)} ${c} ${c})\">${rail}${mount}${holeAxis}${marks}${unit}<\/g>${txt}`;\n}\n\n\/\/ the nipple of head 1, magnified six times\nfunction insetSVG(st){\n  const hd=st.heads[0], sp=hd?S[hd.i]:null, engaging=!!hd&&hd.engaging&&!st.done, on=engaging&&hd.spinning;\n  const pulsesNow=hd?st.pul[hd.i]:0, spin=(pulsesNow*60)%360;\n  const sc6=6, X0=-150, Y0=70, wall=2.2, head=nip.head||2.2, len=nip.len, z0=-(len-head)+nip.threadStart, travel=pulsesNow*mm, tip=z0+travel, taut=nip.add;\n  const x=z=>X0-z*sc6, r=3.4*sc6\/2, rh=(nip.type==='internal'?2.2:3.2)*sc6\/2, rs=1.0*sc6;\n  const sgn=sp&&sp.side==='R'?1:-1;\n  const aHole=hd?holeAngle(hd.i):0, aSpoke=hd?spokeAngle(hd.i):0, off=hd?offAngle(hd.i,0):0, aDrv=aHole-off;\n  const th=sgn*aHole*Math.PI\/180, xe=x(-(len-head)), edge=X0+126;\n  const xa=x(0), xb=x(-wall), tn=Math.tan(th), hw=(r+1.5)\/Math.cos(th), yc=xx=>Y0+tn*(xx-X0);\n  const wallPoly=`<polygon points=\"${f(xa)},${Y0-40} ${f(xb)},${Y0-40} ${f(xb)},${f(yc(xb)-hw)} ${f(xa)},${f(yc(xa)-hw)}\" fill=\"url(#gAlu)\" stroke=\"#6e7a87\" stroke-width=\".6\"\/><polygon points=\"${f(xa)},${f(yc(xa)+hw)} ${f(xb)},${f(yc(xb)+hw)} ${f(xb)},${Y0+40} ${f(xa)},${Y0+40}\" fill=\"url(#gAlu)\" stroke=\"#6e7a87\" stroke-width=\".6\"\/>`;\n  \/\/ double-wall rim: the nipple seats on the spoke bed (above); the tyre bed sits 7 mm further out and has a wider access hole\n  \/\/ (\u00d87.2 mm) for the nipple and the screwdriver bit, drilled on the same axis as the spoke-bed hole\n  const gapT=opt.depth-9.8, brk=gapT>9.6, zb=brk?9.5:gapT;     \/\/ spoke bed to tyre bed, mm; deeper rims are drawn with a break\n  const xo0=x(zb), xo1=x(zb+1.8), hwO=ACCESS\/2*sc6\/Math.cos(th), xk=x(8.6);\n  const zig=dx=>Array.from({length:15},(_,q)=>`${f(xk+dx+(q%2?2:-2))},${Y0-42+q*6}`).join(' ');\n  const breakG=brk?`<rect x=\"${f(xk-2)}\" y=\"${Y0-42}\" width=\"7\" height=\"84\" fill=\"#fff\"\/><polyline points=\"${zig(-2)}\" fill=\"none\" stroke=\"#6e7a87\" stroke-width=\".8\"\/><polyline points=\"${zig(5)}\" fill=\"none\" stroke=\"#6e7a87\" stroke-width=\".8\"\/>\n    <text x=\"${f(xk+1.5)}\" y=\"${Y0-30}\" font-size=\"8\" font-weight=\"700\" fill=\"#56687f\" text-anchor=\"middle\" ${halo}>${gapT.toFixed(0)} mm<\/text>`:'';\n  const bedPoly=`<polygon points=\"${f(xo1)},${Y0-40} ${f(xo0)},${Y0-40} ${f(xo0)},${f(yc(xo0)-hwO)} ${f(xo1)},${f(yc(xo1)-hwO)}\" fill=\"url(#gAlu)\" stroke=\"#6e7a87\" stroke-width=\".6\"\/><polygon points=\"${f(xo1)},${f(yc(xo1)+hwO)} ${f(xo0)},${f(yc(xo0)+hwO)} ${f(xo0)},${Y0+40} ${f(xo1)},${Y0+40}\" fill=\"url(#gAlu)\" stroke=\"#6e7a87\" stroke-width=\".6\"\/>`;\n  const bedLabels=`<text x=\"${f((xo0+xo1)\/2)}\" y=\"${Y0+50}\" font-size=\"8\" fill=\"#56687f\" text-anchor=\"middle\">tyre bed<\/text><text x=\"${f((xa+xb)\/2)}\" y=\"${Y0+50}\" font-size=\"8\" fill=\"#56687f\" text-anchor=\"middle\">spoke bed<\/text>`;\n  const sx=hd?x(tip):x(z0), thEnd=x(tip-9.5);\n  const threads=(x0,x1)=>Array.from({length:60},(_,q)=>{ const xx=x0+2+q*2.7; return xx<x1&&xx<edge?`<line x1=\"${f(xx)}\" y1=\"${f(Y0-rs)}\" x2=\"${f(xx-1.6)}\" y2=\"${f(Y0+rs)}\" stroke=\"#465C73\" stroke-width=\".7\" opacity=\".75\"\/>`:''; }).join('');\n  const outer=opt.drill==='straight'&&hd?sgn*aSpoke:0;\n  const spokeIn=`<rect x=\"${f(sx)}\" y=\"${f(Y0-rs)}\" width=\"${f(Math.max(0,xe-sx))}\" height=\"${f(2*rs)}\" fill=\"url(#gSpoke)\" stroke=\"#5d6874\" stroke-width=\".7\"\/>${threads(sx,Math.min(xe,thEnd))}`;\n  const spokeOut=rot=>`<g transform=\"rotate(${f(rot)} ${f(xe)} ${Y0})\"><rect x=\"${f(Math.max(sx,xe))}\" y=\"${f(Y0-rs)}\" width=\"${f(edge+30-Math.max(sx,xe))}\" height=\"${f(2*rs)}\" fill=\"url(#gSpoke)\" stroke=\"#5d6874\" stroke-width=\".7\"\/>${threads(Math.max(sx,xe),thEnd)}<\/g>`;\n  const nippleG=`<g transform=\"rotate(${f(sgn*aHole)} ${X0} ${Y0})\">\n      <rect x=\"${f(x(0))}\" y=\"${f(Y0-r)}\" width=\"${f((len-head)*sc6)}\" height=\"${f(2*r)}\" rx=\"2\" fill=\"url(#gNip)\" stroke=\"#5e6e7e\" stroke-width=\".8\"\/>\n      <rect x=\"${f(x(head))}\" y=\"${f(Y0-rh)}\" width=\"${f(head*sc6)}\" height=\"${f(2*rh)}\" rx=\"3\" fill=\"url(#gNip)\" stroke=\"#5e6e7e\" stroke-width=\".8\"\/><rect x=\"${f(x(head))}\" y=\"${f(Y0-1.2)}\" width=\"${f(head*sc6*.55)}\" height=\"2.4\" fill=\"#4a525b\"\/>\n      ${spokeIn}${opt.drill==='angle'||!hd?spokeOut(0):''}<\/g>${opt.drill==='straight'&&hd?spokeOut(outer):''}`;\n  \/\/ the nipple bit: a sleeve over the nipple head with the blade in its slot, on a steel shank; the dark line turns with it\n  const slotY=Y0+8*Math.sin(spin*Math.PI\/180);\n  const driverG=`<g transform=\"rotate(${f(sgn*aDrv)} ${X0} ${Y0})\">\n      <rect x=\"${f(x(head)-90)}\" y=\"${Y0-5}\" width=\"73\" height=\"10\" fill=\"url(#gSteel)\" stroke=\"#6e7a87\" stroke-width=\".6\"\/>\n      <rect x=\"${f(x(head)-18)}\" y=\"${Y0-11}\" width=\"19\" height=\"22\" rx=\"3\" fill=\"url(#gSteel)\" stroke=\"${on?'#005389':'#5e6e7e'}\" stroke-width=\"${on?1.4:.8}\"\/>\n      <line x1=\"${f(x(head)-17)}\" y1=\"${f(slotY)}\" x2=\"${f(x(head))}\" y2=\"${f(slotY)}\" stroke=\"#3c4650\" stroke-width=\"1.2\" opacity=\".55\"\/><\/g>`;\n  const axisL=`<line x1=\"${f(X0-70)}\" y1=\"${f(Y0-70*tn)}\" x2=\"${f(X0+120)}\" y2=\"${f(Y0+120*tn)}\" stroke=\"${HOLE}\" stroke-width=\"1\" stroke-dasharray=\"7 3 2 3\" opacity=\".7\"\/>`;\n  const line1=!hd?(opt.drill==='straight'?'radially drilled hole':'hole drilled along the spoke')\n    :opt.drill==='straight'?`radial hole \u00b7 spoke bends ${aSpoke.toFixed(1)}\u00b0 into the nipple`\n    :`hole along the spoke \u00b7 ${off<0.05?'screwdriver in line':`screwdriver ${off.toFixed(1)}\u00b0 off`}`;\n  const col1=!hd?'#56687f':opt.drill==='straight'?AMBER:(off<0.05?GREEN:RED);\n  const line2=hd?`${(pulsesNow*mm).toFixed(1)} mm of thread screwed on`:'waiting for the next spoke';\n  return `<rect x=\"-230\" y=\"${Y0-60}\" width=\"212\" height=\"148\" rx=\"12\" fill=\"#fff\" stroke=\"#d4dce7\"\/>\n    <text x=\"-124\" y=\"${Y0-44}\" font-size=\"10\" font-weight=\"700\" fill=\"#56687f\" text-anchor=\"middle\">NIPPLE \u00d76 \u00b7 double-wall rim${plan.off.length>1?' \u00b7 head 1':''}<\/text>\n    <g clip-path=\"url(#insetClip)\">${axisL}${wallPoly}${bedPoly}${nippleG}${driverG}${breakG}${bedLabels}<\/g>\n    <text x=\"-124\" y=\"${Y0+62}\" font-size=\"9.5\" font-weight=\"700\" fill=\"${col1}\" text-anchor=\"middle\">${line1}<\/text>\n    <text x=\"-124\" y=\"${Y0+77}\" font-size=\"9.5\" fill=\"#56687f\" text-anchor=\"middle\">${line2}<\/text>`;\n}\n\nfunction sideSVG(st){\n  const twin=plan.off.length>1, px=wlFs.c-wlFs.rIn, py=wlFs.c, ext=reach();\n  const column=twin?(!straightM(opt.mach)\n    ?`<rect x=\"${f(px-250-ext)}\" y=\"${f(py-150)}\" width=\"18\" height=\"300\" rx=\"4\" fill=\"url(#gRailV)\" stroke=\"#8d98a3\"\/>`\n    :`<rect x=\"${f(px-250-ext)}\" y=\"${f(py-150)}\" width=\"18\" height=\"300\" rx=\"4\" fill=\"url(#gDarkV)\"\/>`):'';\n  return `    ${wheelSVG(st)}${column}${plan.off.map((_,h)=>headSVG(st,h)).join('')}${insetSVG(st)}`;\n}\n\n\/* ---- side view: the rim edge-on, the hub with its flanges, the screwdriver's angle in the bracing plane ---- *\/\nfunction dishSVG(st){\n  const H=176, yc=96, maxFc=Math.max(20,opt.width\/2,...S.map(s=>Math.abs(s.hub.x))), AX=Math.min(2.4, 60\/(maxFc*wlFs.sc));\n  const X=(r,a)=>wlFs.c+r*Math.cos(a+st.rot), Y=x=>yc+x*wlFs.sc*AX;\n  const mean=a=>a.reduce((x,y)=>x+y,0)\/Math.max(1,a.length);\n  const yL=Y(mean(S.filter(s=>s.side==='L').map(s=>s.hub.x))), yR=Y(mean(S.filter(s=>s.side==='R').map(s=>s.hub.x)));\n  const rimW=opt.width*wlFs.sc*AX, x0=wlFs.c-wlFs.rimOut, x1=wlFs.c+wlFs.rimOut;\n  const active=new Set(st.heads.filter(Boolean).map(h=>h.i));\n  const colorOf=i=>{ const g=S[i].group, col=GC[g]; if(st.done||active.has(i)||st.pul[i]>D.taut[g]) return col; const n=parseInt(col.slice(1),16); return `rgba(${(n>>16)&255},${(n>>8)&255},${n&255},0.42)`; };\n  let spokes='', nips='';\n  S.forEach((s,i)=>{ const fr=st.frac[i]; if(fr<=0) return; const hx=X(rFl(s),s.hub.a), hy=Y(s.hub.x), rx=X(wlFs.rIn,s.rim.a), ry=Y(s.rim.x);\n    const ex=hx+(rx-hx)*fr, ey=hy+(ry-hy)*fr, act=active.has(i);\n    spokes+=`<line x1=\"${f(hx)}\" y1=\"${f(hy)}\" x2=\"${f(ex)}\" y2=\"${f(ey)}\" stroke=\"#fff\" stroke-width=\"${act?4:2.2}\" stroke-opacity=\".8\" stroke-linecap=\"round\"\/><line x1=\"${f(hx)}\" y1=\"${f(hy)}\" x2=\"${f(ex)}\" y2=\"${f(ey)}\" stroke=\"${colorOf(i)}\" stroke-width=\"${act?2.6:1.1}\" stroke-linecap=\"round\"\/>`;\n    if(st.pul[i]>0) nips+=`<rect x=\"${f(rx-2)}\" y=\"${f(ry-3.5)}\" width=\"4\" height=\"7\" rx=\"1\" fill=\"url(#gNipH)\" stroke=\"#5e6e7e\" stroke-width=\".6\"\/>`; });\n  const outward=s=>Math.atan2((s.rim.x-s.hub.x)*wlFs.sc*AX, rFl(s)*Math.cos(s.hub.a-s.rim.a)-wlFs.rIn);\n  const holeOut=s=>opt.drill==='angle'?outward(s):Math.PI;\n  let drv='', note='';\n  const hd=st.heads[0];\n  if(hd){ const sp=S[hd.i], px=wlFs.c-wlFs.rIn, py=Y(sp.rim.x), engaging=hd.engaging&&!st.done;\n    const bang=outward(sp), hang=holeOut(sp);\n    const a0=opt.mach==='inline'&&hd.prev!=null?holeOut(S[hd.prev]):Math.PI, a1=opt.mach==='inline'?hang:Math.PI;   \/\/ only the Inline Lacer tilts sideways\n    const ang=a0+angDiff(a1,a0)*(1-Math.pow(1-st.idxP,3)), ux=Math.cos(ang), uy=Math.sin(ang), deg=ang*180\/Math.PI;\n    const d0=46*(1-hd.adv)-(hd.spinning?hd.thr*10:0);\n    const P=d=>[px+ux*d, py+uy*d];\n    const [tx,ty]=P(d0+2), [gx,gy]=P(250+reach()), spin=(st.pul[hd.i]*60)%360;\n    const on=engaging&&hd.spinning, led=on?'#119915':(engaging?'#d68910':'#8a9bb0'), braceDeg=Math.abs(sp.brace);\n    drv=guideG(gx,gy,deg+180,190)+`<g transform=\"translate(${f(tx)} ${f(ty)}) rotate(${f(deg+180)})\">${driverG({on,spin,led,name:'',ext:reach()})}<\/g>\n      <line x1=\"${f(px+110*Math.cos(hang))}\" y1=\"${f(py+110*Math.sin(hang))}\" x2=\"${f(px-120*Math.cos(hang))}\" y2=\"${f(py-120*Math.sin(hang))}\" stroke=\"${HOLE}\" stroke-width=\"1.4\" stroke-dasharray=\"7 3 2 3\"\/>\n      <g transform=\"rotate(${f(hang*180\/Math.PI-180)} ${f(px)} ${f(py)})\"><rect x=\"${f(px)}\" y=\"${f(py-2.6)}\" width=\"13\" height=\"5.2\" rx=\"1.5\" fill=\"url(#gNip)\" stroke=\"#5e6e7e\" stroke-width=\".8\"\/><rect x=\"${f(px-3.2)}\" y=\"${f(py-3.4)}\" width=\"3.6\" height=\"6.8\" rx=\"1.4\" fill=\"url(#gNip)\" stroke=\"#5e6e7e\" stroke-width=\".8\"\/><\/g>`;\n    const wedge=(a0,a1,rad,col)=>{ const A=[px+rad*Math.cos(a0),py+rad*Math.sin(a0)], B=[px+rad*Math.cos(a1),py+rad*Math.sin(a1)], d=angDiff(a1,a0);\n      return `<path d=\"M${f(px)} ${f(py)} L${f(A[0])} ${f(A[1])} A${rad} ${rad} 0 0 ${d>0?1:0} ${f(B[0])} ${f(B[1])} Z\" fill=\"${col}\" fill-opacity=\".18\" stroke=\"${col}\" stroke-width=\"1\"\/>`; };\n    const offD=engaging&&opt.mach!=='inline'&&opt.drill==='angle'&&braceDeg>0.05, bendD=opt.drill==='straight'&&braceDeg>0.05, inward=bang-Math.PI;\n    const lines=[];\n    if(offD) lines.push([`${braceDeg.toFixed(1)}\u00b0 off the nipple axis`,RED,1],['screwdriver, sideways (bracing)',RED,0]);\n    if(bendD) lines.push([`spoke bends ${braceDeg.toFixed(1)}\u00b0`,AMBER,1],['into the nipple, sideways (bracing)',AMBER,0]);\n    if(engaging&&!offD) lines.push(['screwdriver in line',GREEN,1]);\n    const ly=k=>sp.side==='L'?py+34+13*k:py-27-13*(lines.length-1-k);\n    note=(offD?`<line x1=\"${f(px)}\" y1=\"${f(py)}\" x2=\"${f(px+120)}\" y2=\"${f(py)}\" stroke=\"#7f93a8\" stroke-width=\"1.4\" stroke-dasharray=\"5 4\"\/>`+wedge(0,inward,70,RED):'')\n      +(bendD?wedge(0,inward,58,AMBER):'')\n      +lines.map(([s,col,b],k)=>`<text x=\"${f(px+40)}\" y=\"${f(ly(k))}\" font-size=\"${b?10.5:9}\" font-weight=\"${b?700:400}\" fill=\"${col}\" ${halo}>${s}<\/text>`).join('');\n  }\n  return `    <line x1=\"${f(x0-30)}\" y1=\"${yc}\" x2=\"${f(x1+30)}\" y2=\"${yc}\" stroke=\"#c9d3dd\" stroke-dasharray=\"4 4\"\/>\n    <rect x=\"${f(x0)}\" y=\"${f(yc-rimW\/2)}\" width=\"${f(x1-x0)}\" height=\"${f(rimW)}\" rx=\"${f(rimW\/2)}\" fill=\"url(#gRimBar)\" stroke=\"#111418\" stroke-width=\".8\"\/>\n    ${spokes}\n    <line x1=\"${wlFs.c}\" y1=\"${f(yL-14)}\" x2=\"${wlFs.c}\" y2=\"${f(yR+14)}\" stroke=\"#7f93a8\" stroke-width=\"4\" stroke-linecap=\"round\"\/>\n    <rect x=\"${wlFs.c-7}\" y=\"${f(Math.min(yL,yR)-3)}\" width=\"14\" height=\"${f(Math.abs(yR-yL)+6)}\" rx=\"5\" fill=\"url(#gRailV)\" stroke=\"#5e6e7e\" stroke-width=\"1\"\/>\n    <rect x=\"${f(wlFs.c-wlFs.rFlL)}\" y=\"${f(yL-3)}\" width=\"${f(2*wlFs.rFlL)}\" height=\"6\" rx=\"2\" fill=\"url(#gSteel)\" stroke=\"${GC.NDS}\" stroke-width=\"1.2\"\/><rect x=\"${f(wlFs.c-wlFs.rFlR)}\" y=\"${f(yR-3)}\" width=\"${f(2*wlFs.rFlR)}\" height=\"6\" rx=\"2\" fill=\"url(#gSteel)\" stroke=\"${GC.DS}\" stroke-width=\"1.2\"\/>\n    <text x=\"${f(wlFs.c+wlFs.rFlL+8)}\" y=\"${f(yL+3.5)}\" font-size=\"10\" font-weight=\"800\" fill=\"${GC.NDS}\">NDS<\/text><text x=\"${f(wlFs.c+wlFs.rFlR+8)}\" y=\"${f(yR+3.5)}\" font-size=\"10\" font-weight=\"800\" fill=\"${GC.DS}\">DS<\/text>\n    ${nips}${drv}${note}`;\n}\n\n\/* ---- rim drilling: a section through the hole of screwdriver 1, and the spoke bed seen from the hub ---- *\/\nconst holesByAngle=[...BASE.keys()].sort((a,b)=>norm(BASE[a].rimA)-norm(BASE[b].rimA)), holeNo=new Map(holesByAngle.map((h,k)=>[h,k]));\nfunction drillSVG(st){\n  const hd=st.heads[0], i=hd?hd.i:(st.done?plan.cycles[plan.cycles.length-1].sp[0]:plan.cycles[0].sp[0]);\n  const sp=S[i], s=sp.side==='L'?1:-1, ang=opt.drill==='angle', EX=2;          \/\/ NDS flange to the left; sideways angles \u00d72, as in the side view\n  const bd=Math.atan(EX*Math.tan(sp.brace*Math.PI\/180)), hb=ang?bd:0;\n  const Dp=opt.depth, W=opt.width\/2, k=Math.min(4.6, 140\/opt.width, 205\/(Dp+24)), ox=185, oy=42, a0=sp.rim.x;\n  const X=a=>ox+a*k, Y=r=>oy+r*k, ax=r=>a0-s*Math.tan(hb)*(r-(Dp-1));           \/\/ the hole axis: axial position (mm) at depth r\n  \/\/ sides run straight, then curve in to the spoke bed: a short radius on shallow and fat rims, a long taper on deep rims\n  const rr=Math.min(W-4, Dp*.35), bw=W-rr, yT=Dp-Math.max(rr, .55*(Dp-24)+rr), Wi=W-1.6, bwi=Math.max(1,bw-1.6), yTi=Math.max(9.8,yT);\n  const outer=`M${f(X(-W))} ${f(Y(0))} L${f(X(-Wi))} ${f(Y(0))} L${f(X(-Wi))} ${f(Y(6))} L${f(X(Wi))} ${f(Y(6))} L${f(X(Wi))} ${f(Y(0))} L${f(X(W))} ${f(Y(0))} L${f(X(W))} ${f(Y(yT))} Q${f(X(W))} ${f(Y(Dp))} ${f(X(bw))} ${f(Y(Dp))} L${f(X(-bw))} ${f(Y(Dp))} Q${f(X(-W))} ${f(Y(Dp))} ${f(X(-W))} ${f(Y(yT))} Z`\n    +`M${f(X(-Wi))} ${f(Y(0))} h${f(k)} v${f(1.3*k)} h${f(-k)} Z M${f(X(Wi))} ${f(Y(0))} h${f(-k)} v${f(1.3*k)} h${f(k)} Z`;\n  const cavity=`M${f(X(-Wi))} ${f(Y(7.8))} L${f(X(Wi))} ${f(Y(7.8))} L${f(X(Wi))} ${f(Y(yTi))} Q${f(X(Wi))} ${f(Y(Dp-2))} ${f(X(bwi))} ${f(Y(Dp-2))} L${f(X(-bwi))} ${f(Y(Dp-2))} Q${f(X(-Wi))} ${f(Y(Dp-2))} ${f(X(-Wi))} ${f(Y(yTi))} Z`;\n  const cut=(r0,r1,hw)=>`<polygon points=\"${[[r0,-hw],[r0,hw],[r1,hw],[r1,-hw]].map(([r,w])=>`${f(X(ax(r)+w\/Math.cos(hb)))},${f(Y(r))}`).join(' ')}\" fill=\"#EEF2F8\"\/>`;\n  const G=[X(ax(Dp-2)),Y(Dp-2)], hl=(nip.len-nip.head)*k, dirH=[-s*Math.sin(hb),Math.cos(hb)], E=[G[0]+dirH[0]*hl,G[1]+dirH[1]*hl];\n  const nippleS=`<g transform=\"translate(${f(G[0])} ${f(G[1])}) rotate(${f(s*hb*180\/Math.PI)})\">\n      <rect x=\"${f(-1.7*k)}\" y=\"0\" width=\"${f(3.4*k)}\" height=\"${f(hl)}\" rx=\"1.5\" fill=\"url(#gNipH)\" stroke=\"#5e6e7e\" stroke-width=\".7\"\/>\n      <rect x=\"${f(-2*k)}\" y=\"${f(-nip.head*k)}\" width=\"${f(4*k)}\" height=\"${f(nip.head*k)}\" rx=\"2.5\" fill=\"url(#gNipH)\" stroke=\"#5e6e7e\" stroke-width=\".7\"\/>\n      <rect x=\"${f(-.4*k)}\" y=\"${f(-nip.head*k)}\" width=\"${f(.8*k)}\" height=\"${f(.9*k)}\" fill=\"#4a525b\"\/><\/g>`;\n  const spokeS=`<g transform=\"translate(${f(E[0])} ${f(E[1])}) rotate(${f(s*bd*180\/Math.PI)})\"><rect x=\"${f(-k)}\" y=\"-2\" width=\"${f(2*k)}\" height=\"62\" fill=\"url(#gDarkV)\" stroke=\"#2a3139\" stroke-width=\".6\"\/><\/g>`;\n  const C=[X(a0),Y(Dp-1)];\n  \/\/ the screwdriver bit, sideways: radial on the ECL \/ ISL and the CT, along the hole on the Inline Lacer; it must pass the access hole\n  const db=Math.atan(EX*Math.tan(drvBrace(i)*Math.PI\/180)), up=[s*Math.sin(db),-Math.cos(db)];\n  const Ht=[G[0]-dirH[0]*nip.head*k, G[1]-dirH[1]*nip.head*k], bl=(Ht[1]-Math.max(Y(-5),36))\/Math.cos(db);   \/\/ the shank runs out above the rim, below the title\n  const miss=missAt(i,0), hit=miss>CLEAR, hd2=(Ht[1]-Y(6.9))\/Math.cos(db), Hb=[Ht[0]+up[0]*hd2, Ht[1]+up[1]*hd2];\n  const bitS=`<g transform=\"translate(${f(Ht[0])} ${f(Ht[1])}) rotate(${f(s*db*180\/Math.PI)})\"><rect x=\"${f(-BIT\/2*k)}\" y=\"${f(-bl)}\" width=\"${f(BIT*k)}\" height=\"${f(bl)}\" fill=\"url(#gSteelH)\" stroke=\"${hit?RED:'#5e6e7e'}\" stroke-width=\"${hit?1.4:.7}\" opacity=\".95\"\/><\/g>`\n    +(hit?`<circle cx=\"${f(Hb[0])}\" cy=\"${f(Hb[1])}\" r=\"${f(Math.max(7,ACCESS\/2*k+3))}\" fill=\"none\" stroke=\"${RED}\" stroke-width=\"2\"\/>`:'');\n  const wedge=(P0,a1,rad,col)=>{ const A=[P0[0],P0[1]+rad], B=[P0[0]-s*rad*Math.sin(a1),P0[1]+rad*Math.cos(a1)];\n    return `<path d=\"M${f(P0[0])} ${f(P0[1])} L${f(A[0])} ${f(A[1])} A${rad} ${rad} 0 0 ${s>0?1:0} ${f(B[0])} ${f(B[1])} Z\" fill=\"${col}\" fill-opacity=\".16\" stroke=\"${col}\" stroke-width=\"1\"\/>`; };\n  const side=sp.side==='L'?'NDS':'DS', bTxt=sp.brace.toFixed(1);\n  const lab=(x,y,t,anchor='end',col='#56687f',w=400,size=9.5)=>`<text x=\"${f(x)}\" y=\"${f(y)}\" font-size=\"${size}\" font-weight=\"${w}\" fill=\"${col}\" text-anchor=\"${anchor}\" ${halo}>${t}<\/text>`;\n  const ys=[Y(1.5)]; ys.push(Math.max(Y(7.4),ys[0]+11)); ys.push(Math.max(Y(Dp-.6),ys[1]+11));   \/\/ left labels at least 11 px apart\n  const lx=X(-W)-8, ly=Math.max(Y(Dp+12),ys[2]+24);\n  const angLab=ang\n    ? wedge(C,hb,86,HOLE)+lab(lx,ly,`drilled ${bTxt}\u00b0`,'end',HOLE,700,10.5)+lab(lx,ly+13,`towards the ${side} flange`,'end',HOLE)\n    : wedge(E,bd,46,AMBER)+lab(lx,ly,`spoke bends ${bTxt}\u00b0`,'end',AMBER,700,10.5)+lab(lx,ly+13,'leaving the nipple','end',AMBER);\n  const section=`<text x=\"10\" y=\"16\" font-size=\"11\" font-weight=\"700\" fill=\"#1F5F7C\" letter-spacing=\".08em\">SECTION THROUGH THE HOLE<\/text>\n    <text x=\"10\" y=\"29\" font-size=\"9\" fill=\"#787979\">${side} spoke \u00b7 sideways angles drawn \u00d72<\/text>\n    <line x1=\"${f(C[0])}\" y1=\"${f(Y(-3))}\" x2=\"${f(C[0])}\" y2=\"258\" stroke=\"#9aa5b0\" stroke-width=\"1\" stroke-dasharray=\"4 4\"\/>\n    <path fill-rule=\"evenodd\" d=\"${outer} ${cavity}\" fill=\"url(#gAlu)\" stroke=\"#5d6874\" stroke-width=\".9\"\/>\n    ${cut(5.6,8.2,ACCESS\/2)}${cut(Dp-2.3,Dp+.3,2.2)}\n    ${spokeS}${nippleS}${bitS}\n    <line x1=\"${f(X(ax(-3)))}\" y1=\"${f(Y(-3))}\" x2=\"${f(X(ax(Dp+22)))}\" y2=\"${f(Y(Dp+22))}\" stroke=\"${HOLE}\" stroke-width=\"1.3\" stroke-dasharray=\"7 3 2 3\"\/>\n    ${angLab}\n    ${lab(X(-W)-8,ys[0],'tyre side')}${lab(X(-W)-8,ys[1],'tyre bed')}${lab(X(-W)-8,ys[2],'spoke bed')}${lab(X(W)+8,Y(Dp+5),'nipple','start')}\n    ${lab(X(W)+8,Y(7.4),'access hole','start')}${lab(X(W)+8,Y(7.4)+13,hit?`bit ${miss.toFixed(1)} mm off`:'bit passes','start',hit?RED:GREEN,700)}${lab(C[0]+(s>0?5:-5),250,'radial',s>0?'start':'end','#787979')}`;\n  \/\/ the spoke bed, unrolled: a window of holes round the one at screwdriver 1, sliding as the wheel indexes\n  const x0=330, x1=800, y0=82, y1=170, cy=(y0+y1)\/2, sx=52, KT=230;\n  const kc=holeNo.get(i), kp=hd&&hd.prev!=null?holeNo.get(hd.prev):kc; let dk=kc-kp; if(dk>N\/2) dk-=N; if(dk<-N\/2) dk+=N;\n  const centre=kp+dk*(1-Math.pow(1-st.idxP,2)), xc=(x0+x1)\/2, at=new Set(st.heads.filter(Boolean).map(h=>h.i));\n  let holes='', marks='';\n  for(let q=Math.floor(centre)-6;q<=Math.ceil(centre)+6;q++){ const j=holesByAngle[((q%N)+N)%N], sj=S[j], xp=xc+(q-centre)*sx, yh=cy+sj.rim.x*Math.min(2.6,80\/opt.width);\n    const vx=-Math.tan(spokeTilt(j))*KT, vy=(sj.side==='L'?-1:1)*Math.tan(sj.brace*Math.PI\/180)*KT, va=Math.atan2(vy,vx)*180\/Math.PI, vl=Math.hypot(vx,vy);\n    holes+=ang\n      ? `<ellipse cx=\"${f(xp)}\" cy=\"${f(yh)}\" rx=\"8\" ry=\"6\" transform=\"rotate(${f(va)} ${f(xp)} ${f(yh)})\" fill=\"#0b0e11\" stroke=\"#c9d0d7\" stroke-width=\"1.6\"\/>\n         <line x1=\"${f(xp)}\" y1=\"${f(yh)}\" x2=\"${f(xp+vx*(vl-6)\/vl)}\" y2=\"${f(yh+vy*(vl-6)\/vl)}\" stroke=\"#fff\" stroke-width=\"4.4\" stroke-linecap=\"round\"\/>\n         <line x1=\"${f(xp)}\" y1=\"${f(yh)}\" x2=\"${f(xp+vx*(vl-6)\/vl)}\" y2=\"${f(yh+vy*(vl-6)\/vl)}\" stroke=\"${GC[sj.group]}\" stroke-width=\"2.4\" stroke-linecap=\"round\"\/>\n         <polygon points=\"0,0 -8,-4.5 -8,4.5\" transform=\"translate(${f(xp+vx)} ${f(yh+vy)}) rotate(${f(va)})\" fill=\"${GC[sj.group]}\" stroke=\"#fff\" stroke-width=\"1\"\/>`\n      : `<circle cx=\"${f(xp)}\" cy=\"${f(yh)}\" r=\"6.5\" fill=\"#0b0e11\" stroke=\"#c9d0d7\" stroke-width=\"1.6\"\/><circle cx=\"${f(xp)}\" cy=\"${f(yh)}\" r=\"1.6\" fill=\"${GC[sj.group]}\"\/>`;\n    if(xp>x0+6&&xp<x1-6) marks+=`<circle cx=\"${f(xp)}\" cy=\"${y1+12}\" r=\"4\" fill=\"${GC[sj.group]}\"\/>`;\n    if(at.has(j)) marks+=`<text x=\"${f(xp)}\" y=\"${y0-8}\" font-size=\"9.5\" font-weight=\"700\" fill=\"#005389\" text-anchor=\"middle\">${plan.off.length>1?'screwdriver '+(st.heads.findIndex(h=>h&&h.i===j)+1):'screwdriver'}<\/text>`;\n    if(at.has(j)) holes+=`<circle cx=\"${f(xp)}\" cy=\"${f(yh)}\" r=\"15\" fill=\"none\" stroke=\"#B3E9FF\" stroke-width=\"2.4\"\/>`; }\n  const strip=`<text x=\"${x0}\" y=\"16\" font-size=\"11\" font-weight=\"700\" fill=\"#1F5F7C\" letter-spacing=\".08em\">SPOKE BED, SEEN FROM THE HUB<\/text>\n    <text x=\"${x0}\" y=\"29\" font-size=\"9\" fill=\"#787979\">${ang?'each hole drilled along its spoke: the arrow shows the direction (angles exaggerated)':'every hole drilled radially, straight towards the hub'}<\/text>\n    <rect x=\"${x0}\" y=\"${y0}\" width=\"${x1-x0}\" height=\"${y1-y0}\" rx=\"8\" fill=\"url(#gRimBar)\"\/>\n    <g clip-path=\"url(#stripClip)\">${holes}<\/g>${marks}\n    <text x=\"${x0+2}\" y=\"${y0-8}\" font-size=\"9.5\" font-weight=\"800\" fill=\"${GC.NDS}\">\u2191 NDS flange<\/text><text x=\"${x0+2}\" y=\"${y1+30}\" font-size=\"9.5\" font-weight=\"800\" fill=\"${GC.DS}\">\u2193 DS flange<\/text>\n    <text x=\"${x0}\" y=\"${y1+48}\" font-size=\"9\" fill=\"#787979\">${ang?'arrows lean along the rim (leading or trailing) and towards their own flange (bracing)':'the spokes bend into their nipples, see the section'}<\/text>`;\n  return section+strip;\n}\nfunction updateSpecs(){\n  const ang=opt.drill==='angle', tilt=Math.max(...S.map((_,i)=>Math.abs(spokeTilt(i))))*180\/Math.PI;\n  const br=sd=>S.find(x=>x.side===sd).brace.toFixed(1);\n  const mx=maxMiss();\n  const items=[['Wheel',`${SIZES[opt.bsd][0]} (${opt.bsd} mm bead seat) \u00b7 ${N} holes \u00b7 ERD ${WH.erd} mm`],['Rim',`${opt.width} \u00d7 ${opt.depth} mm, double wall${WH.off>0.05?` \u00b7 holes \u00b1${WH.off.toFixed(1)} mm off-centre`:''}`],\n    ['Hub',`${WH.hub.name} \u00b7 flanges \u00d8${opt.dL} \/ \u00d8${opt.dR} mm \u00b7 OLD ${opt.old} mm, flanges ${WH.fcL.toFixed(0)} \/ ${WH.fcR.toFixed(0)} mm from centre \u00b7 ${WH.hub.cross}-cross`],\n    ['Hole direction',ang?'along each spoke':'radial'],\n    ['In the wheel plane',ang?`\u00b1${tilt.toFixed(1)}\u00b0 (leading \/ trailing)`:'0\u00b0'],\n    ['Sideways (bracing)',ang?`${br('L')}\u00b0 to NDS \u00b7 ${br('R')}\u00b0 to DS`:'0\u00b0'],\n    [ang?'Hole angle':'Spoke bend at the nipple',`up to ${maxOf(spokeAngle).toFixed(1)}\u00b0`],\n    ['Bit through the tyre bed',`${reachMM().toFixed(0)} mm reach \u00b7 ${mx>CLEAR?`up to ${mx.toFixed(1)} mm off the access hole`:'passes the access hole'} (\u00d8${BIT} bit, \u00d8${ACCESS} hole, illustrative)`]];\n  document.getElementById('specs').innerHTML=items.map(([a,b])=>`<span class=\"spec\"><b>${a}<\/b>${b}<\/span>`).join('');\n}\n\n\/* ---- text under the animation ---- *\/\nconst maxOf=fn=>Math.max(...S.map((_,i)=>fn(i)));\nfunction updateText(){\n  const maxBend=maxOf(spokeAngle), m=straightM(opt.mach)?'ecl':opt.mach, ang=opt.drill==='angle';\n  const off=Math.max(...plan.off.map((_,h)=>Math.max(...headSpokes(h).map(i=>offAngle(i,h)))));\n  const set=(id,txt,cls)=>{ const e=document.getElementById(id); e.textContent=txt; if(cls!=null) e.className='v '+cls; };\n  if(off>0.05) set('c-drv',`Up to ${off.toFixed(1)}\u00b0 off the nipple axis`,'bad');\n  else set('c-drv','In line with every nipple','ok');\n  set('c-drv-t',{\n    'ecl\/straight':'Radial holes: the nipples sit radially, like the screwdriver.',\n    'ecl\/angle':'The screwdriver works radially, the nipples point along their spokes.',\n    'inline\/straight':'Radial holes: the angled screwdriver simply works straight.',\n    'inline\/angle':off>0.05?'The screwdriver tilts onto each hole, but this wheel needs more than its 15\u00b0 radial \/ 25\u00b0 axial range.':'The screwdriver tilts onto each hole before it engages.',\n    'ct\/straight':'Both screwdrivers are set radially for radial holes.',\n    'ct\/angle':'Set to the spoke angle in the wheel plane, but they do not tilt sideways (bracing).',\n  }[m+'\/'+opt.drill]);\n  if(!ang) { set('c-bend',`Bends up to ${maxBend.toFixed(1)}\u00b0`,'warn'); set('c-bend-t','The nipple sits radially, so the spoke bends where it enters the nipple.'); }\n  else { set('c-bend','Straight into the nipple','ok'); set('c-bend-t','Each hole is drilled along its spoke line: spoke and nipple are in line.'); }\n  updateSpecs();\n  const fit=evaluate(opt.mach);\n  set('c-mac',SPEC[opt.mach].full,fit.verdict==='no'?'bad':fit.verdict==='opt'?'warn':'');\n  set('c-mac-t',{\n    ecl:`Straight engagement: one screwdriver works radially. One spoke per index step: ${plan.cycles.length} steps for ${N} spokes.`,\n    inline:`One angled screwdriver tilts onto every hole, in the wheel plane and sideways. One spoke per index step: ${plan.cycles.length} steps for ${N} spokes.`,\n    ct:`Two screwdrivers screw on a leading and a trailing spoke at a time: ${plan.cycles.length} index steps for ${N} spokes. Their angle in the wheel plane is set per wheel; for offset-drilled holes they move up and down by about 1 cm.`,\n  }[m]+(fit.verdict==='no'?` Not for this wheel: ${fit.why}.`:fit.verdict==='opt'?` For this wheel: ${fit.why}.`:''));\n  document.getElementById('explain').textContent={\n    'ecl\/straight':`The holes are drilled radially, so every nipple sits radially and the straight screwdriver meets it in line. The spoke enters the nipple at an angle of up to ${maxBend.toFixed(1)}\u00b0.`,\n    'ecl\/angle':`Each hole is drilled along its spoke, so spoke and nipple are in line. The straight screwdriver works radially and meets the nipples up to ${off.toFixed(1)}\u00b0 off their axis.`,\n    'inline\/straight':`With radially drilled holes the Inline Lacer's angled screwdriver simply works straight: it meets every nipple in line. The spoke still enters the nipple at an angle of up to ${maxBend.toFixed(1)}\u00b0.`,\n    'inline\/angle':off>0.05?`Each hole is drilled along its spoke and the Inline Lacer's screwdriver tilts onto every hole, but this wheel's spokes lean more than its 15\u00b0 radial \/ 25\u00b0 axial range, so it still meets the nipples up to ${off.toFixed(1)}\u00b0 off.`:`Each hole is drilled along its spoke, and the Inline Lacer's screwdriver tilts onto every hole: spoke, nipple and screwdriver are in line.`,\n    'ct\/straight':`The holes are drilled radially and both CT screwdrivers are set radially, so each meets its nipple in line, two spokes at a time. The spokes enter the nipples at an angle of up to ${maxBend.toFixed(1)}\u00b0.`,\n    'ct\/angle':`Each hole is drilled along its spoke. The CT screwdrivers are set to the spoke angle in the wheel plane, but they do not tilt sideways, so they meet the nipples up to ${off.toFixed(1)}\u00b0 off their axis, which is the spokes' bracing angle.`,\n  }[m+'\/'+opt.drill]+deepText();\n  renderCompare();\n}\n\n\/* ---- which machine fits this wheel: published ranges, screwdriver on the nipple, bit through the tyre bed ---- *\/\nfunction evaluate(m){\n  const keep={mach:opt.mach, plan}; opt.mach=m; buildPlan();\n  const off=Math.max(...plan.off.map((_,h)=>Math.max(...headSpokes(h).map(i=>offAngle(i,h))))), miss=maxMiss(), steps=plan.cycles.length;\n  opt.mach=keep.mach; plan=keep.plan;\n  const sp=SPEC[m], inch=SIZES[opt.bsd][1], res={off, miss, steps, checks:{}};\n  const chk=(k,st,txt)=>res.checks[k]={st,txt};\n  chk('size', inch>=sp.inch[0]&&inch<=sp.inch[1]?'ok':'no', `${sp.inch[0]}\u2013${sp.inch[1]}\"`);\n  chk('width', opt.width<=sp.w?'ok':(sp.optW&&opt.width<=sp.optW?'opt':'no'), opt.width<=sp.w?`up to ${sp.w} mm`:sp.optW&&opt.width<=sp.optW?`Fat option, up to ${sp.optW} mm`:`max ${sp.optW||sp.w} mm`);\n  const hOpt=sp.optH&&sp.optH.find(h=>opt.depth<=h);\n  chk('depth', opt.depth<=sp.h?'ok':(hOpt?'opt':'no'), opt.depth<=sp.h?`up to ${sp.h} mm`:hOpt?`High option, up to ${hOpt} mm`:`max ${sp.optH?sp.optH[sp.optH.length-1]:sp.h} mm`);\n  const why=[];\n  if(res.checks.size.st==='no') why.push(`wheel size ${SIZES[opt.bsd][0]} outside ${sp.inch[0]}\u2013${sp.inch[1]}\"`);\n  if(res.checks.width.st!=='ok') why.push(res.checks.width.st==='opt'?`rim width ${opt.width} mm needs the Fat option`:`rim width ${opt.width} mm, max ${sp.optW||sp.w} mm`);\n  if(res.checks.depth.st!=='ok') why.push(res.checks.depth.st==='opt'?`rim depth ${opt.depth} mm needs the High ${hOpt} mm option`:`rim depth ${opt.depth} mm, max ${sp.optH?sp.optH[sp.optH.length-1]:sp.h} mm`);\n  const sts=Object.values(res.checks).map(c=>c.st);\n  res.verdict=sts.includes('no')?'no':sts.includes('opt')?'opt':'ok'; res.why=why.join(', ');\n  return res;\n}\nfunction renderCompare(){\n  const dot=(st,txt)=>`<span class=\"st ${st}\"><i><\/i>${txt}<\/span>`;\n  const rows=Object.keys(SPEC).map(m=>{ const e=evaluate(m), sp=SPEC[m], c=e.checks;\n    const fitTxt=e.verdict==='ok'?dot('ok','fits'):e.verdict==='opt'?dot('opt','with option'):dot('no','not in range');\n    const engage=e.off>0.05?dot('no',`up to ${e.off.toFixed(1)}\u00b0 off`):dot('ok','in line');\n    const bit=e.miss>CLEAR?dot('no',`${e.miss.toFixed(1)} mm off the hole`):dot('ok','passes');\n    return `<tr data-m=\"${m}\" class=\"${m===opt.mach?'sel':''}\"><td><b>${sp.full}<\/b><small>${sp.drv}<\/small><\/td><td>${fitTxt}${e.why?`<small>${e.why}<\/small>`:''}<\/td>\n      <td>${dot(c.size.st,SIZES[opt.bsd][0])}<small>${c.size.txt}<\/small><\/td><td>${dot(c.width.st,opt.width+' mm')}<small>${c.width.txt}<\/small><\/td><td>${dot(c.depth.st,opt.depth+' mm')}<small>${c.depth.txt}<\/small><\/td>\n      <td>${engage}<small>${opt.drill==='angle'?'angle-drilled holes':'radial holes'}<\/small><\/td><td>${bit}<small>${reachMM().toFixed(0)} mm reach<\/small><\/td>\n      <td>${e.steps}<small>index steps<\/small><\/td><td>${sp.cap}<small>wheels \/ hour<\/small><\/td><\/tr>`; }).join('');\n  const tb=document.getElementById('cmp');\n  tb.innerHTML=`<thead><tr><th>Machine<\/th><th>For this wheel<\/th><th>Wheel size<\/th><th>Rim width<\/th><th>Rim depth<\/th><th>Screwdriver on the nipple<\/th><th>Bit through the tyre bed<\/th><th>Cycle<\/th><th>Capacity<\/th><\/tr><\/thead><tbody>${rows}<\/tbody>`;\n}\ndocument.getElementById('cmp').addEventListener('click',e=>{ const tr=e.target.closest('tr[data-m]'); if(!tr) return; const was=MACH[opt.mach].heads; opt.mach=tr.dataset.m;\n  if(MACH[opt.mach].heads!==was){ buildPlan(); t=0; } syncButtons(); updateText(); draw(); });\nfunction deepText(){ const mx=maxMiss(), rm=reachMM().toFixed(0);\n  if(mx>CLEAR) return ` On this ${opt.depth} mm deep rim the bit reaches about ${rm} mm through the rim to the nipple; at this angle it runs up to ${mx.toFixed(1)} mm off the access hole in the tyre bed.`;\n  if(opt.depth>=45) return ` On this ${opt.depth} mm deep rim the bit reaches about ${rm} mm through the rim and passes straight through the access hole.`;\n  return ''; }\n\n\/* ---- clock, controls, embedding ---- *\/\nconst side=document.getElementById('side'), dish=document.getElementById('dish'), drillA=document.getElementById('drillA'), drillB=document.getElementById('drillB'), playBtn=document.getElementById('play');\nlet t=0, playing=!matchMedia('(prefers-reduced-motion: reduce)').matches, visible=true, last=performance.now();\nfunction draw(){ const st=state(t); side.innerHTML=sideSVG(st); dish.innerHTML=dishSVG(st); { const h=drillSVG(st); drillA.innerHTML=h; drillB.innerHTML=h; }\n  const st2=st.done?`<strong>All ${N} spokes<\/strong> screwed on`:`Spokes screwed on: <strong>${st.loaded} \/ ${N}<\/strong> \u00b7 index step <strong>${Math.max(1,st.steps)} \/ ${plan.cycles.length}<\/strong>`;\n  const fit=evaluate(opt.mach);\n  document.getElementById('status').innerHTML=st2+(fit.verdict==='no'?` \u00b7 <span class=\"warn-line\">${SPEC[opt.mach].full}: not in range for this wheel (${fit.why})<\/span>`:''); }\nfunction frame(now){ const dt=Math.min(0.1,(now-last)\/1000); last=now;\n  if(playing&&visible){ t+=dt*opt.speed; if(t>=plan.total) t=0; draw(); }\n  requestAnimationFrame(frame); }\nfunction syncButtons(){ document.querySelectorAll('.seg[data-key]').forEach(g=>{ const k=g.dataset.key; g.querySelectorAll('button').forEach(b=>b.setAttribute('aria-pressed', String(String(opt[k])===b.dataset.v))); });\n  playBtn.textContent=playing?'Pause':'Play';\n  for(const key of ['depth','width','dL','dR','old']){ document.getElementById('r-'+key).value=opt[key]; document.getElementById('o-'+key).textContent=opt[key]+' mm'; } }\nfor(const key of ['depth','width','dL','dR','old']) document.getElementById('r-'+key).addEventListener('input',e=>{ opt[key]=+e.target.value; applyWheel(); syncButtons(); updateText(); draw(); });\ndocument.querySelectorAll('.seg[data-key]').forEach(g=>g.addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b) return; const k=g.dataset.key;\n  const was=MACH[opt.mach].heads; opt[k]=(k==='speed'||k==='bsd')?+b.dataset.v:b.dataset.v;\n  if(k==='bsd') applyWheel();\n  if(k==='mach'&&MACH[opt.mach].heads!==was){ buildPlan(); t=0; }\n  if(k==='hub'){ const H=HUBS[opt.hub]; opt.dL=opt.dR=H.d; opt.old=H.old; applyWheel(); }   \/\/ a hub type loads its typical flanges and width\n  syncButtons(); updateText(); draw(); }));\nplayBtn.addEventListener('click',()=>{ playing=!playing; syncButtons(); });\ndocument.getElementById('restart').addEventListener('click',()=>{ t=0; draw(); });\nif('IntersectionObserver' in window) new IntersectionObserver(es=>{ visible=es[0].isIntersecting; }).observe(side);\n\/\/ options from the embed URL: ?bare (no heading), ?rim=angle, ?machine=ecl|isl|inline|ct, ?size=16|20|24|26|27.5|28|29|32, ?hub=std|enviolo|motor, ?depth=20\u201390, ?width=20\u2013100, ?fnds=30\u2013200 ?fds=30\u2013200 (flange \u00d8), ?old=100\u2013200 (hub width)\nconst q=new URLSearchParams(location.search);\nif(q.has('bare')) document.body.classList.add('bare');\nif(q.get('rim')==='angle') opt.drill='angle';\n{ const hq=(q.get('hub')||'').toLowerCase(); if(hq==='igh'||hq==='enviolo') opt.hub='igh'; if(hq==='motor'||hq==='ebike') opt.hub='motor';\n  const num=(k,lo,hi)=>{ const v=+q.get(k); if(q.has(k)&&v>=lo&&v<=hi) opt[k]=Math.round(v); }; num('depth',20,90); num('width',20,100);\n  opt.dL=opt.dR=HUBS[opt.hub].d; opt.old=HUBS[opt.hub].old;\n  for(const [k,key,lo,hi] of [['fnds','dL',30,200],['fds','dR',30,200],['old','old',100,200]]){ const v=+q.get(k); if(q.has(k)&&v>=lo&&v<=hi) opt[key]=Math.round(v); } }\n{ const mq=(q.get('machine')||'').toLowerCase(); if(mq==='isl') opt.mach='isl'; if(mq==='inline'||mq==='il') opt.mach='inline'; if(mq==='ct'||mq==='twin') opt.mach='ct';\n  const sz={'16':305,'20':406,'24':507,'26':559,'27.5':584,'28':622,'29':622,'32':686}[q.get('size')]; if(sz) opt.bsd=sz; }\n\/\/ tell a parent page our height, so its iframe can fit the content\nconst post=()=>{ try{ parent.postMessage({type:'hm-lacing-height', height:document.documentElement.scrollHeight},'*'); }catch(e){} };\nnew ResizeObserver(post).observe(document.body);\napplyWheel(); buildPlan(); t=plan.cycles[0].dur*0.6; syncButtons(); updateText(); draw(); requestAnimationFrame(frame);\n<\/script>\n<\/body>\n<\/html>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-862dc85 elementor-widget elementor-widget-spacer\" data-id=\"862dc85\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Wheel lacing Calculator Lacing Screwdriver Explainer Wheel lacing Straight or angled screwdriver Rims are drilled either radially or along each spoke line. See how the screwdriver of the ECL, ISL, Inline Lacer and CT Twin Lacer meets the nipple in each case, and which machine fits your wheel. Rim drillingStraight-drilledAngle-drilled MachineECLISLInline LacerCT Twin Lacer Wheel [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"content-type":"","inline_featured_image":false,"footnotes":""},"class_list":["post-50395","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.6 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Wheel Lacing Calculator - Holland Mechanics<\/title>\n<meta name=\"description\" content=\"The Holland Mechanics wheel lacing calculator measure every angle and spoke for optimal results, You will be able to see what is possible.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/hollandmechanics.com\/en\/wheel-lacing-calculator\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Wheel Lacing Calculator - Holland Mechanics\" \/>\n<meta property=\"og:description\" content=\"The Holland Mechanics wheel lacing calculator measure every angle and spoke for optimal results, You will be able to see what is possible.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/hollandmechanics.com\/en\/wheel-lacing-calculator\/\" \/>\n<meta property=\"og:site_name\" content=\"Holland Mechanics\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-09T15:03:31+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"19 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/wheel-lacing-calculator\\\/\",\"url\":\"https:\\\/\\\/hollandmechanics.com\\\/wheel-lacing-calculator\\\/\",\"name\":\"Wheel Lacing Calculator - Holland Mechanics\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/#website\"},\"datePublished\":\"2026-10-09T14:15:29+00:00\",\"dateModified\":\"2026-10-09T15:03:31+00:00\",\"description\":\"The Holland Mechanics wheel lacing calculator measure every angle and spoke for optimal results, You will be able to see what is possible.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/wheel-lacing-calculator\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/hollandmechanics.com\\\/wheel-lacing-calculator\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/wheel-lacing-calculator\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/hollandmechanics.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Wheel Lacing Calculator\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/#website\",\"url\":\"https:\\\/\\\/hollandmechanics.com\\\/\",\"name\":\"Holland Mechanics\",\"description\":\"Go to homepage\",\"publisher\":{\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/hollandmechanics.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/#organization\",\"name\":\"Holland Mechanics\",\"url\":\"https:\\\/\\\/hollandmechanics.com\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/hollandmechanics.com\\\/wp-content\\\/uploads\\\/2023\\\/01\\\/cropped-HMLogo-1-e1672838939593.png\",\"contentUrl\":\"https:\\\/\\\/hollandmechanics.com\\\/wp-content\\\/uploads\\\/2023\\\/01\\\/cropped-HMLogo-1-e1672838939593.png\",\"width\":200,\"height\":70,\"caption\":\"Holland Mechanics\"},\"image\":{\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.linkedin.com\\\/company\\\/holland-mechanics-b.v.\\\/\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Wheel Lacing Calculator - Holland Mechanics","description":"The Holland Mechanics wheel lacing calculator measure every angle and spoke for optimal results, You will be able to see what is possible.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/hollandmechanics.com\/en\/wheel-lacing-calculator\/","og_locale":"en_US","og_type":"article","og_title":"Wheel Lacing Calculator - Holland Mechanics","og_description":"The Holland Mechanics wheel lacing calculator measure every angle and spoke for optimal results, You will be able to see what is possible.","og_url":"https:\/\/hollandmechanics.com\/en\/wheel-lacing-calculator\/","og_site_name":"Holland Mechanics","article_modified_time":"2026-10-09T15:03:31+00:00","twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"19 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/hollandmechanics.com\/wheel-lacing-calculator\/","url":"https:\/\/hollandmechanics.com\/wheel-lacing-calculator\/","name":"Wheel Lacing Calculator - Holland Mechanics","isPartOf":{"@id":"https:\/\/hollandmechanics.com\/#website"},"datePublished":"2026-10-09T14:15:29+00:00","dateModified":"2026-10-09T15:03:31+00:00","description":"The Holland Mechanics wheel lacing calculator measure every angle and spoke for optimal results, You will be able to see what is possible.","breadcrumb":{"@id":"https:\/\/hollandmechanics.com\/wheel-lacing-calculator\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/hollandmechanics.com\/wheel-lacing-calculator\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/hollandmechanics.com\/wheel-lacing-calculator\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/hollandmechanics.com\/"},{"@type":"ListItem","position":2,"name":"Wheel Lacing Calculator"}]},{"@type":"WebSite","@id":"https:\/\/hollandmechanics.com\/#website","url":"https:\/\/hollandmechanics.com\/","name":"Holland Mechanics","description":"Go to homepage","publisher":{"@id":"https:\/\/hollandmechanics.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/hollandmechanics.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/hollandmechanics.com\/#organization","name":"Holland Mechanics","url":"https:\/\/hollandmechanics.com\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/hollandmechanics.com\/#\/schema\/logo\/image\/","url":"https:\/\/hollandmechanics.com\/wp-content\/uploads\/2023\/01\/cropped-HMLogo-1-e1672838939593.png","contentUrl":"https:\/\/hollandmechanics.com\/wp-content\/uploads\/2023\/01\/cropped-HMLogo-1-e1672838939593.png","width":200,"height":70,"caption":"Holland Mechanics"},"image":{"@id":"https:\/\/hollandmechanics.com\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.linkedin.com\/company\/holland-mechanics-b.v.\/"]}]}},"_links":{"self":[{"href":"https:\/\/hollandmechanics.com\/en\/wp-json\/wp\/v2\/pages\/50395","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hollandmechanics.com\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/hollandmechanics.com\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/hollandmechanics.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hollandmechanics.com\/en\/wp-json\/wp\/v2\/comments?post=50395"}],"version-history":[{"count":22,"href":"https:\/\/hollandmechanics.com\/en\/wp-json\/wp\/v2\/pages\/50395\/revisions"}],"predecessor-version":[{"id":50428,"href":"https:\/\/hollandmechanics.com\/en\/wp-json\/wp\/v2\/pages\/50395\/revisions\/50428"}],"wp:attachment":[{"href":"https:\/\/hollandmechanics.com\/en\/wp-json\/wp\/v2\/media?parent=50395"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}