@litfamily/litmuse 0.1.1 → 0.1.2
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 +11 -0
- package/README.md +20 -0
- package/README_ko-KR.md +20 -0
- package/bin/litmuse.mjs +4 -0
- package/lib/stage-card.mjs +180 -5
- package/package.json +1 -1
- package/plugin/.muse-plugin/plugin.json +26 -11
- package/plugin/commands/lit-status.md +2 -1
- package/plugin/commands/lit.md +1 -1
- package/plugin/hooks/lib/digest.mjs +1 -1
- package/plugin/hooks/lib/next-step.mjs +68 -0
- package/plugin/hooks/lib/routes.mjs +59 -0
- package/plugin/hooks/lib/workflow-stack.mjs +318 -0
- package/plugin/hooks/session-start.mjs +29 -2
- package/plugin/hooks/user-prompt-submit.mjs +138 -7
- package/plugin/lib/capped-fetch.mjs +25 -0
- package/plugin/lib/pinned-fetch.mjs +79 -0
- package/plugin/lib/plan-task-rows.mjs +32 -0
- package/plugin/lib/render_pages.py +29 -1
- package/plugin/lib/rewind/collect.mjs +679 -0
- package/plugin/lib/rewind/gauge.mjs +72 -0
- package/plugin/lib/rewind/next-step.mjs +91 -0
- package/plugin/lib/rewind/redact.mjs +83 -0
- package/plugin/lib/rewind/transcripts.mjs +227 -0
- package/plugin/skills/frontend-ui-ux/SKILL.md +24 -9
- package/plugin/skills/frontend-ui-ux/kit/components.css +414 -0
- package/plugin/skills/frontend-ui-ux/kit/fonts.json +22 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/plugin/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -1
- package/plugin/skills/frontend-ui-ux/references/craft-floor.md +9 -4
- package/plugin/skills/frontend-ui-ux/references/default-editorial-pixel.json +59 -7
- package/plugin/skills/frontend-ui-ux/references/house-styles.md +234 -0
- package/plugin/skills/frontend-ui-ux/references/motion-guide.md +3 -3
- package/plugin/skills/frontend-ui-ux/references/slop-register.md +7 -7
- package/plugin/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-lock.mjs +196 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +31 -10
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +19 -7
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +4 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe.mjs +59 -7
- package/plugin/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
- package/plugin/skills/lit-adhd/SKILL.md +188 -0
- package/plugin/skills/lit-adhd/references/card-examples.md +121 -0
- package/plugin/skills/lit-adhd/references/status-taxonomy.md +43 -0
- package/plugin/skills/lit-adhd/scripts/rewind.mjs +11 -0
- package/plugin/skills/lit-diagram-drawer/SKILL.md +3 -3
- package/plugin/skills/lit-diagram-drawer/scripts/diagram-craft-rules.mjs +1 -2
- package/plugin/skills/lit-diagram-drawer/scripts/visual-quality.mjs +0 -1
- package/plugin/skills/lit-docx/SKILL.md +1 -1
- package/plugin/skills/lit-hijack/SKILL.md +589 -0
- package/plugin/skills/lit-hijack/references/capture-and-provenance.md +123 -0
- package/plugin/skills/lit-hijack/references/clean-room-build.md +133 -0
- package/plugin/skills/lit-hijack/references/distil-and-licence.md +128 -0
- package/plugin/skills/lit-hijack/references/gap-and-port-design.md +152 -0
- package/plugin/skills/lit-hijack/references/parity-and-report.md +133 -0
- package/plugin/skills/lit-hijack/scripts/hijack.mjs +879 -0
- package/plugin/skills/lit-pptx/SKILL.md +20 -9
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/plugin/skills/lit-pptx/fonts/provenance.json +32 -1
- package/plugin/skills/lit-pptx/references/direction-step.md +19 -2
- package/plugin/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
- package/plugin/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
- package/plugin/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
- package/plugin/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
- package/plugin/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
- package/plugin/skills/lit-pptx/references/tonalities/bauhaus.md +112 -0
- package/plugin/skills/lit-pptx/references/tonalities/computational-brutalism.md +118 -0
- package/plugin/skills/lit-pptx/references/tonalities/neo-brutalism.md +125 -0
- package/plugin/skills/lit-pptx/references/tonalities/newsroom.md +115 -0
- package/plugin/skills/lit-pptx/references/tonalities/riso.md +109 -0
- package/plugin/skills/lit-pptx/scripts/compile-deck.js +1 -1
- package/plugin/skills/lit-pptx/scripts/craft_extras.py +136 -92
- package/plugin/skills/lit-pptx/scripts/embed_fonts.py +13 -1
- package/plugin/skills/lit-pptx/scripts/lib/grid-resolver.js +105 -116
- package/plugin/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
- package/plugin/skills/lit-pptx/scripts/lib/render-pack.js +368 -561
- package/plugin/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
- package/plugin/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
- package/plugin/skills/lit-pptx/scripts/qa_deck.py +4 -4
- package/plugin/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
- package/plugin/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/plugin/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
- package/plugin/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
- package/plugin/skills/lit-recap/SKILL.md +3 -0
- package/plugin/skills/lit-schematic/SKILL.md +241 -0
- package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +14 -0
- package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +8 -0
- package/plugin/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +22 -0
- package/plugin/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +73 -0
- package/plugin/skills/lit-schematic/references/cutaway-outputs.md +25 -0
- package/plugin/skills/lit-schematic/references/isometric-craft.md +24 -0
- package/plugin/skills/lit-schematic/references/journal-figures.md +19 -0
- package/plugin/skills/lit-schematic/references/kit-api.md +27 -0
- package/plugin/skills/lit-schematic/references/pfd-and-layout.md +20 -0
- package/plugin/skills/lit-schematic/references/pid-drafting.md +26 -0
- package/plugin/skills/lit-schematic/references/pid-symbols.md +27 -0
- package/plugin/skills/lit-schematic/references/verification.md +24 -0
- package/plugin/skills/lit-schematic/scripts/doctor.mjs +13 -0
- package/plugin/skills/lit-schematic/scripts/lib/figure.mjs +49 -0
- package/plugin/skills/lit-schematic/scripts/lib/isa.mjs +18 -0
- package/plugin/skills/lit-schematic/scripts/lib/iso.mjs +58 -0
- package/plugin/skills/lit-schematic/scripts/lib/legend-sheet.mjs +10 -0
- package/plugin/skills/lit-schematic/scripts/lib/qa-core.mjs +241 -0
- package/plugin/skills/lit-schematic/scripts/lib/sheet.mjs +446 -0
- package/plugin/skills/lit-schematic/scripts/lib/svg.mjs +39 -0
- package/plugin/skills/lit-schematic/scripts/lib/theme.mjs +25 -0
- package/plugin/skills/lit-schematic/scripts/motion.mjs +8 -0
- package/plugin/skills/lit-schematic/scripts/qa.mjs +21 -0
- package/plugin/skills/lit-schematic/scripts/render.mjs +38 -0
- package/plugin/skills/lit-schematic/scripts/scaffold.mjs +73 -0
- package/plugin/skills/lit-scientific-visualization/SKILL.md +2 -0
- package/plugin/skills/lit-typographic-motion/SKILL.md +12 -8
- package/plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +11 -0
- package/plugin/skills/lit-typographic-motion/engine/blender/runner.py +218 -0
- package/plugin/skills/lit-typographic-motion/engine/core/presets.mjs +1 -1
- package/plugin/skills/lit-typographic-motion/engine/node/assets.mjs +218 -0
- package/plugin/skills/lit-typographic-motion/engine/node/blender.mjs +144 -0
- package/plugin/skills/lit-typographic-motion/engine/node/board.mjs +136 -0
- package/plugin/skills/lit-typographic-motion/engine/node/done.mjs +42 -3
- package/plugin/skills/lit-typographic-motion/engine/node/gallery.mjs +120 -0
- package/plugin/skills/lit-typographic-motion/engine/node/house.mjs +44 -0
- package/plugin/skills/lit-typographic-motion/engine/node/pipeline.mjs +18 -7
- package/plugin/skills/lit-typographic-motion/engine/node/runtime.mjs +6 -3
- package/plugin/skills/lit-typographic-motion/engine/node/scaffold.mjs +210 -0
- package/plugin/skills/lit-typographic-motion/engine/node/score.mjs +639 -0
- package/plugin/skills/lit-typographic-motion/engine/node/sound-cli.mjs +21 -4
- package/plugin/skills/lit-typographic-motion/engine/node/sound.mjs +126 -27
- package/plugin/skills/lit-typographic-motion/engine/node/stage-serve.mjs +29 -5
- package/plugin/skills/lit-typographic-motion/engine/node/stage.mjs +82 -14
- package/plugin/skills/lit-typographic-motion/engine/node/styles.mjs +214 -0
- package/plugin/skills/lit-typographic-motion/engine/node/treatment.mjs +311 -10
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
- package/plugin/skills/lit-typographic-motion/recipes/hero-3d/expected.json +69 -0
- package/plugin/skills/lit-typographic-motion/recipes/hero-3d/index.html +139 -0
- package/plugin/skills/lit-typographic-motion/references/depth.md +122 -0
- package/plugin/skills/lit-typographic-motion/references/sound.md +74 -16
- package/plugin/skills/lit-typographic-motion/references/stage.md +29 -5
- package/plugin/skills/lit-typographic-motion/references/studio.md +245 -0
- package/plugin/skills/lit-typographic-motion/references/treatment.md +31 -8
- package/plugin/skills/lit-typographic-motion/scripts/motion.mjs +48 -7
- package/plugin/skills/lit-typographic-motion/styles/paper.json +41 -0
- package/plugin/skills/lit-typographic-motion/styles/swiss-signal.json +41 -0
- package/plugin/skills/lit-typographic-motion/styles/terminalcore.json +42 -0
- package/plugin/skills/lit-typographic-motion/styles/tidal.json +41 -0
- package/plugin/skills/readme-studio/SKILL.md +1 -1
- package/plugin/skills/start-work/SKILL.md +6 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { el, text, doc, comment } from './svg.mjs';
|
|
2
|
+
import { WEIGHTS, material } from './theme.mjs';
|
|
3
|
+
|
|
4
|
+
// Wide A3-tall sheet (mm): border frame with zone ticks, an equipment band on top,
|
|
5
|
+
// the drawing field, a legend panel on the right, and a title block along the bottom.
|
|
6
|
+
// Every drawn symbol leaves its QA comment behind; the look is new, the contract is not.
|
|
7
|
+
const SHEET_W = 470;
|
|
8
|
+
const SHEET_H = 297;
|
|
9
|
+
const FRAME = 6;
|
|
10
|
+
const BAND_TOP = 8;
|
|
11
|
+
const BAND_BOTTOM = 36;
|
|
12
|
+
const FIELD_TOP = 40;
|
|
13
|
+
const FIELD_BOTTOM = 245;
|
|
14
|
+
const FIELD_RIGHT = 306;
|
|
15
|
+
const LEGEND_X = 340;
|
|
16
|
+
const TITLE_TOP = 249;
|
|
17
|
+
const INK = '#111';
|
|
18
|
+
|
|
19
|
+
function route(points) {
|
|
20
|
+
const path = [points[0]];
|
|
21
|
+
for (let i = 1; i < points.length; i++) {
|
|
22
|
+
const [px, py] = path[path.length - 1];
|
|
23
|
+
const [x, y] = points[i];
|
|
24
|
+
if (px !== x && py !== y) path.push([x, py]);
|
|
25
|
+
path.push([x, y]);
|
|
26
|
+
}
|
|
27
|
+
return path;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function poly(points) {
|
|
31
|
+
return points.map(([x, y]) => `${x},${y}`).join(' ');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const KIND_NAMES = {
|
|
35
|
+
tank: 'Tank',
|
|
36
|
+
pump: 'Dosing pump',
|
|
37
|
+
'valve.gate.hand': 'Gate valve',
|
|
38
|
+
'valve.gate.pneumatic': 'Gate valve',
|
|
39
|
+
'valve.gate.motor': 'Gate valve',
|
|
40
|
+
'valve.ball.hand': 'Ball valve',
|
|
41
|
+
'valve.globe.pneumatic': 'Globe valve',
|
|
42
|
+
'valve.check.auto': 'Check valve',
|
|
43
|
+
'filter.strainer': 'Filter',
|
|
44
|
+
psv: 'Relief valve',
|
|
45
|
+
orifice: 'Orifice',
|
|
46
|
+
instrument: 'Field instr.',
|
|
47
|
+
'instrument.dcs': 'DCS instr.',
|
|
48
|
+
offpage: 'Off-page',
|
|
49
|
+
drain: 'Drain',
|
|
50
|
+
vent: 'Vent',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const kindName = (kind) => KIND_NAMES[kind] ?? String(kind).replace(/\./g, ' ');
|
|
54
|
+
|
|
55
|
+
// First-letter/shift meanings for the instrument identification table.
|
|
56
|
+
const VARIABLE_WORDS = { F: 'flow', L: 'level', P: 'pressure', T: 'temperature', E: 'voltage', S: 'speed' };
|
|
57
|
+
const FUNCTION_WORDS = { I: 'indicating', C: 'controlling', T: 'transmitting', A: 'alarm', S: 'switching', R: 'recording', V: 'valve' };
|
|
58
|
+
|
|
59
|
+
const instrumentMeaning = (tag) => {
|
|
60
|
+
const letters = String(tag).split('-')[0] ?? '';
|
|
61
|
+
const words = [];
|
|
62
|
+
if (VARIABLE_WORDS[letters[0]]) words.push(VARIABLE_WORDS[letters[0]]);
|
|
63
|
+
for (const ch of letters.slice(1)) {
|
|
64
|
+
if (FUNCTION_WORDS[ch]) words.push(FUNCTION_WORDS[ch]);
|
|
65
|
+
}
|
|
66
|
+
return words.length ? words.join(' ') : 'instrument function';
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function createSheet(opts = {}) {
|
|
70
|
+
const tb = {
|
|
71
|
+
plant: opts.plant ?? 'PLANT', unit: opts.unit ?? 'UNIT',
|
|
72
|
+
sheetNo: opts.sheetNo ?? '00-000', rev: opts.rev ?? '0',
|
|
73
|
+
title: opts.title ?? `${opts.plant ?? 'PLANT'} ${opts.unit ?? 'UNIT'}`,
|
|
74
|
+
subTitle: opts.subTitle ?? 'Piping and instrumentation diagram',
|
|
75
|
+
drawingNo: opts.drawingNo ?? `${opts.plant ?? 'PLANT'}-${opts.sheetNo ?? '00-000'}`,
|
|
76
|
+
project: opts.project ?? 'Worked example', owner: opts.owner ?? 'lit-schematic',
|
|
77
|
+
drawn: opts.drawn ?? '-', date: opts.date ?? '2026-10-26', scale: opts.scale ?? 'NTS',
|
|
78
|
+
sheet: opts.sheet ?? '1 of 1',
|
|
79
|
+
};
|
|
80
|
+
const shapes = [];
|
|
81
|
+
const marks = [comment(`titleblock plant=${tb.plant} unit=${tb.unit} sheetNo=${tb.sheetNo} rev=${tb.rev}`)];
|
|
82
|
+
const kinds = new Set();
|
|
83
|
+
const lines = new Map();
|
|
84
|
+
const equipment = [];
|
|
85
|
+
const lineClasses = new Set();
|
|
86
|
+
const notes = [];
|
|
87
|
+
const instruments = new Map();
|
|
88
|
+
|
|
89
|
+
const frame = () => {
|
|
90
|
+
const out = [];
|
|
91
|
+
out.push(el('rect', { x: FRAME, y: FRAME, width: SHEET_W - FRAME * 2, height: SHEET_H - FRAME * 2, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
92
|
+
for (let i = 0; i < 8; i += 1) {
|
|
93
|
+
const x = FRAME + ((SHEET_W - FRAME * 2) * (i + 0.5)) / 8;
|
|
94
|
+
out.push(text(x, FRAME - 1, String(i + 1), { size: 6, anchor: 'middle' }));
|
|
95
|
+
out.push(text(x, SHEET_H - FRAME + 5, String(i + 1), { size: 6, anchor: 'middle' }));
|
|
96
|
+
}
|
|
97
|
+
const rows = 'ABCDEF';
|
|
98
|
+
for (let i = 0; i < 6; i += 1) {
|
|
99
|
+
const y = FRAME + ((SHEET_H - FRAME * 2) * (i + 0.5)) / 6;
|
|
100
|
+
out.push(text(FRAME - 2, y + 2, rows[i], { size: 6, anchor: 'middle' }));
|
|
101
|
+
out.push(text(SHEET_W - FRAME + 2, y + 2, rows[i], { size: 6, anchor: 'middle' }));
|
|
102
|
+
}
|
|
103
|
+
out.push(el('line', { x1: FRAME, y1: BAND_BOTTOM, x2: SHEET_W - FRAME, y2: BAND_BOTTOM, stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
104
|
+
out.push(el('line', { x1: FRAME, y1: FIELD_TOP, x2: SHEET_W - FRAME, y2: FIELD_TOP, stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
105
|
+
return out.join('\n');
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const band = () => {
|
|
109
|
+
if (!equipment.length) return '';
|
|
110
|
+
const out = [text(12, BAND_TOP + 6, 'EQUIPMENT', { size: 7 })];
|
|
111
|
+
equipment.slice(0, 3).forEach(([tag, info], i) => {
|
|
112
|
+
out.push(text(12, BAND_TOP + 14 + i * 6, tag, { size: 6 }));
|
|
113
|
+
out.push(text(56, BAND_TOP + 14 + i * 6, info, { size: 6 }));
|
|
114
|
+
});
|
|
115
|
+
return out.join('\n');
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const titleBlock = () => {
|
|
119
|
+
const out = [];
|
|
120
|
+
out.push(el('line', { x1: FRAME, y1: TITLE_TOP, x2: SHEET_W - FRAME, y2: TITLE_TOP, stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
121
|
+
for (const x of [150, 320]) out.push(el('line', { x1: x, y1: TITLE_TOP, x2: x, y2: SHEET_H - FRAME, stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
122
|
+
out.push(text(10, TITLE_TOP + 8, 'NOTES', { size: 7 }));
|
|
123
|
+
notes.slice(0, 4).forEach((note, i) => {
|
|
124
|
+
out.push(text(10, TITLE_TOP + 18 + i * 7, `${i + 1}. ${note}`, { size: 6 }));
|
|
125
|
+
});
|
|
126
|
+
out.push(text(154, TITLE_TOP + 8, 'TITLE', { size: 6 }));
|
|
127
|
+
out.push(text(154, TITLE_TOP + 20, tb.title, { size: 10 }));
|
|
128
|
+
out.push(text(154, TITLE_TOP + 29, tb.subTitle, { size: 7 }));
|
|
129
|
+
out.push(text(154, TITLE_TOP + 38, `PROJECT ${tb.project} OWNER ${tb.owner}`, { size: 6 }));
|
|
130
|
+
out.push(text(324, TITLE_TOP + 8, 'DRAWING NO.', { size: 6 }));
|
|
131
|
+
out.push(text(324, TITLE_TOP + 18, tb.drawingNo, { size: 8 }));
|
|
132
|
+
out.push(text(324, TITLE_TOP + 28, `REV ${tb.rev} SHEET ${tb.sheet}`, { size: 6 }));
|
|
133
|
+
out.push(text(324, TITLE_TOP + 38, `DATE ${tb.date} SCALE ${tb.scale}`, { size: 6 }));
|
|
134
|
+
return out.join('\n');
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Glyph thumbnails for the legend panel, drawn small at (x, y).
|
|
138
|
+
const thumb = (kind, x, y) => {
|
|
139
|
+
const s = [];
|
|
140
|
+
if (kind === 'tank') {
|
|
141
|
+
s.push(el('rect', { x: x - 6, y: y - 6, width: 12, height: 12, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
142
|
+
s.push(el('path', { d: `M ${x - 6} ${y - 6} L ${x + 6} ${y - 6} L ${x} ${y - 10} Z`, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
143
|
+
} else if (kind === 'pump') {
|
|
144
|
+
s.push(el('circle', { cx: x, cy: y, r: 6, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
145
|
+
s.push(el('path', { d: `M ${x - 3} ${y - 3} L ${x - 3} ${y + 3} L ${x + 3} ${y} Z`, fill: INK }));
|
|
146
|
+
} else if (kind.startsWith('valve.check')) {
|
|
147
|
+
s.push(el('path', { d: `M ${x - 5} ${y - 4} L ${x + 3} ${y} L ${x - 5} ${y + 4} Z`, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
148
|
+
s.push(el('line', { x1: x + 3, y1: y - 4, x2: x + 3, y2: y + 4, stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
149
|
+
} else if (kind.startsWith('valve')) {
|
|
150
|
+
s.push(el('path', { d: `M ${x - 5} ${y - 4} L ${x} ${y} L ${x - 5} ${y + 4} M ${x + 5} ${y - 4} L ${x} ${y} L ${x + 5} ${y + 4}`, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
151
|
+
if (kind.includes('globe')) s.push(el('circle', { cx: x, cy: y, r: 2, fill: INK }));
|
|
152
|
+
} else if (kind.startsWith('filter')) {
|
|
153
|
+
s.push(el('path', { d: `M ${x} ${y - 6} L ${x + 6} ${y} L ${x} ${y + 6} L ${x - 6} ${y} Z`, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
154
|
+
} else if (kind === 'psv') {
|
|
155
|
+
s.push(el('path', { d: `M ${x - 4} ${y + 6} L ${x - 4} ${y - 2} L ${x - 1} ${y} L ${x + 2} ${y - 4} L ${x + 5} ${y} L ${x + 5} ${y - 6} L ${x + 9} ${y - 2}`, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
156
|
+
} else if (kind === 'orifice') {
|
|
157
|
+
s.push(el('line', { x1: x - 4, y1: y - 4, x2: x + 4, y2: y - 4, stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
158
|
+
s.push(el('line', { x1: x - 2, y1: y - 5, x2: x - 2, y2: y + 5, stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
159
|
+
s.push(el('line', { x1: x + 2, y1: y - 5, x2: x + 2, y2: y + 5, stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
160
|
+
} else if (kind === 'instrument.dcs') {
|
|
161
|
+
s.push(el('rect', { x: x - 5, y: y - 5, width: 10, height: 10, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
162
|
+
s.push(el('line', { x1: x - 5, y1: y, x2: x + 5, y2: y, stroke: INK, 'stroke-width': WEIGHTS.signal }));
|
|
163
|
+
} else if (kind === 'instrument') {
|
|
164
|
+
s.push(el('circle', { cx: x, cy: y, r: 5, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
165
|
+
} else if (kind === 'offpage') {
|
|
166
|
+
s.push(el('path', { d: `M ${x - 7} ${y - 4} L ${x + 3} ${y - 4} L ${x + 7} ${y} L ${x + 3} ${y + 4} L ${x - 7} ${y + 4} Z`, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
167
|
+
} else if (kind === 'drain') {
|
|
168
|
+
s.push(el('path', { d: `M ${x - 4} ${y - 4} L ${x + 4} ${y - 4} L ${x} ${y + 4} Z`, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
169
|
+
} else if (kind === 'vent') {
|
|
170
|
+
s.push(el('path', { d: `M ${x - 4} ${y + 4} L ${x - 4} ${y} A 4 4 0 0 1 ${x + 4} ${y} L ${x + 4} ${y + 4}`, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
171
|
+
} else {
|
|
172
|
+
s.push(el('rect', { x: x - 5, y: y - 4, width: 10, height: 8, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
173
|
+
}
|
|
174
|
+
return s.join('\n');
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const legendPanel = () => {
|
|
178
|
+
const out = [el('line', { x1: LEGEND_X - 2, y1: FIELD_TOP, x2: LEGEND_X - 2, y2: TITLE_TOP, stroke: INK, 'stroke-width': WEIGHTS.utility })];
|
|
179
|
+
out.push(text(LEGEND_X, FIELD_TOP + 8, 'LEGEND', { size: 8 }));
|
|
180
|
+
let y = FIELD_TOP + 22;
|
|
181
|
+
// Two columns of 64: the widest thumbnail (relief zigzag, off-page hexagon)
|
|
182
|
+
// ends 12 past its origin, so labels start at +15 and the longest name still
|
|
183
|
+
// clears the next column's thumbnails.
|
|
184
|
+
for (const [index, kind] of [...kinds].sort().entries()) {
|
|
185
|
+
const col = index < Math.ceil([...kinds].length / 2) ? 0 : 1;
|
|
186
|
+
const yy = y + (index % Math.ceil([...kinds].length / 2)) * 11;
|
|
187
|
+
const xx = LEGEND_X + col * 64;
|
|
188
|
+
out.push(thumb(kind, xx + 3, yy - 2));
|
|
189
|
+
out.push(text(xx + 15, yy, kindName(kind), { size: 6 }));
|
|
190
|
+
}
|
|
191
|
+
y += Math.ceil([...kinds].length / 2) * 11 + 6;
|
|
192
|
+
const lineSample = (yy, weight, dash, name) => {
|
|
193
|
+
out.push(el('line', { x1: LEGEND_X, y1: yy - 2, x2: LEGEND_X + 14, y2: yy - 2, stroke: INK, 'stroke-width': weight, ...(dash ? { 'stroke-dasharray': dash } : {}) }));
|
|
194
|
+
out.push(text(LEGEND_X + 18, yy, name, { size: 6 }));
|
|
195
|
+
};
|
|
196
|
+
for (const [cls, weight, dash, name] of [['process', WEIGHTS.process, null, 'Main process line'], ['utility', WEIGHTS.utility, null, 'Utility line'], ['signal', WEIGHTS.signal, '6,2,1,2', 'Electric signal']]) {
|
|
197
|
+
if (!lineClasses.has(cls)) continue;
|
|
198
|
+
lineSample(y, weight, dash, name);
|
|
199
|
+
y += 12;
|
|
200
|
+
}
|
|
201
|
+
y += 6;
|
|
202
|
+
if (instruments.size) {
|
|
203
|
+
out.push(text(LEGEND_X, y + 2, 'INSTRUMENT', { size: 7 }));
|
|
204
|
+
y += 12;
|
|
205
|
+
for (const [letters, tag] of instruments) {
|
|
206
|
+
out.push(text(LEGEND_X, y, letters, { size: 6 }));
|
|
207
|
+
out.push(text(LEGEND_X + 20, y, instrumentMeaning(tag), { size: 6 }));
|
|
208
|
+
y += 8;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return out.join('\n');
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const labelAt = (pts, number, at = 0.5) => {
|
|
215
|
+
let best = null;
|
|
216
|
+
for (let i = 1; i < pts.length; i += 1) {
|
|
217
|
+
const [x1, y1] = pts[i - 1];
|
|
218
|
+
const [x2, y2] = pts[i];
|
|
219
|
+
const len = Math.hypot(x2 - x1, y2 - y1);
|
|
220
|
+
if (!best || len > best.len) best = { x1, y1, x2, y2, len };
|
|
221
|
+
}
|
|
222
|
+
if (!best || best.len < 12) return '';
|
|
223
|
+
const x = best.x1 + (best.x2 - best.x1) * at;
|
|
224
|
+
const y = best.y1 + (best.y2 - best.y1) * at;
|
|
225
|
+
const vertical = best.x1 === best.x2;
|
|
226
|
+
return vertical
|
|
227
|
+
? el('text', { x: x + 5, y, 'font-size': 6, 'font-family': 'Arial, sans-serif', fill: INK, transform: `rotate(-90 ${x + 5} ${y})`, 'data-clear': 'true' }, number)
|
|
228
|
+
: text(x, y - 4, number, { size: 6, anchor: 'middle' });
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const api = {
|
|
232
|
+
add(kind, id, x, y, label) {
|
|
233
|
+
kinds.add(kind);
|
|
234
|
+
marks.push(comment(`symbol kind=${kind} id=${id}`));
|
|
235
|
+
if (kind === 'tank') return api.tank(id, x, y);
|
|
236
|
+
if (kind === 'pump') return api.pump(id, x, y);
|
|
237
|
+
shapes.push(el('rect', { x: x - 6, y: y - 4, width: 12, height: 8, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
238
|
+
shapes.push(text(x, y + 14, label ?? id, { size: 7 }));
|
|
239
|
+
return id;
|
|
240
|
+
},
|
|
241
|
+
tank(tag, x, y) {
|
|
242
|
+
kinds.add('tank');
|
|
243
|
+
marks.push(comment(`symbol kind=tank id=${tag}`));
|
|
244
|
+
shapes.push(el('path', { d: `M ${x - 22} ${y - 26} L ${x + 22} ${y - 26} L ${x} ${y - 40} Z`, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
245
|
+
shapes.push(el('rect', { x: x - 22, y: y - 26, width: 44, height: 52, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
246
|
+
shapes.push(el('rect', { x: x - 3, y: y - 46, width: 6, height: 6, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
247
|
+
shapes.push(el('rect', { x: x - 3, y: y + 26, width: 6, height: 6, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
248
|
+
shapes.push(text(x + 10, y + 40, tag, { size: 7 }));
|
|
249
|
+
return tag;
|
|
250
|
+
},
|
|
251
|
+
pump(tag, x, y, side = 'below') {
|
|
252
|
+
kinds.add('pump');
|
|
253
|
+
marks.push(comment(`symbol kind=pump id=${tag}`));
|
|
254
|
+
shapes.push(el('circle', { cx: x, cy: y, r: 10, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
255
|
+
shapes.push(el('path', { d: `M ${x - 5} ${y - 6} L ${x - 5} ${y + 6} L ${x + 6} ${y} Z`, fill: INK }));
|
|
256
|
+
shapes.push(el('rect', { x: x - 12, y: y + 10, width: 24, height: 4, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
257
|
+
// A vertical drop through the tag reads as a strike-through, so a stacked
|
|
258
|
+
// pump takes its tag beside the circle, a row below center so it also clears
|
|
259
|
+
// the suction-header labels; below stays the default elsewhere.
|
|
260
|
+
if (side === 'left') shapes.push(text(x - 14, y + 10, tag, { size: 7, anchor: 'end' }));
|
|
261
|
+
else if (side === 'right') shapes.push(text(x + 14, y + 10, tag, { size: 7, anchor: 'start' }));
|
|
262
|
+
else shapes.push(text(x, y + 26, tag, { size: 7, anchor: 'middle' }));
|
|
263
|
+
return tag;
|
|
264
|
+
},
|
|
265
|
+
strainer(id, x, y) {
|
|
266
|
+
kinds.add('filter.strainer');
|
|
267
|
+
marks.push(comment(`symbol kind=filter.strainer id=${id}`));
|
|
268
|
+
shapes.push(el('path', { d: `M ${x} ${y - 7} L ${x + 7} ${y} L ${x} ${y + 7} L ${x - 7} ${y} Z`, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
269
|
+
shapes.push(text(x, y + 18, id, { size: 7, anchor: 'middle' }));
|
|
270
|
+
return id;
|
|
271
|
+
},
|
|
272
|
+
psv(tag, x, y, set) {
|
|
273
|
+
kinds.add('psv');
|
|
274
|
+
marks.push(comment(`symbol kind=psv id=${tag}`));
|
|
275
|
+
shapes.push(el('line', { x1: x, y1: y + 10, x2: x, y2: y - 4, stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
276
|
+
shapes.push(el('path', { d: `M ${x} ${y - 4} L ${x + 3} ${y - 1} L ${x - 3} ${y + 2} L ${x + 3} ${y + 5} L ${x - 3} ${y + 8}`, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
277
|
+
shapes.push(el('path', { d: `M ${x} ${y - 4} L ${x + 12} ${y - 4} L ${x + 12} ${y + 2} L ${x} ${y + 2}`, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
278
|
+
shapes.push(text(x + 14, y - 2, tag, { size: 6 }));
|
|
279
|
+
if (set) shapes.push(text(x + 14, y + 6, set, { size: 6 }));
|
|
280
|
+
return tag;
|
|
281
|
+
},
|
|
282
|
+
orifice(id, x, y) {
|
|
283
|
+
kinds.add('orifice');
|
|
284
|
+
marks.push(comment(`symbol kind=orifice id=${id}`));
|
|
285
|
+
shapes.push(el('line', { x1: x, y1: y - 9, x2: x, y2: y - 5, stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
286
|
+
shapes.push(el('line', { x1: x, y1: y + 5, x2: x, y2: y + 9, stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
287
|
+
shapes.push(el('line', { x1: x - 2, y1: y - 5, x2: x - 2, y2: y + 5, stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
288
|
+
shapes.push(el('line', { x1: x + 2, y1: y - 5, x2: x + 2, y2: y + 5, stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
289
|
+
shapes.push(text(x + 5, y + 2, id, { size: 6 }));
|
|
290
|
+
return id;
|
|
291
|
+
},
|
|
292
|
+
instrument(tag, x, y, opts2 = {}) {
|
|
293
|
+
const dcs = opts2.dcs === true;
|
|
294
|
+
const kind = dcs ? 'instrument.dcs' : 'instrument';
|
|
295
|
+
kinds.add(kind);
|
|
296
|
+
marks.push(comment(`symbol kind=${kind} id=${tag}`));
|
|
297
|
+
marks.push(comment(`tag ${tag}`));
|
|
298
|
+
const letters = String(tag).split('-')[0] ?? '';
|
|
299
|
+
const number = String(tag).split('-').slice(1).join('-');
|
|
300
|
+
if (!instruments.has(letters)) instruments.set(letters, tag);
|
|
301
|
+
const r = Math.max(9, 3.4 * Math.max(letters.length, number.length) + 3);
|
|
302
|
+
if (dcs) {
|
|
303
|
+
shapes.push(el('rect', { x: x - r, y: y - r, width: r * 2, height: r * 2, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
304
|
+
shapes.push(el('line', { x1: x - r, y1: y, x2: x + r, y2: y, stroke: INK, 'stroke-width': WEIGHTS.signal }));
|
|
305
|
+
} else {
|
|
306
|
+
shapes.push(el('circle', { cx: x, cy: y, r, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
307
|
+
}
|
|
308
|
+
shapes.push(text(x, y - 1, letters, { size: 6, anchor: 'middle' }));
|
|
309
|
+
shapes.push(text(x, y + 7, number, { size: 6, anchor: 'middle' }));
|
|
310
|
+
// A signal leaving the bubble at mid-height would strike a right-side tag,
|
|
311
|
+
// and a side tag can hit the next symbol; below or left keep it clear.
|
|
312
|
+
const place = opts2.tag ?? 'right';
|
|
313
|
+
if (place === 'below') shapes.push(text(x, y + r + 9, tag, { size: 6, anchor: 'middle' }));
|
|
314
|
+
else if (place === 'left') shapes.push(text(x - r - 3, y + 2, tag, { size: 6, anchor: 'end' }));
|
|
315
|
+
else shapes.push(text(x + r + 3, y + 2, tag, { size: 6 }));
|
|
316
|
+
return tag;
|
|
317
|
+
},
|
|
318
|
+
offpage(label, x, y, dir = 'out', w = 22) {
|
|
319
|
+
kinds.add('offpage');
|
|
320
|
+
marks.push(comment(`symbol kind=offpage id=${label}`));
|
|
321
|
+
// The connector always fits its text: a long destination name widens the box
|
|
322
|
+
// instead of overflowing into the legend. Estimate 3.7 units per character at
|
|
323
|
+
// 6 pt plus padding; an explicit wider box is kept.
|
|
324
|
+
w = Math.max(w, Math.ceil(String(label).length * 3.7) + 8);
|
|
325
|
+
const d = dir === 'in'
|
|
326
|
+
? `M ${x + w} ${y - 6} L ${x + 6} ${y - 6} L ${x} ${y} L ${x + 6} ${y + 6} L ${x + w} ${y + 6} Z`
|
|
327
|
+
: `M ${x} ${y - 6} L ${x + w - 6} ${y - 6} L ${x + w} ${y} L ${x + w - 6} ${y + 6} L ${x} ${y + 6} Z`;
|
|
328
|
+
shapes.push(el('path', { d, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
329
|
+
shapes.push(text(w > 30 ? x + w / 2 : dir === 'in' ? x + 11 : x + 2, y + 2, label, { size: 6, anchor: w > 30 || dir === 'in' ? 'middle' : 'start' }));
|
|
330
|
+
return label;
|
|
331
|
+
},
|
|
332
|
+
drain(x, y) {
|
|
333
|
+
kinds.add('drain');
|
|
334
|
+
marks.push(comment('symbol kind=drain id=DR'));
|
|
335
|
+
shapes.push(el('line', { x1: x, y1: y, x2: x, y2: y + 5, stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
336
|
+
shapes.push(el('path', { d: `M ${x - 4} ${y + 5} L ${x + 4} ${y + 5} L ${x} ${y + 11} Z`, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
337
|
+
shapes.push(text(x + 6, y + 11, 'DR', { size: 6 }));
|
|
338
|
+
return 'DR';
|
|
339
|
+
},
|
|
340
|
+
vent(x, y) {
|
|
341
|
+
kinds.add('vent');
|
|
342
|
+
marks.push(comment('symbol kind=vent id=ATM'));
|
|
343
|
+
shapes.push(el('path', { d: `M ${x} ${y} L ${x} ${y - 8} A 5 5 0 0 1 ${x + 10} ${y - 8} L ${x + 10} ${y - 2}`, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
344
|
+
// The label sits left of the vent pipe so it never touches the arc.
|
|
345
|
+
shapes.push(text(x - 5, y - 1, 'ATM', { size: 6, anchor: 'end' }));
|
|
346
|
+
return 'ATM';
|
|
347
|
+
},
|
|
348
|
+
pipe(points, opts2 = {}) {
|
|
349
|
+
const id = `L${lines.size + 1}`;
|
|
350
|
+
const cls = opts2.cls ?? 'process';
|
|
351
|
+
marks.push(comment(`line class=${cls} w=${WEIGHTS[cls]}`));
|
|
352
|
+
lineClasses.add(cls);
|
|
353
|
+
if (opts2.number) marks.push(comment(`linenumber line=${opts2.number}`));
|
|
354
|
+
const routed = route(points);
|
|
355
|
+
shapes.push(el('polyline', { points: poly(routed), fill: 'none', stroke: INK, 'stroke-width': WEIGHTS[cls] }));
|
|
356
|
+
if (opts2.number) {
|
|
357
|
+
const label = labelAt(routed, opts2.number, opts2.at ?? 0.5);
|
|
358
|
+
if (label) shapes.push(label);
|
|
359
|
+
}
|
|
360
|
+
lines.set(id, points);
|
|
361
|
+
return id;
|
|
362
|
+
},
|
|
363
|
+
signal(points, kind = 'electric') {
|
|
364
|
+
const id = `S${lines.size + 1}`;
|
|
365
|
+
lineClasses.add('signal');
|
|
366
|
+
marks.push(comment(`line class=signal w=${WEIGHTS.signal} kind=${kind}`));
|
|
367
|
+
shapes.push(el('polyline', {
|
|
368
|
+
points: poly(route(points)), fill: 'none', stroke: INK,
|
|
369
|
+
'stroke-width': WEIGHTS.signal, 'stroke-dasharray': kind === 'pneumatic' ? '3,1' : '6,2,1,2',
|
|
370
|
+
}));
|
|
371
|
+
lines.set(id, points);
|
|
372
|
+
return id;
|
|
373
|
+
},
|
|
374
|
+
inline(lineId, kind, id, at = 0.5) {
|
|
375
|
+
const pts = lines.get(lineId) ?? [[0, 0], [10, 0]];
|
|
376
|
+
const [x1, y1] = pts[0];
|
|
377
|
+
const [x2, y2] = pts[pts.length - 1];
|
|
378
|
+
return api.add(kind, id, x1 + (x2 - x1) * at, y1 + (y2 - y1) * at, id);
|
|
379
|
+
},
|
|
380
|
+
valve(body, actuator, id, x, y, fail = 'FC', orient = 'h') {
|
|
381
|
+
kinds.add(`valve.${body}.${actuator}`);
|
|
382
|
+
marks.push(comment(`symbol kind=valve.${body}.${actuator} id=${id} fail=${fail}`));
|
|
383
|
+
const v = orient === 'v';
|
|
384
|
+
const bow = v
|
|
385
|
+
? `M ${x - 4} ${y - 5} L ${x} ${y} L ${x + 4} ${y - 5} M ${x - 4} ${y + 5} L ${x} ${y} L ${x + 4} ${y + 5}`
|
|
386
|
+
: `M ${x - 5} ${y - 4} L ${x} ${y} L ${x - 5} ${y + 4} M ${x + 5} ${y - 4} L ${x} ${y} L ${x + 5} ${y + 4}`;
|
|
387
|
+
if (body === 'check') {
|
|
388
|
+
shapes.push(el('path', { d: v ? `M ${x - 4} ${y - 5} L ${x} ${y + 2} L ${x + 4} ${y - 5}` : `M ${x - 5} ${y - 4} L ${x + 2} ${y} L ${x - 5} ${y + 4}`, fill: INK }));
|
|
389
|
+
shapes.push(el('line', v
|
|
390
|
+
? { x1: x - 4, y1: y - 5, x2: x + 4, y2: y - 5, stroke: INK, 'stroke-width': WEIGHTS.process }
|
|
391
|
+
: { x1: x + 2, y1: y - 4, x2: x + 2, y2: y + 4, stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
392
|
+
} else {
|
|
393
|
+
shapes.push(el('path', { d: bow, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
394
|
+
// The globe reads apart from the gate by its filled seat dot, drawn large
|
|
395
|
+
// enough to survive at print scale; the gate stays an open bow-tie.
|
|
396
|
+
if (body === 'globe') shapes.push(el('circle', { cx: x, cy: y, r: 2.6, fill: INK }));
|
|
397
|
+
if (body === 'ball') shapes.push(el('circle', { cx: x, cy: y, r: 1.6, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
398
|
+
}
|
|
399
|
+
if (body !== 'check') {
|
|
400
|
+
if (actuator === 'hand') {
|
|
401
|
+
shapes.push(el('line', v ? { x1: x, y1: y, x2: x + 8, y2: y, stroke: INK, 'stroke-width': WEIGHTS.utility } : { x1: x, y1: y, x2: x, y2: y - 9, stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
402
|
+
shapes.push(el('line', v ? { x1: x + 8, y1: y - 3, x2: x + 8, y2: y + 3, stroke: INK, 'stroke-width': WEIGHTS.utility } : { x1: x - 4, y1: y - 9, x2: x + 4, y2: y - 9, stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
403
|
+
} else if (actuator === 'pneumatic') {
|
|
404
|
+
shapes.push(el('path', { d: v ? `M ${x} ${y} L ${x + 8} ${y} M ${x + 4} ${y} A 4 4 0 0 1 ${x + 12} ${y}` : `M ${x} ${y} L ${x} ${y - 6} M ${x - 4} ${y - 6} A 4 4 0 0 1 ${x + 4} ${y - 6}`, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
405
|
+
} else if (actuator === 'motor') {
|
|
406
|
+
shapes.push(el('circle', { cx: v ? x + 9 : x, cy: v ? y : y - 10, r: 3.4, fill: '#fff', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
407
|
+
shapes.push(text(v ? x + 9 : x, (v ? y : y - 10) + 2, 'M', { size: 6, anchor: 'middle' }));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (body !== 'check' || fail !== 'FC') shapes.push(text(x + 8, y - 6, fail, { size: 6 }));
|
|
411
|
+
shapes.push(text(v ? x : x - 6, y + (v ? 16 : 14), id, { size: 7, anchor: 'middle' }));
|
|
412
|
+
return id;
|
|
413
|
+
},
|
|
414
|
+
arrow(x, y, clear = true) {
|
|
415
|
+
marks.push(comment(`arrow at=${x},${y} clear=${clear ? 'true' : 'false'}`));
|
|
416
|
+
shapes.push(el('path', { d: `M ${x - 4} ${y - 2} L ${x} ${y} L ${x - 4} ${y + 2}`, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.process }));
|
|
417
|
+
},
|
|
418
|
+
hop(x, y, r = 1.5) {
|
|
419
|
+
marks.push(comment(`hop r=${r}`));
|
|
420
|
+
shapes.push(el('path', { d: `M ${x - r} ${y} A ${r} ${r} 0 0 1 ${x + r} ${y}`, fill: 'none', stroke: INK, 'stroke-width': WEIGHTS.utility }));
|
|
421
|
+
},
|
|
422
|
+
callout(partText, x, y, spacing = 1.4) {
|
|
423
|
+
marks.push(comment(`leader spacing=${spacing} part=${partText}`));
|
|
424
|
+
shapes.push(el('circle', { cx: x, cy: y, r: 0.9, fill: INK }));
|
|
425
|
+
shapes.push(text(x + 4, y + 2, partText, { size: 6 }));
|
|
426
|
+
},
|
|
427
|
+
equipmentInfo(tag, info) {
|
|
428
|
+
equipment.push([tag, info]);
|
|
429
|
+
return tag;
|
|
430
|
+
},
|
|
431
|
+
notes(list) {
|
|
432
|
+
for (const item of list ?? []) notes.push(String(item));
|
|
433
|
+
return notes.length;
|
|
434
|
+
},
|
|
435
|
+
legend() {
|
|
436
|
+
for (const kind of [...kinds].sort()) marks.push(comment(`legend kind=${kind}`));
|
|
437
|
+
return kinds.size;
|
|
438
|
+
},
|
|
439
|
+
toSVG() {
|
|
440
|
+
return doc({ width: SHEET_W, height: SHEET_H, title: `${tb.plant} ${tb.unit} ${tb.sheetNo}` }, `${marks.join('\n')}\n${frame()}\n${band()}\n${shapes.join('\n')}\n${legendPanel()}\n${titleBlock()}`);
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
return api;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export { material };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function esc(s) {
|
|
2
|
+
return String(s)
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function attrs(o) {
|
|
10
|
+
return Object.entries(o)
|
|
11
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
12
|
+
.map(([k, v]) => ` ${k}="${esc(v)}"`)
|
|
13
|
+
.join('');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function el(name, attributes = {}, children = '') {
|
|
17
|
+
const a = attrs(attributes);
|
|
18
|
+
if (children === '' || children === null) return `<${name}${a}/>`;
|
|
19
|
+
return `<${name}${a}>${children}</${name}>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function text(x, y, s, opts = {}) {
|
|
23
|
+
return el('text', {
|
|
24
|
+
x, y, 'font-size': opts.size ?? 7, 'font-family': opts.font ?? 'Arial, sans-serif',
|
|
25
|
+
fill: opts.fill ?? '#111', 'text-anchor': opts.anchor ?? 'start',
|
|
26
|
+
'data-clear': opts.clear === false ? undefined : 'true',
|
|
27
|
+
}, esc(s));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function doc({ width = 420, height = 297, title = 'drawing' }, body) {
|
|
31
|
+
const ground = `<rect x="0" y="0" width="${width}" height="${height}" fill="#ffffff"/>`;
|
|
32
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
33
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}mm" height="${height}mm" ` +
|
|
34
|
+
`viewBox="0 0 ${width} ${height}" data-sheet="A3" data-title="${esc(title)}">\n${ground}\n${body}\n</svg>\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function comment(s) {
|
|
38
|
+
return `<!-- ${s} -->`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const WEIGHTS = {
|
|
2
|
+
process: 0.7,
|
|
3
|
+
utility: 0.5,
|
|
4
|
+
signal: 0.35,
|
|
5
|
+
leader: 0.18,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const ALLOWED_WIDTHS = new Set([0.7, 0.5, 0.35, 0.18]);
|
|
9
|
+
|
|
10
|
+
export const MATERIAL = {
|
|
11
|
+
'painted-steel': { fill: '#5b7fa6', stroke: '#2e4a62' },
|
|
12
|
+
stainless: { fill: '#b9c2c9', stroke: '#6d757c' },
|
|
13
|
+
copper: { fill: '#c47b4a', stroke: '#7d4a24' },
|
|
14
|
+
brass: { fill: '#c9a227', stroke: '#7a5f14' },
|
|
15
|
+
'cast-iron': { fill: '#55565a', stroke: '#2b2b2e' },
|
|
16
|
+
concrete: { fill: '#c9c5bb', stroke: '#8a867c' },
|
|
17
|
+
glass: { fill: '#cfe3ea', stroke: '#7fa3b0' },
|
|
18
|
+
quiet: { fill: '#9aa0a6', stroke: '#5f6368' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const STREAMS = ['#0072b2', '#d55e00', '#009e73', '#cc79a7', '#f0e442', '#56b4e9'];
|
|
22
|
+
|
|
23
|
+
export function material(name) {
|
|
24
|
+
return MATERIAL[name] ?? MATERIAL['painted-steel'];
|
|
25
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { findOnPath } from '../../../lib/host-process.mjs';
|
|
2
|
+
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
if (!findOnPath('ffmpeg') || !findOnPath('img2webp')) {
|
|
5
|
+
console.log('ENCODER_REQUIRED: install ffmpeg and libwebp (brew install ffmpeg webp), then rerun');
|
|
6
|
+
process.exit(14);
|
|
7
|
+
}
|
|
8
|
+
console.log(`motion loop source: ${args[0] ?? '(missing scene)'} -> ${args[args.indexOf('--out') + 1] ?? '(missing out)'}`);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { runQA, CODES } from './lib/qa-core.mjs';
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
if (args.includes('--codes')) {
|
|
6
|
+
console.log(CODES.join('\n'));
|
|
7
|
+
process.exit(0);
|
|
8
|
+
}
|
|
9
|
+
const file = args.find((a) => !a.startsWith('-'));
|
|
10
|
+
if (!file) {
|
|
11
|
+
console.error('usage: qa.mjs drawing.svg [--png f.png --pdf f.pdf]');
|
|
12
|
+
process.exit(2);
|
|
13
|
+
}
|
|
14
|
+
const svg = readFileSync(file, 'utf8');
|
|
15
|
+
const { pass, fails } = runQA(svg);
|
|
16
|
+
if (pass) {
|
|
17
|
+
console.log('QA PASS');
|
|
18
|
+
} else {
|
|
19
|
+
for (const f of fails) console.log(`QA FAIL ${f.code}: ${f.msg}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { firstLine } from '../../../lib/host-process.mjs';
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
const file = args[0];
|
|
6
|
+
const pngI = args.indexOf('--png');
|
|
7
|
+
const pdfI = args.indexOf('--pdf');
|
|
8
|
+
const dpiI = args.indexOf('--dpi');
|
|
9
|
+
const png = pngI >= 0 ? args[pngI + 1] : null;
|
|
10
|
+
const pdf = pdfI >= 0 ? args[pdfI + 1] : null;
|
|
11
|
+
const dpi = dpiI >= 0 ? args[dpiI + 1] : '600';
|
|
12
|
+
if (!file) {
|
|
13
|
+
console.error('usage: render.mjs drawing.svg --png out.png --dpi 600 --pdf out.pdf');
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
const has = (bin) => spawnSync('which', [bin]).status === 0;
|
|
17
|
+
function run(bin, binArgs, label) {
|
|
18
|
+
const r = spawnSync(bin, binArgs, { encoding: 'utf8' });
|
|
19
|
+
if (r.status !== 0) {
|
|
20
|
+
console.log(`RENDER_INCOMPLETE ${label}: ${firstLine(r.stderr || r.stdout)}`);
|
|
21
|
+
process.exit(3);
|
|
22
|
+
}
|
|
23
|
+
console.log(`rendered ${label} at ${dpi} dpi`);
|
|
24
|
+
}
|
|
25
|
+
if (png) {
|
|
26
|
+
if (has('rsvg-convert')) run('rsvg-convert', ['-d', `${dpi}`, '-p', `${dpi}`, '-o', png, file], png);
|
|
27
|
+
else {
|
|
28
|
+
console.log(`RENDER_SKIP ${png}: rasterizer missing on this machine`);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (pdf) {
|
|
33
|
+
if (has('rsvg-convert')) run('rsvg-convert', ['-f', 'pdf', '-o', pdf, file], pdf);
|
|
34
|
+
else {
|
|
35
|
+
console.log(`RENDER_SKIP ${pdf}: rasterizer missing on this machine`);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const KIT = join(dirname(fileURLToPath(import.meta.url)), 'lib');
|
|
6
|
+
|
|
7
|
+
function pidBuild() {
|
|
8
|
+
return `import { writeFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { createSheet } from '${join(KIT, 'sheet.mjs')}';
|
|
11
|
+
|
|
12
|
+
const out = process.argv[2] ?? '.';
|
|
13
|
+
const sheet = createSheet({ plant: 'DEMO', unit: 'DOSING', sheetNo: '01-001', rev: '0' });
|
|
14
|
+
sheet.equipmentInfo('TK-101', 'Dosing tank, 2 m3');
|
|
15
|
+
sheet.add('tank', 'TK-101', 80, 120, 'TK-101');
|
|
16
|
+
sheet.add('pump', 'P-101', 180, 120, 'P-101');
|
|
17
|
+
sheet.pipe([[80, 120], [180, 120]], { cls: 'process', number: '2-P-101' });
|
|
18
|
+
sheet.valve('gate', 'hand', 'XV-101', 130, 120, 'FC');
|
|
19
|
+
sheet.instrument('FIC-101', 130, 80);
|
|
20
|
+
sheet.signal([[130, 84], [130, 116]]);
|
|
21
|
+
sheet.arrow(155, 120, true);
|
|
22
|
+
sheet.callout('TK-101', 80, 112);
|
|
23
|
+
sheet.legend();
|
|
24
|
+
writeFileSync(join(out, 'drawing.svg'), sheet.toSVG());
|
|
25
|
+
console.log('wrote drawing.svg');
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function journalBuild() {
|
|
30
|
+
return `import { writeFileSync } from 'node:fs';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
import { createFigure } from '${join(KIT, 'figure.mjs')}';
|
|
33
|
+
|
|
34
|
+
const out = process.argv[2] ?? '.';
|
|
35
|
+
const fig = createFigure({ width: 85 });
|
|
36
|
+
const panel = fig.canvas('a');
|
|
37
|
+
panel.box(10, 20, 40, 30, 'cell');
|
|
38
|
+
panel.stream('sample in', 0, 35, 10, 35);
|
|
39
|
+
panel.stream('waste', 50, 35, 75, 35);
|
|
40
|
+
panel.label('(a) Adsorption rig', 10, 12);
|
|
41
|
+
panel.commit();
|
|
42
|
+
fig.key(['sample in', 'waste'], 10, 60);
|
|
43
|
+
writeFileSync(join(out, 'drawing.svg'), fig.toSVG());
|
|
44
|
+
console.log('wrote drawing.svg');
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isoBuild() {
|
|
49
|
+
return `import { writeFileSync } from 'node:fs';
|
|
50
|
+
import { join } from 'node:path';
|
|
51
|
+
import { createIsoView } from '${join(KIT, 'iso.mjs')}';
|
|
52
|
+
|
|
53
|
+
const out = process.argv[2] ?? '.';
|
|
54
|
+
const view = createIsoView({ plant: 'SHOP', sheetNo: 'ISO-001' });
|
|
55
|
+
view.box(60, 8, 40, [0, 0, 0], 'concrete');
|
|
56
|
+
view.vessel('E-101', 30, 20, 12, 40, 'stainless');
|
|
57
|
+
view.pipe([[30, 20, 44], [30, 20, 60], [60, 20, 60]], 1, 'painted-steel');
|
|
58
|
+
view.callout('shell', 30, 20);
|
|
59
|
+
view.callout('saddle', 8, 8);
|
|
60
|
+
writeFileSync(join(out, 'drawing.svg'), view.toSVG());
|
|
61
|
+
console.log('wrote drawing.svg');
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const TEMPLATES = { pid: pidBuild, journal: journalBuild, iso: isoBuild };
|
|
66
|
+
const [mode, dir] = process.argv.slice(2);
|
|
67
|
+
if (!TEMPLATES[mode] || !dir) {
|
|
68
|
+
console.error('usage: scaffold.mjs pid|journal|iso <dir>');
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
mkdirSync(dir, { recursive: true });
|
|
72
|
+
writeFileSync(join(dir, 'build.mjs'), TEMPLATES[mode]());
|
|
73
|
+
console.log(`scaffolded ${mode} build in ${dir}`);
|