compasso 0.1.0
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/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/chunk-5RRRE2GF.js +1125 -0
- package/dist/chunk-5RRRE2GF.js.map +1 -0
- package/dist/chunk-E456YKAJ.js +86 -0
- package/dist/chunk-E456YKAJ.js.map +1 -0
- package/dist/chunk-L5CYESBI.js +208 -0
- package/dist/chunk-L5CYESBI.js.map +1 -0
- package/dist/core/index.cjs +98 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +36 -0
- package/dist/core/index.d.ts +36 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -0
- package/dist/ecomap/index.cjs +287 -0
- package/dist/ecomap/index.cjs.map +1 -0
- package/dist/ecomap/index.d.cts +53 -0
- package/dist/ecomap/index.d.ts +53 -0
- package/dist/ecomap/index.js +4 -0
- package/dist/ecomap/index.js.map +1 -0
- package/dist/genogram/index.cjs +1222 -0
- package/dist/genogram/index.cjs.map +1 -0
- package/dist/genogram/index.d.cts +149 -0
- package/dist/genogram/index.d.ts +149 -0
- package/dist/genogram/index.js +4 -0
- package/dist/genogram/index.js.map +1 -0
- package/dist/index.cjs +1441 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/kinship-BARO5-qz.d.cts +115 -0
- package/dist/kinship-Bkf87Jhu.d.ts +115 -0
- package/dist/locales/pt-br.cjs +123 -0
- package/dist/locales/pt-br.cjs.map +1 -0
- package/dist/locales/pt-br.d.cts +11 -0
- package/dist/locales/pt-br.d.ts +11 -0
- package/dist/locales/pt-br.js +117 -0
- package/dist/locales/pt-br.js.map +1 -0
- package/dist/stroke-MQ427drt.d.cts +35 -0
- package/dist/stroke-MQ427drt.d.ts +35 -0
- package/package.json +72 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { clampLabel, estimateTextWidth, qualityLineStyle, EDGE_STROKE, xmlEscape, FONT_FAMILY, wrapLabel } from './chunk-E456YKAJ.js';
|
|
2
|
+
|
|
3
|
+
// src/ecomap/render.ts
|
|
4
|
+
var ECOMAP_LABELS_EN = {
|
|
5
|
+
bondStyles: {
|
|
6
|
+
close: "Close",
|
|
7
|
+
distant: "Distant",
|
|
8
|
+
conflict: "Conflictual",
|
|
9
|
+
cutoff: "Cut off (no contact)"
|
|
10
|
+
},
|
|
11
|
+
neutralTie: "Declared tie",
|
|
12
|
+
direction: "Declared direction of the tie",
|
|
13
|
+
ariaLabel: "Ecomap"
|
|
14
|
+
};
|
|
15
|
+
var PADDING = 32;
|
|
16
|
+
var LINE_H = 14;
|
|
17
|
+
var NODE_PAD_X = 14;
|
|
18
|
+
var NODE_PAD_Y = 9;
|
|
19
|
+
var CENTER_MIN_R = 36;
|
|
20
|
+
var RING_MIN_R = 150;
|
|
21
|
+
var NODE_GAP = 24;
|
|
22
|
+
var RADIAL_GAP = 40;
|
|
23
|
+
var RING_GAP = 26;
|
|
24
|
+
var SINGLE_RING_MAX = 8;
|
|
25
|
+
var NODE_STROKE = "#52525b";
|
|
26
|
+
var LABEL_FILL = "#3f3f46";
|
|
27
|
+
var EDGE_INK = "#71717a";
|
|
28
|
+
var LEGEND_ROW_H = 18;
|
|
29
|
+
var LEGEND_PAD = 16;
|
|
30
|
+
var LEGEND_SWATCH_W = 22;
|
|
31
|
+
var LEGEND_GAP = 14;
|
|
32
|
+
var LEGEND_FONT = 11;
|
|
33
|
+
var round = (n) => Math.round(n * 100) / 100;
|
|
34
|
+
function wrapTieLabel(displayLabel) {
|
|
35
|
+
const perLine = Math.min(26, Math.max(14, Math.ceil(displayLabel.length / 2) + 2));
|
|
36
|
+
return wrapLabel(displayLabel, perLine);
|
|
37
|
+
}
|
|
38
|
+
function arrowHead(tipX, tipY, ux, uy, opacity) {
|
|
39
|
+
const LEN = 9;
|
|
40
|
+
const HALF_W = 4.5;
|
|
41
|
+
const bx = tipX - ux * LEN;
|
|
42
|
+
const by = tipY - uy * LEN;
|
|
43
|
+
const px = -uy;
|
|
44
|
+
const py = ux;
|
|
45
|
+
const points = [
|
|
46
|
+
`${round(tipX)},${round(tipY)}`,
|
|
47
|
+
`${round(bx + px * HALF_W)},${round(by + py * HALF_W)}`,
|
|
48
|
+
`${round(bx - px * HALF_W)},${round(by - py * HALF_W)}`
|
|
49
|
+
].join(" ");
|
|
50
|
+
return `<polygon points="${points}" fill="${EDGE_INK}" fill-opacity="${opacity}"/>`;
|
|
51
|
+
}
|
|
52
|
+
function ecomapSvg(input, opts = {}) {
|
|
53
|
+
const fontSize = opts.fontSize ?? 12;
|
|
54
|
+
const labels = opts.labels ?? ECOMAP_LABELS_EN;
|
|
55
|
+
const sats = [...input.ties].sort((a, b) => a.id - b.id).map((tie) => {
|
|
56
|
+
const lines = wrapTieLabel(clampLabel(tie.label, opts.maxLabelChars));
|
|
57
|
+
const w2 = Math.max(...lines.map((l) => estimateTextWidth(l, fontSize))) + NODE_PAD_X * 2;
|
|
58
|
+
const h2 = lines.length * LINE_H + NODE_PAD_Y * 2;
|
|
59
|
+
return {
|
|
60
|
+
tie,
|
|
61
|
+
lines,
|
|
62
|
+
rx: Math.max(40, w2 / 2),
|
|
63
|
+
ry: Math.max(20, h2 / 2),
|
|
64
|
+
x: 0,
|
|
65
|
+
y: 0,
|
|
66
|
+
angle: 0,
|
|
67
|
+
style: qualityLineStyle(tie.quality, opts.qualityLexicon)
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
const centerLines = wrapTieLabel(input.centerLabel);
|
|
71
|
+
const centerR = Math.max(
|
|
72
|
+
CENTER_MIN_R,
|
|
73
|
+
Math.max(...centerLines.map((l) => estimateTextWidth(l, fontSize))) / 2 + 12,
|
|
74
|
+
centerLines.length * LINE_H / 2 + 12
|
|
75
|
+
);
|
|
76
|
+
const n = sats.length;
|
|
77
|
+
const maxRx = n > 0 ? Math.max(...sats.map((s) => s.rx)) : 0;
|
|
78
|
+
const maxRy = n > 0 ? Math.max(...sats.map((s) => s.ry)) : 0;
|
|
79
|
+
const twoRings = n > SINGLE_RING_MAX;
|
|
80
|
+
const stepAngle = n > 0 ? Math.PI * 2 / n : Math.PI;
|
|
81
|
+
const safeDist = Math.hypot(2 * maxRx + NODE_GAP, 2 * maxRy + NODE_GAP);
|
|
82
|
+
const sameRingRadius = (steps) => {
|
|
83
|
+
const theta = steps * stepAngle;
|
|
84
|
+
if (n <= 1 || theta >= Math.PI) return 0;
|
|
85
|
+
return safeDist / (2 * Math.sin(theta / 2));
|
|
86
|
+
};
|
|
87
|
+
const ringStep = maxRy * 2 + RING_GAP;
|
|
88
|
+
let innerR = Math.max(
|
|
89
|
+
RING_MIN_R,
|
|
90
|
+
centerR + RADIAL_GAP + maxRy,
|
|
91
|
+
sameRingRadius(twoRings ? 2 : 1)
|
|
92
|
+
);
|
|
93
|
+
if (twoRings) {
|
|
94
|
+
const crossDist = (r) => Math.sqrt(r * r + (r + ringStep) ** 2 - 2 * r * (r + ringStep) * Math.cos(stepAngle));
|
|
95
|
+
while (crossDist(innerR) < safeDist) innerR += 8;
|
|
96
|
+
}
|
|
97
|
+
const outerR = innerR + ringStep;
|
|
98
|
+
for (let i = 0; i < n; i++) {
|
|
99
|
+
const s = sats[i];
|
|
100
|
+
s.angle = -Math.PI / 2 + i * Math.PI * 2 / n;
|
|
101
|
+
const r = twoRings && i % 2 === 1 ? outerR : innerR;
|
|
102
|
+
s.x = r * Math.cos(s.angle);
|
|
103
|
+
s.y = r * Math.sin(s.angle);
|
|
104
|
+
}
|
|
105
|
+
let minX = -centerR;
|
|
106
|
+
let minY = -centerR;
|
|
107
|
+
let maxX = centerR;
|
|
108
|
+
let maxY = centerR;
|
|
109
|
+
for (const s of sats) {
|
|
110
|
+
minX = Math.min(minX, s.x - s.rx);
|
|
111
|
+
minY = Math.min(minY, s.y - s.ry);
|
|
112
|
+
maxX = Math.max(maxX, s.x + s.rx);
|
|
113
|
+
maxY = Math.max(maxY, s.y + s.ry);
|
|
114
|
+
}
|
|
115
|
+
const dx = PADDING - minX;
|
|
116
|
+
const dy = PADDING - minY;
|
|
117
|
+
const cx = dx;
|
|
118
|
+
const cy = dy;
|
|
119
|
+
for (const s of sats) {
|
|
120
|
+
s.x += dx;
|
|
121
|
+
s.y += dy;
|
|
122
|
+
}
|
|
123
|
+
let width = maxX - minX + PADDING * 2;
|
|
124
|
+
let height = maxY - minY + PADDING * 2;
|
|
125
|
+
const parts = [];
|
|
126
|
+
for (const s of sats) {
|
|
127
|
+
const ux = (cx - s.x) / Math.hypot(cx - s.x, cy - s.y);
|
|
128
|
+
const uy = (cy - s.y) / Math.hypot(cx - s.x, cy - s.y);
|
|
129
|
+
const scale = 1 / Math.hypot(ux / s.rx, uy / s.ry);
|
|
130
|
+
const x1 = s.x + ux * scale;
|
|
131
|
+
const y1 = s.y + uy * scale;
|
|
132
|
+
const x2 = cx - ux * centerR;
|
|
133
|
+
const y2 = cy - uy * centerR;
|
|
134
|
+
const ink = EDGE_STROKE[s.style];
|
|
135
|
+
const dashAttr = ink.dash === null ? "" : ` stroke-dasharray="${ink.dash[0]} ${ink.dash[1]}"`;
|
|
136
|
+
const title = s.tie.title ?? (s.tie.quality !== null ? `${s.tie.label} \xB7 ${s.tie.quality}` : s.tie.label);
|
|
137
|
+
const body = [
|
|
138
|
+
`<line x1="${round(x1)}" y1="${round(y1)}" x2="${round(x2)}" y2="${round(y2)}" stroke="${EDGE_INK}" stroke-width="${ink.width}" stroke-opacity="${ink.opacity}"${dashAttr}/>`
|
|
139
|
+
];
|
|
140
|
+
if (s.tie.direction === "in" || s.tie.direction === "both") {
|
|
141
|
+
body.push(arrowHead(x2, y2, ux, uy, ink.opacity));
|
|
142
|
+
}
|
|
143
|
+
if (s.tie.direction === "out" || s.tie.direction === "both") {
|
|
144
|
+
body.push(arrowHead(x1, y1, -ux, -uy, ink.opacity));
|
|
145
|
+
}
|
|
146
|
+
parts.push(`<g data-edge-id="${s.tie.id}"><title>${xmlEscape(title)}</title>${body.join("")}</g>`);
|
|
147
|
+
}
|
|
148
|
+
{
|
|
149
|
+
const tspans = centerLines.map(
|
|
150
|
+
(line, i) => `<tspan x="${round(cx)}" y="${round(cy - (centerLines.length - 1) * LINE_H / 2 + i * LINE_H + fontSize * 0.32)}">${xmlEscape(line)}</tspan>`
|
|
151
|
+
).join("");
|
|
152
|
+
parts.push(
|
|
153
|
+
`<g data-individual-id="center"><title>${xmlEscape(input.centerLabel)}</title><circle cx="${round(cx)}" cy="${round(cy)}" r="${round(centerR)}" fill="transparent" stroke="${NODE_STROKE}" stroke-width="2"/><text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL}">${tspans}</text></g>`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
for (const s of sats) {
|
|
157
|
+
const tspans = s.lines.map(
|
|
158
|
+
(line, i) => `<tspan x="${round(s.x)}" y="${round(s.y - (s.lines.length - 1) * LINE_H / 2 + i * LINE_H + fontSize * 0.32)}">${xmlEscape(line)}</tspan>`
|
|
159
|
+
).join("");
|
|
160
|
+
parts.push(
|
|
161
|
+
`<g data-individual-id="e${s.tie.id}"><title>${xmlEscape(s.tie.label)}</title><ellipse cx="${round(s.x)}" cy="${round(s.y)}" rx="${round(s.rx)}" ry="${round(s.ry)}" fill="transparent" stroke="${NODE_STROKE}" stroke-width="1.5"/><text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL}">${tspans}</text></g>`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (opts.legend !== false && sats.length > 0) {
|
|
165
|
+
const entries = [];
|
|
166
|
+
if (sats.some((s) => s.style === "plain")) {
|
|
167
|
+
entries.push({
|
|
168
|
+
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W}" y2="${y}" stroke="${EDGE_INK}" stroke-width="${EDGE_STROKE.plain.width}" stroke-opacity="${EDGE_STROKE.plain.opacity}"/>`,
|
|
169
|
+
label: labels.neutralTie
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const stylesUsed = new Set(sats.map((s) => s.style));
|
|
173
|
+
for (const style of ["close", "distant", "conflict", "cutoff"]) {
|
|
174
|
+
if (!stylesUsed.has(style)) continue;
|
|
175
|
+
const ink = EDGE_STROKE[style];
|
|
176
|
+
const dashAttr = ink.dash === null ? "" : ` stroke-dasharray="${ink.dash[0]} ${ink.dash[1]}"`;
|
|
177
|
+
entries.push({
|
|
178
|
+
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W}" y2="${y}" stroke="${EDGE_INK}" stroke-width="${ink.width}" stroke-opacity="${ink.opacity}"${dashAttr}/>`,
|
|
179
|
+
label: labels.bondStyles[style]
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (sats.some((s) => s.tie.direction !== null)) {
|
|
183
|
+
entries.push({
|
|
184
|
+
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W - 8}" y2="${y}" stroke="${EDGE_INK}" stroke-width="1.5" stroke-opacity="0.75"/>` + arrowHead(x + LEGEND_SWATCH_W, y, 1, 0, 0.75),
|
|
185
|
+
label: labels.direction
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (entries.length > 0) {
|
|
189
|
+
const startY = height;
|
|
190
|
+
const rows = entries.map((entry, i) => {
|
|
191
|
+
const rowCenterY = startY + i * LEGEND_ROW_H + LEGEND_ROW_H / 2;
|
|
192
|
+
const textX = LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP;
|
|
193
|
+
return entry.swatch(LEGEND_PAD, rowCenterY) + `<text x="${textX}" y="${rowCenterY + LEGEND_FONT * 0.32}" font-family="${FONT_FAMILY}" font-size="${LEGEND_FONT}" fill="${NODE_STROKE}">${xmlEscape(entry.label)}</text>`;
|
|
194
|
+
});
|
|
195
|
+
parts.push(`<g data-compasso-legend="true">${rows.join("")}</g>`);
|
|
196
|
+
const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT)), 0);
|
|
197
|
+
width = Math.max(width, LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP + widestLabel + LEGEND_PAD);
|
|
198
|
+
height = startY + entries.length * LEGEND_ROW_H + LEGEND_PAD / 2;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const w = Math.ceil(width);
|
|
202
|
+
const h = Math.ceil(height);
|
|
203
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" role="img" aria-label="${xmlEscape(labels.ariaLabel)}">` + parts.join("") + `</svg>`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export { ECOMAP_LABELS_EN, ecomapSvg };
|
|
207
|
+
//# sourceMappingURL=chunk-L5CYESBI.js.map
|
|
208
|
+
//# sourceMappingURL=chunk-L5CYESBI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ecomap/render.ts"],"names":["w","h"],"mappings":"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","file":"chunk-L5CYESBI.js","sourcesContent":["// Radial ecomap renderer — center node + declared external ties on 1–2 concentric\n// rings, as a SELF-CONTAINED SVG string. Pure (data in, string out), deterministic\n// (same input → same SVG), no DOM, no dependencies.\n//\n// HONESTY RULE: the diagram presents ONLY what the caller declared. A tie's line style\n// is a conservative lexical hint from the author's own quality word (ambiguous/null →\n// neutral solid line, never a synthesized value); an arrowhead appears ONLY when a\n// direction was declared (null = no arrow, never a default). The verbatim text always\n// rides the element's <title>, so a style never replaces what was said.\n//\n// Ring assignment is a deterministic PRESENTATION rule — by input order (sorted by id),\n// alternating rings when the set is large — never by any reading of the tie's content.\n//\n// Every interpolated text passes xmlEscape (labels are author-controlled; the SVG may\n// be inlined via innerHTML or embedded in a PDF). All presentation attributes are\n// literal: a standalone SVG has no stylesheet. Arrowheads are explicit polygons, NOT\n// <marker> defs — marker support is unreliable in SVG-to-PDF embedders, and inline\n// polygons also avoid def-id collisions when several SVGs share one document.\n\nimport {\n EDGE_STROKE,\n FONT_FAMILY,\n clampLabel,\n estimateTextWidth,\n qualityLineStyle,\n wrapLabel,\n xmlEscape,\n type EdgeLineStyle,\n type QualityLexicon,\n} from \"../core\";\n\n// ── Input model ───────────────────────────────────────────────────────────────\n\n/** Declared flow of the tie, relative to the CENTER: \"in\" = toward the center. */\nexport type EcomapDirection = \"in\" | \"out\" | \"both\";\n\n/**\n * One declared tie between the center (person/household) and an external system\n * (work, faith, friends, healthcare…). `label` is the author's own naming, kept\n * verbatim. `quality` is the author's verbatim wording about the tie (null = not\n * declared = neutral line). `direction` follows the same doctrine: null = no arrow.\n */\nexport interface EcomapTie {\n id: number;\n label: string;\n quality: string | null;\n direction: EcomapDirection | null;\n /** Optional verbatim <title> override (defaults to \"label · quality\"). */\n title?: string;\n}\n\nexport interface EcomapInput {\n /** Center node label (e.g. the person or household the map is about). */\n centerLabel: string;\n ties: EcomapTie[];\n}\n\n// ── Display vocabulary ────────────────────────────────────────────────────────\n\nexport interface EcomapLabels {\n bondStyles: Record<Exclude<EdgeLineStyle, \"plain\">, string>;\n /** Legend label for the neutral solid line (a declared tie with no styled quality). */\n neutralTie: string;\n /** Legend label for the arrowhead (declared direction of the tie). */\n direction: string;\n ariaLabel: string;\n}\n\nexport const ECOMAP_LABELS_EN: EcomapLabels = {\n bondStyles: {\n close: \"Close\",\n distant: \"Distant\",\n conflict: \"Conflictual\",\n cutoff: \"Cut off (no contact)\",\n },\n neutralTie: \"Declared tie\",\n direction: \"Declared direction of the tie\",\n ariaLabel: \"Ecomap\",\n};\n\nexport interface EcomapSvgOptions {\n /** Display clamp per tie label (verbatim text stays in the <title>). */\n maxLabelChars?: number;\n /** Node label font size (px); default 12. */\n fontSize?: number;\n /** Set false to suppress the legend (compact preview); default true. */\n legend?: boolean;\n /** Quality-word lexicon — English default; see `compasso/locales/pt-br`. */\n qualityLexicon?: QualityLexicon;\n /** Display vocabulary — English default; see locale packs. */\n labels?: EcomapLabels;\n}\n\n// ── Geometry constants ────────────────────────────────────────────────────────\n\nconst PADDING = 32;\nconst LINE_H = 14;\nconst NODE_PAD_X = 14;\nconst NODE_PAD_Y = 9;\nconst CENTER_MIN_R = 36;\nconst RING_MIN_R = 150;\n/** Min gap between two adjacent node boxes on the same ring. */\nconst NODE_GAP = 24;\n/** Radial gap between the center circle and the inner ring's nearest box edge. */\nconst RADIAL_GAP = 40;\n/** Radial gap between the two rings when the set is large enough to split. */\nconst RING_GAP = 26;\n/** Above this many ties the layout alternates between two rings. */\nconst SINGLE_RING_MAX = 8;\n\n// Ink (zinc ramp on white — matches the genogram emitter).\nconst NODE_STROKE = \"#52525b\";\nconst LABEL_FILL = \"#3f3f46\";\nconst EDGE_INK = \"#71717a\";\n\n// Legend metrics (same vocabulary as the genogram emitter).\nconst LEGEND_ROW_H = 18;\nconst LEGEND_PAD = 16;\nconst LEGEND_SWATCH_W = 22;\nconst LEGEND_GAP = 14;\nconst LEGEND_FONT = 11;\n\nconst round = (n: number): number => Math.round(n * 100) / 100;\n\n// ── Internal shapes ───────────────────────────────────────────────────────────\n\ninterface SatNode {\n tie: EcomapTie;\n lines: string[];\n rx: number;\n ry: number;\n /** Filled once ring radii are known. */\n x: number;\n y: number;\n angle: number;\n style: EdgeLineStyle;\n}\n\n/** Wrap policy mirroring the genogram's: roughly-balanced lines, sane width. */\nfunction wrapTieLabel(displayLabel: string): string[] {\n const perLine = Math.min(26, Math.max(14, Math.ceil(displayLabel.length / 2) + 2));\n return wrapLabel(displayLabel, perLine);\n}\n\n/** Arrowhead polygon: tip at (tipX,tipY), pointing along the unit vector (ux,uy). */\nfunction arrowHead(tipX: number, tipY: number, ux: number, uy: number, opacity: number): string {\n const LEN = 9;\n const HALF_W = 4.5;\n const bx = tipX - ux * LEN;\n const by = tipY - uy * LEN;\n const px = -uy;\n const py = ux;\n const points = [\n `${round(tipX)},${round(tipY)}`,\n `${round(bx + px * HALF_W)},${round(by + py * HALF_W)}`,\n `${round(bx - px * HALF_W)},${round(by - py * HALF_W)}`,\n ].join(\" \");\n return `<polygon points=\"${points}\" fill=\"${EDGE_INK}\" fill-opacity=\"${opacity}\"/>`;\n}\n\n/**\n * Renders a declared ecomap to a self-contained SVG string. Deterministic: same data →\n * same SVG. Zero ties yield a valid center-only SVG; callers decide their own empty\n * state before calling. The root keeps numeric width/height attributes plus a matching\n * viewBox (PDF-embedder contract).\n */\nexport function ecomapSvg(input: EcomapInput, opts: EcomapSvgOptions = {}): string {\n const fontSize = opts.fontSize ?? 12;\n const labels = opts.labels ?? ECOMAP_LABELS_EN;\n\n // ── Measure every satellite box (deterministic order: by tie id). ───────────\n const sats: SatNode[] = [...input.ties]\n .sort((a, b) => a.id - b.id)\n .map((tie) => {\n const lines = wrapTieLabel(clampLabel(tie.label, opts.maxLabelChars));\n const w = Math.max(...lines.map((l) => estimateTextWidth(l, fontSize))) + NODE_PAD_X * 2;\n const h = lines.length * LINE_H + NODE_PAD_Y * 2;\n return {\n tie,\n lines,\n rx: Math.max(40, w / 2),\n ry: Math.max(20, h / 2),\n x: 0,\n y: 0,\n angle: 0,\n style: qualityLineStyle(tie.quality, opts.qualityLexicon),\n };\n });\n\n const centerLines = wrapTieLabel(input.centerLabel);\n const centerR = Math.max(\n CENTER_MIN_R,\n Math.max(...centerLines.map((l) => estimateTextWidth(l, fontSize))) / 2 + 12,\n (centerLines.length * LINE_H) / 2 + 12,\n );\n\n // ── Ring radii: wide enough that NO two node boxes can ever touch. The safe\n // center-to-center distance is the diagonal of the worst-case box pair: if the\n // euclidean distance is ≥ hypot(width-sum, height-sum), the axis-aligned boxes\n // cannot overlap in both axes at once. Overlap-proof by construction, not by luck.\n const n = sats.length;\n const maxRx = n > 0 ? Math.max(...sats.map((s) => s.rx)) : 0;\n const maxRy = n > 0 ? Math.max(...sats.map((s) => s.ry)) : 0;\n const twoRings = n > SINGLE_RING_MAX;\n const stepAngle = n > 0 ? (Math.PI * 2) / n : Math.PI;\n const safeDist = Math.hypot(2 * maxRx + NODE_GAP, 2 * maxRy + NODE_GAP);\n /** Min radius so two nodes `steps` angular steps apart on the SAME ring clear safeDist. */\n const sameRingRadius = (steps: number): number => {\n const theta = steps * stepAngle;\n if (n <= 1 || theta >= Math.PI) return 0; // ≤2 nodes on the ring: no chord constraint\n return safeDist / (2 * Math.sin(theta / 2));\n };\n const ringStep = maxRy * 2 + RING_GAP;\n let innerR = Math.max(\n RING_MIN_R,\n centerR + RADIAL_GAP + maxRy,\n sameRingRadius(twoRings ? 2 : 1),\n );\n if (twoRings) {\n // Cross-ring neighbors sit 1 step apart at radii r and r+ringStep. Their distance\n // sqrt(r² + (r+s)² − 2r(r+s)cosΔ) grows monotonically with r — widen the inner ring\n // in fixed increments until it clears safeDist (deterministic, always terminates).\n const crossDist = (r: number): number =>\n Math.sqrt(r * r + (r + ringStep) ** 2 - 2 * r * (r + ringStep) * Math.cos(stepAngle));\n while (crossDist(innerR) < safeDist) innerR += 8;\n }\n const outerR = innerR + ringStep;\n\n for (let i = 0; i < n; i++) {\n const s = sats[i]!;\n s.angle = -Math.PI / 2 + (i * Math.PI * 2) / n;\n const r = twoRings && i % 2 === 1 ? outerR : innerR;\n s.x = r * Math.cos(s.angle);\n s.y = r * Math.sin(s.angle);\n }\n\n // ── Canvas bounds (center at origin until here), then shift positive. ───────\n let minX = -centerR;\n let minY = -centerR;\n let maxX = centerR;\n let maxY = centerR;\n for (const s of sats) {\n minX = Math.min(minX, s.x - s.rx);\n minY = Math.min(minY, s.y - s.ry);\n maxX = Math.max(maxX, s.x + s.rx);\n maxY = Math.max(maxY, s.y + s.ry);\n }\n const dx = PADDING - minX;\n const dy = PADDING - minY;\n const cx = dx;\n const cy = dy;\n for (const s of sats) {\n s.x += dx;\n s.y += dy;\n }\n let width = maxX - minX + PADDING * 2;\n let height = maxY - minY + PADDING * 2;\n\n const parts: string[] = [];\n\n // ── Tie lines first (nodes sit on top), one group per declared tie. ─────────\n for (const s of sats) {\n const ux = (cx - s.x) / Math.hypot(cx - s.x, cy - s.y);\n const uy = (cy - s.y) / Math.hypot(cx - s.x, cy - s.y);\n // Node-edge endpoint: ellipse boundary along the unit vector toward the center.\n const scale = 1 / Math.hypot(ux / s.rx, uy / s.ry);\n const x1 = s.x + ux * scale;\n const y1 = s.y + uy * scale;\n // Center-edge endpoint: circle boundary along the same direction.\n const x2 = cx - ux * centerR;\n const y2 = cy - uy * centerR;\n\n const ink = EDGE_STROKE[s.style];\n const dashAttr = ink.dash === null ? \"\" : ` stroke-dasharray=\"${ink.dash[0]} ${ink.dash[1]}\"`;\n const title = s.tie.title ?? (s.tie.quality !== null ? `${s.tie.label} · ${s.tie.quality}` : s.tie.label);\n const body: string[] = [\n `<line x1=\"${round(x1)}\" y1=\"${round(y1)}\" x2=\"${round(x2)}\" y2=\"${round(y2)}\" stroke=\"${EDGE_INK}\" stroke-width=\"${ink.width}\" stroke-opacity=\"${ink.opacity}\"${dashAttr}/>`,\n ];\n // Arrowheads ONLY for a declared direction. \"in\" points at the center, \"out\" at\n // the external system, \"both\" draws both. Tips back off the boundary so the\n // triangle never pokes into the shape.\n if (s.tie.direction === \"in\" || s.tie.direction === \"both\") {\n body.push(arrowHead(x2, y2, ux, uy, ink.opacity)); // tip at the center edge\n }\n if (s.tie.direction === \"out\" || s.tie.direction === \"both\") {\n body.push(arrowHead(x1, y1, -ux, -uy, ink.opacity)); // tip at the system edge\n }\n parts.push(`<g data-edge-id=\"${s.tie.id}\"><title>${xmlEscape(title)}</title>${body.join(\"\")}</g>`);\n }\n\n // ── Center node. ─────────────────────────────────────────────────────────────\n {\n const tspans = centerLines\n .map(\n (line, i) =>\n `<tspan x=\"${round(cx)}\" y=\"${round(cy - ((centerLines.length - 1) * LINE_H) / 2 + i * LINE_H + fontSize * 0.32)}\">${xmlEscape(line)}</tspan>`,\n )\n .join(\"\");\n parts.push(\n `<g data-individual-id=\"center\"><title>${xmlEscape(input.centerLabel)}</title>` +\n `<circle cx=\"${round(cx)}\" cy=\"${round(cy)}\" r=\"${round(centerR)}\" fill=\"transparent\" stroke=\"${NODE_STROKE}\" stroke-width=\"2\"/>` +\n `<text text-anchor=\"middle\" font-family=\"${FONT_FAMILY}\" font-size=\"${fontSize}\" fill=\"${LABEL_FILL}\">${tspans}</text></g>`,\n );\n }\n\n // ── Satellite nodes. ─────────────────────────────────────────────────────────\n for (const s of sats) {\n const tspans = s.lines\n .map(\n (line, i) =>\n `<tspan x=\"${round(s.x)}\" y=\"${round(s.y - ((s.lines.length - 1) * LINE_H) / 2 + i * LINE_H + fontSize * 0.32)}\">${xmlEscape(line)}</tspan>`,\n )\n .join(\"\");\n parts.push(\n `<g data-individual-id=\"e${s.tie.id}\"><title>${xmlEscape(s.tie.label)}</title>` +\n `<ellipse cx=\"${round(s.x)}\" cy=\"${round(s.y)}\" rx=\"${round(s.rx)}\" ry=\"${round(s.ry)}\" fill=\"transparent\" stroke=\"${NODE_STROKE}\" stroke-width=\"1.5\"/>` +\n `<text text-anchor=\"middle\" font-family=\"${FONT_FAMILY}\" font-size=\"${fontSize}\" fill=\"${LABEL_FILL}\">${tspans}</text></g>`,\n );\n }\n\n // ── Minimal legend: only entries actually used. ──────────────────────────────\n if (opts.legend !== false && sats.length > 0) {\n const entries: { swatch: (x: number, y: number) => string; label: string }[] = [];\n\n if (sats.some((s) => s.style === \"plain\")) {\n entries.push({\n swatch: (x, y) =>\n `<line x1=\"${x}\" y1=\"${y}\" x2=\"${x + LEGEND_SWATCH_W}\" y2=\"${y}\" stroke=\"${EDGE_INK}\" stroke-width=\"${EDGE_STROKE.plain.width}\" stroke-opacity=\"${EDGE_STROKE.plain.opacity}\"/>`,\n label: labels.neutralTie,\n });\n }\n const stylesUsed = new Set(sats.map((s) => s.style));\n for (const style of [\"close\", \"distant\", \"conflict\", \"cutoff\"] as const) {\n if (!stylesUsed.has(style)) continue;\n const ink = EDGE_STROKE[style];\n const dashAttr = ink.dash === null ? \"\" : ` stroke-dasharray=\"${ink.dash[0]} ${ink.dash[1]}\"`;\n entries.push({\n swatch: (x, y) =>\n `<line x1=\"${x}\" y1=\"${y}\" x2=\"${x + LEGEND_SWATCH_W}\" y2=\"${y}\" stroke=\"${EDGE_INK}\" stroke-width=\"${ink.width}\" stroke-opacity=\"${ink.opacity}\"${dashAttr}/>`,\n label: labels.bondStyles[style],\n });\n }\n if (sats.some((s) => s.tie.direction !== null)) {\n entries.push({\n swatch: (x, y) =>\n `<line x1=\"${x}\" y1=\"${y}\" x2=\"${x + LEGEND_SWATCH_W - 8}\" y2=\"${y}\" stroke=\"${EDGE_INK}\" stroke-width=\"1.5\" stroke-opacity=\"0.75\"/>` +\n arrowHead(x + LEGEND_SWATCH_W, y, 1, 0, 0.75),\n label: labels.direction,\n });\n }\n\n if (entries.length > 0) {\n const startY = height;\n const rows = entries.map((entry, i) => {\n const rowCenterY = startY + i * LEGEND_ROW_H + LEGEND_ROW_H / 2;\n const textX = LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP;\n return (\n entry.swatch(LEGEND_PAD, rowCenterY) +\n `<text x=\"${textX}\" y=\"${rowCenterY + LEGEND_FONT * 0.32}\" font-family=\"${FONT_FAMILY}\" font-size=\"${LEGEND_FONT}\" fill=\"${NODE_STROKE}\">${xmlEscape(entry.label)}</text>`\n );\n });\n parts.push(`<g data-compasso-legend=\"true\">${rows.join(\"\")}</g>`);\n const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT)), 0);\n width = Math.max(width, LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP + widestLabel + LEGEND_PAD);\n height = startY + entries.length * LEGEND_ROW_H + LEGEND_PAD / 2;\n }\n }\n\n const w = Math.ceil(width);\n const h = Math.ceil(height);\n return (\n `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${w} ${h}\" width=\"${w}\" height=\"${h}\" role=\"img\" aria-label=\"${xmlEscape(labels.ariaLabel)}\">` +\n parts.join(\"\") +\n `</svg>`\n );\n}\n"]}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/xml.ts
|
|
4
|
+
function xmlEscape(text) {
|
|
5
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/core/geometry.ts
|
|
9
|
+
function pathData(points) {
|
|
10
|
+
return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/core/text.ts
|
|
14
|
+
var CHAR_W = 0.6;
|
|
15
|
+
function estimateTextWidth(text, fontPx) {
|
|
16
|
+
return text.length * fontPx * CHAR_W;
|
|
17
|
+
}
|
|
18
|
+
function wrapLabel(label, perLine) {
|
|
19
|
+
if (label.length <= perLine) return [label];
|
|
20
|
+
const cap = (s) => s.length > perLine ? s.slice(0, Math.max(1, perLine - 1)) + "\u2026" : s;
|
|
21
|
+
let line1 = "";
|
|
22
|
+
let line2 = "";
|
|
23
|
+
for (const word of label.split(/\s+/)) {
|
|
24
|
+
if (line2 === "" && (line1 === "" || (line1 + " " + word).length <= perLine)) {
|
|
25
|
+
line1 = line1 === "" ? word : `${line1} ${word}`;
|
|
26
|
+
} else {
|
|
27
|
+
line2 = line2 === "" ? word : `${line2} ${word}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return line2 === "" ? [cap(line1)] : [cap(line1), cap(line2)];
|
|
31
|
+
}
|
|
32
|
+
function clampLabel(label, maxChars) {
|
|
33
|
+
if (maxChars === void 0 || label.length <= maxChars) return label;
|
|
34
|
+
return label.slice(0, Math.max(1, maxChars - 1)) + "\u2026";
|
|
35
|
+
}
|
|
36
|
+
var FONT_FAMILY = "Helvetica, Arial, sans-serif";
|
|
37
|
+
|
|
38
|
+
// src/core/stroke.ts
|
|
39
|
+
var EDGE_STROKE = {
|
|
40
|
+
plain: { width: 1.5, dash: null, opacity: 0.6 },
|
|
41
|
+
close: { width: 3, dash: null, opacity: 0.85 },
|
|
42
|
+
distant: { width: 1.5, dash: [4, 4], opacity: 0.55 },
|
|
43
|
+
conflict: { width: 2, dash: [2, 2], opacity: 0.75 },
|
|
44
|
+
cutoff: { width: 1.5, dash: [6, 5], opacity: 0.4 }
|
|
45
|
+
};
|
|
46
|
+
var QUALITY_LEXICON_EN = {
|
|
47
|
+
buckets: [
|
|
48
|
+
{
|
|
49
|
+
style: "close",
|
|
50
|
+
needles: ["close", "warm", "support", "lov", "affection", "caring", "tight", "harmon", "healthy"]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
style: "distant",
|
|
54
|
+
needles: ["distant", "detach", "absent", "cold", "drift"]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
style: "conflict",
|
|
58
|
+
needles: ["conflict", "fight", "tens", "difficult", "hostil", "violen", "abus", "aggress", "complicat", "toxic", "argu"]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
style: "cutoff",
|
|
62
|
+
needles: ["estrang", "cut off", "cutoff", "no contact", "broken off", "sever"]
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
negations: ["not", "never", "no longer", "hardly"]
|
|
66
|
+
};
|
|
67
|
+
function normalizeText(text) {
|
|
68
|
+
return text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
71
|
+
function qualityLineStyle(quality, lexicon = QUALITY_LEXICON_EN) {
|
|
72
|
+
if (quality === null) return "plain";
|
|
73
|
+
const haystack = normalizeText(quality);
|
|
74
|
+
if (haystack.trim() === "") return "plain";
|
|
75
|
+
if (lexicon.negations.length > 0) {
|
|
76
|
+
const negation = new RegExp(`\\b(${lexicon.negations.map(escapeRegExp).join("|")})\\b`);
|
|
77
|
+
if (negation.test(haystack)) return "plain";
|
|
78
|
+
}
|
|
79
|
+
const matched = [];
|
|
80
|
+
for (const { style, needles } of lexicon.buckets) {
|
|
81
|
+
if (needles.some((n) => haystack.includes(n))) matched.push(style);
|
|
82
|
+
}
|
|
83
|
+
return matched.length === 1 ? matched[0] : "plain";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
exports.CHAR_W = CHAR_W;
|
|
87
|
+
exports.EDGE_STROKE = EDGE_STROKE;
|
|
88
|
+
exports.FONT_FAMILY = FONT_FAMILY;
|
|
89
|
+
exports.QUALITY_LEXICON_EN = QUALITY_LEXICON_EN;
|
|
90
|
+
exports.clampLabel = clampLabel;
|
|
91
|
+
exports.estimateTextWidth = estimateTextWidth;
|
|
92
|
+
exports.normalizeText = normalizeText;
|
|
93
|
+
exports.pathData = pathData;
|
|
94
|
+
exports.qualityLineStyle = qualityLineStyle;
|
|
95
|
+
exports.wrapLabel = wrapLabel;
|
|
96
|
+
exports.xmlEscape = xmlEscape;
|
|
97
|
+
//# sourceMappingURL=index.cjs.map
|
|
98
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/xml.ts","../../src/core/geometry.ts","../../src/core/text.ts","../../src/core/stroke.ts"],"names":[],"mappings":";;;AAMO,SAAS,UAAU,IAAA,EAAsB;AAC9C,EAAA,OAAO,KACJ,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA,CACrB,OAAA,CAAQ,MAAM,MAAM,CAAA,CACpB,QAAQ,IAAA,EAAM,MAAM,EACpB,OAAA,CAAQ,IAAA,EAAM,QAAQ,CAAA,CACtB,OAAA,CAAQ,MAAM,QAAQ,CAAA;AAC3B;;;ACLO,SAAS,SAAS,MAAA,EAAyB;AAChD,EAAA,OAAO,OAAO,GAAA,CAAI,CAAC,GAAG,CAAA,KAAM,CAAA,EAAG,MAAM,CAAA,GAAI,GAAA,GAAM,GAAG,CAAA,CAAA,EAAI,CAAA,CAAE,CAAC,CAAA,CAAA,EAAI,CAAA,CAAE,CAAC,CAAA,CAAE,CAAA,CAAE,KAAK,GAAG,CAAA;AAC9E;;;ACCO,IAAM,MAAA,GAAS;AAGf,SAAS,iBAAA,CAAkB,MAAc,MAAA,EAAwB;AACtE,EAAA,OAAO,IAAA,CAAK,SAAS,MAAA,GAAS,MAAA;AAChC;AASO,SAAS,SAAA,CAAU,OAAe,OAAA,EAA2B;AAClE,EAAA,IAAI,KAAA,CAAM,MAAA,IAAU,OAAA,EAAS,OAAO,CAAC,KAAK,CAAA;AAC1C,EAAA,MAAM,MAAM,CAAC,CAAA,KACX,CAAA,CAAE,MAAA,GAAS,UAAU,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,IAAA,CAAK,IAAI,CAAA,EAAG,OAAA,GAAU,CAAC,CAAC,IAAI,QAAA,GAAM,CAAA;AACpE,EAAA,IAAI,KAAA,GAAQ,EAAA;AACZ,EAAA,IAAI,KAAA,GAAQ,EAAA;AACZ,EAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,CAAM,KAAA,CAAM,KAAK,CAAA,EAAG;AACrC,IAAA,IAAI,KAAA,KAAU,OAAO,KAAA,KAAU,EAAA,IAAA,CAAO,QAAQ,GAAA,GAAM,IAAA,EAAM,UAAU,OAAA,CAAA,EAAU;AAC5E,MAAA,KAAA,GAAQ,UAAU,EAAA,GAAK,IAAA,GAAO,CAAA,EAAG,KAAK,IAAI,IAAI,CAAA,CAAA;AAAA,IAChD,CAAA,MAAO;AACL,MAAA,KAAA,GAAQ,UAAU,EAAA,GAAK,IAAA,GAAO,CAAA,EAAG,KAAK,IAAI,IAAI,CAAA,CAAA;AAAA,IAChD;AAAA,EACF;AACA,EAAA,OAAO,KAAA,KAAU,EAAA,GAAK,CAAC,GAAA,CAAI,KAAK,CAAC,CAAA,GAAI,CAAC,GAAA,CAAI,KAAK,CAAA,EAAG,GAAA,CAAI,KAAK,CAAC,CAAA;AAC9D;AAGO,SAAS,UAAA,CAAW,OAAe,QAAA,EAAsC;AAC9E,EAAA,IAAI,QAAA,KAAa,MAAA,IAAa,KAAA,CAAM,MAAA,IAAU,UAAU,OAAO,KAAA;AAC/D,EAAA,OAAO,KAAA,CAAM,MAAM,CAAA,EAAG,IAAA,CAAK,IAAI,CAAA,EAAG,QAAA,GAAW,CAAC,CAAC,CAAA,GAAI,QAAA;AACrD;AAGO,IAAM,WAAA,GAAc;;;ACjCpB,IAAM,WAAA,GAAiD;AAAA,EAC5D,OAAO,EAAE,KAAA,EAAO,KAAK,IAAA,EAAM,IAAA,EAAM,SAAS,GAAA,EAAI;AAAA,EAC9C,OAAO,EAAE,KAAA,EAAO,GAAG,IAAA,EAAM,IAAA,EAAM,SAAS,IAAA,EAAK;AAAA,EAC7C,OAAA,EAAS,EAAE,KAAA,EAAO,GAAA,EAAK,IAAA,EAAM,CAAC,CAAA,EAAG,CAAC,CAAA,EAAG,OAAA,EAAS,IAAA,EAAK;AAAA,EACnD,QAAA,EAAU,EAAE,KAAA,EAAO,CAAA,EAAG,IAAA,EAAM,CAAC,CAAA,EAAG,CAAC,CAAA,EAAG,OAAA,EAAS,IAAA,EAAK;AAAA,EAClD,MAAA,EAAQ,EAAE,KAAA,EAAO,GAAA,EAAK,IAAA,EAAM,CAAC,CAAA,EAAG,CAAC,CAAA,EAAG,OAAA,EAAS,GAAA;AAC/C;AAiBO,IAAM,kBAAA,GAAqC;AAAA,EAChD,OAAA,EAAS;AAAA,IACP;AAAA,MACE,KAAA,EAAO,OAAA;AAAA,MACP,OAAA,EAAS,CAAC,OAAA,EAAS,MAAA,EAAQ,SAAA,EAAW,OAAO,WAAA,EAAa,QAAA,EAAU,OAAA,EAAS,QAAA,EAAU,SAAS;AAAA,KAClG;AAAA,IACA;AAAA,MACE,KAAA,EAAO,SAAA;AAAA,MACP,SAAS,CAAC,SAAA,EAAW,QAAA,EAAU,QAAA,EAAU,QAAQ,OAAO;AAAA,KAC1D;AAAA,IACA;AAAA,MACE,KAAA,EAAO,UAAA;AAAA,MACP,OAAA,EAAS,CAAC,UAAA,EAAY,OAAA,EAAS,MAAA,EAAQ,WAAA,EAAa,QAAA,EAAU,QAAA,EAAU,MAAA,EAAQ,SAAA,EAAW,WAAA,EAAa,OAAA,EAAS,MAAM;AAAA,KACzH;AAAA,IACA;AAAA,MACE,KAAA,EAAO,QAAA;AAAA,MACP,SAAS,CAAC,SAAA,EAAW,WAAW,QAAA,EAAU,YAAA,EAAc,cAAc,OAAO;AAAA;AAC/E,GACF;AAAA,EACA,SAAA,EAAW,CAAC,KAAA,EAAO,OAAA,EAAS,aAAa,QAAQ;AACnD;AAGO,SAAS,cAAc,IAAA,EAAsB;AAClD,EAAA,OAAO,IAAA,CAAK,UAAU,KAAK,CAAA,CAAE,QAAQ,QAAA,EAAU,EAAE,EAAE,WAAA,EAAY;AACjE;AAEA,IAAM,eAAe,CAAC,CAAA,KAAsB,CAAA,CAAE,OAAA,CAAQ,uBAAuB,MAAM,CAAA;AAQ5E,SAAS,gBAAA,CACd,OAAA,EACA,OAAA,GAA0B,kBAAA,EACX;AACf,EAAA,IAAI,OAAA,KAAY,MAAM,OAAO,OAAA;AAC7B,EAAA,MAAM,QAAA,GAAW,cAAc,OAAO,CAAA;AACtC,EAAA,IAAI,QAAA,CAAS,IAAA,EAAK,KAAM,EAAA,EAAI,OAAO,OAAA;AAEnC,EAAA,IAAI,OAAA,CAAQ,SAAA,CAAU,MAAA,GAAS,CAAA,EAAG;AAChC,IAAA,MAAM,QAAA,GAAW,IAAI,MAAA,CAAO,CAAA,IAAA,EAAO,OAAA,CAAQ,SAAA,CAAU,GAAA,CAAI,YAAY,CAAA,CAAE,IAAA,CAAK,GAAG,CAAC,CAAA,IAAA,CAAM,CAAA;AACtF,IAAA,IAAI,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,EAAG,OAAO,OAAA;AAAA,EACtC;AAEA,EAAA,MAAM,UAA2B,EAAC;AAClC,EAAA,KAAA,MAAW,EAAE,KAAA,EAAO,OAAA,EAAQ,IAAK,QAAQ,OAAA,EAAS;AAChD,IAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAA,KAAM,QAAA,CAAS,QAAA,CAAS,CAAC,CAAC,CAAA,EAAG,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAA;AAAA,EACnE;AACA,EAAA,OAAO,OAAA,CAAQ,MAAA,KAAW,CAAA,GAAI,OAAA,CAAQ,CAAC,CAAA,GAAK,OAAA;AAC9C","file":"index.cjs","sourcesContent":["// XML/SVG escaping — every interpolated text in an emitted SVG MUST pass through\n// this. Diagram labels are typically user/author-controlled, and the SVG string may\n// be injected via innerHTML in a browser or embedded as vectors in a PDF — an\n// unescaped label is an XSS / `</svg>`-breakout vector on every surface at once.\n\n/** Escapes a string for use in SVG/XML text content AND attribute values. */\nexport function xmlEscape(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n","// Shared geometry primitives.\n\nexport interface Point {\n x: number;\n y: number;\n}\n\n/** \"M x y L x y …\" path data from a polyline. */\nexport function pathData(points: Point[]): string {\n return points.map((p, i) => `${i === 0 ? \"M\" : \"L\"} ${p.x} ${p.y}`).join(\" \");\n}\n","// Pure, deterministic text metrics — no DOM, no canvas, no font files. Every layout\n// in this library reserves space from these estimates and every emitter draws with\n// the same constants, so what is proven collision-free is what is drawn.\n\n/**\n * Conservative per-character advance, as a fraction of the font size. The pure module\n * can't measure real glyphs (no DOM/canvas), so it estimates width = chars * font *\n * CHAR_W. It is deliberately a touch WIDER than Helvetica's ~0.5 average advance: a\n * layout reserves slightly more room than the text needs, so the real render always\n * fits inside its reserved box.\n */\nexport const CHAR_W = 0.6;\n\n/** Pure, deterministic width estimate for a single line of text at `fontPx`. */\nexport function estimateTextWidth(text: string, fontPx: number): number {\n return text.length * fontPx * CHAR_W;\n}\n\n/**\n * Wraps a label onto up to TWO lines so long labels don't sprawl. Greedy word fill;\n * each line capped at `perLine` chars (…-truncated only if a single word overflows).\n * Pure + shared so every renderer wraps identically. The full text is always kept\n * elsewhere (the node's `label`, an SVG <title>, or a side list), so nothing is\n * silently lost.\n */\nexport function wrapLabel(label: string, perLine: number): string[] {\n if (label.length <= perLine) return [label];\n const cap = (s: string): string =>\n s.length > perLine ? s.slice(0, Math.max(1, perLine - 1)) + \"…\" : s;\n let line1 = \"\";\n let line2 = \"\";\n for (const word of label.split(/\\s+/)) {\n if (line2 === \"\" && (line1 === \"\" || (line1 + \" \" + word).length <= perLine)) {\n line1 = line1 === \"\" ? word : `${line1} ${word}`;\n } else {\n line2 = line2 === \"\" ? word : `${line2} ${word}`;\n }\n }\n return line2 === \"\" ? [cap(line1)] : [cap(line1), cap(line2)];\n}\n\n/** Caps a verbatim label for a COMPACT render (preview); full text kept by the caller. */\nexport function clampLabel(label: string, maxChars: number | undefined): string {\n if (maxChars === undefined || label.length <= maxChars) return label;\n return label.slice(0, Math.max(1, maxChars - 1)) + \"…\";\n}\n\n/** Font stack shared by every emitter; PDF embedders typically map it onto Helvetica. */\nexport const FONT_FAMILY = \"Helvetica, Arial, sans-serif\";\n","// Edge line styles — a neutral, presentation-only style for a relationship line.\n// NOT a judgment: a deterministic, lexical hint derived from the author's own quality\n// word. The literal word always rides the element's <title>, so a style never replaces\n// what was said. Unknown/ambiguous quality → \"plain\".\n\nexport type EdgeLineStyle = \"plain\" | \"close\" | \"distant\" | \"conflict\" | \"cutoff\";\n\n/** Stroke attributes per style — shared by every renderer so SVG and PDF match. */\nexport interface EdgeStroke {\n width: number;\n /** SVG dash array / PDF dash pattern as [dash, gap]; null = solid. */\n dash: [number, number] | null;\n opacity: number;\n}\n\nexport const EDGE_STROKE: Record<EdgeLineStyle, EdgeStroke> = {\n plain: { width: 1.5, dash: null, opacity: 0.6 },\n close: { width: 3, dash: null, opacity: 0.85 },\n distant: { width: 1.5, dash: [4, 4], opacity: 0.55 },\n conflict: { width: 2, dash: [2, 2], opacity: 0.75 },\n cutoff: { width: 1.5, dash: [6, 5], opacity: 0.4 },\n};\n\n/**\n * A pluggable lexicon mapping free-text quality words onto line styles. Needles are\n * diacritic-free, lowercase substrings of the author's own words; `negations` are\n * whole words that flip the meaning of the very word a needle would match (\"not\n * close\"), so their presence makes the matcher abstain to \"plain\" rather than risk\n * an inverted visual. Locale packs (e.g. `compasso/locales/pt-br`) ship alternates.\n */\nexport interface QualityLexicon {\n buckets: readonly { style: Exclude<EdgeLineStyle, \"plain\">; needles: readonly string[] }[];\n negations: readonly string[];\n}\n\n// English default. Conservative stems: each needle is a substring match on the\n// normalized text, and the SINGLE-BUCKET rule below keeps competing signals honest.\n// \"no\" alone is deliberately NOT a negation: \"no contact\" is a genuine cutoff signal.\nexport const QUALITY_LEXICON_EN: QualityLexicon = {\n buckets: [\n {\n style: \"close\",\n needles: [\"close\", \"warm\", \"support\", \"lov\", \"affection\", \"caring\", \"tight\", \"harmon\", \"healthy\"],\n },\n {\n style: \"distant\",\n needles: [\"distant\", \"detach\", \"absent\", \"cold\", \"drift\"],\n },\n {\n style: \"conflict\",\n needles: [\"conflict\", \"fight\", \"tens\", \"difficult\", \"hostil\", \"violen\", \"abus\", \"aggress\", \"complicat\", \"toxic\", \"argu\"],\n },\n {\n style: \"cutoff\",\n needles: [\"estrang\", \"cut off\", \"cutoff\", \"no contact\", \"broken off\", \"sever\"],\n },\n ],\n negations: [\"not\", \"never\", \"no longer\", \"hardly\"],\n};\n\n/** Lowercase + strip diacritics so matching ignores accents. */\nexport function normalizeText(text: string): string {\n return text.normalize(\"NFD\").replace(/[̀-ͯ]/g, \"\").toLowerCase();\n}\n\nconst escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n/**\n * Maps a free-text relationship quality to a neutral line style. Deterministic and\n * conservative: returns a specific style ONLY when exactly one lexical bucket matches;\n * null/empty/ambiguous/negated/unknown all fall back to \"plain\". The caller still\n * keeps the verbatim quality word, so nothing the author said is ever lost.\n */\nexport function qualityLineStyle(\n quality: string | null,\n lexicon: QualityLexicon = QUALITY_LEXICON_EN,\n): EdgeLineStyle {\n if (quality === null) return \"plain\";\n const haystack = normalizeText(quality);\n if (haystack.trim() === \"\") return \"plain\";\n\n if (lexicon.negations.length > 0) {\n const negation = new RegExp(`\\\\b(${lexicon.negations.map(escapeRegExp).join(\"|\")})\\\\b`);\n if (negation.test(haystack)) return \"plain\";\n }\n\n const matched: EdgeLineStyle[] = [];\n for (const { style, needles } of lexicon.buckets) {\n if (needles.some((n) => haystack.includes(n))) matched.push(style);\n }\n return matched.length === 1 ? matched[0]! : \"plain\";\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export { E as EDGE_STROKE, a as EdgeLineStyle, b as EdgeStroke, Q as QUALITY_LEXICON_EN, c as QualityLexicon, n as normalizeText, q as qualityLineStyle } from '../stroke-MQ427drt.cjs';
|
|
2
|
+
|
|
3
|
+
/** Escapes a string for use in SVG/XML text content AND attribute values. */
|
|
4
|
+
declare function xmlEscape(text: string): string;
|
|
5
|
+
|
|
6
|
+
interface Point {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
}
|
|
10
|
+
/** "M x y L x y …" path data from a polyline. */
|
|
11
|
+
declare function pathData(points: Point[]): string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Conservative per-character advance, as a fraction of the font size. The pure module
|
|
15
|
+
* can't measure real glyphs (no DOM/canvas), so it estimates width = chars * font *
|
|
16
|
+
* CHAR_W. It is deliberately a touch WIDER than Helvetica's ~0.5 average advance: a
|
|
17
|
+
* layout reserves slightly more room than the text needs, so the real render always
|
|
18
|
+
* fits inside its reserved box.
|
|
19
|
+
*/
|
|
20
|
+
declare const CHAR_W = 0.6;
|
|
21
|
+
/** Pure, deterministic width estimate for a single line of text at `fontPx`. */
|
|
22
|
+
declare function estimateTextWidth(text: string, fontPx: number): number;
|
|
23
|
+
/**
|
|
24
|
+
* Wraps a label onto up to TWO lines so long labels don't sprawl. Greedy word fill;
|
|
25
|
+
* each line capped at `perLine` chars (…-truncated only if a single word overflows).
|
|
26
|
+
* Pure + shared so every renderer wraps identically. The full text is always kept
|
|
27
|
+
* elsewhere (the node's `label`, an SVG <title>, or a side list), so nothing is
|
|
28
|
+
* silently lost.
|
|
29
|
+
*/
|
|
30
|
+
declare function wrapLabel(label: string, perLine: number): string[];
|
|
31
|
+
/** Caps a verbatim label for a COMPACT render (preview); full text kept by the caller. */
|
|
32
|
+
declare function clampLabel(label: string, maxChars: number | undefined): string;
|
|
33
|
+
/** Font stack shared by every emitter; PDF embedders typically map it onto Helvetica. */
|
|
34
|
+
declare const FONT_FAMILY = "Helvetica, Arial, sans-serif";
|
|
35
|
+
|
|
36
|
+
export { CHAR_W, FONT_FAMILY, type Point, clampLabel, estimateTextWidth, pathData, wrapLabel, xmlEscape };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export { E as EDGE_STROKE, a as EdgeLineStyle, b as EdgeStroke, Q as QUALITY_LEXICON_EN, c as QualityLexicon, n as normalizeText, q as qualityLineStyle } from '../stroke-MQ427drt.js';
|
|
2
|
+
|
|
3
|
+
/** Escapes a string for use in SVG/XML text content AND attribute values. */
|
|
4
|
+
declare function xmlEscape(text: string): string;
|
|
5
|
+
|
|
6
|
+
interface Point {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
}
|
|
10
|
+
/** "M x y L x y …" path data from a polyline. */
|
|
11
|
+
declare function pathData(points: Point[]): string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Conservative per-character advance, as a fraction of the font size. The pure module
|
|
15
|
+
* can't measure real glyphs (no DOM/canvas), so it estimates width = chars * font *
|
|
16
|
+
* CHAR_W. It is deliberately a touch WIDER than Helvetica's ~0.5 average advance: a
|
|
17
|
+
* layout reserves slightly more room than the text needs, so the real render always
|
|
18
|
+
* fits inside its reserved box.
|
|
19
|
+
*/
|
|
20
|
+
declare const CHAR_W = 0.6;
|
|
21
|
+
/** Pure, deterministic width estimate for a single line of text at `fontPx`. */
|
|
22
|
+
declare function estimateTextWidth(text: string, fontPx: number): number;
|
|
23
|
+
/**
|
|
24
|
+
* Wraps a label onto up to TWO lines so long labels don't sprawl. Greedy word fill;
|
|
25
|
+
* each line capped at `perLine` chars (…-truncated only if a single word overflows).
|
|
26
|
+
* Pure + shared so every renderer wraps identically. The full text is always kept
|
|
27
|
+
* elsewhere (the node's `label`, an SVG <title>, or a side list), so nothing is
|
|
28
|
+
* silently lost.
|
|
29
|
+
*/
|
|
30
|
+
declare function wrapLabel(label: string, perLine: number): string[];
|
|
31
|
+
/** Caps a verbatim label for a COMPACT render (preview); full text kept by the caller. */
|
|
32
|
+
declare function clampLabel(label: string, maxChars: number | undefined): string;
|
|
33
|
+
/** Font stack shared by every emitter; PDF embedders typically map it onto Helvetica. */
|
|
34
|
+
declare const FONT_FAMILY = "Helvetica, Arial, sans-serif";
|
|
35
|
+
|
|
36
|
+
export { CHAR_W, FONT_FAMILY, type Point, clampLabel, estimateTextWidth, pathData, wrapLabel, xmlEscape };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|