cadcode 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,215 @@
1
- # Temporary Holding Version
1
+ # cadcode
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Code-first CAD. Write a model in JavaScript, see it live in the browser while you edit, and export STL for printing and PNG for previews. There is no modeling UI: your editor is the CAD system.
4
+
5
+ The geometry runs on [manifold](https://github.com/elalish/manifold), the same boolean engine recent OpenSCAD uses, so solids always come out watertight. The API keeps OpenSCAD's ideas (cube, cylinder, hull, difference, `$fn`), so OpenSCAD models port line by line.
6
+
7
+ ```js
8
+ import { cube, cylinder, difference } from 'cadcode';
9
+
10
+ export default difference(
11
+ cube([20, 20, 5], { center: true }),
12
+ cylinder({ d: 6, h: 10, center: true }),
13
+ );
14
+ ```
15
+
16
+ ```bash
17
+ npx cadcode dev plate.js
18
+ ```
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install cadcode
24
+ ```
25
+
26
+ Requires Node 20 or newer.
27
+
28
+ ## Commands
29
+
30
+ ```bash
31
+ cadcode dev <file> # live preview in the browser, reloads on save
32
+ cadcode stl <file> # export a part to binary STL
33
+ cadcode png <file> # render a part to a PNG preview
34
+ cadcode parts <file> # list the parts a program defines
35
+ ```
36
+
37
+ | Option | Applies to | Meaning |
38
+ | --- | --- | --- |
39
+ | `-p, --part <name>` | all | Part to use. The default is the first one. |
40
+ | `-a, --all` | stl, png | Export every part, one file each. |
41
+ | `-s, --set <key=value>` | all | Override a parameter. Repeatable. Values are parsed as JSON, falling back to strings. |
42
+ | `-o, --out <path>` | stl, png | Output file, or a directory with `--all`. The default is `<name>.stl` or `<name>-<part>.stl` in the current directory. |
43
+ | `--view <name>` | png | `iso` (default), `iso-back`, `front`, `back`, `left`, `right`, `top`, `bottom`, or `sheet` for iso, front, right and top in one image. |
44
+ | `--size <WxH>` | png | Image size. The default is `800x600`. |
45
+ | `--port <n>` | dev | The default is `5173`. |
46
+ | `--host <addr>` | dev | The default is `127.0.0.1`. |
47
+ | `--open` | dev | Open the browser. |
48
+
49
+ ```bash
50
+ cadcode stl case.js --part box --set wall=2.4
51
+ cadcode stl case.js --all -o out/
52
+ cadcode png case.js --part assembly --view sheet --size 1600x1200
53
+ ```
54
+
55
+ ### The dev viewer
56
+
57
+ `cadcode dev` watches the program and every file it imports. On each save it rebuilds the model and swaps it into the page without touching the camera, so you keep your angle, position and zoom while you iterate. The camera is also remembered per file across browser refreshes and server restarts.
58
+
59
+ - **Rotate**: drag
60
+ - **Pan**: shift+drag or right-drag
61
+ - **Zoom**: mouse wheel
62
+ - **Fit the model to the view**: F, or the Fit button
63
+ - **Switch parts**: the part dropdown
64
+
65
+ Errors appear in the page and in the terminal, with the line in your file. The last good model stays on screen until you fix them. Whatever the program prints with `console.log` appears in the corner of the page.
66
+
67
+ ## Writing a program
68
+
69
+ A program is an ES module. Its default export is one of these:
70
+
71
+ - **A model**: a solid, or an array of solids. Arrays may nest.
72
+ - **An object of named parts**: each value is a model, or a function that returns one. Functions are only called for the part being shown or exported.
73
+ - **A function of `params`**: it returns either of the above.
74
+
75
+ Parameters you might want to change from the command line go in `export const params`. `--set` can only override a parameter declared there, so a typo is an error instead of being silently ignored.
76
+
77
+ ```js
78
+ import { cube, cylinder, hull, product, setResolution } from 'cadcode';
79
+
80
+ setResolution({ fn: 48 });
81
+
82
+ export const params = {
83
+ width: 40,
84
+ depth: 30,
85
+ height: 12,
86
+ wall: 2,
87
+ radius: 3,
88
+ };
89
+
90
+ export default (p) => {
91
+ const roundedBox = (w, d, h, r) =>
92
+ hull(product([r, w - r], [r, d - r]).map(([x, y]) => cylinder({ r, h }).translate([x, y, 0])));
93
+
94
+ const tray = () =>
95
+ roundedBox(p.width, p.depth, p.height, p.radius).subtract(
96
+ roundedBox(p.width - 2 * p.wall, p.depth - 2 * p.wall, p.height, p.radius - p.wall).translate([p.wall, p.wall, p.wall]),
97
+ );
98
+
99
+ return {
100
+ assembly: () => [tray().color('steelblue'), cube([10, 10, 4]).translate([5, 5, p.wall]).color('orange', 0.6)],
101
+ tray,
102
+ };
103
+ };
104
+ ```
105
+
106
+ The first part is the default one. The STL of a part merges all its solids into one body. In the viewer and in PNGs each solid keeps its own color and transparency, which is what assembly views are for.
107
+
108
+ A program can import its own helper files and npm packages like any module. TypeScript files work too.
109
+
110
+ [`examples/sensor-case.js`](examples/sensor-case.js) is a complete project: a snap-fit case for a board, a battery and a sensor, with `assembly`, `box`, `lid` and `print` parts.
111
+
112
+ ## API
113
+
114
+ Every shape is immutable: methods return a new shape. Units are whatever your slicer assumes, usually millimeters. Angles are in degrees.
115
+
116
+ Wherever a list of shapes is accepted, it may contain nested arrays and falsy values. That makes loops and conditionals read naturally:
117
+
118
+ ```js
119
+ difference(base, holes.map(([x, y]) => cylinder({ d: 3, h: 10 }).translate([x, y, -1])), withSlot && slot);
120
+ ```
121
+
122
+ ### 3D primitives
123
+
124
+ | Function | Notes |
125
+ | --- | --- |
126
+ | `cube(size, { center })` | `size` is a number or `[x, y, z]`. A corner sits at the origin unless `center: true`. |
127
+ | `cylinder({ h, r \| d, r1, r2, d1, d2, center, fn })` | Stands on XY along +Z. Give `r1`/`r2` for a cone. |
128
+ | `sphere(r)` or `sphere({ r \| d, fn })` | Centered on the origin. |
129
+ | `polyhedron(points, faces)` | Faces list point indices clockwise when seen from outside, as in OpenSCAD. |
130
+
131
+ ### 2D primitives
132
+
133
+ | Function | Notes |
134
+ | --- | --- |
135
+ | `square(size, { center })` | `size` is a number or `[x, y]`. |
136
+ | `circle(r)` or `circle({ r \| d, fn })` | |
137
+ | `polygon(points, paths?)` | Paths inside other paths become holes. |
138
+
139
+ ### Solid methods
140
+
141
+ | Method | Notes |
142
+ | --- | --- |
143
+ | `.translate([x, y, z])` | |
144
+ | `.rotate([x, y, z])` | Around X, then Y, then Z, like OpenSCAD. `.rotate(a)` turns around Z, and `.rotate(a, [x, y, z])` around any axis. |
145
+ | `.scale(s)` or `.scale([x, y, z])` | |
146
+ | `.mirror([nx, ny, nz])` | Across the plane through the origin with this normal. |
147
+ | `.transform(matrix)` | A column-major 4×4 matrix, as 16 numbers. |
148
+ | `.color(name, alpha?)` | A CSS color name, `#rrggbb`, or `[r, g, b, a]` in 0..1. Colors survive booleans. |
149
+ | `.union(...shapes)` | |
150
+ | `.subtract(...shapes)` | |
151
+ | `.intersect(...shapes)` | |
152
+ | `.hull(...shapes)` | Convex hull of this shape and the others. |
153
+ | `.slice(z)` | The 2D cross-section at height `z`. |
154
+ | `.project()` | The 2D outline seen from above. |
155
+ | `.volume()`, `.surfaceArea()`, `.bounds()`, `.isEmpty()` | `bounds()` returns `{ min: [x, y, z], max: [x, y, z] }`. |
156
+
157
+ ### 2D shape methods
158
+
159
+ | Method | Notes |
160
+ | --- | --- |
161
+ | `.translate([x, y])`, `.rotate(a)`, `.scale(s)`, `.mirror([nx, ny])` | |
162
+ | `.offset(r)` | Grow (or shrink, when negative) with rounded corners. `.offset({ delta })` keeps corners sharp, and `.offset({ delta, chamfer: true })` cuts them. |
163
+ | `.union()`, `.subtract()`, `.intersect()`, `.hull()` | Same as for solids. |
164
+ | `.extrude(h, { center, twist, scale, divisions })` | Extrudes along +Z, like `linear_extrude`. |
165
+ | `.revolve({ angle, fn })` | Revolves the shape around the Y axis into a solid around Z, like `rotate_extrude`. |
166
+ | `.area()`, `.bounds()`, `.isEmpty()`, `.toPolygons()` | |
167
+
168
+ ### Functions
169
+
170
+ These take any mix of shapes and nested arrays, so OpenSCAD's nesting style works as well as method chaining:
171
+
172
+ | Function | Equivalent |
173
+ | --- | --- |
174
+ | `union(...shapes)` | |
175
+ | `difference(base, ...cutters)` | `base.subtract(...cutters)` |
176
+ | `intersection(...shapes)` | |
177
+ | `hull(...shapes)` | |
178
+ | `translate(v, ...shapes)` | `union(...shapes).translate(v)` |
179
+ | `rotate(a, ...shapes)` | `union(...shapes).rotate(a)` |
180
+ | `scale(v, ...shapes)` | `union(...shapes).scale(v)` |
181
+ | `mirror(n, ...shapes)` | `union(...shapes).mirror(n)` |
182
+ | `color(c, alpha?, ...shapes)` | `union(...shapes).color(c, alpha)` |
183
+ | `offset(r, ...shapes)` | `union(...shapes).offset(r)` |
184
+ | `linearExtrude(h, options, ...shapes)` | `union(...shapes).extrude(h, options)` |
185
+
186
+ ### Helpers
187
+
188
+ | Function | Notes |
189
+ | --- | --- |
190
+ | `range(start, end, step = 1)` | Includes `end`, like OpenSCAD's `[start : step : end]`. |
191
+ | `product(...lists)` | Every combination, like `for (x = xs, y = ys)`: `product([1, 2], ['a', 'b'])` gives `[[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']]`. |
192
+ | `setResolution({ fn, fa, fs })` | Default segment count for curves, with the same rules as OpenSCAD's `$fn`, `$fa` and `$fs`. Any primitive also takes its own `fn`. |
193
+
194
+ ## Porting from OpenSCAD
195
+
196
+ | OpenSCAD | cadcode |
197
+ | --- | --- |
198
+ | `$fn = 40;` | `setResolution({ fn: 40 });` |
199
+ | `cylinder(d = 5, h = 2, center = true);` | `cylinder({ d: 5, h: 2, center: true })` |
200
+ | `translate([x, y, 0]) rotate([0, 90, 0]) cube(s);` | `cube(s).rotate([0, 90, 0]).translate([x, y, 0])` |
201
+ | `difference() { a(); b(); c(); }` | `difference(a(), b(), c())` |
202
+ | `for (x = xs, y = ys) translate([x, y]) c();` | `product(xs, ys).map(([x, y]) => c().translate([x, y]))` |
203
+ | `for (i = [0 : n - 1])` | `range(0, n - 1).map((i) => ...)` |
204
+ | `linear_extrude(h) offset(r = 1) square(s, center = true);` | `square(s, { center: true }).offset(1).extrude(h)` |
205
+ | `color("orange", 0.35) lid();` | `lid().color('orange', 0.35)` |
206
+ | `module box() { ... }` | `const box = () => ...` |
207
+ | `part = "box";` with `-D 'part="box"'` | Named parts and `--part box` |
208
+ | `wall = 2;` with `-D wall=2.4` | `export const params = { wall: 2 }` and `--set wall=2.4` |
209
+ | `echo(...)` | `console.log(...)` |
210
+
211
+ When chaining, read OpenSCAD's transforms from the innermost one out. `translate(t) rotate(r) child` becomes `child.rotate(r).translate(t)`.
212
+
213
+ ## License
214
+
215
+ MIT
@@ -0,0 +1,51 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>cadcode</title>
7
+ <link rel="stylesheet" href="/viewer.css" />
8
+ <script type="importmap">
9
+ {
10
+ "imports": {
11
+ "three": "/vendor/three/build/three.module.js",
12
+ "three/addons/": "/vendor/three/examples/jsm/"
13
+ }
14
+ }
15
+ </script>
16
+ </head>
17
+ <body>
18
+ <canvas id="view"></canvas>
19
+
20
+ <div class="top">
21
+ <header class="bar">
22
+ <span class="brand">cadcode</span>
23
+ <span id="file" class="file"
24
+ ><span class="dir"></span><span class="name"></span
25
+ ></span>
26
+ <label class="part">
27
+ <span>part</span>
28
+ <select id="part"></select>
29
+ </label>
30
+ <span id="status" class="status" data-state="connecting"
31
+ >connecting</span
32
+ >
33
+ <button id="fit" type="button" title="Fit model to view (F)">
34
+ Fit
35
+ </button>
36
+ </header>
37
+
38
+ <section id="logs" class="logs" hidden></section>
39
+ </div>
40
+
41
+ <section id="error" class="error" hidden>
42
+ <pre id="error-text"></pre>
43
+ </section>
44
+
45
+ <footer class="hint">
46
+ drag: rotate · shift+drag or right-drag: pan · wheel: zoom · F: fit
47
+ </footer>
48
+
49
+ <script type="module" src="/viewer.js"></script>
50
+ </body>
51
+ </html>
@@ -0,0 +1,216 @@
1
+ :root {
2
+ --bg: #eef0f3;
3
+ --panel: rgba(255, 255, 255, 0.86);
4
+ --text: #1d2228;
5
+ --muted: #6b7480;
6
+ --border: rgba(20, 30, 40, 0.12);
7
+ --ok: #2f9e5b;
8
+ --busy: #c98a12;
9
+ --bad: #c8372d;
10
+ font: 13px/1.4 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
11
+ color: var(--text);
12
+ }
13
+
14
+ * {
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ html,
19
+ body {
20
+ margin: 0;
21
+ height: 100%;
22
+ overflow: hidden;
23
+ background: var(--bg);
24
+ }
25
+
26
+ #view {
27
+ position: fixed;
28
+ inset: 0;
29
+ width: 100%;
30
+ height: 100%;
31
+ display: block;
32
+ }
33
+
34
+ /* Toolbar on the left, program output on the right; they share one row so they never overlap. */
35
+ .top {
36
+ position: fixed;
37
+ top: 12px;
38
+ left: 12px;
39
+ right: 12px;
40
+ display: flex;
41
+ justify-content: space-between;
42
+ align-items: flex-start;
43
+ gap: 12px;
44
+ pointer-events: none;
45
+ }
46
+
47
+ .top > * {
48
+ pointer-events: auto;
49
+ }
50
+
51
+ .bar {
52
+ flex: 0 1 auto;
53
+ min-width: 0;
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 12px;
57
+ padding: 6px 8px 6px 12px;
58
+ background: var(--panel);
59
+ backdrop-filter: blur(8px);
60
+ border: 1px solid var(--border);
61
+ border-radius: 10px;
62
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
63
+ }
64
+
65
+ .bar > :not(.file) {
66
+ flex-shrink: 0;
67
+ }
68
+
69
+ .brand {
70
+ font-weight: 650;
71
+ letter-spacing: 0.02em;
72
+ }
73
+
74
+ .file {
75
+ display: flex;
76
+ min-width: 0;
77
+ color: var(--muted);
78
+ font-family: ui-monospace, 'SF Mono', Menlo, monospace;
79
+ font-size: 12px;
80
+ white-space: nowrap;
81
+ }
82
+
83
+ /* A long path gives up its directory first; the file name always stays. */
84
+ .file .dir {
85
+ min-width: 0;
86
+ overflow: hidden;
87
+ text-overflow: ellipsis;
88
+ }
89
+
90
+ .file .name {
91
+ flex-shrink: 0;
92
+ }
93
+
94
+ .part {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 6px;
98
+ color: var(--muted);
99
+ }
100
+
101
+ select,
102
+ button {
103
+ font: inherit;
104
+ color: var(--text);
105
+ background: #fff;
106
+ border: 1px solid var(--border);
107
+ border-radius: 6px;
108
+ padding: 3px 8px;
109
+ }
110
+
111
+ button {
112
+ cursor: pointer;
113
+ }
114
+
115
+ button:hover,
116
+ select:hover {
117
+ border-color: rgba(20, 30, 40, 0.28);
118
+ }
119
+
120
+ .status {
121
+ display: inline-flex;
122
+ align-items: center;
123
+ gap: 6px;
124
+ color: var(--muted);
125
+ white-space: nowrap;
126
+ font-variant-numeric: tabular-nums;
127
+ }
128
+
129
+ .status::before {
130
+ content: '';
131
+ width: 8px;
132
+ height: 8px;
133
+ border-radius: 50%;
134
+ background: var(--muted);
135
+ }
136
+
137
+ .status[data-state='ready']::before {
138
+ background: var(--ok);
139
+ }
140
+
141
+ .status[data-state='building']::before,
142
+ .status[data-state='evaluating']::before {
143
+ background: var(--busy);
144
+ }
145
+
146
+ .status[data-state='error']::before,
147
+ .status[data-state='disconnected']::before {
148
+ background: var(--bad);
149
+ }
150
+
151
+ .error {
152
+ position: fixed;
153
+ left: 12px;
154
+ right: 12px;
155
+ bottom: 40px;
156
+ max-height: 45vh;
157
+ overflow: auto;
158
+ background: #fff6f5;
159
+ border: 1px solid rgba(200, 55, 45, 0.35);
160
+ border-left: 4px solid var(--bad);
161
+ border-radius: 8px;
162
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
163
+ }
164
+
165
+ .error pre {
166
+ margin: 0;
167
+ padding: 12px 14px;
168
+ font: 12px/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
169
+ white-space: pre-wrap;
170
+ color: #7a1d16;
171
+ }
172
+
173
+ .logs {
174
+ flex: 0 1 auto;
175
+ min-width: 0;
176
+ max-width: min(420px, 40vw);
177
+ max-height: 40vh;
178
+ overflow: auto;
179
+ padding: 8px 10px;
180
+ background: var(--panel);
181
+ border: 1px solid var(--border);
182
+ border-radius: 10px;
183
+ font: 12px/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
184
+ white-space: pre-wrap;
185
+ }
186
+
187
+ .logs .log-warn {
188
+ color: var(--busy);
189
+ }
190
+
191
+ .logs .log-error {
192
+ color: var(--bad);
193
+ }
194
+
195
+ .hint {
196
+ position: fixed;
197
+ left: 12px;
198
+ bottom: 12px;
199
+ color: var(--muted);
200
+ font-size: 12px;
201
+ pointer-events: none;
202
+ }
203
+
204
+ @media (max-width: 720px) {
205
+ .file,
206
+ .hint {
207
+ display: none;
208
+ }
209
+ .logs {
210
+ position: fixed;
211
+ left: 12px;
212
+ right: 12px;
213
+ bottom: 40px;
214
+ max-width: none;
215
+ }
216
+ }