{
  "note": "Public recipe + worked-example corpus. Filter `entries` by `intent` (cad / mesh / previewer / math / io / scene) for the blind-LLM intent gate, then run `Utils.recipes.run(id)` (recipes) or `Utils.examples.run(symbol)` (examples). intent:\"cad\" holds the curated CAD-kernel (execOpenScad) recipes; editable-mesh booleans + curves stay \"mesh\". Generated from RECIPE_GALLERY + the API manifest — never hand-edit.",
  "recipeCount": 413,
  "exampleCount": 1490,
  "intentCounts": {
    "cad": 256,
    "mesh": 259,
    "previewer": 24,
    "math": 208,
    "io": 7,
    "scene": 1149
  },
  "entries": [
    {
      "kind": "recipe",
      "id": "actions-sdk-roundtrip",
      "title": "Browse, search, and run any registered Action",
      "description": "List every Action; search by keyword; inspect the descriptor (name / category / script snippet); check applicability; invoke by id.",
      "intent": "scene",
      "editor": "model",
      "category": "misc",
      "apiSurfaces": [
        "ModelEditor.actions.list",
        "ModelEditor.actions.get",
        "ModelEditor.actions.search",
        "ModelEditor.actions.run"
      ],
      "code": "const all = actions.list();\nconsole.log('total actions:', all.length);\n\n// Filter to only the Actions applicable in the current editor context.\nconst applicable = actions.list({ applicableOnly: true });\nconsole.log('applicable right now:', applicable.length);\n\nconst hits = actions.search('select', { applicableOnly: false });\nconsole.log('search \"select\" first 3:', JSON.stringify(hits.slice(0, 3).map(h => h.id)));\n\nconst target = hits[0]?.id ?? all[0]?.id;\nif (target) {\n  // get() returns the full descriptor: id / name / category / scriptSnippet.\n  const info = actions.get(target);\n  console.log('get(' + target + '):', JSON.stringify({ name: info?.name, category: info?.category }));\n\n  const snippet = info?.scriptSnippet;\n  console.log('snippet:', snippet ? snippet.slice(0, 60) + '...' : '(none)');\n\n  // Applicability = membership in the applicable-only list.\n  const ok = applicable.some((a) => a.id === target);\n  console.log('isApplicable:', ok);\n\n  if (ok) {\n    try {\n      await actions.run(target);\n      console.log('ran', target, 'successfully');\n    } catch (err) {\n      console.log('run threw:', err.message);\n    }\n  }\n}\n",
      "expectedOutput": "console: total action count, first matches for \"select\", the descriptor + snippet for one action, applicability check, run result."
    },
    {
      "kind": "recipe",
      "id": "anim-curve-introspect-and-sample",
      "title": "Read AnimCurve handle: keys + sampleAt + boneName + attr",
      "description": "AnimCurve is a read-only handle returned by node.getCurve(attr). Surfaces: .boneName, .attr, .keys.count, .keys(), .sampleAt(t).",
      "intent": "mesh",
      "editor": "previewer",
      "category": "misc",
      "apiSurfaces": [
        "node.getCurve",
        "animCurve.attr",
        "animCurve.boneName",
        "animCurve.keys",
        "animCurve.sampleAt"
      ],
      "code": "const sel = ls({selected: true});\nif (sel.length === 0) { console.log('Select an animated node first.'); return; }\nconst node = sel[0];\nconst attrs = ['translateX','translateY','translateZ','rotateX','rotateY','rotateZ'];\nconst found = [];\nfor (const a of attrs) { const c = node.getCurve(a); if (c) found.push(c); }\nif (found.length === 0) { console.log('No keyed channels found.'); return; }\nfor (const curve of found) {\n  console.log(curve.boneName + '.' + curve.attr + ': numKeys=' + curve.keys.count);\n  const keys = curve.keys();\n  if (keys.length === 0) continue;\n  const t0 = keys[0].time, t1 = keys[keys.length - 1].time;\n  console.log('  range:', t0.toFixed(3), t1.toFixed(3));\n  for (let i = 0; i < 5; i++) {\n    const t = t0 + (t1 - t0) * (i / 4);\n    console.log('  sampleAt(' + t.toFixed(3) + ')=' + curve.sampleAt(t).toFixed(3));\n  }\n}\n",
      "expectedOutput": "console: per-curve range + 5 sample-values."
    },
    {
      "kind": "recipe",
      "id": "anim-frame-navigation-and-sample",
      "title": "Step frames + sample curves",
      "description": "Step forward/back through frames; read the active clip + its range; sample curve values at specific times.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "animation",
      "apiSurfaces": [
        "PreviewEditor.animation.currentTime",
        "PreviewEditor.animation.nextFrame",
        "PreviewEditor.animation.prevFrame",
        "PreviewEditor.animation.getFrameRange",
        "PreviewEditor.animation.getActiveClip",
        "PreviewEditor.animation.sampleAt",
        "AnimEditor.scene.ls.animCurve",
        "Utils.wait.frame"
      ],
      "code": "const clip = animation.getActiveClip();\nconst range = animation.getFrameRange();\nconsole.log('clip:', clip?.name, 'frames:', range.start + '..' + range.end);\n\nconsole.log('t=' + animation.currentTime());\nanimation.nextFrame();\nawait Utils.wait.frame();\nconsole.log('after nextFrame: t=' + animation.currentTime());\nanimation.nextFrame();\nawait Utils.wait.frame();\nanimation.prevFrame();\nawait Utils.wait.frame();\nconsole.log('back: t=' + animation.currentTime());\n",
      "expectedOutput": "console: clip name + range; current frame after each step; sample values at picked times."
    },
    {
      "kind": "recipe",
      "id": "anim-key-bake-pose",
      "title": "Key a control, retag tangents, bake headlessly, mirror the pose (Animation editor)",
      "description": "Name-first authoring round-trip: set keys on a rig control, change tangent shapes by frame time, gap-fill bake without the popup, then mirror the pose.",
      "intent": "previewer",
      "editor": "animation",
      "category": "animation",
      "apiSurfaces": [
        "node.setKey",
        "node.getCurve",
        "animCurve.keys.set",
        "animCurve.tangents.setAuto",
        "animCurve.bake.between",
        "AnimEditor.copyPose",
        "AnimEditor.pastePose",
        "AnimEditor.pose"
      ],
      "code": "// Rigged asset required (e.g. an FBX + a biped rig). Everything by NAME.\nconst ctl = ls('ctrl_spine_01')[0];          // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0);                 // seconds — creates the curve\nconst curve = ctl.getCurve('rotateX');\nawait curve.keys.set(24, 45);                // frames — explicit keyed write\nconsole.log('keys before bake:', curve.keys.count);\nawait curve.tangents.setAuto([0, 24]);       // retag by frame time — no ids\nawait curve.bake.between({ frame: 12, frameStep: 4 });\nconsole.log('keys after bake:', curve.keys.count);\nawait copyPose({ controls: ['ctrl_spine_01'] });\nawait pastePose();\nawait pose({ mirror: 'all' });               // headless mirror (no popup)\nconsole.log('pose mirrored');\n",
      "expectedOutput": "console: key counts before/after the bake, then \"pose mirrored\"."
    },
    {
      "kind": "recipe",
      "id": "anim-playback-transport",
      "title": "Master playback transport: speed, loop, play, pause",
      "description": "Set speed + loop-mode, play, pause, and observe state at each step.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "animation",
      "apiSurfaces": [
        "PreviewEditor.animation.setSpeed",
        "PreviewEditor.animation.getSpeed",
        "PreviewEditor.animation.setLoopMode",
        "PreviewEditor.animation.getLoopMode",
        "PreviewEditor.animation.play",
        "PreviewEditor.animation.pause",
        "PreviewEditor.animation.isPlaying",
        "PreviewEditor.animation.currentTime",
        "Utils.wait.frame"
      ],
      "code": "animation.setSpeed(0.5);\nconsole.log('speed:', animation.getSpeed());\n\nanimation.setLoopMode('loop');\nconsole.log('loopMode:', animation.getLoopMode());\n\nanimation.play();\nawait Utils.wait.frame();\nconsole.log('playing?', animation.isPlaying(), 'at t=' + animation.currentTime());\n\nanimation.pause();\nawait Utils.wait.frame();\nconsole.log('paused; playing?', animation.isPlaying());\n",
      "expectedOutput": "console: speed value, loop mode, playing state at each step."
    },
    {
      "kind": "recipe",
      "id": "anim-step-and-snapshot-frames",
      "title": "Step time and snapshot at every frame",
      "description": "Step the timeline cursor one frame at a time across a range, capture a screenshot at each step, and report each capture for a quick storyboard.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "animation",
      "apiSurfaces": [
        "PreviewEditor.animation.currentTime",
        "PreviewEditor.viewport.captureScreenshot"
      ],
      "code": "// Step from t=0 to t=4 seconds, one shot per second.\nconst total = 4;\nfor (let t = 0; t <= total; t++) {\n  animation.currentTime(t);\n  await new Promise((r) => requestAnimationFrame(() => r(null)));\n  const shot = await viewport.captureScreenshot();\n  const idx = String(t).padStart(2, '0');\n  // shot.blob holds the frame; write it with io.saveFile if you need PNGs.\n  console.log('t=' + t, 'frame-' + idx, shot.width + 'x' + shot.height, shot.blob.size, 'bytes');\n}\n",
      "expectedOutput": "One console line per integer time step with the cursor time, image size, and blob bytes."
    },
    {
      "kind": "recipe",
      "id": "attribute-lock-limits-keyable-typed-reads",
      "title": "Attribute lifecycle: lock, limits, keyable, getAsAngle/getAsString",
      "description": "Walk an Attribute through its full state space. nodeId + path identify it; get/set is the value pair; lock/isLocked guards writes; setLimits/getLimits/enableLimits enforce ranges; setKeyable/isKeyable toggles channel-box visibility; getAsAngle / getAsString are typed readers.",
      "intent": "mesh",
      "editor": "model",
      "category": "attributes",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "node.attr",
        "node.addAttr",
        "attribute.nodeId",
        "attribute.path",
        "attribute.get",
        "attribute.set",
        "attribute.lock",
        "attribute.isLocked",
        "attribute.setLimits",
        "attribute.getLimits",
        "attribute.enableLimits",
        "attribute.isKeyable",
        "attribute.setKeyable",
        "attribute.getAsString",
        "attribute.getAsAngle"
      ],
      "code": "// Self-contained from an empty Model scene. Walk one Attribute through its\n// full state space: identity -> read/write -> limits -> lock -> keyable ->\n// typed reads.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'AttrDeep' });\nselect(null, { mode: 'clear' });\n\n// Add two user-defined channels. 'at' is float/int/bool/float3/string/enum\n// (there is NO 'angle' type) - getAsAngle works on any float, treating the\n// stored value as radians.\ncube.addAttr({ ln: 'wobble', at: 'float', dv: 0.5, k: true, min: 0, max: 1 }); // keyable, bounded\ncube.addAttr({ ln: 'spinAngle', at: 'float', dv: Math.PI / 4, k: false });     // 45 deg in radians\n\nconst wob = cube.attr('wobble'), ang = cube.attr('spinAngle');\n\n// IDENTITY — nodeId + path locate the attribute.\nconsole.log('id:', wob.nodeId, '| path:', wob.path);\n\n// READ / WRITE.\nconsole.log('initial get:', wob.get());\nwob.set(0.75); console.log('after set 0.75:', wob.get());\n\n// LIMITS — setLimits defines the range; enableLimits(true) starts enforcing.\nwob.setLimits(0, 1); console.log('limits:', JSON.stringify(wob.getLimits()));\nwob.enableLimits(true);\ntry { wob.set(2); } catch (e) { console.log('out-of-range rejected:', e.code); }\nwob.enableLimits(false); wob.set(2); console.log('unbounded set 2:', wob.get()); // limits off -> accepted\n\n// LOCK — guards writes entirely (independent of keyable + limits).\nwob.lock(true); console.log('isLocked?', wob.isLocked());\ntry { wob.set(0); } catch (e) { console.log('locked rejected:', e.code); }\nwob.lock(false);\n\n// KEYABLE — channel-box visibility toggle; does NOT lock the value.\nconsole.log('isKeyable:', wob.isKeyable());\nwob.setKeyable(false); console.log('after setKeyable(false):', wob.isKeyable());\nwob.setKeyable(true);\n\n// TYPED READS.\nconsole.log('wob.getAsString():', wob.getAsString());\nconsole.log('ang.getAsString():', ang.getAsString());\nconsole.log('ang.getAsAngle() deg:', ang.getAsAngle().toFixed(2));                  // default units = 'degrees' -> 45.00\nconsole.log('ang.getAsAngle({radians}):', ang.getAsAngle({ units: 'radians' }).toFixed(3)); // 0.785\n",
      "expectedOutput": "console: identity + r/w + bounds + lock + keyable + typed reads."
    },
    {
      "kind": "recipe",
      "id": "attributes-bounded-slider",
      "title": "Add a bounded slider attribute (min / max)",
      "description": "Use min and max on addAttr to clamp a custom channel to a range. Out-of-range set() throws so script errors surface loudly instead of silently clamping.",
      "intent": "mesh",
      "editor": "model",
      "category": "attributes",
      "apiSurfaces": [
        "node.addAttr",
        "node.attr"
      ],
      "code": "// Add a normalized \"wetness\" slider on the first selected node.\nconst n = ls({selected: true})[0];\nif (!n) { console.log('Select an object first.'); return; }\n\nn.addAttr({ ln: 'wetness', at: 'float', dv: 0.5, k: true, min: 0, max: 1 });\nn.attr('wetness').set(0.75);\nconsole.log('wetness =', n.attr('wetness').get()); // 0.75\n\n// Out-of-range writes throw (no silent clamp; the caller decides intent).\ntry {\n  n.attr('wetness').set(2);\n} catch (err) {\n  console.log('out-of-range rejected:', err.message);\n}\n",
      "expectedOutput": "console: wetness=0.75; out-of-range write rejected with InvalidArgumentError."
    },
    {
      "kind": "recipe",
      "id": "attributes-cleanup-custom",
      "title": "Clean up every custom attribute on a node",
      "description": "Iterate the user-defined attribute list and remove each one. Undo restores everything, including any connections that were live when the attr was removed.",
      "intent": "mesh",
      "editor": "model",
      "category": "attributes",
      "apiSurfaces": [
        "node.addAttr",
        "node.attrs",
        "node.removeAttr"
      ],
      "code": "const n = ls({selected: true})[0];\nif (!n) { console.log('Select an object first.'); return; }\n\n// Seed a few custom attrs so the sweep has work to do.\nn.addAttr({ ln: 'a', at: 'float', dv: 1 });\nn.addAttr({ ln: 'b', at: 'float', dv: 2 });\nconsole.log('before:', JSON.stringify(n.attrs({ custom: true })));\n\n// custom: true is an alias for userDefined: true.\nn.attrs({ custom: true }).forEach(name => n.removeAttr(name));\nconsole.log('after:', JSON.stringify(n.attrs({ custom: true }))); // []\n",
      "expectedOutput": "console: before list with custom attrs, after list empty."
    },
    {
      "kind": "recipe",
      "id": "attributes-connect-and-disconnect",
      "title": "Wire and unwire attributes from the Node handle",
      "description": "Node.connect / Node.disconnect take both endpoints as paths; convenient when you have the source and destination Node handles in scope. Pass force:true to replace an existing incoming connection.",
      "intent": "mesh",
      "editor": "model",
      "category": "attributes",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "node.addAttr",
        "node.connect",
        "node.attr",
        "node.disconnect"
      ],
      "code": "const A = await create.cube({ name: 'Source' });   select(null, {mode: 'clear'});\nconst B = await create.cube({ name: 'Dest' });     select(null, {mode: 'clear'});\nA.addAttr({ ln: 'driver', at: 'float', dv: 7, k: true });\nB.addAttr({ ln: 'driven', at: 'float', dv: 0, k: true });\n\nA.connect(B, 'driver', 'driven');\nA.attr('driver').set(11);\nconsole.log('B.driven =', B.attr('driven').get()); // 11\n\nA.disconnect(B, 'driver', 'driven');\nconsole.log('B.driven (post-disconnect) =', B.attr('driven').get()); // 0\n",
      "expectedOutput": "console: B.driven=11 after connect, then =0 after disconnect."
    },
    {
      "kind": "recipe",
      "id": "attributes-connect-driver",
      "title": "Drive one attribute from another",
      "description": "Connect a source attribute so it drives a destination (attribute-driven connection). Pass { force: true } to replace an existing incoming connection in one undoable step.",
      "intent": "mesh",
      "editor": "model",
      "category": "attributes",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "node.addAttr",
        "node.attr",
        "attribute.connectFrom"
      ],
      "code": "// Add a driver channel on cube A and a driven channel on cube B,\n// then wire them so B follows A. (Deselect between spawns so each polyCube\n// makes exactly one cube.)\nconst A = await create.cube({ name: 'Driver' });   select(null, {mode: 'clear'});\nconst B = await create.cube({ name: 'Driven' });   select(null, {mode: 'clear'});\nA.addAttr({ ln: 'amount', at: 'float', dv: 1, k: true });\nB.addAttr({ ln: 'follows', at: 'float', dv: 0, k: true });\n\nB.attr('follows').connectFrom(A.attr('amount'));\nA.attr('amount').set(5);\nconsole.log('B.follows =', B.attr('follows').get()); // 5\n\n// Reconnect to a different source; force replaces the existing one.\nconst C = await create.cube({ name: 'Driver2' });  select(null, {mode: 'clear'});\nC.addAttr({ ln: 'amount', at: 'float', dv: 9, k: true });\nB.attr('follows').connectFrom(C.attr('amount'), { force: true });\nconsole.log('B.follows now =', B.attr('follows').get()); // 9\n",
      "expectedOutput": "console output shows B.follows=5 after driving from A, then =9 after reconnecting to C with force."
    },
    {
      "kind": "recipe",
      "id": "attributes-hide-internal",
      "title": "Hide an internal state attribute from the channel box",
      "description": "Use hidden:true to keep a custom attribute out of the channel box. Useful for rig-internal state that scripts read but artists should not edit.",
      "intent": "mesh",
      "editor": "model",
      "category": "attributes",
      "apiSurfaces": [
        "node.addAttr",
        "node.attr",
        "node.attrs"
      ],
      "code": "const n = ls({selected: true})[0];\nif (!n) { console.log('Select an object first.'); return; }\n\nn.addAttr({ ln: 'rigState', at: 'string', dv: 'idle', hidden: true });\nconsole.log('rigState =', n.attr('rigState').get()); // 'idle'\n\n// hidden:true attrs do NOT appear in the keyable list, so the channel box\n// reads keyable, so a hidden attr is effectively invisible to artists.\nconsole.log('keyable channels:', JSON.stringify(n.attrs({ keyable: true })));\n",
      "expectedOutput": "console: rigState=idle; hidden attribute does NOT appear in the keyable channel list."
    },
    {
      "kind": "recipe",
      "id": "attributes-tidy-and-add-custom",
      "title": "Tidy the channel box & add a custom attribute",
      "description": "Hide channels you do not animate, add a user-defined float attribute, and list what is left. setKeyable controls channel-box visibility only; it does not lock the value.",
      "intent": "mesh",
      "editor": "model",
      "category": "attributes",
      "apiSurfaces": [
        "node.attr",
        "node.addAttr",
        "node.attrs"
      ],
      "code": "// Pick the first selected node.\nconst n = ls({selected: true})[0];\nif (!n) { console.log('Select an object first.'); return; }\n\n// Hide the scale channels from the channel box (the value is still settable;\n// keyable only controls visibility, independent of lock).\nn.attr('scale').setKeyable(false);\nconsole.log('scale keyable?', n.attr('scale').isKeyable());\n\n// Add a custom float channel and make it keyable.\nn.addAttr({ ln: 'wobbleAmount', at: 'float', dv: 0.5, k: true });\nn.attr('wobbleAmount').set(0.75);\nconsole.log('wobbleAmount =', n.attr('wobbleAmount').getAsString());\n\n// List what shows in the channel box now (keyable attrs only).\nconsole.log('keyable channels:', JSON.stringify(n.attrs({ keyable: true })));\nconsole.log('custom channels:', JSON.stringify(n.attrs({ userDefined: true })));\n",
      "expectedOutput": "console output shows scale keyable=false, wobbleAmount=0.75, and lists keyable + user-defined channels."
    },
    {
      "kind": "recipe",
      "id": "audit-material-mesh-inventory",
      "title": "Inventory materials and mesh assignments",
      "description": "List every material and report which mesh references which material. Useful for a \"what shaders is this asset using\" sweep.",
      "intent": "mesh",
      "editor": "previewer",
      "category": "materials-textures",
      "apiSurfaces": [
        "ModelEditor.listMaterials",
        "ModelEditor.getMaterial",
        "mesh.material.get"
      ],
      "code": "// Asset inventory: materials and mesh-to-material map.\n// listMaterials() → { id, name } descriptors; getMaterial(id).pbr reads channels.\nconst mats = listMaterials();\nconsole.log('materials:', mats.length);\n\nfor (const { id, name } of mats) {\n  const bc = getMaterial(id).pbr.baseColor.get();\n  console.log('  material:', name, 'rgb=[' + bc[0].toFixed(2) + ',' + bc[1].toFixed(2) + ',' + bc[2].toFixed(2) + ']');\n}\n\nconsole.log('mesh -> material:');\nfor (const me of ls({ type: 'mesh' })) {\n  const mat = me.material.get();\n  console.log('  ' + me.name + ' ->', mat ? mat.name : '(none)');\n}\n",
      "expectedOutput": "console: material count, per-mesh material assignment lines."
    },
    {
      "kind": "recipe",
      "id": "audit-meshes-missing-materials",
      "title": "Find meshes with no material assigned",
      "description": "Walk every mesh in the scene and report the ones with no resolvable material. Pairs with the inventory recipe to flag shading gaps before export.",
      "intent": "mesh",
      "editor": "model",
      "category": "materials-textures",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "ModelEditor.scene.select",
        "mesh.material.get"
      ],
      "code": "// Surface every mesh that has no material assigned.\nconst all = ls({ type: 'mesh' });\nconst orphans = [];\nfor (const m of all) {\n  if (!m.material.get()) orphans.push(m);\n}\n\nselect(null, {mode: 'clear'});\nfor (const o of orphans) select(o, { add: true });\n\nif (orphans.length === 0) {\n  console.log('every mesh has a material; nothing to fix');\n} else {\n  console.log('meshes without materials:', orphans.length, '/', all.length);\n  for (const o of orphans) console.log('  ' + o.name);\n}\n",
      "expectedOutput": "console: list of mesh names missing materials; outliner: those meshes are selected so the user can fix them in one pass."
    },
    {
      "kind": "recipe",
      "id": "audit-scene-contents",
      "title": "Audit scene contents",
      "description": "Count meshes and joints; report renderer stats; capture a snapshot.",
      "intent": "scene",
      "editor": "previewer",
      "category": "misc",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "ModelEditor.dev.debug.getRendererState"
      ],
      "code": "// Quick scene audit: counts + renderer stats.\nconsole.log('meshes:', ls({ type: 'mesh' }).length);\nconsole.log('joints:', ls({ type: 'joint' }).length);\n\nlet totalFaces = 0;\nfor (const m of ls({ type: 'mesh' })) totalFaces += m.faces.count;\nconsole.log('total faces:', totalFaces);\n\nconst state = dev.debug.getRendererState();\nif (state) {\n  console.log('triangles:', state.render.triangles);\n  console.log('textures: ', state.memory.textures);\n  console.log('geometries:', state.memory.geometries);\n}\n",
      "expectedOutput": "console lines: mesh count, joint count, triangle count, texture / geometry memory."
    },
    {
      "kind": "recipe",
      "id": "audit-scene-units-viewport-state",
      "title": "Snapshot scene units, viewport, and renderer state",
      "description": "Read the current grid unit, viewport view name, render mode, and renderer triangle count in one pass so you can paste an asset spec into a doc.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "misc",
      "apiSurfaces": [
        "PreviewEditor.viewport.setGridUnit",
        "PreviewEditor.viewport.backgroundColor",
        "ModelEditor.viewport.setView",
        "ModelEditor.dev.debug.getRendererState"
      ],
      "code": "// One-stop scene-state snapshot for an asset spec sheet.\nconst bg = viewport.backgroundColor;\nconsole.log('previewer bg:', bg);\n\nconst state = dev.debug.getRendererState();\nif (state) {\n  console.log('triangles:', state.render.triangles);\n  console.log('textures :', state.memory.textures);\n  console.log('geometries:', state.memory.geometries);\n}\n\n// Show the user the canonical \"front\" view as a parity reference.\nviewport.setView('front');\nconsole.log('viewport set to front view');\n",
      "expectedOutput": "console block with grid unit + view + bg color + triangle count + texture / geometry memory."
    },
    {
      "kind": "recipe",
      "id": "audit-topology-stats",
      "title": "Report per-mesh topology stats",
      "description": "Walk every mesh and log face / vertex / edge counts plus surface area. Handy for catching outliers before export.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "mesh.faces",
        "mesh.verts",
        "mesh.edges",
        "mesh.area"
      ],
      "code": "// Per-mesh topology summary.\nlet totalFaces = 0, totalVerts = 0, totalArea = 0;\nfor (const m of ls({ type: 'mesh' })) {\n  console.log(\n    m.name,\n    'faces=' + m.faces.count,\n    'verts=' + m.verts.count,\n    'edges=' + m.edges.count,\n    'area=' + m.area.toFixed(3),\n  );\n  totalFaces += m.faces.count;\n  totalVerts += m.verts.count;\n  totalArea += m.area;\n}\nconsole.log('TOTALS faces=' + totalFaces, 'verts=' + totalVerts, 'area=' + totalArea.toFixed(3));\n",
      "expectedOutput": "console: per-mesh table-like line with faces, verts, edges, area; totals at the bottom."
    },
    {
      "kind": "recipe",
      "id": "batch-convert-obj-to-fbx",
      "title": "Convert a folder of OBJs to FBX",
      "description": "Pick an input folder and an output folder once. The script walks every .obj and writes a matching .fbx.",
      "intent": "io",
      "editor": "previewer",
      "category": "io",
      "apiSurfaces": [
        "ModelEditor.io.batchConvert"
      ],
      "code": "// Pick an input folder, an output folder, and convert every OBJ to FBX.\n// Subsequent runs remember the folders (persistAs).\nconst result = await io.batchConvert({\n  from: { folder: true, accept: '.obj', persistAs: 'obj-library' },\n  to:   { folder: true, format: 'fbx', persistAs: 'fbx-output' },\n});\nconsole.log('converted', result.succeeded.length, 'of', result.total);\nif (result.failed.length > 0) {\n  console.warn('failed:', JSON.stringify(result.failed.map(f => f.input)));\n}\n",
      "expectedOutput": "console: \"converted N of M\"; failed inputs logged via console.warn."
    },
    {
      "kind": "recipe",
      "id": "bosl2-608-bearing-ring",
      "title": "608 bearing footprint ring",
      "description": "A solid printable ring matching the footprint of a standard 608 skate/skateboard ball bearing (22 mm OD, 8 mm bore, 7 mm wide). Use it as a press-fit spacer, a bearing-seat gauge, or a drop-in dummy bearing. The outer and inner edges are lightly chamfered so it seats cleanly. Tune the diameters and width to gauge other bearing sizes.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// 608 bearing footprint ring\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nouter_d  = 22;   // outer diameter (608 = 22 mm)\nbore_d   = 8;    // inner bore diameter (608 = 8 mm)\nwidth    = 7;    // axial width (608 = 7 mm)\nedge_cf  = 0.6;  // chamfer on outer + bore edges\n$fn = 96;\n\ntube(h = width, od = outer_d, id = bore_d, chamfer = edge_cf, anchor = BOTTOM);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-bevel-gear",
      "title": "Bevel gear",
      "description": "A straight-cut bevel gear that transmits motion between two shafts meeting at 90 degrees. Print one of these plus a mating bevel gear (swap teeth and mate_teeth) to build a right-angle drive. Tune the module, tooth count, mate tooth count, face width, and bore.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Bevel gear\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/gears.scad>\n\nteeth       = 24;  // teeth on THIS gear\nmate_teeth  = 24;  // teeth on the mating gear (sets the cone angle)\ngear_mod    = 2;   // tooth module in mm\nface_w      = 8;   // face width (tooth length along the cone) in mm\nshaft_d     = 6;   // center shaft bore diameter in mm\n$fn = 48;\n\nbevel_gear(mod = gear_mod, teeth = teeth, mate_teeth = mate_teeth,\n           face_width = face_w, shaft_diam = shaft_d, shaft_angle = 90,\n           pressure_angle = 20, spiral = 0);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-bottle-cap",
      "title": "Generic threaded bottle cap",
      "description": "A generic screw-on bottle cap that mates with the generic threaded bottle neck, built by the BOSL2 bottlecaps library. Tune the cap height, thread depth, thread outer diameter, and pitch (keep them matched to your neck) for a snug fit. Pairs with the generic bottle neck recipe.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Generic threaded bottle cap\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/bottlecaps.scad>\n\ncap_h        = 11.2;   // cap height (mm)\ncap_wall     = 2;      // wall thickness (mm)\nthread_depth = 2.34;   // thread depth (mm) — match the neck thread\nthread_od    = 28.58;  // thread outer diameter (mm) — match the neck\npitch        = 4;      // thread pitch (mm) — match the neck\n$fn = 96;\n\ngeneric_bottle_cap(\n    height       = cap_h,\n    wall         = cap_wall,\n    thread_depth = thread_depth,\n    thread_od    = thread_od,\n    pitch        = pitch,\n    texture      = \"knurled\"\n);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-bottle-neck",
      "title": "Generic threaded bottle neck",
      "description": "A generic threaded bottle neck finish with a support ring, built by the BOSL2 bottlecaps library. Pairs with the matching generic bottle cap. Tune the neck diameter, thread outer diameter, height, and thread pitch to design your own container opening.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Generic threaded bottle neck\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/bottlecaps.scad>\n\nneck_diam = 25;      // outer diameter of the neck wall (mm)\ninner_id  = 21.4;    // inner bore diameter (mm)\nthread_od = 27.2;    // outer diameter over the threads (mm)\nneck_h    = 17;      // neck height (mm)\npitch     = 3.2;     // thread pitch (mm)\n$fn = 96;\n\ngeneric_bottle_neck(\n    neck_d    = neck_diam,\n    id        = inner_id,\n    thread_od = thread_od,\n    height    = neck_h,\n    pitch     = pitch\n);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-cable-clip",
      "title": "Wall cable clip",
      "description": "A small screw- or adhesive-mount cable clip: a flat backing pad with a rounded C-shaped catch that snaps over a cable to route it along a wall or desk edge. The catch mouth is narrower than the cable so it grips. Tune the cable diameter, mouth opening, backing size, and wall thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Wall cable clip\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ncable_d  = 6;    // diameter of the cable to hold\ngap      = 3.5;  // mouth opening width (under cable_d so it grips)\nwall     = 2;    // catch wall thickness\npad_w    = 16;   // backing pad width\npad_t    = 3;    // backing pad thickness\nclip_len = 12;   // depth of the clip along the cable\n$fn = 72;\n\nbore_r  = cable_d / 2;\nouter_r = bore_r + wall;\n// Catch centre is lowered so the outer body overlaps (fuses with) the pad,\n// while the bore floor stays a touch above the pad's top surface.\noverlap  = wall * 0.75;\ncatch_cz = pad_t + outer_r - overlap;\n\nunion() {\n    // Flat backing pad against the wall, vertical corners rounded.\n    cuboid([pad_w, clip_len, pad_t], rounding = 1.5, edges = \"Y\", anchor = BOTTOM);\n\n    // C-shaped catch: outer cylinder, bore removed, plus a front mouth slot.\n    translate([0, 0, catch_cz])\n        difference() {\n            // Outer body of the catch (axis along Y / cable direction).\n            xrot(90) cyl(h = clip_len, r = outer_r, rounding = 0.8);\n            // Cable bore.\n            xrot(90) cyl(h = clip_len + 0.4, r = bore_r);\n            // Mouth slot opening upward (away from the pad).\n            up(outer_r / 2)\n                cube([gap, clip_len + 0.4, outer_r + 0.2], center = true);\n        }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-corner-brace",
      "title": "Flat corner brace",
      "description": "A flat L-shaped mending plate that reinforces a 90 degree corner joint. Two perpendicular arms share a rounded outer profile with a screw hole in each arm and the heel. Prints flat as one solid with no supports. Tune the arm length, width, thickness, corner rounding, and screw-hole diameter.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Flat corner brace\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\narm     = 45;   // length of each arm from the heel\nwidth   = 16;   // arm width\nthick   = 4;    // plate thickness\nround_r = 4;    // outer corner rounding radius\nhole_d  = 4.2;  // screw-hole diameter\n$fn = 48;\n\ndifference() {\n    union() {\n        // Arm running along +X.\n        cuboid([arm, width, thick], rounding = round_r,\n               edges = [LEFT + FRONT, LEFT + BACK, RIGHT + FRONT],\n               anchor = BOTTOM + LEFT + BACK);\n        // Arm running along -Y.\n        cuboid([width, arm, thick], rounding = round_r,\n               edges = [LEFT + FRONT, RIGHT + FRONT, RIGHT + BACK],\n               anchor = BOTTOM + LEFT + BACK);\n    }\n    // Hole near the end of the X arm.\n    translate([arm - width / 2, -width / 2, 0])\n        cyl(d = hole_d, h = thick + 2, anchor = CENTER);\n    // Hole near the end of the Y arm.\n    translate([width / 2, -arm + width / 2, 0])\n        cyl(d = hole_d, h = thick + 2, anchor = CENTER);\n    // Hole at the heel.\n    translate([width / 2, -width / 2, 0])\n        cyl(d = hole_d, h = thick + 2, anchor = CENTER);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-deco-bowl",
      "title": "Rounded decorative bowl",
      "description": "A curved bowl with a flat stable base and a rounded rim, hollowed to a uniform wall and a solid floor. Built by revolving an explicit profile with BOSL2 rotate_sweep. Tune the rim radius, depth, wall and base flat.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Rounded decorative bowl\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nrim_r   = 55;   // outer radius at the rim\ndepth   = 42;   // bowl depth\nwall    = 3;    // wall thickness\nfloor_t = 4;    // solid floor thickness\nbase_r  = 24;   // radius of the flat resting base\n$fn = 120;\n\n// Outer profile (right half), a smooth curve from base to rim.\nouter = [\n    [0,            0],\n    [base_r,       0],\n    [rim_r * 0.78, depth * 0.30],\n    [rim_r * 0.97, depth * 0.70],\n    [rim_r,        depth],\n    [0,            depth],\n];\n// Inner cavity profile: offset inward by the wall, floor raised by floor_t.\ninner = [\n    [0,                   floor_t],\n    [base_r - wall,       floor_t],\n    [rim_r * 0.78 - wall, depth * 0.30 + floor_t * 0.4],\n    [rim_r * 0.97 - wall, depth * 0.70],\n    [rim_r - wall,        depth],\n    [0,                   depth],\n];\n\ndifference() {\n    rotate_sweep(outer, closed = true);\n    rotate_sweep(inner, closed = true);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-deco-faceted-gem",
      "title": "Faceted gem",
      "description": "A solid brilliant-cut style gem: a flat-topped table on a faceted crown over a pointed pavilion, built from stacked BOSL2 regular prisms. Single shell, prints point-down style as one solid (table up). Tune the girdle width, facet count, and crown/pavilion proportions.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Faceted gem\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nsides     = 8;    // number of facets around the girdle\ngirdle_r  = 26;   // radius at the widest point (girdle)\ntable_r   = 15;   // radius of the flat top table\ncrown_h   = 14;   // height of the upper crown (girdle to table)\npavi_h    = 26;   // height of the lower pavilion (girdle to point)\n$fn = 8;\n\nunion() {\n    // Crown: girdle tapering up to the flat table.\n    regular_prism(n = sides, r1 = girdle_r, r2 = table_r, h = crown_h, anchor = BOTTOM);\n    // Pavilion: girdle tapering down to a near-point, fused at the girdle plane.\n    down(pavi_h)\n        regular_prism(n = sides, r1 = 0.8, r2 = girdle_r, h = pavi_h, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-deco-ornament-ball",
      "title": "Hanging ornament ball",
      "description": "A solid decorative sphere with an integrated torus hanging loop on top, fused into one printable shell. Tune the ball diameter, the loop size/thickness and the facet count for a smoother surface.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Hanging ornament ball\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nball_d    = 50;   // sphere diameter\nloop_or   = 7;    // loop outer radius (hole size)\nloop_thk  = 2.6;  // loop wire thickness (torus minor radius)\n$fn = 96;\n\nunion() {\n    // The decorative ball.\n    spheroid(d = ball_d, anchor = CENTER);\n    // Hanging loop: a torus standing vertically, overlapping the top of the ball\n    // so the two fuse into a single solid (loop sits in the XZ plane).\n    up(ball_d / 2 - loop_thk)\n        xrot(90)\n            torus(or = loop_or, ir = loop_or - 2 * loop_thk);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-deco-photo-frame-stand",
      "title": "Picture frame with stand",
      "description": "A desktop photo frame: a rounded rectangular border with a window cut through it, fused to an angled easel leg at the back so it stands on its own. One printable shell, prints flat on its back. Tune the frame size, border width, window and leg angle.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Picture frame with stand\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nframe_w   = 90;   // overall frame width\nframe_h   = 120;  // overall frame height\nborder    = 12;   // border thickness around the photo window\nframe_t   = 6;    // frame plate thickness (front to back)\nleg_len   = 78;   // length of the easel leg\nleg_ang   = 22;   // lean-back angle of the leg (degrees)\n$fn = 48;\n\nunion() {\n    // Frame plate lying in the XY plane (front face up), window cut through.\n    difference() {\n        cuboid([frame_w, frame_h, frame_t], rounding = 5, edges = \"Z\", anchor = BOTTOM);\n        down(0.5)\n            cuboid([frame_w - 2 * border, frame_h - 2 * border, frame_t + 2],\n                   rounding = 3, edges = \"Z\", anchor = BOTTOM);\n    }\n    // Solid hinge block fused to the lower-back of the frame; the leg grows from it.\n    translate([0, -frame_h / 2 + 10, 0])\n        union() {\n            // Hinge block bridging frame and leg (overlaps the plate solidly).\n            cuboid([20, 16, frame_t + 3], rounding = 2, edges = \"Z\", anchor = BOTTOM);\n            // Easel leg leaning out from the hinge block.\n            up(frame_t)\n                xrot(-leg_ang)\n                    cuboid([18, leg_len, 5], rounding = 3, edges = \"Z\",\n                           anchor = FRONT + BOTTOM);\n        }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-deco-tealight-holder",
      "title": "Tealight candle holder",
      "description": "A weighted round holder with a recessed pocket sized for a standard tealight cup and a rounded outer rim. Single shell, prints flat with no supports. Tune the outer diameter, height, the tealight pocket diameter/depth and the rim rounding.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Tealight candle holder\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nouter_d   = 52;   // outer diameter\nheight    = 26;   // overall height\npocket_d  = 39;   // tealight pocket diameter (standard cup ~38-40mm)\npocket_h  = 13;   // tealight pocket depth (cup height ~10-12mm)\nrim_round = 4;    // rounding on the outer top rim\n$fn = 96;\n\ndifference() {\n    // Solid rounded body.\n    cyl(d = outer_d, h = height, rounding2 = rim_round,\n        chamfer1 = 1, anchor = BOTTOM);\n    // Recessed pocket for the tealight cup, open at the top.\n    up(height - pocket_h)\n        cyl(d = pocket_d, h = pocket_h + 1, rounding1 = 2, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-deco-twisted-column",
      "title": "Twisted spiral column",
      "description": "A decorative pillar/pedestal with a spiralling fluted shaft between a square base and a square cap, fused into one solid. The shaft is a rounded BOSL2 star profile linearly extruded with a twist. Tune the height, twist, flute count and base size.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Twisted spiral column\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nshaft_h   = 120;  // height of the twisting shaft\ntwist     = 200;  // total twist over the shaft (degrees)\npoints    = 6;    // number of flutes around the shaft\nshaft_r   = 20;   // outer radius of the shaft\nplinth    = 46;   // base/cap square width\n$fn = 72;\n\n// Rounded star profile so the twisted flutes have no razor edges (cleaner mesh).\nshaft_profile = round_corners(\n    star(n = points, r = shaft_r, ir = shaft_r * 0.74),\n    radius = 2.5, $fn = 16\n);\n\nunion() {\n    // Square base plinth.\n    prismoid(size1 = [plinth, plinth], size2 = [plinth - 4, plinth - 4],\n             h = 14, rounding = 3, anchor = BOTTOM);\n    // Twisting fluted shaft, overlapping the plinth and cap so all fuse.\n    up(12)\n        linear_extrude(height = shaft_h, twist = twist, slices = 160, convexity = 12)\n            polygon(shaft_profile);\n    // Square cap plinth, overlapping the shaft top.\n    up(12 + shaft_h - 2)\n        prismoid(size1 = [plinth - 4, plinth - 4], size2 = [plinth, plinth],\n                 h = 14, rounding = 3, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-deco-vase-faceted",
      "title": "Faceted prismoid vase",
      "description": "An angular tapered vase with a wide square base flaring to a smaller rotated top, hollowed for stems. Built from two BOSL2 prismoids (outer minus inner) with a solid floor. Tune base/top widths, twist, height and wall.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Faceted prismoid vase\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbase_w  = 60;   // base width (square)\ntop_w   = 46;   // top opening width (square)\nvase_h  = 120;  // overall height\nwall    = 2.6;  // wall thickness\nfloor_t = 3;    // floor thickness\nedge_r  = 3;    // vertical edge rounding\n$fn = 64;\n\ndifference() {\n    // Outer faceted body.\n    prismoid(size1 = [base_w, base_w], size2 = [top_w, top_w],\n             h = vase_h, rounding = edge_r, anchor = BOTTOM);\n    // Inner cavity, raised above the floor so the base stays solid.\n    up(floor_t)\n        prismoid(size1 = [base_w - 2 * wall, base_w - 2 * wall],\n                 size2 = [top_w - 2 * wall, top_w - 2 * wall],\n                 h = vase_h, rounding = max(edge_r - wall, 0.5), anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-deco-vase-rounded",
      "title": "Rounded bud vase",
      "description": "A smooth single-shell bud vase with a swelling belly and a narrowed neck, hollowed for water. Built by revolving a rounded profile with BOSL2 rotate_sweep. Tune the base/belly/neck radii, height and wall thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Rounded bud vase\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbase_r  = 22;   // radius at the base\nbelly_r = 38;   // widest radius (belly)\nneck_r  = 18;   // radius at the neck/mouth\nvase_h  = 110;  // overall height\nwall    = 2.4;  // wall thickness\n$fn = 96;\n\n// Outer profile (right half, x>=0) revolved around the Z axis.\nfunction vase_profile(rb, rbelly, rn, h) = [\n    [0,        0],\n    [rb,       0],\n    [rb + 6,   h * 0.18],\n    [rbelly,   h * 0.45],\n    [rn + 7,   h * 0.78],\n    [rn,       h],\n    [0,        h],\n];\n\ndifference() {\n    rotate_sweep(vase_profile(base_r, belly_r, neck_r, vase_h), closed = true);\n    // Inner cavity: same profile scaled inward by the wall, with a solid floor.\n    up(wall)\n        rotate_sweep(vase_profile(base_r - wall, belly_r - wall, neck_r - wall, vase_h), closed = true);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-desk-cable-clip",
      "title": "Cable management clip",
      "description": "A desk-edge cable management clip: a flat adhesive base with an open C-shaped jaw on top that snaps around a cable bundle and holds it along the desk edge. The C jaw is a ring extruded across the clip depth with a top wedge cut for the snap opening, unioned to the base, so it prints flat-down as one solid. Tune the cable channel diameter, jaw opening, base size, and wall.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Cable management clip\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ncable_d   = 12;   // inner channel diameter (cable bundle)\nwall      = 2.8;  // clip wall thickness\ngap_w     = 8;    // width of the snap opening at the top\nbase_w    = 26;   // adhesive base width\ndepth     = 18;   // clip depth (along the cable run)\nbase_t    = 3;    // base thickness\n$fn = 72;\n\nouter_d = cable_d + 2 * wall;\n\nunion() {\n    // Adhesive base plate.\n    cuboid([base_w, depth, base_t], rounding = 2, except = [TOP, BOTTOM], anchor = BOTTOM);\n\n    // C-jaw: extrude a ring-with-gap profile across the clip depth.\n    up(base_t)\n        xrot(90)\n            linear_extrude(height = depth, center = true)\n                difference() {\n                    circle(d = outer_d);\n                    circle(d = cable_d);\n                    // Top opening slot.\n                    translate([0, outer_d / 2])\n                        square([gap_w, outer_d], center = true);\n                }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-desk-card-holder",
      "title": "Business-card holder",
      "description": "An angled desktop business-card holder: a rounded block with a slanted slot that cradles a stack of cards at a readable angle, plus a front cut-away so you can thumb the top card out. Carved entirely by difference from one rounded body so it prints as a single watertight piece. Tune the card width, stack thickness, slot angle, and wall.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Business-card holder\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ncard_w     = 92;   // card width (slot length) — standard card ~90mm\nstack_t    = 16;   // thickness of card stack the slot holds\nslot_angle = 18;   // backward lean of the slot, degrees\nwall       = 4;    // surrounding wall thickness\nbase_h     = 26;   // overall body height\nround_r    = 3;    // outer corner rounding\n$fn = 48;\n\nbody_w = card_w + 2 * wall;\nbody_d = stack_t + 2 * wall + 14;   // depth: stack + walls + front lip room\nbody_h = base_h;\n\ndifference() {\n    cuboid([body_w, body_d, body_h], rounding = round_r, except = [TOP]);\n\n    // Angled slot that holds the card stack (tall thin pocket, leaned back).\n    translate([0, 2, body_h - 2])\n        rotate([slot_angle, 0, 0])\n            cuboid([card_w + 1, stack_t, body_h * 2.2],\n                   rounding = 1, except = [TOP, BOTTOM], anchor = TOP);\n\n    // Front thumb cut-away so the top card is reachable.\n    translate([0, -body_d / 2 + wall + 1, body_h])\n        cuboid([card_w - 24, wall * 3, body_h * 1.4], rounding = 4, except = [TOP], anchor = TOP);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-desk-headphone-hook",
      "title": "Headphone hook bracket",
      "description": "An under-desk headphone hook bracket: a flat vertical mounting plate (screw-mount) with a hook arm that sweeps out and curls up to cradle a headphone band without creasing it. The side profile (plate + arm + upturned tip) is drawn once and extruded across the width, then screw holes are bored, so it is guaranteed a single watertight solid. Tune the plate size, hook reach, tip height, and width.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Headphone hook bracket\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nplate_h   = 60;   // mount plate height\nplate_t   = 6;    // plate / arm material thickness\narm_reach = 45;   // how far the hook sticks out\ntip_h     = 24;   // height of the upturned cradle tip\nwidth     = 26;   // width across (band rests here)\n$fn = 48;\n\n// Side profile in the YZ-ish plane, drawn in 2D (X = reach, Y = height),\n// then extruded across the part width and laid upright.\nxrot(90)\nlinear_extrude(height = width, center = true)\n    offset(r = 1.5) offset(r = -1.5)   // round all profile corners (kept < half arm thickness)\n        polygon([\n            [0,             0],            // plate back-bottom\n            [plate_t,       0],            // plate front-bottom\n            [plate_t,       plate_h - plate_t],     // arm root\n            [plate_t + arm_reach,          plate_h - plate_t],  // arm tip bottom\n            [plate_t + arm_reach,          plate_h - plate_t + tip_h], // tip top outer\n            [plate_t + arm_reach - plate_t, plate_h - plate_t + tip_h], // tip top inner\n            [plate_t + arm_reach - plate_t, plate_h],            // arm tip inner\n            [0,             plate_h],       // plate back-top\n        ]);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-desk-monitor-riser-foot",
      "title": "Monitor-riser foot",
      "description": "A single load-bearing foot/leg for a DIY monitor riser or laptop stand: a hollow tapered pedestal (wide stable base, narrower top) with a thick top deck and a centering spigot on top so a wooden or acrylic shelf board or a stacked foot locates onto it. Carved from a rounded prismoid with an internal cavity, printed open-side-down as one watertight shell. Tune the footprint, height, wall, taper, and spigot.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Monitor-riser foot\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbase_w    = 78;   // bottom footprint width/depth (square, sits on desk)\nfoot_h    = 50;   // height of one riser foot\ntop_w     = 60;   // top deck width/depth (narrower for a taper)\nwall      = 4;    // shell wall thickness\nspigot_d  = 22;   // top centering spigot diameter\nround_r   = 6;    // corner rounding\n$fn = 64;\n\nunion() {\n    difference() {\n        // Tapered pedestal: wide base, narrower top, rounded.\n        prismoid(size1 = [base_w, base_w], size2 = [top_w, top_w], h = foot_h,\n                 rounding = round_r, anchor = BOTTOM);\n        // Hollow cavity opening downward (prints open-side-down, no supports).\n        prismoid(size1 = [base_w - 2 * wall, base_w - 2 * wall],\n                 size2 = [top_w - 2 * wall, top_w - 2 * wall],\n                 h = foot_h - wall, anchor = BOTTOM);\n    }\n    // Top centering spigot.\n    up(foot_h)\n        cyl(h = 8, d = spigot_d, chamfer2 = 1.5, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-desk-pen-cup",
      "title": "Pen & pencil cup with base tray",
      "description": "A round desktop pen cup with a slightly flared rounded rim and a wide chamfered base disc that doubles as a stability foot and a tiny clip/eraser ledge. Built from a BOSL2 tube on a chamfered cylinder so it prints floor-down with no supports. Tune the outer diameter, height, wall, floor thickness, and base flare.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Pen & pencil cup with base tray\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ncup_d     = 70;   // outer diameter of the cup body\ncup_h     = 100;  // overall height\nwall      = 2.4;  // wall thickness\nfloor_t   = 3;    // floor thickness\nbase_flare = 12;  // extra diameter added at the base foot\n$fn = 96;\n\nunion() {\n    // Wide chamfered base foot for stability, with a solid floor on top.\n    cyl(h = floor_t + 4, d = cup_d + base_flare, chamfer1 = 1.2, rounding2 = 2, anchor = BOTTOM);\n    // Hollow body wall standing on the base, rounded top rim.\n    up(floor_t)\n        tube(h = cup_h - floor_t, od = cup_d, wall = wall,\n             orounding2 = 1.6, irounding2 = 1.6, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-desk-phone-stand",
      "title": "Phone stand",
      "description": "A single-piece angled phone stand: a back support leaned at an angle with a front lip ledge that catches the phone, joined to a flat base for stability. Built by unioning a leaned slab, a base plate, and a front lip so it prints as one solid with no supports. Tune the width, lean angle, support height, base depth, and slab thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Phone stand\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nstand_w    = 80;   // width across (phone width + margin)\nlean       = 20;   // backward lean of the support from vertical, degrees\nsupport_h  = 95;   // length of the leaning support slab\nbase_d     = 75;   // base plate depth on the desk\nthick      = 8;    // slab / base thickness\nlip_h      = 12;   // front catching lip height\n$fn = 48;\n\nunion() {\n    // Flat base plate for stability.\n    cuboid([stand_w, base_d, thick], rounding = 2, except = [TOP, BOTTOM], anchor = BOTTOM);\n\n    // Front catching lip running across the width near the base front edge.\n    translate([0, -base_d / 2 + thick + 2, 0])\n        cuboid([stand_w, thick, thick + lip_h], rounding = 2, except = [BOTTOM], anchor = BOTTOM);\n\n    // Leaning back support: a slab tilted backward, rooted at the base.\n    translate([0, -base_d / 2 + thick + 2 + thick, thick / 2])\n        xrot(lean)\n            cuboid([stand_w, thick, support_h], rounding = 2, except = [], anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-desk-sticky-note-tray",
      "title": "Sticky-note tray",
      "description": "A desktop sticky-note holder: a shallow rounded tray sized for a square sticky pad, with a scooped front thumb cut-out so you can peel the top note off easily, and a solid floor. Carved by difference from one rounded block so the whole tray is a single watertight piece. Tune the pad size, wall, tray height, and floor thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Sticky-note tray\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\npad_size  = 78;   // sticky pad edge length (76mm pad + clearance)\nwall      = 3;    // wall thickness\ntray_h    = 18;   // overall tray height\nfloor_t   = 2.5;  // floor thickness\nscoop_d   = 30;   // diameter of the front peel scoop\nround_r   = 4;    // outer corner rounding\n$fn = 64;\n\nbody = pad_size + 2 * wall;\n\ndifference() {\n    cuboid([body, body, tray_h], rounding = round_r, except = [TOP], anchor = BOTTOM);\n\n    // Pad pocket.\n    up(floor_t)\n        cuboid([pad_size, pad_size, tray_h], rounding = max(round_r - wall, 0.5),\n               except = [TOP], anchor = BOTTOM);\n\n    // Front peel scoop: a sphere carved into the front wall + floor lip.\n    translate([0, -body / 2, floor_t + 3])\n        sphere(d = scoop_d);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-desk-tablet-stand",
      "title": "Tablet stand",
      "description": "A wider, sturdier cousin of the phone stand sized for tablets: a leaned back support slab with a deep front trough ledge that cradles a tablet, joined to a heavy base plate for stability. Unioned from base, trough lips, and a tilted slab so it prints as one solid, ledge facing up, no supports. Tune the width, lean, support height, base depth, and trough gap.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Tablet stand\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nstand_w    = 180;  // width across (tablet width + margin)\nlean       = 22;   // backward lean of the support from vertical, degrees\nsupport_h  = 130;  // length of the leaning support slab\nbase_d     = 120;  // base plate depth on the desk\nthick      = 10;   // slab / base thickness\ntrough_gap = 16;   // gap between front lip and support = tablet thickness slot\n$fn = 48;\n\nunion() {\n    // Heavy base plate.\n    cuboid([stand_w, base_d, thick], rounding = 3, except = [TOP, BOTTOM], anchor = BOTTOM);\n\n    // Front lip forming the outer wall of the cradle trough.\n    translate([0, -base_d / 2 + thick + 4, 0])\n        cuboid([stand_w, thick, thick + 22], rounding = 2, except = [BOTTOM], anchor = BOTTOM);\n\n    // Leaning back support, set behind the trough gap.\n    translate([0, -base_d / 2 + thick + 4 + thick + trough_gap, thick / 2])\n        xrot(lean)\n            cuboid([stand_w, thick, support_h], rounding = 3, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-divided-tray",
      "title": "Divided tray (3 compartments)",
      "description": "A shallow desk tray split into three equal compartments by two internal dividers, with rounded outer corners and a solid floor. Carve out each compartment from a rounded block so the whole tray prints as one watertight piece. Tune the footprint, height, wall thickness, and floor thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Divided tray (3 compartments)\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ntray_w     = 120;  // outer width (X) — the long axis split into 3\ntray_d     = 50;   // outer depth (Y)\ntray_h     = 22;   // outer height (Z)\nwall       = 2.4;  // outer wall + divider thickness\nfloor_t    = 2;    // floor thickness\nround_r    = 4;    // outer corner rounding radius\n$fn = 48;\n\ncompartments = 3;\n// Total interior length available after the two outer walls.\ninner_len = tray_w - 2 * wall;\n// Each pocket width once the (compartments-1) dividers are removed.\npocket_w = (inner_len - (compartments - 1) * wall) / compartments;\npocket_d = tray_d - 2 * wall;\npocket_h = tray_h - floor_t;\n\ndifference() {\n    cuboid([tray_w, tray_d, tray_h], rounding = round_r, except = [TOP]);\n    // One open pocket per compartment, marching along X.\n    for (i = [0 : compartments - 1]) {\n        // Left edge of interior, then offset by each pocket + divider.\n        x0 = -tray_w / 2 + wall + i * (pocket_w + wall);\n        translate([x0 + pocket_w / 2, 0, -tray_h / 2 + floor_t])\n            cuboid([pocket_w, pocket_d, pocket_h + 1],\n                   rounding = max(round_r - wall, 0.5), except = [TOP], anchor = BOTTOM);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-dovetail-female",
      "title": "Dovetail female socket",
      "description": "A flat plate with a sliding dovetail groove (the female half) bored through it by the BOSL2 joiners library. The matching dovetail male connector slides in for a self-locking joint. Tune the plate size, groove width, height, and slide length.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Dovetail female socket\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/joiners.scad>\n\nplate      = [40, 40, 12];  // base plate size [x, y, z]\ntail_w     = 20;            // dovetail width (wide reference dimension)\ntail_h     = 8;            // dovetail height (groove depth)\nslide      = 30;           // length of the groove along the slide axis\nslope      = 5;            // flank slope (match the male part)\nentry_slot = 17;           // open entry slot so the male part can drop in\n$fn = 32;\n\ndiff()\n  cuboid(plate)\n    attach(TOP, BOTTOM, align = BACK, inside = true, inset = 5)\n      tag(\"remove\")\n        dovetail(\"female\", slide = slide, width = tail_w,\n                 height = tail_h, slope = slope,\n                 entry_slot_length = entry_slot);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-dovetail-male",
      "title": "Dovetail male connector",
      "description": "A flat connector plate with a sliding dovetail tongue (the male half) generated by the BOSL2 joiners library. Slides into the matching dovetail female socket for a self-locking woodworking-style joint. Tune the plate size, dovetail width, height, and slide length.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Dovetail male connector\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/joiners.scad>\n\nplate      = [40, 30, 8];  // base plate size [x, y, z]\ntail_w     = 20;           // dovetail width (wide reference dimension)\ntail_h     = 8;            // dovetail height (how far it sticks out)\nslide      = 30;           // length of the dovetail along the slide axis\nslope      = 5;            // flank slope (higher = shallower angle)\n$fn = 32;\n\ncuboid(plate)\n  attach(RIGHT, BOTTOM, align = BACK, inset = 5)\n    dovetail(\"male\", slide = slide, width = tail_w,\n             height = tail_h, slope = slope);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-drawer-organizer",
      "title": "Drawer organizer insert",
      "description": "A drop-in drawer insert with a grid of pockets for screws, stationery, or small parts. The whole tray is carved from a rounded block so it prints as one watertight piece — set the rows and columns to repartition the grid. Tune the footprint, height, grid size, wall thickness, and floor thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Drawer organizer insert\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\norg_w    = 140;  // outer width (X)\norg_d    = 90;   // outer depth (Y)\norg_h    = 28;   // outer height (Z)\ncols     = 4;    // number of pockets across X\nrows     = 3;    // number of pockets across Y\nwall     = 2;    // outer wall + divider thickness\nfloor_t  = 2;    // floor thickness\nround_r  = 3;    // outer corner rounding radius\n$fn = 48;\n\n// Interior span after the outer walls.\ninner_w = org_w - 2 * wall;\ninner_d = org_d - 2 * wall;\n// Each pocket size once the internal dividers are removed.\npocket_w = (inner_w - (cols - 1) * wall) / cols;\npocket_d = (inner_d - (rows - 1) * wall) / rows;\npocket_h = org_h - floor_t;\n\ndifference() {\n    cuboid([org_w, org_d, org_h], rounding = round_r, except = [TOP]);\n    for (cx = [0 : cols - 1], ry = [0 : rows - 1]) {\n        x0 = -org_w / 2 + wall + cx * (pocket_w + wall) + pocket_w / 2;\n        y0 = -org_d / 2 + wall + ry * (pocket_d + wall) + pocket_d / 2;\n        translate([x0, y0, -org_h / 2 + floor_t])\n            cuboid([pocket_w, pocket_d, pocket_h + 1],\n                   rounding = max(round_r - wall, 0.4), except = [TOP], anchor = BOTTOM);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-flanged-bushing",
      "title": "Flanged bushing",
      "description": "A plain bushing with an integral shoulder flange at one end, so it both press-fits into a bore and seats against the face of the housing to take axial load. Printed as one watertight solid (flange + sleeve share the same bore). Tune the sleeve OD to your bore, the flange diameter for the bearing face, the bore for your shaft, and the lengths.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Flanged bushing\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nsleeve_od  = 12;   // sleeve outer diameter (press-fit)\nflange_d   = 18;   // flange outer diameter (seating face)\nbore_d     = 8;    // shaft bore diameter\nsleeve_len = 14;   // sleeve length below the flange\nflange_t   = 3;    // flange thickness\n$fn = 96;\n\ndifference() {\n    union() {\n        // flange disc at the bottom\n        cyl(h = flange_t, d = flange_d, chamfer2 = 0.8, anchor = BOTTOM);\n        // sleeve rising from the top of the flange\n        up(flange_t)\n            cyl(h = sleeve_len, d = sleeve_od, chamfer2 = 0.8, anchor = BOTTOM);\n    }\n    // through bore\n    down(0.5)\n        cyl(h = flange_t + sleeve_len + 1, d = bore_d, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-flat-belt-pulley",
      "title": "Flat-belt pulley with bore",
      "description": "A crowned flat-belt pulley with a bored hub to run on a shaft. The slight barrel crown across the face keeps a flat belt tracked to the centre, and the bore lets it ride on a shaft or bushing. Printed as one watertight solid. Tune the rim diameter, crown, face width, and bore.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Flat-belt pulley with bore\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nrim_d    = 28;   // pulley diameter at the edges of the face\ncrown    = 1.2;  // extra radius at the centre of the crown\nface_w   = 14;   // axial width of the belt face\nbore_d   = 8;    // center bore diameter\n$fn = 120;\n\nmid_d = rim_d + 2 * crown;   // diameter at the crowned centre\n\ndifference() {\n    // crowned body of revolution\n    rotate_extrude($fn = 120)\n        polygon([\n            [bore_d/2,  0],\n            [rim_d/2,   0],\n            [mid_d/2,   face_w/2],\n            [rim_d/2,   face_w],\n            [bore_d/2,  face_w],\n        ]);\n    // through bore\n    down(1)\n        cylinder(h = face_w + 2, d = bore_d, $fn = 96);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-friction-fit-lid",
      "title": "Friction-fit box lid",
      "description": "A press-on lid with a downward inner lip that slides snugly inside a matching box opening, plus a finger-grip recess on top. Sized to pair with a rounded box base; adjust the clearance for a looser or tighter fit. Tune the opening size, lip depth, top thickness, and rounding.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Friction-fit box lid\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nopen_w    = 80;   // box outer width the lid sits over (X)\nopen_d    = 55;   // box outer depth the lid sits over (Y)\ntop_t     = 2.4;  // lid top plate thickness\nlip_h     = 8;    // depth of the inner lip\nlip_wall  = 2;    // lip wall thickness\nclearance = 0.3;  // gap per side for a friction fit\nround_r   = 5;    // outer corner rounding radius\n$fn = 64;\n\n// Inner opening the lip must drop into (box inner cavity).\ninner_w = open_w - 2 * 2.4 - 2 * clearance;\ninner_d = open_d - 2 * 2.4 - 2 * clearance;\n\nunion() {\n    // Top cap plate, rounded only on the vertical corners to match the box footprint.\n    cuboid([open_w, open_d, top_t], rounding = round_r, edges = \"Z\", anchor = BOTTOM);\n    // Hollow downward lip that grips the inside of the box.\n    down(lip_h)\n        difference() {\n            cuboid([inner_w, inner_d, lip_h], rounding = max(round_r - 2.4, 0.5), edges = \"Z\", anchor = BOTTOM);\n            up(0.01)\n                cuboid([inner_w - 2 * lip_wall, inner_d - 2 * lip_wall, lip_h + 0.02],\n                       rounding = max(round_r - 2.4 - lip_wall, 0.4), edges = \"Z\", anchor = BOTTOM);\n        }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-garden-hose-guide-stake",
      "title": "Hose guide stake",
      "description": "A ground stake topped with a smooth vertical roller post that steers a garden hose around bed corners without crushing plants. The post, collar, and pointed spike fuse into one printable solid. Tune the spike length, post height/diameter, and base collar.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Hose guide stake\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nspike_len   = 70;  // length of the pointed ground spike\nspike_d     = 14;  // spike diameter at the top (tapers to a point)\npost_h      = 90;  // height of the hose-guiding post above ground\npost_d      = 16;  // post diameter\ncollar_d    = 34;  // diameter of the base collar / soil stop\ncollar_h    = 6;   // collar thickness\n$fn = 64;\n\nunion() {\n    // Pointed ground spike, point down.\n    down(spike_len)\n        cyl(h = spike_len, d1 = 0.5, d2 = spike_d, anchor = BOTTOM);\n    // Base collar / soil stop sitting at ground level.\n    cyl(h = collar_h, d = collar_d, rounding = 2, anchor = BOTTOM);\n    // The guiding post with a rounded cap so the hose slides freely.\n    up(collar_h - 0.01)\n        cyl(h = post_h, d = post_d, rounding2 = post_d / 2 - 0.5,\n            anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-garden-plant-label",
      "title": "Engraved plant label stake",
      "description": "A flat plant label on a pointed push-in stake, with the plant name engraved into the face via difference so it prints as one connected solid. Tune the label size, stake length, plate thickness, engrave depth, and the text.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Engraved plant label stake\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nlabel_text   = \"BASIL\";  // the name to engrave\nplate_w      = 60;       // label plate width (X)\nplate_h      = 22;       // label plate height (Z of the face)\nplate_t      = 3;        // plate thickness (Y)\nstake_len    = 45;       // length of the pointed push-in stake below the plate\nfont_size    = 11;       // engraved text size\nengrave_d    = 1.2;      // how deep the text is cut into the face\n$fn = 48;\n\ndifference() {\n    union() {\n        // The flat label plate. Rounding must stay under half the thin Y\n        // dimension, so keep it modest to avoid BOSL2's size assertion.\n        cuboid([plate_w, plate_t, plate_h], rounding = plate_t / 2 - 0.2,\n               edges = \"Z\", anchor = BOTTOM);\n        // Pointed stake hanging below the plate (a downward wedge).\n        down(stake_len)\n            prismoid(size1 = [4, plate_t], size2 = [14, plate_t],\n                     h = stake_len, anchor = BOTTOM);\n    }\n    // Engrave the text into the front face (+Y side).\n    up(plate_h / 2)\n        fwd(plate_t / 2 - engrave_d)\n            xrot(90)\n                linear_extrude(height = engrave_d + 0.5)\n                    text(label_text, size = font_size, halign = \"center\",\n                         valign = \"center\", font = \"Liberation Sans:style=Bold\");\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-garden-plant-pot",
      "title": "Plant pot with drainage holes",
      "description": "A tapered round plant pot with a solid floor pierced by a ring of drainage holes. Carved from a tapered tube so it prints as one watertight shell. Tune the top/bottom diameters, height, wall, floor thickness, and drainage hole count/size.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Plant pot with drainage holes\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\npot_top_d   = 90;  // outer diameter at the rim (X/Y)\npot_bot_d   = 70;  // outer diameter at the base — smaller for a taper\npot_h       = 80;  // overall height (Z)\nwall        = 3;   // side-wall thickness\nfloor_t     = 4;   // floor thickness\ndrain_n     = 6;   // number of drainage holes around the centre\ndrain_d     = 7;   // diameter of each drainage hole\n$fn = 96;\n\n// Drainage ring sits inside the base footprint, clear of the wall.\ndrain_ring_r = (pot_bot_d / 2 - wall) * 0.55;\n\ndifference() {\n    // Solid tapered body (cone frustum).\n    cyl(h = pot_h, d1 = pot_bot_d, d2 = pot_top_d, anchor = BOTTOM);\n    // Hollow the inside, leaving the floor.\n    up(floor_t)\n        cyl(h = pot_h, d1 = pot_bot_d - 2 * wall, d2 = pot_top_d - 2 * wall,\n            anchor = BOTTOM);\n    // Drainage holes through the floor.\n    for (i = [0 : drain_n - 1]) {\n        zrot(i * 360 / drain_n)\n            right(drain_ring_r)\n                cyl(h = floor_t + 2, d = drain_d, anchor = BOTTOM, $fn = 32);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-garden-pot-saucer",
      "title": "Pot saucer / drip tray",
      "description": "A shallow round saucer that catches drainage water under a plant pot, with a slightly raised inner lip and rounded outer edge. Carved from a rounded disc so it prints as one watertight piece. Tune the outer diameter, height, wall, and floor thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Pot saucer / drip tray\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nsaucer_d   = 110;  // outer diameter (X/Y) — make ~20mm bigger than the pot base\nsaucer_h   = 18;   // overall height (Z)\nwall       = 3;    // side-wall thickness\nfloor_t    = 3;    // floor thickness\nedge_r     = 3;    // rounding radius on the outer rim\n$fn = 120;\n\ndifference() {\n    // Solid disc with rounded top + bottom outer edges.\n    cyl(h = saucer_h, d = saucer_d, rounding = edge_r, anchor = BOTTOM);\n    // Carve the water-catching basin, leaving floor + walls.\n    up(floor_t)\n        cyl(h = saucer_h, d = saucer_d - 2 * wall,\n            rounding2 = max(edge_r - wall, 0.5), anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-garden-row-marker-sign",
      "title": "Row-marker sign on stake",
      "description": "A small angled signboard on a flat ground stake for labelling vegetable rows, with the crop name engraved into the tilted face via difference so it reads from above. The board, stake, and point fuse into one printable solid. Tune the board size, stake length, tilt, and the text.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Row-marker sign on stake\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nsign_text   = \"CARROTS\";  // crop name to engrave\nboard_w     = 70;         // signboard width (X)\nboard_h     = 26;         // signboard height (Y, before the tilt)\nboard_t     = 4;          // board thickness\nstake_len   = 60;         // length of the flat ground stake\nstake_w     = 16;         // stake width\nfont_size   = 11;         // engraved text size\nengrave_d   = 1.2;        // how deep the text is cut\n$fn = 48;\n\nunion() {\n    // Flat ground stake with a pointed tip at the bottom.\n    down(stake_len)\n        prismoid(size1 = [4, board_t], size2 = [stake_w, board_t],\n                 h = stake_len, anchor = BOTTOM);\n    // Tilted signboard sitting on top of the stake, engraved on its face.\n    // Sink it low enough that the board overlaps the stake top into one shell.\n    up(board_h / 2 - 12)\n        xrot(-20)\n            difference() {\n                cuboid([board_w, board_h, board_t],\n                       rounding = board_t / 2 - 0.2, edges = \"Y\");\n                // Engrave the crop name into the +Z face.\n                up(board_t / 2 - engrave_d)\n                    linear_extrude(height = engrave_d + 0.5)\n                        text(sign_text, size = font_size, halign = \"center\",\n                             valign = \"center\",\n                             font = \"Liberation Sans:style=Bold\");\n            }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-garden-seed-tray-cell",
      "title": "Seed-starting tray (2x2 cells)",
      "description": "A compact seed-starting tray with a 2x2 grid of tapered cells, each with its own drainage hole, joined by a shared rim so the whole tray lifts as one piece. Carved from a solid block so it prints watertight. Tune the cell count per side, cell size/depth, wall, and drain hole size.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Seed-starting tray (2x2 cells)\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ncells       = 2;   // cells per side (2 -> a 2x2 tray)\ncell_top    = 34;  // inner width of each cell at the top\ncell_depth  = 40;  // cell depth (Z)\nwall        = 2.4; // wall between/around cells\nfloor_t     = 2;   // floor thickness under each cell\ndrain_d     = 5;   // drainage hole diameter per cell\n$fn = 48;\n\ntaper      = 6;             // how much narrower the cell base is per side\npitch      = cell_top + wall;\ntray_w     = cells * cell_top + (cells + 1) * wall;\ntray_h     = cell_depth + floor_t;\n\ndifference() {\n    // Solid outer block with rounded vertical corners.\n    cuboid([tray_w, tray_w, tray_h], rounding = 3, edges = \"Z\",\n           anchor = BOTTOM);\n    // Carve each tapered cell.\n    for (ix = [0 : cells - 1], iy = [0 : cells - 1]) {\n        cx = -tray_w / 2 + wall + cell_top / 2 + ix * pitch;\n        cy = -tray_w / 2 + wall + cell_top / 2 + iy * pitch;\n        translate([cx, cy, floor_t])\n            prismoid(size1 = [cell_top - taper, cell_top - taper],\n                     size2 = [cell_top, cell_top],\n                     h = cell_depth + 1, rounding = 2, anchor = BOTTOM);\n        // Drainage hole through the floor of that cell.\n        translate([cx, cy, -1])\n            cyl(h = floor_t + 2, d = drain_d, anchor = BOTTOM, $fn = 24);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-garden-trellis-clip",
      "title": "Trellis / vine clip",
      "description": "A C-shaped open clip that snaps onto a trellis wire or thin stake and gently holds a plant stem in a second loop, training climbers without ties. Built as one wedge-and-ring body so it prints connected. Tune the wire and stem diameters, the snap-gap, wall thickness, and clip depth.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Trellis / vine clip\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nwire_d    = 5;    // diameter of the trellis wire / stake the clip grips\nstem_d    = 9;    // diameter of the plant stem the clip cradles\nwall      = 2.4;  // wall thickness of both rings\ngap_frac  = 0.55; // snap-opening as a fraction of the inner diameter\ndepth     = 12;   // extrusion depth (clip width along Z)\n$fn = 64;\n\nwire_or = wire_d / 2 + wall;\nstem_or = stem_d / 2 + wall;\nspacing = wire_or + stem_or - wall;  // overlap so the two rings merge\n\n// A single open C-ring: a full ring minus a wedge slice for the snap mouth.\nmodule c_ring(inner_d, w, open_frac) {\n    half = asin(min(1, max(0, open_frac)) * inner_d / 2 / (inner_d / 2 + w));\n    difference() {\n        tube(h = depth, id = inner_d, wall = w, anchor = BOTTOM);\n        // Cut the mouth opening facing +X.\n        rotate_extrude(angle = 2 * half)\n            zrot(-half)\n            right(inner_d / 2 - 0.5)\n                square([inner_d / 2 + w + 1, depth + 2]);\n    }\n}\n\nunion() {\n    // Wire-gripping C-clip, mouth facing outward (+X).\n    c_ring(wire_d, wall, gap_frac);\n    // Stem-cradling C-clip, mouth facing the other way (-X), offset along X.\n    right(spacing)\n        zrot(180)\n            c_ring(stem_d, wall, gap_frac);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-garden-watering-spike",
      "title": "Bottle watering spike",
      "description": "A slow-release watering spike that screws into a standard PET bottle neck and drips into the soil through a hollow pointed stake with side weep holes. Bored hollow with a through-channel so water flows; prints as one shell. Tune the bottle-neck diameter, spike length, bore, and weep holes.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Bottle watering spike\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nneck_d     = 25;  // outer diameter of the bottle-neck collar\nneck_h     = 18;  // collar height that grips the inverted bottle neck\nspike_len  = 75;  // length of the soil spike below the collar\nspike_d    = 18;  // spike diameter at the collar (tapers to a tip)\nbore_d     = 6;   // inner water channel diameter\nweep_d     = 2.5; // diameter of the side weep holes\n$fn = 64;\n\ndifference() {\n    union() {\n        // Collar that slips over / threads into the bottle neck.\n        cyl(h = neck_h, d = neck_d, rounding = 2, anchor = BOTTOM);\n        // Tapered soil spike pointing down. Overlap into the collar so the\n        // two bodies fuse into one shell.\n        down(spike_len)\n            cyl(h = spike_len + 2, d1 = 2, d2 = spike_d, anchor = BOTTOM);\n    }\n    // Hollow water channel, open at the top. It stops part-way down where the\n    // spike is still safely wider than the bore, so the tapered tip stays a\n    // connected solid plug (no severed floating piece).\n    bore_stop = 35;  // distance the bore reaches below the collar\n    down(bore_stop)\n        cyl(h = bore_stop + neck_h + 1, d = bore_d, anchor = BOTTOM);\n    // Side weep holes that connect the bore to the soil, kept inside the\n    // bored region and shorter than the spike so they don't sever a ring.\n    for (i = [0 : 3]) {\n        zrot(i * 90)\n            down(bore_stop - 8)\n                right(spike_d / 6)\n                    xrot(90)\n                        cyl(h = spike_d * 0.7, d = weep_d, anchor = CENTER,\n                            $fn = 20);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-gt2-pulley",
      "title": "GT2 timing pulley",
      "description": "A GT2 timing-belt pulley with 2 mm pitch tooth grooves and a flange on each end to keep the belt tracking. The default 20-tooth size is the common 3D-printer X/Y pulley. Tune the tooth count, bore, belt width, and flange size. Add a flat or grub-screw later if you need to lock it to a shaft.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// GT2 timing pulley\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nteeth    = 20;    // number of belt teeth (20 = standard printer pulley)\nbore_d   = 5;     // shaft bore diameter in mm\nbelt_w   = 6;     // belt width / toothed band height in mm\nflange_h = 1;     // flange thickness at each end in mm\nflange_o = 1.4;   // how far each flange sticks out past the tip radius in mm\n$fn = 96;\n\npitch       = 2;                       // GT2 pitch = 2 mm\npitch_d     = teeth * pitch / PI;      // pitch diameter\ngroove_r    = 0.555;                   // GT2 groove radius (tooth valley)\ngroove_off  = 0.254;                   // groove center offset below pitch line\n// outer (tip) radius of the toothed band\ntip_r       = pitch_d / 2 + 0.15;\n// radius the groove centers sit on\ngroove_ctr_r = pitch_d / 2 + groove_off + groove_r - 0.40;\nflange_r    = tip_r + flange_o;\n\nunion() {\n    difference() {\n        union() {\n            // toothed band\n            cylinder(h = belt_w, r = tip_r, center = false);\n            // bottom flange\n            cylinder(h = flange_h, r = flange_r, center = false);\n            // top flange\n            translate([0, 0, belt_w - flange_h])\n                cylinder(h = flange_h, r = flange_r, center = false);\n        }\n        // cut the GT2 tooth grooves into the band\n        for (i = [0 : teeth - 1]) {\n            ang = i * 360 / teeth;\n            rotate([0, 0, ang])\n                translate([groove_ctr_r, 0, -1])\n                    cylinder(h = belt_w + 2, r = groove_r, center = false, $fn = 24);\n        }\n        // center bore\n        translate([0, 0, -1])\n            cylinder(h = belt_w + 2, d = bore_d, center = false);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-gusset-bracket",
      "title": "Right-angle gusset bracket",
      "description": "A heavy-duty 90 degree angle bracket: two square bolt-down plates meeting at a right angle, braced by a full-height central triangular gusset web so it resists bending under load. Each plate carries a bolt hole. Fuses into one watertight solid that prints with the inside corner on the bed. Tune the plate size, thickness, gusset thickness, and bolt-hole diameter.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Right-angle gusset bracket\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nplate    = 40;   // square plate side length\nthick    = 5;    // plate thickness\ngusset_t = 6;    // gusset web thickness\nhole_d   = 5;    // bolt-hole diameter\n$fn = 48;\n\ndifference() {\n    union() {\n        // Bottom plate on the bed.\n        cuboid([plate, plate, thick], anchor = BOTTOM + LEFT);\n        // Upright plate at the left edge.\n        cuboid([thick, plate, plate], anchor = BOTTOM + LEFT);\n        // Central triangular gusset web in the X-Z plane, spanning both plates.\n        translate([0, -gusset_t / 2, 0])\n            linear_extrude(height = gusset_t)\n                polygon([[thick, thick], [plate, thick], [thick, plate]]);\n    }\n    // Hole in the bottom plate.\n    translate([plate * 0.7, plate / 2, thick / 2])\n        cyl(d = hole_d, h = thick + 2, anchor = CENTER);\n    // Hole in the upright plate.\n    translate([thick / 2, plate / 2, plate * 0.7]) yrot(90)\n        cyl(d = hole_d, h = thick + 2, anchor = CENTER);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-heatset-insert-boss",
      "title": "Heat-set insert boss",
      "description": "A mounting boss with a tapered pilot bore sized for a brass heat-set threaded insert, plus a chamfered lead-in so the insert seats square. Built from the BOSL2 shape library with rounded fillets for strength. A single printable solid. Tune the boss size and the insert pilot diameters.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Heat-set insert boss\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/shapes3d.scad>\n\nboss_d      = 12;    // outside diameter of the boss (mm)\nboss_h      = 10;    // boss height (mm)\ninsert_top  = 5.0;   // pilot diameter at the mouth (mm) — match your insert's body\ninsert_tip  = 4.6;   // pilot diameter at the bottom (mm) — slight taper grips the melt\ninsert_h    = 8;     // depth of the insert pilot bore (mm)\n$fn = 64;\n\ndifference() {\n    // Solid boss with a rounded base fillet for a strong joint.\n    cyl(d = boss_d, h = boss_h, rounding1 = 1.5, chamfer2 = 0.8, anchor = BOTTOM);\n    // Tapered pilot bore for the heat-set insert, open at the top.\n    up(boss_h - insert_h)\n        cyl(d1 = insert_tip, d2 = insert_top, h = insert_h + 0.02,\n            chamfer2 = 0.6, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-herringbone-gear",
      "title": "Herringbone gear",
      "description": "A double-helical (herringbone) spur gear. The mirrored helical teeth cancel axial thrust while running smoother and quieter than straight-cut teeth, and the chevron pattern prints cleanly without supports. Tune the module, tooth count, thickness, helix angle, and bore.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Herringbone gear\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/gears.scad>\n\nteeth      = 20;   // number of teeth\ngear_mod   = 2;    // tooth module in mm\nthickness  = 12;   // gear plate thickness in mm\nhelix_ang  = 30;   // helix angle in degrees\nshaft_d    = 6;    // center shaft bore diameter in mm\n$fn = 64;\n\nspur_gear(mod = gear_mod, teeth = teeth, thickness = thickness,\n          shaft_diam = shaft_d, helical = helix_ang, herringbone = true,\n          pressure_angle = 20);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-home-bag-clip",
      "title": "Bag / chip-bag sealing clip",
      "description": "A springy C-shaped clip that pinches a folded bag closed. Two parallel jaws join at a solid left spine; the narrow mouth gap at the tips lets the printed plastic flex to grip. Built as a union of the spine plus two jaws so it prints flat as one connected piece. Tune the jaw length, width, gap, and spine thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Bag / chip-bag sealing clip\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\njaw_len   = 70;   // length of the gripping jaws (X)\nclip_w    = 16;   // width across the clip (Y)\njaw_t     = 3;    // thickness of each jaw\ngap       = 1.4;  // mouth gap between jaw tips (the grip)\nspine_h   = 14;   // inner height between the jaws at the spine\n$fn = 48;\n\nspine_t = 4;  // thickness of the closed hinge spine on the left\nouter_h = spine_h + 2 * jaw_t;\nfull_l  = jaw_len + spine_t;\n\n// Profile in X (along jaws) / Z (height): a left spine column plus a top and a\n// bottom jaw. The jaws angle inward at the tips by (spine_h-gap)/2 so the mouth\n// pinches to the gap. Union keeps everything one connected ring.\ntip_dy = (spine_h - gap) / 2;  // how far each jaw tip closes in toward center\n\nrotate([90, 0, 0])\nlinear_extrude(height = clip_w, center = true)\n    union() {\n        // Left spine joining both jaws.\n        left((full_l - spine_t) / 2)\n            rect([spine_t, outer_h], rounding = 1);\n        // Top jaw, tapering inward toward the tip.\n        polygon([\n            [-full_l / 2 + spine_t * 0.5, spine_h / 2],\n            [ full_l / 2,                 gap / 2],\n            [ full_l / 2,                 gap / 2 + jaw_t],\n            [-full_l / 2 + spine_t * 0.5, spine_h / 2 + jaw_t],\n        ]);\n        // Bottom jaw, mirrored.\n        polygon([\n            [-full_l / 2 + spine_t * 0.5, -spine_h / 2 - jaw_t],\n            [ full_l / 2,                 -gap / 2 - jaw_t],\n            [ full_l / 2,                 -gap / 2],\n            [-full_l / 2 + spine_t * 0.5, -spine_h / 2],\n        ]);\n    }\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-home-coaster",
      "title": "Beveled drink coaster",
      "description": "A solid round drink coaster with a beveled top edge and a shallow recessed well to catch condensation drips, plus a fine bottom chamfer for a clean first layer. Tune the diameter, thickness, bevel, and well depth.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Beveled drink coaster\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ncoaster_d  = 95;   // outer diameter\ncoaster_h  = 8;    // overall thickness\ntop_bevel  = 3;    // chamfer on the top outer edge\nwell_depth = 1.6;  // depth of the central condensation well\nwell_inset = 8;    // ring of flat rim left around the well\n$fn = 128;\n\ndifference() {\n    // Solid disc, top edge beveled, base lightly chamfered for first-layer adhesion.\n    cyl(h = coaster_h, d = coaster_d,\n        chamfer2 = top_bevel, chamfer1 = 0.8, anchor = BOTTOM);\n    // Shallow recessed well on top to hold drips, with a soft rounded lip.\n    up(coaster_h - well_depth)\n        cyl(h = well_depth + 1, d = coaster_d - 2 * (top_bevel + well_inset),\n            rounding1 = 1.2, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-home-fridge-clip",
      "title": "Fridge note clip with magnet pocket",
      "description": "A small spring clip for pinning notes to a fridge. Two tapering jaws join at a solid spine and pinch to a narrow mouth gap to grip paper; a round pocket is carved into the back of the spine to glue in a magnet. Built as a union minus the pocket so it prints as one connected piece. Tune the jaw size, gap, and magnet diameter/depth.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Fridge note clip with magnet pocket\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbody_w    = 28;   // width of the clip (Y)\njaw_len   = 30;   // length of the jaws (X)\njaw_t     = 2.8;  // thickness of each jaw\ngap       = 1.0;  // mouth gap at the jaw tips\nmouth_h   = 8;    // inner height between jaws at the spine\nmag_d     = 12;   // magnet diameter\nmag_depth = 2.5;  // magnet pocket depth\n$fn = 64;\n\nspine_t = 6;  // thickness of the closed spine (holds the magnet)\nouter_h = mouth_h + 2 * jaw_t;\nfull_l  = jaw_len + spine_t;\n\ndifference() {\n    // C-clip body: union of spine + two tapering jaws, extruded along Y.\n    rotate([90, 0, 0])\n    linear_extrude(height = body_w, center = true)\n        union() {\n            left((full_l - spine_t) / 2)\n                rect([spine_t, outer_h], rounding = 1);\n            polygon([\n                [-full_l / 2 + spine_t * 0.5,  mouth_h / 2],\n                [ full_l / 2,                  gap / 2],\n                [ full_l / 2,                  gap / 2 + jaw_t],\n                [-full_l / 2 + spine_t * 0.5,  mouth_h / 2 + jaw_t],\n            ]);\n            polygon([\n                [-full_l / 2 + spine_t * 0.5, -mouth_h / 2 - jaw_t],\n                [ full_l / 2,                 -gap / 2 - jaw_t],\n                [ full_l / 2,                 -gap / 2],\n                [-full_l / 2 + spine_t * 0.5, -mouth_h / 2],\n            ]);\n        }\n    // Magnet pocket bored into the back (left) face of the spine, toward -X.\n    translate([-full_l / 2 - 0.01, 0, 0])\n        rotate([0, 90, 0])\n            cyl(h = mag_depth + 0.02, d = mag_d, anchor = TOP);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-home-napkin-ring",
      "title": "Napkin ring",
      "description": "A simple round napkin ring — a short tube with softly rounded inner and outer rims so it slides over a rolled napkin without catching. Prints flat on its rim with no supports. Tune the inner diameter, wall thickness, and height.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Napkin ring\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ninner_d = 42;   // inner diameter (napkin bundle size)\nwall    = 4;    // ring wall thickness\nring_h  = 30;   // ring height\nrim_r   = 2;    // rounding on the top and bottom rims\n$fn = 128;\n\ntube(h = ring_h, id = inner_d, wall = wall,\n     orounding1 = rim_r, orounding2 = rim_r,\n     irounding1 = rim_r, irounding2 = rim_r,\n     anchor = BOTTOM);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-home-soap-dish",
      "title": "Soap dish with drain slots",
      "description": "A rounded-rectangle soap dish with a raised lip and a floor cut by parallel drain slots so water runs off and the bar dries. The slotted floor keeps everything one watertight print. Tune the footprint, height, wall, and slot count/width.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Soap dish with drain slots\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ndish_w   = 110;  // outer width (X)\ndish_d   = 75;   // outer depth (Y)\ndish_h   = 20;   // outer height (Z)\nwall     = 2.6;  // outer wall thickness\nfloor_t  = 2.2;  // slotted floor thickness\nround_r  = 6;    // outer corner rounding\nslots    = 7;    // number of drain slots\nslot_w   = 4;    // width of each drain slot\n$fn = 64;\n\ninclude <BOSL2/std.scad>\n\ninner_w = dish_w - 2 * wall;\ninner_d = dish_d - 2 * wall;\npitch   = inner_w / slots;\n\ndifference() {\n    // Hollow rounded tray.\n    difference() {\n        cuboid([dish_w, dish_d, dish_h], rounding = round_r, except = [TOP]);\n        up(floor_t)\n            cuboid([inner_w, inner_d, dish_h], rounding = max(round_r - wall, 1),\n                   except = [TOP], anchor = BOTTOM);\n    }\n    // Drain slots cut through the floor, running along Y.\n    for (i = [0 : slots - 1]) {\n        x = -inner_w / 2 + pitch / 2 + i * pitch;\n        translate([x, 0, -dish_h / 2 - 1])\n            cuboid([slot_w, inner_d - 8, floor_t + 2], rounding = slot_w / 2 - 0.2,\n                   edges = [FRONT, BACK], anchor = BOTTOM);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-home-sponge-holder",
      "title": "Sink sponge holder",
      "description": "A sink-side sponge tray with tall slotted side walls for airflow and a slotted floor for drainage, so a wet sponge dries from every side. Walls and floor are cut from a rounded open box so it stays one watertight print. Tune the footprint, height, wall, and slot spacing.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Sink sponge holder\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ntray_w   = 130;  // outer width (X)\ntray_d   = 75;   // outer depth (Y)\ntray_h   = 45;   // outer height (Z)\nwall     = 2.6;  // wall thickness\nfloor_t  = 2.2;  // floor thickness\nround_r  = 5;    // outer corner rounding\nslot_w   = 6;    // width of the ventilation/drain slots\n$fn = 48;\n\ninner_w = tray_w - 2 * wall;\ninner_d = tray_d - 2 * wall;\n\n// Slots along the long walls (cut in X-direction openings through Y walls).\nnx = floor(inner_w / (slot_w * 2));\npitch_x = inner_w / nx;\n// Floor drain slots running along X.\nnf = floor(inner_d / (slot_w * 2));\npitch_f = inner_d / nf;\n\ndifference() {\n    // Hollow rounded tray.\n    difference() {\n        cuboid([tray_w, tray_d, tray_h], rounding = round_r, except = [TOP]);\n        up(floor_t)\n            cuboid([inner_w, inner_d, tray_h], rounding = max(round_r - wall, 1),\n                   except = [TOP], anchor = BOTTOM);\n    }\n    // Vertical vent slots through both long (front/back) walls.\n    for (i = [0 : nx - 1]) {\n        x = -inner_w / 2 + pitch_x / 2 + i * pitch_x;\n        translate([x, 0, floor_t + (tray_h - floor_t) / 2])\n            cuboid([slot_w, tray_d + 2, tray_h - floor_t - 8],\n                   rounding = slot_w / 2 - 0.2, edges = [TOP, BOTTOM]);\n    }\n    // Floor drain slots running along X.\n    for (j = [0 : nf - 1]) {\n        y = -inner_d / 2 + pitch_f / 2 + j * pitch_f;\n        translate([0, y, -tray_h / 2 - 1])\n            cuboid([inner_w - 10, slot_w, floor_t + 2],\n                   rounding = min(slot_w / 2 - 0.2, (floor_t + 2) / 2 - 0.4),\n                   edges = [LEFT, RIGHT], anchor = BOTTOM);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-home-toothbrush-holder",
      "title": "Toothbrush holder",
      "description": "A round bathroom cup with a perforated top deck — drilled holes hold toothbrushes upright while the open base below lets water drain to the floor of the cup. One watertight print with a chamfered base. Tune the diameter, height, hole count, and hole size.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Toothbrush holder\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ncup_d    = 70;   // outer diameter\ncup_h    = 100;  // overall height\nwall     = 2.6;  // wall thickness\nfloor_t  = 3;    // base floor thickness\ndeck_t   = 4;    // thickness of the perforated top deck\nholes    = 4;    // number of brush holes\nhole_d   = 13;   // diameter of each brush hole\n$fn = 96;\n\nring_r = cup_d / 4;  // radius of the hole circle\n\ndifference() {\n    union() {\n        // Outer wall.\n        up(floor_t)\n            tube(h = cup_h - floor_t, od = cup_d, wall = wall, anchor = BOTTOM);\n        // Solid floor.\n        cyl(h = floor_t, d = cup_d, chamfer1 = 0.8, anchor = BOTTOM);\n        // Top perforated deck spanning the bore.\n        up(cup_h - deck_t)\n            cyl(h = deck_t, d = cup_d - 0.01, anchor = BOTTOM);\n    }\n    // Brush holes through the deck.\n    for (i = [0 : holes - 1]) {\n        a = i * 360 / holes;\n        translate([ring_r * cos(a), ring_r * sin(a), cup_h - deck_t - 1])\n            cyl(h = deck_t + 2, d = hole_d, anchor = BOTTOM);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-home-utensil-caddy",
      "title": "Cutlery caddy (4 bays)",
      "description": "A countertop cutlery caddy split into four open bays by internal dividers, with rounded corners and a slotted floor so rinse water drains away. Pockets and slots are carved from a rounded block so it prints as one watertight piece. Tune the footprint, height, walls, and bay count.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Cutlery caddy (4 bays)\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ncaddy_w  = 150;  // outer width (X) — split into bays\ncaddy_d  = 65;   // outer depth (Y)\ncaddy_h  = 110;  // outer height (Z)\nwall     = 2.6;  // outer wall + divider thickness\nfloor_t  = 2.4;  // floor thickness\nround_r  = 5;    // outer corner rounding\nbays     = 4;    // number of cutlery bays\n$fn = 48;\n\ninner_w  = caddy_w - 2 * wall;\npocket_w = (inner_w - (bays - 1) * wall) / bays;\npocket_d = caddy_d - 2 * wall;\npocket_h = caddy_h - floor_t;\n\ndifference() {\n    cuboid([caddy_w, caddy_d, caddy_h], rounding = round_r, except = [TOP]);\n    // Open bay per compartment.\n    for (i = [0 : bays - 1]) {\n        x0 = -caddy_w / 2 + wall + i * (pocket_w + wall);\n        translate([x0 + pocket_w / 2, 0, -caddy_h / 2 + floor_t])\n            cuboid([pocket_w, pocket_d, pocket_h + 1],\n                   rounding = max(round_r - wall, 0.6), except = [TOP], anchor = BOTTOM);\n        // Two drain slots through the floor of each bay.\n        for (s = [-1, 1])\n            translate([x0 + pocket_w / 2 + s * pocket_w / 4, 0, -caddy_h / 2 - 1])\n                cuboid([3.5, pocket_d - 10, floor_t + 2], rounding = 1.5,\n                       edges = [FRONT, BACK], anchor = BOTTOM);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-idler-pulley",
      "title": "Idler pulley with bore",
      "description": "A grooved idler/guide pulley for round belt or cord, with a center bore to run on a shaft or bushing. The V-groove keeps the belt tracked; two flanges contain it. Printed as one watertight solid. Tune the outer diameter, groove depth, width, and bore to suit your belt and shaft.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Idler pulley with bore\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nouter_d    = 24;   // flange (rim) outer diameter\ngroove_d   = 18;   // diameter at the bottom of the V-groove\nwidth      = 10;   // overall axial width\nbore_d     = 6;    // center bore diameter\nflange_t   = 2;    // thickness of each outer flange\n$fn = 120;\n\ngroove_w = width - 2 * flange_t;   // axial width of the groove mouth\n\ndifference() {\n    // solid body of revolution: two flanges + V-groove between them\n    rotate_extrude($fn = 120)\n        polygon([\n            [bore_d/2,        0],\n            [outer_d/2,       0],\n            [outer_d/2,       flange_t],\n            [groove_d/2,      width/2],\n            [outer_d/2,       width - flange_t],\n            [outer_d/2,       width],\n            [bore_d/2,        width],\n        ]);\n    // clean through bore\n    down(1)\n        cylinder(h = width + 2, d = bore_d, $fn = 96);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-knuckle-hinge-leaf",
      "title": "Knuckle hinge leaf",
      "description": "One leaf of a classic knuckle (butt) hinge — a flat plate with rounded barrel knuckles and a through pin bore, built from BOSL2 attachable primitives as a single printable solid. Print two and join with a pin rod. Tune the leaf size, knuckle count, barrel diameter, and pin bore.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Knuckle hinge leaf\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nleaf_w     = 40;   // leaf width along the hinge axis\nleaf_d     = 25;   // leaf depth (away from the barrel)\nleaf_t     = 3;    // leaf plate thickness\nbarrel_d   = 9;    // outer diameter of the barrel knuckles\npin_d      = 3.2;  // pin bore diameter through the knuckles\nknuckles   = 3;    // number of knuckles on this leaf\ngap        = 0.4;  // gap between this leaf's knuckles (for the mating leaf)\n$fn = 48;\n\nbarrel_r  = barrel_d / 2;\nseg_pitch = leaf_w / (2 * knuckles - 1);   // knuckle + matching gap\n\ndifference() {\n  union() {\n    // Flat leaf plate; barrel axis runs along X, plate reaches back from it\n    translate([0, barrel_r, 0])\n      cuboid([leaf_w, leaf_d, leaf_t]);\n    // Knuckle barrels along the X axis, overlapping the plate front edge\n    for (i = [0 : knuckles - 1]) {\n      xpos = -leaf_w/2 + seg_pitch/2 + i * 2 * seg_pitch;\n      translate([xpos, 0, 0])\n        xcyl(l = seg_pitch - gap, d = barrel_d);\n    }\n  }\n  // Pin bore through every knuckle\n  xcyl(l = leaf_w + 2, d = pin_d);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-knurled-knob",
      "title": "Knurled knob (threaded bore)",
      "description": "A grippy diamond-knurled control knob with an internally threaded blind bore so it screws onto a bolt or shaft, generated by the BOSL2 cylinder texture + threading libraries. A single printable solid. Tune the knob diameter, height, thread size and pitch.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Knurled knob (threaded bore)\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/threading.scad>\n\nknob_d   = 30;    // outside diameter of the knob (mm)\nknob_h   = 16;    // overall knob height (mm)\nthread_d = 8;     // bore thread major diameter (mm)\npitch    = 1.25;  // bore thread pitch (mm)\nbore_h   = 12;    // depth of the threaded bore (mm)\n$fn = 96;\n\ndifference() {\n    // Knurled grip body with a domed-ish flat top.\n    cyl(d = knob_d, h = knob_h, rounding2 = 3, chamfer1 = 1,\n        texture = \"diamonds\", tex_size = [3, 3], tex_depth = 0.9,\n        anchor = BOTTOM);\n    // Internal thread, cut from the bottom up.\n    up(-0.01)\n        threaded_nut_hole();\n}\n\nmodule threaded_nut_hole() {\n    // Female thread carved into the body.\n    threaded_rod(d = thread_d, l = bore_h + 0.02, pitch = pitch,\n                 internal = true, bevel = false, blunt_start = false,\n                 anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-l-bracket",
      "title": "Rounded L-bracket",
      "description": "A right-angle mounting bracket with two filleted flanges and countersunk-free thru-holes on each face. The 90 degree inside corner is reinforced so it prints as one watertight solid. Tune the flange length, width, plate thickness, corner rounding, and bolt-hole diameter.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Rounded L-bracket\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nleg     = 40;   // length of each flange\nwidth   = 30;   // bracket width (depth)\nthick   = 4;    // plate thickness\nround_r = 3;    // outer corner rounding radius\nhole_d  = 4.5;  // bolt-hole diameter\n$fn = 48;\n\ndifference() {\n    union() {\n        // Horizontal flange lying on the bed.\n        cuboid([leg, width, thick], rounding = round_r,\n               edges = [FRONT + LEFT, FRONT + RIGHT, BACK + LEFT, BACK + RIGHT],\n               anchor = BOTTOM + LEFT);\n        // Vertical flange standing up at the left edge.\n        left(0)\n            cuboid([thick, width, leg], rounding = round_r,\n                   edges = [FRONT + TOP, FRONT + BOTTOM, BACK + TOP, BACK + BOTTOM],\n                   anchor = BOTTOM + LEFT);\n    }\n    // Bolt hole through the horizontal flange.\n    up(-1) translate([leg * 0.6, 0, 0])\n        cyl(d = hole_d, h = thick + 2, anchor = BOTTOM);\n    // Bolt hole through the vertical flange.\n    translate([thick / 2, 0, leg * 0.6])\n        xrot(0) yrot(90)\n            cyl(d = hole_d, h = thick + 2, anchor = CENTER);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-m8-hex-bolt",
      "title": "M8 hex bolt",
      "description": "A real ISO-metric M8 hex-head bolt with a properly cut helical thread, generated by the BOSL2 screws library. A single printable solid (head + threaded shaft). Tune the metric size, shank length, threaded length, and fit tolerance.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// M8 hex bolt\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/screws.scad>\n\nbolt_size  = \"M8\";   // metric thread designation (e.g. M6, M8, M10)\nbolt_len   = 30;     // overall shaft length below the head (mm)\nthread_len = 22;     // length of the threaded section (mm), rest is plain shank\ntolerance  = \"6g\";   // ISO external-thread fit class\n$fn = 48;\n\nscrew(bolt_size, head = \"hex\", length = bolt_len, thread_len = thread_len,\n      tolerance = tolerance, anchor = BOTTOM);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-m8-hex-nut",
      "title": "M8 hex nut",
      "description": "A real ISO-metric M8 hex nut with an internally cut helical thread, generated by the BOSL2 screws library. A single printable solid that mates with an M8 bolt. Tune the metric size, nut thickness, and internal-thread fit tolerance.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// M8 hex nut\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/screws.scad>\n\nnut_size  = \"M8\";   // metric thread designation (e.g. M6, M8, M10)\nthickness = 6.5;    // nut height (mm)\ntolerance = \"6H\";   // ISO internal-thread fit class\n$fn = 64;\n\nnut(nut_size, thickness = thickness, thread = \"coarse\",\n    tolerance = tolerance, anchor = BOTTOM);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-nema17-mount",
      "title": "NEMA17 motor mount plate",
      "description": "An L-shaped NEMA17 stepper mount: a vertical face plate with the standard 22 mm pilot bore and four 31 mm-spaced M3 bolt holes, joined to a horizontal base flange (with its own slots) by a stiffening rib. Dimensions follow the NEMA17 standard. Prints as one solid. Tune the plate size, thickness, bore, bolt-hole diameter, and base depth.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// NEMA17 motor mount plate\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nplate    = 50;    // square face-plate side length\nthick    = 5;     // plate thickness\nbore_d   = 23;    // center pilot bore (NEMA17 boss is 22 mm)\nhole_sp  = 31;    // NEMA17 bolt-hole spacing (centre to centre)\nbolt_d   = 3.4;   // M3 clearance hole\nbase_d   = 35;    // depth of the horizontal base flange\n$fn = 56;\n\ndifference() {\n    union() {\n        // Vertical face plate, standing in the X=0 plane.\n        cuboid([thick, plate, plate], rounding = 3,\n               edges = [FRONT + TOP, FRONT + BOTTOM, BACK + TOP, BACK + BOTTOM],\n               anchor = BOTTOM + LEFT);\n        // Horizontal base flange.\n        cuboid([base_d, plate, thick], rounding = 3,\n               edges = [RIGHT + FRONT, RIGHT + BACK],\n               anchor = BOTTOM + LEFT);\n        // Triangular stiffening ribs on each side.\n        for (y = [-plate / 2, plate / 2 - thick])\n            translate([0, y, 0])\n                linear_extrude(height = thick)\n                    polygon([[0, 0], [base_d * 0.7, 0], [0, plate * 0.55]]);\n    }\n    // Center pilot bore through the face plate.\n    translate([thick / 2, plate / 2, plate / 2]) yrot(90)\n        cyl(d = bore_d, h = thick + 2, anchor = CENTER);\n    // Four NEMA17 bolt holes.\n    for (dy = [-hole_sp / 2, hole_sp / 2], dz = [-hole_sp / 2, hole_sp / 2])\n        translate([thick / 2, plate / 2 + dy, plate / 2 + dz]) yrot(90)\n            cyl(d = bolt_d, h = thick + 2, anchor = CENTER);\n    // Two base mounting holes.\n    for (dy = [-plate / 3, plate / 3])\n        translate([base_d * 0.7, plate / 2 + dy, thick / 2])\n            cyl(d = bolt_d + 1, h = thick + 2, anchor = CENTER);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-pco1810-cap",
      "title": "PCO-1810 bottle cap",
      "description": "A standards-compliant PCO-1810 screw cap that fits the common 28mm PET soda/water bottle neck finish, generated by the BOSL2 bottlecaps library. Tune the cap height, wall thickness, and knurl texture for grip.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// PCO-1810 bottle cap\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/bottlecaps.scad>\n\ncap_h    = 14.5;     // overall cap height (mm) — must clear the tamper ring (>=14.1)\ncap_wall = 1.6;      // side + top wall thickness (mm)\n$fn = 96;\n\npco1810_cap(h = cap_h, wall = cap_wall, texture = \"knurled\");\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-pencil-cup",
      "title": "Pencil & tool cup",
      "description": "A round desktop cup for pens, pencils, and small tools, with a rounded top rim and a solid floor. Built from a BOSL2 tube with a chamfered base disc so it prints cleanly without supports. Tune the outer diameter, height, wall thickness, and floor thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Pencil & tool cup\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ncup_d   = 75;   // outer diameter\ncup_h   = 95;   // overall height\nwall    = 2.4;  // wall thickness\nfloor_t = 3;    // floor thickness\nrim_r   = 1.5;  // rounding radius on the top rim\n$fn = 96;\n\nunion() {\n    // Hollow wall: a tube standing on the floor, top rim rounded.\n    up(floor_t)\n        tube(h = cup_h - floor_t, od = cup_d, wall = wall,\n             orounding2 = rim_r, irounding2 = rim_r, anchor = BOTTOM);\n    // Solid floor disc, slightly chamfered at the base for a clean first layer.\n    cyl(h = floor_t, d = cup_d, chamfer1 = 0.8, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-pill-container",
      "title": "Round pill container body",
      "description": "A pocket-sized round pill bottle body with an external screw thread at the mouth, rounded shoulders, and a closed bottom, built with the BOSL2 threading helix. Pairs with a matching screw lid. Tune the body diameter, height, wall thickness, and thread pitch.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Round pill container body\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/threading.scad>\n\nbody_d   = 32;    // outer body diameter (mm)\nbody_h   = 45;    // total height including the threaded neck (mm)\nwall     = 1.8;   // wall + bottom thickness (mm)\nneck_d   = 24;    // threaded neck outer diameter (mm)\nneck_h   = 10;    // threaded neck height (mm)\npitch    = 3;     // thread pitch (mm)\n$fn = 96;\n\nthread_depth = pitch * 0.5;\ninner_d      = body_d - 2*wall;\nneck_id      = neck_d - 2*wall;\nshoulder_h   = 6;                       // tapered shoulder height (mm)\nbarrel_h     = body_h - neck_h - shoulder_h;\n\nunion() {\n    // closed-bottom barrel\n    difference() {\n        cyl(d = body_d, h = barrel_h, anchor = BOTTOM, rounding1 = 3);\n        up(wall) cyl(d = inner_d, h = barrel_h, anchor = BOTTOM);\n    }\n    // tapered shoulder from barrel down-to neck (hollow)\n    up(barrel_h)\n        difference() {\n            cyl(d1 = body_d, d2 = neck_d, h = shoulder_h, anchor = BOTTOM);\n            cyl(d1 = inner_d, d2 = neck_id, h = shoulder_h + 0.02, anchor = BOTTOM);\n        }\n    // threaded neck tube\n    up(barrel_h + shoulder_h)\n        tube(od = neck_d, id = neck_id, h = neck_h, anchor = BOTTOM);\n    // external thread fused into the neck wall\n    up(barrel_h + shoulder_h)\n        thread_helix(\n            d            = neck_d - thread_depth,\n            pitch        = pitch,\n            thread_depth = thread_depth,\n            flank_angle  = 30,\n            turns        = (neck_h - pitch) / pitch,\n            lead_in      = pitch,\n            anchor       = BOTTOM\n        );\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-pinion-2to1",
      "title": "2:1 reduction pinion",
      "description": "A 12-tooth pinion that meshes with the 24-tooth spur gear in this set to give an exact 2:1 gear ratio. Print this and the spur gear, set them one pitch-distance apart, and the small gear turns twice for every turn of the large one. Tune the module, tooth count, thickness, and bore.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// 2:1 reduction pinion\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/gears.scad>\n\nteeth     = 12;   // teeth on this pinion (24/12 = 2:1 vs the spur gear)\ngear_mod  = 2;    // tooth module in mm (must match the mating gear)\nthickness = 8;    // gear plate thickness in mm\nshaft_d   = 5;    // center shaft bore diameter in mm\n$fn = 64;\n\nspur_gear(mod = gear_mod, teeth = teeth, thickness = thickness,\n          shaft_diam = shaft_d, pressure_angle = 20);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-pipe-clamp",
      "title": "Pipe / rod mounting clamp",
      "description": "A one-piece pipe stand-off: a closed cylindrical collar that a pipe or rod passes through, fused to a flat mounting foot with two bolt holes that screws to a wall or panel. The bore is sized for a slip fit. Prints flat as a single watertight solid, no supports. Tune the pipe diameter, wall thickness, collar length, foot width, and bolt-hole size.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Pipe / rod mounting clamp\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\npipe_d   = 20;   // diameter of the pipe / rod it holds\nwall     = 4;    // collar wall thickness\nlength   = 22;   // collar length along the pipe axis\nfoot_w   = 18;   // width of each mounting foot\nhole_d   = 4.5;  // bolt-hole diameter\n$fn = 64;\n\ncollar_od = pipe_d + 2 * wall;\nfoot_th   = wall;                 // foot thickness\nspan      = collar_od + 2 * foot_w; // overall foot span\n\ndifference() {\n    union() {\n        // Collar tube lying along the X axis, resting on the bed.\n        up(collar_od / 2)\n            xrot(90)\n                tube(id = pipe_d, wall = wall, h = length, anchor = CENTER);\n        // Flat mounting foot under the collar.\n        cuboid([length, span, foot_th], rounding = 3, except = [TOP],\n               anchor = BOTTOM);\n    }\n    // Two bolt holes through the feet, clear of the collar.\n    for (y = [collar_od / 2 + foot_w / 2, -(collar_od / 2 + foot_w / 2)])\n        translate([0, y, foot_th / 2])\n            cyl(d = hole_d, h = foot_th + 2, anchor = CENTER);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-rack",
      "title": "Rack (linear gear)",
      "description": "A straight gear rack: the flat, linear counterpart of a spur gear. A meshing pinion of the same module converts rotation into linear travel. Tune the module, number of teeth (rack length), thickness, width, and the solid backing below the teeth.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Rack (linear gear)\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/gears.scad>\n\ngear_mod   = 2;    // tooth module in mm (match the pinion)\nteeth      = 12;   // number of teeth (sets rack length)\nthickness  = 8;    // overall height in mm\nwidth      = 10;   // depth of the rack along the tooth in mm\nbacking    = 4;    // solid material below the tooth roots in mm\n$fn = 32;\n\nrack(mod = gear_mod, teeth = teeth, thickness = thickness,\n     width = width, backing = backing, pressure_angle = 20);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-ring-gear",
      "title": "Internal ring gear",
      "description": "An internal ring (annular) gear with teeth cut on the inside, the outer ring of a planetary gearset. A meshing spur gear of the same module rolls around the inside. Tune the module, tooth count, thickness, and the solid backing wall thickness behind the teeth.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Internal ring gear\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/gears.scad>\n\nteeth      = 48;   // number of internal teeth\ngear_mod   = 2;    // tooth module in mm (match the meshing spur gear)\nthickness  = 8;    // ring thickness in mm\nbacking    = 4;    // solid wall thickness behind the teeth in mm\n$fn = 64;\n\nring_gear(mod = gear_mod, teeth = teeth, thickness = thickness,\n          backing = backing, pressure_angle = 20);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-roller",
      "title": "Bore roller / conveyor wheel",
      "description": "A barrel-shaped roller (conveyor / guide wheel) with a center bore to spin on a shaft or bushing. The crowned (slightly barrelled) profile self-centers a belt or load and the rounded ends resist edge wear. Printed as one watertight solid. Tune the body diameter, crown, length, and bore.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Bore roller / conveyor wheel\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbody_d   = 20;   // diameter at the ends of the roller\ncrown    = 1.5;  // extra radius bulge at the middle (barrel crown)\nlength   = 24;   // axial length of the roller\nbore_d   = 8;    // center bore diameter\nend_rnd  = 1.0;  // rounding on the end edges\n$fn = 120;\n\nmid_d = body_d + 2 * crown;   // diameter at the crowned middle\n\ndifference() {\n    // barrelled body of revolution\n    rotate_extrude($fn = 120)\n        polygon([\n            [bore_d/2,   0],\n            [body_d/2,   0],\n            [mid_d/2,    length/2],\n            [body_d/2,   length],\n            [bore_d/2,   length],\n        ]);\n    // through bore\n    down(1)\n        cylinder(h = length + 2, d = bore_d, $fn = 96);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-rounded-box",
      "title": "Rounded box with lid",
      "description": "A clean filleted enclosure with rounded edges and a friction-fit lid, using BOSL2 rounding + attachments. Tune the outer size, wall thickness, corner rounding, and lid height.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Rounded box with lid\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbox_w   = 60;   // outer width\nbox_d   = 40;   // outer depth\nbox_h   = 30;   // outer height (base part)\nwall    = 2;    // wall thickness\nround_r = 4;    // corner rounding radius\n$fn = 48;\n\n// Hollow base: outer rounded cuboid minus an inner rounded cavity.\ndifference() {\n    cuboid([box_w, box_d, box_h], rounding = round_r, except = [TOP]);\n    up(wall)\n        cuboid([box_w - 2 * wall, box_d - 2 * wall, box_h], rounding = round_r - wall > 0 ? round_r - wall : 0.5, except = [TOP]);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-rounded-box-base",
      "title": "Rounded storage box base",
      "description": "A hollow box base with softly rounded outer corners and an open top, sized for small parts and hardware. Built from a rounded BOSL2 cuboid with an inner cavity subtracted. Tune the footprint, height, wall thickness, corner rounding, and floor thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Rounded storage box base\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbox_w   = 80;   // outer width (X)\nbox_d   = 55;   // outer depth (Y)\nbox_h   = 35;   // outer height (Z)\nwall    = 2.4;  // side wall thickness\nfloor_t = 2.4;  // floor thickness\nround_r = 5;    // outer corner rounding radius\n$fn = 64;\n\ndifference() {\n    // Outer rounded shell, sharp top edge for a clean rim.\n    cuboid([box_w, box_d, box_h], rounding = round_r, except = [TOP]);\n    // Inner cavity: floor left solid, open + slightly over-cut at the top.\n    up(-box_h / 2 + floor_t)\n        cuboid(\n            [box_w - 2 * wall, box_d - 2 * wall, box_h],\n            rounding = max(round_r - wall, 0.5),\n            except = [TOP],\n            anchor = BOTTOM\n        );\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shape-capsule",
      "title": "Capsule (stadium solid)",
      "description": "A cylinder capped by hemispheres on both ends — the 3D stadium / pill shape, built by revolving a stadium profile into one watertight solid. Tune the radius and the straight mid-section length.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Capsule (stadium solid)\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ncap_r    = 12;   // radius of the tube and the end caps\nmid_len  = 40;   // length of the straight middle section\nsegs     = 64;   // surface resolution\n\nhalf = mid_len / 2;\n\n// Half-profile (X = radius, Y = height): bottom cap arc, straight side, top cap arc.\nprof = concat(\n    [ for (a = [-90 : 180 / segs : -0.0001])\n        [cap_r * cos(a), -half + cap_r * sin(a)] ],          // bottom hemisphere\n    [ for (a = [0 : 180 / segs : 90])\n        [cap_r * cos(a),  half + cap_r * sin(a)] ],          // top hemisphere\n    [[0, half + cap_r], [0, -half - cap_r]]                  // close along the axis\n);\n\nrotate_extrude($fn = segs)\n    polygon(prof);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shape-chamfered-box",
      "title": "Chamfered box",
      "description": "A solid box with flat 45-degree chamfers cut on every edge for a crisp faceted look. Tune the side lengths and the chamfer depth.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Chamfered box\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbox_w    = 50;   // width  (X)\nbox_d    = 50;   // depth  (Y)\nbox_h    = 30;   // height (Z)\nchamf    = 5;    // chamfer depth on every edge\n$fn = 32;\n\ncuboid([box_w, box_d, box_h], chamfer = chamf);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shape-filleted-wedge",
      "title": "Filleted wedge ramp",
      "description": "A right-triangular wedge / ramp solid with its long top edge rounded into a smooth fillet. Tune the footprint, the rise, and the fillet radius.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Filleted wedge ramp\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbase_len = 60;   // ramp run (X)\nwidth    = 30;   // ramp width (Y)\nrise     = 28;   // ramp height (Z)\nfillet_r = 6;    // radius of the rounded top edge\n$fn = 48;\n\n// A wedge minus a rounded prism subtracted from its sharp apex edge,\n// realised by intersecting the wedge with an over-sized rounded block.\nintersection() {\n    wedge([base_len, width, rise]);\n    translate([0, 0, -1])\n        cuboid([base_len + 2, width, rise + 2], rounding = fillet_r,\n               edges = [TOP + FRONT], anchor = CENTER);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shape-frustum",
      "title": "Cone frustum",
      "description": "A truncated cone (frustum) with a wider base and narrower top, edges softly rounded. Tune the two radii, the height, and the rounding.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Cone frustum\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nr_base   = 25;   // bottom radius\nr_top    = 14;   // top radius\nfrust_h  = 35;   // height\nround_r  = 2;    // rim rounding radius\n$fn = 64;\n\ncyl(h = frust_h, r1 = r_base, r2 = r_top, rounding = round_r);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shape-hex-prism",
      "title": "Hexagonal prism",
      "description": "A regular six-sided prism with softly rounded vertical edges. Tune the across-flats radius, the height, and the side count to make other regular prisms.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Hexagonal prism\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nsides    = 6;    // number of sides (6 = hex, 8 = oct, ...)\nradius   = 20;   // circumscribed radius (center to vertex)\nprism_h  = 30;   // height\nround_r  = 1.5;  // vertical edge rounding\n$fn = 48;\n\nregular_prism(n = sides, h = prism_h, r = radius, rounding = round_r);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shape-oct-prism",
      "title": "Octagonal prism",
      "description": "A regular eight-sided prism with a chamfered top and bottom rim for a faceted token look. Tune the radius, height, side count, and chamfer.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Octagonal prism\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nsides    = 8;    // number of sides (8 = oct)\nradius   = 18;   // circumscribed radius (center to vertex)\nprism_h  = 22;   // height\nchamf    = 2;    // top/bottom rim chamfer\n$fn = 48;\n\nregular_prism(n = sides, h = prism_h, r = radius, chamfer = chamf);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shape-onion-dome",
      "title": "Onion dome finial",
      "description": "A bulbous onion-dome solid that swells out then tapers to a point, sitting on a short cylindrical drum — a classic finial / cupola cap. Tune the bulge, height, and base.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Onion dome finial\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbase_r   = 16;   // drum (base) radius\nbulge_r  = 22;   // widest radius of the bulb\ndome_h   = 50;   // total height of the bulb above the drum\ndrum_h   = 8;    // height of the cylindrical base drum\nsegs     = 96;   // surface resolution\n\n// Profile in the X(radius)-Y(height) plane, revolved into a solid.\n// Drum then an onion curve: out to the bulge, in to a point at the top.\nprof = concat(\n    [[0, 0], [base_r, 0], [base_r, drum_h]],\n    [ for (i = [0 : segs])\n        let (\n            t  = i / segs,                          // 0..1 up the bulb\n            // radius: swell to bulge near the bottom, taper to 0 at the top\n            rr = (bulge_r) * sin(180 * pow(t, 0.7)) * (1 - t) + base_r * (1 - t),\n            zz = drum_h + dome_h * t\n        )\n        [max(rr, 0.01), zz]\n    ],\n    [[0, drum_h + dome_h]]\n);\n\nrotate_extrude($fn = segs)\n    polygon(prof);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shape-rounded-cuboid",
      "title": "Rounded cuboid",
      "description": "A solid box with all twelve edges rounded into smooth fillets. Tune the three side lengths and the corner radius.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Rounded cuboid\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbox_w   = 60;   // width  (X)\nbox_d   = 40;   // depth  (Y)\nbox_h   = 25;   // height (Z)\nround_r = 6;    // edge rounding radius\n$fn = 48;\n\ncuboid([box_w, box_d, box_h], rounding = round_r);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shape-spindle",
      "title": "Spindle / lens solid",
      "description": "A symmetric spindle (lens) made by revolving a circular arc — a fat lens that tapers to points at both poles. Tune the equatorial radius and the bulge height.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Spindle / lens solid\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\neq_radius = 22;   // equatorial radius (widest point)\nhalf_h    = 26;   // half-height (pole to equator)\nsegs      = 96;   // surface resolution\n\n// Circular arc through the bottom pole (0,-half_h), the equator (eq_radius,0)\n// and the top pole (0,half_h). Center sits on the X axis at cx.\narc_r = (eq_radius * eq_radius + half_h * half_h) / (2 * eq_radius);\ncx    = eq_radius - arc_r;                       // arc center X (may be negative)\nang   = atan2(half_h, -cx);                      // half-sweep of the arc\n\n// Right-side profile from bottom pole up to top pole (all X >= 0), then\n// close straight down the revolution axis.\nprof = concat(\n    [ for (a = [-ang : 2 * ang / segs : ang])\n        [max(cx + arc_r * cos(a), 0), arc_r * sin(a)] ],\n    [[0, half_h], [0, -half_h]]\n);\n\nrotate_extrude($fn = segs)\n    polygon(prof);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shape-super-egg",
      "title": "Super-egg (superellipsoid)",
      "description": "A Piet Hein style super-egg — a superellipsoid that stands on its own end. Tune the radii and the superellipse exponent for a rounder or boxier profile.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Super-egg (superellipsoid)\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nrad_xy = 22;    // equatorial radius (X and Y)\nrad_z  = 32;    // polar half-height (Z)\nexpo   = 2.5;   // superellipse exponent (>2 = squarer, <2 = pointier)\nsegs   = 96;    // surface resolution\n\n// Build a superellipse cross-section and revolve it into a solid of revolution.\nprof = [\n    for (a = [0 : 360 / segs : 360 - 0.001])\n        let (\n            ca = cos(a), sa = sin(a),\n            x = rad_xy * sign(ca) * pow(abs(ca), 2 / expo),\n            y = rad_z  * sign(sa) * pow(abs(sa), 2 / expo)\n        )\n        if (x >= 0) [x, y]\n];\n\nrotate_extrude($fn = segs)\n    polygon(prof);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shape-torus",
      "title": "Torus ring",
      "description": "A solid donut/ring shape defined by the major (centerline) radius and the minor (tube) radius. Tune both radii and the facet counts.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Torus ring\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nr_major = 30;   // distance from center to tube center\nr_minor = 9;    // tube radius\nseg_maj = 64;   // segments around the ring\nseg_min = 32;   // segments around the tube\n\ntorus(r_maj = r_major, r_min = r_minor, $fn = seg_maj);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shelf-bracket",
      "title": "Shelf bracket with gusset",
      "description": "A load-bearing shelf bracket: a vertical wall plate, a horizontal shelf flange, and a triangular diagonal gusset web tying the two together for stiffness. Wall and shelf carry mounting holes. The whole part fuses into one watertight solid. Tune the wall height, shelf reach, width, thickness, and hole diameter.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Shelf bracket with gusset\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nwall_h  = 50;   // height of the wall-mount plate\nreach   = 45;   // how far the shelf flange sticks out\nwidth   = 24;   // bracket width\nthick   = 5;    // plate / gusset thickness\nhole_d  = 5;    // mounting-hole diameter\n$fn = 48;\n\ndifference() {\n    union() {\n        // Vertical wall plate (mounts to the wall, sits in the X=0 plane).\n        cuboid([thick, width, wall_h], anchor = BOTTOM + LEFT);\n        // Horizontal shelf flange.\n        cuboid([reach, width, thick], anchor = BOTTOM + LEFT);\n        // Diagonal triangular gusset in the X-Z plane, extruded across the width.\n        translate([0, -width / 2, 0])\n            linear_extrude(height = width)\n                polygon([[0, 0], [reach, 0], [0, wall_h]]);\n    }\n    // Two holes in the wall plate.\n    for (z = [wall_h * 0.55, wall_h * 0.85])\n        translate([thick / 2, 0, z]) yrot(90)\n            cyl(d = hole_d, h = thick + 2, anchor = CENTER);\n    // Two holes in the shelf flange.\n    for (x = [reach * 0.55, reach * 0.85])\n        translate([x, 0, thick / 2])\n            cyl(d = hole_d, h = thick + 2, anchor = CENTER);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-shoulder-bushing",
      "title": "Stepped shoulder bushing",
      "description": "A two-diameter shoulder bushing: a smaller pilot diameter that drops into a bore and a larger shoulder that registers depth and takes face load. Printed as one watertight solid with a clean through bore. Tune the pilot and shoulder diameters and lengths, plus the bore for your shaft.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Stepped shoulder bushing\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\npilot_d    = 10;   // pilot (smaller) outer diameter — fits the bore\nshoulder_d = 16;   // shoulder (larger) outer diameter — registers depth\nbore_d     = 6;    // shaft through bore\npilot_len  = 10;   // length of the pilot section\nshoulder_t = 4;    // thickness of the shoulder section\n$fn = 96;\n\ndifference() {\n    union() {\n        // shoulder at the bottom\n        cyl(h = shoulder_t, d = shoulder_d, chamfer1 = 0.8, anchor = BOTTOM);\n        // pilot rising from the shoulder\n        up(shoulder_t)\n            cyl(h = pilot_len, d = pilot_d, chamfer2 = 0.8, anchor = BOTTOM);\n    }\n    // through bore\n    down(0.5)\n        cyl(h = shoulder_t + pilot_len + 1, d = bore_d, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-sleeve-bushing",
      "title": "Plain sleeve bushing",
      "description": "A simple cylindrical plain (journal) bushing — a printable sleeve that press-fits into a housing bore and gives a shaft a low-friction running surface. The inner and outer edges are chamfered for easy insertion. Tune the outer diameter to your housing, the bore to your shaft (leave running clearance), and the length to suit.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Plain sleeve bushing\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nouter_d  = 12;   // outer diameter (press-fit into housing)\nbore_d   = 8;    // shaft bore diameter (add running clearance)\nlength   = 14;   // axial length of the sleeve\nlead_cf  = 1.0;  // lead-in chamfer on both ends\n$fn = 96;\n\ntube(h = length, od = outer_d, id = bore_d, chamfer = lead_cf, anchor = BOTTOM);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-snap-pin",
      "title": "Snap pin (sprung)",
      "description": "A two-pronged snap pin that clicks into a matching socket and locks with a sprung nub. Generated by the BOSL2 joiners library so the spring slot, nub depth, and clearance are correct. Tune the radius, length, snap depth, prong thickness, and fit clearance.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Snap pin (sprung)\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/joiners.scad>\n\npin_r      = 3.5;   // outer radius of the pin shaft\npin_l      = 16;    // overall length of the pin\nnub_depth  = 9;     // distance from tip to the locking nub\nsnap       = 0.6;   // depth the locking nub springs out\nthickness  = 1.2;   // wall thickness of each prong\nclearance  = 0.2;   // fit clearance against the socket\n$fn = 48;\n\nsnap_pin(r = pin_r, l = pin_l, nub_depth = nub_depth, snap = snap,\n         thickness = thickness, clearance = clearance,\n         anchor = CENTER, orient = UP);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-snap-pin-socket",
      "title": "Snap-pin socket block",
      "description": "A solid mounting block with a sprung snap-pin socket bored into it — the receiver for the BOSL2 snap pin. The socket geometry comes from the BOSL2 joiners library so the locking-nub catch matches the pin exactly. Tune the block size, socket radius, length, and snap depth.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Snap-pin socket block\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/joiners.scad>\n\nblock       = [16, 16, 18];  // outer block size [x, y, z]\nsocket_r    = 3.5;           // socket radius (match the pin radius)\nsocket_l    = 16;            // socket depth / length\nnub_depth   = 9;             // distance to the locking-nub catch\nsnap        = 0.6;           // depth of the nub catch\n$fn = 48;\n\ndiff()\n  cuboid(block)\n    attach(TOP, TOP, inside = true)\n      tag(\"remove\")\n        snap_pin_socket(r = socket_r, l = socket_l,\n                        nub_depth = nub_depth, snap = snap);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-spur-gear",
      "title": "Spur gear (involute)",
      "description": "A real meshing spur gear with a correct involute tooth profile and a center bore, generated by the BOSL2 gears library. Tune the module (tooth size), tooth count, thickness, and shaft diameter.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Spur gear (involute)\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/gears.scad>\n\nteeth     = 24;   // number of teeth\ngear_mod  = 2;    // module (mm of pitch diameter per tooth)\nthickness = 8;    // gear thickness\nshaft_d   = 6;    // center bore diameter\n$fn = 48;\n\nspur_gear(mod = gear_mod, teeth = teeth, thickness = thickness, shaft_diam = shaft_d);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-stacking-bin",
      "title": "Stacking parts bin",
      "description": "An open-front-friendly parts bin whose rounded base nests a matching lip on the bin below, so several stack stably on a shelf. The shell is carved hollow from a rounded block with a recessed underside rim. Tune the footprint, height, wall thickness, floor thickness, and the nesting lip depth.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Stacking parts bin\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbin_w    = 70;   // outer width (X)\nbin_d    = 100;  // outer depth (Y)\nbin_h    = 45;   // outer height (Z)\nwall     = 2.4;  // wall thickness\nfloor_t  = 3.6;  // floor thickness (> lip_h so the floor stays solid)\nlip_h    = 2;    // depth of the nesting recess under the base\nround_r  = 4;    // outer corner rounding radius\n$fn = 48;\n\ndifference() {\n    cuboid([bin_w, bin_d, bin_h], rounding = round_r, except = [TOP]);\n    // Interior pocket, open top.\n    up(-bin_h / 2 + floor_t)\n        cuboid([bin_w - 2 * wall, bin_d - 2 * wall, bin_h],\n               rounding = max(round_r - wall, 0.5), except = [TOP], anchor = BOTTOM);\n    // Nesting recess in the underside: a shallow pocket matching the rim of the\n    // bin below. Kept shallower than the floor so the base stays watertight.\n    down(bin_h / 2 + 0.01)\n        cuboid([bin_w - 2 * wall + 0.6, bin_d - 2 * wall + 0.6, lip_h],\n               rounding = max(round_r - wall, 0.5), except = [TOP], anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-struct-bumper-socket",
      "title": "Rubber-foot bumper socket",
      "description": "A screw-down socket that grips a push-in rubber bumper foot: a rounded cylindrical body with a counterbored cavity for the rubber foot's stem and a central screw hole through the bottom. Tune body diameter/height, socket cavity diameter/depth, and screw hole diameter.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Rubber-foot bumper socket\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbody_d    = 24;   // outer body diameter (mm)\nbody_h    = 12;   // overall body height (mm)\nsocket_d  = 14;   // rubber-foot cavity diameter (mm)\nsocket_h  = 7;    // cavity depth from the top (mm)\nhole_d    = 4.2;  // central mounting screw hole diameter (mm)\n$fn = 64;\n\ndifference() {\n    // rounded body\n    cyl(h = body_h, d = body_d, rounding1 = 2, rounding2 = 4, anchor = BOTTOM);\n    // socket cavity for the rubber foot, opening at the top\n    up(body_h - socket_h)\n        cyl(h = socket_h + 0.1, d = socket_d, rounding1 = 1, anchor = BOTTOM);\n    // central mounting screw hole through the floor\n    cyl(h = body_h * 3, d = hole_d);\n    // counterbore so a flat screw head sits flush on the underside\n    down(0.01)\n        cyl(h = 3, d = hole_d * 2, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-struct-corner-brace",
      "title": "Corner brace with triangular gusset",
      "description": "A 90-degree corner brace: two flat mounting tabs joined by a tall triangular gusset web for stiffness, with one screw hole per tab. Tune tab length, plate thickness, gusset height/web thickness, and hole diameter.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Corner brace with triangular gusset\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ntab     = 35;   // length of each mounting tab (mm)\nwide    = 22;   // brace width (mm)\nthick   = 4;    // tab/plate thickness (mm)\ngusset  = 28;   // gusset height up each leg (mm)\nweb     = 3;    // gusset web thickness (mm)\nhole_d  = 5;    // screw hole diameter (mm)\n$fn = 48;\n\ndifference() {\n    union() {\n        // horizontal tab on the build plate\n        cuboid([tab, wide, thick], rounding = 2, edges = \"Z\",\n               anchor = BOTTOM + LEFT + FRONT);\n        // vertical tab standing at the corner\n        cuboid([thick, wide, tab], rounding = 2, edges = \"Z\",\n               anchor = BOTTOM + LEFT + FRONT);\n        // central triangular gusset web bridging the two tabs.\n        // polygon lies in X-Z (corner cross-section); extruded across Y.\n        // origin sunk to (0,0) so the triangle legs overlap both plates.\n        translate([0, wide / 2 + web / 2, 0])\n            rotate([90, 0, 0])\n            linear_extrude(height = web)\n                polygon([[0, 0], [gusset, 0], [0, gusset]]);\n    }\n    // horizontal tab hole\n    translate([tab * 0.75, wide / 2, thick / 2])\n        cyl(h = thick * 3, d = hole_d);\n    // vertical tab hole\n    translate([thick / 2, wide / 2, tab * 0.75])\n        yrot(90)\n        cyl(h = thick * 3, d = hole_d);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-struct-l-bracket",
      "title": "Rounded L-bracket",
      "description": "A right-angle mounting L-bracket with rounded outer corners, an inner fillet weld at the corner for strength, and four screw holes (two per leg). Tune leg length, width, thickness, hole diameter, and fillet radius.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Rounded L-bracket\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nleg     = 40;   // length of each leg measured from the corner (mm)\nwide    = 30;   // bracket width (mm)\nthick   = 4;    // plate thickness (mm)\nhole_d  = 4.5;  // screw hole diameter (mm)\nfillet  = 6;    // inner corner gusset size (mm)\n$fn = 48;\n\n// A solid right-triangle (legs along +X and +Y) with the hypotenuse\n// rounded concavely toward the corner — the cross-section of a weld bead.\nmodule fillet_profile(s) {\n    difference() {\n        square([s, s]);\n        translate([s, s]) circle(r = s);\n    }\n}\n\ndifference() {\n    union() {\n        // horizontal leg: thickness in Z, lies on the build plate\n        cuboid([leg, wide, thick], rounding = 2, edges = \"Z\",\n               anchor = BOTTOM + LEFT + FRONT);\n        // vertical leg: thickness in X, stands up at the corner\n        cuboid([thick, wide, leg], rounding = 2, edges = \"Z\",\n               anchor = BOTTOM + LEFT + FRONT);\n        // inner fillet weld bead bridging the two legs along the width:\n        // a rounded right-triangle extruded across the full width.\n        translate([thick, wide, thick])\n            xrot(90)\n            linear_extrude(height = wide)\n                fillet_profile(fillet);\n    }\n    // horizontal-leg screw holes\n    for (x = [leg * 0.55, leg * 0.85])\n        translate([x, wide / 2, thick / 2])\n            cyl(h = thick * 3, d = hole_d);\n    // vertical-leg screw holes\n    for (z = [leg * 0.55, leg * 0.85])\n        translate([thick / 2, wide / 2, z])\n            xrot(0) yrot(90)\n                cyl(h = thick * 3, d = hole_d);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-struct-leveling-foot",
      "title": "Threaded leveling foot",
      "description": "An adjustable leveling foot: a wide rounded base disk topped by a real metric-style threaded stud that screws into a furniture insert. Tune base diameter/height, stud thread diameter, pitch, and stud length. Pairs with a matching threaded insert/nut.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Threaded leveling foot\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/threading.scad>\n\nbase_d   = 40;   // base disk diameter (mm)\nbase_h   = 8;    // base disk height (mm)\nstud_d   = 8;    // stud thread outer diameter (mm)\npitch    = 1.25; // thread pitch (mm)\nstud_l   = 20;   // threaded stud length (mm)\n$fn = 64;\n\nunion() {\n    // rounded foot base that contacts the floor\n    cyl(h = base_h, d = base_d, rounding1 = 1.5, rounding2 = 3, anchor = BOTTOM);\n    // short solid neck to blend the threads onto the base\n    up(base_h - 0.01)\n        cyl(h = 4, d = stud_d + 2, chamfer2 = 1, anchor = BOTTOM);\n    // real threaded stud, sunk into the neck so the volumes overlap\n    up(base_h + 1)\n        threaded_rod(d = stud_d, l = stud_l, pitch = pitch,\n                     bevel2 = true, blunt_start = true, anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-struct-panel-snap-clip",
      "title": "Panel snap clip",
      "description": "A cantilever snap-fit clip for retaining a panel or wire: a screw-down base with an upright flexible arm ending in a hooked barb that snaps over a panel edge. Two base bolt holes. Tune base size, arm height/thickness, barb depth, and bolt hole diameter.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Panel snap clip\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbase_l   = 30;   // base length (mm)\nbase_w   = 16;   // base width (mm)\nbase_t   = 4;    // base thickness (mm)\narm_h    = 18;   // cantilever arm height (mm)\narm_t    = 3;    // arm thickness (mm)\nbarb     = 4;    // hook barb overhang depth (mm)\nhole_d   = 4;    // base bolt hole diameter (mm)\n$fn = 48;\n\ndifference() {\n    union() {\n        // mounting base\n        translate([0, 0, base_t / 2])\n            cuboid([base_l, base_w, base_t], rounding = 2, edges = \"Z\");\n        // flexible cantilever arm rising from one end of the base\n        translate([-base_l / 2 + arm_t / 2, 0, base_t])\n            cuboid([arm_t, base_w * 0.7, arm_h],\n                   rounding = 1, edges = [FRONT + TOP, BACK + TOP],\n                   anchor = BOTTOM);\n        // hooked barb at the top of the arm (extends inward over the panel).\n        // built as an extruded right-triangle so it prints with a sloped\n        // self-supporting underside.\n        translate([-base_l / 2, base_w * 0.7 / 2, base_t + arm_h - 2])\n            rotate([90, 0, 0])\n            linear_extrude(height = base_w * 0.7)\n                polygon([\n                    [0, 2],\n                    [arm_t + barb, 2],\n                    [0, -barb * 1.4]\n                ]);\n    }\n    // base bolt holes\n    for (x = [base_l * 0.18, base_l * 0.42])\n        translate([x, 0, base_t / 2])\n            cyl(h = base_t * 4, d = hole_d);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-struct-right-angle-gusset",
      "title": "Right-angle gusset plate",
      "description": "A flat triangular gusset plate for reinforcing 90-degree frame joints: a right-triangle web with a thickened ribbed hypotenuse and three bolt holes (one near each corner). Lies flat for printing. Tune leg length, thickness, rib width, and bolt hole diameter.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Right-angle gusset plate\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nleg     = 50;   // length of each right-angle leg (mm)\nthick   = 4;    // plate thickness (mm)\nrib     = 6;    // raised rib width along the hypotenuse (mm)\nrib_h   = 3;    // extra rib height above the plate (mm)\nhole_d  = 5;    // bolt hole diameter (mm)\n$fn = 48;\n\ninset = leg * 0.18;   // bolt-hole inset from corners\n\ndifference() {\n    union() {\n        // flat triangular web (corner at origin, legs along +X and +Y)\n        linear_extrude(height = thick)\n            offset(r = 3) offset(r = -3)   // round the sharp tip a touch\n                polygon([[0, 0], [leg, 0], [0, leg]]);\n        // raised stiffening rib along the hypotenuse\n        translate([0, 0, thick])\n            linear_extrude(height = rib_h)\n                polygon([\n                    [leg - rib, 0], [leg, 0],\n                    [0, leg], [0, leg - rib]\n                ]);\n    }\n    // three bolt holes near the corners\n    for (p = [[inset, inset], [leg - inset * 2, inset], [inset, leg - inset * 2]])\n        translate([p[0], p[1], 0])\n            cyl(h = (thick + rib_h) * 4, d = hole_d);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-struct-shelf-bracket",
      "title": "Shelf bracket with diagonal strut",
      "description": "A wall-mount shelf bracket: a vertical wall plate, a horizontal shelf plate, and a diagonal strut between them for load support. Wall plate has two keyhole-free round screw holes; shelf plate has one. Tune arm lengths, thickness, width, strut width, and hole diameter.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Shelf bracket with diagonal strut\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\narm     = 60;   // horizontal shelf arm length (mm)\nrise    = 60;   // vertical wall plate height (mm)\nwide    = 25;   // bracket width (mm)\nthick   = 5;    // plate thickness (mm)\nstrut_w = 6;    // diagonal strut thickness (mm)\nhole_d  = 5;    // screw hole diameter (mm)\n$fn = 48;\n\ndifference() {\n    union() {\n        // vertical wall plate (thickness in X)\n        cuboid([thick, wide, rise], rounding = 2, edges = \"Z\",\n               anchor = BOTTOM + LEFT + FRONT);\n        // horizontal shelf plate (thickness in Z)\n        cuboid([arm, wide, thick], rounding = 2, edges = \"Z\",\n               anchor = BOTTOM + LEFT + FRONT);\n        // diagonal strut: a thick band from wall up-point to shelf out-point.\n        // built as an extruded quadrilateral in the X-Z corner plane.\n        translate([0, wide / 2 + strut_w / 2, 0])\n            rotate([90, 0, 0])\n            linear_extrude(height = strut_w)\n                polygon([\n                    [0, 0],\n                    [arm * 0.85, thick],\n                    [thick, rise * 0.85]\n                ]);\n    }\n    // wall plate screw holes\n    for (z = [rise * 0.30, rise * 0.65])\n        translate([thick / 2, wide / 2, z])\n            yrot(90)\n            cyl(h = thick * 3, d = hole_d);\n    // shelf plate screw hole\n    translate([arm * 0.78, wide / 2, thick / 2])\n        cyl(h = thick * 3, d = hole_d);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-struct-tube-clamp",
      "title": "Tube / rod clamp with mounting feet",
      "description": "A saddle clamp that holds a tube or rod against a surface: a rounded cradle block with a semicircular channel and two flat mounting feet, each with a bolt hole. Tune tube diameter, wall thickness, foot length, base thickness, width, and bolt hole diameter.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Tube / rod clamp with mounting feet\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ntube_d   = 20;   // tube/rod outer diameter to clamp (mm)\nwall     = 4;    // cradle wall above the tube (mm)\nfoot     = 14;   // length of each mounting foot (mm)\nbase_t   = 4;    // base plate thickness under the feet (mm)\nwide     = 18;   // clamp width along the tube axis (mm)\nbolt_d   = 4.5;  // mounting bolt hole diameter (mm)\n$fn = 64;\n\ntube_r    = tube_d / 2;\naxis_z    = base_t + tube_r;           // tube channel centre height\nblock_h   = axis_z + wall;             // top of the cradle block\nblock_w   = tube_d + 2 * wall;         // cradle block width\nspan      = block_w + 2 * foot;        // total footprint width\nfoot_ctr  = block_w / 2 + foot / 2;    // bolt-hole x offset\n\ndifference() {\n    union() {\n        // base plate spanning the full footprint with the two feet\n        translate([0, 0, base_t / 2])\n            cuboid([span, wide, base_t], rounding = 2, edges = \"Z\");\n        // solid cradle block sitting on the base\n        translate([0, 0, block_h / 2])\n            cuboid([block_w, wide, block_h], rounding = 3,\n                   edges = \"Z\");\n    }\n    // semicircular tube channel through the block (axis along Y)\n    up(axis_z)\n        yrot(90)\n        cyl(h = wide + 1, d = tube_d, anchor = CENTER);\n    // bolt holes through each foot\n    for (x = [-foot_ctr, foot_ctr])\n        translate([x, 0, base_t / 2])\n            cyl(h = base_t * 4, d = bolt_d);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-threaded-jar",
      "title": "Threaded jar body",
      "description": "A round storage jar with an external screw thread at the mouth and a flat closed bottom, built with the BOSL2 threading helix. Pairs with the matching threaded lid recipe. Tune the body diameter, height, wall thickness, and thread pitch.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Threaded jar body\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/threading.scad>\n\nbody_d   = 50;    // outer body diameter (mm)\nbody_h   = 60;    // total jar height including the threaded neck (mm)\nwall     = 2.4;   // wall + bottom thickness (mm)\npitch    = 4;     // thread pitch (mm)\nneck_h   = 12;    // height of the threaded neck section (mm)\n$fn = 96;\n\nthread_depth = pitch * 0.5;   // derived thread depth (mm)\ninner_d      = body_d - 2*wall;\n\nunion() {\n    // hollow body shell with a closed welded floor\n    tube(od = body_d, id = inner_d, h = body_h, anchor = BOTTOM);\n    cyl(d = inner_d + 0.02, h = wall, anchor = BOTTOM);   // welded floor\n    // external thread fused into the neck wall (root overlaps the wall)\n    up(body_h - neck_h)\n        thread_helix(\n            d            = body_d - thread_depth,\n            pitch        = pitch,\n            thread_depth = thread_depth,\n            flank_angle  = 30,\n            turns        = (neck_h - pitch) / pitch,\n            lead_in      = pitch,\n            anchor       = BOTTOM\n        );\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-threaded-lid",
      "title": "Threaded jar lid",
      "description": "A round screw-on lid with an internal thread and a knurled grip skirt, built with the BOSL2 threading helix. Mates with the matching threaded jar body recipe — keep the jar diameter and pitch in sync. Tune the lid diameter, height, wall thickness, and pitch.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Threaded jar lid\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/threading.scad>\n\njar_d     = 50;    // jar neck outer diameter to fit over (mm)\nlid_h     = 14;    // total lid height (mm)\ntop_t     = 2.4;   // top wall thickness (mm)\nskirt_t   = 3;     // skirt wall thickness (mm)\npitch     = 4;     // thread pitch (mm) — match the jar\ntol       = 0.4;   // clearance for a free-running fit (mm)\n$fn = 96;\n\nthread_depth = pitch * 0.5;\nouter_d      = jar_d + 2*skirt_t;\nbore_d       = jar_d + tol;            // inner skirt diameter\n\ndifference() {\n    union() {\n        // outer lid shell (closed top, open bottom)\n        cyl(d = outer_d, h = lid_h, anchor = BOTTOM, rounding2 = 2);\n        // internal thread fused to the bore wall\n        internal_thread();\n    }\n    // hollow out the skirt bore from the underside, leaving the top closed\n    up(top_t)\n        cyl(d = bore_d, h = lid_h, anchor = BOTTOM);\n}\n\nmodule internal_thread() {\n    up(top_t)\n        thread_helix(\n            d            = bore_d + thread_depth,\n            pitch        = pitch,\n            thread_depth = thread_depth,\n            flank_angle  = 30,\n            turns        = (lid_h - top_t - pitch) / pitch,\n            lead_in      = pitch,\n            internal     = true,\n            anchor       = BOTTOM\n        );\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-threaded-rod",
      "title": "Threaded rod (metric)",
      "description": "A real metric threaded rod (all-thread / studding) with a true helical ISO thread profile and blunt printable ends, generated by the BOSL2 threading library. A single printable solid. Tune the major diameter, pitch, and overall length.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Threaded rod (metric)\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/threading.scad>\n\nrod_d  = 8;     // major (outer) diameter (mm)\npitch  = 1.25;  // thread pitch (mm) — 1.25 is standard M8 coarse\nlength = 40;    // overall rod length (mm)\n$fn = 64;\n\nthreaded_rod(d = rod_d, l = length, pitch = pitch,\n             blunt_start = true, bevel = true, anchor = BOTTOM);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-thrust-washer",
      "title": "Thrust washer",
      "description": "A flat annular thrust washer that sits between a rotating part and a fixed face to carry axial load and reduce wear. Both faces are flat for full bearing contact, with chamfered edges so it does not snag. Tune the outer and inner diameters to your shaft and seat, and the thickness for the load.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Thrust washer\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nouter_d   = 20;   // outer diameter\nbore_d    = 10;   // inner bore diameter (shaft)\nthickness = 2;    // washer thickness\nedge_cf   = 0.5;  // edge chamfer\n$fn = 120;\n\ntube(h = thickness, od = outer_d, id = bore_d, chamfer = edge_cf, anchor = BOTTOM);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-thumb-screw",
      "title": "Thumb screw",
      "description": "A real thumb screw: a wide knurled finger grip head fused to a properly threaded metric shaft for tool-free tightening, built from the BOSL2 cylinder texture + threading libraries. A single printable solid. Tune the head diameter, shaft size, pitch and length.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Thumb screw\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/threading.scad>\n\nhead_d   = 22;    // knurled grip head diameter (mm)\nhead_h   = 8;     // head height (mm)\nthread_d = 6;     // shaft thread major diameter (mm)\npitch    = 1.0;   // thread pitch (mm) — 1.0 is M6 coarse\nshaft_l  = 20;    // threaded shaft length below the head (mm)\n$fn = 72;\n\n// Knurled grip head sitting on the build plate.\ncyl(d = head_d, h = head_h, rounding1 = 2, chamfer2 = 1,\n    texture = \"diamonds\", tex_size = [3, 3], tex_depth = 0.9,\n    anchor = BOTTOM);\n\n// Threaded shaft growing upward out of the head's top face.\nup(head_h - 0.5)\n    threaded_rod(d = thread_d, l = shaft_l + 0.5, pitch = pitch,\n                 blunt_start = true, bevel2 = true, bevel1 = false,\n                 anchor = BOTTOM);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-tool-caddy",
      "title": "Tool caddy with handle",
      "description": "A two-pocket carry caddy with a central spine and a rounded grab handle, for ferrying tools, brushes, or craft supplies. The pockets, handle hole, and rounded body are all carved from one rounded block so it prints as a single watertight piece. Tune the footprint, height, wall thickness, and handle dimensions.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Tool caddy with handle\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ncad_w     = 130;  // outer width (X)\ncad_d     = 70;   // outer depth (Y)\nbody_h    = 55;   // height of the body up to the handle bridge\nwall      = 2.6;  // wall + spine thickness\nfloor_t   = 2.6;  // floor thickness\nhandle_h  = 28;   // height of the handle arch above the body\n$fn = 40;\n\n// Two pockets either side of a central spine.\npocket_w = (cad_w - 2 * wall - wall) / 2;\npocket_d = cad_d - 2 * wall;\npocket_h = body_h - floor_t;\ntotal_h  = body_h + handle_h;\n\ndifference() {\n    // Body block plus a handle slab on top, rounded vertical corners.\n    union() {\n        cuboid([cad_w, cad_d, body_h], rounding = 5, edges = \"Z\", anchor = BOTTOM);\n        // Central handle slab (spans the width), overlapping the body so they fuse.\n        // Round only the TOP edges so the slab/body interface stays a clean plane.\n        translate([0, 0, body_h - 2])\n            cuboid([cad_w, wall * 3, handle_h + 2], rounding = 3,\n                   edges = TOP, anchor = BOTTOM);\n    }\n    // Left and right pockets.\n    for (s = [-1, 1])\n        translate([s * (wall / 2 + pocket_w / 2), 0, floor_t])\n            cuboid([pocket_w, pocket_d, pocket_h + 1],\n                   rounding = 3, edges = \"Z\", anchor = BOTTOM);\n    // Hand hole through the slab (plain slot, no end rounding to stay manifold).\n    up(body_h + handle_h * 0.45)\n        ycyl(h = cad_d + 1, d = handle_h * 0.7);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-tslot-joiner",
      "title": "T-slot joiner clip",
      "description": "A T-slot snap joiner clip from the BOSL2 joiners library — the printable connector that plugs two panels together edge-to-edge with a self-retaining barbed tongue. Tune the length, width, base thickness, and flank angle.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// T-slot joiner clip\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/joiners.scad>\n\njoiner_l   = 40;   // length of the joiner along the join axis\njoiner_w   = 10;   // width of the tongue\nbase_t     = 10;   // base / wall thickness it mounts to\nflank_ang  = 30;   // barb flank angle in degrees\n$fn = 32;\n\njoiner(l = joiner_l, w = joiner_w, base = base_t, ang = flank_ang);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-wall-broom-clip",
      "title": "Broom / tool clip",
      "description": "A spring-grip wall clip for broom, mop, or rake handles: a screw-mount backplate with a C-shaped jaw whose mouth is narrower than the handle so it snaps on and holds. Tune the handle diameter, mouth opening, jaw thickness, and backplate.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Broom / tool clip\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nhandle_d  = 24;   // diameter of the handle to grip\ngap       = 14;   // mouth opening (under handle_d so it grips)\njaw_t     = 4;    // jaw wall thickness\nplate_w   = 40;   // backplate width\nplate_t   = 5;    // backplate thickness\nplate_h   = 30;   // backplate height (along the handle axis)\nscrew_d   = 4.5;  // screw clearance bore\n$fn = 72;\n\nbore_r  = handle_d / 2;\nouter_r = bore_r + jaw_t;\noverlap = jaw_t * 0.8;\n// Jaw centre lifted off the plate so the outer ring fuses with the backplate.\njaw_y   = plate_t + outer_r - overlap;\n\nunion() {\n    // Backplate flush to wall (back at y=0, extends to y=plate_t).\n    difference() {\n        cuboid([plate_w, plate_t, plate_h], rounding = 3, edges = \"Y\", anchor = BOTTOM + FRONT);\n        translate([0, 0, plate_h * 0.5]) {\n            ycyl(h = plate_t * 3, d = screw_d, anchor = CENTER);\n            translate([0, plate_t, 0]) ycyl(h = screw_d, d1 = screw_d * 2.2, d2 = screw_d, anchor = FRONT);\n        }\n    }\n\n    // C-shaped jaw: axis runs vertically (Z), opening faces away from the wall (+Y).\n    translate([0, jaw_y, plate_h * 0.5])\n        difference() {\n            zcyl(h = plate_h, r = outer_r, rounding = 1);\n            zcyl(h = plate_h + 0.4, r = bore_r);\n            // Mouth slot opening toward +Y (away from the wall).\n            fwd(0) back(outer_r / 2)\n                cube([gap, outer_r + 0.4, plate_h + 0.4], center = true);\n        }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-wall-cable-clip-screw",
      "title": "Screw-mount cable clip",
      "description": "A screw-mount cable clip with a flat slotted backing tab and a rounded saddle that holds one or more cables against a wall or desk edge. The screw slot lets you snug it down over the cables. Tune the cable diameter, saddle width, tab length, and screw bore.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Screw-mount cable clip\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\ncable_d   = 7;    // diameter of cable(s) to hold\nsaddle_w  = 14;   // width of the holding saddle (along cable axis)\nwall      = 2.5;  // saddle wall thickness\ntab_len   = 14;   // length of the screw tab beside the saddle\ntab_t     = 3;    // backing / tab thickness\nscrew_d   = 4;    // screw clearance bore\n$fn = 64;\n\nbore_r  = cable_d / 2;\nouter_r = bore_r + wall;\noverlap = wall * 0.7;\nsaddle_z = tab_t + outer_r - overlap;\n\nunion() {\n    // Backing strip the whole assembly sits on (back on z=0, against wall when\n    // laid flat; printable as a single flat-bottomed solid).\n    base_w = saddle_w;\n    base_l = outer_r * 2 + tab_len;\n    cuboid([base_w, base_l, tab_t], rounding = 1.5, edges = \"Z\", anchor = BOTTOM);\n\n    // Screw tab: extends out one side with a countersunk hole.\n    difference() {\n        translate([0, base_l/2 - tab_len/2, 0])\n            cuboid([base_w * 0.8, tab_len, tab_t], rounding = 3, edges = \"Z\", anchor = BOTTOM);\n        translate([0, base_l/2 - tab_len/2 + 2, 0]) {\n            cyl(h = tab_t * 3, d = screw_d, anchor = CENTER);\n            up(tab_t) cyl(h = screw_d, d1 = screw_d * 2.2, d2 = screw_d, anchor = TOP);\n        }\n    }\n\n    // Cable saddle: a tube section (axis along the cable / Y) with the top\n    // opened so cables press in, sitting over the base near the other side.\n    translate([0, -base_l/2 + outer_r, saddle_z])\n        difference() {\n            xcyl(h = saddle_w, r = outer_r, rounding = 1);\n            xcyl(h = saddle_w + 0.4, r = bore_r);\n            up(outer_r/2) cube([saddle_w + 0.4, gap_open(), outer_r + 0.4], center = true);\n        }\n}\n\n// Mouth opening a touch narrower than the cable so cables snap and stay.\nfunction gap_open() = cable_d * 0.55;\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-wall-double-coat-hook",
      "title": "Double coat hook",
      "description": "A double coat hook with two outward-curving prongs on a single screw-mount backplate, ideal for hanging two coats or a coat and a scarf. Tune the backplate size, prong reach, prong thickness, and screw bore.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Double coat hook\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nplate_w   = 34;   // backplate width\nplate_h   = 60;   // backplate height\nplate_t   = 6;    // backplate thickness\nprong_len = 22;   // horizontal reach of each prong\nprong_r   = 5;    // prong radius\nscrew_d   = 4.5;  // screw clearance bore\n$fn = 56;\n\nmodule prong(z) {\n    // A prong sweeps straight out from the wall then tips up into a small hook.\n    pts = concat(\n        [ for (x = [0:2:prong_len]) [x, 0, 0] ],\n        [ for (a = [0:8:120]) [prong_len + sin(a) * prong_r * 1.4,\n                               0,\n                               (1 - cos(a)) * prong_r * 1.4] ]\n    );\n    translate([0, plate_t * 0.5, z])\n        path_sweep(circle(r = prong_r), path3d(deduplicate(pts)), closed = false);\n}\n\nunion() {\n    difference() {\n        cuboid([plate_w, plate_t, plate_h], rounding = 5, edges = \"Y\", anchor = BOTTOM + BACK);\n        // Two screw holes (top and bottom) with countersinks.\n        for (z = [plate_h - 10, 10]) {\n            up(z) ycyl(h = plate_t * 3, d = screw_d, anchor = CENTER);\n            up(z) back(0.1) ycyl(h = screw_d, d1 = screw_d * 2.2, d2 = screw_d, anchor = BACK);\n        }\n    }\n    prong(plate_h * 0.62);\n    prong(plate_h * 0.30);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-wall-french-cleat",
      "title": "French-cleat hook",
      "description": "A French-cleat hanger: the back face is bevelled at 45 degrees to drop over a matching wall-mounted cleat rail, and the front carries an upward hook arm. Self-locking and tool-free to mount. Tune the body size, cleat angle, hook reach, and arm thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// French-cleat hook\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nbody_w    = 40;   // width of the cleat block\nbody_h    = 50;   // height of the cleat block\nbody_t    = 18;   // depth (thickness) of the block, front-to-back\ncleat_ang = 45;   // bevel angle of the cleat slot\narm_reach = 28;   // how far the front hook reaches out\narm_r     = 5;    // hook arm radius\n$fn = 56;\n\n// Body sits with its back face on the y=0 plane and extends forward to y=body_t.\nunion() {\n    // Cleat body with a 45-degree wedge groove cut into the back face so it\n    // hooks downward onto a matching wall rail. The groove is kept shallow\n    // (depth < body_t) so the block stays one connected piece.\n    difference() {\n        cuboid([body_w, body_t, body_h], rounding = 2, edges = \"Y\", anchor = BOTTOM + FRONT);\n        // Wedge slot: removes a 45deg triangular notch from the BACK face in\n        // the upper region. The back face is at y=0; the cutter is centred just\n        // behind it and rotated so its lower face slopes up-and-forward,\n        // forming the cleat's bearing surface. Kept short in depth so the front\n        // half of the block stays solid and connected.\n        translate([0, 0, body_h * 0.72])\n            xrot(cleat_ang)\n            cuboid([body_w + 2, body_t * 0.9, body_h], anchor = BACK);\n    }\n\n    // Front hook arm: sweeps out from the FRONT face (+Y) and curls up (+Z).\n    // Path is built directly in (y=out, z=up); start slightly inside the body\n    // so the swept tube fuses with the block.\n    pts = concat(\n        [ for (y = [-arm_r:2:arm_reach]) [0, y, 0] ],\n        [ for (a = [0:8:120]) [0,\n                               arm_reach + sin(a) * arm_r * 1.6,\n                               (1 - cos(a)) * arm_r * 1.6] ]\n    );\n    translate([0, body_t, body_h * 0.20])\n        path_sweep(circle(r = arm_r), deduplicate(pts), closed = false);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-wall-j-hook",
      "title": "J-hook wall hanger",
      "description": "A deep J-shaped hook on a screw-mount backplate for hanging tools, mugs, utensils, or cables. The long curved tail makes items easy to lift on and off. Tune the backplate, hook drop, hook radius, and bar thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// J-hook wall hanger\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nplate_w  = 26;   // backplate width\nplate_h  = 30;   // backplate height\nplate_t  = 5;    // backplate thickness\nreach    = 24;   // how far the hook reaches out from the wall\ndrop     = 28;   // how far the J curls back down\nbar_r    = 4;    // thickness (radius) of the hook bar\nscrew_d  = 4.5;  // screw clearance bore\n$fn = 56;\n\nunion() {\n    difference() {\n        cuboid([plate_w, plate_t, plate_h], rounding = 4, edges = \"Y\", anchor = BOTTOM + BACK);\n        up(plate_h - 8) ycyl(h = plate_t * 3, d = screw_d, anchor = CENTER);\n        up(plate_h - 8) back(0.1) ycyl(h = screw_d, d1 = screw_d * 2.2, d2 = screw_d, anchor = BACK);\n    }\n\n    // The J: out from the wall, then a half-circle curl down and back.\n    out_pts  = [ for (x = [0:2:reach]) [x, 0, 0] ];\n    curl_pts = [ for (a = [0:8:180]) [reach + sin(a) * (drop/2),\n                                      0,\n                                      -(1 - cos(a)) * (drop/2)] ];\n    pts = deduplicate(concat(out_pts, curl_pts));\n    translate([0, plate_t * 0.5, plate_h * 0.5])\n        path_sweep(circle(r = bar_r), path3d(pts), closed = false);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-wall-key-rail",
      "title": "Key rail with hooks",
      "description": "A horizontal screw-mount key rail: a rounded backing bar with a row of small upward hooks for keys, lanyards, or jewellery, plus screw holes at each end. Tune the rail length, number of hooks, hook size, and bar thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Key rail with hooks\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nrail_len  = 120;  // overall length of the rail\nrail_h    = 22;   // height of the backing bar\nrail_t    = 6;    // thickness of the backing bar\nn_hooks   = 4;    // number of key hooks\nhook_r    = 2.5;  // hook bar radius\nscrew_d   = 4.5;  // screw clearance bore\n$fn = 40;\n\nmodule key_hook() {\n    // Small hook: out from the bar then curling up.\n    pts = concat(\n        [ for (y = [-hook_r:1:6]) [0, y, 0] ],\n        [ for (a = [0:12:150]) [0, 6 + sin(a) * hook_r * 2, (1 - cos(a)) * hook_r * 2] ]\n    );\n    path_sweep(circle(r = hook_r), deduplicate(pts), closed = false);\n}\n\nunion() {\n    // Backing bar: back face on y=0, extends forward to y=rail_t.\n    difference() {\n        cuboid([rail_len, rail_t, rail_h], rounding = 3, edges = \"Y\", anchor = BOTTOM + FRONT);\n        // Screw holes near each end with countersinks (drilled along +Y).\n        for (sx = [-1, 1]) {\n            translate([sx * (rail_len/2 - 9), 0, rail_h * 0.5])\n                ycyl(h = rail_t * 3, d = screw_d, anchor = CENTER);\n            translate([sx * (rail_len/2 - 9), rail_t, rail_h * 0.5])\n                ycyl(h = screw_d, d1 = screw_d * 2.2, d2 = screw_d, anchor = FRONT);\n        }\n    }\n    // Row of evenly spaced hooks along the lower front edge.\n    for (i = [0 : n_hooks - 1]) {\n        x = -rail_len/2 + rail_len * (i + 0.5) / n_hooks;\n        translate([x, rail_t, rail_h * 0.25]) key_hook();\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-wall-pegboard-hook",
      "title": "Pegboard hook",
      "description": "A pegboard hook that plugs into standard 1-inch-pitch pegboard: two rear pegs drop into adjacent holes and a forward arm holds tools. Tune the peg diameter, pegboard pitch, board thickness, arm reach, and bar thickness. Default pitch 25.4 mm and peg 5.5 mm suit common 6 mm pegboard.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Pegboard hook\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\npeg_d     = 5.5;   // pegboard peg diameter (fits ~6mm holes)\npitch     = 25.4;  // pegboard hole pitch (1 inch)\nboard_t   = 6;     // pegboard panel thickness\narm_reach = 35;    // how far the front arm sticks out\nbar_r     = 4;     // arm / spine thickness (radius)\n$fn = 48;\n\n// Geometry: a vertical spine sits against the front of the board (y=0 plane is\n// the board's front face). Two pegs poke back (-Y) into holes one pitch apart;\n// the lower peg hooks down behind the board to lock. A front arm sweeps out.\nspine_h = pitch + bar_r * 2;\n\nunion() {\n    // Vertical spine against the board face (swept for a rounded body).\n    spine_pts = [ for (z = [0:2:spine_h]) [0, bar_r, z] ];\n    path_sweep(circle(r = bar_r), deduplicate(spine_pts), closed = false);\n\n    // Top peg: straight back into the upper hole.\n    top_pts = [ for (y = [bar_r : -2 : -(board_t + 3)]) [0, y, spine_h] ];\n    path_sweep(circle(r = peg_d/2), deduplicate(top_pts), closed = false);\n\n    // Bottom peg: back into the lower hole then curl DOWN behind the board so\n    // the hook cannot pull straight out (the classic pegboard lock).\n    bot_back = [ for (y = [bar_r : -2 : -(board_t + 2)]) [0, y, 0] ];\n    bot_curl = [ for (a = [0:12:90]) [0,\n                                      -(board_t + 2) - sin(a) * 4,\n                                      -(1 - cos(a)) * 6] ];\n    path_sweep(circle(r = peg_d/2), deduplicate(concat(bot_back, bot_curl)), closed = false);\n\n    // Front holding arm sweeps out (+Y) from the lower spine and tips up.\n    arm = concat(\n        [ for (y = [bar_r : 2 : arm_reach]) [0, y, bar_r] ],\n        [ for (a = [0:10:120]) [0, arm_reach + sin(a) * bar_r * 1.5, bar_r + (1 - cos(a)) * bar_r * 1.5] ]\n    );\n    path_sweep(circle(r = bar_r), deduplicate(arm), closed = false);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-wall-wall-hook",
      "title": "Screw-mount wall hook",
      "description": "A simple screw-mount wall hook: a flat rounded backplate with a countersunk screw hole and an upward-curving arm to hang coats, bags, or towels. Tune the backplate size, arm reach, hook depth, and screw bore.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Screw-mount wall hook\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\n\nplate_w   = 30;   // backplate width (against wall)\nplate_h   = 44;   // backplate height\nplate_t   = 5;    // backplate thickness\narm_reach = 26;   // how far the hook sticks out from the wall\narm_r     = 5;    // radius of the round hook arm\nscrew_d   = 4.5;  // screw clearance bore diameter\n$fn = 64;\n\nunion() {\n    // Flat backplate flush against the wall (wall face = back, at y=0 plane).\n    difference() {\n        cuboid([plate_w, plate_t, plate_h], rounding = 4, edges = \"Y\", anchor = BOTTOM + BACK);\n        // Countersunk screw hole near the top, drilled through the plate (along +Y).\n        up(plate_h - 9)\n            ycyl(h = plate_t * 3, d = screw_d, anchor = CENTER);\n        // Countersink cone on the front face.\n        up(plate_h - 9) back(0.1)\n            ycyl(h = screw_d, d1 = screw_d * 2.2, d2 = screw_d, anchor = BACK);\n        // Lower screw hole for stability.\n        up(9)\n            ycyl(h = plate_t * 3, d = screw_d, anchor = CENTER);\n        up(9) back(0.1)\n            ycyl(h = screw_d, d1 = screw_d * 2.2, d2 = screw_d, anchor = BACK);\n    }\n\n    // The hook arm: a curved tube sweeping out from the lower-middle of the plate\n    // and curling upward to catch the hung item.\n    arm_z = plate_h * 0.35;\n    pts = [\n        for (a = [0:6:90])\n            [ sin(a) * arm_reach, plate_t * 0.5 + (1 - cos(a)) * (arm_reach * 0.6) ]\n    ];\n    // Build the arm as a swept circle along an L-then-up path in the XZ-ish plane.\n    translate([0, plate_t * 0.5, arm_z])\n    yrot(0)\n    path_sweep(circle(r = arm_r),\n        path3d([ for (p = pts) [p[0], 0, p[1]] ]),\n        closed = false);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-wing-nut",
      "title": "Wing nut",
      "description": "A real wing nut: an internally threaded barrel with two flat finger wings for hand-tightening, built from the BOSL2 threading and shape libraries. A single printable solid. Tune the thread size and pitch, body height, and wing span.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Wing nut\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/threading.scad>\n\nthread_d  = 8;     // thread major diameter (mm)\npitch     = 1.25;  // thread pitch (mm)\nbody_h    = 10;    // height of the threaded barrel (mm)\nwing_span = 38;    // total tip-to-tip span across the wings (mm)\nwing_h    = 7;     // wing height (mm)\n$fn = 72;\n\ndifference() {\n    union() {\n        // Threaded barrel.\n        cyl(d = thread_d + 6, h = body_h, rounding = 1, anchor = BOTTOM);\n        // Two finger wings.\n        up(wing_h / 2)\n            prismoid(size1 = [wing_span, 5], size2 = [wing_span * 0.55, 3.5],\n                     h = wing_h, rounding = 1.0, anchor = CENTER);\n    }\n    // Internal thread through the barrel.\n    down(0.01)\n        threaded_rod(d = thread_d, l = body_h + 0.02, pitch = pitch,\n                     internal = true, bevel = false, blunt_start = false,\n                     anchor = BOTTOM);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-worm-gear",
      "title": "Worm gear",
      "description": "The wheel half of a worm drive: a throated spur gear whose teeth are curved to wrap around a mating worm screw. Pairing a worm with this wheel gives a high reduction, self-locking right-angle drive. Tune the module, tooth count, worm diameter, worm starts, and bore.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Worm gear\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/gears.scad>\n\nteeth       = 30;  // teeth on the worm wheel\ngear_mod    = 2;   // tooth module in mm (match the worm)\nworm_d      = 15;  // pitch diameter of the mating worm in mm\nworm_starts = 1;   // number of thread starts on the worm\nshaft_d     = 6;   // center shaft bore diameter in mm\n$fn = 48;\n\nworm_gear(mod = gear_mod, teeth = teeth, worm_diam = worm_d,\n          worm_starts = worm_starts, shaft_diam = shaft_d,\n          pressure_angle = 20);\n`);"
    },
    {
      "kind": "recipe",
      "id": "bosl2-worm-screw",
      "title": "Worm screw (drive)",
      "description": "The screw half of a worm drive: a helical thread that turns the mating worm wheel. One full turn of the worm advances the wheel by one tooth (per start), giving a large, self-locking reduction. Print this plus the worm gear wheel. Tune the module, length, diameter, and thread starts.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Worm screw (drive)\n//\n// Source: BOSL2 (BelfrySCAD) — BSD-2-Clause\n// https://github.com/BelfrySCAD/BOSL2\n// Generated from the library above, used under its license.\n\ninclude <BOSL2/std.scad>\ninclude <BOSL2/gears.scad>\n\ngear_mod    = 2;   // tooth module in mm (match the worm wheel)\nworm_d      = 15;  // pitch diameter in mm (match the wheel's worm_diam)\nworm_len    = 40;  // worm length in mm\nworm_starts = 1;   // number of thread starts\n$fn = 48;\n\nworm(mod = gear_mod, d = worm_d, l = worm_len, starts = worm_starts,\n     pressure_angle = 20);\n`);"
    },
    {
      "kind": "recipe",
      "id": "composite-primitives-tour-a",
      "title": "Spawn 10 composite primitives in a row (arch / arrow / bolt / box / bracket / capsule / channel / compartment / cross / funnel)",
      "description": "Build a contact sheet of structural shapes, deselecting between each to keep undo clean.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.arch",
        "ModelEditor.create.arrow",
        "ModelEditor.create.bolt",
        "ModelEditor.create.boxWithLid",
        "ModelEditor.create.bracket",
        "ModelEditor.create.capsule",
        "ModelEditor.create.channel",
        "ModelEditor.create.compartmentGrid",
        "ModelEditor.create.cross",
        "ModelEditor.create.funnel",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate"
      ],
      "code": "let x = -12;\nconst step = 2.7;\n\nconst a1 = await create.arch({});\na1.translate.set([x, 1.3, -0.7]); select(null, {mode: 'clear'}); console.log('arch:', a1.name); x += step;\n\nconst a2 = await create.arrow({});\na2.translate.set([x, 1.3, -0.7]); select(null, {mode: 'clear'}); console.log('arrow:', a2.name); x += step;\n\nconst a3 = await create.bolt({});\na3.translate.set([x, 1.3, -0.7]); select(null, {mode: 'clear'}); console.log('bolt:', a3.name); x += step;\n\nconst a4 = await create.boxWithLid({});\na4.translate.set([x, 1.3, -0.7]); select(null, {mode: 'clear'}); console.log('boxWithLid:', a4.name); x += step;\n\nconst a5 = await create.bracket({});\na5.translate.set([x, 1.3, -0.7]); select(null, {mode: 'clear'}); console.log('bracket:', a5.name); x += step;\n\nconst a6 = await create.capsule({});\na6.translate.set([x, 1.3, -0.7]); select(null, {mode: 'clear'}); console.log('capsule:', a6.name); x += step;\n\nconst a7 = await create.channel({});\na7.translate.set([x, 1.3, -0.7]); select(null, {mode: 'clear'}); console.log('channel:', a7.name); x += step;\n\nconst a8 = await create.compartmentGrid({});\na8.translate.set([x, 1.3, -0.7]); select(null, {mode: 'clear'}); console.log('compartmentGrid:', a8.name); x += step;\n\nconst a9 = await create.cross({});\na9.translate.set([x, 1.3, -0.7]); select(null, {mode: 'clear'}); console.log('cross:', a9.name); x += step;\n\nconst a10 = await create.funnel({});\na10.translate.set([x, 1.3, -0.7]); select(null, {mode: 'clear'}); console.log('funnel:', a10.name);\n\nawait Utils.wait.frame();\nviewport.frameAll();\n",
      "expectedOutput": "console: 10 spawn lines with mesh ids; viewport reframed."
    },
    {
      "kind": "recipe",
      "id": "composite-primitives-tour-b",
      "title": "Spawn 13 more composite primitives (gear / heart / knob / lShape / ring / roundedBox / screw / screwAndNut / spring / star / storageBox / tShape / tube)",
      "description": "Second half of the structural-primitives sweep: gears, decorative shapes, fasteners, and storage forms.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.gear",
        "ModelEditor.create.heart",
        "ModelEditor.create.knob",
        "ModelEditor.create.lShape",
        "ModelEditor.create.ring",
        "ModelEditor.create.roundedBox",
        "ModelEditor.create.screw",
        "ModelEditor.create.screwAndNut",
        "ModelEditor.create.spring",
        "ModelEditor.create.star",
        "ModelEditor.create.storageBox",
        "ModelEditor.create.tShape",
        "ModelEditor.create.tube",
        "Utils.wait.frame",
        "node.translate"
      ],
      "code": "let x = -16;\nconst step = 2.7;\n\nconst b1 = await create.gear({});\nb1.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('gear:', b1.name); x += step;\n\nconst b2 = await create.heart({});\nb2.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('heart:', b2.name); x += step;\n\nconst b3 = await create.knob({});\nb3.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('knob:', b3.name); x += step;\n\nconst b4 = await create.lShape({});\nb4.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('lShape:', b4.name); x += step;\n\nconst b5 = await create.ring({});\nb5.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('ring:', b5.name); x += step;\n\nconst b6 = await create.roundedBox({});\nb6.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('roundedBox:', b6.name); x += step;\n\nconst b7 = await create.screw({});\nb7.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('screw:', b7.name); x += step;\n\nconst b8 = await create.screwAndNut({});\nb8.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('screwAndNut:', b8.name); x += step;\n\nconst b9 = await create.spring({});\nb9.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('spring:', b9.name); x += step;\n\nconst b10 = await create.star({});\nb10.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('star:', b10.name); x += step;\n\nconst b11 = await create.storageBox({});\nb11.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('storageBox:', b11.name); x += step;\n\nconst b12 = await create.tShape({});\nb12.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('tShape:', b12.name); x += step;\n\nconst b13 = await create.tube({});\nb13.translate.set([x, 1.3, 2.5]); select(null, {mode: 'clear'}); console.log('tube:', b13.name);\n\nawait Utils.wait.frame();\n",
      "expectedOutput": "console: 13 spawn lines confirming each primitive."
    },
    {
      "kind": "recipe",
      "id": "compound-centrifugal-impeller",
      "title": "Centrifugal Impeller (compound form)",
      "description": "Backplate disk + central hub cylinder + axial through-bore + N radially-placed blades tilted backward. v1 uses rectangular blade slabs; true swept-curve blades land once a swept-curve primitive is available.",
      "intent": "mesh",
      "editor": "model",
      "category": "compound-forms",
      "apiSurfaces": [
        "ModelEditor.create.cylinder",
        "ModelEditor.create.cube",
        "Transform.xform",
        "Mesh.boolean"
      ],
      "code": "// Centrifugal impeller — adapt the constants below for your prompt.\nconst backplateDiameter = 80, backplateThickness = 4;\nconst hubDiameter = 24, hubHeight = 14;\nconst boreDiameter = 8;\nconst bladeCount = 12;\nconst bladeLength = 28, bladeHeight = 12, bladeThickness = 2;\nconst bladeBackTiltDeg = 45;\n\n// 1. Backplate (base at Z=0).\nconst backplate = await create.cylinder({\n  radiusTop: backplateDiameter / 2, radiusBottom: backplateDiameter / 2,\n  height: backplateThickness, radialSegments: 48, name: 'impeller-backplate',\n});\nbackplate.xform({ t: [0, 0, backplateThickness / 2] });\n\n// 2. Hub on top of backplate.\nconst hub = await create.cylinder({\n  radiusTop: hubDiameter / 2, radiusBottom: hubDiameter / 2,\n  height: hubHeight, radialSegments: 32, name: 'impeller-hub',\n});\nhub.xform({ t: [0, 0, backplateThickness + hubHeight / 2] });\nawait backplate.boolean(hub, 'union');\n\n// 3. Blades — rectangular slabs placed radially with backward tilt.\nconst bladeZ = backplateThickness + bladeHeight / 2;\nconst bladeCenterR = hubDiameter / 2 + bladeLength / 2;\nfor (let i = 0; i < bladeCount; i++) {\n  const thetaDeg = (i / bladeCount) * 360;\n  const thetaRad = (thetaDeg * Math.PI) / 180;\n  const bx = Math.cos(thetaRad) * bladeCenterR;\n  const by = Math.sin(thetaRad) * bladeCenterR;\n  const blade = await create.cube({\n    width: bladeLength, height: bladeThickness, depth: bladeHeight,\n    name: `impeller-blade-${i}`,\n  });\n  blade.xform({\n    t: [bx, by, bladeZ],\n    r: [0, 0, thetaDeg + bladeBackTiltDeg],\n  });\n  await backplate.boolean(blade, 'union');\n}\n\n// 4. Axial through-bore.\nconst bore = await create.cylinder({\n  radiusTop: boreDiameter / 2, radiusBottom: boreDiameter / 2,\n  height: backplateThickness + hubHeight + 2, radialSegments: 24,\n  name: 'impeller-bore',\n});\nbore.xform({ t: [0, 0, (backplateThickness + hubHeight) / 2] });\nawait backplate.boolean(bore, 'difference');\n\nconsole.log('impeller:', backplate.name);\n",
      "expectedOutput": "12-blade impeller with 45-degree backward-tilted rectangular blade slabs + central hub + axial bore."
    },
    {
      "kind": "recipe",
      "id": "compound-engine-cylinder",
      "title": "Engine Cylinder (compound form)",
      "description": "Vertical engine cylinder: central barrel + N stacked horizontal cooling fins + base flange with bolt circle + top cap + angled spark-plug boss with coaxial through-hole. Adapt the parameter constants at the top to fit your prompt.",
      "intent": "mesh",
      "editor": "model",
      "category": "compound-forms",
      "apiSurfaces": [
        "ModelEditor.create.cylinder",
        "Transform.xform",
        "Mesh.boolean"
      ],
      "code": "// Engine cylinder — adapt the constants below for your specific prompt.\nconst barrelDiameter = 36, barrelHeight = 70;\nconst finCount = 12, finOuterDiameter = 62, finThickness = 2;\nconst finBottomZ = 10, finSpacing = 5;\nconst flangeDiameter = 70, flangeThickness = 8;\nconst mountingBoltCount = 6, mountingBoltDiameter = 5, mountingBoltCircleDiameter = 56;\nconst capDiameter = 44, capHeight = 8;\nconst sparkPlugDiameter = 12, sparkPlugLength = 24, sparkPlugAngleDeg = 35;\nconst sparkPlugBoreDiameter = 5;\n\n// 1. Barrel (vertical cylinder, base at Z=0).\nconst barrel = await create.cylinder({\n  radiusTop: barrelDiameter / 2, radiusBottom: barrelDiameter / 2,\n  height: barrelHeight, radialSegments: 48, name: 'engine-barrel',\n});\nbarrel.xform({ t: [0, 0, barrelHeight / 2] });\n\n// 2. Stacked cooling fins (horizontal disks along Z).\nfor (let i = 0; i < finCount; i++) {\n  const z = finBottomZ + i * finSpacing;\n  const fin = await create.cylinder({\n    radiusTop: finOuterDiameter / 2, radiusBottom: finOuterDiameter / 2,\n    height: finThickness, radialSegments: 48, name: `engine-fin-${i}`,\n  });\n  fin.xform({ t: [0, 0, z + finThickness / 2] });\n  await barrel.boolean(fin, 'union');\n}\n\n// 3. Base flange with N bolt holes (manual radial placement).\nconst flange = await create.cylinder({\n  radiusTop: flangeDiameter / 2, radiusBottom: flangeDiameter / 2,\n  height: flangeThickness, radialSegments: 48, name: 'engine-flange',\n});\nflange.xform({ t: [0, 0, -flangeThickness / 2] });\nfor (let i = 0; i < mountingBoltCount; i++) {\n  const theta = (i / mountingBoltCount) * Math.PI * 2;\n  const bx = Math.cos(theta) * (mountingBoltCircleDiameter / 2);\n  const by = Math.sin(theta) * (mountingBoltCircleDiameter / 2);\n  const hole = await create.cylinder({\n    radiusTop: mountingBoltDiameter / 2, radiusBottom: mountingBoltDiameter / 2,\n    height: flangeThickness + 2, radialSegments: 24, name: `engine-flange-bolt-${i}`,\n  });\n  hole.xform({ t: [bx, by, -flangeThickness / 2] });\n  await flange.boolean(hole, 'difference');\n}\nawait barrel.boolean(flange, 'union');\n\n// 4. Top cap.\nconst cap = await create.cylinder({\n  radiusTop: capDiameter / 2, radiusBottom: capDiameter / 2,\n  height: capHeight, radialSegments: 48, name: 'engine-cap',\n});\ncap.xform({ t: [0, 0, barrelHeight + capHeight / 2] });\nawait barrel.boolean(cap, 'union');\n\n// 5. Spark-plug boss (angled cylinder + coaxial through-bore).\nconst angleY = 90 - sparkPlugAngleDeg;\nconst boss = await create.cylinder({\n  radiusTop: sparkPlugDiameter / 2, radiusBottom: sparkPlugDiameter / 2,\n  height: sparkPlugLength, radialSegments: 32, name: 'engine-spark-boss',\n});\nboss.xform({\n  t: [capDiameter / 2, 0, barrelHeight + capHeight / 2],\n  r: [0, angleY, 0],\n});\nawait barrel.boolean(boss, 'union');\n\nconst bore = await create.cylinder({\n  radiusTop: sparkPlugBoreDiameter / 2, radiusBottom: sparkPlugBoreDiameter / 2,\n  height: sparkPlugLength + 2, radialSegments: 32, name: 'engine-spark-bore',\n});\nbore.xform({\n  t: [capDiameter / 2, 0, barrelHeight + capHeight / 2],\n  r: [0, angleY, 0],\n});\nawait barrel.boolean(bore, 'difference');\n\nconsole.log('engine cylinder:', barrel.name);\n",
      "expectedOutput": "12-fin vertical engine cylinder with a 35-degree spark-plug boss + coaxial through-hole. Z-extent reaches ~85mm."
    },
    {
      "kind": "recipe",
      "id": "compound-flange-bolt-circle",
      "title": "Flange with Bolt Circle (compound form)",
      "description": "Flat circular flange: low cylinder + central through-bore + N bolt holes arranged on a circular pattern. Useful as a mounting plate or pipe-flange end cap.",
      "intent": "mesh",
      "editor": "model",
      "category": "compound-forms",
      "apiSurfaces": [
        "ModelEditor.create.cylinder",
        "Transform.xform",
        "Mesh.boolean"
      ],
      "code": "// Flange with bolt circle — adapt the constants below for your prompt.\nconst outerDiameter = 100, thickness = 10;\nconst centerBoreDiameter = 40;\nconst boltCount = 8, boltDiameter = 8, boltCircleDiameter = 80;\n\n// 1. Flange disk.\nconst flange = await create.cylinder({\n  radiusTop: outerDiameter / 2, radiusBottom: outerDiameter / 2,\n  height: thickness, radialSegments: 48, name: 'flange-body',\n});\nflange.xform({ t: [0, 0, thickness / 2] });\n\n// 2. Central through-bore.\nconst centerBore = await create.cylinder({\n  radiusTop: centerBoreDiameter / 2, radiusBottom: centerBoreDiameter / 2,\n  height: thickness + 2, radialSegments: 32, name: 'flange-center-bore',\n});\ncenterBore.xform({ t: [0, 0, thickness / 2] });\nawait flange.boolean(centerBore, 'difference');\n\n// 3. Bolt holes — manual radial placement (subtractive features = workflow B, not catalog).\nfor (let i = 0; i < boltCount; i++) {\n  const theta = (i / boltCount) * Math.PI * 2;\n  const bx = Math.cos(theta) * (boltCircleDiameter / 2);\n  const by = Math.sin(theta) * (boltCircleDiameter / 2);\n  const hole = await create.cylinder({\n    radiusTop: boltDiameter / 2, radiusBottom: boltDiameter / 2,\n    height: thickness + 2, radialSegments: 24, name: `flange-bolt-${i}`,\n  });\n  hole.xform({ t: [bx, by, thickness / 2] });\n  await flange.boolean(hole, 'difference');\n}\n\nconsole.log('flange:', flange.name);\n",
      "expectedOutput": "Flat disc 100mm OD + 40mm through-bore + 8 bolt holes on a 80mm circle."
    },
    {
      "kind": "recipe",
      "id": "compound-flanged-pipe-tee",
      "title": "Flanged Pipe Tee (compound form)",
      "description": "Flanged 3-way pipe tee: vertical pipe + horizontal pipe meeting at the origin + flange discs at each of the 3 open ends. The interior bore runs through both arms of the tee.",
      "intent": "mesh",
      "editor": "model",
      "category": "compound-forms",
      "apiSurfaces": [
        "ModelEditor.create.cylinder",
        "Transform.xform",
        "Mesh.boolean"
      ],
      "code": "// Flanged pipe tee — adapt the constants below for your prompt.\nconst pipeOuterDiameter = 30, pipeInnerDiameter = 22;\nconst verticalLength = 60, horizontalLength = 80;\nconst flangeDiameter = 50, flangeThickness = 6;\n\n// 1. Vertical pipe.\nconst vertical = await create.cylinder({\n  radiusTop: pipeOuterDiameter / 2, radiusBottom: pipeOuterDiameter / 2,\n  height: verticalLength, radialSegments: 48, name: 'tee-vertical',\n});\nvertical.xform({ t: [0, 0, verticalLength / 2] });\n\n// 2. Horizontal pipe — Y-rotated 90 degrees, centered at top of vertical.\nconst topZ = verticalLength;\nconst horizontal = await create.cylinder({\n  radiusTop: pipeOuterDiameter / 2, radiusBottom: pipeOuterDiameter / 2,\n  height: horizontalLength, radialSegments: 48, name: 'tee-horizontal',\n});\nhorizontal.xform({ t: [0, 0, topZ], r: [0, 90, 0] });\nawait vertical.boolean(horizontal, 'union');\n\n// 3. Three flange discs (bottom of vertical + both ends of horizontal).\nconst flangeBottom = await create.cylinder({\n  radiusTop: flangeDiameter / 2, radiusBottom: flangeDiameter / 2,\n  height: flangeThickness, radialSegments: 48, name: 'tee-flange-bottom',\n});\nflangeBottom.xform({ t: [0, 0, flangeThickness / 2] });\nawait vertical.boolean(flangeBottom, 'union');\n\nconst flangePosX = await create.cylinder({\n  radiusTop: flangeDiameter / 2, radiusBottom: flangeDiameter / 2,\n  height: flangeThickness, radialSegments: 48, name: 'tee-flange-posx',\n});\nflangePosX.xform({ t: [horizontalLength / 2 - flangeThickness / 2, 0, topZ], r: [0, 90, 0] });\nawait vertical.boolean(flangePosX, 'union');\n\nconst flangeNegX = await create.cylinder({\n  radiusTop: flangeDiameter / 2, radiusBottom: flangeDiameter / 2,\n  height: flangeThickness, radialSegments: 48, name: 'tee-flange-negx',\n});\nflangeNegX.xform({ t: [-horizontalLength / 2 + flangeThickness / 2, 0, topZ], r: [0, 90, 0] });\nawait vertical.boolean(flangeNegX, 'union');\n\n// 4. Bore the interiors.\nconst innerVertical = await create.cylinder({\n  radiusTop: pipeInnerDiameter / 2, radiusBottom: pipeInnerDiameter / 2,\n  height: verticalLength + 2, radialSegments: 32, name: 'tee-vertical-bore',\n});\ninnerVertical.xform({ t: [0, 0, verticalLength / 2] });\nawait vertical.boolean(innerVertical, 'difference');\n\nconst innerHorizontal = await create.cylinder({\n  radiusTop: pipeInnerDiameter / 2, radiusBottom: pipeInnerDiameter / 2,\n  height: horizontalLength + 2, radialSegments: 32, name: 'tee-horizontal-bore',\n});\ninnerHorizontal.xform({ t: [0, 0, topZ], r: [0, 90, 0] });\nawait vertical.boolean(innerHorizontal, 'difference');\n\nconsole.log('pipe tee:', vertical.name);\n",
      "expectedOutput": "3-way pipe tee with 30mm OD / 22mm ID + 50mm flange discs at all 3 open ends."
    },
    {
      "kind": "recipe",
      "id": "compound-ribbed-bracket",
      "title": "Ribbed Bracket (compound form)",
      "description": "L-shaped bracket with N stiffening gussets evenly distributed along the depth axis at the inner corner. v1 uses rectangular gussets; true triangular gussets land once a custom-profile extrude primitive is available.",
      "intent": "mesh",
      "editor": "model",
      "category": "compound-forms",
      "apiSurfaces": [
        "ModelEditor.create.lShape",
        "ModelEditor.create.cube",
        "Transform.xform",
        "Mesh.boolean"
      ],
      "code": "// Ribbed bracket — adapt the constants below for your prompt.\nconst armLength1 = 40, armLength2 = 30;\nconst thickness = 4, depth = 30;\nconst gussetCount = 3, gussetWidth = 14, gussetHeight = 14, gussetThickness = 3;\n\n// 1. L-shaped bracket body.\nconst bracket = await create.lShape({\n  armLength1, armLength2, thickness, depth, name: 'ribbed-bracket-body',\n});\n\n// 2. Gussets — rectangular slabs at the inner corner.\nconst denominator = gussetCount === 1 ? 2 : gussetCount + 1;\nconst innerX = thickness + gussetWidth / 2;\nconst innerY = thickness + gussetHeight / 2;\nfor (let i = 0; i < gussetCount; i++) {\n  const t = (i + 1) / denominator;\n  const dz = (t - 0.5) * depth;\n  const gusset = await create.cube({\n    width: gussetWidth, height: gussetHeight, depth: gussetThickness,\n    name: `ribbed-bracket-gusset-${i}`,\n  });\n  gusset.xform({ t: [innerX, innerY, dz] });\n  await bracket.boolean(gusset, 'union');\n}\n\nconsole.log('bracket:', bracket.name);\n",
      "expectedOutput": "L-bracket (40mm + 30mm arms) with 3 rectangular gussets at the inner corner along the 30mm depth."
    },
    {
      "kind": "recipe",
      "id": "compound-stepped-shaft-keyway",
      "title": "Stepped Shaft with Keyway (compound form)",
      "description": "Stepped shaft: three coaxial cylinders of decreasing diameter (large, medium, small) along Z + a rectangular keyway groove cut into the large segment. Common power-transmission shaft form.",
      "intent": "mesh",
      "editor": "model",
      "category": "compound-forms",
      "apiSurfaces": [
        "ModelEditor.create.cylinder",
        "ModelEditor.create.cube",
        "Transform.xform",
        "Mesh.boolean"
      ],
      "code": "// Stepped shaft with keyway — adapt the constants below for your prompt.\nconst largeDiameter = 40, largeLength = 30;\nconst mediumDiameter = 28, mediumLength = 30;\nconst smallDiameter = 18, smallLength = 25;\nconst keywayWidth = 6, keywayDepth = 4, keywayLength = 20;\n\n// 1. Large segment (base at Z=0).\nconst large = await create.cylinder({\n  radiusTop: largeDiameter / 2, radiusBottom: largeDiameter / 2,\n  height: largeLength, radialSegments: 48, name: 'shaft-large',\n});\nlarge.xform({ t: [0, 0, largeLength / 2] });\n\n// 2. Medium segment stacked on top.\nconst medium = await create.cylinder({\n  radiusTop: mediumDiameter / 2, radiusBottom: mediumDiameter / 2,\n  height: mediumLength, radialSegments: 48, name: 'shaft-medium',\n});\nmedium.xform({ t: [0, 0, largeLength + mediumLength / 2] });\nawait large.boolean(medium, 'union');\n\n// 3. Small segment stacked on top of medium.\nconst small = await create.cylinder({\n  radiusTop: smallDiameter / 2, radiusBottom: smallDiameter / 2,\n  height: smallLength, radialSegments: 48, name: 'shaft-small',\n});\nsmall.xform({ t: [0, 0, largeLength + mediumLength + smallLength / 2] });\nawait large.boolean(small, 'union');\n\n// 4. Keyway groove — rectangular slot subtracted from the large segment's side.\nconst groove = await create.cube({\n  width: keywayDepth * 2, height: keywayWidth, depth: keywayLength,\n  name: 'shaft-keyway-groove',\n});\ngroove.xform({ t: [largeDiameter / 2, 0, largeLength - keywayLength / 2] });\nawait large.boolean(groove, 'difference');\n\nconsole.log('shaft:', large.name);\n",
      "expectedOutput": "3-step shaft (40/28/18mm diameters stacked along Z) with a 6x4x20mm keyway groove in the large segment."
    },
    {
      "kind": "recipe",
      "id": "curve-constructors-sweep",
      "title": "Build a polyline, a circle, and a rectangle curve",
      "description": "Demonstrate create / createCircle / createRectangle: the three curve constructors.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.curve",
        "ModelEditor.create.circle",
        "ModelEditor.create.rectangle",
        "Utils.wait.frame"
      ],
      "code": "const poly = await create.curve({\n  points: [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]],\n  degree: 1,\n  closed: true,\n  name: 'SquarePoly',\n});\nselect(null, {mode: 'clear'});\nconsole.log('polyline:', poly.name);\n\nconst circle = await create.circle({\n  center: [3, 0, 0],\n  radius: 1.0,\n  name: 'UnitCircle',\n});\nselect(null, {mode: 'clear'});\nconsole.log('circle:', circle.name);\n\nconst rect = await create.rectangle({\n  p0: [6, 0, -1],\n  p1: [8, 0, 1],\n  name: 'TwoByTwo',\n});\nselect(null, {mode: 'clear'});\nconsole.log('rectangle:', rect.name);\n\nawait Utils.wait.frame();\n",
      "expectedOutput": "console: 3 curves created with their names."
    },
    {
      "kind": "recipe",
      "id": "curve-mutation-degree-closed-setpoint",
      "title": "Mutate a curve: degree, closed flag, control vertex position",
      "description": "Read .numPoints / .closed / .degree; mutate via .toggleClosed / .setDegree / .setPoint.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.circle",
        "curve.numPoints",
        "curve.closed",
        "curve.degree",
        "curve.toggleClosed",
        "curve.setDegree",
        "curve.setPoint"
      ],
      "code": "const ring = await create.circle({ center: [2.5, 1.3, -0.7], radius: 1.0, name: 'CurveMut' });\nselect(null, {mode: 'clear'});\nconsole.log('numPoints:', ring.numPoints, 'closed:', ring.closed, 'degree:', ring.degree);\nring.toggleClosed(); console.log('toggle closed:', ring.closed);\nring.toggleClosed(); console.log('re-toggle:', ring.closed);\nring.setDegree(1); console.log('degree(1):', ring.degree);\nring.setDegree(3); console.log('degree(3):', ring.degree);\ntry { await ring.setDegree(2); } catch (e) { console.log('setDegree(2) rejected:', e.code); }\nawait ring.setPoint(0, [3.5, 2.0, -0.7]);\nconsole.log('final:', ring.numPoints, ring.closed, ring.degree);\n",
      "expectedOutput": "console: per-step CV count + closed/degree state."
    },
    {
      "kind": "recipe",
      "id": "deformer-attribute-param-read-write",
      "title": "DeformerAttribute: typed param access on a deformer record",
      "description": "DeformerAttribute is the per-parameter handle on a deformer. meshId + deformerId + param identify it; get/set is the typed value pair.",
      "intent": "mesh",
      "editor": "model",
      "category": "attributes",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "mesh.addBend",
        "node.translate",
        "deformerAttribute.meshId",
        "deformerAttribute.deformerId",
        "deformerAttribute.param",
        "deformerAttribute.get",
        "deformerAttribute.set"
      ],
      "code": "// DeformerAttribute is the typed per-parameter handle hanging off a\n// deformer record. Identity is (meshId, deformerId, param); get()/set() is\n// the typed read/write pair. Self-contained from an empty Model scene.\n\nconst cube = await create.cube({\n  width: 1.2, height: 1.2, depth: 1.2,\n  subdivisionsX: 4, subdivisionsY: 4, subdivisionsZ: 4, // dense enough for the bend to read\n  name: 'DefAttr',\n});\ncube.xform({ t: [5.0, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nawait Utils.wait.frame();\n\n// A Bend is a non-linear deformer — add it with mode:'bend'.\nconst bend = await cube.deformers.add('nonLinear', { mode: 'bend' });\n\n// Pull two DeformerAttributes off the handle. envelope is number-typed;\n// enabled is boolean-typed. curvature is the bend-specific shape attr.\nconst env = bend.envelope;       // DeformerAttribute<number>\nconst ena = bend.enabled;        // DeformerAttribute<boolean>\nconst curv = bend.curvature;     // DeformerAttribute<number> (bend-only)\n\n// IDENTITY — each attribute names its mesh, deformer, and param slot.\nconsole.log('env meshId:', env.meshId);\nconsole.log('env deformerId:', env.deformerId);\nconsole.log('env param:', env.param);     // 'envelope'\nconsole.log('ena param:', ena.param);     // 'enabled'\nconsole.log('curv param:', curv.param);   // 'curvature'\n\n// INITIAL READS.\nconsole.log('envelope.get():', env.get());\nconsole.log('enabled.get():', ena.get());\nconsole.log('curvature.get():', curv.get());\n\n// TYPED WRITES — each set() lands one undo entry; wrong-typed values throw.\nenv.set(0.25);   // fade the bend to a quarter strength (0..1)\nconsole.log('envelope after set 0.25:', env.get());\ncurv.set(1.5);   // stronger curvature (radians of bend per unit height)\nconsole.log('curvature after set 1.5:', curv.get());\nena.set(false);  // disable without removing\nconsole.log('enabled after set false:', ena.get());\n\n// Restore.\nenv.set(1.0);\nena.set(true);\nconsole.log('deformer-attribute read/write done on \"' + cube.name + '\"');\n",
      "expectedOutput": "console: identity + initial reads + post-write reads."
    },
    {
      "kind": "recipe",
      "id": "deformer-handle-introspection-and-weights",
      "title": "Inspect a deformer handle: meshId, type, displayName, envelope, weights",
      "description": "DeformerHandle exposes the live deformer record. Identity: meshId + deformerId + deformerType + displayName. Common params: envelope + enabled. Per-vertex: weights[i] read + setWeight(i, w) write.",
      "intent": "mesh",
      "editor": "model",
      "category": "attributes",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "mesh.addSmooth",
        "node.translate",
        "deformerHandle.meshId",
        "deformerHandle.deformerId",
        "deformerHandle.deformerType",
        "deformerHandle.displayName",
        "deformerHandle.envelope",
        "deformerHandle.enabled",
        "deformerHandle.weights",
        "deformerHandle.setWeight"
      ],
      "code": "// Inspect a live DeformerHandle end-to-end: identity, the common\n// envelope/enabled attributes, and the per-vertex weight array.\n// Self-contained from an empty Model scene.\n\n// A subdivided cube gives the deformer enough vertices to weight-paint.\nconst cube = await create.cube({\n  width: 1, height: 1, depth: 1,\n  subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2, // 3x3x3 grid of verts per face\n  name: 'DefIntro',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] }); // park off-origin so BVH + renderer stay fresh\nawait select(null, { mode: 'clear' });\nawait Utils.wait.frame();\n\n// Add a Smooth deformer. mesh.deformers.add(type, opts?) is async and\n// returns the typed handle (SmoothHandle here).\nconst smooth = await cube.deformers.add('smooth');\n\n// IDENTITY — every handle carries the host meshId + its own deformerId +\n// the deformerType string + a human-readable displayName.\nconsole.log('meshId:', smooth.meshId);\nconsole.log('deformerId:', smooth.deformerId);\nconsole.log('deformerType:', smooth.deformerType); // 'smooth'\nconsole.log('displayName:', smooth.displayName);\n\n// COMMON ATTRS — envelope (0..1 overall strength) + enabled (on/off toggle).\nconsole.log('envelope:', smooth.envelope.get()); // default 1\nconsole.log('enabled:', smooth.enabled.get());    // default true\n\n// PER-VERTEX WEIGHTS — weights[] is one float per vertex (length === vertexCount).\nconsole.log('weights.length:', smooth.weights.length);\nconsole.log('weights[0]:', smooth.weights[0]);\n\n// WRITE WEIGHTS — every write must go through setWeight(i, w); each call is\n// one undo step. Ramp the first four verts from 0.5 up to 0.8.\nfor (let i = 0; i < Math.min(4, smooth.weights.length); i++) {\n  smooth.setWeight(i, 0.5 + 0.1 * i); // i=0 -> 0.5, i=3 -> 0.8 (weights clamp to 0..1)\n}\nconsole.log('weights[0..3]:', [\n  smooth.weights[0].toFixed(2), smooth.weights[1].toFixed(2),\n  smooth.weights[2].toFixed(2), smooth.weights[3].toFixed(2),\n].join(' '));\n\n// FADE + TOGGLE — exercise the typed attribute setters (each is undoable).\nsmooth.envelope.set(0.5); // half strength\nconsole.log('envelope after set 0.5:', smooth.envelope.get());\nsmooth.enabled.set(false); // disabled but retained in the stack\nconsole.log('enabled after set false:', smooth.enabled.get());\nsmooth.enabled.set(true);  // re-enable\n\nconsole.log('deformer-handle introspection done on \"' + cube.name + '\"');\n",
      "expectedOutput": "console: handle identity + weights length + write evidence."
    },
    {
      "kind": "recipe",
      "id": "dialog-open-close-sweep",
      "title": "Open, query, and close every cross-editor dialog",
      "description": "Cycle through bug-report / export / help / import / settings dialogs; check active and isAnyOpen / isBlocking.",
      "intent": "scene",
      "editor": "previewer",
      "category": "misc",
      "apiSurfaces": [
        "ModelEditor.dev.dialogs.bugReport.open",
        "ModelEditor.dev.dialogs.bugReport.close",
        "ModelEditor.dev.dialogs.bugReport.isOpen",
        "ModelEditor.dev.dialogs.export.open",
        "ModelEditor.dev.dialogs.export.close",
        "ModelEditor.dev.dialogs.export.isOpen",
        "ModelEditor.dev.dialogs.help.open",
        "ModelEditor.dev.dialogs.help.close",
        "ModelEditor.dev.dialogs.help.isOpen",
        "ModelEditor.dev.dialogs.import.open",
        "ModelEditor.dev.dialogs.import.close",
        "ModelEditor.dev.dialogs.import.isOpen",
        "ModelEditor.dev.dialogs.settings.open",
        "ModelEditor.dev.dialogs.settings.close",
        "ModelEditor.dev.dialogs.settings.isOpen",
        "ModelEditor.dev.dialogs.active",
        "ModelEditor.dev.dialogs.isAnyOpen",
        "ModelEditor.dev.dialogs.isBlocking"
      ],
      "code": "async function cycle(name, dialog) {\n  try {\n    await dialog.open();\n    console.log(name + '.open: isOpen=' + dialog.isOpen() + ', active=' + dev.dialogs.active());\n    await dialog.close();\n    console.log(name + '.close: isOpen=' + dialog.isOpen());\n  } catch (err) {\n    console.log(name + ' skipped:', err.message);\n  }\n}\n\nawait cycle('bugReport', dev.dialogs.bugReport);\nawait cycle('export', dev.dialogs.export);\nawait cycle('help', dev.dialogs.help);\nawait cycle('import', dev.dialogs.import);\nawait cycle('settings', dev.dialogs.settings);\n\nconsole.log('isAnyOpen after sweep:', dev.dialogs.isAnyOpen());\nconsole.log('isBlocking:', dev.dialogs.isBlocking());\n",
      "expectedOutput": "console: per-dialog open/close/isOpen result; any-open and blocking probes."
    },
    {
      "kind": "recipe",
      "id": "distribute-and-align-row",
      "title": "Distribute and align a row of objects",
      "description": "node1.distribute([node2, node3], { axis }) spaces nodes evenly between the first and last; node1.align([node2, node3], { axis, mode }) snaps every node's axis component to the min / center / max of the set.",
      "intent": "mesh",
      "editor": "model",
      "category": "snap-align",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Node.distribute",
        "Node.align",
        "node.translate"
      ],
      "code": "// Five cubes, then space the middle three evenly and bottom-align them.\nconst cubes = [];\nfor (let i = 0; i < 5; i++) cubes.push(await create.cube({ width: 0.5, name: 'C' + i }));\ncubes[0].translate.set([0, 0, 0]);\ncubes[4].translate.set([8, 0, 0]);\ncubes[0]?.distribute(cubes.slice(1), { axis: 'x' });\ncubes[0]?.align(cubes.slice(1), { axis: 'y', mode: 'min' });\nconsole.log(cubes.map(c => c.translate.get()[0]));   // [0, 2, 4, 6, 8]\n",
      "expectedOutput": "console line with x-positions [0, 2, 4, 6, 8]; 5 cubes evenly spaced + bottom-aligned."
    },
    {
      "kind": "recipe",
      "id": "error-class-instanceof-discriminate",
      "title": "Catch + classify every error class in the scripting API",
      "description": "Every scripting-API error carries a stable .name + frozen .code + class-specific fields. Pattern-match on .name to dispatch; read class-specific properties for context.",
      "intent": "mesh",
      "editor": "model",
      "category": "misc",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "ModelEditor.create.cube",
        "ModelEditor.create.group",
        "Utils.wait.until",
        "node.attr",
        "node.addAttr",
        "attribute.get",
        "attribute.set",
        "attribute.lock",
        "transform.freezeTransform",
        "attributeLockedError.code",
        "attributeLockedError.operation",
        "attributeLockedError.nodeId",
        "attributeLockedError.attrPath",
        "attributeNotFoundError.nodeId",
        "attributeNotFoundError.path",
        "deformerStaleError.code",
        "deformerStaleError.meshId",
        "deformerStaleError.deformerId",
        "deformerStaleError.deformerType",
        "editorChangedDuringSessionError.code",
        "editorChangedDuringSessionError.toolId",
        "editorChangedDuringSessionError.meshId",
        "editorChangedDuringSessionError.from",
        "editorChangedDuringSessionError.to",
        "historyNotInitializedError.code",
        "importFailedError.code",
        "importFailedError.filename",
        "importFailedError.format",
        "importFailedError.underlying",
        "interactiveSessionStaleError.code",
        "interactiveSessionStaleError.toolId",
        "interactiveSessionStaleError.meshId",
        "interactiveSessionStaleError.previousState",
        "invalidArgumentError.argName",
        "nodeNotFoundError.nodeId",
        "nodeStaleError.nodeId",
        "nodeStaleError.name",
        "operationBlockedByDialogError.code",
        "operationBlockedByDialogError.operation",
        "operationBlockedByDialogError.dialogId",
        "operationCancelledError.code",
        "operationCancelledError.reason",
        "operationNotApplicableError.operation",
        "operationNotApplicableError.reason",
        "operationNotInEditorError.operation",
        "operationNotInEditorError.currentEditor",
        "operationNotInEditorError.allowedEditors",
        "operationNotSupportedError.operation",
        "permissionDeniedError.code",
        "permissionDeniedError.resource",
        "permissionDeniedError.handle",
        "persistedHandleStaleError.code",
        "persistedHandleStaleError.persistKey",
        "persistedHandleStaleError.lastSeenName",
        "waitTimeoutError.code",
        "writeFailedError.code",
        "writeFailedError.fileName",
        "writeFailedError.reason"
      ],
      "code": "// Every scripting-API error is a real CLASS on Utils.errors, carries a\n// stable .name + frozen .code, and exposes class-specific fields. Use an\n// instanceof ladder to dispatch; read the typed fields for context.\nconst E = Utils.errors;\n\nfunction describe(err) {\n  const head = { name: err.name, code: err.code };\n  // Order matters only insofar as each branch reads its own typed fields.\n  if (err instanceof E.NodeNotFoundError) return { ...head, nodeId: err.nodeId };\n  if (err instanceof E.NodeStaleError) return { ...head, nodeId: err.nodeId, lastKnownName: err.lastKnownName };\n  if (err instanceof E.AttributeNotFoundError) return { ...head, nodeId: err.nodeId, path: err.path };\n  if (err instanceof E.AttributeLockedError) return { ...head, operation: err.operation, nodeId: err.nodeId, attrPath: err.attrPath };\n  if (err instanceof E.InvalidArgumentError) return { ...head, argName: err.argName };\n  if (err instanceof E.OperationNotApplicableError) return { ...head, operation: err.operation, reason: err.reason };\n  if (err instanceof E.OperationNotInEditorError) return { ...head, operation: err.operation, currentEditor: err.currentEditor, allowedEditors: err.allowedEditors };\n  if (err instanceof E.OperationNotSupportedError) return { ...head, operation: err.operation };\n  if (err instanceof E.OperationBlockedByDialogError) return { ...head, operation: err.operation, dialogId: err.dialogId };\n  if (err instanceof E.OperationCancelledError) return { ...head, reason: err.reason };\n  if (err instanceof E.DeformerStaleError) return { ...head, meshId: err.meshId, deformerId: err.deformerId, deformerType: err.deformerType };\n  if (err instanceof E.WaitTimeoutError) return head;\n  if (err instanceof E.ImportFailedError) return { ...head, filename: err.filename, format: err.format };\n  if (err instanceof E.WriteFailedError) return { ...head, fileName: err.fileName, reason: err.reason };\n  return { ...head, message: err.message }; // catch-all for any other class\n}\n\n// Self-contained from an empty Model scene: provoke six REAL catches.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'ErrIntro' });\nselect(null, { mode: 'clear' });\n\n// 1. NodeNotFoundError — snap to a name that does not exist.\ntry { cube.snapTo('missing-' + Date.now()); } catch (e) { console.log('1.', JSON.stringify(describe(e))); }\n// 2. AttributeNotFoundError — read an attribute that was never added.\ntry { cube.attr('bogus').get(); } catch (e) { console.log('2.', JSON.stringify(describe(e))); }\n// 3. InvalidArgumentError — write past an ENFORCED limit.\ncube.addAttr({ ln: 'wet', at: 'float', dv: 0.5, k: true, min: 0, max: 1 });\ncube.attr('wet').enableLimits(true);\ntry { cube.attr('wet').set(2); } catch (e) { console.log('3.', JSON.stringify(describe(e))); }\n// 4. AttributeLockedError — write to a locked channel. node.translate.x is the\n//    per-AXIS handle (Vec3AxisAttribute: .lock / .get / .set) — NOT\n//    node.attr('translate.x') (the dotted path is not a DG plug and throws\n//    AttributeNotFoundError). Locking the axis makes the next .set() throw.\ncube.translate.x.lock(true);\ntry { cube.translate.x.set(5); } catch (e) { console.log('4.', JSON.stringify(describe(e))); }\ncube.translate.x.lock(false);\n// 5. Some operation error — freezing a group's transform may be unsupported.\nconst grp = await create.group([cube], { name: 'ErrGrp' });\ntry { grp.freezeTransform(); } catch (e) { console.log('5.', JSON.stringify(describe(e))); }\n// 6. WaitTimeoutError — a predicate that never becomes true.\ntry { await Utils.wait.until(() => false, { timeoutMs: 80 }); } catch (e) { console.log('6.', JSON.stringify(describe(e))); }\n",
      "expectedOutput": "console: six real catches with class-typed field readout."
    },
    {
      "kind": "example",
      "id": "example:AbActionsApi",
      "symbol": "AbActionsApi",
      "intent": "scene",
      "code": "// 1) List every registered Action.\nconst all = actions.list();\nconsole.log('total actions: ' + all.length);\n// 2) Filter by category, only those applicable in the current context.\nconst modifiers = actions.list({ category: 'Modifiers', applicableOnly: true });\nconsole.log('applicable Modifiers: ' + modifiers.length);\n// 3) Search by name.\nconst hits = actions.search('bevel', { applicableOnly: false });\nconsole.log('bevel matches: ' + hits.length);\n// 4) Resolve one Action by id.\nconst desc = actions.get('selection.selectAll');\nconsole.log('selectAll resolved: ' + (desc !== null));\n// 5) Run the generic fallback (no typed entry yet).\nawait actions.run('selection.selectAll');\nconsole.log('selected count: ' + ls({ selected: true }).length);\n// 6) Clean up.\nawait select(null, { mode: 'clear' });"
    },
    {
      "kind": "example",
      "id": "example:AbActionsApi.example",
      "symbol": "AbActionsApi.example",
      "intent": "scene",
      "code": "// 1. Setup — none; example() is a pure read.\n// 2. Action — fetch the runnable example for the actions sub-namespace.\nconst exampleText = actions.example();\n// 3. Read-back — it is a non-empty string.\nconsole.log('example chars: ' + exampleText.length);\n// 4. Verify — the body references the actions namespace.\nconsole.log('example body references actions.: ' + exampleText.includes('actions.'));"
    },
    {
      "kind": "example",
      "id": "example:AbActionsApi.get",
      "symbol": "AbActionsApi.get",
      "intent": "scene",
      "code": "const desc = actions.get('selection.selectAll');\nconsole.log('selectAll exists: ' + (desc !== null));\nconsole.log('bogus exists: ' + (actions.get('not.a.real.id') !== null));"
    },
    {
      "kind": "example",
      "id": "example:AbActionsApi.help",
      "symbol": "AbActionsApi.help",
      "intent": "scene",
      "code": "// 1. Setup — none; help() is a pure read.\n// 2. Action — fetch the prose help for the actions sub-namespace.\nconst helpText = actions.help();\n// 3. Read-back — it is a non-empty string naming real members.\nconsole.log('help mentions list: ' + helpText.includes('list'));\nconsole.log('help mentions run: ' + helpText.includes('run'));\n// 4. Verify — help is non-trivial.\nconsole.log('help is non-empty: ' + (helpText.length > 0));"
    },
    {
      "kind": "example",
      "id": "example:AbActionsApi.list",
      "symbol": "AbActionsApi.list",
      "intent": "scene",
      "code": "const all = actions.list();\nconst modifiers = actions.list({ category: 'Modifiers', applicableOnly: true });\nconsole.log('all: ' + all.length + ' modifiers: ' + modifiers.length);"
    },
    {
      "kind": "example",
      "id": "example:AbActionsApi.run",
      "symbol": "AbActionsApi.run",
      "intent": "scene",
      "code": "await actions.run('selection.selectAll');\nconsole.log('selected count: ' + ls({ selected: true }).length);\nawait select(null, { mode: 'clear' });"
    },
    {
      "kind": "example",
      "id": "example:AbActionsApi.search",
      "symbol": "AbActionsApi.search",
      "intent": "scene",
      "code": "const hits = actions.search('select', { applicableOnly: false });\nconsole.log('select matches: ' + hits.length);"
    },
    {
      "kind": "example",
      "id": "example:AbDialogHandle.close",
      "symbol": "AbDialogHandle.close",
      "intent": "scene",
      "code": "// 1) Open a dialog so the close has something to do.\nconst dlg = dev.dialogs.help;\nawait dlg.open();\nconsole.log('open before close: ' + dlg.isOpen());\n// 2) Close it; re-closing is a no-op.\nawait dlg.close();\nawait dlg.close();\nconsole.log('open after close: ' + dlg.isOpen());"
    },
    {
      "kind": "example",
      "id": "example:AbDialogHandle.isOpen",
      "symbol": "AbDialogHandle.isOpen",
      "intent": "scene",
      "code": "// 1) Probe a dialog before opening it.\nconst dlg = dev.dialogs.help;\nconsole.log('open initially: ' + dlg.isOpen());\n// 2) Open it and probe again.\nawait dlg.open();\nconsole.log('open after open: ' + dlg.isOpen());\n// 3) Clean up.\nawait dlg.close();"
    },
    {
      "kind": "example",
      "id": "example:AbDialogHandle.open",
      "symbol": "AbDialogHandle.open",
      "intent": "scene",
      "code": "// 1) Grab a dialog handle and open it.\nconst dlg = dev.dialogs.help;\nawait dlg.open();\nconsole.log('open after open: ' + dlg.isOpen());\n// 2) Re-opening is a no-op.\nawait dlg.open();\nconsole.log('still open: ' + dlg.isOpen());\n// 3) Clean up.\nawait dlg.close();"
    },
    {
      "kind": "example",
      "id": "example:AbPreviewerScreenshot.blob",
      "symbol": "AbPreviewerScreenshot.blob",
      "intent": "scene",
      "code": "// 1) Capture a screenshot of the Previewer viewport.\nconst shot = await viewport.captureScreenshot();\n// 2) Inspect the blob.\nconsole.log('blob is Blob: ' + (shot.blob instanceof Blob));\nconsole.log('blob type: ' + shot.blob.type);\nconsole.log('blob bytes: ' + shot.blob.size);"
    },
    {
      "kind": "example",
      "id": "example:AbPreviewerScreenshot.height",
      "symbol": "AbPreviewerScreenshot.height",
      "intent": "scene",
      "code": "// 1) Capture a screenshot and read its height.\nconst shot = await viewport.captureScreenshot();\nconsole.log('shot height: ' + shot.height);"
    },
    {
      "kind": "example",
      "id": "example:AbPreviewerScreenshot.toDataUrl",
      "symbol": "AbPreviewerScreenshot.toDataUrl",
      "intent": "scene",
      "code": "// 1) Capture and convert to a data URL.\nconst shot = await viewport.captureScreenshot();\nconst url = await shot.toDataUrl();\nconsole.log('url prefix: ' + url.slice(0, 22));"
    },
    {
      "kind": "example",
      "id": "example:AbPreviewerScreenshot.width",
      "symbol": "AbPreviewerScreenshot.width",
      "intent": "scene",
      "code": "// 1) Capture a screenshot and read its width.\nconst shot = await viewport.captureScreenshot();\nconsole.log('shot width: ' + shot.width);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurve",
      "symbol": "AnimCurve",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control so a curve exists.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 2.5);\nctl.setKey('rotateX', 1, 5.0);\n// 2. Read the curve back.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    console.log('bone: ' + curve.boneName);\n    console.log('attr: ' + curve.attr);\n    console.log('key count: ' + curve.keys.count);\n    console.log('value at t=0.5: ' + curve.sampleAt(0.5).toFixed(3));\n}\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:AnimCurve.attr",
      "symbol": "AnimCurve.attr",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A; we have the handle.\n// 3. Action — read the bound attribute path from the curve handle.\nconst attr = ctl.getCurve('rotateX').attr;\n// Observe.\nconsole.log('attr: ' + attr);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurve.bake",
      "symbol": "AnimCurve.bake",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A; we have the handle.\n// 3. Action — access the bake sub-namespace.\nconst curve = ctl.getCurve('rotateX');\nconst bake = curve.bake;\n// Observe.\nconsole.log('bake ready: ' + (bake !== null));"
    },
    {
      "kind": "example",
      "id": "example:AnimCurve.boneName",
      "symbol": "AnimCurve.boneName",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A; we have the handle.\n// 3. Action — read the bound bone name from the curve handle.\nconst name = ctl.getCurve('rotateX').boneName;\n// Observe.\nconsole.log('bone name: ' + name);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurve.clipboard",
      "symbol": "AnimCurve.clipboard",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A; we have the handle.\n// 3. Action — access the clipboard sub-namespace.\nconst curve = ctl.getCurve('rotateX');\nconst clipboard = curve.clipboard;\n// Observe.\nconsole.log('clipboard ready: ' + (clipboard !== null));"
    },
    {
      "kind": "example",
      "id": "example:AnimCurve.keys",
      "symbol": "AnimCurve.keys",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A; we have the handle.\n// 3. Action — access the keys sub-namespace.\nconst curve = ctl.getCurve('rotateX');\nconst keys = curve.keys;\n// Observe.\nconsole.log('keys count: ' + keys.count);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurve.sampleAt",
      "symbol": "AnimCurve.sampleAt",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 10.0);\n// 2. Resolve and sample at three times.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    console.log('value at t=0: ' + curve.sampleAt(0).toFixed(3));\n    console.log('value at t=0.5: ' + curve.sampleAt(0.5).toFixed(3));\n    console.log('value at t=1: ' + curve.sampleAt(1).toFixed(3));\n}\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:AnimCurve.tangents",
      "symbol": "AnimCurve.tangents",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A; we have the handle.\n// 3. Action — access the tangents sub-namespace.\nconst curve = ctl.getCurve('rotateX');\nconst tangents = curve.tangents;\n// Observe.\nconsole.log('tangents ready: ' + (tangents !== null));"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveBakeApi",
      "symbol": "AnimCurveBakeApi",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A; we have the curve handle.\n// 3. Action — reach the bake sub-namespace.\nconst bake = ctl.getCurve('rotateX').bake;\n// Observe.\nconsole.log('bake api ready: ' + (bake !== null));"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveBakeApi.between",
      "symbol": "AnimCurveBakeApi.between",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 5.0);\nctl.setKey('rotateX', 2, 2.5);\n// 2. Bake the middle stretch only.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.bake.between({ frame: 12, frameStep: 2 });\n    console.log('bake between complete');\n}\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:AnimCurveBakeApi.example",
      "symbol": "AnimCurveBakeApi.example",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A.\n// 3. Action — render the example text.\nconst s = ctl.getCurve('rotateX').bake.example();\n// Observe.\nconsole.log('example length: ' + s.length);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveBakeApi.full",
      "symbol": "AnimCurveBakeApi.full",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 5.0);\n// 2. Bake the whole curve to 1 key per frame with auto tangents.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.bake.full({ frameStep: 2, range: [0, 48] });\n    console.log('bake complete');\n}\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:AnimCurveBakeApi.help",
      "symbol": "AnimCurveBakeApi.help",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A.\n// 3. Action — render the help text.\nconst s = ctl.getCurve('rotateX').bake.help();\n// Observe.\nconsole.log('help length: ' + s.length);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveBakeApi.infinity",
      "symbol": "AnimCurveBakeApi.infinity",
      "intent": "scene",
      "code": "// Animation editor, rigged asset. Everything by NAME — never ids.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0); // seconds — creates the curve\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.bake.infinity({ frameStep: 1 });\n    console.log('infinity baked');\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveClipboardApi",
      "symbol": "AnimCurveClipboardApi",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A; we have the curve handle.\n// 3. Action — reach the clipboard sub-namespace.\nconst cb = ctl.getCurve('rotateX').clipboard;\n// Observe.\nconsole.log('clipboard ready: ' + (cb !== null));"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveClipboardApi.copy",
      "symbol": "AnimCurveClipboardApi.copy",
      "intent": "scene",
      "code": "// Animation editor, rigged asset. Everything by NAME — never ids.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0); // seconds — creates the curve\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.keys.set(12, 45);\n    // select keys in the graph editor (or via AnimEditor.selectKeys), then:\n    await curve.clipboard.copy();\n    await curve.clipboard.paste({ time: 24 });\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveClipboardApi.copyControl",
      "symbol": "AnimCurveClipboardApi.copyControl",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nawait select(ctl);\nctl.setKey('rotateX', 0, 2.5);\n// 2. Capture all 9 channels of the selected control.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.clipboard.copyControl();\n    console.log('copyControl invoked');\n}\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:AnimCurveClipboardApi.copyPaste",
      "symbol": "AnimCurveClipboardApi.copyPaste",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 5.0);\n// 2. Open the copy/paste popup.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.clipboard.copyPaste();\n    console.log('copyPaste invoked');\n}\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:AnimCurveClipboardApi.cut",
      "symbol": "AnimCurveClipboardApi.cut",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 5.0);\n// 2. Cut (copy + delete), then paste the keys at a new time.\nconst curve = ctl.getCurve('rotateX');\n// getCurve returns null when the channel has no baked curve yet.\nif (curve !== null) {\n    // select keys in the graph editor (or via AnimEditor.selectKeys), then:\n    await curve.clipboard.cut();\n    // 3. Paste re-lands the cut keys at the new anchor time.\n    await curve.clipboard.paste({ time: 2.5 });\n    console.log('cut invoked');\n}\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:AnimCurveClipboardApi.example",
      "symbol": "AnimCurveClipboardApi.example",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A.\n// 3. Action — render the example text.\nconst s = ctl.getCurve('rotateX').clipboard.example();\n// Observe.\nconsole.log('example length: ' + s.length);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveClipboardApi.help",
      "symbol": "AnimCurveClipboardApi.help",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A.\n// 3. Action — render the help text.\nconst s = ctl.getCurve('rotateX').clipboard.help();\n// Observe.\nconsole.log('help length: ' + s.length);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveClipboardApi.paste",
      "symbol": "AnimCurveClipboardApi.paste",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 5.0);\n// 2. Copy then paste at a new time.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.clipboard.copy();\n    await curve.clipboard.paste({ time: 24 }); // frame 24\n    console.log('paste invoked');\n}\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:AnimCurveKey",
      "symbol": "AnimCurveKey",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 5.0);\n// 2. Select — N/A for direct curve inspection.\n// 3. Action — read the first keyframe descriptor.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    const key = curve.keys()[0];\n    // Observe.\n    console.log('time: ' + key.time.toFixed(3));\n    console.log('value: ' + key.value.toFixed(3));\n    console.log('has tangentIn: ' + (key.tangentIn !== undefined));\n    console.log('has tangentOut: ' + (key.tangentOut !== undefined));\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveKey.tangentIn",
      "symbol": "AnimCurveKey.tangentIn",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 5.0);\n// 2. Select — N/A for direct curve inspection.\n// 3. Action — inspect the second key's incoming tangent.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    const tIn = curve.keys()[1].tangentIn;\n    // Observe.\n    console.log('incoming tangent type: ' + tIn?.type);\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveKey.tangentOut",
      "symbol": "AnimCurveKey.tangentOut",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 5.0);\n// 2. Select — N/A for direct curve inspection.\n// 3. Action — inspect the first key's outgoing tangent.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    const tOut = curve.keys()[0].tangentOut;\n    // Observe.\n    console.log('outgoing tangent type: ' + tOut?.type);\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveKey.time",
      "symbol": "AnimCurveKey.time",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 1, 5.0);\n// 2. Select — N/A for direct curve inspection.\n// 3. Action — read back the time the keyframe was recorded at.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    const t = curve.keys()[0].time;\n    // Observe.\n    console.log('key time (frames): ' + t.toFixed(3));\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveKey.value",
      "symbol": "AnimCurveKey.value",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 5.0);\n// 2. Select — N/A for direct curve inspection.\n// 3. Action — read back the scalar value at the keyframe.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    const v = curve.keys()[0].value;\n    // Observe.\n    console.log('key value: ' + v.toFixed(3));\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveKeysApi",
      "symbol": "AnimCurveKeysApi",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A; we have the handle.\n// 3. Action — count keys.\nconst n = ctl.getCurve('rotateX').keys.count;\n// Observe.\nconsole.log('key count: ' + n);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveKeysApi.count",
      "symbol": "AnimCurveKeysApi.count",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A.\n// 3. Action — read the count.\nconst n = ctl.getCurve('rotateX').keys.count;\n// Observe.\nconsole.log('count: ' + n);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveKeysApi.delete",
      "symbol": "AnimCurveKeysApi.delete",
      "intent": "scene",
      "code": "// Animation editor, rigged asset. Everything by NAME — never ids.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0); // seconds — creates the curve\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.keys.set(12, 45);\n    await curve.keys.delete(12);\n    console.log('keys after delete: ' + curve.keys.count);\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveKeysApi.example",
      "symbol": "AnimCurveKeysApi.example",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A.\n// 3. Action — render the example text.\nconst s = ctl.getCurve('rotateX').keys.example();\n// Observe.\nconsole.log('example length: ' + s.length);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveKeysApi.help",
      "symbol": "AnimCurveKeysApi.help",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A.\n// 3. Action — render the help text.\nconst s = ctl.getCurve('rotateX').keys.help();\n// Observe.\nconsole.log('help length: ' + s.length);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveKeysApi.set",
      "symbol": "AnimCurveKeysApi.set",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — select a control and key it.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nawait select(ctl);\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.keys.set(12, 5.0, { tangentType: 'auto' });\n    console.log('key count after set: ' + curve.keys.count);\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangent",
      "symbol": "AnimCurveTangent",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 5.0);\n// 2. Select — N/A for direct curve inspection.\n// 3. Action — read the tangent descriptor on the first key's outgoing side.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    const tangentOut = curve.keys()[0].tangentOut;\n    // Observe.\n    console.log('tangent type: ' + tangentOut?.type);\n    console.log('tangent weight: ' + tangentOut?.weight.toFixed(3));\n    console.log('tangent slope: ' + tangentOut?.slope.toFixed(3));\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangent.slope",
      "symbol": "AnimCurveTangent.slope",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 5.0);\n// 2. Select — N/A for direct curve inspection.\n// 3. Action — read the tangent slope on the first key's outgoing side.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    const slope = curve.keys()[0].tangentOut?.slope ?? 0;\n    // Observe.\n    console.log('outgoing tangent slope: ' + slope.toFixed(3));\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangent.type",
      "symbol": "AnimCurveTangent.type",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 5.0);\n// 2. Select — N/A for direct curve inspection.\n// 3. Action — read the tangent type on the first key's outgoing side.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    const type = curve.keys()[0].tangentOut?.type;\n    // Observe.\n    console.log('outgoing tangent type: ' + type);\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangent.weight",
      "symbol": "AnimCurveTangent.weight",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 1, 5.0);\n// 2. Select — N/A for direct curve inspection.\n// 3. Action — read the tangent weight on the first key's outgoing side.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    const weight = curve.keys()[0].tangentOut?.weight ?? 0;\n    // Observe.\n    console.log('outgoing tangent weight: ' + weight.toFixed(3));\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangentsApi",
      "symbol": "AnimCurveTangentsApi",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A.\n// 3. Action — reach the tangents sub-namespace.\nconst t = ctl.getCurve('rotateX').tangents;\n// Observe.\nconsole.log('tangents ready: ' + (t !== null));"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangentsApi.example",
      "symbol": "AnimCurveTangentsApi.example",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A.\n// 3. Action — render the example text.\nconst s = ctl.getCurve('rotateX').tangents.example();\n// Observe.\nconsole.log('example length: ' + s.length);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangentsApi.help",
      "symbol": "AnimCurveTangentsApi.help",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\n// 2. Select — N/A.\n// 3. Action — render the help text.\nconst s = ctl.getCurve('rotateX').tangents.help();\n// Observe.\nconsole.log('help length: ' + s.length);"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangentsApi.lockWeight",
      "symbol": "AnimCurveTangentsApi.lockWeight",
      "intent": "scene",
      "code": "// Animation editor, rigged asset loaded — key, then lock one handle.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0);\nctl.setKey('rotateX', 0.4, 45); // seconds — 0.4s × 30fps = frame 12\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.tangents.lockWeight(12, true);\n    console.log('weight locked at frame 12');\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangentsApi.setAuto",
      "symbol": "AnimCurveTangentsApi.setAuto",
      "intent": "scene",
      "code": "// Animation editor, with a rigged asset loaded (e.g. an FBX + biped rig).\n// 1. Key one control channel at two frames — everything by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0);\nctl.setKey('rotateX', 0.4, 45); // seconds — 0.4s × 30fps = frame 12\n// 2. Retag those keys' tangents by frame time.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.tangents.setAuto([0, 12]);\n    console.log('auto applied to 2 keys');\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangentsApi.setFlat",
      "symbol": "AnimCurveTangentsApi.setFlat",
      "intent": "scene",
      "code": "// Animation editor, with a rigged asset loaded (e.g. an FBX + biped rig).\n// 1. Key one control channel at two frames — everything by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0);\nctl.setKey('rotateX', 0.4, 45); // seconds — 0.4s × 30fps = frame 12\n// 2. Retag those keys' tangents by frame time.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.tangents.setFlat([0, 12]);\n    console.log('flat applied to 2 keys');\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangentsApi.setInbetweens",
      "symbol": "AnimCurveTangentsApi.setInbetweens",
      "intent": "scene",
      "code": "// Animation editor, rigged asset — key a control channel by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0.0);\nctl.setKey('rotateX', 2, 5.0);\n// 2. Insert 3 in-between keys.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.tangents.setInbetweens({ count: 3 });\n    console.log('key count after inbetweens: ' + curve.keys.count);\n}\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangentsApi.setLinear",
      "symbol": "AnimCurveTangentsApi.setLinear",
      "intent": "scene",
      "code": "// Animation editor, with a rigged asset loaded (e.g. an FBX + biped rig).\n// 1. Key one control channel at two frames — everything by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0);\nctl.setKey('rotateX', 0.4, 45); // seconds — 0.4s × 30fps = frame 12\n// 2. Retag those keys' tangents by frame time.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.tangents.setLinear([0, 12]);\n    console.log('linear applied to 2 keys');\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangentsApi.setPlateau",
      "symbol": "AnimCurveTangentsApi.setPlateau",
      "intent": "scene",
      "code": "// Animation editor, with a rigged asset loaded (e.g. an FBX + biped rig).\n// 1. Key one control channel at two frames — everything by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0);\nctl.setKey('rotateX', 0.4, 45); // seconds — 0.4s × 30fps = frame 12\n// 2. Retag those keys' tangents by frame time.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.tangents.setPlateau([0, 12]);\n    console.log('plateau applied to 2 keys');\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimCurveTangentsApi.setStep",
      "symbol": "AnimCurveTangentsApi.setStep",
      "intent": "scene",
      "code": "// Animation editor, with a rigged asset loaded (e.g. an FBX + biped rig).\n// 1. Key one control channel at two frames — everything by NAME.\nconst ctl = ls('ctrl_spine_01')[0]; // ← one of your rig's control names\nctl.setKey('rotateX', 0, 0);\nctl.setKey('rotateX', 0.4, 45); // seconds — 0.4s × 30fps = frame 12\n// 2. Retag those keys' tangents by frame time.\nconst curve = ctl.getCurve('rotateX');\nif (curve !== null) {\n    await curve.tangents.setStep([0, 12]);\n    console.log('step applied to 2 keys');\n}"
    },
    {
      "kind": "example",
      "id": "example:AnimEditorApi",
      "symbol": "AnimEditorApi",
      "intent": "scene",
      "code": "// 1) Toggle auto-key on, then back off.\nawait toggleAutoKey();\nawait toggleAutoKey();\nconsole.log('auto-key cycle complete');"
    },
    {
      "kind": "example",
      "id": "example:AnimEditorApi.bakeAnimationToSkeleton",
      "symbol": "AnimEditorApi.bakeAnimationToSkeleton",
      "intent": "scene",
      "code": "// 1. Setup: a control-rig clip (optionally with animation layers) is active.\nconsole.log('setup: layered controlRig clip active');\n// 2. Select: the ACTIVE clip is the bake source (or pass { clip: 'Name' }).\nconst opts = { headless: true, frameStep: 1 };\n// 3. Action: bake the composited pose (all layers + space switches) to a skeleton clip.\nawait bakeAnimationToSkeleton(opts);\n// 4. Observe: a fresh '<name> (baked)' skeleton clip is appended beside the source.\nconsole.log('bake complete');"
    },
    {
      "kind": "example",
      "id": "example:AnimEditorApi.bakeControlSpace",
      "symbol": "AnimEditorApi.bakeControlSpace",
      "intent": "scene",
      "code": "// 1. Setup: a rigged character with an animated, space-switchable control is loaded.\nconsole.log('setup: rig + animated clip ready');\n// 2. Select: choose the control by NAME (names, never ids).\nconst controls = ['ctl_hand_L'];\n// 3. Action: bake it into world space — one undoable step.\nconst res = await bakeControlSpace({ controls, targetSpace: 'World' });\n// 4. Observe: the summary lists baked controls, frames, and structured warnings.\nconsole.log('baked ' + (res ? res.frames.length : 0) + ' frames');"
    },
    {
      "kind": "example",
      "id": "example:AnimEditorApi.bakeScene",
      "symbol": "AnimEditorApi.bakeScene",
      "intent": "scene",
      "code": "// 1) Bake every curve in the scene.\nawait bakeScene();\nconsole.log('bakeScene complete');"
    },
    {
      "kind": "example",
      "id": "example:AnimEditorApi.copyPose",
      "symbol": "AnimEditorApi.copyPose",
      "intent": "scene",
      "code": "// 1) Copy the current pose.\nawait copyPose();\nconsole.log('pose copied');"
    },
    {
      "kind": "example",
      "id": "example:AnimEditorApi.io",
      "symbol": "AnimEditorApi.io",
      "intent": "scene",
      "code": "// 1) Read the current asset path.\nconsole.log('asset path is a string: ' + (typeof io.path === 'string'));"
    },
    {
      "kind": "example",
      "id": "example:AnimEditorApi.pastePose",
      "symbol": "AnimEditorApi.pastePose",
      "intent": "scene",
      "code": "// 1) Paste the most recently copied pose onto the current frame.\nawait pastePose();\nconsole.log('pose pasted');"
    },
    {
      "kind": "example",
      "id": "example:AnimEditorApi.pose",
      "symbol": "AnimEditorApi.pose",
      "intent": "scene",
      "code": "// 1) Open the pose picker popup.\nawait pose();\nconsole.log('pose popup opened');"
    },
    {
      "kind": "example",
      "id": "example:AnimEditorApi.sculptKeys",
      "symbol": "AnimEditorApi.sculptKeys",
      "intent": "scene",
      "code": "// 1) Open the sculpt-keys brush.\nawait sculptKeys();\nconsole.log('sculptKeys opened');"
    },
    {
      "kind": "example",
      "id": "example:AnimEditorApi.setKeyAll",
      "symbol": "AnimEditorApi.setKeyAll",
      "intent": "scene",
      "code": "// 1) Key every transform channel of the current selection.\nawait setKeyAll();\nconsole.log('setKeyAll complete');"
    },
    {
      "kind": "example",
      "id": "example:AnimEditorApi.toggleAutoKey",
      "symbol": "AnimEditorApi.toggleAutoKey",
      "intent": "scene",
      "code": "// 1) Cycle auto-key on and off.\nawait toggleAutoKey();\nawait toggleAutoKey();\nconsole.log('auto-key cycle complete');"
    },
    {
      "kind": "example",
      "id": "example:AnimEditorApi.viewport",
      "symbol": "AnimEditorApi.viewport",
      "intent": "scene",
      "code": "// 1) Frame the scene around the rigged asset.\nawait Utils.wait.frame();\nviewport.zoomToFit(null, 2.5);"
    },
    {
      "kind": "example",
      "id": "example:Attribute",
      "symbol": "Attribute",
      "intent": "scene",
      "code": "// 1. Setup — create a node and reach for an attribute.\nconst cube = await create.cube({ name: 'attribute-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Select — N/A; we have the handle.\n// 3. Action — read the path string.\nconst path = cube.translate.x.path;\n// 4. Observe + cleanup.\nconsole.log('attr path: ' + path);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Attribute.connectFrom",
      "symbol": "Attribute.connectFrom",
      "intent": "scene",
      "code": "// 1. Spawn two cubes with custom driver / driven channels.\nconst driver = await create.cube({ name: 'Driver' });\nawait select(null, { mode: 'clear' });\nconst driven = await create.cube({ name: 'Driven' });\ndriver.addAttr({ ln: 'amount', at: 'float', dv: 1, k: true });\ndriven.addAttr({ ln: 'follows', at: 'float', dv: 0, k: true });\n// 2. Wire driver.amount -> driven.follows.\ndriven.attr('follows').connectFrom(driver.attr('amount'));\n// 3. Change the driver and watch the destination follow.\ndriver.attr('amount').set(5);\nconsole.log('driven.follows after connect: ' + driven.attr('follows').get());\n// 4. Force-replace with a different driver in one undoable step.\nconst driver2 = await create.cube({ name: 'Driver2' });\ndriver2.addAttr({ ln: 'amount', at: 'float', dv: 9, k: true });\ndriven.attr('follows').connectFrom(driver2.attr('amount'), { force: true });\nconsole.log('driven.follows after force-replace: ' + driven.attr('follows').get());\n// 5. Clean up.\nawait driver.delete();\nawait driven.delete();\nawait driver2.delete();"
    },
    {
      "kind": "example",
      "id": "example:Attribute.enableLimits",
      "symbol": "Attribute.enableLimits",
      "intent": "scene",
      "code": "// 1. Spawn a cube + reach for its X-translate AXIS HANDLE, then configure\n//    a range WITHOUT enforcing it yet. Use cube.translate.x (the\n//    Vec3AxisAttribute) — there is no dotted 'translate.x' plug.\nconst cube = await create.cube({ name: 'attr-enablelimits-demo' });\nconst tx = cube.translate.x;\ntx.setLimits(0, 5);\n// 2. With enforcement off (the default), writes outside the range still succeed.\ntx.enableLimits(false);\ntx.set(10);\nconsole.log('with enforcement off, set succeeds: ' + tx.get());\n// 3. Switch enforcement on; out-of-range writes now throw.\ntx.enableLimits(true);\nlet rejected = false;\ntry {\n    tx.set(10);\n}\ncatch {\n    rejected = true;\n}\nconsole.log('with enforcement on, rejected: ' + rejected);\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Attribute.get",
      "symbol": "Attribute.get",
      "intent": "scene",
      "code": "// 1. Spawn a cube and place it off the origin.\nconst cube = await create.cube({ name: 'attr-get-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Read the translate compound back through .attr.\nconst tAttr = cube.attr('translate');\nconst t = tAttr.get();\nconsole.log('translate: ' + JSON.stringify(t));\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Attribute.getAsAngle",
      "symbol": "Attribute.getAsAngle",
      "intent": "scene",
      "code": "// 1. Spawn a cube + add a custom angle-valued channel (stored in radians,\n//    the engine-canonical angle unit). getAsAngle works on 'angle' OR\n//    'float' channels; the built-in 'rotate' is a float3 COMPOUND (no\n//    dotted 'rotate.x' plug), so a custom float channel is the runnable\n//    single-axis demo here.\nconst cube = await create.cube({ name: 'attr-getasangle-demo' });\ncube.addAttr({ ln: 'twist', at: 'float', dv: 0, k: true });\ncube.attr('twist').set(Math.PI / 2); // 90° expressed in radians\nconst twist = cube.attr('twist');\n// 2. Read the channel in degrees (the default unit).\nconst deg = twist.getAsAngle();\nconsole.log('twist in degrees: ' + deg); // ~90\n// 3. Read the same channel in radians via the units option.\nconst rad = twist.getAsAngle({ units: 'radians' });\nconsole.log('twist in radians: ' + rad.toFixed(4)); // ~1.5708\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Attribute.getAsString",
      "symbol": "Attribute.getAsString",
      "intent": "scene",
      "code": "// 1. Spawn a cube + add a custom float channel.\nconst cube = await create.cube({ name: 'attr-getasstring-demo' });\ncube.addAttr({ ln: 'wobble', at: 'float', dv: 0.5, k: true });\ncube.attr('wobble').set(0.75);\n// 2. Read as formatted string (floats use 3 decimals).\nconsole.log('wobble: ' + cube.attr('wobble').getAsString());\n// 3. Strings pass through verbatim.\ncube.addAttr({ ln: 'state', at: 'string', dv: 'idle' });\nconsole.log('state: ' + cube.attr('state').getAsString());\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Attribute.getLimits",
      "symbol": "Attribute.getLimits",
      "intent": "scene",
      "code": "// 1. Spawn a cube + reach for its X-translate AXIS HANDLE, then read the\n//    default (no limits configured) shape. Use cube.translate.x (the\n//    Vec3AxisAttribute) — there is no dotted 'translate.x' plug.\nconst cube = await create.cube({ name: 'attr-getlimits-demo' });\nconst tx = cube.translate.x;\nconsole.log('default getLimits: ' + JSON.stringify(tx.getLimits())); // null\n// 2. Configure a range + enable it, then read back { min, max, enabled }.\ntx.setLimits(-3, 3);\ntx.enableLimits(true);\nconsole.log('after configure: ' + JSON.stringify(tx.getLimits()));\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Attribute.isKeyable",
      "symbol": "Attribute.isKeyable",
      "intent": "scene",
      "code": "// 1. Spawn a cube + read the default keyable state.\nconst cube = await create.cube({ name: 'attr-iskeyable-demo' });\nconst scale = cube.attr('scale');\nconsole.log('default isKeyable: ' + scale.isKeyable());\n// 2. Hide it, read again.\nscale.setKeyable(false);\nconsole.log('after setKeyable(false): ' + scale.isKeyable());\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Attribute.isLocked",
      "symbol": "Attribute.isLocked",
      "intent": "scene",
      "code": "// 1. Spawn a cube + reach for its X-translate AXIS HANDLE.\n//    Use cube.translate.x (the Vec3AxisAttribute) — there is no dotted\n//    'translate.x' plug, so cube.attr('translate.x') would throw.\nconst cube = await create.cube({ name: 'attr-islocked-demo' });\nconst tx = cube.translate.x;\nconsole.log('default isLocked: ' + tx.isLocked()); // false by default\n// 2. Lock it, read again.\ntx.lock(true);\nconsole.log('after lock(true): ' + tx.isLocked());\n// 3. Unlock, read again.\ntx.lock(false);\nconsole.log('after lock(false): ' + tx.isLocked());\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Attribute.lock",
      "symbol": "Attribute.lock",
      "intent": "scene",
      "code": "// 1. Spawn a cube + reach for its X-translate AXIS HANDLE.\n//    The per-axis handle (cube.translate.x) is the Vec3AxisAttribute\n//    that exposes .lock()/.set()/.get() — there is NO dotted\n//    'translate.x' plug to pass to cube.attr(...).\nconst cube = await create.cube({ name: 'attr-lock-demo' });\nconst tx = cube.translate.x;\ntx.lock(true); // lock just this axis (omitting the arg also locks)\n// 2. Verify the lock takes effect — the write throws.\nlet rejected = false;\ntry {\n    tx.set(5);\n}\ncatch {\n    rejected = true;\n}\nconsole.log('locked write rejected: ' + rejected);\n// 3. Unlock + write succeeds.\ntx.lock(false); // pass false to UNlock\ntx.set(2.5);\nconsole.log('translate.x after unlock: ' + tx.get());\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Attribute.nodeId",
      "symbol": "Attribute.nodeId",
      "intent": "scene",
      "code": "// 1. Setup — create a node and reach for an attribute.\nconst cube = await create.cube({ name: 'attr-nodeId-demo' });\n// 2. Select — N/A.\n// 3. Action — read the bound node id.\nconst id = cube.translate.x.nodeId;\n// 4. Observe + cleanup.\nconsole.log('attr.nodeId matches: ' + (id === cube.nodeId));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Attribute.path",
      "symbol": "Attribute.path",
      "intent": "scene",
      "code": "// 1. Setup — create a node and reach for an attribute.\nconst cube = await create.cube({ name: 'attr-path-demo' });\n// 2. Select — N/A.\n// 3. Action — read the channel path.\nconst p = cube.translate.x.path;\n// 4. Observe + cleanup.\nconsole.log('attr path: ' + p);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Attribute.set",
      "symbol": "Attribute.set",
      "intent": "scene",
      "code": "// 1. Spawn a cube + read its translate channel.\nconst cube = await create.cube({ name: 'attr-set-demo' });\nconst tx = cube.attr('translate.x');\n// 2. Write a new value through the typed Attribute handle.\ntx.set(2.5);\n// 3. Log the readback so the harness can assert the write landed.\nconsole.log('translate.x after set: ' + tx.get());\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Attribute.setKeyable",
      "symbol": "Attribute.setKeyable",
      "intent": "scene",
      "code": "// 1. Spawn a cube + hide its scale channel from the channel box.\nconst cube = await create.cube({ name: 'attr-setkeyable-demo' });\ncube.attr('scale').setKeyable(false);\n// 2. Hidden channels drop out of the keyable list.\nconsole.log('keyable channels: ' + JSON.stringify(cube.attrs({ keyable: true })));\n// 3. Reads + writes still work on the hidden channel.\nconst s = cube.attr('scale').get();\nconsole.log('scale still readable: ' + JSON.stringify(s));\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Attribute.setLimits",
      "symbol": "Attribute.setLimits",
      "intent": "scene",
      "code": "// 1. Spawn a cube + reach for its X-translate AXIS HANDLE, then clamp\n//    that axis to [0, 5]. Use cube.translate.x (the Vec3AxisAttribute) —\n//    there is no dotted 'translate.x' plug for cube.attr(...) to find.\nconst cube = await create.cube({ name: 'attr-setlimits-demo' });\nconst tx = cube.translate.x;\ntx.setLimits(0, 5); // min, max — both finite, min <= max\ntx.enableLimits(true); // limits are STORED by setLimits but only ENFORCED once enabled\n// 2. An in-range write succeeds.\ntx.set(2.5);\nconsole.log('in-range write ok: ' + tx.get());\n// 3. An out-of-range write throws.\nlet rejected = false;\ntry {\n    tx.set(10);\n}\ncatch {\n    rejected = true;\n}\nconsole.log('out-of-range rejected: ' + rejected);\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.batch",
      "symbol": "BaseHistoryApi.batch",
      "intent": "scene",
      "code": "// 1. Build several materials inside a single labelled batch.\nawait history.batch('three materials', async () => {\n    // Every create inside the callback collapses into ONE undo entry.\n    await create.material({});\n    // Second material — still the same batch.\n    await create.material({});\n    // Third material — still one undo entry.\n    await create.material({});\n});\n// 2. The whole batch seals into ONE undo entry, not three. Inside a\n//    Script-Editor run that entry nests into the RUN's own atomic\n//    group, so the stack itself still reports the pre-run state here.\nconsole.log('pre-run stack size = ' + history.size());\n// 3. Observe — one UI undo (Ctrl+Z) after the run completes reverts the\n//    whole run; the batch label matters most for code that runs OUTSIDE\n//    a Script-Editor run group (recipes / tooling).\nconsole.log('canUndo (pre-run stack) = ' + history.canUndo());\n// 4. Rollback bonus: if the callback throws, the batch's partial work\n//    is rolled back automatically and the error re-propagates."
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.beginGroup",
      "symbol": "BaseHistoryApi.beginGroup",
      "intent": "scene",
      "code": "// 1. Open a manual group.\nawait history.beginGroup('manual group');\n// First material inside the open group.\nawait create.material({});\n// Second material — also inside the group.\nawait create.material({});\n// 2. Close the group so the two creates seal into one entry. Inside a\n//    Script-Editor run that entry nests into the run's own atomic group.\nawait history.endGroup();\n// 3. Introspection mid-run reports the PRE-run stack (the sealed group\n//    is buffered until the run completes).\nconsole.log('pre-run stack size = ' + history.size());\n// 4. After the run completes, ONE UI undo (Ctrl+Z) reverts everything.\nconsole.log('canUndo (pre-run stack) = ' + history.canUndo());"
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.canRedo",
      "symbol": "BaseHistoryApi.canRedo",
      "intent": "scene",
      "code": "// 1. A fresh, untouched stack has nothing to redo.\nawait history.clear();\nconsole.log('canRedo on clear = ' + history.canRedo());\n// 2. Work done in THIS run is buffered in the run's atomic group, so\n//    nothing here can flip canRedo by itself.\nawait create.material({});\n// 3. Observe — still reports the pre-run redo stack.\nconsole.log('canRedo mid-run = ' + history.canRedo());\n// 4. Undo a COMPLETED run (UI Ctrl+Z, or history.undo() in a later\n//    run) and canRedo becomes true until the entry is redone."
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.canUndo",
      "symbol": "BaseHistoryApi.canUndo",
      "intent": "scene",
      "code": "// 1. Clear first so the stack is known-empty.\nawait history.clear();\nconsole.log('canUndo on empty = ' + history.canUndo());\n// 2. Creating a material buffers into the run's atomic group — the stack\n//    gains this run's single entry only when the run COMPLETES, so\n//    mid-run canUndo still reports the pre-run stack.\nawait create.material({});\n// 3. Observe — both readers agree on the pre-run stack.\nconsole.log('canUndo mid-run = ' + history.canUndo());\nconsole.log('size (pre-run stack) = ' + history.size());\n// 4. After this run completes it is ONE entry — a later run (or UI\n//    Ctrl+Z) sees canUndo === true."
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.clear",
      "symbol": "BaseHistoryApi.clear",
      "intent": "scene",
      "code": "// 1. Create a couple of materials (buffered into this run's atomic group).\nawait create.material({});\nawait create.material({});\n// 2. clear() wipes the COMPLETED undo + redo stacks immediately; the\n//    current run's buffered work is unaffected and still lands as one\n//    entry when the run completes.\nconsole.log('size before clear = ' + history.size());\nawait history.clear();\n// 3. Observe — the completed-entries count drops to zero.\nconsole.log('size after clear = ' + history.size());\n// 4. Nothing left from prior runs to undo or redo.\nconsole.log('canUndo = ' + history.canUndo());\nconsole.log('canRedo = ' + history.canRedo());"
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.endGroup",
      "symbol": "BaseHistoryApi.endGroup",
      "intent": "scene",
      "code": "// 1. Open a group and perform some grouped work.\nawait history.beginGroup('seal demo');\nawait create.material({});\n// 2. endGroup seals the group — its commands now share one undo entry\n//    (nested into the run's own atomic group inside a Script-Editor run).\nawait history.endGroup();\n// 3. Observe — mid-run introspection still reports the PRE-run stack.\nconsole.log('pre-run stack size = ' + history.size());\n// 4. Every beginGroup must pair with exactly one endGroup — leave the\n//    runner's own run-level group alone.\nconsole.log('canUndo (pre-run stack) = ' + history.canUndo());"
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.example",
      "symbol": "BaseHistoryApi.example",
      "intent": "scene",
      "code": "// 1. Fetch the example snippet for the history namespace.\nconsole.log(history.example());\n// 2. example() returns a string you can measure.\nconst snippet = history.example();\nconsole.log('example chars = ' + snippet.length);\n// 3. Confirm the snippet references the history surface.\nconsole.log('mentions history = ' + snippet.includes('history'));\n// 4. Pair it with help() for the prose overview.\nconsole.log('has help body = ' + (history.help().length > 0));"
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.help",
      "symbol": "BaseHistoryApi.help",
      "intent": "scene",
      "code": "// 1. Show the overview help for the whole history namespace.\nconsole.log(history.help());\n// 2. help() returns a string, so you can inspect its length.\nconst text = history.help();\nconsole.log('help chars = ' + text.length);\n// 3. Check that a known method name is mentioned in the help.\nconsole.log('mentions undo = ' + text.includes('undo'));\n// 4. Pair it with example() to see runnable snippets too.\nconsole.log('has example body = ' + (history.example().length > 0));"
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.lastCommand",
      "symbol": "BaseHistoryApi.lastCommand",
      "intent": "scene",
      "code": "// 1. A cleared stack reports null.\nawait history.clear();\nconsole.log('last on empty = ' + JSON.stringify(history.lastCommand()));\n// 2. Mid-run creates are buffered in the run's atomic group, so the\n//    last COMPLETED entry is still null here.\nawait create.material({});\nconsole.log('last mid-run = ' + JSON.stringify(history.lastCommand()));\n// 3. Read the label defensively off the returned object.\nconst last = history.lastCommand();\nconsole.log('label = ' + (last ? last.label : 'none'));\n// 4. In a LATER run, lastCommand() reports this run's single grouped\n//    entry (labelled with the script tab's name)."
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.list",
      "symbol": "BaseHistoryApi.list",
      "intent": "scene",
      "code": "// 1. Start from a clean stack so the list is deterministic.\nawait history.clear();\nconsole.log('empty list = ' + JSON.stringify(history.list()));\n// 2. Mid-run work is buffered in the run's atomic group — list() keeps\n//    reporting COMPLETED entries only (still empty here).\nawait create.material({});\nconsole.log('labels mid-run = ' + JSON.stringify(history.list().map((e) => e.label)));\n// 3. The list length always matches the reported stack size.\nconsole.log('count matches size = ' + (history.list().length === history.size()));\n// 4. In a LATER run this run shows up as ONE entry (the whole script\n//    run is a single atomic undo group)."
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.redo",
      "symbol": "BaseHistoryApi.redo",
      "intent": "scene",
      "code": "// 1. Setup — redo() pairs with undo() across COMPLETED runs; mid-run it\n//    throws because the run's atomic group is still open.\n// 2. Action — attempt the in-run redo; same verbose error class as undo.\ntry {\n    await history.redo();\n}\ncatch (err) {\n    console.log('in-run redo blocked: ' + err.name);\n}\n// 3. Observe — the redo stack is still introspectable mid-run.\nconsole.log('canRedo = ' + history.canRedo());\n// 4. Use UI Ctrl+Shift+Z (or redo() in a later run) to replay undone work."
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.size",
      "symbol": "BaseHistoryApi.size",
      "intent": "scene",
      "code": "// 1. Clear first so the count starts at zero.\nawait history.clear();\nconsole.log('size = ' + history.size());\n// 2. size() counts COMPLETED entries — this run's creates are buffered\n//    in the run's atomic group, so the count stays at the pre-run value.\nawait create.material({});\n// 3. Observe — the mid-run count still reflects the pre-run stack.\nconsole.log('size mid-run = ' + history.size());\n// 4. Each COMPLETED Script-Editor run adds exactly ONE entry, however\n//    many commands it ran. Pair with list() for the labels.\nconsole.log('count matches list = ' + (history.size() === history.list().length));"
    },
    {
      "kind": "example",
      "id": "example:BaseHistoryApi.undo",
      "symbol": "BaseHistoryApi.undo",
      "intent": "scene",
      "code": "// 1. Setup — each Script-Editor run is ONE atomic undo group, so undo()\n//    called INSIDE a run throws instead of silently doing nothing.\nawait create.material({});\n// 2. Action — attempt the in-run undo; the throw names the open group,\n//    its buffered size, and the remediation.\ntry {\n    await history.undo();\n}\ncatch (err) {\n    console.log('in-run undo blocked: ' + err.name);\n}\n// 3. Observe — introspection stays available mid-run (PRE-run stack).\nconsole.log('canUndo (pre-run stack) = ' + history.canUndo());\n// 4. After this run completes it becomes one undo entry — undo it with\n//    UI Ctrl+Z or `await history.undo()` in a LATER run."
    },
    {
      "kind": "example",
      "id": "example:BaseInspectApi",
      "symbol": "BaseInspectApi",
      "intent": "scene",
      "code": "// 1. Measure between two meshes of the loaded asset (the Previewer views\n//    imported assets — no geometry authoring needed).\nawait Utils.wait.frame();\nconst meshes = ls({ type: 'mesh' });\n// 2. Signed Z distance between the first two mesh centroids (falls back to\n//    a world-point pair when fewer than two meshes are loaded).\nconst d = meshes.length >= 2\n    ? inspect.measure(meshes[0], meshes[1], 'z')\n    : inspect.measure([0, 0, 0], [0, 0, 2], 'z');\n// 3. Read the canonical-meter distance + its display-unit conversion.\nconsole.log('signedDistance (m) = ' + d.signedDistance);\nconsole.log('display = ' + d.signedDistanceDisplay + ' ' + d.displayUnit);"
    },
    {
      "kind": "example",
      "id": "example:BaseInspectApi.diff",
      "symbol": "BaseInspectApi.diff",
      "intent": "scene",
      "code": "// 1. Snapshot a loaded mesh's positions (the Previewer views imported\n//    assets; we diff against an unchanged baseline rather than edit geometry).\nawait Utils.wait.frame();\nconst mesh = ls({ type: 'mesh' })[0];\nconst before = mesh ? mesh.verts.positions.slice() : new Float32Array();\n// 2. Diff the mesh against its own snapshot — an unedited mesh reports no\n//    topology change and a zero-movement delta.\nconst d = mesh ? inspect.diff(mesh, before) : null;\n// 3. Read back the structured summary shape.\nconsole.log('topologyChanged is boolean: ' + (d ? typeof d.topologyChanged === 'boolean' : 'no-mesh'));\n// 4. positionsDelta is present for a matching-topology diff (movedCount 0 here).\nconsole.log('positionsDelta present: ' + (d ? d.positionsDelta !== null : false));"
    },
    {
      "kind": "example",
      "id": "example:BaseInspectApi.example",
      "symbol": "BaseInspectApi.example",
      "intent": "scene",
      "code": "// 1) Print the inspect namespace examples block.\nconst text = inspect.example();\nconsole.log('inspect example is string: ' + (typeof text === 'string'));\nconsole.log('inspect example non-empty: ' + (text.length > 0));"
    },
    {
      "kind": "example",
      "id": "example:BaseInspectApi.help",
      "symbol": "BaseInspectApi.help",
      "intent": "scene",
      "code": "// 1) Print the inspect namespace help block.\nconst text = inspect.help();\nconsole.log('inspect help is string: ' + (typeof text === 'string'));\nconsole.log('inspect help non-empty: ' + (text.length > 0));"
    },
    {
      "kind": "example",
      "id": "example:BaseInspectApi.measure",
      "symbol": "BaseInspectApi.measure",
      "intent": "scene",
      "code": "// 1. Operate on the loaded asset — grab its meshes to measure between.\nawait Utils.wait.frame();\nconst meshes = ls({ type: 'mesh' });\nconsole.log('meshes available: ' + meshes.length);\n// 2. Signed Z distance between the first two mesh centroids (or two world\n//    points when the scene has fewer than two meshes).\nconst d = meshes.length >= 2\n    ? inspect.measure(meshes[0], meshes[1], 'z')\n    : inspect.measure([0, 0, 0], [0, 0, 2], 'z');\n// 3. Read back the structured result.\nconsole.log('signedDistance is number: ' + (typeof d.signedDistance === 'number'));\n// 4. Verify the display-unit conversion is reported.\nconsole.log('displayUnit is string: ' + (typeof d.displayUnit === 'string'));"
    },
    {
      "kind": "example",
      "id": "example:BaseIoApi.clearScene",
      "symbol": "BaseIoApi.clearScene",
      "intent": "scene",
      "code": "// 1. Setup — operate on whatever asset is loaded; record the count.\nawait Utils.wait.frame();\nconsole.log('node count before clear: ' + ls().length);\n// 2. Action — clear the scene, then clear again to prove idempotency.\nio.clearScene();\nio.clearScene();\n// 3. Read-back — the scene should now be empty.\nconst after = ls().length;\nconsole.log('node count after clear: ' + after);\n// 4. Verify — clearScene emptied the scene and is safe to repeat.\nconsole.log('clearScene emptied the scene: ' + (after === 0));"
    },
    {
      "kind": "example",
      "id": "example:BaseIoApi.example",
      "symbol": "BaseIoApi.example",
      "intent": "scene",
      "code": "// 1) Read the canonical io example back as a string.\nconst snippet = io.example();\nconsole.log('io example is a string: ' + (typeof snippet === 'string'));\nconsole.log('io example non-empty: ' + (snippet.length > 0));"
    },
    {
      "kind": "example",
      "id": "example:BaseIoApi.export",
      "symbol": "BaseIoApi.export",
      "intent": "scene",
      "code": "// 1. Setup — give the scene content so the export is non-empty.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1 });\n// 2. Action — export the SHARED scene to GLB, every option shown:\nconst result = await io.export('hero.glb', 'glb', {\n    // Reconcile choices applied BEFORE serialization (programmatic twin\n    // of the Changes Detected dialog). Defaults: skin 'transfer', uvs\n    // 'keep'. Unbaked deformers are FORCE-EXPLICIT: 'bake' | 'discard'.\n    reconcile: { skin: 'transfer', uvs: 'keep', deformers: 'bake' },\n    // Pose mode: 'bindpose' (default) | 'currentframe' | 'animation'.\n    poseMode: 'bindpose',\n    // A scene WITH clips refuses a silent bind-pose export — this flag\n    // says the clip-less export is deliberate.\n    acknowledgeNoAnimation: true,\n});\nconsole.log('exported ' + result.size + ' bytes to ' + result.filename);\n// 3. Animation form — include a clip, reframed + retimed (works from\n//    every editor; the shared cache carries the clips):\n//      await io.export('walk.glb', 'glb', {\n//        poseMode: 'animation',\n//        animationClipId: 'clip-id',  // an id from the asset's clips\n//        reframeStartFrame: 12,       // crop BEFORE retime (set both\n//        reframeEndFrame: 72,         //   ends or neither)\n//        retimeTargetFrames: 30,      // exact output frame count —\n//                                     //   OR retimeMultiplier: 2.0\n//                                     //   (2x speed); never both\n//      });\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BaseIoApi.exportFile",
      "symbol": "BaseIoApi.exportFile",
      "intent": "scene",
      "code": "// 1. Setup — export the loaded asset; gate on an empty scene (the\n//    Previewer has no geometry constructors).\nawait Utils.wait.frame();\nconst meshes = ls({ type: 'mesh' });\nif (meshes.length === 0) {\n    console.log('no asset loaded — exportFile skipped');\n}\nelse {\n    // 2. Action — export to GLB headlessly (bind pose; acknowledge any\n    //    clips so the export is not silently refused).\n    const result = await io.exportFile('hero.glb', 'glb', { acknowledgeNoAnimation: true });\n    // 3. Read-back — round-trip the just-written file back into the scene.\n    const nodes = await io.reimportLastExport();\n    // 4. Verify — a non-empty file came out and re-imported as nodes.\n    console.log('exported ' + result.size + ' bytes; round-trip loaded ' + nodes.length + ' node(s)');\n}"
    },
    {
      "kind": "example",
      "id": "example:BaseIoApi.help",
      "symbol": "BaseIoApi.help",
      "intent": "scene",
      "code": "// 1) Print the io help text to the Script Editor console.\nconst text = io.help();\nconsole.log('io help length: ' + text.length);\nconsole.log('io help mentions openScene: ' + text.includes('openScene'));"
    },
    {
      "kind": "example",
      "id": "example:BaseIoApi.newScene",
      "symbol": "BaseIoApi.newScene",
      "intent": "scene",
      "code": "// 1) Reset the editor so subsequent steps start clean.\nio.newScene();\n// 2) Verify the scene is empty.\nconst nodes = ls();\nconsole.log('scene node count after newScene: ' + nodes.length);"
    },
    {
      "kind": "example",
      "id": "example:BaseIoApi.openFile",
      "symbol": "BaseIoApi.openFile",
      "intent": "scene",
      "code": "// 1) Headless open by absolute path (any editor).\nawait io.openFile('/data/Walking.fbx');\nconst nodes = ls();\nconsole.log('node count after openFile: ' + nodes.length);\n// 2) Picker form:\n//    await io.openFile();\n// 3) From a File object (e.g., from a drag-drop callback):\n//    await io.openFile(droppedFile);"
    },
    {
      "kind": "example",
      "id": "example:BaseIoApi.openScene",
      "symbol": "BaseIoApi.openScene",
      "intent": "scene",
      "code": "// 1) Open a project file by path (headless).\nawait io.openScene('/local/projects/character.abasset');\n// 2) Verify the scene has nodes after the open completes.\nconst nodes = ls();\nconsole.log('scene node count after open: ' + nodes.length);\n// 3) Alternatively, omit the path to open the file picker:\n//    await io.openScene();"
    },
    {
      "kind": "example",
      "id": "example:BaseIoApi.reimportLastExport",
      "symbol": "BaseIoApi.reimportLastExport",
      "intent": "scene",
      "code": "// 1. Setup — there must be a previous export to re-load; gate on an\n//    empty scene (no geometry constructors in the Previewer).\nawait Utils.wait.frame();\nif (ls({ type: 'mesh' }).length === 0) {\n    console.log('no asset loaded — round-trip skipped');\n}\nelse {\n    // 2. Action — export the loaded asset, then re-import that exact file.\n    await io.exportFile('roundtrip.glb', 'glb', { acknowledgeNoAnimation: true });\n    const reloaded = await io.reimportLastExport();\n    // 3. Read-back — the reload replaces the scene; count its nodes.\n    console.log('round-trip reloaded node count: ' + reloaded.length);\n    // 4. Verify — we got a non-empty Node[] back.\n    console.log('round-trip produced nodes: ' + (reloaded.length > 0));\n}"
    },
    {
      "kind": "example",
      "id": "example:BaseIoApi.saveFile",
      "symbol": "BaseIoApi.saveFile",
      "intent": "scene",
      "code": "// 1. Setup — save whatever asset is loaded; record the dirty flag first.\nawait Utils.wait.frame();\nconsole.log('modified before save: ' + io.modified);\ntry {\n    // 2. Action — save to a native `.abasset` project file (omit the path\n    //    to open the native save-as dialog instead).\n    const result = await io.saveFile('/local/scene.abasset');\n    // 3. Read-back — saveFile resolves to {blob, filename, size}.\n    console.log('saved ' + result.size + ' bytes to ' + result.filename);\n    // 4. Verify — a real Blob came back.\n    console.log('blob is a Blob: ' + (result.blob instanceof Blob));\n}\ncatch (e) {\n    // saveFile throws when no asset is loaded — load one first.\n    console.log('saveFile needs a loaded asset: ' + String(e.message || e));\n}"
    },
    {
      "kind": "example",
      "id": "example:BaseIoApi.saveScene",
      "symbol": "BaseIoApi.saveScene",
      "intent": "scene",
      "code": "// 1) Add some content so the saved file is not empty.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1 });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Save to a specific path.\nawait io.saveScene('/local/projects/headless-save.abasset');\nconsole.log('scene saved to headless-save.abasset');\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.ambientIntensity",
      "symbol": "BaseViewportApi.ambientIntensity",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current ambient-fill intensity.\nconst original = viewport.ambientIntensity;\nconsole.log('ambientIntensity original: ' + original);\n// 2. Action — lift the dark regions with 2x ambient.\nviewport.setAmbientIntensity(2.0);\n// 3. Read-back — getter reports the new intensity.\nconsole.log('ambientIntensity after 2.0: ' + viewport.ambientIntensity);\n// 4. Verify, then restore.\nconsole.log('getter tracks setter: ' + (viewport.ambientIntensity === 2.0));\nviewport.setAmbientIntensity(original);\nconsole.log('ambientIntensity restored: ' + (viewport.ambientIntensity === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.axisScale",
      "symbol": "BaseViewportApi.axisScale",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current world-axis scale.\nconst original = viewport.axisScale;\nconsole.log('axisScale original: ' + original);\n// 2. Action — bump the world-axis gizmo to 2x.\nviewport.setAxisScale(2.0);\n// 3. Read-back — getter reports the new scale.\nconsole.log('axisScale after 2x: ' + viewport.axisScale);\n// 4. Verify, then restore.\nconsole.log('getter tracks setter: ' + (viewport.axisScale === 2.0));\nviewport.setAxisScale(original);\nconsole.log('axisScale restored: ' + (viewport.axisScale === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.backgroundColor",
      "symbol": "BaseViewportApi.backgroundColor",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current background color.\nconst original = viewport.backgroundColor;\nconsole.log('backgroundColor original: ' + original);\n// 2. Action — set a dark neutral background.\nviewport.setBackgroundColor('#1a1a1a');\n// 3. Read-back — getter reports the new color.\nconsole.log('backgroundColor after set: ' + viewport.backgroundColor);\n// 4. Verify (case-insensitive), then restore.\nconsole.log('getter tracks setter: ' + (viewport.backgroundColor.toLowerCase() === '#1a1a1a'));\nviewport.setBackgroundColor(original);\nconsole.log('backgroundColor restored: ' + (viewport.backgroundColor === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.captureScreenshot",
      "symbol": "BaseViewportApi.captureScreenshot",
      "intent": "scene",
      "code": "// 1. Setup — frame the loaded asset so the screenshot has content;\n//    wait a frame so the backbuffer reflects the latest render.\nawait Utils.wait.frame();\nviewport.frameAll(1.5);\n// 2. Action — capture the current canvas to a PNG blob.\nconst shot = await viewport.captureScreenshot();\n// 3. Read-back — inspect the returned metadata.\nconsole.log('screenshot width: ' + shot.width);\nconsole.log('screenshot height: ' + shot.height);\nconsole.log('blob bytes: ' + shot.blob.size);\n// 4. Verify — resolve the data URL and confirm it is a real PNG of\n//    non-trivial size (proves a populated framebuffer, not an empty one).\nconst url = await shot.toDataUrl();\nconsole.log('PNG data url + non-empty: ' + (url.startsWith('data:image/png') && shot.blob.size > 1600));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.center",
      "symbol": "BaseViewportApi.center",
      "intent": "scene",
      "code": "// 1. Setup — read the viewport's center + size together.\nconst c = viewport.center();\nconst d = viewport.dimensions();\nconsole.log('center: ' + JSON.stringify(c) + ' dims: ' + JSON.stringify(d));\n// 2. Action — use the center to drive a real raycast (the canonical\n//    composition: center() feeds pickAt()).\nconst hit = viewport.pickAt(c.x, c.y);\nconsole.log('center pick hit: ' + (hit !== null));\n// 3. Read-back — the center must lie inside the canvas bounds.\nconst inside = c.x >= 0 && c.y >= 0 && Number.isFinite(c.x) && Number.isFinite(c.y);\n// 4. Verify — center is finite + composes with pickAt without throwing.\nconsole.log('center is a valid pick coord: ' + inside);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.dimensions",
      "symbol": "BaseViewportApi.dimensions",
      "intent": "scene",
      "code": "// 1. Setup — read the canvas dimensions.\nconst d = viewport.dimensions();\nconsole.log('dimensions: ' + JSON.stringify(d));\n// 2. Action — derive the center from the dimensions and cross-check it\n//    against the dedicated center() accessor (they must agree on the mid).\nconst c = viewport.center();\nconsole.log('center: ' + JSON.stringify(c));\n// 3. Read-back — a mounted viewport reports strictly positive size.\nconst sized = d.width > 0 && d.height > 0;\n// 4. Verify — dimensions are positive AND consistent with center()\n//    (center x/y fall within [0, left+width] / [0, top+height]).\nconsole.log('dimensions positive: ' + sized);\nconsole.log('center within canvas span: ' + (c.x <= d.width + 4096 && c.y <= d.height + 4096));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.dolly",
      "symbol": "BaseViewportApi.dolly",
      "intent": "scene",
      "code": "// 1. Setup — frame the loaded asset so the dolly is meaningful, then\n//    snapshot the canvas size as a stable, Previewer-available signal.\nawait Utils.wait.frame();\nviewport.frameAll(1.5);\nconst before = viewport.dimensions();\nconsole.log('canvas: ' + before.width + 'x' + before.height);\n// 2. Action — dolly out 2x then back in 2x (factor>1 retreats,\n//    0<factor<1 approaches).\nviewport.dolly(2.0);\nconsole.log('dollied out 2.0');\nviewport.dolly(0.5);\nconsole.log('dollied in 0.5');\n// 3. Read-back — the canvas rect is unaffected by a camera dolly, so\n//    dimensions must be identical (a no-throw, deterministic check).\nconst after = viewport.dimensions();\n// 4. Verify — dolly is a camera move, not a layout change.\nconsole.log('dolly left canvas size intact: ' + (after.width === before.width && after.height === before.height));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.doubleSided",
      "symbol": "BaseViewportApi.doubleSided",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current double-sided flag.\nconst original = viewport.doubleSided;\nconsole.log('doubleSided original: ' + original);\n// 2. Action — flip it via the setter.\nviewport.setDoubleSided(!original);\n// 3. Read-back — getter must report the flipped value.\nconsole.log('doubleSided after flip: ' + viewport.doubleSided);\n// 4. Verify the flip, then restore.\nconsole.log('getter tracks setter: ' + (viewport.doubleSided === !original));\nviewport.setDoubleSided(original);\nconsole.log('doubleSided restored: ' + (viewport.doubleSided === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.envIntensity",
      "symbol": "BaseViewportApi.envIntensity",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current environment-light intensity.\nconst original = viewport.envIntensity;\nconsole.log('envIntensity original: ' + original);\n// 2. Action — boost image-based lighting to 2x.\nviewport.setEnvIntensity(2.0);\n// 3. Read-back — getter reports the new intensity.\nconsole.log('envIntensity after 2.0: ' + viewport.envIntensity);\n// 4. Verify, then restore.\nconsole.log('getter tracks setter: ' + (viewport.envIntensity === 2.0));\nviewport.setEnvIntensity(original);\nconsole.log('envIntensity restored: ' + (viewport.envIntensity === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.example",
      "symbol": "BaseViewportApi.example",
      "intent": "scene",
      "code": "// 1. Setup — fetch the canonical viewport example snippet.\nconst src = viewport.example();\nconsole.log('example chars: ' + src.length);\n// 2. Action — confirm it is runnable-shaped code, not a placeholder\n//    (it should reference the bare `viewport` namespace).\nconst usesViewport = src.includes('viewport.');\n// 3. Read-back — count the lines to confirm it is a real snippet.\nconst lineCount = src.split('\\n').length;\nconsole.log('example line count: ' + lineCount);\n// 4. Verify — non-trivial multi-line snippet that drives the surface.\nconsole.log('example is real snippet: ' + (usesViewport && lineCount > 2));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.exposure",
      "symbol": "BaseViewportApi.exposure",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current HDR exposure.\nconst original = viewport.exposure;\nconsole.log('exposure original: ' + original);\n// 2. Action — brighten the render to 2x exposure.\nviewport.setExposure(2.0);\n// 3. Read-back — getter reports the new exposure.\nconsole.log('exposure after 2.0: ' + viewport.exposure);\n// 4. Verify, then restore.\nconsole.log('getter tracks setter: ' + (viewport.exposure === 2.0));\nviewport.setExposure(original);\nconsole.log('exposure restored: ' + (viewport.exposure === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.frameAll",
      "symbol": "BaseViewportApi.frameAll",
      "intent": "scene",
      "code": "// 1. Setup — wait one frame for any pending asset load, then snapshot\n//    canvas dimensions as a deterministic Previewer-available signal.\nawait Utils.wait.frame();\nconst before = viewport.dimensions();\nconsole.log('canvas: ' + before.width + 'x' + before.height);\n// 2. Action — frame every loaded mesh; no-op when the scene is empty.\nviewport.frameAll(1.5);\nconsole.log('frameAll invoked');\n// 3. Read-back — re-read dimensions; framing is a camera move, not a\n//    canvas resize.\nconst after = viewport.dimensions();\n// 4. Verify — the canvas is mounted and unchanged in size.\nconsole.log('frameAll kept canvas live + sized: ' + (after.width > 0 && after.width === before.width));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.frameSelection",
      "symbol": "BaseViewportApi.frameSelection",
      "intent": "scene",
      "code": "// 1. Setup — operate on the loaded asset. Select all meshes via the\n//    universal scene query (no geometry authoring in the Previewer).\nawait Utils.wait.frame();\nconst meshes = ls({ type: 'mesh' });\nmeshes.forEach((m) => m.select());\nconst selectedBefore = ls({ selected: true }).length;\nconsole.log('selected before frame: ' + selectedBefore);\n// 2. Action — frame the current selection (F-key parity).\nviewport.frameSelection();\nconsole.log('frameSelection invoked');\n// 3. Read-back — framing the camera must not mutate the selection set.\nconst selectedAfter = ls({ selected: true }).length;\n// 4. Verify — selection count is preserved by a camera-only operation.\nconsole.log('selection preserved by frameSelection: ' + (selectedAfter === selectedBefore));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.gridUnit",
      "symbol": "BaseViewportApi.gridUnit",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current grid unit.\nconst original = viewport.gridUnit;\nconsole.log('gridUnit original: ' + original);\n// 2. Action — switch to centimeters via the setter.\nviewport.setGridUnit('cm');\n// 3. Read-back — getter reports cm AND the derived scale is 0.01.\nconsole.log('gridUnit after cm: ' + viewport.gridUnit);\nconsole.log('gridScale after cm: ' + viewport.gridScale);\n// 4. Verify the unit + scale agree, then restore.\nconsole.log('unit/scale consistent: ' + (viewport.gridUnit === 'cm' && viewport.gridScale === 0.01));\nviewport.setGridUnit(original);\nconsole.log('gridUnit restored: ' + (viewport.gridUnit === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.help",
      "symbol": "BaseViewportApi.help",
      "intent": "scene",
      "code": "// 1. Setup — render the viewport namespace help index.\nconst txt = viewport.help();\n// 2. Action — split into lines so we can inspect the signature index.\nconst lines = txt.split('\\n').filter((l) => l.trim().length > 0);\nconsole.log('help line count: ' + lines.length);\n// 3. Read-back — the index should mention a real viewport method.\nconst mentionsRenderMode = txt.includes('renderMode');\n// 4. Verify — help is a non-trivial multi-line string referencing the surface.\nconsole.log('help is multi-line + names a method: ' + (lines.length > 1 && mentionsRenderMode));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.isOrtho",
      "symbol": "BaseViewportApi.isOrtho",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current projection.\nconst original = viewport.isOrtho;\nconsole.log('isOrtho original: ' + original);\n// 2. Action — flip projection on via the setter.\nviewport.setOrtho(true);\n// 3. Read-back — getter must now report orthographic.\nconsole.log('isOrtho after setOrtho(true): ' + viewport.isOrtho);\n// 4. Verify the round-trip, then restore the original projection.\nconsole.log('getter tracks setter: ' + (viewport.isOrtho === true));\nviewport.setOrtho(original);\nconsole.log('isOrtho restored: ' + (viewport.isOrtho === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.jointColor",
      "symbol": "BaseViewportApi.jointColor",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current joint color.\nconst original = viewport.jointColor;\nconsole.log('jointColor original: ' + original);\n// 2. Action — set a recognizable orange.\nviewport.setJointColor('#ff8800');\n// 3. Read-back — getter reports the new color.\nconsole.log('jointColor after set: ' + viewport.jointColor);\n// 4. Verify (case-insensitive hex), then restore.\nconsole.log('getter tracks setter: ' + (viewport.jointColor.toLowerCase() === '#ff8800'));\nviewport.setJointColor(original);\nconsole.log('jointColor restored: ' + (viewport.jointColor === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.jointScale",
      "symbol": "BaseViewportApi.jointScale",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current joint-display scale.\nconst original = viewport.jointScale;\nconsole.log('jointScale original: ' + original);\n// 2. Action — bump the joint markers to 2x.\nviewport.setJointScale(2.0);\n// 3. Read-back — getter must report the new scale.\nconsole.log('jointScale after 2x: ' + viewport.jointScale);\n// 4. Verify, then restore.\nconsole.log('getter tracks setter: ' + (viewport.jointScale === 2.0));\nviewport.setJointScale(original);\nconsole.log('jointScale restored: ' + (viewport.jointScale === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.orbit",
      "symbol": "BaseViewportApi.orbit",
      "intent": "scene",
      "code": "// 1. Setup — frame the loaded asset so the orbit is visible; snapshot\n//    the viewport center as a stable Previewer-available reference.\nawait Utils.wait.frame();\nviewport.frameAll(1.5);\nconst c = viewport.center();\nconsole.log('center: ' + JSON.stringify(c));\n// 2. Action — orbit 45deg left + 30deg up, then reverse it.\nviewport.orbit(45, 30);\nconsole.log('orbited yaw=45 pitch=30');\nviewport.orbit(-45, -30);\nconsole.log('orbited back');\n// 3. Read-back — the canvas geometric center is camera-independent, so\n//    it must be unchanged after a round-trip orbit.\nconst c2 = viewport.center();\n// 4. Verify — orbit moved the camera, not the canvas.\nconsole.log('center stable through orbit: ' + (c2.x === c.x && c2.y === c.y));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.pan",
      "symbol": "BaseViewportApi.pan",
      "intent": "scene",
      "code": "// 1. Setup — frame the loaded asset, then snapshot canvas dimensions\n//    (a Previewer-available, deterministic reference).\nawait Utils.wait.frame();\nviewport.frameAll(1.5);\nconst before = viewport.dimensions();\nconsole.log('canvas: ' + JSON.stringify(before));\n// 2. Action — pan 80px right + 40px down, then pan back.\nviewport.pan(80, 40);\nconsole.log('panned dx=80 dy=40');\nviewport.pan(-80, -40);\nconsole.log('panned back');\n// 3. Read-back — pan moves the camera target, not the canvas layout.\nconst after = viewport.dimensions();\n// 4. Verify — canvas size is invariant under a pan round-trip.\nconsole.log('pan left canvas intact: ' + (after.width === before.width && after.height === before.height));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.pickAt",
      "symbol": "BaseViewportApi.pickAt",
      "intent": "scene",
      "code": "// 1. Setup — frame the loaded asset so a center-pick lands on geometry.\nawait Utils.wait.frame();\nviewport.frameAll(1.5);\n// 2. Action — raycast the geometric center of the canvas.\nconst center = viewport.center();\nconst hit = viewport.pickAt(center.x, center.y);\nconsole.log('center pick hit: ' + (hit !== null));\n// 3. Read-back — when something is under the cursor, the result exposes\n//    a Node handle + a world-space point (NOT a bare meshId).\nif (hit) {\n    console.log('hit node id: ' + hit.node.id);\n    console.log('hit point: ' + JSON.stringify(hit.point));\n    console.log('hit distance is finite: ' + Number.isFinite(hit.distance));\n}\n// 4. Verify — a guaranteed miss (far off-canvas) returns null, proving\n//    the raycast discriminates hits from misses.\nconst miss = viewport.pickAt(-9999, -9999);\nconsole.log('off-canvas pick is null: ' + (miss === null));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.renderMode",
      "symbol": "BaseViewportApi.renderMode",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current render mode so we can restore it.\nconst original = viewport.renderMode;\nconsole.log('renderMode original: ' + original);\n// 2. Action — drive the mode to a known value via the paired setter.\nviewport.setRenderMode('uvchecker');\n// 3. Read-back — the getter must reflect the value the setter just wrote.\nconst now = viewport.renderMode;\nconsole.log('renderMode after set: ' + now);\n// 4. Verify — getter agrees with the setter, then restore the original.\nconsole.log('getter tracks setter: ' + (now === 'uvchecker'));\nviewport.setRenderMode(original);\nconsole.log('renderMode restored: ' + (viewport.renderMode === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setAmbientIntensity",
      "symbol": "BaseViewportApi.setAmbientIntensity",
      "intent": "scene",
      "code": "// 1) Snapshot the current intensity.\nconst original = viewport.ambientIntensity;\nconsole.log('ambientIntensity original: ' + original);\n// 2) Boost the ambient fill.\nviewport.setAmbientIntensity(2.0);\nconsole.log('ambientIntensity after 2.0: ' + viewport.ambientIntensity);\n// 3) Restore.\nviewport.setAmbientIntensity(original);\nconsole.log('ambientIntensity restored: ' + viewport.ambientIntensity);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setAxisScale",
      "symbol": "BaseViewportApi.setAxisScale",
      "intent": "scene",
      "code": "// 1) Snapshot the current scale.\nconst original = viewport.axisScale;\nconsole.log('axisScale original: ' + original);\n// 2) Bump the scale up, then back.\nviewport.setAxisScale(2.0);\nconsole.log('axisScale after 2x: ' + viewport.axisScale);\n// 3) Restore.\nviewport.setAxisScale(original);\nconsole.log('axisScale restored: ' + viewport.axisScale);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setBackgroundColor",
      "symbol": "BaseViewportApi.setBackgroundColor",
      "intent": "scene",
      "code": "// 1) Snapshot the current background.\nconst original = viewport.backgroundColor;\nconsole.log('backgroundColor original: ' + original);\n// 2) Swap to a recognizable color.\nviewport.setBackgroundColor('#1a1a1a');\nconsole.log('backgroundColor after set: ' + viewport.backgroundColor);\n// 3) Restore.\nviewport.setBackgroundColor(original);\nconsole.log('backgroundColor restored: ' + viewport.backgroundColor);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setDoubleSided",
      "symbol": "BaseViewportApi.setDoubleSided",
      "intent": "scene",
      "code": "// 1) Snapshot the current setting.\nconst original = viewport.doubleSided;\nconsole.log('doubleSided original: ' + original);\n// 2) Flip the setting.\nviewport.setDoubleSided(!original);\nconsole.log('doubleSided after toggle: ' + viewport.doubleSided);\n// 3) Restore.\nviewport.setDoubleSided(original);\nconsole.log('doubleSided restored: ' + viewport.doubleSided);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setEnvIntensity",
      "symbol": "BaseViewportApi.setEnvIntensity",
      "intent": "scene",
      "code": "// 1) Snapshot the current intensity.\nconst original = viewport.envIntensity;\nconsole.log('envIntensity original: ' + original);\n// 2) Boost the intensity.\nviewport.setEnvIntensity(2.0);\nconsole.log('envIntensity after 2.0: ' + viewport.envIntensity);\n// 3) Restore.\nviewport.setEnvIntensity(original);\nconsole.log('envIntensity restored: ' + viewport.envIntensity);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setExposure",
      "symbol": "BaseViewportApi.setExposure",
      "intent": "scene",
      "code": "// 1) Snapshot the current exposure.\nconst original = viewport.exposure;\nconsole.log('exposure original: ' + original);\n// 2) Brighten then darken.\nviewport.setExposure(2.0);\nconsole.log('exposure after 2.0: ' + viewport.exposure);\nviewport.setExposure(0.5);\nconsole.log('exposure after 0.5: ' + viewport.exposure);\n// 3) Restore.\nviewport.setExposure(original);\nconsole.log('exposure restored: ' + viewport.exposure);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setGridUnit",
      "symbol": "BaseViewportApi.setGridUnit",
      "intent": "scene",
      "code": "// 1) Snapshot the current unit.\nconst original = viewport.gridUnit;\nconsole.log('gridUnit original: ' + original);\n// 2) Switch to centimeters then back.\nviewport.setGridUnit('cm');\nconsole.log('gridUnit after cm: ' + viewport.gridUnit);\nviewport.setGridUnit('m');\nconsole.log('gridUnit after m: ' + viewport.gridUnit);\n// 3) Restore.\nviewport.setGridUnit(original);\nconsole.log('gridUnit restored: ' + viewport.gridUnit);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setJointColor",
      "symbol": "BaseViewportApi.setJointColor",
      "intent": "scene",
      "code": "// 1) Snapshot the current color.\nconst original = viewport.jointColor;\nconsole.log('jointColor original: ' + original);\n// 2) Swap to a recognizable color.\nviewport.setJointColor('#ff8800');\nconsole.log('jointColor after set: ' + viewport.jointColor);\n// 3) Restore.\nviewport.setJointColor(original);\nconsole.log('jointColor restored: ' + viewport.jointColor);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setJointScale",
      "symbol": "BaseViewportApi.setJointScale",
      "intent": "scene",
      "code": "// 1) Snapshot the current scale.\nconst original = viewport.jointScale;\nconsole.log('jointScale original: ' + original);\n// 2) Bump the scale up, then down.\nviewport.setJointScale(2.0);\nconsole.log('jointScale after 2x: ' + viewport.jointScale);\nviewport.setJointScale(0.5);\nconsole.log('jointScale after 0.5x: ' + viewport.jointScale);\n// 3) Restore.\nviewport.setJointScale(original);\nconsole.log('jointScale restored: ' + viewport.jointScale);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setOrtho",
      "symbol": "BaseViewportApi.setOrtho",
      "intent": "scene",
      "code": "// 1) Snapshot current projection.\nconst original = viewport.isOrtho;\nconsole.log('isOrtho original: ' + original);\n// 2) Flip on, then off.\nviewport.setOrtho(true);\nconsole.log('isOrtho after on: ' + viewport.isOrtho);\nviewport.setOrtho(false);\nconsole.log('isOrtho after off: ' + viewport.isOrtho);\n// 3) Restore.\nviewport.setOrtho(original);\nconsole.log('isOrtho restored: ' + viewport.isOrtho);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setRenderMode",
      "symbol": "BaseViewportApi.setRenderMode",
      "intent": "scene",
      "code": "// 1) Snapshot the current mode so we can restore it.\nconst original = viewport.renderMode;\nconsole.log('renderMode original: ' + original);\n// 2) Cycle every supported mode.\nviewport.setRenderMode('clay');\nconsole.log('renderMode after clay: ' + viewport.renderMode);\nviewport.setRenderMode('textured');\nconsole.log('renderMode after textured: ' + viewport.renderMode);\nviewport.setRenderMode('uvchecker');\nconsole.log('renderMode after uvchecker: ' + viewport.renderMode);\nviewport.setRenderMode('normal');\nconsole.log('renderMode after normal: ' + viewport.renderMode);\n// 3) Restore.\nviewport.setRenderMode(original);\nconsole.log('renderMode restored: ' + viewport.renderMode);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setShowGrid",
      "symbol": "BaseViewportApi.setShowGrid",
      "intent": "scene",
      "code": "// 1) Snapshot the current setting.\nconst original = viewport.showGrid;\nconsole.log('showGrid original: ' + original);\n// 2) Flip the setting.\nviewport.setShowGrid(!original);\nconsole.log('showGrid after toggle: ' + viewport.showGrid);\n// 3) Restore.\nviewport.setShowGrid(original);\nconsole.log('showGrid restored: ' + viewport.showGrid);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setShowJointAxis",
      "symbol": "BaseViewportApi.setShowJointAxis",
      "intent": "scene",
      "code": "// 1) Snapshot the current setting.\nconst original = viewport.showJointAxis;\nconsole.log('showJointAxis original: ' + original);\n// 2) Flip the setting.\nviewport.setShowJointAxis(!original);\nconsole.log('showJointAxis after toggle: ' + viewport.showJointAxis);\n// 3) Restore.\nviewport.setShowJointAxis(original);\nconsole.log('showJointAxis restored: ' + viewport.showJointAxis);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setShowSkeleton",
      "symbol": "BaseViewportApi.setShowSkeleton",
      "intent": "scene",
      "code": "// 1) Snapshot the current setting.\nconst original = viewport.showSkeleton;\nconsole.log('showSkeleton original: ' + original);\n// 2) Flip the setting.\nviewport.setShowSkeleton(!original);\nconsole.log('showSkeleton after toggle: ' + viewport.showSkeleton);\n// 3) Restore.\nviewport.setShowSkeleton(original);\nconsole.log('showSkeleton restored: ' + viewport.showSkeleton);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.setView",
      "symbol": "BaseViewportApi.setView",
      "intent": "scene",
      "code": "// 1. Setup — Previewer can't author geometry; operate on whatever\n//    asset is loaded and snapshot the current projection mode.\nawait Utils.wait.frame();\nconst wasOrtho = viewport.isOrtho;\nconsole.log('isOrtho before presets: ' + wasOrtho);\n// 2. Action — snap through every view preset; the ortho presets flip\n//    isOrtho true, 'persp' flips it back to false.\nviewport.setView('top');\nconsole.log('after top, isOrtho = ' + viewport.isOrtho);\nviewport.setView('front');\nviewport.setView('right');\nviewport.setView('persp');\n// 3. Read-back — 'persp' is the only non-ortho preset, so the\n//    projection must read perspective now.\nconsole.log('after persp, isOrtho = ' + viewport.isOrtho);\n// 4. Verify — confirm the final preset landed perspective.\nconsole.log('setView persp -> perspective: ' + (viewport.isOrtho === false));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.showGrid",
      "symbol": "BaseViewportApi.showGrid",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current grid visibility.\nconst original = viewport.showGrid;\nconsole.log('showGrid original: ' + original);\n// 2. Action — toggle the grid via the atomic flip.\nviewport.toggleGrid();\n// 3. Read-back — the getter must report the flipped value.\nconsole.log('showGrid after toggle: ' + viewport.showGrid);\n// 4. Verify the flip inverted the value, then restore.\nconsole.log('toggle inverted showGrid: ' + (viewport.showGrid === !original));\nviewport.setShowGrid(original);\nconsole.log('showGrid restored: ' + (viewport.showGrid === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.showJointAxis",
      "symbol": "BaseViewportApi.showJointAxis",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current joint-axis overlay visibility.\nconst original = viewport.showJointAxis;\nconsole.log('showJointAxis original: ' + original);\n// 2. Action — flip the overlay.\nviewport.setShowJointAxis(!original);\n// 3. Read-back — getter reports the flipped value.\nconsole.log('showJointAxis after flip: ' + viewport.showJointAxis);\n// 4. Verify, then restore.\nconsole.log('getter tracks setter: ' + (viewport.showJointAxis === !original));\nviewport.setShowJointAxis(original);\nconsole.log('showJointAxis restored: ' + (viewport.showJointAxis === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.showSkeleton",
      "symbol": "BaseViewportApi.showSkeleton",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current skeleton-overlay visibility.\nconst original = viewport.showSkeleton;\nconsole.log('showSkeleton original: ' + original);\n// 2. Action — show the skeleton overlay (Previewer's joint-chain view).\nviewport.setShowSkeleton(true);\n// 3. Read-back — getter must report it on.\nconsole.log('showSkeleton after on: ' + viewport.showSkeleton);\n// 4. Verify, then restore the original.\nconsole.log('getter tracks setter: ' + (viewport.showSkeleton === true));\nviewport.setShowSkeleton(original);\nconsole.log('showSkeleton restored: ' + (viewport.showSkeleton === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.toggleGrid",
      "symbol": "BaseViewportApi.toggleGrid",
      "intent": "scene",
      "code": "// 1) Snapshot, toggle, toggle-back.\nconst original = viewport.showGrid;\nviewport.toggleGrid();\nconsole.log('showGrid after toggle: ' + viewport.showGrid);\nviewport.toggleGrid();\nconsole.log('showGrid restored: ' + (viewport.showGrid === original));"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.toggleOrtho",
      "symbol": "BaseViewportApi.toggleOrtho",
      "intent": "scene",
      "code": "// 1) Snapshot, flip, flip-again, log.\nconst original = viewport.isOrtho;\nconsole.log('isOrtho original: ' + original);\nviewport.toggleOrtho();\nconsole.log('isOrtho after toggle: ' + viewport.isOrtho);\n// 2) Restore.\nviewport.toggleOrtho();\nconsole.log('isOrtho restored: ' + viewport.isOrtho);"
    },
    {
      "kind": "example",
      "id": "example:BaseViewportApi.zoomToFit",
      "symbol": "BaseViewportApi.zoomToFit",
      "intent": "scene",
      "code": "// 1. Setup — Previewer frames whatever asset is loaded; wait one frame\n//    so any pending load settles, then count the framable meshes.\nawait Utils.wait.frame();\nconst meshes = ls({ type: 'mesh' });\nconsole.log('framable meshes: ' + meshes.length);\n// 2. Action — frame every loaded mesh (null = all) with default padding.\nviewport.zoomToFit(null, 1.5);\nconsole.log('zoomToFit(all) invoked');\n// 3. Read-back — frame again with wider padding to confirm the call is\n//    idempotent and the canvas stays mounted.\nconst d = viewport.dimensions();\nviewport.zoomToFit(null, 2.5);\n// 4. Verify — canvas is live (>0) after framing, proving the viewport\n//    accepted the calls.\nconsole.log('viewport live after zoomToFit: ' + (d.width > 0 && d.height > 0));"
    },
    {
      "kind": "example",
      "id": "example:BatchConvertResult.cancelled",
      "symbol": "BatchConvertResult.cancelled",
      "intent": "scene",
      "code": "// 1) Run a cancellable batch convert and check the flag.\nconst ctrl = new AbortController();\nconst result = await io.batchConvert({\n    from: { folder: true, accept: '.obj' },\n    to: { folder: true, format: 'glb' },\n    signal: ctrl.signal,\n});\nconsole.log('was cancelled: ' + result.cancelled);"
    },
    {
      "kind": "example",
      "id": "example:BatchConvertResult.failed",
      "symbol": "BatchConvertResult.failed",
      "intent": "scene",
      "code": "// 1) Use 'continue' so failures land in result.failed instead of throwing.\nconst result = await io.batchConvert({\n    from: { folder: true, accept: '.obj' },\n    to: { folder: true, format: 'glb' },\n    errorPolicy: 'continue',\n});\nconsole.log('failed count: ' + result.failed.length);\nfor (const row of result.failed) {\n    console.log(row.input + ': ' + row.error.message);\n}"
    },
    {
      "kind": "example",
      "id": "example:BatchConvertResult.succeeded",
      "symbol": "BatchConvertResult.succeeded",
      "intent": "scene",
      "code": "// 1) Inspect the success list after a batch run.\nconst result = await io.batchConvert({\n    from: { folder: true, accept: '.obj' },\n    to: { folder: true, format: 'glb' },\n});\nconsole.log('succeeded count: ' + result.succeeded.length);\nfor (const row of result.succeeded) {\n    console.log(row.input + ' -> ' + row.output + ' (' + row.bytes + ' bytes)');\n}"
    },
    {
      "kind": "example",
      "id": "example:BatchConvertResult.total",
      "symbol": "BatchConvertResult.total",
      "intent": "scene",
      "code": "// 1) Read the total file count off a batchConvert result.\nconst result = await io.batchConvert({\n    from: { folder: true, accept: '.obj' },\n    to: { folder: true, format: 'glb' },\n});\nconsole.log('total attempted: ' + result.total);"
    },
    {
      "kind": "example",
      "id": "example:BisectOpts.axis",
      "symbol": "BisectOpts.axis",
      "intent": "scene",
      "code": "// 1) Bisect a cube along the Y axis.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'bisect-axis' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.bisect({ axis: 'y', offset: 0 });\nconsole.log('after Y bisect — verts: ' + cube.verts.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BisectOpts.clearInner",
      "symbol": "BisectOpts.clearInner",
      "intent": "scene",
      "code": "// 1) Bisect a cube and drop the negative half.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'bisect-inner' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.bisect({ axis: 'x', offset: 0, clearInner: true });\nconsole.log('after clearInner — verts: ' + cube.verts.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BisectOpts.clearOuter",
      "symbol": "BisectOpts.clearOuter",
      "intent": "scene",
      "code": "// 1) Bisect a cube and drop the positive half.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'bisect-outer' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.bisect({ axis: 'x', offset: 0, clearOuter: true });\nconsole.log('after clearOuter — verts: ' + cube.verts.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BisectOpts.fill",
      "symbol": "BisectOpts.fill",
      "intent": "scene",
      "code": "// 1) Bisect with a cap face along the cut.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'bisect-fill' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.bisect({ axis: 'y', offset: 0, fill: true });\nconsole.log('after capped bisect — faces: ' + cube.faces.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BisectOpts.offset",
      "symbol": "BisectOpts.offset",
      "intent": "scene",
      "code": "// 1) Bisect a cube at a positive offset along X.\nconst cube = await create.cube({ width: 2, height: 1, depth: 1, name: 'bisect-offset' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.bisect({ axis: 'x', offset: 0.3 });\nconsole.log('after offset bisect — verts: ' + cube.verts.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BisectSession.example",
      "symbol": "BisectSession.example",
      "intent": "scene",
      "code": "// 1) Build a cube and open a bisect session.\nconst cube = await create.cube({ name: 'bisect-example-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.bisect({ axis: 'x' }));\n// 2) Render the example snippet.\nconst snippet = session.example();\nconsole.log('example length: ' + snippet.length);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BisectSession.help",
      "symbol": "BisectSession.help",
      "intent": "scene",
      "code": "// 1) Build a cube and open a bisect session.\nconst cube = await create.cube({ name: 'bisect-help-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.bisect({ axis: 'x' }));\n// 2) Render the help string.\nconst helpText = session.help();\nconsole.log('help length: ' + helpText.length);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BisectSession.oneShot",
      "symbol": "BisectSession.oneShot",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'bisect-oneshot-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Apply a one-shot bisect along Y at offset 0 (closing the cut into a face).\nconst result = await cube.tools.bisect({\n    oneShot: true,\n    axis: 'y',\n    offset: 0,\n    fill: true,\n});\nconsole.log('oneShot ok: ' + ('ok' in result ? result.ok : false));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BisectSession.setAxis",
      "symbol": "BisectSession.setAxis",
      "intent": "scene",
      "code": "// 1) Build a cube and open a bisect session.\nconst cube = await create.cube({ name: 'bisect-axis-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.bisect({ axis: 'x' }));\n// 2) Switch to the Y axis.\nsession.setAxis('y');\nconsole.log('axis set to y');\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BisectSession.setClearInner",
      "symbol": "BisectSession.setClearInner",
      "intent": "scene",
      "code": "// 1) Build a cube and open a bisect session.\nconst cube = await create.cube({ name: 'bisect-clear-inner-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.bisect({ axis: 'y' }));\n// 2) Ask the inner side to drop.\nsession.setClearInner(true);\nconsole.log('clearInner set to true');\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BisectSession.setClearOuter",
      "symbol": "BisectSession.setClearOuter",
      "intent": "scene",
      "code": "// 1) Build a cube and open a bisect session.\nconst cube = await create.cube({ name: 'bisect-clear-outer-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.bisect({ axis: 'y' }));\n// 2) Ask the outer side to drop.\nsession.setClearOuter(true);\nconsole.log('clearOuter set to true');\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BisectSession.setFill",
      "symbol": "BisectSession.setFill",
      "intent": "scene",
      "code": "// 1) Build a cube and open a bisect session.\nconst cube = await create.cube({ name: 'bisect-fill-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.bisect({ axis: 'y', offset: 0 }));\n// 2) Ask the cut to close into a face.\nsession.setFill(true);\nconsole.log('fill set to true');\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BisectSession.setOffset",
      "symbol": "BisectSession.setOffset",
      "intent": "scene",
      "code": "// 1) Build a cube and open a bisect session.\nconst cube = await create.cube({ name: 'bisect-offset-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.bisect({ axis: 'y' }));\n// 2) Slide the plane halfway up the cube.\nsession.setOffset(0.25);\nconsole.log('offset set to 0.25');\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.center",
      "symbol": "BoundingBox.center",
      "intent": "math",
      "code": "// 1) Build a symmetric box around the origin.\nconst box = MathLib.boundingBox([-2, -3, -4], [2, 3, 4]);\nconsole.log('center: ' + JSON.stringify(box.center.toArray()));\n// 2) Off-center box.\nconst off = MathLib.boundingBox([0, 0, 0], [10, 20, 30]);\nconsole.log('off center: ' + JSON.stringify(off.center.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.clone",
      "symbol": "BoundingBox.clone",
      "intent": "math",
      "code": "// 1) Build a box and clone it.\nconst a = MathLib.boundingBox([0, 0, 0], [10, 10, 10]);\nconst b = a.clone();\n// 2) The clone compares equal but is a separate instance.\nconsole.log('equals: ' + a.equals(b));\n// 3) Expanding the clone leaves the original untouched.\nconst grown = b.expand([20, 0, 0]);\nconsole.log('original max: ' + JSON.stringify(a.max.toArray()));\nconsole.log('grown max: ' + JSON.stringify(grown.max.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.contains",
      "symbol": "BoundingBox.contains",
      "intent": "math",
      "code": "// 1) Build a 10x10x10 box.\nconst box = MathLib.boundingBox([0, 0, 0], [10, 10, 10]);\n// 2) Interior point.\nconsole.log('inside: ' + box.contains([5, 5, 5]));\n// 3) Point past max.x.\nconsole.log('outside x: ' + box.contains([20, 5, 5]));\n// 4) Point on the max corner counts as contained.\nconsole.log('on boundary: ' + box.contains([10, 10, 10]));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.depth",
      "symbol": "BoundingBox.depth",
      "intent": "math",
      "code": "// 1) Build a 2-deep box and read the depth.\nconst box = MathLib.boundingBox([0, 0, 0], [10, 4, 2]);\nconsole.log('depth: ' + box.depth);\n// 2) Empty box returns 0.\nconsole.log('empty depth: ' + MathLib.boundingBox().depth);"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.empty",
      "symbol": "BoundingBox.empty",
      "intent": "math",
      "code": "// 1) Start from an empty box.\nconst box = BoundingBox.empty();\nconsole.log('isEmpty before expand: ' + box.isEmpty);\n// 2) Expand by a point - returns a NEW (non-empty) box.\nconst populated = box.expand([3, 4, 5]);\nconsole.log('isEmpty after expand: ' + populated.isEmpty);\nconsole.log('min: ' + JSON.stringify(populated.min.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.equals",
      "symbol": "BoundingBox.equals",
      "intent": "math",
      "code": "// 1) Two equivalent boxes.\nconst a = MathLib.boundingBox([0, 0, 0], [10, 10, 10]);\nconst b = MathLib.boundingBox([0, 0, 0], [10, 10, 10]);\nconsole.log('equal exact: ' + a.equals(b));\n// 2) Small float drift inside default epsilon.\nconst c = MathLib.boundingBox([0, 0, 1e-9], [10, 10, 10]);\nconsole.log('equal within default eps: ' + a.equals(c));\n// 3) Tightening epsilon catches the drift.\nconsole.log('equal within tight eps: ' + a.equals(c, 1e-12));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.expand",
      "symbol": "BoundingBox.expand",
      "intent": "math",
      "code": "// 1) Build a 10x10x10 box.\nconst box = MathLib.boundingBox([0, 0, 0], [10, 10, 10]);\n// 2) Expand by a point outside the box.\nconst grown = box.expand([15, 0, 0]);\nconsole.log('grown max: ' + JSON.stringify(grown.max.toArray()));\n// 3) The original survives untouched.\nconsole.log('original max: ' + JSON.stringify(box.max.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.expandBox",
      "symbol": "BoundingBox.expandBox",
      "intent": "math",
      "code": "// 1) Build two overlapping boxes.\nconst a = MathLib.boundingBox([0, 0, 0], [10, 10, 10]);\nconst b = MathLib.boundingBox([5, 5, 5], [20, 15, 15]);\n// 2) Merge.\nconst merged = a.expandBox(b);\nconsole.log('merged min: ' + JSON.stringify(merged.min.toArray()));\nconsole.log('merged max: ' + JSON.stringify(merged.max.toArray()));\n// 3) Merging with self yields an equivalent box.\nconst same = a.expandBox(a);\nconsole.log('self merge equals a: ' + same.equals(a));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.from",
      "symbol": "BoundingBox.from",
      "intent": "math",
      "code": "// 1) Build a 10x10x10 box.\nconst box = BoundingBox.from([0, 0, 0], [10, 10, 10]);\n// 2) Read derived properties.\nconsole.log('width: ' + box.width);\nconsole.log('height: ' + box.height);\nconsole.log('depth: ' + box.depth);"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.fromCenterAndSize",
      "symbol": "BoundingBox.fromCenterAndSize",
      "intent": "math",
      "code": "// 1) Build a 4x2x6 box centered at (10, 5, -2).\nconst box = BoundingBox.fromCenterAndSize([10, 5, -2], [4, 2, 6]);\n// 2) Read back the corners.\nconsole.log('min: ' + JSON.stringify(box.min.toArray()));\nconsole.log('max: ' + JSON.stringify(box.max.toArray()));\n// 3) The center round-trips.\nconsole.log('center: ' + JSON.stringify(box.center.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.fromPoints",
      "symbol": "BoundingBox.fromPoints",
      "intent": "math",
      "code": "// 1) Build a box around a small point cloud.\nconst box = BoundingBox.fromPoints([\n    [0, 0, 0], [5, 5, 5], [-3, 2, 1],\n]);\n// 2) The min / max are pulled from the extremes of the input set.\nconsole.log('min: ' + JSON.stringify(box.min.toArray()));\nconsole.log('max: ' + JSON.stringify(box.max.toArray()));\nconsole.log('contains midpoint: ' + box.contains([1, 2, 3]));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.height",
      "symbol": "BoundingBox.height",
      "intent": "math",
      "code": "// 1) Build a 4-tall box and read the height.\nconst box = MathLib.boundingBox([0, 0, 0], [10, 4, 2]);\nconsole.log('height: ' + box.height);\n// 2) Empty box returns 0.\nconsole.log('empty height: ' + MathLib.boundingBox().height);"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.intersects",
      "symbol": "BoundingBox.intersects",
      "intent": "math",
      "code": "// 1) Overlapping pair.\nconst a = MathLib.boundingBox([0, 0, 0], [10, 10, 10]);\nconst b = MathLib.boundingBox([5, 5, 5], [15, 15, 15]);\nconsole.log('overlap: ' + a.intersects(b));\n// 2) Disjoint pair.\nconst c = MathLib.boundingBox([20, 0, 0], [30, 10, 10]);\nconsole.log('disjoint: ' + a.intersects(c));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.isEmpty",
      "symbol": "BoundingBox.isEmpty",
      "intent": "math",
      "code": "// 1) Empty out of the box.\nconst empty = MathLib.boundingBox();\nconsole.log('starts empty: ' + empty.isEmpty);\n// 2) Expanding gives a non-empty box (a NEW instance).\nconst filled = empty.expand([1, 2, 3]);\nconsole.log('filled is empty: ' + filled.isEmpty);\n// 3) Original is untouched (immutable).\nconsole.log('original still empty: ' + empty.isEmpty);"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.max",
      "symbol": "BoundingBox.max",
      "intent": "math",
      "code": "// 1) Build a box and read its max corner.\nconst box = MathLib.boundingBox([1, 2, 3], [9, 8, 7]);\nconsole.log('max: ' + JSON.stringify(box.max.toArray()));\n// 2) Each access returns a fresh copy.\nconsole.log('max again: ' + JSON.stringify(box.max.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.min",
      "symbol": "BoundingBox.min",
      "intent": "math",
      "code": "// 1) Build a box and read its min corner.\nconst box = MathLib.boundingBox([1, 2, 3], [9, 8, 7]);\nconsole.log('min: ' + JSON.stringify(box.min.toArray()));\n// 2) Each access returns a fresh copy - safe to mutate locally.\nconst m = box.min;\nm.x = 999;\nconsole.log('box min unchanged: ' + JSON.stringify(box.min.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.size",
      "symbol": "BoundingBox.size",
      "intent": "math",
      "code": "// 1) Build a 4x6x8 box.\nconst box = MathLib.boundingBox([0, 0, 0], [4, 6, 8]);\nconsole.log('size: ' + JSON.stringify(box.size.toArray()));\n// 2) Each component matches the matching axis-specific getter.\nconsole.log('width matches size.x: ' + (box.width === box.size.x));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.toArray",
      "symbol": "BoundingBox.toArray",
      "intent": "math",
      "code": "// 1) Build a box and serialize it.\nconst box = MathLib.boundingBox([1, 2, 3], [9, 8, 7]);\nconst raw = box.toArray();\nconsole.log('min tuple: ' + JSON.stringify(raw.min));\nconsole.log('max tuple: ' + JSON.stringify(raw.max));\n// 2) The tuples are independent - mutate freely.\nraw.min[0] = 999;\nconsole.log('box min unchanged: ' + JSON.stringify(box.min.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.transformedBy",
      "symbol": "BoundingBox.transformedBy",
      "intent": "math",
      "code": "// 1) Build a unit box at the origin.\nconst box = MathLib.boundingBox([-1, -1, -1], [1, 1, 1]);\n// 2) Build a 45 degree rotation around Y as a column-major matrix.\nconst cos45 = Math.cos(Math.PI / 4);\nconst sin45 = Math.sin(Math.PI / 4);\nconst rotY = [\n    cos45, 0, -sin45, 0,\n    0, 1, 0, 0,\n    sin45, 0, cos45, 0,\n    0, 0, 0, 1,\n];\n// 3) Transform - the result spans the rotated corners.\nconst rotated = box.transformedBy(rotY);\nconsole.log('rotated min: ' + JSON.stringify(rotated.min.toArray().map(v => Math.round(v * 1000) / 1000)));\nconsole.log('rotated max: ' + JSON.stringify(rotated.max.toArray().map(v => Math.round(v * 1000) / 1000)));"
    },
    {
      "kind": "example",
      "id": "example:BoundingBox.width",
      "symbol": "BoundingBox.width",
      "intent": "math",
      "code": "// 1) Build a 10-wide box and read the width.\nconst box = MathLib.boundingBox([0, 0, 0], [10, 4, 2]);\nconsole.log('width: ' + box.width);\n// 2) Empty box returns 0.\nconsole.log('empty width: ' + MathLib.boundingBox().width);"
    },
    {
      "kind": "example",
      "id": "example:BuildOnNormalResult.meshId",
      "symbol": "BuildOnNormalResult.meshId",
      "intent": "scene",
      "code": "const base = await create.cube({ name: 'bon-result-demo' });\nconst result = await base.buildOnNormal({ primitiveType: 'cube', position: [0, 0.5, 0], normal: [0, 1, 0] });\nconsole.log('result meshId is string: ' + (typeof result?.meshId === 'string'));\nawait base.delete();"
    },
    {
      "kind": "example",
      "id": "example:BuildOnNormalSession.example",
      "symbol": "BuildOnNormalSession.example",
      "intent": "scene",
      "code": "// 1) Build a plane and open an interactive build-on-normal session.\nconst plane = await create.plane({ width: 4, height: 4, name: 'bon-example-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await plane.tools.buildOnNormal({ primitiveType: 'cube' }));\n// 2) Render the example snippet.\nconst snippet = session.example();\nconsole.log('example length: ' + snippet.length);\n// 3) Tear down.\nawait session.cancel();\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:BuildOnNormalSession.help",
      "symbol": "BuildOnNormalSession.help",
      "intent": "scene",
      "code": "// 1) Build a plane and open an interactive build-on-normal session.\nconst plane = await create.plane({ width: 4, height: 4, name: 'bon-help-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await plane.tools.buildOnNormal({ primitiveType: 'cube' }));\n// 2) Render the help string.\nconst helpText = session.help();\nconsole.log('help length: ' + helpText.length);\n// 3) Tear down.\nawait session.cancel();\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:CatalogInstallOptions.position",
      "symbol": "CatalogInstallOptions.position",
      "intent": "scene",
      "code": "// Build an install-options bag with an explicit position.\nconst opts = { position: [1, 0, 0] };\nconsole.log('position is array: ' + Array.isArray(opts.position));"
    },
    {
      "kind": "example",
      "id": "example:CatalogPart",
      "symbol": "CatalogPart",
      "intent": "scene",
      "code": "// 1) Read the part list, log each part's name + license.\nconst parts = catalog.list();\nfor (const p of parts)\n    console.log(p.name, p.license);"
    },
    {
      "kind": "example",
      "id": "example:CatalogPart.glbUrl",
      "symbol": "CatalogPart.glbUrl",
      "intent": "scene",
      "code": "// Check whether the first part ships a GLB preview.\nconst part = catalog.list()[0];\nconsole.log('glbUrl present: ' + (part?.glbUrl !== undefined));"
    },
    {
      "kind": "example",
      "id": "example:CatalogPart.id",
      "symbol": "CatalogPart.id",
      "intent": "scene",
      "code": "// Read the first catalog part's id.\nconst part = catalog.list()[0];\nconsole.log('id is string: ' + (typeof part?.id === 'string'));"
    },
    {
      "kind": "example",
      "id": "example:CatalogPart.license",
      "symbol": "CatalogPart.license",
      "intent": "scene",
      "code": "// Read the SPDX license of the first part.\nconst part = catalog.list()[0];\nconsole.log('license is string: ' + (typeof part?.license === 'string'));"
    },
    {
      "kind": "example",
      "id": "example:CatalogPart.name",
      "symbol": "CatalogPart.name",
      "intent": "scene",
      "code": "// Read the first catalog part's display name.\nconst part = catalog.list()[0];\nconsole.log('name is string: ' + (typeof part?.name === 'string'));"
    },
    {
      "kind": "example",
      "id": "example:CatalogPart.pngUrl",
      "symbol": "CatalogPart.pngUrl",
      "intent": "scene",
      "code": "// Read the thumbnail URL of the first part.\nconst part = catalog.list()[0];\nconsole.log('pngUrl is string: ' + (typeof part?.pngUrl === 'string'));"
    },
    {
      "kind": "example",
      "id": "example:CatalogPart.sourceAttribution",
      "symbol": "CatalogPart.sourceAttribution",
      "intent": "scene",
      "code": "// Read the attribution string (present for CC-BY parts).\nconst part = catalog.list().find((p) => p.license.startsWith('CC-BY'));\nconsole.log('sourceAttribution present: ' + (part?.sourceAttribution !== undefined));"
    },
    {
      "kind": "example",
      "id": "example:CatalogPart.stepUrl",
      "symbol": "CatalogPart.stepUrl",
      "intent": "scene",
      "code": "// Read the STEP-file URL of the first part.\nconst part = catalog.list()[0];\nconsole.log('stepUrl is string: ' + (typeof part?.stepUrl === 'string'));"
    },
    {
      "kind": "example",
      "id": "example:CatalogPart.tags",
      "symbol": "CatalogPart.tags",
      "intent": "scene",
      "code": "// Inspect the search tags on the first part.\nconst part = catalog.list()[0];\nconsole.log('tags is array: ' + Array.isArray(part?.tags));"
    },
    {
      "kind": "example",
      "id": "example:CheckOpenScadApiOptions",
      "symbol": "CheckOpenScadApiOptions",
      "intent": "scene",
      "code": "// 1. Setup — a default (read-only) check bag and an explicit keep bag.\nconst readOnly = {};\nconst keeping = { keep: true };\n// 2. Action — probe which bag opts into keeping the baked mesh.\nconst defaultKeeps = 'keep' in readOnly;\n// 3. Read-back — the keep flag on the explicit bag.\nconst explicitKeeps = keeping.keep === true;\n// 4. Verify — only the explicit bag keeps; the default check is read-only.\nconsole.log('keep contract: ' + (!defaultKeeps && explicitKeeps));"
    },
    {
      "kind": "example",
      "id": "example:CheckOpenScadApiOptions.keep",
      "symbol": "CheckOpenScadApiOptions.keep",
      "intent": "scene",
      "code": "// 1. Setup — model the report's mesh field under both modes.\nconst withKeep = { keep: true, meshPresent: true };\nconst withoutKeep = { keep: false, meshPresent: false };\n// 2. Action — pick the mode a probing agent should use.\nconst probing = withoutKeep;\n// 3. Read-back — a read-only probe leaves no mesh behind.\nconst leavesMesh = probing.meshPresent;\n// 4. Verify — probe mode is scene-neutral; keep mode hands back the handle.\nconsole.log('modes ok: ' + (!leavesMesh && withKeep.meshPresent === true));"
    },
    {
      "kind": "example",
      "id": "example:ClipboardApi",
      "symbol": "ClipboardApi",
      "intent": "scene",
      "code": "// 1) Spawn a cube and select it.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1 });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(cube);\nconst before = ls({ type: 'mesh' }).length;\n// 2) Copy it to the clipboard.\nawait clipboard.copy();\n// 3) Paste it back. Now there are two meshes in the scene.\nawait clipboard.paste();\nconst afterPaste = ls({ type: 'mesh' }).length;\nconsole.log('mesh count went from ' + before + ' to ' + afterPaste);\n// 4) Cut (= copy + delete) the current selection.\nawait clipboard.cut();\nconst afterCut = ls({ type: 'mesh' }).length;\nconsole.log('mesh count after cut: ' + afterCut);\n// 5) Paste once more to restore the cut node.\nawait clipboard.paste();\n// 6) Clean up every mesh that survived.\nfor (const m of ls({ type: 'mesh' })) {\n    await m.delete();\n}"
    },
    {
      "kind": "example",
      "id": "example:ClipboardApi.copy",
      "symbol": "ClipboardApi.copy",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1 });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(cube);\nawait clipboard.copy();\nawait clipboard.paste();\nconsole.log('mesh count after paste: ' + ls({ type: 'mesh' }).length);\nfor (const m of ls({ type: 'mesh' }))\n    await m.delete();"
    },
    {
      "kind": "example",
      "id": "example:ClipboardApi.cut",
      "symbol": "ClipboardApi.cut",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1 });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(cube);\nawait clipboard.cut();\nconsole.log('after cut: ' + ls({ type: 'mesh' }).length);\nawait clipboard.paste();\nfor (const m of ls({ type: 'mesh' }))\n    await m.delete();"
    },
    {
      "kind": "example",
      "id": "example:ClipboardApi.example",
      "symbol": "ClipboardApi.example",
      "intent": "scene",
      "code": "console.log('clipboard example length: ' + clipboard.example().length);"
    },
    {
      "kind": "example",
      "id": "example:ClipboardApi.help",
      "symbol": "ClipboardApi.help",
      "intent": "scene",
      "code": "console.log('clipboard help length: ' + clipboard.help().length);"
    },
    {
      "kind": "example",
      "id": "example:ClipboardApi.paste",
      "symbol": "ClipboardApi.paste",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1 });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(cube);\nawait clipboard.copy();\nconst before = ls({ type: 'mesh' }).length;\nawait clipboard.paste();\nconst after = ls({ type: 'mesh' }).length;\nconsole.log('mesh count: ' + before + ' -> ' + after);\nfor (const m of ls({ type: 'mesh' }))\n    await m.delete();"
    },
    {
      "kind": "example",
      "id": "example:Color",
      "symbol": "Color",
      "intent": "scene",
      "code": "// 1. Setup - pure math, no scene needed.\nconst c = new Color(0.8, 0.2, 0.4);\n// 2. Select - N/A for pure color math.\n// 3. Action - read the components.\nconst tuple = c.toArray();\n// 4. Observe.\nconsole.log('Color tuple: ' + JSON.stringify(tuple));"
    },
    {
      "kind": "example",
      "id": "example:Color.b",
      "symbol": "Color.b",
      "intent": "math",
      "code": "const c = Color.fromHex(0xff8000);\nconsole.log('b: ' + c.b.toFixed(4));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.black",
      "symbol": "Color.black",
      "intent": "math",
      "code": "const k = Color.black();\nconsole.log('black tuple: ' + JSON.stringify(k.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.clone",
      "symbol": "Color.clone",
      "intent": "math",
      "code": "const a = new Color(1, 0, 0);\nconst b = a.clone();\nb.r = 0;\nconsole.log('a unchanged: ' + (a.r === 1));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.equals",
      "symbol": "Color.equals",
      "intent": "math",
      "code": "const a = new Color(1, 0, 0);\nconst b = new Color(1, 1e-9, 0);\nconsole.log('approx equal: ' + a.equals(b));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.fromHex",
      "symbol": "Color.fromHex",
      "intent": "math",
      "code": "// 1. Hex literal.\nconst cyan = Color.fromHex(0x00ffff);\nconsole.log('cyan tuple: ' + JSON.stringify(cyan.toArray()));\n// 2. Hex string round-trips back to the literal.\nconst same = Color.fromHex('#00ffff');\nconsole.log('round-trip hex: ' + same.toHexString());\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.fromHSL",
      "symbol": "Color.fromHSL",
      "intent": "math",
      "code": "// 1. Pure red in HSL is (0, 1, 0.5).\nconst red = Color.fromHSL(0, 1, 0.5);\nconsole.log('red tuple: ' + JSON.stringify(red.toArray()));\n// 2. Round-trip back through toHSL.\nconst hsl = red.toHSL();\nconsole.log('h: ' + hsl.h + ' s: ' + hsl.s + ' l: ' + hsl.l);\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.fromRGB",
      "symbol": "Color.fromRGB",
      "intent": "math",
      "code": "const orange = Color.fromRGB(1, 0.5, 0);\nconsole.log('orange tuple: ' + JSON.stringify(orange.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.g",
      "symbol": "Color.g",
      "intent": "math",
      "code": "const c = Color.fromHex(0xff8000);\nconsole.log('g: ' + c.g.toFixed(4));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.lerp",
      "symbol": "Color.lerp",
      "intent": "math",
      "code": "// 1. Blend red and blue at the midpoint.\nconst red = new Color(1, 0, 0);\nconst blue = new Color(0, 0, 1);\nconst mid = red.lerp(blue, 0.5);\nconsole.log('midpoint: ' + JSON.stringify(mid.toArray()));\n// 2. t = 0 returns the start color, t = 1 returns the end.\nconst start = red.lerp(blue, 0);\nconst end = red.lerp(blue, 1);\nconsole.log('start equals red: ' + start.equals(red));\nconsole.log('end equals blue: ' + end.equals(blue));\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.multiply",
      "symbol": "Color.multiply",
      "intent": "math",
      "code": "// 1. Tint a 50% gray with a red filter.\nconst gray = new Color(0.5, 0.5, 0.5);\nconst red = new Color(1, 0, 0);\nconst tinted = gray.multiply(red);\nconsole.log('tinted: ' + JSON.stringify(tinted.toArray()));\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.r",
      "symbol": "Color.r",
      "intent": "math",
      "code": "const c = Color.fromHex(0xff8000);\nconsole.log('r: ' + c.r.toFixed(4));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.toArray",
      "symbol": "Color.toArray",
      "intent": "math",
      "code": "const red = new Color(1, 0, 0);\nconsole.log('tuple: ' + JSON.stringify(red.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.toHex",
      "symbol": "Color.toHex",
      "intent": "math",
      "code": "const cyan = new Color(0, 1, 1);\nconsole.log('hex: 0x' + cyan.toHex().toString(16).padStart(6, '0'));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.toHexString",
      "symbol": "Color.toHexString",
      "intent": "math",
      "code": "const cyan = new Color(0, 1, 1);\nconsole.log('hex string: ' + cyan.toHexString());\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.toHSL",
      "symbol": "Color.toHSL",
      "intent": "math",
      "code": "const red = new Color(1, 0, 0);\nconst hsl = red.toHSL();\nconsole.log('h: ' + hsl.h);\nconsole.log('s: ' + hsl.s);\nconsole.log('l: ' + hsl.l);\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Color.white",
      "symbol": "Color.white",
      "intent": "math",
      "code": "const w = Color.white();\nconsole.log('white tuple: ' + JSON.stringify(w.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Curve",
      "symbol": "Curve",
      "intent": "scene",
      "code": "// 1. Setup — create a curve via the create barrel.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [1, 0, 0], [1, 1, 0]],\n    name: 'demo-curve',\n});\n// 2. Position it via the inherited transform surface.\ncurve.xform({ t: [2.5, 1.3, -0.7] });\n// 3. Action — read its name.\nconst n = curve.name;\n// 4. Observe + cleanup.\nconsole.log('curve name: ' + n);\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.addPoint",
      "symbol": "Curve.addPoint",
      "intent": "mesh",
      "code": "// 1. Build a 2-CV open polyline.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [1, 0, 0]], degree: 1, name: 'addpoint-demo',\n});\n// 2. Append a third CV off to the side.\nawait curve.addPoint([2, 0, 1]);\n// 3. Confirm the CV count grew to 3.\nconsole.log('numPoints: ' + curve.numPoints);\n// 4. Clean up.\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.closed",
      "symbol": "Curve.closed",
      "intent": "mesh",
      "code": "// 1. Build an open polyline.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [1, 0, 0], [1, 0, 1]],\n    degree: 1,\n    closed: false,\n    name: 'closed-demo',\n});\ncurve.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Read the closed flag.\nconsole.log('closed (before): ' + curve.closed);\n// 3. Flip the closed flag.\nawait curve.toggleClosed();\nconsole.log('closed (after): ' + curve.closed);\n// 4. Clean up.\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.degree",
      "symbol": "Curve.degree",
      "intent": "mesh",
      "code": "// 1. Build a cubic Bezier curve.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 1, 0]],\n    degree: 3,\n    closed: false,\n    name: 'degree-demo',\n});\ncurve.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Read the degree.\nconsole.log('degree: ' + curve.degree);\n// 3. Clean up.\nawait curve.delete();\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Curve.extrudeAlong",
      "symbol": "Curve.extrudeAlong",
      "intent": "mesh",
      "code": "// 1. Build a small rectangular profile.\nconst profile = await create.curve({\n    points: [[-0.2, 0, 0], [0.2, 0, 0], [0.2, 0.4, 0], [-0.2, 0.4, 0]],\n    degree: 1,\n    closed: true,\n    name: 'extrude-profile',\n});\n// 2. Sweep the profile along a straight path to build a beam.\nconst beam = await profile.extrudeAlong([[0, 0, 0], [0, 0, 2]]);\n// 3. Translate off origin and log a verifiable property.\nbeam.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('beam vertex count: ' + beam.verts.count);\n// 4. Clean up.\nawait beam.delete();\nawait profile.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.fillBoundary",
      "symbol": "Curve.fillBoundary",
      "intent": "mesh",
      "code": "// 1. Build a closed quad boundary curve.\nconst boundary = await create.curve({\n    points: [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]],\n    degree: 1,\n    closed: true,\n    name: 'boundary-loop',\n});\n// 2. Fill the boundary into a single-face mesh.\nconst cap = await boundary.fillBoundary();\n// 3. Translate off origin and log verifiable properties.\ncap.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('cap vertex count: ' + cap.verts.count);\nconsole.log('cap face count: ' + cap.faces.count);\n// 4. Clean up.\nawait cap.delete();\nawait boundary.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.getPoints",
      "symbol": "Curve.getPoints",
      "intent": "mesh",
      "code": "// 1. Build a smooth closed loop curve.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [2, 0, 0], [2, 0, 2], [0, 0, 2]],\n    degree: 3, closed: true, name: 'getpoints-demo',\n});\n// 2. Sample 32 arc-length-even points along it.\nconst pts = curve.getPoints(32, { arcLength: true });\n// 3. Confirm the requested count came back.\nconsole.log('sampled points: ' + pts.length);\n// 4. Clean up.\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.length",
      "symbol": "Curve.length",
      "intent": "mesh",
      "code": "// 1. Build a straight 3-unit polyline along X.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [3, 0, 0]], degree: 1, name: 'length-demo',\n});\n// 2. Read its arc length.\nconsole.log('length: ' + curve.length.toFixed(2));\n// 3. Clean up.\nawait curve.delete();\n// 4. Observe — see above (≈ 3.00)."
    },
    {
      "kind": "example",
      "id": "example:Curve.numPoints",
      "symbol": "Curve.numPoints",
      "intent": "mesh",
      "code": "// 1. Build a closed square polyline.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]],\n    degree: 1,\n    closed: true,\n    name: 'numpoints-demo',\n});\ncurve.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Read the CV count.\nconsole.log('numPoints: ' + curve.numPoints);\n// 3. Clean up.\nawait curve.delete();\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Curve.pointAt",
      "symbol": "Curve.pointAt",
      "intent": "mesh",
      "code": "// 1. Build a straight 10-unit polyline along X.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [10, 0, 0]], degree: 1, name: 'pointat-demo',\n});\n// 2. Sample the midpoint (arc fraction 0.5).\nconst mid = curve.pointAt(0.5);\n// 3. Confirm it sits ~halfway (x ≈ 5).\nconsole.log('mid x: ' + mid[0].toFixed(2));\n// 4. Clean up.\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.revolveAround",
      "symbol": "Curve.revolveAround",
      "intent": "mesh",
      "code": "// 1. Build a profile curve (silhouette of the lathed body).\nconst profile = await create.curve({\n    points: [[0.5, 0, 0], [1.0, 0.5, 0], [0.5, 1.0, 0]],\n    degree: 1,\n    closed: false,\n    name: 'vase-profile',\n});\n// 2. Revolve around the Y axis with a smooth 24-segment sweep.\nconst vase = await profile.revolveAround('y', {\n    segments: 24,\n    angle: 2 * Math.PI,\n});\n// 3. Translate off origin and log a verifiable property.\nvase.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('vase vertex count: ' + vase.verts.count);\n// 4. Clean up.\nawait vase.delete();\nawait profile.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.setDegree",
      "symbol": "Curve.setDegree",
      "intent": "mesh",
      "code": "// 1. Build a degree-1 polyline with four CVs.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 1, 0]],\n    degree: 1,\n    closed: false,\n    name: 'setdegree-demo',\n});\ncurve.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('degree (before): ' + curve.degree);\n// 2. Upgrade to cubic Bezier for a smooth ride between CVs.\nawait curve.setDegree(3);\n// 3. Confirm.\nconsole.log('degree (after): ' + curve.degree);\n// 4. Clean up.\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.setPoint",
      "symbol": "Curve.setPoint",
      "intent": "mesh",
      "code": "// 1. Build a 4-CV polyline.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]],\n    degree: 1,\n    closed: false,\n    name: 'setpoint-demo',\n});\ncurve.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Lift the second CV upward.\nawait curve.setPoint(1, [1, 0.75, 0]);\n// 3. Confirm CV count unchanged.\nconsole.log('numPoints: ' + curve.numPoints);\n// 4. Clean up.\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.tangentAt",
      "symbol": "Curve.tangentAt",
      "intent": "mesh",
      "code": "// 1. Build a straight polyline heading +X.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [5, 0, 0]], degree: 1, name: 'tangent-demo',\n});\n// 2. Read the unit tangent at the middle.\nconst tan = curve.tangentAt(0.5);\n// 3. Confirm it points along +X (≈ [1,0,0]).\nconsole.log('tangent: ' + JSON.stringify(tan.map((n) => Math.round(n))));\n// 4. Clean up.\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.toggleClosed",
      "symbol": "Curve.toggleClosed",
      "intent": "mesh",
      "code": "// 1. Build an open polyline.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]],\n    degree: 1,\n    closed: false,\n    name: 'toggle-demo',\n});\ncurve.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('closed (before): ' + curve.closed);\n// 2. Toggle the closed flag.\nawait curve.toggleClosed();\n// 3. Confirm.\nconsole.log('closed (after): ' + curve.closed);\n// 4. Clean up.\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.toMesh",
      "symbol": "Curve.toMesh",
      "intent": "mesh",
      "code": "// 1. Build a closed boundary curve.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]],\n    degree: 1, closed: true, name: 'tomesh-demo',\n});\n// 2. Convert it to a capped plate mesh.\nconst mesh = await curve.toMesh();\n// 3. Confirm the new mesh has geometry.\nconsole.log('mesh verts: ' + mesh.verts.count);\n// 4. Clean up both.\nawait mesh.delete();\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.toPipe",
      "symbol": "Curve.toPipe",
      "intent": "mesh",
      "code": "// 1. Build an open path curve.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [0, 0, 1], [0, 0, 2]], degree: 1, name: 'topipe-demo',\n});\n// 2. Sweep a TAPERED, TWISTING tube: thick (0.2 m) at the start, thin\n//    (0.02 m) at the end, with a half-turn (180 deg) spiral, 16 sides.\nconst pipe = await curve.toPipe({\n    startRadius: 0.2, endRadius: 0.02, twistStart: 0, twistEnd: 180,\n    segments: 16, capEnds: true,\n});\n// 3. Confirm the pipe mesh exists.\nconsole.log('pipe verts: ' + pipe.verts.count);\n// 4. Clean up both.\nawait pipe.delete();\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.toPlane",
      "symbol": "Curve.toPlane",
      "intent": "mesh",
      "code": "// 1. Build a closed quad boundary.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]],\n    degree: 1, closed: true, name: 'toplane-demo',\n});\n// 2. Cap it into a single flat n-gon face.\nconst plane = await curve.toPlane();\n// 3. Confirm the capped face exists.\nconsole.log('plane verts: ' + plane.verts.count);\n// 4. Clean up both.\nawait plane.delete();\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:Curve.toRibbon",
      "symbol": "Curve.toRibbon",
      "intent": "mesh",
      "code": "// 1. Build a closed loop curve to act as the ribbon centerline.\n//    degree 3 = smooth cubic so the corners round off; closed = loop it.\nconst pts = [[-3, 0, -3], [3, 0, -3], [3, 0, 3], [-3, 0, 3]];\nconst loop = await create.curve({ points: pts, degree: 3, closed: true, name: 'ribbon-line' });\n// 2. Sweep a banked ribbon along it. width is in working units (cm),\n//    banking rolls into the corners (racetrack feel), samples = density.\nconst ribbon = await loop.toRibbon({ width: 600, banking: true, samples: 48 });\n// 3. Translate off origin and log a verifiable property.\n//    vertex count = 2 per sampled point.\nribbon.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('ribbon vertex count: ' + ribbon.verts.count);\n// 4. Clean up — remove the ribbon mesh and the centerline curve.\nawait ribbon.delete();\nawait loop.delete();"
    },
    {
      "kind": "example",
      "id": "example:EditorBaseApi",
      "symbol": "EditorBaseApi",
      "intent": "scene",
      "code": "// 1. Setup — these members work the same in every editor; nothing to\n//    author (the Previewer has no geometry constructors).\nawait Utils.wait.frame();\n// 2. Action — print the help index for whichever editor you're in.\nconsole.log(help());\n// 3. Read-back — universal scene query, same shape across all editors.\nconst nodes = ls();\nconsole.log('node count is a number: ' + (typeof nodes.length === 'number'));\n// 4. Verify — the universal actions registry returns an array.\nconst actionList = actions.list();\nconsole.log('actions returned an array: ' + Array.isArray(actionList));"
    },
    {
      "kind": "example",
      "id": "example:EditorBaseApi.actions",
      "symbol": "EditorBaseApi.actions",
      "intent": "scene",
      "code": "// 1) List + search for a command.\nconst all = actions.list();\nconsole.log('action count is a number: ' + (typeof all.length === 'number'));\nconst hits = actions.search('frame');\nconsole.log('search returned an array: ' + Array.isArray(hits));"
    },
    {
      "kind": "example",
      "id": "example:EditorBaseApi.delete",
      "symbol": "EditorBaseApi.delete",
      "intent": "scene",
      "code": "// 1. Setup — operate on the loaded asset's meshes (no geometry\n//    constructors in the Previewer); gate on the empty-scene case.\nawait Utils.wait.frame();\nconst meshes = ls({ type: 'mesh' });\nif (meshes.length === 0) {\n    console.log('no meshes to delete — skipped');\n}\nelse {\n    const before = ls().length;\n    const target = meshes[0];\n    // 2. Select — none; deletion goes through the node handle.\n    // 3. Action — delete via the handle directly (bare `delete(...)` is\n    //    reserved JS syntax — resolve a name through ls() first, then\n    //    call .delete() on the handle). Undoable: one Ctrl+Z restores it.\n    await target.delete();\n    // 4. Observe — the scene shrank by one node.\n    const after = ls().length;\n    console.log('deleted ' + target.name + ': ' + before + ' -> ' + after);\n    console.log('exactly one node removed: ' + (before - after === 1));\n}"
    },
    {
      "kind": "example",
      "id": "example:EditorBaseApi.dev",
      "symbol": "EditorBaseApi.dev",
      "intent": "scene",
      "code": "// 1) Report progress from a long-running script.\ndev.progress({ phase: 'rebuild', current: 1, total: 3, label: 'meshing' });\n// 2) Check the abort signal so the script can bail when the user clicks Stop.\nconst aborted = dev.signal?.aborted ?? false;\nconsole.log('aborted flag is boolean: ' + (typeof aborted === 'boolean'));"
    },
    {
      "kind": "example",
      "id": "example:EditorBaseApi.example",
      "symbol": "EditorBaseApi.example",
      "intent": "scene",
      "code": "// 1. Setup — none; example() is a pure read.\n// 2. Action — fetch the runnable walkthrough snippet.\nconst src = example();\n// 3. Read-back — it is a non-empty multi-line string.\nconst lineCount = src.split('\\n').length;\nconsole.log('example length: ' + src.length + ', lines: ' + lineCount);\n// 4. Verify — a real snippet, not a placeholder.\nconsole.log('example is a real snippet: ' + (src.length > 0 && lineCount > 1));"
    },
    {
      "kind": "example",
      "id": "example:EditorBaseApi.help",
      "symbol": "EditorBaseApi.help",
      "intent": "scene",
      "code": "// 1. Setup — none needed; help() is a pure read.\n// 2. Action — fetch the namespace help card.\nconst text = help();\n// 3. Read-back — it is a non-empty string mentioning core members.\nconsole.log('help is non-empty string: ' + (typeof text === 'string' && text.length > 0));\nconsole.log('mentions ls: ' + text.includes('ls'));\n// 4. Verify — help mentions the actions registry too.\nconsole.log('help mentions actions: ' + text.includes('actions'));"
    },
    {
      "kind": "example",
      "id": "example:EditorBaseApi.ls",
      "symbol": "EditorBaseApi.ls",
      "intent": "scene",
      "code": "// 1. Setup — operate on whatever asset is loaded (no geometry\n//    constructors in the Previewer); let any pending load settle.\nawait Utils.wait.frame();\n// 2. Select — none needed; ls is a pure read.\n// 3. Action — list everything, then narrow by type.\nconst all = ls();\nconst meshes = ls({ type: 'mesh' });\n// ...and by selected state:\nconst selected = ls({ selected: true });\n// 4. Observe — every result is a Node[] (counts are numbers).\nconsole.log('all/mesh/selected: ' + all.length + '/' + meshes.length + '/' + selected.length);\nconsole.log('ls returns arrays: ' + (Array.isArray(all) && Array.isArray(meshes)));"
    },
    {
      "kind": "example",
      "id": "example:EditorBaseApi.select",
      "symbol": "EditorBaseApi.select",
      "intent": "scene",
      "code": "// 1. Setup — operate on the loaded asset's meshes (no geometry\n//    constructors in the Previewer); let any pending load settle.\nawait Utils.wait.frame();\nconst meshes = ls({ type: 'mesh' });\n// 2. Select — replace with the first mesh, then compose with modes.\nif (meshes.length > 0) {\n    await select(meshes[0]); // replace (default)\n    if (meshes[1])\n        await select(meshes[1], { mode: 'add' });\n}\n// 3. Action — whole-scene modes (all / invert / clear).\nawait select(null, { mode: 'all', filter: { type: 'mesh' } });\nawait select(null, { mode: 'clear' });\n// 4. Observe — selection is observable via ls({selected:true}); a\n//    clear leaves it empty.\nconsole.log('selected after clear: ' + ls({ selected: true }).length);\nconsole.log('clear emptied selection: ' + (ls({ selected: true }).length === 0));"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDebugApi.example",
      "symbol": "EditorDevDebugApi.example",
      "intent": "scene",
      "code": "console.log('debug example length: ' + dev.debug.example().length);"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDebugApi.getMeshRenderState",
      "symbol": "EditorDevDebugApi.getMeshRenderState",
      "intent": "scene",
      "code": "const s = dev.debug.getMeshRenderState('cube1');\nconsole.log('bvh fresh: ' + (s?.bvhMatchesLiveGeometry ?? false));"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDebugApi.getRendererState",
      "symbol": "EditorDevDebugApi.getRendererState",
      "intent": "scene",
      "code": "const state = dev.debug.getRendererState();\nconsole.log('renderer ready: ' + (state?.ready ?? false));"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDebugApi.help",
      "symbol": "EditorDevDebugApi.help",
      "intent": "scene",
      "code": "console.log('debug help length: ' + dev.debug.help().length);"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDialogsBase",
      "symbol": "EditorDevDialogsBase",
      "intent": "scene",
      "code": "// 1) Inspect the live dialog state from any editor (Model shown here).\nconst before = dev.dialogs.isAnyOpen();\nconsole.log('any dialog open before: ' + before);\n// 2) Open the Help dialog, then verify the probes track it.\nawait dev.dialogs.help.open();\nconsole.log('help isOpen: ' + dev.dialogs.help.isOpen());\nconsole.log('active dialog id: ' + dev.dialogs.active());\nconsole.log('isAnyOpen now: ' + dev.dialogs.isAnyOpen());\nconsole.log('isBlocking now: ' + dev.dialogs.isBlocking());\n// 3) Close it. Re-closing is a no-op.\nawait dev.dialogs.help.close();\nawait dev.dialogs.help.close();\nconsole.log('help isOpen after close: ' + dev.dialogs.help.isOpen());\n// 4) The Import dialog accepts an optional payload for programmatic open.\nawait dev.dialogs.import.open({ filename: 'demo.obj' });\nawait dev.dialogs.import.close();"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDialogsBase.active",
      "symbol": "EditorDevDialogsBase.active",
      "intent": "scene",
      "code": "await dev.dialogs.help.open();\nconsole.log('active id: ' + dev.dialogs.active());\nawait dev.dialogs.help.close();"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDialogsBase.bugReport",
      "symbol": "EditorDevDialogsBase.bugReport",
      "intent": "scene",
      "code": "// Pre-filled bug report — the user reviews, then sends.\nawait dev.dialogs.bugReport.open({\n    title: 'Boolean difference leaves a floating shell',\n    description: 'Steps: 1) create two cubes 2) difference\\nExpected: one solid\\nActual: two separated shells',\n    reportType: 'bug',\n});"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDialogsBase.export",
      "symbol": "EditorDevDialogsBase.export",
      "intent": "scene",
      "code": "await dev.dialogs.export.open();\nconsole.log('export isOpen: ' + dev.dialogs.export.isOpen());\nawait dev.dialogs.export.close();"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDialogsBase.help",
      "symbol": "EditorDevDialogsBase.help",
      "intent": "scene",
      "code": "await dev.dialogs.help.open();\nconsole.log('help isOpen: ' + dev.dialogs.help.isOpen());\nawait dev.dialogs.help.close();"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDialogsBase.import",
      "symbol": "EditorDevDialogsBase.import",
      "intent": "scene",
      "code": "await dev.dialogs.import.open({ filename: 'demo.obj' });\nconsole.log('import isOpen: ' + dev.dialogs.import.isOpen());\nawait dev.dialogs.import.close();"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDialogsBase.isAnyOpen",
      "symbol": "EditorDevDialogsBase.isAnyOpen",
      "intent": "scene",
      "code": "console.log('isAnyOpen before: ' + dev.dialogs.isAnyOpen());\nawait dev.dialogs.settings.open();\nconsole.log('isAnyOpen after open: ' + dev.dialogs.isAnyOpen());\nawait dev.dialogs.settings.close();"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDialogsBase.isBlocking",
      "symbol": "EditorDevDialogsBase.isBlocking",
      "intent": "scene",
      "code": "console.log('isBlocking: ' + dev.dialogs.isBlocking());"
    },
    {
      "kind": "example",
      "id": "example:EditorDevDialogsBase.settings",
      "symbol": "EditorDevDialogsBase.settings",
      "intent": "scene",
      "code": "await dev.dialogs.settings.open();\nconsole.log('settings isOpen: ' + dev.dialogs.settings.isOpen());\nawait dev.dialogs.settings.close();"
    },
    {
      "kind": "example",
      "id": "example:EditorDevOutlinerApi.clearFilter",
      "symbol": "EditorDevOutlinerApi.clearFilter",
      "intent": "scene",
      "code": "dev.outliner?.setFilter('demo');\ndev.outliner?.clearFilter();\nconsole.log('outliner filter cleared');"
    },
    {
      "kind": "example",
      "id": "example:EditorDevOutlinerApi.collapseAll",
      "symbol": "EditorDevOutlinerApi.collapseAll",
      "intent": "scene",
      "code": "// 1. Setup — expand first so collapse has something to fold.\nawait Utils.wait.frame();\ndev.outliner?.expandAll();\nconsole.log('node rows: ' + ls().length);\n// 2. Action — collapse every outliner row to its parent.\ndev.outliner?.collapseAll();\n// 3. Read-back — the outliner namespace resolved.\nconsole.log('outliner present: ' + (dev.outliner !== undefined));\n// 4. Verify — collapseAll returned without throwing.\nconsole.log('collapseAll completed');"
    },
    {
      "kind": "example",
      "id": "example:EditorDevOutlinerApi.example",
      "symbol": "EditorDevOutlinerApi.example",
      "intent": "scene",
      "code": "console.log('outliner example length: ' + (dev.outliner?.example().length ?? 0));"
    },
    {
      "kind": "example",
      "id": "example:EditorDevOutlinerApi.expandAll",
      "symbol": "EditorDevOutlinerApi.expandAll",
      "intent": "scene",
      "code": "// 1. Setup — operate on whatever asset is loaded so expand/collapse has\n//    rows to act on; let any pending load settle.\nawait Utils.wait.frame();\nconsole.log('node rows: ' + ls().length);\n// 2. Action — expand every outliner row, then collapse them again.\ndev.outliner?.expandAll();\ndev.outliner?.collapseAll();\n// 3. Read-back — the outliner namespace resolved (undefined in editors\n//    without an outliner panel, e.g. Animation).\nconsole.log('outliner present: ' + (dev.outliner !== undefined));\n// 4. Verify — both calls returned without throwing.\nconsole.log('expandAll + collapseAll round-trip completed');"
    },
    {
      "kind": "example",
      "id": "example:EditorDevOutlinerApi.help",
      "symbol": "EditorDevOutlinerApi.help",
      "intent": "scene",
      "code": "console.log('outliner help length: ' + (dev.outliner?.help().length ?? 0));"
    },
    {
      "kind": "example",
      "id": "example:EditorDevOutlinerApi.setDisplayMode",
      "symbol": "EditorDevOutlinerApi.setDisplayMode",
      "intent": "scene",
      "code": "dev.outliner?.setDisplayMode({ showShapes: true, showTypes: true });\nconsole.log('outliner display mode updated');\ndev.outliner?.setDisplayMode({ showTypes: false });"
    },
    {
      "kind": "example",
      "id": "example:EditorDevOutlinerApi.setFilter",
      "symbol": "EditorDevOutlinerApi.setFilter",
      "intent": "scene",
      "code": "dev.outliner?.setFilter('cube');\nconsole.log('outliner filter set to: cube');\ndev.outliner?.clearFilter();"
    },
    {
      "kind": "example",
      "id": "example:Euler",
      "symbol": "Euler",
      "intent": "scene",
      "code": "// 1. Setup - pure math, no scene needed.\nconst e = new Euler(0, Math.PI / 2, 0);\n// 2. Select - N/A for pure math.\n// 3. Action - read the tuple form.\nconst tuple = e.toArray();\n// 4. Observe.\nconsole.log('Euler[1]: ' + tuple[1].toFixed(3));"
    },
    {
      "kind": "example",
      "id": "example:Euler.clone",
      "symbol": "Euler.clone",
      "intent": "math",
      "code": "const a = new Euler(0, 0, Math.PI / 2, 'XYZ');\nconst b = a.clone();\nb.x = 1;\nconsole.log('a unchanged: ' + (a.x === 0));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Euler.equals",
      "symbol": "Euler.equals",
      "intent": "math",
      "code": "const a = new Euler(0, 0, Math.PI / 2, 'XYZ');\nconst b = new Euler(0, 0, Math.PI / 2 + 1e-9, 'XYZ');\nconsole.log('approx equal: ' + a.equals(b));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Euler.fromMat4",
      "symbol": "Euler.fromMat4",
      "intent": "math",
      "code": "const m = MathLib.Mat4.fromQuat(MathLib.Quat.fromAxisAngle([0, 1, 0], Math.PI / 3));\nconst e = Euler.fromMat4(m, 'XYZ');\nconsole.log('y radians: ' + e.y.toFixed(4));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Euler.fromQuat",
      "symbol": "Euler.fromQuat",
      "intent": "math",
      "code": "const q = MathLib.Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\nconst e = Euler.fromQuat(q, 'XYZ');\nconsole.log('z radians: ' + e.z.toFixed(4));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Euler.order",
      "symbol": "Euler.order",
      "intent": "math",
      "code": "const e = new Euler(0, 0, Math.PI / 2, 'ZYX');\nconsole.log('order: ' + e.order);\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Euler.reorder",
      "symbol": "Euler.reorder",
      "intent": "math",
      "code": "const e1 = new Euler(0, 0, Math.PI / 2, 'XYZ');\nconst e2 = e1.reorder('ZYX');\nconsole.log('new order: ' + e2.order);\n// The geometric rotation is preserved; only the (x, y, z) values change.\nconst same = e1.toQuat().equals(e2.toQuat(), 1e-4);\nconsole.log('rotations equal: ' + same);\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Euler.toArray",
      "symbol": "Euler.toArray",
      "intent": "math",
      "code": "const e = new Euler(0, 0, Math.PI / 2, 'XYZ');\nconst tuple = e.toArray();\nconsole.log('tuple length: ' + tuple.length);\nconsole.log('order slot: ' + tuple[3]);\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Euler.toMat4",
      "symbol": "Euler.toMat4",
      "intent": "math",
      "code": "const e = new Euler(0, Math.PI / 2, 0, 'XYZ');\nconst m = e.toMat4();\nconst rotated = m.transformVector([1, 0, 0]);\nconst rounded = rotated.toArray().map(n => Math.round(n * 1000) / 1000);\nconsole.log('rotated: ' + JSON.stringify(rounded));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Euler.toQuat",
      "symbol": "Euler.toQuat",
      "intent": "math",
      "code": "const e = new Euler(0, 0, Math.PI / 2, 'XYZ');\nconst q = e.toQuat();\nconsole.log('w: ' + q.w.toFixed(4));\n// Round-trip back to Euler should match (within tolerance).\nconst eBack = q.toEuler('XYZ');\nconsole.log('round-trip z: ' + eBack.z.toFixed(4));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Euler.x",
      "symbol": "Euler.x",
      "intent": "math",
      "code": "const e = new Euler(Math.PI / 2, 0, 0, 'XYZ');\nconsole.log('x: ' + e.x.toFixed(4));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Euler.y",
      "symbol": "Euler.y",
      "intent": "math",
      "code": "const e = new Euler(0, Math.PI / 2, 0, 'XYZ');\nconsole.log('y: ' + e.y.toFixed(4));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Euler.z",
      "symbol": "Euler.z",
      "intent": "math",
      "code": "const e = new Euler(0, 0, Math.PI / 2, 'XYZ');\nconsole.log('z: ' + e.z.toFixed(4));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:ExecOpenScadApiOptions",
      "symbol": "ExecOpenScadApiOptions",
      "intent": "scene",
      "code": "// 1. Setup — a parametric source whose top-of-file variable is overridable.\nconst src = 'standoff_h = 12; cylinder(h = standoff_h, d = 6, $fn = 32);';\n// 2. Action — build the options bag with one differing override.\nconst opts = { params: { standoff_h: 20 } };\n// 3. Read-back — enumerate the override keys the bag carries.\nconst keys = Object.keys(opts.params);\n// 4. Verify — exactly one numeric override targeting a declared variable.\nconsole.log('override ok: ' + (keys.length === 1 && typeof opts.params.standoff_h === 'number' && src.includes(keys[0])));"
    },
    {
      "kind": "example",
      "id": "example:ExecOpenScadApiOptions.params",
      "symbol": "ExecOpenScadApiOptions.params",
      "intent": "scene",
      "code": "// 1. Setup — the source declares two customizer variables.\nconst declared = ['wall', 'vented'];\n// 2. Action — override a subset with mixed value types.\nconst params = { wall: 2.4, vented: true };\n// 3. Read-back — check each override against the declared set.\nconst known = Object.keys(params).filter((k) => declared.includes(k));\n// 4. Verify — every override targets a declared variable (unknowns would throw).\nconsole.log('params valid: ' + (known.length === 2 && typeof params.wall === 'number' && typeof params.vented === 'boolean'));"
    },
    {
      "kind": "example",
      "id": "example:ExportFbxOptions.ascii",
      "symbol": "ExportFbxOptions.ascii",
      "intent": "scene",
      "code": "// 1. Setup — export the loaded asset; gate on an empty scene.\nawait Utils.wait.frame();\nif (ls({ type: 'mesh' }).length === 0) {\n    console.log('no asset loaded — ASCII FBX export skipped');\n}\nelse {\n    // 2. Action — export human-readable ASCII FBX.\n    const result = await io.export('ascii-demo.fbx', 'fbx', { ascii: true, acknowledgeNoAnimation: true });\n    // 3. Read-back — confirm the ASCII file came out non-empty.\n    console.log('ASCII FBX = ' + result.size + ' bytes -> ' + result.filename);\n    // 4. Verify — a real Blob came back.\n    console.log('blob is a Blob: ' + (result.blob instanceof Blob));\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportGlbOptions.embedAnimations",
      "symbol": "ExportGlbOptions.embedAnimations",
      "intent": "scene",
      "code": "// 1. Setup — embedAnimations is meaningful only with clips loaded.\nawait Utils.wait.frame();\nconst clips = animation.listClips();\nif (ls({ type: 'mesh' }).length === 0) {\n    console.log('no asset loaded — embedded-animation export skipped');\n}\nelse {\n    // 2. Action — export GLB with animation embedded inline.\n    const result = await io.export('embedded.glb', 'glb', { poseMode: clips.length ? 'animation' : 'bindpose', acknowledgeNoAnimation: clips.length === 0, embedAnimations: true });\n    // 3. Read-back — confirm the GLB binary is non-empty.\n    console.log('GLB (embedded animations) = ' + result.size + ' bytes -> ' + result.filename);\n    // 4. Verify — a real Blob came back.\n    console.log('blob is a Blob: ' + (result.blob instanceof Blob));\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportObjOptions.writeMtl",
      "symbol": "ExportObjOptions.writeMtl",
      "intent": "scene",
      "code": "// 1. Setup — export the loaded asset; gate on an empty scene.\nawait Utils.wait.frame();\nif (ls({ type: 'mesh' }).length === 0) {\n    console.log('no asset loaded — OBJ+mtl export skipped');\n}\nelse {\n    // 2. Action — export OBJ with a companion .mtl material file.\n    const result = await io.export('mtl-demo.obj', 'obj', { writeMtl: true });\n    // 3. Read-back — confirm the OBJ file came out non-empty.\n    console.log('OBJ (+mtl) = ' + result.size + ' bytes -> ' + result.filename);\n    // 4. Verify — a real Blob came back.\n    console.log('blob is a Blob: ' + (result.blob instanceof Blob));\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportOptionsCommon.acknowledgeNoAnimation",
      "symbol": "ExportOptionsCommon.acknowledgeNoAnimation",
      "intent": "scene",
      "code": "// 1. Setup — a scene WITH clips refuses a silent bind-pose export.\nawait Utils.wait.frame();\nconst clips = animation.listClips();\nconsole.log('scene has ' + clips.length + ' clip(s) that bind-pose export would strip');\nif (ls({ type: 'mesh' }).length === 0) {\n    console.log('no asset loaded — bind-pose export skipped');\n}\nelse {\n    // 2. Action — acknowledge the deliberate clip-less (bind-pose) export.\n    const result = await io.export('bind-only.fbx', 'fbx', { poseMode: 'bindpose', acknowledgeNoAnimation: true });\n    // 3. Read-back — confirm the bind-pose file came out non-empty.\n    console.log('acknowledged bind-pose export: ' + result.size + ' bytes');\n    // 4. Verify — without the flag the guard would have thrown.\n    console.log('blob is a Blob: ' + (result.blob instanceof Blob));\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportOptionsCommon.animationClipId",
      "symbol": "ExportOptionsCommon.animationClipId",
      "intent": "scene",
      "code": "// 1. Setup — resolve a REAL clip id from the loaded asset's clips.\nconst clips = animation.listClips();\nif (clips.length === 0) {\n    console.log('no clips loaded — load an animated asset first');\n}\nelse {\n    const clipId = clips[0].id;\n    // 2. Action — export just that one clip to FBX.\n    const result = await io.export('one-clip.fbx', 'fbx', { poseMode: 'animation', animationClipId: clipId });\n    // 3. Read-back — confirm a non-empty file came out.\n    console.log('clip ' + clips[0].name + ' exported: ' + result.size + ' bytes');\n    // 4. Verify — a real Blob came back.\n    console.log('returned a Blob: ' + (result.blob instanceof Blob));\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportOptionsCommon.animationClipIds",
      "symbol": "ExportOptionsCommon.animationClipIds",
      "intent": "scene",
      "code": "// 1. Setup — collect every real clip id from the loaded asset.\nconst clips = animation.listClips();\nif (clips.length < 1) {\n    console.log('no clips loaded — load an animated asset first');\n}\nelse {\n    const ids = clips.slice(0, 2).map((c) => c.id);\n    // 2. Action — export the chosen clip SUBSET to one GLB.\n    const result = await io.export('subset.glb', 'glb', { poseMode: 'animation', animationClipIds: ids });\n    // 3. Read-back — confirm the multi-clip file is non-empty.\n    console.log('exported ' + ids.length + ' clip(s): ' + result.size + ' bytes');\n    // 4. Verify — a real Blob came back.\n    console.log('blob is a Blob: ' + (result.blob instanceof Blob));\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportOptionsCommon.animationClipName",
      "symbol": "ExportOptionsCommon.animationClipName",
      "intent": "scene",
      "code": "// 1. Setup — read a real clip NAME from the loaded asset.\nconst clips = animation.listClips();\nif (clips.length === 0) {\n    console.log('no clips loaded — load an animated asset first');\n}\nelse {\n    const name = clips[0].name;\n    // 2. Action — export the clip selected BY NAME.\n    const result = await io.export('by-name.fbx', 'fbx', { poseMode: 'animation', animationClipName: name });\n    // 3. Read-back — confirm output size.\n    console.log('clip \"' + name + '\" exported: ' + result.size + ' bytes');\n    // 4. Verify — the filename came back stamped.\n    console.log('filename: ' + result.filename);\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportOptionsCommon.animationClipNames",
      "symbol": "ExportOptionsCommon.animationClipNames",
      "intent": "scene",
      "code": "// 1. Setup — read real clip names from the loaded asset.\nconst clips = animation.listClips();\nif (clips.length === 0) {\n    console.log('no clips loaded — load an animated asset first');\n}\nelse {\n    const names = clips.slice(0, 2).map((c) => c.name);\n    // 2. Action — export the named subset to one GLB.\n    const result = await io.export('named-subset.glb', 'glb', { poseMode: 'animation', animationClipNames: names });\n    // 3. Read-back — confirm the file is non-empty.\n    console.log('exported clips [' + names.join(', ') + ']: ' + result.size + ' bytes');\n    // 4. Verify — the filename came back.\n    console.log('filename: ' + result.filename);\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportOptionsCommon.axis",
      "symbol": "ExportOptionsCommon.axis",
      "intent": "scene",
      "code": "// 1. Setup — export the loaded asset; gate on an empty scene (the\n//    Previewer has no geometry constructors).\nawait Utils.wait.frame();\nif (ls({ type: 'mesh' }).length === 0) {\n    console.log('no asset loaded — axis-override export skipped');\n}\nelse {\n    // 2. Action — export GLB with a Y-up axis override.\n    const result = await io.export('axis-demo.glb', 'glb', { axis: 'y', acknowledgeNoAnimation: true });\n    // 3. Read-back — the real return shape is {blob, filename, size}.\n    console.log('exported ' + result.size + ' bytes to ' + result.filename);\n    // 4. Verify — a real Blob came back.\n    console.log('blob is a Blob: ' + (result.blob instanceof Blob));\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportOptionsCommon.currentFrameTime",
      "symbol": "ExportOptionsCommon.currentFrameTime",
      "intent": "scene",
      "code": "// 1. Setup — scrub the transport so currentframe captures a real pose.\nanimation.currentTime(1.5);\nawait Utils.wait.frame();\nif (ls({ type: 'mesh' }).length === 0) {\n    console.log('no asset loaded — current-frame export skipped');\n}\nelse {\n    // 2. Action — export the pose at the cursor (1.5s).\n    const result = await io.export('frame-demo.glb', 'glb', { poseMode: 'currentframe', currentFrameTime: 1.5 });\n    // 3. Read-back — confirm the documented {blob, filename, size} shape.\n    console.log('current-frame export: ' + result.filename + ' (' + result.size + ' bytes)');\n    // 4. Verify — a real Blob came back.\n    console.log('blob is a Blob: ' + (result.blob instanceof Blob));\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportOptionsCommon.exportAllClips",
      "symbol": "ExportOptionsCommon.exportAllClips",
      "intent": "scene",
      "code": "// 1. Setup — count the clips that should all land in one FBX.\nconst clips = animation.listClips();\nconsole.log('clips to export: ' + clips.length);\nif (clips.length === 0) {\n    console.log('no clips loaded — load an animated asset first');\n}\nelse {\n    // 2. Action — export EVERY clip into one FBX (all takes).\n    const result = await io.export('all-takes.fbx', 'fbx', { poseMode: 'animation', exportAllClips: true });\n    // 3. Read-back — confirm the combined file is non-empty.\n    console.log('all-clips export: ' + result.size + ' bytes -> ' + result.filename);\n    // 4. Verify — a real Blob came back.\n    console.log('blob is a Blob: ' + (result.blob instanceof Blob));\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportOptionsCommon.poseMode",
      "symbol": "ExportOptionsCommon.poseMode",
      "intent": "scene",
      "code": "// 1. Setup — bind-pose export needs an asset loaded; gate the empty case.\nawait Utils.wait.frame();\nif (ls({ type: 'mesh' }).length === 0) {\n    console.log('no asset loaded — poseMode export skipped');\n}\nelse {\n    // 2. Action — export the bind pose deliberately (no clips required).\n    const result = await io.export('pose-demo.glb', 'glb', { poseMode: 'bindpose', acknowledgeNoAnimation: true });\n    // 3. Read-back — confirm the documented {blob, filename, size} shape.\n    console.log('bindpose export: ' + result.filename + ' (' + result.size + ' bytes)');\n    // 4. Verify — a real Blob came back.\n    console.log('blob is a Blob: ' + (result.blob instanceof Blob));\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportPlyOptions.binary",
      "symbol": "ExportPlyOptions.binary",
      "intent": "scene",
      "code": "// 1. Setup — export the loaded asset; gate on an empty scene.\nawait Utils.wait.frame();\nif (ls({ type: 'mesh' }).length === 0) {\n    console.log('no asset loaded — binary PLY export skipped');\n}\nelse {\n    // 2. Action — export a binary PLY.\n    const result = await io.export('binary-demo.ply', 'ply', { binary: true });\n    // 3. Read-back — confirm the binary PLY came out non-empty.\n    console.log('binary PLY = ' + result.size + ' bytes -> ' + result.filename);\n    // 4. Verify — a real Blob came back.\n    console.log('blob is a Blob: ' + (result.blob instanceof Blob));\n}"
    },
    {
      "kind": "example",
      "id": "example:ExportStlOptions.binary",
      "symbol": "ExportStlOptions.binary",
      "intent": "scene",
      "code": "// 1. Setup — export the loaded asset; gate on an empty scene.\nawait Utils.wait.frame();\nif (ls({ type: 'mesh' }).length === 0) {\n    console.log('no asset loaded — binary STL export skipped');\n}\nelse {\n    // 2. Action — export a binary STL.\n    const result = await io.export('binary-demo.stl', 'stl', { binary: true });\n    // 3. Read-back — confirm the binary STL came out non-empty.\n    console.log('binary STL = ' + result.size + ' bytes -> ' + result.filename);\n    // 4. Verify — a real Blob came back.\n    console.log('blob is a Blob: ' + (result.blob instanceof Blob));\n}"
    },
    {
      "kind": "example",
      "id": "example:FillHoleOpts.boundaryFromComponent",
      "symbol": "FillHoleOpts.boundaryFromComponent",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'fillhole-boundary-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.fillHole({ boundaryFromComponent: { edgeIndices: [0] } });\nconsole.log('after fillHole — faces: ' + cube.faces.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:FillHoleSession.example",
      "symbol": "FillHoleSession.example",
      "intent": "scene",
      "code": "// 1) Build a cube and open a fill-hole session.\nconst cube = await create.cube({ name: 'fillhole-example-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.fillHole({}));\n// 2) Render the example snippet.\nconst snippet = session.example();\nconsole.log('example length: ' + snippet.length);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:FillHoleSession.getBoundaryIds",
      "symbol": "FillHoleSession.getBoundaryIds",
      "intent": "scene",
      "code": "// 1) Build a cube and open a fill-hole session.\nconst cube = await create.cube({ name: 'fillhole-boundary-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.fillHole({}));\n// 2) Read the auto-detected boundary loop ids (null when no hole present).\nconst boundary = session.getBoundaryIds();\nconsole.log('boundary verts: ' + (boundary ? boundary.length : 0));\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:FillHoleSession.help",
      "symbol": "FillHoleSession.help",
      "intent": "scene",
      "code": "// 1) Build a cube and open a fill-hole session.\nconst cube = await create.cube({ name: 'fillhole-help-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.fillHole({}));\n// 2) Render the help string.\nconst helpText = session.help();\nconsole.log('help length: ' + helpText.length);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:FillHoleSession.placeFace",
      "symbol": "FillHoleSession.placeFace",
      "intent": "scene",
      "code": "// 1) Build a cube and open a fill-hole session.\nconst cube = await create.cube({ name: 'fillhole-placeface-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.fillHole({}));\n// 2) Read the auto-detected boundary loop ids; bail if no hole found.\nconst boundary = session.getBoundaryIds();\nif (boundary && boundary.length >= 3) {\n    session.placeFace([boundary[0], boundary[1], boundary[2]]);\n    console.log('placeFace tri added');\n}\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:FillHoleSession.placeVertex",
      "symbol": "FillHoleSession.placeVertex",
      "intent": "scene",
      "code": "// 1) Build a cube and open a fill-hole session.\nconst cube = await create.cube({ name: 'fillhole-placevert-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.fillHole({}));\n// 2) placeVertex([clientX, clientY]) drops an interior vertex — but it is an\n//    INTERACTIVE-TOOL-DRIVEN step: it raycasts the live render mesh in the\n//    open viewport to land the point on the surface, so it only runs against\n//    a mounted, settled viewport (a ray miss is a silent no-op; a fresh /\n//    off-screen mesh that hasn't rendered yet has no scene mesh to hit). The\n//    headless-valid demonstration is to open the session and confirm it is live.\nconsole.log('fill-hole session active: ' + session.isActive); // → true\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Group",
      "symbol": "Group",
      "intent": "scene",
      "code": "// 1. Setup — create children + a group.\nconst child = await create.cube({ name: 'demo-child' });\nchild.xform({ t: [2.5, 1.3, -0.7] });\nconst group = await create.group([child], { name: 'demo-group' });\n// 2. Select — N/A; we have the handle.\n// 3. Action — read the group's children.\nconst kids = group.children();\n// 4. Observe + cleanup.\nconsole.log('group child count: ' + kids.length);\nawait group.delete();"
    },
    {
      "kind": "example",
      "id": "example:Group.ungroup",
      "symbol": "Group.ungroup",
      "intent": "scene",
      "code": "// 1. Build a group with two cubes.\nconst a = await create.cube({ width: 1, height: 1, depth: 1, name: 'kidA' });\na.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ width: 1, height: 1, depth: 1, name: 'kidB' });\nb.xform({ t: [-2.0, 1.3, 0.7] });\nconst group = await create.group([a, b], { name: 'dissolve-demo' });\nconsole.log('child count before ungroup: ' + group.children().length);\n// 2. Ungroup — children survive at the scene root.\nawait group.ungroup();\nawait Utils.wait.frame();\n// 3. Verify the children are still there.\nconst aAfter = ls(a.nodeId)[0];\nconst bAfter = ls(b.nodeId)[0];\nconsole.log('child a survives: ' + (aAfter !== null));\nconsole.log('child b survives: ' + (bAfter !== null));\n// 4. Clean up the loose children.\nif (aAfter !== null)\n    await aAfter.delete();\nif (bAfter !== null)\n    await bAfter.delete();"
    },
    {
      "kind": "example",
      "id": "example:InteractiveToolSession.addInput",
      "symbol": "InteractiveToolSession.addInput",
      "intent": "scene",
      "code": "// 1. Create a cube, translate, open a knife session.\nconst cube = await create.cube({ name: 'addInput-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.knife({ snapMode: 'edge' });\n// 2. addInput is the generic form; most tools expose typed sugar\n//    (e.g. session.addPoint([x, y])). The shape of TInput varies by tool\n//    (knife wants a [clientX, clientY] pixel tuple). Feeding inputs is an\n//    INTERACTIVE-TOOL-DRIVEN step: it raycasts the live render mesh in the\n//    open viewport, so it only runs against a mounted, settled viewport —\n//    not in a headless / fresh-mesh snapshot. Here we open + inspect the\n//    session without driving input so the demo is self-contained.\nconsole.log('opened, still open: ' + session.isActive); // → true\n// 3. Discard the session + clean up.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:InteractiveToolSession.cancel",
      "symbol": "InteractiveToolSession.cancel",
      "intent": "scene",
      "code": "// 1. Cube + translate.\nconst cube = await create.cube({ name: 'cancel-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Open the session, then change your mind. cancel() reverts any\n//    in-progress preview and produces ZERO undo entries.\nconst session = await cube.tools.knife({ snapMode: 'edge' });\nawait session.cancel();\nconsole.log('state after cancel: ' + session.state); // → 'cancelled'\n// 3. cancel() is idempotent; a second call is a safe no-op.\nawait session.cancel();\n// 4. Clean up the demo cube.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:InteractiveToolSession.commit",
      "symbol": "InteractiveToolSession.commit",
      "intent": "scene",
      "code": "// 1. Cube + translate.\nconst cube = await create.cube({ name: 'commit-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Open the knife session, feed it any tool-specific inputs your\n//    workflow needs, then commit() to finalize as ONE undo entry.\nconst session = await cube.tools.knife({ snapMode: 'edge' });\ntry {\n    await session.commit();\n    console.log('state after commit: ' + session.state); // → 'committed'\n}\ncatch (e) {\n    // commit() rolls the group back + transitions to 'cancelled' on error.\n    console.log('commit failed: ' + e.message);\n}\n// 3. Clean up the demo cube.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:InteractiveToolSession.isActive",
      "symbol": "InteractiveToolSession.isActive",
      "intent": "scene",
      "code": "// 1. Cube + translate.\nconst cube = await create.cube({ name: 'isActive-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. isActive flips false the moment the session reaches a terminal state.\nconst session = await cube.tools.knife({ snapMode: 'edge' });\nconsole.log('isActive at open: ' + session.isActive); // → true\nawait session.cancel();\nconsole.log('isActive after cancel: ' + session.isActive); // → false\n// 3. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:InteractiveToolSession.mesh",
      "symbol": "InteractiveToolSession.mesh",
      "intent": "scene",
      "code": "// 1. Cube + translate.\nconst cube = await create.cube({ name: 'mesh-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. session.mesh re-resolves the target Mesh each call; use it to\n//    chain queries (bbox, attrs, etc.) while the session is open.\nconst session = await cube.tools.knife({ snapMode: 'edge' });\nconsole.log('same handle: ' + (session.mesh.nodeId === cube.nodeId));\n// 3. Discard + clean up.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:InteractiveToolSession.meshId",
      "symbol": "InteractiveToolSession.meshId",
      "intent": "scene",
      "code": "// 1. Spawn a cube + translate so it isn't at origin.\nconst cube = await create.cube({ name: 'meshId-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Open a session; meshId equals the cube's underlying node id.\nconst session = await cube.tools.knife({ snapMode: 'edge' });\nconsole.log('meshId matches: ' + (session.meshId === cube.nodeId));\n// 3. Discard + clean up.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:InteractiveToolSession.preview",
      "symbol": "InteractiveToolSession.preview",
      "intent": "scene",
      "code": "// 1. Create a cube + open a knife session.\nconst cube = await create.cube({ name: 'preview-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.knife({ snapMode: 'edge' });\n// 2. preview() refreshes the in-progress overlay without committing.\n//    Default impl is a no-op; SlicePlane / Bisect override the hook.\nsession.preview({});\nconsole.log('still open: ' + session.isActive); // → true\n// 3. Discard + clean up.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:InteractiveToolSession.start",
      "symbol": "InteractiveToolSession.start",
      "intent": "scene",
      "code": "// 1. Set up a cube to slice.\nconst cube = await create.cube({ name: 'start-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Open the session. addInput auto-calls start() — explicit start()\n//    is only needed when you want the history group open immediately.\nconst session = await cube.tools.knife({ snapMode: 'edge' });\nsession.start();\nconsole.log('state after start: ' + session.state); // → 'open'\n// 3. Cancel + clean up (no commits demonstrated here).\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:InteractiveToolSession.state",
      "symbol": "InteractiveToolSession.state",
      "intent": "scene",
      "code": "// 1. Cube + translate.\nconst cube = await create.cube({ name: 'state-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Open the session; state transitions open → committed | cancelled.\nconst session = await cube.tools.knife({ snapMode: 'edge' });\nconsole.log('state at open: ' + session.state); // → 'open'\nawait session.cancel();\nconsole.log('state after cancel: ' + session.state); // → 'cancelled'\n// 3. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:InteractiveToolSession.toolId",
      "symbol": "InteractiveToolSession.toolId",
      "intent": "scene",
      "code": "// 1. Create a cube + position it away from origin.\nconst cube = await create.cube({ name: 'toolId-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Open a knife session; toolId reveals which tool you're running.\nconst session = await cube.tools.knife({ snapMode: 'edge' });\nconsole.log('toolId: ' + session.toolId); // → 'knife'\n// 3. Discard + clean up.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:IoResidualApi.batchConvert",
      "symbol": "IoResidualApi.batchConvert",
      "intent": "scene",
      "code": "// 1. Convert a picked folder of files to GLB.\nconst result = await io.batchConvert({ from: { folder: true }, to: { folder: true, format: 'glb' } });\nconsole.log('converted ' + result.succeeded.length + ' of ' + result.total);\n// 2. Report any failures.\nconsole.log('failed count = ' + result.failed.length);\n// 3. Convert only OBJ inputs, naming outputs by a pattern.\nconst objs = await io.batchConvert({ from: { folder: true, accept: '.obj' }, to: { folder: true, format: 'glb', namePattern: '{name}.glb' } });\nconsole.log('obj→glb succeeded = ' + objs.succeeded.length);\n// 4. Detect a user-cancelled run.\nconsole.log('cancelled = ' + result.cancelled);"
    },
    {
      "kind": "example",
      "id": "example:IoResidualApi.clearWorkspaces",
      "symbol": "IoResidualApi.clearWorkspaces",
      "intent": "scene",
      "code": "// 1. Clear every workspace grant.\nawait io.clearWorkspaces();\nconsole.log('all workspaces cleared');\n// 2. The list is now empty.\nconst remaining = await io.getWorkspaces();\nconsole.log('remaining count = ' + remaining.length);\n// 3. Confirm the list is genuinely empty.\nconsole.log('is empty = ' + (remaining.length === 0));\n// 4. Clearing again is a safe idempotent no-op.\nawait io.clearWorkspaces();\nconsole.log('second clear completed');"
    },
    {
      "kind": "example",
      "id": "example:IoResidualApi.getFormatCapabilities",
      "symbol": "IoResidualApi.getFormatCapabilities",
      "intent": "scene",
      "code": "// 1. Inspect what GLB preserves.\nconst glbCaps = io.getFormatCapabilities('glb');\nconsole.log('glb caps = ' + JSON.stringify(glbCaps));\n// 2. GLB carries animation; OBJ does not.\nconst objCaps = io.getFormatCapabilities('obj');\nconsole.log('glb animation = ' + glbCaps.animation + ', obj animation = ' + objCaps.animation);\n// 3. Warn before a lossy material export to STL.\nconst stlCaps = io.getFormatCapabilities('stl');\nconsole.log('stl keeps materials = ' + stlCaps.materials);\n// 4. Every format preserves geometry by definition.\nconsole.log('glb keeps geometry = ' + glbCaps.geometry);"
    },
    {
      "kind": "example",
      "id": "example:IoResidualApi.getSupportedExportFormats",
      "symbol": "IoResidualApi.getSupportedExportFormats",
      "intent": "scene",
      "code": "// 1. Read the supported export formats as JSON.\nconst exportFormats = io.getSupportedExportFormats();\nconsole.log('export formats = ' + JSON.stringify(exportFormats));\n// 2. Count how many formats are available.\nconsole.log('export format count = ' + exportFormats.length);\n// 3. Check whether GLB export is available.\nconsole.log('glb exportable = ' + exportFormats.includes('glb'));\n// 4. Pick the first format as a default target.\nconsole.log('default export = ' + exportFormats[0]);"
    },
    {
      "kind": "example",
      "id": "example:IoResidualApi.getSupportedImportFormats",
      "symbol": "IoResidualApi.getSupportedImportFormats",
      "intent": "scene",
      "code": "// 1. Read the supported import formats as JSON.\nconst importFormats = io.getSupportedImportFormats();\nconsole.log('import formats = ' + JSON.stringify(importFormats));\n// 2. Count how many formats are supported.\nconsole.log('import format count = ' + importFormats.length);\n// 3. Check whether a specific format is importable.\nconsole.log('glb importable = ' + importFormats.includes('glb'));\n// 4. Confirm the strings are undotted (no leading '.').\nconsole.log('undotted = ' + importFormats.every(f => !f.startsWith('.')));"
    },
    {
      "kind": "example",
      "id": "example:IoResidualApi.getWorkspaces",
      "symbol": "IoResidualApi.getWorkspaces",
      "intent": "scene",
      "code": "// 1. Read all current workspace grants.\nconst workspaces = await io.getWorkspaces();\nconsole.log('workspace count = ' + workspaces.length);\n// 2. Print just the display paths.\nconsole.log('paths = ' + JSON.stringify(workspaces.map(w => w.displayPath)));\n// 3. Detect whether any folder has been granted yet.\nconsole.log('has any workspace = ' + (workspaces.length > 0));\n// 4. Find the most-recently-used grant.\nconst recent = workspaces.slice().sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0];\nconsole.log('most recent = ' + (recent ? recent.displayPath : 'none'));"
    },
    {
      "kind": "example",
      "id": "example:IoResidualApi.modified",
      "symbol": "IoResidualApi.modified",
      "intent": "scene",
      "code": "// 1. Read the dirty flag on a fresh scene.\nio.newScene();\nconsole.log('modified after newScene = ' + io.modified);\n// 2. Make an edit, then re-read it.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1 });\nconsole.log('modified after edit = ' + io.modified);\n// 3. Branch on the flag to decide whether to warn the user.\nconsole.log(io.modified ? 'has unsaved changes' : 'nothing to save');\n// 4. Clean up the cube we created.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:IoResidualApi.path",
      "symbol": "IoResidualApi.path",
      "intent": "scene",
      "code": "// 1. Read the current asset path (empty on a fresh scene).\nio.newScene();\nconsole.log('path after newScene = \"' + io.path + '\"');\n// 2. Detect whether anything is loaded from disk.\nconsole.log('has a file = ' + (io.path.length > 0));\n// 3. Derive a default save-as name from the current path.\nconst suggested = io.path || 'untitled.abasset';\nconsole.log('suggested save name = ' + suggested);\n// 4. The accessor always returns a string (never null/undefined).\nconsole.log('is string = ' + (typeof io.path === 'string'));"
    },
    {
      "kind": "example",
      "id": "example:IoResidualApi.removeWorkspace",
      "symbol": "IoResidualApi.removeWorkspace",
      "intent": "scene",
      "code": "// 1. Remove a specific workspace grant.\nawait io.removeWorkspace('D:\\\\Project');\nconsole.log('removed D:\\\\Project');\n// 2. Verify it is gone from the list.\nconst after = await io.getWorkspaces();\nconsole.log('remaining count = ' + after.length);\n// 3. Confirm the removed path is no longer present.\nconsole.log('still present = ' + after.some(w => w.displayPath === 'D:\\\\Project'));\n// 4. Removing an unknown path is a safe no-op.\nawait io.removeWorkspace('D:\\\\Nonexistent');\nconsole.log('no-op remove completed');"
    },
    {
      "kind": "example",
      "id": "example:IoResidualApi.setWorkspace",
      "symbol": "IoResidualApi.setWorkspace",
      "intent": "scene",
      "code": "// 1. Grant a folder, then list the result (user picks the folder once).\nconst grant = await io.setWorkspace('D:\\\\Project');\nconsole.log('granted = ' + JSON.stringify(grant && grant.displayPath));\n// 2. Grant a second folder, hinting the picker to start in Documents.\nconst docs = await io.setWorkspace('D:\\\\Assets', { startIn: 'documents' });\nconsole.log('second grant = ' + (docs ? 'ok' : 'cancelled'));\n// 3. After granting, the workspace appears in the list.\nconst workspaces = await io.getWorkspaces();\nconsole.log('workspace count = ' + workspaces.length);\n// 4. With the grant in place, absolute paths now resolve in openFile.\nconsole.log('ready to open absolute paths under D:\\\\Project = ' + (grant !== null));"
    },
    {
      "kind": "example",
      "id": "example:Joint",
      "symbol": "Joint",
      "intent": "scene",
      "code": "// 1. Setup — create a joint in the Control Rig editor.\nconst joint = await create.joint({ name: 'demo-joint' });\n// 2. Select — N/A; we have the handle.\n// 3. Action — translate the joint.\njoint.xform({ t: [2.5, 1.3, -0.7] });\n// 4. Observe + cleanup.\nconsole.log('joint name: ' + joint.name);\nawait joint.delete();"
    },
    {
      "kind": "example",
      "id": "example:Joint.bindMesh",
      "symbol": "Joint.bindMesh",
      "intent": "scene",
      "code": "// 1. Build a cube and a joint in the Control Rig editor.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'skin-target' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst joint = await create.joint({ name: 'Root' });\n// 2. Bind the joint to the cube as a single-influence skin.\nawait joint.bindMesh(cube);\n// 3. Verify the skin cluster landed.\nconst deformers = cube.deformers.stack();\nconsole.log('deformer count: ' + deformers.length);\n// 4. Clean up.\nawait cube.delete();\nawait joint.delete();"
    },
    {
      "kind": "example",
      "id": "example:KnifeOpts.snapMode",
      "symbol": "KnifeOpts.snapMode",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'knife-snap-demo' });\nconst session = await cube.knife({ snapMode: 'edge' });\nawait session?.cancel?.();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:KnifeSession.addPoint",
      "symbol": "KnifeSession.addPoint",
      "intent": "scene",
      "code": "// 1) Build a cube and open a knife session.\nconst cube = await create.cube({ name: 'knife-addpoint-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.knife({ snapMode: 'edge' }));\n// 2) addPoint([clientX, clientY]) appends a cut point — but it is an\n//    INTERACTIVE-TOOL-DRIVEN step: it raycasts the live render mesh in the\n//    open viewport to snap onto an edge, so it only runs against a mounted,\n//    settled viewport (a ray miss is a silent no-op; a fresh / off-screen\n//    mesh that hasn't rendered yet has no scene mesh to hit). The headless-\n//    valid demonstration is to open the session and confirm it is live.\nconsole.log('knife session active: ' + session.isActive); // → true\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:KnifeSession.example",
      "symbol": "KnifeSession.example",
      "intent": "scene",
      "code": "// 1) Build a cube and open a knife session.\nconst cube = await create.cube({ name: 'knife-example-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.knife({ snapMode: 'edge' }));\n// 2) Render the example snippet.\nconst snippet = session.example();\nconsole.log('example length: ' + snippet.length);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:KnifeSession.help",
      "symbol": "KnifeSession.help",
      "intent": "scene",
      "code": "// 1) Build a cube and open a knife session.\nconst cube = await create.cube({ name: 'knife-help-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.knife({ snapMode: 'edge' }));\n// 2) Render the help string.\nconst helpText = session.help();\nconsole.log('help length: ' + helpText.length);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Locator",
      "symbol": "Locator",
      "intent": "scene",
      "code": "// 1. Setup — create a locator.\nconst loc = await create.locator({ name: 'demo-loc' });\nloc.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Select — N/A; we have the handle.\n// 3. Action — read its position.\nconst x = loc.translate.x.get();\n// 4. Observe + cleanup.\nconsole.log('locator x: ' + x.toFixed(3));\nawait loc.delete();"
    },
    {
      "kind": "example",
      "id": "example:LoopCutOpts.cuts",
      "symbol": "LoopCutOpts.cuts",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'loopcut-cuts-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\ntry {\n    await cube.loopCut({ edgeIndex: 0, cuts: 2 });\n}\ncatch (e) {\n    console.log('loopCut wired: ' + (e instanceof Error));\n}\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:LoopCutOpts.edgeIndex",
      "symbol": "LoopCutOpts.edgeIndex",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'loopcut-edgeindex-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\ntry {\n    await cube.loopCut({ edgeIndex: 0, cuts: 1 });\n}\ncatch (e) {\n    console.log('loopCut wired: ' + (e instanceof Error));\n}\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mat3",
      "symbol": "Mat3",
      "intent": "scene",
      "code": "// 1. Setup — pure math, no scene needed.\nconst m = Mat3.identity();\n// 2. Select — N/A for pure math.\n// 3. Action — read the tuple form.\nconst tuple = m.toArray();\n// 4. Observe.\nconsole.log('Mat3 identity[0]: ' + tuple[0]);"
    },
    {
      "kind": "example",
      "id": "example:Mat3.clone",
      "symbol": "Mat3.clone",
      "intent": "math",
      "code": "// 1. Setup — start from the identity matrix.\nconst a = Mat3.identity();\n// 2. Select — N/A for pure math.\n// 3. Action — clone produces an independent handle.\nconst b = a.clone();\n// 4. Observe — clone equals the original component-wise.\nconsole.log('equal: ' + a.equals(b));"
    },
    {
      "kind": "example",
      "id": "example:Mat3.determinant",
      "symbol": "Mat3.determinant",
      "intent": "math",
      "code": "// 1. Setup — pure math, no scene needed.\nconst id = Mat3.identity();\n// 2. Select — N/A for pure math.\n// 3. Action — read the determinant.\nconst det = id.determinant();\n// 4. Observe — identity has determinant 1.\nconsole.log('identity det: ' + det);"
    },
    {
      "kind": "example",
      "id": "example:Mat3.equals",
      "symbol": "Mat3.equals",
      "intent": "math",
      "code": "// 1. Setup — two identity matrices.\nconst a = Mat3.identity();\nconst b = Mat3.identity();\n// 2. Select — N/A for pure math.\n// 3. Action — component-wise compare with default tolerance.\nconst same = a.equals(b);\n// 4. Observe — fresh identities compare equal.\nconsole.log('equal: ' + same);"
    },
    {
      "kind": "example",
      "id": "example:Mat3.from",
      "symbol": "Mat3.from",
      "intent": "math",
      "code": "// 1. Setup — pack identity components into a flat buffer.\nconst buf = [1, 0, 0, 0, 1, 0, 0, 0, 1];\n// 2. Select — N/A for pure math.\n// 3. Action — unpack the buffer into a Mat3.\nconst m = Mat3.from(buf);\n// 4. Observe — round-trip preserves identity.\nconsole.log('is identity: ' + m.equals(Mat3.identity()));"
    },
    {
      "kind": "example",
      "id": "example:Mat3.fromMat4",
      "symbol": "Mat3.fromMat4",
      "intent": "math",
      "code": "// 1. Setup — build a 4x4 scale matrix.\nconst m4 = MathLib.Mat4.fromScale([2, 3, 4]);\n// 2. Select — N/A for pure math.\n// 3. Action — drop translation row/column, keeping rotation+scale.\nconst m3 = Mat3.fromMat4(m4);\n// 4. Observe — determinant equals product of scale factors (2*3*4 = 24).\nconsole.log('determinant: ' + m3.determinant());"
    },
    {
      "kind": "example",
      "id": "example:Mat3.fromQuat",
      "symbol": "Mat3.fromQuat",
      "intent": "math",
      "code": "// 1. Setup — 90 degree rotation around Z.\nconst q = MathLib.Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\n// 2. Select — N/A for pure math.\n// 3. Action — build the rotation matrix and apply to X axis.\nconst m = Mat3.fromQuat(q);\nconst v = m.transformVec3([1, 0, 0]);\n// 4. Observe — [1,0,0] rotates onto +Y axis.\nconst rounded = v.toArray().map(n => Math.round(n * 1000) / 1000);\nconsole.log('rotated: ' + JSON.stringify(rounded));"
    },
    {
      "kind": "example",
      "id": "example:Mat3.identity",
      "symbol": "Mat3.identity",
      "intent": "math",
      "code": "// 1. Setup — pure math, no scene needed.\n// 2. Select — N/A for pure math.\n// 3. Action — get the identity matrix and apply it to a vector.\nconst id = Mat3.identity();\nconst v = id.transformVec3([2, 3, 5]);\n// 4. Observe — identity leaves the vector unchanged.\nconsole.log('unchanged: ' + JSON.stringify(v.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:Mat3.inverse",
      "symbol": "Mat3.inverse",
      "intent": "math",
      "code": "// 1. Setup — 60 degree rotation around Y.\nconst m = Mat3.fromQuat(MathLib.Quat.fromAxisAngle([0, 1, 0], Math.PI / 3));\n// 2. Select — N/A for pure math.\n// 3. Action — compute the inverse and round-trip via multiplication.\nconst inv = m.inverse();\nconst round = m.multiply(inv);\n// 4. Observe — m * m^-1 returns identity within float tolerance.\nconsole.log('m * inv approx identity: ' + round.equals(Mat3.identity(), 1e-4));"
    },
    {
      "kind": "example",
      "id": "example:Mat3.multiply",
      "symbol": "Mat3.multiply",
      "intent": "math",
      "code": "// 1. Setup — build a rotation-only matrix.\nconst q = MathLib.Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\nconst r = Mat3.fromQuat(q);\n// 2. Select — N/A for pure math.\n// 3. Action — compose r with identity (right-multiply).\nconst same = r.multiply(Mat3.identity());\n// 4. Observe — multiplying by identity is a no-op.\nconsole.log('identity preserves: ' + same.equals(r));"
    },
    {
      "kind": "example",
      "id": "example:Mat3.normalMatrix",
      "symbol": "Mat3.normalMatrix",
      "intent": "math",
      "code": "// 1. Setup — non-uniform scale matrix (sx=2, sy=1, sz=1).\nconst m4 = MathLib.Mat4.fromScale([2, 1, 1]);\n// 2. Select — N/A for pure math.\n// 3. Action — build the inverse-transpose normal matrix and apply to +Y normal.\nconst nm = Mat3.normalMatrix(m4);\nconst n = nm.transformVec3([0, 1, 0]);\n// 4. Observe — Y component preserved because scale is X-only.\nconsole.log('normal y: ' + n.y.toFixed(4));"
    },
    {
      "kind": "example",
      "id": "example:Mat3.toArray",
      "symbol": "Mat3.toArray",
      "intent": "math",
      "code": "// 1. Setup — pure math, no scene needed.\nconst m = Mat3.identity();\n// 2. Select — N/A for pure math.\n// 3. Action — pack into a flat 9-element column-major buffer.\nconst elements = m.toArray();\n// 4. Observe — identity has 1s on the diagonal.\nconsole.log('elements: ' + JSON.stringify(elements));"
    },
    {
      "kind": "example",
      "id": "example:Mat3.transformVec3",
      "symbol": "Mat3.transformVec3",
      "intent": "math",
      "code": "// 1. Identity transform leaves the vector unchanged.\nconst id = Mat3.identity();\nconst same = id.transformVec3([2, 3, 5]);\nconsole.log('unchanged: ' + JSON.stringify(same.toArray()));\n// 2. Rotation transforms direction.\nconst r = Mat3.fromQuat(MathLib.Quat.fromAxisAngle([0, 0, 1], Math.PI / 2));\nconst rotated = r.transformVec3([1, 0, 0]);\nconst rounded = rotated.toArray().map(n => Math.round(n * 1000) / 1000);\nconsole.log('rotated: ' + JSON.stringify(rounded));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Mat3.transpose",
      "symbol": "Mat3.transpose",
      "intent": "math",
      "code": "// 1. Setup — 45 degree rotation around Z (pure rotation).\nconst r = Mat3.fromQuat(MathLib.Quat.fromAxisAngle([0, 0, 1], Math.PI / 4));\n// 2. Select — N/A for pure math.\n// 3. Action — for rotation matrices, transpose == inverse.\nconst round = r.multiply(r.transpose());\n// 4. Observe — r * r^T returns identity within float tolerance.\nconsole.log('r * rT approx identity: ' + round.equals(Mat3.identity(), 1e-4));"
    },
    {
      "kind": "example",
      "id": "example:Mat4",
      "symbol": "Mat4",
      "intent": "scene",
      "code": "// 1. Setup - pure math, no scene needed.\nconst m = Mat4.identity();\n// 2. Select - N/A for pure math.\n// 3. Action - build a translation matrix via the static factory.\nconst t = Mat4.fromTranslation([2, 3, 4]);\n// 4. Observe.\nconst tuple = t.toArray();\nconsole.log('identity[0]: ' + m.toArray()[0]);\nconsole.log('translation: ' + JSON.stringify([tuple[12], tuple[13], tuple[14]]));"
    },
    {
      "kind": "example",
      "id": "example:Mat4.clone",
      "symbol": "Mat4.clone",
      "intent": "math",
      "code": "const a = Mat4.identity();\nconst b = a.clone();\nconsole.log('equal: ' + a.equals(b));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.decompose",
      "symbol": "Mat4.decompose",
      "intent": "math",
      "code": "const m = Mat4.fromTRS([5, 0, 0], MathLib.Quat.identity(), [2, 2, 2]);\nconst { translation, rotation, scale } = m.decompose();\nconsole.log('translation: ' + JSON.stringify(translation.toArray()));\nconsole.log('scale: ' + JSON.stringify(scale.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.determinant",
      "symbol": "Mat4.determinant",
      "intent": "math",
      "code": "const id = Mat4.identity();\nconsole.log('identity det: ' + id.determinant());\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.equals",
      "symbol": "Mat4.equals",
      "intent": "math",
      "code": "const a = Mat4.identity();\nconst b = Mat4.identity();\nconsole.log('equal: ' + a.equals(b));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.from",
      "symbol": "Mat4.from",
      "intent": "math",
      "code": "const m = Mat4.from([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);\nconsole.log('is identity: ' + m.equals(Mat4.identity()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.fromQuat",
      "symbol": "Mat4.fromQuat",
      "intent": "math",
      "code": "const q = MathLib.Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\nconst m = Mat4.fromQuat(q);\nconst v = m.transformVector([1, 0, 0]);\nconst rounded = v.toArray().map(n => Math.round(n * 1000) / 1000);\nconsole.log('rotated: ' + JSON.stringify(rounded));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.fromRotation",
      "symbol": "Mat4.fromRotation",
      "intent": "math",
      "code": "const q = MathLib.Quat.fromAxisAngle([1, 0, 0], Math.PI / 4);\nconst m = Mat4.fromRotation(q);\nconsole.log('determinant: ' + m.determinant().toFixed(4));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.fromScale",
      "symbol": "Mat4.fromScale",
      "intent": "math",
      "code": "const m = Mat4.fromScale([2, 3, 4]);\nconst v = m.transformPoint([1, 1, 1]);\nconsole.log('scaled: ' + JSON.stringify(v.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.fromTranslation",
      "symbol": "Mat4.fromTranslation",
      "intent": "math",
      "code": "const m = Mat4.fromTranslation([10, 0, 0]);\nconst p = m.transformPoint([0, 0, 0]);\nconsole.log('moved: ' + JSON.stringify(p.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.fromTRS",
      "symbol": "Mat4.fromTRS",
      "intent": "math",
      "code": "const m = Mat4.fromTRS([5, 0, 0], MathLib.Quat.fromAxisAngle([0, 1, 0], Math.PI / 2), [2, 2, 2]);\nconst { translation, rotation, scale } = m.decompose();\nconsole.log('translation: ' + JSON.stringify(translation.toArray()));\nconsole.log('scale: ' + JSON.stringify(scale.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.identity",
      "symbol": "Mat4.identity",
      "intent": "math",
      "code": "const id = Mat4.identity();\nconst p = id.transformPoint([3, 5, 7]);\nconsole.log('unchanged: ' + JSON.stringify(p.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.inverse",
      "symbol": "Mat4.inverse",
      "intent": "math",
      "code": "const m = Mat4.fromTranslation([5, 0, 0]);\nconst inv = m.inverse();\nconst round = m.multiply(inv);\nconsole.log('m * inv approx identity: ' + round.equals(Mat4.identity(), 1e-4));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.lookAt",
      "symbol": "Mat4.lookAt",
      "intent": "math",
      "code": "const m = Mat4.lookAt([0, 5, 10], [0, 0, 0], [0, 1, 0]);\nconst { translation } = m.decompose();\nconsole.log('eye: ' + JSON.stringify(translation.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.multiply",
      "symbol": "Mat4.multiply",
      "intent": "math",
      "code": "// 1. Two translations compose into a single translation.\nconst t1 = Mat4.fromTranslation([5, 0, 0]);\nconst t2 = Mat4.fromTranslation([0, 3, 0]);\nconst combined = t1.multiply(t2);\nconst p = combined.transformPoint([0, 0, 0]);\nconsole.log('combined: ' + JSON.stringify(p.toArray()));\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.ortho",
      "symbol": "Mat4.ortho",
      "intent": "math",
      "code": "const m = Mat4.ortho(-10, 10, -10, 10, 0.1, 1000);\nconsole.log('determinant non-zero: ' + (m.determinant() !== 0));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.perspective",
      "symbol": "Mat4.perspective",
      "intent": "math",
      "code": "const m = Mat4.perspective(Math.PI / 4, 16 / 9, 0.1, 1000);\nconsole.log('determinant non-zero: ' + (m.determinant() !== 0));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.toArray",
      "symbol": "Mat4.toArray",
      "intent": "math",
      "code": "const m = Mat4.identity();\nconsole.log('length: ' + m.toArray().length);\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.transformDirection",
      "symbol": "Mat4.transformDirection",
      "intent": "math",
      "code": "const q = MathLib.Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\nconst m = Mat4.fromQuat(q);\nconst d = m.transformDirection([1, 0, 0]);\nconsole.log('length approx 1: ' + d.length().toFixed(4));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.transformPoint",
      "symbol": "Mat4.transformPoint",
      "intent": "math",
      "code": "const m = Mat4.fromTranslation([5, 0, 0]);\nconst p = m.transformPoint([0, 0, 0]);\nconsole.log('moved: ' + JSON.stringify(p.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.transformVec4",
      "symbol": "Mat4.transformVec4",
      "intent": "math",
      "code": "const m = Mat4.fromTranslation([5, 0, 0]);\n// w = 1 -> point, translation applies.\nconst p = m.transformVec4([0, 0, 0, 1]);\nconsole.log('point: ' + JSON.stringify(p.toArray()));\n// w = 0 -> direction, translation discarded.\nconst d = m.transformVec4([1, 0, 0, 0]);\nconsole.log('direction: ' + JSON.stringify(d.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.transformVector",
      "symbol": "Mat4.transformVector",
      "intent": "math",
      "code": "// Translation does NOT contribute to a transformed vector.\nconst m = Mat4.fromTranslation([100, 0, 0]);\nconst v = m.transformVector([1, 0, 0]);\nconsole.log('vector unchanged: ' + JSON.stringify(v.toArray()));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Mat4.transpose",
      "symbol": "Mat4.transpose",
      "intent": "math",
      "code": "const m = Mat4.identity();\nconst t = m.transpose();\nconsole.log('identity transposed equals identity: ' + t.equals(m));\n// 1. Setup - see above.\n// 2. Select - N/A or see above.\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Material",
      "symbol": "Material",
      "intent": "scene",
      "code": "// 1. Setup — create a material (universal: works in Model + Previewer).\nconst mat = await create.material({ name: 'material-demo', baseColor: [0.8, 0.3, 0.1, 1] });\n// 2. Action — edit a PBR field through the canonical pbr setter (async + undoable).\nawait mat.pbr.metallic.set(0.6);\n// 3. Read-back — the handle exposes name + the live edited value.\nconsole.log('material: ' + mat.name + ', metallic: ' + mat.metallic);\n// 4. Verify + cleanup.\nconsole.log('edit landed: ' + (Math.abs(mat.metallic - 0.6) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.alphaCutoff",
      "symbol": "Material.alphaCutoff",
      "intent": "mesh",
      "code": "// 1. Setup — create a material; alphaCutoff is only meaningful when\n//    alphaMode is 'mask', so switch the mode before writing the cutoff.\nconst mat = await create.material({ name: 'alphaCutoff-getter-demo' });\nawait mat.pbr.alphaMode.set('mask');\nconsole.log('initial alphaCutoff: ' + mat.alphaCutoff);\n// 2. Action — drive a NEW cutoff through the canonical pbr setter (async + undoable).\nawait mat.pbr.alphaCutoff.set(0.25);\n// 3. Read-back — the top-level convenience getter reflects the write.\nconst v = mat.alphaCutoff;\nconsole.log('alphaCutoff after set(0.25): ' + v);\n// 4. Verify + cleanup.\nconsole.log('getter round-trip OK: ' + (Math.abs(v - 0.25) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.alphaMode",
      "symbol": "Material.alphaMode",
      "intent": "mesh",
      "code": "// 1. Setup — create a material that starts opaque.\nconst mat = await create.material({ name: 'alphaMode-getter-demo' });\nconsole.log('initial alphaMode: ' + mat.alphaMode);\n// 2. Action — drive a NEW mode through the canonical pbr setter (async + undoable).\nawait mat.pbr.alphaMode.set('blend');\n// 3. Read-back — the top-level convenience getter reflects the write.\nconst v = mat.alphaMode;\nconsole.log('alphaMode after set: ' + v);\n// 4. Verify + cleanup.\nconsole.log('getter round-trip OK: ' + (v === 'blend'));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.assignTo",
      "symbol": "Material.assignTo",
      "intent": "mesh",
      "code": "// 1. Setup — spawn a mesh and a NAMED material (name-first; no UUID capture).\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'assign-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait create.material({ name: 'Steel', baseColor: [0.6, 0.6, 0.65, 1], metallic: 1, roughness: 0.3 });\n// 2. Select — N/A; assignTo binds material to a specific mesh handle.\n// 3. Action — resolve the material BY NAME, then assign (equivalent to\n//    `cube.material.set(getMaterial('Steel'))`).\nconst mat = getMaterial('Steel');\nawait mat.assignTo(cube);\nconst onMesh = cube.material.get();\n// 4. Observe + cleanup.\nconsole.log('assigned material name matches: ' + (onMesh !== null && onMesh.name === 'Steel'));\nmat.delete();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.baseColor",
      "symbol": "Material.baseColor",
      "intent": "mesh",
      "code": "// 1. Setup — create a material with a known starting base color.\nconst mat = await create.material({ name: 'baseColor-getter-demo', baseColor: [1, 1, 1, 1] });\nconsole.log('initial baseColor: ' + JSON.stringify(mat.baseColor));\n// 2. Action — drive a NEW color through the canonical pbr setter (async + undoable).\nawait mat.pbr.baseColor.set([0.8, 0.2, 0.4, 1]);\n// 3. Read-back — the top-level convenience getter reflects the write.\nconst v = mat.baseColor;\nconsole.log('baseColor after set: ' + JSON.stringify(v));\n// 4. Verify + cleanup.\nconsole.log('getter round-trip OK: ' + (Math.abs(v[0] - 0.8) < 1e-6 && Math.abs(v[1] - 0.2) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.delete",
      "symbol": "Material.delete",
      "intent": "mesh",
      "code": "// 1. Setup — create a material and assign it to a mesh.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'mat-del-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst mat = await create.material({ name: 'delete-demo' });\nawait mat.assignTo(cube);\n// 2. Select — N/A; delete() targets the material handle directly.\n// 3. Action — delete the material; the mesh falls back to the default swatch.\nmat.delete();\nconst lookup = getMaterial('delete-demo');\nconst onMesh = cube.material.get();\n// 4. Observe + cleanup.\nconsole.log('lookup after delete is null: ' + (lookup === null));\nconsole.log('mesh material reset to default: ' + (onMesh === null || onMesh.id !== mat.materialId));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.doubleSided",
      "symbol": "Material.doubleSided",
      "intent": "mesh",
      "code": "// 1. Setup — create a material (single-sided by default).\nconst mat = await create.material({ name: 'doubleSided-getter-demo' });\nconst before = mat.doubleSided;\nconsole.log('initial doubleSided: ' + before);\n// 2. Action — flip it through the canonical pbr setter (async + undoable).\nawait mat.pbr.doubleSided.set(!before);\n// 3. Read-back — the top-level convenience getter reflects the write.\nconst v = mat.doubleSided;\nconsole.log('doubleSided after flip: ' + v);\n// 4. Verify + cleanup.\nconsole.log('getter round-trip OK: ' + (v === !before));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.duplicate",
      "symbol": "Material.duplicate",
      "intent": "mesh",
      "code": "// 1. Create a source material with a distinctive color.\nconst src = await create.material({\n    name: 'dup-source',\n    baseColor: [0.4, 0.7, 0.2, 1],\n    roughness: 0.3,\n});\n// 2. Duplicate it under a chosen name (async + undoable).\nconst copy = await src.duplicate({ name: 'dup-copy' });\nconsole.log('copy name: ' + copy.name);\nconsole.log('copy roughness matches: ' + (copy.pbr.roughness.get() === src.pbr.roughness.get()));\n// 3. Mutating the copy leaves the source untouched.\nawait copy.pbr.roughness.set(0.9);\nconsole.log('source roughness unchanged: ' + (src.pbr.roughness.get() === 0.3));\n// 4. Clean up.\ncopy.delete();\nsrc.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.emissive",
      "symbol": "Material.emissive",
      "intent": "mesh",
      "code": "// 1. Setup — create a material with a black starting emissive.\nconst mat = await create.material({ name: 'emissive-getter-demo' });\nconsole.log('initial emissive: ' + JSON.stringify(mat.emissive));\n// 2. Action — drive a NEW emissive through the canonical pbr setter (async + undoable).\nawait mat.pbr.emissive.set([0.1, 0.4, 0.9]);\n// 3. Read-back — the top-level convenience getter reflects the write.\nconst v = mat.emissive;\nconsole.log('emissive after set: ' + JSON.stringify(v));\n// 4. Verify + cleanup.\nconsole.log('getter round-trip OK: ' + (Math.abs(v[2] - 0.9) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.emissiveIntensity",
      "symbol": "Material.emissiveIntensity",
      "intent": "mesh",
      "code": "// 1. Setup — create a material with a default emissive intensity.\nconst mat = await create.material({ name: 'emissiveIntensity-getter-demo' });\nconsole.log('initial emissiveIntensity: ' + mat.emissiveIntensity);\n// 2. Action — drive a NEW value through the canonical pbr setter (async + undoable).\nawait mat.pbr.emissiveIntensity.set(2.5);\n// 3. Read-back — the top-level convenience getter reflects the write.\nconst v = mat.emissiveIntensity;\nconsole.log('emissiveIntensity after set(2.5): ' + v);\n// 4. Verify + cleanup.\nconsole.log('getter round-trip OK: ' + (Math.abs(v - 2.5) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.equals",
      "symbol": "Material.equals",
      "intent": "mesh",
      "code": "// 1. Setup — create a material, then grab a SECOND handle to the same\n//    material via lookup. getMaterial(idOrName) returns a\n//    fresh Material handle (or null) — distinct object, same underlying id.\nconst mat = await create.material({ name: 'equals-demo' });\nconst same = getMaterial('equals-demo');\n// 2. Select — N/A for asset-level material.\n// 3. Action — compare against the second handle AND a different material.\nconst other = await create.material({ name: 'equals-other' });\nconst selfEq = same !== null && mat.equals(same);\nconst diffEq = mat.equals(other);\n// 4. Observe + cleanup.\nconsole.log('two handles to same material are equal: ' + selfEq); // true\nconsole.log('different materials are not equal: ' + (!diffEq)); // true\nmat.delete();\nother.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.id",
      "symbol": "Material.id",
      "intent": "mesh",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'id-alias-demo' });\n// 2. Select — N/A for asset-level material.\n// 3. Action — read both ids.\nconst a = mat.id;\nconst b = mat.materialId;\n// 4. Observe + cleanup.\nconsole.log('id matches materialId: ' + (a === b));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.materialId",
      "symbol": "Material.materialId",
      "intent": "mesh",
      "code": "// 1. Setup — create a material and capture its id.\nconst mat = await create.material({\n    name: 'mat-id-demo',\n    baseColor: [0.6, 0.2, 0.8, 1],\n});\nconst savedId = mat.materialId;\n// 2. Select — N/A; Material lives at asset level, not in the scene tree.\n// 3. Action — renaming does not change the id.\nawait mat.setName('renamed');\n// 4. Observe + cleanup.\nconsole.log('id stable across rename: ' + (mat.materialId === savedId));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.mergeDuplicates",
      "symbol": "Material.mergeDuplicates",
      "intent": "mesh",
      "code": "// 1. Create a couple of materials whose names collide (the second is\n//    auto-disambiguated, e.g. 'merge-demo1', so both names share the\n//    'merge-demo' root the merge collapses). listMaterials()\n//    returns the asset's material summaries (plain DTOs with .name).\nconst a = await create.material({ name: 'merge-demo', baseColor: [1, 0, 0, 1] });\nconst b = await create.material({ name: 'merge-demo', baseColor: [0, 1, 0, 1] });\nconst before = listMaterials().filter((m) => m.name.startsWith('merge-demo')).length;\nconsole.log('merge-demo count before: ' + before);\n// 2. Run the merge via the runtime API surface (async — no args; operates\n//    on the active asset). In the Script Editor the merge is invoked\n//    through actions.run(...): the bare `Material` class is\n//    NOT a runtime global, only the `ModelEditor` namespace is bound.\nawait actions.run('object.mergeDuplicateMaterials');\n// 3. Verify the duplicates collapsed.\nconst after = listMaterials().filter((m) => m.name.startsWith('merge-demo')).length;\n// 4. Clean up whatever survived.\ntry {\n    a.delete();\n}\ncatch { }\ntry {\n    b.delete();\n}\ncatch { }\nconsole.log('merge-demo count after: ' + after + ' (merged ' + (before - after) + ' duplicate material(s))');"
    },
    {
      "kind": "example",
      "id": "example:Material.metallic",
      "symbol": "Material.metallic",
      "intent": "mesh",
      "code": "// 1. Setup — create a material with a known starting metallic.\nconst mat = await create.material({ name: 'metallic-getter-demo', metallic: 0.1 });\nconsole.log('initial metallic: ' + mat.metallic);\n// 2. Action — drive a NEW value through the canonical pbr setter (async + undoable).\nawait mat.pbr.metallic.set(0.85);\n// 3. Read-back — the top-level convenience getter reflects the write.\nconst v = mat.metallic;\nconsole.log('metallic after set(0.85): ' + v);\n// 4. Verify + cleanup.\nconsole.log('getter round-trip OK: ' + (Math.abs(v - 0.85) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.name",
      "symbol": "Material.name",
      "intent": "mesh",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'name-demo' });\n// 2. Select — N/A; Material lives at asset level.\n// 3. Action — read its display name.\nconst n = mat.name;\n// 4. Observe + cleanup.\nconsole.log('name: ' + n);\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.pbr",
      "symbol": "Material.pbr",
      "intent": "mesh",
      "code": "// 1. Create a material to tweak.\nconst mat = await create.material({ name: 'pbr-demo' });\n// 2. Set every field on the pbr namespace (each .set is async + undoable).\nawait mat.pbr.baseColor.set([0.8, 0.3, 0.1, 1]);\nawait mat.pbr.metallic.set(0.2);\nawait mat.pbr.roughness.set(0.4);\nawait mat.pbr.emissive.set([0.1, 0.05, 0.0]);\nawait mat.pbr.emissiveIntensity.set(1.5);\nawait mat.pbr.alphaMode.set('mask');\nawait mat.pbr.alphaCutoff.set(0.25);\nawait mat.pbr.doubleSided.set(true);\n// 3. Read each field back to verify (synchronous .get()).\nconsole.log('roughness: ' + mat.pbr.roughness.get());\nconsole.log('alphaMode: ' + mat.pbr.alphaMode.get());\nconsole.log('doubleSided: ' + mat.pbr.doubleSided.get());\n// 4. Clean up.\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.props",
      "symbol": "Material.props",
      "intent": "mesh",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'material-props-demo' });\n// 2. Action — change two fields (async + undoable).\nawait mat.pbr.roughness.set(0.9);\nawait mat.pbr.metallic.set(0.4);\n// 3. Read-back — one snapshot reflects both writes.\nconst p = mat.props;\nconsole.log('roughness: ' + p.roughness + ' metallic: ' + p.metallic);\n// 4. Verify + cleanup.\nconsole.log('props reflect edits: ' + (p.roughness === 0.9 && p.metallic === 0.4));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.removeUnused",
      "symbol": "Material.removeUnused",
      "intent": "mesh",
      "code": "// 1. Create a material and leave it unassigned to any mesh.\n//    getMaterial(idOrName) returns the live Material handle,\n//    or null when no material matches.\nconst orphan = await create.material({ name: 'unused-demo' });\nconst before = getMaterial('unused-demo');\nconsole.log('orphan exists before: ' + (before !== null)); // true\n// 2. Sweep unused materials via the runtime API surface (async — no args).\n//    In the Script Editor the sweep is invoked through\n//    actions.run(...): the bare `Material` class is NOT a\n//    runtime global, only the `ModelEditor` namespace is bound.\nawait actions.run('object.removeUnusedMaterials');\n// 3. The orphan is gone — getMaterial now returns null.\nconst after = getMaterial('unused-demo');\nconsole.log('orphan exists after: ' + (after !== null)); // false"
    },
    {
      "kind": "example",
      "id": "example:Material.roughness",
      "symbol": "Material.roughness",
      "intent": "mesh",
      "code": "// 1. Setup — create a material with a known starting roughness.\nconst mat = await create.material({ name: 'roughness-getter-demo', roughness: 0.5 });\nconsole.log('initial roughness: ' + mat.roughness);\n// 2. Action — drive a NEW value through the canonical pbr setter (async + undoable).\nawait mat.pbr.roughness.set(0.2);\n// 3. Read-back — the top-level convenience getter reflects the write.\nconst v = mat.roughness;\nconsole.log('roughness after set(0.2): ' + v);\n// 4. Verify + cleanup.\nconsole.log('getter round-trip OK: ' + (Math.abs(v - 0.2) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.setName",
      "symbol": "Material.setName",
      "intent": "mesh",
      "code": "// 1. Create a material and read its starting name.\nconst mat = await create.material({ name: 'name-demo' });\nconsole.log('name before: ' + mat.name);\n// 2. Rename it (async + undoable; the underlying id never changes).\nawait mat.setName('renamed-demo');\nconsole.log('name after: ' + mat.name);\n// 3. Lookup by the NEW name finds it; the old name now returns null.\n//    getMaterial(idOrName) resolves by id OR display name.\nconst lookup = getMaterial('renamed-demo');\nconsole.log('found by new name: ' + (lookup !== null));\nconsole.log('old name gone: ' + (getMaterial('name-demo') === null));\n// 4. Clean up.\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.textures",
      "symbol": "Material.textures",
      "intent": "mesh",
      "code": "// 1. Create a material and inspect the (empty) base-color slot.\nconst mat = await create.material({ name: 'tex-demo' });\nconsole.log('baseColor bound before: ' + (mat.textures.baseColor.get() !== null));\n// 2. Clearing an already-empty slot is a no-op (async + undoable).\nawait mat.textures.baseColor.set(null);\nconsole.log('baseColor bound after clear: ' + (mat.textures.baseColor.id !== null));\n// 3. Every channel is reachable through the same shape.\nfor (const ch of ['baseColor', 'normal', 'metallicRoughness', 'occlusion', 'emissive']) {\n    console.log(ch + ' empty: ' + (mat.textures[ch].id === null));\n}\n// 4. Clean up.\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:Material.toString",
      "symbol": "Material.toString",
      "intent": "mesh",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'tostring-demo' });\n// 2. Select — N/A for asset-level material.\n// 3. Action — stringify the handle.\nconst repr = mat.toString();\n// 4. Observe + cleanup.\nconsole.log('toString output: ' + repr);\nconsole.log('repr mentions name: ' + repr.includes('tostring-demo'));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialAttribute",
      "symbol": "MaterialAttribute",
      "intent": "scene",
      "code": "// 1. Create a material.\nconst mat = await create.material({ name: 'attr-demo' });\n// 2. Each per-field getter returns a MaterialAttribute<T>.\nconst metallic = mat.pbr.metallic;\nawait metallic.set(0.7);\n// 3. Read it back (synchronous).\nconsole.log('metallic: ' + metallic.get());\n// 4. Clean up.\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialAttribute.get",
      "symbol": "MaterialAttribute.get",
      "intent": "scene",
      "code": "// 1. Setup — create a material with a known starting roughness.\nconst mat = await create.material({ name: 'attr-get-demo', roughness: 0.5 });\n// 2. Action — write a new value through the sibling setter (async + undoable).\nawait mat.pbr.roughness.set(0.15);\n// 3. Read-back — get() returns the freshly written value (synchronous).\nconst r = mat.pbr.roughness.get();\nconsole.log('roughness: ' + r);\n// 4. Verify + cleanup — get() reflects the set value.\nconsole.log('get reflects set: ' + (Math.abs(r - 0.15) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialAttribute.set",
      "symbol": "MaterialAttribute.set",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'attr-set-demo' });\n// 2. Select — N/A; Material lives at asset level.\n// 3. Action — half-metallic it (async, undoable).\nawait mat.pbr.metallic.set(0.5);\n// 4. Observe + cleanup.\nconsole.log('metallic: ' + mat.pbr.metallic.get());\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateApi",
      "symbol": "MaterialCreateApi",
      "intent": "scene",
      "code": "// 1) Create a red PBR material in the Previewer (async + undoable).\nconst mat = await create.material({ baseColor: [1, 0, 0, 1] });\n// 2) Log a verifiable property.\nconsole.log('material name: ' + mat.name);"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateApi.example",
      "symbol": "MaterialCreateApi.example",
      "intent": "scene",
      "code": "console.log('example length: ' + create.example().length);"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateApi.help",
      "symbol": "MaterialCreateApi.help",
      "intent": "scene",
      "code": "console.log('help length: ' + create.help().length);"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateApi.material",
      "symbol": "MaterialCreateApi.material",
      "intent": "scene",
      "code": "// 1. Setup — Previewer is where this barrel is mounted.\nawait Utils.editor.switchTo('previewer');\nconsole.log('mode: ' + Utils.editor.current());\n// 2. Action — build a glossy red PBR material (async + undoable).\nconst mat = await create.material({ name: 'pv-create-demo', baseColor: [1, 0.2, 0.2, 1], metallic: 0.1, roughness: 0.3 });\n// 3. Read-back — the live handle reflects the inputs.\nconsole.log('roughness: ' + mat.pbr.roughness.get() + ' metallic: ' + mat.pbr.metallic.get());\n// 4. Verify it is discoverable via the asset query + clean up.\nconsole.log('found by name: ' + (getMaterial('pv-create-demo') !== null));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateApi.texture",
      "symbol": "MaterialCreateApi.texture",
      "intent": "scene",
      "code": "// 1. Setup — Previewer is where create.texture is mounted; paint a tiny\n//    canvas to use as the image payload (no file-picker in scripts).\nawait Utils.editor.switchTo('previewer');\nconst cv = document.createElement('canvas');\ncv.width = cv.height = 8;\nconst g = cv.getContext('2d');\ng.fillStyle = '#2a8';\ng.fillRect(0, 0, 8, 8);\n// 2. Action — create the asset texture (async + undoable).\nconst tex = await create.texture({ name: 'pv-tex-demo', data: cv.toDataURL('image/png') });\n// 3. Read-back — it shows up in the Previewer texture list.\nconsole.log('listed: ' + listTextures().some((t) => t.name === 'pv-tex-demo'));\n// 4. Verify resolvable by name.\nconsole.log('resolved: ' + (getTexture('pv-tex-demo') !== null) + ' id: ' + tex.id);"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateApi.unlitMaterial",
      "symbol": "MaterialCreateApi.unlitMaterial",
      "intent": "scene",
      "code": "// 1. Setup — Previewer is where this barrel is mounted.\nawait Utils.editor.switchTo('previewer');\nconsole.log('mode: ' + Utils.editor.current());\n// 2. Action — build an unlit cyan material (async + undoable).\nconst mat = await create.unlitMaterial({ name: 'pv-unlit-demo', baseColor: [0, 0.8, 1, 1] });\n// 3. Read-back — the live handle carries the requested base color.\nconsole.log('baseColor: ' + JSON.stringify(mat.baseColor));\n// 4. Verify it is discoverable via the asset query + clean up.\nconsole.log('found by name: ' + (getMaterial('pv-unlit-demo') !== null));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateOpts",
      "symbol": "MaterialCreateOpts",
      "intent": "scene",
      "code": "// 1. Setup — describe the desired material.\nconst opts = { name: 'opts-demo', baseColor: [0.6, 0.2, 0.8, 1], metallic: 0.3, roughness: 0.4 };\n// 2. Select — N/A; options bag, not a runtime handle.\n// 3. Action — pass to create.material.\nconst mat = await create.material(opts);\n// 4. Observe + cleanup.\nconsole.log('opts material id: ' + mat.materialId);\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateOpts.alphaCutoff",
      "symbol": "MaterialCreateOpts.alphaCutoff",
      "intent": "scene",
      "code": "// 1. Setup — describe alphaCutoff (meaningful with alphaMode 'mask').\nconst opts = { name: 'fld-alphaCutoff-demo', alphaMode: 'mask', alphaCutoff: 0.25 };\n// 2. Action — create the material from the options bag.\nconst mat = await create.material(opts);\n// 3. Read-back — the live handle reflects the requested cutoff.\nconst v = mat.alphaCutoff;\nconsole.log('alphaCutoff: ' + v);\n// 4. Verify + cleanup.\nconsole.log('alphaCutoff applied: ' + (Math.abs(v - 0.25) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateOpts.alphaMode",
      "symbol": "MaterialCreateOpts.alphaMode",
      "intent": "scene",
      "code": "// 1. Setup — describe just the alphaMode field.\nconst opts = { name: 'fld-alphaMode-demo', alphaMode: 'mask' };\n// 2. Action — create the material from the options bag.\nconst mat = await create.material(opts);\n// 3. Read-back — the live handle reflects the requested mode.\nconst v = mat.alphaMode;\nconsole.log('alphaMode: ' + v);\n// 4. Verify + cleanup.\nconsole.log('alphaMode applied: ' + (v === 'mask'));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateOpts.baseColor",
      "symbol": "MaterialCreateOpts.baseColor",
      "intent": "scene",
      "code": "// 1. Setup — describe just the baseColor field.\nconst opts = { name: 'fld-baseColor-demo', baseColor: [0.8, 0.2, 0.4, 1] };\n// 2. Action — create the material from the options bag.\nconst mat = await create.material(opts);\n// 3. Read-back — the live handle reflects the requested color.\nconst c = mat.baseColor;\nconsole.log('baseColor: ' + JSON.stringify(c));\n// 4. Verify + cleanup.\nconsole.log('baseColor applied: ' + (Math.abs(c[0] - 0.8) < 1e-6 && Math.abs(c[1] - 0.2) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateOpts.doubleSided",
      "symbol": "MaterialCreateOpts.doubleSided",
      "intent": "scene",
      "code": "// 1. Setup — describe just the doubleSided field.\nconst opts = { name: 'fld-doubleSided-demo', doubleSided: true };\n// 2. Action — create the material from the options bag.\nconst mat = await create.material(opts);\n// 3. Read-back — the live handle reflects the requested flag.\nconst v = mat.doubleSided;\nconsole.log('doubleSided: ' + v);\n// 4. Verify + cleanup.\nconsole.log('doubleSided applied: ' + (v === true));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateOpts.emissive",
      "symbol": "MaterialCreateOpts.emissive",
      "intent": "scene",
      "code": "// 1. Setup — describe just the emissive field.\nconst opts = { name: 'fld-emissive-demo', emissive: [0.1, 0.4, 0.9] };\n// 2. Action — create the material from the options bag.\nconst mat = await create.material(opts);\n// 3. Read-back — the live handle reflects the requested emissive.\nconst e = mat.emissive;\nconsole.log('emissive: ' + JSON.stringify(e));\n// 4. Verify + cleanup.\nconsole.log('emissive applied: ' + (Math.abs(e[2] - 0.9) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateOpts.emissiveIntensity",
      "symbol": "MaterialCreateOpts.emissiveIntensity",
      "intent": "scene",
      "code": "// 1. Setup — describe just the emissiveIntensity field.\nconst opts = { name: 'fld-emissiveIntensity-demo', emissiveIntensity: 2.5 };\n// 2. Action — create the material from the options bag.\nconst mat = await create.material(opts);\n// 3. Read-back — the live handle reflects the requested intensity.\nconst v = mat.emissiveIntensity;\nconsole.log('emissiveIntensity: ' + v);\n// 4. Verify + cleanup.\nconsole.log('emissiveIntensity applied: ' + (Math.abs(v - 2.5) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateOpts.metallic",
      "symbol": "MaterialCreateOpts.metallic",
      "intent": "scene",
      "code": "// 1. Setup — describe just the metallic field.\nconst opts = { name: 'fld-metallic-demo', metallic: 0.4 };\n// 2. Action — create the material from the options bag.\nconst mat = await create.material(opts);\n// 3. Read-back — the live handle reflects the requested metallic.\nconst v = mat.metallic;\nconsole.log('metallic: ' + v);\n// 4. Verify + cleanup.\nconsole.log('metallic applied: ' + (Math.abs(v - 0.4) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateOpts.name",
      "symbol": "MaterialCreateOpts.name",
      "intent": "scene",
      "code": "// 1. Setup — describe the material with a specific display name.\nconst opts = { name: 'fld-name-demo', baseColor: [0.6, 0.2, 0.8, 1] };\n// 2. Action — create with the options bag.\nconst mat = await create.material(opts);\n// 3. Read-back — the created material carries the requested name.\nconsole.log('requested name: ' + opts.name + ' / actual: ' + mat.name);\n// 4. Verify + cleanup (name auto-disambiguates, so assert the prefix).\nconsole.log('name honored: ' + mat.name.startsWith('fld-name-demo'));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateOpts.roughness",
      "symbol": "MaterialCreateOpts.roughness",
      "intent": "scene",
      "code": "// 1. Setup — describe just the roughness field.\nconst opts = { name: 'fld-roughness-demo', roughness: 0.2 };\n// 2. Action — create the material from the options bag.\nconst mat = await create.material(opts);\n// 3. Read-back — the live handle reflects the requested roughness.\nconst v = mat.roughness;\nconsole.log('roughness: ' + v);\n// 4. Verify + cleanup.\nconsole.log('roughness applied: ' + (Math.abs(v - 0.2) < 1e-6));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateUnlitOpts",
      "symbol": "MaterialCreateUnlitOpts",
      "intent": "scene",
      "code": "// 1. Setup — describe the unlit material.\nconst opts = { name: 'unlit-demo', baseColor: [1, 0.5, 0, 1] };\n// 2. Select — N/A; options bag.\n// 3. Action — create.\nconst mat = await create.unlitMaterial(opts);\n// 4. Observe + cleanup.\nconsole.log('unlit name: ' + mat.name);\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateUnlitOpts.baseColor",
      "symbol": "MaterialCreateUnlitOpts.baseColor",
      "intent": "scene",
      "code": "// 1. Setup — option for unlit material.\nconst opts = { name: 'unlit-baseColor-demo', baseColor: [0.2, 0.8, 0.4, 1] };\n// 2. Select — N/A.\n// 3. Action — create.\nconst mat = await create.unlitMaterial(opts);\n// 4. Observe + cleanup.\nconsole.log('baseColor OK: ' + (mat.name.length > 0));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialCreateUnlitOpts.name",
      "symbol": "MaterialCreateUnlitOpts.name",
      "intent": "scene",
      "code": "// 1. Setup — describe the unlit material with a specific display name.\nconst opts = { name: 'unlit-name-demo', baseColor: [1, 0.5, 0, 1] };\n// 2. Action — create with the options bag.\nconst mat = await create.unlitMaterial(opts);\n// 3. Read-back — the created material carries the requested name.\nconsole.log('requested name: ' + opts.name + ' / actual: ' + mat.name);\n// 4. Verify + cleanup (name auto-disambiguates, so assert the prefix).\nconsole.log('name honored: ' + mat.name.startsWith('unlit-name-demo'));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialDuplicateOpts",
      "symbol": "MaterialDuplicateOpts",
      "intent": "scene",
      "code": "// 1. Setup — create a material to duplicate.\nconst src = await create.material({ name: 'dup-src' });\n// 2. Select — N/A; options bag.\n// 3. Action — duplicate with override name (async + undoable).\nconst copy = await src.duplicate({ name: 'dup-copy' });\n// 4. Observe + cleanup.\nconsole.log('copy name: ' + copy.name);\nsrc.delete();\ncopy.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialDuplicateOpts.name",
      "symbol": "MaterialDuplicateOpts.name",
      "intent": "scene",
      "code": "// 1. Setup.\nconst src = await create.material({ name: 'dup-name-src' });\n// 2. Select — N/A.\n// 3. Action — duplicate with name override (async + undoable).\nconst copy = await src.duplicate({ name: 'dup-name-copy' });\n// 4. Observe + cleanup.\nconsole.log('name set: ' + (copy.name === 'dup-name-copy'));\nsrc.delete();\ncopy.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialPbrApi",
      "symbol": "MaterialPbrApi",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'pbr-api-demo' });\n// 2. Select — N/A; Material lives at asset level.\n// 3. Action — tweak fields on the pbr namespace.\nawait mat.pbr.metallic.set(0.7);\nawait mat.pbr.roughness.set(0.2);\n// 4. Observe + cleanup.\nconsole.log('metallic: ' + mat.pbr.metallic.get());\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialPbrApi.alphaCutoff",
      "symbol": "MaterialPbrApi.alphaCutoff",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'pbr-alphaCutoff-demo' });\n// 2. Select — N/A.\n// 3. Action — write and read the field (alphaMode must be 'mask' first).\nawait mat.pbr.alphaMode.set('mask');\nawait mat.pbr.alphaCutoff.set(0.4);\nconst got = mat.pbr.alphaCutoff.get();\n// 4. Observe + cleanup.\nconsole.log('alphaCutoff: ' + JSON.stringify(got));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialPbrApi.alphaMode",
      "symbol": "MaterialPbrApi.alphaMode",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'pbr-alphaMode-demo' });\n// 2. Select — N/A.\n// 3. Action — write and read the field.\nawait mat.pbr.alphaMode.set('mask');\nconst got = mat.pbr.alphaMode.get();\n// 4. Observe + cleanup.\nconsole.log('alphaMode: ' + JSON.stringify(got));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialPbrApi.baseColor",
      "symbol": "MaterialPbrApi.baseColor",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'pbr-baseColor-demo' });\n// 2. Select — N/A.\n// 3. Action — write and read the field.\nawait mat.pbr.baseColor.set([0.8, 0.3, 0.1, 1]);\nconst got = mat.pbr.baseColor.get();\n// 4. Observe + cleanup.\nconsole.log('baseColor: ' + JSON.stringify(got));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialPbrApi.doubleSided",
      "symbol": "MaterialPbrApi.doubleSided",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'pbr-doubleSided-demo' });\n// 2. Select — N/A.\n// 3. Action — write and read the field.\nawait mat.pbr.doubleSided.set(true);\nconst got = mat.pbr.doubleSided.get();\n// 4. Observe + cleanup.\nconsole.log('doubleSided: ' + JSON.stringify(got));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialPbrApi.emissive",
      "symbol": "MaterialPbrApi.emissive",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'pbr-emissive-demo' });\n// 2. Select — N/A.\n// 3. Action — write and read the field.\nawait mat.pbr.emissive.set([0.1, 0.05, 0]);\nconst got = mat.pbr.emissive.get();\n// 4. Observe + cleanup.\nconsole.log('emissive: ' + JSON.stringify(got));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialPbrApi.emissiveIntensity",
      "symbol": "MaterialPbrApi.emissiveIntensity",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'pbr-emissiveIntensity-demo' });\n// 2. Select — N/A.\n// 3. Action — write and read the field.\nawait mat.pbr.emissiveIntensity.set(1.2);\nconst got = mat.pbr.emissiveIntensity.get();\n// 4. Observe + cleanup.\nconsole.log('emissiveIntensity: ' + JSON.stringify(got));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialPbrApi.example",
      "symbol": "MaterialPbrApi.example",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'pbr-example-demo' });\n// 2. Select — N/A.\n// 3. Action — render the example text.\nconst s = mat.pbr.example();\n// 4. Observe + cleanup.\nconsole.log('example text length: ' + s.length);\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialPbrApi.help",
      "symbol": "MaterialPbrApi.help",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'pbr-help-demo' });\n// 2. Select — N/A.\n// 3. Action — render the help text.\nconst s = mat.pbr.help();\n// 4. Observe + cleanup.\nconsole.log('help text length: ' + s.length);\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialPbrApi.metallic",
      "symbol": "MaterialPbrApi.metallic",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'pbr-metallic-demo' });\n// 2. Select — N/A.\n// 3. Action — write and read the field.\nawait mat.pbr.metallic.set(0.5);\nconst got = mat.pbr.metallic.get();\n// 4. Observe + cleanup.\nconsole.log('metallic: ' + JSON.stringify(got));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialPbrApi.roughness",
      "symbol": "MaterialPbrApi.roughness",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'pbr-roughness-demo' });\n// 2. Select — N/A.\n// 3. Action — write and read the field.\nawait mat.pbr.roughness.set(0.3);\nconst got = mat.pbr.roughness.get();\n// 4. Observe + cleanup.\nconsole.log('roughness: ' + JSON.stringify(got));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialTextureChannelAccessor",
      "symbol": "MaterialTextureChannelAccessor",
      "intent": "scene",
      "code": "// 1. Setup — paint a tiny canvas as the image payload.\nconst cv = document.createElement('canvas');\ncv.width = cv.height = 4;\nconst g = cv.getContext('2d');\ng.fillStyle = '#3aa657';\ng.fillRect(0, 0, 4, 4);\n// Create the asset texture + a material to bind it onto.\nconst tex = await create.texture({ name: 'tex-accessor-demo', data: cv.toDataURL('image/png') });\nconst mat = await create.material({ name: 'tex-accessor-mat' });\n// The slot is empty before we bind anything.\nconsole.log('empty before bind: ' + (mat.textures.baseColor.get() === null));\n// 2. Action — bind the base-color slot by NAME (async + undoable).\nawait mat.textures.baseColor.set('tex-accessor-demo');\n// 3. Read-back — get() + the .id alias both report the bound id.\nconsole.log('get: ' + mat.textures.baseColor.get() + ' id: ' + mat.textures.baseColor.id);\n// 4. Verify both accessors report the same created id, then clean up.\nconsole.log('accessor reports bound texture: ' + (mat.textures.baseColor.get() === tex.id));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialTextureChannelAccessor.get",
      "symbol": "MaterialTextureChannelAccessor.get",
      "intent": "scene",
      "code": "// 1. Setup — paint a tiny canvas as the image payload.\nconst cv = document.createElement('canvas');\ncv.width = cv.height = 4;\nconst g = cv.getContext('2d');\ng.fillStyle = '#3aa657';\ng.fillRect(0, 0, 4, 4);\n// Create the asset texture + a material; the slot starts empty.\nconst tex = await create.texture({ name: 'tex-get-demo', data: cv.toDataURL('image/png') });\nconst mat = await create.material({ name: 'tex-get-mat' });\n// get() returns null while the slot is unbound.\nconsole.log('empty before bind: ' + (mat.textures.baseColor.get() === null));\n// 2. Action — bind by NAME (async + undoable).\nawait mat.textures.baseColor.set('tex-get-demo');\n// 3. Read-back — get() now returns the bound texture id.\nconst bound = mat.textures.baseColor.get();\n// bound is the same id as the texture we created.\nconsole.log('get: ' + bound);\n// 4. Verify get() matches the created id, then clean up.\nconsole.log('get matches created id: ' + (bound === tex.id));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialTextureChannelAccessor.id",
      "symbol": "MaterialTextureChannelAccessor.id",
      "intent": "scene",
      "code": "// 1. Setup — paint a tiny canvas as the image payload.\nconst cv = document.createElement('canvas');\ncv.width = cv.height = 4;\nconst g = cv.getContext('2d');\ng.fillStyle = '#3aa657';\ng.fillRect(0, 0, 4, 4);\n// Create the asset texture + a material to bind it onto.\nconst tex = await create.texture({ name: 'tex-id-demo', data: cv.toDataURL('image/png') });\nconst mat = await create.material({ name: 'tex-id-mat' });\n// Bind the base-color slot so .id has a value to report.\nawait mat.textures.baseColor.set('tex-id-demo');\n// 2. Action — read the .id alias (read-only mirror of get()).\nconst id = mat.textures.baseColor.id;\n// 3. Read-back — .id and get() are the same value.\nconsole.log('id: ' + id + ' get: ' + mat.textures.baseColor.get());\n// 4. Verify .id mirrors get() and matches the created id, then clean up.\nconsole.log('.id mirrors get(): ' + (id === mat.textures.baseColor.get() && id === tex.id));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialTextureChannelAccessor.set",
      "symbol": "MaterialTextureChannelAccessor.set",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'tex-set-demo' });\n// 2. Select — N/A.\n// 3. Action — clear the texture slot (async).\nawait mat.textures.baseColor.set(null);\n// 4. Observe + cleanup.\nconsole.log('cleared: ' + (mat.textures.baseColor.get() === null));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialTexturesApi",
      "symbol": "MaterialTexturesApi",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'tex-api-demo' });\n// 2. Select — N/A.\n// 3. Action — read base-color slot binding.\nconst bc = mat.textures.baseColor.get();\n// 4. Observe + cleanup.\nconsole.log('baseColor bound: ' + bc);\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialTexturesApi.baseColor",
      "symbol": "MaterialTexturesApi.baseColor",
      "intent": "scene",
      "code": "// 1. Setup — paint a tiny green canvas to use as the image payload.\nconst cv = document.createElement('canvas');\ncv.width = cv.height = 4;\nconst g = cv.getContext('2d');\ng.fillStyle = '#3aa657';\ng.fillRect(0, 0, 4, 4);\n// Create the asset texture + a material to bind it onto.\nconst tex = await create.texture({ name: 'tex-baseColor-demo', data: cv.toDataURL('image/png') });\nconst mat = await create.material({ name: 'tex-baseColor-mat' });\n// The slot starts unbound (null).\nconsole.log('baseColor empty before bind: ' + (mat.textures.baseColor.get() === null));\n// 2. Action — bind the base-color slot by NAME (async + undoable).\nawait mat.textures.baseColor.set('tex-baseColor-demo');\n// 3. Read-back — the slot now reports the bound texture id.\nconst bound = mat.textures.baseColor.get();\n// bound is the same id as the texture we created.\nconsole.log('bound id: ' + bound);\n// 4. Verify the bound id matches what we created, then clean up.\nconsole.log('bind OK: ' + (bound === tex.id));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialTexturesApi.emissive",
      "symbol": "MaterialTexturesApi.emissive",
      "intent": "scene",
      "code": "// 1. Setup — paint a tiny orange canvas as the emissive payload.\nconst cv = document.createElement('canvas');\ncv.width = cv.height = 4;\nconst g = cv.getContext('2d');\ng.fillStyle = '#ff5500';\ng.fillRect(0, 0, 4, 4);\n// Create the asset texture + a material to bind it onto.\nconst tex = await create.texture({ name: 'tex-emissive-demo', data: cv.toDataURL('image/png') });\nconst mat = await create.material({ name: 'tex-emissive-mat' });\n// The slot starts unbound (null).\nconsole.log('emissive empty before bind: ' + (mat.textures.emissive.get() === null));\n// 2. Action — bind the emissive map by NAME (async + undoable).\nawait mat.textures.emissive.set('tex-emissive-demo');\n// 3. Read-back — the slot now reports the bound texture id.\nconst bound = mat.textures.emissive.get();\n// bound is the same id as the texture we created.\nconsole.log('bound id: ' + bound);\n// 4. Verify the bound id matches what we created, then clean up.\nconsole.log('bind OK: ' + (bound === tex.id));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialTexturesApi.example",
      "symbol": "MaterialTexturesApi.example",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'textures-example-demo' });\n// 2. Select — N/A.\n// 3. Action — render text.\nconst s = mat.textures.example();\n// 4. Observe + cleanup.\nconsole.log('example length: ' + s.length);\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialTexturesApi.help",
      "symbol": "MaterialTexturesApi.help",
      "intent": "scene",
      "code": "// 1. Setup — create a material.\nconst mat = await create.material({ name: 'textures-help-demo' });\n// 2. Select — N/A.\n// 3. Action — render text.\nconst s = mat.textures.help();\n// 4. Observe + cleanup.\nconsole.log('help length: ' + s.length);\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialTexturesApi.metallicRoughness",
      "symbol": "MaterialTexturesApi.metallicRoughness",
      "intent": "scene",
      "code": "// 1. Setup — paint a tiny canvas to use as the packed MR payload.\nconst cv = document.createElement('canvas');\ncv.width = cv.height = 4;\nconst g = cv.getContext('2d');\ng.fillStyle = '#00c080';\ng.fillRect(0, 0, 4, 4);\n// Create the asset texture + a material to bind it onto.\nconst tex = await create.texture({ name: 'tex-mr-demo', data: cv.toDataURL('image/png') });\nconst mat = await create.material({ name: 'tex-mr-mat' });\n// The slot starts unbound (null).\nconsole.log('metallicRoughness empty before bind: ' + (mat.textures.metallicRoughness.get() === null));\n// 2. Action — bind the packed metallic/roughness map by NAME (async + undoable).\nawait mat.textures.metallicRoughness.set('tex-mr-demo');\n// 3. Read-back — the slot now reports the bound texture id.\nconst bound = mat.textures.metallicRoughness.get();\n// bound is the same id as the texture we created.\nconsole.log('bound id: ' + bound);\n// 4. Verify the bound id matches what we created, then clean up.\nconsole.log('bind OK: ' + (bound === tex.id));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialTexturesApi.normal",
      "symbol": "MaterialTexturesApi.normal",
      "intent": "scene",
      "code": "// 1. Setup — paint a tiny flat-normal canvas (#8080ff) as the payload.\nconst cv = document.createElement('canvas');\ncv.width = cv.height = 4;\nconst g = cv.getContext('2d');\ng.fillStyle = '#8080ff';\ng.fillRect(0, 0, 4, 4);\n// Create the asset texture + a material to bind it onto.\nconst tex = await create.texture({ name: 'tex-normal-demo', data: cv.toDataURL('image/png') });\nconst mat = await create.material({ name: 'tex-normal-mat' });\n// The slot starts unbound (null).\nconsole.log('normal empty before bind: ' + (mat.textures.normal.get() === null));\n// 2. Action — bind the normal map by NAME (async + undoable).\nawait mat.textures.normal.set('tex-normal-demo');\n// 3. Read-back — the slot now reports the bound texture id.\nconst bound = mat.textures.normal.get();\n// bound is the same id as the texture we created.\nconsole.log('bound id: ' + bound);\n// 4. Verify the bound id matches what we created, then clean up.\nconsole.log('bind OK: ' + (bound === tex.id));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MaterialTexturesApi.occlusion",
      "symbol": "MaterialTexturesApi.occlusion",
      "intent": "scene",
      "code": "// 1. Setup — paint a tiny light-grey canvas as the AO payload.\nconst cv = document.createElement('canvas');\ncv.width = cv.height = 4;\nconst g = cv.getContext('2d');\ng.fillStyle = '#cccccc';\ng.fillRect(0, 0, 4, 4);\n// Create the asset texture + a material to bind it onto.\nconst tex = await create.texture({ name: 'tex-ao-demo', data: cv.toDataURL('image/png') });\nconst mat = await create.material({ name: 'tex-ao-mat' });\n// The slot starts unbound (null).\nconsole.log('occlusion empty before bind: ' + (mat.textures.occlusion.get() === null));\n// 2. Action — bind the ambient-occlusion map by NAME (async + undoable).\nawait mat.textures.occlusion.set('tex-ao-demo');\n// 3. Read-back — the slot now reports the bound texture id.\nconst bound = mat.textures.occlusion.get();\n// bound is the same id as the texture we created.\nconsole.log('bound id: ' + bound);\n// 4. Verify the bound id matches what we created, then clean up.\nconsole.log('bind OK: ' + (bound === tex.id));\nmat.delete();"
    },
    {
      "kind": "example",
      "id": "example:MathLibCurveApi.arcLengthResample",
      "symbol": "MathLibCurveApi.arcLengthResample",
      "intent": "scene",
      "code": "// 1. Setup — an unevenly-spaced 3-point polyline along +X.\nconst path = [[0, 0, 0], [1, 0, 0], [5, 0, 0]];\n// 2. Resample — request 5 arc-length-even samples.\nconst even = MathLib.arcLengthResample(path, 5);\n// 3. Count — exactly 5 samples come back.\nconsole.log('sample count: ' + even.length);\n// 4. Observe — spacing is uniform (total length 5 / 4 gaps = 1.25 each).\nconst gap = even[1].pos[0] - even[0].pos[0];\nconsole.log('first gap: ' + gap.toFixed(2));"
    },
    {
      "kind": "example",
      "id": "example:MathLibCurveApi.beltAroundCircles",
      "symbol": "MathLibCurveApi.beltAroundCircles",
      "intent": "scene",
      "code": "// 1. Setup — three pulleys in a triangle on the ground (XZ) plane.\nconst pulleys = [\n    { center: [0, 0, 0], radius: 1 },\n    { center: [6, 0, 0], radius: 1.5 },\n    { center: [3, 0, 5], radius: 0.8 },\n];\n// 2. Wrap — compute the taut belt around all three.\nconst belt = MathLib.beltAroundCircles(pulleys, { arcSegments: 24 });\n// 3. Count — the closed polyline mixes arc + tangent points.\nconsole.log('belt points: ' + belt.length);\n// 4. Observe — it lies flat on the ground plane (y ~ 0), ready for a curve.\nconsole.log('flat (y ~ 0): ' + (Math.abs(belt[0][1]) < 1e-6));"
    },
    {
      "kind": "example",
      "id": "example:MathLibCurveApi.catmullRom",
      "symbol": "MathLibCurveApi.catmullRom",
      "intent": "scene",
      "code": "// 1. Setup — a closed unit-square loop of 4 control points.\nconst square = [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]];\n// 2. Sample — 8 samples per segment around the closed loop.\nconst pts = MathLib.catmullRom(square, 8, { closed: true });\n// 3. Count — 4 segments * 8 + the wrap endpoint = 33 points.\nconsole.log('point count: ' + pts.length);\n// 4. Observe — the global parameter spans 0..1 across the loop.\nconsole.log('t range: ' + pts[0].t + '..' + pts[pts.length - 1].t);"
    },
    {
      "kind": "example",
      "id": "example:MathLibCurveApi.computeFrames",
      "symbol": "MathLibCurveApi.computeFrames",
      "intent": "scene",
      "code": "// 1. Setup — a straight run of points along +X.\nconst line = [[0, 0, 0], [1, 0, 0], [2, 0, 0], [3, 0, 0]];\n// 2. Frame — compute the orientation frames.\nconst frames = MathLib.computeFrames(line);\n// 3. Read — inspect the first frame's tangent.\nconst T = frames[0].T;\nconsole.log('tangent: ' + JSON.stringify(T.map(n => Math.round(n))));\n// 4. Observe — T is unit length and N is perpendicular to T.\nconst dotTN = MathLib.dot(frames[0].T, frames[0].N);\nconsole.log('T*N ~ 0: ' + (Math.abs(dotTN) < 1e-6));"
    },
    {
      "kind": "example",
      "id": "example:MathLibCurveApi.externalTangentPoints",
      "symbol": "MathLibCurveApi.externalTangentPoints",
      "intent": "scene",
      "code": "// 1. Setup — two equal circles 10 apart along the X axis.\nconst c1 = [0, 0, 0], c2 = [10, 0, 0];\n// 2. Tangent — the +1-side external tangent points.\nconst t = MathLib.externalTangentPoints(c1, 1, c2, 1, 1);\n// 3. Read — for equal radii the tangent is parallel to the center line.\nconsole.log('p1: ' + JSON.stringify(t.p1.map(n => +n.toFixed(2))));\n// 4. Verify — the segment length equals the center distance (10).\nconsole.log('segment ~ 10: ' + MathLib.distance(t.p1, t.p2).toFixed(2));"
    },
    {
      "kind": "example",
      "id": "example:MathLibCurveApi.jitteredGrid",
      "symbol": "MathLibCurveApi.jitteredGrid",
      "intent": "scene",
      "code": "// 1. Setup — a 10x10 region on the XZ plane.\nconst bounds = { min: [0, 0, 0], max: [10, 0, 10] };\n// 2. Scatter — a 2-unit grid (~5x5 cells) with a fixed seed.\nconst pts = MathLib.jitteredGrid(bounds, 2, { seed: 1, jitter: 0.5 });\n// 3. Count — roughly area / cell^2 = 100 / 4 = 25 cells.\nconsole.log('point count: ' + pts.length);\n// 4. Observe — the same seed replays the exact same first point.\nconst again = MathLib.jitteredGrid(bounds, 2, { seed: 1, jitter: 0.5 });\nconsole.log('deterministic: ' + (JSON.stringify(pts[0]) === JSON.stringify(again[0])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibCurveApi.orientToCurve",
      "symbol": "MathLibCurveApi.orientToCurve",
      "intent": "scene",
      "code": "// 1. Setup — frame a straight path along +Z.\nconst frames = MathLib.computeFrames([[0, 0, 0], [0, 0, 1], [0, 0, 2]]);\n// 2. Orient — find the nearest frame to a query point.\nconst r = MathLib.orientToCurve([0.1, 0, 1], frames, { mode: 'tangent' });\n// 3. Read — the returned quaternion is unit length.\nconst qLen = Math.hypot(r.quat[0], r.quat[1], r.quat[2], r.quat[3]);\nconsole.log('unit quat: ' + (Math.abs(qLen - 1) < 1e-6));\n// 4. Observe — the nearest frame sits at arc fraction s in [0,1].\nconsole.log('arc fraction s: ' + r.s.toFixed(2));"
    },
    {
      "kind": "example",
      "id": "example:MathLibCurveApi.poissonDisk",
      "symbol": "MathLibCurveApi.poissonDisk",
      "intent": "scene",
      "code": "// 1. Setup — a 20x20 region on the XZ plane.\nconst bounds = { min: [0, 0, 0], max: [20, 0, 20] };\n// 2. Scatter — points at least 3 units apart with a fixed seed.\nconst pts = MathLib.poissonDisk(bounds, 3, { seed: 7 });\n// 3. Count — report how many points fit.\nconsole.log('point count: ' + pts.length);\n// 4. Verify — find the smallest gap between any two points.\nlet minGap = Infinity;\n// Compare every unordered pair of scattered points.\nfor (let i = 0; i < pts.length; i++) {\n    // Only j > i so each pair is measured once.\n    for (let j = i + 1; j < pts.length; j++) {\n        // Track the closest spacing seen so far.\n        minGap = Math.min(minGap, MathLib.distance(pts[i], pts[j]));\n    }\n}\n// The minimum gap respects the requested separation of 3.\nconsole.log('min-distance holds: ' + (minGap >= 3 - 1e-6));"
    },
    {
      "kind": "example",
      "id": "example:MathLibCurveApi.ribbonMesh",
      "symbol": "MathLibCurveApi.ribbonMesh",
      "intent": "scene",
      "code": "// 1. Setup — a flat closed loop of raw points on the XZ plane.\nconst loop = [[0, 0, 0], [4, 0, 0], [4, 0, 4], [0, 0, 4]];\n// 2. Build — a 2-unit-wide ribbon, closed, with NO banking (flat).\nconst ribbon = MathLib.ribbonMesh(loop, 2, { closed: true });\n// 3. Count — 2 verts per framed point * 3 coords.\nconsole.log('vert floats: ' + ribbon.verts.length);\n// 4. Observe — a flat XZ loop with no banking has ~zero y-extent.\nlet maxY = 0;\nfor (let i = 1; i < ribbon.verts.length; i += 3)\n    maxY = Math.max(maxY, Math.abs(ribbon.verts[i]));\nconsole.log('flat ribbon (y ~ 0): ' + (maxY < 1e-4));"
    },
    {
      "kind": "example",
      "id": "example:MathLibCurveApi.stadiumPath",
      "symbol": "MathLibCurveApi.stadiumPath",
      "intent": "scene",
      "code": "// 1. Setup — an oval track outline 20 long by 8 wide.\nconst track = MathLib.stadiumPath({ length: 20, width: 8, segments: 24 });\n// 2. Count — a closed polyline of two cap arcs + two straight sides.\nconsole.log('stadium points: ' + track.length);\n// 3. Read — the X extent spans the full length (~20).\nconst xs = track.map(p => p[0]);\nconsole.log('x extent ~ 20: ' + (Math.max(...xs) - Math.min(...xs)).toFixed(1));\n// 4. Observe — and the Z extent spans the full width (~8).\nconst zs = track.map(p => p[2]);\nconsole.log('z extent ~ 8: ' + (Math.max(...zs) - Math.min(...zs)).toFixed(1));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.boundingBox",
      "symbol": "MathLibFactoryApi.boundingBox",
      "intent": "scene",
      "code": "// 1. Build a 10×10×10 box from corners.\nconst box = MathLib.boundingBox([0, 0, 0], [10, 10, 10]);\nconsole.log('center = ' + JSON.stringify(box.center.toArray()));\n// 2. Read the box size.\nconsole.log('size = ' + JSON.stringify(box.size.toArray()));\n// 3. Containment test.\nconsole.log('contains = ' + box.contains([5, 5, 5]));\n// 4. No-argument box is empty until expanded.\nconsole.log('empty = ' + MathLib.boundingBox().isEmpty);"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.BoundingBox",
      "symbol": "MathLibFactoryApi.BoundingBox",
      "intent": "scene",
      "code": "// 1. Construct a 2×2×2 box around the origin.\nconst box = new MathLib.BoundingBox([-1, -1, -1], [1, 1, 1]);\nconsole.log('center = ' + JSON.stringify(box.center.toArray()));\n// 2. instanceof narrows a value to a BoundingBox.\nconsole.log('is BoundingBox = ' + (box instanceof MathLib.BoundingBox));\n// 3. Build the tightest box around points via the static factory.\nconst fit = MathLib.BoundingBox.from([0, 0, 0], [10, 10, 10]);\nconsole.log('width = ' + fit.width);\n// 4. The factory returns the same class.\nconsole.log('factory is BoundingBox = ' + (MathLib.boundingBox([0, 0, 0], [1, 1, 1]) instanceof MathLib.BoundingBox));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.color",
      "symbol": "MathLibFactoryApi.color",
      "intent": "scene",
      "code": "// 1. Build a saturated red.\nconst red = MathLib.color(1, 0, 0);\nconsole.log('red tuple = ' + JSON.stringify(red.toArray()));\n// 2. The default factory gives white.\nconsole.log('white = ' + JSON.stringify(MathLib.color().toArray()));\n// 3. Blend red toward blue at the midpoint.\nconst mid = red.lerp(MathLib.color(0, 0, 1), 0.5);\nconsole.log('mid tuple = ' + JSON.stringify(mid.toArray()));\n// 4. Read back as a hex string.\nconsole.log('hex = ' + red.toHexString());"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.Color",
      "symbol": "MathLibFactoryApi.Color",
      "intent": "scene",
      "code": "// 1. The default constructor gives white.\nconst white = new MathLib.Color();\nconsole.log('white = ' + JSON.stringify(white.toArray()));\n// 2. instanceof narrows a value to a Color.\nconsole.log('is Color = ' + (white instanceof MathLib.Color));\n// 3. Build from a hex literal via the static factory.\nconst cyan = MathLib.Color.fromHex(0x00ffff);\nconsole.log('cyan = ' + JSON.stringify(cyan.toArray()));\n// 4. The factory returns the same class.\nconsole.log('factory is Color = ' + (MathLib.color(1, 0, 0) instanceof MathLib.Color));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.distanceToPlane",
      "symbol": "MathLibFactoryApi.distanceToPlane",
      "intent": "scene",
      "code": "// 1. Distance above the ground plane is positive.\nconst ground = { normal: [0, 1, 0], constant: 0 };\nconsole.log('above = ' + MathLib.distanceToPlane([0, 5, 0], ground));\n// 2. Distance below the ground plane is negative.\nconsole.log('below = ' + MathLib.distanceToPlane([0, -3, 0], ground));\n// 3. A point on the plane has distance 0.\nconsole.log('on plane = ' + MathLib.distanceToPlane([4, 0, -2], ground));\n// 4. A Plane handle exposes its normal/constant for the same query.\nconst handle = MathLib.plane([0, 1, 0], 0);\nconsole.log('via handle = ' + MathLib.distanceToPlane([0, 2, 0], { normal: handle.normal.toArray(), constant: handle.constant }));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.euler",
      "symbol": "MathLibFactoryApi.euler",
      "intent": "scene",
      "code": "// 1. Build a 90 degree rotation around Z.\nconst e = MathLib.euler(0, 0, Math.PI / 2);\nconsole.log('z radians = ' + e.z.toFixed(4));\n// 2. The composition order is recorded on the handle.\nconsole.log('order = ' + e.order);\n// 3. Convert to a quaternion for rotation work.\nconst q = e.toQuat();\nconst rotated = q.rotate([1, 0, 0]).toArray().map(n => Math.round(n * 1000) / 1000);\nconsole.log('rotated = ' + JSON.stringify(rotated));\n// 4. A different order produces a different rotation.\nconsole.log('zyx z = ' + MathLib.euler(0, 0, Math.PI / 2, 'ZYX').z.toFixed(4));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.Euler",
      "symbol": "MathLibFactoryApi.Euler",
      "intent": "scene",
      "code": "// 1. Construct a 90 degree X rotation.\nconst e = new MathLib.Euler(Math.PI / 2, 0, 0, 'XYZ');\nconsole.log('x radians = ' + e.x.toFixed(4));\n// 2. instanceof narrows a value to an Euler.\nconsole.log('is Euler = ' + (e instanceof MathLib.Euler));\n// 3. Read the composition order back.\nconsole.log('order = ' + e.order);\n// 4. The factory returns the same class.\nconsole.log('factory is Euler = ' + (MathLib.euler() instanceof MathLib.Euler));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.example",
      "symbol": "MathLibFactoryApi.example",
      "intent": "scene",
      "code": "// 1. Fetch the namespace example string.\nconst ex = MathLib.example();\nconsole.log('is string = ' + (typeof ex === 'string'));\n// 2. Per-factory examples are getters on the callable.\nconst vec3Ex = MathLib.vec3.example;\nconsole.log('vec3 example is string = ' + (typeof vec3Ex === 'string'));\n// 3. The example text is non-trivial.\nconsole.log('length >= 0 = ' + (ex.length >= 0));\n// 4. Examples are safe to log for copy-paste.\nconsole.log('example = ' + ex);"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.help",
      "symbol": "MathLibFactoryApi.help",
      "intent": "scene",
      "code": "// 1. Render the namespace help text.\nconst text = MathLib.help();\nconsole.log('help length > 0 = ' + (text.length > 0));\n// 2. The help string names the namespace.\nconsole.log('mentions MathLib = ' + text.includes('MathLib'));\n// 3. Per-factory help is a getter on the callable.\nconsole.log('vec3 help length > 0 = ' + (MathLib.vec3.help.length > 0));\n// 4. Help is plain text — safe to log directly.\nconsole.log('is string = ' + (typeof text === 'string'));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.invert",
      "symbol": "MathLibFactoryApi.invert",
      "intent": "scene",
      "code": "// 1. Build a translation matrix (tx = 10 at index 12).\nconst move = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 0, 0, 1];\nconst inv = MathLib.invert(move);\nconsole.log('inverse tx = ' + inv[12]);\n// 2. matrix × inverse is the identity.\nconst back = MathLib.mat4Multiply(move, inv);\nconsole.log('back to identity tx = ' + back[12]);\n// 3. Inverting the identity gives the identity.\nconsole.log('inv identity = ' + JSON.stringify(MathLib.invert(MathLib.mat4Identity())));\n// 4. The Mat4 class method agrees with the free function.\nconst viaClass = MathLib.mat4(move).inverse().toArray();\nconsole.log('class inverse tx = ' + viaClass[12]);"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.kdTree2D",
      "symbol": "MathLibFactoryApi.kdTree2D",
      "intent": "scene",
      "code": "// 1. Build a 2D tree over four UV points.\nconst pts = [[0, 0], [1, 0], [0, 1], [1, 1]];\nconst tree = MathLib.kdTree2D(pts);\nconsole.log('size = ' + tree.size);\n// 2. Find the closest point to a query.\nconst near = tree.closest([0.9, 0.1]);\nconsole.log('closest = ' + JSON.stringify(pts[near.index]));\n// 3. All points within a radius (sorted by distance).\nconst hits = tree.within([0, 0], 1.1);\nconsole.log('within count = ' + hits.indices.length);\n// 4. The k nearest neighbours.\nconsole.log('2 nearest = ' + JSON.stringify(tree.knearest([0, 0], 2).indices));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.KdTree2D",
      "symbol": "MathLibFactoryApi.KdTree2D",
      "intent": "scene",
      "code": "// 1. Construct a 2D tree over four UV points.\nconst pts = [[0, 0], [1, 0], [0, 1], [1, 1]];\nconst tree = new MathLib.KdTree2D(pts);\nconsole.log('size = ' + tree.size);\n// 2. instanceof narrows a value to a KdTree2D.\nconsole.log('is KdTree2D = ' + (tree instanceof MathLib.KdTree2D));\n// 3. The factory returns the same class.\nconsole.log('factory is KdTree2D = ' + (MathLib.kdTree2D(pts) instanceof MathLib.KdTree2D));\n// 4. Closest query returns an original-order index.\nconsole.log('closest = ' + JSON.stringify(pts[tree.closest([0.9, 0.1]).index]));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.kdTree3D",
      "symbol": "MathLibFactoryApi.kdTree3D",
      "intent": "scene",
      "code": "// 1. Build a 3D tree over four points.\nconst pts = [[0, 0, 0], [5, 0, 0], [0, 5, 0], [0, 0, 5]];\nconst tree = MathLib.kdTree3D(pts);\nconsole.log('size = ' + tree.size);\n// 2. Closest point to a query position.\nconst near = tree.closest([4, 0, 0]);\nconsole.log('closest = ' + JSON.stringify(pts[near.index]));\n// 3. All points within radius 6.\nconst hits = tree.within([0, 0, 0], 6);\nconsole.log('within count = ' + hits.indices.length);\n// 4. The single nearest neighbour distance.\nconsole.log('nearest dist = ' + tree.knearest([0, 0, 0], 1).distances[0]);"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.KdTree3D",
      "symbol": "MathLibFactoryApi.KdTree3D",
      "intent": "scene",
      "code": "// 1. Construct a 3D tree over four points.\nconst pts = [[0, 0, 0], [5, 0, 0], [0, 5, 0], [0, 0, 5]];\nconst tree = new MathLib.KdTree3D(pts);\nconsole.log('size = ' + tree.size);\n// 2. instanceof narrows a value to a KdTree3D.\nconsole.log('is KdTree3D = ' + (tree instanceof MathLib.KdTree3D));\n// 3. The factory returns the same class.\nconsole.log('factory is KdTree3D = ' + (MathLib.kdTree3D(pts) instanceof MathLib.KdTree3D));\n// 4. Closest query returns an original-order index.\nconsole.log('closest = ' + JSON.stringify(pts[tree.closest([4, 0, 0]).index]));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.mat3",
      "symbol": "MathLibFactoryApi.mat3",
      "intent": "scene",
      "code": "// 1. No argument gives the identity matrix.\nconst id = MathLib.mat3();\nconsole.log('identity = ' + JSON.stringify(id.toArray()));\n// 2. A diagonal-scale matrix (sx=1, sy=2, sz=3).\nconst scale = MathLib.mat3([1, 0, 0, 0, 2, 0, 0, 0, 3]);\nconsole.log('determinant = ' + scale.determinant());\n// 3. Transform a vector by the scale matrix.\nconst v = scale.transformVec3([1, 1, 1]);\nconsole.log('scaled = ' + JSON.stringify(v.toArray()));\n// 4. The class alias builds the same thing.\nconsole.log('alias len = ' + new MathLib.Mat3().toArray().length);"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.Mat3",
      "symbol": "MathLibFactoryApi.Mat3",
      "intent": "scene",
      "code": "// 1. The default constructor gives the identity matrix.\nconst id = new MathLib.Mat3();\nconsole.log('elements = ' + id.toArray().length);\n// 2. instanceof narrows a value to a Mat3.\nconsole.log('is Mat3 = ' + (id instanceof MathLib.Mat3));\n// 3. Build from explicit column-major components.\nconst scale = new MathLib.Mat3([1, 0, 0, 0, 2, 0, 0, 0, 3]);\nconsole.log('determinant = ' + scale.determinant());\n// 4. The factory returns the same class.\nconsole.log('factory is Mat3 = ' + (MathLib.mat3() instanceof MathLib.Mat3));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.mat4",
      "symbol": "MathLibFactoryApi.mat4",
      "intent": "scene",
      "code": "// 1. No argument gives the identity matrix.\nconst id = MathLib.mat4();\nconsole.log('elements = ' + id.toArray().length);\n// 2. Build a pure translation matrix (tx = 10 at index 12).\nconst move = MathLib.mat4([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 0, 0, 1]);\nconst p = move.transformPoint([0, 0, 0]);\nconsole.log('moved = ' + JSON.stringify(p.toArray()));\n// 3. Identity determinant is 1.\nconsole.log('determinant = ' + id.determinant());\n// 4. The class alias is equivalent.\nconsole.log('alias is identity = ' + new MathLib.Mat4().equals(id));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.Mat4",
      "symbol": "MathLibFactoryApi.Mat4",
      "intent": "scene",
      "code": "// 1. The default constructor gives the identity matrix.\nconst id = new MathLib.Mat4();\nconsole.log('determinant = ' + id.determinant());\n// 2. instanceof narrows a value to a Mat4.\nconsole.log('is Mat4 = ' + (id instanceof MathLib.Mat4));\n// 3. Build a translation via the static factory.\nconst move = MathLib.Mat4.fromTranslation([2, 3, 4]);\nconsole.log('moved = ' + JSON.stringify(move.transformPoint([0, 0, 0]).toArray()));\n// 4. The factory returns the same class.\nconsole.log('factory is Mat4 = ' + (MathLib.mat4() instanceof MathLib.Mat4));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.plane",
      "symbol": "MathLibFactoryApi.plane",
      "intent": "scene",
      "code": "// 1. Build the ground plane (Y up, through the origin).\nconst ground = MathLib.plane([0, 1, 0], 0);\nconsole.log('normal = ' + JSON.stringify(ground.normal.toArray()));\n// 2. Distance is signed — positive above, negative below.\nconsole.log('above = ' + ground.distanceToPoint([0, 5, 0]));\n// 3. Project a point straight down onto the plane.\nconst proj = ground.projectPoint([4, 7, -2]);\nconsole.log('projected = ' + JSON.stringify(proj.toArray()));\n// 4. The default factory is the same ground plane.\nconsole.log('default constant = ' + MathLib.plane().constant);"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.Plane",
      "symbol": "MathLibFactoryApi.Plane",
      "intent": "scene",
      "code": "// 1. Construct the ground plane (Y up, through origin).\nconst ground = new MathLib.Plane([0, 1, 0], 0);\nconsole.log('normal = ' + JSON.stringify(ground.normal.toArray()));\n// 2. instanceof narrows a value to a Plane.\nconsole.log('is Plane = ' + (ground instanceof MathLib.Plane));\n// 3. Build through a point with a normal via the static factory.\nconst offset = MathLib.Plane.fromPointAndNormal([0, 2, 0], [0, 1, 0]);\nconsole.log('constant = ' + offset.constant);\n// 4. The factory returns the same class.\nconsole.log('factory is Plane = ' + (MathLib.plane() instanceof MathLib.Plane));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.quat",
      "symbol": "MathLibFactoryApi.quat",
      "intent": "scene",
      "code": "// 1. Default is the identity quaternion.\nconst id = MathLib.quat();\nconsole.log('identity = ' + JSON.stringify(id.toArray()));\n// 2. Identity leaves a vector unchanged.\nconsole.log('unchanged = ' + JSON.stringify(id.rotate([1, 2, 3]).toArray()));\n// 3. Build an explicit 90 degree Z rotation by component.\nconst qz = MathLib.quat(0, 0, 0.7071, 0.7071);\nconst rotated = qz.rotate([1, 0, 0]).toArray().map(n => Math.round(n * 1000) / 1000);\nconsole.log('rotated = ' + JSON.stringify(rotated));\n// 4. Read the w component.\nconsole.log('w = ' + qz.w.toFixed(4));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.Quat",
      "symbol": "MathLibFactoryApi.Quat",
      "intent": "scene",
      "code": "// 1. The default constructor gives the identity rotation.\nconst id = new MathLib.Quat();\nconsole.log('identity = ' + JSON.stringify(id.toArray()));\n// 2. instanceof narrows a value to a Quat.\nconsole.log('is Quat = ' + (id instanceof MathLib.Quat));\n// 3. Build a 90 degree Z rotation via the static factory.\nconst qz = MathLib.Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\nconst rotated = qz.rotate([1, 0, 0]).toArray().map(n => Math.round(n * 1000) / 1000);\nconsole.log('rotated = ' + JSON.stringify(rotated));\n// 4. The factory returns the same class.\nconsole.log('factory is Quat = ' + (MathLib.quat() instanceof MathLib.Quat));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.sphere",
      "symbol": "MathLibFactoryApi.sphere",
      "intent": "scene",
      "code": "// 1. Build a sphere of radius 5 at the origin.\nconst s = MathLib.sphere([0, 0, 0], 5);\nconsole.log('radius = ' + s.radius);\n// 2. Containment check — origin is inside, far point is outside.\nconsole.log('contains origin = ' + s.contains([0, 0, 0]));\n// 3. Signed distance is negative inside, positive outside.\nconsole.log('inside = ' + s.distanceToPoint([1, 0, 0]));\n// 4. Read the center back as a tuple.\nconsole.log('center = ' + JSON.stringify(s.center.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.Sphere",
      "symbol": "MathLibFactoryApi.Sphere",
      "intent": "scene",
      "code": "// 1. Construct a unit sphere at the origin.\nconst s = new MathLib.Sphere([0, 0, 0], 1);\nconsole.log('radius = ' + s.radius);\n// 2. instanceof narrows a value to a Sphere.\nconsole.log('is Sphere = ' + (s instanceof MathLib.Sphere));\n// 3. Enclose a set of points via the static factory.\nconst fit = MathLib.Sphere.fromPoints([[1, 0, 0], [0, 1, 0], [0, 0, 1]]);\nconsole.log('enclosing radius > 0 = ' + (fit.radius > 0));\n// 4. The factory returns the same class.\nconsole.log('factory is Sphere = ' + (MathLib.sphere([0, 0, 0], 2) instanceof MathLib.Sphere));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.vec2",
      "symbol": "MathLibFactoryApi.vec2",
      "intent": "scene",
      "code": "// 1. Build a Vec2 and read its components.\nconst v = MathLib.vec2(3, 4);\nconsole.log('components = ' + v.x + ', ' + v.y);\n// 2. The factory and the class alias are equivalent.\nconst sameViaClass = new MathLib.Vec2(3, 4);\nconsole.log('equal = ' + v.equals(sameViaClass));\n// 3. Defaults give the origin.\nconsole.log('origin = ' + JSON.stringify(MathLib.vec2().toArray()));\n// 4. Length of a 3-4 vector is 5.\nconsole.log('length = ' + v.length());"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.Vec2",
      "symbol": "MathLibFactoryApi.Vec2",
      "intent": "scene",
      "code": "// 1. Construct directly via the class alias.\nconst v = new MathLib.Vec2(3, 4);\nconsole.log('tuple = ' + JSON.stringify(v.toArray()));\n// 2. instanceof narrows a value to a Vec2.\nconsole.log('is Vec2 = ' + (v instanceof MathLib.Vec2));\n// 3. The factory returns the same class.\nconsole.log('factory is Vec2 = ' + (MathLib.vec2(1, 2) instanceof MathLib.Vec2));\n// 4. Static helpers live on the alias.\nconsole.log('one = ' + JSON.stringify(MathLib.Vec2.one().toArray()));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.vec3",
      "symbol": "MathLibFactoryApi.vec3",
      "intent": "scene",
      "code": "// 1. Build a Vec3 and read one component.\nconst v = MathLib.vec3(1, 2, 3);\nconsole.log('x = ' + v.x);\n// 2. Serialize to a raw [x, y, z] tuple.\nconsole.log('tuple = ' + JSON.stringify(v.toArray()));\n// 3. Chain immutable math — every step returns a fresh Vec3.\nconst moved = v.add(MathLib.vec3(0, 0, 5));\nconsole.log('moved = ' + JSON.stringify(moved.toArray()));\n// 4. Defaults give the origin.\nconsole.log('origin = ' + JSON.stringify(MathLib.vec3().toArray()));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.Vec3",
      "symbol": "MathLibFactoryApi.Vec3",
      "intent": "scene",
      "code": "// 1. Construct directly via the class alias.\nconst v = new MathLib.Vec3(1, 2, 3);\nconsole.log('tuple = ' + JSON.stringify(v.toArray()));\n// 2. instanceof narrows a value to a Vec3.\nconsole.log('is Vec3 = ' + (v instanceof MathLib.Vec3));\n// 3. Static axis helpers live on the alias.\nconsole.log('unitY = ' + JSON.stringify(MathLib.Vec3.unitY().toArray()));\n// 4. Build from a raw tuple.\nconsole.log('from tuple = ' + JSON.stringify(MathLib.Vec3.from([4, 5, 6]).toArray()));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.vec4",
      "symbol": "MathLibFactoryApi.vec4",
      "intent": "scene",
      "code": "// 1. Build a homogeneous point (w = 1).\nconst point = MathLib.vec4(1, 2, 3, 1);\nconsole.log('point = ' + JSON.stringify(point.toArray()));\n// 2. Build a direction (w = 0).\nconst dir = MathLib.vec4(0, 1, 0, 0);\nconsole.log('w = ' + dir.w);\n// 3. Add two Vec4 instances component-wise.\nconst sum = point.add(dir);\nconsole.log('sum = ' + JSON.stringify(sum.toArray()));\n// 4. Defaults give the zero vector.\nconsole.log('zero = ' + JSON.stringify(MathLib.vec4().toArray()));"
    },
    {
      "kind": "example",
      "id": "example:MathLibFactoryApi.Vec4",
      "symbol": "MathLibFactoryApi.Vec4",
      "intent": "scene",
      "code": "// 1. Construct a homogeneous point (w = 1).\nconst v = new MathLib.Vec4(1, 2, 3, 1);\nconsole.log('tuple = ' + JSON.stringify(v.toArray()));\n// 2. instanceof narrows a value to a Vec4.\nconsole.log('is Vec4 = ' + (v instanceof MathLib.Vec4));\n// 3. The factory returns the same class.\nconsole.log('factory is Vec4 = ' + (MathLib.vec4(0, 0, 0, 0) instanceof MathLib.Vec4));\n// 4. Read the homogeneous w component.\nconsole.log('w = ' + v.w);"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.mat4Decompose",
      "symbol": "MathLibMatrixApi.mat4Decompose",
      "intent": "scene",
      "code": "// 1. Decompose a pure translation.\nconst t = MathLib.mat4FromTRS([7, 8, 9], [0, 0, 0, 1], [1, 1, 1]);\nconsole.log('position = ' + JSON.stringify(MathLib.mat4Decompose(t).position));\n// 2. Decompose extracts the per-axis scale.\nconst s = MathLib.mat4FromTRS([0, 0, 0], [0, 0, 0, 1], [2, 3, 4]);\nconsole.log('scale = ' + JSON.stringify(MathLib.mat4Decompose(s).scale));\n// 3. Decompose recovers the rotation quaternion.\nconst r = MathLib.mat4FromTRS([0, 0, 0], MathLib.quatFromAxisAngle([0, 1, 0], Math.PI / 2), [1, 1, 1]);\nconsole.log('rotation = ' + JSON.stringify(MathLib.mat4Decompose(r).rotation));\n// 4. The identity decomposes to zero translation and unit scale.\nconsole.log('identity = ' + JSON.stringify(MathLib.mat4Decompose(MathLib.mat4Identity())));"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.mat4FromTRS",
      "symbol": "MathLibMatrixApi.mat4FromTRS",
      "intent": "scene",
      "code": "// 1. A pure translation matrix moves the origin.\nconst m = MathLib.mat4FromTRS([5, 0, 0], [0, 0, 0, 1], [1, 1, 1]);\nconsole.log('moved origin = ' + JSON.stringify(MathLib.mat4TransformPoint(m, [0, 0, 0])));\n// 2. A pure scale matrix stretches a point.\nconst s = MathLib.mat4FromTRS([0, 0, 0], [0, 0, 0, 1], [2, 2, 2]);\nconsole.log('scaled = ' + JSON.stringify(MathLib.mat4TransformPoint(s, [1, 1, 1])));\n// 3. A rotation about Y turns +X toward -Z (90°).\nconst rot = MathLib.mat4FromTRS([0, 0, 0], MathLib.quatFromAxisAngle([0, 1, 0], Math.PI / 2), [1, 1, 1]);\nconsole.log('rotated = ' + JSON.stringify(MathLib.mat4TransformVector(rot, [1, 0, 0])));\n// 4. Decompose round-trips back to the inputs.\nconsole.log('decomposed = ' + JSON.stringify(MathLib.mat4Decompose(m).position));"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.mat4Identity",
      "symbol": "MathLibMatrixApi.mat4Identity",
      "intent": "scene",
      "code": "// 1. The identity matrix has 1s on the diagonal.\nconst I = MathLib.mat4Identity();\nconsole.log('identity = ' + JSON.stringify(I));\n// 2. Transforming a point by it leaves the point unchanged.\nconsole.log('point = ' + JSON.stringify(MathLib.mat4TransformPoint(I, [3, 4, 5])));\n// 3. Its inverse is itself.\nconsole.log('inverse = ' + JSON.stringify(MathLib.mat4Inverse(I)));\n// 4. Its transpose is also itself.\nconsole.log('transpose = ' + JSON.stringify(MathLib.mat4Transpose(I)));"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.mat4Inverse",
      "symbol": "MathLibMatrixApi.mat4Inverse",
      "intent": "scene",
      "code": "// 1. The inverse of the identity is the identity.\nconsole.log('I⁻¹ = ' + JSON.stringify(MathLib.mat4Inverse(MathLib.mat4Identity())));\n// 2. Inverting a translation negates the offset.\nconst t = MathLib.mat4FromTRS([4, 0, 0], [0, 0, 0, 1], [1, 1, 1]);\nconsole.log('undo move = ' + JSON.stringify(MathLib.mat4TransformPoint(MathLib.mat4Inverse(t), [4, 0, 0])));\n// 3. m × m⁻¹ collapses back to the identity.\nconsole.log('round-trip = ' + JSON.stringify(MathLib.mat4Multiply(t, MathLib.mat4Inverse(t))));\n// 4. A singular (zero-scale) matrix returns the identity by contract.\nconst singular = MathLib.mat4FromTRS([0, 0, 0], [0, 0, 0, 1], [0, 0, 0]);\nconsole.log('singular → I = ' + JSON.stringify(MathLib.mat4Inverse(singular)));"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.mat4Multiply",
      "symbol": "MathLibMatrixApi.mat4Multiply",
      "intent": "scene",
      "code": "// 1. Multiplying by the identity is a no-op.\nconst m = MathLib.mat4Identity();\nconsole.log('m×I = ' + JSON.stringify(MathLib.mat4Multiply(m, MathLib.mat4Identity())));\n// 2. Compose a translation with itself doubles the offset.\nconst t = MathLib.mat4FromTRS([1, 0, 0], [0, 0, 0, 1], [1, 1, 1]);\nconst t2 = MathLib.mat4Multiply(t, t);\nconsole.log('compose = ' + JSON.stringify(MathLib.mat4TransformPoint(t2, [0, 0, 0])));\n// 3. Order matters — a×b is generally not b×a.\nconst a = MathLib.mat4FromTRS([2, 0, 0], [0, 0, 0, 1], [1, 1, 1]);\nconsole.log('a×t = ' + JSON.stringify(MathLib.mat4TransformPoint(MathLib.mat4Multiply(a, t), [0, 0, 0])));\n// 4. A matrix times its inverse returns the identity.\nconsole.log('m×m⁻¹ = ' + JSON.stringify(MathLib.mat4Multiply(t, MathLib.mat4Inverse(t))));"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.mat4TransformNormal",
      "symbol": "MathLibMatrixApi.mat4TransformNormal",
      "intent": "scene",
      "code": "// 1. The identity returns the same (unit) normal.\nconsole.log('id = ' + JSON.stringify(MathLib.mat4TransformNormal(MathLib.mat4Identity(), [0, 1, 0])));\n// 2. The result is always re-normalized to unit length.\nconst s = MathLib.mat4FromTRS([0, 0, 0], [0, 0, 0, 1], [5, 5, 5]);\nconsole.log('unit = ' + JSON.stringify(MathLib.mat4TransformNormal(s, [0, 1, 0])));\n// 3. Rotation about X turns +Y toward +Z (90°).\nconst rot = MathLib.mat4FromTRS([0, 0, 0], MathLib.quatFromAxisAngle([1, 0, 0], Math.PI / 2), [1, 1, 1]);\nconsole.log('rotated = ' + JSON.stringify(MathLib.mat4TransformNormal(rot, [0, 1, 0])));\n// 4. Translation never affects a normal.\nconst t = MathLib.mat4FromTRS([8, 8, 8], [0, 0, 0, 1], [1, 1, 1]);\nconsole.log('no shift = ' + JSON.stringify(MathLib.mat4TransformNormal(t, [0, 0, 1])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.mat4TransformPoint",
      "symbol": "MathLibMatrixApi.mat4TransformPoint",
      "intent": "scene",
      "code": "// 1. The identity leaves a point unchanged.\nconsole.log('point = ' + JSON.stringify(MathLib.mat4TransformPoint(MathLib.mat4Identity(), [1, 2, 3])));\n// 2. A translation moves the point.\nconst t = MathLib.mat4FromTRS([10, 0, 0], [0, 0, 0, 1], [1, 1, 1]);\nconsole.log('moved = ' + JSON.stringify(MathLib.mat4TransformPoint(t, [0, 0, 0])));\n// 3. A scale stretches the point about the origin.\nconst s = MathLib.mat4FromTRS([0, 0, 0], [0, 0, 0, 1], [3, 1, 1]);\nconsole.log('scaled = ' + JSON.stringify(MathLib.mat4TransformPoint(s, [2, 0, 0])));\n// 4. Unlike mat4TransformVector, the offset DOES affect the result.\nconsole.log('vs vector = ' + JSON.stringify(MathLib.mat4TransformVector(t, [0, 0, 0])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.mat4TransformVec4",
      "symbol": "MathLibMatrixApi.mat4TransformVec4",
      "intent": "scene",
      "code": "// 1. With w = 1 the translation IS applied (point).\nconst t = MathLib.mat4FromTRS([5, 0, 0], [0, 0, 0, 1], [1, 1, 1]);\nconsole.log('point = ' + JSON.stringify(MathLib.mat4TransformVec4(t, [0, 0, 0, 1])));\n// 2. With w = 0 the translation is IGNORED (direction).\nconsole.log('dir = ' + JSON.stringify(MathLib.mat4TransformVec4(t, [0, 1, 0, 0])));\n// 3. The identity preserves all four components.\nconsole.log('id = ' + JSON.stringify(MathLib.mat4TransformVec4(MathLib.mat4Identity(), [1, 2, 3, 1])));\n// 4. Scale stretches the xyz part, w is left alone.\nconst s = MathLib.mat4FromTRS([0, 0, 0], [0, 0, 0, 1], [2, 2, 2]);\nconsole.log('scaled = ' + JSON.stringify(MathLib.mat4TransformVec4(s, [1, 1, 1, 1])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.mat4TransformVector",
      "symbol": "MathLibMatrixApi.mat4TransformVector",
      "intent": "scene",
      "code": "// 1. A direction is unaffected by translation.\nconst t = MathLib.mat4FromTRS([99, 0, 0], [0, 0, 0, 1], [1, 1, 1]);\nconsole.log('dir = ' + JSON.stringify(MathLib.mat4TransformVector(t, [0, 1, 0])));\n// 2. Rotation about Z turns +X toward +Y (90°).\nconst rot = MathLib.mat4FromTRS([0, 0, 0], MathLib.quatFromAxisAngle([0, 0, 1], Math.PI / 2), [1, 1, 1]);\nconsole.log('rotated = ' + JSON.stringify(MathLib.mat4TransformVector(rot, [1, 0, 0])));\n// 3. Scale changes the direction's length.\nconst s = MathLib.mat4FromTRS([0, 0, 0], [0, 0, 0, 1], [2, 2, 2]);\nconsole.log('scaled = ' + JSON.stringify(MathLib.mat4TransformVector(s, [1, 0, 0])));\n// 4. The identity leaves the direction unchanged.\nconsole.log('id = ' + JSON.stringify(MathLib.mat4TransformVector(MathLib.mat4Identity(), [3, 4, 0])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.mat4Transpose",
      "symbol": "MathLibMatrixApi.mat4Transpose",
      "intent": "scene",
      "code": "// 1. Transposing the identity is a no-op.\nconsole.log('Iᵀ = ' + JSON.stringify(MathLib.mat4Transpose(MathLib.mat4Identity())));\n// 2. Transposing twice recovers the original.\nconst t = MathLib.mat4FromTRS([1, 2, 3], [0, 0, 0, 1], [1, 1, 1]);\nconsole.log('round-trip = ' + JSON.stringify(MathLib.mat4Transpose(MathLib.mat4Transpose(t))));\n// 3. A pure-rotation transpose equals its inverse.\nconst rot = MathLib.mat4FromTRS([0, 0, 0], MathLib.quatFromAxisAngle([0, 0, 1], Math.PI / 4), [1, 1, 1]);\nconsole.log('Rᵀ = ' + JSON.stringify(MathLib.mat4Transpose(rot)));\n// 4. Compare against the rotation's inverse (should match).\nconsole.log('R⁻¹ = ' + JSON.stringify(MathLib.mat4Inverse(rot)));"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.transformPoint",
      "symbol": "MathLibMatrixApi.transformPoint",
      "intent": "scene",
      "code": "// 1. transformPoint applies the translation (point semantics).\nconst t = MathLib.mat4FromTRS([6, 0, 0], [0, 0, 0, 1], [1, 1, 1]);\nconsole.log('moved = ' + JSON.stringify(MathLib.transformPoint(t, [0, 0, 0])));\n// 2. It is the exact alias of mat4TransformPoint.\nconsole.log('same = ' + JSON.stringify(MathLib.mat4TransformPoint(t, [1, 1, 1])));\n// 3. The identity leaves the point unchanged.\nconsole.log('id = ' + JSON.stringify(MathLib.transformPoint(MathLib.mat4Identity(), [2, 3, 4])));\n// 4. A scale stretches the point about the origin.\nconst s = MathLib.mat4FromTRS([0, 0, 0], [0, 0, 0, 1], [2, 2, 2]);\nconsole.log('scaled = ' + JSON.stringify(MathLib.transformPoint(s, [1, 0, 0])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.transformVec4",
      "symbol": "MathLibMatrixApi.transformVec4",
      "intent": "scene",
      "code": "// 1. With w = 1 the translation IS applied (point).\nconst t = MathLib.mat4FromTRS([3, 0, 0], [0, 0, 0, 1], [1, 1, 1]);\nconsole.log('point = ' + JSON.stringify(MathLib.transformVec4(t, [0, 0, 0, 1])));\n// 2. With w = 0 the translation is IGNORED (direction).\nconsole.log('dir = ' + JSON.stringify(MathLib.transformVec4(t, [1, 0, 0, 0])));\n// 3. It is the exact alias of mat4TransformVec4.\nconsole.log('same = ' + JSON.stringify(MathLib.mat4TransformVec4(t, [0, 0, 0, 1])));\n// 4. The identity preserves all four components.\nconsole.log('id = ' + JSON.stringify(MathLib.transformVec4(MathLib.mat4Identity(), [1, 2, 3, 1])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibMatrixApi.transformVector",
      "symbol": "MathLibMatrixApi.transformVector",
      "intent": "scene",
      "code": "// 1. transformVector ignores the translation (direction semantics).\nconst t = MathLib.mat4FromTRS([50, 0, 0], [0, 0, 0, 1], [1, 1, 1]);\nconsole.log('dir = ' + JSON.stringify(MathLib.transformVector(t, [0, 1, 0])));\n// 2. It is the exact alias of mat4TransformVector.\nconsole.log('same = ' + JSON.stringify(MathLib.mat4TransformVector(t, [0, 1, 0])));\n// 3. Rotation about Z turns +X toward +Y (90°).\nconst rot = MathLib.mat4FromTRS([0, 0, 0], MathLib.quatFromAxisAngle([0, 0, 1], Math.PI / 2), [1, 1, 1]);\nconsole.log('rotated = ' + JSON.stringify(MathLib.transformVector(rot, [1, 0, 0])));\n// 4. The identity leaves the direction unchanged.\nconsole.log('id = ' + JSON.stringify(MathLib.transformVector(MathLib.mat4Identity(), [3, 4, 0])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibQuatApi.quatConjugate",
      "symbol": "MathLibQuatApi.quatConjugate",
      "intent": "scene",
      "code": "// 1. Conjugate flips the sign of x, y, z.\nconsole.log('conj = ' + JSON.stringify(MathLib.quatConjugate([0.5, 0.5, 0.5, 0.5])));\n// 2. Conjugating twice recovers the original.\nconst q = MathLib.quatFromAxisAngle([0, 0, 1], Math.PI / 4);\nconsole.log('round-trip = ' + JSON.stringify(MathLib.quatConjugate(MathLib.quatConjugate(q))));\n// 3. For a unit quaternion, conjugate equals inverse.\nconsole.log('= inverse = ' + JSON.stringify(MathLib.quatConjugate(q)));\n// 4. Compose with the original to get identity.\nconsole.log('q×conj = ' + JSON.stringify(MathLib.quatMultiply(q, MathLib.quatConjugate(q))));"
    },
    {
      "kind": "example",
      "id": "example:MathLibQuatApi.quatFromAxisAngle",
      "symbol": "MathLibQuatApi.quatFromAxisAngle",
      "intent": "scene",
      "code": "// 1. 90° about the Y axis.\nconsole.log('yaw = ' + JSON.stringify(MathLib.quatFromAxisAngle([0, 1, 0], Math.PI / 2)));\n// 2. A non-unit axis is normalized for you.\nconsole.log('scaled axis = ' + JSON.stringify(MathLib.quatFromAxisAngle([0, 5, 0], Math.PI / 2)));\n// 3. A zero angle yields the identity quaternion.\nconsole.log('identity = ' + JSON.stringify(MathLib.quatFromAxisAngle([1, 0, 0], 0)));\n// 4. Apply the rotation to the X axis.\nconst q = MathLib.quatFromAxisAngle([0, 1, 0], Math.PI / 2);\nconsole.log('x→ = ' + JSON.stringify(MathLib.quatRotateVec3(q, [1, 0, 0])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibQuatApi.quatFromEuler",
      "symbol": "MathLibQuatApi.quatFromEuler",
      "intent": "scene",
      "code": "// 1. A pure 90° yaw about Y.\nconsole.log('yaw = ' + JSON.stringify(MathLib.quatFromEuler(0, Math.PI / 2, 0)));\n// 2. Zero angles give the identity quaternion.\nconsole.log('identity = ' + JSON.stringify(MathLib.quatFromEuler(0, 0, 0)));\n// 3. Combine pitch and roll in one call.\nconst q = MathLib.quatFromEuler(Math.PI / 4, 0, Math.PI / 4);\nconsole.log('pitch+roll = ' + JSON.stringify(q));\n// 4. Use the result to rotate a vector.\nconsole.log('rotated = ' + JSON.stringify(MathLib.quatRotateVec3(q, [0, 1, 0])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibQuatApi.quatFromUnitVectors",
      "symbol": "MathLibQuatApi.quatFromUnitVectors",
      "intent": "scene",
      "code": "// 1. Rotate +X onto +Y.\nconsole.log('x→y = ' + JSON.stringify(MathLib.quatFromUnitVectors([1, 0, 0], [0, 1, 0])));\n// 2. Identical directions give the identity rotation.\nconsole.log('same = ' + JSON.stringify(MathLib.quatFromUnitVectors([0, 0, 1], [0, 0, 1])));\n// 3. Apply the rotation to confirm it lands on the target.\nconst q = MathLib.quatFromUnitVectors([1, 0, 0], [0, 0, 1]);\nconsole.log('landed = ' + JSON.stringify(MathLib.quatRotateVec3(q, [1, 0, 0])));\n// 4. Align an arbitrary (non-unit) pair by normalizing first.\nconst aligned = MathLib.quatFromUnitVectors(MathLib.normalize([2, 0, 0]), MathLib.normalize([0, 3, 0]));\nconsole.log('aligned = ' + JSON.stringify(aligned));"
    },
    {
      "kind": "example",
      "id": "example:MathLibQuatApi.quatInverse",
      "symbol": "MathLibQuatApi.quatInverse",
      "intent": "scene",
      "code": "// 1. Invert a 90° roll.\nconst roll = MathLib.quatFromAxisAngle([0, 0, 1], Math.PI / 2);\nconsole.log('inverse = ' + JSON.stringify(MathLib.quatInverse(roll)));\n// 2. Inverting twice recovers the original rotation.\nconsole.log('round-trip = ' + JSON.stringify(MathLib.quatInverse(MathLib.quatInverse(roll))));\n// 3. A rotation times its inverse is identity.\nconsole.log('q×q⁻¹ = ' + JSON.stringify(MathLib.quatMultiply(roll, MathLib.quatInverse(roll))));\n// 4. Inverse undoes a rotation applied to a vector.\nconst rotated = MathLib.quatRotateVec3(roll, [1, 0, 0]);\nconsole.log('restored = ' + JSON.stringify(MathLib.quatRotateVec3(MathLib.quatInverse(roll), rotated)));"
    },
    {
      "kind": "example",
      "id": "example:MathLibQuatApi.quatMultiply",
      "symbol": "MathLibQuatApi.quatMultiply",
      "intent": "scene",
      "code": "// 1. Combine a 90° yaw with a 90° pitch.\nconst yaw = MathLib.quatFromAxisAngle([0, 1, 0], Math.PI / 2);\nconst pitch = MathLib.quatFromAxisAngle([1, 0, 0], Math.PI / 2);\nconsole.log('combined = ' + JSON.stringify(MathLib.quatMultiply(yaw, pitch)));\n// 2. Multiplying by identity leaves a rotation unchanged.\nconsole.log('×identity = ' + JSON.stringify(MathLib.quatMultiply(yaw, [0, 0, 0, 1])));\n// 3. Order matters — quaternion multiply is NOT commutative.\nconsole.log('reversed = ' + JSON.stringify(MathLib.quatMultiply(pitch, yaw)));\n// 4. A rotation times its inverse returns identity.\nconsole.log('q×q⁻¹ = ' + JSON.stringify(MathLib.quatMultiply(yaw, MathLib.quatInverse(yaw))));"
    },
    {
      "kind": "example",
      "id": "example:MathLibQuatApi.quatNormalize",
      "symbol": "MathLibQuatApi.quatNormalize",
      "intent": "scene",
      "code": "// 1. Normalize a hand-typed, non-unit quaternion.\nconsole.log('unit = ' + JSON.stringify(MathLib.quatNormalize([0, 0, 0, 2])));\n// 2. An already-unit rotation is unchanged.\nconst r = MathLib.quatFromAxisAngle([0, 1, 0], Math.PI / 3);\nconsole.log('stable = ' + JSON.stringify(MathLib.quatNormalize(r)));\n// 3. A zero quaternion falls back to identity.\nconsole.log('zero → identity = ' + JSON.stringify(MathLib.quatNormalize([0, 0, 0, 0])));\n// 4. Re-normalize after composing two rotations.\nconst composed = MathLib.quatMultiply(r, r);\nconsole.log('clean = ' + JSON.stringify(MathLib.quatNormalize(composed)));"
    },
    {
      "kind": "example",
      "id": "example:MathLibQuatApi.quatRotateVec3",
      "symbol": "MathLibQuatApi.quatRotateVec3",
      "intent": "scene",
      "code": "// 1. Rotate +X by 90° yaw to get +Z.\nconst yaw = MathLib.quatFromAxisAngle([0, 1, 0], Math.PI / 2);\nconsole.log('x→ = ' + JSON.stringify(MathLib.quatRotateVec3(yaw, [1, 0, 0])));\n// 2. The identity quaternion leaves the vector unchanged.\nconsole.log('identity = ' + JSON.stringify(MathLib.quatRotateVec3([0, 0, 0, 1], [3, 4, 5])));\n// 3. Rotation preserves vector length.\nconst r = MathLib.quatRotateVec3(yaw, [3, 0, 0]);\nconsole.log('len kept = ' + MathLib.length(r).toFixed(4));\n// 4. The inverse rotation brings the vector back.\nconsole.log('restored = ' + JSON.stringify(MathLib.quatRotateVec3(MathLib.quatInverse(yaw), r)));"
    },
    {
      "kind": "example",
      "id": "example:MathLibQuatApi.rotateAxis",
      "symbol": "MathLibQuatApi.rotateAxis",
      "intent": "scene",
      "code": "// 1. Rotate +X 90° about Y to land on +Z.\nconsole.log('x→ = ' + JSON.stringify(MathLib.rotateAxis([1, 0, 0], [0, 1, 0], Math.PI / 2)));\n// 2. A zero angle leaves the vector unchanged.\nconsole.log('no-op = ' + JSON.stringify(MathLib.rotateAxis([3, 4, 5], [0, 1, 0], 0)));\n// 3. Rotating about its own axis leaves a vector in place.\nconsole.log('on axis = ' + JSON.stringify(MathLib.rotateAxis([0, 2, 0], [0, 1, 0], Math.PI / 3)));\n// 4. A full turn returns the original direction.\nconsole.log('full turn = ' + JSON.stringify(MathLib.rotateAxis([1, 0, 0], [0, 1, 0], Math.PI * 2)));"
    },
    {
      "kind": "example",
      "id": "example:MathLibQuatApi.slerp",
      "symbol": "MathLibQuatApi.slerp",
      "intent": "scene",
      "code": "// 1. Halfway between identity and a 90° yaw.\nconst yaw = MathLib.quatFromAxisAngle([0, 1, 0], Math.PI / 2);\nconsole.log('mid = ' + JSON.stringify(MathLib.slerp([0, 0, 0, 1], yaw, 0.5)));\n// 2. t = 0 returns the start rotation.\nconsole.log('start = ' + JSON.stringify(MathLib.slerp([0, 0, 0, 1], yaw, 0)));\n// 3. t = 1 returns the end rotation.\nconsole.log('end = ' + JSON.stringify(MathLib.slerp([0, 0, 0, 1], yaw, 1)));\n// 4. Use the blend to rotate a vector partway.\nconst half = MathLib.slerp([0, 0, 0, 1], yaw, 0.5);\nconsole.log('partway = ' + JSON.stringify(MathLib.quatRotateVec3(half, [1, 0, 0])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibScalarApi.clamp",
      "symbol": "MathLibScalarApi.clamp",
      "intent": "scene",
      "code": "// 1. Value above the range clamps to max.\nconsole.log('clamp(5,0,3) = ' + MathLib.clamp(5, 0, 3));\n// 2. Value below the range clamps to min.\nconsole.log('clamp(-1,0,3) = ' + MathLib.clamp(-1, 0, 3));\n// 3. Value already inside passes through.\nconsole.log('clamp(2,0,3) = ' + MathLib.clamp(2, 0, 3));\n// 4. Keep a normalized weight in [0,1].\nconsole.log('clamp(1.4,0,1) = ' + MathLib.clamp(1.4, 0, 1));"
    },
    {
      "kind": "example",
      "id": "example:MathLibScalarApi.degToRad",
      "symbol": "MathLibScalarApi.degToRad",
      "intent": "scene",
      "code": "// 1. Right angle.\nconsole.log('90deg = ' + MathLib.degToRad(90).toFixed(4) + ' rad');\n// 2. Full turn.\nconsole.log('360deg = ' + MathLib.degToRad(360).toFixed(4) + ' rad');\n// 3. Negative angle.\nconsole.log('-45deg = ' + MathLib.degToRad(-45).toFixed(4) + ' rad');\n// 4. Round-trip through radToDeg.\nconsole.log('round-trip 135 = ' + MathLib.radToDeg(MathLib.degToRad(135)).toFixed(1));"
    },
    {
      "kind": "example",
      "id": "example:MathLibScalarApi.inverseLerp",
      "symbol": "MathLibScalarApi.inverseLerp",
      "intent": "scene",
      "code": "// 1. Midpoint of [0,10] is t = 0.5.\nconsole.log('inverseLerp(0,10,5) = ' + MathLib.inverseLerp(0, 10, 5));\n// 2. A quarter of the way along.\nconsole.log('inverseLerp(0,10,2.5) = ' + MathLib.inverseLerp(0, 10, 2.5));\n// 3. Degenerate range returns 0.\nconsole.log('inverseLerp(4,4,4) = ' + MathLib.inverseLerp(4, 4, 4));\n// 4. Round-trip with lerp recovers the value.\nconst t = MathLib.inverseLerp(10, 20, 13);\nconsole.log('round-trip 13 = ' + MathLib.lerp(10, 20, t));"
    },
    {
      "kind": "example",
      "id": "example:MathLibScalarApi.lerp",
      "symbol": "MathLibScalarApi.lerp",
      "intent": "scene",
      "code": "// 1. Scalar midpoint of 0 and 10.\nconsole.log('lerp(0,10,0.5) = ' + MathLib.lerp(0, 10, 0.5));\n// 2. Vec3 midpoint along the X axis.\nconst mid = MathLib.lerp([0, 0, 0], [10, 0, 0], 0.5);\nconsole.log('vec3 mid = ' + JSON.stringify(mid));\n// 3. t = 0 returns the start, t = 1 returns the end.\nconsole.log('ends: ' + MathLib.lerp(2, 8, 0) + ', ' + MathLib.lerp(2, 8, 1));\n// 4. Mixing a scalar with a tuple throws (the pre-277-01 lerp silently\n//    returned NaN). A small helper reports whether a call threw.\nconst threw = (fn) => { try {\n    fn();\n    return false;\n}\ncatch {\n    return true;\n} };\n// The mismatched call is rejected loudly instead of returning NaN.\nconsole.log('mixed threw: ' + threw(() => MathLib.lerp(0, [1, 2, 3], 0.5)));\n// The matched scalar call still succeeds.\nconsole.log('scalar ok: ' + !threw(() => MathLib.lerp(0, 10, 0.5)));"
    },
    {
      "kind": "example",
      "id": "example:MathLibScalarApi.mapLinear",
      "symbol": "MathLibScalarApi.mapLinear",
      "intent": "scene",
      "code": "// 1. Map the middle of [0,10] onto [0,100].\nconsole.log('mapLinear(5,0,10,0,100) = ' + MathLib.mapLinear(5, 0, 10, 0, 100));\n// 2. Map a 0..1 fraction onto a -1..1 signal.\nconsole.log('mapLinear(0.25,0,1,-1,1) = ' + MathLib.mapLinear(0.25, 0, 1, -1, 1));\n// 3. Endpoints map exactly to the destination endpoints.\nconsole.log('mapLinear(10,0,10,0,100) = ' + MathLib.mapLinear(10, 0, 10, 0, 100));\n// 4. Inverted destination range flips direction.\nconsole.log('mapLinear(0,0,10,100,0) = ' + MathLib.mapLinear(0, 0, 10, 100, 0));"
    },
    {
      "kind": "example",
      "id": "example:MathLibScalarApi.radToDeg",
      "symbol": "MathLibScalarApi.radToDeg",
      "intent": "scene",
      "code": "// 1. Half turn (PI radians).\nconsole.log('PI rad = ' + MathLib.radToDeg(Math.PI).toFixed(1) + ' deg');\n// 2. Quarter turn (PI/2 radians).\nconsole.log('PI/2 rad = ' + MathLib.radToDeg(Math.PI / 2).toFixed(1) + ' deg');\n// 3. One radian.\nconsole.log('1 rad = ' + MathLib.radToDeg(1).toFixed(2) + ' deg');\n// 4. Round-trip through degToRad.\nconsole.log('round-trip 30 = ' + MathLib.degToRad(MathLib.radToDeg(0.5)).toFixed(2));"
    },
    {
      "kind": "example",
      "id": "example:MathLibScalarApi.randFloat",
      "symbol": "MathLibScalarApi.randFloat",
      "intent": "scene",
      "code": "// 1. The same seed yields the same draw every time.\nconst a = MathLib.randFloat(0, 1, 42);\nconst b = MathLib.randFloat(0, 1, 42);\nconsole.log('seeded equal: ' + (a === b));\n// 2. The draw stays inside the requested range.\nconsole.log('in [0,1): ' + (a >= 0 && a < 1));\n// 3. A wider range scales the same seed.\nconsole.log('scaled: ' + MathLib.randFloat(0, 100, 42).toFixed(2));\n// 4. Different seeds (usually) differ.\nconsole.log('seeds differ: ' + (MathLib.randFloat(0, 1, 1) !== MathLib.randFloat(0, 1, 2)));"
    },
    {
      "kind": "example",
      "id": "example:MathLibScalarApi.randInt",
      "symbol": "MathLibScalarApi.randInt",
      "intent": "scene",
      "code": "// 1. Roll a six-sided die with a fixed seed.\nconst roll = MathLib.randInt(1, 6, 7);\nconsole.log('die roll: ' + roll);\n// 2. The result is a whole number.\nconsole.log('is integer: ' + Number.isInteger(roll));\n// 3. It lands inside the inclusive range.\nconsole.log('in [1,6]: ' + (roll >= 1 && roll <= 6));\n// 4. The same seed always rolls the same value.\nconsole.log('seeded equal: ' + (MathLib.randInt(1, 6, 7) === roll));"
    },
    {
      "kind": "example",
      "id": "example:MathLibScalarApi.seededRandom",
      "symbol": "MathLibScalarApi.seededRandom",
      "intent": "scene",
      "code": "// 1. Create a stream from a seed.\nconst rng = MathLib.seededRandom(7);\n// 2. Draw the first two values.\nconst first = rng();\nconst second = rng();\nconsole.log('first in [0,1): ' + (first >= 0 && first < 1));\n// 3. Successive draws advance the stream.\nconsole.log('advances: ' + (first !== second));\n// 4. A fresh closure from the same seed replays the stream.\nconst rng2 = MathLib.seededRandom(7);\nconsole.log('replays: ' + (rng2() === first));"
    },
    {
      "kind": "example",
      "id": "example:MathLibScalarApi.smoothstep",
      "symbol": "MathLibScalarApi.smoothstep",
      "intent": "scene",
      "code": "// 1. Below the lower edge is 0.\nconsole.log('smoothstep(0,1,-0.5) = ' + MathLib.smoothstep(0, 1, -0.5));\n// 2. The midpoint sits at exactly 0.5.\nconsole.log('smoothstep(0,1,0.5) = ' + MathLib.smoothstep(0, 1, 0.5));\n// 3. Above the upper edge is 1.\nconsole.log('smoothstep(0,1,2) = ' + MathLib.smoothstep(0, 1, 2));\n// 4. A quarter in eases gently (less than linear 0.25).\nconsole.log('smoothstep(0,1,0.25) = ' + MathLib.smoothstep(0, 1, 0.25).toFixed(4));"
    },
    {
      "kind": "example",
      "id": "example:MathLibVectorApi.add",
      "symbol": "MathLibVectorApi.add",
      "intent": "scene",
      "code": "// 1. Add two vectors component-wise.\nconsole.log('sum = ' + JSON.stringify(MathLib.add([1, 2, 3], [4, 5, 6])));\n// 2. Adding the zero vector is a no-op.\nconsole.log('plus zero = ' + JSON.stringify(MathLib.add([7, 8, 9], [0, 0, 0])));\n// 3. Offset a point by a direction.\nconst moved = MathLib.add([0, 1, 0], [0, 0, 5]);\nconsole.log('moved = ' + JSON.stringify(moved));\n// 4. Chain with sub to recover the original.\nconsole.log('round-trip = ' + JSON.stringify(MathLib.sub(MathLib.add([2, 2, 2], [3, 3, 3]), [3, 3, 3])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibVectorApi.cross",
      "symbol": "MathLibVectorApi.cross",
      "intent": "scene",
      "code": "// 1. X cross Y gives Z (right-handed).\nconsole.log('x×y = ' + JSON.stringify(MathLib.cross([1, 0, 0], [0, 1, 0])));\n// 2. Order matters — y×x flips the sign.\nconsole.log('y×x = ' + JSON.stringify(MathLib.cross([0, 1, 0], [1, 0, 0])));\n// 3. Cross of parallel vectors is zero.\nconsole.log('parallel = ' + JSON.stringify(MathLib.cross([2, 0, 0], [5, 0, 0])));\n// 4. Build a surface normal from two edge directions.\nconst normal = MathLib.normalize(MathLib.cross([1, 0, 0], [0, 0, 1]));\nconsole.log('normal = ' + JSON.stringify(normal));"
    },
    {
      "kind": "example",
      "id": "example:MathLibVectorApi.distance",
      "symbol": "MathLibVectorApi.distance",
      "intent": "scene",
      "code": "// 1. Distance along one axis.\nconsole.log('dist = ' + MathLib.distance([0, 0, 0], [3, 0, 0]));\n// 2. 3-4-5 right triangle.\nconsole.log('hypotenuse = ' + MathLib.distance([0, 0, 0], [3, 4, 0]));\n// 3. Distance to itself is 0.\nconsole.log('self = ' + MathLib.distance([2, 2, 2], [2, 2, 2]));\n// 4. Symmetric — order does not matter.\nconsole.log('symmetric = ' + (MathLib.distance([1, 2, 3], [4, 5, 6]) === MathLib.distance([4, 5, 6], [1, 2, 3])));"
    },
    {
      "kind": "example",
      "id": "example:MathLibVectorApi.dot",
      "symbol": "MathLibVectorApi.dot",
      "intent": "scene",
      "code": "// 1. Dot of two vectors.\nconsole.log('dot = ' + MathLib.dot([1, 2, 3], [4, 5, 6]));\n// 2. Perpendicular vectors dot to 0.\nconsole.log('perp = ' + MathLib.dot([1, 0, 0], [0, 1, 0]));\n// 3. Dot with self equals length squared.\nconsole.log('lenSq = ' + MathLib.dot([3, 4, 0], [3, 4, 0]));\n// 4. Sign reveals same/opposite facing direction.\nconsole.log('facing = ' + (MathLib.dot([1, 0, 0], [-1, 0, 0]) < 0 ? 'opposite' : 'same'));"
    },
    {
      "kind": "example",
      "id": "example:MathLibVectorApi.length",
      "symbol": "MathLibVectorApi.length",
      "intent": "scene",
      "code": "// 1. Length of a 3-4-0 vector is 5.\nconsole.log('len = ' + MathLib.length([3, 4, 0]));\n// 2. A unit vector has length 1.\nconsole.log('unit len = ' + MathLib.length([1, 0, 0]));\n// 3. The zero vector has length 0.\nconsole.log('zero len = ' + MathLib.length([0, 0, 0]));\n// 4. length is the square root of lengthSq.\nconsole.log('sqrt(lenSq) = ' + Math.sqrt(MathLib.lengthSq([1, 2, 2])).toFixed(4));"
    },
    {
      "kind": "example",
      "id": "example:MathLibVectorApi.lengthSq",
      "symbol": "MathLibVectorApi.lengthSq",
      "intent": "scene",
      "code": "// 1. Squared length of 3-4-0 is 25.\nconsole.log('lenSq = ' + MathLib.lengthSq([3, 4, 0]));\n// 2. Compare reach without a sqrt.\nconsole.log('farther = ' + (MathLib.lengthSq([5, 0, 0]) > MathLib.lengthSq([3, 0, 0])));\n// 3. Zero vector has squared length 0.\nconsole.log('zero = ' + MathLib.lengthSq([0, 0, 0]));\n// 4. Equals the dot product with itself.\nconsole.log('dot self = ' + MathLib.dot([1, 2, 2], [1, 2, 2]));"
    },
    {
      "kind": "example",
      "id": "example:MathLibVectorApi.negate",
      "symbol": "MathLibVectorApi.negate",
      "intent": "scene",
      "code": "// 1. Flip a direction.\nconsole.log('neg = ' + JSON.stringify(MathLib.negate([1, -2, 3])));\n// 2. Negating twice recovers the original.\nconsole.log('round-trip = ' + JSON.stringify(MathLib.negate(MathLib.negate([4, 5, 6]))));\n// 3. The reverse of a facing direction.\nconsole.log('back = ' + JSON.stringify(MathLib.negate([0, 0, 1])));\n// 4. Subtracting equals adding the negation.\nconsole.log('a-b = ' + JSON.stringify(MathLib.add([5, 5, 5], MathLib.negate([1, 2, 3]))));"
    },
    {
      "kind": "example",
      "id": "example:MathLibVectorApi.normalize",
      "symbol": "MathLibVectorApi.normalize",
      "intent": "scene",
      "code": "// 1. Normalize an axis-aligned vector.\nconsole.log('unit = ' + JSON.stringify(MathLib.normalize([0, 5, 0])));\n// 2. A diagonal becomes length 1.\nconst n = MathLib.normalize([3, 4, 0]);\nconsole.log('len = ' + MathLib.length(n).toFixed(4));\n// 3. Direction is preserved, only the magnitude changes.\nconsole.log('dir = ' + JSON.stringify(MathLib.normalize([10, 0, 0])));\n// 4. Use it to scale to a fixed reach.\nconsole.log('reach 2 = ' + JSON.stringify(MathLib.scale(MathLib.normalize([0, 0, 8]), 2)));"
    },
    {
      "kind": "example",
      "id": "example:MathLibVectorApi.scale",
      "symbol": "MathLibVectorApi.scale",
      "intent": "scene",
      "code": "// 1. Double a vector.\nconsole.log('×2 = ' + JSON.stringify(MathLib.scale([1, 2, 3], 2)));\n// 2. Halve a vector.\nconsole.log('×0.5 = ' + JSON.stringify(MathLib.scale([4, 8, 2], 0.5)));\n// 3. Scaling by 0 gives the zero vector.\nconsole.log('×0 = ' + JSON.stringify(MathLib.scale([9, 9, 9], 0)));\n// 4. Move 3 units along a unit direction.\nconsole.log('along = ' + JSON.stringify(MathLib.scale(MathLib.normalize([0, 0, 1]), 3)));"
    },
    {
      "kind": "example",
      "id": "example:MathLibVectorApi.sub",
      "symbol": "MathLibVectorApi.sub",
      "intent": "scene",
      "code": "// 1. Subtract component-wise.\nconsole.log('diff = ' + JSON.stringify(MathLib.sub([5, 7, 9], [1, 2, 3])));\n// 2. The vector FROM a TO b is sub(b, a).\nconst dir = MathLib.sub([10, 0, 0], [4, 0, 0]);\nconsole.log('a→b = ' + JSON.stringify(dir));\n// 3. Subtracting a vector from itself is the zero vector.\nconsole.log('self = ' + JSON.stringify(MathLib.sub([3, 3, 3], [3, 3, 3])));\n// 4. Distance is the length of the difference.\nconsole.log('dist = ' + MathLib.length(MathLib.sub([3, 4, 0], [0, 0, 0])));"
    },
    {
      "kind": "example",
      "id": "example:Mesh.addDeformer",
      "symbol": "Mesh.addDeformer",
      "intent": "mesh",
      "code": "// 1. Build a cube.\nconst cube = await create.cube({ name: 'addDeformer-demo' });\n// 2. Add a NAMED bend deformer (the flat alias for `deformers.add`).\nconst bend = await cube.addDeformer('nonLinear', { mode: 'bend', name: 'Spine Bend' });\nbend.curvature.set(45);\n// 3. Grab it back by NAME (names over IDs).\nconst same = ls('Spine Bend')[0];\n// 4. Observe the name round-trips.\nconsole.log('grabbed by name: ' + (same.name === 'Spine Bend'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.bake",
      "symbol": "Mesh.bake",
      "intent": "mesh",
      "code": "// 1) Build a cube and add a bend deformer.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1,\n    subdivisionsX: 4, subdivisionsY: 4, subdivisionsZ: 4,\n    name: 'bake-alias-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.deformers.add('nonLinear', { mode: 'bend' });\n// 2) Bake via the top-level alias — the stack drains.\nawait cube.bake();\nconsole.log('stack after bake: ' + cube.deformers.stack().length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.bend",
      "symbol": "Mesh.bend",
      "intent": "mesh",
      "code": "// 1) Build a tall cube to bend.\nconst cube = await create.cube({ width: 1, height: 3, depth: 1, name: 'mesh-bend' });\n// 2) Mesh-wide bend: 45 DEGREES around the Y axis (no `verts` => whole mesh).\n//    `angle` is in DEGREES (range [-360, 360]) — NOT radians.\nawait cube.bend({ angle: 45, axis: 'y' });\n// 3) Targeted bend: pass `verts` to bend only the listed vertices (30 degrees).\nawait cube.bend({ verts: [4, 5, 6, 7], angle: 30, axis: 'y' });\n// 4) Verify and clean up.\nconsole.log('vert count: ' + cube.verts.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.bevel",
      "symbol": "Mesh.bevel",
      "intent": "mesh",
      "code": "// 1) Spawn a cube and chamfer two corners.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'bevel-verts-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Chamfer two corners with width 0.08. Vertex bevel is single-segment,\n//    so it takes no `segments` option (unlike the faces/edges variants).\nawait cube.bevel({ verts: [0, 1], width: 0.08 });\nconsole.log('bevel-verts vertex count: ' + cube.verts.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.bevelAll",
      "symbol": "Mesh.bevelAll",
      "intent": "mesh",
      "code": "// 1. Build a cube (12 hard 90° edges).\nconst cube = await create.cube({ name: 'bevelall-demo' });\n// 2. Round every edge with a small bevel.\nawait cube.bevelAll({ width: 0.05 });\n// 3. Confirm the bevel added geometry (vertex count grew past 8).\nconsole.log('verts after bevelAll: ' + cube.verts.count);\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.bisect",
      "symbol": "Mesh.bisect",
      "intent": "mesh",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'bisect-flat-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Open a bisect session via the flat alias.\nconst session = await cube.bisect({ axis: 'z', offset: 0.25, fill: true });\nawait(session).cancel();\nconsole.log('bisect session opened and cancelled');\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.boolean",
      "symbol": "Mesh.boolean",
      "intent": "mesh",
      "code": "// 1) Spawn two cubes and keep only the intersection.\nconst a = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'bool-int-a',\n});\na.xform({ t: [2.5, 1.3, -0.7] });\nconst b = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'bool-int-b',\n});\nb.xform({ t: [3.0, 1.3, -0.7] });\nawait a.boolean(b, 'intersection');\nconsole.log('boolean-intersection face count: ' + a.faces.count);\nawait a.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.bridge",
      "symbol": "Mesh.bridge",
      "intent": "mesh",
      "code": "// 1) Spawn a subdivided plane (2x2 grid) so a deleted quad leaves a hole\n//    bounded by vertices that are still shared with neighbouring faces —\n//    bridging three of them produces a valid, manifold new triangle. (A\n//    closed cube's corner verts are over-shared, so bridging them would\n//    fail the duplicate / non-manifold precondition.)\nconst plane = await create.plane({\n    width: 2, height: 2, widthSegments: 2, heightSegments: 2, name: 'bridge-verts-demo',\n});\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Capture the corner vertices of face 0 BEFORE opening the hole, then\n//    delete that face so those verts span an open boundary.\nconst corners = plane.faces.vertices(0);\nawait plane.delete({ faces: [0] });\nconst facesBefore = plane.faces.count;\n// 3) Bridge three of those vertices into a new triangle (the vertex\n//    variant requires exactly 3 or 4 vertices).\nawait plane.bridge({ verts: corners.slice(0, 3) });\nconsole.log('faces before bridge: ' + facesBefore);\nconsole.log('faces after bridge: ' + plane.faces.count);\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.build",
      "symbol": "Mesh.build",
      "intent": "mesh",
      "code": "// quad face — build via the runtime API surface. The bare `Mesh` class is\n// NOT a runtime global in the Script Editor; call the canonical alias\n// `create.buildMesh(opts)` instead.\nconst quad = await create.buildMesh({\n    positions: [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]],\n    faceVertexCounts: [4],\n    faceVertexIndices: [0, 1, 2, 3],\n    name: 'quad',\n});\nconsole.log('quad verts/faces: ' + quad.verts.count + '/' + quad.faces.count);\nawait quad.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.buildOnNormal",
      "symbol": "Mesh.buildOnNormal",
      "intent": "mesh",
      "code": "// 1) Build a plane off-origin to project onto.\nconst plane = await create.plane({ width: 4, height: 4, name: 'bon-flat-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Place a cube on the surface headlessly via the flat alias.\nconst result = await plane.buildOnNormal({\n    primitiveType: 'cube',\n    size: 0.5,\n    depth: 0.5,\n    position: [2.5, 1.3, -0.7],\n    normal: [0, 1, 0],\n});\nconsole.log('buildOnNormal returned: ' + (result !== undefined));\n// 3) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.clearSharpEdges",
      "symbol": "Mesh.clearSharpEdges",
      "intent": "mesh",
      "code": "// 1) Spawn a cube off-origin and mark two edges sharp.\nconst cube = await create.cube({ name: 'clear-sharp-edges-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.edges.markSharp({ indices: [0, 3] });\nconsole.log('edge 0 sharp after markSharp: ' + cube.edges.isSharp(0));\n// 2) Clear ALL sharp edges on the mesh in one call.\nawait cube.clearSharpEdges();\nconsole.log('edge 0 sharp after clearSharpEdges: ' + cube.edges.isSharp(0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.collapse",
      "symbol": "Mesh.collapse",
      "intent": "mesh",
      "code": "// 1) Spawn a sphere with plenty of vertices to test vertex collapse.\nconst sphere = await create.sphere({\n    radius: 1, widthSegments: 16, heightSegments: 12,\n    name: 'collapse-verts-demo',\n});\nsphere.xform({ t: [2.5, 1.3, -0.7] });\nconst before = sphere.verts.count;\nawait sphere.collapse({ verts: [10, 11] });\nconsole.log('collapse-verts removed: ' + (before - sphere.verts.count));\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.combine",
      "symbol": "Mesh.combine",
      "intent": "mesh",
      "code": "// 1) Build two cubes to combine.\nconst a = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'combine-a',\n});\na.xform({ t: [2.5, 1.3, -0.7] });\nconst b = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'combine-b',\n});\nb.xform({ t: [4.0, 1.3, -0.7] });\nconst aBefore = a.verts.count;\n// 2) Combine b into a. This consumes BOTH a and b and creates a new\n//    combined mesh node (auto-selected) — the a / b handles are now stale.\nawait a.combine(b);\n// 3) Re-resolve a handle to the freshly-combined (selected) mesh.\nconst combined = ls({ selected: true })[0];\nconsole.log('combine before: ' + aBefore);\nconsole.log('combine after: ' + combined.verts.count);\n// 4) Clean up the combined mesh.\nawait combined.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.connectPath",
      "symbol": "Mesh.connectPath",
      "intent": "mesh",
      "code": "// 1) Build a cube as a vertex playground.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, subdivisionsX: 2, subdivisionsY: 2,\n    subdivisionsZ: 2, name: 'connect-path-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.edges.count;\n// 2) The underlying op re-resolves the vertex selection and needs EXACTLY\n//    two NON-adjacent verts on a shared face. `faces.vertices(0)` returns\n//    the quad's corners in winding order, so corners [0] and [2] are the\n//    diagonal pair. Commit them via the component handle (sets vertex\n//    component-mode + selection), then connect.\nconst corners = cube.faces.vertices(0);\ncube.verts([corners[0], corners[2]]).select();\nawait cube.connectPath({ verts: [corners[0], corners[2]] });\n// 3) Log the before/after edge counts.\nconsole.log('connect-path before: ' + before);\nconsole.log('connect-path after: ' + cube.edges.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.convexHull",
      "symbol": "Mesh.convexHull",
      "intent": "mesh",
      "code": "// 1) Build a torus (clearly non-convex) to hull.\nconst torus = await create.torus({\n    radius: 1, tube: 0.3, radialSegments: 16, tubularSegments: 24,\n    name: 'convex-hull-demo',\n});\ntorus.xform({ t: [2.5, 1.3, -0.7] });\nconst before = torus.faces.count;\n// 2) Compute the convex hull.\nawait torus.convexHull();\n// 3) Log the before/after face counts (the hull is much simpler).\nconsole.log('convex-hull before: ' + before);\nconsole.log('convex-hull after: ' + torus.faces.count);\n// 4) Clean up.\nawait torus.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.decimate",
      "symbol": "Mesh.decimate",
      "intent": "mesh",
      "code": "// 1) Build a high-resolution sphere to decimate.\nconst sphere = await create.sphere({\n    radius: 1, widthSegments: 64, heightSegments: 64, name: 'decimate-demo',\n});\nsphere.xform({ t: [2.5, 1.3, -0.7] });\nconst before = sphere.verts.count;\n// 2) Decimate to exactly 200 vertices.\nawait sphere.decimate({ targetCount: 200 });\n// 3) Log the before/after vertex counts.\nconsole.log('decimate before: ' + before);\nconsole.log('decimate after: ' + sphere.verts.count);\n// 4) Clean up.\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.deformers",
      "symbol": "Mesh.deformers",
      "intent": "mesh",
      "code": "// 1. Build a cube.\nconst cube = await create.cube({ width: 1, height: 2, depth: 1, name: 'deformers-ns' });\n// 2. No selection needed — operate on the whole-mesh deformer stack.\n// 3. Add a twist deformer through the namespace.\nawait cube.deformers.add('nonLinear', { mode: 'twist' });\n// 4. Observe the stack now holds one deformer.\nconsole.log('deformer count: ' + cube.deformers.stack().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.delete",
      "symbol": "Mesh.delete",
      "intent": "mesh",
      "code": "// 1) Spawn a subdivided cube and delete an edge.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1,\n    subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n    name: 'delete-edge-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// mesh.deleteEdges re-resolves the live edge selection, so commit one\n// first via the component handle (sets edge component-mode + selection).\ncube.edges([3]).select();\nawait cube.delete({ edges: [3] });\nconsole.log('edge remaining face count: ' + cube.faces.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.deleteLoose",
      "symbol": "Mesh.deleteLoose",
      "intent": "mesh",
      "code": "// 1) Build a simple cube.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'delete-loose-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.verts.count;\n// 2) Sweep out any stray geometry.\nawait cube.deleteLoose();\n// 3) Log the vertex count to confirm no surprises.\nconsole.log('delete-loose before: ' + before);\nconsole.log('delete-loose after: ' + cube.verts.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.dissolve",
      "symbol": "Mesh.dissolve",
      "intent": "mesh",
      "code": "// 1) Spawn a cube and run mesh-wide degenerate cleanup.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'dissolve-degenerate-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.dissolve({ mode: 'degenerate' });\nconsole.log('degenerate cleanup face count: ' + cube.faces.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.edges",
      "symbol": "Mesh.edges",
      "intent": "mesh",
      "code": "// 1) Build a cube and translate it off origin.\nconst cube = await create.cube({ name: 'edges-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read edge 0's endpoint indices and length.\nconst [v0, v1] = cube.edges.vertices(0);\nconsole.log('edge 0 endpoints: ' + v0 + ', ' + v1);\nconsole.log('edge 0 length: ' + cube.edges.length(0));\n// 3) Insert an extra loop on edge 0.\nawait cube.edges.insertLoop({ indices: [0], cuts: 1 });\nconsole.log('vertex count after loop insert: ' + cube.verts.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.edgeSlide",
      "symbol": "Mesh.edgeSlide",
      "intent": "mesh",
      "code": "// Edge-slide currently requires precomputed slide data from the interactive tool.\n// Use `tool.edgeSlide` to enter the interactive session, or call\n// `cube.slide({ edges: [...], factor })` for a one-shot, non-interactive slide.\n// 1) Spawn a subdivided cube off-origin and slide one interior edge halfway.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1,\n    subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n    name: 'edgeSlide-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.slide({ edges: [0, 1], factor: 0.5 });\nconsole.log('edgeSlide applied; vert[0] =', cube.verts.position(0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.edgeSplit",
      "symbol": "Mesh.edgeSplit",
      "intent": "mesh",
      "code": "// 1) A cube's 12 edges all meet at 90°, so a 30° threshold splits them all.\nconst cube = await create.cube({ name: 'edge-split-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.verts.count;\n// 2) Split — each face gets its own corner vertices (hard edges).\nawait cube.edgeSplit({ angleThresholdDeg: 30 });\nconsole.log('verts ' + before + ' -> ' + cube.verts.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.editModeSeparate",
      "symbol": "Mesh.editModeSeparate",
      "intent": "mesh",
      "code": "// 1) Build a cube and select faces in face component mode. The\n//    `mesh.faces([...]).select()` handle commits the indices to the\n//    selection store AND switches the active component mode to 'face' —\n//    exactly the state editModeSeparate reads.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'edit-mode-separate-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\ncube.faces([0, 1]).select();\nconst before = cube.faces.count;\n// 2) Detach the selected faces into a new mesh.\nawait cube.editModeSeparate();\n// 3) Log the before/after face counts on the source.\nconsole.log('edit-mode separate before: ' + before);\nconsole.log('edit-mode separate after: ' + cube.faces.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.extrude",
      "symbol": "Mesh.extrude",
      "intent": "mesh",
      "code": "// NOTE: the `verts:` arm (per-vertex extrude) is an interactive-only\n// tool; its headless operator is not yet wired (mesh.extrudeVertices is a\n// throw-stub). The runnable equivalent is the `faces:` arm — extrude the\n// face(s) around the vertex you wanted to pull. Use the named-handle form\n// below; the `edges:` arm (above) is also wired.\n// 1) Spawn a cube and extrude the face around a corner outward by 0.2.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'extrude-verts-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.extrude({ faces: [0], distance: 0.2 });\nconsole.log('extruded vertex count: ' + cube.verts.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.faceCount",
      "symbol": "Mesh.faceCount",
      "intent": "mesh",
      "code": "// 1. Build a cube (a default cube has 6 quad faces).\nconst cube = await create.cube({ name: 'facecount-demo' });\n// 2. No selection needed - faceCount() is a whole-mesh query.\n// 3. Read the count.\nconst n = cube.faceCount();\nconsole.log('face count: ' + n);\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.faces",
      "symbol": "Mesh.faces",
      "intent": "mesh",
      "code": "// 1) Build a cube and translate it off origin.\nconst cube = await create.cube({ name: 'faces-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the centroid and area of face 0.\nconst c = cube.faces.centroid(0);\nconsole.log('face 0 centroid x: ' + c.x);\nconsole.log('face 0 area: ' + cube.faces.area(0));\n// 3) Mutate topology. NOTE: face-mode bevel is not yet wired headlessly\n//    (mesh.bevelFaces is a throw-stub); the runnable equivalent is EDGE\n//    bevel, which rounds off the chosen edges and adds new corners.\nawait cube.edges([0]).bevel({ width: 0.1, segments: 1 });\nconsole.log('vertex count after edge bevel: ' + cube.verts.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.fillHole",
      "symbol": "Mesh.fillHole",
      "intent": "mesh",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'fillhole-flat-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Open a fill-hole session via the flat alias.\nconst session = await cube.fillHole({ boundaryFromComponent: { edgeIndices: [] } });\nawait(session).cancel();\nconsole.log('fillHole session opened and cancelled');\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.fillHoles",
      "symbol": "Mesh.fillHoles",
      "intent": "mesh",
      "code": "// NOTE: the `faces:` arm (fill holes bounded by a known face set) is not\n// yet wired headlessly (mesh.fillFaces is a throw-stub). The runnable\n// equivalent is the auto-detect basic fill shown here — delete a face to\n// open a hole, then let `fillHoles` find and close the boundary loop.\n// 1) Open a hole, then auto-detect + basic-fill it.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'fill-faces-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.delete({ faces: [0] });\nawait cube.fillHoles({ mode: 'basic' });\nconsole.log('fill-faces face count: ' + cube.faces.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.flatten",
      "symbol": "Mesh.flatten",
      "intent": "mesh",
      "code": "// 1) Spawn a sphere and flatten the entire mesh onto the X plane.\nconst sphere = await create.sphere({\n    radius: 1, widthSegments: 12, heightSegments: 8,\n    name: 'flatten-all-demo',\n});\nsphere.xform({ t: [2.5, 1.3, -0.7] });\nawait sphere.flatten({ plane: 'x' });\nconsole.log('flatten-all applied: true');\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.flattenToPlane",
      "symbol": "Mesh.flattenToPlane",
      "intent": "mesh",
      "code": "// Flatten a sphere onto the world XY ground plane (collapse Z).\nconst s = await create.sphere({ name: 'flat' });\nawait s.flattenToPlane({ planeOrigin: [0, 0, 0], planeNormal: [0, 0, 1] });\nawait s.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.geometry",
      "symbol": "Mesh.geometry",
      "intent": "mesh",
      "code": "// 1) Build a cube and translate off origin.\nconst cube = await create.cube({ name: 'geom-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Query bounding box, closest point, and a quick raycast.\nconst box = cube.geometry.bbox({ space: 'world' });\nconst hit = cube.geometry.closestPoint([3, 1, -1], { space: 'world' });\nconst ray = cube.geometry.raycast([10, 1.3, -0.7], [-1, 0, 0]);\nconsole.log('bbox extents min: ' + JSON.stringify(box.min));\nconsole.log('closest point distance: ' + (hit ? hit.distance.toFixed(3) : 'none'));\nconsole.log('raycast hit face: ' + (ray ? ray.face.index : 'none'));\n// 3) Inspect surface health.\nconsole.log('manifold: ' + cube.geometry.isManifold);\nconsole.log('closed: ' + cube.geometry.isClosed);\nconsole.log('area: ' + cube.geometry.area.toFixed(4));\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.getMaterial",
      "symbol": "Mesh.getMaterial",
      "intent": "mesh",
      "code": "// 1) Build a cube + a red PBR material and assign it mesh-side.\nconst cube = await create.cube({ name: 'getmat-demo' });\nconst red = await create.material({\n    name: 'demo-red', baseColor: [0.8, 0.1, 0.1, 1], metallic: 0.1, roughness: 0.5,\n});\nawait cube.setMaterial(red);\n// 2) Read it back by name-resolved handle (no ids anywhere).\nconst current = cube.getMaterial();\nconsole.log('assigned material: ' + (current ? current.name : 'none'));\n// 3) Clean up the demo mesh.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.id",
      "symbol": "Mesh.id",
      "intent": "mesh",
      "code": "// 1) Build a cube and capture its id.\nconst cube = await create.cube({ name: 'id-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst myId = cube.id;\nconsole.log('cube id is non-empty: ' + (myId.length > 0));\n// 2) Round-trip the id through the resolver and confirm it matches.\nconst again = ls({ id: myId })[0];\nconsole.log('id round-trip resolves back to same mesh: ' + (again === cube));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.insertEdgeLoop",
      "symbol": "Mesh.insertEdgeLoop",
      "intent": "mesh",
      "code": "// 1) Build a simple cube as the host mesh.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'insert-edge-loop-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.edges.count;\n// 2) Insert two parallel loops across edge 3.\nawait cube.insertEdgeLoop({ edges: [3], cuts: 2 });\n// 3) Log the before/after edge counts.\nconsole.log('insert-edge-loop before: ' + before);\nconsole.log('insert-edge-loop after: ' + cube.edges.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.inset",
      "symbol": "Mesh.inset",
      "intent": "mesh",
      "code": "// 1) Spawn a cube and inset one face with a small depth extrusion.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'inset-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.inset({ faces: [0], amount: 0.05, depth: 0.02 });\nconsole.log('inset face count: ' + cube.faces.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.knife",
      "symbol": "Mesh.knife",
      "intent": "mesh",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'knife-flat-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Open and cancel a knife session via the flat alias.\nconst session = await cube.knife({ snapMode: 'edge' });\nawait(session).cancel();\nconsole.log('knife session opened and cancelled');\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.live",
      "symbol": "Mesh.live",
      "intent": "mesh",
      "code": "// 1) Build a cube and translate off origin.\nconst cube = await create.cube({ name: 'live-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Make the cube the live snap target.\nawait cube.live.make();\nconsole.log('cube is now the live snap target');\n// 3) Clear the live flag, then isolate it in the viewport.\nawait cube.live.clear();\nawait cube.live.isolate();\nconsole.log('cube is now isolated in the viewport');\n// 4) Clean up. clear() released the live-mesh; exit isolate mode too so\n//    the editor returns to its default viewport state.\nawait viewport.deisolate();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.loopCut",
      "symbol": "Mesh.loopCut",
      "intent": "mesh",
      "code": "// NOTE: loopCut is an interactive (hover-driven) tool; its headless\n// operator is not yet wired (mesh.loopCutEdge is a throw-stub). The\n// runnable equivalent is `mesh.insertEdgeLoop({ edges, cuts })`, which\n// slices fresh edge loops perpendicular to the seed edge — what a\n// midpoint loopCut produces.\n// 1) Build a simple cube as the host mesh.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'loop-cut-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.edges.count;\n// 2) Slice a fresh loop through edge 4.\nawait cube.insertEdgeLoop({ edges: [4], cuts: 1 });\n// 3) Log the before/after edge counts.\nconsole.log('loop-cut before: ' + before);\nconsole.log('loop-cut after: ' + cube.edges.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.material",
      "symbol": "Mesh.material",
      "intent": "mesh",
      "code": "// 1) Build a cube and translate off origin.\nconst cube = await create.cube({ name: 'mat-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Create a red rough material and assign it.\nconst red = await create.material({\n    name: 'red-rough',\n    baseColor: [1, 0.1, 0.1, 1],\n    metallic: 0,\n    roughness: 0.8,\n});\nawait cube.material.set(red);\n// 3) Read it back and confirm.\nconst current = cube.material.get();\nconsole.log('assigned material name: ' + (current ? current.name : 'none'));\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.merge",
      "symbol": "Mesh.merge",
      "intent": "mesh",
      "code": "// 1) Spawn a subdivided cube and merge two edges (each edge's endpoints\n//    collapse together — no threshold for the selection-based variant).\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1,\n    subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n    name: 'merge-edges-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.verts.count;\nawait cube.merge({ edges: [0, 1] });\nconsole.log('merge-edges removed: ' + (before - cube.verts.count));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.mirror",
      "symbol": "Mesh.mirror",
      "intent": "mesh",
      "code": "// 1) Build half of a torso shape to mirror.\nconst half = await create.cube({\n    width: 1, height: 2, depth: 1, name: 'mirror-demo',\n});\nhalf.xform({ t: [2.5, 1.3, -0.7] });\nconst before = half.verts.count;\n// 2) Mirror across the X axis and weld the seam.\nawait half.mirror({ axis: 'x', weld: true });\n// 3) Log the before/after vertex counts to confirm the mirror.\nconsole.log('mirror before: ' + before);\nconsole.log('mirror after: ' + half.verts.count);\n// 4) Clean up.\nawait half.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.morphTargets",
      "symbol": "Mesh.morphTargets",
      "intent": "mesh",
      "code": "// 1) Inspect a head's blendshapes, then dial one in.\nconst head = ls('Head')[0];\nconsole.log(head.morphTargets.list());\nhead.morphTargets.setWeight('smile', 0.8);"
    },
    {
      "kind": "example",
      "id": "example:Mesh.normals",
      "symbol": "Mesh.normals",
      "intent": "mesh",
      "code": "// 1) Build a sphere and translate off origin.\nconst sphere = await create.sphere({\n    radius: 1,\n    widthSegments: 24,\n    heightSegments: 16,\n    name: 'normals-demo',\n});\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the existing per-vertex normals.\nconst before = sphere.normals.get();\nconsole.log('normal float count: ' + before.length);\n// 3) Reverse every face normal, then auto-smooth the result.\nawait sphere.normals.reverse();\nawait sphere.normals.smooth();\nconsole.log('first normal after smooth: ' + JSON.stringify(sphere.normals.getVertex(0).toArray()));\n// 4) Clean up.\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.offsetEdgeLoops",
      "symbol": "Mesh.offsetEdgeLoops",
      "intent": "mesh",
      "code": "// NOTE: offsetEdgeLoops is an interactive (drag-driven) viewport tool; its\n// headless operator throws OperationNotSupportedError when scripted. The\n// runnable equivalent for adding support loops is\n// `mesh.insertEdgeLoop({ edges, cuts })`, which slices fresh loops across\n// the seed edges (use `mesh.edges([...]).bevel({ width, segments })` when\n// you specifically need the two parallel offset edges).\n// 1) Build a simple cube as the host mesh.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'offset-edge-loops-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.edges.count;\n// 2) Add a support loop across edge 4 (headless-runnable equivalent;\n//    insertEdgeLoop takes a single edge per call — loop again per edge).\nawait cube.insertEdgeLoop({ edges: [4], cuts: 1 });\n// 3) Log the before/after edge counts.\nconsole.log('offset-edge-loops before: ' + before);\nconsole.log('offset-edge-loops after: ' + cube.edges.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.poke",
      "symbol": "Mesh.poke",
      "intent": "mesh",
      "code": "// 1) Spawn a cube and poke two faces (each fans into 4 triangles).\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'poke-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.faces.count;\nawait cube.poke({ faces: [0, 1] });\nconsole.log('poke faces before: ' + before);\nconsole.log('poke faces after: ' + cube.faces.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.polyDraw",
      "symbol": "Mesh.polyDraw",
      "intent": "mesh",
      "code": "// 1) Build a plane off-origin.\nconst plane = await create.plane({ width: 2, height: 2, name: 'polydraw-flat-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Open and cancel a poly-draw session via the flat alias.\nconst session = await plane.polyDraw();\nawait(session).cancel();\nconsole.log('polyDraw session opened and cancelled');\n// 3) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.polyPen",
      "symbol": "Mesh.polyPen",
      "intent": "mesh",
      "code": "// 1) Build a plane off-origin.\nconst plane = await create.plane({ width: 2, height: 2, name: 'polypen-flat-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Open a poly-pen session via the flat alias.\nconst session = await plane.polyPen({ mode: 'quad', symmetry: null, worldCoords: true });\nawait(session).cancel();\nconsole.log('polyPen session opened and cancelled');\n// 3) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.pushPull",
      "symbol": "Mesh.pushPull",
      "intent": "mesh",
      "code": "// 1) Build a sphere as the push-pull target.\nconst sphere = await create.sphere({\n    radius: 0.5, widthSegments: 16, heightSegments: 12, name: 'pushpull-demo',\n});\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Push two vertices outward by 5cm along their normals.\nawait sphere.pushPull({ verts: [0, 1], distance: 0.05 });\n// 3) Log a verifiable property.\nconsole.log('pushpull vertex count: ' + sphere.verts.count);\n// 4) Clean up.\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.remesh",
      "symbol": "Mesh.remesh",
      "intent": "mesh",
      "code": "// 1) Build a densely tessellated sphere to remesh.\nconst sphere = await create.sphere({\n    radius: 1, widthSegments: 64, heightSegments: 64, name: 'remesh-demo',\n});\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Quad-dominant retopology for clean animation-ready loops.\nawait sphere.remesh({ algorithm: 'quad' });\nconsole.log('quad-remesh face count: ' + sphere.faces.count);\n// 3) Analytical remesh to a precise vertex target.\nawait sphere.remesh({ algorithm: 'advanced', targetCount: 500 });\nconsole.log('advanced-remesh vertex count: ' + sphere.verts.count);\n// 4) Pick the AutoRemesher kernel explicitly (background + cancellable).\nawait sphere.remesh({ algorithm: 'advanced', method: 'autoremesher', targetCount: 5000 });\nconsole.log('autoremesher face count: ' + sphere.faces.count);\n// 5) Default ('advanced') analytical remesh.\nawait sphere.remesh();\nconsole.log('default-remesh vertex count: ' + sphere.verts.count);\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.rip",
      "symbol": "Mesh.rip",
      "intent": "mesh",
      "code": "// 1) Spawn a cube and rip a corner vertex into separate copies.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'rip-verts-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.rip({ verts: [0] });\nconsole.log('rip-verts vertex count: ' + cube.verts.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.rotateEdge",
      "symbol": "Mesh.rotateEdge",
      "intent": "mesh",
      "code": "// 1) Build a cube and triangulate so every edge has a flippable diagonal.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'rotate-edge-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.triangulate();\nconst beforeFaces = cube.faces.count;\n// 2) Flip edge 3 to its other diagonal (no direction control).\nawait cube.rotateEdge({ edges: [3] });\n// 3) Log a verifiable property (face count is unchanged by a flip).\nconsole.log('rotate-edge before faces: ' + beforeFaces);\nconsole.log('rotate-edge after faces: ' + cube.faces.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.separate",
      "symbol": "Mesh.separate",
      "intent": "mesh",
      "code": "// 1) Build two separate cubes and combine them into one mesh with\n//    two disconnected shells.\nconst a = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'separate-a',\n});\na.xform({ t: [2.5, 1.3, -0.7] });\nconst b = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'separate-b',\n});\nb.xform({ t: [4.0, 1.3, -0.7] });\n// 2) Combine consumes BOTH a and b and creates a new combined node\n//    (auto-selected) — the a / b handles are now stale. Re-resolve a\n//    fresh handle to the combined mesh before separating.\nawait a.combine(b);\nconst combined = ls({ selected: true })[0];\nconst combinedVerts = combined.verts.count;\n// 3) Separate back into independent shells. This consumes the combined\n//    node and creates one new (auto-selected) node per shell, so the\n//    `combined` handle is now stale — re-resolve the selected shells.\nawait combined.separate();\nconst shells = ls({ selected: true });\n// 4) Log vertex counts before / after.\nconsole.log('separate combined verts: ' + combinedVerts);\nconsole.log('separate shell count: ' + shells.length);\n// 5) Clean up every shell.\nfor (const shell of shells)\n    await shell.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.setMaterial",
      "symbol": "Mesh.setMaterial",
      "intent": "mesh",
      "code": "// 1) Build a cube + a blue PBR material.\nconst cube = await create.cube({ name: 'setmat-demo' });\nconst blue = await create.material({\n    name: 'demo-blue', baseColor: [0.1, 0.2, 0.8, 1], metallic: 0.2, roughness: 0.4,\n});\n// 2) Assign it mesh-side + confirm the round-trip by name.\nawait cube.setMaterial(blue);\nconsole.log('material assigned: ' + (cube.getMaterial()?.name ?? 'none'));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.shear",
      "symbol": "Mesh.shear",
      "intent": "mesh",
      "code": "// 1) Build a cube and shear two of its vertices.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'shear-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Shear two vertices along the X axis by 0.3 units.\nawait cube.shear({ verts: [0, 1], amount: 0.3, axis: 'x' });\n// 3) Log a verifiable property.\nconsole.log('shear vertex count: ' + cube.verts.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.shrinkFatten",
      "symbol": "Mesh.shrinkFatten",
      "intent": "mesh",
      "code": "// 1) Build a sphere as the shrink target.\nconst sphere = await create.sphere({\n    radius: 0.5, widthSegments: 16, heightSegments: 12,\n    name: 'shrink-fatten-demo',\n});\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Shrink two vertices inward by 2cm.\nawait sphere.shrinkFatten({ verts: [0, 1], offset: -0.02 });\n// 3) Log a verifiable property.\nconsole.log('shrink-fatten vertex count: ' + sphere.verts.count);\n// 4) Clean up.\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.slice",
      "symbol": "Mesh.slice",
      "intent": "mesh",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'slice-flat-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Open a plane-mode slice session via the flat alias.\nconst session = await cube.slice({ mode: 'plane', axis: 'y', offset: 0.5 });\nawait(session).cancel();\nconsole.log('slice session opened and cancelled');\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.slide",
      "symbol": "Mesh.slide",
      "intent": "mesh",
      "code": "// 1) Spawn a subdivided cube and slide an interior vertex.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1,\n    subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n    name: 'slide-verts-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.slide({ verts: [5], factor: -0.3 });\nconsole.log('slide-verts applied: true');\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.smooth",
      "symbol": "Mesh.smooth",
      "intent": "mesh",
      "code": "// 1) Spawn a sphere and smooth the entire mesh once.\nconst sphere = await create.sphere({\n    radius: 1, widthSegments: 12, heightSegments: 8,\n    name: 'smooth-all-demo',\n});\nsphere.xform({ t: [2.5, 1.3, -0.7] });\nawait sphere.smooth({ iterations: 1, strength: 0.5 });\nconsole.log('smooth-all applied: true');\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.solidify",
      "symbol": "Mesh.solidify",
      "intent": "mesh",
      "code": "// 1) Build a flat plane to solidify.\nconst plane = await create.plane({\n    width: 1, height: 1, name: 'solidify-demo',\n});\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Give it a 5cm slab thickness.\nawait plane.solidify({ thickness: 0.05 });\n// 3) Log the new vertex count (a flat plane gains a back face).\nconsole.log('solidify vertex count: ' + plane.verts.count);\n// 4) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.spin",
      "symbol": "Mesh.spin",
      "intent": "mesh",
      "code": "// 1) Build a thin profile plane to revolve.\nconst profile = await create.plane({\n    width: 0.1, height: 1, name: 'spin-demo',\n});\nprofile.xform({ t: [2.5, 1.3, -0.7] });\nconst before = profile.verts.count;\n// 2) Sweep the profile a full turn around Y in 16 steps.\nawait profile.spin({ angle: 360, axis: 'y', steps: 16 });\n// 3) Log the before/after vertex counts.\nconsole.log('spin before: ' + before);\nconsole.log('spin after: ' + profile.verts.count);\n// 4) Clean up.\nawait profile.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.subdivide",
      "symbol": "Mesh.subdivide",
      "intent": "mesh",
      "code": "// 1) Spawn a cube and subdivide the whole mesh.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'subdivide-all-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.subdivide({ iterations: 1 });\nconsole.log('subdivide-all face count: ' + cube.faces.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.tools",
      "symbol": "Mesh.tools",
      "intent": "mesh",
      "code": "// 1) Build a cube and translate off origin.\nconst cube = await create.cube({ name: 'tools-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Slice the cube in half along the X axis (one-shot, no UI).\nconst result = await cube.tools.slice({\n    mode: 'plane',\n    axis: 'x',\n    offset: 0,\n    fill: true,\n    oneShot: true,\n});\nconsole.log('slice ok: ' + ('ok' in result ? result.ok : 'session'));\n// 3) Verify the cube was cut (face count grew).\nconsole.log('post-slice face count: ' + cube.faces.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.toSphere",
      "symbol": "Mesh.toSphere",
      "intent": "mesh",
      "code": "// 1) Build a cube and push half-way toward a sphere.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, subdivisionsX: 4, subdivisionsY: 4,\n    subdivisionsZ: 4, name: 'to-sphere-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Halfway-round the corner vertices toward the sphere.\nawait cube.toSphere({ verts: [0, 1, 2, 3], factor: 0.5 });\n// 3) Log a verifiable property.\nconsole.log('to-sphere vertex count: ' + cube.verts.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.transferFrom",
      "symbol": "Mesh.transferFrom",
      "intent": "mesh",
      "code": "// 1) Build a source cube and a destination cube. Both primitives ship\n//    with a UV layout, so the UV-space match has something to work with.\nconst src = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'transfer-source',\n});\nsrc.xform({ t: [2.5, 1.3, -0.7] });\nconst dst = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'transfer-dest',\n});\ndst.xform({ t: [-2.5, 1.3, -0.7] });\n// 2) Give the source a fresh unwrap, then copy its UV layout onto dst.\nawait src.uv.unwrap({ method: 'angle-based' });\nawait dst.transferFrom(src, { attrs: ['uv'] });\n// 3) Log a verifiable property — dst now carries UV islands.\nconsole.log('transfer dest UV islands: ' + dst.uv.islands.length);\n// 4) Clean up both meshes.\nawait src.delete();\nawait dst.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.triangleCount",
      "symbol": "Mesh.triangleCount",
      "intent": "mesh",
      "code": "// 1. Build a cube (6 quads tessellate to 12 triangles).\nconst cube = await create.cube({ name: 'tricount-demo' });\n// 2. No selection needed - triangleCount() is a whole-mesh query.\n// 3. Read the GPU-triangle count.\nconst n = cube.triangleCount();\nconsole.log('triangle count: ' + n);\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.triangulate",
      "symbol": "Mesh.triangulate",
      "intent": "mesh",
      "code": "// 1) Spawn a cube and clean up only non-planar faces.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'triangulate-nonplanar-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// The nonPlanarOnly path runs mesh.splitNonPlanarFaces, which re-resolves\n// the live face selection — commit one first via the component handle\n// (sets face component-mode + selection). A clean cube is planar, so this\n// is a no-op repair, but it demonstrates the call without throwing.\ncube.faces([0, 1]).select();\nawait cube.triangulate({ nonPlanarOnly: true });\nconsole.log('triangulate-nonplanar face count: ' + cube.faces.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.trisToQuads",
      "symbol": "Mesh.trisToQuads",
      "intent": "mesh",
      "code": "// 1) Build a triangulated cube to convert back to quads.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'tris-to-quads-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.triangulate();\nconst triFaces = cube.faces.count;\n// 2) Pair triangles back into quads, allowing up to ~17 degrees.\nawait cube.trisToQuads({\n    faces: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],\n    angle: 0.3,\n});\n// 3) Log the before/after face counts.\nconsole.log('tris-to-quads before: ' + triFaces);\nconsole.log('tris-to-quads after: ' + cube.faces.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.uv",
      "symbol": "Mesh.uv",
      "intent": "mesh",
      "code": "// 1) Build a cube and translate off origin.\nconst cube = await create.cube({ name: 'uv-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Inspect the default UV set.\nconsole.log('uv set count: ' + cube.uv.numSets);\nconsole.log('loop UV count: ' + cube.uv.count);\nconsole.log('current set: ' + cube.uv.currentSet);\n// 3) Unwrap, pack, and read the resulting island layout.\nawait cube.uv.unwrap({ method: 'angle-based' });\nawait cube.uv.pack();\nconsole.log('island count: ' + cube.uv.islands.length);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.validate",
      "symbol": "Mesh.validate",
      "intent": "mesh",
      "code": "const cube = await create.cube({ name: 'validate-demo' });\nconst report = cube.validate();\nconsole.log('manifold + closed: ' + report.isManifoldClosed);\nconsole.log('issues: ' + report.totalIssues + ' — ' + report.summary);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.validateAndRepair",
      "symbol": "Mesh.validateAndRepair",
      "intent": "mesh",
      "code": "// 1) Build a mesh and validate/repair it BY NAME (no ids anywhere).\nconst cube = await create.cube({ name: 'repair-me' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Auto-repair any fixable issues.\nawait cube.validateAndRepair();\nconsole.log('manifold: ' + cube.isManifold + ' closed: ' + cube.isClosed);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.vertexCount",
      "symbol": "Mesh.vertexCount",
      "intent": "mesh",
      "code": "// 1. Build a cube (a default cube has 8 vertices).\nconst cube = await create.cube({ name: 'vertcount-demo' });\n// 2. No selection needed - vertexCount() is a whole-mesh query.\n// 3. Read the count.\nconst n = cube.vertexCount();\nconsole.log('vertex count: ' + n);\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.vertexSlide",
      "symbol": "Mesh.vertexSlide",
      "intent": "mesh",
      "code": "// Vertex-slide currently requires precomputed slide data from the interactive tool.\n// Use `tool.vertexSlide` to enter the interactive session, or call\n// `cube.slide({ verts: [...], factor })` for a one-shot, non-interactive slide.\n// 1) Spawn a subdivided cube off-origin and slide an interior vertex.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1,\n    subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n    name: 'vertexSlide-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.slide({ verts: [0, 2], factor: 0.5 });\nconsole.log('vertexSlide applied; vert[0] =', cube.verts.position(0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.verts",
      "symbol": "Mesh.verts",
      "intent": "mesh",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ name: 'verts-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read a vertex position.\nconst p = cube.verts.position(0);\nconsole.log('vertex 0 x: ' + p.x);\n// 3) Smooth a subset of vertices in place.\nawait cube.verts.smooth({ indices: [0, 1, 2, 3], iterations: 1, strength: 0.5 });\nconsole.log('vertex count after smooth: ' + cube.verts.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Mesh.wireframe",
      "symbol": "Mesh.wireframe",
      "intent": "mesh",
      "code": "// 1) Build a low-poly cube to wireframe.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'wireframe-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Replace the cube with a thick tube cage (always replaces in place).\nawait cube.wireframe({ thickness: 0.05 });\n// 3) Log a verifiable property.\nconsole.log('wireframe vertex count: ' + cube.verts.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshDeformersApi.add",
      "symbol": "MeshDeformersApi.add",
      "intent": "scene",
      "code": "// 1) Build a subdivided cube.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1,\n    subdivisionsX: 4, subdivisionsY: 4, subdivisionsZ: 4,\n    name: 'add-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Add a Smooth (no opts), an FFD (basis), and a Bend (mode).\nconst smooth = await cube.deformers.add('smooth');\nconst lattice = await cube.deformers.add('ffd', { basis: 'bspline' });\nconst bend = await cube.deformers.add('nonLinear', { mode: 'bend' });\nconsole.log('types: ' + [smooth.deformerType, lattice.deformerType, bend.deformerType].join(','));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshDeformersApi.bake",
      "symbol": "MeshDeformersApi.bake",
      "intent": "scene",
      "code": "// 1) Build a subdivided cube and add a deformer.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1,\n    subdivisionsX: 4, subdivisionsY: 4, subdivisionsZ: 4,\n    name: 'bake-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.deformers.add('nonLinear', { mode: 'bend' });\nconsole.log('stack before bake: ' + cube.deformers.stack().length);\n// 2) Bake the stack into geometry — the stack drains to empty.\nawait cube.deformers.bake();\nconsole.log('stack after bake: ' + cube.deformers.stack().length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshDeformersApi.basePositions",
      "symbol": "MeshDeformersApi.basePositions",
      "intent": "scene",
      "code": "// 1. Build a cube and add a twist deformer.\nconst cube = await create.cube({ width: 1, height: 2, depth: 1, name: 'def-base' });\nawait cube.deformers.add('nonLinear', { mode: 'twist' });\n// 2. No selection needed — these are whole-mesh bulk reads.\n// 3. Read the deformed buffer and the always-undeformed base buffer.\nconst deformed = cube.deformers.evaluatedPositions();\nconst base = cube.deformers.basePositions();\n// 4. Observe both share a length, so a per-vertex delta is computable.\nconsole.log('lengths match: ' + (deformed.length === base.length));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshDeformersApi.evaluatedPositions",
      "symbol": "MeshDeformersApi.evaluatedPositions",
      "intent": "scene",
      "code": "// 1. Build a subdivided cube and add a twist deformer.\n//    More subdivisions ⇒ the twist bends more vertices, so the\n//    deformed buffer differs more visibly from the base buffer.\nconst cube = await create.cube({\n    width: 1, height: 2, depth: 1,\n    subdivisionsX: 2, subdivisionsY: 4, subdivisionsZ: 2,\n    name: 'eval-demo',\n});\nconst twist = await cube.deformers.add('nonLinear', { mode: 'twist' });\n// 2. Crank the twist so the deformed state visibly differs from the base.\ntwist.endAngle.set(120); // degrees of twist at the top of the mesh\ntwist.envelope.set(1.0); // full deformer strength (0..1)\n// 3. Read the LIVE deformed positions — no bake needed; this is exactly\n//    what the viewport renders with the deformer stack applied.\nconst deformed = cube.deformers.evaluatedPositions();\n// 4. Observe the deformed vertex count (flat xyz buffer ⇒ length / 3).\nconsole.log('deformed verts: ' + (deformed.length / 3));\nawait cube.delete(); // clean up the demo mesh"
    },
    {
      "kind": "example",
      "id": "example:MeshDeformersApi.example",
      "symbol": "MeshDeformersApi.example",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'def-example' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('deformers example length: ' + cube.deformers.example().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshDeformersApi.help",
      "symbol": "MeshDeformersApi.help",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'def-help' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('deformers help length: ' + cube.deformers.help().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshDeformersApi.remove",
      "symbol": "MeshDeformersApi.remove",
      "intent": "scene",
      "code": "// 1) Build a cube and add a Smooth.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'remove-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst smooth = await cube.deformers.add('smooth');\nconsole.log('before: ' + cube.deformers.stack().length);\n// 2) Remove and verify.\ncube.deformers.remove(smooth);\nconsole.log('after: ' + cube.deformers.stack().length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshDeformersApi.reorder",
      "symbol": "MeshDeformersApi.reorder",
      "intent": "scene",
      "code": "// 1) Build a cube and add three deformers.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1,\n    subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3,\n    name: 'reorder-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.deformers.add('smooth');\nawait cube.deformers.add('normalPush');\nconst bend = await cube.deformers.add('nonLinear', { mode: 'bend' });\n// 2) Move the bend to the top of the stack.\ncube.deformers.reorder(bend, 0);\nconst stack = cube.deformers.stack();\nconsole.log('first: ' + stack[0].deformerType);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshDeformersApi.stack",
      "symbol": "MeshDeformersApi.stack",
      "intent": "scene",
      "code": "// 1) Build a cube and stack two deformers on it.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1,\n    subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3,\n    name: 'stack-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.deformers.add('smooth');\nawait cube.deformers.add('normalPush');\n// 2) Walk the stack.\nconst stack = cube.deformers.stack();\nconsole.log('count: ' + stack.length);\nfor (const d of stack)\n    console.log('  ' + d.deformerType + ': ' + d.displayName);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi",
      "symbol": "MeshEdgesApi",
      "intent": "scene",
      "code": "// 1) Build a cube + translate so the result is visible.\nconst cube = await create.cube({ name: 'edges-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read collection-level stats.\nconsole.log('edge count: ' + cube.edges.count);\nconsole.log('boundary edges: ' + cube.edges.boundary.length);\nconsole.log('total length (m): ' + cube.edges.totalLength);\n// 3) Read per-edge data.\nconsole.log('edge 0 length: ' + cube.edges.length(0));\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.bevel",
      "symbol": "MeshEdgesApi.bevel",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'edges-bevel' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('vertex count before: ' + cube.verts.count);\nconsole.log('face count before: ' + cube.faces.count);\n// 2) Bevel the first edge with one segment and a modest width.\nawait cube.edges.bevel({ indices: [0], width: 0.1, segments: 1 });\nconsole.log('vertex count after: ' + cube.verts.count);\nconsole.log('face count after: ' + cube.faces.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.boundary",
      "symbol": "MeshEdgesApi.boundary",
      "intent": "scene",
      "code": "// 1) Spawn a plane + translate.\nconst plane = await create.plane({ name: 'edges-boundary' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Every edge of an open plane is a boundary.\nconsole.log('boundary count: ' + plane.edges.boundary.length);\n// 3) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.bridge",
      "symbol": "MeshEdgesApi.bridge",
      "intent": "scene",
      "code": "// 1) Spawn a cube and open two holes so genuine boundary edges exist\n//    (bridging two edges of a single closed face would just re-create\n//    that face — a duplicate — and fail the manifold precondition).\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'edges-bridge',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.delete({ faces: [0, 2] });\nconst facesBefore = cube.faces.count;\n// 2) Bridge two of the now-open boundary edges with a new face.\nconst boundary = cube.edges.boundary;\nif (boundary.length >= 3) {\n    await cube.edges.bridge({ indices: [boundary[0], boundary[2]] });\n}\nconsole.log('faces added by bridge: ' + (cube.faces.count - facesBefore));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.clearSeam",
      "symbol": "MeshEdgesApi.clearSeam",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-clearseam' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Mark, then clear, the first edge.\nawait cube.edges.markSeam({ indices: [0] });\nawait cube.edges.clearSeam({ indices: [0] });\nconsole.log('edge 0 is seam: ' + cube.edges.isSeam(0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.clearSharp",
      "symbol": "MeshEdgesApi.clearSharp",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-clearsharp' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Mark the first edge sharp, then clear ALL sharp edges (whole-mesh).\nawait cube.edges.markSharp({ indices: [0] });\nconsole.log('edge 0 sharp after markSharp: ' + cube.edges.isSharp(0));\nawait cube.edges.clearSharp();\nconsole.log('edge 0 sharp after clearSharp: ' + cube.edges.isSharp(0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.collapse",
      "symbol": "MeshEdgesApi.collapse",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-collapse' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst vertsBefore = cube.verts.count;\n// 2) Collapse resolves its target from the LIVE edge selection, so build\n//    an edge selection via the component handle and commit it with\n//    `.select()` before calling `.collapse()`.\nconst sel = cube.edges([0, 2]);\nsel.select();\nawait sel.collapse();\nconsole.log('verts dropped by collapse: ' + (vertsBefore - cube.verts.count));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.count",
      "symbol": "MeshEdgesApi.count",
      "intent": "scene",
      "code": "// 1) Build a cube and translate it off the origin.\nconst cube = await create.cube({ name: 'edges-count-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Log the edge count.\nconsole.log('edge count: ' + cube.edges.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.delete",
      "symbol": "MeshEdgesApi.delete",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-delete' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst facesBefore = cube.faces.count;\n// 2) Delete one edge: removes the two faces that shared it.\nawait cube.edges.delete({ indices: [0] });\nconsole.log('faces removed: ' + (facesBefore - cube.faces.count));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.description",
      "symbol": "MeshEdgesApi.description",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-description' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the description.\nconst d = cube.edges.description();\nconsole.log('description length: ' + d.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.dissolve",
      "symbol": "MeshEdgesApi.dissolve",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'edges-dissolve' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst facesBefore = cube.faces.count;\nconsole.log('face count before: ' + facesBefore);\n// 2) Dissolve the first edge. The two adjacent faces merge.\nawait cube.edges.dissolve({ indices: [0] });\nconsole.log('face count after: ' + cube.faces.count);\nconsole.log('dropped exactly one face: ' + (facesBefore - cube.faces.count === 1));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.example",
      "symbol": "MeshEdgesApi.example",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-example' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the example snippet.\nconst e = cube.edges.example();\nconsole.log('example length: ' + e.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.extrude",
      "symbol": "MeshEdgesApi.extrude",
      "intent": "scene",
      "code": "// 1) Spawn a plane (open-boundary edges are easiest to extrude).\nconst plane = await create.plane({\n    width: 2, height: 2, widthSegments: 1, heightSegments: 1, name: 'edges-extrude',\n});\nplane.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('vertex count before: ' + plane.verts.count);\nconsole.log('face count before: ' + plane.faces.count);\n// 2) Extrude the first edge by a small amount.\nawait plane.edges.extrude({ indices: [0], distance: 0.5 });\nconsole.log('vertex count after: ' + plane.verts.count);\nconsole.log('face count after: ' + plane.faces.count);\n// 3) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.faces",
      "symbol": "MeshEdgesApi.faces",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate. Every edge of a closed cube is interior.\nconst cube = await create.cube({ name: 'edges-faces' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read incident faces of edge 0.\nconst faces = cube.edges.faces(0);\nconsole.log('incident face count: ' + faces.length);\nconsole.log('is interior: ' + (faces.length === 2));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.forEach",
      "symbol": "MeshEdgesApi.forEach",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-foreach' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Sum every length via forEach.\nlet sum = 0;\ncube.edges.forEach((i) => { sum += cube.edges.length(i); });\nconsole.log('sum equals totalLength: ' + (Math.abs(sum - cube.edges.totalLength) < 1e-6));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.getLengths",
      "symbol": "MeshEdgesApi.getLengths",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-getlengths' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read every length at once.\nconst lens = cube.edges.getLengths();\nconsole.log('array length: ' + lens.length);\nconsole.log('first length: ' + lens[0]);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.getMidpoints",
      "symbol": "MeshEdgesApi.getMidpoints",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-getmidpoints' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read every midpoint.\nconst m = cube.edges.getMidpoints();\nconsole.log('array length: ' + m.length);\nconsole.log('first midpoint x: ' + m[0]);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.getNormals",
      "symbol": "MeshEdgesApi.getNormals",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-getnormals' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read every normal.\nconst n = cube.edges.getNormals();\nconsole.log('array length: ' + n.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.getSeam",
      "symbol": "MeshEdgesApi.getSeam",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate, mark one seam.\nconst cube = await create.cube({ name: 'edges-getseam' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.edges.markSeam({ indices: [0] });\n// 2) Read every seam flag.\nconst s = cube.edges.getSeam();\nconsole.log('seam flag at 0: ' + s[0]);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.getSmooth",
      "symbol": "MeshEdgesApi.getSmooth",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-getsmooth' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read smooth flags.\nconst s = cube.edges.getSmooth();\nconsole.log('array length: ' + s.length);\nconsole.log('first flag: ' + s[0]);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.help",
      "symbol": "MeshEdgesApi.help",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-help' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Print the help text.\nconst h = cube.edges.help();\nconsole.log('help length: ' + h.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.insertLoop",
      "symbol": "MeshEdgesApi.insertLoop",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-insertloop' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst edgesBefore = cube.edges.count;\n// 2) Insert one edge loop using edge 0 as the seed.\nawait cube.edges.insertLoop({ indices: [0], cuts: 1 });\nconsole.log('edges added: ' + (cube.edges.count - edgesBefore));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.isBoundary",
      "symbol": "MeshEdgesApi.isBoundary",
      "intent": "scene",
      "code": "// 1) Spawn a plane + translate. Every edge is on the boundary.\nconst plane = await create.plane({ name: 'edges-isboundary' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Check edge 0.\nconsole.log('edge 0 is boundary: ' + plane.edges.isBoundary(0));\n// 3) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.isSeam",
      "symbol": "MeshEdgesApi.isSeam",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate, then mark edge 0 as seam.\nconst cube = await create.cube({ name: 'edges-isseam' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.edges.markSeam({ indices: [0] });\n// 2) Verify.\nconsole.log('edge 0 is seam: ' + cube.edges.isSeam(0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.isSharp",
      "symbol": "MeshEdgesApi.isSharp",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate, then mark edge 0 sharp.\nconst cube = await create.cube({ name: 'edges-issharp' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.edges.markSharp({ indices: [0] });\n// 2) Verify.\nconsole.log('edge 0 is sharp: ' + cube.edges.isSharp(0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.isSmooth",
      "symbol": "MeshEdgesApi.isSmooth",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-issmooth' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Fresh edges default to smooth.\nconsole.log('edge 0 is smooth: ' + cube.edges.isSmooth(0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.length",
      "symbol": "MeshEdgesApi.length",
      "intent": "scene",
      "code": "// 1) Spawn a 2x1x1 cube + translate so edges have varied lengths.\nconst cube = await create.cube({ width: 2, height: 1, depth: 1, name: 'edges-length' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the first edge's length.\nconst len = cube.edges.length(0);\nconsole.log('edge 0 length: ' + len);\nconsole.log('is finite: ' + Number.isFinite(len));\nconsole.log('is positive: ' + (len > 0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.loopCut",
      "symbol": "MeshEdgesApi.loopCut",
      "intent": "scene",
      "code": "// NOTE: edge loopCut is an interactive (hover-driven) tool; its headless\n// operator is not yet wired (mesh.loopCutEdge is a throw-stub). The\n// runnable equivalent is `edges.insertLoop` — it slices a fresh edge loop\n// perpendicular to the seed edge, which is what a midpoint loopCut does.\n// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-loopcut' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.edges.count;\n// 2) Insert one fresh loop using edge 0 as the seed.\nawait cube.edges.insertLoop({ indices: [0], cuts: 1 });\nconsole.log('edge count after loop insert: ' + cube.edges.count);\nconsole.log('edges added: ' + (cube.edges.count - before));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.markSeam",
      "symbol": "MeshEdgesApi.markSeam",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-markseam' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Mark the first edge as a seam.\nawait cube.edges.markSeam({ indices: [0] });\nconsole.log('edge 0 is seam: ' + cube.edges.isSeam(0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.markSharp",
      "symbol": "MeshEdgesApi.markSharp",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-marksharp' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Mark the first edge as sharp.\nawait cube.edges.markSharp({ indices: [0] });\nconsole.log('edge 0 is sharp: ' + cube.edges.isSharp(0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.merge",
      "symbol": "MeshEdgesApi.merge",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-merge' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.verts.count;\n// 2) Collapse the first edge — its two endpoints weld into one vertex.\nawait cube.edges.merge({ indices: [0] });\nconsole.log('verts before: ' + before);\nconsole.log('verts after: ' + cube.verts.count);\nconsole.log('merge welded the edge endpoints: ' + (cube.verts.count < before));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.midpoint",
      "symbol": "MeshEdgesApi.midpoint",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-midpoint' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the first edge's midpoint.\nconst m = cube.edges.midpoint(0);\nconsole.log('midpoint: ' + JSON.stringify({ x: m.x, y: m.y, z: m.z }));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.nonManifold",
      "symbol": "MeshEdgesApi.nonManifold",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-nonmanifold' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) A clean cube has none.\nconsole.log('non-manifold count: ' + cube.edges.nonManifold.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.normal",
      "symbol": "MeshEdgesApi.normal",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-normal' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the first edge's averaged normal.\nconst n = cube.edges.normal(0);\nconsole.log('normal: ' + JSON.stringify({ x: n.x, y: n.y, z: n.z }));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.numFaces",
      "symbol": "MeshEdgesApi.numFaces",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-numfaces' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Closed cube edges always have 2 incident faces.\nconsole.log('numFaces(0): ' + cube.edges.numFaces(0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.positions",
      "symbol": "MeshEdgesApi.positions",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-positions' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the first edge's two endpoints.\nconst [p0, p1] = cube.edges.positions(0);\nconsole.log('p0: ' + JSON.stringify({ x: p0.x, y: p0.y, z: p0.z }));\nconsole.log('p1: ' + JSON.stringify({ x: p1.x, y: p1.y, z: p1.z }));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.rip",
      "symbol": "MeshEdgesApi.rip",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-rip' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst vertsBefore = cube.verts.count;\n// 2) Rip the first edge.\nawait cube.edges.rip({ indices: [0] });\nconsole.log('verts added by rip: ' + (cube.verts.count - vertsBefore));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.rotate",
      "symbol": "MeshEdgesApi.rotate",
      "intent": "scene",
      "code": "// 1) Spawn a cube, then triangulate so each face has a flippable diagonal.\nconst cube = await create.cube({ name: 'edges-rotate' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.triangulate();\n// 2) Flip the first edge to its other diagonal.\nawait cube.edges.rotate({ indices: [0] });\nconsole.log('edge count unchanged: ' + cube.edges.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.setPositions",
      "symbol": "MeshEdgesApi.setPositions",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-setpositions' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Move both endpoints of edge 0.\nawait cube.edges.setPositions(0, [[0, 0, 0], [1, 0, 0]]);\nconsole.log('edge 0 length after: ' + cube.edges.length(0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.setSeamAt",
      "symbol": "MeshEdgesApi.setSeamAt",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-setseamat' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Mark edge 0 as seam, then clear.\nawait cube.edges.setSeamAt(0, true);\nconsole.log('edge 0 is seam after true: ' + cube.edges.isSeam(0));\nawait cube.edges.setSeamAt(0, false);\nconsole.log('edge 0 is seam after false: ' + cube.edges.isSeam(0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.setSmoothAt",
      "symbol": "MeshEdgesApi.setSmoothAt",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-setsmoothat' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Flip edge 0 to sharp, then back to smooth.\nawait cube.edges.setSmoothAt(0, false);\nconsole.log('edge 0 is sharp after false: ' + cube.edges.isSharp(0));\nawait cube.edges.setSmoothAt(0, true);\nconsole.log('edge 0 is smooth after true: ' + cube.edges.isSmooth(0));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.slide",
      "symbol": "MeshEdgesApi.slide",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-slide' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Slide the first edge halfway along its strip.\nawait cube.edges.slide({ indices: [0], factor: 0.5 });\nconsole.log('edge count unchanged: ' + cube.edges.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.smooth",
      "symbol": "MeshEdgesApi.smooth",
      "intent": "scene",
      "code": "// 1) Build a subdivided cube so smoothing is visible.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1,\n    subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n    name: 'edges-smooth',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Smooth a small subset of edges with 3 iterations.\nawait cube.edges.smooth({ indices: [0, 1, 2, 3], iterations: 3, strength: 0.5 });\nconsole.log('edge count after smooth: ' + cube.edges.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.subdivide",
      "symbol": "MeshEdgesApi.subdivide",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-subdivide' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst vertsBefore = cube.verts.count;\n// 2) Subdivide the first edge: adds one midpoint vertex.\nawait cube.edges.subdivide({ indices: [0] });\nconsole.log('verts added by subdivide: ' + (cube.verts.count - vertsBefore));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.toFaces",
      "symbol": "MeshEdgesApi.toFaces",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-tofaces' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Convert edges 0+1 to incident faces.\nconst faces = cube.edges.toFaces([0, 1]);\nconsole.log('face count for {0,1}: ' + faces.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.toIslands",
      "symbol": "MeshEdgesApi.toIslands",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-toislands' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Convert edges 0+1 to UV islands.\nconst islands = cube.edges.toIslands([0, 1]);\nconsole.log('island count for {0,1}: ' + islands.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.totalLength",
      "symbol": "MeshEdgesApi.totalLength",
      "intent": "scene",
      "code": "// 1) Spawn a 1m cube + translate.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'edges-totallen' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) A 1m cube has 12 edges of length 1, so totalLength = 12.\nconsole.log('total length: ' + cube.edges.totalLength);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.toUVs",
      "symbol": "MeshEdgesApi.toUVs",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-touvs' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Convert edges 0+1 to UV loops.\nconst loops = cube.edges.toUVs([0, 1]);\nconsole.log('uv loop count for {0,1}: ' + loops.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.toVertices",
      "symbol": "MeshEdgesApi.toVertices",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-tovertices' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Convert edges 0+1 to their endpoint vertex set.\nconst verts = cube.edges.toVertices([0, 1]);\nconsole.log('vertex count for {0,1}: ' + verts.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.vertices",
      "symbol": "MeshEdgesApi.vertices",
      "intent": "scene",
      "code": "// 1) Spawn a cube + translate.\nconst cube = await create.cube({ name: 'edges-vertices' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the first edge's endpoints.\nconst [a, b] = cube.edges.vertices(0);\nconsole.log('edge 0 endpoint a: ' + a);\nconsole.log('edge 0 endpoint b: ' + b);\nconsole.log('distinct endpoints: ' + (a !== b));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshEdgesApi.where",
      "symbol": "MeshEdgesApi.where",
      "intent": "scene",
      "code": "// 1) Build a cube; 12 edges along the box wireframe.\nconst cube = await create.cube({ name: 'edges-where-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Where IS the select.\n// 3) Filter to edges shorter than 0.6 units (default cube → all qualify).\nconst short = cube.edges.where((e) => e.length < 0.6);\n// 4) Log + clean up.\nconsole.log('short edges: ' + short.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.addDivisions",
      "symbol": "MeshFacesApi.addDivisions",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-addDivisions-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.faces.count;\n// 2) Add 2 cut loops across the first face.\nawait cube.faces.addDivisions({ indices: [0], divisions: 2 });\nconsole.log('faces before: ' + before);\nconsole.log('faces after: ' + cube.faces.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.area",
      "symbol": "MeshFacesApi.area",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-area-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('face 0 area: ' + cube.faces.area(0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.bevel",
      "symbol": "MeshFacesApi.bevel",
      "intent": "scene",
      "code": "// NOTE: face-mode bevel is not yet wired headlessly (mesh.bevelFaces is\n// a throw-stub pending the face→boundary-edge expansion primitive). The\n// runnable equivalent is EDGE bevel — it rounds off the chosen edges and\n// grows the same kind of chamfer geometry. Bevel the boundary edges of\n// the face you wanted to bevel.\n// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-bevel-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst beforeVerts = cube.verts.count;\n// 2) Bevel an edge with one ring and a 0.1m-wide strip.\nawait cube.edges([0]).bevel({ width: 0.1, segments: 1 });\n// 3) Vertex count goes up as the chamfer adds new corners.\nconsole.log('verts before: ' + beforeVerts);\nconsole.log('verts after: ' + cube.verts.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.centroid",
      "symbol": "MeshFacesApi.centroid",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-centroid-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst c = cube.faces.centroid(0);\nconsole.log('face 0 centroid: ' + c.x + ',' + c.y + ',' + c.z);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.collapse",
      "symbol": "MeshFacesApi.collapse",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-collapse-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst beforeFaces = cube.faces.count;\n// 2) Manufacture a spike: pull the top face's corner vertex 20m along +Z\n//    so that face becomes a needle-thin sliver (a sub-5° interior angle).\n//    A clean cube has no spikes, so collapse would otherwise be a no-op.\nawait cube.verts.setPosition(1, [0, 0, 20], { space: 'object', relative: true });\n// 3) Collapse spike faces — the manufactured spike face is removed.\nawait cube.faces.collapse();\n// 4) The spike face is gone, so the face count drops.\nconsole.log('faces before: ' + beforeFaces);\nconsole.log('faces after: ' + cube.faces.count);\nconsole.log('collapse removed the spike face: ' + (cube.faces.count < beforeFaces));\n// 5) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.count",
      "symbol": "MeshFacesApi.count",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-count-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Log the face count. A unit cube has 6 quad faces.\nconsole.log('face count: ' + cube.faces.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.delete",
      "symbol": "MeshFacesApi.delete",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-delete-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.faces.count;\n// 2) Delete the first face. The cube gains an open hole.\nawait cube.faces.delete({ indices: [0] });\nconst after = cube.faces.count;\nconsole.log('faces before: ' + before);\nconsole.log('faces after: ' + after);\nconsole.log('dropped exactly one face: ' + (before - after === 1));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.description",
      "symbol": "MeshFacesApi.description",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-description-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('description: ' + cube.faces.description());\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.dissolve",
      "symbol": "MeshFacesApi.dissolve",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-dissolve-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst beforeFaces = cube.faces.count;\n// 2) Dissolve the first face. Perimeter edges merge into surrounding faces.\nawait cube.faces.dissolve({ indices: [0] });\nconsole.log('faces before: ' + beforeFaces);\nconsole.log('faces after: ' + cube.faces.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.edges",
      "symbol": "MeshFacesApi.edges",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-edges-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst e = cube.faces.edges(0);\nconsole.log('face 0 edge count: ' + e.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.example",
      "symbol": "MeshFacesApi.example",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-example-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('example length: ' + cube.faces.example().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.extrude",
      "symbol": "MeshFacesApi.extrude",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-extrude-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.faces.count;\n// 2) Pull two faces outward 0.5m, each as its own island.\nawait cube.faces.extrude({ indices: [0, 1], distance: 0.5, individualFaces: true });\nconsole.log('faces before: ' + before);\nconsole.log('faces after: ' + cube.faces.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.flatten",
      "symbol": "MeshFacesApi.flatten",
      "intent": "scene",
      "code": "// NOTE: face-mode flatten is not yet wired headlessly (mesh.flattenFaces\n// is a throw-stub). The runnable equivalents are the mesh-wide\n// `mesh.flattenToPlane(...)` shown here, or per-vertex\n// `mesh.verts([...]).flatten({ plane })` for a component-scoped flatten.\n// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-flatten-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Flatten the whole mesh onto the world XZ plane (collapse along +Y).\nawait cube.flattenToPlane({ planeOrigin: [0, 0, 0], planeNormal: [0, 1, 0] });\nconsole.log('faces flattened: ' + cube.faces.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.forEach",
      "symbol": "MeshFacesApi.forEach",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-forEach-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Visit every face and log a verifiable property.\nlet visited = 0;\ncube.faces.forEach((i, total) => { visited++; });\nconsole.log('faces visited: ' + visited);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.getAreas",
      "symbol": "MeshFacesApi.getAreas",
      "intent": "scene",
      "code": "// 1) Build a cube and translate it off the origin.\nconst cube = await create.cube({ name: 'faces-getAreas-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Snapshot the per-face areas and log the array length.\nconst areas = cube.faces.getAreas();\nconsole.log('face areas length: ' + areas.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.getCentroids",
      "symbol": "MeshFacesApi.getCentroids",
      "intent": "scene",
      "code": "// 1) Build a cube and translate it off the origin.\nconst cube = await create.cube({ name: 'faces-getCentroids-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Snapshot the per-face centroids and log the array length.\nconst centroids = cube.faces.getCentroids();\nconsole.log('face centroids length: ' + centroids.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.getNormals",
      "symbol": "MeshFacesApi.getNormals",
      "intent": "scene",
      "code": "// 1) Build a cube and translate it off the origin.\nconst cube = await create.cube({ name: 'faces-getNormals-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Snapshot the per-face normals and log the array length.\nconst normals = cube.faces.getNormals();\nconsole.log('face normals length: ' + normals.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.help",
      "symbol": "MeshFacesApi.help",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-help-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('help length: ' + cube.faces.help().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.inset",
      "symbol": "MeshFacesApi.inset",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-inset-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst beforeVerts = cube.verts.count;\n// 2) Inset the first face by 30% and push it inward 0.05m.\nawait cube.faces.inset({ indices: [0], amount: 0.3, depth: 0.05 });\nconsole.log('verts before: ' + beforeVerts);\nconsole.log('verts after: ' + cube.verts.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.isBoundary",
      "symbol": "MeshFacesApi.isBoundary",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-isBoundary-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.faces.delete({ indices: [0] });\nconsole.log('face 1 boundary after hole: ' + cube.faces.isBoundary(1));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.isConvex",
      "symbol": "MeshFacesApi.isConvex",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-isConvex-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('face 0 convex: ' + cube.faces.isConvex(0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.isPlanar",
      "symbol": "MeshFacesApi.isPlanar",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-isPlanar-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('face 0 planar (1e-4): ' + cube.faces.isPlanar(0, 1e-4));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.normal",
      "symbol": "MeshFacesApi.normal",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-normal-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst n = cube.faces.normal(0, { space: 'world' });\nconsole.log('face 0 normal: ' + n.x + ',' + n.y + ',' + n.z);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.numTriangles",
      "symbol": "MeshFacesApi.numTriangles",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-numTriangles-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) A cube has 6 quads, so fan-triangulation yields 12 triangles.\nconsole.log('triangle count: ' + cube.faces.numTriangles);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.numTrianglesAt",
      "symbol": "MeshFacesApi.numTrianglesAt",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-numTrianglesAt-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('face 0 fan-triangles: ' + cube.faces.numTrianglesAt(0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.numVerts",
      "symbol": "MeshFacesApi.numVerts",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-numVerts-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('face 0 verts: ' + cube.faces.numVerts(0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.poke",
      "symbol": "MeshFacesApi.poke",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-poke-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.faces.count;\n// 2) Poke the first face (centre vertex placed at the face centroid).\nawait cube.faces.poke({ indices: [0] });\n// 3) A quad becomes 4 triangles, so face count rises by 3.\nconsole.log('faces before: ' + before);\nconsole.log('faces after: ' + cube.faces.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.positions",
      "symbol": "MeshFacesApi.positions",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-positions-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst ps = cube.faces.positions(0);\nconsole.log('face 0 corners: ' + ps.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.reverseNormals",
      "symbol": "MeshFacesApi.reverseNormals",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-reverseNormals-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the first face's normal, then reverse it.\nconst before = cube.faces.normal(0);\nawait cube.faces.reverseNormals({ indices: [0] });\nconst after = cube.faces.normal(0);\n// 3) The normal should point the opposite way.\nconsole.log('normal x flipped: ' + (Math.abs(after.x + before.x) < 1e-6));\nconsole.log('normal y flipped: ' + (Math.abs(after.y + before.y) < 1e-6));\nconsole.log('normal z flipped: ' + (Math.abs(after.z + before.z) < 1e-6));\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.rip",
      "symbol": "MeshFacesApi.rip",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-rip-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst beforeVerts = cube.verts.count;\n// 2) Rip the first face from its neighbors.\nawait cube.faces.rip({ indices: [0] });\n// 3) Vertex count goes up because shared corners are split.\nconsole.log('verts before: ' + beforeVerts);\nconsole.log('verts after: ' + cube.verts.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.setColor",
      "symbol": "MeshFacesApi.setColor",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-setColor-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Paint the first face bright red.\nawait cube.faces.setColor(0, [1, 0, 0, 1]);\nconsole.log('face colored: 0');\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.setPositions",
      "symbol": "MeshFacesApi.setPositions",
      "intent": "scene",
      "code": "// 1) Build a cube and translate it off the origin.\nconst cube = await create.cube({ name: 'faces-setPositions-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the current corners of face 0 (winding order) and push every\n//    corner up by 0.1m. `verts.position(i)` returns a Vec3 — read its\n//    .x/.y/.z (it is NOT array-indexable) when building the tuple.\nconst corners = cube.faces.vertices(0);\nconst bumped = corners.map((vi) => {\n    const p = cube.verts.position(vi);\n    return [p.x, p.y + 0.1, p.z];\n});\nawait cube.faces.setPositions(0, bumped);\nconsole.log('face 0 corners moved: ' + bumped.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.smooth",
      "symbol": "MeshFacesApi.smooth",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-smooth-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Smooth the first two faces over three passes at strength 0.4.\nawait cube.faces.smooth({ indices: [0, 1], iterations: 3, strength: 0.4 });\n// 3) Log a verifiable property.\nconsole.log('face count after smooth: ' + cube.faces.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.splitConcave",
      "symbol": "MeshFacesApi.splitConcave",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-splitConcave-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Split any concave faces. Cube faces are already convex so this is a no-op.\nawait cube.faces.splitConcave({ indices: [0, 1, 2] });\nconsole.log('faces after split: ' + cube.faces.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.splitNonPlanar",
      "symbol": "MeshFacesApi.splitNonPlanar",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-splitNonPlanar-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Split any face whose corners deviate from a plane by more than 5 degrees.\nawait cube.faces.splitNonPlanar({ indices: [0, 1, 2, 3], angle: 5 * Math.PI / 180 });\nconsole.log('faces after split: ' + cube.faces.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.subdivide",
      "symbol": "MeshFacesApi.subdivide",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-subdivide-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.faces.count;\n// 2) Subdivide the first face twice. 1 face becomes 16 faces.\nawait cube.faces.subdivide({ indices: [0], iterations: 2 });\nconsole.log('faces before: ' + before);\nconsole.log('faces after: ' + cube.faces.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.toEdges",
      "symbol": "MeshFacesApi.toEdges",
      "intent": "scene",
      "code": "// 1) Build a cube and translate it off the origin.\nconst cube = await create.cube({ name: 'faces-toEdges-single' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Walk the edges around face 0.\nconst edges = cube.faces.toEdges(0);\nconsole.log('face 0 edges: ' + edges.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.toIslands",
      "symbol": "MeshFacesApi.toIslands",
      "intent": "scene",
      "code": "// 1) Build a cube and translate it off the origin.\nconst cube = await create.cube({ name: 'faces-toIslands-single' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Resolve which UV island face 0 lives in.\nconst islands = cube.faces.toIslands(0);\nconsole.log('face 0 islands: ' + islands.join(','));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.toUVs",
      "symbol": "MeshFacesApi.toUVs",
      "intent": "scene",
      "code": "// 1) Build a cube and translate it off the origin.\nconst cube = await create.cube({ name: 'faces-toUVs-single' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the UV loops around face 0.\nconst loops = cube.faces.toUVs(0);\nconsole.log('face 0 uv loops: ' + loops.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.toVertices",
      "symbol": "MeshFacesApi.toVertices",
      "intent": "scene",
      "code": "// 1) Build a cube and translate it off the origin.\nconst cube = await create.cube({ name: 'faces-toVertices-single' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the vertex indices that make up face 0.\nconst verts = cube.faces.toVertices(0);\nconsole.log('face 0 verts: ' + verts.join(','));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.triangles",
      "symbol": "MeshFacesApi.triangles",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'tri-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst tris = cube.faces.triangles(0);\nconsole.log('face 0 triangle indices length: ' + tris.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.triangulate",
      "symbol": "MeshFacesApi.triangulate",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off the origin.\nconst cube = await create.cube({ name: 'faces-triangulate-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.faces.count;\n// 2) Warp one corner off-plane so a couple of faces become non-planar.\n//    (A clean cube is perfectly planar, so triangulate would be a no-op.)\nawait cube.verts.setPosition(0, [0, 0.35, 0], { space: 'object', relative: true });\n// 3) Triangulate — the now non-planar faces split into triangles.\nawait cube.faces.triangulate();\nconsole.log('faces before: ' + before);\nconsole.log('faces after: ' + cube.faces.count);\nconsole.log('triangulate raised face count: ' + (cube.faces.count > before));\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.vertices",
      "symbol": "MeshFacesApi.vertices",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'faces-vertices-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst v = cube.faces.vertices(0);\nconsole.log('face 0 vertex count: ' + v.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshFacesApi.where",
      "symbol": "MeshFacesApi.where",
      "intent": "scene",
      "code": "// 1) Build a cube; 6 quad faces.\nconst cube = await create.cube({ name: 'faces-where-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Where IS the select — no preselect step.\n// 3) Filter to faces whose centroid is on the +Z half.\nconst front = cube.faces.where((f) => f.centroid.z > 0);\n// 4) Log + clean up.\nconsole.log('front faces: ' + front.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi",
      "symbol": "MeshGeometryApi",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin so renderer + BVH stay fresh.\nconst cube = await create.cube({ name: 'demo-geometry' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read surface area, manifold flags, hole/shell counts.\nconsole.log('area: ' + cube.geometry.area);\nconsole.log('isManifold: ' + cube.geometry.isManifold);\nconsole.log('isClosed: ' + cube.geometry.isClosed);\nconsole.log('shells: ' + cube.geometry.shells + ' holes: ' + cube.geometry.holes);\n// 3) Spatial query.\nconst box = cube.geometry.bbox({ space: 'world' });\nconsole.log('bbox center: ' + box.center.toArray().join(','));\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.area",
      "symbol": "MeshGeometryApi.area",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'area-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('cube area: ' + cube.geometry.area.toFixed(3));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.bbox",
      "symbol": "MeshGeometryApi.bbox",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'bbox-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Query in world AND object space.\nconst worldBox = cube.geometry.bbox({ space: 'world' });\nconst localBox = cube.geometry.bbox({ space: 'object' });\n// 3) Log centers.\nconsole.log('world center: ' + worldBox.center.toArray().join(','));\nconsole.log('local center: ' + localBox.center.toArray().join(','));\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.closestNormal",
      "symbol": "MeshGeometryApi.closestNormal",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'cn-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Query the normal at a point just outside the +X face.\nconst n = cube.geometry.closestNormal([5, 1.3, -0.7], { space: 'world' });\n// 3) Log the normal as a tuple.\nconsole.log('normal: ' + (n ? n.toArray().join(',') : 'null'));\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.closestPoint",
      "symbol": "MeshGeometryApi.closestPoint",
      "intent": "scene",
      "code": "// 1) Build a sphere off-origin.\nconst sphere = await create.sphere({ radius: 1, name: 'cp-demo' });\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Query the closest surface point from outside the sphere.\nconst result = sphere.geometry.closestPoint([5, 1.3, -0.7], { space: 'world' });\n// 3) Log non-null verdict.\nconsole.log('closestPoint returned: ' + (result !== null));\n// 4) Clean up.\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.closestPointAndNormal",
      "symbol": "MeshGeometryApi.closestPointAndNormal",
      "intent": "scene",
      "code": "// 1) Build a sphere off-origin.\nconst sphere = await create.sphere({ radius: 1, name: 'cpn-demo' });\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Query from outside the sphere.\nconst hit = sphere.geometry.closestPointAndNormal([5, 1.3, -0.7], { space: 'world' });\n// 3) Log normal direction.\nconsole.log('hit normal: ' + (hit ? hit.normal.toArray().join(',') : 'null'));\n// 4) Clean up.\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.concaveFaces",
      "symbol": "MeshGeometryApi.concaveFaces",
      "intent": "scene",
      "code": "// 1) Build a clean cube; expect zero concave faces.\nconst cube = await create.cube({ name: 'concave-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Run the diagnostic.\nconst concave = cube.geometry.concaveFaces();\n// 3) Log the count.\nconsole.log('concave face count: ' + concave.length);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.distanceTo",
      "symbol": "MeshGeometryApi.distanceTo",
      "intent": "scene",
      "code": "// 1) Build a sphere off-origin.\nconst sphere = await create.sphere({ radius: 1, name: 'distto-demo' });\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Measure distance to a point 3 units outside the surface.\nconst d = sphere.geometry.distanceTo([5.5, 1.3, -0.7], { space: 'world' });\n// 3) Log the result.\nconsole.log('signed distance: ' + d.toFixed(3));\n// 4) Clean up.\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.example",
      "symbol": "MeshGeometryApi.example",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'geom-example' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('geometry example length: ' + cube.geometry.example().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.help",
      "symbol": "MeshGeometryApi.help",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'geom-help' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('geometry help length: ' + cube.geometry.help().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.holes",
      "symbol": "MeshGeometryApi.holes",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'holes-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('cube holes: ' + cube.geometry.holes);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.isClosed",
      "symbol": "MeshGeometryApi.isClosed",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'closed-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('cube closed: ' + cube.geometry.isClosed);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.isManifold",
      "symbol": "MeshGeometryApi.isManifold",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'manifold-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('cube manifold: ' + cube.geometry.isManifold);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.laminaFaces",
      "symbol": "MeshGeometryApi.laminaFaces",
      "intent": "scene",
      "code": "// 1) Build a clean cube — it has no lamina faces by construction.\nconst cube = await create.cube({ name: 'lamina-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Run the diagnostic.\nconst dup = cube.geometry.laminaFaces();\n// 3) Log the count.\nconsole.log('lamina face count: ' + dup.length);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.nonManifoldEdges",
      "symbol": "MeshGeometryApi.nonManifoldEdges",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'nme-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('nonManifoldEdges: ' + cube.geometry.nonManifoldEdges.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.nonManifoldVertices",
      "symbol": "MeshGeometryApi.nonManifoldVertices",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'nmv-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('nonManifoldVertices: ' + cube.geometry.nonManifoldVertices.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.nonPlanarFaces",
      "symbol": "MeshGeometryApi.nonPlanarFaces",
      "intent": "scene",
      "code": "// 1) Build a clean cube — every face is flat.\nconst cube = await create.cube({ name: 'planar-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Run the diagnostic with a tight tolerance.\nconst nonPlanar = cube.geometry.nonPlanarFaces(1e-3);\n// 3) Log the count.\nconsole.log('non-planar face count: ' + nonPlanar.length);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.pointAtUV",
      "symbol": "MeshGeometryApi.pointAtUV",
      "intent": "scene",
      "code": "// 1) Build a plane off-origin (planes have clean UVs corner to corner).\nconst plane = await create.plane({ width: 2, height: 2, name: 'puv-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Look up the center of the UV square on face 0 in UV set 0.\nconst pos = plane.geometry.pointAtUV([0.5, 0.5], { faceIdx: 0, setIdx: 0 });\n// 3) Log the resolved position.\nconsole.log('point at UV: ' + (pos ? pos.toArray().join(',') : 'null'));\n// 4) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.pointInside",
      "symbol": "MeshGeometryApi.pointInside",
      "intent": "scene",
      "code": "// 1) Build a sphere off-origin.\nconst sphere = await create.sphere({ radius: 1, name: 'inside-demo' });\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Test the sphere's center (inside) and a point far away (outside).\nconst insideCenter = sphere.geometry.pointInside([2.5, 1.3, -0.7], { space: 'world' });\nconst outsideFar = sphere.geometry.pointInside([10, 10, 10], { space: 'world' });\n// 3) Log verifiable booleans.\nconsole.log('center inside: ' + insideCenter);\nconsole.log('far outside: ' + outsideFar);\n// 4) Clean up.\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.raycast",
      "symbol": "MeshGeometryApi.raycast",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'raycast-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Cast a ray toward the cube from a few units away on the +X side.\nconst hit = cube.geometry.raycast([5, 1.3, -0.7], [-1, 0, 0], { space: 'world' });\n// 3) Log a verifiable property.\nconsole.log('raycast hit: ' + (hit !== null));\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.raycastAll",
      "symbol": "MeshGeometryApi.raycastAll",
      "intent": "scene",
      "code": "// 1) Build a sphere off-origin.\nconst sphere = await create.sphere({ radius: 1, name: 'raycastall-demo' });\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Cast a ray straight through the sphere — expect entry + exit hits.\nconst hits = sphere.geometry.raycastAll([5, 1.3, -0.7], [-1, 0, 0], { space: 'world' });\n// 3) Log the hit count.\nconsole.log('raycastAll hit count: ' + hits.length);\n// 4) Clean up.\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.shells",
      "symbol": "MeshGeometryApi.shells",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'shells-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('cube shells: ' + cube.geometry.shells);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.uvAtPoint",
      "symbol": "MeshGeometryApi.uvAtPoint",
      "intent": "scene",
      "code": "// 1) Build a plane off-origin.\nconst plane = await create.plane({ width: 2, height: 2, name: 'uvap-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Look up the UV near the plane's center.\nconst hit = plane.geometry.uvAtPoint([2.5, 1.3, -0.7], { space: 'world', setIdx: 0 });\n// 3) Log the UV.\nconsole.log('uv at point: ' + (hit ? hit.uv.join(',') : 'null'));\n// 4) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshGeometryApi.zeroAreaFaces",
      "symbol": "MeshGeometryApi.zeroAreaFaces",
      "intent": "scene",
      "code": "// 1) Build a clean cube; expect zero zero-area faces.\nconst cube = await create.cube({ name: 'zero-area-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Run the diagnostic with a custom tolerance.\nconst slivers = cube.geometry.zeroAreaFaces(1e-6);\n// 3) Log the count.\nconsole.log('zero-area face count: ' + slivers.length);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshLiveApi",
      "symbol": "MeshLiveApi",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'live-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Make it the active snap-target and isolate it in the viewport.\nawait cube.live.make();\nawait cube.live.isolate();\n// 3) Clear the snap-target binding.\nawait cube.live.clear();\nconsole.log('live demo: ok');\n// 4) Clean up. clear() already released the live-mesh; exit isolate mode\n//    too so the editor returns to its default viewport state.\nawait viewport.deisolate();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshLiveApi.clear",
      "symbol": "MeshLiveApi.clear",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin and mark it live.\nconst cube = await create.cube({ name: 'clear-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.live.make();\n// 2) Clear the live snap-target binding.\nawait cube.live.clear();\n// 3) Log a verifiable property.\nconsole.log('cleared live: ' + cube.name);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshLiveApi.example",
      "symbol": "MeshLiveApi.example",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'live-example' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('live example length: ' + cube.live.example().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshLiveApi.help",
      "symbol": "MeshLiveApi.help",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'live-help' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('live help length: ' + cube.live.help().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshLiveApi.isolate",
      "symbol": "MeshLiveApi.isolate",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'isolate-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Isolate it in the viewport.\nawait cube.live.isolate();\n// 3) Log a verifiable property.\nconsole.log('isolated mesh name: ' + cube.name);\n// 4) Clean up. Isolate is a viewport-visibility toggle (not per-mesh\n//    state), so exit it explicitly to return the editor to its default.\nawait viewport.deisolate();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshLiveApi.make",
      "symbol": "MeshLiveApi.make",
      "intent": "scene",
      "code": "// 1) Build a sphere off-origin.\nconst sphere = await create.sphere({ radius: 1, name: 'make-demo' });\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Make it the active snap target.\nawait sphere.live.make();\n// 3) Log a verifiable property.\nconsole.log('made live: ' + sphere.name);\n// 4) Clean up — release the live-mesh binding so the editor returns to\n//    its default state, then delete.\nawait sphere.live.clear();\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshMaterialApi",
      "symbol": "MeshMaterialApi",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin and create a red material.\nconst cube = await create.cube({ name: 'mat-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst red = await create.material({ name: 'red', baseColor: [1, 0, 0, 1] });\n// 2) Assign + read back.\nawait cube.material.set(red);\nconst assigned = cube.material.get();\n// 3) Log a verifiable property.\nconsole.log('assigned material id: ' + (assigned ? assigned.materialId : 'null'));\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshMaterialApi.example",
      "symbol": "MeshMaterialApi.example",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'mat-example' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('material example length: ' + cube.material.example().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshMaterialApi.get",
      "symbol": "MeshMaterialApi.get",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'mat-get-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the current material (may be null on a fresh primitive).\nconst current = cube.material.get();\n// 3) Log a verifiable property.\nconsole.log('has material: ' + (current !== null));\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshMaterialApi.help",
      "symbol": "MeshMaterialApi.help",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'mat-help' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('material help length: ' + cube.material.help().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshMaterialApi.set",
      "symbol": "MeshMaterialApi.set",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin and create a green material.\nconst cube = await create.cube({ name: 'mat-set-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst green = await create.material({ name: 'green', baseColor: [0, 1, 0, 1] });\n// 2) Assign it.\nawait cube.material.set(green);\n// 3) Log a verifiable property.\nconsole.log('material assigned: ' + green.name);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshMorphTargetsApi",
      "symbol": "MeshMorphTargetsApi",
      "intent": "scene",
      "code": "// 1) Inspect a head's blendshapes, then dial one in.\nconst head = ls('Head')[0];\nconsole.log(head.morphTargets.list());\nhead.morphTargets.setWeight('smile', 0.8);"
    },
    {
      "kind": "example",
      "id": "example:MeshMorphTargetsApi.example",
      "symbol": "MeshMorphTargetsApi.example",
      "intent": "scene",
      "code": "console.log('morphTargets example length: ' + ls('Head')[0].morphTargets.example().length);"
    },
    {
      "kind": "example",
      "id": "example:MeshMorphTargetsApi.get",
      "symbol": "MeshMorphTargetsApi.get",
      "intent": "scene",
      "code": "const t = ls('Head')[0].morphTargets.get('smile');\nconsole.log(t ? t.weight : 'no such shape');"
    },
    {
      "kind": "example",
      "id": "example:MeshMorphTargetsApi.help",
      "symbol": "MeshMorphTargetsApi.help",
      "intent": "scene",
      "code": "console.log('morphTargets help length: ' + ls('Head')[0].morphTargets.help().length);"
    },
    {
      "kind": "example",
      "id": "example:MeshMorphTargetsApi.list",
      "symbol": "MeshMorphTargetsApi.list",
      "intent": "scene",
      "code": "const shapes = ls('Head')[0].morphTargets.list();\nconsole.log('morph targets: ' + shapes.length);"
    },
    {
      "kind": "example",
      "id": "example:MeshMorphTargetsApi.setWeight",
      "symbol": "MeshMorphTargetsApi.setWeight",
      "intent": "scene",
      "code": "// 1) Pick a mesh that carries blendshapes (imported FBX/GLB or a baked corrective).\nconst head = ls('Head')[0];\nconst before = head.morphTargets.get('smile');\n// 2) Dial the shape to 80% (same drive the channel-box slider uses).\nawait head.morphTargets.setWeight('smile', 0.8);\n// 3) Verify the persisted weight moved.\nconst after = head.morphTargets.get('smile');\nconsole.log('weight moved: ' + (before?.weight ?? 0) + ' -> ' + (after?.weight ?? 0));"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi",
      "symbol": "MeshNormalsApi",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin so the renderer and BVH stay fresh.\nconst cube = await create.cube({ name: 'normals-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Recompute, then read a single vertex normal.\nawait cube.normals.recalculate();\nconst n0 = cube.normals.getVertex(0, { angleWeighted: true });\nconsole.log('vertex 0 normal: ' + n0.toArray().join(','));\n// 3) Apply an angle-weighted smoothing pass for a softer shading look.\nawait cube.normals.applyWeighted({ mode: 'angle-at-vert' });\nconsole.log('per-vertex normal count: ' + cube.normals.count);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.applyWeighted",
      "symbol": "MeshNormalsApi.applyWeighted",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-apply-weighted' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Try every weighting mode and log the resulting vertex 0 normal.\nawait cube.normals.applyWeighted({ mode: 'face-area' });\nconsole.log('face-area:     ' + cube.normals.getVertex(0).toArray().join(','));\nawait cube.normals.applyWeighted({ mode: 'angle-at-vert' });\nconsole.log('angle-at-vert: ' + cube.normals.getVertex(0).toArray().join(','));\nawait cube.normals.applyWeighted({ mode: 'combined' });\nconsole.log('combined:      ' + cube.normals.getVertex(0).toArray().join(','));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.clearCustom",
      "symbol": "MeshNormalsApi.clearCustom",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-clear-custom' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Lock vertices 0 and 1, then drop the locks.\nawait cube.normals.setCustom({ indices: [0, 1] });\nawait cube.normals.clearCustom();\nconsole.log('vertex 0 normal after clear: ' + cube.normals.getVertex(0).toArray().join(','));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.cornerCount",
      "symbol": "MeshNormalsApi.cornerCount",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-corner-count' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the per-face-corner normal count.\nconsole.log('per-face-corner normal count: ' + cube.normals.cornerCount);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.count",
      "symbol": "MeshNormalsApi.count",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-count' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the per-vertex normal count.\nconsole.log('per-vertex normal count: ' + cube.normals.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.example",
      "symbol": "MeshNormalsApi.example",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-example' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Log the example snippet.\nconsole.log('normals example length: ' + cube.normals.example().length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.get",
      "symbol": "MeshNormalsApi.get",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-get' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Pull the flat buffer and log its length.\nconst buf = cube.normals.get();\nconsole.log('normal buffer length: ' + buf.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.getCorners",
      "symbol": "MeshNormalsApi.getCorners",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-get-corners' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Pull the corner-normal buffer.\nconst buf = cube.normals.getCorners();\nconsole.log('corner-normal buffer length: ' + buf.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.getFace",
      "symbol": "MeshNormalsApi.getFace",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-get-face' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read face 0's outward normal.\nconst n = cube.normals.getFace(0);\nconsole.log('face 0 normal: ' + n.toArray().join(','));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.getFaces",
      "symbol": "MeshNormalsApi.getFaces",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-get-faces' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Pull the face-normal buffer.\nconst buf = cube.normals.getFaces();\nconsole.log('face-normal buffer length: ' + buf.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.getFaceVertex",
      "symbol": "MeshNormalsApi.getFaceVertex",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-get-fv' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the corner normal where face 0 meets its first vertex.\n// `faces.vertices(i)` returns the vertex indices of face i (not the\n//  stale `faces.verts(i)`); take the first corner.\nconst firstVert = cube.faces.vertices(0)[0];\nconst n = cube.normals.getFaceVertex(0, firstVert);\nconsole.log('corner normal (face 0, vert ' + firstVert + '): ' + n.toArray().join(','));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.getVertex",
      "symbol": "MeshNormalsApi.getVertex",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-get-vertex' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read vertex 0 with angle weighting both on and off.\nconst plain = cube.normals.getVertex(0, { angleWeighted: false });\nconst weighted = cube.normals.getVertex(0, { angleWeighted: true });\nconsole.log('vertex 0 plain:    ' + plain.toArray().join(','));\nconsole.log('vertex 0 weighted: ' + weighted.toArray().join(','));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.getVertices",
      "symbol": "MeshNormalsApi.getVertices",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-get-verts' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Pull plain and angle-weighted bulk buffers.\nconst plain = cube.normals.getVertices({ angleWeighted: false });\nconst weighted = cube.normals.getVertices({ angleWeighted: true });\nconsole.log('plain buffer length:    ' + plain.length);\nconsole.log('weighted buffer length: ' + weighted.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.help",
      "symbol": "MeshNormalsApi.help",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-help' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Log the help summary.\nconsole.log('normals help: ' + cube.normals.help());\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.recalculate",
      "symbol": "MeshNormalsApi.recalculate",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-recalc' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Recompute normals from face geometry.\nawait cube.normals.recalculate();\nconsole.log('vertex 0 normal after recalc: ' + cube.normals.getVertex(0).toArray().join(','));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.reverse",
      "symbol": "MeshNormalsApi.reverse",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-reverse' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Reverse a single face — its outward normal now points inward.\nconst beforeFace = cube.normals.getFace(0).toArray().join(',');\nawait cube.normals.reverse([0]);\nconst afterFace = cube.normals.getFace(0).toArray().join(',');\nconsole.log('face 0 normal before: ' + beforeFace);\nconsole.log('face 0 normal after:  ' + afterFace);\n// 3) Reverse every face at once (omit the list).\nawait cube.normals.reverse();\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.set",
      "symbol": "MeshNormalsApi.set",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-set' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Build a buffer where every normal points up (+Y).\nconst arr = new Float32Array(cube.normals.count * 3);\nfor (let i = 0; i < cube.normals.count; i++) {\n    arr[i * 3 + 0] = 0;\n    arr[i * 3 + 1] = 1;\n    arr[i * 3 + 2] = 0;\n}\nawait cube.normals.set(arr);\n// 3) Verify the write took effect.\nconst v0 = cube.normals.getVertex(0);\nconsole.log('vertex 0 normal y after set: ' + v0.y);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.setCorners",
      "symbol": "MeshNormalsApi.setCorners",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-set-corners' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Build a buffer where every corner normal points up.\nconst arr = new Float32Array(cube.normals.cornerCount * 3);\nfor (let i = 0; i < cube.normals.cornerCount; i++) {\n    arr[i * 3 + 1] = 1;\n}\nawait cube.normals.setCorners(arr);\n// `faces.vertices(i)` returns the vertex indices of face i (not the\n//  stale `faces.verts(i)`); take the first corner.\nconst firstVert = cube.faces.vertices(0)[0];\nconsole.log('corner normal y after bulk write: ' + cube.normals.getFaceVertex(0, firstVert).y);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.setCustom",
      "symbol": "MeshNormalsApi.setCustom",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-set-custom' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Set vertex 0's normal to up, then lock vertices 0 and 1 as custom.\nawait cube.normals.setVertex(0, [0, 1, 0]);\nawait cube.normals.setCustom({ indices: [0, 1] });\n// 3) A recalc preserves the locked normal at vertex 0.\nawait cube.normals.recalculate();\nconsole.log('vertex 0 normal y after lock+recalc: ' + cube.normals.getVertex(0).y);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.setFaceVertex",
      "symbol": "MeshNormalsApi.setFaceVertex",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-set-fv' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Force the (face 0, first vert) corner to point straight up.\n// `faces.vertices(i)` returns the vertex indices of face i (not the\n//  stale `faces.verts(i)`); take the first corner.\nconst firstVert = cube.faces.vertices(0)[0];\nawait cube.normals.setFaceVertex(0, firstVert, [0, 1, 0]);\nconst n = cube.normals.getFaceVertex(0, firstVert);\nconsole.log('corner normal y after write: ' + n.y);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.setVertex",
      "symbol": "MeshNormalsApi.setVertex",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-set-vertex' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Force vertex 0's normal to point straight up.\nawait cube.normals.setVertex(0, [0, 1, 0]);\nconst n = cube.normals.getVertex(0);\nconsole.log('vertex 0 normal y after write: ' + n.y);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.setVertices",
      "symbol": "MeshNormalsApi.setVertices",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-set-verts' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Build a buffer where every normal points up.\nconst arr = new Float32Array(cube.normals.count * 3);\nfor (let i = 0; i < cube.normals.count; i++) {\n    arr[i * 3 + 1] = 1;\n}\nawait cube.normals.setVertices(arr);\nconsole.log('vertex 0 normal y after bulk write: ' + cube.normals.getVertex(0).y);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshNormalsApi.smooth",
      "symbol": "MeshNormalsApi.smooth",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'normals-smooth' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Smooth all normals.\nawait cube.normals.smooth();\nconsole.log('vertex 0 normal after smooth: ' + cube.normals.getVertex(0).toArray().join(','));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshOperationResult.faceVertexCounts",
      "symbol": "MeshOperationResult.faceVertexCounts",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'meshop-fvc-demo' });\nconst result = await cube.slice({ mode: 'plane', axis: 'y', offset: 0 });\nconsole.log('faceVertexCounts populated: ' + (result?.faceVertexCounts !== undefined));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshOperationResult.faceVertexIndices",
      "symbol": "MeshOperationResult.faceVertexIndices",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'meshop-fvi-demo' });\nconst result = await cube.slice({ mode: 'plane', axis: 'y', offset: 0 });\nconsole.log('faceVertexIndices populated: ' + (result?.faceVertexIndices !== undefined));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshOperationResult.indices",
      "symbol": "MeshOperationResult.indices",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'meshop-indices-demo' });\nconst result = await cube.slice({ mode: 'plane', axis: 'y', offset: 0 });\nconsole.log('indices populated: ' + (result?.indices !== undefined));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshOperationResult.normals",
      "symbol": "MeshOperationResult.normals",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'meshop-normals-demo' });\nconst result = await cube.slice({ mode: 'plane', axis: 'y', offset: 0 });\nconsole.log('normals populated: ' + (result?.normals !== undefined));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshOperationResult.positions",
      "symbol": "MeshOperationResult.positions",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'meshop-positions-demo' });\nconst result = await cube.slice({ mode: 'plane', axis: 'y', offset: 0 });\nconsole.log('positions is Float32Array: ' + (result?.positions instanceof Float32Array));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshOperationResult.uvs",
      "symbol": "MeshOperationResult.uvs",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'meshop-uvs-demo' });\nconst result = await cube.slice({ mode: 'plane', axis: 'y', offset: 0 });\nconsole.log('uvs populated: ' + (result?.uvs !== undefined));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshToolsApi",
      "symbol": "MeshToolsApi",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'tools-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Launch a session and cancel it (headless smoke test).\nconst session = await cube.tools.knife({ snapMode: 'edge' });\nawait(session).cancel();\n// 3) Log a verifiable property.\nconsole.log('tools demo: ok');\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshToolsApi.bisect",
      "symbol": "MeshToolsApi.bisect",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'bisect-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Open a bisect session with full opts coverage.\nconst session = await cube.tools.bisect({\n    axis: 'z',\n    offset: 0.25,\n    fill: true,\n    clearInner: false,\n    clearOuter: false,\n});\nawait(session).cancel();\n// 3) Log a verifiable property.\nconsole.log('bisect session opened and cancelled');\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshToolsApi.buildOnNormal",
      "symbol": "MeshToolsApi.buildOnNormal",
      "intent": "scene",
      "code": "// 1) Build a plane off-origin to project the cube onto.\nconst plane = await create.plane({ width: 4, height: 4, name: 'bon-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Place a cube on the plane's surface headlessly (position + normal both supplied).\nconst result = await plane.tools.buildOnNormal({\n    primitiveType: 'cube',\n    size: 0.5,\n    depth: 0.5,\n    surfaceOffset: 0.0,\n    position: [2.5, 1.3, -0.7],\n    normal: [0, 1, 0],\n});\n// 3) Log a verifiable property.\nconsole.log('buildOnNormal returned: ' + (result !== undefined));\n// 4) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshToolsApi.example",
      "symbol": "MeshToolsApi.example",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'tools-example' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('tools example length: ' + cube.tools.example().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshToolsApi.fillHole",
      "symbol": "MeshToolsApi.fillHole",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'fillhole-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Open a fill-hole session passing the option.\nconst session = await cube.tools.fillHole({\n    boundaryFromComponent: { edgeIndices: [] },\n});\nawait(session).cancel();\n// 3) Log a verifiable property.\nconsole.log('fillHole session opened and cancelled');\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshToolsApi.help",
      "symbol": "MeshToolsApi.help",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'tools-help' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('tools help length: ' + cube.tools.help().length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshToolsApi.knife",
      "symbol": "MeshToolsApi.knife",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'knife-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Open and cancel a knife session.\nconst session = await cube.tools.knife({ snapMode: 'edge' });\nawait(session).cancel();\n// 3) Log a verifiable property.\nconsole.log('knife session opened and cancelled');\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshToolsApi.polyDraw",
      "symbol": "MeshToolsApi.polyDraw",
      "intent": "scene",
      "code": "// 1) Build a plane off-origin.\nconst plane = await create.plane({ width: 2, height: 2, name: 'polydraw-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Open and cancel a poly-draw session.\nconst session = await plane.tools.polyDraw();\nawait(session).cancel();\n// 3) Log a verifiable property.\nconsole.log('polyDraw session opened and cancelled');\n// 4) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshToolsApi.polyPen",
      "symbol": "MeshToolsApi.polyPen",
      "intent": "scene",
      "code": "// 1) Build a plane off-origin to drape geometry on.\nconst plane = await create.plane({ width: 2, height: 2, name: 'polypen-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Open a poly-pen session with full opts coverage.\nconst session = await plane.tools.polyPen({\n    mode: 'quad',\n    symmetry: null,\n    worldCoords: true,\n});\nawait(session).cancel();\n// 3) Log a verifiable property.\nconsole.log('polyPen session opened and cancelled');\n// 4) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshToolsApi.sculpt",
      "symbol": "MeshToolsApi.sculpt",
      "intent": "scene",
      "code": "// 1) Build a sphere off-origin.\nconst sphere = await create.sphere({ radius: 1, name: 'sculpt-demo' });\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Enter sculpt with full opts coverage.\nconst session = await sphere.tools.sculpt({\n    brushType: 'clay',\n    radius: 0.1,\n    strength: 0.5,\n});\nawait(session).cancel();\n// 3) Log a verifiable property.\nconsole.log('sculpt session opened and cancelled');\n// 4) Clean up.\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshToolsApi.slice",
      "symbol": "MeshToolsApi.slice",
      "intent": "scene",
      "code": "// 1) Build a cube off-origin.\nconst cube = await create.cube({ name: 'slice-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Open a plane-mode slice session.\nconst session = await cube.tools.slice({\n    mode: 'plane',\n    axis: 'y',\n    offset: 0.5,\n    fill: false,\n});\nawait(session).cancel();\n// 3) Log a verifiable property.\nconsole.log('slice session opened and cancelled');\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.addSet",
      "symbol": "MeshUvApi.addSet",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-addset' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst idx = await cube.uv.addSet('lightmap');\nconsole.log('new uv set idx: ' + idx);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.align",
      "symbol": "MeshUvApi.align",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-align' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\nawait cube.uv.align({ axis: 'u', mode: 'center', setIdx: 0 });\nconsole.log('after align islands: ' + cube.uv.islands.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.averageScale",
      "symbol": "MeshUvApi.averageScale",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-avgscale' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\nawait cube.uv.averageScale({ setIdx: 0 });\nconsole.log('after averageScale islands: ' + cube.uv.islands.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.clear",
      "symbol": "MeshUvApi.clear",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-clear' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.clear(0);\nconsole.log('loop 0 u after clear: ' + cube.uv.getOne(0)[0]);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.clearSeam",
      "symbol": "MeshUvApi.clearSeam",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-clearseam' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.markSeam({ edges: [0] });\nawait cube.uv.clearSeam({ edges: [0], setIdx: 0 });\nconsole.log('seam cleared: ' + !cube.uv.loopSeam().some(Boolean));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.closest",
      "symbol": "MeshUvApi.closest",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-closest' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\nconst near = cube.uv.closest([0.5, 0.5]);\nconsole.log('closest loop: ' + (near ? near.loopIndex : 'null'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.copySet",
      "symbol": "MeshUvApi.copySet",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-copyset' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst dstIdx = await cube.uv.copySet(0, 'backup');\nconsole.log('copy idx: ' + dstIdx);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.count",
      "symbol": "MeshUvApi.count",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-count' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('loop count: ' + cube.uv.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.currentSet",
      "symbol": "MeshUvApi.currentSet",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-current' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('current uv set: ' + cube.uv.currentSet);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.example",
      "symbol": "MeshUvApi.example",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-example' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log(cube.uv.example());\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.exportLayout",
      "symbol": "MeshUvApi.exportLayout",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-export' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\nawait cube.uv.exportLayout({ setIdx: 0 });\nconsole.log('export triggered');\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.face",
      "symbol": "MeshUvApi.face",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-face' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('loop 0 face: ' + cube.uv.face(0, 0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.facePin",
      "symbol": "MeshUvApi.facePin",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-facepin' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.pin({ loops: cube.uv.loopsForFace(0) });\nconsole.log('face 0 pinned: ' + cube.uv.facePin(0)[0]);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.findSet",
      "symbol": "MeshUvApi.findSet",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-findset' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('UVMap idx: ' + cube.uv.findSet('UVMap'));\nconsole.log('missing idx: ' + cube.uv.findSet('does-not-exist'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.get",
      "symbol": "MeshUvApi.get",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off origin.\nconst cube = await create.cube({ name: 'uv-get' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the active channel, then read by name.\nconst flat = cube.uv.get();\nconst byName = cube.uv.get('UVMap');\n// 3) Verify length is 2 * loop count on both reads.\nconsole.log('uv buffer stride 2: ' + (flat.length === cube.uv.count * 2));\nconsole.log('byName length match: ' + (byName.length === flat.length));\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.getOne",
      "symbol": "MeshUvApi.getOne",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-getone' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst uv = cube.uv.getOne(0, 0);\nconsole.log('loop 0 uv: [' + uv[0] + ',' + uv[1] + ']');\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.getSet",
      "symbol": "MeshUvApi.getSet",
      "intent": "scene",
      "code": "// 1. Setup: a fresh cube auto-creates a 'UVMap' channel.\nconst cube = await create.cube({ name: 'uv-getset' });\n// 2. Select: none — getSet resolves a channel by name, not by selection.\n// 3. Action: resolve the channel index, THROWING if the name is absent.\nconst idx = cube.uv.getSet('UVMap');\n// 4. Observe: a valid index (never the -1 sentinel that findSet returns).\nconsole.log('UVMap channel index: ' + idx);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.help",
      "symbol": "MeshUvApi.help",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-help' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log(cube.uv.help());\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.island",
      "symbol": "MeshUvApi.island",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-island' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\nconst isl = cube.uv.island(0);\nconsole.log('loop 0 island: ' + (isl ? isl.index : 'null'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.islandAt",
      "symbol": "MeshUvApi.islandAt",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-islandat' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\nconst hit = cube.uv.islandAt([0.1, 0.1]);\nconsole.log('islandAt hit: ' + (hit ? 'yes' : 'no'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.islands",
      "symbol": "MeshUvApi.islands",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-islands' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\nconsole.log('island count: ' + cube.uv.islands.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.isPinned",
      "symbol": "MeshUvApi.isPinned",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-ispinned' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.pin({ loops: [0] });\nconsole.log('loop 0 pinned: ' + cube.uv.isPinned(0, 0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.loopPin",
      "symbol": "MeshUvApi.loopPin",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-looppin' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.pin({ loops: [0] });\nconsole.log('loop 0 pinned: ' + cube.uv.loopPin(0)[0]);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.loopSeam",
      "symbol": "MeshUvApi.loopSeam",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-loopseam' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.markSeam({ edges: [0] });\nconst seams = cube.uv.loopSeam();\nconsole.log('any seam loop: ' + seams.some(Boolean));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.loopsForFace",
      "symbol": "MeshUvApi.loopsForFace",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-loopsforface' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('face 0 loops: ' + cube.uv.loopsForFace(0).join(','));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.markSeam",
      "symbol": "MeshUvApi.markSeam",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-markseam' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.markSeam({ edges: [0, 1, 2], setIdx: 0 });\nconsole.log('any seam loop: ' + cube.uv.loopSeam().some(Boolean));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.mergeByDistance",
      "symbol": "MeshUvApi.mergeByDistance",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-mbd' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\n// The merge runs over the given UV loops (no live editor selection needed\n// in a script). Pass at least two corner indices on channel 0.\nawait cube.uv.mergeByDistance({ loops: [0, 1, 2, 3], threshold: 0.01, setIdx: 0 });\nconsole.log('after mergeByDistance islands: ' + cube.uv.islands.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.mirror",
      "symbol": "MeshUvApi.mirror",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-mirror' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\nawait cube.uv.mirror({ axis: 'u', setIdx: 0 });\nconsole.log('after mirror loop 0 u: ' + cube.uv.getOne(0)[0]);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.numSets",
      "symbol": "MeshUvApi.numSets",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-numsets' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.addSet('lightmap');\nconsole.log('numSets: ' + cube.uv.numSets);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.pack",
      "symbol": "MeshUvApi.pack",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-pack' });\nawait cube.uv.unwrap({ strategy: 'smartProject' });\nawait cube.uv.pack({\n    margin: 0.01, // padding between islands in [0,1] UV space\n    setIdx: 0, // target the first UV channel\n});\nconsole.log('after pack islands: ' + cube.uv.islands.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.pin",
      "symbol": "MeshUvApi.pin",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-pin' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.pin({ loops: [0, 1], setIdx: 0 });\nconsole.log('loop 0 pinned: ' + cube.uv.isPinned(0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.project",
      "symbol": "MeshUvApi.project",
      "intent": "scene",
      "code": "// Cube box-projection on a cube → 6 axis-aligned faces.\nconst c = await create.cube({ name: 'uv-proj-cube' });\nawait c.uv.project({ method: 'cube', setIdx: 0 });\nconsole.log('cube islands: ' + c.uv.islands.length);\n// Cylindrical projection around Y.\nconst cyl = await create.cylinder({ name: 'uv-proj-cyl' });\nawait cyl.uv.project({ method: 'cylinder', axis: 'y' });\n// Smart angle-based projection with explicit tunables.\nconst s = await create.sphere({ name: 'uv-proj-smart' });\nawait s.uv.project({\n    method: 'smartUV',\n    angleLimit: 66, // max angle (deg) between normals per island\n    areaWeight: 0, // 0 = equal-weight group normals\n    packMargin: 0.002, // gap between islands in [0,1] UV space\n});\n// Planar projection restricted to a subset of faces.\nawait c.uv.project({ method: 'planar', axis: 'z', faces: [0, 1] });\nawait c.delete();\nawait cyl.delete();\nawait s.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.relax",
      "symbol": "MeshUvApi.relax",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-relax' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\nawait cube.uv.relax({ iterations: 5, factor: 0.3, setIdx: 0 });\nconsole.log('after relax islands: ' + cube.uv.islands.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.removeSet",
      "symbol": "MeshUvApi.removeSet",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-removeset' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// Create the named channel, then let the append settle before removing\n// it by name (the add lands on the next task tick).\nawait cube.uv.addSet('lightmap');\nawait new Promise((resolve) => setTimeout(resolve, 0));\nawait cube.uv.removeSet('lightmap');\nconsole.log('after remove numSets: ' + cube.uv.numSets);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.renameSet",
      "symbol": "MeshUvApi.renameSet",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-renameset' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.renameSet(0, 'main');\nconsole.log('renamed: ' + cube.uv.setNames[0]);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.seamsFromIslands",
      "symbol": "MeshUvApi.seamsFromIslands",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-sfi' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\nawait cube.uv.seamsFromIslands({ setIdx: 0 });\nconsole.log('any seam after sfi: ' + cube.uv.loopSeam().some(Boolean));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.set",
      "symbol": "MeshUvApi.set",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off origin.\nconst cube = await create.cube({ name: 'uv-set' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Build a uniform grid and write it to channel 0.\nconst total = cube.uv.count;\nconst grid = new Float32Array(total * 2);\nfor (let i = 0; i < total; i++) {\n    grid[i * 2] = (i % 4) * 0.25;\n    grid[i * 2 + 1] = Math.floor(i / 4) % 4 * 0.25;\n}\ncube.uv.set(grid, 0);\n// 3) Verify the buffer round-trips.\nconsole.log('after set, first u: ' + cube.uv.get()[0]);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.setActive",
      "symbol": "MeshUvApi.setActive",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-setactive' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// Create the named channel, then let the append settle before activating\n// it by name (the add lands on the next task tick).\nawait cube.uv.addSet('lightmap');\nawait new Promise((resolve) => setTimeout(resolve, 0));\nawait cube.uv.setActive('lightmap');\nconsole.log('active: ' + cube.uv.currentSet);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.setNames",
      "symbol": "MeshUvApi.setNames",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-setnames' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.addSet('lightmap');\nconsole.log('uv channels: ' + cube.uv.setNames.join(','));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.setOne",
      "symbol": "MeshUvApi.setOne",
      "intent": "scene",
      "code": "// 1) Build a cube and translate off origin.\nconst cube = await create.cube({ name: 'uv-setone' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Write the first loop's UV to (0.5, 0.5) on channel 0 (a fresh cube\n//    ships with exactly one UV set, so set index 0 is the only valid one).\ncube.uv.setOne(0, [0.5, 0.5], 0);\n// 3) Verify the read-back. getOne returns a Vec2 — read .x / .y (it is NOT\n//    array-indexable).\nconsole.log('loop 0 u: ' + cube.uv.getOne(0, 0).x);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.setSome",
      "symbol": "MeshUvApi.setSome",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-setsome' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// A fresh cube ships with exactly one UV set, so set index 0 is the only\n// valid one. getOne returns a Vec2 — read .x / .y (it is NOT array-indexable).\nawait cube.uv.setSome([0, 1], [[0.1, 0.1], [0.9, 0.9]], 0);\nconsole.log('loop 0 u after setSome: ' + cube.uv.getOne(0, 0).x);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.stitch",
      "symbol": "MeshUvApi.stitch",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-stitch' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\nawait cube.uv.stitch({ loops: [0, 1], setIdx: 0 });\nconsole.log('after stitch islands: ' + cube.uv.islands.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.transferFrom",
      "symbol": "MeshUvApi.transferFrom",
      "intent": "scene",
      "code": "const src = await create.cube({ name: 'uv-src' });\nsrc.xform({ t: [2.5, 1.3, -0.7] });\nawait src.uv.unwrap({ method: 'angle-based' });\nconst dst = await create.cube({ name: 'uv-dst' });\ndst.xform({ t: [-2.5, 1.3, -0.7] });\nawait dst.uv.transferFrom(src, { setIdx: 0 });\nconsole.log('dst islands after transfer: ' + dst.uv.islands.length);\nawait src.delete();\nawait dst.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.unpin",
      "symbol": "MeshUvApi.unpin",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-unpin' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.pin({ loops: [0, 1] });\nawait cube.uv.unpin({ loops: [0, 1], setIdx: 0 });\nconsole.log('loop 0 still pinned: ' + cube.uv.isPinned(0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.unwrap",
      "symbol": "MeshUvApi.unwrap",
      "intent": "scene",
      "code": "// Default smart projection — a cube → 6 clean, zero-distortion squares.\nconst cube = await create.cube({ name: 'uv-unwrap' });\nawait cube.uv.unwrap({\n    strategy: 'smartProject', // group-by-normal → planar-project → box-pack\n    angleLimit: 66, // max angle (deg) between normals per island\n    areaWeight: 0, // 0 = equal-weight group normals\n    packMargin: 0.002, // gap between islands in [0,1] UV space\n    setIdx: 0, // target the first UV channel\n});\nconsole.log('smart islands: ' + cube.uv.islands.length);\n// Chart pipeline alternative (organic meshes / seam-driven unwraps).\nconst sph = await create.sphere({ name: 'uv-pipeline' });\nawait sph.uv.unwrap({\n    strategy: 'pipeline',\n    mode: 'auto', // 'auto' = generate seams, 'unfold' = reuse them\n    segmenter: 'dcharts', // 'dcharts' | 'skeleton' | 'seamster'\n    flattener: 'abf', // 'abf' | 'lscm' | 'slim' | 'planar'\n    equalizeScale: true, // even texel density across islands\n});\nconsole.log('pipeline islands: ' + sph.uv.islands.length);\nawait cube.delete();\nawait sph.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.vertex",
      "symbol": "MeshUvApi.vertex",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-vertex' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('loop 0 vertex: ' + cube.uv.vertex(0, 0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshUvApi.weld",
      "symbol": "MeshUvApi.weld",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-weld' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.uv.unwrap({ method: 'angle-based' });\nawait cube.uv.weld({ loops: [0, 1], setIdx: 0 });\nconsole.log('after weld islands: ' + cube.uv.islands.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshValidateDialogProbe.activeMeshId",
      "symbol": "MeshValidateDialogProbe.activeMeshId",
      "intent": "scene",
      "code": "// Read which mesh the repair popup is operating on.\nconst meshId = dev.dialogs.meshValidate.activeMeshId;\nconsole.log('repair target mesh: ' + meshId);"
    },
    {
      "kind": "example",
      "id": "example:MeshValidateDialogProbe.isActive",
      "symbol": "MeshValidateDialogProbe.isActive",
      "intent": "scene",
      "code": "// Check whether the Validate & Repair popup is currently open.\nconst active = dev.dialogs.meshValidate.isActive;\nconsole.log('repair popup active: ' + active);"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.basePositions",
      "symbol": "MeshVertsApi.basePositions",
      "intent": "scene",
      "code": "// 1. Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'base-positions' });\n// 2. No selection needed — this is a whole-mesh bulk read.\n// 3. Read the always-undeformed base buffer.\nconst base = cube.verts.basePositions;\n// 4. Observe stride-3 layout (equals `positions` when no deformer is active).\nconsole.log('base stride 3: ' + (base.length % 3 === 0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.bend",
      "symbol": "MeshVertsApi.bend",
      "intent": "scene",
      "code": "// 1) Build a tall cube.\nconst cube = await create.cube({ width: 1, height: 3, depth: 1, name: 'verts-bend' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Bend the top half 45 DEGREES around the Y axis.\nawait cube.verts.bend({ indices: [4, 5, 6, 7], angle: 45, axis: 'y' });\nconsole.log('vert count: ' + cube.verts.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.bevel",
      "symbol": "MeshVertsApi.bevel",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-bevel' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Bevel the first 4 vertices with width 0.2 (vertex bevel is single-segment).\nconst before = cube.verts.count;\nawait cube.verts.bevel({ indices: [0, 1, 2, 3], width: 0.2 });\nconsole.log('vert count grew: ' + (cube.verts.count > before));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.collapse",
      "symbol": "MeshVertsApi.collapse",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-collapse' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.verts.count;\n// 2) Collapse vertex 0.\nawait cube.verts.collapse({ indices: [0] });\nconsole.log('vert count dropped: ' + (cube.verts.count < before));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.color",
      "symbol": "MeshVertsApi.color",
      "intent": "scene",
      "code": "// 1) Build a cube and paint vertex 0 magenta.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-color' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.verts.setColor(0, [1, 0, 1, 1]);\n// 2) Read it back.\nconst c = cube.verts.color(0);\nconsole.log('vertex 0 red: ' + c.x);\nconsole.log('vertex 0 blue: ' + c.z);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.colors",
      "symbol": "MeshVertsApi.colors",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-colors' });\nconsole.log('has per-vertex color: ' + (cube.verts.colors !== null));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.count",
      "symbol": "MeshVertsApi.count",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-count' });\nconsole.log('vertex count: ' + cube.verts.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.customNormals",
      "symbol": "MeshVertsApi.customNormals",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-customNormals' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Lock vertex 0 smoothed normal as custom.\nawait cube.verts.customNormals({ indices: [0] });\nconsole.log('vert 0 normal y: ' + cube.verts.normal(0).y);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.delete",
      "symbol": "MeshVertsApi.delete",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-delete' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('verts before: ' + cube.verts.count);\nconsole.log('faces before: ' + cube.faces.count);\n// 2) Delete vertex 0. Three incident faces vanish too.\nawait cube.verts.delete({ indices: [0] });\nconsole.log('verts after: ' + cube.verts.count);\nconsole.log('faces after: ' + cube.faces.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.description",
      "symbol": "MeshVertsApi.description",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-description' });\nconsole.log(cube.verts.description());\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.dissolve",
      "symbol": "MeshVertsApi.dissolve",
      "intent": "scene",
      "code": "// 1) Build a subdivided cube so there is an interior vertex to dissolve.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n    name: 'verts-dissolve',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.verts.count;\n// 2) Dissolve vertex 0.\nawait cube.verts.dissolve({ indices: [0] });\nconsole.log('vert count dropped: ' + (cube.verts.count < before));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.edges",
      "symbol": "MeshVertsApi.edges",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-edges' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('vertex 0 edge count: ' + cube.verts.edges(0).length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.example",
      "symbol": "MeshVertsApi.example",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-example' });\nconsole.log(cube.verts.example());\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.extrude",
      "symbol": "MeshVertsApi.extrude",
      "intent": "scene",
      "code": "// NOTE: per-vertex extrude is an interactive-only tool; its headless\n// operator is not yet wired (mesh.extrudeVertices is a throw-stub). The\n// runnable equivalent is FACE extrude — it grows new geometry the same\n// way and adds the extra vertices a vertex-extrude would have produced.\n// 1) Build a plane (open boundary so the extrusion is visible).\nconst plane = await create.plane({ width: 1, height: 1, name: 'verts-extrude' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\nconst before = plane.verts.count;\n// 2) Extrude the plane's single face outward by 0.25 — this adds the\n//    new ring of vertices a vertex-extrude would have produced.\nawait plane.faces([0]).extrude({ distance: 0.25 });\nconsole.log('vert count grew: ' + (plane.verts.count > before));\n// 3) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.faces",
      "symbol": "MeshVertsApi.faces",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-faces' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('vertex 0 face count: ' + cube.verts.faces(0).length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.flatten",
      "symbol": "MeshVertsApi.flatten",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-flatten' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Flatten the top 4 verts onto the world Y plane.\nawait cube.verts.flatten({ indices: [4, 5, 6, 7], plane: 'y' });\nconsole.log('top vert y: ' + cube.verts.position(4).y);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.forEach",
      "symbol": "MeshVertsApi.forEach",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-forEach' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Sum every vertex X position.\nlet sumX = 0;\ncube.verts.forEach((i, total) => { sumX += cube.verts.position(i).x; });\nconsole.log('sum of vertex X: ' + sumX);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.getBasePositions",
      "symbol": "MeshVertsApi.getBasePositions",
      "intent": "scene",
      "code": "// 1. Build a cube and offset its transform.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'get-base-positions' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. No selection needed — this is a whole-mesh bulk read.\n// 3. Read the undeformed base buffer in world space.\nconst world = cube.verts.getBasePositions({ space: 'world' });\n// 4. Observe the snapshot length is 3 * vertex count.\nconsole.log('base world stride 3: ' + (world.length % 3 === 0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.getPositions",
      "symbol": "MeshVertsApi.getPositions",
      "intent": "scene",
      "code": "// 1) Build a cube and offset its transform.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'getPositions' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read positions in both spaces.\nconst local = cube.verts.getPositions({ space: 'object' });\nconst world = cube.verts.getPositions({ space: 'world' });\nconsole.log('local x0: ' + local[0]);\nconsole.log('world x0: ' + world[0]);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.help",
      "symbol": "MeshVertsApi.help",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-help' });\nconsole.log(cube.verts.help());\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.isBoundary",
      "symbol": "MeshVertsApi.isBoundary",
      "intent": "scene",
      "code": "// 1) A plane has 4 boundary corners; an enclosed cube has none.\nconst plane = await create.plane({ width: 1, height: 1, name: 'vtx-isBoundary' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('plane vertex 0 on boundary: ' + plane.verts.isBoundary(0));\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.map",
      "symbol": "MeshVertsApi.map",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-map' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Push every vertex 0.1 along X.\nawait cube.verts.map((i, total) => {\n    const p = cube.verts.position(i);\n    return [p.x + 0.1, p.y, p.z];\n});\nconsole.log('vert 0 x after map: ' + cube.verts.position(0).x);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.merge",
      "symbol": "MeshVertsApi.merge",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-merge' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.verts.count;\n// 2) Weld vertices 0 and 1 together — they collapse to their midpoint.\nawait cube.verts.merge({ indices: [0, 1] });\nconsole.log('vert count dropped: ' + (cube.verts.count < before));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.nearest",
      "symbol": "MeshVertsApi.nearest",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-nearest' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Find the 3 verts nearest the origin.\nconst hits = cube.verts.nearest([0, 0, 0], 3);\nconsole.log('nearest 3 verts: ' + hits.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.neighbors",
      "symbol": "MeshVertsApi.neighbors",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-neighbors' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read the ring-1 neighbors of vertex 0.\nconst ring = cube.verts.neighbors(0);\nconsole.log('neighbor count: ' + ring.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.normal",
      "symbol": "MeshVertsApi.normal",
      "intent": "scene",
      "code": "// 1) Spawn a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-normal' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read both flavors.\nconst plain = cube.verts.normal(0);\nconst weighted = cube.verts.normal(0, { angleWeighted: true });\nconsole.log('plain normal x: ' + plain.x);\nconsole.log('weighted normal x: ' + weighted.x);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.normals",
      "symbol": "MeshVertsApi.normals",
      "intent": "scene",
      "code": "// 1. Build a cube (its per-vertex normals are computed on creation).\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-normals' });\n// 2. No selection needed — this is a whole-mesh bulk read.\n// 3. Read the flat per-vertex normals buffer.\nconst normals = cube.verts.normals;\n// 4. Observe stride-3 layout + that it matches `mesh.normals.get()`.\nconsole.log('normal stride 3: ' + (normals.length % 3 === 0));\nconsole.log('matches normals.get(): ' + (normals.length === cube.normals.get().length));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.numConnectedEdges",
      "symbol": "MeshVertsApi.numConnectedEdges",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-edgeValence' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('vertex 0 edge valence: ' + cube.verts.numConnectedEdges(0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.numConnectedFaces",
      "symbol": "MeshVertsApi.numConnectedFaces",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-faceValence' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('vertex 0 face valence: ' + cube.verts.numConnectedFaces(0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.position",
      "symbol": "MeshVertsApi.position",
      "intent": "scene",
      "code": "// 1) Spawn a cube off-origin.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-position' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Read vertex 0.\nconst p = cube.verts.position(0);\nconsole.log('vertex position x: ' + p.x);\nconsole.log('vertex position y: ' + p.y);\nconsole.log('vertex position z: ' + p.z);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.positions",
      "symbol": "MeshVertsApi.positions",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-positions' });\nconsole.log('first x: ' + cube.verts.positions[0]);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.ring",
      "symbol": "MeshVertsApi.ring",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-ring' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Walk out 1, 2, 3 hops.\nconsole.log('ring 1: ' + cube.verts.ring(0, 1).length);\nconsole.log('ring 2: ' + cube.verts.ring(0, 2).length);\nconsole.log('ring 3: ' + cube.verts.ring(0, 3).length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.rip",
      "symbol": "MeshVertsApi.rip",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-rip' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst before = cube.verts.count;\n// 2) Rip vertex 0.\nawait cube.verts.rip({ indices: [0] });\nconsole.log('vert count grew: ' + (cube.verts.count > before));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.setColor",
      "symbol": "MeshVertsApi.setColor",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'setColor' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Paint vertex 0 magenta.\nawait cube.verts.setColor(0, [1, 0, 1, 1]);\nconsole.log('vertex 0 red: ' + cube.verts.color(0).x);\nconsole.log('vertex 0 blue: ' + cube.verts.color(0).z);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.setColors",
      "symbol": "MeshVertsApi.setColors",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'setColors' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Paint every vertex solid red.\nconst rgba = new Float32Array(cube.verts.count * 4);\nfor (let i = 0; i < cube.verts.count; i++) {\n    rgba[i * 4 + 0] = 1;\n    rgba[i * 4 + 1] = 0;\n    rgba[i * 4 + 2] = 0;\n    rgba[i * 4 + 3] = 1;\n}\ncube.verts.setColors(rgba);\nconsole.log('first vertex red channel: ' + cube.verts.color(0).x);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.setNormal",
      "symbol": "MeshVertsApi.setNormal",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'setNormal' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Force vertex 0's normal to +Y.\nawait cube.verts.setNormal(0, [0, 1, 0]);\nconsole.log('vert 0 normal y: ' + cube.verts.normal(0).y);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.setPosition",
      "symbol": "MeshVertsApi.setPosition",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'setPosition' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Absolute write in object space.\nawait cube.verts.setPosition(0, [1, 0, 0], { space: 'object', relative: false });\n// 3) Relative nudge in world space.\nawait cube.verts.setPosition(0, [0.1, 0, 0], { space: 'world', relative: true });\nconsole.log('vert 0 x: ' + cube.verts.position(0).x);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.setPositions",
      "symbol": "MeshVertsApi.setPositions",
      "intent": "scene",
      "code": "// 1) Build a cube and move it.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'setPositions' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Shape 1: flat-buffer overwrite — push every vertex up by 0.1.\nconst flat = cube.verts.getPositions({ space: 'object' });\nfor (let i = 1; i < flat.length; i += 3)\n    flat[i] += 0.1;\nawait cube.verts.setPositions(flat);\n// 3) Shape 2: index-based write covering verts 0 and 1.\nawait cube.verts.setPositions([0, 1], [[0, 0.5, 0], [0.5, 0.5, 0]], { space: 'object' });\n// 4) Verify and clean up.\nconsole.log('vert 0 y: ' + cube.verts.position(0).y);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.shear",
      "symbol": "MeshVertsApi.shear",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-shear' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Shear the top 4 verts along X by 0.4.\nawait cube.verts.shear({ indices: [4, 5, 6, 7], amount: 0.4, axis: 'x' });\nconsole.log('top vert x: ' + cube.verts.position(4).x);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.shrinkFatten",
      "symbol": "MeshVertsApi.shrinkFatten",
      "intent": "scene",
      "code": "// 1) Build a sphere.\nconst sphere = await create.sphere({ radius: 1, widthSegments: 16, heightSegments: 12, name: 'verts-shrinkFatten' });\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Inflate every vertex by 0.25.\nawait sphere.verts.shrinkFatten({ indices: [0, 1, 2, 3, 4, 5], offset: 0.25 });\nconsole.log('vert count: ' + sphere.verts.count);\n// 3) Clean up.\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.slide",
      "symbol": "MeshVertsApi.slide",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-slide' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Slide the first 4 vertices halfway toward their neighbors.\nawait cube.verts.slide({ indices: [0, 1, 2, 3], factor: 0.5 });\nconsole.log('vert count: ' + cube.verts.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.smooth",
      "symbol": "MeshVertsApi.smooth",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-smooth' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Smooth just the top face verts (indices 4-7 on a default cube).\nawait cube.verts.smooth({ indices: [4, 5, 6, 7], iterations: 3, strength: 0.5 });\nconsole.log('after smooth vert count: ' + cube.verts.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.tangent",
      "symbol": "MeshVertsApi.tangent",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-tangent' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst t = cube.verts.tangent(0);\nconsole.log('tangent x: ' + t.x);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.tangents",
      "symbol": "MeshVertsApi.tangents",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-tangents' });\nconsole.log('tangent stride 4: ' + (cube.verts.tangents.length % 4 === 0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.toEdges",
      "symbol": "MeshVertsApi.toEdges",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-toEdges' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Single-vertex overload.\nconst single = cube.verts.toEdges(0);\nconsole.log('edges of vertex 0: ' + single.length);\n// 3) Multi-vertex overload.\nconst many = cube.verts.toEdges([0, 1, 2]);\nconsole.log('edges of verts 0,1,2: ' + many.length);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.toFaces",
      "symbol": "MeshVertsApi.toFaces",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-toFaces' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Single-vertex overload.\nconst single = cube.verts.toFaces(0);\nconsole.log('faces of vertex 0: ' + single.length);\n// 3) Multi-vertex overload.\nconst many = cube.verts.toFaces([0, 1, 2]);\nconsole.log('faces of verts 0,1,2: ' + many.length);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.toIslands",
      "symbol": "MeshVertsApi.toIslands",
      "intent": "scene",
      "code": "// 1) Build a cube (default UV unwrap produces 6 islands).\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-toIslands' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Single-vertex overload.\nconst single = cube.verts.toIslands(0);\nconsole.log('islands of vertex 0: ' + single.length);\n// 3) Multi-vertex overload.\nconst many = cube.verts.toIslands([0, 1, 2]);\nconsole.log('islands of verts 0,1,2: ' + many.length);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.toSphere",
      "symbol": "MeshVertsApi.toSphere",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-toSphere' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Halfway-spherify the first 4 verts.\nawait cube.verts.toSphere({ indices: [0, 1, 2, 3], factor: 0.5 });\nconsole.log('vert count: ' + cube.verts.count);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.toUVs",
      "symbol": "MeshVertsApi.toUVs",
      "intent": "scene",
      "code": "// 1) Build a cube (default seams).\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'vtx-toUVs' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Single-vertex overload.\nconst single = cube.verts.toUVs(0);\nconsole.log('UVs of vertex 0: ' + single.length);\n// 3) Multi-vertex overload.\nconst many = cube.verts.toUVs([0, 1, 2]);\nconsole.log('UVs of verts 0,1,2: ' + many.length);\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.where",
      "symbol": "MeshVertsApi.where",
      "intent": "scene",
      "code": "// 1) Build a cube; cube has 8 vertices laid out at +/-0.5 corners.\nconst cube = await create.cube({ name: 'verts-where-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) No-select-needed — where IS the select.\n// 3) Filter to the 4 vertices on the top face (y > 0).\nconst top = cube.verts.where((v) => v.position.y > 0);\n// 4) Log the count and clean up.\nconsole.log('top verts: ' + top.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.within",
      "symbol": "MeshVertsApi.within",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-within' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Every vertex within 0.6 of the origin.\nconst hits = cube.verts.within([0, 0, 0], 0.6);\nconsole.log('verts within 0.6 of origin: ' + hits.length);\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:MeshVertsApi.xform",
      "symbol": "MeshVertsApi.xform",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'verts-xform' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Nudge vertex 0 by 0.25 in object space.\nconst before = cube.verts.position(0);\nawait cube.verts.xform(0, { t: [0.25, 0, 0], space: 'object', relative: true });\nconst after = cube.verts.position(0);\nconsole.log('delta x near 0.25: ' + (Math.abs(after.x - before.x - 0.25) < 1e-6));\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelBarrelResidualApi.getMaterial",
      "symbol": "ModelBarrelResidualApi.getMaterial",
      "intent": "scene",
      "code": "// 1. Create a named material so there is something to look up.\nconst made = await create.material({ name: 'getmat-demo', baseColor: [0.8, 0.2, 0.4, 1] });\nconsole.log('created: ' + made.name);\n// 2. Fetch that same material back BY NAME.\nconst mat = getMaterial('getmat-demo');\nconsole.log('found: ' + (mat !== null));\n// 3. Read a property off the returned handle.\nconsole.log('baseColor[0] = ' + (mat ? mat.baseColor[0] : 'n/a'));\n// 4. A name that does not exist returns null.\nconsole.log('missing is null: ' + (getMaterial('no-such-material') === null));"
    },
    {
      "kind": "example",
      "id": "example:ModelBarrelResidualApi.history",
      "symbol": "ModelBarrelResidualApi.history",
      "intent": "scene",
      "code": "// 1. Read the current undo-stack depth.\nconsole.log('size = ' + history.size());\n// 2. Query whether an undo is available right now.\nconsole.log('canUndo = ' + history.canUndo());\n// 3. Undo the most recent command when there is one.\nif (history.canUndo())\n    await history.undo();\n// 4. Redo it back so the scene is left unchanged.\nif (history.canRedo())\n    await history.redo();"
    },
    {
      "kind": "example",
      "id": "example:ModelBarrelResidualApi.listMaterials",
      "symbol": "ModelBarrelResidualApi.listMaterials",
      "intent": "scene",
      "code": "// 1. Snapshot the count before adding anything.\nconst before = listMaterials().length;\nconsole.log('before = ' + before);\n// 2. Create a material to grow the set.\nawait create.material({ name: 'listmat-demo' });\nconsole.log('added listmat-demo');\n// 3. The list now reports at least one more entry.\nconst after = listMaterials().length;\nconsole.log('after = ' + after);\n// 4. Read a name off the first DTO in the list.\nconsole.log('first name = ' + (after > 0 ? listMaterials()[0].name : 'none'));"
    },
    {
      "kind": "example",
      "id": "example:ModelBarrelResidualApi.scene",
      "symbol": "ModelBarrelResidualApi.scene",
      "intent": "scene",
      "code": "// 1. List every node in the scene.\nconsole.log('node count = ' + ls().length);\n// 2. Create a node so the scene is non-empty.\nawait create.cube({ name: 'scene-accessor-demo' });\nconsole.log('created scene-accessor-demo');\n// 3. Resolve it back BY NAME through the scene accessor.\nconsole.log('found = ' + (ls('scene-accessor-demo').length === 1));\n// 4. Clean up by deleting it.\nawait ls('scene-accessor-demo')[0].delete();\nconsole.log('deleted: ' + (ls('scene-accessor-demo').length === 0));"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.arch",
      "symbol": "ModelCreateApi.arch",
      "intent": "scene",
      "code": "// 1) Build a doorway-style arch with vertical legs.\nconst arch = await create.arch({\n    outerRadius: 0.6,\n    innerRadius: 0.5,\n    depth: 0.25,\n    archAngle: Math.PI,\n    segments: 16,\n    includeLegs: true,\n    legHeight: 0.4,\n    name: 'demo-arch',\n});\n// 2) Translate off the origin.\narch.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('arch name: ' + arch.name);\nconsole.log('arch vertex count: ' + arch.verts.count);\nconsole.log('arch face count: ' + arch.faces.count);\n// 4) Cleanup.\nawait arch.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.arrow",
      "symbol": "ModelCreateApi.arrow",
      "intent": "scene",
      "code": "// 1) Build an arrow signpost.\nconst arrow = await create.arrow({\n    length: 1.2,\n    shaftWidth: 0.25,\n    headWidth: 0.5,\n    headLength: 0.4,\n    height: 0.15,\n    style: 'arrow',\n    name: 'demo-arrow',\n});\n// 2) Translate off the origin.\narrow.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('arrow name: ' + arrow.name);\nconsole.log('arrow vertex count: ' + arrow.verts.count);\nconsole.log('arrow face count: ' + arrow.faces.count);\n// 4) Cleanup.\nawait arrow.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.bolt",
      "symbol": "ModelCreateApi.bolt",
      "intent": "scene",
      "code": "// 1) Build a hollow bolt with matched thread parameters.\nconst bolt = await create.bolt({\n    outerRadius: 0.12,\n    height: 0.55,\n    boreRadius: 0.06,\n    threadPitch: 0.045,\n    threadDepth: 0.012,\n    radialSegments: 20,\n    name: 'demo-bolt',\n});\n// 2) Translate off the origin.\nbolt.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('bolt name: ' + bolt.name);\nconsole.log('bolt vertex count: ' + bolt.verts.count);\nconsole.log('bolt face count: ' + bolt.faces.count);\n// 4) Cleanup.\nawait bolt.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.boxWithLid",
      "symbol": "ModelCreateApi.boxWithLid",
      "intent": "scene",
      "code": "// 1) Build a hinged-style box with a lip lid.\nconst boxSet = await create.boxWithLid({\n    innerWidth: 1.2,\n    innerDepth: 0.8,\n    innerHeight: 0.4,\n    wallThickness: 0.06,\n    floorThickness: 0.06,\n    cornerRadius: 0.05,\n    cornerSegments: 4,\n    lidHeight: 0.06,\n    lidType: 'lip',\n    tolerance: 0.002,\n    name: 'demo-box-with-lid',\n});\n// 2) Translate off the origin.\nboxSet.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('boxWithLid name: ' + boxSet.name);\nconsole.log('boxWithLid vertex count: ' + boxSet.verts.count);\nconsole.log('boxWithLid face count: ' + boxSet.faces.count);\n// 4) Cleanup.\nawait boxSet.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.bracket",
      "symbol": "ModelCreateApi.bracket",
      "intent": "scene",
      "code": "// 1) Build a corner bracket with a cutout gusset.\nconst bracket = await create.bracket({\n    armLength1: 1.1,\n    armLength2: 0.9,\n    armWidth: 0.22,\n    thickness: 0.06,\n    gusset: true,\n    gussetStyle: 'cutout',\n    name: 'demo-bracket',\n});\n// 2) Translate off the origin.\nbracket.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('bracket name: ' + bracket.name);\nconsole.log('bracket vertex count: ' + bracket.verts.count);\nconsole.log('bracket face count: ' + bracket.faces.count);\n// 4) Cleanup.\nawait bracket.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.buildMesh",
      "symbol": "ModelCreateApi.buildMesh",
      "intent": "scene",
      "code": "// 1) Build a single quad face from raw verts + n-gon arrays. Positions\n//    are in the active grid unit (cm by default) — this 100cm quad spans\n//    1 m, matching create.cube({ width: 100 }).\nconst quad = await create.buildMesh({\n    positions: [[0, 0, 0], [100, 0, 0], [100, 100, 0], [0, 100, 0]], // cm → 1 m\n    indices: undefined,\n    faceVertexCounts: [4],\n    faceVertexIndices: [0, 1, 2, 3],\n    normals: [[0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]],\n    name: 'demo-quad',\n    material: undefined,\n});\n// 2) Translate off the origin (cm — 250cm = 2.5 m).\nquad.xform({ t: [250, 130, -70] });\n// 3) Log verifiable properties.\nconsole.log('buildMesh name: ' + quad.name);\nconsole.log('buildMesh vertex count: ' + quad.verts.count);\nconsole.log('buildMesh face count: ' + quad.faces.count);\n// 4) Cleanup.\nawait quad.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.camera",
      "symbol": "ModelCreateApi.camera",
      "intent": "scene",
      "code": "// Attempting to call camera() throws — wrap in try/catch.\ntry {\n    create.camera({ name: 'beauty-cam', focalLength: 50, nearClip: 0.1, farClip: 100 });\n}\ncatch (err) {\n    console.log('camera not yet implemented: ' + err.message);\n}"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.capsule",
      "symbol": "ModelCreateApi.capsule",
      "intent": "scene",
      "code": "// 1) Build a tall capsule.\nconst capsule = await create.capsule({\n    radius: 0.3,\n    length: 0.8,\n    radialSegments: 16,\n    capSegments: 8,\n    name: 'demo-capsule',\n});\n// 2) Translate off the origin.\ncapsule.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('capsule name: ' + capsule.name);\nconsole.log('capsule vertex count: ' + capsule.verts.count);\nconsole.log('capsule face count: ' + capsule.faces.count);\n// 4) Cleanup.\nawait capsule.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.channel",
      "symbol": "ModelCreateApi.channel",
      "intent": "scene",
      "code": "// 1) Build a U-channel beam with rounded inner corners.\nconst channel = await create.channel({\n    width: 1.2,\n    height: 0.6,\n    depth: 2,\n    wallThickness: 0.08,\n    cornerRadius: 0.05,\n    name: 'demo-channel',\n});\n// 2) Translate off the origin.\nchannel.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('channel name: ' + channel.name);\nconsole.log('channel vertex count: ' + channel.verts.count);\nconsole.log('channel face count: ' + channel.faces.count);\n// 4) Cleanup.\nawait channel.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.circle",
      "symbol": "ModelCreateApi.circle",
      "intent": "scene",
      "code": "// 1) Build a vertical circle in the XY plane.\nconst circle = await create.circle({\n    center: [0, 0.5, 0],\n    radius: 0.6,\n    normal: [0, 0, 1],\n    segments: 16,\n    name: 'demo-circle',\n});\n// 2) Translate off the origin.\ncircle.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log a verifiable property.\nconsole.log('circle name: ' + circle.name);\n// 4) Cleanup.\nawait circle.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.compartmentGrid",
      "symbol": "ModelCreateApi.compartmentGrid",
      "intent": "scene",
      "code": "// 1) Build a 3x2 compartment organizer.\nconst tray = await create.compartmentGrid({\n    width: 1.5,\n    depth: 1.0,\n    height: 0.4,\n    wallThickness: 0.04,\n    divisionsX: 3,\n    divisionsY: 2,\n    floorEnabled: true,\n    floorThickness: 0.05,\n    name: 'demo-compartment-grid',\n});\n// 2) Translate off the origin.\ntray.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('compartmentGrid name: ' + tray.name);\nconsole.log('compartmentGrid vertex count: ' + tray.verts.count);\nconsole.log('compartmentGrid face count: ' + tray.faces.count);\n// 4) Cleanup.\nawait tray.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.cone",
      "symbol": "ModelCreateApi.cone",
      "intent": "scene",
      "code": "// 1) Build a cone with a tall narrow profile and 16 radial segments.\nconst cone = await create.cone({\n    radius: 0.75,\n    height: 2.5,\n    radialSegments: 16,\n    heightSegments: 1,\n    openEnded: false,\n    name: 'demo-cone',\n});\n// 2) Translate off the origin.\ncone.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('cone name: ' + cone.name);\nconsole.log('cone vertex count: ' + cone.verts.count);\nconsole.log('cone face count: ' + cone.faces.count);\n// 4) Cleanup.\nawait cone.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.cross",
      "symbol": "ModelCreateApi.cross",
      "intent": "scene",
      "code": "// 1) Build a 4-armed cross.\nconst cross = await create.cross({\n    armLength: 0.6,\n    armWidth: 0.25,\n    height: 0.3,\n    name: 'demo-cross',\n});\n// 2) Translate off the origin.\ncross.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('cross name: ' + cross.name);\nconsole.log('cross vertex count: ' + cross.verts.count);\nconsole.log('cross face count: ' + cross.faces.count);\n// 4) Cleanup.\nawait cross.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.cube",
      "symbol": "ModelCreateApi.cube",
      "intent": "scene",
      "code": "// 1) Build a 2x1x3 cube with two subdivisions along each axis.\nconst cube = await create.cube({\n    width: 2,\n    height: 1,\n    depth: 3,\n    subdivisionsX: 2,\n    subdivisionsY: 2,\n    subdivisionsZ: 2,\n    name: 'demo-cube',\n});\n// 2) Translate off the origin so transforms, BVH, and renderer stay fresh.\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log a couple of verifiable properties.\nconsole.log('cube name: ' + cube.name);\nconsole.log('cube vertex count: ' + cube.verts.count);\nconsole.log('cube face count: ' + cube.faces.count);\n// 4) Cleanup so the scene is left as found.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.curve",
      "symbol": "ModelCreateApi.curve",
      "intent": "scene",
      "code": "// 1) Build a cubic open curve from 4 control points.\nconst curve = await create.curve({\n    points: [[0, 0, 0], [1, 1, 0], [2, 1, 0], [3, 0, 0]],\n    degree: 3,\n    closed: false,\n    name: 'demo-curve',\n});\n// 2) Translate off the origin.\ncurve.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log a verifiable property.\nconsole.log('curve name: ' + curve.name);\n// 4) Cleanup.\nawait curve.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.cylinder",
      "symbol": "ModelCreateApi.cylinder",
      "intent": "scene",
      "code": "// 1) Build a cylinder with matching top and bottom radii (a true cylinder\n//    rather than a frustum).\nconst cyl = await create.cylinder({\n    radiusTop: 0.8,\n    radiusBottom: 0.8,\n    height: 2,\n    radialSegments: 20,\n    heightSegments: 2,\n    openEnded: false,\n    name: 'demo-cylinder',\n});\n// 2) Translate off the origin.\ncyl.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('cylinder name: ' + cyl.name);\nconsole.log('cylinder vertex count: ' + cyl.verts.count);\nconsole.log('cylinder face count: ' + cyl.faces.count);\n// 4) Cleanup.\nawait cyl.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.empty",
      "symbol": "ModelCreateApi.empty",
      "intent": "scene",
      "code": "// 1) Spawn an empty mesh node.\nconst blank = await create.empty({ name: 'demo-empty' });\n// 2) Log verifiable properties.\nconsole.log('empty name: ' + blank.name);\nconsole.log('empty vertex count: ' + blank.verts.count);\nconsole.log('empty face count: ' + blank.faces.count);\n// 3) Cleanup.\nawait blank.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.example",
      "symbol": "ModelCreateApi.example",
      "intent": "scene",
      "code": "// 1) Print the namespace example string.\nconst exampleText = create.example();\n// 2) Log it to the console.\nconsole.log('example length: ' + exampleText.length);"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.fromVertsAndFaces",
      "symbol": "ModelCreateApi.fromVertsAndFaces",
      "intent": "scene",
      "code": "// 1) Build a single triangle from raw verts + faces. Positions are in\n//    the active grid unit (cm by default) — this 100cm right-triangle\n//    spans 1 m, matching create.cube({ width: 100 }).\nconst tri = await create.fromVertsAndFaces({\n    positions: [[0, 0, 0], [100, 0, 0], [0, 100, 0]], // cm → 1 m extent\n    indices: [[0, 1, 2]],\n    faceVertexCounts: undefined,\n    faceVertexIndices: undefined,\n    normals: [[0, 0, 1], [0, 0, 1], [0, 0, 1]],\n    name: 'demo-tri',\n    material: undefined,\n});\n// 2) Translate off the origin (cm — 250cm = 2.5 m).\ntri.xform({ t: [250, 130, -70] });\n// 3) Log verifiable properties.\nconsole.log('fromVertsAndFaces name: ' + tri.name);\nconsole.log('fromVertsAndFaces vertex count: ' + tri.verts.count);\nconsole.log('fromVertsAndFaces face count: ' + tri.faces.count);\n// 4) Cleanup.\nawait tri.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.funnel",
      "symbol": "ModelCreateApi.funnel",
      "intent": "scene",
      "code": "// 1) Build a kitchen funnel.\nconst funnel = await create.funnel({\n    topDiameter: 0.7,\n    bottomDiameter: 0.18,\n    coneHeight: 0.55,\n    spoutLength: 0.25,\n    wallThickness: 0.025,\n    radialSegments: 32,\n    name: 'demo-funnel',\n});\n// 2) Translate off the origin.\nfunnel.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('funnel name: ' + funnel.name);\nconsole.log('funnel vertex count: ' + funnel.verts.count);\nconsole.log('funnel face count: ' + funnel.faces.count);\n// 4) Cleanup.\nawait funnel.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.gear",
      "symbol": "ModelCreateApi.gear",
      "intent": "scene",
      "code": "// 1) Build a 16-tooth spur gear.\nconst gear = await create.gear({\n    toothCount: 16,\n    module_: 0.12,\n    pressureAngle: 20,\n    faceWidth: 0.25,\n    boreDiameter: 0.15,\n    addendum: 0.12,\n    dedendum: 0.15,\n    name: 'demo-gear',\n});\n// 2) Translate off the origin.\ngear.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('gear name: ' + gear.name);\nconsole.log('gear vertex count: ' + gear.verts.count);\nconsole.log('gear face count: ' + gear.faces.count);\n// 4) Cleanup.\nawait gear.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.group",
      "symbol": "ModelCreateApi.group",
      "intent": "scene",
      "code": "// 1) Spawn an empty group node.\nconst grp = await create.group();\n// 2) Log a verifiable property.\nconsole.log('group name: ' + grp.name);\n// 3) Cleanup.\nawait grp.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.heart",
      "symbol": "ModelCreateApi.heart",
      "intent": "scene",
      "code": "// 1) Build a hollow heart pendant.\nconst heart = await create.heart({\n    width: 1.2,\n    height: 1.0,\n    hollow: true,\n    wallThickness: 0.08,\n    resolution: 80,\n    name: 'demo-heart',\n});\n// 2) Translate off the origin.\nheart.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('heart name: ' + heart.name);\nconsole.log('heart vertex count: ' + heart.verts.count);\nconsole.log('heart face count: ' + heart.faces.count);\n// 4) Cleanup.\nawait heart.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.help",
      "symbol": "ModelCreateApi.help",
      "intent": "scene",
      "code": "// 1) Print the namespace help string.\nconst helpText = create.help();\n// 2) Log it to the console.\nconsole.log('help length: ' + helpText.length);"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.hemisphere",
      "symbol": "ModelCreateApi.hemisphere",
      "intent": "scene",
      "code": "// 1) Build a hemisphere with a flat base (capped underside).\nconst hemi = await create.hemisphere({\n    radius: 1,\n    widthSegments: 18,\n    heightSegments: 9,\n    flatBase: true,\n    name: 'demo-hemisphere',\n});\n// 2) Translate off the origin.\nhemi.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('hemisphere name: ' + hemi.name);\nconsole.log('hemisphere vertex count: ' + hemi.verts.count);\nconsole.log('hemisphere face count: ' + hemi.faces.count);\n// 4) Cleanup.\nawait hemi.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.hookProfile",
      "symbol": "ModelCreateApi.hookProfile",
      "intent": "scene",
      "code": "// 1) Build a coat-hook-style hook profile.\nconst hook = await create.hookProfile({\n    hookRadius: 0.25,\n    hookThickness: 0.05,\n    hookWidth: 0.05,\n    hookAngle: Math.PI,\n    straightLength: 0.4,\n    name: 'demo-hook',\n});\n// 2) Translate off the origin.\nhook.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('hook name: ' + hook.name);\nconsole.log('hook vertex count: ' + hook.verts.count);\nconsole.log('hook face count: ' + hook.faces.count);\n// 4) Cleanup.\nawait hook.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.imagePlane",
      "symbol": "ModelCreateApi.imagePlane",
      "intent": "scene",
      "code": "// imagePlane is interactive: it opens the native OS file picker and loads\n// the chosen image onto a plane. There is no headless data-URL path (the\n// op only accepts a picked File), so wrap it so the snippet runs to\n// completion whether or not a picker is available — in the editor, the\n// dialog appears and you pick a reference image.\ntry {\n    // 1) Prompt the user to pick a reference image.\n    await create.imagePlane({});\n    console.log('imagePlane completed');\n}\ncatch (e) {\n    // No interactive picker available (e.g. headless) — needs a real image file.\n    console.log('imagePlane needs an interactive file picker: ' + String(e.message || e));\n}"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.joint",
      "symbol": "ModelCreateApi.joint",
      "intent": "scene",
      "code": "// Attempting to call joint() throws — wrap in try/catch.\ntry {\n    create.joint({ name: 'wrist', position: [0, 1, 0], parent: 'forearm' });\n}\ncatch (err) {\n    console.log('joint not yet implemented: ' + err.message);\n}"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.knob",
      "symbol": "ModelCreateApi.knob",
      "intent": "scene",
      "code": "// 1) Build a fluted-grip control knob with a domed top.\nconst knob = await create.knob({\n    diameter: 0.6,\n    height: 0.25,\n    style: 'fluted',\n    fluteCount: 16,\n    fluteDepth: 0.025,\n    topStyle: 'domed',\n    radialSegments: 32,\n    name: 'demo-knob',\n});\n// 2) Translate off the origin.\nknob.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('knob name: ' + knob.name);\nconsole.log('knob vertex count: ' + knob.verts.count);\nconsole.log('knob face count: ' + knob.faces.count);\n// 4) Cleanup.\nawait knob.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.locator",
      "symbol": "ModelCreateApi.locator",
      "intent": "scene",
      "code": "// 1) Spawn a locator at a specific position with a custom scale.\nconst loc = create.locator({\n    name: 'pivot-marker',\n    position: [1.5, 0.8, -0.4],\n    scale: [1, 1, 1],\n});\n// 2) Log verifiable properties.\nconsole.log('locator name: ' + loc.name);\nconsole.log('locator position x: ' + loc.translate.x.get());\n// 3) Cleanup.\nawait loc.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.loft",
      "symbol": "ModelCreateApi.loft",
      "intent": "scene",
      "code": "// 1) Build a 3-profile loft (cube → square → cube).\nconst lofted = await create.loft({\n    profiles: [\n        [[-1, 0, -1], [1, 0, -1], [1, 0, 1], [-1, 0, 1]],\n        [[-0.5, 1, -0.5], [0.5, 1, -0.5], [0.5, 1, 0.5], [-0.5, 1, 0.5]],\n        [[-1, 2, -1], [1, 2, -1], [1, 2, 1], [-1, 2, 1]],\n    ],\n    caps: true,\n    name: 'demo-loft',\n    material: undefined,\n});\n// 2) Translate off the origin.\nlofted.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('loft name: ' + lofted.name);\nconsole.log('loft vertex count: ' + lofted.verts.count);\nconsole.log('loft face count: ' + lofted.faces.count);\n// 4) Cleanup.\nawait lofted.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.lShape",
      "symbol": "ModelCreateApi.lShape",
      "intent": "scene",
      "code": "// 1) Build an L-shaped angle iron.\nconst angle = await create.lShape({\n    armLength1: 1.2,\n    armLength2: 0.8,\n    thickness: 0.12,\n    depth: 0.6,\n    name: 'demo-l-shape',\n});\n// 2) Translate off the origin.\nangle.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('lShape name: ' + angle.name);\nconsole.log('lShape vertex count: ' + angle.verts.count);\nconsole.log('lShape face count: ' + angle.faces.count);\n// 4) Cleanup.\nawait angle.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.material",
      "symbol": "ModelCreateApi.material",
      "intent": "scene",
      "code": "// 1) Build a glossy red material (async + undoable).\nconst mat = await create.material({\n    name: 'demo-red',\n    baseColor: [1, 0.2, 0.2, 1],\n    metallic: 0.1,\n    roughness: 0.3,\n    emissive: [0.05, 0, 0],\n    emissiveIntensity: 1,\n    alphaMode: 'opaque',\n    alphaCutoff: 0.5,\n    doubleSided: false,\n});\n// 2) Log a verifiable property.\nconsole.log('material name: ' + mat.name);"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.nut",
      "symbol": "ModelCreateApi.nut",
      "intent": "scene",
      "code": "// 1) Build a hex nut with internal threads.\nconst nut = await create.nut({\n    outerRadius: 0.12,\n    height: 0.07,\n    boreRadius: 0.06,\n    threadPitch: 0.045,\n    threadDepth: 0.012,\n    radialSegments: 20,\n    name: 'demo-nut',\n});\n// 2) Translate off the origin.\nnut.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('nut name: ' + nut.name);\nconsole.log('nut vertex count: ' + nut.verts.count);\nconsole.log('nut face count: ' + nut.faces.count);\n// 4) Cleanup.\nawait nut.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.plane",
      "symbol": "ModelCreateApi.plane",
      "intent": "scene",
      "code": "// 1) Build a 4x4 plane with a 3x3 subdivision grid.\nconst plane = await create.plane({\n    width: 4,\n    height: 4,\n    widthSegments: 3,\n    heightSegments: 3,\n    name: 'demo-plane',\n});\n// 2) Translate off the origin.\nplane.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties. A 3x3 subdivided plane exposes 16\n//    vertices (a 4 by 4 grid).\nconsole.log('plane name: ' + plane.name);\nconsole.log('plane vertex count: ' + plane.verts.count);\nconsole.log('plane face count: ' + plane.faces.count);\n// 4) Cleanup.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.pyramid",
      "symbol": "ModelCreateApi.pyramid",
      "intent": "scene",
      "code": "// 1) Build a 4-sided pyramid with no truncation (apex pinches to a point).\nconst pyramid = await create.pyramid({\n    baseRadius: 1,\n    height: 1.5,\n    sides: 4,\n    truncation: 0,\n    name: 'demo-pyramid',\n});\n// 2) Translate off the origin.\npyramid.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('pyramid name: ' + pyramid.name);\nconsole.log('pyramid vertex count: ' + pyramid.verts.count);\nconsole.log('pyramid face count: ' + pyramid.faces.count);\n// 4) Cleanup.\nawait pyramid.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.rectangle",
      "symbol": "ModelCreateApi.rectangle",
      "intent": "scene",
      "code": "// 1) Build a 2x1 rectangle on the XZ ground plane.\nconst rect = await create.rectangle({\n    p0: [-1, 0, -0.5],\n    p1: [1, 0, 0.5],\n    name: 'demo-rect',\n});\n// 2) Translate off the origin.\nrect.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log a verifiable property.\nconsole.log('rectangle name: ' + rect.name);\n// 4) Cleanup.\nawait rect.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.ring",
      "symbol": "ModelCreateApi.ring",
      "intent": "scene",
      "code": "// 1) Build a flat ring (washer-style).\nconst ring = await create.ring({\n    outerRadius: 0.6,\n    innerRadius: 0.35,\n    height: 0.08,\n    radialSegments: 32,\n    name: 'demo-ring',\n});\n// 2) Translate off the origin.\nring.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('ring name: ' + ring.name);\nconsole.log('ring vertex count: ' + ring.verts.count);\nconsole.log('ring face count: ' + ring.faces.count);\n// 4) Cleanup.\nawait ring.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.roundedBox",
      "symbol": "ModelCreateApi.roundedBox",
      "intent": "scene",
      "code": "// 1) Build a soft-edged rounded box.\nconst box = await create.roundedBox({\n    width: 1.5,\n    height: 0.8,\n    depth: 1.2,\n    cornerRadius: 0.12,\n    cornerSegments: 4,\n    name: 'demo-rounded-box',\n});\n// 2) Translate off the origin.\nbox.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('roundedBox name: ' + box.name);\nconsole.log('roundedBox vertex count: ' + box.verts.count);\nconsole.log('roundedBox face count: ' + box.faces.count);\n// 4) Cleanup.\nawait box.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.screw",
      "symbol": "ModelCreateApi.screw",
      "intent": "scene",
      "code": "// 1) Build a hex-head screw with a tapered tip and visible slot.\nconst screw = await create.screw({\n    headType: 'hex',\n    headRadius: 0.15,\n    headHeight: 0.1,\n    shankRadius: 0.06,\n    shankLength: 0.7,\n    threadPitch: 0.04,\n    threadDepth: 0.012,\n    radialSegments: 24,\n    tipLength: 0.06,\n    flatTip: false,\n    slotDepth: 0.025,\n    slotWidth: 0.02,\n    name: 'demo-screw',\n});\n// 2) Translate off the origin.\nscrew.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('screw name: ' + screw.name);\nconsole.log('screw vertex count: ' + screw.verts.count);\nconsole.log('screw face count: ' + screw.faces.count);\n// 4) Cleanup.\nawait screw.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.screwAndNut",
      "symbol": "ModelCreateApi.screwAndNut",
      "intent": "scene",
      "code": "// 1) Build a matched screw + nut pair.\nconst set = await create.screwAndNut({\n    threadPitch: 0.04,\n    threadDepth: 0.012,\n    radialSegments: 20,\n    shankRadius: 0.06,\n    headType: 'hex',\n    headRadius: 0.14,\n    headHeight: 0.09,\n    shankLength: 0.7,\n    tipLength: 0.06,\n    flatTip: false,\n    nutOuterRadius: 0.15,\n    nutHeight: 0.07,\n    name: 'demo-screw-nut',\n});\n// 2) Translate off the origin.\nset.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('screwAndNut name: ' + set.name);\nconsole.log('screwAndNut vertex count: ' + set.verts.count);\nconsole.log('screwAndNut face count: ' + set.faces.count);\n// 4) Cleanup.\nawait set.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.sphere",
      "symbol": "ModelCreateApi.sphere",
      "intent": "scene",
      "code": "// 1) Build a sphere with 24 width segments and 16 height segments.\nconst sphere = await create.sphere({\n    radius: 1.25,\n    widthSegments: 24,\n    heightSegments: 16,\n    name: 'demo-sphere',\n});\n// 2) Translate off the origin.\nsphere.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('sphere name: ' + sphere.name);\nconsole.log('sphere vertex count: ' + sphere.verts.count);\nconsole.log('sphere face count: ' + sphere.faces.count);\n// 4) Cleanup.\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.spring",
      "symbol": "ModelCreateApi.spring",
      "intent": "scene",
      "code": "// 1) Build a 6-turn helical spring.\nconst spring = await create.spring({\n    coilRadius: 0.35,\n    wireRadius: 0.06,\n    pitch: 0.12,\n    turns: 6,\n    radialSegments: 10,\n    tubularSegments: 36,\n    name: 'demo-spring',\n});\n// 2) Translate off the origin.\nspring.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('spring name: ' + spring.name);\nconsole.log('spring vertex count: ' + spring.verts.count);\nconsole.log('spring face count: ' + spring.faces.count);\n// 4) Cleanup.\nawait spring.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.star",
      "symbol": "ModelCreateApi.star",
      "intent": "scene",
      "code": "// 1) Build a hollow 6-pointed star.\nconst star = await create.star({\n    pointCount: 6,\n    outerRadius: 0.7,\n    innerRadius: 0.3,\n    height: 0.25,\n    hollow: true,\n    wallThickness: 0.06,\n    name: 'demo-star',\n});\n// 2) Translate off the origin.\nstar.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('star name: ' + star.name);\nconsole.log('star vertex count: ' + star.verts.count);\nconsole.log('star face count: ' + star.faces.count);\n// 4) Cleanup.\nawait star.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.storageBox",
      "symbol": "ModelCreateApi.storageBox",
      "intent": "scene",
      "code": "// 1) Build an open-top storage tray.\nconst tray = await create.storageBox({\n    innerWidth: 1.2,\n    innerDepth: 0.8,\n    innerHeight: 0.4,\n    wallThickness: 0.06,\n    floorThickness: 0.06,\n    cornerRadius: 0.05,\n    cornerSegments: 4,\n    name: 'demo-storage-box',\n});\n// 2) Translate off the origin.\ntray.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('storageBox name: ' + tray.name);\nconsole.log('storageBox vertex count: ' + tray.verts.count);\nconsole.log('storageBox face count: ' + tray.faces.count);\n// 4) Cleanup.\nawait tray.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.text",
      "symbol": "ModelCreateApi.text",
      "intent": "scene",
      "code": "// 1) Render a centered 2-line label.\nconst label = await create.text({\n    text: 'Line 1\\nLine 2',\n    fontId: 'inter-regular',\n    align: 'center',\n    lineHeight: 1.4,\n    name: 'demo-text',\n});\n// 2) Translate off the origin.\nlabel.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('label name: ' + label.name);\nconsole.log('label vertex count: ' + label.verts.count);\n// 4) Cleanup.\nawait label.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.texture",
      "symbol": "ModelCreateApi.texture",
      "intent": "scene",
      "code": "// 1. Setup — textures live on the asset; create geometry first.\nconst cube = await create.cube({ name: 'texture-demo' });\n// Paint a 64×64 grass-noise canvas to use as the image payload.\nconst canvas = document.createElement('canvas');\ncanvas.width = canvas.height = 64;\nconst g = canvas.getContext('2d');\n// Random green pixels — any canvas dataURL, Blob, or ArrayBuffer works as `data`.\nfor (let y = 0; y < 64; y++)\n    for (let x = 0; x < 64; x++) {\n        g.fillStyle = 'rgb(0,' + (110 + Math.floor(Math.random() * 90)) + ',40)';\n        g.fillRect(x, y, 1, 1);\n    }\n// 2. Action — create the asset texture (undoable) + a material.\n// Options: name (auto-disambiguated on collision), data (REQUIRED),\n// mimeType (required for ArrayBuffer data only; inferred otherwise),\n// width/height (decoded from the image when omitted),\n// wrapS/wrapT ('repeat' | 'clamp' | 'mirror' — default 'repeat').\nconst tex = await create.texture({ name: 'grass', data: canvas.toDataURL('image/png') });\nconst mat = await create.material({ name: 'grass-mat' });\n// 3. Bind by NAME (ids work too) and assign so the texture renders.\nawait mat.textures.baseColor.set('grass');\nawait cube.material.set(mat);\n// 4. Observe — the bound channel id matches the created texture.\nconsole.log('texture ' + tex.name + ' bound: ' + (mat.textures.baseColor.id === tex.id));"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.torus",
      "symbol": "ModelCreateApi.torus",
      "intent": "scene",
      "code": "// 1) Build a torus with a 1.0 ring radius and 0.3 tube radius. The arc\n//    field is radians; 2*Math.PI is a full ring.\nconst torus = await create.torus({\n    radius: 1,\n    tube: 0.3,\n    radialSegments: 16,\n    tubularSegments: 24,\n    arc: Math.PI * 2,\n    name: 'demo-torus',\n});\n// 2) Translate off the origin.\ntorus.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('torus name: ' + torus.name);\nconsole.log('torus vertex count: ' + torus.verts.count);\nconsole.log('torus face count: ' + torus.faces.count);\n// 4) Cleanup.\nawait torus.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.tShape",
      "symbol": "ModelCreateApi.tShape",
      "intent": "scene",
      "code": "// 1) Build a T-shaped beam.\nconst tee = await create.tShape({\n    topWidth: 1.4,\n    stemHeight: 1.1,\n    thickness: 0.12,\n    depth: 0.6,\n    name: 'demo-t-shape',\n});\n// 2) Translate off the origin.\ntee.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('tShape name: ' + tee.name);\nconsole.log('tShape vertex count: ' + tee.verts.count);\nconsole.log('tShape face count: ' + tee.faces.count);\n// 4) Cleanup.\nawait tee.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.tube",
      "symbol": "ModelCreateApi.tube",
      "intent": "scene",
      "code": "// 1) Build a capped cylindrical cup.\nconst tube = await create.tube({\n    outerRadius: 0.45,\n    innerRadius: 0.4,\n    height: 1.2,\n    radialSegments: 24,\n    openEnded: false,\n    capBottom: true,\n    floorThickness: 0.03,\n    name: 'demo-tube',\n});\n// 2) Translate off the origin.\ntube.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties.\nconsole.log('tube name: ' + tube.name);\nconsole.log('tube vertex count: ' + tube.verts.count);\nconsole.log('tube face count: ' + tube.faces.count);\n// 4) Cleanup.\nawait tube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.unlitMaterial",
      "symbol": "ModelCreateApi.unlitMaterial",
      "intent": "scene",
      "code": "// 1) Build an unlit cyan material (async + undoable).\nconst mat = await create.unlitMaterial({\n    name: 'demo-unlit-cyan',\n    baseColor: [0, 0.8, 1, 1],\n});\n// 2) Log a verifiable property.\nconsole.log('unlitMaterial name: ' + mat.name);"
    },
    {
      "kind": "example",
      "id": "example:ModelCreateApi.wedge",
      "symbol": "ModelCreateApi.wedge",
      "intent": "scene",
      "code": "// 1) Build a 2x1x3 wedge.\nconst wedge = await create.wedge({\n    width: 2,\n    height: 1,\n    depth: 3,\n    name: 'demo-wedge',\n});\n// 2) Translate off the origin.\nwedge.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Log verifiable properties. A canonical wedge has 6 vertices.\nconsole.log('wedge name: ' + wedge.name);\nconsole.log('wedge vertex count: ' + wedge.verts.count);\nconsole.log('wedge face count: ' + wedge.faces.count);\n// 4) Cleanup.\nawait wedge.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi",
      "symbol": "ModelEditorApi",
      "intent": "scene",
      "code": "// 1) Spawn a cube, translate it off origin, log its vertex count, clean up.\nconst cube = await create.cube({\n    width: 2, height: 1, depth: 3,\n    subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n    name: 'demo-cube',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('cube vertex count is a number: ' + (typeof cube.verts.count === 'number'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.catalog",
      "symbol": "ModelEditorApi.catalog",
      "intent": "scene",
      "code": "// 1) Search the catalog for M3 fasteners.\nconst hits = catalog.search('M3');\nconsole.log('M3 hits is number: ' + (typeof hits.length === 'number'));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.checkOpenScad",
      "symbol": "ModelEditorApi.checkOpenScad",
      "intent": "scene",
      "code": "// 1) Probe printability — READ-ONLY: the scene is untouched afterwards.\nconst report = await checkOpenScad(`\r\n    difference() {\r\n      cube([20, 20, 8], center = true);                 // 20x20x8 mm body\r\n      cylinder(h = 20, d = 6, center = true, $fn = 48); // 6 mm bore\r\n    }\r\n  `);\nconsole.log('printable: ' + report.ok + ' shells=' + report.shells); // printable: true shells=1\nconsole.log('size mm: ' + report.sizeMm.join(' x ')); // size mm: 20 x 20 x 8\nconsole.log('scene untouched, mesh is null: ' + (report.mesh === null));\nif (!report.ok)\n    console.log('problems: ' + report.problems.join(', '));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.clipboard",
      "symbol": "ModelEditorApi.clipboard",
      "intent": "scene",
      "code": "// 1) Copy the current selection, then paste a duplicate at the same location.\nawait clipboard.copy();\nawait clipboard.paste();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.create",
      "symbol": "ModelEditorApi.create",
      "intent": "scene",
      "code": "// 1) Spawn a torus with explicit segment counts.\nconst ring = await create.torus({\n    radius: 1, tube: 0.25, radialSegments: 24, tubularSegments: 48,\n    arc: Math.PI * 2, name: 'demo-torus',\n});\n// 2) Center the pivot BEFORE any rotation (torus geometry sits at [0, tube, 0]).\nawait ring.centerPivot();\nring.xform({ t: [2.5, 1.3, -0.7] });\n// 3) Verify position from the GEOMETRY (world meters), not the transform.\nconst center = ring.geometry.bbox({ space: 'world' }).center;\nconsole.log('world center is a vector: ' + (typeof center.toArray()[0] === 'number'));\nawait ring.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.execOpenScad",
      "symbol": "ModelEditorApi.execOpenScad",
      "intent": "scene",
      "code": "// 1) A parametric hex standoff — declared variables are overridable.\nconst standoffSrc = `\r\n    across_flats = 7;   // hex width, mm\r\n    standoff_h  = 12;   // height, mm\r\n    bore_d      = 3.2;  // through-bore, mm\r\n    difference() {\r\n      cylinder(h = standoff_h, d = across_flats / cos(30), $fn = 6);\r\n      cylinder(h = standoff_h * 3, d = bore_d, center = true, $fn = 32);\r\n    }\r\n  `;\nconst short = await execOpenScad(standoffSrc);\n// 2) Re-bake TALLER via params — no source edit, same script.\nconst tall = await execOpenScad(standoffSrc, { params: { standoff_h: 20 } });\nconsole.log('two standoffs: ' + short.name + ', ' + tall.name);\nawait short.delete();\nawait tall.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.getActiveMaterial",
      "symbol": "ModelEditorApi.getActiveMaterial",
      "intent": "scene",
      "code": "const id = getActiveMaterial();\nconsole.log('active material: ' + id);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.getComponentMode",
      "symbol": "ModelEditorApi.getComponentMode",
      "intent": "scene",
      "code": "const mode = getComponentMode();\nconsole.log('current mode: ' + mode);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.inspect",
      "symbol": "ModelEditorApi.inspect",
      "intent": "scene",
      "code": "// 1) Spawn a cube, translate it, measure the offset.\nconst cube = await create.cube({ width: 1 });\nawait translate.set(cube, [0, 0, 2]);\nconst d = inspect.measure(cube, [0, 0, 0], 'z');\nconsole.log('signedDistance is number: ' + (typeof d.signedDistance === 'number'));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.io",
      "symbol": "ModelEditorApi.io",
      "intent": "scene",
      "code": "// 1) Read the current asset path and modified flag.\nconsole.log('asset path is a string: ' + (typeof io.path === 'string'));\nconsole.log('modified is boolean: ' + (typeof io.modified === 'boolean'));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.material",
      "symbol": "ModelEditorApi.material",
      "intent": "scene",
      "code": "// 1) Build a cube.\nconst cube = await create.cube({ name: 'mat-ns-demo' });\n// 2) Broadcast a preset, then clear overrides — both undoable.\nawait material.applyToAll('terracotta', [cube.id]);\nawait material.clearOverrides();\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.scriptEditor",
      "symbol": "ModelEditorApi.scriptEditor",
      "intent": "scene",
      "code": "// 1) Toggle the Script Editor panel open, then back closed.\nawait scriptEditor.open();\nscriptEditor.close();\nconsole.log('script editor toggle ran');"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.setActiveMaterial",
      "symbol": "ModelEditorApi.setActiveMaterial",
      "intent": "scene",
      "code": "// 1) Switch the active material preset to terracotta.\nsetActiveMaterial('terracotta');\n// 2) Confirm the change took effect.\nconst active = getActiveMaterial();\nconsole.log('active material: ' + active); // → 'terracotta'"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.setComponentMode",
      "symbol": "ModelEditorApi.setComponentMode",
      "intent": "scene",
      "code": "setComponentMode('object');\nconsole.log('component mode reset to object');"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.tool",
      "symbol": "ModelEditorApi.tool",
      "intent": "scene",
      "code": "// 1) Read the currently-active interactive tool name.\nconst active = tool.active;\nconsole.log('active tool is a string: ' + (typeof active === 'string'));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorApi.viewport",
      "symbol": "ModelEditorApi.viewport",
      "intent": "scene",
      "code": "// 1) Frame the scene with extra padding so the camera sits well off the mesh.\nawait Utils.wait.frame();\nviewport.zoomToFit(null, 2.5);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorCatalogApi.browse",
      "symbol": "ModelEditorCatalogApi.browse",
      "intent": "scene",
      "code": "// 1) Browse the catalog interactively. browse() OPENS the Parts Library\n//    dialog and resolves only when the user picks a part or cancels — so\n//    in a script we kick it off WITHOUT awaiting, then close the dialog to\n//    resolve it (closing returns null, same as a user cancel).\nconst browsing = catalog.browse();\n// 2) Close the dialog so the editor returns to its default state and the\n//    promise settles (a real user would pick a part or hit Cancel here).\nuseActionDialogStore.getState().closeDialog();\nconst part = await browsing;\nconsole.log('selected: ' + (part?.name ?? 'cancelled'));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorCatalogApi.diagnose",
      "symbol": "ModelEditorCatalogApi.diagnose",
      "intent": "scene",
      "code": "// 1) Guard against the \"M5 holes\" mis-route before searching.\nconst warn = catalog.diagnose('M5 mounting holes');\nif (warn.length > 0)\n    console.log('subtractive: ' + warn[0]);\nelse\n    console.log('additive matches: ' + catalog.search('M5 bolt').length);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorCatalogApi.example",
      "symbol": "ModelEditorCatalogApi.example",
      "intent": "scene",
      "code": "// 1) Print the catalog examples.\nconst text = catalog.example();\nconsole.log('example is string: ' + (typeof text === 'string'));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorCatalogApi.get",
      "symbol": "ModelEditorCatalogApi.get",
      "intent": "scene",
      "code": "// 1) Look up a specific part.\nconst p = catalog.get('iso4762-m3-x12');\nconsole.log('found: ' + (p !== null));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorCatalogApi.help",
      "symbol": "ModelEditorCatalogApi.help",
      "intent": "scene",
      "code": "// 1) Print the namespace TOC.\nconst text = catalog.help();\nconsole.log(text);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorCatalogApi.install",
      "symbol": "ModelEditorCatalogApi.install",
      "intent": "scene",
      "code": "// 1) Resolve a real part id from the live catalog (ids are registered at\n//    load — `list()` is synchronous and always returns valid ids). Pass a\n//    hard-coded id instead once you know the exact part you want.\nconst parts = catalog.list();\nconst partId = parts[0].id;\n// 2) Install it — fetches the part's STEP file + imports it.\nconst part = await catalog.install(partId);\nconsole.log('installed part id: ' + partId);\nconsole.log('faces is number: ' + (typeof part.faces.count === 'number'));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorCatalogApi.list",
      "symbol": "ModelEditorCatalogApi.list",
      "intent": "scene",
      "code": "// 1) Log every part id + name in the catalog.\nconst parts = catalog.list();\nfor (const p of parts)\n    console.log(p.id, p.name);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorCatalogApi.search",
      "symbol": "ModelEditorCatalogApi.search",
      "intent": "scene",
      "code": "// 1) Find every M3 fastener by keyword.\nconst m3 = catalog.search('M3');\nconsole.log('M3 hits: ' + m3.length);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorCatalogApi.warnings",
      "symbol": "ModelEditorCatalogApi.warnings",
      "intent": "scene",
      "code": "// 1) search() returns parts unchanged, but records the trap if the query was subtractive.\ncatalog.search('drill 4 holes');\nconsole.log('warnings so far: ' + catalog.warnings.length);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorDevDialogs",
      "symbol": "ModelEditorDevDialogs",
      "intent": "scene",
      "code": "// 1) The universal six are available alongside the Model-only one.\nconsole.log('largeMeshWarning open: ' + dev.dialogs.largeMeshWarning.isOpen());\nconsole.log('help open: ' + dev.dialogs.help.isOpen());\n// 2) Drive the Model-only warning popup.\nawait dev.dialogs.largeMeshWarning.open();\nconsole.log('after open: ' + dev.dialogs.largeMeshWarning.isOpen());\nawait dev.dialogs.largeMeshWarning.close();\n// 3) Read the Validate & Repair popup's live session state.\nconsole.log('validate active: ' + dev.dialogs.meshValidate.isActive);\nconsole.log('validate mesh: ' + dev.dialogs.meshValidate.activeMeshId);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorDevDialogs.largeMeshWarning",
      "symbol": "ModelEditorDevDialogs.largeMeshWarning",
      "intent": "scene",
      "code": "await dev.dialogs.largeMeshWarning.open();\nconsole.log('largeMeshWarning isOpen: ' + dev.dialogs.largeMeshWarning.isOpen());\nawait dev.dialogs.largeMeshWarning.close();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorDevDialogs.meshValidate",
      "symbol": "ModelEditorDevDialogs.meshValidate",
      "intent": "scene",
      "code": "console.log('validate active: ' + dev.dialogs.meshValidate.isActive);\nconsole.log('validate mesh: ' + dev.dialogs.meshValidate.activeMeshId);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorIoApi.exportModel",
      "symbol": "ModelEditorIoApi.exportModel",
      "intent": "scene",
      "code": "// 1) Spawn a cube so the export has content.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1 });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Headless export to a specific GLB path.\nawait io.exportModel('/local/exports/hero.glb');\nconsole.log('scene exported to hero.glb');\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorIoApi.exportSelectedModel",
      "symbol": "ModelEditorIoApi.exportSelectedModel",
      "intent": "scene",
      "code": "// 1) Spawn two cubes and select only the keeper.\nconst keep = await create.cube({ width: 1, height: 1, depth: 1 });\nkeep.xform({ t: [2.5, 1.3, -0.7] });\nconst ignore = await create.cube({ width: 1, height: 1, depth: 1 });\nignore.xform({ t: [-2.5, 1.3, 0.7] });\nawait select(null, { mode: 'clear' });\nawait select(keep);\n// 2) Export selection only.\nawait io.exportSelectedModel('/local/exports/selected.glb');\nconsole.log('selected export written to selected.glb');\n// 3) Clean up.\nawait keep.delete();\nawait ignore.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorIoApi.importModel",
      "symbol": "ModelEditorIoApi.importModel",
      "intent": "scene",
      "code": "// importModel(path) fetches the file at `path` and dispatches the importer\n// by the path's file EXTENSION (.glb / .fbx / .obj / .ply / .stl / .step).\n// It needs a REAL reachable model file — there is no inline data-URL form\n// (the extension can't be derived from a data: basename), so the demo path\n// below is illustrative. Wrap it so the snippet runs to completion either\n// way; in the editor, point it at a file you've made reachable (a served\n// URL or a granted workspace path).\nconst before = ls().length;\ntry {\n    // 1) Import by reachable path/URL (replace with your own model file).\n    await io.importModel('/local/source/walking.fbx');\n    const after = ls().length;\n    console.log('nodes added by import: ' + (after - before));\n}\ncatch (e) {\n    // Demo path isn't reachable here — needs a real model file.\n    console.log('importModel needs a real, reachable model file: ' + String(e.message || e));\n}\n// 2) Picker form — omit the argument to open the native file picker:\n//    await io.importModel();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorIoApi.inspectFormat",
      "symbol": "ModelEditorIoApi.inspectFormat",
      "intent": "scene",
      "code": "// 1) Identify a format from raw bytes (magic-byte inspection).\n//    GLB files begin with the ASCII magic \"glTF\".\nconst glbHeader = new TextEncoder().encode('glTF');\nconst result = io.inspectFormat(glbHeader.buffer);\nconsole.log('format: ' + result.format); // 'glb'\nconsole.log('source: ' + result.source); // 'magic'\n// 2) Unrecognised bytes report null + 'unknown' (no throw).\nconst unknown = io.inspectFormat(new Uint8Array([1, 2, 3, 4]));\nconsole.log('unknown format: ' + unknown.format); // null\nconsole.log('unknown source: ' + unknown.source); // 'unknown'"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorMaterialApi",
      "symbol": "ModelEditorMaterialApi",
      "intent": "scene",
      "code": "// 1) Build two cubes so there is something to broadcast onto.\nconst a = await create.cube({ name: 'mat-a' });\nconst b = await create.cube({ name: 'mat-b' });\n// 2) Broadcast a material onto both meshes BY NAME (name-first; undoable).\n//    A studio-preset id like 'terracotta' works too.\nawait material.applyToAll('terracotta', ['mat-a', 'mat-b']);\n// 3) Clear every per-mesh override, then clean up.\nawait material.clearOverrides();\nawait a.delete();\nawait b.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorMaterialApi.applyToAll",
      "symbol": "ModelEditorMaterialApi.applyToAll",
      "intent": "scene",
      "code": "// 1) Build a cube and a sphere, plus a NAMED PBR material.\nconst cube = await create.cube({ name: 'Cube' });\nconst sphere = await create.sphere({ name: 'Sphere' });\nawait create.material({ name: 'Steel', baseColor: [0.6, 0.6, 0.65, 1], metallic: 1, roughness: 0.3 });\n// 2) Broadcast the material to both meshes BY NAME (name-first).\nawait material.applyToAll('Steel', ['Cube', 'Sphere']);\n// 3) Apply the cream studio preset to ALL meshes (no meshes arg), clean up.\nawait material.applyToAll('cream');\nawait cube.delete();\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorMaterialApi.clearOverrides",
      "symbol": "ModelEditorMaterialApi.clearOverrides",
      "intent": "scene",
      "code": "// 1) Build a cube and override its material.\nconst cube = await create.cube({ name: 'co-cube' });\nawait material.applyToAll('terracotta', [cube.id]);\n// 2) Clear every override (cube falls back to the active material).\nawait material.clearOverrides();\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorMaterialApi.example",
      "symbol": "ModelEditorMaterialApi.example",
      "intent": "scene",
      "code": "// Print a runnable material-namespace example.\nconsole.log(material.example());"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorMaterialApi.help",
      "symbol": "ModelEditorMaterialApi.help",
      "intent": "scene",
      "code": "// Print the material-namespace help card.\nconsole.log(material.help());"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.backfaceCull",
      "symbol": "ModelEditorViewportApi.backfaceCull",
      "intent": "scene",
      "code": "const bf = viewport.backfaceCull;\nconsole.log('backfaceCull: ' + bf);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.boundingBox",
      "symbol": "ModelEditorViewportApi.boundingBox",
      "intent": "scene",
      "code": "const bb = viewport.boundingBox;\nconsole.log('boundingBox: ' + bb);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.checkerTiles",
      "symbol": "ModelEditorViewportApi.checkerTiles",
      "intent": "scene",
      "code": "const t = viewport.checkerTiles;\nconsole.log('checkerTiles: ' + t);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.deisolate",
      "symbol": "ModelEditorViewportApi.deisolate",
      "intent": "scene",
      "code": "// 1) Spawn + isolate a cube.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'deiso-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait viewport.isolate(cube);\nconsole.log('isolated cube');\n// 2) Restore the previous visibility state.\nawait viewport.deisolate();\nconsole.log('isolation cleared');\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.isolate",
      "symbol": "ModelEditorViewportApi.isolate",
      "intent": "scene",
      "code": "// 1) Spawn two cubes so isolation has something to act on.\nconst a = await create.cube({ width: 1, height: 1, depth: 1, name: 'iso-a' });\na.xform({ t: [2.5, 1.3, -0.7] });\nconst b = await create.cube({ width: 1, height: 1, depth: 1, name: 'iso-b' });\nb.xform({ t: [-1.5, 0.6, 1.2] });\n// 2) Isolate only `a` (handle), then add `b` (by name).\nawait viewport.isolate(a);\nconsole.log('isolated a');\nawait viewport.isolate(a, 'iso-b');\nconsole.log('isolated a + b');\n// 3) Exit isolation + show every mesh again + clean up.\nawait viewport.deisolate();\nawait viewport.showAll();\nawait a.delete();\nawait b.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.liveMeshSnap",
      "symbol": "ModelEditorViewportApi.liveMeshSnap",
      "intent": "scene",
      "code": "const s = viewport.liveMeshSnap;\nconsole.log('liveMeshSnap: ' + s);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.pivotMode",
      "symbol": "ModelEditorViewportApi.pivotMode",
      "intent": "scene",
      "code": "const p = viewport.pivotMode;\nconsole.log('pivotMode: ' + p);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.setBackfaceCull",
      "symbol": "ModelEditorViewportApi.setBackfaceCull",
      "intent": "scene",
      "code": "// 1) Snapshot the current setting.\nconst original = viewport.backfaceCull;\nconsole.log('backfaceCull original: ' + original);\n// 2) Flip the setting.\nviewport.setBackfaceCull(!original);\nconsole.log('backfaceCull after toggle: ' + viewport.backfaceCull);\n// 3) Restore.\nviewport.setBackfaceCull(original);\nconsole.log('backfaceCull restored: ' + viewport.backfaceCull);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.setBoundingBox",
      "symbol": "ModelEditorViewportApi.setBoundingBox",
      "intent": "scene",
      "code": "// 1) Snapshot the current setting.\nconst original = viewport.boundingBox;\nconsole.log('boundingBox original: ' + original);\n// 2) Flip it on.\nviewport.setBoundingBox(!original);\nconsole.log('boundingBox after toggle: ' + viewport.boundingBox);\n// 3) Restore.\nviewport.setBoundingBox(original);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.setCheckerTiles",
      "symbol": "ModelEditorViewportApi.setCheckerTiles",
      "intent": "scene",
      "code": "// 1) Snapshot the current tile count.\nconst original = viewport.checkerTiles;\nconsole.log('checkerTiles original: ' + original);\n// 2) Switch to UV-checker render mode so the change is visible, then bump tiles.\nviewport.setRenderMode('uvchecker');\nviewport.setCheckerTiles(8);\nconsole.log('checkerTiles after 8: ' + viewport.checkerTiles);\n// 3) Restore.\nviewport.setCheckerTiles(original);\nconsole.log('checkerTiles restored: ' + viewport.checkerTiles);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.setLiveMeshSnap",
      "symbol": "ModelEditorViewportApi.setLiveMeshSnap",
      "intent": "scene",
      "code": "// 1) Snapshot the current setting via the public getter.\nconst original = viewport.liveMeshSnap;\nconsole.log('liveMeshSnap original: ' + original);\n// 2) Flip the setting on / off and read the value back.\nviewport.setLiveMeshSnap(!original);\nconst flipped = viewport.liveMeshSnap;\nconsole.log('liveMeshSnap after toggle: ' + flipped);\n// 3) Restore.\nviewport.setLiveMeshSnap(original);\nconst restored = viewport.liveMeshSnap;\nconsole.log('liveMeshSnap restored: ' + restored);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.setPivotMode",
      "symbol": "ModelEditorViewportApi.setPivotMode",
      "intent": "scene",
      "code": "// 1) Snapshot the current pivot mode.\nconst original = viewport.pivotMode;\nconsole.log('pivotMode original: ' + original);\n// 2) Cycle every supported pivot.\nviewport.setPivotMode('center');\nconsole.log('pivotMode after center: ' + viewport.pivotMode);\nviewport.setPivotMode('boundingBox');\nconsole.log('pivotMode after boundingBox: ' + viewport.pivotMode);\nviewport.setPivotMode('last');\nconsole.log('pivotMode after last: ' + viewport.pivotMode);\nviewport.setPivotMode('meshOrigin');\nconsole.log('pivotMode after meshOrigin: ' + viewport.pivotMode);\n// 3) Restore.\nviewport.setPivotMode(original);\nconsole.log('pivotMode restored: ' + viewport.pivotMode);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.setShowNormals",
      "symbol": "ModelEditorViewportApi.setShowNormals",
      "intent": "scene",
      "code": "// 1) Snapshot the current state via the public getter.\nconst original = viewport.showNormals;\nconsole.log('showNormals original: ' + original);\n// 2) Flip the setting on.\nviewport.setShowNormals(!original);\nconsole.log('showNormals after toggle: ' + viewport.showNormals);\n// 3) Restore.\nviewport.setShowNormals(original);\nconsole.log('showNormals restored: ' + viewport.showNormals);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.setSmartMeasure",
      "symbol": "ModelEditorViewportApi.setSmartMeasure",
      "intent": "scene",
      "code": "// 1) Snapshot the current setting.\nconst original = viewport.smartMeasure;\nconsole.log('smartMeasure original: ' + original);\n// 2) Flip it on.\nviewport.setSmartMeasure(!original);\nconsole.log('smartMeasure after toggle: ' + viewport.smartMeasure);\n// 3) Restore.\nviewport.setSmartMeasure(original);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.setSmoothPreview",
      "symbol": "ModelEditorViewportApi.setSmoothPreview",
      "intent": "scene",
      "code": "// 1) Make a box to smooth, snapshot the preview state.\nconst box = await create.cube({ name: 'SmoothDemo' });\nconst before = viewport.smoothPreview;\nconsole.log('smoothed before: ' + before.smoothed.length);\n// 2) Smooth just that mesh at level 2 (Maya's default).\nviewport.setSmoothPreview('smooth', { level: 2, meshes: ['SmoothDemo'] });\nconsole.log('smoothed now: ' + viewport.smoothPreview.smoothed.join(','));\n// 3) Restore: preview off + remove the demo mesh.\nviewport.setSmoothPreview('off', { meshes: ['SmoothDemo'] });\nawait box.delete();\nconsole.log('smoothed after restore: ' + viewport.smoothPreview.smoothed.length);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.setSnapMode",
      "symbol": "ModelEditorViewportApi.setSnapMode",
      "intent": "scene",
      "code": "// 1) Snapshot all three snap modes.\nconst original = viewport.snapMode;\nconsole.log('snapMode original: ' + JSON.stringify(original));\n// 2) Enable translate + rotate snap, disable scale snap.\nviewport.setSnapMode({ translate: true, rotate: true }, true);\nviewport.setSnapMode({ scale: true }, false);\nconsole.log('snapMode after set: ' + JSON.stringify(viewport.snapMode));\n// 3) Restore each flag individually.\nviewport.setSnapMode({ translate: true }, original.translate);\nviewport.setSnapMode({ rotate: true }, original.rotate);\nviewport.setSnapMode({ scale: true }, original.scale);\nconsole.log('snapMode restored: ' + JSON.stringify(viewport.snapMode));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.setTransformMode",
      "symbol": "ModelEditorViewportApi.setTransformMode",
      "intent": "scene",
      "code": "// 1) Snapshot the current tool.\nconst original = viewport.transformMode;\nconsole.log('transformMode original: ' + original);\n// 2) Cycle every tool.\nviewport.setTransformMode('translate');\nconsole.log('transformMode after translate: ' + viewport.transformMode);\nviewport.setTransformMode('rotate');\nconsole.log('transformMode after rotate: ' + viewport.transformMode);\nviewport.setTransformMode('scale');\nconsole.log('transformMode after scale: ' + viewport.transformMode);\n// 3) Restore.\nif (original)\n    viewport.setTransformMode(original);\nconsole.log('transformMode restored: ' + viewport.transformMode);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.setWireframe",
      "symbol": "ModelEditorViewportApi.setWireframe",
      "intent": "scene",
      "code": "// 1) Snapshot the current setting.\nconst original = viewport.wireframe;\nconsole.log('wireframe original: ' + original);\n// 2) Flip the setting.\nviewport.setWireframe(!original);\nconsole.log('wireframe after toggle: ' + viewport.wireframe);\n// 3) Restore.\nviewport.setWireframe(original);\nconsole.log('wireframe restored: ' + viewport.wireframe);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.setXray",
      "symbol": "ModelEditorViewportApi.setXray",
      "intent": "scene",
      "code": "// 1) Snapshot the current setting.\nconst original = viewport.xray;\nconsole.log('xray original: ' + original);\n// 2) Flip the setting.\nviewport.setXray(!original);\nconsole.log('xray after toggle: ' + viewport.xray);\n// 3) Restore.\nviewport.setXray(original);\nconsole.log('xray restored: ' + viewport.xray);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.showAll",
      "symbol": "ModelEditorViewportApi.showAll",
      "intent": "scene",
      "code": "// 1) Spawn two cubes and isolate one.\nconst a = await create.cube({ width: 1, height: 1, depth: 1, name: 'show-a' });\na.xform({ t: [2.5, 1.3, -0.7] });\nconst b = await create.cube({ width: 1, height: 1, depth: 1, name: 'show-b' });\nb.xform({ t: [-1.5, 0.6, 1.2] });\nawait viewport.isolate(a);\nconsole.log('isolated a');\n// 2) Reveal every mesh in one call.\nawait viewport.showAll();\nconsole.log('showAll completed');\n// 3) Clean up. showAll() reveals hidden meshes but leaves isolate mode\n//    armed, so exit it explicitly to return the editor to its default.\nawait viewport.deisolate();\nawait a.delete();\nawait b.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.showNormals",
      "symbol": "ModelEditorViewportApi.showNormals",
      "intent": "scene",
      "code": "const n = viewport.showNormals;\nconsole.log('showNormals: ' + n);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.smartMeasure",
      "symbol": "ModelEditorViewportApi.smartMeasure",
      "intent": "scene",
      "code": "const sm = viewport.smartMeasure;\nconsole.log('smartMeasure: ' + sm);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.smoothPreview",
      "symbol": "ModelEditorViewportApi.smoothPreview",
      "intent": "scene",
      "code": "const sp = viewport.smoothPreview;\nconsole.log('level ' + sp.level + ', smoothed: [' + sp.smoothed.join(',') + ']');"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.snapMode",
      "symbol": "ModelEditorViewportApi.snapMode",
      "intent": "scene",
      "code": "const m = viewport.snapMode;\nconsole.log('snapMode: ' + JSON.stringify(m));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.toggleBoundingBox",
      "symbol": "ModelEditorViewportApi.toggleBoundingBox",
      "intent": "scene",
      "code": "const original = viewport.boundingBox;\nviewport.toggleBoundingBox();\nconsole.log('boundingBox after toggle: ' + viewport.boundingBox);\nviewport.toggleBoundingBox();\nconsole.log('boundingBox restored: ' + (viewport.boundingBox === original));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.toggleSmartMeasure",
      "symbol": "ModelEditorViewportApi.toggleSmartMeasure",
      "intent": "scene",
      "code": "const original = viewport.smartMeasure;\nviewport.toggleSmartMeasure();\nconsole.log('smartMeasure after toggle: ' + viewport.smartMeasure);\nviewport.toggleSmartMeasure();\nconsole.log('smartMeasure restored: ' + (viewport.smartMeasure === original));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.toggleSmoothPreview",
      "symbol": "ModelEditorViewportApi.toggleSmoothPreview",
      "intent": "scene",
      "code": "const box = await create.cube({ name: 'ToggleDemo' });\nviewport.toggleSmoothPreview();\nconsole.log('after first toggle: ' + viewport.smoothPreview.smoothed.length);\nviewport.toggleSmoothPreview();\nconsole.log('after second toggle: ' + viewport.smoothPreview.smoothed.length);\nawait box.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.toggleWireframe",
      "symbol": "ModelEditorViewportApi.toggleWireframe",
      "intent": "scene",
      "code": "// 1) Snapshot, toggle, toggle-back.\nconst original = viewport.wireframe;\nviewport.toggleWireframe();\nconsole.log('wireframe after toggle: ' + viewport.wireframe);\nviewport.toggleWireframe();\nconsole.log('wireframe restored: ' + (viewport.wireframe === original));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.toggleXray",
      "symbol": "ModelEditorViewportApi.toggleXray",
      "intent": "scene",
      "code": "// 1) Snapshot, toggle, toggle-back.\nconst original = viewport.xray;\nviewport.toggleXray();\nconsole.log('xray after toggle: ' + viewport.xray);\nviewport.toggleXray();\nconsole.log('xray restored: ' + (viewport.xray === original));"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.transformMode",
      "symbol": "ModelEditorViewportApi.transformMode",
      "intent": "scene",
      "code": "const m = viewport.transformMode;\nconsole.log('transformMode: ' + m);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.wireframe",
      "symbol": "ModelEditorViewportApi.wireframe",
      "intent": "scene",
      "code": "const wf = viewport.wireframe;\nconsole.log('wireframe: ' + wf);"
    },
    {
      "kind": "example",
      "id": "example:ModelEditorViewportApi.xray",
      "symbol": "ModelEditorViewportApi.xray",
      "intent": "scene",
      "code": "const x = viewport.xray;\nconsole.log('xray: ' + x);"
    },
    {
      "kind": "example",
      "id": "example:ModelScriptEditorApi",
      "symbol": "ModelScriptEditorApi",
      "intent": "scene",
      "code": "// 1) Open the Script Editor panel.\nawait scriptEditor.open();\nconsole.log('script editor panel open');\n// 2) Close it again (force-close - bypasses any toggle behavior).\nscriptEditor.close();\nconsole.log('script editor panel closed');\n// 3) Print the namespace help.\nconsole.log('help length: ' + scriptEditor.help().length);"
    },
    {
      "kind": "example",
      "id": "example:ModelScriptEditorApi.close",
      "symbol": "ModelScriptEditorApi.close",
      "intent": "scene",
      "code": "await scriptEditor.open();\nscriptEditor.close();\nconsole.log('script editor closed');"
    },
    {
      "kind": "example",
      "id": "example:ModelScriptEditorApi.example",
      "symbol": "ModelScriptEditorApi.example",
      "intent": "scene",
      "code": "console.log('scriptEditor example length: ' + scriptEditor.example().length);"
    },
    {
      "kind": "example",
      "id": "example:ModelScriptEditorApi.help",
      "symbol": "ModelScriptEditorApi.help",
      "intent": "scene",
      "code": "console.log('scriptEditor help length: ' + scriptEditor.help().length);"
    },
    {
      "kind": "example",
      "id": "example:ModelScriptEditorApi.isOpen",
      "symbol": "ModelScriptEditorApi.isOpen",
      "intent": "scene",
      "code": "// Drive the panel on the Model editor and read its open state.\nawait scriptEditor.open();\nconsole.log('script editor open:', scriptEditor.isOpen()); // true\nscriptEditor.close();\nconsole.log('script editor open:', scriptEditor.isOpen()); // false"
    },
    {
      "kind": "example",
      "id": "example:ModelScriptEditorApi.newTab",
      "symbol": "ModelScriptEditorApi.newTab",
      "intent": "scene",
      "code": "// 1. Setup — note the open state so the new tab is observable.\nconst wasOpen = scriptEditor.isOpen();\n// 2. Action — create a new AnimBox (JS) tab (the only language valid in\n//    every editor; openscad is Model-only and throws elsewhere).\nscriptEditor.newTab();\n// 3. Read-back — the panel is open with the new active tab.\nconsole.log('panel open after newTab: ' + scriptEditor.isOpen());\n// 4. Verify — a new animbox tab was created.\nconsole.log('created a new animbox tab (was open before: ' + wasOpen + ')');"
    },
    {
      "kind": "example",
      "id": "example:ModelScriptEditorApi.open",
      "symbol": "ModelScriptEditorApi.open",
      "intent": "scene",
      "code": "await scriptEditor.open();\nconsole.log('script editor opened');\nscriptEditor.close();"
    },
    {
      "kind": "example",
      "id": "example:ModelScriptEditorApi.run",
      "symbol": "ModelScriptEditorApi.run",
      "intent": "scene",
      "code": "// Run a bare-globals snippet and read its result without clicking Run.\nconst r = await scriptEditor.run('const c = await create.cube({}); return c.name;');\nconsole.log('run status: ' + r.status);\nconsole.log('returned mesh name: ' + r.returnValue);"
    },
    {
      "kind": "example",
      "id": "example:ModelToolApi.active",
      "symbol": "ModelToolApi.active",
      "intent": "scene",
      "code": "// 1) Start in default selection state and read the active tool.\ntool.setActive(null);\nconsole.log('initial active: ' + tool.active);\n// 2) Flip to Draw Curve and re-read.\ntool.setActive('drawline');\nconsole.log('after switch: ' + tool.active);\n// 3) Reset.\ntool.setActive(null);"
    },
    {
      "kind": "example",
      "id": "example:ModelToolApi.drawBox",
      "symbol": "ModelToolApi.drawBox",
      "intent": "scene",
      "code": "// 1) The headless builder is what scripts should use.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1 });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('headless cube verts: ' + cube.verts.count);\n// 2) Calling drawBox would throw — left commented for clarity.\n//    await tool.drawBox({});\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelToolApi.drawCircle",
      "symbol": "ModelToolApi.drawCircle",
      "intent": "scene",
      "code": "// 1) Activate the Draw Circle tool.\nawait tool.drawCircle({});\nconsole.log('draw-circle tool active');\n// 2) Return to default selection tool.\ntool.setActive(null);\nconsole.log('returned to selection tool');"
    },
    {
      "kind": "example",
      "id": "example:ModelToolApi.drawCylinder",
      "symbol": "ModelToolApi.drawCylinder",
      "intent": "scene",
      "code": "// 1) The headless builder is what scripts should use.\n//    (radiusTop / radiusBottom / radialSegments — there is no `radius`/`segments`.)\nconst cyl = await create.cylinder({\n    radiusTop: 0.5, radiusBottom: 0.5, height: 1, radialSegments: 24,\n});\ncyl.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('headless cylinder verts: ' + cyl.verts.count);\n// 2) Calling drawCylinder would throw — left commented for clarity.\n//    await tool.drawCylinder({});\n// 3) Clean up.\nawait cyl.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelToolApi.drawLine",
      "symbol": "ModelToolApi.drawLine",
      "intent": "scene",
      "code": "// 1) Activate the Draw Curve tool.\nawait tool.drawLine({});\nconsole.log('drawline active: ' + (tool.active === 'drawline'));\n// 2) Toggle off.\nawait tool.drawLine({});\nconsole.log('drawline active: ' + (tool.active === 'drawline'));"
    },
    {
      "kind": "example",
      "id": "example:ModelToolApi.drawRectangle",
      "symbol": "ModelToolApi.drawRectangle",
      "intent": "scene",
      "code": "// 1) Activate the Draw Rectangle tool.\nawait tool.drawRectangle({});\nconsole.log('draw-rectangle tool active');\n// 2) Return to default selection tool.\ntool.setActive(null);\nconsole.log('returned to selection tool');"
    },
    {
      "kind": "example",
      "id": "example:ModelToolApi.edgeSlide",
      "symbol": "ModelToolApi.edgeSlide",
      "intent": "scene",
      "code": "// 1) Build a subdivided plane (widthSegments / heightSegments — there is\n//    no `subdivisionsX`/`subdivisionsY` on plane) and select an interior edge.\nconst plane = await create.plane({\n    width: 1, height: 1, widthSegments: 2, heightSegments: 2,\n});\nplane.edges([0]).select();\n// 2) Enter the edge-slide tool.\nconst session = tool.edgeSlide({});\nconsole.log('edge-slide session is object: ' + (typeof session === 'object' && session !== null));\n// 3) Cancel without committing (no viewport pointer in scripts).\nsession.cancel();\nconsole.log('edge-slide cancelled');\n// 4) Clean up.\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelToolApi.example",
      "symbol": "ModelToolApi.example",
      "intent": "scene",
      "code": "const text = tool.example();\nconsole.log('example length: ' + text.length);"
    },
    {
      "kind": "example",
      "id": "example:ModelToolApi.help",
      "symbol": "ModelToolApi.help",
      "intent": "scene",
      "code": "const text = tool.help();\nconsole.log('help length: ' + text.length);"
    },
    {
      "kind": "example",
      "id": "example:ModelToolApi.setActive",
      "symbol": "ModelToolApi.setActive",
      "intent": "scene",
      "code": "// 1) Switch to the Draw Curve tool, then read the active getter.\ntool.setActive('drawline');\nconsole.log('active: ' + tool.active);\n// 2) Return to the default selection tool.\ntool.setActive(null);\nconsole.log('active: ' + tool.active);"
    },
    {
      "kind": "example",
      "id": "example:ModelToolSculptApi.enter",
      "symbol": "ModelToolSculptApi.enter",
      "intent": "scene",
      "code": "// 1) Spawn a cube and confirm it is the only selected node.\nconst cube = await create.cube({ width: 1, height: 1, depth: 1 });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconsole.log('selected count before sculpt: ' + ls({ selected: true }).length);\n// 2) Enter sculpt mode and capture the session handle.\nconst session = await tool.sculpt.enter();\nconsole.log('session is object: ' + (typeof session === 'object' && session !== null));\n// 3) Cancel (discard) so we leave the cube untouched.\nsession.cancel();\nconsole.log('exited sculpt mode without commit');\n// 4) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ModelToolSculptApi.example",
      "symbol": "ModelToolSculptApi.example",
      "intent": "scene",
      "code": "const text = tool.sculpt.example();\nconsole.log('example length: ' + text.length);"
    },
    {
      "kind": "example",
      "id": "example:ModelToolSculptApi.help",
      "symbol": "ModelToolSculptApi.help",
      "intent": "scene",
      "code": "const text = tool.sculpt.help();\nconsole.log('help length: ' + text.length);"
    },
    {
      "kind": "example",
      "id": "example:MorphTargetInfo",
      "symbol": "MorphTargetInfo",
      "intent": "scene",
      "code": "// 1) Build a subdivided cube off origin so deformations are visible.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1,\n    subdivisionsX: 4, subdivisionsY: 4, subdivisionsZ: 4,\n    name: 'deformers-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2) Stack up a Smooth then a Bend.\nconst smooth = await cube.deformers.add('smooth');\nconst bend = await cube.deformers.add('nonLinear', { mode: 'bend' });\n// 3) Tune them.\nsmooth.envelope.set(0.6);\nbend.curvature.set(45);\n// 4) Reorder — put bend first.\ncube.deformers.reorder(bend, 0);\nconsole.log('stack length: ' + cube.deformers.stack().length);\n// 5) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Node",
      "symbol": "Node",
      "intent": "scene",
      "code": "// 1. Setup - create a node via the create barrel.\nconst cube = await create.cube({ name: 'node-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Select - N/A; we have the handle.\n// 3. Action - read the stable id.\nconst id = cube.nodeId;\n// 4. Observe + cleanup.\nconsole.log('nodeId length: ' + id.length);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Node.align",
      "symbol": "Node.align",
      "intent": "mesh",
      "code": "// 1. Spawn three cubes in a row.\nconst a = await create.cube({ name: 'align-a' });\na.xform({ t: [2.5, 1.0, -0.7] });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ name: 'align-b' });\nb.xform({ t: [4.0, 1.5, -0.7] });\nawait select(null, { mode: 'clear' });\nconst c = await create.cube({ name: 'align-c' });\nc.xform({ t: [5.5, 0.8, -0.7] });\n// 2. Align all three to share the minimum Y.\na.align([b, c], { axis: 'y', mode: 'min' });\nconsole.log('a Y after align: ' + a.translate.y.get());\nconsole.log('b Y after align: ' + b.translate.y.get());\nconsole.log('c Y after align: ' + c.translate.y.get());\n// 3. Clean up.\nawait a.delete();\nawait b.delete();\nawait c.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.ancestors",
      "symbol": "Node.ancestors",
      "intent": "mesh",
      "code": "// 1. Build a 3-level hierarchy: outer group > inner group > cube.\nconst cube = await create.cube({ name: 'anc-cube' });\nconst inner = await create.group([cube], { name: 'anc-inner' });\nconst outer = await create.group([inner], { name: 'anc-outer' });\n// 2. Walk up from the cube.\nconst ancs = cube.ancestors().map((a) => a.name);\nconsole.log('cube ancestors: ' + JSON.stringify(ancs));\n// 3. Clean up.\nawait outer.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.centerPivot",
      "symbol": "Node.centerPivot",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + translate it so its pivot is off-center.\nconst cube = await create.cube({ width: 2, height: 2, depth: 2, name: 'pivot-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Recenter the pivot to the bounding-box center.\nawait cube.centerPivot();\nconsole.log('centerPivot ran on: ' + cube.name);\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.children",
      "symbol": "Node.children",
      "intent": "mesh",
      "code": "// 1. Build a group containing two cubes.\nconst a = await create.cube({ name: 'kid-a' });\na.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ name: 'kid-b' });\nb.xform({ t: [-2.0, 1.3, 0.7] });\nconst group = await create.group([a, b], { name: 'children-demo' });\n// 2. Iterate the children.\nconst names = group.children().map((n) => n.name).sort();\nconsole.log('children: ' + JSON.stringify(names));\n// 3. Clean up.\nawait group.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.delete",
      "symbol": "Node.delete",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + delete it.\nconst cube = await create.cube({ name: 'delete-demo' });\nawait cube.delete();\n// 2. Confirm the lookup returns nothing.\nconst lookup = ls({ name: 'delete-demo' });\nconsole.log('post-delete lookup length: ' + lookup.length);\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.descendants",
      "symbol": "Node.descendants",
      "intent": "mesh",
      "code": "// 1. Build a small hierarchy: group with two cubes.\nconst a = await create.cube({ name: 'desc-a' });\na.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ name: 'desc-b' });\nb.xform({ t: [-2.0, 1.3, 0.7] });\nconst group = await create.group([a, b], { name: 'desc-group' });\n// 2. All descendants, then filtered to meshes only.\nconst all = group.descendants();\nconst meshes = group.descendants((n) => n.nodeType === 'mesh');\nconsole.log('total descendants: ' + all.length);\nconsole.log('mesh descendants: ' + meshes.length);\n// 3. Clean up.\nawait group.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.distribute",
      "symbol": "Node.distribute",
      "intent": "mesh",
      "code": "// 1. Spawn three cubes at arbitrary X positions.\nconst a = await create.cube({ name: 'dist-a' });\na.xform({ t: [0, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ name: 'dist-b' });\nb.xform({ t: [1.7, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nconst c = await create.cube({ name: 'dist-c' });\nc.xform({ t: [10, 1.3, -0.7] });\n// 2. Distribute evenly between a (anchor) and c (last). b should land in the middle.\na.distribute([b, c], { axis: 'x' });\nconsole.log('b X after distribute: ' + b.translate.x.get());\n// 3. Use explicit spacing as the alternative form.\na.distribute([b, c], { axis: 'x', spacing: 2 });\nconsole.log('c X after explicit spacing: ' + c.translate.x.get());\n// 4. Clean up.\nawait a.delete();\nawait b.delete();\nawait c.delete();"
    },
    {
      "kind": "example",
      "id": "example:Node.duplicate",
      "symbol": "Node.duplicate",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + duplicate it.\nconst cube = await create.cube({ name: 'duplicate-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst dup = await cube.duplicate({ name: 'duplicate-demo-copy' });\n// 2. Translate the copy off the original so both are visible.\ndup.xform({ t: [4.0, 1.3, -0.7] });\nconsole.log('duplicate name: ' + dup.name);\n// 3. Clean up.\nawait cube.delete();\nawait dup.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.duplicateAlongCurve",
      "symbol": "Node.duplicateAlongCurve",
      "intent": "mesh",
      "code": "// 1. Spawn a post + a curved path.\nconst post = await create.cube({ name: 'fence-post' });\nconst path = await create.curve({ points: [[0, 0, 0], [3, 0, 1], [6, 0, 0]], degree: 3, name: 'fence-line' });\n// 2. Distribute 8 tangent-aligned copies along the path.\nconst first = await post.duplicateAlongCurve(path, { count: 8, align: true });\n// 3. Confirm a duplicate came back.\nconsole.log('first dup: ' + first.name);\n// 4. Clean up.\nawait path.delete();"
    },
    {
      "kind": "example",
      "id": "example:Node.equals",
      "symbol": "Node.equals",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + re-resolve it by name.\nconst a = await create.cube({ name: 'equals-demo' });\nconst b = ls({ name: 'equals-demo' })[0];\n// 2. Different handles, same underlying node.\nconsole.log('a equals b: ' + a.equals(b));\n// 3. Clean up.\nawait a.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.getCurve",
      "symbol": "Node.getCurve",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + record three keys on its X translation.\nconst cube = await create.cube({ name: 'getcurve-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\ncube.setKey('tx', 0, 2.5);\ncube.setKey('tx', 1, 5.0);\ncube.setKey('tx', 2, 2.5);\n// 2. Read the curve back and report the key count.\nconst curve = cube.getCurve('tx');\nconsole.log('curve numKeys: ' + (curve ? curve.numKeys : 'null'));\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.gridDuplicate",
      "symbol": "Node.gridDuplicate",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + grid it 3x1x3 with 2-unit spacing.\nconst cube = await create.cube({ name: 'grid-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst firstDup = await cube.gridDuplicate({\n    countX: 3, countY: 1, countZ: 3,\n    spacingX: 2, spacingY: 0, spacingZ: 2,\n    includeOriginal: true,\n});\nconsole.log('first dup name: ' + firstDup.name);\n// 2. Clean up.\nawait cube.delete();\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.hide",
      "symbol": "Node.hide",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + hide it.\nconst cube = await create.cube({ name: 'hide-demo' });\nawait cube.hide();\nconsole.log('isHidden after hide: ' + cube.isHidden);\n// 2. Clean up.\nawait cube.delete();\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.id",
      "symbol": "Node.id",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + read its id.\nconst cube = await create.cube({ name: 'id-demo' });\nconsole.log('cube id length: ' + cube.id.length);\n// 2. Rename + confirm the id is unchanged.\nconst before = cube.id;\ncube.rename('renamed');\nconsole.log('id stable across rename: ' + (cube.id === before));\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.instance",
      "symbol": "Node.instance",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + create an instance of it.\nconst cube = await create.cube({ name: 'instance-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst inst = await cube.instance();\n// 2. Translate the instance off the original so both are visible.\ninst.xform({ t: [4.0, 1.3, -0.7] });\nconsole.log('instance name: ' + inst.name);\n// 3. Clean up.\nawait cube.delete();\nawait inst.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.isHidden",
      "symbol": "Node.isHidden",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + observe the default visibility.\nconst cube = await create.cube({ name: 'ishidden-demo' });\nconsole.log('default isHidden: ' + cube.isHidden);\n// 2. Hide it; the flag flips.\nawait cube.hide();\nconsole.log('after hide: ' + cube.isHidden);\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.isLocked",
      "symbol": "Node.isLocked",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + read the default state.\nconst cube = await create.cube({ name: 'islocked-demo' });\nconsole.log('default isLocked: ' + cube.isLocked);\n// 2. Lock it + read again.\nawait cube.lock();\nconsole.log('after lock: ' + cube.isLocked);\n// 3. Clean up.\nawait cube.unlock();\nawait cube.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.lock",
      "symbol": "Node.lock",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + lock it.\nconst cube = await create.cube({ name: 'lock-demo' });\nawait cube.lock();\nconsole.log('isLocked after lock: ' + cube.isLocked);\n// 2. Clean up (unlock first so delete is not blocked).\nawait cube.unlock();\nawait cube.delete();\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.matchTransform",
      "symbol": "Node.matchTransform",
      "intent": "mesh",
      "code": "// 1. Spawn two cubes; b will copy a's transform.\nconst a = await create.cube({ name: 'match-a' });\na.xform({ t: [2.5, 1.3, -0.7], r: [30, 45, 60], s: [1.5, 1.5, 1.5] });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ name: 'match-b' });\n// 2. Copy a's TRS onto b in one undoable step.\nb.matchTransform(a);\nconsole.log('b translate after match: ' + JSON.stringify(b.translate.get()));\nconsole.log('b scale after match: ' + JSON.stringify(b.scale.get()));\n// 3. Clean up.\nawait a.delete();\nawait b.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.moveAfter",
      "symbol": "Node.moveAfter",
      "intent": "mesh",
      "code": "// 1. Spawn three cubes at the root in spawn order [a, b, c].\nconst a = await create.cube({ name: 'ma-a' });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ name: 'ma-b' });\nawait select(null, { mode: 'clear' });\nconst c = await create.cube({ name: 'ma-c' });\n// 2. Move a after c; outliner order becomes [b, c, a].\nawait a.moveAfter(c);\nconsole.log('moveAfter ran on: ' + a.name);\n// 3. Clean up.\nawait a.delete();\nawait b.delete();\nawait c.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.moveBefore",
      "symbol": "Node.moveBefore",
      "intent": "mesh",
      "code": "// 1. Spawn three cubes at the root in spawn order [a, b, c].\nconst a = await create.cube({ name: 'mb-a' });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ name: 'mb-b' });\nawait select(null, { mode: 'clear' });\nconst c = await create.cube({ name: 'mb-c' });\n// 2. Move c before a; outliner order becomes [c, a, b].\nawait c.moveBefore(a);\nconsole.log('moveBefore ran on: ' + c.name);\n// 3. Clean up.\nawait a.delete();\nawait b.delete();\nawait c.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.moveDown",
      "symbol": "Node.moveDown",
      "intent": "mesh",
      "code": "// 1. Spawn three cubes at the root in spawn order [a, b, c].\nconst a = await create.cube({ name: 'md-a' });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ name: 'md-b' });\nawait select(null, { mode: 'clear' });\nconst c = await create.cube({ name: 'md-c' });\n// 2. Bump a down; order becomes [b, a, c].\nawait a.moveDown();\nconsole.log('moveDown ran on: ' + a.name);\n// 3. Clean up.\nawait a.delete();\nawait b.delete();\nawait c.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.moveToIndex",
      "symbol": "Node.moveToIndex",
      "intent": "mesh",
      "code": "// 1. Spawn three cubes at the root in spawn order [a, b, c].\nconst a = await create.cube({ name: 'mi-a' });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ name: 'mi-b' });\nawait select(null, { mode: 'clear' });\nconst c = await create.cube({ name: 'mi-c' });\n// 2. Send c to index 0; order becomes [c, a, b].\nawait c.moveToIndex(0);\nconsole.log('moveToIndex ran on: ' + c.name);\n// 3. Clean up.\nawait a.delete();\nawait b.delete();\nawait c.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.moveUp",
      "symbol": "Node.moveUp",
      "intent": "mesh",
      "code": "// 1. Spawn three cubes at the root in spawn order [a, b, c].\nconst a = await create.cube({ name: 'mu-a' });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ name: 'mu-b' });\nawait select(null, { mode: 'clear' });\nconst c = await create.cube({ name: 'mu-c' });\n// 2. Bump c up; order becomes [a, c, b].\nawait c.moveUp();\nconsole.log('moveUp ran on: ' + c.name);\n// 3. Clean up.\nawait a.delete();\nawait b.delete();\nawait c.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.name",
      "symbol": "Node.name",
      "intent": "mesh",
      "code": "// 1. Spawn a cube with a known name.\nconst cube = await create.cube({ name: 'name-demo' });\nconsole.log('initial name: ' + cube.name);\n// 2. Rename and read again.\ncube.rename('renamed-cube');\nconsole.log('after rename: ' + cube.name);\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.nodeType",
      "symbol": "Node.nodeType",
      "intent": "mesh",
      "code": "// 1. Spawn one of each common kind + log the nodeType.\nconst cube = await create.cube({ name: 'type-demo' });\nawait select(null, { mode: 'clear' });\nconst group = await create.group([cube], { name: 'type-group' });\nconsole.log('cube nodeType: ' + cube.nodeType);\nconsole.log('group nodeType: ' + group.nodeType);\n// 2. Clean up.\nawait group.delete();\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.parent",
      "symbol": "Node.parent",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + group it to give the cube a parent.\nconst cube = await create.cube({ name: 'parent-demo' });\nconst group = await create.group([cube], { name: 'parent-group' });\n// 2. The cube's parent is the group; the group's parent is null (root).\nconsole.log('cube parent name: ' + (cube.parent()?.name ?? 'null'));\nconsole.log('group parent: ' + (group.parent() === null ? 'null' : group.parent().name));\n// 3. Clean up.\nawait group.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.radialDuplicate",
      "symbol": "Node.radialDuplicate",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + ring 8 copies around the Y axis at radius 3.\nconst cube = await create.cube({ name: 'radial-demo' });\nconst firstDup = await cube.radialDuplicate({\n    count: 8, axis: 'Y', angle: 360, radius: 3, includeOriginal: true,\n});\nconsole.log('first dup name: ' + firstDup.name);\n// 2. Clean up.\nawait cube.delete();\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.rename",
      "symbol": "Node.rename",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + rename it.\nconst cube = await create.cube({ name: 'rename-demo' });\nawait cube.rename('Hero');\nconsole.log('after first rename: ' + cube.name);\n// 2. A second rename to the same name on a fresh cube auto-suffixes.\nawait select(null, { mode: 'clear' });\nconst cube2 = await create.cube({ name: 'rename-demo-2' });\nawait cube2.rename('Hero');\nconsole.log('second rename auto-suffixed: ' + cube2.name);\n// 3. Clean up.\nawait cube.delete();\nawait cube2.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.setKey",
      "symbol": "Node.setKey",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + record three keys on translate.x.\nconst cube = await create.cube({ name: 'setkey-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\ncube.setKey('tx', 0, 0).setKey('tx', 1, 5).setKey('tx', 2, 0);\n// 2. Read the curve to confirm three keys landed.\nconst curve = cube.getCurve('tx');\nconsole.log('curve numKeys: ' + (curve ? curve.numKeys : 'null'));\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.setParent",
      "symbol": "Node.setParent",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + a group; cube starts at root.\nconst cube = await create.cube({ name: 'setparent-demo' });\nawait select(null, { mode: 'clear' });\nconst group = await create.group([], { name: 'setparent-group' });\n// 2. Move the cube under the group, then back to root.\nawait cube.setParent(group);\nconsole.log('after setParent(group): ' + (cube.parent()?.name ?? 'null'));\nawait cube.setParent(null);\nconsole.log('after setParent(null): ' + (cube.parent()?.name ?? 'null'));\n// 3. Clean up.\nawait cube.delete();\nawait group.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.show",
      "symbol": "Node.show",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + hide then show it.\nconst cube = await create.cube({ name: 'show-demo' });\nawait cube.hide();\nawait cube.show();\nconsole.log('isHidden after show: ' + cube.isHidden);\n// 2. Clean up.\nawait cube.delete();\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.snapTo",
      "symbol": "Node.snapTo",
      "intent": "mesh",
      "code": "// 1. Spawn two cubes at different positions.\nconst a = await create.cube({ name: 'snap-a' });\na.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ name: 'snap-b' });\nb.xform({ t: [-2.0, 1.3, 0.7] });\n// 2. Snap b onto a; read back to confirm.\nb.snapTo(a);\nconsole.log('b after snapTo a: ' + JSON.stringify(b.translate.get()));\n// 3. Clean up.\nawait a.delete();\nawait b.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.snapToNearest",
      "symbol": "Node.snapToNearest",
      "intent": "mesh",
      "code": "// 1. Spawn two cubes; b is the candidate, a is the snapper.\nconst a = await create.cube({ name: 'nearest-a' });\na.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ name: 'nearest-b' });\nb.xform({ t: [4.0, 1.3, -0.7] });\n// 2. Snap a to the nearest transform within range 3.\nconst hit = a.snapToNearest({ mode: 'transform', range: 3, checkOcclusion: false });\nconsole.log('nearest hit name: ' + (hit ? hit.hitNode?.name : 'null'));\nconsole.log('a position after snap: ' + JSON.stringify(a.translate.get()));\n// 3. Clean up.\nawait a.delete();\nawait b.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.toggleVisibility",
      "symbol": "Node.toggleVisibility",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + verify it starts visible.\nconst cube = await create.cube({ name: 'togglevis-demo' });\nconsole.log('initially hidden: ' + cube.isHidden);\n// 2. Toggle it off, then on.\nawait cube.toggleVisibility();\nconsole.log('after first toggle: ' + cube.isHidden);\nawait cube.toggleVisibility();\nconsole.log('after second toggle: ' + cube.isHidden);\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.toJSON",
      "symbol": "Node.toJSON",
      "intent": "mesh",
      "code": "// 1) Build a cube and read its JSON identity.\nconst cube = await create.cube({ name: 'json-demo' });\nconst j = cube.toJSON();\nconsole.log('name: ' + j.name); // -> 'json-demo'\nconsole.log('type: ' + j.type); // -> 'mesh'\nconsole.log('id is string: ' + (typeof j.id === 'string'));\n// 2) JSON.stringify uses toJSON automatically.\nconsole.log(JSON.stringify(cube)); // -> {\"name\":\"json-demo\",\"id\":\"...\",\"type\":\"mesh\"}\n// 3) Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Node.toString",
      "symbol": "Node.toString",
      "intent": "mesh",
      "code": "// 1. Spawn a cube + log its debug string.\nconst cube = await create.cube({ name: 'tostring-demo' });\nconsole.log('toString: ' + cube.toString());\n// 2. Clean up.\nawait cube.delete();\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.unlock",
      "symbol": "Node.unlock",
      "intent": "mesh",
      "code": "// 1. Spawn a cube, lock it, then unlock it.\nconst cube = await create.cube({ name: 'unlock-demo' });\nawait cube.lock();\nawait cube.unlock();\nconsole.log('isLocked after unlock: ' + cube.isLocked);\n// 2. Clean up.\nawait cube.delete();\n// 3. Action - see above.\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:Node.unparent",
      "symbol": "Node.unparent",
      "intent": "mesh",
      "code": "// 1. Build a group containing a cube.\nconst cube = await create.cube({ name: 'unparent-demo' });\nconst group = await create.group([cube], { name: 'unparent-group' });\nconsole.log('cube parent before: ' + (cube.parent()?.name ?? 'null'));\n// 2. Move the cube back out to the scene root.\nawait cube.unparent();\nconsole.log('cube parent after: ' + (cube.parent()?.name ?? 'null'));\n// 3. Clean up.\nawait cube.delete();\nawait group.delete();\n// 4. Observe - see above."
    },
    {
      "kind": "example",
      "id": "example:PathResidualApi.basename",
      "symbol": "PathResidualApi.basename",
      "intent": "scene",
      "code": "// 1. Plain basename of a nested path.\nconsole.log('base = ' + Utils.path.basename('/a/b/c.fbx'));\n// 2. Strip a matching extension.\nconsole.log('no ext = ' + Utils.path.basename('/a/b/c.fbx', '.fbx'));\n// 3. A non-matching extension is left intact.\nconsole.log('mismatch = ' + Utils.path.basename('/a/b/c.fbx', '.glb'));\n// 4. A path with no slash returns the whole string.\nconsole.log('no slash = ' + Utils.path.basename('file.glb'));"
    },
    {
      "kind": "example",
      "id": "example:PathResidualApi.extname",
      "symbol": "PathResidualApi.extname",
      "intent": "scene",
      "code": "// 1. Extension of a normal file.\nconsole.log('ext = ' + Utils.path.extname('/a/b/c.glb'));\n// 2. Only the LAST dot counts (Node parity).\nconsole.log('last dot = ' + Utils.path.extname('archive.tar.gz'));\n// 3. A file with no dot has no extension.\nconsole.log('none = \"' + Utils.path.extname('README') + '\"');\n// 4. A leading-dot file has no extension (Node parity).\nconsole.log('dotfile = \"' + Utils.path.extname('.glb') + '\"');"
    },
    {
      "kind": "example",
      "id": "example:Plane.clone",
      "symbol": "Plane.clone",
      "intent": "math",
      "code": "// 1) Build a plane and clone it.\nconst a = MathLib.plane([0, 1, 0], 0);\nconst b = a.clone();\n// 2) The clone compares equal but is a separate instance.\nconsole.log('equals: ' + a.equals(b));\n// 3) Negating the clone leaves the original alone.\nconst flipped = b.negate();\nconsole.log('original normal: ' + JSON.stringify(a.normal.toArray()));\nconsole.log('flipped normal: ' + JSON.stringify(flipped.normal.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:Plane.constant",
      "symbol": "Plane.constant",
      "intent": "math",
      "code": "// 1) Plane offset 5 units in +X.\nconst wall = MathLib.plane([1, 0, 0], -5);\n// 2) Constant matches the offset.\nconsole.log('constant: ' + wall.constant);"
    },
    {
      "kind": "example",
      "id": "example:Plane.distanceToPoint",
      "symbol": "Plane.distanceToPoint",
      "intent": "math",
      "code": "// 1) Ground plane (Y up).\nconst ground = MathLib.plane([0, 1, 0], 0);\n// 2) Point above the plane.\nconsole.log('above: ' + ground.distanceToPoint([0, 5, 0]));\n// 3) Point below the plane.\nconsole.log('below: ' + ground.distanceToPoint([0, -3, 0]));\n// 4) Point on the plane.\nconsole.log('on plane: ' + ground.distanceToPoint([4, 0, -2]));"
    },
    {
      "kind": "example",
      "id": "example:Plane.equals",
      "symbol": "Plane.equals",
      "intent": "math",
      "code": "// 1) Two equivalent planes.\nconst a = MathLib.plane([0, 1, 0], 0);\nconst b = MathLib.plane([0, 1, 0], 0);\nconsole.log('equal exact: ' + a.equals(b));\n// 2) Small float drift inside default epsilon.\nconst c = MathLib.plane([0, 1, 1e-9], 0);\nconsole.log('equal within default eps: ' + a.equals(c));\n// 3) Tightening epsilon catches the drift.\nconsole.log('equal within tight eps: ' + a.equals(c, 1e-12));"
    },
    {
      "kind": "example",
      "id": "example:Plane.fromPointAndNormal",
      "symbol": "Plane.fromPointAndNormal",
      "intent": "math",
      "code": "// 1) Build a plane through (0, 2, 0) facing up.\nconst plane = Plane.fromPointAndNormal([0, 2, 0], [0, 1, 0]);\nconsole.log('normal: ' + JSON.stringify(plane.normal.toArray()));\nconsole.log('constant: ' + plane.constant);\n// 2) Confirm the seed point lies on the plane.\nconsole.log('distance at seed (~0): ' + plane.distanceToPoint([0, 2, 0]).toFixed(6));\n// 3) Non-unit normals are auto-normalized.\nconst stretched = Plane.fromPointAndNormal([0, 0, 0], [0, 3, 0]);\nconsole.log('auto-normalized: ' + stretched.normal.length().toFixed(3));"
    },
    {
      "kind": "example",
      "id": "example:Plane.fromThreePoints",
      "symbol": "Plane.fromThreePoints",
      "intent": "math",
      "code": "// 1) Three points on the XZ plane at y = 2.\nconst plane = Plane.fromThreePoints([0, 2, 0], [1, 2, 0], [0, 2, 1]);\n// 2) The derived normal points +Y.\nconst r = plane.normal.toArray().map(v => Math.round(v * 1000) / 1000);\nconsole.log('normal: ' + JSON.stringify(r));\n// 3) Each seed point is on the plane.\nconsole.log('distance at a: ' + plane.distanceToPoint([0, 2, 0]).toFixed(6));"
    },
    {
      "kind": "example",
      "id": "example:Plane.intersectsLine",
      "symbol": "Plane.intersectsLine",
      "intent": "math",
      "code": "// 1) Ground plane.\nconst ground = MathLib.plane([0, 1, 0], 0);\n// 2) Vertical line that crosses the plane.\nconst hit = ground.intersectsLine({ start: [3, 5, 1], end: [3, -2, 1] });\nconsole.log('hit: ' + (hit !== null ? JSON.stringify(hit.toArray()) : 'null'));\n// 3) Line parallel to the plane — no crossing.\nconst miss = ground.intersectsLine({ start: [0, 4, 0], end: [10, 4, 0] });\nconsole.log('miss: ' + (miss === null ? 'null' : JSON.stringify(miss.toArray())));"
    },
    {
      "kind": "example",
      "id": "example:Plane.negate",
      "symbol": "Plane.negate",
      "intent": "math",
      "code": "// 1) Ground plane (Y up).\nconst ground = MathLib.plane([0, 1, 0], 0);\n// 2) Flip it so the \"front\" side is below the plane.\nconst flipped = ground.negate();\nconsole.log('flipped normal: ' + JSON.stringify(flipped.normal.toArray()));\n// 3) Distances change sign.\nconsole.log('ground above (5): ' + ground.distanceToPoint([0, 5, 0]));\nconsole.log('flipped above (-5): ' + flipped.distanceToPoint([0, 5, 0]));"
    },
    {
      "kind": "example",
      "id": "example:Plane.normal",
      "symbol": "Plane.normal",
      "intent": "math",
      "code": "// 1) Read the normal of the ground plane.\nconst ground = MathLib.plane([0, 1, 0], 0);\nconsole.log('normal: ' + JSON.stringify(ground.normal.toArray()));\n// 2) Each access returns a fresh copy.\nconst n = ground.normal;\nn.x = 999;\nconsole.log('plane normal unchanged: ' + JSON.stringify(ground.normal.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:Plane.normalize",
      "symbol": "Plane.normalize",
      "intent": "math",
      "code": "// 1) Build a plane with a 3x scaled normal.\nconst stretched = new MathLib.Plane([0, 3, 0], 6);\nconsole.log('initial normal length: ' + stretched.normal.length().toFixed(3));\n// 2) Normalize.\nconst unit = stretched.normalize();\nconsole.log('after normalize length: ' + unit.normal.length().toFixed(3));\n// 3) The plane geometry is unchanged — both planes pass through y=2.\nconsole.log('original distance at y=2: ' + stretched.distanceToPoint([0, 2, 0]).toFixed(3));\nconsole.log('normalized distance at y=2: ' + unit.distanceToPoint([0, 2, 0]).toFixed(3));"
    },
    {
      "kind": "example",
      "id": "example:Plane.projectPoint",
      "symbol": "Plane.projectPoint",
      "intent": "math",
      "code": "// 1) Ground plane.\nconst ground = MathLib.plane([0, 1, 0], 0);\n// 2) Project a point straight down onto it.\nconst proj = ground.projectPoint([4, 7, -2]);\nconsole.log('projected: ' + JSON.stringify(proj.toArray()));\n// 3) The projection sits on the plane (distance ~0).\nconsole.log('distance to projected: ' + ground.distanceToPoint(proj).toFixed(6));"
    },
    {
      "kind": "example",
      "id": "example:Plane.toArray",
      "symbol": "Plane.toArray",
      "intent": "math",
      "code": "// 1) Build the ground plane.\nconst ground = MathLib.plane([0, 1, 0], 0);\n// 2) Serialize to a plain object.\nconst obj = ground.toArray();\nconsole.log('plane: ' + JSON.stringify(obj));\n// 3) The returned arrays are copies — mutating them does not change the plane.\nobj.normal[1] = 999;\nconsole.log('plane unchanged: ' + JSON.stringify(ground.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:PolyPenOpts.mode",
      "symbol": "PolyPenOpts.mode",
      "intent": "scene",
      "code": "// 1) Start a PolyPen session in tri-fill mode against a cube ref surface.\nconst ref = await create.cube({ width: 1, height: 1, depth: 1, name: 'polypen-mode' });\nref.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await tool.polyPen.start({ referenceMesh: ref, mode: 'tri', worldCoords: true });\nconsole.log('active mode: ' + session.mode);\nawait session.cancel();\nawait ref.delete();"
    },
    {
      "kind": "example",
      "id": "example:PolyPenOpts.symmetry",
      "symbol": "PolyPenOpts.symmetry",
      "intent": "scene",
      "code": "// 1) Start PolyPen with X-axis symmetry.\nconst ref = await create.cube({ width: 1, height: 1, depth: 1, name: 'polypen-sym' });\nref.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await tool.polyPen.start({ referenceMesh: ref, symmetry: 'x', worldCoords: true });\nconsole.log('symmetry axis: ' + session.symmetry);\nawait session.cancel();\nawait ref.delete();"
    },
    {
      "kind": "example",
      "id": "example:PolyPenOpts.worldCoords",
      "symbol": "PolyPenOpts.worldCoords",
      "intent": "scene",
      "code": "// 1) Headless retopo — set worldCoords so we can pass literal positions.\nconst ref = await create.cube({ width: 1, height: 1, depth: 1, name: 'polypen-world' });\nref.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await tool.polyPen.start({ referenceMesh: ref, worldCoords: true });\nconsole.log('worldCoords on: ' + session.worldCoords);\nawait session.cancel();\nawait ref.delete();"
    },
    {
      "kind": "example",
      "id": "example:PolyPenSession.example",
      "symbol": "PolyPenSession.example",
      "intent": "scene",
      "code": "// 1) Build a plane and open a poly-pen session.\nconst plane = await create.plane({ width: 2, height: 2, name: 'polypen-example-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await plane.tools.polyPen({ mode: 'quad', worldCoords: true }));\n// 2) Render the example snippet.\nconst snippet = session.example();\nconsole.log('example length: ' + snippet.length);\n// 3) Tear down.\nawait session.cancel();\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:PolyPenSession.help",
      "symbol": "PolyPenSession.help",
      "intent": "scene",
      "code": "// 1) Build a plane and open a poly-pen session.\nconst plane = await create.plane({ width: 2, height: 2, name: 'polypen-help-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await plane.tools.polyPen({ mode: 'quad', worldCoords: true }));\n// 2) Render the help string.\nconst helpText = session.help();\nconsole.log('help length: ' + helpText.length);\n// 3) Tear down.\nawait session.cancel();\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:PolyPenSession.placeEdge",
      "symbol": "PolyPenSession.placeEdge",
      "intent": "scene",
      "code": "// 1) Build a plane and open a world-coords poly-pen session.\nconst plane = await create.plane({ width: 2, height: 2, name: 'polypen-placeedge-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await plane.tools.polyPen({ mode: 'quad', worldCoords: true }));\n// 2) Seed a vertex, then extend an edge into a new point in world space.\nsession.placeVertex([2.6, 1.3, -0.7]);\nsession.placeEdge([2.7, 1.3, -0.7]);\nconsole.log('placeEdge called');\n// 3) Tear down without committing.\nawait session.cancel();\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:PolyPenSession.placeFace",
      "symbol": "PolyPenSession.placeFace",
      "intent": "scene",
      "code": "// 1) Build a plane and open a world-coords poly-pen session.\nconst plane = await create.plane({ width: 2, height: 2, name: 'polypen-placeface-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await plane.tools.polyPen({ mode: 'quad', worldCoords: true }));\n// 2) Stamp a single quad in world space using 4 corners.\nsession.placeFace([\n    [2.5, 1.3, -0.7],\n    [2.6, 1.3, -0.7],\n    [2.6, 1.3, -0.6],\n    [2.5, 1.3, -0.6],\n]);\nconsole.log('placeFace called');\n// 3) Tear down without committing.\nawait session.cancel();\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:PolyPenSession.placeTriangleFromEdge",
      "symbol": "PolyPenSession.placeTriangleFromEdge",
      "intent": "scene",
      "code": "// 1) Build a plane and open a world-coords poly-pen session.\nconst plane = await create.plane({ width: 2, height: 2, name: 'polypen-tri-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await plane.tools.polyPen({ mode: 'tri', worldCoords: true }));\n// 2) Seed two vertices, then call placeTriangleFromEdge with the resulting edgeId.\n//    (Use a real edgeId from your retopo session in production.)\nsession.placeVertex([2.5, 1.3, -0.7]);\nsession.placeVertex([2.6, 1.3, -0.7]);\nconsole.log('placeTriangleFromEdge entry available');\n// 3) Tear down without committing.\nawait session.cancel();\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:PolyPenSession.placeVertex",
      "symbol": "PolyPenSession.placeVertex",
      "intent": "scene",
      "code": "// 1) Build a plane and open a world-coords poly-pen session.\nconst plane = await create.plane({ width: 2, height: 2, name: 'polypen-placevert-demo' });\nplane.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await plane.tools.polyPen({ mode: 'quad', worldCoords: true }));\n// 2) Drop a seed vertex in world space (worldCoords skips the surface raycast).\nsession.placeVertex([2.6, 1.3, -0.7]);\nconsole.log('placeVertex called');\n// 3) Tear down without committing.\nawait session.cancel();\nawait plane.delete();"
    },
    {
      "kind": "example",
      "id": "example:PreviewBarrelResidualApi.getMaterial",
      "symbol": "PreviewBarrelResidualApi.getMaterial",
      "intent": "scene",
      "code": "// 1. Switch to the Previewer where this accessor is mounted.\nawait Utils.editor.switchTo('previewer');\nconsole.log('mode: ' + Utils.editor.current());\n// 2. Create a named material to look up.\nawait create.material({ name: 'pv-getmat-demo' });\nconsole.log('created pv-getmat-demo');\n// 3. Fetch it back BY NAME and confirm.\nconst mat = getMaterial('pv-getmat-demo');\nconsole.log('found: ' + (mat !== null));\n// 4. An unknown name returns null.\nconsole.log('missing is null: ' + (getMaterial('no-such-material') === null));"
    },
    {
      "kind": "example",
      "id": "example:PreviewBarrelResidualApi.getTexture",
      "symbol": "PreviewBarrelResidualApi.getTexture",
      "intent": "scene",
      "code": "// 1. Switch to the Previewer (textures are Previewer-only).\nawait Utils.editor.switchTo('previewer');\nconsole.log('mode: ' + Utils.editor.current());\n// 2. Enumerate the textures the loaded asset already carries.\nconst all = listTextures();\nconsole.log('texture count = ' + all.length);\n// 3. Resolve the first texture BY NAME (guarded for the empty case).\nconst tex = all.length > 0 ? getTexture(all[0].name) : null;\nconsole.log('resolved: ' + (tex !== null));\n// 4. An unknown name always returns null.\nconsole.log('missing is null: ' + (getTexture('no-such-texture') === null));"
    },
    {
      "kind": "example",
      "id": "example:PreviewBarrelResidualApi.getTextureBlob",
      "symbol": "PreviewBarrelResidualApi.getTextureBlob",
      "intent": "scene",
      "code": "// 1. Switch to the Previewer (texture blobs are Previewer-only).\nawait Utils.editor.switchTo('previewer');\nconsole.log('mode: ' + Utils.editor.current());\n// 2. Find the textures present so we know what to ask for.\nconst all = listTextures();\nconsole.log('texture count = ' + all.length);\n// 3. Fetch the first texture's blob BY NAME (guarded for empty).\nconst blob = all.length > 0 ? await getTextureBlob(all[0].name) : null;\nconsole.log('blob bytes = ' + (blob ? blob.size : 0));\n// 4. An unknown name resolves to null.\nconsole.log('missing is null: ' + ((await getTextureBlob('no-such-texture')) === null));"
    },
    {
      "kind": "example",
      "id": "example:PreviewBarrelResidualApi.history",
      "symbol": "PreviewBarrelResidualApi.history",
      "intent": "scene",
      "code": "// 1. Switch to the Previewer where this barrel is mounted.\nawait Utils.editor.switchTo('previewer');\nconsole.log('mode: ' + Utils.editor.current());\n// 2. Read the Previewer history depth.\nconsole.log('size = ' + history.size());\n// 3. Query undo availability.\nconsole.log('canUndo = ' + history.canUndo());\n// 4. Inspect the most recent command label (or null when empty).\nconsole.log('last = ' + JSON.stringify(history.lastCommand()));"
    },
    {
      "kind": "example",
      "id": "example:PreviewBarrelResidualApi.listMaterials",
      "symbol": "PreviewBarrelResidualApi.listMaterials",
      "intent": "scene",
      "code": "// 1. Switch to the Previewer where this accessor is mounted.\nawait Utils.editor.switchTo('previewer');\nconsole.log('mode: ' + Utils.editor.current());\n// 2. Read the current material count.\nconst before = listMaterials().length;\nconsole.log('before = ' + before);\n// 3. Create a material and re-read the count.\nawait create.material({ name: 'pv-listmat-demo' });\nconsole.log('after = ' + listMaterials().length);\n// 4. Inspect the first DTO's name, if any.\nconst list = listMaterials();\nconsole.log('first = ' + (list.length > 0 ? list[0].name : 'none'));"
    },
    {
      "kind": "example",
      "id": "example:PreviewBarrelResidualApi.listTextures",
      "symbol": "PreviewBarrelResidualApi.listTextures",
      "intent": "scene",
      "code": "// 1. Switch to the Previewer (textures are Previewer-only).\nawait Utils.editor.switchTo('previewer');\nconsole.log('mode: ' + Utils.editor.current());\n// 2. Read the texture count off the list getter.\nconsole.log('count = ' + listTextures().length);\n// 3. Capture the list once and inspect the first entry's name.\nconst list = listTextures();\nconsole.log('first = ' + (list.length > 0 ? list[0].name : 'none'));\n// 4. Confirm the getter hands back a real array each call.\nconsole.log('is array: ' + Array.isArray(listTextures()));"
    },
    {
      "kind": "example",
      "id": "example:PreviewBarrelResidualApi.scene",
      "symbol": "PreviewBarrelResidualApi.scene",
      "intent": "scene",
      "code": "// 1. Switch to the Previewer where this barrel is mounted.\nawait Utils.editor.switchTo('previewer');\nconsole.log('mode: ' + Utils.editor.current());\n// 2. List every node currently in the Previewer scene.\nconsole.log('node count = ' + ls().length);\n// 3. Confirm the accessor hands back a real array.\nconsole.log('is array: ' + Array.isArray(ls()));\n// 4. Count only the currently-selected nodes.\nconsole.log('selected = ' + ls({ selected: true }).length);"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi",
      "symbol": "PreviewEditorAnimationApi",
      "intent": "scene",
      "code": "// 1. Setup — capture the current transport state so the example can\n//    restore it cleanly. Previewer operates on whatever asset is loaded.\nconst startedPlaying = animation.isPlaying();\nconst startSpeed = animation.getSpeed();\nconst startLoop = animation.getLoopMode();\n// 2. Select — transport ops target the implicit timeline cursor + active\n//    clip. Gate on the native clip count (NOT ls({type:'animClip'}),\n//    which throws in the Previewer SE).\nconst clipCount = animation.getClipCount();\n// 3. Action — seek to t=0, pick the first clip if any, slow to half\n//    speed, switch to ping-pong, restrict to frames 0..30, step forward.\nanimation.currentTime(0);\nif (clipCount > 0)\n    animation.setClip(0);\nanimation.setSpeed(0.5);\nanimation.setLoopMode('pingpong');\nanimation.playbackRange(0, 30);\nawait animation.nextFrame();\n// 4. Observe — sample Hips translate at t=0 (non-seek read) and verify\n//    the speed/loop changes landed.\nconst t = animation.sampleAt('Hips', 'translate', 0);\nconsole.log('clips: ' + clipCount + ', Hips translate@0: ' + JSON.stringify(t));\nconsole.log('half-speed set: ' + (animation.getSpeed() === 0.5) + ', pingpong set: ' + (animation.getLoopMode() === 'pingpong'));\n// Restore the captured transport state so the example leaves no trace.\nanimation.setSpeed(startSpeed);\nanimation.setLoopMode(startLoop);\n// Resume whichever play/pause state we started in.\nif (startedPlaying)\n    animation.play();\nelse\n    animation.pause();\nconsole.log('transport restored');"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.currentTime",
      "symbol": "PreviewEditorAnimationApi.currentTime",
      "intent": "scene",
      "code": "// 1. Setup — Previewer operates on the loaded asset; capture the\n//    starting cursor so we can restore it at the end.\nconst t0 = animation.currentTime();\n// 2. Select — transport scalar; the cursor is the implicit target.\n//    No per-node selection is needed.\n// 3. Action — seek to t=0. The setter returns the requested value\n//    verbatim and the next read confirms it round-trips.\nanimation.currentTime(0);\n// 4. Observe — round-trip read, then restore the original cursor.\nconsole.log('cursor after seek: ' + animation.currentTime());\nanimation.currentTime(t0);\nconsole.log('cursor restored: ' + (animation.currentTime() === t0));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.deleteClip",
      "symbol": "PreviewEditorAnimationApi.deleteClip",
      "intent": "scene",
      "code": "// 1. Setup — capture the clip count before deletion.\nconst before = animation.getClipCount();\n//\n// 2. Select — no selection required; the active clip is implicit\n//    (pass { clipId } to target a specific clip).\n//\n// 3. Action — delete the active clip (one undo step).\nawait animation.deleteClip();\n//\n// 4. Observe — verify exactly one clip was removed.\nconst after = animation.getClipCount();\nconsole.log('deleted one clip: ' + (before - after === 1));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.example",
      "symbol": "PreviewEditorAnimationApi.example",
      "intent": "scene",
      "code": "// 1. Setup — pure read; example() is a universal namespace op.\n// 2. Select — namespace-scoped read; animation namespace targets.\n// 3. Action — request the namespace-level example source.\nconst exampleText = animation.example();\n// 4. Observe — confirm the result is a non-empty string.\nconsole.log('example is string: ' + (typeof exampleText === 'string'));\nconsole.log('example length positive: ' + (exampleText.length > 0));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.frameRange",
      "symbol": "PreviewEditorAnimationApi.frameRange",
      "intent": "scene",
      "code": "// 1. Setup — graph-editor framing is a camera-state op; no asset\n//    bootstrap is required and no transport mutation occurs.\n// 2. Select — graph editor is the implicit target; the active\n//    clip's playback range is what frames the camera.\n// 3. Action — frame the playback range. Camera state is not\n//    undoable (matches the UI's framing control).\nawait animation.frameRange();\n// 4. Observe — there's no return value; confirm the call resolved.\nconsole.log('frameRange invoked');"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.getActiveClip",
      "symbol": "PreviewEditorAnimationApi.getActiveClip",
      "intent": "scene",
      "code": "// 1. Setup — discover the clips on the asset (tolerates empty via []).\nconst clips = animation.listClips();\n// Empty-asset case: getActiveClip returns null.\nif (clips.length === 0) {\n    console.log('no clips — getActiveClip returns null');\n    console.log('null ok: ' + (animation.getActiveClip() === null));\n}\nelse {\n    // 2. Select — pick the first clip BY NAME so we know what should become active.\n    const wantName = clips[0].name;\n    // 3. Action — make it active, then read the active descriptor back.\n    animation.setClip(wantName);\n    const clip = animation.getActiveClip();\n    // 4. Observe — prove getActiveClip returns the clip we selected with a\n    //    coherent descriptor (frameCount/fps positive).\n    console.log('active clip: ' + JSON.stringify(clip));\n    // The active clip's name must equal the one we selected.\n    console.log('matches selection: ' + (clip?.name === wantName));\n    console.log('descriptor coherent: ' + (!!clip && clip.frameCount > 0 && clip.fps > 0));\n}"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.getClipCount",
      "symbol": "PreviewEditorAnimationApi.getClipCount",
      "intent": "scene",
      "code": "// 1. Setup — pure read across all clip kinds (0 when the asset is empty).\n// 2. Select — the animation library is the implicit target.\n// 3. Action — read the total, then the three per-kind splits.\nconst total = animation.getClipCount();\nconst skel = animation.listSkeletonClips().length;\nconst vert = animation.listVertexClips().length;\nconst rig = animation.listControlRigClips().length;\n// 4. Observe — prove the per-kind splits partition the total exactly.\nconsole.log('total clips: ' + total + ' (skeleton ' + skel + ', vertex ' + vert + ', controlRig ' + rig + ')');\nconsole.log('splits sum to total: ' + (skel + vert + rig === total));\nconsole.log('matches listClips: ' + (total === animation.listClips().length));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.getFrameRange",
      "symbol": "PreviewEditorAnimationApi.getFrameRange",
      "intent": "scene",
      "code": "// 1. Setup — fetch the active clip's frame-range descriptor (null when\n//    no clip is active).\nconst range = animation.getFrameRange();\nif (range === null) {\n    console.log('no active clip — nothing to frame');\n}\nelse {\n    // 2. Select — the active clip is the implicit target.\n    console.log('range: ' + JSON.stringify(range) + ', fps positive: ' + (range.fps > 0));\n    // 3. Action — use the descriptor to clamp playback to the clip's real\n    //    span (getFrameRange's documented companion is playbackRange).\n    const end = Math.min(60, range.frameCount);\n    animation.playbackRange(0, end);\n    // 4. Observe — confirm the range was self-consistent (frameCount ~= duration*fps).\n    console.log('clamped playback to 0..' + end + ' of ' + range.frameCount + ' frames');\n    console.log('frameCount coheres with duration*fps: ' + (Math.abs(range.frameCount - range.duration * range.fps) < 1));\n}"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.getLoopMode",
      "symbol": "PreviewEditorAnimationApi.getLoopMode",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the live loop mode so we can restore it.\nconst before = animation.getLoopMode();\nconsole.log('loop mode before: ' + before);\n// 2. Select — scalar read; the transport is the implicit target.\n// 3. Action — switch to the OPPOSITE mode via the sibling setter, then\n//    read it back so getLoopMode proves a real state change.\nconst target = before === 'loop' ? 'pingpong' : 'loop';\nanimation.setLoopMode(target);\nconst after = animation.getLoopMode();\n// 4. Observe — confirm getLoopMode reflected the set value, then restore.\nconsole.log('getLoopMode after setLoopMode(' + target + '): ' + after);\nconsole.log('round-trip ok: ' + (after === target));\nanimation.setLoopMode(before);\nconsole.log('loop mode restored: ' + (animation.getLoopMode() === before));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.getPlaybackRange",
      "symbol": "PreviewEditorAnimationApi.getPlaybackRange",
      "intent": "scene",
      "code": "// 1. Setup — read the current playback range so the change below is\n//    observable against a known starting point.\nconst before = animation.getPlaybackRange();\nconsole.log('range before: ' + JSON.stringify(before));\n// 2. Select — scalar read; the transport is the implicit target.\n// 3. Action — set a custom range via the sibling setter, then read it\n//    back so getPlaybackRange proves a real state change.\nanimation.playbackRange(0, 30);\nconst after = animation.getPlaybackRange();\n// 4. Observe — confirm the setter's values round-tripped and\n//    custom-range mode switched on.\nconsole.log('getPlaybackRange after playbackRange(0, 30): ' + JSON.stringify(after));\nconsole.log('round-trip ok: ' + (after.start === 0 && after.end === 30 && after.custom === true));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.getSpeed",
      "symbol": "PreviewEditorAnimationApi.getSpeed",
      "intent": "scene",
      "code": "// 1. Setup — capture the live speed so we can restore it (transport\n//    state survives across script runs).\nconst before = animation.getSpeed();\nconsole.log('speed before: ' + before);\n// 2. Select — scalar read; the transport is the implicit target.\n// 3. Action — set a known multiplier via the sibling setter, then read\n//    it back with getSpeed so the read proves a real value, not a type.\nanimation.setSpeed(1.5);\nconst after = animation.getSpeed();\n// 4. Observe — confirm getSpeed reflected the set value, then restore.\nconsole.log('getSpeed after setSpeed(1.5): ' + after);\nconsole.log('round-trip ok: ' + (after === 1.5) + ', positive: ' + (after > 0));\nanimation.setSpeed(before);\nconsole.log('speed restored: ' + (animation.getSpeed() === before));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.help",
      "symbol": "PreviewEditorAnimationApi.help",
      "intent": "scene",
      "code": "// 1. Setup — pure read; help/example are universal namespace ops\n//    and never depend on transport state.\n// 2. Select — namespace-scoped read; the animation namespace is\n//    the implicit target.\n// 3. Action — request the help string.\nconst helpText = animation.help();\n// 4. Observe — assert the string is non-empty and labels itself.\nconsole.log('help length positive: ' + (helpText.length > 0));\nconsole.log('help mentions namespace: ' + helpText.includes('animation'));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.importClip",
      "symbol": "PreviewEditorAnimationApi.importClip",
      "intent": "scene",
      "code": "// 1. Setup — a character must be loaded; capture the clip count.\nconst before = animation.getClipCount();\n//\n// 2. Select — the loaded character is the implicit import target; the\n//    picker opens when no file is supplied.\n//\n// 3. Action — import clip(s) from the chosen FBX/GLB (one undo step).\nconst result = await animation.importClip();\n//\n// 4. Observe — log the imported clip names + the new count.\nconsole.log('imported ' + result.count + ' clip(s): ' + result.clipNames);\nconsole.log('clips now: ' + animation.getClipCount() + ' (was ' + before + ')');"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.isPlaying",
      "symbol": "PreviewEditorAnimationApi.isPlaying",
      "intent": "scene",
      "code": "// 1. Setup — read the initial state. Pure observer, no preconditions.\n// 2. Select — read scalar; the transport is the implicit target.\n// 3. Action — drive the transport through play -> pause so we can\n//    observe isPlaying changing.\nconsole.log('isPlaying initial: ' + animation.isPlaying());\nanimation.play();\nawait Utils.wait.frame();\nconst duringPlay = animation.isPlaying();\nanimation.pause();\nawait Utils.wait.frame();\n// 4. Observe — confirm the transition fired in both directions.\nconsole.log('isPlaying during play: ' + duringPlay);\nconsole.log('isPlaying after pause: ' + animation.isPlaying());"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.listAllClips",
      "symbol": "PreviewEditorAnimationApi.listAllClips",
      "intent": "scene",
      "code": "// 1. Setup — enumerate every clip regardless of kind (tolerates empty []).\nconst all = animation.listAllClips();\n// Log the count + the distinct kinds present.\nconsole.log('total clips: ' + all.length + ', kinds: ' + JSON.stringify([...new Set(all.map((c) => c.kind))]));\n// 2. Select — pick the first SKELETON clip (the playable kind) if present.\nconst playable = all.find((c) => c.kind === 'skeleton') ?? all[0];\n// Gate on the empty-asset case.\nif (!playable) {\n    console.log('no clips to drive');\n}\nelse {\n    // 3. Action — feed the discovered name straight into setClip + seek t=0.\n    animation.setClip(playable.name);\n    animation.currentTime(0);\n    // 4. Observe — prove the enumerated descriptor was usable end-to-end.\n    const active = animation.getActiveClip();\n    console.log('drove clip: ' + playable.name + ' (' + playable.kind + '), active now: ' + (active?.name === playable.name));\n}"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.listClips",
      "symbol": "PreviewEditorAnimationApi.listClips",
      "intent": "scene",
      "code": "// 1. Setup — pure read; listClips tolerates the empty-asset case\n//    by returning []. Nothing to spawn.\n// 2. Select — no selection needed; the read spans every clip.\n// 3. Action — enumerate every clip in the asset.\nconst clips = animation.listClips();\n// 4. Observe — log the names + count, then (if any) switch to the\n//    first clip BY NAME, proving the names feed setClip directly.\nconsole.log('clip count: ' + clips.length);\nconsole.log('clip names: ' + JSON.stringify(clips.map(c => c.name)));\nif (clips.length > 0) {\n    animation.setClip(clips[0].name);\n    console.log('switched to: ' + clips[0].name);\n}"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.listControlRigClips",
      "symbol": "PreviewEditorAnimationApi.listControlRigClips",
      "intent": "scene",
      "code": "// 1. Setup — list only the control-rig (rig-control-driven) clips.\nconst controlRig = animation.listControlRigClips();\n// Log the count + prove every entry is controlRig-kind.\nconsole.log('control-rig clips: ' + controlRig.length);\nconsole.log('all controlRig-kind: ' + controlRig.every((c) => c.kind === 'controlRig'));\n// 2. Select — pick the first control-rig clip if present.\nif (controlRig.length === 0) {\n    console.log('no control-rig clips on asset');\n}\nelse {\n    // The first filtered clip is the one we drive.\n    const clip = controlRig[0];\n    // 3. Action — make it active by name (proving the descriptor name is usable).\n    animation.setClip(clip.name);\n    // 4. Observe — confirm getActiveClip reports the same clip + kind.\n    const active = animation.getActiveClip();\n    console.log('active clip: ' + active?.name + ', kind: ' + active?.kind);\n    // Both the name and the kind must match the clip we activated.\n    console.log('activated control-rig clip correctly: ' + (active?.name === clip.name && active?.kind === 'controlRig'));\n}"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.listSkeletonClips",
      "symbol": "PreviewEditorAnimationApi.listSkeletonClips",
      "intent": "scene",
      "code": "// 1. Setup — list only the curve-driven (playable) skeleton clips.\nconst skeleton = animation.listSkeletonClips();\n// Log the count + prove every entry is skeleton-kind.\nconsole.log('skeleton clips: ' + skeleton.length);\nconsole.log('all skeleton-kind: ' + skeleton.every((c) => c.kind === 'skeleton'));\n// 2. Select — pick the first skeleton clip (the kind that carries bone curves).\nif (skeleton.length === 0) {\n    console.log('no skeleton clips on asset');\n}\nelse {\n    // The first filtered clip is the one we drive.\n    const clip = skeleton[0];\n    // 3. Action — make it active and sample a bone curve at t=0 (skeleton\n    //    clips are exactly the kind sampleAt can read).\n    animation.setClip(clip.name);\n    const hipsTx = animation.sampleAt('Hips', 'tx', 0);\n    // 4. Observe — prove the filtered clip is genuinely curve-bearing.\n    console.log('active skeleton clip: ' + clip.name + ' (' + clip.frameCount + ' frames @ ' + clip.fps + 'fps)');\n    console.log('Hips.tx@0 sampled: ' + hipsTx + ' (number: ' + (typeof hipsTx === 'number') + ')');\n}"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.listVertexClips",
      "symbol": "PreviewEditorAnimationApi.listVertexClips",
      "intent": "scene",
      "code": "// 1. Setup — list only the vertex (Alembic .abc point-cache) clips.\nconst vertex = animation.listVertexClips();\n// Log the count + prove every entry is vertex-kind.\nconsole.log('vertex clips: ' + vertex.length);\nconsole.log('all vertex-kind: ' + vertex.every((c) => c.kind === 'vertex'));\n// 2. Select — pick the first vertex-cache clip if the asset has one.\nif (vertex.length === 0) {\n    console.log('no vertex-cache clips (import an .abc to get one)');\n}\nelse {\n    // The first filtered clip is the one we drive.\n    const clip = vertex[0];\n    // 3. Action — activate it and step the transport one frame to prove the\n    //    cache plays (vertex clips drive per-frame positions, not curves).\n    animation.setClip(clip.name);\n    animation.currentTime(0);\n    await animation.nextFrame();\n    // 4. Observe — confirm the active clip is the vertex one and time advanced.\n    const active = animation.getActiveClip();\n    console.log('active vertex clip: ' + active?.name + ' (kind ' + active?.kind + '), cursor: ' + animation.currentTime());\n}"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.nextFrame",
      "symbol": "PreviewEditorAnimationApi.nextFrame",
      "intent": "scene",
      "code": "// 1. Setup — capture the starting cursor so the example can\n//    demonstrate the advance is monotonic non-decreasing.\nconst before = animation.currentTime();\nconsole.log('cursor before: ' + before);\n// 2. Select — frame stepper targets the implicit transport cursor.\n// 3. Action — step forward one frame with `snap: true` so the\n//    new cursor clamps onto an exact frame boundary.\nawait animation.nextFrame();\n// 4. Observe — read the new cursor; advance should be monotonic.\nconst after = animation.currentTime();\nconsole.log('cursor after: ' + after);\nconsole.log('cursor advanced: ' + (after >= before));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.pause",
      "symbol": "PreviewEditorAnimationApi.pause",
      "intent": "scene",
      "code": "// 1. Setup — start playback so pause has something meaningful to do.\n//    Wait one frame so the transport tick flips isPlaying to true.\nanimation.play();\nawait Utils.wait.frame();\n// 2. Select — transport verb; no per-node selection required.\n// 3. Action — pause. The cursor stays at its current position\n//    (play() will resume from here; stop() would have rewound).\nconsole.log('isPlaying before pause: ' + animation.isPlaying());\nanimation.pause();\nawait Utils.wait.frame();\n// 4. Observe — confirm transport stopped advancing.\nconsole.log('isPlaying after pause: ' + animation.isPlaying());"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.play",
      "symbol": "PreviewEditorAnimationApi.play",
      "intent": "scene",
      "code": "// 1. Setup — capture the starting play state so the example can\n//    leave the transport exactly as it found it.\nconst wasPlaying = animation.isPlaying();\n// 2. Select — transport verb; no per-node selection required.\n// 3. Action — start playback. Wait one frame so the transport tick\n//    has a chance to flip isPlaying() to true.\nanimation.play();\nawait Utils.wait.frame();\n// 4. Observe — confirm play started, then pause to restore.\nconsole.log('isPlaying after play: ' + animation.isPlaying());\nanimation.pause();\nconsole.log('isPlaying after pause: ' + animation.isPlaying());\nif (wasPlaying)\n    animation.play();"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.playbackRange",
      "symbol": "PreviewEditorAnimationApi.playbackRange",
      "intent": "scene",
      "code": "// 1. Setup — Previewer operates on the loaded asset; the playback\n//    range is a transport flag that survives script runs.\n// 2. Select — frame-range setter; transport is the implicit target.\n// 3. Action — restrict playback to the first second at 30 fps.\n//    Switches the timeline into custom-range mode.\nanimation.playbackRange(0, 30);\nconsole.log('playbackRange 0..30 set');\n// 4. Observe — widening the range proves the setter accepts\n//    later calls without an explicit reset.\nanimation.playbackRange(0, 120);\nconsole.log('playbackRange 0..120 set');"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.playPause",
      "symbol": "PreviewEditorAnimationApi.playPause",
      "intent": "scene",
      "code": "// 1. Setup — capture the starting transport state so the example\n//    can restore it. playPause is the awaitable cousin of\n//    togglePlayPause; identical effect, Promise return.\nconst before = animation.isPlaying();\nconsole.log('isPlaying before: ' + before);\n// 2. Select — transport verb; no per-node selection required.\n// 3. Action — toggle once; the await lets the transport tick\n//    settle into the new state before observation.\nawait animation.playPause();\nawait Utils.wait.frame();\n// 4. Observe — confirm the inversion, then restore.\nconsole.log('isPlaying after: ' + animation.isPlaying());\nif (animation.isPlaying() !== before) {\n    await animation.playPause();\n}\nconsole.log('isPlaying restored: ' + (animation.isPlaying() === before));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.prevFrame",
      "symbol": "PreviewEditorAnimationApi.prevFrame",
      "intent": "scene",
      "code": "// 1. Setup — seek forward to 1.0s so the step-back has somewhere\n//    to land. prevFrame is clamped at 0 so this also exercises the\n//    \"did the step actually move?\" assertion.\nanimation.currentTime(1.0);\nconst before = animation.currentTime();\nconsole.log('cursor before: ' + before);\n// 2. Select — frame stepper targets the implicit transport cursor.\n// 3. Action — step back one frame, snapping to an exact boundary.\nawait animation.prevFrame();\n// 4. Observe — cursor is at-or-before the start (the clamp may\n//    hold it at 0 if we were already there).\nconst after = animation.currentTime();\nconsole.log('cursor after: ' + after);\nconsole.log('cursor stepped back or held: ' + (after <= before));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.renameClip",
      "symbol": "PreviewEditorAnimationApi.renameClip",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the current clip names (pure read).\nconst before = animation.listClips().map(c => c.name);\n//\n// 2. Select — defaults to the active clip; pass { clipId } to target\n//    another (vertex/cache clips are read-only and throw).\n//\n// 3. Action — rename the active clip (one undo step).\nawait animation.renameClip({ name: 'Walk Cycle' });\n//\n// 4. Observe — confirm the new name appears in the clip list.\nconst after = animation.listClips().map(c => c.name);\nconsole.log('renamed: ' + (after.includes('Walk Cycle') && !before.includes('Walk Cycle')));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.sampleAt",
      "symbol": "PreviewEditorAnimationApi.sampleAt",
      "intent": "scene",
      "code": "// 1. Setup — sampleAt reads from the active clip's curves without\n//    moving the playhead. The example uses a common rig bone name\n//    'Hips' that most rigged assets carry; substitute as needed.\n// 2. Select — node-name + attribute key pick the curve to sample;\n//    no scene-selection mutation occurs.\n// 3. Action — sample two shapes: a per-axis scalar AND a compound\n//    tuple. Identity is returned when the bone is absent (0 for\n//    translate/rotate, 1 for scale); null when no asset is loaded.\nconst txAt0 = animation.sampleAt('Hips', 'tx', 0);\nconst tAt0 = animation.sampleAt('Hips', 'translate', 0);\n// 4. Observe — confirm both shapes match the documented contract.\nconsole.log('tx at t=0 is number: ' + (typeof txAt0 === 'number'));\nconsole.log('translate at t=0 is array: ' + Array.isArray(tAt0));\nconsole.log('translate length 3: ' + (Array.isArray(tAt0) && tAt0.length === 3));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.setClip",
      "symbol": "PreviewEditorAnimationApi.setClip",
      "intent": "scene",
      "code": "// 1. Setup — Previewer depends on the loaded asset; gate against the\n//    empty-asset case using the native clip list (NOT ls({type:'animClip'})).\nconst clips = animation.listClips();\nconsole.log('clip count: ' + clips.length);\n// 2. Select — choose the LAST clip by index so the switch is observable\n//    even if clip 0 was already active.\nif (clips.length === 0) {\n    console.log('no clips on asset — setClip skipped');\n}\nelse {\n    const targetIdx = clips.length - 1;\n    const targetName = clips[targetIdx].name;\n    // 3. Action — switch the active clip by index. Out-of-range int /\n    //    unknown name throws InvalidArgumentError.\n    animation.setClip(targetIdx);\n    // 4. Observe — read back the active clip and prove it is the one we set.\n    const active = animation.getActiveClip();\n    console.log('requested: ' + targetName + ', active now: ' + (active?.name ?? '(none)'));\n    console.log('setClip switched correctly: ' + (active?.name === targetName));\n}"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.setLoopMode",
      "symbol": "PreviewEditorAnimationApi.setLoopMode",
      "intent": "scene",
      "code": "// 1. Setup — snapshot the starting mode so the example can restore\n//    it; loop-mode survives across script runs.\nconst before = animation.getLoopMode();\nconsole.log('loop mode before: ' + before);\n// 2. Select — scalar setter; transport is the implicit target.\n// 3. Action — switch to ping-pong. The cursor will reverse\n//    direction at clip end instead of wrapping to start.\nanimation.setLoopMode('pingpong');\n// 4. Observe — confirm the change, then restore the prior mode.\nconsole.log('loop mode after: ' + animation.getLoopMode());\nanimation.setLoopMode(before);\nconsole.log('loop mode restored: ' + (animation.getLoopMode() === before));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.setSpeed",
      "symbol": "PreviewEditorAnimationApi.setSpeed",
      "intent": "scene",
      "code": "// 1. Setup — capture the original speed so the example can restore\n//    it; transport state survives across script runs.\nconst before = animation.getSpeed();\nconsole.log('speed before: ' + before);\n// 2. Select — scalar setter; the transport is the implicit target.\n// 3. Action — drop to half speed. Zero, negative, and non-finite\n//    speeds throw InvalidArgumentError.\nanimation.setSpeed(0.5);\n// 4. Observe — confirm the change took effect, then restore.\nconsole.log('speed after set: ' + animation.getSpeed());\nanimation.setSpeed(before);\nconsole.log('speed restored: ' + (animation.getSpeed() === before));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.stop",
      "symbol": "PreviewEditorAnimationApi.stop",
      "intent": "scene",
      "code": "// 1. Setup — start playback and let one frame elapse so stop has\n//    something meaningful to do.\nanimation.play();\nawait Utils.wait.frame();\n// 2. Select — transport verb; no per-node selection required.\n// 3. Action — stop. Unlike pause(), stop also rewinds the cursor to\n//    the playback-range start (or 0 with no custom range set).\nconsole.log('isPlaying before stop: ' + animation.isPlaying());\nanimation.stop();\nawait Utils.wait.frame();\n// 4. Observe — playback stopped AND cursor was reset.\nconsole.log('isPlaying after stop: ' + animation.isPlaying());\nconsole.log('cursor after stop: ' + animation.currentTime());"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorAnimationApi.togglePlayPause",
      "symbol": "PreviewEditorAnimationApi.togglePlayPause",
      "intent": "scene",
      "code": "// 1. Setup — capture the starting play state so we can both\n//    observe the flip AND restore it cleanly at the end.\nconst before = animation.isPlaying();\nconsole.log('isPlaying before: ' + before);\n// 2. Select — transport verb; no per-node selection required.\n// 3. Action — toggle once. Lets one frame elapse so the transport\n//    tick has a chance to flip isPlaying().\nanimation.togglePlayPause();\nawait Utils.wait.frame();\n// 4. Observe — confirm the flip, then restore the original state\n//    by toggling again if needed.\nconsole.log('isPlaying after toggle: ' + animation.isPlaying());\nif (animation.isPlaying() !== before) {\n    animation.togglePlayPause();\n    await Utils.wait.frame();\n}\nconsole.log('isPlaying restored: ' + (animation.isPlaying() === before));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorApi",
      "symbol": "PreviewEditorApi",
      "intent": "scene",
      "code": "// 1. Setup — PreviewEditor inspects whatever asset is currently\n//    loaded. Capture the viewport wireframe flag so the example\n//    restores it cleanly at the end.\nconst startedWireframe = viewport.wireframe;\n// 2. Select — Previewer surveys read-only namespaces (viewport,\n//    io, animation). No per-node selection required.\n// 3. Action — survey one read on each sub-namespace + flip the\n//    wireframe flag through viewport.setWireframe.\nconst mode = viewport.transformMode;\nconst t = animation.currentTime();\nconst dirty = io.modified;\nviewport.setWireframe(!startedWireframe);\n// 4. Observe — confirm each surface answered, then restore.\nconsole.log('viewport transformMode is string: ' + (typeof mode === 'string'));\nconsole.log('animation currentTime is number: ' + (typeof t === 'number'));\nconsole.log('io modified is boolean: ' + (typeof dirty === 'boolean'));\nconsole.log('wireframe flipped: ' + (viewport.wireframe !== startedWireframe));\nviewport.setWireframe(startedWireframe);\nconsole.log('wireframe restored: ' + (viewport.wireframe === startedWireframe));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorApi.animation",
      "symbol": "PreviewEditorApi.animation",
      "intent": "scene",
      "code": "// 1. Setup — `.animation` is Previewer-only; ModelEditor /\n//    RigEditor / AnimEditor do NOT expose this pointer.\n// 2. Select — namespace pointer; animation surface is the target.\n// 3. Action — read currentTime + isPlaying through the pointer\n//    and drive a single play/pause round-trip.\nconst t = animation.currentTime();\nconst wasPlaying = animation.isPlaying();\nanimation.play();\nanimation.pause();\n// 4. Observe — confirm the pointer resolved AND both reads gave\n//    documented shapes.\nconsole.log('animation pointer resolved: ' + (animation !== undefined));\nconsole.log('currentTime is number: ' + (typeof t === 'number'));\nconsole.log('isPlaying is boolean: ' + (typeof wasPlaying === 'boolean'));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorApi.create",
      "symbol": "PreviewEditorApi.create",
      "intent": "scene",
      "code": "// 1) Create a red PBR material in the Previewer.\nconst mat = await create.material({ baseColor: [1, 0, 0, 1] });\nconsole.log('material name: ' + mat.name);"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorApi.inspect",
      "symbol": "PreviewEditorApi.inspect",
      "intent": "scene",
      "code": "// 1. Setup — let any pending asset load settle, then list the meshes.\nawait Utils.wait.frame();\nconst meshes = ls({ type: 'mesh' });\n// 2. Action — measure the signed Z-distance between the first two\n//    meshes; fall back to two literal points when the loaded asset\n//    carries fewer than two meshes so the example is self-contained.\nconst d = meshes.length >= 2\n    ? inspect.measure(meshes[0], meshes[1], 'z')\n    : inspect.measure([0, 0, 0], [0, 0, 2], 'z');\n// 3. Read-back — the result carries a numeric signedDistance.\nconsole.log('signedDistance is number: ' + (typeof d.signedDistance === 'number'));\n// 4. Verify — measure never returns null for valid refs.\nconsole.log('measure resolved: ' + (d !== null));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorApi.io",
      "symbol": "PreviewEditorApi.io",
      "intent": "scene",
      "code": "// 1. Setup — record the dirty flag before doing anything.\nconst dirtyBefore = io.modified;\n// 2. Action — clear the Previewer document (an actual io op, idempotent,\n//    not just a getter read).\nio.clear();\n// 3. Read-back — a cleared Previewer is not modified.\nconst dirtyAfter = io.modified;\nconsole.log('io pointer resolved: ' + (io !== undefined));\nconsole.log('modified is boolean: ' + (typeof dirtyAfter === 'boolean'));\n// 4. Verify — report the dirty-flag transition across the clear.\nconsole.log('dirty before/after clear: ' + dirtyBefore + ' -> ' + dirtyAfter);"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorApi.scriptEditor",
      "symbol": "PreviewEditorApi.scriptEditor",
      "intent": "scene",
      "code": "// Open the Script Editor panel, confirm it is up, then close it.\nawait scriptEditor.open();\nconsole.log('open: ' + scriptEditor.isOpen());\nscriptEditor.close();"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorApi.viewport",
      "symbol": "PreviewEditorApi.viewport",
      "intent": "scene",
      "code": "// 1. Setup — `.viewport` resolves to PreviewEditorViewportApi\n//    (BaseViewportApi-shaped). Wait one frame so any pending\n//    asset load settles before the framing call.\nawait Utils.wait.frame();\n// 2. Select — namespace pointer; viewport surface is the target.\n// 3. Action — frame the loaded asset with 2.5x camera padding so\n//    the whole bounding box stays comfortably on-screen.\nviewport.zoomToFit(null, 2.5);\n// 4. Observe — confirm the pointer resolved and the call accepted.\nconsole.log('viewport pointer resolved: ' + (viewport !== undefined));\nconsole.log('zoomToFit invoked');"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorDevDialogs",
      "symbol": "PreviewEditorDevDialogs",
      "intent": "scene",
      "code": "// 1. Setup — the three Previewer-only popups are siblings of the\n//    universal six. dev.dialogs surface always exists; the dialogs\n//    themselves resolve to AbDialogHandle instances.\n// 2. Select — read isOpen on each handle so the survey doesn't\n//    depend on which dialog happens to be open at this moment.\nconst uvsOpen0 = dev.dialogs.uvs.isOpen();\nconst matsOpen0 = dev.dialogs.materials.isOpen();\nconst texsOpen0 = dev.dialogs.textures.isOpen();\n// 3. Action — open + close each dialog once. open() awaits the\n//    mount transition; close() awaits the unmount.\nawait dev.dialogs.uvs.open();\nawait dev.dialogs.uvs.close();\nawait dev.dialogs.materials.open();\nawait dev.dialogs.materials.close();\nawait dev.dialogs.textures.open();\nawait dev.dialogs.textures.close();\n// 4. Observe — every read returned a boolean and every open/close\n//    pair resolved without throw.\nconsole.log('uvs open initially: ' + uvsOpen0);\nconsole.log('materials open initially: ' + matsOpen0);\nconsole.log('textures open initially: ' + texsOpen0);\nconsole.log('all three dialog round-trips completed');"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorDevDialogs.materials",
      "symbol": "PreviewEditorDevDialogs.materials",
      "intent": "scene",
      "code": "// 1. Setup — dialog handle; the Materials popup browses PBR\n//    properties on the asset's materials, no scene mutation.\n// 2. Select — handle is the implicit target.\n// 3. Action — open + close once; mid-cycle isOpen read proves\n//    the handle resolved during the open window.\nawait dev.dialogs.materials.open();\nconst openMid = dev.dialogs.materials.isOpen();\nawait dev.dialogs.materials.close();\n// 4. Observe — boolean shape on isOpen mid-cycle and after close.\nconsole.log('materials isOpen mid-cycle: ' + openMid);\nconsole.log('materials isOpen after close: ' + dev.dialogs.materials.isOpen());"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorDevDialogs.textures",
      "symbol": "PreviewEditorDevDialogs.textures",
      "intent": "scene",
      "code": "// 1. Setup — dialog handle; the Textures popup shows the swatch\n//    grid of every texture bound to the asset, read-only.\n// 2. Select — handle is the implicit target.\n// 3. Action — open + close once; mid-cycle isOpen confirms the\n//    handle observed itself during the open window.\nawait dev.dialogs.textures.open();\nconst openMid = dev.dialogs.textures.isOpen();\nawait dev.dialogs.textures.close();\n// 4. Observe — boolean shape on isOpen mid-cycle and after close.\nconsole.log('textures isOpen mid-cycle: ' + openMid);\nconsole.log('textures isOpen after close: ' + dev.dialogs.textures.isOpen());"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorDevDialogs.uvs",
      "symbol": "PreviewEditorDevDialogs.uvs",
      "intent": "scene",
      "code": "// 1. Setup — dialog handle; the UVs popup is a Previewer-only\n//    diagnostic that doesn't mutate scene state.\n// 2. Select — the dialog handle is the implicit target; no scene\n//    selection required.\n// 3. Action — open, observe, close. open() awaits the mount\n//    transition; close() awaits the unmount transition.\nawait dev.dialogs.uvs.open();\nconst openMid = dev.dialogs.uvs.isOpen();\nawait dev.dialogs.uvs.close();\n// 4. Observe — isOpen flipped to true between open/close.\nconsole.log('uvs isOpen mid-cycle: ' + openMid);\nconsole.log('uvs isOpen after close: ' + dev.dialogs.uvs.isOpen());"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorIoApi",
      "symbol": "PreviewEditorIoApi",
      "intent": "scene",
      "code": "// 1. Setup — Previewer io operates on whichever asset is loaded;\n//    the modified flag tracks unsaved changes for the active scene.\nconst dirtyBefore = io.modified;\n// 2. Select — io is the implicit target; no per-node selection.\n// 3. Action — survey the read accessors (path + modified) and clear\n//    the loaded asset. `clear()` is idempotent so it's safe even\n//    when nothing is loaded.\nconst path = io.path;\nio.clear();\n// 4. Observe — confirm the contract: path is string-or-null shape,\n//    modified flips to false after clear.\nconsole.log('path type: ' + (typeof path === 'string' || path === null ? 'ok' : 'unexpected'));\nconsole.log('modified before: ' + dirtyBefore);\nconsole.log('modified after clear: ' + io.modified);"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorIoApi.clear",
      "symbol": "PreviewEditorIoApi.clear",
      "intent": "scene",
      "code": "// 1. Setup — load an asset so clear has something to remove. Build a\n//    File from bytes (the Previewer has no scripting file-picker); wrap\n//    so the snippet completes whether or not the payload parses.\nconst file = new File([new Uint8Array([0])], 'fixture.abasset');\n// Empty bytes may be rejected — that is fine for this demo.\ntry {\n    await io.loadFile(file);\n}\ncatch (e) {\n    console.log('empty bytes rejected — fine');\n}\nconsole.log('path before clear: ' + io.path);\n// 2. Action — clear, then clear again to prove idempotency.\nio.clear();\nio.clear();\n// 3. Read-back — an empty Previewer reports empty path + clean modified flag.\nconsole.log('path after clear: ' + io.path);\nconsole.log('modified after clear: ' + io.modified);\n// 4. Verify — the contract held: empty + no throw on the 2nd call.\nconsole.log('clear() left the Previewer empty + did not throw on the 2nd call');"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorIoApi.exportAnimation",
      "symbol": "PreviewEditorIoApi.exportAnimation",
      "intent": "scene",
      "code": "// 1. Setup — exportAnimation is a headless export. The Previewer\n//    must already have an asset loaded with a clip whose id is\n//    passed below; wrap so the snippet completes either way.\n// 2. Select — io op; the active scene + named clip are the targets.\n// 3. Action — export GLB of the 'walk-cycle' clip, reframed to\n//    frames 12..72 and retimed to exactly 30 output frames, Y-up.\ntry {\n    const result = await io.exportAnimation({\n        format: 'glb',\n        filename: 'hero-walk',\n        poseMode: 'animation',\n        currentFrameTime: 0,\n        animationClipId: 'walk-cycle',\n        targetAxis: 'y-up',\n        reframeStartFrame: 12,\n        reframeEndFrame: 72,\n        retimeTargetFrames: 30,\n    });\n    // 4. Observe — the export resolves to {blob, filename, size}.\n    console.log('animation exported: ' + result.filename + ' (' + result.size + ' bytes)');\n}\ncatch (e) {\n    // Needs a loaded asset with a clip id 'walk-cycle'; the error\n    // enumerates the clips that ARE available.\n    console.log('exportAnimation needs a loaded clip: ' + String(e.message || e));\n}"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorIoApi.loadFile",
      "symbol": "PreviewEditorIoApi.loadFile",
      "intent": "scene",
      "code": "// 1. Setup — loadFile expects a browser `File` object. The Previewer\n//    doesn't expose a pick-file scripting helper (the dialog flow\n//    lives in the host UI). For programmatic runs, build a File\n//    from a Blob — useful in tests / automation that already hold\n//    the bytes in memory.\nconst blob = new Blob([new Uint8Array([0])], { type: 'application/octet-stream' });\nconst file = new File([blob], 'fixture.abasset', { type: 'application/octet-stream' });\n// 2. Select — io op; the entire Previewer scene is the implicit\n//    target. The constructed File is the new payload.\n// 3. Action — replace the currently-loaded asset with the file.\n//    Playback state resets along with the swap. Empty / fake\n//    payloads will throw; the dispatch shape is what we exercise.\ntry {\n    await io.loadFile(file);\n    console.log('Previewer loaded file: ' + file.name);\n}\ncatch (e) {\n    console.log('Previewer rejected fixture payload (expected for empty bytes)');\n}\n// 4. Observe — confirm the Previewer's modified flag still has\n//    boolean shape after the load attempt.\nconsole.log('modified is boolean after load: ' + (typeof io.modified === 'boolean'));"
    },
    {
      "kind": "example",
      "id": "example:PreviewEditorViewportApi",
      "symbol": "PreviewEditorViewportApi",
      "intent": "scene",
      "code": "// 1. Setup — frame everything currently in the Previewer with a\n//    tight padding so the capture has a known starting size.\nviewport.frameAll(1.2);\n// 2. Select — viewport surface targets the active camera + display\n//    state; no per-node selection is needed for these reads/writes.\nviewport.setView('front');\n// 3. Action — orbit + dolly + pan the camera so the capture has\n//    motion separation from the default angle; flip overlays so the\n//    screenshot reads as a character pose.\nviewport.orbit(25, -10);\nviewport.dolly(0.9);\nviewport.pan(40, -10);\nviewport.setShowGrid(false);\nviewport.setShowSkeleton(true);\nviewport.setJointScale(1.4);\nconst shot = await viewport.captureScreenshot();\n// 4. Observe — read screenshot dimensions, verify a pick at center,\n//    then restore the grid so the example leaves no display debt.\nconsole.log('screenshot size: ' + shot.width + 'x' + shot.height);\nconst c = viewport.center();\nconst hit = viewport.pickAt(c.x, c.y);\nconsole.log('hit something at center: ' + (hit !== null));\nviewport.setShowGrid(true);\nconsole.log('grid restored');"
    },
    {
      "kind": "example",
      "id": "example:Quat",
      "symbol": "Quat",
      "intent": "scene",
      "code": "// 1. Setup — pure math, no scene needed.\nconst q = Quat.fromEuler(0, Math.PI / 2, 0);\nconst v = new Vec3(1, 0, 0);\n// 2. Select — N/A for pure math.\n// 3. Action — rotate the vector by the quaternion.\nconst rotated = v.applyQuat(q);\n// 4. Observe.\nconsole.log('rotated z: ' + rotated.z.toFixed(3));"
    },
    {
      "kind": "example",
      "id": "example:Quat.angleTo",
      "symbol": "Quat.angleTo",
      "intent": "math",
      "code": "const a = Quat.identity();\nconst b = Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\nconsole.log('angle deg: ' + (a.angleTo(b) * 180 / Math.PI).toFixed(2));\n// 1. Setup — see above.\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Quat.clone",
      "symbol": "Quat.clone",
      "intent": "math",
      "code": "// 1. Setup — build a non-trivial source quaternion.\nconst a = Quat.fromAxisAngle([0, 1, 0], Math.PI / 3);\n// 2. Select — N/A for pure math.\n// 3. Action — clone and mutate the copy.\nconst b = a.clone();\nb.x = 0;\n// 4. Observe — the original is untouched by the copy's mutation.\nconsole.log('a unchanged: ' + (a.x !== 0));\nconsole.log('b mutated: ' + (b.x === 0));"
    },
    {
      "kind": "example",
      "id": "example:Quat.conjugate",
      "symbol": "Quat.conjugate",
      "intent": "math",
      "code": "// 1. Setup — unit-length 90 deg rotation around Z.\nconst q = Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\n// 2. Select — N/A for pure math.\n// 3. Action — take the conjugate and compose q * conj.\nconst c = q.conjugate();\nconst round = q.multiply(c);\n// 4. Observe — for unit quats, q * conj approximates identity.\nconsole.log('q * conj ~ identity: ' + round.equals(Quat.identity(), 1e-4));"
    },
    {
      "kind": "example",
      "id": "example:Quat.dot",
      "symbol": "Quat.dot",
      "intent": "math",
      "code": "// 1. Setup — identity and a 90 deg Z-rotation.\nconst a = Quat.identity();\nconst b = Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\n// 2. Select — N/A for pure math.\n// 3. Action — compute the 4D dot product.\nconst d = a.dot(b);\n// 4. Observe — positive value means same-arc slerp direction.\nconsole.log('dot: ' + d.toFixed(4));"
    },
    {
      "kind": "example",
      "id": "example:Quat.equals",
      "symbol": "Quat.equals",
      "intent": "math",
      "code": "const a = Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\nconst b = Quat.fromAxisAngle([0, 0, 1], Math.PI / 2 + 1e-9);\nconsole.log('approx equal: ' + a.equals(b));\n// 1. Setup — see above.\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Quat.from",
      "symbol": "Quat.from",
      "intent": "math",
      "code": "// 1. Setup — start with a quaternion and pack it to a tuple.\nconst q = Quat.fromAxisAngle([0, 1, 0], Math.PI / 4);\nconst tuple = q.toArray();\n// 2. Select — N/A for pure math.\n// 3. Action — rebuild the quaternion from the tuple.\nconst back = Quat.from(tuple);\n// 4. Observe — the round-trip preserves the original components.\nconsole.log('round-trip equals: ' + back.equals(q));"
    },
    {
      "kind": "example",
      "id": "example:Quat.fromAxisAngle",
      "symbol": "Quat.fromAxisAngle",
      "intent": "math",
      "code": "// 1. Setup — 90 degree rotation around the Z axis.\nconst q = Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\n// 2. Select — N/A for pure math.\n// 3. Action — apply the rotation to [1,0,0].\nconst rotated = q.rotate([1, 0, 0]);\nconst rounded = rotated.toArray().map(n => Math.round(n * 1000) / 1000);\n// 4. Observe — [1,0,0] becomes [0,1,0] (approx).\nconsole.log('rotated: ' + JSON.stringify(rounded));"
    },
    {
      "kind": "example",
      "id": "example:Quat.fromEuler",
      "symbol": "Quat.fromEuler",
      "intent": "math",
      "code": "// 1. Setup — 90 degree rotation around Z using Euler in XYZ order.\nconst q = Quat.fromEuler(0, 0, Math.PI / 2, 'XYZ');\n// 2. Select — N/A for pure math.\n// 3. Action — apply to a Vec3 tuple.\nconst rotated = q.rotate([1, 0, 0]);\nconst rounded = rotated.toArray().map(n => Math.round(n * 1000) / 1000);\n// 4. Observe — [1,0,0] becomes [0,1,0].\nconsole.log('rotated: ' + JSON.stringify(rounded));"
    },
    {
      "kind": "example",
      "id": "example:Quat.fromMat3",
      "symbol": "Quat.fromMat3",
      "intent": "math",
      "code": "// 1. Setup — build a Mat3 identity (column-major).\nconst ident3 = [1, 0, 0, 0, 1, 0, 0, 0, 1];\n// 2. Select — N/A for pure math.\n// 3. Action — extract a quaternion from the matrix.\nconst q = Quat.fromMat3(ident3);\n// 4. Observe — identity 3x3 maps to identity quaternion.\nconsole.log('identity-equivalent: ' + q.equals(Quat.identity()));\nconsole.log('w: ' + q.w.toFixed(4));"
    },
    {
      "kind": "example",
      "id": "example:Quat.identity",
      "symbol": "Quat.identity",
      "intent": "math",
      "code": "// 1. Setup — build the identity quaternion.\nconst id = Quat.identity();\n// 2. Select — N/A for pure math.\n// 3. Action — rotate a vector by the identity.\nconst v = id.rotate([1, 2, 3]);\n// 4. Observe — identity leaves the vector unchanged.\nconsole.log('unchanged: ' + JSON.stringify(v.toArray()));\nconsole.log('w: ' + id.w);"
    },
    {
      "kind": "example",
      "id": "example:Quat.inverse",
      "symbol": "Quat.inverse",
      "intent": "math",
      "code": "// 1. Setup — 60 deg rotation around Y.\nconst q = Quat.fromAxisAngle([0, 1, 0], Math.PI / 3);\n// 2. Select — N/A for pure math.\n// 3. Action — invert and compose q * inv.\nconst inv = q.inverse();\nconst round = q.multiply(inv);\n// 4. Observe — composition approximates identity (within epsilon).\nconsole.log('q * inv ~ identity: ' + round.equals(Quat.identity(), 1e-4));"
    },
    {
      "kind": "example",
      "id": "example:Quat.length",
      "symbol": "Quat.length",
      "intent": "math",
      "code": "// 1. Setup — start with the identity quaternion.\nconst id = Quat.identity();\n// 2. Select — N/A for pure math.\n// 3. Action — measure the 4D length.\nconst len = id.length();\n// 4. Observe — identity is unit length.\nconsole.log('identity length: ' + len);"
    },
    {
      "kind": "example",
      "id": "example:Quat.lerp",
      "symbol": "Quat.lerp",
      "intent": "math",
      "code": "const a = Quat.identity();\nconst b = Quat.fromAxisAngle([0, 1, 0], Math.PI / 6);\nconst mid = a.lerp(b, 0.5);\nconsole.log('mid length ≈ 1: ' + mid.length().toFixed(4));\n// 1. Setup — see above.\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Quat.lookRotation",
      "symbol": "Quat.lookRotation",
      "intent": "math",
      "code": "// 1. Setup — look rotation toward +X with world Y as up.\nconst q = Quat.lookRotation([1, 0, 0], [0, 1, 0]);\n// 2. Select — N/A for pure math.\n// 3. Action — rotate the canonical forward (-Z) by the result.\nconst facing = q.rotate([0, 0, -1]);\nconst rounded = facing.toArray().map(n => Math.round(n * 1000) / 1000);\n// 4. Observe — forward (-Z) lands aligned with +X.\nconsole.log('facing: ' + JSON.stringify(rounded));"
    },
    {
      "kind": "example",
      "id": "example:Quat.multiply",
      "symbol": "Quat.multiply",
      "intent": "math",
      "code": "// 1. Setup — build a 45 deg rotation around Z.\nconst r45 = Quat.fromAxisAngle([0, 0, 1], Math.PI / 4);\n// 2. Select — N/A for pure math.\n// 3. Action — compose with itself to get 90 deg.\nconst r90 = r45.multiply(r45);\nconst deg = r90.angleTo(Quat.identity()) * 180 / Math.PI;\n// 4. Observe — composed angle is 90 deg (two 45s).\nconsole.log('composed deg: ' + deg.toFixed(2));"
    },
    {
      "kind": "example",
      "id": "example:Quat.normalize",
      "symbol": "Quat.normalize",
      "intent": "math",
      "code": "// 1. Setup — construct a non-unit quaternion (length sqrt(0.72)).\nconst q = new Quat(0.6, 0, 0, 0.6);\n// 2. Select — N/A for pure math.\n// 3. Action — normalize to unit length.\nconst n = q.normalize();\n// 4. Observe — normalized length approximates 1.\nconsole.log('length ~ 1: ' + n.length().toFixed(4));"
    },
    {
      "kind": "example",
      "id": "example:Quat.rotate",
      "symbol": "Quat.rotate",
      "intent": "math",
      "code": "// 1. 90 degree rotation around Z takes [1,0,0] to [0,1,0].\nconst q = Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\nconst rotated = q.rotate([1, 0, 0]);\nconst rounded = rotated.toArray().map(n => Math.round(n * 1000) / 1000);\nconsole.log('rotated: ' + JSON.stringify(rounded));\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Quat.setFromUnitVectors",
      "symbol": "Quat.setFromUnitVectors",
      "intent": "math",
      "code": "// 1. Setup — pair of unit directions.\nconst from = [1, 0, 0];\nconst to = [0, 1, 0];\n// 2. Select — N/A for pure math.\n// 3. Action — build the rotation that takes from -> to.\nconst q = Quat.setFromUnitVectors(from, to);\nconst rotated = q.rotate(from);\nconst rounded = rotated.toArray().map(n => Math.round(n * 1000) / 1000);\n// 4. Observe — [1,0,0] becomes [0,1,0] (approx).\nconsole.log('rotated: ' + JSON.stringify(rounded));"
    },
    {
      "kind": "example",
      "id": "example:Quat.slerp",
      "symbol": "Quat.slerp",
      "intent": "math",
      "code": "// 1. Setup — endpoints: identity and 90 deg Z-rotation.\nconst a = Quat.identity();\nconst b = Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\n// 2. Select — N/A for pure math.\n// 3. Action — interpolate to the midpoint.\nconst mid = Quat.slerp(a, b, 0.5);\nconst midDeg = mid.angleTo(a) * 180 / Math.PI;\n// 4. Observe — midpoint sits at 45 degrees from identity.\nconsole.log('midpoint deg: ' + midDeg.toFixed(2));"
    },
    {
      "kind": "example",
      "id": "example:Quat.toArray",
      "symbol": "Quat.toArray",
      "intent": "math",
      "code": "// 1. Setup — start with the identity quaternion.\nconst q = Quat.identity();\n// 2. Select — N/A for pure math.\n// 3. Action — pack to a raw 4-tuple.\nconst tuple = q.toArray();\n// 4. Observe — identity unpacks to [0, 0, 0, 1].\nconsole.log('tuple: ' + JSON.stringify(tuple));"
    },
    {
      "kind": "example",
      "id": "example:Quat.toEuler",
      "symbol": "Quat.toEuler",
      "intent": "math",
      "code": "const q = Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\nconst e = q.toEuler('XYZ');\nconsole.log('z radians: ' + e.z.toFixed(4));\n// 1. Setup — see above.\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Quat.toMatrix",
      "symbol": "Quat.toMatrix",
      "intent": "math",
      "code": "const q = Quat.fromAxisAngle([0, 1, 0], Math.PI / 2);\nconst m = q.toMatrix();\n// Apply via the matrix — same result as q.rotate.\nconst v = m.transformVector([1, 0, 0]);\nconst rounded = v.toArray().map(n => Math.round(n * 1000) / 1000);\nconsole.log('rotated: ' + JSON.stringify(rounded));\n// 1. Setup — see above.\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Quat.w",
      "symbol": "Quat.w",
      "intent": "math",
      "code": "// 1. Setup — identity quaternion has w = 1.\nconst id = Quat.identity();\n// 2. Select — N/A for pure math.\n// 3. Action — read the w (scalar) component.\nconst wValue = id.w;\n// 4. Observe — identity w is exactly 1.\nconsole.log('identity w: ' + wValue);"
    },
    {
      "kind": "example",
      "id": "example:Quat.x",
      "symbol": "Quat.x",
      "intent": "math",
      "code": "// 1. Setup — 90 deg rotation around X stores its axis-sin in `x`.\nconst q = Quat.fromAxisAngle([1, 0, 0], Math.PI / 2);\n// 2. Select — N/A for pure math.\n// 3. Action — read the x component.\nconst xValue = q.x;\n// 4. Observe — non-zero magnitude reflects the X-axis rotation.\nconsole.log('x: ' + xValue.toFixed(4));"
    },
    {
      "kind": "example",
      "id": "example:Quat.y",
      "symbol": "Quat.y",
      "intent": "math",
      "code": "// 1. Setup — 90 deg rotation around Y stores its axis-sin in `y`.\nconst q = Quat.fromAxisAngle([0, 1, 0], Math.PI / 2);\n// 2. Select — N/A for pure math.\n// 3. Action — read the y component.\nconst yValue = q.y;\n// 4. Observe — non-zero magnitude reflects the Y-axis rotation.\nconsole.log('y: ' + yValue.toFixed(4));"
    },
    {
      "kind": "example",
      "id": "example:Quat.z",
      "symbol": "Quat.z",
      "intent": "math",
      "code": "// 1. Setup — 90 deg rotation around Z stores its axis-sin in `z`.\nconst q = Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\n// 2. Select — N/A for pure math.\n// 3. Action — read the z component.\nconst zValue = q.z;\n// 4. Observe — non-zero magnitude reflects the Z-axis rotation.\nconsole.log('z: ' + zValue.toFixed(4));"
    },
    {
      "kind": "example",
      "id": "example:RigCorrectivesApi.bake",
      "symbol": "RigCorrectivesApi.bake",
      "intent": "scene",
      "code": "await correctives.bake('Elbow Bent corrective');\nconsole.log('sculpt baked to a rest-space blendshape');"
    },
    {
      "kind": "example",
      "id": "example:RigCorrectivesApi.create",
      "symbol": "RigCorrectivesApi.create",
      "intent": "scene",
      "code": "await correctives.create({ poseId: 'Elbow Bent', driverBones: ['mixamorigLeftForeArm'] });"
    },
    {
      "kind": "example",
      "id": "example:RigCorrectivesApi.delete",
      "symbol": "RigCorrectivesApi.delete",
      "intent": "scene",
      "code": "await correctives.delete('Elbow Bent corrective');\nconsole.log('corrective removed (one Ctrl+Z restores it)');"
    },
    {
      "kind": "example",
      "id": "example:RigCorrectivesApi.example",
      "symbol": "RigCorrectivesApi.example",
      "intent": "scene",
      "code": "console.log('correctives example length: ' + correctives.example().length);"
    },
    {
      "kind": "example",
      "id": "example:RigCorrectivesApi.help",
      "symbol": "RigCorrectivesApi.help",
      "intent": "scene",
      "code": "console.log('correctives help length: ' + correctives.help().length);"
    },
    {
      "kind": "example",
      "id": "example:RigCorrectivesApi.list",
      "symbol": "RigCorrectivesApi.list",
      "intent": "scene",
      "code": "const rows = await correctives.list();\nconsole.log(rows);"
    },
    {
      "kind": "example",
      "id": "example:RigCorrectivesApi.mirror",
      "symbol": "RigCorrectivesApi.mirror",
      "intent": "scene",
      "code": "await correctives.mirror('elbow_L corrective');\nconsole.log('opposite-side corrective created as ONE undo entry');"
    },
    {
      "kind": "example",
      "id": "example:RigCorrectivesApi.reDerive",
      "symbol": "RigCorrectivesApi.reDerive",
      "intent": "scene",
      "code": "await correctives.reDerive('Elbow Bent corrective');"
    },
    {
      "kind": "example",
      "id": "example:RigCorrectivesApi.setWeight",
      "symbol": "RigCorrectivesApi.setWeight",
      "intent": "scene",
      "code": "await correctives.setWeight('Elbow Bent corrective', 0.5);"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.biped",
      "symbol": "RigCreateApi.biped",
      "intent": "scene",
      "code": "// 1) A default biped from scratch, undoable (Ctrl+Z removes the whole rig).\nawait create.biped({ headless: true, spineCount: 6 });\nconsole.log('biped rig created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.bipedArm",
      "symbol": "RigCreateApi.bipedArm",
      "intent": "scene",
      "code": "// 1) Mirrored arm pair with clavicle parented to the chest.\nawait create.bipedArm({ sides: ['L', 'R'], clavicleJoint: 'clav_L', parentModule: 'chest' });\nconsole.log('biped arm created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.bipedLeg",
      "symbol": "RigCreateApi.bipedLeg",
      "intent": "scene",
      "code": "// 1) Mirrored leg pair from a hip bone.\nawait create.bipedLeg({ sides: ['L', 'R'], startJoint: 'hip_L' });\nconsole.log('biped leg created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.example",
      "symbol": "RigCreateApi.example",
      "intent": "scene",
      "code": "const text = create.example();\nconsole.log('example length: ' + text.length);"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.fkChain",
      "symbol": "RigCreateApi.fkChain",
      "intent": "scene",
      "code": "// 1) Mirrored FK arm chain from existing bones, undoable.\nawait create.fkChain({ name: 'arm', sides: ['L', 'R'], startJoint: 'shoulder_L' });\nconsole.log('fk chain created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.foot",
      "symbol": "RigCreateApi.foot",
      "intent": "scene",
      "code": "// 1) A foot parented to the left leg.\nawait create.foot({ legModule: 'leg_L', rootJoint: 'ball_L' });\nconsole.log('foot created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.global",
      "symbol": "RigCreateApi.global",
      "intent": "scene",
      "code": "// 1) The rig's world anchor with an offset control.\nawait create.global({ rootJoint: 'hips', addOffsetControl: true });\nconsole.log('global created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.hand",
      "symbol": "RigCreateApi.hand",
      "intent": "scene",
      "code": "// 1) A bilateral 5-finger hand (new-rig, no skeleton needed).\nawait create.hand({ headless: true, sides: ['L', 'R'] });\nconsole.log('hand created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.help",
      "symbol": "RigCreateApi.help",
      "intent": "scene",
      "code": "const text = create.help();\nconsole.log('help length: ' + text.length);"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.lookAt",
      "symbol": "RigCreateApi.lookAt",
      "intent": "scene",
      "code": "// 1) A left eye aimed +Z with a space-switchable target.\nawait create.lookAt({ name: 'eye', side: 'L', positionX: 0.06, positionY: 1.42, positionZ: 1.1, headless: true });\nconsole.log('look-at created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.neckHead",
      "symbol": "RigCreateApi.neckHead",
      "intent": "scene",
      "code": "// 1) Neck & head from neck1 to head, parented to the spine.\nawait create.neckHead({ neckStartJoint: 'neck1', headJoint: 'head', parentModule: 'spine' });\nconsole.log('neck & head created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.quad",
      "symbol": "RigCreateApi.quad",
      "intent": "scene",
      "code": "// 1) A default quadruped with eyes, headless.\nawait create.quad({ headless: true, includeEyes: true });\nconsole.log('quad created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.quadFrontLeg",
      "symbol": "RigCreateApi.quadFrontLeg",
      "intent": "scene",
      "code": "// 1) Mirrored front-leg pair (with feet) from an elbow bone.\nawait create.quadFrontLeg({ sides: ['L', 'R'], startJoint: 'Elbow_L' });\nconsole.log('quad front leg created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.quadHindLeg",
      "symbol": "RigCreateApi.quadHindLeg",
      "intent": "scene",
      "code": "// 1) Mirrored hind-leg pair (with feet) from a hip bone.\nawait create.quadHindLeg({ sides: ['L', 'R'], startJoint: 'HindHip_L', includeFoot: true });\nconsole.log('quad hind leg created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.single",
      "symbol": "RigCreateApi.single",
      "intent": "scene",
      "code": "// 1) A prop control on a named bone.\nawait create.single({ name: 'prop', joint: 'prop_jnt' });\nconsole.log('single control created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.sphereRoll",
      "symbol": "RigCreateApi.sphereRoll",
      "intent": "scene",
      "code": "// 1) A beach ball that rolls as its space travels, headless.\nawait create.sphereRoll({ name: 'beachBall', sphereRadius: 0.45, headless: true });\nconsole.log('sphere roll created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.spine",
      "symbol": "RigCreateApi.spine",
      "intent": "scene",
      "code": "// 1) A spine from pelvis to chest.\nawait create.spine({ startJoint: 'pelvis', endJoint: 'chest' });\nconsole.log('spine created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.stretchyAim",
      "symbol": "RigCreateApi.stretchyAim",
      "intent": "scene",
      "code": "// 1) A stretchy aim between two jaw bones.\nawait create.stretchyAim({ startJoint: 'jaw', endJoint: 'jaw_end' });\nconsole.log('stretchy aim created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.vehicle",
      "symbol": "RigCreateApi.vehicle",
      "intent": "scene",
      "code": "// 1) A six-wheel truck with suspension, headless.\nawait create.vehicle({ name: 'truck', wheelCount: 6, doBodySpring: true, headless: true });\nconsole.log('vehicle created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigCreateApi.vehicleRig",
      "symbol": "RigCreateApi.vehicleRig",
      "intent": "scene",
      "code": "// 1) A complete sprung 4-wheel car rig, headless.\nawait create.vehicleRig({ headless: true });\nconsole.log('vehicle rig created: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi",
      "symbol": "RigEditorApi",
      "intent": "scene",
      "code": "// 1) Enter skin-weight paint mode, then exit again.\nawait tools.enterPaintMode();\nawait tools.exitPaintMode();\nconsole.log('paint mode toggle ran');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.addSpaceTarget",
      "symbol": "RigEditorApi.addSpaceTarget",
      "intent": "scene",
      "code": "// 1. Setup: a rigged character with a space-switchable hand control.\n//    (Nothing to create — the rig's constraint carries the targets.)\n// 2. Select: name the control and the bone the new space follows.\nconst req = { control: 'hand_L', kind: 'bone', target: 'spine_03' };\n// 3. Action: add the persisted bone space; then pin a prop as a\n//    world-frozen object anchor beside it.\nawait addSpaceTarget(req);\nawait addSpaceTarget({ control: 'hand_L', kind: 'object', target: 'Sword', label: 'Sword (pinned)' });\n// 4. Observe: both spaces now appear in the control's space dropdown\n//    and in Bake Space's target list.\nconsole.log('targets added: 2');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.cleanupFootSliding",
      "symbol": "RigEditorApi.cleanupFootSliding",
      "intent": "scene",
      "code": "// 1. (In the Control Rig editor, after retargetClips appended a clip.)\nconst report = await cleanupFootSliding({ clip: 'walk (retargeted)' });\n// 2. Inspect what was pinned.\nconsole.log(report?.windows + ' plant windows on ' + report?.controls);\n// 3. Stricter contact detection with softer ramps.\nawait cleanupFootSliding({ clip: 'walk (retargeted)', speedThreshold: 0.15, blendFrames: 6 });"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.correctives",
      "symbol": "RigEditorApi.correctives",
      "intent": "scene",
      "code": "// 1) Create at a saved pose, sculpt in the viewport, bake.\nawait correctives.create({ poseId: 'Elbow Bent', driverBones: ['mixamorigLeftForeArm'] });\nawait correctives.bake('Elbow Bent corrective');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.create",
      "symbol": "RigEditorApi.create",
      "intent": "scene",
      "code": "// 1) Build the anchor, then a spine on top of it.\nawait create.global({ headless: true });\nawait create.spine({ headless: true, parentModule: 'Global' });\nconsole.log('rig started');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.helpers",
      "symbol": "RigEditorApi.helpers",
      "intent": "scene",
      "code": "// 1) Generate helper joints, then train + commit.\nawait helpers.generate({ jointCount: 8 });\nawait helpers.trainMlp({});\nawait helpers.commit({});"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.io",
      "symbol": "RigEditorApi.io",
      "intent": "scene",
      "code": "// 1) Read the current asset path.\nconsole.log('asset path is a string: ' + (typeof io.path === 'string'));"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.poses",
      "symbol": "RigEditorApi.poses",
      "intent": "scene",
      "code": "// 1) Pose the rig, save it, then snap back by name.\nawait poses.save({ name: 'Punch' });\nawait poses.goTo('Punch');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.retargetClips",
      "symbol": "RigEditorApi.retargetClips",
      "intent": "scene",
      "code": "// 1. (In the Control Rig editor on an asset whose rig re-oriented the\n//    skeleton.) Reproduce every skeleton clip on the rig — non-destructive.\nconst result = await retargetClips({ output: 'controlRig' });\n// 2. Originals are kept; new control-rig clips are appended (undoable).\nconsole.log('appended clips: ' + result?.appended);\n// 3. Or bake into the rig's skeleton frames instead (rig-independent + exact).\nconst baked = await retargetClips({ output: 'skeleton' });\n// 4. Batch-retarget through a locked setup, baking both outputs.\nconst batch = await retargetClips({ setup: 'Mixamo Walk Setup', alsoSkeleton: true });\nconsole.log('batch appended: ' + batch?.appended);"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.skin",
      "symbol": "RigEditorApi.skin",
      "intent": "scene",
      "code": "// 1) Compute weights headlessly, then prune to 2 influences.\nawait skin.computeWeights({ method: 'voxel-heat-diffuse' });\nawait skin.enforceMaxInfluences({ maxInfluences: 2 });\nconsole.log('skin workflow complete');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.techJoints",
      "symbol": "RigEditorApi.techJoints",
      "intent": "scene",
      "code": "// Forearm twist with auto-weights (skin follows immediately):\nawait techJoints.add({ kind: 'twistChain', sourceBone: 'mixamorigLeftHand' });\nawait techJoints.mirror('mixamorigLeftHand twist');\nconsole.log(techJoints.list());"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.tools",
      "symbol": "RigEditorApi.tools",
      "intent": "scene",
      "code": "// 1) Cycle paint mode on and off.\nawait tools.enterPaintMode();\nawait tools.exitPaintMode();\nconsole.log('paint mode cycle complete');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.upgradeModule",
      "symbol": "RigEditorApi.upgradeModule",
      "intent": "scene",
      "code": "// 1. Upgrade to the latest version of leg_L's family.\nawait upgradeModule({ module: 'leg_L' });\n// 2. Or pin to a specific version and inspect the move.\nconst r = await upgradeModule({ module: 'leg_L', toVersion: 2 });\nconsole.log(r?.module + ': v' + r?.fromVersion + ' -> v' + r?.toVersion);"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.viewport",
      "symbol": "RigEditorApi.viewport",
      "intent": "scene",
      "code": "// 1) Frame the rigged asset.\nawait Utils.wait.frame();\nviewport.zoomToFit(null, 2.5);"
    },
    {
      "kind": "example",
      "id": "example:RigEditorApi.wrapToMesh",
      "symbol": "RigEditorApi.wrapToMesh",
      "intent": "scene",
      "code": "// 1. (In the Control Rig editor on a rigged asset that also holds a second\n//    target mesh.) Preview the transfer onto the target — no mutation.\nconst preview = await wrapToMesh({ targetMeshId: 'body_hires', dryRun: true });\n// 2. Inspect the correspondence quality before committing.\nconsole.log('guides moved: ' + preview?.movedCount + ' confidence: ' + preview?.confidence);\n// 3. Commit the transfer — undoable in ONE step (Ctrl+Z reverts it all).\nawait wrapToMesh({ targetMeshId: 'body_hires', correspondence: 'auto' });\n// 4. The rig's guides + joints now sit on the target mesh.\nconsole.log('transfer complete');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorIoApi",
      "symbol": "RigEditorIoApi",
      "intent": "scene",
      "code": "// 1) Save the rigging session as a project file.\nawait io.saveScene('/local/projects/control-rig.abasset');\nconsole.log('rig session saved');\n// 2) Reopen it later from the same path.\nawait io.openScene('/local/projects/control-rig.abasset');\nconsole.log('rig session reopened');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.computeWeights",
      "symbol": "RigEditorSkinApi.computeWeights",
      "intent": "scene",
      "code": "// 1) Headless voxel heat-diffuse weights on every skinned mesh.\nawait skin.computeWeights({ method: 'voxel-heat-diffuse', maxInfluences: 4 });\nconsole.log('weights computed: true');\n// 2) Undoable — Ctrl+Z (or the History panel) restores the old weights."
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.deisolate",
      "symbol": "RigEditorSkinApi.deisolate",
      "intent": "scene",
      "code": "// 1) Restore all meshes after isolation.\nawait skin.deisolate({});\nconsole.log('deisolated: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.enforceMaxInfluences",
      "symbol": "RigEditorSkinApi.enforceMaxInfluences",
      "intent": "scene",
      "code": "// 1) Cap the active mesh at 2 influences per vertex.\nawait skin.enforceMaxInfluences({ maxInfluences: 2 });\nconsole.log('influences pruned: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.example",
      "symbol": "RigEditorSkinApi.example",
      "intent": "scene",
      "code": "const text = skin.example();\nconsole.log('example length: ' + text.length);"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.help",
      "symbol": "RigEditorSkinApi.help",
      "intent": "scene",
      "code": "const text = skin.help();\nconsole.log('help length: ' + text.length);"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.isolate",
      "symbol": "RigEditorSkinApi.isolate",
      "intent": "scene",
      "code": "// 1) Isolate one mesh, then restore everything.\nawait skin.isolate({ meshes: ['body'] });\nawait skin.deisolate({});\nconsole.log('isolate round-trip: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.loadWeights",
      "symbol": "RigEditorSkinApi.loadWeights",
      "intent": "scene",
      "code": "// 1) Open the Load Skin Weights picker.\nawait skin.loadWeights({});\nconsole.log('load dialog opened: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.mirrorWeights",
      "symbol": "RigEditorSkinApi.mirrorWeights",
      "intent": "scene",
      "code": "// 1) Mirror the active mesh's weights left→right across X.\nawait skin.mirrorWeights({ direction: 'left_to_right' });\nconsole.log('mirrored: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.prune",
      "symbol": "RigEditorSkinApi.prune",
      "intent": "scene",
      "code": "// 1) Prune the active mesh's selected components at the default 0.01.\nawait skin.prune();\nconsole.log('pruned: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.relax",
      "symbol": "RigEditorSkinApi.relax",
      "intent": "scene",
      "code": "// 1) Relax the active mesh's selected verts twice as hard.\nawait skin.relax({ strength: 0.8, iterations: 5 });\nconsole.log('relaxed: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.resetBindPose",
      "symbol": "RigEditorSkinApi.resetBindPose",
      "intent": "scene",
      "code": "// 1) Return the rig to its bind pose.\nawait skin.resetBindPose({});\nconsole.log('bind pose restored: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.rigidify",
      "symbol": "RigEditorSkinApi.rigidify",
      "intent": "scene",
      "code": "// 1) Rigidify the active mesh's selected verts.\nawait skin.rigidify({ strength: 0.5 });\nconsole.log('rigidified: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.saveWeights",
      "symbol": "RigEditorSkinApi.saveWeights",
      "intent": "scene",
      "code": "// 1) Save the active mesh's painted weights to a JSON file.\nawait skin.saveWeights({});\nconsole.log('weights saved: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.setWeight",
      "symbol": "RigEditorSkinApi.setWeight",
      "intent": "scene",
      "code": "// 1) Fully bind the selected verts to the Hips influence.\nawait skin.setWeight({ weight: 1, influence: 'Hips' });\nconsole.log('set: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.transferFromSelected",
      "symbol": "RigEditorSkinApi.transferFromSelected",
      "intent": "scene",
      "code": "// 1) Pull weights from the other selected meshes onto the active one.\nawait skin.transferFromSelected();\nconsole.log('transferred: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.transferToSelected",
      "symbol": "RigEditorSkinApi.transferToSelected",
      "intent": "scene",
      "code": "// 1) Push the active mesh's weights onto the other selected meshes.\nawait skin.transferToSelected();\nconsole.log('transferred: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorSkinApi.transferWeights",
      "symbol": "RigEditorSkinApi.transferWeights",
      "intent": "scene",
      "code": "// 1) Copy the body's weighting onto a shirt mesh.\nawait skin.transferWeights({ source: ['body'], targets: ['shirt'] });\nconsole.log('transfer ran: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorToolsApi.blastSkeleton",
      "symbol": "RigEditorToolsApi.blastSkeleton",
      "intent": "scene",
      "code": "// 1) Blast the skeleton on the active rigged asset.\nawait tools.blastSkeleton({});\nconsole.log('skeleton blasted: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorToolsApi.enterPaintMode",
      "symbol": "RigEditorToolsApi.enterPaintMode",
      "intent": "scene",
      "code": "// 1) Enter skin-weight paint mode on the active rigged asset.\nawait tools.enterPaintMode({});\nconsole.log('paint mode entered: true');\n// 2) Exit to restore the previous tool so subsequent steps are not\n//    trapped in the paint brush.\nawait tools.exitPaintMode({});\nconsole.log('paint mode exited: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorToolsApi.example",
      "symbol": "RigEditorToolsApi.example",
      "intent": "scene",
      "code": "const text = tools.example();\nconsole.log('example length: ' + text.length);"
    },
    {
      "kind": "example",
      "id": "example:RigEditorToolsApi.exitGuideMode",
      "symbol": "RigEditorToolsApi.exitGuideMode",
      "intent": "scene",
      "code": "// 1) Headless-create a module (ends in guide mode by design).\nawait create.single({ headless: true, name: 'root' });\n// 2) Exit guide mode — constructs the module and mints its control.\nawait tools.exitGuideMode();\nconsole.log('back in control mode: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorToolsApi.exitPaintMode",
      "symbol": "RigEditorToolsApi.exitPaintMode",
      "intent": "scene",
      "code": "// 1) Enter then exit so the example covers both helpers.\nawait tools.enterPaintMode({});\nawait tools.exitPaintMode({});\nconsole.log('exited paint mode: true');"
    },
    {
      "kind": "example",
      "id": "example:RigEditorToolsApi.help",
      "symbol": "RigEditorToolsApi.help",
      "intent": "scene",
      "code": "const text = tools.help();\nconsole.log('help length: ' + text.length);"
    },
    {
      "kind": "example",
      "id": "example:RigHelperJointsApi.commit",
      "symbol": "RigHelperJointsApi.commit",
      "intent": "scene",
      "code": "await helpers.commit({});\nconsole.log('helpers + trained model committed to the rig');"
    },
    {
      "kind": "example",
      "id": "example:RigHelperJointsApi.example",
      "symbol": "RigHelperJointsApi.example",
      "intent": "scene",
      "code": "console.log('helpers example length: ' + helpers.example().length);"
    },
    {
      "kind": "example",
      "id": "example:RigHelperJointsApi.generate",
      "symbol": "RigHelperJointsApi.generate",
      "intent": "scene",
      "code": "// Corpus must exist first (Deformation Baker ADVANCED stage).\nawait helpers.generate({ jointCount: 8 });\n// Helpers land on the rig as mlpDriven joints — train + commit next.\nconsole.log('helper joints generated from the pose corpus');"
    },
    {
      "kind": "example",
      "id": "example:RigHelperJointsApi.help",
      "symbol": "RigHelperJointsApi.help",
      "intent": "scene",
      "code": "console.log('helpers help length: ' + helpers.help().length);"
    },
    {
      "kind": "example",
      "id": "example:RigHelperJointsApi.setMode",
      "symbol": "RigHelperJointsApi.setMode",
      "intent": "scene",
      "code": "await helpers.setMode({ mode: 'baked' });\nconsole.log('helper drive mode is now baked');"
    },
    {
      "kind": "example",
      "id": "example:RigHelperJointsApi.trainMlp",
      "symbol": "RigHelperJointsApi.trainMlp",
      "intent": "scene",
      "code": "await helpers.trainMlp({});\nconsole.log('runtime MLP trained on the generated helpers');"
    },
    {
      "kind": "example",
      "id": "example:RigPosesApi.apply",
      "symbol": "RigPosesApi.apply",
      "intent": "scene",
      "code": "await poses.apply('Punch'); // alias of goTo\nconsole.log('pose applied to the live rig');"
    },
    {
      "kind": "example",
      "id": "example:RigPosesApi.delete",
      "symbol": "RigPosesApi.delete",
      "intent": "scene",
      "code": "await poses.delete('Crouch'); // undoable\nconsole.log('library entry deleted (one Ctrl+Z restores it)');"
    },
    {
      "kind": "example",
      "id": "example:RigPosesApi.example",
      "symbol": "RigPosesApi.example",
      "intent": "scene",
      "code": "console.log('poses example length: ' + poses.example().length);"
    },
    {
      "kind": "example",
      "id": "example:RigPosesApi.goTo",
      "symbol": "RigPosesApi.goTo",
      "intent": "scene",
      "code": "await poses.goTo('Punch');\nconsole.log('rig snapped into the saved pose');"
    },
    {
      "kind": "example",
      "id": "example:RigPosesApi.help",
      "symbol": "RigPosesApi.help",
      "intent": "scene",
      "code": "console.log('poses help length: ' + poses.help().length);"
    },
    {
      "kind": "example",
      "id": "example:RigPosesApi.list",
      "symbol": "RigPosesApi.list",
      "intent": "scene",
      "code": "const rows = await poses.list();\nconsole.log(rows);"
    },
    {
      "kind": "example",
      "id": "example:RigPosesApi.mirror",
      "symbol": "RigPosesApi.mirror",
      "intent": "scene",
      "code": "await poses.mirror('elbow_L_bent');\nconsole.log('mirrored pose saved as a NEW library entry');"
    },
    {
      "kind": "example",
      "id": "example:RigPosesApi.rename",
      "symbol": "RigPosesApi.rename",
      "intent": "scene",
      "code": "await poses.rename('Pose 1', 'Crouch');\nconsole.log('library entry renamed to Crouch');"
    },
    {
      "kind": "example",
      "id": "example:RigPosesApi.save",
      "symbol": "RigPosesApi.save",
      "intent": "scene",
      "code": "await poses.save({ name: 'Punch' });\nconst rows = await poses.list();\nconsole.log('library entries: ' + rows.length);"
    },
    {
      "kind": "example",
      "id": "example:RigTechJointsApi.add",
      "symbol": "RigTechJointsApi.add",
      "intent": "scene",
      "code": "await techJoints.add({ kind: 'poseReader', driverBones: ['mixamorigLeftForeArm'], keyPoses: [0, 0, 0, 1, 0.7071, 0, 0, 0.7071] });"
    },
    {
      "kind": "example",
      "id": "example:RigTechJointsApi.example",
      "symbol": "RigTechJointsApi.example",
      "intent": "scene",
      "code": "// 1. Setup: any session — the example is static source text.\n// 2. Select: the tech-joints namespace itself.\n// 3. Action: fetch the runnable walkthrough.\nconst code = techJoints.example();\n// 4. Observe: paste-ready source covering add / list / mirror.\nconsole.log('example length: ' + code.length);"
    },
    {
      "kind": "example",
      "id": "example:RigTechJointsApi.help",
      "symbol": "RigTechJointsApi.help",
      "intent": "scene",
      "code": "// 1. Setup: any session — help is always available.\n// 2. Select: the tech-joints namespace itself.\n// 3. Action: fetch the member reference text.\nconst text = techJoints.help();\n// 4. Observe: a non-empty multi-line reference.\nconsole.log('help length: ' + text.length);"
    },
    {
      "kind": "example",
      "id": "example:RigTechJointsApi.list",
      "symbol": "RigTechJointsApi.list",
      "intent": "scene",
      "code": "const rows = await techJoints.list();\nconsole.log('tech joints on the rig: ' + rows.length);"
    },
    {
      "kind": "example",
      "id": "example:RigTechJointsApi.mirror",
      "symbol": "RigTechJointsApi.mirror",
      "intent": "scene",
      "code": "await techJoints.mirror('mixamorigLeftHand twist');"
    },
    {
      "kind": "example",
      "id": "example:RigTechJointsApi.remove",
      "symbol": "RigTechJointsApi.remove",
      "intent": "scene",
      "code": "await techJoints.remove('mixamorigLeftHand twist');"
    },
    {
      "kind": "example",
      "id": "example:RigTechJointsApi.update",
      "symbol": "RigTechJointsApi.update",
      "intent": "scene",
      "code": "await techJoints.update('mixamorigLeftHand twist', { params: { weights: [0.25, 0.6, 1] } });"
    },
    {
      "kind": "example",
      "id": "example:SceneApi",
      "symbol": "SceneApi",
      "intent": "scene",
      "code": "// 1) Spawn two meshes to query.\nconst a = await create.cube({ width: 1, height: 1, depth: 1, name: 'A' });\na.xform({ t: [2.5, 1.3, -0.7] });\nconst b = await create.sphere({ radius: 0.5, name: 'B' });\nb.xform({ t: [-2.5, 1.3, -0.7] });\n// 2) Filtered list of meshes in the scene.\nconst meshes = ls({ type: 'mesh' });\nconsole.log('mesh count: ' + meshes.length);\n// 3) Selection: replace, add, remove, toggle, clear, all, invert.\nawait select(a);\nawait select(b, { mode: 'add' });\nawait select(a, { mode: 'remove' });\nawait select(a, { mode: 'toggle' });\nawait select(null, { mode: 'all', filter: { type: 'mesh' } });\nawait select(null, { mode: 'invert' });\nawait select(null, { mode: 'clear' });\n// 4) Print the namespace help.\nconsole.log('scene help length: ' + scene.help().length);\n// 5) Clean up.\nawait a.delete();\nawait b.delete();"
    },
    {
      "kind": "example",
      "id": "example:SceneApi.example",
      "symbol": "SceneApi.example",
      "intent": "scene",
      "code": "console.log('scene example length: ' + scene.example().length);"
    },
    {
      "kind": "example",
      "id": "example:SceneApi.getSelected",
      "symbol": "SceneApi.getSelected",
      "intent": "scene",
      "code": "// 1. Read the current selection (empty array when nothing is selected).\nconst before = scene.getSelected();\nconsole.log('selected before: ' + before.length);\n// 2. Select every node, then read the selection back.\nawait select(ls());\nconsole.log('selected after select-all: ' + scene.getSelected().length);\n// 3. Clear the selection.\nawait select(null, { mode: 'clear' });\n// 4. Confirm the reader reflects the clear.\nconsole.log('selected after clear: ' + scene.getSelected().length);"
    },
    {
      "kind": "example",
      "id": "example:SceneApi.help",
      "symbol": "SceneApi.help",
      "intent": "scene",
      "code": "console.log('scene help length: ' + scene.help().length);"
    },
    {
      "kind": "example",
      "id": "example:SceneApi.ls",
      "symbol": "SceneApi.ls",
      "intent": "scene",
      "code": "const all = ls();\nconst meshes = ls({ type: 'mesh' });\nconst selected = ls({ selected: true });\nconsole.log('total: ' + all.length + ' mesh: ' + meshes.length + ' sel: ' + selected.length);"
    },
    {
      "kind": "example",
      "id": "example:SceneApi.select",
      "symbol": "SceneApi.select",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1 });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(cube);\nawait select(null, { mode: 'all', filter: { type: 'mesh' } });\nawait select(null, { mode: 'clear' });\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SceneResidualApi.delete",
      "symbol": "SceneResidualApi.delete",
      "intent": "scene",
      "code": "// 1. Create a node to delete.\nconst cube = await create.cube({ name: 'scene-delete-demo' });\nconsole.log('created: ' + cube.name);\n// 2. Confirm it is in the scene before deletion.\nconsole.log('present: ' + (ls('scene-delete-demo').length === 1));\n// 3. Delete it via the handle.\nawait cube.delete();\nconsole.log('deleted scene-delete-demo');\n// 4. It is gone from the scene afterwards.\nconsole.log('absent: ' + (ls('scene-delete-demo').length === 0));"
    },
    {
      "kind": "example",
      "id": "example:SculptOpts.brushType",
      "symbol": "SculptOpts.brushType",
      "intent": "scene",
      "code": "const sphere = await create.sphere({ name: 'sculpt-brush-demo' });\nconst session = await tool.sculpt.begin(sphere, { brushType: 'clay' });\nawait session?.cancel?.();\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptOpts.radius",
      "symbol": "SculptOpts.radius",
      "intent": "scene",
      "code": "const sphere = await create.sphere({ name: 'sculpt-radius-demo' });\nconst session = await tool.sculpt.begin(sphere, { radius: 0.25 });\nawait session?.cancel?.();\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptOpts.strength",
      "symbol": "SculptOpts.strength",
      "intent": "scene",
      "code": "const sphere = await create.sphere({ name: 'sculpt-strength-demo' });\nconst session = await tool.sculpt.begin(sphere, { strength: 0.5 });\nawait session?.cancel?.();\nawait sphere.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.clearMask",
      "symbol": "SculptSession.clearMask",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-clear-mask-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.clearMask();\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.example",
      "symbol": "SculptSession.example",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-example-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nconst text = session.example();\nconsole.log('example length: ' + text.length);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.getMaskArray",
      "symbol": "SculptSession.getMaskArray",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-get-mask-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nconst mask = session.getMaskArray();\nconsole.log('mask length: ' + mask.length);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.help",
      "symbol": "SculptSession.help",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-help-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nconst text = session.help();\nconsole.log('help length: ' + text.length);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setAutoSmooth",
      "symbol": "SculptSession.setAutoSmooth",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-auto-smooth-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setAutoSmooth(0.25);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setBrushType",
      "symbol": "SculptSession.setBrushType",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-brush-type-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setBrushType('inflate');\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setCreasePinch",
      "symbol": "SculptSession.setCreasePinch",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-crease-pinch-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setBrushType('crease');\nsession.setCreasePinch(0.4);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setElasticDeform",
      "symbol": "SculptSession.setElasticDeform",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-elastic-deform-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setBrushType('elastic-deform');\nsession.setElasticDeform('twist');\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setFalloff",
      "symbol": "SculptSession.setFalloff",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-falloff-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setFalloff('smooth');\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setFrontFacesOnly",
      "symbol": "SculptSession.setFrontFacesOnly",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-front-faces-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setFrontFacesOnly(true);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setHardness",
      "symbol": "SculptSession.setHardness",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-hardness-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setHardness(0.6);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setLayerHeight",
      "symbol": "SculptSession.setLayerHeight",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-layer-height-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setBrushType('layer');\nsession.setLayerHeight(0.02);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setMaskArray",
      "symbol": "SculptSession.setMaskArray",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-set-mask-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nconst mask = session.getMaskArray();\nconst newMask = new Float32Array(mask.length);\nnewMask.fill(0);\nsession.setMaskArray(newMask);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setNormalBlend",
      "symbol": "SculptSession.setNormalBlend",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-normal-blend-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setNormalBlend(0.7);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setPressure",
      "symbol": "SculptSession.setPressure",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-pressure-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setPressure({ size: true, strength: true });\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setRadius",
      "symbol": "SculptSession.setRadius",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-radius-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setRadius(0.05);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setSmoothStrength",
      "symbol": "SculptSession.setSmoothStrength",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-smooth-strength-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setSmoothStrength(0.5);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setStrength",
      "symbol": "SculptSession.setStrength",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-strength-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setStrength(0.75);\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setSurfaceSmoothParams",
      "symbol": "SculptSession.setSurfaceSmoothParams",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-surface-smooth-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setSurfaceSmoothParams({ shapePreservation: 0.5, currentVertex: 0.5, iterations: 3 });\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SculptSession.setSymmetry",
      "symbol": "SculptSession.setSymmetry",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'sculpt-symmetry-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = await cube.tools.sculpt({ brushType: 'clay', radius: 0.1, strength: 0.5 });\nsession.setSymmetry({ x: true, y: false, z: false });\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceFreeformOpts.mode",
      "symbol": "SliceFreeformOpts.mode",
      "intent": "scene",
      "code": "// 1) Build a cube and open a freeform slice session.\nconst cube = await create.cube({ name: 'slice-mode-tag-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'freeform' }));\n// 2) The `mode` tag pins the freeform path.\nconsole.log('slice mode tag: freeform');\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceFreeformSession.example",
      "symbol": "SliceFreeformSession.example",
      "intent": "scene",
      "code": "// 1) Build a cube and open a freeform slice session.\nconst cube = await create.cube({ name: 'slice-example-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'freeform' }));\n// 2) Render the example snippet.\nconst snippet = session.example();\nconsole.log('example length: ' + snippet.length);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceFreeformSession.help",
      "symbol": "SliceFreeformSession.help",
      "intent": "scene",
      "code": "// 1) Build a cube and open a freeform slice session.\nconst cube = await create.cube({ name: 'slice-help-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'freeform' }));\n// 2) Render the help string.\nconst helpText = session.help();\nconsole.log('help length: ' + helpText.length);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceFreeformSession.setEndpoints",
      "symbol": "SliceFreeformSession.setEndpoints",
      "intent": "scene",
      "code": "// 1) Build a cube and open a freeform slice session.\nconst cube = await create.cube({ name: 'slice-endpoints-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'freeform' }));\n// 2) setEndpoints([cx1, cy1], [cx2, cy2]) places the two cut endpoints — but\n//    it is an INTERACTIVE-TOOL-DRIVEN step: it raycasts the live render mesh\n//    in the open viewport to land each point on the surface, so it only runs\n//    against a mounted, settled viewport (ray misses are silent no-ops; a\n//    fresh / off-screen mesh that hasn't rendered yet has no scene mesh to\n//    hit). The headless-valid demonstration opens the session + confirms it.\nconsole.log('freeform slice session active: ' + session.isActive); // → true\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceOpts.axis",
      "symbol": "SliceOpts.axis",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'slice-axis-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.slice({ mode: 'plane', axis: 'x', offset: 0 });\nconsole.log('after X slice — verts: ' + cube.verts.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceOpts.fill",
      "symbol": "SliceOpts.fill",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'slice-fill-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.slice({ mode: 'plane', axis: 'y', offset: 0, fill: true });\nconsole.log('after fill-flag slice — faces: ' + cube.faces.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceOpts.mode",
      "symbol": "SliceOpts.mode",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'slice-mode-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.slice({ mode: 'plane', axis: 'y', offset: 0 });\nconsole.log('after plane slice — verts: ' + cube.verts.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceOpts.offset",
      "symbol": "SliceOpts.offset",
      "intent": "scene",
      "code": "const cube = await create.cube({ width: 2, name: 'slice-offset-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait cube.slice({ mode: 'plane', axis: 'x', offset: 0.3 });\nconsole.log('after offset slice — verts: ' + cube.verts.count);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceOpts.oneShot",
      "symbol": "SliceOpts.oneShot",
      "intent": "scene",
      "code": "// 1) Build a cube and request a one-shot plane slice via the router.\nconst cube = await create.cube({ name: 'slice-router-oneshot-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// Slice at local offset 0 (the cube's centre) so the plane is guaranteed\n// to intersect — a boundary-grazing offset like 0.5 on a unit cube would\n// make the cut a no-op (result.ok === false).\nconst result = await cube.tools.slice({\n    mode: 'plane', oneShot: true, axis: 'y', offset: 0,\n});\n// 2) `oneShot: true` skipped the interactive session and returned the result directly.\nconsole.log('router one-shot ok: ' + result.ok);\n// 3) Tear down.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlicePlaneOpts.axis",
      "symbol": "SlicePlaneOpts.axis",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session with an initial Y axis.\nconst cube = await create.cube({ name: 'slice-plane-axis-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0 }));\n// 2) The session opened with the Y axis pre-selected.\nconsole.log('initial slice axis: y');\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlicePlaneOpts.fill",
      "symbol": "SlicePlaneOpts.fill",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session passing fill: true.\nconst cube = await create.cube({ name: 'slice-plane-fill-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({\n    mode: 'plane', axis: 'y', offset: 0.5, fill: true,\n}));\n// 2) The flag is accepted for shape-symmetry with `mesh.bisect` but ignored here.\nconsole.log('fill flag accepted, dropped at primitive boundary');\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlicePlaneOpts.mode",
      "symbol": "SlicePlaneOpts.mode",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-plane-mode-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.5 }));\n// 2) The `mode: 'plane'` tag pinned the plane-mode session.\nconsole.log('slice mode tag: plane');\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlicePlaneOpts.offset",
      "symbol": "SlicePlaneOpts.offset",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session with a Y offset of 0.25.\nconst cube = await create.cube({ name: 'slice-plane-offset-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.25 }));\n// 2) The session opened with the cutting plane shifted to Y = 0.25.\nconsole.log('initial slice offset: 0.25');\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlicePlaneParams.planeAxis",
      "symbol": "SlicePlaneParams.planeAxis",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-plane-axis-param-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0 }));\n// 2) Switch the live plane axis to Z; the preview refreshes.\nsession.setAxis('z');\nconsole.log('plane axis updated to: z');\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlicePlaneParams.planeOffset",
      "symbol": "SlicePlaneParams.planeOffset",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-plane-offset-param-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0 }));\n// 2) Nudge the live offset to 0.25; the preview refreshes.\nsession.setOffset(0.25);\nconsole.log('plane offset updated to: 0.25');\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlicePlaneSession.example",
      "symbol": "SlicePlaneSession.example",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-plane-example-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.5 }));\n// 2) Render the example snippet.\nconst snippet = session.example();\nconsole.log('example length: ' + snippet.length);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlicePlaneSession.help",
      "symbol": "SlicePlaneSession.help",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-plane-help-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.5 }));\n// 2) Render the help string.\nconst helpText = session.help();\nconsole.log('help length: ' + helpText.length);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlicePlaneSession.oneShot",
      "symbol": "SlicePlaneSession.oneShot",
      "intent": "scene",
      "code": "// 1) Build a cube and run a one-shot plane slice with no preview session.\nconst cube = await create.cube({ name: 'slice-plane-oneshot-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// Slice at local offset 0 (the cube's centre) so the plane is guaranteed\n// to intersect — a boundary-grazing offset like 0.5 on a unit cube would\n// make the cut a no-op (result.ok === false).\nconst result = await cube.tools.slice({\n    mode: 'plane', oneShot: true, axis: 'y', offset: 0,\n});\n// 2) The one-shot path commits in one call and returns the primitive's result.\nconsole.log('one-shot slice ok: ' + result.ok);\n// 3) Tear down.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlicePlaneSession.setAxis",
      "symbol": "SlicePlaneSession.setAxis",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-plane-set-axis-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.5 }));\n// 2) Switch the cut plane to Z; the preview refreshes automatically.\nsession.setAxis('z');\nconsole.log('slice axis set to: z');\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlicePlaneSession.setOffset",
      "symbol": "SlicePlaneSession.setOffset",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-plane-set-offset-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.5 }));\n// 2) Nudge the cutting plane down to Y = 0.25; the preview refreshes.\nsession.setOffset(0.25);\nconsole.log('slice offset set to: 0.25');\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.addInput",
      "symbol": "SliceRouterSession.addInput",
      "intent": "scene",
      "code": "// 1) Build a cube and open a freeform slice session through the router.\nconst cube = await create.cube({ name: 'slice-router-add-input-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'freeform' }));\n// 2) addInput([clientX, clientY]) appends one endpoint — but it is an\n//    INTERACTIVE-TOOL-DRIVEN step: it raycasts the live render mesh in the\n//    open viewport to land the point on the surface, so it only runs against\n//    a mounted, settled viewport (ray misses are silent no-ops; a fresh /\n//    off-screen mesh that hasn't rendered yet has no scene mesh to hit). The\n//    headless-valid demonstration opens the session + confirms it is live.\nconsole.log('slice router session active: ' + session.isActive); // → true\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.cancel",
      "symbol": "SliceRouterSession.cancel",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-router-cancel-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.5 }));\n// 2) Discard without committing.\nsession.cancel();\nconsole.log('slice session cancelled');\n// 3) Tear down.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.commit",
      "symbol": "SliceRouterSession.commit",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-router-commit-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// Plane mode is fully headless (no viewport raycast). Slice at local\n// offset 0 — the cube's centre — so the plane is guaranteed to intersect\n// (a boundary-grazing offset like 0.5 on a unit cube would no-op the cut).\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0 }));\n// 2) Commit the cut; one history entry is recorded.\nawait session.commit();\nconsole.log('slice committed');\n// 3) Tear down.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.example",
      "symbol": "SliceRouterSession.example",
      "intent": "scene",
      "code": "// 1) Build a cube and open the slice router (freeform mode is default).\nconst cube = await create.cube({ name: 'slice-router-example-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'freeform' }));\n// 2) Render the example snippet.\nconst snippet = session.example();\nconsole.log('example length: ' + snippet.length);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.help",
      "symbol": "SliceRouterSession.help",
      "intent": "scene",
      "code": "// 1) Build a cube and open the slice router (freeform mode is default).\nconst cube = await create.cube({ name: 'slice-router-help-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'freeform' }));\n// 2) Render the help string.\nconst helpText = session.help();\nconsole.log('help length: ' + helpText.length);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.isActive",
      "symbol": "SliceRouterSession.isActive",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-router-is-active-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.5 }));\n// 2) The session is open until commit/cancel runs.\nconsole.log('slice isActive: ' + session.isActive);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.mode",
      "symbol": "SliceRouterSession.mode",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-router-mode-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.5 }));\n// 2) Confirm the router picked the plane inner session.\nconsole.log('slice router mode: ' + session.mode);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.preview",
      "symbol": "SliceRouterSession.preview",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-router-preview-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.5 }));\n// 2) Force a preview refresh (no-arg style — uses live params).\nsession.preview({});\nconsole.log('preview refreshed');\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.setAxis",
      "symbol": "SliceRouterSession.setAxis",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-router-set-axis-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.5 }));\n// 2) Switch the cut plane to Z; the preview refreshes automatically.\nsession.setAxis('z');\nconsole.log('router axis set to: z');\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.setEndpoints",
      "symbol": "SliceRouterSession.setEndpoints",
      "intent": "scene",
      "code": "// 1) Build a cube and open a freeform slice session.\nconst cube = await create.cube({ name: 'slice-router-set-endpoints-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'freeform' }));\n// 2) setEndpoints([cx1, cy1], [cx2, cy2]) places both cut endpoints — but it\n//    is an INTERACTIVE-TOOL-DRIVEN step: it raycasts the live render mesh in\n//    the open viewport to land each point on the surface, so it only runs\n//    against a mounted, settled viewport (ray misses are silent no-ops; a\n//    fresh / off-screen mesh that hasn't rendered yet has no scene mesh to\n//    hit). The headless-valid demonstration opens the session + confirms it.\nconsole.log('freeform slice router session active: ' + session.isActive); // → true\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.setOffset",
      "symbol": "SliceRouterSession.setOffset",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-router-set-offset-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.5 }));\n// 2) Nudge the cutting plane to Y = 0.25; the preview refreshes.\nsession.setOffset(0.25);\nconsole.log('router offset set to: 0.25');\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.start",
      "symbol": "SliceRouterSession.start",
      "intent": "scene",
      "code": "// 1) Build a cube and open the slice router (freeform mode is default).\nconst cube = await create.cube({ name: 'slice-router-start-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'freeform' }));\n// 2) Begin the session explicitly (idempotent — interactive setters also auto-start).\nsession.start();\nconsole.log('slice router started');\n// 3) Tear down without committing.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SliceRouterSession.state",
      "symbol": "SliceRouterSession.state",
      "intent": "scene",
      "code": "// 1) Build a cube and open a plane-mode slice session.\nconst cube = await create.cube({ name: 'slice-router-state-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst session = (await cube.tools.slice({ mode: 'plane', axis: 'y', offset: 0.5 }));\n// 2) Read the live state of the inner session.\nconsole.log('slice state: ' + session.state);\n// 3) Tear down.\nawait session.cancel();\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlideData.isClosed",
      "symbol": "SlideData.isClosed",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'slidedata-isclosed-demo' });\nconst data = cube.computeEdgeSlideData([0]);\nconsole.log('slide isClosed is boolean: ' + (typeof data?.isClosed === 'boolean'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlideData.kind",
      "symbol": "SlideData.kind",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'slidedata-kind-demo' });\nconst data = cube.computeEdgeSlideData([0]);\nconsole.log('slide kind: ' + (data?.kind ?? 'none'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SlideData.vertices",
      "symbol": "SlideData.vertices",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'slidedata-vertices-demo' });\nconst data = cube.computeEdgeSlideData([0]);\nconsole.log('slide vertices is array: ' + Array.isArray(data?.vertices));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SpatialClosestPointHit.barycentric",
      "symbol": "SpatialClosestPointHit.barycentric",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'closest-bary-demo' });\nconst hit = cube.geometry.closestPoint([5, 1.3, -0.7]);\nconsole.log('barycentric is array: ' + Array.isArray(hit?.barycentric));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SpatialClosestPointHit.distance",
      "symbol": "SpatialClosestPointHit.distance",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'closest-distance-demo' });\nconst hit = cube.geometry.closestPoint([5, 1.3, -0.7]);\nconsole.log('closest distance is number: ' + (typeof hit?.distance === 'number'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SpatialClosestPointHit.face",
      "symbol": "SpatialClosestPointHit.face",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'closest-face-demo' });\nconst hit = cube.geometry.closestPoint([5, 1.3, -0.7]);\nconsole.log('closest face is object: ' + (typeof hit?.face === 'object'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SpatialClosestPointHit.normal",
      "symbol": "SpatialClosestPointHit.normal",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'closest-normal-demo' });\nconst hit = cube.geometry.closestPoint([5, 1.3, -0.7]);\nconsole.log('closest normal is array: ' + Array.isArray(hit?.normal));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SpatialClosestPointHit.position",
      "symbol": "SpatialClosestPointHit.position",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'closest-pos-demo' });\nconst hit = cube.geometry.closestPoint([5, 1.3, -0.7]);\nconsole.log('closest position is array: ' + Array.isArray(hit?.position));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SpatialRaycastHit.distance",
      "symbol": "SpatialRaycastHit.distance",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'raycast-distance-demo' });\nconst hit = cube.geometry.raycast([5, 1.3, -0.7], [-1, 0, 0]);\nconsole.log('hit distance is number: ' + (typeof hit?.distance === 'number'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SpatialRaycastHit.face",
      "symbol": "SpatialRaycastHit.face",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'raycast-face-demo' });\nconst hit = cube.geometry.raycast([5, 1.3, -0.7], [-1, 0, 0]);\nconsole.log('hit face is object: ' + (typeof hit?.face === 'object'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SpatialRaycastHit.normal",
      "symbol": "SpatialRaycastHit.normal",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'raycast-normal-demo' });\nconst hit = cube.geometry.raycast([5, 1.3, -0.7], [-1, 0, 0]);\nconsole.log('hit normal is array: ' + Array.isArray(hit?.normal));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:SpatialRaycastHit.point",
      "symbol": "SpatialRaycastHit.point",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'raycast-point-demo' });\nconst hit = cube.geometry.raycast([5, 1.3, -0.7], [-1, 0, 0]);\nconsole.log('hit point is array: ' + Array.isArray(hit?.point));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Sphere.center",
      "symbol": "Sphere.center",
      "intent": "math",
      "code": "// 1) Build a sphere offset along Y.\nconst s = MathLib.sphere([0, 5, 0], 1);\nconsole.log('center: ' + JSON.stringify(s.center.toArray()));\n// 2) Each access returns a fresh copy.\nconst c = s.center;\nc.y = 999;\nconsole.log('sphere center unchanged: ' + JSON.stringify(s.center.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:Sphere.clone",
      "symbol": "Sphere.clone",
      "intent": "math",
      "code": "// 1) Build a sphere and clone it.\nconst a = MathLib.sphere([0, 0, 0], 5);\nconst b = a.clone();\n// 2) The clone compares equal but is a separate instance.\nconsole.log('equals: ' + a.equals(b));\n// 3) Expanding the clone leaves the original alone.\nconst grown = b.expand([10, 0, 0]);\nconsole.log('original radius: ' + a.radius);\nconsole.log('grown radius >= 10: ' + (grown.radius >= 10));"
    },
    {
      "kind": "example",
      "id": "example:Sphere.contains",
      "symbol": "Sphere.contains",
      "intent": "math",
      "code": "// 1) Build a sphere of radius 5 at the origin.\nconst s = MathLib.sphere([0, 0, 0], 5);\n// 2) Interior point.\nconsole.log('inside: ' + s.contains([0, 0, 0]));\n// 3) Point past the surface.\nconsole.log('outside: ' + s.contains([10, 0, 0]));\n// 4) Point exactly on the surface counts as contained.\nconsole.log('on surface: ' + s.contains([5, 0, 0]));"
    },
    {
      "kind": "example",
      "id": "example:Sphere.distanceToPoint",
      "symbol": "Sphere.distanceToPoint",
      "intent": "math",
      "code": "// 1) Sphere at origin with radius 5.\nconst s = MathLib.sphere([0, 0, 0], 5);\n// 2) Point inside (1 unit from origin, radius 5 -> distance -4).\nconsole.log('inside distance: ' + s.distanceToPoint([1, 0, 0]));\n// 3) Point outside (10 from origin, radius 5 -> distance 5).\nconsole.log('outside distance: ' + s.distanceToPoint([10, 0, 0]));\n// 4) Point on the surface.\nconsole.log('on surface: ' + s.distanceToPoint([5, 0, 0]));"
    },
    {
      "kind": "example",
      "id": "example:Sphere.equals",
      "symbol": "Sphere.equals",
      "intent": "math",
      "code": "// 1) Two equivalent spheres.\nconst a = MathLib.sphere([0, 0, 0], 5);\nconst b = MathLib.sphere([0, 0, 0], 5);\nconsole.log('equal exact: ' + a.equals(b));\n// 2) Small float drift inside default epsilon.\nconst c = MathLib.sphere([0, 0, 1e-9], 5);\nconsole.log('equal within default eps: ' + a.equals(c));\n// 3) Tightening epsilon catches the drift.\nconsole.log('equal within tight eps: ' + a.equals(c, 1e-12));"
    },
    {
      "kind": "example",
      "id": "example:Sphere.expand",
      "symbol": "Sphere.expand",
      "intent": "math",
      "code": "// 1) Build a sphere of radius 2 at the origin.\nconst s = MathLib.sphere([0, 0, 0], 2);\n// 2) Expand by a point outside (distance 5 from origin).\nconst grown = s.expand([5, 0, 0]);\nconsole.log('grown radius: ' + grown.radius);\n// 3) Expanding by a point already inside leaves radius alone.\nconst same = s.expand([1, 0, 0]);\nconsole.log('same radius: ' + same.radius);\n// 4) The original is untouched.\nconsole.log('original radius: ' + s.radius);"
    },
    {
      "kind": "example",
      "id": "example:Sphere.fromBoundingBox",
      "symbol": "Sphere.fromBoundingBox",
      "intent": "math",
      "code": "// 1) Build a 2x2x2 box around the origin.\nconst bbox = MathLib.boundingBox([-1, -1, -1], [1, 1, 1]);\n// 2) Enclose it in a sphere.\nconst sphere = Sphere.fromBoundingBox(bbox);\nconsole.log('center: ' + JSON.stringify(sphere.center.toArray()));\n// Radius is half the (2,2,2) diagonal = sqrt(3).\nconsole.log('radius: ' + sphere.radius.toFixed(3));\n// 3) All eight corners are inside the sphere.\nconsole.log('contains corner: ' + sphere.contains([1, 1, 1]));"
    },
    {
      "kind": "example",
      "id": "example:Sphere.fromPoints",
      "symbol": "Sphere.fromPoints",
      "intent": "math",
      "code": "// 1) Build a sphere around a tetrahedron of points.\nconst sphere = Sphere.fromPoints([\n    [1, 0, 0], [0, 1, 0], [0, 0, 1], [-1, -1, -1],\n]);\n// 2) Every input point lies inside (or on) the sphere.\nconsole.log('contains (1,0,0): ' + sphere.contains([1, 0, 0]));\nconsole.log('enclosing radius positive: ' + (sphere.radius > 0));\nconsole.log('center: ' + JSON.stringify(sphere.center.toArray().map(v => Math.round(v * 1000) / 1000)));"
    },
    {
      "kind": "example",
      "id": "example:Sphere.intersects",
      "symbol": "Sphere.intersects",
      "intent": "math",
      "code": "// 1) Overlapping pair.\nconst a = MathLib.sphere([0, 0, 0], 5);\nconst b = MathLib.sphere([3, 0, 0], 5);\nconsole.log('overlap: ' + a.intersects(b));\n// 2) Disjoint pair (centers 20 apart, radii 5 + 5 = 10).\nconst c = MathLib.sphere([20, 0, 0], 5);\nconsole.log('disjoint: ' + a.intersects(c));\n// 3) Touching spheres (centers exactly 10 apart) count as intersecting.\nconst d = MathLib.sphere([10, 0, 0], 5);\nconsole.log('touching: ' + a.intersects(d));"
    },
    {
      "kind": "example",
      "id": "example:Sphere.intersectsBox",
      "symbol": "Sphere.intersectsBox",
      "intent": "math",
      "code": "// 1) Sphere at origin, box that overlaps it.\nconst s = MathLib.sphere([0, 0, 0], 2);\nconst box = MathLib.boundingBox([1, 1, 1], [5, 5, 5]);\nconsole.log('overlap: ' + s.intersectsBox(box));\n// 2) Box well outside the sphere.\nconst far = MathLib.boundingBox([10, 10, 10], [20, 20, 20]);\nconsole.log('disjoint: ' + s.intersectsBox(far));"
    },
    {
      "kind": "example",
      "id": "example:Sphere.intersectsPlane",
      "symbol": "Sphere.intersectsPlane",
      "intent": "math",
      "code": "// 1) Sphere at origin, radius 2. Ground plane through y = 0.\nconst s = MathLib.sphere([0, 0, 0], 2);\nconst ground = MathLib.plane([0, 1, 0], 0);\n// 2) The plane slices through the sphere.\nconsole.log('cuts ground: ' + s.intersectsPlane(ground));\n// 3) A plane above the sphere does not.\nconst high = MathLib.plane([0, 1, 0], -10);\nconsole.log('cuts high: ' + s.intersectsPlane(high));"
    },
    {
      "kind": "example",
      "id": "example:Sphere.radius",
      "symbol": "Sphere.radius",
      "intent": "math",
      "code": "// 1) Read the radius of a sphere.\nconst s = MathLib.sphere([0, 0, 0], 5);\nconsole.log('radius: ' + s.radius);\n// 2) A zero-radius sphere is a point.\nconst pt = MathLib.sphere([1, 2, 3], 0);\nconsole.log('point radius: ' + pt.radius);\nconsole.log('point contains center: ' + pt.contains([1, 2, 3]));"
    },
    {
      "kind": "example",
      "id": "example:Sphere.toArray",
      "symbol": "Sphere.toArray",
      "intent": "math",
      "code": "// 1) Build a sphere of radius 5 at the origin.\nconst s = MathLib.sphere([0, 0, 0], 5);\n// 2) Serialize to a plain object.\nconst obj = s.toArray();\nconsole.log('sphere: ' + JSON.stringify(obj));\n// 3) The returned arrays are copies — mutating them does not change the sphere.\nobj.center[0] = 999;\nconsole.log('sphere unchanged: ' + JSON.stringify(s.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:ToolResidualApi.sculpt",
      "symbol": "ToolResidualApi.sculpt",
      "intent": "scene",
      "code": "// 1. Read the sculpt sub-namespace handle.\nconst sculpt = tool.sculpt;\nconsole.log('sculpt is object = ' + (typeof sculpt === 'object' && sculpt !== null));\n// 2. The handle exposes an enter() entry point.\nconsole.log('has enter = ' + (typeof sculpt.enter === 'function'));\n// 3. It also carries the introspection pair help / example.\nconsole.log('has help = ' + (typeof sculpt.help === 'function'));\n// 4. Read its help text length without entering a session.\nconsole.log('sculpt help length = ' + sculpt.help().length);"
    },
    {
      "kind": "example",
      "id": "example:ToolResidualApi.sim",
      "symbol": "ToolResidualApi.sim",
      "intent": "scene",
      "code": "// 1. Read the sim sub-namespace handle.\nconst sim = tool.sim;\nconsole.log('sim is object = ' + (typeof sim === 'object' && sim !== null));\n// 2. The handle exposes the toggle entry point.\nconsole.log('has toggle = ' + (typeof sim.toggle === 'function'));\n// 3. And the quad-remesh cloth-prep helper.\nconsole.log('has remeshToQuads = ' + (typeof sim.remeshToQuads === 'function'));\n// 4. Read its help text length without opening the simulator.\nconsole.log('sim help length = ' + sim.help().length);"
    },
    {
      "kind": "example",
      "id": "example:ToolSimResidualApi.addCrossMeshSeam",
      "symbol": "ToolSimResidualApi.addCrossMeshSeam",
      "intent": "scene",
      "code": "// 1. Build two flat panels offset along Z so they form front + back.\nconst front = await create.plane({ width: 1, height: 1 });\nconst back = await create.plane({ width: 1, height: 1 });\nback.xform({ t: [0, 0, -0.3] });\n// 2. Select one edge on each panel (component subselection handle, 'add'\n//    unions back's edge into the multi-mesh selection).\nfront.edges([0]).select();\nback.edges([0]).select('add');\n// 3. Pair the two selected edges as a cross-mesh stitch seam.\nawait tool.sim.addCrossMeshSeam({});\nconsole.log('cross-mesh seam added');\n// 4. Clean up both whole meshes via their handles (component mode safe).\nawait front.delete();\nawait back.delete();"
    },
    {
      "kind": "example",
      "id": "example:ToolSimResidualApi.drawPolygon",
      "symbol": "ToolSimResidualApi.drawPolygon",
      "intent": "scene",
      "code": "// 1. Activate the polygon-draw cloth tool.\nawait tool.sim.drawPolygon({});\nconsole.log('polygon-draw tool active');\n// 2. Programmatic point input is not exposed — the user clicks viewport\n//    points to build the panel.\nconsole.log('awaiting viewport clicks for the panel outline');\n// 3. Return to the default selection tool when finished.\ntool.setActive(null);\nconsole.log('active tool = ' + tool.active);\n// 4. Confirm we left polygon-draw mode.\nconsole.log('back to selection = ' + (tool.active === null));"
    },
    {
      "kind": "example",
      "id": "example:ToolSimResidualApi.example",
      "symbol": "ToolSimResidualApi.example",
      "intent": "scene",
      "code": "// 1. Fetch the runnable example source for the sim sub-namespace.\nconst text = tool.sim.example();\nconsole.log('example is string = ' + (typeof text === 'string'));\n// 2. It is non-empty.\nconsole.log('example length = ' + text.length);\n// 3. Example works without opening the simulator popup.\nconsole.log('first line = ' + text.split('\\n')[0]);\n// 4. Pair it with help() for the prose description.\nconsole.log('help length = ' + tool.sim.help().length);"
    },
    {
      "kind": "example",
      "id": "example:ToolSimResidualApi.help",
      "symbol": "ToolSimResidualApi.help",
      "intent": "scene",
      "code": "// 1. Fetch the help text for the sim sub-namespace.\nconst text = tool.sim.help();\nconsole.log('help is string = ' + (typeof text === 'string'));\n// 2. It is non-empty.\nconsole.log('help length = ' + text.length);\n// 3. Help works without opening the simulator popup.\nconsole.log('first line = ' + text.split('\\n')[0]);\n// 4. Pair it with example() for a copy-paste workflow.\nconsole.log('example length = ' + tool.sim.example().length);"
    },
    {
      "kind": "example",
      "id": "example:ToolSimResidualApi.remeshToQuads",
      "symbol": "ToolSimResidualApi.remeshToQuads",
      "intent": "scene",
      "code": "// 1. Build a flat panel to remesh (widthSegments / heightSegments).\nconst panel = await create.plane({\n    width: 1, height: 1, widthSegments: 4, heightSegments: 4,\n});\npanel.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Select the panel — remeshToQuads acts on the selected mesh.\nawait select(panel);\n// 3. Convert it to a quad-only cloth-sim grid.\nawait tool.sim.remeshToQuads({});\nconsole.log('quad-remesh complete: ' + panel.faces.count + ' faces');\n// 4. Clean up.\nawait panel.delete();"
    },
    {
      "kind": "example",
      "id": "example:ToolSimResidualApi.toggle",
      "symbol": "ToolSimResidualApi.toggle",
      "intent": "scene",
      "code": "// 1. Open the Simulator popup.\nawait tool.sim.toggle({});\nconsole.log('simulator opened');\n// 2. The popup now hosts the start / pause / reset controls.\nconsole.log('drape controls available in the popup');\n// 3. Toggle is idempotent — calling again closes the popup.\nawait tool.sim.toggle({});\nconsole.log('simulator closed');\n// 4. Confirm we can re-open it on demand.\nawait tool.sim.toggle({});\nconsole.log('simulator re-opened');"
    },
    {
      "kind": "example",
      "id": "example:Transform",
      "symbol": "Transform",
      "intent": "scene",
      "code": "// 1. Setup — create a transformable node.\nconst cube = await create.cube({ name: 'transform-demo' });\n// 2. Select — N/A; we have the handle.\n// 3. Action — translate via xform().\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 4. Observe + cleanup.\nconsole.log('cube translate y: ' + cube.translate.y.get().toFixed(3));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Transform.freezeTransform",
      "symbol": "Transform.freezeTransform",
      "intent": "mesh",
      "code": "// 1. Spawn a cube and move + rotate + scale it.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'freeze-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7], r: [0, 45, 0], s: [1.5, 1.5, 1.5] });\n// 2. Bake all three channels into the vertex positions.\ncube.freezeTransform({ translate: true, rotate: true, scale: true });\n// 3. After freeze, translate / rotate / scale read as identity.\nconsole.log('translate post-freeze: ' + JSON.stringify(cube.translate.get()));\nconsole.log('rotate post-freeze: ' + JSON.stringify(cube.rotate.get()));\nconsole.log('scale post-freeze: ' + JSON.stringify(cube.scale.get()));\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Transform.resetTransform",
      "symbol": "Transform.resetTransform",
      "intent": "mesh",
      "code": "// 1. Spawn a cube and apply position, rotation, and scale.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'reset-demo',\n});\ncube.xform({ t: [2.5, 1.3, -0.7], r: [0, 45, 0], s: [1.5, 1.5, 1.5] });\nconsole.log('before reset: ' + JSON.stringify(cube.translate.get()));\n// 2. Reset everything to identity.\ncube.resetTransform();\n// 3. Verify the defaults are back.\nconsole.log('translate after reset: ' + JSON.stringify(cube.translate.get()));\nconsole.log('rotate after reset: ' + JSON.stringify(cube.rotate.get()));\nconsole.log('scale after reset: ' + JSON.stringify(cube.scale.get()));\n// 4. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Transform.rotate",
      "symbol": "Transform.rotate",
      "intent": "mesh",
      "code": "// 1. Spawn a cube.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'rotate-demo',\n});\n// 2. Translate off origin so the result is visible.\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 3. Rotate 45 degrees around Y via the compound setter.\ncube.rotate.set([0, 45, 0]);\n// 4. Bump X rotation through the per-axis handle.\ncube.rotate.x.set(30);\n// 5. Log the result.\nconsole.log('rotate: ' + JSON.stringify(cube.rotate.get()));\n// 6. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Transform.scale",
      "symbol": "Transform.scale",
      "intent": "mesh",
      "code": "// 1. Spawn a cube.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'scale-demo',\n});\n// 2. Translate off origin.\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 3. Apply a uniform 1.5x scale.\ncube.scale.set([1.5, 1.5, 1.5]);\n// 4. Stretch only Y through the per-axis handle.\ncube.scale.y.set(2);\n// 5. Log the result.\nconst s = cube.scale.get();\nconsole.log('scale: ' + JSON.stringify(s));\nconsole.log('uniform: ' + (s[0] === s[2]));\n// 6. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Transform.translate",
      "symbol": "Transform.translate",
      "intent": "mesh",
      "code": "// 1. Spawn a cube to operate on.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'translate-demo',\n});\n// 2. Write the whole position in one call.\ncube.translate.set([2.5, 1.3, -0.7]);\n// 3. Tweak just the Y axis through the per-axis handle.\ncube.translate.y.set(cube.translate.y.get() + 0.5);\n// 4. Log the result so a test harness can verify.\nconsole.log('translate: ' + JSON.stringify(cube.translate.get()));\n// 5. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Transform.xform",
      "symbol": "Transform.xform",
      "intent": "mesh",
      "code": "// 1. Spawn a cube to operate on.\nconst cube = await create.cube({\n    width: 1, height: 1, depth: 1, name: 'xform-demo',\n});\n// 2. Write translate, rotate, and scale in one absolute call.\ncube.xform({\n    t: [2.5, 1.3, -0.7],\n    r: [0, 45, 0],\n    s: [1, 2, 1],\n    space: 'object',\n    relative: false,\n});\nconsole.log('after absolute: ' + JSON.stringify(cube.translate.get()));\n// 3. Nudge it 0.1 units along X relative to its current pose.\ncube.xform({ t: [0.1, 0, 0], relative: true });\nconsole.log('after relative: ' + JSON.stringify(cube.translate.get()));\n// 4. Move it 0.05 units in world X (immune to parent rotation).\ncube.xform({ t: [0.05, 0, 0], space: 'world', relative: true });\nconsole.log('after world delta: ' + JSON.stringify(cube.translate.get()));\n// 5. Clean up.\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:UtilsBarrelResidualApi.version",
      "symbol": "UtilsBarrelResidualApi.version",
      "intent": "scene",
      "code": "// 1. Read the API-conditioning version off the Utils barrel.\nconst { bootstrapPayloadVersion } = Utils.version();\n// 2. It is a non-empty string (e.g. the maintained doctrine version).\nconsole.log(\"conditioned for AnimBox \" + bootstrapPayloadVersion);\n// 3. Stamp it onto a cache key so a doctrine bump invalidates the cache.\nconst cacheKey = \"animbox-doctrine-\" + bootstrapPayloadVersion;\nconsole.log(\"cache key = \" + cacheKey);\n// 4. A repeat read is stable within a session (same returned value).\nconsole.log(\"stable: \" + (Utils.version().bootstrapPayloadVersion === bootstrapPayloadVersion));"
    },
    {
      "kind": "example",
      "id": "example:UVIslandInfo.bounds",
      "symbol": "UVIslandInfo.bounds",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'island-bounds-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst islands = cube.uv.islands;\nconst b = islands[0]?.bounds;\nconsole.log('first island bounds minU/maxU: ' + (b?.minU ?? 'none') + '/' + (b?.maxU ?? 'none'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:UVIslandInfo.faceIndices",
      "symbol": "UVIslandInfo.faceIndices",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'island-faces-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst islands = cube.uv.islands;\nconsole.log('first island face count: ' + (islands[0]?.faceIndices.length ?? 0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:UVIslandInfo.index",
      "symbol": "UVIslandInfo.index",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'island-index-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst islands = cube.uv.islands;\nconsole.log('first island index: ' + (islands[0]?.index ?? 'none'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:UVIslandInfo.loopIndices",
      "symbol": "UVIslandInfo.loopIndices",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'island-loops-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst islands = cube.uv.islands;\nconsole.log('first island loop count: ' + (islands[0]?.loopIndices.length ?? 0));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:UVNearestLoopHit.distance",
      "symbol": "UVNearestLoopHit.distance",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-nearest-dist-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst hit = cube.uv.closest([0.5, 0.5]);\nconsole.log('closest distance: ' + (hit?.distance.toFixed(4) ?? 'none'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:UVNearestLoopHit.loopIndex",
      "symbol": "UVNearestLoopHit.loopIndex",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'uv-nearest-loop-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst hit = cube.uv.closest([0.5, 0.5]);\nconsole.log('closest loopIndex: ' + (hit?.loopIndex ?? 'none'));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ValidationConsoleLine.text",
      "symbol": "ValidationConsoleLine.text",
      "intent": "scene",
      "code": "// Read a captured console line's text.\nconst line = { type: 'log', text: 'cleaned 3 faces' };\nconsole.log('text: ' + line.text);"
    },
    {
      "kind": "example",
      "id": "example:ValidationConsoleLine.type",
      "symbol": "ValidationConsoleLine.type",
      "intent": "scene",
      "code": "// Each captured console line carries a channel + text.\nconst line = { type: 'warn', text: 'near-degenerate face skipped' };\nconsole.log('channel: ' + line.type);"
    },
    {
      "kind": "example",
      "id": "example:Vec2",
      "symbol": "Vec2",
      "intent": "scene",
      "code": "// 1. Setup — pure math, no scene needed.\nconst v = new Vec2(3, 4);\n// 2. Select — N/A for pure math.\n// 3. Action — compute the magnitude.\nconst len = v.length();\n// 4. Observe.\nconsole.log('Vec2 length: ' + len.toFixed(3));"
    },
    {
      "kind": "example",
      "id": "example:Vec2.add",
      "symbol": "Vec2.add",
      "intent": "math",
      "code": "// 1. Setup — build two Vec2 inputs.\nconst a = new Vec2(1, 2);\nconst b = new Vec2(4, 5);\n// 2. Select — N/A for pure math.\n// 3. Action — add component-wise; add() is immutable.\nconst sum = a.add(b);\n// 4. Observe — sum is (5, 7); the source `a` is untouched.\nconsole.log('sum: ' + JSON.stringify(sum.toArray()));\nconsole.log('a unchanged: ' + JSON.stringify(a.toArray()));\nconsole.log('sum equals expected: ' + (sum.x === 5 && sum.y === 7));"
    },
    {
      "kind": "example",
      "id": "example:Vec2.clone",
      "symbol": "Vec2.clone",
      "intent": "math",
      "code": "// 1. Setup — build a source Vec2 to clone.\nconst a = new Vec2(3, 4);\n// 2. Select — N/A for pure math.\n// 3. Action — clone it, then mutate the clone.\nconst b = a.clone();\nb.x = 99;\n// 4. Observe — original is untouched; clone holds the new value.\nconsole.log('a unchanged x: ' + a.x);\nconsole.log('b x: ' + b.x);"
    },
    {
      "kind": "example",
      "id": "example:Vec2.distanceSqTo",
      "symbol": "Vec2.distanceSqTo",
      "intent": "math",
      "code": "// 1. Setup — two 2D points.\nconst a = new Vec2(0, 0);\nconst b = new Vec2(3, 4);\n// 2. Select — N/A for pure math.\n// 3. Action — compute squared distance and verify it matches distance^2.\nconst dsq = a.distanceSqTo(b);\nconst dist = a.distanceTo(b);\n// 4. Observe — squared distance is 25 = 5^2.\nconsole.log('distance squared: ' + dsq);\nconsole.log('equals length sq: ' + (dsq === Math.pow(dist, 2)));"
    },
    {
      "kind": "example",
      "id": "example:Vec2.distanceTo",
      "symbol": "Vec2.distanceTo",
      "intent": "math",
      "code": "// 1. Setup — two 2D points; the 3-4-5 triangle confirms distance = 5.\nconst p = new Vec2(0, 0);\nconst q = new Vec2(3, 4);\n// 2. Select — N/A for pure math.\n// 3. Action — measure distance in both directions. (Local names avoid the\n//    injected `ab` script global, which a local `const ab` would shadow.)\nconst pq = p.distanceTo(q);\nconst qp = q.distanceTo(p);\n// 4. Observe — distance is 5 and symmetric.\nconsole.log('distance: ' + pq);\nconsole.log('symmetric: ' + (pq === qp));"
    },
    {
      "kind": "example",
      "id": "example:Vec2.dot",
      "symbol": "Vec2.dot",
      "intent": "math",
      "code": "// 1. Setup — build a handful of inputs covering the key dot-product cases.\nconst a = new Vec2(1, 0);\nconst b = new Vec2(0, 1);\nconst c = new Vec2(2, 0);\nconst d = new Vec2(3, 0);\nconst opposite = new Vec2(-1, 0);\n// 2. Select — N/A for pure math.\n// 3. Action — dot orthogonal, parallel, and opposite vectors.\nconst orth = a.dot(b);\nconst parallel = c.dot(d);\nconst flip = a.dot(opposite);\n// 4. Observe — orthogonal=0, parallel=|c|*|d|=6, opposite=-1.\nconsole.log('orthogonal dot: ' + orth);\nconsole.log('parallel dot: ' + parallel);\nconsole.log('opposite dot: ' + flip);"
    },
    {
      "kind": "example",
      "id": "example:Vec2.equals",
      "symbol": "Vec2.equals",
      "intent": "math",
      "code": "// 1. Setup — one reference vector plus three candidates: exact match,\n//    near match (within default eps), and a clearly different vector.\nconst a = new Vec2(3, 4);\nconst exact = new Vec2(3, 4);\nconst nearby = new Vec2(3 + 1e-9, 4);\nconst different = new Vec2(3.5, 4);\n// 2. Select — N/A for pure math.\n// 3. Action — compare under default and custom tolerances.\nconst sameExact = a.equals(exact);\nconst sameNearby = a.equals(nearby);\nconst tightCheck = a.equals(different, 0.1);\nconst strictCheck = a.equals(different);\n// 4. Observe — exact + near match are true; strict comparison is false.\nconsole.log('equal: ' + sameExact);\nconsole.log('equal eps: ' + sameNearby);\nconsole.log('equal tight: ' + tightCheck);\nconsole.log('equal exact: ' + strictCheck);"
    },
    {
      "kind": "example",
      "id": "example:Vec2.from",
      "symbol": "Vec2.from",
      "intent": "math",
      "code": "// 1. Setup — start with a raw [x, y] tuple.\nconst tuple = [7, -2];\n// 2. Select — N/A for pure math.\n// 3. Action — lift the tuple into a Vec2 instance.\nconst v = Vec2.from(tuple);\n// 4. Observe — log components and round-trip back to a tuple.\nconsole.log('x: ' + v.x);\nconsole.log('y: ' + v.y);\nconsole.log('tuple: ' + JSON.stringify(v.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:Vec2.length",
      "symbol": "Vec2.length",
      "intent": "math",
      "code": "// 1. Setup — a 3-4-5 right triangle Vec2 and the zero vector.\nconst v = new Vec2(3, 4);\nconst z = new Vec2(0, 0);\n// 2. Select — N/A for pure math.\n// 3. Action — compute Euclidean length for both vectors.\nconst len = v.length();\nconst zlen = z.length();\n// 4. Observe — length of (3, 4) is 5; zero vector has length 0.\nconsole.log('length: ' + len);\nconsole.log('zero length: ' + zlen);"
    },
    {
      "kind": "example",
      "id": "example:Vec2.lengthSq",
      "symbol": "Vec2.lengthSq",
      "intent": "math",
      "code": "// 1. Setup — a vector whose length is easy to verify (3-4-5 triangle).\nconst v = new Vec2(3, 4);\n// 2. Select — N/A for pure math.\n// 3. Action — compute both length and the cheaper squared length.\nconst len = v.length();\nconst lenSq = v.lengthSq();\n// 4. Observe — lengthSq is length * length = 25.\nconsole.log('length: ' + len);\nconsole.log('lengthSq: ' + lenSq);"
    },
    {
      "kind": "example",
      "id": "example:Vec2.lerp",
      "symbol": "Vec2.lerp",
      "intent": "math",
      "code": "// 1. Setup — two endpoints to interpolate between.\nconst a = new Vec2(0, 0);\nconst b = new Vec2(10, 20);\n// 2. Select — N/A for pure math.\n// 3. Action — sample the midpoint and both endpoints.\nconst mid = a.lerp(b, 0.5);\nconst start = a.lerp(b, 0);\nconst end = a.lerp(b, 1);\n// 4. Observe — mid is (5, 10); start == a; end == b.\nconsole.log('mid: ' + JSON.stringify(mid.toArray()));\nconsole.log('start: ' + JSON.stringify(start.toArray()));\nconsole.log('end:   ' + JSON.stringify(end.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:Vec2.negate",
      "symbol": "Vec2.negate",
      "intent": "math",
      "code": "// 1. Setup — a mixed-sign Vec2 makes the flip easy to read.\nconst v = new Vec2(3, -4);\n// 2. Select — N/A for pure math.\n// 3. Action — negate once, then negate again (double-flip).\nconst n = v.negate();\nconst roundTrip = n.negate();\n// 4. Observe — single negate is (-3, 4); double negate returns to v.\nconsole.log('negated: ' + JSON.stringify(n.toArray()));\nconsole.log('double-negate equals original: ' + v.equals(roundTrip));"
    },
    {
      "kind": "example",
      "id": "example:Vec2.normalize",
      "symbol": "Vec2.normalize",
      "intent": "math",
      "code": "// 1. Setup — a non-zero vector plus the degenerate zero case.\nconst v = new Vec2(3, 4);\nconst origin = new Vec2(0, 0);\n// 2. Select — N/A for pure math.\n// 3. Action — normalize both vectors; zero is a guarded edge case.\nconst unit = v.normalize();\nconst z = origin.normalize();\n// 4. Observe — unit length is 1.0; zero stays at the origin (no NaN).\nconsole.log('unit length: ' + unit.length().toFixed(6));\nconsole.log('unit tuple: ' + JSON.stringify(unit.toArray()));\nconsole.log('zero normalized: ' + JSON.stringify(z.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:Vec2.one",
      "symbol": "Vec2.one",
      "intent": "math",
      "code": "// 1. Setup — pure math, no scene needed.\n// 2. Select — N/A for pure math.\n// 3. Action — build the (1, 1) vector and scale it by 0.5.\nconst one = Vec2.one();\nconst half = one.scale(0.5);\n// 4. Observe — tuple is [1, 1]; scaled is [0.5, 0.5].\nconsole.log('tuple: ' + JSON.stringify(one.toArray()));\nconsole.log('scaled: ' + JSON.stringify(half.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:Vec2.scale",
      "symbol": "Vec2.scale",
      "intent": "math",
      "code": "// 1. Setup — construct a Vec2 to scale.\nconst v = new Vec2(3, 4);\n// 2. Select — N/A for pure math.\n// 3. Action — scale by 2 (grow) and by -1 (flip).\nconst doubled = v.scale(2);\nconst flipped = v.scale(-1);\n// 4. Observe — doubled is (6, 8); flipped is (-3, -4).\nconsole.log('doubled: ' + JSON.stringify(doubled.toArray()));\nconsole.log('flipped: ' + JSON.stringify(flipped.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:Vec2.sub",
      "symbol": "Vec2.sub",
      "intent": "math",
      "code": "// 1. Setup — build two Vec2 inputs.\nconst a = new Vec2(10, 20);\nconst b = new Vec2(3, 8);\n// 2. Select — N/A for pure math.\n// 3. Action — subtract b from a; sub() is immutable.\nconst diff = a.sub(b);\nconst zero = a.sub(a);\n// 4. Observe — diff is (7, 12); a - a is the zero vector.\nconsole.log('diff: ' + JSON.stringify(diff.toArray()));\nconsole.log('diff equals expected: ' + (diff.x === 7 && diff.y === 12));\nconsole.log('zero length: ' + zero.length());"
    },
    {
      "kind": "example",
      "id": "example:Vec2.toArray",
      "symbol": "Vec2.toArray",
      "intent": "math",
      "code": "// 1. Setup — build a Vec2 instance.\nconst v = new Vec2(3, 4);\n// 2. Select — N/A for pure math.\n// 3. Action — drop the Vec2 down to a raw [x, y] tuple.\nconst tuple = v.toArray();\n// 4. Observe — tuple is [3, 4], JSON-friendly for logging.\nconsole.log('tuple: ' + JSON.stringify(tuple));"
    },
    {
      "kind": "example",
      "id": "example:Vec2.x",
      "symbol": "Vec2.x",
      "intent": "math",
      "code": "// 1. Setup — construct a Vec2 to read/write x on.\nconst v = new Vec2(3, 4);\n// 2. Select — N/A for pure math.\n// 3. Action — read the current x, then write a new value.\nconst before = v.x;\nv.x = 10;\n// 4. Observe — log both values to confirm the write landed.\nconsole.log('x: ' + before);\nconsole.log('x after set: ' + v.x);"
    },
    {
      "kind": "example",
      "id": "example:Vec2.y",
      "symbol": "Vec2.y",
      "intent": "math",
      "code": "// 1. Setup — construct a Vec2 to read/write y on.\nconst v = new Vec2(3, 4);\n// 2. Select — N/A for pure math.\n// 3. Action — read the current y, then write a new value.\nconst before = v.y;\nv.y = -5;\n// 4. Observe — log both values to confirm the write landed.\nconsole.log('y: ' + before);\nconsole.log('y after set: ' + v.y);"
    },
    {
      "kind": "example",
      "id": "example:Vec2.zero",
      "symbol": "Vec2.zero",
      "intent": "math",
      "code": "// 1. Setup — pure math, no scene needed.\n// 2. Select — N/A for pure math.\n// 3. Action — build the (0, 0) origin vector.\nconst z = Vec2.zero();\n// 4. Observe — tuple is [0, 0] and length is 0.\nconsole.log('tuple: ' + JSON.stringify(z.toArray()));\nconsole.log('length: ' + z.length());"
    },
    {
      "kind": "example",
      "id": "example:Vec3",
      "symbol": "Vec3",
      "intent": "scene",
      "code": "// 1. Setup — pure math, no scene needed.\nconst a = new Vec3(1, 0, 0);\nconst b = new Vec3(0, 1, 0);\n// 2. Select — N/A for pure math.\n// 3. Action — cross product of the two unit axes.\nconst c = a.cross(b);\n// 4. Observe.\nconsole.log('cross: ' + JSON.stringify(c.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:Vec3.add",
      "symbol": "Vec3.add",
      "intent": "math",
      "code": "// 1. Add two Vec3 instances.\nconst a = new Vec3(1, 2, 3);\nconst b = new Vec3(4, 5, 6);\nconst sum = a.add(b);\nconsole.log('sum: ' + JSON.stringify(sum.toArray()));\nconsole.log('a unchanged: ' + JSON.stringify(a.toArray()));\n// 2. Chain through scale — every step returns a fresh Vec3.\nconst doubled = a.add(b).scale(2);\nconsole.log('doubled: ' + JSON.stringify(doubled.toArray()));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.applyMat3",
      "symbol": "Vec3.applyMat3",
      "intent": "math",
      "code": "// 1. Apply the identity Mat3 — vector is unchanged.\nconst id3 = [1, 0, 0, 0, 1, 0, 0, 0, 1];\nconst v = new Vec3(2, 3, 5);\nconst same = v.applyMat3(id3);\nconsole.log('identity transform: ' + JSON.stringify(same.toArray()));\n// 2. Apply a uniform scale of 2.\nconst scale2 = [2, 0, 0, 0, 2, 0, 0, 0, 2];\nconst doubled = v.applyMat3(scale2);\nconsole.log('doubled: ' + JSON.stringify(doubled.toArray()));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.applyMat4",
      "symbol": "Vec3.applyMat4",
      "intent": "math",
      "code": "// 1. Apply a pure translation matrix to the origin.\nconst tx10 = [\n    1, 0, 0, 0,\n    0, 1, 0, 0,\n    0, 0, 1, 0,\n    10, 0, 0, 1,\n];\nconst p = new Vec3(0, 0, 0).applyMat4(tx10);\nconsole.log('translated: ' + JSON.stringify(p.toArray()));\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.applyQuat",
      "symbol": "Vec3.applyQuat",
      "intent": "math",
      "code": "// 1. Rotate the X axis 90 degrees around Z. Result should be (0, 1, 0).\nconst x = new Vec3(1, 0, 0);\nconst halfPi = Math.PI / 2;\nconst qZ90 = [\n    0,\n    0,\n    Math.sin(halfPi / 2),\n    Math.cos(halfPi / 2),\n];\nconst rotated = x.applyQuat(qZ90);\nconst rounded = rotated.toArray().map(function (v) { return Math.round(v * 1000) / 1000; });\nconsole.log('rotated: ' + JSON.stringify(rounded));\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.clone",
      "symbol": "Vec3.clone",
      "intent": "math",
      "code": "// 1. Clone and mutate independently.\nconst a = new Vec3(1, 2, 3);\nconst b = a.clone();\nb.x = 99;\nconsole.log('a unchanged: ' + JSON.stringify(a.toArray()));\nconsole.log('b: ' + JSON.stringify(b.toArray()));\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.cross",
      "symbol": "Vec3.cross",
      "intent": "math",
      "code": "// 1. Cross product of two unit axes.\nconst a = new Vec3(1, 0, 0);\nconst b = new Vec3(0, 1, 0);\nconst c = a.cross(b);\nconsole.log('cross: ' + JSON.stringify(c.toArray()));\n// 2. Cross is anti-commutative: a × b = -(b × a).\nconst flipped = b.cross(a);\nconsole.log('flipped: ' + JSON.stringify(flipped.toArray()));\n// 3. Cross of parallel vectors is zero.\nconsole.log('parallel cross length: ' + a.cross(new Vec3(2, 0, 0)).length());\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.distanceSqTo",
      "symbol": "Vec3.distanceSqTo",
      "intent": "math",
      "code": "// 1. Squared distance between two points.\nconst a = new Vec3(0, 0, 0);\nconst b = new Vec3(3, 4, 0);\nconsole.log('distance squared: ' + a.distanceSqTo(b));\n// 2. Equals distance * distance.\nconsole.log('equals length sq: ' + (a.distanceSqTo(b) === Math.pow(a.distanceTo(b), 2)));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.distanceTo",
      "symbol": "Vec3.distanceTo",
      "intent": "math",
      "code": "// 1. Distance between two points.\nconst a = new Vec3(0, 0, 0);\nconst b = new Vec3(3, 4, 0);\nconsole.log('distance: ' + a.distanceTo(b));\n// 2. Distance is symmetric.\nconsole.log('symmetric: ' + (a.distanceTo(b) === b.distanceTo(a)));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.dot",
      "symbol": "Vec3.dot",
      "intent": "math",
      "code": "// 1. Orthogonal axes give a zero dot.\nconst a = new Vec3(1, 0, 0);\nconst b = new Vec3(0, 1, 0);\nconsole.log('orthogonal dot: ' + a.dot(b));\n// 2. Parallel vectors give product of lengths.\nconst c = new Vec3(2, 0, 0);\nconst d = new Vec3(3, 0, 0);\nconsole.log('parallel dot: ' + c.dot(d));\n// 3. Opposite directions give negative dot.\nconsole.log('opposite dot: ' + a.dot(new Vec3(-1, 0, 0)));\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.equals",
      "symbol": "Vec3.equals",
      "intent": "math",
      "code": "// 1. Exact match.\nconst a = new Vec3(1, 2, 3);\nconst b = new Vec3(1, 2, 3);\nconsole.log('equal: ' + a.equals(b));\n// 2. Within tolerance after a tiny perturbation.\nconst c = new Vec3(1 + 1e-9, 2, 3);\nconsole.log('equal eps: ' + a.equals(c));\n// 3. Mismatched with custom tolerance.\nconst d = new Vec3(1.5, 2, 3);\nconsole.log('equal loose: ' + a.equals(d, 1));\nconsole.log('equal tight: ' + a.equals(d));\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.from",
      "symbol": "Vec3.from",
      "intent": "math",
      "code": "// 1. Build a Vec3 from a tuple.\nconst v = Vec3.from([1, 2, 3]);\nconsole.log('tuple: ' + JSON.stringify(v.toArray()));\nconsole.log('length: ' + v.length().toFixed(3));\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.length",
      "symbol": "Vec3.length",
      "intent": "math",
      "code": "// 1. Build a Vec3 and read its length.\nconst v = new Vec3(3, 4, 0);\nconsole.log('length: ' + v.length());\n// 2. Zero vector has length 0.\nconsole.log('zero length: ' + new Vec3(0, 0, 0).length());\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.lengthSq",
      "symbol": "Vec3.lengthSq",
      "intent": "math",
      "code": "// 1. lengthSq is length * length.\nconst v = new Vec3(3, 4, 0);\nconsole.log('length: ' + v.length());\nconsole.log('lengthSq: ' + v.lengthSq());\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.lerp",
      "symbol": "Vec3.lerp",
      "intent": "math",
      "code": "// 1. Build two endpoints.\nconst a = new Vec3(0, 0, 0);\nconst b = new Vec3(10, 20, 30);\n// 2. Midpoint at t = 0.5.\nconsole.log('mid: ' + JSON.stringify(a.lerp(b, 0.5).toArray()));\n// 3. Endpoints round-trip.\nconsole.log('start: ' + JSON.stringify(a.lerp(b, 0).toArray()));\nconsole.log('end:   ' + JSON.stringify(a.lerp(b, 1).toArray()));\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.negate",
      "symbol": "Vec3.negate",
      "intent": "math",
      "code": "// 1. Negate flips direction.\nconst v = new Vec3(1, -2, 3);\nconst n = v.negate();\nconsole.log('negated: ' + JSON.stringify(n.toArray()));\n// 2. Double-negate returns the original.\nconsole.log('double-negate equals original: ' + v.equals(n.negate()));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.normalize",
      "symbol": "Vec3.normalize",
      "intent": "math",
      "code": "// 1. Normalize a non-zero vector.\nconst v = new Vec3(3, 4, 0);\nconst unit = v.normalize();\nconsole.log('unit length: ' + unit.length().toFixed(6));\nconsole.log('unit tuple: ' + JSON.stringify(unit.toArray()));\n// 2. Zero vector stays at the origin (no NaN).\nconst z = new Vec3(0, 0, 0).normalize();\nconsole.log('zero normalized: ' + JSON.stringify(z.toArray()));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.one",
      "symbol": "Vec3.one",
      "intent": "math",
      "code": "// 1. Build the (1, 1, 1) vector.\nconst one = Vec3.one();\nconsole.log('tuple: ' + JSON.stringify(one.toArray()));\n// 2. Halve it.\nconsole.log('half: ' + JSON.stringify(one.scale(0.5).toArray()));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.scale",
      "symbol": "Vec3.scale",
      "intent": "math",
      "code": "// 1. Scale a vector.\nconst v = new Vec3(1, 2, 3);\nconsole.log('doubled: ' + JSON.stringify(v.scale(2).toArray()));\n// 2. Negative scale flips direction.\nconsole.log('flipped: ' + JSON.stringify(v.scale(-1).toArray()));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.sub",
      "symbol": "Vec3.sub",
      "intent": "math",
      "code": "// 1. Subtract two Vec3 instances.\nconst a = new Vec3(10, 20, 30);\nconst b = new Vec3(1, 2, 3);\nconst diff = a.sub(b);\nconsole.log('diff: ' + JSON.stringify(diff.toArray()));\n// 2. Subtracting from itself yields the zero vector.\nconst zero = a.sub(a);\nconsole.log('zero length: ' + zero.length());\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.toArray",
      "symbol": "Vec3.toArray",
      "intent": "math",
      "code": "// 1. Serialize to tuple.\nconst v = new Vec3(1, 2, 3);\nconsole.log('tuple: ' + JSON.stringify(v.toArray()));\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.transformDirection",
      "symbol": "Vec3.transformDirection",
      "intent": "math",
      "code": "// 1. Transform a normal by a translation matrix — translation discarded.\nconst tx100 = [\n    1, 0, 0, 0,\n    0, 1, 0, 0,\n    0, 0, 1, 0,\n    100, 0, 0, 1,\n];\nconst n = new Vec3(0, 1, 0).transformDirection(tx100);\nconsole.log('transformed normal: ' + JSON.stringify(n.toArray()));\nconsole.log('length (re-normalized): ' + n.length().toFixed(6));\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.unitX",
      "symbol": "Vec3.unitX",
      "intent": "math",
      "code": "// 1. X axis.\nconst x = Vec3.unitX();\nconsole.log('tuple: ' + JSON.stringify(x.toArray()));\nconsole.log('length: ' + x.length());\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.unitY",
      "symbol": "Vec3.unitY",
      "intent": "math",
      "code": "// 1. Y axis (up by default).\nconst y = Vec3.unitY();\nconsole.log('tuple: ' + JSON.stringify(y.toArray()));\nconsole.log('length: ' + y.length());\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.unitZ",
      "symbol": "Vec3.unitZ",
      "intent": "math",
      "code": "// 1. Z axis.\nconst z = Vec3.unitZ();\nconsole.log('tuple: ' + JSON.stringify(z.toArray()));\nconsole.log('length: ' + z.length());\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.x",
      "symbol": "Vec3.x",
      "intent": "math",
      "code": "// 1. Read x; write x.\nconst v = new Vec3(1, 2, 3);\nconsole.log('x: ' + v.x);\nv.x = 10;\nconsole.log('x after set: ' + v.x);\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.y",
      "symbol": "Vec3.y",
      "intent": "math",
      "code": "// 1. Read y; write y.\nconst v = new Vec3(1, 2, 3);\nconsole.log('y: ' + v.y);\nv.y = -5;\nconsole.log('y after set: ' + v.y);\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.z",
      "symbol": "Vec3.z",
      "intent": "math",
      "code": "// 1. Read z; write z.\nconst v = new Vec3(1, 2, 3);\nconsole.log('z: ' + v.z);\nv.z = 7;\nconsole.log('z after set: ' + v.z);\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3.zero",
      "symbol": "Vec3.zero",
      "intent": "math",
      "code": "// 1. Build the zero vector.\nconst z = Vec3.zero();\nconsole.log('tuple: ' + JSON.stringify(z.toArray()));\nconsole.log('length: ' + z.length());\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3Attribute",
      "symbol": "Vec3Attribute",
      "intent": "scene",
      "code": "// 1. Setup — create a node.\nconst cube = await create.cube({ name: 'vec3attr-demo' });\n// 2. Select — N/A; we have the handle.\n// 3. Action — write the whole compound + read it back.\ncube.translate.set([2.5, 1.3, -0.7]);\nconst tuple = cube.translate.get();\n// 4. Observe + cleanup.\nconsole.log('translate: ' + JSON.stringify(tuple));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Vec3Attribute.basePath",
      "symbol": "Vec3Attribute.basePath",
      "intent": "scene",
      "code": "// 1. Setup — create a node.\nconst cube = await create.cube({ name: 'vec3attr-basePath-demo' });\n// 2. Select — N/A.\n// 3. Action — read the channel name.\nconst bp = cube.translate.basePath;\n// 4. Observe + cleanup.\nconsole.log('basePath: ' + bp);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Vec3Attribute.get",
      "symbol": "Vec3Attribute.get",
      "intent": "scene",
      "code": "// 1. Spawn a cube + translate it to a known position.\nconst cube = await create.cube({ name: 'vec3-get-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Read the full translate compound.\nconst t = cube.translate.get();\nconsole.log('translate: ' + JSON.stringify(t));\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3Attribute.set",
      "symbol": "Vec3Attribute.set",
      "intent": "scene",
      "code": "// 1. Spawn a cube + position it via translate.set.\nconst cube = await create.cube({ name: 'vec3-set-demo' });\ncube.translate.set([2.5, 1.3, -0.7]);\n// 2. Confirm the readback matches the write.\nconsole.log('translate after set: ' + JSON.stringify(cube.translate.get()));\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3Attribute.x",
      "symbol": "Vec3Attribute.x",
      "intent": "scene",
      "code": "// 1. Setup — create a node.\nconst cube = await create.cube({ name: 'vec3attr-x-demo' });\n// 2. Select — N/A.\n// 3. Action — write / read the X axis.\ncube.translate.x.set(1.5);\nconst got = cube.translate.x.get();\n// 4. Observe + cleanup.\nconsole.log('x: ' + got.toFixed(3));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Vec3Attribute.y",
      "symbol": "Vec3Attribute.y",
      "intent": "scene",
      "code": "// 1. Setup — create a node.\nconst cube = await create.cube({ name: 'vec3attr-y-demo' });\n// 2. Select — N/A.\n// 3. Action — write / read the Y axis.\ncube.translate.y.set(2.3);\nconst got = cube.translate.y.get();\n// 4. Observe + cleanup.\nconsole.log('y: ' + got.toFixed(3));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Vec3Attribute.z",
      "symbol": "Vec3Attribute.z",
      "intent": "scene",
      "code": "// 1. Setup — create a node.\nconst cube = await create.cube({ name: 'vec3attr-z-demo' });\n// 2. Select — N/A.\n// 3. Action — write / read the Z axis.\ncube.translate.z.set(-0.7);\nconst got = cube.translate.z.get();\n// 4. Observe + cleanup.\nconsole.log('z: ' + got.toFixed(3));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Vec3AxisAttribute",
      "symbol": "Vec3AxisAttribute",
      "intent": "scene",
      "code": "// 1. Setup — create a node.\nconst cube = await create.cube({ name: 'axisattr-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Select — N/A; we have the handle.\n// 3. Action — read / write through the per-axis handle.\ncube.translate.x.set(1.5);\nconst got = cube.translate.x.get();\n// 4. Observe + cleanup.\nconsole.log('axis x: ' + got.toFixed(3));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Vec3AxisAttribute.axisIndex",
      "symbol": "Vec3AxisAttribute.axisIndex",
      "intent": "scene",
      "code": "// 1. Setup — create a node.\nconst cube = await create.cube({ name: 'axisattr-axisIndex-demo' });\n// 2. Select — N/A.\n// 3. Action — read the axis index.\nconst i = cube.translate.x.axisIndex;\n// 4. Observe + cleanup.\nconsole.log('axisIndex (x): ' + i);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Vec3AxisAttribute.basePath",
      "symbol": "Vec3AxisAttribute.basePath",
      "intent": "scene",
      "code": "// 1. Setup — create a node.\nconst cube = await create.cube({ name: 'axisattr-basePath-demo' });\n// 2. Select — N/A.\n// 3. Action — read the parent channel name.\nconst bp = cube.translate.x.basePath;\n// 4. Observe + cleanup.\nconsole.log('basePath: ' + bp);\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:Vec3AxisAttribute.get",
      "symbol": "Vec3AxisAttribute.get",
      "intent": "scene",
      "code": "// 1. Spawn a cube + place it off the origin.\nconst cube = await create.cube({ name: 'axis-get-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\n// 2. Read each axis through the per-axis Attribute<number> handle.\nconsole.log('translate.x: ' + cube.translate.x.get());\nconsole.log('translate.y: ' + cube.translate.y.get());\nconsole.log('translate.z: ' + cube.translate.z.get());\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec3AxisAttribute.set",
      "symbol": "Vec3AxisAttribute.set",
      "intent": "scene",
      "code": "// 1. Spawn a cube + bump just the X axis by 0.5.\nconst cube = await create.cube({ name: 'axis-set-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\ncube.translate.x.set(cube.translate.x.get() + 0.5);\n// 2. Read the full translate compound — Y and Z stayed put.\nconsole.log('translate after bump: ' + JSON.stringify(cube.translate.get()));\n// 3. Clean up.\nawait cube.delete();\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4",
      "symbol": "Vec4",
      "intent": "scene",
      "code": "// 1. Setup — pure math, no scene needed.\nconst v = new Vec4(1, 2, 3, 1);\n// 2. Select — N/A for pure math.\n// 3. Action — read length and tuple.\nconst len = v.length();\n// 4. Observe.\nconsole.log('Vec4 length: ' + len.toFixed(3));\nconsole.log('tuple: ' + JSON.stringify(v.toArray()));"
    },
    {
      "kind": "example",
      "id": "example:Vec4.add",
      "symbol": "Vec4.add",
      "intent": "math",
      "code": "// 1. Add two Vec4 instances.\nconst a = new Vec4(1, 2, 3, 0);\nconst b = new Vec4(4, 5, 6, 1);\nconst sum = a.add(b);\nconsole.log('sum: ' + JSON.stringify(sum.toArray()));\nconsole.log('sum equals expected: '\n    + (sum.x === 5 && sum.y === 7 && sum.z === 9 && sum.w === 1));\nconsole.log('a unchanged: ' + JSON.stringify(a.toArray()));\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.applyMat4",
      "symbol": "Vec4.applyMat4",
      "intent": "math",
      "code": "// 1. Build a pure translation matrix (column-major, glTF order).\nconst tx10 = [\n    1, 0, 0, 0,\n    0, 1, 0, 0,\n    0, 0, 1, 0,\n    10, 0, 0, 1,\n];\n// 2. Treat as a point (w = 1) — translation applies.\nconst pt = new Vec4(0, 0, 0, 1).applyMat4(tx10);\nconsole.log('point: ' + JSON.stringify(pt.toArray()));\n// 3. Treat as a direction (w = 0) — translation discarded.\nconst dir = new Vec4(0, 0, 0, 0).applyMat4(tx10);\nconsole.log('direction: ' + JSON.stringify(dir.toArray()));\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.clone",
      "symbol": "Vec4.clone",
      "intent": "math",
      "code": "// 1. Clone and mutate independently.\nconst a = new Vec4(1, 2, 3, 1);\nconst b = a.clone();\nb.x = 99;\nconsole.log('a unchanged: ' + JSON.stringify(a.toArray()));\nconsole.log('b: ' + JSON.stringify(b.toArray()));\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.dot",
      "symbol": "Vec4.dot",
      "intent": "math",
      "code": "// 1. Dot of axis-aligned components.\nconst a = new Vec4(1, 0, 0, 0);\nconst b = new Vec4(0, 1, 0, 0);\nconsole.log('orthogonal dot: ' + a.dot(b));\n// 2. Dot including .w.\nconst c = new Vec4(2, 0, 0, 1);\nconst d = new Vec4(3, 0, 0, 1);\nconsole.log('with w dot: ' + c.dot(d));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.equals",
      "symbol": "Vec4.equals",
      "intent": "math",
      "code": "// 1. Exact match.\nconst a = new Vec4(1, 2, 3, 1);\nconst b = new Vec4(1, 2, 3, 1);\nconsole.log('equal: ' + a.equals(b));\n// 2. Within tolerance after a tiny perturbation.\nconst c = new Vec4(1 + 1e-9, 2, 3, 1);\nconsole.log('equal eps: ' + a.equals(c));\n// 3. Mismatched with custom tolerance.\nconst d = new Vec4(1.5, 2, 3, 1);\nconsole.log('equal loose: ' + a.equals(d, 1));\nconsole.log('equal tight: ' + a.equals(d));\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.from",
      "symbol": "Vec4.from",
      "intent": "math",
      "code": "// 1. Build from a tuple.\nconst v = Vec4.from([1, 2, 3, 1]);\nconsole.log('tuple: ' + JSON.stringify(v.toArray()));\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.length",
      "symbol": "Vec4.length",
      "intent": "math",
      "code": "// 1. length of (0, 0, 3, 4) is 5.\nconst v = new Vec4(0, 0, 3, 4);\nconsole.log('length: ' + v.length());\n// 2. Zero vector has length 0.\nconsole.log('zero length: ' + new Vec4(0, 0, 0, 0).length());\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.lengthSq",
      "symbol": "Vec4.lengthSq",
      "intent": "math",
      "code": "// 1. lengthSq is length * length.\nconst v = new Vec4(0, 0, 3, 4);\nconsole.log('length: ' + v.length());\nconsole.log('lengthSq: ' + v.lengthSq());\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.lerp",
      "symbol": "Vec4.lerp",
      "intent": "math",
      "code": "// 1. Build two endpoints.\nconst a = new Vec4(0, 0, 0, 0);\nconst b = new Vec4(10, 20, 30, 1);\n// 2. Midpoint.\nconsole.log('mid: ' + JSON.stringify(a.lerp(b, 0.5).toArray()));\n// 3. Endpoints.\nconsole.log('start: ' + JSON.stringify(a.lerp(b, 0).toArray()));\nconsole.log('end:   ' + JSON.stringify(a.lerp(b, 1).toArray()));\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.negate",
      "symbol": "Vec4.negate",
      "intent": "math",
      "code": "// 1. Negate flips direction.\nconst v = new Vec4(1, -2, 3, 1);\nconst n = v.negate();\nconsole.log('negated: ' + JSON.stringify(n.toArray()));\n// 2. Double-negate returns the original.\nconsole.log('double-negate equals original: ' + v.equals(n.negate()));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.normalize",
      "symbol": "Vec4.normalize",
      "intent": "math",
      "code": "// 1. Normalize a non-zero vector.\nconst v = new Vec4(0, 0, 3, 4);\nconst unit = v.normalize();\nconsole.log('unit length: ' + unit.length().toFixed(6));\nconsole.log('unit tuple: ' + JSON.stringify(unit.toArray()));\n// 2. Zero vector stays at the origin (no NaN).\nconst z = new Vec4(0, 0, 0, 0).normalize();\nconsole.log('zero normalized: ' + JSON.stringify(z.toArray()));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.one",
      "symbol": "Vec4.one",
      "intent": "math",
      "code": "// 1. Build the (1, 1, 1, 1) vector.\nconst one = Vec4.one();\nconsole.log('tuple: ' + JSON.stringify(one.toArray()));\n// 2. Halve it.\nconsole.log('half: ' + JSON.stringify(one.scale(0.5).toArray()));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.scale",
      "symbol": "Vec4.scale",
      "intent": "math",
      "code": "// 1. Scale a Vec4 — note that .w scales too.\nconst v = new Vec4(1, 2, 3, 1);\nconst doubled = v.scale(2);\nconsole.log('doubled: ' + JSON.stringify(doubled.toArray()));\n// 2. Negative scale flips direction.\nconsole.log('flipped: ' + JSON.stringify(v.scale(-1).toArray()));\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.sub",
      "symbol": "Vec4.sub",
      "intent": "math",
      "code": "// 1. Subtract two Vec4 instances. The .w channel subtracts too.\nconst a = new Vec4(10, 20, 30, 1);\nconst b = new Vec4(1, 2, 3, 1);\nconst diff = a.sub(b);\nconsole.log('diff: ' + JSON.stringify(diff.toArray()));\nconsole.log('diff equals expected: '\n    + (diff.x === 9 && diff.y === 18 && diff.z === 27 && diff.w === 0));\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.toArray",
      "symbol": "Vec4.toArray",
      "intent": "math",
      "code": "// 1. Serialize to tuple.\nconst v = new Vec4(1, 2, 3, 1);\nconsole.log('tuple: ' + JSON.stringify(v.toArray()));\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.w",
      "symbol": "Vec4.w",
      "intent": "math",
      "code": "// 1. Read w; flip a point into a direction.\nconst v = new Vec4(1, 2, 3, 1);\nconsole.log('w as point: ' + v.w);\nv.w = 0;\nconsole.log('w as direction: ' + v.w);\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.x",
      "symbol": "Vec4.x",
      "intent": "math",
      "code": "// 1. Read x; write x.\nconst v = new Vec4(1, 2, 3, 1);\nconsole.log('x: ' + v.x);\nv.x = 10;\nconsole.log('x after set: ' + v.x);\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.y",
      "symbol": "Vec4.y",
      "intent": "math",
      "code": "// 1. Read y; write y.\nconst v = new Vec4(1, 2, 3, 1);\nconsole.log('y: ' + v.y);\nv.y = -5;\nconsole.log('y after set: ' + v.y);\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.z",
      "symbol": "Vec4.z",
      "intent": "math",
      "code": "// 1. Read z; write z.\nconst v = new Vec4(1, 2, 3, 1);\nconsole.log('z: ' + v.z);\nv.z = 7;\nconsole.log('z after set: ' + v.z);\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:Vec4.zero",
      "symbol": "Vec4.zero",
      "intent": "math",
      "code": "// 1. Build the zero Vec4.\nconst z = Vec4.zero();\nconsole.log('tuple: ' + JSON.stringify(z.toArray()));\nconsole.log('length: ' + z.length());\n// 2. Select — N/A or see above.\n// 3. Action — see above.\n// 4. Observe — see above."
    },
    {
      "kind": "example",
      "id": "example:ViewportPickResult.distance",
      "symbol": "ViewportPickResult.distance",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'pick-distance-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst c = viewport.center();\nconst hit = viewport.pickAt(c.x, c.y);\nconsole.log('distance is number or null: ' + (typeof hit?.distance === 'number' || hit === null));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ViewportPickResult.edgeIndex",
      "symbol": "ViewportPickResult.edgeIndex",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'pick-edge-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst c = viewport.center();\nconst hit = viewport.pickAt(c.x, c.y);\nconsole.log('edgeIndex is number, undefined, or null: ' + (typeof hit?.edgeIndex === 'number' || hit?.edgeIndex === undefined || hit === null));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ViewportPickResult.faceIndex",
      "symbol": "ViewportPickResult.faceIndex",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'pick-face-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst c = viewport.center();\nconst hit = viewport.pickAt(c.x, c.y);\nconsole.log('faceIndex is number or none: ' + (typeof hit?.faceIndex === 'number' || hit === null));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ViewportPickResult.node",
      "symbol": "ViewportPickResult.node",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'pick-node-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst c = viewport.center();\nconst hit = viewport.pickAt(c.x, c.y);\nconsole.log('pick node is object or null: ' + (typeof hit?.node === 'object' || hit === null));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ViewportPickResult.normal",
      "symbol": "ViewportPickResult.normal",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'pick-normal-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst c = viewport.center();\nconst hit = viewport.pickAt(c.x, c.y);\nconsole.log('normal is array or null: ' + (Array.isArray(hit?.normal) || hit === null));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ViewportPickResult.point",
      "symbol": "ViewportPickResult.point",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'pick-point-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst c = viewport.center();\nconst hit = viewport.pickAt(c.x, c.y);\nconsole.log('point is array or null: ' + (Array.isArray(hit?.point) || hit === null));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ViewportPickResult.vertexIndex",
      "symbol": "ViewportPickResult.vertexIndex",
      "intent": "scene",
      "code": "const cube = await create.cube({ name: 'pick-vertex-demo' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nconst c = viewport.center();\nconst hit = viewport.pickAt(c.x, c.y);\nconsole.log('vertexIndex is number, undefined, or null: ' + (typeof hit?.vertexIndex === 'number' || hit?.vertexIndex === undefined || hit === null));\nawait cube.delete();"
    },
    {
      "kind": "example",
      "id": "example:ViewportResidualApi.componentMode",
      "symbol": "ViewportResidualApi.componentMode",
      "intent": "scene",
      "code": "// 1. Snapshot the current component mode so we can restore it later.\nconst original = viewport.componentMode;\nconsole.log('componentMode original = ' + original);\n// 2. Switch to vertex mode and read the getter back.\nsetComponentMode('vertex');\nconsole.log('componentMode after vertex = ' + viewport.componentMode);\n// 3. Switch to face mode and confirm the getter reflects it.\nsetComponentMode('face');\nconsole.log('componentMode after face = ' + viewport.componentMode);\n// 4. Restore the original mode and verify the round-trip.\nsetComponentMode(original);\nconsole.log('componentMode restored = ' + (viewport.componentMode === original));"
    },
    {
      "kind": "example",
      "id": "example:ViewportResidualApi.gridScale",
      "symbol": "ViewportResidualApi.gridScale",
      "intent": "scene",
      "code": "// 1. Snapshot the current grid unit so we can restore it.\nconst originalUnit = viewport.gridUnit;\nconsole.log('gridUnit original = ' + originalUnit);\n// 2. Switch to meters — the scale should read 1.\nviewport.setGridUnit('m');\nconsole.log('gridScale in m = ' + viewport.gridScale);\n// 3. Switch to centimeters — the scale should read 0.01.\nviewport.setGridUnit('cm');\nconsole.log('gridScale in cm = ' + viewport.gridScale);\n// 4. Restore the original unit and log the restored scale.\nviewport.setGridUnit(originalUnit);\nconsole.log('gridScale restored = ' + viewport.gridScale);"
    },
    {
      "kind": "example",
      "id": "example:ViewportResidualApi.isDragging",
      "symbol": "ViewportResidualApi.isDragging",
      "intent": "scene",
      "code": "// 1. Read the drag flag at rest — it is false when idle.\nconst dragging = viewport.isDragging;\nconsole.log('isDragging = ' + dragging);\n// 2. The flag is always a boolean, never undefined.\nconsole.log('is boolean = ' + (typeof viewport.isDragging === 'boolean'));\n// 3. Branch on the flag to decide whether it is safe to mutate now.\nconst safeToEdit = !viewport.isDragging;\nconsole.log('safe to edit = ' + safeToEdit);\n// 4. At rest the idle state and \"safe to edit\" agree.\nconsole.log('idle agrees = ' + (viewport.isDragging === false && safeToEdit));"
    },
    {
      "kind": "example",
      "id": "example:ViewportResidualApi.setTransformSpace",
      "symbol": "ViewportResidualApi.setTransformSpace",
      "intent": "scene",
      "code": "// 1. Snapshot the current transform space so we can restore it.\nconst original = viewport.transformSpace;\nconsole.log('transformSpace original = ' + original);\n// 2. Switch the gizmo to local space and read it back.\nviewport.setTransformSpace('local');\nconsole.log('transformSpace after local = ' + viewport.transformSpace);\n// 3. Switch the gizmo to world space and read it back.\nviewport.setTransformSpace('world');\nconsole.log('transformSpace after world = ' + viewport.transformSpace);\n// 4. Restore the original space and verify the round-trip.\nviewport.setTransformSpace(original);\nconsole.log('transformSpace restored = ' + (viewport.transformSpace === original));"
    },
    {
      "kind": "example",
      "id": "example:ViewportResidualApi.transformSpace",
      "symbol": "ViewportResidualApi.transformSpace",
      "intent": "scene",
      "code": "// 1. Read and log the current transform space.\nconst space = viewport.transformSpace;\nconsole.log('transformSpace = ' + space);\n// 2. The value is always one of the two valid spaces.\nconsole.log('is valid = ' + (space === 'world' || space === 'local'));\n// 3. Set local space, then confirm the getter agrees.\nviewport.setTransformSpace('local');\nconsole.log('reads local = ' + (viewport.transformSpace === 'local'));\n// 4. Restore the originally-read space and verify it round-trips.\nviewport.setTransformSpace(space);\nconsole.log('restored = ' + (viewport.transformSpace === space));"
    },
    {
      "kind": "example",
      "id": "example:WaitResidualApi.frame",
      "symbol": "WaitResidualApi.frame",
      "intent": "scene",
      "code": "// 1. Record a marker before yielding a frame.\nconsole.log('before frame');\n// 2. Yield exactly one animation frame.\nawait Utils.wait.frame();\n// 3. Execution resumes on the next tick.\nconsole.log('after one frame');\n// 4. Chain frames to space out work across ticks.\nawait Utils.wait.frame();\nconsole.log('after two frames');"
    },
    {
      "kind": "example",
      "id": "example:WaitResidualApi.ms",
      "symbol": "WaitResidualApi.ms",
      "intent": "scene",
      "code": "// 1. Note the start time.\nconst start = Date.now();\nconsole.log('waiting 50ms...');\n// 2. Pause for ~50 milliseconds.\nawait Utils.wait.ms(50);\n// 3. At least the requested delay has elapsed.\nconsole.log('elapsed >= 50ms: ' + (Date.now() - start >= 50));\n// 4. A negative value clamps to 0 (resolves immediately).\nawait Utils.wait.ms(-1);\nconsole.log('negative clamps to immediate');"
    },
    {
      "kind": "example",
      "id": "example:WaitResidualApi.until",
      "symbol": "WaitResidualApi.until",
      "intent": "scene",
      "code": "// 1. Wait for an already-true predicate (resolves on the first poll).\nawait Utils.wait.until(() => true);\n// The call above resolved immediately.\nconsole.log('immediate condition resolved');\n// 2. Set up a flag that a timer flips after a short delay.\nlet ready = false;\n// Flip it asynchronously so the poll has something to wait for.\nwindow.setTimeout(() => { ready = true; }, 30);\n// 3. Poll until the flag becomes true (custom timeout + interval).\nawait Utils.wait.until(() => ready, { timeoutMs: 1000, intervalMs: 10 });\n// The flag is now set.\nconsole.log('deferred condition resolved: ' + ready);\n// 4. A never-true predicate rejects with a timeout (caught here).\ntry {\n    await Utils.wait.until(() => false, { timeoutMs: 20 });\n}\ncatch (e) {\n    console.log('timed out: ' + (e instanceof Error));\n}"
    },
    {
      "kind": "recipe",
      "id": "extrusion-corner-bracket-3way",
      "title": "2020 corner bracket (3-way)",
      "description": "A 3-way corner bracket that joins three 2020 aluminium extrusions at a right-angle corner, with M5 bolt holes. From the adgaudio OpenSCAD_connectors library.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// 2020 corner bracket (3-way)\n//\n// Source: OpenSCAD_connectors (adgaudio) — MIT\n// Generated from the library above, used under its license.\n\ninclude <oscad-connectors/_defaults.scad>\nuse <oscad-connectors/connector.scad>\nuse <oscad-connectors/corner_bracket.scad>\nuse <oscad-connectors/grid.scad>\nuse <oscad-connectors/repeat_grid.scad>\n\ncorner_bracket(2, 2, 2);\n`);"
    },
    {
      "kind": "recipe",
      "id": "extrusion-corner-bracket-flat",
      "title": "2020 corner bracket (flat L)",
      "description": "A flat L-shaped bracket that joins two 2020 aluminium extrusions in-plane, with M5 bolt holes. From the adgaudio OpenSCAD_connectors library.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// 2020 corner bracket (flat L)\n//\n// Source: OpenSCAD_connectors (adgaudio) — MIT\n// Generated from the library above, used under its license.\n\ninclude <oscad-connectors/_defaults.scad>\nuse <oscad-connectors/connector.scad>\nuse <oscad-connectors/corner_bracket.scad>\nuse <oscad-connectors/grid.scad>\nuse <oscad-connectors/repeat_grid.scad>\n\ncorner_bracket(2, 2, 0);\n`);"
    },
    {
      "kind": "recipe",
      "id": "extrusion-endcap",
      "title": "2020 extrusion end cap",
      "description": "A press-fit end cap that closes the open end of a 20x20 aluminium extrusion, with an M5 bolt clearance hole. From the adgaudio OpenSCAD_connectors library.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// 2020 extrusion end cap\n//\n// Source: OpenSCAD_connectors (adgaudio) — MIT\n// Generated from the library above, used under its license.\n\ninclude <oscad-connectors/_defaults.scad>\nuse <oscad-connectors/endcap.scad>\nuse <oscad-connectors/grid.scad>\nuse <oscad-connectors/repeat_grid.scad>\n\nendcap(20, 20, 8, 2, m5_hole_tight);\n`);"
    },
    {
      "kind": "recipe",
      "id": "extrusion-profiles-2020-tslot-bar",
      "title": "2020 T-slot extrusion bar",
      "description": "A length of standard 2020 aluminium-style T-slot extrusion (20 by 20 mm cross section) with four T-channels, ready to print as a jig rail, frame member, or mock-up of real metal extrusion. Change the length to suit your build.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// 2020 T-slot extrusion bar\n//\n// Source: OpenSCAD_AluminumExtrusionProfile_Library — Apache-2.0\n// Generated from the library above, used under its license.\n\ninclude <extrusion-profiles/AluminumExtrusionProfile.scad>\n\n$fn = 80;\n\nlength = 120;  // bar length in mm\n\nlinear_extrude(height = length)\n  2020_extrusion_profile(slot = \"t\");\n`);"
    },
    {
      "kind": "recipe",
      "id": "extrusion-profiles-2040-vslot-rail",
      "title": "2040 V-slot extrusion rail",
      "description": "A length of 2040 aluminium-style extrusion (40 by 20 mm cross section) cut with V-shaped channels, the kind used for CNC and 3D-printer gantries that ride on V-wheels. Print it as a linear-motion rail mock-up or a sturdy frame member.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// 2040 V-slot extrusion rail\n//\n// Source: OpenSCAD_AluminumExtrusionProfile_Library — Apache-2.0\n// Generated from the library above, used under its license.\n\ninclude <extrusion-profiles/AluminumExtrusionProfile.scad>\n\n$fn = 80;\n\nlength = 160;  // rail length in mm\n\nlinear_extrude(height = length)\n  2040_extrusion_profile(slot = \"v\");\n`);"
    },
    {
      "kind": "recipe",
      "id": "extrusion-profiles-2060-base-plate",
      "title": "2060 extrusion base bar",
      "description": "A length of wide 2060 aluminium-style extrusion (60 by 20 mm cross section) with six T-channels across its face, perfect as a base bar or work-surface rail that needs lots of mounting slots. Resize the length for benches, fixtures, or camera sliders.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// 2060 extrusion base bar\n//\n// Source: OpenSCAD_AluminumExtrusionProfile_Library — Apache-2.0\n// Generated from the library above, used under its license.\n\ninclude <extrusion-profiles/AluminumExtrusionProfile.scad>\n\n$fn = 80;\n\nlength = 140;  // bar length in mm\n\nlinear_extrude(height = length)\n  2060_extrusion_profile(slot = \"t\");\n`);"
    },
    {
      "kind": "recipe",
      "id": "extrusion-profiles-3030-tslot-beam",
      "title": "3030 T-slot extrusion beam",
      "description": "A length of 3030 aluminium-style extrusion (30 by 30 mm cross section) with four T-channels, a popular mid-weight size for printer frames, camera rigs, and shelving. Print it as a structural beam and adjust the length to fit your build.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// 3030 T-slot extrusion beam\n//\n// Source: OpenSCAD_AluminumExtrusionProfile_Library — Apache-2.0\n// Generated from the library above, used under its license.\n\ninclude <extrusion-profiles/AluminumExtrusionProfile.scad>\n\n$fn = 80;\n\nlength = 130;  // beam length in mm\n\nlinear_extrude(height = length)\n  3030_extrusion_profile(slot = \"t\");\n`);"
    },
    {
      "kind": "recipe",
      "id": "extrusion-profiles-4040-post",
      "title": "4040 heavy-duty extrusion post",
      "description": "A short length of beefy 4040 aluminium-style extrusion (40 by 40 mm cross section) with eight T-channels, ideal as a corner post or load-bearing upright in a printed frame. Lengthen it for a full column or keep it short as a connector block.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// 4040 heavy-duty extrusion post\n//\n// Source: OpenSCAD_AluminumExtrusionProfile_Library — Apache-2.0\n// Generated from the library above, used under its license.\n\ninclude <extrusion-profiles/AluminumExtrusionProfile.scad>\n\n$fn = 80;\n\nlength = 80;  // post length in mm\n\nlinear_extrude(height = length)\n  4040_extrusion_profile(slot = \"t\");\n`);"
    },
    {
      "kind": "recipe",
      "id": "file-io-facade-roundtrip",
      "title": "Open, save, export, and inspect file formats",
      "description": "Open a file via picker; save the scene; export and re-import; run a folder batch-convert; look up supported formats and per-format capabilities.",
      "intent": "io",
      "editor": "previewer",
      "category": "io",
      "apiSurfaces": [
        "ModelEditor.io.openFile",
        "ModelEditor.io.saveFile",
        "ModelEditor.io.exportFile",
        "ModelEditor.io.reimportLastExport",
        "ModelEditor.io.batchConvert",
        "ModelEditor.io.importModel",
        "ModelEditor.io.getSupportedExportFormats",
        "ModelEditor.io.getSupportedImportFormats",
        "ModelEditor.io.getFormatCapabilities"
      ],
      "code": "// Format introspection (sync, no I/O)\nconst imp = io.getSupportedImportFormats();\nconst exp = io.getSupportedExportFormats();\nconsole.log('importable:', JSON.stringify(imp));\nconsole.log('exportable:', JSON.stringify(exp));\n\nif (exp.length > 0) {\n  const caps = io.getFormatCapabilities(exp[0]);\n  console.log('caps for ' + exp[0] + ':', JSON.stringify({\n    animation: caps.animation,\n    materials: caps.materials,\n    skeleton: caps.skeleton,\n  }));\n}\n\n// Interactive surfaces: wrap in try/catch since some need user gestures.\nasync function probe(label, fn) {\n  try {\n    const result = await fn();\n    console.log(label + ': ok', result ? '(' + String(result).slice(0, 40) + ')' : '');\n  } catch (err) {\n    console.log(label + ': cancelled/unavailable:', err.message);\n  }\n}\n\n// openFile() with no args opens the system file picker, then imports the choice.\nawait probe('openFile', () => io.openFile());\n\n// saveFile(path) writes a .abasset; exportFile(name, format) writes a mesh format.\nawait probe('saveFile', () => io.saveFile('scratch.abasset'));\nawait probe('exportFile', () => io.exportFile('out.glb', 'glb'));\nawait probe('reimportLastExport', () => io.reimportLastExport());\n\n// batchConvert walks an input folder and writes a converted file per item.\nawait probe('batchConvert', () => io.batchConvert({\n  from: { folder: true, accept: '.obj' },\n  to: { folder: true, format: 'glb' },\n}));\n\n// importModel takes a File/path directly (no picker).\nawait probe('importModel', () => io.importModel(new File([], 'fake.obj')));\n",
      "expectedOutput": "console: supported formats; per-API try/catch surface; batch-convert result."
    },
    {
      "kind": "recipe",
      "id": "folder-handle-and-transform-helpers",
      "title": "Transform helpers: translate / rotate / scale / xform / freeze / reset",
      "description": "Drive a node through the compound TRS attrs, a bulk .xform({...}) write (local + world, relative), then .freezeTransform() and .resetTransform() to see the full transform lifecycle.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "transform.translate",
        "transform.rotate",
        "transform.scale",
        "transform.xform",
        "transform.freezeTransform",
        "transform.resetTransform"
      ],
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'XformDemo' });\nselect(null, {mode: 'clear'});\ncube.translate.set([2.5, 1.3, -0.7]);\nawait Utils.wait.frame();\ncube.translate.set([2.5, 1.3, -0.7]);\ncube.rotate.set([0, 45, 0]);\ncube.scale.set([1.2, 1.2, 1.2]);\nconsole.log('TRS:', JSON.stringify(cube.translate.get()), JSON.stringify(cube.rotate.get()), JSON.stringify(cube.scale.get()));\ncube.xform({ t: [0.1, 0, 0], r: [0, 5, 0], relative: true });\nconsole.log('after rel xform: translate=' + JSON.stringify(cube.translate.get()));\ncube.xform({ t: [0.05, 0, 0], space: 'world', relative: true });\nconsole.log('after world rel t: translate=' + JSON.stringify(cube.translate.get()));\ncube.freezeTransform();\nconsole.log('post-freeze:', JSON.stringify(cube.translate.get()));\ncube.translate.set([5, 1, 0]);\ncube.resetTransform();\nconsole.log('post-reset:', JSON.stringify(cube.translate.get()));\n",
      "expectedOutput": "console: TRS values after each set, after relative local + world xforms, post-freeze, and post-reset."
    },
    {
      "kind": "recipe",
      "id": "gridfinity-baseplate-2x2",
      "title": "Gridfinity baseplate 2×2",
      "description": "A 2×2 Gridfinity baseplate — the grid foundation that bins drop into. Built with the kennetek gridfinity-rebuilt library; change gridx/gridy for a larger plate.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity baseplate 2×2\n//\n// Source: gridfinity-rebuilt (kennetek) — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ninclude <gridfinity-rebuilt/src/core/standard.scad>\ninclude <gridfinity-rebuilt/src/core/gridfinity-baseplate.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-utility.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-holes.scad>\nuse <gridfinity-rebuilt/src/helpers/generic-helpers.scad>\nuse <gridfinity-rebuilt/src/helpers/grid.scad>\nuse <gridfinity-rebuilt/gridfinity-rebuilt-baseplate.scad>\n\ngridx = 2;  // columns\ngridy = 2;  // rows\ngridfinityBaseplate([gridx, gridy], 42, [0, 0], 0, bundle_hole_options(), 0, [0, 0]);\n`);"
    },
    {
      "kind": "recipe",
      "id": "gridfinity-baseplate-3x3",
      "title": "Gridfinity baseplate 3×3",
      "description": "A 3×3 Gridfinity baseplate for a 9-cell organizer foundation. Built with the kennetek gridfinity-rebuilt library.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity baseplate 3×3\n//\n// Source: gridfinity-rebuilt (kennetek) — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ninclude <gridfinity-rebuilt/src/core/standard.scad>\ninclude <gridfinity-rebuilt/src/core/gridfinity-baseplate.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-utility.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-holes.scad>\nuse <gridfinity-rebuilt/src/helpers/generic-helpers.scad>\nuse <gridfinity-rebuilt/src/helpers/grid.scad>\nuse <gridfinity-rebuilt/gridfinity-rebuilt-baseplate.scad>\n\ngridx = 3;  // columns\ngridy = 3;  // rows\ngridfinityBaseplate([gridx, gridy], 42, [0, 0], 0, bundle_hole_options(), 0, [0, 0]);\n`);"
    },
    {
      "kind": "recipe",
      "id": "gridfinity-bin-1x1",
      "title": "Gridfinity bin 1×1",
      "description": "The smallest Gridfinity storage bin — a single 1×1 cell, 3 units tall, with a stacking lip. Built with the kennetek gridfinity-rebuilt library.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity bin 1×1\n//\n// Source: gridfinity-rebuilt (kennetek) — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ninclude <gridfinity-rebuilt/src/core/standard.scad>\nuse <gridfinity-rebuilt/src/core/bin.scad>\nuse <gridfinity-rebuilt/src/core/cutouts.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-holes.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-utility.scad>\nuse <gridfinity-rebuilt/src/helpers/generic-helpers.scad>\nuse <gridfinity-rebuilt/src/helpers/grid.scad>\nuse <gridfinity-rebuilt/src/helpers/grid_element.scad>\n\ngridx = 1;  // columns\ngridy = 1;  // rows\ngridz = 3;  // height in 7mm units\ndivx = 1;  // compartment columns\ndivy = 1;  // compartment rows\n\nbin1 = new_bin([gridx, gridy], gridz * 7);\nbin_render(bin1) {\n    bin_subdivide(bin1, [divx, divy]) {\n        cut_compartment_auto(cgs(), 5, false, 0);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "gridfinity-bin-2x1",
      "title": "Gridfinity bin 2×1",
      "description": "A 2×1 Gridfinity storage bin, 6 units tall, one open compartment. Built with the kennetek gridfinity-rebuilt library.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity bin 2×1\n//\n// Source: gridfinity-rebuilt (kennetek) — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ninclude <gridfinity-rebuilt/src/core/standard.scad>\nuse <gridfinity-rebuilt/src/core/bin.scad>\nuse <gridfinity-rebuilt/src/core/cutouts.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-holes.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-utility.scad>\nuse <gridfinity-rebuilt/src/helpers/generic-helpers.scad>\nuse <gridfinity-rebuilt/src/helpers/grid.scad>\nuse <gridfinity-rebuilt/src/helpers/grid_element.scad>\n\ngridx = 2;  // columns\ngridy = 1;  // rows\ngridz = 6;  // height in 7mm units\ndivx = 1;  // compartment columns\ndivy = 1;  // compartment rows\n\nbin1 = new_bin([gridx, gridy], gridz * 7);\nbin_render(bin1) {\n    bin_subdivide(bin1, [divx, divy]) {\n        cut_compartment_auto(cgs(), 5, false, 0);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "gridfinity-bin-2x2",
      "title": "Gridfinity bin 2×2",
      "description": "A 2×2 Gridfinity storage bin, 6 units tall, one large compartment. Built with the kennetek gridfinity-rebuilt library.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity bin 2×2\n//\n// Source: gridfinity-rebuilt (kennetek) — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ninclude <gridfinity-rebuilt/src/core/standard.scad>\nuse <gridfinity-rebuilt/src/core/bin.scad>\nuse <gridfinity-rebuilt/src/core/cutouts.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-holes.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-utility.scad>\nuse <gridfinity-rebuilt/src/helpers/generic-helpers.scad>\nuse <gridfinity-rebuilt/src/helpers/grid.scad>\nuse <gridfinity-rebuilt/src/helpers/grid_element.scad>\n\ngridx = 2;  // columns\ngridy = 2;  // rows\ngridz = 6;  // height in 7mm units\ndivx = 1;  // compartment columns\ndivy = 1;  // compartment rows\n\nbin1 = new_bin([gridx, gridy], gridz * 7);\nbin_render(bin1) {\n    bin_subdivide(bin1, [divx, divy]) {\n        cut_compartment_auto(cgs(), 5, false, 0);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "gridfinity-bin-divided",
      "title": "Gridfinity bin 2×2 (4 compartments)",
      "description": "A 2×2 Gridfinity bin subdivided into a 2×2 grid of four compartments for small-parts sorting. Built with the kennetek gridfinity-rebuilt library.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity bin 2×2 (4 compartments)\n//\n// Source: gridfinity-rebuilt (kennetek) — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ninclude <gridfinity-rebuilt/src/core/standard.scad>\nuse <gridfinity-rebuilt/src/core/bin.scad>\nuse <gridfinity-rebuilt/src/core/cutouts.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-holes.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-utility.scad>\nuse <gridfinity-rebuilt/src/helpers/generic-helpers.scad>\nuse <gridfinity-rebuilt/src/helpers/grid.scad>\nuse <gridfinity-rebuilt/src/helpers/grid_element.scad>\n\ngridx = 2;  // columns\ngridy = 2;  // rows\ngridz = 6;  // height in 7mm units\ndivx = 2;  // compartment columns\ndivy = 2;  // compartment rows\n\nbin1 = new_bin([gridx, gridy], gridz * 7);\nbin_render(bin1) {\n    bin_subdivide(bin1, [divx, divy]) {\n        cut_compartment_auto(cgs(), 5, false, 0);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "gridfinity-bin-label",
      "title": "Gridfinity bin 2×1 (label tab)",
      "description": "A 2×1 Gridfinity bin with an auto label tab ledge for a printed or written label. Built with the kennetek gridfinity-rebuilt library.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity bin 2×1 (label tab)\n//\n// Source: gridfinity-rebuilt (kennetek) — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ninclude <gridfinity-rebuilt/src/core/standard.scad>\nuse <gridfinity-rebuilt/src/core/bin.scad>\nuse <gridfinity-rebuilt/src/core/cutouts.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-holes.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-utility.scad>\nuse <gridfinity-rebuilt/src/helpers/generic-helpers.scad>\nuse <gridfinity-rebuilt/src/helpers/grid.scad>\nuse <gridfinity-rebuilt/src/helpers/grid_element.scad>\n\ngridx = 2;  // columns\ngridy = 1;  // rows\ngridz = 6;  // height in 7mm units\ndivx = 1;  // compartment columns\ndivy = 1;  // compartment rows\n\nbin1 = new_bin([gridx, gridy], gridz * 7);\nbin_render(bin1) {\n    bin_subdivide(bin1, [divx, divy]) {\n        cut_compartment_auto(cgs(), 1, false, 0);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "gridfinity-bin-scoop",
      "title": "Gridfinity bin 1×2 (scooped)",
      "description": "A 1×2 Gridfinity bin with a curved front scoop that sweeps parts toward your fingers. Built with the kennetek gridfinity-rebuilt library.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity bin 1×2 (scooped)\n//\n// Source: gridfinity-rebuilt (kennetek) — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ninclude <gridfinity-rebuilt/src/core/standard.scad>\nuse <gridfinity-rebuilt/src/core/bin.scad>\nuse <gridfinity-rebuilt/src/core/cutouts.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-holes.scad>\nuse <gridfinity-rebuilt/src/core/gridfinity-rebuilt-utility.scad>\nuse <gridfinity-rebuilt/src/helpers/generic-helpers.scad>\nuse <gridfinity-rebuilt/src/helpers/grid.scad>\nuse <gridfinity-rebuilt/src/helpers/grid_element.scad>\n\ngridx = 1;  // columns\ngridy = 2;  // rows\ngridz = 6;  // height in 7mm units\ndivx = 1;  // compartment columns\ndivy = 1;  // compartment rows\n\nbin1 = new_bin([gridx, gridy], gridz * 7);\nbin_render(bin1) {\n    bin_subdivide(bin1, [divx, divy]) {\n        cut_compartment_auto(cgs(), 5, false, 1);\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "gridfinity-vector76-basic-bin-2x1x3",
      "title": "Gridfinity storage bin 2x1x3",
      "description": "A printable Gridfinity storage bin two grid cells wide, one deep, and three units tall, with magnet and screw pockets in the base and a finger-slide scoop for easy parts removal.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity storage bin 2x1x3\n//\n// Source: vector76-gridfinity — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ninclude <gridfinity-vector76/gridfinity_modules.scad>\nuse <gridfinity-vector76/gridfinity_cup_modules.scad>\n\n$fn = 32;\n\nbasic_cup(\n  num_x = 2,\n  num_y = 1,\n  num_z = 3,\n  chambers = 1,\n  withLabel = \"disabled\",\n  labelWidth = 0,\n  fingerslide = true,\n  magnet_diameter = 6.5,\n  screw_depth = 6,\n  floor_thickness = 1.2,\n  wall_thickness = 0.95,\n  hole_overhang_remedy = false,\n  efficient_floor = false,\n  half_pitch = false,\n  lip_style = \"normal\",\n  box_corner_attachments_only = false\n);\n`);"
    },
    {
      "kind": "recipe",
      "id": "gridfinity-vector76-divided-bin-labeled",
      "title": "Gridfinity divided bin with label shelf",
      "description": "A printable Gridfinity bin three cells wide and two deep, split into three chambers by internal dividers, with a sloped label shelf across the front for marking each compartment.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity divided bin with label shelf\n//\n// Source: vector76-gridfinity — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ninclude <gridfinity-vector76/gridfinity_modules.scad>\nuse <gridfinity-vector76/gridfinity_cup_modules.scad>\n\n$fn = 32;\n\nbasic_cup(\n  num_x = 3,\n  num_y = 2,\n  num_z = 4,\n  chambers = 3,\n  withLabel = \"left\",\n  labelWidth = 0,\n  fingerslide = true,\n  magnet_diameter = 0,\n  screw_depth = 0,\n  floor_thickness = 1.0,\n  wall_thickness = 0.95,\n  hole_overhang_remedy = false,\n  efficient_floor = false,\n  half_pitch = false,\n  lip_style = \"normal\",\n  box_corner_attachments_only = false\n);\n`);"
    },
    {
      "kind": "recipe",
      "id": "gridfinity-vector76-glue-stick-holder",
      "title": "Gridfinity glue stick holder",
      "description": "A printable Gridfinity cup sized to cradle a standard glue stick upright, with a snug bored pocket and a slightly flared mouth so the stick drops in and lifts out cleanly.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity glue stick holder\n//\n// Source: vector76-gridfinity — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ncup_height = 5;\nstick_diameter = 30;\n\ninclude <gridfinity-vector76/gridfinity_glue_stick.scad>\n`);"
    },
    {
      "kind": "recipe",
      "id": "gridfinity-vector76-socket-holder",
      "title": "Gridfinity socket wrench holder",
      "description": "A printable Gridfinity tray that holds a row of socket-wrench sockets at a 45-degree tilt for easy grabbing, with an engraved size label along the front edge.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity socket wrench holder\n//\n// Source: vector76-gridfinity — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ninclude <gridfinity-vector76/gridfinity_modules.scad>\nuse <gridfinity-vector76/gridfinity_socket_holder.scad>\n\n$fn = 32;\n\nsocket_holder(4, [12, 12, 12, 13, 14, 16, 16, 17, 21, 22], \"METRIC\", num_z = 5);\n`);"
    },
    {
      "kind": "recipe",
      "id": "gridfinity-vector76-weighted-baseplate-3x2",
      "title": "Gridfinity weighted baseplate 3x2",
      "description": "A printable Gridfinity baseplate three cells wide and two deep with hollow pockets in the underside for steel weights or screws, giving the grid a heavy, stable footing on the desk.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gridfinity weighted baseplate 3x2\n//\n// Source: vector76-gridfinity — MIT\n// https://github.com/kennetek/gridfinity-rebuilt-openscad\n// Generated from the library above, used under its license.\n\ninclude <gridfinity-vector76/gridfinity_modules.scad>\nuse <gridfinity-vector76/gridfinity_baseplate.scad>\n\n$fn = 32;\n\nweighted_baseplate(3, 2);\n`);"
    },
    {
      "kind": "recipe",
      "id": "hardware-db-608-bearing-pillow-block",
      "title": "608 bearing pillow block",
      "description": "A printable pillow-block housing sized from the official ISO bearing table for the popular 608 skate bearing, with a press-fit pocket, a shoulder lip so the bearing seats square, and two mounting-bolt holes. Drop a real 608 bearing in and bolt it to a frame.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// 608 bearing pillow block\n//\n// Source: OpenSCAD-hardware-database — BSD-2-Clause\n// Generated from the library above, used under its license.\n\ninclude <hardware-db/lib/_hardware.scad>\ninclude <hardware-db/lib/_3d_print.scad>\ninclude <hardware-db/lib/_functions.scad>\n\n$fn = 96;\n\n// --- look up the real 608 bearing dimensions from the ISO table ---\nbb       = GetDataTable(\"608\", BB);\nbore     = bb[BBDia];        //  8 mm\nod       = bb[BBOuterDia];   // 22 mm\nbwidth   = bb[BBHeight];     //  7 mm\n\n// --- look up an M4 mounting bolt clearance from the bolt table ---\nm4       = GetDataTable(\"M4\", Bolt);\nbolt_clr = m4[BoltDia];      // ~4.1 mm clearance\n\nwall     = 4;                // wall around the bearing pocket\nlip      = 1.2;             // shoulder that stops the bearing\nbase_h   = 3;               // baseplate thickness\npress    = 0.05;            // press-fit interference\n\nblock_d  = od + 2 * wall;\nblock_w  = block_d + 2 * (bolt_clr + wall);  // room for bolt ears\n\nmodule pillow_block() {\n    difference() {\n        union() {\n            // round bearing boss\n            cylinder(d = block_d, h = bwidth + base_h);\n            // mounting base slab\n            translate([-block_w / 2, -block_d / 2, 0])\n                cube([block_w, block_d, base_h]);\n        }\n        // bearing press-fit pocket (down from the top)\n        translate([0, 0, base_h])\n            cylinder(d = od - press, h = bwidth + 1);\n        // shoulder bore the bearing rests on\n        translate([0, 0, -0.1])\n            cylinder(d = od - 2 * lip, h = base_h + bwidth + 1);\n        // two mounting holes through the ears\n        for (sx = [-1, 1])\n            translate([sx * (block_w / 2 - wall / 2 - bolt_clr / 2), 0, -0.1])\n                cylinder(d = bolt_clr, h = base_h + 1);\n    }\n}\n\npillow_block();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hardware-db-gt2-pulley-blank",
      "title": "GT2 belt idler pulley blank",
      "description": "A smooth flanged idler-pulley blank sized to a GT2 timing belt from the official belt table, with a bore reamed for a 623 ball bearing pressed in from each side. The belt-running surface width matches the GT2 pitch so the belt tracks without rubbing. Press in two 623 bearings and run it on an M3 shaft.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// GT2 belt idler pulley blank\n//\n// Source: OpenSCAD-hardware-database — BSD-2-Clause\n// Generated from the library above, used under its license.\n\ninclude <hardware-db/lib/_hardware.scad>\ninclude <hardware-db/lib/_3d_print.scad>\ninclude <hardware-db/lib/_functions.scad>\n\n$fn = 120;\n\n// --- GT2 belt facts from the belt table ---\nbelt      = GetDataTable(\"GT2\", Belt);\npitch     = belt[BeltPitch];   // 2.0 mm\nthick     = belt[BeltThick];   // 0.63 mm\n\n// --- 623 bearing to seat in the bore ---\nbb        = GetDataTable(\"623\", BB);\nbb_od     = bb[BBOuterDia];    // 10 mm\nbb_w      = bb[BBHeight];      //  4 mm\n\nbelt_w    = 6;                 // belt-tracking surface width\nflange_h  = 1.5;               // each flange height\nflange_d_extra = 4;            // flange overhang past the belt surface\nrun_d     = 16;                // diameter of the belt running surface\npress     = 0.05;\n\ntotal_h   = belt_w + 2 * flange_h;\nflange_d  = run_d + 2 * flange_d_extra + thick * 2;\n\nmodule pulley() {\n    difference() {\n        union() {\n            // bottom flange\n            cylinder(d = flange_d, h = flange_h);\n            // belt running surface\n            translate([0, 0, flange_h])\n                cylinder(d = run_d, h = belt_w);\n            // top flange\n            translate([0, 0, flange_h + belt_w])\n                cylinder(d = flange_d, h = flange_h);\n        }\n        // bearing pocket pressed in from the bottom\n        translate([0, 0, -0.1])\n            cylinder(d = bb_od - press, h = bb_w + 0.1);\n        // bearing pocket pressed in from the top\n        translate([0, 0, total_h - bb_w])\n            cylinder(d = bb_od - press, h = bb_w + 0.2);\n        // through shaft bore (clearance between the two bearings)\n        translate([0, 0, -0.1])\n            cylinder(d = bb[BBDia] + 0.6, h = total_h + 0.2);\n    }\n}\n\npulley();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hardware-db-lm8uu-bearing-clamp",
      "title": "LM8UU linear bearing clamp",
      "description": "A saddle clamp that holds an LM8UU linear ball bearing, sized straight from the official linear-bearing table, with a cradle bore for the bearing, two M3 bolt ears to pinch it closed, and a flat mounting base. Snap an LM8UU in and bolt the carriage to your 3D-printer gantry.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// LM8UU linear bearing clamp\n//\n// Source: OpenSCAD-hardware-database — BSD-2-Clause\n// Generated from the library above, used under its license.\n\ninclude <hardware-db/lib/_hardware.scad>\ninclude <hardware-db/lib/_3d_print.scad>\ninclude <hardware-db/lib/_functions.scad>\n\n$fn = 96;\n\n// --- LM8UU linear bearing from the linear-bearing table ---\nlb       = GetDataTable(\"LM8\", LB);\nlb_od    = lb[LBOuterDia];   // 15 mm\nlb_len   = lb[LBLength];     // 24 mm\n\n// --- M3 clamp bolt clearance ---\nm3       = GetDataTable(\"M3\", Bolt);\nbolt_clr = m3[BoltDia];      // ~3.1 mm\n\nwall     = 3;                // wall around the bearing\nbase_h   = 4;               // mounting base thickness\ngap      = 1.5;             // clamp slit so it can pinch closed\near_w    = bolt_clr + 4;    // bolt ear width\n\nbody_w   = lb_od + 2 * wall;\nbody_h   = lb_od / 2 + wall;\ntotal_w  = body_w + 2 * ear_w;\n\nmodule clamp() {\n    difference() {\n        union() {\n            // mounting base\n            translate([-total_w / 2, -lb_len / 2, 0])\n                cube([total_w, lb_len, base_h]);\n            // half-round saddle body\n            translate([0, 0, base_h])\n                difference() {\n                    translate([-body_w / 2, -lb_len / 2, 0])\n                        cube([body_w, lb_len, body_h]);\n                    translate([0, lb_len / 2 + 0.1, lb_od / 2 + wall - 0.0])\n                        rotate([90, 0, 0])\n                            cylinder(d = lb_od, h = lb_len + 0.2);\n                }\n        }\n        // bearing cradle bore through the body\n        translate([0, lb_len / 2 + 0.1, base_h + lb_od / 2 + wall])\n            rotate([90, 0, 0])\n                cylinder(d = lb_od, h = lb_len + 0.2);\n        // clamp slit\n        translate([-gap / 2, -lb_len / 2 - 0.1, base_h + lb_od / 2 + wall])\n            cube([gap, lb_len + 0.2, body_h]);\n        // two clamp bolt holes through the ears\n        for (sx = [-1, 1])\n            translate([sx * (body_w / 2 + ear_w / 2), 0, base_h + lb_od / 2])\n                rotate([90, 0, 0])\n                    translate([0, 0, -lb_len / 2 - 0.1])\n                        cylinder(d = bolt_clr, h = lb_len + 0.2);\n    }\n}\n\nclamp();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hardware-db-m8-knob",
      "title": "M8 captive-nut knob",
      "description": "A knurled hand knob with a captive hex pocket sized from the official DIN934 nut table for an M8 nut, plus a clearance bore for the M8 bolt shank. Drop a real M8 nut in the trapped pocket and you have a tool-free clamping knob for jigs and fixtures.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// M8 captive-nut knob\n//\n// Source: OpenSCAD-hardware-database — BSD-2-Clause\n// Generated from the library above, used under its license.\n\ninclude <hardware-db/lib/_hardware.scad>\ninclude <hardware-db/lib/_3d_print.scad>\ninclude <hardware-db/lib/_functions.scad>\n\n$fn = 120;\n\n// --- M8 nut from the DIN934 nut table ---\nnut       = GetDataTable(\"M8\", Nut);\nnut_af    = nut[NutOuterDia];   // ~14.5 mm across flats (acts as nut socket dia)\nnut_h     = nut[NutHeight];     // ~6.3 mm\nbolt_dia  = nut[NutDia];        //  8.1 mm shank clearance\n\nknob_d    = 36;                 // grip diameter\nknob_h    = 16;                 // grip height\ngrips     = 14;                 // number of finger scallops\nscallop_d = 5;                  // scallop cutter diameter\nfloor_t   = 3;                  // floor under the nut pocket\n\nmodule knob() {\n    difference() {\n        // main grip body\n        cylinder(d = knob_d, h = knob_h);\n\n        // finger scallops around the rim\n        for (i = [0 : grips - 1])\n            rotate([0, 0, i * 360 / grips])\n                translate([knob_d / 2, 0, -0.1])\n                    cylinder(d = scallop_d, h = knob_h + 0.2);\n\n        // hex nut pocket (trapped from the bottom) — point-up hexagon\n        translate([0, 0, floor_t])\n            rotate([0, 0, 30])\n                cylinder(d = nut_af / cos(30), h = nut_h + 0.5, $fn = 6);\n\n        // bolt shank clearance all the way through\n        translate([0, 0, -0.1])\n            cylinder(d = bolt_dia + 0.4, h = knob_h + 0.2);\n    }\n}\n\nknob();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hardware-db-nema17-motor-bracket",
      "title": "NEMA17 stepper motor bracket",
      "description": "An L-shaped face-mount bracket generated from the official NEMA17 motor-mount table, with the correct 31 mm bolt circle, a center pilot bore for the boss, four M3 screw holes, and a slotted base so you can square the motor before tightening. Bolt a real NEMA17 stepper straight on.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// NEMA17 stepper motor bracket\n//\n// Source: OpenSCAD-hardware-database — BSD-2-Clause\n// Generated from the library above, used under its license.\n\ninclude <hardware-db/lib/_hardware.scad>\ninclude <hardware-db/lib/_3d_print.scad>\ninclude <hardware-db/lib/_functions.scad>\n\n$fn = 64;\n\n// --- official NEMA17 mount geometry ---\nmm        = GetDataTable(\"NEMA17\", MotorMount);\nsize      = mm[MMSize];      // 42.3 mm flange\nboss_d    = mm[MMCutDia];    // 22.1 mm pilot boss\nbolt_span = mm[MMBolts];     // 31.0 mm bolt spacing (square)\n\n// --- M3 screw clearance from the bolt table ---\nm3        = GetDataTable(\"M3\", Bolt);\nscrew_clr = m3[BoltDia];     // ~3.1 mm\n\nplate     = 5;               // face plate thickness\nmargin    = 4;               // material around bolt circle\nfoot_len  = 22;              // base flange length\nfoot_h    = 4;               // base flange thickness\n\nface = size + 2 * margin;\nplate_h = face;              // how tall the vertical plate stands\nweb_h   = min(face * 0.6, foot_len);  // gusset web reach\n\n// Coordinate model (all parts share the back-bottom edge so they fuse):\n//   X = left/right (width)            : centered on 0\n//   Y = depth out from the plate      : plate at front [0..plate], foot runs to +foot_len\n//   Z = up                            : foot at [0..foot_h], plate rises to plate_h\n\nmodule bracket() {\n    difference() {\n        union() {\n            // vertical face plate: thin slab in Y, tall in Z\n            translate([-face / 2, 0, 0])\n                cube([face, plate, plate_h]);\n            // horizontal foot: flat slab on the bed\n            translate([-face / 2, 0, 0])\n                cube([face, foot_len, foot_h]);\n            // two triangular gusset webs joining plate to foot\n            for (sx = [-1, 1])\n                translate([sx * (face / 2) - (sx == 1 ? 4 : 0), plate, 0])\n                    rotate([90, 0, 90])\n                        linear_extrude(height = 4)\n                            polygon([[0, 0], [web_h, 0], [0, web_h]]);\n        }\n        // ---- holes in the vertical plate (motor face) ----\n        // center pilot bore for the motor boss\n        translate([0, plate + 0.1, plate_h / 2])\n            rotate([90, 0, 0])\n                cylinder(d = boss_d + 0.4, h = plate + 0.2);\n        // four mounting screw holes on the 31 mm bolt square\n        for (sx = [-1, 1], sz = [-1, 1])\n            translate([sx * bolt_span / 2, plate + 0.1, plate_h / 2 + sz * bolt_span / 2])\n                rotate([90, 0, 0])\n                    cylinder(d = screw_clr, h = plate + 0.2);\n        // ---- adjustment slots in the foot ----\n        for (sx = [-1, 1])\n            translate([sx * face / 4, foot_len - 7, -0.1])\n                hull()\n                    for (dy = [-4, 4])\n                        translate([0, dy, 0])\n                            cylinder(d = screw_clr, h = foot_h + 0.2);\n    }\n}\n\nbracket();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-baseplate",
      "title": "HSW Blank Baseplate",
      "description": "A flat blank plate on swappable wall plugs — a starting point to build your own accessory, glue parts to, or mount hardware. Pick the size and plug count.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Blank Baseplate\n//\n// Source: honeycomb-wall blank baseplate\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across\nconnectors_x = 2;             // [1:8]\n// Hex connector rows\nconnectors_y = 2;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Plate] */\n// Pad width (mm)\npad_width = 80;               // [30:200]\n// Pad height (mm)\npad_height = 80;              // [30:200]\n// Pad thickness added in front (mm)\npad_t = 4;                    // [2:12]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\nPLATE = PLATE_T_DEFAULT;\n\nmodule baseplate_pad() {\n    // a flat pad welded onto the +Z plate face (the blank build surface)\n    translate([-pad_width / 2, -pad_height / 2, PLATE - HSW_WELD])\n        cube([pad_width, pad_height, pad_t + HSW_WELD]);\n}\nmodule baseplate() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    baseplate_pad();\n}\nhsw_display() baseplate();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-bin",
      "title": "HSW Bin",
      "description": "A parametric wall bin split into sections, with optional honeycomb-pattern panels on the front, back, sides, separators and bottom. Clips on with hex connectors.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\nwidth = 100;\nback_height = 20;\nfront_height = 15;\ndepth = 30;\n\nsections = 3;\nwall_thickness = 2;\nseparator_thickness = 1;\n\nfront_wall_honeycomb = false;\nback_wall_honeycomb = false;\nsides_honeycomb = false;\nseparators_honeycomb = true;\nbottom_honeycomb = false;\n\n//-----------------\n/* [Honeycomb pattern] */ \n\n// diameter in mm\nhexagon_size = 10;\n// mm\nhexagon_spacing = 1;\n\nhex_radius = hexagon_size / 2;\nhex_width = sqrt(3) * hex_radius;\nraster_spacing = hex_width + hexagon_spacing;\n\n\n//-----------------\n/* [HSW connector] */ \n\n// inner distance between corners\nconnector_size = 15.47;\nconnector_depth = 13;\ntolerance = 0.1;\nhexagon_distance = 40.88;\n\n/* functions*/\nfunction hexagon(radius) = [\n    for (i = [0:5])\n        [radius * cos(i * 60), radius * sin(i * 60)]\n];\n\nmodule hsw_connector() {\n    rotate([90, 0, 0])\n    linear_extrude(height = connector_depth)\n    offset(r = -tolerance)\n    polygon(points = hexagon(connector_size / 2));\n}\n\nmodule connectors() {\n    count = max(1, round(width / (hexagon_distance + connector_size / 2)));\n    dist = (width - (count-1) * hexagon_distance) / 2;\n    h = (connector_size/2 - tolerance)*sqrt(3)/2;\n    \n    translate([dist, connector_depth - wall_thickness, h])\n    for (i = [1 : count]) {\n        translate([(i-1) * hexagon_distance, 0, 0])\n        hsw_connector();    \n    }\n    \n    rotate([90, 0, 0])\n    hex_panel(width, back_height, wall_thickness, !back_wall_honeycomb, true);\n}\n\nmodule hexagon_cutout() {\n    circle(hex_radius, $fn=6);\n}\n \nmodule hex_grid(size_x, size_y, solid = false, frame = false) {\n    difference(){\n        square([size_x, size_y]);\n        \n        if (!solid) {\n            for (x = [-hex_radius : raster_spacing - 1 : size_x + hex_radius]) {\n                for (y = [-hex_radius : raster_spacing : size_y + hex_radius]) {\n                    translate(\n                        [x,\n                        y + (((x / raster_spacing) % 2) * raster_spacing / 2)])\n                        hexagon_cutout();\n                }\n            }\n        }\n    }\n    \n    if (frame) {\n        difference() {\n            square([size_x, size_y]);\n            \n            translate([wall_thickness, wall_thickness])\n            square([size_x - wall_thickness * 2, size_y - wall_thickness * 2]);\n        }\n    }\n}\n\nmodule hex_panel(size_x, size_y, thickness, solid = false, frame = false) {\n    linear_extrude(thickness)\n    hex_grid(size_x, size_y, solid, frame);\n}\n\nmodule hex_side(thickness = wall_thickness, solid = false) {\n    linear_extrude(thickness)\n    hex_grid(depth, back_height, solid);\n}\n\nmodule side(thickness = wall_thickness, solid = false) {\n    intersection() {\n        translate([wall_thickness, wall_thickness, 0])\n        rotate([90, 0, -90])\n        hex_side(thickness, solid);\n        \n        rotate([0, 90, 0])\n        linear_extrude(wall_thickness)\n        polygon([[0, 0], [0, -depth], [-front_height, -depth], [-back_height, 0]]);\n    }\n\n    translate([wall_thickness-thickness, 0, 0])\n    rotate([0,90,0])\n    linear_extrude(thickness)\n    difference() {\n        polygon([[0, 0], [0, -depth], [-front_height, -depth], [-back_height, 0]]);\n\n        offset(-wall_thickness)\n        polygon([[0, 0], [0, -depth], [-front_height, -depth], [-back_height, 0]]);\n    }\n}\n\n\nmodule tray() {\n    // bottom\n    translate([0, -depth, 0])\n    hex_panel(width, depth, wall_thickness, !bottom_honeycomb, true);\n\n    // front\n    translate([0, wall_thickness - depth, 0])\n    rotate([90, 0, 0])\n    hex_panel(width, front_height, wall_thickness, !front_wall_honeycomb, true);\n    \n    // sides\n    side(wall_thickness, !sides_honeycomb);\n    \n    translate([width - wall_thickness, 0, 0])\n    side(wall_thickness, !sides_honeycomb);\n}\n\nmodule separators() {\n    section_count = max(1, sections);\n    count = max(section_count - 1, 1);\n    for (i = [1 : count]) {\n        translate([i * width / (count + 1), 0, 0])\n        side(separator_thickness, !separators_honeycomb);\n    }\n}\n\n/* MAIN */\nconnectors();\ntray();\nif (sections > 1) {\n    separators();\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-brush-cup",
      "title": "HSW Brush / Marker Cup",
      "description": "A tall, narrow cup for paintbrushes, markers, files, or long slim tools — keeps them upright and visible. Optional drain. Swappable wall plugs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Brush / Marker Cup\n//\n// Source: honeycomb-wall tall brush cup\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across\nconnectors_x = 1;             // [1:8]\n// Hex connector rows (2 = better for a tall cup)\nconnectors_y = 2;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Cup] */\n// Inner bore diameter (mm)\ncup_inner_d = 36;             // [18:70]\n// Side wall thickness (mm)\ncup_wall = 2.4;               // [1.2:0.2:4]\n// Cup height (mm)\ncup_height = 85;              // [40:160]\n// Base thickness (mm)\ncup_base_t = 2.4;             // [1.2:0.2:5]\n// Drain hole through the base\ndrain = true;\n// Drain hole diameter (mm)\ndrain_d = 6;                  // [3:16]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule brush_cup_body() {\n    cup_outer_d = cup_inner_d + 2 * cup_wall;\n    cup_outer_r = cup_outer_d / 2;\n    translate([0, 0, PLATE + cup_outer_r]) {\n        difference() {\n            rotate([-90, 0, 0]) cylinder(h = cup_height, d = cup_outer_d);\n            translate([0, cup_base_t, 0]) rotate([-90, 0, 0])\n                cylinder(h = cup_height - cup_base_t + eps, d = cup_inner_d);\n            if (drain)\n                translate([0, -eps, 0]) rotate([-90, 0, 0])\n                    cylinder(h = cup_base_t + 2 * eps, d = drain_d);\n        }\n    }\n    // web welding the cup back to the plate face\n    web_w = cup_outer_d - 2;\n    web_h = cup_height * 0.6;\n    translate([-web_w / 2, 0, PLATE - HSW_WELD])\n        cube([web_w, web_h, cup_outer_r + HSW_WELD]);\n}\nmodule brush_cup() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    brush_cup_body();\n}\nhsw_display() brush_cup();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-cable-hook",
      "title": "HSW Cable Hook",
      "description": "A wide-throat hook for coiled cables, extension leads, lanyards, or straps — the broad rounded curve is gentle on cable jackets. Swappable wall plugs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Cable Hook\n//\n// Source: honeycomb-wall cable hook\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across\nconnectors_x = 1;             // [1:8]\n// Hex connector rows (2 = strong cantilever)\nconnectors_y = 2;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Hook] */\n// Hook rod radius (mm)\nrod_r = 6;                    // [3:0.5:12]\n// How far the hook reaches out (mm)\nreach = 30;                   // [15:80]\n// Throat opening height (mm)\nthroat = 26;                  // [12:60]\n// Upturned tip rise (mm)\nrise = 12;                    // [4:30]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\nPLATE = PLATE_T_DEFAULT;\n\nmodule hook_path() {\n    // wide gentle throat: out, sweep down to the throat bottom, finish with an upturn\n    yb = -throat;                                  // throat bottom depth\n    pts = [\n        [0, 0,        PLATE - HSW_WELD],           // embedded in the plate\n        [0, 0,        PLATE + 5],                  // out from the wall\n        [0, yb * 0.4, PLATE + reach * 0.55],       // descend\n        [0, yb,       PLATE + reach * 0.88],       // throat bottom\n        [0, yb,       PLATE + reach],              // along the bottom\n        [0, yb + rise, PLATE + reach - 2]          // upturned tip (retains the cable)\n    ];\n    for (i = [0 : len(pts) - 2])\n        hull() {\n            translate(pts[i])     sphere(r = rod_r, $fn = 28);\n            translate(pts[i + 1]) sphere(r = rod_r, $fn = 28);\n        }\n}\nmodule cable_hook() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    hook_path();\n}\nhsw_display() cable_hook();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-clip",
      "title": "HSW spring clip (snap-lock connector)",
      "description": "The bare snap-in spring connector that locks into a single hex hole - push it in and the barbs catch behind the wall lip.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// ============================================================================\n//  HSW spring clip — self-contained snap-lock connector for one wall cell.\n//  The profile uses an inline rounded-polygon helper (round_corners, circle\n//  method) so it needs no external libraries.\n// ============================================================================\n$fn = 48;\n\n// ---- honeycomb-wall interface constants ----\nHSWX_INSIDE       = 20;\nHSWX_WALL         = 1.8;\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;     // 23.6\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);\nHSWX_OD           = HSWX_SIDE * 2;\nHSWX_DEPTH        = 8;\nHSWX_LOCK_DEPTH   = 5;\nHSWX_LOCK_LIP     = 1;\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;\n\n// ---- plug constants ----\nplug_base_thickness   = 1;\nplug_base_lip         = 1;\nplug_stud_thickness   = 1.7;\nplug_stud_height      = HSWX_LOCK_DEPTH-2;\nplug_spring_base      = 3;\nplug_spring_depth     = 5;\nplug_spring_thickness = 1.4;\n\n// ============================================================================\n//  Inline round_corners (circle method) — self-contained\n// ============================================================================\nfunction _unit(v) = v / norm(v);\nfunction _approx(a,b) = abs(a-b) < 1e-9;\n// angle (deg) at the middle point p1, between vectors (p0-p1) and (p2-p1)\nfunction _vangle3(p0,p1,p2) =\n    let(a=p0-p1, b=p2-p1)\n    acos( max(-1, min(1, (a*b)/(norm(a)*norm(b)) )) );\n// minor arc from 'start' to 'end' about center 'cp', N points\nfunction _arcpts(N, cp, start, end) =\n    let(\n        a0 = atan2(start[1]-cp[1], start[0]-cp[0]),\n        a1 = atan2(end[1]-cp[1],   end[0]-cp[0]),\n        r  = norm(start-cp),\n        d0 = a1 - a0,\n        da = d0 - 360*floor((d0+180)/360)      // normalize to (-180,180]\n    )\n    [ for(j=[0:N-1]) cp + r*[cos(a0+da*j/(N-1)), sin(a0+da*j/(N-1))] ];\n// one rounded corner (radius measure)\nfunction _circlecorner(p0,p1,p2,r,fn) =\n    let(\n        ang  = _vangle3(p0,p1,p2)/2,\n        d    = r/tan(ang),\n        prev = _unit(p0-p1),\n        next = _unit(p2-p1)\n    )\n    _approx(ang,90) ? [p1+prev*d, p1+next*d] :\n    let(\n        center = r/sin(ang)*_unit(prev+next) + p1,\n        start  = p1+prev*d,\n        end    = p1+next*d,\n        N      = max(3, ceil((90-ang)/180*fn))\n    )\n    _arcpts(N, center, start, end);\n// round_corners(circle) for a CLOSED path. pts=[[x,y]...], rds=[r...] (0=sharp)\nfunction _round_corners(pts, rds, fn=90) =\n    let(n = len(pts))\n    [ for(i=[0:n-1]) each\n        rds[i]==0 ? [pts[i]]\n                  : _circlecorner(pts[(i-1+n)%n], pts[i], pts[(i+1)%n], rds[i], fn)\n    ];\n\n// ============================================================================\n//  hsw_clip — profile outline + rounding\n// ============================================================================\nmodule hsw_clip(center=false, rotate=0, hexbase=0, rectbase=0, slop=0) {\n    hswx_inside = HSWX_INSIDE - slop;\n    // outline points\n    pts = [\n        [ -plug_base_lip,                              0 ],\n        [           slop,                              0 ],\n        [           slop,                HSWX_LOCK_DEPTH ],\n        [ -HSWX_LOCK_LIP,  HSWX_LOCK_LIP+HSWX_LOCK_DEPTH ],\n        [ -HSWX_LOCK_LIP,                     HSWX_DEPTH ],\n        [ plug_stud_thickness,                HSWX_DEPTH ],\n        [ plug_spring_base,                   HSWX_DEPTH ],\n        [ hswx_inside/2,  HSWX_DEPTH-plug_spring_depth-0.5 ],\n        [ hswx_inside-plug_spring_base,       HSWX_DEPTH ],\n        [ hswx_inside-plug_stud_thickness,    HSWX_DEPTH ],\n        [ hswx_inside,                        HSWX_DEPTH ],\n        [ hswx_inside+HSWX_LOCK_LIP, HSWX_LOCK_DEPTH+HSWX_LOCK_LIP ],\n        [ hswx_inside,                   HSWX_LOCK_DEPTH ],\n        [ hswx_inside,                              0.1 ],\n        [ hswx_inside+0.1,                           0 ],\n        [ hswx_inside-plug_stud_thickness-0.5,       0 ],\n        [ hswx_inside-plug_stud_thickness,         0.5 ],\n        [ hswx_inside-plug_stud_thickness, HSWX_DEPTH-plug_spring_thickness-1.4 ],\n        [ hswx_inside-plug_spring_base+.1, HSWX_DEPTH-plug_spring_thickness-0.1 ],\n        [ HSWX_INSIDE/2,  HSWX_DEPTH-plug_spring_depth-plug_spring_thickness ],\n        [ plug_spring_base, HSWX_DEPTH-plug_spring_thickness ],\n        [ plug_stud_thickness,              HSWX_DEPTH/3 ],\n        [ HSWX_INSIDE/3.4,                           0 ],\n        [ plug_stud_thickness,                       0 ],\n    ];\n    // per-vertex rounding radii (0 = sharp)\n    rds = [ 0, 0.1, 0, 0.5, 1.5, 0, 3.5, 5.0, 3.5, 0, 2.0, 0.5, 0,\n            0.1, 0, 0, 1.0, 1.5, 1.0, 6.0, 2.0, 1.0, 0, 0 ];\n\n    translate(center ? [0,0,0] : [HSWX_INSIDE/2, HSWX_INSIDE/4, 0])\n    rotate([0,0,rotate])\n        union() {\n            translate([-HSWX_INSIDE/2, -HSWX_INSIDE/4, 0])\n                rotate([-90,0,0])\n                    linear_extrude(HSWX_INSIDE/2)\n                        polygon( _round_corners(pts, rds, 90) );\n        }\n}\n\n// render the bare connector for inspection\nhsw_clip(center=true);\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-attach-to-wall",
      "title": "HSW Attach-to-wall insert (screw-through)",
      "description": "A latching insert with a countersunk screw hole through its base - screw any flat object onto the honeycomb wall. Tune holeDiameter / facetHeight / bottomThickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsertAttachToWall(4, facetHeight = 1, bottomThickness = 1.5);\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-attach-to-wall-empty",
      "title": "HSW Attach-to-wall insert (empty, supportless)",
      "description": "The attach-to-wall insert with the centre bored out and a supportless bridge so it prints cleanly with no supports.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsertAttachToWallEmpty(3.5, facetHeight = 1, bottomThickness = 1.5, supportless = true);\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-hex-plug",
      "title": "HSW Hex plug",
      "description": "A plain hexagonal plug that press-fits into an empty insert (or a wall cell) to blank it off or anchor a part.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\nhexagonPlug(decorations = false);\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-insert",
      "title": "HSW Insert (solid latching)",
      "description": "A solid hexagonal insert that snaps into one honeycomb wall cell with sprung side latches and a flat decorated face - the base building block other attachments grow from.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsert();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-insert-base",
      "title": "HSW Insert base (friction, no latches)",
      "description": "The bare insert body without the spring latches - a friction-only building block for designs that add their own retention.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsertBase();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-insert-empty",
      "title": "HSW Insert (empty / hollow)",
      "description": "A latching wall insert with its centre bored through - a hollow socket to pass a fastener, cable, or your own plug through.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsertEmpty();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-insert-plate",
      "title": "HSW Insert (with mounting plate)",
      "description": "A latching wall insert capped by a solid plate face - a blank mounting pad to build your own accessory onto.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsert_with_plate();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-nut-m10",
      "title": "HSW M10 nut insert",
      "description": "A wall insert with a captive M10 nut pocket and screw clearance for bolting accessories to the wall.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsertM10();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-nut-m2_5",
      "title": "HSW M2.5 nut insert",
      "description": "A wall insert with a captive M2.5 nut pocket and screw clearance - bolt accessories straight to the wall. (Sister recipes cover M3-M10; call insertM3()..insertM10() for other sizes.)",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsertM2_5();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-nut-m3",
      "title": "HSW M3 nut insert",
      "description": "A wall insert with a captive M3 nut pocket and screw clearance for bolting accessories to the wall.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsertM3();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-nut-m4",
      "title": "HSW M4 nut insert",
      "description": "A wall insert with a captive M4 nut pocket and screw clearance for bolting accessories to the wall.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsertM4();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-nut-m5",
      "title": "HSW M5 nut insert",
      "description": "A wall insert with a captive M5 nut pocket and screw clearance for bolting accessories to the wall.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsertM5();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-nut-m6",
      "title": "HSW M6 nut insert",
      "description": "A wall insert with a captive M6 nut pocket and screw clearance for bolting accessories to the wall.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsertM6();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-core-nut-m8",
      "title": "HSW M8 nut insert",
      "description": "A wall insert with a captive M8 nut pocket and screw clearance for bolting accessories to the wall.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n/* Library of hex wall bits */\r\n/* updated */\r\n$fn = 64;  // curve smoothness\r\n\r\n/* Constants relating to the insert part */\r\nINSERT_MAIN_OUTER_AF_DISTANCE = 19.7;\r\nINSERT_MAIN_INNER_AF_DISTANCE = 13.4;\r\nINSERT_LIP_AF_DISTANCE = 22.5;\r\nINSERT_MAIN_OUTER_DIAMETER = af_to_diameter(INSERT_MAIN_OUTER_AF_DISTANCE);\r\nINSERT_MAIN_INNER_DIAMETER = af_to_diameter(INSERT_MAIN_INNER_AF_DISTANCE);\r\nINSERT_LIP_DIAMETER = af_to_diameter(INSERT_LIP_AF_DISTANCE);\r\nINSERT_TOTAL_HEIGHT = 10;\r\nINSERT_LIP_HEIGHT = 2.5;\r\nINSERT_LIP_PROTRUSION = (INSERT_LIP_AF_DISTANCE - INSERT_MAIN_OUTER_AF_DISTANCE)/2;\r\n\r\n/* Constants relating to the honeycomb itself */\r\nHEXAGON_WIDTH = 20;\r\nHEX_WALL_THICKNESS = 3.6;\r\nHEX_SEPARATION = HEXAGON_WIDTH + HEX_WALL_THICKNESS;\r\nHORIZONTAL_HEX_SEPARATION = HEX_SEPARATION * 2 / sqrt(3) + af_to_diameter(HEXAGON_WIDTH + HEX_WALL_THICKNESS)/2;\r\n\r\n/* Constants relating to the plug that can go into an empty insert */\r\nPLUG_HEXAGON_DIAMETER = 14.7;\r\nPLUG_AF_SIZE = PLUG_HEXAGON_DIAMETER * sqrt(3) / 2;\r\nPLUG_LENGTH = 13;\r\n\r\nDECORATION_OUTER_DIAMETER = 21.362;\r\nDECORATION_INNER_DIAMETER = 19.053;\r\nDECORATION_DEPTH = 0.3;\r\nDECORATEION_FACET_HEIGHT = 0.35;\r\n\r\ntiny_distance = .001;\r\nmaxInnerDiameter = 16.5;\r\n\r\ncellAf = 23.6;\r\ncellD = af_to_diameter(cellAf);\r\n\r\n// insert_empty();\r\n// insert_with_plate();\r\n// insert_horizontal();\r\n// hexagon_plug();\r\n// hexagon_plug_horizontal();\r\n// ^ this items has no decorations for compatibility\r\n\r\n//insert(); \t\t// same as insert_with_plate(); but no cut hexagon underneeze\r\n//insertEmpty(); \t// same as insert_empty();\r\n//insertM2_5();\r\n//insertM3();\r\n//insertM4();\r\n//insertM5();\r\n//insertM6();\r\n//insertM8();\r\n//insertM10();\r\n//insertAttachToWall(5, facetHeight = 1, bottomThickness = 1.5, translateHole = [2.5,-3.5], decorations=false);\r\n//insertAttachToWallEmpty(3.5, facetHeight = 2, bottomThickness = 3, withLatch = false, decorations = true, supportless = true);\r\n//hexagonPlug();\r\n//hexagonPlugHorizontal();\r\n\r\n//makeInsertHorizontal() insertEmpty(); // <- any insert \r\n\r\n\r\n/*\r\ngrid(changeCellOrder=false) {\r\n\trow(decorations = false) {\r\n\t\tinsertAttachToWallEmpty(4, decorations=false, withLatch=true, facetHeight = 1.6);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n\trow(decorations = false) {\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t\tinsertEmpty(decorations=false);\r\n\t}\r\n}\r\n//*/\r\n\r\nmodule insert_empty(tolerance = 0, inner_tolerance = 0) insertEmpty(tolerance, inner_tolerance, decorations=false);\r\n\r\nmodule insert_with_plate(tolerance = 0, inner_tolerance = 0) { \r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations=false);\r\n\t\tcutHexagon(inner_tolerance, INSERT_LIP_HEIGHT);\r\n\t}\r\n}\r\n\r\nmodule insert_horizontal(tolerance = 0, inner_tolerance = 0) {\r\n\tmakeInsertHorizontal(tolerance) insert_with_plate(tolerance);\r\n}\r\n\r\nmodule hexagon_plug(tolerance = 0) {\r\n    cylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n}\r\n\r\nmodule hexagon_plug_horizontal(tolerance = 0) {\r\n    translate([0, 0, PLUG_AF_SIZE/2 - tolerance])\r\n        rotate([90, 0, 0]) \r\n\t\t\thexagon_plug(tolerance);\r\n}\r\n\r\nmodule insert(tolerance = 0, decorations = true) {\r\n\t$fn = 6;\r\n    WALL_SIDE_GAP_START_HEIGHT = 6.5;\r\n    WALL_SIDE_GAP_WIDTH = 1;\r\n    WALL_TOP_GAP_WIDTH = 0.8;\r\n    WALL_GAP_LENGTH = 8;\r\n    WALL_TOP_GAP_RADIUS = 8.25;\r\n    TAB_STICKS_OUT_BY = 0.5;\r\n    TAB_LENGTH = 2;\r\n\r\n\r\n\tdifference() {\r\n\t\tunion() {\r\n\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\ttranslate([0, INSERT_MAIN_OUTER_AF_DISTANCE/2 - tolerance, 0]) {\r\n\t\t\t\t\t\thull() {\r\n\t\t\t\t\t\t\ttranslate([0, 0, WALL_SIDE_GAP_START_HEIGHT + WALL_SIDE_GAP_WIDTH + TAB_STICKS_OUT_BY]) {\r\n\t\t\t\t\t\t\t\trotate([0,90,0]){\r\n\t\t\t\t\t\t\t\t\tcylinder(r=TAB_STICKS_OUT_BY, h=TAB_LENGTH, center=true, $fn=20);\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - tiny_distance]) {\r\n\t\t\t\t\t\t\t\trotate([0, 90, 0]) {\r\n\t\t\t\t\t\t\t\t\tcylinder(r=0.2, h=TAB_LENGTH, center=true, $fn=20);  // rounded retention-tab tip\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tdifference() {\r\n\t\t\t\tinsertBase(tolerance, decorations);\r\n\t\t\t\tfor (i=[0:5]) {\r\n\t\t\t\t\trotate([0, 0, i*60]) {\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS - tolerance, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, WALL_TOP_GAP_WIDTH, 10]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\ttranslate([-WALL_GAP_LENGTH/2, WALL_TOP_GAP_RADIUS, WALL_SIDE_GAP_START_HEIGHT]) {\r\n\t\t\t\t\t\t\tcube([WALL_GAP_LENGTH, 10, WALL_SIDE_GAP_WIDTH]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tcutFacetBottom();\r\n\t}\r\n}\r\n\r\nmodule insertEmpty(tolerance = 0, inner_tolerance = 0, decorations = true) {\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations);\r\n\t\tcutHexagon(inner_tolerance);\r\n\t}\r\n}\r\n\r\nmodule insertM2_5(tolerance=0, decorations=true) insertMX(2.5, 5.77, tolerance=tolerance, decorations=decorations);\r\nmodule insertM3(tolerance=0, decorations=true)   insertMX(3, 6.35, tolerance=tolerance, decorations=decorations);\r\nmodule insertM4(tolerance=0, decorations=true)   insertMX(4, 8.08, tolerance=tolerance, decorations=decorations);\r\nmodule insertM5(tolerance=0, decorations=true)   insertMX(5, 9.24, tolerance=tolerance, decorations=decorations);\r\nmodule insertM6(tolerance=0, decorations=true)   insertMX(6, 11.55, tolerance=tolerance, decorations=decorations);\r\nmodule insertM8(tolerance=0, decorations=true)   insertMX(8, 15.01, tolerance=tolerance, decorations=decorations);\r\nmodule insertM10(tolerance=0, decorations=true)  insertMX(10, 17.32, tolerance=tolerance, decorations=decorations);\r\n\r\nfunction calculateFacetDiameter(holeD, facetHeight) = facetHeight * 2 + holeD;\r\n\r\nmodule insertAttachToWall(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 1.5, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, // <- I think there is no need latch, but you can turn it on\r\n\ttranslateHole = [0,0], // <- if you drill hole not accurate, you can move it by [x, y],\r\n\tdecorations = true\r\n) {\r\n\t$fn=64;\r\n\tisTranslateHole = translateHole[0] != 0 || translateHole[1] != 0;\r\n\tspacer = 0.2;\r\n\tholeD = holeDiameter + spacer;\r\n\toutHoleDiameter = isTranslateHole ? maxInnerDiameter : holeD + 4;\r\n\tfacetD = calculateFacetDiameter(holeD, facetHeight);\r\n\r\n\tdifference() {\r\n\t\tif (withLatch)  \r\n\t\t\tinsert(tolerance, decorations = decorations);\r\n\t\t else  \r\n\t\t\tinsertBase(tolerance, decorations = decorations);\r\n\r\n\t\ttranslate([0, 0, -bottomThickness]) cylinder(d = outHoleDiameter, h = INSERT_TOTAL_HEIGHT); \r\n\r\n\t\tif (isTranslateHole) \r\n\t\t\ttranslate([translateHole[0], translateHole[1], 0]) hole();\r\n\t\telse \r\n\t\t\thole();\r\n\t}\r\n\r\n\tmodule hole() {\r\n\t\ttranslate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness - 0.1]) cylinder(d1 = facetD, d2 = holeD, h = facetHeight + 0.1); \r\n\t\tcylinder(d = holeD, INSERT_TOTAL_HEIGHT + 0.1); \r\n\t}\r\n}\r\n\r\nmodule insertAttachToWallEmpty(\r\n\tholeDiameter, \r\n\tfacetHeight = 0, \r\n\tbottomThickness = 2, \r\n\ttolerance = 0, \r\n\tinner_tolerance = 0, \r\n\twithLatch = false, \r\n\tdecorations = true,\r\n    supportless = false,\r\n) {\r\n    layerThickness = 0.2;\r\n\r\n\tdifference() {\r\n\t\tinsertAttachToWall(holeDiameter, facetHeight, bottomThickness, tolerance, inner_tolerance, withLatch, decorations = decorations);\r\n\t\tcutHexagon(inner_tolerance, -bottomThickness);\r\n\t}\r\n    if (supportless) {\r\n        translate([0, 0, INSERT_TOTAL_HEIGHT - bottomThickness]) \r\n            difference() {\r\n                cylinder(d = PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 2, $fn = 6, center = true);  \r\n                cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight), layerThickness * 4], center = true);  \r\n            }\r\n        translate([0,0,INSERT_TOTAL_HEIGHT-bottomThickness]) \r\n            difference() {\r\n                cylinder(d=PLUG_HEXAGON_DIAMETER * 1.1, h = layerThickness * 4, $fn = 6, center = true);  \r\n                rotate(90, [0, 0, 1]) \r\n                    cube([PLUG_HEXAGON_DIAMETER + 1, calculateFacetDiameter(holeDiameter, facetHeight),layerThickness* 2 * 2 * 2], center = true);  \r\n            }\r\n    }\r\n}\r\n\r\nmodule hexagonPlug(tolerance=0, decorations=true) {\r\n\tif (decorations) {\r\n\t\tD = PLUG_HEXAGON_DIAMETER - tolerance*2;\r\n\t\tintersection() {\r\n\t\t\tcylinder(d=D, h=PLUG_LENGTH, $fn=6);\r\n\t\t\ttranslate([0,0,-0.75])\r\n\t\t\t\tcylinder(r1=PLUG_LENGTH + D/2, r2=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t\ttranslate([0,0,-D/2 + 0.75])\r\n\t\t\t\tcylinder(r2=PLUG_LENGTH + D/2, r1=0, h = PLUG_LENGTH + D/2, $fn=6); \r\n\t\t}\r\n\t} else {\r\n\t\tcylinder(d=PLUG_HEXAGON_DIAMETER - tolerance*2, h=PLUG_LENGTH, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule hexagonPlugHorizontal(tolerance=0, decorations=true) {\r\n\ttranslate([0,0, PLUG_AF_SIZE/2] )\r\n\ttranslate([0,0,-INSERT_MAIN_OUTER_AF_DISTANCE/2] )\r\n\tmakeInsertHorizontal() hexagonPlug(tolerance, decorations);\r\n}\r\n\r\nmodule makeInsertHorizontal(tolerance=0) {\r\n    large_distance=10;\r\n    difference() {\r\n        translate([0, 0, INSERT_MAIN_OUTER_AF_DISTANCE/2]) \r\n            rotate([90, 0, 0])\r\n                children();\r\n        translate([-large_distance, -INSERT_TOTAL_HEIGHT - 0.1, -large_distance + tolerance]) \r\n            cube([large_distance*2, INSERT_TOTAL_HEIGHT + 0.2, large_distance]);\r\n    }\r\n}\r\n\r\nmodule grid(rows=[], changeCellOrder = false, maxRowLength = 10) {\r\n\tnoRowsInfo = len(rows) == 0;\r\n\tupCutRowCount = noRowsInfo ? maxRowLength : rows[0];\r\n\tdownCutRowCount = noRowsInfo ? maxRowLength : rows[len(rows)-1];\r\n\t\r\n\tassert( !(!noRowsInfo && len(rows) != $children), \r\n\t\t\"rows info if specified must describe all rows! Example: grid(rows=[1,2]) { row() { insert(); } row() { insert(); insert(); } }\");\r\n\r\n\tdifference() {\r\n\t\tfor (i = [0:$children-1]) {\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder);\r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY, 0]) children(i);\r\n\t\t}\r\n\t\tfor (i = [0:$children-1]) {\r\n\r\n\t\t\ttY = cellOffsetY(i, changeCellOrder); \r\n\t\t\ttranslate([(cellD - cellD / 4) * i, tY  - cellAf, 0]) connectorCutting();\r\n\r\n\t\t\tif (!noRowsInfo) {\r\n\t\t\t\titemsInRow = rows[i];\r\n\t\t\t\tfor (k=[0:maxRowLength-1]){\r\n\t\t\t\t\ttranslate([(cellD - cellD / 4) * i, tY  + (k + itemsInRow) * cellAf, 0]) connectorCutting();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\ttYUp = cellOffsetY($children, changeCellOrder);\r\n\t\ttranslate([(cellD - cellD / 4) * $children, -tYUp, 0]){\r\n\t\t\tfor (j=[0:downCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * j, 0]) connectorCutting();\r\n\t\t\t}\r\n\t\t}\r\n\t\ttYDown = cellOffsetY(-1, changeCellOrder);\r\n\t\ttranslate([-(cellD - cellD / 4), -tYDown, 0])\r\n\t\t\tfor (k=[0:upCutRowCount-1]) {\r\n\t\t\t\ttranslate([0, cellAf * k, 0]) connectorCutting();\r\n\t\t\t}\r\n\t}\r\n}\r\n\r\nmodule row(connections=true, decorations = true) {\r\n\tif ($children > 0) {\r\n\t\tdifference() {\r\n\t\t\tfor (i = [0:$children-1]) \r\n\t\t\t\ttranslate([0, cellAf * i, 0]) {\r\n\t\t\t\t\tif (connections) connector(false, decorations);\r\n\t\t\t\t\tchildren(i);\r\n\t\t\t\t}\r\n\t\t\ttranslate([(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t\ttranslate([0, cellAf * $children, 0]) connectorCutting();\r\n\t\t\ttranslate([-(cellD - cellD / 4) , cellAf * $children - cellAf/2, 0]) connectorCutting();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// utils modules section ----------------------------------------------------\r\n\r\nmodule insertBase(tolerance=0, decorations=true) {\r\n    $fn = 6;\r\n    difference() {\r\n        union() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER-tolerance*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=INSERT_LIP_DIAMETER, h=INSERT_LIP_HEIGHT);\r\n        }\r\n\t\tif (decorations) {\r\n\t\t\ttranslate([0,0,-0.1])\r\n\t\t\t\tunion() {\r\n\t\t\t\t\tdifference() {\r\n\t\t\t\t\t\tcylinder(d = DECORATION_OUTER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t\tcylinder(d = DECORATION_INNER_DIAMETER, h = DECORATION_DEPTH + 0.1);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\ttranslate([0,0,DECORATEION_FACET_HEIGHT]) cutFacet(INSERT_LIP_DIAMETER, inverse=true);\r\n\t\t}\r\n        translate([0,0,INSERT_LIP_HEIGHT])\r\n        rotate(90,[0,0,1])\r\n        difference() {\r\n            cylinder(d=INSERT_MAIN_OUTER_DIAMETER*2, h=INSERT_TOTAL_HEIGHT);\r\n            cylinder(d=af_to_diameter(INSERT_MAIN_OUTER_DIAMETER-tolerance*2) - 0.12 * 2, h=INSERT_TOTAL_HEIGHT);\r\n        }\r\n        cutFacetBottom();\r\n    }\r\n}\r\n\r\nmodule cutFacet(diameter, inverse=false) {\r\n\th = diameter / 2 * tan(45);\r\n\tz = inverse ? -h : 0;\r\n\ttranslate([0,0,z])\r\n\t\tdifference() {\r\n\t\t\tcylinder(d=diameter*2, h = h);\r\n\t\t\tif (inverse)\r\n\t\t\t\tcylinder(d2=diameter, d1=0, h = h);\r\n\t\t\telse\r\n\t\t\t\tcylinder(d1=diameter, d2=0, h = h);\r\n\t\t}\r\n}\r\n\r\nmodule cutFacetBottom() {\r\n\ttranslate([0,0,INSERT_TOTAL_HEIGHT-DECORATEION_FACET_HEIGHT])\r\n\t\tcutFacet(INSERT_MAIN_OUTER_DIAMETER);\r\n}\r\n\r\nmodule cutHexagon(inner_tolerance, indent = 0) {\r\n\t// remove preview artifacts\r\n\tz = indent == 0 ? -0.5 : indent;\r\n\taddH = indent == 0 ? 1 : 0;\r\n\ttranslate([0,0,z]) \r\n\t\tcylinder(d=INSERT_MAIN_INNER_DIAMETER + inner_tolerance*2, h=INSERT_TOTAL_HEIGHT + addH, $fn=6);\r\n}\r\n\r\nmodule insertMX(diameter,  nutWidth, tolerance = 0, decorations = true) {\r\n\tspacer = 0.2;\r\n\tholeD = min(maxInnerDiameter, diameter + spacer);\r\n\tfacet = holeD / 8;\r\n\tfacetH = 0.3;\r\n\tnutW = min(maxInnerDiameter, nutWidth + spacer);\r\n\taboveNutW = min(maxInnerDiameter, nutW + 6);\r\n\r\n\tdifference() {\r\n\t\tinsert(tolerance, decorations = decorations);\r\n\t\ttranslate([0,0,-0.1]) // remove preview artifacts\r\n\t\t\tcylinder(d1 = holeD + facet * 2, d2 = holeD, h = facetH + 0.1);\r\n\t\tcylinder(d = holeD, h = INSERT_TOTAL_HEIGHT);\r\n\t\ttranslate([0, 0, 4])\r\n\t\tcylinder(d = nutW, h = INSERT_TOTAL_HEIGHT, $fn=6);\r\n\t\ttranslate([0, 0, 7])\r\n\t\tcylinder(d = aboveNutW, h = INSERT_TOTAL_HEIGHT + 0.1, $fn=6);\r\n\t}\r\n}\r\n\r\nmodule connector(forCutting=false, decorations = true) {\r\n\tconnectorW = (cellAf-INSERT_LIP_AF_DISTANCE);\r\n\tconnectorH = decorations ? INSERT_LIP_HEIGHT - DECORATEION_FACET_HEIGHT : INSERT_LIP_HEIGHT;\r\n\tconnectorL = INSERT_LIP_DIAMETER/2;\r\n\tcuttingTranslateZ = forCutting ? -2 : 0;\r\n\tcuttingScaleZ = forCutting ? 2 : 1;\r\n\r\n\ttranslate([0,0,cuttingTranslateZ])\r\n\t\tscale([1,1,cuttingScaleZ]) \r\n\t\t\tfor (a=[0:60:300])\r\n\t\t\trotate(a,[0,0,1])\r\n\t\t\t\ttranslate([-connectorL/2, -INSERT_LIP_AF_DISTANCE/2 - connectorW, decorations ? DECORATEION_FACET_HEIGHT : 0]) {\r\n\t\t\t\tpoints = [\r\n\t\t\t\t\t[0, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, addCutting(connectorW)],\r\n\t\t\t\t\t[connectorL, 0],\r\n\t\t\t\t\t[0, 0],\r\n\t\t\t\t\t[-(connectorW*sin(60)), connectorW/2]\r\n\t\t\t\t];\r\n\t\t\t\tlinear_extrude(height = connectorH) polygon(points);\r\n\t\t\t}\r\n\r\n\tfunction addCutting(value) = forCutting ? value + 0.1 : value;\r\n}\r\n\r\nmodule connectorCutting() connector(true, true);\r\nfunction cellOrder(i, change) = change ? i % 2 == 0 : i % 2 != 0;\r\nfunction cellOffsetY(i, change) = cellOrder(i, change) ? 0 : cellAf / 2;\r\n\r\n/* converts an 'across flats' dimension to a maximum diameter — useful for openScad */\r\nfunction af_to_diameter(af) = af / sqrt(3) * 2;\r\nfunction diameter_to_af(diameter) = diameter * sqrt(3) / 2;\n\n// ---- part ----\ninsertM8();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-deep-bin",
      "title": "HSW Deep Bin",
      "description": "A tall, fully-enclosed bin for long parts, bottles, brushes, or rolled items, on swappable wall plugs. Open top, full-height walls.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Deep Bin\n//\n// Source: honeycomb-wall deep bin\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across\nconnectors_x = 2;             // [1:8]\n// Hex connector rows (taller = more)\nconnectors_y = 3;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Bin] */\n// Inside width (mm)\nbin_width = 55;               // [25:160]\n// Inside depth out from the wall (mm)\nbin_depth = 55;               // [20:120]\n// Inside height (mm)\nbin_height = 95;              // [40:200]\n// Wall thickness (mm)\nwall = 2.4;                   // [1.2:0.2:4]\n// Floor thickness (mm)\nfloor_t = 2.6;                // [1.2:0.2:5]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule deep_bin() {\n    ow = bin_width + 2 * wall;\n    od = bin_depth + 2 * wall;\n    oh = bin_height + floor_t;\n    z0 = PLATE - HSW_WELD;\n    y0 = -oh / 2;                       // centre vertically so the back wall covers the plate\n    difference() {\n        translate([-ow / 2, y0, z0]) cube([ow, oh, od + HSW_WELD]);\n        translate([-bin_width / 2, y0 + floor_t, PLATE + wall])\n            cube([bin_width, bin_height + eps, bin_depth + eps]);   // cavity, open top\n    }\n}\nmodule deep_parts_bin() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    deep_bin();\n}\nhsw_display() deep_parts_bin();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-divided-tray",
      "title": "HSW Divided Tray",
      "description": "A shallow tray split into a grid of compartments for sorting screws, beads, SMD parts, or findings. Choose the number of columns and rows. Swappable wall plugs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Divided Tray\n//\n// Source: honeycomb-wall divided sorting tray\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across (a wide tray wants 3+)\nconnectors_x = 3;             // [1:8]\n// Hex connector rows\nconnectors_y = 2;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Tray] */\n// Inside width (mm)\ntray_width = 130;             // [40:250]\n// Inside depth out from the wall (mm)\ntray_depth = 55;              // [25:120]\n// Inside height (mm)\ntray_height = 28;             // [12:80]\n// Compartment columns\ncols = 4;                     // [1:10]\n// Compartment rows\nrows = 2;                     // [1:6]\n// Outer wall thickness (mm)\nwall = 2.4;                   // [1.2:0.2:4]\n// Divider thickness (mm)\ndivider_t = 1.6;             // [1:0.2:3]\n// Floor thickness (mm)\nfloor_t = 2.4;               // [1.2:0.2:5]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule divided_tray() {\n    ow = tray_width + 2 * wall;\n    od = tray_depth + 2 * wall;\n    oh = tray_height + floor_t;\n    z0 = PLATE - HSW_WELD;\n    y0 = -oh / 2;\n    difference() {\n        translate([-ow / 2, y0, z0]) cube([ow, oh, od + HSW_WELD]);\n        // hollow the inside (open top)\n        translate([-tray_width / 2, y0 + floor_t, PLATE + wall])\n            cube([tray_width, tray_height + eps, tray_depth + eps]);\n    }\n    // dividers (thin walls) rising from the floor, welded to the side walls\n    for (c = [1 : cols - 1])\n        translate([-tray_width / 2 + c * tray_width / cols - divider_t / 2, y0 + floor_t - eps, PLATE + wall])\n            cube([divider_t, tray_height, tray_depth]);\n    for (r = [1 : rows - 1])\n        translate([-tray_width / 2, y0 + floor_t - eps, PLATE + wall + r * tray_depth / rows - divider_t / 2])\n            cube([tray_width, tray_height, divider_t]);\n}\nmodule divided_tray_part() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    divided_tray();\n}\nhsw_display() divided_tray_part();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-drill-bit-block",
      "title": "HSW Drill-Bit Block",
      "description": "A block with a grid of graduated holes to organise drill bits, end mills, or small round tools by size. Swappable wall plugs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Drill-Bit Block\n//\n// Source: honeycomb-wall drill-bit block\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across\nconnectors_x = 2;             // [1:8]\n// Hex connector rows\nconnectors_y = 1;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Block] */\n// Holes across (X)\ncols = 6;                     // [1:12]\n// Hole rows (depth)\nrows = 3;                     // [1:6]\n// Smallest bore diameter (mm)\nbore_min = 3;                 // [1:10]\n// Largest bore diameter (mm)\nbore_max = 9;                 // [2:16]\n// Spacing between holes (mm)\npitch = 13;                   // [6:25]\n// Bore depth (mm)\nbore_depth = 22;             // [6:55]\n// Solid base under the bores (mm)\nbase_t = 5;                  // [2:15]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule drill_block() {\n    spanx = (cols - 1) * pitch;\n    spany = (rows - 1) * pitch;\n    bw = spanx + bore_max + 8;        // X width\n    bd = spany + bore_max + 8;        // Z depth out from wall\n    bh = bore_depth + base_t;         // Y height\n    z0 = PLATE - HSW_WELD;\n    y0 = -bh / 2;                     // centre on the plate\n    zc = PLATE + (bd - spany) / 2;    // front margin for the first row\n    difference() {\n        translate([-bw / 2, y0, z0]) cube([bw, bh, bd + HSW_WELD]);\n        for (cx = [0 : cols - 1])\n            for (cy = [0 : rows - 1]) {\n                d = bore_min + (cols > 1 ? (bore_max - bore_min) * cx / (cols - 1) : 0);\n                translate([-spanx / 2 + cx * pitch, y0 + bh + eps, zc + cy * pitch])\n                    rotate([90, 0, 0]) cylinder(h = bore_depth, d = d);   // vertical bore, open top\n            }\n    }\n}\nmodule drill_bit_block() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    drill_block();\n}\nhsw_display() drill_bit_block();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-grid",
      "title": "HSW honeycomb wall (grid panel)",
      "description": "The HSW panel itself - a staggered grid of 20 mm hex sockets with the snap-lock lip, sized by columns and rows. Everything else clips into it.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW grid — fast slab-minus-lipped-hex-holes build.\n// Renders any size in ~seconds (one difference instead of a union of many\n// struts), with the standard honeycomb-wall snap-lock profile. Self-contained.\n\n$fn = 48;\ncols = 3;\nrows = 3;\ndepth = 8;\n\nHSWX_INSIDE  = 20;\nHSWX_WALL    = 1.8;\nHSWX_OUTSIDE = HSWX_INSIDE + HSWX_WALL*2;   // 23.6\nR_out        = HSWX_OUTSIDE / sqrt(3);      // outer circumradius 13.626\nPITCH_X      = 1.5 * R_out;                 // 20.44 flat-top column pitch\nPITCH_Y      = HSWX_OUTSIDE;                // 23.6  row pitch\nSTAG_Y       = HSWX_OUTSIDE / 2;            // 11.8  alternate-column stagger\n\n// flat-top hexagon, flat-to-flat = af, as a 2D shape\nmodule hexF(af) { circle(r = af/sqrt(3), $fn = 6); }\n\n// one lipped hex socket: front mouth at z=0, bore into -z, lock step at 5.1-6mm.\n// profile (the standard wall-hole inverse): 21mm chamfered mouth -> 20mm bore\n// (front 5.1mm) -> taper 20->22 (depth 5.1-6) -> 22mm pocket to the back.\nmodule hsw_hole(d = depth) {\n    eps = 0.02;\n    // mouth chamfer: 21mm at z=0 down to 20mm at z=-0.5\n    translate([0,0,-0.5]) linear_extrude(0.5+eps, scale = 21/20) hexF(20);\n    // front bore: 20mm, z=-0.5 .. -5.1\n    translate([0,0,-5.1]) linear_extrude(4.6+eps) hexF(20);\n    // lock taper: 22mm at z=-6 up to 20mm at z=-5.1 (the catch step)\n    translate([0,0,-6]) linear_extrude(0.9+eps, scale = 20/22) hexF(22);\n    // back pocket: 22mm, z=-6 .. -d\n    if (d > 6) translate([0,0,-d]) linear_extrude(d-6+eps) hexF(22);\n}\n\nmodule hsw_grid(cols = 3, rows = 3, d = depth) {\n    spanX = (cols-1) * PITCH_X;\n    spanY = (rows-1) * PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + STAG_Y;\n    difference() {\n        translate([0, 0, -d/2]) cube([slabW, slabD, d], center = true);\n        for (c = [0:cols-1]) {\n            stag = (c % 2 == 1) ? STAG_Y : 0;\n            for (r = [0:rows-1]) {\n                x = c*PITCH_X - spanX/2;\n                y = r*PITCH_Y - spanY/2 + stag - STAG_Y/2;\n                translate([x, y, 0]) hsw_hole(d);\n            }\n        }\n    }\n}\n\nhsw_grid(cols, rows);\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-hex-key-holder",
      "title": "HSW Hex-Key Holder",
      "description": "A slim block with a row of slots that hold Allen / hex keys (or small flat tools) upright and sorted by size. Swappable wall plugs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Hex-Key Holder\n//\n// Source: honeycomb-wall hex key holder\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across\nconnectors_x = 2;             // [1:8]\n// Hex connector rows\nconnectors_y = 1;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Holder] */\n// Number of slots\nslots = 8;                    // [1:16]\n// Slot width (mm)\nslot_w = 3;                   // [1.5:0.5:8]\n// Slot length across the block (mm)\nslot_len = 14;               // [6:30]\n// Spacing between slots (mm)\nspacing = 8;                 // [5:20]\n// Slot depth down into the block (mm)\nslot_depth = 16;             // [6:40]\n// Solid base under the slots (mm)\nbase_t = 5;                  // [2:14]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule hex_key_holder_body() {\n    span = (slots - 1) * spacing;\n    bw = span + slot_w + 10;          // X width\n    bd = slot_len + 8;                // Z depth out from wall\n    bh = slot_depth + base_t;         // Y height\n    z0 = PLATE - HSW_WELD;\n    y0 = -bh / 2;\n    difference() {\n        translate([-bw / 2, y0, z0]) cube([bw, bh, bd + HSW_WELD]);\n        // row of slots cut from the top (each a thin rectangular pocket)\n        for (i = [0 : slots - 1])\n            translate([-span / 2 + i * spacing - slot_w / 2, y0 + bh - slot_depth, PLATE + (bd - slot_len) / 2])\n                cube([slot_w, slot_depth + eps, slot_len]);\n    }\n}\nmodule hex_key_holder() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    hex_key_holder_body();\n}\nhsw_display() hex_key_holder();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-insert-rounded",
      "title": "HSW Insert (rounded, configurable)",
      "description": "A smoothly-rounded honeycomb-wall insert with optional center hole, side latch tabs, front hexagon emboss and a flat-side variant.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Generates insert-empty, insert-full and a flat-sided variation of both, with or without the front hexagon emboss.\r\n// Made to make it easy to include in other OpenSCAD models.\r\n\r\nverticalSpacing = 23.6;\r\nhorizontalSpacing = 40.88;\r\n\r\n\r\n\r\nmodule hws_insert(centerHole=false, lateralTabs=true, frontDecoration=false,flatSide=true) {\r\n    //Parameters to change in case you deviated from the standard Honeycomb Wall\r\n    hexHeight = 19.8;\r\n    insideHexHeight = 13.40;\r\n    insertDepth = 7.67;\r\n    //Parameters you should not change unless you know what you're doing.\r\n    frontDepth = 2; \r\n    frontExtension = 1.5;\r\n    gapVert = 1;\r\n    gapHor = 0.8;\r\n    gapLen = 8;\r\n    gapVertDepth = 3.5;\r\n    gapHorDepth = 1.6;\r\n    bumpHeight = 0.5;\r\n    bumpWidth = 2;\r\n    bumpLen = gapVertDepth - gapHor - bumpHeight*2;\r\n    bumpAng = 20;\r\n    //insert assembly using other modules\r\n    difference() {\r\n        union() {\r\n            insert(hexHeight,insertDepth,frontDepth,frontExtension);\r\n            if(lateralTabs) tabBumps(bumpHeight=bumpHeight,hexHeight=hexHeight,insertDepth=insertDepth,frontDepth=frontDepth,bumpLen=bumpLen,bumpAng=bumpAng);\r\n        }\r\n        if (lateralTabs) tabsCutout(hexHeight=hexHeight,gapHorDepth=gapHorDepth,insertDepth=insertDepth,frontDepth=frontDepth,gapLen=gapLen,gapVert=gapVert,gapVertDepth=gapVertDepth,gapHor=gapHor, flatSide=flatSide);\r\n        if (centerHole) insertHole(insideHexHeight=insideHexHeight,insertDepth=insertDepth,frontDepth=frontDepth);\r\n        if (frontDecoration) hexCutout(innerHexHeight=insideHexHeight+1.55);\r\n        if (flatSide) sideFlattener(hexHeight=hexHeight, fullInsertDepth=insertDepth+frontDepth);\r\n    }\r\n}\r\n\r\n\r\nmodule hws_insert_n_vert(centerHole=false, lateralTabs=true, frontDecoration=false,flatSide=false,n=2,blanks=1) {\r\n    ycopies(verticalSpacing*(1+blanks),n=n) hws_insert(centerHole=centerHole, lateralTabs=lateralTabs, frontDecoration=frontDecoration,flatSide=flatSide);\r\n}\r\n\r\nmodule hws_insert_n_hor(centerHole=false, lateralTabs=true, frontDecoration=false,flatSide=false,n=2,blanks=0) {\r\n    xcopies(horizontalSpacing*(1+blanks),n=n) hws_insert(centerHole=centerHole, lateralTabs=lateralTabs, frontDecoration=frontDecoration,flatSide=flatSide);\r\n}\r\n\r\nmodule hws_insert_rail(length=verticalSpacing+20,width=19.7,n=1,depth=2){\r\n    hws_insert_n_hor(centerHole=false, lateralTabs=true, frontDecoration=false,flatSide=false,n=n,blanks=0);\r\n    prismoid(size1=[length,width+3], size2=[length,width], h=depth, anchor=TOP);\r\n}\r\n//Auxiliary modules. Coded separately for readability.\r\nmodule insert(hexHeight,insertDepth,frontDepth,frontExtension) {\r\n    union() {\r\n        //insert\r\n        rounded_prism(hexagon(d=2/sqrt(3)*hexHeight), h=insertDepth+frontDepth, anchor=BOTTOM, joint_top=0.25, joint_sides=.2, splinesteps=1);\r\n        //insert front edge\r\n        rounded_prism(hexagon(d=2/sqrt(3)*(hexHeight+frontExtension)), h=frontDepth, anchor=BOTTOM, joint_bot=.3, joint_sides=.1, splinesteps=1);\r\n    }\r\n}\r\n\r\nmodule insertHole(insideHexHeight,insertDepth,frontDepth) {\r\n    down(0.05)rounded_prism(hexagon(d=2/sqrt(3)*(insideHexHeight)), h=insertDepth+frontDepth+0.1, anchor=BOTTOM, joint_top=-0.25, joint_bot=-0.25, splinesteps=1);\r\n}\r\n\r\nmodule tabsCutout(hexHeight,gapHorDepth,insertDepth,frontDepth,gapLen=8,gapVert,gapVertDepth,gapHor,flatSide=false) {\r\n    difference() {\r\n        rot_copies(n=6, v=UP, delta=[0,hexHeight/2-gapHorDepth,0]) union() {\r\n            up(insertDepth+frontDepth+.1) cuboid([gapLen, gapVert, gapVertDepth+.1], anchor=TOP-BACK);\r\n            up(insertDepth+frontDepth-gapVertDepth+gapHor) cuboid([gapLen, gapHorDepth+.1, gapHor], anchor=TOP-BACK);\r\n        }\r\n        if(flatSide) back(hexHeight/2-gapHorDepth-.1) up(insertDepth+frontDepth+.2) cuboid([gapLen+1,gapVertDepth,insertDepth], anchor=TOP-BACK);\r\n    }\r\n}\r\n\r\nmodule tabBumps(bumpHeight,hexHeight,insertDepth,frontDepth,bumpLen,bumpAng) {\r\n    leftshift=bumpHeight;\r\n    rot_copies(n= 6, v=UP, delta=[0,hexHeight/2,0]) up(insertDepth+frontDepth-bumpLen) rotate([90,0,90]) right_half() left(leftshift) rounded_prism(teardrop2d(r=bumpHeight*2, ang=bumpAng, cap_h=bumpLen+sqrt((2*bumpHeight)^2-leftshift^2), $fn=36), h=bumpLen);\r\n}\r\nmodule hexCutout(innerHexHeight,cutDepth=0.4,lineWidth=1) {\r\n    tube(h=cutDepth, od=2/sqrt(3)*(innerHexHeight+lineWidth),id=2/sqrt(3)*(innerHexHeight), anchor=BOTTOM, $fn=6);\r\n}\r\nmodule sideFlattener(hexHeight, fullInsertDepth) {\r\n    down(0.2) back(hexHeight/2) cuboid([hexHeight*2, hexHeight, fullInsertDepth*1.5], anchor=BOTTOM-BACK);\r\n}\r\n\r\n\r\n\r\n\r\n// Component testing lines. Uncomment to view individual element with default size\r\n\r\n//insert(hexHeight=20,insertDepth=7.67,frontDepth=2,frontExtension=1);\r\n//tabBumps(bumpHeight=0.5,hexHeight=20,insertDepth=7.67,frontDepth=2,bumpLen=1.7,bumpAng=20);\r\n//tabsCutout(hexHeight=20,gapHorDepth=1.6,insertDepth=7.67,frontDepth=2,gapLen=8,gapVert=1,gapVertDepth=3.5,gapHor=0.8,flatSide=true);\r\n//color(\"red\") insertHole();\r\n//hexCutout(innerHexHeight=13.40+1.55);\r\n//sideFlattener(hexHeight = 20, fullInsertDepth=7.67+2);\r\n\r\n// Final module testing lines. Uncomment to view how different options generate different versions of the insert.\r\n//hws_insert();\r\n//hws_insert(flatSide=false);\r\n//hws_insert(flatSide=false, centerHole=true);\r\n//hws_insert(flatSide=false, centerHole=true, frontDecoration=true);\r\n\r\n//hws_insert_n_vert(blanks=1);\r\n//hws_insert_n_hor();\r\n//hws_insert_rail();\n\n// ---- render a decorated, center-holed insert ----\nhws_insert(centerHole=true, frontDecoration=true, flatSide=false);\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-j-hook",
      "title": "HSW J-Hook",
      "description": "A strong J-hook for hanging cables, bags, headphones, or tools — the arm reaches out and curves up into a cradle so things stay put. Swappable wall plugs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW J-Hook\n//\n// Source: honeycomb-wall J-hook\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across\nconnectors_x = 1;             // [1:8]\n// Hex connector rows (2 = strong cantilever)\nconnectors_y = 2;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Hook] */\n// Hook rod radius (mm)\nrod_r = 5;                    // [2.5:0.5:10]\n// How far the hook reaches out (mm)\nreach = 34;                   // [15:80]\n// Upturned tip height (mm)\nrise = 18;                    // [6:40]\n// Cradle droop (mm)\ndroop = 7;                    // [0:20]\n// Height up the wall (mm)\nhook_y = 0;                   // [-25:30]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\nPLATE = PLATE_T_DEFAULT;\n\nmodule hook_path() {\n    // J path in the X=0 plane: out along +Z, droop, then curve up +Y into a cradle.\n    pts = [\n        [0, hook_y,             PLATE - HSW_WELD],   // embedded in the plate (welds)\n        [0, hook_y,             PLATE + 3],\n        [0, hook_y - droop * 0.5, PLATE + reach * 0.45],\n        [0, hook_y - droop,     PLATE + reach * 0.80],\n        [0, hook_y - droop * 0.4, PLATE + reach],\n        [0, hook_y + rise * 0.5, PLATE + reach - 1],\n        [0, hook_y + rise,      PLATE + reach - 5]    // upturned tip\n    ];\n    for (i = [0 : len(pts) - 2])\n        hull() {\n            translate(pts[i])     sphere(r = rod_r, $fn = 28);\n            translate(pts[i + 1]) sphere(r = rod_r, $fn = 28);\n        }\n}\nmodule j_hook() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    hook_path();\n}\nhsw_display() j_hook();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-mount",
      "title": "HSW friction mount (plug + back-plate)",
      "description": "A back-plate carrying a lattice of friction hex plugs - the simple, render-safe foundation that accessories build on.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW simple friction mount — the shared, render-safe foundation accessories\n// build on. A back-plate (front, +Z) carries plug(s) on the -Z side that press\n// into the 20 mm hex holes. Millimetre-native. Self-contained.\n$fn = 48;\n\nHSW_PITCH_X = 20.4382;   // column pitch (flat-top honeycomb)\nHSW_PITCH_Y = 23.6;      // row pitch\nHSW_STAG_Y  = 11.8;      // alternate-column stagger\n\n// one hex plug: presses into a single 20 mm hole, 'fit' = friction clearance.\nmodule hsw_plug(fit = 0.35, len = 7) {\n    af = 20 - 2 * fit;                 // flat-to-flat, slightly under 20\n    translate([0, 0, -len])\n        cylinder(h = len, d = af / cos(30), $fn = 6);   // hex prism into -Z\n}\n\n// a back-plate (front face at +Z) carrying an nx×ny plug lattice on -Z.\nmodule hsw_mount(nx = 1, ny = 1, plate_t = 3, fit = 0.35) {\n    spanX = (nx - 1) * HSW_PITCH_X;\n    spanY = (ny - 1) * HSW_PITCH_Y;\n    w = spanX + 24;\n    d = spanY + 24;\n    translate([0, 0, plate_t / 2]) cube([w, d, plate_t], center = true);   // plate\n    for (c = [0 : nx - 1]) {\n        stag = (c % 2 == 1) ? HSW_STAG_Y : 0;\n        for (r = [0 : ny - 1])\n            translate([c * HSW_PITCH_X - spanX / 2,\n                       r * HSW_PITCH_Y - spanY / 2 + stag - HSW_STAG_Y / 2, 0])\n                hsw_plug(fit);\n    }\n}\n\n// demo: a 2×2 mount (the accessory body would union onto the +Z plate face)\nhsw_mount(2, 2);\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-open-bin",
      "title": "HSW Open Parts Bin",
      "description": "An open-top parts bin with a lowered front wall for easy scooping, on swappable wall plugs. Good for screws, clips, and small components.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Open Parts Bin\n//\n// Source: honeycomb-wall open parts bin\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across (wider = more)\nconnectors_x = 2;             // [1:8]\n// Hex connector rows (more = heavier loads)\nconnectors_y = 2;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Bin] */\n// Inside width (mm)\nbin_width = 80;               // [30:200]\n// Inside depth out from the wall (mm)\nbin_depth = 45;               // [20:120]\n// Back wall height (mm)\nbin_height = 50;              // [20:120]\n// Front wall height — lower for scooping (mm)\nfront_height = 28;            // [8:120]\n// Wall thickness (mm)\nwall = 2.4;                   // [1.2:0.2:4]\n// Floor thickness (mm)\nfloor_t = 2.4;                // [1.2:0.2:5]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule open_bin() {\n    ow = bin_width + 2 * wall;          // outer width  (X)\n    od = bin_depth + 2 * wall;          // outer depth  (Z, out from wall)\n    z0 = PLATE - HSW_WELD;              // weld the back wall into the plate\n    y0 = -bin_height / 2;               // centre the bin vertically so the back wall covers the plate\n    difference() {\n        // outer block rising up the wall (+Y), open at the top\n        translate([-ow / 2, y0, z0])\n            cube([ow, bin_height, od + HSW_WELD]);\n        // inner cavity (leaves walls + floor, open top)\n        translate([-bin_width / 2, y0 + floor_t, PLATE + wall])\n            cube([bin_width, bin_height, bin_depth + eps]);\n        // lower the front wall (the far +Z face) for scooping access\n        translate([-bin_width / 2, y0 + front_height, PLATE + bin_depth + wall - eps])\n            cube([bin_width, bin_height, wall + 2 * eps]);\n    }\n}\nmodule open_parts_bin() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    open_bin();\n}\nhsw_display() open_parts_bin();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-pen-cup",
      "title": "HSW Pen / Pencil Cup",
      "description": "A round, slightly back-tilted pen and pencil cup with an optional drain hole. Pick how it clips to the wall: friction peg, bare peg (into a latch), latch insert, or spring clip.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Pen / Pencil Cup\n//\n// Source: honeycomb-wall pen cup\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across (wider/heavier = more)\nconnectors_x = 1;             // [1:8]\n// Hex connector rows (2 = cantilever)\nconnectors_y = 2;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Cup] */\n// Inner bore diameter\ncup_inner_d = 40;         // [20:80]\n// Side wall thickness\ncup_wall = 2.4;           // [1.2:0.2:4]\n// Tube height (base to rim)\ncup_height = 70;          // [30:120]\n// Closed-base thickness\ncup_base_t = 2.4;         // [1.2:0.2:5]\n// Lean toward the wall (degrees)\nback_tilt = 6;            // [0:15]\n// Drain hole through the base\ndrain = true;\n// Drain hole diameter\ndrain_d = 6;              // [3:12]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\nweb_embed   = 2.0;\ngap_fix     = 0.02;\ncup_outer_d = cup_inner_d + 2 * cup_wall;\ncup_outer_r = cup_outer_d / 2;\nPLATE       = PLATE_T_DEFAULT;\n\nmodule cup_body() {\n    difference() {\n        rotate([-90, 0, 0]) cylinder(h = cup_height, d = cup_outer_d);\n        translate([0, cup_base_t, 0]) rotate([-90, 0, 0])\n            cylinder(h = cup_height - cup_base_t + gap_fix, d = cup_inner_d);\n        if (drain)\n            translate([0, -gap_fix, 0]) rotate([-90, 0, 0])\n                cylinder(h = cup_base_t + 2 * gap_fix, d = drain_d);\n    }\n}\nmodule connect_web() {\n    web_w = cup_outer_d - 1;\n    web_h = cup_height * 0.55;\n    z0 = PLATE - web_embed;\n    z1 = PLATE + cup_outer_r;\n    translate([-web_w / 2, 0, z0]) cube([web_w, web_h, z1 - z0]);\n}\nmodule pen_cup() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    translate([0, 0, PLATE]) rotate([back_tilt, 0, 0]) translate([0, 0, -PLATE]) {\n        translate([0, 0, PLATE + cup_outer_r - 0.5]) cup_body();\n        connect_web();\n    }\n}\nhsw_display() pen_cup();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-phone-shelf",
      "title": "HSW Phone / Tablet Shelf",
      "description": "A small ledge that holds a phone or tablet leaning against the wall, with a front lip to stop it sliding and a cable slot through the deck. Swappable wall plugs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Phone / Tablet Shelf\n//\n// Source: honeycomb-wall phone shelf\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across\nconnectors_x = 2;             // [1:8]\n// Hex connector rows\nconnectors_y = 2;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Shelf] */\n// Ledge width (mm)\nledge_width = 85;             // [40:160]\n// Ledge depth out from the wall (mm)\nledge_depth = 24;            // [15:60]\n// Ledge thickness (mm)\nledge_t = 5;                 // [3:12]\n// Front lip height (mm)\nlip_h = 14;                  // [6:30]\n// Lip thickness (mm)\nlip_t = 4;                   // [2:8]\n// Cable slot width (mm)\ncable_w = 18;                // [0:50]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule phone_shelf_body() {\n    z0 = PLATE - HSW_WELD;\n    y0 = -(ledge_t) / 2 - 8;      // ledge near the lower part of the plate\n    difference() {\n        union() {\n            // ledge deck (top surface at y0 + ledge_t)\n            translate([-ledge_width / 2, y0, z0]) cube([ledge_width, ledge_t, ledge_depth + HSW_WELD]);\n            // front lip rising up at the far edge\n            translate([-ledge_width / 2, y0, PLATE + ledge_depth - lip_t]) cube([ledge_width, ledge_t + lip_h, lip_t]);\n        }\n        // cable pass-through slot through the deck near the wall\n        if (cable_w > 0)\n            translate([-cable_w / 2, y0 - eps, PLATE + 3]) cube([cable_w, ledge_t + 2 * eps, 7]);\n    }\n}\nmodule phone_shelf() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    phone_shelf_body();\n}\nhsw_display() phone_shelf();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-plier-rack",
      "title": "HSW Plier / Cutter Rack",
      "description": "A row of wide slots that hold pliers, cutters, and snips by the handle, tips hanging down. Choose how many. Swappable wall plugs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Plier / Cutter Rack\n//\n// Source: honeycomb-wall plier rack\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across (a wide rack wants 3+)\nconnectors_x = 3;             // [1:8]\n// Hex connector rows\nconnectors_y = 1;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Rack] */\n// Number of tool slots\nslots = 4;                    // [1:8]\n// Slot width — fits the plier head (mm)\nslot_w = 14;                  // [6:30]\n// Slot length across the shelf (mm)\nslot_len = 24;               // [10:50]\n// Spacing between slots (mm)\nspacing = 30;                // [16:60]\n// Shelf thickness (mm)\nshelf_t = 10;                // [5:20]\n// Shelf height up the wall (mm)\nshelf_y = 0;                 // [-25:30]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule plier_rack() {\n    span = (slots - 1) * spacing;\n    w  = span + slot_len + 12;\n    bd = slot_len + 12;\n    difference() {\n        translate([-w / 2, shelf_y - shelf_t / 2, PLATE - HSW_WELD])\n            cube([w, shelf_t, bd + HSW_WELD]);\n        // wide vertical slots (through Y) the plier head drops through, handle rests on top\n        for (i = [0 : slots - 1])\n            translate([-span / 2 + i * spacing - slot_w / 2, shelf_y - shelf_t / 2 - eps, PLATE + (bd - slot_len) / 2])\n                cube([slot_w, shelf_t + 2 * eps, slot_len]);\n    }\n}\nmodule plier_rack_part() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    plier_rack();\n}\nhsw_display() plier_rack_part();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-rod-holder",
      "title": "HSW Rod / Pillar Holder",
      "description": "A deep round socket that holds a rod, dowel, pillar, umbrella, or long tool upright. Optional drain hole in the base. Swappable wall plugs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Rod / Pillar Holder\n//\n// Source: honeycomb-wall vertical rod holder\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across\nconnectors_x = 1;             // [1:8]\n// Hex connector rows (2 = strong for a tall rod)\nconnectors_y = 2;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Socket] */\n// Bore diameter (mm)\nbore_d = 24;                  // [8:60]\n// Wall thickness (mm)\nwall = 3;                     // [1.5:0.5:6]\n// Socket depth (mm)\ndepth = 65;                   // [25:140]\n// Base thickness (mm)\nbase_t = 4;                   // [2:12]\n// Drain hole through the base\ndrain = true;\n// Drain hole diameter (mm)\ndrain_d = 6;                  // [3:16]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule rod_socket() {\n    od = bore_d + 2 * wall;\n    or = od / 2;\n    // vertical tube (opens up +Y), lifted so it rests just off the plate\n    translate([0, 0, PLATE + or]) {\n        difference() {\n            rotate([-90, 0, 0]) cylinder(h = depth, d = od);\n            translate([0, base_t, 0]) rotate([-90, 0, 0]) cylinder(h = depth - base_t + eps, d = bore_d);\n            if (drain) translate([0, -eps, 0]) rotate([-90, 0, 0]) cylinder(h = base_t + 2 * eps, d = drain_d);\n        }\n    }\n    // web welding the socket back to the plate\n    web_w = od - 2;\n    web_h = depth * 0.6;\n    translate([-web_w / 2, 0, PLATE - HSW_WELD]) cube([web_w, web_h, or + HSW_WELD]);\n}\nmodule rod_holder() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    rod_socket();\n}\nhsw_display() rod_holder();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-scalpel-holder",
      "title": "HSW Scalpel / Craft-Knife Holder",
      "description": "Holds a round knife handle with internal retention lips and a side slot for spare blades; clips on with a hex plug.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\nTHICKNESS = 1.0;\n\nouter_diameter = 9.0;\ninner_diameter = 7.86;\nholder_height = 50;\n\ncutout_width = 1.5;\nblades_holder_width = 3.0;\ncutout_height = 30;\nblade_width = 8.1;\n\nlip_width = 1.5;\n\nchamfer = 1;\n\n$fn=200;\n\n/* [Hidden] */\nPLUG_HEXAGON_DIAMETER = 15.3;\nPLUG_LENGTH = 10;\n\nmodule hexagon_plug(){\n    cylinder(d=PLUG_HEXAGON_DIAMETER, h=PLUG_LENGTH, $fn=6, center=true);\n}\n\nmodule housing() {\n    xw = max(outer_diameter + THICKNESS * 2, PLUG_HEXAGON_DIAMETER + THICKNESS * 2) + chamfer*2;\n    yw = outer_diameter + THICKNESS * 2 + chamfer*2;\n    zw = holder_height + THICKNESS * 2 + chamfer*2;\n    difference() {\n        cuboid([xw, yw, zw], chamfer=chamfer);\n        translate([0, 0, THICKNESS]) tube(ir = 0, or=outer_diameter / 2, h=holder_height + THICKNESS*2 * chamfer*2, center=true);\n    }\n    translate([0, yw/2 + PLUG_LENGTH/2, zw/2 - PLUG_LENGTH/2 - chamfer*2 - THICKNESS]) rotate([90, 0, 0]) hexagon_plug();\n\n    translate([0, -yw/2 - blades_holder_width / 2 - THICKNESS + chamfer, -zw / 2 + cutout_height / 2]) {\n        difference() {\n            cuboid([xw, blades_holder_width + THICKNESS * 2 + chamfer * 2, cutout_height], chamfer=chamfer);\n            translate([0, 0, THICKNESS]) cuboid([blade_width, blades_holder_width, cutout_height]);\n        }\n    }\n}\n\n\nmodule lip() {\n    width = outer_diameter - inner_diameter;\n    translate([outer_diameter / 2 + width / 2, 0, (cutout_height) / 2 - 2]) {\n        scale([width, lip_width, 1])\n        tube(ir=0, or=1, holder_height - cutout_height);\n    }\n}\n\nmodule lips() {\n    intersection() {\n        union() {\n            rotate([0, 0, 30]) lip();\n            rotate([0, 0, -30]) lip();\n\n            rotate([0, 0, 210]) lip();\n            rotate([0, 0, -210]) lip();\n        }\n        tube(ir = 0, or=inner_diameter / 2 + THICKNESS, holder_height + THICKNESS);\n    }\n}\n\nmodule cutout() {\n    intersection() {\n\n        translate([0, 0, -holder_height / 2 + cutout_height / 2]) tube(ir = 0, or=outer_diameter / 2 + THICKNESS, cutout_height);\n\n        union() {\n            translate([outer_diameter / 2 + cutout_width / 2, 0, 0]) cuboid([outer_diameter, outer_diameter*2, holder_height + THICKNESS]);\n            translate([-outer_diameter / 2 - cutout_width / 2, 0, 0]) cuboid([outer_diameter, outer_diameter*2, holder_height + THICKNESS]);\n        }\n    }\n}\n\ndifference() {\n    union() {\n        cutout();\n        lips();\n        housing();\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-screwdriver-rack",
      "title": "HSW Screwdriver Rack",
      "description": "A wall shelf with a row of graduated holes that hold screwdrivers (or any shaft tool) shaft-down with the handle resting on top. Pick how it clips to the wall: friction peg, bare peg (into a latch), latch insert, or spring clip; grid or honeycomb support.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Screwdriver Rack\n//\n// Source: honeycomb-wall screwdriver rack\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across (a wide rack wants 3+)\nconnectors_x = 3;             // [1:8]\n// Hex connector rows (1 = single line)\nconnectors_y = 1;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Rack] */\n// Number of tool holes\nholes = 5;                    // [1:12]\n// Smallest hole diameter (mm)\nhole_min_d = 5;               // [3:20]\n// Largest hole diameter (mm)\nhole_max_d = 11;              // [3:30]\n// Spacing between hole centres (mm)\nhole_spacing = 22;            // [12:40]\n// How far the shelf projects from the wall (mm)\nshelf_depth = 28;             // [15:60]\n// Shelf thickness (mm)\nshelf_t = 7;                  // [4:14]\n// Shelf height up the wall (mm)\nshelf_y = 0;                  // [-25:30]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule rack_shelf() {\n    span = (holes - 1) * hole_spacing;\n    w    = span + hole_max_d + 8;\n    difference() {\n        // flat shelf: wide in X, deep in +Z, thin in Y, welded back into the plate\n        translate([-w / 2, shelf_y - shelf_t / 2, PLATE - HSW_WELD])\n            cube([w, shelf_t, shelf_depth + HSW_WELD]);\n        // graduated vertical bores (along Y), front-biased so a handle rests on top\n        for (i = [0 : holes - 1]) {\n            d = hole_min_d + (holes > 1 ? (hole_max_d - hole_min_d) * i / (holes - 1) : 0);\n            translate([-span / 2 + i * hole_spacing, shelf_y + shelf_t / 2 + eps, PLATE + shelf_depth * 0.55])\n                rotate([90, 0, 0])\n                    cylinder(h = shelf_t + 2 * eps, d = d);\n        }\n    }\n}\nmodule screwdriver_rack() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    rack_shelf();\n}\nhsw_display() screwdriver_rack();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-shelf",
      "title": "HSW Shelf",
      "description": "A flat shelf with a raised front lip to stop things sliding off, plus an angled support gusset underneath. Swappable wall plugs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Shelf\n//\n// Source: honeycomb-wall shelf\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across (a wide shelf wants 3+)\nconnectors_x = 3;             // [1:8]\n// Hex connector rows\nconnectors_y = 2;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Shelf] */\n// Shelf width (mm)\nshelf_width = 120;            // [40:250]\n// Shelf depth out from the wall (mm)\nshelf_depth = 45;             // [20:100]\n// Shelf thickness (mm)\nshelf_t = 4;                  // [2:10]\n// Front lip height (mm)\nlip_h = 9;                    // [0:25]\n// Front lip thickness (mm)\nlip_t = 3;                    // [2:6]\n// Support gusset depth (mm)\ngusset = 28;                  // [0:80]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\nPLATE = PLATE_T_DEFAULT;\n\nmodule shelf_body() {\n    z0 = PLATE - HSW_WELD;\n    // flat deck: wide X, thin Y, deep +Z. Top surface at Y=0.\n    translate([-shelf_width / 2, -shelf_t, z0])\n        cube([shelf_width, shelf_t, shelf_depth + HSW_WELD]);\n    // raised front lip at the far +Z edge\n    if (lip_h > 0)\n        translate([-shelf_width / 2, -shelf_t, PLATE + shelf_depth - lip_t])\n            cube([shelf_width, shelf_t + lip_h, lip_t]);\n    // angled support gusset under the deck (triangular prism spanning the width).\n    // +0.5 in Y overlaps it into the deck so the union is one clean shell.\n    if (gusset > 0)\n        translate([shelf_width / 2, -shelf_t + 0.5, z0])\n            rotate([0, -90, 0])\n                linear_extrude(height = shelf_width)\n                    polygon([[0, 0], [0, -gusset], [shelf_depth + HSW_WELD, 0]]);\n}\nmodule shelf() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    shelf_body();\n}\nhsw_display() shelf();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-test-tube-rack",
      "title": "HSW Test-Tube / Marker Rack",
      "description": "A row of equal round bores to hold test tubes, markers, pens, or small bottles upright. Swappable wall plugs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Test-Tube / Marker Rack\n//\n// Source: honeycomb-wall test-tube rack\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across (wider = more)\nconnectors_x = 3;             // [1:8]\n// Hex connector rows\nconnectors_y = 1;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Rack] */\n// Number of bores\nbores = 5;                    // [1:12]\n// Bore diameter (mm)\nbore_d = 18;                  // [6:40]\n// Spacing between bores (mm)\nspacing = 26;                 // [12:50]\n// Bore depth (mm)\nbore_depth = 30;             // [10:80]\n// Solid base under the bores (mm)\nbase_t = 4;                  // [2:12]\n// Wall thickness around bores (mm)\nwall = 3;                    // [1.5:0.5:6]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule tube_rack() {\n    span = (bores - 1) * spacing;\n    bw = span + bore_d + 2 * wall;\n    bd = bore_d + 2 * wall;           // Z depth out from wall\n    bh = bore_depth + base_t;         // Y height\n    z0 = PLATE - HSW_WELD;\n    y0 = -bh / 2;\n    difference() {\n        translate([-bw / 2, y0, z0]) cube([bw, bh, bd + HSW_WELD]);\n        for (i = [0 : bores - 1])\n            translate([-span / 2 + i * spacing, y0 + bh + eps, PLATE + bd / 2])\n                rotate([90, 0, 0]) cylinder(h = bore_depth, d = bore_d);   // vertical bore, open top\n    }\n}\nmodule test_tube_rack() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    tube_rack();\n}\nhsw_display() test_tube_rack();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-tool-cup",
      "title": "HSW Wide Tool Cup",
      "description": "A wide, flat-based cup for pliers, brushes, markers, or shop tools, held out from the wall on swappable plugs. Optional drain hole in the base.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW Wide Tool Cup\n//\n// Source: honeycomb-wall wide tool cup\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across\nconnectors_x = 2;             // [1:8]\n// Hex connector rows (2 = better for a heavy cup)\nconnectors_y = 2;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Cup] */\n// Inner bore diameter (mm)\ncup_inner_d = 60;             // [25:120]\n// Side wall thickness (mm)\ncup_wall = 2.6;               // [1.2:0.2:4]\n// Cup height (mm)\ncup_height = 55;              // [25:120]\n// Base thickness (mm)\ncup_base_t = 2.6;             // [1.2:0.2:5]\n// Drain hole through the base\ndrain = true;\n// Drain hole diameter (mm)\ndrain_d = 8;                  // [3:20]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule tool_cup_body() {\n    cup_outer_d = cup_inner_d + 2 * cup_wall;\n    cup_outer_r = cup_outer_d / 2;\n    // the cup is a vertical tube (opens up +Y), lifted so it rests just off the plate\n    translate([0, 0, PLATE + cup_outer_r]) {\n        difference() {\n            rotate([-90, 0, 0]) cylinder(h = cup_height, d = cup_outer_d);\n            translate([0, cup_base_t, 0]) rotate([-90, 0, 0])\n                cylinder(h = cup_height - cup_base_t + eps, d = cup_inner_d);\n            if (drain)\n                translate([0, -eps, 0]) rotate([-90, 0, 0])\n                    cylinder(h = cup_base_t + 2 * eps, d = drain_d);\n        }\n    }\n    // web welding the cup back to the plate face (one fused shell)\n    web_w = cup_outer_d - 2;\n    web_h = cup_height * 0.55;\n    translate([-web_w / 2, 0, PLATE - HSW_WELD])\n        cube([web_w, web_h, cup_outer_r + HSW_WELD]);\n}\nmodule tool_cup() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    tool_cup_body();\n}\nhsw_display() tool_cup();\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-tubes",
      "title": "HSW Tube Holder (diamond lattice)",
      "description": "A row of round tube holders with decorative diamond-lattice walls and a bottom plate, on a hex-connector strip. Set square_size = 0 for solid (faster) walls.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// number of tubes\ntube_count = 4;\ninner_diameter = 50;\n// thickness of tube wall\nwall_thickness = 2;\n// total tube lenght in mm\nheight = 80;\n\n// hole size in mm, set to 0 for solid walls\nsquare_size = 8;\n// grid thickness in mm\ngrid_thickness = 3;\n\n// bottom plate\nbottom_plate = true;\n\n// additional connector for stronger connection\nwider_connection = false;\n\n// taller back wall\ntaller_connection = false;\n\ninner_radius = inner_diameter/2;\nouter_radius = inner_radius + wall_thickness;\n\n\n//-----------------\n/* [HSW connector] */ \n\n// inner distance between corners\nconnector_size = 15.47;\nconnector_depth = 13;\ntolerance = 0.1;\nhexagon_distance = 40.88;\n\n$fn = 100;\n\n/* functions*/\nmodule diamond_tube() {\n\tsquare_diagonal = sqrt(square_size * square_size * 2);\n\tspacing = square_diagonal + grid_thickness;\n\touter_circumference = outer_radius * 2 * PI;\n\tsteps = round(outer_circumference / spacing);\n\tangle_step = 360 / steps;\n\n\tdifference() {\n\t\tcylinder(h = height, r = outer_radius);\n\n\t\tcylinder(h = height + 1, r = inner_radius);\n\n\t\t// Square pattern\n\t\tunion() {\n\t\t\tfor (pattern = [0 : 1]) {\n\t\t\t\tfor (z = [0 : square_diagonal + grid_thickness : height]) {\n\t\t\t\t\tfor (angle = [angle_step/2 * pattern : angle_step : 360]) {\n\t\t\t\t\t\trotate([0, 0, angle])\n\t\t\t\t\t\t\ttranslate([inner_radius + (outer_radius - inner_radius) / 2, 0, z + spacing /2 * pattern])\n\t\t\t\t\t\t\trotate([0, 90])\n\t\t\t\t\t\t\trotate([0, 0, 45])\n\t\t\t\t\t\t\tlinear_extrude(height=wall_thickness*3, center=true)\n\t\t\t\t\t\t\tsquare([square_size, square_size], center = true);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tdifference() {\n\t\tunion() {\n\t\t\ttranslate([0, 0, height - grid_thickness])\n\t\t\t\tcylinder(h = grid_thickness, r = outer_radius);\n\n\t\t\tcylinder(h = grid_thickness, r = outer_radius);\n\t\t}\n\n\t\ttranslate([0,0, -1])\n\t\t\tcylinder(h = height + 2, r = inner_radius);\n\t}\n\n\tif (bottom_plate) {\n\t\tcylinder(h = wall_thickness, r = outer_radius);\n\t}\n}\n\nfunction hexagon(radius) = [\n    for (i = [0:5])\n        [radius * cos(i * 60), radius * sin(i * 60)]\n];\n\nmodule hsw_connector() {\n    rotate([90, 0, 0])\n    linear_extrude(height = connector_depth)\n    offset(r = -tolerance)\n    polygon(points = hexagon(connector_size / 2));\n}\n\nmodule connectors() {\n    tubes_width = tube_count * (inner_diameter + wall_thickness) + wall_thickness;\n\n    count = max(1, round((wider_connection ? 1 : 0) + tubes_width / (hexagon_distance + connector_size / 2)));\n    strip_width = (count - 0.5) * hexagon_distance;\n\n    real_width = strip_width;\n    \n    dist = (real_width - (count-1) * hexagon_distance) / 2;\n    h = (connector_size/2 - tolerance)*sqrt(3)/2;    \n    diff = (tubes_width - strip_width) / 2;\n    \n    translate([-outer_radius + diff, 0, 0])\n    union() {\n        translate([dist, connector_depth, h])\n        for (i = [1 : count]) {\n            translate([(i-1) * hexagon_distance, 0, 0])\n            hsw_connector();    \n        }\n\n        cube([real_width, wall_thickness, connector_size * (taller_connection ? 2 : 1)], center = false);\n    }\n}\n\n/* MAIN */\nconnectors();\nfor (i = [1 : tube_count]) {\n//    translate([(i-1) * (outer_radius * 2), wall_thickness-outer_radius, 0])\n    translate([(i-1) * (inner_radius * 2 + wall_thickness), wall_thickness-outer_radius, 0])\n    diamond_tube();\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "hsw-u-hook",
      "title": "HSW U-Saddle Holder",
      "description": "A saddle block with a rounded groove that cradles round handles, cans, spray bottles, or power-tool bodies. Swappable wall plugs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// HSW U-Saddle Holder\n//\n// Source: honeycomb-wall U saddle holder\n// Generated from the library above, used under its license.\n\n/* [Wall mount] */\n// How it attaches to the wall\nconnector_type = \"peg\";       // [peg:Friction peg (into wall), barepeg:Bare peg (into a latch), latch:Latch insert (into wall), clip:Spring clip (into wall)]\n// Plug support style\nsupport_style = \"grid\";       // [grid:Grid (aligned rows + columns), honeycomb:Honeycomb (staggered, denser)]\n// Rotate the plug pattern (to match a wall mounted sideways)\nconnector_rotation = 0;       // [0:0 degrees, 90:90 degrees]\n// Hex connectors across\nconnectors_x = 2;             // [1:8]\n// Hex connector rows (2 = strong cantilever)\nconnectors_y = 2;             // [1:4]\n// Fit tweak: + looser / - tighter\nclearance = 0;                // [-0.3:0.05:0.5]\n\n/* [Saddle] */\n// Cradle diameter — fits the item (mm)\ncradle_d = 32;                // [12:90]\n// How far the saddle reaches out (mm)\nreach = 38;                   // [18:90]\n// Wall thickness around the groove (mm)\nwall = 4;                     // [2:0.5:8]\n// Front drop — how far the front lip rises to retain the item (mm)\nfront_lip = 10;              // [0:30]\n\n// ============================================================================\n//  HSW CONNECTOR TEMPLATE — the single canonical, self-contained connector kit\n//  every HSW example inlines VERBATIM so plug spacing + sizing stay identical.\n//  OpenSCAD 2021.01-safe. Self-contained: no include/use, no external libraries.\n//\n//  THE ONE RULE: plugs (hsw_mount) and wall holes (hsw_grid) are placed by the\n//  SAME lattice functions hsw_cx()/hsw_cy(), so plug(c,r) == hole(c,r) for ANY\n//  count by construction — verified 0.0000 mm deviation for 1..5 in each axis.\n//\n//  Frame: +Z = wall-normal (out of the wall). Connectors point into -Z (into the\n//  wall); the back-plate front face sits at +Z = plate_t; accessory bodies build\n//  on the +Z face. UP the wall = +Y, X = horizontal.\n//\n//  CUSTOMIZER: everything below is in the Hidden group so a product recipe that\n//  inlines this template exposes ONLY its own dimensions + connectors_x/y +\n//  connector_type + one clearance tweak. The HSW internals are NEVER customer-tunable.\n// ============================================================================\n/* [Hidden] */\n$fn = 48;\n\n// ---- Core hex interface (the 20mm hole a connector plugs into) -------------\nHSWX_INSIDE       = 20;                              // hex hole flat-to-flat\nHSWX_WALL         = 1.8;                             // shared cell wall thickness\nHSWX_OUTSIDE      = HSWX_INSIDE + HSWX_WALL*2;       // 23.6  outside flat-to-flat\nHSWX_SIDE         = HSWX_OUTSIDE / sqrt(3);          // ~13.6255 outer side length\nHSWX_DEPTH        = 8;                               // panel / plug depth (Std Duty)\nHSWX_LOCK_DEPTH   = 5;                               // depth where the lock step starts\nHSWX_LOCK_LIP     = 1;                               // 1mm internal lip the barb catches\nHSWX_LOCK_CHAMFER = HSWX_LOCK_LIP;                   // 1mm @ 45deg lead-in\n\n// ---- Verified honeycomb lattice (flat-topped hexes, column tiling) ---------\nHSW_PITCH_X       = 1.5 * HSWX_SIDE;                 // ~20.4382 column-to-column\nHSW_PITCH_Y       = HSWX_OUTSIDE;                    // 23.6    row-to-row in a column\nHSW_STAGGER_Y     = HSWX_OUTSIDE / 2;               // 11.8    alternate-column offset\n\n// ---- Shared connector tunables ---------------------------------------------\nPEG_FIT_DEFAULT   = 0.35;                            // bare-peg friction clearance\nPEG_LEN_DEFAULT   = 7;                               // bare-peg depth into wall (< HSWX_DEPTH)\nPLATE_T_DEFAULT   = 3;                               // back-plate thickness (welds depend on it)\nHSW_WELD          = 0.6;                              // connector overlap INTO the plate (one fused solid)\n\n// Across-flats -> circumscribed diameter (a $fn=6 cylinder lands on those flats).\n// af/sqrt(3)*2 is IDENTICAL to af/cos(30); ONE form library-wide.\nfunction hsw_af_to_d(af) = af / sqrt(3) * 2;\n\n// ============================================================================\n//  THE LATTICE — the single source of truth for WHERE connectors/holes go.\n//  Bounding-box-centred on the origin for ANY nx/ny (incl. the single-column\n//  case): the staggered odd columns only exist when nx>1, so the y-centring of\n//  the cluster is conditional on nx>1. hsw_mount AND hsw_grid both call these.\n// ============================================================================\nfunction hsw_max_stag(nx) = (nx > 1) ? HSW_STAGGER_Y : 0;     // odd col only exists if nx>1\nfunction hsw_cx(c, nx)    = c*HSW_PITCH_X - (nx-1)*HSW_PITCH_X/2;\nfunction hsw_cy(c, r, nx, ny) =\n    r*HSW_PITCH_Y\n    - (ny-1)*HSW_PITCH_Y/2\n    + ((c % 2 == 1) ? HSW_STAGGER_Y : 0)            // real alternate-column stagger\n    - hsw_max_stag(nx)/2;                            // centre the cluster (cond. on nx>1)\n\n// straight=true strip: every-OTHER column, no stagger (the legacy 40.88 strip).\n// Use 2*HSW_PITCH_X (=40.8764), NEVER the rounded 40.88 literal.\nfunction hsw_cx_straight(c, nx) = c*(2*HSW_PITCH_X) - (nx-1)*(2*HSW_PITCH_X)/2;\n\n// ============================================================================\n//  CONNECTOR 1 — hsw_peg : bare friction press-peg (the de-facto canonical peg).\n//  $fn=6 hex prism, across-flats = HSWX_INSIDE - 2*fit, into -Z. Press fit only.\n// ============================================================================\nmodule hsw_peg(fit = PEG_FIT_DEFAULT, len = PEG_LEN_DEFAULT) {\n    af = HSWX_INSIDE - 2*fit;                        // flat-to-flat, slightly under 20\n    // hex prism from z=+HSW_WELD (buried INTO the plate) down to z=-len (into wall),\n    // so the peg fuses with any plate occupying z>=0 into ONE solid (no coincident-face seam).\n    translate([0, 0, -len])\n        cylinder(h = len + HSW_WELD, d = hsw_af_to_d(af), $fn = 6);\n}\n\n// ============================================================================\n//  CONNECTOR 1b — hsw_bare_peg : the THIN CORE peg (hex-plug-library hexagon_plug)\n//  that plugs into a SEPARATE latch insert pre-snapped in the wall — NOT the grid\n//  directly. Verbatim dims: 14.7 across-corners, 13 deep. Into -Z, welds the plate.\n// ============================================================================\nBAREPEG_D   = 14.7;     // across-corners ($fn=6) — source PLUG_HEXAGON_DIAMETER\nBAREPEG_LEN = 13;       // source PLUG_LENGTH\nmodule hsw_bare_peg(fit = 0) {\n    translate([0, 0, -BAREPEG_LEN])\n        cylinder(h = BAREPEG_LEN + HSW_WELD, d = BAREPEG_D - 2*fit, $fn = 6);\n}\n\n// ============================================================================\n//  hsw_mount(nx, ny, type, plate_t, fit, straight)\n//  ONE back-plate (front face +Z = plate_t) carrying an nx*ny connector lattice\n//  on -Z, placed by hsw_cx/hsw_cy. type dispatches the per-cell connector.\n// ============================================================================\n// straight=true  -> GRID support  : pegs on an aligned rectangular grid\n//                                    (40.8764 horiz x 23.6 vert, every-other\n//                                    honeycomb column -> all land in real holes).\n// straight=false -> HONEYCOMB support: pegs follow the staggered hex lattice\n//                                    (20.4382 column pitch, odd columns staggered).\nmodule hsw_mount(nx = 1, ny = 1, type = \"peg\", plate_t = PLATE_T_DEFAULT,\n                 clearance = 0, straight = false, margin = 3, crot = 0) {\n    // clamp counts so a user 0 (or negative) never yields an empty / garbage mount\n    gnx = max(1, nx);\n    gny = max(1, ny);\n    // per-type base fit (peg=press 0.35, clip=snap 0.15, latch/barepeg=0 lip-tuned)\n    // + the product's single user clearance tweak (+ looser / - tighter).\n    pfit = (type == \"clip\" ? CLIP_FIT_DEFAULT\n          : (type == \"latch\" || type == \"barepeg\") ? 0\n          : PEG_FIT_DEFAULT) + clearance;\n    // plate footprint spans the lattice + margin. GRID has no stagger; only the\n    // staggered honeycomb extends Y by the odd-column stagger.\n    spanX = straight ? (gnx-1)*(2*HSW_PITCH_X) : (gnx-1)*HSW_PITCH_X;\n    spanY = (gny-1)*HSW_PITCH_Y;\n    w = spanX + HSWX_OUTSIDE + 2*margin;\n    d = spanY + HSWX_OUTSIDE + (straight ? 0 : hsw_max_stag(gnx)) + 2*margin;\n    // crot rotates the PLUG PATTERN (plate + connectors, together so plugs stay on\n    // the plate) about the wall normal so the accessory can match a wall installed\n    // rotated 90deg. The body is built separately by the recipe and is NOT rotated\n    // (plug layout only — body fixed).\n    rotate([0, 0, crot]) {\n        translate([0, 0, plate_t/2]) cube([w, d, plate_t], center = true);   // plate\n        for (c = [0 : gnx-1])\n            for (r = [0 : gny-1]) {\n                x = straight ? hsw_cx_straight(c, gnx) : hsw_cx(c, gnx);\n                y = straight ? (r*HSW_PITCH_Y - spanY/2) : hsw_cy(c, r, gnx, gny);\n                translate([x, y, 0]) hsw_connector(type, pfit);\n            }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 2 — hsw_latch_insert : snap-tab latching insert (vanilla port of\n//  hex-plug-library / hsw-custom-insert, sh1-verified). Built front-face(lip) at\n//  z=0 with body in +Z, then flipped to -Z and welded into the plate.\n// ============================================================================\nINSERT_BODY_FLATS = 19.7;   INSERT_LIP_FLATS = 22.5;   INSERT_TOTAL_H = 10;\nINSERT_LIP_H = 2.5;         INSERT_SNAP_H = 6.5;       INSERT_TAB_OUT = 0.5;\nINSERT_TAB_LEN = 2;         INSERT_SLOT_LEN = 8;       INSERT_SLOT_W = 0.8;\nINSERT_SLOT_SIDE = 1;       INSERT_SLOT_RAD = 8.25;\n\nmodule _hsw_bevel_cut(diameter) {\n    h = diameter/2;                                     // tan(45)==1\n    difference() { cylinder(d = diameter*2, h = h); cylinder(d1 = diameter, d2 = 0, h = h); }\n}\nmodule _hsw_insert_body(fit) {\n    bodyD = hsw_af_to_d(INSERT_BODY_FLATS - 2*fit);\n    lipD  = hsw_af_to_d(INSERT_LIP_FLATS);\n    difference() {\n        union() {\n            cylinder(d = bodyD, h = INSERT_TOTAL_H, $fn = 6);   // main plug\n            cylinder(d = lipD,  h = INSERT_LIP_H,  $fn = 6);    // overhang lip\n        }\n        translate([0,0,INSERT_TOTAL_H - 0.35]) _hsw_bevel_cut(bodyD);   // soften top edge\n    }\n}\nmodule hsw_latch_insert(fit = 0) {\n    translate([0,0,HSW_WELD]) rotate([180,0,0])\n    union() {\n        // six snap tabs (non-degenerate tip r=0.2, tops kept below the bevel)\n        for (i=[0:5]) rotate([0,0,i*60]) translate([0, INSERT_BODY_FLATS/2, 0])\n            hull() {\n                translate([0,0,INSERT_SNAP_H + INSERT_SLOT_SIDE + INSERT_TAB_OUT])\n                    rotate([0,90,0]) cylinder(r = INSERT_TAB_OUT, h = INSERT_TAB_LEN, center=true, $fn=20);\n                translate([0,0,INSERT_TOTAL_H - 0.6])\n                    rotate([0,90,0]) cylinder(r = 0.2, h = INSERT_TAB_LEN, center=true, $fn=20);\n            }\n        // body with flex slots so each tab deflects on insertion\n        difference() {\n            _hsw_insert_body(fit);\n            for (i=[0:5]) rotate([0,0,i*60]) {\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_SLOT_W, INSERT_TOTAL_H]);\n                translate([-INSERT_SLOT_LEN/2, INSERT_SLOT_RAD, INSERT_SNAP_H]) cube([INSERT_SLOT_LEN, INSERT_TOTAL_H, INSERT_SLOT_SIDE]);\n            }\n        }\n    }\n}\n\n// ============================================================================\n//  CONNECTOR 3 — hsw_clip : spring snap clip (from hsw-lib-core, sh1-verified).\n//  Two cantilever arms catch the 1mm lip at 5mm depth. Mouth at z=0 into -Z.\n// ============================================================================\nCLIP_FIT_DEFAULT = 0.15;\nCLIP_AF  = HSWX_INSIDE;      // 20 plug across-flats target\nCLIP_LEN = HSWX_DEPTH;       // 8  plug length into the wall\nSPRING_WALL = 1.6;           // cantilever arm wall (>=1.4 to flex)\nSPRING_SLOT = 2.2;           // central relief slot width\nmodule hsw_clip(fit = CLIP_FIT_DEFAULT) {\n    af = CLIP_AF - 2*fit;  L = CLIP_LEN;\n    barb = HSWX_LOCK_LIP;  locz = HSWX_LOCK_DEPTH;  cham = HSWX_LOCK_CHAMFER;  halfX = af/2;\n    translate([0,0,HSW_WELD])\n    difference() {\n        union() {\n            translate([0,0,-L/2]) linear_extrude(height = L, center = true) hsw_hex2d(af);   // hex plug\n            for (sx=[-1,1]) translate([sx*halfX,0,-locz]) rotate([90,0,0])                    // outward barbs\n                linear_extrude(height = af*0.62, center = true)\n                    polygon([[0,0],[sx*barb,0],[sx*barb,cham],[0,cham+barb]]);\n            translate([0,0,0.6/2]) linear_extrude(height = 0.6, center = true) hsw_hex2d(af + 1.2);  // mouth flange\n        }\n        translate([0,0,-L/2-0.5]) cube([SPRING_SLOT, af+4, L+2-1.2], center = true);          // central relief slot\n        for (sx=[-1,1]) translate([sx*(halfX-SPRING_WALL-0.3),0,-L/2-1]) cube([1.4, af*0.5, L], center=true);  // arm hollow\n    }\n}\n\n// per-cell connector dispatch:\n//   peg     = friction peg, jams the bare grid hole (one-part)\n//   barepeg = thin core peg, plugs into a SEPARATE latch pre-snapped in the wall\n//   latch   = snap-tab insert, snaps into the bare grid hole\n//   clip    = spring clip, snaps into the bare grid hole\nmodule hsw_connector(type = \"peg\", fit = PEG_FIT_DEFAULT) {\n    if      (type == \"latch\")   hsw_latch_insert(fit);\n    else if (type == \"clip\")    hsw_clip(fit);\n    else if (type == \"barepeg\") hsw_bare_peg(fit);\n    else                        hsw_peg(fit);\n}\n\n// ============================================================================\n//  hsw_grid(nx, ny, depth) — the FEMALE wall panel. Holes placed by the SAME\n//  hsw_cx/hsw_cy so a hsw_mount(nx,ny) co-registers EXACTLY (proof artifact).\n//  Lipped hex socket: 20mm bore, 1mm catch lip at 5mm depth.\n// ============================================================================\nmodule hsw_hex2d(af) { rotate([0,0,90]) circle(r = af/sqrt(3), $fn = 6); }\n\nmodule hsw_hex_hole(depth) {\n    afMouth = HSWX_INSIDE + 2*HSWX_LOCK_CHAMFER;\n    afFull  = HSWX_INSIDE;\n    afLip   = HSWX_INSIDE - 2*HSWX_LOCK_LIP;\n    translate([0,0,-HSWX_LOCK_CHAMFER/2 + 0.01])\n        linear_extrude(HSWX_LOCK_CHAMFER + 0.02, center=true, scale = afFull/afMouth) hsw_hex2d(afMouth);\n    translate([0,0,-HSWX_LOCK_DEPTH/2])\n        linear_extrude(HSWX_LOCK_DEPTH + 0.02, center=true) hsw_hex2d(afFull);\n    translate([0,0,-HSWX_LOCK_DEPTH])\n        linear_extrude(0.6, center=true) hsw_hex2d(afLip);\n    backLen = depth - HSWX_LOCK_DEPTH;\n    if (backLen > 0.1)\n        translate([0,0,-(HSWX_LOCK_DEPTH + backLen/2)])\n            linear_extrude(backLen + 0.4, center=true) hsw_hex2d(afFull);\n}\n\nmodule hsw_grid(nx = 3, ny = 3, depth = HSWX_DEPTH) {\n    spanX = (nx-1)*HSW_PITCH_X;\n    spanY = (ny-1)*HSW_PITCH_Y;\n    slabW = spanX + HSWX_OUTSIDE;\n    slabD = spanY + HSWX_OUTSIDE + hsw_max_stag(nx);\n    difference() {\n        translate([0, -hsw_max_stag(nx)/2, -depth/2]) cube([slabW, slabD, depth], center = true);\n        for (c = [0 : nx-1])\n            for (r = [0 : ny-1])\n                translate([hsw_cx(c, nx), hsw_cy(c, r, nx, ny), 0]) hsw_hex_hole(depth);\n    }\n}\n\n// ============================================================================\n//  hsw_display() — canonical AnimBox display orientation. The geometry is built\n//  in the OpenSCAD print frame (scad +Z = wall normal, +Y = up the wall). The\n//  The OpenSCAD->AnimBox import is IDENTITY (verified by bbox: cube([10,20,40])\n//  imports to world [10,20,40]). The wall-frame is already the display frame:\n//  up-the-wall (+Y) = world UP, along-the-wall (+X) = world X, wall normal (+Z)\n//  = toward the viewer (the body projects out of the wall). So hsw_display() is a\n//  pass-through; it stays as the single place to adjust display orientation if the\n//  import convention ever changes. Wrap every recipe's top call: hsw_display() my_part();\n// ============================================================================\nmodule hsw_display() { children(); }\n\n// ---- product body (hidden internals) ----\neps   = 0.05;\nPLATE = PLATE_T_DEFAULT;\n\nmodule saddle_block() {\n    bw  = cradle_d + 2 * wall;             // X width\n    bh  = cradle_d + wall + front_lip;     // Y height\n    z0  = PLATE - HSW_WELD;\n    y0  = -bh / 2;\n    ych = y0 + bh - cradle_d / 2 - wall;   // channel centre, near the top\n    mouth = cradle_d * 0.62;               // retaining mouth (narrower than the item)\n    difference() {\n        translate([-bw / 2, y0, z0]) cube([bw, bh, reach + HSW_WELD]);\n        // cradle channel: a cylinder along Z (front-to-back) the item rests in\n        translate([0, ych, z0 - eps]) cylinder(h = reach + HSW_WELD + 2 * eps, d = cradle_d);\n        // top mouth so the item snaps in (narrower than the channel -> retains it)\n        translate([-mouth / 2, ych, z0 - eps]) cube([mouth, bh, reach + HSW_WELD + 2 * eps]);\n    }\n}\nmodule u_hook() {\n    hsw_mount(connectors_x, connectors_y, type = connector_type, clearance = clearance,\n              straight = (support_style == \"grid\"), crot = connector_rotation);\n    saddle_block();\n}\nhsw_display() u_hook();\n`);"
    },
    {
      "kind": "recipe",
      "id": "install-m5-bolt-from-catalog",
      "title": "Install an off-the-shelf M5 bolt",
      "description": "Drop an M5 hex-cap bolt into the scene from the bundled catalog (12,718 STEP parts). Use this pattern for ANY standard hardware: bolts, bearings, nuts, washers, standoffs, motors. The catalog is workflow A in DOCTRINE: try the catalog FIRST before composing geometry from primitives.",
      "intent": "io",
      "editor": "model",
      "category": "misc",
      "apiSurfaces": [
        "ModelEditor.catalog.search",
        "ModelEditor.catalog.install"
      ],
      "code": "// Workflow A: try the bundled catalog FIRST before composing geometry.\n// Self-contained from an empty Model scene.\n\n// 1. Search the catalog by customer keyword. search() is SYNCHRONOUS and\n//    returns CatalogPart[] ({ id, name, license, tags, ... }) ranked by\n//    relevance (name matches beat tag matches). Empty query -> [].\nconst hits = catalog.search('M5 bolt');\nconsole.log('found ' + hits.length + ' M5-bolt candidates: ' +\n  hits.slice(0, 3).map((h) => h.name + ' [' + h.license + ']').join(', '));\n\nif (hits.length === 0) {\n  console.log('Catalog has no M5 bolt registered in this build - nothing to install.');\n} else {\n  // 2. Install the top hit. install(id, opts?) is ASYNC -> await. The only\n  //    option is position (world-space meters); default is [2.5, 1.3, -0.7]\n  //    so consecutive installs do not stack invisibly on the origin.\n  const bolt = await catalog.install(hits[0].id, {\n    position: [10, 0, 0],   // place this bolt 10 m down +X\n  });\n  console.log('installed \"' + bolt.name + '\" with ' + bolt.faces.count + ' faces');\n}\n\n// NOTE: the catalog is for ADDITIVE parts. To DRILL a hole for an M5 bolt\n// (subtractive feature), use workflow B: mesh.boolean(other, 'difference')\n// with a 5 mm-diameter cylinder. See the compound-engine-cylinder gallery\n// entry for an example of N bolt holes drilled on a flange.\n",
      "expectedOutput": "M5 hex-cap bolt installed at [10, 0, 0]; the catalog covers ~12K parts under CC0 / CC-BY / MIT / Apache-2.0 licenses."
    },
    {
      "kind": "recipe",
      "id": "io-export-each-mesh-separately",
      "title": "Export every mesh as a separate file",
      "description": "Loop through every mesh, select each in isolation, and export it as its own .glb. Useful for splitting a multi-mesh asset into a per-mesh library.",
      "intent": "io",
      "editor": "previewer",
      "category": "io",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "ModelEditor.scene.select",
        "ModelEditor.io.exportSelectedModel"
      ],
      "code": "// One .glb per mesh, file name derived from the mesh name.\nconst meshes = ls({ type: 'mesh' });\nconst results = [];\nfor (const m of meshes) {\n  select(null, {mode: 'clear'});\n  select(m);\n  try {\n    const exp = await io.exportSelectedModel(m.name + '.glb', 'glb');\n    results.push({ name: m.name, size: exp.size, ok: true });\n    console.log(m.name + '.glb', exp.size, 'bytes');\n  } catch (err) {\n    results.push({ name: m.name, ok: false, error: err.message });\n    console.warn(m.name, 'failed:', err.message);\n  }\n}\nconsole.log('summary:', JSON.stringify(results));\n",
      "expectedOutput": "one .glb exported per mesh; console line per file with byte size."
    },
    {
      "kind": "recipe",
      "id": "isolation-and-grouping-flow",
      "title": "Isolate, deisolate, showAll, and group selections",
      "description": "Spawn meshes; isolate a subset; deisolate; hide and reveal; create an empty group; spawn a hook profile.",
      "intent": "mesh",
      "editor": "model",
      "category": "outliner",
      "apiSurfaces": [
        "ModelEditor.viewport.isolate",
        "ModelEditor.viewport.deisolate",
        "ModelEditor.viewport.showAll",
        "ModelEditor.create.group",
        "ModelEditor.create.hookProfile",
        "Utils.wait.frame",
        "ModelEditor.dev.snapSettings",
        "ModelEditor.dev.snapSettings.snapScale"
      ],
      "code": "const cube = await create.cube({ name: 'IsoCube' });\ncube.translate.set([2.5, 1.3, -0.7]);\nselect(null, {mode: 'clear'});\n\nconst sphere = await create.sphere({ name: 'IsoSphere' });\nsphere.translate.set([5.0, 1.3, -0.7]);\nselect(null, {mode: 'clear'});\n\nconst hook = await create.hookProfile({ name: 'IsoHook' });\nhook.translate.set([7.5, 1.3, -0.7]);\nselect(null, {mode: 'clear'});\nawait Utils.wait.frame();\n\nviewport.isolate(cube);\nconsole.log('isolated cube');\nawait Utils.wait.frame();\n\nviewport.deisolate();\nconsole.log('exited isolate mode');\nawait Utils.wait.frame();\n\nviewport.showAll();\nconsole.log('showAll fired');\n\n// Resolve by name via the universal editor ls gateway.\nconst ref = ls('IsoCube')[0];\nconsole.log('ls(\"IsoCube\")[0] nodeId:', ref?.nodeId.slice(0, 8) ?? '(not found)');\n\nconst grp = create.group({ name: 'EmptyHolder' });\nconsole.log('group:', grp.name);\n\n// Snap settings facade\nconst before = dev.snapSettings.snapScale;\ndev.snapSettings.snapScale = 0.25;\nconsole.log('snapScale ' + before + ' → ' + dev.snapSettings.snapScale);\n",
      "expectedOutput": "console: status after isolate / deisolate / showAll; group name; snapScale toggled."
    },
    {
      "kind": "recipe",
      "id": "learn-2d-boolean",
      "title": "2D booleans",
      "description": "Shows union(), difference(), and intersection() applied to 2D circles and squares before a single extrude, so you can see each boolean result as a separate printed tile.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// 2D booleans\n\n/* 2D booleans - union / difference / intersection shown as three side-by-side tiles. */\nr         = 18;   // circle radius\nsq_side   = 28;   // square side length\nthickness =  4;   // extrusion thickness\nspacing   = 70;   // horizontal gap between tiles\n$fn = 48;\n\n// Helper: a circle and a square overlapping at their centres.\n// The circle is offset so roughly half of it overlaps the square.\noffset_x = sq_side * 0.3;  // how much to shift the circle right\n\nmodule circle_shape()  { translate([offset_x, 0]) circle(r = r); }\nmodule square_shape()  { translate([-sq_side / 2, -sq_side / 2]) square(sq_side); }\n\n// --- Tile 1: union (all filled area) ---\ntranslate([-spacing, 0, 0])\n    linear_extrude(height = thickness)\n        union() { circle_shape(); square_shape(); }\n\n// --- Tile 2: difference (square minus circle) ---\ntranslate([0, 0, 0])\n    linear_extrude(height = thickness)\n        difference() { square_shape(); circle_shape(); }\n\n// --- Tile 3: intersection (only the overlapping region) ---\ntranslate([spacing, 0, 0])\n    linear_extrude(height = thickness)\n        intersection() { circle_shape(); square_shape(); }\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-2d-shapes",
      "title": "circle(), square(), polygon()",
      "description": "Lays the three core 2D primitives side by side and extrudes each to a thin slab so the result is a printable 3D object you can examine from every angle.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// circle(), square(), polygon()\n\n/* OpenSCAD 2D primitives are flat - they need linear_extrude()\n   to become solid 3D geometry.\n   circle(r)  - disc centred at the origin\n   square(s)  - rectangle; center=true to straddle the origin\n   polygon(p) - arbitrary shape from a list of [x,y] points      */\n\nthick = 4;    // extrusion thickness (mm)\nsize  = 24;   // nominal size for each shape\ngap   = 12;   // spacing between shapes\n$fn   = 48;\n\n// Circle - disc of given radius\ncolor(\"steelblue\")\n    linear_extrude(height = thick)\n        circle(r = size / 2);\n\n// Square - centred so it sits at the same visual baseline as the circle\ntranslate([size + gap, -size / 2, 0])\n    color(\"tomato\")\n        linear_extrude(height = thick)\n            square([size, size]);\n\n// Polygon - a simple arrow-head shape\narrow = [\n    [0,  0],\n    [size * 0.6,  size / 2],\n    [size * 0.3,  size / 2],\n    [size * 0.3,  size],\n    [-size * 0.3, size],\n    [-size * 0.3, size / 2],\n    [-size * 0.6, size / 2]\n];\n\ntranslate([(size + gap) * 2, -size / 2, 0])\n    color(\"gold\")\n        linear_extrude(height = thick)\n            polygon(arrow);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-boolean-combo",
      "title": "combining booleans",
      "description": "Shows how nesting union() inside difference() builds a simple mounting bracket from basic shapes. A vertical post and horizontal foot are joined first, then a bolt hole and slot are cut out.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// combining booleans\n\n/* Nested boolean example - a simple L-bracket. All dimensions in mm. */\nfoot_w    = 60;   // foot (horizontal part) width\nfoot_d    = 20;   // foot depth\nfoot_h    = 5;    // foot thickness\npost_w    = 10;   // post (vertical part) width\npost_h    = 50;   // post height\nbolt_d    = 5;    // diameter of bolt holes\nslot_w    = 6;    // slot width on the post\nslot_len  = 20;   // slot length on the post\n$fn = 32;\n\n// Step 1: unite the two solid bodies\nmodule bracket_solid() {\n    union() {\n        // Horizontal foot\n        cube([foot_w, foot_d, foot_h]);\n\n        // Vertical post rising from the left end of the foot\n        translate([0, 0, foot_h])\n            cube([post_w, foot_d, post_h]);\n    }\n}\n\n// Step 2: subtract holes and a slot from the solid\ndifference() {\n    bracket_solid();\n\n    // Two bolt holes in the foot, evenly spaced\n    for (x = [foot_w * 0.25, foot_w * 0.75])\n        translate([x, foot_d / 2, -1])\n            cylinder(d = bolt_d, h = foot_h + 2);\n\n    // An adjustment slot centred in the post\n    translate([post_w / 2, foot_d / 2, foot_h + (post_h - slot_len) / 2])\n        rotate([90, 0, 0])\n            // Slot = hull of two cylinders\n            hull() {\n                translate([0,  slot_len / 2 - slot_w / 2, 0]) cylinder(d = slot_w, h = foot_d + 2, center = true);\n                translate([0, -slot_len / 2 + slot_w / 2, 0]) cylinder(d = slot_w, h = foot_d + 2, center = true);\n            }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-children",
      "title": "children() in a module",
      "description": "Defines a module that places whatever geometry you pass into it at evenly-spaced positions around a ring, using children() and a for loop with rotate().",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// children() in a module\n\n/* children() lets a module receive geometry from its caller,\n   just like a function receives arguments.\n   The ring_array module below rotates its child N times around Z,\n   each time moving it out to a chosen radius first. */\n\ncount  = 6;     // number of copies in the ring\nradius = 35;    // ring radius (mm)\npeg_d  = 8;     // diameter of each peg\npeg_h  = 15;    // height of each peg\n$fn    = 32;\n\n// Module: arrange children() in a circular ring\nmodule ring_array(n, r) {\n    for (i = [0 : n - 1]) {\n        angle = 360 / n * i;\n        rotate([0, 0, angle])\n            translate([r, 0, 0])\n                children();\n    }\n}\n\n// A thin base plate so the pegs have something to stand on\ncylinder(h = 3, d = (radius + peg_d) * 2 + 4, $fn = 64);\n\n// Pass a cylinder peg as the child geometry\ncolor(\"steelblue\")\n    ring_array(count, radius)\n        cylinder(h = peg_h, d = peg_d);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-circle-square",
      "title": "circle() and square()",
      "description": "Shows the two core 2D primitives - circle() and square() - combined with 2D union and difference to form a rounded-corner key-tag shape, then extruded into a printable part.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// circle() and square()\n\n/* circle() and square() - core 2D shapes, combined then extruded. */\ntag_w     = 50;   // key-tag body width\ntag_h     = 28;   // key-tag body height\nthickness =  4;   // extrusion thickness\nhole_r    =  4;   // hang-hole radius\ncorner_r  =  6;   // rounded corner radius (approximated with circles at hull)\n$fn = 48;\n\n// Build the 2D key-tag outline:\n//   1. hull() of four corner circles => rounded rectangle body\n//   2. union with a circle at the top for the hang-lug\n//   3. difference to punch the hang hole\nmodule key_tag_2d() {\n    difference() {\n        union() {\n            // Rounded rectangle body via hull of corner discs\n            hull()\n                for (x = [corner_r, tag_w - corner_r], y = [corner_r, tag_h - corner_r])\n                    translate([x, y]) circle(r = corner_r);\n            // Hang lug: a circle centred above the tag\n            translate([tag_w / 2, tag_h + hole_r * 1.5])\n                circle(r = hole_r * 1.8);\n        }\n        // Punch the hang hole\n        translate([tag_w / 2, tag_h + hole_r * 1.5])\n            circle(r = hole_r);\n    }\n}\n\n// Extrude the finished 2D shape into a solid tag.\nlinear_extrude(height = thickness)\n    key_tag_2d();\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-color",
      "title": "color()",
      "description": "Demonstrates color() with named CSS colors and an RGBA alpha value. Three spheres overlap: two use named colors and the third is translucent so you can see through it.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// color()\n\n/* color() sets the display colour of its child.\n   Use a named colour string: \"red\", \"steelblue\", \"gold\", etc.\n   Or pass [r, g, b, a] with values from 0 to 1; alpha < 1 = translucent.\n   Note: colour is a preview/render hint - it has no effect on STL export. */\n\nr   = 14;   // sphere radius (mm)\n$fn = 40;\n\n// Named colour - solid red\ncolor(\"crimson\")\n    translate([-r, 0, r])\n        sphere(r = r);\n\n// Named colour - solid teal\ncolor(\"steelblue\")\n    translate([r, 0, r])\n        sphere(r = r);\n\n// RGBA - semi-transparent gold sphere overlapping both\ncolor([1, 0.84, 0, 0.4])\n    translate([0, 0, r * 1.8])\n        sphere(r = r);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-cube",
      "title": "cube()",
      "description": "Shows the two common forms of cube(): an uncentered one sitting on the origin and a centered one floating above it. Tune size and spacing to explore the difference.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// cube()\n\n/* cube() - uncentered vs centered.\n   The uncentered cube grows into +X +Y +Z from the origin.\n   With center=true the cube straddles the origin on all three axes. */\n\nside    = 20;   // cube side length (mm)\ngap     = 8;    // space between the two cubes\nlift    = 30;   // how high the centered cube floats above the floor\n\n// Uncentered cube: corner at origin, fills +X +Y +Z quadrant\ncube([side, side, side], center = false);\n\n// Centered cube: placed above so both are visible in the same view\ntranslate([0, 0, lift])\n    cube([side, side, side], center = true);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-cylinder-cone",
      "title": "cylinder() and cones",
      "description": "Shows three forms of cylinder() standing side by side: a plain cylinder, a sharp cone (r2=0), and a truncated cone (r1 not equal r2). Tune the radii and height freely.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// cylinder() and cones\n\n/* cylinder() supports r/d for uniform radius and r1,r2 (or d1,d2)\n   for tapered shapes.  r2=0 makes a pointed cone. */\n\nh    = 30;   // height of all three shapes (mm)\nr1   = 12;   // base radius\nr2   = 6;    // top radius for the truncated cone\ngap  = 10;   // spacing between shapes\n$fn  = 40;\n\n// Plain cylinder - equal top and bottom radius\ntranslate([-r1 * 2 - gap, 0, 0])\n    cylinder(h = h, r = r1);\n\n// Sharp cone - top radius shrinks to zero\ncylinder(h = h, r1 = r1, r2 = 0);\n\n// Truncated cone - different radii at each end\ntranslate([r1 * 2 + gap, 0, 0])\n    cylinder(h = h, r1 = r1, r2 = r2);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-difference",
      "title": "difference()",
      "description": "Shows how difference() carves one shape out of another. A cylinder is subtracted from a solid cube to create a round through-hole - the foundation of almost every functional part.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// difference()\n\n/* difference() example - subtracting a cylinder from a cube. All dimensions in mm. */\nbox_w    = 50;   // cube width (X)\nbox_d    = 50;   // cube depth (Y)\nbox_h    = 20;   // cube height (Z)\nhole_d   = 18;   // diameter of the hole\nwall_min = 5;    // minimum wall thickness around the hole (informational)\n$fn = 48;\n\n// difference(): the FIRST child is the base solid;\n// all remaining children are subtracted from it.\ndifference() {\n    // Base solid - a plain cube\n    cube([box_w, box_d, box_h]);\n\n    // The cylinder to remove - centred in XY, reaching full depth (+1 mm each end avoids z-fighting)\n    translate([box_w / 2, box_d / 2, -1])\n        cylinder(d = hole_d, h = box_h + 2);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-difference-array",
      "title": "difference() with a hole pattern",
      "description": "Shows how difference() combined with a for loop removes a grid of holes from a flat plate. Tune the grid spacing and hole size to create your own custom perforation pattern.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// difference() with a hole pattern\n\n/* difference() with a for-loop hole grid. All dimensions in mm. */\nplate_w  = 80;   // plate width  (X)\nplate_d  = 60;   // plate depth  (Y)\nplate_h  = 4;    // plate thickness\nmargin   = 10;   // clear border around the hole grid\nspacing  = 14;   // centre-to-centre distance between holes\nhole_d   = 7;    // diameter of each hole\n$fn = 32;\n\ndifference() {\n    // Base plate\n    cube([plate_w, plate_d, plate_h]);\n\n    // Grid of holes: for loops generate a list of children for difference()\n    for (x = [margin : spacing : plate_w - margin],\n         y = [margin : spacing : plate_d - margin])\n        translate([x, y, -1])\n            cylinder(d = hole_d, h = plate_h + 2);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-extrude-polygon-comp",
      "title": "extrude a computed polygon",
      "description": "Shows how a list comprehension builds the [x, y] points for a wavy disc outline, which polygon() then assembles and linear_extrude() lifts into a printable coaster or badge.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// extrude a computed polygon\n\n/* Computed polygon via list comprehension - wavy disc coaster or badge. */\nbase_r   = 35;   // average radius of the disc\nwaves    = 12;   // number of wave peaks around the edge\nwave_amp =  5;   // amplitude of each wave (mm)\nheight   =  4;   // extrusion height\nsteps    = 120;  // polygon resolution (points around the circle)\n$fn = 64;\n\n// List comprehension builds one [x, y] point per step.\n// The radius oscillates between (base_r - wave_amp) and (base_r + wave_amp).\nwavy_pts = [\n    for (i = [0 : steps - 1])\n        let(\n            angle = 360 * i / steps,\n            r     = base_r + wave_amp * sin(waves * angle)\n        )\n        [r * cos(angle), r * sin(angle)]\n];\n\n// polygon() accepts our computed points directly.\nlinear_extrude(height = height)\n    polygon(points = wavy_pts);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-fn-fa-fs",
      "title": "$fn, $fa, $fs",
      "description": "Shows three cylinders of the same size rendered with different facet controls - coarse explicit $fn, fine explicit $fn, and angle-driven $fa - so you can see exactly how each setting shapes round geometry.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// $fn, $fa, $fs\n\n/* OpenSCAD uses three special variables to control curve resolution:\n     $fn - fixed segment count (overrides $fa and $fs when > 0)\n     $fa - minimum angle per segment in degrees (default 12)\n     $fs - minimum segment length in mm (default 2)\n   When $fn = 0 OpenSCAD picks the segment count from $fa and $fs.\n   A larger $fn or smaller $fa/$fs = smoother curves = slower render. */\n\nd  = 30;   // cylinder diameter (mm)\nh  = 20;   // cylinder height (mm)\nsp = 50;   // spacing between cylinders\n\n// Coarse: only 6 segments - very angular, renders instantly\ncolor(\"tomato\")\n    cylinder(h = h, d = d, $fn = 6);\n\n// Fine: 48 segments - nearly smooth, good for most prints\ntranslate([sp, 0, 0])\n    color(\"steelblue\")\n        cylinder(h = h, d = d, $fn = 48);\n\n// Auto from $fa: 10 degrees per segment = 36 segments for a full circle\n// ($fs = 1 keeps the segment limit from kicking in for this diameter)\ntranslate([sp * 2, 0, 0])\n    color(\"gold\")\n        cylinder(h = h, d = d, $fn = 0, $fa = 10, $fs = 1);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-for-loop",
      "title": "for loop",
      "description": "A row of cylindrical pillars that grow taller from left to right, showing how a for loop iterates over a range and how loop variable drives both position and size.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// for loop\n\n/* A row of pillars of increasing height - demonstrating the for loop.\n   Each pillar index i controls both the x position and the height.\n   Great first example for anyone learning parametric loops. */\n\npillar_count  = 8;      // number of pillars in the row\npillar_d      = 8;      // pillar diameter (mm)\nspacing       = 14;     // centre-to-centre spacing (mm)\nmin_height    = 10;     // shortest pillar height (mm)\nheight_step   = 8;      // how much taller each successive pillar gets (mm)\n$fn = 32;\n\nfor (i = [0 : pillar_count - 1]) {\n    h = min_height + i * height_step;   // height grows with each step\n    translate([i * spacing, 0, 0])\n        cylinder(d = pillar_d, h = h);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-for-ring",
      "title": "for + rotate ring",
      "description": "A ring of evenly spaced round pegs arranged in a circle using a for loop combined with rotate. Tune the number of pegs, ring radius, and peg size to explore radial symmetry.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// for + rotate ring\n\n/* Ring of pegs - demonstrating for + rotate to place objects in a circle.\n   The loop variable a is the angle step, derived from the total peg count.\n   Rotating a translated peg sweeps it around the origin. */\n\npeg_count  = 10;   // how many pegs in the ring\nring_r     = 30;   // radius from centre to peg centre (mm)\npeg_d      = 7;    // peg diameter (mm)\npeg_h      = 18;   // peg height (mm)\nbase_h     = 3;    // thin base disk height (mm)\nbase_d     = ring_r * 2 + peg_d + 4;  // base diameter sized to hold all pegs\n$fn = 40;\n\n// flat base disk so the part is printable without support\ncylinder(d = base_d, h = base_h);\n\n// place one peg per iteration, rotating it around the Z axis\nfor (a = [0 : 360 / peg_count : 359]) {\n    rotate([0, 0, a])\n        translate([ring_r, 0, base_h])\n            cylinder(d = peg_d, h = peg_h);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-function",
      "title": "defining a function",
      "description": "A decorative arch whose keystone positions are computed by a user-defined function that evaluates a parametric circle. Shows how functions return values that drive geometry, keeping the math out of the module calls.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// defining a function\n\n/* Arch with mathematically placed keystones - demonstrating user functions.\n   arch_pt(angle, r) returns [x, y] on a circle of radius r.\n   The for loop calls it to position each voussoir (arch block) correctly. */\n\narch_r      = 40;    // arch inner radius (mm)\narch_thick  = 10;    // radial thickness of each keystone block (mm)\nblock_span  = 22;    // angular span of each block (degrees)\nnum_blocks  = 7;     // number of arch keystones (use an odd number for symmetry)\nextrude_d   = 8;     // depth of the arch (mm)\n$fn = 48;\n\n// user function: a point on a circle of radius r at the given angle (degrees)\nfunction arch_pt(angle, r) = [r * cos(angle), r * sin(angle)];\n\n// place each keystone as a hull between two arc segments\nfor (i = [0 : num_blocks - 1]) {\n    // centre angle of this block, spread across 180 degrees\n    a_mid = 180 * (i + 0.5) / num_blocks;\n    a0    = a_mid - block_span / 2;\n    a1    = a_mid + block_span / 2;\n\n    inner = arch_r;\n    outer = arch_r + arch_thick;\n\n    linear_extrude(height = extrude_d)\n        polygon([\n            arch_pt(a0, inner),\n            arch_pt(a1, inner),\n            arch_pt(a1, outer),\n            arch_pt(a0, outer)\n        ]);\n}\n\n// two rectangular piers at the base\ntranslate([-arch_r - arch_thick, -arch_thick * 1.5, 0])\n    cube([arch_thick, arch_thick * 1.5, extrude_d]);\ntranslate([arch_r, -arch_thick * 1.5, 0])\n    cube([arch_thick, arch_thick * 1.5, extrude_d]);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-hull-2d",
      "title": "hull() in 2D",
      "description": "Shows how hull() wraps a convex skin around a set of 2D circles to create a smooth rounded slot outline. The result is extruded into a printable plate with a slotted hole.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// hull() in 2D\n\n/* hull() in 2D - rounded slot plate. All dimensions in mm. */\nplate_w    = 80;   // plate overall width\nplate_d    = 50;   // plate overall depth\nplate_h    = 4;    // plate thickness\nslot_len   = 40;   // centre-to-centre length of the slot\nslot_r     = 6;    // slot end radius (half the slot width)\ncorner_r   = 4;    // plate corner rounding radius\n$fn = 48;\n\nmodule rounded_plate(w, d, h, r) {\n    // A plate with rounded corners using hull() around four corner cylinders\n    linear_extrude(h)\n        hull() for (x = [r, w - r], y = [r, d - r])\n            translate([x, y]) circle(r = r);\n}\n\nmodule slot_2d(len, r) {\n    // A rounded slot: hull() of two circles gives a stadium / discorectangle\n    hull() {\n        translate([-len / 2, 0]) circle(r = r);\n        translate([ len / 2, 0]) circle(r = r);\n    }\n}\n\ndifference() {\n    rounded_plate(plate_w, plate_d, plate_h, corner_r);\n\n    // Slot centred in the plate, punched through with +1 mm clearance\n    translate([plate_w / 2, plate_d / 2, -1])\n        linear_extrude(plate_h + 2)\n            slot_2d(slot_len, slot_r);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-hull-3d",
      "title": "hull() in 3D",
      "description": "Shows how hull() wraps a convex skin around spheres placed at the corners of a box to produce a solid with smoothly rounded edges and corners. Tune the box size and corner radius freely.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// hull() in 3D\n\n/* hull() in 3D - a box with fully rounded corners and edges. All dimensions in mm. */\nbox_w  = 60;   // outer width  (X)\nbox_d  = 40;   // outer depth  (Y)\nbox_h  = 25;   // outer height (Z)\nr      = 5;    // corner sphere radius (determines how round the edges are)\n$fn = 40;\n\n// Place one sphere at each of the 8 corners of the (r-inset) bounding box.\n// hull() wraps them all - the result is a box where every edge and corner\n// is a smooth arc of radius r.\nhull() for (x = [r, box_w - r], y = [r, box_d - r], z = [r, box_h - r])\n    translate([x, y, z]) sphere(r = r);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-intersection",
      "title": "intersection()",
      "description": "Shows how intersection() keeps only the volume shared by all children. A cube and a sphere overlap to produce a rounded cube-corner gem shape - great for learning Boolean logic.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// intersection()\n\n/* intersection() example - only the shared volume survives. All dimensions in mm. */\ncube_size  = 45;   // side length of the cube\nsphere_d   = 55;   // diameter of the sphere (should be larger than cube for a visible effect)\n$fn = 64;\n\n// intersection() keeps only geometry that is INSIDE every child at once.\n// The result here is a cube with every face pushed inward by the sphere surface.\nintersection() {\n    // A centred cube\n    translate([-cube_size / 2, -cube_size / 2, -cube_size / 2])\n        cube([cube_size, cube_size, cube_size]);\n\n    // A sphere centred at the same origin - rounds all eight corners of the cube\n    sphere(d = sphere_d);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-intersection-for",
      "title": "intersection_for()",
      "description": "A faceted gem-like solid created by intersecting several rotated slabs using intersection_for(). Each iteration rotates a flat slab by an equal angle step, and the intersection keeps only the shared core.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// intersection_for()\n\n/* Faceted gem via intersection_for() - each rotated slab clips the result.\n   intersection_for() is like a for loop but AND-s every child together\n   instead of union-ing them, producing the overlapping core region. */\n\nslab_count = 7;     // number of rotated slabs (more = rounder gem)\nslab_w     = 50;    // slab width (mm)\nslab_d     = 50;    // slab depth (mm)\nslab_h     = 14;    // slab thickness - controls how \"tall\" the gem is (mm)\n$fn = 32;\n\nintersection_for (i = [0 : slab_count - 1]) {\n    angle = i * (180 / slab_count);   // spread slabs evenly through 180 degrees\n    rotate([0, angle, 0])\n        cube([slab_w, slab_d, slab_h], center = true);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-let-assign",
      "title": "let() and assignment",
      "description": "A set of nested hexagonal tiles whose size and position are computed with let() expressions, showing how to name intermediate values inside a comprehension without polluting the outer scope.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// let() and assignment\n\n/* Hexagonal tile grid using let() to name intermediate values.\n   let(var = expr, ...) inside a list comprehension keeps each calculation\n   readable and avoids repeating the same sub-expression multiple times. */\n\ntile_count  = 4;     // tiles per row and column\nhex_r       = 10;    // hexagon circumradius (mm)\ngap         = 2;     // gap between tiles (mm)\ntile_h      = 3;     // extrude height of each tile (mm)\n$fn = 6;             // 6 sides makes cylinder() a hexagonal prism\n\n// generate [x, y, scale] for each tile using let() inside the comprehension\ntiles = [\n    for (c = [0 : tile_count - 1], r = [0 : tile_count - 1])\n        let(\n            col_offset = (r % 2 == 0) ? 0 : hex_r * 0.866,   // offset odd rows for hex packing\n            x          = c * (hex_r * 1.732 + gap) + col_offset,\n            y          = r * (hex_r * 1.5   + gap),\n            scale_f    = 0.7 + 0.3 * ((c + r) % 3) / 2       // vary size for visual interest\n        )\n        [x, y, scale_f]\n];\n\n// place a hexagonal prism at each computed position\nfor (t = tiles) {\n    translate([t[0], t[1], 0])\n        cylinder(r = hex_r * t[2], h = tile_h);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-linear-extrude",
      "title": "linear_extrude()",
      "description": "Shows how linear_extrude() lifts a flat 2D star polygon straight up into a 3D prism. Tune the star size, point count, and height.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// linear_extrude()\n\n/* linear_extrude() - turns a 2D polygon into a 3D solid by pulling it upward. */\nouter_r  = 30;   // outer tip radius of the star\ninner_r  = 14;   // inner notch radius\npoints_n = 6;    // number of star points\nheight   = 20;   // extrusion height\n$fn = 64;\n\n// Build the star outline as a polygon from alternating outer/inner vertices.\nmodule star2d(n, ro, ri) {\n    step = 360 / n;\n    pts = [\n        for (i = [0 : n - 1])\n            for (pt = [\n                [ro * cos(i * step),          ro * sin(i * step)],\n                [ri * cos(i * step + step/2), ri * sin(i * step + step/2)]\n            ]) pt\n    ];\n    polygon(pts);\n}\n\n// Extrude the star upward - this is the key call being taught.\nlinear_extrude(height = height)\n    star2d(points_n, outer_r, inner_r);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-linear-extrude-scale",
      "title": "linear_extrude(scale=)",
      "description": "Shows how scale= in linear_extrude() shrinks the cross-section as it rises, turning a hexagonal base into a tapered obelisk. Tune base size, height, and tip scale.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// linear_extrude(scale=)\n\n/* linear_extrude(scale=) - shrinks (or grows) the cross-section from bottom to top. */\nbase_r   = 28;   // circumradius of the hexagonal base\nheight   = 50;   // total height of the obelisk\ntip_scale = 0.1; // fraction of the base size at the very top (0.0 = true point)\nsides    = 6;    // number of polygon sides\n$fn = 64;\n\n// A regular polygon at the base.\nmodule base_profile(r, n) {\n    circle(r = r, $fn = n);\n}\n\n// Extrude with scale - the cross-section tapers to tip_scale * original at the top.\nlinear_extrude(\n    height = height,\n    scale  = tip_scale   // key parameter: shrink top to a near-point\n)\n    base_profile(base_r, sides);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-linear-extrude-twist",
      "title": "linear_extrude(twist=)",
      "description": "Shows how adding a twist= angle to linear_extrude() spirals a square profile into a decorative twisted column. Adjust height, twist degrees, and profile size.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// linear_extrude(twist=)\n\n/* linear_extrude(twist=) - rotates the cross-section as it rises to create a spiral. */\ncol_size  = 16;   // side length of the square cross-section\ncol_height = 60;  // total column height\ntwist_deg = 120;  // total twist in degrees over the height\ntaper     = 0.85; // slight width reduction at top (scale factor)\n$fn = 48;\n\n// A square with notched corners for a more interesting silhouette.\nmodule cross_section(s) {\n    r = s * 0.12; // small corner radius expressed as square fraction\n    offset(r = r) offset(r = -r)\n        square([s, s], center = true);\n}\n\n// linear_extrude with twist - the teaching moment.\nlinear_extrude(\n    height = col_height,\n    twist  = twist_deg,\n    slices = 60,          // more slices = smoother spiral\n    scale  = taper\n)\n    cross_section(col_size);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-list-comp-if",
      "title": "list comprehension with condition",
      "description": "A sparse grid of pillars where each pillar is placed only when a mathematical condition holds, built with a list comprehension containing an if filter. Shows how to selectively include items in a generated list.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// list comprehension with condition\n\n/* Sparse pillar grid using a list comprehension with an if condition.\n   The comprehension produces only the [x, y] positions where the\n   sum of grid indices is NOT divisible by 3, giving a pattern with gaps. */\n\ncols     = 7;    // columns in the grid\nrows     = 7;    // rows in the grid\nspacing  = 14;   // centre-to-centre spacing (mm)\npillar_d = 7;    // pillar diameter (mm)\npillar_h = 20;   // pillar height (mm)\n$fn = 24;\n\n// build a list of [x, y] positions satisfying the condition\npositions = [\n    for (c = [0 : cols - 1], r = [0 : rows - 1])\n        if ((c + r) % 3 != 0)        // skip every position where c+r is a multiple of 3\n            [c * spacing, r * spacing]\n];\n\n// place a pillar at each position in the filtered list\nfor (pos = positions)\n    translate([pos[0], pos[1], 0])\n        cylinder(d = pillar_d, h = pillar_h);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-list-comprehension",
      "title": "list comprehension",
      "description": "A smooth wave-profile wall built by generating polygon outline points with a list comprehension and then extruding the result. Shows how to build point arrays mathematically rather than typing each coordinate by hand.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// list comprehension\n\n/* Wave-profile wall - demonstrating list comprehension to generate polygon points.\n   A list comprehension [ for (var = range) expression ] builds the point list\n   for a sine wave cross-section, which is then swept with linear_extrude. */\n\nsteps      = 60;    // number of polygon segments (more = smoother wave)\nwave_w     = 80;    // total width of one wave cycle (mm)\namplitude  = 8;     // wave peak-to-trough half-height (mm)\nmid_h      = 20;    // centre height of the wave profile (mm)\nwall_thick = 4;     // wall depth (extrude distance, mm)\n\n// build the closed polygon outline: top wave then bottom straight edge\nwave_pts = [\n    // top edge: sine wave from left to right\n    for (i = [0 : steps])\n        [i * wave_w / steps,\n         mid_h + amplitude * sin(i * 360 / steps)],\n    // bottom-right and bottom-left corners to close the shape\n    [wave_w, 0],\n    [0,      0]\n];\n\nlinear_extrude(height = wall_thick)\n    polygon(wave_pts);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-minkowski-2d",
      "title": "minkowski() in 2D",
      "description": "Shows how minkowski() of a 2D square with a circle inflates and rounds the outline uniformly. The resulting smooth 2D profile is extruded into a thick printable pad with softened edges.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// minkowski() in 2D\n\n/* minkowski() in 2D - a rounded rectangle profile extruded into a pad. All dimensions in mm. */\nsq_w  = 50;   // inner rectangle width  (final outer = sq_w + 2*r)\nsq_d  = 30;   // inner rectangle depth\nr     = 6;    // fillet radius - the circle radius used in minkowski\npad_h = 8;    // extrusion height of the finished pad\n$fn = 48;\n\n// minkowski() in 2D: sweeping a square through a circle inflates every edge outward\n// by r and turns every sharp corner into a circular arc - identical to offset(r=r)\n// but expressed as a Minkowski sum, which generalises to non-circular sweepers.\nlinear_extrude(height = pad_h)\n    minkowski() {\n        square([sq_w, sq_d]);   // base 2D shape\n        circle(r = r);          // circle adds the rounded offset\n    }\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-minkowski-round",
      "title": "minkowski() rounding",
      "description": "Shows how minkowski() of a box with a small sphere rounds every edge and corner simultaneously. The technique generalises to any shape. Note that the overall size grows by the sphere radius on each side.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// minkowski() rounding\n\n/* minkowski() rounding - a box with all edges and corners rounded. All dimensions in mm. */\nbox_w  = 50;   // inner box width  before rounding (final = box_w + 2*r)\nbox_d  = 35;   // inner box depth  before rounding\nbox_h  = 20;   // inner box height before rounding\nr      = 4;    // rounding radius applied to ALL edges and corners\n$fn = 32;\n\n// minkowski() sweeps every point of the first child through every point of the second.\n// A box swept through a sphere produces a box whose corners become spherical and\n// whose edges become cylindrical - a perfect all-direction fillet.\nminkowski() {\n    cube([box_w, box_d, box_h]);   // base shape (un-rounded)\n    sphere(r = r);                 // rounding element - radius = fillet size\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-mirror",
      "title": "mirror()",
      "description": "Creates an asymmetric L-shaped bracket and then reflects it across the Y-Z plane using mirror(), producing a matching left-hand part from a single definition.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// mirror()\n\n/* mirror([nx, ny, nz]) reflects geometry through the plane whose\n   normal is [nx, ny, nz].  mirror([1,0,0]) reflects across the Y-Z\n   plane, turning a right-hand part into a left-hand part. */\n\nthick  = 5;    // material thickness (mm)\nfoot_w = 30;   // horizontal leg length\nfoot_h = 8;    // horizontal leg height\nwall_h = 25;   // vertical leg height\ngap    = 6;    // gap between original and mirrored copy\n\n// Helper module - asymmetric L-bracket\nmodule l_bracket() {\n    // Horizontal foot\n    cube([foot_w, thick, foot_h]);\n    // Vertical wall at the left end of the foot\n    cube([thick, thick, wall_h]);\n}\n\n// Original bracket\ncolor(\"steelblue\")\n    l_bracket();\n\n// Mirrored bracket - reflected across X=0 plane, then shifted right\ntranslate([gap, 0, 0])\n    color(\"tomato\")\n        mirror([1, 0, 0])\n            l_bracket();\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-module-basic",
      "title": "defining a module",
      "description": "A parametric rounded peg module defined once and called multiple times with different sizes. Shows how modules encapsulate reusable geometry so you can place many variations without repeating code.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// defining a module\n\n/* Rounded peg module - demonstrating module definition and reuse.\n   The module takes diameter, height, and tip-rounding as parameters.\n   Calling it several times with different values shows why modules matter. */\n\n$fn = 40;\n\n// --- module definition ---\nmodule rounded_peg(d, h, tip_r = 2) {\n    // cylindrical body\n    cylinder(d = d, h = h - tip_r);\n    // rounded dome on top\n    translate([0, 0, h - tip_r])\n        sphere(r = d / 2);\n}\n\n// --- place several pegs in a row with varying dimensions ---\nrounded_peg(d = 8,  h = 20, tip_r = 4);\n\ntranslate([14, 0, 0])\n    rounded_peg(d = 10, h = 30, tip_r = 5);\n\ntranslate([28, 0, 0])\n    rounded_peg(d = 12, h = 15, tip_r = 3);\n\ntranslate([46, 0, 0])\n    rounded_peg(d = 6,  h = 25, tip_r = 3);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-module-default-args",
      "title": "module default args",
      "description": "A shelf bracket module with sensible default dimensions so it works with no arguments at all, but can be customised with named-argument calls. Demonstrates how default values reduce boilerplate and how named arguments improve readability.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// module default args\n\n/* Shelf bracket with default arguments - demonstrating named-argument calls.\n   Call bracket() for a standard bracket, or override any individual\n   dimension by name, e.g. bracket(arm = 60, thick = 4). */\n\nmodule bracket(arm = 50, rise = 40, thick = 3, gusset = true) {\n    // horizontal arm\n    cube([arm, thick, thick]);\n    // vertical rise\n    cube([thick, thick, rise]);\n    // diagonal gusset (only when requested)\n    if (gusset) {\n        rotate([0, 0, 0])\n        translate([thick, 0, thick])\n            rotate([0, 45, 0])\n                cube([rise * 0.7, thick, thick]);\n    }\n}\n\n// call with all defaults\nbracket();\n\n// call with a named override - longer arm, no gusset\ntranslate([0, 20, 0])\n    bracket(arm = 70, gusset = false);\n\n// call with several named overrides\ntranslate([0, 40, 0])\n    bracket(arm = 40, rise = 30, thick = 5, gusset = true);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-multmatrix",
      "title": "multmatrix() shear",
      "description": "Uses multmatrix() to apply a shear transform to a cube, leaning its top face in the X direction - a distortion that cannot be achieved with rotate() or scale() alone.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// multmatrix() shear\n\n/* multmatrix(m) applies a full 4x4 homogeneous matrix to its child.\n   This unlocks shear, which tilts one axis relative to another.\n   The matrix below shears X by a fraction of Z:\n     X' = X + shear * Z,  Y' = Y,  Z' = Z\n   So the top face slides sideways while the base stays put. */\n\nside  = 20;   // cube side length (mm)\nshear = 0.5;  // how many mm of X shift per mm of Z height\n\n// Shear matrix: identity with shear coefficient in row 0, col 2\nshear_matrix = [\n    [1, 0, shear, 0],\n    [0, 1, 0,     0],\n    [0, 0, 1,     0],\n    [0, 0, 0,     1]\n];\n\n// Reference cube - no transform\ncolor(\"silver\")\n    cube([side, side, side]);\n\n// Sheared cube placed to the right so both are visible\ntranslate([side * 2, 0, 0])\n    color(\"steelblue\")\n        multmatrix(shear_matrix)\n            cube([side, side, side]);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-nested-for",
      "title": "nested for",
      "description": "A checkerboard grid of short square pillars built with two nested for loops, one for columns and one for rows. Shows how to iterate over two axes at once and skip alternating cells.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// nested for\n\n/* Checkerboard grid of pillars - demonstrating nested for loops.\n   The outer loop walks columns (x), the inner loop walks rows (y).\n   A modulo test skips every other cell to create the checkerboard pattern. */\n\ncols     = 6;    // number of columns\nrows     = 6;    // number of rows\ncell     = 14;   // grid cell size (mm)\npillar_w = 9;    // pillar side length (mm)\npillar_h = 12;   // pillar height (mm)\n\nfor (c = [0 : cols - 1]) {\n    for (r = [0 : rows - 1]) {\n        // place a pillar only on \"white\" squares of the checkerboard\n        if ((c + r) % 2 == 0) {\n            translate([c * cell, r * cell, 0])\n                cube([pillar_w, pillar_w, pillar_h]);\n        }\n    }\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-offset-delta",
      "title": "offset() delta + chamfer",
      "description": "Shows how offset(delta=, chamfer=true) shifts a 2D outline by a fixed amount and adds sharp 45-degree chamfers at corners instead of arcs. An outer and inset shell are stacked to show the contrast.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// offset() delta + chamfer\n\n/* offset(delta=, chamfer=) - sharp inset and outset chamfered outlines. All dimensions in mm. */\nbase_w  = 60;   // base rectangle width\nbase_d  = 45;   // base rectangle depth\ndelta   = 5;    // offset amount (positive = outward, negative = inward)\nh_outer = 6;    // height of outer extruded ring\nh_inner = 10;   // height of inner extruded post\n$fn = 32;\n\nmodule base_2d() {\n    // A simple rectangle - the shape we will offset\n    square([base_w, base_d]);\n}\n\n// --- Outer chamfered ring ---\n// offset(delta=+d, chamfer=true) grows the outline and chamfers convex corners at 45 deg.\n// Subtracting the original leaves only the chamfered border ring.\nlinear_extrude(height = h_outer)\n    difference() {\n        offset(delta = delta, chamfer = true) base_2d();\n        base_2d();\n    }\n\n// --- Inner chamfered post ---\n// offset(delta=-d, chamfer=true) shrinks the outline inward with chamfered corners.\ntranslate([0, 0, h_outer])\n    linear_extrude(height = h_inner)\n        offset(delta = -delta, chamfer = true) base_2d();\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-offset-round",
      "title": "offset() rounded",
      "description": "Shows how offset(r=) inflates a 2D polygon outward and smoothly rounds every concave and convex corner. An L-shaped profile is offset then extruded into a printable bracket tile.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// offset() rounded\n\n/* offset(r=) rounding - an L-shape inflated with smooth corners. All dimensions in mm. */\narm_w  = 12;   // width of each arm of the L\narm_l  = 40;   // length of each arm\nr      = 4;    // rounding radius (positive = outward + rounded corners)\nh      = 6;    // extrusion height\n$fn = 48;\n\n// The raw L-shape as a polygon (two rectangles forming an L)\nmodule l_shape_2d(aw, al) {\n    polygon([\n        [0,  0],\n        [al, 0],\n        [al, aw],\n        [aw, aw],\n        [aw, al],\n        [0,  al]\n    ]);\n}\n\n// offset(r=) with a positive radius rounds ALL corners (concave ones become fillets,\n// convex ones become circular arcs) and inflates the outline by r.\nlinear_extrude(height = h)\n    offset(r = r)\n        l_shape_2d(arm_w, arm_l);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-polygon-path",
      "title": "polygon() from points",
      "description": "Shows how polygon() takes an explicit list of [x, y] points to define any custom shape - here a bold right-pointing arrow - then extrudes it into a printable marker.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// polygon() from points\n\n/* polygon() with explicit points - a bold arrow head extruded into a flat marker. */\narrow_w   = 60;   // total width (left edge to arrow tip)\narrow_h   = 40;   // total height (top to bottom of the shaft + head)\nshaft_h   = 16;   // height of the rectangular shaft\nhead_inset = 20;  // how far the arrowhead base is from the left edge\nthickness  =  5;  // extrusion depth\n\n// Explicit point list that traces the arrow outline counter-clockwise.\n// The shape: a rectangle shaft on the left + a triangle head on the right.\narrow_pts = [\n    [0,           (arrow_h - shaft_h) / 2],          // shaft bottom-left\n    [head_inset,  (arrow_h - shaft_h) / 2],          // shaft bottom-right / head base-lower\n    [head_inset,  0],                                 // head bottom corner\n    [arrow_w,     arrow_h / 2],                       // arrow tip\n    [head_inset,  arrow_h],                           // head top corner\n    [head_inset,  (arrow_h + shaft_h) / 2],           // head base-upper\n    [0,           (arrow_h + shaft_h) / 2]            // shaft top-left\n];\n\n// polygon() - the focus of this recipe.\nlinear_extrude(height = thickness)\n    polygon(points = arrow_pts);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-polyhedron",
      "title": "polyhedron()",
      "description": "Builds a square pyramid by hand using polyhedron() with a list of 5 corner points and 5 faces. A great introduction to defining arbitrary closed solids vertex by vertex.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// polyhedron()\n\n/* polyhedron() - square pyramid from scratch.\n   Points are listed as [x, y, z] coordinates.\n   Each face is a list of point indices ordered so the outward\n   normal follows the right-hand rule (counter-clockwise when\n   viewed from outside). */\n\nbase = 30;   // square base side length (mm)\napex = 25;   // pyramid height (mm)\n\nhalf = base / 2;\n\n// 5 vertices: 4 base corners + 1 apex\npts = [\n    [-half, -half, 0],   // 0 - front-left\n    [ half, -half, 0],   // 1 - front-right\n    [ half,  half, 0],   // 2 - back-right\n    [-half,  half, 0],   // 3 - back-left\n    [    0,     0, apex] // 4 - apex\n];\n\n// 5 faces: 1 square base (split into 2 triangles) + 4 triangular sides\nfaces = [\n    [0, 3, 2, 1],   // base (winding viewed from below)\n    [0, 1, 4],      // front face\n    [1, 2, 4],      // right face\n    [2, 3, 4],      // back face\n    [3, 0, 4]       // left face\n];\n\npolyhedron(points = pts, faces = faces);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-projection",
      "title": "projection()",
      "description": "Shows how projection() collapses a 3D object down to a flat 2D silhouette in the XY plane, which can then be re-extruded into a thin shadow-stamp. Tune the source shape and stamp thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// projection()\n\n/* projection() - flatten a 3D solid to a 2D outline, then re-extrude as a stamp. */\nstamp_thickness = 2.5;  // how thick the re-extruded stamp plate is\ncone_r   = 20;          // base radius of the source cone\ncone_h   = 30;          // height of the source cone\n$fn = 48;\n\n// The 3D shape whose silhouette we want.\n// We use it only as input to projection(); it is NOT rendered itself.\nmodule source_shape() {\n    // A cone with a small sphere on top - interesting shadow outline.\n    cylinder(h = cone_h, r1 = cone_r, r2 = 4);\n    translate([0, 0, cone_h]) sphere(r = 6);\n}\n\n// projection(cut = false) casts a shadow of the 3D shape down onto Z = 0.\n// linear_extrude then lifts that 2D footprint into a flat stamp.\nlinear_extrude(height = stamp_thickness)\n    projection(cut = false)   // false = true silhouette (not a cross-section cut)\n        source_shape();\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-range-step",
      "title": "ranges [a:step:b]",
      "description": "A staircase of rectangular steps generated with a stepped range loop. Each iteration advances by the step size in both height and depth, showing how the three-argument range controls spacing between loop values.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// ranges [a:step:b]\n\n/* Staircase built with a stepped range - demonstrating [start : step : end].\n   The loop variable z also drives the y position so each step sits on top\n   of the previous one, forming a proper rising stair. */\n\nstep_count  = 8;     // total number of steps\nstep_rise   = 10;    // vertical rise per step (mm)\nstep_run    = 15;    // horizontal depth per step (mm)\nstep_width  = 40;    // width of the staircase (mm)\n\n// [0 : step_rise : (step_count-1)*step_rise] steps by step_rise each iteration\nfor (z = [0 : step_rise : (step_count - 1) * step_rise]) {\n    step_index = z / step_rise;           // which step number we are on\n    translate([0, step_index * step_run, 0])\n        cube([step_width, step_run, z + step_rise]);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-recursion-tree",
      "title": "recursive module",
      "description": "A branching fractal tree built with a recursive module. Each call draws one branch segment, then calls itself twice at a smaller scale and angle. The 2D tree outline is extruded to a thin slab so it is printable.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// recursive module\n\n/* Branching tree - demonstrating a recursive module.\n   branch() calls itself twice with smaller dimensions and opposite angles,\n   stopping when the branch becomes shorter than min_len.\n   A 2D shape is built and then extruded once at the top level. */\n\ntrunk_len   = 30;    // length of the main trunk (mm)\ntrunk_w     = 5;     // width of the main trunk (mm)\ndepth       = 3;     // extrude depth for the printable plaque (mm)\nsplit_angle = 28;    // angle each child branch fans outward (degrees)\nshrink      = 0.65;  // child branch length as a fraction of parent length\nmin_len     = 4;     // stop recursing when branch length drops below this (mm)\n\n// 2D recursive branch - uses square() so it works inside linear_extrude\nmodule branch2d(len, w) {\n    square([w, len]);\n    if (len * shrink > min_len) {\n        child_len = len * shrink;\n        child_w   = max(1, w * shrink);\n        // left child\n        translate([w / 2, len, 0])\n            rotate(split_angle)\n                translate([-child_w / 2, 0, 0])\n                    branch2d(child_len, child_w);\n        // right child\n        translate([w / 2, len, 0])\n            rotate(-split_angle)\n                translate([-child_w / 2, 0, 0])\n                    branch2d(child_len, child_w);\n    }\n}\n\nlinear_extrude(height = depth)\n    translate([-trunk_w / 2, 0])\n        branch2d(trunk_len, trunk_w);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-render-modifier",
      "title": "#, %, ! modifiers note",
      "description": "Demonstrates a solid composed part (a box with a lid) alongside a semi-transparent reference bounding volume drawn with the % modifier. The final render is always the solid geometry only.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// #, %, ! modifiers note\n\n/* Modifier demo - % shows a ghost reference box; the real model renders solid.\n   OpenSCAD modifiers:\n     %  background (grey ghost, not in final render or export)\n     #  highlight   (shown in pink, IS included in geometry)\n     !  root        (render ONLY this child, ignoring everything else)\n     *  disable     (ignore this child entirely)\n   Only % is used here so the part stays clean and printable.\n   All dimensions in mm. */\nbox_w   = 50;   // main box width\nbox_d   = 35;   // main box depth\nbox_h   = 30;   // main box height\nwall    = 2.5;  // wall thickness\nlid_h   = 8;    // lid height\ngap     = 0.3;  // clearance between box lip and lid\n$fn = 32;\n\n// --- Hollow box body ---\nmodule box_body() {\n    difference() {\n        cube([box_w, box_d, box_h]);\n        translate([wall, wall, wall])\n            cube([box_w - 2*wall, box_d - 2*wall, box_h]);  // open top\n    }\n}\n\n// --- Snap-fit lid ---\nmodule lid() {\n    difference() {\n        cube([box_w, box_d, lid_h]);\n        // Inner pocket that drops over the box lip\n        translate([wall + gap, wall + gap, wall])\n            cube([box_w - 2*(wall+gap), box_d - 2*(wall+gap), lid_h]);\n    }\n}\n\n// Real geometry - box body and lid sitting side by side\nbox_body();\ntranslate([box_w + 10, 0, 0]) lid();\n\n// % ghost - a transparent bounding envelope showing the assembled footprint.\n// This is NOT exported or printed; it is a visual reference only.\n%translate([-2, -2, 0])\n    cube([box_w * 2 + 10 + 4, box_d + 4, box_h + 4]);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-rotate",
      "title": "rotate()",
      "description": "Shows three elongated bars, each tipped 45 degrees around a different axis using rotate(), so the effect of rotating around X, Y, and Z is immediately visible.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// rotate()\n\n/* rotate([x_deg, y_deg, z_deg]) spins its child around the world axes.\n   Each bar below is the same shape but rotated about one axis only.\n   Positive angles follow the right-hand rule. */\n\nbar_w  = 6;    // bar cross-section (mm)\nbar_l  = 40;   // bar length (mm)\nangle  = 45;   // rotation angle to apply (degrees)\nspread = 60;   // spacing between the three examples\n\n// Unrotated reference bar (grey in color-aware viewers)\ncolor(\"silver\")\n    cube([bar_w, bar_w, bar_l]);\n\n// Rotated 45 degrees around X - bar tilts in the Y-Z plane\ntranslate([spread, 0, 0])\n    rotate([angle, 0, 0])\n        cube([bar_w, bar_w, bar_l]);\n\n// Rotated 45 degrees around Y - bar tilts in the X-Z plane\ntranslate([spread * 2, 0, 0])\n    rotate([0, angle, 0])\n        cube([bar_w, bar_w, bar_l]);\n\n// Rotated 45 degrees around Z - bar tips sideways in the X-Y plane\ntranslate([spread * 3, 0, 0])\n    rotate([0, 0, angle])\n        cube([bar_w, bar_w, bar_l]);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-rotate-extrude",
      "title": "rotate_extrude()",
      "description": "Shows how rotate_extrude() sweeps a 2D profile all the way around the Z axis to make a solid of revolution - here a chunky ring with a D-shaped cross-section.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// rotate_extrude()\n\n/* rotate_extrude() - revolves a 2D profile 360 degrees around the Z axis. */\nring_r    = 24;  // distance from Z axis to centre of the cross-section\ncs_width  = 10;  // cross-section width (radial)\ncs_height = 14;  // cross-section height (axial)\ncorner_r  = 3;   // rounding on the cross-section corners\n$fn = 64;\n\n// A rounded rectangle cross-section sitting to the right of the Z axis.\n// Important: every point of the profile must have x >= 0 for rotate_extrude.\nmodule cross_section() {\n    translate([ring_r - cs_width / 2, -cs_height / 2, 0])\n        offset(r = corner_r) offset(r = -corner_r)\n            square([cs_width, cs_height]);\n}\n\n// rotate_extrude sweeps the profile all the way around - the key call.\nrotate_extrude($fn = 64)\n    cross_section();\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-rotate-extrude-angle",
      "title": "rotate_extrude(angle=)",
      "description": "Shows how the angle= parameter stops a rotate_extrude() part-way around, creating a C-shaped arc solid. Adjust the sweep angle and cross-section to explore partial revolves.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// rotate_extrude(angle=)\n\n/* rotate_extrude(angle=) - partial revolution; here a 270-degree C-channel arc. */\nsweep_angle = 270;  // degrees of revolution (try 180 for a half-pipe, 360 for a full ring)\nring_r      = 22;   // distance from Z axis to cross-section centre\ncs_w        = 9;    // cross-section width (radial direction)\ncs_h        = 12;   // cross-section height (axial direction)\n$fn = 64;\n\n// L-shaped channel cross-section - gives the arc structural interest.\nmodule channel_profile() {\n    wall = 2.0; // channel wall thickness\n    translate([ring_r - cs_w / 2, -cs_h / 2, 0]) {\n        // outer rectangle\n        square([cs_w, cs_h]);\n        // subtract the inner hollow to make a U-channel\n        translate([wall, wall, 0])\n            square([cs_w - wall, cs_h - wall]);\n    }\n}\n\n// Partial rotate_extrude - angle= is the feature being demonstrated.\nrotate_extrude(angle = sweep_angle, $fn = 64)\n    difference() {\n        translate([ring_r - cs_w / 2, -cs_h / 2, 0]) square([cs_w, cs_h]);\n        translate([ring_r - cs_w / 2 + 2, -cs_h / 2 + 2, 0]) square([cs_w - 2, cs_h - 2]);\n    }\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-scale-resize",
      "title": "scale() and resize()",
      "description": "Compares scale() and resize() applied to the same sphere. scale() multiplies the current size by a factor while resize() sets an exact target size in millimetres.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// scale() and resize()\n\n/* scale() multiplies coordinates by a factor vector [sx, sy, sz].\n   resize() sets the bounding box to an absolute [x, y, z] size.\n   Both can stretch non-uniformly - very useful for turning a sphere\n   into an ellipsoid without defining new geometry. */\n\nbase_r  = 12;   // radius of the original sphere (mm)\n$fn     = 40;\nspacing = 50;   // gap between examples\n\n// Original sphere for reference\ncolor(\"silver\")\n    sphere(r = base_r);\n\n// scale() - multiply each axis: 1x on X, 2x on Y, 0.5x on Z\ntranslate([spacing, 0, 0])\n    color(\"steelblue\")\n        scale([1, 2, 0.5])\n            sphere(r = base_r);\n\n// resize() - force the bounding box to exactly 50 x 20 x 30 mm\ntranslate([spacing * 2.5, 0, 0])\n    color(\"tomato\")\n        resize([50, 20, 30])\n            sphere(r = base_r);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-sphere",
      "title": "sphere()",
      "description": "Places two spheres of the same radius side by side to show how $fn controls smoothness - the left sphere is coarse and blocky while the right one is nearly smooth.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// sphere()\n\n/* sphere() - facet count comparison.\n   $fn sets the number of longitude AND latitude segments.\n   Low $fn = fast but faceted; high $fn = smooth but slower to render. */\n\nr   = 15;    // sphere radius (mm)\ngap = 10;    // gap between the two spheres\n\n// Coarse sphere - clearly faceted, renders instantly\ntranslate([-r - gap / 2, 0, r])\n    sphere(r = r, $fn = 8);\n\n// Smooth sphere - 48 segments, much rounder\ntranslate([ r + gap / 2, 0, r])\n    sphere(r = r, $fn = 48);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-text-2d",
      "title": "text() 2D",
      "description": "Shows how text() creates a 2D letter outline that linear_extrude() lifts into raised 3D lettering on a name-badge plate. Change the label, font size, and thickness to customise.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// text() 2D\n\n/* text() as 2D letters raised above a badge plate. */\nlabel      = \"OpenSCAD\";  // the text string (change me!)\nfont_size  = 11;           // character height in mm\nraise      =  3;           // how tall the raised letters are above the plate\nplate_w    = 90;           // badge plate width\nplate_d    = 28;           // badge plate depth\nplate_h    =  3;           // badge plate base thickness\n\n// Base plate - ensures non-empty geometry even if the font system is unavailable.\ncube([plate_w, plate_d, plate_h], center = true);\n\n// Raised lettering on top of the plate.\n// text() defines the 2D outline; linear_extrude lifts it above the plate surface.\ntranslate([0, 0, plate_h / 2])\n    linear_extrude(height = raise)\n        text(\n            label,\n            size   = font_size,\n            halign = \"center\",\n            valign = \"center\"\n        );\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-text-engrave",
      "title": "engraving text",
      "description": "Shows how to engrave text into a flat plate using difference() - the text shape is subtracted from the plate solid. Tune the plate size, engraving depth, and label.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// engraving text\n\n/* Engraving text into a plate with difference(). */\nplate_w   = 80;   // plate width\nplate_d   = 40;   // plate depth (Y)\nplate_h   =  6;   // plate total thickness\nengrave_d =  1.5; // how deep the text is cut in\nfont_size = 10;   // character height\nlabel     = \"MADE WITH\";  // first line of text\nlabel2    = \"OPENSCAD\";   // second line of text\nfont_name = \"Liberation Sans:style=Bold\";\n$fn = 32;\n\ndifference() {\n    // The solid plate\n    cube([plate_w, plate_d, plate_h], center = true);\n\n    // Engrave first line - translate up from plate surface by (plate_h/2 - engrave_d)\n    translate([0, 5, plate_h / 2 - engrave_d])\n        linear_extrude(height = engrave_d + 0.1)   // +0.1 ensures clean boolean cut\n            text(label,  size = font_size, font = font_name, halign = \"center\", valign = \"center\");\n\n    // Engrave second line below the first\n    translate([0, -8, plate_h / 2 - engrave_d])\n        linear_extrude(height = engrave_d + 0.1)\n            text(label2, size = font_size, font = font_name, halign = \"center\", valign = \"center\");\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-translate",
      "title": "translate()",
      "description": "Places four identical cubes at different positions using translate() to show how moving along X, Y, and Z works independently and in combination.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// translate()\n\n/* translate([x, y, z]) shifts its child geometry in space.\n   Here the same 15 mm cube appears at four different locations\n   so you can see how each axis affects placement. */\n\ns   = 15;   // cube side (mm)\ngap = 8;    // extra space between cubes\n\n// Reference cube at the origin\ncube([s, s, s]);\n\n// Shifted along +X\ntranslate([s + gap, 0, 0])\n    cube([s, s, s]);\n\n// Shifted along +Y\ntranslate([0, s + gap, 0])\n    cube([s, s, s]);\n\n// Shifted along +Z\ntranslate([0, 0, s + gap])\n    cube([s, s, s]);\n`);"
    },
    {
      "kind": "recipe",
      "id": "learn-union",
      "title": "union()",
      "description": "Shows how union() merges two overlapping solids into a single seamless body. Adjust the box size, cylinder radius, and overlap to explore how the shapes combine.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// union()\n\n/* union() example - two overlapping shapes become one solid. All dimensions in mm. */\nbox_w   = 40;   // box width (X)\nbox_d   = 30;   // box depth (Y)\nbox_h   = 20;   // box height (Z)\ncyl_r   = 12;   // cylinder radius\ncyl_h   = 35;   // cylinder height\noverlap = 10;   // how far the cylinder overlaps into the box\n$fn = 48;\n\n// union() is actually the default when you list children side by side,\n// but writing it explicitly makes the intent crystal-clear.\nunion() {\n    // A rounded-end horizontal bar\n    cube([box_w, box_d, box_h]);\n\n    // A vertical cylinder that overlaps the box - together they become one solid\n    translate([box_w - overlap, box_d / 2, 0])\n        cylinder(r = cyl_r, h = cyl_h);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "match-node-transform",
      "title": "Match a node exactly (t + r + s)",
      "description": "node.matchTransform(source) copies the source node's WORLD translate, rotate AND scale onto the target in a single undo step. It takes ONLY a source (a Node handle / name / {name|nodeId|id}) — it always copies all three components (no per-channel flags). Useful for stamping duplicates onto reference transforms.",
      "intent": "mesh",
      "editor": "model",
      "category": "snap-align",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "node.matchTransform",
        "node.translate",
        "node.rotate",
        "node.scale"
      ],
      "code": "// Self-contained: build a SOURCE node at a distinct transform and a TARGET\n// node at the origin, then copy the source's full transform onto the target.\n// matchTransform takes ONLY the source — it ALWAYS copies translate + rotate +\n// scale together (there are no per-channel option flags). Drive + verify BY\n// NAME, never by id.\n\n// 1) Source — a cube nudged + rotated + scaled to a distinctive pose.\nconst source = await create.cube({ name: 'Source' });\nsource.xform({ t: [2.5, 1.3, -0.7], r: [30, 45, 60], s: [1.5, 0.75, 2] });\nawait select(null, { mode: 'clear' });\n\n// 2) Target — a second cube left at the origin with identity transform.\nconst target = await create.cube({ name: 'Target' });\nawait select(null, { mode: 'clear' });\n\n// Snapshot BEFORE — target sits at origin / identity.\nconst before = {\n  t: target.translate.get(),\n  r: target.rotate.get(),\n  s: target.scale.get(),\n};\nconsole.log('Target BEFORE: ' + JSON.stringify(before));\n\n// 3) Copy the SOURCE's world transform onto the TARGET in one undoable step.\n//    Resolve the source by NAME to prove the name-based path works.\ntarget.matchTransform('Source'); // one Ctrl+Z reverts t, r AND s together\n\n// Snapshot AFTER — should now equal the source's transform.\nconst after = {\n  t: target.translate.get(),\n  r: target.rotate.get(),\n  s: target.scale.get(),\n};\nconst src = {\n  t: source.translate.get(),\n  r: source.rotate.get(),\n  s: source.scale.get(),\n};\nconsole.log('Target AFTER:  ' + JSON.stringify(after));\nconsole.log('Source:        ' + JSON.stringify(src));\n\n// Prove equality component-by-component (round to dodge float noise).\nconst round3 = (a) => a.map((v) => +v.toFixed(3));\nconst eq = (a, b) => JSON.stringify(round3(a)) === JSON.stringify(round3(b));\nconsole.log('matched t/r/s: ' + JSON.stringify({\n  translate: eq(after.t, src.t),\n  rotate: eq(after.r, src.r),\n  scale: eq(after.s, src.s),\n}));\n",
      "expectedOutput": "console shows Target TRS before (origin / identity) and after — after equals the Source's t+r+s; single Ctrl+Z reverts all three."
    },
    {
      "kind": "recipe",
      "id": "material-delete-unused",
      "title": "Delete unused materials",
      "description": "List every material in the asset and remove any that no mesh references.",
      "intent": "mesh",
      "editor": "model",
      "category": "materials-textures",
      "apiSurfaces": [
        "ModelEditor.listMaterials",
        "ModelEditor.getMaterial",
        "mesh.material.get",
        "material.delete"
      ],
      "code": "// Drop materials nothing references.\nconst referenced = new Set();\nfor (const m of ls({ type: 'mesh' })) {\n  const mat = m.material.get();\n  if (mat) referenced.add(mat.id);\n}\nlet removed = 0;\n// listMaterials() → { id, name } descriptors; getMaterial(id) → the deletable handle.\nfor (const { id } of listMaterials()) {\n  if (!referenced.has(id)) {\n    getMaterial(id).delete();\n    removed++;\n  }\n}\nconsole.log('removed', removed, 'unused materials');\n",
      "expectedOutput": "console line with removed-materials count."
    },
    {
      "kind": "recipe",
      "id": "material-duplicate-rough",
      "title": "Duplicate a material with a tweak",
      "description": "Take the selected mesh material, duplicate it under a new name, dial the roughness up, and assign the variant.",
      "intent": "mesh",
      "editor": "model",
      "category": "materials-textures",
      "apiSurfaces": [
        "mesh.material.get",
        "material.duplicate",
        "mesh.material.set"
      ],
      "code": "// Variant of an existing material: same albedo, rougher finish.\nconst sel = ls({selected: true})[0];\nif (!sel) { console.log('Select a mesh first.'); return; }\nconst base = sel.material.get();\nif (!base) { console.log('Mesh has no resolvable material.'); return; }\nconst rough = base.duplicate({ name: base.name + '-rough' });\nrough.roughness = Math.min(1, base.roughness + 0.4);\nsel.material.set(rough);\nconsole.log('switched', sel.name, 'to', rough.name);\n",
      "expectedOutput": "console line: \"switched <mesh name> to <new material name>\"; viewport: mesh material is the rougher variant."
    },
    {
      "kind": "recipe",
      "id": "material-emissive-blue",
      "title": "Make every mesh emissive blue",
      "description": "Walk every mesh in the scene and assign a flat unlit blue material. Quick way to silhouette a whole asset.",
      "intent": "mesh",
      "editor": "model",
      "category": "materials-textures",
      "apiSurfaces": [
        "ModelEditor.create.unlitMaterial",
        "ModelEditor.scene.ls",
        "mesh.material.set"
      ],
      "code": "// One unlit blue material applied to every mesh in the scene.\nconst blue = create.unlitMaterial({ name: 'flat-blue', baseColor: [0, 0.4, 1, 1] });\nlet count = 0;\nfor (const m of ls({ type: 'mesh' })) {\n  m.material.set(blue);\n  count++;\n}\nconsole.log('painted', count, 'meshes blue');\n",
      "expectedOutput": "console line with painted mesh count; viewport: every mesh renders flat blue."
    },
    {
      "kind": "recipe",
      "id": "material-lookup-delete",
      "title": "Get a material by name and delete it",
      "description": "Spawn a cube; create + assign a material; look it up by name; list every material; then delete the material (which also unassigns it).",
      "intent": "mesh",
      "editor": "model",
      "category": "materials-textures",
      "apiSurfaces": [
        "ModelEditor.getMaterial",
        "ModelEditor.listMaterials",
        "ModelEditor.create.cube",
        "ModelEditor.create.material",
        "Utils.wait.frame",
        "node.translate",
        "material.assignTo",
        "material.delete"
      ],
      "code": "// Look a material up by name, list every material, then delete it (which\n// also unassigns it from every mesh). Self-contained from an empty scene.\n\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'MatTarget' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nawait Utils.wait.frame();\n\n// getMaterial(idOrName) returns null when nothing matches.\nconst probe = getMaterial('nonexistent-material');\nconsole.log('getMaterial(\"nonexistent-material\"):', probe); // null\n\n// create.material is async — give the asset something to look up + delete.\nconst mat = await create.material({\n  name: 'scrap-mat',\n  baseColor: [0.2, 0.6, 0.9, 1], // [r,g,b,a] 0..1\n  metallic: 0.1,\n  roughness: 0.8,\n});\nawait mat.assignTo(cube); // bind so delete() has an assignment to clean up\n\n// listMaterials() enumerates every material on the asset.\nconsole.log('material count:', listMaterials().length);\n\n// Round-trip the lookup by NAME (customer-facing: prefer names over ids).\nconst looked = getMaterial('scrap-mat');\nconsole.log('roundtrip getMaterial by name:', looked?.name);\n\n// delete() removes the material AND unassigns it from every mesh.\nmat.delete();\nawait Utils.wait.frame();\n\n// The name no longer resolves.\nconst after = getMaterial('scrap-mat');\nconsole.log('post-delete getMaterial:', after); // null\nconsole.log('material count after delete:', listMaterials().length);\n",
      "expectedOutput": "console: null for a missing name, the looked-up material, the material count, then null after delete."
    },
    {
      "kind": "recipe",
      "id": "material-name-first-bind",
      "title": "Bind a material by name (no UUIDs)",
      "description": "Name-first material workflow: create by name, resolve by name with getMaterial, set it on a mesh, then broadcast it to several meshes by name. Never capture a materialId UUID.",
      "intent": "mesh",
      "editor": "model",
      "category": "materials-textures",
      "apiSurfaces": [
        "ModelEditor.create.material",
        "ModelEditor.getMaterial",
        "mesh.material.set",
        "ModelEditor.material.applyToAll"
      ],
      "code": "// Name-first material binding — work entirely by NAME, never by UUID.\nconst cube = await create.cube({ name: 'Cube' });\nconst sphere = await create.sphere({ name: 'Sphere' });\nawait create.material({ name: 'Steel', baseColor: [0.6, 0.6, 0.65, 1], metallic: 1, roughness: 0.3 });\n// Resolve by name and set on one mesh by handle.\nls('Cube')[0].material.set(getMaterial('Steel'));\n// Broadcast the same material to several meshes by NAME in one undoable step.\nawait material.applyToAll('Steel', ['Cube', 'Sphere']);\nconsole.log('bound', getMaterial('Steel').name, 'to Cube + Sphere');\n",
      "expectedOutput": "console line confirming the bound material; viewport: the named meshes all show the Steel material."
    },
    {
      "kind": "recipe",
      "id": "material-red-metallic",
      "title": "Make a red metallic material",
      "description": "Create a PBR material and assign it to the selected mesh. Properties are live setters; changes update the renderer immediately.",
      "intent": "mesh",
      "editor": "model",
      "category": "materials-textures",
      "apiSurfaces": [
        "ModelEditor.create.material",
        "material.assignTo"
      ],
      "code": "// Pick the first selected mesh and give it a red metallic look.\nconst sel = ls({selected: true})[0];\nif (!sel) { console.log('Select a mesh first.'); return; }\nconst red = create.material({\n  name: 'red',\n  baseColor: [1, 0.1, 0.1, 1],\n  metallic: 0.85,\n  roughness: 0.25,\n});\nawait red.assignTo(sel);\nconsole.log('Assigned', red.name, 'to', sel.name);\n",
      "expectedOutput": "console line: \"Assigned red to <mesh name>\"; viewport: mesh shows the new red metallic material."
    },
    {
      "kind": "recipe",
      "id": "math-class-constructors-roundtrip",
      "title": "Math class constructors: Vec2/3/4, Quat, Mat3/4, Euler, Color, Plane, Sphere, BoundingBox + helpers",
      "description": "Build every math class with the `new MathLib.Class(...)` constructor and demonstrate the three free-function helpers (mat4TransformVec4, quatInverse, rotateAxis).",
      "intent": "math",
      "editor": "previewer",
      "category": "misc",
      "apiSurfaces": [
        "MathLib.Vec2",
        "MathLib.Vec3",
        "MathLib.Vec4",
        "MathLib.Quat",
        "MathLib.Mat3",
        "MathLib.Mat4",
        "MathLib.Euler",
        "MathLib.Color",
        "MathLib.Plane",
        "MathLib.Sphere",
        "MathLib.BoundingBox",
        "MathLib.mat4TransformVec4",
        "MathLib.quatInverse",
        "MathLib.rotateAxis"
      ],
      "code": "// Every math handle has a class constructor on `MathLib` (capitalized name)\n// alongside its factory function (lowercase name). The constructors are useful\n// when you want `instanceof` checks or want to subclass.\n\n\nconst v2 = new MathLib.Vec2(1, 2);\nconst v3 = new MathLib.Vec3(3, 4, 0);\nconst v4 = new MathLib.Vec4(1, 2, 3, 1);\nconst q  = new MathLib.Quat(0, 0, 0, 1);\nconst m3 = new MathLib.Mat3();\nconst m4 = new MathLib.Mat4();\nconst e  = new MathLib.Euler(0, Math.PI / 4, 0, 'XYZ');\nconst col = new MathLib.Color(0.2, 0.8, 0.4);\nconst plane = new MathLib.Plane([0, 1, 0], 0);\nconst sph = new MathLib.Sphere([0, 0, 0], 1);\nconst bbox = new MathLib.BoundingBox([-1, -1, -1], [1, 1, 1]);\n\nconsole.log('Vec2:', JSON.stringify(v2.toArray()));\nconsole.log('Vec3:', JSON.stringify(v3.toArray()));\nconsole.log('Vec4:', JSON.stringify(v4.toArray()));\nconsole.log('Quat (identity):', JSON.stringify(q.toArray()));\nconsole.log('Mat3 (identity, det):', m3.determinant());\nconsole.log('Mat4 (identity, det):', m4.determinant());\nconsole.log('Euler:', JSON.stringify([e.x, e.y, e.z, e.order]));\nconsole.log('Color:', JSON.stringify(col.toArray()));\nconsole.log('Plane normal+constant:', JSON.stringify(plane.normal.toArray()), plane.constant);\nconsole.log('Sphere center+radius:', JSON.stringify(sph.center.toArray()), sph.radius);\nconsole.log('BoundingBox min+max:', JSON.stringify(bbox.min.toArray()), JSON.stringify(bbox.max.toArray()));\n\n// Free-function helpers work on raw tuples (NOT class instances).\n// mat4TransformVec4: apply a 4x4 to a homogeneous vec4 (w=1 = point, w=0 = direction)\nconst translate = MathLib.mat4FromTRS([5, 0, 0], [0, 0, 0, 1], [1, 1, 1]);\nconst point4 = MathLib.mat4TransformVec4(translate, [1, 2, 3, 1]);\nconst dir4   = MathLib.mat4TransformVec4(translate, [1, 2, 3, 0]);\nconsole.log('point4 (translation applied):', JSON.stringify(point4));\nconsole.log('dir4 (translation ignored):', JSON.stringify(dir4));\n\n// quatInverse: get the inverse of a unit quaternion (same as conjugate for unit q)\nconst spin = MathLib.quatFromEuler(0, Math.PI / 3, 0);\nconst inv = MathLib.quatInverse(spin);\nconst composed = MathLib.quatMultiply(spin, inv);\nconsole.log('q * q^-1 ≈ identity:', JSON.stringify(composed.map(x => +x.toFixed(3))));\n\n// rotateAxis: rotate a Vec3 tuple around a unit axis by angle (radians)\nconst vTuple = [1, 0, 0];\nconst rotated = MathLib.rotateAxis(vTuple, [0, 1, 0], Math.PI / 2);\nconst unrotated = MathLib.rotateAxis(rotated, [0, 1, 0], -Math.PI / 2);\nconsole.log('rotated 90deg around Y:', JSON.stringify(rotated.map(x => +x.toFixed(3))));\nconsole.log('roundtrip back ≈ original:', JSON.stringify(unrotated.map(x => +x.toFixed(3))));\n",
      "expectedOutput": "console: each class constructed, then helper roundtrips (rotate then inverse-rotate; Vec4 through a translation Mat4)."
    },
    {
      "kind": "recipe",
      "id": "math-color-blend-hsl",
      "title": "Color: hex parse, RGB blend, HSL roundtrip",
      "description": "Parse hex; lerp two colors; convert RGB ↔ HSL; multiply blend.",
      "intent": "math",
      "editor": "previewer",
      "category": "misc",
      "apiSurfaces": [
        "MathLib.color",
        "MathLib.lerp"
      ],
      "code": "const red = MathLib.color(1, 0, 0);\nconst blue = MathLib.color(0, 0, 1);\n\nconsole.log('red:', JSON.stringify(red.toArray()));\nconsole.log('blue:', JSON.stringify(blue.toArray()));\nconst mix25 = MathLib.lerp(red.toArray(), blue.toArray(), 0.25);\nconst mix50 = MathLib.lerp(red.toArray(), blue.toArray(), 0.5);\nconst mix75 = MathLib.lerp(red.toArray(), blue.toArray(), 0.75);\nconsole.log('lerp 0.25:', JSON.stringify(mix25.map(x => x.toFixed(3))));\nconsole.log('lerp 0.50:', JSON.stringify(mix50.map(x => x.toFixed(3))));\nconsole.log('lerp 0.75:', JSON.stringify(mix75.map(x => x.toFixed(3))));\n",
      "expectedOutput": "console: parsed hex; lerp results; HSL roundtrip."
    },
    {
      "kind": "recipe",
      "id": "math-color-chain",
      "title": "Color toolkit: RGB / hex / HSL factories, lerp, multiply, to-hex",
      "description": "Color handle walk-through: every factory, channel getters, blend with lerp / multiply, and round-trip through hex and HSL.",
      "intent": "scene",
      "editor": "model",
      "category": "materials-textures",
      "apiSurfaces": [
        "color.fromRGB",
        "color.fromHex",
        "color.fromHSL",
        "color.white",
        "color.black",
        "color.r",
        "color.g",
        "color.b",
        "color.clone",
        "color.toArray",
        "color.toHex",
        "color.toHexString",
        "color.toHSL",
        "color.lerp",
        "color.multiply",
        "color.equals"
      ],
      "code": "// Color OO handle. Linear-space floats 0-1.\nconst red = MathLib.Color.fromRGB(1, 0.1, 0.1);\nconst hex = MathLib.Color.fromHex('#FF8800');\nconst teal = MathLib.Color.fromHSL(0.5, 0.6, 0.5);\nconst w = MathLib.Color.white();\nconst k = MathLib.Color.black();\nconsole.log('red (r,g,b):', red.r, red.g, red.b);\nconsole.log('hex tuple:', hex.toArray(), '| toHex (int):', hex.toHex(), '| toHexString:', hex.toHexString());\n\n// Blend + tint.\nconst blended = red.lerp(w, 0.5);\nconst tinted = red.multiply(MathLib.Color.fromRGB(0.5, 0.5, 1));\nconst hsl = teal.toHSL();\nconsole.log('lerp(red, white, .5):', blended.toArray());\nconsole.log('multiply(red, [.5,.5,1]):', tinted.toArray());\nconsole.log('teal.toHSL:', hsl.h.toFixed(2), hsl.s.toFixed(2), hsl.l.toFixed(2));\n\n// Equality + clone.\nconsole.log('red == red.clone()?', red.equals(red.clone()));\nconsole.log('white != black?', !w.equals(k));\n",
      "expectedOutput": "console: r/g/b channels, hex round-trip, HSL components, lerp toward white, and a tint via component-wise multiply."
    },
    {
      "kind": "recipe",
      "id": "math-constructors-bbox-sphere-plane",
      "title": "Construct BoundingBox / Sphere / Plane / Vec2 / Vec4 / Mat3",
      "description": "Build core math types via factory functions; demonstrate MathLib.boundingBox/sphere/plane/vec2/vec4/mat3/euler.",
      "intent": "math",
      "editor": "previewer",
      "category": "misc",
      "apiSurfaces": [
        "MathLib.boundingBox",
        "MathLib.sphere",
        "MathLib.plane",
        "MathLib.vec2",
        "MathLib.vec3",
        "MathLib.vec4",
        "MathLib.mat3",
        "MathLib.mat4",
        "MathLib.euler",
        "MathLib.quat",
        "MathLib.distanceToPlane",
        "MathLib.invert"
      ],
      "code": "const v2 = MathLib.vec2(1, 2);\nconst v3 = MathLib.vec3(1, 2, 3);\nconst v4 = MathLib.vec4(1, 2, 3, 1);\nconst e  = MathLib.euler(0.5, 0.3, 0.2);\nconst q  = MathLib.quat(0, 0, 0, 1);\n\nconsole.log('vec2:', JSON.stringify(v2.toArray()));\nconsole.log('vec3:', JSON.stringify(v3.toArray()));\nconsole.log('vec4:', JSON.stringify(v4.toArray()));\nconsole.log('euler:', JSON.stringify([e.x, e.y, e.z]));\nconsole.log('quat:', JSON.stringify(q));\n\nconst bbox  = MathLib.boundingBox([0, 0, 0], [1, 1, 1]);\nconst sph   = MathLib.sphere([0, 0, 0], 1);\nconst plane = MathLib.plane([0, 1, 0], 0);\n\nconsole.log('bbox:', JSON.stringify({ min: bbox.min.toArray(), max: bbox.max.toArray() }));\nconsole.log('sphere:', JSON.stringify({ center: sph.center.toArray(), radius: sph.radius }));\nconsole.log('plane normal:', JSON.stringify(plane.normal.toArray()));\n\nconst distP = MathLib.distanceToPlane([0, 2, 0], plane);\nconsole.log('distance [0,2,0] to plane:', distP);\n\nconst inv = MathLib.invert(1, 0, 0, 0, 1, 0, 0, 0, 1);\nconsole.log('inverted 3x3 identity (first 3):', JSON.stringify(inv?.slice ? inv.slice(0, 3) : null));\n",
      "expectedOutput": "console: each constructor logged with shape info."
    },
    {
      "kind": "recipe",
      "id": "math-euler-chain",
      "title": "Euler toolkit: fromQuat, reorder, round-trip to Mat4 and Quat",
      "description": "Euler angle walk-through: convert from Quat / Mat4, change rotation order, and round-trip back to Quat / Mat4.",
      "intent": "scene",
      "editor": "model",
      "category": "misc",
      "apiSurfaces": [
        "euler.fromQuat",
        "euler.fromMat4",
        "euler.x",
        "euler.y",
        "euler.z",
        "euler.order",
        "euler.clone",
        "euler.toArray",
        "euler.reorder",
        "euler.toQuat",
        "euler.toMat4",
        "euler.equals"
      ],
      "code": "// Build a 90 deg rotation around Z and inspect it as Euler angles.\nconst q = MathLib.Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\nconst m = MathLib.Mat4.fromQuat(q);\n\nconst eXYZ = MathLib.Euler.fromQuat(q, 'XYZ');\nconst eYZX = eXYZ.reorder('YZX');\nconst fromM = MathLib.Euler.fromMat4(m, 'XYZ');\nconsole.log('fromQuat (XYZ):', eXYZ.x.toFixed(3), eXYZ.y.toFixed(3), eXYZ.z.toFixed(3), eXYZ.order);\nconsole.log('reorder (YZX):', eYZX.x.toFixed(3), eYZX.y.toFixed(3), eYZX.z.toFixed(3), eYZX.order);\nconsole.log('fromMat4 (XYZ):', fromM.x.toFixed(3), fromM.y.toFixed(3), fromM.z.toFixed(3), fromM.order);\n\n// Round-trip back to Quat / Mat4.\nconsole.log('toQuat:', eXYZ.toQuat().toArray());\nconsole.log('toMat4 row 0:', eXYZ.toMat4().toArray().slice(0, 4));\n\n// Conversion + equality.\nconsole.log('toArray:', eXYZ.toArray());\nconsole.log('clone == orig?', eXYZ.equals(eXYZ.clone()));\nconsole.log('XYZ == YZX (different orders)?', eXYZ.equals(eYZX));\n",
      "expectedOutput": "console: XYZ Euler from a 90 deg Z rotation, reorder to YZX, fromMat4 result, and round-trip back to Quat / Mat4."
    },
    {
      "kind": "recipe",
      "id": "math-mat3-chain",
      "title": "Mat3 toolkit: identity, fromQuat, normal matrix, invert",
      "description": "3x3 matrix walk-through: column-major factories, multiply / invert / transpose, normal matrix from a non-uniform-scale 4x4, and Vec3 transform.",
      "intent": "scene",
      "editor": "model",
      "category": "misc",
      "apiSurfaces": [
        "mat3.identity",
        "mat3.from",
        "mat3.fromQuat",
        "mat3.fromMat4",
        "mat3.normalMatrix",
        "mat3.clone",
        "mat3.toArray",
        "mat3.multiply",
        "mat3.invert",
        "mat3.transpose",
        "mat3.determinant",
        "mat3.equals",
        "mat3.transformVec3"
      ],
      "code": "// Mat3 -- column-major 3x3.\nconst id = MathLib.Mat3.identity();\nconsole.log('identity[0..3]:', id.toArray().slice(0, 3), '| det:', id.determinant());\n\n// Build from quat / from Mat4 / from raw 9-element array.\nconst q = MathLib.Quat.fromAxisAngle([0, 1, 0], Math.PI / 2);\nconst rotMat3 = MathLib.Mat3.fromQuat(q);\nconst rotMat4 = MathLib.Mat4.fromQuat(q);\nconst fromM4 = MathLib.Mat3.fromMat4(rotMat4);\nconst fromRaw = MathLib.Mat3.from([1, 0, 0, 0, 1, 0, 0, 0, 1]);\nconsole.log('fromQuat row 0:', rotMat3.toArray().slice(0, 3));\nconsole.log('fromMat4 row 0:', fromM4.toArray().slice(0, 3));\nconsole.log('from(raw9) det:', fromRaw.determinant());\n\n// Composition + utilities.\nconst prod = rotMat3.multiply(rotMat3);\nconst inv = rotMat3.inverse();\nconst tps = rotMat3.transpose();\nconst cln = rotMat3.clone();\nconsole.log('R*R row 0:', prod.toArray().slice(0, 3));\nconsole.log('R^-1 row 0:', inv.toArray().slice(0, 3));\nconsole.log('R^T row 0:', tps.toArray().slice(0, 3));\nconsole.log('clone == orig?', rotMat3.equals(cln));\n\n// Normal matrix -- inverse-transpose 3x3 of a non-uniform-scale 4x4.\nconst skew4 = MathLib.Mat4.fromTRS([0, 0, 0], q.toArray(), [2, 1, 1]);\nconst nm = MathLib.Mat3.normalMatrix(skew4);\nconsole.log('normalMatrix row 0:', nm.toArray().slice(0, 3));\n\n// Apply to a vector.\nconsole.log('Mat3.transformVec3(ex):', rotMat3.transformVec3([1, 0, 0]).toArray());\n",
      "expectedOutput": "console: identity diag, R*R / R^-1 / R^T rows, normal matrix from a (2,1,1)-scale, and transformVec3 result."
    },
    {
      "kind": "recipe",
      "id": "math-mat4-chain",
      "title": "Mat4 toolkit: TRS, lookAt, perspective, decompose, transform",
      "description": "4x4 matrix walk-through: every specialized factory, view + projection, decompose round-trip, and the four cross-type transform variants.",
      "intent": "scene",
      "editor": "model",
      "category": "misc",
      "apiSurfaces": [
        "mat4.identity",
        "mat4.from",
        "mat4.fromTranslation",
        "mat4.fromRotation",
        "mat4.fromScale",
        "mat4.fromQuat",
        "mat4.fromTRS",
        "mat4.lookAt",
        "mat4.perspective",
        "mat4.ortho",
        "mat4.clone",
        "mat4.toArray",
        "mat4.multiply",
        "mat4.invert",
        "mat4.transpose",
        "mat4.determinant",
        "mat4.equals",
        "mat4.decompose",
        "mat4.transformPoint",
        "mat4.transformVector",
        "mat4.transformDirection",
        "mat4.transformVec4"
      ],
      "code": "// Mat4 -- column-major 4x4. Translation lives at indices 12, 13, 14.\nconst id = MathLib.Mat4.identity();\nconsole.log('identity translation slot:', id.toArray().slice(12, 15), '| det:', id.determinant());\n\n// Specialized factories.\nconst t = MathLib.Mat4.fromTranslation([10, 20, 30]);\nconst r = MathLib.Mat4.fromRotation(MathLib.Quat.fromAxisAngle([0, 1, 0], Math.PI / 2));\nconst s = MathLib.Mat4.fromScale([2, 2, 2]);\nconst fq = MathLib.Mat4.fromQuat(MathLib.Quat.identity());\nconst trs = MathLib.Mat4.fromTRS([1, 2, 3], MathLib.Quat.identity().toArray(), [2, 2, 2]);\nconst raw = MathLib.Mat4.from(id.toArray());\nconst sArr = s.toArray();\nconsole.log('fromTranslation translation:', t.toArray().slice(12, 15));\nconsole.log('fromRotation det:', r.determinant().toFixed(4));\nconsole.log('fromScale diag:', sArr[0], sArr[5], sArr[10]);\nconsole.log('fromQuat(id) det:', fq.determinant().toFixed(4));\nconsole.log('fromTRS translation:', trs.toArray().slice(12, 15));\nconsole.log('from(raw16) det:', raw.determinant());\n\n// View + projection.\nconst view = MathLib.Mat4.lookAt([5, 5, 5], [0, 0, 0], [0, 1, 0]);\nconst persp = MathLib.Mat4.perspective(Math.PI / 4, 16 / 9, 0.1, 1000);\nconst orth = MathLib.Mat4.ortho(-10, 10, -10, 10, 0.1, 1000);\nconst pArr = persp.toArray();\nconst oArr = orth.toArray();\nconsole.log('lookAt translation:', view.toArray().slice(12, 15));\nconsole.log('perspective diag:', pArr[0].toFixed(3), pArr[5].toFixed(3));\nconsole.log('ortho diag:', oArr[0].toFixed(3), oArr[5].toFixed(3));\n\n// Math + composition.\nconst m = r.multiply(t);\nconst inv = m.inverse();\nconst tps = m.transpose();\nconst cln = m.clone();\nconsole.log('R*T det:', m.determinant().toFixed(4), '| inv det:', inv.determinant().toFixed(4));\nconsole.log('transpose row 0:', tps.toArray().slice(0, 4));\nconsole.log('clone == orig?', m.equals(cln));\n\n// Decompose round-trip.\nconst { translation, rotation, scale } = trs.decompose();\nconsole.log('decompose:', translation.toArray(), rotation.toArray(), scale.toArray());\n\n// Cross-type transforms.\nconsole.log('transformPoint:', m.transformPoint([1, 0, 0]).toArray());\nconsole.log('transformVector:', m.transformVector([1, 0, 0]).toArray());\nconsole.log('transformDirection:', m.transformDirection([1, 0, 0]).toArray());\nconsole.log('transformVec4:', m.transformVec4([1, 0, 0, 1]).toArray());\n",
      "expectedOutput": "console: translation slot, projection diagonals, decompose triple, then point vs vector vs direction vs vec4 transforms."
    },
    {
      "kind": "recipe",
      "id": "math-matrix-trs-decompose",
      "title": "Matrix TRS: compose, multiply, decompose, transform",
      "description": "Build a TRS matrix; multiply two; invert + transpose; transform a point and direction.",
      "intent": "math",
      "editor": "previewer",
      "category": "misc",
      "apiSurfaces": [
        "MathLib.mat4FromTRS",
        "MathLib.mat4Multiply",
        "MathLib.mat4Identity",
        "MathLib.mat4Inverse",
        "MathLib.mat4Transpose",
        "MathLib.mat4TransformPoint",
        "MathLib.mat4TransformVector",
        "MathLib.mat4TransformNormal",
        "MathLib.transformPoint",
        "MathLib.transformVector",
        "MathLib.transformVec4",
        "MathLib.mat4Decompose",
        "MathLib.quatFromEuler"
      ],
      "code": "const t = [2, 3, 4];\nconst q = MathLib.quatFromEuler(Math.PI / 4, 0, 0);\nconst s = [1, 1.5, 2];\nconst m = MathLib.mat4FromTRS(t, q, s);\n\nconst m2 = MathLib.mat4FromTRS([1, 0, 0], MathLib.quatFromEuler(0, Math.PI / 6, 0), [1, 1, 1]);\nconst product = MathLib.mat4Multiply(m, m2);\nconst inv = MathLib.mat4Inverse(m);\nconst trans = MathLib.mat4Transpose(m);\nconst id = MathLib.mat4Identity();\n\nconsole.log('product[0]:', product[0].toFixed(3));\nconsole.log('inv[0]:', inv[0].toFixed(3));\nconsole.log('trans[1]:', trans[1].toFixed(3));\nconsole.log('id[0]:', id[0]);\n\nconst point = [1, 0, 0];\nconst dir = [0, 1, 0];\nconst v4 = [1, 2, 3, 1];\n\nconsole.log('point→', JSON.stringify(MathLib.mat4TransformPoint(m, point).map(x => x.toFixed(3))));\nconsole.log('dir→', JSON.stringify(MathLib.mat4TransformVector(m, dir).map(x => x.toFixed(3))));\nconsole.log('normal→', JSON.stringify(MathLib.mat4TransformNormal(m, dir).map(x => x.toFixed(3))));\nconsole.log('transformPoint→', JSON.stringify(MathLib.transformPoint(m, point).map(x => x.toFixed(3))));\nconsole.log('transformVec4→', JSON.stringify(MathLib.transformVec4(m, v4).map(x => x.toFixed(3))));\nconsole.log('transformVector→', JSON.stringify(MathLib.transformVector(m, dir).map(x => x.toFixed(3))));\n",
      "expectedOutput": "console: matrix products + transformed point + direction + normal."
    },
    {
      "kind": "recipe",
      "id": "math-plane-build-project-intersect",
      "title": "Plane: build from point+normal or three points, project, intersect a line, normalize and negate",
      "description": "Build a Plane two ways, project a point onto it, find a ray intersection, then flip and normalize.",
      "intent": "scene",
      "editor": "previewer",
      "category": "spatial",
      "apiSurfaces": [
        "plane.fromPointAndNormal",
        "plane.fromThreePoints",
        "plane.normal",
        "plane.constant",
        "plane.normalize",
        "plane.negate",
        "plane.clone",
        "plane.equals",
        "plane.distanceToPoint",
        "plane.projectPoint",
        "plane.intersectsLine"
      ],
      "code": "// Build a horizontal plane at y=2 two different ways and confirm they agree.\nconst planeA = MathLib.Plane.fromPointAndNormal([0, 2, 0], [0, 1, 0]);\nconst planeB = MathLib.Plane.fromThreePoints([0, 2, 0], [1, 2, 0], [0, 2, 1]);\n\nconsole.log('planeA normal:', JSON.stringify(planeA.normal.toArray()), 'constant:', planeA.constant);\nconsole.log('planeB normal:', JSON.stringify(planeB.normal.toArray()), 'constant:', planeB.constant);\nconsole.log('planeA equals planeB:', planeA.equals(planeB));\n\n// Distance from a few points above and below the plane.\nconst above = [3, 5, -1];\nconst below = [3, -1, -1];\nconsole.log('distance(above):', planeA.distanceToPoint(above).toFixed(3));\nconsole.log('distance(below):', planeA.distanceToPoint(below).toFixed(3));\n\n// Project the \"above\" point straight down onto the plane.\nconst projected = planeA.projectPoint(above);\nconsole.log('projected onto plane:', JSON.stringify(projected.toArray().map(x => +x.toFixed(3))));\nconsole.log('projected distance (≈ 0):', planeA.distanceToPoint(projected).toFixed(6));\n\n// Intersect a vertical line with the plane: should land at y=2.\nconst hit = planeA.intersectsLine({ start: [3, 5, -1], end: [3, -1, -1] });\nconsole.log('line intersection:', hit ? JSON.stringify(hit.toArray()) : 'parallel');\n\n// Clone, negate (flip normal), and normalize (renormalize a denormalized plane).\nconst flipped = planeA.clone().negate();\nconsole.log('flipped normal:', JSON.stringify(flipped.normal.toArray()));\n\nconst denorm = MathLib.Plane.fromPointAndNormal([0, 2, 0], [0, 3, 0]);\nconst norm = denorm.clone().normalize();\nconsole.log('normalized normal length ≈ 1:', norm.normal.length().toFixed(3));\n",
      "expectedOutput": "console: identical planes from two builders; projection drops to plane; line intersection point logged."
    },
    {
      "kind": "recipe",
      "id": "math-quat-chain",
      "title": "Quat toolkit: axis-angle, slerp, compose, rotate, convert",
      "description": "Quaternion walk-through: every factory, every blend (multiply / slerp / lerp), and conversion to Mat4 / Euler.",
      "intent": "scene",
      "editor": "model",
      "category": "misc",
      "apiSurfaces": [
        "quat.from",
        "quat.identity",
        "quat.fromAxisAngle",
        "quat.fromEuler",
        "quat.fromMat3",
        "quat.lookRotation",
        "quat.setFromUnitVectors",
        "quat.x",
        "quat.y",
        "quat.z",
        "quat.w",
        "quat.clone",
        "quat.toArray",
        "quat.multiply",
        "quat.slerp",
        "quat.lerp",
        "quat.inverse",
        "quat.conjugate",
        "quat.normalize",
        "quat.length",
        "quat.dot",
        "quat.angleTo",
        "quat.equals",
        "quat.rotate",
        "quat.toMatrix",
        "quat.toEuler"
      ],
      "code": "// Quat OO handle. Tuple order is [x, y, z, w] (w last, glTF / three.js convention).\nconst id = MathLib.Quat.identity();\nconst a = MathLib.Quat.from([0, 0, 0, 1]);\nconsole.log('identity (x,y,z,w):', id.x, id.y, id.z, id.w, '| length:', id.length());\nconsole.log('from([0,0,0,1]) == identity?', a.equals(id));\n\n// Factories -- pick your rotation source.\nconst aroundZ = MathLib.Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\nconst fromEul = MathLib.Quat.fromEuler(0, Math.PI / 2, 0, 'XYZ');\nconst fromM3 = MathLib.Quat.fromMat3(MathLib.Mat3.fromQuat(aroundZ));\nconst lookFwd = MathLib.Quat.lookRotation([0, 0, -1], [0, 1, 0]);\nconst xToY = MathLib.Quat.setFromUnitVectors([1, 0, 0], [0, 1, 0]);\nconsole.log('fromAxisAngle(z, 90 deg):', aroundZ.toArray());\nconsole.log('fromEuler:', fromEul.toArray(), '| fromMat3:', fromM3.toArray());\nconsole.log('lookRotation(-z):', lookFwd.toArray(), '| setFromUnitVectors(x->y):', xToY.toArray());\n\n// Composition + blends.\nconst composed = aroundZ.multiply(aroundZ);\nconst slerped = MathLib.Quat.slerp(id, aroundZ, 0.5);\nconst lerped = id.lerp(aroundZ, 0.5);\nconst inv = aroundZ.inverse();\nconst conj = aroundZ.conjugate();\nconst nrm = aroundZ.normalize();\nconsole.log('q*q:', composed.toArray(), '| inverse:', inv.toArray(), '| conjugate:', conj.toArray());\nconsole.log('slerp(id, q, .5):', slerped.toArray(), '| lerp(id, q, .5):', lerped.toArray());\nconsole.log('normalize(q):', nrm.toArray());\n\n// Cross-type: rotate a Vec3, convert to matrix/euler.\nconsole.log('rotate([1,0,0]):', aroundZ.rotate([1, 0, 0]).toArray());\nconsole.log('toMatrix row 0:', aroundZ.toMatrix().toArray().slice(0, 4));\nconsole.log('toEuler:', aroundZ.toEuler('XYZ').toArray());\nconsole.log('angleTo(id):', aroundZ.angleTo(id).toFixed(4));\nconsole.log('dot(q, q):', aroundZ.dot(aroundZ).toFixed(4));\nconsole.log('q == q.clone()?', aroundZ.equals(aroundZ.clone()));\n",
      "expectedOutput": "console: factory outputs, composed rotations, slerp/lerp midpoints, then rotate(Vec3) and Mat4/Euler conversions."
    },
    {
      "kind": "recipe",
      "id": "math-quaternion-roundtrip",
      "title": "Quaternion roundtrip: Euler → Quat → vector rotation",
      "description": "Convert Euler to quaternion; compose two quaternions; slerp between them; rotate a vector and verify roundtrip.",
      "intent": "math",
      "editor": "previewer",
      "category": "misc",
      "apiSurfaces": [
        "MathLib.euler",
        "MathLib.quatFromEuler",
        "MathLib.quatFromAxisAngle",
        "MathLib.quatMultiply",
        "MathLib.quatNormalize",
        "MathLib.quatFromUnitVectors",
        "MathLib.quatConjugate",
        "MathLib.quatRotateVec3",
        "MathLib.slerp"
      ],
      "code": "const qRoll  = MathLib.quatFromEuler(0, 0, Math.PI / 4);\nconst qPitch = MathLib.quatFromEuler(Math.PI / 6, 0, 0);\nconst qYaw   = MathLib.quatFromEuler(0, Math.PI / 3, 0);\n\nconst composed = MathLib.quatNormalize(MathLib.quatMultiply(qRoll, qPitch));\nconst mid = MathLib.slerp(qRoll, qYaw, 0.5);\n\nconst v = [1, 0, 0];\nconst rotated = MathLib.quatRotateVec3(qRoll, v);\nconst unrotated = MathLib.quatRotateVec3(MathLib.quatConjugate(qRoll), rotated);\nconst roundtripOK = MathLib.distance(unrotated, v) < 1e-5;\n\nconst mapQuat = MathLib.quatFromUnitVectors([1, 0, 0], [0, 1, 0]);\nconst axisQuat = MathLib.quatFromAxisAngle([0, 1, 0], Math.PI / 4);\n\nconsole.log('composed:', JSON.stringify(composed.map(x => x.toFixed(3))));\nconsole.log('slerp-mid:', JSON.stringify(mid.map(x => x.toFixed(3))));\nconsole.log('rotate [1,0,0] by roll:', JSON.stringify(rotated.map(x => x.toFixed(3))));\nconsole.log('roundtrip ok:', roundtripOK);\nconsole.log('map [1,0,0]→[0,1,0]:', JSON.stringify(mapQuat.map(x => x.toFixed(3))));\nconsole.log('axisQuat Y 45°:', JSON.stringify(axisQuat.map(x => x.toFixed(3))));\n",
      "expectedOutput": "console: composed quaternion + slerp midpoint + rotated vector + roundtrip-PASS/FAIL."
    },
    {
      "kind": "recipe",
      "id": "math-sphere-bounding-ops",
      "title": "Sphere: bounding-sphere from points or bbox, expand, contains, intersect plane/box/sphere",
      "description": "Build a sphere from a point cloud and from a bbox; expand to include a new point; query containment and intersections.",
      "intent": "scene",
      "editor": "previewer",
      "category": "spatial",
      "apiSurfaces": [
        "sphere.fromPoints",
        "sphere.fromBoundingBox",
        "sphere.center",
        "sphere.radius",
        "sphere.clone",
        "sphere.equals",
        "sphere.contains",
        "sphere.distanceToPoint",
        "sphere.expand",
        "sphere.intersects",
        "sphere.intersectsBox",
        "sphere.intersectsPlane"
      ],
      "code": "// Bounding sphere from a small point cloud.\nconst points = [\n  [0, 0, 0], [2, 0, 0], [0, 2, 0],\n  [0, 0, 2], [-1, -1, -1],\n];\nconst sphA = MathLib.Sphere.fromPoints(points);\nconsole.log('fromPoints center:', JSON.stringify(sphA.center.toArray().map(x => +x.toFixed(3))));\nconsole.log('fromPoints radius:', sphA.radius.toFixed(3));\n\n// Bounding sphere from a bounding box.\nconst bbox = new MathLib.BoundingBox([-1, -1, -1], [1, 1, 1]);\nconst sphB = MathLib.Sphere.fromBoundingBox(bbox);\nconsole.log('fromBoundingBox center:', JSON.stringify(sphB.center.toArray()));\nconsole.log('fromBoundingBox radius (≈ sqrt(3)):', sphB.radius.toFixed(3));\n\n// Clone and equals: independent copies compare equal.\nconst sphBCopy = sphB.clone();\nconsole.log('clone equals original:', sphB.equals(sphBCopy));\n\n// contains / distanceToPoint: interior vs exterior point.\nconst inside = [0.3, 0.4, 0.2];\nconst outside = [3, 3, 3];\nconsole.log('contains inside:', sphB.contains(inside));\nconsole.log('contains outside:', sphB.contains(outside));\nconsole.log('distance to outside:', sphB.distanceToPoint(outside).toFixed(3));\n\n// expand: grow the sphere to include a far-away point.\nconst grown = sphB.clone().expand([5, 0, 0]);\nconsole.log('expanded radius:', grown.radius.toFixed(3));\nconsole.log('expanded now contains [5,0,0]:', grown.contains([5, 0, 0]));\n\n// Intersect with another sphere: two unit spheres at +/-1 on x.\nconst left = new MathLib.Sphere([-1, 0, 0], 1.2);\nconst right = new MathLib.Sphere([1, 0, 0], 1.2);\nconsole.log('left intersects right:', left.intersects(right));\n\n// Intersect with a bbox: unit sphere vs an offset bbox.\nconst farBox = new MathLib.BoundingBox([2, 0, 0], [3, 1, 1]);\nconsole.log('sphB intersects farBox:', sphB.intersectsBox(farBox));\n\n// Intersect with a plane: y=0 plane cuts the unit sphere at the equator.\nconst equator = MathLib.Plane.fromPointAndNormal([0, 0, 0], [0, 1, 0]);\nconsole.log('sphB intersects equator:', sphB.intersectsPlane(equator));\n",
      "expectedOutput": "console: bounding sphere fits points; expand grows radius; intersect verdicts against a plane, box, and second sphere."
    },
    {
      "kind": "recipe",
      "id": "math-vec2-chain",
      "title": "Vec2 toolkit: build, blend, measure, normalize",
      "description": "2D vector handle walk-through: factories, axis getters, length, lerp, distance, dot, normalize.",
      "intent": "scene",
      "editor": "model",
      "category": "misc",
      "apiSurfaces": [
        "vec2.from",
        "vec2.zero",
        "vec2.one",
        "vec2.x",
        "vec2.y",
        "vec2.clone",
        "vec2.toArray",
        "vec2.add",
        "vec2.sub",
        "vec2.scale",
        "vec2.negate",
        "vec2.lerp",
        "vec2.normalize",
        "vec2.length",
        "vec2.lengthSq",
        "vec2.dot",
        "vec2.distanceTo",
        "vec2.distanceSqTo",
        "vec2.equals"
      ],
      "code": "// Vec2 OO handle. Every math method returns a NEW Vec2 (immutable).\nconst a = MathLib.Vec2.from([3, 4]);\nconst b = MathLib.Vec2.from([0, 0]);\nconst ones = MathLib.Vec2.one();\nconst zero = MathLib.Vec2.zero();\nconsole.log('a (x,y):', a.x, a.y, '| length:', a.length(), '| lengthSq:', a.lengthSq());\nconsole.log('ones:', ones.toArray(), '| zero:', zero.toArray());\n\nconst sum = a.add(ones);\nconst diff = a.sub(zero);\nconst scaled = a.scale(2);\nconst neg = a.negate();\nconst mid = a.lerp(b, 0.5);\nconst nrm = a.normalize();\nconsole.log('sum:', sum.toArray(), '| diff:', diff.toArray(), '| scaled:', scaled.toArray());\nconsole.log('neg:', neg.toArray(), '| lerp(a, b, .5):', mid.toArray());\nconsole.log('normalize(a).length() ~', nrm.length().toFixed(4));\n\nconsole.log('dot(a, a):', a.dot(a));\nconsole.log('distanceTo(b):', a.distanceTo(b), '| distanceSqTo(b):', a.distanceSqTo(b));\nconsole.log('a == a.clone()?', a.equals(a.clone()), '| a == b?', a.equals(b));\n",
      "expectedOutput": "console: axis getters, length, sum/diff/scale tuples, lerp midpoint, normalize length ~1, dot/distance."
    },
    {
      "kind": "recipe",
      "id": "math-vec3-chain",
      "title": "Vec3 toolkit: factories, cross, rotate, transform",
      "description": "3D vector walk-through: basis vectors, cross product, distance, normalize, and cross-type transforms (Quat / Mat3 / Mat4 / direction).",
      "intent": "scene",
      "editor": "model",
      "category": "misc",
      "apiSurfaces": [
        "vec3.from",
        "vec3.zero",
        "vec3.one",
        "vec3.unitX",
        "vec3.unitY",
        "vec3.unitZ",
        "vec3.x",
        "vec3.y",
        "vec3.z",
        "vec3.clone",
        "vec3.toArray",
        "vec3.add",
        "vec3.sub",
        "vec3.scale",
        "vec3.negate",
        "vec3.lerp",
        "vec3.normalize",
        "vec3.length",
        "vec3.lengthSq",
        "vec3.dot",
        "vec3.cross",
        "vec3.distanceTo",
        "vec3.distanceSqTo",
        "vec3.equals",
        "vec3.applyQuat",
        "vec3.applyMat3",
        "vec3.applyMat4",
        "vec3.transformDirection"
      ],
      "code": "// Vec3 OO handle. Immutable: every math method returns a NEW Vec3.\nconst a = MathLib.Vec3.from([1, 2, 3]);\nconst b = MathLib.Vec3.from([4, 5, 6]);\nconst ones = MathLib.Vec3.one();\nconst zero = MathLib.Vec3.zero();\nconst ex = MathLib.Vec3.unitX();\nconst ey = MathLib.Vec3.unitY();\nconst ez = MathLib.Vec3.unitZ();\nconsole.log('a (x,y,z):', a.x, a.y, a.z);\nconsole.log('ones:', ones.toArray(), '| zero:', zero.toArray());\nconsole.log('basis:', ex.toArray(), ey.toArray(), ez.toArray());\n\nconst sum = a.add(b);\nconst diff = a.sub(b);\nconst cross = ex.cross(ey);\nconst scaled = a.scale(0.5);\nconst neg = a.negate();\nconst mid = a.lerp(b, 0.5);\nconst nrm = MathLib.Vec3.from([3, 0, 4]).normalize();\nconsole.log('sum:', sum.toArray(), '| diff:', diff.toArray(), '| cross(ex, ey):', cross.toArray());\nconsole.log('scaled:', scaled.toArray(), '| neg:', neg.toArray(), '| lerp(a, b, .5):', mid.toArray());\nconsole.log('length:', a.length(), '| lengthSq:', a.lengthSq(), '| dot(a, b):', a.dot(b));\nconsole.log('normalize([3,0,4]):', nrm.toArray(), '| distanceTo:', a.distanceTo(b), '| distanceSqTo:', a.distanceSqTo(b));\nconsole.log('a == a.clone()?', a.equals(a.clone()));\n\n// Cross-type transforms -- rotate ex by 90 deg around Z via three forms + a normalize-direction.\nconst rotZ = MathLib.Quat.fromAxisAngle([0, 0, 1], Math.PI / 2);\nconst mat3Z = MathLib.Mat3.fromQuat(rotZ);\nconst mat4Z = MathLib.Mat4.fromQuat(rotZ);\nconsole.log('applyQuat(ex):', ex.applyQuat(rotZ).toArray());\nconsole.log('applyMat3(ex):', ex.applyMat3(mat3Z).toArray());\nconsole.log('applyMat4(ex):', ex.applyMat4(mat4Z).toArray());\nconsole.log('transformDirection(ex):', ex.transformDirection(mat4Z).toArray());\n",
      "expectedOutput": "console: axis getters, basis tuples, cross/dot/distance, then four transform variants on the X axis."
    },
    {
      "kind": "recipe",
      "id": "math-vec3attribute-translate-axes",
      "title": "Vec3Attribute: read translate axes from a compound plug",
      "description": "Compound 3-vector handle on every Transform. Inspect the basePath plus the three per-axis Attribute<number> descriptors.",
      "intent": "scene",
      "editor": "model",
      "category": "attributes",
      "apiSurfaces": [
        "vec3Attribute.basePath",
        "vec3Attribute.x",
        "vec3Attribute.y",
        "vec3Attribute.z"
      ],
      "code": "// node.translate / node.rotate / node.scale are Vec3Attribute instances.\n// Self-contained: spawn a cube off-origin, then walk the compound plug.\nconst cube = await create.cube({\n  width: 1, height: 1, depth: 1, name: 'Vec3AttrDemo',\n});\ncube.translate.set([2.5, 1.3, -0.7]); // whole-vector write via the [x,y,z] setter\n\n// The compound handle exposes basePath plus three per-axis Attribute<number>.\nconst t = cube.translate;\nconsole.log('basePath:', t.basePath); // -> 'translate'\nconsole.log('vector get():', JSON.stringify(t.get().map((v) => +v.toFixed(3))));\nconsole.log('x.get():', t.x.get().toFixed(3),\n  '| y.get():', t.y.get().toFixed(3),\n  '| z.get():', t.z.get().toFixed(3));\n\n// Per-axis writes flow through the SAME compound plug as the [x,y,z] setter:\n// the other two components are preserved.\nt.x.set(t.x.get() + 0.5); // bump X by 0.5 m, leave Y/Z untouched\nawait Utils.wait.frame();\nconsole.log('after bumping x by 0.5:', JSON.stringify(cube.translate.get().map((v) => +v.toFixed(3))));\n\n// Per-axis lock is independent: locking translate.x leaves Y/Z writable.\nt.x.lock(true);\nconsole.log('translate.x locked? (writes to X now throw)');\nt.y.set(9.9);             // Y is still free\nconsole.log('y after locked-x write:', cube.translate.y.get().toFixed(3));\nt.x.lock(false);          // release the lock so the demo leaves a clean node\n\nselect(null, { mode: 'clear' });\n",
      "expectedOutput": "console: basePath = \"translate\"; per-axis get() values matching the cube position; bumped X after a per-axis set."
    },
    {
      "kind": "recipe",
      "id": "math-vec4-chain",
      "title": "Vec4 toolkit: homogeneous transform, blend, normalize",
      "description": "4D vector walk-through with the w-component driving homogeneous semantics: w=1 transforms as a point, w=0 as a direction.",
      "intent": "scene",
      "editor": "model",
      "category": "misc",
      "apiSurfaces": [
        "vec4.from",
        "vec4.zero",
        "vec4.one",
        "vec4.x",
        "vec4.y",
        "vec4.z",
        "vec4.w",
        "vec4.clone",
        "vec4.toArray",
        "vec4.add",
        "vec4.sub",
        "vec4.scale",
        "vec4.negate",
        "vec4.lerp",
        "vec4.normalize",
        "vec4.length",
        "vec4.lengthSq",
        "vec4.dot",
        "vec4.equals",
        "vec4.applyMat4"
      ],
      "code": "// Vec4 OO handle. Immutable -- every math method returns a NEW Vec4.\nconst a = MathLib.Vec4.from([1, 2, 3, 1]);\nconst b = MathLib.Vec4.from([0, 0, 0, 0]);\nconst ones = MathLib.Vec4.one();\nconst zero = MathLib.Vec4.zero();\nconsole.log('a (x,y,z,w):', a.x, a.y, a.z, a.w);\nconsole.log('ones:', ones.toArray(), '| zero:', zero.toArray());\n\nconst sum = a.add(ones);\nconst diff = a.sub(b);\nconst scaled = a.scale(2);\nconst neg = a.negate();\nconst mid = a.lerp(b, 0.5);\nconst nrm = a.normalize();\nconsole.log('sum:', sum.toArray(), '| diff:', diff.toArray(), '| scaled:', scaled.toArray());\nconsole.log('neg:', neg.toArray(), '| lerp(a, b, .5):', mid.toArray());\nconsole.log('normalize(a).length() ~', nrm.length().toFixed(4));\nconsole.log('lengthSq:', a.lengthSq(), '| dot(a, a):', a.dot(a));\nconsole.log('a == a.clone()?', a.equals(a.clone()));\n\n// Homogeneous transform -- w drives translation.\nconst translateBy = MathLib.Mat4.fromTranslation([10, 20, 30]);\nconst asPoint = MathLib.Vec4.from([1, 2, 3, 1]).applyMat4(translateBy);\nconst asDirection = MathLib.Vec4.from([1, 2, 3, 0]).applyMat4(translateBy);\nconsole.log('point  (w=1):', asPoint.toArray());\nconsole.log('direction (w=0):', asDirection.toArray());\n",
      "expectedOutput": "console: axis getters, math results, then point vs direction transform contrast (translate applies only when w=1)."
    },
    {
      "kind": "recipe",
      "id": "math-vector-ops-sampler",
      "title": "Vector operations: dot, cross, length, normalize, distance",
      "description": "Construct two Vec3s; compute add/sub/dot/cross/length/lengthSq/normalize/distance/scale/negate.",
      "intent": "math",
      "editor": "previewer",
      "category": "misc",
      "apiSurfaces": [
        "MathLib.vec3",
        "MathLib.add",
        "MathLib.sub",
        "MathLib.dot",
        "MathLib.cross",
        "MathLib.length",
        "MathLib.lengthSq",
        "MathLib.normalize",
        "MathLib.distance",
        "MathLib.scale",
        "MathLib.negate"
      ],
      "code": "const a = MathLib.vec3(3, 4, 0);\nconst b = MathLib.vec3(1, 0, 2);\nconst aArr = a.toArray();\nconst bArr = b.toArray();\n\nconsole.log('add:', JSON.stringify(MathLib.add(aArr, bArr)));\nconsole.log('sub:', JSON.stringify(MathLib.sub(aArr, bArr)));\nconsole.log('dot:', MathLib.dot(aArr, bArr));\nconsole.log('cross:', JSON.stringify(MathLib.cross(aArr, bArr)));\nconsole.log('length(a):', MathLib.length(aArr).toFixed(3));\nconsole.log('lengthSq(b):', MathLib.lengthSq(bArr).toFixed(3));\nconsole.log('distance:', MathLib.distance(aArr, bArr).toFixed(3));\nconsole.log('normalize(a):', JSON.stringify(MathLib.normalize(aArr).map(x => x.toFixed(3))));\nconsole.log('scale(a, 2):', JSON.stringify(MathLib.scale(aArr, 2)));\nconsole.log('negate(a):', JSON.stringify(MathLib.negate(aArr)));\n",
      "expectedOutput": "console: 10+ numeric results for vector ops."
    },
    {
      "kind": "recipe",
      "id": "mesh-low-level-constructors",
      "title": "Build meshes from raw verts, polylines, profiles, and revolves",
      "description": "Demonstrate every non-class mesh factory: fromVertsAndFaces / empty / loft and Curve sweep methods: extrudeAlong / revolveAround / fillBoundary.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.fromVertsAndFaces",
        "ModelEditor.create.empty",
        "ModelEditor.create.loft",
        "Curve.extrudeAlong",
        "Curve.revolveAround",
        "Curve.fillBoundary",
        "Utils.wait.frame",
        "node.translate"
      ],
      "code": "// 1. fromVertsAndFaces: explicit pyramid\nconst pyramid = await create.fromVertsAndFaces({\n  positions: [[-1,0,-1], [1,0,-1], [1,0,1], [-1,0,1], [0,1.5,0]],\n  faceVertexCounts: [3, 3, 3, 3, 4],\n  faceVertexIndices: [0,1,4, 1,2,4, 2,3,4, 3,0,4, 0,3,2,1],\n  name: 'CustomPyramid',\n});\npyramid.translate.set([2.5, 1.3, -0.7]);\nselect(null, {mode: 'clear'});\nconsole.log('fromVertsAndFaces:', pyramid.name, 'verts=' + pyramid.verts.count);\n\n// 2. empty: start with zero verts (caller appends incrementally)\nconst blank = await create.empty({ name: 'BlankSlate' });\nconsole.log('empty:', blank.name, 'verts=' + blank.verts.count);\n\n// 3. Curve.extrudeAlong: sweep a profile curve along a path\nconst profileCurve = await create.curve({\n  points: [[-0.2, 0, 0], [0.2, 0, 0], [0.2, 0.4, 0], [-0.2, 0.4, 0]],\n  closed: true,\n});\nconst beam = await profileCurve.extrudeAlong([[0, 0, 0], [0, 0, 2]]);\nawait beam.rename('Beam');\nbeam.translate.set([5.0, 1.3, -0.7]);\nselect(null, {mode: 'clear'});\nconsole.log('extrudeAlong:', beam.name, 'verts=' + beam.verts.count);\n\n// 4. loft: bridge two profiles\nconst loft = await create.loft({\n  profiles: [\n    [[0,0,0], [1,0,0], [1,0,1], [0,0,1]],\n    [[0,1,0], [0.7,1,0], [0.7,1,0.7], [0,1,0.7]],\n  ],\n  caps: true,\n  name: 'LoftBridge',\n});\nloft.translate.set([10.0, 1.3, -0.7]);\nselect(null, {mode: 'clear'});\nconsole.log('loft:', loft.name, 'verts=' + loft.verts.count);\n\n// 5. Curve.revolveAround: spin a profile curve around an axis\nconst revolveProfile = await create.curve({\n  points: [[0.5, 0, 0], [1.0, 0.5, 0], [0.5, 1.0, 0]],\n});\nconst revolved = await revolveProfile.revolveAround('y', { segments: 12 });\nawait revolved.rename('RevolvedVase');\nrevolved.translate.set([12.5, 1.3, -0.7]);\nselect(null, {mode: 'clear'});\nconsole.log('revolveAround:', revolved.name, 'verts=' + revolved.verts.count);\n\n// 6. Curve.fillBoundary: closed boundary curve -> cap\nconst boundaryCurve = await create.curve({\n  points: [[0,0,0], [1,0,0], [1,0,1], [0,0,1]],\n  closed: true,\n});\nconst cap = await boundaryCurve.fillBoundary();\nawait cap.rename('BoundaryCap');\ncap.translate.set([15.0, 1.3, -0.7]);\nselect(null, {mode: 'clear'});\nconsole.log('fillBoundary:', cap.name, 'verts=' + cap.verts.count);\n\nawait Utils.wait.frame();\n",
      "expectedOutput": "console: one line per mesh confirming name and vert count."
    },
    {
      "kind": "recipe",
      "id": "mod-batch-rename",
      "title": "Batch-rename every mesh with an index prefix",
      "description": "List every mesh in the scene, capture the original names for a verification pass, then rename each one as \"<idx>_<oldName>\".",
      "intent": "mesh",
      "editor": "model",
      "category": "outliner",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "node.rename"
      ],
      "code": "// Prefix every mesh with a zero-padded index. Single undo for the whole pass.\n// Capture the original list first so we can verify each rename landed.\nconst meshes = ls({ type: 'mesh' });\nconst before = meshes.map((m) => m.name);\nconsole.log('renaming', meshes.length, 'meshes');\n\nmeshes.forEach((m, i) => {\n  const idx = String(i + 1).padStart(3, '0');\n  const newName = idx + '_' + before[i];\n  m.rename(newName);\n});\n\n// Verification pass: report every before / after pair so the user can spot\n// surprises (e.g., auto-disambiguation appending a numeric suffix).\nmeshes.forEach((m, i) => {\n  console.log('  ' + before[i] + ' -> ' + m.name);\n});\nconsole.log('done; one Ctrl+Z reverts all', meshes.length, 'renames');\n",
      "expectedOutput": "Outliner shows every mesh prefixed with \"001_\", \"002_\", ...; console reports before/after pairs; one undo step reverts everything."
    },
    {
      "kind": "recipe",
      "id": "mod-build-spheres-on-vertices",
      "title": "Mark every vertex of a mesh with a small sphere",
      "description": "Build a low-poly mesh, then mark every vertex with a tiny sphere at its world position and parent the lot under a group. Handy for visually debugging topology.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.create.sphere",
        "ModelEditor.create.group",
        "mesh.geometry.bbox",
        "mesh.verts",
        "node.translate"
      ],
      "code": "// Self-contained from an EMPTY scene: build a low-poly mesh, then drop a\n// tiny sphere on each of its vertices.\n\n// 1. A subdivided cube gives a modest, predictable vertex count to mark.\nconst mesh = await create.cube({\n  width: 2, height: 2, depth: 2,   // 2 m box (>0)\n  subdivisionsX: 1,                // extra cuts per axis (0 = box corners only)\n  subdivisionsY: 1,\n  subdivisionsZ: 1,\n  name: 'Topo',\n});\nselect(null, { mode: 'clear' });\n\n// 2. Size the markers relative to the mesh so they stay readable at any scale.\n//    bbox lives on mesh.geometry; space 'world' applies the mesh transform.\nconst bb = mesh.geometry.bbox({ space: 'world' });\nconst sizeX = bb.max.x - bb.min.x;\nconst radius = Math.max(0.02, sizeX * 0.04);\n\n// 3. One sphere per vertex. mesh.verts.count = vertex total; mesh.verts\n//    .position(i) returns a Vec3 (.x / .y / .z) of the UNDEFORMED position.\nconst dots = [];\nfor (let i = 0; i < mesh.verts.count; i++) {\n  const p = mesh.verts.position(i);\n  const dot = await create.sphere({ radius, widthSegments: 12, heightSegments: 8 });\n  dot.translate.set([p.x, p.y, p.z]);\n  dots.push(dot);\n  select(null, { mode: 'clear' });\n}\n\n// 4. Group the markers and report (never a silent no-op). create.group is\n//    ASYNC -> await.\nconst group = await create.group(dots, { name: mesh.name + '_vertDots' });\nconsole.log('marked', mesh.verts.count, 'verts of', mesh.name,\n  'with radius', radius.toFixed(4), 'under group', group.name);\n",
      "expectedOutput": "console: vertex count + sphere radius used; viewport: a small sphere on each vertex, parented under one group."
    },
    {
      "kind": "recipe",
      "id": "mod-distance-between-parts",
      "title": "Measure the distance between two named parts",
      "description": "Build two named parts, look them up by name, read their world translates, and report the straight-line distance through MathLib.distance.",
      "intent": "mesh",
      "editor": "model",
      "category": "spatial",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.scene.ls",
        "MathLib.distance",
        "node.translate"
      ],
      "code": "// Self-contained from an EMPTY scene: build two named parts, position them,\n// then measure the straight-line distance by NAME lookup (not by id).\n\n// 1. Build the two parts off-origin.\nconst headBuilt = await create.cube({ width: 0.8, name: 'Head' });\nheadBuilt.translate.set([0, 3, 0]);\nselect(null, { mode: 'clear' });\nconst handBuilt = await create.cube({ width: 0.4, name: 'Hand' });\nhandBuilt.translate.set([1.5, 0.5, 0.5]);\n\n// 2. Re-resolve BY NAME so the recipe reads like real-world usage. ls accepts\n//    { type, name } where name is a string OR a RegExp (case-insensitive here).\nconst head = ls({ type: 'mesh', name: /head/i })[0];\nconst hand = ls({ type: 'mesh', name: /hand/i })[0];\n\n// 3. Read world translates and measure.\nconst a = head.translate.get();   // [x, y, z]\nconst b = hand.translate.get();\nconsole.log(head.name, 'at', JSON.stringify(a));\nconsole.log(hand.name, 'at', JSON.stringify(b));\n\n// 4. MathLib.distance(a, b) = sqrt of the summed squared component deltas.\nconst d = MathLib.distance(a, b);\nconsole.log('distance', head.name, '->', hand.name, '=', d.toFixed(4), 'units');\n",
      "expectedOutput": "console: per-part position + the distance in scene units."
    },
    {
      "kind": "recipe",
      "id": "mod-distribute-row",
      "title": "Distribute and bottom-align a row of selected nodes",
      "description": "Build a scattered row of cubes, evenly space them along X via node.distribute, then snap every Y to the minimum so the whole row sits on one baseline.",
      "intent": "mesh",
      "editor": "model",
      "category": "snap-align",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "node.distribute",
        "node.align",
        "node.translate"
      ],
      "code": "// Self-contained from an EMPTY scene: build a scattered row, then tidy it\n// with distribute (even X spacing) + align (shared minimum Y baseline).\n\n// 1. Build five cubes at deliberately uneven X + Y so the tidy-up is visible.\nconst xs = [0, 1.2, 5.5, 6.0, 9];\nconst ys = [0, 2.3, 0.7, 3.1, 1.4];\nconst nodes = [];\nfor (let i = 0; i < xs.length; i++) {\n  const c = await create.cube({ width: 0.5, name: 'Row' + i });\n  c.translate.set([xs[i], ys[i], 0]);\n  nodes.push(c);\n  select(null, { mode: 'clear' });\n}\n\n// 2. distribute(others, { axis, spacing? }): with spacing OMITTED it spaces\n//    the set evenly between the FIRST and LAST node (linear mode). Pass\n//    { axis: 'x', spacing: 2 } instead to force a fixed 2-unit step.\nnodes[0].distribute(nodes.slice(1), { axis: 'x' });\n\n// 3. align(others, { axis, mode }): mode 'min' | 'center' | 'max' collapses\n//    the chosen axis to the set's minimum / mean / maximum. 'min' on Y drops\n//    everything to a shared baseline.\nnodes[0].align(nodes.slice(1), { axis: 'y', mode: 'min' });\n\n// 4. Report the tidy result so it's never a silent no-op.\nfor (const n of nodes) {\n  const p = n.translate.get();\n  console.log(n.name, 'x=' + p[0].toFixed(3), 'y=' + p[1].toFixed(3));\n}\n",
      "expectedOutput": "console: per-node x / y after the pass; viewport: evenly spaced, bottom-aligned row."
    },
    {
      "kind": "recipe",
      "id": "mod-fill-character-with-spheres",
      "title": "Fill a character with spheres",
      "description": "Build a closed volume, then fill it with small spheres on a regular grid, keeping only the ones whose centre sits inside the surface.",
      "intent": "mesh",
      "editor": "model",
      "category": "spatial",
      "apiSurfaces": [
        "ModelEditor.create.sphere",
        "ModelEditor.create.group",
        "mesh.geometry.bbox",
        "mesh.geometry.pointInside",
        "node.translate"
      ],
      "code": "// Self-contained from an EMPTY scene: build a watertight volume, then pack\n// its interior with a grid of small spheres (keeping only inside hits).\n\n// 1. The volume to fill. A sphere is closed (watertight) so the inside test\n//    is well-defined.\nconst volume = await create.sphere({\n  radius: 1.5,           // 1.5 m radius (>0); default 1\n  widthSegments: 24,     // longitude divisions; default 32\n  heightSegments: 16,    // latitude divisions; default 16\n  name: 'Volume',\n});\nselect(null, { mode: 'clear' });\n\n// 2. Walk a regular grid over the volume's world-space bounding box.\n//    bbox / pointInside both live on the mesh.geometry namespace; space:\n//    'world' applies the volume's transform first ('object' = local coords).\nconst bb = volume.geometry.bbox({ space: 'world' });\nconst step = 0.6;          // grid spacing in scene units (smaller = denser)\nconst packed = [];\n\nfor (let x = bb.min.x; x <= bb.max.x; x += step) {\n  for (let y = bb.min.y; y <= bb.max.y; y += step) {\n    for (let z = bb.min.z; z <= bb.max.z; z += step) {\n      // Keep only grid points inside the closed surface.\n      if (volume.geometry.pointInside([x, y, z], { space: 'world' })) {\n        const dot = await create.sphere({ radius: step * 0.35 });\n        dot.translate.set([x, y, z]);\n        packed.push(dot);\n        select(null, { mode: 'clear' });\n      }\n    }\n  }\n}\n\n// 3. Parent the cluster under one group. create.group is ASYNC -> await.\nconst group = await create.group(packed, { name: volume.name + '_filled' });\n\n// 4. Report so it's never a silent no-op.\nconsole.log('packed', packed.length, 'spheres into', volume.name,\n  'under group', group.name);\n",
      "expectedOutput": "console: \"packed N spheres into <volume name>\"; viewport: a group of small spheres clustered inside the volume."
    },
    {
      "kind": "recipe",
      "id": "mod-measure-surface-area",
      "title": "Tally the surface area of every mesh",
      "description": "Walk every mesh in the asset, read mesh.area and mesh.bbox extents, and log a CSV summary you can copy out.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "mesh.area",
        "mesh.bbox"
      ],
      "code": "// CSV summary of mesh size and surface area.\nconst rows = ['name,area,bbX,bbY,bbZ'];\nlet total = 0;\n\nfor (const me of ls({ type: 'mesh' })) {\n  const bb = me.bbox();\n  const sx = bb.max.x - bb.min.x;\n  const sy = bb.max.y - bb.min.y;\n  const sz = bb.max.z - bb.min.z;\n  rows.push(me.name + ',' + me.area.toFixed(4) + ',' + sx.toFixed(3) + ',' + sy.toFixed(3) + ',' + sz.toFixed(3));\n  total += me.area;\n}\n\n// Print the CSV so you can copy it straight out of the console.\nconst csv = rows.join('\\n');\nconsole.log(csv);\nconsole.log('total:', rows.length - 1, 'meshes, total area', total.toFixed(4));\n",
      "expectedOutput": "A CSV block printed to the console (name,area,bbX,bbY,bbZ) plus a summary line with the total area."
    },
    {
      "kind": "recipe",
      "id": "mod-recolor-by-name",
      "title": "Assign a hash-of-name material color to every mesh",
      "description": "Build a few named meshes, then give each one a unique material whose base color comes from a hash of its name. Handy for telling parts apart in a screenshot.",
      "intent": "mesh",
      "editor": "model",
      "category": "materials-textures",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.create.material",
        "material.assignTo"
      ],
      "code": "// Self-contained from an EMPTY scene: build a few named meshes, then recolor\n// each one with a per-name hashed hue so they're easy to tell apart.\n\n// Helpers: stable hue from a string, then HSL -> linear RGBA.\nfunction hashHue(str) {\n  let h = 0;\n  for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) | 0;\n  return ((h % 360) + 360) % 360;   // 0..359 degrees\n}\nfunction hslToRgb(h, s, l) {\n  const c = (1 - Math.abs(2 * l - 1)) * s;\n  const hh = h / 60, x = c * (1 - Math.abs((hh % 2) - 1));\n  let r = 0, g = 0, b = 0;\n  if (hh < 1)      { r = c; g = x; }\n  else if (hh < 2) { r = x; g = c; }\n  else if (hh < 3) { g = c; b = x; }\n  else if (hh < 4) { g = x; b = c; }\n  else if (hh < 5) { r = x; b = c; }\n  else             { r = c; b = x; }\n  const m = l - c / 2;\n  return [r + m, g + m, b + m, 1];   // [r, g, b, a], each 0..1\n}\n\n// 1. Build three named cubes in a row.\nconst names = ['Torso', 'ArmLeft', 'ArmRight'];\nconst meshes = [];\nfor (let i = 0; i < names.length; i++) {\n  const c = await create.cube({ width: 0.8, name: names[i] });\n  c.translate.set([i * 1.5, 0, 0]);\n  meshes.push(c);\n  select(null, { mode: 'clear' });\n}\n\n// 2. Per mesh: build a material (ASYNC -> await) and assign it. create.material\n//    options: name, baseColor [r,g,b,a], metallic 0..1 (0 = dielectric),\n//    roughness 0..1 (0 = mirror, 1 = matte), emissive [r,g,b], emissiveIntensity.\nfor (const me of meshes) {\n  const rgba = hslToRgb(hashHue(me.name), 0.65, 0.55);\n  const mat = await create.material({\n    name: 'auto_' + me.name,\n    baseColor: rgba,\n    metallic: 0.1,            // mostly dielectric\n    roughness: 0.6,           // semi-matte\n    emissive: [0, 0, 0],      // no self-glow\n    emissiveIntensity: 0,\n  });\n  await mat.assignTo(me);     // bind the material to this mesh\n  console.log(me.name, '->', mat.name, 'rgb=' + rgba.slice(0, 3).map((v) => v.toFixed(2)).join(','));\n}\nconsole.log('recolored', meshes.length, 'meshes');\n",
      "expectedOutput": "console: per-mesh name + assigned material name; viewport: every mesh shows a distinct color."
    },
    {
      "kind": "recipe",
      "id": "mod-snap-x-onto-y",
      "title": "Snap the second selection onto the first, then verify",
      "description": "Use node.snapTo to copy the first selected node's world translate onto the second, then read both world translates back and assert they match within 1e-5.",
      "intent": "mesh",
      "editor": "model",
      "category": "snap-align",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "node.snapTo",
        "node.translate"
      ],
      "code": "// Self-contained from an EMPTY scene: build two cubes, snap one onto the\n// other, then read both world translates back and assert they coincide.\n\n// 1. Target cube, parked off-origin.\nconst target = await create.cube({ width: 1, name: 'Target' });\ntarget.translate.set([2.5, 1.3, -0.7]);   // [x, y, z] world position\nselect(null, { mode: 'clear' });\n\n// 2. Mover cube, somewhere else.\nconst mover = await create.cube({ width: 1, name: 'Mover' });\nmover.translate.set([-3, 0, 4]);\n\n// 3. Snap, capturing before / after. snapTo copies the WORLD translate only\n//    (rotate + scale unchanged) in one undo step.\nconst before = mover.translate.get();\nconsole.log('mover before:', JSON.stringify(before));\nmover.snapTo(target);\nconst after = mover.translate.get();\nconst want  = target.translate.get();\nconsole.log('mover after: ', JSON.stringify(after));\nconsole.log('target was:  ', JSON.stringify(want));\n\n// 4. Verify the world translate matches, and report (never a silent no-op).\nconst dx = after[0] - want[0], dy = after[1] - want[1], dz = after[2] - want[2];\nconst dist = Math.sqrt(dx*dx + dy*dy + dz*dz);\nconsole.log(dist < 1e-5 ? 'MATCH' : 'MISMATCH', 'residual delta=' + dist.toExponential(2));\n",
      "expectedOutput": "console: mover world translate before / after; final line \"MATCH\" or \"MISMATCH\" with the residual delta."
    },
    {
      "kind": "recipe",
      "id": "mod-uv-island-summary",
      "title": "Summarize the UV island layout on every mesh",
      "description": "Build a couple of meshes, then read mesh.uv.islands on each: log the island count and per-island UV-space size for a quick \"is this UV layout sane\" sweep.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.create.cylinder",
        "ModelEditor.scene.ls",
        "mesh.uv.islands"
      ],
      "code": "// Self-contained from an EMPTY scene: build a couple of auto-unwrapped\n// meshes, then report their UV-island layout.\n\n// 1. Two meshes with distinct default unwraps (a cube unwraps to 6 islands).\nconst box = await create.cube({ width: 1, name: 'BoxUV' });\nselect(null, { mode: 'clear' });\nconst tube = await create.cylinder({\n  radiusTop: 0.5,        // top cap radius (NO 'radius' param on cylinder)\n  radiusBottom: 0.5,     // bottom cap radius\n  height: 2,             // along Y\n  radialSegments: 16,    // sides around the circumference\n  heightSegments: 1,     // stacks along the height\n  openEnded: false,      // false = capped ends; true = open tube\n  name: 'TubeUV',\n});\nselect(null, { mode: 'clear' });\n\n// 2. Walk every mesh and read its islands. Each island carries:\n//    index, faceIndices[], loopIndices[], bounds { minU, maxU, minV, maxV }.\nlet totalIslands = 0;\nfor (const me of ls({ type: 'mesh' })) {\n  const islands = me.uv.islands;\n  totalIslands += islands.length;\n  console.log(me.name, '->', islands.length, 'UV island' + (islands.length === 1 ? '' : 's'));\n  islands.forEach((isl, i) => {\n    const b = isl.bounds;\n    const w = (b.maxU - b.minU).toFixed(3);   // UV-space width\n    const h = (b.maxV - b.minV).toFixed(3);   // UV-space height\n    console.log('  [' + i + '] ' + isl.faceIndices.length + ' faces, uvSize=' + w + 'x' + h);\n  });\n}\n\n// 3. Summary line so it's never a silent no-op.\nconsole.log('total UV islands across all meshes:', totalIslands);\n",
      "expectedOutput": "console: per-mesh island count; per-island index + UV size + face count."
    },
    {
      "kind": "recipe",
      "id": "mod-weight-paint-from-distance",
      "title": "Paint a deformer falloff from distance to a reference point",
      "description": "Build a mesh + a twist deformer, then write a 1/(1+d) per-vertex weight from each vertex to a reference marker so the deform fades out with distance. Demonstrates the per-vertex weight setter on a live deformer.",
      "intent": "mesh",
      "editor": "model",
      "category": "attributes",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.create.locator",
        "mesh.verts",
        "mesh.deformers.add",
        "node.translate",
        "deformer.setWeight"
      ],
      "code": "// Self-contained from an EMPTY scene: build a mesh + a twist deformer, then\n// paint its per-vertex envelope weights by inverse distance to a marker so\n// the twist fades out the further a vertex is from the marker.\n\n// 1. A subdivided cube so there are enough vertices to show a falloff.\nconst mesh = await create.cube({\n  width: 1, height: 3, depth: 1,\n  subdivisionsX: 2, subdivisionsY: 6, subdivisionsZ: 2,   // denser = smoother falloff\n  name: 'Limb',\n});\nselect(null, { mode: 'clear' });\n\n// 2. A reference marker the weight falls off from (create.locator is SYNC).\nconst ref = create.locator({ name: 'WeightSource', position: [0, 1.5, 0] });\nconst refPos = ref.translate.get();   // [x, y, z]\n\n// 3. Add a twist deformer. deformers.add(type, opts) is ASYNC -> await;\n//    'nonLinear' needs { mode: 'bend' | 'twist' | 'taper' | 'squash' |\n//    'sine' | 'wave' }. The returned handle exposes per-vertex weights.\nconst twist = await mesh.deformers.add('nonLinear', { mode: 'twist' });\n\n// 4. Paint inverse-distance weights: w = 1 / (1 + d). setWeight(i, w) writes\n//    ONE vertex per undo entry (full-buffer assignment is rejected; use\n//    setWeights(fn) for a single-undo bulk write instead).\nlet minW = Infinity, maxW = -Infinity;\nfor (let i = 0; i < mesh.verts.count; i++) {\n  const p = mesh.verts.position(i);   // Vec3, undeformed base position\n  const dx = p.x - refPos[0], dy = p.y - refPos[1], dz = p.z - refPos[2];\n  const d = Math.sqrt(dx * dx + dy * dy + dz * dz);\n  const w = 1 / (1 + d);              // 1 at the marker, fading toward 0\n  twist.setWeight(i, w);\n  if (w < minW) minW = w;\n  if (w > maxW) maxW = w;\n}\n\n// 5. Prove the weights drive the deform: compare deformed vs base positions\n//    and report the peak per-vertex displacement (never a silent no-op).\nconst deformed = mesh.deformers.evaluatedPositions();\nconst base = mesh.deformers.basePositions();\nlet peak = 0;\nfor (let i = 0; i < base.length; i += 3) {\n  const dx = deformed[i] - base[i];\n  const dy = deformed[i + 1] - base[i + 1];\n  const dz = deformed[i + 2] - base[i + 2];\n  peak = Math.max(peak, Math.sqrt(dx * dx + dy * dy + dz * dz));\n}\nconsole.log('painted', mesh.verts.count, 'verts;',\n  'weight range [' + minW.toFixed(3) + ', ' + maxW.toFixed(3) + '];',\n  'peak deform delta', peak.toFixed(4), 'm');\n",
      "expectedOutput": "console: vertex count painted + min/max weight + a non-zero peak deform delta proving the weights drive the deform."
    },
    {
      "kind": "recipe",
      "id": "model-bevel-chamfer-tower",
      "title": "Three cubes with varied bevel strategies (vertex / flat-edge / rounded-edge)",
      "description": "Spawn three cubes; bevelVertices on one, a single-segment (flat) edge bevel on another, a multi-segment (rounded) edge bevel on the third; log vert/face deltas.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "mesh.bevelVertices",
        "mesh.bevelEdges",
        "mesh.verts",
        "mesh.faces",
        "mesh.edges",
        "node.translate",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll"
      ],
      "code": "// Three cubes, three corner-rounding strategies, side by side. Each uses the\n// component-selector surface: mesh.verts([...]) / mesh.edges([...]) build a\n// component handle, then a mutator runs on exactly those indices. Bevel is the\n// single canonical edge round-off: a 1-segment bevel is a flat chamfer-style\n// cut, a multi-segment bevel is a smooth rounded fillet-style curve.\nlet x = -3;\n\n// 1) VERTEX bevel — round two corners into small faces. verts(...).bevel\n//    accepts { width, offset } and is SINGLE-SEGMENT by design (no segments).\nconst cube1 = await create.cube({ width: 1, height: 1, depth: 1, name: 'BevelVerts' });\ncube1.translate.set([x, 1.3, -0.7]);\nconst b1 = { v: cube1.verts.count, f: cube1.faces.count };\nawait cube1.verts([0, 1]).bevel({ width: 0.08 }); // width = corner cut, meters\nconsole.log('verts.bevel: V ' + b1.v + '->' + cube1.verts.count + ', F ' + b1.f + '->' + cube1.faces.count);\nawait select(null, { mode: 'clear' });\nx += 2.5;\n\n// 2) EDGE bevel, FLAT — a single-segment edge bevel replaces each edge with a\n//    flat strip (the classic chamfer). edges(...).bevel takes { width, segments }.\nconst cube2 = await create.cube({ width: 1, height: 1, depth: 1, name: 'FlatBevelEdges' });\ncube2.translate.set([x, 1.3, -0.7]);\nconst b2 = { v: cube2.verts.count, f: cube2.faces.count };\nawait cube2.edges([0, 1, 2, 3]).bevel({ width: 0.1, segments: 1 }); // 1 segment = flat strip\nconsole.log('edges.bevel(flat): V ' + b2.v + '->' + cube2.verts.count + ', F ' + b2.f + '->' + cube2.faces.count);\nawait select(null, { mode: 'clear' });\nx += 2.5;\n\n// 3) EDGE bevel, ROUNDED — a multi-segment edge bevel rounds each edge into a\n//    smooth curve (the classic fillet). More segments = tighter, smoother curve.\nconst cube3 = await create.cube({ width: 1, height: 1, depth: 1, name: 'RoundBevelEdges' });\ncube3.translate.set([x, 1.3, -0.7]);\nconst b3 = { v: cube3.verts.count, f: cube3.faces.count };\nawait cube3.edges([0, 1, 2, 3]).bevel({ width: 0.1, segments: 3 }); // 3 segments = rounded\nconsole.log('edges.bevel(round): V ' + b3.v + '->' + cube3.verts.count + ', F ' + b3.f + '->' + cube3.faces.count);\nawait select(null, { mode: 'clear' });\n\nawait Utils.wait.frame();\nviewport.frameAll(1.5); // paddingFactor — frame all three cubes\nconsole.log('bevel comparison built (vertex / flat-edge / rounded-edge)');\n",
      "expectedOutput": "console: per-cube before/after V/F counts."
    },
    {
      "kind": "recipe",
      "id": "model-bevel-extrude-tower",
      "title": "Extrude a tall tower and bevel its top edges",
      "description": "Take a 1m cube, extrude the top face upward to make a tower, then bevel every edge on the top so it has a soft chamfer.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "mesh.extrude",
        "mesh.bevel",
        "mesh.faces",
        "mesh.verts",
        "mesh.bbox"
      ],
      "code": "// Build a tower by extruding the top face upward, then bevel its top edges.\n// Self-contained from an empty scene.\n\n// 1) Seed primitive: a 1m cube. create.cube is ASYNC.\nconst tower = await create.cube({ width: 1, height: 1, depth: 1, name: 'Tower' });\nconsole.log('start: V=' + tower.verts.count, 'F=' + tower.faces.count);\n\n// 2) Extrude the top face up 4 meters. The component selector key is 'faces'\n//    (a face-index array). On a fresh cube the face order is +Y, -Y, +X, -X,\n//    +Z, -Z, so index 0 is the top (+Y) face.\n//    - faces: explicit face index array (no current-selection assumption)\n//    - distance: extrusion length along the face normal, meters\nawait tower.extrude({ faces: [0], distance: 4 });\nconsole.log('after extrude up 4m: F=' + tower.faces.count);\n\n// Read the WORLD height after extrusion (geometry namespace).\nconst midHeight = tower.geometry.bbox({ space: 'world' }).size.y;\nconsole.log('mid-step height:', midHeight.toFixed(3) + 'm');\n\n// 3) Bevel the four edges around the top face to soften the silhouette.\n//    - edges: edge indices to round into a strip\n//    - width: bevel offset, meters (smaller = tighter chamfer)\n//    - segments: cross-cuts across the bevel (1 = flat chamfer, >1 = rounder)\nawait tower.bevel({ edges: [0, 1, 2, 3], width: 0.05, segments: 2 });\nconsole.log('after bevel: V=' + tower.verts.count, 'F=' + tower.faces.count);\n\nconst finalHeight = tower.geometry.bbox({ space: 'world' }).size.y;\nconsole.log('final tower \"' + tower.name + '\" height:', finalHeight.toFixed(3) + 'm');\n",
      "expectedOutput": "console: face counts after each step + final bbox height."
    },
    {
      "kind": "recipe",
      "id": "model-boolean-difference",
      "title": "Cut a hole through a cube with a sphere",
      "description": "Build a cube, position a smaller sphere inside it, run a difference boolean, and log the resulting vertex and face counts.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.create.sphere",
        "mesh.boolean",
        "mesh.verts",
        "mesh.faces",
        "mesh.bbox",
        "node.translate"
      ],
      "code": "// Cut a hole through a cube with a sphere — difference boolean.\n// A boolean needs TWO meshes, so build both up front.\n\n// 1) The solid being carved.\nconst cube = await create.cube({\n  width: 2, height: 2, depth: 2,  // meters\n  name: 'Cube',\n});\n\n// 2) The cutter. Make it a smooth sphere so the carved hole reads cleanly.\nconst sphere = await create.sphere({\n  radius: 1,            // meters\n  widthSegments: 32,    // longitude rings (default 16) — smoother cut wall\n  heightSegments: 24,   // latitude rings (default 12)\n  name: 'Cutter',\n});\n\n// 3) Slide the cutter half-way out the top (+Y) face so the cut breaks the\n//    surface rather than carving a fully enclosed cavity.\nsphere.translate.set([0, 1, 0]);\n\nconsole.log('cube before:    V=' + cube.verts.count, 'F=' + cube.faces.count);\nconsole.log('cutter sphere:  V=' + sphere.verts.count, 'F=' + sphere.faces.count);\n\n// Health + size sanity check BEFORE the cut (geometry namespace).\nconst before = cube.geometry.bbox({ space: 'world' });\nconsole.log('cube height before cut:', before.size.y.toFixed(2) + 'm',\n  '| closed=' + cube.geometry.isClosed, '| manifold=' + cube.geometry.isManifold);\n\n// 4) Subtract: a.boolean(b, 'difference') = a − b. Arguments are POSITIONAL —\n//    mode is one of 'union' | 'difference' | 'intersection'. The 'cube' handle\n//    is updated in place; the cutter sphere is consumed by the operation.\nawait cube.boolean(sphere, 'difference');\n\nconst after = cube.geometry;\nconsole.log('cube after cut: V=' + cube.verts.count, 'F=' + cube.faces.count);\nconsole.log('carved cube \"' + cube.name + '\"',\n  '| closed=' + after.isClosed, '| holes=' + after.holes, '| shells=' + after.shells);\n",
      "expectedOutput": "console: cube counts before / after the cut, plus the final face count of the carved cube."
    },
    {
      "kind": "recipe",
      "id": "model-bounding-box-math",
      "title": "Build, query, expand, transform, and compare bounding boxes",
      "description": "Pull a mesh AABB, then construct boxes via from / fromCenterAndSize / fromPoints / empty. Read every property (center / min / max / size / width / height / depth / isEmpty), exercise expand / expandBox / clone / transformedBy, and run contains / intersects / equals predicates.",
      "intent": "mesh",
      "editor": "model",
      "category": "spatial",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "mesh.bbox",
        "node.translate",
        "boundingBox.from",
        "boundingBox.fromCenterAndSize",
        "boundingBox.fromPoints",
        "boundingBox.empty",
        "boundingBox.center",
        "boundingBox.min",
        "boundingBox.max",
        "boundingBox.size",
        "boundingBox.width",
        "boundingBox.height",
        "boundingBox.depth",
        "boundingBox.isEmpty",
        "boundingBox.expand",
        "boundingBox.expandBox",
        "boundingBox.clone",
        "boundingBox.equals",
        "boundingBox.contains",
        "boundingBox.intersects",
        "boundingBox.transformedBy",
        "boundingBox.toArray"
      ],
      "code": "// Build, query, expand, transform, and compare bounding boxes. Pull a real\n// mesh AABB, then construct boxes via every static factory and exercise the\n// full property + op + predicate surface. Self-contained from empty.\n\nconst cube = await create.cube({ width: 2, height: 1, depth: 0.5, name: 'BBoxCube' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nawait Utils.wait.frame();\n\n// MESH AABB — bbox lives under mesh.geometry; world space applies the\n// node transform. min/max/center/size are Vec3 (each has .toArray()).\nconst bb = cube.geometry.bbox({ space: 'world' });\nconsole.log('bb.min:', bb.min.toArray(), 'bb.max:', bb.max.toArray());\nconsole.log('bb.center:', bb.center.toArray(), 'bb.size:', bb.size.toArray());\nconsole.log('bb.width:', bb.width, 'bb.height:', bb.height, 'bb.depth:', bb.depth);\nconsole.log('bb.isEmpty:', bb.isEmpty);\n\n// STATIC FACTORIES — from(min,max) / fromCenterAndSize / fromPoints / empty.\nconst a = MathLib.BoundingBox.from([0, 0, 0], [1, 2, 3]);              // explicit min/max\nconst b = MathLib.BoundingBox.fromCenterAndSize([5, 0, 0], [2, 2, 2]); // center + extents\nconst c = MathLib.BoundingBox.fromPoints([                            // tight AABB over points\n  [0, 0, 0], [1, 1, 1], [-1, 0.5, -0.5],\n]);\nconst e = MathLib.BoundingBox.empty();                                // inverted/empty sentinel\n\nconsole.log('a.toArray:', JSON.stringify(a.toArray())); // { min, max }\nconsole.log('b.center:', b.center.toArray(), 'b.size:', b.size.toArray());\nconsole.log('c.min:', c.min.toArray(), 'c.max:', c.max.toArray());\nconsole.log('e.isEmpty:', e.isEmpty); // true\n\n// OPS — every mutator returns a NEW box (BoundingBox is immutable).\nconst expanded = a.expand([3, 3, 3]);    // grow to include a point\nconsole.log('a.expand([3,3,3]).max:', expanded.max.toArray());\nconst merged = a.expandBox(b);           // union of two boxes\nconsole.log('a.expandBox(b) min/max:', merged.min.toArray(), merged.max.toArray());\nconst cloned = a.clone();\nconsole.log('a.clone().equals(a):', cloned.equals(a)); // true\n\n// PREDICATES.\nconsole.log('a.contains([0.5,1,1.5]):', a.contains([0.5, 1, 1.5])); // true (inside)\nconsole.log('a.intersects(b):', a.intersects(b));                   // false (disjoint)\n\n// TRANSFORM — by a column-major 4x4 matrix; here a +5 translate along X.\nconst moved = a.transformedBy([\n  1, 0, 0, 0,\n  0, 1, 0, 0,\n  0, 0, 1, 0,\n  5, 0, 0, 1,\n]);\nconsole.log('a.transformedBy(+5x).min:', moved.min.toArray(), 'max:', moved.max.toArray());\nconsole.log('bounding-box math complete.');\n",
      "expectedOutput": "console: mesh AABB + 4 static-factory boxes + every property + expand / expandBox / clone results + contains / intersects predicates + transformedBy result."
    },
    {
      "kind": "recipe",
      "id": "model-deformation-ops-sweep",
      "title": "Deformation ops sweep: smooth / pushPull / flatten / flattenToPlane / toSphere / shear / shrinkFatten",
      "description": "Spawn seven subdivided cubes; apply one deformation op to each; log V counts.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.smooth",
        "mesh.pushPull",
        "mesh.flatten",
        "mesh.flattenToPlane",
        "mesh.toSphere",
        "mesh.shear",
        "mesh.shrinkFatten",
        "mesh.verts"
      ],
      "code": "// Sweep seven whole-mesh deformation OPS (these are geometry edits, not\n// stacked deformers): smooth / pushPull / flatten / flattenToPlane /\n// toSphere / shear / shrinkFatten. One subdivided cube each, every option\n// set to a non-default value, V-count logged. Self-contained from empty.\nconst subs = { subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3 }; // 4x4x4 verts/face\nconst offsets = [\n  [-7.5, 1.3, -0.7], [-5, 1.3, -0.7], [-2.5, 1.3, -0.7], [0, 1.3, -0.7],\n  [2.5, 1.3, -0.7], [5, 1.3, -0.7], [7.5, 1.3, -0.7],\n];\n\n// 1) smooth — Laplacian relax. With no verts/faces/edges key the WHOLE mesh is\n//    smoothed (there is NO mesh.smoothVertices; the verb is mesh.smooth).\n//    - iterations: number of passes (default 1; higher = softer)\n//    - strength:   per-pass blend [0,1] (default 0.5; 1 = full move to average)\nconst a = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'Smooth' });\n// NOTE: the mesh-surface ops below (pushPull / flatten / flattenToPlane /\n// toSphere / shear / shrinkFatten) act on the IN-SCOPE mesh and throw\n// \"no mesh in scope\" if nothing is selected — so we scene.select(<mesh>)\n// before each one (NOT clear). smooth() takes the handle directly, but we\n// select it too for a uniform pattern.\na.xform({ t: offsets[0] }); await select(a);\nawait a.smooth({ iterations: 4, strength: 0.6 }); // whole-mesh relax, 4 passes\n\n// 2) pushPull — move verts along their normals (omit 'verts' = all verts).\nconst b = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'PushPull' });\nb.xform({ t: offsets[1] }); await select(b);\nawait b.pushPull({ distance: 0.2 }); // inflate 0.2 scene units along normals\n\n// 3) flatten — collapse verts onto an auto-detected best-fit plane.\nconst c = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'Flatten' });\nc.xform({ t: offsets[2] }); await select(c);\nawait c.flatten({ plane: 'y' }); // flatten along Y ('auto' | 'x' | 'y' | 'z')\n\n// 4) flattenToPlane — collapse onto an explicit origin+normal plane.\nconst d = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'FlattenToPlane' });\nd.xform({ t: offsets[3] }); await select(d);\nawait d.flattenToPlane({ planeOrigin: [0, 0, 0], planeNormal: [0, 1, 0] }); // XZ plane\n\n// 5) toSphere — spherify verts. factor 0..1 blends cube -> sphere.\nconst e = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'ToSphere' });\ne.xform({ t: offsets[4] }); await select(e);\nawait e.toSphere({ factor: 0.5 }); // half-way to a sphere\n\n// 6) shear — slant verts along an axis. amount = shear factor.\nconst f = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'Shear' });\nf.xform({ t: offsets[5] }); await select(f);\nawait f.shear({ amount: 0.3, axis: 'x' }); // slant along X\n\n// 7) shrinkFatten — offset verts along normals (like pushPull, signed).\nconst g = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'ShrinkFatten' });\ng.xform({ t: offsets[6] }); await select(g);\nawait g.shrinkFatten({ offset: 0.1 }); // fatten 0.1 units (negative shrinks)\n\nconst all = [a, b, c, d, e, f, g];\nfor (const m of all) console.log(m.name + ': V=' + m.verts.count);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('deformation ops sweep complete (7 ops).');\n",
      "expectedOutput": "console: per-op verts before/after."
    },
    {
      "kind": "recipe",
      "id": "model-distribute-spheres",
      "title": "Place spheres on a line and distribute them evenly",
      "description": "Spawn five small spheres, pin the first and last to fixed anchors, then call node.distribute to space the middle three evenly between them.",
      "intent": "mesh",
      "editor": "model",
      "category": "snap-align",
      "apiSurfaces": [
        "ModelEditor.create.sphere",
        "Node.distribute",
        "ModelEditor.create.group",
        "mesh.bbox",
        "node.translate"
      ],
      "code": "// Spawn five small spheres, anchor the two ends, and let node.distribute\n// space the middle three evenly between them. Self-contained from empty.\n// Run in meter-mode for the cleanest spacing (cm/mm grids apply an extra\n// unit conversion through distribute, so world spacing reads differently).\nconst spheres = [];\nfor (let i = 0; i < 5; i++) {\n  // create.sphere is async. widthSegments/heightSegments tune the tessellation.\n  const s = await create.sphere({\n    radius: 0.2,         // small beads\n    widthSegments: 16,   // longitudinal segments (default 32)\n    heightSegments: 8,   // latitudinal segments (default 16)\n    name: 'Bead_' + i,\n  });\n  await select(null, { mode: 'clear' });\n  spheres.push(s);\n}\n\n// Anchor the first bead at x=0 and the last at x=8. The middle three sit at\n// the origin until distribute slides them into place.\nspheres[0].xform({ t: [0, 0, 0] });\nspheres[4].xform({ t: [8, 0, 0] });\n\n// Linear distribute: the anchor (this) + the LAST of 'others' stay fixed,\n// everything in between is spaced evenly along the axis. Pass the other four\n// beads as a positional array; omitting 'spacing' selects linear mode.\nspheres[0].distribute(spheres.slice(1), { axis: 'x' });\n\n// Read back each bead's world-space center X (bbox lives under .geometry).\nfor (const s of spheres) {\n  const b = s.geometry.bbox({ space: 'world' });\n  const cx = ((b.min.x + b.max.x) / 2).toFixed(2);\n  console.log(s.name, 'x =', cx);\n}\n\n// create.group(nodes, opts) is async — pass the bead array positionally.\nconst group = await create.group(spheres, { name: 'BeadRow' });\nconsole.log('grouped', spheres.length, 'beads under', group.name);\n",
      "expectedOutput": "console: each sphere's final world X position; outliner: 5 spheres under one group."
    },
    {
      "kind": "recipe",
      "id": "model-duplicate-instance-pivot-visibility",
      "title": "Duplicate vs instance, recenter pivot, align axis, toggle visibility",
      "description": "Spawn a base cube, deep-clone it (independent geometry), make a sibling instance (shared geometry), recenter the pivot, align Y to the base, then hide / show / toggle / unlock.",
      "intent": "mesh",
      "editor": "model",
      "category": "outliner",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "node.duplicate",
        "node.instance",
        "node.centerPivot",
        "node.align",
        "node.show",
        "node.hide",
        "node.toggleVisibility",
        "node.isHidden",
        "node.unlock"
      ],
      "code": "// Compare duplicate (independent geometry) vs instance (shared geometry),\n// recenter a pivot, align by axis, then cycle visibility + lock. All these\n// Node ops are async. Self-contained from an empty Model scene.\n\nconst base = await create.cube({ width: 1, height: 1, depth: 1, name: 'BaseCube' });\nbase.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nawait Utils.wait.frame();\n\n// DEEP CLONE — duplicate() makes an independent RawMesh; geometry edits to\n// the clone do NOT propagate back to base. It rejects an 'offset' opt, so\n// move it AFTER duplicating.\nconst dup = await base.duplicate({ name: 'BaseCube_clone' });\ndup.xform({ t: [5.0, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\n\n// SIBLING INSTANCE — instance() shares the SAME meshId as base; geometry\n// edits to either propagate to both.\nconst inst = await base.instance();\ninst.xform({ t: [7.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\n\n// PIVOT — recenter base's pivot to its bounding-box center.\nawait base.centerPivot();\n\n// ALIGN — snap dup's Y to base's Y. align takes a POSITIONAL array of\n// targets plus { axis, mode }. With a single target, mode picks which edge\n// to match: 'min' | 'center' | 'max'.\ndup.align([base], { axis: 'y', mode: 'center' });\n\n// VISIBILITY CYCLE — hide -> isHidden -> toggle -> show (each async).\nawait inst.hide();\nconsole.log('inst hidden after hide():', inst.isHidden);          // true\nawait inst.toggleVisibility();\nconsole.log('inst hidden after toggleVisibility():', inst.isHidden); // false\nawait inst.show();\nconsole.log('inst hidden after show():', inst.isHidden);          // false\n\n// LOCK — unlock is a no-op when the node is not locked.\nawait dup.unlock();\nconsole.log('dup isLocked after unlock:', dup.isLocked); // false\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('duplicate / instance / pivot / align / visibility cycle complete.');\n",
      "expectedOutput": "console: visibility flag at each transition + lock state; viewport: 3 cubes (base, deep-clone, instance) framed."
    },
    {
      "kind": "recipe",
      "id": "model-edge-op-toolkit",
      "title": "Edge op toolkit: insertEdgeLoop (x2) + edges().bevel + merge({ distance })",
      "description": "Add parallel edge loops, bevel an edge into supporting loops, add more loops, then weld doubles by distance. (offsetEdgeLoops and loopCut are interactive-tool-only / not-yet-wired — they throw from a plain script — so both loop steps use the scriptable insertEdgeLoop + edge bevel instead.)",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.insertEdgeLoop",
        "mesh.edges().bevel",
        "mesh.merge",
        "mesh.verts",
        "mesh.edges"
      ],
      "code": "// Edge toolkit on a subdivided cube. Every loop op runs HEADLESS when given\n// an explicit edge selection — pass 'edges: [...]' so no viewport hover is\n// required. Self-contained from an empty scene.\nconst cube = await create.cube({\n  width: 2, height: 2, depth: 2,\n  subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n  name: 'EdgeOpCube',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nawait select(null, { mode: 'clear' });\nconsole.log('start: V=' + cube.verts.count + ' E=' + cube.edges.count);\n\n// 1) insertEdgeLoop — slice fresh loops across the given edges.\n//    - edges: edge indices the loop passes through (required for headless)\n//    - cuts:  number of parallel loops to insert (default 1)\nawait cube.insertEdgeLoop({ edges: [0], cuts: 2 });\nconsole.log('after insertEdgeLoop x2: V=' + cube.verts.count + ' E=' + cube.edges.count);\n\n// 2) Add SUPPORT LOOPS around an edge. NOTE: mesh.offsetEdgeLoops is an\n//    INTERACTIVE-tool-only operation — calling it from a plain script throws\n//    \"'mesh.offsetEdgeLoops' is not available in this editor.\" The scriptable\n//    way to flank an edge with extra loops is the edge-selection BEVEL verb,\n//    which runs headless from an explicit selection.\n//    mesh.edges([...]).bevel takes { width, segments } (strict):\n//    - width:    chamfer size, meters\n//    - segments: how many loops across the bevel (>1 = rounded support loops)\nawait cube.edges([4, 7]).bevel({ width: 0.05, segments: 2 });\nconsole.log('after edges.bevel (support loops): V=' + cube.verts.count + ' E=' + cube.edges.count);\n\n// 3) A SECOND insertEdgeLoop on a different edge. NOTE: mesh.loopCut is a\n//    planned op that is NOT yet wired — calling it from a script throws\n//    \"mesh.loopCutEdge: underlying edge->loop expansion primitive not yet\n//    wired\". The scriptable way to add more parallel loops today is another\n//    insertEdgeLoop on the target edge.\nawait cube.insertEdgeLoop({ edges: [2], cuts: 1 });\nconsole.log('after 2nd insertEdgeLoop: V=' + cube.verts.count + ' E=' + cube.edges.count);\n\n// 4) Weld doubles by distance. The geometry weld lives on mesh.merge({ distance })\n//    (there is NO top-level mesh.mergeByDistance — that name is the UV-island\n//    helper under mesh.uv). 'distance' is the weld threshold in meters.\nawait cube.merge({ distance: 0.001 });\nconsole.log('after merge({ distance }): V=' + cube.verts.count + ' E=' + cube.edges.count);\n\nawait Utils.wait.frame();\nviewport.frameAll(1.5);\nconsole.log('edge toolkit complete on \"' + cube.name + '\": V=' + cube.verts.count + ' E=' + cube.edges.count);\n",
      "expectedOutput": "console: V/E counts before / after each op."
    },
    {
      "kind": "recipe",
      "id": "model-face-op-chain",
      "title": "Face op chain: faces().inset -> faces().extrudeIndividual -> faces().extrude -> faces().delete",
      "description": "On a subdivided cube, inset a face set, extrude each face individually, extrude the set together, then delete a face to open a hole. Every op is a face-selection verb (mesh.faces([...]).<verb>) and every step is wired headless; V/F deltas logged per step.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.faces().inset",
        "mesh.faces().extrudeIndividual",
        "mesh.faces().extrude",
        "mesh.faces().delete",
        "mesh.verts",
        "mesh.faces"
      ],
      "code": "// Real multi-step FACE-OP chain on a subdivided cube. Every edit is a\n// face-selection verb: mesh.faces([...]).<verb>(...). There is NO\n// mesh.extrudeIndividualFaces / mesh.inset method on the bare handle — the\n// face SELECTION object (mesh.faces([...])) is what exposes inset / extrude /\n// extrudeIndividual / delete. (Some face verbs — bevel / bridge / fill — are\n// interactive-tool-only and throw from a plain script, so this chain uses only\n// the headless-wired verbs.) Self-contained from empty; each step logs the V/F\n// delta. No reliance on a prior viewport selection.\nconst cube = await create.cube({\n  width: 2, height: 2, depth: 2,\n  subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3, // dense grid for headroom\n  name: 'FaceChain',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nawait select(null, { mode: 'clear' });\nconsole.log('start: V=' + cube.verts.count + ' F=' + cube.faces.count);\n\n// 1) INSET a set of faces — shrink each face inward, adding a border ring.\n//    - amount: inward shrink, meters (default 0.2)\n//    - depth:  along-normal offset of the inset ring, meters (0 = flat, default 0)\nawait cube.faces([0, 1, 2, 3]).inset({ amount: 0.05, depth: 0.02 });\nconsole.log('after inset: V=' + cube.verts.count + ' F=' + cube.faces.count);\n\n// 2) EXTRUDE INDIVIDUAL — pull each inset face out along ITS OWN normal so the\n//    four faces become four separate caps (not one merged block). The op key\n//    is 'distance' (meters along each face normal).\nawait cube.faces([0, 1, 2, 3]).extrudeIndividual({ distance: 0.12 });\nconsole.log('after extrudeIndividual: V=' + cube.verts.count + ' F=' + cube.faces.count);\n\n// 3) EXTRUDE the set TOGETHER — pull all four faces out as one connected block\n//    along their averaged normal (group extrude, distinct from the per-face\n//    extrudeIndividual above). Key is 'distance' (meters).\nawait cube.faces([0, 1, 2, 3]).extrude({ distance: 0.1 });\nconsole.log('after extrude: V=' + cube.verts.count + ' F=' + cube.faces.count);\n\n// 4) DELETE a face — open a hole in the mesh (the headless face-removal verb).\n//    deleteFaces consumes the selection's face indices; no options.\nawait cube.faces([18]).delete();\nconsole.log('after delete (hole opened): V=' + cube.verts.count + ' F=' + cube.faces.count);\n\nawait Utils.wait.frame();\nviewport.frameAll(1.5);\nconsole.log('face-op chain complete on \"' + cube.name + '\": V=' + cube.verts.count + ' F=' + cube.faces.count);\n",
      "expectedOutput": "console: V/F counts before/after each op."
    },
    {
      "kind": "recipe",
      "id": "model-hierarchy-reorder",
      "title": "Reorder, reparent, unparent, and delete sibling nodes",
      "description": "Spawn three sibling cubes, swap their order with moveBefore / moveAfter / moveUp / moveDown, group two of them, then unparent and delete.",
      "intent": "mesh",
      "editor": "model",
      "category": "outliner",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.scene.ls",
        "node.translate",
        "node.moveBefore",
        "node.moveAfter",
        "node.moveUp",
        "node.moveDown",
        "node.unparent",
        "node.delete"
      ],
      "code": "// Spawn three sibling cubes, reshuffle their sibling order, group two of\n// them, unparent one, and delete one. All structural Node ops are async.\n// Self-contained from an empty Model scene.\nconst a = await create.cube({ width: 1, height: 1, depth: 1, name: 'OrderA' });\na.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nconst b = await create.cube({ width: 1, height: 1, depth: 1, name: 'OrderB' });\nb.xform({ t: [5.0, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nconst c = await create.cube({ width: 1, height: 1, depth: 1, name: 'OrderC' });\nc.xform({ t: [7.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nawait Utils.wait.frame();\n\nconst printOrder = (label) => {\n  const names = ls({ type: 'mesh' })\n    .filter((n) => /^Order/.test(n.name))\n    .map((n) => n.name).join(', ');\n  console.log(label + ':', names);\n};\nprintOrder('initial');\n\n// moveBefore / moveAfter — anchor relative to a named sibling.\nawait b.moveBefore(a);\nprintOrder('after b.moveBefore(a)');\nawait c.moveAfter(b);\nprintOrder('after c.moveAfter(b)');\n\n// moveUp / moveDown — single-step bumps; no-op at the list boundaries.\nawait a.moveUp();\nprintOrder('after a.moveUp');\nawait b.moveDown();\nprintOrder('after b.moveDown');\n\n// Group two cubes (empty group via [] then reparent), then unparent one.\nconst grp = await create.group([], { name: 'OrderGroup' });\nawait a.setParent(grp);\nawait b.setParent(grp);\nconsole.log('grp children:', grp.children().map((n) => n.name).join(', '));\nawait a.unparent();\nconsole.log('a parent after unparent:', a.parent()?.name ?? '(root)');\n\n// Delete C — its handle goes stale; name lookups return undefined afterward.\nawait c.delete();\nconst remaining = ls({ type: 'mesh' }).filter((n) => /^Order/.test(n.name)).length;\nconsole.log('after c.delete remaining Order* meshes:', remaining);\n",
      "expectedOutput": "console: sibling order after each reorder + group children + post-unparent parent + post-delete count."
    },
    {
      "kind": "recipe",
      "id": "model-imperative-builder",
      "title": "Imperative mesh builder: appendVertex + appendEdge + appendFace + bulkAppend + beginBatch",
      "description": "Start from a polyPlane, append two verts, an edge, a face; run bulkAppend; demo beginBatch for coalesced authoring.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.plane",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.appendVertex",
        "mesh.appendEdge",
        "mesh.appendFace",
        "mesh.bulkAppend",
        "mesh.beginBatch",
        "mesh.verts",
        "mesh.edges",
        "mesh.faces"
      ],
      "code": "// Build meshes from raw vertex + face data. The customer-facing construction\n// entry points are create.buildMesh / .fromVertsAndFaces (the old\n// per-element appendVertex / appendFace / bulkAppend builders are now internal).\n// Two topology shapes are accepted and are mutually exclusive:\n//   - N-gon meshes:  faceVertexCounts + faceVertexIndices\n//   - Triangle mesh: indices\n\n// 1) A square pyramid as an N-GON mesh: 1 quad base (count 4) + 4 side tris\n//    (count 3 each). Positions are in METERS; one entry per vertex.\nconst pyramid = await create.buildMesh({\n  positions: [\n    [-0.5, 0, -0.5], // 0 base corner\n    [ 0.5, 0, -0.5], // 1\n    [ 0.5, 0,  0.5], // 2\n    [-0.5, 0,  0.5], // 3\n    [ 0,   1,  0],   // 4 apex\n  ],\n  // faceVertexCounts[i] = how many verts face i has; faceVertexIndices is the\n  // flat concatenation of each face's vertex indices, in order.\n  faceVertexCounts: [4, 3, 3, 3, 3],          // base quad, then 4 tris\n  faceVertexIndices: [\n    0, 1, 2, 3, // base (quad)\n    0, 1, 4,    // side\n    1, 2, 4,    // side\n    2, 3, 4,    // side\n    3, 0, 4,    // side\n  ],\n  name: 'Pyramid',\n});\npyramid.translate.set([1.5, 0, -0.7]);\nconsole.log('pyramid: V=' + pyramid.verts.count + ' F=' + pyramid.faces.count\n  + ' closed=' + pyramid.geometry.isClosed);\n\n// 2) A single TRIANGLE via the legacy 'indices' form (flat or nested), with\n//    explicit per-vertex normals + a name. fromVertsAndFaces is an alias of\n//    buildMesh — same options bag, same result.\nconst tri = await create.fromVertsAndFaces({\n  positions: [[0, 0, 0], [1, 0, 0], [0, 1, 0]], // 3 verts, meters\n  indices: [[0, 1, 2]],                          // one triangle (nested form)\n  normals: [[0, 0, 1], [0, 0, 1], [0, 0, 1]],    // facing +Z\n  name: 'Triangle',\n});\ntri.translate.set([4, 0.5, -0.7]);\nconsole.log('triangle: V=' + tri.verts.count + ' F=' + tri.faces.count);\n\nawait Utils.wait.frame();\nviewport.frameAll(1.5);\nconsole.log('built \"' + pyramid.name + '\" + \"' + tri.name + '\" from raw data');\n",
      "expectedOutput": "console: V/E/F counts after each append + batch commit."
    },
    {
      "kind": "recipe",
      "id": "model-interactive-sessions",
      "title": "Interactive sessions: polyDraw / polyPen / sculptSession / buildOnNormal / fillHoles",
      "description": "Open each interactive session against a reference cube; immediately cancel or finish so the recipe runs headless.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.polyDraw",
        "mesh.polyPen",
        "mesh.sculptSession",
        "mesh.buildOnNormal",
        "mesh.fillHoles"
      ],
      "code": "// Self-contained from an empty Model scene. Open each interactive session\n// against a reference cube, then cancel / commit immediately so the recipe\n// runs fully headless (no manual viewport interaction needed).\nconst refCube = await create.cube({\n  width: 1.5, height: 1.5, depth: 1.5,\n  subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3, // dense grid for sculpt/retopo\n  name: 'SessionRef',\n});\nrefCube.translate.set([2.5, 1.3, -0.7]); // park off-origin so BVH + renderer stay fresh\nselect(null, { mode: 'clear' });\n\n// polyDraw — freehand quad placement; no opts (reads the global poly-draw store).\nconst polyDraw = await refCube.tools.polyDraw();\nconsole.log('polyDraw opened; cancel is fn:', typeof polyDraw.cancel === 'function');\nawait polyDraw.cancel(); // lifecycle: open -> cancel (discard)\n\n// polyPen — vertex/edge/face placement by raycast. Exercise every opt.\nconst polyPen = await refCube.tools.polyPen({\n  mode: 'quad',        // 'quad' | 'tri' | 'extrude' | 'append'\n  symmetry: null,      // 'x' | 'y' | 'z' | null (no mirror)\n  worldCoords: true,   // interpret placements as world [x,y,z], skip the raycast\n});\nconsole.log('polyPen opened; cancel is fn:', typeof polyPen.cancel === 'function');\nawait polyPen.cancel();\n\n// sculpt — entry point is mesh.tools.sculpt (NOT sculptSession). Config-only\n// then cancel so no real strokes are baked.\nconst sculpt = await refCube.tools.sculpt({\n  brushType: 'clay',   // brush algorithm id\n  radius: 0.1,         // world-METERS, floored at 1e-5\n  strength: 0.5,       // [0.01, 1.0]\n});\nconsole.log('sculpt opened; cancel is fn:', typeof sculpt.cancel === 'function');\nawait sculpt.cancel();\n\n// buildOnNormal — HEADLESS one-shot: supplying BOTH position + normal commits\n// immediately and resolves with the built result (not a session handle).\nconst built = await refCube.tools.buildOnNormal({\n  primitiveType: 'sphere',     // 'cube' | 'sphere' | 'cylinder' | 'cone' | 'torus' | ...\n  size: 0.15,                  // uniform size in grid-units (>0)\n  depth: 0.15,                 // extent along the normal (>0)\n  surfaceOffset: 0.0,          // lift off the surface along the normal\n  position: [2.5, 2.05, -0.7], // world surface point (top face of the cube)\n  normal: [0, 1, 0],           // world surface normal (non-zero)\n});\nconsole.log('buildOnNormal (headless) resolved:', built !== undefined);\n\n// fillHole vs fillHoles: fillHole (no 's') is the INTERACTIVE session;\n// fillHoles (with 's') is the headless op. Open the interactive one + cancel.\nconst fill = await refCube.tools.fillHole({ boundaryFromComponent: { edgeIndices: [] } });\nconsole.log('fillHole opened; cancel is fn:', typeof fill.cancel === 'function');\nawait fill.cancel();\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('All five interactive sessions opened and resolved headlessly.');\n",
      "expectedOutput": "console: per-session handle inspection + cancel/commit confirmation."
    },
    {
      "kind": "recipe",
      "id": "model-material-pbr-roundtrip",
      "title": "Author a PBR material and roundtrip every channel",
      "description": "Create a red metallic material, assign it to a cube, log every PBR field, then mutate via live setters to flip it to a blue rough double-sided variant.",
      "intent": "mesh",
      "editor": "model",
      "category": "materials-textures",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.create.material",
        "node.translate",
        "ModelEditor.getMaterial",
        "material.assignTo",
        "material.name",
        "material.id",
        "material.materialId",
        "material.toString",
        "material.baseColor",
        "material.metallic",
        "material.roughness",
        "material.emissive",
        "material.emissiveIntensity",
        "material.alphaMode",
        "material.alphaCutoff",
        "material.doubleSided",
        "material.equals"
      ],
      "code": "// Author a PBR material, assign it, read every channel back, then flip it\n// to a different look via the live async setters. Self-contained from empty.\n\nconst cube = await create.cube({ width: 1, height: 1, depth: 1, name: 'PbrCube' });\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\n\n// create.material is async. The constructor opts seed every PBR channel.\nconst m = await create.material({\n  name: 'red-metal',\n  baseColor: [0.9, 0.1, 0.1, 1], // [r,g,b,a] each 0..1\n  metallic: 0.85,                // 0 (dielectric) .. 1 (metal)\n  roughness: 0.25,               // 0 (mirror) .. 1 (matte)\n  emissive: [0.4, 0.0, 0.0],     // [r,g,b] glow color\n  emissiveIntensity: 0.5,        // emissive multiplier (>= 0)\n  alphaMode: 'opaque',           // 'opaque' | 'mask' | 'blend'\n  alphaCutoff: 0.5,              // only used when alphaMode === 'mask'\n  doubleSided: false,            // render back faces?\n});\nawait m.assignTo(cube); // bind the material to the cube\n\n// IDENTITY — toString + name + id/materialId (id === materialId).\nconsole.log('toString:', m.toString());\nconsole.log('name:', m.name, 'id:', m.id, 'materialId:', m.materialId);\nconsole.log('id matches materialId?', m.id === m.materialId);\n\n// READ — PBR channels live under material.pbr.<channel>.get() (synchronous).\nconsole.log('baseColor:', JSON.stringify(m.pbr.baseColor.get()));\nconsole.log('metallic:', m.pbr.metallic.get(), 'roughness:', m.pbr.roughness.get());\nconsole.log('emissive:', JSON.stringify(m.pbr.emissive.get()), 'intensity:', m.pbr.emissiveIntensity.get());\nconsole.log('alphaMode:', m.pbr.alphaMode.get(), 'alphaCutoff:', m.pbr.alphaCutoff.get(), 'doubleSided:', m.pbr.doubleSided.get());\n\n// WRITE — every channel setter is ASYNC + undoable (await each).\n// alphaCutoff is only meaningful when alphaMode === 'mask', so set the mode FIRST.\nawait m.pbr.baseColor.set([0.1, 0.5, 0.9, 1.0]); // blue\nawait m.pbr.metallic.set(0.0);                   // dielectric\nawait m.pbr.roughness.set(1.0);                  // fully matte\nawait m.pbr.emissive.set([0.0, 0.4, 0.8]);       // cyan glow\nawait m.pbr.emissiveIntensity.set(1.5);\nawait m.pbr.alphaMode.set('mask');               // set BEFORE alphaCutoff\nawait m.pbr.alphaCutoff.set(0.25);\nawait m.pbr.doubleSided.set(true);\nawait m.setName('blue-rough');                   // rename via setName (async)\n\nconsole.log('after setters - name:', m.name, 'baseColor:', JSON.stringify(m.pbr.baseColor.get()));\nconsole.log('after setters - alphaMode:', m.pbr.alphaMode.get(), 'alphaCutoff:', m.pbr.alphaCutoff.get(), 'doubleSided:', m.pbr.doubleSided.get());\n\n// Equality is by materialId, not handle reference — re-resolve and compare.\nconst same = getMaterial(m.id);\nconsole.log('m equals re-resolved?', same ? m.equals(same) : false);\nawait Utils.wait.frame();\nconsole.log('PBR roundtrip complete on \"' + m.name + '\"');\n",
      "expectedOutput": "console: every PBR field read back, then re-read after the live setters flip the material from red metallic to blue rough."
    },
    {
      "kind": "recipe",
      "id": "model-mesh-handle-getters",
      "title": "Mesh handle getters: id / meshId / faces / edges / vertices / verts / e / f + wireframe",
      "description": "Inspect a cube via every handle getter, iterate vertices(), and tube-ify the mesh via wireframe().",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.id",
        "mesh.meshId",
        "mesh.faces",
        "mesh.edges",
        "mesh.verts",
        "mesh.vertices",
        "mesh.e",
        "mesh.f",
        "mesh.wireframe",
        "mesh.verts",
        "mesh.faces",
        "mesh.edges"
      ],
      "code": "// Self-contained: build a cube and inspect it through every live handle\n// getter, iterate its verts, then tube-ify it with the wireframe modifier.\nconst cube = await create.cube({\n  width: 1.5, height: 1.5, depth: 1.5,\n  subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n  name: 'HandleCube',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\n\n// IDENTITY — id is the stable DG node id; name is the human-facing label.\nconsole.log('cube.id:', cube.id);\nconsole.log('cube.name:', cube.name);\nconsole.log('nodeType:', cube.nodeType);\n\n// COUNT GETTERS — verts / faces / edges expose .count on their namespace.\nconsole.log('counts: V=' + cube.verts.count + ' F=' + cube.faces.count + ' E=' + cube.edges.count);\n\n// COMPONENT BUILDERS — faces([...]) / edges([...]) / verts([...]) build a\n// component handle whose .indices / .numIndices report the selected set.\nconst faceComp = cube.faces([0, 1]);\nconsole.log('faces([0,1]) numIndices:', faceComp.numIndices, 'indices:', JSON.stringify(faceComp.indices));\n\nconst edgeComp = cube.edges([0, 1, 2]);\nconsole.log('edges([0,1,2]) numIndices:', edgeComp.numIndices);\n\nconst vertComp = cube.verts([0, 1, 2, 3]);\nconsole.log('verts([0,1,2,3]) numIndices:', vertComp.numIndices);\n\n// ITERATOR — verts.forEach(cb) walks every vertex; (index, total) supplied.\nlet iterCount = 0;\ncube.verts.forEach((i, total) => { iterCount++; });\nconsole.log('vertices iterated:', iterCount, 'of', cube.verts.count);\n\n// WIREFRAME MODIFIER — replace each edge with a solid tube of the given radius.\nconst before = { v: cube.verts.count, f: cube.faces.count, e: cube.edges.count };\nawait cube.wireframe({\n  thickness: 0.02,   // tube radius in scene units (default ~0.02)\n  offset: 0,         // inward/outward shell offset (default 0)\n});\nconsole.log('after wireframe: V ' + before.v + '->' + cube.verts.count + ', F ' + before.f + '->' + cube.faces.count + ', E ' + before.e + '->' + cube.edges.count);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('mesh handle getter sweep complete.');\n",
      "expectedOutput": "console: id, faces[0] component, e/f lengths, vertices() iterator count, V/F after wireframe."
    },
    {
      "kind": "recipe",
      "id": "model-nonlinear-deformer-tour",
      "title": "Non-linear deformer tour: bend / twist / taper / squash / wave / sine",
      "description": "Spawn six subdivided cubes and apply each non-linear deformer to one. Log the resulting deformer stack on each.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.addBend",
        "mesh.addTwist",
        "mesh.addTaper",
        "mesh.addSquash",
        "mesh.addWave",
        "mesh.addSine",
        "mesh.deformerStack"
      ],
      "code": "// Tour every non-linear deformer MODE, applying one to each of six\n// subdivided cubes. The six non-linear modes\n// ('bend' | 'twist' | 'taper' | 'squash' | 'sine' | 'wave') all go through\n// mesh.deformers.add('nonLinear', { mode }). Self-contained from an empty\n// Model scene.\nconst subs = { subdivisionsX: 4, subdivisionsY: 4, subdivisionsZ: 4 }; // dense so curves read smoothly\nconst offsets = [\n  [-7.5, 1.3, -0.7], [-5, 1.3, -0.7], [-2.5, 1.3, -0.7],\n  [0, 1.3, -0.7], [2.5, 1.3, -0.7], [5, 1.3, -0.7],\n];\n\n// BEND — curvature is the bend amount (radians of bend across the height).\nconst bendCube = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'Bend' });\nbendCube.xform({ t: offsets[0] });\nconst bend = await bendCube.deformers.add('nonLinear', { mode: 'bend' });\nbend.curvature.set(1.2); // strong bend (default ~0)\nawait select(null, { mode: 'clear' });\n\n// TWIST — startAngle / endAngle (degrees) set the twist at each end.\nconst twistCube = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'Twist' });\ntwistCube.xform({ t: offsets[1] });\nconst twist = await twistCube.deformers.add('nonLinear', { mode: 'twist' });\ntwist.startAngle.set(0);    // base untwisted\ntwist.endAngle.set(120);    // 120deg twist at the top\nawait select(null, { mode: 'clear' });\n\n// TAPER — endScaleX / endScaleZ pinch (or flare) the far end.\nconst taperCube = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'Taper' });\ntaperCube.xform({ t: offsets[2] });\nconst taper = await taperCube.deformers.add('nonLinear', { mode: 'taper' });\ntaper.endScaleX.set(0.4);   // pinch X to 40% at the top\ntaper.endScaleZ.set(0.4);   // pinch Z to 40% at the top\nawait select(null, { mode: 'clear' });\n\n// SQUASH — factor squashes/stretches; expand controls the bulge.\nconst squashCube = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'Squash' });\nsquashCube.xform({ t: offsets[3] });\nconst squash = await squashCube.deformers.add('nonLinear', { mode: 'squash' });\nsquash.factor.set(-0.5);    // negative = squash (positive = stretch)\nsquash.expand.set(1.0);     // full lateral bulge\nawait select(null, { mode: 'clear' });\n\n// WAVE — waveAmplitude / waveWavelength shape a radial ripple.\nconst waveCube = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'Wave' });\nwaveCube.xform({ t: offsets[4] });\nconst wave = await waveCube.deformers.add('nonLinear', { mode: 'wave' });\nwave.waveAmplitude.set(0.15);  // ripple height (scene units)\nwave.waveWavelength.set(0.5);  // distance between crests\nawait select(null, { mode: 'clear' });\n\n// SINE — amplitude / wavelength drive a planar sine deform.\nconst sineCube = await create.cube({ width: 1, height: 1, depth: 1, ...subs, name: 'Sine' });\nsineCube.xform({ t: offsets[5] });\nconst sine = await sineCube.deformers.add('nonLinear', { mode: 'sine' });\nsine.amplitude.set(0.2);    // wave height\nsine.wavelength.set(0.6);   // crest-to-crest distance\nawait select(null, { mode: 'clear' });\n\nawait Utils.wait.frame();\nviewport.frameAll();\n\n// Confirm each mesh carries exactly one deformer via deformers.stack().\nconst cubes = [bendCube, twistCube, taperCube, squashCube, waveCube, sineCube];\nfor (const c of cubes) console.log(c.name + ': stack=' + c.deformers.stack().length);\nconsole.log('non-linear deformer tour complete (6 modes).');\n",
      "expectedOutput": "console: per-cube deformer-stack length after each non-linear add."
    },
    {
      "kind": "recipe",
      "id": "model-normals-workflow",
      "title": "Reverse / recalc / smooth normals",
      "description": "Spawn a cube; flip normals; recalculate; smooth; inspect getNormals at each step.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "mesh.normals.reverse",
        "mesh.normals.recalculate",
        "mesh.normals.smooth",
        "mesh.normals.get",
        "node.translate",
        "Utils.wait.frame"
      ],
      "code": "// Reverse -> recalculate -> smooth the normals of a cube, sampling the first\n// normal vector at each step. Self-contained from an empty scene.\nconst cube = await create.cube({\n  width: 1.5, height: 1.5, depth: 1.5, name: 'NormalsCube',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nawait select(null, { mode: 'clear' });\nawait Utils.wait.frame();\n\n// normals.get() returns a FLAT Float32Array [nx0,ny0,nz0, ...]; [0..2] is the\n// first vertex's normal. A helper keeps the per-step logging readable.\nconst sample = (label) => {\n  const n = cube.normals.get();\n  console.log(label + ' n[0..2]:', n[0].toFixed(3), n[1].toFixed(3), n[2].toFixed(3));\n};\nsample('orig');\n\n// 1) reverse(faceIndices?) flips winding. Pass a face-index array to flip only\n//    those faces; omit it to flip the whole mesh. Here: flip the first 3 faces.\nawait cube.normals.reverse([0, 1, 2]);\nawait Utils.wait.frame();\nsample('partial-reverse');\n\n// 2) recalculate() rebuilds face normals from geometry — undoes the flip and\n//    restores outward-facing normals across the whole mesh.\nawait cube.normals.recalculate();\nawait Utils.wait.frame();\nsample('recalc');\n\n// 3) smooth() averages adjacent face normals into per-vertex normals for a\n//    rounded shading look (no geometry change).\nawait cube.normals.smooth();\nawait Utils.wait.frame();\nsample('smoothed');\n\nconsole.log('normals workflow complete on \"' + cube.name + '\"');\n",
      "expectedOutput": "console: normal sample at each step."
    },
    {
      "kind": "recipe",
      "id": "model-primitive-scene",
      "title": "Build a cube, sphere, and cylinder and group them",
      "description": "Create three primitive meshes, place them at offset positions so they do not overlap, then wrap them under one named group and log the group bounding box.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.create.sphere",
        "ModelEditor.create.cylinder",
        "ModelEditor.create.group",
        "mesh.bbox",
        "node.translate"
      ],
      "code": "// Build a 3-primitive scene from an EMPTY Model scene and group it.\n// create.* primitives are ASYNC — every one must be awaited.\n\n// 1) Cube — explicit full size + a 2x2x2 subdivision grid (default 0).\nconst cube = await create.cube({\n  width: 1,            // X extent, meters (default 1)\n  height: 1,           // Y extent, meters (default 1)\n  depth: 1,            // Z extent, meters (default 1)\n  subdivisionsX: 2,    // extra cuts along X (default 0)\n  subdivisionsY: 2,    // extra cuts along Y (default 0)\n  subdivisionsZ: 2,    // extra cuts along Z (default 0)\n  name: 'Cube',\n});\n\n// 2) Sphere — radius + longitude/latitude ring counts (NO 'size' param).\nconst sphere = await create.sphere({\n  radius: 0.5,         // meters (default 0.5)\n  widthSegments: 24,   // longitude rings (default 16 — higher = rounder)\n  heightSegments: 16,  // latitude rings (default 12)\n  name: 'Sphere',\n});\n\n// 3) Cylinder — separate top/bottom radii (NO 'radius'); openEnded caps off.\nconst cyl = await create.cylinder({\n  radiusTop: 0.4,      // top cap radius, meters (default 0.5)\n  radiusBottom: 0.6,   // bottom cap radius, meters (default 0.5) — a frustum\n  height: 1,           // meters (default 1)\n  radialSegments: 24,  // sides around the axis (default 16)\n  heightSegments: 2,   // rings up the side (default 1)\n  openEnded: false,    // false = capped both ends (default false)\n  name: 'Cylinder',\n});\n\n// Space them apart along X so they don't overlap. translate is a Vec3Attribute\n// whose .set([x, y, z]) batches all three axes into one history command.\ncube.translate.set([0, 0, 0]);\nsphere.translate.set([1.5, 0, 0]);\ncyl.translate.set([3, 0, 0]);\n\n// create.group is ASYNC — pass the child nodes + a name (no empty-name form).\nconst group = await create.group([cube, sphere, cyl], { name: 'PrimitiveTrio' });\n\nconsole.log('built:', cube.name, sphere.name, cyl.name);\n\n// Walk the children and log each WORLD-space bounding-box span.\n// geometry.bbox(...) returns a BoundingBox with Vec3 .min / .max / .size.\nfor (const m of [cube, sphere, cyl]) {\n  const size = m.geometry.bbox({ space: 'world' }).size; // Vec3 (meters)\n  console.log(' ', m.name, 'size',\n    size.x.toFixed(2) + ' x ' + size.y.toFixed(2) + ' x ' + size.z.toFixed(2) + ' m');\n}\n\n// Group-level span: union of the three child boxes.\nconst trio = [cube, sphere, cyl];\nlet minX = Infinity, maxX = -Infinity;\nfor (const m of trio) {\n  const b = m.geometry.bbox({ space: 'world' });\n  minX = Math.min(minX, b.min.x);\n  maxX = Math.max(maxX, b.max.x);\n}\nconsole.log('grouped under \"' + group.name + '\"; total X span',\n  (maxX - minX).toFixed(2) + ' m across', group.children().length, 'children');\n",
      "expectedOutput": "console: each primitive name + final group bounding-box span; outliner: a new group containing three meshes."
    },
    {
      "kind": "recipe",
      "id": "model-raycast-closest-point",
      "title": "Raycast a sphere and place a locator at the hit",
      "description": "Spawn a sphere; raycast from camera origin; place a locator at the hit; query closestPoint to a test position.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.sphere",
        "ModelEditor.create.locator",
        "mesh.raycast",
        "mesh.raycastAll",
        "mesh.closestPoint",
        "mesh.distanceTo",
        "node.translate",
        "Utils.wait.frame"
      ],
      "code": "// Raycast a sphere, drop a locator at the first hit, then query the nearest\n// surface point + signed distance to a test position. Spatial queries live\n// under mesh.geometry and take POSITIONAL (origin, dir, {space}) args.\n// Self-contained from an empty Model scene.\n\nconst sphere = await create.sphere({\n  radius: 1.5,\n  widthSegments: 32,\n  heightSegments: 16,\n  name: 'RayTarget',\n});\nsphere.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nawait Utils.wait.frame();\n\n// Cast a ray from in front of the sphere straight through it (world space).\nconst origin = [2.5, 1.3, -10];\nconst dir = [0, 0, 1];\nconst hit = sphere.geometry.raycast(origin, dir, { space: 'world' }); // {point,normal,faceIdx,distance} | null\nif (hit) {\n  const p = hit.point; // Vec3 — has .x/.y/.z + .toArray()\n  console.log('hit point:', p.x.toFixed(3), p.y.toFixed(3), p.z.toFixed(3));\n  console.log('hit normal:', hit.normal.toArray().map((n) => n.toFixed(2)).join(','));\n  console.log('hit distance:', hit.distance.toFixed(3), 'faceIdx:', hit.faceIdx);\n\n  // Place a locator right on the hit point (create.locator is synchronous;\n  // position can be passed in the opts directly).\n  create.locator({ name: 'HitMarker', position: [p.x, p.y, p.z] });\n}\nawait select(null, { mode: 'clear' });\n\n// raycastAll returns every intersection sorted by distance — expect 2\n// (entry + exit) through a solid sphere.\nconst allHits = sphere.geometry.raycastAll(origin, dir, { space: 'world' });\nconsole.log('raycastAll count:', allHits.length);\n\n// closestPoint returns { position: Vec3, ... } for the nearest surface point.\nconst closest = sphere.geometry.closestPoint([0, 0, 0], { space: 'world' });\nif (closest) {\n  const cp = closest.position;\n  console.log('closestPoint to world origin:', cp.x.toFixed(3), cp.y.toFixed(3), cp.z.toFixed(3));\n}\n\n// distanceTo gives the signed distance from a point to the surface.\nconst d = sphere.geometry.distanceTo([0, 0, 0], { space: 'world' });\nconsole.log('distance from world origin to surface:', d.toFixed(3));\n",
      "expectedOutput": "console: raycast hit + locator placement + closestPoint result."
    },
    {
      "kind": "recipe",
      "id": "model-resolution-control",
      "title": "Resolution control: decimate + remesh + solidify",
      "description": "Decimate a sphere, voxel-remesh a separate sphere, solidify a plane; log V/F deltas.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.sphere",
        "ModelEditor.create.plane",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.decimate",
        "mesh.remesh",
        "mesh.solidify",
        "mesh.verts",
        "mesh.faces"
      ],
      "code": "// Self-contained: build three independent working meshes from an empty\n// scene, then drive one resolution op against each and log V/F deltas.\n\n// 1) DECIMATE — collapse a dense sphere down to a vertex/face budget.\n//    create.* is async; spread the mesh out so they don't overlap.\nconst dec = await create.sphere({\n  radius: 1,            // sphere radius in scene units (default 1)\n  widthSegments: 32,    // longitudinal segments (default 32) — high so there is plenty to decimate\n  heightSegments: 16,   // latitudinal segments (default 16)\n  name: 'Decimate',\n});\ndec.translate.set([-2.5, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\nconst dec0 = { v: dec.verts.count, f: dec.faces.count };\nawait dec.decimate({\n  targetCount: 200,     // absolute target face count (alternative form: { ratio: 0.25 })\n});\nconsole.log('decimate: V ' + dec0.v + '->' + dec.verts.count + ', F ' + dec0.f + '->' + dec.faces.count);\n\n// 2) REMESH — rebuild a coarse sphere as clean topology.\n//    algorithm is 'advanced' (analytical) or 'quad' (FEQ retopology) — there is NO 'voxel'.\nconst rem = await create.sphere({\n  radius: 1,\n  widthSegments: 16,\n  heightSegments: 8,\n  name: 'Remesh',\n});\nrem.translate.set([0, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\nconst rem0 = { v: rem.verts.count, f: rem.faces.count };\nawait rem.remesh({\n  algorithm: 'advanced', // 'advanced' = analytical remesh (default) | 'quad' = FEQ retopology\n  // Optional kernel picker for 'advanced': method: 'instantMeshes' (fast) |\n  // 'quadwild' (sharp-feature strong) | 'autoremesher' (organic edge flow,\n  // runs in the background + cancellable). Omitted = current popup selection.\n  targetCount: 500,      // approximate target element count\n});\nconsole.log('remesh: V ' + rem0.v + '->' + rem.verts.count + ', F ' + rem0.f + '->' + rem.faces.count);\n\n// 3) SOLIDIFY — give a flat panel real thickness (shell extrude).\n//    plane subdivisions are widthSegments / heightSegments (NOT subdivisionsX/Y).\nconst sol = await create.plane({\n  width: 1.5,\n  height: 1.5,\n  widthSegments: 4,     // columns of quads (default 1)\n  heightSegments: 4,    // rows of quads (default 1)\n  name: 'Solidify',\n});\nsol.translate.set([2.5, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\nconst sol0 = { v: sol.verts.count, f: sol.faces.count };\nawait sol.solidify({\n  thickness: 0.05,      // shell thickness in scene units (must be > 0; 0 throws / no-ops)\n});\nconsole.log('solidify: V ' + sol0.v + '->' + sol.verts.count + ', F ' + sol0.f + '->' + sol.faces.count);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('resolution control done: decimate + remesh + solidify on three meshes.');\n",
      "expectedOutput": "console: V/F before+after for decimate / remesh / solidify."
    },
    {
      "kind": "recipe",
      "id": "model-scene-graph-navigation",
      "title": "Walk a small parent + children scene with the node API",
      "description": "Build a Group with two cube children, then read identity (name, id, nodeType, toString) and walk parent / children / descendants / ancestors.",
      "intent": "mesh",
      "editor": "model",
      "category": "outliner",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.scene.ls",
        "node.translate",
        "node.parent",
        "node.children",
        "node.descendants",
        "node.ancestors",
        "node.equals",
        "node.name",
        "node.id",
        "node.nodeId",
        "node.nodeType",
        "node.toString"
      ],
      "code": "// Build a small tree (Group with two cube children) then read identity and\n// walk it in both directions with the Node API. Self-contained from empty.\n\nconst cube1 = await create.cube({ width: 1, height: 1, depth: 1, name: 'NavLeaf1' });\ncube1.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\n\nconst cube2 = await create.cube({ width: 0.6, height: 0.6, depth: 0.6, name: 'NavLeaf2' });\ncube2.xform({ t: [5.0, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\n\n// create.group(nodes, opts) is async — pass [] for an empty group, then\n// reparent the leaves under it (setParent is async).\nconst group = await create.group([], { name: 'NavRoot' });\nawait cube1.setParent(group);\nawait cube2.setParent(group);\nawait Utils.wait.frame();\n\n// IDENTITY — toString + name + nodeType + id/nodeId (id === nodeId).\nconsole.log('group toString:', group.toString());\nconsole.log('group name:', group.name, 'nodeType:', group.nodeType); // 'group'\nconsole.log('group id matches nodeId?', group.id === group.nodeId);\n\n// WALK DOWN — direct children vs every transitive descendant.\nconst direct = group.children();\nconsole.log('direct children:', direct.map((c) => c.name).join(', '));\nconst all = group.descendants();\nconsole.log('total descendants:', all.length);\n\n// WALK UP — parent + the full ancestor chain from a leaf.\nconsole.log('cube1 parent:', cube1.parent()?.name ?? '(root)');\nconst up = cube1.ancestors();\nconsole.log('cube1 ancestors:', up.map((a) => a.name).join(' -> '));\n\n// EQUALITY — by nodeId, not handle reference. Re-resolve by NAME and compare.\nconst same = ls({ name: 'NavLeaf1' })[0];\nconsole.log('cube1 equals re-resolved handle?', same ? cube1.equals(same) : false);\nconsole.log('scene-graph navigation complete.');\n",
      "expectedOutput": "console: group toString + identity, direct + transitive child lists, leaf ancestors, equality across re-resolved handles."
    },
    {
      "kind": "recipe",
      "id": "model-skinning-snap-shrinkwrap-cycle",
      "title": "Skinning, vertex-snap, shrinkwrap + reorder + remove cycle",
      "description": "Add skinCluster + vertexSnap + shrinkwrap; reorder the stack; remove one; log each step.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.create.sphere",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "ModelEditor.scene.ls",
        "node.translate",
        "mesh.addSkinCluster",
        "mesh.addVertexSnap",
        "mesh.addShrinkwrap",
        "mesh.deformerStack",
        "mesh.reorderDeformer",
        "mesh.removeDeformer"
      ],
      "code": "// Constraint-family deformers + full stack lifecycle: add skinCluster\n// (graceful skip if no joints) + vertexSnap + shrinkwrap, reorder the stack,\n// then remove one. Self-contained from an empty Model scene — we build a\n// target sphere for the snap/wrap deformers (both REQUIRE a target by name).\nconst cube = await create.cube({\n  width: 1.5, height: 1.5, depth: 1.5,\n  subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3,\n  name: 'DeformerCycle',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\n\n// Build the target surface the snap + shrinkwrap deformers wrap onto.\nconst target = await create.sphere({ radius: 1.2, name: 'WrapTarget' });\ntarget.xform({ t: [2.5, 1.3, -0.7] }); // co-located so verts have something to snap to\nawait select(null, { mode: 'clear' });\n\n// SKIN CLUSTER — requires a non-empty Joint[]. Joints are a Control-Rig\n// construct; in a bare Model scene there are none, so skip gracefully.\nconst joints = ls({ type: 'joint' });\nif (joints.length > 0) {\n  const skin = await cube.deformers.add('skinCluster', { joints: [joints[0]] });\n  console.log('after skinCluster (' + skin.deformerType + '): stack=' + cube.deformers.stack().length);\n} else {\n  console.log('skinCluster skipped: no joints in scene (Control-Rig only).');\n}\n\n// VERTEX SNAP — snap verts onto the target when within threshold.\nconst snap = await cube.deformers.add('vertexSnap', { target: 'WrapTarget' });\nsnap.threshold.set(0.5); // max snap distance in scene units\nconsole.log('after vertexSnap: stack=' + cube.deformers.stack().length);\n\n// SHRINKWRAP — project every vert onto the target surface, with an offset.\nconst wrap = await cube.deformers.add('shrinkwrap', { target: 'WrapTarget' });\nwrap.offset.set(0.02); // hover 0.02 units off the surface\nconsole.log('after shrinkwrap: stack=' + cube.deformers.stack().length);\n\n// Inspect order by deformerType (top-down = evaluation order).\nconst stack = cube.deformers.stack();\nconsole.log('order: ' + stack.map((d) => d.deformerType).join(', '));\n\n// REORDER — move the last deformer to the top of the stack (index 0).\nif (stack.length >= 2) {\n  cube.deformers.reorder(stack[stack.length - 1], 0);\n  console.log('after reorder: ' + cube.deformers.stack().map((d) => d.deformerType).join(', '));\n}\n\n// REMOVE — drop the vertexSnap; its handle goes stale afterward.\ncube.deformers.remove(snap);\nconsole.log('after remove(snap): stack=' + cube.deformers.stack().length);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('skinning / snap / shrinkwrap cycle complete on \"' + cube.name + '\"');\n",
      "expectedOutput": "console: stack length and ordering at each step."
    },
    {
      "kind": "recipe",
      "id": "model-slide-toolkit",
      "title": "Slide tools: compute slide data + edgeSlide + vertexSlide + generic slide",
      "description": "Compute slide data on chosen edges + vertices, then apply edgeSlide / vertexSlide at factor 0.3, plus the generic slide router.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.computeEdgeSlideData",
        "mesh.computeVertexSlideData",
        "mesh.edgeSlide",
        "mesh.vertexSlide",
        "mesh.slide",
        "mesh.verts",
        "mesh.edges"
      ],
      "code": "// Slide tools — push edges / vertices along their incident rails without\n// changing the topology (vertex / edge counts stay constant; only positions\n// move). The one-shot 'mesh.slide({ edges|verts, factor })' form resolves the\n// rails internally, so no precomputed slide-data is needed. Self-contained.\nconst cube = await create.cube({\n  width: 2, height: 2, depth: 2,\n  subdivisionsX: 4, subdivisionsY: 4, subdivisionsZ: 4, // dense = many slidable rails\n  name: 'SlideCube',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nawait select(null, { mode: 'clear' });\nconsole.log('start: V=' + cube.verts.count + ' E=' + cube.edges.count);\n\n// 1) Edge slide via the flat key-dispatch form.\n//    - edges: edge indices to slide\n//    - factor: signed [-1, 1] ratio (0 = no move, 1 = all the way to the next\n//      connected edge, negative = the other direction)\nawait cube.slide({ edges: [4, 5, 6], factor: 0.3 });\nconsole.log('after slide(edges, 0.3): V=' + cube.verts.count + ' E=' + cube.edges.count);\n\n// 2) Vertex slide via the flat key-dispatch form.\nawait cube.slide({ verts: [8, 9, 10], factor: -0.25 });\nconsole.log('after slide(verts, -0.25): V=' + cube.verts.count + ' E=' + cube.edges.count);\n\n// 3) Component-selector form — mesh.edges([...]) builds an EdgeComponent, then\n//    .slide({ factor }) operates on exactly those edges (same rail resolution).\nawait cube.edges([12, 13]).slide({ factor: 0.5 });\nconsole.log('after edges.slide(0.5): V=' + cube.verts.count + ' E=' + cube.edges.count);\n\n// 4) Component-selector vertex slide.\nawait cube.verts([0, 1]).slide({ factor: 0.4 });\nconsole.log('after verts.slide(0.4): V=' + cube.verts.count + ' E=' + cube.edges.count);\n\nawait Utils.wait.frame();\nviewport.frameAll(1.5);\nconsole.log('slide toolkit complete on \"' + cube.name + '\"');\n",
      "expectedOutput": "console: slide-data sample + verts/edges after each slide."
    },
    {
      "kind": "recipe",
      "id": "model-smoothing-deformer-stack",
      "title": "Smoothing deformer stack: smooth + deltaMush + normalPush + FFD + cage",
      "description": "Layer smooth + deltaMush + normalPush + FFD + cage deformers on one cube; log the stack growing.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.addSmooth",
        "mesh.addDeltaMush",
        "mesh.addNormalPush",
        "mesh.addFFD",
        "mesh.addCage",
        "mesh.deformerStack"
      ],
      "code": "// Layer five smoothing / relaxation / lattice deformers on one cube and\n// watch deformers.stack() grow 1 -> 5. Each add() returns its typed handle,\n// so we tune one signature attribute per deformer. Self-contained.\nconst cube = await create.cube({\n  width: 1.5, height: 1.5, depth: 1.5,\n  subdivisionsX: 4, subdivisionsY: 4, subdivisionsZ: 4, // dense so smoothing visibly reads\n  name: 'SmoothStack',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\n\n// 1) SMOOTH — Laplacian/Taubin relax. iterations + lambda control strength.\nconst smooth = await cube.deformers.add('smooth');\nsmooth.iterations.set(5);   // number of relaxation passes (default ~1)\nsmooth.lambda.set(0.6);     // per-step weight in 0..1\nconsole.log('after Smooth: stack=' + cube.deformers.stack().length);\n\n// 2) DELTA MUSH — preserves detail while smoothing. blend mixes mushed result.\nconst dm = await cube.deformers.add('deltaMush');\ndm.iterations.set(8);       // mush smoothing passes\ndm.blend.set(0.75);         // 0 = base, 1 = fully mushed\nawait select(null, { mode: 'clear' });\nconsole.log('after DeltaMush: stack=' + cube.deformers.stack().length);\n\n// 3) NORMAL PUSH — inflate along normals. pushAmount is the offset distance.\nconst np = await cube.deformers.add('normalPush');\nnp.pushAmount.set(0.05);    // inflate 0.05 scene units along each normal\nnp.averageNormals.set(1);   // 1 = use smoothed normals (softer push)\nconsole.log('after NormalPush: stack=' + cube.deformers.stack().length);\n\n// 4) FFD — free-form lattice. basis 'bspline' = smoother falloff than 'bernstein'.\nconst ffd = await cube.deformers.add('ffd', { basis: 'bspline' });\nconsole.log('FFD basis:', ffd.basis.get());\nconsole.log('after FFD: stack=' + cube.deformers.stack().length);\n\n// 5) CAGE — bind to a wrapping cage. coordinateType 'meanValue' = MVC weights.\nawait cube.deformers.add('cage', { coordinateType: 'meanValue' });\nconsole.log('after Cage: stack=' + cube.deformers.stack().length);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('smoothing deformer stack built (5 deformers) on \"' + cube.name + '\"');\n",
      "expectedOutput": "console: stack length after each add (1..5)."
    },
    {
      "kind": "recipe",
      "id": "model-subdivide-and-bend",
      "title": "Subdivide a cube and bend it",
      "description": "Start with a 1m cube, subdivide it twice to add resolution, bend it 60 degrees around the Y axis, and log the before / after vertex count.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "mesh.subdivide",
        "mesh.bend",
        "mesh.verts",
        "mesh.faces",
        "mesh.bbox"
      ],
      "code": "// Subdivide a cube, then bend it, then read back the final shape.\n// Self-contained from an empty scene.\n\n// 1) Seed primitive: a tall, thin bar so the bend reads clearly.\nconst cube = await create.cube({\n  width: 1,   // meters\n  height: 3,  // tall along Y — this is the bend axis the curl wraps around\n  depth: 1,   // meters\n  name: 'BentBar',\n});\n\nconst startVerts = cube.verts.count;   // mesh.verts.count, NOT mesh.numVerts\nconst startFaces = cube.faces.count;   // mesh.faces.count\nconsole.log('start: V=' + startVerts, 'F=' + startFaces);\n\n// 2) Add resolution so the bend has segments to deform. iterations is the\n//    Catmull-Clark pass count (default 1); each pass roughly quadruples faces.\nawait cube.subdivide({ iterations: 2 });\nconsole.log('after 2 subdivisions: V=' + cube.verts.count, 'F=' + cube.faces.count);\n\n// 3) Bend about the Y axis. angle is in RADIANS (Math.PI/3 = 60 degrees);\n//    positive curls one way, negative the other. axis is a letter 'x'|'y'|'z'.\nawait cube.bend({ axis: 'y', angle: Math.PI / 3 });\nconsole.log('after bend: V=' + cube.verts.count);\n\n// 4) Final shape's WORLD bounding box. Spatial/health queries live under\n//    mesh.geometry.*, not flat on the mesh. .size is a Vec3 in meters.\nconst size = cube.geometry.bbox({ space: 'world' }).size;\nconsole.log('final bbox:',\n  size.x.toFixed(3) + ' x ' + size.y.toFixed(3) + ' x ' + size.z.toFixed(3) + ' meters');\n",
      "expectedOutput": "console: starting / post-subdivide / post-bend vertex count + final bbox size in meters."
    },
    {
      "kind": "recipe",
      "id": "model-tool-bisect-axis-aligned-cut",
      "title": "Bisect tool: axis + offset + fill, then commit; cancel on a second pass",
      "description": "Open a Bisect session, configure the plane (axis + offset) and fill behavior, commit a cut, then open a second session and cancel to demonstrate the abort path.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.bisect",
        "bisectSession.toolId",
        "bisectSession.meshId",
        "bisectSession.state",
        "bisectSession.isActive",
        "bisectSession.setAxis",
        "bisectSession.setOffset",
        "bisectSession.setFill",
        "bisectSession.setClearInner",
        "bisectSession.setClearOuter",
        "bisectSession.commit",
        "bisectSession.cancel"
      ],
      "code": "// Self-contained: spawn a subdivided cube the cut can land on.\nconst cube = await create.cube({\n  width: 1, height: 1, depth: 1,\n  subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3, // edges for the plane to cross\n  name: 'BisectDemo',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\nconsole.log('start: V=' + cube.verts.count + ' F=' + cube.faces.count);\n\n// Open a Bisect session. The ctor opts seed the starting plane; every value\n// can be re-set via the chained setters below. toolId / meshId / state /\n// isActive reflect the live session (isActive is the customer-facing getter).\nconst session = await cube.bisect({\n  axis: 'y',          // 'x' | 'y' | 'z' — plane is perpendicular to this axis (default 'x')\n  offset: 0,          // signed offset along axis, scene units (default 0)\n  fill: true,         // cap the cut with a new face (default false)\n  clearInner: false,  // delete the negative half (default false)\n  clearOuter: false,  // delete the positive half (default false)\n});\nconsole.log('session opened: ' + JSON.stringify({\n  toolId: session.toolId,\n  meshIdHead: session.meshId.slice(0, 8),\n  state: session.state,\n  isActive: session.isActive,\n}));\n\n// Re-configure via the chained setters (each returns `this`). Headless commit\n// requires at least one of fill / clearInner / clearOuter to be true.\nsession\n  .setAxis('y')        // re-assert the cutting axis\n  .setOffset(0)        // mid-mesh cut\n  .setFill(true)       // close the seam with a cap face\n  .setClearInner(false)\n  .setClearOuter(false);\n\n// Bake the cut into history.\nawait session.commit();\nconsole.log('after commit: V=' + cube.verts.count + ' F=' + cube.faces.count + ' state=' + session.state);\n\n// Second pass — exercise the clear options + the abort path. cancel() reverts\n// the preview and tears down the editor subscription (idempotent).\nconst second = await cube.bisect({ axis: 'x', offset: 0.5 });\nsecond\n  .setAxis('x')        // cut perpendicular to X\n  .setOffset(0.5)      // half a unit off-center\n  .setFill(false)      // leave the cut open\n  .setClearInner(true) // would drop the negative half on commit\n  .setClearOuter(false);\nsecond.cancel();\nconsole.log('after cancel: state=' + second.state + ' isActive=' + second.isActive);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('bisect demo complete: one committed cut + one cancelled pass.');\n",
      "expectedOutput": "console: session getters before commit, V/F counts after the cut, then a second session that is cancelled."
    },
    {
      "kind": "recipe",
      "id": "model-tool-build-on-normal-extrude-along-face-normal",
      "title": "Build on Normal: activate the surface-stamp tool, inspect the session, then cancel",
      "description": "Open the Build-on-Normal interactive session against a cube. The tool stays active so the user can click surfaces in the viewport; the script inspects every session getter then cancels.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.buildOnNormal",
        "buildOnNormalSession.toolId",
        "buildOnNormalSession.meshId",
        "buildOnNormalSession.state",
        "buildOnNormalSession.isActive",
        "buildOnNormalSession.commit",
        "buildOnNormalSession.cancel"
      ],
      "code": "// Self-contained: Build-on-Normal stamps a parametric primitive onto the\n// face the user clicks, oriented to that face's normal. Two call modes:\n//  - position + normal supplied  -> headless one-shot (places immediately)\n//  - position + normal BOTH omitted -> interactive session for viewport clicks\n// (supplying exactly one of position/normal throws InvalidArgumentError).\nconst cube = await create.cube({\n  width: 2, height: 2, depth: 2,\n  subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n  name: 'BuildOnNormalDemo',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\n\n// INTERACTIVE MODE — omit position+normal. Returns a session handle that\n// activates the tool; the getters mirror the live tool state.\nconst session = await cube.buildOnNormal({\n  primitiveType: 'sphere', // primitive to stamp ('cube'|'sphere'|'cylinder'|'cone'|'torus'|...)\n  size: 0.2,               // uniform size in scene units (default 1 grid unit; must be > 0)\n  depth: 0.2,              // extent along the surface normal (default 1 grid unit; must be > 0)\n  surfaceOffset: 0,        // gap above the surface along the normal (default 0 = sits on it)\n});\nif (typeof session === 'object' && session && 'toolId' in session) {\n  console.log('session opened: ' + JSON.stringify({\n    toolId: session.toolId,\n    meshIdHead: session.meshId.slice(0, 8),\n    state: session.state,\n    isActive: session.isActive,\n  }));\n\n  // In interactive mode commit() does NOT auto-place; it transitions the\n  // session to 'committed' so further input throws. cancel() deactivates.\n  session.commit();\n  console.log('after commit: state=' + session.state);\n\n  // Second pass with a different primitive — open then cancel to demo abort.\n  const second = await cube.buildOnNormal({ primitiveType: 'cube', size: 0.15, depth: 0.15, surfaceOffset: 0.05 });\n  if (typeof second === 'object' && second && 'cancel' in second) {\n    second.cancel();\n    console.log('after cancel: state=' + second.state + ' isActive=' + second.isActive);\n  }\n} else {\n  // (Only reached if a position+normal were supplied — the headless one-shot.)\n  console.log('headless mode placed: ' + JSON.stringify(session));\n}\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('build-on-normal demo complete: tool activated then torn down.');\n",
      "expectedOutput": "console: session getters confirm the tool is active, then cancel tear it down."
    },
    {
      "kind": "recipe",
      "id": "model-tool-fill-hole-place-faces-around-boundary",
      "title": "Fill Hole tool: open against an auto-detected boundary loop, place an interior vert + tri face, then commit; cancel a second pass",
      "description": "Open a Fill Hole session against an auto-detected boundary loop on the live mesh. Demonstrate placeVertex (client-pixel coords) + placeFace (vertex ids) then commit; a second pass shows cancel.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.fillHole",
        "fillHoleSession.toolId",
        "fillHoleSession.meshId",
        "fillHoleSession.state",
        "fillHoleSession.isActive",
        "fillHoleSession.placeVertex",
        "fillHoleSession.placeFace",
        "fillHoleSession.commit",
        "fillHoleSession.cancel"
      ],
      "code": "// Self-contained: spawn a cube so the auto-boundary detection has a mesh.\n// (A real workflow deletes a face first; the session always picks the first\n// detected boundary loop.)\nconst cube = await create.cube({\n  width: 1.5, height: 1.5, depth: 1.5,\n  subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n  name: 'FillHoleDemo',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\n\n// Open the session. boundaryFromComponent is a reserved disambiguator for\n// meshes with multiple loops (accepted but currently ignored — the first\n// loop found is always used).\nconst session = await cube.fillHole({\n  boundaryFromComponent: { edgeIndices: [0] }, // hint: which loop (reserved; first loop wins today)\n});\nconsole.log('session opened: ' + JSON.stringify({\n  toolId: session.toolId,\n  meshIdHead: session.meshId.slice(0, 8),\n  state: session.state,\n  isActive: session.isActive,\n}));\n\n// placeVertex takes client-pixel coords [clientX, clientY] and raycasts onto\n// the surface; off-mesh picks are silent no-ops. placeFace takes 3 (tri) or 4\n// (quad) vertex ids — either boundary ids or ids returned by prior placeVertex.\n// In a headless script there is no live boundary/viewport, so this throws and\n// we fall through to cancel().\ntry {\n  session.placeVertex([400, 300]);                                       // interior point (client px)\n  session.placeFace(['boundary-vid-0', 'boundary-vid-1', 'boundary-vid-2']); // tri spanning the loop\n  console.log('placement attempted; committing.');\n  await session.commit();\n} catch (e) {\n  console.log('placement skipped (no live boundary in headless context): ' + (e && e.message ? e.message : String(e)));\n  session.cancel();\n}\nconsole.log('after first session: state=' + session.state);\n\n// Second pass: open + cancel to demonstrate the abort path explicitly.\nconst second = await cube.fillHole({});\nsecond.cancel();\nconsole.log('after cancel: state=' + second.state + ' isActive=' + second.isActive);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('fill-hole demo complete: placeVertex + placeFace lifecycle exercised.');\n",
      "expectedOutput": "console: session getters before placement, optional placement attempts, then commit OR cancel."
    },
    {
      "kind": "recipe",
      "id": "model-tool-knife-cut-through-points",
      "title": "Knife tool: open a session, addPoint twice to seed a cut line, commit; cancel a second pass",
      "description": "Open the Knife session, seed two cut points (client-pixel coords) with addPoint, then commit. A second pass demonstrates cancel.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.knife",
        "knifeSession.toolId",
        "knifeSession.meshId",
        "knifeSession.state",
        "knifeSession.isActive",
        "knifeSession.addPoint",
        "knifeSession.commit",
        "knifeSession.cancel"
      ],
      "code": "// Self-contained: subdivided cube so the knife has interior edges to cross.\nconst cube = await create.cube({\n  width: 1, height: 1, depth: 1,\n  subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3,\n  name: 'KnifeDemo',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\nconsole.log('start: V=' + cube.verts.count + ' F=' + cube.faces.count);\n\n// snapMode is a reserved hint for which targets the cut attracts to; today\n// every session snaps to both edges and vertices regardless.\nconst session = await cube.knife({\n  snapMode: 'edge', // 'edge' | 'vertex' | 'none' (reserved hint)\n});\nconsole.log('session opened: ' + JSON.stringify({\n  toolId: session.toolId,\n  meshIdHead: session.meshId.slice(0, 8),\n  state: session.state,\n  isActive: session.isActive,\n}));\n\n// addPoint takes a KnifePoint = [clientX, clientY] (client-pixel coords).\n// Two points define the cut line; commit() bakes it into history. A headless\n// script has no live raycast context, so commit throws -> we cancel.\ntry {\n  session.addPoint([300, 200]); // first cut-line endpoint (client px)\n  session.addPoint([600, 400]); // second endpoint\n  await session.commit();\n  console.log('after commit: V=' + cube.verts.count + ' F=' + cube.faces.count);\n} catch (e) {\n  console.log('cut skipped (no live raycast context): ' + (e && e.message ? e.message : String(e)));\n  session.cancel();\n}\n\n// Second pass: open + cancel to demonstrate the abort path.\nconst second = await cube.knife({ snapMode: 'vertex' });\nsecond.cancel();\nconsole.log('after cancel: state=' + second.state + ' isActive=' + second.isActive);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('knife demo complete: two cut points seeded, lifecycle exercised.');\n",
      "expectedOutput": "console: session getters before the cut, optional commit, then cancel on the second pass."
    },
    {
      "kind": "recipe",
      "id": "model-tool-polypen-author-quad-fan",
      "title": "PolyPen tool: placeVertex + placeEdge + placeFace + placeTriangleFromEdge in one session",
      "description": "Open a PolyPen retopo session against a reference surface and demonstrate every placement primitive: vertex, edge, quad face, triangle from edge. Commit bakes the new geometry; a second pass cancels.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.polyPen",
        "polyPenSession.toolId",
        "polyPenSession.meshId",
        "polyPenSession.state",
        "polyPenSession.isActive",
        "polyPenSession.placeVertex",
        "polyPenSession.placeEdge",
        "polyPenSession.placeFace",
        "polyPenSession.placeTriangleFromEdge",
        "polyPenSession.commit",
        "polyPenSession.cancel"
      ],
      "code": "// Self-contained: reference cube the PolyPen retopo session draws onto.\nconst refCube = await create.cube({\n  width: 1.5, height: 1.5, depth: 1.5,\n  subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3,\n  name: 'PolyPenDemo',\n});\nrefCube.translate.set([2.5, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\n\n// Open with every PolyPen opt set. worldCoords:true lets a headless script\n// pass literal [x,y,z] positions instead of screen-space clicks.\nconst session = await refCube.polyPen({\n  mode: 'quad',       // 'quad' | 'tri' | 'extrude' | 'append' — fill behavior (default 'quad')\n  symmetry: 'x',      // 'x' | 'y' | 'z' | null — mirror every placement across this axis\n  worldCoords: true,  // interpret inputs as literal world positions (skips raycasting)\n});\nconsole.log('session opened: ' + JSON.stringify({\n  toolId: session.toolId,\n  meshIdHead: session.meshId.slice(0, 8),\n  state: session.state,\n  isActive: session.isActive,\n}));\n\n// World-coords mode: placeVertex / placeEdge accept a single [x,y,z];\n// placeFace requires exactly 4 [x,y,z] tuples (a quad); placeTriangleFromEdge\n// grows a tri from an existing QuadDraw edge id. Headless -> may throw, so\n// guard with try/catch and cancel on failure.\ntry {\n  session.placeVertex([0, 0.5, 0]);            // seed vertex\n  session.placeEdge([0.2, 0.5, 0]);            // edge to the next point\n  session.placeFace([                          // quad = 4 corners\n    [0.4, 0.5, 0],\n    [0.4, 0.5, 0.4],\n    [0.6, 0.5, 0.4],\n    [0.6, 0.5, 0],\n  ]);\n  // apex id is empty when the executor merged the apex into a nearby vert.\n  const apex = session.placeTriangleFromEdge('seed-vert-A-seed-vert-B', [0.8, 0.5, 0.2]);\n  console.log('placeTriangleFromEdge apex id: ' + JSON.stringify(apex));\n  await session.commit();\n} catch (e) {\n  console.log('placement skipped (no QuadDraw seed in headless context): ' + (e && e.message ? e.message : String(e)));\n  session.cancel();\n}\nconsole.log('after first session: state=' + session.state);\n\n// Second pass: open + cancel to demonstrate the abort path.\nconst second = await refCube.polyPen({ mode: 'tri', worldCoords: true });\nsecond.cancel();\nconsole.log('after cancel: state=' + second.state + ' isActive=' + second.isActive);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('polypen demo complete: vertex + edge + quad + tri placements exercised.');\n",
      "expectedOutput": "console: session getters, placement attempts, optional commit, then cancel."
    },
    {
      "kind": "recipe",
      "id": "model-tool-slice-freeform-endpoint-cut",
      "title": "Slice (freeform): set two endpoints in client-pixel space, commit; cancel a second pass",
      "description": "Open the freeform Slice session and drive setEndpoints with two client-pixel points to define a camera-relative cut. Commit bakes the cut; a second pass cancels.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.slice",
        "sliceFreeformSession.toolId",
        "sliceFreeformSession.meshId",
        "sliceFreeformSession.state",
        "sliceFreeformSession.isActive",
        "sliceFreeformSession.setEndpoints",
        "sliceFreeformSession.commit",
        "sliceFreeformSession.cancel"
      ],
      "code": "// Self-contained: subdivided cube for the freeform slice to cross.\nconst cube = await create.cube({\n  width: 1, height: 1, depth: 1,\n  subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3,\n  name: 'SliceFreeformDemo',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\n\n// mode defaults to 'freeform'; set it explicitly for clarity.\nconst session = await cube.slice({\n  mode: 'freeform', // 'freeform' (two screen points) | 'plane' (axis + offset)\n});\nconsole.log('session opened: ' + JSON.stringify({\n  toolId: session.toolId,\n  meshIdHead: session.meshId.slice(0, 8),\n  state: session.state,\n  isActive: session.isActive,\n}));\n\n// setEndpoints takes two SlicePoint = [clientX, clientY] arrays. The freeform\n// kernel projects them through the active camera to build a cutting plane\n// perpendicular to the screen. Headless -> no live camera, so commit throws.\ntry {\n  session.setEndpoints([300, 250], [700, 350]); // two screen-space endpoints\n  await session.commit();\n  console.log('after commit: V=' + cube.verts.count + ' F=' + cube.faces.count);\n} catch (e) {\n  console.log('cut skipped (no live camera context): ' + (e && e.message ? e.message : String(e)));\n  session.cancel();\n}\n\n// Second pass: open + cancel to demonstrate the abort path.\nconst second = await cube.slice({ mode: 'freeform' });\nsecond.cancel();\nconsole.log('after cancel: state=' + second.state + ' isActive=' + second.isActive);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('freeform slice demo complete: two endpoints set, lifecycle exercised.');\n",
      "expectedOutput": "console: session getters, endpoint configuration, optional commit, then cancel."
    },
    {
      "kind": "recipe",
      "id": "model-tool-slice-plane-axis-offset-fill",
      "title": "Slice (plane, one-shot): mode + axis + offset + fill + oneShot — headless cut",
      "description": "Cut a mesh headless with mesh.slice({ mode:\"plane\", axis, offset, fill, oneShot:true }). A non-oneShot slice returns an INTERACTIVE session and applies nothing, so a script MUST pass oneShot:true (with axis + offset) to actually cut. There is no setAxis/setOffset/setFill chain.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.slice",
        "mesh.verts",
        "mesh.faces"
      ],
      "code": "// Self-contained: subdivided cube for the axis-aligned plane cut. mesh.slice\n// in PLANE mode with oneShot:true runs HEADLESS (no interactive drag session).\n// The interactive setAxis()/setOffset()/setFill() chain does NOT exist on this\n// API — the cut is fully described by the SliceOpts bag passed up front.\nconst cube = await create.cube({\n  width: 1, height: 1, depth: 1,\n  subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3,\n  name: 'SlicePlaneDemo',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nawait select(null, { mode: 'clear' });\nconst before = { v: cube.verts.count, f: cube.faces.count };\nconsole.log('start: V=' + before.v + ' F=' + before.f);\n\n// Every SliceOpts field, each with an explicit non-default value + comment:\nconst result = await cube.slice({\n  mode: 'plane',   // 'plane' = axis + offset cut; 'freeform' is interactive-only\n  axis: 'z',       // 'x' | 'y' | 'z' — cutting plane is perpendicular to this axis\n  offset: 0.15,    // signed plane position along axis, scene units (off-center cut)\n  fill: true,      // reserved for symmetry with Bisect; the slice kernel IGNORES it\n  oneShot: true,   // REQUIRED for scripts — headless commit (omit it and nothing cuts)\n});\nconsole.log('slice result: ' + ('ok' in result ? ('ok=' + result.ok) : 'session'));\nconsole.log('after slice: V ' + before.v + '->' + cube.verts.count\n  + ', F ' + before.f + '->' + cube.faces.count);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('plane slice (one-shot) complete on \"' + cube.name + '\".');\n",
      "expectedOutput": "console: V/F counts before and after the one-shot plane cut."
    },
    {
      "kind": "recipe",
      "id": "model-tool-slice-router-router-cut",
      "title": "Slice router: open in plane mode (axis/offset) and freeform mode (endpoints) and exercise every router method",
      "description": "The slice router is the public entry behind mesh.slice(): it discriminates on mode and forwards to either SlicePlaneSession or SliceFreeformSession. This recipe opens both modes and exercises every router-level method.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.slice",
        "sliceRouterSession.toolId",
        "sliceRouterSession.meshId",
        "sliceRouterSession.state",
        "sliceRouterSession.isActive",
        "sliceRouterSession.setAxis",
        "sliceRouterSession.setEndpoints",
        "sliceRouterSession.setOffset",
        "sliceRouterSession.commit",
        "sliceRouterSession.cancel"
      ],
      "code": "// Self-contained: two cubes — one for plane mode, one for freeform. The\n// slice router (mesh.slice) discriminates on mode and forwards to either the\n// SlicePlaneSession or the SliceFreeformSession behind a single API.\nconst planeCube = await create.cube({\n  width: 1, height: 1, depth: 1,\n  subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3,\n  name: 'SliceRouterPlane',\n});\nplaneCube.translate.set([-1.5, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\n\n// PLANE MODE — setAxis / setOffset live on the router; setEndpoints would throw.\nconst planeSession = await planeCube.slice({ mode: 'plane', axis: 'x', offset: 0 });\nconsole.log('plane session: ' + JSON.stringify({\n  toolId: planeSession.toolId,\n  meshIdHead: planeSession.meshId.slice(0, 8),\n  state: planeSession.state,\n  isActive: planeSession.isActive, // customer-facing status getter\n}));\nplaneSession\n  .setAxis('x')   // cut perpendicular to X\n  .setOffset(0);  // through center\nawait planeSession.commit();\nconsole.log('plane mode committed; state=' + planeSession.state);\n\n// FREEFORM MODE — setEndpoints lives on the router; setAxis / setOffset would throw.\nconst freeCube = await create.cube({\n  width: 1, height: 1, depth: 1,\n  subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3,\n  name: 'SliceRouterFreeform',\n});\nfreeCube.translate.set([1.5, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\n\nconst freeSession = await freeCube.slice({ mode: 'freeform' });\nconsole.log('freeform session: ' + JSON.stringify({\n  toolId: freeSession.toolId,\n  meshIdHead: freeSession.meshId.slice(0, 8),\n  state: freeSession.state,\n  isActive: freeSession.isActive,\n}));\ntry {\n  freeSession.setEndpoints([300, 250], [700, 350]); // SlicePoint = [clientX, clientY]\n  await freeSession.commit();\n  console.log('freeform mode committed; state=' + freeSession.state);\n} catch (e) {\n  console.log('freeform commit skipped (no camera context): ' + (e && e.message ? e.message : String(e)));\n  freeSession.cancel();\n}\n\n// Third (plane) session — configure then cancel to demonstrate the abort path.\nconst third = await planeCube.slice({ mode: 'plane' });\nthird\n  .setAxis('y')\n  .setOffset(0.5);\nthird.cancel(); // reverts the preview + tears down the editor subscription (idempotent)\nconsole.log('after cancel: state=' + third.state + ' isActive=' + third.isActive);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('slice router demo complete: plane committed, freeform + cancel exercised.');\n",
      "expectedOutput": "console: per-mode session getters, configuration applied, optional commit, then cancel."
    },
    {
      "kind": "recipe",
      "id": "model-topology-audit",
      "title": "Topology query and manifold audit across primitives",
      "description": "Spawn cube + sphere + plane (with explicit deselect between via select(null, {mode: \"clear\"})); query isClosed / isManifold / holes / shells / triangle counts.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.create.sphere",
        "ModelEditor.create.plane",
        "mesh.isClosed",
        "mesh.isManifold",
        "mesh.holes",
        "mesh.shells",
        "mesh.nonManifoldEdges",
        "mesh.nonManifoldVertices",
        "mesh.faces",
        "node.translate",
        "Utils.wait.frame"
      ],
      "code": "// Topology + manifold audit across three primitives, built from an empty\n// scene. A closed solid (cube), a closed sphere, and an OPEN plane contrast\n// the closed/holes/shells readings. Spatial & health queries live under\n// mesh.geometry.*, NOT flat on the mesh.\n\n// 1) Subdivided cube — closed solid.\nconst cube = await create.cube({\n  width: 1, height: 1, depth: 1,\n  subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n  name: 'TopoCube',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nawait select(null, { mode: 'clear' }); // deselect between builds\n\n// 2) Sphere — closed, smooth.\nconst sphere = await create.sphere({\n  radius: 0.8, widthSegments: 24, heightSegments: 16, name: 'TopoSphere',\n});\nsphere.translate.set([5.0, 1.3, -0.7]);\nawait select(null, { mode: 'clear' });\n\n// 3) Plane — a single OPEN quad sheet (one boundary loop, not closed).\n//    Note: plane uses widthSegments / heightSegments (NOT subdivisionsX/Y).\nconst plane = await create.plane({\n  width: 1, height: 1, widthSegments: 2, heightSegments: 2, name: 'TopoPlane',\n});\nplane.translate.set([7.5, 1.3, -0.7]);\nawait select(null, { mode: 'clear' });\nawait Utils.wait.frame(); // let topology caches settle before reading\n\nfor (const m of [cube, sphere, plane]) {\n  const g = m.geometry;\n  console.log(m.name + ': verts=' + m.verts.count\n    + ' faces=' + m.faces.count\n    + ' tris=' + m.faces.numTriangles\n    + ' area=' + g.area.toFixed(3)\n    + ' closed=' + g.isClosed\n    + ' manifold=' + g.isManifold\n    + ' holes=' + g.holes\n    + ' shells=' + g.shells\n    + ' badEdges=' + g.nonManifoldEdges.length\n    + ' badVerts=' + g.nonManifoldVertices.length);\n}\nconsole.log('audit complete: 3 meshes scanned');\n",
      "expectedOutput": "console: per-mesh closed/manifold/holes/shells/triangle counts."
    },
    {
      "kind": "recipe",
      "id": "model-topology-cut-tools",
      "title": "Topology cut tools: bisect + knife + slice + spin + mirror",
      "description": "Spawn four cubes; bisect by plane, slice axis-aligned, spin around Y, mirror across X. Knife runs as a session that gets cancelled.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.bisect",
        "mesh.knife",
        "mesh.slice",
        "mesh.spin",
        "mesh.mirror",
        "mesh.verts",
        "mesh.faces"
      ],
      "code": "// Topology cut tools across four cubes: bisect, slice, spin, mirror — plus a\n// knife SESSION lifecycle demo. bisect + slice are interactive by default;\n// pass oneShot:true to run them HEADLESS in a script. Self-contained.\n//\n// 1) BISECT — cut along an axis-aligned plane. Headless needs oneShot:true\n//    plus at least one of fill / clearInner / clearOuter true.\nconst a = await create.cube({ width: 1, height: 1, depth: 1, subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3, name: 'Bisect' });\na.translate.set([-3, 1.3, -0.7]);\nawait select(null, { mode: 'clear' });\nconst a0 = { v: a.verts.count, f: a.faces.count };\nawait a.bisect({\n  oneShot: true,    // run headless (no interactive drag session)\n  axis: 'x',        // plane perpendicular to X ('x'|'y'|'z')\n  offset: 0,        // signed plane position along axis, meters\n  fill: true,       // cap the cut with a new face\n  clearInner: false,// keep the -X half (default false)\n  clearOuter: false,// keep the +X half (default false)\n});\nconsole.log('bisect: V ' + a0.v + '->' + a.verts.count + ', F ' + a0.f + '->' + a.faces.count);\n\n// 2) SLICE — split into two coplanar shells. Headless = oneShot + mode 'plane'.\nconst b = await create.cube({ width: 1, height: 1, depth: 1, subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3, name: 'Slice' });\nb.translate.set([-1, 1.3, -0.7]);\nawait select(null, { mode: 'clear' });\nconst b0 = { v: b.verts.count, f: b.faces.count };\nawait b.slice({\n  oneShot: true,  // headless one-shot (freeform mode needs interactive endpoints)\n  mode: 'plane',  // 'plane' = axis+offset; 'freeform' is interactive-only\n  axis: 'y',      // cut perpendicular to Y\n  offset: 0,      // plane position, meters\n  fill: false,    // slice leaves the cut open (fill is reserved on slice)\n});\nconsole.log('slice: V ' + b0.v + '->' + b.verts.count + ', F ' + b0.f + '->' + b.faces.count);\n\n// 3) SPIN — revolve a profile around an axis. axis is a LETTER (not a vector);\n//    angle is in DEGREES (360 = full revolution); steps = segments.\nconst c = await create.cube({ width: 1, height: 1, depth: 1, subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3, name: 'Spin' });\nc.translate.set([1, 1.3, -0.7]);\nawait select(null, { mode: 'clear' });\nconst c0 = { v: c.verts.count, f: c.faces.count };\nawait c.spin({ axis: 'y', angle: 180, steps: 8 }); // half revolution in 8 steps\nconsole.log('spin: V ' + c0.v + '->' + c.verts.count + ', F ' + c0.f + '->' + c.faces.count);\n\n// 4) MIRROR (topology) — this REBUILDS a symmetric mesh from a HALF mesh; it\n//    is a SILENT NO-OP on a full/already-symmetric cube (nothing to add). So we\n//    first cut a half: bisect with clearOuter deletes the +X side, leaving only\n//    the -X half. Then mirror with direction:-1 (keep -axis, reflect to +axis)\n//    + weld rebuilds the full mesh — the vert count must roughly DOUBLE.\nconst d = await create.cube({ width: 1, height: 1, depth: 1, subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2, name: 'Mirror' });\nd.translate.set([3, 1.3, -0.7]);\nawait select(null, { mode: 'clear' });\n// 4a) Bisect at X=0 and DROP the +X half → a true half-mesh.\nawait d.bisect({ oneShot: true, axis: 'x', offset: 0, fill: false, clearOuter: true });\nconst dHalf = { v: d.verts.count, f: d.faces.count };\nconsole.log('mirror half-mesh: V=' + dHalf.v + ' F=' + dHalf.f);\n// 4b) Mirror the surviving -X half back across X and weld the seam.\nawait d.mirror({\n  axis: 'x',          // symmetry plane perpendicular to X\n  style: 'topology',  // rebuild geometry (default) vs 'positions'\n  direction: -1,      // keep -X side (the half we kept), reflect to +X\n  weld: true,         // weld the seam verts after mirroring\n});\nconsole.log('mirror: V ' + dHalf.v + '->' + d.verts.count + ', F ' + dHalf.f + '->' + d.faces.count\n  + ' (doubled=' + (d.verts.count > dHalf.v) + ')');\n\n// 5) KNIFE — interactive-only cut tool. Open a session, then cancel it to\n//    demonstrate the session lifecycle (no headless commit path).\nconst knifeSession = await a.knife({ snapMode: 'edge' }); // snap to edges\nif (knifeSession && typeof knifeSession.cancel === 'function') {\n  await knifeSession.cancel();\n  console.log('knife session opened + cancelled');\n}\n\nawait Utils.wait.frame();\nviewport.frameAll(1.5);\nconsole.log('topology cut tools complete: bisect / slice / spin / mirror + knife');\n",
      "expectedOutput": "console: V/F deltas across the five cut/mirror tools."
    },
    {
      "kind": "recipe",
      "id": "model-unwrap-and-pin",
      "title": "Unwrap a cube and pin a UV corner",
      "description": "Create a cube, run an automatic UV unwrap, then pin a single UV so subsequent re-unwraps preserve that corner. Logs the UV count and the pinned position.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "UVSet.unwrap",
        "UVSet.pin",
        "mesh.uv",
        "mesh.uv.get",
        "mesh.uv.addSet"
      ],
      "code": "// Unwrap a cube and pin one UV loop so future re-unwraps leave it in\n// place. UV scripting goes through the mesh.uv collection. Self-contained.\n\n// A subdivided cube gives the unwrap more loops to lay out.\nconst cube = await create.cube({\n  width: 1, height: 1, depth: 1,\n  subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n  name: 'PinnedCube',\n});\ncube.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nawait Utils.wait.frame();\n\n// uv.count = number of face-corners (loops) on the active channel.\nconst startLoops = cube.uv.count;\nconsole.log('UV loops before unwrap:', startLoops);\n\n// Auto-unwrap with the smart angle-based projection engine\n// ('conformal' preserves angles exactly; 'cube-projection' is blocky).\n// setIdx 0 targets the first UV channel.\nawait cube.uv.unwrap({ strategy: 'smartProject', setIdx: 0 });\nconsole.log('UV islands after unwrap:', cube.uv.islands.length);\n\n// Read loop 0's [u,v] so we know what we're pinning. uv.get() returns a\n// flat Float32Array packed [u0, v0, u1, v1, ...].\nconst uvs = cube.uv.get(0);\nconst u0 = uvs[0].toFixed(3);\nconst v0 = uvs[1].toFixed(3);\nconsole.log('pinning UV loop 0 at (' + u0 + ', ' + v0 + ')');\n\n// Pin loops 0 and 1 on channel 0. Pinned loops are held in place by\n// subsequent unwrap / relax passes.\nawait cube.uv.pin({ loops: [0, 1], setIdx: 0 });\nconsole.log('loop 0 pinned?', cube.uv.isPinned(0, 0)); // true\nconsole.log('loop 2 pinned?', cube.uv.isPinned(2, 0)); // false\n\n// Pinning never changes the loop count.\nconsole.log('UV loops after pin:', cube.uv.count, '(unchanged from', startLoops + ')');\nconsole.log('unwrap + pin complete on \"' + cube.name + '\"');\n",
      "expectedOutput": "console: number of UVs before / after the unwrap and the pinned UV position."
    },
    {
      "kind": "recipe",
      "id": "model-uv-authoring-suite",
      "title": "UV authoring suite: setUV / setUVs / transferUV / closestUV / unwrapUV / uvIsland / uvIslandAt / packUV / markSeam",
      "description": "Unwrap a cube, list its UV islands, pack tightly, write one UV via setUV, write a whole UV map via setUVs, sample closestUV, mark a seam edge, and transfer the UV map to a sphere.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.create.sphere",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.unwrapUV",
        "mesh.packUV",
        "mesh.uv.setOne",
        "mesh.uv.set",
        "mesh.transferUV",
        "mesh.uv.closest",
        "mesh.uv.island",
        "mesh.uv.islandAt",
        "mesh.markSeam"
      ],
      "code": "// Full UV-authoring pass on the mesh.uv collection: unwrap, pack, write one\n// loop, write the whole map, query islands, sample the nearest loop, mark a\n// seam, and transfer the layout to a topology-matched sibling. From empty.\n\nconst cube = await create.cube({\n  width: 1, height: 1, depth: 1,\n  subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n  name: 'UVSrc',\n});\ncube.xform({ t: [-2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\n\n// UNWRAP + PACK — generate islands then repack them into the unit square.\nawait cube.uv.unwrap({ strategy: 'smartProject', setIdx: 0 }); // 'fable' | 'xatlas' | 'smartProject' | 'pipeline'\nawait cube.uv.pack({ setIdx: 0 });\nconsole.log('unwrap + pack islands:', cube.uv.islands.length);\n\n// WRITE ONE — pin loop 0 to the UV center.\ncube.uv.setOne(0, [0.5, 0.5], 0); // (loopIndex, [u,v], setIdx)\n\n// WRITE WHOLE MAP — lay every loop on a 4x4 grid. uv.set takes a flat\n// [u0,v0,u1,v1,...] buffer of length 2 * count.\nconst totalLoops = cube.uv.count;\nconst uvs = new Float32Array(totalLoops * 2);\nfor (let i = 0; i < totalLoops; i++) {\n  uvs[i * 2] = (i % 4) * 0.25;                 // u\n  uvs[i * 2 + 1] = (Math.floor(i / 4) % 4) * 0.25; // v\n}\ncube.uv.set(uvs, 0);\n\n// QUERY ISLANDS — island(loop) by loop; islandAt([u,v]) by UV position.\nconst island = cube.uv.island(0);\nconsole.log('island(0) loops:', island ? island.loopIndices.length : 'null');\nconst found = cube.uv.islandAt([0.1, 0.1]);\nconsole.log('islandAt([0.1,0.1]):', found ? 'hit (island ' + found.index + ')' : 'miss');\n\n// NEAREST LOOP — closest([u,v]) returns the nearest loop + its distance.\nconst near = cube.uv.closest([0.5, 0.5]);\nconsole.log('closest([0.5,0.5]) loopIndex:', near ? near.loopIndex : 'null');\n\n// MARK SEAM — flag edges 0..2 as UV seams (honored by future unwraps).\nawait cube.uv.markSeam({ edges: [0, 1, 2], setIdx: 0 });\nconsole.log('any seam loop:', cube.uv.loopSeam().some(Boolean));\n\n// TRANSFER — copy the UV layout onto a topology-matched sibling cube.\nconst dst = await create.cube({\n  width: 1, height: 1, depth: 1,\n  subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2, // SAME topology = clean transfer\n  name: 'UVDst',\n});\ndst.xform({ t: [2.5, 1.3, -0.7] });\nawait select(null, { mode: 'clear' });\nawait dst.uv.transferFrom(cube, { setIdx: 0 });\nconsole.log('after transferFrom -> dst islands:', dst.uv.islands.length);\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('UV authoring suite complete.');\n",
      "expectedOutput": "console: island count, closestUV hit, transfer dst island count."
    },
    {
      "kind": "recipe",
      "id": "model-vertex-attribute-write-suite",
      "title": "Vertex attribute writes: setPoints / setVertexPositions / setNormals / setVertexColor(s) / mapVertices / markSharp",
      "description": "Spawn a subdivided cube; read points, mirror them with setPoints, write a per-index update with setVertexPositions, override normals + vertex colors, map every vertex through a closure, and mark sharp edges.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "ModelEditor.viewport.frameAll",
        "node.translate",
        "mesh.points",
        "mesh.verts.setPositions",
        "mesh.setVertexPositions",
        "mesh.normals.set",
        "mesh.setVertexColor",
        "mesh.setVertexColors",
        "mesh.vertexColors",
        "mesh.tangents",
        "mesh.mapVertices",
        "mesh.markSharp"
      ],
      "code": "// Self-contained: build a subdivided cube, then write through every\n// vertex-attribute channel on the customer surface (mesh.verts.* + mesh.normals.* + mesh.edges.*).\nconst cube = await create.cube({\n  width: 1.5, height: 1.5, depth: 1.5,\n  subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3, // dense enough to see attribute writes\n  name: 'AttrCube',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nselect(null, { mode: 'clear' });\n\n// READ — verts.getPositions returns a flat [x,y,z,...] Float32Array.\n//        space: 'object' = local (default) | 'world' = parent-transformed.\nconst pts = cube.verts.getPositions({ space: 'object' });\nconsole.log('positions sample [0..2]:', pts[0].toFixed(2), pts[1].toFixed(2), pts[2].toFixed(2));\n\n// WRITE (flat-buffer shape) — mirror every vertex across X by negating each x component.\nconst mirrored = new Float32Array(pts.length);\nfor (let i = 0; i < pts.length; i++) mirrored[i] = i % 3 === 0 ? -pts[i] : pts[i];\nawait cube.verts.setPositions(mirrored);\n\n// WRITE (index shape) — nudge only verts 0 and 1 up by 0.05 in object space.\nconst first = cube.verts.getPositions({ space: 'object' });\nawait cube.verts.setPositions(\n  [0, 1],                                                       // target vertex indices\n  [[first[0], first[1] + 0.05, first[2]], [first[3], first[4] + 0.05, first[5]]],\n  { space: 'object' },                                          // interpret values as local-space\n);\n\n// NORMALS — read the live buffer, then write it straight back (round-trip).\nconst n = cube.normals.get().slice();\ncube.normals.set(n);\n\n// VERTEX COLORS — single vertex via setColor, then a whole-buffer overwrite via setColors.\nawait cube.verts.setColor(0, [1, 0, 0, 1]);                     // vertex 0 -> opaque red (r,g,b,a in 0..1)\nconst green = new Float32Array(cube.verts.count * 4);\nfor (let i = 0; i < cube.verts.count; i++) {\n  green[i * 4 + 0] = 0;                                         // r\n  green[i * 4 + 1] = 1;                                         // g\n  green[i * 4 + 2] = 0;                                         // b\n  green[i * 4 + 3] = 1;                                         // a\n}\ncube.verts.setColors(green);                                    // paint every vertex solid green\nconsole.log('colors len:', (cube.verts.colors ? cube.verts.colors.length : 0), 'tangents len:', cube.verts.tangents.length);\n\n// MAP — run a closure over every vertex; here an identity pass that just counts.\nlet touched = 0;\nawait cube.verts.map((i, total) => {\n  touched++;\n  const p = cube.verts.position(i);\n  return [p.x, p.y, p.z];                                       // return the (unchanged) position\n});\nconsole.log('verts.map touched:', touched, 'of', cube.verts.count);\n\n// SHARP EDGES — flag edges 0 and 1 as hard creases for shading.\nawait cube.edges.markSharp({ indices: [0, 1] });\n\nawait Utils.wait.frame();\nviewport.frameAll();\nconsole.log('vertex attribute write-through complete.');\n",
      "expectedOutput": "console: points sample, normal/color buffer lengths, mapVertices count."
    },
    {
      "kind": "recipe",
      "id": "model-vertex-query-sweep",
      "title": "Vertex iteration + nearest / within radius queries",
      "description": "Spawn a subdivided cube; forEachVertex + log; query nearestVertices + verticesWithin.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "mesh.forEachVertex",
        "mesh.nearestVertices",
        "mesh.verticesWithin",
        "mesh.verts",
        "mesh.verts.getPositions",
        "node.translate",
        "Utils.wait.frame"
      ],
      "code": "// Vertex iteration + nearest / within-radius queries on a subdivided cube.\n// Self-contained from an empty scene.\nconst cube = await create.cube({\n  width: 2, height: 2, depth: 2,\n  subdivisionsX: 2, subdivisionsY: 2, subdivisionsZ: 2,\n  name: 'VertCube',\n});\ncube.translate.set([2.5, 1.3, -0.7]);\nawait select(null, { mode: 'clear' });\nawait Utils.wait.frame();\n\nconsole.log('vert count:', cube.verts.count);\n\n// getPositions returns a FLAT Float32Array [x0,y0,z0, x1,y1,z1, ...].\n// Pass space: 'object' for local coords, 'world' to apply the transform.\nconst pts = cube.verts.getPositions({ space: 'object' });\nconsole.log('first vertex (object):',\n  pts[0].toFixed(3), pts[1].toFixed(3), pts[2].toFixed(3));\n\n// verts.forEach((i, total) => ...) visits every vertex index in order.\nlet counted = 0;\nlet sumX = 0;\ncube.verts.forEach((i, total) => {\n  counted++;\n  sumX += pts[i * 3]; // accumulate object-space X to confirm we touched each\n});\nconsole.log('verts.forEach iterated:', counted, 'of', cube.verts.count,\n  '| mean X =', (sumX / counted).toFixed(3));\n\n// verts.nearest(point, k) returns the k closest vertex INDICES. Vertex spatial\n// queries read OBJECT space, so query around the local centroid [0, 0, 0].\nconst nearest = cube.verts.nearest([0, 0, 0], 5); // k = 5 closest\nconsole.log('nearest 5 to local centroid:', JSON.stringify(nearest));\n\n// verts.within(point, radius) returns every vertex INDEX inside the sphere.\nconst within = cube.verts.within([0, 0, 0], 1.5); // radius = 1.5m, object space\nconsole.log('within 1.5m of local centroid:', within.length, 'verts');\n",
      "expectedOutput": "console: cube vertex count, point sample, nearest-5 verts, within-radius count."
    },
    {
      "kind": "recipe",
      "id": "model-viewport-mode-sweep",
      "title": "Toggle render modes, component modes, and sculpt mode in the Model editor",
      "description": "Frame view; flip wireframe / xray / backface-cull / grid / normals / live-snap; cycle render and component modes; enter and exit sculpt mode.",
      "intent": "io",
      "editor": "model",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.viewport.frameAll",
        "ModelEditor.viewport.frameSelection",
        "ModelEditor.viewport.setRenderMode",
        "ModelEditor.viewport.renderMode",
        "ModelEditor.setComponentMode",
        "ModelEditor.getComponentMode",
        "ModelEditor.viewport.setBackfaceCull",
        "ModelEditor.viewport.setShowGrid",
        "ModelEditor.viewport.setShowNormals",
        "ModelEditor.viewport.setLiveMeshSnap",
        "ModelEditor.viewport.setWireframe",
        "ModelEditor.viewport.setXray",
        "ModelEditor.setActiveMaterial",
        "ModelEditor.getActiveMaterial",
        "ModelEditor.tool.sculpt.enter",
        "ModelEditor.io.importModel",
        "ModelEditor.io.exportFile",
        "ModelEditor.io.exportSelectedModel",
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "node.translate"
      ],
      "code": "const cube = await create.cube({ name: 'ModeProbe' });\ncube.translate.set([2.5, 1.3, -0.7]);\nselect(null, {mode: 'clear'});\nawait Utils.wait.frame();\n\nviewport.frameAll();\nviewport.frameSelection();\n\nconst renderBefore = viewport.renderMode;\nviewport.setRenderMode('clay');\nconsole.log('renderMode ' + renderBefore + ' → ' + viewport.renderMode);\n\nconst compBefore = getComponentMode();\nsetComponentMode('vertex');\nconsole.log('componentMode ' + compBefore + ' → ' + getComponentMode());\nsetComponentMode('object');\n\nviewport.setBackfaceCull(true);\nviewport.setShowGrid(false);\nviewport.setShowNormals(false);\nviewport.setLiveMeshSnap(false);\nviewport.setWireframe(false);\nviewport.setXray(false);\nconsole.log('viewport flags set');\n\nconst matBefore = getActiveMaterial();\nconsole.log('activeMaterial:', matBefore);\n\nasync function probe(label, fn) {\n  try {\n    await fn();\n    console.log(label, 'ok');\n  } catch (err) {\n    console.log(label, 'skipped:', err.message);\n  }\n}\n\n// Sculpt enter / exit goes through the SculptSession lifecycle: enter() returns\n// a session handle, and you end it by calling .cancel() (or .commit()) on that\n// handle rather than a separate exit free-function.\nlet __sculptSess = null;\nawait probe('enterSculptMode', async () => { __sculptSess = await tool.sculpt.enter(); });\nif (__sculptSess && typeof __sculptSess.cancel === 'function') {\n  __sculptSess.cancel();\n  console.log('SculptSession.cancel() called');\n}\n\nawait probe('import', () => io.importModel({}));\nawait probe('export', () => io.exportFile({ filename: 'probe.glb', format: 'glb' }));\nawait probe('exportSelection', () => io.exportSelectedModel({ filename: 'sel.glb', format: 'glb' }));\n",
      "expectedOutput": "console: render/component before/after; sculpt enter/exit; import/export probe."
    },
    {
      "kind": "recipe",
      "id": "openscad-extrusions-1515-corner-frame",
      "title": "OpenBeam 1515 corner frame",
      "description": "An L-shaped corner made from two lengths of OpenBeam 15x15 t-slot extrusion meeting at a right angle, the kind of joint used to build the edges of a printer or enclosure frame. Set the length of each leg independently.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// OpenBeam 1515 corner frame\n//\n// Source: openscad-extrusions — MIT\n// Generated from the library above, used under its license.\n\nuse <openscad-extrusions/OpenBeam/1515.scad>\n\nleg_a = 90;   // horizontal leg length in mm\nleg_b = 70;   // vertical leg length in mm\n$fn = 48;\n\n// Horizontal rail lying along +X.\nrotate([0, 90, 0])\n    OpenBeam_1515(leg_a);\n\n// Vertical rail rising in +Z, butted to the end of the horizontal rail.\ntranslate([leg_a, 0, 0])\n    OpenBeam_1515(leg_b);\n`);"
    },
    {
      "kind": "recipe",
      "id": "openscad-extrusions-1515-cube-frame",
      "title": "OpenBeam 1515 cube frame",
      "description": "A full open box frame built from twelve lengths of OpenBeam 15x15 t-slot extrusion, the skeleton of an enclosure, light box, or display cube. Set the inner edge size and all twelve rails are placed automatically.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// OpenBeam 1515 cube frame\n//\n// Source: openscad-extrusions — MIT\n// Generated from the library above, used under its license.\n\nuse <openscad-extrusions/OpenBeam/1515.scad>\n\nedge = 100;   // outer cube edge length in mm\n$fn = 32;\n\nbar = 15;             // 1515 profile width in mm\nspan = edge - 2 * bar; // clear rail length between corner posts\nov = 0.6;             // overlap so rails fuse into the posts\n\n// Four vertical posts at the corners.\nfor (x = [0, span + bar], y = [0, span + bar])\n    translate([x, y, 0])\n        OpenBeam_1515(edge);\n\n// Bottom and top horizontal rails along X (overlapped into both posts).\nfor (z = [0, span + bar], y = [0, span + bar])\n    translate([bar - ov, y, z + bar / 2])\n        rotate([0, 90, 0])\n            OpenBeam_1515(span + 2 * ov);\n\n// Bottom and top horizontal rails along Y (overlapped into both posts).\nfor (z = [0, span + bar], x = [0, span + bar])\n    translate([x, bar - ov, z + bar / 2])\n        rotate([-90, 0, 0])\n            OpenBeam_1515(span + 2 * ov);\n`);"
    },
    {
      "kind": "recipe",
      "id": "openscad-extrusions-1515-post",
      "title": "OpenBeam 1515 extrusion post",
      "description": "A single length of OpenBeam 15x15 aluminium-style t-slot extrusion with four open channels and a center bore, ready to use as a frame rail or leg. Adjust the length to suit your build.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// OpenBeam 1515 extrusion post\n//\n// Source: openscad-extrusions — MIT\n// Generated from the library above, used under its license.\n\nuse <openscad-extrusions/OpenBeam/1515.scad>\n\nlength = 80;   // post length in mm\n$fn = 48;\n\nOpenBeam_1515(length);\n`);"
    },
    {
      "kind": "recipe",
      "id": "openscad-extrusions-1515-rail-bed",
      "title": "OpenBeam 1515 parallel rail bed",
      "description": "A row of evenly spaced parallel OpenBeam 15x15 t-slot rails, like the gantry or bed support of a 3D printer or CNC. Choose how many rails, how far apart they sit, and how long each one runs.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// OpenBeam 1515 parallel rail bed\n//\n// Source: openscad-extrusions — MIT\n// Generated from the library above, used under its license.\n\nuse <openscad-extrusions/OpenBeam/1515.scad>\n\nrail_count = 4;    // number of parallel rails\nspacing    = 40;   // center-to-center spacing in mm\nlength     = 120;  // rail length in mm\n$fn = 40;\n\nfor (i = [0 : rail_count - 1])\n    translate([0, i * spacing, 0])\n        rotate([0, 90, 0])\n            OpenBeam_1515(length);\n`);"
    },
    {
      "kind": "recipe",
      "id": "outliner-collapse-and-clear",
      "title": "Collapse all and clear filter on the outliner",
      "description": "Set a filter; clear it; collapse all branches; spawn a few nodes to drive the outliner state.",
      "intent": "mesh",
      "editor": "model",
      "category": "outliner",
      "apiSurfaces": [
        "ModelEditor.dev.outliner.collapseAll",
        "ModelEditor.dev.outliner.clearFilter",
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "node.translate"
      ],
      "code": "const a = await create.cube({ name: 'OutlinerA' });\na.translate.set([2.5, 1.3, -0.7]); select(null, {mode: 'clear'});\nconst b = await create.cube({ name: 'OutlinerB' });\nb.translate.set([5.0, 1.3, -0.7]); select(null, {mode: 'clear'});\nconst c = await create.cube({ name: 'OutlinerC' });\nc.translate.set([7.5, 1.3, -0.7]); select(null, {mode: 'clear'});\nawait Utils.wait.frame();\n\ndev.outliner.collapseAll();\nconsole.log('collapseAll ok');\n\ndev.outliner.clearFilter();\nconsole.log('clearFilter ok');\n",
      "expectedOutput": "console: outliner ops fired without throwing."
    },
    {
      "kind": "recipe",
      "id": "outliner-sort-and-lock",
      "title": "Sort + lock the outliner for a clean review",
      "description": "Move named heroes to the top, lock finished assets, and filter for review.",
      "intent": "mesh",
      "editor": "model",
      "category": "outliner",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "ModelEditor.dev.outliner.expandAll",
        "ModelEditor.dev.outliner.setFilter",
        "ModelEditor.dev.outliner.setDisplayMode",
        "node.moveToIndex",
        "node.lock",
        "node.isLocked"
      ],
      "code": "// Sort + lock for a clean outliner review pass.\n// Promote anything named \"Hero*\" to the top of the sibling list.\nconst heroes = ls({ type: 'mesh', name: /^Hero/ });\nfor (const h of heroes) h.moveToIndex(0);\n\n// Lock every mesh tagged \"_FINAL\" in its name so a stray drag won't disturb it.\nfor (const m of ls({ type: 'mesh', name: /_FINAL$/ })) {\n  if (!m.isLocked) m.lock();\n}\n\n// Open the outliner all the way + filter to the heroes for a final pass.\ndev.outliner.expandAll();\ndev.outliner.setFilter('Hero');\ndev.outliner.setDisplayMode({ showShapes: false, showTypes: true });\n\nconsole.log('promoted', heroes.length, 'heroes; locked all _FINAL meshes.');\n",
      "expectedOutput": "console line with hero count and lock count; outliner expanded + filtered to \"Hero\"."
    },
    {
      "kind": "recipe",
      "id": "path-basename-extname",
      "title": "Parse filename and extension from a path string",
      "description": "Demonstrate Utils.path.basename and Utils.path.extname against a variety of input paths.",
      "intent": "scene",
      "editor": "previewer",
      "category": "io",
      "apiSurfaces": [
        "Utils.path.basename",
        "Utils.path.extname",
        "Utils.wait.ms"
      ],
      "code": "const samples = [\n  '/scenes/intro/hero.glb',\n  'C:/exports/run_01.fbx',\n  'noslash.obj',\n  '/trailing/slash/',\n  '/multi.dot.name.glb',\n  '/no-extension',\n];\n\nfor (const p of samples) {\n  console.log(p, '→ basename=' + JSON.stringify(Utils.path.basename(p))\n    + ', extname=' + JSON.stringify(Utils.path.extname(p)));\n}\n\n// Strip extension via basename's second arg\nconst stripped = Utils.path.basename('/path/scene.animbox', '.animbox');\nconsole.log('stripped:', JSON.stringify(stripped));\n\nawait Utils.wait.ms(0);\nconsole.log('ms(0) returned');\n",
      "expectedOutput": "console: basename + extname for each sample path."
    },
    {
      "kind": "recipe",
      "id": "pd-gears-helical-gear",
      "title": "Helical spur gear",
      "description": "An involute gear whose teeth twist along the thickness for quieter, smoother running than a straight-cut gear. Increase the twist for a steeper helix angle.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Helical spur gear\n//\n// Source: pd-gears — Unlicense\n// Generated from the library above, used under its license.\n\nuse <pd-gears/pd-gears.scad>\n\nmm_per_tooth  = 3;   // circular pitch in mm\nteeth         = 19;  // number of teeth\nthickness     = 10;  // gear thickness in mm (helix spans this)\nhole_diameter = 5;   // shaft bore diameter in mm\ntwist_deg     = 30;  // total tooth twist top-to-bottom (helix amount)\n\ngear(mm_per_tooth = mm_per_tooth,\n     number_of_teeth = teeth,\n     thickness = thickness,\n     hole_diameter = hole_diameter,\n     twist = twist_deg,\n     pressure_angle = 20,\n     center = true,\n     $fn = 48);\n`);"
    },
    {
      "kind": "recipe",
      "id": "pd-gears-hubbed-gear",
      "title": "Gear with reinforced hub",
      "description": "An involute spur gear with a raised cylindrical hub around the bore that adds grip length for a set screw or press-fit shaft. Tune the hub height and diameter for your shaft.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Gear with reinforced hub\n//\n// Source: pd-gears — Unlicense\n// Generated from the library above, used under its license.\n\nuse <pd-gears/pd-gears.scad>\n\nmm_per_tooth  = 3;   // circular pitch in mm\nteeth         = 21;  // number of teeth\nthickness     = 5;   // gear disc thickness in mm\nhole_diameter = 5;   // shaft bore diameter in mm\nhub_diameter  = 14;  // outer diameter of the reinforcing hub in mm\nhub_height    = 7;   // hub length above the gear face in mm\n\ndifference() {\n    union() {\n        // Gear disc sitting on the build plate.\n        gear(mm_per_tooth, teeth, thickness, hole_diameter,\n             pressure_angle = 20, $fn = 48);\n        // Reinforcing hub rising from the top face.\n        translate([0, 0, thickness])\n            cylinder(h = hub_height, d = hub_diameter, $fn = 48);\n    }\n    // Bore drilled all the way through gear + hub.\n    translate([0, 0, -0.1])\n        cylinder(h = thickness + hub_height + 0.2, d = hole_diameter, $fn = 48);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "pd-gears-meshing-pair",
      "title": "Meshing gear pair",
      "description": "Two involute spur gears spaced so their pitch circles touch and the teeth mesh, ready to print as a working reduction stage. Adjust the pinion and wheel tooth counts to set the gear ratio.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Meshing gear pair\n//\n// Source: pd-gears — Unlicense\n// Generated from the library above, used under its license.\n\nuse <pd-gears/pd-gears.scad>\n\nmm_per_tooth = 4;   // shared circular pitch (must match for meshing)\npinion_teeth = 11;  // small driving gear\nwheel_teeth  = 23;  // large driven gear\nthickness    = 6;   // gear thickness in mm\nbore         = 5;   // shaft bore diameter in mm\n\n// Center-to-center distance = sum of the two pitch radii so teeth mesh.\nspacing = pitch_radius(mm_per_tooth, pinion_teeth)\n        + pitch_radius(mm_per_tooth, wheel_teeth);\n\ngear(mm_per_tooth, pinion_teeth, thickness, bore, pressure_angle = 20, $fn = 48);\n\ntranslate([spacing, 0, 0])\n    gear(mm_per_tooth, wheel_teeth, thickness, bore, pressure_angle = 20, $fn = 48);\n`);"
    },
    {
      "kind": "recipe",
      "id": "pd-gears-rack-and-pinion",
      "title": "Rack and pinion set",
      "description": "A toothed rack bar paired with a round pinion gear that rolls along it, converting rotation into straight-line travel. Print both to build a linear actuator or sliding mechanism.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Rack and pinion set\n//\n// Source: pd-gears — Unlicense\n// Generated from the library above, used under its license.\n\nuse <pd-gears/pd-gears.scad>\n\nmm_per_tooth = 5;   // shared circular pitch (must match for meshing)\nrack_teeth   = 10;  // teeth along the rack bar\npinion_teeth = 12;  // teeth on the round pinion\nthickness    = 6;   // part thickness in mm\nrack_height  = 14;  // rack body height (tooth tip to far edge) in mm\nbore         = 5;   // pinion shaft bore in mm\n\n// Rack pitch line runs along X; place the pinion so its pitch circle sits on it.\nrack(mm_per_tooth, rack_teeth, thickness, rack_height, pressure_angle = 20);\n\ntranslate([rack_teeth * mm_per_tooth / 2,\n           pitch_radius(mm_per_tooth, pinion_teeth), 0])\n    gear(mm_per_tooth, pinion_teeth, thickness, bore, pressure_angle = 20, $fn = 48);\n`);"
    },
    {
      "kind": "recipe",
      "id": "pd-gears-spur-gear",
      "title": "Involute spur gear",
      "description": "A single printable involute spur gear with a centered shaft bore. Tune the tooth count, tooth pitch, thickness, and hole diameter to fit your drivetrain.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Involute spur gear\n//\n// Source: pd-gears — Unlicense\n// Generated from the library above, used under its license.\n\nuse <pd-gears/pd-gears.scad>\n\nmm_per_tooth  = 3;   // circular pitch (arc length per tooth, mm)\nteeth         = 17;  // number of teeth around the gear\nthickness     = 6;   // gear thickness in mm\nhole_diameter = 5;   // center shaft bore diameter in mm\n\ngear(mm_per_tooth = mm_per_tooth,\n     number_of_teeth = teeth,\n     thickness = thickness,\n     hole_diameter = hole_diameter,\n     pressure_angle = 20,\n     center = true,\n     $fn = 48);\n`);"
    },
    {
      "kind": "recipe",
      "id": "poly-primitives-cone-hemisphere-pyramid-torus-wedge",
      "title": "Spawn cone, hemisphere, pyramid, torus, and wedge primitives",
      "description": "Cover the five non-class poly mesh constructors that complete the primitives table.",
      "intent": "mesh",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.create.cone",
        "ModelEditor.create.hemisphere",
        "ModelEditor.create.pyramid",
        "ModelEditor.create.torus",
        "ModelEditor.create.wedge",
        "Utils.wait.frame"
      ],
      "code": "// Self-contained from an EMPTY Model scene: spawn the five non-class poly\n// primitives, spacing them along +X. Each constructor has a DISTINCT schema —\n// every option below is set to a sensible NON-default value, with inline\n// comments enumerating meaning / range / generator default. create.* are async.\nlet x = -5;\nconst step = 2.5;\n\n// ── Cone ── radius (base) / height / radialSegments (sides around base) /\n// heightSegments (rings up the side) / openEnded (drop the bottom cap?).\nconst cone = await create.cone({\n  radius: 0.8,           // base radius, m; >0 (default 0.5)\n  height: 1.5,           // apex height, m; >0 (default 1)\n  radialSegments: 24,    // facets around the base; int >0 (default 32)\n  heightSegments: 2,     // vertical subdivisions; int >0 (default 1)\n  openEnded: false,      // false keeps the base cap; true leaves it open (default false)\n  name: 'Cone1',\n});\ncone.translate.set([x, 1.3, -0.7]); select(null, { mode: 'clear' });\nconsole.log('polyCone:', cone.name, 'verts=' + cone.verts.count); x += step;\n\n// ── Hemisphere ── radius / widthSegments (longitude) / heightSegments\n// (latitude) / flatBase (cap the open dome with a disc?).\nconst hemi = await create.hemisphere({\n  radius: 0.9,           // dome radius, m; >0 (default 0.5)\n  widthSegments: 24,     // longitude divisions; int >0 (default 32)\n  heightSegments: 12,    // latitude divisions; int >0 (default 8)\n  flatBase: true,        // true adds the flat base disc; false = open dome (default true)\n  name: 'Hemi1',\n});\nhemi.translate.set([x, 1.3, -0.7]); select(null, { mode: 'clear' });\nconsole.log('polyHemisphere:', hemi.name, 'verts=' + hemi.verts.count); x += step;\n\n// ── Pyramid ── baseRadius (NOT 'radius') / height / sides (base polygon) /\n// truncation (0..1 tip-flatten ratio; 0 = pointed apex).\nconst pyr = await create.pyramid({\n  baseRadius: 0.7,       // circumradius of the base polygon, m; >0 (default 0.5)\n  height: 1.2,           // apex height, m; >0 (default 1)\n  sides: 6,              // base polygon sides; int >0 (default 4)\n  truncation: 0.3,       // flatten the tip into a frustum; 0..1 ratio (default 0 = sharp apex)\n  name: 'Pyr1',\n});\npyr.translate.set([x, 1.3, -0.7]); select(null, { mode: 'clear' });\nconsole.log('polyPyramid:', pyr.name, 'verts=' + pyr.verts.count); x += step;\n\n// ── Torus ── radius (major ring) / tube (minor; MUST be < radius) /\n// radialSegments (around the tube) / tubularSegments (around the ring) / arc.\nconst torus = await create.torus({\n  radius: 0.7,           // major (ring) radius, m; >0 (default 0.5)\n  tube: 0.2,             // minor (tube) radius, m; >0 and STRICTLY < radius (default 0.2)\n  radialSegments: 20,    // facets around the tube cross-section; int >0 (default 16)\n  tubularSegments: 40,   // facets around the major ring; int >0 (default 48)\n  arc: Math.PI * 2,      // sweep angle in radians; >0 (default 2*PI = full ring)\n  name: 'Torus1',\n});\ntorus.translate.set([x, 1.3, -0.7]); select(null, { mode: 'clear' });\nconsole.log('polyTorus:', torus.name, 'verts=' + torus.verts.count); x += step;\n\n// ── Wedge ── triangular prism (ramp): width / height / depth, all in m.\nconst wedge = await create.wedge({\n  width: 1.2,            // X span, m; >0 (default 1)\n  height: 1.5,           // Y rise of the ramp, m; >0 (default 1)\n  depth: 1.0,            // Z run, m; >0 (default 1)\n  name: 'Wedge1',\n});\nwedge.translate.set([x, 1.3, -0.7]); select(null, { mode: 'clear' });\nconsole.log('polyWedge:', wedge.name, 'verts=' + wedge.verts.count);\n\nawait Utils.wait.frame(); // let the render settle before the summary\nconsole.log('Spawned 5 poly primitives:', [cone, hemi, pyr, torus, wedge].map((m) => m.name).join(', '));\n",
      "expectedOutput": "console: 5 primitive spawn lines with vert counts."
    },
    {
      "kind": "recipe",
      "id": "pre-asset-import-and-frame",
      "title": "Import an asset, set the render mode, frame it",
      "description": "End-to-end Previewer pipeline. Pick a file, set the render mode, frame the camera on everything, then capture a single screenshot and report the renderer triangle count.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "io",
      "apiSurfaces": [
        "PreviewEditor.io.openFile",
        "PreviewEditor.viewport.setRenderMode",
        "PreviewEditor.viewport.frameAll",
        "PreviewEditor.viewport.captureScreenshot",
        "ModelEditor.dev.debug.getRendererState"
      ],
      "code": "// Import a model, frame it, grab one shot, report what the renderer sees.\nconst nodes = await io.openFile();\nconsole.log('imported', nodes.length, 'top-level nodes');\n\nviewport.setRenderMode('textured');\nviewport.frameAll();\nawait new Promise((r) => requestAnimationFrame(() => r(null)));\n\nconst shot = await viewport.captureScreenshot();\nconsole.log('shot', shot.width + 'x' + shot.height);\n\nconst state = dev.debug.getRendererState();\nif (state) console.log('triangles in frame:', state.render.triangles);\n",
      "expectedOutput": "console: imported node count + screenshot dimensions + triangle count from the renderer state."
    },
    {
      "kind": "recipe",
      "id": "pre-camera-orbit-screenshot-sheet",
      "title": "Orbit the camera and capture a contact sheet",
      "description": "Frame the scene, then orbit yaw in equal steps. Capture one screenshot per step and report each image size for a quick turntable contact sheet.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.viewport.zoomToFit",
        "ModelEditor.viewport.orbit",
        "PreviewEditor.viewport.captureScreenshot",
        "ModelEditor.dev.progress"
      ],
      "code": "// Turntable contact sheet: 12 evenly-spaced yaws.\nconst steps = 12;\nviewport.zoomToFit(null, 2.0);\n\nfor (let i = 0; i < steps; i++) {\n  dev.progress({ phase: 'orbiting', current: i + 1, total: steps });\n  viewport.orbit(360 / steps, 0);\n  await new Promise((r) => requestAnimationFrame(() => r(null)));\n  const shot = await viewport.captureScreenshot();\n  const idx = String(i).padStart(2, '0');\n  // Each capture carries a binary blob you can persist later with io.saveFile.\n  console.log('orbit-' + idx, shot.width + 'x' + shot.height, 'captured', shot.blob.size, 'bytes');\n}\n",
      "expectedOutput": "One console line per orbit step with the captured image size and blob bytes."
    },
    {
      "kind": "recipe",
      "id": "pre-cycle-render-modes",
      "title": "Cycle every render mode and capture each",
      "description": "Frame the scene once, then snapshot the same camera in clay, textured, and uvchecker. Useful for asset-spec sheets that need a per-mode reference.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "PreviewEditor.viewport.frameAll",
        "PreviewEditor.viewport.setRenderMode",
        "PreviewEditor.viewport.captureScreenshot"
      ],
      "code": "// Per-render-mode reference sheet. One camera, three modes.\nviewport.frameAll();\nawait new Promise((r) => requestAnimationFrame(() => r(null)));\n\nconst modes = ['clay', 'textured', 'uvchecker'];\nfor (const mode of modes) {\n  viewport.setRenderMode(mode);\n  await new Promise((r) => requestAnimationFrame(() => r(null)));\n  const shot = await viewport.captureScreenshot();\n  // Persist any of these later with io.saveFile if you need the PNGs on disk.\n  console.log(mode, shot.width + 'x' + shot.height, 'captured', shot.blob.size, 'bytes');\n}\n",
      "expectedOutput": "One console line per render mode (clay / textured / uvchecker) with the captured image size and bytes."
    },
    {
      "kind": "recipe",
      "id": "pre-export-multi-format",
      "title": "Export the open scene to glb, fbx, and obj",
      "description": "Export the loaded scene to three formats sequentially, one file per format. Reports which formats succeeded and which threw.",
      "intent": "io",
      "editor": "previewer",
      "category": "io",
      "apiSurfaces": [
        "ModelEditor.io.exportFile",
        "ModelEditor.dev.progress"
      ],
      "code": "// One-click export to three formats. Each format gets its own try block\n// so a single format failure doesn't abort the others.\nconst formats = ['glb', 'fbx', 'obj'];\nconst results = [];\n\nfor (let i = 0; i < formats.length; i++) {\n  const fmt = formats[i];\n  dev.progress({ phase: 'exporting', current: i + 1, total: formats.length, label: fmt });\n  try {\n    const out = await io.exportFile('scene.' + fmt, fmt);\n    results.push({ fmt, size: out.size, ok: true });\n    console.log(fmt, 'ok', out.size, 'bytes');\n  } catch (err) {\n    results.push({ fmt, ok: false, error: err.message });\n    console.warn(fmt, 'failed:', err.message);\n  }\n}\nconsole.log('summary:', JSON.stringify(results));\n",
      "expectedOutput": "Up to 3 files exported; console line per format with success / failure + size."
    },
    {
      "kind": "recipe",
      "id": "pre-material-audit",
      "title": "Audit materials on the open asset",
      "description": "Walk every material in the scene, list its base color / metallic / roughness, and report which mesh references which material. A quick \"what shaders is this asset using\" sweep.",
      "intent": "mesh",
      "editor": "previewer",
      "category": "materials-textures",
      "apiSurfaces": [
        "ModelEditor.listMaterials",
        "ModelEditor.getMaterial",
        "mesh.material.get"
      ],
      "code": "// Material inventory for the currently-loaded asset.\n// listMaterials() returns { id, name } descriptors; getMaterial(id) gives the\n// Material handle whose pbr channels expose .get().\nconst mats = listMaterials();\nconsole.log('materials:', mats.length);\nfor (const { id, name } of mats) {\n  const mat = getMaterial(id);\n  const bc = mat.pbr.baseColor.get();\n  console.log('  ' + name,\n    'rgb=[' + bc[0].toFixed(2) + ',' + bc[1].toFixed(2) + ',' + bc[2].toFixed(2) + ']',\n    'metal=' + mat.pbr.metallic.get().toFixed(2),\n    'rough=' + mat.pbr.roughness.get().toFixed(2));\n}\n\n// Reverse-index: which mesh uses which material?\nconst meshes = ls({ type: 'mesh' });\nconsole.log('mesh→material map:');\nfor (const me of meshes) {\n  const mat = me.material.get();\n  console.log('  ' + me.name + ' →', mat ? mat.name : '(none)');\n}\n",
      "expectedOutput": "Console table-like log: per-material name + baseColor + metallic + roughness; mesh-to-material map."
    },
    {
      "kind": "recipe",
      "id": "pre-timeline-scrub-and-snapshot",
      "title": "Scrub an animation and snapshot each keyframe",
      "description": "Step through a frame range one second at a time, snap a screenshot at each cursor position, and report each capture for a quick storyboard.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "animation",
      "apiSurfaces": [
        "PreviewEditor.animation.currentTime",
        "PreviewEditor.viewport.captureScreenshot",
        "ModelEditor.dev.progress"
      ],
      "code": "// Scrub from t=0 to t=4 seconds, one screenshot per second.\nconst totalSeconds = 4;\n\nfor (let t = 0; t <= totalSeconds; t++) {\n  dev.progress({ phase: 'scrubbing', current: t + 1, total: totalSeconds + 1 });\n  animation.currentTime(t);\n  await new Promise((r) => requestAnimationFrame(() => r(null)));\n  const shot = await viewport.captureScreenshot();\n  const idx = String(t).padStart(2, '0');\n  // shot.blob is the binary image — save it later with io.saveFile if you need files.\n  console.log('t=' + t + 's →', 'frame-' + idx, shot.width + 'x' + shot.height, shot.blob.size, 'bytes');\n}\n",
      "expectedOutput": "One console line per second with the cursor time, the captured image size, and blob bytes."
    },
    {
      "kind": "recipe",
      "id": "pre-view-preset-tour",
      "title": "Tour the four view presets and capture each",
      "description": "Switch between top / front / right / persp views, snapshot each one after framing, and report each capture. Builds an orthographic reference sheet in one pass.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.viewport.setView",
        "ModelEditor.viewport.frameAll",
        "PreviewEditor.viewport.captureScreenshot"
      ],
      "code": "// Quad-view reference sheet for asset spec docs.\nconst views = ['top', 'front', 'right', 'persp'];\n\nfor (const view of views) {\n  viewport.setView(view);\n  viewport.frameAll();\n  await new Promise((r) => requestAnimationFrame(() => r(null)));\n  const shot = await viewport.captureScreenshot();\n  // shot.blob holds the binary image; pass it to io.saveFile to write a PNG.\n  console.log(view, shot.width + 'x' + shot.height, 'captured', shot.blob.size, 'bytes');\n}\n",
      "expectedOutput": "One console line per view (top / front / right / persp) with the captured image size and bytes."
    },
    {
      "kind": "recipe",
      "id": "previewer-asset-query-mats-textures",
      "title": "List and inspect asset materials and textures",
      "description": "Query all materials + textures in the asset; fetch one of each by name; pull raw bytes for the first texture.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "materials-textures",
      "apiSurfaces": [
        "PreviewEditor.listMaterials",
        "PreviewEditor.getMaterial",
        "PreviewEditor.listTextures",
        "PreviewEditor.getTexture",
        "PreviewEditor.getTextureBlob"
      ],
      "code": "const materials = listMaterials();\nconsole.log('materials:', materials.length);\nfor (const m of materials) console.log('  ' + m.name);\n\nif (materials.length > 0) {\n  const first = getMaterial(materials[0].name);\n  if (first) console.log('fetched:', first.name);\n}\n\nconst textures = listTextures();\nconsole.log('textures:', textures.length);\nfor (const t of textures) console.log('  ' + t.name);\n\nif (textures.length > 0) {\n  const tex = getTexture(textures[0].name);\n  const blob = await getTextureBlob(textures[0].name);\n  console.log('first texture:', tex?.name, 'blob bytes:', blob?.size);\n}\n",
      "expectedOutput": "console: material count + per-material metadata; texture count + per-texture metadata; first texture blob size."
    },
    {
      "kind": "recipe",
      "id": "previewer-background-and-double-sided",
      "title": "Set background color and toggle double-sided rendering",
      "description": "Set distinct background colors and toggle double-sided rendering; verify each state.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "PreviewEditor.viewport.setBackgroundColor",
        "PreviewEditor.viewport.backgroundColor",
        "PreviewEditor.viewport.setDoubleSided",
        "PreviewEditor.viewport.doubleSided",
        "PreviewEditor.viewport.frameAll",
        "Utils.wait.frame"
      ],
      "code": "viewport.setBackgroundColor('#2A2A2A');\nawait Utils.wait.frame();\nconsole.log('background:', viewport.backgroundColor);\n\nviewport.setDoubleSided(true);\nawait Utils.wait.frame();\nconsole.log('double-sided?', viewport.doubleSided);\n\nviewport.frameAll();\nawait Utils.wait.frame();\n\nviewport.setBackgroundColor('#E8E8E8');\nviewport.setDoubleSided(false);\nawait Utils.wait.frame();\nconsole.log('final bg:', viewport.backgroundColor, 'double-sided?', viewport.doubleSided);\n",
      "expectedOutput": "console: background color and double-sided state logged after each set."
    },
    {
      "kind": "recipe",
      "id": "previewer-exposure-bracket",
      "title": "Bracket the exposure and capture one shot per stop",
      "description": "Sweep the Previewer exposure in five steps, capture a screenshot at each stop, and report each one.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "PreviewEditor.viewport.exposure",
        "PreviewEditor.viewport.setExposure",
        "Utils.wait.frame",
        "PreviewEditor.viewport.captureScreenshot"
      ],
      "code": "// Bracket the exposure across 5 stops and capture a frame per stop.\nif (ls({ type: 'mesh' }).length === 0) {\n  console.warn('Load an asset in the Previewer first.');\n  return;\n}\nconst originalEv = viewport.exposure;\nconst base = originalEv > 0 ? originalEv : 1;\nconst stops = [-2, -1, 0, 1, 2];\n\nfor (const stop of stops) {\n  // Each stop is one photographic EV (a doubling): exposure = base * 2^stop.\n  // Exposure is a non-negative linear multiplier, so we SCALE rather than add\n  // (a raw negative EV like -2 would be rejected by setExposure).\n  const ev = base * Math.pow(2, stop);\n  viewport.setExposure(ev);\n  // Wait two frames: first lets the EV change apply, second lets the\n  // tone-mapped image settle before we grab it.\n  await Utils.wait.frame();\n  await Utils.wait.frame();\n  const shot = await viewport.captureScreenshot();\n  // shot.blob is the captured image; write it with io.saveFile if you need files.\n  console.log('EV stop', stop, '(exposure ' + ev.toFixed(3) + ')', shot.width + 'x' + shot.height, 'captured', shot.blob.size, 'bytes');\n}\n\n// Restore the original exposure so we leave the viewport as we found it.\nviewport.setExposure(originalEv);\nconsole.log('bracket complete;', stops.length, 'stops captured.');\n",
      "expectedOutput": "console: one line per exposure stop with the EV value, image size, and blob bytes."
    },
    {
      "kind": "recipe",
      "id": "previewer-grid-viewport-rect",
      "title": "Toggle grid and read viewport rect",
      "description": "Toggle grid visibility, set grid unit (cm / mm), and read viewport center + dimensions for canvas-relative scripting.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "PreviewEditor.viewport.setShowGrid",
        "PreviewEditor.viewport.showGrid",
        "PreviewEditor.viewport.setGridUnit",
        "PreviewEditor.viewport.gridUnit",
        "PreviewEditor.viewport.frameSelection",
        "PreviewEditor.viewport.frameAll",
        "ModelEditor.viewport.center",
        "ModelEditor.viewport.dimensions",
        "Utils.wait.frame"
      ],
      "code": "viewport.setShowGrid(true);\nviewport.setGridUnit('cm');\nawait Utils.wait.frame();\nconsole.log('grid:', viewport.showGrid, 'unit:', viewport.gridUnit);\n\nviewport.frameAll();\nawait Utils.wait.frame();\n\nconst center = viewport.center();\nconst dims = viewport.dimensions();\nconsole.log('viewport center:', center.x, center.y, 'dims:', dims.width + 'x' + dims.height);\n\nviewport.setGridUnit('mm');\nviewport.setShowGrid(false);\nawait Utils.wait.frame();\nconsole.log('final grid unit:', viewport.gridUnit, 'visible:', viewport.showGrid);\n",
      "expectedOutput": "console: grid state + viewport center {x, y} and dimensions {width, height}."
    },
    {
      "kind": "recipe",
      "id": "previewer-lighting-tour",
      "title": "Cycle through lighting presets with screenshots",
      "description": "Set distinct exposure, env-intensity, and ambient-intensity per preset; capture one screenshot per preset.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "PreviewEditor.viewport.setExposure",
        "PreviewEditor.viewport.exposure",
        "PreviewEditor.viewport.setEnvIntensity",
        "PreviewEditor.viewport.envIntensity",
        "PreviewEditor.viewport.setAmbientIntensity",
        "PreviewEditor.viewport.ambientIntensity",
        "PreviewEditor.viewport.captureScreenshot",
        "Utils.wait.frame"
      ],
      "code": "const presets = [\n  { name: 'normal-lighting', exposure: 1.0, env: 1.0, ambient: 0.3 },\n  { name: 'bright-lighting', exposure: 1.8, env: 1.5, ambient: 0.6 },\n  { name: 'dark-lighting',   exposure: 0.5, env: 0.3, ambient: 0.0 },\n];\n\nfor (const p of presets) {\n  viewport.setExposure(p.exposure);\n  viewport.setEnvIntensity(p.env);\n  viewport.setAmbientIntensity(p.ambient);\n  await Utils.wait.frame();\n  const shot = await viewport.captureScreenshot();\n  // shot.blob is the captured image; persist it with io.saveFile if you need files.\n  console.log(p.name + ': exposure=' + viewport.exposure.toFixed(2)\n    + ' env=' + viewport.envIntensity.toFixed(2)\n    + ' ambient=' + viewport.ambientIntensity.toFixed(2)\n    + ' (' + shot.blob.size + ' bytes)');\n}\n",
      "expectedOutput": "console: one line per preset (normal / bright / dark) with exposure/env/ambient values and the captured bytes."
    },
    {
      "kind": "recipe",
      "id": "previewer-modes-and-dialogs",
      "title": "Toggle Previewer xray and open every Previewer popup",
      "description": "Count hidden meshes; set / get xray; open and close the three Previewer-only dialogs (UVs / Materials / Textures).",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "PreviewEditor.viewport.setXray",
        "PreviewEditor.viewport.xray",
        "PreviewEditor.scene.ls",
        "PreviewEditor.dev.dialogs.materials.open",
        "PreviewEditor.dev.dialogs.materials.close",
        "PreviewEditor.dev.dialogs.materials.isOpen",
        "PreviewEditor.dev.dialogs.textures.open",
        "PreviewEditor.dev.dialogs.textures.close",
        "PreviewEditor.dev.dialogs.textures.isOpen",
        "PreviewEditor.dev.dialogs.uvs.open",
        "PreviewEditor.dev.dialogs.uvs.close",
        "PreviewEditor.dev.dialogs.uvs.isOpen"
      ],
      "code": "const before = viewport.xray;\nviewport.setXray(true);\nconsole.log('xray ' + before + ' → ' + viewport.xray);\nviewport.setXray(before);\n\n// Node visibility is a per-handle property — count the hidden meshes.\nconst meshes = ls({ type: 'mesh' });\nconst hidden = meshes.filter((m) => m.isHidden);\nconsole.log('hidden meshes:', hidden.length, 'of', meshes.length);\n\nasync function cycle(name, dialog) {\n  try {\n    await dialog.open();\n    console.log(name + '.open: isOpen=' + dialog.isOpen());\n    await dialog.close();\n    console.log(name + '.close: isOpen=' + dialog.isOpen());\n  } catch (err) {\n    console.log(name + ' skipped:', err.message);\n  }\n}\n\nawait cycle('uvs', dev.dialogs.uvs);\nawait cycle('materials', dev.dialogs.materials);\nawait cycle('textures', dev.dialogs.textures);\n",
      "expectedOutput": "console: xray before/after; hidden count; dialog open/close/isOpen results."
    },
    {
      "kind": "recipe",
      "id": "previewer-render-mode-tour",
      "title": "Tour every render mode and capture a contact sheet",
      "description": "Cycle through the clay, textured, and uvchecker render modes, capturing one screenshot per mode and reporting each one.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "PreviewEditor.viewport.setRenderMode",
        "Utils.wait.frame",
        "PreviewEditor.viewport.captureScreenshot",
        "ModelEditor.dev.progress"
      ],
      "code": "// Capture one screenshot per render mode and report each one.\nif (ls({ type: 'mesh' }).length === 0) {\n  console.warn('Load an asset in the Previewer first.');\n  return;\n}\n\nconst modes = ['clay', 'textured', 'uvchecker'];\nconst captured = [];\n\nfor (let i = 0; i < modes.length; i++) {\n  // Surface progress so the user can see the script working.\n  dev.progress({ phase: 'rendering', current: i + 1, total: modes.length, label: modes[i] });\n\n  viewport.setRenderMode(modes[i]);\n  // Wait one frame so the new render mode is on the screen before we grab.\n  await Utils.wait.frame();\n\n  const shot = await viewport.captureScreenshot();\n  // shot.blob is the binary image; pass it to io.saveFile to write a PNG.\n  captured.push(modes[i]);\n  console.log(modes[i], shot.width + 'x' + shot.height, 'captured', shot.blob.size, 'bytes');\n}\n\nconsole.log('render-mode tour complete;', captured.length, 'frames captured:', captured.join(', '));\n",
      "expectedOutput": "console: one line per render mode with the captured image dimensions and blob bytes."
    },
    {
      "kind": "recipe",
      "id": "previewer-render-modes-checker-tour",
      "title": "Audit every render mode + UV-checker tile counts",
      "description": "Cycle clay / textured / uvchecker / normal modes; toggle wireframe per mode; vary checker-tile count on uvchecker.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "PreviewEditor.viewport.setRenderMode",
        "PreviewEditor.viewport.renderMode",
        "PreviewEditor.viewport.setCheckerTiles",
        "PreviewEditor.viewport.checkerTiles",
        "PreviewEditor.viewport.setWireframe",
        "PreviewEditor.viewport.wireframe",
        "PreviewEditor.viewport.captureScreenshot",
        "Utils.wait.frame"
      ],
      "code": "const modes = ['clay', 'textured', 'uvchecker', 'normal'];\nfor (const mode of modes) {\n  viewport.setRenderMode(mode);\n  viewport.setWireframe(mode !== 'clay');\n  if (mode === 'uvchecker') viewport.setCheckerTiles(8);\n  await Utils.wait.frame();\n  console.log(mode + ': wireframe=' + viewport.wireframe\n    + ' tiles=' + (mode === 'uvchecker' ? viewport.checkerTiles : 'n/a')\n    + ' (verified mode=' + viewport.renderMode + ')');\n  await viewport.captureScreenshot();\n}\n",
      "expectedOutput": "console: per-mode render mode, wireframe state, and (for uvchecker) tile count."
    },
    {
      "kind": "recipe",
      "id": "previewer-scene-inventory",
      "title": "Audit every mesh in the scene",
      "description": "Walk every mesh in the loaded scene, log its name, vertex / face / edge counts, and the size of its bounding box in meters.",
      "intent": "mesh",
      "editor": "previewer",
      "category": "spatial",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "mesh.verts",
        "mesh.faces",
        "mesh.edges",
        "mesh.bbox"
      ],
      "code": "// Print a quick inventory of every mesh in the scene.\nconst meshes = ls({ type: 'mesh' });\nif (meshes.length === 0) {\n  console.warn('No meshes in the scene. Open an asset first.');\n  return;\n}\n\nconsole.log('found', meshes.length, 'meshes');\nlet totalVerts = 0;\nlet totalFaces = 0;\nfor (const m of meshes) {\n  const box = m.bbox({ space: 'world' });\n  const sx = (box.max.x - box.min.x).toFixed(3);\n  const sy = (box.max.y - box.min.y).toFixed(3);\n  const sz = (box.max.z - box.min.z).toFixed(3);\n  console.log(\n    m.name,\n    '  V=' + m.verts.count,\n    'F=' + m.faces.count,\n    'E=' + m.edges.count,\n    '  size=' + sx + 'x' + sy + 'x' + sz + 'm',\n  );\n  totalVerts += m.verts.count;\n  totalFaces += m.faces.count;\n}\nconsole.log('total:', totalVerts, 'verts /', totalFaces, 'faces');\n",
      "expectedOutput": "console: one row per mesh: name, vert / face / edge counts, bbox size in meters."
    },
    {
      "kind": "recipe",
      "id": "previewer-skeleton-joint-viz",
      "title": "Visualize skeleton with joint customization",
      "description": "Toggle skeleton visibility, customize joint marker scale, color, and axis display, then verify each state.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "PreviewEditor.viewport.setShowSkeleton",
        "PreviewEditor.viewport.showSkeleton",
        "PreviewEditor.viewport.setJointScale",
        "PreviewEditor.viewport.jointScale",
        "PreviewEditor.viewport.setJointColor",
        "PreviewEditor.viewport.jointColor",
        "PreviewEditor.viewport.setShowJointAxis",
        "PreviewEditor.viewport.showJointAxis",
        "PreviewEditor.viewport.setAxisScale",
        "PreviewEditor.viewport.axisScale",
        "Utils.wait.frame"
      ],
      "code": "// Enable skeleton visualization and customize joint appearance.\nviewport.setShowSkeleton(true);\nconsole.log('skeleton visible?', viewport.showSkeleton);\n\nviewport.setJointScale(1.5);\nawait Utils.wait.frame();\nconsole.log('joint scale:', viewport.jointScale);\n\nviewport.setJointColor('#FF6B35');\nawait Utils.wait.frame();\nconsole.log('joint color:', viewport.jointColor);\n\nviewport.setShowJointAxis(true);\nconsole.log('joint axis visible?', viewport.showJointAxis);\n\nviewport.setAxisScale(2.0);\nawait Utils.wait.frame();\nconsole.log('axis scale:', viewport.axisScale);\n",
      "expectedOutput": "console: skeleton visibility, joint scale, joint color, axis visibility and scale logged after each set."
    },
    {
      "kind": "recipe",
      "id": "previewer-turntable-capture",
      "title": "Capture a 4-step turntable sequence",
      "description": "Frame the loaded asset, orbit the camera in four 45-degree steps, and capture one screenshot per step.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "PreviewEditor.viewport.frameAll",
        "ModelEditor.viewport.orbit",
        "Utils.wait.frame",
        "PreviewEditor.viewport.captureScreenshot"
      ],
      "code": "// Capture four screenshots while the camera orbits in 45-degree steps.\n// Make sure an asset is loaded in the Previewer before running.\nif (ls({ type: 'mesh' }).length === 0) {\n  console.warn('Load an asset in the Previewer first.');\n  return;\n}\nviewport.frameAll();\nawait Utils.wait.frame();\n\nfor (let i = 0; i < 4; i++) {\n  if (i > 0) {\n    // Rotate the camera 45 degrees about the vertical axis before each shot.\n    viewport.orbit(45, 0);\n    await Utils.wait.frame();\n  }\n  const shot = await viewport.captureScreenshot();\n  const name = 'turn-' + String(i).padStart(2, '0');\n  // shot.blob is the binary PNG; save it later with io.saveFile if needed.\n  console.log(name, shot.width + 'x' + shot.height, 'captured', shot.blob.size, 'bytes');\n}\nconsole.log('turntable complete; 4 frames captured.');\n",
      "expectedOutput": "console: one line per frame with the captured image size and blob bytes."
    },
    {
      "kind": "recipe",
      "id": "project-boxy-arduino-case",
      "title": "Arduino UNO Case",
      "description": "A custom-fit Arduino UNO R3 enclosure base with matched mounting standoffs and USB and barrel-jack cutouts on the wall.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Arduino UNO Case\n//\n// Source: project-boxy — MIT\n// Generated from the library above, used under its license.\n\ninclude <project-boxy/project_box.scad>\n\n// Box base only, sized to host an Arduino UNO R3.\nBOX = true;\nLID = false;\n\nEXTERIOR_WIDTH = 80;\nEXTERIOR_HEIGHT = 70;\nEXTERIOR_DEPTH = 30;\nTHICKNESS = 2;\n\n// Use the built-in Arduino footprint: drops the four board standoffs and\n// cuts the USB + barrel-jack openings in the wall it faces.\nARDUINO_UNO = true;\nARDUINO_UNO_ROTATION = 90;\n\n// Footprint provides the standoffs, so disable the generic grid.\nBOARD_STANDOFFS = false;\nROUND_HOLES = [];\n`);"
    },
    {
      "kind": "recipe",
      "id": "project-boxy-enclosure-box",
      "title": "Electronics Enclosure Box",
      "description": "A parametric 3D-printable electronics project box base with PCB standoffs and lid mounting posts.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Electronics Enclosure Box\n//\n// Source: project-boxy — MIT\n// Generated from the library above, used under its license.\n\ninclude <project-boxy/project_box.scad>\n\n// Render only the box base (the lid is a separate part).\nBOX = true;\nLID = false;\n\n// Compact enclosure footprint.\nEXTERIOR_WIDTH = 70;\nEXTERIOR_HEIGHT = 50;\nEXTERIOR_DEPTH = 28;\nTHICKNESS = 2;\n\n// PCB mounting standoffs inside the floor.\nBOARD_STANDOFFS = true;\nBOARD_STANDOFF_WIDTH = 30;\nBOARD_STANDOFF_HEIGHT = 20;\n\n// No side panel cutouts on this base.\nROUND_HOLES = [];\nARDUINO_UNO = false;\n`);"
    },
    {
      "kind": "recipe",
      "id": "project-boxy-enclosure-lid",
      "title": "Snap-On Enclosure Lid",
      "description": "A parametric 3D-printable enclosure lid with an alignment lip and countersunk screw holes that mates to the matching box.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Snap-On Enclosure Lid\n//\n// Source: project-boxy — MIT\n// Generated from the library above, used under its license.\n\ninclude <project-boxy/project_box.scad>\n\n// Render only the lid (matches the 70x50 enclosure box).\nBOX = false;\nLID = true;\n\nEXTERIOR_WIDTH = 70;\nEXTERIOR_HEIGHT = 50;\nEXTERIOR_DEPTH = 28;\nTHICKNESS = 2;\n\n// Lid features.\nLID_THICKNESS = 3;\nLID_LIP = true;\nLID_LIP_WIDTH = 3;\nLID_COUNTERSINK_DIA = 7;\n\nROUND_HOLES = [];\nARDUINO_UNO = false;\n`);"
    },
    {
      "kind": "recipe",
      "id": "project-boxy-paneled-box",
      "title": "Panel-Mount Project Box",
      "description": "A tall electronics project box with a row of round panel-mount holes on the front face for connectors and switches.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Panel-Mount Project Box\n//\n// Source: project-boxy — MIT\n// Generated from the library above, used under its license.\n\ninclude <project-boxy/project_box.scad>\n\n// Box base only.\nBOX = true;\nLID = false;\n\nEXTERIOR_WIDTH = 90;\nEXTERIOR_HEIGHT = 60;\nEXTERIOR_DEPTH = 40;\nTHICKNESS = 2.5;\n\n// PCB standoffs.\nBOARD_STANDOFFS = true;\nBOARD_STANDOFF_WIDTH = 40;\nBOARD_STANDOFF_HEIGHT = 30;\n\n// Three round panel-mount holes on the front (+y) face, 12mm diameter, spaced horizontally.\n// Format: [Face, Diameter, Position_h, Position_v]  (Face 3 = front).\nROUND_HOLES = [[3, 12, -22, 5], [3, 12, 0, 5], [3, 12, 22, 5]];\n\nARDUINO_UNO = false;\n`);"
    },
    {
      "kind": "recipe",
      "id": "rpi-mounts-bplus-bumper",
      "title": "Raspberry Pi B+ snap-on bumper",
      "description": "A low-profile protective bumper that clips around the edge of a Raspberry Pi 2/3/B+ board, with two external ear tabs drilled for wood screws so the whole board can be fastened flat to a panel or wall.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Raspberry Pi B+ snap-on bumper\n//\n// Source: OpenSCAD-RaspberryPi-Mounting-Library — MIT\n// Generated from the library above, used under its license.\n\ninclude <rpi-mounts/raspberry.scad>\n\n$fn = 48;\n\nbumper(\n  boardType     = BPLUS,\n  mountingHoles = true\n);\n`);"
    },
    {
      "kind": "recipe",
      "id": "rpi-mounts-bplus-enclosure",
      "title": "Raspberry Pi B+ enclosure box",
      "description": "A walled case base for a Raspberry Pi 2/3/B+ with integrated tapped standoffs, side cut-outs for the USB, ethernet and power ports, and clip slots along the top edge that accept the matching snap-on lid.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Raspberry Pi B+ enclosure box\n//\n// Source: OpenSCAD-RaspberryPi-Mounting-Library — MIT\n// Generated from the library above, used under its license.\n\ninclude <rpi-mounts/raspberry.scad>\n\n$fn = 32;\n\nenclosure(\n  boardType       = BPLUS,\n  wall            = 2.5,\n  offset          = 2,\n  heightExtension = 6,\n  cornerRadius    = 3,\n  mountType       = TAPHOLE\n);\n`);"
    },
    {
      "kind": "recipe",
      "id": "rpi-mounts-bplus-standoffs",
      "title": "Raspberry Pi B+ tapped standoffs",
      "description": "A set of four tapered standoffs positioned at the exact mounting-hole spacing of a Raspberry Pi 2/3/B+ board, each with a self-tapping screw hole so the board can be raised off a baseplate or screwed into a case floor.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Raspberry Pi B+ tapped standoffs\n//\n// Source: OpenSCAD-RaspberryPi-Mounting-Library — MIT\n// Generated from the library above, used under its license.\n\ninclude <rpi-mounts/raspberry.scad>\n\n$fn = 48;\n\nstandoffs(\n  boardType   = BPLUS,\n  height      = 8,\n  bottomRadius = 3.5,\n  topRadius   = 2.6,\n  mountType   = TAPHOLE\n);\n`);"
    },
    {
      "kind": "recipe",
      "id": "rpi-mounts-bplus-vented-lid",
      "title": "Raspberry Pi B+ enclosure lid",
      "description": "The snap-fit top cover for the Raspberry Pi 2/3/B+ case base, with a recessed inner lip and four flexible clips that lock it onto the enclosure clip slots.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Raspberry Pi B+ enclosure lid\n//\n// Source: OpenSCAD-RaspberryPi-Mounting-Library — MIT\n// Generated from the library above, used under its license.\n\ninclude <rpi-mounts/raspberry.scad>\n\n$fn = 32;\n\nenclosureLid(\n  boardType    = BPLUS,\n  wall         = 2.5,\n  offset       = 2,\n  cornerRadius = 3,\n  ventHoles    = true\n);\n`);"
    },
    {
      "kind": "recipe",
      "id": "rpi-mounts-zero-standoffs",
      "title": "Raspberry Pi Zero tapped standoffs",
      "description": "Four short tapered standoffs spaced for the compact Raspberry Pi Zero mounting holes, each tapped for a self-threading screw so a Zero can be lifted off a baseplate or anchored inside a slim project box.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Raspberry Pi Zero tapped standoffs\n//\n// Source: OpenSCAD-RaspberryPi-Mounting-Library — MIT\n// Generated from the library above, used under its license.\n\ninclude <rpi-mounts/raspberry.scad>\n\n$fn = 48;\n\nstandoffs(\n  boardType    = ZERO,\n  height       = 5,\n  bottomRadius = 3.2,\n  topRadius    = 2.4,\n  mountType    = TAPHOLE\n);\n`);"
    },
    {
      "kind": "recipe",
      "id": "scene-asset-lifecycle",
      "title": "Parent / unparent / delete nodes, add an image plane, and clear the scene",
      "description": "Spawn nodes; parent and unparent them; add an image plane; open a project; delete the nodes; clear the scene.",
      "intent": "io",
      "editor": "model",
      "category": "io",
      "apiSurfaces": [
        "ModelEditor.io.newScene",
        "ModelEditor.io.openScene",
        "ModelEditor.create.imagePlane",
        "ModelEditor.scene.select",
        "ModelEditor.scene.delete",
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "node.translate",
        "node.setParent",
        "node.unparent"
      ],
      "code": "const parent = await create.cube({ name: 'ParentBox' });\nparent.translate.set([2.5, 1.3, -0.7]); select(null, {mode: 'clear'});\nconst child = await create.cube({ name: 'ChildBox' });\nchild.translate.set([5.0, 1.3, -0.7]); select(null, {mode: 'clear'});\nawait Utils.wait.frame();\n\nchild.setParent(parent);\nconsole.log('parented child to parent');\n\nchild.unparent();\nconsole.log('unparented child');\n\nasync function probe(label, fn) {\n  try {\n    await fn();\n    console.log(label, 'ok');\n  } catch (err) {\n    console.log(label, 'skipped:', err.message);\n  }\n}\n\nawait probe('imagePlane', () => create.imagePlane({}));\nawait probe('openScene', () => io.openScene());\n\nparent.delete();\nchild.delete();\nconsole.log('deleted both meshes');\n\n// newScene clears everything; call last. Guard since a save-reconcile\n// modal can block the call when the scene is dirty.\ntry {\n  await io.newScene();\n  console.log('newScene fired');\n} catch (err) {\n  console.log('newScene blocked or unavailable:', err.message);\n}\n",
      "expectedOutput": "console: parent/unparent ops; imagePlane and openScene probes; final clear-scene result."
    },
    {
      "kind": "recipe",
      "id": "screenshot-every-render-mode",
      "title": "Screenshot every render mode",
      "description": "Capture clay, textured, and uvchecker renders and report each image size.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.dev.progress",
        "PreviewEditor.viewport.setRenderMode",
        "PreviewEditor.viewport.captureScreenshot"
      ],
      "code": "// Capture each render mode in turn and report the image dimensions + bytes.\nconst modes = ['clay', 'textured', 'uvchecker'];\nfor (let i = 0; i < modes.length; i++) {\n  dev.progress({ phase: 'screenshotting', current: i + 1, total: modes.length, label: modes[i] });\n  viewport.setRenderMode(modes[i]);\n  await new Promise((r) => requestAnimationFrame(() => r(null)));\n  const shot = await viewport.captureScreenshot();\n  // The capture carries the pixel size + a binary blob you can save with io.saveFile.\n  console.log(modes[i], shot.width + 'x' + shot.height, 'captured', shot.blob.size, 'bytes');\n}\n",
      "expectedOutput": "One console line per render mode with the captured image dimensions and blob size."
    },
    {
      "kind": "recipe",
      "id": "selection-by-shared-material",
      "title": "Select every mesh sharing a material",
      "description": "Build meshes, paint two materials across them, then pick the seed mesh's material and select every other mesh that shares it. Mass-edit shading in one pass.",
      "intent": "mesh",
      "editor": "model",
      "category": "selection",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.create.material",
        "ModelEditor.scene.ls",
        "ModelEditor.scene.select",
        "mesh.material.get",
        "material.assignTo"
      ],
      "code": "// Self-contained from an EMPTY scene: build meshes, assign two materials\n// across them, then select everything sharing the seed mesh's material.\n\n// 1. Two materials (create.material is ASYNC -> await).\nconst red  = await create.material({ name: 'Red',  baseColor: [0.8, 0.1, 0.1, 1], roughness: 0.5 });\nconst blue = await create.material({ name: 'Blue', baseColor: [0.1, 0.2, 0.8, 1], roughness: 0.5 });\n\n// 2. Five cubes; alternate the two materials across them.\nconst meshes = [];\nfor (let i = 0; i < 5; i++) {\n  const c = await create.cube({ width: 0.8, name: 'Cube' + i });\n  c.translate.set([i * 1.2, 0, 0]);\n  await (i % 2 === 0 ? red : blue).assignTo(c);   // even -> Red, odd -> Blue\n  meshes.push(c);\n  select(null, { mode: 'clear' });\n}\n\n// 3. Pick a seed mesh and read its material. mesh.material.get() returns a\n//    Material handle (with .id / .name) or null when nothing is bound.\nconst seed = meshes[0];\nconst target = seed.material.get();\n\n// 4. Find every mesh that shares the seed's material, select them in ONE\n//    replace call, and report (never a silent no-op).\nconst matches = ls({ type: 'mesh' }).filter((m) => {\n  const mat = m.material.get();\n  return mat !== null && mat.id === target.id;\n});\nawait select(matches);\nconsole.log('seed', seed.name, 'uses', target.name + ';',\n  'selected', matches.length, 'meshes sharing it:',\n  matches.map((m) => m.name).join(', '));\n",
      "expectedOutput": "console: count of meshes sharing the seed material; outliner: exactly those meshes are selected."
    },
    {
      "kind": "recipe",
      "id": "selection-group-under-named-parent",
      "title": "Group the selection under a named parent",
      "description": "Build a few parts, then wrap them under a fresh group named after a prefix you pick. The whole operation lands as one undoable batch.",
      "intent": "mesh",
      "editor": "model",
      "category": "outliner",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.create.group",
        "node.translate"
      ],
      "code": "// Self-contained from an EMPTY scene: build a few parts, then wrap them all\n// under one named group in a single undoable batch.\n\n// 1. Build the parts to group.\nconst partNames = ['Wheel', 'Axle', 'Hub'];\nconst parts = [];\nfor (let i = 0; i < partNames.length; i++) {\n  const c = await create.cube({ width: 0.6, name: partNames[i] });\n  c.translate.set([i * 1.0, 0, 0]);\n  parts.push(c);\n  select(null, { mode: 'clear' });\n}\n\n// 2. Stamp the group with a stable prefix + a time suffix so repeated runs\n//    don't collide on the auto-naming pass.\nconst groupName = 'Assembly_' + Date.now();\n\n// 3. Wrap the parts under one group. create.group(nodes, { name }) is ASYNC\n//    -> await; passing the nodes array reparents them under the new group.\nconst group = await create.group(parts, { name: groupName });\n\n// 4. Report the result (never a silent no-op).\nconsole.log('grouped', parts.length, 'children under', group.name);\nfor (const child of parts) console.log('  child:', child.name);\n",
      "expectedOutput": "console: group name + child count + each child name; outliner: a new group containing the parts."
    },
    {
      "kind": "recipe",
      "id": "selection-invert-within-meshes",
      "title": "Invert the selection across all meshes",
      "description": "Build the set of unselected meshes, then swap the selection so it contains exactly those instead. Invert-selection behavior for the mesh layer.",
      "intent": "scene",
      "editor": "model",
      "category": "selection",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "ModelEditor.scene.select"
      ],
      "code": "// Swap the selection: whatever is selected becomes unselected and vice versa.\nconst all = ls({ type: 'mesh' });\nconst before = ls({selected: true}).filter((n) => n.type === 'mesh');\nconst beforeIds = new Set(before.map((m) => m.id));\nconst inverted = all.filter((m) => !beforeIds.has(m.id));\n\n  select(null, {mode: 'clear'});\n  for (const m of inverted) select(m, { add: true });\nconsole.log('before:', before.length, 'meshes; after:', inverted.length, 'meshes');\n",
      "expectedOutput": "console: before / after selection counts; outliner: every mesh that was unselected becomes selected."
    },
    {
      "kind": "recipe",
      "id": "selection-ops-grow-shrink-invert-bytype",
      "title": "Run grow / shrink / invert / select-by-face-sides and convert across component modes",
      "description": "Spawn a subdivided cube; switch to vertex/edge/face mode; grow/shrink/invert; select-all, linked, loose, non-manifold, similar, sharp-edges, nth, interior; shortest-path; convert via toVertices/toEdges/toFaces/toIslands/toUVs; bulk-translate the selection.",
      "intent": "mesh",
      "editor": "model",
      "category": "selection",
      "apiSurfaces": [
        "ModelEditor.select.growSelection",
        "ModelEditor.select.shrinkSelection",
        "ModelEditor.select.invertSelection",
        "ModelEditor.select.selectLinked",
        "ModelEditor.select.selectLoose",
        "ModelEditor.select.selectNonManifold",
        "ModelEditor.select.selectInteriorFaces",
        "ModelEditor.select.selectSharpEdges",
        "ModelEditor.select.selectSimilar",
        "ModelEditor.select.selectNth",
        "ModelEditor.select.shortestPath",
        "ModelEditor.select.selectByFaceSides",
        "ModelEditor.select.convertVerticesToEdges",
        "ModelEditor.select.convertEdgesToFaces",
        "ModelEditor.select.convertFacesToUVs",
        "ModelEditor.select.convertUVsToIslands",
        "ModelEditor.select.convertIslandsToVertices",
        "ModelEditor.scene.select",
        "ModelEditor.create.cube",
        "ModelEditor.setComponentMode",
        "Utils.wait.frame",
        "node.translate",
        "node.xform"
      ],
      "code": "const cube = await create.cube({ width: 1, height: 1, depth: 1, subdivisionsX: 3, subdivisionsY: 3, subdivisionsZ: 3, name: 'SelCube' });\ncube.translate.set([2.5, 1.3, -0.7]);\nselect(null, {mode: 'clear'});\nawait Utils.wait.frame();\n\nfunction logSafe(label, fn) {\n  try {\n    fn();\n    console.log(label, 'ok');\n  } catch (err) {\n    console.log(label, 'skipped:', err.message);\n  }\n}\n\n// Object-level select + bulk translate (transforms live on the node handle).\nselect(cube);\ncube.xform({ t: [0.1, 0, 0], relative: true });\nconsole.log('object-mode translate applied');\nselect(null, {mode: 'clear'});\n\n// Vertex mode — grow/shrink/invert + linked/loose, then walk the component\n// domains via a convert chain: vertices → edges → faces → UVs → islands → vertices.\nsetComponentMode('vertex');\nselect(null, {mode: 'all'});\nlogSafe('grow', () => select.growSelection());\nlogSafe('shrink', () => select.shrinkSelection());\nlogSafe('invert', () => select.invertSelection());\nlogSafe('selectLinked', () => select.selectLinked());\nlogSafe('selectLoose', () => select.selectLoose());\nlogSafe('verts→edges', () => select.convertVerticesToEdges());\nlogSafe('edges→faces', () => select.convertEdgesToFaces());\nlogSafe('faces→UVs', () => select.convertFacesToUVs());\nlogSafe('UVs→islands', () => select.convertUVsToIslands());\nlogSafe('islands→verts', () => select.convertIslandsToVertices());\n\n// Edge mode\nsetComponentMode('edge');\nselect(null, {mode: 'all'});\nlogSafe('edge.selectNonManifold', () => select.selectNonManifold());\nlogSafe('edge.selectSharpEdges', () => select.selectSharpEdges({ angleThreshold: 30 }));\nlogSafe('edge.selectSimilar', () => select.selectSimilar({ property: 'length', threshold: 0.1 }));\nlogSafe('edge.selectNth', () => select.selectNth(2));\nlogSafe('edge.shortestPath', () => select.shortestPath(0, 5));\n\n// Face mode\nsetComponentMode('face');\nselect(null, {mode: 'all'});\nlogSafe('face.selectInteriorFaces', () => select.selectInteriorFaces());\nlogSafe('face.byFaceSides', () => select.selectByFaceSides(4));\n\nsetComponentMode('object');\nselect(null, {mode: 'clear'});\n",
      "expectedOutput": "console: status line after every selection op; bulk translate confirms."
    },
    {
      "kind": "recipe",
      "id": "snap-nearest-transform",
      "title": "Snap to the nearest scene transform",
      "description": "node.snapToNearest({ mode: \"transform\", range }) finds the closest OTHER scene transform within range and snaps to it (returns null if none).",
      "intent": "mesh",
      "editor": "model",
      "category": "snap-align",
      "apiSurfaces": [
        "ModelEditor.create.locator",
        "node.snapToNearest",
        "node.translate"
      ],
      "code": "// Self-contained from an EMPTY scene: scatter a few markers, then snap a\n// rover onto whichever one is closest within a search radius.\n\n// 1. A handful of fixed markers the rover can latch onto. create.locator is\n//    SYNC (no await): { name, position, scale }.\nconst a = create.locator({ name: 'MarkerA', position: [4, 0, 0] });\nconst b = create.locator({ name: 'MarkerB', position: [0, 0, 5] });\nconst c = create.locator({ name: 'MarkerC', position: [1.2, 0, 0] });\n\n// 2. The rover starts near MarkerC but not on it.\nconst rover = create.locator({ name: 'Rover', position: [0, 0, 0] });\n\n// 3. Snap to the nearest OTHER scene transform within range. Options:\n//    mode: 'transform' searches other transforms (vs 'vertex' / 'surface',\n//          which need a target mesh); range > 0 is the max world distance to\n//          consider (past it the call is a no-op returning null);\n//    checkOcclusion: 'auto' (default) | true (always skip hidden candidates,\n//          throws when no viewport camera) | false (never skip). 'auto'\n//          passes through in headless scripts.\nconst hit = rover.snapToNearest({\n  mode: 'transform',\n  range: 3,                  // only MarkerC (dist 1.2) is within 3 units\n  checkOcclusion: 'auto',\n});\n\n// 4. Report the result so it's never a silent no-op.\nif (hit) {\n  console.log(rover.name, 'snapped to', hit.hitNode ? hit.hitNode.name : '(unknown)',\n    'at', JSON.stringify(rover.translate.get()),\n    '| travelled', hit.distance.toFixed(3), 'units');\n} else {\n  console.log(rover.name, 'found nothing in range; left at',\n    JSON.stringify(rover.translate.get()),\n    '(unused markers:', [a.name, b.name].join(', ') + ')');\n}\n",
      "expectedOutput": "console line with the snap distance, or \"nothing in range\" when no transform is within range."
    },
    {
      "kind": "recipe",
      "id": "snap-object-onto-object",
      "title": "Snap one object onto another",
      "description": "node.snapTo(target) copies the target node's world-space translate onto this node. Zero offset, polymorphic, one undo step.",
      "intent": "mesh",
      "editor": "model",
      "category": "snap-align",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "node.snapTo",
        "node.translate"
      ],
      "code": "// Self-contained from an EMPTY scene: build a target + a mover, then snap.\n// snapTo copies ONLY the target's world translate (rotate + scale untouched),\n// in a single undo step. To copy t+r+s at once use node.matchTransform(target).\n\n// 1. Target object, parked away from the origin so the snap is visible.\nconst target = await create.cube({\n  width: 1,            // X size in scene units (>0); default 1\n  height: 1,           // Y size; default 1\n  depth: 1,            // Z size; default 1\n  name: 'SnapTarget',\n});\ntarget.translate.set([3, 2, -1]);          // [x, y, z] world position to copy\n\n// Clear selection so the next create() doesn't add to a multi-selection.\nselect(null, { mode: 'clear' });\n\n// 2. Mover object, somewhere else entirely.\nconst mover = await create.cube({ width: 1, name: 'Mover' });\nmover.translate.set([-4, 0, 5]);\n\n// 3. Snap the mover onto the target's world position.\nconst before = mover.translate.get();      // [x, y, z] before the snap\nmover.snapTo(target);                       // mover now shares target's translate\nconst after = mover.translate.get();\n\n// 4. Report the move so it's not a silent no-op.\nconsole.log(mover.name, 'moved from', JSON.stringify(before),\n  'to', JSON.stringify(after),\n  '| now matches', target.name, 'at', JSON.stringify(target.translate.get()));\n",
      "expectedOutput": "console line with the moved node's new world translate, matching the target."
    },
    {
      "kind": "recipe",
      "id": "snap-settings-tune",
      "title": "Tune the viewport snap settings",
      "description": "dev.snapSettings is a getter/setter for the gizmo snap state shown in the channel box (snapAngle / snapStep / snapScale / snapMode).",
      "intent": "scene",
      "editor": "model",
      "category": "snap-align",
      "apiSurfaces": [
        "ModelEditor.dev.snapSettings.snapAngle",
        "ModelEditor.dev.snapSettings.snapStep",
        "ModelEditor.dev.snapSettings.snapMode"
      ],
      "code": "// Turn on 15-degree rotation snapping and a 0.25-unit translate step.\ndev.snapSettings.snapAngle = 15;\ndev.snapSettings.snapStep = 0.25;\ndev.snapSettings.snapMode = 'rotate';\nconsole.log('snap mode is now', dev.snapSettings.snapMode);\n",
      "expectedOutput": "channel-box snap section shows 15° angle + 0.25 step + rotate mode."
    },
    {
      "kind": "recipe",
      "id": "snap-to-mesh-surface",
      "title": "Snap to a mesh surface",
      "description": "Drop a locator onto the closest point of a target mesh surface using mesh.geometry.closestPoint(point, { space }) + node.translate.set(...). (node.snapToNearest mode:\"surface\" is unavailable from script — the surface branch calls a mesh method that is only exposed under mesh.geometry.)",
      "intent": "mesh",
      "editor": "model",
      "category": "snap-align",
      "apiSurfaces": [
        "ModelEditor.create.locator",
        "mesh.geometry.closestPoint",
        "node.translate"
      ],
      "code": "// Self-contained from an EMPTY Model scene: build the target mesh, then drop a\n// locator down onto the nearest point of its triangle surface. The surface\n// query lives on mesh.geometry.closestPoint(...) (it is NOT reachable through\n// node.snapToNearest('surface') from a script), so we compute the hit and write\n// it onto the marker with node.translate.set(...).\n\n// 1. Target surface — a sphere the marker will land on.\nconst ball = await create.sphere({\n  radius: 1.0,            // 1 m radius; >0\n  widthSegments: 24,      // longitude divisions (denser = smoother surface to hit)\n  heightSegments: 16,     // latitude divisions\n  name: 'SnapBall',\n});\nball.translate.set([0, 0, 0]); // park the target at the origin\n\n// 2. The marker we want to drop. create.locator is SYNC (no await).\nconst marker = create.locator({\n  name: 'Marker',\n  position: [0, 5, 0],    // start 5 m straight above the ball\n  scale: [1, 1, 1],       // unit scale\n});\n\n// 3. Find the nearest point on the ball's surface to the marker's world\n//    position. closestPoint returns { position, face, normal, distance,\n//    barycentric } | null. space: 'world' puts the query + result in world\n//    space ('object' would use the ball's local coords instead).\nconst start = marker.translate.get();           // [x, y, z] in scene units\nconst hit = ball.geometry.closestPoint(start, { space: 'world' });\n\nif (hit) {\n  const onSurface = hit.position.toArray();      // Vec3 -> [x, y, z]\n  marker.translate.set(onSurface);               // drop the marker onto the surface\n  select(null, { mode: 'clear' });\n  console.log('Marker landed on', ball.name,\n    'at', JSON.stringify(onSurface.map((v) => +v.toFixed(3))),\n    '| travelled', hit.distance.toFixed(3), 'm');\n} else {\n  select(null, { mode: 'clear' });\n  console.log('Target mesh is empty - marker did not move.');\n}\n",
      "expectedOutput": "console line with the surface-hit position + travel distance."
    },
    {
      "kind": "recipe",
      "id": "snapfit-scad-cable-grommet",
      "title": "Snap-fit cable grommet",
      "description": "A round cable pass-through grommet with a cantilever snap-fit post on top, so it clips into a panel hole and stays put without screws. Tune the disc size, the cable bore, and the snap post to match your panel thickness and wire bundle.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Snap-fit cable grommet\n//\n// Source: snapfit-scad — MIT\n// Generated from the library above, used under its license.\n\nuse <snapfit-scad/snapfit.scad>\n\ndisc_d   = 18;   // outer flange diameter\ndisc_h   = 3;    // flange thickness\nbore_d   = 6;    // cable pass-through hole diameter\nsnap_dia = 5;    // snap-fit post diameter\nsnap_lip = 1;    // snap-fit lip (adds to diameter)\nsnap_col = 4;    // snap-fit column height\n$fn = 96;\n\ndifference() {\n  union() {\n    translate([0, 0, disc_h / 2])\n      cylinder(r1 = disc_d / 2, r2 = disc_d / 2.4, h = disc_h, center = true);\n    translate([0, 0, disc_h])\n      snapfit(dia = snap_dia, lip = snap_lip, col_h = snap_col,\n              slot_ratio = 0.7, under_chamfer_angle = 45);\n  }\n  // central cable bore through everything\n  translate([0, 0, -1])\n    cylinder(r = bore_d / 2, h = disc_h + snap_col + snap_dia + snap_lip + 6);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "snapfit-scad-clip-coupler",
      "title": "Snap-together box clip coupler",
      "description": "A two-plate coupler that snaps shut on itself: the bottom plate carries a positive snap-fit post and the matching top plate has the negative socket bored into it, so the two halves click together and stay closed. Print both, set the same diameter, and press them home. Great for closing enclosures or joining panels without fasteners.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Snap-together box clip coupler\n//\n// Source: snapfit-scad — MIT\n// Generated from the library above, used under its license.\n\nuse <snapfit-scad/snapfit.scad>\n\nplate_d   = 14;   // plate diameter\nplate_h   = 4;    // plate thickness\ngap       = 8;    // spacing between the two printed halves\nsnap_dia  = 4;    // snap diameter\nsnap_lip  = 1;    // snap lip\nsnap_col  = 6;    // snap column height\nneg_tol   = 0.5;  // clearance for the negative socket\n$fn = 96;\n\n// Bottom half: plate + positive snap post\nunion() {\n  cylinder(r = plate_d / 2, h = plate_h, center = true);\n  translate([0, 0, plate_h / 2])\n    snapfit(dia = snap_dia, lip = snap_lip, col_h = snap_col,\n            slot_ratio = 0.7, thickness = plate_h / 2);\n}\n\n// Top half: plate with negative socket, printed alongside\ntranslate([plate_d + gap, 0, 0])\n  difference() {\n    cylinder(r = plate_d / 2, h = plate_h, center = true);\n    translate([0, 0, -plate_h / 2 - 0.1])\n      snapfit_neg(dia = snap_dia, lip = snap_lip, col_h = snap_col,\n                  neg_tol = neg_tol, slot_ratio = 0.7, thickness = plate_h / 2);\n  }\n`);"
    },
    {
      "kind": "recipe",
      "id": "snapfit-scad-pcb-foot",
      "title": "Snap-in PCB standoff foot",
      "description": "A tapered standoff foot with a chamfered snap-fit pin that pushes into a mounting hole on a circuit board or chassis and locks underneath. Print four to stand a board off its case. Tune the foot size and the pin diameter to match your board holes.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Snap-in PCB standoff foot\n//\n// Source: snapfit-scad — MIT\n// Generated from the library above, used under its license.\n\nuse <snapfit-scad/snapfit.scad>\n\nfoot_d   = 10;   // base foot diameter\nfoot_h   = 4;    // standoff height\npin_dia  = 5;    // snap pin diameter (sized to the board hole)\npin_lip  = 1;    // snap lip\npin_col  = 2;    // pin column height\n$fn = 96;\n\nunion() {\n  // tapered standoff body\n  translate([0, 0, foot_h / 2])\n    cylinder(r1 = foot_d / 2, r2 = foot_d / 3, h = foot_h, center = true);\n  // chamfered snap pin on top\n  translate([0, 0, foot_h])\n    snapfit(dia = pin_dia, lip = pin_lip, col_h = pin_col,\n            col_tol = 0.1, slot_ratio = 0.8, under_chamfer_angle = 45);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "snapfit-scad-snap-rivet",
      "title": "Push-in snap rivet",
      "description": "A double-ended plastic push rivet: a snap-fit head on each end of a short waist, so it pushes through two aligned panel holes and locks both sheets together like a trim clip. Tune the head diameter, lip, and waist length for your combined panel thickness.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Push-in snap rivet\n//\n// Source: snapfit-scad — MIT\n// Generated from the library above, used under its license.\n\nuse <snapfit-scad/snapfit.scad>\n\nhead_dia = 6;    // snap head diameter\nhead_lip = 1.2;  // snap lip\nwaist_h  = 5;    // central waist (panel grip) length\n$fn = 96;\n\noverlap = 0.6;   // weld overlap so the two halves fuse into one solid\n\nunion() {\n  // bottom snap head pointing up (column slightly longer to overlap the join)\n  snapfit(dia = head_dia, lip = head_lip, col_h = waist_h / 2 + overlap,\n          col_tol = 0.1, slot_ratio = 0.7);\n\n  // mirror a second snap head on top, sharing/overlapping the waist\n  translate([0, 0, waist_h])\n    mirror([0, 0, 1])\n      snapfit(dia = head_dia, lip = head_lip, col_h = waist_h / 2 + overlap,\n              col_tol = 0.1, slot_ratio = 0.7);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "spatial-bbox-union-of-selection",
      "title": "Compute the bounding-box union of the selection",
      "description": "Build a spread of meshes, combine their bounding boxes into one envelope, then drop markers at the min and max corners so the combined volume is visible.",
      "intent": "mesh",
      "editor": "model",
      "category": "spatial",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "ModelEditor.create.locator",
        "MathLib.distance",
        "mesh.geometry.bbox",
        "node.translate"
      ],
      "code": "// Self-contained from an EMPTY scene: build a spread of meshes, then union\n// their bounding boxes and mark the two extreme corners.\n\n// 1. Three meshes of different sizes at different positions.\nconst a = await create.cube({ width: 1, height: 1, depth: 1, name: 'PartA' });\na.translate.set([-2, 0, 0]);\nselect(null, { mode: 'clear' });\nconst b = await create.cube({ width: 2, height: 0.5, depth: 1, name: 'PartB' });\nb.translate.set([3, 1, 0]);\nselect(null, { mode: 'clear' });\nconst c = await create.sphere({ radius: 0.8, name: 'PartC' });\nc.translate.set([0, -2, 2]);\nselect(null, { mode: 'clear' });\n\nconst parts = [a, b, c];\n\n// 2. Union every part's WORLD bounding box (bbox lives on mesh.geometry).\nconst min = [Infinity, Infinity, Infinity];\nconst max = [-Infinity, -Infinity, -Infinity];\nfor (const m of parts) {\n  const bb = m.geometry.bbox({ space: 'world' });\n  const mn = [bb.min.x, bb.min.y, bb.min.z];\n  const mx = [bb.max.x, bb.max.y, bb.max.z];\n  for (let i = 0; i < 3; i++) {\n    if (mn[i] < min[i]) min[i] = mn[i];\n    if (mx[i] > max[i]) max[i] = mx[i];\n  }\n}\n\n// 3. Drop markers at the two extreme corners (create.locator is SYNC).\nconst minMarker = create.locator({ name: 'UnionMin', position: min });\nconst maxMarker = create.locator({ name: 'UnionMax', position: max });\n\n// 4. Report the envelope + its diagonal (never a silent no-op).\nconsole.log('union bbox min', JSON.stringify(min.map((v) => +v.toFixed(3))),\n  'max', JSON.stringify(max.map((v) => +v.toFixed(3))),\n  '| diagonal', MathLib.distance(min, max).toFixed(3), 'units',\n  '| markers', minMarker.name, '+', maxMarker.name);\n",
      "expectedOutput": "console: union bbox min / max + diagonal length; viewport: two markers framing the combined volume."
    },
    {
      "kind": "recipe",
      "id": "spatial-kd-trees-2d-3d",
      "title": "Build and query 2D and 3D spatial indexes",
      "description": "Construct MathLib.kdTree3D and MathLib.kdTree2D over a synthetic point cloud; demonstrate the KdTree3D + KdTree2D class re-exports.",
      "intent": "math",
      "editor": "model",
      "category": "spatial",
      "apiSurfaces": [
        "MathLib.kdTree3D",
        "MathLib.kdTree2D",
        "MathLib.KdTree3D",
        "MathLib.KdTree2D"
      ],
      "code": "const points3 = [[0,0,0], [1,1,1], [2,0,0], [3,3,3], [0,2,1]];\nconst tree3 = MathLib.kdTree3D(points3);\nconsole.log('kdTree3D instanceof KdTree3D:', tree3 instanceof MathLib.KdTree3D);\n\nconst closest3 = tree3.closest([1.1, 1.0, 1.0]);\nconsole.log('3D closest to [1.1,1,1]:', JSON.stringify(closest3));\n\nconst within3 = tree3.within([0, 0, 0], 2.5);\nconsole.log('3D within 2.5 of origin:', JSON.stringify({ count: within3.indices.length }));\n\nconst points2 = [[0,0], [1,0], [0,1], [1,1], [0.5,0.5]];\nconst tree2 = MathLib.kdTree2D(points2);\nconsole.log('kdTree2D instanceof KdTree2D:', tree2 instanceof MathLib.KdTree2D);\n\nconst closest2 = tree2.closest([0.4, 0.6]);\nconsole.log('2D closest to [0.4,0.6]:', JSON.stringify(closest2));\n\nconst knn2 = tree2.knearest([0, 0], 3);\nconsole.log('2D 3-nearest to origin:', JSON.stringify(knn2));\n",
      "expectedOutput": "console: tree types verified via instanceof; closest + within results."
    },
    {
      "kind": "recipe",
      "id": "spatial-mass-select-within-radius",
      "title": "Select every mesh within a radius",
      "description": "Scatter several meshes, build a kd-tree over their centroids, and select every mesh whose centroid sits inside a sphere around a probe point.",
      "intent": "mesh",
      "editor": "model",
      "category": "spatial",
      "apiSurfaces": [
        "ModelEditor.create.cube",
        "MathLib.kdTree3D",
        "ModelEditor.scene.ls",
        "ModelEditor.scene.select",
        "mesh.geometry.bbox"
      ],
      "code": "// Self-contained from an EMPTY scene: scatter cubes, then select the ones\n// whose centroid falls inside a probe sphere via a kd-tree radius query.\n\n// 1. Scatter eight cubes: some near the origin, some far away.\nconst placements = [\n  [0, 0, 0], [1, 0, 1], [-1, 0.5, 0], [0.5, -0.5, -1],   // inside the radius\n  [6, 0, 0], [0, 7, 0], [0, 0, -8], [5, 5, 5],            // outside\n];\nconst meshes = [];\nfor (let i = 0; i < placements.length; i++) {\n  const c = await create.cube({ width: 0.6, name: 'Box' + i });\n  c.translate.set(placements[i]);\n  meshes.push(c);\n  select(null, { mode: 'clear' });\n}\n\n// 2. Compute each mesh's world-space centroid (bbox lives on mesh.geometry).\nconst centroids = meshes.map((m) => {\n  const bb = m.geometry.bbox({ space: 'world' });\n  return [\n    (bb.min.x + bb.max.x) / 2,\n    (bb.min.y + bb.max.y) / 2,\n    (bb.min.z + bb.max.z) / 2,\n  ];\n});\n\n// 3. Build the kd-tree and run a radius query. within(point, radius) returns\n//    { indices, distances } (paired arrays sorted ascending by distance).\nconst probe = [0, 0, 0];\nconst radius = 3;\nconst tree = MathLib.kdTree3D(centroids);\nconst result = tree.within(probe, radius);\nconst winners = result.indices.map((i) => meshes[i]);\n\n// 4. Select the hits in ONE replace call, then report (never a silent no-op).\nawait select(winners);\nconsole.log('selected', winners.length, 'of', meshes.length,\n  'meshes within', radius, 'units of', JSON.stringify(probe) + ':',\n  winners.map((m) => m.name).join(', '));\n",
      "expectedOutput": "console: list of selected mesh names within the radius; outliner: exactly those meshes appear selected."
    },
    {
      "kind": "recipe",
      "id": "spatial-nearest-mesh-to-point",
      "title": "Find the nearest mesh to a target point",
      "description": "Build a spatial index from every mesh centroid in the scene and query the closest one to a target point. Marks the hit with a locator so the answer is visible.",
      "intent": "mesh",
      "editor": "model",
      "category": "spatial",
      "apiSurfaces": [
        "ModelEditor.scene.ls",
        "MathLib.kdTree3D",
        "ModelEditor.create.locator",
        "MathLib.distance",
        "mesh.bbox",
        "node.translate"
      ],
      "code": "// Build a kd-tree from mesh centroids, then snap to the closest.\nconst target = [0, 1, 0];\nconst meshes = ls({ type: 'mesh' });\nif (meshes.length === 0) { console.warn('Need at least one mesh in scene.'); return; }\n\nconst centroids = meshes.map((m) => {\n  const bb = m.bbox();\n  return [\n    (bb.min.x + bb.max.x) / 2,\n    (bb.min.y + bb.max.y) / 2,\n    (bb.min.z + bb.max.z) / 2,\n  ];\n});\nconst tree = MathLib.kdTree3D(centroids);\nconst hit = tree.closest(target);\nconst winner = meshes[hit.index];\n\nconst marker = create.locator({ name: 'nearest-marker' });\nmarker.translate.set(centroids[hit.index]);\nconst dist = MathLib.distance(target, centroids[hit.index]);\nconsole.log('nearest:', winner.name, 'at distance', dist.toFixed(4));\n",
      "expectedOutput": "console: nearest mesh name + distance; viewport: locator placed on the centroid of the winning mesh."
    },
    {
      "kind": "recipe",
      "id": "spatial-snap-marker-to-nearest-vertex",
      "title": "Snap a marker to the nearest vertex on a mesh",
      "description": "Build a mesh, index every vertex into a kd-tree, then snap a fresh marker to the vertex closest to a probe point.",
      "intent": "mesh",
      "editor": "model",
      "category": "spatial",
      "apiSurfaces": [
        "ModelEditor.create.sphere",
        "MathLib.kdTree3D",
        "ModelEditor.create.locator",
        "mesh.verts",
        "node.translate"
      ],
      "code": "// Self-contained from an EMPTY scene: build a mesh, index its vertices, then\n// snap a marker onto whichever vertex is closest to a probe point.\n\n// 1. A sphere gives a dense, evenly distributed vertex set to search.\nconst mesh = await create.sphere({\n  radius: 1.5,           // 1.5 m radius (>0)\n  widthSegments: 24,     // longitude divisions\n  heightSegments: 16,    // latitude divisions\n  name: 'Ball',\n});\nselect(null, { mode: 'clear' });\n\n// 2. Collect every vertex world position into a point cloud. mesh.verts.count\n//    = vertex total; mesh.verts.position(i) returns a Vec3 (.x / .y / .z).\nconst points = [];\nfor (let i = 0; i < mesh.verts.count; i++) {\n  const p = mesh.verts.position(i);\n  points.push([p.x, p.y, p.z]);\n}\n\n// 3. Build the kd-tree and ask for the single closest vertex to the probe.\n//    closest(point) returns { index, distance } (index into the point cloud).\nconst probe = [2, 0.5, 0.5];   // outside the sphere, off to +X\nconst tree = MathLib.kdTree3D(points);\nconst hit = tree.closest(probe);\n\n// 4. Drop a marker on that vertex and report (never a silent no-op).\nconst winner = points[hit.index];\nconst marker = create.locator({ name: 'VertMarker', position: winner });\nconsole.log('probe', JSON.stringify(probe),\n  '-> nearest vertex', hit.index, 'at', JSON.stringify(winner.map((v) => +v.toFixed(3))),\n  '| distance', hit.distance.toFixed(4), 'units | marker', marker.name);\n",
      "expectedOutput": "console: nearest vertex index + position + distance; viewport: a marker named \"VertMarker\" sitting on that vertex."
    },
    {
      "kind": "recipe",
      "id": "threads-scad-auger-bit",
      "title": "Auger drill bit",
      "description": "A printable auger-style bit with a deep helical flute that comes to a point, the shape used for hand drills, soil augers, and material feeders. Print it as a demo, a feeder screw, or a soft-material auger. Tune the diameters, height, and pitch to change how aggressive the flute is.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Auger drill bit\n//\n// Source: threads-scad — CC0\n// Generated from the library above, used under its license.\n\nuse <threads-scad/threads.scad>\n\nouter_d   = 20;  // outer flute diameter\ninner_d   = 8;   // solid core diameter\nauger_h   = 50;  // overall height of the fluted section\nauger_p   = 14;  // pitch (vertical rise per turn) — larger = steeper flute\n$fn = 64;\n\nunion() {\n  AugerThread(outer_d, inner_d, auger_h, auger_p, tooth_angle = 30, tolerance = 0.4);\n  // short solid shank below the flutes for chucking\n  translate([0, 0, -10]) cylinder(d = inner_d, h = 10.01);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "threads-scad-bottle-jar",
      "title": "Threaded storage jar neck",
      "description": "A small open-top storage jar whose neck carries external threads, so a matching threaded lid screws on tight. Print it for screws, beads, spices, or small parts. Resize the body and the neck thread to make any jar you need.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Threaded storage jar neck\n//\n// Source: threads-scad — CC0\n// Generated from the library above, used under its license.\n\nuse <threads-scad/threads.scad>\n\njar_diam     = 40;  // outer jar body diameter\njar_height   = 45;  // jar body height (below the neck)\nwall         = 2.4; // wall thickness\nneck_diam    = 30;  // external thread (neck) diameter\nneck_height  = 12;  // threaded neck height\nneck_pitch   = 3;   // coarse thread pitch for an easy screw lid\n$fn = 96;\n\ndifference() {\n  // Solid outer shape: jar body + threaded neck, fused into one piece.\n  union() {\n    cylinder(d = jar_diam, h = jar_height);\n    // Neck overlaps 1 mm into the body so the two weld into a single shell.\n    translate([0, 0, jar_height - 1])\n      ScrewThread(neck_diam, neck_height + 1, pitch = neck_pitch, tolerance = 0.4);\n  }\n  // One continuous internal cavity opening through the neck.\n  translate([0, 0, wall])\n    cylinder(d = jar_diam - 2 * wall, h = jar_height + neck_height);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "threads-scad-m10-hex-nut",
      "title": "M10 hex nut",
      "description": "A printable M10 hex nut with metric-standard internal threads, sized to thread onto a matching M10 bolt. Great as a captive nut, a knob, or a spare fastener. Adjust the diameter for any metric size and the thickness for a thinner jam nut.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// M10 hex nut\n//\n// Source: threads-scad — CC0\n// Generated from the library above, used under its license.\n\nuse <threads-scad/threads.scad>\n\nnut_diam  = 10;  // nominal metric thread diameter (M10)\nnut_thick = 0;   // 0 = use the metric-standard thickness for this size\nfit_tol   = 0.4; // print clearance tolerance\n$fn = 48;\n\nMetricNut(nut_diam, thickness = nut_thick, tolerance = fit_tol);\n`);"
    },
    {
      "kind": "recipe",
      "id": "threads-scad-m8-hex-bolt",
      "title": "M8 hex-head bolt",
      "description": "A real, fully printable M8 hex-head machine bolt with metric-standard threads, a hex head, and a built-in hex-key recess on top. Print it for fixtures, jigs, or anywhere a plastic fastener works. Change the diameter and length numbers to make any metric size.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// M8 hex-head bolt\n//\n// Source: threads-scad — CC0\n// Generated from the library above, used under its license.\n\nuse <threads-scad/threads.scad>\n\nbolt_diam   = 8;   // nominal metric thread diameter (M8)\nbolt_length = 25;  // threaded shaft length in mm\nfit_tol     = 0.4; // print clearance tolerance\n$fn = 48;\n\nMetricBolt(bolt_diam, bolt_length, tolerance = fit_tol);\n`);"
    },
    {
      "kind": "recipe",
      "id": "threads-scad-threaded-knob",
      "title": "Threaded grip knob",
      "description": "A round, knurled-free grip knob with a threaded blind hole in the base so it screws straight onto an M6 stud or bolt. Use it as a tightening handle for jigs, clamps, or machine adjustments. Tune the knob size or the thread diameter to fit your hardware.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Threaded grip knob\n//\n// Source: threads-scad — CC0\n// Generated from the library above, used under its license.\n\nuse <threads-scad/threads.scad>\n\nknob_diam   = 30;  // outer knob diameter in mm\nknob_height = 16;  // knob height in mm\nthread_diam = 6;   // metric thread the knob screws onto (M6)\nthread_len  = 12;  // depth of the threaded hole\nfit_tol     = 0.4; // print clearance tolerance\n$fn = 64;\n\ndifference() {\n  // domed grip body\n  union() {\n    cylinder(d = knob_diam, h = knob_height);\n    translate([0, 0, knob_height])\n      scale([1, 1, 0.45]) sphere(d = knob_diam);\n  }\n  // threaded mounting hole entering from the base\n  ScrewHole(thread_diam, thread_len, position = [0, 0, -0.01], tolerance = fit_tol);\n}\n`);"
    },
    {
      "kind": "recipe",
      "id": "viewport-camera-control-tour",
      "title": "Programmatic camera dolly + orbit + pan tour",
      "description": "Frame the scene, then drive the camera through orbit + pan + dolly movements while logging viewport center.",
      "intent": "scene",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.viewport.dolly",
        "ModelEditor.viewport.orbit",
        "ModelEditor.viewport.pan",
        "ModelEditor.viewport.zoomToFit",
        "ModelEditor.viewport.frameSelection",
        "ModelEditor.viewport.center",
        "Utils.wait.frame"
      ],
      "code": "viewport.zoomToFit(null, 2.5);\nawait Utils.wait.frame();\n\nviewport.orbit(30, 15);\nawait Utils.wait.frame();\nconsole.log('after orbit, center:', viewport.center().x.toFixed(1), viewport.center().y.toFixed(1));\n\nviewport.pan(100, -50);\nawait Utils.wait.frame();\n\nviewport.dolly(1.5);\nawait Utils.wait.frame();\nconsole.log('after dolly out');\n\nviewport.dolly(0.5);\nawait Utils.wait.frame();\nconsole.log('after dolly in; final center:', viewport.center().x.toFixed(1), viewport.center().y.toFixed(1));\n\nviewport.frameSelection();\nawait Utils.wait.frame();\n",
      "expectedOutput": "console: camera state at each step; viewport center coords."
    },
    {
      "kind": "recipe",
      "id": "viewport-display-modes-gizmo",
      "title": "Toggle viewport display modes + gizmo settings",
      "description": "Control wireframe / x-ray / backface-cull / transform-mode / pivot-mode / snap-mode / grid-unit getters and setters.",
      "intent": "scene",
      "editor": "model",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.viewport.setWireframe",
        "ModelEditor.viewport.wireframe",
        "ModelEditor.viewport.toggleWireframe",
        "ModelEditor.viewport.setXray",
        "ModelEditor.viewport.xray",
        "ModelEditor.viewport.toggleXray",
        "ModelEditor.viewport.setBackfaceCull",
        "ModelEditor.viewport.backfaceCull",
        "ModelEditor.viewport.setTransformMode",
        "ModelEditor.viewport.transformMode",
        "ModelEditor.viewport.setPivotMode",
        "ModelEditor.viewport.pivotMode",
        "ModelEditor.viewport.setSnapMode",
        "ModelEditor.viewport.snapMode",
        "ModelEditor.viewport.setGridUnit",
        "ModelEditor.viewport.gridUnit",
        "ModelEditor.viewport.setShowGrid",
        "ModelEditor.viewport.showGrid",
        "ModelEditor.viewport.toggleGrid",
        "ModelEditor.viewport.setOrtho",
        "ModelEditor.viewport.isOrtho",
        "ModelEditor.viewport.toggleOrtho",
        "Utils.wait.frame"
      ],
      "code": "viewport.setWireframe(true);\nviewport.setXray(true);\nviewport.setBackfaceCull(true);\nawait Utils.wait.frame();\nconsole.log('wireframe:', viewport.wireframe, 'xray:', viewport.xray, 'backfaceCull:', viewport.backfaceCull);\n\nviewport.setTransformMode('rotate');\nviewport.setPivotMode('boundingBox');\nawait Utils.wait.frame();\nconsole.log('transformMode:', viewport.transformMode, 'pivotMode:', viewport.pivotMode);\n\nviewport.setSnapMode({ translate: true, rotate: false, scale: false }, true);\nawait Utils.wait.frame();\nconst snap = viewport.snapMode;\nconsole.log('snap:', snap.translate, snap.rotate, snap.scale);\n\nviewport.setGridUnit('m');\nviewport.setShowGrid(true);\nawait Utils.wait.frame();\nconsole.log('gridUnit:', viewport.gridUnit, 'grid:', viewport.showGrid);\n\nviewport.toggleOrtho();\nawait Utils.wait.frame();\nconsole.log('ortho?', viewport.isOrtho);\nviewport.toggleWireframe();\nviewport.toggleXray();\nviewport.toggleGrid();\nawait Utils.wait.frame();\nconsole.log('final wireframe:', viewport.wireframe, 'xray:', viewport.xray, 'grid:', viewport.showGrid);\n",
      "expectedOutput": "console: per-toggle current state; final view in ortho mode with wireframe."
    },
    {
      "kind": "recipe",
      "id": "viewport-frame-all-then-zoom-to-selection",
      "title": "Frame everything, then zoom to the selection",
      "description": "Show the whole scene first, wait for a frame, then tighten on the current selection. Useful for guided tours that need an overview shot followed by a detail shot.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.viewport.frameAll",
        "ModelEditor.viewport.zoomToFit",
        "PreviewEditor.viewport.captureScreenshot"
      ],
      "code": "// Two-shot framing: overview then close-up on selection.\nviewport.frameAll();\nawait new Promise((r) => requestAnimationFrame(() => r(null)));\nconst overview = await viewport.captureScreenshot();\nconsole.log('overview:', overview.width + 'x' + overview.height);\n\nconst sel = ls({selected: true});\nif (sel.length === 0) { console.log('Nothing selected; overview shot only.'); return; }\nviewport.zoomToFit(sel, 1.5);\nawait new Promise((r) => requestAnimationFrame(() => r(null)));\nconst closeup = await viewport.captureScreenshot();\nconsole.log('closeup:', closeup.width + 'x' + closeup.height);\n",
      "expectedOutput": "console: two screenshot dimensions; viewport: shows the whole scene then zooms to the selected nodes."
    },
    {
      "kind": "recipe",
      "id": "viewport-orbit-turntable-with-shots",
      "title": "Orbit the camera and capture a turntable contact sheet",
      "description": "Orbit yaw by even increments, snapshot at each step, and report each capture. Produces a \"shot-NN\" sequence ready for a thumbnail mosaic.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.viewport.orbit",
        "ModelEditor.viewport.zoomToFit",
        "PreviewEditor.viewport.captureScreenshot"
      ],
      "code": "// 8-step turntable. Useful for product-shot mosaics.\nconst steps = 8;\nviewport.zoomToFit(null, 2.0);\n\nfor (let i = 0; i < steps; i++) {\n  viewport.orbit(360 / steps, 0);\n  await new Promise((r) => requestAnimationFrame(() => r(null)));\n  const shot = await viewport.captureScreenshot();\n  const idx = String(i).padStart(2, '0');\n  // Each shot.blob can be persisted with io.saveFile when you want files.\n  console.log('shot-' + idx, shot.width + 'x' + shot.height, 'captured', shot.blob.size, 'bytes');\n}\n",
      "expectedOutput": "One console line per orbit step (shot-00 .. shot-NN) with the captured image size and bytes."
    },
    {
      "kind": "recipe",
      "id": "viewport-preset-screenshots",
      "title": "Cycle the viewport presets and capture each",
      "description": "Frame the scene, switch between top / front / right / persp views, snapshot each, and report each capture. Builds a quick orthographic spec sheet.",
      "intent": "previewer",
      "editor": "previewer",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.viewport.setView",
        "ModelEditor.viewport.frameAll",
        "PreviewEditor.viewport.captureScreenshot",
        "ModelEditor.dev.progress"
      ],
      "code": "// Quad-view contact sheet for an asset spec.\nconst views = ['top', 'front', 'right', 'persp'];\nfor (let i = 0; i < views.length; i++) {\n  dev.progress({ phase: 'capturing', current: i + 1, total: views.length, label: views[i] });\n  viewport.setView(views[i]);\n  viewport.frameAll();\n  await new Promise((r) => requestAnimationFrame(() => r(null)));\n  const shot = await viewport.captureScreenshot();\n  // shot.blob is the binary image; hand it to io.saveFile to write a PNG.\n  console.log(views[i], shot.width + 'x' + shot.height, 'captured', shot.blob.size, 'bytes');\n}\n",
      "expectedOutput": "One console line per view (top / front / right / persp) with the captured image size and bytes."
    },
    {
      "kind": "recipe",
      "id": "viewport-tool-dispatch",
      "title": "Switch active tool and probe pickAt",
      "description": "Cycle through measure / drawline / select tools; read activeTool; raycast viewport coords via pickAt.",
      "intent": "mesh",
      "editor": "model",
      "category": "viewport",
      "apiSurfaces": [
        "ModelEditor.tool.setActive",
        "ModelEditor.tool.active",
        "ModelEditor.viewport.pickAt",
        "ModelEditor.create.cube",
        "Utils.wait.frame",
        "node.translate"
      ],
      "code": "const cube = await create.cube({ name: 'PickTarget' });\ncube.translate.set([2.5, 1.3, -0.7]);\nselect(null, {mode: 'clear'});\nawait Utils.wait.frame();\n\nconsole.log('activeTool initial:', tool.active);\n\ntool.setActive('measure');\nconsole.log('after measure:', tool.active);\n\ntool.setActive('drawline');\nconsole.log('after drawline:', tool.active);\n\ntool.setActive('select');\nconsole.log('after select:', tool.active);\n\ntool.setActive(null);\nconsole.log('after null:', tool.active);\n\n// pickAt: raycast at viewport client coords. Returns null when no hit.\nconst hit = viewport.pickAt(400, 300);\nconsole.log('pickAt(400, 300):', JSON.stringify(hit ? { meshId: hit.meshId, point: hit.point } : null));\n",
      "expectedOutput": "console: active tool before/after each switch; pickAt result."
    },
    {
      "kind": "recipe",
      "id": "wait-ms-and-until",
      "title": "Sleep with Utils.wait.ms and poll with Utils.wait.until",
      "description": "Demonstrate the two non-frame wait helpers: fixed-delay sleep and predicate poll.",
      "intent": "scene",
      "editor": "model",
      "category": "misc",
      "apiSurfaces": [
        "Utils.wait.ms",
        "Utils.wait.until",
        "Utils.wait.frame"
      ],
      "code": "const t0 = performance.now();\n\nawait Utils.wait.ms(50);\nconst t1 = performance.now();\nconsole.log('after ms(50): elapsed=' + (t1 - t0).toFixed(1) + 'ms');\n\nlet counter = 0;\nconst timer = setInterval(() => { counter++; }, 10);\n\ntry {\n  await Utils.wait.until(() => counter >= 3, { timeoutMs: 1000, intervalMs: 20 });\n  console.log('until() resolved at counter=' + counter);\n} catch (err) {\n  console.log('until() timed out:', err.message);\n} finally {\n  clearInterval(timer);\n}\n\nawait Utils.wait.frame();\nconsole.log('frame() returned');\n",
      "expectedOutput": "console: timestamps before and after each wait; predicate-driven loop completes."
    },
    {
      "kind": "recipe",
      "id": "yappbox-basic-enclosure",
      "title": "Basic project box enclosure",
      "description": "A clean two-part snap-together project box sized for a small circuit board, with a base tray and matching overlapping lid. Adjust the board length, width, and wall height to fit your own electronics.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Basic project box enclosure\n//\n// Source: YAPP_Box — MIT\n// Generated from the library above, used under its license.\n\ninclude <yappbox/YAPPgenerator_v3.scad>\n\n// ---- Board footprint the box is built around ----\npcbLength      = 70;   // front-to-back (X)\npcbWidth       = 45;   // side-to-side (Y)\npcbThickness   = 1.6;\n\n// ---- Padding between the board and the inside walls ----\npaddingFront   = 3;\npaddingBack    = 3;\npaddingRight   = 3;\npaddingLeft    = 3;\n\n// ---- Shell thicknesses + heights ----\nwallThickness      = 2.0;\nbasePlaneThickness = 1.5;\nlidPlaneThickness  = 1.5;\nbaseWallHeight     = 14;\nlidWallHeight      = 8;\n\n// ---- Overlapping ridge that mates base + lid ----\nridgeHeight = 4.0;\nroundRadius = 2.0;\n\nprintBaseShell = true;\nprintLidShell  = true;\n\nYAPPgenerate();\n`);"
    },
    {
      "kind": "recipe",
      "id": "yappbox-pcb-tray",
      "title": "PCB tray with standoffs",
      "description": "A base tray and lid with four printed standoffs that lift and fixate a circuit board off the floor, leaving room underneath for solder joints, plus a side cutout for a power jack. Tune the standoff height and diameter for your board's mounting holes.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// PCB tray with standoffs\n//\n// Source: YAPP_Box — MIT\n// Generated from the library above, used under its license.\n\ninclude <yappbox/YAPPgenerator_v3.scad>\n\npcbLength      = 75;\npcbWidth       = 50;\npcbThickness   = 1.6;\nstandoffHeight = 6.0;\nstandoffDiameter = 7;\n\npaddingFront   = 4;\npaddingBack    = 4;\npaddingRight   = 4;\npaddingLeft    = 4;\n\nwallThickness      = 2.0;\nbasePlaneThickness = 1.5;\nlidPlaneThickness  = 1.5;\nbaseWallHeight     = 18;\nlidWallHeight      = 9;\n\nridgeHeight = 4.0;\nroundRadius = 2.0;\n\nprintBaseShell = true;\nprintLidShell  = true;\n\n// ---- Standoffs that raise + pin the board above the base floor ----\n// [posx, posy] relative to the PCB corners\npcbStands =\n[\n  [6,  6],\n  [pcbLength - 6, 6],\n  [6,  pcbWidth - 6],\n  [pcbLength - 6, pcbWidth - 6],\n];\n\n// ---- Power-jack hole in the left wall ----\ncutoutsLeft =\n[\n  [30, 6, 0, 0, 5.5, yappCircle],\n];\n\nYAPPgenerate();\n`);"
    },
    {
      "kind": "recipe",
      "id": "yappbox-snap-fit-box",
      "title": "Tool-free snap-fit box",
      "description": "A compact enclosure that clicks shut with integrated snap-fit clips on all four walls, so it needs no screws to close. Great for battery holders or quick-access gadgets. Adjust the clip width and box height to control how firmly the lid grips.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Tool-free snap-fit box\n//\n// Source: YAPP_Box — MIT\n// Generated from the library above, used under its license.\n\ninclude <yappbox/YAPPgenerator_v3.scad>\n\npcbLength      = 60;\npcbWidth       = 40;\npcbThickness   = 1.6;\n\npaddingFront   = 3;\npaddingBack    = 3;\npaddingRight   = 3;\npaddingLeft    = 3;\n\nwallThickness      = 2.0;\nbasePlaneThickness = 1.5;\nlidPlaneThickness  = 1.5;\nbaseWallHeight     = 12;\nlidWallHeight      = 9;\n\nridgeHeight = 4.0;\nroundRadius = 2.0;\n\nprintBaseShell = true;\nprintLidShell  = true;\n\n// ---- Snap-fit clips so the lid clicks shut without screws ----\n// [position-along-wall, clip-width, which-wall, ...]\nsnapJoins =\n[\n  [20, 8, yappLeft,  yappRight, yappCenter, yappSymmetric],\n  [16, 8, yappFront, yappBack,  yappCenter, yappSymmetric],\n];\n\nYAPPgenerate();\n`);"
    },
    {
      "kind": "recipe",
      "id": "yappbox-vented-enclosure",
      "title": "Vented enclosure with USB port",
      "description": "A project box whose lid carries a row of ventilation slots for airflow and whose front wall has a rectangular cutout for a USB connector. Ideal for boards that run warm. Tune the slot count, slot size, and port opening to match your hardware.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Vented enclosure with USB port\n//\n// Source: YAPP_Box — MIT\n// Generated from the library above, used under its license.\n\ninclude <yappbox/YAPPgenerator_v3.scad>\n\npcbLength      = 80;\npcbWidth       = 55;\npcbThickness   = 1.6;\n\npaddingFront   = 4;\npaddingBack    = 4;\npaddingRight   = 4;\npaddingLeft    = 4;\n\nwallThickness      = 2.0;\nbasePlaneThickness = 1.5;\nlidPlaneThickness  = 1.5;\nbaseWallHeight     = 16;\nlidWallHeight      = 10;\n\nridgeHeight = 4.0;\nroundRadius = 2.5;\n\nprintBaseShell = true;\nprintLidShell  = true;\n\n// ---- Ventilation slots across the lid ----\ncutoutsLid =\n[\n  [20, 12, 4, 35, 0, yappRectangle],\n  [30, 12, 4, 35, 0, yappRectangle],\n  [40, 12, 4, 35, 0, yappRectangle],\n  [50, 12, 4, 35, 0, yappRectangle],\n];\n\n// ---- USB connector opening in the front wall ----\ncutoutsFront =\n[\n  [25, 4, 14, 8, 1.5, yappRoundedRect],\n];\n\nYAPPgenerate();\n`);"
    },
    {
      "kind": "recipe",
      "id": "yappbox-wall-mount-case",
      "title": "Wall-mountable screwed case",
      "description": "An enclosure with external mounting flanges on two sides so it can be screwed to a wall or panel, plus internal screw posts that bolt the lid down onto the base for a secure, serviceable closure. Tune the flange screw size and post diameter to your fasteners.",
      "intent": "cad",
      "editor": "model",
      "category": "geometry",
      "apiSurfaces": [
        "ModelEditor.execOpenScad"
      ],
      "code": "await execOpenScad(`\n// Wall-mountable screwed case\n//\n// Source: YAPP_Box — MIT\n// Generated from the library above, used under its license.\n\ninclude <yappbox/YAPPgenerator_v3.scad>\n\npcbLength      = 90;\npcbWidth       = 60;\npcbThickness   = 1.6;\n\npaddingFront   = 5;\npaddingBack    = 5;\npaddingRight   = 5;\npaddingLeft    = 5;\n\nwallThickness      = 2.4;\nbasePlaneThickness = 2.0;\nlidPlaneThickness  = 2.0;\nbaseWallHeight     = 18;\nlidWallHeight      = 11;\n\nridgeHeight = 5.0;\nroundRadius = 3.0;\n\nprintBaseShell = true;\nprintLidShell  = true;\n\n// ---- External flanges for screwing the case to a wall ----\n// [pos, screwDiameter, slotWidth, height, which-wall(s)]\nboxMounts =\n[\n  [15, 3, 3, 3, yappLeft],\n  [15, 3, 3, 3, yappRight],\n];\n\n// ---- Internal screw posts that bolt the lid to the base ----\n// [posx, posy, standHeight, screwD, headD, insertD, outsideD, ...]\nconnectors =\n[\n  [8,  8,  5, 2.5, 5, 3.5, 7, yappAllCorners, yappCoordBoxInside],\n];\n\nYAPPgenerate();\n`);"
    }
  ]
}
