@anthusai/papyrus 0.0.0 → 1.0.0-next.1
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/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2037 -0
- package/amplify/data/resource.ts +2148 -0
- package/amplify/data/schema.js +1850 -0
- package/amplify/data/schema.ts +1953 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +115 -0
- package/amplify/functions/shared/python-bundle.ts +141 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +30 -0
- package/amplify/site-backend.js +587 -0
- package/amplify/site-backend.ts +704 -0
- package/amplify/storage/resource.js +18 -0
- package/amplify/storage/resource.ts +19 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10329 -0
- package/app/layout.tsx +146 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +10 -0
- package/bin/papyrus-infra.mjs +39 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +47 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +119 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +319 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/topic-steering-workspace.tsx +18063 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +109 -0
- package/infra/amplify-app-shell.ts +155 -0
- package/infra/index.js +12 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +89 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +40 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +986 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-nav.ts +42 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +41 -0
- package/lib/site-brand.ts +179 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/middleware.ts +78 -0
- package/package.json +296 -1
- package/publications/anth_us/brand.ts +68 -0
- package/publications/anth_us/theme.css +66 -0
- package/publications/papyrus/theme.css +11 -0
- package/publications/pilobol_us/brand.ts +37 -0
- package/publications/pilobol_us/theme.css +140 -0
- package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
- package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
- package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
- package/publications/threat_intelligence/brand.ts +40 -0
- package/publications/threat_intelligence/pictograms/art.tsx +608 -0
- package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
- package/publications/threat_intelligence/pictograms/registry.ts +30 -0
- package/publications/threat_intelligence/pictograms/system.tsx +305 -0
- package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
- package/publications/threat_intelligence/theme.css +1440 -0
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +174 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +477 -0
- package/routes.manifest.json +182 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- package/src/with-papyrus.mjs +47 -0
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BLOG_DEFENSE_CORE_NODE_ID,
|
|
3
|
+
BLOG_DEFENSE_EDGES,
|
|
4
|
+
BLOG_DEFENSE_NODES,
|
|
5
|
+
BLOG_DEFENSE_VIEWBOX_HEIGHT,
|
|
6
|
+
BLOG_DEFENSE_VIEWBOX_WIDTH,
|
|
7
|
+
type BlogDefenseEdge,
|
|
8
|
+
type BlogDefenseNode,
|
|
9
|
+
} from "./graph";
|
|
10
|
+
|
|
11
|
+
export type LayoutRect = {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type LayoutDefenseGraphInput = {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
obstacles?: LayoutRect[];
|
|
22
|
+
padX?: number;
|
|
23
|
+
padTop?: number;
|
|
24
|
+
padBottom?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type LayoutDefenseNode = BlogDefenseNode;
|
|
28
|
+
|
|
29
|
+
export type LayoutDefenseEdge = {
|
|
30
|
+
id: string;
|
|
31
|
+
from: string;
|
|
32
|
+
to: string;
|
|
33
|
+
x1: number;
|
|
34
|
+
y1: number;
|
|
35
|
+
x2: number;
|
|
36
|
+
y2: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type LayoutDefenseGraphResult = {
|
|
40
|
+
nodes: LayoutDefenseNode[];
|
|
41
|
+
edges: LayoutDefenseEdge[];
|
|
42
|
+
scale: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function uniformNodeRadius(scale: number): number {
|
|
46
|
+
return Math.max(4.4, scale * 6);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const RIGHT_ARM_BASE_CANDIDATE_IDS = ["right_attach_bottom", "right_c0"] as const;
|
|
50
|
+
const LEFT_ARM_ATTACH_CANDIDATE_IDS = [
|
|
51
|
+
"left_bridge_top",
|
|
52
|
+
"left_attach_top",
|
|
53
|
+
"left_attach_bottom",
|
|
54
|
+
"left_a0",
|
|
55
|
+
"left_b0",
|
|
56
|
+
"left_c0",
|
|
57
|
+
] as const;
|
|
58
|
+
const LEFT_ARM_CORE_TARGET_IDS = [
|
|
59
|
+
"halo_nw",
|
|
60
|
+
"halo_n",
|
|
61
|
+
"halo_w",
|
|
62
|
+
] as const;
|
|
63
|
+
const LEFT_ARM_ROW_NODE_IDS = [
|
|
64
|
+
["left_tail_4", "left_tail_3", "left_tail_2", "left_tail_1", "left_tip", "left_a3", "left_a2", "left_a1", "left_a0", "left_bridge_top"],
|
|
65
|
+
["left_b3", "left_b0", "left_attach_top"],
|
|
66
|
+
["left_c3", "left_c0", "left_attach_bottom"],
|
|
67
|
+
] as const;
|
|
68
|
+
const RIGHT_ARM_CORE_TARGET_IDS = [
|
|
69
|
+
"halo_e",
|
|
70
|
+
"halo_se",
|
|
71
|
+
"halo_s",
|
|
72
|
+
"halo_sw",
|
|
73
|
+
] as const;
|
|
74
|
+
const RIGHT_ARM_ROW_NODE_IDS = [
|
|
75
|
+
["right_c0", "right_b0", "right_a0"],
|
|
76
|
+
["right_attach_bottom", "right_c1", "right_b1", "right_a1"],
|
|
77
|
+
["right_c2", "right_b2", "right_a2"],
|
|
78
|
+
["right_c3", "right_b3", "right_a3"],
|
|
79
|
+
["right_c4", "right_b4", "right_a4"],
|
|
80
|
+
["right_c5", "right_b5", "right_a5"],
|
|
81
|
+
] as const;
|
|
82
|
+
|
|
83
|
+
function pointInRect(x: number, y: number, rect: LayoutRect, inflate = 0): boolean {
|
|
84
|
+
return x >= rect.x - inflate
|
|
85
|
+
&& x <= rect.x + rect.width + inflate
|
|
86
|
+
&& y >= rect.y - inflate
|
|
87
|
+
&& y <= rect.y + rect.height + inflate;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function orientation(ax: number, ay: number, bx: number, by: number, cx: number, cy: number): number {
|
|
91
|
+
const value = (by - ay) * (cx - bx) - (bx - ax) * (cy - by);
|
|
92
|
+
if (Math.abs(value) < 0.0001) return 0;
|
|
93
|
+
return value > 0 ? 1 : 2;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function onSegment(ax: number, ay: number, bx: number, by: number, cx: number, cy: number): boolean {
|
|
97
|
+
return bx <= Math.max(ax, cx) + 0.0001
|
|
98
|
+
&& bx + 0.0001 >= Math.min(ax, cx)
|
|
99
|
+
&& by <= Math.max(ay, cy) + 0.0001
|
|
100
|
+
&& by + 0.0001 >= Math.min(ay, cy);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function segmentsIntersect(
|
|
104
|
+
x1: number,
|
|
105
|
+
y1: number,
|
|
106
|
+
x2: number,
|
|
107
|
+
y2: number,
|
|
108
|
+
x3: number,
|
|
109
|
+
y3: number,
|
|
110
|
+
x4: number,
|
|
111
|
+
y4: number,
|
|
112
|
+
): boolean {
|
|
113
|
+
const o1 = orientation(x1, y1, x2, y2, x3, y3);
|
|
114
|
+
const o2 = orientation(x1, y1, x2, y2, x4, y4);
|
|
115
|
+
const o3 = orientation(x3, y3, x4, y4, x1, y1);
|
|
116
|
+
const o4 = orientation(x3, y3, x4, y4, x2, y2);
|
|
117
|
+
if (o1 !== o2 && o3 !== o4) return true;
|
|
118
|
+
if (o1 === 0 && onSegment(x1, y1, x3, y3, x2, y2)) return true;
|
|
119
|
+
if (o2 === 0 && onSegment(x1, y1, x4, y4, x2, y2)) return true;
|
|
120
|
+
if (o3 === 0 && onSegment(x3, y3, x1, y1, x4, y4)) return true;
|
|
121
|
+
if (o4 === 0 && onSegment(x3, y3, x2, y2, x4, y4)) return true;
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function segmentIntersectsRect(
|
|
126
|
+
x1: number,
|
|
127
|
+
y1: number,
|
|
128
|
+
x2: number,
|
|
129
|
+
y2: number,
|
|
130
|
+
rect: LayoutRect,
|
|
131
|
+
inflate = 0,
|
|
132
|
+
): boolean {
|
|
133
|
+
const inflated = {
|
|
134
|
+
x: rect.x - inflate,
|
|
135
|
+
y: rect.y - inflate,
|
|
136
|
+
width: rect.width + inflate * 2,
|
|
137
|
+
height: rect.height + inflate * 2,
|
|
138
|
+
};
|
|
139
|
+
if (pointInRect(x1, y1, inflated) || pointInRect(x2, y2, inflated)) return true;
|
|
140
|
+
const left = inflated.x;
|
|
141
|
+
const right = inflated.x + inflated.width;
|
|
142
|
+
const top = inflated.y;
|
|
143
|
+
const bottom = inflated.y + inflated.height;
|
|
144
|
+
return segmentsIntersect(x1, y1, x2, y2, left, top, right, top)
|
|
145
|
+
|| segmentsIntersect(x1, y1, x2, y2, right, top, right, bottom)
|
|
146
|
+
|| segmentsIntersect(x1, y1, x2, y2, right, bottom, left, bottom)
|
|
147
|
+
|| segmentsIntersect(x1, y1, x2, y2, left, bottom, left, top);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function edgeIntersectsObstacles(
|
|
151
|
+
x1: number,
|
|
152
|
+
y1: number,
|
|
153
|
+
x2: number,
|
|
154
|
+
y2: number,
|
|
155
|
+
obstacles: LayoutRect[],
|
|
156
|
+
): boolean {
|
|
157
|
+
return obstacles.some((obstacle) => segmentIntersectsRect(x1, y1, x2, y2, obstacle, 2));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function nodeIntersectsObstacles(
|
|
161
|
+
x: number,
|
|
162
|
+
y: number,
|
|
163
|
+
radius: number,
|
|
164
|
+
obstacles: LayoutRect[],
|
|
165
|
+
): boolean {
|
|
166
|
+
return obstacles.some((obstacle) => {
|
|
167
|
+
const nearestX = Math.max(obstacle.x, Math.min(x, obstacle.x + obstacle.width));
|
|
168
|
+
const nearestY = Math.max(obstacle.y, Math.min(y, obstacle.y + obstacle.height));
|
|
169
|
+
const dx = x - nearestX;
|
|
170
|
+
const dy = y - nearestY;
|
|
171
|
+
return (dx * dx) + (dy * dy) <= (radius * radius);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function coreAnchor(
|
|
176
|
+
width: number,
|
|
177
|
+
height: number,
|
|
178
|
+
padX: number,
|
|
179
|
+
scale: number,
|
|
180
|
+
coreNode: BlogDefenseNode,
|
|
181
|
+
): { x: number; y: number } {
|
|
182
|
+
const coreZoneNodes = BLOG_DEFENSE_NODES.filter((node) => node.zone === "core");
|
|
183
|
+
const maxCoreDx = coreZoneNodes.reduce((maxDx, node) => Math.max(maxDx, node.x - coreNode.x), 0);
|
|
184
|
+
const rightAlignedX = width - padX - maxCoreDx * scale;
|
|
185
|
+
return {
|
|
186
|
+
x: rightAlignedX,
|
|
187
|
+
y: Math.max(76, Math.min(height * 0.24, 208)),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function bestCoreAnchor(input: {
|
|
192
|
+
width: number;
|
|
193
|
+
height: number;
|
|
194
|
+
padX: number;
|
|
195
|
+
padTop: number;
|
|
196
|
+
padBottom: number;
|
|
197
|
+
scale: number;
|
|
198
|
+
coreNode: BlogDefenseNode;
|
|
199
|
+
nodeRadius: number;
|
|
200
|
+
obstacles: LayoutRect[];
|
|
201
|
+
}): { x: number; y: number } {
|
|
202
|
+
const {
|
|
203
|
+
width,
|
|
204
|
+
height,
|
|
205
|
+
padX,
|
|
206
|
+
padTop,
|
|
207
|
+
padBottom,
|
|
208
|
+
scale,
|
|
209
|
+
coreNode,
|
|
210
|
+
nodeRadius,
|
|
211
|
+
obstacles,
|
|
212
|
+
} = input;
|
|
213
|
+
const base = coreAnchor(width, height, padX, scale, coreNode);
|
|
214
|
+
if (!obstacles.length) return base;
|
|
215
|
+
|
|
216
|
+
const coreZoneNodes = BLOG_DEFENSE_NODES.filter((node) => node.zone === "core");
|
|
217
|
+
const maxAnchorX = width - padX - nodeRadius;
|
|
218
|
+
const step = Math.max(4, Math.round(width * 0.012));
|
|
219
|
+
let best = base;
|
|
220
|
+
let bestScore = -Infinity;
|
|
221
|
+
|
|
222
|
+
for (let x = base.x; x <= maxAnchorX + 0.001; x += step) {
|
|
223
|
+
let score = 0;
|
|
224
|
+
for (const node of coreZoneNodes) {
|
|
225
|
+
const candidateX = x + (node.x - coreNode.x) * scale;
|
|
226
|
+
const candidateY = base.y + (node.y - coreNode.y) * scale;
|
|
227
|
+
const inBounds = candidateX > padX + nodeRadius
|
|
228
|
+
&& candidateX < width - padX - nodeRadius
|
|
229
|
+
&& candidateY > padTop + nodeRadius
|
|
230
|
+
&& candidateY < height - padBottom - nodeRadius;
|
|
231
|
+
if (!inBounds) continue;
|
|
232
|
+
if (nodeIntersectsObstacles(candidateX, candidateY, nodeRadius + 2, obstacles)) continue;
|
|
233
|
+
score += node.id === BLOG_DEFENSE_CORE_NODE_ID ? 100 : 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const distancePenalty = Math.abs(x - base.x) / Math.max(1, width);
|
|
237
|
+
const adjustedScore = score - distancePenalty;
|
|
238
|
+
if (adjustedScore > bestScore) {
|
|
239
|
+
bestScore = adjustedScore;
|
|
240
|
+
best = { x, y: base.y };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return best;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function bestCorridorX(
|
|
248
|
+
width: number,
|
|
249
|
+
height: number,
|
|
250
|
+
padX: number,
|
|
251
|
+
padTop: number,
|
|
252
|
+
padBottom: number,
|
|
253
|
+
scale: number,
|
|
254
|
+
coreNode: BlogDefenseNode,
|
|
255
|
+
anchor: { x: number; y: number },
|
|
256
|
+
obstacles: LayoutRect[],
|
|
257
|
+
): number {
|
|
258
|
+
const preferred = width - padX - Math.max(12, 22 * scale);
|
|
259
|
+
const minX = Math.max(padX + 10, preferred - Math.max(40, width * 0.13));
|
|
260
|
+
const maxX = preferred;
|
|
261
|
+
const step = Math.max(6, Math.round(width * 0.01));
|
|
262
|
+
let bestX = preferred;
|
|
263
|
+
let bestScore = -1;
|
|
264
|
+
|
|
265
|
+
for (let x = maxX; x >= minX; x -= step) {
|
|
266
|
+
let clearCount = 0;
|
|
267
|
+
for (const node of BLOG_DEFENSE_NODES) {
|
|
268
|
+
if (node.zone !== "right_corridor") continue;
|
|
269
|
+
const dy = node.y - coreNode.y;
|
|
270
|
+
const y = Math.min(
|
|
271
|
+
height - padBottom - node.radius * scale,
|
|
272
|
+
Math.max(padTop + node.radius * scale, anchor.y + dy * scale),
|
|
273
|
+
);
|
|
274
|
+
const blocked = obstacles.some((obstacle) => pointInRect(x, y, obstacle, node.radius * scale + 2));
|
|
275
|
+
if (!blocked) clearCount += 1;
|
|
276
|
+
}
|
|
277
|
+
if (clearCount > bestScore || (clearCount === bestScore && x > bestX)) {
|
|
278
|
+
bestScore = clearCount;
|
|
279
|
+
bestX = x;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return bestX;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function templateNodeById(id: string): BlogDefenseNode | undefined {
|
|
287
|
+
return BLOG_DEFENSE_NODES.find((node) => node.id === id);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normalizeAngle(angle: number): number {
|
|
291
|
+
while (angle > Math.PI) angle -= Math.PI * 2;
|
|
292
|
+
while (angle < -Math.PI) angle += Math.PI * 2;
|
|
293
|
+
return angle;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function buildEdgeRecord(from: LayoutDefenseNode, to: LayoutDefenseNode, id: string): LayoutDefenseEdge {
|
|
297
|
+
return {
|
|
298
|
+
id,
|
|
299
|
+
from: from.id,
|
|
300
|
+
to: to.id,
|
|
301
|
+
x1: from.x,
|
|
302
|
+
y1: from.y,
|
|
303
|
+
x2: to.x,
|
|
304
|
+
y2: to.y,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function keepNodeOutsideCoreEnvelope(input: {
|
|
309
|
+
nextX: number;
|
|
310
|
+
nextY: number;
|
|
311
|
+
node: LayoutDefenseNode;
|
|
312
|
+
coreCenter?: LayoutDefenseNode;
|
|
313
|
+
outerCoreRadius: number;
|
|
314
|
+
preferredHorizontalDirection: -1 | 1;
|
|
315
|
+
}): { x: number; y: number } {
|
|
316
|
+
const {
|
|
317
|
+
nextX,
|
|
318
|
+
nextY,
|
|
319
|
+
node,
|
|
320
|
+
coreCenter,
|
|
321
|
+
outerCoreRadius,
|
|
322
|
+
preferredHorizontalDirection,
|
|
323
|
+
} = input;
|
|
324
|
+
if (!coreCenter) {
|
|
325
|
+
return { x: nextX, y: nextY };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const clearance = Math.max(4, node.radius * 0.5);
|
|
329
|
+
const minDistanceFromCore = outerCoreRadius + node.radius + clearance;
|
|
330
|
+
const dx = nextX - coreCenter.x;
|
|
331
|
+
const dy = nextY - coreCenter.y;
|
|
332
|
+
const distance = Math.hypot(dx, dy);
|
|
333
|
+
if (distance >= minDistanceFromCore) {
|
|
334
|
+
return { x: nextX, y: nextY };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const verticalDistance = Math.abs(dy);
|
|
338
|
+
if (verticalDistance < minDistanceFromCore) {
|
|
339
|
+
const horizontalDistance = Math.sqrt((minDistanceFromCore ** 2) - (verticalDistance ** 2));
|
|
340
|
+
return {
|
|
341
|
+
x: coreCenter.x + preferredHorizontalDirection * horizontalDistance,
|
|
342
|
+
y: nextY,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const ux = distance > 0.0001 ? dx / distance : preferredHorizontalDirection;
|
|
347
|
+
const uy = distance > 0.0001 ? dy / distance : 0;
|
|
348
|
+
return {
|
|
349
|
+
x: coreCenter.x + ux * minDistanceFromCore,
|
|
350
|
+
y: coreCenter.y + uy * minDistanceFromCore,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function leftArmIrregularityOffset(nodeId: string, rowIndex: number, scale: number): number {
|
|
355
|
+
if (rowIndex === 0) {
|
|
356
|
+
const offsets: Record<string, number> = {
|
|
357
|
+
left_tail_4: -24,
|
|
358
|
+
left_tail_3: -18,
|
|
359
|
+
left_tail_2: -12,
|
|
360
|
+
left_tail_1: -7,
|
|
361
|
+
left_tip: -2,
|
|
362
|
+
left_a3: 1,
|
|
363
|
+
left_a2: 2,
|
|
364
|
+
left_a1: 5,
|
|
365
|
+
left_a0: 1,
|
|
366
|
+
left_bridge_top: 14,
|
|
367
|
+
};
|
|
368
|
+
return (offsets[nodeId] ?? 0) * scale;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (rowIndex === 1) {
|
|
372
|
+
const offsets: Record<string, number> = {
|
|
373
|
+
left_b3: -10,
|
|
374
|
+
left_b0: -4,
|
|
375
|
+
left_attach_top: 12,
|
|
376
|
+
};
|
|
377
|
+
return (offsets[nodeId] ?? 0) * scale;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const offsets: Record<string, number> = {
|
|
381
|
+
left_c3: -7,
|
|
382
|
+
left_c0: -3,
|
|
383
|
+
left_attach_bottom: 11,
|
|
384
|
+
};
|
|
385
|
+
return (offsets[nodeId] ?? 0) * scale;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function layoutLeftArmRows(input: {
|
|
389
|
+
coreNodes: LayoutDefenseNode[];
|
|
390
|
+
leftArmNodes: LayoutDefenseNode[];
|
|
391
|
+
positionedNodes: Map<string, LayoutDefenseNode>;
|
|
392
|
+
padX: number;
|
|
393
|
+
padTop: number;
|
|
394
|
+
scale: number;
|
|
395
|
+
coreCenter?: LayoutDefenseNode;
|
|
396
|
+
}): void {
|
|
397
|
+
const {
|
|
398
|
+
coreNodes,
|
|
399
|
+
leftArmNodes,
|
|
400
|
+
positionedNodes,
|
|
401
|
+
padX,
|
|
402
|
+
padTop,
|
|
403
|
+
scale,
|
|
404
|
+
coreCenter,
|
|
405
|
+
} = input;
|
|
406
|
+
|
|
407
|
+
if (!leftArmNodes.length) return;
|
|
408
|
+
|
|
409
|
+
const coreLeftEdge = coreNodes.reduce((min, node) => Math.min(min, node.x - node.radius), Infinity);
|
|
410
|
+
const coreTopY = coreNodes.reduce((min, node) => Math.min(min, node.y), Infinity);
|
|
411
|
+
const outerCoreRadius = coreCenter
|
|
412
|
+
? coreNodes.reduce((maxRadius, node) => (
|
|
413
|
+
Math.max(maxRadius, Math.hypot(node.x - coreCenter.x, node.y - coreCenter.y) + node.radius)
|
|
414
|
+
), 0)
|
|
415
|
+
: 0;
|
|
416
|
+
|
|
417
|
+
const visibleRows = LEFT_ARM_ROW_NODE_IDS
|
|
418
|
+
.map((rowIds, templateIndex) => ({
|
|
419
|
+
templateIndex,
|
|
420
|
+
nodes: rowIds
|
|
421
|
+
.map((id) => positionedNodes.get(id))
|
|
422
|
+
.filter((node): node is LayoutDefenseNode => Boolean(node)),
|
|
423
|
+
fullIds: [...rowIds] as string[],
|
|
424
|
+
}))
|
|
425
|
+
.filter((row) => row.nodes.length > 0);
|
|
426
|
+
if (!visibleRows.length) return;
|
|
427
|
+
|
|
428
|
+
const topRow = visibleRows[0];
|
|
429
|
+
const topRightNode = topRow.nodes
|
|
430
|
+
.slice()
|
|
431
|
+
.sort((left, right) => right.x - left.x)[0];
|
|
432
|
+
const topRowY = Math.max(padTop + (topRightNode?.radius ?? 0), coreTopY);
|
|
433
|
+
const rowGap = Math.max(20 * scale, Math.min(34 * scale, outerCoreRadius * 0.32));
|
|
434
|
+
const topRowLeft = topRow.nodes.reduce((min, node) => Math.min(min, node.x), Infinity) - (62 * scale);
|
|
435
|
+
const topRowRight = topRightNode?.x ?? coreLeftEdge - (10 * scale);
|
|
436
|
+
const topRowWidth = Math.max(12 * scale, topRowRight - topRowLeft);
|
|
437
|
+
|
|
438
|
+
for (let visibleIndex = 0; visibleIndex < visibleRows.length; visibleIndex += 1) {
|
|
439
|
+
const row = visibleRows[visibleIndex];
|
|
440
|
+
const rowY = topRowY + rowGap * visibleIndex;
|
|
441
|
+
const slotCount = row.fullIds.length;
|
|
442
|
+
const rawRowLeft = row.nodes.reduce((min, node) => Math.min(min, node.x), Infinity);
|
|
443
|
+
const rawRowRight = row.nodes.reduce((max, node) => Math.max(max, node.x), -Infinity);
|
|
444
|
+
const rawRowWidth = Math.max(1, rawRowRight - rawRowLeft);
|
|
445
|
+
const rowRightNode = row.nodes
|
|
446
|
+
.slice()
|
|
447
|
+
.sort((left, right) => right.x - left.x)[0];
|
|
448
|
+
const rowRightCenter = visibleIndex === 0
|
|
449
|
+
? topRowRight
|
|
450
|
+
: Math.min(
|
|
451
|
+
rowRightNode?.x ?? (coreLeftEdge - (10 * scale)),
|
|
452
|
+
coreLeftEdge - Math.max(8 * scale, (rowRightNode?.radius ?? 0) + (4 * scale)),
|
|
453
|
+
);
|
|
454
|
+
const taperInset = visibleIndex === 0 ? 0 : topRowWidth * (0.16 + (visibleIndex - 1) * 0.07);
|
|
455
|
+
const currentRowLeft = rawRowLeft;
|
|
456
|
+
const monotonicLeft = topRowLeft + taperInset;
|
|
457
|
+
const rowLeftCenter = Math.min(
|
|
458
|
+
rowRightCenter - Math.max(10 * scale, row.nodes[0]?.radius ?? 0),
|
|
459
|
+
Math.max(
|
|
460
|
+
padX + (row.nodes[0]?.radius ?? 0),
|
|
461
|
+
visibleIndex === 0
|
|
462
|
+
? Math.min(currentRowLeft - (10 * scale), monotonicLeft)
|
|
463
|
+
: Math.max(currentRowLeft - (4 * scale), monotonicLeft),
|
|
464
|
+
),
|
|
465
|
+
);
|
|
466
|
+
const rowSpan = Math.max(1, rowRightCenter - rowLeftCenter);
|
|
467
|
+
const templateBlend = visibleIndex === 0 ? 0.76 : visibleIndex === 1 ? 0.56 : 0.42;
|
|
468
|
+
|
|
469
|
+
for (const node of row.nodes) {
|
|
470
|
+
const slotIndex = row.fullIds.indexOf(node.id);
|
|
471
|
+
if (slotIndex < 0) continue;
|
|
472
|
+
const slotProgress = slotCount === 1 ? 1 : slotIndex / (slotCount - 1);
|
|
473
|
+
const rawProgress = rawRowWidth <= 1 ? slotProgress : (node.x - rawRowLeft) / rawRowWidth;
|
|
474
|
+
let blendedProgress = (slotProgress * (1 - templateBlend)) + (rawProgress * templateBlend);
|
|
475
|
+
if (visibleIndex === 0) {
|
|
476
|
+
if (slotProgress <= 0.45) blendedProgress -= 0.03;
|
|
477
|
+
if (slotProgress >= 0.82) blendedProgress += 0.015;
|
|
478
|
+
} else if (visibleIndex === 1) {
|
|
479
|
+
if (slotProgress <= 0.3) blendedProgress -= 0.02;
|
|
480
|
+
if (slotProgress >= 0.72) blendedProgress += 0.02;
|
|
481
|
+
} else {
|
|
482
|
+
if (slotProgress <= 0.35) blendedProgress -= 0.015;
|
|
483
|
+
}
|
|
484
|
+
blendedProgress = Math.max(0, Math.min(1, blendedProgress));
|
|
485
|
+
let nextX = rowLeftCenter + rowSpan * blendedProgress + leftArmIrregularityOffset(node.id, visibleIndex, scale);
|
|
486
|
+
let nextY = rowY;
|
|
487
|
+
|
|
488
|
+
const outsideCore = keepNodeOutsideCoreEnvelope({
|
|
489
|
+
nextX,
|
|
490
|
+
nextY,
|
|
491
|
+
node,
|
|
492
|
+
coreCenter,
|
|
493
|
+
outerCoreRadius,
|
|
494
|
+
preferredHorizontalDirection: -1,
|
|
495
|
+
});
|
|
496
|
+
nextX = outsideCore.x;
|
|
497
|
+
nextY = outsideCore.y;
|
|
498
|
+
|
|
499
|
+
positionedNodes.set(node.id, {
|
|
500
|
+
...node,
|
|
501
|
+
x: Math.max(padX + node.radius, nextX),
|
|
502
|
+
y: Math.max(padTop + node.radius, nextY),
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function layoutRightArmRows(input: {
|
|
509
|
+
coreNodes: LayoutDefenseNode[];
|
|
510
|
+
rightArmNodes: LayoutDefenseNode[];
|
|
511
|
+
positionedNodes: Map<string, LayoutDefenseNode>;
|
|
512
|
+
padX: number;
|
|
513
|
+
padTop: number;
|
|
514
|
+
padBottom: number;
|
|
515
|
+
height: number;
|
|
516
|
+
scale: number;
|
|
517
|
+
coreCenter?: LayoutDefenseNode;
|
|
518
|
+
}): void {
|
|
519
|
+
const {
|
|
520
|
+
coreNodes,
|
|
521
|
+
rightArmNodes,
|
|
522
|
+
positionedNodes,
|
|
523
|
+
padX,
|
|
524
|
+
padTop,
|
|
525
|
+
padBottom,
|
|
526
|
+
height,
|
|
527
|
+
scale,
|
|
528
|
+
coreCenter,
|
|
529
|
+
} = input;
|
|
530
|
+
const drawableNodes = rightArmNodes.filter((node) => !node.id.startsWith("right_bridge_"));
|
|
531
|
+
if (!drawableNodes.length) return;
|
|
532
|
+
|
|
533
|
+
const coreRightEdge = coreNodes.reduce((max, node) => Math.max(max, node.x + node.radius), -Infinity);
|
|
534
|
+
const coreLeftEdge = coreNodes.reduce((min, node) => Math.min(min, node.x - node.radius), Infinity);
|
|
535
|
+
const baseEnvelopeWidth = Math.max(16, (coreRightEdge - coreLeftEdge) * 0.82);
|
|
536
|
+
const outerCoreRadius = coreCenter
|
|
537
|
+
? coreNodes.reduce((maxRadius, node) => (
|
|
538
|
+
Math.max(maxRadius, Math.hypot(node.x - coreCenter.x, node.y - coreCenter.y) + node.radius)
|
|
539
|
+
), 0)
|
|
540
|
+
: 0;
|
|
541
|
+
const minRadius = drawableNodes.reduce((min, node) => Math.min(min, node.radius), Infinity);
|
|
542
|
+
const tailWidthTarget = Math.max(2.2 * scale, minRadius * 2);
|
|
543
|
+
|
|
544
|
+
const visibleRows = RIGHT_ARM_ROW_NODE_IDS
|
|
545
|
+
.map((rowIds, templateIndex) => ({
|
|
546
|
+
templateIndex,
|
|
547
|
+
nodes: rowIds
|
|
548
|
+
.map((id) => positionedNodes.get(id))
|
|
549
|
+
.filter((node): node is LayoutDefenseNode => Boolean(node)),
|
|
550
|
+
fullIds: [...rowIds] as string[],
|
|
551
|
+
}))
|
|
552
|
+
.filter((row) => row.nodes.length > 0);
|
|
553
|
+
if (!visibleRows.length) return;
|
|
554
|
+
|
|
555
|
+
const topY = visibleRows.reduce((min, row) => Math.min(min, ...row.nodes.map((node) => node.y)), Infinity);
|
|
556
|
+
const bottomY = visibleRows.reduce((max, row) => Math.max(max, ...row.nodes.map((node) => node.y)), -Infinity);
|
|
557
|
+
const rowSpan = Math.max(1, bottomY - topY);
|
|
558
|
+
const rowGap = visibleRows.length === 1 ? 0 : rowSpan / (visibleRows.length - 1);
|
|
559
|
+
const rowTilt = Math.min(Math.max(10 * scale, rowGap * 0.44), 24 * scale);
|
|
560
|
+
|
|
561
|
+
for (let visibleIndex = 0; visibleIndex < visibleRows.length; visibleIndex += 1) {
|
|
562
|
+
const row = visibleRows[visibleIndex];
|
|
563
|
+
const rowProgress = visibleRows.length === 1 ? 0 : visibleIndex / (visibleRows.length - 1);
|
|
564
|
+
const rowWidth = baseEnvelopeWidth - (baseEnvelopeWidth - tailWidthTarget) * rowProgress;
|
|
565
|
+
const rowY = topY + rowSpan * rowProgress;
|
|
566
|
+
const slotCount = row.fullIds.length;
|
|
567
|
+
|
|
568
|
+
for (const node of row.nodes) {
|
|
569
|
+
const slotIndex = row.fullIds.indexOf(node.id);
|
|
570
|
+
if (slotIndex < 0) continue;
|
|
571
|
+
const slotProgress = slotCount === 1 ? 1 : slotIndex / (slotCount - 1);
|
|
572
|
+
const rowRightCenter = coreRightEdge - node.radius;
|
|
573
|
+
const rowLeftCenter = Math.max(padX + node.radius, rowRightCenter - rowWidth);
|
|
574
|
+
let nextX = rowLeftCenter + (rowRightCenter - rowLeftCenter) * slotProgress;
|
|
575
|
+
const slotOffset = slotCount === 1 ? 0 : ((slotProgress - 0.5) * rowTilt);
|
|
576
|
+
let nextY = rowY + slotOffset;
|
|
577
|
+
|
|
578
|
+
if (node.id === "right_c0") {
|
|
579
|
+
nextX += 112 * scale;
|
|
580
|
+
nextY += 118 * scale;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (coreCenter) {
|
|
584
|
+
const clearance = Math.max(4 * scale, 3);
|
|
585
|
+
const minDistanceFromCore = outerCoreRadius + node.radius + clearance;
|
|
586
|
+
const dx = nextX - coreCenter.x;
|
|
587
|
+
const dy = nextY - coreCenter.y;
|
|
588
|
+
const distance = Math.hypot(dx, dy);
|
|
589
|
+
if (distance < minDistanceFromCore) {
|
|
590
|
+
const verticalDistance = Math.abs(dy);
|
|
591
|
+
if (verticalDistance < minDistanceFromCore) {
|
|
592
|
+
const horizontalDistance = Math.sqrt((minDistanceFromCore ** 2) - (verticalDistance ** 2));
|
|
593
|
+
const direction = dx <= 0 ? -1 : 1;
|
|
594
|
+
nextX = coreCenter.x + direction * horizontalDistance;
|
|
595
|
+
} else {
|
|
596
|
+
const ux = distance > 0.0001 ? dx / distance : 1;
|
|
597
|
+
const uy = distance > 0.0001 ? dy / distance : 0;
|
|
598
|
+
nextX = coreCenter.x + ux * minDistanceFromCore;
|
|
599
|
+
nextY = coreCenter.y + uy * minDistanceFromCore;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
nextX = Math.min(Math.max(nextX, padX + node.radius), coreRightEdge - node.radius);
|
|
605
|
+
nextY = Math.min(height - padBottom - node.radius, Math.max(padTop + node.radius, nextY));
|
|
606
|
+
positionedNodes.set(node.id, {
|
|
607
|
+
...node,
|
|
608
|
+
x: nextX,
|
|
609
|
+
y: nextY,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
for (const node of rightArmNodes.filter((candidate) => candidate.id.startsWith("right_bridge_"))) {
|
|
615
|
+
positionedNodes.set(node.id, {
|
|
616
|
+
...node,
|
|
617
|
+
x: Math.min(node.x, coreRightEdge - node.radius),
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
type BuildRightArmAttachmentEdgesInput = {
|
|
623
|
+
positionedNodes: Map<string, LayoutDefenseNode>;
|
|
624
|
+
coreCenter: LayoutDefenseNode;
|
|
625
|
+
obstacles: LayoutRect[];
|
|
626
|
+
usedEdgeIds: Set<string>;
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
function buildRightArmAttachmentEdges(input: BuildRightArmAttachmentEdgesInput): LayoutDefenseEdge[] {
|
|
630
|
+
const { positionedNodes, coreCenter, obstacles, usedEdgeIds } = input;
|
|
631
|
+
const candidateNodes = RIGHT_ARM_BASE_CANDIDATE_IDS
|
|
632
|
+
.map((id) => positionedNodes.get(id))
|
|
633
|
+
.filter((node): node is LayoutDefenseNode => Boolean(node));
|
|
634
|
+
const preferredExtraCandidateIds = RIGHT_ARM_ROW_NODE_IDS[1]
|
|
635
|
+
.filter((id) => id !== "right_attach_bottom");
|
|
636
|
+
const extraCandidate = preferredExtraCandidateIds
|
|
637
|
+
.map((id) => positionedNodes.get(id))
|
|
638
|
+
.filter((node): node is LayoutDefenseNode => Boolean(node))
|
|
639
|
+
.sort((a, b) => b.x - a.x || a.y - b.y)[0]
|
|
640
|
+
?? Array.from(positionedNodes.values())
|
|
641
|
+
.filter((node) => (
|
|
642
|
+
node.zone === "right_arm"
|
|
643
|
+
&& !RIGHT_ARM_BASE_CANDIDATE_IDS.includes(node.id as (typeof RIGHT_ARM_BASE_CANDIDATE_IDS)[number])
|
|
644
|
+
&& !node.id.startsWith("right_bridge_")
|
|
645
|
+
))
|
|
646
|
+
.sort((a, b) => a.y - b.y || b.x - a.x)[0];
|
|
647
|
+
if (extraCandidate) {
|
|
648
|
+
candidateNodes.push(extraCandidate);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const targets = RIGHT_ARM_CORE_TARGET_IDS
|
|
652
|
+
.map((id) => positionedNodes.get(id))
|
|
653
|
+
.filter((node): node is LayoutDefenseNode => Boolean(node));
|
|
654
|
+
if (!candidateNodes.length || !targets.length) {
|
|
655
|
+
return [];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const usedTargetIds = new Set<string>();
|
|
659
|
+
const attachmentEdges: LayoutDefenseEdge[] = [];
|
|
660
|
+
const orderedCandidates = candidateNodes
|
|
661
|
+
.slice()
|
|
662
|
+
.sort((a, b) => a.x - b.x || a.y - b.y);
|
|
663
|
+
|
|
664
|
+
for (const candidate of orderedCandidates) {
|
|
665
|
+
const candidateAngle = Math.atan2(candidate.y - coreCenter.y, candidate.x - coreCenter.x);
|
|
666
|
+
const bestTarget = targets
|
|
667
|
+
.filter((target) => candidate.id === "right_c0" || !usedTargetIds.has(target.id))
|
|
668
|
+
.filter((target) => !edgeIntersectsObstacles(candidate.x, candidate.y, target.x, target.y, obstacles))
|
|
669
|
+
.sort((left, right) => {
|
|
670
|
+
const leftDistance = Math.hypot(candidate.x - left.x, candidate.y - left.y);
|
|
671
|
+
const rightDistance = Math.hypot(candidate.x - right.x, candidate.y - right.y);
|
|
672
|
+
if (Math.abs(leftDistance - rightDistance) > 0.001) {
|
|
673
|
+
return leftDistance - rightDistance;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const leftAngleDelta = Math.abs(normalizeAngle(candidateAngle - Math.atan2(left.y - coreCenter.y, left.x - coreCenter.x)));
|
|
677
|
+
const rightAngleDelta = Math.abs(normalizeAngle(candidateAngle - Math.atan2(right.y - coreCenter.y, right.x - coreCenter.x)));
|
|
678
|
+
if (Math.abs(leftAngleDelta - rightAngleDelta) > 0.001) {
|
|
679
|
+
return leftAngleDelta - rightAngleDelta;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return left.x - right.x;
|
|
683
|
+
})[0];
|
|
684
|
+
|
|
685
|
+
if (!bestTarget) continue;
|
|
686
|
+
|
|
687
|
+
const edgeId = `right_dynamic_attach_${candidate.id}_${bestTarget.id}`;
|
|
688
|
+
if (usedEdgeIds.has(edgeId)) continue;
|
|
689
|
+
attachmentEdges.push(buildEdgeRecord(candidate, bestTarget, edgeId));
|
|
690
|
+
usedEdgeIds.add(edgeId);
|
|
691
|
+
usedTargetIds.add(bestTarget.id);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return attachmentEdges;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
type BuildLeftArmAttachmentEdgesInput = {
|
|
698
|
+
positionedNodes: Map<string, LayoutDefenseNode>;
|
|
699
|
+
coreCenter: LayoutDefenseNode;
|
|
700
|
+
obstacles: LayoutRect[];
|
|
701
|
+
usedEdgeIds: Set<string>;
|
|
702
|
+
scale: number;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
function buildLeftArmAttachmentEdges(input: BuildLeftArmAttachmentEdgesInput): LayoutDefenseEdge[] {
|
|
706
|
+
const {
|
|
707
|
+
positionedNodes,
|
|
708
|
+
coreCenter,
|
|
709
|
+
obstacles,
|
|
710
|
+
usedEdgeIds,
|
|
711
|
+
scale,
|
|
712
|
+
} = input;
|
|
713
|
+
const candidateNodes = LEFT_ARM_ATTACH_CANDIDATE_IDS
|
|
714
|
+
.map((id) => positionedNodes.get(id))
|
|
715
|
+
.filter((node): node is LayoutDefenseNode => Boolean(node))
|
|
716
|
+
.sort((a, b) => {
|
|
717
|
+
const priority = ["left_a0", "left_b0", "left_c0", "left_attach_top", "left_attach_bottom", "left_bridge_top"];
|
|
718
|
+
const aIndex = priority.indexOf(a.id);
|
|
719
|
+
const bIndex = priority.indexOf(b.id);
|
|
720
|
+
if (aIndex !== bIndex) return aIndex - bIndex;
|
|
721
|
+
return a.y - b.y || b.x - a.x;
|
|
722
|
+
});
|
|
723
|
+
const targets = LEFT_ARM_CORE_TARGET_IDS
|
|
724
|
+
.map((id) => positionedNodes.get(id))
|
|
725
|
+
.filter((node): node is LayoutDefenseNode => Boolean(node));
|
|
726
|
+
if (!candidateNodes.length || !targets.length) {
|
|
727
|
+
return [];
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const usedTargetIds = new Set<string>();
|
|
731
|
+
const attachmentEdges: LayoutDefenseEdge[] = [];
|
|
732
|
+
|
|
733
|
+
for (const candidate of candidateNodes) {
|
|
734
|
+
const targetPool = targets.filter((target) => !edgeIntersectsObstacles(candidate.x, candidate.y, target.x, target.y, obstacles));
|
|
735
|
+
if (!targetPool.length) continue;
|
|
736
|
+
const preferredTargets = targetPool.filter((target) => !usedTargetIds.has(target.id));
|
|
737
|
+
const shouldIgnoreDedupe = candidate.id === "left_a0" || candidate.id === "left_b0" || candidate.id === "left_c0";
|
|
738
|
+
const availableTargets = shouldIgnoreDedupe || !preferredTargets.length ? targetPool : preferredTargets;
|
|
739
|
+
const verticalBias = candidate.y <= coreCenter.y - (16 * scale)
|
|
740
|
+
? ["halo_n", "halo_nw", "halo_w"]
|
|
741
|
+
: candidate.y >= coreCenter.y + (16 * scale)
|
|
742
|
+
? ["halo_w", "halo_nw", "halo_n"]
|
|
743
|
+
: ["halo_nw", "halo_w", "halo_n"];
|
|
744
|
+
|
|
745
|
+
const bestTarget = availableTargets
|
|
746
|
+
.slice()
|
|
747
|
+
.sort((left, right) => {
|
|
748
|
+
const leftDistance = Math.hypot(candidate.x - left.x, candidate.y - left.y);
|
|
749
|
+
const rightDistance = Math.hypot(candidate.x - right.x, candidate.y - right.y);
|
|
750
|
+
if (Math.abs(leftDistance - rightDistance) > 0.001) {
|
|
751
|
+
return leftDistance - rightDistance;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const leftPreference = verticalBias.indexOf(left.id);
|
|
755
|
+
const rightPreference = verticalBias.indexOf(right.id);
|
|
756
|
+
if (leftPreference !== rightPreference) {
|
|
757
|
+
return leftPreference - rightPreference;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return right.x - left.x;
|
|
761
|
+
})[0];
|
|
762
|
+
|
|
763
|
+
if (!bestTarget) continue;
|
|
764
|
+
|
|
765
|
+
const edgeId = `left_dynamic_attach_${candidate.id}_${bestTarget.id}`;
|
|
766
|
+
if (usedEdgeIds.has(edgeId)) continue;
|
|
767
|
+
attachmentEdges.push(buildEdgeRecord(candidate, bestTarget, edgeId));
|
|
768
|
+
usedEdgeIds.add(edgeId);
|
|
769
|
+
usedTargetIds.add(bestTarget.id);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return attachmentEdges;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export function layoutDefenseGraph(input: LayoutDefenseGraphInput): LayoutDefenseGraphResult {
|
|
776
|
+
const width = Math.max(1, input.width);
|
|
777
|
+
const height = Math.max(1, input.height);
|
|
778
|
+
const padX = input.padX ?? 18;
|
|
779
|
+
const padTop = input.padTop ?? 8;
|
|
780
|
+
const padBottom = input.padBottom ?? 12;
|
|
781
|
+
const obstacles = input.obstacles ?? [];
|
|
782
|
+
const core = templateNodeById(BLOG_DEFENSE_CORE_NODE_ID) ?? BLOG_DEFENSE_NODES[0];
|
|
783
|
+
const baseScale = Math.min(width / BLOG_DEFENSE_VIEWBOX_WIDTH, height / BLOG_DEFENSE_VIEWBOX_HEIGHT);
|
|
784
|
+
const scaleBoost = width >= 720
|
|
785
|
+
? 1.08
|
|
786
|
+
: width >= 520
|
|
787
|
+
? 1.05
|
|
788
|
+
: 1.02;
|
|
789
|
+
const scale = Math.max(0.48, Math.min(1.18, baseScale * scaleBoost));
|
|
790
|
+
const nodeRadius = uniformNodeRadius(scale);
|
|
791
|
+
const anchor = bestCoreAnchor({
|
|
792
|
+
width,
|
|
793
|
+
height,
|
|
794
|
+
padX,
|
|
795
|
+
padTop,
|
|
796
|
+
padBottom,
|
|
797
|
+
scale,
|
|
798
|
+
coreNode: core,
|
|
799
|
+
nodeRadius,
|
|
800
|
+
obstacles,
|
|
801
|
+
});
|
|
802
|
+
const corridorX = bestCorridorX(width, height, padX, padTop, padBottom, scale, core, anchor, obstacles);
|
|
803
|
+
|
|
804
|
+
const positionedNodes = new Map<string, LayoutDefenseNode>();
|
|
805
|
+
for (const node of BLOG_DEFENSE_NODES) {
|
|
806
|
+
const dx = node.x - core.x;
|
|
807
|
+
const dy = node.y - core.y;
|
|
808
|
+
let x = anchor.x + dx * scale;
|
|
809
|
+
let y = anchor.y + dy * scale;
|
|
810
|
+
const radius = nodeRadius;
|
|
811
|
+
|
|
812
|
+
if (node.zone === "right_corridor") {
|
|
813
|
+
x = corridorX;
|
|
814
|
+
y = Math.min(height - padBottom - radius, Math.max(padTop + radius, y));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const inBounds = x > padX + radius
|
|
818
|
+
&& x < width - padX - radius
|
|
819
|
+
&& y > padTop + radius
|
|
820
|
+
&& y < height - padBottom - radius;
|
|
821
|
+
if (!inBounds) continue;
|
|
822
|
+
|
|
823
|
+
positionedNodes.set(node.id, {
|
|
824
|
+
...node,
|
|
825
|
+
x,
|
|
826
|
+
y,
|
|
827
|
+
radius,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const coreNodes = Array.from(positionedNodes.values()).filter((node) => node.zone === "core");
|
|
832
|
+
const leftArmNodes = Array.from(positionedNodes.values()).filter((node) => node.zone === "left_arm");
|
|
833
|
+
const rightArmNodes = Array.from(positionedNodes.values()).filter((node) => node.zone === "right_arm");
|
|
834
|
+
if (coreNodes.length && leftArmNodes.length) {
|
|
835
|
+
layoutLeftArmRows({
|
|
836
|
+
coreNodes,
|
|
837
|
+
leftArmNodes,
|
|
838
|
+
positionedNodes,
|
|
839
|
+
padX,
|
|
840
|
+
padTop,
|
|
841
|
+
scale,
|
|
842
|
+
coreCenter: positionedNodes.get(BLOG_DEFENSE_CORE_NODE_ID),
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
if (coreNodes.length && rightArmNodes.length) {
|
|
846
|
+
layoutRightArmRows({
|
|
847
|
+
coreNodes,
|
|
848
|
+
rightArmNodes,
|
|
849
|
+
positionedNodes,
|
|
850
|
+
padX,
|
|
851
|
+
padTop,
|
|
852
|
+
padBottom,
|
|
853
|
+
height,
|
|
854
|
+
scale,
|
|
855
|
+
coreCenter: positionedNodes.get(BLOG_DEFENSE_CORE_NODE_ID),
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
for (const [nodeId, node] of positionedNodes.entries()) {
|
|
860
|
+
if (nodeIntersectsObstacles(node.x, node.y, node.radius + 2, obstacles)) {
|
|
861
|
+
positionedNodes.delete(nodeId);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const phaseAEdges: LayoutDefenseEdge[] = [];
|
|
866
|
+
const usedEdgeIds = new Set<string>();
|
|
867
|
+
for (const edge of BLOG_DEFENSE_EDGES) {
|
|
868
|
+
if (edge.id.startsWith("left_to_outer_")) continue;
|
|
869
|
+
const from = positionedNodes.get(edge.from);
|
|
870
|
+
const to = positionedNodes.get(edge.to);
|
|
871
|
+
if (!from || !to) continue;
|
|
872
|
+
if (edgeIntersectsObstacles(from.x, from.y, to.x, to.y, obstacles)) continue;
|
|
873
|
+
phaseAEdges.push(buildEdgeRecord(from, to, edge.id));
|
|
874
|
+
usedEdgeIds.add(edge.id);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const coreCenter = positionedNodes.get(BLOG_DEFENSE_CORE_NODE_ID);
|
|
878
|
+
if (coreCenter) {
|
|
879
|
+
phaseAEdges.push(...buildLeftArmAttachmentEdges({
|
|
880
|
+
positionedNodes,
|
|
881
|
+
coreCenter,
|
|
882
|
+
obstacles,
|
|
883
|
+
usedEdgeIds,
|
|
884
|
+
scale,
|
|
885
|
+
}));
|
|
886
|
+
phaseAEdges.push(...buildRightArmAttachmentEdges({
|
|
887
|
+
positionedNodes,
|
|
888
|
+
coreCenter,
|
|
889
|
+
obstacles,
|
|
890
|
+
usedEdgeIds,
|
|
891
|
+
}));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const corridorNodes = Array.from(positionedNodes.values())
|
|
895
|
+
.filter((node) => node.zone === "right_corridor")
|
|
896
|
+
.sort((a, b) => a.y - b.y);
|
|
897
|
+
|
|
898
|
+
const phaseBEdges = [...phaseAEdges];
|
|
899
|
+
for (let index = 0; index < corridorNodes.length - 1; index += 1) {
|
|
900
|
+
const from = corridorNodes[index];
|
|
901
|
+
const to = corridorNodes[index + 1];
|
|
902
|
+
if (edgeIntersectsObstacles(from.x, from.y, to.x, to.y, obstacles)) continue;
|
|
903
|
+
const templateEdge = BLOG_DEFENSE_EDGES.find((edge) => (
|
|
904
|
+
(edge.from === from.id && edge.to === to.id) || (edge.from === to.id && edge.to === from.id)
|
|
905
|
+
));
|
|
906
|
+
const id = templateEdge ? templateEdge.id : `corridor_relink_${from.id}_${to.id}`;
|
|
907
|
+
if (usedEdgeIds.has(id)) continue;
|
|
908
|
+
phaseBEdges.push(buildEdgeRecord(from, to, id));
|
|
909
|
+
usedEdgeIds.add(id);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const corridorCoreTargets = corridorNodes
|
|
913
|
+
.slice()
|
|
914
|
+
.sort((a, b) => Math.abs(a.y - anchor.y) - Math.abs(b.y - anchor.y))
|
|
915
|
+
.slice(0, 2);
|
|
916
|
+
const rightArmCandidates = Array.from(positionedNodes.values())
|
|
917
|
+
.filter((node) => node.zone === "right_arm")
|
|
918
|
+
.sort((a, b) => Math.abs(a.y - anchor.y) - Math.abs(b.y - anchor.y));
|
|
919
|
+
for (let index = 0; index < Math.min(corridorCoreTargets.length, rightArmCandidates.length); index += 1) {
|
|
920
|
+
const from = corridorCoreTargets[index];
|
|
921
|
+
const to = rightArmCandidates[index];
|
|
922
|
+
if (edgeIntersectsObstacles(from.x, from.y, to.x, to.y, obstacles)) continue;
|
|
923
|
+
const id = `corridor_bridge_${from.id}_${to.id}`;
|
|
924
|
+
if (usedEdgeIds.has(id)) continue;
|
|
925
|
+
phaseBEdges.push(buildEdgeRecord(from, to, id));
|
|
926
|
+
usedEdgeIds.add(id);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return {
|
|
930
|
+
nodes: Array.from(positionedNodes.values()),
|
|
931
|
+
edges: phaseBEdges,
|
|
932
|
+
scale,
|
|
933
|
+
};
|
|
934
|
+
}
|