@litfamily/litopencode 1.0.31 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README-Ko-KR.md +6 -6
  3. package/README.md +6 -6
  4. package/TRADEMARK.md +12 -0
  5. package/dist/activation-primary-prompts.js +5 -1
  6. package/dist/activation-probe.js +1 -0
  7. package/dist/activation-prompt-utils.d.ts +1 -1
  8. package/dist/activation-reentry.d.ts +20 -0
  9. package/dist/activation-reentry.js +110 -1
  10. package/dist/activation-routing.d.ts +2 -1
  11. package/dist/activation-routing.js +100 -11
  12. package/dist/activation-workflow-prompts.d.ts +4 -0
  13. package/dist/activation-workflow-prompts.js +30 -0
  14. package/dist/activation.d.ts +1 -1
  15. package/dist/activation.js +44 -5
  16. package/dist/adhd-next.d.ts +51 -0
  17. package/dist/adhd-next.js +150 -0
  18. package/dist/agents/defaults.js +1 -1
  19. package/dist/cli/managed-skill-assets.d.ts +1 -1
  20. package/dist/cli/managed-skill-assets.js +7 -1
  21. package/dist/cli/native-skill-integrity.js +1 -1
  22. package/dist/cli.js +7 -1
  23. package/dist/commands.d.ts +28 -1
  24. package/dist/commands.js +58 -5
  25. package/dist/durable-plan.d.ts +5 -1
  26. package/dist/durable-plan.js +14 -2
  27. package/dist/features.d.ts +80 -1
  28. package/dist/features.js +105 -1
  29. package/dist/hooks.js +3 -0
  30. package/dist/ledger.js +12 -1
  31. package/dist/plan-scope-roots.d.ts +23 -0
  32. package/dist/plan-scope-roots.js +116 -0
  33. package/dist/restart-notice.d.ts +7 -0
  34. package/dist/restart-notice.js +40 -0
  35. package/dist/secret-shapes.js +5 -5
  36. package/dist/server.js +4 -0
  37. package/dist/session-liveness.d.ts +20 -0
  38. package/dist/session-liveness.js +99 -0
  39. package/dist/skills.d.ts +22 -1
  40. package/dist/skills.js +36 -0
  41. package/dist/start-work-typed.d.ts +34 -0
  42. package/dist/start-work-typed.js +167 -0
  43. package/dist/state.d.ts +3 -0
  44. package/dist/state.js +3 -0
  45. package/dist/tools.js +22 -2
  46. package/dist/tui/mission-band.d.ts +22 -1
  47. package/dist/tui/mission-band.js +75 -16
  48. package/dist/tui/mission-looks.d.ts +38 -0
  49. package/dist/tui/mission-looks.js +73 -0
  50. package/dist/tui/mission-model.d.ts +2 -0
  51. package/dist/tui/mission-model.js +3 -1
  52. package/dist/tui/mission-rainbow.d.ts +33 -0
  53. package/dist/tui/mission-rainbow.js +90 -0
  54. package/dist/tui/mission-source.d.ts +49 -0
  55. package/dist/tui/mission-source.js +217 -2
  56. package/dist/tui/mission-text.d.ts +2 -0
  57. package/dist/tui/mission-text.js +9 -1
  58. package/dist/tui/mission-threads.d.ts +58 -0
  59. package/dist/tui/mission-threads.js +128 -0
  60. package/dist/tui.js +112 -7
  61. package/dist/uiux-visual-catalog.js +8 -1
  62. package/dist/v2/events.js +3 -0
  63. package/dist/v2/session-hooks.js +38 -5
  64. package/dist/workflow-stack.d.ts +53 -0
  65. package/dist/workflow-stack.js +196 -0
  66. package/docs/assets/readme/badge-version.svg +1 -1
  67. package/docs/assets/skills/lit-schematic.webp +0 -0
  68. package/docs/reference-Ko-KR.md +48 -3
  69. package/docs/reference.md +54 -3
  70. package/package.json +1 -1
  71. package/skills/frontend-ui-ux/SKILL.md +3 -2
  72. package/skills/frontend-ui-ux/kit/components.css +414 -0
  73. package/skills/frontend-ui-ux/kit/fonts.json +22 -0
  74. package/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
  75. package/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
  76. package/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
  77. package/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
  78. package/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
  79. package/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
  80. package/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
  81. package/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
  82. package/skills/frontend-ui-ux/kit/ui.js +226 -0
  83. package/skills/frontend-ui-ux/references/brand-and-imagery.md +2 -2
  84. package/skills/frontend-ui-ux/references/complete-contract.md +2 -1
  85. package/skills/frontend-ui-ux/references/craft-floor.md +6 -2
  86. package/skills/frontend-ui-ux/references/default-editorial-pixel.json +3 -0
  87. package/skills/frontend-ui-ux/references/house-styles.md +236 -0
  88. package/skills/frontend-ui-ux/references/motion-guide.md +2 -2
  89. package/skills/frontend-ui-ux/references/production.md +3 -1
  90. package/skills/frontend-ui-ux/references/slop-register.md +4 -4
  91. package/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
  92. package/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
  93. package/skills/frontend-ui-ux/scripts/probe-lock.mjs +235 -0
  94. package/skills/frontend-ui-ux/scripts/probe-page.mjs +34 -12
  95. package/skills/frontend-ui-ux/scripts/probe-static.mjs +16 -3
  96. package/skills/frontend-ui-ux/scripts/probe-thresholds.json +8 -2
  97. package/skills/frontend-ui-ux/scripts/probe.mjs +35 -7
  98. package/skills/lit-adhd/SKILL.md +209 -0
  99. package/skills/lit-adhd/references/brief-format.md +46 -0
  100. package/skills/lit-adhd/references/card-examples.md +103 -0
  101. package/skills/lit-adhd/references/status-taxonomy.md +34 -0
  102. package/skills/lit-adhd/scripts/rewind.mjs +933 -0
  103. package/skills/lit-diagram-drawer/SKILL.md +4 -3
  104. package/skills/lit-diagram-drawer/references/style-guide.md +20 -11
  105. package/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
  106. package/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
  107. package/skills/lit-diagram-drawer/references/type-tree.md +3 -3
  108. package/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
  109. package/skills/lit-diagram-drawer/references/verifier-guide.md +1 -0
  110. package/skills/lit-diagram-drawer/scripts/craft-thresholds.json +1 -2
  111. package/skills/lit-diagram-drawer/scripts/visual-quality.mjs +1 -2
  112. package/skills/lit-docx/SKILL.md +2 -1
  113. package/skills/lit-handoff/SKILL.md +2 -0
  114. package/skills/lit-hijack/SKILL.md +422 -0
  115. package/skills/lit-hijack/references/capture-and-provenance.md +87 -0
  116. package/skills/lit-hijack/references/clean-room-build.md +111 -0
  117. package/skills/lit-hijack/references/distil-and-licence.md +100 -0
  118. package/skills/lit-hijack/references/gap-and-port-design.md +126 -0
  119. package/skills/lit-hijack/references/parity-and-report.md +110 -0
  120. package/skills/lit-hijack/scripts/hijack.mjs +993 -0
  121. package/skills/lit-plan/SKILL.md +8 -0
  122. package/skills/lit-pptx/SKILL.md +27 -11
  123. package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
  124. package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
  125. package/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
  126. package/skills/lit-pptx/fonts/jetbrains-mono/SOURCE.md +17 -0
  127. package/skills/lit-pptx/references/density-and-fill.md +9 -3
  128. package/skills/lit-pptx/references/direction-step.md +21 -6
  129. package/skills/lit-pptx/references/examples/04-lecture-chalk-ko.md +3 -6
  130. package/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +1 -5
  131. package/skills/lit-pptx/references/examples/08-reliability-keynote-night-en.md +0 -4
  132. package/skills/lit-pptx/references/examples/09-status-update-ledger-en.md +2 -2
  133. package/skills/lit-pptx/references/examples/11-forecast-review-night-ko.md +4 -7
  134. package/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
  135. package/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
  136. package/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
  137. package/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
  138. package/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
  139. package/skills/lit-pptx/references/layout-families.md +65 -47
  140. package/skills/lit-pptx/references/title-treatments.md +83 -134
  141. package/skills/lit-pptx/references/tonalities/atlas.md +3 -3
  142. package/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
  143. package/skills/lit-pptx/references/tonalities/chalk.md +7 -6
  144. package/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
  145. package/skills/lit-pptx/references/tonalities/gazette.md +1 -1
  146. package/skills/lit-pptx/references/tonalities/ledger.md +1 -1
  147. package/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
  148. package/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
  149. package/skills/lit-pptx/references/tonalities/night.md +1 -1
  150. package/skills/lit-pptx/references/tonalities/paper.md +2 -2
  151. package/skills/lit-pptx/references/tonalities/riso.md +137 -0
  152. package/skills/lit-pptx/references/tonalities/signal.md +3 -3
  153. package/skills/lit-pptx/references/tonalities/studio.md +7 -6
  154. package/skills/lit-pptx/run.mjs +2 -1
  155. package/skills/lit-pptx/scripts/compile-deck.js +1 -1
  156. package/skills/lit-pptx/scripts/craft-thresholds.json +4 -8
  157. package/skills/lit-pptx/scripts/craft_extras.py +56 -3
  158. package/skills/lit-pptx/scripts/deck_output.py +75 -90
  159. package/skills/lit-pptx/scripts/embed_fonts.py +13 -1
  160. package/skills/lit-pptx/scripts/layout_inventory.py +12 -5
  161. package/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
  162. package/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
  163. package/skills/lit-pptx/scripts/lib/render-pack.js +373 -566
  164. package/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
  165. package/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
  166. package/skills/lit-pptx/scripts/qa_deck.py +10 -11
  167. package/skills/lit-pptx/scripts/render_pages.py +118 -0
  168. package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
  169. package/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
  170. package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
  171. package/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
  172. package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
  173. package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
  174. package/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
  175. package/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
  176. package/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
  177. package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
  178. package/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
  179. package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
  180. package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
  181. package/skills/lit-recap/SKILL.md +4 -0
  182. package/skills/lit-schematic/SKILL.md +118 -0
  183. package/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
  184. package/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
  185. package/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
  186. package/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
  187. package/skills/lit-schematic/references/cutaway-outputs.md +70 -0
  188. package/skills/lit-schematic/references/isometric-craft.md +101 -0
  189. package/skills/lit-schematic/references/journal-figures.md +72 -0
  190. package/skills/lit-schematic/references/kit-api.md +134 -0
  191. package/skills/lit-schematic/references/pfd-and-layout.md +53 -0
  192. package/skills/lit-schematic/references/pid-drafting.md +103 -0
  193. package/skills/lit-schematic/references/pid-symbols.md +163 -0
  194. package/skills/lit-schematic/references/verification.md +100 -0
  195. package/skills/lit-schematic/scripts/doctor.mjs +57 -0
  196. package/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
  197. package/skills/lit-schematic/scripts/lib/fonts.mjs +25 -0
  198. package/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
  199. package/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
  200. package/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
  201. package/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
  202. package/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
  203. package/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
  204. package/skills/lit-schematic/scripts/lib/png.mjs +132 -0
  205. package/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
  206. package/skills/lit-schematic/scripts/lib/route.mjs +218 -0
  207. package/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
  208. package/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
  209. package/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
  210. package/skills/lit-schematic/scripts/motion.mjs +129 -0
  211. package/skills/lit-schematic/scripts/qa.mjs +92 -0
  212. package/skills/lit-schematic/scripts/render.mjs +233 -0
  213. package/skills/lit-schematic/scripts/scaffold.mjs +60 -0
  214. package/skills/lit-scientific-visualization/SKILL.md +27 -4
  215. package/skills/lit-typographic-motion/SKILL.md +19 -17
  216. package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +9 -0
  217. package/skills/lit-typographic-motion/engine/accents.mjs +6 -0
  218. package/skills/lit-typographic-motion/engine/assets.mjs +282 -0
  219. package/skills/lit-typographic-motion/engine/blender/runner.py +228 -0
  220. package/skills/lit-typographic-motion/engine/blender.mjs +126 -0
  221. package/skills/lit-typographic-motion/engine/board.mjs +138 -0
  222. package/skills/lit-typographic-motion/engine/cover/assets.mjs +11 -8
  223. package/skills/lit-typographic-motion/engine/cover/capture.mjs +6 -6
  224. package/skills/lit-typographic-motion/engine/cover/render.mjs +20 -7
  225. package/skills/lit-typographic-motion/engine/cover/treatment.mjs +30 -3
  226. package/skills/lit-typographic-motion/engine/fill.mjs +54 -0
  227. package/skills/lit-typographic-motion/engine/gallery.mjs +118 -0
  228. package/skills/lit-typographic-motion/engine/house.mjs +42 -0
  229. package/skills/lit-typographic-motion/engine/look.mjs +65 -8
  230. package/skills/lit-typographic-motion/engine/loudness.mjs +101 -0
  231. package/skills/lit-typographic-motion/engine/scaffold.mjs +219 -0
  232. package/skills/lit-typographic-motion/engine/score.mjs +594 -0
  233. package/skills/lit-typographic-motion/engine/sound.mjs +9 -57
  234. package/skills/lit-typographic-motion/engine/stage/capture.mjs +23 -9
  235. package/skills/lit-typographic-motion/engine/stage/clock.js +8 -2
  236. package/skills/lit-typographic-motion/engine/stage/cover-init.js +12 -1
  237. package/skills/lit-typographic-motion/engine/stage/frame-worker.mjs +15 -7
  238. package/skills/lit-typographic-motion/engine/stage/gate.mjs +48 -4
  239. package/skills/lit-typographic-motion/engine/stage/qa-page.js +16 -5
  240. package/skills/lit-typographic-motion/engine/stage/qa.mjs +148 -7
  241. package/skills/lit-typographic-motion/engine/stage/render.mjs +69 -7
  242. package/skills/lit-typographic-motion/engine/stage/scan.mjs +20 -1
  243. package/skills/lit-typographic-motion/engine/stage/stage-kit.js +85 -4
  244. package/skills/lit-typographic-motion/engine/stills.mjs +1 -1
  245. package/skills/lit-typographic-motion/engine/styles.mjs +221 -0
  246. package/skills/lit-typographic-motion/engine/text.mjs +1 -1
  247. package/skills/lit-typographic-motion/engine/treatment.mjs +190 -6
  248. package/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
  249. package/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
  250. package/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
  251. package/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
  252. package/skills/lit-typographic-motion/gate.mjs +18 -2
  253. package/skills/lit-typographic-motion/probe.mjs +13 -1
  254. package/skills/lit-typographic-motion/recipes/hero-3d/expected.json +64 -0
  255. package/skills/lit-typographic-motion/recipes/hero-3d/index.html +132 -0
  256. package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +2 -0
  257. package/skills/lit-typographic-motion/recipes/painted-light/expected.json +2 -0
  258. package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +2 -0
  259. package/skills/lit-typographic-motion/references/cover.md +56 -13
  260. package/skills/lit-typographic-motion/references/craft-loop.md +20 -7
  261. package/skills/lit-typographic-motion/references/depth.md +112 -0
  262. package/skills/lit-typographic-motion/references/runtime.md +21 -6
  263. package/skills/lit-typographic-motion/references/sound.md +76 -0
  264. package/skills/lit-typographic-motion/references/stage.md +50 -7
  265. package/skills/lit-typographic-motion/references/studio.md +162 -0
  266. package/skills/lit-typographic-motion/references/style-bibles.md +18 -0
  267. package/skills/lit-typographic-motion/references/treatment.md +108 -9
  268. package/skills/lit-typographic-motion/render.mjs +60 -3
  269. package/skills/lit-typographic-motion/styles/paper.json +185 -0
  270. package/skills/lit-typographic-motion/styles/swiss-signal.json +188 -0
  271. package/skills/lit-typographic-motion/styles/terminalcore.json +182 -0
  272. package/skills/lit-typographic-motion/styles/tidal.json +187 -0
  273. package/skills/litresearch/SKILL.md +2 -0
  274. package/skills/managed-skill-manifest.json +517 -88
  275. package/skills/readme-studio/SKILL.md +1 -1
  276. package/skills/review-work/SKILL.md +2 -0
  277. package/skills/start-work/SKILL.md +8 -0
  278. package/skills/workflow-loop/SKILL.md +4 -0
  279. package/tools/check-pack-payload.mjs +5 -2
  280. package/tools/payload-substance-parity.json +4 -3
  281. package/tools/qa-cover-recipes.mjs +4 -3
  282. package/tools/version-manifests.json +2 -2
@@ -0,0 +1,133 @@
1
+ // The heat exchanger as a scene: one function of the camera, the explode amount and the flow
2
+ // phase, so the static figure, the interactive page and the motion loop share one geometry.
3
+ //
4
+ // Parts list (written before any geometry): base pad, two saddles with base plates; front
5
+ // channel with ellipsoidal bonnet, channel and shell flanges with bolt circles, shell with a
6
+ // window cutaway, rear bonnet and flange; inside: twelve tubes split at every baffle, four
7
+ // baffles, two tubesheets; nozzles: shell-side and tube-side inlets with flanges and bolts.
8
+ //
9
+ // Assemblies (groups) and how they come apart for maintenance: the front head (channel, its
10
+ // flange and the tube-side nozzle) comes off first, then the tube bundle is pulled out of the
11
+ // front, the rear head comes off the back; shell and supports stay.
12
+
13
+ import { createFigure } from "../../scripts/lib/figure.mjs";
14
+ import { createIsoView, ellipsoidalHead, frameAlong } from "../../scripts/lib/iso.mjs";
15
+
16
+ export const MOVES = Object.freeze({ front: [-230, 0, 0], bundle: [-150, 0, 0], rear: [110, 0, 0] });
17
+ export const ORDER = Object.freeze(["front", "bundle", "rear"]);
18
+ export const LAYERS = Object.freeze([
19
+ { group: "shell", label: "Shell" },
20
+ { group: "bundle", label: "Tube bundle" },
21
+ { group: "front", label: "Front head" },
22
+ { group: "rear", label: "Rear head" },
23
+ ]);
24
+ export const VIEWS = Object.freeze([
25
+ { id: "front-left", label: "Front left", azimuth: 62, elevation: 28 },
26
+ { id: "front-high", label: "From above", azimuth: 50, elevation: 48 },
27
+ { id: "rear-right", label: "Rear right", azimuth: 118, elevation: 26 },
28
+ ]);
29
+
30
+ const Z = 120;
31
+ const R = 60;
32
+ const SHELL = [8, 330];
33
+ const BAFFLES = [80, 140, 200, 260];
34
+ const TUBE_ROWS = [[-30, 18], [-10, 18], [10, 18], [30, 18], [-20, 0], [0, 0], [20, 0], [-30, -18], [-10, -18], [10, -18], [30, -18], [0, 36]];
35
+
36
+ // explode: 0 assembled .. 1 apart, per group or one number for all. flow: dash phase in mm
37
+ // along the tubes, or null for no flow. reach: keep the exploded positions in frame.
38
+ export function scene({ azimuth = 62, elevation = 28, explode = 0, flow = null, callouts = true, reach = false, width = 160, height = 92 } = {}) {
39
+ const share = (g) => (typeof explode === "number" ? explode : explode[g] ?? 0);
40
+ const shift = (g, p) => { const m = MOVES[g] ?? [0, 0, 0]; return [p[0] + m[0] * share(g), p[1] + m[1] * share(g), p[2] + m[2] * share(g)]; };
41
+ const v = createIsoView({ azimuth, elevation });
42
+
43
+ v.halo({ plan: { x: -130, y: -95, w: 520, d: 190, r: 6 }, z: -12 });
44
+ v.box({ name: "base pad", plan: { x: -130, y: -95, w: 520, d: 190, r: 6 }, z: -12, h: 12, tone: "lo", layer: -1, group: "support" });
45
+ const saddles = [70, 270].map((sx) => {
46
+ v.box({ name: `saddle base ${sx}`, plan: { x: sx - 12, y: -52, w: 24, d: 104, r: 2 }, z: 0, h: 5, tone: "frame", group: "support" });
47
+ return v.box({ name: `saddle ${sx}`, plan: { x: sx - 4, y: -46, w: 8, d: 92 }, z: 5, h: Z - R - 2, tone: "frame", group: "support" });
48
+ });
49
+ if (reach) {
50
+ const far = (g, x) => [[x + MOVES[g][0], -R - 12, Z - R - 12], [x + MOVES[g][0], R + 12, Z + R + 40]];
51
+ v.extent([...far("front", -110), ...far("rear", 390)]);
52
+ }
53
+
54
+ const axis = "x";
55
+ const disc = (name, s0, s1, r, { tone = "mid", into = null, group }) =>
56
+ v.lathe({ name, origin: shift(group, [0, 0, Z]), axis, profile: [[s0, 0], [s0, r], [s1, r], [s1, 0]], tone, into, group });
57
+
58
+ // Front head: channel with bonnet, channel flange and bolts, tube-side nozzle.
59
+ const bonnet = ellipsoidalHead(62).map(([s, r, sm]) => [-70 - s, r, sm]).reverse();
60
+ v.lathe({ name: "channel", origin: shift("front", [0, 0, Z]), axis, profile: [...bonnet, [-70, 62], [-8, 62], [-8, 0]], tone: "mid", seams: [-70], group: "front" });
61
+ const channelFlange = disc("channel flange", -8, 0, 72, { group: "front" });
62
+ v.boltCircle({ name: "channel flange bolts", frame: frameAlong(shift("front", [0, 0, Z]), axis), s: -4, radius: 67, count: 16, group: "front" });
63
+
64
+ // Shell with its flanges; the cutaway window shows the bundle inside.
65
+ disc("shell flange", 0, 8, 72, { group: "shell" });
66
+ v.boltCircle({ name: "shell flange bolts", frame: frameAlong([0, 0, Z], axis), s: 4, radius: 67, count: 16, group: "shell" });
67
+ const shell = v.lathe({
68
+ name: "shell", origin: [0, 0, Z], axis, profile: [[SHELL[0], 0], [SHELL[0], R], [SHELL[1], R], [SHELL[1], 0]],
69
+ tone: "hi", cut: { from: 40, to: 290, span: 96 }, wall: 4, seams: [120, 220], group: "shell",
70
+ });
71
+
72
+ // Rear head.
73
+ disc("rear flange", 330, 338, 72, { group: "rear" });
74
+ v.lathe({ name: "rear bonnet", origin: shift("rear", [0, 0, Z]), axis, profile: [[338, 0], [338, 62], ...ellipsoidalHead(62).map(([s, r, sm]) => [338 + s, r, sm]).slice(1)], tone: "mid", group: "rear" });
75
+ v.boltCircle({ name: "rear flange bolts", frame: frameAlong(shift("rear", [0, 0, Z]), axis), s: 334, radius: 67, count: 16, group: "rear" });
76
+
77
+ // Tube bundle, drawn inside the shell's cutaway: tubesheets, baffles, tubes split at each baffle.
78
+ disc("front tubesheet", 8, 16, R - 4, { tone: "brass", into: shell, group: "bundle" });
79
+ disc("rear tubesheet", 322, 330, R - 4, { tone: "brass", into: shell, group: "bundle" });
80
+ const baffles = BAFFLES.map((s, i) => v.lathe({
81
+ name: `baffle ${i + 1}`, origin: shift("bundle", [0, 0, Z]), axis,
82
+ profile: [[s, 0], [s, R - 5], [s + 3, R - 5], [s + 3, 0]], tone: "stainless", into: shell, group: "bundle",
83
+ }));
84
+ const stops = [16, ...BAFFLES.flatMap((s) => [s, s + 3]), 322];
85
+ const tubes = [];
86
+ TUBE_ROWS.forEach(([y, z], t) => {
87
+ for (let k = 0; k < stops.length; k += 2) {
88
+ const origin = shift("bundle", [0, y, Z + z]);
89
+ tubes.push(v.lathe({ name: `tube ${t + 1}.${k / 2 + 1}`, origin, axis, profile: [[stops[k], 0], [stops[k], 3.5], [stops[k + 1], 3.5], [stops[k + 1], 0]], tone: "copper", steps: 32, into: shell, group: "bundle" }));
90
+ if (flow !== null) {
91
+ // Tube-side fluid: dashes on the tube's crown line, marching from the channel end.
92
+ const a = [origin[0] + stops[k], origin[1], origin[2] + 3.6];
93
+ const b = [origin[0] + stops[k + 1], origin[1], origin[2] + 3.6];
94
+ v.polyline({ name: `tube flow ${t + 1}.${k / 2 + 1}`, points: [a, b], color: "#0b5e8e", width: 0.5, bias: 1.2, into: shell, group: "bundle", flow: { dash: [9, 9], start: stops[k], offset: flow } });
95
+ }
96
+ }
97
+ });
98
+
99
+ // Nozzles with flanges: shell side on the shell, tube side on the channel.
100
+ const nozzle = (name, s, group) => {
101
+ const at = shift(group, [s, 0, Z]);
102
+ const body = v.lathe({ name, origin: at, axis: "z", profile: [[R - 6, 0], [R - 6, 13], [R + 26, 13], [R + 26, 0]], tone: "mid", group });
103
+ v.lathe({ name: `${name} flange`, origin: at, axis: "z", profile: [[R + 26, 0], [R + 26, 21], [R + 32, 21], [R + 32, 0]], tone: "mid", group });
104
+ v.boltCircle({ name: `${name} bolts`, frame: frameAlong([at[0], at[1], at[2] + R + 32], "z"), s: 0, radius: 17, count: 8, group });
105
+ return body;
106
+ };
107
+ const shellInlet = nozzle("shell inlet", 312, "shell");
108
+ const tubeInlet = nozzle("tube inlet", -40, "front");
109
+
110
+ if (callouts) {
111
+ v.callout({ part: shell, at: shell.anchor(0.93, -25), text: "Shell" });
112
+ v.callout({ part: baffles[2], at: baffles[2].anchor(0.5, -72), text: "Segmental baffle" });
113
+ v.callout({ part: tubeInlet, text: "Tube-side inlet" });
114
+ v.callout({ part: shellInlet, text: "Shell-side inlet" });
115
+ v.callout({ part: channelFlange, at: channelFlange.anchor(0.5, -40), text: "Channel flange" });
116
+ v.callout({ part: saddles[1], at: saddles[1].anchor(), text: "Saddle support" });
117
+ v.callout({ part: tubes[stops.length / 2 * 1 + 2], text: "Tube bundle" });
118
+ }
119
+
120
+ const fig = createFigure({ width, height, mode: "iso", title: "Shell-and-tube heat exchanger, window cutaway", desc: "Isometric cutaway of a shell-and-tube heat exchanger showing tubes, baffles, tubesheets, nozzles and saddles." });
121
+ fig.panel(null, [0, 0, width, height]).iso(v, { labelColumn: callouts ? 26 : null });
122
+ const svg = fig.render();
123
+ return { svg, view: v, warnings: fig.warnings() };
124
+ }
125
+
126
+ // One loop for the motion export: the bundle and heads come together, tube-side fluid runs,
127
+ // then they come apart again, so the last frame leads straight back into the first.
128
+ const smooth = (t) => t * t * (3 - 2 * t);
129
+ export function frame(t) {
130
+ const together = t < 0.3 ? 1 - smooth(t / 0.3) : t < 0.7 ? 0 : smooth((t - 0.7) / 0.3);
131
+ const explode = Object.fromEntries(ORDER.map((g, i) => [g, Math.min(1, Math.max(0, together * 1.25 - i * 0.12))]));
132
+ return scene({ explode, flow: t * 4 * 18, callouts: false, reach: true }).svg;
133
+ }
@@ -0,0 +1,87 @@
1
+ // Worked example: a two-panel journal figure (double column, 178 mm) of a breakthrough
2
+ // adsorption apparatus. (a) the flow schematic with stream colours and a key; (b) an
3
+ // isometric window cutaway of the packed column.
4
+ // node build.mjs [out-dir] -> journal-adsorption-rig.svg
5
+
6
+ import { mkdirSync, writeFileSync } from "node:fs";
7
+ import { join, resolve } from "node:path";
8
+ import { createFigure } from "../../scripts/lib/figure.mjs";
9
+ import { createIsoView } from "../../scripts/lib/iso.mjs";
10
+ import * as S from "../../scripts/lib/pid.mjs";
11
+
12
+ const out = resolve(process.argv[2] ?? ".");
13
+ const fig = createFigure({
14
+ width: "double",
15
+ height: 84,
16
+ title: "Breakthrough adsorption apparatus",
17
+ desc: "(a) Flow schematic: feed gases through mass flow controllers to a packed column in an oven, back-pressure regulator and mass spectrometer. (b) Cutaway of the packed column.",
18
+ });
19
+
20
+ // ---------------------------------------------------------------- (a) flow schematic
21
+ const a = fig.panel("a", [0, 0, 106, 84]);
22
+ const c = a.canvas({ box: [0, 7, 106, 74] });
23
+ const co2 = c.add(S.gasCylinder({ x: 7, y: 50, w: 7, h: 22, name: "CO2/N2" }));
24
+ const he = c.add(S.gasCylinder({ x: 18, y: 50, w: 7, h: 22, name: "He" }));
25
+ const mfc1 = c.add(S.mfc({ x: 33, y: 15 }));
26
+ const mfc2 = c.add(S.mfc({ x: 33, y: 29 }));
27
+ const column = c.add(S.vessel({ x: 60, y: 40, w: 9, h: 34, packing: true, name: "Column" }));
28
+ const ms = c.add(S.unit({ x: 96, y: 30, w: 14, h: 10, caption: "MS", ports: { in: ["left", 0.5], vent: ["top", 0.65] } }));
29
+ const vent = c.add(S.vent({ x: 98.1, y: 17, text: "Vent" }));
30
+ c.zone([43, 17, 76, 64], "Oven");
31
+
32
+ c.stream({ from: co2.port("outlet"), to: mfc1.port("a"), role: "gas" });
33
+ c.stream({ from: he.port("outlet"), to: mfc2.port("a"), role: "gas" });
34
+ const feed = c.stream({ from: mfc1.port("b"), to: column.port("top"), role: "gas" });
35
+ c.stream({ from: mfc2.port("b"), to: feed.tap([40, 15]), role: "gas" });
36
+ const effluent = c.stream({ from: column.port("bottom"), to: ms.port("in"), role: "sample", via: [[60, 68], [80, 68], [80, 30]] });
37
+ c.inline(effluent, (o) => S.regulator({ ...o, backPressure: true }), { at: [80, 42] });
38
+ c.stream({ from: ms.port("vent"), to: vent.port("in"), role: "vent" });
39
+ const pressureTap = effluent.tap([80, 54]);
40
+ const pt = c.add(S.instrument({ x: 90, y: 54, tag: "PT-1" }));
41
+ c.connect({ from: pressureTap.dir[0] < 0 ? pressureTap.flip() : pressureTap, to: pt.port("left") });
42
+
43
+ fig.key(["gas", "sample", "vent"], [4, 81], { columns: 3, colWidth: 30 });
44
+
45
+ // ---------------------------------------------------------------- (b) column cutaway
46
+ const b = fig.panel("b", [110, 0, 68, 84]);
47
+ const v = createIsoView({ azimuth: 40, elevation: 24 });
48
+ const L = 160;
49
+ const R = 14;
50
+ v.halo({ plan: { x: -36, y: -36, w: 72, d: 72, r: 6 }, z: -30 });
51
+ v.box({ name: "stand base", plan: { x: -36, y: -36, w: 72, d: 72, r: 4 }, z: -30, h: 6, tone: "cast", layer: -1 });
52
+ v.lathe({ name: "stand post", origin: [-26, -26, -24], axis: "z", profile: [[0, 0], [0, 3], [L + 26, 3], [L + 26, 0]], tone: "stainless" });
53
+ v.box({ name: "clamp", plan: { x: -28, y: -14, w: 8, d: 6, r: 1 }, z: L * 0.55, h: 10, tone: "cast" });
54
+ const tube = v.lathe({ name: "column", origin: [0, 0, 0], axis: "z", profile: [[0, 0], [0, R], [L, R], [L, 0]], tone: "glass", cut: { from: 22, to: L - 22, span: 84 }, wall: 2.2 });
55
+ const bed = v.lathe({ name: "adsorbent bed", origin: [0, 0, 0], axis: "z", profile: [[36, 0], [36, R - 2.4], [L - 36, R - 2.4], [L - 36, 0]], tone: "warm", into: tube });
56
+ const woolLow = v.lathe({ name: "glass wool plug", origin: [0, 0, 0], axis: "z", profile: [[24, 0], [24, R - 2.4], [36, R - 2.4], [36, 0]], tone: "cool", into: tube });
57
+ v.lathe({ name: "glass wool plug top", origin: [0, 0, 0], axis: "z", profile: [[L - 36, 0], [L - 36, R - 2.4], [L - 24, R - 2.4], [L - 24, 0]], tone: "cool", into: tube });
58
+ // Pellet texture on the bed's lit face: rows that stop short of the limbs and the plugs.
59
+ const pellets = [];
60
+ for (let i = 0; i < 260; i++) {
61
+ const h = Math.sin(i * 12.9898) * 43758.5453;
62
+ const g = Math.sin(i * 78.233) * 12345.6789;
63
+ const s = 40 + (h - Math.floor(h)) * (L - 80);
64
+ const t = tube.facingAngle + ((g - Math.floor(g)) - 0.5) * 1.5;
65
+ pellets.push(bed.point(s, R - 2.4, t));
66
+ }
67
+ v.dots({ name: "pellets", points: pellets, r: 0.26, color: "#b08a6a", into: tube });
68
+ for (const [z, name] of [[0, "bottom fitting"], [L, "top fitting"]]) {
69
+ v.lathe({ name, origin: [0, 0, z], axis: z ? "z" : "-z", profile: [[0, 0], [0, R + 3], [8, R + 3], [8, 0]], tone: "brass" });
70
+ }
71
+ const inlet = v.pipe({ name: "gas inlet", points: [[0, 0, -8], [0, 0, -18], [34, 0, -18]], r: 2.2, tone: "stainless" });
72
+ const outlet = v.pipe({ name: "effluent line", points: [[0, 0, L + 8], [0, 0, L + 18], [34, 0, L + 18]], r: 2.2, tone: "stainless" });
73
+ const wells = [0.3, 0.5, 0.7].map((f, i) => v.pipe({ name: `thermocouple ${i + 1}`, points: [[R + 0.2, 0, L * f], [R + 24, 0, L * f]], r: 1, tone: "stainless", caps: [false, true] }));
74
+
75
+ v.callout({ part: tube, at: tube.anchor(0.92, -35), text: "Column, 26 mm ID" });
76
+ v.callout({ part: bed, text: "Adsorbent bed" });
77
+ v.callout({ part: woolLow, text: "Glass wool" });
78
+ v.callout({ part: wells[1], at: wells[1].anchor(0.8), text: "Thermocouples" });
79
+ v.callout({ part: inlet, at: inlet.anchor(0.85), text: "Feed in" });
80
+ v.callout({ part: outlet, at: outlet.anchor(0.85), text: "To MS" });
81
+ b.iso(v);
82
+
83
+ mkdirSync(out, { recursive: true });
84
+ const file = join(out, "journal-adsorption-rig.svg");
85
+ writeFileSync(file, fig.render());
86
+ for (const warning of fig.warnings()) console.warn(`WARN ${warning}`);
87
+ console.log(file);
@@ -0,0 +1,85 @@
1
+ // Worked example: P&ID of a chemical dosing skid on an A3 sheet.
2
+ // node build.mjs [out-dir] -> pid-dosing-skid.svg (then run scripts/qa.mjs and scripts/render.mjs)
3
+
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
6
+ import * as S from "../../scripts/lib/pid.mjs";
7
+ import { createSheet } from "../../scripts/lib/sheet.mjs";
8
+
9
+ const out = resolve(process.argv[2] ?? ".");
10
+
11
+ const sheet = createSheet({
12
+ size: "A3",
13
+ title: "Sodium hypochlorite dosing skid",
14
+ subtitle: "Piping and instrumentation diagram",
15
+ number: "EX-PID-001",
16
+ rev: "A",
17
+ date: "2026-10-25",
18
+ project: "Worked example",
19
+ owner: "lit-schematic",
20
+ drawn: "LS",
21
+ notes: [
22
+ "Line numbers read size - fluid code - sequence - piping class.",
23
+ "Pumps P-101A/B are duty and standby; one runs at a time.",
24
+ "PSV-101 relieves pump discharge back to TK-101 above the liquid level.",
25
+ ],
26
+ });
27
+
28
+ // Equipment: storage tank on the left, duty/standby metering pumps stacked to its right.
29
+ const tank = sheet.add(S.tank({ x: 92, y: 150, w: 40, h: 50, tag: "TK-101", name: "Hypochlorite tank" }));
30
+ const pumpA = sheet.add(S.pump({ x: 190, y: 125, type: "metering", tag: "P-101A", name: "Dosing pump" }));
31
+ const pumpB = sheet.add(S.pump({ x: 190, y: 185, type: "metering", tag: "P-101B", name: "Dosing pump" }));
32
+ sheet.equipmentInfo(tank, ["2.5 m3, HDPE", "Atmospheric"]);
33
+ sheet.equipmentInfo(pumpA, ["0-40 L/h at 6 barg", "Duty"]);
34
+ sheet.equipmentInfo(pumpB, ["0-40 L/h at 6 barg", "Standby"]);
35
+
36
+ // Fill, vent and drain on the tank.
37
+ const fill = sheet.add(S.offPage({ x: 30, y: 72, text: "FROM UNLOADING" }));
38
+ const fillLine = sheet.pipe({ from: fill.port("out"), to: tank.port("top-left"), kind: "process-minor", number: '1"-NAOCL-101-P1' });
39
+ sheet.inline(fillLine, S.valve, { type: "ball", at: 0.6 });
40
+ const vent = sheet.add(S.vent({ x: 92, y: 100 }));
41
+ sheet.pipe({ from: tank.port("top"), to: vent.port("in"), kind: "process-minor", arrows: false });
42
+ const drain = sheet.add(S.drain({ x: 82, y: 222 }));
43
+ const drainLine = sheet.pipe({ from: tank.port("bottom", 0.3), to: drain.port("in"), kind: "process-minor", arrows: false });
44
+ sheet.inline(drainLine, S.valve, { type: "gate" });
45
+
46
+ // Suction: one main from the tank up a vertical manifold to P-101A, a branch down to P-101B.
47
+ const suction = sheet.pipe({ from: tank.port("right", 0.9), to: pumpA.port("suction"), via: [[150, 171], [150, 125]], number: '1"-NAOCL-102-P1' });
48
+ sheet.inline(suction, S.valve, { type: "ball", at: 0.12 });
49
+ sheet.inline(suction, S.filter, { at: 0.9 });
50
+ const branchB = sheet.pipe({ from: suction.tap([138, 171]), to: pumpB.port("suction"), kind: "process-minor" });
51
+ sheet.inline(branchB, S.filter, { at: 0.7 });
52
+
53
+ // Discharge: check valve on each pump into a common header that rises to the injection point.
54
+ const injection = sheet.add(S.offPage({ x: 300, y: 95, text: "TO INJECTION POINT" }));
55
+ const header = sheet.pipe({ from: pumpB.port("discharge"), to: injection.port("in"), via: [[226, 180], [226, 95]], number: '1/2"-NAOCL-103-P2' });
56
+ sheet.inline(header, S.valve, { type: "check", at: 0.06 });
57
+ const flowEl = sheet.inline(header, S.orifice, { at: [286, 95], tag: "FE-101" });
58
+ const dischargeA = sheet.pipe({ from: pumpA.port("discharge"), to: header.tap([226, 120]).flip(), kind: "process-minor" });
59
+ sheet.inline(dischargeA, S.valve, { type: "check" });
60
+
61
+ // Relief: PSV above the header, discharge back into the tank side above the liquid.
62
+ const psv = sheet.add(S.safetyValve({ x: 246, y: 76, outlet: "left", tag: "PSV-101", setPressure: "SET 8 barg" }));
63
+ sheet.pipe({ from: header.tap([246, 95]).flip(), to: psv.port("inlet"), kind: "process-minor", arrows: false });
64
+ sheet.pipe({ from: psv.port("outlet"), to: tank.port("right", 0.1), kind: "process-minor" });
65
+
66
+ // Instruments: tank level to the DCS, local discharge pressure, flow loop on the pump stroke.
67
+ const levelNozzle = tank.port("left", 0.8);
68
+ const lt = sheet.add(S.instrument({ x: 44, y: levelNozzle.y, tag: "LT-101" }));
69
+ sheet.connect({ from: levelNozzle, to: lt.port("right") });
70
+ const lia = sheet.add(S.instrument({ x: 44, y: 140, tag: "LIA-101", location: "dcs" }));
71
+ sheet.signal({ from: lt.port("top"), to: lia.port("bottom") });
72
+ const pi = sheet.add(S.instrument({ x: 266, y: 116, tag: "PI-102" }));
73
+ sheet.connect({ from: header.tap([266, 95]), to: pi.port("top") });
74
+ const ftX = flowEl.port("b").x + 4;
75
+ const ft = sheet.add(S.instrument({ x: ftX, y: 116, tag: "FT-101" }));
76
+ sheet.connect({ from: header.tap([ftX, 95]), to: ft.port("top") });
77
+ const fic = sheet.add(S.instrument({ x: ftX, y: 150, tag: "FIC-101", location: "dcs" }));
78
+ sheet.signal({ from: ft.port("bottom"), to: fic.port("top") });
79
+ sheet.signal({ from: fic.port("bottom"), to: pumpB.port("signal") });
80
+
81
+ mkdirSync(out, { recursive: true });
82
+ const file = join(out, "pid-dosing-skid.svg");
83
+ writeFileSync(file, sheet.render({ desc: "A3 P&ID of a hypochlorite dosing skid: tank, duty and standby metering pumps, relief and flow control." }));
84
+ for (const warning of sheet.warnings) console.warn(`WARN ${warning}`);
85
+ console.log(file);
@@ -0,0 +1,70 @@
1
+ # Cutaway outputs: static, interactive page, motion loop
2
+
3
+ An isometric cutaway can leave the skill in three forms. The **static** figure (SVG with PDF and 600 dpi PNG) is the default and the only form a paper, a report or a P&ID set takes. An **interactive page** (one HTML file) lets a reader take the machine apart, switch views and find a part by its name. A **motion loop** (MP4 with an animated WebP and GIF) shows something happening: an assembly coming together, coolant running through a coil, an impeller turning. All three come from one scene function, so they always show the same machine.
4
+
5
+ ## Contents
6
+ 1. Choosing the output
7
+ 2. One scene, three outputs
8
+ 3. The interactive page
9
+ 4. Designing the explode
10
+ 5. The motion loop
11
+ 6. Checking each output
12
+ 7. When a film needs more than a loop
13
+
14
+ ## 1. Choosing the output
15
+
16
+ | The request says | Output |
17
+ | --- | --- |
18
+ | nothing about the medium, or "figure", "그림", "논문", "보고서", "slide", "PNG", "PDF" | static (default) |
19
+ | "HTML", "interactive", "web page", "웹", "인터랙티브", "explorable", "hover", "분해해서 볼 수 있게" | interactive page, plus the static figure |
20
+ | "animation", "motion", "video", "MP4", "GIF", "loop", "애니메이션", "모션", "영상", "동영상", "움직이는" | motion loop, plus the static figure |
21
+ | both kinds of words | both, plus the static figure |
22
+
23
+ A bare request keeps the static default and asks nothing. Ask **one short question** only when the user is present (an interactive session, not a headless run) and the request names a use that a moving or explorable output would serve without naming the form, for example "for our lab website" or "for the open-day screen": "A static figure, an interactive web page, or a short looping video?" Take the answer and do not ask again. In a headless run, choose from the words alone and say in the reply which form you made and that the others are available.
24
+
25
+ The static figure is always delivered with the others: it is the reference the page and the loop are checked against, and the form a journal or a print can use.
26
+
27
+ ## 2. One scene, three outputs
28
+
29
+ Write the cutaway as `scene({ azimuth, elevation, explode, flow, callouts, reach })` returning `{ svg, view, warnings }`, following `examples/iso-heat-exchanger/scene.mjs`:
30
+
31
+ - **Groups.** Give every part the assembly it belongs to with `group`: the shell and its flanges, the internals, the drive, the front and rear heads, the supports. A group is what a reader can hide or pull out; parts that bolt together and move together share one.
32
+ - **Moves.** Keep the world move of each group that comes apart in one table (`MOVES`), the order they leave in (`ORDER`, outside parts first), and the layers a reader can hide (`LAYERS`). The scene shifts each group's origins by `MOVES[g] × explode[g]`, so a film frame is real geometry with a correct painter's order.
33
+ - **Extents.** With `reach: true` the scene calls `view.extent(points)` with where the moved groups end up, so every frame of a film and every state of a page share one scale and nothing leaves the frame.
34
+ - **Flow.** A `flow` phase in mm drives dashed stripes along pipes (`pipe({ flow })`) or along tube crowns (`polyline({ flow })`); give collinear pieces of one run the same offset and their own start.
35
+ - **Callouts** stay on for the figure and the page and go off for the film, where moving leaders would distract.
36
+
37
+ `build.mjs` writes the static figure from `scene()` and, with `--html`, the page from one `scene({ reach: true })` per preset view.
38
+
39
+ ## 3. The interactive page
40
+
41
+ `renderInteractive` turns those views into one HTML file with no external file, font or script, so it opens offline, from a USB stick or from an email. It gives the reader:
42
+
43
+ - **Preset views** as buttons (keys `1` to `n`): two or three useful camera positions, such as the default front-left, a higher view into the cut, and the opposite side. Draw each with its own callouts; anchors that use `side` offsets relative to the facing angle stay on visible faces when the camera turns.
44
+ - **Exploded view** (button or `E`): each group slides by its screen vector, one after another in `ORDER`, and comes home in reverse; the leader dots ride with their parts. At zero the transform is removed, so the assembled page is exactly the static art.
45
+ - **Layers** as checkboxes: hiding the shell leaves the internals drawn inside it.
46
+ - **Callouts as controls**: each label is a focusable button; hover or focus lights its part's outline and its leader, `Enter` or `Space` toggles the selection, `Esc` clears it. Hovering a part lights its callout.
47
+ - **Access**: visible focus rings, `aria-pressed` on toggles, an `aria-live` line that says what changed ("Exploded view", "Hiding Shell"), and `prefers-reduced-motion` honoured by moving parts straight to their end positions.
48
+ - **State in the URL**: `#view=front-high&explode=1&hide=shell&select=C3` opens the page in that state, which is how screenshots for a report or a check are taken.
49
+
50
+ ## 4. Designing the explode
51
+
52
+ Take the machine apart the way a fitter would. Outside parts leave first and come back last: a head comes off before the bundle slides out; a drive comes off before the cover lifts; a cover lifts before the internals rise. Move each group along a real path: along the axis it is bolted on, straight up for a lid, outward along a radius for side parts. Keep the moves short enough that the exploded machine still fits the page at a readable scale, usually less than the part's own length. Parts that join two assemblies belong to the one they stay with. A part nested inside another (a bundle inside its shell) can move on its own; the page and the film both treat it as its own group.
53
+
54
+ On the page the explode is a 2D slide of the static art, so painter's order is that of the assembled view: check the exploded state for a part that should now be in front and is drawn behind. The film rebuilds every frame from the scene, so its order is always right.
55
+
56
+ ## 5. The motion loop
57
+
58
+ `node "$SKILL_ROOT"/scripts/motion.mjs scene.mjs --out <dir> --name <name>` draws `frame(t)` for 4 s at 24 fps by default (`--seconds`, `--fps`, `--width 1280`, `--small 720`) and writes `name.mp4` (H.264, yuv420p, faststart, plays in slides and browsers), `name.webp` and `name.gif` (720 px wide, for a README or a chat). Design the loop so its last frame leads into its first: start in the state you end in (both exploded, or both assembled), ease every move (smoothstep), and make a flow phase advance a whole number of dash periods over the loop. Keep it calm: one thing happening at a time, nothing faster than a reader can follow, no flashes. A 4–8 s loop of about 100–200 frames renders in one to three minutes; run it under `nice`.
59
+
60
+ The encoder needs ffmpeg (and img2webp for WebP). Nothing is installed: a missing tool stops the run with `ENCODER_REQUIRED` and the install line for the user; report the loop as `BLOCKED` for that format and deliver the others.
61
+
62
+ ## 6. Checking each output
63
+
64
+ - **Static**: as for every drawing: `qa.mjs`, render, look (`references/verification.md`).
65
+ - **Page**: open it in a real browser (headless Chrome with the mock keychain flags, or a browser-automation session) and exercise it: focus the first callout and Tab to the next (the part outline lights), toggle the explode (groups move, then return to exactly the static art), hide a layer (nested groups stay), switch each view, and open it once with reduced motion forced (moves are immediate). Screenshot the states through the URL hash and read them. Confirm the page has no `http` reference other than the SVG namespace and no console errors.
66
+ - **Loop**: tile a dozen frames from the MP4 (`ffmpeg … -vf "select='not(mod(n\,12))',scale=640:-1,tile=4x3"`) and read the sheet: the start and end states match, parts move along their paths without passing through each other, nothing leaves the frame, flow stripes stay on their pipes. Check the MP4 stream is H.264 yuv420p at the frame count you asked for.
67
+
68
+ ## 7. When a film needs more than a loop
69
+
70
+ A silent loop of the machine is this skill's job. A film with a treatment, titles, narration beats or a sound bed (an explainer for a launch, a conference opener) belongs to `lit-typographic-motion`: hand it the interactive page or the scene as the stage it captures, and let it own the treatment, timing, sound and its own QA. Keep the machine's geometry here; never redraw the equipment in the film skill.
@@ -0,0 +1,101 @@
1
+ # Isometric and cutaway craft
2
+
3
+ An isometric illustration explains equipment by showing it as the real, well-made object: a reader recognises a shell-and-tube exchanger, a stirred autoclave or a packed column at a glance and then finds the part the text is about. The rules below are what separates such a figure from a 3D-looking diagram; each one comes from a fault that makes a figure read as fake.
4
+
5
+ ## Contents
6
+ 1. What a good figure has
7
+ 2. Size it, then write the parts list
8
+ 3. Camera and composition
9
+ 4. Solids, tones and light
10
+ 5. Painter's order
11
+ 6. Window cutaways
12
+ 7. Turned parts, flanges, nozzles and pipes
13
+ 8. Detail that belongs to parts
14
+ 9. Callouts
15
+ 10. Groups for pages and films
16
+ 11. Anti-patterns
17
+
18
+ ## 1. What a good figure has
19
+
20
+ 1. **A real object.** Model the actual equipment from its real proportions (shell diameter to length, nozzle sizes, flange thickness). Take mood and finish from references; never trace one.
21
+ 2. **Many small, precise parts.** A single-machine figure has 40–80 parts; a dense one 80–150. Density comes from structure (saddles, flanges, bolts, nozzles, brackets, baffles), never from texture.
22
+ 3. **Volume.** Every part has thickness, shaded faces and a lighter top; a soft halo sits under the base.
23
+ 4. **Material colour.** Thin outlines (0.2 mm) over the full-colour material palette, the default of `createIsoView`: every part wears the finish it really has (painted steel, stainless, copper tubes, brass fittings, cast iron, concrete, glass, insulation cladding), so the reader tells parts apart by what they are made of. Signal red (`tone: "accent"`) marks the one part the text is about when its own material would not stand out. The grey palette (`createIsoView({ palette: "grey" })`) is opt-in, for a request that asks for a quiet, monochrome or print-grey figure.
24
+ 5. **Truth.** Dimensions and counts that the caption states are the ones drawn (12 tubes, 4 baffles, 26 mm ID).
25
+
26
+ ## 2. Size it, then write the parts list
27
+
28
+ Name the size before drawing: **Figure** (one machine, 40–80 parts) or **Hero** (a machine with its piping and skid, 80–150). A whole plant in 3D is a package: propose one unit.
29
+
30
+ Write the parts list in the build script header before any geometry, grouped:
31
+
32
+ - **Ground**: pad or skid (`box`, `layer: -1`), halo, feet.
33
+ - **Structure**: saddles, legs, skirt, frame, brackets, stand and clamp.
34
+ - **Body**: shell, heads, channel, bonnet, flanges.
35
+ - **Internals** (inside a cutaway): tubes, baffles, tubesheets, impeller and shaft, coil, packing, liquid, frits, wool plugs.
36
+ - **Connections**: nozzles with flanges and bolt circles, pipes with bends, valves, fittings.
37
+ - **Instruments**: gauges, thermowells, transmitters, cable glands.
38
+
39
+ Every part sits on another part: a nozzle on the shell, a flange on the nozzle, bolts on the flange, the shell on saddles, saddles on the pad.
40
+
41
+ ## 3. Camera and composition
42
+
43
+ - `createIsoView({ azimuth, elevation })`: azimuth 45° shows two faces equally; turn it (30–65°) so the face that carries the mechanism, or the long side of a horizontal vessel, faces the viewer (the exchanger example uses 62°). Elevation 25–30° for most equipment; 35–45° to look into an open vessel or down onto a skid.
44
+ - World units are the object's millimetres: x and y on the floor, z up. Build everything at its real position and let `fit` scale it into the panel.
45
+ - The art fills most of its panel; callouts take side columns only as wide as their longest label, and `fit` chooses the sides that leave the art largest.
46
+ - A set of figures keeps one azimuth, elevation and scale so parts compare.
47
+
48
+ ## 4. Solids, tones and light
49
+
50
+ - **Solids, never wireframes.** Every part is filled, so what is in front hides what is behind. No dashed hidden edges, no see-through boxes.
51
+ - **One fixed light** from the upper left in front; faces turned toward it are lighter. Flat parts (`box`, `prism`) shade each side face by its facing and lighten the top; turned parts (`lathe`) shade continuously around their axis, so a cylinder or head never steps in tone.
52
+ - **Tones are materials.** Name each part's finish: `steel` (painted carbon steel), `stainless`, `frame` (painted structural steel), `copper`, `brass`, `cast` (cast iron, motors, dark machinery), `concrete`, `insulation` (aluminium cladding), `rubber` (gaskets, hoses), `glass`. The generic roles still work and follow the palette: ground `lo` (concrete), structure and heads `mid` (painted steel), the body `hi` (stainless), the subject `accent`, the inside of a cut vessel `cavity`, cut wall faces `section`, a liquid `liquid`, a granular solid `warm`, a soft plug `cool`. A figure usually shows four to six materials; a single grey body with one accent is the opt-in quiet look, not the default.
53
+ - **Halo.** `halo({ plan, z })` under the base grounds the object.
54
+
55
+ ## 5. Painter's order
56
+
57
+ Items are painted far to near by the depth of their centre, after their layer: halo (−2), ground (−1), everything else (0). Two rules fix almost every ordering fault:
58
+
59
+ 1. **Ground first.** A large pad has its centre far behind the parts that stand on its front; give it `layer: -1`.
60
+ 2. **Split anything that wraps or interleaves.** Tubes that pass through baffles are split at every baffle into segments, each its own item; a pipe is drawn as chunks that sort separately; a long body cut by a ring is two bodies and the ring. An item that is both in front of and behind another cannot be ordered by any key.
61
+
62
+ When something behind shows through something in front, the item in front is painted too early: lower the far item's `bias`, raise the near one's, or split one of them.
63
+
64
+ ## 6. Window cutaways
65
+
66
+ `lathe({ cut: { from, to, span }, wall })` removes a window from the side facing the viewer between `s = from` and `s = to`, `span` degrees wide. Through it the reader sees the inner wall (`cavity` tones), the internals passed with `into: shell`, and the cut wall faces (`section` tone) along both edges and on the window sill. Everything outside the window stays closed and hides what is behind it.
67
+
68
+ - Keep the rims closed: start the window a little after a flange or head and end it before the next.
69
+ - **Never put a nozzle, bracket or gauge on shell the window removes.** With a wide span the window reaches the top and bottom of the shell; move the nozzle along the axis past the window, or narrow the span (80–100°).
70
+ - Internals sit fully inside the inner radius (`R - wall`), so nothing pokes through the wall.
71
+ - A liquid is a lathe of `liquid` tone up to its level, inside the vessel; an interface is a seam ring.
72
+
73
+ ## 7. Turned parts, flanges, nozzles and pipes
74
+
75
+ - **Think in half-sections.** A turned part is its profile `[[s, r], …]` along an axis: a flat cap is a jump in `r` at one `s`; a head is `ellipsoidalHead(R)` with smooth knots. One lathe is one convex body; a flange is its own lathe (a disc), a nozzle its own lathe, so each sorts by its own depth.
76
+ - **Flanges sit flush.** A flange's back face lands on the end of its nozzle or pipe; two flanges meet face to face at a joint; add a `boltCircle` on the face that points at the viewer.
77
+ - **Nozzles leave along the surface normal**: radially from a shell (`axis: "z"` for a top nozzle), axially from a head.
78
+ - **Pipes bend; they do not kink.** `pipe({ points, r })` fillets every corner with a bend of about 2.6 r. Start on the port's normal, follow the structure (parallel to the body, along racks), cross other pipes only on straights and clearly apart in depth, and end on a flange or fitting.
79
+ - **Few pipe sizes**, all distinguishable: main lines, feed lines, tubing for instruments.
80
+
81
+ ## 8. Detail that belongs to parts
82
+
83
+ At 1× the figure reads as one machine; at 4× every region still has something true on it. Before finishing, render the close-up sweep (`render.mjs --sheet`) and look for bare faces, then add the real part that belongs there: bolts on every flange, a nameplate with rule lines, seams where a head meets the shell (`seams`), a drain plug, earthing lug, feet under a skid, a thermowell with its head. Never add decoration that belongs to no part. A texture (pellets, a weave) stops at a real edge or thins toward a limb; it never stops on a straight cut in mid-face.
84
+
85
+ ## 9. Callouts
86
+
87
+ Words stay out of the art. `callout({ part, text })` puts each label in a column beside the art with a hairline leader that ends on a dot on the named part; labels are ordered so no two leaders cross or run within 1.4 mm of each other, and QA fails a leader whose dot is off its named part (`LEADER.LOOSE`), that crosses another (`LEADER.CROSSING`) or that crowds another or its dot (`LEADER.SPACING`). Two dots a few millimetres apart on the page (a baffle and the tube in front of it) crowd whatever the order: spread them before you build, on faces far enough apart. Choose the anchor on a face the viewer can see (`handle.anchor(f, sideDegrees)`): QA reads outlines, not visibility, so a dot on a hidden face passes the gate and fails the eye. One label per part, a noun phrase, sentence case, no full stop; numbers with units (`Column, 26 mm ID`). For a journal figure the caption names the parts in the same words.
88
+
89
+ ## 10. Groups for pages and films
90
+
91
+ Give every part its assembly with `group` (shell, internals, drive, heads, supports) even for a static figure: it costs nothing, it is written as `data-group`, and it is what an interactive page hides and explodes and a motion loop moves. Write the cutaway as a scene function of the camera, the explode amount and a flow phase when the user may want a page or a film; `references/cutaway-outputs.md` has the pattern.
92
+
93
+ ## 11. Anti-patterns
94
+
95
+ - Abstract stacks: floating slabs or boxes standing for units read as nothing; draw the real equipment.
96
+ - One big plain body: a shell with nothing on it is unfinished; add flanges, nozzles, supports, seams.
97
+ - Labels or leader balloons over the object.
98
+ - Colour that names no material: a hue on a part must be its real finish, a fluid, or the one accent; never paint parts in rainbow categories.
99
+ - A part sunk into another, or floating off it: every contact is flush or seated.
100
+ - A curved surface in visible tone steps: use `lathe`, never stacked boxes.
101
+ - A cutaway that shows internals the real equipment does not have, or hides the one the text is about.
@@ -0,0 +1,72 @@
1
+ # Journal figures of apparatus and equipment
2
+
3
+ An apparatus figure in a paper answers one question: what did the experiment use, and how was it connected? A reviewer reads it beside the methods section, often printed in grey, sometimes reduced. Everything here is about surviving that reading.
4
+
5
+ ## Contents
6
+ 1. Size
7
+ 2. Type
8
+ 3. Lines and colour
9
+ 4. Panels
10
+ 5. What goes in the art and what goes in the caption
11
+ 6. Korean labels
12
+ 7. Export
13
+ 8. Publisher notes
14
+
15
+ ## 1. Size
16
+
17
+ Draw at final size; never scale a figure afterwards, because type and lines scale with it.
18
+
19
+ | Width | mm | Kit |
20
+ | --- | --- | --- |
21
+ | Single column | 85 (Elsevier 90, ACS 82.5, Nature 89) | `createFigure({ width: "single" })` |
22
+ | One and a half | 120–140 | `"onehalf"` |
23
+ | Double column | 178 (Elsevier 190, ACS 178, Nature 183) | `"double"` |
24
+
25
+ Use the target journal's own numbers when the user names it (`createFigure({ width: 183 })`). Height is what the content needs, at most 230 mm; an apparatus figure is usually 60–100 mm tall. Check the current author guidelines when submission accuracy matters; numbers here can go stale.
26
+
27
+ ## 2. Type
28
+
29
+ - **7 pt** for labels (`JOURNAL.textPt`), **8 pt bold** for panel labels, never under **6 pt** at print size; the QA gate fails anything smaller (`TEXT.MIN_SIZE`).
30
+ - One sans family: **Arial** (Helvetica-compatible) for Latin text; it is what most publishers ask for and Chrome embeds it in a PDF as real TrueType text.
31
+ - Sentence case, no full stops on labels; units with a space (`5 MPa`, `2 °C`); chemical formulas with true subscripts when the font allows, otherwise plain (`CO2`) consistently.
32
+ - No title inside the art: the caption is the title.
33
+
34
+ ## 3. Lines and colour
35
+
36
+ - Lines 0.5–1 pt (0.18–0.35 mm); nothing under 0.25 pt (`STROKE.MIN`). Stream lines 1 pt, symbol outlines 1–1.4 pt.
37
+ - **Colour-vision-safe stream roles** (Okabe–Ito): gas blue `#0072B2`, liquid sky `#56B4E9`, coolant green `#009E73`, heating vermillion `#D55E00`, sample purple `#CC79A7`, vacuum orange `#E69F00` dashed, vent grey dashed, signal grey dashed. `PALETTE.CVD` fails two role colours that become hard to tell apart under normal, deuteranopic, protanopic or tritanopic vision.
38
+ - **Colour is never the only carrier.** Vent, vacuum and signal lines also differ by dash; every role used is in the key (`fig.key`), and the caption can name the streams.
39
+ - Figures print in grey: check the PNG in greyscale when streams differ only by hue.
40
+
41
+ ## 4. Panels
42
+
43
+ - Label panels **(a), (b), …** at their top left, bold 8 pt, in reading order left to right, top to bottom (`PANEL.LABELS` checks order, size and position).
44
+ - Each panel makes one point: (a) the flow scheme, (b) the cell or column in detail, (c) a photograph or a measured result (that panel belongs to `lit-scientific-visualization`; leave it a box and say so).
45
+ - Align panel edges; give the 3D panel enough width for its callouts, or put its callouts on one side.
46
+ - A shared key goes once, under the panels.
47
+
48
+ ## 5. What goes in the art and what goes in the caption
49
+
50
+ | In the art | In the caption |
51
+ | --- | --- |
52
+ | Equipment names (`Column`, `MS`, `MFC`), short and consistent with the methods | What the apparatus does and the experimental sequence |
53
+ | Stream key | Model numbers, suppliers, dimensions not visible in the art |
54
+ | Panel labels | The meaning of each panel |
55
+ | One number when it is the point (`26 mm ID`) | Operating ranges, uncertainties |
56
+
57
+ Tags such as `PT-1` belong in a methods-style figure only when the text refers to them; otherwise use names (`Pressure transducer`). Journal canvases label equipment with `name` and fall back to the tag.
58
+
59
+ ## 6. Korean labels
60
+
61
+ Hangul labels use **Pretendard** for the whole label (`fontFor` switches automatically when a label contains Hangul). Keep units and formulas in Latin (`압력 5 MPa`, `CO2 공급`). Korean labels run wider: allow about 1 em per syllable. The renderer loads Pretendard from the sibling `lit-diagram-drawer` skill; in the SVG the family name stays `Pretendard` with system fallbacks. For an English paper with Korean annotations remove the Korean before submission.
62
+
63
+ ## 7. Export
64
+
65
+ - **SVG**: the editable master, vector, millimetre-sized.
66
+ - **PDF**: `render.mjs --pdf`; vector, page size equals the figure size (`PDF.SIZE`). Latin text in Arial is embedded as TrueType text. Chrome embeds Pretendard (a CFF variable font) as **Type 3** glyphs; some checkers (for example IEEE PDF eXpress) reject Type 3, so for those journals keep labels in Latin or submit the PNG/TIFF.
67
+ - **PNG**: `render.mjs --png --dpi 600` (line art: 600–1200 dpi; 1000+ when the journal asks for line-art TIFF). The file records its resolution in a pHYs chunk; `qa.mjs --png` checks the resolution and that the pixel width matches the drawing width.
68
+ - Name files by figure number (`fig2-apparatus.pdf`).
69
+
70
+ ## 8. Publisher notes
71
+
72
+ Commonly published values, to confirm against the current guide: Elsevier lists Arial, Courier, Times and Symbol, column widths of 90/140/190 mm and 1000 dpi for line-art TIFF; ACS uses 3.25 in and 7 in columns, Helvetica or Arial, and 1200 dpi line art; Nature uses 89/183 mm and Helvetica or Arial at 5–7 pt; RSC uses 8.3/17.1 cm columns. Korean journals (대한화학회, 한국화학공학회): follow the template; Hangul in a sans such as Pretendard or 맑은 고딕. Always confirm the current guide for the target journal.