@stacksjs/defaults 0.74.40 → 0.74.42
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/ai/skills/stacks-cms/SKILL.md +4 -4
- package/ai/skills/stacks-composables/SKILL.md +1 -1
- package/ai/skills/stacks-config/SKILL.md +1 -1
- package/ai/skills/stacks-dashboard/SKILL.md +1 -1
- package/ai/skills/stacks-technical-diagrams/LICENSE +1 -1
- package/ai/skills/stacks-technical-diagrams/SKILL.md +257 -229
- package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +69 -0
- package/ai/skills/stacks-technical-diagrams/assets/JetBrainsMono-OFL.txt +93 -0
- package/ai/skills/stacks-technical-diagrams/assets/template.html +14588 -929
- package/ai/skills/stacks-technical-diagrams/bin/diagrams +10 -0
- package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +86 -0
- package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +654 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +1898 -89
- package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +829 -0
- package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +31 -0
- package/ai/skills/stacks-technical-diagrams/brand-marks/catalog.json +131 -0
- package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +1221 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +18 -22
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +58 -52
- package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +61 -0
- package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +47 -0
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +30 -23
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +31 -0
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +31 -0
- package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +49 -0
- package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +57 -0
- package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +64 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +22 -16
- package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +71 -0
- package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +62 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +16 -11
- package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +279 -0
- package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +391 -0
- package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +243 -0
- package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +65 -0
- package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +120 -0
- package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +45 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +780 -73
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +25 -3
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +240 -52
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +31 -7
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +227 -50
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +36 -6
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +270 -63
- package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +563 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/bun-runtime.mjs +20 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +186 -10
- package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +26 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +127 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/engineering-profiles.mjs +157 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-brand-marks.mjs +2003 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +3 -3
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +1195 -2
- package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +595 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +217 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +340 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +238 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +58 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +49 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +163 -19
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +51 -5
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -17
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +24 -470
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +4400 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +154 -11
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +61 -4
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +72 -0
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +40 -18
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +43 -18
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +41 -4
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +97 -1
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +551 -12
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +3 -4
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/CookieConsent.stx +146 -0
- package/resources/components/CookieConsent.test.ts +79 -0
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { collectAmbiguousCorridors, collectBorderRuns, collectLabelRouteClearance, collectRouteRhythmIssues, routeBudgetMetrics } from '../renderers/shared/geometry.mjs';
|
|
6
|
+
import {
|
|
7
|
+
DESKTOP_READABILITY_VIEWPORT,
|
|
8
|
+
DESKTOP_READER_DIAGRAM_WIDTH,
|
|
9
|
+
MIN_PROJECTED_NODE_TEXT_PX,
|
|
10
|
+
projectedNodeTextPx,
|
|
11
|
+
} from '../renderers/shared/desktop-readability.mjs';
|
|
5
12
|
|
|
6
13
|
const input = process.argv[2];
|
|
7
14
|
|
|
8
15
|
if (!input || input === '-h' || input === '--help') {
|
|
9
|
-
console.error('Usage:
|
|
16
|
+
console.error('Usage: node scripts/check-render-output.mjs <diagram.html>');
|
|
10
17
|
process.exit(input ? 0 : 2);
|
|
11
18
|
}
|
|
12
19
|
|
|
@@ -24,6 +31,33 @@ try {
|
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
const checks = [];
|
|
34
|
+
let composition = {
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
profile: 'standard',
|
|
37
|
+
status: 'pass',
|
|
38
|
+
summary: { errors: 0, warnings: 0 },
|
|
39
|
+
metrics: {
|
|
40
|
+
properCrossings: 0,
|
|
41
|
+
ambiguousCorridors: 0,
|
|
42
|
+
containerBorderRuns: 0,
|
|
43
|
+
labelRouteClearanceIssues: 0,
|
|
44
|
+
minLabelRouteClearance: null,
|
|
45
|
+
maxBends: 0,
|
|
46
|
+
routesOverSuggestedBends: 0,
|
|
47
|
+
maxStretch: null,
|
|
48
|
+
routesOverSuggestedStretch: 0,
|
|
49
|
+
minSegmentPx: null,
|
|
50
|
+
minInteriorSegmentPx: null,
|
|
51
|
+
shortSegmentCount: 0,
|
|
52
|
+
shortEndpointSegmentCount: 0,
|
|
53
|
+
shortInteriorSegmentCount: 0,
|
|
54
|
+
microSegmentCount: 0,
|
|
55
|
+
desktopReadabilityIssues: 0,
|
|
56
|
+
minProjectedNodeTextPx: null,
|
|
57
|
+
},
|
|
58
|
+
suggestedLimits: { bendsPerRelationship: 2, stretch: 1.35, segmentPx: 16, microSegmentPx: 8 },
|
|
59
|
+
issues: [],
|
|
60
|
+
};
|
|
27
61
|
|
|
28
62
|
function addCheck(name, ok, details = []) {
|
|
29
63
|
checks.push({ name, ok, details });
|
|
@@ -34,15 +68,193 @@ addCheck('single_svg', svgMatches.length === 1, [`found ${svgMatches.length} <sv
|
|
|
34
68
|
|
|
35
69
|
if (svgMatches.length === 1) {
|
|
36
70
|
const svg = svgMatches[0][0];
|
|
71
|
+
const svgRoot = svg.match(/<svg\b[^>]*>/i)?.[0] || '';
|
|
72
|
+
const svgAttrs = parseAttrs(svgRoot);
|
|
73
|
+
const qualityProfile = svgAttrs['data-quality-profile'] || 'standard';
|
|
74
|
+
const qualityGatesEnforced = svgAttrs['data-quality-gates'] !== 'advisory';
|
|
37
75
|
addCheck('finite_svg', !/\b(?:NaN|undefined|Infinity|-Infinity)\b/.test(svg));
|
|
38
76
|
const legendStart = svg.indexOf('<!-- Legend -->');
|
|
39
77
|
const beforeLegend = legendStart >= 0 ? svg.slice(0, legendStart) : svg;
|
|
78
|
+
const desktopReadabilityIssue = collectDesktopReadability(svgAttrs, beforeLegend);
|
|
40
79
|
const arrows = collectArrows(beforeLegend);
|
|
41
|
-
const diagonal = arrows.
|
|
80
|
+
const diagonal = arrows.flatMap((arrow) => diagonalStraightSegments(arrow).map((segment) => ({ arrow, ...segment })));
|
|
42
81
|
addCheck(
|
|
43
82
|
'orthogonal_arrows',
|
|
44
83
|
diagonal.length === 0,
|
|
45
|
-
diagonal.map((arrow) => `${arrow.kind} ${arrow.index}: ${arrow.raw}`),
|
|
84
|
+
diagonal.map(({ arrow, segmentIndex }) => `${arrow.kind} ${arrow.index} segment ${segmentIndex + 1}: ${arrow.raw}`),
|
|
85
|
+
);
|
|
86
|
+
const relationshipCrossings = collectRelationshipCrossings(arrows);
|
|
87
|
+
const compositionFrames = collectCompositionFrames(beforeLegend);
|
|
88
|
+
const containerBorderRuns = collectBorderRuns({
|
|
89
|
+
routedRelations: arrows
|
|
90
|
+
.filter((arrow) => arrow.from && arrow.to && arrow.borderSegments.length)
|
|
91
|
+
.map((arrow) => ({
|
|
92
|
+
relation: arrow,
|
|
93
|
+
relationIndex: arrow.index,
|
|
94
|
+
segments: arrow.borderSegments,
|
|
95
|
+
})),
|
|
96
|
+
frames: compositionFrames,
|
|
97
|
+
});
|
|
98
|
+
const routedRelationships = arrows
|
|
99
|
+
.filter((arrow) => arrow.from && arrow.to && arrow.routePoints.length)
|
|
100
|
+
.map((arrow) => ({ relation: arrow, relationIndex: arrow.index, points: arrow.routePoints }));
|
|
101
|
+
const routeMetrics = routeBudgetMetrics({ routedRelations: routedRelationships });
|
|
102
|
+
const routeRhythmIssues = collectRouteRhythmIssues({ routedRelations: routedRelationships });
|
|
103
|
+
const ambiguousCorridors = collectAmbiguousCorridors({ routedRelations: routedRelationships });
|
|
104
|
+
const relationshipLabels = collectRelationshipLabelMasks(beforeLegend, arrows);
|
|
105
|
+
const labelClearanceThreshold = qualityProfile === 'showcase' ? 4 : 2;
|
|
106
|
+
const labelRouteMeasurements = collectLabelRouteClearance({
|
|
107
|
+
labels: relationshipLabels,
|
|
108
|
+
routedRelations: arrows.map((arrow) => ({ relation: arrow, relationIndex: arrow.index, points: arrow.routePoints })),
|
|
109
|
+
threshold: Number.MAX_VALUE,
|
|
110
|
+
});
|
|
111
|
+
const labelRouteClearance = collectLabelRouteClearance({
|
|
112
|
+
labels: relationshipLabels,
|
|
113
|
+
routedRelations: arrows.map((arrow) => ({ relation: arrow, relationIndex: arrow.index, points: arrow.routePoints })),
|
|
114
|
+
threshold: labelClearanceThreshold,
|
|
115
|
+
});
|
|
116
|
+
const crossingIsError = qualityProfile === 'showcase';
|
|
117
|
+
const corridorIsError = qualityProfile === 'showcase';
|
|
118
|
+
const rhythmIsError = qualityProfile === 'showcase';
|
|
119
|
+
const labelClearanceIsError = qualityProfile === 'showcase';
|
|
120
|
+
const desktopReadabilityIsError = qualityProfile === 'showcase';
|
|
121
|
+
const compositionErrors = (qualityGatesEnforced ? containerBorderRuns.length : 0)
|
|
122
|
+
+ (crossingIsError ? relationshipCrossings.length : 0)
|
|
123
|
+
+ (corridorIsError ? ambiguousCorridors.length : 0)
|
|
124
|
+
+ (labelClearanceIsError ? labelRouteClearance.length : 0)
|
|
125
|
+
+ (rhythmIsError ? routeRhythmIssues.length : 0)
|
|
126
|
+
+ (desktopReadabilityIsError && desktopReadabilityIssue ? 1 : 0);
|
|
127
|
+
const compositionWarnings = (qualityGatesEnforced ? 0 : containerBorderRuns.length)
|
|
128
|
+
+ (crossingIsError ? 0 : relationshipCrossings.length)
|
|
129
|
+
+ (corridorIsError ? 0 : ambiguousCorridors.length)
|
|
130
|
+
+ (labelClearanceIsError ? 0 : labelRouteClearance.length)
|
|
131
|
+
+ (rhythmIsError ? 0 : routeRhythmIssues.length)
|
|
132
|
+
+ (desktopReadabilityIsError || !desktopReadabilityIssue ? 0 : 1);
|
|
133
|
+
composition = {
|
|
134
|
+
schemaVersion: 1,
|
|
135
|
+
profile: qualityProfile,
|
|
136
|
+
status: compositionErrors ? 'fail' : 'pass',
|
|
137
|
+
summary: {
|
|
138
|
+
errors: compositionErrors,
|
|
139
|
+
warnings: compositionWarnings,
|
|
140
|
+
},
|
|
141
|
+
metrics: {
|
|
142
|
+
properCrossings: relationshipCrossings.length,
|
|
143
|
+
ambiguousCorridors: ambiguousCorridors.length,
|
|
144
|
+
containerBorderRuns: containerBorderRuns.length,
|
|
145
|
+
labelRouteClearanceIssues: labelRouteClearance.length,
|
|
146
|
+
minLabelRouteClearance: labelRouteMeasurements.length
|
|
147
|
+
? Math.round(Math.min(...labelRouteMeasurements.map((hit) => hit.clearance)) * 10) / 10
|
|
148
|
+
: null,
|
|
149
|
+
desktopReadabilityIssues: desktopReadabilityIssue ? 1 : 0,
|
|
150
|
+
minProjectedNodeTextPx: desktopReadabilityIssue?.projectedFontPx ?? null,
|
|
151
|
+
...roundedRouteMetrics(routeMetrics),
|
|
152
|
+
},
|
|
153
|
+
suggestedLimits: { bendsPerRelationship: 2, stretch: 1.35, segmentPx: 16, microSegmentPx: 8 },
|
|
154
|
+
issues: [
|
|
155
|
+
...containerBorderRuns.map((hit) => ({
|
|
156
|
+
severity: qualityGatesEnforced ? 'error' : 'warning',
|
|
157
|
+
code: 'composition/container-border-run',
|
|
158
|
+
relationship: relationshipRecord(hit.relation),
|
|
159
|
+
frame: frameRecord(hit.frame),
|
|
160
|
+
side: hit.side,
|
|
161
|
+
segmentIndex: hit.segmentIndex,
|
|
162
|
+
overlapLength: Math.round(hit.overlapLength * 10) / 10,
|
|
163
|
+
from: hit.overlapStart.map((value) => Math.round(value * 10) / 10),
|
|
164
|
+
to: hit.overlapEnd.map((value) => Math.round(value * 10) / 10),
|
|
165
|
+
})),
|
|
166
|
+
...labelRouteClearance.map((hit) => ({
|
|
167
|
+
severity: labelClearanceIsError ? 'error' : 'warning',
|
|
168
|
+
code: 'composition/label-route-clearance',
|
|
169
|
+
label: hit.label?.label || hit.labelRelation?.label || '',
|
|
170
|
+
labelRelationship: relationshipRecord(hit.labelRelation),
|
|
171
|
+
otherRelationship: relationshipRecord(hit.otherRelation),
|
|
172
|
+
segmentIndex: hit.segmentIndex,
|
|
173
|
+
labelRect: roundedRect(hit.rect),
|
|
174
|
+
clearance: Math.round(hit.clearance * 10) / 10,
|
|
175
|
+
intersectionLength: Math.round((hit.intersectionLength || 0) * 10) / 10,
|
|
176
|
+
threshold: hit.threshold,
|
|
177
|
+
from: hit.start.map((value) => Math.round(value * 10) / 10),
|
|
178
|
+
to: hit.end.map((value) => Math.round(value * 10) / 10),
|
|
179
|
+
})),
|
|
180
|
+
...relationshipCrossings.map((hit) => ({
|
|
181
|
+
severity: crossingIsError ? 'error' : 'warning',
|
|
182
|
+
code: 'composition/proper-crossing',
|
|
183
|
+
relationship: relationshipRecord(hit.left),
|
|
184
|
+
otherRelationship: relationshipRecord(hit.right),
|
|
185
|
+
point: hit.point.map((value) => Math.round(value * 10) / 10),
|
|
186
|
+
})),
|
|
187
|
+
...ambiguousCorridors.map((hit) => ({
|
|
188
|
+
severity: corridorIsError ? 'error' : 'warning',
|
|
189
|
+
code: 'composition/ambiguous-corridor',
|
|
190
|
+
relationship: relationshipRecord(hit.left.relation),
|
|
191
|
+
otherRelationship: relationshipRecord(hit.right.relation),
|
|
192
|
+
segmentIndex: hit.leftSegment,
|
|
193
|
+
otherSegmentIndex: hit.rightSegment,
|
|
194
|
+
overlapLength: Math.round(hit.overlapLength * 10) / 10,
|
|
195
|
+
from: hit.overlapStart.map((value) => Math.round(value * 10) / 10),
|
|
196
|
+
to: hit.overlapEnd.map((value) => Math.round(value * 10) / 10),
|
|
197
|
+
})),
|
|
198
|
+
...routeRhythmIssues.map((hit) => ({
|
|
199
|
+
severity: rhythmIsError ? 'error' : 'warning',
|
|
200
|
+
code: hit.code,
|
|
201
|
+
relationship: relationshipRecord(hit.relation),
|
|
202
|
+
segmentIndex: hit.segmentIndex,
|
|
203
|
+
position: hit.position,
|
|
204
|
+
length: Math.round(hit.length * 10) / 10,
|
|
205
|
+
from: hit.start.map((value) => Math.round(value * 10) / 10),
|
|
206
|
+
to: hit.end.map((value) => Math.round(value * 10) / 10),
|
|
207
|
+
})),
|
|
208
|
+
...(desktopReadabilityIssue ? [{
|
|
209
|
+
severity: desktopReadabilityIsError ? 'error' : 'warning',
|
|
210
|
+
code: 'composition/desktop-readability',
|
|
211
|
+
viewportWidth: DESKTOP_READABILITY_VIEWPORT.width,
|
|
212
|
+
viewportHeight: DESKTOP_READABILITY_VIEWPORT.height,
|
|
213
|
+
availableDiagramWidth: DESKTOP_READER_DIAGRAM_WIDTH,
|
|
214
|
+
viewBoxWidth: desktopReadabilityIssue.viewBoxWidth,
|
|
215
|
+
scale: desktopReadabilityIssue.scale,
|
|
216
|
+
text: desktopReadabilityIssue.text,
|
|
217
|
+
detail: desktopReadabilityIssue.detail,
|
|
218
|
+
sourceFontPx: desktopReadabilityIssue.sourceFontPx,
|
|
219
|
+
projectedFontPx: desktopReadabilityIssue.projectedFontPx,
|
|
220
|
+
minimumProjectedFontPx: MIN_PROJECTED_NODE_TEXT_PX,
|
|
221
|
+
}] : []),
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
addCheck(
|
|
225
|
+
'label_route_clearance',
|
|
226
|
+
!labelClearanceIsError || labelRouteClearance.length === 0,
|
|
227
|
+
labelRouteClearance.map((hit) => (
|
|
228
|
+
`[composition/label-route-clearance] ${qualityProfile} label "${hit.label?.label || hit.labelRelation?.label || ''}" on ${relationshipName(hit.labelRelation)} is ${Math.round(hit.clearance * 10) / 10}px from ${relationshipName(hit.otherRelation)} segment ${hit.segmentIndex} [${formatPoint(hit.start)}] -> [${formatPoint(hit.end)}]${hit.intersectionLength > 0 ? ` with ${Math.round(hit.intersectionLength * 10) / 10}px hidden by the mask` : ''} (minimum ${hit.threshold}px) - use renderer-supported label controls (message y for sequence; otherwise labelAt, labelDx, labelDy, or labelSegment), or adjust the other relationship route/via/channel.`
|
|
229
|
+
)),
|
|
230
|
+
);
|
|
231
|
+
addCheck(
|
|
232
|
+
'relationship_crossings',
|
|
233
|
+
!crossingIsError || relationshipCrossings.length === 0,
|
|
234
|
+
relationshipCrossings.map((hit) => (
|
|
235
|
+
`[composition/proper-crossing] ${qualityProfile} ${relationshipName(hit.left)} crosses ${relationshipName(hit.right)} at [${formatPoint(hit.point)}]`
|
|
236
|
+
)),
|
|
237
|
+
);
|
|
238
|
+
addCheck(
|
|
239
|
+
'relationship_corridors',
|
|
240
|
+
!corridorIsError || ambiguousCorridors.length === 0,
|
|
241
|
+
ambiguousCorridors.map((hit) => (
|
|
242
|
+
`[composition/ambiguous-corridor] ${qualityProfile} ${relationshipName(hit.left.relation)} shares a ${Math.round(hit.overlapLength * 10) / 10}px corridor with ${relationshipName(hit.right.relation)} at [${formatPoint(hit.overlapStart)}] -> [${formatPoint(hit.overlapEnd)}]`
|
|
243
|
+
)),
|
|
244
|
+
);
|
|
245
|
+
addCheck(
|
|
246
|
+
'container_border_runs',
|
|
247
|
+
!qualityGatesEnforced || containerBorderRuns.length === 0,
|
|
248
|
+
containerBorderRuns.map((hit) => (
|
|
249
|
+
`[composition/container-border-run] ${relationshipName(hit.relation)} follows ${frameName(hit.frame)} ${hit.side} border for ${Math.round(hit.overlapLength * 10) / 10}px on segment ${hit.segmentIndex} [${formatPoint(hit.overlapStart)}] -> [${formatPoint(hit.overlapEnd)}]`
|
|
250
|
+
)),
|
|
251
|
+
);
|
|
252
|
+
addCheck(
|
|
253
|
+
'route_rhythm',
|
|
254
|
+
!rhythmIsError || routeRhythmIssues.length === 0,
|
|
255
|
+
routeRhythmIssues.map((hit) => (
|
|
256
|
+
`[${hit.code}] ${qualityProfile} ${relationshipName(hit.relation)} has a ${Math.round(hit.length * 10) / 10}px ${hit.position} segment ${hit.segmentIndex} [${formatPoint(hit.start)}] -> [${formatPoint(hit.end)}]`
|
|
257
|
+
)),
|
|
46
258
|
);
|
|
47
259
|
|
|
48
260
|
if (legendStart >= 0) {
|
|
@@ -59,9 +271,10 @@ if (svgMatches.length === 1) {
|
|
|
59
271
|
}
|
|
60
272
|
}
|
|
61
273
|
|
|
62
|
-
const ok = checks.every((check) => check.ok);
|
|
63
|
-
console.log(JSON.stringify({ ok, file: htmlPath, checks }, null, 2));
|
|
64
|
-
|
|
274
|
+
const ok = checks.every((check) => check.ok) && composition.status !== 'fail';
|
|
275
|
+
console.log(JSON.stringify({ ok, file: htmlPath, checks, composition }, null, 2));
|
|
276
|
+
// Let pending stdout writes drain: large receipts are asynchronous when piped.
|
|
277
|
+
process.exitCode = ok ? 0 : 1;
|
|
65
278
|
|
|
66
279
|
function collectArrows(fragment) {
|
|
67
280
|
const arrows = [];
|
|
@@ -75,12 +288,184 @@ function collectArrows(fragment) {
|
|
|
75
288
|
const segments = tag[1].toLowerCase() === 'line'
|
|
76
289
|
? lineSegments(attrs)
|
|
77
290
|
: pathSegments(attrs.d || '');
|
|
78
|
-
|
|
291
|
+
const borderSegments = tag[1].toLowerCase() === 'line'
|
|
292
|
+
? segments
|
|
293
|
+
: straightPathSegments(attrs.d || '');
|
|
294
|
+
arrows.push({
|
|
295
|
+
kind: tag[1].toLowerCase(),
|
|
296
|
+
index: index += 1,
|
|
297
|
+
raw,
|
|
298
|
+
segments,
|
|
299
|
+
borderSegments,
|
|
300
|
+
routePoints: parseRoutePoints(attrs['data-composition-points']) || (
|
|
301
|
+
borderSegments.length ? [borderSegments[0].start, ...borderSegments.map((segment) => segment.end)] : []
|
|
302
|
+
),
|
|
303
|
+
from: attrs['data-edge-from'] || attrs['data-composition-edge-from'],
|
|
304
|
+
to: attrs['data-edge-to'] || attrs['data-composition-edge-to'],
|
|
305
|
+
id: attrs['data-edge-id'] || attrs['data-composition-edge-id'],
|
|
306
|
+
key: attrs['data-edge-key'],
|
|
307
|
+
label: attrs['data-edge-label'],
|
|
308
|
+
offset: tag.index,
|
|
309
|
+
});
|
|
79
310
|
}
|
|
80
311
|
|
|
81
312
|
return arrows;
|
|
82
313
|
}
|
|
83
314
|
|
|
315
|
+
function collectRelationshipLabelMasks(fragment, arrows) {
|
|
316
|
+
const labels = [];
|
|
317
|
+
for (const match of fragment.matchAll(/<g\b[^>]*\bdata-edge-(?:key|id|from)="[^"]*"[^>]*>[\s\S]*?<\/g>/gi)) {
|
|
318
|
+
const group = match[0];
|
|
319
|
+
const groupAttrs = parseAttrs(group.match(/<g\b[^>]*>/i)?.[0] || '');
|
|
320
|
+
const rectTag = [...group.matchAll(/<rect\b[^>]*>/gi)]
|
|
321
|
+
.map((item) => item[0])
|
|
322
|
+
.find((tag) => /\bclass="[^"]*\bc-mask\b/.test(tag));
|
|
323
|
+
if (!rectTag) continue;
|
|
324
|
+
const attrs = parseAttrs(rectTag);
|
|
325
|
+
const rect = {
|
|
326
|
+
x: numberAttr(attrs, 'x'),
|
|
327
|
+
y: numberAttr(attrs, 'y'),
|
|
328
|
+
width: numberAttr(attrs, 'width'),
|
|
329
|
+
height: numberAttr(attrs, 'height'),
|
|
330
|
+
};
|
|
331
|
+
if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) continue;
|
|
332
|
+
const groupStart = match.index;
|
|
333
|
+
const groupEnd = groupStart + group.length;
|
|
334
|
+
const containedOwner = arrows.find((arrow) => (
|
|
335
|
+
arrow.offset > groupStart
|
|
336
|
+
&& arrow.offset < groupEnd
|
|
337
|
+
&& arrow.from === groupAttrs['data-edge-from']
|
|
338
|
+
&& arrow.to === groupAttrs['data-edge-to']
|
|
339
|
+
&& (!groupAttrs['data-edge-id'] || !arrow.id || arrow.id === groupAttrs['data-edge-id'])
|
|
340
|
+
));
|
|
341
|
+
const owner = arrows.find((arrow) => (
|
|
342
|
+
groupAttrs['data-edge-key'] !== undefined && arrow.key === groupAttrs['data-edge-key']
|
|
343
|
+
)) || containedOwner || arrows.find((arrow) => (
|
|
344
|
+
arrow.id === groupAttrs['data-edge-id']
|
|
345
|
+
&& arrow.from === groupAttrs['data-edge-from']
|
|
346
|
+
&& arrow.to === groupAttrs['data-edge-to']
|
|
347
|
+
));
|
|
348
|
+
if (!owner) continue;
|
|
349
|
+
if (owner.key === undefined && groupAttrs['data-edge-key'] !== undefined) owner.key = groupAttrs['data-edge-key'];
|
|
350
|
+
if (!owner.id && groupAttrs['data-edge-id']) owner.id = groupAttrs['data-edge-id'];
|
|
351
|
+
if (!owner.label && groupAttrs['data-edge-label']) owner.label = groupAttrs['data-edge-label'];
|
|
352
|
+
labels.push({
|
|
353
|
+
relation: owner,
|
|
354
|
+
relationIndex: owner.index,
|
|
355
|
+
label: groupAttrs['data-edge-label'] || '',
|
|
356
|
+
rect,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return labels;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function roundedRect(rect) {
|
|
363
|
+
return Object.fromEntries(Object.entries(rect).map(([key, value]) => [key, Math.round(value * 10) / 10]));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function roundedRouteMetrics(metrics) {
|
|
367
|
+
return {
|
|
368
|
+
...metrics,
|
|
369
|
+
maxStretch: metrics.maxStretch == null ? null : Math.round(metrics.maxStretch * 1000) / 1000,
|
|
370
|
+
minSegmentPx: metrics.minSegmentPx == null ? null : Math.round(metrics.minSegmentPx * 10) / 10,
|
|
371
|
+
minInteriorSegmentPx: metrics.minInteriorSegmentPx == null ? null : Math.round(metrics.minInteriorSegmentPx * 10) / 10,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function parseRoutePoints(value) {
|
|
376
|
+
if (!value) return null;
|
|
377
|
+
const points = value.split(';').map((pair) => pair.split(',').map(Number));
|
|
378
|
+
return points.length >= 2 && points.every(isPoint) ? points : null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function collectRelationshipCrossings(arrows) {
|
|
382
|
+
const relationships = arrows.filter((arrow) => arrow.from && arrow.to && arrow.segments.length);
|
|
383
|
+
const crossings = [];
|
|
384
|
+
for (let leftIndex = 0; leftIndex < relationships.length; leftIndex += 1) {
|
|
385
|
+
const left = relationships[leftIndex];
|
|
386
|
+
for (let rightIndex = leftIndex + 1; rightIndex < relationships.length; rightIndex += 1) {
|
|
387
|
+
const right = relationships[rightIndex];
|
|
388
|
+
if ([left.from, left.to].some((id) => id === right.from || id === right.to)) continue;
|
|
389
|
+
let point = null;
|
|
390
|
+
for (const leftSegment of left.segments) {
|
|
391
|
+
for (const rightSegment of right.segments) {
|
|
392
|
+
point = properSegmentIntersection(leftSegment.start, leftSegment.end, rightSegment.start, rightSegment.end);
|
|
393
|
+
if (point) break;
|
|
394
|
+
}
|
|
395
|
+
if (point) break;
|
|
396
|
+
}
|
|
397
|
+
if (point) crossings.push({ left, right, point });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return crossings;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function relationshipName(arrow) {
|
|
404
|
+
return arrow.id
|
|
405
|
+
? `relationship id "${arrow.id}" ("${arrow.from}" -> "${arrow.to}")`
|
|
406
|
+
: `relationship "${arrow.from}" -> "${arrow.to}"`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function relationshipRecord(arrow) {
|
|
410
|
+
const stableIndex = Number(arrow.key);
|
|
411
|
+
return {
|
|
412
|
+
id: arrow.id,
|
|
413
|
+
from: arrow.from,
|
|
414
|
+
to: arrow.to,
|
|
415
|
+
label: arrow.label || '',
|
|
416
|
+
collectionIndex: Number.isInteger(stableIndex) && stableIndex >= 0 ? stableIndex : arrow.index - 1,
|
|
417
|
+
artifactIndex: arrow.index,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function collectCompositionFrames(fragment) {
|
|
422
|
+
const frames = [];
|
|
423
|
+
for (const match of fragment.matchAll(/<(rect|path|line)\b[^>]*>/gi)) {
|
|
424
|
+
const attrs = parseAttrs(match[0]);
|
|
425
|
+
const kind = attrs['data-composition-frame-kind'];
|
|
426
|
+
if (!kind) continue;
|
|
427
|
+
const identity = attrs['data-composition-frame-id'] || frames.length;
|
|
428
|
+
if (match[1].toLowerCase() === 'rect') {
|
|
429
|
+
const frame = {
|
|
430
|
+
kind,
|
|
431
|
+
id: identity,
|
|
432
|
+
x: numberAttr(attrs, 'x'),
|
|
433
|
+
y: numberAttr(attrs, 'y'),
|
|
434
|
+
width: numberAttr(attrs, 'width'),
|
|
435
|
+
height: numberAttr(attrs, 'height'),
|
|
436
|
+
radius: numberAttr(attrs, 'rx') || 0,
|
|
437
|
+
};
|
|
438
|
+
if ([frame.x, frame.y, frame.width, frame.height].every(Number.isFinite)) frames.push(frame);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const segments = match[1].toLowerCase() === 'line'
|
|
442
|
+
? lineSegments(attrs)
|
|
443
|
+
: pathSegments(attrs.d || '');
|
|
444
|
+
for (const [segmentIndex, segment] of segments.entries()) {
|
|
445
|
+
frames.push({
|
|
446
|
+
kind,
|
|
447
|
+
id: segments.length > 1 ? `${identity}:${segmentIndex}` : identity,
|
|
448
|
+
shape: 'line',
|
|
449
|
+
start: segment.start,
|
|
450
|
+
end: segment.end,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return frames;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function frameName(frame) {
|
|
458
|
+
return `${frame.kind || 'frame'} "${frame.id}"`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function frameRecord(frame) {
|
|
462
|
+
return { kind: frame.kind, id: frame.id };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function formatPoint(point) {
|
|
466
|
+
return point.map((value) => Math.round(value * 10) / 10).join(', ');
|
|
467
|
+
}
|
|
468
|
+
|
|
84
469
|
function lineSegments(attrs) {
|
|
85
470
|
const start = [numberAttr(attrs, 'x1'), numberAttr(attrs, 'y1')];
|
|
86
471
|
const end = [numberAttr(attrs, 'x2'), numberAttr(attrs, 'y2')];
|
|
@@ -97,10 +482,85 @@ function pathSegments(d) {
|
|
|
97
482
|
return segments;
|
|
98
483
|
}
|
|
99
484
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
485
|
+
// Border runs use exact visible primitives. Non-collinear Q curves are never
|
|
486
|
+
// flattened into chords here: a tangent or sampled near-horizontal curve is
|
|
487
|
+
// not a structural border run. A fully collinear Q remains a straight visible
|
|
488
|
+
// primitive and is included.
|
|
489
|
+
function straightPathSegments(d) {
|
|
490
|
+
const tokens = d.match(/[MLHVQZmlhvqz]|[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?/g) || [];
|
|
491
|
+
const segments = [];
|
|
492
|
+
let i = 0;
|
|
493
|
+
let command = '';
|
|
494
|
+
let current = [0, 0];
|
|
495
|
+
let start = null;
|
|
496
|
+
while (i < tokens.length) {
|
|
497
|
+
if (isCommand(tokens[i])) command = tokens[i++];
|
|
498
|
+
if (!command) break;
|
|
499
|
+
const absolute = command === command.toUpperCase();
|
|
500
|
+
switch (command.toUpperCase()) {
|
|
501
|
+
case 'M':
|
|
502
|
+
case 'L': {
|
|
503
|
+
let first = true;
|
|
504
|
+
while (i + 1 < tokens.length && !isCommand(tokens[i])) {
|
|
505
|
+
const point = [Number.parseFloat(tokens[i++]), Number.parseFloat(tokens[i++])];
|
|
506
|
+
if (!point.every(Number.isFinite)) break;
|
|
507
|
+
const next = absolute ? point : [current[0] + point[0], current[1] + point[1]];
|
|
508
|
+
if (command.toUpperCase() === 'L' || !first) segments.push({ start: current, end: next });
|
|
509
|
+
current = next;
|
|
510
|
+
if (!start) start = current;
|
|
511
|
+
first = false;
|
|
512
|
+
}
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
case 'H': {
|
|
516
|
+
while (i < tokens.length && !isCommand(tokens[i])) {
|
|
517
|
+
const value = Number.parseFloat(tokens[i++]);
|
|
518
|
+
if (!Number.isFinite(value)) break;
|
|
519
|
+
const next = [absolute ? value : current[0] + value, current[1]];
|
|
520
|
+
segments.push({ start: current, end: next });
|
|
521
|
+
current = next;
|
|
522
|
+
}
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
case 'V': {
|
|
526
|
+
while (i < tokens.length && !isCommand(tokens[i])) {
|
|
527
|
+
const value = Number.parseFloat(tokens[i++]);
|
|
528
|
+
if (!Number.isFinite(value)) break;
|
|
529
|
+
const next = [current[0], absolute ? value : current[1] + value];
|
|
530
|
+
segments.push({ start: current, end: next });
|
|
531
|
+
current = next;
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
case 'Q': {
|
|
536
|
+
while (i + 3 < tokens.length && !isCommand(tokens[i])) {
|
|
537
|
+
const values = [0, 0, 0, 0].map(() => Number.parseFloat(tokens[i++]));
|
|
538
|
+
if (!values.every(Number.isFinite)) break;
|
|
539
|
+
const control = absolute ? values.slice(0, 2) : [current[0] + values[0], current[1] + values[1]];
|
|
540
|
+
const end = absolute ? values.slice(2, 4) : [current[0] + values[2], current[1] + values[3]];
|
|
541
|
+
if (Math.abs(crossProduct(current, control, end)) <= 1e-9) segments.push({ start: current, end });
|
|
542
|
+
current = end;
|
|
543
|
+
}
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
case 'Z':
|
|
547
|
+
if (start) segments.push({ start: current, end: start });
|
|
548
|
+
current = start || current;
|
|
549
|
+
command = '';
|
|
550
|
+
break;
|
|
551
|
+
default:
|
|
552
|
+
return [];
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return segments.filter(({ start: a, end: b }) => isPoint(a) && isPoint(b));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function diagonalStraightSegments(arrow) {
|
|
559
|
+
return arrow.borderSegments.flatMap(({ start, end }, segmentIndex) => (
|
|
560
|
+
Math.abs(start[0] - end[0]) > 0.01 && Math.abs(start[1] - end[1]) > 0.01
|
|
561
|
+
? [{ segmentIndex, start, end }]
|
|
562
|
+
: []
|
|
563
|
+
));
|
|
104
564
|
}
|
|
105
565
|
|
|
106
566
|
function collectLegendBoxes(fragment) {
|
|
@@ -159,6 +619,37 @@ function textBox(attrs, text) {
|
|
|
159
619
|
};
|
|
160
620
|
}
|
|
161
621
|
|
|
622
|
+
function collectDesktopReadability(svgAttrs, fragment) {
|
|
623
|
+
const viewBox = String(svgAttrs.viewBox || '').trim().split(/[\s,]+/).map(Number);
|
|
624
|
+
const viewBoxWidth = viewBox.length === 4 ? viewBox[2] : Number.NaN;
|
|
625
|
+
if (!Number.isFinite(viewBoxWidth) || viewBoxWidth <= 0) return null;
|
|
626
|
+
const scale = Math.min(1, DESKTOP_READER_DIAGRAM_WIDTH / viewBoxWidth);
|
|
627
|
+
let worst = null;
|
|
628
|
+
for (const match of fragment.matchAll(/<text\b([^>]*)>([\s\S]*?)<\/text>/gi)) {
|
|
629
|
+
const primary = /\bdata-node-label(?:\s*=|\s|$)/i.test(match[1]);
|
|
630
|
+
const boundary = /\bdata-boundary-label(?:\s*=|\s|$)/i.test(match[1]);
|
|
631
|
+
const context = /\bdata-detail\s*=\s*"context"/i.test(match[1]);
|
|
632
|
+
if (!primary && !boundary && !context) continue;
|
|
633
|
+
const attrs = parseAttrs(match[1]);
|
|
634
|
+
const fontSize = Number.parseFloat(attrs['font-size'] || '');
|
|
635
|
+
if (!Number.isFinite(fontSize)) continue;
|
|
636
|
+
const projected = projectedNodeTextPx(fontSize, viewBoxWidth);
|
|
637
|
+
if (projected >= MIN_PROJECTED_NODE_TEXT_PX) continue;
|
|
638
|
+
const candidate = {
|
|
639
|
+
viewBoxWidth,
|
|
640
|
+
scale,
|
|
641
|
+
text: stripTags(match[2]).trim(),
|
|
642
|
+
detail: primary
|
|
643
|
+
? 'primary'
|
|
644
|
+
: boundary ? 'boundary' : 'context',
|
|
645
|
+
sourceFontPx: fontSize,
|
|
646
|
+
projectedFontPx: projected,
|
|
647
|
+
};
|
|
648
|
+
if (!worst || candidate.projectedFontPx < worst.projectedFontPx) worst = candidate;
|
|
649
|
+
}
|
|
650
|
+
return worst;
|
|
651
|
+
}
|
|
652
|
+
|
|
162
653
|
function estimatedTextWidth(text, fontSize) {
|
|
163
654
|
let units = 0;
|
|
164
655
|
for (const char of text) units += char.charCodeAt(0) > 255 ? 1.8 : 0.62;
|
|
@@ -166,7 +657,7 @@ function estimatedTextWidth(text, fontSize) {
|
|
|
166
657
|
}
|
|
167
658
|
|
|
168
659
|
function pointsFromPath(d) {
|
|
169
|
-
const tokens = d.match(/[
|
|
660
|
+
const tokens = d.match(/[MLHVQZmlhvqz]|[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?/g) || [];
|
|
170
661
|
const points = [];
|
|
171
662
|
let i = 0;
|
|
172
663
|
let command = '';
|
|
@@ -209,6 +700,32 @@ function pointsFromPath(d) {
|
|
|
209
700
|
}
|
|
210
701
|
break;
|
|
211
702
|
}
|
|
703
|
+
case 'Q': {
|
|
704
|
+
while (i + 3 < tokens.length && !isCommand(tokens[i])) {
|
|
705
|
+
const controlX = Number.parseFloat(tokens[i++]);
|
|
706
|
+
const controlY = Number.parseFloat(tokens[i++]);
|
|
707
|
+
const endX = Number.parseFloat(tokens[i++]);
|
|
708
|
+
const endY = Number.parseFloat(tokens[i++]);
|
|
709
|
+
if (![controlX, controlY, endX, endY].every(Number.isFinite)) break;
|
|
710
|
+
const control = absolute
|
|
711
|
+
? [controlX, controlY]
|
|
712
|
+
: [current[0] + controlX, current[1] + controlY];
|
|
713
|
+
const end = absolute
|
|
714
|
+
? [endX, endY]
|
|
715
|
+
: [current[0] + endX, current[1] + endY];
|
|
716
|
+
const startPoint = current;
|
|
717
|
+
for (let step = 1; step <= 8; step += 1) {
|
|
718
|
+
const amount = step / 8;
|
|
719
|
+
const remaining = 1 - amount;
|
|
720
|
+
points.push([
|
|
721
|
+
remaining * remaining * startPoint[0] + 2 * remaining * amount * control[0] + amount * amount * end[0],
|
|
722
|
+
remaining * remaining * startPoint[1] + 2 * remaining * amount * control[1] + amount * amount * end[1],
|
|
723
|
+
]);
|
|
724
|
+
}
|
|
725
|
+
current = end;
|
|
726
|
+
}
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
212
729
|
case 'Z': {
|
|
213
730
|
if (start) points.push(start);
|
|
214
731
|
break;
|
|
@@ -221,6 +738,28 @@ function pointsFromPath(d) {
|
|
|
221
738
|
return points.filter(isPoint);
|
|
222
739
|
}
|
|
223
740
|
|
|
741
|
+
function properSegmentIntersection(a, b, c, d) {
|
|
742
|
+
const abC = crossProduct(a, b, c);
|
|
743
|
+
const abD = crossProduct(a, b, d);
|
|
744
|
+
const cdA = crossProduct(c, d, a);
|
|
745
|
+
const cdB = crossProduct(c, d, b);
|
|
746
|
+
const epsilon = 1e-9;
|
|
747
|
+
const opposite = (left, right) => (left > epsilon && right < -epsilon) || (left < -epsilon && right > epsilon);
|
|
748
|
+
if (!opposite(abC, abD) || !opposite(cdA, cdB)) return null;
|
|
749
|
+
const denominator = (a[0] - b[0]) * (c[1] - d[1]) - (a[1] - b[1]) * (c[0] - d[0]);
|
|
750
|
+
if (Math.abs(denominator) < epsilon) return null;
|
|
751
|
+
const ab = a[0] * b[1] - a[1] * b[0];
|
|
752
|
+
const cd = c[0] * d[1] - c[1] * d[0];
|
|
753
|
+
return [
|
|
754
|
+
(ab * (c[0] - d[0]) - (a[0] - b[0]) * cd) / denominator,
|
|
755
|
+
(ab * (c[1] - d[1]) - (a[1] - b[1]) * cd) / denominator,
|
|
756
|
+
];
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function crossProduct(a, b, c) {
|
|
760
|
+
return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
|
761
|
+
}
|
|
762
|
+
|
|
224
763
|
function segmentIntersectsBox(segment, box) {
|
|
225
764
|
const { start, end } = segment;
|
|
226
765
|
if (pointInsideBox(start, box) || pointInsideBox(end, box)) return true;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { runtimeArgs } from '../renderers/shared/bun-runtime.mjs';
|
|
7
8
|
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const skillRoot = path.resolve(__dirname, '..');
|
|
@@ -18,11 +19,9 @@ const TARGETS = [
|
|
|
18
19
|
];
|
|
19
20
|
|
|
20
21
|
for (const [mode, input, output] of TARGETS) {
|
|
21
|
-
execFileSync(process.execPath, [
|
|
22
|
-
`--config=${path.join(skillRoot, 'bunfig.toml')}`,
|
|
23
|
-
'--no-env-file',
|
|
22
|
+
execFileSync(process.execPath, runtimeArgs([
|
|
24
23
|
path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`),
|
|
25
24
|
path.join(skillRoot, 'examples', input),
|
|
26
25
|
path.join(outputRoot, output),
|
|
27
|
-
], { stdio: 'inherit' });
|
|
26
|
+
]), { stdio: 'inherit' });
|
|
28
27
|
}
|
package/ide/vscode/package.json
CHANGED