@litfamily/lithermes 1.0.28 → 1.0.30

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 (167) hide show
  1. package/README.md +12 -9
  2. package/README_Ko-KR.md +12 -9
  3. package/assets/lithermes-plugin/README.md +15 -2
  4. package/assets/lithermes-plugin/__init__.py +49 -10
  5. package/assets/lithermes-plugin/adhd_stack.py +228 -0
  6. package/assets/lithermes-plugin/core.py +30 -4
  7. package/assets/lithermes-plugin/core_commands.py +99 -0
  8. package/assets/lithermes-plugin/core_contexts.py +54 -4
  9. package/assets/lithermes-plugin/core_contract.py +2 -2
  10. package/assets/lithermes-plugin/core_routing.py +126 -0
  11. package/assets/lithermes-plugin/diagnostics.py +3 -2
  12. package/assets/lithermes-plugin/lit_mark.py +75 -0
  13. package/assets/lithermes-plugin/payload-version.json +472 -60
  14. package/assets/lithermes-plugin/plugin.yaml +1 -1
  15. package/assets/lithermes-plugin/scientific_visualization.py +23 -0
  16. package/assets/lithermes-plugin/skills/frontend-ui-ux/SKILL.md +7 -7
  17. package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/components.css +414 -0
  18. package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/fonts.json +42 -0
  19. package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
  20. package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
  21. package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
  22. package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
  23. package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
  24. package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
  25. package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
  26. package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
  27. package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/ui.js +226 -0
  28. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/README.md +2 -4
  29. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +4 -4
  30. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/craft-floor.md +10 -0
  31. package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
  32. package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe-rules.json +6 -1
  33. package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe.mjs +76 -4
  34. package/assets/lithermes-plugin/skills/lit-adhd/SKILL.md +194 -0
  35. package/assets/lithermes-plugin/skills/lit-adhd/references/brief-contract.md +55 -0
  36. package/assets/lithermes-plugin/skills/lit-adhd/references/card-examples.md +86 -0
  37. package/assets/lithermes-plugin/skills/lit-adhd/references/status-taxonomy.md +40 -0
  38. package/assets/lithermes-plugin/skills/lit-adhd/scripts/adhd.mjs +11 -0
  39. package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/collect.mjs +554 -0
  40. package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/gauge.mjs +89 -0
  41. package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/hermes-sessions.mjs +219 -0
  42. package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/next-step.mjs +101 -0
  43. package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/plan-task-rows.mjs +32 -0
  44. package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/redact.mjs +42 -0
  45. package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/secret-shapes.mjs +59 -0
  46. package/assets/lithermes-plugin/skills/lit-diagram-drawer/SKILL.md +7 -4
  47. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/style-guide.md +20 -11
  48. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
  49. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
  50. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-tree.md +3 -3
  51. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
  52. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/verifier-guide.md +5 -0
  53. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_quality.py +4 -4
  54. package/assets/lithermes-plugin/skills/lit-docx/SKILL.md +1 -1
  55. package/assets/lithermes-plugin/skills/lit-hijack/SKILL.md +239 -0
  56. package/assets/lithermes-plugin/skills/lit-hijack/references/capture-and-provenance.md +123 -0
  57. package/assets/lithermes-plugin/skills/lit-hijack/references/clean-room-build.md +128 -0
  58. package/assets/lithermes-plugin/skills/lit-hijack/references/distil-and-licence.md +128 -0
  59. package/assets/lithermes-plugin/skills/lit-hijack/references/gap-and-port-design.md +152 -0
  60. package/assets/lithermes-plugin/skills/lit-hijack/references/parity-and-report.md +131 -0
  61. package/assets/lithermes-plugin/skills/lit-hijack/scripts/hijack.mjs +995 -0
  62. package/assets/lithermes-plugin/skills/lit-plan/SKILL.md +1 -0
  63. package/assets/lithermes-plugin/skills/lit-pptx/SKILL.md +11 -6
  64. package/assets/lithermes-plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
  65. package/assets/lithermes-plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
  66. package/assets/lithermes-plugin/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
  67. package/assets/lithermes-plugin/skills/lit-pptx/fonts/provenance.json +1 -0
  68. package/assets/lithermes-plugin/skills/lit-pptx/references/anti-slop-checklist.md +3 -0
  69. package/assets/lithermes-plugin/skills/lit-pptx/references/authoring-guide.md +1 -1
  70. package/assets/lithermes-plugin/skills/lit-pptx/references/consensus-protocol.md +1 -1
  71. package/assets/lithermes-plugin/skills/lit-pptx/references/direction-step.md +8 -2
  72. package/assets/lithermes-plugin/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +175 -0
  73. package/assets/lithermes-plugin/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +156 -0
  74. package/assets/lithermes-plugin/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +141 -0
  75. package/assets/lithermes-plugin/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +160 -0
  76. package/assets/lithermes-plugin/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +183 -0
  77. package/assets/lithermes-plugin/skills/lit-pptx/references/layout-families.md +1 -1
  78. package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/bauhaus.md +105 -0
  79. package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/computational-brutalism.md +111 -0
  80. package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/neo-brutalism.md +114 -0
  81. package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/newsroom.md +106 -0
  82. package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/riso.md +101 -0
  83. package/assets/lithermes-plugin/skills/lit-pptx/scripts/craft_extras.py +49 -0
  84. package/assets/lithermes-plugin/skills/lit-pptx/scripts/deck_output.py +4 -2
  85. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/render-pack.js +96 -10
  86. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/template-registry.js +50 -6
  87. package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +103 -0
  88. package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
  89. package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
  90. package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
  91. package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
  92. package/assets/lithermes-plugin/skills/lit-recap/SKILL.md +1 -1
  93. package/assets/lithermes-plugin/skills/lit-schematic/SKILL.md +144 -0
  94. package/assets/lithermes-plugin/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
  95. package/assets/lithermes-plugin/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
  96. package/assets/lithermes-plugin/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
  97. package/assets/lithermes-plugin/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
  98. package/assets/lithermes-plugin/skills/lit-schematic/references/cutaway-outputs.md +70 -0
  99. package/assets/lithermes-plugin/skills/lit-schematic/references/isometric-craft.md +101 -0
  100. package/assets/lithermes-plugin/skills/lit-schematic/references/journal-figures.md +72 -0
  101. package/assets/lithermes-plugin/skills/lit-schematic/references/kit-api.md +134 -0
  102. package/assets/lithermes-plugin/skills/lit-schematic/references/pfd-and-layout.md +53 -0
  103. package/assets/lithermes-plugin/skills/lit-schematic/references/pid-drafting.md +103 -0
  104. package/assets/lithermes-plugin/skills/lit-schematic/references/pid-symbols.md +163 -0
  105. package/assets/lithermes-plugin/skills/lit-schematic/references/verification.md +100 -0
  106. package/assets/lithermes-plugin/skills/lit-schematic/scripts/doctor.mjs +57 -0
  107. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
  108. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/fonts.mjs +27 -0
  109. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
  110. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
  111. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
  112. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
  113. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
  114. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
  115. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/png.mjs +132 -0
  116. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
  117. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/route.mjs +218 -0
  118. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
  119. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
  120. package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
  121. package/assets/lithermes-plugin/skills/lit-schematic/scripts/motion.mjs +129 -0
  122. package/assets/lithermes-plugin/skills/lit-schematic/scripts/qa.mjs +92 -0
  123. package/assets/lithermes-plugin/skills/lit-schematic/scripts/render.mjs +233 -0
  124. package/assets/lithermes-plugin/skills/lit-schematic/scripts/scaffold.mjs +60 -0
  125. package/assets/lithermes-plugin/skills/lit-scientific-visualization/SKILL.md +19 -1
  126. package/assets/lithermes-plugin/skills/lit-typographic-motion/SKILL.md +12 -11
  127. package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/motion.mjs +56 -2
  128. package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/runtime.mjs +129 -4
  129. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +9 -0
  130. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/blender/runner.py +218 -0
  131. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/blender.mjs +154 -0
  132. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/board.mjs +132 -0
  133. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/completion.mjs +48 -5
  134. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/gate.mjs +13 -0
  135. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/house.mjs +44 -0
  136. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/scaffold.mjs +238 -0
  137. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/score.mjs +643 -0
  138. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/sound-dsp.mjs +21 -0
  139. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/sound.mjs +171 -0
  140. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-qa.mjs +23 -3
  141. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage.mjs +124 -17
  142. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/styles.mjs +240 -0
  143. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/treatment.mjs +152 -10
  144. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
  145. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
  146. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
  147. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
  148. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/hero-3d/expected.json +67 -0
  149. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/hero-3d/index.html +139 -0
  150. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/depth.md +74 -0
  151. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/studio.md +87 -0
  152. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/style-bibles.md +9 -2
  153. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/treatment.md +24 -8
  154. package/assets/lithermes-plugin/skills/lit-typographic-motion/styles/paper.json +41 -0
  155. package/assets/lithermes-plugin/skills/lit-typographic-motion/styles/swiss-signal.json +41 -0
  156. package/assets/lithermes-plugin/skills/lit-typographic-motion/styles/terminalcore.json +42 -0
  157. package/assets/lithermes-plugin/skills/lit-typographic-motion/styles/tidal.json +41 -0
  158. package/assets/lithermes-plugin/skills/litresearch/SKILL.md +4 -0
  159. package/assets/lithermes-plugin/skills/readme-studio/SKILL.md +1 -1
  160. package/assets/lithermes-plugin/skills/readme-studio/templates/hyperframes/package-lock.json +3 -3
  161. package/assets/lithermes-plugin/skills/readme-studio/templates/remotion/package-lock.json +3 -3
  162. package/assets/lithermes-plugin/skills/review-work/SKILL.md +2 -0
  163. package/assets/lithermes-plugin/skills/start-work/SKILL.md +2 -1
  164. package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +3 -3
  165. package/package.json +1 -1
  166. package/readme-assets/badge-version.svg +1 -1
  167. package/src/lib/skillPayload.js +1 -1
@@ -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 this plugin; 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.
@@ -0,0 +1,134 @@
1
+ # Kit API
2
+
3
+ Every kit drawing is authored in **millimetres at final print size**: the SVG root carries `width="…mm"`, `height="…mm"` and a viewBox in the same numbers, so a stroke of `0.35` is 0.35 mm on paper and a font size of `2.47` is 7 pt. Import modules from `$SKILL_ROOT/scripts/lib/` (the scaffold writes absolute `file://` imports for you).
4
+
5
+ ## Contents
6
+ 1. Sheets and canvases (`sheet.mjs`)
7
+ 2. Symbols (`pid.mjs`)
8
+ 3. Routing (`route.mjs`)
9
+ 4. Journal figures (`figure.mjs`)
10
+ 5. Isometric views (`iso.mjs`)
11
+ 6. Tags (`isa.mjs`), theme (`theme.mjs`), helpers
12
+ 7. QA and render from code
13
+ 8. Legend sheet (`legend-sheet.mjs`)
14
+ 9. Interactive page (`interactive.mjs`) and motion loop (`scripts/motion.mjs`)
15
+
16
+ ## 1. Sheets and canvases (`sheet.mjs`)
17
+
18
+ ### `createSheet(options)` → sheet
19
+ An ISO sheet with a zone frame, an equipment header band, a legend column, a notes block and a title block. Options: `mode` (`"pid"` | `"pfd"`), `size` (`"A4"`, `"A3"` default, `"A2"`, `"A1"`, or `[w, h]` mm), `title` (required), `subtitle`, `number`, `rev`, `date`, `scale` (`"NTS"` default), `sheet` (`"1 OF 1"`), `project`, `owner`, `drawn`, `checked`, `approved`, `notes` (array of strings, numbered), `lang`, `legendTitle`, `legendColumn` (default `true`), `headerBand` (default `true`). When the legend column would overflow, the instrument identification letters move to the right half of the notes block; if it still overflows, `sheet.warnings` says so.
20
+
21
+ The drawing area on A3 is about `[24, 39, 342, 246]`; read `sheet.drawing` for the exact box. Place every symbol inside it. A sheet has every canvas method below plus:
22
+
23
+ | Method | Does |
24
+ | --- | --- |
25
+ | `sheet.equipmentInfo(sym, lines)` | Adds the symbol's tag, name and spec lines to the header band above it (columns de-overlap themselves). |
26
+ | `sheet.render({ desc })` | The finished SVG string (`data-schematic-mode` is the sheet's mode). |
27
+ | `sheet.warnings` | Placement problems the build could not solve (a label with no clear place, a symbol outside the canvas, a legend that overflows). Print them; QA will fail on the same defects. |
28
+
29
+ ### `createCanvas({ box, style, grid, clearance, stub, labelSize })` → canvas
30
+ A routed drawing area in a sheet or a journal panel. `style` is `"pid"` (black ink, ISA line weights) or `"journal"` (stream colours, names instead of tags, 7 pt labels, a 1 mm routing grid).
31
+
32
+ | Method | Does |
33
+ | --- | --- |
34
+ | `add(sym)` | Places a symbol; warns when it overlaps another or leaves the canvas. Returns the symbol. |
35
+ | `pipe({ from, to, via, points, kind, number, arrows })` | A process line. `kind`: `process-major` (default, 0.7 mm), `process-minor` (0.5), `utility` (0.35); every class has its own weight. `from`/`to` are ports or taps. With no `points` it is routed; `via` adds waypoints; `points` gives every corner yourself (checked orthogonal, first and last legs on the port normals). `number` places the line number on the longest clear straight. `arrows`: `"auto"`, `false`, or fractions `[0.3, 0.8]`. |
36
+ | `signal({ from, to, type })` | An instrument signal (ISA-5.1 Table 5.3.2): `type` `electric` (dashed, default), `pneumatic` (double slashes), `hydraulic` (L marks), `capillary` (crosses), `guided` (sine wave), `unguided` (broken sine wave), `data` (open circles; `software` is the same), `mechanical` (filled dots). Run a controller output to `valve.port("actuator")`. |
37
+ | `connect({ from, to })` | Instrument supply or process connection (0.18 mm thin solid impulse line). |
38
+ | `stream({ from, to, role, via })` | Journal stream line in the colour of `role` (`gas`, `liquid`, `coolant`, `heating`, `vent`, `signal`, `sample`, `vacuum`). |
39
+ | `inline(line, factory, options)` | Puts a valve, element or filter on the line at `options.at` (fraction, or a point near the line) and gaps the line under it. It snaps to the nearest straight run long enough; it throws when none is. Returns the symbol. |
40
+ | `line.tap(at)` | A branch point on a line (fraction, arc length, or a point within 1 mm of the line, and at least 3 mm from a bend). The port normal points to the line's left; `.flip()` gives the other side. A junction dot is drawn. |
41
+ | `line.at(fraction)` | `{ point, direction }` along the routed line. |
42
+ | `zone(box, name)` | A dashed enclosure (oven, cold box, skid boundary) with its name inside the top-left corner; lines may cross it, labels avoid it. |
43
+ | `streamNumber(line, n, { at })` | PFD stream number diamond on a straight run. |
44
+ | `streamTable({ box: [x0, y0, x1], streams, rows: [{ label, values }] })` | A stream table; returns its bottom y. |
45
+ | `text(x, y, text, { size, anchor, bold, color, kind })` | A fixed label that routes and later labels avoid. |
46
+ | `render()` | The canvas group. |
47
+
48
+ ## 2. Symbols (`pid.mjs`)
49
+
50
+ Every factory takes `x, y` (the centre unless noted) in sheet mm and returns a symbol with `kind`, `tag`, `box`, `boxes`, `port(name[, t])`. Equipment tags must parse as `R-101`, `P-101A`, `TK-101`, …; instrument tags as ISA-5.1 (`PIC-101`). A bad tag throws.
51
+
52
+ | Factory | Kind(s) | Ports |
53
+ | --- | --- | --- |
54
+ | `vessel({ w, h, top, bottom, jacket, agitator, coil, packing, trays, demister, supports, liquid, tag, name })` | `vessel.vertical`, `.reactor`, `.jacketed`, `.column` | `top`, `bottom`, `top-left`, `top-right`, `bottom-left`, `bottom-right`, `jacket-in`, `jacket-out`, and `port("left" \| "right", t)` along the straight shell (t 0 top tangent → 1 bottom tangent), `port("top" \| "bottom", t)` across a head |
55
+ | `reactor(o)`, `column(o)`, `tank(o)` | jacketed stirred reactor; column with trays; cone-roof tank | as `vessel` |
56
+ | `drum({ w, h, saddles })` | `vessel.horizontal` | `left`, `right`, `port("top" \| "bottom", t)` |
57
+ | `gasCylinder({ w, h })` | `gas.cylinder` | `outlet` (up) |
58
+ | `pump({ r, type, facing })` | `pump.centrifugal`, `.positive`, `.metering` | `suction`, `discharge`, `signal` (stroke or speed input, bottom) |
59
+ | `compressor({ r, type, facing })` | `compressor.centrifugal`, `.reciprocating` (also a gas booster) | `inlet`, `outlet`, `drive` |
60
+ | `motor({ r })` | `drive.motor` | `shaft`, `left`, `right`, `bottom` |
61
+ | `exchanger({ r })` | `exchanger.general` | `tube-in`, `tube-out`, `shell-in`, `shell-out` |
62
+ | `shellTube({ w, h, type })` | `exchanger.shelltube` (`type: "fixed"`, default), `exchanger.floating`, `exchanger.utube` | `tube-in`, `tube-out`, `shell-in`, `shell-out` |
63
+ | `plateExchanger({ w, h })` | `exchanger.plate` (ISO 10628-2 item 3.6) | `hot-in`, `hot-out`, `cold-in`, `cold-out` |
64
+ | `airCooler({ w, h })` | `exchanger.aircooler` | `in`, `out` |
65
+ | `unit({ w, h, caption, glyph, ports })` | `package.unit` (chiller, circulator, analyser, DAQ, booster skid) | as declared: `ports: { supply: ["right", 0.3] }`; also `port(side, t)` |
66
+ | `valve({ type, orient, actuator, fail, tag })` | `valve.gate`, `.globe`, `.ball`, `.butterfly`, `.check`, `.needle`, `.plug`, `.diaphragm`, `.threeway`; with `actuator` (`diaphragm`, `piston`, `motor`, `solenoid`, `hand`) the kind is `valve.<type>.<actuator>`; `fail` is `FO`, `FC`, `FL` or `FI` | `a`, `b` (and `c` on a three-way), `actuator` when actuated |
67
+ | `angleValve({ inlet, outlet, actuator, fail, tag })` | `valve.angle`, `valve.angle.<actuator>` | `inlet`, `outlet` on two perpendicular sides, `actuator` |
68
+ | `regulator({ backPressure })` | `valve.regulator`, `valve.backpressure` | `a`, `b` |
69
+ | `safetyValve({ outlet, setPressure, tag })` | `valve.safety` | `inlet` (from below), `outlet` (side) |
70
+ | `ruptureDisk`, `orifice`, `rotameter`, `mfc`, `filter`, `reducer({ eccentric, flat })`, `flange`, `sightGlass`, `blind({ state })` | `relief.rupture`, `flow.orifice`, `flow.rotameter`, `flow.mfc` (+ `signal`), `line.filter`, `line.reducer`, `line.reducer-eccentric`, `line.flange`, `line.sightglass`, `line.blind`, `line.blind-open` | `a`, `b` |
71
+ | `flowElement({ type })` | `flow.orifice`, `flow.venturi`, `flow.magnetic`, `.coriolis`, `.vortex`, `.turbine`, `.ultrasonic`, `.thermal` | `a`, `b` |
72
+ | `instrument({ tag, fn, location, r })` | `instrument.<fn>-<location>`: `fn` `discrete` (default), `shared`, `computer`, `plc`; `location` `field` (default), `main`, `main-rear`, `aux`, `aux-rear` (round-1 names `dcs`, `plc`, `panel`, `panel-rear`, `local-panel` still work) | `top`, `bottom`, `left`, `right` |
73
+ | `interlock({ text })` | `logic.interlock` | `top`, `bottom`, `left`, `right` |
74
+ | `vent({ text })`, `drain({ text })`, `samplePoint({ text, facing })` | `terminal.vent`, `terminal.drain`, `terminal.sample` | `in` |
75
+ | `offPage({ text, pointing })`, `utility({ text, pointing })` | `terminal.offpage`, `terminal.utility` | `in`, `out` |
76
+
77
+ Inline factories take `orient` (`right`, `left`, `up`, `down`); `sheet.inline` sets it from the line. A symbol on a line running left is mirrored, not turned over, so an actuator still stands above the line; letters on an inline symbol (fail positions) always read upright, and on a vertical line they sit beside the body opposite the actuator. A later process line that crosses an earlier one hops it; the gate accepts the hop. `describeKind(kind, { short })` gives the legend text for every kind, including composed ones; `KIND_DESCRIPTIONS`, `ACTUATORS`, `FUNCTIONS`, `LOCATIONS` and `FAIL_POSITIONS` hold the parts.
78
+
79
+ ## 3. Routing (`route.mjs`)
80
+
81
+ `routeOrthogonal({ from, to, obstacles, lines, bounds, grid, clearance, stub, bendCost, crossCost, nearCost, exempt })` returns corner points. It leaves each port along its normal far enough to clear every symbol's clearance zone, never enters an inflated symbol box, never runs along or turns on an existing line, crosses existing lines only at right angles (with a cost), and minimises length plus bends. Two ports that face each other on one axis join straight. It throws when no route exists. `orthogonal(points, { from, to })` validates a hand-given polyline.
82
+
83
+ ## 4. Journal figures (`figure.mjs`)
84
+
85
+ `createFigure({ width, height, title, desc, lang, mode, labelStyle, textPt })`: `width` is `"single"` (85 mm), `"onehalf"` (120), `"double"` (178) or a number; `height` ≤ 230 mm; `labelStyle` `"paren"` → (a), `"bold"` → a, `"upper"` → A.
86
+
87
+ | Method | Does |
88
+ | --- | --- |
89
+ | `fig.panel(label, [x, y, w, h])` | A panel with its label at the top left (`label` `null` for a single-panel figure without a label). |
90
+ | `panel.canvas({ box })` | A journal-style canvas inside the panel (default box: below the label). |
91
+ | `panel.iso(view, { box, labelColumn, calloutSize })` | Fits an isometric view and its callouts into the panel. |
92
+ | `panel.text(x, y, text, options)`, `panel.raw(svg)` | Extra content. |
93
+ | `fig.key(roles, [x, y], { columns, colWidth })` | Stream-colour key; QA requires every role drawn to be in it. |
94
+ | `fig.render()` | The SVG (`data-min-text-pt="6"`). |
95
+
96
+ ## 5. Isometric views (`iso.mjs`)
97
+
98
+ `createIsoView({ azimuth = 45, elevation = 30, light })`. World units are the object's own millimetres, x and y on the floor, z up. Nothing is projected until `fit`.
99
+
100
+ | Function | Does |
101
+ | --- | --- |
102
+ | `lathe({ name, origin, axis, profile, tone, steps, seams, cut, wall, layer, bias, into })` | A turned part: `profile` is `[[s, r], …]` along `axis` (`"x"`, `"y"`, `"z"`, `"-z"` or a vector) from `origin`; add a third element `1` to mark a smooth knot (no crease ring). One lathe is one convex body: build steps, flanges and nozzles as separate lathes. `seams: [s…]` draws weld rings. `cut: { from, to, span, turn }` opens a window from `s = from` to `to`, `span` degrees wide, centred on the side facing the viewer (`turn` rotates it); `wall` is the cut wall thickness. Returns a handle with `point(s, r, angle)`, `surface(s, sideDeg)`, `anchor(f, sideDeg)`, `facingAngle`, `frame`. |
103
+ | `box({ name, plan: { x, y, w, d, r }, z, h, tone, layer })`, `prism({ ring, z, h, … })` | Flat parts: pads, skids, saddles, brackets, plates. `anchor()` is the top centre. |
104
+ | `pipe({ name, points, r, tone, bend, caps, flow })` | A filleted route drawn as depth-sorted chunks with shine and shade bands; `anchor(f)` a point along it. `flow: { color, dash: [on, off], offset, width }` adds a dashed stripe whose dashes march with `offset` (screen mm), continuous across chunks. |
105
+ | `sphere({ name, center, r })`, `dots({ points, r, color })`, `polyline({ points, color, width, dash, flow })`, `boltCircle({ frame, s, radius, count })` | Balls, textures, hairlines, bolt heads. `polyline` `flow: { dash, offset, start }` in world mm marches dashes along collinear pieces of one run. |
106
+ | `extent(points)` | World points the fit keeps in frame although nothing is drawn there: where exploded or moving parts go, so every frame and every page state shares one scale. |
107
+ | `halo({ plan, z })` | Soft ground shadow (layer −2). |
108
+ | `callout({ part, at, text, side, number })` | A label in a column beside the art with a leader to a dot on `part` (its `anchor()`, or `at`). Sides are chosen to leave the art largest; leaders are reordered so none cross. |
109
+ | `fit(box, { pad, labelColumn, size })`, `render()`, `renderCallouts({ box, size })` | Used by `panel.iso`. Callout leaders are ordered so none cross and none run within 1.4 mm of another; what cannot be cleared lands in `view.warnings` (and in `fig.warnings()`). |
110
+ | `screenVector([dx, dy, dz])` | The screen offset in mm of a world move at the fitted scale: the explode vector an interactive page needs. |
111
+
112
+ Every factory also takes `group`: the assembly the part belongs to (`"shell"`, `"internals"`, `"drive"`). It is written as `data-group` around the part; interactive pages toggle and explode by it.
113
+
114
+ `createIsoView({ azimuth, elevation, light, palette })`: `palette` is `"material"` (default, full colour) or `"grey"` (opt-in quiet look); any other name throws. Material tones: `steel`, `stainless`, `frame`, `copper`, `brass`, `cast`, `concrete`, `insulation`, `rubber`, `glass` (the same in both palettes). Role tones, painted by the palette: `lo`, `mid`, `hi`, `accent` (the one part to watch), `cavity`, `section`, `liquid`, `warm`, `cool`. Layers: `-2` halo, `-1` ground, `0` everything else; `into: handle` draws a part inside a cut lathe, between its inner wall and its outer shell. Helpers: `ellipsoidalHead(R)`, `vesselProfile(R, straight, { top, bottom })`, `frameAlong(origin, axis)`, `fillet(points, radius)`.
115
+
116
+ ## 6. Tags, theme, helpers
117
+
118
+ - `parseInstrumentTag(tag)` → `{ ok, letters, loop, first, modifier, functions, meaning }` or `{ ok: false, error }`; `parseEquipmentTag(tag)`; `parseLineNumber(text)` → `{ ok, size, fluid, sequence, spec, insulation }`.
119
+ - `theme.mjs`: `PID` (weights and text sizes), `JOURNAL` (widths, 7 pt text, 6 pt floor, 0.75 pt lines), `OKABE_ITO`, `STREAM` roles, `ISO` tokens, `fontFor(text)` (Arial for Latin, Pretendard for Hangul).
120
+ - `svg.mjs`: `svgDocument`, `el`, `pathOf`, `textBox`, `ptToMm`, `mmToPt`.
121
+
122
+ ## 7. QA and render from code
123
+
124
+ `check(svgText, { mode, minTextPt, minStrokeMm })` from `qa-core.mjs` returns `{ verdict, fails, warnings, findings, stats }`. `renderPng`, `renderPdf`, `renderSheet`, `findChrome` from `scripts/render.mjs`; `checkExports` from `scripts/qa.mjs`.
125
+
126
+ ## 8. Legend sheet (`legend-sheet.mjs`)
127
+
128
+ `createLegendSheet({ size = "A2", title, number, rev, date, scale, project, owner, drawn, checked, approved, sheet })` returns the SVG of the "P&ID symbols and abbreviations" sheet: every line class, the equipment, valves, actuators, self-acting and relief devices, piping items, flow elements, connections, the ISA-5.1 shape-and-location matrix, the identification letters by position, fail positions, the line-number form and the equipment prefixes, laid out in columns that never split a short section, with the usual frame, notes and title block. It is built from the library, so it lists exactly what the kit draws.
129
+
130
+ ## 9. Interactive page (`interactive.mjs`) and motion loop (`scripts/motion.mjs`)
131
+
132
+ `renderInteractive({ title, description, lang, views: [{ id, label, svg, explode }], layers: [{ group, label }], order, duration, stagger, hint, text })` returns one self-contained HTML page: a button per preset view, an exploded-view toggle, a checkbox per layer group, callout labels that are focusable buttons, a part outline lit on hover or focus, keys `E` and `1`…`n`, `Esc` to clear, reduced motion honoured, an `aria-live` line for state changes, and `#view=…&explode=1&hide=a,b&select=C3` to open in a given state. `explode` maps each group to its screen vector in that view (`view.screenVector(worldMove)`); `order` is the order groups leave in (they come back in reverse). Build each view from the same scene function with `reach: true` style extents so the exploded parts stay on the page.
133
+
134
+ `renderMotion({ frame, out, name, seconds, fps, width, small, formats })` (or `node scripts/motion.mjs scene.mjs --out <dir>`) draws `frame(t)` for t in [0, 1), rasterises each frame in headless Chrome and writes `name.mp4` (H.264, yuv420p, even size), `name.webp` and `name.gif` (`small` px wide). It needs ffmpeg (and img2webp for WebP) and stops with `ENCODER_REQUIRED` and the install line when one is missing; it never installs anything. `examples/iso-heat-exchanger/scene.mjs` shows the pattern: one `scene({ azimuth, elevation, explode, flow, callouts, reach })` for the figure, the page and the film.
@@ -0,0 +1,53 @@
1
+ # BFD, PFD and equipment layout
2
+
3
+ ISO 10628-1 names three process diagrams that refine each other: the **block flow diagram** (units as boxes and the main streams), the **process flow diagram** (equipment, main streams with numbers, operating conditions, a stream table, the control loops that define the process), and the **P&ID** (every line, valve, instrument and safety device). Pick the lightest one that answers the reader's question.
4
+
5
+ ## Contents
6
+ 1. Which diagram
7
+ 2. Block flow diagram
8
+ 3. Process flow diagram
9
+ 4. Stream table
10
+ 5. Equipment layout and plot plan
11
+ 6. Faults
12
+
13
+ ## 1. Which diagram
14
+
15
+ | Reader's question | Diagram | Mode |
16
+ | --- | --- | --- |
17
+ | What are the units and how does material move between them? | BFD | `pfd` with `unit()` boxes |
18
+ | What equipment, at what conditions, with what mass balance? | PFD | `pfd` |
19
+ | How is it built, controlled, isolated and protected? | P&ID | `pid` |
20
+ | How is it arranged in space, and what does it look like? | Layout or isometric | `pfd` sheet in plan, or `iso` |
21
+ | What did the experiment use? (paper) | Apparatus figure | `journal` |
22
+
23
+ A BFD drawn with real equipment symbols, or a PFD drawn with every isolation valve, answers the wrong question and costs the reader time.
24
+
25
+ ## 2. Block flow diagram
26
+
27
+ Units are labelled boxes (`unit({ caption: "Hydrate formation" })`) of one size per level of importance, arranged left to right in process order; streams are `pipe` lines with arrows and short names (`"Feed gas, 5 MPa"` as a line number); recycles return along the bottom lane; utilities stay off the diagram unless they are the point. Use `createSheet({ mode: "pfd" })` and `streamNumber(line, n)` when a mass balance accompanies it.
28
+
29
+ ## 3. Process flow diagram
30
+
31
+ - **Equipment**: every major item with its tag and name in the header band, drawn with the same symbols as the P&ID; spares (`P-101B`) and minor items stay off.
32
+ - **Streams**: main process lines, heavy; each carries a **stream number** in a diamond (`sheet.streamNumber(line, 3)`), numbered in flow order. The stream number is the key into the stream table.
33
+ - **Conditions**: pressure and temperature flags at key points as short texts beside the line (`sheet.text(x, y, "5.0 MPa, 2 °C")`), or only in the table.
34
+ - **Control**: the loops that define the process (reactor pressure, jacket temperature, level), with their controllers and final elements; no isolation valves, drains or vents.
35
+ - **Utilities**: heating and cooling media as short utility lines into exchangers, labelled with the medium.
36
+
37
+ ## 4. Stream table
38
+
39
+ `streamTable({ box: [x0, y0, x1], streams: [1, 2, 3], rows: [{ label: "Pressure (MPa)", values: [0.1, 5.0, 5.0] }, …] })` draws one column per stream and one row per property inside the box and returns its bottom. Usual rows: phase, temperature, pressure, total mass flow, molar flow, composition of the key components. Put it below the drawing or across the bottom of the sheet; values come from the user's balance or simulation (HYSYS, Aspen, a spreadsheet), never from estimation without saying so. Units go in the row label, never in the cells.
40
+
41
+ ## 5. Equipment layout and plot plan
42
+
43
+ A plan view is a PFD-style canvas drawn to scale: `createSheet({ scale: "1:50" })` and coordinates in sheet millimetres = real millimetres ÷ 50. Equipment outlines are their real footprints (`unit` boxes for skids and packages, plain circles added with `canvas.raw()` for vessels seen from above), with aisles at least 1 m real, maintenance envelopes dashed (`zone`), and north arrow and grid lines when the site needs them. For a three-dimensional impression of the same arrangement use an isometric view (`references/isometric-craft.md`): boxes for skids, lathes for vessels, pipes along racks.
44
+
45
+ ## 6. Faults
46
+
47
+ | You see | Fix |
48
+ | --- | --- |
49
+ | A PFD full of block valves and drains | Remove them; they belong to the P&ID. |
50
+ | Stream numbers that skip or jump around | Number in flow order; recycles take the next free number. |
51
+ | A stream table without units | Units in the row labels. |
52
+ | Values that do not close the balance | Take them from the user's balance or say they are illustrative. |
53
+ | A BFD with equipment symbols | Use labelled boxes; the BFD is about units, not hardware. |