@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
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { expect, type Page } from "@playwright/test";
|
|
2
|
-
import
|
|
2
|
+
import { TOOLCRAFT_CANVAS_RENDER_SCALE } from "@repo/toolcraft-runtime";
|
|
3
3
|
|
|
4
|
+
import type { ToolcraftPerformancePipelinePhase } from "../src/app/test-evidence/browser-performance-contract";
|
|
4
5
|
import type { ToolcraftRenderScaleState } from "../src/app/starter-acceptance";
|
|
5
6
|
import { attachToolcraftBrowserRuntimeEvidence } from "./browser-runtime-evidence";
|
|
6
7
|
|
|
7
|
-
type
|
|
8
|
+
export type ToolcraftCanvasBackingMetrics = Readonly<{
|
|
8
9
|
backingHeight: number;
|
|
9
10
|
backingWidth: number;
|
|
10
11
|
cssHeight: number;
|
|
@@ -12,6 +13,8 @@ type CanvasMetrics = Readonly<{
|
|
|
12
13
|
devicePixelRatio: number;
|
|
13
14
|
}>;
|
|
14
15
|
|
|
16
|
+
export type ToolcraftCanvasVisibleSizePolicy = "fixed" | "viewport-driven";
|
|
17
|
+
|
|
15
18
|
export type ToolcraftRenderScaleStateTransition = Readonly<{
|
|
16
19
|
run: () => Promise<void>;
|
|
17
20
|
state: ToolcraftRenderScaleState;
|
|
@@ -19,20 +22,12 @@ export type ToolcraftRenderScaleStateTransition = Readonly<{
|
|
|
19
22
|
|
|
20
23
|
export type ToolcraftRenderScaleBackingOptions = Readonly<{
|
|
21
24
|
baselineCssSize?: Readonly<{ height: number; width: number }>;
|
|
22
|
-
state?:
|
|
25
|
+
state?:
|
|
26
|
+
| ToolcraftRenderScaleState
|
|
27
|
+
| "performance-baseline"
|
|
28
|
+
| `performance-${ToolcraftPerformancePipelinePhase}`;
|
|
23
29
|
}>;
|
|
24
30
|
|
|
25
|
-
export function isToolcraftRenderScalePerformancePath(
|
|
26
|
-
path: Pick<ToolcraftPerformancePath, "targets" | "workloadDimensions">,
|
|
27
|
-
): boolean {
|
|
28
|
-
return (
|
|
29
|
-
path.targets.includes("canvas.renderScale") ||
|
|
30
|
-
path.workloadDimensions.some((dimension) =>
|
|
31
|
-
/render[-_.]?scale|resolution[-_.]?scale/iu.test(dimension),
|
|
32
|
-
)
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
31
|
function assertPositiveFiniteScale(selectedScale: number): void {
|
|
37
32
|
if (!Number.isFinite(selectedScale) || selectedScale <= 0) {
|
|
38
33
|
throw new Error(
|
|
@@ -64,7 +59,7 @@ export function assertToolcraftRenderScaleBackingProofConfig(
|
|
|
64
59
|
async function readCanvasMetrics(
|
|
65
60
|
page: Page,
|
|
66
61
|
canvasSelector: string,
|
|
67
|
-
): Promise<
|
|
62
|
+
): Promise<ToolcraftCanvasBackingMetrics> {
|
|
68
63
|
if (!canvasSelector.trim()) {
|
|
69
64
|
throw new Error(
|
|
70
65
|
"Toolcraft canvas backing-pixel checks require a non-empty canvas selector.",
|
|
@@ -95,8 +90,8 @@ async function readCanvasMetrics(
|
|
|
95
90
|
});
|
|
96
91
|
}
|
|
97
92
|
|
|
98
|
-
function
|
|
99
|
-
metrics:
|
|
93
|
+
export function assertToolcraftCanvasBackingMetricsForRenderScale(
|
|
94
|
+
metrics: ToolcraftCanvasBackingMetrics,
|
|
100
95
|
selectedScale: number,
|
|
101
96
|
options: ToolcraftRenderScaleBackingOptions,
|
|
102
97
|
): void {
|
|
@@ -128,14 +123,16 @@ function assertBackingMetrics(
|
|
|
128
123
|
metrics.cssWidth * metrics.devicePixelRatio * selectedScale;
|
|
129
124
|
const expectedHeight =
|
|
130
125
|
metrics.cssHeight * metrics.devicePixelRatio * selectedScale;
|
|
126
|
+
const widthError = Math.abs(metrics.backingWidth - expectedWidth);
|
|
127
|
+
const heightError = Math.abs(metrics.backingHeight - expectedHeight);
|
|
131
128
|
expect(
|
|
132
|
-
|
|
133
|
-
`${stateLabel} expected canvas backing width
|
|
134
|
-
).
|
|
129
|
+
widthError,
|
|
130
|
+
`${stateLabel} expected exact canvas backing width within one physical pixel for Resolution scale ${selectedScale}.`,
|
|
131
|
+
).toBeLessThanOrEqual(1);
|
|
135
132
|
expect(
|
|
136
|
-
|
|
137
|
-
`${stateLabel} expected canvas backing height
|
|
138
|
-
).
|
|
133
|
+
heightError,
|
|
134
|
+
`${stateLabel} expected exact canvas backing height within one physical pixel for Resolution scale ${selectedScale}.`,
|
|
135
|
+
).toBeLessThanOrEqual(1);
|
|
139
136
|
}
|
|
140
137
|
|
|
141
138
|
export async function expectToolcraftCanvasBackingPixelsForRenderScale(
|
|
@@ -146,7 +143,11 @@ export async function expectToolcraftCanvasBackingPixelsForRenderScale(
|
|
|
146
143
|
): Promise<Readonly<{ height: number; width: number }>> {
|
|
147
144
|
assertPositiveFiniteScale(selectedScale);
|
|
148
145
|
const metrics = await readCanvasMetrics(page, canvasSelector);
|
|
149
|
-
|
|
146
|
+
assertToolcraftCanvasBackingMetricsForRenderScale(
|
|
147
|
+
metrics,
|
|
148
|
+
selectedScale,
|
|
149
|
+
options,
|
|
150
|
+
);
|
|
150
151
|
return { height: metrics.cssHeight, width: metrics.cssWidth };
|
|
151
152
|
}
|
|
152
153
|
|
|
@@ -216,13 +217,17 @@ export async function expectToolcraftPerformanceRenderScaleBackingEvidence(
|
|
|
216
217
|
page: Page,
|
|
217
218
|
{
|
|
218
219
|
canvasSelector,
|
|
220
|
+
baselineCssSize,
|
|
221
|
+
cssSizePolicy,
|
|
219
222
|
pathId,
|
|
220
|
-
|
|
223
|
+
phase,
|
|
221
224
|
target,
|
|
222
225
|
}: Readonly<{
|
|
226
|
+
baselineCssSize: Readonly<{ height: number; width: number }>;
|
|
223
227
|
canvasSelector: string;
|
|
228
|
+
cssSizePolicy: ToolcraftCanvasVisibleSizePolicy;
|
|
224
229
|
pathId: string;
|
|
225
|
-
|
|
230
|
+
phase: ToolcraftPerformancePipelinePhase;
|
|
226
231
|
target?: string;
|
|
227
232
|
}>,
|
|
228
233
|
): Promise<void> {
|
|
@@ -234,12 +239,15 @@ export async function expectToolcraftPerformanceRenderScaleBackingEvidence(
|
|
|
234
239
|
await expectToolcraftCanvasBackingPixelsForRenderScale(
|
|
235
240
|
page,
|
|
236
241
|
canvasSelector,
|
|
237
|
-
|
|
238
|
-
{
|
|
242
|
+
TOOLCRAFT_CANVAS_RENDER_SCALE.defaultValue,
|
|
243
|
+
{
|
|
244
|
+
...(cssSizePolicy === "fixed" ? { baselineCssSize } : {}),
|
|
245
|
+
state: `performance-${phase}`,
|
|
246
|
+
},
|
|
239
247
|
);
|
|
240
248
|
await attachToolcraftBrowserRuntimeEvidence({
|
|
241
249
|
evidenceType: "performance-render-scale",
|
|
242
|
-
requirementId: pathId
|
|
250
|
+
requirementId: `${pathId}#${phase}`,
|
|
243
251
|
target,
|
|
244
252
|
});
|
|
245
253
|
}
|
|
@@ -173,7 +173,10 @@ export default class ToolcraftBrowserRuntimeEvidenceReporter implements Reporter
|
|
|
173
173
|
);
|
|
174
174
|
this.performanceRequirements =
|
|
175
175
|
this.performanceRequirementsOverride ??
|
|
176
|
-
deriveToolcraftPerformancePathRuntimeRequirements(
|
|
176
|
+
deriveToolcraftPerformancePathRuntimeRequirements(
|
|
177
|
+
this.canonicalPaths,
|
|
178
|
+
this.performanceSchema,
|
|
179
|
+
);
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
onEnd(_result: FullResult): { status: "failed" } | undefined {
|
|
@@ -3,6 +3,9 @@ import type {
|
|
|
3
3
|
ToolcraftPerformancePath,
|
|
4
4
|
} from "@repo/toolcraft-runtime";
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
TOOLCRAFT_PERFORMANCE_PIPELINE_PHASES,
|
|
8
|
+
} from "../src/app/test-evidence/browser-performance-contract";
|
|
6
9
|
import type { ToolcraftBrowserRuntimeRequirement } from "../src/app/test-evidence/browser-runtime-contract";
|
|
7
10
|
import { schemaHasVideoExportPanelAction } from "../src/app/acceptance/output-export";
|
|
8
11
|
import {
|
|
@@ -16,7 +19,6 @@ import {
|
|
|
16
19
|
type ToolcraftTimelinePlaybackCoverage,
|
|
17
20
|
TOOLCRAFT_REQUIRED_MODEL_IMPORT_COVERAGE,
|
|
18
21
|
} from "../src/app/starter-acceptance";
|
|
19
|
-
import { isToolcraftRenderScalePerformancePath } from "./browser-render-scale-evidence";
|
|
20
22
|
import { getToolcraftPerformancePathTestName } from "./performance-path-helpers";
|
|
21
23
|
|
|
22
24
|
type BrowserAcceptanceRequirementSource = Pick<
|
|
@@ -27,6 +29,7 @@ type BrowserAcceptanceRequirementSource = Pick<
|
|
|
27
29
|
| "canvasHandle"
|
|
28
30
|
| "controlPartCoverage"
|
|
29
31
|
| "evidence"
|
|
32
|
+
| "exportArtifactCoverage"
|
|
30
33
|
| "id"
|
|
31
34
|
| "infinityCanvasCoverage"
|
|
32
35
|
| "layerCoverage"
|
|
@@ -188,7 +191,25 @@ export function deriveToolcraftBrowserRuntimeRequirements(
|
|
|
188
191
|
entry.orientationGizmoCoverage !== undefined;
|
|
189
192
|
const evidenceTypes: ToolcraftBrowserRuntimeRequirement["evidenceType"][] = [];
|
|
190
193
|
const baseEvidenceType = getAcceptanceEvidenceType(entry.evidence);
|
|
191
|
-
|
|
194
|
+
const exportArtifactCoverage = entry.exportArtifactCoverage
|
|
195
|
+
? typeof entry.exportArtifactCoverage === "string"
|
|
196
|
+
? [entry.exportArtifactCoverage]
|
|
197
|
+
: entry.exportArtifactCoverage
|
|
198
|
+
: [];
|
|
199
|
+
if (
|
|
200
|
+
baseEvidenceType &&
|
|
201
|
+
entry.renderScaleCoverage === undefined &&
|
|
202
|
+
exportArtifactCoverage.length === 0
|
|
203
|
+
) {
|
|
204
|
+
evidenceTypes.push(baseEvidenceType);
|
|
205
|
+
}
|
|
206
|
+
for (const coverage of exportArtifactCoverage) {
|
|
207
|
+
evidenceTypes.push(
|
|
208
|
+
coverage === "all-required-image-export-behavior"
|
|
209
|
+
? "image-export-artifact"
|
|
210
|
+
: "video-export-artifact",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
192
213
|
|
|
193
214
|
if (entry.canvasHandle && !hasOrientationCoverage) {
|
|
194
215
|
evidenceTypes.push("canvas-handle-interaction");
|
|
@@ -321,7 +342,9 @@ export function deriveToolcraftBrowserRuntimeRequirements(
|
|
|
321
342
|
});
|
|
322
343
|
}
|
|
323
344
|
|
|
324
|
-
const pathProductOutcomeInteractions = new Set<
|
|
345
|
+
const pathProductOutcomeInteractions = new Set<
|
|
346
|
+
ToolcraftPerformancePath["interaction"]
|
|
347
|
+
>([
|
|
325
348
|
"control-change",
|
|
326
349
|
"control-drag",
|
|
327
350
|
"mask-drag",
|
|
@@ -332,6 +355,7 @@ const pathProductOutcomeInteractions = new Set<ToolcraftPerformancePath["interac
|
|
|
332
355
|
|
|
333
356
|
export function deriveToolcraftPerformancePathRuntimeRequirements(
|
|
334
357
|
paths: readonly ToolcraftPerformancePath[],
|
|
358
|
+
schema: ResolvedToolcraftAppSchema,
|
|
335
359
|
): ToolcraftBrowserRuntimeRequirement[] {
|
|
336
360
|
return paths.flatMap((path) => {
|
|
337
361
|
const evidenceTypes: ToolcraftBrowserRuntimeRequirement["evidenceType"][] = [
|
|
@@ -353,19 +377,29 @@ export function deriveToolcraftPerformancePathRuntimeRequirements(
|
|
|
353
377
|
if (path.interaction === "animation-frame") {
|
|
354
378
|
evidenceTypes.push("performance-animation-frames");
|
|
355
379
|
}
|
|
356
|
-
if (
|
|
380
|
+
if (
|
|
381
|
+
path.interaction === "viewport-drag" ||
|
|
382
|
+
path.interaction === "viewport-zoom"
|
|
383
|
+
) {
|
|
357
384
|
evidenceTypes.push("performance-viewport");
|
|
358
385
|
}
|
|
359
|
-
if (isToolcraftRenderScalePerformancePath(path)) {
|
|
360
|
-
evidenceTypes.push("performance-render-scale");
|
|
361
|
-
}
|
|
362
|
-
|
|
363
386
|
const target = path.targets.length === 1 ? path.targets[0] : undefined;
|
|
364
|
-
|
|
387
|
+
const requirements = evidenceTypes.map((evidenceType) => ({
|
|
365
388
|
evidenceType,
|
|
366
389
|
requirementId: path.id,
|
|
367
390
|
target,
|
|
368
391
|
testName: getToolcraftPerformancePathTestName(path),
|
|
369
392
|
}));
|
|
393
|
+
if (schema.canvas.renderScale.enabled) {
|
|
394
|
+
for (const phase of TOOLCRAFT_PERFORMANCE_PIPELINE_PHASES) {
|
|
395
|
+
requirements.push({
|
|
396
|
+
evidenceType: "performance-render-scale",
|
|
397
|
+
requirementId: `${path.id}#${phase}`,
|
|
398
|
+
target,
|
|
399
|
+
testName: getToolcraftPerformancePathTestName(path),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return requirements;
|
|
370
404
|
});
|
|
371
405
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { expect } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
import { attachToolcraftBrowserRuntimeEvidence } from "./browser-runtime-evidence";
|
|
4
|
+
import {
|
|
5
|
+
getToolcraftBrowserProofPage,
|
|
6
|
+
type ToolcraftBrowserProofSession,
|
|
7
|
+
} from "./browser-proof-session";
|
|
8
|
+
|
|
9
|
+
async function readMarkerSignature(
|
|
10
|
+
page: Awaited<ReturnType<typeof getToolcraftBrowserProofPage>>,
|
|
11
|
+
markerSelector: string,
|
|
12
|
+
): Promise<string> {
|
|
13
|
+
return page.locator(markerSelector).evaluate((element) => {
|
|
14
|
+
const node = element as HTMLElement;
|
|
15
|
+
return `${node.dataset.timelineProgress ?? "missing"}:${node.style.transform || node.style.left}`;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function expectToolcraftStandardTimelinePlayback(
|
|
20
|
+
session: ToolcraftBrowserProofSession,
|
|
21
|
+
options: Readonly<{
|
|
22
|
+
markerSelector: string;
|
|
23
|
+
requirementId: string;
|
|
24
|
+
}>,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
const page = await getToolcraftBrowserProofPage(session);
|
|
27
|
+
const scrubber = page.getByRole("slider", { name: "Playback position" });
|
|
28
|
+
if (!(await scrubber.isVisible())) {
|
|
29
|
+
await page
|
|
30
|
+
.locator('[data-toolcraft-control-target="panels.timeline.extended"]')
|
|
31
|
+
.getByRole("switch")
|
|
32
|
+
.click();
|
|
33
|
+
await expect(scrubber).toBeVisible();
|
|
34
|
+
}
|
|
35
|
+
const pause = page.getByRole("button", { name: "Pause playback" });
|
|
36
|
+
if (await pause.isVisible()) await pause.click();
|
|
37
|
+
|
|
38
|
+
await scrubber.press("Home");
|
|
39
|
+
await expect(scrubber).toHaveAttribute("aria-valuenow", "0");
|
|
40
|
+
const initialStart = await readMarkerSignature(page, options.markerSelector);
|
|
41
|
+
await scrubber.press("End");
|
|
42
|
+
const initialEnd = await readMarkerSignature(page, options.markerSelector);
|
|
43
|
+
expect(initialEnd).toBe(initialStart);
|
|
44
|
+
await scrubber.press("ArrowLeft");
|
|
45
|
+
const scrubbed = await readMarkerSignature(page, options.markerSelector);
|
|
46
|
+
expect(scrubbed).not.toBe(initialStart);
|
|
47
|
+
await attachToolcraftBrowserRuntimeEvidence({
|
|
48
|
+
evidenceType: "timeline-scrub",
|
|
49
|
+
requirementId: options.requirementId,
|
|
50
|
+
target: "timeline.playback",
|
|
51
|
+
});
|
|
52
|
+
await attachToolcraftBrowserRuntimeEvidence({
|
|
53
|
+
evidenceType: "timeline-rendered-frame",
|
|
54
|
+
requirementId: options.requirementId,
|
|
55
|
+
target: "timeline.playback",
|
|
56
|
+
});
|
|
57
|
+
await attachToolcraftBrowserRuntimeEvidence({
|
|
58
|
+
evidenceType: "product-observable-change",
|
|
59
|
+
requirementId: options.requirementId,
|
|
60
|
+
target: "timeline.playback",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await page.getByRole("button", { name: "Edit timeline duration" }).click();
|
|
64
|
+
const durationEditor = page.getByRole("textbox", { name: "timeline duration" });
|
|
65
|
+
await durationEditor.fill("2s");
|
|
66
|
+
await durationEditor.press("Enter");
|
|
67
|
+
await expect(scrubber).toHaveAttribute("aria-valuemax", "2");
|
|
68
|
+
await scrubber.press("Home");
|
|
69
|
+
const resizedStart = await readMarkerSignature(page, options.markerSelector);
|
|
70
|
+
await scrubber.press("End");
|
|
71
|
+
const resizedEnd = await readMarkerSignature(page, options.markerSelector);
|
|
72
|
+
expect(resizedEnd).toBe(resizedStart);
|
|
73
|
+
await attachToolcraftBrowserRuntimeEvidence({
|
|
74
|
+
evidenceType: "timeline-duration",
|
|
75
|
+
requirementId: options.requirementId,
|
|
76
|
+
target: "timeline.playback",
|
|
77
|
+
});
|
|
78
|
+
await attachToolcraftBrowserRuntimeEvidence({
|
|
79
|
+
evidenceType: "timeline-loop",
|
|
80
|
+
requirementId: options.requirementId,
|
|
81
|
+
target: "timeline.playback",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await scrubber.press("Home");
|
|
85
|
+
await page.getByRole("button", { name: "Play playback" }).click();
|
|
86
|
+
await expect
|
|
87
|
+
.poll(() => scrubber.getAttribute("aria-valuenow"))
|
|
88
|
+
.not.toBe("0");
|
|
89
|
+
await page.getByRole("button", { name: "Pause playback" }).click();
|
|
90
|
+
const pausedTime = await scrubber.getAttribute("aria-valuenow");
|
|
91
|
+
const pausedSignature = await readMarkerSignature(page, options.markerSelector);
|
|
92
|
+
await page.waitForTimeout(100);
|
|
93
|
+
expect(await scrubber.getAttribute("aria-valuenow")).toBe(pausedTime);
|
|
94
|
+
expect(await readMarkerSignature(page, options.markerSelector)).toBe(
|
|
95
|
+
pausedSignature,
|
|
96
|
+
);
|
|
97
|
+
await page.getByRole("button", { name: "Play playback" }).click();
|
|
98
|
+
await expect
|
|
99
|
+
.poll(() => scrubber.getAttribute("aria-valuenow"))
|
|
100
|
+
.not.toBe(pausedTime);
|
|
101
|
+
await attachToolcraftBrowserRuntimeEvidence({
|
|
102
|
+
evidenceType: "timeline-pause-resume",
|
|
103
|
+
requirementId: options.requirementId,
|
|
104
|
+
target: "timeline.playback",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
assertToolcraftExpectedDecodedPixels,
|
|
5
|
+
observeToolcraftDecodedPixels,
|
|
6
|
+
validateToolcraftExpectedDecodedPixels,
|
|
7
|
+
} from "./decoded-pixel-observation";
|
|
8
|
+
|
|
9
|
+
function createCenteredFixture(): Uint8ClampedArray {
|
|
10
|
+
const pixels = new Uint8ClampedArray(64 * 64 * 4);
|
|
11
|
+
for (let y = 0; y < 64; y += 1) {
|
|
12
|
+
for (let x = 0; x < 64; x += 1) {
|
|
13
|
+
const offset = (y * 64 + x) * 4;
|
|
14
|
+
const product = x >= 16 && x < 48 && y >= 16 && y < 48;
|
|
15
|
+
pixels.set(product ? [255, 0, 0, 255] : [0, 0, 0, 255], offset);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return pixels;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("decoded pixels produce stable semantic bounds and hash", async () => {
|
|
22
|
+
const observation = await observeToolcraftDecodedPixels({
|
|
23
|
+
backgroundRgba: [0, 0, 0, 255],
|
|
24
|
+
pixels: createCenteredFixture(),
|
|
25
|
+
sourceHeight: 64,
|
|
26
|
+
sourceWidth: 64,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(observation.decodedPixelHash).toMatch(/^[a-f0-9]{64}$/u);
|
|
30
|
+
expect(observation.nonBackgroundBounds).toEqual({
|
|
31
|
+
height: 0.5,
|
|
32
|
+
width: 0.5,
|
|
33
|
+
x: 0.25,
|
|
34
|
+
y: 0.25,
|
|
35
|
+
});
|
|
36
|
+
expect(observation.centroid).toEqual({ x: 0.5, y: 0.5 });
|
|
37
|
+
expect(observation.occupiedAreaRatio).toBe(0.25);
|
|
38
|
+
expect(() =>
|
|
39
|
+
assertToolcraftExpectedDecodedPixels({
|
|
40
|
+
expectedPixels: [
|
|
41
|
+
{ rgba: [255, 0, 0, 255], xRatio: 0.5, yRatio: 0.5 },
|
|
42
|
+
],
|
|
43
|
+
mediaType: "image/png",
|
|
44
|
+
observation,
|
|
45
|
+
}),
|
|
46
|
+
).not.toThrow();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("decoded pixel expectations cannot provide empty, invalid, or loosened checks", async () => {
|
|
50
|
+
const observation = await observeToolcraftDecodedPixels({
|
|
51
|
+
backgroundRgba: [0, 0, 0, 255],
|
|
52
|
+
pixels: createCenteredFixture(),
|
|
53
|
+
sourceHeight: 64,
|
|
54
|
+
sourceWidth: 64,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(() => validateToolcraftExpectedDecodedPixels([])).toThrow(
|
|
58
|
+
"at least one exact pixel sample",
|
|
59
|
+
);
|
|
60
|
+
expect(() =>
|
|
61
|
+
validateToolcraftExpectedDecodedPixels([
|
|
62
|
+
{ rgba: [256, 0, 0, 255], xRatio: 2, yRatio: 0 } as never,
|
|
63
|
+
]),
|
|
64
|
+
).toThrow();
|
|
65
|
+
expect(() =>
|
|
66
|
+
assertToolcraftExpectedDecodedPixels({
|
|
67
|
+
expectedPixels: [
|
|
68
|
+
{ rgba: [0, 255, 0, 255], xRatio: 0.5, yRatio: 0.5 },
|
|
69
|
+
],
|
|
70
|
+
mediaType: "image/png",
|
|
71
|
+
observation,
|
|
72
|
+
}),
|
|
73
|
+
).toThrow("fixed tolerance is 0");
|
|
74
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { ToolcraftNormalizedPixelBounds } from "./export-artifact-helpers";
|
|
2
|
+
|
|
3
|
+
export const TOOLCRAFT_DECODED_OBSERVATION_SIZE = 64;
|
|
4
|
+
export const TOOLCRAFT_LOSSLESS_PIXEL_DISTANCE_TOLERANCE = 0;
|
|
5
|
+
export const TOOLCRAFT_LOSSY_PIXEL_DISTANCE_TOLERANCE = 48;
|
|
6
|
+
export const TOOLCRAFT_BACKGROUND_PIXEL_DISTANCE_THRESHOLD = 24;
|
|
7
|
+
|
|
8
|
+
export type ToolcraftExpectedDecodedPixel = Readonly<{
|
|
9
|
+
rgba: readonly [number, number, number, number];
|
|
10
|
+
xRatio: number;
|
|
11
|
+
yRatio: number;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
export type ToolcraftExpectedDecodedVideoSample = Readonly<{
|
|
15
|
+
pixels: readonly ToolcraftExpectedDecodedPixel[];
|
|
16
|
+
timeSeconds: number;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export type ToolcraftDecodedPixelObservation = Readonly<{
|
|
20
|
+
centroid: Readonly<{ x: number; y: number }> | null;
|
|
21
|
+
decodedPixelHash: string;
|
|
22
|
+
nonBackgroundBounds: ToolcraftNormalizedPixelBounds | null;
|
|
23
|
+
normalizedPixels: Uint8ClampedArray;
|
|
24
|
+
occupiedAreaRatio: number;
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
function assertRgba(
|
|
28
|
+
rgba: readonly number[],
|
|
29
|
+
label: string,
|
|
30
|
+
): asserts rgba is readonly [number, number, number, number] {
|
|
31
|
+
if (
|
|
32
|
+
rgba.length !== 4 ||
|
|
33
|
+
rgba.some(
|
|
34
|
+
(channel) =>
|
|
35
|
+
!Number.isSafeInteger(channel) || channel < 0 || channel > 255,
|
|
36
|
+
)
|
|
37
|
+
) {
|
|
38
|
+
throw new Error(`${label} must contain four integer RGBA channels from 0 to 255.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertRatio(value: number, label: string): void {
|
|
43
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
44
|
+
throw new Error(`${label} must be a finite normalized coordinate from 0 to 1.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validateToolcraftExpectedDecodedPixels(
|
|
49
|
+
pixels: readonly ToolcraftExpectedDecodedPixel[],
|
|
50
|
+
label = "Decoded pixel expectations",
|
|
51
|
+
): void {
|
|
52
|
+
if (!Array.isArray(pixels) || pixels.length === 0) {
|
|
53
|
+
throw new Error(`${label} must contain at least one exact pixel sample.`);
|
|
54
|
+
}
|
|
55
|
+
for (const [index, pixel] of pixels.entries()) {
|
|
56
|
+
assertRatio(pixel.xRatio, `${label}[${index}].xRatio`);
|
|
57
|
+
assertRatio(pixel.yRatio, `${label}[${index}].yRatio`);
|
|
58
|
+
assertRgba(pixel.rgba, `${label}[${index}].rgba`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getToolcraftRgbaDistance(
|
|
63
|
+
left: readonly number[],
|
|
64
|
+
right: readonly number[],
|
|
65
|
+
): number {
|
|
66
|
+
return Math.sqrt(
|
|
67
|
+
left.reduce((sum, channel, index) => {
|
|
68
|
+
const delta = channel - (right[index] ?? 0);
|
|
69
|
+
return sum + delta * delta;
|
|
70
|
+
}, 0),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizePixels({
|
|
75
|
+
pixels,
|
|
76
|
+
sourceHeight,
|
|
77
|
+
sourceWidth,
|
|
78
|
+
}: Readonly<{
|
|
79
|
+
pixels: Uint8ClampedArray;
|
|
80
|
+
sourceHeight: number;
|
|
81
|
+
sourceWidth: number;
|
|
82
|
+
}>): Uint8ClampedArray {
|
|
83
|
+
if (
|
|
84
|
+
!Number.isSafeInteger(sourceWidth) ||
|
|
85
|
+
sourceWidth <= 0 ||
|
|
86
|
+
!Number.isSafeInteger(sourceHeight) ||
|
|
87
|
+
sourceHeight <= 0 ||
|
|
88
|
+
pixels.length !== sourceWidth * sourceHeight * 4
|
|
89
|
+
) {
|
|
90
|
+
throw new Error("Decoded pixel input must match positive source dimensions.");
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
sourceWidth === TOOLCRAFT_DECODED_OBSERVATION_SIZE &&
|
|
94
|
+
sourceHeight === TOOLCRAFT_DECODED_OBSERVATION_SIZE
|
|
95
|
+
) {
|
|
96
|
+
return new Uint8ClampedArray(pixels);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const normalized = new Uint8ClampedArray(
|
|
100
|
+
TOOLCRAFT_DECODED_OBSERVATION_SIZE * TOOLCRAFT_DECODED_OBSERVATION_SIZE * 4,
|
|
101
|
+
);
|
|
102
|
+
for (let y = 0; y < TOOLCRAFT_DECODED_OBSERVATION_SIZE; y += 1) {
|
|
103
|
+
const sourceY = Math.min(
|
|
104
|
+
sourceHeight - 1,
|
|
105
|
+
Math.floor(((y + 0.5) / TOOLCRAFT_DECODED_OBSERVATION_SIZE) * sourceHeight),
|
|
106
|
+
);
|
|
107
|
+
for (let x = 0; x < TOOLCRAFT_DECODED_OBSERVATION_SIZE; x += 1) {
|
|
108
|
+
const sourceX = Math.min(
|
|
109
|
+
sourceWidth - 1,
|
|
110
|
+
Math.floor(((x + 0.5) / TOOLCRAFT_DECODED_OBSERVATION_SIZE) * sourceWidth),
|
|
111
|
+
);
|
|
112
|
+
const sourceOffset = (sourceY * sourceWidth + sourceX) * 4;
|
|
113
|
+
const outputOffset =
|
|
114
|
+
(y * TOOLCRAFT_DECODED_OBSERVATION_SIZE + x) * 4;
|
|
115
|
+
normalized.set(pixels.subarray(sourceOffset, sourceOffset + 4), outputOffset);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function hashPixels(pixels: Uint8ClampedArray): Promise<string> {
|
|
122
|
+
const source = new Uint8Array(
|
|
123
|
+
pixels.buffer,
|
|
124
|
+
pixels.byteOffset,
|
|
125
|
+
pixels.byteLength,
|
|
126
|
+
);
|
|
127
|
+
const hash = await crypto.subtle.digest("SHA-256", source);
|
|
128
|
+
return Array.from(new Uint8Array(hash), (byte) =>
|
|
129
|
+
byte.toString(16).padStart(2, "0"),
|
|
130
|
+
).join("");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function observeToolcraftDecodedPixels({
|
|
134
|
+
backgroundRgba,
|
|
135
|
+
pixels,
|
|
136
|
+
sourceHeight,
|
|
137
|
+
sourceWidth,
|
|
138
|
+
}: Readonly<{
|
|
139
|
+
backgroundRgba: readonly [number, number, number, number];
|
|
140
|
+
pixels: Uint8ClampedArray;
|
|
141
|
+
sourceHeight: number;
|
|
142
|
+
sourceWidth: number;
|
|
143
|
+
}>): Promise<ToolcraftDecodedPixelObservation> {
|
|
144
|
+
assertRgba(backgroundRgba, "backgroundRgba");
|
|
145
|
+
const normalizedPixels = normalizePixels({
|
|
146
|
+
pixels,
|
|
147
|
+
sourceHeight,
|
|
148
|
+
sourceWidth,
|
|
149
|
+
});
|
|
150
|
+
let count = 0;
|
|
151
|
+
let minX = TOOLCRAFT_DECODED_OBSERVATION_SIZE;
|
|
152
|
+
let minY = TOOLCRAFT_DECODED_OBSERVATION_SIZE;
|
|
153
|
+
let maxX = -1;
|
|
154
|
+
let maxY = -1;
|
|
155
|
+
let sumX = 0;
|
|
156
|
+
let sumY = 0;
|
|
157
|
+
|
|
158
|
+
for (let y = 0; y < TOOLCRAFT_DECODED_OBSERVATION_SIZE; y += 1) {
|
|
159
|
+
for (let x = 0; x < TOOLCRAFT_DECODED_OBSERVATION_SIZE; x += 1) {
|
|
160
|
+
const offset = (y * TOOLCRAFT_DECODED_OBSERVATION_SIZE + x) * 4;
|
|
161
|
+
const pixel = normalizedPixels.subarray(offset, offset + 4);
|
|
162
|
+
if (
|
|
163
|
+
getToolcraftRgbaDistance(pixel, backgroundRgba) <=
|
|
164
|
+
TOOLCRAFT_BACKGROUND_PIXEL_DISTANCE_THRESHOLD
|
|
165
|
+
) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
count += 1;
|
|
169
|
+
minX = Math.min(minX, x);
|
|
170
|
+
minY = Math.min(minY, y);
|
|
171
|
+
maxX = Math.max(maxX, x);
|
|
172
|
+
maxY = Math.max(maxY, y);
|
|
173
|
+
sumX += x + 0.5;
|
|
174
|
+
sumY += y + 0.5;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const size = TOOLCRAFT_DECODED_OBSERVATION_SIZE;
|
|
179
|
+
return Object.freeze({
|
|
180
|
+
centroid:
|
|
181
|
+
count === 0
|
|
182
|
+
? null
|
|
183
|
+
: Object.freeze({ x: sumX / count / size, y: sumY / count / size }),
|
|
184
|
+
decodedPixelHash: await hashPixels(normalizedPixels),
|
|
185
|
+
nonBackgroundBounds:
|
|
186
|
+
count === 0
|
|
187
|
+
? null
|
|
188
|
+
: Object.freeze({
|
|
189
|
+
height: (maxY - minY + 1) / size,
|
|
190
|
+
width: (maxX - minX + 1) / size,
|
|
191
|
+
x: minX / size,
|
|
192
|
+
y: minY / size,
|
|
193
|
+
}),
|
|
194
|
+
normalizedPixels,
|
|
195
|
+
occupiedAreaRatio: count / (size * size),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function assertToolcraftExpectedDecodedPixels({
|
|
200
|
+
expectedPixels,
|
|
201
|
+
mediaType,
|
|
202
|
+
observation,
|
|
203
|
+
}: Readonly<{
|
|
204
|
+
expectedPixels: readonly ToolcraftExpectedDecodedPixel[];
|
|
205
|
+
mediaType: "image/jpeg" | "image/png" | "video/mp4" | "video/webm";
|
|
206
|
+
observation: ToolcraftDecodedPixelObservation;
|
|
207
|
+
}>): void {
|
|
208
|
+
validateToolcraftExpectedDecodedPixels(expectedPixels);
|
|
209
|
+
const tolerance =
|
|
210
|
+
mediaType === "image/png"
|
|
211
|
+
? TOOLCRAFT_LOSSLESS_PIXEL_DISTANCE_TOLERANCE
|
|
212
|
+
: TOOLCRAFT_LOSSY_PIXEL_DISTANCE_TOLERANCE;
|
|
213
|
+
const size = TOOLCRAFT_DECODED_OBSERVATION_SIZE;
|
|
214
|
+
|
|
215
|
+
for (const expected of expectedPixels) {
|
|
216
|
+
const x = Math.min(size - 1, Math.floor(expected.xRatio * size));
|
|
217
|
+
const y = Math.min(size - 1, Math.floor(expected.yRatio * size));
|
|
218
|
+
const offset = (y * size + x) * 4;
|
|
219
|
+
const actual = observation.normalizedPixels.subarray(offset, offset + 4);
|
|
220
|
+
const distance = getToolcraftRgbaDistance(actual, expected.rgba);
|
|
221
|
+
if (distance > tolerance) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Decoded pixel at (${expected.xRatio}, ${expected.yRatio}) differs from expected RGBA by ${distance.toFixed(2)}; fixed tolerance is ${tolerance}.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|