@smallpen/local-package 0.1.0-alpha.11 → 0.1.0-alpha.12
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 +2 -2
- package/runtime/manifest.json +1 -1
- package/runtime/renderer.mjs +7376 -7416
- package/src/index.mjs +1 -0
- package/src/layout-inspection.mjs +124 -0
- package/src/render-shared.mjs +28 -5
- package/src/render.mjs +8 -19
package/src/index.mjs
CHANGED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { canonicalJSON, createSemanticTree } from "@smallpen/core";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { renderAppProjection } from "./render-shared.mjs";
|
|
4
|
+
|
|
5
|
+
const hash = (value) =>
|
|
6
|
+
createHash("sha256").update(canonicalJSON(value)).digest("hex");
|
|
7
|
+
const box = (value) =>
|
|
8
|
+
value &&
|
|
9
|
+
["x", "y", "width", "height"].every((key) => Number.isFinite(value[key]))
|
|
10
|
+
? Object.fromEntries(
|
|
11
|
+
["x", "y", "width", "height"].map((key) => [key, value[key]]),
|
|
12
|
+
)
|
|
13
|
+
: null;
|
|
14
|
+
|
|
15
|
+
// Specifications, diagnostics, wireframes and raster evidence consume this
|
|
16
|
+
// result. Authored constraints stay in projection; only native results are
|
|
17
|
+
// allowed to supply actual geometry. In particular, missing measurements
|
|
18
|
+
// must never silently fall back to the projection's seed rectangles.
|
|
19
|
+
export async function inspectProjection(product, projection, options = {}) {
|
|
20
|
+
const root = projection.nodes[projection.rootId];
|
|
21
|
+
let render, failure;
|
|
22
|
+
try {
|
|
23
|
+
render = await renderAppProjection(product, projection, {
|
|
24
|
+
...options,
|
|
25
|
+
cropNodeId: options.crop
|
|
26
|
+
? undefined
|
|
27
|
+
: (options.nodeId ?? projection.rootId),
|
|
28
|
+
measureOnly: !options.raster,
|
|
29
|
+
});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (
|
|
32
|
+
options.raster ||
|
|
33
|
+
![
|
|
34
|
+
"shared_renderer_unavailable",
|
|
35
|
+
"shared_render_failed",
|
|
36
|
+
"layout_not_settled",
|
|
37
|
+
"font_resource_unavailable",
|
|
38
|
+
"preview_fallback_not_renderable",
|
|
39
|
+
].includes(error.code)
|
|
40
|
+
)
|
|
41
|
+
throw error;
|
|
42
|
+
failure = { code: error.code, message: error.message };
|
|
43
|
+
}
|
|
44
|
+
const origin = {
|
|
45
|
+
x: (root?.x ?? 0) + (options.crop?.x ?? 0),
|
|
46
|
+
y: (root?.y ?? 0) + (options.crop?.y ?? 0),
|
|
47
|
+
};
|
|
48
|
+
const translate = (value) => {
|
|
49
|
+
const actual = box(value);
|
|
50
|
+
return actual
|
|
51
|
+
? { ...actual, x: actual.x + origin.x, y: actual.y + origin.y }
|
|
52
|
+
: null;
|
|
53
|
+
};
|
|
54
|
+
const tree = createSemanticTree(product, projection, {
|
|
55
|
+
selection: options.selection,
|
|
56
|
+
});
|
|
57
|
+
const ids = [];
|
|
58
|
+
const collect = (node) => {
|
|
59
|
+
ids.push(node.id);
|
|
60
|
+
node.children.forEach(collect);
|
|
61
|
+
};
|
|
62
|
+
collect(tree.root);
|
|
63
|
+
const nodes = Object.fromEntries(
|
|
64
|
+
ids.map((id) => {
|
|
65
|
+
const bounds = translate(render?.bounds[id]);
|
|
66
|
+
return [
|
|
67
|
+
id,
|
|
68
|
+
{
|
|
69
|
+
status: bounds ? "resolved" : "partial",
|
|
70
|
+
bounds,
|
|
71
|
+
layoutBox: translate(render?.layoutBoxes[id]),
|
|
72
|
+
...(bounds
|
|
73
|
+
? {}
|
|
74
|
+
: { reason: failure?.code ?? "native_bounds_unavailable" }),
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
const update = (node, parentVisible = true) => {
|
|
80
|
+
node.bounds = nodes[node.id]?.bounds ?? null;
|
|
81
|
+
node.visible = parentVisible && node.visible;
|
|
82
|
+
nodes[node.id].visible = node.visible;
|
|
83
|
+
for (const child of node.children) update(child, node.visible);
|
|
84
|
+
};
|
|
85
|
+
update(tree.root);
|
|
86
|
+
const diagnostics = failure
|
|
87
|
+
? [failure]
|
|
88
|
+
: (render?.diagnostics ?? []).filter(
|
|
89
|
+
(item) => item.code !== "layout_projection_partial",
|
|
90
|
+
);
|
|
91
|
+
const snapshot = {
|
|
92
|
+
status: Object.values(nodes).every((node) => node.status === "resolved")
|
|
93
|
+
? "resolved"
|
|
94
|
+
: "partial",
|
|
95
|
+
renderer: render?.renderer ?? "app-wasm",
|
|
96
|
+
kernelHash: render?.kernelHash ?? null,
|
|
97
|
+
runtimeHash: render?.runtimeHash ?? null,
|
|
98
|
+
resourcesHash: render?.resourcesHash ?? null,
|
|
99
|
+
revision: product.revision,
|
|
100
|
+
...(options.foundation
|
|
101
|
+
? { foundationRevision: options.foundation.revision }
|
|
102
|
+
: {}),
|
|
103
|
+
libraries: (options.libraries ?? []).map((library) => ({
|
|
104
|
+
packageId: library.manifest.packageId,
|
|
105
|
+
revision: library.revision,
|
|
106
|
+
})),
|
|
107
|
+
selection: options.selection ?? {},
|
|
108
|
+
coordinateSpace: "page",
|
|
109
|
+
viewport: options.crop
|
|
110
|
+
? { ...origin, width: options.crop.width, height: options.crop.height }
|
|
111
|
+
: (nodes[options.nodeId ?? projection.rootId]?.bounds ?? null),
|
|
112
|
+
nodes,
|
|
113
|
+
diagnostics,
|
|
114
|
+
};
|
|
115
|
+
snapshot.id = hash({
|
|
116
|
+
...snapshot,
|
|
117
|
+
// Two theme/viewport/context selections must not share an identity merely
|
|
118
|
+
// because they happen to paint the same pixels.
|
|
119
|
+
packageId: product.manifest.packageId,
|
|
120
|
+
projection,
|
|
121
|
+
});
|
|
122
|
+
snapshot.hash = snapshot.id;
|
|
123
|
+
return { snapshot, tree, render };
|
|
124
|
+
}
|
package/src/render-shared.mjs
CHANGED
|
@@ -15,7 +15,7 @@ async function loadRenderer() {
|
|
|
15
15
|
const manifest = JSON.parse(
|
|
16
16
|
await readFile(join(runtime, "manifest.json"), "utf8"),
|
|
17
17
|
);
|
|
18
|
-
const [renderer, factory, wasmBytes] = await Promise.all([
|
|
18
|
+
const [renderer, factory, wasmBytes, rendererBytes, wrapperBytes] = await Promise.all([
|
|
19
19
|
import(
|
|
20
20
|
pathToFileURL(join(runtime, "renderer.mjs")).href +
|
|
21
21
|
"?version=" +
|
|
@@ -27,10 +27,14 @@ async function loadRenderer() {
|
|
|
27
27
|
manifest.wrapperHash
|
|
28
28
|
),
|
|
29
29
|
readFile(join(runtime, "render-wasm.wasm")),
|
|
30
|
+
readFile(join(runtime, "renderer.mjs")),
|
|
31
|
+
readFile(join(runtime, "render-wasm.js")),
|
|
30
32
|
]);
|
|
31
33
|
if (
|
|
32
34
|
createHash("sha256").update(wasmBytes).digest("hex") !==
|
|
33
|
-
manifest.kernelHash
|
|
35
|
+
manifest.kernelHash ||
|
|
36
|
+
createHash("sha256").update(rendererBytes).digest("hex") !== manifest.rendererHash ||
|
|
37
|
+
createHash("sha256").update(wrapperBytes).digest("hex") !== manifest.wrapperHash
|
|
34
38
|
)
|
|
35
39
|
throw new Error(
|
|
36
40
|
"Rendering runtime changed during initialization; retry after the build finishes",
|
|
@@ -60,6 +64,7 @@ async function loadRenderer() {
|
|
|
60
64
|
fonts,
|
|
61
65
|
families,
|
|
62
66
|
kernelHash: createHash("sha256").update(wasmBytes).digest("hex"),
|
|
67
|
+
runtimeHash: createHash("sha256").update(rendererBytes).update(wrapperBytes).digest("hex"),
|
|
63
68
|
};
|
|
64
69
|
} catch (cause) {
|
|
65
70
|
throw new SmallPenError(
|
|
@@ -85,6 +90,7 @@ async function resources(engine, product, options) {
|
|
|
85
90
|
const fontId = `custom-${snapshot.runtime.fonts[family.id]}`;
|
|
86
91
|
fontFamilies.push({
|
|
87
92
|
fontId,
|
|
93
|
+
names: [family.family],
|
|
88
94
|
variants: family.variants.map((variant) => ({
|
|
89
95
|
id: `${variant.style}-${variant.weight}`,
|
|
90
96
|
weight: String(variant.weight),
|
|
@@ -151,9 +157,8 @@ export async function renderAppProjection(product, projection, options = {}) {
|
|
|
151
157
|
!Number.isFinite(height) ||
|
|
152
158
|
width < 1 ||
|
|
153
159
|
height < 1 ||
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
width * height > 32 * 1024 * 1024
|
|
160
|
+
(!options.measureOnly && !options.cropNodeId &&
|
|
161
|
+
(width > 8192 || height > 8192 || width * height > 32 * 1024 * 1024))
|
|
157
162
|
)
|
|
158
163
|
throw new SmallPenError(
|
|
159
164
|
"render_size_exceeded",
|
|
@@ -223,6 +228,7 @@ export async function renderAppProjection(product, projection, options = {}) {
|
|
|
223
228
|
nodes,
|
|
224
229
|
nodeIds,
|
|
225
230
|
rootId,
|
|
231
|
+
cropNodeId: options.cropNodeId ? nodeIds[options.cropNodeId] : undefined,
|
|
226
232
|
scale,
|
|
227
233
|
measureOnly: options.measureOnly === true,
|
|
228
234
|
forExport: options.forExport === true,
|
|
@@ -323,10 +329,27 @@ export async function renderAppProjection(product, projection, options = {}) {
|
|
|
323
329
|
],
|
|
324
330
|
renderer: "app-wasm",
|
|
325
331
|
kernelHash: engine.kernelHash,
|
|
332
|
+
runtimeHash: engine.runtimeHash,
|
|
333
|
+
resourcesHash: createHash("sha256").update(JSON.stringify({
|
|
334
|
+
fonts: fontResources.fonts.map(face => ({
|
|
335
|
+
id: face.fontId, weight: face.weight, style: face.style,
|
|
336
|
+
hash: createHash("sha256").update(face.bytes).digest("hex"),
|
|
337
|
+
})).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
|
|
338
|
+
images: request.images.map(image => ({
|
|
339
|
+
id: image.id,
|
|
340
|
+
hash: createHash("sha256").update(image.svg ?? image.bytes).digest("hex"),
|
|
341
|
+
})).sort((a, b) => a.id.localeCompare(b.id)),
|
|
342
|
+
})).digest("hex"),
|
|
326
343
|
renderHash: createHash("sha256").update(bytes).digest("hex"),
|
|
327
344
|
geometry: result.geometry,
|
|
345
|
+
layoutBoxes: Object.fromEntries(Object.entries(result.geometry).map(([uuid, value]) => [sourceIds[uuid], value])),
|
|
328
346
|
bounds,
|
|
329
347
|
textMetrics,
|
|
348
|
+
fontRequests: described.fonts.filter(face => sourceIds[face.nodeId]).map(face => {
|
|
349
|
+
const names = assets.fontFamilies.find(family => family.fontId === face.fontId)?.names;
|
|
350
|
+
return { nodeId: sourceIds[face.nodeId], family: names?.[1] ?? names?.[0] ?? face.fontId,
|
|
351
|
+
fontId: face.fontId, weight: face.weight, style: face.style };
|
|
352
|
+
}),
|
|
330
353
|
};
|
|
331
354
|
} catch (error) {
|
|
332
355
|
if (error instanceof SmallPenError) throw error;
|
package/src/render.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createSemanticTree,
|
|
3
2
|
projectDesignView,
|
|
4
3
|
resolveDesignView,
|
|
5
4
|
sha256Hex,
|
|
6
5
|
} from "@smallpen/core";
|
|
7
6
|
import { renderAppProjection } from "./render-shared.mjs";
|
|
7
|
+
import { inspectProjection } from "./layout-inspection.mjs";
|
|
8
8
|
|
|
9
9
|
// Rendering and measurement both use the App projector and Skia/WASM runtime.
|
|
10
10
|
export const renderProjection = renderAppProjection;
|
|
@@ -38,13 +38,6 @@ export async function measureProjectionText(product, projection, options = {}) {
|
|
|
38
38
|
return { items: result.textMetrics, diagnostics: result.diagnostics };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function updateBounds(node, bounds, origin) {
|
|
42
|
-
const actual = bounds[node.id];
|
|
43
|
-
if (actual)
|
|
44
|
-
node.bounds = { ...actual, x: actual.x + origin.x, y: actual.y + origin.y };
|
|
45
|
-
for (const child of node.children) updateBounds(child, bounds, origin);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
41
|
function flattenRegions(node, scale, width, height, origin, result = []) {
|
|
49
42
|
const left = Math.max(0, Math.min(width, (node.bounds.x - origin.x) * scale));
|
|
50
43
|
const top = Math.max(0, Math.min(height, (node.bounds.y - origin.y) * scale));
|
|
@@ -71,23 +64,19 @@ function flattenRegions(node, scale, width, height, origin, result = []) {
|
|
|
71
64
|
export async function createEvidence(product, options = {}) {
|
|
72
65
|
const resolved = resolveDesignView(product, options);
|
|
73
66
|
const projection = projectDesignView(product, resolved, options);
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
foundationRevision: options.foundation?.revision,
|
|
77
|
-
scenarioId: resolved.scenarioId,
|
|
78
|
-
selection: resolved.selection,
|
|
67
|
+
const inspection = await inspectProjection(product, projection, {
|
|
68
|
+
...options, raster: true, selection: resolved.selection,
|
|
79
69
|
});
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
y: root.y + (options.crop?.y ?? 0),
|
|
84
|
-
};
|
|
85
|
-
updateBounds(semanticTree.root, render.bounds, origin);
|
|
70
|
+
const render = { ...inspection.render, diagnostics: inspection.snapshot.diagnostics };
|
|
71
|
+
const semanticTree = inspection.tree;
|
|
72
|
+
const origin = inspection.snapshot.viewport;
|
|
86
73
|
return {
|
|
87
74
|
projection,
|
|
88
75
|
render,
|
|
89
76
|
evidence: {
|
|
90
77
|
diagnostics: render.diagnostics,
|
|
78
|
+
layout: inspection.snapshot,
|
|
79
|
+
snapshotId: inspection.snapshot.id,
|
|
91
80
|
nodeRegions: flattenRegions(
|
|
92
81
|
semanticTree.root,
|
|
93
82
|
options.scale ?? 1,
|