@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
package/package.json
CHANGED
|
@@ -224,6 +224,12 @@ describe("Toolcraft template component contracts", () => {
|
|
|
224
224
|
expect(contract.aiUsageRules).toContain(
|
|
225
225
|
"Do not use a custom control to recreate a built-in Slider, RangeSlider, Select, Segmented, Switch, Checkbox, Color, ColorOpacity, Gradient, FontPicker, ImagePicker, FileDrop, TextInput, CodeTextarea, RangeInput, Palette, Actions, CollectionActions, Curves, AnchorGrid, ChannelMixer, Vector, or PanelActions control.",
|
|
226
226
|
);
|
|
227
|
+
expect(contract.aiUsageRules).toContain(
|
|
228
|
+
"When a custom control owns a growable, removable, selectable, or reorderable runtime item set, its builtInFitCheck must explicitly check collectionActions and actions before choosing custom; this is based on the value model and user workflow, not on entity names such as masks or glyphs.",
|
|
229
|
+
);
|
|
230
|
+
expect(contract.aiUsageRules).toContain(
|
|
231
|
+
"Do not justify custom controls with icons, layout, styling, compactness, or custom buttons alone. The fit check must name the product interaction or value model that built-ins cannot express.",
|
|
232
|
+
);
|
|
227
233
|
expect(contract.aiUsageRules).toContain(
|
|
228
234
|
"Custom controls may use Toolcraft primitives for small app-specific chrome, but must not import or render low-level runtime surfaces or duplicate toolbar, timeline, layers, canvas, panel, or built-in control mechanics.",
|
|
229
235
|
);
|
|
@@ -254,6 +260,12 @@ describe("Toolcraft template component contracts", () => {
|
|
|
254
260
|
expect(contract.aiUsageRules).toContain(
|
|
255
261
|
"Do not use Actions for animation transport; Play, Pause, Resume, Restart, and Scrub belong to the top timeline when timeline behavior exists.",
|
|
256
262
|
);
|
|
263
|
+
expect(contract.decisionCatalog?.layoutConstraints).toContain(
|
|
264
|
+
"When an Actions button group wraps below its label, the wrapped button row starts at the left edge of the control content instead of centering or staying right-aligned.",
|
|
265
|
+
);
|
|
266
|
+
expect(contract.aiUsageRules).toContain(
|
|
267
|
+
"When an Actions button group no longer fits beside its label and wraps to a second row, align the wrapped button row to the left.",
|
|
268
|
+
);
|
|
257
269
|
expect(contract.aiUsageRules).toContain(
|
|
258
270
|
'For local reset-like actions, use product-specific values such as "reset-current-layer" or "reset-palette" and handle them through ToolcraftApp onPanelAction; do not use a bare "reset" value unless the action intentionally runs controls.reset.',
|
|
259
271
|
);
|
|
@@ -286,6 +298,10 @@ describe("Toolcraft template component contracts", () => {
|
|
|
286
298
|
"recommendedMaxItems is advisory only and must not disable the plus button. Use hardMaxItems only when a real product, algorithm, API, export, or measured performance limit requires it.",
|
|
287
299
|
);
|
|
288
300
|
expect(contract.aiUsageRules.join(" ")).toMatch(/TextInput/);
|
|
301
|
+
expect(contract.aiUsageRules.join(" ")).toMatch(/FontPicker/);
|
|
302
|
+
expect(contract.aiUsageRules).toContain(
|
|
303
|
+
"Use FontPicker as the collection item control when each repeated item is a typography/text-style entity; do not split its font, color, opacity, size, case, letter-spacing, or line-height into sibling collection fields.",
|
|
304
|
+
);
|
|
289
305
|
});
|
|
290
306
|
|
|
291
307
|
it("documents Palette as a constrained design-token color control", () => {
|
|
@@ -568,22 +584,31 @@ describe("Toolcraft template component contracts", () => {
|
|
|
568
584
|
'Pair renderer-owned output background color with export.includeBackground in one Background section directly before export settings. Use an equal-width inline row with the export.includeBackground Switch labeled "Include" on the left and the background Color parameter with label false on the right; each control occupies one half of the row.',
|
|
569
585
|
);
|
|
570
586
|
expect(color.aiUsageRules).toContain(
|
|
571
|
-
"Preview, PNG export, and video export must read the runtime background color value instead of hardcoding that background in CSS, Canvas fillStyle, or WebGL clearColor. export.includeBackground controls
|
|
587
|
+
"Preview, PNG export, and video export must read the runtime background color value instead of hardcoding that background in CSS, Canvas fillStyle, or WebGL clearColor. export.includeBackground controls live preview product background and PNG alpha; it must not make the Toolcraft canvas shell/backing or video output transparent.",
|
|
572
588
|
);
|
|
573
589
|
expect(color.aiUsageRules).toContain(
|
|
574
590
|
"When one short numeric/text field and one Color field configure the same entity, keep them in one two-column inline layout group.",
|
|
575
591
|
);
|
|
576
592
|
expect(color.aiUsageRules).toContain(
|
|
577
|
-
'Mixed inline rows require visible labels on both controls. The required Background row is the
|
|
593
|
+
'Mixed inline rows require visible labels on both controls. The required Background row is the section-title-owned exception: use the Switch label "Include" and set the background Color control label to false. Palette variation color banks are the other exception when the group/section label already names the color bank.',
|
|
578
594
|
);
|
|
579
595
|
expect(color.aiUsageRules).toContain(
|
|
580
596
|
"Plain Color popovers must not show opacity controls. If opacity is editable, use ColorOpacity instead.",
|
|
581
597
|
);
|
|
582
598
|
expect(color.aiUsageRules).toContain(
|
|
583
|
-
"
|
|
599
|
+
"Decide color label visibility from the user's point of view. Omit labels for color banks that only add palette variety, such as Accent Shades, Bead Colors, or palette.accent1..5.",
|
|
600
|
+
);
|
|
601
|
+
expect(color.aiUsageRules).toContain(
|
|
602
|
+
"Make color label visibility a group-level decision: do not mix labeled and unlabeled items inside one semantic color bank.",
|
|
584
603
|
);
|
|
585
604
|
expect(color.aiUsageRules).toContain(
|
|
586
|
-
"
|
|
605
|
+
"Keep visible labels when colors edit distinct user-facing entities or roles, such as Fill, Stroke, Background, Connector, Object, or Highlight.",
|
|
606
|
+
);
|
|
607
|
+
expect(color.aiUsageRules).toContain(
|
|
608
|
+
"A color bank can share a section with distribution controls such as Spread, Mix, or Randomness and still keep each color item unlabeled when the section title names the palette context.",
|
|
609
|
+
);
|
|
610
|
+
expect(color.aiUsageRules).toContain(
|
|
611
|
+
"If a multi-color bank has an odd trailing plain Color, keep that last Color at half width; only ColorOpacity or intentionally full-width compound controls occupy a full row.",
|
|
587
612
|
);
|
|
588
613
|
expect(gradient.aiUsageRules).toContain(
|
|
589
614
|
"Gradient is a compound control; acceptance must prove gradient.gradientType, gradient.angle, gradient.stops.position, gradient.stops.color, and gradient.stops.opacity all affect the product output when visible.",
|
|
@@ -600,6 +625,9 @@ describe("Toolcraft template component contracts", () => {
|
|
|
600
625
|
expect(fontPicker.aiUsageRules).toContain(
|
|
601
626
|
'Do not recreate FontPicker with a plain Select plus separate sliders; use type: "fontPicker" so the popup mechanics and footer controls stay intact.',
|
|
602
627
|
);
|
|
628
|
+
expect(fontPicker.aiUsageRules).toContain(
|
|
629
|
+
"FontPicker standard/default text color is #FFFFFF with opacity 100; omit color/opacity or use those values unless the prompt or reference explicitly requires a different initial text color.",
|
|
630
|
+
);
|
|
603
631
|
expect(fontPicker.aiUsageRules).toContain(
|
|
604
632
|
"Any product text controlled by FontPicker must render fontId, fontWeight, fontSize, letterSpacing, lineHeight, textCase, color, and opacity in preview and export; do not leave typography values as panel-only runtime state.",
|
|
605
633
|
);
|
|
@@ -743,14 +771,20 @@ describe("Toolcraft template component contracts", () => {
|
|
|
743
771
|
"Custom renderers must define performance budgets for media import, preview updates, control drags, and export/copy before implementation.",
|
|
744
772
|
);
|
|
745
773
|
expect(contract.aiUsageRules).toContain(
|
|
746
|
-
"Controls that change renderer workload
|
|
774
|
+
"Controls that change renderer workload by changing output dimensions, element count, density, sample count, iteration count, blur/filter radius, shader branch cost, media processing, text layout, or export quality must be tested at min, default, and max values.",
|
|
747
775
|
);
|
|
748
776
|
expect(contract.aiUsageRules).toContain(
|
|
749
|
-
"
|
|
777
|
+
"For workload control scenarios, stressFixture is the tested control value. If the app has an independent heavy baseline such as large media, long text, many items, high render scale, or dense source state, declare workloadFixture and apply it before the measured control interaction.",
|
|
778
|
+
);
|
|
779
|
+
expect(contract.aiUsageRules).toContain(
|
|
780
|
+
"Hash differs is not enough for workload controls; tests must assert semantic direction, such as density increasing item count or size changes reducing/increasing rendered cells.",
|
|
750
781
|
);
|
|
751
782
|
expect(contract.aiUsageRules).toContain(
|
|
752
783
|
"Performance tests must use representative fixtures and the same renderer/export path as the running app, not only tiny 32px fixtures or isolated helper state.",
|
|
753
784
|
);
|
|
785
|
+
expect(contract.aiUsageRules).toContain(
|
|
786
|
+
'Media-import workload fixtures and media workload baselines must use fixture kind "media" with numeric width and height at least 1920x1080-equivalent; 640x480 preview fixtures cannot satisfy upload, effect-control, or image-processing performance coverage.',
|
|
787
|
+
);
|
|
754
788
|
expect(contract.aiUsageRules).toContain(
|
|
755
789
|
"Slider drags and high-frequency controls must debounce or coalesce preview work, cancel stale async renders, and avoid re-decoding media on every control change.",
|
|
756
790
|
);
|
|
@@ -790,8 +824,19 @@ describe("Toolcraft template component contracts", () => {
|
|
|
790
824
|
expect(contract.aiUsageRules).toContain(
|
|
791
825
|
'productRepresentation "mixed" is valid only when rendererTechnique.layers proves at least two different content families.',
|
|
792
826
|
);
|
|
827
|
+
expect(contract.aiUsageRules).toContain(
|
|
828
|
+
"Custom renderer apps must declare rendererPipeline with render passes, cache keys, execution location, preview/export quality, and interaction invalidation before implementation.",
|
|
829
|
+
);
|
|
830
|
+
expect(contract.aiUsageRules).toContain(
|
|
831
|
+
"Render Pipeline Inventory must explain which runtime targets invalidate each expensive pass; high-frequency interactions such as animation frames, drag, zoom, pan, timeline playback, and mask movement must not invalidate upstream decode/preprocess/pixel-transform work unless that target truly changes the upstream result.",
|
|
832
|
+
);
|
|
833
|
+
expect(contract.aiUsageRules).toContain(
|
|
834
|
+
"Cache-sensitive render passes such as decode, preprocess, pixel-transform, text-layout, rasterize, and composite must declare cache keys so tests can reject full recomputation on every control change.",
|
|
835
|
+
);
|
|
793
836
|
expect(contract.aiUsageRules.join("\n")).toMatch(/rendererTechnique/);
|
|
794
837
|
expect(contract.aiUsageRules.join("\n")).toMatch(/rendererTechnique\.layers/);
|
|
838
|
+
expect(contract.aiUsageRules.join("\n")).toMatch(/rendererPipeline/);
|
|
839
|
+
expect(contract.aiUsageRules.join("\n")).toMatch(/Render Pipeline Inventory/);
|
|
795
840
|
expect(contract.aiUsageRules.join("\n")).toMatch(/sourceRepresentation/);
|
|
796
841
|
expect(contract.aiUsageRules.join("\n")).toMatch(/productRepresentation/);
|
|
797
842
|
expect(contract.aiUsageRules.join("\n")).toMatch(/previewRenderer/);
|
|
@@ -809,11 +854,14 @@ describe("Toolcraft template component contracts", () => {
|
|
|
809
854
|
"Text-output and vector-output visible previews must preserve native output fidelity. Do not render a low-resolution offscreen canvas or texture and upscale it to the product size.",
|
|
810
855
|
);
|
|
811
856
|
expect(contract.aiUsageRules).toContain(
|
|
812
|
-
"Pixel-output renderers must
|
|
857
|
+
"Pixel-output renderers must treat WebGL/WebGPU as the default candidate even when the scene is static; Canvas 2D is allowed only when measured worst-case evidence shows the CPU path preserves quality and remains responsive.",
|
|
813
858
|
);
|
|
814
859
|
expect(contract.aiUsageRules).toContain(
|
|
815
860
|
"Procedural pixel renderers, shader-like effects, animated mesh gradients, and large exportable previews should use WebGL or WebGPU for pixel work instead of main-thread ImageData loops.",
|
|
816
861
|
);
|
|
862
|
+
expect(contract.aiUsageRules).toContain(
|
|
863
|
+
"Detail-heavy Canvas 2D pixel/media renderers may stay on CPU only when rendererTechnique records measured stress evidence for rejecting WebGL/WebGPU; if the heavy media stress fails, move pixel work to GPU instead of lowering quality.",
|
|
864
|
+
);
|
|
817
865
|
expect(contract.aiUsageRules).toContain(
|
|
818
866
|
"WebGL and WebGPU renderers must initialize contexts, programs, shaders, pipelines, textures, and large buffers once, then update uniforms or stable buffers when controls change.",
|
|
819
867
|
);
|
|
@@ -830,11 +878,14 @@ describe("Toolcraft template component contracts", () => {
|
|
|
830
878
|
"Animated preview renderers must suspend or coalesce non-essential animation work while the user drags, pans, pinches, zooms, or centers the canvas viewport, then resume from the correct timeline or autonomous time without changing the user's play/pause state.",
|
|
831
879
|
);
|
|
832
880
|
expect(contract.aiUsageRules).toContain(
|
|
833
|
-
"If a generated app uses ImageData, getImageData, or putImageData for procedural output, performance validation must fail unless the
|
|
881
|
+
"If a generated app uses ImageData, getImageData, or putImageData for procedural output, performance validation must fail unless rendererTechnique records measured WebGL/WebGPU comparison evidence that the CPU path preserves quality and responsiveness, or the renderer is moved to GPU.",
|
|
834
882
|
);
|
|
835
883
|
expect(contract.aiUsageRules).toContain(
|
|
836
884
|
"Performance matrices must declare rendererStrategy so tests can distinguish none, dom, svg, canvas-2d, webgl, and webgpu renderer paths.",
|
|
837
885
|
);
|
|
886
|
+
expect(contract.aiUsageRules).toContain(
|
|
887
|
+
"If a renderer cannot meet the budget, first optimize renderer technique, caching, invalidation, scheduling, and critical-path work. Only change exposed product ranges, work units, or controls after recording measured evidence that the requested quality ceiling is impossible; do not silently reduce product quality to pass budgets.",
|
|
888
|
+
);
|
|
838
889
|
});
|
|
839
890
|
|
|
840
891
|
it("documents reference runtime clone mode as a tested composition contract", () => {
|
|
@@ -1156,10 +1207,10 @@ describe("Toolcraft template component contracts", () => {
|
|
|
1156
1207
|
'Product-output apps must expose a dedicated "Background" section directly before the first export settings section. With PNG export that first section is Image Export; with video-only export it is Video Export.',
|
|
1157
1208
|
);
|
|
1158
1209
|
expect(contract.aiUsageRules).toContain(
|
|
1159
|
-
"Product-output apps must pass the includeBackground runtime value to createToolcraftPngExportCanvas
|
|
1210
|
+
"Product-output apps must pass the includeBackground runtime value to createToolcraftPngExportCanvas for PNG alpha and call shouldIncludeToolcraftPreviewBackground(state) for live preview product background.",
|
|
1160
1211
|
);
|
|
1161
1212
|
expect(contract.aiUsageRules).toContain(
|
|
1162
|
-
"PNG export must use createToolcraftPngExportCanvas so background transparency and selected image dimensions or retina fallback are applied consistently
|
|
1213
|
+
"PNG export must use createToolcraftPngExportCanvas so background transparency and selected image dimensions or retina fallback are applied consistently; turning Include off makes preview product background and PNG alpha transparent without hiding the Toolcraft canvas backing or video background.",
|
|
1163
1214
|
);
|
|
1164
1215
|
expect(contract.aiUsageRules).toContain(
|
|
1165
1216
|
"Video export must keep product background and use getToolcraftRetinaExportSize for retina dimensions.",
|
|
@@ -1184,15 +1235,33 @@ describe("Toolcraft template component contracts", () => {
|
|
|
1184
1235
|
it("documents file upload ownership across single-layer and multi-layer apps", () => {
|
|
1185
1236
|
const contract = getToolcraftComponentContract("fileDrop");
|
|
1186
1237
|
|
|
1187
|
-
expect(contract.commands).toEqual(["media.delete", "media.import"]);
|
|
1238
|
+
expect(contract.commands).toEqual(["media.delete", "media.import", "media.reorder"]);
|
|
1239
|
+
expect(contract.aiUsageRules).toContain(
|
|
1240
|
+
'Use fileDrop with assetKind: "image" for image-only source media and assetKind: "file" for arbitrary uploaded files.',
|
|
1241
|
+
);
|
|
1188
1242
|
expect(contract.aiUsageRules).toContain(
|
|
1189
1243
|
"In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.",
|
|
1190
1244
|
);
|
|
1245
|
+
expect(contract.aiUsageRules).toContain(
|
|
1246
|
+
"In file mode, the runtime shows uploaded files as a sortable list with paperclip icons, file names, remove buttons, and --border/5 separators.",
|
|
1247
|
+
);
|
|
1248
|
+
expect(contract.aiUsageRules).toContain(
|
|
1249
|
+
"In single-layer apps, global Reset controls and section reset must remove uploaded fileDrop source media and return the fileDrop target to defaultValue.",
|
|
1250
|
+
);
|
|
1191
1251
|
expect(contract.aiUsageRules).toContain(
|
|
1192
1252
|
"Use fileDrop with multiple: true when the app needs several uploaded images as one source set; do not build a custom thumbnail uploader for this.",
|
|
1193
1253
|
);
|
|
1194
1254
|
expect(contract.aiUsageRules).toContain(
|
|
1195
|
-
"When multiple uploaded images are present, the runtime appends media, shows a four-column preview grid, puts the add-more tile last, and exposes per-image removal.",
|
|
1255
|
+
"When multiple uploaded images are present, the runtime appends media, shows a sortable four-column preview grid, puts the add-more tile last, and exposes per-image removal.",
|
|
1256
|
+
);
|
|
1257
|
+
expect(contract.aiUsageRules).toContain(
|
|
1258
|
+
"Canvas drops route to the first visible matching fileDrop target by asset kind: image files prefer image uploaders, non-image files prefer file uploaders, and file uploaders accept images only when no image uploader matches.",
|
|
1259
|
+
);
|
|
1260
|
+
expect(contract.aiUsageRules).toContain(
|
|
1261
|
+
"Dragging thumbnails reorders runtime mediaAssets; preview, export, and renderer mapping must consume that media order instead of maintaining a separate product-only order.",
|
|
1262
|
+
);
|
|
1263
|
+
expect(contract.aiUsageRules).toContain(
|
|
1264
|
+
"Do not create custom upload buttons, file lists, or file sorting for generic source uploads when fileDrop can represent the source set.",
|
|
1196
1265
|
);
|
|
1197
1266
|
expect(contract.aiUsageRules).toContain(
|
|
1198
1267
|
"In multi-layer apps, deletion and visibility belong to the Layers panel; fileDrop remains an upload target.",
|
|
@@ -1226,6 +1295,9 @@ describe("Toolcraft template component contracts", () => {
|
|
|
1226
1295
|
expect(contract.aiUsageRules).toContain(
|
|
1227
1296
|
"Render multiple related color fields in one section with at most two colors per row.",
|
|
1228
1297
|
);
|
|
1298
|
+
expect(contract.aiUsageRules).toContain(
|
|
1299
|
+
"If a multi-color bank has an odd trailing plain Color, keep that last Color at half width; only ColorOpacity or intentionally full-width compound controls occupy a full row.",
|
|
1300
|
+
);
|
|
1229
1301
|
});
|
|
1230
1302
|
|
|
1231
1303
|
it("documents CodeTextarea as generic multiline text input", () => {
|
|
@@ -375,6 +375,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
375
375
|
layoutConstraints: [
|
|
376
376
|
"Keep local actions close to the entity they affect.",
|
|
377
377
|
"Keep action labels short and scoped by the section title; prefer Randomize, Clear, Sort, Normalize, Duplicate, or Reset when the section already names the target.",
|
|
378
|
+
"When an Actions button group wraps below its label, the wrapped button row starts at the left edge of the control content instead of centering or staying right-aligned.",
|
|
378
379
|
],
|
|
379
380
|
requiredAcceptance: [
|
|
380
381
|
"Prove each action dispatches the intended command or product side effect for the nearby entity only.",
|
|
@@ -388,6 +389,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
388
389
|
"Do not use Actions for final product delivery actions; use sticky panelActions for Export, Copy, Download, Generate, or Apply.",
|
|
389
390
|
"Do not use Actions for global reset; the controls panel header owns global reset.",
|
|
390
391
|
"Do not use Actions for animation transport; Play, Pause, Resume, Restart, and Scrub belong to the top timeline when timeline behavior exists.",
|
|
392
|
+
"When an Actions button group no longer fits beside its label and wraps to a second row, align the wrapped button row to the left.",
|
|
391
393
|
'For local reset-like actions, use product-specific values such as "reset-current-layer" or "reset-palette" and handle them through ToolcraftApp onPanelAction; do not use a bare "reset" value unless the action intentionally runs controls.reset.',
|
|
392
394
|
"Acceptance and browser tests must click each Actions button and prove the product output or runtime state for the nearby entity changed.",
|
|
393
395
|
],
|
|
@@ -435,7 +437,8 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
435
437
|
"Adding or removing collection items must update the runtime target array consumed by the renderer and export; do not add panel-only items.",
|
|
436
438
|
"Do not model add/remove item behavior with a Slider count when users need to edit the actual items.",
|
|
437
439
|
"recommendedMaxItems is advisory only and must not disable the plus button. Use hardMaxItems only when a real product, algorithm, API, export, or measured performance limit requires it.",
|
|
438
|
-
"CollectionActions item controls use built-in controls whenever possible, such as Color, ColorOpacity, TextInput, Select, Segmented, Slider, Switch, Checkbox, or
|
|
440
|
+
"CollectionActions item controls use built-in controls whenever possible, such as Color, ColorOpacity, TextInput, Select, Segmented, Slider, Switch, Checkbox, RangeInput, or FontPicker.",
|
|
441
|
+
"Use FontPicker as the collection item control when each repeated item is a typography/text-style entity; do not split its font, color, opacity, size, case, letter-spacing, or line-height into sibling collection fields.",
|
|
439
442
|
"Do not add visible labels like Color 1, Color 2, Item 1, or Item 2 for homogeneous collection items when the collection label already explains the group.",
|
|
440
443
|
"Use compact half-width item layout whenever the child control is allowed to fit in a half row; color items without opacity are the default two-column case.",
|
|
441
444
|
"Acceptance must add and remove items through the browser UI and prove canvas/export output follows the changed collection.",
|
|
@@ -501,8 +504,8 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
501
504
|
"Video export browser coverage must load the exported blob metadata and prove video.duration matches the edited runtime timeline duration; blobSize/blobType checks alone are not enough.",
|
|
502
505
|
"Video export must report frame-based progress through reportProgress during render/encode steps. PNG export should report phase progress for render, blob, and handoff when those phases are asynchronous.",
|
|
503
506
|
'Product-output apps must expose a dedicated "Background" section directly before the first export settings section. With PNG export that first section is Image Export; with video-only export it is Video Export.',
|
|
504
|
-
"Product-output apps must pass the includeBackground runtime value to createToolcraftPngExportCanvas
|
|
505
|
-
"PNG export must use createToolcraftPngExportCanvas so background transparency and selected image dimensions or retina fallback are applied consistently
|
|
507
|
+
"Product-output apps must pass the includeBackground runtime value to createToolcraftPngExportCanvas for PNG alpha and call shouldIncludeToolcraftPreviewBackground(state) for live preview product background.",
|
|
508
|
+
"PNG export must use createToolcraftPngExportCanvas so background transparency and selected image dimensions or retina fallback are applied consistently; turning Include off makes preview product background and PNG alpha transparent without hiding the Toolcraft canvas backing or video background.",
|
|
506
509
|
"Video export must keep product background and use getToolcraftRetinaExportSize for retina dimensions.",
|
|
507
510
|
"Copy PNG can be a secondary action when clipboard output is useful, but copy does not replace export.",
|
|
508
511
|
"Add Copy PNG as a secondary action only when the prompt/reference includes clipboard output or the product clearly benefits from paste/share workflows.",
|
|
@@ -621,7 +624,8 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
621
624
|
],
|
|
622
625
|
layoutConstraints: [
|
|
623
626
|
"Plain Color controls may render two per row only when no opacity is present.",
|
|
624
|
-
"Color
|
|
627
|
+
"Odd trailing plain Color controls in a multi-color bank keep the same half-width footprint as paired colors.",
|
|
628
|
+
"Color label visibility depends on user usefulness: palette variation banks omit per-item labels; distinct color roles keep labels.",
|
|
625
629
|
],
|
|
626
630
|
requiredAcceptance: [
|
|
627
631
|
"Prove the selected color affects product output, preview, or export.",
|
|
@@ -633,18 +637,21 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
633
637
|
"Keep a color inside a section when it configures the same entity as nearby controls. Example: Square 1 (Right) contains Connections, Hover radius, and Color in one section.",
|
|
634
638
|
"Use a standalone color section only when the color itself is the whole semantic section; the section title must describe the product role such as Background, Object, Connector, Accent, Gradient, or Brand.",
|
|
635
639
|
"When color belongs to the same object or effect as nearby controls, keep it inside that section and use a concise field label that is unambiguous in context, such as Color in a Square section or Symbol color in a mixed Style section.",
|
|
636
|
-
"
|
|
637
|
-
"
|
|
640
|
+
"Decide color label visibility from the user's point of view. Omit labels for color banks that only add palette variety, such as Accent Shades, Bead Colors, or palette.accent1..5.",
|
|
641
|
+
"Make color label visibility a group-level decision: do not mix labeled and unlabeled items inside one semantic color bank.",
|
|
642
|
+
"Keep visible labels when colors edit distinct user-facing entities or roles, such as Fill, Stroke, Background, Connector, Object, or Highlight.",
|
|
643
|
+
"A color bank can share a section with distribution controls such as Spread, Mix, or Randomness and still keep each color item unlabeled when the section title names the palette context.",
|
|
638
644
|
"The standalone default applies only to color-only sections; mixed semantic sections keep color grouped with nearby controls.",
|
|
639
645
|
"Never use generic Color or Colors as a generated section title. If no meaningful color role exists and the colors are just basic colors, use a neutral section title such as Appearance instead of omitting the title.",
|
|
640
646
|
"Do not split a grouped object section into a separate generated Color section; if the color role is unclear, ask the user before implementation.",
|
|
641
647
|
"When one short numeric/text field and one Color field configure the same entity, keep them in one two-column inline layout group.",
|
|
642
|
-
'Mixed inline rows require visible labels on both controls. The required Background row is the
|
|
648
|
+
'Mixed inline rows require visible labels on both controls. The required Background row is the section-title-owned exception: use the Switch label "Include" and set the background Color control label to false. Palette variation color banks are the other exception when the group/section label already names the color bank.',
|
|
643
649
|
"Plain Color popovers must not show opacity controls. If opacity is editable, use ColorOpacity instead.",
|
|
644
650
|
"Product-output apps always expose renderer-owned output background color as a schema color target such as appearance.background or scene.background.",
|
|
645
651
|
'Pair renderer-owned output background color with export.includeBackground in one Background section directly before export settings. Use an equal-width inline row with the export.includeBackground Switch labeled "Include" on the left and the background Color parameter with label false on the right; each control occupies one half of the row.',
|
|
646
|
-
"Preview, PNG export, and video export must read the runtime background color value instead of hardcoding that background in CSS, Canvas fillStyle, or WebGL clearColor. export.includeBackground controls
|
|
652
|
+
"Preview, PNG export, and video export must read the runtime background color value instead of hardcoding that background in CSS, Canvas fillStyle, or WebGL clearColor. export.includeBackground controls live preview product background and PNG alpha; it must not make the Toolcraft canvas shell/backing or video output transparent.",
|
|
647
653
|
"Render multiple related color fields in one section with at most two colors per row.",
|
|
654
|
+
"If a multi-color bank has an odd trailing plain Color, keep that last Color at half width; only ColorOpacity or intentionally full-width compound controls occupy a full row.",
|
|
648
655
|
],
|
|
649
656
|
},
|
|
650
657
|
gradient: {
|
|
@@ -725,6 +732,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
725
732
|
"FontPicker owns the font preview select, virtualized font popup, category filters, search, preview loading, font-weight select, font-size input, text-case select, text color/opacity control, letter-spacing slider, and line-height slider.",
|
|
726
733
|
"Do not recreate FontPicker with a plain Select plus separate sliders; use type: \"fontPicker\" so the popup mechanics and footer controls stay intact.",
|
|
727
734
|
"Use one object value with fontId, fontWeight, fontSize, letterSpacing, lineHeight, textCase, color, and opacity. Keep typography renderers wired to all eight parts.",
|
|
735
|
+
"FontPicker standard/default text color is #FFFFFF with opacity 100; omit color/opacity or use those values unless the prompt or reference explicitly requires a different initial text color.",
|
|
728
736
|
"Any product text controlled by FontPicker must render fontId, fontWeight, fontSize, letterSpacing, lineHeight, textCase, color, and opacity in preview and export; do not leave typography values as panel-only runtime state.",
|
|
729
737
|
"FontPicker is an atomic compound typography control. Do not split any owned typography part into a neighboring schema control for the same product text entity.",
|
|
730
738
|
"Do not put a help tooltip on FontPicker just to list its owned fields. If the section title and FontPicker labels already make the text target clear, omit description.",
|
|
@@ -863,20 +871,27 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
863
871
|
],
|
|
864
872
|
layoutConstraints: [
|
|
865
873
|
"FileDrop lives in the controls panel; single-layer apps use its preview and clear behavior.",
|
|
866
|
-
"When fileDrop has multiple: true and more than one image is present, the runtime renders a four-column thumbnail grid with the add-more tile last.",
|
|
874
|
+
"When fileDrop has multiple: true and more than one image is present, the runtime renders a sortable four-column thumbnail grid with the add-more tile last.",
|
|
875
|
+
"When fileDrop has assetKind: file, the runtime renders a sortable file list with a paperclip icon, filename, remove button, and row separators using --border/5.",
|
|
867
876
|
],
|
|
868
877
|
requiredAcceptance: [
|
|
869
|
-
"Prove file import changes media state and product output; prove clear
|
|
878
|
+
"Prove file import changes media state and product output; prove clear, reorder, and section/global reset update source material.",
|
|
870
879
|
],
|
|
871
880
|
}),
|
|
872
881
|
aiUsageRules: [
|
|
873
882
|
"Use fileDrop for source material uploads in the controls panel, not on the canvas.",
|
|
883
|
+
'Use fileDrop with assetKind: "image" for image-only source media and assetKind: "file" for arbitrary uploaded files.',
|
|
874
884
|
"In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.",
|
|
885
|
+
"In file mode, the runtime shows uploaded files as a sortable list with paperclip icons, file names, remove buttons, and --border/5 separators.",
|
|
886
|
+
"In single-layer apps, global Reset controls and section reset must remove uploaded fileDrop source media and return the fileDrop target to defaultValue.",
|
|
875
887
|
"Use fileDrop with multiple: true when the app needs several uploaded images as one source set; do not build a custom thumbnail uploader for this.",
|
|
876
|
-
"When multiple uploaded images are present, the runtime appends media, shows a four-column preview grid, puts the add-more tile last, and exposes per-image removal.",
|
|
888
|
+
"When multiple uploaded images are present, the runtime appends media, shows a sortable four-column preview grid, puts the add-more tile last, and exposes per-image removal.",
|
|
889
|
+
"Canvas drops route to the first visible matching fileDrop target by asset kind: image files prefer image uploaders, non-image files prefer file uploaders, and file uploaders accept images only when no image uploader matches.",
|
|
890
|
+
"Dragging thumbnails reorders runtime mediaAssets; preview, export, and renderer mapping must consume that media order instead of maintaining a separate product-only order.",
|
|
891
|
+
"Do not create custom upload buttons, file lists, or file sorting for generic source uploads when fileDrop can represent the source set.",
|
|
877
892
|
"In multi-layer apps, deletion and visibility belong to the Layers panel; fileDrop remains an upload target.",
|
|
878
893
|
],
|
|
879
|
-
commands: ["media.delete", "media.import"],
|
|
894
|
+
commands: ["media.delete", "media.import", "media.reorder"],
|
|
880
895
|
},
|
|
881
896
|
imagePicker: {
|
|
882
897
|
...control("imagePicker", "ImagePicker", "standalone", "component-owned"),
|
|
@@ -978,6 +993,8 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
978
993
|
aiUsageRules: [
|
|
979
994
|
"Use custom controls only for product interactions that built-in controls cannot express.",
|
|
980
995
|
"Do not use a custom control to recreate a built-in Slider, RangeSlider, Select, Segmented, Switch, Checkbox, Color, ColorOpacity, Gradient, FontPicker, ImagePicker, FileDrop, TextInput, CodeTextarea, RangeInput, Palette, Actions, CollectionActions, Curves, AnchorGrid, ChannelMixer, Vector, or PanelActions control.",
|
|
996
|
+
"When a custom control owns a growable, removable, selectable, or reorderable runtime item set, its builtInFitCheck must explicitly check collectionActions and actions before choosing custom; this is based on the value model and user workflow, not on entity names such as masks or glyphs.",
|
|
997
|
+
"Do not justify custom controls with icons, layout, styling, compactness, or custom buttons alone. The fit check must name the product interaction or value model that built-ins cannot express.",
|
|
981
998
|
"Custom controls may use Toolcraft primitives for small app-specific chrome, but must not import or render low-level runtime surfaces or duplicate toolbar, timeline, layers, canvas, panel, or built-in control mechanics.",
|
|
982
999
|
"Custom controls must render the minimum UI needed to understand the value, context, and available actions; avoid decorative metadata and text that repeats what the section, label, or visible item already explains.",
|
|
983
1000
|
"Every visible custom-control element must justify its space by enabling selection, ordering, preview, removal, upload, editing, or status that affects the product.",
|
|
@@ -1101,9 +1118,11 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
1101
1118
|
performanceAcceptance: {
|
|
1102
1119
|
aiUsageRules: [
|
|
1103
1120
|
"Custom renderers must define performance budgets for media import, preview updates, control drags, and export/copy before implementation.",
|
|
1104
|
-
"Controls that change renderer workload
|
|
1105
|
-
"
|
|
1121
|
+
"Controls that change renderer workload by changing output dimensions, element count, density, sample count, iteration count, blur/filter radius, shader branch cost, media processing, text layout, or export quality must be tested at min, default, and max values.",
|
|
1122
|
+
"For workload control scenarios, stressFixture is the tested control value. If the app has an independent heavy baseline such as large media, long text, many items, high render scale, or dense source state, declare workloadFixture and apply it before the measured control interaction.",
|
|
1123
|
+
"Hash differs is not enough for workload controls; tests must assert semantic direction, such as density increasing item count or size changes reducing/increasing rendered cells.",
|
|
1106
1124
|
"Performance tests must use representative fixtures and the same renderer/export path as the running app, not only tiny 32px fixtures or isolated helper state.",
|
|
1125
|
+
"Media-import workload fixtures and media workload baselines must use fixture kind \"media\" with numeric width and height at least 1920x1080-equivalent; 640x480 preview fixtures cannot satisfy upload, effect-control, or image-processing performance coverage.",
|
|
1107
1126
|
"Expensive renderers must cache decoded media, source pixels, glyph atlases, gradients, and other reusable inputs by media id, canvas size, and stable control keys.",
|
|
1108
1127
|
"Slider drags and high-frequency controls must debounce or coalesce preview work, cancel stale async renders, and avoid re-decoding media on every control change.",
|
|
1109
1128
|
"Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
|
|
@@ -1114,6 +1133,9 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
1114
1133
|
"Renderer specs must include a Renderer Technique Decision Matrix with sourceRepresentation, productRepresentation, previewRenderer, exportRenderer, rendererWorkload, rendererStrategy, whyNotAlternativeStrategies, fidelityRisks, and performanceRisks.",
|
|
1115
1134
|
"Custom renderer apps must mirror the Renderer Technique Decision Matrix in typed rendererTechnique config so validation can reject contradictory renderer choices.",
|
|
1116
1135
|
"Custom renderer specs must include a Renderer Layer Inventory and mirror it in typed rendererTechnique.layers so dense raster backgrounds cannot silently rasterize semantic foreground output.",
|
|
1136
|
+
"Custom renderer apps must declare rendererPipeline with render passes, cache keys, execution location, preview/export quality, and interaction invalidation before implementation.",
|
|
1137
|
+
"Render Pipeline Inventory must explain which runtime targets invalidate each expensive pass; high-frequency interactions such as animation frames, drag, zoom, pan, timeline playback, and mask movement must not invalidate upstream decode/preprocess/pixel-transform work unless that target truly changes the upstream result.",
|
|
1138
|
+
"Cache-sensitive render passes such as decode, preprocess, pixel-transform, text-layout, rasterize, and composite must declare cache keys so tests can reject full recomputation on every control change.",
|
|
1117
1139
|
"Semantic foreground output such as product lines, shapes, icons, text, object bounds, and meaningful markers should use DOM or SVG by default; dense raster backgrounds do not justify rasterizing low-count foreground geometry or text.",
|
|
1118
1140
|
"Editing handles must be DOM/SVG overlays, excluded from export, and written through runtime state instead of being drawn into the product raster layer.",
|
|
1119
1141
|
"Product foreground and editing handle renderer layers must declare uiSelector so browser tests can verify the visible layer exists.",
|
|
@@ -1122,17 +1144,18 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
1122
1144
|
"Do not switch renderer technology just because it seems more modern or faster. Preview and export may use different renderers only when the decision matrix explains why and export/copy remains product-quality.",
|
|
1123
1145
|
"Choose renderer workload by product fidelity before choosing rendering technology: ASCII, glyph grids, code art, subtitles, typography, or monospace text products are text-output unless the product intentionally rasterizes them into per-pixel effects.",
|
|
1124
1146
|
"Text-output and vector-output visible previews must preserve native output fidelity. Do not render a low-resolution offscreen canvas or texture and upscale it to the product size.",
|
|
1125
|
-
"Pixel-output renderers must
|
|
1147
|
+
"Pixel-output renderers must treat WebGL/WebGPU as the default candidate even when the scene is static; Canvas 2D is allowed only when measured worst-case evidence shows the CPU path preserves quality and remains responsive.",
|
|
1126
1148
|
"Procedural pixel renderers, shader-like effects, animated mesh gradients, and large exportable previews should use WebGL or WebGPU for pixel work instead of main-thread ImageData loops.",
|
|
1149
|
+
"Detail-heavy Canvas 2D pixel/media renderers may stay on CPU only when rendererTechnique records measured stress evidence for rejecting WebGL/WebGPU; if the heavy media stress fails, move pixel work to GPU instead of lowering quality.",
|
|
1127
1150
|
"WebGL and WebGPU renderers must initialize contexts, programs, shaders, pipelines, textures, and large buffers once, then update uniforms or stable buffers when controls change.",
|
|
1128
1151
|
"For keyframe or playback renderers, texture upload and media decode must be keyed to source media/resource changes, not to timeline time or evaluated settings. Timeline-only updates must reuse decoded media and existing GPU resources.",
|
|
1129
1152
|
"Do not create WebGL/WebGPU contexts, shader programs, textures, or requestAnimationFrame loops directly in the React render path.",
|
|
1130
1153
|
"Animation loops must cancel scheduled frames during cleanup.",
|
|
1131
1154
|
"Animated preview renderers must suspend or coalesce non-essential animation work while the user drags, pans, pinches, zooms, or centers the canvas viewport, then resume from the correct timeline or autonomous time without changing the user's play/pause state.",
|
|
1132
|
-
"If a generated app uses ImageData, getImageData, or putImageData for procedural output, performance validation must fail unless the
|
|
1155
|
+
"If a generated app uses ImageData, getImageData, or putImageData for procedural output, performance validation must fail unless rendererTechnique records measured WebGL/WebGPU comparison evidence that the CPU path preserves quality and responsiveness, or the renderer is moved to GPU.",
|
|
1133
1156
|
"Performance matrices must declare rendererStrategy so tests can distinguish none, dom, svg, canvas-2d, webgl, and webgpu renderer paths.",
|
|
1134
1157
|
"Browser verification must interact with the actual UI, exercise worst-case control values, and fail if the app freezes, creates runaway render loops, drops canvas zoom/offset, or misses the performance budget.",
|
|
1135
|
-
"If a renderer cannot meet the budget,
|
|
1158
|
+
"If a renderer cannot meet the budget, first optimize renderer technique, caching, invalidation, scheduling, and critical-path work. Only change exposed product ranges, work units, or controls after recording measured evidence that the requested quality ceiling is impossible; do not silently reduce product quality to pass budgets.",
|
|
1136
1159
|
],
|
|
1137
1160
|
capabilities: ["performance-budgets", "workload-control-tests"],
|
|
1138
1161
|
commands: [],
|
|
@@ -177,7 +177,7 @@ export const TOOLCRAFT_DECISION_CONTRACT = [
|
|
|
177
177
|
currentConstraint:
|
|
178
178
|
"Product-output apps expose final output delivery through sticky footer panelActions.",
|
|
179
179
|
desiredBehavior:
|
|
180
|
-
'Static products include Export PNG plus an "Image Export" section for format and 2K/4K/8K resolution; animated products include Export Video and Export PNG plus "Video Export" settings. Copy can be secondary, but it does not replace export. Product apps expose a required "Background" section directly before export settings, with a Switch labeled "Include" and a background color control with label false in one equal-width row. Standard
|
|
180
|
+
'Static products include Export PNG plus an "Image Export" section for format and 2K/4K/8K resolution; animated products include Export Video and Export PNG plus "Video Export" settings. Copy can be secondary, but it does not replace export. Product apps expose a required "Background" section directly before export settings, with a Switch labeled "Include" and a background color control with label false in one equal-width row. Standard helpers own runtime PNG transparency, live preview product-background visibility, and selected image dimensions or retina fallback, while Toolcraft canvas backing and video keep the background.',
|
|
181
181
|
enforcement: ["acceptance-validator", "performance-validator", "browser-helper", "starter-agents"],
|
|
182
182
|
id: "output-export-required",
|
|
183
183
|
level: "invariant",
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getToolcraftRetinaExportPixelRatio,
|
|
9
9
|
getToolcraftRetinaExportSize,
|
|
10
10
|
shouldIncludeToolcraftExportBackground,
|
|
11
|
+
shouldIncludeToolcraftPreviewBackground,
|
|
11
12
|
} from "./export";
|
|
12
13
|
|
|
13
14
|
function createState(schema = defineToolcraft({ canvas: { enabled: true }, panels: {} })): ToolcraftState {
|
|
@@ -89,6 +90,36 @@ describe("Toolcraft export helpers", () => {
|
|
|
89
90
|
expect(shouldIncludeToolcraftExportBackground({ format: "video", schema })).toBe(true);
|
|
90
91
|
});
|
|
91
92
|
|
|
93
|
+
it("uses include-background runtime state for live preview background", () => {
|
|
94
|
+
const state = createState();
|
|
95
|
+
|
|
96
|
+
expect(shouldIncludeToolcraftPreviewBackground({ state })).toBe(true);
|
|
97
|
+
|
|
98
|
+
state.values["export.includeBackground"] = false;
|
|
99
|
+
expect(shouldIncludeToolcraftPreviewBackground({ state })).toBe(false);
|
|
100
|
+
|
|
101
|
+
state.values["export.includeBackground"] = true;
|
|
102
|
+
expect(shouldIncludeToolcraftPreviewBackground({ state })).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("keeps video export background independent from preview include state", () => {
|
|
106
|
+
const schema = defineToolcraft({
|
|
107
|
+
canvas: { enabled: true },
|
|
108
|
+
export: {
|
|
109
|
+
png: {
|
|
110
|
+
background: "transparent",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
panels: {},
|
|
114
|
+
});
|
|
115
|
+
const state = createState(schema);
|
|
116
|
+
|
|
117
|
+
state.values["export.includeBackground"] = false;
|
|
118
|
+
|
|
119
|
+
expect(shouldIncludeToolcraftPreviewBackground({ state })).toBe(false);
|
|
120
|
+
expect(shouldIncludeToolcraftExportBackground({ format: "video", schema })).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
92
123
|
it("uses at least 2x pixel ratio for retina export", () => {
|
|
93
124
|
expect(getToolcraftRetinaExportPixelRatio(1)).toBe(2);
|
|
94
125
|
expect(getToolcraftRetinaExportPixelRatio(1.5)).toBe(2);
|
|
@@ -16,6 +16,11 @@ export type ToolcraftExportBackgroundOptions = {
|
|
|
16
16
|
schema: ResolvedToolcraftAppSchema;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
export type ToolcraftPreviewBackgroundOptions = {
|
|
20
|
+
includeBackgroundTarget?: string;
|
|
21
|
+
state: ToolcraftState;
|
|
22
|
+
};
|
|
23
|
+
|
|
19
24
|
export type ToolcraftExportSizeOptions = {
|
|
20
25
|
devicePixelRatio?: number;
|
|
21
26
|
state: ToolcraftState;
|
|
@@ -129,6 +134,42 @@ export function shouldIncludeToolcraftExportBackground({
|
|
|
129
134
|
return schema.export.png.background !== "transparent";
|
|
130
135
|
}
|
|
131
136
|
|
|
137
|
+
export function shouldIncludeToolcraftPreviewBackground({
|
|
138
|
+
includeBackgroundTarget = "export.includeBackground",
|
|
139
|
+
state,
|
|
140
|
+
}: ToolcraftPreviewBackgroundOptions): boolean {
|
|
141
|
+
const includeBackgroundValue = state.values[includeBackgroundTarget];
|
|
142
|
+
|
|
143
|
+
if (typeof includeBackgroundValue === "boolean") {
|
|
144
|
+
return includeBackgroundValue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (typeof includeBackgroundValue === "string") {
|
|
148
|
+
const normalizedValue = includeBackgroundValue.trim().toLowerCase();
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
normalizedValue === "false" ||
|
|
152
|
+
normalizedValue === "off" ||
|
|
153
|
+
normalizedValue === "no" ||
|
|
154
|
+
normalizedValue === "transparent" ||
|
|
155
|
+
normalizedValue === "exclude"
|
|
156
|
+
) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (
|
|
161
|
+
normalizedValue === "true" ||
|
|
162
|
+
normalizedValue === "on" ||
|
|
163
|
+
normalizedValue === "yes" ||
|
|
164
|
+
normalizedValue === "include"
|
|
165
|
+
) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
132
173
|
export function createToolcraftPngExportCanvas({
|
|
133
174
|
background = "#000000",
|
|
134
175
|
canvasFactory = () => document.createElement("canvas"),
|
|
@@ -43,7 +43,13 @@ function CanvasStateProbe() {
|
|
|
43
43
|
<span data-testid="canvas-zoom">{state.canvas.zoom}</span>
|
|
44
44
|
<span data-testid="media-count">{state.mediaAssets.length}</span>
|
|
45
45
|
<span data-testid="media-size">
|
|
46
|
-
{state.mediaAssets[0]?.size
|
|
46
|
+
{state.mediaAssets[0]?.size?.width ?? 0},{state.mediaAssets[0]?.size?.height ?? 0}
|
|
47
|
+
</span>
|
|
48
|
+
<span data-testid="media-targets">
|
|
49
|
+
{state.mediaAssets.map((asset) => asset.sourceTarget ?? "none").join(",")}
|
|
50
|
+
</span>
|
|
51
|
+
<span data-testid="media-kinds">
|
|
52
|
+
{state.mediaAssets.map((asset) => asset.assetKind ?? "image").join(",")}
|
|
47
53
|
</span>
|
|
48
54
|
</>
|
|
49
55
|
);
|
|
@@ -376,6 +382,76 @@ describe("CanvasShell", () => {
|
|
|
376
382
|
expect(screen.getByRole("button", { name: "Select material.png" })).toBeTruthy();
|
|
377
383
|
});
|
|
378
384
|
|
|
385
|
+
it("routes canvas drops to image and file upload targets by asset kind", async () => {
|
|
386
|
+
const schema = defineToolcraft({
|
|
387
|
+
canvas: {
|
|
388
|
+
enabled: true,
|
|
389
|
+
size: { width: 300, height: 200, unit: "px" },
|
|
390
|
+
upload: true,
|
|
391
|
+
},
|
|
392
|
+
panels: {
|
|
393
|
+
controls: {
|
|
394
|
+
sections: [
|
|
395
|
+
{
|
|
396
|
+
controls: {
|
|
397
|
+
files: {
|
|
398
|
+
assetKind: "file",
|
|
399
|
+
label: "Files",
|
|
400
|
+
multiple: true,
|
|
401
|
+
target: "source.files",
|
|
402
|
+
type: "fileDrop",
|
|
403
|
+
},
|
|
404
|
+
image: {
|
|
405
|
+
label: "Image",
|
|
406
|
+
target: "source.image",
|
|
407
|
+
type: "fileDrop",
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
title: "Sources",
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
title: "Controls",
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
render(
|
|
419
|
+
<ToolcraftRoot schema={schema}>
|
|
420
|
+
<div style={{ height: 640, position: "relative", width: 640 }}>
|
|
421
|
+
<CanvasShell />
|
|
422
|
+
<CanvasStateProbe />
|
|
423
|
+
</div>
|
|
424
|
+
</ToolcraftRoot>,
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
const canvas = screen.getByRole("application", { name: "Canvas viewport" });
|
|
428
|
+
const imageFile = new File(["image"], "material.png", { type: "image/png" });
|
|
429
|
+
const textFile = new File(["notes"], "notes.txt", { type: "text/plain" });
|
|
430
|
+
|
|
431
|
+
fireEvent.drop(canvas, {
|
|
432
|
+
clientX: 0,
|
|
433
|
+
clientY: 0,
|
|
434
|
+
dataTransfer: {
|
|
435
|
+
files: [imageFile, textFile],
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
await waitFor(() => {
|
|
440
|
+
expect(screen.getByTestId("media-count").textContent).toBe("2");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(screen.getByTestId("media-targets").textContent?.split(",").sort()).toEqual([
|
|
444
|
+
"source.files",
|
|
445
|
+
"source.image",
|
|
446
|
+
]);
|
|
447
|
+
expect(screen.getByTestId("media-kinds").textContent?.split(",").sort()).toEqual([
|
|
448
|
+
"file",
|
|
449
|
+
"image",
|
|
450
|
+
]);
|
|
451
|
+
expect(screen.getByRole("button", { name: "Select material.png" })).toBeTruthy();
|
|
452
|
+
expect(screen.queryByRole("button", { name: "Select notes.txt" })).toBeNull();
|
|
453
|
+
});
|
|
454
|
+
|
|
379
455
|
it("hides media when a parent layer group is hidden", () => {
|
|
380
456
|
render(
|
|
381
457
|
<ToolcraftRoot
|