hoi4-agent-tools 3.2.0 → 3.4.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/CHANGELOG.md +19 -0
- package/README.md +4 -0
- package/dist/hoi4_agent_tools/core/analysis-job-operations.js +11 -1
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +18 -9
- package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +3 -0
- package/dist/hoi4_agent_tools/core/operation-tasks.js +7 -0
- package/dist/hoi4_agent_tools/core/scenario-model.js +85 -0
- package/dist/hoi4_agent_tools/event/comparison-selection.js +78 -0
- package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
- package/dist/hoi4_agent_tools/event/operations.js +16 -0
- package/dist/hoi4_agent_tools/event/service.js +21 -2
- package/dist/hoi4_agent_tools/focus/compact.js +241 -27
- package/dist/hoi4_agent_tools/focus/layout.js +119 -8
- package/dist/hoi4_agent_tools/focus/render.js +62 -0
- package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
- package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
- package/dist/hoi4_agent_tools/gui/assets.js +54 -4
- package/dist/hoi4_agent_tools/gui/layout.js +935 -104
- package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
- package/dist/hoi4_agent_tools/gui/scenario-conditions.js +2 -23
- package/dist/hoi4_agent_tools/gui/shader.js +515 -0
- package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
- package/dist/hoi4_agent_tools/gui/studio.js +245 -19
- package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
- package/dist/hoi4_agent_tools/gui/validators.js +38 -12
- package/dist/hoi4_agent_tools/map/operations.js +70 -1
- package/dist/hoi4_agent_tools/map/render.js +70 -1
- package/dist/hoi4_agent_tools/map/service.js +30 -6
- package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +3 -0
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +90 -0
- package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
- package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -0
- package/dist/hoi4_agent_tools/mechanic/assertions.js +112 -0
- package/dist/hoi4_agent_tools/mechanic/interpreter.js +619 -0
- package/dist/hoi4_agent_tools/mechanic/service.js +145 -0
- package/dist/hoi4_agent_tools/mechanic/source.js +89 -0
- package/dist/hoi4_agent_tools/package-check/service.js +231 -0
- package/dist/hoi4_agent_tools/probability/evaluation.js +6 -1
- package/dist/hoi4_agent_tools/probability/operations.js +8 -0
- package/dist/hoi4_agent_tools/probability/service.js +33 -12
- package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
- package/dist/hoi4_agent_tools/scenario-suite/service.js +341 -0
- package/dist/hoi4_agent_tools/schemas/event.js +8 -0
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
- package/dist/hoi4_agent_tools/schemas/scenarios.js +264 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
- package/dist/hoi4_agent_tools/technology/graph.js +25 -10
- package/dist/hoi4_agent_tools/technology/render.js +286 -31
- package/dist/hoi4_agent_tools/technology/service.js +55 -8
- package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/events.md +3 -1
- package/docs/focus.md +10 -0
- package/docs/gui.md +49 -3
- package/docs/map.md +8 -0
- package/docs/mechanics.md +70 -0
- package/docs/probability.md +3 -1
- package/docs/technology.md +7 -2
- package/package.json +2 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +650 -0
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +164 -0
- package/schemas/package-manifest.schema.json +142 -0
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1004 -0
- package/schemas/scenario-test-input.schema.json +1057 -0
- package/schemas/shared-scenario.schema.json +237 -0
- package/server.json +2 -2
|
@@ -6,6 +6,9 @@ import { GuiAssetCatalog } from './assets.js';
|
|
|
6
6
|
import { GUI_GRAPH_MAX_EDGES, GUI_GRAPH_MAX_ELEMENTS, GUI_GRAPH_MAX_NODES, GUI_SCENE_MAX_DEPTH, GUI_SCENE_MAX_ELEMENTS, GUI_SCENE_MAX_TEXT_CHARACTERS, GUI_SCENE_MAX_TEXT_LAYOUT_OPERATIONS, GUI_SCENE_MAX_WORK, GUI_TEXT_MAX_CHARACTERS, GUI_TEXT_MAX_MISSING_GLYPH_SAMPLES, } from './limits.js';
|
|
7
7
|
import { guiElementAttributeFidelity } from './source-graph.js';
|
|
8
8
|
import { emptyFidelityReport } from './types.js';
|
|
9
|
+
import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
|
|
10
|
+
import { evaluateGuiCondition, guiConditionScenario } from './scenario-conditions.js';
|
|
11
|
+
import { parseClausewitz } from '../core/source/index.js';
|
|
9
12
|
const clickableTypes = /(?:button|checkbox|editbox|scrollbar|progressbar)/iu;
|
|
10
13
|
const numericDynamicPlaceholder = '[X]';
|
|
11
14
|
const textDynamicPlaceholder = '[dynamic_loc]';
|
|
@@ -25,6 +28,8 @@ function scalarString(value) {
|
|
|
25
28
|
return undefined;
|
|
26
29
|
}
|
|
27
30
|
const defaultTextColour = '#f5f2e8';
|
|
31
|
+
// Native text-box default, independently visible in installed inlays without maxWidth.
|
|
32
|
+
const nativeTextBoxWidth = 200;
|
|
28
33
|
const hoi4TextColours = {
|
|
29
34
|
C: '#23ceff',
|
|
30
35
|
L: '#c3b091',
|
|
@@ -117,9 +122,9 @@ function scaledNumber(value, reference, scale) {
|
|
|
117
122
|
function scalarBoolean(value) {
|
|
118
123
|
if (typeof value === 'boolean')
|
|
119
124
|
return value;
|
|
120
|
-
if (value === 'yes' || value === 'true')
|
|
125
|
+
if (value === 'yes' || value === 'true' || value === 1)
|
|
121
126
|
return true;
|
|
122
|
-
if (value === 'no' || value === 'false')
|
|
127
|
+
if (value === 'no' || value === 'false' || value === 0)
|
|
123
128
|
return false;
|
|
124
129
|
return undefined;
|
|
125
130
|
}
|
|
@@ -483,7 +488,7 @@ function colourRunsForLine(catalog, fontName, line, colours, fontSize, work, inl
|
|
|
483
488
|
}
|
|
484
489
|
return runs;
|
|
485
490
|
}
|
|
486
|
-
function frameFor(element, sprite, scenario, rowValues) {
|
|
491
|
+
function frameFor(element, sprite, scenario, rowValues, effectiveState) {
|
|
487
492
|
const frameCount = Math.max(1, sprite.frameCount);
|
|
488
493
|
const selected = scenario.selectedFrames[element.name] ?? scenario.selectedFrames[sprite.name];
|
|
489
494
|
if (selected !== undefined)
|
|
@@ -493,6 +498,11 @@ function frameFor(element, sprite, scenario, rowValues) {
|
|
|
493
498
|
scenario.scriptedGui[`${element.name}.frame`];
|
|
494
499
|
if (typeof scriptedFrame === 'number' && Number.isFinite(scriptedFrame))
|
|
495
500
|
return Math.min(frameCount - 1, Math.max(0, Math.trunc(scriptedFrame > 0 ? scriptedFrame - 1 : scriptedFrame)));
|
|
501
|
+
const state = effectiveState ?? scenario.elementStates[element.name] ?? scenario.state;
|
|
502
|
+
const checked = rowValues?.[`${element.name}.checked`] ?? scenario.values[`${element.name}.checked`];
|
|
503
|
+
if (/checkbox/iu.test(element.elementType) &&
|
|
504
|
+
(typeof checked === 'boolean' || state === 'selected' || state === 'active'))
|
|
505
|
+
return Math.min(frameCount - 1, checked === false ? 0 : 1);
|
|
496
506
|
const explicit = scalarNumber(property(element.attributes, 'frame'), frameCount);
|
|
497
507
|
if (explicit !== undefined)
|
|
498
508
|
return Math.min(frameCount - 1, Math.max(0, Math.trunc(explicit > 0 ? explicit - 1 : explicit)));
|
|
@@ -510,7 +520,10 @@ function frameFor(element, sprite, scenario, rowValues) {
|
|
|
510
520
|
? frameCount - 1
|
|
511
521
|
: Math.min(frameCount - 1, Math.floor(phase * framesPerSecond));
|
|
512
522
|
}
|
|
513
|
-
|
|
523
|
+
// Effects select a shader entry point, not an unrelated atlas frame.
|
|
524
|
+
// Explicit frames and frame animation above remain independent of button state.
|
|
525
|
+
if (sprite.effectFile !== undefined || /checkbox/iu.test(element.elementType))
|
|
526
|
+
return 0;
|
|
514
527
|
const mapped = state === 'hover'
|
|
515
528
|
? 1
|
|
516
529
|
: state === 'selected' || state === 'active'
|
|
@@ -525,7 +538,9 @@ function textIconSpriteCandidates(token) {
|
|
|
525
538
|
return [normalized, `${normalized}_texticon`, `${normalized}_text_icon`];
|
|
526
539
|
}
|
|
527
540
|
function alignment(attributes, buttonText) {
|
|
528
|
-
const explicitFormat = scalarString(property(attributes, 'format'))
|
|
541
|
+
const explicitFormat = scalarString(property(attributes, 'format'))
|
|
542
|
+
?.toLowerCase()
|
|
543
|
+
.replaceAll('centre', 'center');
|
|
529
544
|
const format = explicitFormat ?? '';
|
|
530
545
|
const horizontalAlignment = format.includes('center')
|
|
531
546
|
? 'center'
|
|
@@ -534,13 +549,18 @@ function alignment(attributes, buttonText) {
|
|
|
534
549
|
: buttonText && explicitFormat === undefined
|
|
535
550
|
? 'center'
|
|
536
551
|
: 'left';
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
552
|
+
const vertical = scalarString(property(attributes, 'vertical_alignment'))
|
|
553
|
+
?.toLowerCase()
|
|
554
|
+
.replaceAll('centre', 'center');
|
|
555
|
+
const verticalAlignment = vertical === 'top'
|
|
556
|
+
? 'top'
|
|
557
|
+
: vertical === 'bottom' || format.includes('bottom')
|
|
558
|
+
? 'bottom'
|
|
559
|
+
: vertical === 'center'
|
|
542
560
|
? 'center'
|
|
543
|
-
:
|
|
561
|
+
: buttonText && explicitFormat === undefined
|
|
562
|
+
? 'center'
|
|
563
|
+
: 'top';
|
|
544
564
|
return { horizontalAlignment, verticalAlignment };
|
|
545
565
|
}
|
|
546
566
|
function anchorAxes(value) {
|
|
@@ -570,10 +590,109 @@ function anchoredCoordinate(start, span, offset, anchor) {
|
|
|
570
590
|
? start + span / 2 + offset
|
|
571
591
|
: start + offset;
|
|
572
592
|
}
|
|
593
|
+
function legacyRowSuffix(instanceSuffix) {
|
|
594
|
+
return instanceSuffix.replace(/#list-[^#]+/gu, '');
|
|
595
|
+
}
|
|
596
|
+
function explicitElementVisibility(definition, scenario, rowValues) {
|
|
597
|
+
const scripted = rowValues?.[`${definition.name}.visible`] ??
|
|
598
|
+
scenario.values[`${definition.name}.visible`] ??
|
|
599
|
+
scenario.scriptedGui[`${definition.name}.visible`];
|
|
600
|
+
return (scenario.visibility[definition.name] ??
|
|
601
|
+
scenario.visibility[definition.id] ??
|
|
602
|
+
(definition.sourceOwnerId === undefined
|
|
603
|
+
? undefined
|
|
604
|
+
: scenario.visibility[definition.sourceOwnerId]) ??
|
|
605
|
+
(typeof scripted === 'boolean' ? scripted : undefined));
|
|
606
|
+
}
|
|
607
|
+
function clippedByChain(chain) {
|
|
608
|
+
let result;
|
|
609
|
+
for (const clip of chain)
|
|
610
|
+
result =
|
|
611
|
+
result === undefined
|
|
612
|
+
? clip
|
|
613
|
+
: (rectIntersection(result, clip) ?? { ...clip, width: 0, height: 0 });
|
|
614
|
+
return result;
|
|
615
|
+
}
|
|
616
|
+
function refreshSceneClips(context) {
|
|
617
|
+
for (const element of context.output) {
|
|
618
|
+
const geometry = context.geometry.get(element.id);
|
|
619
|
+
context.work.spend('resolved scroll clipping', geometry.clipChain.length + 1);
|
|
620
|
+
const clip = clippedByChain(geometry.clipChain);
|
|
621
|
+
const intersection = clip === undefined ? element.unclippedRect : rectIntersection(element.unclippedRect, clip);
|
|
622
|
+
if (clip === undefined)
|
|
623
|
+
delete element.clipRect;
|
|
624
|
+
else
|
|
625
|
+
element.clipRect = { ...clip };
|
|
626
|
+
element.rect =
|
|
627
|
+
intersection === undefined
|
|
628
|
+
? { ...element.unclippedRect, width: 0, height: 0 }
|
|
629
|
+
: { ...intersection };
|
|
630
|
+
element.clipped =
|
|
631
|
+
clip !== undefined &&
|
|
632
|
+
(intersection === undefined || !equalRect(element.unclippedRect, intersection));
|
|
633
|
+
element.visible = geometry.appearanceVisible && intersection !== undefined;
|
|
634
|
+
element.visibilityReason = element.visible
|
|
635
|
+
? 'visible'
|
|
636
|
+
: geometry.appearanceVisible
|
|
637
|
+
? 'outside_clip'
|
|
638
|
+
: (element.visibilityReason ?? 'visibility_false');
|
|
639
|
+
if (clickableTypes.test(element.elementType)) {
|
|
640
|
+
const reason = element.visible ? geometry.interactionBlock : 'hidden';
|
|
641
|
+
if (reason === undefined)
|
|
642
|
+
delete element.disabledReason;
|
|
643
|
+
else
|
|
644
|
+
element.disabledReason = reason;
|
|
645
|
+
element.clickable = reason === undefined;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function attachedScrollbars(definition, context) {
|
|
650
|
+
if (!/^(?:containerwindowtype|windowtype)$/iu.test(definition.elementType))
|
|
651
|
+
return [];
|
|
652
|
+
const result = [];
|
|
653
|
+
const add = (candidate, horizontal, reference) => {
|
|
654
|
+
if (candidate?.elementType.toLowerCase() !== 'extendedscrollbartype') {
|
|
655
|
+
context.diagnostics.push(diagnostic('GUI_SCROLLBAR_TEMPLATE_MISSING', 'warning', `Container ${definition.name} cannot resolve extended scrollbar ${reference}.`, definition));
|
|
656
|
+
addFidelity(context.fidelity, 'missing', 'attached_scrollbar', `Extended scrollbar ${reference} is missing.`, definition);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if ((scalarBoolean(property(candidate.attributes, 'horizontal')) ?? false) !== horizontal) {
|
|
660
|
+
context.diagnostics.push(diagnostic('GUI_SCROLLBAR_AXIS_MISMATCH', 'warning', `Scrollbar ${reference} has the wrong axis for container ${definition.name}.`, definition));
|
|
661
|
+
addFidelity(context.fidelity, 'unsupported', 'attached_scrollbar', `Scrollbar ${reference} has the wrong axis.`, definition);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (result.some((entry) => entry.horizontal === horizontal)) {
|
|
665
|
+
context.diagnostics.push(diagnostic('GUI_SCROLLBAR_AXIS_DUPLICATE', 'warning', `Container ${definition.name} assigns more than one ${horizontal ? 'horizontal' : 'vertical'} scrollbar.`, definition));
|
|
666
|
+
addFidelity(context.fidelity, 'unsupported', 'attached_scrollbar', 'Multiple scrollbars target the same container axis.', definition);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
result.push({ definition: candidate, horizontal });
|
|
670
|
+
};
|
|
671
|
+
for (const horizontal of [false, true]) {
|
|
672
|
+
const name = scalarString(property(definition.attributes, horizontal ? 'horizontalScrollbar' : 'verticalScrollbar'));
|
|
673
|
+
if (name !== undefined) {
|
|
674
|
+
const candidate = context.elementsByName.get(name);
|
|
675
|
+
add(candidate?.parentId === undefined ? candidate : undefined, horizontal, name);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
for (const id of definition.childIds) {
|
|
679
|
+
const child = context.elementsById.get(id);
|
|
680
|
+
if (child?.elementType.toLowerCase() === 'extendedscrollbartype')
|
|
681
|
+
add(child, scalarBoolean(property(child.attributes, 'horizontal')) ?? false, child.name);
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
573
685
|
function dynamicCountryFlag(elementName, expression, scenario, rowValues) {
|
|
574
|
-
|
|
686
|
+
const explicitTag = rowValues?.[`${elementName}.countryTag`] ??
|
|
687
|
+
scenario.values[`${elementName}.countryTag`] ??
|
|
688
|
+
scenario.scriptedGui[`${elementName}.countryTag`];
|
|
689
|
+
// Native engine-owned flag slots have no scripted GetFlag property. Require an
|
|
690
|
+
// explicit per-element binding; a generic country context must not replace every icon.
|
|
691
|
+
if (typeof expression !== 'string' && explicitTag === undefined)
|
|
575
692
|
return undefined;
|
|
576
|
-
const match =
|
|
693
|
+
const match = typeof expression === 'string'
|
|
694
|
+
? /^\[\?([^\]|]+)(?:\|[^\]]+)?\]$|^\[([^\]]+)\]$/u.exec(expression)
|
|
695
|
+
: null;
|
|
577
696
|
const token = match?.[1] ?? match?.[2];
|
|
578
697
|
const shortToken = token?.slice(token.lastIndexOf('.') + 1);
|
|
579
698
|
const tagCandidates = [
|
|
@@ -590,7 +709,9 @@ function dynamicCountryFlag(elementName, expression, scenario, rowValues) {
|
|
|
590
709
|
scenario.country?.GetTag,
|
|
591
710
|
scenario.country?.tag,
|
|
592
711
|
];
|
|
593
|
-
const rawTag =
|
|
712
|
+
const rawTag = explicitTag === undefined
|
|
713
|
+
? tagCandidates.find((candidate) => typeof candidate === 'string' && /^[A-Za-z0-9_]{2,64}$/u.test(candidate))
|
|
714
|
+
: String(explicitTag);
|
|
594
715
|
if (rawTag === undefined)
|
|
595
716
|
return undefined;
|
|
596
717
|
const ideologyCandidates = [
|
|
@@ -652,10 +773,403 @@ class GuiSceneWorkBudget {
|
|
|
652
773
|
this.textLayoutOperations += amount;
|
|
653
774
|
}
|
|
654
775
|
}
|
|
655
|
-
|
|
776
|
+
/** Native scrollbar children are role templates; their authored coordinates are not layout. */
|
|
777
|
+
async function extendedScrollbarChildren(definition, children, rect, scale, context) {
|
|
778
|
+
const horizontal = scalarBoolean(property(definition.attributes, 'horizontal')) ?? false;
|
|
779
|
+
const roles = new Map();
|
|
780
|
+
for (const role of ['track', 'slider', 'decreaseButton', 'increaseButton']) {
|
|
781
|
+
const value = property(definition.attributes, role);
|
|
782
|
+
if (value === undefined)
|
|
783
|
+
continue;
|
|
784
|
+
const attributes = objectProperty(value);
|
|
785
|
+
if (attributes === undefined) {
|
|
786
|
+
addFidelity(context.fidelity, 'unsupported', 'extended_scrollbar_role', `${role} must be a single inline role definition.`, definition);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const child = {
|
|
790
|
+
...definition,
|
|
791
|
+
id: `${definition.id}:role-${role}`,
|
|
792
|
+
sourceOwnerId: definition.sourceOwnerId ?? definition.id,
|
|
793
|
+
parentId: definition.id,
|
|
794
|
+
name: scalarString(property(attributes, 'name')) ?? `${definition.name}_${role}`,
|
|
795
|
+
elementType: role === 'track' ? 'iconType' : 'buttonType',
|
|
796
|
+
attributes,
|
|
797
|
+
childIds: [],
|
|
798
|
+
unsupportedAttributes: Object.keys(attributes).filter((key) => guiElementAttributeFidelity(key, 'buttonType') === 'unsupported'),
|
|
799
|
+
};
|
|
800
|
+
const spriteName = scalarString(property(attributes, 'spriteType', 'quadTextureSprite'));
|
|
801
|
+
const sprite = spriteName === undefined ? undefined : context.spritesByName.get(spriteName.toLowerCase());
|
|
802
|
+
const frame = sprite === undefined ? undefined : await context.catalog.loadSpriteFrame(sprite, 0);
|
|
803
|
+
const size = objectProperty(property(attributes, 'size'));
|
|
804
|
+
const position = objectProperty(property(attributes, 'position'));
|
|
805
|
+
const width = scaledNumber(size === undefined ? undefined : property(size, 'width', 'x'), rect.width, scale) ?? (frame?.width ?? 0) * scale;
|
|
806
|
+
const height = scaledNumber(size === undefined ? undefined : property(size, 'height', 'y'), rect.height, scale) ?? (frame?.height ?? 0) * scale;
|
|
807
|
+
roles.set(role, {
|
|
808
|
+
definition: child,
|
|
809
|
+
width,
|
|
810
|
+
height,
|
|
811
|
+
x: scaledNumber(position === undefined ? undefined : property(position, 'x'), rect.width, scale) ?? (role === 'increaseButton' ? -width : 0),
|
|
812
|
+
y: scaledNumber(position === undefined ? undefined : property(position, 'y'), rect.height, scale) ?? (role === 'increaseButton' ? -height : 0),
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
const length = horizontal ? rect.width : rect.height;
|
|
816
|
+
const decrease = roles.get('decreaseButton');
|
|
817
|
+
const increase = roles.get('increaseButton');
|
|
818
|
+
const start = Math.max(0, Math.min(length, decrease === undefined
|
|
819
|
+
? 0
|
|
820
|
+
: horizontal
|
|
821
|
+
? decrease.x + decrease.width
|
|
822
|
+
: decrease.y + decrease.height));
|
|
823
|
+
const end = Math.max(start, Math.min(length, increase === undefined ? length : length + (horizontal ? increase.x : increase.y)));
|
|
824
|
+
const minimum = scalarNumber(property(definition.attributes, 'minValue'), 1) ?? 0;
|
|
825
|
+
const maximum = scalarNumber(property(definition.attributes, 'maxValue'), 1) ?? 1;
|
|
826
|
+
const value = scalarNumber(property(definition.attributes, 'startValue'), 1) ?? minimum;
|
|
827
|
+
const ratio = maximum > minimum ? Math.max(0, Math.min(1, (value - minimum) / (maximum - minimum))) : 0;
|
|
828
|
+
const track = roles.get('track');
|
|
829
|
+
const trackAxis = start + (track === undefined ? 0 : horizontal ? track.x : track.y);
|
|
830
|
+
const trackLength = Math.max(0, end - trackAxis);
|
|
831
|
+
const result = [];
|
|
832
|
+
for (const [role, part] of roles) {
|
|
833
|
+
let x = part.x;
|
|
834
|
+
let y = part.y;
|
|
835
|
+
let width = part.width;
|
|
836
|
+
let height = part.height;
|
|
837
|
+
if (role === 'increaseButton') {
|
|
838
|
+
x += rect.width;
|
|
839
|
+
y += rect.height;
|
|
840
|
+
}
|
|
841
|
+
else if (role === 'track') {
|
|
842
|
+
if (horizontal) {
|
|
843
|
+
x = trackAxis;
|
|
844
|
+
width = trackLength;
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
y = trackAxis;
|
|
848
|
+
height = trackLength;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
else if (role === 'slider') {
|
|
852
|
+
const axis = trackAxis + Math.max(0, trackLength - (horizontal ? width : height)) * ratio;
|
|
853
|
+
if (horizontal) {
|
|
854
|
+
x += axis;
|
|
855
|
+
y += track?.y ?? 0;
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
x += track?.x ?? 0;
|
|
859
|
+
y += axis;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
result.push({
|
|
863
|
+
...part.definition,
|
|
864
|
+
attributes: {
|
|
865
|
+
...part.definition.attributes,
|
|
866
|
+
position: { x: x / scale, y: y / scale },
|
|
867
|
+
size: { width: width / scale, height: height / scale },
|
|
868
|
+
orientation: 'upper_left',
|
|
869
|
+
origo: 'upper_left',
|
|
870
|
+
centerposition: false,
|
|
871
|
+
...(maximum <= minimum ? { enabled: false } : {}),
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
addFidelity(context.fidelity, 'modelled', 'native_extended_scrollbar_roles', `Composed inline ${horizontal ? 'horizontal' : 'vertical'} scrollbar artwork with source role offsets and ${ratio} of its value range.`, definition);
|
|
876
|
+
return [...result, ...children];
|
|
877
|
+
}
|
|
878
|
+
async function scrollbarChildren(definition, children, rect, scale, context, rowValues) {
|
|
879
|
+
if (definition.elementType.toLowerCase() === 'extendedscrollbartype')
|
|
880
|
+
return extendedScrollbarChildren(definition, children, rect, scale, context);
|
|
881
|
+
if (definition.elementType.toLowerCase() !== 'scrollbartype')
|
|
882
|
+
return children;
|
|
883
|
+
const horizontal = scalarBoolean(property(definition.attributes, 'horizontal')) ?? false;
|
|
884
|
+
const length = horizontal ? rect.width : rect.height;
|
|
885
|
+
const crossLength = horizontal ? rect.height : rect.width;
|
|
886
|
+
const roles = new Map();
|
|
887
|
+
for (const role of ['track', 'leftbutton', 'rightbutton', 'slider']) {
|
|
888
|
+
const name = scalarString(property(definition.attributes, role));
|
|
889
|
+
if (name === undefined)
|
|
890
|
+
continue;
|
|
891
|
+
const child = children.find((candidate) => candidate.name === name);
|
|
892
|
+
if (child === undefined) {
|
|
893
|
+
context.diagnostics.push(diagnostic('GUI_SCROLLBAR_ROLE_MISSING', 'warning', `Scrollbar ${definition.name} cannot resolve ${role} template ${name}.`, definition));
|
|
894
|
+
addFidelity(context.fidelity, 'missing', 'scrollbar_role', `${role} template ${name} is missing.`, definition);
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
const spriteName = scalarString(property(child.attributes, 'spriteType', 'quadTextureSprite'));
|
|
898
|
+
const sprite = spriteName === undefined ? undefined : context.spritesByName.get(spriteName.toLowerCase());
|
|
899
|
+
const frame = sprite === undefined
|
|
900
|
+
? undefined
|
|
901
|
+
: await context.catalog.loadSpriteFrame(sprite, frameFor(child, sprite, context.scenario, rowValues));
|
|
902
|
+
const size = objectProperty(property(child.attributes, 'size'));
|
|
903
|
+
roles.set(role, {
|
|
904
|
+
definition: child,
|
|
905
|
+
width: scaledNumber(size === undefined ? undefined : property(size, 'width', 'x'), rect.width, scale) ?? (frame?.width ?? 0) * scale,
|
|
906
|
+
height: scaledNumber(size === undefined ? undefined : property(size, 'height', 'y'), rect.height, scale) ?? (frame?.height ?? 0) * scale,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
const border = objectProperty(property(definition.attributes, 'borderSize'));
|
|
910
|
+
const borderValue = scaledNumber(border === undefined
|
|
911
|
+
? undefined
|
|
912
|
+
: property(border, horizontal ? 'x' : 'y', horizontal ? 'width' : 'height'), length, scale);
|
|
913
|
+
const extent = (role) => horizontal ? (roles.get(role)?.width ?? 0) : (roles.get(role)?.height ?? 0);
|
|
914
|
+
const start = Math.max(0, Math.min(length, borderValue ?? extent('leftbutton')));
|
|
915
|
+
const end = Math.max(0, Math.min(length - start, borderValue ?? extent('rightbutton')));
|
|
916
|
+
const trackLength = Math.max(0, length - start - end);
|
|
917
|
+
const minimum = scalarNumber(property(definition.attributes, 'minValue'), 1) ?? 0;
|
|
918
|
+
const maximum = scalarNumber(property(definition.attributes, 'maxValue'), 1) ?? 100;
|
|
919
|
+
const explicitValue = rowValues?.[`${definition.name}.value`] ??
|
|
920
|
+
rowValues?.[definition.name] ??
|
|
921
|
+
context.scenario.values[`${definition.name}.value`] ??
|
|
922
|
+
context.scenario.values[definition.name] ??
|
|
923
|
+
context.scenario.scriptedGui[definition.name];
|
|
924
|
+
const state = context.scenario.elementStates[definition.name] ?? context.scenario.state;
|
|
925
|
+
const value = state === 'minimum-value'
|
|
926
|
+
? minimum
|
|
927
|
+
: state === 'maximum-value'
|
|
928
|
+
? maximum
|
|
929
|
+
: typeof explicitValue === 'number'
|
|
930
|
+
? explicitValue
|
|
931
|
+
: (scalarNumber(property(definition.attributes, 'startValue'), 1) ?? minimum);
|
|
932
|
+
const ratio = maximum > minimum ? Math.max(0, Math.min(1, (value - minimum) / (maximum - minimum))) : 0;
|
|
933
|
+
const result = [];
|
|
934
|
+
const controlledIds = new Set();
|
|
935
|
+
for (const [role, part] of roles) {
|
|
936
|
+
controlledIds.add(part.definition.id);
|
|
937
|
+
const partLength = role === 'track' ? trackLength : Math.min(length, extent(role));
|
|
938
|
+
const crossSize = role === 'track' ? crossLength : horizontal ? part.height : part.width;
|
|
939
|
+
const axis = role === 'track'
|
|
940
|
+
? start
|
|
941
|
+
: role === 'rightbutton'
|
|
942
|
+
? length - partLength
|
|
943
|
+
: role === 'slider'
|
|
944
|
+
? start + Math.max(0, trackLength - partLength) * ratio
|
|
945
|
+
: 0;
|
|
946
|
+
const cross = (crossLength - crossSize) / 2;
|
|
947
|
+
result.push({
|
|
948
|
+
...part.definition,
|
|
949
|
+
attributes: {
|
|
950
|
+
...part.definition.attributes,
|
|
951
|
+
position: {
|
|
952
|
+
x: (horizontal ? axis : cross) / scale,
|
|
953
|
+
y: (horizontal ? cross : axis) / scale,
|
|
954
|
+
},
|
|
955
|
+
size: {
|
|
956
|
+
width: (horizontal ? partLength : crossSize) / scale,
|
|
957
|
+
height: (horizontal ? crossSize : partLength) / scale,
|
|
958
|
+
},
|
|
959
|
+
orientation: 'upper_left',
|
|
960
|
+
origo: 'upper_left',
|
|
961
|
+
centerposition: false,
|
|
962
|
+
...(maximum <= minimum ? { enabled: false } : {}),
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
addFidelity(context.fidelity, 'modelled', 'native_scrollbar_roles', `Composed ${horizontal ? 'horizontal' : 'vertical'} track, end buttons, and thumb at ${ratio} of its value range.`, definition);
|
|
967
|
+
return [...result, ...children.filter(({ id }) => !controlledIds.has(id))];
|
|
968
|
+
}
|
|
969
|
+
async function layoutAttachedScrollbars(definition, scrollbars, owner, childOutput, context, depth, scale, rowIndex, rowValues, instanceSuffix) {
|
|
970
|
+
const geometry = context.geometry.get(owner.id);
|
|
971
|
+
const viewport = geometry.contentRect;
|
|
972
|
+
const attachmentRect = owner.unclippedRect;
|
|
973
|
+
const attachmentWidth = viewport.width > 0 ? attachmentRect.width : 0;
|
|
974
|
+
const attachmentHeight = viewport.height > 0 ? attachmentRect.height : 0;
|
|
975
|
+
const directChildren = childOutput.filter((child) => child.parentId === owner.id);
|
|
976
|
+
const fixedChildren = new Set(directChildren
|
|
977
|
+
.filter((child) => {
|
|
978
|
+
const rect = child.unclippedRect;
|
|
979
|
+
const outer = rectIntersection(rect, owner.unclippedRect);
|
|
980
|
+
return (outer !== undefined &&
|
|
981
|
+
equalRect(rect, outer) &&
|
|
982
|
+
rectIntersection(rect, viewport) === undefined);
|
|
983
|
+
})
|
|
984
|
+
.map(({ id }) => id));
|
|
985
|
+
let right = viewport.x + viewport.width;
|
|
986
|
+
let bottom = viewport.y + viewport.height;
|
|
987
|
+
for (const child of directChildren) {
|
|
988
|
+
const childGeometry = context.geometry.get(child.id);
|
|
989
|
+
if (!childGeometry.appearanceVisible || fixedChildren.has(child.id))
|
|
990
|
+
continue;
|
|
991
|
+
const measured = childGeometry.measuredRect;
|
|
992
|
+
right = Math.max(right, measured.x + measured.width);
|
|
993
|
+
bottom = Math.max(bottom, measured.y + measured.height);
|
|
994
|
+
}
|
|
995
|
+
const maximumX = scrollbars.some(({ horizontal }) => horizontal)
|
|
996
|
+
? Math.max(0, right - viewport.x - viewport.width)
|
|
997
|
+
: 0;
|
|
998
|
+
const maximumY = scrollbars.some(({ horizontal }) => !horizontal)
|
|
999
|
+
? Math.max(0, bottom - viewport.y - viewport.height)
|
|
1000
|
+
: 0;
|
|
1001
|
+
const requestedOffset = (horizontal) => {
|
|
1002
|
+
const requestedState = context.scenario.elementStates[definition.name] ?? context.scenario.state;
|
|
1003
|
+
if (requestedState === 'minimum-value')
|
|
1004
|
+
return 0;
|
|
1005
|
+
if (requestedState === 'maximum-value')
|
|
1006
|
+
return horizontal ? maximumX : maximumY;
|
|
1007
|
+
const key = `${definition.name}.scroll${horizontal ? 'X' : 'Y'}`;
|
|
1008
|
+
const explicit = rowValues?.[key] ?? context.scenario.values[key] ?? context.scenario.scriptedGui[key];
|
|
1009
|
+
if (typeof explicit === 'number' && Number.isFinite(explicit))
|
|
1010
|
+
return Math.max(0, explicit) * scale;
|
|
1011
|
+
return horizontal
|
|
1012
|
+
? 0
|
|
1013
|
+
: (context.scenario.scrollOffsets[`${definition.name}${instanceSuffix}`] ??
|
|
1014
|
+
context.scenario.scrollOffsets[`${definition.name}${legacyRowSuffix(instanceSuffix)}`] ??
|
|
1015
|
+
context.scenario.scrollOffsets[definition.name] ??
|
|
1016
|
+
context.scenario.scrollOffsets[definition.id] ??
|
|
1017
|
+
0) * scale;
|
|
1018
|
+
};
|
|
1019
|
+
const offsetX = Math.min(maximumX, requestedOffset(true));
|
|
1020
|
+
const offsetY = Math.min(maximumY, requestedOffset(false));
|
|
1021
|
+
owner.scroll = {
|
|
1022
|
+
viewport,
|
|
1023
|
+
contentWidth: right - viewport.x,
|
|
1024
|
+
contentHeight: bottom - viewport.y,
|
|
1025
|
+
offsetX,
|
|
1026
|
+
offsetY,
|
|
1027
|
+
maximumX,
|
|
1028
|
+
maximumY,
|
|
1029
|
+
};
|
|
1030
|
+
let fixed = false;
|
|
1031
|
+
for (const child of childOutput) {
|
|
1032
|
+
context.work.spend('container scroll translation');
|
|
1033
|
+
if (child.parentId === owner.id)
|
|
1034
|
+
fixed = fixedChildren.has(child.id);
|
|
1035
|
+
const childGeometry = context.geometry.get(child.id);
|
|
1036
|
+
if (fixed) {
|
|
1037
|
+
// A child wholly in the fixed margin keeps the outer window clip, not the scrolled inner clip.
|
|
1038
|
+
childGeometry.clipChain = childGeometry.clipChain.map((clip) => clip === viewport ? owner.unclippedRect : clip);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
for (const rect of new Set([
|
|
1042
|
+
child.unclippedRect,
|
|
1043
|
+
childGeometry.contentRect,
|
|
1044
|
+
childGeometry.measuredRect,
|
|
1045
|
+
])) {
|
|
1046
|
+
rect.x -= offsetX;
|
|
1047
|
+
rect.y -= offsetY;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
addFidelity(context.fidelity, 'modelled', 'native_container_scrolling', `Resolved content extents ${right - viewport.x} by ${bottom - viewport.y}; offsets ${offsetX},${offsetY} are clamped to ${maximumX},${maximumY}. Nested container bounds, not their overflowing descendants, contribute to the parent range.`, definition);
|
|
1051
|
+
if (property(definition.attributes, 'margin') !== undefined)
|
|
1052
|
+
addFidelity(context.fidelity, 'modelled', 'margin', 'Container margins preserve fixed border content and inset the scrolling clip without adding child-layout padding.', definition);
|
|
1053
|
+
const autohide = scalarBoolean(property(definition.attributes, 'autohide_scrollbars')) ?? true;
|
|
1054
|
+
const barLayouts = [];
|
|
1055
|
+
for (const [index, attached] of scrollbars.entries()) {
|
|
1056
|
+
const maximum = attached.horizontal ? maximumX : maximumY;
|
|
1057
|
+
const offset = attached.horizontal ? offsetX : offsetY;
|
|
1058
|
+
const size = objectProperty(property(attached.definition.attributes, 'size'));
|
|
1059
|
+
const barScale = scale * (scalarNumber(property(attached.definition.attributes, 'scale'), 1) ?? 1);
|
|
1060
|
+
if (!Number.isFinite(barScale) || barScale <= 0) {
|
|
1061
|
+
addFidelity(context.fidelity, 'unsupported', 'attached_scrollbar_scale', 'Attached scrollbar scale must be positive and finite.', attached.definition);
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
const barWidth = attached.horizontal
|
|
1065
|
+
? attachmentWidth
|
|
1066
|
+
: (scaledNumber(size === undefined ? undefined : property(size, 'width', 'x'), attachmentWidth, barScale) ?? 0);
|
|
1067
|
+
const barHeight = attached.horizontal
|
|
1068
|
+
? (scaledNumber(size === undefined ? undefined : property(size, 'height', 'y'), attachmentHeight, barScale) ?? 0)
|
|
1069
|
+
: attachmentHeight;
|
|
1070
|
+
const position = objectProperty(property(attached.definition.attributes, 'position'));
|
|
1071
|
+
const axis = anchorAxes(scalarString(property(attached.definition.attributes, 'orientation'))?.toLowerCase() ??
|
|
1072
|
+
'upper_left');
|
|
1073
|
+
const origin = scalarBoolean(property(attached.definition.attributes, 'centerposition')) === true
|
|
1074
|
+
? { horizontal: 'center', vertical: 'center' }
|
|
1075
|
+
: anchorAxes(scalarString(property(attached.definition.attributes, 'origo'))?.toLowerCase() ??
|
|
1076
|
+
'upper_left');
|
|
1077
|
+
const coordinate = (horizontal) => {
|
|
1078
|
+
const key = `${attached.definition.name}.${horizontal ? 'x' : 'y'}`;
|
|
1079
|
+
const explicit = rowValues?.[key] ?? context.scenario.values[key] ?? context.scenario.scriptedGui[key];
|
|
1080
|
+
const span = horizontal ? attachmentWidth : attachmentHeight;
|
|
1081
|
+
const offset = typeof explicit === 'number' && Number.isFinite(explicit)
|
|
1082
|
+
? explicit * scale
|
|
1083
|
+
: (scaledNumber(position === undefined ? undefined : property(position, horizontal ? 'x' : 'y'), span, scale) ?? 0);
|
|
1084
|
+
const anchor = horizontal ? axis.horizontal : axis.vertical;
|
|
1085
|
+
const origo = horizontal ? origin.horizontal : origin.vertical;
|
|
1086
|
+
const length = horizontal ? barWidth : barHeight;
|
|
1087
|
+
return (anchoredCoordinate(horizontal ? attachmentRect.x : attachmentRect.y, span, offset, anchor) -
|
|
1088
|
+
(origo === 'center' ? length / 2 : origo === 'right' || origo === 'bottom' ? length : 0));
|
|
1089
|
+
};
|
|
1090
|
+
const bar = {
|
|
1091
|
+
...attached.definition,
|
|
1092
|
+
id: `${attached.definition.id}:attached-${definition.id}-${index}`,
|
|
1093
|
+
sourceOwnerId: attached.definition.id,
|
|
1094
|
+
parentId: definition.id,
|
|
1095
|
+
attributes: {
|
|
1096
|
+
...attached.definition.attributes,
|
|
1097
|
+
size: {
|
|
1098
|
+
width: attached.horizontal
|
|
1099
|
+
? attachmentWidth / barScale
|
|
1100
|
+
: ((size === undefined ? undefined : property(size, 'width', 'x')) ?? 0),
|
|
1101
|
+
height: attached.horizontal
|
|
1102
|
+
? ((size === undefined ? undefined : property(size, 'height', 'y')) ?? 0)
|
|
1103
|
+
: attachmentHeight / barScale,
|
|
1104
|
+
},
|
|
1105
|
+
minValue: 0,
|
|
1106
|
+
maxValue: maximum,
|
|
1107
|
+
startValue: offset,
|
|
1108
|
+
...(maximum === 0 ? { enabled: false } : {}),
|
|
1109
|
+
},
|
|
1110
|
+
};
|
|
1111
|
+
const sourceShown = scalarBoolean(property(bar.attributes, 'visible')) ??
|
|
1112
|
+
!(scalarBoolean(property(bar.attributes, 'hide', 'hidden')) ?? false);
|
|
1113
|
+
barLayouts.push({
|
|
1114
|
+
definition: bar,
|
|
1115
|
+
rect: { x: coordinate(true), y: coordinate(false), width: barWidth, height: barHeight },
|
|
1116
|
+
scale: barScale,
|
|
1117
|
+
horizontal: attached.horizontal,
|
|
1118
|
+
shown: explicitElementVisibility(bar, context.scenario, rowValues) ??
|
|
1119
|
+
(sourceShown && (!autohide || maximum > 0)),
|
|
1120
|
+
index,
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
const horizontal = barLayouts.find((bar) => bar.horizontal && bar.shown);
|
|
1124
|
+
const vertical = barLayouts.find((bar) => !bar.horizontal && bar.shown);
|
|
1125
|
+
if (horizontal !== undefined &&
|
|
1126
|
+
vertical !== undefined &&
|
|
1127
|
+
rectIntersection(horizontal.rect, vertical.rect) !== undefined) {
|
|
1128
|
+
const horizontalRect = { ...horizontal.rect };
|
|
1129
|
+
const verticalRect = { ...vertical.rect };
|
|
1130
|
+
// Each visible orthogonal scrollbar reserves its actual anchored gutter, including its inset.
|
|
1131
|
+
if (verticalRect.x + verticalRect.width / 2 >=
|
|
1132
|
+
owner.unclippedRect.x + owner.unclippedRect.width / 2)
|
|
1133
|
+
horizontal.rect.width = Math.max(0, verticalRect.x - horizontalRect.x);
|
|
1134
|
+
else {
|
|
1135
|
+
horizontal.rect.x = Math.max(horizontalRect.x, verticalRect.x + verticalRect.width);
|
|
1136
|
+
horizontal.rect.width = Math.max(0, horizontalRect.x + horizontalRect.width - horizontal.rect.x);
|
|
1137
|
+
}
|
|
1138
|
+
if (horizontalRect.y + horizontalRect.height / 2 >=
|
|
1139
|
+
owner.unclippedRect.y + owner.unclippedRect.height / 2)
|
|
1140
|
+
vertical.rect.height = Math.max(0, horizontalRect.y - verticalRect.y);
|
|
1141
|
+
else {
|
|
1142
|
+
vertical.rect.y = Math.max(verticalRect.y, horizontalRect.y + horizontalRect.height);
|
|
1143
|
+
vertical.rect.height = Math.max(0, verticalRect.y + verticalRect.height - vertical.rect.y);
|
|
1144
|
+
}
|
|
1145
|
+
addFidelity(context.fidelity, 'modelled', 'orthogonal_scrollbar_gutters', 'Visible horizontal and vertical scrollbars reserve their anchored cross-axis gutters instead of drawing their end buttons over the same corner.', definition);
|
|
1146
|
+
}
|
|
1147
|
+
for (const bar of barLayouts) {
|
|
1148
|
+
await layoutElement({
|
|
1149
|
+
...bar.definition,
|
|
1150
|
+
attributes: {
|
|
1151
|
+
...bar.definition.attributes,
|
|
1152
|
+
position: {
|
|
1153
|
+
x: (bar.rect.x - owner.unclippedRect.x) / scale,
|
|
1154
|
+
y: (bar.rect.y - owner.unclippedRect.y) / scale,
|
|
1155
|
+
},
|
|
1156
|
+
size: { width: bar.rect.width / bar.scale, height: bar.rect.height / bar.scale },
|
|
1157
|
+
orientation: 'upper_left',
|
|
1158
|
+
origo: 'upper_left',
|
|
1159
|
+
centerposition: false,
|
|
1160
|
+
},
|
|
1161
|
+
}, owner.unclippedRect, clippedByChain(geometry.clipChain), scale, depth + 1, context, `${instanceSuffix}#scrollbar-${definition.id}-${bar.index}`, rowIndex, {
|
|
1162
|
+
...rowValues,
|
|
1163
|
+
[`${bar.definition.name}.x`]: (bar.rect.x - owner.unclippedRect.x) / scale,
|
|
1164
|
+
[`${bar.definition.name}.y`]: (bar.rect.y - owner.unclippedRect.y) / scale,
|
|
1165
|
+
}, owner.id, 'upper_left', geometry.appearanceVisible && bar.shown, geometry.clipChain);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
async function layoutElement(definition, parentRect, inheritedClip, parentScale, depth, context, instanceSuffix = '', rowIndex, rowValues, parentInstanceId, inheritedOrientation = 'upper_left', inheritedVisible = true, inheritedClipChain) {
|
|
656
1169
|
context.work.admitElement(depth);
|
|
657
1170
|
const { scenario, catalog, fidelity, diagnostics } = context;
|
|
658
1171
|
const instanceId = instanceSuffix.length === 0 ? definition.id : `${definition.id}${instanceSuffix}`;
|
|
1172
|
+
const clipChain = inheritedClipChain ?? (inheritedClip === undefined ? [] : [inheritedClip]);
|
|
659
1173
|
const localScale = scalarNumber(property(definition.attributes, 'scale'), 1) ?? 1;
|
|
660
1174
|
const scale = parentScale * localScale;
|
|
661
1175
|
const position = objectProperty(property(definition.attributes, 'position'));
|
|
@@ -676,16 +1190,44 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
676
1190
|
: undefined) ??
|
|
677
1191
|
scaledNumber(position === undefined ? undefined : property(position, 'y'), parentRect.height, parentScale) ??
|
|
678
1192
|
0;
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1193
|
+
const orientation = scalarString(property(definition.attributes, 'orientation'))?.toLowerCase() ??
|
|
1194
|
+
inheritedOrientation;
|
|
1195
|
+
const origo = scalarString(property(definition.attributes, 'origo'))?.toLowerCase() ?? 'upper_left';
|
|
1196
|
+
const orientationAxes = anchorAxes(orientation);
|
|
1197
|
+
const anchorX = anchoredCoordinate(parentRect.x, parentRect.width, localX, orientationAxes.horizontal);
|
|
1198
|
+
const anchorY = anchoredCoordinate(parentRect.y, parentRect.height, localY, orientationAxes.vertical);
|
|
1199
|
+
const widthValue = size === undefined ? undefined : property(size, 'width', 'x');
|
|
1200
|
+
const heightValue = size === undefined ? undefined : property(size, 'height', 'y');
|
|
1201
|
+
let width = scaledNumber(widthValue, parentRect.width, scale) ?? 0;
|
|
1202
|
+
let height = scaledNumber(heightValue, parentRect.height, scale) ?? 0;
|
|
1203
|
+
// A doubled percent names a far-edge coordinate in the parent, whereas a single
|
|
1204
|
+
// percent is a proportional extent. Resolve the anchor first, including screen-centred roots.
|
|
1205
|
+
if (typeof widthValue === 'string' && widthValue.endsWith('%%'))
|
|
1206
|
+
width = Math.max(0, width - (anchorX - parentRect.x));
|
|
1207
|
+
if (typeof heightValue === 'string' && heightValue.endsWith('%%'))
|
|
1208
|
+
height = Math.max(0, height - (anchorY - parentRect.y));
|
|
1209
|
+
const inferWidth = !(typeof widthValue === 'string' && widthValue.endsWith('%%'));
|
|
1210
|
+
const inferHeight = !(typeof heightValue === 'string' && heightValue.endsWith('%%'));
|
|
1211
|
+
// Native negative extents specify an inset from the parent's far edge.
|
|
1212
|
+
if (width < 0)
|
|
1213
|
+
width = Math.max(0, parentRect.width - localX + width);
|
|
1214
|
+
if (height < 0)
|
|
1215
|
+
height = Math.max(0, parentRect.height - localY + height);
|
|
1216
|
+
const explicitlyVisible = explicitElementVisibility(definition, scenario, rowValues);
|
|
1217
|
+
const declaredHidden = scalarBoolean(property(definition.attributes, 'hide', 'hidden')) ?? false;
|
|
1218
|
+
const sourceVisible = scalarBoolean(property(definition.attributes, 'visible')) ?? !declaredHidden;
|
|
1219
|
+
const resolveVisibleAppearance = inheritedVisible && (explicitlyVisible ?? sourceVisible);
|
|
1220
|
+
const scriptedEnabled = rowValues?.[`${definition.name}.enabled`] ??
|
|
1221
|
+
scenario.values[`${definition.name}.enabled`] ??
|
|
1222
|
+
scenario.scriptedGui[`${definition.name}.enabled`] ??
|
|
1223
|
+
context.constantElementEnabled[definition.name] ??
|
|
1224
|
+
scalarBoolean(property(definition.attributes, 'enabled'));
|
|
1225
|
+
const requestedState = scenario.elementStates[definition.name] ?? scenario.state;
|
|
1226
|
+
const state = scriptedEnabled === false ? 'disabled' : requestedState;
|
|
1227
|
+
const backgroundValue = property(definition.attributes, 'background');
|
|
1228
|
+
const backgrounds = (Array.isArray(backgroundValue) ? backgroundValue : [backgroundValue])
|
|
1229
|
+
.map(objectProperty)
|
|
1230
|
+
.filter((value) => value !== undefined);
|
|
689
1231
|
const scriptedImage = rowValues?.[`${definition.name}.image`] ??
|
|
690
1232
|
rowValues?.[definition.name] ??
|
|
691
1233
|
scenario.values[`${definition.name}.image`] ??
|
|
@@ -693,22 +1235,22 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
693
1235
|
scenario.scriptedGui[`${definition.name}.image`] ??
|
|
694
1236
|
scenario.scriptedGui[definition.name];
|
|
695
1237
|
const spriteName = (typeof scriptedImage === 'string' && scriptedImage.length > 0 ? scriptedImage : undefined) ??
|
|
696
|
-
scalarString(property(definition.attributes, 'spriteType', 'quadTextureSprite'))
|
|
697
|
-
scalarString(background === undefined
|
|
698
|
-
? undefined
|
|
699
|
-
: property(background, 'spriteType', 'quadTextureSprite'));
|
|
1238
|
+
scalarString(property(definition.attributes, 'spriteType', 'quadTextureSprite'));
|
|
700
1239
|
const spriteDefinition = spriteName === undefined ? undefined : context.spritesByName.get(spriteName.toLowerCase());
|
|
701
1240
|
let sprite;
|
|
702
1241
|
let secondarySprite;
|
|
703
1242
|
let spriteRenderMode;
|
|
1243
|
+
let spriteShader;
|
|
1244
|
+
let progressShader;
|
|
1245
|
+
let progressColours;
|
|
704
1246
|
const countryFlag = dynamicCountryFlag(definition.name, context.countryFlagProperties.get(definition.name), scenario, rowValues);
|
|
705
1247
|
if (countryFlag !== undefined && resolveVisibleAppearance) {
|
|
706
1248
|
sprite = await catalog.loadCountryFlag(countryFlag.tag, spriteDefinition, countryFlag.ideology, spriteName);
|
|
707
1249
|
spriteRenderMode = 'stretch';
|
|
708
1250
|
if (sprite.supported) {
|
|
709
|
-
if (width === 0)
|
|
1251
|
+
if (width === 0 && inferWidth)
|
|
710
1252
|
width = sprite.width * scale;
|
|
711
|
-
if (height === 0)
|
|
1253
|
+
if (height === 0 && inferHeight)
|
|
712
1254
|
height = sprite.height * scale;
|
|
713
1255
|
addFidelity(fidelity, 'modelled', 'dynamic_country_flag', `${definition.name} resolved the ${countryFlag.tag} flag from scenario country context.`, definition);
|
|
714
1256
|
}
|
|
@@ -738,25 +1280,78 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
738
1280
|
: spriteType === 'maskedshieldtype'
|
|
739
1281
|
? 'masked-shield'
|
|
740
1282
|
: 'stretch';
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1283
|
+
const colourProgress = spriteRenderMode === 'progressbar' &&
|
|
1284
|
+
spriteDefinition.texturePath === undefined &&
|
|
1285
|
+
spriteDefinition.texturePath2 === undefined &&
|
|
1286
|
+
spriteDefinition.primaryColour !== undefined &&
|
|
1287
|
+
spriteDefinition.secondaryColour !== undefined;
|
|
1288
|
+
if (colourProgress) {
|
|
1289
|
+
progressColours = {
|
|
1290
|
+
first: spriteDefinition.primaryColour,
|
|
1291
|
+
second: spriteDefinition.secondaryColour,
|
|
1292
|
+
};
|
|
1293
|
+
if (width === 0 && inferWidth)
|
|
1294
|
+
width = (spriteDefinition.declaredSize?.width ?? 0) * scale;
|
|
1295
|
+
if (height === 0 && inferHeight)
|
|
1296
|
+
height = (spriteDefinition.declaredSize?.height ?? 0) * scale;
|
|
1297
|
+
}
|
|
1298
|
+
if (spriteDefinition.effectFile !== undefined && spriteRenderMode === 'progressbar') {
|
|
1299
|
+
const shader = catalog.progressShader(spriteDefinition, colourProgress ? 'Color' : 'Texture');
|
|
1300
|
+
if (shader.supported) {
|
|
1301
|
+
progressShader = shader.shader;
|
|
1302
|
+
addFidelity(fidelity, 'modelled', 'progress_shader_threshold', `${progressShader.sourcePath} :: ${progressShader.entryPoint} selects one of two ${colourProgress ? 'colours' : 'textures'} at the supplied progress.`, definition);
|
|
1303
|
+
}
|
|
1304
|
+
else {
|
|
1305
|
+
const detail = `Effect ${spriteDefinition.effectFile}: ${shader.reason}`;
|
|
1306
|
+
addFidelity(fidelity, 'unsupported', 'effectFile', detail, definition);
|
|
1307
|
+
partialAppearance.push(detail);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
else if (spriteDefinition.effectFile !== undefined) {
|
|
1311
|
+
const pressed = rowValues?.[`${definition.name}.pressed`] ?? scenario.values[`${definition.name}.pressed`];
|
|
1312
|
+
const effect = state === 'disabled' || state === 'locked'
|
|
1313
|
+
? 'Disable'
|
|
1314
|
+
: pressed === true
|
|
1315
|
+
? 'Down'
|
|
1316
|
+
: state === 'hover'
|
|
1317
|
+
? 'Over'
|
|
1318
|
+
: 'Up';
|
|
1319
|
+
const elapsed = rowValues?.[`${definition.name}.stateTimeSeconds`] ??
|
|
1320
|
+
scenario.values[`${definition.name}.stateTimeSeconds`] ??
|
|
1321
|
+
scenario.animationTimeSeconds;
|
|
1322
|
+
const shader = catalog.spriteShader(spriteDefinition, effect, {
|
|
1323
|
+
time: typeof elapsed === 'number'
|
|
1324
|
+
? Math.max(0, Math.min(86_400, elapsed))
|
|
1325
|
+
: scenario.animationTimeSeconds,
|
|
1326
|
+
animationTime: 0,
|
|
1327
|
+
});
|
|
1328
|
+
if (shader.supported) {
|
|
1329
|
+
spriteShader = shader.shader;
|
|
1330
|
+
addFidelity(fidelity, 'modelled', 'sprite_shader_colour', `${spriteShader.sourcePath} :: ${spriteShader.entryPoint} supplies the ${effect} colour transform.`, definition);
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
const detail = `Effect ${spriteDefinition.effectFile}: ${shader.reason}`;
|
|
1334
|
+
addFidelity(fidelity, 'unsupported', 'effectFile', detail, definition);
|
|
1335
|
+
partialAppearance.push(detail);
|
|
1336
|
+
}
|
|
745
1337
|
}
|
|
746
1338
|
if (partialAppearance.length > 0)
|
|
747
1339
|
diagnostics.push(diagnostic('GUI_SPRITE_RENDER_PARTIAL', 'warning', `Element ${definition.name} has a partial sprite appearance: ${partialAppearance.join(' ')}`, definition));
|
|
748
|
-
const frame = frameFor(definition, spriteDefinition, scenario, rowValues);
|
|
1340
|
+
const frame = frameFor(definition, spriteDefinition, scenario, rowValues, state);
|
|
749
1341
|
sprite = await catalog.loadSpriteFrame(spriteDefinition, frame);
|
|
750
1342
|
if (spriteDefinition.texturePath2 !== undefined)
|
|
751
1343
|
secondarySprite = await catalog.loadSecondarySpriteFrame(spriteDefinition, frame);
|
|
752
|
-
if (
|
|
1344
|
+
if (colourProgress) {
|
|
1345
|
+
addFidelity(fidelity, partialAppearance.length === 0 ? 'modelled' : 'approximated', 'progressbar_composition', `Progress bar ${spriteDefinition.name} uses its declared primary and secondary colours without a texture.`, definition);
|
|
1346
|
+
}
|
|
1347
|
+
else if (!sprite?.supported) {
|
|
753
1348
|
addFidelity(fidelity, 'unsupported', 'texture', sprite?.reason ?? `Sprite ${spriteDefinition.name} has no texture.`, definition);
|
|
754
1349
|
diagnostics.push(diagnostic('GUI_TEXTURE_UNSUPPORTED', 'warning', sprite?.reason ?? `Sprite ${spriteDefinition.name} has no texture.`, definition));
|
|
755
1350
|
}
|
|
756
1351
|
else {
|
|
757
|
-
if (width === 0)
|
|
1352
|
+
if (width === 0 && inferWidth)
|
|
758
1353
|
width = sprite.width * scale;
|
|
759
|
-
if (height === 0)
|
|
1354
|
+
if (height === 0 && inferHeight)
|
|
760
1355
|
height = sprite.height * scale;
|
|
761
1356
|
addFidelity(fidelity, partialAppearance.length === 0 ? 'modelled' : 'approximated', 'sprite_frame', partialAppearance.length === 0
|
|
762
1357
|
? `${spriteDefinition.name} frame ${sprite.frame + 1}/${sprite.frameCount}`
|
|
@@ -794,11 +1389,18 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
794
1389
|
if (typeof scriptedX === 'number' || typeof scriptedY === 'number')
|
|
795
1390
|
addFidelity(fidelity, 'modelled', 'scripted_gui_position', `Scenario properties positioned ${definition.name} at ${localX},${localY} before anchoring.`, definition);
|
|
796
1391
|
const rawButtonText = scalarString(property(definition.attributes, 'buttonText'));
|
|
797
|
-
const
|
|
1392
|
+
const overrideText = rowValues?.[`${definition.name}.text`] ??
|
|
1393
|
+
scenario.values[`${definition.name}.text`] ??
|
|
1394
|
+
scenario.scriptedGui[`${definition.name}.text`];
|
|
1395
|
+
const rawText = (typeof overrideText === 'string' || typeof overrideText === 'number'
|
|
1396
|
+
? String(overrideText)
|
|
1397
|
+
: undefined) ??
|
|
1398
|
+
scalarString(property(definition.attributes, 'text', 'context_aware_text')) ??
|
|
1399
|
+
rawButtonText;
|
|
798
1400
|
const embeddedButtonText = rawText !== undefined &&
|
|
799
1401
|
(rawButtonText !== undefined || definition.elementType.toLowerCase().includes('button'));
|
|
800
1402
|
let text;
|
|
801
|
-
if (rawText !== undefined && resolveVisibleAppearance) {
|
|
1403
|
+
if (rawText !== undefined && rawText.length > 0 && resolveVisibleAppearance) {
|
|
802
1404
|
const resolved = resolveTokenText(rawText, scenario, context.localisation, rowValues);
|
|
803
1405
|
let displayText = resolved.text;
|
|
804
1406
|
const state = scenario.elementStates[definition.name] ?? scenario.state;
|
|
@@ -849,9 +1451,11 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
849
1451
|
}
|
|
850
1452
|
const declaredMaxWidth = scaledNumber(property(definition.attributes, 'maxWidth'), parentRect.width, scale);
|
|
851
1453
|
const declaredMaxHeight = scaledNumber(property(definition.attributes, 'maxHeight'), parentRect.height, scale);
|
|
852
|
-
if (width === 0 && declaredMaxWidth !== undefined)
|
|
1454
|
+
if (width === 0 && inferWidth && declaredMaxWidth !== undefined)
|
|
853
1455
|
width = declaredMaxWidth;
|
|
854
|
-
if (
|
|
1456
|
+
if (width === 0 && inferWidth && /textbox/iu.test(definition.elementType))
|
|
1457
|
+
width = nativeTextBoxWidth * scale;
|
|
1458
|
+
if (height === 0 && inferHeight && declaredMaxHeight !== undefined)
|
|
855
1459
|
height = declaredMaxHeight;
|
|
856
1460
|
const maxWidth = declaredMaxWidth ?? width;
|
|
857
1461
|
const wrapped = wrapText(catalog, fontName, displayText, visibleText.colours, fontSize * scale, maxWidth, context.work, resolvedInlineIcons);
|
|
@@ -866,9 +1470,9 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
866
1470
|
? wrapped.lines.map((line, index) => colourRunsForLine(catalog, fontName, line, wrapped.colourLines[index] ?? [], fontSize * scale, context.work, resolvedInlineIcons, fontDefinition?.colour ?? defaultTextColour))
|
|
867
1471
|
: undefined;
|
|
868
1472
|
const maximumLineWidth = wrapped.widths.reduce((maximum, value) => Math.max(maximum, value), 0);
|
|
869
|
-
if (width === 0)
|
|
1473
|
+
if (width === 0 && inferWidth)
|
|
870
1474
|
width = maximumLineWidth;
|
|
871
|
-
if (height === 0)
|
|
1475
|
+
if (height === 0 && inferHeight)
|
|
872
1476
|
height = wrapped.lines.length * wrapped.lineHeight;
|
|
873
1477
|
const measuredWidth = maximumLineWidth;
|
|
874
1478
|
const measuredHeight = wrapped.lines.length * wrapped.lineHeight;
|
|
@@ -914,17 +1518,13 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
914
1518
|
: 'Deterministic project font paths substitute for unavailable workspace glyph data.', definition);
|
|
915
1519
|
if (resolvedFontMetrics.source === 'bmfont')
|
|
916
1520
|
addFidelity(fidelity, resolvedFontMetrics.baselineModelled ? 'modelled' : 'approximated', 'font_native_metrics', `BMFont native size ${resolvedFontMetrics.nativeSize}, line height ${resolvedFontMetrics.nativeLineHeight}, and baseline ${resolvedFontMetrics.nativeBaseline}${explicitFontSize === undefined ? ' determine this element layout' : ' are scaled to the element fontSize'}.`, definition);
|
|
917
|
-
if (resolved.missingLocalisation)
|
|
1521
|
+
if (resolved.missingLocalisation && overrideText === undefined)
|
|
918
1522
|
addFidelity(fidelity, 'missing', 'localisation', `No ${scenario.language} localisation for ${rawText}.`, definition);
|
|
919
1523
|
if (resolved.unresolved.length > 0)
|
|
920
1524
|
addFidelity(fidelity, 'unresolved', 'dynamic_text', `Unresolved tokens: ${resolved.unresolved.join(', ')}.`, definition);
|
|
921
1525
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const origo = scalarString(property(definition.attributes, 'origo'))?.toLowerCase() ?? 'upper_left';
|
|
925
|
-
const orientationAxes = anchorAxes(orientation);
|
|
926
|
-
let x = anchoredCoordinate(parentRect.x, parentRect.width, localX, orientationAxes.horizontal);
|
|
927
|
-
let y = anchoredCoordinate(parentRect.y, parentRect.height, localY, orientationAxes.vertical);
|
|
1526
|
+
let x = anchorX;
|
|
1527
|
+
let y = anchorY;
|
|
928
1528
|
const centerPosition = scalarBoolean(property(definition.attributes, 'centerposition')) ?? false;
|
|
929
1529
|
const origoAxes = centerPosition
|
|
930
1530
|
? { horizontal: 'center', vertical: 'center' }
|
|
@@ -938,26 +1538,60 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
938
1538
|
y -= height / 2;
|
|
939
1539
|
else if (origoAxes.vertical === 'bottom')
|
|
940
1540
|
y -= height;
|
|
941
|
-
const scrollOffset = (scenario.scrollOffsets[definition.name] ?? scenario.scrollOffsets[definition.id] ?? 0) * scale;
|
|
942
1541
|
const unclippedRect = { x, y, width: Math.max(0, width), height: Math.max(0, height) };
|
|
943
1542
|
const ownClipping = scalarBoolean(property(definition.attributes, 'clipping')) ??
|
|
944
|
-
(
|
|
1543
|
+
(backgrounds.length > 0 && /(?:containerwindow|windowtype)/iu.test(definition.elementType));
|
|
1544
|
+
let scrollbars = attachedScrollbars(definition, context);
|
|
1545
|
+
if (scrollbars.length > 0 && (!ownClipping || backgrounds.length === 0)) {
|
|
1546
|
+
diagnostics.push(diagnostic('GUI_SCROLLBAR_CONTAINER_BOUNDARY_MISSING', 'warning', `Container ${definition.name} needs a background and clipping for attached scrolling.`, definition));
|
|
1547
|
+
addFidelity(fidelity, 'unsupported', 'attached_scrollbar', 'The container has no active clipped background boundary.', definition);
|
|
1548
|
+
scrollbars = [];
|
|
1549
|
+
}
|
|
1550
|
+
const margin = scrollbars.length === 0 ? undefined : objectProperty(property(definition.attributes, 'margin'));
|
|
1551
|
+
const inset = (name, reference) => Math.max(0, Math.min(reference, scaledNumber(margin === undefined ? undefined : property(margin, name), reference, scale) ??
|
|
1552
|
+
0));
|
|
1553
|
+
const left = inset('left', unclippedRect.width);
|
|
1554
|
+
const top = inset('top', unclippedRect.height);
|
|
1555
|
+
const contentRect = {
|
|
1556
|
+
x: x + left,
|
|
1557
|
+
y: y + top,
|
|
1558
|
+
width: Math.max(0, unclippedRect.width - left - inset('right', unclippedRect.width)),
|
|
1559
|
+
height: Math.max(0, unclippedRect.height - top - inset('bottom', unclippedRect.height)),
|
|
1560
|
+
};
|
|
1561
|
+
const scrollOffset = scrollbars.length > 0
|
|
1562
|
+
? 0
|
|
1563
|
+
: (scenario.scrollOffsets[`${definition.name}${instanceSuffix}`] ??
|
|
1564
|
+
scenario.scrollOffsets[`${definition.name}${legacyRowSuffix(instanceSuffix)}`] ??
|
|
1565
|
+
scenario.scrollOffsets[definition.name] ??
|
|
1566
|
+
scenario.scrollOffsets[definition.id] ??
|
|
1567
|
+
0) * scale;
|
|
945
1568
|
const availableClip = inheritedClip === undefined ? unclippedRect : rectIntersection(inheritedClip, unclippedRect);
|
|
946
1569
|
const clipRect = inheritedClip;
|
|
947
1570
|
const clipped = clipRect !== undefined &&
|
|
948
1571
|
(availableClip === undefined || !equalRect(unclippedRect, availableClip));
|
|
949
|
-
const visible =
|
|
950
|
-
(explicitlyVisible ?? (availableClip !== undefined || inheritedClip === undefined));
|
|
1572
|
+
const visible = resolveVisibleAppearance && (availableClip !== undefined || inheritedClip === undefined);
|
|
951
1573
|
const clickThrough = scalarBoolean(property(definition.attributes, 'clickThrough', 'alwaystransparent', 'allwaystransparent')) ?? false;
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1574
|
+
const interactive = clickableTypes.test(definition.elementType);
|
|
1575
|
+
const interactionBlock = !interactive
|
|
1576
|
+
? undefined
|
|
1577
|
+
: clickThrough
|
|
1578
|
+
? 'click_through'
|
|
1579
|
+
: scriptedEnabled === false
|
|
1580
|
+
? 'scripted_enabled_false'
|
|
1581
|
+
: requestedState === 'disabled' || requestedState === 'locked'
|
|
1582
|
+
? 'scenario_state'
|
|
1583
|
+
: undefined;
|
|
1584
|
+
const disabledReason = interactive && !visible ? 'hidden' : interactionBlock;
|
|
1585
|
+
const clickable = interactive && disabledReason === undefined;
|
|
1586
|
+
const visibilityReason = visible
|
|
1587
|
+
? 'visible'
|
|
1588
|
+
: !inheritedVisible
|
|
1589
|
+
? 'parent_hidden'
|
|
1590
|
+
: explicitlyVisible === false
|
|
1591
|
+
? 'visibility_false'
|
|
1592
|
+
: 'outside_clip';
|
|
959
1593
|
let progressRatio;
|
|
960
|
-
if (/progressbar/iu.test(definition.elementType)) {
|
|
1594
|
+
if (/progressbar/iu.test(definition.elementType) || spriteRenderMode === 'progressbar') {
|
|
961
1595
|
const minimum = scalarNumber(property(definition.attributes, 'minValue'), 1) ?? 0;
|
|
962
1596
|
const maximum = scalarNumber(property(definition.attributes, 'maxValue'), 1) ?? 100;
|
|
963
1597
|
const startValueToken = scalarString(property(definition.attributes, 'startValue'));
|
|
@@ -978,10 +1612,15 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
978
1612
|
value = maximum;
|
|
979
1613
|
progressRatio =
|
|
980
1614
|
maximum === minimum ? 0 : Math.max(0, Math.min(1, (value - minimum) / (maximum - minimum)));
|
|
1615
|
+
const explicitProgress = rowValues?.[`${definition.name}.progress`] ??
|
|
1616
|
+
scenario.values[`${definition.name}.progress`] ??
|
|
1617
|
+
scenario.scriptedGui[`${definition.name}.progress`];
|
|
1618
|
+
if (typeof explicitProgress === 'number')
|
|
1619
|
+
progressRatio = Math.max(0, Math.min(1, explicitProgress));
|
|
981
1620
|
}
|
|
982
1621
|
const sceneElement = {
|
|
983
1622
|
id: instanceId,
|
|
984
|
-
sourceId: definition.id,
|
|
1623
|
+
sourceId: definition.sourceOwnerId ?? definition.id,
|
|
985
1624
|
name: definition.name,
|
|
986
1625
|
elementType: definition.elementType,
|
|
987
1626
|
...(parentInstanceId === undefined
|
|
@@ -994,9 +1633,11 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
994
1633
|
}
|
|
995
1634
|
: { parentId: parentInstanceId }),
|
|
996
1635
|
depth,
|
|
997
|
-
zIndex:
|
|
1636
|
+
zIndex: context.output.length,
|
|
998
1637
|
visible,
|
|
1638
|
+
visibilityReason,
|
|
999
1639
|
clickable,
|
|
1640
|
+
...(disabledReason === undefined ? {} : { disabledReason }),
|
|
1000
1641
|
clickThrough,
|
|
1001
1642
|
rect: availableClip ?? { x: unclippedRect.x, y: unclippedRect.y, width: 0, height: 0 },
|
|
1002
1643
|
unclippedRect,
|
|
@@ -1008,6 +1649,9 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1008
1649
|
...(sprite === undefined ? {} : { sprite }),
|
|
1009
1650
|
...(secondarySprite === undefined ? {} : { secondarySprite }),
|
|
1010
1651
|
...(spriteRenderMode === undefined ? {} : { spriteRenderMode }),
|
|
1652
|
+
...(spriteShader === undefined ? {} : { spriteShader }),
|
|
1653
|
+
...(progressShader === undefined ? {} : { progressShader }),
|
|
1654
|
+
...(progressColours === undefined ? {} : { progressColours }),
|
|
1011
1655
|
...(spriteDefinition?.borderSize === undefined
|
|
1012
1656
|
? {}
|
|
1013
1657
|
: { spriteBorderSize: spriteDefinition.borderSize }),
|
|
@@ -1025,9 +1669,44 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1025
1669
|
};
|
|
1026
1670
|
context.output.push(sceneElement);
|
|
1027
1671
|
context.instancesById.set(instanceId, sceneElement);
|
|
1672
|
+
context.geometry.set(instanceId, {
|
|
1673
|
+
appearanceVisible: resolveVisibleAppearance,
|
|
1674
|
+
...(interactionBlock === undefined ? {} : { interactionBlock }),
|
|
1675
|
+
clipChain,
|
|
1676
|
+
contentRect,
|
|
1677
|
+
measuredRect: unclippedRect,
|
|
1678
|
+
});
|
|
1679
|
+
for (const [index, attributes] of backgrounds.entries()) {
|
|
1680
|
+
const backgroundSprite = scalarString(property(attributes, 'spriteType', 'quadTextureSprite'));
|
|
1681
|
+
const nativeSprite = backgroundSprite === undefined
|
|
1682
|
+
? undefined
|
|
1683
|
+
: context.spritesByName.get(backgroundSprite.toLowerCase());
|
|
1684
|
+
const tiled = nativeSprite?.spriteType.toLowerCase() === 'corneredtilespritetype';
|
|
1685
|
+
const backgroundDefinition = {
|
|
1686
|
+
...definition,
|
|
1687
|
+
id: `${definition.id}:background-${index}`,
|
|
1688
|
+
sourceOwnerId: definition.sourceOwnerId ?? definition.id,
|
|
1689
|
+
name: scalarString(property(attributes, 'name')) ?? `${definition.name}_background_${index}`,
|
|
1690
|
+
elementType: 'background',
|
|
1691
|
+
parentId: definition.id,
|
|
1692
|
+
childIds: [],
|
|
1693
|
+
attributes: {
|
|
1694
|
+
...attributes,
|
|
1695
|
+
...(tiled && property(attributes, 'size') === undefined
|
|
1696
|
+
? { size: { width: width / scale, height: height / scale } }
|
|
1697
|
+
: {}),
|
|
1698
|
+
alwaystransparent: true,
|
|
1699
|
+
},
|
|
1700
|
+
unsupportedAttributes: Object.keys(attributes).filter((key) => guiElementAttributeFidelity(key) === 'unsupported'),
|
|
1701
|
+
};
|
|
1702
|
+
await layoutElement(backgroundDefinition, unclippedRect, inheritedClip, scale, depth + 1, context, instanceSuffix, rowIndex, rowValues, instanceId, 'upper_left', resolveVisibleAppearance, clipChain);
|
|
1703
|
+
addFidelity(fidelity, 'modelled', 'native_background', tiled
|
|
1704
|
+
? 'Cornered background fills its container; fixed source borders retain their native size.'
|
|
1705
|
+
: 'Fixed background retains its native texture size and local position independently of the container bounds.', definition);
|
|
1706
|
+
}
|
|
1028
1707
|
addFidelity(fidelity, 'modelled', 'nested_offset', `Positioned ${definition.name} at ${x},${y} with scale ${scale}.`, definition);
|
|
1029
1708
|
for (const attribute of Object.keys(definition.attributes).sort((left, right) => compareCodeUnits(left, right))) {
|
|
1030
|
-
const classification = guiElementAttributeFidelity(attribute);
|
|
1709
|
+
const classification = guiElementAttributeFidelity(attribute, definition.elementType);
|
|
1031
1710
|
if (classification === 'structural' || classification === 'unsupported')
|
|
1032
1711
|
continue;
|
|
1033
1712
|
addFidelity(fidelity, classification, attribute, classification === 'modelled'
|
|
@@ -1041,29 +1720,73 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1041
1720
|
const childParentRect = { ...unclippedRect, y: unclippedRect.y - scrollOffset };
|
|
1042
1721
|
const childClip = ownClipping
|
|
1043
1722
|
? inheritedClip === undefined
|
|
1044
|
-
?
|
|
1045
|
-
: rectIntersection(inheritedClip,
|
|
1723
|
+
? contentRect
|
|
1724
|
+
: (rectIntersection(inheritedClip, contentRect) ?? {
|
|
1725
|
+
...contentRect,
|
|
1726
|
+
width: 0,
|
|
1727
|
+
height: 0,
|
|
1728
|
+
})
|
|
1046
1729
|
: inheritedClip;
|
|
1047
|
-
const
|
|
1730
|
+
const childClipChain = ownClipping ? [...clipChain, contentRect] : clipChain;
|
|
1731
|
+
const childDefinitions = await scrollbarChildren(definition, definition.childIds
|
|
1048
1732
|
.map((id) => context.elementsById.get(id))
|
|
1049
|
-
.filter((element) => element !== undefined)
|
|
1050
|
-
|
|
1733
|
+
.filter((element) => element !== undefined)
|
|
1734
|
+
.filter((element) => !/^(?:containerwindowtype|windowtype)$/iu.test(definition.elementType) ||
|
|
1735
|
+
element.elementType.toLowerCase() !== 'extendedscrollbartype')
|
|
1736
|
+
.sort((left, right) => (scalarNumber(property(left.attributes, 'priority'), 0) ?? 0) -
|
|
1737
|
+
(scalarNumber(property(right.attributes, 'priority'), 0) ?? 0) ||
|
|
1738
|
+
left.definitionOrder - right.definitionOrder), unclippedRect, scale, context, rowValues);
|
|
1739
|
+
const rowList = rowValues?.[`${definition.name}.list`];
|
|
1740
|
+
let rows = (typeof rowList === 'string' ? scenario.lists[rowList] : undefined) ??
|
|
1741
|
+
scenario.lists[`${definition.name}${instanceSuffix}`] ??
|
|
1742
|
+
scenario.lists[`${definition.name}${legacyRowSuffix(instanceSuffix)}`] ??
|
|
1743
|
+
scenario.lists[definition.name] ??
|
|
1744
|
+
scenario.lists[definition.id];
|
|
1051
1745
|
if (state === 'empty-list')
|
|
1052
1746
|
rows = [];
|
|
1053
1747
|
if (state === 'full-list' && rows === undefined)
|
|
1054
1748
|
rows = Array.from({ length: 12 }, (_unused, index) => ({ index }));
|
|
1055
1749
|
const dynamicList = context.dynamicListsByName.get(definition.name);
|
|
1056
|
-
const listElement = /(?:grid|listbox|
|
|
1750
|
+
const listElement = /(?:grid|listbox|overlappingelements)/iu.test(definition.elementType);
|
|
1057
1751
|
const slotSize = objectProperty(property(definition.attributes, 'slotsize'));
|
|
1058
1752
|
const slotHeight = scaledNumber(slotSize === undefined ? undefined : property(slotSize, 'height', 'y'), height, scale);
|
|
1753
|
+
const slotWidth = scaledNumber(slotSize === undefined ? undefined : property(slotSize, 'width', 'x'), width, scale);
|
|
1754
|
+
const spacingValue = objectProperty(property(definition.attributes, 'spacing'));
|
|
1755
|
+
const spacingY = (scalarNumber(spacingValue === undefined
|
|
1756
|
+
? property(definition.attributes, 'spacing')
|
|
1757
|
+
: property(spacingValue, 'y'), height) ?? 0) * scale;
|
|
1758
|
+
const spacingX = (scalarNumber(spacingValue === undefined
|
|
1759
|
+
? property(definition.attributes, 'spacing')
|
|
1760
|
+
: property(spacingValue, 'x'), width) ?? 0) * scale;
|
|
1761
|
+
const maxHorizontal = scalarNumber(property(definition.attributes, 'max_slots_horizontal'), 1);
|
|
1762
|
+
const maxVertical = scalarNumber(property(definition.attributes, 'max_slots_vertical'), 1);
|
|
1763
|
+
const grid = /grid/iu.test(definition.elementType);
|
|
1764
|
+
const rowPosition = (index, fallbackY) => {
|
|
1765
|
+
if (!grid || slotWidth === undefined || slotHeight === undefined)
|
|
1766
|
+
return { x: 0, y: fallbackY };
|
|
1767
|
+
if (maxVertical !== undefined && maxVertical > 0) {
|
|
1768
|
+
const rowsPerColumn = Math.max(1, Math.trunc(maxVertical));
|
|
1769
|
+
return {
|
|
1770
|
+
x: Math.floor(index / rowsPerColumn) * (slotWidth + spacingX),
|
|
1771
|
+
y: (index % rowsPerColumn) * (slotHeight + spacingY),
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
const fit = Math.max(1, Math.floor((width + spacingX) / Math.max(1, slotWidth + spacingX)));
|
|
1775
|
+
const columns = maxHorizontal === undefined || maxHorizontal <= 0
|
|
1776
|
+
? fit
|
|
1777
|
+
: Math.max(1, Math.min(fit, Math.trunc(maxHorizontal)));
|
|
1778
|
+
return {
|
|
1779
|
+
x: (index % columns) * (slotWidth + spacingX),
|
|
1780
|
+
y: Math.floor(index / columns) * (slotHeight + spacingY),
|
|
1781
|
+
};
|
|
1782
|
+
};
|
|
1783
|
+
const firstChildOutput = context.output.length;
|
|
1784
|
+
const rowSuffix = (index) => `${instanceSuffix}#list-${definition.id}#row-${index}`;
|
|
1059
1785
|
if (rows !== undefined && childDefinitions.length > 0 && listElement) {
|
|
1060
|
-
const spacingValue = objectProperty(property(definition.attributes, 'spacing'));
|
|
1061
|
-
const spacingY = scalarNumber(spacingValue === undefined
|
|
1062
|
-
? property(definition.attributes, 'spacing')
|
|
1063
|
-
: property(spacingValue, 'y'), height) ?? 0;
|
|
1064
1786
|
let rowY = 0;
|
|
1065
1787
|
for (const [index, row] of rows.entries()) {
|
|
1066
1788
|
context.work.spend('scenario list row expansion');
|
|
1789
|
+
const offset = rowPosition(index, rowY);
|
|
1067
1790
|
let rowHeight = 0;
|
|
1068
1791
|
for (const child of childDefinitions) {
|
|
1069
1792
|
context.work.spend('scenario list child expansion');
|
|
@@ -1071,17 +1794,23 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1071
1794
|
const originalY = scalarNumber(childPosition === undefined ? undefined : property(childPosition, 'y'), height) ?? 0;
|
|
1072
1795
|
const shiftedParent = {
|
|
1073
1796
|
...childParentRect,
|
|
1074
|
-
|
|
1797
|
+
x: childParentRect.x + offset.x,
|
|
1798
|
+
y: childParentRect.y + offset.y - originalY * scale,
|
|
1799
|
+
...(slotWidth === undefined ? {} : { width: slotWidth }),
|
|
1800
|
+
...(slotHeight === undefined ? {} : { height: slotHeight }),
|
|
1075
1801
|
};
|
|
1076
|
-
await layoutElement(child, shiftedParent, childClip, scale, depth + 1, context,
|
|
1077
|
-
const rendered = context.instancesById.get(`${child.id}${
|
|
1802
|
+
await layoutElement(child, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId, origo, resolveVisibleAppearance, childClipChain);
|
|
1803
|
+
const rendered = context.instancesById.get(`${child.id}${rowSuffix(index)}`);
|
|
1078
1804
|
rowHeight = Math.max(rowHeight, rendered?.unclippedRect.height ?? 0);
|
|
1079
1805
|
}
|
|
1080
|
-
rowY += (slotHeight ?? rowHeight) + spacingY
|
|
1806
|
+
rowY += (slotHeight ?? rowHeight) + spacingY;
|
|
1081
1807
|
}
|
|
1082
1808
|
addFidelity(fidelity, 'modelled', 'scroll_rows', `Expanded ${rows.length} scenario rows for ${definition.name}.`, definition);
|
|
1083
1809
|
}
|
|
1084
|
-
else if (rows !== undefined &&
|
|
1810
|
+
else if (rows !== undefined &&
|
|
1811
|
+
listElement &&
|
|
1812
|
+
(dynamicList !== undefined ||
|
|
1813
|
+
rows.some(({ entryContainer }) => typeof entryContainer === 'string'))) {
|
|
1085
1814
|
let rowY = 0;
|
|
1086
1815
|
let renderedRows = 0;
|
|
1087
1816
|
for (const [index, row] of rows.entries()) {
|
|
@@ -1091,8 +1820,8 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1091
1820
|
? explicitTemplate
|
|
1092
1821
|
: undefined) ??
|
|
1093
1822
|
(row.countryScope === true
|
|
1094
|
-
? (dynamicList
|
|
1095
|
-
: (dynamicList
|
|
1823
|
+
? (dynamicList?.countryScopeEntryContainer ?? dynamicList?.entryContainer)
|
|
1824
|
+
: (dynamicList?.entryContainer ?? dynamicList?.countryScopeEntryContainer));
|
|
1096
1825
|
const template = templateName === undefined ? undefined : context.elementsByName.get(templateName);
|
|
1097
1826
|
if (template === undefined) {
|
|
1098
1827
|
diagnostics.push(diagnostic('GUI_DYNAMIC_LIST_TEMPLATE_MISSING', 'error', templateName === undefined
|
|
@@ -1101,24 +1830,41 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1101
1830
|
continue;
|
|
1102
1831
|
}
|
|
1103
1832
|
const templatePosition = objectProperty(property(template.attributes, 'position'));
|
|
1833
|
+
const offset = rowPosition(index, rowY);
|
|
1834
|
+
const originalX = scalarNumber(templatePosition === undefined ? undefined : property(templatePosition, 'x'), width) ?? 0;
|
|
1104
1835
|
const originalY = scalarNumber(templatePosition === undefined ? undefined : property(templatePosition, 'y'), height) ?? 0;
|
|
1105
1836
|
const shiftedParent = {
|
|
1106
1837
|
...childParentRect,
|
|
1107
|
-
|
|
1838
|
+
x: childParentRect.x + offset.x - originalX * scale,
|
|
1839
|
+
y: childParentRect.y + offset.y - originalY * scale,
|
|
1840
|
+
...(slotWidth === undefined ? {} : { width: slotWidth }),
|
|
1841
|
+
...(slotHeight === undefined ? {} : { height: slotHeight }),
|
|
1108
1842
|
};
|
|
1109
|
-
await layoutElement(template, shiftedParent, childClip, scale, depth + 1, context,
|
|
1110
|
-
const rendered = context.instancesById.get(`${template.id}${
|
|
1111
|
-
rowY += slotHeight ?? rendered?.unclippedRect.height ?? 0;
|
|
1843
|
+
await layoutElement(template, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId, origo, resolveVisibleAppearance, childClipChain);
|
|
1844
|
+
const rendered = context.instancesById.get(`${template.id}${rowSuffix(index)}`);
|
|
1845
|
+
rowY += (slotHeight ?? rendered?.unclippedRect.height ?? 0) + spacingY;
|
|
1112
1846
|
renderedRows += 1;
|
|
1113
1847
|
}
|
|
1114
|
-
addFidelity(fidelity, renderedRows === rows.length ? 'modelled' : 'missing', 'scripted_gui_dynamic_list', `Expanded ${renderedRows}/${rows.length} scenario rows for ${definition.name} through
|
|
1848
|
+
addFidelity(fidelity, renderedRows === rows.length ? 'modelled' : 'missing', dynamicList === undefined ? 'native_dynamic_list' : 'scripted_gui_dynamic_list', `Expanded ${renderedRows}/${rows.length} scenario rows for ${definition.name} through ${dynamicList === undefined ? 'explicit native' : 'scripted-GUI'} entry containers.`, definition);
|
|
1115
1849
|
}
|
|
1116
1850
|
else {
|
|
1117
1851
|
for (const child of childDefinitions) {
|
|
1118
1852
|
context.work.spend('nested child expansion');
|
|
1119
|
-
await layoutElement(child, childParentRect, childClip, scale, depth + 1, context, instanceSuffix, rowIndex, rowValues, instanceId, origo,
|
|
1853
|
+
await layoutElement(child, childParentRect, childClip, scale, depth + 1, context, instanceSuffix, rowIndex, rowValues, instanceId, origo, resolveVisibleAppearance, childClipChain);
|
|
1120
1854
|
}
|
|
1121
1855
|
}
|
|
1856
|
+
const childOutput = context.output.slice(firstChildOutput);
|
|
1857
|
+
context.work.spend('child content measurement', childOutput.length);
|
|
1858
|
+
const directChildren = childOutput.filter((child) => child.parentId === instanceId && context.geometry.get(child.id).appearanceVisible);
|
|
1859
|
+
if (listElement && rows !== undefined) {
|
|
1860
|
+
// Declared list bounds remain a minimum even when its runtime row collection is short.
|
|
1861
|
+
context.geometry.get(instanceId).measuredRect = unionRects([
|
|
1862
|
+
unclippedRect,
|
|
1863
|
+
...directChildren.map((child) => child.unclippedRect),
|
|
1864
|
+
]);
|
|
1865
|
+
}
|
|
1866
|
+
if (scrollbars.length > 0)
|
|
1867
|
+
await layoutAttachedScrollbars(definition, scrollbars, sceneElement, childOutput, context, depth, scale, rowIndex, rowValues, instanceSuffix);
|
|
1122
1868
|
}
|
|
1123
1869
|
/** Build a deterministic GUI scene from the connected source graph and scenario. */
|
|
1124
1870
|
export async function buildGuiScene(graph, scannedFiles, windowName, scenario, catalog = new GuiAssetCatalog(graph, scannedFiles)) {
|
|
@@ -1141,9 +1887,8 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
1141
1887
|
const candidates = graph.elements.filter((element) => element.name === windowName);
|
|
1142
1888
|
const root = candidates.toSorted((left, right) => compareCodeUnits(right.sourcePath, left.sourcePath) ||
|
|
1143
1889
|
right.definitionOrder - left.definitionOrder)[0];
|
|
1144
|
-
const baseScale =
|
|
1145
|
-
|
|
1146
|
-
addFidelity(fidelity, 'approximated', 'resolution_scale', `Coordinates use a 1920x1080 reference with UI scale ${scenario.uiScale}.`);
|
|
1890
|
+
const baseScale = scenario.uiScale;
|
|
1891
|
+
addFidelity(fidelity, 'modelled', 'resolution_scale', `Pixel dimensions use UI scale ${scenario.uiScale}; resolution changes the viewport and anchors, not the pixel scale.`);
|
|
1147
1892
|
if (root === undefined) {
|
|
1148
1893
|
diagnostics.push({
|
|
1149
1894
|
code: 'GUI_WINDOW_MISSING',
|
|
@@ -1194,13 +1939,47 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
1194
1939
|
const resolvedScriptedProperties = {};
|
|
1195
1940
|
const dynamicListsByName = new Map();
|
|
1196
1941
|
const countryFlagProperties = new Map();
|
|
1942
|
+
const conditionDefinitions = relevantScriptedGuiNames.size === 0
|
|
1943
|
+
? undefined
|
|
1944
|
+
: ClausewitzEvaluationDefinitions.build({ files: [...scannedFiles] });
|
|
1945
|
+
const conditionScenario = guiConditionScenario(scenario);
|
|
1197
1946
|
for (const scripted of graph.scriptedGuis.filter(({ name }) => relevantScriptedGuiNames.has(name))) {
|
|
1947
|
+
const source = scannedFiles.find(({ displayPath }) => displayPath === scripted.sourcePath);
|
|
1948
|
+
const subject = {
|
|
1949
|
+
id: scripted.id,
|
|
1950
|
+
provenance: [],
|
|
1951
|
+
...(source === undefined
|
|
1952
|
+
? {}
|
|
1953
|
+
: { document: parseClausewitz(source.bytes, source.displayPath) }),
|
|
1954
|
+
};
|
|
1955
|
+
for (const image of scripted.imageDefinitions ?? []) {
|
|
1956
|
+
let selected;
|
|
1957
|
+
let unresolved = false;
|
|
1958
|
+
for (const choice of image.choices) {
|
|
1959
|
+
const result = evaluateGuiCondition(choice.conditionExpression, conditionScenario, subject, conditionDefinitions);
|
|
1960
|
+
if (result.state === 'unresolved') {
|
|
1961
|
+
unresolved = true;
|
|
1962
|
+
addFidelity(fidelity, 'unresolved', 'focus_inlay_image', `${scripted.name}.${image.elementName} requires more scenario facts before selecting a sprite.`);
|
|
1963
|
+
break;
|
|
1964
|
+
}
|
|
1965
|
+
if (result.state === 'true') {
|
|
1966
|
+
selected = choice.spriteName;
|
|
1967
|
+
break;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
if (unresolved)
|
|
1971
|
+
continue;
|
|
1972
|
+
constantElementVisibility[image.elementName] = selected !== undefined;
|
|
1973
|
+
if (selected !== undefined)
|
|
1974
|
+
resolvedScriptedProperties[`${image.elementName}.image`] = selected;
|
|
1975
|
+
addFidelity(fidelity, 'modelled', 'focus_inlay_image', `${scripted.name}.${image.elementName} ${selected === undefined ? 'has no matching sprite and is hidden' : `selects ${selected}`}.`);
|
|
1976
|
+
}
|
|
1198
1977
|
for (const dynamicList of scripted.dynamicListDefinitions)
|
|
1199
1978
|
dynamicListsByName.set(dynamicList.name, dynamicList);
|
|
1200
1979
|
for (const propertyDefinition of scripted.propertyDefinitions) {
|
|
1201
1980
|
for (const [attribute, expression] of Object.entries(propertyDefinition.attributes)) {
|
|
1202
1981
|
const suffix = attribute.toLowerCase();
|
|
1203
|
-
if (!['image', 'frame', 'x', 'y', 'visible', 'enabled'].includes(suffix))
|
|
1982
|
+
if (!['image', 'frame', 'x', 'y', 'visible', 'enabled', 'progress'].includes(suffix))
|
|
1204
1983
|
continue;
|
|
1205
1984
|
const countryFlagExpression = suffix === 'image' &&
|
|
1206
1985
|
typeof expression === 'string' &&
|
|
@@ -1209,7 +1988,10 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
1209
1988
|
countryFlagProperties.set(propertyDefinition.elementName, expression);
|
|
1210
1989
|
if (countryFlagExpression)
|
|
1211
1990
|
continue;
|
|
1212
|
-
const resolved = scenarioExpressionValue(expression,
|
|
1991
|
+
const resolved = scenarioExpressionValue(expression, {
|
|
1992
|
+
...scenario.variables,
|
|
1993
|
+
...scenario.values,
|
|
1994
|
+
});
|
|
1213
1995
|
if (resolved === undefined)
|
|
1214
1996
|
continue;
|
|
1215
1997
|
const key = `${propertyDefinition.elementName}.${suffix}`;
|
|
@@ -1219,28 +2001,44 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
1219
2001
|
}
|
|
1220
2002
|
}
|
|
1221
2003
|
for (const trigger of scripted.triggerDefinitions) {
|
|
1222
|
-
|
|
2004
|
+
const evaluated = trigger.conditionExpression === undefined
|
|
2005
|
+
? undefined
|
|
2006
|
+
: evaluateGuiCondition(trigger.conditionExpression, conditionScenario, subject, conditionDefinitions);
|
|
2007
|
+
const result = evaluated === undefined
|
|
2008
|
+
? trigger.constantResult
|
|
2009
|
+
: evaluated.state === 'unresolved'
|
|
2010
|
+
? undefined
|
|
2011
|
+
: evaluated.state === 'true';
|
|
2012
|
+
if (result === undefined) {
|
|
2013
|
+
addFidelity(fidelity, 'unresolved', 'scripted_gui_trigger', `${trigger.name} requires more scenario facts.`);
|
|
1223
2014
|
continue;
|
|
2015
|
+
}
|
|
1224
2016
|
if (trigger.name.endsWith('_visible'))
|
|
1225
|
-
constantElementVisibility[trigger.elementName] =
|
|
2017
|
+
constantElementVisibility[trigger.elementName] = result;
|
|
1226
2018
|
if (trigger.name.endsWith('_click_enabled'))
|
|
1227
|
-
constantElementEnabled[trigger.elementName] =
|
|
1228
|
-
addFidelity(fidelity, 'modelled',
|
|
2019
|
+
constantElementEnabled[trigger.elementName] = result;
|
|
2020
|
+
addFidelity(fidelity, 'modelled', trigger.constantResult === undefined
|
|
2021
|
+
? 'scripted_gui_trigger'
|
|
2022
|
+
: 'scripted_gui_constant_trigger', `${trigger.name} resolves to ${result ? 'yes' : 'no'}.`);
|
|
1229
2023
|
}
|
|
1230
2024
|
addFidelity(fidelity, 'modelled', 'scripted_gui_context', `${scripted.name} uses ${scripted.contextType ?? '<unspecified>'} context.`);
|
|
1231
2025
|
if (scripted.visibleExpression === undefined)
|
|
1232
2026
|
continue;
|
|
2027
|
+
const evaluatedVisibility = evaluateGuiCondition(scripted.visibleExpression, conditionScenario, subject, conditionDefinitions);
|
|
1233
2028
|
const mockedVisibility = scenario.visibility[scripted.name] ??
|
|
1234
2029
|
scenario.values[`${scripted.name}.visible`] ??
|
|
1235
|
-
scenario.scriptedGui[`${scripted.name}.visible`]
|
|
2030
|
+
scenario.scriptedGui[`${scripted.name}.visible`] ??
|
|
2031
|
+
(evaluatedVisibility.state === 'unresolved'
|
|
2032
|
+
? undefined
|
|
2033
|
+
: evaluatedVisibility.state === 'true');
|
|
1236
2034
|
if (typeof mockedVisibility === 'boolean') {
|
|
1237
2035
|
if (scripted.windowName !== undefined)
|
|
1238
2036
|
scriptedWindowVisibility[scripted.windowName] =
|
|
1239
2037
|
(scriptedWindowVisibility[scripted.windowName] ?? true) && mockedVisibility;
|
|
1240
|
-
addFidelity(fidelity, 'modelled', 'scripted_gui_visibility', `${scripted.name}.visible
|
|
2038
|
+
addFidelity(fidelity, 'modelled', 'scripted_gui_visibility', `${scripted.name}.visible resolves to ${mockedVisibility}.`);
|
|
1241
2039
|
}
|
|
1242
2040
|
else {
|
|
1243
|
-
addFidelity(fidelity, 'unresolved', 'scripted_gui_visibility', `${scripted.name}.visible requires
|
|
2041
|
+
addFidelity(fidelity, 'unresolved', 'scripted_gui_visibility', `${scripted.name}.visible requires more scenario facts; its source visibility remains unresolved.`);
|
|
1244
2042
|
}
|
|
1245
2043
|
}
|
|
1246
2044
|
const layoutScenario = {
|
|
@@ -1276,6 +2074,7 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
1276
2074
|
localisation,
|
|
1277
2075
|
output,
|
|
1278
2076
|
instancesById: new Map(),
|
|
2077
|
+
geometry: new Map(),
|
|
1279
2078
|
work: new GuiSceneWorkBudget(),
|
|
1280
2079
|
baseScale,
|
|
1281
2080
|
};
|
|
@@ -1308,7 +2107,7 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
1308
2107
|
if (parentScene === undefined || attachedDefinition === undefined)
|
|
1309
2108
|
continue;
|
|
1310
2109
|
if (!context.instancesById.has(attachedDefinition.id)) {
|
|
1311
|
-
await layoutElement(attachedDefinition, parentScene.unclippedRect, parentScene.unclippedRect, parentScene.scale, parentScene.depth + 1, context, '', undefined, undefined, parentScene.id, 'upper_left', parentScene.
|
|
2110
|
+
await layoutElement(attachedDefinition, parentScene.unclippedRect, parentScene.unclippedRect, parentScene.scale, parentScene.depth + 1, context, '', undefined, undefined, parentScene.id, 'upper_left', context.geometry.get(parentScene.id).appearanceVisible, [...context.geometry.get(parentScene.id).clipChain, parentScene.unclippedRect]);
|
|
1312
2111
|
addFidelity(fidelity, 'modelled', 'parent_scripted_gui_attachment', `${scripted.windowName} is attached to ${parentWindowName} through ${scripted.parentScriptedGui}.`, attachedDefinition);
|
|
1313
2112
|
}
|
|
1314
2113
|
pendingAttached.splice(index, 1);
|
|
@@ -1324,8 +2123,40 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
1324
2123
|
...(scripted.location === undefined ? {} : { location: scripted.location }),
|
|
1325
2124
|
});
|
|
1326
2125
|
}
|
|
2126
|
+
refreshSceneClips(context);
|
|
1327
2127
|
output.sort((left, right) => left.zIndex - right.zIndex || compareCodeUnits(left.id, right.id));
|
|
1328
|
-
const bounds = unionRects(output.
|
|
2128
|
+
const bounds = unionRects(output.flatMap((element) => {
|
|
2129
|
+
if (!element.visible)
|
|
2130
|
+
return [];
|
|
2131
|
+
const painted = element.sprite === undefined && element.progressColours === undefined ? [] : [element.rect];
|
|
2132
|
+
const text = element.text;
|
|
2133
|
+
if (text !== undefined && text.text.length > 0) {
|
|
2134
|
+
const rect = element.unclippedRect;
|
|
2135
|
+
let textRect = {
|
|
2136
|
+
x: rect.x +
|
|
2137
|
+
(text.horizontalAlignment === 'center'
|
|
2138
|
+
? (rect.width - text.measuredWidth) / 2
|
|
2139
|
+
: text.horizontalAlignment === 'right'
|
|
2140
|
+
? rect.width - text.measuredWidth
|
|
2141
|
+
: 0),
|
|
2142
|
+
y: rect.y +
|
|
2143
|
+
(text.verticalAlignment === 'center'
|
|
2144
|
+
? (rect.height - text.measuredHeight) / 2
|
|
2145
|
+
: text.verticalAlignment === 'bottom'
|
|
2146
|
+
? rect.height - text.measuredHeight
|
|
2147
|
+
: 0),
|
|
2148
|
+
width: text.measuredWidth,
|
|
2149
|
+
height: text.measuredHeight,
|
|
2150
|
+
};
|
|
2151
|
+
if (text.fixedSize)
|
|
2152
|
+
textRect = rectIntersection(textRect, rect);
|
|
2153
|
+
if (textRect !== undefined && element.clipRect !== undefined)
|
|
2154
|
+
textRect = rectIntersection(textRect, element.clipRect);
|
|
2155
|
+
if (textRect !== undefined)
|
|
2156
|
+
painted.push(textRect);
|
|
2157
|
+
}
|
|
2158
|
+
return painted;
|
|
2159
|
+
}));
|
|
1329
2160
|
return {
|
|
1330
2161
|
windowName,
|
|
1331
2162
|
scenario,
|