@drawcall/design 0.8.1 → 0.9.0

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.
@@ -58,6 +58,10 @@ export const httpUrlSchema = z
58
58
  .url()
59
59
  .refine(isHttpUrl, "URLs must use HTTP or HTTPS");
60
60
  export const imageUrlSchema = httpUrlSchema;
61
+ export const imageInputSchema = z.union([
62
+ z.file().mime(["image/png", "image/jpeg", "image/webp"]),
63
+ imageUrlSchema,
64
+ ]);
61
65
  /** An absolute path in the hosted project filesystem. */
62
66
  export const projectPathSchema = z.string().max(1_024).refine(isProjectPath, {
63
67
  message: "Project paths must be absolute and cannot contain empty, . or .. segments",
@@ -122,33 +126,45 @@ export const gltsFrameSchema = frameBaseSchema.extend({
122
126
  });
123
127
  export const imageGenerationOperationSchema = z.enum(["generate", "edit"]);
124
128
  export const imageGenerationResultSchema = z.enum(["new", "replace"]);
125
- export const imageGenerationReferenceSchema = z.object({
126
- frameId: designIdSchema,
127
- frameUpdatedAt: z.string().datetime(),
128
- });
129
+ const imageGenerationDigestSchema = z
130
+ .string()
131
+ .regex(/^[a-f0-9]{64}$/, "Image generation digests must be SHA-256 hex");
132
+ export const imageInputProvenanceSchema = z.discriminatedUnion("kind", [
133
+ z
134
+ .object({
135
+ kind: z.literal("url"),
136
+ url: imageUrlSchema,
137
+ digest: imageGenerationDigestSchema,
138
+ })
139
+ .strict(),
140
+ z
141
+ .object({
142
+ kind: z.literal("file"),
143
+ name: z
144
+ .string()
145
+ .trim()
146
+ .min(1)
147
+ .max(255)
148
+ .refine(isFileName, "Image filenames cannot contain path separators"),
149
+ digest: imageGenerationDigestSchema,
150
+ })
151
+ .strict(),
152
+ ]);
129
153
  export const IMAGE_GENERATION_PROVIDER = "fal-ai";
130
154
  export const IMAGE_GENERATION_MODEL = "openai/gpt-image-2";
131
- export const LEGACY_IMAGE_GENERATION_PROVIDER = "cloudflare-workers-ai";
132
- export const LEGACY_IMAGE_GENERATION_MODEL = "@cf/black-forest-labs/flux-2-klein-4b";
133
155
  const imageGenerationProvenanceBaseSchema = z.object({
134
156
  kind: z.literal("generated"),
135
157
  operation: imageGenerationOperationSchema,
136
158
  prompt: z.string().min(1).max(MAX_IMAGE_GENERATION_PROMPT_LENGTH),
137
159
  references: z
138
- .array(imageGenerationReferenceSchema)
160
+ .array(imageInputProvenanceSchema)
139
161
  .max(MAX_IMAGE_GENERATION_REFERENCES),
140
162
  result: imageGenerationResultSchema,
141
163
  });
142
- export const imageGenerationProvenanceSchema = z.discriminatedUnion("provider", [
143
- imageGenerationProvenanceBaseSchema.extend({
144
- provider: z.literal(IMAGE_GENERATION_PROVIDER),
145
- model: z.literal(IMAGE_GENERATION_MODEL),
146
- }),
147
- imageGenerationProvenanceBaseSchema.extend({
148
- provider: z.literal(LEGACY_IMAGE_GENERATION_PROVIDER),
149
- model: z.literal(LEGACY_IMAGE_GENERATION_MODEL),
150
- }),
151
- ]);
164
+ export const imageGenerationProvenanceSchema = imageGenerationProvenanceBaseSchema.extend({
165
+ provider: z.literal(IMAGE_GENERATION_PROVIDER),
166
+ model: z.literal(IMAGE_GENERATION_MODEL),
167
+ });
152
168
  export const imageFrameSchema = frameBaseSchema.extend({
153
169
  type: z.literal("image"),
154
170
  file: projectFilePathSchema,
@@ -179,10 +195,7 @@ export const createGltsFrameSchema = createFrameBaseSchema
179
195
  export const createImageFrameSchema = createFrameBaseSchema
180
196
  .extend({
181
197
  type: z.literal("image"),
182
- image: z.union([
183
- z.file().mime(["image/png", "image/jpeg", "image/webp"]),
184
- imageUrlSchema,
185
- ]),
198
+ image: imageInputSchema,
186
199
  })
187
200
  .strict();
188
201
  export const createMarketFrameSchema = createFrameBaseSchema
@@ -196,85 +209,56 @@ export const createFrameSchema = z.discriminatedUnion("type", [
196
209
  createImageFrameSchema,
197
210
  createMarketFrameSchema,
198
211
  ]);
199
- export const generateImageFrameSchema = z
200
- .object({
212
+ const imageGenerationPromptSchema = z
213
+ .string()
214
+ .trim()
215
+ .min(1)
216
+ .max(MAX_IMAGE_GENERATION_PROMPT_LENGTH);
217
+ const imageGenerationBase = {
201
218
  project: designIdSchema,
202
- operation: imageGenerationOperationSchema,
203
- prompt: z.string().trim().min(1).max(MAX_IMAGE_GENERATION_PROMPT_LENGTH),
204
- target: designIdSchema.optional(),
205
- references: z
206
- .array(designIdSchema)
207
- .max(MAX_IMAGE_GENERATION_REFERENCES)
208
- .default([]),
209
- result: imageGenerationResultSchema,
210
- name: frameNameSchema.optional(),
219
+ prompt: imageGenerationPromptSchema,
220
+ };
221
+ const newGeneratedFrame = {
222
+ result: z.literal("new"),
223
+ name: frameNameSchema,
211
224
  x: framePositionSchema.optional(),
212
225
  y: framePositionSchema.optional(),
213
- })
214
- .strict()
215
- .superRefine((input, context) => {
216
- const orderedReferences = [
217
- ...(input.target ? [input.target] : []),
218
- ...input.references,
219
- ];
220
- if (new Set(orderedReferences).size !== orderedReferences.length) {
221
- context.addIssue({
222
- code: "custom",
223
- path: ["references"],
224
- message: "Image references must be unique",
225
- });
226
- }
227
- if (orderedReferences.length > MAX_IMAGE_GENERATION_REFERENCES) {
228
- context.addIssue({
229
- code: "custom",
230
- path: ["references"],
231
- message: `Image generation supports at most ${MAX_IMAGE_GENERATION_REFERENCES} references`,
232
- });
233
- }
234
- if (input.operation === "generate" && input.target) {
235
- context.addIssue({
236
- code: "custom",
237
- path: ["target"],
238
- message: "A generation request cannot have an edit target",
239
- });
240
- }
241
- if (input.operation === "edit" && !input.target) {
242
- context.addIssue({
243
- code: "custom",
244
- path: ["target"],
245
- message: "An edit request requires an image target",
246
- });
247
- }
248
- if (input.result === "replace" && input.operation !== "edit") {
249
- context.addIssue({
250
- code: "custom",
251
- path: ["result"],
252
- message: "Only an image edit can replace a frame",
253
- });
254
- }
255
- if (input.result === "new" && !input.name) {
256
- context.addIssue({
257
- code: "custom",
258
- path: ["name"],
259
- message: "A new generated frame requires a name",
260
- });
261
- }
262
- if (input.result === "replace" && input.name) {
263
- context.addIssue({
264
- code: "custom",
265
- path: ["name"],
266
- message: "A replacement keeps the target frame name",
267
- });
268
- }
269
- if (input.result === "replace" &&
270
- (input.x !== undefined || input.y !== undefined)) {
271
- context.addIssue({
272
- code: "custom",
273
- path: [input.x !== undefined ? "x" : "y"],
274
- message: "A replacement keeps the target frame position",
275
- });
276
- }
277
- });
226
+ };
227
+ export const generateImageSchema = imageGenerationSchema(imageInputSchema);
228
+ export const generateImageUrlSchema = imageGenerationSchema(imageUrlSchema);
229
+ function imageGenerationSchema(input) {
230
+ return z.union([
231
+ z
232
+ .object({
233
+ ...imageGenerationBase,
234
+ operation: z.literal("generate"),
235
+ references: z.array(input).max(4).default([]),
236
+ ...newGeneratedFrame,
237
+ })
238
+ .strict(),
239
+ z
240
+ .object({
241
+ ...imageGenerationBase,
242
+ operation: z.literal("edit"),
243
+ target: designIdSchema,
244
+ references: z.array(input).max(3).default([]),
245
+ ...newGeneratedFrame,
246
+ })
247
+ .strict(),
248
+ z
249
+ .object({
250
+ ...imageGenerationBase,
251
+ operation: z.literal("edit"),
252
+ target: designIdSchema,
253
+ references: z.array(input).max(3).default([]),
254
+ result: z.literal("replace"),
255
+ name: z.never().optional(),
256
+ x: z.never().optional(),
257
+ y: z.never().optional(),
258
+ })
259
+ .strict(),
260
+ ]);
261
+ }
278
262
  export const fileListSchema = z.object({
279
263
  paths: z.array(projectFilePathSchema),
280
264
  });
@@ -387,3 +371,10 @@ function isNormalizedQuaternion(value) {
387
371
  value.w * value.w;
388
372
  return Math.abs(squaredLength - 1) <= 1e-6;
389
373
  }
374
+ function isFileName(value) {
375
+ return (value !== "." &&
376
+ value !== ".." &&
377
+ !value.includes("/") &&
378
+ !value.includes("\\") &&
379
+ !value.includes("\0"));
380
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawcall/design",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Typed API and remote CLI for Drawcall Design",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,9 +7,11 @@ description: Create, modify, inspect, compose, and reuse 3D GLTS assets and opti
7
7
 
8
8
  ## CLI transport
9
9
 
10
- The CLI is the only Drawcall transport in this environment. Do not search for MCP tools or make direct HTTP requests. A failed CLI command does not justify switching transport.
10
+ The commands below are the Drawcall transport in this environment. Use only this documented command interface. A failed command does not make the transport unavailable.
11
11
 
12
- CLI calls are shell command strings. Select the project with `-p <project-id>`. File commands take project-absolute paths that include the frame ID.
12
+ Image frame creation and image-generation references accept public HTTP(S) URLs or local PNG, JPEG, and WebP files.
13
+
14
+ Select the project with `-p <project-id>`. File commands take project-absolute paths that include the frame ID. Repeat `--reference` to preserve image-generation reference order.
13
15
 
14
16
  ```sh
15
17
  npx @drawcall/design project list
@@ -17,6 +19,8 @@ npx @drawcall/design -p r6z2n9k4x8m1qc frame list
17
19
  npx @drawcall/design -p r6z2n9k4x8m1qc ls
18
20
  npx @drawcall/design -p r6z2n9k4x8m1qc read /a4z8m2q7v9kcde/index.glts
19
21
  npx @drawcall/design -p r6z2n9k4x8m1qc edit /a4z8m2q7v9kcde/index.glts 'color: 0xffffff' 'color: 0x000000'
22
+ npx @drawcall/design -p r6z2n9k4x8m1qc frame generate-image 'Product photograph' --prompt 'A product photograph of this object' --reference https://r6z2n9k4x8m1qc.design.drawcallcontent.com/a4z8m2q7v9kcde.webp
23
+ npx @drawcall/design -p r6z2n9k4x8m1qc frame edit-image b4z8m2q7v9kcdf --prompt 'Use warmer light' --name 'Warm product photograph' --reference ./lighting.webp
20
24
  ```
21
25
 
22
26
  Design is a remote, current-state canvas. Inspect the project and its frames before changing them. Use immutable IDs for every project and frame target; names are only labels.
@@ -25,10 +29,14 @@ Design is a remote, current-state canvas. Inspect the project and its frames bef
25
29
 
26
30
  A project is a hosted filesystem at `https://<project-id>.design.drawcallcontent.com/`. Each frame owns one top-level directory, `/<frame-id>`. Files use project-absolute paths that include that directory, for example `/a4z8m2q7v9kcde/index.glts`.
27
31
 
28
- Create frames with an explicit type. Choose the type from the requested artifact, not from the word "frame": use GLTS for a 3D object or scene, especially one another frame will reuse. Use image only for a supplied 2D image URL and Market only for an exact public asset reference, `name@version`. GLTS frames require a viewport size; image and Market frames derive their canvas size.
32
+ Create frames with an explicit type. Choose the type from the requested artifact, not from the word "frame": use GLTS for a 3D object or scene, especially one another frame will reuse. Use image for an existing 2D image and Market only for an exact public asset reference, `name@version`. GLTS frames require a viewport size; image and Market frames derive their canvas size.
29
33
 
30
34
  Read a file before editing it. Use a narrow edit for one known change and write a complete file when replacing it. Only GLTS frame files may be created or deleted. Image and Market frame files are read-only.
31
35
 
36
+ ## Frame images
37
+
38
+ Every frame has a public image at `https://<project-id>.design.drawcallcontent.com/<frame-id>.webp`. This URL represents the current rendered frame whether its type is GLTS, image, or Market. Pass it with `--reference` when one frame's appearance should inform another image. A reference may instead be a local PNG, JPEG, or WebP file.
39
+
32
40
  ## Failures
33
41
 
34
42
  An error means the requested operation did not happen. Follow its next action without switching transport. Correct invalid arguments from the documented shape. Refresh projects, frames, or files after a not-found or conflict error, then reuse the exact returned IDs and paths. Retry an upstream or internal failure once; if it repeats, report the failed operation and error. Never repeat an unchanged failed operation.
@@ -63,7 +71,7 @@ For a non-GLTS file from an image or Market frame, preserve the project filesyst
63
71
  const modelUrl = new URL("/market-frame-id/models/car.glb", import.meta.url);
64
72
  ```
65
73
 
66
- GLTS supports static `.glts`, `three`, Three addons, and bare npm imports. It does not support local helper `.ts` modules, dynamic imports, cyclic GLTS graphs, or cross-asset inheritance. Keep the asset self-contained and compose with nested GLTS assets.
74
+ GLTS supports static `.glts`, `three`, Three addons, and bare npm imports. It does not support helper `.ts` modules, dynamic imports, cyclic GLTS graphs, or cross-asset inheritance. Keep the asset self-contained and compose with nested GLTS assets.
67
75
 
68
76
  The viewer uses the first camera found by depth-first traversal. If none exists, it autofits the asset. Put an authored camera in the scene only when its framing is intentional. Double-clicking a frame enters orbit from that resolved view; deselecting restores it.
69
77