@pixel-point/toolcraft 0.0.16 → 0.0.17

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.
Files changed (170) hide show
  1. package/package.json +1 -1
  2. package/src/generate-performance-iteration-execution.test.mjs +2 -2
  3. package/src/generate-test-demand-only-lifecycle.mjs +14 -3
  4. package/src/generate-test-dependency-sandbox.mjs +42 -5
  5. package/src/generate-test-doc-assertions.mjs +68 -18
  6. package/src/generate-test-doc-assertions.test.mjs +22 -0
  7. package/src/generate-test-file-assertions.mjs +43 -76
  8. package/src/generate-test-output-assertions.mjs +95 -0
  9. package/src/generate-test-package-assertions.mjs +6 -0
  10. package/src/generate-test-performance-boundary-assertions.mjs +45 -0
  11. package/src/generate-test-product-fixture.mjs +1 -0
  12. package/src/generate-test-product-fixture.test.mjs +18 -0
  13. package/src/generate-test-product-iteration.mjs +1 -0
  14. package/src/generate-test-product-iteration.test.mjs +1 -0
  15. package/src/generate-test-standalone-delivery-stages.mjs +14 -6
  16. package/src/generate.test.mjs +14 -0
  17. package/templates/runtime/contracts/component-contracts.choices.test.ts +12 -9
  18. package/templates/runtime/contracts/component-contracts.choices.ts +10 -9
  19. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +17 -2
  20. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +1 -1
  21. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +1 -1
  22. package/templates/runtime/contracts/component-contracts.runtime.ts +3 -2
  23. package/templates/runtime/contracts/decision-contracts.test.ts +25 -4
  24. package/templates/runtime/contracts/decision-contracts.ts +3 -3
  25. package/templates/runtime/contracts/performance-verification-policy.test.ts +59 -6
  26. package/templates/runtime/contracts/performance-verification-policy.ts +5 -2
  27. package/templates/runtime/export/artifact-download.test.ts +71 -0
  28. package/templates/runtime/export/artifact-download.ts +80 -0
  29. package/templates/runtime/export/artifact-export-request.ts +23 -0
  30. package/templates/runtime/export/artifact-export-settings.test.ts +75 -0
  31. package/templates/runtime/export/artifact-export-settings.ts +96 -0
  32. package/templates/runtime/export/artifact-frame-renderer.test.ts +107 -0
  33. package/templates/runtime/export/artifact-frame-renderer.ts +96 -0
  34. package/templates/runtime/export/artifact-frame-state.test.ts +54 -0
  35. package/templates/runtime/export/artifact-frame-state.ts +44 -0
  36. package/templates/runtime/export/artifact-scene-frame.test.ts +84 -0
  37. package/templates/runtime/export/artifact-scene-frame.ts +118 -0
  38. package/templates/runtime/export/export-background.ts +65 -0
  39. package/templates/runtime/export/export-error.ts +43 -0
  40. package/templates/runtime/export/export.test.ts +6 -186
  41. package/templates/runtime/export/image-artifact-export.test.ts +111 -0
  42. package/templates/runtime/export/image-artifact-export.ts +145 -0
  43. package/templates/runtime/export/index.ts +14 -1
  44. package/templates/runtime/export/product-export-renderer.test.ts +77 -0
  45. package/templates/runtime/export/product-export-renderer.ts +63 -0
  46. package/templates/runtime/export/video-artifact-export.test.ts +166 -0
  47. package/templates/runtime/export/video-artifact-export.ts +164 -0
  48. package/templates/runtime/export/video-encoding-backend.ts +124 -0
  49. package/templates/runtime/export/video-encoding-policy.test.ts +68 -0
  50. package/templates/runtime/export/video-encoding-policy.ts +86 -0
  51. package/templates/runtime/export/video-frame-schedule.test.ts +58 -0
  52. package/templates/runtime/export/video-frame-schedule.ts +42 -0
  53. package/templates/runtime/index.ts +1 -0
  54. package/templates/runtime/react/app-shell/runtime-public-api-runtime.test.tsx +291 -0
  55. package/templates/runtime/react/app-shell/runtime-public-api-test-support.ts +21 -0
  56. package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +4 -303
  57. package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +12 -27
  58. package/templates/runtime/react/app-shell/toolcraft-app.tsx +22 -4
  59. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.test.tsx +16 -57
  60. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.tsx +75 -434
  61. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.test.ts +194 -0
  62. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.ts +227 -0
  63. package/templates/runtime/react/app-shell/toolcraft-source-asset-provider-ownership.test.tsx +243 -0
  64. package/templates/runtime/react/app-shell/toolcraft-source-asset-test-support.ts +51 -0
  65. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.test.ts +138 -0
  66. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.ts +378 -0
  67. package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +7 -12
  68. package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +17 -83
  69. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +42 -84
  70. package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +62 -0
  71. package/templates/runtime/react/panel-host/panel-host-runtime-placement.test.tsx +30 -0
  72. package/templates/runtime/react/panel-host/panel-host.tsx +4 -0
  73. package/templates/runtime/scene/scene-bounds.ts +1 -2
  74. package/templates/runtime/schema/canvas-render-scale.ts +62 -0
  75. package/templates/runtime/schema/controls-panel-section-id.test.ts +1 -1
  76. package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +3 -3
  77. package/templates/runtime/schema/define-toolcraft.setup-background.test.ts +1 -1
  78. package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +71 -3
  79. package/templates/runtime/schema/define-toolcraft.ts +1 -1
  80. package/templates/runtime/schema/schema-resolvers.ts +0 -56
  81. package/templates/runtime/schema/types.ts +4 -4
  82. package/templates/runtime/state/canvas-background-state.ts +6 -0
  83. package/templates/runtime/testing/performance-render-plan-integration.test.ts +64 -0
  84. package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +18 -1
  85. package/templates/starter/AGENTS.md +8 -7
  86. package/templates/starter/NOTICE.md +6 -0
  87. package/templates/starter/docs/toolcraft/README.md +2 -1
  88. package/templates/starter/docs/toolcraft/acceptance-testing.md +8 -6
  89. package/templates/starter/docs/toolcraft/agent-worklog.md +3 -3
  90. package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -3
  91. package/templates/starter/docs/toolcraft/core/media-upload.md +2 -2
  92. package/templates/starter/docs/toolcraft/core/performance.md +14 -3
  93. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +3 -1
  94. package/templates/starter/docs/toolcraft/core/setup-export.md +16 -12
  95. package/templates/starter/docs/toolcraft/core/timeline-animation.md +4 -4
  96. package/templates/starter/docs/toolcraft/performance.md +20 -6
  97. package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
  98. package/templates/starter/docs/toolcraft/schema-reference.md +2 -6
  99. package/templates/starter/docs/toolcraft/workflow.md +7 -2
  100. package/templates/starter/e2e/app-browser-control-coverage.spec.ts +33 -0
  101. package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +43 -1
  102. package/templates/starter/e2e/app-browser-render-scale-evidence.spec.ts +60 -6
  103. package/templates/starter/e2e/app-browser-render-scale-requirements.spec.ts +77 -1
  104. package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +8 -0
  105. package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +35 -0
  106. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +4 -1
  107. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +0 -1
  108. package/templates/starter/e2e/app-browser-shell.spec.ts +13 -5
  109. package/templates/starter/e2e/app-performance-path-adapters.ts +8 -1
  110. package/templates/starter/e2e/app-performance.spec.ts +16 -9
  111. package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +9 -330
  112. package/templates/starter/e2e/browser-infinity-canvas-unavailable-image-evidence.ts +370 -0
  113. package/templates/starter/e2e/browser-media-export-evidence.spec.ts +77 -0
  114. package/templates/starter/e2e/browser-media-export-evidence.ts +208 -0
  115. package/templates/starter/e2e/browser-render-scale-evidence.ts +37 -29
  116. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +4 -1
  117. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +43 -9
  118. package/templates/starter/e2e/browser-standard-timeline-evidence.ts +106 -0
  119. package/templates/starter/e2e/decoded-pixel-observation.spec.ts +74 -0
  120. package/templates/starter/e2e/decoded-pixel-observation.ts +227 -0
  121. package/templates/starter/e2e/export-artifact-helpers.ts +206 -25
  122. package/templates/starter/e2e/image-artifact-inspection.spec.ts +41 -0
  123. package/templates/starter/e2e/image-artifact-inspection.ts +99 -0
  124. package/templates/starter/e2e/performance-canvas-helpers.ts +22 -21
  125. package/templates/starter/e2e/performance-canvas-quality-guard.spec.ts +312 -0
  126. package/templates/starter/e2e/performance-canvas-quality-guard.ts +196 -0
  127. package/templates/starter/e2e/performance-canvas-quality-observer.ts +351 -0
  128. package/templates/starter/e2e/performance-control-layout-helpers.ts +2 -2
  129. package/templates/starter/e2e/performance-path-adapter-contract.ts +4 -4
  130. package/templates/starter/e2e/performance-path-helpers.spec.ts +203 -70
  131. package/templates/starter/e2e/performance-path-helpers.ts +210 -91
  132. package/templates/starter/e2e/performance-pipeline-evidence-continuity.spec.ts +76 -0
  133. package/templates/starter/e2e/performance-pipeline-evidence-test-fixtures.ts +11 -2
  134. package/templates/starter/e2e/performance-pipeline-invariants.ts +47 -4
  135. package/templates/starter/e2e/performance-render-scale-runner.spec.ts +140 -38
  136. package/templates/starter/e2e/video-artifact-inspection.spec.ts +64 -0
  137. package/templates/starter/e2e/video-artifact-inspection.ts +281 -0
  138. package/templates/starter/gitignore +1 -0
  139. package/templates/starter/package.json +1 -0
  140. package/templates/starter/scripts/check-toolcraft-docs.mjs +17 -0
  141. package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +60 -0
  142. package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +7 -2
  143. package/templates/starter/scripts/toolcraft-delivery-plan.mjs +7 -3
  144. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +24 -2
  145. package/templates/starter/scripts/toolcraft-product-boundary.mjs +12 -0
  146. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +46 -0
  147. package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +85 -20
  148. package/templates/starter/scripts/toolcraft-product-evidence-import-policy.mjs +43 -0
  149. package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +168 -0
  150. package/templates/starter/scripts/toolcraft-typescript-analysis.d.mts +73 -0
  151. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade-test-helpers.mjs +318 -0
  152. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +155 -206
  153. package/templates/starter/scripts/toolcraft-verification-inventory.mjs +1 -0
  154. package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +51 -0
  155. package/templates/starter/scripts/toolcraft-vite-env-activation.test.mjs +128 -59
  156. package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +113 -0
  157. package/templates/starter/src/app/acceptance/output-background-rules.ts +1 -1
  158. package/templates/starter/src/app/acceptance/output-export.ts +13 -2
  159. package/templates/starter/src/app/acceptance/types.ts +7 -0
  160. package/templates/starter/src/app/acceptance/validate-coverage.ts +2 -1
  161. package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +131 -0
  162. package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +14 -4
  163. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +42 -1
  164. package/templates/starter/src/app/starter-acceptance.ts +2 -0
  165. package/templates/starter/src/app/starter-performance-test-utils.ts +103 -0
  166. package/templates/starter/src/app/starter-performance.gates.test.ts +231 -278
  167. package/templates/starter/src/app/starter-performance.lifecycle.test.ts +261 -0
  168. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +2 -0
  169. package/templates/starter/src/routes/index.tsx +1 -0
  170. package/templates/runtime/export/export.ts +0 -166
@@ -39,83 +39,6 @@ function createExportSchema(
39
39
  }
40
40
 
41
41
  describe("ControlsPanel scene export actions", () => {
42
- it("skips product bounds for finite export and calls them once for infinite export", () => {
43
- const schema = createExportSchema();
44
- const provider = vi.fn(() => [
45
- { height: 10, width: 10, x: 100, y: 50 },
46
- ]);
47
- let finiteFrame: unknown;
48
- const finite = renderControlsPanelWithSchema(schema, {
49
- onPanelAction: ({ resolveSceneExportFrame }) => {
50
- finiteFrame = resolveSceneExportFrame();
51
- },
52
- sceneExport: {
53
- boundsProvider: provider,
54
- boundsRequired: true,
55
- visibility: defaultToolcraftRuntimeSceneVisibility,
56
- },
57
- });
58
-
59
- fireEvent.click(screen.getByRole("button", { name: "Export PNG" }));
60
- expect(finiteFrame).toEqual({
61
- frame: { height: 180, width: 320, x: 0, y: 0 },
62
- ok: true,
63
- });
64
- expect(provider).not.toHaveBeenCalled();
65
- finite.unmount();
66
-
67
- let infiniteFrame: unknown;
68
- renderControlsPanelWithSchema(
69
- schema,
70
- {
71
- onPanelAction: ({ resolveSceneExportFrame }) => {
72
- infiniteFrame = resolveSceneExportFrame({
73
- timeRange: { endSeconds: 8, startSeconds: 0 },
74
- });
75
- },
76
- sceneExport: {
77
- boundsProvider: provider,
78
- boundsRequired: true,
79
- visibility: defaultToolcraftRuntimeSceneVisibility,
80
- },
81
- },
82
- {
83
- canvas: { mode: "infinite" },
84
- layers: [
85
- {
86
- displayName: "Image",
87
- id: "image-layer",
88
- kind: "layer",
89
- name: "image",
90
- visible: true,
91
- },
92
- ],
93
- mediaAssets: [
94
- {
95
- assetKind: "image",
96
- dataUrl: "data:image/png;base64,test",
97
- fileName: "image.png",
98
- id: "image",
99
- layerId: "image-layer",
100
- mimeType: "image/png",
101
- position: { x: 0, y: 0 },
102
- size: { height: 20, unit: "px", width: 20 },
103
- },
104
- ],
105
- },
106
- );
107
-
108
- fireEvent.click(screen.getByRole("button", { name: "Export PNG" }));
109
- expect(provider).toHaveBeenCalledTimes(1);
110
- expect(provider).toHaveBeenCalledWith(
111
- expect.objectContaining({ timeRange: { endSeconds: 8, startSeconds: 0 } }),
112
- );
113
- expect(infiniteFrame).toEqual({
114
- frame: { height: 70, width: 120, x: -10, y: -10 },
115
- ok: true,
116
- });
117
- });
118
-
119
42
  it("shows only feedback reported by the latest panel action", async () => {
120
43
  const schema = createExportSchema([
121
44
  { label: "First export", value: "first", variant: "secondary" },
@@ -143,17 +66,27 @@ describe("ControlsPanel scene export actions", () => {
143
66
  });
144
67
 
145
68
  it("presents a missing required bounds provider as typed visible feedback", async () => {
69
+ const onPanelAction = vi.fn();
146
70
  renderControlsPanelWithSchema(
147
- createExportSchema(),
148
- {
149
- onPanelAction: ({ reportFeedback, resolveSceneExportFrame }) => {
150
- const result = resolveSceneExportFrame();
151
- if (!result.ok) reportFeedback(result);
71
+ createExportSchema([
72
+ {
73
+ command: "controls.apply",
74
+ label: "Export PNG",
75
+ role: "export-image",
76
+ value: "export",
77
+ variant: "default",
152
78
  },
79
+ ]),
80
+ {
153
81
  sceneExport: {
154
- boundsRequired: true,
82
+ exportRenderer: {
83
+ baseFileName: "scene",
84
+ renderFrame: vi.fn(),
85
+ },
86
+ productSceneRequired: true,
155
87
  visibility: defaultToolcraftRuntimeSceneVisibility,
156
88
  },
89
+ onPanelAction,
157
90
  },
158
91
  { canvas: { mode: "infinite" } },
159
92
  );
@@ -164,6 +97,7 @@ describe("ControlsPanel scene export actions", () => {
164
97
  "scene-bounds-unavailable",
165
98
  );
166
99
  expect(alert.textContent).toContain("does not provide export bounds");
100
+ expect(onPanelAction).not.toHaveBeenCalled();
167
101
  });
168
102
 
169
103
  it("presents rejected typed export failures as visible feedback", async () => {
@@ -3,18 +3,11 @@
3
3
  import * as React from "react";
4
4
 
5
5
  import {
6
- resolveToolcraftExportFrame,
6
+ ToolcraftArtifactExportError,
7
7
  ToolcraftSceneExportError,
8
- type ToolcraftExportFrame,
9
- type ToolcraftExportFrameResult,
10
8
  } from "../../../export";
11
9
  import type { ToolcraftRendererPipelineClient } from "../../../rendering";
12
- import {
13
- defaultToolcraftRuntimeSceneVisibility,
14
- resolveToolcraftSceneBounds,
15
- type ToolcraftProductSceneBoundsProvider,
16
- type ToolcraftRuntimeSceneVisibility,
17
- } from "../../../scene";
10
+ import { defaultToolcraftRuntimeSceneVisibility } from "../../../scene";
18
11
  import type {
19
12
  ToolcraftActionCommand,
20
13
  ToolcraftActionSchema,
@@ -25,46 +18,31 @@ import type {
25
18
  } from "../../../state/types";
26
19
  import { getToolcraftCanvasFrame } from "../../../state/canvas-frame";
27
20
  import type { ActionControlRunAction } from "../renderers/controls-panel-action-renderer";
28
- import {
29
- renderToolcraftRuntimeSceneToCanvas,
30
- type ToolcraftRuntimeSceneExportResult,
31
- } from "../../canvas/runtime-scene-export";
21
+ import { renderToolcraftRuntimeSceneToCanvas } from "../../canvas/runtime-scene-export";
32
22
  import { useToolcraftPipeline } from "../../app-shell/use-toolcraft-pipeline";
33
23
  import { useToolcraftSourceAssetCoordinator } from "../../app-shell/toolcraft-source-asset-context";
34
- import {
35
- renderToolcraftModelsToCanvas,
36
- type ToolcraftRenderModelsToCanvas,
37
- } from "../../model-rendering/model-export";
38
24
  import { useOptionalToolcraftModelRenderHost } from "../../model-rendering/model-render-provider";
25
+ import {
26
+ runToolcraftExportAction,
27
+ type ToolcraftControlsSceneExport,
28
+ } from "./export-action-runner";
29
+
30
+ export type { ToolcraftControlsSceneExport } from "./export-action-runner";
39
31
 
40
32
  export type ToolcraftPanelActionFeedback = Readonly<{
41
33
  code: string;
42
34
  message: string;
43
35
  }>;
44
36
 
45
- export type ToolcraftControlsSceneExport = Readonly<{
46
- boundsProvider?: ToolcraftProductSceneBoundsProvider;
47
- boundsRequired: boolean;
48
- visibility: ToolcraftRuntimeSceneVisibility;
49
- }>;
50
-
51
37
  export type ToolcraftPanelActionContext = {
52
38
  action: ToolcraftActionSchema;
53
39
  dispatch: React.Dispatch<ToolcraftCommand>;
54
- renderRuntimeSceneToCanvas: (
55
- canvas: HTMLCanvasElement,
56
- frame: ToolcraftExportFrame,
57
- ) => Promise<ToolcraftRuntimeSceneExportResult>;
58
40
  reportProgress: (progress: number) => void;
59
41
  reportFeedback: (feedback: ToolcraftPanelActionFeedback) => void;
60
- renderModelsToCanvas: ToolcraftRenderModelsToCanvas;
61
42
  resolveMediaResource: (
62
43
  resourceRef: string,
63
44
  options: Readonly<{ signal: AbortSignal }>,
64
45
  ) => Promise<Uint8Array | null>;
65
- resolveSceneExportFrame: (options?: Readonly<{
66
- timeRange?: Readonly<{ endSeconds: number; startSeconds: number }>;
67
- }>) => ToolcraftExportFrameResult;
68
46
  rendererPipeline?: ToolcraftRendererPipelineClient | null;
69
47
  state: ToolcraftState;
70
48
  };
@@ -90,42 +68,10 @@ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
90
68
  function noopReportProgress(): void {}
91
69
 
92
70
  const defaultSceneExport: ToolcraftControlsSceneExport = Object.freeze({
93
- boundsRequired: false,
71
+ productSceneRequired: false,
94
72
  visibility: defaultToolcraftRuntimeSceneVisibility,
95
73
  });
96
74
 
97
- function unavailableProductBounds(message: string): ToolcraftExportFrameResult {
98
- return { code: "scene-bounds-unavailable", message, ok: false };
99
- }
100
-
101
- function resolveActionSceneExportFrame(
102
- sceneExport: ToolcraftControlsSceneExport,
103
- state: ToolcraftState,
104
- timeRange?: Readonly<{ endSeconds: number; startSeconds: number }>,
105
- ): ToolcraftExportFrameResult {
106
- const finiteFrame = resolveToolcraftExportFrame(state, null);
107
- if (finiteFrame.ok) return finiteFrame;
108
-
109
- if (sceneExport.boundsRequired && !sceneExport.boundsProvider) {
110
- return unavailableProductBounds(
111
- "This infinite product scene does not provide export bounds.",
112
- );
113
- }
114
-
115
- let productRects: unknown = [];
116
- try {
117
- productRects = sceneExport.boundsProvider?.({ state, timeRange }) ?? [];
118
- } catch {
119
- return unavailableProductBounds("Product scene bounds could not be resolved.");
120
- }
121
- const bounds = resolveToolcraftSceneBounds(
122
- state,
123
- productRects,
124
- sceneExport.visibility,
125
- );
126
- return resolveToolcraftExportFrame(state, bounds);
127
- }
128
-
129
75
  function clampFooterActionProgress(progress: number): number {
130
76
  if (!Number.isFinite(progress)) {
131
77
  return 0;
@@ -228,7 +174,10 @@ export function useControlsPanelActions({
228
174
 
229
175
  void Promise.resolve(result)
230
176
  .catch((error: unknown) => {
231
- if (error instanceof ToolcraftSceneExportError) {
177
+ if (
178
+ error instanceof ToolcraftSceneExportError ||
179
+ error instanceof ToolcraftArtifactExportError
180
+ ) {
232
181
  reportFeedback(error.feedback);
233
182
  } else {
234
183
  console.error("Toolcraft panel action failed.", error);
@@ -257,7 +206,11 @@ export function useControlsPanelActions({
257
206
  setPanelActionFeedback(feedback);
258
207
  }
259
208
  };
260
- const command = action.command ?? (onPanelAction ? null : getActionCommand(action));
209
+ const runtimeOwnsExport =
210
+ action.role === "export-image" || action.role === "export-video";
211
+ const command = runtimeOwnsExport
212
+ ? null
213
+ : action.command ?? (onPanelAction ? null : getActionCommand(action));
261
214
 
262
215
  if (command) {
263
216
  dispatch({ type: command });
@@ -270,36 +223,41 @@ export function useControlsPanelActions({
270
223
  const actionState = getState();
271
224
  let result: PromiseLike<unknown> | void;
272
225
  try {
273
- result = onPanelAction?.({
226
+ const reportProgress =
227
+ footerActionProgressTracker?.reportProgress ?? noopReportProgress;
228
+ const exportResult = runToolcraftExportAction({
274
229
  action,
275
- dispatch,
276
- renderRuntimeSceneToCanvas: (canvas, frame) =>
230
+ renderRuntimeScene: (canvas, frame, state) =>
277
231
  renderToolcraftRuntimeSceneToCanvas({
278
232
  canvas,
279
- canvasFrame: getToolcraftCanvasFrame(actionState.canvas),
233
+ canvasFrame: getToolcraftCanvasFrame(state.canvas),
280
234
  exportFrame: frame,
281
235
  host: modelRenderHost,
282
236
  resolveImageResource: sourceAssetCoordinator.resolveResource,
283
- state: actionState,
237
+ state,
284
238
  visibility: sceneExport.visibility,
285
239
  }),
286
- reportFeedback,
287
- reportProgress:
288
- footerActionProgressTracker?.reportProgress ?? noopReportProgress,
289
- renderModelsToCanvas: (canvas) =>
290
- renderToolcraftModelsToCanvas(modelRenderHost, actionState, canvas),
291
- resolveMediaResource,
292
- resolveSceneExportFrame: (resolveOptions) =>
293
- resolveActionSceneExportFrame(
294
- sceneExport,
295
- actionState,
296
- resolveOptions?.timeRange,
297
- ),
298
240
  rendererPipeline,
241
+ reportProgress,
242
+ sceneExport,
299
243
  state: actionState,
300
244
  });
245
+ result =
246
+ exportResult ??
247
+ onPanelAction?.({
248
+ action,
249
+ dispatch,
250
+ reportFeedback,
251
+ reportProgress,
252
+ resolveMediaResource,
253
+ rendererPipeline,
254
+ state: actionState,
255
+ });
301
256
  } catch (error) {
302
- if (error instanceof ToolcraftSceneExportError) {
257
+ if (
258
+ error instanceof ToolcraftSceneExportError ||
259
+ error instanceof ToolcraftArtifactExportError
260
+ ) {
303
261
  reportFeedback(error.feedback);
304
262
  result = undefined;
305
263
  } else {
@@ -0,0 +1,62 @@
1
+ import {
2
+ exportToolcraftImageArtifact,
3
+ exportToolcraftVideoArtifact,
4
+ type ToolcraftArtifactExportRequest,
5
+ type ToolcraftExportFrame,
6
+ type ToolcraftProductExportRenderer,
7
+ } from "../../../export";
8
+ import type { ToolcraftRendererPipelineClient } from "../../../rendering";
9
+ import type {
10
+ ToolcraftProductSceneBoundsProvider,
11
+ ToolcraftRuntimeSceneVisibility,
12
+ } from "../../../scene";
13
+ import type { ToolcraftActionSchema } from "../../../schema/types";
14
+ import type { ToolcraftState } from "../../../state/types";
15
+
16
+ export type ToolcraftControlsSceneExport = Readonly<{
17
+ boundsProvider?: ToolcraftProductSceneBoundsProvider;
18
+ exportRenderer?: ToolcraftProductExportRenderer;
19
+ productSceneRequired: boolean;
20
+ visibility: ToolcraftRuntimeSceneVisibility;
21
+ }>;
22
+
23
+ export type ToolcraftExportActionRequest = Readonly<{
24
+ action: ToolcraftActionSchema;
25
+ renderRuntimeScene: (
26
+ canvas: HTMLCanvasElement,
27
+ frame: ToolcraftExportFrame,
28
+ state: ToolcraftState,
29
+ ) => Promise<unknown>;
30
+ rendererPipeline: ToolcraftRendererPipelineClient | null;
31
+ reportProgress: (progress: number) => void;
32
+ sceneExport: ToolcraftControlsSceneExport;
33
+ state: ToolcraftState;
34
+ }>;
35
+
36
+ function createSharedArtifactRequest(
37
+ request: ToolcraftExportActionRequest,
38
+ ): ToolcraftArtifactExportRequest {
39
+ return {
40
+ boundsProvider: request.sceneExport.boundsProvider,
41
+ exportRenderer: request.sceneExport.exportRenderer,
42
+ productSceneRequired: request.sceneExport.productSceneRequired,
43
+ renderRuntimeScene: request.renderRuntimeScene,
44
+ rendererPipeline: request.rendererPipeline,
45
+ reportProgress: request.reportProgress,
46
+ state: request.state,
47
+ visibility: request.sceneExport.visibility,
48
+ };
49
+ }
50
+
51
+ export function runToolcraftExportAction(
52
+ request: ToolcraftExportActionRequest,
53
+ ): PromiseLike<unknown> | null {
54
+ switch (request.action.role) {
55
+ case "export-image":
56
+ return exportToolcraftImageArtifact(createSharedArtifactRequest(request));
57
+ case "export-video":
58
+ return exportToolcraftVideoArtifact(createSharedArtifactRequest(request));
59
+ default:
60
+ return null;
61
+ }
62
+ }
@@ -195,4 +195,34 @@ describe("ToolcraftPanelHost runtime placement", () => {
195
195
  );
196
196
  });
197
197
  });
198
+
199
+ it("keeps the CSS-anchored default placement during viewport resize", async () => {
200
+ const { container } = renderToolbar();
201
+ const host = container.querySelector<HTMLElement>(
202
+ "[data-slot='toolcraft-runtime-panel-host']",
203
+ );
204
+
205
+ if (!host) {
206
+ throw new Error("Expected the toolbar panel host to render.");
207
+ }
208
+ vi.spyOn(host, "getBoundingClientRect").mockReturnValue({
209
+ bottom: 900,
210
+ height: 100,
211
+ left: 1_500,
212
+ right: 1_700,
213
+ top: 800,
214
+ width: 200,
215
+ x: 1_500,
216
+ y: 800,
217
+ toJSON: () => ({}),
218
+ });
219
+
220
+ fireEvent(window, new Event("resize"));
221
+
222
+ await waitFor(() => {
223
+ expect(screen.getByTestId("toolbar-placement").textContent).toBe(
224
+ "0,0,free",
225
+ );
226
+ });
227
+ });
198
228
  });
@@ -191,6 +191,10 @@ function usePanelSnapControls({
191
191
  return;
192
192
  }
193
193
 
194
+ if (x.get() === 0 && y.get() === 0 && snapEdge === undefined) {
195
+ return;
196
+ }
197
+
194
198
  const currentPlacement = {
195
199
  offset: { x: x.get(), y: y.get() },
196
200
  ...(snapEdge ? { snapEdge } : {}),
@@ -36,8 +36,7 @@ export function createToolcraftRuntimeSceneVisibility(
36
36
 
37
37
  export type ToolcraftProductSceneBoundsProvider = (
38
38
  context: Readonly<{
39
- state: ToolcraftState;
40
- timeRange?: Readonly<{ endSeconds: number; startSeconds: number }>;
39
+ state: Readonly<ToolcraftState>;
41
40
  }>,
42
41
  ) => readonly ToolcraftSceneRect[];
43
42
 
@@ -0,0 +1,62 @@
1
+ import type {
2
+ ResolvedToolcraftCanvasRenderScaleSchema,
3
+ ToolcraftCanvasRenderScaleSchema,
4
+ } from "./types";
5
+
6
+ export const TOOLCRAFT_CANVAS_RENDER_SCALE = Object.freeze({
7
+ defaultValue: 2,
8
+ max: 2,
9
+ min: 1,
10
+ step: 0.25,
11
+ });
12
+
13
+ const minimumCanvasRenderScaleStep = 0.01;
14
+ const canvasRenderScaleStepTolerance = 1e-9;
15
+
16
+ function resolveCanvasRenderScaleStep(value: number | undefined): number {
17
+ if (value === undefined) {
18
+ return TOOLCRAFT_CANVAS_RENDER_SCALE.step;
19
+ }
20
+
21
+ const maximumCanvasRenderScaleStep =
22
+ TOOLCRAFT_CANVAS_RENDER_SCALE.max - TOOLCRAFT_CANVAS_RENDER_SCALE.min;
23
+
24
+ if (
25
+ typeof value !== "number" ||
26
+ !Number.isFinite(value) ||
27
+ value < minimumCanvasRenderScaleStep ||
28
+ value > maximumCanvasRenderScaleStep
29
+ ) {
30
+ throw new Error(
31
+ "Canvas render scale step must be a finite value between 0.01 and 1.",
32
+ );
33
+ }
34
+
35
+ const stepCount = maximumCanvasRenderScaleStep / value;
36
+
37
+ if (Math.abs(stepCount - Math.round(stepCount)) > canvasRenderScaleStepTolerance) {
38
+ throw new Error("Canvas render scale step must divide the canonical range exactly.");
39
+ }
40
+
41
+ return value;
42
+ }
43
+
44
+ export function resolveToolcraftCanvasRenderScale(
45
+ renderScale: ToolcraftCanvasRenderScaleSchema | undefined,
46
+ ): ResolvedToolcraftCanvasRenderScaleSchema {
47
+ if (!renderScale) {
48
+ return {
49
+ ...TOOLCRAFT_CANVAS_RENDER_SCALE,
50
+ enabled: false,
51
+ };
52
+ }
53
+
54
+ return {
55
+ ...TOOLCRAFT_CANVAS_RENDER_SCALE,
56
+ enabled: true,
57
+ step:
58
+ renderScale === true
59
+ ? TOOLCRAFT_CANVAS_RENDER_SCALE.step
60
+ : resolveCanvasRenderScaleStep(renderScale.step),
61
+ };
62
+ }
@@ -140,7 +140,7 @@ describe("Toolcraft control section identity", () => {
140
140
  )?.id,
141
141
  ).toBe(legacyId);
142
142
  expect(
143
- defineToolcraft(resolved as ToolcraftAppSchema).panels.controls?.sections.find(
143
+ defineToolcraft(resolved as unknown as ToolcraftAppSchema).panels.controls?.sections.find(
144
144
  (section) => section.id.startsWith("legacy."),
145
145
  )?.id,
146
146
  ).toBe(legacyId);
@@ -38,7 +38,7 @@ describe("Toolcraft control section identity roundtrips", () => {
38
38
  },
39
39
  },
40
40
  });
41
- const roundTripped = defineToolcraft(resolved as ToolcraftAppSchema);
41
+ const roundTripped = defineToolcraft(resolved as unknown as ToolcraftAppSchema);
42
42
  const resolvedIds = resolved.panels.controls?.sections.map((section) => section.id);
43
43
  const roundTrippedIds = roundTripped.panels.controls?.sections.map(
44
44
  (section) => section.id,
@@ -101,7 +101,7 @@ describe("Toolcraft control section identity roundtrips", () => {
101
101
  const productIds = resolved.panels.controls?.sections
102
102
  .map((section) => section.id)
103
103
  .filter((id) => !id.startsWith("runtime."));
104
- const roundTripped = defineToolcraft(resolved as ToolcraftAppSchema);
104
+ const roundTripped = defineToolcraft(resolved as unknown as ToolcraftAppSchema);
105
105
  const roundTrippedProductIds = roundTripped.panels.controls?.sections
106
106
  .map((section) => section.id)
107
107
  .filter((id) => !id.startsWith("runtime."));
@@ -147,7 +147,7 @@ describe("Toolcraft control section identity roundtrips", () => {
147
147
  productIds?.every((id) => (id.match(/\.part-/gu) ?? []).length === 2),
148
148
  ).toBe(true);
149
149
 
150
- const roundTripped = defineToolcraft(resolved as ToolcraftAppSchema);
150
+ const roundTripped = defineToolcraft(resolved as unknown as ToolcraftAppSchema);
151
151
  const roundTrippedProductIds = roundTripped.panels.controls?.sections
152
152
  .map((section) => section.id)
153
153
  .filter((id) => !id.startsWith("runtime."));
@@ -118,7 +118,7 @@ describe("defineToolcraft runtime setup background controls", () => {
118
118
 
119
119
  it("keeps the normalized background pair when a resolved schema is defined again", () => {
120
120
  const first = defineToolcraft(createBackgroundSchema());
121
- const second = defineToolcraft(first);
121
+ const second = defineToolcraft(first as unknown as ToolcraftAppSchema);
122
122
  const setupSection = second.panels.controls?.sections[0];
123
123
 
124
124
  expect(second.panels.controls?.sections.map((section) => section.title)).toEqual([
@@ -1,6 +1,40 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
3
  import { defineToolcraft } from "./define-toolcraft";
4
+ import type { ToolcraftCanvasRenderScaleSchema } from "./types";
5
+
6
+ const authoredRenderScaleStep: ToolcraftCanvasRenderScaleSchema = { step: 0.5 };
7
+ const authoredRenderScaleDefaultValue = { defaultValue: 1, step: 0.5 };
8
+ const authoredRenderScaleEnabled = { enabled: true, step: 0.5 };
9
+ const authoredRenderScaleMax = { max: 1, step: 0.5 };
10
+ const authoredRenderScaleMin = { min: 1, step: 0.5 };
11
+
12
+ // @ts-expect-error Authored render scale cannot select its resolved default value.
13
+ const authoredRenderScaleDefaultValueAssignment: ToolcraftCanvasRenderScaleSchema =
14
+ authoredRenderScaleDefaultValue;
15
+
16
+ // @ts-expect-error Authored render scale is always enabled when provided.
17
+ const authoredRenderScaleEnabledAssignment: ToolcraftCanvasRenderScaleSchema = {
18
+ ...authoredRenderScaleEnabled,
19
+ };
20
+
21
+ // @ts-expect-error Authored render scale cannot change the canonical maximum.
22
+ const authoredRenderScaleMaxAssignment: ToolcraftCanvasRenderScaleSchema = authoredRenderScaleMax;
23
+
24
+ // @ts-expect-error Authored render scale cannot change the canonical minimum.
25
+ const authoredRenderScaleMinAssignment: ToolcraftCanvasRenderScaleSchema = {
26
+ ...authoredRenderScaleMin,
27
+ };
28
+
29
+ void authoredRenderScaleStep;
30
+ void authoredRenderScaleDefaultValue;
31
+ void authoredRenderScaleEnabled;
32
+ void authoredRenderScaleMax;
33
+ void authoredRenderScaleMin;
34
+ void authoredRenderScaleDefaultValueAssignment;
35
+ void authoredRenderScaleEnabledAssignment;
36
+ void authoredRenderScaleMaxAssignment;
37
+ void authoredRenderScaleMinAssignment;
4
38
 
5
39
  describe("defineToolcraft runtime setup render scale and timeline controls", () => {
6
40
  it("appends raster render scale to the technical setup controls when enabled", () => {
@@ -95,11 +129,11 @@ describe("defineToolcraft runtime setup render scale and timeline controls", ()
95
129
  ]);
96
130
  });
97
131
 
98
- it("keeps timeline standalone with render scale for fixed output sizing", () => {
132
+ it("uses the canonical authored raster scale with a custom step", () => {
99
133
  const app = defineToolcraft({
100
134
  canvas: {
101
135
  enabled: true,
102
- renderScale: { defaultValue: 1.5 },
136
+ renderScale: { step: 0.5 },
103
137
  size: { height: 900, unit: "px", width: 1440 },
104
138
  sizing: { mode: "fixed-output" },
105
139
  },
@@ -120,10 +154,20 @@ describe("defineToolcraft runtime setup render scale and timeline controls", ()
120
154
  "timelineExtended",
121
155
  ]);
122
156
  expect(setupSection?.controls.canvasRenderScale).toMatchObject({
123
- defaultValue: 1.5,
157
+ defaultValue: 2,
158
+ max: 2,
159
+ min: 1,
160
+ step: 0.5,
124
161
  target: "canvas.renderScale",
125
162
  type: "slider",
126
163
  });
164
+ expect(app.canvas.renderScale).toEqual({
165
+ defaultValue: 2,
166
+ enabled: true,
167
+ max: 2,
168
+ min: 1,
169
+ step: 0.5,
170
+ });
127
171
  expect(setupSection?.controls.timelineExtended).toMatchObject({
128
172
  defaultValue: false,
129
173
  label: "Timeline",
@@ -133,4 +177,28 @@ describe("defineToolcraft runtime setup render scale and timeline controls", ()
133
177
  expect(setupSection?.controls.timelineExtended).not.toHaveProperty("description");
134
178
  expect(setupSection?.layoutGroups ?? []).toEqual([]);
135
179
  });
180
+
181
+ it("rejects an authored step that cannot reach the canonical maximum", () => {
182
+ expect(() =>
183
+ defineToolcraft({
184
+ canvas: {
185
+ enabled: true,
186
+ renderScale: { step: 0.3 },
187
+ },
188
+ panels: {},
189
+ }),
190
+ ).toThrow("Canvas render scale step must divide the canonical range exactly.");
191
+ });
192
+
193
+ it.each([0, Number.NaN])("rejects invalid authored step %s", (step) => {
194
+ expect(() =>
195
+ defineToolcraft({
196
+ canvas: {
197
+ enabled: true,
198
+ renderScale: { step },
199
+ },
200
+ panels: {},
201
+ }),
202
+ ).toThrow("Canvas render scale step must be a finite value between 0.01 and 1.");
203
+ });
136
204
  });
@@ -1,12 +1,12 @@
1
1
  import { createToolcraftAssemblyContract } from "./assembly-contract";
2
2
  import { resolveToolcraftAppCapabilities } from "./app-capabilities";
3
3
  import { resolveToolcraftAppIdentity } from "./app-identity";
4
+ import { resolveToolcraftCanvasRenderScale } from "./canvas-render-scale";
4
5
  import { normalizeToolcraftPanels } from "./panels-schema-normalization";
5
6
  import { resolveToolcraftPersistencePlan } from "./persistence-plan";
6
7
  import { resolveToolcraftSettingsTransfer } from "./runtime-setup-section";
7
8
  import {
8
9
  defaultToolcraftCanvasSize,
9
- resolveToolcraftCanvasRenderScale,
10
10
  resolveToolcraftCanvasSizing,
11
11
  resolveToolcraftExport,
12
12
  resolveToolcraftMedia,