dsh-artcraft-plugin-gridcraft 0.2.9 → 0.2.10
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/kit/preview.js +76 -9
- package/src/kit/tools.js +26 -6
package/package.json
CHANGED
package/src/kit/preview.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* @module @dsh-artcraft/craft-kit/preview
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
14
15
|
import { homedir } from 'node:os'
|
|
15
16
|
import { basename, dirname, join } from 'node:path'
|
|
16
17
|
import { runCraft } from './exec.js'
|
|
@@ -32,19 +33,19 @@ export function previewRoot(configured ) {
|
|
|
32
33
|
* picture (SoundCraft has no page, and the MCP-only crafts need their server).
|
|
33
34
|
*/
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
/** Per-craft render recipes, verified against the 2026-11 CLI releases. */
|
|
40
41
|
const RECIPES = {
|
|
41
42
|
// `render FILE OUT [--page N] [--scale S]` draws a page as a PNG.
|
|
42
|
-
wordcraft: { argv: (file, out, scale) => ['render', file, out, '--page',
|
|
43
|
+
wordcraft: { argv: (file, out, scale, index) => ['render', file, out, '--page', String(index + 1), '--scale', String(scale)] },
|
|
43
44
|
// `convert` picks the format from the output extension.
|
|
44
45
|
cadcraft: { argv: (file, out) => ['convert', file, out] },
|
|
45
46
|
figcraft: { argv: (file, out) => ['convert', file, out] },
|
|
46
47
|
// `render FILE [--slide N] [--scale S] OUT` draws one slide as a PNG.
|
|
47
|
-
deckcraft: { argv: (file, out, scale) => ['render', file, '--slide',
|
|
48
|
+
deckcraft: { argv: (file, out, scale, index) => ['render', file, '--slide', String(index), '--scale', String(scale), out] },
|
|
48
49
|
// VectorCraft exports the active artboard through `run --export`.
|
|
49
50
|
vectorcraft: { argv: (file, out) => ['run', file, '--export', out] },
|
|
50
51
|
// TextureCraft renders the viewport (2D view; the 3D one needs a GPU).
|
|
@@ -73,6 +74,10 @@ export function canPreview(profile ) {
|
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
76
81
|
|
|
77
82
|
|
|
78
83
|
|
|
@@ -103,6 +108,35 @@ export function canPreview(profile ) {
|
|
|
103
108
|
|
|
104
109
|
|
|
105
110
|
|
|
111
|
+
/**
|
|
112
|
+
* How many pages or slides a document has, for the crafts that can report it.
|
|
113
|
+
* @param profile - the craft.
|
|
114
|
+
* @param binary - its binary.
|
|
115
|
+
* @param file - the document.
|
|
116
|
+
* @returns the count, or 1 when unknown (rendering the first page is always safe).
|
|
117
|
+
*/
|
|
118
|
+
export async function pageCount(profile , binary , file , timeoutMs = 30_000) {
|
|
119
|
+
if (profile.id !== 'wordcraft' && profile.id !== 'deckcraft') return 1
|
|
120
|
+
try {
|
|
121
|
+
const result = await runCraft({ profile, binary, argv: ['info', file], cwd: process.cwd(), timeoutMs })
|
|
122
|
+
if (!result.ok) return 1
|
|
123
|
+
const parsed = JSON.parse(result.stdout)
|
|
124
|
+
const count = profile.id === 'wordcraft' ? parsed.pages : parsed.slides
|
|
125
|
+
return typeof count === 'number' && count > 0 ? count : 1
|
|
126
|
+
} catch {
|
|
127
|
+
return 1
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Content hash of a rendered image, so an unchanged page is not a new step. */
|
|
132
|
+
export function imageHash(path ) {
|
|
133
|
+
try {
|
|
134
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
135
|
+
} catch {
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
106
140
|
/**
|
|
107
141
|
* Render one document to `<dir>/<craft>/step-NNN.png`.
|
|
108
142
|
*
|
|
@@ -116,16 +150,18 @@ export function canPreview(profile ) {
|
|
|
116
150
|
export async function renderPreview(
|
|
117
151
|
profile ,
|
|
118
152
|
binary ,
|
|
119
|
-
options
|
|
120
|
-
)
|
|
153
|
+
options ,
|
|
154
|
+
) {
|
|
121
155
|
const recipe = RECIPES[profile.id]
|
|
122
156
|
if (recipe === undefined) return undefined
|
|
123
157
|
if (!existsSync(options.file)) return undefined
|
|
124
158
|
try {
|
|
125
159
|
mkdirSync(options.dir, { recursive: true })
|
|
126
|
-
const argv = recipe.argv(options.file, options.out, options.scale ?? 1.5)
|
|
160
|
+
const argv = recipe.argv(options.file, options.out, options.scale ?? 1.5, options.page ?? 0)
|
|
127
161
|
const result = await runCraft({ profile, binary, argv, cwd: process.cwd(), timeoutMs: options.timeoutMs ?? 60_000 })
|
|
128
|
-
|
|
162
|
+
if (!result.ok || !existsSync(options.out)) return undefined
|
|
163
|
+
const hash = imageHash(options.out)
|
|
164
|
+
return hash === undefined ? undefined : { path: options.out, hash }
|
|
129
165
|
} catch {
|
|
130
166
|
return undefined
|
|
131
167
|
}
|
|
@@ -150,6 +186,37 @@ export function nextStepNumber(root , craft ) {
|
|
|
150
186
|
}
|
|
151
187
|
}
|
|
152
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Remove a render that matched the previous step's image.
|
|
191
|
+
* @param path - the rendered file.
|
|
192
|
+
* @returns nothing; a missing file is not an error.
|
|
193
|
+
*/
|
|
194
|
+
export function discardRender(path ) {
|
|
195
|
+
try {
|
|
196
|
+
unlinkSync(path)
|
|
197
|
+
} catch {
|
|
198
|
+
// Already gone.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The hash of the last rendered step, for skipping a render that repeats it.
|
|
204
|
+
* @param root - preview root.
|
|
205
|
+
* @param craft - craft id.
|
|
206
|
+
* @returns the hash, or `undefined` when there is no earlier render.
|
|
207
|
+
*/
|
|
208
|
+
export function lastStepHash(root , craft ) {
|
|
209
|
+
try {
|
|
210
|
+
const state = JSON.parse(readFileSync(join(root, craft, 'state.json'), 'utf8'))
|
|
211
|
+
const last = state.steps[state.steps.length - 1]
|
|
212
|
+
if (last === undefined) return undefined
|
|
213
|
+
if (last.hash !== undefined) return last.hash
|
|
214
|
+
return last.image === undefined ? undefined : imageHash(join(root, craft, last.image))
|
|
215
|
+
} catch {
|
|
216
|
+
return undefined
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
153
220
|
/** Read-modify-write one craft's state and refresh the index. */
|
|
154
221
|
export function recordPreview(
|
|
155
222
|
root ,
|
package/src/kit/tools.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { craftCallView, craftResultView } from './present.js'
|
|
12
|
-
import { canPreview, nextStepNumber, previewRoot, recordPreview, renderPreview, stepName } from './preview.js'
|
|
12
|
+
import { canPreview, discardRender, lastStepHash, nextStepNumber, pageCount, previewRoot, recordPreview, renderPreview, stepName } from './preview.js'
|
|
13
13
|
import { workflowsOf } from './suite.js'
|
|
14
14
|
import { appendJournal } from './journal.js'
|
|
15
15
|
import { CraftMcpClient, renderSchema } from './mcp.js'
|
|
@@ -179,18 +179,38 @@ export function createRuntime(profile , packageRoot , config
|
|
|
179
179
|
const dir = join(previewDir, profile.id)
|
|
180
180
|
const out = join(dir, stepName(n))
|
|
181
181
|
const scale = options.scale ?? config.previewScale ?? 1.5
|
|
182
|
-
const
|
|
183
|
-
|
|
182
|
+
const previous = lastStepHash(previewDir, profile.id)
|
|
183
|
+
|
|
184
|
+
// Render the first page, and when it did not change look at the last page:
|
|
185
|
+
// appending a slide or typing on page 2 leaves page 1 identical, and a wall
|
|
186
|
+
// of identical thumbnails is worse than no step at all.
|
|
187
|
+
let rendered = await renderPreview(profile, binary, { file: options.file, dir, out, scale, page: 0 })
|
|
188
|
+
let pages = 1
|
|
189
|
+
if (rendered !== undefined && previous !== undefined && rendered.hash === previous) {
|
|
190
|
+
pages = await pageCount(profile, binary, options.file)
|
|
191
|
+
rendered = pages > 1
|
|
192
|
+
? await renderPreview(profile, binary, { file: options.file, dir, out, scale, page: pages - 1 })
|
|
193
|
+
: undefined
|
|
194
|
+
if (rendered !== undefined && rendered.hash === previous) rendered = undefined
|
|
195
|
+
}
|
|
196
|
+
if (rendered === undefined) {
|
|
197
|
+
// Nothing visible changed; keep the step list meaningful by not adding one.
|
|
198
|
+
discardRender(out)
|
|
199
|
+
return undefined
|
|
200
|
+
}
|
|
184
201
|
recordPreview(previewDir, profile, {
|
|
185
202
|
n,
|
|
186
203
|
at: new Date().toISOString(),
|
|
187
204
|
tool: options.tool,
|
|
188
205
|
title: options.title,
|
|
189
206
|
summary: options.summary,
|
|
190
|
-
|
|
207
|
+
image: stepName(n),
|
|
208
|
+
thumb: stepName(n),
|
|
209
|
+
hash: rendered.hash,
|
|
210
|
+
page: pages,
|
|
191
211
|
...options.durationMs === undefined ? {} : { durationMs: options.durationMs },
|
|
192
|
-
}, options.file,
|
|
193
|
-
return rendered
|
|
212
|
+
}, options.file, pages)
|
|
213
|
+
return rendered.path
|
|
194
214
|
},
|
|
195
215
|
startJob(binary, argv) {
|
|
196
216
|
jobCounter += 1
|