@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.
- package/package.json +1 -1
- package/src/generate-performance-iteration-execution.test.mjs +2 -2
- package/src/generate-test-demand-only-lifecycle.mjs +14 -3
- package/src/generate-test-dependency-sandbox.mjs +42 -5
- package/src/generate-test-doc-assertions.mjs +68 -18
- package/src/generate-test-doc-assertions.test.mjs +22 -0
- package/src/generate-test-file-assertions.mjs +43 -76
- package/src/generate-test-output-assertions.mjs +95 -0
- package/src/generate-test-package-assertions.mjs +6 -0
- package/src/generate-test-performance-boundary-assertions.mjs +45 -0
- package/src/generate-test-product-fixture.mjs +1 -0
- package/src/generate-test-product-fixture.test.mjs +18 -0
- package/src/generate-test-product-iteration.mjs +1 -0
- package/src/generate-test-product-iteration.test.mjs +1 -0
- package/src/generate-test-standalone-delivery-stages.mjs +14 -6
- package/src/generate.test.mjs +14 -0
- package/templates/runtime/contracts/component-contracts.choices.test.ts +12 -9
- package/templates/runtime/contracts/component-contracts.choices.ts +10 -9
- package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +17 -2
- package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +1 -1
- package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +1 -1
- package/templates/runtime/contracts/component-contracts.runtime.ts +3 -2
- package/templates/runtime/contracts/decision-contracts.test.ts +25 -4
- package/templates/runtime/contracts/decision-contracts.ts +3 -3
- package/templates/runtime/contracts/performance-verification-policy.test.ts +59 -6
- package/templates/runtime/contracts/performance-verification-policy.ts +5 -2
- package/templates/runtime/export/artifact-download.test.ts +71 -0
- package/templates/runtime/export/artifact-download.ts +80 -0
- package/templates/runtime/export/artifact-export-request.ts +23 -0
- package/templates/runtime/export/artifact-export-settings.test.ts +75 -0
- package/templates/runtime/export/artifact-export-settings.ts +96 -0
- package/templates/runtime/export/artifact-frame-renderer.test.ts +107 -0
- package/templates/runtime/export/artifact-frame-renderer.ts +96 -0
- package/templates/runtime/export/artifact-frame-state.test.ts +54 -0
- package/templates/runtime/export/artifact-frame-state.ts +44 -0
- package/templates/runtime/export/artifact-scene-frame.test.ts +84 -0
- package/templates/runtime/export/artifact-scene-frame.ts +118 -0
- package/templates/runtime/export/export-background.ts +65 -0
- package/templates/runtime/export/export-error.ts +43 -0
- package/templates/runtime/export/export.test.ts +6 -186
- package/templates/runtime/export/image-artifact-export.test.ts +111 -0
- package/templates/runtime/export/image-artifact-export.ts +145 -0
- package/templates/runtime/export/index.ts +14 -1
- package/templates/runtime/export/product-export-renderer.test.ts +77 -0
- package/templates/runtime/export/product-export-renderer.ts +63 -0
- package/templates/runtime/export/video-artifact-export.test.ts +166 -0
- package/templates/runtime/export/video-artifact-export.ts +164 -0
- package/templates/runtime/export/video-encoding-backend.ts +124 -0
- package/templates/runtime/export/video-encoding-policy.test.ts +68 -0
- package/templates/runtime/export/video-encoding-policy.ts +86 -0
- package/templates/runtime/export/video-frame-schedule.test.ts +58 -0
- package/templates/runtime/export/video-frame-schedule.ts +42 -0
- package/templates/runtime/index.ts +1 -0
- package/templates/runtime/react/app-shell/runtime-public-api-runtime.test.tsx +291 -0
- package/templates/runtime/react/app-shell/runtime-public-api-test-support.ts +21 -0
- package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +4 -303
- package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +12 -27
- package/templates/runtime/react/app-shell/toolcraft-app.tsx +22 -4
- package/templates/runtime/react/app-shell/toolcraft-source-asset-context.test.tsx +16 -57
- package/templates/runtime/react/app-shell/toolcraft-source-asset-context.tsx +75 -434
- package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.test.ts +194 -0
- package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.ts +227 -0
- package/templates/runtime/react/app-shell/toolcraft-source-asset-provider-ownership.test.tsx +243 -0
- package/templates/runtime/react/app-shell/toolcraft-source-asset-test-support.ts +51 -0
- package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.test.ts +138 -0
- package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.ts +378 -0
- package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +7 -12
- package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +17 -83
- package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +42 -84
- package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +62 -0
- package/templates/runtime/react/panel-host/panel-host-runtime-placement.test.tsx +30 -0
- package/templates/runtime/react/panel-host/panel-host.tsx +4 -0
- package/templates/runtime/scene/scene-bounds.ts +1 -2
- package/templates/runtime/schema/canvas-render-scale.ts +62 -0
- package/templates/runtime/schema/controls-panel-section-id.test.ts +1 -1
- package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +3 -3
- package/templates/runtime/schema/define-toolcraft.setup-background.test.ts +1 -1
- package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +71 -3
- package/templates/runtime/schema/define-toolcraft.ts +1 -1
- package/templates/runtime/schema/schema-resolvers.ts +0 -56
- package/templates/runtime/schema/types.ts +4 -4
- package/templates/runtime/state/canvas-background-state.ts +6 -0
- package/templates/runtime/testing/performance-render-plan-integration.test.ts +64 -0
- package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +18 -1
- package/templates/starter/AGENTS.md +8 -7
- package/templates/starter/NOTICE.md +6 -0
- package/templates/starter/docs/toolcraft/README.md +2 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +8 -6
- package/templates/starter/docs/toolcraft/agent-worklog.md +3 -3
- package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -3
- package/templates/starter/docs/toolcraft/core/media-upload.md +2 -2
- package/templates/starter/docs/toolcraft/core/performance.md +14 -3
- package/templates/starter/docs/toolcraft/core/runtime-boundary.md +3 -1
- package/templates/starter/docs/toolcraft/core/setup-export.md +16 -12
- package/templates/starter/docs/toolcraft/core/timeline-animation.md +4 -4
- package/templates/starter/docs/toolcraft/performance.md +20 -6
- package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +2 -6
- package/templates/starter/docs/toolcraft/workflow.md +7 -2
- package/templates/starter/e2e/app-browser-control-coverage.spec.ts +33 -0
- package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +43 -1
- package/templates/starter/e2e/app-browser-render-scale-evidence.spec.ts +60 -6
- package/templates/starter/e2e/app-browser-render-scale-requirements.spec.ts +77 -1
- package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +8 -0
- package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +35 -0
- package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +4 -1
- package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +0 -1
- package/templates/starter/e2e/app-browser-shell.spec.ts +13 -5
- package/templates/starter/e2e/app-performance-path-adapters.ts +8 -1
- package/templates/starter/e2e/app-performance.spec.ts +16 -9
- package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +9 -330
- package/templates/starter/e2e/browser-infinity-canvas-unavailable-image-evidence.ts +370 -0
- package/templates/starter/e2e/browser-media-export-evidence.spec.ts +77 -0
- package/templates/starter/e2e/browser-media-export-evidence.ts +208 -0
- package/templates/starter/e2e/browser-render-scale-evidence.ts +37 -29
- package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +4 -1
- package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +43 -9
- package/templates/starter/e2e/browser-standard-timeline-evidence.ts +106 -0
- package/templates/starter/e2e/decoded-pixel-observation.spec.ts +74 -0
- package/templates/starter/e2e/decoded-pixel-observation.ts +227 -0
- package/templates/starter/e2e/export-artifact-helpers.ts +206 -25
- package/templates/starter/e2e/image-artifact-inspection.spec.ts +41 -0
- package/templates/starter/e2e/image-artifact-inspection.ts +99 -0
- package/templates/starter/e2e/performance-canvas-helpers.ts +22 -21
- package/templates/starter/e2e/performance-canvas-quality-guard.spec.ts +312 -0
- package/templates/starter/e2e/performance-canvas-quality-guard.ts +196 -0
- package/templates/starter/e2e/performance-canvas-quality-observer.ts +351 -0
- package/templates/starter/e2e/performance-control-layout-helpers.ts +2 -2
- package/templates/starter/e2e/performance-path-adapter-contract.ts +4 -4
- package/templates/starter/e2e/performance-path-helpers.spec.ts +203 -70
- package/templates/starter/e2e/performance-path-helpers.ts +210 -91
- package/templates/starter/e2e/performance-pipeline-evidence-continuity.spec.ts +76 -0
- package/templates/starter/e2e/performance-pipeline-evidence-test-fixtures.ts +11 -2
- package/templates/starter/e2e/performance-pipeline-invariants.ts +47 -4
- package/templates/starter/e2e/performance-render-scale-runner.spec.ts +140 -38
- package/templates/starter/e2e/video-artifact-inspection.spec.ts +64 -0
- package/templates/starter/e2e/video-artifact-inspection.ts +281 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +1 -0
- package/templates/starter/scripts/check-toolcraft-docs.mjs +17 -0
- package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +60 -0
- package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +7 -2
- package/templates/starter/scripts/toolcraft-delivery-plan.mjs +7 -3
- package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +24 -2
- package/templates/starter/scripts/toolcraft-product-boundary.mjs +12 -0
- package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +46 -0
- package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +85 -20
- package/templates/starter/scripts/toolcraft-product-evidence-import-policy.mjs +43 -0
- package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +168 -0
- package/templates/starter/scripts/toolcraft-typescript-analysis.d.mts +73 -0
- package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade-test-helpers.mjs +318 -0
- package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +155 -206
- package/templates/starter/scripts/toolcraft-verification-inventory.mjs +1 -0
- package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +51 -0
- package/templates/starter/scripts/toolcraft-vite-env-activation.test.mjs +128 -59
- package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +113 -0
- package/templates/starter/src/app/acceptance/output-background-rules.ts +1 -1
- package/templates/starter/src/app/acceptance/output-export.ts +13 -2
- package/templates/starter/src/app/acceptance/types.ts +7 -0
- package/templates/starter/src/app/acceptance/validate-coverage.ts +2 -1
- package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +131 -0
- package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +14 -4
- package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +42 -1
- package/templates/starter/src/app/starter-acceptance.ts +2 -0
- package/templates/starter/src/app/starter-performance-test-utils.ts +103 -0
- package/templates/starter/src/app/starter-performance.gates.test.ts +231 -278
- package/templates/starter/src/app/starter-performance.lifecycle.test.ts +261 -0
- package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +2 -0
- package/templates/starter/src/routes/index.tsx +1 -0
- package/templates/runtime/export/export.ts +0 -166
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { defineToolcraft } from "../schema/define-toolcraft";
|
|
4
|
+
import { createToolcraftState } from "../state/create-template-state";
|
|
5
|
+
import { createToolcraftRuntimeSceneVisibility } from "../scene";
|
|
6
|
+
import { ToolcraftSceneExportError } from "./export-frame";
|
|
7
|
+
import {
|
|
8
|
+
resolveToolcraftImageArtifactFrame,
|
|
9
|
+
resolveToolcraftVideoArtifactFrame,
|
|
10
|
+
} from "./artifact-scene-frame";
|
|
11
|
+
import { createToolcraftVideoArtifactFramePlan } from "./artifact-frame-state";
|
|
12
|
+
import { createToolcraftVideoFrameSchedule } from "./video-frame-schedule";
|
|
13
|
+
|
|
14
|
+
const visibility = createToolcraftRuntimeSceneVisibility({
|
|
15
|
+
renderDefaultImages: true,
|
|
16
|
+
suppressedModelTargets: [],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function createState(mode: "finite" | "infinite") {
|
|
20
|
+
return createToolcraftState(
|
|
21
|
+
defineToolcraft({ canvas: { enabled: true }, panels: {} }),
|
|
22
|
+
{ canvas: { mode } },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("artifact scene frame", () => {
|
|
27
|
+
it("returns a finite frame without invoking product bounds", () => {
|
|
28
|
+
const state = createState("finite");
|
|
29
|
+
const boundsProvider = vi.fn(() => []);
|
|
30
|
+
|
|
31
|
+
expect(
|
|
32
|
+
resolveToolcraftImageArtifactFrame({
|
|
33
|
+
boundsProvider,
|
|
34
|
+
productSceneRequired: true,
|
|
35
|
+
state,
|
|
36
|
+
visibility,
|
|
37
|
+
}),
|
|
38
|
+
).toEqual({
|
|
39
|
+
height: state.canvas.size.height,
|
|
40
|
+
width: state.canvas.size.width,
|
|
41
|
+
x: 0,
|
|
42
|
+
y: 0,
|
|
43
|
+
});
|
|
44
|
+
expect(boundsProvider).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("unions moving infinite-scene bounds across the exact frame plan", () => {
|
|
48
|
+
const state = createState("infinite");
|
|
49
|
+
const plan = createToolcraftVideoArtifactFramePlan(
|
|
50
|
+
state,
|
|
51
|
+
createToolcraftVideoFrameSchedule(0.1),
|
|
52
|
+
);
|
|
53
|
+
const boundsProvider = vi.fn(({ state: frameState }) => [
|
|
54
|
+
{
|
|
55
|
+
height: 10,
|
|
56
|
+
width: 20,
|
|
57
|
+
x: frameState.timeline.currentTimeSeconds * 300,
|
|
58
|
+
y: -5,
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
expect(
|
|
63
|
+
resolveToolcraftVideoArtifactFrame({
|
|
64
|
+
boundsProvider,
|
|
65
|
+
framePlan: plan,
|
|
66
|
+
productSceneRequired: true,
|
|
67
|
+
visibility,
|
|
68
|
+
}),
|
|
69
|
+
).toEqual({ height: 10, width: 40, x: 0, y: -5 });
|
|
70
|
+
expect(boundsProvider).toHaveBeenCalledTimes(3);
|
|
71
|
+
expect(boundsProvider.mock.calls[1]?.[0].state).toBe(plan[1]?.state);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("fails visibly when an infinite product scene has no bounds provider", () => {
|
|
75
|
+
expect(() =>
|
|
76
|
+
resolveToolcraftImageArtifactFrame({
|
|
77
|
+
boundsProvider: undefined,
|
|
78
|
+
productSceneRequired: true,
|
|
79
|
+
state: createState("infinite"),
|
|
80
|
+
visibility,
|
|
81
|
+
}),
|
|
82
|
+
).toThrow(ToolcraftSceneExportError);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveToolcraftSceneBounds,
|
|
3
|
+
unionToolcraftSceneRects,
|
|
4
|
+
type ToolcraftProductSceneBoundsProvider,
|
|
5
|
+
type ToolcraftRuntimeSceneVisibility,
|
|
6
|
+
type ToolcraftSceneRect,
|
|
7
|
+
} from "../scene";
|
|
8
|
+
import { getToolcraftCanvasFrame } from "../state/canvas-frame";
|
|
9
|
+
import type { ToolcraftState } from "../state/types";
|
|
10
|
+
import type { ToolcraftVideoArtifactFramePlanEntry } from "./artifact-frame-state";
|
|
11
|
+
import {
|
|
12
|
+
resolveToolcraftExportFrame,
|
|
13
|
+
ToolcraftSceneExportError,
|
|
14
|
+
type ToolcraftExportFrame,
|
|
15
|
+
type ToolcraftExportFrameResult,
|
|
16
|
+
} from "./export-frame";
|
|
17
|
+
|
|
18
|
+
type ToolcraftArtifactSceneFrameBaseRequest = Readonly<{
|
|
19
|
+
boundsProvider: ToolcraftProductSceneBoundsProvider | undefined;
|
|
20
|
+
productSceneRequired: boolean;
|
|
21
|
+
visibility: ToolcraftRuntimeSceneVisibility;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export type ToolcraftImageArtifactSceneFrameRequest =
|
|
25
|
+
ToolcraftArtifactSceneFrameBaseRequest &
|
|
26
|
+
Readonly<{ state: ToolcraftState }>;
|
|
27
|
+
|
|
28
|
+
export type ToolcraftVideoArtifactSceneFrameRequest =
|
|
29
|
+
ToolcraftArtifactSceneFrameBaseRequest &
|
|
30
|
+
Readonly<{ framePlan: readonly ToolcraftVideoArtifactFramePlanEntry[] }>;
|
|
31
|
+
|
|
32
|
+
function requireFrame(result: ToolcraftExportFrameResult): ToolcraftExportFrame {
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
throw new ToolcraftSceneExportError(result);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return result.frame;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function requireProductRects(
|
|
41
|
+
state: ToolcraftState,
|
|
42
|
+
request: ToolcraftArtifactSceneFrameBaseRequest,
|
|
43
|
+
): readonly ToolcraftSceneRect[] {
|
|
44
|
+
if (!request.boundsProvider) {
|
|
45
|
+
if (request.productSceneRequired) {
|
|
46
|
+
throw new ToolcraftSceneExportError({
|
|
47
|
+
code: "scene-bounds-unavailable",
|
|
48
|
+
message: "This infinite product scene does not provide export bounds.",
|
|
49
|
+
ok: false,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
return request.boundsProvider({ state });
|
|
58
|
+
} catch {
|
|
59
|
+
throw new ToolcraftSceneExportError({
|
|
60
|
+
code: "scene-bounds-unavailable",
|
|
61
|
+
message: "Product scene bounds could not be resolved.",
|
|
62
|
+
ok: false,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isFiniteCanvas(state: ToolcraftState): boolean {
|
|
68
|
+
return getToolcraftCanvasFrame(state.canvas).kind === "finite";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolveToolcraftImageArtifactFrame(
|
|
72
|
+
request: ToolcraftImageArtifactSceneFrameRequest,
|
|
73
|
+
): ToolcraftExportFrame {
|
|
74
|
+
if (isFiniteCanvas(request.state)) {
|
|
75
|
+
return requireFrame(resolveToolcraftExportFrame(request.state, null));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const bounds = resolveToolcraftSceneBounds(
|
|
79
|
+
request.state,
|
|
80
|
+
requireProductRects(request.state, request),
|
|
81
|
+
request.visibility,
|
|
82
|
+
);
|
|
83
|
+
return requireFrame(resolveToolcraftExportFrame(request.state, bounds));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveToolcraftVideoArtifactFrame(
|
|
87
|
+
request: ToolcraftVideoArtifactSceneFrameRequest,
|
|
88
|
+
): ToolcraftExportFrame {
|
|
89
|
+
const firstState = request.framePlan[0]?.state;
|
|
90
|
+
if (!firstState) {
|
|
91
|
+
throw new ToolcraftSceneExportError({
|
|
92
|
+
code: "empty-scene",
|
|
93
|
+
message: "Video export has no scheduled frames.",
|
|
94
|
+
ok: false,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (isFiniteCanvas(firstState)) {
|
|
98
|
+
return requireFrame(resolveToolcraftExportFrame(firstState, null));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const frameBounds: ToolcraftSceneRect[] = [];
|
|
102
|
+
for (const entry of request.framePlan) {
|
|
103
|
+
const result = resolveToolcraftSceneBounds(
|
|
104
|
+
entry.state,
|
|
105
|
+
requireProductRects(entry.state, request),
|
|
106
|
+
request.visibility,
|
|
107
|
+
);
|
|
108
|
+
if (result.ok) {
|
|
109
|
+
frameBounds.push(result.bounds);
|
|
110
|
+
} else if (result.code !== "empty-scene") {
|
|
111
|
+
throw new ToolcraftSceneExportError(result);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return requireFrame(
|
|
116
|
+
resolveToolcraftExportFrame(firstState, unionToolcraftSceneRects(frameBounds)),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ResolvedToolcraftAppSchema } from "../schema/types";
|
|
2
|
+
import type { ToolcraftState } from "../state/types";
|
|
3
|
+
|
|
4
|
+
export type ToolcraftExportFormat = "png" | "video";
|
|
5
|
+
|
|
6
|
+
export type ToolcraftExportBackgroundOptions = {
|
|
7
|
+
format: ToolcraftExportFormat;
|
|
8
|
+
schema: ResolvedToolcraftAppSchema;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ToolcraftPreviewBackgroundOptions = {
|
|
12
|
+
includeBackgroundTarget?: string;
|
|
13
|
+
state: ToolcraftState;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function shouldIncludeToolcraftExportBackground({
|
|
17
|
+
format,
|
|
18
|
+
schema,
|
|
19
|
+
}: ToolcraftExportBackgroundOptions): boolean {
|
|
20
|
+
if (format === "video") {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return schema.export.png.background !== "transparent";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function shouldIncludeToolcraftPreviewBackground({
|
|
28
|
+
includeBackgroundTarget = "export.includeBackground",
|
|
29
|
+
state,
|
|
30
|
+
}: ToolcraftPreviewBackgroundOptions): boolean {
|
|
31
|
+
if (state.canvas.mode === "infinite") {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const includeBackgroundValue = state.values[includeBackgroundTarget];
|
|
36
|
+
|
|
37
|
+
if (typeof includeBackgroundValue === "boolean") {
|
|
38
|
+
return includeBackgroundValue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof includeBackgroundValue === "string") {
|
|
42
|
+
const normalizedValue = includeBackgroundValue.trim().toLowerCase();
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
normalizedValue === "false" ||
|
|
46
|
+
normalizedValue === "off" ||
|
|
47
|
+
normalizedValue === "no" ||
|
|
48
|
+
normalizedValue === "transparent" ||
|
|
49
|
+
normalizedValue === "exclude"
|
|
50
|
+
) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
normalizedValue === "true" ||
|
|
56
|
+
normalizedValue === "on" ||
|
|
57
|
+
normalizedValue === "yes" ||
|
|
58
|
+
normalizedValue === "include"
|
|
59
|
+
) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ToolcraftSceneExportError } from "./export-frame";
|
|
2
|
+
|
|
3
|
+
export type ToolcraftArtifactExportFailure = Readonly<{
|
|
4
|
+
code:
|
|
5
|
+
| "artifact-download-failed"
|
|
6
|
+
| "canvas-context-unavailable"
|
|
7
|
+
| "image-encode-failed"
|
|
8
|
+
| "invalid-export-setting"
|
|
9
|
+
| "product-frame-render-failed"
|
|
10
|
+
| "runtime-scene-render-failed"
|
|
11
|
+
| "video-artifact-too-large"
|
|
12
|
+
| "video-encode-failed"
|
|
13
|
+
| "video-encoder-unavailable";
|
|
14
|
+
message: string;
|
|
15
|
+
target?: string;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export class ToolcraftArtifactExportError extends Error {
|
|
19
|
+
readonly feedback: ToolcraftArtifactExportFailure;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
feedback: ToolcraftArtifactExportFailure,
|
|
23
|
+
options?: ErrorOptions,
|
|
24
|
+
) {
|
|
25
|
+
super(feedback.message, options);
|
|
26
|
+
this.name = "ToolcraftArtifactExportError";
|
|
27
|
+
this.feedback = feedback;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizeToolcraftExportError(
|
|
32
|
+
error: unknown,
|
|
33
|
+
feedback: ToolcraftArtifactExportFailure,
|
|
34
|
+
): ToolcraftArtifactExportError | ToolcraftSceneExportError {
|
|
35
|
+
if (
|
|
36
|
+
error instanceof ToolcraftArtifactExportError ||
|
|
37
|
+
error instanceof ToolcraftSceneExportError
|
|
38
|
+
) {
|
|
39
|
+
return error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new ToolcraftArtifactExportError(feedback, { cause: error });
|
|
43
|
+
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { defineToolcraft } from "../schema/define-toolcraft";
|
|
4
4
|
import type { ToolcraftState } from "../state/types";
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
shouldIncludeToolcraftExportBackground,
|
|
7
|
+
shouldIncludeToolcraftPreviewBackground,
|
|
8
|
+
} from "./export-background";
|
|
9
|
+
import {
|
|
7
10
|
getToolcraftImageExportSize,
|
|
8
11
|
getToolcraftRetinaExportPixelRatio,
|
|
9
12
|
getToolcraftRetinaExportSize,
|
|
10
13
|
getToolcraftVideoExportSize,
|
|
11
|
-
|
|
12
|
-
shouldIncludeToolcraftPreviewBackground,
|
|
13
|
-
} from "./export";
|
|
14
|
+
} from "./export-sizing";
|
|
14
15
|
|
|
15
16
|
function createState(schema = defineToolcraft({ canvas: { enabled: true }, panels: {} })): ToolcraftState {
|
|
16
17
|
return {
|
|
@@ -48,25 +49,6 @@ function createState(schema = defineToolcraft({ canvas: { enabled: true }, panel
|
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
function createMockCanvas() {
|
|
52
|
-
const context = {
|
|
53
|
-
clearRect: vi.fn(),
|
|
54
|
-
fillRect: vi.fn(),
|
|
55
|
-
fillStyle: "",
|
|
56
|
-
restore: vi.fn(),
|
|
57
|
-
save: vi.fn(),
|
|
58
|
-
scale: vi.fn(),
|
|
59
|
-
translate: vi.fn(),
|
|
60
|
-
};
|
|
61
|
-
const canvas = {
|
|
62
|
-
getContext: vi.fn(() => context),
|
|
63
|
-
height: 0,
|
|
64
|
-
width: 0,
|
|
65
|
-
} as unknown as HTMLCanvasElement;
|
|
66
|
-
|
|
67
|
-
return { canvas, context };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
52
|
describe("Toolcraft export helpers", () => {
|
|
71
53
|
it("normalizes png export background to include by default", () => {
|
|
72
54
|
const schema = defineToolcraft({
|
|
@@ -266,166 +248,4 @@ describe("Toolcraft export helpers", () => {
|
|
|
266
248
|
});
|
|
267
249
|
});
|
|
268
250
|
|
|
269
|
-
it("creates a transparent retina png canvas when png background is disabled", () => {
|
|
270
|
-
const schema = defineToolcraft({
|
|
271
|
-
canvas: { enabled: true },
|
|
272
|
-
export: {
|
|
273
|
-
png: {
|
|
274
|
-
background: "transparent",
|
|
275
|
-
},
|
|
276
|
-
},
|
|
277
|
-
panels: {},
|
|
278
|
-
});
|
|
279
|
-
const state = createState(schema);
|
|
280
|
-
const { canvas, context } = createMockCanvas();
|
|
281
|
-
const render = vi.fn();
|
|
282
|
-
|
|
283
|
-
const exportedCanvas = createToolcraftPngExportCanvas({
|
|
284
|
-
canvasFactory: () => canvas,
|
|
285
|
-
devicePixelRatio: 2,
|
|
286
|
-
render,
|
|
287
|
-
state,
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
expect(exportedCanvas.width).toBe(400);
|
|
291
|
-
expect(exportedCanvas.height).toBe(200);
|
|
292
|
-
expect(context.clearRect).toHaveBeenCalledWith(0, 0, 400, 200);
|
|
293
|
-
expect(context.fillRect).not.toHaveBeenCalled();
|
|
294
|
-
expect(context.scale).toHaveBeenCalledWith(2, 2);
|
|
295
|
-
expect(render).toHaveBeenCalledWith(
|
|
296
|
-
expect.objectContaining({
|
|
297
|
-
cssHeight: 100,
|
|
298
|
-
cssWidth: 200,
|
|
299
|
-
includeBackground: false,
|
|
300
|
-
pixelHeight: 200,
|
|
301
|
-
pixelRatio: 2,
|
|
302
|
-
pixelWidth: 400,
|
|
303
|
-
}),
|
|
304
|
-
);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it("fills the png background when png background is enabled", () => {
|
|
308
|
-
const state = createState();
|
|
309
|
-
const { canvas, context } = createMockCanvas();
|
|
310
|
-
|
|
311
|
-
createToolcraftPngExportCanvas({
|
|
312
|
-
background: "#112233",
|
|
313
|
-
canvasFactory: () => canvas,
|
|
314
|
-
devicePixelRatio: 2,
|
|
315
|
-
render: vi.fn(),
|
|
316
|
-
state,
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
expect(context.fillStyle).toBe("#112233");
|
|
320
|
-
expect(context.fillRect).toHaveBeenCalledWith(0, 0, 400, 200);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it("creates a png canvas at the selected image export resolution", () => {
|
|
324
|
-
const state = createState();
|
|
325
|
-
const { canvas, context } = createMockCanvas();
|
|
326
|
-
|
|
327
|
-
createToolcraftPngExportCanvas({
|
|
328
|
-
canvasFactory: () => canvas,
|
|
329
|
-
render: vi.fn(),
|
|
330
|
-
resolution: "4k",
|
|
331
|
-
state,
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
expect(canvas.width).toBe(4096);
|
|
335
|
-
expect(canvas.height).toBe(2048);
|
|
336
|
-
expect(context.scale).toHaveBeenCalledWith(20.48, 20.48);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it("allows runtime controls to disable the png background", () => {
|
|
340
|
-
const state = createState();
|
|
341
|
-
const { canvas, context } = createMockCanvas();
|
|
342
|
-
|
|
343
|
-
createToolcraftPngExportCanvas({
|
|
344
|
-
background: "#112233",
|
|
345
|
-
canvasFactory: () => canvas,
|
|
346
|
-
devicePixelRatio: 2,
|
|
347
|
-
includeBackground: false,
|
|
348
|
-
render: vi.fn(),
|
|
349
|
-
state,
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
expect(context.fillRect).not.toHaveBeenCalled();
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("allows runtime controls to include the png background for transparent schema defaults", () => {
|
|
356
|
-
const schema = defineToolcraft({
|
|
357
|
-
canvas: { enabled: true },
|
|
358
|
-
export: {
|
|
359
|
-
png: {
|
|
360
|
-
background: "transparent",
|
|
361
|
-
},
|
|
362
|
-
},
|
|
363
|
-
panels: {},
|
|
364
|
-
});
|
|
365
|
-
const state = createState(schema);
|
|
366
|
-
const { canvas, context } = createMockCanvas();
|
|
367
|
-
|
|
368
|
-
createToolcraftPngExportCanvas({
|
|
369
|
-
background: "#445566",
|
|
370
|
-
canvasFactory: () => canvas,
|
|
371
|
-
devicePixelRatio: 2,
|
|
372
|
-
includeBackground: true,
|
|
373
|
-
render: vi.fn(),
|
|
374
|
-
state,
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
expect(context.fillStyle).toBe("#445566");
|
|
378
|
-
expect(context.fillRect).toHaveBeenCalledWith(0, 0, 400, 200);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it("renders an infinite scene through its resolved world crop", () => {
|
|
382
|
-
const state = createState();
|
|
383
|
-
state.canvas.mode = "infinite";
|
|
384
|
-
const { canvas, context } = createMockCanvas();
|
|
385
|
-
const render = vi.fn();
|
|
386
|
-
|
|
387
|
-
createToolcraftPngExportCanvas({
|
|
388
|
-
canvasFactory: () => canvas,
|
|
389
|
-
devicePixelRatio: 2,
|
|
390
|
-
frame: { height: 40, width: 80, x: -30, y: 10 },
|
|
391
|
-
render,
|
|
392
|
-
state,
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
expect(canvas.width).toBe(160);
|
|
396
|
-
expect(canvas.height).toBe(80);
|
|
397
|
-
expect(context.scale).toHaveBeenCalledWith(2, 2);
|
|
398
|
-
expect(context.translate).toHaveBeenCalledWith(30, -10);
|
|
399
|
-
expect(render).toHaveBeenCalledWith(
|
|
400
|
-
expect.objectContaining({
|
|
401
|
-
cssHeight: 40,
|
|
402
|
-
cssWidth: 80,
|
|
403
|
-
frame: { height: 40, width: 80, x: -30, y: 10 },
|
|
404
|
-
}),
|
|
405
|
-
);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
it("rejects infinite and oversized exports before canvas allocation", () => {
|
|
409
|
-
const state = createState();
|
|
410
|
-
state.canvas.mode = "infinite";
|
|
411
|
-
const { canvas } = createMockCanvas();
|
|
412
|
-
|
|
413
|
-
expect(() =>
|
|
414
|
-
createToolcraftPngExportCanvas({
|
|
415
|
-
canvasFactory: () => canvas,
|
|
416
|
-
render: vi.fn(),
|
|
417
|
-
state,
|
|
418
|
-
}),
|
|
419
|
-
).toThrow("no visible exportable elements");
|
|
420
|
-
expect(() =>
|
|
421
|
-
createToolcraftPngExportCanvas({
|
|
422
|
-
canvasFactory: () => canvas,
|
|
423
|
-
frame: { height: 5000, width: 5000, x: 0, y: 0 },
|
|
424
|
-
render: vi.fn(),
|
|
425
|
-
state,
|
|
426
|
-
}),
|
|
427
|
-
).toThrow("8192 px per edge");
|
|
428
|
-
expect(canvas.width).toBe(0);
|
|
429
|
-
expect(canvas.height).toBe(0);
|
|
430
|
-
});
|
|
431
251
|
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { defineToolcraft } from "../schema/define-toolcraft";
|
|
4
|
+
import { createToolcraftState } from "../state/create-template-state";
|
|
5
|
+
import { createToolcraftRuntimeSceneVisibility } from "../scene";
|
|
6
|
+
import {
|
|
7
|
+
toolcraftImageExportFormatTarget,
|
|
8
|
+
toolcraftImageExportResolutionTarget,
|
|
9
|
+
} from "./artifact-export-settings";
|
|
10
|
+
import { exportToolcraftImageArtifact } from "./image-artifact-export";
|
|
11
|
+
|
|
12
|
+
function createState(format: "jpg" | "png" = "png") {
|
|
13
|
+
return createToolcraftState(
|
|
14
|
+
defineToolcraft({ canvas: { enabled: true }, panels: {} }),
|
|
15
|
+
{
|
|
16
|
+
values: {
|
|
17
|
+
[toolcraftImageExportFormatTarget]: format,
|
|
18
|
+
[toolcraftImageExportResolutionTarget]: "2k",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createCanvas(blob: Blob | null) {
|
|
25
|
+
const context = {
|
|
26
|
+
clearRect: vi.fn(),
|
|
27
|
+
fillRect: vi.fn(),
|
|
28
|
+
fillStyle: "",
|
|
29
|
+
restore: vi.fn(),
|
|
30
|
+
save: vi.fn(),
|
|
31
|
+
scale: vi.fn(),
|
|
32
|
+
setTransform: vi.fn(),
|
|
33
|
+
translate: vi.fn(),
|
|
34
|
+
};
|
|
35
|
+
const canvas = {
|
|
36
|
+
getContext: vi.fn(() => context),
|
|
37
|
+
height: 0,
|
|
38
|
+
toBlob: vi.fn((callback: BlobCallback) => callback(blob)),
|
|
39
|
+
width: 0,
|
|
40
|
+
} as unknown as HTMLCanvasElement;
|
|
41
|
+
return { canvas, context };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const visibility = createToolcraftRuntimeSceneVisibility({
|
|
45
|
+
renderDefaultImages: true,
|
|
46
|
+
suppressedModelTargets: [],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("exportToolcraftImageArtifact", () => {
|
|
50
|
+
it("renders and downloads the selected real image format and dimensions", async () => {
|
|
51
|
+
const { canvas } = createCanvas(
|
|
52
|
+
new Blob(["jpeg"], { type: "image/jpeg" }),
|
|
53
|
+
);
|
|
54
|
+
const downloadArtifact = vi.fn(() => ({ fileName: "scene.jpg" }));
|
|
55
|
+
|
|
56
|
+
const result = await exportToolcraftImageArtifact({
|
|
57
|
+
boundsProvider: undefined,
|
|
58
|
+
canvasFactory: () => canvas,
|
|
59
|
+
downloadArtifact,
|
|
60
|
+
exportRenderer: {
|
|
61
|
+
baseFileName: "scene",
|
|
62
|
+
renderFrame: vi.fn(),
|
|
63
|
+
},
|
|
64
|
+
productSceneRequired: true,
|
|
65
|
+
renderRuntimeScene: async () => undefined,
|
|
66
|
+
rendererPipeline: null,
|
|
67
|
+
reportProgress: vi.fn(),
|
|
68
|
+
state: createState("jpg"),
|
|
69
|
+
visibility,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result).toEqual({
|
|
73
|
+
byteLength: 4,
|
|
74
|
+
extension: ".jpg",
|
|
75
|
+
height: 1152,
|
|
76
|
+
mediaType: "image/jpeg",
|
|
77
|
+
width: 2048,
|
|
78
|
+
});
|
|
79
|
+
expect(canvas.width).toBe(1);
|
|
80
|
+
expect(canvas.height).toBe(1);
|
|
81
|
+
expect(canvas.toBlob).toHaveBeenCalledWith(
|
|
82
|
+
expect.any(Function),
|
|
83
|
+
"image/jpeg",
|
|
84
|
+
0.95,
|
|
85
|
+
);
|
|
86
|
+
expect(downloadArtifact).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({ extension: ".jpg", rawBaseFileName: "scene" }),
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("fails visibly when canvas encoding returns no blob", async () => {
|
|
92
|
+
const { canvas } = createCanvas(null);
|
|
93
|
+
|
|
94
|
+
await expect(
|
|
95
|
+
exportToolcraftImageArtifact({
|
|
96
|
+
boundsProvider: undefined,
|
|
97
|
+
canvasFactory: () => canvas,
|
|
98
|
+
downloadArtifact: vi.fn(),
|
|
99
|
+
exportRenderer: undefined,
|
|
100
|
+
productSceneRequired: false,
|
|
101
|
+
renderRuntimeScene: async () => undefined,
|
|
102
|
+
rendererPipeline: null,
|
|
103
|
+
reportProgress: vi.fn(),
|
|
104
|
+
state: createState(),
|
|
105
|
+
visibility,
|
|
106
|
+
}),
|
|
107
|
+
).rejects.toMatchObject({
|
|
108
|
+
feedback: { code: "image-encode-failed" },
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|