@call-me-sensei/toonlab 0.4.19 → 0.4.21
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/NPM-LIBRARY.md +230 -0
- package/README.md +78 -36
- package/agents/PROMPTS.md +91 -0
- package/agents/README.md +9 -14
- package/agents/claude/CLAUDE.md +8 -0
- package/agents/codex/AGENTS.md +161 -0
- package/agents/cursor/toonlab.mdc +37 -0
- package/agents/references/mcp-asset-discovery.md +12 -9
- package/agents/references/runtime-entry-points.md +5 -4
- package/agents/skills/claude/asset-sourcing/SKILL.md +9 -6
- package/agents/skills/claude/rock-ground-shaders/SKILL.md +16 -0
- package/agents/skills/claude/vegetation-sky/SKILL.md +7 -4
- package/agents/skills/codex/asset-sourcing/SKILL.md +9 -6
- package/agents/skills/codex/rock-ground-shaders/SKILL.md +16 -0
- package/agents/skills/codex/vegetation-sky/SKILL.md +7 -4
- package/cli/toonlab.mjs +3 -2
- package/database/fal-patina.mjs +131 -0
- package/database/generation-service.mjs +108 -7
- package/database/migrations/0007_creation_revisions.sql +80 -0
- package/database/migrations/0008_creation_revision_hardening.sql +98 -0
- package/database/migrations/0009_creation_revision_delete_integrity.sql +11 -0
- package/database/migrations/0010_external_catalog_assets.sql +28 -0
- package/database/providers.mjs +195 -16
- package/database/repository.mjs +518 -70
- package/database/seeds/catalog/0003_2026-08-open-assets.sql +159086 -0
- package/database/seeds/catalog/0004_2026-08-c7-rocks.sql +39405 -0
- package/mcp/public-catalog.mjs +4 -52
- package/mcp/server.mjs +175 -12
- package/mcp/vite-plugin.mjs +78 -2
- package/package.json +32 -12
- package/scripts/generate-catalog-seed.mjs +34 -7
- package/src/asset-policy/catalogLicenses.js +64 -6
- package/src/assetlib/loadImported.js +15 -1
- package/src/catalog/officialCatalogAssetRuntime.js +13 -2
- package/src/catalog/officialCatalogLod.js +34 -1
- package/src/catalog/officialCatalogPlacement.js +29 -14
- package/src/character/animationRetarget.js +2 -0
- package/src/character/characterRig.js +6 -1
- package/src/character/characterRuntime.js +175 -7
- package/src/environment/environmentMaterialAdapter.js +30 -0
- package/src/environment/environmentPresets.js +90 -0
- package/src/environment/environmentSunShadowPass.js +42 -0
- package/src/environment/toonLabSurfaceLighting.js +108 -10
- package/src/environment/urbanPropMaterial.js +104 -2
- package/src/lighting/lightingSystem.js +65 -10
- package/src/renderer/index.js +7 -0
- package/src/renderer/styleComparison.js +985 -0
- package/src/rock-shader/index.js +5 -0
- package/src/rock-shader/rockGeometryDetail.js +701 -0
- package/src/rock-shader/rockMaterial.js +782 -39
- package/src/rock-shader/rockRegionRuntime.js +185 -0
- package/src/rock-shader/rockSemanticMaterialRuntime.js +337 -0
- package/src/rock-shader/rockShaderRuntime.js +354 -67
- package/src/rock-shader/rockShaderSettings.js +246 -16
- package/src/rock-shader/rockTangentIntegrity.js +136 -0
- package/src/rock-shader/rockTextureIntegrity.js +115 -0
- package/src/rockgen/index.js +1 -0
- package/src/rockgen/rockDocument.js +27 -3
- package/src/rockgen/surface/c7GeologySurface.js +372 -0
- package/src/shaders-tsl/water.js +8 -0
- package/src/sky/cloudShadow.js +25 -1
- package/src/sky/skySystem.js +9 -0
- package/src/styles/index.js +23 -0
- package/src/styles/neutralStylePresets.js +712 -0
- package/src/styles/sceneStyleRuntime.js +51 -4
- package/src/styles/styleAdapters.js +13 -2
- package/src/styles/styleBundle.js +1 -1
- package/src/texgen/evaluateTexture.js +30 -3
- package/src/texgen/textureGenerators.js +63 -31
- package/src/texgen/textureSettings.js +25 -8
- package/src/toon/toonSettings.js +96 -2
- package/src/vegetation/branchTree.js +327 -42
- package/src/vegetation/index.js +24 -2
- package/src/vegetation/scatter.js +372 -1
- package/src/vegetation/stylizedTree.js +312 -126
- package/src/vegetation/stylizedTreeFoliage.js +229 -7
- package/src/vegetation/treeSurfaceTextures.js +328 -0
- package/src/version.js +1 -1
- package/src/water/waterSettings.js +15 -7
- package/types/asset-policy/catalogLicenses.d.ts +2 -0
- package/types/asset-policy/index.d.ts +2 -0
- package/types/assetlib/loadImported.d.ts +3 -1
- package/types/catalog/officialCatalogLod.d.ts +63 -5
- package/types/character/characterRig.d.ts +59 -0
- package/types/character/characterRuntime.d.ts +1 -0
- package/types/environment/environmentMaterialAdapter.d.ts +3 -1
- package/types/environment/toonLabSurfaceLighting.d.ts +20 -3
- package/types/index.d.ts +89 -7
- package/types/lighting/lightingSystem.d.ts +7 -0
- package/types/react/index.d.ts +20 -0
- package/types/renderer/index.d.ts +1 -0
- package/types/renderer/styleComparison.d.ts +409 -0
- package/types/rock-shader/index.d.ts +5 -0
- package/types/rock-shader/rockGeometryDetail.d.ts +300 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockRegionRuntime.d.ts +86 -0
- package/types/rock-shader/rockSemanticMaterialRuntime.d.ts +29 -0
- package/types/rock-shader/rockShaderRuntime.d.ts +2 -0
- package/types/rock-shader/rockShaderSettings.d.ts +19 -5
- package/types/rock-shader/rockTangentIntegrity.d.ts +51 -0
- package/types/rock-shader/rockTextureIntegrity.d.ts +61 -0
- package/types/rockgen/index.d.ts +1 -0
- package/types/rockgen/rockDocument.d.ts +29 -1
- package/types/rockgen/surface/c7GeologySurface.d.ts +8 -0
- package/types/sky/cloudShadow.d.ts +1 -0
- package/types/styles/index.d.ts +1 -0
- package/types/styles/neutralStylePresets.d.ts +944 -0
- package/types/styles/sceneStyleRuntime.d.ts +20 -0
- package/types/vegetation/branchTree.d.ts +4 -0
- package/types/vegetation/index.d.ts +9 -7
- package/types/vegetation/scatter.d.ts +1 -0
- package/types/vegetation/stylizedTree.d.ts +1 -2
- package/types/vegetation/stylizedTreeFoliage.d.ts +1 -0
- package/types/version.d.ts +1 -1
- package/agents/references/geology-playbook.md +0 -118
- package/agents/skills/claude/karst-cliff-construction/SKILL.md +0 -258
- package/agents/skills/claude/outdoor-world/SKILL.md +0 -492
- package/agents/skills/claude/outdoor-world/references/coastal-landform-formation.md +0 -100
- package/agents/skills/codex/karst-cliff-construction/SKILL.md +0 -258
- package/agents/skills/codex/outdoor-world/SKILL.md +0 -492
- package/agents/skills/codex/outdoor-world/references/coastal-landform-formation.md +0 -100
- package/src/vegetation/legacyTreePresets.js +0 -120
- package/types/vegetation/legacyTreePresets.d.ts +0 -22
package/database/repository.mjs
CHANGED
|
@@ -27,25 +27,37 @@ function normalizeEntry(entry) {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
function revisionRow(row) {
|
|
31
|
+
if (!row) return null;
|
|
32
|
+
return {
|
|
33
|
+
id: row.id,
|
|
34
|
+
creationId: row.creation_id,
|
|
35
|
+
number: row.revision_number,
|
|
36
|
+
label: row.label,
|
|
37
|
+
description: row.description,
|
|
38
|
+
tags: row.tags ?? [],
|
|
39
|
+
document: row.document,
|
|
40
|
+
aiGenerated: row.ai_generated,
|
|
41
|
+
contentHash: row.content_hash,
|
|
42
|
+
saveSource: row.save_source,
|
|
43
|
+
dependencies: row.dependency_snapshot ?? {},
|
|
44
|
+
restoredFromRevisionId: row.restored_from_revision_id,
|
|
45
|
+
name: row.version_name,
|
|
46
|
+
versionTags: row.version_tags ?? [],
|
|
47
|
+
note: row.version_note,
|
|
48
|
+
pinned: row.pinned,
|
|
49
|
+
createdAt: row.created_at,
|
|
50
|
+
annotationUpdatedAt: row.annotation_updated_at,
|
|
51
|
+
};
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
);
|
|
48
|
-
return result.rows.map((row) => ({
|
|
54
|
+
function revisionSummaryRow(row) {
|
|
55
|
+
const { document: _document, dependencies: _dependencies, ...summary } = revisionRow(row);
|
|
56
|
+
return summary;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function creationRowToEntry(row) {
|
|
60
|
+
return {
|
|
49
61
|
...row.document,
|
|
50
62
|
id: row.doc_key,
|
|
51
63
|
type: row.type,
|
|
@@ -55,56 +67,392 @@ export async function listLibraryEntries() {
|
|
|
55
67
|
aiGenerated: row.ai_generated || row.document?.aiGenerated === true,
|
|
56
68
|
_local: {
|
|
57
69
|
creationId: row.id,
|
|
70
|
+
currentRevisionId: row.current_revision_id,
|
|
71
|
+
dependencySnapshot: row.current_dependency_snapshot ?? {},
|
|
58
72
|
source: row.source,
|
|
59
73
|
createdAt: row.created_at,
|
|
60
74
|
updatedAt: row.updated_at,
|
|
61
75
|
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function styleBundleDependencySnapshot(client, document) {
|
|
80
|
+
const slots = document?.slots;
|
|
81
|
+
if (!slots || typeof slots !== 'object' || Array.isArray(slots)) return {};
|
|
82
|
+
const references = Object.entries(slots).flatMap(([slotId, payload]) => {
|
|
83
|
+
const reference = payload && typeof payload === 'object' ? String(payload.creation ?? '').trim() : '';
|
|
84
|
+
return reference ? [{ reference, slotId }] : [];
|
|
85
|
+
});
|
|
86
|
+
if (!references.length) return {};
|
|
87
|
+
const values = [...new Set(references.map(({ reference }) => reference))];
|
|
88
|
+
const result = await client.query(
|
|
89
|
+
`select id, doc_key, current_revision_id
|
|
90
|
+
from creations
|
|
91
|
+
where id::text = any($1::text[]) or doc_key = any($1::text[])`,
|
|
92
|
+
[values],
|
|
93
|
+
);
|
|
94
|
+
const byReference = new Map();
|
|
95
|
+
for (const reference of values) {
|
|
96
|
+
const exactId = result.rows.find((row) => row.id === reference);
|
|
97
|
+
const matches = exactId ? [exactId] : result.rows.filter((row) => row.doc_key === reference);
|
|
98
|
+
if (matches.length > 1) {
|
|
99
|
+
throw Object.assign(
|
|
100
|
+
new Error(`Style bundle reference "${reference}" is ambiguous; use the creation UUID.`),
|
|
101
|
+
{ statusCode: 409 },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (matches[0]) byReference.set(reference, matches[0]);
|
|
105
|
+
}
|
|
106
|
+
const missing = references.filter(({ reference }) => !byReference.get(reference)?.current_revision_id);
|
|
107
|
+
if (missing.length) {
|
|
108
|
+
throw Object.assign(
|
|
109
|
+
new Error(`Style bundle references must resolve to saved creations with version history: ${[...new Set(missing.map(({ reference }) => reference))].join(', ')}`),
|
|
110
|
+
{ statusCode: 409 },
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return Object.fromEntries(references.map(({ reference, slotId }) => {
|
|
114
|
+
const row = byReference.get(reference);
|
|
115
|
+
return [slotId, { creationId: row.id, revisionId: row.current_revision_id }];
|
|
62
116
|
}));
|
|
63
117
|
}
|
|
64
118
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
119
|
+
async function commitCreationRevision(client, creation, {
|
|
120
|
+
dependencies,
|
|
121
|
+
force = false,
|
|
122
|
+
restoredFromRevisionId = null,
|
|
123
|
+
saveSource = 'manual',
|
|
124
|
+
} = {}) {
|
|
125
|
+
const dependencySnapshot = dependencies
|
|
126
|
+
?? (creation.type === 'style-bundle'
|
|
127
|
+
? await styleBundleDependencySnapshot(client, creation.document)
|
|
128
|
+
: {});
|
|
129
|
+
const locked = await client.query(
|
|
130
|
+
`select c.current_revision_id, r.content_hash
|
|
131
|
+
from creations c
|
|
132
|
+
left join creation_revisions r on r.id = c.current_revision_id
|
|
133
|
+
where c.id = $1
|
|
134
|
+
for update of c`,
|
|
135
|
+
[creation.id],
|
|
136
|
+
);
|
|
137
|
+
if (!locked.rows[0]) throw Object.assign(new Error('Creation not found.'), { statusCode: 404 });
|
|
138
|
+
const digest = await client.query(
|
|
139
|
+
`select encode(digest(convert_to(jsonb_build_object(
|
|
140
|
+
'label', $1::text,
|
|
141
|
+
'description', $2::text,
|
|
142
|
+
'tags', $3::text[],
|
|
143
|
+
'document', $4::jsonb,
|
|
144
|
+
'aiGenerated', $5::boolean,
|
|
145
|
+
'dependencies', $6::jsonb
|
|
146
|
+
)::text, 'UTF8'), 'sha256'), 'hex') as value`,
|
|
147
|
+
[
|
|
148
|
+
creation.label,
|
|
149
|
+
creation.description,
|
|
150
|
+
creation.tags,
|
|
151
|
+
JSON.stringify(creation.document),
|
|
152
|
+
creation.aiGenerated,
|
|
153
|
+
JSON.stringify(dependencySnapshot),
|
|
154
|
+
],
|
|
155
|
+
);
|
|
156
|
+
const contentHash = digest.rows[0].value;
|
|
157
|
+
if (!force && locked.rows[0].content_hash === contentHash) {
|
|
158
|
+
return { created: false, id: locked.rows[0].current_revision_id };
|
|
159
|
+
}
|
|
160
|
+
const inserted = await client.query(
|
|
161
|
+
`insert into creation_revisions (
|
|
162
|
+
creation_id, revision_number, label, description, tags, document,
|
|
163
|
+
ai_generated, content_hash, save_source, dependency_snapshot,
|
|
164
|
+
restored_from_revision_id
|
|
165
|
+
)
|
|
166
|
+
values (
|
|
167
|
+
$1,
|
|
168
|
+
coalesce((select max(revision_number) + 1 from creation_revisions where creation_id = $1), 1),
|
|
169
|
+
$2, $3, $4, $5::jsonb, $6, $7, $8, $9::jsonb, $10
|
|
170
|
+
)
|
|
171
|
+
returning id`,
|
|
172
|
+
[
|
|
173
|
+
creation.id,
|
|
174
|
+
creation.label,
|
|
175
|
+
creation.description,
|
|
176
|
+
creation.tags,
|
|
177
|
+
JSON.stringify(creation.document),
|
|
178
|
+
creation.aiGenerated,
|
|
179
|
+
contentHash,
|
|
180
|
+
saveSource,
|
|
181
|
+
JSON.stringify(dependencySnapshot),
|
|
182
|
+
restoredFromRevisionId,
|
|
183
|
+
],
|
|
184
|
+
);
|
|
185
|
+
await client.query(
|
|
186
|
+
`update creations set
|
|
187
|
+
label = $2,
|
|
188
|
+
description = $3,
|
|
189
|
+
tags = $4,
|
|
190
|
+
document = $5::jsonb,
|
|
191
|
+
ai_generated = $6,
|
|
192
|
+
current_revision_id = $7,
|
|
77
193
|
updated_at = now()
|
|
78
|
-
|
|
194
|
+
where id = $1`,
|
|
79
195
|
[
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
196
|
+
creation.id,
|
|
197
|
+
creation.label,
|
|
198
|
+
creation.description,
|
|
199
|
+
creation.tags,
|
|
200
|
+
JSON.stringify(creation.document),
|
|
201
|
+
creation.aiGenerated,
|
|
202
|
+
inserted.rows[0].id,
|
|
87
203
|
],
|
|
88
204
|
);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
205
|
+
return { created: true, id: inserted.rows[0].id };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function databaseInfo() {
|
|
209
|
+
const pool = await getPool();
|
|
210
|
+
const result = await pool.query(
|
|
211
|
+
`select
|
|
212
|
+
(select count(*)::int from creations) as creation_count,
|
|
213
|
+
(select count(*)::int from catalog_assets) as catalog_count`,
|
|
214
|
+
);
|
|
215
|
+
return result.rows[0];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function listLibraryEntries() {
|
|
219
|
+
const pool = await getPool();
|
|
220
|
+
const result = await pool.query(
|
|
221
|
+
`select c.id, c.doc_key, c.type, c.label, c.description, c.tags, c.document, c.source,
|
|
222
|
+
c.ai_generated, c.current_revision_id, c.created_at, c.updated_at,
|
|
223
|
+
r.dependency_snapshot as current_dependency_snapshot
|
|
224
|
+
from creations c
|
|
225
|
+
left join creation_revisions r on r.id = c.current_revision_id
|
|
226
|
+
order by c.updated_at desc, c.label`,
|
|
227
|
+
);
|
|
228
|
+
return result.rows.map(creationRowToEntry);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function saveLibraryEntry(entry) {
|
|
232
|
+
const value = normalizeEntry(entry);
|
|
233
|
+
return withTransaction(async (client) => {
|
|
234
|
+
const existing = await client.query(
|
|
235
|
+
`select * from creations where type = $1 and doc_key = $2 for update`,
|
|
236
|
+
[value.type, value.docKey],
|
|
95
237
|
);
|
|
96
|
-
|
|
238
|
+
let row = existing.rows[0];
|
|
239
|
+
if (!row) {
|
|
240
|
+
const inserted = await client.query(
|
|
241
|
+
`insert into creations (doc_key, type, label, description, tags, document, ai_generated)
|
|
242
|
+
values ($1, $2, $3, $4, $5, $6::jsonb, $7)
|
|
243
|
+
returning *`,
|
|
244
|
+
[
|
|
245
|
+
value.docKey,
|
|
246
|
+
value.type,
|
|
247
|
+
value.label,
|
|
248
|
+
value.description,
|
|
249
|
+
value.tags,
|
|
250
|
+
JSON.stringify(value.document),
|
|
251
|
+
value.aiGenerated,
|
|
252
|
+
],
|
|
253
|
+
);
|
|
254
|
+
row = inserted.rows[0];
|
|
255
|
+
}
|
|
256
|
+
const snapshot = {
|
|
257
|
+
...row,
|
|
258
|
+
label: value.label,
|
|
259
|
+
description: value.description,
|
|
260
|
+
tags: value.tags,
|
|
261
|
+
document: value.document,
|
|
262
|
+
aiGenerated: row.ai_generated || value.aiGenerated,
|
|
263
|
+
};
|
|
264
|
+
const revision = await commitCreationRevision(client, {
|
|
265
|
+
...snapshot,
|
|
266
|
+
aiGenerated: snapshot.aiGenerated,
|
|
267
|
+
});
|
|
268
|
+
const fileId = entry?.result?.file?.id ?? entry?.file?.id ?? null;
|
|
269
|
+
if (fileId) {
|
|
270
|
+
await client.query(
|
|
271
|
+
`update files set creation_id = $1
|
|
272
|
+
where id = $2 and creation_id is null`,
|
|
273
|
+
[row.id, fileId],
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const saved = await client.query(
|
|
277
|
+
`select c.*, r.dependency_snapshot as current_dependency_snapshot
|
|
278
|
+
from creations c
|
|
279
|
+
left join creation_revisions r on r.id = c.current_revision_id
|
|
280
|
+
where c.id = $1`,
|
|
281
|
+
[row.id],
|
|
282
|
+
);
|
|
283
|
+
return {
|
|
284
|
+
...creationRowToEntry(saved.rows[0]),
|
|
285
|
+
_local: {
|
|
286
|
+
...creationRowToEntry(saved.rows[0])._local,
|
|
287
|
+
revisionCreated: revision.created,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function listCreationRevisions(creationId, { limit = 25, offset = 0 } = {}) {
|
|
294
|
+
const boundedLimit = Math.max(1, Math.min(100, Math.floor(Number(limit) || 25)));
|
|
295
|
+
const boundedOffset = Math.max(0, Math.floor(Number(offset) || 0));
|
|
296
|
+
const pool = await getPool();
|
|
297
|
+
const [result, countResult] = await Promise.all([
|
|
298
|
+
pool.query(
|
|
299
|
+
`select
|
|
300
|
+
r.id, r.creation_id, r.revision_number, r.label, r.description, r.tags,
|
|
301
|
+
r.ai_generated, r.content_hash, r.save_source, r.restored_from_revision_id,
|
|
302
|
+
r.version_name, r.version_tags, r.version_note, r.pinned,
|
|
303
|
+
r.annotation_updated_at, r.created_at,
|
|
304
|
+
(r.id = c.current_revision_id) as is_current
|
|
305
|
+
from creation_revisions r
|
|
306
|
+
join creations c on c.id = r.creation_id
|
|
307
|
+
where r.creation_id = $1
|
|
308
|
+
order by r.pinned desc, r.revision_number desc
|
|
309
|
+
limit $2 offset $3`,
|
|
310
|
+
[creationId, boundedLimit, boundedOffset],
|
|
311
|
+
),
|
|
312
|
+
pool.query(
|
|
313
|
+
`select count(*)::int as total from creation_revisions where creation_id = $1`,
|
|
314
|
+
[creationId],
|
|
315
|
+
),
|
|
316
|
+
]);
|
|
97
317
|
return {
|
|
98
|
-
...
|
|
99
|
-
|
|
100
|
-
creationId: saved.rows[0].id,
|
|
101
|
-
source: saved.rows[0].source,
|
|
102
|
-
createdAt: saved.rows[0].created_at,
|
|
103
|
-
updatedAt: saved.rows[0].updated_at,
|
|
104
|
-
},
|
|
318
|
+
revisions: result.rows.map((row) => ({ ...revisionSummaryRow(row), isCurrent: row.is_current })),
|
|
319
|
+
total: countResult.rows[0]?.total ?? 0,
|
|
105
320
|
};
|
|
106
321
|
}
|
|
107
322
|
|
|
323
|
+
export async function getCreationRevision(creationId, revisionId) {
|
|
324
|
+
const pool = await getPool();
|
|
325
|
+
const result = await pool.query(
|
|
326
|
+
`select r.*, (r.id = c.current_revision_id) as is_current
|
|
327
|
+
from creation_revisions r
|
|
328
|
+
join creations c on c.id = r.creation_id
|
|
329
|
+
where r.creation_id = $1 and r.id = $2`,
|
|
330
|
+
[creationId, revisionId],
|
|
331
|
+
);
|
|
332
|
+
if (!result.rows[0]) throw Object.assign(new Error('Revision not found.'), { statusCode: 404 });
|
|
333
|
+
return { ...revisionRow(result.rows[0]), isCurrent: result.rows[0].is_current };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function resolveStyleBundleEntry(selector) {
|
|
337
|
+
const value = String(selector ?? '').trim();
|
|
338
|
+
const pool = await getPool();
|
|
339
|
+
const result = await pool.query(
|
|
340
|
+
`select c.*, r.dependency_snapshot
|
|
341
|
+
from creations c
|
|
342
|
+
join creation_revisions r on r.id = c.current_revision_id
|
|
343
|
+
where c.type = 'style-bundle' and (c.id::text = $1 or c.doc_key = $1)
|
|
344
|
+
order by (c.id::text = $1) desc
|
|
345
|
+
limit 1`,
|
|
346
|
+
[value],
|
|
347
|
+
);
|
|
348
|
+
const bundle = result.rows[0];
|
|
349
|
+
if (!bundle) throw Object.assign(new Error('Style bundle not found.'), { statusCode: 404 });
|
|
350
|
+
const slots = { ...(bundle.document?.slots ?? {}) };
|
|
351
|
+
const dependencies = bundle.dependency_snapshot ?? {};
|
|
352
|
+
for (const [slotId, payload] of Object.entries(slots)) {
|
|
353
|
+
if (!payload?.creation) continue;
|
|
354
|
+
const lock = dependencies[slotId];
|
|
355
|
+
if (!lock?.creationId || !lock?.revisionId) {
|
|
356
|
+
throw Object.assign(new Error(`Style bundle slot "${slotId}" has no locked dependency revision.`), { statusCode: 409 });
|
|
357
|
+
}
|
|
358
|
+
const locked = await pool.query(
|
|
359
|
+
`select r.document, c.doc_key
|
|
360
|
+
from creation_revisions r
|
|
361
|
+
join creations c on c.id = r.creation_id
|
|
362
|
+
where r.id = $1 and r.creation_id = $2`,
|
|
363
|
+
[lock.revisionId, lock.creationId],
|
|
364
|
+
);
|
|
365
|
+
if (
|
|
366
|
+
!locked.rows[0]
|
|
367
|
+
|| (payload.creation !== lock.creationId && payload.creation !== locked.rows[0].doc_key)
|
|
368
|
+
) {
|
|
369
|
+
throw Object.assign(new Error(`Locked dependency for style bundle slot "${slotId}" is unavailable.`), { statusCode: 409 });
|
|
370
|
+
}
|
|
371
|
+
slots[slotId] = { document: locked.rows[0].document };
|
|
372
|
+
}
|
|
373
|
+
return { ...bundle.document, slots };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export async function annotateCreationRevision(creationId, revisionId, patch) {
|
|
377
|
+
const assignments = [];
|
|
378
|
+
const values = [creationId, revisionId];
|
|
379
|
+
const assign = (column, value) => {
|
|
380
|
+
values.push(value);
|
|
381
|
+
assignments.push(`${column} = $${values.length}`);
|
|
382
|
+
};
|
|
383
|
+
if (Object.hasOwn(patch, 'name')) {
|
|
384
|
+
assign(
|
|
385
|
+
'version_name',
|
|
386
|
+
patch.name == null || !String(patch.name).trim()
|
|
387
|
+
? null
|
|
388
|
+
: String(patch.name).trim().slice(0, 120),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (Object.hasOwn(patch, 'tags')) assign('version_tags', normalizeCreationTags(patch.tags));
|
|
392
|
+
if (Object.hasOwn(patch, 'note')) {
|
|
393
|
+
assign(
|
|
394
|
+
'version_note',
|
|
395
|
+
patch.note == null || !String(patch.note).trim()
|
|
396
|
+
? null
|
|
397
|
+
: String(patch.note).trim().slice(0, 2000),
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
if (Object.hasOwn(patch, 'pinned')) assign('pinned', patch.pinned === true);
|
|
401
|
+
assignments.push('annotation_updated_at = now()');
|
|
402
|
+
const pool = await getPool();
|
|
403
|
+
try {
|
|
404
|
+
const result = await pool.query(
|
|
405
|
+
`update creation_revisions set
|
|
406
|
+
${assignments.join(', ')}
|
|
407
|
+
where creation_id = $1 and id = $2
|
|
408
|
+
returning *`,
|
|
409
|
+
values,
|
|
410
|
+
);
|
|
411
|
+
if (!result.rows[0]) throw Object.assign(new Error('Revision not found.'), { statusCode: 404 });
|
|
412
|
+
return revisionRow(result.rows[0]);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
if (error?.code === '23505') {
|
|
415
|
+
throw Object.assign(new Error('Version names must be unique within a creation.'), { statusCode: 409 });
|
|
416
|
+
}
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export async function restoreCreationRevision(creationId, revisionId) {
|
|
422
|
+
return withTransaction(async (client) => {
|
|
423
|
+
const target = await client.query(
|
|
424
|
+
`select r.*, c.doc_key, c.type, c.source, c.created_at
|
|
425
|
+
from creation_revisions r
|
|
426
|
+
join creations c on c.id = r.creation_id
|
|
427
|
+
where r.creation_id = $1 and r.id = $2`,
|
|
428
|
+
[creationId, revisionId],
|
|
429
|
+
);
|
|
430
|
+
const revision = target.rows[0];
|
|
431
|
+
if (!revision) throw Object.assign(new Error('Revision not found.'), { statusCode: 404 });
|
|
432
|
+
await commitCreationRevision(client, {
|
|
433
|
+
aiGenerated: revision.ai_generated,
|
|
434
|
+
description: revision.description,
|
|
435
|
+
document: revision.document,
|
|
436
|
+
id: creationId,
|
|
437
|
+
label: revision.label,
|
|
438
|
+
tags: revision.tags,
|
|
439
|
+
}, {
|
|
440
|
+
dependencies: revision.dependency_snapshot,
|
|
441
|
+
force: true,
|
|
442
|
+
restoredFromRevisionId: revisionId,
|
|
443
|
+
saveSource: 'restore',
|
|
444
|
+
});
|
|
445
|
+
const saved = await client.query(
|
|
446
|
+
`select c.*, r.dependency_snapshot as current_dependency_snapshot
|
|
447
|
+
from creations c
|
|
448
|
+
left join creation_revisions r on r.id = c.current_revision_id
|
|
449
|
+
where c.id = $1`,
|
|
450
|
+
[creationId],
|
|
451
|
+
);
|
|
452
|
+
return creationRowToEntry(saved.rows[0]);
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
108
456
|
export function resolveUnambiguousLibraryDeletion(matches, docKey) {
|
|
109
457
|
if (matches.length > 1) {
|
|
110
458
|
throw new Error(
|
|
@@ -200,13 +548,8 @@ async function setLabStateWithClient(client, key, value) {
|
|
|
200
548
|
);
|
|
201
549
|
}
|
|
202
550
|
if (!/\.(?:presets|projects)(?:\.|$)/.test(stateKey)) return;
|
|
203
|
-
await client.query(
|
|
204
|
-
`delete from creations
|
|
205
|
-
where source = 'lab-state'
|
|
206
|
-
and left(doc_key, char_length($1)) = $1`,
|
|
207
|
-
[`state:${stateKey}:`],
|
|
208
|
-
);
|
|
209
551
|
const storedEntries = extractNamedLabStateDocuments(parsed);
|
|
552
|
+
const retainedDocKeys = [];
|
|
210
553
|
for (const { document, storeKey } of storedEntries) {
|
|
211
554
|
if (!document || typeof document !== 'object' || Array.isArray(document)) continue;
|
|
212
555
|
const identity = String(
|
|
@@ -220,6 +563,7 @@ async function setLabStateWithClient(client, key, value) {
|
|
|
220
563
|
// in lab_state/lab_drafts, not in the Library.
|
|
221
564
|
if (!identity || !explicitLabel || identity.startsWith('__')) continue;
|
|
222
565
|
const docKey = `state:${stateKey}:${identity}`;
|
|
566
|
+
retainedDocKeys.push(docKey);
|
|
223
567
|
const type = String(document.type ?? `${labId}-${stateKey.includes('.projects') ? 'project' : 'preset'}`);
|
|
224
568
|
const label = explicitLabel.slice(0, 120);
|
|
225
569
|
const entry = {
|
|
@@ -230,17 +574,55 @@ async function setLabStateWithClient(client, key, value) {
|
|
|
230
574
|
tags: normalizeCreationTags(document.tags),
|
|
231
575
|
type,
|
|
232
576
|
};
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
[docKey, type, label, entry.tags, JSON.stringify(entry)],
|
|
577
|
+
// The doc key is the stable identity. Preserve its creation id and history
|
|
578
|
+
// even if a document-schema migration changes the type discriminator.
|
|
579
|
+
const matches = await client.query(
|
|
580
|
+
`select * from creations
|
|
581
|
+
where source = 'lab-state' and doc_key = $1
|
|
582
|
+
order by (type = $2) desc, updated_at desc
|
|
583
|
+
for update`,
|
|
584
|
+
[docKey, type],
|
|
242
585
|
);
|
|
586
|
+
let saved;
|
|
587
|
+
if (matches.rows.length) {
|
|
588
|
+
const keeper = matches.rows[0];
|
|
589
|
+
if (matches.rows.length > 1) {
|
|
590
|
+
await client.query(
|
|
591
|
+
`delete from creations where id = any($1::uuid[])`,
|
|
592
|
+
[matches.rows.slice(1).map((row) => row.id)],
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
saved = await client.query(
|
|
596
|
+
`update creations set
|
|
597
|
+
type = $2,
|
|
598
|
+
label = $3,
|
|
599
|
+
tags = $4,
|
|
600
|
+
document = $5::jsonb,
|
|
601
|
+
updated_at = now()
|
|
602
|
+
where id = $1
|
|
603
|
+
returning *`,
|
|
604
|
+
[keeper.id, type, label, entry.tags, JSON.stringify(entry)],
|
|
605
|
+
);
|
|
606
|
+
} else {
|
|
607
|
+
saved = await client.query(
|
|
608
|
+
`insert into creations (doc_key, type, label, tags, document, source)
|
|
609
|
+
values ($1, $2, $3, $4, $5::jsonb, 'lab-state')
|
|
610
|
+
returning *`,
|
|
611
|
+
[docKey, type, label, entry.tags, JSON.stringify(entry)],
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
await commitCreationRevision(client, {
|
|
615
|
+
...saved.rows[0],
|
|
616
|
+
aiGenerated: saved.rows[0].ai_generated,
|
|
617
|
+
}, { saveSource: 'lab-state' });
|
|
243
618
|
}
|
|
619
|
+
await client.query(
|
|
620
|
+
`delete from creations
|
|
621
|
+
where source = 'lab-state'
|
|
622
|
+
and left(doc_key, char_length($1)) = $1
|
|
623
|
+
and not (doc_key = any($2::text[]))`,
|
|
624
|
+
[`state:${stateKey}:`, retainedDocKeys],
|
|
625
|
+
);
|
|
244
626
|
}
|
|
245
627
|
|
|
246
628
|
export async function setLabState(key, value) {
|
|
@@ -302,11 +684,17 @@ export async function migrateLegacy({
|
|
|
302
684
|
const insert = await client.query(
|
|
303
685
|
`insert into creations (doc_key, type, label, description, document, ai_generated)
|
|
304
686
|
values ($1, $2, $3, $4, $5::jsonb, $6)
|
|
305
|
-
on conflict (type, doc_key) do nothing
|
|
687
|
+
on conflict (type, doc_key) do nothing
|
|
688
|
+
returning *`,
|
|
306
689
|
[value.docKey, value.type, value.label, value.description, JSON.stringify(entry), value.aiGenerated],
|
|
307
690
|
);
|
|
308
|
-
if (insert.rowCount)
|
|
309
|
-
|
|
691
|
+
if (insert.rowCount) {
|
|
692
|
+
await commitCreationRevision(client, {
|
|
693
|
+
...insert.rows[0],
|
|
694
|
+
aiGenerated: insert.rows[0].ai_generated,
|
|
695
|
+
}, { saveSource: 'legacy-import' });
|
|
696
|
+
imported += 1;
|
|
697
|
+
} else skipped += 1;
|
|
310
698
|
} catch (error) {
|
|
311
699
|
failures.push({ id: entry?.id ?? null, error: error.message });
|
|
312
700
|
}
|
|
@@ -336,7 +724,15 @@ export async function migrateLegacy({
|
|
|
336
724
|
return report;
|
|
337
725
|
}
|
|
338
726
|
|
|
339
|
-
export async function listCatalogAssets({
|
|
727
|
+
export async function listCatalogAssets({
|
|
728
|
+
q = '',
|
|
729
|
+
kind = '',
|
|
730
|
+
source = '',
|
|
731
|
+
license = '',
|
|
732
|
+
size = '',
|
|
733
|
+
limit = 60,
|
|
734
|
+
offset = 0,
|
|
735
|
+
} = {}) {
|
|
340
736
|
const pool = await getPool();
|
|
341
737
|
const values = [];
|
|
342
738
|
const where = [`availability_status = 'active'`];
|
|
@@ -356,6 +752,14 @@ export async function listCatalogAssets({ q = '', kind = '', source = '', limit
|
|
|
356
752
|
values.push(source);
|
|
357
753
|
where.push(`source = $${values.length}`);
|
|
358
754
|
}
|
|
755
|
+
if (license) {
|
|
756
|
+
values.push(license);
|
|
757
|
+
where.push(`license = $${values.length}`);
|
|
758
|
+
}
|
|
759
|
+
if (size) {
|
|
760
|
+
values.push(size);
|
|
761
|
+
where.push(`metadata->>'catalog' = 'rocks' and $${values.length} = any(tags)`);
|
|
762
|
+
}
|
|
359
763
|
values.push(Math.min(Math.max(Number(limit) || 60, 1), 500));
|
|
360
764
|
const limitIndex = values.length;
|
|
361
765
|
values.push(Math.max(Number(offset) || 0, 0));
|
|
@@ -374,6 +778,38 @@ export async function listCatalogAssets({ q = '', kind = '', source = '', limit
|
|
|
374
778
|
};
|
|
375
779
|
}
|
|
376
780
|
|
|
781
|
+
export async function listCatalogFacets() {
|
|
782
|
+
const pool = await getPool();
|
|
783
|
+
const [sources, licenses, kinds] = await Promise.all([
|
|
784
|
+
pool.query(
|
|
785
|
+
`select source, license, count(*)::int as count
|
|
786
|
+
from catalog_assets
|
|
787
|
+
where availability_status = 'active'
|
|
788
|
+
group by source, license
|
|
789
|
+
order by source, license`,
|
|
790
|
+
),
|
|
791
|
+
pool.query(
|
|
792
|
+
`select license, count(*)::int as count
|
|
793
|
+
from catalog_assets
|
|
794
|
+
where availability_status = 'active'
|
|
795
|
+
group by license
|
|
796
|
+
order by license`,
|
|
797
|
+
),
|
|
798
|
+
pool.query(
|
|
799
|
+
`select kind, count(*)::int as count
|
|
800
|
+
from catalog_assets
|
|
801
|
+
where availability_status = 'active'
|
|
802
|
+
group by kind
|
|
803
|
+
order by kind`,
|
|
804
|
+
),
|
|
805
|
+
]);
|
|
806
|
+
return {
|
|
807
|
+
sources: sources.rows,
|
|
808
|
+
licenses: licenses.rows,
|
|
809
|
+
kinds: kinds.rows,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
377
813
|
export async function getCatalogAsset(id) {
|
|
378
814
|
const pool = await getPool();
|
|
379
815
|
const asset = await pool.query(
|
|
@@ -490,6 +926,7 @@ export function providerConfiguration() {
|
|
|
490
926
|
gemini: Boolean(process.env.GEMINI_API_KEY || process.env.GEMINI_API_KEYS),
|
|
491
927
|
openai: Boolean(process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEYS),
|
|
492
928
|
ark: Boolean(process.env.ARK_API_KEY || process.env.ARK_API_KEYS),
|
|
929
|
+
fal: Boolean(process.env.FAL_KEY || process.env.FAL_KEYS),
|
|
493
930
|
polypizza: Boolean(process.env.POLYPIZZA_API_KEY),
|
|
494
931
|
};
|
|
495
932
|
return {
|
|
@@ -524,6 +961,17 @@ export function providerConfiguration() {
|
|
|
524
961
|
resolutions: ['1k', '2k', '4k'],
|
|
525
962
|
},
|
|
526
963
|
],
|
|
964
|
+
materialModels: [
|
|
965
|
+
{
|
|
966
|
+
configured: providers.fal,
|
|
967
|
+
id: 'patina',
|
|
968
|
+
label: 'Patina PBR Material',
|
|
969
|
+
maps: ['basecolor', 'normal', 'roughness', 'metalness', 'height'],
|
|
970
|
+
model: 'fal-ai/patina/material',
|
|
971
|
+
provider: 'fal',
|
|
972
|
+
resolutions: ['1k', '2k', '4k', '8k'],
|
|
973
|
+
},
|
|
974
|
+
],
|
|
527
975
|
promptEnhancementConfigured: providers.gemini || providers.openai,
|
|
528
976
|
providers,
|
|
529
977
|
styles: [
|