@litfamily/litopencode 1.0.31 → 1.0.32
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.
- package/CHANGELOG.md +24 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/dist/activation-probe.js +1 -0
- package/dist/activation-prompt-utils.d.ts +1 -1
- package/dist/activation-reentry.d.ts +20 -0
- package/dist/activation-reentry.js +110 -1
- package/dist/activation-routing.d.ts +2 -1
- package/dist/activation-routing.js +100 -11
- package/dist/activation-workflow-prompts.d.ts +4 -0
- package/dist/activation-workflow-prompts.js +30 -0
- package/dist/activation.d.ts +1 -1
- package/dist/activation.js +44 -5
- package/dist/adhd-next.d.ts +51 -0
- package/dist/adhd-next.js +150 -0
- package/dist/cli/managed-skill-assets.d.ts +1 -1
- package/dist/cli/managed-skill-assets.js +7 -1
- package/dist/cli/native-skill-integrity.js +1 -1
- package/dist/cli.js +7 -1
- package/dist/commands.d.ts +28 -1
- package/dist/commands.js +48 -1
- package/dist/features.d.ts +80 -1
- package/dist/features.js +105 -1
- package/dist/ledger.js +12 -1
- package/dist/secret-shapes.js +5 -5
- package/dist/skills.d.ts +22 -1
- package/dist/skills.js +36 -0
- package/dist/start-work-typed.d.ts +34 -0
- package/dist/start-work-typed.js +156 -0
- package/dist/state.d.ts +2 -0
- package/dist/state.js +2 -0
- package/dist/tools.js +22 -2
- package/dist/tui/mission-band.d.ts +20 -1
- package/dist/tui/mission-band.js +73 -16
- package/dist/tui/mission-looks.d.ts +38 -0
- package/dist/tui/mission-looks.js +73 -0
- package/dist/tui/mission-model.d.ts +2 -0
- package/dist/tui/mission-model.js +3 -1
- package/dist/tui/mission-rainbow.d.ts +33 -0
- package/dist/tui/mission-rainbow.js +90 -0
- package/dist/tui/mission-source.d.ts +48 -0
- package/dist/tui/mission-source.js +184 -2
- package/dist/tui/mission-text.d.ts +2 -0
- package/dist/tui/mission-text.js +9 -1
- package/dist/tui/mission-threads.d.ts +50 -0
- package/dist/tui/mission-threads.js +119 -0
- package/dist/tui.js +84 -6
- package/dist/uiux-visual-catalog.js +8 -1
- package/dist/v2/session-hooks.js +38 -5
- package/dist/workflow-stack.d.ts +53 -0
- package/dist/workflow-stack.js +196 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/assets/skills/lit-schematic.webp +0 -0
- package/docs/reference-Ko-KR.md +30 -2
- package/docs/reference.md +34 -2
- package/package.json +1 -1
- package/skills/frontend-ui-ux/SKILL.md +3 -2
- package/skills/frontend-ui-ux/kit/components.css +414 -0
- package/skills/frontend-ui-ux/kit/fonts.json +22 -0
- package/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/skills/frontend-ui-ux/references/brand-and-imagery.md +2 -2
- package/skills/frontend-ui-ux/references/complete-contract.md +2 -1
- package/skills/frontend-ui-ux/references/craft-floor.md +6 -2
- package/skills/frontend-ui-ux/references/default-editorial-pixel.json +3 -0
- package/skills/frontend-ui-ux/references/house-styles.md +236 -0
- package/skills/frontend-ui-ux/references/motion-guide.md +2 -2
- package/skills/frontend-ui-ux/references/production.md +3 -1
- package/skills/frontend-ui-ux/references/slop-register.md +4 -4
- package/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
- package/skills/frontend-ui-ux/scripts/probe-lock.mjs +235 -0
- package/skills/frontend-ui-ux/scripts/probe-page.mjs +34 -12
- package/skills/frontend-ui-ux/scripts/probe-static.mjs +16 -3
- package/skills/frontend-ui-ux/scripts/probe-thresholds.json +8 -2
- package/skills/frontend-ui-ux/scripts/probe.mjs +35 -7
- package/skills/lit-adhd/SKILL.md +208 -0
- package/skills/lit-adhd/references/brief-format.md +43 -0
- package/skills/lit-adhd/references/card-examples.md +82 -0
- package/skills/lit-adhd/references/status-taxonomy.md +29 -0
- package/skills/lit-adhd/scripts/rewind.mjs +835 -0
- package/skills/lit-diagram-drawer/SKILL.md +4 -3
- package/skills/lit-diagram-drawer/references/style-guide.md +20 -11
- package/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
- package/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
- package/skills/lit-diagram-drawer/references/type-tree.md +3 -3
- package/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
- package/skills/lit-diagram-drawer/references/verifier-guide.md +1 -0
- package/skills/lit-diagram-drawer/scripts/craft-thresholds.json +1 -2
- package/skills/lit-diagram-drawer/scripts/visual-quality.mjs +1 -2
- package/skills/lit-docx/SKILL.md +2 -1
- package/skills/lit-handoff/SKILL.md +2 -0
- package/skills/lit-hijack/SKILL.md +422 -0
- package/skills/lit-hijack/references/capture-and-provenance.md +87 -0
- package/skills/lit-hijack/references/clean-room-build.md +111 -0
- package/skills/lit-hijack/references/distil-and-licence.md +100 -0
- package/skills/lit-hijack/references/gap-and-port-design.md +126 -0
- package/skills/lit-hijack/references/parity-and-report.md +110 -0
- package/skills/lit-hijack/scripts/hijack.mjs +993 -0
- package/skills/lit-plan/SKILL.md +2 -0
- package/skills/lit-pptx/SKILL.md +27 -11
- package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/skills/lit-pptx/fonts/jetbrains-mono/SOURCE.md +17 -0
- package/skills/lit-pptx/references/density-and-fill.md +9 -3
- package/skills/lit-pptx/references/direction-step.md +21 -6
- package/skills/lit-pptx/references/examples/04-lecture-chalk-ko.md +3 -6
- package/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +1 -5
- package/skills/lit-pptx/references/examples/08-reliability-keynote-night-en.md +0 -4
- package/skills/lit-pptx/references/examples/09-status-update-ledger-en.md +2 -2
- package/skills/lit-pptx/references/examples/11-forecast-review-night-ko.md +4 -7
- package/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
- package/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
- package/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
- package/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
- package/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
- package/skills/lit-pptx/references/layout-families.md +65 -47
- package/skills/lit-pptx/references/title-treatments.md +83 -134
- package/skills/lit-pptx/references/tonalities/atlas.md +3 -3
- package/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
- package/skills/lit-pptx/references/tonalities/chalk.md +7 -6
- package/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
- package/skills/lit-pptx/references/tonalities/gazette.md +1 -1
- package/skills/lit-pptx/references/tonalities/ledger.md +1 -1
- package/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
- package/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
- package/skills/lit-pptx/references/tonalities/night.md +1 -1
- package/skills/lit-pptx/references/tonalities/paper.md +2 -2
- package/skills/lit-pptx/references/tonalities/riso.md +137 -0
- package/skills/lit-pptx/references/tonalities/signal.md +3 -3
- package/skills/lit-pptx/references/tonalities/studio.md +7 -6
- package/skills/lit-pptx/run.mjs +2 -1
- package/skills/lit-pptx/scripts/compile-deck.js +1 -1
- package/skills/lit-pptx/scripts/craft-thresholds.json +4 -8
- package/skills/lit-pptx/scripts/craft_extras.py +56 -3
- package/skills/lit-pptx/scripts/deck_output.py +75 -90
- package/skills/lit-pptx/scripts/embed_fonts.py +13 -1
- package/skills/lit-pptx/scripts/layout_inventory.py +12 -5
- package/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
- package/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
- package/skills/lit-pptx/scripts/lib/render-pack.js +373 -566
- package/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
- package/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
- package/skills/lit-pptx/scripts/qa_deck.py +10 -11
- package/skills/lit-pptx/scripts/render_pages.py +118 -0
- package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
- package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
- package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
- package/skills/lit-recap/SKILL.md +4 -0
- package/skills/lit-schematic/SKILL.md +118 -0
- package/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
- package/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
- package/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
- package/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
- package/skills/lit-schematic/references/cutaway-outputs.md +70 -0
- package/skills/lit-schematic/references/isometric-craft.md +101 -0
- package/skills/lit-schematic/references/journal-figures.md +72 -0
- package/skills/lit-schematic/references/kit-api.md +134 -0
- package/skills/lit-schematic/references/pfd-and-layout.md +53 -0
- package/skills/lit-schematic/references/pid-drafting.md +103 -0
- package/skills/lit-schematic/references/pid-symbols.md +163 -0
- package/skills/lit-schematic/references/verification.md +100 -0
- package/skills/lit-schematic/scripts/doctor.mjs +57 -0
- package/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
- package/skills/lit-schematic/scripts/lib/fonts.mjs +25 -0
- package/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
- package/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
- package/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
- package/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
- package/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
- package/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
- package/skills/lit-schematic/scripts/lib/png.mjs +132 -0
- package/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
- package/skills/lit-schematic/scripts/lib/route.mjs +218 -0
- package/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
- package/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
- package/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
- package/skills/lit-schematic/scripts/motion.mjs +129 -0
- package/skills/lit-schematic/scripts/qa.mjs +92 -0
- package/skills/lit-schematic/scripts/render.mjs +233 -0
- package/skills/lit-schematic/scripts/scaffold.mjs +60 -0
- package/skills/lit-scientific-visualization/SKILL.md +27 -4
- package/skills/lit-typographic-motion/SKILL.md +19 -17
- package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +9 -0
- package/skills/lit-typographic-motion/engine/accents.mjs +6 -0
- package/skills/lit-typographic-motion/engine/assets.mjs +282 -0
- package/skills/lit-typographic-motion/engine/blender/runner.py +228 -0
- package/skills/lit-typographic-motion/engine/blender.mjs +126 -0
- package/skills/lit-typographic-motion/engine/board.mjs +138 -0
- package/skills/lit-typographic-motion/engine/cover/assets.mjs +11 -8
- package/skills/lit-typographic-motion/engine/cover/capture.mjs +6 -6
- package/skills/lit-typographic-motion/engine/cover/render.mjs +20 -7
- package/skills/lit-typographic-motion/engine/cover/treatment.mjs +30 -3
- package/skills/lit-typographic-motion/engine/fill.mjs +54 -0
- package/skills/lit-typographic-motion/engine/gallery.mjs +118 -0
- package/skills/lit-typographic-motion/engine/house.mjs +42 -0
- package/skills/lit-typographic-motion/engine/look.mjs +65 -8
- package/skills/lit-typographic-motion/engine/loudness.mjs +101 -0
- package/skills/lit-typographic-motion/engine/scaffold.mjs +219 -0
- package/skills/lit-typographic-motion/engine/score.mjs +594 -0
- package/skills/lit-typographic-motion/engine/sound.mjs +9 -57
- package/skills/lit-typographic-motion/engine/stage/capture.mjs +23 -9
- package/skills/lit-typographic-motion/engine/stage/clock.js +8 -2
- package/skills/lit-typographic-motion/engine/stage/cover-init.js +12 -1
- package/skills/lit-typographic-motion/engine/stage/frame-worker.mjs +15 -7
- package/skills/lit-typographic-motion/engine/stage/gate.mjs +48 -4
- package/skills/lit-typographic-motion/engine/stage/qa-page.js +16 -5
- package/skills/lit-typographic-motion/engine/stage/qa.mjs +148 -7
- package/skills/lit-typographic-motion/engine/stage/render.mjs +69 -7
- package/skills/lit-typographic-motion/engine/stage/scan.mjs +20 -1
- package/skills/lit-typographic-motion/engine/stage/stage-kit.js +85 -4
- package/skills/lit-typographic-motion/engine/stills.mjs +1 -1
- package/skills/lit-typographic-motion/engine/styles.mjs +221 -0
- package/skills/lit-typographic-motion/engine/text.mjs +1 -1
- package/skills/lit-typographic-motion/engine/treatment.mjs +190 -6
- package/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
- package/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
- package/skills/lit-typographic-motion/gate.mjs +18 -2
- package/skills/lit-typographic-motion/probe.mjs +13 -1
- package/skills/lit-typographic-motion/recipes/hero-3d/expected.json +64 -0
- package/skills/lit-typographic-motion/recipes/hero-3d/index.html +132 -0
- package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +2 -0
- package/skills/lit-typographic-motion/recipes/painted-light/expected.json +2 -0
- package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +2 -0
- package/skills/lit-typographic-motion/references/cover.md +56 -13
- package/skills/lit-typographic-motion/references/craft-loop.md +20 -7
- package/skills/lit-typographic-motion/references/depth.md +112 -0
- package/skills/lit-typographic-motion/references/runtime.md +21 -6
- package/skills/lit-typographic-motion/references/sound.md +76 -0
- package/skills/lit-typographic-motion/references/stage.md +50 -7
- package/skills/lit-typographic-motion/references/studio.md +162 -0
- package/skills/lit-typographic-motion/references/style-bibles.md +18 -0
- package/skills/lit-typographic-motion/references/treatment.md +108 -9
- package/skills/lit-typographic-motion/render.mjs +60 -3
- package/skills/lit-typographic-motion/styles/paper.json +185 -0
- package/skills/lit-typographic-motion/styles/swiss-signal.json +188 -0
- package/skills/lit-typographic-motion/styles/terminalcore.json +182 -0
- package/skills/lit-typographic-motion/styles/tidal.json +187 -0
- package/skills/litresearch/SKILL.md +2 -0
- package/skills/managed-skill-manifest.json +517 -88
- package/skills/readme-studio/SKILL.md +1 -1
- package/skills/review-work/SKILL.md +2 -0
- package/skills/start-work/SKILL.md +8 -0
- package/skills/workflow-loop/SKILL.md +4 -0
- package/tools/check-pack-payload.mjs +4 -2
- package/tools/payload-substance-parity.json +4 -3
- package/tools/qa-cover-recipes.mjs +4 -3
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
// P&ID and PFD symbol library after ISO 10628-2 and ANSI/ISA-5.1 conventions, drawn in
|
|
2
|
+
// millimetres for a 1:1 sheet. Every symbol is an object with:
|
|
3
|
+
// kind a stable id ("valve.gate", "pump.centrifugal") the legend and the QA gate read,
|
|
4
|
+
// box its overall extent, boxes its obstacle parts (for routing and LINE.THROUGH_SYMBOL),
|
|
5
|
+
// port() named connection points with an outward axis-aligned normal,
|
|
6
|
+
// svg() the drawing, wrapped in <g data-sym data-box data-ports>.
|
|
7
|
+
// Coordinates are absolute sheet millimetres; (x, y) is the symbol centre unless noted.
|
|
8
|
+
|
|
9
|
+
import { el, esc, n, pathOf } from "./svg.mjs";
|
|
10
|
+
import { parseEquipmentTag, parseInstrumentTag } from "./isa.mjs";
|
|
11
|
+
import { FONT_LATIN, PID, fontFor } from "./theme.mjs";
|
|
12
|
+
|
|
13
|
+
const W = PID.weight;
|
|
14
|
+
const UP = [0, -1];
|
|
15
|
+
const DOWN = [0, 1];
|
|
16
|
+
const LEFT = [-1, 0];
|
|
17
|
+
const RIGHT = [1, 0];
|
|
18
|
+
const SIDE_DIR = { left: LEFT, right: RIGHT, top: UP, bottom: DOWN };
|
|
19
|
+
|
|
20
|
+
const rot = (angle) => {
|
|
21
|
+
const a = ((angle % 360) + 360) % 360;
|
|
22
|
+
const c = Math.round(Math.cos((a * Math.PI) / 180));
|
|
23
|
+
const s = Math.round(Math.sin((a * Math.PI) / 180));
|
|
24
|
+
return ([x, y]) => [x * c - y * s, x * s + y * c];
|
|
25
|
+
};
|
|
26
|
+
const ORIENT = { right: 0, down: 90, left: 180, up: 270 };
|
|
27
|
+
|
|
28
|
+
export const KIND_DESCRIPTIONS = Object.freeze({
|
|
29
|
+
"vessel.vertical": "Vertical vessel",
|
|
30
|
+
"vessel.reactor": "Stirred reactor",
|
|
31
|
+
"vessel.jacketed": "Jacketed vessel",
|
|
32
|
+
"vessel.horizontal": "Horizontal drum",
|
|
33
|
+
"vessel.column": "Column",
|
|
34
|
+
"vessel.tank": "Atmospheric tank",
|
|
35
|
+
"gas.cylinder": "Gas cylinder",
|
|
36
|
+
"pump.centrifugal": "Centrifugal pump",
|
|
37
|
+
"pump.positive": "Positive-displacement pump (rotary)",
|
|
38
|
+
"pump.metering": "Metering pump",
|
|
39
|
+
"compressor.centrifugal": "Centrifugal compressor",
|
|
40
|
+
"compressor.reciprocating": "Reciprocating compressor / gas booster",
|
|
41
|
+
"exchanger.general": "Heat exchanger",
|
|
42
|
+
"exchanger.shelltube": "Shell-and-tube exchanger, fixed tube sheets",
|
|
43
|
+
"exchanger.floating": "Shell-and-tube exchanger, floating head",
|
|
44
|
+
"exchanger.utube": "Shell-and-tube exchanger, U-tubes",
|
|
45
|
+
"exchanger.plate": "Plate exchanger",
|
|
46
|
+
"exchanger.aircooler": "Air-cooled exchanger (fin fan)",
|
|
47
|
+
"package.unit": "Packaged unit",
|
|
48
|
+
"valve.gate": "Gate valve",
|
|
49
|
+
"valve.globe": "Globe valve",
|
|
50
|
+
"valve.ball": "Ball valve",
|
|
51
|
+
"valve.butterfly": "Butterfly valve",
|
|
52
|
+
"valve.check": "Check valve",
|
|
53
|
+
"valve.needle": "Needle valve",
|
|
54
|
+
"valve.plug": "Plug valve",
|
|
55
|
+
"valve.diaphragm": "Diaphragm valve",
|
|
56
|
+
"valve.threeway": "Three-way valve",
|
|
57
|
+
"valve.angle": "Angle valve",
|
|
58
|
+
"valve.regulator": "Pressure-reducing regulator",
|
|
59
|
+
"valve.backpressure": "Back-pressure regulator",
|
|
60
|
+
"valve.safety": "Pressure safety or relief valve",
|
|
61
|
+
"relief.rupture": "Rupture disk",
|
|
62
|
+
"flow.orifice": "Orifice plate / restriction orifice",
|
|
63
|
+
"flow.venturi": "Venturi tube",
|
|
64
|
+
"flow.magnetic": "Magnetic flowmeter (MAG)",
|
|
65
|
+
"flow.coriolis": "Coriolis mass flowmeter (COR)",
|
|
66
|
+
"flow.vortex": "Vortex flowmeter (VOR)",
|
|
67
|
+
"flow.turbine": "Turbine flowmeter (TUR)",
|
|
68
|
+
"flow.ultrasonic": "Ultrasonic flowmeter (US)",
|
|
69
|
+
"flow.thermal": "Thermal mass flowmeter (TMF)",
|
|
70
|
+
"flow.rotameter": "Variable-area flowmeter",
|
|
71
|
+
"flow.mfc": "Mass flow controller",
|
|
72
|
+
"line.filter": "Inline filter",
|
|
73
|
+
"line.reducer": "Concentric reducer",
|
|
74
|
+
"line.reducer-eccentric": "Eccentric reducer",
|
|
75
|
+
"line.flange": "Flange pair",
|
|
76
|
+
"line.sightglass": "Sight glass",
|
|
77
|
+
"line.blind": "Spectacle blind, normally closed",
|
|
78
|
+
"line.blind-open": "Spectacle blind, normally open",
|
|
79
|
+
"logic.interlock": "Interlock or logic",
|
|
80
|
+
"terminal.vent": "Vent to atmosphere",
|
|
81
|
+
"terminal.drain": "Drain",
|
|
82
|
+
"terminal.sample": "Sample point",
|
|
83
|
+
"terminal.offpage": "Off-page connector",
|
|
84
|
+
"terminal.utility": "Utility connection",
|
|
85
|
+
"drive.motor": "Electric motor",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ANSI/ISA-5.1-2009 Table 5.4.2 actuators and Table 5.1.1 function shapes and locations.
|
|
89
|
+
export const ACTUATORS = Object.freeze({ diaphragm: "spring-diaphragm actuator", piston: "piston actuator", motor: "electric motor actuator", solenoid: "solenoid actuator", hand: "handwheel" });
|
|
90
|
+
export const FUNCTIONS = Object.freeze({ discrete: "Discrete instrument", shared: "Shared display and control (DCS)", computer: "Computer function", plc: "PLC or safety-system logic" });
|
|
91
|
+
export const LOCATIONS = Object.freeze({
|
|
92
|
+
field: "field mounted",
|
|
93
|
+
main: "main control room, operator accessible",
|
|
94
|
+
"main-rear": "main control room, not operator accessible",
|
|
95
|
+
aux: "auxiliary or local panel, operator accessible",
|
|
96
|
+
"aux-rear": "auxiliary or local panel, not operator accessible",
|
|
97
|
+
});
|
|
98
|
+
// Table 5.4.4: fail open, fail closed, fail locked (last position), fail indeterminate.
|
|
99
|
+
export const FAIL_POSITIONS = Object.freeze({ FO: "fail open", FC: "fail closed", FL: "fail locked in last position", FI: "fail indeterminate" });
|
|
100
|
+
|
|
101
|
+
// Shorter wording for the legend column of a single sheet, where space is tight.
|
|
102
|
+
const SHORT = Object.freeze({
|
|
103
|
+
actuator: { diaphragm: "diaphragm actuator", piston: "piston actuator", motor: "motor actuator", solenoid: "solenoid", hand: "handwheel" },
|
|
104
|
+
fn: { discrete: "Instrument", shared: "DCS function", computer: "Computer function", plc: "PLC / SIS function" },
|
|
105
|
+
location: { field: "field", main: "control room", "main-rear": "control room, behind panel", aux: "local panel", "aux-rear": "local panel, behind" },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// The legend text for any kind this library draws, including composed ones such as
|
|
109
|
+
// "valve.globe.diaphragm" and "instrument.shared-main".
|
|
110
|
+
export function describeKind(kind, { short = false } = {}) {
|
|
111
|
+
if (KIND_DESCRIPTIONS[kind]) return KIND_DESCRIPTIONS[kind];
|
|
112
|
+
const valve = /^valve\.([a-z]+)\.([a-z]+)$/u.exec(kind ?? "");
|
|
113
|
+
if (valve && KIND_DESCRIPTIONS[`valve.${valve[1]}`] && ACTUATORS[valve[2]]) return `${KIND_DESCRIPTIONS[`valve.${valve[1]}`]}, ${(short ? SHORT.actuator : ACTUATORS)[valve[2]]}`;
|
|
114
|
+
const bubble = /^instrument\.([a-z]+)-([a-z-]+)$/u.exec(kind ?? "");
|
|
115
|
+
if (bubble && FUNCTIONS[bubble[1]] && LOCATIONS[bubble[2]]) return short ? `${SHORT.fn[bubble[1]]}, ${SHORT.location[bubble[2]]}` : `${FUNCTIONS[bubble[1]]}, ${LOCATIONS[bubble[2]]}`;
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let counter = 0;
|
|
120
|
+
export const resetIds = () => { counter = 0; };
|
|
121
|
+
|
|
122
|
+
function symbol({ kind, tag = null, box, boxes = null, ports = {}, body, sidePort = null, label = null, meta = {} }) {
|
|
123
|
+
if (!describeKind(kind)) throw new Error(`pid: unknown symbol kind ${kind}`);
|
|
124
|
+
const id = `S${++counter}`;
|
|
125
|
+
const used = new Map();
|
|
126
|
+
const obstacleBoxes = boxes ?? [box];
|
|
127
|
+
const sym = {
|
|
128
|
+
id, kind, tag, box, boxes: obstacleBoxes, label, meta,
|
|
129
|
+
port(name, t) {
|
|
130
|
+
let p = ports[name];
|
|
131
|
+
if (!p && sidePort && SIDE_DIR[name] && t !== undefined) p = sidePort(name, t);
|
|
132
|
+
if (!p) throw new Error(`pid: ${kind}${tag ? ` ${tag}` : ""} has no port "${name}"${sidePort ? " (side ports take a fraction 0..1: port('left', 0.3))" : ""}; ports: ${Object.keys(ports).join(", ")}`);
|
|
133
|
+
const key = t === undefined ? name : `${name}@${n(t)}`;
|
|
134
|
+
const port = { x: p[0], y: p[1], dir: p[2], owner: id, name: key };
|
|
135
|
+
used.set(key, port);
|
|
136
|
+
return port;
|
|
137
|
+
},
|
|
138
|
+
get usedPorts() { return [...used.values()]; },
|
|
139
|
+
get allPorts() { return Object.entries(ports).map(([name, p]) => ({ name, x: p[0], y: p[1], dir: p[2] })); },
|
|
140
|
+
svg() {
|
|
141
|
+
const declared = [...Object.entries(ports).map(([name, p]) => [name, p]), ...[...used.values()].map((p) => [p.name, [p.x, p.y, p.dir]])];
|
|
142
|
+
const uniq = new Map(declared);
|
|
143
|
+
return el("g", {
|
|
144
|
+
"data-sym": kind,
|
|
145
|
+
"data-sym-id": id,
|
|
146
|
+
"data-tag": tag ?? undefined,
|
|
147
|
+
"data-box": obstacleBoxes.map((b) => b.map(n).join(" ")).join("|"),
|
|
148
|
+
"data-ports": [...uniq].map(([name, p]) => `${name}:${n(p[0])},${n(p[1])},${p[2][0]},${p[2][1]}`).join(";"),
|
|
149
|
+
fill: "none",
|
|
150
|
+
stroke: PID.ink,
|
|
151
|
+
"stroke-width": W.symbol,
|
|
152
|
+
"stroke-linecap": "round",
|
|
153
|
+
"stroke-linejoin": "round",
|
|
154
|
+
}, body);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
return sym;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const line = (points, extra = {}) => el("path", { d: pathOf(points), ...extra });
|
|
161
|
+
const poly = (points, extra = {}) => el("path", { d: pathOf(points, true), ...extra });
|
|
162
|
+
const circle = (cx, cy, r, extra = {}) => el("circle", { cx, cy, r, ...extra });
|
|
163
|
+
const label = (x, y, text, size = PID.text.tag, extra = {}) =>
|
|
164
|
+
el("text", { x, y, "font-family": fontFor(text), "font-size": size, "text-anchor": "middle", fill: PID.ink, stroke: "none", "data-label": "symbol", ...extra }, esc(text));
|
|
165
|
+
|
|
166
|
+
function checkEquipmentTag(tag) {
|
|
167
|
+
if (!tag) return;
|
|
168
|
+
const parsed = parseEquipmentTag(tag);
|
|
169
|
+
if (!parsed.ok) throw new Error(`pid: equipment tag "${tag}": ${parsed.error}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------- vessels
|
|
173
|
+
|
|
174
|
+
export function vessel({
|
|
175
|
+
x, y, w = 24, h = 48, tag = null, name = null, top = "ellipse", bottom = "ellipse",
|
|
176
|
+
jacket = false, agitator = null, coil = false, packing = false, trays = 0, trayStyle = "full", demister = false,
|
|
177
|
+
supports = null, liquid = null, kind,
|
|
178
|
+
}) {
|
|
179
|
+
checkEquipmentTag(tag);
|
|
180
|
+
const half = w / 2;
|
|
181
|
+
const headDepth = w / 4;
|
|
182
|
+
const topT = y - h / 2 + (top === "flat" ? 0 : headDepth);
|
|
183
|
+
const botT = y + h / 2 - (bottom === "flat" ? 0 : headDepth);
|
|
184
|
+
const parts = [];
|
|
185
|
+
// Shell: left side down, bottom head, right side up, top head.
|
|
186
|
+
let d = `M${n(x - half)} ${n(topT)}L${n(x - half)} ${n(botT)}`;
|
|
187
|
+
if (bottom === "ellipse") d += `A${n(half)} ${n(headDepth)} 0 0 0 ${n(x + half)} ${n(botT)}`;
|
|
188
|
+
else if (bottom === "cone") d += `L${n(x)} ${n(y + h / 2)}L${n(x + half)} ${n(botT)}`;
|
|
189
|
+
else d += `L${n(x + half)} ${n(botT)}`;
|
|
190
|
+
d += `L${n(x + half)} ${n(topT)}`;
|
|
191
|
+
if (top === "ellipse") d += `A${n(half)} ${n(headDepth)} 0 0 0 ${n(x - half)} ${n(topT)}`;
|
|
192
|
+
else if (top === "cone") d += `L${n(x)} ${n(y - h / 2)}L${n(x - half)} ${n(topT)}`;
|
|
193
|
+
else d += `L${n(x - half)} ${n(topT)}`;
|
|
194
|
+
parts.push(el("path", { d: `${d}Z`, "stroke-width": W.equipment, fill: PID.paper }));
|
|
195
|
+
if (top !== "flat") parts.push(line([[x - half, topT], [x + half, topT]], { "stroke-width": W.detail }));
|
|
196
|
+
if (bottom !== "flat") parts.push(line([[x - half, botT], [x + half, botT]], { "stroke-width": W.detail }));
|
|
197
|
+
if (liquid !== null) {
|
|
198
|
+
const ly = topT + (botT - topT) * (1 - liquid);
|
|
199
|
+
parts.push(line([[x - half, ly], [x + half, ly]], { "stroke-width": W.detail, "stroke-dasharray": "1.2 0.8" }));
|
|
200
|
+
parts.push(poly([[x + half - 4, ly - 1.4], [x + half - 2.6, ly - 1.4], [x + half - 3.3, ly - 0.2]], { "stroke-width": W.detail }));
|
|
201
|
+
}
|
|
202
|
+
let boxTop = y - h / 2;
|
|
203
|
+
let boxBottom = y + h / 2;
|
|
204
|
+
let boxLeft = x - half;
|
|
205
|
+
let boxRight = x + half;
|
|
206
|
+
const boxes = [];
|
|
207
|
+
const ports = {
|
|
208
|
+
top: [x, y - h / 2, UP],
|
|
209
|
+
bottom: [x, y + h / 2, DOWN],
|
|
210
|
+
};
|
|
211
|
+
const headY = (dx, upward) => {
|
|
212
|
+
const k = Math.min(1, Math.abs(dx) / half);
|
|
213
|
+
const yt = upward ? topT : botT;
|
|
214
|
+
const shape = upward ? top : bottom;
|
|
215
|
+
if (shape === "flat") return yt;
|
|
216
|
+
if (shape === "cone") return yt + (upward ? -1 : 1) * headDepth * (1 - k);
|
|
217
|
+
return yt + (upward ? -1 : 1) * headDepth * Math.sqrt(1 - k * k);
|
|
218
|
+
};
|
|
219
|
+
ports["top-left"] = [x - half / 2, headY(-half / 2, true), UP];
|
|
220
|
+
ports["top-right"] = [x + half / 2, headY(half / 2, true), UP];
|
|
221
|
+
ports["bottom-left"] = [x - half / 2, headY(-half / 2, false), DOWN];
|
|
222
|
+
ports["bottom-right"] = [x + half / 2, headY(half / 2, false), DOWN];
|
|
223
|
+
let jacketTop = null;
|
|
224
|
+
const j = 2.6;
|
|
225
|
+
if (jacket) {
|
|
226
|
+
jacketTop = topT + (botT - topT) * 0.22;
|
|
227
|
+
let jd = `M${n(x - half - j)} ${n(jacketTop)}L${n(x - half - j)} ${n(botT)}`;
|
|
228
|
+
if (bottom === "ellipse") jd += `A${n(half + j)} ${n(headDepth + j)} 0 0 0 ${n(x + half + j)} ${n(botT)}`;
|
|
229
|
+
else jd += `L${n(x + half + j)} ${n(botT + (bottom === "flat" ? j : 0))}`;
|
|
230
|
+
jd += `L${n(x + half + j)} ${n(jacketTop)}`;
|
|
231
|
+
parts.unshift(el("path", { d: jd, "stroke-width": W.equipment }));
|
|
232
|
+
parts.push(line([[x - half - j, jacketTop], [x - half, jacketTop]], { "stroke-width": W.equipment }));
|
|
233
|
+
parts.push(line([[x + half, jacketTop], [x + half + j, jacketTop]], { "stroke-width": W.equipment }));
|
|
234
|
+
boxLeft -= j;
|
|
235
|
+
boxRight += j;
|
|
236
|
+
boxBottom = Math.max(boxBottom, botT + (bottom === "ellipse" ? headDepth + j : j));
|
|
237
|
+
ports["jacket-in"] = [x + half + j, botT - (botT - jacketTop) * 0.12, RIGHT];
|
|
238
|
+
ports["jacket-out"] = [x - half - j, jacketTop + (botT - jacketTop) * 0.12, LEFT];
|
|
239
|
+
}
|
|
240
|
+
if (coil) {
|
|
241
|
+
const c0 = topT + (botT - topT) * 0.45;
|
|
242
|
+
const c1 = botT - 2;
|
|
243
|
+
const zig = [];
|
|
244
|
+
const count = 7;
|
|
245
|
+
for (let i = 0; i <= count; i++) zig.push([x + (i % 2 ? half - 3 : -half + 3), c0 + ((c1 - c0) * i) / count]);
|
|
246
|
+
parts.push(line(zig, { "stroke-width": W.detail }));
|
|
247
|
+
}
|
|
248
|
+
if (packing) {
|
|
249
|
+
const p0 = topT + (botT - topT) * 0.3;
|
|
250
|
+
const p1 = topT + (botT - topT) * 0.75;
|
|
251
|
+
parts.push(poly([[x - half, p0], [x + half, p0], [x + half, p1], [x - half, p1]], { "stroke-width": W.detail }));
|
|
252
|
+
parts.push(line([[x - half, p0], [x + half, p1]], { "stroke-width": W.detail }));
|
|
253
|
+
parts.push(line([[x + half, p0], [x - half, p1]], { "stroke-width": W.detail }));
|
|
254
|
+
}
|
|
255
|
+
if (demister) {
|
|
256
|
+
const m = topT + 3;
|
|
257
|
+
parts.push(poly([[x - half, m], [x + half, m], [x + half, m + 2.2], [x - half, m + 2.2]], { "stroke-width": W.detail }));
|
|
258
|
+
for (let i = 1; i < 6; i++) parts.push(line([[x - half + (w * i) / 6 - 1, m], [x - half + (w * i) / 6 + 1, m + 2.2]], { "stroke-width": W.detail }));
|
|
259
|
+
}
|
|
260
|
+
// ISO 10628-2 item 2.2 draws trays across the whole shell; item 2.5 staggers baffles.
|
|
261
|
+
for (let i = 0; i < trays; i++) {
|
|
262
|
+
const ty = topT + ((botT - topT) * (i + 1)) / (trays + 1);
|
|
263
|
+
const fromLeft = i % 2 === 0;
|
|
264
|
+
const run = trayStyle === "staggered"
|
|
265
|
+
? (fromLeft ? [[x - half, ty], [x + half * 0.5, ty]] : [[x - half * 0.5, ty], [x + half, ty]])
|
|
266
|
+
: [[x - half, ty], [x + half, ty]];
|
|
267
|
+
parts.push(line(run, { "stroke-width": W.detail, "stroke-dasharray": "2.2 1" }));
|
|
268
|
+
}
|
|
269
|
+
if (agitator) {
|
|
270
|
+
const motorY = y - h / 2 - 6.5;
|
|
271
|
+
const shaftTop = motorY + 3.2;
|
|
272
|
+
const shaftBottom = topT + (botT - topT) * 0.82;
|
|
273
|
+
parts.push(line([[x, shaftTop], [x, shaftBottom]], { "stroke-width": W.symbol }));
|
|
274
|
+
parts.push(circle(x, motorY, 3.2, { fill: PID.paper }));
|
|
275
|
+
parts.push(label(x, motorY + 0.9, "M", 2.4));
|
|
276
|
+
if (agitator === "anchor") {
|
|
277
|
+
parts.push(line([[x - half + 2.5, shaftBottom - 7], [x - half + 2.5, shaftBottom], [x + half - 2.5, shaftBottom], [x + half - 2.5, shaftBottom - 7]]));
|
|
278
|
+
} else if (agitator === "propeller") {
|
|
279
|
+
parts.push(poly([[x, shaftBottom], [x - 5, shaftBottom - 1.6], [x - 5, shaftBottom + 0.6]]));
|
|
280
|
+
parts.push(poly([[x, shaftBottom], [x + 5, shaftBottom + 1.6], [x + 5, shaftBottom - 0.6]]));
|
|
281
|
+
} else {
|
|
282
|
+
parts.push(line([[x - 5.5, shaftBottom], [x + 5.5, shaftBottom]]));
|
|
283
|
+
parts.push(poly([[x - 5.5, shaftBottom - 1.8], [x - 3.3, shaftBottom - 1.8], [x - 3.3, shaftBottom + 1.8], [x - 5.5, shaftBottom + 1.8]]));
|
|
284
|
+
parts.push(poly([[x + 3.3, shaftBottom - 1.8], [x + 5.5, shaftBottom - 1.8], [x + 5.5, shaftBottom + 1.8], [x + 3.3, shaftBottom + 1.8]]));
|
|
285
|
+
}
|
|
286
|
+
boxes.push([x - 3.4, motorY - 3.4, x + 3.4, y - h / 2]);
|
|
287
|
+
boxTop = motorY - 3.4;
|
|
288
|
+
}
|
|
289
|
+
if (supports === "legs") {
|
|
290
|
+
const legBottom = boxBottom + 4;
|
|
291
|
+
for (const sx of [x - half * 0.75, x + half * 0.75]) {
|
|
292
|
+
const top0 = headY(sx - x, false);
|
|
293
|
+
parts.push(line([[sx, top0], [sx, legBottom]], { "stroke-width": W.equipment }));
|
|
294
|
+
parts.push(line([[sx - 1.6, legBottom], [sx + 1.6, legBottom]], { "stroke-width": W.equipment }));
|
|
295
|
+
}
|
|
296
|
+
boxBottom = legBottom;
|
|
297
|
+
} else if (supports === "skirt") {
|
|
298
|
+
const skBottom = boxBottom + 5;
|
|
299
|
+
parts.push(line([[x - half * 0.8, headY(-half * 0.8, false)], [x - half * 0.8, skBottom]], { "stroke-width": W.equipment }));
|
|
300
|
+
parts.push(line([[x + half * 0.8, headY(half * 0.8, false)], [x + half * 0.8, skBottom]], { "stroke-width": W.equipment }));
|
|
301
|
+
parts.push(line([[x - half * 0.8 - 1.5, skBottom], [x + half * 0.8 + 1.5, skBottom]], { "stroke-width": W.equipment }));
|
|
302
|
+
boxBottom = skBottom;
|
|
303
|
+
}
|
|
304
|
+
const shellBox = [boxLeft, y - h / 2, boxRight, boxBottom];
|
|
305
|
+
boxes.unshift(shellBox);
|
|
306
|
+
const resolvedKind = kind ?? (agitator ? "vessel.reactor" : jacket ? "vessel.jacketed" : trays || packing ? "vessel.column" : top === "cone" && bottom === "flat" ? "vessel.tank" : "vessel.vertical");
|
|
307
|
+
return symbol({
|
|
308
|
+
kind: resolvedKind,
|
|
309
|
+
tag,
|
|
310
|
+
label: name,
|
|
311
|
+
box: [boxLeft, boxTop, boxRight, boxBottom],
|
|
312
|
+
boxes,
|
|
313
|
+
ports,
|
|
314
|
+
sidePort: (side, t) => {
|
|
315
|
+
if (side !== "left" && side !== "right") {
|
|
316
|
+
const dx = (t - 0.5) * w * 0.8;
|
|
317
|
+
return side === "top" ? [x + dx, headY(dx, true), UP] : [x + dx, headY(dx, false), DOWN];
|
|
318
|
+
}
|
|
319
|
+
const yy = topT + (botT - topT) * t;
|
|
320
|
+
const covered = jacket && yy >= jacketTop - 0.01;
|
|
321
|
+
const xx = side === "left" ? x - half - (covered ? j : 0) : x + half + (covered ? j : 0);
|
|
322
|
+
return [xx, yy, side === "left" ? LEFT : RIGHT];
|
|
323
|
+
},
|
|
324
|
+
body: parts,
|
|
325
|
+
meta: { topT, botT, w, h, jacket, agitator },
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export const reactor = (options) => vessel({ jacket: true, agitator: "turbine", ...options });
|
|
330
|
+
export const column = (options) => vessel({ w: 16, h: 90, trays: 8, ...options, kind: "vessel.column" });
|
|
331
|
+
export const tank = (options) => vessel({ w: 30, h: 30, top: "cone", bottom: "flat", ...options, kind: "vessel.tank" });
|
|
332
|
+
|
|
333
|
+
export function drum({ x, y, w = 48, h = 20, tag = null, name = null, saddles = true }) {
|
|
334
|
+
checkEquipmentTag(tag);
|
|
335
|
+
const headDepth = h / 4;
|
|
336
|
+
const leftT = x - w / 2 + headDepth;
|
|
337
|
+
const rightT = x + w / 2 - headDepth;
|
|
338
|
+
const half = h / 2;
|
|
339
|
+
const d = `M${n(leftT)} ${n(y - half)}L${n(rightT)} ${n(y - half)}A${n(headDepth)} ${n(half)} 0 0 1 ${n(rightT)} ${n(y + half)}L${n(leftT)} ${n(y + half)}A${n(headDepth)} ${n(half)} 0 0 1 ${n(leftT)} ${n(y - half)}Z`;
|
|
340
|
+
const parts = [el("path", { d, "stroke-width": W.equipment, fill: PID.paper })];
|
|
341
|
+
parts.push(line([[leftT, y - half], [leftT, y + half]], { "stroke-width": W.detail }));
|
|
342
|
+
parts.push(line([[rightT, y - half], [rightT, y + half]], { "stroke-width": W.detail }));
|
|
343
|
+
let bottom = y + half;
|
|
344
|
+
if (saddles) {
|
|
345
|
+
for (const sx of [leftT + 4, rightT - 4]) {
|
|
346
|
+
parts.push(poly([[sx - 3, y + half - 0.6], [sx + 3, y + half - 0.6], [sx + 3.6, y + half + 3], [sx - 3.6, y + half + 3]], { "stroke-width": W.equipment }));
|
|
347
|
+
}
|
|
348
|
+
bottom = y + half + 3;
|
|
349
|
+
}
|
|
350
|
+
return symbol({
|
|
351
|
+
kind: "vessel.horizontal",
|
|
352
|
+
tag,
|
|
353
|
+
label: name,
|
|
354
|
+
box: [x - w / 2, y - half, x + w / 2, bottom],
|
|
355
|
+
ports: { left: [x - w / 2, y, LEFT], right: [x + w / 2, y, RIGHT] },
|
|
356
|
+
sidePort: (side, t) => {
|
|
357
|
+
const xx = leftT + (rightT - leftT) * t;
|
|
358
|
+
if (side === "top") return [xx, y - half, UP];
|
|
359
|
+
if (side === "bottom") return [xx, y + half, DOWN];
|
|
360
|
+
return [side === "left" ? x - w / 2 : x + w / 2, y - half + h * t, side === "left" ? LEFT : RIGHT];
|
|
361
|
+
},
|
|
362
|
+
body: parts,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function gasCylinder({ x, y, w = 10, h = 30, tag = null, name = null }) {
|
|
367
|
+
checkEquipmentTag(tag);
|
|
368
|
+
const half = w / 2;
|
|
369
|
+
const top = y - h / 2;
|
|
370
|
+
const shoulder = top + half;
|
|
371
|
+
const parts = [
|
|
372
|
+
el("path", { d: `M${n(x - half)} ${n(y + h / 2)}L${n(x - half)} ${n(shoulder)}A${n(half)} ${n(half)} 0 0 1 ${n(x + half)} ${n(shoulder)}L${n(x + half)} ${n(y + h / 2)}Z`, "stroke-width": W.equipment, fill: PID.paper }),
|
|
373
|
+
poly([[x - 1.2, top - 3], [x + 1.2, top - 3], [x + 1.2, top], [x - 1.2, top]]),
|
|
374
|
+
line([[x - 2.6, top - 3], [x + 2.6, top - 3]]),
|
|
375
|
+
];
|
|
376
|
+
return symbol({
|
|
377
|
+
kind: "gas.cylinder",
|
|
378
|
+
tag,
|
|
379
|
+
label: name,
|
|
380
|
+
box: [x - half, top - 3, x + half, y + h / 2],
|
|
381
|
+
ports: { outlet: [x, top - 3, UP] },
|
|
382
|
+
body: parts,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ---------------------------------------------------------------- machines
|
|
387
|
+
|
|
388
|
+
export function pump({ x, y, r = 5, type = "centrifugal", facing = "right", tag = null, name = null }) {
|
|
389
|
+
checkEquipmentTag(tag);
|
|
390
|
+
const m = facing === "left" ? -1 : 1;
|
|
391
|
+
const parts = [circle(x, y, r, { "stroke-width": W.equipment, fill: PID.paper })];
|
|
392
|
+
parts.push(poly([[x - r * 0.62, y + r * 0.78], [x - r * 0.95, y + r + 1.8], [x + r * 0.95, y + r + 1.8], [x + r * 0.62, y + r * 0.78]], { "stroke-width": W.equipment }));
|
|
393
|
+
const discharge = [x + m * (r + 2.4), y - r];
|
|
394
|
+
parts.push(line([[x, y - r], discharge], { "stroke-width": W.equipment }));
|
|
395
|
+
if (type === "positive") {
|
|
396
|
+
parts.push(circle(x - r * 0.32, y, r * 0.32), circle(x + r * 0.32, y, r * 0.32));
|
|
397
|
+
} else if (type === "metering") {
|
|
398
|
+
parts.push(line([[x - r * 0.9, y + r * 0.9], [x + r * 0.9, y - r * 0.9]]));
|
|
399
|
+
parts.push(poly([[x + r * 0.9, y - r * 0.9], [x + r * 0.35, y - r * 0.7], [x + r * 0.7, y - r * 0.35]], { fill: PID.ink }));
|
|
400
|
+
} else {
|
|
401
|
+
parts.push(line([[x - r * 0.5, y], [x + r * 0.5, y]], { "stroke-width": W.detail }));
|
|
402
|
+
}
|
|
403
|
+
return symbol({
|
|
404
|
+
kind: type === "positive" ? "pump.positive" : type === "metering" ? "pump.metering" : "pump.centrifugal",
|
|
405
|
+
tag,
|
|
406
|
+
label: name,
|
|
407
|
+
box: [Math.min(x - r, discharge[0]), y - r, Math.max(x + r, discharge[0]), y + r + 1.8],
|
|
408
|
+
ports: {
|
|
409
|
+
suction: [x - m * r, y, m > 0 ? LEFT : RIGHT],
|
|
410
|
+
discharge: [discharge[0], discharge[1], m > 0 ? RIGHT : LEFT],
|
|
411
|
+
signal: [x + m * r * 0.6, y + r + 1.8, DOWN],
|
|
412
|
+
},
|
|
413
|
+
body: parts,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function compressor({ x, y, r = 7, type = "centrifugal", facing = "right", tag = null, name = null }) {
|
|
418
|
+
checkEquipmentTag(tag);
|
|
419
|
+
const m = facing === "left" ? -1 : 1;
|
|
420
|
+
const parts = [circle(x, y, r, { "stroke-width": W.equipment, fill: PID.paper })];
|
|
421
|
+
if (type === "reciprocating") {
|
|
422
|
+
parts.push(poly([[x - r * 0.55, y - r * 0.4], [x + r * 0.55, y - r * 0.4], [x + r * 0.55, y + r * 0.4], [x - r * 0.55, y + r * 0.4]]));
|
|
423
|
+
parts.push(line([[x - r * 0.1, y - r * 0.4], [x - r * 0.1, y + r * 0.4]]));
|
|
424
|
+
parts.push(line([[x - r * 0.1, y], [x - r * 0.85, y]]));
|
|
425
|
+
} else {
|
|
426
|
+
parts.push(poly([[x - m * r * 0.72, y - r * 0.62], [x + m * r * 0.72, y - r * 0.28], [x + m * r * 0.72, y + r * 0.28], [x - m * r * 0.72, y + r * 0.62]]));
|
|
427
|
+
}
|
|
428
|
+
return symbol({
|
|
429
|
+
kind: type === "reciprocating" ? "compressor.reciprocating" : "compressor.centrifugal",
|
|
430
|
+
tag,
|
|
431
|
+
label: name,
|
|
432
|
+
box: [x - r, y - r, x + r, y + r],
|
|
433
|
+
ports: {
|
|
434
|
+
inlet: [x - m * r, y, m > 0 ? LEFT : RIGHT],
|
|
435
|
+
outlet: [x + m * r, y, m > 0 ? RIGHT : LEFT],
|
|
436
|
+
drive: [x, y + r, DOWN],
|
|
437
|
+
},
|
|
438
|
+
body: parts,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function motor({ x, y, r = 3.6, tag = null }) {
|
|
443
|
+
return symbol({
|
|
444
|
+
kind: "drive.motor",
|
|
445
|
+
tag,
|
|
446
|
+
box: [x - r, y - r, x + r, y + r],
|
|
447
|
+
ports: { shaft: [x, y - r, UP], left: [x - r, y, LEFT], right: [x + r, y, RIGHT], bottom: [x, y + r, DOWN] },
|
|
448
|
+
body: [circle(x, y, r, { fill: PID.paper }), label(x, y + 0.9, "M", 2.5)],
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ---------------------------------------------------------------- heat transfer
|
|
453
|
+
|
|
454
|
+
export function exchanger({ x, y, r = 7, tag = null, name = null }) {
|
|
455
|
+
checkEquipmentTag(tag);
|
|
456
|
+
const zig = [[x - r, y], [x - r * 0.45, y], [x - r * 0.2, y - r * 0.45], [x + r * 0.2, y + r * 0.45], [x + r * 0.45, y], [x + r, y]];
|
|
457
|
+
return symbol({
|
|
458
|
+
kind: "exchanger.general",
|
|
459
|
+
tag,
|
|
460
|
+
label: name,
|
|
461
|
+
box: [x - r, y - r, x + r, y + r],
|
|
462
|
+
ports: { "tube-in": [x - r, y, LEFT], "tube-out": [x + r, y, RIGHT], "shell-in": [x, y - r, UP], "shell-out": [x, y + r, DOWN] },
|
|
463
|
+
body: [circle(x, y, r, { "stroke-width": W.equipment, fill: PID.paper }), line(zig)],
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Shell-and-tube exchangers after ISO 10628-2 items 3.3 (fixed tube sheets, one tube pass),
|
|
468
|
+
// 3.4 (floating head, two passes from the front channel) and 3.5 (U-tubes).
|
|
469
|
+
export function shellTube({ x, y, w = 36, h = 12, type = "fixed", tag = null, name = null }) {
|
|
470
|
+
checkEquipmentTag(tag);
|
|
471
|
+
const half = h / 2;
|
|
472
|
+
const x0 = x - w / 2;
|
|
473
|
+
const x1 = x + w / 2;
|
|
474
|
+
const rect = (a, b, c, d, extra = {}) => poly([[a, b], [c, b], [c, d], [a, d]], extra);
|
|
475
|
+
const parts = [];
|
|
476
|
+
let ports;
|
|
477
|
+
let kind;
|
|
478
|
+
if (type === "utube") {
|
|
479
|
+
const channel = x0 + 6;
|
|
480
|
+
const d = `M${n(channel)} ${n(y - half)}L${n(x1 - half)} ${n(y - half)}A${n(half)} ${n(half)} 0 0 1 ${n(x1 - half)} ${n(y + half)}L${n(channel)} ${n(y + half)}Z`;
|
|
481
|
+
parts.push(
|
|
482
|
+
el("path", { d, "stroke-width": W.equipment, fill: PID.paper }),
|
|
483
|
+
rect(x0, y - half, channel, y + half, { "stroke-width": W.equipment, fill: PID.paper }),
|
|
484
|
+
line([[x0, y], [channel, y]], { "stroke-width": W.detail }),
|
|
485
|
+
el("path", { d: `M${n(channel)} ${n(y - 2.4)}L${n(x1 - half - 1)} ${n(y - 2.4)}A2.4 2.4 0 0 1 ${n(x1 - half - 1)} ${n(y + 2.4)}L${n(channel)} ${n(y + 2.4)}`, "stroke-width": W.detail }),
|
|
486
|
+
);
|
|
487
|
+
kind = "exchanger.utube";
|
|
488
|
+
ports = { "tube-in": [x0 + 3, y + half, DOWN], "tube-out": [x0 + 3, y - half, UP], "shell-in": [x1 - half - 4, y - half, UP], "shell-out": [channel + 4, y + half, DOWN] };
|
|
489
|
+
} else {
|
|
490
|
+
const sheet = 1.4;
|
|
491
|
+
const front = x0 + 4;
|
|
492
|
+
parts.push(rect(x0, y - half, x1, y + half, { "stroke-width": W.equipment, fill: PID.paper }));
|
|
493
|
+
parts.push(rect(front, y - half, front + sheet, y + half, { "stroke-width": W.detail, fill: PID.paper }));
|
|
494
|
+
if (type === "floating") {
|
|
495
|
+
const head = x1 - 8;
|
|
496
|
+
parts.push(rect(head, y - half + 1.6, head + 2.6, y + half - 1.6, { "stroke-width": W.detail, fill: PID.paper }));
|
|
497
|
+
parts.push(line([[x0, y], [front, y]], { "stroke-width": W.detail }));
|
|
498
|
+
for (const dy of [-half * 0.45, 0, half * 0.45]) parts.push(line([[front + sheet, y + dy], [head, y + dy]], { "stroke-width": W.detail }));
|
|
499
|
+
kind = "exchanger.floating";
|
|
500
|
+
ports = { "tube-in": [x0 + 2, y + half, DOWN], "tube-out": [x0 + 2, y - half, UP], "shell-in": [x1 - 12, y - half, UP], "shell-out": [front + 6, y + half, DOWN] };
|
|
501
|
+
} else {
|
|
502
|
+
const rear = x1 - 4 - sheet;
|
|
503
|
+
parts.push(rect(rear, y - half, rear + sheet, y + half, { "stroke-width": W.detail, fill: PID.paper }));
|
|
504
|
+
for (const dy of [-half * 0.45, 0, half * 0.45]) parts.push(line([[front + sheet, y + dy], [rear, y + dy]], { "stroke-width": W.detail }));
|
|
505
|
+
kind = "exchanger.shelltube";
|
|
506
|
+
ports = { "tube-in": [x0 + 2, y + half, DOWN], "tube-out": [x1 - 2, y - half, UP], "shell-in": [rear - 4, y - half, UP], "shell-out": [front + sheet + 4, y + half, DOWN] };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return symbol({ kind, tag, label: name, box: [x0, y - half, x1, y + half], ports, body: parts });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ISO 10628-2 item 3.6: the plate pack as a rectangle with both diagonals and a centre line.
|
|
513
|
+
export function plateExchanger({ x, y, w = 18, h = 10, tag = null, name = null }) {
|
|
514
|
+
checkEquipmentTag(tag);
|
|
515
|
+
const [x0, y0, x1, y1] = [x - w / 2, y - h / 2, x + w / 2, y + h / 2];
|
|
516
|
+
const parts = [
|
|
517
|
+
poly([[x0, y0], [x1, y0], [x1, y1], [x0, y1]], { "stroke-width": W.equipment, fill: PID.paper }),
|
|
518
|
+
line([[x0, y0], [x1, y1]], { "stroke-width": W.detail }),
|
|
519
|
+
line([[x0, y1], [x1, y0]], { "stroke-width": W.detail }),
|
|
520
|
+
line([[x, y0], [x, y1]], { "stroke-width": W.detail }),
|
|
521
|
+
];
|
|
522
|
+
return symbol({
|
|
523
|
+
kind: "exchanger.plate",
|
|
524
|
+
tag,
|
|
525
|
+
label: name,
|
|
526
|
+
box: [x - w / 2, y - h / 2, x + w / 2, y + h / 2],
|
|
527
|
+
ports: {
|
|
528
|
+
"hot-in": [x - w / 2, y - h / 2 + 3, LEFT], "hot-out": [x + w / 2, y + h / 2 - 3, RIGHT],
|
|
529
|
+
"cold-in": [x + w / 2, y - h / 2 + 3, RIGHT], "cold-out": [x - w / 2, y + h / 2 - 3, LEFT],
|
|
530
|
+
},
|
|
531
|
+
body: parts,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// An air-cooled exchanger: the tube bundle as a box with the tube run inside, the fan under it
|
|
536
|
+
// as a blade sweep on a hub, driven by a motor.
|
|
537
|
+
export function airCooler({ x, y, w = 30, h = 7, tag = null, name = null }) {
|
|
538
|
+
checkEquipmentTag(tag);
|
|
539
|
+
const [x0, y0, x1, y1] = [x - w / 2, y - h / 2, x + w / 2, y + h / 2];
|
|
540
|
+
const zig = [];
|
|
541
|
+
for (let i = 0; i <= 10; i++) zig.push([x0 + 2 + ((w - 4) * i) / 10, i % 2 ? y - h * 0.22 : y + h * 0.22]);
|
|
542
|
+
const fanY = y1 + 3;
|
|
543
|
+
const motorY = fanY + 4.6;
|
|
544
|
+
const parts = [
|
|
545
|
+
poly([[x0, y0], [x1, y0], [x1, y1], [x0, y1]], { "stroke-width": W.equipment, fill: PID.paper }),
|
|
546
|
+
line(zig, { "stroke-width": W.detail }),
|
|
547
|
+
line([[x, y1], [x, motorY - 2.4]], { "stroke-width": W.symbol }),
|
|
548
|
+
el("path", { d: `M${n(x - w * 0.3)} ${n(fanY)}Q${n(x - w * 0.15)} ${n(fanY - 1.4)} ${n(x)} ${n(fanY)}Q${n(x + w * 0.15)} ${n(fanY + 1.4)} ${n(x + w * 0.3)} ${n(fanY)}`, "stroke-width": W.symbol }),
|
|
549
|
+
circle(x, fanY, 0.8, { fill: PID.ink }),
|
|
550
|
+
circle(x, motorY, 2.4, { fill: PID.paper }),
|
|
551
|
+
label(x, motorY + 0.8, "M", 2.2),
|
|
552
|
+
];
|
|
553
|
+
return symbol({
|
|
554
|
+
kind: "exchanger.aircooler",
|
|
555
|
+
tag,
|
|
556
|
+
label: name,
|
|
557
|
+
box: [x0, y0, x1, motorY + 2.4],
|
|
558
|
+
boxes: [[x0, y0, x1, fanY + 1.6], [x - 2.6, fanY + 1.6, x + 2.6, motorY + 2.4]],
|
|
559
|
+
ports: { in: [x0, y, LEFT], out: [x1, y, RIGHT] },
|
|
560
|
+
body: parts,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// A packaged unit (chiller, circulator, gas booster skid, analyzer, DAQ): a box with a
|
|
565
|
+
// centred caption and ports at fractions of its sides.
|
|
566
|
+
export function unit({ x, y, w = 26, h = 16, tag = null, name = null, caption = null, ports = {}, glyph = null }) {
|
|
567
|
+
checkEquipmentTag(tag);
|
|
568
|
+
const x0 = x - w / 2;
|
|
569
|
+
const y0 = y - h / 2;
|
|
570
|
+
const parts = [poly([[x0, y0], [x0 + w, y0], [x0 + w, y0 + h], [x0, y0 + h]], { "stroke-width": W.equipment, fill: PID.paper })];
|
|
571
|
+
if (glyph === "coil") {
|
|
572
|
+
const z = [];
|
|
573
|
+
for (let i = 0; i <= 8; i++) z.push([x0 + 3 + ((w - 6) * i) / 8, y0 + h - 3.2 - (i % 2 ? 2 : 0)]);
|
|
574
|
+
parts.push(line(z, { "stroke-width": W.detail }));
|
|
575
|
+
} else if (glyph === "wave") {
|
|
576
|
+
const z = [];
|
|
577
|
+
for (let i = 0; i <= 16; i++) z.push([x0 + 3 + ((w - 6) * i) / 16, y0 + h - 3.2 + Math.sin((i / 16) * Math.PI * 4) * 0.8]);
|
|
578
|
+
parts.push(line(z, { "stroke-width": W.detail }));
|
|
579
|
+
}
|
|
580
|
+
if (caption) parts.push(label(x, glyph ? y - 0.2 : y + 0.9, caption, PID.text.label));
|
|
581
|
+
const resolved = {};
|
|
582
|
+
const at = (side, t) => {
|
|
583
|
+
if (side === "left") return [x0, y0 + h * t, LEFT];
|
|
584
|
+
if (side === "right") return [x0 + w, y0 + h * t, RIGHT];
|
|
585
|
+
if (side === "top") return [x0 + w * t, y0, UP];
|
|
586
|
+
return [x0 + w * t, y0 + h, DOWN];
|
|
587
|
+
};
|
|
588
|
+
for (const [name0, [side, t]] of Object.entries(ports)) resolved[name0] = at(side, t);
|
|
589
|
+
return symbol({ kind: "package.unit", tag, label: name, box: [x0, y0, x0 + w, y0 + h], ports: resolved, sidePort: at, body: parts });
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ---------------------------------------------------------------- inline items
|
|
593
|
+
|
|
594
|
+
// Inline symbols are drawn along +x in a local frame and turned to the line direction. A line
|
|
595
|
+
// running left mirrors the symbol instead of turning it over, so an actuator still stands above
|
|
596
|
+
// the line; letters are set upright at their turned position.
|
|
597
|
+
function inline({ kind, x, y, orient = "right", half = 3.5, height = 2.4, body, extraPorts = {}, extraBoxes = [], tag = null }) {
|
|
598
|
+
const mirrored = orient === "left";
|
|
599
|
+
const turn = mirrored ? ([px, py]) => [-px, py] : rot(ORIENT[orient] ?? 0);
|
|
600
|
+
const place = ([px, py]) => { const [rx, ry] = turn([px, py]); return [x + rx, y + ry]; };
|
|
601
|
+
const dirOf = (d) => turn(d).map((v) => Math.round(v));
|
|
602
|
+
const corners = [[-half, -height], [half, -height], [half, height], [-half, height]].map(place);
|
|
603
|
+
const xs = corners.map((c) => c[0]);
|
|
604
|
+
const ys = corners.map((c) => c[1]);
|
|
605
|
+
const box = [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)];
|
|
606
|
+
const ports = { a: [...place([-half, 0]), dirOf(LEFT)], b: [...place([half, 0]), dirOf(RIGHT)] };
|
|
607
|
+
for (const [name, [lx, ly, d]] of Object.entries(extraPorts)) ports[name] = [...place([lx, ly]), dirOf(d)];
|
|
608
|
+
const boxes = [box];
|
|
609
|
+
for (const local of extraBoxes) {
|
|
610
|
+
const pts = [[local[0], local[1]], [local[2], local[1]], [local[2], local[3]], [local[0], local[3]]].map(place);
|
|
611
|
+
boxes.push([Math.min(...pts.map((p) => p[0])), Math.min(...pts.map((p) => p[1])), Math.max(...pts.map((p) => p[0])), Math.max(...pts.map((p) => p[1]))]);
|
|
612
|
+
}
|
|
613
|
+
const all = boxes.reduce((acc, b) => [Math.min(acc[0], b[0]), Math.min(acc[1], b[1]), Math.max(acc[2], b[2]), Math.max(acc[3], b[3])]);
|
|
614
|
+
const parts = [body].flat();
|
|
615
|
+
const isText = (part) => typeof part === "string" && part.startsWith("<text");
|
|
616
|
+
const texts = parts.filter(isText).map((t) => {
|
|
617
|
+
const attr = (name) => Number(t.match(new RegExp(` ${name}="([^"]*)"`, "u"))?.[1] ?? 0);
|
|
618
|
+
const [tx, ty] = place([attr("x"), attr("y")]);
|
|
619
|
+
const along = turn([1, 0]);
|
|
620
|
+
const anchor = t.match(/ text-anchor="([^"]*)"/u)?.[1] ?? "middle";
|
|
621
|
+
const flip = { start: "end", end: "start", middle: "middle" };
|
|
622
|
+
const upright = along[0] < 0 ? flip[anchor] : along[0] !== 0 ? anchor : tx < x - 0.5 ? "end" : tx > x + 0.5 ? "start" : "middle";
|
|
623
|
+
return t.replace(/ x="[^"]*"/u, ` x="${n(tx)}"`).replace(/ y="[^"]*"/u, ` y="${n(ty)}"`).replace(/ text-anchor="[^"]*"/u, ` text-anchor="${upright}"`);
|
|
624
|
+
});
|
|
625
|
+
const transform = mirrored ? `translate(${n(x)} ${n(y)}) scale(-1 1)` : `translate(${n(x)} ${n(y)}) rotate(${ORIENT[orient] ?? 0})`;
|
|
626
|
+
return symbol({
|
|
627
|
+
kind,
|
|
628
|
+
tag,
|
|
629
|
+
box: all,
|
|
630
|
+
boxes,
|
|
631
|
+
ports,
|
|
632
|
+
body: [el("g", { transform }, parts.filter((p) => !isText(p))), ...texts].join(""),
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const bowtie = (half = 3.5, hh = 2.2) => poly([[-half, -hh], [half, hh], [half, -hh], [-half, hh]]);
|
|
637
|
+
|
|
638
|
+
const ACTUATOR_ALIASES = Object.freeze({ manual: "hand", handwheel: "hand" });
|
|
639
|
+
|
|
640
|
+
function checkFail(fail) {
|
|
641
|
+
if (fail && !FAIL_POSITIONS[fail]) throw new Error(`pid: fail position "${fail}" must be FO, FC, FL or FI (ISA-5.1 Table 5.4.4)`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// How far each actuator reaches from the valve body: its signal connects at that end.
|
|
645
|
+
const ACTUATOR_REACH = Object.freeze({ diaphragm: 7.6, piston: 8, motor: 9, solenoid: 8.8, hand: 4.2 });
|
|
646
|
+
|
|
647
|
+
// Actuators stand perpendicular to the line, on the local -y side (ISA-5.1 Table 5.4.2).
|
|
648
|
+
// On a vertical line the fail letters go beside the body, opposite the actuator, reading upright.
|
|
649
|
+
function actuatorParts(type, fail, orient = "right") {
|
|
650
|
+
const parts = [line([[0, 0], [0, -4.2]])];
|
|
651
|
+
const boxes = [[-3.4, -9.2, 3.4, 0]];
|
|
652
|
+
if (type === "diaphragm") parts.push(el("path", { d: "M-3.4 -4.2A3.4 3.4 0 0 1 3.4 -4.2Z", fill: "#ffffff" }));
|
|
653
|
+
else if (type === "solenoid") parts.push(poly([[-2.4, -4.2], [2.4, -4.2], [2.4, -8.8], [-2.4, -8.8]], { fill: "#ffffff" }), label(0, -5.6, "S", 2.4));
|
|
654
|
+
else if (type === "motor") parts.push(circle(0, -6.6, 2.4, { fill: "#ffffff" }), label(0, -5.7, "M", 2.4));
|
|
655
|
+
else if (type === "piston") parts.push(poly([[-2.6, -4.2], [2.6, -4.2], [2.6, -8], [-2.6, -8]], { fill: "#ffffff" }), line([[-2.6, -6.1], [2.6, -6.1]]));
|
|
656
|
+
else parts.push(line([[-2.6, -4.2], [2.6, -4.2]]));
|
|
657
|
+
const vertical = orient === "up" || orient === "down";
|
|
658
|
+
const baseline = orient === "up" ? -0.8 : 0.8;
|
|
659
|
+
if (fail) parts.push(vertical ? label(baseline, 3, fail, 2.2, { "data-label": "fail" }) : label(4.6, -1.2, fail, 2.2, { "text-anchor": "start", "data-label": "fail" }));
|
|
660
|
+
if (fail) boxes.push(vertical ? [-2.2, 2.4, 2.2, 9] : [3.4, -3.6, 9.2, 0.6]);
|
|
661
|
+
return { parts, boxes };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export function valve({ x, y, type = "gate", orient = "right", actuator = null, fail = null, tag = null }) {
|
|
665
|
+
const parts = [];
|
|
666
|
+
const act = actuator ? ACTUATOR_ALIASES[actuator] ?? actuator : null;
|
|
667
|
+
if (act && !ACTUATORS[act]) throw new Error(`pid: actuator must be one of ${Object.keys(ACTUATORS).join(", ")}`);
|
|
668
|
+
checkFail(fail);
|
|
669
|
+
let kind = act ? `valve.${type}.${act}` : `valve.${type}`;
|
|
670
|
+
let extraPorts = {};
|
|
671
|
+
let extraBoxes = [];
|
|
672
|
+
if (type === "butterfly") {
|
|
673
|
+
parts.push(line([[-3, -2.4], [-3, 2.4]]), line([[3, -2.4], [3, 2.4]]), line([[-3, 0], [3, 0]]), line([[-1.8, 1.8], [1.8, -1.8]]), circle(0, 0, 0.7, { fill: PID.ink }));
|
|
674
|
+
} else if (type === "check") {
|
|
675
|
+
parts.push(bowtie(), poly([[-3.5, -2.2], [0, 0], [-3.5, 2.2]], { fill: PID.ink }));
|
|
676
|
+
} else {
|
|
677
|
+
parts.push(bowtie());
|
|
678
|
+
if (type === "globe") parts.push(circle(0, 0, 0.9, { fill: PID.ink }));
|
|
679
|
+
if (type === "ball") parts.push(circle(0, 0, 1.3, { fill: "#ffffff" }));
|
|
680
|
+
if (type === "plug") parts.push(poly([[-0.9, -1.1], [0.9, -1.1], [0.9, 1.1], [-0.9, 1.1]], { fill: "#ffffff" }));
|
|
681
|
+
if (type === "needle") parts.push(poly([[-1, -3.6], [1, -3.6], [0, -0.6]], { fill: PID.ink }), line([[0, -3.6], [0, -5.2]]));
|
|
682
|
+
if (type === "diaphragm") parts.push(el("path", { d: "M-2.4 -2.2A2.4 1.6 0 0 1 2.4 -2.2" }));
|
|
683
|
+
if (type === "threeway") {
|
|
684
|
+
parts.push(poly([[0, 0], [-2.2, 3.5], [2.2, 3.5]]));
|
|
685
|
+
extraPorts = { c: [0, 3.5, DOWN] };
|
|
686
|
+
extraBoxes = [[-2.4, 0, 2.4, 3.6]];
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (type === "needle") extraBoxes.push([-1.2, -5.4, 1.2, 0]);
|
|
690
|
+
if (type === "diaphragm") extraBoxes.push([-2.6, -3.9, 2.6, 0]);
|
|
691
|
+
if (act) {
|
|
692
|
+
const a = actuatorParts(act, fail, orient);
|
|
693
|
+
parts.push(...a.parts);
|
|
694
|
+
extraBoxes.push(...a.boxes);
|
|
695
|
+
extraPorts = { ...extraPorts, actuator: [0, -ACTUATOR_REACH[act], UP] };
|
|
696
|
+
}
|
|
697
|
+
if (!describeKind(kind)) throw new Error(`pid: unknown valve type ${type}`);
|
|
698
|
+
return inline({ kind, x, y, orient, body: parts, extraPorts, extraBoxes, tag });
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// An angle valve sits where the line turns: one body toward the inlet, one toward the outlet,
|
|
702
|
+
// the actuator (if any) on the side away from the inlet.
|
|
703
|
+
export function angleValve({ x, y, inlet = "down", outlet = "right", actuator = null, fail = null, tag = null }) {
|
|
704
|
+
const dirs = { right: RIGHT, left: LEFT, up: UP, down: DOWN };
|
|
705
|
+
const din = dirs[inlet];
|
|
706
|
+
const dout = dirs[outlet];
|
|
707
|
+
if (!din || !dout || din[0] * dout[0] + din[1] * dout[1] !== 0) throw new Error("angleValve: inlet and outlet must be two perpendicular sides (left, right, up, down)");
|
|
708
|
+
const act = actuator ? ACTUATOR_ALIASES[actuator] ?? actuator : null;
|
|
709
|
+
if (act && !ACTUATORS[act]) throw new Error(`pid: actuator must be one of ${Object.keys(ACTUATORS).join(", ")}`);
|
|
710
|
+
checkFail(fail);
|
|
711
|
+
const triangle = (d) => [[x, y], [x + d[0] * 3.5 - d[1] * 2.2, y + d[1] * 3.5 + d[0] * 2.2], [x + d[0] * 3.5 + d[1] * 2.2, y + d[1] * 3.5 - d[0] * 2.2]];
|
|
712
|
+
const tris = [triangle(din), triangle(dout)];
|
|
713
|
+
const parts = tris.map((t) => poly(t));
|
|
714
|
+
const corners = tris.flat();
|
|
715
|
+
const boxes = [[Math.min(...corners.map((p) => p[0])), Math.min(...corners.map((p) => p[1])), Math.max(...corners.map((p) => p[0])), Math.max(...corners.map((p) => p[1]))]];
|
|
716
|
+
if (act) {
|
|
717
|
+
const away = [-din[0], -din[1]];
|
|
718
|
+
const angle = Math.round((Math.atan2(away[1], away[0]) * 180) / Math.PI) + 90;
|
|
719
|
+
const a = actuatorParts(act, fail);
|
|
720
|
+
parts.push(el("g", { transform: `translate(${n(x)} ${n(y)}) rotate(${angle})` }, a.parts));
|
|
721
|
+
const r = rot(angle);
|
|
722
|
+
for (const b of a.boxes) {
|
|
723
|
+
const pts = [[b[0], b[1]], [b[2], b[1]], [b[2], b[3]], [b[0], b[3]]].map(r).map(([px, py]) => [x + px, y + py]);
|
|
724
|
+
boxes.push([Math.min(...pts.map((p) => p[0])), Math.min(...pts.map((p) => p[1])), Math.max(...pts.map((p) => p[0])), Math.max(...pts.map((p) => p[1]))]);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const all = boxes.reduce((acc, b) => [Math.min(acc[0], b[0]), Math.min(acc[1], b[1]), Math.max(acc[2], b[2]), Math.max(acc[3], b[3])]);
|
|
728
|
+
return symbol({
|
|
729
|
+
kind: act ? `valve.angle.${act}` : "valve.angle",
|
|
730
|
+
tag,
|
|
731
|
+
box: all,
|
|
732
|
+
boxes,
|
|
733
|
+
ports: {
|
|
734
|
+
inlet: [x + din[0] * 3.5, y + din[1] * 3.5, din],
|
|
735
|
+
outlet: [x + dout[0] * 3.5, y + dout[1] * 3.5, dout],
|
|
736
|
+
...(act ? { actuator: [x - din[0] * ACTUATOR_REACH[act], y - din[1] * ACTUATOR_REACH[act], [-din[0], -din[1]]] } : {}),
|
|
737
|
+
},
|
|
738
|
+
body: parts,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export function regulator({ x, y, orient = "right", backPressure = false, tag = null }) {
|
|
743
|
+
const sense = backPressure ? -3.5 : 3.5;
|
|
744
|
+
const parts = [
|
|
745
|
+
bowtie(),
|
|
746
|
+
line([[0, 0], [0, -4.2]]),
|
|
747
|
+
el("path", { d: "M-3.4 -4.2A3.4 3.4 0 0 1 3.4 -4.2Z", fill: "#ffffff" }),
|
|
748
|
+
line([[backPressure ? -3.4 : 3.4, -5.2], [sense + (backPressure ? -2.2 : 2.2), -5.2], [sense + (backPressure ? -2.2 : 2.2), 0]], { "stroke-width": W.signal, "stroke-dasharray": "0.8 0.6" }),
|
|
749
|
+
];
|
|
750
|
+
const sx = sense + (backPressure ? -2.2 : 2.2);
|
|
751
|
+
return inline({ kind: backPressure ? "valve.backpressure" : "valve.regulator", x, y, orient, body: parts, extraBoxes: [[Math.min(-3.4, sx), -7.8, Math.max(3.4, sx), 0]], tag });
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// A pressure safety valve: inlet from below, outlet to the side, spring on top.
|
|
755
|
+
export function safetyValve({ x, y, outlet = "right", tag = null, setPressure = null }) {
|
|
756
|
+
const m = outlet === "left" ? -1 : 1;
|
|
757
|
+
const parts = [
|
|
758
|
+
poly([[x, y], [x - 2.4, y + 4], [x + 2.4, y + 4]]),
|
|
759
|
+
poly([[x, y], [x + m * 4, y - 2.4], [x + m * 4, y + 2.4]]),
|
|
760
|
+
line([[x, y], [x, y - 3]]),
|
|
761
|
+
line([[x, y - 3], [x - 1.6, y - 3.8], [x + 1.6, y - 4.8], [x - 1.6, y - 5.8], [x + 1.6, y - 6.8], [x, y - 7.4]], { "stroke-width": W.detail }),
|
|
762
|
+
];
|
|
763
|
+
const boxes = [[Math.min(x - 2.4, x + m * 4), y - 7.6, Math.max(x + 2.4, x + m * 4), y + 4]];
|
|
764
|
+
if (setPressure) {
|
|
765
|
+
parts.push(label(x - m * 3, y - 4.6, setPressure, 2.2, { "text-anchor": m > 0 ? "end" : "start", "data-label": "set" }));
|
|
766
|
+
}
|
|
767
|
+
return symbol({
|
|
768
|
+
kind: "valve.safety",
|
|
769
|
+
tag,
|
|
770
|
+
box: boxes[0],
|
|
771
|
+
boxes,
|
|
772
|
+
ports: { inlet: [x, y + 4, DOWN], outlet: [x + m * 4, y, m > 0 ? RIGHT : LEFT] },
|
|
773
|
+
body: parts,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export function ruptureDisk({ x, y, orient = "up", tag = null }) {
|
|
778
|
+
const parts = [line([[-1.6, -3], [-1.6, 3]]), line([[1.6, -3], [1.6, 3]]), el("path", { d: "M-1.6 -2.6Q1.2 0 -1.6 2.6" })];
|
|
779
|
+
return inline({ kind: "relief.rupture", x, y, orient, half: 1.6, height: 3, body: parts, tag });
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
export function orifice({ x, y, orient = "right", tag = null }) {
|
|
783
|
+
return inline({ kind: "flow.orifice", x, y, orient, half: 1, height: 3, body: [line([[-1, -3], [-1, 3]]), line([[1, -3], [1, 3]])], tag });
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
export function rotameter({ x, y, orient = "up", tag = null }) {
|
|
787
|
+
const parts = [poly([[-3.5, -1.2], [3.5, -2.2], [3.5, 2.2], [-3.5, 1.2]]), circle(-0.6, 0, 0.8, { fill: PID.ink })];
|
|
788
|
+
return inline({ kind: "flow.rotameter", x, y, orient, half: 3.5, height: 2.2, body: parts, tag });
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export function mfc({ x, y, orient = "right", tag = null }) {
|
|
792
|
+
const parts = [poly([[-6, -3.5], [6, -3.5], [6, 3.5], [-6, 3.5]], { fill: "#ffffff" }), label(0, 0.9, "MFC", 2.4)];
|
|
793
|
+
return inline({ kind: "flow.mfc", x, y, orient, half: 6, height: 3.5, body: parts, extraPorts: { signal: [0, -3.5, UP] }, tag });
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function filter({ x, y, orient = "right", tag = null }) {
|
|
797
|
+
const parts = [poly([[-3.4, 0], [0, -3.4], [3.4, 0], [0, 3.4]], { fill: "#ffffff" }), line([[0, -3.4], [0, 3.4]], { "stroke-dasharray": "0.8 0.6" })];
|
|
798
|
+
return inline({ kind: "line.filter", x, y, orient, half: 3.4, height: 3.4, body: parts, tag });
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Concentric reducers taper on both sides; an eccentric one keeps one side flat (bottom flat
|
|
802
|
+
// by default, as on pump suctions, so no vapour pocket forms).
|
|
803
|
+
export function reducer({ x, y, orient = "right", eccentric = false, flat = "bottom" }) {
|
|
804
|
+
const shape = !eccentric
|
|
805
|
+
? [[-2.4, -2.4], [2.4, -1.4], [2.4, 1.4], [-2.4, 2.4]]
|
|
806
|
+
: flat === "top" ? [[-2.4, -2.4], [2.4, -2.4], [2.4, 0.4], [-2.4, 2.4]] : [[-2.4, -2.4], [2.4, -0.4], [2.4, 2.4], [-2.4, 2.4]];
|
|
807
|
+
return inline({ kind: eccentric ? "line.reducer-eccentric" : "line.reducer", x, y, orient, half: 2.4, height: 2.4, body: [poly(shape)] });
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export function flange({ x, y, orient = "right" }) {
|
|
811
|
+
return inline({ kind: "line.flange", x, y, orient, half: 0.7, height: 2.8, body: [line([[-0.7, -2.8], [-0.7, 2.8]], { "stroke-width": W.symbol }), line([[0.7, -2.8], [0.7, 2.8]], { "stroke-width": W.symbol })] });
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export function sightGlass({ x, y, orient = "right" }) {
|
|
815
|
+
return inline({ kind: "line.sightglass", x, y, orient, half: 2.8, height: 2.8, body: [circle(0, 0, 2.8, { fill: "#ffffff" }), circle(0, 0, 1.2)] });
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// A spectacle (figure-8) blind: the half in the line is solid when the line is blinded
|
|
819
|
+
// (normally closed) and a ring when it is open; the other half hangs off the line.
|
|
820
|
+
export function blind({ x, y, orient = "right", state = "closed" }) {
|
|
821
|
+
const closed = state !== "open";
|
|
822
|
+
const body = [
|
|
823
|
+
line([[0, -1.8], [0, -3.2]], { "stroke-width": W.symbol }),
|
|
824
|
+
circle(0, 0, 1.8, { fill: closed ? PID.ink : PID.paper }),
|
|
825
|
+
circle(0, -5, 1.8, { fill: closed ? PID.paper : PID.ink }),
|
|
826
|
+
];
|
|
827
|
+
return inline({ kind: closed ? "line.blind" : "line.blind-open", x, y, orient, half: 1.8, height: 1.8, body, extraBoxes: [[-1.9, -6.9, 1.9, 0]] });
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Primary flow elements (ISA-5.1 Table 5.2.3). Elements without a drawn form of their own
|
|
831
|
+
// carry the measurement notation of Table 5.2.2 in a small box on the line.
|
|
832
|
+
const FLOW_NOTATION = Object.freeze({ magnetic: "MAG", coriolis: "COR", vortex: "VOR", turbine: "TUR", ultrasonic: "US", thermal: "TMF" });
|
|
833
|
+
export function flowElement({ x, y, type = "orifice", orient = "right", tag = null }) {
|
|
834
|
+
if (type === "orifice") return orifice({ x, y, orient, tag });
|
|
835
|
+
if (type === "venturi") {
|
|
836
|
+
const body = [line([[-4, -2.4], [-1.2, -1.1], [1.2, -1.1], [4, -2.4]]), line([[-4, 2.4], [-1.2, 1.1], [1.2, 1.1], [4, 2.4]])];
|
|
837
|
+
return inline({ kind: "flow.venturi", x, y, orient, half: 4, height: 2.4, body, tag });
|
|
838
|
+
}
|
|
839
|
+
const note = FLOW_NOTATION[type];
|
|
840
|
+
if (!note) throw new Error(`flowElement: type must be orifice, venturi or one of ${Object.keys(FLOW_NOTATION).join(", ")}`);
|
|
841
|
+
const body = [poly([[-4.4, -2.4], [4.4, -2.4], [4.4, 2.4], [-4.4, 2.4]], { fill: PID.paper }), label(0, 0.85, note, 2.4)];
|
|
842
|
+
return inline({ kind: `flow.${type}`, x, y, orient, half: 4.4, height: 2.4, body, tag });
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ---------------------------------------------------------------- instruments
|
|
846
|
+
|
|
847
|
+
// Round-1 location names: a location alone used to pick the whole bubble.
|
|
848
|
+
const LEGACY_LOCATION = Object.freeze({
|
|
849
|
+
panel: ["discrete", "main"], "panel-rear": ["discrete", "main-rear"], "local-panel": ["discrete", "aux"],
|
|
850
|
+
dcs: ["shared", "main"], plc: ["plc", "main"],
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
// ISA-5.1 Table 5.1.1: the shape says what kind of device or function it is, the lines across
|
|
854
|
+
// it say where it lives (none in the field, one in the main control room, two on an auxiliary
|
|
855
|
+
// or local panel; dashed when the operator cannot normally reach it).
|
|
856
|
+
export function instrument({ x, y, tag, fn = null, location = "field", r = 5 }) {
|
|
857
|
+
const parsed = parseInstrumentTag(tag);
|
|
858
|
+
if (!parsed.ok) throw new Error(`pid: instrument tag "${tag}": ${parsed.error}`);
|
|
859
|
+
const [legacyFn, legacyLocation] = LEGACY_LOCATION[location] ?? [null, location];
|
|
860
|
+
const func = fn ?? legacyFn ?? "discrete";
|
|
861
|
+
const where = LEGACY_LOCATION[location] ? legacyLocation : location;
|
|
862
|
+
if (!FUNCTIONS[func]) throw new Error(`pid: instrument fn must be one of ${Object.keys(FUNCTIONS).join(", ")}`);
|
|
863
|
+
if (!LOCATIONS[where]) throw new Error(`pid: instrument location must be one of ${Object.keys(LOCATIONS).join(", ")}`);
|
|
864
|
+
const kind = `instrument.${func}-${where}`;
|
|
865
|
+
const parts = [];
|
|
866
|
+
const square = () => poly([[x - r, y - r], [x + r, y - r], [x + r, y + r], [x - r, y + r]], { fill: "#ffffff" });
|
|
867
|
+
if (func === "shared") parts.push(square(), circle(x, y, r, { fill: "#ffffff" }));
|
|
868
|
+
else if (func === "plc") parts.push(square(), poly([[x, y - r], [x + r, y], [x, y + r], [x - r, y]]));
|
|
869
|
+
else if (func === "computer") parts.push(poly([[x - r, y], [x - r / 2, y - r * 0.866], [x + r / 2, y - r * 0.866], [x + r, y], [x + r / 2, y + r * 0.866], [x - r / 2, y + r * 0.866]], { fill: "#ffffff" }));
|
|
870
|
+
else parts.push(circle(x, y, r, { fill: "#ffffff" }));
|
|
871
|
+
const rear = where.endsWith("-rear") ? { "stroke-dasharray": "0.9 0.6" } : {};
|
|
872
|
+
if (where.startsWith("main")) parts.push(line([[x - r, y], [x + r, y]], rear));
|
|
873
|
+
if (where.startsWith("aux")) parts.push(line([[x - r, y - 0.35], [x + r, y - 0.35]], rear), line([[x - r, y + 0.35], [x + r, y + 0.35]], rear));
|
|
874
|
+
const letters = parsed.letters;
|
|
875
|
+
const size = Math.min(letters.length >= 4 ? 2.1 : PID.text.tag, r * 0.5);
|
|
876
|
+
parts.push(el("text", { x, y: y - r * 0.16, "font-family": FONT_LATIN, "font-size": size, "text-anchor": "middle", fill: PID.ink, stroke: "none", "data-label": "tag-letters" }, esc(letters)));
|
|
877
|
+
parts.push(el("text", { x, y: y + r * 0.58, "font-family": FONT_LATIN, "font-size": Math.min(PID.text.small + 0.2, r * 0.44), "text-anchor": "middle", fill: PID.ink, stroke: "none", "data-label": "tag-loop" }, esc(parsed.loop)));
|
|
878
|
+
return symbol({
|
|
879
|
+
kind,
|
|
880
|
+
tag: `${letters}-${parsed.loop}`,
|
|
881
|
+
box: [x - r, y - r, x + r, y + r],
|
|
882
|
+
ports: { top: [x, y - r, UP], bottom: [x, y + r, DOWN], left: [x - r, y, LEFT], right: [x + r, y, RIGHT] },
|
|
883
|
+
body: parts,
|
|
884
|
+
meta: { meaning: parsed.meaning },
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export function interlock({ x, y, text = "I", r = 4.6 }) {
|
|
889
|
+
return symbol({
|
|
890
|
+
kind: "logic.interlock",
|
|
891
|
+
tag: null,
|
|
892
|
+
box: [x - r, y - r, x + r, y + r],
|
|
893
|
+
ports: { top: [x, y - r, UP], bottom: [x, y + r, DOWN], left: [x - r, y, LEFT], right: [x + r, y, RIGHT] },
|
|
894
|
+
body: [poly([[x, y - r], [x + r, y], [x, y + r], [x - r, y]], { fill: "#ffffff" }), label(x, y + 0.9, text, 2.2)],
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ---------------------------------------------------------------- terminals
|
|
899
|
+
|
|
900
|
+
export function vent({ x, y, text = "ATM" }) {
|
|
901
|
+
const parts = [line([[x, y], [x, y - 4]]), el("path", { d: `M${n(x)} ${n(y - 4)}A2 2 0 0 1 ${n(x + 4)} ${n(y - 4)}L${n(x + 4)} ${n(y - 2.6)}` }), label(x + 4, y + 0.6, text, 2.2)];
|
|
902
|
+
return symbol({ kind: "terminal.vent", box: [x - 0.5, y - 6.2, x + 6.4, y + 0.8], ports: { in: [x, y, DOWN] }, body: parts });
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export function drain({ x, y, text = "DR" }) {
|
|
906
|
+
const parts = [poly([[x - 2, y], [x + 2, y], [x, y + 2.8]]), label(x, y + 5.6, text, 2.2)];
|
|
907
|
+
return symbol({ kind: "terminal.drain", box: [x - 2.8, y, x + 2.8, y + 5.8], ports: { in: [x, y, UP] }, body: parts });
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
export function samplePoint({ x, y, text = "SP", facing = "down" }) {
|
|
911
|
+
const s = facing === "up" ? -1 : 1;
|
|
912
|
+
const parts = [line([[x, y], [x, y + s * 3]]), poly([[x - 1.4, y + s * 3], [x + 1.4, y + s * 3], [x, y + s * 4.8]], { fill: PID.ink }), label(x + 2.4, y + s * 4.6 + (s > 0 ? 0.6 : 0), text, 2.2, { "text-anchor": "start" })];
|
|
913
|
+
const y0 = Math.min(y, y + s * 5.4);
|
|
914
|
+
return symbol({ kind: "terminal.sample", box: [x - 1.6, y0, x + 2.4 + text.length * 1.6, Math.max(y, y + s * 5.4)], ports: { in: [x, y, s > 0 ? UP : DOWN] }, body: parts });
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Off-page and utility connectors: a pentagon pointing along the flow, the text inside.
|
|
918
|
+
function connector({ kind, x, y, text, width, pointing }) {
|
|
919
|
+
const h = 5;
|
|
920
|
+
const tip = 2.6;
|
|
921
|
+
const m = pointing === "left" ? -1 : 1;
|
|
922
|
+
const x0 = x;
|
|
923
|
+
const x1 = x + m * (width + tip);
|
|
924
|
+
const pts = [[x0, y - h / 2], [x + m * width, y - h / 2], [x1, y], [x + m * width, y + h / 2], [x0, y + h / 2]];
|
|
925
|
+
const textX = x + (m * width) / 2;
|
|
926
|
+
return symbol({
|
|
927
|
+
kind,
|
|
928
|
+
box: [Math.min(x0, x1), y - h / 2, Math.max(x0, x1), y + h / 2],
|
|
929
|
+
ports: { in: [x0, y, m > 0 ? LEFT : RIGHT], out: [x1, y, m > 0 ? RIGHT : LEFT] },
|
|
930
|
+
body: [poly(pts, { fill: "#ffffff" }), label(textX, y + 0.8, text, 2.2)],
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
export const offPage = ({ x, y, text, pointing = "right" }) =>
|
|
935
|
+
connector({ kind: "terminal.offpage", x, y, text, width: Math.max(14, text.length * 1.45 + 3), pointing });
|
|
936
|
+
export const utility = ({ x, y, text, pointing = "right" }) =>
|
|
937
|
+
connector({ kind: "terminal.utility", x, y, text, width: Math.max(8, text.length * 1.45 + 2.4), pointing });
|