@world-engines/authoring-ui 0.1.0-alpha.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 +46 -0
- package/dist/DesktopAuthoringShell.d.ts +178 -0
- package/dist/DesktopAuthoringShell.js +235 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +12 -0
- package/dist/ladybug/BrowserGraphCanvasAdapter.d.ts +12 -0
- package/dist/ladybug/BrowserGraphCanvasAdapter.js +8 -0
- package/dist/ladybug/GraphCanvas.d.ts +121 -0
- package/dist/ladybug/GraphCanvas.js +1439 -0
- package/dist/ladybug/GraphEdge.d.ts +26 -0
- package/dist/ladybug/GraphEdge.js +41 -0
- package/dist/ladybug/LadybugExplorer.d.ts +58 -0
- package/dist/ladybug/LadybugExplorer.js +83 -0
- package/dist/ladybug/LadybugSurfaces.d.ts +25 -0
- package/dist/ladybug/LadybugSurfaces.js +9 -0
- package/dist/ladybug/LadybugTable.d.ts +15 -0
- package/dist/ladybug/LadybugTable.js +26 -0
- package/dist/ladybug/entity-props.d.ts +98 -0
- package/dist/ladybug/entity-props.js +363 -0
- package/dist/ladybug/graph-edge-geometry.d.ts +19 -0
- package/dist/ladybug/graph-edge-geometry.js +78 -0
- package/dist/ladybug/graph-edge-mask.d.ts +21 -0
- package/dist/ladybug/graph-edge-mask.js +52 -0
- package/dist/ladybug/graph-groups.d.ts +47 -0
- package/dist/ladybug/graph-groups.js +245 -0
- package/dist/ladybug/graph-layout-contract.d.ts +34 -0
- package/dist/ladybug/graph-layout-contract.js +55 -0
- package/dist/ladybug/graph-layout-solver.d.ts +16 -0
- package/dist/ladybug/graph-layout-solver.js +96 -0
- package/dist/ladybug/graph-layout-wasm.d.ts +9 -0
- package/dist/ladybug/graph-layout-wasm.js +88 -0
- package/dist/ladybug/graph-layout-worker-protocol.d.ts +18 -0
- package/dist/ladybug/graph-layout-worker-protocol.js +1 -0
- package/dist/ladybug/graph-layout-worker.d.ts +1 -0
- package/dist/ladybug/graph-layout-worker.js +66 -0
- package/dist/ladybug/graph-layout.d.ts +2 -0
- package/dist/ladybug/graph-layout.js +1 -0
- package/dist/ladybug/graph-layout.wasm +0 -0
- package/dist/ladybug/graph-node-metrics.d.ts +33 -0
- package/dist/ladybug/graph-node-metrics.js +173 -0
- package/dist/ladybug/graph-physics.d.ts +49 -0
- package/dist/ladybug/graph-physics.js +266 -0
- package/dist/ladybug/graph-placement.d.ts +33 -0
- package/dist/ladybug/graph-placement.js +253 -0
- package/dist/ladybug/graph.css +530 -0
- package/dist/ladybug/graph.d.ts +18 -0
- package/dist/ladybug/graph.js +9 -0
- package/dist/ladybug/score-tags.d.ts +11 -0
- package/dist/ladybug/score-tags.js +1 -0
- package/dist/map/MapWorkspace.d.ts +14 -0
- package/dist/map/MapWorkspace.js +9 -0
- package/dist/score/ScoreRadar.d.ts +10 -0
- package/dist/score/ScoreRadar.js +66 -0
- package/dist/score/index.d.ts +3 -0
- package/dist/score/index.js +3 -0
- package/dist/score/radar-index.d.ts +3 -0
- package/dist/score/radar-index.js +2 -0
- package/dist/score/scenario-scoring-features.d.ts +90 -0
- package/dist/score/scenario-scoring-features.js +380 -0
- package/dist/score/scenario-structural-score.d.ts +76 -0
- package/dist/score/scenario-structural-score.js +211 -0
- package/dist/score/score-presentation.d.ts +51 -0
- package/dist/score/score-presentation.js +44 -0
- package/dist/score/score-radar.css +1 -0
- package/dist/sync/DownloadPanel.d.ts +42 -0
- package/dist/sync/DownloadPanel.js +22 -0
- package/dist/sync/SyncSidebar.d.ts +47 -0
- package/dist/sync/SyncSidebar.js +31 -0
- package/dist/sync/UploadPanel.d.ts +64 -0
- package/dist/sync/UploadPanel.js +36 -0
- package/dist/trigger/TriggerBlueprint.d.ts +37 -0
- package/dist/trigger/TriggerBlueprint.js +26 -0
- package/dist/trigger/tmw/TmwBlueprint.d.ts +71 -0
- package/dist/trigger/tmw/TmwBlueprint.js +206 -0
- package/dist/trigger/tmw/TmwFlowLane.d.ts +22 -0
- package/dist/trigger/tmw/TmwFlowLane.js +44 -0
- package/dist/trigger/tmw/TmwSourceLane.d.ts +17 -0
- package/dist/trigger/tmw/TmwSourceLane.js +38 -0
- package/dist/trigger/tmw/TmwTabs.d.ts +20 -0
- package/dist/trigger/tmw/TmwTabs.js +8 -0
- package/dist/trigger/tmw/TmwTraceLane.d.ts +9 -0
- package/dist/trigger/tmw/TmwTraceLane.js +6 -0
- package/dist/trigger/tmw/TriggerDebugTab.d.ts +9 -0
- package/dist/trigger/tmw/TriggerDebugTab.js +25 -0
- package/dist/trigger/tmw/translate.d.ts +3 -0
- package/dist/trigger/tmw/translate.js +2 -0
- package/dist/view/ViewWorkspace.d.ts +19 -0
- package/dist/view/ViewWorkspace.js +8 -0
- package/package.json +89 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { SERIALIZED_PROP_ENVELOPE_MARKER, isSerializedPropEnvelope } from "@world-engines/protocol-ts/entity-prop-envelope";
|
|
2
|
+
const isPlainObject = (input) => input !== null && typeof input === "object" && !Array.isArray(input);
|
|
3
|
+
export const isPropScalar = (v) => typeof v === "string";
|
|
4
|
+
export const isEntityReferenceValue = (value) => {
|
|
5
|
+
if (!isPlainObject(value) || !isPlainObject(value.$ref))
|
|
6
|
+
return false;
|
|
7
|
+
const ref = value.$ref;
|
|
8
|
+
if (ref.kind !== "node" && ref.kind !== "edge" && ref.kind !== "attr")
|
|
9
|
+
return false;
|
|
10
|
+
if (typeof ref.id !== "string" || ref.id.length === 0)
|
|
11
|
+
return false;
|
|
12
|
+
if (ref.kind === "attr") {
|
|
13
|
+
return typeof ref.attr_key === "string" && ref.attr_key.length > 0;
|
|
14
|
+
}
|
|
15
|
+
return ref.attr_key === undefined;
|
|
16
|
+
};
|
|
17
|
+
export const isScenarioAssetReferenceValue = (value) => isPlainObject(value) &&
|
|
18
|
+
Object.keys(value).length === 1 &&
|
|
19
|
+
typeof value.$asset === "string" &&
|
|
20
|
+
/^asset:[0-9a-f]{32}$/.test(value.$asset);
|
|
21
|
+
export const isPropArray = (v) => Array.isArray(v);
|
|
22
|
+
export const isPropReference = (v) => isEntityReferenceValue(v);
|
|
23
|
+
export const isPropRichText = (v) => isPlainObject(v) && Array.isArray(v.$rich) && v.$rich.every((segment) => (isPlainObject(segment) && typeof segment.text === "string") || isEntityReferenceValue(segment));
|
|
24
|
+
export const isPropTree = (v) => !isPropScalar(v) &&
|
|
25
|
+
!isPropArray(v) &&
|
|
26
|
+
!isPropReference(v) &&
|
|
27
|
+
!isScenarioAssetReferenceValue(v) &&
|
|
28
|
+
!isPropRichText(v);
|
|
29
|
+
export const propValueToText = (v) => {
|
|
30
|
+
if (isPropScalar(v))
|
|
31
|
+
return v;
|
|
32
|
+
if (isPropReference(v)) {
|
|
33
|
+
const suffix = v.$ref.kind === "attr" ? `/${v.$ref.attr_key ?? ""}` : "";
|
|
34
|
+
return `@${v.$ref.kind}:${v.$ref.id}${suffix}`;
|
|
35
|
+
}
|
|
36
|
+
if (isScenarioAssetReferenceValue(v))
|
|
37
|
+
return v.$asset;
|
|
38
|
+
if (isPropRichText(v)) {
|
|
39
|
+
return v.$rich.map((segment) => "text" in segment ? segment.text : propValueToText(segment)).join("");
|
|
40
|
+
}
|
|
41
|
+
if (isPropArray(v)) {
|
|
42
|
+
return v.map((item) => propValueToText(item.value)).join(" 路 ");
|
|
43
|
+
}
|
|
44
|
+
return Object.entries(v)
|
|
45
|
+
.map(([subKey, sub]) => `${subKey}: ${propValueToText(sub.value)}`)
|
|
46
|
+
.join(" · ");
|
|
47
|
+
};
|
|
48
|
+
export const propShape = (v) => isPropRichText(v)
|
|
49
|
+
? "richtext"
|
|
50
|
+
: isPropReference(v)
|
|
51
|
+
? "reference"
|
|
52
|
+
: isPropArray(v)
|
|
53
|
+
? "array"
|
|
54
|
+
: isPropTree(v)
|
|
55
|
+
? "tree"
|
|
56
|
+
: "scalar";
|
|
57
|
+
const stringifyValue = (input) => {
|
|
58
|
+
if (input === null || input === undefined)
|
|
59
|
+
return "";
|
|
60
|
+
if (typeof input === "string")
|
|
61
|
+
return input;
|
|
62
|
+
if (typeof input === "number" || typeof input === "boolean")
|
|
63
|
+
return String(input);
|
|
64
|
+
try {
|
|
65
|
+
return JSON.stringify(input);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const sanitizeTriggerDef = (input) => {
|
|
72
|
+
if (input === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (!isPlainObject(input))
|
|
75
|
+
throw new Error("entity_prop_trigger_invalid");
|
|
76
|
+
if (typeof input.id !== "string" || input.id.length === 0) {
|
|
77
|
+
throw new Error("entity_prop_trigger_invalid");
|
|
78
|
+
}
|
|
79
|
+
if (!isPlainObject(input.tree) && !(typeof input.tree === "object" && input.tree !== null)) {
|
|
80
|
+
throw new Error("entity_prop_trigger_invalid");
|
|
81
|
+
}
|
|
82
|
+
return input;
|
|
83
|
+
};
|
|
84
|
+
const tryParseObject = (raw) => {
|
|
85
|
+
if (raw.length === 0 || raw[0] !== "{")
|
|
86
|
+
return null;
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(raw);
|
|
89
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const parsePropValue = (raw) => {
|
|
96
|
+
if (typeof raw === "string")
|
|
97
|
+
return raw;
|
|
98
|
+
if (Array.isArray(raw))
|
|
99
|
+
return raw.map(wrapToEntityProp);
|
|
100
|
+
if (isScenarioAssetReferenceValue(raw))
|
|
101
|
+
return raw;
|
|
102
|
+
if (isEntityReferenceValue(raw))
|
|
103
|
+
return raw;
|
|
104
|
+
if (isPlainObject(raw) && Array.isArray(raw.$rich)) {
|
|
105
|
+
const segments = [];
|
|
106
|
+
for (const segment of raw.$rich) {
|
|
107
|
+
if (isPlainObject(segment) && typeof segment.text === "string")
|
|
108
|
+
segments.push({ text: segment.text });
|
|
109
|
+
else if (isEntityReferenceValue(segment))
|
|
110
|
+
segments.push(segment);
|
|
111
|
+
}
|
|
112
|
+
return { $rich: segments };
|
|
113
|
+
}
|
|
114
|
+
if (isPlainObject(raw)) {
|
|
115
|
+
const tree = {};
|
|
116
|
+
for (const [subKey, subRaw] of Object.entries(raw)) {
|
|
117
|
+
tree[subKey] = wrapToEntityProp(subRaw);
|
|
118
|
+
}
|
|
119
|
+
return tree;
|
|
120
|
+
}
|
|
121
|
+
return stringifyValue(raw);
|
|
122
|
+
};
|
|
123
|
+
const parseAttrState = (raw) => {
|
|
124
|
+
if (!isPlainObject(raw))
|
|
125
|
+
return null;
|
|
126
|
+
if (typeof raw.id !== "string" || raw.id === "")
|
|
127
|
+
return null;
|
|
128
|
+
const value = parsePropValue(raw.value);
|
|
129
|
+
const out = {
|
|
130
|
+
id: raw.id,
|
|
131
|
+
value
|
|
132
|
+
};
|
|
133
|
+
if (typeof raw.enter_when_trigger_id === "string") {
|
|
134
|
+
out.enter_when_trigger_id = raw.enter_when_trigger_id;
|
|
135
|
+
}
|
|
136
|
+
if (typeof raw.locked_until_trigger_id === "string") {
|
|
137
|
+
out.locked_until_trigger_id = raw.locked_until_trigger_id;
|
|
138
|
+
}
|
|
139
|
+
if (raw.dead === true)
|
|
140
|
+
out.dead = true;
|
|
141
|
+
if (raw.disabled === true)
|
|
142
|
+
out.disabled = true;
|
|
143
|
+
return out;
|
|
144
|
+
};
|
|
145
|
+
const parseAttrStates = (raw) => {
|
|
146
|
+
if (!Array.isArray(raw))
|
|
147
|
+
return undefined;
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const item of raw) {
|
|
150
|
+
const parsed = parseAttrState(item);
|
|
151
|
+
if (parsed)
|
|
152
|
+
out.push(parsed);
|
|
153
|
+
}
|
|
154
|
+
return out.length > 0 ? out : undefined;
|
|
155
|
+
};
|
|
156
|
+
const parseSourceReferences = (raw) => {
|
|
157
|
+
if (raw === undefined)
|
|
158
|
+
return undefined;
|
|
159
|
+
if (!Array.isArray(raw))
|
|
160
|
+
throw new Error("entity_prop_source_refs_invalid");
|
|
161
|
+
const refs = [];
|
|
162
|
+
for (const item of raw) {
|
|
163
|
+
if (!isPlainObject(item))
|
|
164
|
+
throw new Error("entity_prop_source_ref_invalid");
|
|
165
|
+
if (typeof item.document_entity_id !== "string" ||
|
|
166
|
+
typeof item.chunk_entity_id !== "string" ||
|
|
167
|
+
typeof item.chunk_sha256 !== "string" ||
|
|
168
|
+
!Number.isInteger(item.source_start) ||
|
|
169
|
+
!Number.isInteger(item.source_end) ||
|
|
170
|
+
item.source_start < 0 ||
|
|
171
|
+
item.source_end < item.source_start)
|
|
172
|
+
throw new Error("entity_prop_source_ref_invalid");
|
|
173
|
+
refs.push({
|
|
174
|
+
document_entity_id: item.document_entity_id,
|
|
175
|
+
chunk_entity_id: item.chunk_entity_id,
|
|
176
|
+
chunk_sha256: item.chunk_sha256,
|
|
177
|
+
source_start: item.source_start,
|
|
178
|
+
source_end: item.source_end
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return refs.length > 0 ? refs : undefined;
|
|
182
|
+
};
|
|
183
|
+
const buildProp = (value, trigger, states, title, initialAvailable, sourceRefs, viewAccess) => {
|
|
184
|
+
const sanitized = sanitizeTriggerDef(trigger);
|
|
185
|
+
const parsedStates = parseAttrStates(states);
|
|
186
|
+
const out = { value };
|
|
187
|
+
if (sanitized)
|
|
188
|
+
out.trigger = sanitized;
|
|
189
|
+
if (parsedStates)
|
|
190
|
+
out.states = parsedStates;
|
|
191
|
+
if (typeof title === "string")
|
|
192
|
+
out.title = title;
|
|
193
|
+
if (typeof initialAvailable === "boolean") {
|
|
194
|
+
out.initial_available = initialAvailable;
|
|
195
|
+
}
|
|
196
|
+
const parsedSourceRefs = parseSourceReferences(sourceRefs);
|
|
197
|
+
if (parsedSourceRefs !== undefined)
|
|
198
|
+
out.source_refs = parsedSourceRefs;
|
|
199
|
+
if (viewAccess === "inherit" ||
|
|
200
|
+
viewAccess === "deny" ||
|
|
201
|
+
viewAccess === "read" ||
|
|
202
|
+
viewAccess === "read_write") {
|
|
203
|
+
out.view_access = viewAccess;
|
|
204
|
+
}
|
|
205
|
+
else if (viewAccess !== undefined) {
|
|
206
|
+
throw new Error("entity_prop_view_access_invalid");
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
};
|
|
210
|
+
const isPropEnvelope = (input) => isSerializedPropEnvelope(input);
|
|
211
|
+
const wrapToEntityProp = (raw) => {
|
|
212
|
+
if (typeof raw === "string") {
|
|
213
|
+
const parsed = tryParseObject(raw);
|
|
214
|
+
if (parsed && isPropEnvelope(parsed)) {
|
|
215
|
+
return buildProp(parsePropValue(parsed.value), parsed.trigger, parsed.states, parsed.title, parsed.initial_available, parsed.source_refs, parsed.view_access);
|
|
216
|
+
}
|
|
217
|
+
return { value: raw };
|
|
218
|
+
}
|
|
219
|
+
if (isPlainObject(raw) && isPropEnvelope(raw)) {
|
|
220
|
+
return buildProp(parsePropValue(raw.value), raw.trigger, raw.states, raw.title, raw.initial_available, raw.source_refs, raw.view_access);
|
|
221
|
+
}
|
|
222
|
+
return { value: parsePropValue(raw) };
|
|
223
|
+
};
|
|
224
|
+
export const normalizeEntityProps = (raw) => {
|
|
225
|
+
if (!isPlainObject(raw))
|
|
226
|
+
return {};
|
|
227
|
+
const out = {};
|
|
228
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
229
|
+
out[k] = wrapToEntityProp(v);
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
};
|
|
233
|
+
const valueToTree = (v, defaultKey) => {
|
|
234
|
+
if (isPropTree(v))
|
|
235
|
+
return v;
|
|
236
|
+
const text = propValueToText(v);
|
|
237
|
+
return text.length > 0 ? { [defaultKey]: { value: v } } : {};
|
|
238
|
+
};
|
|
239
|
+
export const convertPropShape = (v, target, defaultKey) => {
|
|
240
|
+
if (propShape(v) === target)
|
|
241
|
+
return v;
|
|
242
|
+
if (target === "scalar")
|
|
243
|
+
return propValueToText(v);
|
|
244
|
+
return valueToTree(v, defaultKey);
|
|
245
|
+
};
|
|
246
|
+
export const emptyPropValue = (shape) => shape === "tree" ? {} : shape === "array" ? [] : "";
|
|
247
|
+
const serializePropValue = (value) => {
|
|
248
|
+
if (isPropScalar(value))
|
|
249
|
+
return value;
|
|
250
|
+
if (isScenarioAssetReferenceValue(value))
|
|
251
|
+
return value;
|
|
252
|
+
if (isPropReference(value))
|
|
253
|
+
return value;
|
|
254
|
+
if (isPropRichText(value))
|
|
255
|
+
return value;
|
|
256
|
+
if (isPropArray(value))
|
|
257
|
+
return value.map(serializeProp);
|
|
258
|
+
const out = {};
|
|
259
|
+
for (const [subKey, sub] of Object.entries(value)) {
|
|
260
|
+
out[subKey] = serializeProp(sub);
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
};
|
|
264
|
+
const serializeAttrState = (s) => {
|
|
265
|
+
const out = {
|
|
266
|
+
id: s.id,
|
|
267
|
+
value: serializePropValue(s.value)
|
|
268
|
+
};
|
|
269
|
+
if (s.enter_when_trigger_id !== undefined) {
|
|
270
|
+
out.enter_when_trigger_id = s.enter_when_trigger_id;
|
|
271
|
+
}
|
|
272
|
+
if (s.locked_until_trigger_id !== undefined) {
|
|
273
|
+
out.locked_until_trigger_id = s.locked_until_trigger_id;
|
|
274
|
+
}
|
|
275
|
+
if (s.dead === true)
|
|
276
|
+
out.dead = true;
|
|
277
|
+
if (s.disabled === true)
|
|
278
|
+
out.disabled = true;
|
|
279
|
+
return out;
|
|
280
|
+
};
|
|
281
|
+
const serializeProp = (prop) => {
|
|
282
|
+
const value = serializePropValue(prop.value);
|
|
283
|
+
const hasTrigger = prop.trigger !== undefined;
|
|
284
|
+
const hasStates = prop.states !== undefined && prop.states.length > 0;
|
|
285
|
+
const hasMetadata = prop.title !== undefined
|
|
286
|
+
|| prop.initial_available !== undefined
|
|
287
|
+
|| prop.view_access !== undefined
|
|
288
|
+
|| (prop.source_refs !== undefined && prop.source_refs.length > 0);
|
|
289
|
+
if (!hasTrigger && !hasStates && !hasMetadata)
|
|
290
|
+
return value;
|
|
291
|
+
const out = { [SERIALIZED_PROP_ENVELOPE_MARKER]: true, value };
|
|
292
|
+
if (hasTrigger)
|
|
293
|
+
out.trigger = prop.trigger;
|
|
294
|
+
if (hasStates)
|
|
295
|
+
out.states = prop.states.map(serializeAttrState);
|
|
296
|
+
if (prop.title !== undefined)
|
|
297
|
+
out.title = prop.title;
|
|
298
|
+
if (prop.initial_available !== undefined) {
|
|
299
|
+
out.initial_available = prop.initial_available;
|
|
300
|
+
}
|
|
301
|
+
if (prop.source_refs !== undefined && prop.source_refs.length > 0) {
|
|
302
|
+
out.source_refs = prop.source_refs;
|
|
303
|
+
}
|
|
304
|
+
if (prop.view_access !== undefined)
|
|
305
|
+
out.view_access = prop.view_access;
|
|
306
|
+
return out;
|
|
307
|
+
};
|
|
308
|
+
export const serializeEntityProps = (props) => {
|
|
309
|
+
const out = {};
|
|
310
|
+
for (const [k, p] of Object.entries(props)) {
|
|
311
|
+
out[k] = serializeProp(p);
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
314
|
+
};
|
|
315
|
+
export const toStringPropsLegacy = (props) => {
|
|
316
|
+
const out = {};
|
|
317
|
+
for (const [k, p] of Object.entries(props)) {
|
|
318
|
+
out[k] = propValueToText(p.value);
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
321
|
+
};
|
|
322
|
+
export const propValue = (props, key) => {
|
|
323
|
+
const p = props[key];
|
|
324
|
+
return p ? propValueToText(p.value) : "";
|
|
325
|
+
};
|
|
326
|
+
export const activeAttrState = (prop) => {
|
|
327
|
+
if (!prop.states || prop.states.length === 0)
|
|
328
|
+
return undefined;
|
|
329
|
+
for (let i = prop.states.length - 1; i >= 0; i -= 1) {
|
|
330
|
+
const s = prop.states[i];
|
|
331
|
+
if (!s.dead)
|
|
332
|
+
return s;
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
};
|
|
336
|
+
export const isPropDead = (prop) => {
|
|
337
|
+
if (!prop.states || prop.states.length === 0)
|
|
338
|
+
return false;
|
|
339
|
+
return activeAttrState(prop) === undefined;
|
|
340
|
+
};
|
|
341
|
+
export const effectivePropValue = (prop) => {
|
|
342
|
+
const active = activeAttrState(prop);
|
|
343
|
+
if (prop.states && prop.states.length > 0)
|
|
344
|
+
return active ? active.value : undefined;
|
|
345
|
+
return prop.value;
|
|
346
|
+
};
|
|
347
|
+
export const effectivePropValueText = (prop) => {
|
|
348
|
+
const v = effectivePropValue(prop);
|
|
349
|
+
if (v === undefined)
|
|
350
|
+
return "";
|
|
351
|
+
return propValueToText(v);
|
|
352
|
+
};
|
|
353
|
+
export const isEntityFullyDead = (props) => {
|
|
354
|
+
const keys = Object.keys(props);
|
|
355
|
+
if (keys.length === 0)
|
|
356
|
+
return false;
|
|
357
|
+
return keys.every((k) => {
|
|
358
|
+
const p = props[k];
|
|
359
|
+
if (!p)
|
|
360
|
+
return true;
|
|
361
|
+
return isPropDead(p);
|
|
362
|
+
});
|
|
363
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface GraphEdgeGeometryInput {
|
|
2
|
+
readonly sourceX: number;
|
|
3
|
+
readonly sourceY: number;
|
|
4
|
+
readonly targetX: number;
|
|
5
|
+
readonly targetY: number;
|
|
6
|
+
readonly sourceRadius?: number;
|
|
7
|
+
readonly targetRadius?: number;
|
|
8
|
+
readonly fallbackRadius?: number;
|
|
9
|
+
readonly parallelIndex?: number;
|
|
10
|
+
readonly parallelCount?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface GraphEdgeGeometry {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly labelX: number;
|
|
15
|
+
readonly labelY: number;
|
|
16
|
+
readonly selfLoop: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** 将 ReactFlow 的圆心 Handle 裁到可变半径圆边界,并稳定分离平行边。 */
|
|
19
|
+
export declare function computeGraphEdgeGeometry(input: GraphEdgeGeometryInput): GraphEdgeGeometry;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { NODE_RADIUS } from "./graph-placement.js";
|
|
2
|
+
const PARALLEL_LANE_GAP = 18;
|
|
3
|
+
const SELF_LOOP_GAP = 16;
|
|
4
|
+
const EPSILON = 0.001;
|
|
5
|
+
const finiteRadius = (value, fallback) => value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
6
|
+
const numberToken = (value) => {
|
|
7
|
+
const rounded = Math.round(value * 1000) / 1000;
|
|
8
|
+
return String(Object.is(rounded, -0) ? 0 : rounded);
|
|
9
|
+
};
|
|
10
|
+
const lane = (index, count) => {
|
|
11
|
+
const safeCount = Number.isInteger(count) && (count ?? 0) > 0 ? count : 1;
|
|
12
|
+
const safeIndex = Number.isInteger(index) && (index ?? -1) >= 0
|
|
13
|
+
? Math.min(index, safeCount - 1)
|
|
14
|
+
: 0;
|
|
15
|
+
return safeIndex - (safeCount - 1) / 2;
|
|
16
|
+
};
|
|
17
|
+
/** 将 ReactFlow 的圆心 Handle 裁到可变半径圆边界,并稳定分离平行边。 */
|
|
18
|
+
export function computeGraphEdgeGeometry(input) {
|
|
19
|
+
const fallback = finiteRadius(input.fallbackRadius, NODE_RADIUS);
|
|
20
|
+
const sourceRadius = finiteRadius(input.sourceRadius, fallback);
|
|
21
|
+
const targetRadius = finiteRadius(input.targetRadius, fallback);
|
|
22
|
+
const dx = input.targetX - input.sourceX;
|
|
23
|
+
const dy = input.targetY - input.sourceY;
|
|
24
|
+
const distance = Math.hypot(dx, dy);
|
|
25
|
+
const laneIndex = lane(input.parallelIndex, input.parallelCount);
|
|
26
|
+
if (distance < EPSILON) {
|
|
27
|
+
const radius = Math.max(sourceRadius, targetRadius);
|
|
28
|
+
const xFactor = Math.SQRT1_2;
|
|
29
|
+
const yFactor = -Math.SQRT1_2;
|
|
30
|
+
const sourceX = input.sourceX - radius * xFactor;
|
|
31
|
+
const sourceY = input.sourceY + radius * yFactor;
|
|
32
|
+
const targetX = input.targetX + radius * xFactor;
|
|
33
|
+
const targetY = input.targetY + radius * yFactor;
|
|
34
|
+
const expansion = radius * 1.8 + SELF_LOOP_GAP * (laneIndex + ((input.parallelCount ?? 1) - 1) / 2);
|
|
35
|
+
const control1X = input.sourceX - expansion;
|
|
36
|
+
const control1Y = input.sourceY - expansion;
|
|
37
|
+
const control2X = input.targetX + expansion;
|
|
38
|
+
const control2Y = input.targetY - expansion;
|
|
39
|
+
return {
|
|
40
|
+
path: `M${numberToken(sourceX)} ${numberToken(sourceY)}C${numberToken(control1X)} ${numberToken(control1Y)} ${numberToken(control2X)} ${numberToken(control2Y)} ${numberToken(targetX)} ${numberToken(targetY)}`,
|
|
41
|
+
labelX: input.sourceX,
|
|
42
|
+
labelY: input.sourceY - expansion,
|
|
43
|
+
selfLoop: true
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const unitX = dx / distance;
|
|
47
|
+
const unitY = dy / distance;
|
|
48
|
+
const canonicalSign = dx > 0 || (Math.abs(dx) < EPSILON && dy >= 0) ? 1 : -1;
|
|
49
|
+
const normalX = -unitY * canonicalSign;
|
|
50
|
+
const normalY = unitX * canonicalSign;
|
|
51
|
+
const offset = laneIndex * PARALLEL_LANE_GAP;
|
|
52
|
+
if (Math.abs(offset) < EPSILON) {
|
|
53
|
+
const sourceX = input.sourceX + unitX * sourceRadius;
|
|
54
|
+
const sourceY = input.sourceY + unitY * sourceRadius;
|
|
55
|
+
const targetX = input.targetX - unitX * targetRadius;
|
|
56
|
+
const targetY = input.targetY - unitY * targetRadius;
|
|
57
|
+
return {
|
|
58
|
+
path: `M${numberToken(sourceX)} ${numberToken(sourceY)}L${numberToken(targetX)} ${numberToken(targetY)}`,
|
|
59
|
+
labelX: (sourceX + targetX) / 2,
|
|
60
|
+
labelY: (sourceY + targetY) / 2,
|
|
61
|
+
selfLoop: false
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const controlX = (input.sourceX + input.targetX) / 2 + normalX * offset * 2;
|
|
65
|
+
const controlY = (input.sourceY + input.targetY) / 2 + normalY * offset * 2;
|
|
66
|
+
const sourceControlDistance = Math.hypot(controlX - input.sourceX, controlY - input.sourceY);
|
|
67
|
+
const targetControlDistance = Math.hypot(controlX - input.targetX, controlY - input.targetY);
|
|
68
|
+
const sourceX = input.sourceX + (controlX - input.sourceX) / sourceControlDistance * sourceRadius;
|
|
69
|
+
const sourceY = input.sourceY + (controlY - input.sourceY) / sourceControlDistance * sourceRadius;
|
|
70
|
+
const targetX = input.targetX + (controlX - input.targetX) / targetControlDistance * targetRadius;
|
|
71
|
+
const targetY = input.targetY + (controlY - input.targetY) / targetControlDistance * targetRadius;
|
|
72
|
+
return {
|
|
73
|
+
path: `M${numberToken(sourceX)} ${numberToken(sourceY)}Q${numberToken(controlX)} ${numberToken(controlY)} ${numberToken(targetX)} ${numberToken(targetY)}`,
|
|
74
|
+
labelX: (sourceX + 2 * controlX + targetX) / 4,
|
|
75
|
+
labelY: (sourceY + 2 * controlY + targetY) / 4,
|
|
76
|
+
selfLoop: false
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const GRAPH_EDGE_OCCLUSION_HALO = 6;
|
|
2
|
+
export interface GraphEdgeOccluder {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly x: number;
|
|
5
|
+
readonly y: number;
|
|
6
|
+
readonly radius: number;
|
|
7
|
+
}
|
|
8
|
+
export interface GraphEdgeOcclusionIndex {
|
|
9
|
+
readonly cellSize: number;
|
|
10
|
+
readonly cells: ReadonlyMap<string, readonly GraphEdgeOccluder[]>;
|
|
11
|
+
}
|
|
12
|
+
export interface GraphEdgeLabelBounds {
|
|
13
|
+
readonly x: number;
|
|
14
|
+
readonly y: number;
|
|
15
|
+
readonly width: number;
|
|
16
|
+
readonly height: number;
|
|
17
|
+
}
|
|
18
|
+
/** CSS 使用 inverse zoom 保持屏幕字号;遮挡检测需将 badge 屏幕尺寸换回 world 单位。 */
|
|
19
|
+
export declare function graphEdgeLabelWorldBounds(label: string, x: number, y: number, zoom: number): GraphEdgeLabelBounds;
|
|
20
|
+
export declare function buildGraphEdgeOcclusionIndex(occluders: readonly GraphEdgeOccluder[], halo?: number): GraphEdgeOcclusionIndex;
|
|
21
|
+
export declare function isGraphEdgeLabelOccluded(index: GraphEdgeOcclusionIndex, bounds: GraphEdgeLabelBounds, halo?: number): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const GRAPH_EDGE_OCCLUSION_HALO = 6;
|
|
2
|
+
const GRAPH_EDGE_LABEL_MIN_SCREEN_SIZE = 18;
|
|
3
|
+
const GRAPH_EDGE_LABEL_DIGIT_SCREEN_WIDTH = 8;
|
|
4
|
+
const GRAPH_EDGE_LABEL_SCREEN_PADDING = 10;
|
|
5
|
+
/** CSS 使用 inverse zoom 保持屏幕字号;遮挡检测需将 badge 屏幕尺寸换回 world 单位。 */
|
|
6
|
+
export function graphEdgeLabelWorldBounds(label, x, y, zoom) {
|
|
7
|
+
const safeZoom = Number.isFinite(zoom) && zoom > 0 ? zoom : 1;
|
|
8
|
+
return {
|
|
9
|
+
x,
|
|
10
|
+
y,
|
|
11
|
+
width: Math.max(GRAPH_EDGE_LABEL_MIN_SCREEN_SIZE, label.length * GRAPH_EDGE_LABEL_DIGIT_SCREEN_WIDTH + GRAPH_EDGE_LABEL_SCREEN_PADDING) / safeZoom,
|
|
12
|
+
height: GRAPH_EDGE_LABEL_MIN_SCREEN_SIZE / safeZoom
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const cellKey = (x, y) => `${x}:${y}`;
|
|
16
|
+
export function buildGraphEdgeOcclusionIndex(occluders, halo = GRAPH_EDGE_OCCLUSION_HALO) {
|
|
17
|
+
const largestRadius = occluders.reduce((largest, occluder) => Math.max(largest, occluder.radius + halo), 1);
|
|
18
|
+
const cellSize = largestRadius * 2;
|
|
19
|
+
const cells = new Map();
|
|
20
|
+
for (const occluder of occluders) {
|
|
21
|
+
const key = cellKey(Math.floor(occluder.x / cellSize), Math.floor(occluder.y / cellSize));
|
|
22
|
+
const cell = cells.get(key);
|
|
23
|
+
if (cell === undefined)
|
|
24
|
+
cells.set(key, [occluder]);
|
|
25
|
+
else
|
|
26
|
+
cell.push(occluder);
|
|
27
|
+
}
|
|
28
|
+
return { cellSize, cells };
|
|
29
|
+
}
|
|
30
|
+
export function isGraphEdgeLabelOccluded(index, bounds, halo = GRAPH_EDGE_OCCLUSION_HALO) {
|
|
31
|
+
const halfWidth = bounds.width / 2;
|
|
32
|
+
const halfHeight = bounds.height / 2;
|
|
33
|
+
const cellX = Math.floor(bounds.x / index.cellSize);
|
|
34
|
+
const cellY = Math.floor(bounds.y / index.cellSize);
|
|
35
|
+
for (let offsetX = -1; offsetX <= 1; offsetX += 1) {
|
|
36
|
+
for (let offsetY = -1; offsetY <= 1; offsetY += 1) {
|
|
37
|
+
const cell = index.cells.get(cellKey(cellX + offsetX, cellY + offsetY));
|
|
38
|
+
if (cell === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
for (const occluder of cell) {
|
|
41
|
+
const nearestX = Math.max(bounds.x - halfWidth, Math.min(occluder.x, bounds.x + halfWidth));
|
|
42
|
+
const nearestY = Math.max(bounds.y - halfHeight, Math.min(occluder.y, bounds.y + halfHeight));
|
|
43
|
+
const dx = nearestX - occluder.x;
|
|
44
|
+
const dy = nearestY - occluder.y;
|
|
45
|
+
const radius = occluder.radius + halo;
|
|
46
|
+
if (dx * dx + dy * dy <= radius * radius)
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface SharedGraphGroup {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly label?: string;
|
|
4
|
+
readonly x: number;
|
|
5
|
+
readonly y: number;
|
|
6
|
+
readonly width: number;
|
|
7
|
+
readonly height: number;
|
|
8
|
+
}
|
|
9
|
+
export interface GraphGroupBounds {
|
|
10
|
+
readonly x: number;
|
|
11
|
+
readonly y: number;
|
|
12
|
+
readonly width: number;
|
|
13
|
+
readonly height: number;
|
|
14
|
+
}
|
|
15
|
+
export declare const GRAPH_GROUP_MIN_SIZE = 96;
|
|
16
|
+
export declare const GRAPH_GROUP_LABEL_MAX_LENGTH = 40;
|
|
17
|
+
/** 不持久化第二份颜色 authority;相同 group id 在 npm/editor 中得到相同实色。 */
|
|
18
|
+
export declare const graphGroupColor: (id: string) => string;
|
|
19
|
+
export declare const normalizeGraphGroupLabel: (value: string) => string | null;
|
|
20
|
+
export interface GraphGroupPoint {
|
|
21
|
+
readonly x: number;
|
|
22
|
+
readonly y: number;
|
|
23
|
+
}
|
|
24
|
+
export interface GraphGroupTranslation {
|
|
25
|
+
readonly bounds: GraphGroupBounds;
|
|
26
|
+
readonly delta: GraphGroupPoint;
|
|
27
|
+
}
|
|
28
|
+
export type GraphGroupResizeSide = "top" | "right" | "bottom" | "left";
|
|
29
|
+
export declare const isGraphGroupBoundsFinite: (bounds: GraphGroupBounds) => boolean;
|
|
30
|
+
export declare const snapGraphGroupCoordinate: (value: number) => number;
|
|
31
|
+
export declare const snapGraphGroupPoint: (point: GraphGroupPoint) => GraphGroupPoint;
|
|
32
|
+
/** 同时吸附两角,避免 position 与 size 使用不同网格 authority。 */
|
|
33
|
+
export declare const snapGraphGroupBounds: (bounds: GraphGroupBounds) => GraphGroupBounds;
|
|
34
|
+
/** 画框可向任意方向进行;返回归一化且已吸附的非负矩形。 */
|
|
35
|
+
export declare const graphGroupBoundsFromPoints: (start: GraphGroupPoint, end: GraphGroupPoint) => GraphGroupBounds;
|
|
36
|
+
export declare const graphGroupsOverlap: (left: GraphGroupBounds, right: GraphGroupBounds) => boolean;
|
|
37
|
+
export declare const isGraphGroupBoundsValid: (candidate: GraphGroupBounds, groups: readonly SharedGraphGroup[], excludeId?: string) => boolean;
|
|
38
|
+
/** 所有矩形必须有效,且任何两个矩形只能分离或边界相贴。 */
|
|
39
|
+
export declare const validateGraphGroups: (groups: readonly SharedGraphGroup[]) => boolean;
|
|
40
|
+
export declare const nodesInsideGraphGroup: (group: GraphGroupBounds, centers: Readonly<Record<string, {
|
|
41
|
+
readonly x: number;
|
|
42
|
+
readonly y: number;
|
|
43
|
+
}>>) => string[];
|
|
44
|
+
export declare const clampGraphGroupTranslation: (source: GraphGroupBounds, requestedDelta: GraphGroupPoint, groups: readonly SharedGraphGroup[], excludeId?: string) => GraphGroupTranslation;
|
|
45
|
+
export declare const resizeGraphGroupSide: (source: GraphGroupBounds, side: GraphGroupResizeSide, requestedCoordinate: number, groups: readonly SharedGraphGroup[], excludeId?: string) => GraphGroupBounds;
|
|
46
|
+
/** 使用 clamp 返回的同一 delta 更新成员,成员之间的相对偏移不会改变。 */
|
|
47
|
+
export declare const translateGraphGroupMembers: (centers: Readonly<Record<string, GraphGroupPoint>>, memberIds: readonly string[], delta: GraphGroupPoint) => Readonly<Record<string, GraphGroupPoint>>;
|