@drawcall/design 0.1.8 → 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.
@@ -1,8 +1,9 @@
1
1
  import { z } from "zod";
2
2
  export const MAX_FRAME_DIMENSION = 4_096;
3
- export const MAX_SOURCE_FILE_COUNT = 64;
4
- export const MAX_SOURCE_FILE_LENGTH = 512_000;
5
- export const MAX_SOURCE_TOTAL_LENGTH = 4_000_000;
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
- export const sourcePathSchema = z.string().max(1_024).refine(isSourcePath, {
26
- message: "Source paths must be absolute and cannot contain empty, . or .. segments",
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
- export const sourceCodeSchema = z.string().max(MAX_SOURCE_FILE_LENGTH);
29
- export const sourceFilesSchema = z
30
- .record(sourcePathSchema, sourceCodeSchema)
31
- .superRefine((files, context) => {
32
- const entries = Object.entries(files);
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 sourceDeletePathsSchema = z
51
- .array(z.union([sourcePathSchema, z.literal("*")]))
36
+ export const fileTextSchema = z.string().max(MAX_FILE_LENGTH);
37
+ export const marketAssetNameSchema = z
38
+ .string()
52
39
  .min(1)
53
- .refine((paths) => !paths.includes("*") || paths.length === 1, {
54
- message: "* must be the only delete path",
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: z.string().min(1),
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: z.string().uuid(),
70
- projectId: z.string().min(1),
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 codeFrameSchema = frameBaseSchema.extend({
80
- kind: z.literal("code"),
81
- viewUrl: httpUrlSchema,
72
+ export const gltsFrameSchema = frameBaseSchema.extend({
73
+ type: z.literal("glts"),
82
74
  });
83
75
  export const imageFrameSchema = frameBaseSchema.extend({
84
- kind: z.literal("image"),
85
- contentUrl: httpUrlSchema,
76
+ type: z.literal("image"),
77
+ file: projectPathSchema,
86
78
  });
87
- export const frameSchema = z.discriminatedUnion("kind", [
88
- codeFrameSchema,
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: projectNameSchema,
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 createCodeFrameSchema = createFrameBaseSchema.extend({
100
- kind: z.literal("code"),
101
- source: sourceFilesSchema.optional(),
102
- });
103
- export const createImageFrameSchema = createFrameBaseSchema.extend({
104
- kind: z.literal("image"),
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
- export const createFrameSchema = z.discriminatedUnion("kind", [
111
- createCodeFrameSchema,
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 sourceListSchema = z.object({
115
- paths: z.array(sourcePathSchema),
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 sourceFileSchema = z.object({
118
- path: sourcePathSchema,
119
- code: z.string(),
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 sourceMutationSchema = sourceListSchema;
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 isSourcePath(path) {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawcall/design",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Typed API and remote CLI for Drawcall Design",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,35 +1,50 @@
1
1
  ---
2
2
  name: drawcall-design
3
- description: Create, modify, inspect, and screenshot 3D objects and scenes in Drawcall Design using its MCP tools or @drawcall/design CLI. Use for Drawcall Design projects, frames, and code-frame source. Do not use for full games or applications.
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, use `npx @drawcall/design`. Drawcall Design is a remote, current-state canvas: inspect the existing project, frames, and source before changing them.
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
- A code frame stores source as a `Record<absolute path, code>`. A renderable frame has exactly one `/index.ts` or `/index.js`. That module must export `scene`, whose value is a `THREE.Object3D`, and may export `camera`, whose value is a `THREE.Camera`.
10
+ ## Project filesystem
11
11
 
12
- Source runs as browser ESM. Use relative imports with explicit extensions for other frame files and normal package imports for browser-compatible packages. Top-level await is supported. In TypeScript files, use syntax that can be erased; avoid features that generate JavaScript.
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
- A code frame represents a designed state rather than a running application. Complete asynchronous setup before exporting the scene, and add any lighting or environment the object needs.
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
- Create image frames from a public HTTP(S) URL through MCP. In the CLI, `frame create --image` accepts either an HTTP(S) URL or a local PNG, JPEG, or WebP file.
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
- const root = new THREE.Scene();
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
- export const scene: THREE.Object3D = root;
36
+ ```ts
37
+ import Chassis from "/other-frame-id/index.glts";
38
+ ```
25
39
 
26
- const view = new THREE.PerspectiveCamera(35, 1, 0.1, 100);
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
- export const camera: THREE.Camera = view;
42
+ ```ts
43
+ const modelUrl = new URL("/market-frame-id/models/car.glb", import.meta.url);
31
44
  ```
32
45
 
33
- Read source before editing it. Prefer an exact edit for a small change, write one file when replacing that file, and replace the complete source only when the whole frame should change.
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 a frame screenshot and inspect it. Iterate until the rendered object, composition, and frame size satisfy the request.
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
- }