@pixel-point/toolcraft 0.0.7 → 0.0.8
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/package.json +1 -1
- package/templates/runtime/contracts/component-contracts.test.ts +84 -12
- package/templates/runtime/contracts/component-contracts.ts +40 -17
- package/templates/runtime/contracts/decision-contracts.ts +1 -1
- package/templates/runtime/export/export.test.ts +31 -0
- package/templates/runtime/export/export.ts +41 -0
- package/templates/runtime/react/canvas-shell.test.tsx +77 -1
- package/templates/runtime/react/canvas-shell.tsx +178 -23
- package/templates/runtime/react/control-conditions.ts +166 -0
- package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
- package/templates/runtime/react/controls-panel.test.tsx +404 -8
- package/templates/runtime/react/controls-panel.tsx +90 -4
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +1 -0
- package/templates/runtime/schema/define-toolcraft.ts +4 -2
- package/templates/runtime/schema/types.ts +4 -0
- package/templates/runtime/state/reducer.test.ts +304 -0
- package/templates/runtime/state/reducer.ts +148 -9
- package/templates/runtime/state/types.ts +10 -2
- package/templates/runtime/testing/performance.test.ts +1282 -48
- package/templates/runtime/testing/performance.ts +676 -39
- package/templates/starter/AGENTS.md +6 -5
- package/templates/starter/docs/toolcraft/acceptance-testing.md +11 -5
- package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -2
- package/templates/starter/docs/toolcraft/component-rules.md +16 -7
- package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
- package/templates/starter/docs/toolcraft/performance.md +47 -5
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +6 -6
- package/templates/starter/docs/toolcraft/workflow.md +2 -2
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/package.json +3 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +529 -19
- package/templates/starter/src/app/starter-acceptance.ts +269 -12
- package/templates/starter/src/app/starter-performance.test.ts +66 -5
- package/templates/ui/components/controls/actions/actions-control.tsx +3 -5
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
- package/templates/ui/components/controls/file-drop/index.ts +1 -1
- package/templates/ui/components/controls/index.ts +1 -0
- package/templates/ui/components/panel/panel-section.tsx +53 -1
|
@@ -150,6 +150,7 @@ const builtInToolcraftControlTypeValues = [
|
|
|
150
150
|
"channelMixer",
|
|
151
151
|
"checkbox",
|
|
152
152
|
"code",
|
|
153
|
+
"collectionActions",
|
|
153
154
|
"color",
|
|
154
155
|
"colorOpacity",
|
|
155
156
|
"curves",
|
|
@@ -385,6 +386,34 @@ function getAcceptanceEvidenceText(entry: ToolcraftComponentAcceptance): string
|
|
|
385
386
|
].join(" ");
|
|
386
387
|
}
|
|
387
388
|
|
|
389
|
+
function getFileDropLifecycleCoverageErrors(
|
|
390
|
+
label: string,
|
|
391
|
+
entry: ToolcraftComponentAcceptance,
|
|
392
|
+
): string[] {
|
|
393
|
+
const errors: string[] = [];
|
|
394
|
+
const evidenceText = getAcceptanceEvidenceText(entry);
|
|
395
|
+
|
|
396
|
+
if (entry.evidence !== "media-lifecycle") {
|
|
397
|
+
errors.push(
|
|
398
|
+
`${label} fileDrop acceptance evidence must be "media-lifecycle" so upload, clear, and reset behavior cannot be replaced by generic product-output coverage.`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (
|
|
403
|
+
!/\b(upload|import|drop|drag|browse|choose|select file|source image)\b/i.test(
|
|
404
|
+
evidenceText,
|
|
405
|
+
) ||
|
|
406
|
+
!/\b(clear|remove|delete|trash)\b/i.test(evidenceText) ||
|
|
407
|
+
!/\b(reset|reset controls|section reset|global reset)\b/i.test(evidenceText)
|
|
408
|
+
) {
|
|
409
|
+
errors.push(
|
|
410
|
+
`${label} fileDrop acceptance must prove upload/import, clear/remove, and section or global reset remove source media.`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return errors;
|
|
415
|
+
}
|
|
416
|
+
|
|
388
417
|
const conditionOperatorLabels = [
|
|
389
418
|
"equals",
|
|
390
419
|
"notEquals",
|
|
@@ -520,9 +549,109 @@ function isCustomToolcraftControl(control: ToolcraftControlSchema): boolean {
|
|
|
520
549
|
return !builtInToolcraftControlTypes.has(control.type);
|
|
521
550
|
}
|
|
522
551
|
|
|
552
|
+
const collectionEntityCustomControlRe =
|
|
553
|
+
/\b(collection|repeatable|list|lists|item|items|entry|entries|row|rows|asset|assets|object|objects|color|colors|swatch|swatches|glyph|glyphs|symbol|symbols|point|points|stop|stops|variant|variants|rule|rules|mask|masks|shape|shapes|layer|layers|media|image|images|file|files)\b/i;
|
|
554
|
+
const collectionOperationCustomControlRe =
|
|
555
|
+
/\b(add|adding|delete|deleting|remove|removing|reorder|reordering|order|ordering|sort|sorting|select|selecting|selected|selection|duplicate|duplicating|upload|import|clear|clearing)\b/i;
|
|
556
|
+
|
|
557
|
+
const actionLikeCustomControlRe =
|
|
558
|
+
/\b(add|adding|delete|deleting|remove|removing|duplicate|duplicating|sort|sorting|normalize|normalizing|clear|clearing|reset|shuffle|randomize|randomizing)\b/i;
|
|
559
|
+
|
|
560
|
+
const chromeOnlyCustomControlReasonRe =
|
|
561
|
+
/\b(icon|icons|visual|style|styling|layout|spacing|chrome|button|buttons|compact|custom look|custom ui)\b/i;
|
|
562
|
+
const productInteractionCustomControlReasonRe =
|
|
563
|
+
/\b(runtime|state|canvas|output|export|upload|import|preview|reorder|ordering|sort|drag|resize|handle|threshold|density|mapping|geometry|nested|multi|multiple|per-item|metadata|hit target|validation|selection)\b/i;
|
|
564
|
+
const collectionValueKeyRe =
|
|
565
|
+
/^(items?|entries|rows|assets|objects|colors?|glyphs?|symbols?|points?|stops?|variants?|rules?|masks?|shapes?|layers?|media|images?|files?)$/i;
|
|
566
|
+
|
|
567
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
568
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function stringifyUnknownForFitCheck(value: unknown): string {
|
|
572
|
+
if (value == null) {
|
|
573
|
+
return "";
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
577
|
+
return String(value);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (Array.isArray(value)) {
|
|
581
|
+
return value.map(stringifyUnknownForFitCheck).join(" ");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (isRecord(value)) {
|
|
585
|
+
return Object.entries(value)
|
|
586
|
+
.flatMap(([key, entryValue]) => [key, stringifyUnknownForFitCheck(entryValue)])
|
|
587
|
+
.join(" ");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return "";
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function arrayLooksLikeCollectionValue(value: readonly unknown[]): boolean {
|
|
594
|
+
return (
|
|
595
|
+
value.length === 0 ||
|
|
596
|
+
value.some((item) => isRecord(item) || Array.isArray(item) || typeof item === "string")
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function hasCollectionValueShape(value: unknown): boolean {
|
|
601
|
+
if (!isRecord(value)) {
|
|
602
|
+
return Array.isArray(value) && arrayLooksLikeCollectionValue(value);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return Object.entries(value).some(([key, entryValue]) => {
|
|
606
|
+
if (Array.isArray(entryValue)) {
|
|
607
|
+
return collectionValueKeyRe.test(key) || arrayLooksLikeCollectionValue(entryValue);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return collectionValueKeyRe.test(key) && isRecord(entryValue);
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function getCustomFitCheckSearchText(
|
|
615
|
+
entry: ToolcraftComponentAcceptance,
|
|
616
|
+
control?: ToolcraftControlSchema,
|
|
617
|
+
): string {
|
|
618
|
+
return [
|
|
619
|
+
control?.type,
|
|
620
|
+
control?.target,
|
|
621
|
+
typeof control?.label === "string" ? control.label : undefined,
|
|
622
|
+
stringifyUnknownForFitCheck(control?.defaultValue),
|
|
623
|
+
entry.id,
|
|
624
|
+
entry.target,
|
|
625
|
+
entry.componentType,
|
|
626
|
+
entry.expectedObservable,
|
|
627
|
+
entry.userAction,
|
|
628
|
+
entry.builtInFitCheck?.whyInsufficient,
|
|
629
|
+
entry.builtInFitCheck?.productObservable,
|
|
630
|
+
]
|
|
631
|
+
.filter(Boolean)
|
|
632
|
+
.join(" ");
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function isCollectionLikeCustomControl(
|
|
636
|
+
entry: ToolcraftComponentAcceptance,
|
|
637
|
+
control: ToolcraftControlSchema,
|
|
638
|
+
): boolean {
|
|
639
|
+
if (hasCollectionValueShape(control.defaultValue)) {
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const searchText = getCustomFitCheckSearchText(entry, control);
|
|
644
|
+
|
|
645
|
+
return (
|
|
646
|
+
collectionEntityCustomControlRe.test(searchText) &&
|
|
647
|
+
collectionOperationCustomControlRe.test(searchText)
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
523
651
|
function getBuiltInFitCheckErrors(
|
|
524
652
|
label: string,
|
|
525
653
|
entry: ToolcraftComponentAcceptance,
|
|
654
|
+
control: ToolcraftControlSchema,
|
|
526
655
|
): string[] {
|
|
527
656
|
const fitCheck = entry.builtInFitCheck;
|
|
528
657
|
|
|
@@ -574,6 +703,32 @@ function getBuiltInFitCheckErrors(
|
|
|
574
703
|
);
|
|
575
704
|
}
|
|
576
705
|
|
|
706
|
+
const searchText = getCustomFitCheckSearchText(entry, control);
|
|
707
|
+
|
|
708
|
+
if (
|
|
709
|
+
isCollectionLikeCustomControl(entry, control) &&
|
|
710
|
+
!checkedBuiltIns.includes("collectionActions")
|
|
711
|
+
) {
|
|
712
|
+
errors.push(
|
|
713
|
+
`${label} builtInFitCheck.checkedBuiltIns must include collectionActions when the custom control owns a growable, removable, selectable, or reorderable runtime item set.`,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (actionLikeCustomControlRe.test(searchText) && !checkedBuiltIns.includes("actions")) {
|
|
718
|
+
errors.push(
|
|
719
|
+
`${label} builtInFitCheck.checkedBuiltIns must include actions when the custom control exposes local command buttons such as add, remove, delete, duplicate, sort, normalize, or clear.`,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (
|
|
724
|
+
chromeOnlyCustomControlReasonRe.test(fitCheck.whyInsufficient) &&
|
|
725
|
+
!productInteractionCustomControlReasonRe.test(fitCheck.whyInsufficient)
|
|
726
|
+
) {
|
|
727
|
+
errors.push(
|
|
728
|
+
`${label} builtInFitCheck.whyInsufficient cannot justify a custom control only with icons, layout, styling, or custom buttons; name the product interaction or value model that built-ins cannot express.`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
577
732
|
return errors;
|
|
578
733
|
}
|
|
579
734
|
|
|
@@ -714,6 +869,10 @@ function getControlLabelText(control: ToolcraftControlSchema): string {
|
|
|
714
869
|
return typeof control.label === "string" ? control.label : "";
|
|
715
870
|
}
|
|
716
871
|
|
|
872
|
+
function hasVisibleControlLabel(control: ToolcraftControlSchema): boolean {
|
|
873
|
+
return typeof control.label === "string" && control.label.trim().length > 0;
|
|
874
|
+
}
|
|
875
|
+
|
|
717
876
|
const singleCurveSemanticPattern =
|
|
718
877
|
/\b(acceleration|accel|bend|easing|ease|response|depth|mask|opacity|alpha|motion|velocity|threshold|falloff|remap|remapping)\b|speed\s+profile|mapping\s+curve|curve\s+mapping/i;
|
|
719
878
|
const rgbCurveSemanticPattern =
|
|
@@ -1603,13 +1762,23 @@ function getToolcraftControlDescriptionError({
|
|
|
1603
1762
|
}
|
|
1604
1763
|
|
|
1605
1764
|
function isToolcraftColorSectionTitle(sectionTitle: string | undefined): boolean {
|
|
1606
|
-
return /\b(colou?rs?|palette|palettes)\b/i.test(
|
|
1765
|
+
return /\b(colou?rs?|palette|palettes|shades?|accents?)\b/i.test(
|
|
1766
|
+
sectionTitle ?? "",
|
|
1767
|
+
);
|
|
1607
1768
|
}
|
|
1608
1769
|
|
|
1609
1770
|
function isToolcraftSequentialColorLabel(label: string): boolean {
|
|
1610
1771
|
return /^colou?r\s+\d+$/i.test(label.trim());
|
|
1611
1772
|
}
|
|
1612
1773
|
|
|
1774
|
+
function isToolcraftPaletteVariationTarget(target: string): boolean {
|
|
1775
|
+
return (
|
|
1776
|
+
/(?:^|\.)(?:palette|palettes|colou?rs?|shades?|accents?)\b/i.test(
|
|
1777
|
+
target,
|
|
1778
|
+
) || /(?:^|\.)(?:accent|shade|colou?r)\d+\b/i.test(target)
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1613
1782
|
function isToolcraftSimplePaletteDistributionLabel(label: string): boolean {
|
|
1614
1783
|
return /^(spread|mix|distribution)$/i.test(label.trim());
|
|
1615
1784
|
}
|
|
@@ -1648,6 +1817,76 @@ function isToolcraftObviousColorSectionControlDescription({
|
|
|
1648
1817
|
);
|
|
1649
1818
|
}
|
|
1650
1819
|
|
|
1820
|
+
function getToolcraftColorBankLabelErrors({
|
|
1821
|
+
controls,
|
|
1822
|
+
sectionLabel,
|
|
1823
|
+
sectionTitle,
|
|
1824
|
+
}: {
|
|
1825
|
+
controls: readonly [string, ToolcraftControlSchema][];
|
|
1826
|
+
sectionLabel: string;
|
|
1827
|
+
sectionTitle: string | undefined;
|
|
1828
|
+
}): string[] {
|
|
1829
|
+
const colorControls = controls.filter(([, control]) => {
|
|
1830
|
+
if (control.type !== "color" && control.type !== "colorOpacity") {
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
return true;
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
if (colorControls.length < 2) {
|
|
1838
|
+
return [];
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
const loosePrefixes = new Set(
|
|
1842
|
+
colorControls
|
|
1843
|
+
.map(([, control]) => getToolcraftLooseTargetPrefix(control.target))
|
|
1844
|
+
.filter((prefix): prefix is string => Boolean(prefix)),
|
|
1845
|
+
);
|
|
1846
|
+
|
|
1847
|
+
if (loosePrefixes.size !== 1) {
|
|
1848
|
+
return [];
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
const sequentialColorControls = colorControls.filter(([, control]) =>
|
|
1852
|
+
isToolcraftSequentialColorLabel(getControlLabelText(control)),
|
|
1853
|
+
);
|
|
1854
|
+
const isPaletteVariationBank =
|
|
1855
|
+
colorControls.every(([, control]) =>
|
|
1856
|
+
isToolcraftPaletteVariationTarget(control.target),
|
|
1857
|
+
) ||
|
|
1858
|
+
(isToolcraftColorSectionTitle(sectionTitle) &&
|
|
1859
|
+
sequentialColorControls.length > 0);
|
|
1860
|
+
|
|
1861
|
+
if (!isPaletteVariationBank) {
|
|
1862
|
+
return [];
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const visibleColorControls = colorControls.filter(([, control]) =>
|
|
1866
|
+
hasVisibleControlLabel(control),
|
|
1867
|
+
);
|
|
1868
|
+
const errors: string[] = [];
|
|
1869
|
+
|
|
1870
|
+
if (
|
|
1871
|
+
visibleColorControls.length > 0 &&
|
|
1872
|
+
visibleColorControls.length < colorControls.length
|
|
1873
|
+
) {
|
|
1874
|
+
errors.push(
|
|
1875
|
+
`${sectionLabel} mixes labeled and unlabeled color items in one palette variation group. Decide label visibility for the whole group: omit all per-item labels when colors only add variety, or label every item only when each color has a distinct user-facing role.`,
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
for (const [controlId, control] of sequentialColorControls) {
|
|
1880
|
+
const label = getControlLabelText(control).trim();
|
|
1881
|
+
|
|
1882
|
+
errors.push(
|
|
1883
|
+
`${sectionLabel} / ${controlId} uses visible label "${label}" for a palette variation color. When colors only add variety to one shared palette, set label: false or use collectionActions with unlabeled items. Keep visible labels only when each color edits a distinct user-facing entity such as Fill, Stroke, Background, Connector, or Object color.`,
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
return errors;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1651
1890
|
function getToolcraftControlSectionGroupingErrors(
|
|
1652
1891
|
schema: ResolvedToolcraftAppSchema,
|
|
1653
1892
|
): string[] {
|
|
@@ -1737,6 +1976,14 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
1737
1976
|
);
|
|
1738
1977
|
}
|
|
1739
1978
|
|
|
1979
|
+
errors.push(
|
|
1980
|
+
...getToolcraftColorBankLabelErrors({
|
|
1981
|
+
controls,
|
|
1982
|
+
sectionLabel,
|
|
1983
|
+
sectionTitle,
|
|
1984
|
+
}),
|
|
1985
|
+
);
|
|
1986
|
+
|
|
1740
1987
|
for (const [controlId, control] of controls) {
|
|
1741
1988
|
const strictPrefix = getToolcraftStrictTargetPrefix(control.target);
|
|
1742
1989
|
const loosePrefix = getToolcraftLooseTargetPrefix(control.target);
|
|
@@ -2121,7 +2368,7 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2121
2368
|
|
|
2122
2369
|
if (!schemaHasOutputBackgroundToggleControl(controls)) {
|
|
2123
2370
|
errors.push(
|
|
2124
|
-
'Product apps with Export PNG must expose export.includeBackground inside the required "Background" section as a Switch labeled "Include". PNG export must pass that runtime value to createToolcraftPngExportCanvas includeBackground; video export keeps the background.',
|
|
2371
|
+
'Product apps with Export PNG must expose export.includeBackground inside the required "Background" section as a Switch labeled "Include". PNG export must pass that runtime value to createToolcraftPngExportCanvas includeBackground; live preview must use shouldIncludeToolcraftPreviewBackground(state); video export keeps the background.',
|
|
2125
2372
|
);
|
|
2126
2373
|
}
|
|
2127
2374
|
|
|
@@ -2798,6 +3045,10 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2798
3045
|
);
|
|
2799
3046
|
}
|
|
2800
3047
|
|
|
3048
|
+
if (control.type === "fileDrop") {
|
|
3049
|
+
errors.push(...getFileDropLifecycleCoverageErrors(label, entry));
|
|
3050
|
+
}
|
|
3051
|
+
|
|
2801
3052
|
if (
|
|
2802
3053
|
isCustomControl &&
|
|
2803
3054
|
!hasCustomControlCoverage(
|
|
@@ -2811,7 +3062,7 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2811
3062
|
}
|
|
2812
3063
|
|
|
2813
3064
|
if (isCustomControl) {
|
|
2814
|
-
errors.push(...getBuiltInFitCheckErrors(label, entry));
|
|
3065
|
+
errors.push(...getBuiltInFitCheckErrors(label, entry, control));
|
|
2815
3066
|
}
|
|
2816
3067
|
|
|
2817
3068
|
if (
|
|
@@ -2841,17 +3092,23 @@ export function validateToolcraftAcceptanceCoverage(
|
|
|
2841
3092
|
isOutputBackgroundToggleControl({ control, controlId, sectionTitle })
|
|
2842
3093
|
) {
|
|
2843
3094
|
const evidenceText = getAcceptanceEvidenceText(entry);
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
3095
|
+
const provesPngTransparency =
|
|
3096
|
+
/\b(png|image)\b/i.test(evidenceText) &&
|
|
3097
|
+
/\b(transparent|transparency|alpha)\b/i.test(evidenceText);
|
|
3098
|
+
const provesPreviewTransparency =
|
|
3099
|
+
/\b(preview|canvas)\b/i.test(evidenceText) &&
|
|
3100
|
+
/\b(transparent|transparency|alpha|hide|hides|hidden|without background|no background|background off)\b/i.test(
|
|
2850
3101
|
evidenceText,
|
|
2851
|
-
)
|
|
2852
|
-
|
|
3102
|
+
);
|
|
3103
|
+
const provesVideoBackground =
|
|
3104
|
+
/\bvideo\b/i.test(evidenceText) &&
|
|
3105
|
+
/\b(keep|keeps|preserve|preserves|stay|stays|remain|remains|still|background)\b/i.test(
|
|
3106
|
+
evidenceText,
|
|
3107
|
+
);
|
|
3108
|
+
|
|
3109
|
+
if (!provesPngTransparency || !provesPreviewTransparency || !provesVideoBackground) {
|
|
2853
3110
|
errors.push(
|
|
2854
|
-
`${label} controls
|
|
3111
|
+
`${label} controls background inclusion and acceptance must prove disabling it makes PNG output transparent, hides the live preview product background, and keeps video output with the product background.`,
|
|
2855
3112
|
);
|
|
2856
3113
|
}
|
|
2857
3114
|
}
|
|
@@ -138,6 +138,18 @@ function projectDocsIncludeRendererLayerInventory(): boolean {
|
|
|
138
138
|
);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
function projectDocsIncludeRendererPipelineInventory(): boolean {
|
|
142
|
+
const decisionSources = readProjectDecisionSources();
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
/Render Pipeline Inventory|rendererPipeline|render pipeline/i.test(decisionSources) &&
|
|
146
|
+
/pass|passes|cacheKey|cache key|invalidat/i.test(decisionSources) &&
|
|
147
|
+
/control-drag|viewport-zoom|viewport-drag|media-import|animation-frame|timeline-playback|interaction/i.test(
|
|
148
|
+
decisionSources,
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
141
153
|
function projectDocsExplainRendererAlternatives(): boolean {
|
|
142
154
|
const decisionSources = readProjectDecisionSources();
|
|
143
155
|
|
|
@@ -198,12 +210,24 @@ function projectDocsIncludeFixedBackgroundDecision(): boolean {
|
|
|
198
210
|
|
|
199
211
|
function sourceUsesCpuPixelLoop(): boolean {
|
|
200
212
|
const appSources = stripJsComments(readFiles(srcDir, /\.(ts|tsx)$/));
|
|
213
|
+
const cpuPixelMethodCallPattern =
|
|
214
|
+
/(?:\.(?:createImageData|getImageData|putImageData)|\[\s*["'](?:createImageData|getImageData|putImageData)["']\s*\])\s*\(/;
|
|
201
215
|
|
|
202
216
|
return (
|
|
203
217
|
/new\s+ImageData\s*\(/.test(appSources) ||
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
218
|
+
cpuPixelMethodCallPattern.test(appSources)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function appPerformanceHasRenderPipelinePass(kind: string): boolean {
|
|
223
|
+
return (starterPerformance.rendererPipeline?.passes ?? []).some(
|
|
224
|
+
(pass) => pass.kind === kind,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function appPerformanceHasInteractionInvalidation(interaction: string): boolean {
|
|
229
|
+
return (starterPerformance.rendererPipeline?.interactionInvalidation ?? []).some(
|
|
230
|
+
(entry) => entry.interaction === interaction,
|
|
207
231
|
);
|
|
208
232
|
}
|
|
209
233
|
|
|
@@ -345,6 +369,17 @@ describe("Toolcraft template app performance coverage", () => {
|
|
|
345
369
|
).toBe(true);
|
|
346
370
|
});
|
|
347
371
|
|
|
372
|
+
it("requires custom renderer apps to document the render pipeline inventory", () => {
|
|
373
|
+
if (!starterPerformance.usesCustomRenderer) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
expect(
|
|
378
|
+
projectDocsIncludeRendererPipelineInventory(),
|
|
379
|
+
"Custom renderer apps must write a Render Pipeline Inventory in their app spec/plan docs before implementation so expensive passes, cache keys, and invalidation rules are deliberate.",
|
|
380
|
+
).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
|
|
348
383
|
it("requires custom renderer apps to mirror the renderer decision in typed performance config", () => {
|
|
349
384
|
if (!starterPerformance.usesCustomRenderer) {
|
|
350
385
|
return;
|
|
@@ -356,6 +391,17 @@ describe("Toolcraft template app performance coverage", () => {
|
|
|
356
391
|
).toBeDefined();
|
|
357
392
|
});
|
|
358
393
|
|
|
394
|
+
it("requires custom renderer apps to mirror the render pipeline in typed performance config", () => {
|
|
395
|
+
if (!starterPerformance.usesCustomRenderer) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
expect(
|
|
400
|
+
starterPerformance.rendererPipeline,
|
|
401
|
+
"Custom renderer apps must declare rendererPipeline in app-performance.ts; prose specs and plans are not enough.",
|
|
402
|
+
).toBeDefined();
|
|
403
|
+
});
|
|
404
|
+
|
|
359
405
|
it("requires custom renderer apps to mirror layer inventory in typed performance config", () => {
|
|
360
406
|
if (!starterPerformance.usesCustomRenderer) {
|
|
361
407
|
return;
|
|
@@ -453,7 +499,7 @@ describe("Toolcraft template app performance coverage", () => {
|
|
|
453
499
|
);
|
|
454
500
|
});
|
|
455
501
|
|
|
456
|
-
it("requires pixel-output renderers to use
|
|
502
|
+
it("requires pixel-output renderers to use GPU or measured CPU evidence", () => {
|
|
457
503
|
expect(
|
|
458
504
|
validateToolcraftPerformanceCoverage(starterSchema, {
|
|
459
505
|
rendererStrategy: "canvas-2d",
|
|
@@ -464,7 +510,7 @@ describe("Toolcraft template app performance coverage", () => {
|
|
|
464
510
|
}),
|
|
465
511
|
).toEqual(
|
|
466
512
|
expect.arrayContaining([
|
|
467
|
-
'rendererWorkload "pixel-output"
|
|
513
|
+
'rendererWorkload "pixel-output" should use rendererStrategy "webgl" or "webgpu", received "canvas-2d". Keeping a CPU renderer requires measured WebGL/WebGPU evidence in rendererTechnique.whyNotAlternativeStrategies or performanceRisks.',
|
|
468
514
|
]),
|
|
469
515
|
);
|
|
470
516
|
});
|
|
@@ -681,6 +727,21 @@ describe("Toolcraft template app performance coverage", () => {
|
|
|
681
727
|
sourceUsesGpuRenderer(),
|
|
682
728
|
"Procedural pixel renderers must contain an actual WebGL/WebGPU code path, not only declare a GPU strategy.",
|
|
683
729
|
).toBe(true);
|
|
730
|
+
expect(
|
|
731
|
+
appPerformanceHasRenderPipelinePass("pixel-transform"),
|
|
732
|
+
"Procedural pixel renderers must declare a rendererPipeline pixel-transform pass so caching and invalidation are machine-checkable.",
|
|
733
|
+
).toBe(true);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("requires custom renderers to declare high-frequency viewport invalidation", () => {
|
|
737
|
+
if (!starterPerformance.usesCustomRenderer) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
expect(
|
|
742
|
+
appPerformanceHasInteractionInvalidation("viewport-zoom"),
|
|
743
|
+
"Custom renderer apps must declare rendererPipeline interactionInvalidation for viewport-zoom so zoom can stay responsive without recomputing expensive passes.",
|
|
744
|
+
).toBe(true);
|
|
684
745
|
});
|
|
685
746
|
|
|
686
747
|
it("requires WebGL/WebGPU renderers to keep their pipeline lifecycle outside React render", () => {
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
|
|
15
15
|
import { Button, Field } from "../../primitives";
|
|
16
16
|
import { ControlFieldLabel } from "../../control-layout";
|
|
17
|
-
import { cn } from "../../../lib/utils";
|
|
18
17
|
|
|
19
18
|
export type ActionControlIconName =
|
|
20
19
|
| "check"
|
|
@@ -102,6 +101,7 @@ export function ActionsControl({
|
|
|
102
101
|
<Field
|
|
103
102
|
aria-label={showLabel ? undefined : name}
|
|
104
103
|
className="h-fit min-w-0 flex-wrap items-center justify-between gap-y-2!"
|
|
104
|
+
data-slot="actions-control"
|
|
105
105
|
orientation="horizontal"
|
|
106
106
|
>
|
|
107
107
|
{showLabel ? (
|
|
@@ -110,10 +110,8 @@ export function ActionsControl({
|
|
|
110
110
|
</ControlFieldLabel>
|
|
111
111
|
) : null}
|
|
112
112
|
<div
|
|
113
|
-
className=
|
|
114
|
-
|
|
115
|
-
showLabel && "ml-auto",
|
|
116
|
-
)}
|
|
113
|
+
className="flex max-w-full shrink-0 flex-wrap items-center justify-start gap-1.5"
|
|
114
|
+
data-slot="actions-control-buttons"
|
|
117
115
|
>
|
|
118
116
|
{actions.map((action) => {
|
|
119
117
|
const actionContent = getActionContent(action);
|