@drawcall/design 0.1.7 → 0.2.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.
- package/README.md +20 -10
- package/dist/cli.js +142 -141
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/skill.generated.d.ts +1 -1
- package/dist/skill.generated.js +1 -1
- package/dist/target.d.ts +0 -14
- package/dist/target.js +0 -23
- package/dist/v1/contract.d.ts +109 -67
- package/dist/v1/contract.js +32 -54
- package/dist/v1/schemas.d.ts +107 -44
- package/dist/v1/schemas.js +86 -60
- package/package.json +1 -1
- package/skills/drawcall-design/SKILL.md +30 -15
- package/dist/source.d.ts +0 -10
- package/dist/source.js +0 -38
package/dist/v1/schemas.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const MAX_FRAME_DIMENSION = 4_096;
|
|
3
|
-
export const
|
|
4
|
-
export const
|
|
5
|
-
|
|
3
|
+
export const MAX_FILE_LENGTH = 512_000;
|
|
4
|
+
export const designIdSchema = z
|
|
5
|
+
.string()
|
|
6
|
+
.regex(/^[a-z0-9]{14}$/, "IDs are 14 lowercase alphanumeric characters");
|
|
6
7
|
export const projectNameSchema = z
|
|
7
8
|
.string()
|
|
8
9
|
.trim()
|
|
@@ -22,37 +23,28 @@ export const httpUrlSchema = z
|
|
|
22
23
|
.url()
|
|
23
24
|
.refine(isHttpUrl, "URLs must use HTTP or HTTPS");
|
|
24
25
|
export const imageUrlSchema = httpUrlSchema;
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
/** An absolute path in the hosted project filesystem. */
|
|
27
|
+
export const projectPathSchema = z.string().max(1_024).refine(isProjectPath, {
|
|
28
|
+
message: "Project paths must be absolute and cannot contain empty, . or .. segments",
|
|
27
29
|
});
|
|
28
|
-
|
|
29
|
-
export const
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
if (entries.length > MAX_SOURCE_FILE_COUNT) {
|
|
34
|
-
context.addIssue({
|
|
35
|
-
code: "custom",
|
|
36
|
-
message: `Source cannot contain more than ${MAX_SOURCE_FILE_COUNT} files`,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
const totalLength = entries.reduce((total, [, code]) => total + code.length, 0);
|
|
40
|
-
if (totalLength > MAX_SOURCE_TOTAL_LENGTH) {
|
|
41
|
-
context.addIssue({
|
|
42
|
-
code: "custom",
|
|
43
|
-
message: `Source cannot exceed ${MAX_SOURCE_TOTAL_LENGTH} characters`,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
.refine((files) => !("/index.js" in files && "/index.ts" in files), {
|
|
48
|
-
message: "A frame cannot contain both /index.js and /index.ts",
|
|
30
|
+
/** A project directory path. `/` denotes the project root. */
|
|
31
|
+
export const projectDirectoryPathSchema = z
|
|
32
|
+
.union([z.literal("/"), projectPathSchema])
|
|
33
|
+
.refine((path) => path === "/" || !path.endsWith("/"), {
|
|
34
|
+
message: "Directory paths cannot end with /",
|
|
49
35
|
});
|
|
50
|
-
export const
|
|
51
|
-
|
|
36
|
+
export const fileTextSchema = z.string().max(MAX_FILE_LENGTH);
|
|
37
|
+
export const marketAssetNameSchema = z
|
|
38
|
+
.string()
|
|
52
39
|
.min(1)
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
.max(128)
|
|
41
|
+
.regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/, "Asset names must be lowercase alphanumeric with hyphens");
|
|
42
|
+
export const marketAssetVersionSchema = z
|
|
43
|
+
.string()
|
|
44
|
+
.regex(/^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$/, "Asset versions must be exact semver versions (for example 1.0.0)");
|
|
45
|
+
export const marketAssetSchema = z
|
|
46
|
+
.string()
|
|
47
|
+
.refine(isMarketAsset, "Assets must use exact name@version references");
|
|
56
48
|
export const userSchema = z.object({
|
|
57
49
|
id: z.string().min(1),
|
|
58
50
|
name: z.string(),
|
|
@@ -60,72 +52,99 @@ export const userSchema = z.object({
|
|
|
60
52
|
image: z.string().url().nullable(),
|
|
61
53
|
});
|
|
62
54
|
export const projectSchema = z.object({
|
|
63
|
-
id:
|
|
55
|
+
id: designIdSchema,
|
|
64
56
|
name: projectNameSchema,
|
|
65
57
|
createdAt: z.string().datetime(),
|
|
66
58
|
updatedAt: z.string().datetime(),
|
|
67
59
|
});
|
|
68
60
|
const frameBaseSchema = z.object({
|
|
69
|
-
id:
|
|
70
|
-
projectId:
|
|
61
|
+
id: designIdSchema,
|
|
62
|
+
projectId: designIdSchema,
|
|
71
63
|
name: frameNameSchema,
|
|
72
64
|
x: framePositionSchema,
|
|
73
65
|
y: framePositionSchema,
|
|
74
66
|
width: frameSizeSchema,
|
|
75
67
|
height: frameSizeSchema,
|
|
68
|
+
viewUrl: httpUrlSchema,
|
|
76
69
|
createdAt: z.string().datetime(),
|
|
77
70
|
updatedAt: z.string().datetime(),
|
|
78
71
|
});
|
|
79
|
-
export const
|
|
80
|
-
|
|
81
|
-
viewUrl: httpUrlSchema,
|
|
72
|
+
export const gltsFrameSchema = frameBaseSchema.extend({
|
|
73
|
+
type: z.literal("glts"),
|
|
82
74
|
});
|
|
83
75
|
export const imageFrameSchema = frameBaseSchema.extend({
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
type: z.literal("image"),
|
|
77
|
+
file: projectPathSchema,
|
|
86
78
|
});
|
|
87
|
-
export const
|
|
88
|
-
|
|
79
|
+
export const marketFrameSchema = frameBaseSchema.extend({
|
|
80
|
+
type: z.literal("market"),
|
|
81
|
+
asset: marketAssetSchema,
|
|
82
|
+
});
|
|
83
|
+
export const frameSchema = z.discriminatedUnion("type", [
|
|
84
|
+
gltsFrameSchema,
|
|
89
85
|
imageFrameSchema,
|
|
86
|
+
marketFrameSchema,
|
|
90
87
|
]);
|
|
91
88
|
const createFrameBaseSchema = z.object({
|
|
92
|
-
project:
|
|
89
|
+
project: designIdSchema,
|
|
93
90
|
name: frameNameSchema,
|
|
94
|
-
width: frameSizeSchema,
|
|
95
|
-
height: frameSizeSchema,
|
|
96
91
|
x: framePositionSchema.optional(),
|
|
97
92
|
y: framePositionSchema.optional(),
|
|
98
93
|
});
|
|
99
|
-
export const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
export const createGltsFrameSchema = createFrameBaseSchema
|
|
95
|
+
.extend({
|
|
96
|
+
type: z.literal("glts"),
|
|
97
|
+
width: frameSizeSchema,
|
|
98
|
+
height: frameSizeSchema,
|
|
99
|
+
})
|
|
100
|
+
.strict();
|
|
101
|
+
export const createImageFrameSchema = createFrameBaseSchema
|
|
102
|
+
.extend({
|
|
103
|
+
type: z.literal("image"),
|
|
105
104
|
image: z.union([
|
|
106
105
|
z.file().mime(["image/png", "image/jpeg", "image/webp"]),
|
|
107
106
|
imageUrlSchema,
|
|
108
107
|
]),
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
})
|
|
109
|
+
.strict();
|
|
110
|
+
export const createMarketFrameSchema = createFrameBaseSchema
|
|
111
|
+
.extend({
|
|
112
|
+
type: z.literal("market"),
|
|
113
|
+
asset: marketAssetSchema,
|
|
114
|
+
})
|
|
115
|
+
.strict();
|
|
116
|
+
export const createFrameSchema = z.discriminatedUnion("type", [
|
|
117
|
+
createGltsFrameSchema,
|
|
112
118
|
createImageFrameSchema,
|
|
119
|
+
createMarketFrameSchema,
|
|
113
120
|
]);
|
|
114
|
-
export const
|
|
115
|
-
paths: z.array(
|
|
121
|
+
export const fileListSchema = z.object({
|
|
122
|
+
paths: z.array(projectPathSchema),
|
|
123
|
+
});
|
|
124
|
+
export const textFileSchema = z.object({
|
|
125
|
+
type: z.literal("text"),
|
|
126
|
+
path: projectPathSchema,
|
|
127
|
+
text: z.string(),
|
|
128
|
+
mediaType: z.literal("text/plain"),
|
|
116
129
|
});
|
|
117
|
-
export const
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
export const binaryFileSchema = z.object({
|
|
131
|
+
type: z.literal("binary"),
|
|
132
|
+
path: projectPathSchema,
|
|
133
|
+
url: httpUrlSchema,
|
|
134
|
+
mediaType: z.string().min(1),
|
|
120
135
|
});
|
|
121
|
-
export const
|
|
136
|
+
export const fileSchema = z.discriminatedUnion("type", [
|
|
137
|
+
textFileSchema,
|
|
138
|
+
binaryFileSchema,
|
|
139
|
+
]);
|
|
140
|
+
export const fileMutationSchema = fileListSchema;
|
|
122
141
|
export const screenshotSchema = z.object({
|
|
123
142
|
url: httpUrlSchema,
|
|
124
143
|
width: frameSizeSchema,
|
|
125
144
|
height: frameSizeSchema,
|
|
126
145
|
mediaType: z.literal("image/png"),
|
|
127
146
|
});
|
|
128
|
-
function
|
|
147
|
+
function isProjectPath(path) {
|
|
129
148
|
if (!path.startsWith("/") || path === "/" || path.endsWith("/"))
|
|
130
149
|
return false;
|
|
131
150
|
if (path.includes("\\") || path.includes("\0"))
|
|
@@ -135,6 +154,13 @@ function isSourcePath(path) {
|
|
|
135
154
|
.split("/")
|
|
136
155
|
.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
137
156
|
}
|
|
157
|
+
function isMarketAsset(value) {
|
|
158
|
+
const separator = value.lastIndexOf("@");
|
|
159
|
+
if (separator < 1 || separator === value.length - 1)
|
|
160
|
+
return false;
|
|
161
|
+
return (marketAssetNameSchema.safeParse(value.slice(0, separator)).success &&
|
|
162
|
+
marketAssetVersionSchema.safeParse(value.slice(separator + 1)).success);
|
|
163
|
+
}
|
|
138
164
|
function isHttpUrl(value) {
|
|
139
165
|
if (!URL.canParse(value))
|
|
140
166
|
return false;
|
package/package.json
CHANGED
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: drawcall-design
|
|
3
|
-
description: Create, modify, inspect, and screenshot 3D
|
|
3
|
+
description: Create, modify, inspect, and screenshot 3D assets and reference frames in Drawcall Design. Use for Drawcall Design projects, frames, the project filesystem, GLTS assets, or Drawcall Market frames. Do not use for full games or applications.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Drawcall Design
|
|
7
7
|
|
|
8
|
-
Use the Drawcall Design MCP tools when available. Otherwise
|
|
8
|
+
Use the Drawcall Design MCP tools when available. Otherwise use `npx @drawcall/design`. 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.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## Project filesystem
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
A project is a hosted filesystem at `https://<project-id>.design.drawcallcontent.com/`. Each frame owns one top-level folder, `/<frame-id>/`. The CLI selects the project with `-p <project-id>`; its file paths are exactly the absolute paths inside that project, for example `/f7k3m9q2x8vd/index.glts`.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Create frames with an explicit type. GLTS frames require `--size`; image and Market frames derive their canvas size. A Market frame requires an exact public asset reference, `name@version`.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
## GLTS assets
|
|
19
|
+
|
|
20
|
+
A GLTS frame contains only `.glts` files. `index.glts` is its optional root asset; without it the frame renders empty. Every `.glts` file is a trusted TypeScript ESM module that default-exports a no-argument class derived from `THREE.Object3D`. Avoid top-level side effects because reload evaluates the module again. Implement `dispose()` when the asset exclusively owns disposable resources.
|
|
17
21
|
|
|
18
22
|
```ts
|
|
19
23
|
import * as THREE from "three";
|
|
24
|
+
import Wheel from "./parts/wheel.glts";
|
|
25
|
+
|
|
26
|
+
export default class Racecar extends THREE.Group {
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.add(new Wheel());
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
// Build the object or scene.
|
|
34
|
+
Use relative `.glts` imports within a frame. Use a project-absolute path to import a GLTS asset from another frame:
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
```ts
|
|
37
|
+
import Chassis from "/other-frame-id/index.glts";
|
|
38
|
+
```
|
|
25
39
|
|
|
26
|
-
|
|
27
|
-
view.position.set(4, 3, 5);
|
|
28
|
-
view.lookAt(0, 0, 0);
|
|
40
|
+
For a non-GLTS file from an image or Market frame, preserve the project filesystem URL through `import.meta.url`:
|
|
29
41
|
|
|
30
|
-
|
|
42
|
+
```ts
|
|
43
|
+
const modelUrl = new URL("/market-frame-id/models/car.glb", import.meta.url);
|
|
31
44
|
```
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
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.
|
|
34
49
|
|
|
35
|
-
After every meaningful visual change, take
|
|
50
|
+
After every meaningful visual change, take and inspect a screenshot. Iterate until the asset, camera, composition, and requested frame size are right.
|
package/dist/source.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { SourceFiles } from "./v1/schemas.js";
|
|
2
|
-
export declare class SourceEditError extends Error {
|
|
3
|
-
constructor(message: string);
|
|
4
|
-
}
|
|
5
|
-
export declare class SourceFileNotFoundError extends Error {
|
|
6
|
-
constructor(path: string);
|
|
7
|
-
}
|
|
8
|
-
export declare function editSourceCode(code: string, oldText: string, newText: string): string;
|
|
9
|
-
export declare function editSourceFile(files: SourceFiles, path: string, oldText: string, newText: string): SourceFiles;
|
|
10
|
-
export declare function deleteSourceFiles(files: SourceFiles, paths: string[]): SourceFiles;
|
package/dist/source.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export class SourceEditError extends Error {
|
|
2
|
-
constructor(message) {
|
|
3
|
-
super(message);
|
|
4
|
-
this.name = "SourceEditError";
|
|
5
|
-
}
|
|
6
|
-
}
|
|
7
|
-
export class SourceFileNotFoundError extends Error {
|
|
8
|
-
constructor(path) {
|
|
9
|
-
super(`Source file not found: ${path}`);
|
|
10
|
-
this.name = "SourceFileNotFoundError";
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
export function editSourceCode(code, oldText, newText) {
|
|
14
|
-
if (oldText.length === 0)
|
|
15
|
-
throw new SourceEditError("oldText cannot be empty");
|
|
16
|
-
const match = code.indexOf(oldText);
|
|
17
|
-
if (match === -1)
|
|
18
|
-
throw new SourceEditError("oldText does not occur in the source file");
|
|
19
|
-
if (code.indexOf(oldText, match + 1) !== -1) {
|
|
20
|
-
throw new SourceEditError("oldText occurs more than once in the source file");
|
|
21
|
-
}
|
|
22
|
-
return code.slice(0, match) + newText + code.slice(match + oldText.length);
|
|
23
|
-
}
|
|
24
|
-
export function editSourceFile(files, path, oldText, newText) {
|
|
25
|
-
const code = files[path];
|
|
26
|
-
if (code === undefined)
|
|
27
|
-
throw new SourceFileNotFoundError(path);
|
|
28
|
-
return { ...files, [path]: editSourceCode(code, oldText, newText) };
|
|
29
|
-
}
|
|
30
|
-
export function deleteSourceFiles(files, paths) {
|
|
31
|
-
if (paths.length === 1 && paths[0] === "*")
|
|
32
|
-
return {};
|
|
33
|
-
for (const path of paths) {
|
|
34
|
-
if (!Object.hasOwn(files, path))
|
|
35
|
-
throw new SourceFileNotFoundError(path);
|
|
36
|
-
}
|
|
37
|
-
return Object.fromEntries(Object.entries(files).filter(([path]) => !paths.includes(path)));
|
|
38
|
-
}
|